Skip to content

Commit 3464ed8

Browse files
authored
Merge pull request #1246 from berberman/extend-import-list-exactprint
Use exact print to extend import lists
2 parents 759901f + 36573ec commit 3464ed8

File tree

4 files changed

+180
-71
lines changed

4 files changed

+180
-71
lines changed

Diff for: ghcide/src/Development/IDE/GHC/ExactPrint.hs

+6-2
Original file line numberDiff line numberDiff line change
@@ -74,10 +74,10 @@ instance NFData GetAnnotatedParsedSource
7474
instance Binary GetAnnotatedParsedSource
7575
type instance RuleResult GetAnnotatedParsedSource = Annotated ParsedSource
7676

77-
-- | Get the latest version of the annotated parse source.
77+
-- | Get the latest version of the annotated parse source with comments.
7878
getAnnotatedParsedSourceRule :: Rules ()
7979
getAnnotatedParsedSourceRule = define $ \GetAnnotatedParsedSource nfp -> do
80-
pm <- use GetParsedModule nfp
80+
pm <- use GetParsedModuleWithComments nfp
8181
return ([], fmap annotateParsedSource pm)
8282

8383
annotateParsedSource :: ParsedModule -> Annotated ParsedSource
@@ -314,6 +314,10 @@ instance p ~ GhcPs => ASTElement (HsDecl p) where
314314
parseAST = parseDecl
315315
maybeParensAST = id
316316

317+
instance p ~ GhcPs => ASTElement (ImportDecl p) where
318+
parseAST = parseImport
319+
maybeParensAST = id
320+
317321
instance ASTElement RdrName where
318322
parseAST df fp = parseWith df fp parseIdentifier
319323
maybeParensAST = id

Diff for: ghcide/src/Development/IDE/Plugin/CodeAction.hs

+19-58
Original file line numberDiff line numberDiff line change
@@ -116,7 +116,7 @@ codeAction lsp state (TextDocumentIdentifier uri) _range CodeActionContext{_diag
116116
| x <- xs
117117
, Just ps <- [annotatedPS]
118118
, Just dynflags <- [df]
119-
, (title, graft) <- suggestExactAction dynflags ps x
119+
, (title, graft) <- suggestExactAction exportsMap dynflags ps x
120120
, let edit = either error id $
121121
rewriteToEdit dynflags uri (annsA ps) graft
122122
]
@@ -173,14 +173,16 @@ commandHandler lsp _ideState ExecuteCommandParams{..}
173173
= return (Right Null, Nothing)
174174

175175
suggestExactAction ::
176+
ExportsMap ->
176177
DynFlags ->
177178
Annotated ParsedSource ->
178179
Diagnostic ->
179180
[(T.Text, Rewrite)]
180-
suggestExactAction df ps x =
181+
suggestExactAction exportsMap df ps x =
181182
concat
182183
[ suggestConstraint df (astA ps) x
183184
, suggestImplicitParameter (astA ps) x
185+
, suggestExtendImport exportsMap (astA ps) x
184186
]
185187

