X-Git-Url: http://git.lukelau.me/?p=lsp-test.git;a=blobdiff_plain;f=src%2FLanguage%2FHaskell%2FLSP%2FTest.hs;h=696f84aa4df05bae70d411feb75675de2f2a076e;hp=568ead8ff592846fbc77e0e41adbaa9e08a2064b;hb=59380ca81c003f777fdfd48b92fbe873474fb9a8;hpb=f0d93bbe47d55ab650909e0487c65c1048f1bb9a diff --git a/src/Language/Haskell/LSP/Test.hs b/src/Language/Haskell/LSP/Test.hs index 568ead8..696f84a 100644 --- a/src/Language/Haskell/LSP/Test.hs +++ b/src/Language/Haskell/LSP/Test.hs @@ -3,49 +3,41 @@ {-# 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 --- --- functionally. - +{-| +Module : Language.Haskell.LSP.Test +Description : A functional testing framework for LSP servers. +Maintainer : luke_lau@icloud.com +Stability : experimental +Portability : POSIX + +Provides the framework to start functionally testing +. +You should import "Language.Haskell.LSP.Types" alongside this. +-} module Language.Haskell.LSP.Test ( -- * Sessions - runSession - , runSessionWithHandles + Session + , runSession + -- ** Config , runSessionWithConfig - , Session , SessionConfig(..) , defaultConfig - , SessionException(..) - , anySessionException + , C.fullCaps + -- ** Exceptions + , module Language.Haskell.LSP.Test.Exceptions , withTimeout - -- * Capabilities - , fullCaps -- * Sending + , request + , request_ , sendRequest - , sendRequest_ - , sendRequest' , sendNotification - , sendRequestMessage - , sendNotification' , sendResponse -- * Receving - , message - , anyRequest - , anyResponse - , anyNotification - , anyMessage - , loggingNotification - , publishDiagnosticsNotification - -- * Combinators - , satisfy + , module Language.Haskell.LSP.Test.Parsing -- * Utilities + -- | Quick helper functions for common tasks. + -- ** Initialization , initializeResponse -- ** Documents , openDoc @@ -60,9 +52,11 @@ module Language.Haskell.LSP.Test , waitForDiagnostics , waitForDiagnosticsSource , noDiagnostics + , getCurrentDiagnostics -- ** Commands , executeCommand -- ** Code Actions + , getCodeActions , getAllCodeActions , executeCodeAction -- ** Completions @@ -71,6 +65,7 @@ module Language.Haskell.LSP.Test , getReferences -- ** Definitions , getDefinitions + , getTypeDefinitions -- ** Renaming , rename -- ** Hover @@ -97,12 +92,13 @@ import Data.Default import qualified Data.HashMap.Strict as HashMap import qualified Data.Map as Map import Data.Maybe -import Language.Haskell.LSP.Types hiding (id, capabilities, message) -import qualified Language.Haskell.LSP.Types as LSP -import qualified Language.Haskell.LSP.Types.Capabilities as LSP +import Language.Haskell.LSP.Types +import Language.Haskell.LSP.Types.Lens hiding + (id, capabilities, message, executeCommand, applyEdit, rename) +import qualified Language.Haskell.LSP.Types.Lens as LSP +import qualified Language.Haskell.LSP.Types.Capabilities as C 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 @@ -112,20 +108,27 @@ import Language.Haskell.LSP.Test.Server import System.IO import System.Directory import System.FilePath -import qualified Yi.Rope as Rope +import qualified Data.Rope.UTF16 as Rope -- | Starts a new session. +-- +-- > runSession "hie" fullCaps "path/to/root/dir" $ do +-- > doc <- openDoc "Desktop/simple.hs" "haskell" +-- > diags <- waitForDiagnostics +-- > let pos = Position 12 5 +-- > params = TextDocumentPositionParams doc +-- > hover <- request TextDocumentHover params runSession :: String -- ^ The command to run the server. - -> LSP.ClientCapabilities -- ^ The capabilities that the client should declare. + -> C.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. +-- | Starts a new sesion with a custom configuration. runSessionWithConfig :: SessionConfig -- ^ Configuration options for the session. -> String -- ^ The command to run the server. - -> LSP.ClientCapabilities -- ^ The capabilities that the client should declare. + -> C.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 @@ -139,11 +142,12 @@ runSessionWithConfig config serverExe caps rootDir session = do Nothing caps (Just TraceOff) + Nothing withServer serverExe (logStdErr config) $ \serverIn serverOut _ -> 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) @@ -152,6 +156,10 @@ runSessionWithConfig config serverExe caps rootDir session = do sendNotification Initialized InitializedParams + case lspConfig config of + Just cfg -> sendNotification WorkspaceDidChangeConfiguration (DidChangeConfigurationParams cfg) + Nothing -> return () + -- Run the actual test result <- session @@ -203,25 +211,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 } @@ -249,22 +257,13 @@ 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. -> a -- ^ The notification parameters. -> Session () --- | Open a virtual file if we send a did open text document notification +-- Open a virtual file if we send a did open text document notification sendNotification TextDocumentDidOpen params = do let params' = fromJust $ decode $ encode params n :: DidOpenTextDocumentNotification @@ -272,9 +271,9 @@ 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 +-- Close a virtual file if we send a close text document notification sendNotification TextDocumentDidClose params = do let params' = fromJust $ decode $ encode params n :: DidCloseTextDocumentNotification @@ -282,13 +281,11 @@ sendNotification TextDocumentDidClose params = do oldVFS <- vfs <$> get newVFS <- liftIO $ closeVFS oldVFS n modify (\s -> s { vfs = newVFS }) - sendNotification' n + sendMessage n -sendNotification method params = sendNotification' (NotificationMessage "2.0" method params) - -sendNotification' :: (ToJSON a, ToJSON b) => NotificationMessage a b -> Session () -sendNotification' = sendMessage +sendNotification method params = sendMessage (NotificationMessage "2.0" method params) +-- | Sends a response to the server. sendResponse :: ToJSON a => ResponseMessage a -> Session () sendResponse = sendMessage @@ -340,6 +337,8 @@ waitForDiagnostics = do let (List diags) = diagsNot ^. params . LSP.diagnostics return diags +-- | The same as 'waitForDiagnostics', but will only match a specific +-- 'Language.Haskell.LSP.Types._source'. waitForDiagnosticsSource :: String -> Session [Diagnostic] waitForDiagnosticsSource src = do diags <- waitForDiagnostics @@ -352,7 +351,7 @@ waitForDiagnosticsSource src = do matches d = d ^. source == Just (T.pack src) -- | Expects a 'PublishDiagnosticsNotification' and throws an --- 'UnexpectedDiagnosticsException' if there are any diagnostics +-- 'UnexpectedDiagnostics' exception if there are any diagnostics -- returned. noDiagnostics :: Session () noDiagnostics = do @@ -360,27 +359,38 @@ noDiagnostics = do when (diagsNot ^. params . LSP.diagnostics /= List []) $ liftIO $ throw UnexpectedDiagnostics -- | Returns the symbols in a document. -getDocumentSymbols :: TextDocumentIdentifier -> Session [SymbolInformation] +getDocumentSymbols :: TextDocumentIdentifier -> Session (Either [DocumentSymbol] [SymbolInformation]) getDocumentSymbols doc = do - ResponseMessage _ rspLid mRes mErr <- sendRequest TextDocumentDocumentSymbol (DocumentSymbolParams doc) + ResponseMessage _ rspLid mRes mErr <- request TextDocumentDocumentSymbol (DocumentSymbolParams doc) :: Session DocumentSymbolsResponse maybe (return ()) (throw . UnexpectedResponseError rspLid) mErr - let (Just (List symbols)) = mRes - return symbols + case mRes of + Just (DSDocumentSymbols (List xs)) -> return (Left xs) + Just (DSSymbolInformation (List xs)) -> return (Right xs) + Nothing -> Prelude.error "No result and no error in DocumentSymbolsResponse" + +-- | Returns the code actions in the specified range. +getCodeActions :: TextDocumentIdentifier -> Range -> Session [CAResult] +getCodeActions doc range = do + ctx <- getCodeActionContext doc + rsp <- request TextDocumentCodeAction (CodeActionParams doc range ctx) + + case rsp ^. result of + Just (List xs) -> return xs + _ -> throw (UnexpectedResponseError (rsp ^. LSP.id) (fromJust $ rsp ^. LSP.error)) -- | Returns all the code actions in a document by -- querying the code actions at each of the current -- diagnostics' positions. -getAllCodeActions :: TextDocumentIdentifier -> Session [CommandOrCodeAction] +getAllCodeActions :: TextDocumentIdentifier -> Session [CAResult] getAllCodeActions doc = do - curDiags <- fromMaybe [] . Map.lookup (doc ^. uri) . curDiagnostics <$> get - let ctx = CodeActionContext (List curDiags) Nothing + ctx <- getCodeActionContext doc - foldM (go ctx) [] curDiags + foldM (go ctx) [] =<< getCurrentDiagnostics doc where - go :: CodeActionContext -> [CommandOrCodeAction] -> Diagnostic -> Session [CommandOrCodeAction] + go :: CodeActionContext -> [CAResult] -> Diagnostic -> Session [CAResult] 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) @@ -388,12 +398,22 @@ getAllCodeActions doc = do let Just (List cmdOrCAs) = mRes in return (acc ++ cmdOrCAs) +getCodeActionContext :: TextDocumentIdentifier -> Session CodeActionContext +getCodeActionContext doc = do + curDiags <- getCurrentDiagnostics doc + return $ CodeActionContext (List curDiags) Nothing + +-- | Returns the current diagnostics that have been sent to the client. +-- Note that this does not wait for more to come in. +getCurrentDiagnostics :: TextDocumentIdentifier -> Session [Diagnostic] +getCurrentDiagnostics doc = fromMaybe [] . Map.lookup (doc ^. uri) . curDiagnostics <$> get + -- | Executes a command. 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 @@ -429,9 +449,9 @@ applyEdit doc edit = do caps <- asks sessionCapabilities let supportsDocChanges = fromMaybe False $ do - let LSP.ClientCapabilities mWorkspace _ _ = caps - LSP.WorkspaceClientCapabilities _ mEdit _ _ _ _ <- mWorkspace - LSP.WorkspaceEditClientCapabilities mDocChanges <- mEdit + let mWorkspace = C._workspace caps + C.WorkspaceClientCapabilities _ mEdit _ _ _ _ _ _ <- mWorkspace + C.WorkspaceEditClientCapabilities mDocChanges <- mEdit mDocChanges let wEdit = if supportsDocChanges @@ -451,7 +471,7 @@ 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) case getResponseResult rsp of Completions (List items) -> return items @@ -465,7 +485,7 @@ getReferences :: TextDocumentIdentifier -- ^ The document to lookup in. getReferences doc pos inclDecl = let ctx = ReferenceContext inclDecl params = ReferenceParams doc pos ctx - in getResponseResult <$> sendRequest TextDocumentReferences params + in getResponseResult <$> request TextDocumentReferences params -- | Returns the definition(s) for the term at the specified position. getDefinitions :: TextDocumentIdentifier -- ^ The document the term is in. @@ -473,13 +493,21 @@ getDefinitions :: TextDocumentIdentifier -- ^ The document the term is in. -> Session [Location] -- ^ The location(s) of the definitions getDefinitions doc pos = let params = TextDocumentPositionParams doc pos - in getResponseResult <$> sendRequest TextDocumentDefinition params + in getResponseResult <$> request TextDocumentDefinition params --- ^ Renames the term at the specified position. +-- | Returns the type definition(s) for the term at the specified position. +getTypeDefinitions :: TextDocumentIdentifier -- ^ The document the term is in. + -> Position -- ^ The position the term is at. + -> Session [Location] -- ^ The location(s) of the definitions +getTypeDefinitions doc pos = + let params = TextDocumentPositionParams doc pos + in getResponseResult <$> request TextDocumentTypeDefinition 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 <- sendRequest TextDocumentRename params :: Session RenameResponse + rsp <- request TextDocumentRename params :: Session RenameResponse let wEdit = getResponseResult rsp req = RequestMessage "" (IdInt 0) WorkspaceApplyEdit (ApplyWorkspaceEditParams wEdit) updateState (ReqApplyWorkspaceEdit req) @@ -488,13 +516,13 @@ rename doc pos newName = do getHover :: TextDocumentIdentifier -> Position -> Session (Maybe Hover) getHover doc pos = let params = TextDocumentPositionParams doc pos - in getResponseResult <$> sendRequest TextDocumentHover params + 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 <$> sendRequest TextDocumentDocumentHighlight params + in getResponseResult <$> request TextDocumentDocumentHighlight params -- | Checks the response for errors and throws an exception if needed. -- Returns the result if successful. @@ -507,14 +535,14 @@ getResponseResult rsp = fromMaybe exc (rsp ^. result) formatDoc :: TextDocumentIdentifier -> FormattingOptions -> Session () formatDoc doc opts = do let params = DocumentFormattingParams doc opts - edits <- getResponseResult <$> sendRequest TextDocumentFormatting params + 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 <$> sendRequest TextDocumentRangeFormatting params + edits <- getResponseResult <$> request TextDocumentRangeFormatting params applyTextEdits doc edits applyTextEdits :: TextDocumentIdentifier -> List TextEdit -> Session () @@ -522,4 +550,3 @@ applyTextEdits doc edits = let wEdit = WorkspaceEdit (Just (HashMap.singleton (doc ^. uri) edits)) Nothing req = RequestMessage "" (IdInt 0) WorkspaceApplyEdit (ApplyWorkspaceEditParams wEdit) in updateState (ReqApplyWorkspaceEdit req) -