inferno-lsp-0.1.0: src/Inferno/LSP/Completion.hs
{-# LANGUAGE AllowAmbiguousTypes #-}
{-# LANGUAGE NamedFieldPuns #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeApplications #-}
module Inferno.LSP.Completion where
import Control.Monad.Except (MonadError)
import Data.List (delete, nub)
import qualified Data.List.NonEmpty as NonEmpty
import qualified Data.Map as Map
import qualified Data.Maybe as Maybe
import Data.Text (Text)
import qualified Data.Text as Text
import Inferno.Eval.Error (EvalError)
import Inferno.LSP.ParseInfer (getTypeMetadataText, mkPrettyTy)
import Inferno.Module.Prelude (ModuleMap)
import Inferno.Types.Syntax (Ident (..), ModuleName (..), rws)
import Inferno.Types.Type (Namespace (..), TCScheme, TypeMetadata (..))
import Inferno.Utils.Prettyprinter (renderDoc, renderPretty)
import Language.LSP.Types
( CompletionDoc (..),
CompletionItem (..),
CompletionItemKind (..),
MarkupContent (MarkupContent),
MarkupKind (..),
Position (..),
)
import Prettyprinter (Pretty)
-- | Given the cursor position construct the corresponding 'completion query'
-- consisting of the leadup, i.e. text leading up to the word prefix that is to
-- be completed, as well as the prefix that is to be completed.
completionQueryAt :: Text -> Position -> (Text, Text)
completionQueryAt text pos = (completionLeadup, completionPrefix)
where
off = positionToOffset text pos
text' = Text.take off text
breakEnd :: (Char -> Bool) -> Text -> (Text, Text)
breakEnd p =
(\(l, r) -> (Text.reverse l, Text.reverse r)) . Text.break p . Text.reverse
(completionPrefix, completionLeadup) =
breakEnd (`elem` (" \t\n[(,=+*&|}?>" :: String)) text'
positionToOffset :: Text -> Position -> Int
positionToOffset txt (Position line col) =
if fromIntegral line < length ls
then Text.length . unlines' $ take (fromIntegral line) ls ++ [Text.take (fromIntegral col) (ls !! fromIntegral line)]
else Text.length txt -- position lies outside txt
where
ls = NonEmpty.toList (lines' txt)
lines' :: Text -> NonEmpty.NonEmpty Text
lines' t =
case Text.split (== '\n') t of
[] -> "" NonEmpty.:| [] -- this case never occurs!
l : ls -> l NonEmpty.:| ls
unlines' :: [Text] -> Text
unlines' = Text.intercalate "\n"
findInPrelude :: forall c. (Pretty c, Eq c) => Map.Map (Maybe ModuleName, Namespace) (TypeMetadata TCScheme) -> Text -> [((Maybe ModuleName, Namespace), TypeMetadata TCScheme)]
findInPrelude preludeNameToTypeMap prefix =
let prefixIsEnum = "#" `Text.isPrefixOf` prefix
-- 'preludeNameToTypeMap' stores the enum's ident without '#'.
-- When comparing the prefix, we have to drop the '#'.
lcPrefix' = (if prefixIsEnum then Text.drop 1 else id) $ Text.toLower prefix
lcPrefix = (if "." `Text.isSuffixOf` lcPrefix' then Text.dropEnd 1 else id) lcPrefix'
in Map.toList $
Map.filterWithKey
( \(mModule, ns) _ ->
case mModule of
_ | prefixIsEnum -> filterEnum ns lcPrefix
Just m ->
lcPrefix
`Text.isPrefixOf` Text.toLower (Text.toLower $ unModuleName m)
|| filterNs ns lcPrefix
|| filterNsWithModuleName m ns lcPrefix
-- In the case of enum, we should only show enums
Nothing -> filterNs ns lcPrefix
)
preludeNameToTypeMap
where
filterEnum ns lcPrefix = case ns of
EnumNamespace (Ident i) -> lcPrefix `Text.isPrefixOf` Text.toLower i
_ -> False
filterNs ns lcPrefix = case ns of
FunNamespace (Ident i) -> lcPrefix `Text.isPrefixOf` Text.toLower i
OpNamespace (Ident i) -> lcPrefix `Text.isPrefixOf` Text.toLower i
EnumNamespace (Ident i) -> lcPrefix `Text.isPrefixOf` Text.toLower i
ModuleNamespace (ModuleName n) -> lcPrefix `Text.isPrefixOf` Text.toLower n
TypeNamespace _ -> False
filterNsWithModuleName (ModuleName mn) ns lcPrefix = do
let mn' = Text.append (Text.toLower mn) "."
case ns of
FunNamespace (Ident i) -> lcPrefix `Text.isPrefixOf` Text.append mn' (Text.toLower i)
OpNamespace (Ident i) -> lcPrefix `Text.isPrefixOf` Text.append mn' (Text.toLower i)
EnumNamespace (Ident i) -> lcPrefix `Text.isPrefixOf` Text.append mn' (Text.toLower i)
ModuleNamespace (ModuleName n) -> lcPrefix `Text.isPrefixOf` Text.append mn' (Text.toLower n)
TypeNamespace _ -> False
mkCompletionItem :: forall m c. (MonadError EvalError m, Pretty c, Eq c) => ModuleMap m c -> Text -> (Maybe ModuleName, Namespace) -> TypeMetadata TCScheme -> CompletionItem
mkCompletionItem prelude txt (modNm, ns) tm@TypeMetadata {ty} =
CompletionItem
{ _label = insertModNm $ renderPretty ns,
_kind = case ns of
FunNamespace _ -> Just CiFunction
OpNamespace _ -> Just CiFunction
EnumNamespace _ -> Just CiEnum
ModuleNamespace _ -> Just CiModule
TypeNamespace _ -> Nothing,
_tags = Nothing,
_detail = Just $ renderDoc $ mkPrettyTy prelude mempty ty,
_documentation = CompletionDocMarkup . MarkupContent MkMarkdown <$> getTypeMetadataText tm,
_deprecated = Nothing,
_preselect = Nothing,
_sortText = Nothing,
_filterText =
let ftxt = insertModNm $ renderPretty ns
ftxt' = (if "#" `Text.isPrefixOf` ftxt then Text.drop 1 else id) ftxt
in Just ftxt',
_insertText = case ns of
EnumNamespace (Ident i) -> if "#" `Text.isPrefixOf` txt then Just i else Nothing
_ -> Nothing,
_insertTextMode = Nothing,
_insertTextFormat = Nothing,
_textEdit = Nothing,
_additionalTextEdits = Nothing,
_commitCharacters = Nothing,
_command = Nothing,
_xdata = Nothing
}
where
insertModNm txt' = case modNm of
Nothing -> txt'
Just (ModuleName n) -> do
let moduleName = n <> "."
if moduleName `Text.isPrefixOf` txt
then txt'
else moduleName <> txt'
-- | Create completion for user provided identifier e.g. input0, etc
identifierCompletionItems :: [Text] -> Text -> [CompletionItem]
identifierCompletionItems idents prefix
| "." `Text.isSuffixOf` prefix = [] -- For case like "Module.", returned empty because identifier has no namespace/module prefix
| otherwise = fmap makeIdentifierCompletion $ filter (\identifier -> prefix `Text.isPrefixOf` identifier) idents
where
makeIdentifierCompletion identifier =
CompletionItem
{ _label = identifier,
_kind = Just CiVariable,
_tags = Nothing,
_detail = Nothing,
_documentation = Nothing,
_deprecated = Nothing,
_preselect = Nothing,
_sortText = Nothing,
_filterText = Just identifier,
_insertText = Nothing,
_insertTextMode = Nothing,
_insertTextFormat = Nothing,
_textEdit = Nothing,
_additionalTextEdits = Nothing,
_commitCharacters = Nothing,
_command = Nothing,
_xdata = Nothing
}
rwsCompletionItems :: Text -> [CompletionItem]
rwsCompletionItems prefix
| "." `Text.isSuffixOf` prefix = [] -- For case like "Module.", returned empty because identifier has no namespace/module prefix
| otherwise = map mkRwsCompletionItem $ filter (\rw -> prefix `Text.isPrefixOf` rw) filteredRws
where
-- `None` and `Some` are already included elsewhere
filteredRws :: [Text]
filteredRws = delete "Some" $ delete "None" rws
-- Create a CompletionItem for each reserved word
mkRwsCompletionItem :: Text -> CompletionItem
mkRwsCompletionItem rw =
CompletionItem
{ _label = rw,
_kind = Just CiKeyword,
_tags = Nothing,
_detail = Nothing,
_documentation = Nothing,
_deprecated = Nothing,
_preselect = Nothing,
_sortText = Nothing,
_filterText = Nothing,
_insertText = Nothing,
_insertTextMode = Nothing,
_insertTextFormat = Nothing,
_textEdit = Nothing,
_additionalTextEdits = Nothing,
_commitCharacters = Nothing,
_command = Nothing,
_xdata = Nothing
}
moduleNameCompletionItems :: forall c. (Pretty c, Eq c) => Map.Map (Maybe ModuleName, Namespace) (TypeMetadata TCScheme) -> [CompletionItem]
moduleNameCompletionItems preludeNameToTypeMap = fmap mkModuleCompletionItem modules
where
modules = nub . fmap unModuleName . Maybe.catMaybes . fmap fst $ Map.keys preludeNameToTypeMap
mkModuleCompletionItem m =
CompletionItem
{ _label = m,
_kind = Just CiModule,
_tags = Nothing,
_detail = Nothing,
_documentation = Nothing,
_deprecated = Nothing,
_preselect = Nothing,
_sortText = Nothing,
_filterText = Just m,
_insertText = Just m,
_insertTextMode = Nothing,
_insertTextFormat = Nothing,
_textEdit = Nothing,
_additionalTextEdits = Nothing,
_commitCharacters = Nothing,
_command = Nothing,
_xdata = Nothing
}
filterModuleNameCompletionItems :: forall c. (Pretty c, Eq c) => Map.Map (Maybe ModuleName, Namespace) (TypeMetadata TCScheme) -> Text -> [CompletionItem]
filterModuleNameCompletionItems preludeNameToTypeMap prefix = filter (\item -> prefix `Text.isPrefixOf` _label item) (moduleNameCompletionItems @c preludeNameToTypeMap)