on master help sign in
Fix performance of getFileExists (#322)
2d9314aPepe Iborra
exe/Main.hs+3−3
@@ -93,10 +93,10 @@ main = do
93 -- very important we only call loadSession once, and it's fast, so just do it before starting
93 -- very important we only call loadSession once, and it's fast, so just do it before starting
94 session <- loadSession dir
94 session <- loadSession dir
95 let options = (defaultIdeOptions $ return session)
95 let options = (defaultIdeOptions $ return session)
96− { optReportProgress = clientSupportsProgress caps
96+ { optReportProgress = clientSupportsProgress caps
97 , optShakeProfiling = argsShakeProfiling
97 , optShakeProfiling = argsShakeProfiling
98 }
98 }
99− initialise (mainRule >> action kick) getLspId event (logger minBound) options vfs
99+ initialise caps (mainRule >> action kick) getLspId event (logger minBound) options vfs
100 else do
100 else do
101 putStrLn $ "Ghcide setup tester in " ++ dir ++ "."
101 putStrLn $ "Ghcide setup tester in " ++ dir ++ "."
102 putStrLn "Report bugs at https://github.com/digital-asset/ghcide/issues"
102 putStrLn "Report bugs at https://github.com/digital-asset/ghcide/issues"
@@ -125,7 +125,7 @@ main = do
125 let grab file = fromMaybe (head sessions) $ do
125 let grab file = fromMaybe (head sessions) $ do
126 cradle <- Map.lookup file filesToCradles
126 cradle <- Map.lookup file filesToCradles
127 Map.lookup cradle cradlesToSessions
127 Map.lookup cradle cradlesToSessions
128− ide <- initialise mainRule (pure $ IdInt 0) (showEvent lock) (logger Info) (defaultIdeOptions $ return $ return . grab) vfs
128+ ide <- initialise def mainRule (pure $ IdInt 0) (showEvent lock) (logger Info) (defaultIdeOptions $ return $ return . grab) vfs
129
129
130 putStrLn "\nStep 6/6: Type checking the files"
130 putStrLn "\nStep 6/6: Type checking the files"
131 setFilesOfInterest ide $ Set.fromList $ map toNormalizedFilePath files
131 setFilesOfInterest ide $ Set.fromList $ map toNormalizedFilePath files
ghcide.cabal+13
@@ -121,6 +121,7 @@ library
121 Development.IDE.Core.Debouncer
121 Development.IDE.Core.Debouncer
122 Development.IDE.Core.Compile
122 Development.IDE.Core.Compile
123 Development.IDE.Core.Preprocessor
123 Development.IDE.Core.Preprocessor
124+ Development.IDE.Core.FileExists
124 Development.IDE.GHC.Compat
125 Development.IDE.GHC.Compat
125 Development.IDE.GHC.CPP
126 Development.IDE.GHC.CPP
126 Development.IDE.GHC.Error
127 Development.IDE.GHC.Error
@@ -230,4 +231,16 @@ test-suite ghcide-tests
230 Development.IDE.Test
231 Development.IDE.Test
231 Development.IDE.Test.Runfiles
232 Development.IDE.Test.Runfiles
232 default-extensions:
233 default-extensions:
234+ BangPatterns
235+ DeriveFunctor
236+ DeriveGeneric
237+ GeneralizedNewtypeDeriving
238+ LambdaCase
239+ NamedFieldPuns
233 OverloadedStrings
240 OverloadedStrings
241+ RecordWildCards
242+ ScopedTypeVariables
243+ StandaloneDeriving
244+ TupleSections
245+ TypeApplications
246+ ViewPatterns
src/Development/IDE/Core/FileExists.hsnew file+187
@@ -0,0 +1,187 @@
1+{-# LANGUAGE OverloadedLists #-}
2+{-# LANGUAGE TypeFamilies #-}
3+{-# LANGUAGE UndecidableInstances #-}
4+module Development.IDE.Core.FileExists
5+ ( fileExistsRules
6+ , modifyFileExists
7+ , getFileExists
8+ )
9+where
10+
11+import Control.Concurrent.Extra
12+import Control.Exception
13+import Control.Monad.Extra
14+import qualified Data.Aeson as A
15+import Data.Binary
16+import qualified Data.ByteString as BS
17+import Data.Map.Strict ( Map )
18+import qualified Data.Map.Strict as Map
19+import Data.Maybe
20+import qualified Data.Text as T
21+import Development.IDE.Core.FileStore
22+import Development.IDE.Core.Shake
23+import Development.IDE.Types.Location
24+import Development.Shake
25+import Development.Shake.Classes
26+import GHC.Generics
27+import Language.Haskell.LSP.Messages
28+import Language.Haskell.LSP.Types
29+import Language.Haskell.LSP.Types.Capabilities
30+import qualified System.Directory as Dir
31+
32+-- | A map for tracking the file existence
33+type FileExistsMap = (Map NormalizedFilePath Bool)
34+
35+-- | A wrapper around a mutable 'FileExistsMap'
36+newtype FileExistsMapVar = FileExistsMapVar (Var FileExistsMap)
37+
38+instance IsIdeGlobal FileExistsMapVar
39+
40+-- | Grab the current global value of 'FileExistsMap' without acquiring a dependency
41+getFileExistsMapUntracked :: Action FileExistsMap
42+getFileExistsMapUntracked = do
43+ FileExistsMapVar v <- getIdeGlobalAction
44+ liftIO $ readVar v
45+
46+-- | Modify the global store of file exists
47+modifyFileExistsAction :: (FileExistsMap -> IO FileExistsMap) -> Action ()
48+modifyFileExistsAction f = do
49+ FileExistsMapVar var <- getIdeGlobalAction
50+ liftIO $ modifyVar_ var f
51+
52+-- | Modify the global store of file exists
53+modifyFileExists :: IdeState -> [(NormalizedFilePath, Bool)] -> IO ()
54+modifyFileExists state changes = do
55+ FileExistsMapVar var <- getIdeGlobalState state
56+ changesMap <- evaluate $ Map.fromList changes
57+
58+ -- Masked to ensure that the previous values are flushed together with the map update
59+ mask $ \_ -> do
60+ -- update the map
61+ modifyVar_ var $ evaluate . Map.union changesMap
62+ -- flush previous values
63+ mapM_ (deleteValue state GetFileExists . fst) changes
64+
65+-------------------------------------------------------------------------------------
66+
67+type instance RuleResult GetFileExists = Bool
68+
69+data GetFileExists = GetFileExists
70+ deriving (Eq, Show, Typeable, Generic)
71+
72+instance NFData GetFileExists
73+instance Hashable GetFileExists
74+instance Binary GetFileExists
75+
76+-- | Returns True if the file exists
77+-- Note that a file is not considered to exist unless it is saved to disk.
78+-- In particular, VFS existence is not enough.
79+-- Consider the following example:
80+-- 1. The file @A.hs@ containing the line @import B@ is added to the files of interest
81+-- Since @B.hs@ is neither open nor exists, GetLocatedImports finds Nothing
82+-- 2. The editor creates a new buffer @B.hs@
83+-- Unless the editor also sends a @DidChangeWatchedFile@ event, ghcide will not pick it up
84+-- Most editors, e.g. VSCode, only send the event when the file is saved to disk.
85+getFileExists :: NormalizedFilePath -> Action Bool
86+getFileExists fp = use_ GetFileExists fp
87+
88+-- | Installs the 'getFileExists' rules.
89+-- Provides a fast implementation if client supports dynamic watched files.
90+-- Creates a global state as a side effect in that case.
91+fileExistsRules :: IO LspId -> ClientCapabilities -> VFSHandle -> Rules ()
92+fileExistsRules getLspId ClientCapabilities{_workspace}
93+ | Just WorkspaceClientCapabilities{_didChangeWatchedFiles} <- _workspace
94+ , Just DidChangeWatchedFilesClientCapabilities{_dynamicRegistration} <- _didChangeWatchedFiles
95+ , Just True <- _dynamicRegistration
96+ = fileExistsRulesFast getLspId
97+ | otherwise = fileExistsRulesSlow
98+
99+-- Requires an lsp client that provides WatchedFiles notifications.
100+fileExistsRulesFast :: IO LspId -> VFSHandle -> Rules ()
101+fileExistsRulesFast getLspId vfs = do
102+ addIdeGlobal . FileExistsMapVar =<< liftIO (newVar [])
103+ defineEarlyCutoff $ \GetFileExists file -> do
104+ fileExistsMap <- getFileExistsMapUntracked
105+ let mbFilesWatched = Map.lookup file fileExistsMap
106+ case mbFilesWatched of
107+ Just fv -> pure (summarizeExists fv, ([], Just fv))
108+ Nothing -> do
109+ exist <- liftIO $ getFileExistsVFS vfs file
110+ ShakeExtras { eventer } <- getShakeExtras
111+
112+ -- add a listener for VFS Create/Delete file events,
113+ -- taking the FileExistsMap lock to prevent race conditions
114+ -- that would lead to multiple listeners for the same path
115+ modifyFileExistsAction $ \x -> do
116+ case Map.insertLookupWithKey (\_ x _ -> x) file exist x of
117+ (Nothing, x') -> do
118+ -- if the listener addition fails, we never recover. This is a bug.
119+ addListener eventer file
120+ return x'
121+ (Just _, _) ->
122+ -- if the key was already there, do nothing
123+ return x
124+
125+ pure (summarizeExists exist, ([], Just exist))
126+ where
127+ addListener eventer fp = do
128+ reqId <- getLspId
129+ let
130+ req = RequestMessage "2.0" reqId ClientRegisterCapability regParams
131+ fpAsId = T.pack $ fromNormalizedFilePath fp
132+ regParams = RegistrationParams (List [registration])
133+ registration = Registration fpAsId
134+ WorkspaceDidChangeWatchedFiles
135+ (Just (A.toJSON regOptions))
136+ regOptions =
137+ DidChangeWatchedFilesRegistrationOptions { watchers = List [watcher] }
138+ watcher = FileSystemWatcher { globPattern = fromNormalizedFilePath fp
139+ , kind = Just 5 -- Create and Delete events only
140+ }
141+
142+ eventer $ ReqRegisterCapability req
143+
144+summarizeExists :: Bool -> Maybe BS.ByteString
145+summarizeExists x = Just $ if x then BS.singleton 1 else BS.empty
146+
147+fileExistsRulesSlow:: VFSHandle -> Rules ()
148+fileExistsRulesSlow vfs = do
149+ defineEarlyCutoff $ \GetFileExists file -> do
150+ alwaysRerun
151+ exist <- liftIO $ getFileExistsVFS vfs file
152+ pure (summarizeExists exist, ([], Just exist))
153+
154+getFileExistsVFS :: VFSHandle -> NormalizedFilePath -> IO Bool
155+getFileExistsVFS vfs file = do
156+ -- we deliberately and intentionally wrap the file as an FilePath WITHOUT mkAbsolute
157+ -- so that if the file doesn't exist, is on a shared drive that is unmounted etc we get a properly
158+ -- cached 'No' rather than an exception in the wrong place
159+ handle (\(_ :: IOException) -> return False) $
160+ (isJust <$> getVirtualFile vfs (filePathToUri' file)) ||^
161+ Dir.doesFileExist (fromNormalizedFilePath file)
162+
163+--------------------------------------------------------------------------------------------------
164+-- The message definitions below probably belong in haskell-lsp-types
165+
166+data DidChangeWatchedFilesRegistrationOptions = DidChangeWatchedFilesRegistrationOptions
167+ { watchers :: List FileSystemWatcher
168+ }
169+
170+instance A.ToJSON DidChangeWatchedFilesRegistrationOptions where
171+ toJSON DidChangeWatchedFilesRegistrationOptions {..} =
172+ A.object ["watchers" A..= watchers]
173+
174+data FileSystemWatcher = FileSystemWatcher
175+ { -- | The glob pattern to watch.
176+ -- For details on glob pattern syntax, check the spec: https://microsoft.github.io/language-server-protocol/specifications/specification-3-14/#workspace_didChangeWatchedFiles
177+ globPattern :: String
178+ -- | The kind of event to subscribe to. Defaults to all.
179+ -- Defined as a bitmap of Create(1), Change(2), and Delete(4)
180+ , kind :: Maybe Int
181+ }
182+
183+instance A.ToJSON FileSystemWatcher where
184+ toJSON FileSystemWatcher {..} =
185+ A.object
186+ $ ["globPattern" A..= globPattern]
187+ ++ [ "kind" A..= x | Just x <- [kind] ]
src/Development/IDE/Core/FileStore.hs+5−32
@@ -4,7 +4,8 @@
4{-# LANGUAGE TypeFamilies #-}
4{-# LANGUAGE TypeFamilies #-}
5
5
6module Development.IDE.Core.FileStore(
6module Development.IDE.Core.FileStore(
7− getFileExists, getFileContents,
7+ getFileContents,
8+ getVirtualFile,
8 setBufferModified,
9 setBufferModified,
9 setSomethingModified,
10 setSomethingModified,
10 fileStoreRules,
11 fileStoreRules,
@@ -20,16 +21,14 @@ import Fingerprint
20import StringBuffer
21import StringBuffer
21import Development.IDE.GHC.Orphans()
22import Development.IDE.GHC.Orphans()
22import Development.IDE.GHC.Util
23import Development.IDE.GHC.Util
23−
24+import Development.IDE.Core.Shake
24import Control.Concurrent.Extra
25import Control.Concurrent.Extra
25import qualified Data.Map.Strict as Map
26import qualified Data.Map.Strict as Map
26import Data.Maybe
27import Data.Maybe
27import qualified Data.Text as T
28import qualified Data.Text as T
28import Control.Monad.Extra
29import Control.Monad.Extra
29−import qualified System.Directory as Dir
30import Development.Shake
30import Development.Shake
31import Development.Shake.Classes
31import Development.Shake.Classes
32−import Development.IDE.Core.Shake
33import Control.Exception
32import Control.Exception
34import GHC.Generics
33import GHC.Generics
35import Data.Either.Extra
34import Data.Either.Extra
@@ -90,17 +89,8 @@ makeLSPVFSHandle lspFuncs = VFSHandle
90-- | Get the contents of a file, either dirty (if the buffer is modified) or Nothing to mean use from disk.
89-- | Get the contents of a file, either dirty (if the buffer is modified) or Nothing to mean use from disk.
91type instance RuleResult GetFileContents = (FileVersion, Maybe StringBuffer)
90type instance RuleResult GetFileContents = (FileVersion, Maybe StringBuffer)
92
91
93−-- | Does the file exist.
94−type instance RuleResult GetFileExists = Bool
95−
96type instance RuleResult FingerprintSource = Fingerprint
92type instance RuleResult FingerprintSource = Fingerprint
97
93
98−data GetFileExists = GetFileExists
99− deriving (Eq, Show, Generic)
100−instance Hashable GetFileExists
101−instance NFData GetFileExists
102−instance Binary GetFileExists
103−
104data GetFileContents = GetFileContents
94data GetFileContents = GetFileContents
105 deriving (Eq, Show, Generic)
95 deriving (Eq, Show, Generic)
106instance Hashable GetFileContents
96instance Hashable GetFileContents
@@ -122,16 +112,6 @@ fingerprintSourceRule =
122 pure ([], Just fingerprint)
112 pure ([], Just fingerprint)
123 where fpStringBuffer (StringBuffer buf len cur) = withForeignPtr buf $ \ptr -> fingerprintData (ptr `plusPtr` cur) len
113 where fpStringBuffer (StringBuffer buf len cur) = withForeignPtr buf $ \ptr -> fingerprintData (ptr `plusPtr` cur) len
124
114
125−getFileExistsRule :: VFSHandle -> Rules ()
126−getFileExistsRule vfs =
127− defineEarlyCutoff $ \GetFileExists file -> do
128− alwaysRerun
129− res <- liftIO $ handle (\(_ :: IOException) -> return False) $
130− (isJust <$> getVirtualFile vfs (filePathToUri' file)) ||^
131− Dir.doesFileExist (fromNormalizedFilePath file)
132− return (Just $ if res then BS.singleton '1' else BS.empty, ([], Just res))
133−
134−
135getModificationTimeRule :: VFSHandle -> Rules ()
115getModificationTimeRule :: VFSHandle -> Rules ()
136getModificationTimeRule vfs =
116getModificationTimeRule vfs =
137 defineEarlyCutoff $ \GetModificationTime file -> do
117 defineEarlyCutoff $ \GetModificationTime file -> do
@@ -154,6 +134,8 @@ getModificationTimeRule vfs =
154 -- time spent checking file modifications (which happens on every change)
134 -- time spent checking file modifications (which happens on every change)
155 -- from > 0.5s to ~0.15s.
135 -- from > 0.5s to ~0.15s.
156 -- We might also want to try speeding this up on Windows at some point.
136 -- We might also want to try speeding this up on Windows at some point.
137+ -- TODO leverage DidChangeWatchedFile lsp notifications on clients that
138+ -- support them, as done for GetFileExists
157 getModTime :: FilePath -> IO BS.ByteString
139 getModTime :: FilePath -> IO BS.ByteString
158 getModTime f =
140 getModTime f =
159#ifdef mingw32_HOST_OS
141#ifdef mingw32_HOST_OS
@@ -198,20 +180,11 @@ ideTryIOException fp act =
198getFileContents :: NormalizedFilePath -> Action (FileVersion, Maybe StringBuffer)
180getFileContents :: NormalizedFilePath -> Action (FileVersion, Maybe StringBuffer)
199getFileContents = use_ GetFileContents
181getFileContents = use_ GetFileContents
200
182
201−getFileExists :: NormalizedFilePath -> Action Bool
202−getFileExists =
203− -- we deliberately and intentionally wrap the file as an FilePath WITHOUT mkAbsolute
204− -- so that if the file doesn't exist, is on a shared drive that is unmounted etc we get a properly
205− -- cached 'No' rather than an exception in the wrong place
206− use_ GetFileExists
207−
208−
209fileStoreRules :: VFSHandle -> Rules ()
183fileStoreRules :: VFSHandle -> Rules ()
210fileStoreRules vfs = do
184fileStoreRules vfs = do
211 addIdeGlobal vfs
185 addIdeGlobal vfs
212 getModificationTimeRule vfs
186 getModificationTimeRule vfs
213 getFileContentsRule vfs
187 getFileContentsRule vfs
214− getFileExistsRule vfs
215 fingerprintSourceRule
188 fingerprintSourceRule
216
189
217
190
src/Development/IDE/Core/Rules.hs+2−1
@@ -37,7 +37,8 @@ import Development.IDE.Types.Options
37import Development.IDE.Spans.Calculate
37import Development.IDE.Spans.Calculate
38import Development.IDE.Import.DependencyInformation
38import Development.IDE.Import.DependencyInformation
39import Development.IDE.Import.FindImports
39import Development.IDE.Import.FindImports
40−import Development.IDE.Core.FileStore
40+import Development.IDE.Core.FileExists
41+import Development.IDE.Core.FileStore (getFileContents, getSourceFingerprint)
41import Development.IDE.Types.Diagnostics
42import Development.IDE.Types.Diagnostics
42import Development.IDE.Types.Location
43import Development.IDE.Types.Location
43import Development.IDE.GHC.Util
44import Development.IDE.GHC.Util
src/Development/IDE/Core/Service.hs+7−3
@@ -23,13 +23,15 @@ import Control.Concurrent.Async
23import Data.Maybe
23import Data.Maybe
24import Development.IDE.Types.Options (IdeOptions(..))
24import Development.IDE.Types.Options (IdeOptions(..))
25import Control.Monad
25import Control.Monad
26−import Development.IDE.Core.FileStore
26+import Development.IDE.Core.FileStore (VFSHandle, fileStoreRules)
27+import Development.IDE.Core.FileExists (fileExistsRules)
27import Development.IDE.Core.OfInterest
28import Development.IDE.Core.OfInterest
28import Development.IDE.Types.Logger
29import Development.IDE.Types.Logger
29import Development.Shake
30import Development.Shake
30import Data.Either.Extra
31import Data.Either.Extra
31import qualified Language.Haskell.LSP.Messages as LSP
32import qualified Language.Haskell.LSP.Messages as LSP
32import qualified Language.Haskell.LSP.Types as LSP
33import qualified Language.Haskell.LSP.Types as LSP
34+import qualified Language.Haskell.LSP.Types.Capabilities as LSP
33
35
34import Development.IDE.Core.Shake
36import Development.IDE.Core.Shake
35
37
@@ -42,14 +44,15 @@ instance IsIdeGlobal GlobalIdeOptions
42-- Exposed API
44-- Exposed API
43
45
44-- | Initialise the Compiler Service.
46-- | Initialise the Compiler Service.
47+initialise :: LSP.ClientCapabilities
45−initialise :: Rules ()
48+ -> Rules ()
46 -> IO LSP.LspId
49 -> IO LSP.LspId
47 -> (LSP.FromServerMessage -> IO ())
50 -> (LSP.FromServerMessage -> IO ())
48 -> Logger
51 -> Logger
49 -> IdeOptions
52 -> IdeOptions
50 -> VFSHandle
53 -> VFSHandle
51 -> IO IdeState
54 -> IO IdeState
52−initialise mainRule getLspId toDiags logger options vfs =
55+initialise caps mainRule getLspId toDiags logger options vfs =
53 shakeOpen
56 shakeOpen
54 getLspId
57 getLspId
55 toDiags
58 toDiags
@@ -63,6 +66,7 @@ initialise mainRule getLspId toDiags logger options vfs =
63 addIdeGlobal $ GlobalIdeOptions options
66 addIdeGlobal $ GlobalIdeOptions options
64 fileStoreRules vfs
67 fileStoreRules vfs
65 ofInterestRules
68 ofInterestRules
69+ fileExistsRules getLspId caps vfs
66 mainRule
70 mainRule
67
71
68writeProfile :: IdeState -> FilePath -> IO ()
72writeProfile :: IdeState -> FilePath -> IO ()
src/Development/IDE/Core/Shake.hs+13−1
@@ -20,6 +20,7 @@
20-- between runs. To deserialise a Shake value, we just consult Values.
20-- between runs. To deserialise a Shake value, we just consult Values.
21module Development.IDE.Core.Shake(
21module Development.IDE.Core.Shake(
22 IdeState,
22 IdeState,
23+ ShakeExtras(..), getShakeExtras,
23 IdeRule, IdeResult, GetModificationTime(..),
24 IdeRule, IdeResult, GetModificationTime(..),
24 shakeOpen, shakeShut,
25 shakeOpen, shakeShut,
25 shakeRun,
26 shakeRun,
@@ -38,7 +39,8 @@ module Development.IDE.Core.Shake(
38 FileVersion(..),
39 FileVersion(..),
39 Priority(..),
40 Priority(..),
40 updatePositionMapping,
41 updatePositionMapping,
42+ deleteValue,
41− OnDiskRule(..)
43+ OnDiskRule(..),
42 ) where
44 ) where
43
45
44import Development.Shake hiding (ShakeValue, doesFileExist)
46import Development.Shake hiding (ShakeValue, doesFileExist)
@@ -257,6 +259,16 @@ setValues state key file val = modifyVar_ state $ \vals -> do
257 -- Force to make sure the old HashMap is not retained
259 -- Force to make sure the old HashMap is not retained
258 evaluate $ HMap.insert (file, Key key) (fmap toDyn val) vals
260 evaluate $ HMap.insert (file, Key key) (fmap toDyn val) vals
259
261
262+-- | Delete the value stored for a given ide build key
263+deleteValue
264+ :: (Typeable k, Hashable k, Eq k, Show k)
265+ => IdeState
266+ -> k
267+ -> NormalizedFilePath
268+ -> IO ()
269+deleteValue IdeState{shakeExtras = ShakeExtras{state}} key file = modifyVar_ state $ \vals ->
270+ evaluate $ HMap.delete (file, Key key) vals
271+
260-- | We return Nothing if the rule has not run and Just Failed if it has failed to produce a value.
272-- | We return Nothing if the rule has not run and Just Failed if it has failed to produce a value.
261getValues :: forall k v. IdeRule k v => Var Values -> k -> NormalizedFilePath -> IO (Maybe (Value v))
273getValues :: forall k v. IdeRule k v => Var Values -> k -> NormalizedFilePath -> IO (Maybe (Value v))
262getValues state key file = do
274getValues state key file = do
src/Development/IDE/LSP/Notifications.hs+29−12
@@ -2,26 +2,30 @@
2-- SPDX-License-Identifier: Apache-2.0
2-- SPDX-License-Identifier: Apache-2.0
3
3
4{-# LANGUAGE DuplicateRecordFields #-}
4{-# LANGUAGE DuplicateRecordFields #-}
5−{-# LANGUAGE RankNTypes #-}
5+{-# LANGUAGE RankNTypes #-}
6
6
7module Development.IDE.LSP.Notifications
7module Development.IDE.LSP.Notifications
8 ( setHandlersNotifications
8 ( setHandlersNotifications
9 ) where
9 ) where
10
10
11−import Language.Haskell.LSP.Types
12import Development.IDE.LSP.Server
11import Development.IDE.LSP.Server
13−import qualified Language.Haskell.LSP.Core as LSP
12+import qualified Language.Haskell.LSP.Core as LSP
13+import Language.Haskell.LSP.Types
14−import qualified Language.Haskell.LSP.Types as LSP
14+import qualified Language.Haskell.LSP.Types as LSP
15
15
16−import Development.IDE.Types.Logger
17−import Development.IDE.Core.Service
16+import Development.IDE.Core.Service
18−import Development.IDE.Types.Location
17+import Development.IDE.Types.Location
18+import Development.IDE.Types.Logger
19
19
20−import Control.Monad.Extra
20+import Control.Monad.Extra
21+import Data.Foldable as F
22+import Data.Maybe
21−import qualified Data.Set as S
23+import qualified Data.Set as S
24+import qualified Data.Text as Text
22
25
23−import Development.IDE.Core.FileStore
26+import Development.IDE.Core.FileStore (setSomethingModified)
27+import Development.IDE.Core.FileExists (modifyFileExists)
24−import Development.IDE.Core.OfInterest
28+import Development.IDE.Core.OfInterest
25
29
26
30
27whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO ()
31whenUriFile :: Uri -> (NormalizedFilePath -> IO ()) -> IO ()
@@ -52,4 +56,17 @@ setHandlersNotifications = PartialHandlers $ \WithMessage{..} x -> return x
52 whenUriFile _uri $ \file -> do
56 whenUriFile _uri $ \file -> do
53 modifyFilesOfInterest ide (S.delete file)
57 modifyFilesOfInterest ide (S.delete file)
54 logInfo (ideLogger ide) $ "Closed text document: " <> getUri _uri
58 logInfo (ideLogger ide) $ "Closed text document: " <> getUri _uri
59+ ,LSP.didChangeWatchedFilesNotificationHandler = withNotification (LSP.didChangeWatchedFilesNotificationHandler x) $
60+ \_ ide (DidChangeWatchedFilesParams fileEvents) -> do
61+ let events =
62+ mapMaybe
63+ (\(FileEvent uri ev) ->
64+ (, ev /= FcDeleted) . toNormalizedFilePath
65+ <$> LSP.uriToFilePath uri
66+ )
67+ ( F.toList fileEvents )
68+ let msg = Text.pack $ show events
69+ logInfo (ideLogger ide) $ "Files created or deleted: " <> msg
70+ modifyFileExists ide events
71+ setSomethingModified ide
55− }
72+ }
test/exe/Main.hs+11−2
@@ -20,7 +20,8 @@ import qualified Data.Text as T
20import Development.IDE.Test
20import Development.IDE.Test
21import Development.IDE.Test.Runfiles
21import Development.IDE.Test.Runfiles
22import Development.IDE.Types.Location
22import Development.IDE.Types.Location
23−import Language.Haskell.LSP.Test
23+import qualified Language.Haskell.LSP.Test as LSPTest
24+import Language.Haskell.LSP.Test hiding (openDoc')
24import Language.Haskell.LSP.Types
25import Language.Haskell.LSP.Types
25import Language.Haskell.LSP.Types.Capabilities
26import Language.Haskell.LSP.Types.Capabilities
26import System.Environment.Blank (setEnv)
27import System.Environment.Blank (setEnv)
@@ -1583,7 +1584,8 @@ run s = withTempDir $ \dir -> do
1583 -- HIE calls getXgdDirectory which assumes that HOME is set.
1584 -- HIE calls getXgdDirectory which assumes that HOME is set.
1584 -- Only sets HOME if it wasn't already set.
1585 -- Only sets HOME if it wasn't already set.
1585 setEnv "HOME" "/homeless-shelter" False
1586 setEnv "HOME" "/homeless-shelter" False
1586− runSessionWithConfig conf cmd fullCaps { _window = Just $ WindowClientCapabilities $ Just True } dir s
1587+ let lspTestCaps = fullCaps { _window = Just $ WindowClientCapabilities $ Just True }
1588+ runSessionWithConfig conf cmd lspTestCaps dir s
1587 where
1589 where
1588 conf = defaultConfig
1590 conf = defaultConfig
1589 -- If you uncomment this you can see all logging
1591 -- If you uncomment this you can see all logging
@@ -1626,3 +1628,10 @@ unitTests = do
1626 [ testCase "empty file path" $
1628 [ testCase "empty file path" $
1627 uriToFilePath' (fromNormalizedUri $ filePathToUri' "") @?= Just ""
1629 uriToFilePath' (fromNormalizedUri $ filePathToUri' "") @?= Just ""
1628 ]
1630 ]
1631+
1632+-- | Wrapper around 'LSPTest.openDoc'' that sends file creation events
1633+openDoc' :: FilePath -> String -> T.Text -> Session TextDocumentIdentifier
1634+openDoc' fp name contents = do
1635+ res@(TextDocumentIdentifier uri) <- LSPTest.openDoc' fp name contents
1636+ sendNotification WorkspaceDidChangeWatchedFiles (DidChangeWatchedFilesParams $ List [FileEvent uri FcCreated])
1637+ return res