Skip to content

Commit 51db1f2

Browse files
authored
Merge pull request #2838 from xsebek/qualified-completion
Fix completion for qualified import
2 parents 7db6215 + 62129bf commit 51db1f2

File tree

3 files changed

+101
-59
lines changed

3 files changed

+101
-59
lines changed

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -215,7 +215,7 @@ getCompletionsLSP ide plId
215215
plugins = idePlugins $ shakeExtras ide
216216
config <- liftIO $ runAction "" ide $ getCompletionsConfig plId
217217

218-
allCompletions <- liftIO $ getCompletions plugins ideOpts cci' parsedMod astres bindMap pfix clientCaps config moduleExports uri
218+
let allCompletions = getCompletions plugins ideOpts cci' parsedMod astres bindMap pfix clientCaps config moduleExports uri
219219
pure $ InL (orderedCompletions allCompletions)
220220
_ -> return (InL [])
221221
_ -> return (InL [])

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

+79-53
Original file line numberDiff line numberDiff line change
@@ -559,10 +559,54 @@ getCompletions
559559
-> CompletionsConfig
560560
-> ModuleNameEnv (HashSet.HashSet IdentInfo)
561561
-> Uri
562-
-> IO [Scored CompletionItem]
563-
getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, qualCompls, importableModules}
564-
maybe_parsed maybe_ast_res (localBindings, bmapping) prefixInfo caps config moduleExportsMap uri = do
565-
let PosPrefixInfo { fullLine, prefixScope, prefixText } = prefixInfo
562+
-> [Scored CompletionItem]
563+
getCompletions
564+
plugins
565+
ideOpts
566+
CC {allModNamesAsNS, anyQualCompls, unqualCompls, qualCompls, importableModules}
567+
maybe_parsed
568+
maybe_ast_res
569+
(localBindings, bmapping)
570+
prefixInfo@(PosPrefixInfo { fullLine, prefixScope, prefixText })
571+
caps
572+
config
573+
moduleExportsMap
574+
uri
575+
-- ------------------------------------------------------------------------
576+
-- IMPORT MODULENAME (NAM|)
577+
| Just (ImportListContext moduleName) <- maybeContext
578+
= moduleImportListCompletions moduleName
579+
580+
| Just (ImportHidingContext moduleName) <- maybeContext
581+
= moduleImportListCompletions moduleName
582+
583+
-- ------------------------------------------------------------------------
584+
-- IMPORT MODULENAM|
585+
| Just (ImportContext _moduleName) <- maybeContext
586+
= filtImportCompls
587+
588+
-- ------------------------------------------------------------------------
589+
-- {-# LA| #-}
590+
-- we leave this condition here to avoid duplications and return empty list
591+
-- since HLS implements these completions (#haskell-language-server/pull/662)
592+
| "{-# " `T.isPrefixOf` fullLine
593+
= []
594+
595+
-- ------------------------------------------------------------------------
596+
| otherwise =
597+
-- assumes that nubOrdBy is stable
598+
let uniqueFiltCompls = nubOrdBy (uniqueCompl `on` snd . Fuzzy.original) filtCompls
599+
compls = (fmap.fmap.fmap) (mkCompl pId ideOpts uri) uniqueFiltCompls
600+
pId = lookupCommandProvider plugins (CommandId extendImportCommandId)
601+
in
602+
(fmap.fmap) snd $
603+
sortBy (compare `on` lexicographicOrdering) $
604+
mergeListsBy (flip compare `on` score)
605+
[ (fmap.fmap) (notQual,) filtModNameCompls
606+
, (fmap.fmap) (notQual,) filtKeywordCompls
607+
, (fmap.fmap.fmap) (toggleSnippets caps config) compls
608+
]
609+
where
566610
enteredQual = if T.null prefixScope then "" else prefixScope <> "."
567611
fullPrefix = enteredQual <> prefixText
568612

@@ -585,11 +629,9 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
585629
$ Fuzzy.simpleFilter chunkSize maxC fullPrefix
586630
$ (if T.null enteredQual then id else mapMaybe (T.stripPrefix enteredQual))
587631
allModNamesAsNS
588-
589-
filtCompls = Fuzzy.filter chunkSize maxC prefixText ctxCompls (label . snd)
590-
where
591-
592-
mcc = case maybe_parsed of
632+
-- If we have a parsed module, use it to determine which completion to show.
633+
maybeContext :: Maybe Context
634+
maybeContext = case maybe_parsed of
593635
Nothing -> Nothing
594636
Just (pm, pmapping) ->
595637
let PositionMapping pDelta = pmapping
@@ -598,7 +640,9 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
598640
hpos = upperRange position'
599641
in getCContext lpos pm <|> getCContext hpos pm
600642

601-
643+
filtCompls :: [Scored (Bool, CompItem)]
644+
filtCompls = Fuzzy.filter chunkSize maxC prefixText ctxCompls (label . snd)
645+
where
602646
-- We need the hieast to be "fresh". We can't get types from "stale" hie files, so hasfield won't work,
603647
-- since it gets the record fields from the types.
604648
-- Perhaps this could be fixed with a refactor to GHC's IfaceTyCon, to have it also contain record fields.
@@ -636,7 +680,7 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
636680
})
637681

