on master help sign in
Code action to insert new definitions (#309)

* code action to insert new definitions

commit789f4031e6b60df785e47d62747047110dcc569b authorPepe Iborra <pepeiborra@gmail.com> dateMon 13 Jan 2020, 08:08 (7 years ago) parent1b4cd9d
5 files changed+155−59
src/Development/IDE/Core/Completions.hs+17−38 src/Development/IDE/GHC/Error.hs+5 src/Development/IDE/LSP/CodeAction.hs+48−10 src/Development/IDE/Types/Location.hs+1−1 test/exe/Main.hs+84−10
src/Development/IDE/Core/Completions.hs+17−38
@@ -26,7 +26,6 @@ import Packages
2626import DynFlags
2727import ConLike
2828import DataCon
29−import SrcLoc as GHC
3029
3130import Language.Haskell.LSP.Types
3231import Language.Haskell.LSP.Types.Capabilities
@@ -70,12 +69,12 @@ data Context = TypeContext
7069-- i.e. where are the value decls and the type decls
7170getCContext :: Position -> ParsedModule -> Maybe Context
7271getCContext pos pm
73− | Just (L (RealSrcSpan r) modName) <- moduleHeader
74− , pos `isInsideRange` r
72+ | Just (L r modName) <- moduleHeader
73+ , pos `isInsideSrcSpan` r
7574 = Just (ModuleContext (moduleNameString modName))
7675
77− | Just (L (RealSrcSpan r) _) <- exportList
78− , pos `isInsideRange` r
76+ | Just (L r _) <- exportList
77+ , pos `isInsideSrcSpan` r
7978 = Just ExportContext
8079
8180 | Just ctx <- something (Nothing `mkQ` go `extQ` goInline) decl
@@ -93,54 +92,34 @@ getCContext pos pm
9392 imports = hsmodImports $ unLoc $ pm_parsed_source pm
9493
9594 go :: LHsDecl GhcPs -> Maybe Context
96− go (L (RealSrcSpan r) SigD {})
97− | pos `isInsideRange` r = Just TypeContext
95+ go (L r SigD {})
96+ | pos `isInsideSrcSpan` r = Just TypeContext
9897 | otherwise = Nothing
99− go (L (GHC.RealSrcSpan r) GHC.ValD {})
100− | pos `isInsideRange` r = Just ValueContext
98+ go (L r GHC.ValD {})
99+ | pos `isInsideSrcSpan` r = Just ValueContext
101100 | otherwise = Nothing
102101 go _ = Nothing
103102
104103 goInline :: GHC.LHsType GhcPs -> Maybe Context
105− goInline (GHC.L (GHC.RealSrcSpan r) _)
106− | pos `isInsideRange` r = Just TypeContext
107− | otherwise = Nothing
104+ goInline (GHC.L r _)
105+ | pos `isInsideSrcSpan` r = Just TypeContext
108106 goInline _ = Nothing
109107
110− p `isInsideRange` r = sp <= p && p <= ep
111− where (sp, ep) = unpackRealSrcSpan r
112−
113− -- | Converts from one based tuple
114− toPos :: (Int,Int) -> Position
115− toPos (l,c) = Position (l-1) (c-1)
116−
117− unpackRealSrcSpan :: GHC.RealSrcSpan -> (Position, Position)
118− unpackRealSrcSpan rspan =
119− (toPos (l1,c1),toPos (l2,c2))
120− where s = GHC.realSrcSpanStart rspan
121− l1 = GHC.srcLocLine s
122− c1 = GHC.srcLocCol s
123− e = GHC.realSrcSpanEnd rspan
124− l2 = GHC.srcLocLine e
125− c2 = GHC.srcLocCol e
126−
127108 importGo :: GHC.LImportDecl GhcPs -> Maybe Context
128− importGo (L (RealSrcSpan r) impDecl)
129− | pos `isInsideRange` r
109+ importGo (L r impDecl)
110+ | pos `isInsideSrcSpan` r
130111 = importInline importModuleName (ideclHiding impDecl)
131112 <|> Just (ImportContext importModuleName)
132113
133114 | otherwise = Nothing
134115 where importModuleName = moduleNameString $ unLoc $ ideclName impDecl
135116
136− importGo _ = Nothing
137−
138117 importInline :: String -> Maybe (Bool, GHC.Located [LIE GhcPs]) -> Maybe Context
139− importInline modName (Just (True, L (RealSrcSpan r) _))
140− | pos `isInsideRange` r = Just $ ImportHidingContext modName
118+ importInline modName (Just (True, L r _))
119+ | pos `isInsideSrcSpan` r = Just $ ImportHidingContext modName
141120 | otherwise = Nothing
142− importInline modName (Just (False, L (RealSrcSpan r) _))
143− | pos `isInsideRange` r = Just $ ImportListContext modName
121+ importInline modName (Just (False, L r _))
122+ | pos `isInsideSrcSpan` r = Just $ ImportListContext modName
144123 | otherwise = Nothing
145124 importInline _ _ = Nothing
146125
@@ -151,7 +130,7 @@ occNameToComKind ty oc
151130 _ -> CiFunction
152131 | isTcOcc oc = case ty of
153132 Just t
154− | "Constraint" `T.isSuffixOf` t
133+ | "Constraint" `T.isSuffixOf` t
155134 -> CiClass
156135 _ -> CiStruct
157136 | isDataOcc oc = CiConstructor
src/Development/IDE/GHC/Error.hs+5
@@ -16,6 +16,7 @@ module Development.IDE.GHC.Error
1616 , srcSpanToFilename
1717 , zeroSpan
1818 , realSpan
19+ , isInsideSrcSpan
1920
2021 -- * utilities working with severities
2122 , toDSeverity
@@ -80,6 +81,10 @@ srcSpanToLocation src =
8081 -- important that the URI's we produce have been properly normalized, otherwise they point at weird places in VS Code
8182 Location (fromNormalizedUri $ filePathToUri' $ toNormalizedFilePath $ srcSpanToFilename src) (srcSpanToRange src)
8283
84+isInsideSrcSpan :: Position -> SrcSpan -> Bool
85+p `isInsideSrcSpan` r = sp <= p && p <= ep
86+ where Range sp ep = srcSpanToRange r
87+
8388-- | Convert a GHC severity to a DAML compiler Severity. Severities below
8489-- "Warning" level are dropped (returning Nothing).
8590toDSeverity :: GHC.Severity -> Maybe D.DiagnosticSeverity
src/Development/IDE/LSP/CodeAction.hs+48−10
@@ -16,10 +16,12 @@ import Control.Monad (join)
1616import Development.IDE.GHC.Compat
1717import Development.IDE.Core.Rules
1818import Development.IDE.Core.RuleTypes
19+import Development.IDE.Core.Service
1920import Development.IDE.Core.Shake
2021import Development.IDE.GHC.Error
2122import Development.IDE.LSP.Server
2223import Development.IDE.Types.Location
24+import Development.IDE.Types.Options
2325import qualified Data.HashMap.Strict as Map
2426import qualified Data.HashSet as Set
2527import qualified Language.Haskell.LSP.Core as LSP
@@ -47,10 +49,12 @@ codeAction lsp state CodeActionParams{_textDocument=TextDocumentIdentifier uri,_
4749 -- logInfo (ideLogger ide) $ T.pack $ "Code action req: " ++ show arg
4850 contents <- LSP.getVirtualFileFunc lsp $ toNormalizedUri uri
4951 let text = Rope.toText . (_text :: VirtualFile -> Rope.Rope) <$> contents
50− parsedModule <- (runAction state . getParsedModule . toNormalizedFilePath) `traverse` uriToFilePath uri
52+ (ideOptions, parsedModule) <- runAction state $
53+ (,) <$> getIdeOptions
54+ <*> (getParsedModule . toNormalizedFilePath) `traverse` uriToFilePath uri
5155 pure $ List
5256 [ CACodeAction $ CodeAction title (Just CodeActionQuickFix) (Just $ List [x]) (Just edit) Nothing
53− | x <- xs, (title, tedit) <- suggestAction ( join parsedModule ) text x
57+ | x <- xs, (title, tedit) <- suggestAction ideOptions ( join parsedModule ) text x
5458 , let edit = WorkspaceEdit (Just $ Map.singleton uri $ List tedit) Nothing
5559 ]
5660
@@ -89,8 +93,8 @@ executeAddSignatureCommand _lsp _ideState ExecuteCommandParams{..}
8993 | otherwise
9094 = return (Null, Nothing)
9195
92−suggestAction :: Maybe ParsedModule -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
93−suggestAction parsedModule text diag = concat
96+suggestAction :: IdeOptions -> Maybe ParsedModule -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
97+suggestAction ideOptions parsedModule text diag = concat
9498 [ suggestAddExtension diag
9599 , suggestExtendImport text diag
96100 , suggestFillHole diag
@@ -100,7 +104,9 @@ suggestAction parsedModule text diag = concat
100104 , suggestReplaceIdentifier text diag
101105 , suggestSignature True diag
102106 ] ++ concat
103− [ suggestRemoveRedundantImport pm text diag | Just pm <- [parsedModule]]
107+ [ suggestNewDefinition ideOptions pm text diag
108+ ++ suggestRemoveRedundantImport pm text diag
109+ | Just pm <- [parsedModule]]
104110
105111
106112suggestRemoveRedundantImport :: ParsedModule -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
@@ -138,6 +144,36 @@ suggestReplaceIdentifier contents Diagnostic{_range=_range@Range{..},..}
138144 = [ ("Replace with ‘" <> name <> "’", [mkRenameEdit contents _range name]) | name <- renameSuggestions ]
139145 | otherwise = []
140146
147+suggestNewDefinition :: IdeOptions -> ParsedModule -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
148+suggestNewDefinition ideOptions parsedModule contents Diagnostic{_message, _range}
149+-- * Variable not in scope:
150+-- suggestAcion :: Maybe T.Text -> Range -> Range
151+ | Just [name, typ] <- matchRegex message "Variable not in scope: ([^ ]+) :: ([^*•]+)"
152+ = newDefinitionAction ideOptions parsedModule _range name typ
153+ | Just [name, typ] <- matchRegex message "Found hole: _([^ ]+) :: ([^*•]+) Or perhaps"
154+ , [(label, newDefinitionEdits)] <- newDefinitionAction ideOptions parsedModule _range name typ
155+ = [(label, mkRenameEdit contents _range name : newDefinitionEdits)]
156+ | otherwise = []
157+ where
158+ message = unifySpaces _message
159+
160+newDefinitionAction :: IdeOptions -> ParsedModule -> Range -> T.Text -> T.Text -> [(T.Text, [TextEdit])]
161+newDefinitionAction IdeOptions{..} parsedModule Range{_start} name typ
162+ | Range _ lastLineP : _ <-
163+ [ srcSpanToRange l
164+ | (L l _) <- hsmodDecls
165+ , _start `isInsideSrcSpan` l]
166+ , nextLineP <- Position{ _line = _line lastLineP + 1, _character = 0}
167+ = [ ("Define " <> sig
168+ , [TextEdit (Range nextLineP nextLineP) (T.unlines ["", sig, name <> " = error \"not implemented\""])]
169+ )]
170+ | otherwise = []
171+ where
172+ colon = if optNewColonConvention then " : " else " :: "
173+ sig = name <> colon <> T.dropWhileEnd isSpace typ
174+ ParsedModule{pm_parsed_source = L _ HsModule{hsmodDecls}} = parsedModule
175+
176+
141177suggestFillTypeWildcard :: Diagnostic -> [(T.Text, [TextEdit])]
142178suggestFillTypeWildcard Diagnostic{_range=_range@Range{..},..}
143179-- Foo.hs:3:8: error:
@@ -255,8 +291,6 @@ suggestFixConstructorImport _ Diagnostic{_range=_range,..}
255291suggestSignature :: Bool -> Diagnostic -> [(T.Text, [TextEdit])]
256292suggestSignature isQuickFix Diagnostic{_range=_range@Range{..},..}
257293 | "Top-level binding with no type signature" `T.isInfixOf` _message = let
258− filterNewlines = T.concat . T.lines
259− unifySpaces = T.unwords . T.words
260294 signature = T.strip $ unifySpaces $ last $ T.splitOn "type signature: " $ filterNewlines _message
261295 startOfLine = Position (_line _start) 0
262296 beforeLine = Range startOfLine startOfLine
@@ -265,8 +299,6 @@ suggestSignature isQuickFix Diagnostic{_range=_range@Range{..},..}
265299 in [(title, [action])]
266300suggestSignature isQuickFix Diagnostic{_range=_range@Range{..},..}
267301 | "Polymorphic local binding with no type signature" `T.isInfixOf` _message = let
268− filterNewlines = T.concat . T.lines
269− unifySpaces = T.unwords . T.words
270302 signature = removeInitialForAll
271303 $ T.takeWhile (\x -> x/='*' && x/='•')
272304 $ T.strip $ unifySpaces $ last $ T.splitOn "type signature: " $ filterNewlines _message
@@ -403,7 +435,7 @@ addBindingToImportList binding importLine = case T.breakOn "(" importLine of
403435
404436-- | Returns Just (the submatches) for the first capture, or Nothing.
405437matchRegex :: T.Text -> T.Text -> Maybe [T.Text]
406−matchRegex message regex = case T.unwords (T.words message) =~~ regex of
438+matchRegex message regex = case unifySpaces message =~~ regex of
407439 Just (_ :: T.Text, _ :: T.Text, _ :: T.Text, bindings) -> Just bindings
408440 Nothing -> Nothing
409441
@@ -418,6 +450,12 @@ setHandlersCodeLens = PartialHandlers $ \WithMessage{..} x -> return x{
418450 LSP.executeCommandHandler = withResponseAndRequest RspExecuteCommand ReqApplyWorkspaceEdit executeAddSignatureCommand
419451 }
420452
453+filterNewlines :: T.Text -> T.Text
454+filterNewlines = T.concat . T.lines
455+
456+unifySpaces :: T.Text -> T.Text
457+unifySpaces = T.unwords . T.words
458+
421459--------------------------------------------------------------------------------
422460
423461type PositionIndexedString = [(Position, Char)]
src/Development/IDE/Types/Location.hs+1−1
@@ -47,7 +47,7 @@ import Language.Haskell.LSP.Types as LSP (
4747 , toNormalizedUri
4848 , fromNormalizedUri
4949 )
50−import GHC
50+import SrcLoc as GHC
5151import Text.ParserCombinators.ReadP as ReadP
5252
5353
test/exe/Main.hs+84−10
@@ -395,6 +395,7 @@ codeActionTests = testGroup "code actions"
395395 , importRenameActionTests
396396 , fillTypedHoleTests
397397 , addSigActionTests
398+ , insertNewDefinitionTests
398399 ]
399400
400401codeLensesTests :: TestTree
@@ -412,9 +413,7 @@ renameActionTests = testGroup "rename actions"
412413 ]
413414 doc <- openDoc' "Testing.hs" "haskell" content
414415 _ <- waitForDiagnostics
415− [CACodeAction action@CodeAction { _title = actionTitle }]
416− <- getCodeActions doc (Range (Position 2 14) (Position 2 20))
417− liftIO $ "Replace with ‘argName’" @=? actionTitle
416+ action <- findCodeAction doc (Range (Position 2 14) (Position 2 20)) "Replace with ‘argName’"
418417 executeCodeAction action
419418 contentAfterAction <- documentContents doc
420419 let expectedContentAfterAction = T.unlines
@@ -432,9 +431,7 @@ renameActionTests = testGroup "rename actions"
432431 ]
433432 doc <- openDoc' "Testing.hs" "haskell" content
434433 _ <- waitForDiagnostics
435− [CACodeAction action@CodeAction { _title = actionTitle }]
436− <- getCodeActions doc (Range (Position 3 6) (Position 3 16))
437− liftIO $ "Replace with ‘maybeToList’" @=? actionTitle
434+ action <- findCodeAction doc (Range (Position 3 6) (Position 3 16)) "Replace with ‘maybeToList’"
438435 executeCodeAction action
439436 contentAfterAction <- documentContents doc
440437 let expectedContentAfterAction = T.unlines
@@ -452,10 +449,9 @@ renameActionTests = testGroup "rename actions"
452449 ]
453450 doc <- openDoc' "Testing.hs" "haskell" content
454451 _ <- waitForDiagnostics
455− actionsOrCommands <- getCodeActions doc (Range (Position 2 36) (Position 2 45))
456− let actionTitles = [ actionTitle | CACodeAction CodeAction{ _title = actionTitle } <- actionsOrCommands ]
457− expectedActionTitles = ["Replace with ‘argument1’", "Replace with ‘argument2’", "Replace with ‘argument3’"]
458− liftIO $ expectedActionTitles @=? actionTitles
452+ _ <- findCodeActions doc (Range (Position 2 36) (Position 2 45))
453+ ["Replace with ‘argument1’", "Replace with ‘argument2’", "Replace with ‘argument3’"]
454+ return()
459455 , testSession "change infix function" $ do
460456 let content = T.unlines
461457 [ "module Testing where"
@@ -809,6 +805,61 @@ extendImportTests = testGroup "extend import actions"
809805 contentAfterAction <- documentContents docB
810806 liftIO $ expectedContentB @=? contentAfterAction
811807
808+insertNewDefinitionTests :: TestTree
809+insertNewDefinitionTests = testGroup "insert new definition actions"
810+ [ testSession "insert new function definition" $ do
811+ let txtB =
812+ ["foo True = select [True]"
813+ , ""
814+ ,"foo False = False"
815+ ]
816+ txtB' =
817+ [""
818+ ,"someOtherCode = ()"
819+ ]
820+ docB <- openDoc' "ModuleB.hs" "haskell" (T.unlines $ txtB ++ txtB')
821+ _ <- waitForDiagnostics
822+ CACodeAction action@CodeAction { _title = actionTitle } : _
823+ <- sortOn (\(CACodeAction CodeAction{_title=x}) -> x) <$>
824+ getCodeActions docB (R 1 0 1 50)
825+ liftIO $ actionTitle @?= "Define select :: [Bool] -> Bool"
826+ executeCodeAction action
827+ contentAfterAction <- documentContents docB
828+ liftIO $ contentAfterAction @?= T.unlines (txtB ++
829+ [ ""
830+ , "select :: [Bool] -> Bool"
831+ , "select = error \"not implemented\""
832+ ]
833+ ++ txtB')
834+ , testSession "define a hole" $ do
835+ let txtB =
836+ ["foo True = _select [True]"
837+ , ""
838+ ,"foo False = False"
839+ ]
840+ txtB' =
841+ [""
842+ ,"someOtherCode = ()"
843+ ]
844+ docB <- openDoc' "ModuleB.hs" "haskell" (T.unlines $ txtB ++ txtB')
845+ _ <- waitForDiagnostics
846+ CACodeAction action@CodeAction { _title = actionTitle } : _
847+ <- sortOn (\(CACodeAction CodeAction{_title=x}) -> x) <$>
848+ getCodeActions docB (R 1 0 1 50)
849+ liftIO $ actionTitle @?= "Define select :: [Bool] -> Bool"
850+ executeCodeAction action
851+ contentAfterAction <- documentContents docB
852+ liftIO $ contentAfterAction @?= T.unlines (
853+ ["foo True = select [True]"
854+ , ""
855+ ,"foo False = False"
856+ , ""
857+ , "select :: [Bool] -> Bool"
858+ , "select = error \"not implemented\""
859+ ]
860+ ++ txtB')
861+ ]
862+
812863fixConstructorImportTests :: TestTree
813864fixConstructorImportTests = testGroup "fix import actions"
814865 [ testSession "fix constructor import" $ template
@@ -1546,6 +1597,29 @@ openTestDataDoc path = do
15461597 source <- liftIO $ readFileUtf8 $ "test/data" </> path
15471598 openDoc' path "haskell" source
15481599
1600+findCodeActions :: TextDocumentIdentifier -> Range -> [T.Text] -> Session [CodeAction]
1601+findCodeActions doc range expectedTitles = do
1602+ actions <- getCodeActions doc range
1603+ let matches = sequence
1604+ [ listToMaybe
1605+ [ action
1606+ | CACodeAction action@CodeAction { _title = actionTitle } <- actions
1607+ , actionTitle == expectedTitle ]
1608+ | expectedTitle <- expectedTitles]
1609+ let msg = show
1610+ [ actionTitle
1611+ | CACodeAction CodeAction { _title = actionTitle } <- actions
1612+ ]
1613+ ++ "is not a superset of "
1614+ ++ show expectedTitles
1615+ liftIO $ case matches of
1616+ Nothing -> assertFailure msg
1617+ Just _ -> pure ()
1618+ return (fromJust matches)
1619+
1620+findCodeAction :: TextDocumentIdentifier -> Range -> T.Text -> Session CodeAction
1621+findCodeAction doc range t = head <$> findCodeActions doc range [t]
1622+
15491623unitTests :: TestTree
15501624unitTests = do
15511625 testGroup "Unit"