@@ -10,16 +10,18 @@ module Development.IDE.Plugin.Completions.Logic (
1010, localCompletionsForParsedModule
1111, getCompletions
1212, fromIdentInfo
13+ , getCompletionPrefix
1314) where
1415
1516import Control.Applicative
16- import Data.Char (isUpper )
17+ import Data.Char (isAlphaNum , isUpper )
1718import Data.Generics
1819import Data.List.Extra as List hiding
1920 (stripPrefix )
2021import qualified Data.Map as Map
2122
22- import Data.Maybe (fromMaybe , isJust ,
23+ import Data.Maybe (catMaybes , fromMaybe ,
24+ isJust , listToMaybe ,
2325 mapMaybe )
2426import qualified Data.Text as T
2527import qualified Text.Fuzzy.Parallel as Fuzzy
@@ -30,6 +32,7 @@ import Data.Either (fromRight)
3032import Data.Function (on )
3133import Data.Functor
3234import qualified Data.HashMap.Strict as HM
35+
3336import qualified Data.HashSet as HashSet
3437import Data.Monoid (First (.. ))
3538import Data.Ord (Down (Down ))
@@ -67,6 +70,11 @@ import qualified Language.LSP.VFS as VFS
6770import Text.Fuzzy.Parallel (Scored (score ),
6871 original )
6972
73+ import qualified Data.Text.Utf16.Rope as Rope
74+ import Development.IDE
75+
76+ import Development.IDE.Spans.AtPoint (pointCommand )
77+
7078-- Chunk size used for parallelizing fuzzy matching
7179chunkSize :: Int
7280chunkSize = 1000
@@ -564,28 +572,29 @@ getCompletions
564572 -> IdeOptions
565573 -> CachedCompletions
566574 -> Maybe (ParsedModule , PositionMapping )
575+ -> Maybe (HieAstResult , PositionMapping )
567576 -> (Bindings , PositionMapping )
568- -> VFS. PosPrefixInfo
577+ -> PosPrefixInfo
569578 -> ClientCapabilities
570579 -> CompletionsConfig
571580 -> HM. HashMap T. Text (HashSet. HashSet IdentInfo )
572581 -> IO [Scored CompletionItem ]
573582getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls, qualCompls, importableModules}
574- maybe_parsed (localBindings, bmapping) prefixInfo caps config moduleExportsMap = do
575- let VFS. PosPrefixInfo { fullLine, prefixModule , prefixText } = prefixInfo
576- enteredQual = if T. null prefixModule then " " else prefixModule <> " ."
583+ maybe_parsed maybe_ast_res (localBindings, bmapping) prefixInfo caps config moduleExportsMap = do
584+ let PosPrefixInfo { fullLine, prefixScope , prefixText } = prefixInfo
585+ enteredQual = if T. null prefixScope then " " else prefixScope <> " ."
577586 fullPrefix = enteredQual <> prefixText
578587
579588 -- Boolean labels to tag suggestions as qualified (or not)
580- qual = not (T. null prefixModule )
589+ qual = not (T. null prefixScope )
581590 notQual = False
582591
583592 {- correct the position by moving 'foo :: Int -> String -> '
584593 ^
585594 to 'foo :: Int -> String -> '
586595 ^
587596 -}
588- pos = VFS. cursorPos prefixInfo
597+ pos = cursorPos prefixInfo
589598
590599 maxC = maxCompletions config
591600
@@ -608,6 +617,42 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
608617 hpos = upperRange position'
609618 in getCContext lpos pm <|> getCContext hpos pm
610619
620+
621+ -- We need the hieast to be "fresh". We can't get types from "stale" hie files, so hasfield won't work,
622+ -- since it gets the record fields from the types.
623+ -- Perhaps this could be fixed with a refactor to GHC's IfaceTyCon, to have it also contain record fields.
624+ -- Requiring fresh hieast is fine for normal workflows, because it is generated while the user edits.
625+ recordDotSyntaxCompls :: [(Bool , CompItem )]
626+ recordDotSyntaxCompls = case maybe_ast_res of
627+ Just (HAR {hieAst = hieast, hieKind = HieFresh },_) -> concat $ pointCommand hieast (completionPrefixPos prefixInfo) nodeCompletions
628+ _ -> []
629+ where
630+ nodeCompletions :: HieAST Type -> [(Bool , CompItem )]
631+ nodeCompletions node = concatMap g (nodeType $ nodeInfo node)
632+ g :: Type -> [(Bool , CompItem )]
633+ g (TyConApp theTyCon _) = map (dotFieldSelectorToCompl (printOutputable $ GHC. tyConName theTyCon)) $ getSels theTyCon
634+ g _ = []
635+ getSels :: GHC. TyCon -> [T. Text ]
636+ getSels tycon = let f fieldLabel = printOutputable fieldLabel
637+ in map f $ tyConFieldLabels tycon
638+ -- Completions can return more information that just the completion itself, but it will
639+ -- require more than what GHC currently gives us in the HieAST, since it only gives the Type
640+ -- of the fields, not where they are defined, etc. So for now the extra fields remain empty.
641+ -- Also: additionalTextEdits is a todo, since we may want to import the record. It requires a way
642+ -- to get the record's module, which isn't included in the type information used to get the fields.
643+ dotFieldSelectorToCompl :: T. Text -> T. Text -> (Bool , CompItem )
644+ dotFieldSelectorToCompl recname label = (True , CI
645+ { compKind = CiField
646+ , insertText = label
647+ , provenance = DefinedIn recname
648+ , typeText = Nothing
649+ , label = label
650+ , isInfix = Nothing
651+ , docs = emptySpanDoc
652+ , isTypeCompl = False
653+ , additionalTextEdits = Nothing
654+ })
655+
611656 -- completions specific to the current context
612657 ctxCompls' = case mcc of
613658 Nothing -> compls
@@ -618,10 +663,10 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
618663 ctxCompls = (fmap . fmap ) (\ comp -> toggleAutoExtend config $ comp { isInfix = infixCompls }) ctxCompls'
619664
620665 infixCompls :: Maybe Backtick
621- infixCompls = isUsedAsInfix fullLine prefixModule prefixText pos
666+ infixCompls = isUsedAsInfix fullLine prefixScope prefixText pos
622667
623668 PositionMapping bDelta = bmapping
624- oldPos = fromDelta bDelta $ VFS. cursorPos prefixInfo
669+ oldPos = fromDelta bDelta $ cursorPos prefixInfo
625670 startLoc = lowerRange oldPos
626671 endLoc = upperRange oldPos
627672 localCompls = map (uncurry localBindsToCompItem) $ getFuzzyScope localBindings startLoc endLoc
@@ -634,10 +679,14 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
634679 ty = showForSnippet <$> typ
635680 thisModName = Local $ nameSrcSpan name
636681
637- compls = if T. null prefixModule
638- then map (notQual,) localCompls ++ map (qual,) unqualCompls ++ ((notQual,) . ($ Nothing ) <$> anyQualCompls)
639- else ((qual,) <$> Map. findWithDefault [] prefixModule (getQualCompls qualCompls))
640- ++ ((notQual,) . ($ Just prefixModule) <$> anyQualCompls)
682+ -- When record-dot-syntax completions are available, we return them exclusively.
683+ -- They are only available when we write i.e. `myrecord.` with OverloadedRecordDot enabled.
684+ -- Anything that isn't a field is invalid, so those completion don't make sense.
685+ compls
686+ | T. null prefixScope = map (notQual,) localCompls ++ map (qual,) unqualCompls ++ map (\ compl -> (notQual, compl Nothing )) anyQualCompls
687+ | not $ null recordDotSyntaxCompls = recordDotSyntaxCompls
688+ | otherwise = ((qual,) <$> Map. findWithDefault [] prefixScope (getQualCompls qualCompls))
689+ ++ map (\ compl -> (notQual, compl (Just prefixScope))) anyQualCompls
641690
642691 filtListWith f list =
643692 [ fmap f label
@@ -648,7 +697,7 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
648697 filtImportCompls = filtListWith (mkImportCompl enteredQual) importableModules
649698 filterModuleExports moduleName = filtListWith $ mkModuleFunctionImport moduleName
650699 filtKeywordCompls
651- | T. null prefixModule = filtListWith mkExtCompl (optKeywords ideOpts)
700+ | T. null prefixScope = filtListWith mkExtCompl (optKeywords ideOpts)
652701 | otherwise = []
653702
654703 if
@@ -696,6 +745,7 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
696745
697746
698747
748+
699749uniqueCompl :: CompItem -> CompItem -> Ordering
700750uniqueCompl candidate unique =
701751 case compare (label candidate, compKind candidate)
@@ -892,3 +942,32 @@ mergeListsBy cmp all_lists = merge_lists all_lists
892942 [] -> []
893943 [xs] -> xs
894944 lists' -> merge_lists lists'
945+
946+ -- | From the given cursor position, gets the prefix module or record for autocompletion
947+ getCompletionPrefix :: Position -> VFS. VirtualFile -> PosPrefixInfo
948+ getCompletionPrefix pos@ (Position l c) (VFS. VirtualFile _ _ ropetext) =
949+ fromMaybe (PosPrefixInfo " " " " " " pos) $ do -- Maybe monad
950+ let headMaybe = listToMaybe
951+ lastMaybe = headMaybe . reverse
952+
953+ -- grab the entire line the cursor is at
954+ curLine <- headMaybe $ T. lines $ Rope. toText
955+ $ fst $ Rope. splitAtLine 1 $ snd $ Rope. splitAtLine (fromIntegral l) ropetext
956+ let beforePos = T. take (fromIntegral c) curLine
957+ -- the word getting typed, after previous space and before cursor
958+ curWord <-
959+ if | T. null beforePos -> Just " "
960+ | T. last beforePos == ' ' -> Just " " -- don't count abc as the curword in 'abc '
961+ | otherwise -> lastMaybe (T. words beforePos)
962+
963+ let parts = T. split (== ' .' )
964+ $ T. takeWhileEnd (\ x -> isAlphaNum x || x `elem` (" ._'" :: String )) curWord
965+ case reverse parts of
966+ [] -> Nothing
967+ (x: xs) -> do
968+ let modParts = reverse $ filter (not . T. null ) xs
969+ modName = T. intercalate " ." modParts
970+ return $ PosPrefixInfo { fullLine = curLine, prefixScope = modName, prefixText = x, cursorPos = pos }
971+
972+ completionPrefixPos :: PosPrefixInfo -> Position
973+ completionPrefixPos PosPrefixInfo { cursorPos = Position ln co, prefixText = str} = Position ln (co - (fromInteger . toInteger . T. length $ str) - 1 )
0 commit comments