186188
suggestAction
@@ -193,7 +195,6 @@ suggestAction
193195
suggestAction packageExports ideOptions parsedModule text diag = concat
194196
-- Order these suggestions by priority
195197
[ suggestSignature True diag
196-
, suggestExtendImport packageExports text diag
197198
, suggestFillTypeWildcard diag
198199
, suggestFixConstructorImport text diag
199200
, suggestModuleTypo diag
@@ -725,32 +726,31 @@ getIndentedGroupsBy pred inp = case dropWhile (not.pred) inp of
725726
indentation :: T.Text -> Int
726727
indentation = T.length . T.takeWhile isSpace
727728

728-
suggestExtendImport :: ExportsMap -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
729-
suggestExtendImport exportsMap contents Diagnostic{_range=_range,..}
729+
suggestExtendImport :: ExportsMap -> ParsedSource -> Diagnostic -> [(T.Text, Rewrite)]
730+
suggestExtendImport exportsMap (L _ HsModule {hsmodImports}) Diagnostic{_range=_range,..}
730731
| Just [binding, mod, srcspan] <-
731732
matchRegexUnifySpaces _message
732733
"Perhaps you want to add ‘([^’]*)’ to the import list in the import of ‘([^’]*)’ *\\((.*)\\).$"
733-
, Just c <- contents
734-
= suggestions c binding mod srcspan
734+
= suggestions hsmodImports binding mod srcspan
735735
| Just (binding, mod_srcspan) <-
736736
matchRegExMultipleImports _message
737-
, Just c <- contents
738-
= mod_srcspan >>= (\(x, y) -> suggestions c binding x y)
737+
= mod_srcspan >>= uncurry (suggestions hsmodImports binding)
739738
| otherwise = []
740739
where
741-
suggestions c binding mod srcspan
740+
unImportStyle (ImportTopLevel x) = (Nothing, T.unpack x)
741+
unImportStyle (ImportViaParent x y) = (Just $ T.unpack y, T.unpack x)
742+
suggestions decls binding mod srcspan
742743
| range <- case [ x | (x,"") <- readSrcSpan (T.unpack srcspan)] of
743744
[s] -> let x = realSrcSpanToRange s
744745
in x{_end = (_end x){_character = succ (_character (_end x))}}
745746
_ -> error "bug in srcspan parser",
746-
importLine <- textInRange range c,
747+
Just decl <- findImportDeclByRange decls range,
747748
Just ident <- lookupExportMap binding mod
748-
= [ ( "Add " <> rendered <> " to the import list of " <> mod
749-
, [TextEdit range result]
749+
= [ ( "Add " <> renderImportStyle importStyle <> " to the import list of " <> mod
750+
, uncurry extendImport (unImportStyle importStyle) decl
750751
)
751752
| importStyle <- NE.toList $ importStyles ident
752-
, let rendered = renderImportStyle importStyle
753-
, result <- maybeToList $ addBindingToImportList importStyle importLine]
753+
]
754754
| otherwise = []
755755
lookupExportMap binding mod
756756
| Just match <- Map.lookup binding (getExportsMap exportsMap)
@@ -765,6 +765,9 @@ suggestExtendImport exportsMap contents Diagnostic{_range=_range,..}
765765
, parent = Nothing
766766
, isDatacon = False}
767767

768+
findImportDeclByRange :: [LImportDecl GhcPs] -> Range -> Maybe (LImportDecl GhcPs)
769+
findImportDeclByRange xs range = find (\(L l _)-> srcSpanToRange l == Just range) xs
770+
768771
suggestFixConstructorImport :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
769772
suggestFixConstructorImport _ Diagnostic{_range=_range,..}
770773
-- ‘Success’ is a data constructor of ‘Result’
@@ -1187,49 +1190,6 @@ rangesForBinding' b (L l (IEThingWith _ thing _ inners labels))
11871190
[ l' | L l' x <- labels, showSDocUnsafe (ppr x) == b]
11881191
rangesForBinding' _ _ = []
11891192

1190-
-- | Extends an import list with a new binding.
1191-
-- Assumes an import statement of the form:
1192-
-- import (qualified) A (..) ..
1193-
-- Places the new binding first, preserving whitespace.
1194-
-- Copes with multi-line import lists
1195-
addBindingToImportList :: ImportStyle -> T.Text -> Maybe T.Text
1196-
addBindingToImportList importStyle importLine =
1197-
case T.breakOn "(" importLine of
1198-
(pre, T.uncons -> Just (_, rest)) ->
1199-
case importStyle of
1200-
ImportTopLevel rendered ->
1201-
-- the binding has no parent, add it to the head of import list
1202-
Just $ T.concat [pre, "(", rendered, addCommaIfNeeds rest]
1203-
ImportViaParent rendered parent -> case T.breakOn parent rest of
1204-
-- the binding has a parent, and the current import list contains the
1205-
-- parent
1206-
--
1207-
-- `rest'` could be 1. `,...)`
1208-
-- or 2. `(),...)`
1209-
-- or 3. `(ConsA),...)`
1210-
-- or 4. `)`
1211-
(leading, T.stripPrefix parent -> Just rest') -> case T.uncons (T.stripStart rest') of
1212-
-- case 1: no children and parentheses, e.g. `import A(Foo,...)` --> `import A(Foo(Cons), ...)`
1213-
Just (',', rest'') -> Just $ T.concat [pre, "(", leading, parent, "(", rendered, ")", addCommaIfNeeds rest'']
1214-
-- case 2: no children but parentheses, e.g. `import A(Foo(),...)` --> `import A(Foo(Cons), ...)`
1215-
Just ('(', T.uncons -> Just (')', rest'')) -> Just $ T.concat [pre, "(", leading, parent, "(", rendered, ")", rest'']
1216-
-- case 3: children with parentheses, e.g. `import A(Foo(ConsA),...)` --> `import A(Foo(Cons, ConsA), ...)`
1217-
Just ('(', T.breakOn ")" -> (children, rest''))
1218-
| not (T.null children),
1219-
-- ignore A(Foo({-...-}), ...)
1220-
not $ "{-" `T.isPrefixOf` T.stripStart children
1221-
-> Just $ T.concat [pre, "(", leading, parent, "(", rendered, ", ", children, rest'']
1222-
-- case 4: no trailing, e.g. `import A(..., Foo)` --> `import A(..., Foo(Cons))`
1223-
Just (')', _) -> Just $ T.concat [pre, "(", leading, parent, "(", rendered, ")", rest']
1224-
_ -> Nothing
1225-
-- current import list does not contain the parent, e.g. `import A(...)` --> `import A(Foo(Cons), ...)`
1226-
_ -> Just $ T.concat [pre, "(", parent, "(", rendered, ")", addCommaIfNeeds rest]
1227-
_ -> Nothing
1228-
where
1229-
addCommaIfNeeds r = case T.uncons (T.stripStart r) of
1230-
Just (')', _) -> r
1231-
_ -> ", " <> r
1232-
12331193
-- | 'matchRegex' combined with 'unifySpaces'
12341194
matchRegexUnifySpaces :: T.Text -> T.Text -> Maybe [T.Text]
12351195
matchRegexUnifySpaces message = matchRegex (unifySpaces message)
@@ -1321,6 +1281,7 @@ data ImportStyle
13211281
--
13221282
-- @P@ and @?@ can be a data type and a constructor, a class and a method,
13231283
-- a class and an associated type/data family, etc.
1284+
deriving Show
13241285

13251286
importStyles :: IdentInfo -> NonEmpty ImportStyle
13261287
importStyles IdentInfo {parent, rendered, isDatacon}

Diff for: ghcide/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs

+126-1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ module Development.IDE.Plugin.CodeAction.ExactPrint
99

1010
-- * Utilities
1111
appendConstraint,
12+
extendImport,
1213
)
1314
where
1415

@@ -28,6 +29,8 @@ import GhcPlugins (realSrcSpanEnd, realSrcSpanStart, sigPrec)
2829
import Language.Haskell.GHC.ExactPrint
2930
import Language.Haskell.GHC.ExactPrint.Types (DeltaPos (DP), KeywordId (G), mkAnnKey)
3031
import Language.Haskell.LSP.Types
32+
import OccName
33+
import Outputable (ppr, showSDocUnsafe)
3134

3235
------------------------------------------------------------------------------
3336

@@ -58,7 +61,7 @@ rewriteToEdit dflags uri anns (Rewrite dst f) = do
5861
[ ( uri,
5962
List
6063
[ TextEdit (fromJust $ srcSpanToRange dst) $
61-
T.pack $ tail $ exactPrint ast anns
64+
T.pack $ tail $ exactPrint ast anns
6265
]
6366
)
6467
]
@@ -173,3 +176,125 @@ headMaybe (a : _) = Just a
173176
lastMaybe :: [a] -> Maybe a
174177
lastMaybe [] = Nothing
175178
lastMaybe other = Just $ last other
179+
180+
liftMaybe :: String -> Maybe a -> TransformT (Either String) a
181+
liftMaybe _ (Just x) = return x
182+
liftMaybe s _ = lift $ Left s
183+
184+
-- | Copy anns attached to a into b with modification, then delete anns of a
185+
transferAnn :: (Data a, Data b) => Located a -> Located b -> (Annotation -> Annotation) -> TransformT (Either String) ()
186+
transferAnn la lb f = do
187+
anns <- getAnnsT
188+
let oldKey = mkAnnKey la
189+
newKey = mkAnnKey lb
190+
oldValue <- liftMaybe "Unable to find ann" $ Map.lookup oldKey anns
191+
putAnnsT $ Map.delete oldKey $ Map.insert newKey (f oldValue) anns
192+
193+
------------------------------------------------------------------------------
194+
extendImport :: Maybe String -> String -> LImportDecl GhcPs -> Rewrite
195+
extendImport mparent identifier lDecl@(L l _) =
196+
Rewrite l $ \df -> do
197+
case mparent of
198+
Just parent -> extendImportViaParent df parent identifier lDecl
199+
_ -> extendImportTopLevel df identifier lDecl
200+
201+
-- | Add an identifier to import list
202+
--
203+
-- extendImportTopLevel "foo" AST:
204+
--
205+
-- import A --> Error
206+
-- import A (bar) --> import A (bar, foo)
207+
extendImportTopLevel :: DynFlags -> String -> LImportDecl GhcPs -> TransformT (Either String) (LImportDecl GhcPs)
208+
extendImportTopLevel df idnetifier (L l it@ImportDecl {..})
209+
| Just (hide, L l' lies) <- ideclHiding,
210+
hasSibling <- not $ null lies = do
211+
src <- uniqueSrcSpanT
212+
top <- uniqueSrcSpanT
213+
rdr <- liftParseAST df idnetifier
214+
let lie = L src $ IEName rdr
215+
x = L top $ IEVar noExtField lie
216+
when hasSibling $
217+
addTrailingCommaT (last lies)
218+
addSimpleAnnT x (DP (0, if hasSibling then 1 else 0)) []
219+
addSimpleAnnT rdr dp00 $ unqalDP $ hasParen idnetifier
220+
-- Parens are attachted to `lies`, so if `lies` was empty previously,
221+
-- we need change the ann key from `[]` to `:` to keep parens and other anns.
222+
unless hasSibling $
223+
transferAnn (L l' lies) (L l' [x]) id
224+
return $ L l it {ideclHiding = Just (hide, L l' $ lies ++ [x])}
225+
extendImportTopLevel _ _ _ = lift $ Left "Unable to extend the import list"
226+
227+
-- | Add an identifier with its parent to import list
228+
--
229+
-- extendImportViaParent "Bar" "Cons" AST:
230+
--
231+
-- import A --> Error
232+
-- import A () --> import A (Bar(Cons))
233+
-- import A (Foo, Bar) --> import A (Foo, Bar(Cons))
234+
-- import A (Foo, Bar()) --> import A (Foo, Bar(Cons))
235+
extendImportViaParent :: DynFlags -> String -> String -> LImportDecl GhcPs -> TransformT (Either String) (LImportDecl GhcPs)
236+
extendImportViaParent df parent child (L l it@ImportDecl {..})
237+
| Just (hide, L l' lies) <- ideclHiding = go hide l' [] lies
238+
where
239+
go :: Bool -> SrcSpan -> [LIE GhcPs] -> [LIE GhcPs] -> TransformT (Either String) (LImportDecl GhcPs)
240+
go hide l' pre (lAbs@(L ll' (IEThingAbs _ absIE@(L _ ie))) : xs)
241+
-- ThingAbs ie => ThingWith ie child
242+
| parent == unIEWrappedName ie = do
243+
srcChild <- uniqueSrcSpanT
244+
childRdr <- liftParseAST df child
245+
let childLIE = L srcChild $ IEName childRdr
246+
x :: LIE GhcPs = L ll' $ IEThingWith noExtField absIE NoIEWildcard [childLIE] []
247+
-- take anns from ThingAbs, and attatch parens to it
248+
transferAnn lAbs x $ \old -> old {annsDP = annsDP old ++ [(G AnnOpenP, DP (0, 1)), (G AnnCloseP, dp00)]}
249+
addSimpleAnnT childRdr dp00 [(G AnnVal, dp00)]
250+
return $ L l it {ideclHiding = Just (hide, L l' $ reverse pre ++ [x] ++ xs)}
251+
go hide l' pre ((L l'' (IEThingWith _ twIE@(L _ ie) _ lies' _)) : xs)
252+
-- ThingWith ie lies' => ThingWith ie (lies' ++ [child])
253+
| parent == unIEWrappedName ie,
254+
hasSibling <- not $ null lies' =
255+
do
256+
srcChild <- uniqueSrcSpanT
257+
childRdr <- liftParseAST df child
258+
when hasSibling $
259+
addTrailingCommaT (last lies')
260+
let childLIE = L srcChild $ IEName childRdr
261+
addSimpleAnnT childRdr (DP (0, if hasSibling then 1 else 0)) $ unqalDP $ hasParen child
262+
return $ L l it {ideclHiding = Just (hide, L l' $ reverse pre ++ [L l'' (IEThingWith noExtField twIE NoIEWildcard (lies' ++ [childLIE]) [])] ++ xs)}
263+
go hide l' pre (x : xs) = go hide l' (x : pre) xs
264+
go hide l' pre []
265+
| hasSibling <- not $ null pre = do
266+
-- [] => ThingWith parent [child]
267+
l'' <- uniqueSrcSpanT
268+
srcParent <- uniqueSrcSpanT
269+
srcChild <- uniqueSrcSpanT
270+
parentRdr <- liftParseAST df parent
271+
childRdr <- liftParseAST df child
272+
when hasSibling $
273+
addTrailingCommaT (head pre)
274+
let parentLIE = L srcParent $ IEName parentRdr
275+
childLIE = L srcChild $ IEName childRdr
276+
x :: LIE GhcPs = L l'' $ IEThingWith noExtField parentLIE NoIEWildcard [childLIE] []
277+
addSimpleAnnT parentRdr (DP (0, if hasSibling then 1 else 0)) $ unqalDP $ hasParen parent
278+
addSimpleAnnT childRdr (DP (0, 0)) $ unqalDP $ hasParen child
279+
addSimpleAnnT x (DP (0, 0)) [(G AnnOpenP, DP (0, 1)), (G AnnCloseP, DP (0, 0))]
280+
-- Parens are attachted to `pre`, so if `pre` was empty previously,
281+
-- we need change the ann key from `[]` to `:` to keep parens and other anns.
282+
unless hasSibling $
283+
transferAnn (L l' $ reverse pre) (L l' [x]) id
284+
return $ L l it {ideclHiding = Just (hide, L l' $ reverse pre ++ [x])}
285+
extendImportViaParent _ _ _ _ = lift $ Left "Unable to extend the import list via parent"
286+
287+
unIEWrappedName :: IEWrappedName (IdP GhcPs) -> String
288+
unIEWrappedName (occName -> occ) = showSDocUnsafe $ parenSymOcc occ (ppr occ)
289+
290+
hasParen :: String -> Bool
291+
hasParen ('(' : _) = True
292+
hasParen _ = False
293+
294+
unqalDP :: Bool -> [(KeywordId, DeltaPos)]
295+
unqalDP paren =
296+
( if paren
297+
then \x -> (G AnnOpenP, dp00) : x : [(G AnnCloseP, dp00)]
298+
else pure
299+
)
300+
(G AnnVal, dp00)

0 commit comments

Comments
 (0)