packages feed

inferno-lsp-0.1.0: src/Inferno/LSP/Server.hs

{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE ExplicitNamespaces #-}
{-# LANGUAGE GADTs #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE RankNTypes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
{-# LANGUAGE TypeInType #-}

module Inferno.LSP.Server where

import Colog.Core.Action (LogAction (..))
import Control.Concurrent (forkIO)
import Control.Concurrent.STM.TChan (TChan, newTChan, readTChan, writeTChan)
import Control.Concurrent.STM.TVar (TVar, modifyTVar, newTVar, readTVar)
import qualified Control.Exception as E
import Control.Monad (forever)
import Control.Monad.Except (MonadError)
import Control.Monad.IO.Class (MonadIO (..))
import Control.Monad.STM (atomically)
import Control.Monad.Trans.Reader (ReaderT (..), ask)
-- import qualified Data.Aeson as J
-- import           Data.Int (Int32)
import qualified Data.ByteString as BS
import Data.ByteString.Builder.Extra (defaultChunkSize)
import qualified Data.ByteString.Lazy as BSL
import Data.Map (Map)
import qualified Data.Map as Map
import Data.Maybe (catMaybes, fromMaybe)
import qualified Data.Text as T
import qualified Data.Text.Utf16.Rope as Rope
import Data.Time.Clock (UTCTime, getCurrentTime)
import qualified Data.UUID.V4 as UUID.V4
import Inferno.Eval.Error (EvalError)
import Inferno.LSP.Completion (completionQueryAt, filterModuleNameCompletionItems, findInPrelude, identifierCompletionItems, mkCompletionItem, rwsCompletionItems)
import Inferno.LSP.ParseInfer (parseAndInfer)
import Inferno.Module.Prelude (ModuleMap, preludeNameToTypeMap)
import Inferno.Types.Syntax (Expr, Ident (..), InfernoType)
import Inferno.Types.Type (TCScheme)
import Inferno.Types.VersionControl (Pinned)
import Inferno.VersionControl.Types (VCObjectHash)
import Keys.UUID (UUID (..))
import Language.LSP.Diagnostics (partitionBySource)
import Language.LSP.Server
  ( Handler,
    Handlers (..),
    LspT (..),
    Options (..),
    ServerDefinition (..),
    defaultOptions,
    getLspEnv,
    getVirtualFile,
    mapHandlers,
    notificationHandler,
    publishDiagnostics,
    requestHandler,
    runLspT,
    runServerWith,
    type (<~>) (Iso),
  )
import qualified Language.LSP.Types as J
import qualified Language.LSP.Types.Lens as J
import Language.LSP.VFS (VirtualFile (..))
import Lens.Micro (to, (^.))
import Plow.Logging (IOTracer (..), traceWith)
import Plow.Logging.Async (withAsyncHandleTracer)
import Prettyprinter (Pretty)
import System.IO (BufferMode (NoBuffering), hFlush, hSetBuffering, hSetEncoding, stdin, stdout, utf8)

-- import           System.Exit

-- This is the entry point for launching an LSP server, explicitly passing in handles for input and output
-- the `getIdents` parameter is a handle for input parameters, only used by the frontend.
-- This is used in the script editor, where the user only specifies the body of the script in the editor
-- and defines the input arguments separately in the sidebar. When processing in the LSP server, we have to
-- manually join the body of the script coming from the monaco editor with the parameters. i.e. if the user
-- specifies parameters ["a", "b"] and the body of the script is "a + b", then we will pass "fun a b -> a + b"
-- to the inferno typechecker.
runInfernoLspServerWith ::
  forall m c.
  (MonadError EvalError m, Pretty c, Eq c) =>
  IOTracer T.Text ->
  IO BS.ByteString ->
  (BSL.ByteString -> IO ()) ->
  ModuleMap m c ->
  IO [Maybe Ident] ->
  (InfernoType -> Either T.Text ()) ->
  -- | Action to run before start parsing
  ((UUID, UTCTime) -> IO ()) ->
  -- | Action to run after parsing is done
  ((UUID, UTCTime) -> ParsedResult -> IO ParsedResult) ->
  IO Int
runInfernoLspServerWith tracer clientIn clientOut prelude getIdents validateInput before after = flip E.catches handlers $ do
  rin <- atomically newTChan :: IO (TChan ReactorInput)
  docMap <- atomically $ newTVar mempty
  let infernoEnv = InfernoEnv docMap tracer getIdents before after validateInput

  let serverDefinition =
        ServerDefinition
          { defaultConfig = (),
            onConfigurationChange = \old _v -> Right old,
            doInitialize = \env _ -> forkIO (reactor tracer rin) >> pure (Right env),
            staticHandlers = lspHandlers @m @c prelude rin,
            interpretHandler = \env -> Iso (flip runReaderT infernoEnv . runLspT env) liftIO,
            options = lspOptions
          }

  let serverTracer = traceWith tracer . T.pack . show
  i <- runServerWith (LogAction serverTracer) (LogAction (liftIO . serverTracer)) clientIn clientOut serverDefinition
  traceWith tracer "shutting down..."
  pure i
  where
    handlers =
      [ E.Handler ioExcept,
        E.Handler someExcept
      ]
    ioExcept (e :: E.IOException) = traceWith tracer (T.pack (show e)) >> return 1
    someExcept (e :: E.SomeException) = traceWith tracer (T.pack (show e)) >> return 1

runInfernoLspServer :: forall m c. (MonadError EvalError m, Pretty c, Eq c) => ModuleMap m c -> IO Int
runInfernoLspServer prelude = do
  hSetBuffering stdin NoBuffering
  hSetEncoding stdin utf8

  hSetBuffering stdout NoBuffering
  hSetEncoding stdout utf8

  let clientIn = BS.hGetSome stdin defaultChunkSize

      clientOut out = do
        BSL.hPut stdout out
        hFlush stdout
      getIdents = pure []

  withAsyncHandleTracer stdout 100 $ \tracer -> do
    let beforeParse _ = pure ()
        afterParse _ = pure
    runInfernoLspServerWith @m @c tracer clientIn clientOut prelude getIdents (const $ Right ()) beforeParse afterParse

-- ---------------------------------------------------------------------

syncOptions :: J.TextDocumentSyncOptions
syncOptions =
  J.TextDocumentSyncOptions
    { J._openClose = Just True,
      J._change = Just J.TdSyncIncremental,
      J._willSave = Just False,
      J._willSaveWaitUntil = Just False,
      J._save = Just $ J.InR $ J.SaveOptions $ Just False
    }

lspOptions :: Options
lspOptions =
  defaultOptions
    { textDocumentSync = Just syncOptions,
      executeCommandCommands = Nothing
    }

-- ---------------------------------------------------------------------

-- The reactor is a process that serialises and buffers all requests from the
-- LSP client, so they can be sent to the backend compiler one at a time, and a
-- reply sent.

-- | Helper type to reduce typing
type ParsedResult = Either [J.Diagnostic] (Expr (Pinned VCObjectHash) (), TCScheme, [(J.Range, J.MarkupContent)])

withParseAndInfer :: MonadIO m => ((UUID, UTCTime) -> m ()) -> ((UUID, UTCTime) -> ParsedResult -> m ParsedResult) -> m ParsedResult -> m ParsedResult
withParseAndInfer before after action = do
  ts <- liftIO getCurrentTime
  uuid <- UUID <$> liftIO UUID.V4.nextRandom

  before (uuid, ts)
  result <- action
  after (uuid, ts) result

data InfernoEnv = InfernoEnv
  { hovers :: TVar (Map (J.NormalizedUri, J.Int32) [(J.Range, J.MarkupContent)]),
    tracer :: IOTracer T.Text,
    getIdents :: IO [Maybe Ident],
    -- | Action to run before start parsing
    beforeParse :: (UUID, UTCTime) -> IO (),
    -- | Action to run after parsing is done
    afterParse :: (UUID, UTCTime) -> ParsedResult -> IO ParsedResult,
    -- | If you don't care about the input type use (const $ Right ())
    validateInput :: InfernoType -> Either T.Text ()
  }

type InfernoLspM = LspT () (ReaderT InfernoEnv IO)

newtype ReactorInput
  = ReactorAction (IO ())

-- ---------------------------------------------------------------------

-- | The single point that all events flow through, allowing management of state
-- to stitch replies and requests together from the two asynchronous sides: lsp
-- server and backend compiler
reactor :: IOTracer T.Text -> TChan ReactorInput -> IO ()
reactor tracer inp = do
  traceWith tracer "Started the reactor"
  forever $ do
    ReactorAction act <- atomically $ readTChan inp
    act

getInfernoEnv :: InfernoLspM InfernoEnv
getInfernoEnv = LspT $ ReaderT $ \_ -> ask

trace :: String -> InfernoLspM ()
trace s = LspT $
  ReaderT $ \_ -> do
    InfernoEnv {tracer} <- ask
    traceWith tracer (T.pack s)

sendDiagnostics :: J.NormalizedUri -> J.TextDocumentVersion -> [J.Diagnostic] -> InfernoLspM ()
sendDiagnostics fileUri version diags =
  publishDiagnostics 100 fileUri version (partitionBySource diags)

-- | Check if we have a handler, and if we create a haskell-lsp handler to pass it as
-- input into the reactor
lspHandlers :: forall m c. (MonadError EvalError m, Pretty c, Eq c) => ModuleMap m c -> TChan ReactorInput -> Handlers InfernoLspM
lspHandlers prelude rin = mapHandlers goReq goNot (handle @m @c prelude)
  where
    goReq :: forall (a :: J.Method 'J.FromClient 'J.Request). Handler InfernoLspM a -> Handler InfernoLspM a
    goReq f = \msg k -> do
      env <- getLspEnv
      infernoEnv <- getInfernoEnv
      liftIO $ atomically $ writeTChan rin $ ReactorAction (flip runReaderT infernoEnv $ runLspT env $ f msg k)

    goNot :: forall (a :: J.Method 'J.FromClient 'J.Notification). Handler InfernoLspM a -> Handler InfernoLspM a
    goNot f = \msg -> do
      env <- getLspEnv
      infernoEnv <- getInfernoEnv
      liftIO $ atomically $ writeTChan rin $ ReactorAction (flip runReaderT infernoEnv $ runLspT env $ f msg)

-- | Where the actual logic resides for handling requests and notifications.
handle :: forall m c. (MonadError EvalError m, Pretty c, Eq c) => ModuleMap m c -> Handlers InfernoLspM
handle prelude =
  mconcat
    [ notificationHandler J.STextDocumentDidOpen $ \msg -> do
        InfernoEnv {hovers = hoversTV, getIdents, beforeParse, afterParse, validateInput} <- getInfernoEnv
        let doc_uri = msg ^. J.params . J.textDocument . J.uri . to J.toNormalizedUri
            doc_txt = msg ^. J.params . J.textDocument . J.text
        idents <- liftIO getIdents
        trace $ "Processing DidOpenTextDocument for: " ++ show doc_uri
        hovers <-
          withParseAndInfer (liftIO . beforeParse) (\x y -> liftIO $ afterParse x y) (parseAndInfer @m @_ @c prelude idents doc_txt validateInput) >>= \case
            Left errs -> do
              sendDiagnostics doc_uri (Just 0) errs
              pure mempty
            Right (_expr, _ty, hovers) -> do
              trace $ "Created hovers for: " ++ show doc_uri
              sendDiagnostics doc_uri (Just 0) []
              pure hovers

        doc_version <-
          getVirtualFile doc_uri >>= \case
            Just (VirtualFile doc_version _ _) -> pure doc_version
            Nothing -> pure 0 -- Maybe a good default?
        liftIO $ atomically $ modifyTVar hoversTV $ \hoversMap -> Map.insert (doc_uri, doc_version) hovers hoversMap,
      notificationHandler J.STextDocumentDidChange $ \msg -> do
        InfernoEnv {hovers = hoversTV, getIdents, beforeParse, afterParse, validateInput} <- getInfernoEnv
        let doc_uri =
              msg
                ^. J.params
                  . J.textDocument
                  . J.uri
                  . to J.toNormalizedUri
        getVirtualFile doc_uri >>= \case
          Just (VirtualFile doc_version _ rope) -> do
            let txt = Rope.toText rope
            trace $ "Processing DidChangeTextDocument for: " ++ show doc_uri ++ " - " ++ show doc_version
            idents <- liftIO getIdents
            hovers <-
              withParseAndInfer (liftIO . beforeParse) (\x y -> liftIO $ afterParse x y) (parseAndInfer @m @_ @c prelude idents txt validateInput) >>= \case
                Left errs -> do
                  trace $ "Sending errs: " ++ show errs
                  sendDiagnostics doc_uri (Just doc_version) errs
                  pure mempty
                Right (_expr, _ty, hovers) -> do
                  trace $ "Updated hovers for: " ++ show doc_uri ++ " - " ++ show doc_version
                  sendDiagnostics doc_uri (Just doc_version) []
                  pure hovers
            trace $ "Setting hovers: " ++ show hovers
            liftIO $ atomically $ modifyTVar hoversTV $ \hoversMap -> Map.insert (doc_uri, doc_version) hovers hoversMap
          Nothing -> pure (),
      requestHandler J.STextDocumentCompletion $ \req responder -> do
        InfernoEnv {getIdents} <- getInfernoEnv
        let doc_uri = req ^. J.params . J.textDocument . J.uri . to J.toNormalizedUri
            pos = req ^. J.params . J.position

        completionPrefix <-
          getVirtualFile doc_uri >>= \case
            Just (VirtualFile _ _ rope) -> do
              let txt = Rope.toText rope
              let (_completionLeadup, completionPrefix) = completionQueryAt txt pos
              pure $ Just completionPrefix
            Nothing -> pure Nothing
        trace $ "Completion prefix: " <> show completionPrefix
        mIdents <- liftIO $ getIdents
        let completions = maybe [] id $ findInPrelude @c (preludeNameToTypeMap prelude) <$> completionPrefix
            idents = unIdent <$> catMaybes mIdents
            identCompletions = maybe [] id $ identifierCompletionItems idents <$> completionPrefix
            rwsCompletions = maybe [] id $ rwsCompletionItems <$> completionPrefix
            moduleCompletions = maybe [] id $ filterModuleNameCompletionItems @c (preludeNameToTypeMap prelude) <$> completionPrefix
            allCompletions = rwsCompletions ++ moduleCompletions ++ identCompletions ++ map (uncurry $ mkCompletionItem prelude $ fromMaybe "" completionPrefix) completions

        trace $ "Ident completions: " <> show identCompletions
        trace $ "Found completions: " <> show completions

        responder $ Right $ J.InL $ J.List $ allCompletions,
      requestHandler J.STextDocumentHover $ \req responder -> do
        InfernoEnv {hovers = hoversTV} <- getInfernoEnv
        trace "Processing a textDocument/hover request"
        let J.Position l c = req ^. J.params . J.position
            doc_uri =
              req
                ^. J.params
                  . J.textDocument
                  . J.uri
                  . to J.toNormalizedUri

        mDoc_version <-
          getVirtualFile doc_uri >>= \case
            Just (VirtualFile doc_version _ _) -> pure $ Just doc_version
            Nothing -> pure Nothing

        hoversMap <- liftIO $ atomically $ readTVar hoversTV
        responder $
          Right $ case mDoc_version of
            Just doc_version -> case Map.lookup (doc_uri, doc_version) hoversMap of
              Just hovers ->
                (\(r, t) -> J.Hover (J.HoverContents t) (Just r))
                  <$> ( findSmallestRange $
                          flip filter hovers $
                            \(J.Range (J.Position lStart cStart) (J.Position lEnd cEnd), _) ->
                              if l < lStart || l > lEnd
                                then False
                                else
                                  if l == lStart && c < cStart
                                    then False
                                    else
                                      if l == lEnd && c > cEnd
                                        then False
                                        else True
                      )
              Nothing -> Nothing
            Nothing -> Nothing
    ]

findSmallestRange :: [(J.Range, a)] -> Maybe (J.Range, a)
findSmallestRange = \case
  [] -> Nothing
  (r : rs) -> Just $ foldr (\x@(a, _) y@(b, _) -> if a `containsRange` b then y else x) r rs
  where
    containsRange
      (J.Range (J.Position aStartLine aStartColumn) (J.Position aEndLine aEndColumn))
      (J.Range (J.Position bStartLine bStartColumn) (J.Position bEndLine bEndColumn)) =
        if bStartLine < aStartLine || bEndLine < aStartLine
          then False
          else
            if bStartLine > aEndLine || bEndLine > aEndLine
              then False
              else
                if bStartLine == aStartLine && bStartColumn < aStartColumn
                  then False
                  else
                    if bEndLine == aEndLine && bEndColumn > aEndColumn
                      then False
                      else True