swarm-0.4: src/Swarm/Language/LSP/Hover.hs
{-# LANGUAGE OverloadedStrings #-}
module Swarm.Language.LSP.Hover (
showHoverInfo,
-- * Documentation rendering
renderDoc,
treeToMarkdown,
-- * Finding source location
narrowToPosition,
-- * Explaining source position
explain,
) where
import Control.Applicative ((<|>))
import Control.Lens ((^.))
import Control.Monad (guard, void)
import Data.Foldable (asum)
import Data.Graph
import Data.List.NonEmpty (NonEmpty (..))
import Data.Map qualified as M
import Data.Maybe (catMaybes, fromMaybe)
import Data.Text (Text)
import Data.Text qualified as T
import Data.Text.Utf16.Rope qualified as R
import Language.LSP.Types qualified as J
import Language.LSP.VFS
import Swarm.Language.Context as Ctx
import Swarm.Language.Module (Module (..))
import Swarm.Language.Parse (readTerm', unTuple)
import Swarm.Language.Pipeline (ProcessedTerm (..), processParsedTerm)
import Swarm.Language.Pretty (prettyText)
import Swarm.Language.Syntax
import Swarm.Language.Typecheck (inferConst)
import Swarm.Language.Types
import Swarm.Util qualified as U
withinBound :: Int -> SrcLoc -> Bool
withinBound pos (SrcLoc s e) = pos >= s && pos < e
withinBound _ NoLoc = False
ropeToLspPosition :: R.Position -> J.Position
ropeToLspPosition (R.Position l c) =
J.Position (fromIntegral l) (fromIntegral c)
lspToRopePosition :: J.Position -> R.Position
lspToRopePosition (J.Position myLine myCol) =
R.Position (fromIntegral myLine) (fromIntegral myCol)
showHoverInfo ::
J.NormalizedUri ->
J.TextDocumentVersion ->
J.Position ->
VirtualFile ->
Maybe (Text, Maybe J.Range)
showHoverInfo _ _ p vf@(VirtualFile _ _ myRope) =
case readTerm' content of
Left _ -> Nothing
Right Nothing -> Nothing
Right (Just stx) -> Just $ case processParsedTerm stx of
Left _e ->
let found@(Syntax foundSloc _) = narrowToPosition stx $ fromIntegral absolutePos
finalPos = posToRange myRope foundSloc
in (,finalPos) . treeToMarkdown 0 $ explain found
Right (ProcessedTerm modul _req _reqCtx) ->
let found@(Syntax' foundSloc _ _) = narrowToPosition (moduleAST modul) $ fromIntegral absolutePos
finalPos = posToRange myRope foundSloc
in (,finalPos) . treeToMarkdown 0 $ explain found
where
content = virtualFileText vf
absolutePos =
maybe 0 (R.length . fst) $
R.splitAtPosition (lspToRopePosition p) myRope
posToRange :: R.Rope -> SrcLoc -> Maybe J.Range
posToRange myRope foundSloc = do
(s, e) <- case foundSloc of
SrcLoc s e -> Just (s, e)
_ -> Nothing
(startRope, _) <- R.splitAt (fromIntegral s) myRope
(endRope, _) <- R.splitAt (fromIntegral e) myRope
return $
J.Range
(ropeToLspPosition $ R.lengthAsPosition startRope)
(ropeToLspPosition $ R.lengthAsPosition endRope)
descend ::
ExplainableType ty =>
-- | position
Int ->
-- | next element to inspect
Syntax' ty ->
Maybe (Syntax' ty)
descend pos s1@(Syntax' l1 _ _) = do
guard $ withinBound pos l1
return $ narrowToPosition s1 pos
-- | Find the most specific term for a given
-- position within the code.
narrowToPosition ::
ExplainableType ty =>
-- | parent term
Syntax' ty ->
-- | absolute offset within the file
Int ->
Syntax' ty
narrowToPosition s0@(Syntax' _ t ty) pos = fromMaybe s0 $ case t of
SLam lv _ s -> d (locVarToSyntax' lv $ getInnerType ty) <|> d s
SApp s1 s2 -> d s1 <|> d s2
SLet _ lv _ s1@(Syntax' _ _ lty) s2 -> d (locVarToSyntax' lv lty) <|> d s1 <|> d s2
SDef _ lv _ s@(Syntax' _ _ lty) -> d (locVarToSyntax' lv lty) <|> d s
SBind mlv s1@(Syntax' _ _ lty) s2 -> (mlv >>= d . flip locVarToSyntax' (getInnerType lty)) <|> d s1 <|> d s2
SPair s1 s2 -> d s1 <|> d s2
SDelay _ s -> d s
SRcd m -> asum . map d . catMaybes . M.elems $ m
SProj s1 _ -> d s1
SAnnotate s _ -> d s
SRequirements _ s -> d s
-- atoms - return their position and end recursion
TUnit -> Nothing
TConst {} -> Nothing
TDir {} -> Nothing
TInt {} -> Nothing
TText {} -> Nothing
TBool {} -> Nothing
TVar {} -> Nothing
TRequire {} -> Nothing
TRequireDevice {} -> Nothing
-- these should not show up in surface language
TRef {} -> Nothing
TRobot {} -> Nothing
TAntiInt {} -> Nothing
TAntiText {} -> Nothing
where
d = descend pos
renderDoc :: Int -> Text -> Text
renderDoc d t
| d == 0 = t
| otherwise = T.drop 2 . indent (max 0 (4 * (d - 1)) + 2) $ "* " <> t
where
indent x = T.unlines . map (T.replicate x " " <>) . T.lines
treeToMarkdown :: Int -> Tree Text -> Text
treeToMarkdown d (Node t children) =
T.unlines $ renderDoc d t : map (treeToMarkdown $ d + 1) children
class Show t => ExplainableType t where
-- | Pretty print the type.
prettyType :: t -> Text
-- | Strip the type of its outermost layer.
--
-- This allows us to strip lambda or command type
-- and get the type of the bound variable.
getInnerType :: t -> t
-- | Check if this type is same as the given 'Polytype'.
--
-- We use it to not print same type twice (e.g. inferred and generic).
eq :: t -> Polytype -> Bool
instance ExplainableType () where
prettyType = const "?"
getInnerType = id
eq _ _ = False
instance ExplainableType Polytype where
prettyType = prettyText
getInnerType = fmap $ \case
(l :->: _r) -> l
(TyCmd t) -> t
t -> t
eq = (==)
explain :: ExplainableType ty => Syntax' ty -> Tree Text
explain trm = case trm ^. sTerm of
TUnit -> literal "The unit value."
TConst c -> literal . constGenSig c $ briefDoc (constDoc $ constInfo c)
TDir {} -> literal "A direction literal."
TInt {} -> literal "An integer literal."
TText {} -> literal "A text literal."
TBool {} -> literal "A boolean literal."
TVar v -> pure $ typeSignature v ty ""
SRcd {} -> literal "A record literal."
SProj {} -> literal "A record projection."
-- type ascription
SAnnotate lhs typeAnn ->
Node
(typeSignature "_" typeAnn "A type ascription for")
[explain lhs]
-- special forms (function application will show for `$`, but really should be rare)
SApp {} -> explainFunction trm
TRequireDevice {} -> pure "Require a specific device to be equipped."
TRequire {} -> pure "Require a certain number of an entity."
SRequirements {} -> pure "Query the requirements of a term."
-- definition or bindings
SLet isRecursive var mTypeAnn rhs _b -> pure $ explainDefinition False isRecursive var (rhs ^. sType) mTypeAnn
SDef isRecursive var mTypeAnn rhs -> pure $ explainDefinition True isRecursive var (rhs ^. sType) mTypeAnn
SLam (LV _s v) _mType _syn ->
pure $
typeSignature v ty $
"A lambda expression binding the variable " <> U.bquote v <> "."
SBind mv rhs _cmds ->
pure $
typeSignature (maybe "__rhs" lvVar mv) (getInnerType $ rhs ^. sType) $
"A monadic bind for commands" <> maybe "." (\(LV _s v) -> ", that binds variable " <> U.bquote v <> ".") mv
-- composite types
SPair {} ->
Node
(typeSignature "_" ty "A tuple consisting of:")
(explain <$> unTuple trm)
SDelay {} ->
pure . T.unlines $
[ "Delay evaluation of a term, written `{...}`."
, ""
, "Swarm is an eager language, but in some cases (e.g. for `if` statements and recursive bindings) we need to delay evaluation."
, ""
, "The counterpart to `{...}` is `force`:"
, "```"
, "force {t} = t"
, "```"
]
-- internal syntax that should not actually show in hover
TRef {} -> internal "A memory reference."
TAntiInt {} -> internal "An antiquoted Haskell variable name of type Integer."
TAntiText {} -> internal "An antiquoted Haskell variable name of type Text."
TRobot {} -> internal "A robot reference."
where
ty = trm ^. sType
literal = pure . typeSignature (prettyText . void $ trm ^. sTerm) ty
internal description = literal $ description <> "\n**These should never show up in surface syntax.**"
constGenSig c =
let ity = inferConst c
in if ty `eq` ity then id else typeSignature (prettyText c) ity
-- | Helper function to explain function application.
--
-- Note that 'Force' is often inserted internally, so
-- if it shows up here we drop it.
explainFunction :: ExplainableType ty => Syntax' ty -> Tree Text
explainFunction s =
case unfoldApps s of
(Syntax' _ (TConst Force) _ :| [innerT]) -> explain innerT
(Syntax' _ (TConst Force) _ :| f : params) -> explainF f params
(f :| params) -> explainF f params
where
explainF f params =
Node
"Function application of:"
[ explain f
, Node
"with parameters:"
(map explain params)
]
explainDefinition :: ExplainableType ty => Bool -> Bool -> LocVar -> ty -> Maybe Polytype -> Text
explainDefinition isDef isRecursive (LV _s var) ty maybeTypeAnnotation =
typeSignature var ty $
T.unwords
[ "A"
, (if isRecursive then "" else "non-") <> "recursive"
, if isDef then "definition" else "let"
, "expression"
, if null maybeTypeAnnotation then "without" else "with"
, "a type annotation on the variable."
]
typeSignature :: ExplainableType ty => Var -> ty -> Text -> Text
typeSignature v typ body = T.unlines ["```", short, "```", body]
where
short = v <> ": " <> prettyType typ