X-Git-Url: http://git.lukelau.me/?p=lsp-test.git;a=blobdiff_plain;f=src%2FLanguage%2FHaskell%2FLSP%2FTest.hs;h=b7057c2d5a4127f00a519c5ed01aa3e2a9ecf6fc;hp=3ba86905c2d48d6c015812421253fa7319f3a3e6;hb=d7593d08be8201ef453c53a2205f4aa4a893df4c;hpb=bece589927102eb7cc66db5200ec11b568d35748 diff --git a/src/Language/Haskell/LSP/Test.hs b/src/Language/Haskell/LSP/Test.hs index 3ba8690..b7057c2 100644 --- a/src/Language/Haskell/LSP/Test.hs +++ b/src/Language/Haskell/LSP/Test.hs @@ -3,32 +3,36 @@ {-# LANGUAGE ScopedTypeVariables #-} {-# LANGUAGE ExistentialQuantification #-} --- | --- Module : Language.Haskell.LSP.Test --- Description : A functional testing framework for LSP servers. --- Maintainer : luke_lau@icloud.com --- Stability : experimental --- --- A framework for testing at the JSON level. - +{-| +Module : Language.Haskell.LSP.Test +Description : A functional testing framework for LSP servers. +Maintainer : luke_lau@icloud.com +Stability : experimental +Portability : POSIX + +A framework for testing + +functionally. +-} module Language.Haskell.LSP.Test ( -- * Sessions - runSession - , runSessionWithHandles + Session + , runSession + -- ** Config , runSessionWithConfig - , Session , SessionConfig(..) + , defaultConfig + , module Language.Haskell.LSP.Test.Capabilities + -- ** Exceptions , SessionException(..) , anySessionException , withTimeout -- * Sending + , request + , request_ , sendRequest - , sendRequest_ - , sendRequest' , sendNotification - , sendRequestMessage - , sendNotification' , sendResponse -- * Receving , message @@ -44,6 +48,7 @@ module Language.Haskell.LSP.Test , initializeResponse -- ** Documents , openDoc + , closeDoc , documentContents , getDocumentEdit , getDocUri @@ -52,6 +57,7 @@ module Language.Haskell.LSP.Test , getDocumentSymbols -- ** Diagnostics , waitForDiagnostics + , waitForDiagnosticsSource , noDiagnostics -- ** Commands , executeCommand @@ -60,6 +66,19 @@ module Language.Haskell.LSP.Test , executeCodeAction -- ** Completions , getCompletions + -- ** References + , getReferences + -- ** Definitions + , getDefinitions + -- ** Renaming + , rename + -- ** Hover + , getHover + -- ** Highlights + , getHighlights + -- ** Formatting + , formatDoc + , formatRange -- ** Edits , applyEdit ) where @@ -82,6 +101,7 @@ import qualified Language.Haskell.LSP.Types as LSP import qualified Language.Haskell.LSP.Types.Capabilities as LSP import Language.Haskell.LSP.Messages import Language.Haskell.LSP.VFS +import Language.Haskell.LSP.Test.Capabilities import Language.Haskell.LSP.Test.Compat import Language.Haskell.LSP.Test.Decoding import Language.Haskell.LSP.Test.Exceptions @@ -95,18 +115,20 @@ import qualified Yi.Rope as Rope -- | Starts a new session. runSession :: String -- ^ The command to run the server. + -> LSP.ClientCapabilities -- ^ The capabilities that the client should declare. -> FilePath -- ^ The filepath to the root directory for the session. -> Session a -- ^ The session to run. -> IO a runSession = runSessionWithConfig def -- | Starts a new sesion with a client with the specified capabilities. -runSessionWithConfig :: SessionConfig -- ^ The capabilities the client should have. +runSessionWithConfig :: SessionConfig -- ^ Configuration options for the session. -> String -- ^ The command to run the server. + -> LSP.ClientCapabilities -- ^ The capabilities that the client should declare. -> FilePath -- ^ The filepath to the root directory for the session. -> Session a -- ^ The session to run. -> IO a -runSessionWithConfig config serverExe rootDir session = do +runSessionWithConfig config serverExe caps rootDir session = do pid <- getCurrentProcessID absRootDir <- canonicalizePath rootDir @@ -114,13 +136,13 @@ runSessionWithConfig config serverExe rootDir session = do (Just $ T.pack absRootDir) (Just $ filePathToUri absRootDir) Nothing - (capabilities config) + caps (Just TraceOff) withServer serverExe (logStdErr config) $ \serverIn serverOut _ -> - runSessionWithHandles serverIn serverOut listenServer config rootDir $ do + runSessionWithHandles serverIn serverOut listenServer config caps rootDir $ do -- Wrap the session around initialize and shutdown calls - initRspMsg <- sendRequest Initialize initializeParams :: Session InitializeResponse + initRspMsg <- request Initialize initializeParams :: Session InitializeResponse liftIO $ maybe (return ()) (putStrLn . ("Error while initializing: " ++) . show ) (initRspMsg ^. LSP.error) @@ -180,25 +202,25 @@ getDocumentEdit doc = do in maybe False (HashMap.member (doc ^. uri)) mMap -- | Sends a request to the server and waits for its response. +-- Will skip any messages in between the request and the response -- @ --- rsp <- sendRequest TextDocumentDocumentSymbol params :: Session DocumentSymbolsResponse +-- rsp <- request TextDocumentDocumentSymbol params :: Session DocumentSymbolsResponse -- @ -- Note: will skip any messages in between the request and the response. -sendRequest :: (ToJSON params, FromJSON a) => ClientMethod -> params -> Session (ResponseMessage a) -sendRequest m = sendRequest' m >=> skipManyTill anyMessage . responseForId +request :: (ToJSON params, FromJSON a) => ClientMethod -> params -> Session (ResponseMessage a) +request m = sendRequest m >=> skipManyTill anyMessage . responseForId --- | Send a request to the server and wait for its response, --- but discard it. -sendRequest_ :: ToJSON params => ClientMethod -> params -> Session () -sendRequest_ p = void . (sendRequest p :: ToJSON params => params -> Session (ResponseMessage Value)) +-- | The same as 'sendRequest', but discard the response. +request_ :: ToJSON params => ClientMethod -> params -> Session () +request_ p = void . (request p :: ToJSON params => params -> Session (ResponseMessage Value)) --- | Sends a request to the server without waiting on the response. -sendRequest' +-- | Sends a request to the server. Unlike 'request', this doesn't wait for the response. +sendRequest :: ToJSON params => ClientMethod -- ^ The request method. -> params -- ^ The request parameters. -> Session LspId -- ^ The id of the request that was sent. -sendRequest' method params = do +sendRequest method params = do id <- curReqId <$> get modify $ \c -> c { curReqId = nextId id } @@ -226,15 +248,6 @@ instance ToJSON a => ToJSON (RequestMessage' a) where object ["jsonrpc" .= rpc, "id" .= id, "method" .= method, "params" .= params] -sendRequestMessage :: (ToJSON a, ToJSON b) => RequestMessage ClientMethod a b -> Session () -sendRequestMessage req = do - -- Update the request map - reqMap <- requestMap <$> ask - liftIO $ modifyMVar_ reqMap $ - \r -> return $ updateRequestMap r (req ^. LSP.id) (req ^. method) - - sendMessage req - -- | Sends a notification to the server. sendNotification :: ToJSON a => ClientMethod -- ^ The notification method. @@ -249,7 +262,7 @@ sendNotification TextDocumentDidOpen params = do oldVFS <- vfs <$> get newVFS <- liftIO $ openVFS oldVFS n modify (\s -> s { vfs = newVFS }) - sendNotification' n + sendMessage n -- | Close a virtual file if we send a close text document notification sendNotification TextDocumentDidClose params = do @@ -259,12 +272,9 @@ sendNotification TextDocumentDidClose params = do oldVFS <- vfs <$> get newVFS <- liftIO $ closeVFS oldVFS n modify (\s -> s { vfs = newVFS }) - sendNotification' n - -sendNotification method params = sendNotification' (NotificationMessage "2.0" method params) + sendMessage n -sendNotification' :: (ToJSON a, ToJSON b) => NotificationMessage a b -> Session () -sendNotification' = sendMessage +sendNotification method params = sendMessage (NotificationMessage "2.0" method params) sendResponse :: ToJSON a => ResponseMessage a -> Session () sendResponse = sendMessage @@ -292,6 +302,17 @@ openDoc file languageId = do contents <- liftIO $ T.readFile fp return $ TextDocumentItem (filePathToUri fp) (T.pack languageId) 0 contents +-- | Closes a text document and sends a notification to the client. +closeDoc :: TextDocumentIdentifier -> Session () +closeDoc docId = do + let params = DidCloseTextDocumentParams (TextDocumentIdentifier (docId ^. uri)) + sendNotification TextDocumentDidClose params + + oldVfs <- vfs <$> get + let notif = NotificationMessage "" TextDocumentDidClose params + newVfs <- liftIO $ closeVFS oldVfs notif + modify $ \s -> s { vfs = newVfs } + -- | Gets the Uri for the file corrected to the session directory. getDocUri :: FilePath -> Session Uri getDocUri file = do @@ -306,6 +327,17 @@ waitForDiagnostics = do let (List diags) = diagsNot ^. params . LSP.diagnostics return diags +waitForDiagnosticsSource :: String -> Session [Diagnostic] +waitForDiagnosticsSource src = do + diags <- waitForDiagnostics + let res = filter matches diags + if null res + then waitForDiagnosticsSource src + else return res + where + matches :: Diagnostic -> Bool + matches d = d ^. source == Just (T.pack src) + -- | Expects a 'PublishDiagnosticsNotification' and throws an -- 'UnexpectedDiagnosticsException' if there are any diagnostics -- returned. @@ -317,7 +349,7 @@ noDiagnostics = do -- | Returns the symbols in a document. getDocumentSymbols :: TextDocumentIdentifier -> Session [SymbolInformation] getDocumentSymbols doc = do - ResponseMessage _ rspLid mRes mErr <- sendRequest TextDocumentDocumentSymbol (DocumentSymbolParams doc) + ResponseMessage _ rspLid mRes mErr <- request TextDocumentDocumentSymbol (DocumentSymbolParams doc) maybe (return ()) (throw . UnexpectedResponseError rspLid) mErr let (Just (List symbols)) = mRes return symbols @@ -335,7 +367,7 @@ getAllCodeActions doc = do where go :: CodeActionContext -> [CommandOrCodeAction] -> Diagnostic -> Session [CommandOrCodeAction] go ctx acc diag = do - ResponseMessage _ rspLid mRes mErr <- sendRequest TextDocumentCodeAction (CodeActionParams doc (diag ^. range) ctx) + ResponseMessage _ rspLid mRes mErr <- request TextDocumentCodeAction (CodeActionParams doc (diag ^. range) ctx) case mErr of Just e -> throw (UnexpectedResponseError rspLid e) @@ -348,7 +380,7 @@ executeCommand :: Command -> Session () executeCommand cmd = do let args = decode $ encode $ fromJust $ cmd ^. arguments execParams = ExecuteCommandParams (cmd ^. command) args - sendRequest_ WorkspaceExecuteCommand execParams + request_ WorkspaceExecuteCommand execParams -- | Executes a code action. -- Matching with the specification, if a code action @@ -381,7 +413,7 @@ applyEdit doc edit = do verDoc <- getVersionedDoc doc - caps <- asks (capabilities . config) + caps <- asks sessionCapabilities let supportsDocChanges = fromMaybe False $ do let LSP.ClientCapabilities mWorkspace _ _ = caps @@ -406,11 +438,74 @@ applyEdit doc edit = do -- | Returns the completions for the position in the document. getCompletions :: TextDocumentIdentifier -> Position -> Session [CompletionItem] getCompletions doc pos = do - rsp <- sendRequest TextDocumentCompletion (TextDocumentPositionParams doc pos) + rsp <- request TextDocumentCompletion (TextDocumentPositionParams doc pos) - let exc = throw $ UnexpectedResponseError (rsp ^. LSP.id) - (fromJust $ rsp ^. LSP.error) - res = fromMaybe exc (rsp ^. result) - case res of + case getResponseResult rsp of Completions (List items) -> return items CompletionList (CompletionListType _ (List items)) -> return items + +-- | Returns the references for the position in the document. +getReferences :: TextDocumentIdentifier -- ^ The document to lookup in. + -> Position -- ^ The position to lookup. + -> Bool -- ^ Whether to include declarations as references. + -> Session [Location] -- ^ The locations of the references. +getReferences doc pos inclDecl = + let ctx = ReferenceContext inclDecl + params = ReferenceParams doc pos ctx + in getResponseResult <$> request TextDocumentReferences params + +-- | Returns the definition(s) for the term at the specified position. +getDefinitions :: TextDocumentIdentifier -- ^ The document the term is in. + -> Position -- ^ The position the term is at. + -> Session [Location] -- ^ The location(s) of the definitions +getDefinitions doc pos = + let params = TextDocumentPositionParams doc pos + in getResponseResult <$> request TextDocumentDefinition params + +-- ^ Renames the term at the specified position. +rename :: TextDocumentIdentifier -> Position -> String -> Session () +rename doc pos newName = do + let params = RenameParams doc pos (T.pack newName) + rsp <- request TextDocumentRename params :: Session RenameResponse + let wEdit = getResponseResult rsp + req = RequestMessage "" (IdInt 0) WorkspaceApplyEdit (ApplyWorkspaceEditParams wEdit) + updateState (ReqApplyWorkspaceEdit req) + +-- | Returns the hover information at the specified position. +getHover :: TextDocumentIdentifier -> Position -> Session (Maybe Hover) +getHover doc pos = + let params = TextDocumentPositionParams doc pos + in getResponseResult <$> request TextDocumentHover params + +-- | Returns the highlighted occurences of the term at the specified position +getHighlights :: TextDocumentIdentifier -> Position -> Session [DocumentHighlight] +getHighlights doc pos = + let params = TextDocumentPositionParams doc pos + in getResponseResult <$> request TextDocumentDocumentHighlight params + +-- | Checks the response for errors and throws an exception if needed. +-- Returns the result if successful. +getResponseResult :: ResponseMessage a -> a +getResponseResult rsp = fromMaybe exc (rsp ^. result) + where exc = throw $ UnexpectedResponseError (rsp ^. LSP.id) + (fromJust $ rsp ^. LSP.error) + +-- | Applies formatting to the specified document. +formatDoc :: TextDocumentIdentifier -> FormattingOptions -> Session () +formatDoc doc opts = do + let params = DocumentFormattingParams doc opts + edits <- getResponseResult <$> request TextDocumentFormatting params + applyTextEdits doc edits + +-- | Applies formatting to the specified range in a document. +formatRange :: TextDocumentIdentifier -> FormattingOptions -> Range -> Session () +formatRange doc opts range = do + let params = DocumentRangeFormattingParams doc range opts + edits <- getResponseResult <$> request TextDocumentRangeFormatting params + applyTextEdits doc edits + +applyTextEdits :: TextDocumentIdentifier -> List TextEdit -> Session () +applyTextEdits doc edits = + let wEdit = WorkspaceEdit (Just (HashMap.singleton (doc ^. uri) edits)) Nothing + req = RequestMessage "" (IdInt 0) WorkspaceApplyEdit (ApplyWorkspaceEditParams wEdit) + in updateState (ReqApplyWorkspaceEdit req)