Skip to content

Commit

Permalink
Register for workspace/didChangeConfiguration always
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
michaelpj committed Feb 4, 2024
1 parent 52607f1 commit 300fdd6
Show file tree
Hide file tree
Showing 11 changed files with 160 additions and 49 deletions.
9 changes: 9 additions & 0 deletions lsp-test/ChangeLog.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# Revision history for lsp-test

## Unreleased

- `ignoreRegistrationRequests` option to ignore `client/registerCapability` requests, on
by default.
- New functions `setIgnoringRegistrationRequests` to change whether such messages are
ignored during a `Session` without having to change the `SessionConfig`.
- `lsp-test` will no longer send `workspace/didChangConfiguration` notifications unless
the server dynamically registers for them.

## 0.16.0.1

- Support newer versions of dependencies.
Expand Down
2 changes: 2 additions & 0 deletions lsp-test/lsp-test.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,7 @@ library
, Diff >=0.4 && <0.6
, directory ^>=1.3
, exceptions ^>=0.10
, extra
, filepath >=1.4 && < 1.6
, Glob >=0.9 && <0.11
, lens >=5.1 && <5.3
Expand Down Expand Up @@ -101,6 +102,7 @@ test-suite tests
, containers
, data-default
, directory
, extra
, filepath
, hspec
, lens
Expand Down
33 changes: 27 additions & 6 deletions lsp-test/src/Language/LSP/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ module Language.LSP.Test (
runSessionWithHandles',
setIgnoringLogNotifications,
setIgnoringConfigurationRequests,
setIgnoringRegistrationRequests,

-- ** Config
SessionConfig (..),
Expand Down Expand Up @@ -144,8 +145,10 @@ import Control.Monad.IO.Class
import Control.Monad.State (execState)
import Data.Aeson hiding (Null)
import Data.Aeson qualified as J
import Data.Aeson.KeyMap qualified as J
import Data.Default
import Data.List
import Data.List.Extra (firstJust)
import Data.Map.Strict qualified as Map
import Data.Maybe
import Data.Set qualified as Set
Expand Down Expand Up @@ -476,6 +479,10 @@ setIgnoringConfigurationRequests :: Bool -> Session ()
setIgnoringConfigurationRequests value = do
modify (\ss -> ss{ignoringConfigurationRequests = value})

setIgnoringRegistrationRequests :: Bool -> Session ()
setIgnoringRegistrationRequests value = do
modify (\ss -> ss{ignoringRegistrationRequests = value})

{- | Modify the client config. This will send a notification to the server that the
config has changed.
-}
Expand All @@ -485,12 +492,26 @@ modifyConfig f = do
let newConfig = f oldConfig
modify (\ss -> ss{curLspConfig = newConfig})

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

{- | Set the client config. This will send a notification to the server that the
Expand Down
38 changes: 34 additions & 4 deletions lsp-test/src/Language/LSP/Test/Session.hs
Original file line number Diff line number Diff line change
Expand Up @@ -123,14 +123,17 @@ data SessionConfig = SessionConfig
, ignoreConfigurationRequests :: Bool
-- ^ Whether or not to ignore @workspace/configuration@ requests from the server,
-- defaults to True.
, ignoreRegistrationRequests :: Bool
-- ^ Whether or not to ignore @workspace/registerCapability@ requests from the server,
-- defaults to True.
, initialWorkspaceFolders :: Maybe [WorkspaceFolder]
-- ^ The initial workspace folders to send in the @initialize@ request.
-- Defaults to Nothing.
}

-- | The configuration used in 'Language.LSP.Test.runSession'.
defaultConfig :: SessionConfig
defaultConfig = SessionConfig 60 False False True mempty True True Nothing
defaultConfig = SessionConfig 60 False False True mempty True True True Nothing

instance Default SessionConfig where
def = defaultConfig
Expand Down Expand Up @@ -190,6 +193,7 @@ data SessionState = SessionState
, curProgressSessions :: !(Set.Set ProgressToken)
, ignoringLogNotifications :: Bool
, ignoringConfigurationRequests :: Bool
, ignoringRegistrationRequests :: Bool
}

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

mainThreadId <- myThreadId

let context = SessionContext serverIn absRootDir messageChan timeoutIdVar reqMap initRsp config caps
initState = SessionState 0 emptyVFS mempty False Nothing mempty (lspConfig config) mempty (ignoreLogNotifications config) (ignoreConfigurationRequests config)
let context = SessionContext
serverIn
absRootDir
messageChan
timeoutIdVar
reqMap
initRsp
config
caps
initState = SessionState
0
emptyVFS
mempty
False
Nothing
mempty
(lspConfig config)
mempty
(ignoreLogNotifications config)
(ignoreConfigurationRequests config)
(ignoreRegistrationRequests config)
runSession' = runSessionMonad context initState

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

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

