diff --git a/ghcide-test/exe/CompletionTests.hs b/ghcide-test/exe/CompletionTests.hs index 8c44173bd6..c21162834a 100644 --- a/ghcide-test/exe/CompletionTests.hs +++ b/ghcide-test/exe/CompletionTests.hs @@ -8,7 +8,7 @@ module CompletionTests (tests) where import Config -import Control.Lens ((^.)) +import Control.Lens (view, (^.)) import qualified Control.Lens as Lens import Control.Monad import Control.Monad.IO.Class (liftIO) @@ -43,6 +43,7 @@ tests , testGroup "package" packageCompletionTests , testGroup "project" projectCompletionTests , testGroup "other" otherCompletionTests + , testGroup "context" contextCompletionTests , testGroup "doc" completionDocTests ] @@ -52,12 +53,12 @@ testSessionEmpty name = testWithDummyPlugin name (mkIdeTestFs [FS.directCradle [ testSessionEmptyWithCradle :: TestName -> T.Text -> Session () -> TestTree testSessionEmptyWithCradle name cradle = testWithDummyPlugin name (mkIdeTestFs [file "hie.yaml" (text cradle)]) -testSessionSingleFile :: TestName -> FilePath -> T.Text -> Session () -> TestTree -testSessionSingleFile testName fp txt session = - testWithDummyPlugin testName (mkIdeTestFs [FS.directCradle [T.pack fp] , file fp (text txt)]) session +testSessionSingleFile :: TestName -> FilePath -> [T.Text] -> Session () -> TestTree +testSessionSingleFile testName fp txts session = + testWithDummyPlugin testName (mkIdeTestFs [FS.directCradle [T.pack fp] , file fp (text (T.unlines txts))]) session completionTest :: HasCallStack => String -> [T.Text] -> Position -> [(T.Text, CompletionItemKind, T.Text, Bool, Bool, Maybe [TextEdit])] -> TestTree -completionTest name src pos expected = testSessionSingleFile name "A.hs" (T.unlines src) $ do +completionTest name src pos expected = testSessionSingleFile name "A.hs" src $ do docId <- openDoc "A.hs" "haskell" _ <- waitForDiagnostics @@ -199,18 +200,17 @@ localCompletionTests = [ ,("abcde", CompletionItemKind_Function, "abcde", True, False, Nothing) ], testSessionEmpty "incomplete entries" $ do - let src a = "data Data = " <> a - doc <- createDoc "A.hs" "haskell" $ src "AAA" + let src a = a <> " = aaa" + doc <- createDoc "A.hs" "haskell" $ src "aaa" void $ waitForTypecheck doc - let editA rhs = - changeDoc doc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ src rhs] - editA "AAAA" + let editA rhs = changeDoc doc [TextDocumentContentChangeEvent . InR . TextDocumentContentChangeWholeDocument $ src rhs] + editA "aaaa" void $ waitForTypecheck doc - editA "AAAAA" + editA "aaaaa" void $ waitForTypecheck doc - compls <- getCompletions doc (Position 0 15) - liftIO $ filter ("AAA" `T.isPrefixOf`) (mapMaybe _insertText compls) @?= ["AAAAA"] + compls <- getCompletions doc (Position 0 11) + liftIO $ filter ("aaa" `T.isPrefixOf`) (mapMaybe _insertText compls) @?= ["aaaaa"] pure (), completionTest "polymorphic record dot completion" @@ -344,10 +344,10 @@ otherCompletionTests = [ T.unlines [ "module A where", "import B", - "memb" + "3 = memb" ] _ <- waitForDiagnostics - compls <- getCompletions docA $ Position 2 4 + compls <- getCompletions docA $ Position 2 7 let compls' = [txt | CompletionItem {_insertText = Just txt, ..} <- compls, _label == "member"] liftIO $ take 1 compls' @?= ["member"], @@ -480,9 +480,9 @@ projectCompletionTests = "import ALocal" ] compls <- getCompletions doc (Position 1 13) - let item = head $ filter ((== "ALocalModule") . (^. L.label)) compls - liftIO $ do - item ^. L.label @?= "ALocalModule", + + forM_ (listToMaybe $ filter ((== "ALocalModule") . (^. L.label)) compls) $ + \item -> liftIO $ item ^. L.label @?= "ALocalModule", testSessionEmptyWithCradle "auto complete functions from qualified imports without alias" "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}" $ do _ <- createDoc "A.hs" "haskell" $ T.unlines [ "module A (anidentifier) where", @@ -495,9 +495,8 @@ projectCompletionTests = "A." ] compls <- getCompletions doc (Position 2 2) - let item = head compls - liftIO $ do - item ^. L.label @?= "anidentifier", + forM_ (listToMaybe compls) $ + \item -> liftIO $ item ^. L.label @?= "anidentifier", testSessionEmptyWithCradle "auto complete functions from qualified imports with alias" "cradle: {direct: {arguments: [\"-Wmissing-signatures\", \"A\", \"B\"]}}" $ do _ <- createDoc "A.hs" "haskell" $ T.unlines @@ -511,11 +510,247 @@ projectCompletionTests = "foo = Alias." ] compls <- getCompletions doc (Position 2 12) - let item = head compls - liftIO $ do - item ^. L.label @?= "anidentifier" + forM_ (listToMaybe compls) $ + \item -> liftIO $ item ^. L.label @?= "anidentifier" ] +contextCompletionTests :: [TestTree] +contextCompletionTests = + [ testSessionSingleFile "import snippets at top level" "A.hs" + [ "module A where" + , "imp" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 1 3) + let importSnippets = filterSnippetsLabel "import" compls + liftIO $ length importSnippets @?= 4 + + , completionTest "no import snippet past a declaration" + [ "module A where" + , "foo = ()" + , "imp" + ] + (Position 2 3) + [] + + , completionTest + "function snippet at top level" + [ "module A where" + , "foo = ()" + , "fun" + ] + (Position 2 3) + [("function", CompletionItemKind_Snippet, + "${1:identifier} :: ${2:type}\n${1:identifier} = ${3:body}", + False, False, Nothing)] + + , completionTest "no function snippet past an import" + [ "module A where" + , "fun" + , "import Control.Monad hiding (join)" + ] + (Position 2 3) + [] + + , completionTest + "class snippet at top level" + [ "module A where" + , "foo = ()" + , "cla" + ] + (Position 2 3) + [("class", CompletionItemKind_Snippet, "class ${1:name} where", + False, False, Nothing)] + + , completionTest + "instance snippet at top level" + ["module A where", "foo = ()", "inst"] + (Position 2 4) + [("instance", CompletionItemKind_Snippet, "instance ${1:name} where", + False, False, Nothing)] + + , testSessionSingleFile "no snippets in value binding" "A.hs" + [ "module A where" + , "foo = imp" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 1 9) + let snippets = [ c | c@CompletionItem{..} <- compls + , _kind == Just CompletionItemKind_Snippet + , _label == "import" ] + liftIO $ snippets @?= [] + + , testSessionSingleFile "no snippets in instance body" "A.hs" + [ "module A where" + , "class Foo a where" + , " bar :: a -> ()" + , "instance Foo Int where" + , " bar _ = imp" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 4 13) + let snippets = filterSnippetsLabel "import" compls + liftIO $ snippets @?= [] + + , testSessionSingleFile "top level excludes regular completions" "A.hs" + [ "module A where" + , "hea" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 1 3) + let headCompls = [ c | c@CompletionItem{..} <- compls, _label == "head" ] + liftIO $ headCompls @?= [] + + , testSessionSingleFile "unmatched prefix at top level returns empty" "A.hs" + [ "module A where" + , "xyz" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 1 3) + liftIO $ compls @?= [] + + , completionTest + "type context filters out value completions" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A () where" + , "data Xxxtype = Xxxcon" + , "xxxval = ()" + , "g :: Xxx" + ] + (Position 4 8) + [("Xxxtype", CompletionItemKind_Struct, "Xxxtype", False, True, Nothing)] + + , completionTest + "type sig in where-clause gives type completions" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A () where" + , "data Xxxtype = Xxxcon" + , "xxxval = ()" + , "foo x = bar" + , " where" + , " helper :: Xxx" + , " helper = bar" + ] + (Position 6 17) -- after "Xxx" in " helper :: Xxx" + [("Xxxtype", CompletionItemKind_Struct, "Xxxtype", False, True, Nothing)] + + , testSessionSingleFile "value binding in where-clause gives value completions" "A.hs" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A () where" + , "data Xxxtype = Xxxcon" + , "xxxval = ()" + , "foo x = bar" + , " where" + , " helper = xxxv" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 6 16) -- after "xxxv" + let labels = map (view L.label) compls + liftIO $ assertBool "xxxval should appear in value context" ("xxxval" `elem` labels) + liftIO $ assertBool "Xxxtype should not appear in value context" + (not ("Xxxtype" `elem` labels)) + + , testSessionSingleFile "no snippets in where-clause" "A.hs" + [ "module A where" + , "foo x = bar" + , " where" + , " helper = imp" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 3 15) -- after "imp" in " helper = imp" + let snippets = filterSnippetsLabel "import" compls + liftIO $ snippets @?= [] + + , completionTest + "type sig in nested where-clause gives type completions" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A () where" + , "data Xxxtype = Xxxcon" + , "xxxval = ()" + , "foo x = outer" + , " where" + , " inner y = result" + , " where" + , " sig :: Xxx" + , " sig = undefined" + ] + (Position 8 18) + [("Xxxtype", CompletionItemKind_Struct, "Xxxtype", False, True, Nothing)] + + , completionTest + "type sig in match alternative where-clause gives type completions" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A () where" + , "data Xxxtype = Xxxcon" + , "xxxval = ()" + , "foo 0 = bar" + , " where helper :: Xxx" + , "foo _ = baz" + ] + (Position 5 21) -- after "Xxx" in " where helper :: Xxx" + [("Xxxtype", CompletionItemKind_Struct, "Xxxtype", False, True, Nothing)] + + , completionTest + "type sig in pattern binding where-clause gives type completions" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A () where" + , "data Xxxtype = Xxxcon" + , "xxxval = ()" + , "(a, b) = (undefined, undefined)" + , " where" + , " helper :: Xxx" + , " helper = undefined" + ] + (Position 6 17) -- after "Xxx" in " helper :: Xxx" + [("Xxxtype", CompletionItemKind_Struct, "Xxxtype", False, True, Nothing)] + + , completionTest + "type sig in let expression gives type completions" + [ "{-# OPTIONS_GHC -Wunused-binds #-}" + , "module A () where" + , "data Xxxtype = Xxxcon" + , "xxxval = ()" + , "foo =" + , " let helper :: Xxx" + , " helper = undefined" + , " in helper" + ] + (Position 5 19) -- after "Xxx" in " let helper :: Xxx" + [("Xxxtype", CompletionItemKind_Struct, "Xxxtype", False, True, Nothing)] + + , testSessionSingleFile "module header snippet shown when no module declaration" "A.hs" + [ "mod" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 0 3) + let moduleSnippets = filterSnippetsLabel "module" compls + liftIO $ length moduleSnippets @?= 1 + + , testSessionSingleFile "module header snippet not shown when module declaration exists" "A.hs" + [ "module A where" + , "mod" + ] $ do + doc <- openDoc "A.hs" "haskell" + _ <- waitForDiagnostics + compls <- getCompletions doc (Position 1 3) + let moduleSnippets = filterSnippetsLabel "module" compls + liftIO $ moduleSnippets @?= [] + ] + where + filterSnippetsLabel l snippets = + [ c | c@CompletionItem{..} <- snippets + , _kind == Just CompletionItemKind_Snippet + , _label == l + ] + completionDocTests :: [TestTree] completionDocTests = [ testSessionEmpty "local define" $ do diff --git a/ghcide/ghcide.cabal b/ghcide/ghcide.cabal index 18091e2ddc..2df63cd392 100644 --- a/ghcide/ghcide.cabal +++ b/ghcide/ghcide.cabal @@ -174,6 +174,8 @@ library Development.IDE.Monitoring.OpenTelemetry Development.IDE.Plugin Development.IDE.Plugin.Completions + Development.IDE.Plugin.Completions.Context + Development.IDE.Plugin.Completions.Snippet Development.IDE.Plugin.Completions.Types Development.IDE.Plugin.Completions.Logic Development.IDE.Plugin.HLS diff --git a/ghcide/src/Development/IDE/Plugin/Completions.hs b/ghcide/src/Development/IDE/Plugin/Completions.hs index de90d458fa..adc9eb4b02 100644 --- a/ghcide/src/Development/IDE/Plugin/Completions.hs +++ b/ghcide/src/Development/IDE/Plugin/Completions.hs @@ -8,61 +8,69 @@ module Development.IDE.Plugin.Completions , ghcideCompletionsPluginPriority ) where -import Control.Concurrent.Async (concurrently) -import Control.Concurrent.STM.Stats (readTVarIO) -import Control.Lens ((&), (.~), (?~)) +import Control.Concurrent.Async (concurrently) +import Control.Concurrent.STM.Stats (readTVarIO) +import Control.Lens ((&), (.~), (?~)) import Control.Monad.IO.Class -import Control.Monad.Trans.Except (ExceptT (ExceptT), - withExceptT) -import qualified Data.HashMap.Strict as Map -import qualified Data.HashSet as Set +import Control.Monad.Trans.Except (ExceptT (ExceptT), + withExceptT) +import qualified Data.HashMap.Strict as Map +import qualified Data.HashSet as Set import Data.Maybe -import qualified Data.Text as T +import qualified Data.Text as T import Development.IDE.Core.Compile -import Development.IDE.Core.FileStore (getUriContents) +import Development.IDE.Core.FileStore (getUriContents) import Development.IDE.Core.PluginUtils import Development.IDE.Core.PositionMapping import Development.IDE.Core.RuleTypes -import Development.IDE.Core.Service hiding (Log, LogShake) -import Development.IDE.Core.Shake hiding (Log, - knownTargets) -import qualified Development.IDE.Core.Shake as Shake +import Development.IDE.Core.Service hiding (Log, + LogShake) +import Development.IDE.Core.Shake hiding (Log, + knownTargets) +import qualified Development.IDE.Core.Shake as Shake import Development.IDE.GHC.Compat import Development.IDE.GHC.Util import Development.IDE.Graph +import Development.IDE.Plugin.Completions.Context import Development.IDE.Plugin.Completions.Logic import Development.IDE.Plugin.Completions.Types import Development.IDE.Spans.Common import Development.IDE.Spans.Documentation import Development.IDE.Types.Exports -import Development.IDE.Types.HscEnvEq (HscEnvEq (envPackageExports, envVisibleModuleNames), - hscEnv) -import qualified Development.IDE.Types.KnownTargets as KT +import Development.IDE.Types.HscEnvEq (HscEnvEq (envPackageExports, envVisibleModuleNames), + hscEnv) +import qualified Development.IDE.Types.KnownTargets as KT import Development.IDE.Types.Location -import Ide.Logger (Pretty (pretty), - Recorder, - WithPriority, - cmapWithPrio) +import Ide.Logger (Pretty (pretty), + Priority (..), + Recorder, + WithPriority, + cmapWithPrio, + logWith) import Ide.Plugin.Error import Ide.Types -import qualified Language.LSP.Protocol.Lens as L +import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Message import Language.LSP.Protocol.Types import Numeric.Natural -import Prelude hiding (mod) -import Text.Fuzzy.Parallel (Scored (..)) +import Prelude hiding (mod) +import Text.Fuzzy.Parallel (Scored (..)) -import Development.IDE.Core.Rules (usePropertyAction) +import Development.IDE.Core.Rules (usePropertyAction) -import qualified Ide.Plugin.Config as Config +import qualified Ide.Plugin.Config as Config -import qualified GHC.LanguageExtensions as LangExt +import qualified GHC.LanguageExtensions as LangExt -data Log = LogShake Shake.Log deriving Show +data Log + = LogShake Shake.Log + | LogDetectedContext Context + deriving Show instance Pretty Log where pretty = \case LogShake msg -> pretty msg + LogDetectedContext context -> "Completion context detected: " <> pretty context ghcideCompletionsPluginPriority :: Natural ghcideCompletionsPluginPriority = defaultPluginPriority @@ -70,7 +78,7 @@ ghcideCompletionsPluginPriority = defaultPluginPriority descriptor :: Recorder (WithPriority Log) -> PluginId -> PluginDescriptor IdeState descriptor recorder plId = (defaultPluginDescriptor plId desc) { pluginRules = produceCompletions recorder - , pluginHandlers = mkPluginHandler SMethod_TextDocumentCompletion getCompletionsLSP + , pluginHandlers = mkPluginHandler SMethod_TextDocumentCompletion (getCompletionsLSP recorder) <> mkResolveHandler SMethod_CompletionItemResolve resolveCompletion , pluginConfigDescriptor = defaultConfigDescriptor {configCustomConfig = mkCustomConfig properties} , pluginPriority = ghcideCompletionsPluginPriority @@ -89,6 +97,9 @@ produceCompletions recorder = do let cdata = localCompletionsForParsedModule uri pm return ([], Just cdata) _ -> return ([], Nothing) + define (cmapWithPrio LogShake recorder) $ \GetContextMap file -> do + mbPm <- useWithStale GetParsedModule file + return ([], getContextMap . fst <$> mbPm) define (cmapWithPrio LogShake recorder) $ \NonLocalCompletions file -> do -- For non local completions we avoid depending on the parsed module, -- synthesizing a fake module with an empty body from the buffer @@ -156,8 +167,8 @@ resolveCompletion ide _pid comp@CompletionItem{_detail,_documentation,_data_} ur (_,res) -> res -- | Generate code actions. -getCompletionsLSP :: PluginMethodHandler IdeState Method_TextDocumentCompletion -getCompletionsLSP ide plId +getCompletionsLSP :: Recorder (WithPriority Log) -> PluginMethodHandler IdeState Method_TextDocumentCompletion +getCompletionsLSP recorder ide plId CompletionParams{_textDocument=TextDocumentIdentifier uri ,_position=position ,_context=completionContext} = ExceptT $ do @@ -170,7 +181,7 @@ getCompletionsLSP ide plId opts <- liftIO $ getIdeOptionsIO $ shakeExtras ide localCompls <- useWithStaleFast LocalCompletions npath nonLocalCompls <- useWithStaleFast NonLocalCompletions npath - pm <- useWithStaleFast GetParsedModule npath + ctxTree <- useWithStaleFast GetContextMap npath binds <- fromMaybe (mempty, zeroMapping) <$> useWithStaleFast GetBindings npath knownTargets <- liftIO $ runAction "Completion" ide $ useNoFile GetKnownTargets let localModules = maybe [] (Map.keys . targetMap) knownTargets @@ -194,9 +205,9 @@ getCompletionsLSP ide plId -> useWithStaleFast GetHieAst npath _ -> return Nothing - pure (opts, fmap (,pm,binds) compls, moduleExports, astres) + pure (opts, fmap (,ctxTree,binds) compls, moduleExports, astres) case compls of - Just (cci', parsedMod, bindMap) -> do + Just (cci', ctxTree, bindMap) -> do let pfix = getCompletionPrefixFromRope position cnts case (pfix, completionContext) of (PosPrefixInfo _ "" _ _, Just CompletionContext { _triggerCharacter = Just "."}) @@ -204,9 +215,12 @@ getCompletionsLSP ide plId (_, _) -> do let clientCaps = clientCapabilities $ shakeExtras ide plugins = idePlugins $ shakeExtras ide + context = deduceContext ctxTree (cursorPos pfix) + hasModuleHeader = maybe False (contextHasModuleHeader . fst) ctxTree config <- liftIO $ runAction "" ide $ getCompletionsConfig plId - let allCompletions = getCompletions plugins ideOpts cci' parsedMod astres bindMap pfix clientCaps config moduleExports uri + let allCompletions = getCompletions plugins ideOpts cci' context hasModuleHeader astres bindMap pfix clientCaps config moduleExports uri + logWith recorder Debug $ LogDetectedContext context pure $ InL (orderedCompletions allCompletions) _ -> return (InL []) _ -> return (InL []) diff --git a/ghcide/src/Development/IDE/Plugin/Completions/Context.hs b/ghcide/src/Development/IDE/Plugin/Completions/Context.hs new file mode 100644 index 0000000000..891a263d61 --- /dev/null +++ b/ghcide/src/Development/IDE/Plugin/Completions/Context.hs @@ -0,0 +1,312 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE NoFieldSelectors #-} +{-# LANGUAGE TypeFamilies #-} + +module Development.IDE.Plugin.Completions.Context + ( Context (..) + , ContextGroup (..) + , ContextMap + , GetContextMap (..) + , contextHasModuleHeader + , deduceContext + , getContext + , getContextMap + ) where + +import Control.DeepSeq (NFData (..), rwhnf) +import Control.Monad (join) +import Data.Generics (Data (..), GenericQ, + extQ, mkQ) +import Data.Hashable (Hashable) +import Data.List (foldl') +import Data.List.Extra (nub) +import Data.Maybe (isJust, mapMaybe, + maybeToList) +import qualified Data.Text as T +import Development.IDE +import Development.IDE.Core.PositionMapping +import Development.IDE.GHC.Compat hiding (getContext) +import GHC.Generics (Generic) +import Language.LSP.Protocol.Types (isSubrangeOf) + +#if MIN_VERSION_ghc(9,9,0) +import GHC.Hs (HasLoc) +#endif + + +-- | A context of a declaration in the program e.g. is the declaration a +-- type declaration or a value declaration. Used for determining which code +-- completions to show. +data Context + = TypeContext + | ValueContext + | -- | import context with module name. + ImportContext T.Text + | -- | import list context with module name. + ImportListContext T.Text + | -- | import hiding context with module name. + ImportHidingContext T.Text + | -- | Top-level context, with context groups indicating what would be valid + -- in that top-level context. + -- + -- NB: An empty list denotes _all_ contexts, this occurs in splices which + -- overlap with the top-level declaration snippets while typing. + TopContext [ContextGroup] + | -- | Unsupported context, a placeholder context where we give up being smart + -- and show all known symbols. + DefaultContext + deriving (Show, Eq) + +data ContextGroup + = HeaderGroup + | ImportGroup + | DeclarationGroup + deriving (Show, Eq, Ord) + +data GetContextMap = GetContextMap + deriving (Eq, Show, Generic) +instance Hashable GetContextMap +instance NFData GetContextMap +type instance RuleResult GetContextMap = ContextMap + +-- | Entries within each chunk are from a contiguous group of source items +-- (imports or declarations). +data ContextChunk = Chunk + { low :: {-# UNPACK #-} !Position + , high :: {-# UNPACK #-} !Position + , group :: !ContextGroup + , context :: Range -> ContextResult + } + +-- | Build lazy 'ContextChunk' by processing @n@ source items at a time. +groupedChunks :: Int -> ContextGroup -> (a -> Maybe Range) -> (a -> Range -> ContextResult) -> [a] -> [ContextChunk] +groupedChunks n group getPos locate xs = go xs + where + go [] = [] + go xs = + let (chunk, rest) = splitAt n xs + context = foldMap locate chunk + positions = mapMaybe getPos chunk + in case positions of + [] -> go rest + ps -> Chunk + { low = minimum (fmap _start ps) + , high = maximum (fmap _end ps) + , group + , context + } : go rest + +-- | Build lazy 'ContextChunk' by processing @n@ source items at a time. +singletonChunk :: ContextGroup -> (a -> Maybe Range) -> (a -> Range -> ContextResult) -> a -> Maybe ContextChunk +singletonChunk group getPos locate inp = flip fmap (getPos inp) $ + \(Range s e) -> Chunk s e group (locate inp) + +-- | Used during context finding, combines into the tightest interval. +-- As an intuition, the primary interface is through +-- @Monoid (Range -> ContextResult)@. +data ContextResult = NoContext | ContextResult !Range !Context +instance Monoid ContextResult where mempty = NoContext +instance Semigroup ContextResult where (<>) = tighten + +tighten :: ContextResult -> ContextResult -> ContextResult +tighten NoContext b = b +tighten a NoContext = a +tighten ar@(ContextResult a _) br@(ContextResult b _) = + if b `isSubrangeOf` a then br else ar + +-- | A context map, built from a parsed module. Stores whether the module +-- already has a @module ... where@ header, so that the header snippet can +-- be suppressed for files that already declare a module. +data ContextMap = ContextMap !Bool [ContextChunk] +instance Semigroup ContextMap where + ContextMap h1 c1 <> ContextMap h2 c2 = ContextMap (h1 || h2) (c1 <> c2) +instance Monoid ContextMap where + mempty = ContextMap False [] +instance Show ContextMap where show _ = "" +instance NFData ContextMap where rnf = rwhnf + +-- * Building + +-- | Build a @ContextMap@ from a parsed module. +-- +-- Walks module header, exports, imports, and top-level declarations +-- (one level into class bodies). Built once per file edit and cached +-- as a Shake rule. +getContextMap :: ParsedModule -> ContextMap +getContextMap pm = + ContextMap (isJust hsmodName) $ + -- These denote the size of the "jumps" of the cursor when traversing the AST. + -- Reduces the amount of data we have to look at with syb. + moduleChunk + <> groupedChunks 10 ImportGroup rangeOf getImportContext hsmodImports + <> groupedChunks 5 DeclarationGroup rangeOf getDeclContext hsmodDecls + where +#if MIN_VERSION_ghc(9,9,0) + moduleChunk = maybeToList (singletonChunk HeaderGroup rangeOf getHeaderContext hsmodName) +#else + moduleChunk = maybeToList $ join $ fmap (singletonChunk HeaderGroup rangeOf getHeaderContext) hsmodName +#endif + HsModule {hsmodName, hsmodImports, hsmodDecls} = + unLoc (pm_parsed_source pm) + +getHeaderContext :: Data a => a -> Range -> ContextResult +getHeaderContext decl query = + gather + (<>) + ((mempty, False) `mkQ` modNameQ query) + decl + +getImportContext :: LImportDecl GhcPs -> Range -> ContextResult +getImportContext imports query = + gather + (<>) + ((mempty, False) `mkQ` importQ query) + imports + +getDeclContext :: LHsDecl GhcPs -> Range -> ContextResult +getDeclContext declarations query = + gather + (<>) + ((mempty, False) `mkQ` sigQ query `extQ` bindQ query `extQ` declQ query) + declarations + +-- * Querying + +-- | Returns 'True' when the parsed module already has a @module ... where@ +-- declaration. Used downstream to suppress the module header snippet. +contextHasModuleHeader :: ContextMap -> Bool +contextHasModuleHeader (ContextMap h _) = h + +-- | Look up the completion context at a given position. +-- Returns the innermost (most specific) context that contains the position. +-- +-- Only the 'ContextChunks' up to and including the chunk containing the +-- query position are forced; later chunks remain as unevaluated thunks. +getContext :: ContextMap -> PositionResult Position -> Context +getContext (ContextMap _ chunks) query = + case searchChunks HeaderGroup chunks mempty of + (groups, NoContext) -> TopContext $ nub groups + (_, ContextResult _ found) -> found + where + (qLo, qHi) = case query of + PositionExact p -> (p, p) + PositionRange l u -> (l, u) + + searchChunks :: ContextGroup -> [ContextChunk] -> ([ContextGroup], ContextResult) -> ([ContextGroup], ContextResult) + searchChunks _ [] !acc = acc + searchChunks lastChunk (Chunk cLo cHi group contextOf : rest) !acc + | -- query is past this chunk (line-only comparison so cursors + -- past the last column on the final line still match) + _line qLo > _line cHi = searchChunks group rest acc + | -- query is before this chunk + qHi < cLo = ([lastChunk, group], mempty) <> acc + -- this chunk is relevant, emit the group and all relevant intervals + | otherwise = searchChunks group rest (([group], contextOf (Range qLo qHi)) <> acc) + +-- | Look up the completion context at the given position, applying a position +-- mapping to account for stale data. +deduceContext :: Maybe (ContextMap, PositionMapping) -> Position -> Context +deduceContext maybeCtx pos = case maybeCtx of + Nothing -> DefaultContext + Just (ct, pmapping) -> + let PositionMapping pDelta = pmapping + position' = fromDelta pDelta pos + in getContext ct position' + +-- * SYB queries types + +importQ :: Range -> LImportDecl GhcPs -> (ContextResult, Bool) +importQ query impDecl'@(L _ impDecl) = + let importModuleName = T.pack $ moduleNameString $ unLoc $ ideclName impDecl + inlineResults = fst $ importInline query importModuleName (ideclImportList impDecl) + importResult = fst $ contextual (ImportContext importModuleName) True query impDecl' + importInline _ _ Nothing = (mempty, False) + importInline query modName (Just (which, l)) = + case which of + EverythingBut -> contextual (ImportHidingContext modName) True query l + Exactly -> contextual (ImportListContext modName) True query l + in (inlineResults <> importResult, True) + + +declQ :: Range -> LHsDecl GhcPs -> (ContextResult, Bool) +declQ query decl'@(L _ decl) = + let range = rangeOf decl' + in contInRange query range $ \declRange -> case decl of + SigD {} -> (ContextResult declRange TypeContext, True) + ValD {} -> (ContextResult declRange ValueContext, False) + TyClD {} -> (ContextResult declRange TypeContext, False) -- DataDecl, SynDecl, FamilyDecl + InstD {} -> (ContextResult declRange ValueContext, False) + DerivD {} -> (ContextResult declRange TypeContext, True) + SpliceD {} -> (ContextResult declRange (TopContext []), True) + _ -> (ContextResult declRange DefaultContext, True) -- DefD, WarningD, AnnD, RuleD, DocD, KindSigD + +modNameQ :: Range -> XRec GhcPs ModuleName -> (ContextResult, Bool) +modNameQ = contextual (TopContext [HeaderGroup]) True + +sigQ :: Range -> LSig GhcPs -> (ContextResult, Bool) +sigQ = contextual TypeContext True + +bindQ :: Range -> LHsBind GhcPs -> (ContextResult, Bool) +bindQ = contextual ValueContext False + +#if MIN_VERSION_ghc(9,9,0) +contextual :: HasLoc a => Context -> Bool -> Range -> a -> (ContextResult, Bool) +#else +contextual :: Context -> Bool -> Range -> GenLocated (SrcSpanAnn' a) e -> (ContextResult, Bool) +#endif +contextual context shouldStop query s = + let range = rangeOf s + in contInRange query range $ \range -> (ContextResult range context, shouldStop) + +-- | Run a continuation with the 'Range' of a source span, returning no context +-- if the span is missing or outside the query range. +contInRange :: Range -> Maybe Range -> (Range -> (ContextResult, Bool)) -> (ContextResult, Bool) +contInRange query range k = case range of + Nothing -> (NoContext, True) + Just range' | outside query range' -> (NoContext, True) + Just range' -> k range' + +-- * Helpers + +-- | A query range is outside a source range if it ends before the source +-- starts, or it starts on a line after the source ends. +-- We intentionally compare only lines (not columns) for the trailing +-- boundary so that a cursor past the last token on a line still falls +-- inside the node occupying that line. +outside :: Range -> Range -> Bool +outside (Range ps pe) (Range qs qe) = pe < qs || _line ps > _line qe + +#if MIN_VERSION_ghc(9,9,0) +rangeOf :: HasLoc a => a -> Maybe Range +rangeOf = srcSpanToRange . locA +#else +rangeOf :: GenLocated (SrcSpanAnn' a) e -> Maybe Range +rangeOf = srcSpanToRange . getLocA +#endif + +instance Pretty Context where + pretty = \case + TypeContext -> "type context" + ValueContext -> "value context" + ImportContext mod -> "import context " <> pretty mod + ImportListContext mod -> "import explicit context " <> pretty mod + ImportHidingContext mod -> "import hiding context " <> pretty mod + TopContext cg -> "top context " <> pretty cg + DefaultContext -> "unknown context" + +instance Pretty ContextGroup where + pretty = \case + HeaderGroup -> "header" + ImportGroup -> "imports" + DeclarationGroup -> "declarations" + +-- | Variation of @Data.Generics.Schemes.everythingBut@, but uses foldl'. +gather :: forall r. (r -> r -> r) -> GenericQ (r, Bool) -> GenericQ r +gather k f = go + where + go :: GenericQ r + go x = let (v, stop) = f x + in if stop + then v + else foldl' k v (gmapQ go x) diff --git a/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs b/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs index 3fe20d24b9..cb6ccb3d55 100644 --- a/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs +++ b/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs @@ -15,65 +15,68 @@ module Development.IDE.Plugin.Completions.Logic ( ) where import Control.Applicative -import Control.Lens hiding (Context, - parts) -import Data.Char (isAlphaNum, isUpper) -import Data.Default (def) +import Control.Lens hiding (Context, + parts) +import Data.Char (isAlphaNum, + isUpper) import Data.Generics -import Data.List.Extra as List hiding - (stripPrefix) -import qualified Data.Map as Map -import Prelude hiding (mod) - -import Data.Maybe (fromMaybe, isJust, - isNothing, - listToMaybe, - mapMaybe) -import qualified Data.Text as T -import qualified Text.Fuzzy.Parallel as Fuzzy +import Data.List.Extra as List hiding + (stripPrefix) +import qualified Data.Map as Map +import Prelude hiding (mod) + +import Data.Maybe (fromMaybe, isJust, + isNothing, + listToMaybe, + mapMaybe) +import qualified Data.Text as T +import qualified Text.Fuzzy.Parallel as Fuzzy import Control.Monad -import Data.Aeson (ToJSON (toJSON)) -import Data.Function (on) +import Data.Aeson (ToJSON (toJSON)) +import Data.Function (on) -import qualified Data.HashSet as HashSet -import Data.Ord (Down (Down)) -import qualified Data.Set as Set +import qualified Data.HashSet as HashSet +import Data.Ord (Down (Down)) +import qualified Data.Set as Set import Development.IDE.Core.PositionMapping -import Development.IDE.GHC.Compat hiding (isQual, ppr) -import qualified Development.IDE.GHC.Compat as GHC +import Development.IDE.GHC.Compat hiding (isQual, ppr) +import qualified Development.IDE.GHC.Compat as GHC import Development.IDE.GHC.Compat.Util import Development.IDE.GHC.Error import Development.IDE.GHC.Util +import Development.IDE.Plugin.Completions.Context (Context (..)) +import qualified Development.IDE.Plugin.Completions.Context as Context +import Development.IDE.Plugin.Completions.Snippet import Development.IDE.Plugin.Completions.Types import Development.IDE.Spans.LocalBindings import Development.IDE.Types.Exports import Development.IDE.Types.Options -import GHC.Iface.Ext.Types (HieAST, - NodeInfo (..)) -import GHC.Iface.Ext.Utils (nodeInfo) -import Ide.PluginUtils (mkLspCommand) -import Ide.Types (CommandId (..), - IdePlugins (..), - PluginId) +import GHC.Iface.Ext.Types (HieAST, + NodeInfo (..)) +import GHC.Iface.Ext.Utils (nodeInfo) +import Ide.PluginUtils (mkLspCommand) +import Ide.Types (CommandId (..), + IdePlugins (..), + PluginId) import Language.Haskell.Syntax.Basic -import qualified Language.LSP.Protocol.Lens as L +import qualified Language.LSP.Protocol.Lens as L import Language.LSP.Protocol.Types -import qualified Language.LSP.VFS as VFS -import Text.Fuzzy.Parallel (Scored (score), - original) +import qualified Language.LSP.VFS as VFS +import Text.Fuzzy.Parallel (Scored (score), + original) -import qualified Data.Text.Utf16.Rope.Mixed as Rope -import Development.IDE hiding (line) +import qualified Data.Text.Utf16.Rope.Mixed as Rope +import Development.IDE hiding (line) -import Development.IDE.Spans.AtPoint (pointCommand) +import Development.IDE.Spans.AtPoint (pointCommand) -import qualified Development.IDE.Plugin.Completions.Types as C -import GHC.Plugins (Depth (AllTheWay), - mkUserStyle, - neverQualify, - sdocStyle) +import qualified Development.IDE.Plugin.Completions.Types as C +import GHC.Plugins (Depth (AllTheWay), + mkUserStyle, + neverQualify, + sdocStyle) -- See Note [Guidelines For Using CPP In GHCIDE Import Statements] @@ -82,82 +85,6 @@ import GHC.Plugins (Depth (AllTheWay), chunkSize :: Int chunkSize = 1000 --- From haskell-ide-engine/hie-plugin-api/Haskell/Ide/Engine/Context.hs - --- | A context of a declaration in the program --- e.g. is the declaration a type declaration or a value declaration --- Used for determining which code completions to show --- TODO: expand this with more contexts like classes or instances for --- smarter code completion -data Context = TypeContext - | ValueContext - | ModuleContext String -- ^ module context with module name - | ImportContext String -- ^ import context with module name - | ImportListContext String -- ^ import list context with module name - | ImportHidingContext String -- ^ import hiding context with module name - | ExportContext -- ^ List of exported identifiers from the current module - deriving (Show, Eq) - --- | Generates a map of where the context is a type and where the context is a value --- i.e. where are the value decls and the type decls -getCContext :: Position -> ParsedModule -> Maybe Context -getCContext pos pm - | Just (L (locA -> r) modName) <- moduleHeader - , pos `isInsideSrcSpan` r - = Just (ModuleContext (moduleNameString modName)) - - | Just (L (locA -> r) _) <- exportList - , pos `isInsideSrcSpan` r - = Just ExportContext - - | Just ctx <- something (Nothing `mkQ` go `extQ` goInline) decl - = Just ctx - - | Just ctx <- something (Nothing `mkQ` importGo) imports - = Just ctx - - | otherwise - = Nothing - - where decl = hsmodDecls $ unLoc $ pm_parsed_source pm - moduleHeader = hsmodName $ unLoc $ pm_parsed_source pm - exportList = hsmodExports $ unLoc $ pm_parsed_source pm - imports = hsmodImports $ unLoc $ pm_parsed_source pm - - go :: LHsDecl GhcPs -> Maybe Context - go (L (locA -> r) SigD {}) - | pos `isInsideSrcSpan` r = Just TypeContext - | otherwise = Nothing - go (L (locA -> r) GHC.ValD {}) - | pos `isInsideSrcSpan` r = Just ValueContext - | otherwise = Nothing - go _ = Nothing - - goInline :: GHC.LHsType GhcPs -> Maybe Context - goInline (GHC.L (locA -> r) _) - | pos `isInsideSrcSpan` r = Just TypeContext - goInline _ = Nothing - - importGo :: GHC.LImportDecl GhcPs -> Maybe Context - importGo (L (locA -> r) impDecl) - | pos `isInsideSrcSpan` r - = importInline importModuleName (fmap (fmap reLoc) $ ideclImportList impDecl) - <|> Just (ImportContext importModuleName) - - | otherwise = Nothing - where importModuleName = moduleNameString $ unLoc $ ideclName impDecl - - -- importInline :: String -> Maybe (Bool, GHC.Located [LIE GhcPs]) -> Maybe Context - importInline modName (Just (EverythingBut, L r _)) - | pos `isInsideSrcSpan` r = Just $ ImportHidingContext modName - | otherwise = Nothing - - importInline modName (Just (Exactly, L r _)) - | pos `isInsideSrcSpan` r = Just $ ImportListContext modName - | otherwise = Nothing - - importInline _ _ = Nothing - occNameToComKind :: OccName -> CompletionItemKind occNameToComKind oc | isVarOcc oc = case occNameString oc of @@ -286,11 +213,6 @@ mkExtCompl label = defaultCompletionItemWithLabel label & L.kind ?~ CompletionItemKind_Keyword -defaultCompletionItemWithLabel :: T.Text -> CompletionItem -defaultCompletionItemWithLabel label = - CompletionItem label def def def def def def def def def - def def def def def def def def def - fromIdentInfo :: Uri -> IdentInfo -> Maybe T.Text -> CompItem fromIdentInfo doc identInfo@IdentInfo{..} q = CI { compKind= occNameToComKind name @@ -329,7 +251,6 @@ cacheDataProducer uri visibleMods curMod globalEnv inScopeEnv limports = -- Full canonical names of imported modules importDeclarations = map unLoc limports - -- The given namespaces for the imported modules (ie. full name, or alias if used) allModNamesAsNS = map (showModName . asNamespace) importDeclarations @@ -529,7 +450,8 @@ getCompletions :: IdePlugins a -> IdeOptions -> CachedCompletions - -> Maybe (ParsedModule, PositionMapping) + -> Context + -> Bool -> Maybe (HieAstResult, PositionMapping) -> (Bindings, PositionMapping) -> PosPrefixInfo @@ -542,7 +464,8 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, qualCompls, importableModules} - maybe_parsed + context + hasModuleHeader maybe_ast_res (localBindings, bmapping) prefixInfo@(PosPrefixInfo { fullLine, prefixScope, prefixText }) @@ -552,15 +475,15 @@ getCompletions uri -- ------------------------------------------------------------------------ -- IMPORT MODULENAME (NAM|) - | Just (ImportListContext moduleName) <- maybeContext + | ImportListContext moduleName <- context = moduleImportListCompletions moduleName - | Just (ImportHidingContext moduleName) <- maybeContext + | ImportHidingContext moduleName <- context = moduleImportListCompletions moduleName -- ------------------------------------------------------------------------ -- IMPORT MODULENAM| - | Just (ImportContext _moduleName) <- maybeContext + | ImportContext _ <- context = filtImportCompls -- ------------------------------------------------------------------------ @@ -570,6 +493,10 @@ getCompletions | "{-# " `T.isPrefixOf` fullLine = [] + -- ------------------------------------------------------------------------ + | TopContext groups <- context + = fmap (fmap (toggleSnippets caps config)) (filtTopContextCompls groups) + -- ------------------------------------------------------------------------ | otherwise = -- assumes that nubOrdBy is stable @@ -607,16 +534,6 @@ getCompletions $ Fuzzy.simpleFilter chunkSize maxC fullPrefix $ (if T.null enteredQual then id else mapMaybe (T.stripPrefix enteredQual)) allModNamesAsNS - -- If we have a parsed module, use it to determine which completion to show. - maybeContext :: Maybe Context - maybeContext = case maybe_parsed of - Nothing -> Nothing - Just (pm, pmapping) -> - let PositionMapping pDelta = pmapping - position' = fromDelta pDelta pos - lpos = lowerRange position' - hpos = upperRange position' - in getCContext lpos pm <|> getCContext hpos pm filtCompls :: [Scored (Bool, CompItem)] filtCompls = Fuzzy.filter chunkSize maxC prefixText ctxCompls (label . snd) @@ -658,11 +575,11 @@ getCompletions }) -- completions specific to the current context - ctxCompls' = case maybeContext of - Nothing -> compls - Just TypeContext -> filter ( isTypeCompl . snd) compls - Just ValueContext -> filter (not . isTypeCompl . snd) compls - Just _ -> filter (not . isTypeCompl . snd) compls + ctxCompls' = case context of + TypeContext -> filter (isTypeCompl . snd) compls + ValueContext -> filter (not . isTypeCompl . snd) compls + DefaultContext -> compls + _ -> filter (not . isTypeCompl . snd) compls -- Add whether the text to insert has backticks ctxCompls = (fmap.fmap) (\comp -> toggleAutoExtend config $ comp { isInfix = infixCompls }) ctxCompls' @@ -699,12 +616,11 @@ getCompletions , enteredQual `T.isPrefixOf` original label ] - moduleImportListCompletions :: String -> [Scored CompletionItem] - moduleImportListCompletions moduleNameS = - let moduleName = T.pack moduleNameS - funcs = lookupWithDefaultUFM moduleExportsMap HashSet.empty $ mkModuleName moduleNameS - funs = map (show . name) $ HashSet.toList funcs - in filterModuleExports moduleName $ map T.pack funs + moduleImportListCompletions :: T.Text -> [Scored CompletionItem] + moduleImportListCompletions moduleName = + let funcs = lookupWithDefaultUFM moduleExportsMap HashSet.empty $ mkModuleName (T.unpack moduleName) + funs = map (T.pack . show . name) $ HashSet.toList funcs + in filterModuleExports moduleName funs filtImportCompls :: [Scored CompletionItem] filtImportCompls = filtListWith (mkImportCompl enteredQual) importableModules @@ -717,6 +633,12 @@ getCompletions | T.null prefixScope = filtListWith mkExtCompl (optKeywords ideOpts) | otherwise = [] + filtTopContextCompls :: [Context.ContextGroup] -> [Scored CompletionItem] + filtTopContextCompls groups + | T.null prefixScope + = Fuzzy.filter chunkSize maxC fullPrefix (getContextSnippets hasModuleHeader groups) (view L.label) + | otherwise = [] + -- We use this ordering to alphabetically sort suggestions while respecting -- all the previously applied ordering sources. These are: -- 1. Qualified suggestions go first @@ -730,9 +652,6 @@ getCompletions let isLocal = maybe False (":" `T.isPrefixOf`) _detail (Down isQual, Down score, Down isLocal, _label, _detail) - - - uniqueCompl :: CompItem -> CompItem -> Ordering uniqueCompl candidate unique = case compare (label candidate, compKind candidate) diff --git a/ghcide/src/Development/IDE/Plugin/Completions/Snippet.hs b/ghcide/src/Development/IDE/Plugin/Completions/Snippet.hs new file mode 100644 index 0000000000..e639344296 --- /dev/null +++ b/ghcide/src/Development/IDE/Plugin/Completions/Snippet.hs @@ -0,0 +1,83 @@ +module Development.IDE.Plugin.Completions.Snippet (getContextSnippets) where + +import Control.Lens +import Data.Maybe (maybeToList) +import Data.String (IsString) +import Data.Text (Text) +import Development.IDE.Plugin.Completions.Context +import Development.IDE.Plugin.Completions.Types +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Types + +data SnippetCompletion = SnippetCompletion + { snippetLabel :: {-# UNPACK #-} !Text + , snippetDetail :: {-# UNPACK #-} !Text + -- | Might be good to use the structured snippets instead of bare text. + -- This is fine for now though, none of the top-level snippet completions are + -- parameterized. + , snippetContents :: {-# UNPACK #-} !Text + } + +getContextSnippets :: Bool -> [ContextGroup] -> [CompletionItem] +getContextSnippets hasModuleHeader groups = + let tbl = if hasModuleHeader + then filter ((/= HeaderGroup) . fst) topContextSnippets + else topContextSnippets + in fmap mkSnippetCompletion $ case groups of + [] -> concatMap snd tbl + _ -> concatMap (concat . maybeToList . (`lookup` tbl)) groups + +topContextSnippets :: [(ContextGroup, [SnippetCompletion])] +topContextSnippets = + [ ( HeaderGroup + , [ SnippetCompletion "module" "module header" moduleHeaderSnippet + ] + ), + ( ImportGroup + , [ SnippetCompletion "import" "import module" importUnqualifiedSnippet + , SnippetCompletion "import" "import module (explicit list)" importExplicitSnippet + , SnippetCompletion "import" "import module qualified as" importQualifiedAsSnippet + , SnippetCompletion "import" "import module hiding" importHidingSnippet + ] + ), + ( DeclarationGroup + , [ SnippetCompletion "function" "function definition" functionDefinitionSnippet + , SnippetCompletion "instance" "instance declaration" instanceDeclarationSnippet + , SnippetCompletion "class" "class declaration" classDeclarationSnippet + ] + ) + ] + +mkSnippetCompletion :: SnippetCompletion -> CompletionItem +mkSnippetCompletion SnippetCompletion {..} = + defaultCompletionItemWithLabel snippetLabel + & L.kind ?~ CompletionItemKind_Snippet + & L.detail ?~ snippetDetail + & L.insertText ?~ snippetContents + & L.insertTextFormat ?~ InsertTextFormat_Snippet + +moduleHeaderSnippet :: (IsString s) => s +moduleHeaderSnippet = "module ${1:name} where" + +importUnqualifiedSnippet :: (IsString s) => s +importUnqualifiedSnippet = "import ${1:module}" + +importExplicitSnippet :: (IsString s) => s +importExplicitSnippet = "import ${1:module} (${2:names})" + +importHidingSnippet :: (IsString s) => s +importHidingSnippet = "import ${1:module} hiding (${2:names})" + +importQualifiedAsSnippet :: (IsString s) => s +importQualifiedAsSnippet = "import ${1:module} qualified as ${2:alias}" + +functionDefinitionSnippet :: (IsString s) => s +functionDefinitionSnippet = + "${1:identifier} :: ${2:type}\n\ + \${1:identifier} = ${3:body}" + +classDeclarationSnippet :: (IsString s) => s +classDeclarationSnippet = "class ${1:name} where" + +instanceDeclarationSnippet :: (IsString s) => s +instanceDeclarationSnippet = "instance ${1:name} where" diff --git a/ghcide/src/Development/IDE/Plugin/Completions/Types.hs b/ghcide/src/Development/IDE/Plugin/Completions/Types.hs index 698003786c..6603f2d129 100644 --- a/ghcide/src/Development/IDE/Plugin/Completions/Types.hs +++ b/ghcide/src/Development/IDE/Plugin/Completions/Types.hs @@ -14,6 +14,7 @@ import qualified Data.Text as T import Data.Aeson import Data.Aeson.Types +import Data.Default (Default (..)) import Data.Function (on) import Data.Hashable (Hashable) import qualified Data.List as L @@ -26,7 +27,8 @@ import Development.IDE.Spans.Common () import GHC.Generics (Generic) import qualified GHC.Types.Name.Occurrence as Occ import Ide.Plugin.Properties -import Language.LSP.Protocol.Types (CompletionItemKind (..), Uri) +import Language.LSP.Protocol.Types (CompletionItem (..), + CompletionItemKind (..), Uri) import qualified Language.LSP.Protocol.Types as J -- | Produce completions info for a file @@ -137,20 +139,25 @@ snippetLexOrd :: Snippet -> Snippet -> Ordering snippetLexOrd = compare `on` snippetToText data CompItem = CI - { compKind :: CompletionItemKind - , insertText :: Snippet -- ^ Snippet for the completion - , provenance :: Provenance -- ^ From where this item is imported from. - , label :: T.Text -- ^ Label to display to the user. - , typeText :: Maybe T.Text - , isInfix :: Maybe Backtick -- ^ Did the completion happen + { compKind :: {-# UNPACK #-} !CompletionItemKind + , insertText :: {-# UNPACK #-} !Snippet -- ^ Snippet for the completion + , provenance :: {-# UNPACK #-} !Provenance -- ^ From where this item is imported from. + , label :: {-# UNPACK #-} !T.Text -- ^ Label to display to the user. + , typeText :: !(Maybe T.Text) + , isInfix :: !(Maybe Backtick) -- ^ Did the completion happen -- in the context of an infix notation. - , isTypeCompl :: Bool - , additionalTextEdits :: Maybe ExtendImport - , nameDetails :: Maybe NameDetails -- ^ For resolving purposes - , isLocalCompletion :: Bool -- ^ Is it from this module? + , isTypeCompl :: {-# UNPACK #-} !Bool + , additionalTextEdits :: !(Maybe ExtendImport) + , nameDetails :: !(Maybe NameDetails) -- ^ For resolving purposes + , isLocalCompletion :: {-# UNPACK #-} !Bool -- ^ Is it from this module? } deriving (Eq, Show) +defaultCompletionItemWithLabel :: T.Text -> CompletionItem +defaultCompletionItemWithLabel label = + CompletionItem label def def def def def def def def def + def def def def def def def def def + -- Associates a module's qualifier with its members newtype QualCompls = QualCompls { getQualCompls :: Map.Map T.Text [CompItem] } diff --git a/hls-plugin-api/src/Ide/PluginUtils.hs b/hls-plugin-api/src/Ide/PluginUtils.hs index e34d19f8b0..24b078ec54 100644 --- a/hls-plugin-api/src/Ide/PluginUtils.hs +++ b/hls-plugin-api/src/Ide/PluginUtils.hs @@ -7,7 +7,7 @@ module Ide.PluginUtils extendNextLine, extendLineStart, extendToFullLines, - WithDeletions(..), + WithDeletions (..), getProcessID, makeDiffTextEdit, makeDiffTextEditAdditive, @@ -31,10 +31,12 @@ module Ide.PluginUtils rangesOverlap, positionInRange, usePropertyLsp, + -- * Escape unescape, + -- * toAbsolute - toAbsolute + toAbsolute, ) where @@ -99,7 +101,6 @@ extendLineStart (Range (Position sl _) e) = extendToFullLines :: Range -> Range extendToFullLines = extendLineStart . extendNextLine - -- --------------------------------------------------------------------- data WithDeletions = IncludeDeletions | SkipDeletions @@ -278,7 +279,6 @@ fullRange s = Range startPos endPos subRange :: Range -> Range -> Bool subRange = isSubrangeOf - -- | Check whether the two 'Range's overlap in any way. -- -- >>> rangesOverlap (mkRange 1 0 1 4) (mkRange 1 2 1 5) @@ -306,7 +306,6 @@ allLspCmdIds pid commands = concatMap go commands -- --------------------------------------------------------------------- - type TextParser = P.Parsec Void T.Text -- | Unescape printable escape sequences within double quotes. diff --git a/plugins/hls-refactor-plugin/test/Main.hs b/plugins/hls-refactor-plugin/test/Main.hs index 8c73eab52e..5295022289 100644 --- a/plugins/hls-refactor-plugin/test/Main.hs +++ b/plugins/hls-refactor-plugin/test/Main.hs @@ -189,30 +189,30 @@ completionTests = , testGroup "Data constructor" [ completionCommandTest "not imported" - ["module A where", "import Text.Printf ()", "ZeroPad"] - (Position 2 4) + ["module A where", "import Text.Printf ()", "a = ZeroPad"] + (Position 2 8) "ZeroPad" - ["module A where", "import Text.Printf (FormatAdjustment (ZeroPad))", "ZeroPad"] + ["module A where", "import Text.Printf (FormatAdjustment (ZeroPad))", "a = ZeroPad"] , completionCommandTest "parent imported abs" - ["module A where", "import Text.Printf (FormatAdjustment)", "ZeroPad"] - (Position 2 4) + ["module A where", "import Text.Printf (FormatAdjustment)", "a = ZeroP"] + (Position 2 8) "ZeroPad" - ["module A where", "import Text.Printf (FormatAdjustment (ZeroPad))", "ZeroPad"] + ["module A where", "import Text.Printf (FormatAdjustment (ZeroPad))", "a = ZeroP"] , completionNoCommandTest "parent imported all" - ["module A where", "import Text.Printf (FormatAdjustment (..))", "ZeroPad"] - (Position 2 4) + ["module A where", "import Text.Printf (FormatAdjustment (..))", "a = ZeroP"] + (Position 2 8) "ZeroPad" , completionNoCommandTest "already imported" - ["module A where", "import Text.Printf (FormatAdjustment (ZeroPad))", "ZeroPad"] - (Position 2 4) + ["module A where", "import Text.Printf (FormatAdjustment (ZeroPad))", "a = ZeroP"] + (Position 2 8) "ZeroPad" , completionNoCommandTest "function from Prelude" - ["module A where", "import Data.Maybe ()", "Nothing"] - (Position 2 4) + ["module A where", "import Data.Maybe ()", "a = Nothi"] + (Position 2 8) "Nothing" , completionCommandTest "type operator parent" @@ -224,20 +224,20 @@ completionTests = , testGroup "Record completion" [ completionCommandTest "not imported" - ["module A where", "import Text.Printf ()", "FormatParse"] - (Position 2 10) + ["module A where", "import Text.Printf ()", "a :: FormatParse"] + (Position 2 14) "FormatParse" - ["module A where", "import Text.Printf (FormatParse)", "FormatParse"] + ["module A where", "import Text.Printf (FormatParse)", "a :: FormatParse"] , completionCommandTest "parent imported" - ["module A where", "import Text.Printf (FormatParse)", "FormatParse"] - (Position 2 10) + ["module A where", "import Text.Printf (FormatParse)", "a = FormatParse"] + (Position 2 14) "FormatParse" - ["module A where", "import Text.Printf (FormatParse (FormatParse))", "FormatParse"] + ["module A where", "import Text.Printf (FormatParse (FormatParse))", "a = FormatParse"] , completionNoCommandTest "already imported" - ["module A where", "import Text.Printf (FormatParse (FormatParse))", "FormatParse"] - (Position 2 10) + ["module A where", "import Text.Printf (FormatParse (FormatParse))", "a = FormatP"] + (Position 2 14) "FormatParse" ] , testGroup "Package completion"