+{-# LANGUAGE CPP #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE FlexibleContexts #-}
+{-# LANGUAGE RankNTypes #-}
module Language.Haskell.LSP.Test.Session
( Session
, SessionConfig(..)
+ , defaultConfig
, SessionMessage(..)
, SessionContext(..)
, SessionState(..)
, sendMessage
, updateState
, withTimeout
+ , logMsg
+ , LogMsgType(..)
)
where
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.Except
+#if __GLASGOW_HASKELL__ >= 806
+import Control.Monad.Fail
+#endif
import Control.Monad.Trans.Reader (ReaderT, runReaderT)
import qualified Control.Monad.Trans.Reader as Reader (ask)
import Control.Monad.Trans.State (StateT, runStateT)
import Data.Function
import Language.Haskell.LSP.Messages
import Language.Haskell.LSP.Types.Capabilities
-import Language.Haskell.LSP.Types hiding (error)
+import Language.Haskell.LSP.Types
+import Language.Haskell.LSP.Types.Lens hiding (error)
import Language.Haskell.LSP.VFS
import Language.Haskell.LSP.Test.Decoding
import Language.Haskell.LSP.Test.Exceptions
-- | A session representing one instance of launching and connecting to a server.
--
--- You can send and receive messages to the server within 'Session' via 'getMessage',
--- 'sendRequest' and 'sendNotification'.
---
--- @
--- runSession \"path\/to\/root\/dir\" $ do
--- docItem <- getDocItem "Desktop/simple.hs" "haskell"
--- sendNotification TextDocumentDidOpen (DidOpenTextDocumentParams docItem)
--- diagnostics <- getMessage :: Session PublishDiagnosticsNotification
--- @
+-- You can send and receive messages to the server within 'Session' via
+-- 'Language.Haskell.LSP.Test.message',
+-- 'Language.Haskell.LSP.Test.sendRequest' and
+-- 'Language.Haskell.LSP.Test.sendNotification'.
+
type Session = ParserStateReader FromServerMessage SessionState SessionContext IO
+#if __GLASGOW_HASKELL__ >= 806
+instance MonadFail Session where
+ fail s = do
+ lastMsg <- fromJust . lastReceivedMessage <$> get
+ liftIO $ throw (UnexpectedMessage s lastMsg)
+#endif
+
-- | Stuff you can configure for a 'Session'.
data SessionConfig = SessionConfig
- {
- 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.
- , logMessages :: Bool -- ^ When True traces the communication between client and server to stdout. Defaults to True.
+ { 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 False.
+ , logColor :: Bool -- ^ Add ANSI color to the logged messages, defaults to True.
+ , lspConfig :: Maybe Value -- ^ The initial LSP config as JSON value, defaults to Nothing.
}
+-- | The configuration used in 'Language.Haskell.LSP.Test.runSession'.
+defaultConfig :: SessionConfig
+defaultConfig = SessionConfig 60 False False True Nothing
+
instance Default SessionConfig where
- def = SessionConfig 60 False True
+ def = defaultConfig
data SessionMessage = ServerMessage FromServerMessage
| TimeoutMessage Int
yield msg
chanSource
-
watchdog :: ConduitM SessionMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) ()
watchdog = Conduit.awaitForever $ \msg -> do
curId <- curTimeoutId <$> get
hSetBuffering serverIn NoBuffering
hSetBuffering serverOut NoBuffering
+ -- This is required to make sure that we don’t get any
+ -- newline conversion or weird encoding issues.
+ hSetBinaryMode serverIn True
+ hSetBinaryMode serverOut True
reqMap <- newMVar newRequestMap
messageChan <- newChan
initRsp <- newEmptyMVar
+ mainThreadId <- myThreadId
+
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
- (result, _) <- runSession context initState session
-
- killThread threadId
+ launchServerHandler = forkIO $ catch (serverHandler serverOut context)
+ (throwTo mainThreadId :: SessionException -> IO ())
+ (result, _) <- bracket launchServerHandler killThread $
+ const $ runSession context initState session
return result
forM_ bumpedVersions $ \(VersionedTextDocumentIdentifier uri v) ->
modify $ \s ->
let oldVFS = vfs s
- update (VirtualFile oldV t) = VirtualFile (fromMaybe oldV v) t
+ update (VirtualFile oldV t mf) = VirtualFile (fromMaybe oldV v) t mf
newVFS = Map.adjust update uri oldVFS
in s { vfs = newVFS }
sendMessage :: (MonadIO m, HasReader SessionContext m, ToJSON a) => a -> m ()
sendMessage msg = do
h <- serverIn <$> ask
- let encoded = encodePretty msg
-
- shouldLog <- asks $ logMessages . config
- liftIO $ when shouldLog $ do
-
- setSGR [SetColor Foreground Dull Cyan]
- putStrLn $ "--> " ++ B.unpack encoded
- setSGR [Reset]
+ logMsg LogClient msg
+ liftIO $ B.hPut h (addHeader $ encode msg)
- B.hPut h (addHeader encoded)
-
--- | Execute a block f that will throw a 'TimeoutException'
+-- | Execute a block f that will throw a 'Timeout' exception
-- after duration seconds. This will override the global timeout
-- for waiting for messages to arrive defined in 'SessionConfig'.
withTimeout :: Int -> Session a -> Session a
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
+