Skip to content
This repository was archived by the owner on Jan 2, 2021. It is now read-only.

Commit 863392b

Browse files
authored
Extend import suggestions for more than one option (#913)
* Add support for extending import list when multiple options are available * Add function to module export list to make it available for testing * Fix typo * Add doc strings * Add tests for testing regex used to parse multiple choices for import suggestions. * Add test group * Remove trailing spaces * Hlint suggestions * Remove not used variable * Remove temporary code * Reuse matchRegExUnifySpaces * Fix test input. * Use testCase instead of testSession * Update extend import tests to assert on multiple actions. * Extend extendImports to use multiple modules for setup * Hlint changes
1 parent b11d010 commit 863392b

File tree

2 files changed

+145
-41
lines changed

2 files changed

+145
-41
lines changed

src/Development/IDE/Plugin/CodeAction.hs

Lines changed: 56 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ module Development.IDE.Plugin.CodeAction
1919
-- * For testing
2020
, blockCommandId
2121
, typeSignatureCommandId
22+
, matchRegExMultipleImports
2223
) where
2324

2425
import Control.Monad (join, guard)
@@ -381,7 +382,7 @@ suggestExportUnusedTopBinding srcOpt ParsedModule{pm_parsed_source = L _ HsModul
381382
opLetter = ":!#$%&*+./<=>?@\\^|-~"
382383

383384
parenthesizeIfNeeds :: Bool -> T.Text -> T.Text
384-
parenthesizeIfNeeds needsTypeKeyword x
385+
parenthesizeIfNeeds needsTypeKeyword x
385386
| T.head x `elem` opLetter = (if needsTypeKeyword then "type " else "") <> "(" <> x <>")"
386387
| otherwise = x
387388

@@ -649,14 +650,23 @@ suggestExtendImport (Just dflags) contents Diagnostic{_range=_range,..}
649650
"Perhaps you want to add ‘([^’]*)’ to the import list in the import of ‘([^’]*)’ *\\((.*)\\).$"
650651
, Just c <- contents
651652
, POk _ (L _ name) <- runParser dflags (T.unpack binding) parseIdentifier
652-
= let range = case [ x | (x,"") <- readSrcSpan (T.unpack srcspan)] of
653-
[s] -> let x = realSrcSpanToRange s
654-
in x{_end = (_end x){_character = succ (_character (_end x))}}
655-
_ -> error "bug in srcspan parser"
656-
importLine = textInRange range c
657-
in [("Add " <> binding <> " to the import list of " <> mod
658-
, [TextEdit range (addBindingToImportList (T.pack $ printRdrName name) importLine)])]
653+
= [suggestions name c binding mod srcspan]
654+
| Just (binding, mod_srcspan) <-
655+
matchRegExMultipleImports _message
656+
, Just c <- contents
657+
, POk _ (L _ name) <- runParser dflags (T.unpack binding) parseIdentifier
658+
= fmap (\(x, y) -> suggestions name c binding x y) mod_srcspan
659659
| otherwise = []
660+
where
661+
suggestions name c binding mod srcspan = let
662+
range = case [ x | (x,"") <- readSrcSpan (T.unpack srcspan)] of
663+
[s] -> let x = realSrcSpanToRange s
664+
in x{_end = (_end x){_character = succ (_character (_end x))}}
665+
_ -> error "bug in srcspan parser"
666+
importLine = textInRange range c
667+
in
668+
("Add " <> binding <> " to the import list of " <> mod
669+
, [TextEdit range (addBindingToImportList (T.pack $ printRdrName name) importLine)])
660670
suggestExtendImport Nothing _ _ = []
661671

662672
suggestFixConstructorImport :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
@@ -1135,3 +1145,41 @@ filterNewlines = T.concat . T.lines
11351145

11361146
unifySpaces :: T.Text -> T.Text
11371147
unifySpaces = T.unwords . T.words
1148+
1149+
-- functions to help parse multiple import suggestions
1150+
1151+
-- | Returns the first match if found
1152+
regexSingleMatch :: T.Text -> T.Text -> Maybe T.Text
1153+
regexSingleMatch msg regex = case matchRegexUnifySpaces msg regex of
1154+
Just (h:_) -> Just h
1155+
_ -> Nothing
1156+
1157+
-- | Parses tuples like (‘Data.Map’, (app/ModuleB.hs:2:1-18)) and
1158+
-- | return (Data.Map, app/ModuleB.hs:2:1-18)
1159+
regExPair :: (T.Text, T.Text) -> Maybe (T.Text, T.Text)
1160+
regExPair (modname, srcpair) = do
1161+
x <- regexSingleMatch modname "‘([^’]*)’"
1162+
y <- regexSingleMatch srcpair "\\((.*)\\)"
1163+
return (x, y)
1164+
1165+
-- | Process a list of (module_name, filename:src_span) values
1166+
-- | Eg. [(Data.Map, app/ModuleB.hs:2:1-18), (Data.HashMap.Strict, app/ModuleB.hs:3:1-29)]
1167+
regExImports :: T.Text -> Maybe [(T.Text, T.Text)]
1168+
regExImports msg = result
1169+
where
1170+
parts = T.words msg
1171+
isPrefix = not . T.isPrefixOf "("
1172+
(mod, srcspan) = partition isPrefix parts
1173+
-- check we have matching pairs like (Data.Map, (app/src.hs:1:2-18))
1174+
result = if length mod == length srcspan then
1175+
regExPair `traverse` zip mod srcspan
1176+
else Nothing
1177+
1178+
matchRegExMultipleImports :: T.Text -> Maybe (T.Text, [(T.Text, T.Text)])
1179+
matchRegExMultipleImports message = do
1180+
let pat = T.pack "Perhaps you want to add ‘([^’]*)’ to one of these import lists: *(‘.*\\))$"
1181+
(binding, imports) <- case matchRegexUnifySpaces message pat of
1182+
Just [x, xs] -> Just (x, xs)
1183+
_ -> Nothing
1184+
imps <- regExImports imports
1185+
return (binding, imps)

test/exe/Main.hs

Lines changed: 89 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ import Test.Tasty.Ingredients.Rerun
5757
import Test.Tasty.HUnit
5858
import Test.Tasty.QuickCheck
5959
import System.Time.Extra
60-
import Development.IDE.Plugin.CodeAction (typeSignatureCommandId, blockCommandId)
60+
import Development.IDE.Plugin.CodeAction (typeSignatureCommandId, blockCommandId, matchRegExMultipleImports)
6161
import Development.IDE.Plugin.Test (TestRequest(BlockSeconds,GetInterfaceFilesDir))
6262

6363
main :: IO ()
@@ -97,6 +97,8 @@ main = do
9797
, rootUriTests
9898
, asyncTests
9999
, clientSettingsTest
100+
101+
, codeActionHelperFunctionTests
100102
]
101103

