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 (ConduitParser FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) a)
82 deriving (Functor, Applicative, Monad, MonadIO, Alternative)
84 #if __GLASGOW_HASKELL__ >= 806
85 instance MonadFail Session where
87 lastMsg <- fromJust . lastReceivedMessage <$> get
88 liftIO $ throw (UnexpectedMessage s lastMsg)
91 -- | Stuff you can configure for a 'Session'.
92 data SessionConfig = SessionConfig
93 { messageTimeout :: Int -- ^ Maximum time to wait for a message in seconds, defaults to 60.
94 , logStdErr :: Bool -- ^ Redirect the server's stderr to this stdout, defaults to False.
95 , logMessages :: Bool -- ^ Trace the messages sent and received to stdout, defaults to False.
96 , logColor :: Bool -- ^ Add ANSI color to the logged messages, defaults to True.
97 , lspConfig :: Maybe Value -- ^ The initial LSP config as JSON value, defaults to Nothing.
98 -- ^ Whether or not to ignore 'ShowMessageNotification' and 'LogMessageNotification', defaults to False.
100 , ignoreLogNotifications :: Bool
103 -- | The configuration used in 'Language.Haskell.LSP.Test.runSession'.
104 defaultConfig :: SessionConfig
105 defaultConfig = SessionConfig 60 False False True Nothing False
107 instance Default SessionConfig where
110 data SessionMessage = ServerMessage FromServerMessage
114 data SessionContext = SessionContext
117 , rootDir :: FilePath
118 , messageChan :: Chan SessionMessage
119 , requestMap :: MVar RequestMap
120 , initRsp :: MVar InitializeResponse
121 , config :: SessionConfig
122 , sessionCapabilities :: ClientCapabilities
125 class Monad m => HasReader r m where
127 asks :: (r -> b) -> m b
130 instance HasReader SessionContext Session where
131 ask = Session (lift $ lift Reader.ask)
133 instance Monad m => HasReader r (ConduitM a b (StateT s (ReaderT r m))) where
134 ask = lift $ lift Reader.ask
136 data SessionState = SessionState
140 , curDiagnostics :: Map.Map NormalizedUri [Diagnostic]
141 , curTimeoutId :: Int
142 , overridingTimeout :: Bool
143 -- ^ The last received message from the server.
144 -- Used for providing exception information
145 , lastReceivedMessage :: Maybe FromServerMessage
148 class Monad m => HasState s m where
153 modify :: (s -> s) -> m ()
154 modify f = get >>= put . f
156 modifyM :: (HasState s m, Monad m) => (s -> m s) -> m ()
157 modifyM f = get >>= f >>= put
159 instance HasState SessionState Session where
160 get = Session (lift State.get)
161 put = Session . lift . State.put
163 instance Monad m => HasState s (ConduitM a b (StateT s m))
166 put = lift . State.put
168 instance Monad m => HasState s (ConduitParser a (StateT s m))
171 put = lift . State.put
173 runSession :: SessionContext -> SessionState -> Session a -> IO (a, SessionState)
174 runSession context state (Session session) = runReaderT (runStateT conduit state) context
176 conduit = runConduit $ chanSource .| watchdog .| updateStateC .| runConduitParser (catchError session handler)
178 handler (Unexpected "ConduitParser.empty") = do
179 lastMsg <- fromJust . lastReceivedMessage <$> get
180 name <- getParserName
181 liftIO $ throw (UnexpectedMessage (T.unpack name) lastMsg)
186 msg <- liftIO $ readChan (messageChan context)
187 unless (ignoreLogNotifications (config context) && isLogNotification msg) $
191 isLogNotification (ServerMessage (NotShowMessage _)) = True
192 isLogNotification (ServerMessage (NotLogMessage _)) = True
193 isLogNotification _ = False
195 watchdog :: ConduitM SessionMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) ()
196 watchdog = Conduit.awaitForever $ \msg -> do
197 curId <- curTimeoutId <$> get
199 ServerMessage sMsg -> yield sMsg
200 TimeoutMessage tId -> when (curId == tId) $ lastReceivedMessage <$> get >>= throw . Timeout
202 -- | An internal version of 'runSession' that allows for a custom handler to listen to the server.
203 -- It also does not automatically send initialize and exit messages.
204 runSessionWithHandles :: Handle -- ^ Server in
205 -> Handle -- ^ Server out
206 -> ProcessHandle -- ^ Server process
207 -> (Handle -> SessionContext -> IO ()) -- ^ Server listener
209 -> ClientCapabilities
210 -> FilePath -- ^ Root directory
211 -> Session () -- ^ To exit the Server properly
214 runSessionWithHandles serverIn serverOut serverProc serverHandler config caps rootDir exitServer session = do
215 absRootDir <- canonicalizePath rootDir
217 hSetBuffering serverIn NoBuffering
218 hSetBuffering serverOut NoBuffering
219 -- This is required to make sure that we don’t get any
220 -- newline conversion or weird encoding issues.
221 hSetBinaryMode serverIn True
222 hSetBinaryMode serverOut True
224 reqMap <- newMVar newRequestMap
225 messageChan <- newChan
226 initRsp <- newEmptyMVar
228 mainThreadId <- myThreadId
230 let context = SessionContext serverIn absRootDir messageChan reqMap initRsp config caps
231 initState vfs = SessionState (IdInt 0) vfs
232 mempty 0 False Nothing
233 runSession' ses = initVFS $ \vfs -> runSession context (initState vfs) ses
235 errorHandler = throwTo mainThreadId :: SessionException -> IO()
236 serverListenerLauncher =
237 forkIO $ catch (serverHandler serverOut context) errorHandler
238 server = (Just serverIn, Just serverOut, Nothing, serverProc)
239 serverAndListenerFinalizer tid =
240 finally (timeout (messageTimeout config * 1000000)
241 (runSession' exitServer))
242 (cleanupProcess server >> killThread tid)
244 (result, _) <- bracket serverListenerLauncher serverAndListenerFinalizer
245 (const $ runSession' session)
248 updateStateC :: ConduitM FromServerMessage FromServerMessage (StateT SessionState (ReaderT SessionContext IO)) ()
249 updateStateC = awaitForever $ \msg -> do
253 updateState :: (MonadIO m, HasReader SessionContext m, HasState SessionState m)
254 => FromServerMessage -> m ()
255 updateState (NotPublishDiagnostics n) = do
256 let List diags = n ^. params . diagnostics
257 doc = n ^. params . uri
259 let newDiags = Map.insert (toNormalizedUri doc) diags (curDiagnostics s)
260 in s { curDiagnostics = newDiags })
262 updateState (ReqApplyWorkspaceEdit r) = do
264 allChangeParams <- case r ^. params . edit . documentChanges of
266 mapM_ (checkIfNeedsOpened . (^. textDocument . uri)) cs
267 return $ map getParams cs
268 Nothing -> case r ^. params . edit . changes of
270 mapM_ checkIfNeedsOpened (HashMap.keys cs)
271 return $ concatMap (uncurry getChangeParams) (HashMap.toList cs)
272 Nothing -> error "No changes!"
275 newVFS <- liftIO $ changeFromServerVFS (vfs s) r
276 return $ s { vfs = newVFS }
278 let groupedParams = groupBy (\a b -> a ^. textDocument == b ^. textDocument) allChangeParams
279 mergedParams = map mergeParams groupedParams
281 -- TODO: Don't do this when replaying a session
282 forM_ mergedParams (sendMessage . NotificationMessage "2.0" TextDocumentDidChange)
284 -- Update VFS to new document versions
285 let sortedVersions = map (sortBy (compare `on` (^. textDocument . version))) groupedParams
286 latestVersions = map ((^. textDocument) . last) sortedVersions
287 bumpedVersions = map (version . _Just +~ 1) latestVersions
289 forM_ bumpedVersions $ \(VersionedTextDocumentIdentifier uri v) ->
292 update (VirtualFile oldV file_ver t) = VirtualFile (fromMaybe oldV v) (file_ver + 1) t
293 newVFS = updateVFS (Map.adjust update (toNormalizedUri uri)) oldVFS
294 in s { vfs = newVFS }
296 where checkIfNeedsOpened uri = do
297 oldVFS <- vfs <$> get
300 -- if its not open, open it
301 unless (toNormalizedUri uri `Map.member` vfsMap oldVFS) $ do
302 let fp = fromJust $ uriToFilePath uri
303 contents <- liftIO $ T.readFile fp
304 let item = TextDocumentItem (filePathToUri fp) "" 0 contents
305 msg = NotificationMessage "2.0" TextDocumentDidOpen (DidOpenTextDocumentParams item)
306 liftIO $ B.hPut (serverIn ctx) $ addHeader (encode msg)
309 let (newVFS,_) = openVFS (vfs s) msg
310 return $ s { vfs = newVFS }
312 getParams (TextDocumentEdit docId (List edits)) =
313 let changeEvents = map (\e -> TextDocumentContentChangeEvent (Just (e ^. range)) Nothing (e ^. newText)) edits
314 in DidChangeTextDocumentParams docId (List changeEvents)
316 textDocumentVersions uri = map (VersionedTextDocumentIdentifier uri . Just) [0..]
318 textDocumentEdits uri edits = map (\(v, e) -> TextDocumentEdit v (List [e])) $ zip (textDocumentVersions uri) edits
320 getChangeParams uri (List edits) = map getParams (textDocumentEdits uri (reverse edits))
322 mergeParams :: [DidChangeTextDocumentParams] -> DidChangeTextDocumentParams
323 mergeParams params = let events = concat (toList (map (toList . (^. contentChanges)) params))
324 in DidChangeTextDocumentParams (head params ^. textDocument) (List events)
325 updateState _ = return ()
327 sendMessage :: (MonadIO m, HasReader SessionContext m, ToJSON a) => a -> m ()
329 h <- serverIn <$> ask
331 liftIO $ B.hPut h (addHeader $ encode msg)
333 -- | Execute a block f that will throw a 'Timeout' exception
334 -- after duration seconds. This will override the global timeout
335 -- for waiting for messages to arrive defined in 'SessionConfig'.
336 withTimeout :: Int -> Session a -> Session a
337 withTimeout duration f = do
338 chan <- asks messageChan
339 timeoutId <- curTimeoutId <$> get
340 modify $ \s -> s { overridingTimeout = True }
342 threadDelay (duration * 1000000)
343 writeChan chan (TimeoutMessage timeoutId)
345 modify $ \s -> s { curTimeoutId = timeoutId + 1,
346 overridingTimeout = False
350 data LogMsgType = LogServer | LogClient
353 -- | Logs the message if the config specified it
354 logMsg :: (ToJSON a, MonadIO m, HasReader SessionContext m)
355 => LogMsgType -> a -> m ()
357 shouldLog <- asks $ logMessages . config
358 shouldColor <- asks $ logColor . config
359 liftIO $ when shouldLog $ do
360 when shouldColor $ setSGR [SetColor Foreground Dull color]
361 putStrLn $ arrow ++ showPretty msg
362 when shouldColor $ setSGR [Reset]
365 | t == LogServer = "<-- "
368 | t == LogServer = Magenta
371 showPretty = B.unpack . encodePretty