@@ -20,7 +20,9 @@ module Development.IDE.Core.FileStore(
20
20
getModificationTimeImpl ,
21
21
addIdeGlobal ,
22
22
getFileContentsImpl ,
23
- getModTime
23
+ getModTime ,
24
+ isWatchSupported ,
25
+ registerFileWatches
24
26
) where
25
27
26
28
import Control.Concurrent.STM (atomically )
@@ -49,7 +51,8 @@ import Development.IDE.Types.Diagnostics
49
51
import Development.IDE.Types.Location
50
52
import Development.IDE.Types.Options
51
53
import HieDb.Create (deleteMissingRealFiles )
52
- import Ide.Plugin.Config (CheckParents (.. ))
54
+ import Ide.Plugin.Config (CheckParents (.. ),
55
+ Config )
53
56
import System.IO.Error
54
57
55
58
#ifdef mingw32_HOST_OS
@@ -63,13 +66,20 @@ import qualified Development.IDE.Types.Logger as L
63
66
64
67
import qualified Data.Binary as B
65
68
import qualified Data.ByteString.Lazy as LBS
69
+ import qualified Data.Text as Text
66
70
import Language.LSP.Server hiding
67
71
(getVirtualFile )
68
72
import qualified Language.LSP.Server as LSP
69
- import Language.LSP.Types (FileChangeType (FcChanged ),
73
+ import Language.LSP.Types (DidChangeWatchedFilesRegistrationOptions (DidChangeWatchedFilesRegistrationOptions ),
74
+ FileChangeType (FcChanged ),
70
75
FileEvent (FileEvent ),
76
+ FileSystemWatcher (.. ),
77
+ WatchKind (.. ),
78
+ _watchers ,
71
79
toNormalizedFilePath ,
72
80
uriToFilePath )
81
+ import qualified Language.LSP.Types as LSP
82
+ import qualified Language.LSP.Types.Capabilities as LSP
73
83
import Language.LSP.VFS
74
84
import System.FilePath
75
85
@@ -94,6 +104,15 @@ makeLSPVFSHandle lspEnv = VFSHandle
94
104
, setVirtualFileContents = Nothing
95
105
}
96
106
107
+ addWatchedFileRule :: (NormalizedFilePath -> Action Bool ) -> Rules ()
108
+ addWatchedFileRule isWatched = defineNoDiagnostics $ \ AddWatchedFile f -> do
109
+ isAlreadyWatched <- isWatched f
110
+ if isAlreadyWatched then pure (Just True ) else do
111
+ ShakeExtras {lspEnv} <- getShakeExtras
112
+ case lspEnv of
113
+ Just env -> fmap Just $ liftIO $ LSP. runLspT env $
114
+ registerFileWatches [fromNormalizedFilePath f]
115
+ Nothing -> pure Nothing
97
116
98
117
isFileOfInterestRule :: Rules ()
99
118
isFileOfInterestRule = defineEarlyCutoff $ RuleNoDiagnostics $ \ IsFileOfInterest f -> do
@@ -109,45 +128,44 @@ isFileOfInterestRule = defineEarlyCutoff $ RuleNoDiagnostics $ \IsFileOfInterest
109
128
summarize (IsFOI (Modified True )) = BS. singleton 3
110
129
111
130
112
- getModificationTimeRule :: VFSHandle -> ( NormalizedFilePath -> Action Bool ) -> Rules ()
113
- getModificationTimeRule vfs isWatched = defineEarlyCutoff $ Rule $ \ (GetModificationTime_ missingFileDiags) file ->
114
- getModificationTimeImpl vfs isWatched missingFileDiags file
131
+ getModificationTimeRule :: VFSHandle -> Rules ()
132
+ getModificationTimeRule vfs = defineEarlyCutoff $ Rule $ \ (GetModificationTime_ missingFileDiags) file ->
133
+ getModificationTimeImpl vfs missingFileDiags file
115
134
116
135
getModificationTimeImpl :: VFSHandle
117
- -> (NormalizedFilePath -> Action Bool )
118
136
-> Bool
119
137
-> NormalizedFilePath
120
138
-> Action
121
139
(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
140
+ getModificationTimeImpl vfs missingFileDiags file = do
141
+ let file' = fromNormalizedFilePath file
142
+ let wrap time = (Just $ LBS. toStrict $ B. encode $ toRational time, ([] , Just $ ModificationTime time))
143
+ mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri' file
144
+ case mbVirtual of
145
+ Just (virtualFileVersion -> ver) -> do
146
+ alwaysRerun
147
+ pure (Just $ LBS. toStrict $ B. encode ver, ([] , Just $ VFSVersion ver))
148
+ Nothing -> do
149
+ isWF <- use_ AddWatchedFile file
150
+ if isWF
151
+ then -- the file is watched so we can rely on FileWatched notifications,
152
+ -- but also need a dependency on IsFileOfInterest to reinstall
153
+ -- alwaysRerun when the file becomes VFS
154
+ void (use_ IsFileOfInterest file)
155
+ else if isInterface file
156
+ then -- interface files are tracked specially using the closed world assumption
157
+ pure ()
158
+ else -- in all other cases we will need to freshly check the file system
159
+ alwaysRerun
142
160
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 ))
161
+ liftIO $ fmap wrap (getModTime file')
162
+ `catch` \ (e :: IOException ) -> do
163
+ let err | isDoesNotExistError e = " File does not exist: " ++ file'
164
+ | otherwise = " IO error while reading " ++ file' ++ " , " ++ displayException e
165
+ diag = ideErrorText file (T. pack err)
166
+ if isDoesNotExistError e && not missingFileDiags
167
+ then return (Nothing , ([] , Nothing ))
168
+ else return (Nothing , ([diag], Nothing ))
151
169
152
170
-- | Interface files cannot be watched, since they live outside the workspace.
153
171
-- But interface files are private, in that only HLS writes them.
@@ -239,9 +257,10 @@ getFileContents f = do
239
257
fileStoreRules :: VFSHandle -> (NormalizedFilePath -> Action Bool ) -> Rules ()
240
258
fileStoreRules vfs isWatched = do
241
259
addIdeGlobal vfs
242
- getModificationTimeRule vfs isWatched
260
+ getModificationTimeRule vfs
243
261
getFileContentsRule vfs
244
262
isFileOfInterestRule
263
+ addWatchedFileRule isWatched
245
264
246
265
-- | Note that some buffer for a specific file has been modified but not
247
266
-- with what changes.
@@ -290,3 +309,43 @@ setSomethingModified state = do
290
309
-- Update database to remove any files that might have been renamed/deleted
291
310
atomically $ writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) deleteMissingRealFiles
292
311
void $ shakeRestart state []
312
+
313
+ registerFileWatches :: [String ] -> LSP. LspT Config IO Bool
314
+ registerFileWatches globs = do
315
+ watchSupported <- isWatchSupported
316
+ if watchSupported
317
+ then do
318
+ let
319
+ regParams = LSP. RegistrationParams (List [LSP. SomeRegistration registration])
320
+ -- The registration ID is arbitrary and is only used in case we want to deregister (which we won't).
321
+ -- We could also use something like a random UUID, as some other servers do, but this works for
322
+ -- our purposes.
323
+ registration = LSP. Registration " globalFileWatches"
324
+ LSP. SWorkspaceDidChangeWatchedFiles
325
+ regOptions
326
+ regOptions =
327
+ DidChangeWatchedFilesRegistrationOptions { _watchers = List watchers }
328
+ -- See Note [File existence cache and LSP file watchers] for why this exists, and the choice of watch kind
329
+ watchKind = WatchKind { _watchCreate = True , _watchChange = True , _watchDelete = True }
330
+ -- See Note [Which files should we watch?] for an explanation of why the pattern is the way that it is
331
+ -- The patterns will be something like "**/.hs", i.e. "any number of directory segments,
332
+ -- followed by a file with an extension 'hs'.
333
+ watcher glob = FileSystemWatcher { _globPattern = glob, _kind = Just watchKind }
334
+ -- We use multiple watchers instead of one using '{}' because lsp-test doesn't
335
+ -- support that: https://github.com/bubba/lsp-test/issues/77
336
+ watchers = [ watcher (Text. pack glob) | glob <- globs ]
337
+
338
+ void $ LSP. sendRequest LSP. SClientRegisterCapability regParams (const $ pure () ) -- TODO handle response
339
+ return True
340
+ else return False
341
+
342
+ isWatchSupported :: LSP. LspT Config IO Bool
343
+ isWatchSupported = do
344
+ clientCapabilities <- LSP. getClientCapabilities
345
+ pure $ case () of
346
+ _ | LSP. ClientCapabilities {_workspace} <- clientCapabilities
347
+ , Just LSP. WorkspaceClientCapabilities {_didChangeWatchedFiles} <- _workspace
348
+ , Just LSP. DidChangeWatchedFilesClientCapabilities {_dynamicRegistration} <- _didChangeWatchedFiles
349
+ , Just True <- _dynamicRegistration
350
+ -> True
351
+ | otherwise -> False
0 commit comments