X-Git-Url: http://git.lukelau.me/?a=blobdiff_plain;f=src%2FLanguage%2FHaskell%2FLSP%2FTest%2FSession.hs;h=1fee2be798f4ebdf504667556d5aa0523bd3e280;hb=cdb1ba7038c32bac71a3bc783effc1e07049a985;hp=2418cd5ac6e09cab04be5b23c813a0fe0d987a1a;hpb=a4c1143848809be8aed55403dc3187a256dcbe9b;p=lsp-test.git diff --git a/src/Language/Haskell/LSP/Test/Session.hs b/src/Language/Haskell/LSP/Test/Session.hs index 2418cd5..1fee2be 100644 --- a/src/Language/Haskell/LSP/Test/Session.hs +++ b/src/Language/Haskell/LSP/Test/Session.hs @@ -6,19 +6,22 @@ module Language.Haskell.LSP.Test.Session ( Session , SessionConfig(..) + , defaultConfig , SessionMessage(..) , SessionContext(..) , SessionState(..) - , MonadSessionConfig(..) , runSessionWithHandles , get , put , modify + , modifyM , ask , asks , sendMessage , updateState , withTimeout + , logMsg + , LogMsgType(..) ) where @@ -35,6 +38,7 @@ import Control.Monad.Trans.State (StateT, runStateT) import qualified Control.Monad.Trans.State as State (get, put) import qualified Data.ByteString.Lazy.Char8 as B import Data.Aeson +import Data.Aeson.Encode.Pretty import Data.Conduit as Conduit import Data.Conduit.Parser as Parser import Data.Default @@ -45,8 +49,9 @@ import qualified Data.Text as T import qualified Data.Text.IO as T import qualified Data.HashMap.Strict as HashMap import Data.Maybe +import Data.Function import Language.Haskell.LSP.Messages -import Language.Haskell.LSP.TH.ClientCapabilities +import Language.Haskell.LSP.Types.Capabilities import Language.Haskell.LSP.Types hiding (error) import Language.Haskell.LSP.VFS import Language.Haskell.LSP.Test.Decoding @@ -70,20 +75,18 @@ type Session = ParserStateReader FromServerMessage SessionState SessionContext I -- | Stuff you can configure for a 'Session'. data SessionConfig = SessionConfig - { - capabilities :: ClientCapabilities -- ^ Specific capabilities the client should advertise. Default is yes to everything. - , messageTimeout :: Int -- ^ Maximum time to wait for a message in seconds. Defaults to 60. - , logStdErr :: Bool -- ^ When True redirects the servers stderr output to haskell-lsp-test's stdout. Defaults to False + { messageTimeout :: Int -- ^ Maximum time to wait for a message in seconds, defaults to 60. + , logStdErr :: Bool -- ^ Redirect the server's stderr to this stdout, defaults to False. + , logMessages :: Bool -- ^ Trace the messages sent and received to stdout, defaults to True. + , logColor :: Bool -- ^ Add ANSI color to the logged messages, defaults to True. } -instance Default SessionConfig where - def = SessionConfig def 60 False - -class Monad m => MonadSessionConfig m where - sessionConfig :: m SessionConfig +-- | The configuration used in 'Language.Haskell.LSP.Test.runSession'. +defaultConfig :: SessionConfig +defaultConfig = SessionConfig 60 False True True -instance Monad m => MonadSessionConfig (StateT SessionState (ReaderT SessionContext m)) where - sessionConfig = config <$> lift Reader.ask +instance Default SessionConfig where + def = defaultConfig data SessionMessage = ServerMessage FromServerMessage | TimeoutMessage Int @@ -97,6 +100,7 @@ data SessionContext = SessionContext , requestMap :: MVar RequestMap , initRsp :: MVar InitializeResponse , config :: SessionConfig + , sessionCapabilities :: ClientCapabilities } class Monad m => HasReader r m where @@ -130,6 +134,9 @@ class Monad m => HasState s m where modify :: (s -> s) -> m () modify f = get >>= put . f + modifyM :: (HasState s m, Monad m) => (s -> m s) -> m () + modifyM f = get >>= f >>= put + instance Monad m => HasState s (ParserStateReader a s r m) where get = lift State.get put = lift . State.put @@ -142,16 +149,14 @@ instance Monad m => HasState SessionState (ConduitM a b (StateT SessionState m)) type ParserStateReader a s r m = ConduitParser a (StateT s (ReaderT r m)) runSession :: SessionContext -> SessionState -> Session a -> IO (a, SessionState) -runSession context state session = - -- source <- sourceList <$> getChanContents (messageChan context) - runReaderT (runStateT conduit state) context +runSession context state session = runReaderT (runStateT conduit state) context where conduit = runConduit $ chanSource .| watchdog .| updateStateC .| runConduitParser (catchError session handler) handler (Unexpected "ConduitParser.empty") = do lastMsg <- fromJust . lastReceivedMessage <$> get name <- getParserName - liftIO $ throw (UnexpectedMessageException (T.unpack name) lastMsg) + liftIO $ throw (UnexpectedMessage (T.unpack name) lastMsg) handler e = throw e @@ -160,13 +165,12 @@ runSession context state session = yield msg chanSource - watchdog :: ConduitM SessionMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) () watchdog = Conduit.awaitForever $ \msg -> do curId <- curTimeoutId <$> get case msg of ServerMessage sMsg -> yield sMsg - TimeoutMessage tId -> when (curId == tId) $ throw TimeoutException + TimeoutMessage tId -> when (curId == tId) $ throw Timeout -- | An internal version of 'runSession' that allows for a custom handler to listen to the server. -- It also does not automatically send initialize and exit messages. @@ -174,10 +178,11 @@ runSessionWithHandles :: Handle -- ^ Server in -> Handle -- ^ Server out -> (Handle -> SessionContext -> IO ()) -- ^ Server listener -> SessionConfig - -> FilePath + -> ClientCapabilities + -> FilePath -- ^ Root directory -> Session a -> IO a -runSessionWithHandles serverIn serverOut serverHandler config rootDir session = do +runSessionWithHandles serverIn serverOut serverHandler config caps rootDir session = do absRootDir <- canonicalizePath rootDir hSetBuffering serverIn NoBuffering @@ -187,7 +192,7 @@ runSessionWithHandles serverIn serverOut serverHandler config rootDir session = messageChan <- newChan initRsp <- newEmptyMVar - let context = SessionContext serverIn absRootDir messageChan reqMap initRsp config + let context = SessionContext serverIn absRootDir messageChan reqMap initRsp config caps initState = SessionState (IdInt 0) mempty mempty 0 False Nothing threadId <- forkIO $ void $ serverHandler serverOut context @@ -222,9 +227,9 @@ updateState (ReqApplyWorkspaceEdit r) = do return $ concatMap (uncurry getChangeParams) (HashMap.toList cs) Nothing -> error "No changes!" - oldVFS <- vfs <$> get - newVFS <- liftIO $ changeFromServerVFS oldVFS r - modify (\s -> s { vfs = newVFS }) + modifyM $ \s -> do + newVFS <- liftIO $ changeFromServerVFS (vfs s) r + return $ s { vfs = newVFS } let groupedParams = groupBy (\a b -> (a ^. textDocument == b ^. textDocument)) allChangeParams mergedParams = map mergeParams groupedParams @@ -232,6 +237,18 @@ updateState (ReqApplyWorkspaceEdit r) = do -- TODO: Don't do this when replaying a session forM_ mergedParams (sendMessage . NotificationMessage "2.0" TextDocumentDidChange) + -- Update VFS to new document versions + let sortedVersions = map (sortBy (compare `on` (^. textDocument . version))) groupedParams + latestVersions = map ((^. textDocument) . last) sortedVersions + bumpedVersions = map (version . _Just +~ 1) latestVersions + + forM_ bumpedVersions $ \(VersionedTextDocumentIdentifier uri v) -> + modify $ \s -> + let oldVFS = vfs s + update (VirtualFile oldV t) = VirtualFile (fromMaybe oldV v) t + newVFS = Map.adjust update uri oldVFS + in s { vfs = newVFS } + where checkIfNeedsOpened uri = do oldVFS <- vfs <$> get ctx <- ask @@ -244,15 +261,15 @@ updateState (ReqApplyWorkspaceEdit r) = do msg = NotificationMessage "2.0" TextDocumentDidOpen (DidOpenTextDocumentParams item) liftIO $ B.hPut (serverIn ctx) $ addHeader (encode msg) - oldVFS <- vfs <$> get - newVFS <- liftIO $ openVFS oldVFS msg - modify (\s -> s { vfs = newVFS }) + modifyM $ \s -> do + newVFS <- liftIO $ openVFS (vfs s) msg + return $ s { vfs = newVFS } getParams (TextDocumentEdit docId (List edits)) = let changeEvents = map (\e -> TextDocumentContentChangeEvent (Just (e ^. range)) Nothing (e ^. newText)) edits in DidChangeTextDocumentParams docId (List changeEvents) - textDocumentVersions uri = map (VersionedTextDocumentIdentifier uri) [0..] + textDocumentVersions uri = map (VersionedTextDocumentIdentifier uri . Just) [0..] textDocumentEdits uri edits = map (\(v, e) -> TextDocumentEdit v (List [e])) $ zip (textDocumentVersions uri) edits @@ -266,14 +283,8 @@ updateState _ = return () sendMessage :: (MonadIO m, HasReader SessionContext m, ToJSON a) => a -> m () sendMessage msg = do h <- serverIn <$> ask - let encoded = encode msg - liftIO $ do - - setSGR [SetColor Foreground Vivid Cyan] - putStrLn $ "--> " ++ B.unpack encoded - setSGR [Reset] - - B.hPut h (addHeader encoded) + logMsg LogClient msg + liftIO $ B.hPut h (addHeader $ encode msg) -- | Execute a block f that will throw a 'TimeoutException' -- after duration seconds. This will override the global timeout @@ -291,3 +302,26 @@ withTimeout duration f = do overridingTimeout = False } return res + +data LogMsgType = LogServer | LogClient + deriving Eq + +-- | Logs the message if the config specified it +logMsg :: (ToJSON a, MonadIO m, HasReader SessionContext m) + => LogMsgType -> a -> m () +logMsg t msg = do + shouldLog <- asks $ logMessages . config + shouldColor <- asks $ logColor . config + liftIO $ when shouldLog $ do + when shouldColor $ setSGR [SetColor Foreground Dull color] + putStrLn $ arrow ++ showPretty msg + when shouldColor $ setSGR [Reset] + + where arrow + | t == LogServer = "<-- " + | otherwise = "--> " + color + | t == LogServer = Magenta + | otherwise = Cyan + + showPretty = B.unpack . encodePretty