Skip to content

Commit bc5c49a

Browse files
committed
Track file versions accurately
1 parent 96ea854 commit bc5c49a

File tree

14 files changed

+174
-183
lines changed

14 files changed

+174
-183
lines changed

ghcide/ghcide.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ library
5050
dlist,
5151
exceptions,
5252
extra >= 1.7.4,
53+
enummapset,
5354
fuzzy,
5455
filepath,
5556
fingertree,
@@ -148,6 +149,7 @@ library
148149
Development.IDE.Main.HeapStats
149150
Development.IDE.Core.Debouncer
150151
Development.IDE.Core.FileStore
152+
Development.IDE.Core.FileUtils
151153
Development.IDE.Core.IdeConfiguration
152154
Development.IDE.Core.OfInterest
153155
Development.IDE.Core.PositionMapping

ghcide/src/Development/IDE/Core/FileExists.hs

Lines changed: 25 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -157,8 +157,8 @@ allExtensions opts = [extIncBoot | ext <- optExtensions opts, extIncBoot <- [ext
157157
-- | Installs the 'getFileExists' rules.
158158
-- Provides a fast implementation if client supports dynamic watched files.
159159
-- Creates a global state as a side effect in that case.
160-
fileExistsRules :: Maybe (LanguageContextEnv Config) -> VFSHandle -> Rules ()
161-
fileExistsRules lspEnv vfs = do
160+
fileExistsRules :: Maybe (LanguageContextEnv Config) -> Rules ()
161+
fileExistsRules lspEnv = do
162162
supportsWatchedFiles <- case lspEnv of
163163
Nothing -> pure False
164164
Just lspEnv' -> liftIO $ runLspT lspEnv' isWatchSupported
@@ -179,19 +179,19 @@ fileExistsRules lspEnv vfs = do
179179
else const $ pure False
180180

181181
if supportsWatchedFiles
182-
then fileExistsRulesFast isWatched vfs
183-
else fileExistsRulesSlow vfs
182+
then fileExistsRulesFast isWatched
183+
else fileExistsRulesSlow
184184

185-
fileStoreRules vfs isWatched
185+
fileStoreRules isWatched
186186

187187
-- Requires an lsp client that provides WatchedFiles notifications, but assumes that this has already been checked.
188-
fileExistsRulesFast :: (NormalizedFilePath -> Action Bool) -> VFSHandle -> Rules ()
189-
fileExistsRulesFast isWatched vfs =
188+
fileExistsRulesFast :: (NormalizedFilePath -> Action Bool) -> Rules ()
189+
fileExistsRulesFast isWatched =
190190
defineEarlyCutoff $ RuleNoDiagnostics $ \GetFileExists file -> do
191191
isWF <- isWatched file
192192
if isWF
193-
then fileExistsFast vfs file
194-
else fileExistsSlow vfs file
193+
then fileExistsFast file
194+
else fileExistsSlow file
195195

196196
{- Note [Invalidating file existence results]
197197
We have two mechanisms for getting file existence information:
@@ -209,8 +209,8 @@ For the VFS lookup, however, we won't get prompted to flush the result, so inste
209209
we use 'alwaysRerun'.
210210
-}
211211

212-
fileExistsFast :: VFSHandle -> NormalizedFilePath -> Action (Maybe BS.ByteString, Maybe Bool)
213-
fileExistsFast vfs file = do
212+
fileExistsFast :: NormalizedFilePath -> Action (Maybe BS.ByteString, Maybe Bool)
213+
fileExistsFast file = do
214214
-- Could in principle use 'alwaysRerun' here, but it's too slwo, See Note [Invalidating file existence results]
215215
mp <- getFileExistsMapUntracked
216216

@@ -219,28 +219,27 @@ fileExistsFast vfs file = do
219219
Just exist -> pure exist
220220
-- We don't know about it: use the slow route.
221221
-- Note that we do *not* call 'fileExistsSlow', as that would trigger 'alwaysRerun'.
222-
Nothing -> liftIO $ getFileExistsVFS vfs file
222+
Nothing -> getFileExistsVFS file
223223
pure (summarizeExists exist, Just exist)
224224

225225
summarizeExists :: Bool -> Maybe BS.ByteString
226226
summarizeExists x = Just $ if x then BS.singleton 1 else BS.empty
227227

228-
fileExistsRulesSlow :: VFSHandle -> Rules ()
229-
fileExistsRulesSlow vfs =
230-
defineEarlyCutoff $ RuleNoDiagnostics $ \GetFileExists file -> fileExistsSlow vfs file
228+
fileExistsRulesSlow :: Rules ()
229+
fileExistsRulesSlow =
230+
defineEarlyCutoff $ RuleNoDiagnostics $ \GetFileExists file -> fileExistsSlow file
231231

232-
fileExistsSlow :: VFSHandle -> NormalizedFilePath -> Action (Maybe BS.ByteString, Maybe Bool)
233-
fileExistsSlow vfs file = do
232+
fileExistsSlow :: NormalizedFilePath -> Action (Maybe BS.ByteString, Maybe Bool)
233+
fileExistsSlow file = do
234234
-- See Note [Invalidating file existence results]
235235
alwaysRerun
236-
exist <- liftIO $ getFileExistsVFS vfs file
236+
exist <- getFileExistsVFS file
237237
pure (summarizeExists exist, Just exist)
238238

239-
getFileExistsVFS :: VFSHandle -> NormalizedFilePath -> IO Bool
240-
getFileExistsVFS vfs file = do
241-
-- we deliberately and intentionally wrap the file as an FilePath WITHOUT mkAbsolute
242-
-- so that if the file doesn't exist, is on a shared drive that is unmounted etc we get a properly
243-
-- cached 'No' rather than an exception in the wrong place
244-
handle (\(_ :: IOException) -> return False) $
245-
(isJust <$> getVirtualFile vfs (filePathToUri' file)) ||^
246-
Dir.doesFileExist (fromNormalizedFilePath file)
239+
getFileExistsVFS :: NormalizedFilePath -> Action Bool
240+
getFileExistsVFS file = do
241+
vf <- getVirtualFile file
242+
if isJust vf
243+
then pure True
244+
else liftIO $ handle (\(_ :: IOException) -> return False) $
245+
Dir.doesFileExist (fromNormalizedFilePath file)

ghcide/src/Development/IDE/Core/FileStore.hs

Lines changed: 22 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -5,15 +5,11 @@
55

66
module Development.IDE.Core.FileStore(
77
getFileContents,
8-
getVirtualFile,
98
setFileModified,
109
setSomethingModified,
1110
fileStoreRules,
1211
modificationTime,
1312
typecheckParents,
14-
VFSHandle,
15-
makeVFSHandle,
16-
makeLSPVFSHandle,
1713
resetFileStore,
1814
resetInterfaceStore,
1915
getModificationTimeImpl,
@@ -27,20 +23,18 @@ module Development.IDE.Core.FileStore(
2723
import Control.Concurrent.STM.Stats (STM, atomically,
2824
modifyTVar')
2925
import Control.Concurrent.STM.TQueue (writeTQueue)
30-
import Control.Concurrent.Strict
3126
import Control.Exception
3227
import Control.Monad.Extra
3328
import Control.Monad.IO.Class
3429
import qualified Data.ByteString as BS
3530
import Data.Either.Extra
36-
import qualified Data.Map.Strict as Map
37-
import Data.Maybe
3831
import qualified Data.Rope.UTF16 as Rope
3932
import qualified Data.Text as T
4033
import Data.Time
4134
import Data.Time.Clock.POSIX
4235
import Development.IDE.Core.RuleTypes
4336
import Development.IDE.Core.Shake
37+
import Development.IDE.Core.FileUtils
4438
import Development.IDE.GHC.Orphans ()
4539
import Development.IDE.Graph
4640
import Development.IDE.Import.DependencyInformation
@@ -55,8 +49,6 @@ import System.IO.Error
5549
#ifdef mingw32_HOST_OS
5650
import qualified System.Directory as Dir
5751
#else
58-
import System.Posix.Files (getFileStatus,
59-
modificationTimeHiRes)
6052
#endif
6153

6254
import qualified Development.IDE.Types.Logger as L
@@ -67,8 +59,6 @@ import qualified Data.HashSet as HSet
6759
import Data.List (foldl')
6860
import qualified Data.Text as Text
6961
import Development.IDE.Core.IdeConfiguration (isWorkspaceFile)
70-
import Language.LSP.Server hiding
71-
(getVirtualFile)
7262
import qualified Language.LSP.Server as LSP
7363
import Language.LSP.Types (DidChangeWatchedFilesRegistrationOptions (DidChangeWatchedFilesRegistrationOptions),
7464
FileChangeType (FcChanged),
@@ -80,27 +70,6 @@ import qualified Language.LSP.Types.Capabilities as LSP
8070
import Language.LSP.VFS
8171
import System.FilePath
8272

83-
makeVFSHandle :: IO VFSHandle
84-
makeVFSHandle = do
85-
vfsVar <- newVar (1, Map.empty)
86-
pure VFSHandle
87-
{ getVirtualFile = \uri -> do
88-
(_nextVersion, vfs) <- readVar vfsVar
89-
pure $ Map.lookup uri vfs
90-
, setVirtualFileContents = Just $ \uri content ->
91-
void $ modifyVar' vfsVar $ \(nextVersion, vfs) -> (nextVersion + 1, ) $
92-
case content of
93-
Nothing -> Map.delete uri vfs
94-
-- The second version number is only used in persistFileVFS which we do not use so we set it to 0.
95-
Just content -> Map.insert uri (VirtualFile nextVersion 0 (Rope.fromText content)) vfs
96-
}
97-
98-
makeLSPVFSHandle :: LanguageContextEnv c -> VFSHandle
99-
makeLSPVFSHandle lspEnv = VFSHandle
100-
{ getVirtualFile = runLspT lspEnv . LSP.getVirtualFile
101-
, setVirtualFileContents = Nothing
102-
}
103-
10473
addWatchedFileRule :: (NormalizedFilePath -> Action Bool) -> Rules ()
10574
addWatchedFileRule isWatched = defineNoDiagnostics $ \AddWatchedFile f -> do
10675
isAlreadyWatched <- isWatched f
@@ -114,20 +83,19 @@ addWatchedFileRule isWatched = defineNoDiagnostics $ \AddWatchedFile f -> do
11483
Nothing -> pure $ Just False
11584

11685

117-
getModificationTimeRule :: VFSHandle -> Rules ()
118-
getModificationTimeRule vfs = defineEarlyCutoff $ Rule $ \(GetModificationTime_ missingFileDiags) file ->
119-
getModificationTimeImpl vfs missingFileDiags file
86+
getModificationTimeRule :: Rules ()
87+
getModificationTimeRule = defineEarlyCutoff $ Rule $ \(GetModificationTime_ missingFileDiags) file ->
88+
getModificationTimeImpl missingFileDiags file
12089

121-
getModificationTimeImpl :: VFSHandle
122-
-> Bool
123-
-> NormalizedFilePath
124-
-> Action
125-
(Maybe BS.ByteString, ([FileDiagnostic], Maybe FileVersion))
126-
getModificationTimeImpl vfs missingFileDiags file = do
90+
getModificationTimeImpl
91+
:: Bool
92+
-> NormalizedFilePath
93+
-> Action (Maybe BS.ByteString, ([FileDiagnostic], Maybe FileVersion))
94+
getModificationTimeImpl missingFileDiags file = do
12795
let file' = fromNormalizedFilePath file
12896
let wrap time = (Just $ LBS.toStrict $ B.encode $ toRational time, ([], Just $ ModificationTime time))
129-
mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri' file
130-
case mbVirtual of
97+
mbVf <- getVirtualFile file
98+
case mbVf of
13199
Just (virtualFileVersion -> ver) -> do
132100
alwaysRerun
133101
pure (Just $ LBS.toStrict $ B.encode ver, ([], Just $ VFSVersion ver))
@@ -180,43 +148,23 @@ resetFileStore ideState changes = mask $ \_ -> do
180148
_ -> pure ()
181149

182150

183-
-- Dir.getModificationTime is surprisingly slow since it performs
184-
-- a ton of conversions. Since we do not actually care about
185-
-- the format of the time, we can get away with something cheaper.
186-
-- For now, we only try to do this on Unix systems where it seems to get the
187-
-- time spent checking file modifications (which happens on every change)
188-
-- from > 0.5s to ~0.15s.
189-
-- We might also want to try speeding this up on Windows at some point.
190-
-- TODO leverage DidChangeWatchedFile lsp notifications on clients that
191-
-- support them, as done for GetFileExists
192-
getModTime :: FilePath -> IO POSIXTime
193-
getModTime f =
194-
#ifdef mingw32_HOST_OS
195-
utcTimeToPOSIXSeconds <$> Dir.getModificationTime f
196-
#else
197-
modificationTimeHiRes <$> getFileStatus f
198-
#endif
199-
200151
modificationTime :: FileVersion -> Maybe UTCTime
201152
modificationTime VFSVersion{} = Nothing
202153
modificationTime (ModificationTime posix) = Just $ posixSecondsToUTCTime posix
203154

204-
getFileContentsRule :: VFSHandle -> Rules ()
205-
getFileContentsRule vfs = define $ \GetFileContents file -> getFileContentsImpl vfs file
155+
getFileContentsRule :: Rules ()
156+
getFileContentsRule = define $ \GetFileContents file -> getFileContentsImpl file
206157

207158
getFileContentsImpl
208-
:: VFSHandle
209-
-> NormalizedFilePath
159+
:: NormalizedFilePath
210160
-> Action ([FileDiagnostic], Maybe (FileVersion, Maybe T.Text))
211-
getFileContentsImpl vfs file = do
161+
getFileContentsImpl file = do
212162
-- need to depend on modification time to introduce a dependency with Cutoff
213163
time <- use_ GetModificationTime file
214-
res <- liftIO $ ideTryIOException file $ do
215-
mbVirtual <- getVirtualFile vfs $ filePathToUri' file
164+
res <- do
165+
mbVirtual <- getVirtualFile file
216166
pure $ Rope.toText . _text <$> mbVirtual
217-
case res of
218-
Left err -> return ([err], Nothing)
219-
Right contents -> return ([], Just (time, contents))
167+
pure ([], Just (time, res))
220168

221169
ideTryIOException :: NormalizedFilePath -> IO a -> IO (Either FileDiagnostic a)
222170
ideTryIOException fp act =
@@ -240,11 +188,10 @@ getFileContents f = do
240188
pure $ posixSecondsToUTCTime posix
241189
return (modTime, txt)
242190

243-
fileStoreRules :: VFSHandle -> (NormalizedFilePath -> Action Bool) -> Rules ()
244-
fileStoreRules vfs isWatched = do
245-
addIdeGlobal vfs
246-
getModificationTimeRule vfs
247-
getFileContentsRule vfs
191+
fileStoreRules :: (NormalizedFilePath -> Action Bool) -> Rules ()
192+
fileStoreRules isWatched = do
193+
getModificationTimeRule
194+
getFileContentsRule
248195
addWatchedFileRule isWatched
249196

250197
-- | Note that some buffer for a specific file has been modified but not
@@ -260,9 +207,6 @@ setFileModified state saved nfp = do
260207
AlwaysCheck -> True
261208
CheckOnSave -> saved
262209
_ -> False
263-
VFSHandle{..} <- getIdeGlobalState state
264-
when (isJust setVirtualFileContents) $
265-
fail "setFileModified can't be called on this type of VFSHandle"
266210
join $ atomically $ recordDirtyKeys (shakeExtras state) GetModificationTime [nfp]
267211
restartShakeSession (shakeExtras state) (fromNormalizedFilePath nfp ++ " (modified)") []
268212
when checkParents $
@@ -289,9 +233,6 @@ typecheckParentsAction nfp = do
289233
-- independently tracks which files are modified.
290234
setSomethingModified :: IdeState -> [Key] -> String -> IO ()
291235
setSomethingModified state keys reason = do
292-
VFSHandle{..} <- getIdeGlobalState state
293-
when (isJust setVirtualFileContents) $
294-
fail "setSomethingModified can't be called on this type of VFSHandle"
295236
-- Update database to remove any files that might have been renamed/deleted
296237
atomically $ do
297238
writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles)
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
{-# LANGUAGE CPP #-}
2+
3+
module Development.IDE.Core.FileUtils(
4+
getModTime,
5+
) where
6+
7+
8+
import Data.Time.Clock.POSIX
9+
#ifdef mingw32_HOST_OS
10+
import qualified System.Directory as Dir
11+
#else
12+
import System.Posix.Files (getFileStatus,
13+
modificationTimeHiRes)
14+
#endif
15+
16+
-- Dir.getModificationTime is surprisingly slow since it performs
17+
-- a ton of conversions. Since we do not actually care about
18+
-- the format of the time, we can get away with something cheaper.
19+
-- For now, we only try to do this on Unix systems where it seems to get the
20+
-- time spent checking file modifications (which happens on every change)
21+
-- from > 0.5s to ~0.15s.
22+
-- We might also want to try speeding this up on Windows at some point.
23+
-- TODO leverage DidChangeWatchedFile lsp notifications on clients that
24+
-- support them, as done for GetFileExists
25+
getModTime :: FilePath -> IO POSIXTime
26+
getModTime f =
27+
#ifdef mingw32_HOST_OS
28+
utcTimeToPOSIXSeconds <$> Dir.getModificationTime f
29+
#else
30+
modificationTimeHiRes <$> getFileStatus f
31+
#endif

ghcide/src/Development/IDE/Core/RuleTypes.hs

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -290,10 +290,12 @@ pattern GetModificationTime = GetModificationTime_ {missingFileDiagnostics=True}
290290
-- | Get the modification time of a file.
291291
type instance RuleResult GetModificationTime = FileVersion
292292

293+
-- | Either athe mtime from disk or an LSP version
294+
-- LSP versions always compare as greater than on disk versions
293295
data FileVersion
294-
= VFSVersion !Int32
295-
| ModificationTime !POSIXTime
296-
deriving (Show, Generic)
296+
= ModificationTime !POSIXTime
297+
| VFSVersion !Int32
298+
deriving (Show, Generic, Eq, Ord)
297299

298300
instance NFData FileVersion
299301

ghcide/src/Development/IDE/Core/Rules.hs

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,7 @@ import qualified Data.HashMap.Strict as HM
8484
import qualified Data.HashSet as HashSet
8585
import Data.Hashable
8686
import Data.IORef
87+
import Control.Concurrent.STM.TVar
8788
import Data.IntMap.Strict (IntMap)
8889
import qualified Data.IntMap.Strict as IntMap
8990
import Data.List
@@ -98,8 +99,7 @@ import Data.Tuple.Extra
9899
import Development.IDE.Core.Compile
99100
import Development.IDE.Core.FileExists
100101
import Development.IDE.Core.FileStore (getFileContents,
101-
modificationTime,
102-
resetInterfaceStore)
102+
resetInterfaceStore, modificationTime)
103103
import Development.IDE.Core.IdeConfiguration
104104
import Development.IDE.Core.OfInterest
105105
import Development.IDE.Core.PositionMapping
@@ -524,12 +524,11 @@ getHieAstsRule =
524524
persistentHieFileRule :: Rules ()
525525
persistentHieFileRule = addPersistentRule GetHieAst $ \file -> runMaybeT $ do
526526
res <- readHieFileForSrcFromDisk file
527-
vfs <- asks vfs
528-
(currentSource,ver) <- liftIO $ do
529-
mvf <- getVirtualFile vfs $ filePathToUri' file
530-
case mvf of
531-
Nothing -> (,Nothing) . T.decodeUtf8 <$> BS.readFile (fromNormalizedFilePath file)
532-
Just vf -> pure (Rope.toText $ _text vf, Just $ _lsp_version vf)
527+
vfsRef <- asks vfs
528+
vfsData <- liftIO $ vfsMap <$> readTVarIO vfsRef
529+
(currentSource, ver) <- liftIO $ case M.lookup (filePathToUri' file) vfsData of
530+
Nothing -> (,Nothing) . T.decodeUtf8 <$> BS.readFile (fromNormalizedFilePath file)
531+
Just vf -> pure (Rope.toText $ _text vf, Just $ _lsp_version vf)
533532
let refmap = Compat.generateReferencesMap . Compat.getAsts . Compat.hie_asts $ res
534533
del = deltaFromDiff (T.decodeUtf8 $ Compat.hie_hs_src res) currentSource
535534
pure (HAR (Compat.hie_module res) (Compat.hie_asts res) refmap mempty (HieFromDisk res),del,ver)

0 commit comments

Comments
 (0)