638682
-- completions specific to the current context
639-
ctxCompls' = case mcc of
683+
ctxCompls' = case maybeContext of
640684
Nothing -> compls
641685
Just TypeContext -> filter ( isTypeCompl . snd) compls
642686
Just ValueContext -> filter (not . isTypeCompl . snd) compls
@@ -677,54 +721,36 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
677721
, enteredQual `T.isPrefixOf` original label
678722
]
679723

724+
moduleImportListCompletions :: String -> [Scored CompletionItem]
725+
moduleImportListCompletions moduleNameS =
726+
let moduleName = T.pack moduleNameS
727+
funcs = lookupWithDefaultUFM moduleExportsMap HashSet.empty $ mkModuleName moduleNameS
728+
funs = map (show . name) $ HashSet.toList funcs
729+
in filterModuleExports moduleName $ map T.pack funs
730+
731+
filtImportCompls :: [Scored CompletionItem]
680732
filtImportCompls = filtListWith (mkImportCompl enteredQual) importableModules
733+
734+
filterModuleExports :: T.Text -> [T.Text] -> [Scored CompletionItem]
681735
filterModuleExports moduleName = filtListWith $ mkModuleFunctionImport moduleName
736+
737+
filtKeywordCompls :: [Scored CompletionItem]
682738
filtKeywordCompls
683739
| T.null prefixScope = filtListWith mkExtCompl (optKeywords ideOpts)
684740
| otherwise = []
685741