isRegistrationRequest (FromServerMess SMethod_ClientRegisterCapability _) = True
isRegistrationRequest (FromServerMess SMethod_ClientUnregisterCapability _) = True
isRegistrationRequest _ = False

-- extract Uri out from DocumentChange
-- didn't put this in `lsp-types` because TH was getting in the way
documentChangeUri :: DocumentChange -> Uri
Expand Down
4 changes: 2 additions & 2 deletions lsp-test/test/DummyServer.hs
Original file line number Diff line number Diff line change
Expand Up @@ -123,7 +123,7 @@ handlers =
(Just WatchKind_Create)
]
Just token <- runInIO $
registerCapability SMethod_WorkspaceDidChangeWatchedFiles regOpts $
registerCapability mempty SMethod_WorkspaceDidChangeWatchedFiles regOpts $
\_noti ->
sendNotification SMethod_WindowLogMessage $
LogMessageParams MessageType_Log "got workspace/didChangeWatchedFiles"
Expand All @@ -138,7 +138,7 @@ handlers =
(Just WatchKind_Create)
]
Just token <- runInIO $
registerCapability SMethod_WorkspaceDidChangeWatchedFiles regOpts $
registerCapability mempty SMethod_WorkspaceDidChangeWatchedFiles regOpts $
\_noti ->
sendNotification SMethod_WindowLogMessage $
LogMessageParams MessageType_Log "got workspace/didChangeWatchedFiles"
Expand Down
33 changes: 20 additions & 13 deletions lsp-test/test/Test.hs
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,11 @@ import Data.Aeson
import Data.Aeson qualified as J
import Data.Default
import Data.Either
import Data.List.Extra
import Data.Map.Strict qualified as M
import Data.Maybe
import Data.Proxy
import Data.Text qualified as T
import Data.Type.Equality
import DummyServer
import Language.LSP.Protocol.Lens qualified as L
import Language.LSP.Protocol.Message
Expand Down Expand Up @@ -368,25 +368,31 @@ main = hspec $ around withDummyServer $ do
void publishDiagnosticsNotification

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

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

[SomeRegistration (TRegistration _ regMethod regOpts)] <- getRegisteredCapabilities
liftIO $ do
case regMethod `mEqClient` SMethod_WorkspaceDidChangeWatchedFiles of
Just (Right HRefl) ->
regOpts
`shouldBe` ( Just $
DidChangeWatchedFilesRegistrationOptions
[FileSystemWatcher (GlobPattern $ InL $ Pattern "*.watch") (Just WatchKind_Create)]
)
_ -> expectationFailure "Registration wasn't on workspace/didChangeWatchedFiles"
-- Look for the registration, we might have one for didChangeConfiguration in there too
registeredCaps <- getRegisteredCapabilities
let
regOpts :: Maybe DidChangeWatchedFilesRegistrationOptions
regOpts = flip firstJust registeredCaps $ \(SomeRegistration (TRegistration _ regMethod regOpts)) ->
case regMethod of
SMethod_WorkspaceDidChangeWatchedFiles -> regOpts
_ -> Nothing
liftIO $
regOpts
`shouldBe` ( Just $
DidChangeWatchedFilesRegistrationOptions
[FileSystemWatcher (GlobPattern $ InL $ Pattern "*.watch") (Just WatchKind_Create)]
)

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

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

setIgnoringRegistrationRequests False
createDoc ".register.abs" "haskell" ""
message SMethod_ClientRegisterCapability

Expand Down
2 changes: 2 additions & 0 deletions lsp/ChangeLog.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
so client-initiated progress can now be supported properly.
- The server options now allow the user to say whether the server should advertise
support for client-initiated progress or not.
- The server now dynamically registers for `workspace/didChangeConfiguration`
notifications, to ensure that newer clients continue to send them.

## 2.3.0.0

Expand Down
15 changes: 10 additions & 5 deletions lsp/example/Reactor.hs
Original file line number Diff line number Diff line change
Expand Up @@ -218,11 +218,16 @@ handle logger =

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

void $ registerCapability LSP.SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do
logger <& "Processing a textDocument/codeLens request" `WithSeverity` Info
let cmd = LSP.Command "Say hello" "lsp-hello-command" Nothing
rsp = [LSP.CodeLens (LSP.mkRange 0 0 0 100) (Just cmd) Nothing]
responder (Right $ LSP.InL rsp)
void
$ registerCapability
mempty
LSP.SMethod_TextDocumentCodeLens
regOpts
$ \_req responder -> do
logger <& "Processing a textDocument/codeLens request" `WithSeverity` Info
let cmd = LSP.Command "Say hello" "lsp-hello-command" Nothing
rsp = [LSP.CodeLens (LSP.mkRange 0 0 0 100) (Just cmd) Nothing]
responder (Right $ LSP.InL rsp)
, notificationHandler LSP.SMethod_TextDocumentDidOpen $ \msg -> do
let doc = msg ^. LSP.params . LSP.textDocument . LSP.uri
fileName = LSP.uriToFilePath doc
Expand Down
2 changes: 1 addition & 1 deletion lsp/example/Simple.hs
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ handlers =
Right (InL (MessageActionItem "Turn on")) -> do
let regOpts = CodeLensRegistrationOptions (InR Null) Nothing (Just False)

