From 4413e08252092b760effadaefce5e44dd91fdfd3 Mon Sep 17 00:00:00 2001 From: Curtis Chin Jen Sem Date: Fri, 14 Aug 2026 01:29:13 +0200 Subject: [PATCH] Use a unique id for each file watcher registration --- ghcide-test/exe/WatchedFileTests.hs | 19 +++++++++++++++++++ ghcide/src/Development/IDE/Core/FileStore.hs | 19 +++++++++++++++---- 2 files changed, 34 insertions(+), 4 deletions(-) diff --git a/ghcide-test/exe/WatchedFileTests.hs b/ghcide-test/exe/WatchedFileTests.hs index 18299109f0..be61de81fc 100644 --- a/ghcide-test/exe/WatchedFileTests.hs +++ b/ghcide-test/exe/WatchedFileTests.hs @@ -9,6 +9,7 @@ import Config (mkIdeTestFs, import Control.Applicative.Combinators import Control.Monad.IO.Class (liftIO) import qualified Data.Aeson as A +import Data.List (nub) import qualified Data.Text as T import qualified Data.Text.IO as T import Development.IDE.Plugin.Test (WaitForIdeRuleResult (..)) @@ -51,6 +52,15 @@ tests = testGroup "watched files" -- Expect 2 subscriptions: one for all .hs files and one for the hie.yaml cradle liftIO $ length watchedFileRegs @?= 2 + , testWithDummyPluginEmpty' "distinct registration ids" $ \sessionDir -> do + liftIO $ atomicFileWriteString (sessionDir "hie.yaml") "cradle: {direct: {arguments: [\"-isrc\", \"A\", \"WatchedFilesMissingModule\"]}}" + _doc <- createDoc "A.hs" "haskell" "{-#LANGUAGE NoImplicitPrelude #-}\nmodule A where\nimport WatchedFilesMissingModule" + setIgnoringRegistrationRequests False + ids <- getWatchedFilesRegistrationIdsUntil SMethod_TextDocumentPublishDiagnostics + + liftIO $ length ids @?= 2 + liftIO $ assertEqual "registration ids must be distinct" (nub ids) ids + -- TODO add a test for didChangeWorkspaceFolder ] , testGroup "Changes" @@ -190,6 +200,15 @@ tests = testGroup "watched files" ] ] +getWatchedFilesRegistrationIdsUntil :: forall m. SServerMethod m -> Session [T.Text] +getWatchedFilesRegistrationIdsUntil m = do + msgs <- manyTill (Just <$> message SMethod_ClientRegisterCapability <|> Nothing <$ anyMessage) (message m) + return + [ _id + | Just TRequestMessage{_params = RegistrationParams regs} <- msgs + , Registration _id "workspace/didChangeWatchedFiles" _ <- regs + ] + getWatchedFilesSubscriptionsUntil :: forall m. SServerMethod m -> Session [DidChangeWatchedFilesRegistrationOptions] getWatchedFilesSubscriptionsUntil m = do msgs <- manyTill (Just <$> message SMethod_ClientRegisterCapability <|> Nothing <$ anyMessage) (message m) diff --git a/ghcide/src/Development/IDE/Core/FileStore.hs b/ghcide/src/Development/IDE/Core/FileStore.hs index 7d253131d6..1bdd55d7f3 100644 --- a/ghcide/src/Development/IDE/Core/FileStore.hs +++ b/ghcide/src/Development/IDE/Core/FileStore.hs @@ -307,6 +307,18 @@ setSomethingModified vfs state reason actionBetweenSession = do atomically $ writeTaskQueue (indexQueue $ hiedbWriter $ shakeExtras state) (\withHieDb -> withHieDb deleteMissingRealFiles) void $ restartShakeSession (shakeExtras state) vfs reason [] actionBetweenSession +{- Note [Unique file watcher registration ids] + ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +We send more than one 'client/registerCapability' for watched files: one for +the global source globs, plus one foreach cradle dependency that the globs +do not already cover, see 'addWatchedFileRule'. These are logically distinct +registrations, so they must not share an id. + +LSP leaves the meaning of a repeated id undefined. emacs' eglot unregisters the +old watches before re-registering, while neovim and vscode accumulate. We derive +the id from the globs to keep them distinct and idempotent, while supporting +potentially de-registering in the future. +-} registerFileWatches :: [String] -> LSP.LspT Config IO Bool registerFileWatches globs = do watchSupported <- isWatchSupported @@ -314,12 +326,11 @@ registerFileWatches globs = do then do let regParams = LSP.RegistrationParams [toUntypedRegistration 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.TRegistration { _id ="globalFileWatches" + -- See Note [Unique file watcher registration ids] + registration = LSP.TRegistration { _id = registrationId , _method = LSP.SMethod_WorkspaceDidChangeWatchedFiles , _registerOptions = Just regOptions} + registrationId = "hls-file-watches:" <> Text.intercalate "," (map Text.pack globs) regOptions = DidChangeWatchedFilesRegistrationOptions { _watchers = watchers } -- See Note [File existence cache and LSP file watchers] for why this exists, and the choice of watch kind