102104
initializeResponseTests :: TestTree
@@ -560,6 +562,13 @@ codeActionTests = testGroup "code actions"
560562
, exportUnusedTests
561563
]
562564

565+
codeActionHelperFunctionTests :: TestTree
566+
codeActionHelperFunctionTests = testGroup "code action helpers"
567+
[
568+
extendImportTestsRegEx
569+
]
570+
571+
563572
codeLensesTests :: TestTree
564573
codeLensesTests = testGroup "code lenses"
565574
[ addSigLensesTests
@@ -954,139 +963,186 @@ removeImportTests = testGroup "remove import actions"
954963
extendImportTests :: TestTree
955964
extendImportTests = testGroup "extend import actions"
956965
[ testSession "extend single line import with value" $ template
957-
(T.unlines
966+
[("ModuleA.hs", T.unlines
958967
[ "module ModuleA where"
959968
, "stuffA :: Double"
960969
, "stuffA = 0.00750"
961970
, "stuffB :: Integer"
962971
, "stuffB = 123"
963-
])
964-
(T.unlines
972+
])]
973+
("ModuleB.hs", T.unlines
965974
[ "module ModuleB where"
966975
, "import ModuleA as A (stuffB)"
967976
, "main = print (stuffA, stuffB)"
968977
])
969978
(Range (Position 3 17) (Position 3 18))
970-
"Add stuffA to the import list of ModuleA"
979+
["Add stuffA to the import list of ModuleA"]
971980
(T.unlines
972981
[ "module ModuleB where"
973982
, "import ModuleA as A (stuffA, stuffB)"
974983
, "main = print (stuffA, stuffB)"
975984
])
976985
, testSession "extend single line import with operator" $ template
977-
(T.unlines
986+
[("ModuleA.hs", T.unlines
978987
[ "module ModuleA where"
979988
, "(.*) :: Integer -> Integer -> Integer"
980989
, "x .* y = x * y"
981990
, "stuffB :: Integer"
982991
, "stuffB = 123"
983-
])
984-
(T.unlines
992+
])]
993+
("ModuleB.hs", T.unlines
985994
[ "module ModuleB where"
986995
, "import ModuleA as A (stuffB)"
987996
, "main = print (stuffB .* stuffB)"
988997
])
989998
(Range (Position 3 17) (Position 3 18))
990-
"Add .* to the import list of ModuleA"
999+
["Add .* to the import list of ModuleA"]
9911000
(T.unlines
9921001
[ "module ModuleB where"
9931002
, "import ModuleA as A ((.*), stuffB)"
9941003
, "main = print (stuffB .* stuffB)"
9951004
])
9961005
, testSession "extend single line import with type" $ template
997-
(T.unlines
1006+
[("ModuleA.hs", T.unlines
9981007
[ "module ModuleA where"
9991008
, "type A = Double"
1000-
])
1001-
(T.unlines
1009+
])]
1010+
("ModuleB.hs", T.unlines
10021011
[ "module ModuleB where"
10031012
, "import ModuleA ()"
10041013
, "b :: A"
10051014
, "b = 0"
10061015
])
10071016
(Range (Position 2 5) (Position 2 5))
1008-
"Add A to the import list of ModuleA"
1017+
["Add A to the import list of ModuleA"]
10091018
(T.unlines
10101019
[ "module ModuleB where"
10111020
, "import ModuleA (A)"
10121021
, "b :: A"
10131022
, "b = 0"
10141023
])
10151024
, (`xfail` "known broken") $ testSession "extend single line import with constructor" $ template
1016-
(T.unlines
1025+
[("ModuleA.hs", T.unlines
10171026
[ "module ModuleA where"
10181027
, "data A = Constructor"
1019-
])
1020-
(T.unlines
1028+
])]
1029+
("ModuleB.hs", T.unlines
10211030
[ "module ModuleB where"
10221031
, "import ModuleA (A)"
10231032
, "b :: A"
10241033
, "b = Constructor"
10251034
])
10261035
(Range (Position 2 5) (Position 2 5))
1027-
"Add Constructor to the import list of ModuleA"
1036+
["Add Constructor to the import list of ModuleA"]
10281037
(T.unlines
10291038
[ "module ModuleB where"
10301039
, "import ModuleA (A(Constructor))"
10311040
, "b :: A"
10321041
, "b = Constructor"
10331042
])
10341043
, testSession "extend single line qualified import with value" $ template
1035-
(T.unlines
1044+
[("ModuleA.hs", T.unlines
10361045
[ "module ModuleA where"
10371046
, "stuffA :: Double"
10381047
, "stuffA = 0.00750"
10391048
, "stuffB :: Integer"
10401049
, "stuffB = 123"
1041-
])
1042-
(T.unlines
1050+
])]
1051+
("ModuleB.hs", T.unlines
10431052
[ "module ModuleB where"
10441053
, "import qualified ModuleA as A (stuffB)"
10451054
, "main = print (A.stuffA, A.stuffB)"
10461055
])
10471056
(Range (Position 3 17) (Position 3 18))
1048-
"Add stuffA to the import list of ModuleA"
1057+
["Add stuffA to the import list of ModuleA"]
10491058
(T.unlines
10501059
[ "module ModuleB where"
10511060
, "import qualified ModuleA as A (stuffA, stuffB)"
10521061
, "main = print (A.stuffA, A.stuffB)"
10531062
])
10541063
, testSession "extend multi line import with value" $ template
1055-
(T.unlines
1064+
[("ModuleA.hs", T.unlines
10561065
[ "module ModuleA where"
10571066
, "stuffA :: Double"
10581067
, "stuffA = 0.00750"
10591068
, "stuffB :: Integer"
10601069
, "stuffB = 123"
1061-
])
1062-
(T.unlines
1070+
])]
1071+
("ModuleB.hs", T.unlines
10631072
[ "module ModuleB where"
10641073
, "import ModuleA (stuffB"
10651074
, " )"
10661075
, "main = print (stuffA, stuffB)"
10671076
])
10681077
(Range (Position 3 17) (Position 3 18))
1069-
"Add stuffA to the import list of ModuleA"
1078+
["Add stuffA to the import list of ModuleA"]
10701079
(T.unlines
10711080
[ "module ModuleB where"
10721081
, "import ModuleA (stuffA, stuffB"
10731082
, " )"
10741083
, "main = print (stuffA, stuffB)"
10751084
])
1085+
, testSession "extend import list with multiple choices" $ template
1086+
[("ModuleA.hs", T.unlines
1087+
-- this is just a dummy module to help the arguments needed for this test
1088+
[ "module ModuleA (bar) where"
1089+
, "bar = 10"
1090+
]),
1091+
("ModuleB.hs", T.unlines
1092+
-- this is just a dummy module to help the arguments needed for this test
1093+
[ "module ModuleB (bar) where"
1094+
, "bar = 10"
1095+
])]
1096+
("ModuleC.hs", T.unlines
1097+
[ "module ModuleC where"
1098+
, "import ModuleB ()"
1099+
, "import ModuleA ()"
1100+
, "foo = bar"
1101+
])
1102+
(Range (Position 3 17) (Position 3 18))
1103+
["Add bar to the import list of ModuleA",
1104+
"Add bar to the import list of ModuleB"]
1105+
(T.unlines
1106+
[ "module ModuleC where"
1107+
, "import ModuleB ()"
1108+
, "import ModuleA (bar)"
1109+
, "foo = bar"
1110+
])
10761111
]
10771112
where
1078-
template contentA contentB range expectedAction expectedContentB = do
1079-
_docA <- createDoc "ModuleA.hs" "haskell" contentA
1080-
docB <- createDoc "ModuleB.hs" "haskell" contentB
1081-
_ <- waitForDiagnostics
1082-
CACodeAction action@CodeAction { _title = actionTitle } : _
1083-
<- sortOn (\(CACodeAction CodeAction{_title=x}) -> x) <$>
1084-
getCodeActions docB range
1085-
liftIO $ expectedAction @=? actionTitle
1113+
template setUpModules moduleUnderTest range expectedActions expectedContentB = do
1114+
mapM_ (\x -> createDoc (fst x) "haskell" (snd x)) setUpModules
1115+
docB <- createDoc (fst moduleUnderTest) "haskell" (snd moduleUnderTest)
1116+
_ <- waitForDiagnostics
1117+
codeActions <- filter (\(CACodeAction CodeAction{_title=x}) -> T.isPrefixOf "Add" x)
1118+
<$> getCodeActions docB range
1119+
let expectedTitles = (\(CACodeAction CodeAction{_title=x}) ->x) <$> codeActions
1120+
liftIO $ expectedActions @=? expectedTitles
1121+
1122+
-- Get the first action and execute the first action
1123+
let CACodeAction action : _
1124+
= sortOn (\(CACodeAction CodeAction{_title=x}) -> x) codeActions
10861125
executeCodeAction action
10871126
contentAfterAction <- documentContents docB
10881127
liftIO $ expectedContentB @=? contentAfterAction
10891128

1129+
extendImportTestsRegEx :: TestTree
1130+
extendImportTestsRegEx = testGroup "regex parsing"
1131+
[
1132+
testCase "parse invalid multiple imports" $ template "foo bar foo" Nothing
1133+
, testCase "parse malformed import list" $ template
1134+
"\n\8226 Perhaps you want to add \8216fromList\8217 to one of these import lists:\n \8216Data.Map\8217)"
1135+
Nothing
1136+
, testCase "parse multiple imports" $ template
1137+
"\n\8226 Perhaps you want to add \8216fromList\8217 to one of these import lists:\n \8216Data.Map\8217 (app/testlsp.hs:7:1-18)\n \8216Data.HashMap.Strict\8217 (app/testlsp.hs:8:1-29)"
1138+
$ Just ("fromList",[("Data.Map","app/testlsp.hs:7:1-18"),("Data.HashMap.Strict","app/testlsp.hs:8:1-29")])
1139+
]
1140+
where
1141+
template message expected = do
1142+
liftIO $ matchRegExMultipleImports message @=? expected
1143+
1144+
1145+
10901146
suggestImportTests :: TestTree
10911147
suggestImportTests = testGroup "suggest import actions"
10921148
[ testGroup "Dont want suggestion"

0 commit comments

Comments
 (0)