Skip to content

Commit a81ea6d

Browse files
authored
Merge pull request #512 from haskell/mpj/configuration
Fully support use of workspace/configuration
2 parents cde1658 + 9557ea7 commit a81ea6d

File tree

18 files changed

+585
-282
lines changed

18 files changed

+585
-282
lines changed

lsp-test/ChangeLog.md

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

3+
## Unreleased
4+
5+
- The client configuration is now _mandatory_ and is an `Object` rather than a `Value`.
6+
- `lsp-test` now responds to `workspace/configuration` requests.
7+
- `lsp-test` does _not_ send a `workspace/didChangeConfiguration` request on startup.
8+
- New functions for modifying the client configuration and notifying the server.
9+
- `ignoreLogNotifications` is now _on by default_. Experience shows the norm is to ignore these
10+
and it is simpler to turn this on only when they are required.
11+
- `ignoreConfigurationRequests` option to ignore `workspace/configuration` requests, also on
12+
by default.
13+
- New functions `setIgnoringLogNotifications` and `setIgnoringConfigurationRequests` to change
14+
whether such messages are ignored during a `Session` without having to change the `SessionConfig`.
15+
316
## 0.15.0.1
417

518
* Adds helper functions to resolve code lens, code actions, and completion items.

lsp-test/bench/SimpleBench.hs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ handlers = mconcat
3232

