dhall-lsp-server-1.1.2: src/Dhall/LSP/Handlers.hs
{-# LANGUAGE BlockArguments #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE DataKinds #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE ViewPatterns #-}
module Dhall.LSP.Handlers where
import Data.Void (Void)
import Dhall.Core
( Expr (Embed, Note)
, Import (..)
, ImportHashed (..)
, ImportType (..)
, headers
, pretty
)
import Dhall.Import (localToPath)
import Dhall.Parser (Src (..))
import Dhall.LSP.Backend.Completion
( Completion (..)
, buildCompletionContext
, completeEnvironmentImport
, completeFromContext
, completeLocalImport
, completeProjections
, completionQueryAt
)
import Dhall.LSP.Backend.Dhall
( FileIdentifier
, fileIdentifierFromFilePath
, fileIdentifierFromURI
, invalidate
, load
, parse
, parseWithHeader
, typecheck
)
import Dhall.LSP.Backend.Diagnostics
( Diagnosis (..)
, Range (..)
, diagnose
, embedsWithRanges
, explain
, rangeFromDhall
)
import Dhall.LSP.Backend.Formatting (formatExpr, formatExprWithHeader)
import Dhall.LSP.Backend.Freezing
( computeSemanticHash
, getAllImportsWithHashPositions
, getImportHashPosition
, stripHash
)
import Dhall.LSP.Backend.Linting (Suggestion (..), lint, suggest)
import Dhall.LSP.Backend.Parsing (binderExprFromText)
import Dhall.LSP.Backend.Typing (annotateLet, exprAt, typeAt)
import Dhall.LSP.State
import Control.Applicative ((<|>))
import Control.Lens (assign, modifying, use, (^.))
import Control.Monad (forM, guard)
import Control.Monad.Trans (lift, liftIO)
import Control.Monad.Trans.Except (catchE, throwE)
import Data.Aeson (FromJSON(..), Value(..))
import Data.Maybe (maybeToList)
import Data.Text (Text, isPrefixOf)
import Language.LSP.Server (Handlers, LspT)
import Language.LSP.Types hiding (Range(..), line)
import Language.LSP.Types.Lens
import System.FilePath
import Text.Megaparsec (SourcePos (..), unPos)
import qualified Data.Aeson as Aeson
import qualified Data.HashMap.Strict as HashMap
import qualified Data.Map.Strict as Map
import qualified Data.Rope.UTF16 as Rope
import qualified Data.Text as Text
import qualified Language.LSP.Server as LSP
import qualified Language.LSP.Types as LSP.Types
import qualified Language.LSP.VFS as LSP
import qualified Network.URI as URI
import qualified Network.URI.Encode as URI
liftLSP :: LspT ServerConfig IO a -> HandlerM a
liftLSP m = lift (lift m)
-- | A helper function to query haskell-lsp's VFS.
readUri :: Uri -> HandlerM Text
readUri uri_ = do
mVirtualFile <- liftLSP (LSP.getVirtualFile (LSP.Types.toNormalizedUri uri_))
case mVirtualFile of
Just (LSP.VirtualFile _ _ rope) -> return (Rope.toText rope)
Nothing -> throwE (Error, "Could not find " <> Text.pack (show uri_) <> " in VFS.")
loadFile :: Uri -> HandlerM (Expr Src Void)
loadFile uri_ = do
txt <- readUri uri_
fileIdentifier <- fileIdentifierFromUri uri_
cache <- use importCache
expr <- case parse txt of
Right e -> return e
_ -> throwE (Error, "Failed to parse Dhall file.")
loaded <- liftIO $ load fileIdentifier expr cache
(cache', expr') <- case loaded of
Right x -> return x
_ -> throwE (Error, "Failed to resolve imports.")
-- Update cache. Don't cache current expression because it might not have been
-- written to disk yet (readUri reads from the VFS).
assign importCache cache'
return expr'
-- helper
fileIdentifierFromUri :: Uri -> HandlerM FileIdentifier
fileIdentifierFromUri uri_ =
let mFileIdentifier = fmap fileIdentifierFromFilePath (uriToFilePath uri_)
<|> (do uri' <- (URI.parseURI . Text.unpack . getUri) uri_
fileIdentifierFromURI uri')
in case mFileIdentifier of
Just fileIdentifier -> return fileIdentifier
Nothing -> throwE (Error, getUri uri_ <> " is not a valid name for a dhall file.")
-- helper
rangeToJSON :: Range -> LSP.Types.Range
rangeToJSON (Range (x1,y1) (x2,y2)) =
LSP.Types.Range
(Position (fromIntegral x1) (fromIntegral y1))
(Position (fromIntegral x2) (fromIntegral y2))
hoverHandler :: Handlers HandlerM
hoverHandler =
LSP.requestHandler STextDocumentHover \request respond -> handleErrorWithDefault respond Nothing do
let uri_ = request^.params.textDocument.uri
let Position{ _line = fromIntegral -> _line, _character = fromIntegral -> _character } = request^.params.position
errorMap <- use errors
case Map.lookup uri_ errorMap of
Nothing -> do
expr <- loadFile uri_
(welltyped, _) <- case typecheck expr of
Left _ -> throwE (Info, "Can't infer type; code does not type-check.")
Right wt -> return wt
case typeAt (_line, _character) welltyped of
Left err -> throwE (Error, Text.pack err)
Right (mSrc, typ) -> do
let _range = fmap (rangeToJSON . rangeFromDhall) mSrc
let _contents = HoverContents (MarkupContent MkPlainText (pretty typ))
respond (Right (Just Hover{ _contents, _range }))
Just err -> do
let isHovered (Diagnosis _ (Just (Range left right)) _) =
left <= (_line, _character) && (_line, _character) <= right
isHovered _ =
False
let hoverFromDiagnosis (Diagnosis _ (Just (Range left right)) diagnosis) = do
let _range = Just (rangeToJSON (Range left right))
encodedDiag = URI.encode (Text.unpack diagnosis)
_kind = MkMarkdown
_value =
"[Explain error](dhall-explain:?"
<> Text.pack encodedDiag
<> " )"
_contents = HoverContents MarkupContent{..}
Just Hover{ _contents, _range }
hoverFromDiagnosis _ =
Nothing
let mHover = do
explanation <- explain err
guard (isHovered explanation)
hoverFromDiagnosis explanation
respond (Right mHover)
documentLinkHandler :: Handlers HandlerM
documentLinkHandler =
LSP.requestHandler STextDocumentDocumentLink \request respond -> handleErrorWithDefault respond (List []) do
let uri_ = request^.params.textDocument.uri
path <- case uriToFilePath uri_ of
Nothing ->
throwE (Log, "Could not process document links; failed to convert URI to file path.")
Just p ->
return p
txt <- readUri uri_
expr <- case parse txt of
Right e ->
return e
Left _ ->
throwE (Log, "Could not process document links; did not parse.")
let imports = embedsWithRanges expr :: [(Range, Import)]
let basePath = takeDirectory path
let go :: (Range, Import) -> IO [DocumentLink]
go (range_, Import (ImportHashed _ (Local prefix file)) _) = do
filePath <- localToPath prefix file
let filePath' = basePath </> filePath -- absolute file path
let _range = rangeToJSON range_
let _target = Just (filePathToUri filePath')
let _tooltip = Nothing
let _xdata = Nothing
return [DocumentLink {..}]
go (range_, Import (ImportHashed _ (Remote url)) _) = do
let _range = rangeToJSON range_
let url' = url { headers = Nothing }
let _target = Just (Uri (pretty url'))
let _tooltip = Nothing
let _xdata = Nothing
return [DocumentLink {..}]
go _ = return []
links <- liftIO $ mapM go imports
respond (Right (List (concat links)))
diagnosticsHandler :: Uri -> HandlerM ()
diagnosticsHandler _uri = do
txt <- readUri _uri
fileIdentifier <- fileIdentifierFromUri _uri
-- make sure we don't keep a stale version around
modifying importCache (invalidate fileIdentifier)
cache <- use importCache
errs <- flip catchE (return . Just) $ do
expr <- case parse txt of
Right e -> return e
Left err -> throwE err
loaded <- liftIO $ load fileIdentifier expr cache
(cache', expr') <- case loaded of
Right x -> return x
Left err -> throwE err
_ <- case typecheck expr' of
Right (wt, _typ) -> return wt
Left err -> throwE err
assign importCache cache'
return Nothing
let suggestions =
case parse txt of
Right expr -> suggest expr
_ -> []
suggestionToDiagnostic Suggestion { range = range_, .. } =
let _range = rangeToJSON range_
_severity = Just DsHint
_source = Just "Dhall.Lint"
_code = Nothing
_message = suggestion
_tags = Nothing
_relatedInformation = Nothing
in Diagnostic {..}
diagnosisToDiagnostic Diagnosis { range = range_, .. } =
let _range = case range_ of
Just range' -> rangeToJSON range'
Nothing -> LSP.Types.Range (Position 0 0) (Position 0 0)
_severity = Just DsError
_source = Just doctor
_code = Nothing
_tags = Nothing
_message = diagnosis
_relatedInformation = Nothing
in Diagnostic {..}
modifying errors (Map.alter (const errs) _uri) -- cache errors
let _version = Nothing
let _diagnostics =
List
( concatMap (map diagnosisToDiagnostic . diagnose) (maybeToList errs)
++ map suggestionToDiagnostic suggestions
)
liftLSP (LSP.sendNotification STextDocumentPublishDiagnostics PublishDiagnosticsParams{ _uri, _version, _diagnostics })
documentFormattingHandler :: Handlers HandlerM
documentFormattingHandler =
LSP.requestHandler STextDocumentFormatting \request respond -> handleErrorWithDefault respond (List []) do
let _uri = request^.params.textDocument.uri
txt <- readUri _uri
(header, expr) <- case parseWithHeader txt of
Right res -> return res
_ -> throwE (Warning, "Failed to format dhall code; parse error.")
ServerConfig{..} <- liftLSP LSP.getConfig
let numLines = fromIntegral (Text.length txt)
let _newText= formatExprWithHeader chosenCharacterSet expr header
let _range = LSP.Types.Range (Position 0 0) (Position numLines 0)
respond (Right (List [TextEdit{..}]))
executeCommandHandler :: Handlers HandlerM
executeCommandHandler =
LSP.requestHandler SWorkspaceExecuteCommand \request respond -> handleErrorWithDefault respond Aeson.Null do
let command_ = request^.params.command
if | command_ == "dhall.server.lint" ->
executeLintAndFormat request respond
| command_ == "dhall.server.annotateLet" ->
executeAnnotateLet request
| command_ == "dhall.server.freezeImport" ->
executeFreezeImport request
| command_ == "dhall.server.freezeAllImports" ->
executeFreezeAllImports request
| otherwise -> do
throwE
( Warning
, "Command '" <> command_ <> "' not known; ignored."
)
getCommandArguments
:: FromJSON a => RequestMessage 'WorkspaceExecuteCommand -> HandlerM a
-- (HasParams s a, FromJSON a) => s -> HandlerM a
getCommandArguments request = do
json <- case request ^. params . arguments of
Just (List (x : _)) -> return x
_ -> throwE (Error, "Failed to execute command; arguments missing.")
case Aeson.fromJSON json of
Aeson.Success args ->
return args
_ ->
throwE (Error, "Failed to execute command; failed to parse arguments.")
-- implements dhall.server.lint
executeLintAndFormat
:: RequestMessage 'WorkspaceExecuteCommand
-> (Either a Value -> HandlerM b)
-> HandlerM ()
executeLintAndFormat request respond = do
uri_ <- getCommandArguments request
txt <- readUri uri_
(header, expr) <- case parseWithHeader txt of
Right res -> return res
_ -> throwE (Warning, "Failed to lint dhall code; parse error.")
ServerConfig{..} <- liftLSP LSP.getConfig
let numLines = fromIntegral (Text.length txt)
let _newText = formatExprWithHeader chosenCharacterSet (lint expr) header
let _range = LSP.Types.Range (Position 0 0) (Position numLines 0)
let _edit =
WorkspaceEdit
{ _changes = Just (HashMap.singleton uri_ (List [TextEdit{..}]))
, _documentChanges = Nothing
, _changeAnnotations = Nothing
}
let _label = Nothing
_ <- respond (Right Aeson.Null)
_ <- liftLSP (LSP.sendRequest SWorkspaceApplyEdit ApplyWorkspaceEditParams{ _label, _edit } nullHandler)
return ()
executeAnnotateLet
:: RequestMessage 'WorkspaceExecuteCommand
-> HandlerM ()
executeAnnotateLet request = do
args <- getCommandArguments request :: HandlerM TextDocumentPositionParams
let uri_ = args ^. textDocument . uri
line_ = fromIntegral (args ^. position . line)
col_ = fromIntegral (args ^. position . character)
expr <- loadFile uri_
(welltyped, _) <- case typecheck expr of
Left _ -> throwE (Warning, "Failed to annotate let binding; not well-typed.")
Right e -> return e
ServerConfig{..} <- liftLSP LSP.getConfig
(Src (SourcePos _ x1 y1) (SourcePos _ x2 y2) _, annotExpr)
<- case annotateLet (line_, col_) welltyped of
Right x -> return x
Left msg -> throwE (Warning, Text.pack msg)
let _range = LSP.Types.Range (Position (fromIntegral (unPos x1 - 1)) (fromIntegral (unPos y1 - 1)))
(Position (fromIntegral (unPos x2 - 1)) (fromIntegral (unPos y2 - 1)))
let _newText= formatExpr chosenCharacterSet annotExpr
let _edit = WorkspaceEdit
{ _changes = Just (HashMap.singleton uri_ (List [TextEdit{..}]))
, _documentChanges = Nothing
, _changeAnnotations = Nothing
}
let _label = Nothing
_ <- liftLSP (LSP.sendRequest SWorkspaceApplyEdit ApplyWorkspaceEditParams{ _label, _edit } nullHandler)
return ()
executeFreezeAllImports
:: RequestMessage 'WorkspaceExecuteCommand
-> HandlerM ()
executeFreezeAllImports request = do
uri_ <- getCommandArguments request
fileIdentifier <- fileIdentifierFromUri uri_
txt <- readUri uri_
expr <- case parse txt of
Right e -> return e
Left _ -> throwE (Warning, "Could not freeze imports; did not parse.")
let importRanges = getAllImportsWithHashPositions expr
edits_ <- forM importRanges $ \(import_, Range (x1, y1) (x2, y2)) -> do
cache <- use importCache
let importExpr = Embed (stripHash import_)
hashResult <- liftIO $ computeSemanticHash fileIdentifier importExpr cache
(cache', hash) <- case hashResult of
Right (c, t) -> return (c, t)
Left _ -> throwE (Error, "Could not freeze import; failed to evaluate import.")
assign importCache cache'
let _range = LSP.Types.Range (Position (fromIntegral x1) (fromIntegral y1)) (Position (fromIntegral x2) (fromIntegral y2))
let _newText = " " <> hash
return TextEdit{..}
let _edit = WorkspaceEdit
{ _changes = Just (HashMap.singleton uri_ (List edits_))
, _documentChanges = Nothing
, _changeAnnotations = Nothing
}
let _label = Nothing
_ <- liftLSP (LSP.sendRequest SWorkspaceApplyEdit ApplyWorkspaceEditParams{ _edit, _label } nullHandler)
return ()
executeFreezeImport
:: RequestMessage 'WorkspaceExecuteCommand
-> HandlerM ()
executeFreezeImport request = do
args <- getCommandArguments request :: HandlerM TextDocumentPositionParams
let uri_ = args ^. textDocument . uri
let line_ = fromIntegral (args ^. position . line)
let col_ = fromIntegral (args ^. position . character)
txt <- readUri uri_
expr <- case parse txt of
Right e -> return e
Left _ -> throwE (Warning, "Could not freeze import; did not parse.")
(src, import_)
<- case exprAt (line_, col_) expr of
Just (Note src (Embed i)) -> return (src, i)
_ -> throwE (Warning, "You weren't pointing at an import!")
Range (x1, y1) (x2, y2) <- case getImportHashPosition src of
Just range_ -> return range_
Nothing -> throwE (Error, "Failed to re-parse import!")
fileIdentifier <- fileIdentifierFromUri uri_
cache <- use importCache
let importExpr = Embed (stripHash import_)
hashResult <- liftIO $ computeSemanticHash fileIdentifier importExpr cache
(cache', hash) <- case hashResult of
Right (c, t) -> return (c, t)
Left _ -> throwE (Error, "Could not freeze import; failed to evaluate import.")
assign importCache cache'
let _range = LSP.Types.Range (Position (fromIntegral x1) (fromIntegral y1)) (Position (fromIntegral x2) (fromIntegral y2))
let _newText = " " <> hash
let _edit = WorkspaceEdit
{ _changes = Just (HashMap.singleton uri_ (List [TextEdit{..}]))
, _documentChanges = Nothing
, _changeAnnotations = Nothing
}
let _label = Nothing
_ <- liftLSP (LSP.sendRequest SWorkspaceApplyEdit ApplyWorkspaceEditParams{ _edit, _label } nullHandler)
return ()
completionHandler :: Handlers HandlerM
completionHandler =
LSP.requestHandler STextDocumentCompletion \request respond -> handleErrorWithDefault respond (InR (CompletionList False (List []))) do
let uri_ = request ^. params . textDocument . uri
line_ = fromIntegral (request ^. params . position . line)
col_ = fromIntegral (request ^. params . position . character)
txt <- readUri uri_
let (completionLeadup, completionPrefix) = completionQueryAt txt (line_, col_)
let computeCompletions
-- environment variable
| "env:" `isPrefixOf` completionPrefix =
liftIO completeEnvironmentImport
-- local import
| any (`isPrefixOf` completionPrefix) [ "/", "./", "../", "~/" ] = do
let relativeTo | Just path <- uriToFilePath uri_ = path
| otherwise = "."
liftIO $ completeLocalImport relativeTo (Text.unpack completionPrefix)
-- record projection / union constructor
| (target_, _) <- Text.breakOnEnd "." completionPrefix
, not (Text.null target_) = do
let bindersExpr = binderExprFromText completionLeadup
fileIdentifier <- fileIdentifierFromUri uri_
cache <- use importCache
loadedBinders <- liftIO $ load fileIdentifier bindersExpr cache
(cache', bindersExpr') <-
case loadedBinders of
Right (cache', binders) ->
return (cache', binders)
Left _ -> throwE (Log, "Could not complete projection; failed to load binders expression.")
let completionContext = buildCompletionContext bindersExpr'
targetExpr <- case parse (Text.dropEnd 1 target_) of
Right e -> return e
Left _ -> throwE (Log, "Could not complete projection; prefix did not parse.")
loaded' <- liftIO $ load fileIdentifier targetExpr cache'
case loaded' of
Right (cache'', targetExpr') -> do
assign importCache cache''
return (completeProjections completionContext targetExpr')
Left _ -> return []
-- complete identifiers in scope
| otherwise = do
let bindersExpr = binderExprFromText completionLeadup
fileIdentifier <- fileIdentifierFromUri uri_
cache <- use importCache -- todo save cache afterwards
loadedBinders <- liftIO $ load fileIdentifier bindersExpr cache
bindersExpr' <-
case loadedBinders of
Right (cache', binders) -> do
assign importCache cache'
return binders
Left _ -> throwE (Log, "Could not complete projection; failed to load binders expression.")
let context_ = buildCompletionContext bindersExpr'
return (completeFromContext context_)
completions <- computeCompletions
let toCompletionItem (Completion {..}) = CompletionItem {..}
where
_label = completeText
_kind = Nothing
_tags = mempty
_detail = fmap pretty completeType
_documentation = Nothing
_deprecated = Nothing
_preselect = Nothing
_sortText = Nothing
_filterText = Nothing
_insertText = Nothing
_insertTextFormat = Nothing
_insertTextMode = Nothing
_textEdit = Nothing
_additionalTextEdits = Nothing
_commitCharacters = Nothing
_command = Nothing
_xdata = Nothing
let _items = List (map toCompletionItem completions)
let _isIncomplete = False
respond (Right (InR CompletionList{..}))
nullHandler :: a -> LspT ServerConfig IO ()
nullHandler _ = return ()
didOpenTextDocumentNotificationHandler :: Handlers HandlerM
didOpenTextDocumentNotificationHandler =
LSP.notificationHandler STextDocumentDidOpen \notification -> do
let _uri = notification^.params.textDocument.uri
diagnosticsHandler _uri
didSaveTextDocumentNotificationHandler :: Handlers HandlerM
didSaveTextDocumentNotificationHandler =
LSP.notificationHandler STextDocumentDidSave \notification -> do
let _uri = notification^.params.textDocument.uri
diagnosticsHandler _uri
-- this handler is a stab to prevent `lsp:no handler for:` messages.
initializedHandler :: Handlers HandlerM
initializedHandler =
LSP.notificationHandler SInitialized \_ -> return ()
-- this handler is a stab to prevent `lsp:no handler for:` messages.
workspaceChangeConfigurationHandler :: Handlers HandlerM
workspaceChangeConfigurationHandler =
LSP.notificationHandler SWorkspaceDidChangeConfiguration \_ -> return ()
-- this handler is a stab to prevent `lsp:no handler for:` messages.
textDocumentChangeHandler :: Handlers HandlerM
textDocumentChangeHandler =
LSP.notificationHandler STextDocumentDidChange \_ -> return ()
-- this handler is a stab to prevent `lsp:no handler for:` messages.
cancelationHandler :: Handlers HandlerM
cancelationHandler =
LSP.notificationHandler SCancelRequest \_ -> return ()
handleErrorWithDefault :: (Either a1 b -> HandlerM a2)
-> b
-> HandlerM a2
-> HandlerM a2
handleErrorWithDefault respond _default = flip catchE handler
where
handler (Log, _message) = do
let _xtype = MtLog
liftLSP $ LSP.sendNotification SWindowLogMessage LogMessageParams{..}
respond (Right _default)
handler (severity_, _message) = do
let _xtype = case severity_ of
Error -> MtError
Warning -> MtWarning
Info -> MtInfo
Log -> MtLog
liftLSP $ LSP.sendNotification SWindowShowMessage ShowMessageParams{..}
respond (Right _default)