Skip to content

Commit 13cba17

Browse files
authored
Always try to register for didChangeConfiguration (#548)
* Register for workspace/didChangeConfiguration always The new direction of the spec seems to be that you should always do this. In any case, it doesn't hurt. `lsp-test` has some changes to adapt to the increased registration message spam, and also changes to be a more picky client, refusing to send notifications unless you _do_ register. This is good, since it forces us to handle the most annoying version of client behaviour. * Add bound on extra * fix
1 parent 52607f1 commit 13cba17

File tree

11 files changed

+160
-49
lines changed

11 files changed

+160
-49
lines changed

lsp-test/ChangeLog.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
# Revision history for lsp-test
22

3+
## Unreleased
4+
5+
- `ignoreRegistrationRequests` option to ignore `client/registerCapability` requests, on
6+
by default.
7+
- New functions `setIgnoringRegistrationRequests` to change whether such messages are
8+
ignored during a `Session` without having to change the `SessionConfig`.
9+
- `lsp-test` will no longer send `workspace/didChangConfiguration` notifications unless
10+
the server dynamically registers for them.
11+
312
## 0.16.0.1
413

514
- Support newer versions of dependencies.

lsp-test/lsp-test.cabal

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,7 @@ library
5656
, Diff >=0.4 && <0.6
5757
, directory ^>=1.3
5858
, exceptions ^>=0.10
59+
, extra ^>=1.7
5960
, filepath >=1.4 && < 1.6
6061
, Glob >=0.9 && <0.11
6162
, lens >=5.1 && <5.3
@@ -101,6 +102,7 @@ test-suite tests
101102
, containers
102103
, data-default
103104
, directory
105+
, extra
104106
, filepath
105107
, hspec
106108
, lens

lsp-test/src/Language/LSP/Test.hs

Lines changed: 27 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module Language.LSP.Test (
2525
runSessionWithHandles',
2626
setIgnoringLogNotifications,
2727
setIgnoringConfigurationRequests,
28+
setIgnoringRegistrationRequests,
2829

2930
-- ** Config
3031
SessionConfig (..),
@@ -144,8 +145,10 @@ import Control.Monad.IO.Class
144145
import Control.Monad.State (execState)
145146
import Data.Aeson hiding (Null)
146147
import Data.Aeson qualified as J
148+
import Data.Aeson.KeyMap qualified as J
147149
import Data.Default
148150
import Data.List
151+
import Data.List.Extra (firstJust)
149152
import Data.Map.Strict qualified as Map
150153
import Data.Maybe
151154
import Data.Set qualified as Set
@@ -476,6 +479,10 @@ setIgnoringConfigurationRequests :: Bool -> Session ()
476479
setIgnoringConfigurationRequests value = do
477480
modify (\ss -> ss{ignoringConfigurationRequests = value})
478481

482+
setIgnoringRegistrationRequests :: Bool -> Session ()
483+
setIgnoringRegistrationRequests value = do
484+
modify (\ss -> ss{ignoringRegistrationRequests = value})
485+
479486
{- | Modify the client config. This will send a notification to the server that the
480487
config has changed.
481488
-}
@@ -485,12 +492,26 @@ modifyConfig f = do
485492
let newConfig = f oldConfig
486493
modify (\ss -> ss{curLspConfig = newConfig})
487494

488-
caps <- asks sessionCapabilities
489-
let supportsConfiguration = fromMaybe False $ caps ^? L.workspace . _Just . L.configuration . _Just
490-
-- TODO: make this configurable?
491-
-- if they support workspace/configuration then be annoying and don't send the full config so
492-
-- they have to request it
493-
configToSend = if supportsConfiguration then J.Null else Object newConfig
495+
-- We're going to be difficult and follow the new direction of the spec as much
496+
-- as possible. That means _not_ sending didChangeConfiguration notifications
497+
-- unless the server has registered for them
498+
registeredCaps <- getRegisteredCapabilities
499+
let
500+
requestedSections :: Maybe [T.Text]
501+
requestedSections = flip firstJust registeredCaps $ \(SomeRegistration (TRegistration _ regMethod regOpts)) ->
502+
case regMethod of
503+
SMethod_WorkspaceDidChangeConfiguration -> case regOpts of
504+
Just (DidChangeConfigurationRegistrationOptions{_section = section}) -> case section of
505+
Just (InL s) -> Just [s]
506+
Just (InR ss) -> Just ss
507+
Nothing -> Nothing
508+
_ -> Nothing
509+
_ -> Nothing
510+
requestedSectionKeys :: Maybe [J.Key]
511+
requestedSectionKeys = (fmap . fmap) (fromString . T.unpack) requestedSections
512+
let configToSend = case requestedSectionKeys of
513+
Just ss -> Object $ J.filterWithKey (\k _ -> k `elem` ss) newConfig
514+
Nothing -> Object newConfig
494515
sendNotification SMethod_WorkspaceDidChangeConfiguration $ DidChangeConfigurationParams configToSend
495516

496517
{- | Set the client config. This will send a notification to the server that the

lsp-test/src/Language/LSP/Test/Session.hs

Lines changed: 34 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -123,14 +123,17 @@ data SessionConfig = SessionConfig
123123
, ignoreConfigurationRequests :: Bool
124124
-- ^ Whether or not to ignore @workspace/configuration@ requests from the server,
125125
-- defaults to True.
126+
, ignoreRegistrationRequests :: Bool
127+
-- ^ Whether or not to ignore @client/registerCapability@ and @client/unregisterCapability@
128+
-- requests from the server, defaults to True.
126129
, initialWorkspaceFolders :: Maybe [WorkspaceFolder]
127130
-- ^ The initial workspace folders to send in the @initialize@ request.
128131
-- Defaults to Nothing.
129132
}
130133

131134
-- | The configuration used in 'Language.LSP.Test.runSession'.
132135
defaultConfig :: SessionConfig
133-
defaultConfig = SessionConfig 60 False False True mempty True True Nothing
136+
defaultConfig = SessionConfig 60 False False True mempty True True True Nothing
134137

135138
instance Default SessionConfig where
136139
def = defaultConfig
@@ -190,6 +193,7 @@ data SessionState = SessionState
190193
, curProgressSessions :: !(Set.Set ProgressToken)
191194
, ignoringLogNotifications :: Bool
192195
, ignoringConfigurationRequests :: Bool
196+
, ignoringRegistrationRequests :: Bool
193197
}
194198

195199
class Monad m => HasState s m where
@@ -274,8 +278,27 @@ runSession' serverIn serverOut mServerProc serverHandler config caps rootDir exi
274278

275279
mainThreadId <- myThreadId
276280

277-
let context = SessionContext serverIn absRootDir messageChan timeoutIdVar reqMap initRsp config caps
278-
initState = SessionState 0 emptyVFS mempty False Nothing mempty (lspConfig config) mempty (ignoreLogNotifications config) (ignoreConfigurationRequests config)
281+
let context = SessionContext
282+
serverIn
283+
absRootDir
284+
messageChan
285+
timeoutIdVar
286+
reqMap
287+
initRsp
288+
config
289+
caps
290+
initState = SessionState
291+
0
292+
emptyVFS
293+
mempty
294+
False
295+
Nothing
296+
mempty
297+
(lspConfig config)
298+
mempty
299+
(ignoreLogNotifications config)
300+
(ignoreConfigurationRequests config)
301+
(ignoreRegistrationRequests config)
279302
runSession' = runSessionMonad context initState
280303

281304
errorHandler = throwTo mainThreadId :: SessionException -> IO ()
@@ -328,7 +351,10 @@ updateStateC = awaitForever $ \msg -> do
328351
then (Right configs)
329352
else Left $ ResponseError (InL LSPErrorCodes_RequestFailed) ("No configuration for requested sections: " <> (T.pack $ show errs)) Nothing
330353
_ -> pure ()
331-
unless ((ignoringLogNotifications state && isLogNotification msg) || (ignoringConfigurationRequests state && isConfigRequest msg)) $
354+
unless (
355+
(ignoringLogNotifications state && isLogNotification msg)
356+
|| (ignoringConfigurationRequests state && isConfigRequest msg)
357+
|| (ignoringRegistrationRequests state && isRegistrationRequest msg)) $
332358
yield msg
333359

334360
where
@@ -341,6 +367,10 @@ updateStateC = awaitForever $ \msg -> do
341367
isConfigRequest (FromServerMess SMethod_WorkspaceConfiguration _) = True
342368
isConfigRequest _ = False
343369

370+
isRegistrationRequest (FromServerMess SMethod_ClientRegisterCapability _) = True
371+
isRegistrationRequest (FromServerMess SMethod_ClientUnregisterCapability _) = True
372+
isRegistrationRequest _ = False
373+
344374
-- extract Uri out from DocumentChange
345375
-- didn't put this in `lsp-types` because TH was getting in the way
346376
documentChangeUri :: DocumentChange -> Uri

lsp-test/test/DummyServer.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,7 @@ handlers =
123123
(Just WatchKind_Create)
124124
]
125125
Just token <- runInIO $
126-
registerCapability SMethod_WorkspaceDidChangeWatchedFiles regOpts $
126+
registerCapability mempty SMethod_WorkspaceDidChangeWatchedFiles regOpts $
127127
\_noti ->
128128
sendNotification SMethod_WindowLogMessage $
129129
LogMessageParams MessageType_Log "got workspace/didChangeWatchedFiles"
@@ -138,7 +138,7 @@ handlers =
138138
(Just WatchKind_Create)
139139
]
140140
Just token <- runInIO $
141-
registerCapability SMethod_WorkspaceDidChangeWatchedFiles regOpts $
141+
registerCapability mempty SMethod_WorkspaceDidChangeWatchedFiles regOpts $
142142
\_noti ->
143143
sendNotification SMethod_WindowLogMessage $
144144
LogMessageParams MessageType_Log "got workspace/didChangeWatchedFiles"

