2 {-# LANGUAGE OverloadedStrings #-}
3 {-# LANGUAGE GeneralizedNewtypeDeriving #-}
4 {-# LANGUAGE FlexibleInstances #-}
5 {-# LANGUAGE MultiParamTypeClasses #-}
6 {-# LANGUAGE FlexibleContexts #-}
7 {-# LANGUAGE RankNTypes #-}
9 module Language.Haskell.LSP.Test.Session
16 , runSessionWithHandles
32 import Control.Applicative
33 import Control.Concurrent hiding (yield)
34 import Control.Exception
35 import Control.Lens hiding (List)
37 import Control.Monad.IO.Class
38 import Control.Monad.Except
39 #if __GLASGOW_HASKELL__ == 806
40 import Control.Monad.Fail
42 import Control.Monad.Trans.Reader (ReaderT, runReaderT)
43 import qualified Control.Monad.Trans.Reader as Reader (ask)
44 import Control.Monad.Trans.State (StateT, runStateT)
45 import qualified Control.Monad.Trans.State as State
46 import qualified Data.ByteString.Lazy.Char8 as B
48 import Data.Aeson.Encode.Pretty
49 import Data.Conduit as Conduit
50 import Data.Conduit.Parser as Parser
54 import qualified Data.Map as Map
55 import qualified Data.Text as T
56 import qualified Data.Text.IO as T
57 import qualified Data.HashMap.Strict as HashMap
60 import Language.Haskell.LSP.Messages
61 import Language.Haskell.LSP.Types.Capabilities
62 import Language.Haskell.LSP.Types
63 import Language.Haskell.LSP.Types.Lens hiding (error)
64 import Language.Haskell.LSP.VFS
65 import Language.Haskell.LSP.Test.Compat
66 import Language.Haskell.LSP.Test.Decoding
67 import Language.Haskell.LSP.Test.Exceptions
68 import System.Console.ANSI
69 import System.Directory
71 import System.Process (ProcessHandle())
74 -- | A session representing one instance of launching and connecting to a server.
76 -- You can send and receive messages to the server within 'Session' via
77 -- 'Language.Haskell.LSP.Test.message',
78 -- 'Language.Haskell.LSP.Test.sendRequest' and
79 -- 'Language.Haskell.LSP.Test.sendNotification'.
81 -- newtype Session a = Session (ParserStateReader FromServerMessage SessionState SessionContext IO a)
83 newtype Session a = Session (ConduitParser FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) a)
84 deriving (Functor, Applicative, Monad, MonadIO, Alternative)
86 #if __GLASGOW_HASKELL__ >= 806
87 instance MonadFail Session where
89 lastMsg <- fromJust . lastReceivedMessage <$> get
90 liftIO $ throw (UnexpectedMessage s lastMsg)
93 -- | Stuff you can configure for a 'Session'.
94 data SessionConfig = SessionConfig
95 { messageTimeout :: Int -- ^ Maximum time to wait for a message in seconds, defaults to 60.
96 , logStdErr :: Bool -- ^ Redirect the server's stderr to this stdout, defaults to False.
97 , logMessages :: Bool -- ^ Trace the messages sent and received to stdout, defaults to False.
98 , logColor :: Bool -- ^ Add ANSI color to the logged messages, defaults to True.
99 , lspConfig :: Maybe Value -- ^ The initial LSP config as JSON value, defaults to Nothing.
102 -- | The configuration used in 'Language.Haskell.LSP.Test.runSession'.
103 defaultConfig :: SessionConfig
104 defaultConfig = SessionConfig 60 False False True Nothing
106 instance Default SessionConfig where
109 data SessionMessage = ServerMessage FromServerMessage
113 data SessionContext = SessionContext
116 , rootDir :: FilePath
117 , messageChan :: Chan SessionMessage
118 , requestMap :: MVar RequestMap
119 , initRsp :: MVar InitializeResponse
120 , config :: SessionConfig
121 , sessionCapabilities :: ClientCapabilities
124 class Monad m => HasReader r m where
126 asks :: (r -> b) -> m b
129 instance HasReader SessionContext Session where
130 ask = Session (lift $ lift Reader.ask)
132 instance Monad m => HasReader r (ConduitM a b (StateT s (ReaderT r m))) where
133 ask = lift $ lift Reader.ask
135 data SessionState = SessionState
139 , curDiagnostics :: Map.Map NormalizedUri [Diagnostic]
140 , curTimeoutId :: Int
141 , overridingTimeout :: Bool
142 -- ^ The last received message from the server.
143 -- Used for providing exception information
144 , lastReceivedMessage :: Maybe FromServerMessage
147 class Monad m => HasState s m where
152 modify :: (s -> s) -> m ()
153 modify f = get >>= put . f
155 modifyM :: (HasState s m, Monad m) => (s -> m s) -> m ()
156 modifyM f = get >>= f >>= put
158 instance HasState SessionState Session where
159 get = Session (lift State.get)
160 put = Session . lift . State.put
162 instance Monad m => HasState s (ConduitM a b (StateT s m))
165 put = lift . State.put
167 instance Monad m => HasState s (ConduitParser a (StateT s m))
170 put = lift . State.put
172 runSession :: SessionContext -> SessionState -> Session a -> IO (a, SessionState)
173 runSession context state (Session session) = runReaderT (runStateT conduit state) context
175 conduit = runConduit $ chanSource .| watchdog .| updateStateC .| runConduitParser (catchError session handler)
177 handler (Unexpected "ConduitParser.empty") = do
178 lastMsg <- fromJust . lastReceivedMessage <$> get
179 name <- getParserName
180 liftIO $ throw (UnexpectedMessage (T.unpack name) lastMsg)
185 msg <- liftIO $ readChan (messageChan context)
189 watchdog :: ConduitM SessionMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) ()
190 watchdog = Conduit.awaitForever $ \msg -> do
191 curId <- curTimeoutId <$> get
193 ServerMessage sMsg -> yield sMsg
194 TimeoutMessage tId -> when (curId == tId) $ throw Timeout
196 -- | An internal version of 'runSession' that allows for a custom handler to listen to the server.
197 -- It also does not automatically send initialize and exit messages.
198 runSessionWithHandles :: Handle -- ^ Server in
199 -> Handle -- ^ Server out
200 -> ProcessHandle -- ^ Server process
201 -> (Handle -> SessionContext -> IO ()) -- ^ Server listener
203 -> ClientCapabilities
204 -> FilePath -- ^ Root directory
205 -> Session () -- ^ To exit the Server properly
208 runSessionWithHandles serverIn serverOut serverProc serverHandler config caps rootDir exitServer session = do
209 absRootDir <- canonicalizePath rootDir
211 hSetBuffering serverIn NoBuffering
212 hSetBuffering serverOut NoBuffering
213 -- This is required to make sure that we don’t get any
214 -- newline conversion or weird encoding issues.
215 hSetBinaryMode serverIn True
216 hSetBinaryMode serverOut True
218 reqMap <- newMVar newRequestMap
219 messageChan <- newChan
220 initRsp <- newEmptyMVar
222 mainThreadId <- myThreadId
224 let context = SessionContext serverIn absRootDir messageChan reqMap initRsp config caps
225 initState = SessionState (IdInt 0) mempty mempty 0 False Nothing
226 runSession' = runSession context initState
228 errorHandler = throwTo mainThreadId :: SessionException -> IO()
229 serverListenerLauncher =
230 forkIO $ catch (serverHandler serverOut context) errorHandler
231 server = (Just serverIn, Just serverOut, Nothing, serverProc)
232 serverAndListenerFinalizer tid =
233 finally (timeout (messageTimeout config * 1000000)
234 (runSession' exitServer))
235 (cleanupProcess server >> killThread tid)
237 (result, _) <- bracket serverListenerLauncher serverAndListenerFinalizer
238 (const $ runSession' session)
241 updateStateC :: ConduitM FromServerMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) ()
242 updateStateC = awaitForever $ \msg -> do
246 updateState :: (MonadIO m, HasReader SessionContext m, HasState SessionState m)
247 => FromServerMessage -> m ()
248 updateState (NotPublishDiagnostics n) = do
249 let List diags = n ^. params . diagnostics
250 doc = n ^. params . uri
252 let newDiags = Map.insert (toNormalizedUri doc) diags (curDiagnostics s)
253 in s { curDiagnostics = newDiags })
255 updateState (ReqApplyWorkspaceEdit r) = do
257 allChangeParams <- case r ^. params . edit . documentChanges of
259 mapM_ (checkIfNeedsOpened . (^. textDocument . uri)) cs
260 return $ map getParams cs
261 Nothing -> case r ^. params . edit . changes of
263 mapM_ checkIfNeedsOpened (HashMap.keys cs)
264 return $ concatMap (uncurry getChangeParams) (HashMap.toList cs)
265 Nothing -> error "No changes!"
268 newVFS <- liftIO $ changeFromServerVFS (vfs s) r
269 return $ s { vfs = newVFS }
271 let groupedParams = groupBy (\a b -> a ^. textDocument == b ^. textDocument) allChangeParams
272 mergedParams = map mergeParams groupedParams
274 -- TODO: Don't do this when replaying a session
275 forM_ mergedParams (sendMessage . NotificationMessage "2.0" TextDocumentDidChange)
277 -- Update VFS to new document versions
278 let sortedVersions = map (sortBy (compare `on` (^. textDocument . version))) groupedParams
279 latestVersions = map ((^. textDocument) . last) sortedVersions
280 bumpedVersions = map (version . _Just +~ 1) latestVersions
282 forM_ bumpedVersions $ \(VersionedTextDocumentIdentifier uri v) ->
285 update (VirtualFile oldV t mf) = VirtualFile (fromMaybe oldV v) t mf
286 newVFS = Map.adjust update (toNormalizedUri uri) oldVFS
287 in s { vfs = newVFS }
289 where checkIfNeedsOpened uri = do
290 oldVFS <- vfs <$> get
293 -- if its not open, open it
294 unless (toNormalizedUri uri `Map.member` oldVFS) $ do
295 let fp = fromJust $ uriToFilePath uri
296 contents <- liftIO $ T.readFile fp
297 let item = TextDocumentItem (filePathToUri fp) "" 0 contents
298 msg = NotificationMessage "2.0" TextDocumentDidOpen (DidOpenTextDocumentParams item)
299 liftIO $ B.hPut (serverIn ctx) $ addHeader (encode msg)
302 newVFS <- liftIO $ openVFS (vfs s) msg
303 return $ s { vfs = newVFS }
305 getParams (TextDocumentEdit docId (List edits)) =
306 let changeEvents = map (\e -> TextDocumentContentChangeEvent (Just (e ^. range)) Nothing (e ^. newText)) edits
307 in DidChangeTextDocumentParams docId (List changeEvents)
309 textDocumentVersions uri = map (VersionedTextDocumentIdentifier uri . Just) [0..]
311 textDocumentEdits uri edits = map (\(v, e) -> TextDocumentEdit v (List [e])) $ zip (textDocumentVersions uri) edits
313 getChangeParams uri (List edits) = map getParams (textDocumentEdits uri (reverse edits))
315 mergeParams :: [DidChangeTextDocumentParams] -> DidChangeTextDocumentParams
316 mergeParams params = let events = concat (toList (map (toList . (^. contentChanges)) params))
317 in DidChangeTextDocumentParams (head params ^. textDocument) (List events)
318 updateState _ = return ()
320 sendMessage :: (MonadIO m, HasReader SessionContext m, ToJSON a) => a -> m ()
322 h <- serverIn <$> ask
324 liftIO $ B.hPut h (addHeader $ encode msg)
326 -- | Execute a block f that will throw a 'Timeout' exception
327 -- after duration seconds. This will override the global timeout
328 -- for waiting for messages to arrive defined in 'SessionConfig'.
329 withTimeout :: Int -> Session a -> Session a
330 withTimeout duration f = do
331 chan <- asks messageChan
332 timeoutId <- curTimeoutId <$> get
333 modify $ \s -> s { overridingTimeout = True }
335 threadDelay (duration * 1000000)
336 writeChan chan (TimeoutMessage timeoutId)
338 modify $ \s -> s { curTimeoutId = timeoutId + 1,
339 overridingTimeout = False
343 data LogMsgType = LogServer | LogClient
346 -- | Logs the message if the config specified it
347 logMsg :: (ToJSON a, MonadIO m, HasReader SessionContext m)
348 => LogMsgType -> a -> m ()
350 shouldLog <- asks $ logMessages . config
351 shouldColor <- asks $ logColor . config
352 liftIO $ when shouldLog $ do
353 when shouldColor $ setSGR [SetColor Foreground Dull color]
354 putStrLn $ arrow ++ showPretty msg
355 when shouldColor $ setSGR [Reset]
358 | t == LogServer = "<-- "
361 | t == LogServer = Magenta
364 showPretty = B.unpack . encodePretty