Skip to content

Commit 35ee61a

Browse files
committed
Don't output module snippets when present
1 parent 0779306 commit 35ee61a

5 files changed

Lines changed: 111 additions & 35 deletions

File tree

ghcide-test/exe/CompletionTests.hs

Lines changed: 19 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -625,8 +625,6 @@ contextCompletionTests =
625625
(Position 4 8)
626626
[("Xxxtype", CompletionItemKind_Struct, "Xxxtype", False, True, Nothing)]
627627

628-
-- where-clause / local binding context tests
629-
630628
, completionTest
631629
"type sig in where-clause gives type completions"
632630
[ "{-# OPTIONS_GHC -Wunused-binds #-}"
@@ -726,6 +724,25 @@ contextCompletionTests =
726724
]
727725
(Position 5 19) -- after "Xxx" in " let helper :: Xxx"
728726
[("Xxxtype", CompletionItemKind_Struct, "Xxxtype", False, True, Nothing)]
727+
728+
, testSessionSingleFile "module header snippet shown when no module declaration" "A.hs"
729+
[ "mod"
730+
] $ do
731+
doc <- openDoc "A.hs" "haskell"
732+
_ <- waitForDiagnostics
733+
compls <- getCompletions doc (Position 0 3)
734+
let moduleSnippets = filterSnippetsLabel "module" compls
735+
liftIO $ length moduleSnippets @?= 1
736+
737+
, testSessionSingleFile "module header snippet not shown when module declaration exists" "A.hs"
738+
[ "module A where"
739+
, "mod"
740+
] $ do
741+
doc <- openDoc "A.hs" "haskell"
742+
_ <- waitForDiagnostics
743+
compls <- getCompletions doc (Position 1 3)
744+
let moduleSnippets = filterSnippetsLabel "module" compls
745+
liftIO $ moduleSnippets @?= []
729746
]
730747
where
731748
filterSnippetsLabel l snippets =

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

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -216,9 +216,10 @@ getCompletionsLSP recorder ide plId
216216
let clientCaps = clientCapabilities $ shakeExtras ide
217217
plugins = idePlugins $ shakeExtras ide
218218
context = deduceContext ctxTree (cursorPos pfix)
219+
hasModuleHeader = maybe False (contextHasModuleHeader . fst) ctxTree
219220
config <- liftIO $ runAction "" ide $ getCompletionsConfig plId
220221

221-
let allCompletions = getCompletions plugins ideOpts cci' context astres bindMap pfix clientCaps config moduleExports uri
222+
let allCompletions = getCompletions plugins ideOpts cci' context hasModuleHeader astres bindMap pfix clientCaps config moduleExports uri
222223
logWith recorder Debug $ LogDetectedContext context
223224
pure $ InL (orderedCompletions allCompletions)
224225
_ -> return (InL [])

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

Lines changed: 79 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -8,15 +8,24 @@ module Development.IDE.Plugin.Completions.Context
88
, ContextGroup (..)
99
, ContextMap
1010
, GetContextMap (..)
11+
, contextHasModuleHeader
1112
, getContext
1213
, getContextMap
1314
) where
1415