lsp-test/test/Test.hs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,11 @@ import Data.Aeson
1212
import Data.Aeson qualified as J
1313
import Data.Default
1414
import Data.Either
15+
import Data.List.Extra
1516
import Data.Map.Strict qualified as M
1617
import Data.Maybe
1718
import Data.Proxy
1819
import Data.Text qualified as T
19-
import Data.Type.Equality
2020
import DummyServer
2121
import Language.LSP.Protocol.Lens qualified as L
2222
import Language.LSP.Protocol.Message
@@ -368,25 +368,31 @@ main = hspec $ around withDummyServer $ do
368368
void publishDiagnosticsNotification
369369

370370
describe "dynamic capabilities" $ do
371-
it "keeps track" $ \(hin, hout) -> runSessionWithHandles hin hout (def{ignoreLogNotifications = False}) fullCaps "." $ do
371+
let config = def{ignoreLogNotifications = False}
372+
it "keeps track" $ \(hin, hout) -> runSessionWithHandles hin hout config fullCaps "." $ do
372373
loggingNotification -- initialized log message
373374
createDoc ".register" "haskell" ""
375+
setIgnoringRegistrationRequests False
374376
message SMethod_ClientRegisterCapability
375377

376378
doc <- createDoc "Foo.watch" "haskell" ""
377379
msg <- message SMethod_WindowLogMessage
378380
liftIO $ msg ^. L.params . L.message `shouldBe` "got workspace/didChangeWatchedFiles"
379381

