@@ -20,7 +20,9 @@ module Development.IDE.Core.FileStore(
2020 getModificationTimeImpl ,
2121 addIdeGlobal ,
2222 getFileContentsImpl ,
23- getModTime
23+ getModTime ,
24+ isWatchSupported ,
25+ registerFileWatches
2426 ) where
2527
2628import Control.Concurrent.STM (atomically )
@@ -49,7 +51,8 @@ import Development.IDE.Types.Diagnostics
4951import Development.IDE.Types.Location
5052import Development.IDE.Types.Options
5153import HieDb.Create (deleteMissingRealFiles )
52- import Ide.Plugin.Config (CheckParents (.. ))
54+ import Ide.Plugin.Config (CheckParents (.. ),
55+ Config )
5356import System.IO.Error
5457
5558#ifdef mingw32_HOST_OS
@@ -63,13 +66,21 @@ import qualified Development.IDE.Types.Logger as L
6366
6467import qualified Data.Binary as B
6568import qualified Data.ByteString.Lazy as LBS
69+ import qualified Data.Text as Text
70+ import Development.IDE.Core.IdeConfiguration (isWorkspaceFile )
6671import Language.LSP.Server hiding
6772 (getVirtualFile )
6873import qualified Language.LSP.Server as LSP
69- import Language.LSP.Types (FileChangeType (FcChanged ),
74+ import Language.LSP.Types (DidChangeWatchedFilesRegistrationOptions (DidChangeWatchedFilesRegistrationOptions ),
75+ FileChangeType (FcChanged ),
7076 FileEvent (FileEvent ),
77+ FileSystemWatcher (.. ),
78+ WatchKind (.. ),
79+ _watchers ,
7180 toNormalizedFilePath ,
7281 uriToFilePath )
82+ import qualified Language.LSP.Types as LSP
83+ import qualified Language.LSP.Types.Capabilities as LSP
7384import Language.LSP.VFS
7485import System.FilePath
7586
@@ -94,6 +105,17 @@ makeLSPVFSHandle lspEnv = VFSHandle
94105 , setVirtualFileContents = Nothing
95106 }
96107
108+ addWatchedFileRule :: (NormalizedFilePath -> Action Bool ) -> Rules ()
109+ addWatchedFileRule isWatched = defineNoDiagnostics $ \ AddWatchedFile f -> do
110+ isAlreadyWatched <- isWatched f
111+ isWp <- isWorkspaceFile f
112+ if isAlreadyWatched then pure (Just True ) else
113+ if not isWp then pure (Just False ) else do
114+ ShakeExtras {lspEnv} <- getShakeExtras
115+ case lspEnv of
116+ Just env -> fmap Just $ liftIO $ LSP. runLspT env $
117+ registerFileWatches [fromNormalizedFilePath f]
118+ Nothing -> pure $ Just False
97119
98120isFileOfInterestRule :: Rules ()
99121isFileOfInterestRule = defineEarlyCutoff $ RuleNoDiagnostics $ \ IsFileOfInterest f -> do
@@ -109,45 +131,44 @@ isFileOfInterestRule = defineEarlyCutoff $ RuleNoDiagnostics $ \IsFileOfInterest
109131 summarize (IsFOI (Modified True )) = BS. singleton 3
110132
111133
112- getModificationTimeRule :: VFSHandle -> ( NormalizedFilePath -> Action Bool ) -> Rules ()
113- getModificationTimeRule vfs isWatched = defineEarlyCutoff $ Rule $ \ (GetModificationTime_ missingFileDiags) file ->
114- getModificationTimeImpl vfs isWatched missingFileDiags file
134+ getModificationTimeRule :: VFSHandle -> Rules ()
135+ getModificationTimeRule vfs = defineEarlyCutoff $ Rule $ \ (GetModificationTime_ missingFileDiags) file ->
136+ getModificationTimeImpl vfs missingFileDiags file
115137
116138getModificationTimeImpl :: VFSHandle
117- -> (NormalizedFilePath -> Action Bool )
118139 -> Bool
119140 -> NormalizedFilePath
120141 -> Action
121142 (Maybe BS. ByteString , ([FileDiagnostic ], Maybe FileVersion ))
122- getModificationTimeImpl vfs isWatched missingFileDiags file = do
123- let file' = fromNormalizedFilePath file
124- let wrap time = (Just $ LBS. toStrict $ B. encode $ toRational time, ([] , Just $ ModificationTime time))
125- mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri' file
126- case mbVirtual of
127- Just (virtualFileVersion -> ver) -> do
128- alwaysRerun
129- pure (Just $ LBS. toStrict $ B. encode ver, ([] , Just $ VFSVersion ver))
130- Nothing -> do
131- isWF <- isWatched file
132- if isWF
133- then -- the file is watched so we can rely on FileWatched notifications,
134- -- but also need a dependency on IsFileOfInterest to reinstall
135- -- alwaysRerun when the file becomes VFS
136- void (use_ IsFileOfInterest file)
137- else if isInterface file
138- then -- interface files are tracked specially using the closed world assumption
139- pure ()
140- else -- in all other cases we will need to freshly check the file system
141- alwaysRerun
143+ getModificationTimeImpl vfs missingFileDiags file = do
144+ let file' = fromNormalizedFilePath file
145+ let wrap time = (Just $ LBS. toStrict $ B. encode $ toRational time, ([] , Just $ ModificationTime time))
146+ mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri' file
147+ case mbVirtual of
148+ Just (virtualFileVersion -> ver) -> do
149+ alwaysRerun
150+ pure (Just $ LBS. toStrict $ B. encode ver, ([] , Just $ VFSVersion ver))
151+ Nothing -> do
152+ isWF <- use_ AddWatchedFile file
153+ if isWF
154+ then -- the file is watched so we can rely on FileWatched notifications,
155+ -- but also need a dependency on IsFileOfInterest to reinstall
156+ -- alwaysRerun when the file becomes VFS
157+ void (use_ IsFileOfInterest file)
158+ else if isInterface file
159+ then -- interface files are tracked specially using the closed world assumption
160+ pure ()
161+ else -- in all other cases we will need to freshly check the file system
162+ alwaysRerun
142163
143- liftIO $ fmap wrap (getModTime file')
144- `catch` \ (e :: IOException ) -> do
145- let err | isDoesNotExistError e = " File does not exist: " ++ file'
146- | otherwise = " IO error while reading " ++ file' ++ " , " ++ displayException e
147- diag = ideErrorText file (T. pack err)
148- if isDoesNotExistError e && not missingFileDiags
149- then return (Nothing , ([] , Nothing ))
150- else return (Nothing , ([diag], Nothing ))
164+ liftIO $ fmap wrap (getModTime file')
165+ `catch` \ (e :: IOException ) -> do
166+ let err | isDoesNotExistError e = " File does not exist: " ++ file'
167+ | otherwise = " IO error while reading " ++ file' ++ " , " ++ displayException e
168+ diag = ideErrorText file (T. pack err)
169+ if isDoesNotExistError e && not missingFileDiags
170+ then return (Nothing , ([] , Nothing ))
171+ else return (Nothing , ([diag], Nothing ))
151172
152173-- | Interface files cannot be watched, since they live outside the workspace.
153174-- But interface files are private, in that only HLS writes them.
@@ -239,9 +260,10 @@ getFileContents f = do
239260fileStoreRules :: VFSHandle -> (NormalizedFilePath -> Action Bool ) -> Rules ()
240261fileStoreRules vfs isWatched = do
241262 addIdeGlobal vfs
242- getModificationTimeRule vfs isWatched
263+ getModificationTimeRule vfs
243264 getFileContentsRule vfs
244265 isFileOfInterestRule
266+ addWatchedFileRule isWatched
245267
246268-- | Note that some buffer for a specific file has been modified but not
247269-- with what changes.
@@ -290,3 +312,43 @@ setSomethingModified state = do
290312 -- Update database to remove any files that might have been renamed/deleted
291313 atomically $ writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) deleteMissingRealFiles
292314 void $ shakeRestart state []
315+
316+ registerFileWatches :: [String ] -> LSP. LspT Config IO Bool
317+ registerFileWatches globs = do
318+ watchSupported <- isWatchSupported
319+ if watchSupported
320+ then do
321+ let
322+ regParams = LSP. RegistrationParams (List [LSP. SomeRegistration registration])
323+ -- The registration ID is arbitrary and is only used in case we want to deregister (which we won't).
324+ -- We could also use something like a random UUID, as some other servers do, but this works for
325+ -- our purposes.
326+ registration = LSP. Registration " globalFileWatches"
327+ LSP. SWorkspaceDidChangeWatchedFiles
328+ regOptions
329+ regOptions =
330+ DidChangeWatchedFilesRegistrationOptions { _watchers = List watchers }
331+ -- See Note [File existence cache and LSP file watchers] for why this exists, and the choice of watch kind
332+ watchKind = WatchKind { _watchCreate = True , _watchChange = True , _watchDelete = True }
333+ -- See Note [Which files should we watch?] for an explanation of why the pattern is the way that it is
334+ -- The patterns will be something like "**/.hs", i.e. "any number of directory segments,
335+ -- followed by a file with an extension 'hs'.
336+ watcher glob = FileSystemWatcher { _globPattern = glob, _kind = Just watchKind }
337+ -- We use multiple watchers instead of one using '{}' because lsp-test doesn't
338+ -- support that: https://github.com/bubba/lsp-test/issues/77
339+ watchers = [ watcher (Text. pack glob) | glob <- globs ]
340+
341+ void $ LSP. sendRequest LSP. SClientRegisterCapability regParams (const $ pure () )
342+ return True
343+ else return False
344+
345+ isWatchSupported :: LSP. LspT Config IO Bool
346+ isWatchSupported = do
347+ clientCapabilities <- LSP. getClientCapabilities
348+ pure $ case () of
349+ _ | LSP. ClientCapabilities {_workspace} <- clientCapabilities
350+ , Just LSP. WorkspaceClientCapabilities {_didChangeWatchedFiles} <- _workspace
351+ , Just LSP. DidChangeWatchedFilesClientCapabilities {_dynamicRegistration} <- _didChangeWatchedFiles
352+ , Just True <- _dynamicRegistration
353+ -> True
354+ | otherwise -> False
0 commit comments