@@ -10,16 +10,18 @@ module Development.IDE.Plugin.Completions.Logic (
10
10
, localCompletionsForParsedModule
11
11
, getCompletions
12
12
, fromIdentInfo
13
+ , getCompletionPrefix
13
14
) where
14
15
15
16
import Control.Applicative
16
- import Data.Char (isUpper )
17
+ import Data.Char (isAlphaNum , isUpper )
17
18
import Data.Generics
18
19
import Data.List.Extra as List hiding
19
20
(stripPrefix )
20
21
import qualified Data.Map as Map
21
22
22
- import Data.Maybe (fromMaybe , isJust ,
23
+ import Data.Maybe (catMaybes , fromMaybe ,
24
+ isJust , listToMaybe ,
23
25
mapMaybe )
24
26
import qualified Data.Text as T
25
27
import qualified Text.Fuzzy.Parallel as Fuzzy
@@ -30,6 +32,7 @@ import Data.Either (fromRight)
30
32
import Data.Function (on )
31
33
import Data.Functor
32
34
import qualified Data.HashMap.Strict as HM
35
+
33
36
import qualified Data.HashSet as HashSet
34
37
import Data.Monoid (First (.. ))
35
38
import Data.Ord (Down (Down ))
@@ -67,6 +70,11 @@ import qualified Language.LSP.VFS as VFS
67
70
import Text.Fuzzy.Parallel (Scored (score ),
68
71
original )
69
72
73
+ import qualified Data.Text.Utf16.Rope as Rope
74
+ import Development.IDE
75
+
76
+ import Development.IDE.Spans.AtPoint (pointCommand )
77
+
70
78
-- Chunk size used for parallelizing fuzzy matching
71
79
chunkSize :: Int
72
80
chunkSize = 1000
@@ -564,28 +572,29 @@ getCompletions
564
572
-> IdeOptions
565
573
-> CachedCompletions
566
574
-> Maybe (ParsedModule , PositionMapping )
575
+ -> Maybe (HieAstResult , PositionMapping )
567
576
-> (Bindings , PositionMapping )
568
- -> VFS. PosPrefixInfo
577
+ -> PosPrefixInfo
569
578
-> ClientCapabilities
570
579
-> CompletionsConfig
571
580
-> HM. HashMap T. Text (HashSet. HashSet IdentInfo )
572
581
-> IO [Scored CompletionItem ]
573
582
getCompletions 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 <> " ."
577
586
fullPrefix = enteredQual <> prefixText
578
587
579
588
-- Boolean labels to tag suggestions as qualified (or not)
580
- qual = not (T. null prefixModule )
589
+ qual = not (T. null prefixScope )
581
590
notQual = False
582
591
583
592
{- correct the position by moving 'foo :: Int -> String -> '
584
593
^
585
594
to 'foo :: Int -> String -> '
586
595
^
587
596
-}
588
- pos = VFS. cursorPos prefixInfo
597
+ pos = cursorPos prefixInfo
589
598
590
599
maxC = maxCompletions config
591
600
@@ -608,6 +617,42 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
608
617
hpos = upperRange position'
609
618
in getCContext lpos pm <|> getCContext hpos pm
610
619
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
+
611
656
-- completions specific to the current context
612
657
ctxCompls' = case mcc of
613
658
Nothing -> compls
@@ -618,10 +663,10 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
618
663
ctxCompls = (fmap . fmap ) (\ comp -> toggleAutoExtend config $ comp { isInfix = infixCompls }) ctxCompls'
619
664
620
665
infixCompls :: Maybe Backtick
621
- infixCompls = isUsedAsInfix fullLine prefixModule prefixText pos
666
+ infixCompls = isUsedAsInfix fullLine prefixScope prefixText pos
622
667
623
668
PositionMapping bDelta = bmapping
624
- oldPos = fromDelta bDelta $ VFS. cursorPos prefixInfo
669
+ oldPos = fromDelta bDelta $ cursorPos prefixInfo
625
670
startLoc = lowerRange oldPos
626
671
endLoc = upperRange oldPos
627
672
localCompls = map (uncurry localBindsToCompItem) $ getFuzzyScope localBindings startLoc endLoc
@@ -634,10 +679,14 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
634
679
ty = showForSnippet <$> typ
635
680
thisModName = Local $ nameSrcSpan name
636
681
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
641
690
642
691
filtListWith f list =
643
692
[ fmap f label
@@ -648,7 +697,7 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
648
697
filtImportCompls = filtListWith (mkImportCompl enteredQual) importableModules
649
698
filterModuleExports moduleName = filtListWith $ mkModuleFunctionImport moduleName
650
699
filtKeywordCompls
651
- | T. null prefixModule = filtListWith mkExtCompl (optKeywords ideOpts)
700
+ | T. null prefixScope = filtListWith mkExtCompl (optKeywords ideOpts)
652
701
| otherwise = []
653
702
654
703
if
@@ -696,6 +745,7 @@ getCompletions plugins ideOpts CC {allModNamesAsNS, anyQualCompls, unqualCompls,
696
745
697
746
698
747
748
+
699
749
uniqueCompl :: CompItem -> CompItem -> Ordering
700
750
uniqueCompl candidate unique =
701
751
case compare (label candidate, compKind candidate)
@@ -892,3 +942,32 @@ mergeListsBy cmp all_lists = merge_lists all_lists
892
942
[] -> []
893
943
[xs] -> xs
894
944
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