380-
[SomeRegistration (TRegistration _ regMethod regOpts)] <- getRegisteredCapabilities
381-
liftIO $ do
382-
case regMethod `mEqClient` SMethod_WorkspaceDidChangeWatchedFiles of
383-
Just (Right HRefl) ->
384-
regOpts
385-
`shouldBe` ( Just $
386-
DidChangeWatchedFilesRegistrationOptions
387-
[FileSystemWatcher (GlobPattern $ InL $ Pattern "*.watch") (Just WatchKind_Create)]
388-
)
389-
_ -> expectationFailure "Registration wasn't on workspace/didChangeWatchedFiles"
382+
-- Look for the registration, we might have one for didChangeConfiguration in there too
383+
registeredCaps <- getRegisteredCapabilities
384+
let
385+
regOpts :: Maybe DidChangeWatchedFilesRegistrationOptions
386+
regOpts = flip firstJust registeredCaps $ \(SomeRegistration (TRegistration _ regMethod regOpts)) ->
387+
case regMethod of
388+
SMethod_WorkspaceDidChangeWatchedFiles -> regOpts
389+
_ -> Nothing
390+
liftIO $
391+
regOpts
392+
`shouldBe` ( Just $
393+
DidChangeWatchedFilesRegistrationOptions
394+
[FileSystemWatcher (GlobPattern $ InL $ Pattern "*.watch") (Just WatchKind_Create)]
395+
)
390396

