Skip to content

Commit fd163cd

Browse files
pepeiborracocreature
authored andcommitted
Insert imports code action (#295)
* #46 Code action to add suggested imports * code action to fix constructor imports * #46 Add test for (broken) constructor import
1 parent f8e6ab1 commit fd163cd

File tree

3 files changed

+274
-8
lines changed

3 files changed

+274
-8
lines changed

src/Development/IDE/LSP/CodeAction.hs

Lines changed: 81 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import Development.IDE.GHC.Compat
1616
import Development.IDE.Core.Rules
1717
import Development.IDE.Core.RuleTypes
1818
import Development.IDE.Core.Shake
19+
import Development.IDE.GHC.Error
1920
import Development.IDE.LSP.Server
2021
import Development.IDE.Types.Location
2122
import qualified Data.HashMap.Strict as Map
@@ -85,8 +86,22 @@ executeAddSignatureCommand _lsp _ideState ExecuteCommandParams{..}
8586
| otherwise
8687
= return (Null, Nothing)
8788

88-
suggestAction :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
89-
suggestAction contents diag@Diagnostic{_range=_range@Range{..},..}
89+
suggestAction :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
90+
suggestAction text diag = concat
91+
[ suggestAddExtension diag
92+
, suggestExtendImport text diag
93+
, suggestFillHole diag
94+
, suggestFillTypeWildcard diag
95+
, suggestFixConstructorImport text diag
96+
, suggestModuleTypo diag
97+
, suggestRemoveRedundantImport text diag
98+
, suggestReplaceIdentifier text diag
99+
, suggestSignature True diag
100+
]
101+
102+
103+
suggestRemoveRedundantImport :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
104+
suggestRemoveRedundantImport contents Diagnostic{_range=_range@Range{..},..}
90105
-- The qualified import of ‘many’ from module ‘Control.Applicative’ is redundant
91106
| Just [_, bindings] <- matchRegex _message "The( qualified)? import of ‘([^’]*)’ from module [^ ]* is redundant"
92107
, Just c <- contents
@@ -100,7 +115,10 @@ suggestAction contents diag@Diagnostic{_range=_range@Range{..},..}
100115
-- To import instances alone, use: import Data.List()
101116
| _message =~ ("The( qualified)? import of [^ ]* is redundant" :: String)
102117
= [("Remove import", [TextEdit (extendToWholeLineIfPossible contents _range) ""])]
118+
| otherwise = []
103119

120+
suggestReplaceIdentifier :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
121+
suggestReplaceIdentifier contents Diagnostic{_range=_range@Range{..},..}
104122
-- File.hs:52:41: error:
105123
-- * Variable not in scope:
106124
-- suggestAcion :: Maybe T.Text -> Range -> Range
@@ -114,15 +132,21 @@ suggestAction contents diag@Diagnostic{_range=_range@Range{..},..}
114132
-- Module ‘Data.Text’ does not export ‘isPrfixOf’.
115133
| renameSuggestions@(_:_) <- extractRenamableTerms _message
116134
= [ ("Replace with ‘" <> name <> "", [mkRenameEdit contents _range name]) | name <- renameSuggestions ]
135+
| otherwise = []
117136

137+
suggestFillTypeWildcard :: Diagnostic -> [(T.Text, [TextEdit])]
138+
suggestFillTypeWildcard Diagnostic{_range=_range@Range{..},..}
118139
-- Foo.hs:3:8: error:
119140
-- * Found type wildcard `_' standing for `p -> p1 -> p'
120141

121142
| "Found type wildcard" `T.isInfixOf` _message
122143
, " standing for " `T.isInfixOf` _message
123144
, typeSignature <- extractWildCardTypeSignature _message
124145
= [("Use type signature: ‘" <> typeSignature <> "", [TextEdit _range typeSignature])]
146+
| otherwise = []
125147

148+
suggestAddExtension :: Diagnostic -> [(T.Text, [TextEdit])]
149+
suggestAddExtension Diagnostic{_range=_range@Range{..},..}
126150
-- File.hs:22:8: error:
127151
-- Illegal lambda-case (use -XLambdaCase)
128152
-- File.hs:22:6: error:
@@ -145,7 +169,10 @@ suggestAction contents diag@Diagnostic{_range=_range@Range{..},..}
145169
-- In the instance declaration for `Unit (m a)'
146170
| exts@(_:_) <- filter (`Set.member` ghcExtensions) $ T.split (not . isAlpha) $ T.replace "-X" "" _message
147171
= [("Add " <> x <> " extension", [TextEdit (Range (Position 0 0) (Position 0 0)) $ "{-# LANGUAGE " <> x <> " #-}\n"]) | x <- exts]
172+
| otherwise = []
148173

174+
suggestModuleTypo :: Diagnostic -> [(T.Text, [TextEdit])]
175+
suggestModuleTypo Diagnostic{_range=_range@Range{..},..}
149176
-- src/Development/IDE/Core/Compile.hs:58:1: error:
150177
-- Could not find module ‘Data.Cha’
151178
-- Perhaps you meant Data.Char (from base-4.12.0.0)
@@ -154,7 +181,10 @@ suggestAction contents diag@Diagnostic{_range=_range@Range{..},..}
154181
findSuggestedModules = map (head . T.words) . drop 2 . T.lines
155182
proposeModule mod = ("replace with " <> mod, [TextEdit _range mod])
156183
in map proposeModule $ nubOrd $ findSuggestedModules _message
184+
| otherwise = []
157185

186+
suggestFillHole :: Diagnostic -> [(T.Text, [TextEdit])]
187+
suggestFillHole Diagnostic{_range=_range@Range{..},..}
158188
-- ...Development/IDE/LSP/CodeAction.hs:103:9: warning:
159189
-- * Found hole: _ :: Int -> String
160190
-- * In the expression: _
@@ -187,9 +217,36 @@ suggestAction contents diag@Diagnostic{_range=_range@Range{..},..}
187217
extractFitNames = map (T.strip . head . T.splitOn " :: ")
188218
in map proposeHoleFit $ nubOrd $ findSuggestedHoleFits _message
189219

190-
| tlb@[_] <- suggestSignature True diag = tlb
220+
| otherwise = []
191221

192-
suggestAction _ _ = []
222+
suggestExtendImport :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
223+
suggestExtendImport contents Diagnostic{_range=_range,..}
224+
| Just [binding, mod, srcspan] <-
225+
matchRegex _message
226+
"Perhaps you want to add ‘([^’]*)’ to the import list in the import of ‘([^’]*)’ *\\((.*)\\).$"
227+
, Just c <- contents
228+
= let range = case [ x | (x,"") <- readSrcSpan (T.unpack srcspan)] of
229+
[s] -> let x = srcSpanToRange s
230+
in x{_end = (_end x){_character = succ (_character (_end x))}}
231+
_ -> error "bug in srcspan parser"
232+
importLine = textInRange range c
233+
in [("Add " <> binding <> " to the import list of " <> mod
234+
, [TextEdit range (addBindingToImportList binding importLine)])]
235+
| otherwise = []
236+
237+
suggestFixConstructorImport :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
238+
suggestFixConstructorImport _ Diagnostic{_range=_range,..}
239+
-- ‘Success’ is a data constructor of ‘Result’
240+
-- To import it use
241+
-- import Data.Aeson.Types( Result( Success ) )
242+
-- or
243+
-- import Data.Aeson.Types( Result(..) ) (lsp-ui)
244+
| Just [constructor, typ] <-
245+
matchRegex _message
246+
"‘([^’]*)’ is a data constructor of ‘([^’]*)’ To import it use"
247+
= let fixedImport = typ <> "(" <> constructor <> ")"
248+
in [("Fix import of " <> fixedImport, [TextEdit _range fixedImport])]
249+
| otherwise = []
193250

194251
suggestSignature :: Bool -> Diagnostic -> [(T.Text, [TextEdit])]
195252
suggestSignature isQuickFix Diagnostic{_range=_range@Range{..},..}
@@ -282,6 +339,7 @@ splitTextAtPosition (Position row col) x
282339
= (T.intercalate "\n" $ preRow ++ [preCol], T.intercalate "\n" $ postCol : postRow)
283340
| otherwise = (x, T.empty)
284341

342+
-- | Returns [start .. end[
285343
textInRange :: Range -> T.Text -> T.Text
286344
textInRange (Range (Position startRow startCol) (Position endRow endCol)) text =
287345
case compare startRow endRow of
@@ -338,11 +396,27 @@ dropBindingsFromImportLine bindings_ importLine =
338396
joinCloseParens (x : rest) = x : joinCloseParens rest
339397
joinCloseParens [] = []
340398

399+
-- | Extends an import list with a new binding.
400+
-- Assumes an import statement of the form:
401+
-- import (qualified) A (..) ..
402+
-- Places the new binding first, preserving whitespace.
403+
-- Copes with multi-line import lists
404+
addBindingToImportList :: T.Text -> T.Text -> T.Text
405+
addBindingToImportList binding importLine = case T.breakOn "(" importLine of
406+
(pre, T.uncons -> Just (_, rest)) ->
407+
case T.uncons (T.dropWhile isSpace rest) of
408+
Just (')', _) -> T.concat [pre, "(", binding, rest]
409+
_ -> T.concat [pre, "(", binding, ", ", rest]
410+
_ ->
411+
error
412+
$ "importLine does not have the expected structure: "
413+
<> T.unpack importLine
414+
341415
-- | Returns Just (the submatches) for the first capture, or Nothing.
342416
matchRegex :: T.Text -> T.Text -> Maybe [T.Text]
343-
matchRegex message regex = case message =~~ regex of
344-
Just (_ :: T.Text, _ :: T.Text, _ :: T.Text, bindings) -> Just bindings
345-
Nothing -> Nothing
417+
matchRegex message regex = case T.unwords (T.words message) =~~ regex of
418+
Just (_ :: T.Text, _ :: T.Text, _ :: T.Text, bindings) -> Just bindings
419+
Nothing -> Nothing
346420

347421
setHandlersCodeAction :: PartialHandlers
348422
setHandlersCodeAction = PartialHandlers $ \WithMessage{..} x -> return x{

src/Development/IDE/Types/Location.hs

Lines changed: 43 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,15 +21,19 @@ module Development.IDE.Types.Location
2121
, filePathToUri
2222
, filePathToUri'
2323
, uriToFilePath'
24+
, readSrcSpan
2425
) where
2526

27+
import Control.Applicative
2628
import Language.Haskell.LSP.Types (Location(..), Range(..), Position(..))
2729
import Control.DeepSeq
30+
import Control.Monad
2831
import Data.Binary
2932
import Data.Maybe as Maybe
3033
import Data.Hashable
3134
import Data.String
3235
import qualified Data.Text as T
36+
import FastString
3337
import Network.URI
3438
import System.FilePath
3539
import qualified System.FilePath.Posix as FPP
@@ -43,6 +47,8 @@ import Language.Haskell.LSP.Types as LSP (
4347
, toNormalizedUri
4448
, fromNormalizedUri
4549
)
50+
import GHC
51+
import Text.ParserCombinators.ReadP as ReadP
4652

4753

4854
-- | Newtype wrapper around FilePath that always has normalized slashes.
@@ -120,6 +126,42 @@ noFilePath = "<unknown>"
120126
noRange :: Range
121127
noRange = Range (Position 0 0) (Position 100000 0)
122128

123-
124129
showPosition :: Position -> String
125130
showPosition Position{..} = show (_line + 1) ++ ":" ++ show (_character + 1)
131+
132+
-- | Parser for the GHC output format
133+
readSrcSpan :: ReadS SrcSpan
134+
readSrcSpan = readP_to_S (singleLineSrcSpanP <|> multiLineSrcSpanP)
135+
where
136+
singleLineSrcSpanP, multiLineSrcSpanP :: ReadP SrcSpan
137+
singleLineSrcSpanP = do
138+
fp <- filePathP
139+
l <- readS_to_P reads <* char ':'
140+
c0 <- readS_to_P reads
141+
c1 <- (char '-' *> readS_to_P reads) <|> pure c0
142+
let from = mkSrcLoc fp l c0
143+
to = mkSrcLoc fp l c1
144+
return $ mkSrcSpan from to
145+
146+
multiLineSrcSpanP = do
147+
fp <- filePathP
148+
s <- parensP (srcLocP fp)
149+
void $ char '-'
150+
e <- parensP (srcLocP fp)
151+
return $ mkSrcSpan s e
152+
153+
parensP :: ReadP a -> ReadP a
154+
parensP = between (char '(') (char ')')
155+
156+
filePathP :: ReadP FastString
157+
filePathP = fromString <$> (readFilePath <* char ':') <|> pure ""
158+
159+
srcLocP :: FastString -> ReadP SrcLoc
160+
srcLocP fp = do
161+
l <- readS_to_P reads
162+
void $ char ','
163+
c <- readS_to_P reads
164+
return $ mkSrcLoc fp l c
165+
166+
readFilePath :: ReadP FilePath
167+
readFilePath = some ReadP.get

0 commit comments

Comments
 (0)