1 {-# LANGUAGE OverloadedStrings #-}
2 {-# LANGUAGE FlexibleInstances #-}
3 {-# LANGUAGE MultiParamTypeClasses #-}
4 {-# LANGUAGE FlexibleContexts, UndecidableInstances #-}
6 module Language.Haskell.LSP.Test.Session
12 , runSessionWithHandles
27 import Control.Concurrent hiding (yield)
28 import Control.Exception
29 import Control.Lens hiding (List)
31 import Control.Monad.IO.Class
32 import Control.Monad.Except
33 import Control.Monad.Trans.Reader (ReaderT, runReaderT)
34 import qualified Control.Monad.Trans.Reader as Reader (ask)
35 import Control.Monad.Trans.State (StateT, runStateT)
36 import qualified Control.Monad.Trans.State as State (get, put)
37 import qualified Data.ByteString.Lazy.Char8 as B
39 import Data.Aeson.Encode.Pretty
40 import Data.Conduit.Parser as Parser
44 import qualified Data.Map as Map
45 import qualified Data.Text as T
46 import qualified Data.Text.IO as T
47 import qualified Data.HashMap.Strict as HashMap
50 import Language.Haskell.LSP.Messages
51 import Language.Haskell.LSP.Types.Capabilities
52 import Language.Haskell.LSP.Types hiding (error)
53 import Language.Haskell.LSP.VFS
54 import Language.Haskell.LSP.Test.Decoding
55 import Language.Haskell.LSP.Test.Exceptions
56 import System.Console.ANSI
57 import System.Directory
60 -- | A session representing one instance of launching and connecting to a server.
62 -- You can send and receive messages to the server within 'Session' via 'getMessage',
63 -- 'sendRequest' and 'sendNotification'.
66 -- runSession \"path\/to\/root\/dir\" $ do
67 -- docItem <- getDocItem "Desktop/simple.hs" "haskell"
68 -- sendNotification TextDocumentDidOpen (DidOpenTextDocumentParams docItem)
69 -- diagnostics <- getMessage :: Session PublishDiagnosticsNotification
71 type SessionT m = ParserStateReader FromServerMessage SessionState SessionContext m
73 -- | Stuff you can configure for a 'Session'.
74 data SessionConfig = SessionConfig
76 messageTimeout :: Int -- ^ Maximum time to wait for a message in seconds. Defaults to 60.
77 , logStdErr :: Bool -- ^ When True redirects the servers stderr output to haskell-lsp-test's stdout. Defaults to False.
78 , logMessages :: Bool -- ^ When True traces the communication between client and server to stdout. Defaults to True.
81 instance Default SessionConfig where
82 def = SessionConfig 60 False True
84 data SessionMessage = ServerMessage FromServerMessage
88 data SessionContext = SessionContext
92 , messageChan :: Chan SessionMessage
93 , requestMap :: MVar RequestMap
94 , initRsp :: MVar InitializeResponse
95 , config :: SessionConfig
96 , sessionCapabilities :: ClientCapabilities
99 class Monad m => HasReader r m where
101 asks :: (r -> b) -> m b
104 instance Monad m => HasReader r (ParserStateReader a s r m) where
105 ask = lift $ lift Reader.ask
107 instance Monad m => HasReader SessionContext (ConduitM a b (StateT s (ReaderT SessionContext m))) where
108 ask = lift $ lift Reader.ask
110 data SessionState = SessionState
114 , curDiagnostics :: Map.Map Uri [Diagnostic]
115 , curTimeoutId :: Int
116 , overridingTimeout :: Bool
117 -- ^ The last received message from the server.
118 -- Used for providing exception information
119 , lastReceivedMessage :: Maybe FromServerMessage
122 class Monad m => HasState s m where
127 modify :: (s -> s) -> m ()
128 modify f = get >>= put . f
130 modifyM :: (HasState s m, Monad m) => (s -> m s) -> m ()
131 modifyM f = get >>= f >>= put
133 instance Monad m => HasState s (ParserStateReader a s r m) where
135 put = lift . State.put
137 instance Monad m => HasState SessionState (ConduitM a b (StateT SessionState m))
140 put = lift . State.put
142 type ParserStateReader a s r m = ConduitParser a (StateT s (ReaderT r m))
144 runSession :: (MonadIO m, MonadThrow m) => SessionContext -> SessionState -> SessionT m a -> m (a, SessionState)
145 runSession context state session = runReaderT (runStateT conduit state) context
147 conduit = runConduit $ chanSource .| watchdog .| updateStateC .| runConduitParser (catchError session handler)
149 handler :: MonadIO m => ConduitParserException -> SessionT m a
150 handler (Unexpected "ConduitParser.empty") = do
151 lastMsg <- fromJust . lastReceivedMessage <$> get
152 name <- getParserName
153 liftIO $ throw (UnexpectedMessage (T.unpack name) lastMsg)
155 handler e = liftIO $ throw e
157 chanSource :: MonadIO m => ConduitT () SessionMessage m ()
159 msg <- liftIO $ readChan (messageChan context)
164 watchdog :: MonadIO m => ConduitM SessionMessage FromServerMessage (StateT SessionState (ReaderT SessionContext m)) ()
165 watchdog = Conduit.awaitForever $ \msg -> do
166 curId <- curTimeoutId <$> get
168 ServerMessage sMsg -> yield sMsg
169 TimeoutMessage tId -> when (curId == tId) $ throw Timeout
171 -- | An internal version of 'runSession' that allows for a custom handler to listen to the server.
172 -- It also does not automatically send initialize and exit messages.
173 runSessionWithHandles :: (MonadIO m, MonadThrow m)
174 => Handle -- ^ Server in
175 -> Handle -- ^ Server out
176 -> (Handle -> SessionContext -> IO ()) -- ^ Server listener
178 -> ClientCapabilities
179 -> FilePath -- ^ Root directory
182 runSessionWithHandles serverIn serverOut serverHandler config caps rootDir session = do
183 absRootDir <- liftIO $ canonicalizePath rootDir
186 hSetBuffering serverIn NoBuffering
187 hSetBuffering serverOut NoBuffering
189 reqMap <- liftIO $ newMVar newRequestMap
190 messageChan <- liftIO newChan
191 initRsp <- liftIO newEmptyMVar
193 let context = SessionContext serverIn absRootDir messageChan reqMap initRsp config caps
194 initState = SessionState (IdInt 0) mempty mempty 0 False Nothing
196 threadId <- liftIO $ forkIO $ void $ serverHandler serverOut context
197 (result, _) <- runSession context initState session
199 liftIO $ killThread threadId
203 updateStateC :: MonadIO m => ConduitM FromServerMessage FromServerMessage (StateT SessionState (ReaderT SessionContext m)) ()
204 updateStateC = awaitForever $ \msg -> do
208 updateState :: (MonadIO m, HasReader SessionContext m, HasState SessionState m) => FromServerMessage -> m ()
209 updateState (NotPublishDiagnostics n) = do
210 let List diags = n ^. params . diagnostics
211 doc = n ^. params . uri
213 let newDiags = Map.insert doc diags (curDiagnostics s)
214 in s { curDiagnostics = newDiags })
216 updateState (ReqApplyWorkspaceEdit r) = do
218 allChangeParams <- case r ^. params . edit . documentChanges of
220 mapM_ (checkIfNeedsOpened . (^. textDocument . uri)) cs
221 return $ map getParams cs
222 Nothing -> case r ^. params . edit . changes of
224 mapM_ checkIfNeedsOpened (HashMap.keys cs)
225 return $ concatMap (uncurry getChangeParams) (HashMap.toList cs)
226 Nothing -> error "No changes!"
229 newVFS <- liftIO $ changeFromServerVFS (vfs s) r
230 return $ s { vfs = newVFS }
232 let groupedParams = groupBy (\a b -> (a ^. textDocument == b ^. textDocument)) allChangeParams
233 mergedParams = map mergeParams groupedParams
235 -- TODO: Don't do this when replaying a session
236 forM_ mergedParams (sendMessage . NotificationMessage "2.0" TextDocumentDidChange)
238 -- Update VFS to new document versions
239 let sortedVersions = map (sortBy (compare `on` (^. textDocument . version))) groupedParams
240 latestVersions = map ((^. textDocument) . last) sortedVersions
241 bumpedVersions = map (version . _Just +~ 1) latestVersions
243 forM_ bumpedVersions $ \(VersionedTextDocumentIdentifier uri v) ->
246 update (VirtualFile oldV t) = VirtualFile (fromMaybe oldV v) t
247 newVFS = Map.adjust update uri oldVFS
248 in s { vfs = newVFS }
250 where checkIfNeedsOpened uri = do
251 oldVFS <- vfs <$> get
254 -- if its not open, open it
255 unless (uri `Map.member` oldVFS) $ do
256 let fp = fromJust $ uriToFilePath uri
257 contents <- liftIO $ T.readFile fp
258 let item = TextDocumentItem (filePathToUri fp) "" 0 contents
259 msg = NotificationMessage "2.0" TextDocumentDidOpen (DidOpenTextDocumentParams item)
260 liftIO $ B.hPut (serverIn ctx) $ addHeader (encode msg)
263 newVFS <- liftIO $ openVFS (vfs s) msg
264 return $ s { vfs = newVFS }
266 getParams (TextDocumentEdit docId (List edits)) =
267 let changeEvents = map (\e -> TextDocumentContentChangeEvent (Just (e ^. range)) Nothing (e ^. newText)) edits
268 in DidChangeTextDocumentParams docId (List changeEvents)
270 textDocumentVersions uri = map (VersionedTextDocumentIdentifier uri . Just) [0..]
272 textDocumentEdits uri edits = map (\(v, e) -> TextDocumentEdit v (List [e])) $ zip (textDocumentVersions uri) edits
274 getChangeParams uri (List edits) = map getParams (textDocumentEdits uri (reverse edits))
276 mergeParams :: [DidChangeTextDocumentParams] -> DidChangeTextDocumentParams
277 mergeParams params = let events = concat (toList (map (toList . (^. contentChanges)) params))
278 in DidChangeTextDocumentParams (head params ^. textDocument) (List events)
279 updateState _ = return ()
281 sendMessage :: (MonadIO m, HasReader SessionContext m, ToJSON a) => a -> m ()
283 h <- serverIn <$> ask
284 let encoded = encodePretty msg
286 shouldLog <- asks $ logMessages . config
287 liftIO $ when shouldLog $ do
289 setSGR [SetColor Foreground Dull Cyan]
290 putStrLn $ "--> " ++ B.unpack encoded
293 B.hPut h (addHeader encoded)
295 -- | Execute a block f that will throw a 'TimeoutException'
296 -- after duration seconds. This will override the global timeout
297 -- for waiting for messages to arrive defined in 'SessionConfig'.
298 withTimeout :: MonadIO m => Int -> SessionT m a -> SessionT m a
299 withTimeout duration f = do
300 chan <- asks messageChan
301 timeoutId <- curTimeoutId <$> get
302 modify $ \s -> s { overridingTimeout = True }
304 threadDelay (duration * 1000000)
305 writeChan chan (TimeoutMessage timeoutId)
307 modify $ \s -> s { curTimeoutId = timeoutId + 1,
308 overridingTimeout = False