3333
server :: ServerDefinition ()
3434
server = ServerDefinition
35-
{ onConfigurationChange = const $ const $ Right ()
35+
{ parseConfig = const $ const $ Right ()
36+
, onConfigChange = const $ pure ()
3637
, defaultConfig = ()
38+
, configSection = "demo"
3739
, doInitialize = \env _req -> pure $ Right env
3840
, staticHandlers = \_caps -> handlers
3941
, interpretHandler = \env -> Iso (runLspT env) liftIO

lsp-test/func-test/FuncTest.hs

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,10 @@ main = hspec $ do
3232
killVar <- newEmptyMVar
3333

3434
let definition = ServerDefinition
35-
{ onConfigurationChange = const $ const $ Right ()
35+
{ parseConfig = const $ const $ Right ()
36+
, onConfigChange = const $ pure ()
3637
, defaultConfig = ()
38+
, configSection = "demo"
3739
, doInitialize = \env _req -> pure $ Right env
3840
, staticHandlers = \_caps -> handlers killVar
3941
, interpretHandler = \env -> Iso (runLspT env) liftIO
@@ -79,8 +81,10 @@ main = hspec $ do
7981
wf2 = WorkspaceFolder (filePathToUri "/foo/baz") "My other workspace"
8082

8183
definition = ServerDefinition
82-
{ onConfigurationChange = const $ const $ Right ()
84+
{ parseConfig = const $ const $ Right ()
85+
, onConfigChange = const $ pure ()
8386
, defaultConfig = ()
87+
, configSection = "demo"
8488
, doInitialize = \env _req -> pure $ Right env
8589
, staticHandlers = \_caps -> handlers
8690
, interpretHandler = \env -> Iso (runLspT env) liftIO

lsp-test/func-test/func-test.cabal

Lines changed: 0 additions & 18 deletions
This file was deleted.

lsp-test/lsp-test.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -59,6 +59,7 @@ library
5959
, filepath
6060
, Glob >=0.9 && <0.11
6161
, lens
62+
, lens-aeson
6263
, lsp ^>=2.1
6364
, lsp-types ^>=2.0
6465
, mtl <2.4

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

Lines changed: 50 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,8 @@ module Language.LSP.Test
2929
, runSessionWithConfigCustomProcess
3030
, runSessionWithHandles
3131
, runSessionWithHandles'
32+
, setIgnoringLogNotifications
33+
, setIgnoringConfigurationRequests
3234
-- ** Config
3335
, SessionConfig(..)
3436
, defaultConfig
@@ -49,6 +51,11 @@ module Language.LSP.Test
4951

5052
-- ** Initialization
5153
, initializeResponse
54+
-- ** Config
55+
, modifyConfig
56+
, setConfig
57+
, modifyConfigSection
58+
, setConfigSection
5259
-- ** Documents
5360
, createDoc
5461
, openDoc
@@ -121,6 +128,7 @@ import qualified Data.Set as Set
121128
import qualified Data.Text as T
122129
import qualified Data.Text.IO as T
123130
import Data.Aeson hiding (Null)
131+
import qualified Data.Aeson as J
124132
import Data.Default
125133
import Data.List
126134
import Data.Maybe
@@ -143,6 +151,7 @@ import System.Process (ProcessHandle, CreateProcess)
143151
import qualified System.FilePath.Glob as Glob
144152
import Control.Monad.State (execState)
145153
import Data.Traversable (for)
154+
import Data.String (fromString)
146155

147156
-- | Starts a new session.
148157
--
@@ -224,7 +233,8 @@ runSessionWithHandles' serverProc serverIn serverOut config' caps rootDir sessio
224233
Nothing
225234
(InL $ filePathToUri absRootDir)
226235
caps
227-
(lspConfig config')
236+
-- TODO: make this configurable?
237+
(Just $ Object $ lspConfig config')
228238
(Just TraceValues_Off)
229239
(fmap InL $ initialWorkspaceFolders config)
230240
runSession' serverIn serverOut serverProc listenServer config caps rootDir exitServer $ do
@@ -243,10 +253,6 @@ runSessionWithHandles' serverProc serverIn serverOut config' caps rootDir sessio
243253
liftIO $ putMVar initRspVar initRspMsg
244254
sendNotification SMethod_Initialized InitializedParams
245255

246-
case lspConfig config of
247-
Just cfg -> sendNotification SMethod_WorkspaceDidChangeConfiguration (DidChangeConfigurationParams cfg)
248-
Nothing -> return ()
249-
250256
-- ... relay them back to the user Session so they can match on them!
251257
-- As long as they are allowed.
252258
forM_ inBetween checkLegalBetweenMessage
@@ -401,6 +407,45 @@ sendResponse = sendMessage
401407
initializeResponse :: Session (TResponseMessage Method_Initialize)
402408
initializeResponse = ask >>= (liftIO . readMVar) . initRsp
403409

410+
setIgnoringLogNotifications :: Bool -> Session ()
411+
setIgnoringLogNotifications value = do
412+
modify (\ss -> ss { ignoringLogNotifications = value })
413+
414+
setIgnoringConfigurationRequests :: Bool -> Session ()
415+
setIgnoringConfigurationRequests value = do
416+
modify (\ss -> ss { ignoringConfigurationRequests = value })
417+
418+
-- | Modify the client config. This will send a notification to the server that the
419+
-- config has changed.
420+
modifyConfig :: (Object -> Object) -> Session ()
421+
modifyConfig f = do
422+
oldConfig <- curLspConfig <$> get
423+
let newConfig = f oldConfig
424+
modify (\ss -> ss { curLspConfig = newConfig })
425+
426+
caps <- asks sessionCapabilities
427+
let supportsConfiguration = fromMaybe False $ caps ^? L.workspace . _Just . L.configuration . _Just
428+
-- TODO: make this configurable?
429+
-- if they support workspace/configuration then be annoying and don't send the full config so
430+
-- they have to request it
431+
configToSend = if supportsConfiguration then J.Null else Object newConfig
432+
sendNotification SMethod_WorkspaceDidChangeConfiguration $ DidChangeConfigurationParams configToSend
433+
434+
-- | Set the client config. This will send a notification to the server that the
435+
-- config has changed.
436+
setConfig :: Object -> Session ()
437+
setConfig newConfig = modifyConfig (const newConfig)
438+
439+
-- | Modify a client config section (if already present, otherwise does nothing).
440+
-- This will send a notification to the server that the config has changed.
441+
modifyConfigSection :: String -> (Value -> Value) -> Session ()
442+
modifyConfigSection section f = modifyConfig (\o -> o & ix (fromString section) %~ f)
443+
444+
-- | Set a client config section. This will send a notification to the server that the
445+
-- config has changed.
446+
setConfigSection :: String -> Value -> Session ()
447+
setConfigSection section settings = modifyConfig (\o -> o & at(fromString section) ?~ settings)
448+
404449
-- | /Creates/ a new text document. This is different from 'openDoc'
405450
-- as it sends a workspace/didChangeWatchedFiles notification letting the server
406451
-- know that a file was created within the workspace, __provided that the server

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

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,8 @@ module Language.LSP.Test.Parsing
2020
, anyNotification
2121
, anyMessage
2222
, loggingNotification
23+
, configurationRequest
24+
, loggingOrConfiguration
2325
, publishDiagnosticsNotification
2426
) where
2527

@@ -207,6 +209,16 @@ loggingNotification = named "Logging notification" $ satisfy shouldSkip
207209
shouldSkip (FromServerMess SMethod_WindowShowDocument _) = True
208210
shouldSkip _ = False
209211

212+
-- | Matches if the message is a configuration request from the server.
213+
configurationRequest :: Session FromServerMessage
214+
configurationRequest = named "Configuration request" $ satisfy shouldSkip
215+
where
216+
shouldSkip (FromServerMess SMethod_WorkspaceConfiguration _) = True
217+
shouldSkip _ = False
218+
219+
loggingOrConfiguration :: Session FromServerMessage
220+
loggingOrConfiguration = loggingNotification <|> configurationRequest
221+
210222
-- | Matches a 'Language.LSP.Types.TextDocumentPublishDiagnostics'
211223
-- (textDocument/publishDiagnostics) notification.
212224
publishDiagnosticsNotification :: Session (TMessage Method_TextDocumentPublishDiagnostics)

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

Lines changed: 55 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
{-# LANGUAGE RankNTypes #-}
1010
{-# LANGUAGE TypeInType #-}
1111
{-# LANGUAGE TypeOperators #-}
12+
{-# LANGUAGE TypeApplications #-}
1213

1314
module Language.LSP.Test.Session
1415
( Session(..)
@@ -41,10 +42,10 @@ import Control.Concurrent hiding (yield)
4142
import Control.Exception
4243
import Control.Lens hiding (List, Empty)
4344
import Control.Monad
44-
import Control.Monad.Catch (MonadThrow)
45-
import Control.Monad.Except
4645
import Control.Monad.IO.Class
4746
import Control.Monad.Trans.Class
47+
import Control.Monad.Catch (MonadThrow)
48+
import Control.Monad.Except
4849
#if __GLASGOW_HASKELL__ == 806
4950
import Control.Monad.Fail
5051
#endif
@@ -55,6 +56,7 @@ import qualified Control.Monad.Trans.State as State
5556
import qualified Data.ByteString.Lazy.Char8 as B
5657
import Data.Aeson hiding (Error, Null)
5758
import Data.Aeson.Encode.Pretty
59+
import Data.Aeson.Lens ()
5860
import Data.Conduit as Conduit
5961
import Data.Conduit.Parser as Parser
6062
import Data.Default
@@ -84,6 +86,8 @@ import System.Timeout ( timeout )
8486
import Data.IORef
8587
import Colog.Core (LogAction (..), WithSeverity (..), Severity (..))
8688
import Data.Row
89+
import Data.String (fromString)
90+
import Data.Either (partitionEithers)
8791

8892
-- | A session representing one instance of launching and connecting to a server.
8993
--
@@ -112,20 +116,26 @@ data SessionConfig = SessionConfig
112116
-- ^ Trace the messages sent and received to stdout, defaults to False.
113117
-- Can be overriden with the environment variable @LSP_TEST_LOG_MESSAGES@.
114118
, logColor :: Bool -- ^ Add ANSI color to the logged messages, defaults to True.
115-
, lspConfig :: Maybe Value -- ^ The initial LSP config as JSON value, defaults to Nothing.
119+
, lspConfig :: Object
120+
-- ^ The initial LSP config as JSON object, defaults to the empty object.
121+
-- This should include the config section for the server if it has one, i.e. if
122+
-- the server has a 'mylang' config section, then the config should be an object
123+
-- with a 'mylang' key whose value is the actual config for the server. You
124+
-- can also include other config sections if your server may request those.
116125
, ignoreLogNotifications :: Bool
117-
-- ^ Whether or not to ignore 'Language.LSP.Types.ShowMessageNotification' and
118-
-- 'Language.LSP.Types.LogMessageNotification', defaults to False.
119-
--
120-
-- @since 0.9.0.0
126+
-- ^ Whether or not to ignore @window/showMessage@ and @window/logMessage@ notifications
127+
-- from the server, defaults to True.
128+
, ignoreConfigurationRequests :: Bool
129+
-- ^ Whether or not to ignore @workspace/configuration@ requests from the server,
130+
-- defaults to True.
121131
, initialWorkspaceFolders :: Maybe [WorkspaceFolder]
122132
-- ^ The initial workspace folders to send in the @initialize@ request.
123133
-- Defaults to Nothing.
124134
}
125135

126136
-- | The configuration used in 'Language.LSP.Test.runSession'.
127137
defaultConfig :: SessionConfig
128-
defaultConfig = SessionConfig 60 False False True Nothing False Nothing
138+
defaultConfig = SessionConfig 60 False False True mempty True True Nothing
129139

130140
instance Default SessionConfig where
131141
def = defaultConfig
@@ -181,7 +191,10 @@ data SessionState = SessionState
181191
, curDynCaps :: !(Map.Map T.Text SomeRegistration)
182192
-- ^ The capabilities that the server has dynamically registered with us so
183193
-- far
194+
, curLspConfig :: Object
184195
, curProgressSessions :: !(Set.Set ProgressToken)
196+
, ignoringLogNotifications :: Bool
197+
, ignoringConfigurationRequests :: Bool
185198
}
186199

187200
class Monad m => HasState s m where
@@ -227,15 +240,9 @@ runSessionMonad context state (Session session) = runReaderT (runStateT conduit
227240

228241
chanSource = do
229242
msg <- liftIO $ readChan (messageChan context)
230-
unless (ignoreLogNotifications (config context) && isLogNotification msg) $
231-
yield msg
243+
yield msg
232244
chanSource
233245

234-
isLogNotification (ServerMessage (FromServerMess SMethod_WindowShowMessage _)) = True
235-
isLogNotification (ServerMessage (FromServerMess SMethod_WindowLogMessage _)) = True
236-
isLogNotification (ServerMessage (FromServerMess SMethod_WindowShowDocument _)) = True
237-
isLogNotification _ = False
238-
239246
watchdog :: ConduitM SessionMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) ()
240247
watchdog = Conduit.awaitForever $ \msg -> do
241248
curId <- getCurTimeoutId
@@ -273,7 +280,7 @@ runSession' serverIn serverOut mServerProc serverHandler config caps rootDir exi
273280
mainThreadId <- myThreadId
274281

275282
let context = SessionContext serverIn absRootDir messageChan timeoutIdVar reqMap initRsp config caps
276-
initState vfs = SessionState 0 vfs mempty False Nothing mempty mempty
283+
initState vfs = SessionState 0 vfs mempty False Nothing mempty (lspConfig config) mempty (ignoreLogNotifications config) (ignoreConfigurationRequests config)
277284
runSession' ses = initVFS $ \vfs -> runSessionMonad context (initState vfs) ses
278285

279286
errorHandler = throwTo mainThreadId :: SessionException -> IO ()
@@ -302,17 +309,42 @@ runSession' serverIn serverOut mServerProc serverHandler config caps rootDir exi
302309

303310
updateStateC :: ConduitM FromServerMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) ()
304311
updateStateC = awaitForever $ \msg -> do
312+
state <- get @SessionState
305313
updateState msg
306-
respond msg
307-
yield msg
308-
where
309-
respond :: (MonadIO m, HasReader SessionContext m) => FromServerMessage -> m ()
310-
respond (FromServerMess SMethod_WindowWorkDoneProgressCreate req) =
314+
case msg of
315+
FromServerMess SMethod_WindowWorkDoneProgressCreate req ->
311316
sendMessage $ TResponseMessage "2.0" (Just $ req ^. L.id) (Right Null)
312-
respond (FromServerMess SMethod_WorkspaceApplyEdit r) = do
317+
FromServerMess SMethod_WorkspaceApplyEdit r -> do
313318
sendMessage $ TResponseMessage "2.0" (Just $ r ^. L.id) (Right $ ApplyWorkspaceEditResult True Nothing Nothing)
314-
respond _ = pure ()
319+
FromServerMess SMethod_WorkspaceConfiguration r -> do
320+
let requestedSections = mapMaybe (\i -> i ^? L.section . _Just) $ r ^. L.params . L.items
321+
let o = curLspConfig state
322+
-- check for each requested section whether we have it
323+
let configsOrErrs = (flip fmap) requestedSections $ \section ->
324+
case o ^. at (fromString $ T.unpack section) of
325+
Just config -> Right config
326+
Nothing -> Left section
327+
328+
let (errs, configs) = partitionEithers configsOrErrs
329+
330+
-- we have to return exactly the number of sections requested, so if we can't find all of them then that's an error
331+
if null errs
332+
then sendMessage $ TResponseMessage "2.0" (Just $ r ^. L.id) (Right configs)
333+
else sendMessage @_ @(TResponseError Method_WorkspaceConfiguration) $
334+
TResponseError (InL LSPErrorCodes_RequestFailed) ("No configuration for requested sections: " <> (T.pack $ show errs)) Nothing
335+
_ -> pure ()
336+
unless ((ignoringLogNotifications state && isLogNotification msg) || (ignoringConfigurationRequests state && isConfigRequest msg)) $
337+
yield msg
338+
339+
where
340+
341+
isLogNotification (FromServerMess SMethod_WindowShowMessage _) = True
342+
isLogNotification (FromServerMess SMethod_WindowLogMessage _) = True
343+
isLogNotification (FromServerMess SMethod_WindowShowDocument _) = True
344+
isLogNotification _ = False
315345

346+
isConfigRequest (FromServerMess SMethod_WorkspaceConfiguration _) = True
347+
isConfigRequest _ = False
316348

317349
-- extract Uri out from DocumentChange
318350
-- didn't put this in `lsp-types` because TH was getting in the way

0 commit comments

Comments
 (0)