391397
-- now unregister it by sending a specific createDoc
392398
createDoc ".unregister" "haskell" ""
@@ -396,10 +402,11 @@ main = hspec $ around withDummyServer $ do
396402
void $ sendRequest SMethod_TextDocumentHover $ HoverParams doc (Position 0 0) Nothing
397403
void $ anyResponse
398404

399-
it "handles absolute patterns" $ \(hin, hout) -> runSessionWithHandles hin hout (def{ignoreLogNotifications = False}) fullCaps "" $ do
405+
it "handles absolute patterns" $ \(hin, hout) -> runSessionWithHandles hin hout config fullCaps "" $ do
400406
loggingNotification -- initialized log message
401407
curDir <- liftIO $ getCurrentDirectory
402408

409+
setIgnoringRegistrationRequests False
403410
createDoc ".register.abs" "haskell" ""
404411
message SMethod_ClientRegisterCapability
405412

lsp/ChangeLog.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@
88
so client-initiated progress can now be supported properly.
99
- The server options now allow the user to say whether the server should advertise
1010
support for client-initiated progress or not.
11+
- The server now dynamically registers for `workspace/didChangeConfiguration`
12+
notifications, to ensure that newer clients continue to send them.
1113

1214
## 2.3.0.0
1315

lsp/example/Reactor.hs

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -218,11 +218,16 @@ handle logger =
218218

219219
let regOpts = LSP.CodeLensRegistrationOptions (LSP.InR LSP.Null) Nothing (Just False)
220220

221-
void $ registerCapability LSP.SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do
222-
logger <& "Processing a textDocument/codeLens request" `WithSeverity` Info
223-
let cmd = LSP.Command "Say hello" "lsp-hello-command" Nothing
224-
rsp = [LSP.CodeLens (LSP.mkRange 0 0 0 100) (Just cmd) Nothing]
225-
responder (Right $ LSP.InL rsp)
221+
void
222+
$ registerCapability
223+
mempty
224+
LSP.SMethod_TextDocumentCodeLens
225+
regOpts
226+
$ \_req responder -> do
227+
logger <& "Processing a textDocument/codeLens request" `WithSeverity` Info
228+
let cmd = LSP.Command "Say hello" "lsp-hello-command" Nothing
229+
rsp = [LSP.CodeLens (LSP.mkRange 0 0 0 100) (Just cmd) Nothing]
230+
responder (Right $ LSP.InL rsp)
226231
, notificationHandler LSP.SMethod_TextDocumentDidOpen $ \msg -> do
227232
let doc = msg ^. LSP.params . LSP.textDocument . LSP.uri
228233
fileName = LSP.uriToFilePath doc

lsp/example/Simple.hs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ handlers =
2121
Right (InL (MessageActionItem "Turn on")) -> do
2222
let regOpts = CodeLensRegistrationOptions (InR Null) Nothing (Just False)
2323

24-
_ <- registerCapability SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do
24+
_ <- registerCapability mempty SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do
2525
let cmd = Command "Say hello" "lsp-hello-command" Nothing
2626
rsp = [CodeLens (mkRange 0 0 0 100) (Just cmd) Nothing]
2727
responder $ Right $ InL rsp

lsp/src/Language/LSP/Server/Core.hs

Lines changed: 39 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,9 @@ data LspCoreLog
8282
| ConfigurationNotSupported
8383
| BadConfigurationResponse ResponseError
8484
| WrongConfigSections [J.Value]
85-
deriving (Show)
85+
| forall m. CantRegister (SMethod m)
86+
87+
deriving instance (Show LspCoreLog)
8688

8789
instance Pretty LspCoreLog where
8890
pretty (NewConfig config) = "LSP: set new config:" <+> prettyJSON config
@@ -96,6 +98,7 @@ instance Pretty LspCoreLog where
9698
]
9799
pretty (BadConfigurationResponse err) = "LSP: error when requesting configuration: " <+> pretty err
98100
pretty (WrongConfigSections sections) = "LSP: expected only one configuration section, got: " <+> (prettyJSON $ J.toJSON sections)
101+
pretty (CantRegister m) = "LSP: can't register dynamically for:" <+> pretty m
99102