1516
import Control.DeepSeq (NFData (..), rwhnf)
16-
import Data.Generics (extQ, mkQ)
17-
import Data.Generics.Schemes (everythingBut)
17+
import Data.Generics (Data (..), GenericQ,
18+
extQ, mkQ)
1819
import Data.Hashable (Hashable)
19-
import Data.Maybe (mapMaybe)
20+
import Data.List.Extra (nubOrd)
21+
import Data.Maybe (fromJust, isJust,
22+
mapMaybe)
23+
import Data.List.Extra (nubOrd)
24+
import Data.Maybe (isJust, mapMaybe,
25+
maybeToList)
26+
import Data.List.Extra (nub)
27+
import Data.Maybe (isJust, mapMaybe,
28+
maybeToList)
2029
import qualified Data.Text as T
2130
import Development.IDE
2231
import Development.IDE.Core.PositionMapping
@@ -68,31 +77,38 @@ type instance RuleResult GetContextMap = ContextMap
6877
data ContextChunk = Chunk
6978
{ low :: {-# UNPACK #-} !Position
7079
, high :: {-# UNPACK #-} !Position
71-
, group :: {-# UNPACK #-} !ContextGroup
80+
, group :: !ContextGroup
7281
, context :: Range -> ContextResult
7382
}
7483

7584
-- | Build lazy 'ContextChunk' by processing @n@ source items at a time.
76-
groupedChunks :: Int -> ContextGroup -> (a -> Maybe Range) -> (a -> Range -> ContextResult) -> [a] -> ContextMap
77-
groupedChunks n group getPos locate xs = ContextMap $ go xs
85+
groupedChunks :: Int -> ContextGroup -> (a -> Maybe Range) -> (a -> Range -> ContextResult) -> [a] -> [ContextChunk]
86+
groupedChunks n group getPos locate xs = go xs
7887
where
7988
go [] = []
8089
go xs =
8190
let (chunk, rest) = splitAt n xs
8291
context = foldMap locate chunk
83-
in case chunk of
92+
positions = mapMaybe getPos chunk
93+
in case positions of
8494
[] -> go rest
85-
_ -> Chunk
86-
{ low = minimum (mapMaybe (fmap _start . getPos) chunk)
87-
, high = maximum (mapMaybe (fmap _end . getPos) chunk)
95+
ps -> Chunk
96+
{ low = minimum (fmap _start ps)
97+
, high = maximum (fmap _end ps)
8898
, group
8999
, context
90100
} : go rest
91101

102+
-- | Build lazy 'ContextChunk' by processing @n@ source items at a time.
103+
singletonChunk :: ContextGroup -> (a -> Maybe Range) -> (a -> Range -> ContextResult) -> a -> ContextChunk
104+
singletonChunk group getPos locate inp = Chunk s e group (locate inp)
105+
where
106+
Range s e = fromJust $ getPos inp
107+
92108
-- | Used during context finding, combines into the tightest interval.
93109
-- As an intuition, the primary interface is through
94-
-- @Monoid (Position -> ContextResult)@.
95-
data ContextResult = NoContext | ContextResult Range Context
110+
-- @Monoid (Range -> ContextResult)@.
111+
data ContextResult = NoContext | ContextResult !Range !Context
96112
instance Monoid ContextResult where mempty = NoContext
97113
instance Semigroup ContextResult where (<>) = tighten
98114

@@ -102,8 +118,14 @@ tighten a NoContext = a
102118
tighten ar@(ContextResult a _) br@(ContextResult b _) =
103119
if a `dominates` b then br else ar
104120

105-
newtype ContextMap = ContextMap [ContextChunk]
106-
deriving newtype (Monoid, Semigroup)
121+
-- | A context map, built from a parsed module. Stores whether the module
122+
-- already has a @module ... where@ header, so that the header snippet can
123+
-- be suppressed for files that already declare a module.
124+
data ContextMap = ContextMap !Bool [ContextChunk]
125+
instance Semigroup ContextMap where
126+
ContextMap h1 c1 <> ContextMap h2 c2 = ContextMap (h1 || h2) (c1 <> c2)
127+
instance Monoid ContextMap where
128+
mempty = ContextMap False []
107129
instance Show ContextMap where show _ = "<context map>"
108130
instance NFData ContextMap where rnf = rwhnf
109131

@@ -116,10 +138,14 @@ instance NFData ContextMap where rnf = rwhnf
116138
-- as a Shake rule.
117139
getContextMap :: ParsedModule -> ContextMap
118140
getContextMap pm =
119-
groupedChunks 10 ImportGroup rangeOf getImportContext hsmodImports
120-
<> groupedChunks 5 DeclarationGroup rangeOf getDeclContext hsmodDecls
141+
ContextMap (isJust hsmodName) $
142+
-- These denote the size of the "jumps" of the cursor when traversing the AST.
143+
-- Reduces the amount of data we have to look at with syb.
144+
moduleChunk
145+
<> groupedChunks 10 ImportGroup rangeOf getImportContext hsmodImports
146+
<> groupedChunks 4 DeclarationGroup rangeOf getDeclContext hsmodDecls
121147
where
122-
HsModule {hsmodImports, hsmodDecls} =
148+
HsModule {hsmodName, hsmodImports, hsmodDecls} =
123149
unLoc (pm_parsed_source pm)
124150

125151
#if MIN_VERSION_ghc(9,9,0)
@@ -130,47 +156,59 @@ rangeOf :: GenLocated (SrcSpanAnn' a) e -> Maybe Range
130156
rangeOf = srcSpanToRange . getLocA
131157
#endif
132158

159+
getHeaderContext :: Data a => a -> Range -> ContextResult
160+
getHeaderContext decl query =
161+
gather
162+
(<>)
163+
((mempty, False) `mkQ` modNameQ query)
164+
decl
165+
133166
getImportContext :: LImportDecl GhcPs -> Range -> ContextResult
134167
getImportContext imports query =
135-
everythingBut
168+
gather
136169
(<>)
137170
((mempty, False) `mkQ` importQ query)
138171
imports
139172

140173
getDeclContext :: LHsDecl GhcPs -> Range -> ContextResult
141174
getDeclContext declarations query =
142-
everythingBut
175+
gather
143176
(<>)
144177
((mempty, False) `mkQ` sigQ query `extQ` bindQ query `extQ` declQ query)
145178
declarations
146179

147180
-- * Querying
148181

182+
-- | Returns 'True' when the parsed module already has a @module ... where@
183+
-- declaration. Used downstream to suppress the module header snippet.
184+
contextHasModuleHeader :: ContextMap -> Bool
185+
contextHasModuleHeader (ContextMap h _) = h
186+
149187
-- | Look up the completion context at a given position.
150188
-- Returns the innermost (most specific) context that contains the position.
151189
--
152190
-- Only the 'ContextChunks' up to and including the chunk containing the
153191
-- query position are forced; later chunks remain as unevaluated thunks.
154192
getContext :: ContextMap -> PositionResult Position -> Context
155-
getContext (ContextMap chunks) query =
156-
case searchChunks True chunks of
157-
(groups, NoContext) -> TopContext groups
193+
getContext (ContextMap _ chunks) query =
194+
case searchChunks HeaderGroup chunks mempty of
195+
(groups, NoContext) -> TopContext $ nub groups
158196
(_, ContextResult _ found) -> found
159197
where
160198
(qLo, qHi) = case query of
161199
PositionExact p -> (p, p)
162200
PositionRange l u -> (l, u)
163201

164-
searchChunks :: Bool -> [ContextChunk] -> ([ContextGroup], ContextResult)
165-
searchChunks _ [] = ([], mempty)
166-
searchChunks firstChunk (Chunk cLo cHi group contextOf : rest)
202+
searchChunks :: ContextGroup -> [ContextChunk] -> ([ContextGroup], ContextResult) -> ([ContextGroup], ContextResult)
203+
searchChunks _ [] !acc = acc
204+
searchChunks lastChunk (Chunk cLo cHi group contextOf : rest) !acc
167205
| -- query is past this chunk (line-only comparison so cursors
168206
-- past the last column on the final line still match)
169-
_line qLo > _line cHi = searchChunks False rest
207+
_line qLo > _line cHi = searchChunks group rest acc
170208
| -- query is before this chunk
171-
qHi < cLo = (if firstChunk then [HeaderGroup] else [], mempty)
209+
qHi < cLo = ([lastChunk, group], mempty) <> acc
172210
-- this chunk is relevant, emit the group and all relevant intervals
173-
| otherwise = ([group], contextOf (Range qLo qHi)) <> searchChunks False rest
211+
| otherwise = searchChunks group rest (([group], contextOf (Range qLo qHi)) <> acc)
174212

175213
-- * SYB queries types
176214

@@ -200,6 +238,9 @@ declQ query (L (locA -> ss) decl) = case srcSpanToRange ss of
200238
SpliceD {} -> (ContextResult range (TopContext []), True)
201239
_ -> (ContextResult range DefaultContext, True) -- DefD, WarningD, AnnD, RuleD, DocD, KindSigD
202240

241+
modNameQ :: Range -> XRec GhcPs ModuleName -> (ContextResult, Bool)
242+
modNameQ = contextual (TopContext [HeaderGroup]) True
243+
203244
sigQ :: Range -> LSig GhcPs -> (ContextResult, Bool)
204245
sigQ = contextual TypeContext True
205246

@@ -247,3 +288,13 @@ instance Pretty ContextGroup where
247288
HeaderGroup -> "header"
248289
ImportGroup -> "imports"
249290
DeclarationGroup -> "declarations"
291+
292+
-- | Variation of @Data.Generics.Schemes.everythingBut@, but uses foldl'.
293+
gather :: forall r. (r -> r -> r) -> GenericQ (r, Bool) -> GenericQ r
294+
gather k f = go
295+
where
296+
go :: GenericQ r
297+
go x = let (v, stop) = f x
298+
in if stop
299+
then v
300+
else foldl' k v (gmapQ go x)

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -453,6 +453,7 @@ getCompletions
453453
-> IdeOptions
454454
-> CachedCompletions
455455
-> Context
456+
-> Bool
456457
-> Maybe (HieAstResult, PositionMapping)
457458
-> (Bindings, PositionMapping)
458459
-> PosPrefixInfo
@@ -466,6 +467,7 @@ getCompletions
466467
ideOpts
467468
CC {allModNamesAsNS, anyQualCompls, unqualCompls, qualCompls, importableModules}
468469
context
470+
hasModuleHeader
469471
maybe_ast_res
470472
(localBindings, bmapping)
471473
prefixInfo@(PosPrefixInfo { fullLine, prefixScope, prefixText })
@@ -636,7 +638,7 @@ getCompletions
636638
filtTopContextCompls :: [Context.ContextGroup] -> [Scored CompletionItem]
637639
filtTopContextCompls groups
638640
| T.null prefixScope
639-
= Fuzzy.filter chunkSize maxC fullPrefix (getContextSnippets groups) (view L.label)
641+
= Fuzzy.filter chunkSize maxC fullPrefix (getContextSnippets hasModuleHeader groups) (view L.label)
640642
| otherwise = []
641643

642644
-- We use this ordering to alphabetically sort suggestions while respecting

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

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,14 @@ data SnippetCompletion = SnippetCompletion
1818
, snippetContents :: {-# UNPACK #-} !Text
1919
}
2020

21-
getContextSnippets :: [ContextGroup] -> [CompletionItem]
22-
getContextSnippets [] = concatMap (fmap mkSnippetCompletion . snd) topContextSnippets
23-
getContextSnippets groups = concatMap (fmap mkSnippetCompletion . concat . maybeToList . (`lookup` topContextSnippets)) groups
21+
getContextSnippets :: Bool -> [ContextGroup] -> [CompletionItem]
22+
getContextSnippets hasModuleHeader groups =
23+
let tbl = if hasModuleHeader
24+
then filter ((/= HeaderGroup) . fst) topContextSnippets
25+
else topContextSnippets
26+
in fmap mkSnippetCompletion $ case groups of
27+
[] -> concatMap snd tbl
28+
_ -> concatMap (concat . maybeToList . (`lookup` tbl)) groups
2429

2530
topContextSnippets :: [(ContextGroup, [SnippetCompletion])]
2631
topContextSnippets =

0 commit comments

Comments
 (0)