diff --git a/haskell-ide-engine.cabal b/haskell-ide-engine.cabal index 13f9643f5..2d9968975 100644 --- a/haskell-ide-engine.cabal +++ b/haskell-ide-engine.cabal @@ -21,6 +21,7 @@ library hs-source-dirs: src exposed-modules: Haskell.Ide.Engine.Channel Haskell.Ide.Engine.LSP.CodeActions + Haskell.Ide.Engine.LSP.Completions Haskell.Ide.Engine.Plugin.Base Haskell.Ide.Engine.LSP.Reactor Haskell.Ide.Engine.Options diff --git a/src/Haskell/Ide/Engine/LSP/Completions.hs b/src/Haskell/Ide/Engine/LSP/Completions.hs new file mode 100644 index 000000000..b9471b595 --- /dev/null +++ b/src/Haskell/Ide/Engine/LSP/Completions.hs @@ -0,0 +1,513 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE NamedFieldPuns #-} +{-# LANGUAGE OverloadedStrings #-} +module Haskell.Ide.Engine.LSP.Completions + ( WithSnippets(..) + , getCompletions + , resolveCompletion + ) +where + +import Control.Lens.Operators ( (.~) + , (^.) + , (^?) + , (?~) + , (&) + ) +import Control.Lens.Prism ( _Just ) + +import Data.Aeson +import qualified Data.Aeson.Types as J +import Data.Char +import qualified Data.List as List +import qualified Data.Text as T +import qualified Data.Map as Map +import Data.Maybe +import Data.Semigroup (Semigroup(..)) +import Data.Typeable +import GHC.Generics ( Generic ) + +import qualified GhcModCore as GM + ( listVisibleModuleNames ) + +import HscTypes +import qualified DynFlags as GHC +import GHC hiding ( getContext ) +import RdrName +import Name +import TcRnTypes +import Type +import Var + + +import Language.Haskell.Refact.API ( showGhc ) + +import qualified Language.Haskell.LSP.Types as J +import qualified Language.Haskell.LSP.Types.Lens + as J +import qualified Haskell.Ide.Engine.Support.Fuzzy + as Fuzzy +import qualified Haskell.Ide.Engine.Plugin.Hoogle + as Hoogle +import qualified Language.Haskell.LSP.VFS as VFS + +import Haskell.Ide.Engine.Support.HieExtras +import Haskell.Ide.Engine.MonadFunctions +import Haskell.Ide.Engine.MonadTypes +import Haskell.Ide.Engine.PluginUtils +import Haskell.Ide.Engine.Context + +data CompItem = CI + { origName :: Name + , importedFrom :: T.Text + , thingType :: Maybe Type + , label :: T.Text + , isInfix :: Maybe Backtick + } + +data Backtick = Surrounded | LeftSide + +instance Eq CompItem where + ci1 == ci2 = origName ci1 == origName ci2 + +instance Ord CompItem where + compare ci1 ci2 = origName ci1 `compare` origName ci2 + +occNameToComKind :: OccName -> J.CompletionItemKind +occNameToComKind oc + | isVarOcc oc = J.CiFunction + | isTcOcc oc = J.CiClass + | isDataOcc oc = J.CiConstructor + | otherwise = J.CiVariable + +type HoogleQuery = T.Text +data CompItemResolveData + = CompItemResolveData + { nameDetails :: Maybe NameDetails + , hoogleQuery :: HoogleQuery + } deriving (Eq,Generic) + +instance FromJSON CompItemResolveData where + parseJSON = genericParseJSON $ customOptions 0 +instance ToJSON CompItemResolveData where + toJSON = genericToJSON $ customOptions 0 + +resolveCompletion :: J.CompletionItem -> IdeM J.CompletionItem +resolveCompletion origCompl = + case fromJSON <$> origCompl ^. J.xdata of + Just (J.Success compdata) -> do + mdocs <- Hoogle.infoCmd' $ hoogleQuery compdata + let docText = case mdocs of + Right x -> Just x + _ -> Nothing + markup = J.MarkupContent J.MkMarkdown <$> docText + docs = J.CompletionDocMarkup <$> markup + (detail,insert) <- case nameDetails compdata of + Nothing -> pure (Nothing,Nothing) + Just nd -> do + mtyp <- getTypeForNameDetails nd + case mtyp of + Nothing -> pure (Nothing, Nothing) + Just typ -> do + let label = origCompl ^. J.label + insertText = label <> " " <> getArgText typ + det = Just . stripForall $ T.pack (showGhc typ) <> "\n" + pure (det,Just insertText) + return $ origCompl & J.documentation .~ docs + & J.insertText .~ insert + & J.insertTextFormat ?~ J.Snippet + & J.detail .~ (detail <> origCompl ^. J.detail) + Just (J.Error err) -> do + debugm $ "resolveCompletion: Decoding data failed because of: " ++ err + pure origCompl + _ -> pure origCompl + +mkQuery :: T.Text -> T.Text -> HoogleQuery +mkQuery name importedFrom = name <> " module:" <> importedFrom + <> " is:exact" + +mkCompl :: CompItem -> J.CompletionItem +mkCompl CI{origName,importedFrom,thingType,label,isInfix} = + J.CompletionItem label kind (Just $ maybe "" (<>"\n") typeText <> importedFrom) + Nothing Nothing Nothing Nothing Nothing (Just insertText) (Just J.Snippet) + Nothing Nothing Nothing Nothing resolveData + where kind = Just $ occNameToComKind $ occName origName + resolveData = Just $ toJSON $ CompItemResolveData nameDets hoogleQuery + hoogleQuery = mkQuery label importedFrom + insertText = case isInfix of + Nothing -> case getArgText <$> thingType of + Nothing -> label + Just argText -> label <> " " <> argText + Just LeftSide -> label <> "`" + + Just Surrounded -> label + typeText + | Just t <- thingType = Just . stripForall $ T.pack (showGhc t) + | otherwise = Nothing + nameDets = + case (thingType, nameModule_maybe origName) of + (Just _,_) -> Nothing + (Nothing, Nothing) -> Nothing + (Nothing, Just mdl) -> Just (NameDetails mdl (nameOccName origName)) + +stripForall :: T.Text -> T.Text +stripForall t + | T.isPrefixOf "forall" t = + -- We drop 2 to remove the '.' and the space after it + T.drop 2 (T.dropWhile (/= '.') t) + | otherwise = t + +getArgText :: Type -> T.Text +getArgText typ = argText + where + argTypes = getArgs typ + argText :: T.Text + argText = mconcat $ List.intersperse " " $ zipWith snippet [1..] argTypes + snippet :: Int -> Type -> T.Text + snippet i t = T.pack $ "${" <> show i <> ":" <> showGhc t <> "}" + getArgs :: Type -> [Type] + getArgs t + | isPredTy t = [] + | isDictTy t = [] + | isForAllTy t = getArgs $ snd (splitForAllTys t) + | isFunTy t = + let (args, ret) = splitFunTys t + in if isForAllTy ret + then getArgs ret + else filter (not . isDictTy) args + | isPiTy t = getArgs $ snd (splitPiTys t) + | isCoercionTy t = maybe [] (getArgs . snd) (splitCoercionType_maybe t) + | otherwise = [] + +mkModCompl :: T.Text -> J.CompletionItem +mkModCompl label = + J.CompletionItem label (Just J.CiModule) Nothing + Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing Nothing (Just $ toJSON resolveData) + where hoogleQuery = "module:" <> label + resolveData = Just $ CompItemResolveData Nothing hoogleQuery + +mkExtCompl :: T.Text -> J.CompletionItem +mkExtCompl label = + J.CompletionItem label (Just J.CiKeyword) Nothing + Nothing Nothing Nothing Nothing Nothing Nothing Nothing + Nothing Nothing Nothing Nothing Nothing + +mkPragmaCompl :: T.Text -> T.Text -> J.CompletionItem +mkPragmaCompl label insertText = + J.CompletionItem label (Just J.CiKeyword) Nothing + Nothing Nothing Nothing Nothing Nothing (Just insertText) (Just J.Snippet) + Nothing Nothing Nothing Nothing Nothing + +-- Associates a module's qualifier with its members +newtype QualCompls = QualCompls { getQualCompls :: Map.Map T.Text [CompItem] } + +instance Semigroup QualCompls where + (QualCompls a) <> (QualCompls b) = QualCompls $ Map.unionWith (++) a b + +instance Monoid QualCompls where + mempty = QualCompls Map.empty + mappend = (<>) + +data CachedCompletions = CC + { allModNamesAsNS :: [T.Text] + , unqualCompls :: [CompItem] + , qualCompls :: QualCompls + , importableModules :: [T.Text] + } deriving (Typeable) + +-- The supported languages and extensions +languagesAndExts :: [T.Text] +languagesAndExts = map T.pack GHC.supportedLanguagesAndExtensions + +instance ModuleCache CachedCompletions where + cacheDataProducer tm _ = do + let parsedMod = tm_parsed_module tm + curMod = moduleName $ ms_mod $ pm_mod_summary parsedMod + Just (_,limports,_,_) = tm_renamed_source tm + + iDeclToModName :: ImportDecl name -> ModuleName + iDeclToModName = unLoc . ideclName + + showModName :: ModuleName -> T.Text + showModName = T.pack . moduleNameString + + asNamespace :: ImportDecl name -> ModuleName + asNamespace imp = maybe (iDeclToModName imp) GHC.unLoc (ideclAs imp) + -- Full canonical names of imported modules + importDeclerations = map unLoc limports + + -- The list of all importable Modules from all packages + moduleNames = map showModName (GM.listVisibleModuleNames (getDynFlags tm)) + + -- The given namespaces for the imported modules (ie. full name, or alias if used) + allModNamesAsNS = map (showModName . asNamespace) importDeclerations + + typeEnv = tcg_type_env $ fst $ tm_internals_ tm + rdrEnv = tcg_rdr_env $ fst $ tm_internals_ tm + rdrElts = globalRdrEnvElts rdrEnv + + getCompls :: [GlobalRdrElt] -> ([CompItem],QualCompls) + getCompls = foldMap getComplsForOne + + getComplsForOne :: GlobalRdrElt -> ([CompItem],QualCompls) + getComplsForOne (GRE n _ True _) = + case lookupTypeEnv typeEnv n of + Just tt -> case safeTyThingId tt of + Just var -> ([varToCompl var],mempty) + Nothing -> ([toCompItem curMod n],mempty) + Nothing -> ([toCompItem curMod n],mempty) + getComplsForOne (GRE n _ False prov) = + flip foldMap (map is_decl prov) $ \spec -> + let unqual + | is_qual spec = [] + | otherwise = compItem + qual + | is_qual spec = Map.singleton asMod compItem + | otherwise = Map.fromList [(asMod,compItem),(origMod,compItem)] + compItem = [toCompItem (is_mod spec) n] + asMod = showModName (is_as spec) + origMod = showModName (is_mod spec) + in (unqual,QualCompls qual) + + varToCompl :: Var -> CompItem + varToCompl var = CI name (showModName curMod) typ label Nothing + where + typ = Just $ varType var + name = Var.varName var + label = T.pack $ showGhc name + + toCompItem :: ModuleName -> Name -> CompItem + toCompItem mn n = + CI n (showModName mn) Nothing (T.pack $ showGhc n) Nothing + + (unquals,quals) = getCompls rdrElts + return $ CC + { allModNamesAsNS = allModNamesAsNS + , unqualCompls = unquals + , qualCompls = quals + , importableModules = moduleNames + } + +newtype WithSnippets = WithSnippets Bool + +-- | Returns the cached completions for the given module and position. +getCompletions :: Uri -> VFS.PosPrefixInfo -> WithSnippets -> IdeM (IdeResult [J.CompletionItem]) +getCompletions uri prefixInfo (WithSnippets withSnippets) = + pluginGetFile "getCompletions: " uri $ \file -> do + let snippetLens = (^? J.textDocument + . _Just + . J.completion + . _Just + . J.completionItem + . _Just + . J.snippetSupport + . _Just) + supportsSnippets <- fromMaybe False . snippetLens <$> getClientCapabilities + let toggleSnippets x + | withSnippets && supportsSnippets = x + | otherwise = x { J._insertTextFormat = Just J.PlainText + , J._insertText = Nothing + } + + VFS.PosPrefixInfo { VFS.fullLine, VFS.prefixModule, VFS.prefixText } = prefixInfo + debugm $ "got prefix" ++ show (prefixModule, prefixText) + let enteredQual = if T.null prefixModule then "" else prefixModule <> "." + fullPrefix = enteredQual <> prefixText + + ifCachedModuleAndData file (IdeResultOk []) + $ \tm CachedInfo { newPosToOld } CC { allModNamesAsNS, unqualCompls, qualCompls, importableModules } -> + let + -- default to value context if no explicit context + context = fromMaybe ValueContext $ getContext pos (tm_parsed_module tm) + + {- correct the position by moving 'foo :: Int -> String -> ' + ^ + to 'foo :: Int -> String -> ' + ^ + -} + pos = + let newPos = VFS.cursorPos prefixInfo + Position l c = fromMaybe newPos (newPosToOld newPos) + typeStuff = [isSpace, (`elem` (">-." :: String))] + stripTypeStuff = T.dropWhileEnd (\x -> any (\f -> f x) typeStuff) + -- if oldPos points to + -- foo -> bar -> baz + -- ^ + -- Then only take the line up to there, discard '-> bar -> baz' + partialLine = T.take c fullLine + -- drop characters used when writing incomplete type sigs + -- like '-> ' + d = T.length fullLine - T.length (stripTypeStuff partialLine) + in Position l (c - d) + + hasTrailingBacktick = + if T.length fullLine <= trailingBacktickIndex + then False + else (fullLine `T.index` trailingBacktickIndex) == '`' + + trailingBacktickIndex = let Position _ cursorColumn = VFS.cursorPos prefixInfo in cursorColumn + + isUsedAsInfix = if backtickIndex < 0 + then False + else (fullLine `T.index` backtickIndex) == '`' + + backtickIndex = + let Position _ cursorColumn = VFS.cursorPos prefixInfo + prefixLength = T.length prefixText + moduleLength = if prefixModule == "" + then 0 + else T.length prefixModule + 1 {- Because of "." -} + in + cursorColumn - (prefixLength + moduleLength) - 1 {- Points to the first letter of either the module or prefix text -} + + filtModNameCompls = + map mkModCompl + $ mapMaybe (T.stripPrefix enteredQual) + $ Fuzzy.simpleFilter fullPrefix allModNamesAsNS + + filtCompls = Fuzzy.filterBy label prefixText ctxCompls + where + isTypeCompl = isTcOcc . occName . origName + -- completions specific to the current context + ctxCompls' = case context of + TypeContext -> filter isTypeCompl compls + ValueContext -> filter (not . isTypeCompl) compls + -- Add whether the text to insert has backticks + ctxCompls = map (\comp -> comp { isInfix = infixCompls }) ctxCompls' + + infixCompls :: Maybe Backtick + infixCompls = case (isUsedAsInfix, hasTrailingBacktick) of + (True, False) -> Just LeftSide + (True, True) -> Just Surrounded + _ -> Nothing + + compls = if T.null prefixModule + then unqualCompls + else Map.findWithDefault [] prefixModule $ getQualCompls qualCompls + + + mkImportCompl label = (J.detail ?~ label) . mkModCompl $ fromMaybe + "" + (T.stripPrefix enteredQual label) + + filtListWith f list = + [ f label + | label <- Fuzzy.simpleFilter fullPrefix list + , enteredQual `T.isPrefixOf` label + ] + + filtListWithSnippet f list suffix = + [ toggleSnippets (f label (snippet <> suffix)) + | (snippet, label) <- list + , Fuzzy.test fullPrefix label + ] + + filtImportCompls = filtListWith mkImportCompl importableModules + filtPragmaCompls = filtListWithSnippet mkPragmaCompl validPragmas + filtOptsCompls = filtListWith mkExtCompl + + stripLeading :: Char -> String -> String + stripLeading _ [] = [] + stripLeading c (s:ss) + | s == c = ss + | otherwise = s:ss + + result + | "import " `T.isPrefixOf` fullLine + = filtImportCompls + | "{-# language" `T.isPrefixOf` T.toLower fullLine + = filtOptsCompls languagesAndExts + | "{-# options_ghc" `T.isPrefixOf` T.toLower fullLine + = filtOptsCompls (map (T.pack . stripLeading '-') $ GHC.flagsForCompletion False) + | "{-# " `T.isPrefixOf` fullLine + = filtPragmaCompls (pragmaSuffix fullLine) + | otherwise + = filtModNameCompls ++ map (toggleSnippets . mkCompl . stripAutoGenerated) filtCompls + in + return $ IdeResultOk result + where + validPragmas :: [(T.Text, T.Text)] + validPragmas = + [ ("LANGUAGE ${1:extension}" , "LANGUAGE") + , ("OPTIONS_GHC -${1:option}" , "OPTIONS_GHC") + , ("INLINE ${1:function}" , "INLINE") + , ("NOINLINE ${1:function}" , "NOINLINE") + , ("INLINABLE ${1:function}" , "INLINABLE") + , ("WARNING ${1:message}" , "WARNING") + , ("DEPRECATED ${1:message}" , "DEPRECATED") + , ("ANN ${1:annotation}" , "ANN") + , ("RULES" , "RULES") + , ("SPECIALIZE ${1:function}" , "SPECIALIZE") + , ("SPECIALIZE INLINE ${1:function}", "SPECIALIZE INLINE") + ] + + pragmaSuffix :: T.Text -> T.Text + pragmaSuffix fullLine + | "}" `T.isSuffixOf` fullLine = mempty + | otherwise = " #-}" + + +-- --------------------------------------------------------------------- + +-- | Under certain circumstance GHC generates some extra stuff that we +-- don't want in the autocompleted symbols +stripAutoGenerated :: CompItem -> CompItem +stripAutoGenerated ci = + ci {label = stripPrefix (label ci)} + {- When e.g. DuplicateRecordFields is enabled, compiler generates + names like "$sel:accessor:One" and "$sel:accessor:Two" to disambiguate record selectors + https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedRecordFields/DuplicateRecordFields#Implementation + -} + +-- TODO: Turn this into an alex lexer that discards prefixes as if they were whitespace. + +stripPrefix :: T.Text -> T.Text +stripPrefix name = T.takeWhile (/=':') $ go prefixes + where + go [] = name + go (p:ps) + | T.isPrefixOf p name = T.drop (T.length p) name + | otherwise = go ps + +-- | Prefixes that can occur in a GHC OccName +prefixes :: [T.Text] +prefixes = + [ + -- long ones + "$con2tag_" + , "$tag2con_" + , "$maxtag_" + + -- four chars + , "$sel:" + , "$tc'" + + -- three chars + , "$dm" + , "$co" + , "$tc" + , "$cp" + , "$fx" + + -- two chars + , "$W" + , "$w" + , "$m" + , "$b" + , "$c" + , "$d" + , "$i" + , "$s" + , "$f" + , "$r" + , "C:" + , "N:" + , "D:" + , "$p" + , "$L" + , "$f" + , "$t" + , "$c" + , "$m" + ] diff --git a/src/Haskell/Ide/Engine/Support/HieExtras.hs b/src/Haskell/Ide/Engine/Support/HieExtras.hs index 39f5fa3bf..7b32d1241 100644 --- a/src/Haskell/Ide/Engine/Support/HieExtras.hs +++ b/src/Haskell/Ide/Engine/Support/HieExtras.hs @@ -6,10 +6,9 @@ {-# LANGUAGE LambdaCase #-} module Haskell.Ide.Engine.Support.HieExtras ( getDynFlags - , WithSnippets(..) - , getCompletions - , resolveCompletion , getTypeForName + , getTypeForNameDetails + , NameDetails(..) , getSymbolsAtPoint , getReferencesInDoc , getModule @@ -28,17 +27,14 @@ module Haskell.Ide.Engine.Support.HieExtras import Data.Semigroup (Semigroup(..)) import ConLike -import Control.Lens.Operators ( (.~), (^.), (^?), (?~), (&) ) -import Control.Lens.Prism ( _Just ) +import Control.Lens.Operators ( (&) ) import Control.Lens.Setter ((%~)) import Control.Lens.Traversal (traverseOf) import Control.Monad.Reader import Control.Monad.Except import Data.Aeson import qualified Data.Aeson.Types as J -import Data.Char import Data.IORef -import qualified Data.List as List import qualified Data.Map as Map import Data.Maybe import qualified Data.Text as T @@ -51,25 +47,19 @@ import FastString import Finder import GHC hiding (getContext) import GHC.Generics (Generic) -import TcRnTypes -import RdrName import qualified GhcMod as GM (splits',SplitResult(..)) -import qualified GhcModCore as GM (GhcModError(..), listVisibleModuleNames, withMappedFile ) +import qualified GhcModCore as GM (GhcModError(..), withMappedFile ) import Haskell.Ide.Engine.ArtifactMap import Haskell.Ide.Engine.Config -import Haskell.Ide.Engine.Context import Haskell.Ide.Engine.MonadFunctions import Haskell.Ide.Engine.MonadTypes import Haskell.Ide.Engine.PluginUtils -import qualified Haskell.Ide.Engine.Support.Fuzzy as Fuzzy -import qualified Haskell.Ide.Engine.Plugin.Hoogle as Hoogle import HscTypes import qualified Language.Haskell.LSP.Types as J import qualified Language.Haskell.LSP.Types.Lens as J import qualified Language.Haskell.LSP.VFS as VFS -import Language.Haskell.Refact.API (showGhc) import Language.Haskell.Refact.Utils.MonadFunctions import Name import NameCache @@ -102,38 +92,6 @@ instance ModuleCache NameMapData where where nm = initRdrNameMap tm inm = invert nm --- --------------------------------------------------------------------- - -data CompItem = CI - { origName :: Name - , importedFrom :: T.Text - , thingType :: Maybe Type - , label :: T.Text - , isInfix :: Maybe Backtick - } - -data Backtick = Surrounded | LeftSide - -instance Eq CompItem where - ci1 == ci2 = origName ci1 == origName ci2 - -instance Ord CompItem where - compare ci1 ci2 = origName ci1 `compare` origName ci2 - -occNameToComKind :: OccName -> J.CompletionItemKind -occNameToComKind oc - | isVarOcc oc = J.CiFunction - | isTcOcc oc = J.CiClass - | isDataOcc oc = J.CiConstructor - | otherwise = J.CiVariable - -type HoogleQuery = T.Text -data CompItemResolveData - = CompItemResolveData - { nameDetails :: Maybe NameDetails - , hoogleQuery :: HoogleQuery - } deriving (Eq,Generic) - data NameDetails = NameDetails Module OccName deriving (Eq) @@ -171,372 +129,11 @@ instance ToJSON NameDetails where ns = occNameSpace occ occs = occNameString occ -instance FromJSON CompItemResolveData where - parseJSON = genericParseJSON $ customOptions 0 -instance ToJSON CompItemResolveData where - toJSON = genericToJSON $ customOptions 0 - -resolveCompletion :: J.CompletionItem -> IdeM J.CompletionItem -resolveCompletion origCompl = - case fromJSON <$> origCompl ^. J.xdata of - Just (J.Success compdata) -> do - mdocs <- Hoogle.infoCmd' $ hoogleQuery compdata - let docText = case mdocs of - Right x -> Just x - _ -> Nothing - markup = J.MarkupContent J.MkMarkdown <$> docText - docs = J.CompletionDocMarkup <$> markup - (detail,insert) <- case nameDetails compdata of - Nothing -> pure (Nothing,Nothing) - Just nd -> do - mtyp <- getTypeForNameDetails nd - case mtyp of - Nothing -> pure (Nothing, Nothing) - Just typ -> do - let label = origCompl ^. J.label - insertText = label <> " " <> getArgText typ - det = Just . stripForall $ T.pack (showGhc typ) <> "\n" - pure (det,Just insertText) - return $ origCompl & J.documentation .~ docs - & J.insertText .~ insert - & J.insertTextFormat ?~ J.Snippet - & J.detail .~ (detail <> origCompl ^. J.detail) - Just (J.Error err) -> do - debugm $ "resolveCompletion: Decoding data failed because of: " ++ err - pure origCompl - _ -> pure origCompl - - -mkQuery :: T.Text -> T.Text -> HoogleQuery -mkQuery name importedFrom = name <> " module:" <> importedFrom - <> " is:exact" - -mkCompl :: CompItem -> J.CompletionItem -mkCompl CI{origName,importedFrom,thingType,label,isInfix} = - J.CompletionItem label kind (Just $ maybe "" (<>"\n") typeText <> importedFrom) - Nothing Nothing Nothing Nothing Nothing (Just insertText) (Just J.Snippet) - Nothing Nothing Nothing Nothing resolveData - where kind = Just $ occNameToComKind $ occName origName - resolveData = Just $ toJSON $ CompItemResolveData nameDets hoogleQuery - hoogleQuery = mkQuery label importedFrom - insertText = case isInfix of - Nothing -> case getArgText <$> thingType of - Nothing -> label - Just argText -> label <> " " <> argText - Just LeftSide -> label <> "`" - - Just Surrounded -> label - typeText - | Just t <- thingType = Just . stripForall $ T.pack (showGhc t) - | otherwise = Nothing - nameDets = - case (thingType, nameModule_maybe origName) of - (Just _,_) -> Nothing - (Nothing, Nothing) -> Nothing - (Nothing, Just mdl) -> Just (NameDetails mdl (nameOccName origName)) - -stripForall :: T.Text -> T.Text -stripForall t - | T.isPrefixOf "forall" t = - -- We drop 2 to remove the '.' and the space after it - T.drop 2 (T.dropWhile (/= '.') t) - | otherwise = t - -getArgText :: Type -> T.Text -getArgText typ = argText - where - argTypes = getArgs typ - argText :: T.Text - argText = mconcat $ List.intersperse " " $ zipWith snippet [1..] argTypes - snippet :: Int -> Type -> T.Text - snippet i t = T.pack $ "${" <> show i <> ":" <> showGhc t <> "}" - getArgs :: Type -> [Type] - getArgs t - | isPredTy t = [] - | isDictTy t = [] - | isForAllTy t = getArgs $ snd (splitForAllTys t) - | isFunTy t = - let (args, ret) = splitFunTys t - in if isForAllTy ret - then getArgs ret - else filter (not . isDictTy) args - | isPiTy t = getArgs $ snd (splitPiTys t) - | isCoercionTy t = maybe [] (getArgs . snd) (splitCoercionType_maybe t) - | otherwise = [] - -mkModCompl :: T.Text -> J.CompletionItem -mkModCompl label = - J.CompletionItem label (Just J.CiModule) Nothing - Nothing Nothing Nothing Nothing Nothing Nothing Nothing - Nothing Nothing Nothing Nothing (Just $ toJSON resolveData) - where hoogleQuery = "module:" <> label - resolveData = Just $ CompItemResolveData Nothing hoogleQuery - -mkExtCompl :: T.Text -> J.CompletionItem -mkExtCompl label = - J.CompletionItem label (Just J.CiKeyword) Nothing - Nothing Nothing Nothing Nothing Nothing Nothing Nothing - Nothing Nothing Nothing Nothing Nothing - -mkPragmaCompl :: T.Text -> T.Text -> J.CompletionItem -mkPragmaCompl label insertText = - J.CompletionItem label (Just J.CiKeyword) Nothing - Nothing Nothing Nothing Nothing Nothing (Just insertText) (Just J.Snippet) - Nothing Nothing Nothing Nothing Nothing - safeTyThingId :: TyThing -> Maybe Id safeTyThingId (AnId i) = Just i safeTyThingId (AConLike (RealDataCon dc)) = Just $ dataConWrapId dc safeTyThingId _ = Nothing --- Associates a module's qualifier with its members -newtype QualCompls = QualCompls { getQualCompls :: Map.Map T.Text [CompItem] } - -instance Semigroup QualCompls where - (QualCompls a) <> (QualCompls b) = QualCompls $ Map.unionWith (++) a b - -instance Monoid QualCompls where - mempty = QualCompls Map.empty - mappend = (<>) - -data CachedCompletions = CC - { allModNamesAsNS :: [T.Text] - , unqualCompls :: [CompItem] - , qualCompls :: QualCompls - , importableModules :: [T.Text] - } deriving (Typeable) - --- The supported languages and extensions -languagesAndExts :: [T.Text] -languagesAndExts = map T.pack GHC.supportedLanguagesAndExtensions - -instance ModuleCache CachedCompletions where - cacheDataProducer tm _ = do - let parsedMod = tm_parsed_module tm - curMod = moduleName $ ms_mod $ pm_mod_summary parsedMod - Just (_,limports,_,_) = tm_renamed_source tm - - iDeclToModName :: ImportDecl name -> ModuleName - iDeclToModName = unLoc . ideclName - - showModName :: ModuleName -> T.Text - showModName = T.pack . moduleNameString - - asNamespace :: ImportDecl name -> ModuleName - asNamespace imp = maybe (iDeclToModName imp) GHC.unLoc (ideclAs imp) - -- Full canonical names of imported modules - importDeclerations = map unLoc limports - - -- The list of all importable Modules from all packages - moduleNames = map showModName (GM.listVisibleModuleNames (getDynFlags tm)) - - -- The given namespaces for the imported modules (ie. full name, or alias if used) - allModNamesAsNS = map (showModName . asNamespace) importDeclerations - - typeEnv = tcg_type_env $ fst $ tm_internals_ tm - rdrEnv = tcg_rdr_env $ fst $ tm_internals_ tm - rdrElts = globalRdrEnvElts rdrEnv - - getCompls :: [GlobalRdrElt] -> ([CompItem],QualCompls) - getCompls = foldMap getComplsForOne - - getComplsForOne :: GlobalRdrElt -> ([CompItem],QualCompls) - getComplsForOne (GRE n _ True _) = - case lookupTypeEnv typeEnv n of - Just tt -> case safeTyThingId tt of - Just var -> ([varToCompl var],mempty) - Nothing -> ([toCompItem curMod n],mempty) - Nothing -> ([toCompItem curMod n],mempty) - getComplsForOne (GRE n _ False prov) = - flip foldMap (map is_decl prov) $ \spec -> - let unqual - | is_qual spec = [] - | otherwise = compItem - qual - | is_qual spec = Map.singleton asMod compItem - | otherwise = Map.fromList [(asMod,compItem),(origMod,compItem)] - compItem = [toCompItem (is_mod spec) n] - asMod = showModName (is_as spec) - origMod = showModName (is_mod spec) - in (unqual,QualCompls qual) - - varToCompl :: Var -> CompItem - varToCompl var = CI name (showModName curMod) typ label Nothing - where - typ = Just $ varType var - name = Var.varName var - label = T.pack $ showGhc name - - toCompItem :: ModuleName -> Name -> CompItem - toCompItem mn n = - CI n (showModName mn) Nothing (T.pack $ showGhc n) Nothing - - (unquals,quals) = getCompls rdrElts - return $ CC - { allModNamesAsNS = allModNamesAsNS - , unqualCompls = unquals - , qualCompls = quals - , importableModules = moduleNames - } - -newtype WithSnippets = WithSnippets Bool - --- | Returns the cached completions for the given module and position. -getCompletions :: Uri -> VFS.PosPrefixInfo -> WithSnippets -> IdeM (IdeResult [J.CompletionItem]) -getCompletions uri prefixInfo (WithSnippets withSnippets) = - pluginGetFile "getCompletions: " uri $ \file -> do - let snippetLens = (^? J.textDocument - . _Just - . J.completion - . _Just - . J.completionItem - . _Just - . J.snippetSupport - . _Just) - supportsSnippets <- fromMaybe False . snippetLens <$> getClientCapabilities - let toggleSnippets x - | withSnippets && supportsSnippets = x - | otherwise = x { J._insertTextFormat = Just J.PlainText - , J._insertText = Nothing - } - - VFS.PosPrefixInfo { VFS.fullLine, VFS.prefixModule, VFS.prefixText } = prefixInfo - debugm $ "got prefix" ++ show (prefixModule, prefixText) - let enteredQual = if T.null prefixModule then "" else prefixModule <> "." - fullPrefix = enteredQual <> prefixText - - ifCachedModuleAndData file (IdeResultOk []) - $ \tm CachedInfo { newPosToOld } CC { allModNamesAsNS, unqualCompls, qualCompls, importableModules } -> - let - -- default to value context if no explicit context - context = fromMaybe ValueContext $ getContext pos (tm_parsed_module tm) - - {- correct the position by moving 'foo :: Int -> String -> ' - ^ - to 'foo :: Int -> String -> ' - ^ - -} - pos = - let newPos = VFS.cursorPos prefixInfo - Position l c = fromMaybe newPos (newPosToOld newPos) - typeStuff = [isSpace, (`elem` (">-." :: String))] - stripTypeStuff = T.dropWhileEnd (\x -> any (\f -> f x) typeStuff) - -- if oldPos points to - -- foo -> bar -> baz - -- ^ - -- Then only take the line up to there, discard '-> bar -> baz' - partialLine = T.take c fullLine - -- drop characters used when writing incomplete type sigs - -- like '-> ' - d = T.length fullLine - T.length (stripTypeStuff partialLine) - in Position l (c - d) - - hasTrailingBacktick = - if T.length fullLine <= trailingBacktickIndex - then False - else (fullLine `T.index` trailingBacktickIndex) == '`' - - trailingBacktickIndex = let Position _ cursorColumn = VFS.cursorPos prefixInfo in cursorColumn - - isUsedAsInfix = if backtickIndex < 0 - then False - else (fullLine `T.index` backtickIndex) == '`' - - backtickIndex = - let Position _ cursorColumn = VFS.cursorPos prefixInfo - prefixLength = T.length prefixText - moduleLength = if prefixModule == "" - then 0 - else T.length prefixModule + 1 {- Because of "." -} - in - cursorColumn - (prefixLength + moduleLength) - 1 {- Points to the first letter of either the module or prefix text -} - - filtModNameCompls = - map mkModCompl - $ mapMaybe (T.stripPrefix enteredQual) - $ Fuzzy.simpleFilter fullPrefix allModNamesAsNS - - filtCompls = Fuzzy.filterBy label prefixText ctxCompls - where - isTypeCompl = isTcOcc . occName . origName - -- completions specific to the current context - ctxCompls' = case context of - TypeContext -> filter isTypeCompl compls - ValueContext -> filter (not . isTypeCompl) compls - -- Add whether the text to insert has backticks - ctxCompls = map (\comp -> comp { isInfix = infixCompls }) ctxCompls' - - infixCompls :: Maybe Backtick - infixCompls = case (isUsedAsInfix, hasTrailingBacktick) of - (True, False) -> Just LeftSide - (True, True) -> Just Surrounded - _ -> Nothing - - compls = if T.null prefixModule - then unqualCompls - else Map.findWithDefault [] prefixModule $ getQualCompls qualCompls - - - mkImportCompl label = (J.detail ?~ label) . mkModCompl $ fromMaybe - "" - (T.stripPrefix enteredQual label) - - filtListWith f list = - [ f label - | label <- Fuzzy.simpleFilter fullPrefix list - , enteredQual `T.isPrefixOf` label - ] - - filtListWithSnippet f list suffix = - [ toggleSnippets (f label (snippet <> suffix)) - | (snippet, label) <- list - , Fuzzy.test fullPrefix label - ] - - filtImportCompls = filtListWith mkImportCompl importableModules - filtPragmaCompls = filtListWithSnippet mkPragmaCompl validPragmas - filtOptsCompls = filtListWith mkExtCompl - - stripLeading :: Char -> String -> String - stripLeading _ [] = [] - stripLeading c (s:ss) - | s == c = ss - | otherwise = s:ss - - result - | "import " `T.isPrefixOf` fullLine - = filtImportCompls - | "{-# language" `T.isPrefixOf` T.toLower fullLine - = filtOptsCompls languagesAndExts - | "{-# options_ghc" `T.isPrefixOf` T.toLower fullLine - = filtOptsCompls (map (T.pack . stripLeading '-') $ GHC.flagsForCompletion False) - | "{-# " `T.isPrefixOf` fullLine - = filtPragmaCompls (pragmaSuffix fullLine) - | otherwise - = filtModNameCompls ++ map (toggleSnippets . mkCompl . stripAutoGenerated) filtCompls - in - return $ IdeResultOk result - where - validPragmas :: [(T.Text, T.Text)] - validPragmas = - [ ("LANGUAGE ${1:extension}" , "LANGUAGE") - , ("OPTIONS_GHC -${1:option}" , "OPTIONS_GHC") - , ("INLINE ${1:function}" , "INLINE") - , ("NOINLINE ${1:function}" , "NOINLINE") - , ("INLINABLE ${1:function}" , "INLINABLE") - , ("WARNING ${1:message}" , "WARNING") - , ("DEPRECATED ${1:message}" , "DEPRECATED") - , ("ANN ${1:annotation}" , "ANN") - , ("RULES" , "RULES") - , ("SPECIALIZE ${1:function}" , "SPECIALIZE") - , ("SPECIALIZE INLINE ${1:function}", "SPECIALIZE INLINE") - ] - - pragmaSuffix :: T.Text -> T.Text - pragmaSuffix fullLine - | "}" `T.isSuffixOf` fullLine = mempty - | otherwise = " #-}" - -- --------------------------------------------------------------------- getTypeForName :: Name -> IdeM (Maybe Type) @@ -827,72 +424,6 @@ splitCaseCmd' uri newPos = dropLines = drop l textLines dropCharacters = T.drop c (T.unlines dropLines) --- --------------------------------------------------------------------- - --- | Under certain circumstance GHC generates some extra stuff that we --- don't want in the autocompleted symbols -stripAutoGenerated :: CompItem -> CompItem -stripAutoGenerated ci = - ci {label = stripPrefix (label ci)} - {- When e.g. DuplicateRecordFields is enabled, compiler generates - names like "$sel:accessor:One" and "$sel:accessor:Two" to disambiguate record selectors - https://ghc.haskell.org/trac/ghc/wiki/Records/OverloadedRecordFields/DuplicateRecordFields#Implementation - -} - --- TODO: Turn this into an alex lexer that discards prefixes as if they were whitespace. - -stripPrefix :: T.Text -> T.Text -stripPrefix name = T.takeWhile (/=':') $ go prefixes - where - go [] = name - go (p:ps) - | T.isPrefixOf p name = T.drop (T.length p) name - | otherwise = go ps - --- | Prefixes that can occur in a GHC OccName -prefixes :: [T.Text] -prefixes = - [ - -- long ones - "$con2tag_" - , "$tag2con_" - , "$maxtag_" - - -- four chars - , "$sel:" - , "$tc'" - - -- three chars - , "$dm" - , "$co" - , "$tc" - , "$cp" - , "$fx" - - -- two chars - , "$W" - , "$w" - , "$m" - , "$b" - , "$c" - , "$d" - , "$i" - , "$s" - , "$f" - , "$r" - , "C:" - , "N:" - , "D:" - , "$p" - , "$L" - , "$f" - , "$t" - , "$c" - , "$m" - ] - --- --------------------------------------------------------------------- - getFormattingPlugin :: Config -> IdePlugins -> Maybe (PluginDescriptor, FormattingProvider) getFormattingPlugin config plugins = do let providerName = formattingProvider config diff --git a/src/Haskell/Ide/Engine/Transport/LspStdio.hs b/src/Haskell/Ide/Engine/Transport/LspStdio.hs index 3ec5bd81e..5b57a8470 100644 --- a/src/Haskell/Ide/Engine/Transport/LspStdio.hs +++ b/src/Haskell/Ide/Engine/Transport/LspStdio.hs @@ -41,6 +41,7 @@ import qualified GhcModCore as GM ( loadMappedFileSource, getMMapp import Haskell.Ide.Engine.Config import qualified Haskell.Ide.Engine.Ghc as HIE import Haskell.Ide.Engine.LSP.CodeActions +import qualified Haskell.Ide.Engine.LSP.Completions as Completions import Haskell.Ide.Engine.LSP.Reactor import Haskell.Ide.Engine.MonadFunctions import Haskell.Ide.Engine.MonadTypes @@ -640,9 +641,9 @@ reactor inp diagIn = do case mprefix of Nothing -> callback [] Just prefix -> do - snippets <- Hie.WithSnippets <$> configVal completionSnippetsOn + snippets <- Completions.WithSnippets <$> configVal completionSnippetsOn let hreq = IReq tn (req ^. J.id) callback - $ lift $ Hie.getCompletions doc prefix snippets + $ lift $ Completions.getCompletions doc prefix snippets makeRequest hreq ReqCompletionItemResolve req -> do @@ -652,7 +653,7 @@ reactor inp diagIn = do let rspMsg = Core.makeResponseMessage req $ res reactorSend $ RspCompletionItemResolve rspMsg hreq = IReq tn (req ^. J.id) callback $ runIdeResultT $ do - lift $ lift $ Hie.resolveCompletion origCompl + lift $ lift $ Completions.resolveCompletion origCompl makeRequest hreq -- -------------------------------