686-
if
687-
-- TODO: handle multiline imports
688-
| "import " `T.isPrefixOf` fullLine
689-
&& (List.length (words (T.unpack fullLine)) >= 2)
690-
&& "(" `isInfixOf` T.unpack fullLine
691-
-> do
692-
let moduleName = words (T.unpack fullLine) !! 1
693-
funcs = lookupWithDefaultUFM moduleExportsMap HashSet.empty $ mkModuleName moduleName
694-
funs = map (renderOcc . name) $ HashSet.toList funcs
695-
return $ filterModuleExports (T.pack moduleName) funs
696-
| "import " `T.isPrefixOf` fullLine
697-
-> return filtImportCompls
698-
-- we leave this condition here to avoid duplications and return empty list
699-
-- since HLS implements these completions (#haskell-language-server/pull/662)
700-
| "{-# " `T.isPrefixOf` fullLine
701-
-> return []
702-
| otherwise -> do
703-
-- assumes that nubOrdBy is stable
704-
let uniqueFiltCompls = nubOrdBy (uniqueCompl `on` snd . Fuzzy.original) filtCompls
705-
let compls = (fmap.fmap.fmap) (mkCompl pId ideOpts uri) uniqueFiltCompls
706-
pId = lookupCommandProvider plugins (CommandId extendImportCommandId)
707-
return $
708-
(fmap.fmap) snd $
709-
sortBy (compare `on` lexicographicOrdering) $
710-
mergeListsBy (flip compare `on` score)
711-
[ (fmap.fmap) (notQual,) filtModNameCompls
712-
, (fmap.fmap) (notQual,) filtKeywordCompls
713-
, (fmap.fmap.fmap) (toggleSnippets caps config) compls
714-
]
715-
where
716-
-- We use this ordering to alphabetically sort suggestions while respecting
717-
-- all the previously applied ordering sources. These are:
718-
-- 1. Qualified suggestions go first
719-
-- 2. Fuzzy score ranks next
720-
-- 3. In-scope completions rank next
721-
-- 4. label alphabetical ordering next
722-
-- 4. detail alphabetical ordering (proxy for module)
723-
lexicographicOrdering Fuzzy.Scored{score, original} =
724-
case original of
725-
(isQual, CompletionItem{_label,_detail}) -> do
726-
let isLocal = maybe False (":" `T.isPrefixOf`) _detail
727-
(Down isQual, Down score, Down isLocal, _label, _detail)
742+
-- We use this ordering to alphabetically sort suggestions while respecting
743+
-- all the previously applied ordering sources. These are:
744+
-- 1. Qualified suggestions go first
745+
-- 2. Fuzzy score ranks next
746+
-- 3. In-scope completions rank next
747+
-- 4. label alphabetical ordering next
748+
-- 4. detail alphabetical ordering (proxy for module)
749+
lexicographicOrdering Fuzzy.Scored{score, original} =
750+
case original of
751+
(isQual, CompletionItem{_label,_detail}) -> do
752+
let isLocal = maybe False (":" `T.isPrefixOf`) _detail
753+
(Down isQual, Down score, Down isLocal, _label, _detail)
728754

729755

730756

plugins/hls-refactor-plugin/test/Main.hs

+21-5
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,25 @@ completionTests =
176176
"join"
177177
["{-# LANGUAGE NoImplicitPrelude #-}",
178178
"module A where", "import Control.Monad as M ()", "import Control.Monad as N (join)", "f = N.joi"]
179+
-- Regression test for https://github.com/haskell/haskell-language-server/issues/2824
180+
, completionNoCommandTest
181+
"explicit qualified"
182+
["{-# LANGUAGE NoImplicitPrelude #-}",
183+
"module A where", "import qualified Control.Monad as M (j)"]
184+
(Position 2 38)
185+
"join"
186+
, completionNoCommandTest
187+
"explicit qualified post"
188+
["{-# LANGUAGE NoImplicitPrelude, ImportQualifiedPost #-}",
189+
"module A where", "import Control.Monad qualified as M (j)"]
190+
(Position 2 38)
191+
"join"
192+
, completionNoCommandTest
193+
"multiline import"
194+
[ "{-# LANGUAGE NoImplicitPrelude #-}"
195+
, "module A where", "import Control.Monad", " (fore)"]
196+
(Position 3 9)
197+
"forever"
179198
]
180199
, testGroup "Data constructor"
181200
[ completionCommandTest
@@ -288,11 +307,8 @@ completionNoCommandTest name src pos wanted = testSession name $ do
288307
docId <- createDoc "A.hs" "haskell" (T.unlines src)
289308
_ <- waitForDiagnostics
290309
compls <- getCompletions docId pos
291-
let wantedC = find ( \case
292-
CompletionItem {_insertText = Just x} -> wanted `T.isPrefixOf` x
293-
_ -> False
294-
) compls
295-
case wantedC of
310+
let isPrefixOfInsertOrLabel ci = any (wanted `T.isPrefixOf`) [fromMaybe "" (ci ^. L.insertText), ci ^. L.label]
311+
case find isPrefixOfInsertOrLabel compls of
296312
Nothing ->
297313
liftIO $ assertFailure $ "Cannot find expected completion in: " <> show [_label | CompletionItem {_label} <- compls]
298314
Just CompletionItem{..} -> liftIO . assertBool ("Expected no command but got: " <> show _command) $ null _command

0 commit comments

Comments
 (0)