This repository was archived by the owner on Oct 7, 2020. It is now read-only.
-
Notifications
You must be signed in to change notification settings - Fork 206
/
Copy pathPluginUtils.hs
298 lines (254 loc) · 10.4 KB
/
PluginUtils.hs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeOperators #-}
module Haskell.Ide.Engine.PluginUtils
(
mapEithers
, pluginGetFile
, makeDiffResult
, WithDeletions(..)
, makeAdditiveDiffResult
, diffText
, diffText'
, srcSpan2Range
, srcSpan2Loc
, unpackRealSrcSpan
, reverseMapFile
, extractRange
, fullRange
, fileInfo
, realSrcSpan2Range
, canonicalizeUri
, newRangeToOld
, oldRangeToNew
, newPosToOld
, oldPosToNew
, unPos
, toPos
, clientSupportsDocumentChanges
, readVFS
, getRangeFromVFS
, rangeLinesFromVfs
, gcatches
, ErrorHandler(..)
) where
import Control.Monad.IO.Class
import Control.Monad.Reader
import Control.Monad.Trans.Except
import Data.Algorithm.Diff
import Data.Algorithm.DiffOutput
import qualified Data.HashMap.Strict as H
import Data.Monoid
import qualified Data.Text as T
import qualified Data.Text.IO as T
import Data.Maybe
import FastString
import Haskell.Ide.Engine.PluginsIdeMonads
import Haskell.Ide.Engine.GhcModuleCache
import Haskell.Ide.Engine.MonadFunctions
import Haskell.Ide.Engine.ArtifactMap
import Language.Haskell.LSP.VFS
import Language.Haskell.LSP.Types.Capabilities
import qualified Language.Haskell.LSP.Types as J
import Prelude hiding (log)
import SrcLoc (SrcSpan(..), RealSrcSpan(..))
import Exception
import System.Directory
import System.FilePath
-- ---------------------------------------------------------------------
canonicalizeUri :: MonadIO m => Uri -> m Uri
canonicalizeUri uri =
case uriToFilePath uri of
Nothing -> return uri
Just fp -> do
fp' <- liftIO $ canonicalizePath fp
return $ filePathToUri fp'
newRangeToOld :: CachedInfo -> Range -> Maybe Range
newRangeToOld info (Range start end) = do
start' <- newPosToOld info start
end' <- newPosToOld info end
return (Range start' end')
oldRangeToNew :: CachedInfo -> Range -> Maybe Range
oldRangeToNew info (Range start end) = do
start' <- oldPosToNew info start
end' <- oldPosToNew info end
return (Range start' end')
getRealSrcSpan :: SrcSpan -> Either T.Text RealSrcSpan
getRealSrcSpan (RealSrcSpan r) = Right r
getRealSrcSpan (UnhelpfulSpan x) = Left $ T.pack $ unpackFS x
realSrcSpan2Range :: RealSrcSpan -> Range
realSrcSpan2Range = uncurry Range . unpackRealSrcSpan
srcSpan2Range :: SrcSpan -> Either T.Text Range
srcSpan2Range spn =
realSrcSpan2Range <$> getRealSrcSpan spn
reverseMapFile :: MonadIO m => (FilePath -> FilePath) -> FilePath -> m FilePath
reverseMapFile rfm fp = do
fp' <- liftIO $ canonicalizePath fp
debugm $ "reverseMapFile: mapped file is " ++ fp'
let orig = rfm fp'
debugm $ "reverseMapFile: original is " ++ orig
orig' <- liftIO $ canonicalizePath orig
debugm $ "reverseMapFile: Canonicalized original is " ++ orig
return orig'
srcSpan2Loc :: (MonadIO m) => (FilePath -> FilePath) -> SrcSpan -> m (Either T.Text Location)
srcSpan2Loc revMapp spn = runExceptT $ do
let
foo :: (Monad m) => Either T.Text RealSrcSpan -> ExceptT T.Text m RealSrcSpan
foo (Left e) = throwE e
foo (Right v) = pure v
rspan <- foo $ getRealSrcSpan spn
let fp = unpackFS $ srcSpanFile rspan
debugm $ "srcSpan2Loc: mapped file is " ++ fp
file <- reverseMapFile revMapp fp
debugm $ "srcSpan2Loc: Original file is " ++ file
return $ Location (filePathToUri file) (realSrcSpan2Range rspan)
-- ---------------------------------------------------------------------
-- | Helper function that extracts a filepath from a Uri if the Uri
-- is well formed (i.e. begins with a file:// )
-- fails with an ideError otherwise
pluginGetFile
:: Monad m
=> T.Text -> Uri -> (FilePath -> m (IdeResult a)) -> m (IdeResult a)
pluginGetFile name uri f =
case uriToFilePath uri of
Just file -> f file
Nothing -> ideError PluginError $ name <> "Couldn't resolve uri" <> getUri uri
-- ---------------------------------------------------------------------
-- courtesy of http://stackoverflow.com/questions/19891061/mapeithers-function-in-haskell
mapEithers :: (a -> Either b c) -> [a] -> Either b [c]
mapEithers f (x:xs) = case mapEithers f xs of
Left err -> Left err
Right ys -> case f x of
Left err -> Left err
Right y -> Right (y:ys)
mapEithers _ _ = Right []
-- ---------------------------------------------------------------------
data WithDeletions = IncludeDeletions | SkipDeletions
deriving Eq
-- | Generate a 'WorkspaceEdit' value from an original file and text to replace it.
makeDiffResult :: FilePath -> T.Text -> (FilePath -> FilePath) -> IdeM WorkspaceEdit
makeDiffResult orig new fileMap = do
origText <- liftIO $ T.readFile orig
let fp' = fileMap orig
fp <- liftIO $ makeAbsolute fp'
diffText (filePathToUri fp,origText) new IncludeDeletions
-- | A version of 'makeDiffResult' that has does not insert any deletions
makeAdditiveDiffResult :: FilePath -> T.Text -> (FilePath -> FilePath) -> IdeM WorkspaceEdit
makeAdditiveDiffResult orig new fileMap = do
origText <- liftIO $ T.readFile orig
let fp' = fileMap orig
fp <- liftIO $ makeAbsolute fp'
diffText (filePathToUri fp,origText) new SkipDeletions
-- | Generate a 'WorkspaceEdit' value from a pair of source Text
-- TODO: Doesn't seem to work with 'editHpackPackage'?
diffText :: (Uri,T.Text) -> T.Text -> WithDeletions -> IdeM WorkspaceEdit
diffText old new withDeletions = do
supports <- clientSupportsDocumentChanges
return $ diffText' supports old new withDeletions
-- | A pure version of 'diffText' for testing
diffText' :: Bool -> (Uri,T.Text) -> T.Text -> WithDeletions -> WorkspaceEdit
diffText' supports (f,fText) f2Text withDeletions =
if supports
then WorkspaceEdit Nothing (Just docChanges)
else WorkspaceEdit (Just h) Nothing
where
d = getGroupedDiff (lines $ T.unpack fText) (lines $ T.unpack f2Text)
diffOps = filter (\x -> (withDeletions == IncludeDeletions) || not (isDeletion x))
(diffToLineRanges d)
isDeletion (Deletion _ _) = True
isDeletion _ = False
r = map diffOperationToTextEdit diffOps
diff = J.List r
h = H.singleton f diff
docChanges = J.List [docEdit]
docEdit = J.TextDocumentEdit (J.VersionedTextDocumentIdentifier f (Just 0)) diff
diffOperationToTextEdit :: DiffOperation LineRange -> J.TextEdit
diffOperationToTextEdit (Change fm to) = J.TextEdit range nt
where
range = calcRange fm
nt = T.pack $ init $ unlines $ lrContents to
{-
In order to replace everything including newline characters,
the end range should extend below the last line. From the specification:
"If you want to specify a range that contains a line including
the line ending character(s) then use an end position denoting
the start of the next line"
-}
diffOperationToTextEdit (Deletion (LineRange (sl, el) _) _) = J.TextEdit range ""
where
range = J.Range (J.Position (sl - 1) 0)
(J.Position el 0)
diffOperationToTextEdit (Addition fm l) = J.TextEdit range nt
-- fm has a range wrt to the changed file, which starts in the current file at l
-- So the range has to be shifted to start at l
where
range = J.Range (J.Position (l' - 1) 0)
(J.Position (l' - 1) 0)
l' = max l sl -- Needed to add at the end of the file
sl = fst $ lrNumbers fm
nt = T.pack $ unlines $ lrContents fm
calcRange fm = J.Range s e
where
sl = fst $ lrNumbers fm
sc = 0
s = J.Position (sl - 1) sc -- Note: zero-based lines
el = snd $ lrNumbers fm
ec = length $ last $ lrContents fm
e = J.Position (el - 1) ec -- Note: zero-based lines
-- ---------------------------------------------------------------------
extractRange :: Range -> T.Text -> T.Text
extractRange (Range (Position sl _) (Position el _)) s = newS
where focusLines = take (el-sl+1) $ drop sl $ T.lines s
newS = T.unlines focusLines
-- | Gets the range that covers the entire text
fullRange :: T.Text -> Range
fullRange s = Range startPos endPos
where startPos = Position 0 0
endPos = Position lastLine 0
{-
In order to replace everything including newline characters,
the end range should extend below the last line. From the specification:
"If you want to specify a range that contains a line including
the line ending character(s) then use an end position denoting
the start of the next line"
-}
lastLine = length $ T.lines s
-- ---------------------------------------------------------------------
-- | Returns the directory and file name
fileInfo :: T.Text -> (FilePath,FilePath)
fileInfo tfileName =
let sfileName = T.unpack tfileName
dir = takeDirectory sfileName
in (dir,sfileName)
-- ---------------------------------------------------------------------
clientSupportsDocumentChanges :: IdeM Bool
clientSupportsDocumentChanges = do
ClientCapabilities mwCaps _ _ _ <- getClientCapabilities
let supports = do
wCaps <- mwCaps
WorkspaceEditClientCapabilities mDc <- _workspaceEdit wCaps
mDc
return $ fromMaybe False supports
-- ---------------------------------------------------------------------
readVFS :: (MonadIde m, MonadIO m) => Uri -> m (Maybe T.Text)
readVFS uri = do
mvf <- getVirtualFile uri
case mvf of
Just vf -> return $ Just (virtualFileText vf)
Nothing -> return Nothing
getRangeFromVFS :: (MonadIde m, MonadIO m) => Uri -> Range -> m (Maybe T.Text)
getRangeFromVFS uri rg = do
mvf <- getVirtualFile uri
case mvf of
Just vfs -> return $ Just $ rangeLinesFromVfs vfs rg
Nothing -> return Nothing
-- Error catching utilities
data ErrorHandler m a = forall e . Exception e => ErrorHandler (e -> m a)
gcatches :: forall m a . (ExceptionMonad m) => m a -> [ErrorHandler m a] -> m a
gcatches act handlers = gcatch act h
where
h :: SomeException -> m a
h e = foldr (\(ErrorHandler hand) me -> maybe me hand (fromException e)) (liftIO $ throw e) handlers