on master help sign in
Smarter logic to remove redundant import bindings (#308)
b7208a3Pepe Iborra
src/Development/IDE/GHC/Compat.hs+12−1
@@ -23,11 +23,14 @@ module Development.IDE.GHC.Compat(
23 pattern TyClD,
23 pattern TyClD,
24 pattern ValD,
24 pattern ValD,
25 pattern ClassOpSig,
25 pattern ClassOpSig,
26+ pattern IEThingWith,
27+
26 module GHC
28 module GHC
27 ) where
29 ) where
28
30
29import StringBuffer
31import StringBuffer
30import DynFlags
32import DynFlags
33+import FieldLabel
31import GHC.LanguageExtensions.Type
34import GHC.LanguageExtensions.Type
32
35
33#if MIN_GHC_API_VERSION(8,8,0)
36#if MIN_GHC_API_VERSION(8,8,0)
@@ -35,7 +38,7 @@ import Data.List.Extra (enumerate)
35#endif
38#endif
36
39
37import qualified GHC
40import qualified GHC
38−import GHC hiding (ClassOpSig, DerivD, ForD, InstD, TyClD, ValD)
41+import GHC hiding (ClassOpSig, DerivD, ForD, IEThingWith, InstD, TyClD, ValD)
39
42
40#if MIN_GHC_API_VERSION(8,8,0)
43#if MIN_GHC_API_VERSION(8,8,0)
41import HieAst
44import HieAst
@@ -141,3 +144,11 @@ pattern ClassOpSig a b c <-
141#else
144#else
142 GHC.ClassOpSig a b c
145 GHC.ClassOpSig a b c
143#endif
146#endif
147+
148+pattern IEThingWith :: LIEWrappedName (IdP pass) -> IEWildcard -> [LIEWrappedName (IdP pass)] -> [Located (FieldLbl (IdP pass))] -> IE pass
149+pattern IEThingWith a b c d <-
150+#if MIN_GHC_API_VERSION(8,6,0)
151+ GHC.IEThingWith _ a b c d
152+#else
153+ GHC.IEThingWith a b c d
154+#endif
src/Development/IDE/LSP/CodeAction.hs+82−45
@@ -12,6 +12,7 @@ module Development.IDE.LSP.CodeAction
12 ) where
12 ) where
13
13
14import Language.Haskell.LSP.Types
14import Language.Haskell.LSP.Types
15+import Control.Monad (join)
15import Development.IDE.GHC.Compat
16import Development.IDE.GHC.Compat
16import Development.IDE.Core.Rules
17import Development.IDE.Core.Rules
17import Development.IDE.Core.RuleTypes
18import Development.IDE.Core.RuleTypes
@@ -33,6 +34,7 @@ import Data.List.Extra
33import qualified Data.Text as T
34import qualified Data.Text as T
34import Text.Regex.TDFA ((=~), (=~~))
35import Text.Regex.TDFA ((=~), (=~~))
35import Text.Regex.TDFA.Text()
36import Text.Regex.TDFA.Text()
37+import Outputable (ppr, showSDocUnsafe)
36
38
37-- | Generate code actions.
39-- | Generate code actions.
38codeAction
40codeAction
@@ -40,14 +42,15 @@ codeAction
40 -> IdeState
42 -> IdeState
41 -> CodeActionParams
43 -> CodeActionParams
42 -> IO (List CAResult)
44 -> IO (List CAResult)
43−codeAction lsp _ CodeActionParams{_textDocument=TextDocumentIdentifier uri,_context=CodeActionContext{_diagnostics=List xs}} = do
45+codeAction lsp state CodeActionParams{_textDocument=TextDocumentIdentifier uri,_context=CodeActionContext{_diagnostics=List xs}} = do
44 -- disable logging as its quite verbose
46 -- disable logging as its quite verbose
45 -- logInfo (ideLogger ide) $ T.pack $ "Code action req: " ++ show arg
47 -- logInfo (ideLogger ide) $ T.pack $ "Code action req: " ++ show arg
46 contents <- LSP.getVirtualFileFunc lsp $ toNormalizedUri uri
48 contents <- LSP.getVirtualFileFunc lsp $ toNormalizedUri uri
47 let text = Rope.toText . (_text :: VirtualFile -> Rope.Rope) <$> contents
49 let text = Rope.toText . (_text :: VirtualFile -> Rope.Rope) <$> contents
50+ parsedModule <- (runAction state . getParsedModule . toNormalizedFilePath) `traverse` uriToFilePath uri
48 pure $ List
51 pure $ List
49 [ CACodeAction $ CodeAction title (Just CodeActionQuickFix) (Just $ List [x]) (Just edit) Nothing
52 [ CACodeAction $ CodeAction title (Just CodeActionQuickFix) (Just $ List [x]) (Just edit) Nothing
50− | x <- xs, (title, tedit) <- suggestAction text x
53+ | x <- xs, (title, tedit) <- suggestAction ( join parsedModule ) text x
51 , let edit = WorkspaceEdit (Just $ Map.singleton uri $ List tedit) Nothing
54 , let edit = WorkspaceEdit (Just $ Map.singleton uri $ List tedit) Nothing
52 ]
55 ]
53
56
@@ -86,28 +89,29 @@ executeAddSignatureCommand _lsp _ideState ExecuteCommandParams{..}
86 | otherwise
89 | otherwise
87 = return (Null, Nothing)
90 = return (Null, Nothing)
88
91
89−suggestAction :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
92+suggestAction :: Maybe ParsedModule -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
90−suggestAction text diag = concat
93+suggestAction parsedModule text diag = concat
91 [ suggestAddExtension diag
94 [ suggestAddExtension diag
92 , suggestExtendImport text diag
95 , suggestExtendImport text diag
93 , suggestFillHole diag
96 , suggestFillHole diag
94 , suggestFillTypeWildcard diag
97 , suggestFillTypeWildcard diag
95 , suggestFixConstructorImport text diag
98 , suggestFixConstructorImport text diag
96 , suggestModuleTypo diag
99 , suggestModuleTypo diag
97− , suggestRemoveRedundantImport text diag
98 , suggestReplaceIdentifier text diag
100 , suggestReplaceIdentifier text diag
99 , suggestSignature True diag
101 , suggestSignature True diag
100− ]
102+ ] ++ concat
103+ [ suggestRemoveRedundantImport pm text diag | Just pm <- [parsedModule]]
101
104
102
105
103−suggestRemoveRedundantImport :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
106+suggestRemoveRedundantImport :: ParsedModule -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
104−suggestRemoveRedundantImport contents Diagnostic{_range=_range@Range{..},..}
107+suggestRemoveRedundantImport ParsedModule{pm_parsed_source = L _ HsModule{hsmodImports}} contents Diagnostic{_range=_range@Range{..},..}
105-- The qualified import of ‘many’ from module ‘Control.Applicative’ is redundant
108-- The qualified import of ‘many’ from module ‘Control.Applicative’ is redundant
106 | Just [_, bindings] <- matchRegex _message "The( qualified)? import of ‘([^’]*)’ from module [^ ]* is redundant"
109 | Just [_, bindings] <- matchRegex _message "The( qualified)? import of ‘([^’]*)’ from module [^ ]* is redundant"
110+ , Just (L _ impDecl) <- find (\(L l _) -> srcSpanToRange l == _range ) hsmodImports
107 , Just c <- contents
111 , Just c <- contents
108− , importLine <- textInRange _range c
112+ , ranges <- map (rangesForBinding impDecl . T.unpack) (T.splitOn ", " bindings)
113+ , ranges' <- extendAllToIncludeCommaIfPossible (indexedByPosition $ T.unpack c) (concat ranges)
109− = [( "Remove " <> bindings <> " from import"
114+ = [( "Remove " <> bindings <> " from import" , [ TextEdit r "" | r <- ranges' ] )]
110− , [TextEdit _range (dropBindingsFromImportLine (T.splitOn "," bindings) importLine)])]
111
115
112-- File.hs:16:1: warning:
116-- File.hs:16:1: warning:
113-- The import of `Data.List' is redundant
117-- The import of `Data.List' is redundant
@@ -357,44 +361,29 @@ textInRange (Range (Position startRow startCol) (Position endRow endCol)) text =
357 where
361 where
358 linesBeginningWithStartLine = drop startRow (T.splitOn "\n" text)
362 linesBeginningWithStartLine = drop startRow (T.splitOn "\n" text)
359
363
360−-- | Drop all occurrences of a binding in an import line.
364+-- | Returns the ranges for a binding in an import declaration
361−-- Preserves well-formedness but not whitespace between bindings.
365+rangesForBinding :: ImportDecl GhcPs -> String -> [Range]
362−--
366+rangesForBinding ImportDecl{ideclHiding = Just (False, L _ lies)} b =
363−-- >>> dropBindingsFromImportLine ["bA", "bC"] "import A(bA, bB,bC ,bA)"
367+ concatMap (map srcSpanToRange . rangesForBinding' b') lies
364−-- "import A(bB)"
365−--
366−-- >>> dropBindingsFromImportLine ["+"] "import "P" qualified A as B ((+))"
367−-- "import "P" qualified A() as B hiding (bB)"
368−dropBindingsFromImportLine :: [T.Text] -> T.Text -> T.Text
369−dropBindingsFromImportLine bindings_ importLine =
370− importPre <> "(" <> importRest'
371− where
368+ where
372− bindings = map (wrapOperatorInParens . removeQualified) bindings_
369+ b' = wrapOperatorInParens (unqualify b)
373−
374− (importPre, importRest) = T.breakOn "(" importLine
375−
376− wrapOperatorInParens x = if isAlpha (T.head x) then x else "(" <> x <> ")"
377
370
378− removeQualified x = case T.breakOn "." x of
371+ wrapOperatorInParens x = if isAlpha (head x) then x else "(" <> x <> ")"
379− (_qualifier, T.uncons -> Just (_, unqualified)) -> unqualified
380− _ -> x
381
372
382− importRest' = case T.uncons importRest of
373+ unqualify x = snd $ breakOnEnd "." x
383− Just (_, x) ->
384− T.intercalate ","
385− $ joinCloseParens
386− $ mapMaybe (filtering . T.strip)
387− $ T.splitOn "," x
388− Nothing -> importRest
389
374
390− filtering x = case () of
375+rangesForBinding _ _ = []
391− () | x `elem` bindings -> Nothing
392− () | x `elem` map (<> ")") bindings -> Just ")"
393− _ -> Just x
394
376
395− joinCloseParens (x : ")" : rest) = (x <> ")") : joinCloseParens rest
377+rangesForBinding' :: String -> LIE GhcPs -> [SrcSpan]
396− joinCloseParens (x : rest) = x : joinCloseParens rest
378+rangesForBinding' b (L l x@IEVar{}) | showSDocUnsafe (ppr x) == b = [l]
397− joinCloseParens [] = []
379+rangesForBinding' b (L l x@IEThingAbs{}) | showSDocUnsafe (ppr x) == b = [l]
380+rangesForBinding' b (L l x@IEThingAll{}) | showSDocUnsafe (ppr x) == b = [l]
381+rangesForBinding' b (L l (IEThingWith thing _ inners labels))
382+ | showSDocUnsafe (ppr thing) == b = [l]
383+ | otherwise =
384+ [ l' | L l' x <- inners, showSDocUnsafe (ppr x) == b] ++
385+ [ l' | L l' x <- labels, showSDocUnsafe (ppr x) == b]
386+rangesForBinding' _ _ = []
398
387
399-- | Extends an import list with a new binding.
388-- | Extends an import list with a new binding.
400-- Assumes an import statement of the form:
389-- Assumes an import statement of the form:
@@ -428,3 +417,51 @@ setHandlersCodeLens = PartialHandlers $ \WithMessage{..} x -> return x{
428 LSP.codeLensHandler = withResponse RspCodeLens codeLens,
417 LSP.codeLensHandler = withResponse RspCodeLens codeLens,
429 LSP.executeCommandHandler = withResponseAndRequest RspExecuteCommand ReqApplyWorkspaceEdit executeAddSignatureCommand
418 LSP.executeCommandHandler = withResponseAndRequest RspExecuteCommand ReqApplyWorkspaceEdit executeAddSignatureCommand
430 }
419 }
420+
421+--------------------------------------------------------------------------------
422+
423+type PositionIndexedString = [(Position, Char)]
424+
425+indexedByPosition :: String -> PositionIndexedString
426+indexedByPosition = unfoldr f . (Position 0 0,) where
427+ f (_, []) = Nothing
428+ f (p@(Position l _), '\n' : rest) = Just ((p,'\n'), (Position (l+1) 0, rest))
429+ f (p@(Position l c), x : rest) = Just ((p, x), (Position l (c+1), rest))
430+
431+-- | Returns a tuple (before, contents, after)
432+unconsRange :: Range -> PositionIndexedString -> (PositionIndexedString, PositionIndexedString, PositionIndexedString)
433+unconsRange Range {..} indexedString = (before, mid, after)
434+ where
435+ (before, rest) = span ((/= _start) . fst) indexedString
436+ (mid, after) = span ((/= _end) . fst) rest
437+
438+stripRange :: Range -> PositionIndexedString -> PositionIndexedString
439+stripRange r s = case unconsRange r s of
440+ (b, _, a) -> b ++ a
441+
442+extendAllToIncludeCommaIfPossible :: PositionIndexedString -> [Range] -> [Range]
443+extendAllToIncludeCommaIfPossible _ [] = []
444+extendAllToIncludeCommaIfPossible indexedString (r : rr) = r' : extendAllToIncludeCommaIfPossible indexedString' rr
445+ where
446+ r' = case extendToIncludeCommaIfPossible indexedString r of
447+ [] -> r
448+ r' : _ -> r'
449+ indexedString' = stripRange r' indexedString
450+
451+-- | Returns a sorted list of ranges with extended selections includindg preceding or trailing commas
452+extendToIncludeCommaIfPossible :: PositionIndexedString -> Range -> [Range]
453+extendToIncludeCommaIfPossible indexedString range =
454+ -- a, |b|, c ===> a|, b|, c
455+ [ range{_start = start'}
456+ | (start', ',') : _ <- [before']
457+ ]
458+ ++
459+ -- a, |b|, c ===> a, |b, |c
460+ [ range{_end = end'}
461+ | (_, ',') : rest <- [after']
462+ , let (end', _) : _ = dropWhile (isSpace . snd) rest
463+ ]
464+ where
465+ (before, _, after) = unconsRange range indexedString
466+ after' = dropWhile (isSpace . snd) after
467+ before' = dropWhile (isSpace . snd) (reverse before)
test/exe/Main.hs+7−4
@@ -608,19 +608,20 @@ removeImportTests = testGroup "remove import actions"
608 , "stuffA = False"
608 , "stuffA = False"
609 , "stuffB :: Integer"
609 , "stuffB :: Integer"
610 , "stuffB = 123"
610 , "stuffB = 123"
611+ , "stuffC = ()"
611 ]
612 ]
612 _docA <- openDoc' "ModuleA.hs" "haskell" contentA
613 _docA <- openDoc' "ModuleA.hs" "haskell" contentA
613 let contentB = T.unlines
614 let contentB = T.unlines
614 [ "{-# OPTIONS_GHC -Wunused-imports #-}"
615 [ "{-# OPTIONS_GHC -Wunused-imports #-}"
615 , "module ModuleB where"
616 , "module ModuleB where"
616− , "import ModuleA (stuffA, stuffB)"
617+ , "import ModuleA (stuffA, stuffB, stuffC, stuffA)"
617 , "main = print stuffB"
618 , "main = print stuffB"
618 ]
619 ]
619 docB <- openDoc' "ModuleB.hs" "haskell" contentB
620 docB <- openDoc' "ModuleB.hs" "haskell" contentB
620 _ <- waitForDiagnostics
621 _ <- waitForDiagnostics
621 [CACodeAction action@CodeAction { _title = actionTitle }]
622 [CACodeAction action@CodeAction { _title = actionTitle }]
622 <- getCodeActions docB (Range (Position 2 0) (Position 2 5))
623 <- getCodeActions docB (Range (Position 2 0) (Position 2 5))
623− liftIO $ "Remove stuffA from import" @=? actionTitle
624+ liftIO $ "Remove stuffA, stuffC from import" @=? actionTitle
624 executeCodeAction action
625 executeCodeAction action
625 contentAfterAction <- documentContents docB
626 contentAfterAction <- documentContents docB
626 let expectedContentAfterAction = T.unlines
627 let expectedContentAfterAction = T.unlines
@@ -1480,9 +1481,11 @@ run s = withTempDir $ \dir -> do
1480 runSessionWithConfig conf cmd fullCaps { _window = Just $ WindowClientCapabilities $ Just True } dir s
1481 runSessionWithConfig conf cmd fullCaps { _window = Just $ WindowClientCapabilities $ Just True } dir s
1481 where
1482 where
1482 conf = defaultConfig
1483 conf = defaultConfig
1483− -- If you uncomment this you can see all messages
1484+ -- If you uncomment this you can see all logging
1484 -- which can be quite useful for debugging.
1485 -- which can be quite useful for debugging.
1485− -- { logMessages = True, logColor = False, logStdErr = True }
1486+ -- { logStdErr = True, logColor = False }
1487+ -- If you really want to, you can also see all messages
1488+ -- { logMessages = True, logColor = False }
1486
1489
1487openTestDataDoc :: FilePath -> Session TextDocumentIdentifier
1490openTestDataDoc :: FilePath -> Session TextDocumentIdentifier
1488openTestDataDoc path = do
1491openTestDataDoc path = do