on master help sign in
Use haskell-lsp’s builtin VFS in "damlc ide" (#1489)

* Use haskell-lsp’s builtin VFS in "damlc ide" haskell-lsp has a builtin VFS that it updates automatically on the corresponding requests. This PR removes our own VFS implementation and uses that builtin VFS in "damlc ide". To allow the use of functions like setBufferModified (we use that heavily in daml-ghc-shake-test-ci) without having to spawn an LSP server, we also add a fallback where we spin up our own LSP implementation.

commit7c3213c3c568409095c02756af9328fc342ec576 authorMoritz Kiefer <moritz.kiefer@purelyfunctional.org> dateMon 3 Jun 2019, 16:19 (7 years ago) parent85379f5
8 files changed+104−66
BUILD.bazel+1 src/Development/IDE/Functions/Compile.hs+11−8 src/Development/IDE/State/FileStore.hs+70−47 src/Development/IDE/State/Rules.hs+1−1 src/Development/IDE/State/Service.hs+3−2 src/Development/IDE/State/Shake.hs+13−2 src/Development/IDE/Types/Diagnostics.hs+2−6 test/Demo.hs+3
BUILD.bazel+1
@@ -20,6 +20,7 @@ depends = [
2020 "haskell-lsp-types",
2121 "mtl",
2222 "pretty",
23+ "rope-utf16-splay",
2324 "safe-exceptions",
2425 "sorted-list",
2526 "shake",
src/Development/IDE/Functions/Compile.hs+11−8
@@ -55,7 +55,6 @@ import Data.List.Extra
5555import Data.Maybe
5656import Data.Tuple.Extra
5757import qualified Data.Map.Strict as Map
58−import Data.Time
5958import Development.IDE.Types.SpanInfo
6059import GHC.Generics (Generic)
6160import System.FilePath
@@ -107,7 +106,7 @@ parseModule
107106 :: IdeOptions
108107 -> HscEnv
109108 -> FilePath
110− -> (UTCTime, SB.StringBuffer)
109+ -> SB.StringBuffer
111110 -> IO ([FileDiagnostic], Maybe ParsedModule)
112111parseModule opt@IdeOptions{..} packageState file =
113112 fmap (either (, Nothing) (second Just)) . Ex.runExceptT .
@@ -325,11 +324,11 @@ loadModuleHome tmr = modifySession $ \e ->
325324getModSummaryFromBuffer
326325 :: GhcMonad m
327326 => FilePath
328− -> (SB.StringBuffer, UTCTime)
327+ -> SB.StringBuffer
329328 -> DynFlags
330329 -> GHC.ParsedSource
331330 -> Ex.ExceptT [FileDiagnostic] m ModSummary
332−getModSummaryFromBuffer fp (contents, fileDate) dflags parsed = do
331+getModSummaryFromBuffer fp contents dflags parsed = do
333332 (modName, imports) <- FindImports.getImportsParsed dflags parsed
334333
335334 let modLoc = ModLocation
@@ -347,7 +346,11 @@ getModSummaryFromBuffer fp (contents, fileDate) dflags parsed = do
347346 return $ ModSummary
348347 { ms_mod = mkModule (fsToUnitId unitId) modName
349348 , ms_location = modLoc
350− , ms_hs_date = fileDate
349+ , ms_hs_date = error "Rules should not depend on ms_hs_date"
350+ -- ^ When we are working with a virtual file we do not have a file date.
351+ -- To avoid silent issues where something is not processed because the date
352+ -- has not changed, we make sure that things blow up if they depend on the
353+ -- date.
351354 , ms_textual_imps = imports
352355 , ms_hspp_file = fp
353356 , ms_hspp_opts = dflags
@@ -370,9 +373,9 @@ parseFileContents
370373 :: GhcMonad m
371374 => (GHC.ParsedSource -> ([(GHC.SrcSpan, String)], GHC.ParsedSource))
372375 -> FilePath -- ^ the filename (for source locations)
373− -> (UTCTime, SB.StringBuffer) -- ^ Haskell module source text (full Unicode is supported)
376+ -> SB.StringBuffer -- ^ Haskell module source text (full Unicode is supported)
374377 -> Ex.ExceptT [FileDiagnostic] m ([FileDiagnostic], ParsedModule)
375−parseFileContents preprocessor filename (time, contents) = do
378+parseFileContents preprocessor filename contents = do
376379 let loc = mkRealSrcLoc (mkFastString filename) 1 1
377380 dflags <- parsePragmasIntoDynFlags filename contents
378381
@@ -422,7 +425,7 @@ parseFileContents preprocessor filename (time, contents) = do
422425 -- Ok, we got here. It's safe to continue.
423426 let (errs, parsed) = preprocessor rdr_module
424427 unless (null errs) $ Ex.throwE $ mkErrors dflags errs
425− ms <- getModSummaryFromBuffer filename (contents, time) dflags parsed
428+ ms <- getModSummaryFromBuffer filename contents dflags parsed
426429 let pm =
427430 ParsedModule {
428431 pm_mod_summary = ms
src/Development/IDE/State/FileStore.hs+70−47
@@ -6,7 +6,10 @@
66module Development.IDE.State.FileStore(
77 getFileExists, getFileContents,
88 setBufferModified,
9− fileStoreRules
9+ fileStoreRules,
10+ VFSHandle(..),
11+ makeVFSHandle,
12+ makeLSPVFSHandle,
1013 ) where
1114
1215
@@ -14,7 +17,9 @@ module Development.IDE.State.FileStore(
1417import StringBuffer
1518import Development.IDE.Orphans()
1619
20+import Control.Concurrent.Extra
1721import qualified Data.Map.Strict as Map
22+import Data.Maybe
1823import qualified Data.Text as T
1924import Data.Time.Clock
2025import Control.Monad.Extra
@@ -22,28 +27,54 @@ import qualified System.Directory as Dir
2227import Development.Shake
2328import Development.Shake.Classes
2429import Development.IDE.State.Shake
25−import Control.Concurrent.Extra
2630import Control.Exception
2731import GHC.Generics
2832import System.IO.Error
2933import qualified Data.ByteString.Char8 as BS
3034import qualified StringBuffer as SB
3135import Development.IDE.Types.Diagnostics
36+import qualified Data.Rope.UTF16 as Rope
3237import Data.Time
3338
34−
35−-- This module stores the changed files in memory, and answers file system questions
36−-- from either the memory changes OR the file system itself
37−
38−type DirtyFiles = Map.Map FilePath (UTCTime, StringBuffer) -- when it was modified, it's current value
39−
40−-- Store the DirtyFiles globally, so we can get at it through setBufferModified
41−newtype GlobalDirtyFiles = GlobalDirtyFiles (Var DirtyFiles)
42−instance IsIdeGlobal GlobalDirtyFiles
39+import Language.Haskell.LSP.Core
40+import Language.Haskell.LSP.VFS
41+
42+-- | haskell-lsp manages the VFS internally and automatically so we cannot use
43+-- the builtin VFS without spawning up an LSP server. To be able to test things
44+-- like `setBufferModified` we abstract over the VFS implementation.
45+data VFSHandle = VFSHandle
46+ { getVirtualFile :: Uri -> IO (Maybe VirtualFile)
47+ , setVirtualFileContents :: Uri -> T.Text -> IO ()
48+ , removeVirtualFile :: Uri -> IO ()
49+ }
50+
51+instance IsIdeGlobal VFSHandle
52+
53+makeVFSHandle :: IO VFSHandle
54+makeVFSHandle = do
55+ vfsVar <- newVar (1, Map.empty)
56+ pure VFSHandle
57+ { getVirtualFile = \uri -> do
58+ (_nextVersion, vfs) <- readVar vfsVar
59+ pure $ Map.lookup uri vfs
60+ , setVirtualFileContents = \uri content ->
61+ modifyVar_ vfsVar $ \(nextVersion, vfs) ->
62+ pure (nextVersion + 1, Map.insert uri (VirtualFile nextVersion (Rope.fromText content) Nothing) vfs)
63+ , removeVirtualFile = \uri -> modifyVar_ vfsVar $ \(nextVersion, vfs) -> pure (nextVersion, Map.delete uri vfs)
64+ }
65+
66+makeLSPVFSHandle :: LspFuncs c -> VFSHandle
67+makeLSPVFSHandle lspFuncs = VFSHandle
68+ { getVirtualFile = getVirtualFileFunc lspFuncs
69+ , setVirtualFileContents = \_ _ -> pure ()
70+ -- ^ Handled internally by haskell-lsp.
71+ , removeVirtualFile = \_ -> pure ()
72+ -- ^ Handled internally by haskell-lsp.
73+ }
4374
4475
4576-- | Get the contents of a file, either dirty (if the buffer is modified) or from disk.
46−type instance RuleResult GetFileContents = (UTCTime, StringBuffer)
77+type instance RuleResult GetFileContents = (FileVersion, StringBuffer)
4778
4879-- | Does the file exist.
4980type instance RuleResult GetFileExists = Bool
@@ -60,12 +91,12 @@ instance Hashable GetFileContents
6091instance NFData GetFileContents
6192
6293
63−getFileExistsRule :: Var DirtyFiles -> Rules ()
64−getFileExistsRule dirty =
94+getFileExistsRule :: VFSHandle -> Rules ()
95+getFileExistsRule vfs =
6596 defineEarlyCutoff $ \GetFileExists file -> do
6697 alwaysRerun
6798 res <- liftIO $ handle (\(_ :: IOException) -> return False) $
68− (Map.member file <$> readVar dirty) ||^
99+ (isJust <$> getVirtualFile vfs (filePathToUri file)) ||^
69100 Dir.doesFileExist file
70101 return (Just $ if res then BS.singleton '1' else BS.empty, ([], Just res))
71102
@@ -73,36 +104,36 @@ getFileExistsRule dirty =
73104showTimePrecise :: UTCTime -> String
74105showTimePrecise UTCTime{..} = show (toModifiedJulianDay utctDay, diffTimeToPicoseconds utctDayTime)
75106
76−getModificationTimeRule :: Var DirtyFiles -> Rules ()
77−getModificationTimeRule dirty =
107+getModificationTimeRule :: VFSHandle -> Rules ()
108+getModificationTimeRule vfs =
78109 defineEarlyCutoff $ \GetModificationTime file -> do
79− let wrap time = (Just $ BS.pack $ showTimePrecise time, ([], Just time))
110+ let wrap time = (Just $ BS.pack $ showTimePrecise time, ([], Just $ ModificationTime time))
80111 alwaysRerun
81− mp <- liftIO $ readVar dirty
82− case Map.lookup file mp of
83− Just (time, _) -> return $ wrap time
112+ mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri file
113+ case mbVirtual of
114+ Just (VirtualFile ver _ _) -> pure (Just $ BS.pack $ show ver, ([], Just $ VFSVersion ver))
84115 Nothing -> liftIO $ fmap wrap (Dir.getModificationTime file) `catch` \(e :: IOException) -> do
85116 let err | isDoesNotExistError e = "File does not exist: " ++ file
86117 | otherwise = "IO error while reading " ++ file ++ ", " ++ displayException e
87118 return (Nothing, ([ideErrorText file $ T.pack err], Nothing))
88119
89120
90−getFileContentsRule :: Var DirtyFiles -> Rules ()
91−getFileContentsRule dirty =
121+getFileContentsRule :: VFSHandle -> Rules ()
122+getFileContentsRule vfs =
92123 define $ \GetFileContents file -> do
93124 -- need to depend on modification time to introduce a dependency with Cutoff
94125 time <- use_ GetModificationTime file
95126 res <- liftIO $ ideTryIOException file $ do
96− mp <- readVar dirty
97− case Map.lookup file mp of
98− Just (_, contents) -> return contents
127+ mbVirtual <- getVirtualFile vfs $ filePathToUri file
128+ case mbVirtual of
129+ Just (VirtualFile _ rope _) -> return $ textToStringBuffer $ Rope.toText rope
99130 Nothing -> hGetStringBuffer file
100131 case res of
101132 Left err -> return ([err], Nothing)
102133 Right contents -> return ([], Just (time, contents))
103134
104135
105−getFileContents :: FilePath -> Action (UTCTime, StringBuffer)
136+getFileContents :: FilePath -> Action (FileVersion, StringBuffer)
106137getFileContents = use_ GetFileContents
107138
108139getFileExists :: FilePath -> Action Bool
@@ -113,29 +144,21 @@ getFileExists =
113144 use_ GetFileExists
114145
115146
116−fileStoreRules :: Rules ()
117−fileStoreRules = do
118− dirty <- liftIO $ newVar Map.empty
119− addIdeGlobal $ GlobalDirtyFiles dirty
120− getModificationTimeRule dirty
121− getFileContentsRule dirty
122− getFileExistsRule dirty
123−
124−
125−strictPair :: a -> b -> (a, b)
126−strictPair !a !b = (a,b)
147+fileStoreRules :: VFSHandle -> Rules ()
148+fileStoreRules vfs = do
149+ addIdeGlobal vfs
150+ getModificationTimeRule vfs
151+ getFileContentsRule vfs
152+ getFileExistsRule vfs
127153
128154
129155-- | Notify the compiler service of a modified buffer
130−setBufferModified :: IdeState -> FilePath -> (Maybe T.Text, UTCTime) -> IO ()
131−setBufferModified state absFile (mcontents, !time) = do
132− GlobalDirtyFiles envDirtyFiles <- getIdeGlobalState state
133− -- update vars synchronously
134− modifyVar_ envDirtyFiles $ evaluate . case mcontents of
135− Nothing -> Map.delete absFile
136− Just contents -> Map.insert absFile $ strictPair time $ textToStringBuffer contents
137−
138− -- run shake to update results regarding the files of interest
156+setBufferModified :: IdeState -> FilePath -> Maybe T.Text -> IO ()
157+setBufferModified state absFile mbContents = do
158+ VFSHandle{..} <- getIdeGlobalState state
159+ case mbContents of
160+ Nothing -> removeVirtualFile (filePathToUri absFile)
161+ Just contents -> setVirtualFileContents (filePathToUri absFile) contents
139162 void $ shakeRun state []
140163
141164
src/Development/IDE/State/Rules.hs+1−1
@@ -164,7 +164,7 @@ data Priority
164164getParsedModuleRule :: Rules ()
165165getParsedModuleRule =
166166 define $ \GetParsedModule file -> do
167− contents <- getFileContents file
167+ (_, contents) <- getFileContents file
168168 packageState <- use_ GhcSession ""
169169 opt <- getOpts
170170 liftIO $ Compile.parseModule opt packageState file contents
src/Development/IDE/State/Service.hs+3−2
@@ -73,8 +73,9 @@ initialise :: Rules ()
7373 -> Maybe (Event -> IO ())
7474 -> Logger.Handle
7575 -> IdeOptions
76+ -> VFSHandle
7677 -> IO IdeState
77−initialise mainRule toDiags logger options =
78+initialise mainRule toDiags logger options vfs =
7879 shakeOpen
7980 (fromMaybe (const $ pure ()) toDiags)
8081 logger
@@ -83,7 +84,7 @@ initialise mainRule toDiags logger options =
8384 , shakeFiles = "/dev/null"
8485 }) $ do
8586 addIdeGlobal =<< liftIO (mkEnv options)
86− fileStoreRules
87+ fileStoreRules vfs
8788 mainRule
8889
8990writeProfile :: IdeState -> FilePath -> IO ()
src/Development/IDE/State/Shake.hs+13−2
@@ -40,6 +40,8 @@ module Development.IDE.State.Shake(
4040 sendEvent,
4141 Development.IDE.State.Shake.logDebug,
4242 Development.IDE.State.Shake.logSeriousError,
43+ FileVersion(..),
44+ vfsVersion
4345 ) where
4446
4547import Development.Shake
@@ -390,7 +392,7 @@ updateFileDiagnostics fp k ShakeExtras{diagnostics, state} current = do
390392 modTime <- join <$> getValues state GetModificationTime fp
391393 modifyVar diagnostics $ \old -> do
392394 let oldDiags = getFileDiagnostics fp old
393− let newDiagsStore = setStageDiagnostics fp modTime k current old
395+ let newDiagsStore = setStageDiagnostics fp (vfsVersion =<< modTime) k current old
394396 let newDiags = getFileDiagnostics fp newDiagsStore
395397 pure (newDiagsStore, (newDiags, oldDiags))
396398 when (newDiags /= oldDiags) $
@@ -421,4 +423,13 @@ instance Hashable GetModificationTime
421423instance NFData GetModificationTime
422424
423425-- | Get the modification time of a file.
424−type instance RuleResult GetModificationTime = UTCTime
426+type instance RuleResult GetModificationTime = FileVersion
427+
428+data FileVersion = VFSVersion Int | ModificationTime UTCTime
429+ deriving (Show, Generic)
430+
431+instance NFData FileVersion
432+
433+vfsVersion :: FileVersion -> Maybe Int
434+vfsVersion (VFSVersion i) = Just i
435+vfsVersion (ModificationTime _) = Nothing
src/Development/IDE/Types/Diagnostics.hs+2−6
@@ -44,8 +44,6 @@ import Data.Either.Combinators
4444import Data.Maybe as Maybe
4545import Data.Foldable
4646import qualified Data.Map as Map
47−import Data.Time.Clock
48−import Data.Time.Clock.POSIX
4947import qualified Data.Text as T
5048import Data.Text.Prettyprint.Doc.Syntax
5149import qualified Data.SortedList as SL
@@ -179,18 +177,16 @@ emptyDiagnostics = ProjectDiagnostics mempty
179177setStageDiagnostics ::
180178 Show stage =>
181179 FilePath ->
182− Maybe UTCTime ->
180+ Maybe Int ->
183181 -- ^ the time that the file these diagnostics originate from was last edited
184182 stage ->
185183 [LSP.Diagnostic] ->
186184 ProjectDiagnostics stage ->
187185 ProjectDiagnostics stage
188186setStageDiagnostics fp timeM stage diags (ProjectDiagnostics ds) =
189− ProjectDiagnostics $ updateDiagnostics ds uri posixTime diagsBySource
187+ ProjectDiagnostics $ updateDiagnostics ds uri timeM diagsBySource
190188 where
191189 diagsBySource = Map.singleton (Just $ T.pack $ show stage) (SL.toSortedList diags)
192− posixTime :: Maybe Int
193− posixTime = fmap (fromEnum . utcTimeToPOSIXSeconds) timeM
194190 uri = filePathToUri fp
195191
196192fromUri :: LSP.Uri -> FilePath
test/Demo.hs+3
@@ -6,6 +6,7 @@ module Demo(main) where
66import Control.Concurrent.Extra
77import Control.Monad
88import System.Time.Extra
9+import Development.IDE.State.FileStore
910import Development.IDE.State.Service
1011import Development.IDE.State.Rules
1112import Development.IDE.State.Shake
@@ -38,6 +39,7 @@ main = do
3839 -- lock to avoid overlapping output on stdout
3940 lock <- newLock
4041
42+ vfs <- makeVFSHandle
4143 ide <- initialise
4244 mainRule
4345 (Just $ showEvent lock)
@@ -51,6 +53,7 @@ main = do
5153 ,optThreads = 0
5254 ,optShakeProfiling = Nothing -- Just "output.html"
5355 }
56+ vfs
5457 setFilesOfInterest ide $ Set.fromList files
5558 _ <- runAction ide $ uses_ TypeCheck files
5659 -- shake now writes an async message that it is completed with timing info,