Hide some internals
[lsp-test.git] / src / Language / Haskell / LSP / Test.hs
index e47109e03da0f1e83c84466423ad2fd2cd2262aa..b7057c2d5a4127f00a519c5ed01aa3e2a9ecf6fc 100644 (file)
@@ -3,34 +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
--- <https://github.com/Microsoft/language-server-protocol Language Server Protocol servers>
--- functionally.
-
+{-|
+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
+<https://github.com/Microsoft/language-server-protocol Language Server Protocol servers>
+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
@@ -46,6 +48,7 @@ module Language.Haskell.LSP.Test
   , initializeResponse
   -- ** Documents
   , openDoc
+  , closeDoc
   , documentContents
   , getDocumentEdit
   , getDocUri
@@ -73,6 +76,9 @@ module Language.Haskell.LSP.Test
   , getHover
   -- ** Highlights
   , getHighlights
+  -- ** Formatting
+  , formatDoc
+  , formatRange
   -- ** Edits
   , applyEdit
   ) where
@@ -95,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
@@ -108,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
 
@@ -127,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)
 
@@ -193,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 }
 
@@ -239,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.
@@ -262,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
@@ -272,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
@@ -305,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
@@ -341,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
@@ -359,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)
@@ -372,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
@@ -405,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
@@ -430,7 +438,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
@@ -444,7 +452,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.
@@ -452,27 +460,28 @@ 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.
 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)
 
--- ^ Returns the hover information at the specified position.
+-- | Returns the hover information at the specified position.
 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.
@@ -481,3 +490,22 @@ 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)