100103
newtype LspT config m a = LspT {unLspT :: ReaderT (LanguageContextEnv config) m a}
101104
deriving (Functor, Applicative, Monad, MonadCatch, MonadIO, MonadMask, MonadThrow, MonadTrans, MonadUnliftIO, MonadFix)
@@ -550,30 +553,27 @@ getWorkspaceFolders = do
550553
registerCapability ::
551554
forall f t (m :: Method ClientToServer t) config.
552555
MonadLsp config f =>
556+
LogAction f (WithSeverity LspCoreLog) ->
553557
SClientMethod m ->
554558
RegistrationOptions m ->
555559
Handler f m ->
556560
f (Maybe (RegistrationToken m))
557-
registerCapability method regOpts f = do
558-
clientCaps <- resClientCapabilities <$> getLspEnv
561+
registerCapability logger method regOpts f = do
559562
handlers <- resHandlers <$> getLspEnv
560563
let alreadyStaticallyRegistered = case splitClientMethod method of
561564
IsClientNot -> SMethodMap.member method $ notHandlers handlers
562565
IsClientReq -> SMethodMap.member method $ reqHandlers handlers
563566
IsClientEither -> error "Cannot register capability for custom methods"
564-
go clientCaps alreadyStaticallyRegistered
567+
go alreadyStaticallyRegistered
565568
where
566569
-- If the server has already registered statically, don't dynamically register
567570
-- as per the spec
568-
go _clientCaps True = pure Nothing
569-
go clientCaps False
570-
-- First, check to see if the client supports dynamic registration on this method
571-
| dynamicRegistrationSupported method clientCaps = do
572-
uuid <- liftIO $ UUID.toText <$> getStdRandom random
573-
let registration = L.TRegistration uuid method (Just regOpts)
574-
params = L.RegistrationParams [toUntypedRegistration registration]
575-
regId = RegistrationId uuid
576-
rio <- askUnliftIO
571+
go True = pure Nothing
572+
go False = do
573+
rio <- askUnliftIO
574+
mtoken <- trySendRegistration logger method regOpts
575+
case mtoken of
576+
Just token@(RegistrationToken _ regId) -> do
577577
~() <- case splitClientMethod method of
578578
IsClientNot -> modifyState resRegistrationsNot $ \oldRegs ->
579579
let pair = Pair regId (ClientMessageHandler (unliftIO rio . f))
@@ -583,11 +583,33 @@ registerCapability method regOpts f = do
583583
in SMethodMap.insert method pair oldRegs
584584
IsClientEither -> error "Cannot register capability for custom methods"
585585

586-
-- TODO: handle the scenario where this returns an error
587-
_ <- sendRequest SMethod_ClientRegisterCapability params $ \_res -> pure ()
586+
pure $ Just token
587+
Nothing -> pure Nothing
588588

589-
pure (Just (RegistrationToken method regId))
590-
| otherwise = pure Nothing
589+
trySendRegistration ::
590+
forall f t (m :: Method ClientToServer t) config.
591+
MonadLsp config f =>
592+
LogAction f (WithSeverity LspCoreLog) ->
593+
SClientMethod m ->
594+
RegistrationOptions m ->
595+
f (Maybe (RegistrationToken m))
596+
trySendRegistration logger method regOpts = do
597+
clientCaps <- resClientCapabilities <$> getLspEnv
598+
-- First, check to see if the client supports dynamic registration on this method
599+
if dynamicRegistrationSupported method clientCaps
600+
then do
601+
uuid <- liftIO $ UUID.toText <$> getStdRandom random
602+
let registration = L.TRegistration uuid method (Just regOpts)
603+
params = L.RegistrationParams [toUntypedRegistration registration]
604+
regId = RegistrationId uuid
605+
606+
-- TODO: handle the scenario where this returns an error
607+
_ <- sendRequest SMethod_ClientRegisterCapability params $ \_res -> pure ()
608+
609+
pure (Just $ RegistrationToken method regId)
610+
else do
611+
logger <& (CantRegister SMethod_WorkspaceDidChangeConfiguration) `WithSeverity` Warning
612+
pure Nothing
591613

592614
{- | Sends a @client/unregisterCapability@ request and removes the handler
593615
for that associated registration.

0 commit comments

Comments
 (0)