Skip to content

Commit 45f9742

Browse files
authored
Improve completion contexts (#495)
The completion context determines whether we show completions for types or completions for values. This is done by looking at the parsed module. This PR fixes two things: 1. While we only use the parsed module for getting the context previously we got the parsed module out of the typechecked module. This means that if you have a module that parses but doesn’t typecheck, we will use the parsed module at the point where it last typechecked which is out of date and produces incorrect (or just no) contexts. 2. When we could not find a context, we defaulted to assuming we are in a value context. Especially in combination with 1 but also just in general, this is rather annoying. If we aren’t sure we should show the user everything we have and not filter out some completions. Filtering out completions interacts particularly badly with VSCode’s default behavior of accepting the first completion when you press return.
1 parent 07a5d32 commit 45f9742

File tree

3 files changed

+44
-20
lines changed

3 files changed

+44
-20
lines changed

src/Development/IDE/Plugin/Completions.hs

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
module Development.IDE.Plugin.Completions(plugin) where
44

5+
import Control.Applicative
6+
import Data.Maybe
57
import Language.Haskell.LSP.Messages
68
import Language.Haskell.LSP.Types
79
import qualified Language.Haskell.LSP.Core as LSP
@@ -10,8 +12,6 @@ import Language.Haskell.LSP.Types.Capabilities
1012
import Development.Shake.Classes
1113
import Development.Shake
1214
import GHC.Generics
13-
import Data.Maybe
14-
import HscTypes
1515

1616
import Development.IDE.Plugin
1717
import Development.IDE.Core.Service
@@ -37,14 +37,14 @@ produceCompletions =
3737
packageState <- fmap (hscEnv . fst) <$> useWithStale GhcSession file
3838
case (tm, packageState) of
3939
(Just tm', Just packageState') -> do
40-
cdata <- liftIO $ cacheDataProducer packageState' (hsc_dflags packageState')
40+
cdata <- liftIO $ cacheDataProducer packageState'
4141
(tmrModule tm') parsedDeps
42-
return ([], Just (cdata, tm'))
42+
return ([], Just cdata)
4343
_ -> return ([], Nothing)
4444

4545

4646
-- | Produce completions info for a file
47-
type instance RuleResult ProduceCompletions = (CachedCompletions, TcModuleResult)
47+
type instance RuleResult ProduceCompletions = CachedCompletions
4848

4949
data ProduceCompletions = ProduceCompletions
5050
deriving (Eq, Show, Typeable, Generic)
@@ -67,17 +67,21 @@ getCompletionsLSP lsp ide
6767
fmap Right $ case (contents, uriToFilePath' uri) of
6868
(Just cnts, Just path) -> do
6969
let npath = toNormalizedFilePath path
70-
(ideOpts, compls) <- runAction ide ((,) <$> getIdeOptions <*> useWithStale ProduceCompletions npath)
70+
(ideOpts, compls) <- runAction ide $ do
71+
opts <- getIdeOptions
72+
compls <- useWithStale ProduceCompletions npath
73+
pm <- useWithStale GetParsedModule npath
74+
pure (opts, liftA2 (,) compls pm)
7175
case compls of
72-
Just ((cci', tm'), mapping) -> do
73-
let position' = fromCurrentPosition mapping position
76+
Just ((cci', _), (pm, mapping)) -> do
77+
let !position' = fromCurrentPosition mapping position
7478
pfix <- maybe (return Nothing) (flip VFS.getCompletionPrefix cnts) position'
7579
case (pfix, completionContext) of
7680
(Just (VFS.PosPrefixInfo _ "" _ _), Just CompletionContext { _triggerCharacter = Just "."})
7781
-> return (Completions $ List [])
7882
(Just pfix', _) -> do
7983
let fakeClientCapabilities = ClientCapabilities Nothing Nothing Nothing Nothing
80-
Completions . List <$> getCompletions ideOpts cci' (tmrModule tm') pfix' fakeClientCapabilities (WithSnippets True)
84+
Completions . List <$> getCompletions ideOpts cci' pm pfix' fakeClientCapabilities (WithSnippets True)
8185
_ -> return (Completions $ List [])
8286
_ -> return (Completions $ List [])
8387
_ -> return (Completions $ List [])

src/Development/IDE/Plugin/Completions/Logic.hs

Lines changed: 10 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -210,9 +210,10 @@ mkPragmaCompl label insertText =
210210
Nothing Nothing Nothing Nothing Nothing (Just insertText) (Just Snippet)
211211
Nothing Nothing Nothing Nothing Nothing
212212

213-
cacheDataProducer :: HscEnv -> DynFlags -> TypecheckedModule -> [ParsedModule] -> IO CachedCompletions
214-
cacheDataProducer packageState dflags tm deps = do
213+
cacheDataProducer :: HscEnv -> TypecheckedModule -> [ParsedModule] -> IO CachedCompletions
214+
cacheDataProducer packageState tm deps = do
215215
let parsedMod = tm_parsed_module tm
216+
dflags = hsc_dflags packageState
216217
curMod = moduleName $ ms_mod $ pm_mod_summary parsedMod
217218
Just (_,limports,_,_) = tm_renamed_source tm
218219

@@ -306,16 +307,13 @@ toggleSnippets ClientCapabilities { _textDocument } (WithSnippets with) x
306307
where supported = Just True == (_textDocument >>= _completion >>= _completionItem >>= _snippetSupport)
307308

308309
-- | Returns the cached completions for the given module and position.
309-
getCompletions :: IdeOptions -> CachedCompletions -> TypecheckedModule -> VFS.PosPrefixInfo -> ClientCapabilities -> WithSnippets -> IO [CompletionItem]
310+
getCompletions :: IdeOptions -> CachedCompletions -> ParsedModule -> VFS.PosPrefixInfo -> ClientCapabilities -> WithSnippets -> IO [CompletionItem]
310311
getCompletions ideOpts CC { allModNamesAsNS, unqualCompls, qualCompls, importableModules }
311-
tm prefixInfo caps withSnippets = do
312+
pm prefixInfo caps withSnippets = do
312313
let VFS.PosPrefixInfo { VFS.fullLine, VFS.prefixModule, VFS.prefixText } = prefixInfo
313314
enteredQual = if T.null prefixModule then "" else prefixModule <> "."
314315
fullPrefix = enteredQual <> prefixText
315316

316-
-- default to value context if no explicit context
317-
context = fromMaybe ValueContext $ getCContext pos (tm_parsed_module tm)
318-
319317
{- correct the position by moving 'foo :: Int -> String -> '
320318
^
321319
to 'foo :: Int -> String -> '
@@ -344,10 +342,11 @@ getCompletions ideOpts CC { allModNamesAsNS, unqualCompls, qualCompls, importabl
344342
where
345343
isTypeCompl = isTcOcc . occName . origName
346344
-- completions specific to the current context
347-
ctxCompls' = case context of
348-
TypeContext -> filter isTypeCompl compls
349-
ValueContext -> filter (not . isTypeCompl) compls
350-
_ -> filter (not . isTypeCompl) compls
345+
ctxCompls' = case getCContext pos pm of
346+
Nothing -> compls
347+
Just TypeContext -> filter isTypeCompl compls
348+
Just ValueContext -> filter (not . isTypeCompl) compls
349+
Just _ -> filter (not . isTypeCompl) compls
351350
-- Add whether the text to insert has backticks
352351
ctxCompls = map (\comp -> comp { isInfix = infixCompls }) ctxCompls'
353352

test/exe/Main.hs

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1630,6 +1630,27 @@ completionTests
16301630
docId <- openDoc' "A.hs" "haskell" source
16311631
compls <- getCompletions docId (Position 1 9)
16321632
liftIO $ compls @?= [keywordItem "newtype"]
1633+
, testSessionWait "type context" $ do
1634+
let source = T.unlines
1635+
[ "{-# OPTIONS_GHC -Wunused-binds #-}"
1636+
, "module A () where"
1637+
, "f = f"
1638+
]
1639+
docId <- openDoc' "A.hs" "haskell" source
1640+
expectDiagnostics [("A.hs", [(DsWarning, (2, 0), "not used")])]
1641+
changeDoc docId
1642+
[ TextDocumentContentChangeEvent Nothing Nothing $ T.unlines
1643+
[ "{-# OPTIONS_GHC -Wunused-binds #-}"
1644+
, "module A () where"
1645+
, "f = f"
1646+
, "g :: Intege"
1647+
]
1648+
]
1649+
-- At this point the module parses but does not typecheck.
1650+
-- This should be sufficient to detect that we are in a
1651+
-- type context and only show the completion to the type.
1652+
compls <- getCompletions docId (Position 3 11)
1653+
liftIO $ map dropDocs compls @?= [complItem "Integer"(Just CiStruct) (Just "*")]
16331654
]
16341655
where
16351656
dropDocs :: CompletionItem -> CompletionItem

0 commit comments

Comments
 (0)