{-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE OverloadedStrings #-} module Server (run) where -- entry point of the LSP server import Monad import Control.Monad.Reader import qualified Agda import Data.Aeson (FromJSON, ToJSON) import qualified Data.Aeson as JSON import Data.Text (pack) import GHC.Generics (Generic) import GHC.IO.IOMode (IOMode (ReadWriteMode)) import Language.LSP.Server import Language.LSP.Types hiding (TextDocumentSyncClientCapabilities (..)) import qualified Network.Simple.TCP as TCP import Network.Socket (socketToHandle) import qualified Switchboard import Switchboard (Switchboard) import Control.Concurrent import Agda.Misc (onHover) import Data.Maybe (isJust) -------------------------------------------------------------------------------- run :: Maybe Int -> IO Int run viaTCP = do env <- createInitEnv (isJust viaTCP) switchboard <- Switchboard.new env case viaTCP of Just port -> do void $ TCP.serve (TCP.Host "127.0.0.1") (show port) $ \(sock, _remoteAddr) -> do writeChan (envLogChan env) "[Server] connection established" handle <- socketToHandle sock ReadWriteMode _ <- runServerWithHandles handle handle (serverDefn env switchboard) return () Switchboard.destroy switchboard return 0 Nothing -> do runServer (serverDefn env switchboard) where serverDefn :: Env -> Switchboard -> ServerDefinition () serverDefn env switchboard = ServerDefinition { onConfigurationChange = const $ pure $ Right (), doInitialize = \ctxEnv _req -> do Switchboard.setupLanguageContextEnv switchboard ctxEnv pure $ Right ctxEnv, staticHandlers = handlers, interpretHandler = \ctxEnv -> Iso (runLspT ctxEnv . runServerM env) liftIO, options = lspOptions } lspOptions :: Options lspOptions = defaultOptions { textDocumentSync = Just syncOptions } -- these `TextDocumentSyncOptions` are essential for receiving notifications from the client syncOptions :: TextDocumentSyncOptions syncOptions = TextDocumentSyncOptions { _openClose = Just True, -- receive open and close notifications from the client _change = Just changeOptions, -- receive change notifications from the client _willSave = Just False, -- receive willSave notifications from the client _willSaveWaitUntil = Just False, -- receive willSave notifications from the client _save = Just $ InR saveOptions } changeOptions :: TextDocumentSyncKind changeOptions = TdSyncIncremental -- includes the document content on save, so that we don't have to read it from the disk saveOptions :: SaveOptions saveOptions = SaveOptions (Just True) -- handlers of the LSP server handlers :: Handlers (ServerM (LspM ())) handlers = mconcat [ -- custom methods, not part of LSP requestHandler (SCustomMethod "agda") $ \req responder -> do let RequestMessage _ _i _ params = req -- JSON Value => Request => Response response <- case JSON.fromJSON params of JSON.Error msg -> return $ CmdRes $ Just $ CmdErrCannotDecodeJSON $ show msg ++ "\n" ++ show params JSON.Success request -> handleCommandReq request -- respond with the Response responder $ Right $ JSON.toJSON response, -- hover provider requestHandler STextDocumentHover $ \req responder -> do let RequestMessage _ _ _ (HoverParams (TextDocumentIdentifier uri) pos _workDone) = req result <- onHover uri pos responder $ Right result ] -------------------------------------------------------------------------------- handleCommandReq :: CommandReq -> ServerM (LspM ()) CommandRes handleCommandReq CmdReqSYN = return $ CmdResACK Agda.getAgdaVersion handleCommandReq (CmdReq cmd) = do case Agda.parseIOTCM cmd of Left err -> do writeLog $ "[Error] CmdErrCannotParseCommand:\n" <> pack err return $ CmdRes (Just (CmdErrCannotParseCommand err)) Right iotcm -> do writeLog $ "[Request] " <> pack (show cmd) provideCommand iotcm return $ CmdRes Nothing -------------------------------------------------------------------------------- data CommandReq = CmdReqSYN -- ^ Client initiates a 2-way handshake | CmdReq String deriving (Generic) instance FromJSON CommandReq data CommandRes = CmdResACK -- ^ Server complets the 2-way handshake String -- ^ Version number of Agda | CmdRes -- ^ The response for 'CmdReq' (Maybe CommandErr) -- ^ 'Nothing' to indicate success deriving (Generic) instance ToJSON CommandRes data CommandErr = CmdErrCannotDecodeJSON String | CmdErrCannotParseCommand String deriving (Generic) instance ToJSON CommandErr