lsp-2.3.0.0: src/Language/LSP/Server/Control.hs
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
-- So we can keep using the old prettyprinter modules (which have a better
-- compatibility range) for now.
{-# OPTIONS_GHC -Wno-deprecations #-}
module Language.LSP.Server.Control (
-- * Running
runServer,
runServerWith,
runServerWithHandles,
LspServerLog (..),
) where
import Colog.Core (LogAction (..), Severity (..), WithSeverity (..), (<&))
import Colog.Core qualified as L
import Control.Applicative ((<|>))
import Control.Concurrent
import Control.Concurrent.STM.TChan
import Control.Monad
import Control.Monad.IO.Class
import Control.Monad.STM
import Data.Aeson qualified as J
import Data.Attoparsec.ByteString qualified as Attoparsec
import Data.Attoparsec.ByteString.Char8
import Data.ByteString qualified as BS
import Data.ByteString.Builder.Extra (defaultChunkSize)
import Data.ByteString.Lazy qualified as BSL
import Data.List
import Data.Text qualified as T
import Data.Text.Lazy qualified as TL
import Data.Text.Lazy.Encoding qualified as TL
import Data.Text.Prettyprint.Doc
import Language.LSP.Logging (defaultClientLogger)
import Language.LSP.Protocol.Message
import Language.LSP.Server.Core
import Language.LSP.Server.Processing qualified as Processing
import Language.LSP.VFS
import System.IO
data LspServerLog
= LspProcessingLog Processing.LspProcessingLog
| DecodeInitializeError String
| HeaderParseFail [String] String
| EOF
| Starting
| ParsedMsg T.Text
| SendMsg TL.Text
deriving (Show)
instance Pretty LspServerLog where
pretty (LspProcessingLog l) = pretty l
pretty (DecodeInitializeError err) =
vsep
[ "Got error while decoding initialize:"
, pretty err
]
pretty (HeaderParseFail ctxs err) =
vsep
[ "Failed to parse message header:"
, pretty (intercalate " > " ctxs) <> ": " <+> pretty err
]
pretty EOF = "Got EOF"
pretty Starting = "Starting server"
pretty (ParsedMsg msg) = "---> " <> pretty msg
pretty (SendMsg msg) = "<--2-- " <> pretty msg
-- ---------------------------------------------------------------------
{- | Convenience function for 'runServerWithHandles' which:
(1) reads from stdin;
(2) writes to stdout; and
(3) logs to stderr and to the client, with some basic filtering.
-}
runServer :: forall config. ServerDefinition config -> IO Int
runServer =
runServerWithHandles
ioLogger
lspLogger
stdin
stdout
where
prettyMsg l = "[" <> viaShow (L.getSeverity l) <> "] " <> pretty (L.getMsg l)
ioLogger :: LogAction IO (WithSeverity LspServerLog)
ioLogger = L.cmap (show . prettyMsg) L.logStringStderr
lspLogger :: LogAction (LspM config) (WithSeverity LspServerLog)
lspLogger =
let clientLogger = L.cmap (fmap (T.pack . show . pretty)) defaultClientLogger
in clientLogger <> L.hoistLogAction liftIO ioLogger
{- | Starts a language server over the specified handles.
This function will return once the @exit@ notification is received.
-}
runServerWithHandles ::
-- | The logger to use outside the main body of the server where we can't assume the ability to send messages.
LogAction IO (WithSeverity LspServerLog) ->
-- | The logger to use once the server has started and can successfully send messages.
LogAction (LspM config) (WithSeverity LspServerLog) ->
-- | Handle to read client input from.
Handle ->
-- | Handle to write output to.
Handle ->
ServerDefinition config ->
IO Int -- exit code
runServerWithHandles ioLogger logger hin hout serverDefinition = do
hSetBuffering hin NoBuffering
hSetEncoding hin utf8
hSetBuffering hout NoBuffering
hSetEncoding hout utf8
let
clientIn = BS.hGetSome hin defaultChunkSize
clientOut out = do
BSL.hPut hout out
hFlush hout
runServerWith ioLogger logger clientIn clientOut serverDefinition
{- | Starts listening and sending requests and responses
using the specified I/O.
-}
runServerWith ::
-- | The logger to use outside the main body of the server where we can't assume the ability to send messages.
LogAction IO (WithSeverity LspServerLog) ->
-- | The logger to use once the server has started and can successfully send messages.
LogAction (LspM config) (WithSeverity LspServerLog) ->
-- | Client input.
IO BS.ByteString ->
-- | Function to provide output to.
(BSL.ByteString -> IO ()) ->
ServerDefinition config ->
IO Int -- exit code
runServerWith ioLogger logger clientIn clientOut serverDefinition = do
ioLogger <& Starting `WithSeverity` Info
cout <- atomically newTChan :: IO (TChan J.Value)
_rhpid <- forkIO $ sendServer ioLogger cout clientOut
let sendMsg msg = atomically $ writeTChan cout $ J.toJSON msg
ioLoop ioLogger logger clientIn serverDefinition emptyVFS sendMsg
return 1
-- ---------------------------------------------------------------------
ioLoop ::
forall config.
LogAction IO (WithSeverity LspServerLog) ->
LogAction (LspM config) (WithSeverity LspServerLog) ->
IO BS.ByteString ->
ServerDefinition config ->
VFS ->
(FromServerMessage -> IO ()) ->
IO ()
ioLoop ioLogger logger clientIn serverDefinition vfs sendMsg = do
minitialize <- parseOne ioLogger clientIn (parse parser "")
case minitialize of
Nothing -> pure ()
Just (msg, remainder) -> do
case J.eitherDecode $ BSL.fromStrict msg of
Left err -> ioLogger <& DecodeInitializeError err `WithSeverity` Error
Right initialize -> do
mInitResp <- Processing.initializeRequestHandler pioLogger serverDefinition vfs sendMsg initialize
case mInitResp of
Nothing -> pure ()
Just env -> runLspT env $ loop (parse parser remainder)
where
pioLogger = L.cmap (fmap LspProcessingLog) ioLogger
pLogger = L.cmap (fmap LspProcessingLog) logger
loop :: Result BS.ByteString -> LspM config ()
loop = go
where
go r = do
res <- parseOne logger clientIn r
case res of
Nothing -> pure ()
Just (msg, remainder) -> do
Processing.processMessage pLogger $ BSL.fromStrict msg
go (parse parser remainder)
parser = do
try contentType <|> (return ())
len <- contentLength
try contentType <|> (return ())
_ <- string _ONE_CRLF
Attoparsec.take len
contentLength = do
_ <- string "Content-Length: "
len <- decimal
_ <- string _ONE_CRLF
return len
contentType = do
_ <- string "Content-Type: "
skipWhile (/= '\r')
_ <- string _ONE_CRLF
return ()
parseOne ::
MonadIO m =>
LogAction m (WithSeverity LspServerLog) ->
IO BS.ByteString ->
Result BS.ByteString ->
m (Maybe (BS.ByteString, BS.ByteString))
parseOne logger clientIn = go
where
go (Fail _ ctxs err) = do
logger <& HeaderParseFail ctxs err `WithSeverity` Error
pure Nothing
go (Partial c) = do
bs <- liftIO clientIn
if BS.null bs
then do
logger <& EOF `WithSeverity` Error
pure Nothing
else go (c bs)
go (Done remainder msg) = do
-- TODO: figure out how to re-enable
-- This can lead to infinite recursion in logging, see https://github.com/haskell/lsp/issues/447
-- logger <& ParsedMsg (T.decodeUtf8 msg) `WithSeverity` Debug
pure $ Just (msg, remainder)
-- ---------------------------------------------------------------------
-- | Simple server to make sure all output is serialised
sendServer :: LogAction IO (WithSeverity LspServerLog) -> TChan J.Value -> (BSL.ByteString -> IO ()) -> IO ()
sendServer _logger msgChan clientOut = do
forever $ do
msg <- atomically $ readTChan msgChan
-- We need to make sure we only send over the content of the message,
-- and no other tags/wrapper stuff
let str = J.encode msg
let out =
BSL.concat
[ TL.encodeUtf8 $ TL.pack $ "Content-Length: " ++ show (BSL.length str)
, BSL.fromStrict _TWO_CRLF
, str
]
clientOut out
-- TODO: figure out how to re-enable
-- This can lead to infinite recursion in logging, see https://github.com/haskell/lsp/issues/447
-- logger <& SendMsg (TL.decodeUtf8 str) `WithSeverity` Debug
_ONE_CRLF :: BS.ByteString
_ONE_CRLF = "\r\n"
_TWO_CRLF :: BS.ByteString
_TWO_CRLF = "\r\n\r\n"