1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE TypeOperators #-}
3 {-# LANGUAGE FlexibleContexts #-}
4 {-# LANGUAGE KindSignatures #-}
6 {-# LANGUAGE RankNTypes #-}
7 {-# LANGUAGE TypeInType #-}
8 {-# LANGUAGE ScopedTypeVariables #-}
9 {-# LANGUAGE ExistentialQuantification #-}
10 {-# LANGUAGE DuplicateRecordFields #-}
13 Module : Language.LSP.Test
14 Description : A functional testing framework for LSP servers.
15 Maintainer : luke_lau@icloud.com
16 Stability : experimental
17 Portability : non-portable
19 Provides the framework to start functionally testing
20 <https://github.com/Microsoft/language-server-protocol Language Server Protocol servers>.
21 You should import "Language.LSP.Types" alongside this.
23 module Language.LSP.Test
28 , runSessionWithConfig
29 , runSessionWithHandles
35 , module Language.LSP.Test.Exceptions
44 , module Language.LSP.Test.Parsing
46 -- | Quick helper functions for common tasks.
63 , waitForDiagnosticsSource
65 , getCurrentDiagnostics
66 , getIncompleteProgressSessions
96 , getRegisteredCapabilities
99 import Control.Applicative.Combinators
100 import Control.Concurrent
102 import Control.Monad.IO.Class
103 import Control.Exception
104 import Control.Lens hiding ((.=), List, Empty)
105 import qualified Data.Map.Strict as Map
106 import qualified Data.Set as Set
107 import qualified Data.Text as T
108 import qualified Data.Text.IO as T
111 import qualified Data.HashMap.Strict as HashMap
114 import Language.LSP.Types
115 import Language.LSP.Types.Lens hiding
116 (id, capabilities, message, executeCommand, applyEdit, rename)
117 import qualified Language.LSP.Types.Lens as LSP
118 import qualified Language.LSP.Types.Capabilities as C
119 import Language.LSP.VFS
120 import Language.LSP.Test.Compat
121 import Language.LSP.Test.Decoding
122 import Language.LSP.Test.Exceptions
123 import Language.LSP.Test.Parsing
124 import Language.LSP.Test.Session
125 import Language.LSP.Test.Server
126 import System.Environment
128 import System.Directory
129 import System.FilePath
130 import System.Process (ProcessHandle)
131 import qualified System.FilePath.Glob as Glob
133 -- | Starts a new session.
135 -- > runSession "hie" fullCaps "path/to/root/dir" $ do
136 -- > doc <- openDoc "Desktop/simple.hs" "haskell"
137 -- > diags <- waitForDiagnostics
138 -- > let pos = Position 12 5
139 -- > params = TextDocumentPositionParams doc
140 -- > hover <- request STextdocumentHover params
141 runSession :: String -- ^ The command to run the server.
142 -> C.ClientCapabilities -- ^ The capabilities that the client should declare.
143 -> FilePath -- ^ The filepath to the root directory for the session.
144 -> Session a -- ^ The session to run.
146 runSession = runSessionWithConfig def
148 -- | Starts a new sesion with a custom configuration.
149 runSessionWithConfig :: SessionConfig -- ^ Configuration options for the session.
150 -> String -- ^ The command to run the server.
151 -> C.ClientCapabilities -- ^ The capabilities that the client should declare.
152 -> FilePath -- ^ The filepath to the root directory for the session.
153 -> Session a -- ^ The session to run.
155 runSessionWithConfig config' serverExe caps rootDir session = do
156 config <- envOverrideConfig config'
157 withServer serverExe (logStdErr config) $ \serverIn serverOut serverProc ->
158 runSessionWithHandles' (Just serverProc) serverIn serverOut config caps rootDir session
160 -- | Starts a new session, using the specified handles to communicate with the
161 -- server. You can use this to host the server within the same process.
162 -- An example with lsp might look like:
164 -- > (hinRead, hinWrite) <- createPipe
165 -- > (houtRead, houtWrite) <- createPipe
167 -- > forkIO $ void $ runServerWithHandles hinRead houtWrite serverDefinition
168 -- > runSessionWithHandles hinWrite houtRead defaultConfig fullCaps "." $ do
170 runSessionWithHandles :: Handle -- ^ The input handle
171 -> Handle -- ^ The output handle
173 -> C.ClientCapabilities -- ^ The capabilities that the client should declare.
174 -> FilePath -- ^ The filepath to the root directory for the session.
175 -> Session a -- ^ The session to run.
177 runSessionWithHandles = runSessionWithHandles' Nothing
180 runSessionWithHandles' :: Maybe ProcessHandle
181 -> Handle -- ^ The input handle
182 -> Handle -- ^ The output handle
184 -> C.ClientCapabilities -- ^ The capabilities that the client should declare.
185 -> FilePath -- ^ The filepath to the root directory for the session.
186 -> Session a -- ^ The session to run.
188 runSessionWithHandles' serverProc serverIn serverOut config' caps rootDir session = do
189 pid <- getCurrentProcessID
190 absRootDir <- canonicalizePath rootDir
192 config <- envOverrideConfig config'
194 let initializeParams = InitializeParams Nothing
196 (Just lspTestClientInfo)
197 (Just $ T.pack absRootDir)
198 (Just $ filePathToUri absRootDir)
202 (List <$> initialWorkspaceFolders config)
203 runSession' serverIn serverOut serverProc listenServer config caps rootDir exitServer $ do
204 -- Wrap the session around initialize and shutdown calls
205 initReqId <- sendRequest SInitialize initializeParams
207 -- Because messages can be sent in between the request and response,
208 -- collect them and then...
209 (inBetween, initRspMsg) <- manyTill_ anyMessage (responseForId SInitialize initReqId)
211 case initRspMsg ^. LSP.result of
212 Left error -> liftIO $ putStrLn ("Error while initializing: " ++ show error)
215 initRspVar <- initRsp <$> ask
216 liftIO $ putMVar initRspVar initRspMsg
217 sendNotification SInitialized (Just InitializedParams)
219 case lspConfig config of
220 Just cfg -> sendNotification SWorkspaceDidChangeConfiguration (DidChangeConfigurationParams cfg)
223 -- ... relay them back to the user Session so they can match on them!
224 -- As long as they are allowed.
225 forM_ inBetween checkLegalBetweenMessage
226 msgChan <- asks messageChan
227 liftIO $ writeList2Chan msgChan (ServerMessage <$> inBetween)
229 -- Run the actual test
232 -- | Asks the server to shutdown and exit politely
233 exitServer :: Session ()
234 exitServer = request_ SShutdown Empty >> sendNotification SExit Empty
236 -- | Listens to the server output until the shutdown ack,
237 -- makes sure it matches the record and signals any semaphores
238 listenServer :: Handle -> SessionContext -> IO ()
239 listenServer serverOut context = do
240 msgBytes <- getNextMessage serverOut
242 msg <- modifyMVar (requestMap context) $ \reqMap ->
243 pure $ decodeFromServerMsg reqMap msgBytes
244 writeChan (messageChan context) (ServerMessage msg)
247 (FromServerRsp SShutdown _) -> return ()
248 _ -> listenServer serverOut context
250 -- | Is this message allowed to be sent by the server between the intialize
251 -- request and response?
252 -- https://microsoft.github.io/language-server-protocol/specifications/specification-3-15/#initialize
253 checkLegalBetweenMessage :: FromServerMessage -> Session ()
254 checkLegalBetweenMessage (FromServerMess SWindowShowMessage _) = pure ()
255 checkLegalBetweenMessage (FromServerMess SWindowLogMessage _) = pure ()
256 checkLegalBetweenMessage (FromServerMess STelemetryEvent _) = pure ()
257 checkLegalBetweenMessage (FromServerMess SWindowShowMessageRequest _) = pure ()
258 checkLegalBetweenMessage msg = throw (IllegalInitSequenceMessage msg)
260 -- | Check environment variables to override the config
261 envOverrideConfig :: SessionConfig -> IO SessionConfig
262 envOverrideConfig cfg = do
263 logMessages' <- fromMaybe (logMessages cfg) <$> checkEnv "LSP_TEST_LOG_MESSAGES"
264 logStdErr' <- fromMaybe (logStdErr cfg) <$> checkEnv "LSP_TEST_LOG_STDERR"
265 return $ cfg { logMessages = logMessages', logStdErr = logStdErr' }
266 where checkEnv :: String -> IO (Maybe Bool)
267 checkEnv s = fmap convertVal <$> lookupEnv s
268 convertVal "0" = False
271 -- | The current text contents of a document.
272 documentContents :: TextDocumentIdentifier -> Session T.Text
273 documentContents doc = do
275 let file = vfsMap vfs Map.! toNormalizedUri (doc ^. uri)
276 return (virtualFileText file)
278 -- | Parses an ApplyEditRequest, checks that it is for the passed document
279 -- and returns the new content
280 getDocumentEdit :: TextDocumentIdentifier -> Session T.Text
281 getDocumentEdit doc = do
282 req <- message SWorkspaceApplyEdit
284 unless (checkDocumentChanges req || checkChanges req) $
285 liftIO $ throw (IncorrectApplyEditRequest (show req))
289 checkDocumentChanges req =
290 let changes = req ^. params . edit . documentChanges
291 maybeDocs = fmap (fmap documentChangeUri) changes
293 Just docs -> (doc ^. uri) `elem` docs
296 let mMap = req ^. params . edit . changes
297 in maybe False (HashMap.member (doc ^. uri)) mMap
299 -- | Sends a request to the server and waits for its response.
300 -- Will skip any messages in between the request and the response
302 -- rsp <- request STextDocumentDocumentSymbol params
304 -- Note: will skip any messages in between the request and the response.
305 request :: SClientMethod m -> MessageParams m -> Session (ResponseMessage m)
306 request m = sendRequest m >=> skipManyTill anyMessage . responseForId m
308 -- | The same as 'sendRequest', but discard the response.
309 request_ :: SClientMethod (m :: Method FromClient Request) -> MessageParams m -> Session ()
310 request_ p = void . request p
312 -- | Sends a request to the server. Unlike 'request', this doesn't wait for the response.
314 :: SClientMethod m -- ^ The request method.
315 -> MessageParams m -- ^ The request parameters.
316 -> Session (LspId m) -- ^ The id of the request that was sent.
317 sendRequest method params = do
318 idn <- curReqId <$> get
319 modify $ \c -> c { curReqId = idn+1 }
322 let mess = RequestMessage "2.0" id method params
324 -- Update the request map
325 reqMap <- requestMap <$> ask
326 liftIO $ modifyMVar_ reqMap $
327 \r -> return $ fromJust $ updateRequestMap r id method
329 ~() <- case splitClientMethod method of
330 IsClientReq -> sendMessage mess
331 IsClientEither -> sendMessage $ ReqMess mess
335 -- | Sends a notification to the server.
336 sendNotification :: SClientMethod (m :: Method FromClient Notification) -- ^ The notification method.
337 -> MessageParams m -- ^ The notification parameters.
339 -- Open a virtual file if we send a did open text document notification
340 sendNotification STextDocumentDidOpen params = do
341 let n = NotificationMessage "2.0" STextDocumentDidOpen params
342 oldVFS <- vfs <$> get
343 let (newVFS,_) = openVFS oldVFS n
344 modify (\s -> s { vfs = newVFS })
347 -- Close a virtual file if we send a close text document notification
348 sendNotification STextDocumentDidClose params = do
349 let n = NotificationMessage "2.0" STextDocumentDidClose params
350 oldVFS <- vfs <$> get
351 let (newVFS,_) = closeVFS oldVFS n
352 modify (\s -> s { vfs = newVFS })
355 sendNotification STextDocumentDidChange params = do
356 let n = NotificationMessage "2.0" STextDocumentDidChange params
357 oldVFS <- vfs <$> get
358 let (newVFS,_) = changeFromClientVFS oldVFS n
359 modify (\s -> s { vfs = newVFS })
362 sendNotification method params =
363 case splitClientMethod method of
364 IsClientNot -> sendMessage (NotificationMessage "2.0" method params)
365 IsClientEither -> sendMessage (NotMess $ NotificationMessage "2.0" method params)
367 -- | Sends a response to the server.
368 sendResponse :: ToJSON (ResponseResult m) => ResponseMessage m -> Session ()
369 sendResponse = sendMessage
371 -- | Returns the initialize response that was received from the server.
372 -- The initialize requests and responses are not included the session,
373 -- so if you need to test it use this.
374 initializeResponse :: Session (ResponseMessage Initialize)
375 initializeResponse = ask >>= (liftIO . readMVar) . initRsp
377 -- | /Creates/ a new text document. This is different from 'openDoc'
378 -- as it sends a workspace/didChangeWatchedFiles notification letting the server
379 -- know that a file was created within the workspace, __provided that the server
380 -- has registered for it__, and the file matches any patterns the server
382 -- It /does not/ actually create a file on disk, but is useful for convincing
383 -- the server that one does exist.
386 createDoc :: FilePath -- ^ The path to the document to open, __relative to the root directory__.
387 -> T.Text -- ^ The text document's language identifier, e.g. @"haskell"@.
388 -> T.Text -- ^ The content of the text document to create.
389 -> Session TextDocumentIdentifier -- ^ The identifier of the document just created.
390 createDoc file languageId contents = do
391 dynCaps <- curDynCaps <$> get
392 rootDir <- asks rootDir
393 caps <- asks sessionCapabilities
394 absFile <- liftIO $ canonicalizePath (rootDir </> file)
395 let pred :: SomeRegistration -> [Registration WorkspaceDidChangeWatchedFiles]
396 pred (SomeRegistration r@(Registration _ SWorkspaceDidChangeWatchedFiles _)) = [r]
398 regs = concatMap pred $ Map.elems dynCaps
399 watchHits :: FileSystemWatcher -> Bool
400 watchHits (FileSystemWatcher pattern kind) =
401 -- If WatchKind is exlcuded, defaults to all true as per spec
402 fileMatches (T.unpack pattern) && createHits (fromMaybe (WatchKind True True True) kind)
404 fileMatches pattern = Glob.match (Glob.compile pattern) relOrAbs
405 -- If the pattern is absolute then match against the absolute fp
407 | isAbsolute pattern = absFile
410 createHits (WatchKind create _ _) = create
412 regHits :: Registration WorkspaceDidChangeWatchedFiles -> Bool
413 regHits reg = foldl' (\acc w -> acc || watchHits w) False (reg ^. registerOptions . watchers)
416 caps ^? workspace . _Just . didChangeWatchedFiles . _Just . dynamicRegistration . _Just
418 shouldSend = clientCapsSupports && foldl' (\acc r -> acc || regHits r) False regs
421 sendNotification SWorkspaceDidChangeWatchedFiles $ DidChangeWatchedFilesParams $
422 List [ FileEvent (filePathToUri (rootDir </> file)) FcCreated ]
423 openDoc' file languageId contents
425 -- | Opens a text document that /exists on disk/, and sends a
426 -- textDocument/didOpen notification to the server.
427 openDoc :: FilePath -> T.Text -> Session TextDocumentIdentifier
428 openDoc file languageId = do
430 let fp = rootDir context </> file
431 contents <- liftIO $ T.readFile fp
432 openDoc' file languageId contents
434 -- | This is a variant of `openDoc` that takes the file content as an argument.
435 -- Use this is the file exists /outside/ of the current workspace.
436 openDoc' :: FilePath -> T.Text -> T.Text -> Session TextDocumentIdentifier
437 openDoc' file languageId contents = do
439 let fp = rootDir context </> file
440 uri = filePathToUri fp
441 item = TextDocumentItem uri languageId 0 contents
442 sendNotification STextDocumentDidOpen (DidOpenTextDocumentParams item)
443 pure $ TextDocumentIdentifier uri
445 -- | Closes a text document and sends a textDocument/didOpen notification to the server.
446 closeDoc :: TextDocumentIdentifier -> Session ()
448 let params = DidCloseTextDocumentParams (TextDocumentIdentifier (docId ^. uri))
449 sendNotification STextDocumentDidClose params
451 -- | Changes a text document and sends a textDocument/didOpen notification to the server.
452 changeDoc :: TextDocumentIdentifier -> [TextDocumentContentChangeEvent] -> Session ()
453 changeDoc docId changes = do
454 verDoc <- getVersionedDoc docId
455 let params = DidChangeTextDocumentParams (verDoc & version . non 0 +~ 1) (List changes)
456 sendNotification STextDocumentDidChange params
458 -- | Gets the Uri for the file corrected to the session directory.
459 getDocUri :: FilePath -> Session Uri
462 let fp = rootDir context </> file
463 return $ filePathToUri fp
465 -- | Waits for diagnostics to be published and returns them.
466 waitForDiagnostics :: Session [Diagnostic]
467 waitForDiagnostics = do
468 diagsNot <- skipManyTill anyMessage (message STextDocumentPublishDiagnostics)
469 let (List diags) = diagsNot ^. params . LSP.diagnostics
472 -- | The same as 'waitForDiagnostics', but will only match a specific
473 -- 'Language.LSP.Types._source'.
474 waitForDiagnosticsSource :: String -> Session [Diagnostic]
475 waitForDiagnosticsSource src = do
476 diags <- waitForDiagnostics
477 let res = filter matches diags
479 then waitForDiagnosticsSource src
482 matches :: Diagnostic -> Bool
483 matches d = d ^. source == Just (T.pack src)
485 -- | Expects a 'PublishDiagnosticsNotification' and throws an
486 -- 'UnexpectedDiagnostics' exception if there are any diagnostics
488 noDiagnostics :: Session ()
490 diagsNot <- message STextDocumentPublishDiagnostics
491 when (diagsNot ^. params . LSP.diagnostics /= List []) $ liftIO $ throw UnexpectedDiagnostics
493 -- | Returns the symbols in a document.
494 getDocumentSymbols :: TextDocumentIdentifier -> Session (Either [DocumentSymbol] [SymbolInformation])
495 getDocumentSymbols doc = do
496 ResponseMessage _ rspLid res <- request STextDocumentDocumentSymbol (DocumentSymbolParams Nothing Nothing doc)
498 Right (InL (List xs)) -> return (Left xs)
499 Right (InR (List xs)) -> return (Right xs)
500 Left err -> throw (UnexpectedResponseError (SomeLspId $ fromJust rspLid) err)
502 -- | Returns the code actions in the specified range.
503 getCodeActions :: TextDocumentIdentifier -> Range -> Session [Command |? CodeAction]
504 getCodeActions doc range = do
505 ctx <- getCodeActionContextInRange doc range
506 rsp <- request STextDocumentCodeAction (CodeActionParams Nothing Nothing doc range ctx)
508 case rsp ^. result of
509 Right (List xs) -> return xs
510 Left error -> throw (UnexpectedResponseError (SomeLspId $ fromJust $ rsp ^. LSP.id) error)
512 -- | Returns all the code actions in a document by
513 -- querying the code actions at each of the current
514 -- diagnostics' positions.
515 getAllCodeActions :: TextDocumentIdentifier -> Session [Command |? CodeAction]
516 getAllCodeActions doc = do
517 ctx <- getCodeActionContext doc
519 foldM (go ctx) [] =<< getCurrentDiagnostics doc
522 go :: CodeActionContext -> [Command |? CodeAction] -> Diagnostic -> Session [Command |? CodeAction]
524 ResponseMessage _ rspLid res <- request STextDocumentCodeAction (CodeActionParams Nothing Nothing doc (diag ^. range) ctx)
527 Left e -> throw (UnexpectedResponseError (SomeLspId $ fromJust rspLid) e)
528 Right (List cmdOrCAs) -> pure (acc ++ cmdOrCAs)
530 getCodeActionContextInRange :: TextDocumentIdentifier -> Range -> Session CodeActionContext
531 getCodeActionContextInRange doc caRange = do
532 curDiags <- getCurrentDiagnostics doc
533 let diags = [ d | d@Diagnostic{_range=range} <- curDiags
534 , overlappingRange caRange range
536 return $ CodeActionContext (List diags) Nothing
538 overlappingRange :: Range -> Range -> Bool
539 overlappingRange (Range s e) range =
540 positionInRange s range
541 || positionInRange e range
543 positionInRange :: Position -> Range -> Bool
544 positionInRange (Position pl po) (Range (Position sl so) (Position el eo)) =
546 || pl == sl && pl == el && po >= so && po <= eo
547 || pl == sl && po >= so
548 || pl == el && po <= eo
550 getCodeActionContext :: TextDocumentIdentifier -> Session CodeActionContext
551 getCodeActionContext doc = do
552 curDiags <- getCurrentDiagnostics doc
553 return $ CodeActionContext (List curDiags) Nothing
555 -- | Returns the current diagnostics that have been sent to the client.
556 -- Note that this does not wait for more to come in.
557 getCurrentDiagnostics :: TextDocumentIdentifier -> Session [Diagnostic]
558 getCurrentDiagnostics doc = fromMaybe [] . Map.lookup (toNormalizedUri $ doc ^. uri) . curDiagnostics <$> get
560 -- | Returns the tokens of all progress sessions that have started but not yet ended.
561 getIncompleteProgressSessions :: Session (Set.Set ProgressToken)
562 getIncompleteProgressSessions = curProgressSessions <$> get
564 -- | Executes a command.
565 executeCommand :: Command -> Session ()
566 executeCommand cmd = do
567 let args = decode $ encode $ fromJust $ cmd ^. arguments
568 execParams = ExecuteCommandParams Nothing (cmd ^. command) args
569 void $ sendRequest SWorkspaceExecuteCommand execParams
571 -- | Executes a code action.
572 -- Matching with the specification, if a code action
573 -- contains both an edit and a command, the edit will
575 executeCodeAction :: CodeAction -> Session ()
576 executeCodeAction action = do
577 maybe (return ()) handleEdit $ action ^. edit
578 maybe (return ()) executeCommand $ action ^. command
580 where handleEdit :: WorkspaceEdit -> Session ()
582 -- Its ok to pass in dummy parameters here as they aren't used
583 let req = RequestMessage "" (IdInt 0) SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing e)
584 in updateState (FromServerMess SWorkspaceApplyEdit req)
586 -- | Adds the current version to the document, as tracked by the session.
587 getVersionedDoc :: TextDocumentIdentifier -> Session VersionedTextDocumentIdentifier
588 getVersionedDoc (TextDocumentIdentifier uri) = do
589 fs <- vfsMap . vfs <$> get
591 case fs Map.!? toNormalizedUri uri of
592 Just vf -> Just (virtualFileVersion vf)
594 return (VersionedTextDocumentIdentifier uri ver)
596 -- | Applys an edit to the document and returns the updated document version.
597 applyEdit :: TextDocumentIdentifier -> TextEdit -> Session VersionedTextDocumentIdentifier
598 applyEdit doc edit = do
600 verDoc <- getVersionedDoc doc
602 caps <- asks sessionCapabilities
604 let supportsDocChanges = fromMaybe False $ do
605 let mWorkspace = caps ^. LSP.workspace
606 C.WorkspaceClientCapabilities _ mEdit _ _ _ _ _ _ <- mWorkspace
607 C.WorkspaceEditClientCapabilities mDocChanges _ _ <- mEdit
610 let wEdit = if supportsDocChanges
612 let docEdit = TextDocumentEdit verDoc (List [edit])
613 in WorkspaceEdit Nothing (Just (List [InL docEdit]))
615 let changes = HashMap.singleton (doc ^. uri) (List [edit])
616 in WorkspaceEdit (Just changes) Nothing
618 let req = RequestMessage "" (IdInt 0) SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wEdit)
619 updateState (FromServerMess SWorkspaceApplyEdit req)
621 -- version may have changed
624 -- | Returns the completions for the position in the document.
625 getCompletions :: TextDocumentIdentifier -> Position -> Session [CompletionItem]
626 getCompletions doc pos = do
627 rsp <- request STextDocumentCompletion (CompletionParams doc pos Nothing Nothing Nothing)
629 case getResponseResult rsp of
630 InL (List items) -> return items
631 InR (CompletionList _ (List items)) -> return items
633 -- | Returns the references for the position in the document.
634 getReferences :: TextDocumentIdentifier -- ^ The document to lookup in.
635 -> Position -- ^ The position to lookup.
636 -> Bool -- ^ Whether to include declarations as references.
637 -> Session (List Location) -- ^ The locations of the references.
638 getReferences doc pos inclDecl =
639 let ctx = ReferenceContext inclDecl
640 params = ReferenceParams doc pos Nothing Nothing ctx
641 in getResponseResult <$> request STextDocumentReferences params
643 -- | Returns the declarations(s) for the term at the specified position.
644 getDeclarations :: TextDocumentIdentifier -- ^ The document the term is in.
645 -> Position -- ^ The position the term is at.
646 -> Session ([Location] |? [LocationLink])
647 getDeclarations = getDeclarationyRequest STextDocumentDeclaration DeclarationParams
649 -- | Returns the definition(s) for the term at the specified position.
650 getDefinitions :: TextDocumentIdentifier -- ^ The document the term is in.
651 -> Position -- ^ The position the term is at.
652 -> Session ([Location] |? [LocationLink])
653 getDefinitions = getDeclarationyRequest STextDocumentDefinition DefinitionParams
655 -- | Returns the type definition(s) for the term at the specified position.
656 getTypeDefinitions :: TextDocumentIdentifier -- ^ The document the term is in.
657 -> Position -- ^ The position the term is at.
658 -> Session ([Location] |? [LocationLink])
659 getTypeDefinitions = getDeclarationyRequest STextDocumentTypeDefinition TypeDefinitionParams
661 -- | Returns the type definition(s) for the term at the specified position.
662 getImplementations :: TextDocumentIdentifier -- ^ The document the term is in.
663 -> Position -- ^ The position the term is at.
664 -> Session ([Location] |? [LocationLink])
665 getImplementations = getDeclarationyRequest STextDocumentImplementation ImplementationParams
668 getDeclarationyRequest :: (ResponseResult m ~ (Location |? (List Location |? List LocationLink)))
670 -> (TextDocumentIdentifier
672 -> Maybe ProgressToken
673 -> Maybe ProgressToken
675 -> TextDocumentIdentifier
677 -> Session ([Location] |? [LocationLink])
678 getDeclarationyRequest method paramCons doc pos = do
679 let params = paramCons doc pos Nothing Nothing
680 rsp <- request method params
681 case getResponseResult rsp of
682 InL loc -> pure (InL [loc])
683 InR (InL (List locs)) -> pure (InL locs)
684 InR (InR (List locLinks)) -> pure (InR locLinks)
686 -- | Renames the term at the specified position.
687 rename :: TextDocumentIdentifier -> Position -> String -> Session ()
688 rename doc pos newName = do
689 let params = RenameParams doc pos Nothing (T.pack newName)
690 rsp <- request STextDocumentRename params
691 let wEdit = getResponseResult rsp
692 req = RequestMessage "" (IdInt 0) SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wEdit)
693 updateState (FromServerMess SWorkspaceApplyEdit req)
695 -- | Returns the hover information at the specified position.
696 getHover :: TextDocumentIdentifier -> Position -> Session (Maybe Hover)
698 let params = HoverParams doc pos Nothing
699 in getResponseResult <$> request STextDocumentHover params
701 -- | Returns the highlighted occurences of the term at the specified position
702 getHighlights :: TextDocumentIdentifier -> Position -> Session (List DocumentHighlight)
703 getHighlights doc pos =
704 let params = DocumentHighlightParams doc pos Nothing Nothing
705 in getResponseResult <$> request STextDocumentDocumentHighlight params
707 -- | Checks the response for errors and throws an exception if needed.
708 -- Returns the result if successful.
709 getResponseResult :: ResponseMessage m -> ResponseResult m
710 getResponseResult rsp =
711 case rsp ^. result of
713 Left err -> throw $ UnexpectedResponseError (SomeLspId $ fromJust $ rsp ^. LSP.id) err
715 -- | Applies formatting to the specified document.
716 formatDoc :: TextDocumentIdentifier -> FormattingOptions -> Session ()
717 formatDoc doc opts = do
718 let params = DocumentFormattingParams Nothing doc opts
719 edits <- getResponseResult <$> request STextDocumentFormatting params
720 applyTextEdits doc edits
722 -- | Applies formatting to the specified range in a document.
723 formatRange :: TextDocumentIdentifier -> FormattingOptions -> Range -> Session ()
724 formatRange doc opts range = do
725 let params = DocumentRangeFormattingParams Nothing doc range opts
726 edits <- getResponseResult <$> request STextDocumentRangeFormatting params
727 applyTextEdits doc edits
729 applyTextEdits :: TextDocumentIdentifier -> List TextEdit -> Session ()
730 applyTextEdits doc edits =
731 let wEdit = WorkspaceEdit (Just (HashMap.singleton (doc ^. uri) edits)) Nothing
732 -- Send a dummy message to updateState so it can do bookkeeping
733 req = RequestMessage "" (IdInt 0) SWorkspaceApplyEdit (ApplyWorkspaceEditParams Nothing wEdit)
734 in updateState (FromServerMess SWorkspaceApplyEdit req)
736 -- | Returns the code lenses for the specified document.
737 getCodeLenses :: TextDocumentIdentifier -> Session [CodeLens]
738 getCodeLenses tId = do
739 rsp <- request STextDocumentCodeLens (CodeLensParams Nothing Nothing tId)
740 case getResponseResult rsp of
743 -- | Returns a list of capabilities that the server has requested to /dynamically/
744 -- register during the 'Session'.
747 getRegisteredCapabilities :: Session [SomeRegistration]
748 getRegisteredCapabilities = Map.elems . curDynCaps <$> get