Skip to content

Use file watches for all workspace files #1880

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Jun 1, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 11 additions & 16 deletions ghcide/src/Development/IDE/Core/FileExists.hs
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@ import Control.Monad.IO.Class
import qualified Data.ByteString as BS
import Data.HashMap.Strict (HashMap)
import qualified Data.HashMap.Strict as HashMap
import Data.List (partition)
import Data.Maybe
import Development.IDE.Core.FileStore
import Development.IDE.Core.IdeConfiguration
Expand All @@ -25,9 +26,9 @@ import Development.IDE.Core.Shake
import Development.IDE.Graph
import Development.IDE.Types.Location
import Development.IDE.Types.Options
import Ide.Plugin.Config (Config)
import Language.LSP.Server hiding (getVirtualFile)
import Language.LSP.Types
import Language.LSP.Types.Capabilities
import qualified System.Directory as Dir
import qualified System.FilePath.Glob as Glob

Expand Down Expand Up @@ -91,22 +92,23 @@ modifyFileExists :: IdeState -> [FileEvent] -> IO ()
modifyFileExists state changes = do
FileExistsMapVar var <- getIdeGlobalState state
changesMap <- evaluate $ HashMap.fromList $
[ (toNormalizedFilePath' f, newState)
[ (toNormalizedFilePath' f, change)
| FileEvent uri change <- changes
, Just f <- [uriToFilePath uri]
, Just newState <- [fromChange change]
]
-- Masked to ensure that the previous values are flushed together with the map update
mask $ \_ -> do
-- update the map
void $ modifyVar' var $ HashMap.union changesMap
void $ modifyVar' var $ HashMap.union (HashMap.mapMaybe fromChange changesMap)
-- See Note [Invalidating file existence results]
-- flush previous values
mapM_ (deleteValue (shakeExtras state) GetFileExists) (HashMap.keys changesMap)
let (_fileModifChanges, fileExistChanges) =
partition ((== FcChanged) . snd) (HashMap.toList changesMap)
mapM_ (deleteValue (shakeExtras state) GetFileExists . fst) fileExistChanges

fromChange :: FileChangeType -> Maybe Bool
fromChange FcCreated = Just True
fromChange FcDeleted = Just True
fromChange FcDeleted = Just False
fromChange FcChanged = Nothing

-------------------------------------------------------------------------------------
Expand Down Expand Up @@ -153,18 +155,11 @@ allExtensions opts = [extIncBoot | ext <- optExtensions opts, extIncBoot <- [ext
-- | Installs the 'getFileExists' rules.
-- Provides a fast implementation if client supports dynamic watched files.
-- Creates a global state as a side effect in that case.
fileExistsRules :: Maybe (LanguageContextEnv c) -> VFSHandle -> Rules ()
fileExistsRules :: Maybe (LanguageContextEnv Config) -> VFSHandle -> Rules ()
fileExistsRules lspEnv vfs = do
supportsWatchedFiles <- case lspEnv of
Just lspEnv' -> liftIO $ runLspT lspEnv' $ do
ClientCapabilities {_workspace} <- getClientCapabilities
case () of
_ | Just WorkspaceClientCapabilities{_didChangeWatchedFiles} <- _workspace
, Just DidChangeWatchedFilesClientCapabilities{_dynamicRegistration} <- _didChangeWatchedFiles
, Just True <- _dynamicRegistration
-> pure True
_ -> pure False
Nothing -> pure False
Nothing -> pure False
Just lspEnv' -> liftIO $ runLspT lspEnv' isWatchSupported
-- Create the global always, although it should only be used if we have fast rules.
-- But there's a chance someone will send unexpected notifications anyway,
-- e.g. https://github.com/haskell/ghcide/issues/599
Expand Down
134 changes: 98 additions & 36 deletions ghcide/src/Development/IDE/Core/FileStore.hs
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,9 @@ module Development.IDE.Core.FileStore(
getModificationTimeImpl,
addIdeGlobal,
getFileContentsImpl,
getModTime
getModTime,
isWatchSupported,
registerFileWatches
) where

import Control.Concurrent.STM (atomically)
Expand Down Expand Up @@ -49,7 +51,8 @@ import Development.IDE.Types.Diagnostics
import Development.IDE.Types.Location
import Development.IDE.Types.Options
import HieDb.Create (deleteMissingRealFiles)
import Ide.Plugin.Config (CheckParents (..))
import Ide.Plugin.Config (CheckParents (..),
Config)
import System.IO.Error

#ifdef mingw32_HOST_OS
Expand All @@ -63,13 +66,21 @@ import qualified Development.IDE.Types.Logger as L

import qualified Data.Binary as B
import qualified Data.ByteString.Lazy as LBS
import qualified Data.Text as Text
import Development.IDE.Core.IdeConfiguration (isWorkspaceFile)
import Language.LSP.Server hiding
(getVirtualFile)
import qualified Language.LSP.Server as LSP
import Language.LSP.Types (FileChangeType (FcChanged),
import Language.LSP.Types (DidChangeWatchedFilesRegistrationOptions (DidChangeWatchedFilesRegistrationOptions),
FileChangeType (FcChanged),
FileEvent (FileEvent),
FileSystemWatcher (..),
WatchKind (..),
_watchers,
toNormalizedFilePath,
uriToFilePath)
import qualified Language.LSP.Types as LSP
import qualified Language.LSP.Types.Capabilities as LSP
import Language.LSP.VFS
import System.FilePath

Expand All @@ -94,6 +105,17 @@ makeLSPVFSHandle lspEnv = VFSHandle
, setVirtualFileContents = Nothing
}

addWatchedFileRule :: (NormalizedFilePath -> Action Bool) -> Rules ()
addWatchedFileRule isWatched = defineNoDiagnostics $ \AddWatchedFile f -> do
isAlreadyWatched <- isWatched f
isWp <- isWorkspaceFile f
if isAlreadyWatched then pure (Just True) else
if not isWp then pure (Just False) else do
ShakeExtras{lspEnv} <- getShakeExtras
case lspEnv of
Just env -> fmap Just $ liftIO $ LSP.runLspT env $
registerFileWatches [fromNormalizedFilePath f]
Nothing -> pure $ Just False

isFileOfInterestRule :: Rules ()
isFileOfInterestRule = defineEarlyCutoff $ RuleNoDiagnostics $ \IsFileOfInterest f -> do
Expand All @@ -109,45 +131,44 @@ isFileOfInterestRule = defineEarlyCutoff $ RuleNoDiagnostics $ \IsFileOfInterest
summarize (IsFOI (Modified True)) = BS.singleton 3


getModificationTimeRule :: VFSHandle -> (NormalizedFilePath -> Action Bool) -> Rules ()
getModificationTimeRule vfs isWatched = defineEarlyCutoff $ Rule $ \(GetModificationTime_ missingFileDiags) file ->
getModificationTimeImpl vfs isWatched missingFileDiags file
getModificationTimeRule :: VFSHandle -> Rules ()
getModificationTimeRule vfs = defineEarlyCutoff $ Rule $ \(GetModificationTime_ missingFileDiags) file ->
getModificationTimeImpl vfs missingFileDiags file

getModificationTimeImpl :: VFSHandle
-> (NormalizedFilePath -> Action Bool)
-> Bool
-> NormalizedFilePath
-> Action
(Maybe BS.ByteString, ([FileDiagnostic], Maybe FileVersion))
getModificationTimeImpl vfs isWatched missingFileDiags file = do
let file' = fromNormalizedFilePath file
let wrap time = (Just $ LBS.toStrict $ B.encode $ toRational time, ([], Just $ ModificationTime time))
mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri' file
case mbVirtual of
Just (virtualFileVersion -> ver) -> do
alwaysRerun
pure (Just $ LBS.toStrict $ B.encode ver, ([], Just $ VFSVersion ver))
Nothing -> do
isWF <- isWatched file
if isWF
then -- the file is watched so we can rely on FileWatched notifications,
-- but also need a dependency on IsFileOfInterest to reinstall
-- alwaysRerun when the file becomes VFS
void (use_ IsFileOfInterest file)
else if isInterface file
then -- interface files are tracked specially using the closed world assumption
pure ()
else -- in all other cases we will need to freshly check the file system
alwaysRerun
getModificationTimeImpl vfs missingFileDiags file = do
let file' = fromNormalizedFilePath file
let wrap time = (Just $ LBS.toStrict $ B.encode $ toRational time, ([], Just $ ModificationTime time))
mbVirtual <- liftIO $ getVirtualFile vfs $ filePathToUri' file
case mbVirtual of
Just (virtualFileVersion -> ver) -> do
alwaysRerun
pure (Just $ LBS.toStrict $ B.encode ver, ([], Just $ VFSVersion ver))
Nothing -> do
isWF <- use_ AddWatchedFile file
if isWF
then -- the file is watched so we can rely on FileWatched notifications,
-- but also need a dependency on IsFileOfInterest to reinstall
-- alwaysRerun when the file becomes VFS
void (use_ IsFileOfInterest file)
else if isInterface file
then -- interface files are tracked specially using the closed world assumption
pure ()
else -- in all other cases we will need to freshly check the file system
alwaysRerun

liftIO $ fmap wrap (getModTime file')
`catch` \(e :: IOException) -> do
let err | isDoesNotExistError e = "File does not exist: " ++ file'
| otherwise = "IO error while reading " ++ file' ++ ", " ++ displayException e
diag = ideErrorText file (T.pack err)
if isDoesNotExistError e && not missingFileDiags
then return (Nothing, ([], Nothing))
else return (Nothing, ([diag], Nothing))
liftIO $ fmap wrap (getModTime file')
`catch` \(e :: IOException) -> do
let err | isDoesNotExistError e = "File does not exist: " ++ file'
| otherwise = "IO error while reading " ++ file' ++ ", " ++ displayException e
diag = ideErrorText file (T.pack err)
if isDoesNotExistError e && not missingFileDiags
then return (Nothing, ([], Nothing))
else return (Nothing, ([diag], Nothing))

-- | Interface files cannot be watched, since they live outside the workspace.
-- But interface files are private, in that only HLS writes them.
Expand Down Expand Up @@ -239,9 +260,10 @@ getFileContents f = do
fileStoreRules :: VFSHandle -> (NormalizedFilePath -> Action Bool) -> Rules ()
fileStoreRules vfs isWatched = do
addIdeGlobal vfs
getModificationTimeRule vfs isWatched
getModificationTimeRule vfs
getFileContentsRule vfs
isFileOfInterestRule
addWatchedFileRule isWatched

-- | Note that some buffer for a specific file has been modified but not
-- with what changes.
Expand Down Expand Up @@ -290,3 +312,43 @@ setSomethingModified state = do
-- Update database to remove any files that might have been renamed/deleted
atomically $ writeTQueue (indexQueue $ hiedbWriter $ shakeExtras state) deleteMissingRealFiles
void $ shakeRestart state []

registerFileWatches :: [String] -> LSP.LspT Config IO Bool
registerFileWatches globs = do
watchSupported <- isWatchSupported
if watchSupported
then do
let
regParams = LSP.RegistrationParams (List [LSP.SomeRegistration registration])
-- The registration ID is arbitrary and is only used in case we want to deregister (which we won't).
-- We could also use something like a random UUID, as some other servers do, but this works for
-- our purposes.
registration = LSP.Registration "globalFileWatches"
LSP.SWorkspaceDidChangeWatchedFiles
regOptions
regOptions =
DidChangeWatchedFilesRegistrationOptions { _watchers = List watchers }
-- See Note [File existence cache and LSP file watchers] for why this exists, and the choice of watch kind
watchKind = WatchKind { _watchCreate = True, _watchChange = True, _watchDelete = True}
-- See Note [Which files should we watch?] for an explanation of why the pattern is the way that it is
-- The patterns will be something like "**/.hs", i.e. "any number of directory segments,
-- followed by a file with an extension 'hs'.
watcher glob = FileSystemWatcher { _globPattern = glob, _kind = Just watchKind }
-- We use multiple watchers instead of one using '{}' because lsp-test doesn't
-- support that: https://github.com/bubba/lsp-test/issues/77
watchers = [ watcher (Text.pack glob) | glob <- globs ]

void $ LSP.sendRequest LSP.SClientRegisterCapability regParams (const $ pure ())
return True
else return False

isWatchSupported :: LSP.LspT Config IO Bool
isWatchSupported = do
clientCapabilities <- LSP.getClientCapabilities
pure $ case () of
_ | LSP.ClientCapabilities{_workspace} <- clientCapabilities
, Just LSP.WorkspaceClientCapabilities{_didChangeWatchedFiles} <- _workspace
, Just LSP.DidChangeWatchedFilesClientCapabilities{_dynamicRegistration} <- _didChangeWatchedFiles
, Just True <- _dynamicRegistration
-> True
| otherwise -> False
8 changes: 8 additions & 0 deletions ghcide/src/Development/IDE/Core/RuleTypes.hs
Original file line number Diff line number Diff line change
Expand Up @@ -267,6 +267,8 @@ type instance RuleResult GetFileContents = (FileVersion, Maybe Text)

type instance RuleResult GetFileExists = Bool

type instance RuleResult AddWatchedFile = Bool


-- The Shake key type for getModificationTime queries
newtype GetModificationTime = GetModificationTime_
Expand Down Expand Up @@ -493,6 +495,12 @@ instance Binary GetClientSettings

type instance RuleResult GetClientSettings = Hashed (Maybe Value)

data AddWatchedFile = AddWatchedFile deriving (Eq, Show, Typeable, Generic)
instance Hashable AddWatchedFile
instance NFData AddWatchedFile
instance Binary AddWatchedFile


-- A local rule type to get caching. We want to use newCache, but it has
-- thread killed exception issues, so we lift it to a full rule.
-- https://github.com/digital-asset/daml/pull/2808#issuecomment-529639547
Expand Down
48 changes: 12 additions & 36 deletions ghcide/src/Development/IDE/LSP/Notifications.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@ module Development.IDE.LSP.Notifications
, descriptor
) where

import qualified Language.LSP.Server as LSP
import Language.LSP.Types
import qualified Language.LSP.Types as LSP
import qualified Language.LSP.Types.Capabilities as LSP

import Development.IDE.Core.IdeConfiguration
import Development.IDE.Core.Service
Expand All @@ -31,7 +29,8 @@ import qualified Data.Text as Text
import Control.Monad.IO.Class
import Development.IDE.Core.FileExists (modifyFileExists,
watchedGlobs)
import Development.IDE.Core.FileStore (resetFileStore,
import Development.IDE.Core.FileStore (registerFileWatches,
resetFileStore,
setFileModified,
setSomethingModified,
typecheckParents)
Expand Down Expand Up @@ -108,38 +107,15 @@ descriptor plId = (defaultPluginDescriptor plId) { pluginNotificationHandlers =
liftIO $ shakeSessionInit ide

--------- Set up file watchers ------------------------------------------------------------------------
clientCapabilities <- LSP.getClientCapabilities
let watchSupported = case () of
_ | LSP.ClientCapabilities{_workspace} <- clientCapabilities
, Just LSP.WorkspaceClientCapabilities{_didChangeWatchedFiles} <- _workspace
, Just LSP.DidChangeWatchedFilesClientCapabilities{_dynamicRegistration} <- _didChangeWatchedFiles
, Just True <- _dynamicRegistration
-> True
| otherwise -> False
if watchSupported
then do
opts <- liftIO $ getIdeOptionsIO $ shakeExtras ide
let
regParams = RegistrationParams (List [SomeRegistration registration])
-- The registration ID is arbitrary and is only used in case we want to deregister (which we won't).
-- We could also use something like a random UUID, as some other servers do, but this works for
-- our purposes.
registration = Registration "globalFileWatches"
SWorkspaceDidChangeWatchedFiles
regOptions
regOptions =
DidChangeWatchedFilesRegistrationOptions { _watchers = List watchers }
-- See Note [File existence cache and LSP file watchers] for why this exists, and the choice of watch kind
watchKind = WatchKind { _watchCreate = True, _watchChange = True, _watchDelete = True}
-- See Note [Which files should we watch?] for an explanation of why the pattern is the way that it is
-- The patterns will be something like "**/.hs", i.e. "any number of directory segments,
-- followed by a file with an extension 'hs'.
watcher glob = FileSystemWatcher { _globPattern = glob, _kind = Just watchKind }
-- We use multiple watchers instead of one using '{}' because lsp-test doesn't
-- support that: https://github.com/bubba/lsp-test/issues/77
watchers = [ watcher (Text.pack glob) | glob <- watchedGlobs opts ]

void $ LSP.sendRequest SClientRegisterCapability regParams (const $ pure ()) -- TODO handle response
else liftIO $ logDebug (ideLogger ide) "Warning: Client does not support watched files. Falling back to OS polling"
opts <- liftIO $ getIdeOptionsIO $ shakeExtras ide
-- See Note [Which files should we watch?] for an explanation of why the pattern is the way that it is
-- The patterns will be something like "**/.hs", i.e. "any number of directory segments,
-- followed by a file with an extension 'hs'.
-- We use multiple watchers instead of one using '{}' because lsp-test doesn't
-- support that: https://github.com/bubba/lsp-test/issues/77
let globs = watchedGlobs opts
success <- registerFileWatches globs
unless success $
liftIO $ logDebug (ideLogger ide) "Warning: Client does not support watched files. Falling back to OS polling"
]
}
Loading