_ <- registerCapability SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do
_ <- registerCapability mempty SMethod_TextDocumentCodeLens regOpts $ \_req responder -> do
let cmd = Command "Say hello" "lsp-hello-command" Nothing
rsp = [CodeLens (mkRange 0 0 0 100) (Just cmd) Nothing]
responder $ Right $ InL rsp
Expand Down
56 changes: 39 additions & 17 deletions lsp/src/Language/LSP/Server/Core.hs
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,9 @@ data LspCoreLog
| ConfigurationNotSupported
| BadConfigurationResponse ResponseError
| WrongConfigSections [J.Value]
deriving (Show)
| forall m. CantRegister (SMethod m)

deriving instance (Show LspCoreLog)

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

newtype LspT config m a = LspT {unLspT :: ReaderT (LanguageContextEnv config) m a}
deriving (Functor, Applicative, Monad, MonadCatch, MonadIO, MonadMask, MonadThrow, MonadTrans, MonadUnliftIO, MonadFix)
Expand Down Expand Up @@ -550,30 +553,27 @@ getWorkspaceFolders = do
registerCapability ::
forall f t (m :: Method ClientToServer t) config.
MonadLsp config f =>
LogAction f (WithSeverity LspCoreLog) ->
SClientMethod m ->
RegistrationOptions m ->
Handler f m ->
f (Maybe (RegistrationToken m))
registerCapability method regOpts f = do
clientCaps <- resClientCapabilities <$> getLspEnv
registerCapability logger method regOpts f = do
handlers <- resHandlers <$> getLspEnv
let alreadyStaticallyRegistered = case splitClientMethod method of
IsClientNot -> SMethodMap.member method $ notHandlers handlers
IsClientReq -> SMethodMap.member method $ reqHandlers handlers
IsClientEither -> error "Cannot register capability for custom methods"
go clientCaps alreadyStaticallyRegistered
go alreadyStaticallyRegistered
where
-- If the server has already registered statically, don't dynamically register
-- as per the spec
go _clientCaps True = pure Nothing
go clientCaps False
-- First, check to see if the client supports dynamic registration on this method
| dynamicRegistrationSupported method clientCaps = do
uuid <- liftIO $ UUID.toText <$> getStdRandom random
let registration = L.TRegistration uuid method (Just regOpts)
params = L.RegistrationParams [toUntypedRegistration registration]
regId = RegistrationId uuid
rio <- askUnliftIO
go True = pure Nothing
go False = do
rio <- askUnliftIO
mtoken <- trySendRegistration logger method regOpts
case mtoken of
Just token@(RegistrationToken _ regId) -> do
~() <- case splitClientMethod method of
IsClientNot -> modifyState resRegistrationsNot $ \oldRegs ->
let pair = Pair regId (ClientMessageHandler (unliftIO rio . f))
Expand All @@ -583,11 +583,33 @@ registerCapability method regOpts f = do
in SMethodMap.insert method pair oldRegs
IsClientEither -> error "Cannot register capability for custom methods"

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

pure (Just (RegistrationToken method regId))
| otherwise = pure Nothing
trySendRegistration ::
forall f t (m :: Method ClientToServer t) config.
MonadLsp config f =>
LogAction f (WithSeverity LspCoreLog) ->
SClientMethod m ->
RegistrationOptions m ->
f (Maybe (RegistrationToken m))
trySendRegistration logger method regOpts = do
clientCaps <- resClientCapabilities <$> getLspEnv
-- First, check to see if the client supports dynamic registration on this method
if dynamicRegistrationSupported method clientCaps
then do
uuid <- liftIO $ UUID.toText <$> getStdRandom random
let registration = L.TRegistration uuid method (Just regOpts)
params = L.RegistrationParams [toUntypedRegistration registration]
regId = RegistrationId uuid

-- TODO: handle the scenario where this returns an error
_ <- sendRequest SMethod_ClientRegisterCapability params $ \_res -> pure ()

pure (Just $ RegistrationToken method regId)
else do
logger <& (CantRegister SMethod_WorkspaceDidChangeConfiguration) `WithSeverity` Warning
pure Nothing

{- | Sends a @client/unregisterCapability@ request and removes the handler
for that associated registration.
Expand Down
Loading

0 comments on commit 300fdd6

Please sign in to comment.