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

Support extending constructors #916

Merged
merged 9 commits into from
Dec 6, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
83 changes: 49 additions & 34 deletions src/Development/IDE/Plugin/CodeAction.hs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
-- SPDX-License-Identifier: Apache-2.0

{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE CPP #-}
{-# LANGUAGE CPP #-}
#include "ghc-api-version.h"

-- | Go to the definition of a variable.
Expand Down Expand Up @@ -30,7 +30,6 @@ import Development.IDE.Core.RuleTypes
import Development.IDE.Core.Service
import Development.IDE.Core.Shake
import Development.IDE.GHC.Error
import Development.IDE.GHC.Util
import Development.IDE.LSP.Server
import Development.IDE.Plugin.CodeAction.PositionIndexed
import Development.IDE.Plugin.CodeAction.RuleTypes
Expand All @@ -51,8 +50,6 @@ import Data.Maybe
import Data.List.Extra
import qualified Data.Text as T
import Data.Tuple.Extra ((&&&))
import HscTypes
import Parser
import Text.Regex.TDFA (mrAfter, (=~), (=~~))
import Outputable (ppr, showSDocUnsafe)
import GHC.LanguageExtensions.Type (Extension)
Expand Down Expand Up @@ -99,10 +96,9 @@ codeAction lsp state (TextDocumentIdentifier uri) _range CodeActionContext{_diag
pkgExports <- runAction "CodeAction:PackageExports" state $ (useNoFile_ . PackageExports) `traverse` env
localExports <- readVar (exportsMap $ shakeExtras state)
let exportsMap = localExports <> fromMaybe mempty pkgExports
let dflags = hsc_dflags . hscEnv <$> env
pure . Right $
[ CACodeAction $ CodeAction title (Just CodeActionQuickFix) (Just $ List [x]) (Just edit) Nothing
| x <- xs, (title, tedit) <- suggestAction dflags exportsMap ideOptions parsedModule text x
| x <- xs, (title, tedit) <- suggestAction exportsMap ideOptions parsedModule text x
, let edit = WorkspaceEdit (Just $ Map.singleton uri $ List tedit) Nothing
] <> caRemoveRedundantImports parsedModule text diag xs uri

Expand Down Expand Up @@ -153,18 +149,17 @@ commandHandler lsp _ideState ExecuteCommandParams{..}
= return (Right Null, Nothing)

suggestAction
:: Maybe DynFlags
-> ExportsMap
:: ExportsMap
-> IdeOptions
-> Maybe ParsedModule
-> Maybe T.Text
-> Diagnostic
-> [(T.Text, [TextEdit])]
suggestAction dflags packageExports ideOptions parsedModule text diag = concat
suggestAction packageExports ideOptions parsedModule text diag = concat
-- Order these suggestions by priority
[ suggestAddExtension diag -- Highest priority
, suggestSignature True diag
, suggestExtendImport dflags text diag
, suggestExtendImport packageExports text diag
, suggestFillTypeWildcard diag
, suggestFixConstructorImport text diag
, suggestModuleTypo diag
Expand Down Expand Up @@ -643,31 +638,37 @@ getIndentedGroupsBy pred inp = case dropWhile (not.pred) inp of
indentation :: T.Text -> Int
indentation = T.length . T.takeWhile isSpace

suggestExtendImport :: Maybe DynFlags -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
suggestExtendImport (Just dflags) contents Diagnostic{_range=_range,..}
suggestExtendImport :: ExportsMap -> Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
suggestExtendImport exportsMap contents Diagnostic{_range=_range,..}
| Just [binding, mod, srcspan] <-
matchRegexUnifySpaces _message
"Perhaps you want to add ‘([^’]*)’ to the import list in the import of ‘([^’]*)’ *\\((.*)\\).$"
, Just c <- contents
, POk _ (L _ name) <- runParser dflags (T.unpack binding) parseIdentifier
= [suggestions name c binding mod srcspan]
= suggestions c binding mod srcspan
| Just (binding, mod_srcspan) <-
matchRegExMultipleImports _message
, Just c <- contents
, POk _ (L _ name) <- runParser dflags (T.unpack binding) parseIdentifier
= fmap (\(x, y) -> suggestions name c binding x y) mod_srcspan
= mod_srcspan >>= (\(x, y) -> suggestions c binding x y)
| otherwise = []
where
suggestions name c binding mod srcspan = let
range = case [ x | (x,"") <- readSrcSpan (T.unpack srcspan)] of
suggestions c binding mod srcspan
| range <- case [ x | (x,"") <- readSrcSpan (T.unpack srcspan)] of
[s] -> let x = realSrcSpanToRange s
in x{_end = (_end x){_character = succ (_character (_end x))}}
_ -> error "bug in srcspan parser"
importLine = textInRange range c
in
("Add " <> binding <> " to the import list of " <> mod
, [TextEdit range (addBindingToImportList (T.pack $ printRdrName name) importLine)])
suggestExtendImport Nothing _ _ = []
_ -> error "bug in srcspan parser",
importLine <- textInRange range c,
Just (parent,r) <- lookupExportMap binding mod
=
[("Add " <> r <> " to the import list of " <> mod
, [TextEdit range (addBindingToImportList parent r importLine)])]
| otherwise = []
renderImport IdentInfo {parent, rendered}
| Just p <- parent = (p, p <> "(" <> rendered <> ")")
| otherwise = ("", rendered)
lookupExportMap binding mod
| [(renderImport -> pair, _)] <- filter (\(_,m) -> mod == m) $ maybe [] Set.toList $ Map.lookup binding (getExportsMap exportsMap)
= Just pair
| otherwise = Nothing

suggestFixConstructorImport :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])]
suggestFixConstructorImport _ Diagnostic{_range=_range,..}
Expand Down Expand Up @@ -1108,17 +1109,31 @@ rangesForBinding' _ _ = []
-- import (qualified) A (..) ..
-- Places the new binding first, preserving whitespace.
-- Copes with multi-line import lists
addBindingToImportList :: T.Text -> T.Text -> T.Text
addBindingToImportList binding importLine = case T.breakOn "(" importLine of
(pre, T.uncons -> Just (_, rest)) ->
case T.uncons (T.dropWhile isSpace rest) of
Just (')', _) -> T.concat [pre, "(", binding, rest]
_ -> T.concat [pre, "(", binding, ", ", rest]
_ ->
error
$ "importLine does not have the expected structure: "
addBindingToImportList :: T.Text -> T.Text -> T.Text -> T.Text
addBindingToImportList parent renderedBinding importLine = case T.breakOn "(" importLine of
(pre, T.uncons -> Just (_, rest)) ->
-- If the data type is in the import list wiouht the constructor, we should remove it and import it again
let rest' = case parent of
"" -> ", " <> rest
_ -> case T.breakOn parent rest of
(h, T.stripPrefix parent -> Just r) -> case T.uncons (T.dropWhile isSpace r) of
Just (')', _) -> ")" <> h <> r
Just ('(', xs) -> let imported = T.takeWhile (/= ')') xs in T.concat ["," ,imported , "), " , h , removeHeadingComma (T.tail (T.dropWhile (/= ')') r))]
_ -> "), " <> h <> r
_ -> "), " <> rest
binding' = (if T.null parent then id else T.init) renderedBinding
in removeTrailingComma $ T.concat [pre, "(", binding', rest']
Comment on lines +1116 to +1125
Copy link
Collaborator

@pepeiborra pepeiborra Nov 25, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this code very hard to read. Could you try to golf it a bit to make it more readable? Some concrete suggestions:

  • whitespace and layout
  • both rest' and binding' perform branching on whether parent is null. Make this more apparent
  • introduce more names or add more comments, there's a lot of character matching that is hard to follow
  • consider replacing some of the character matching with regexes if that makes things simpler.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for your suggestions! I totally agree. It lacks of extensibility and is hard to read 😢

Copy link
Collaborator

@pepeiborra pepeiborra Dec 5, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@berberman are you planning to implement this suggestion?

_ ->
error $
"importLine does not have the expected structure: "
<> T.unpack importLine

where
removeTrailingComma (T.unsnoc -> Just (T.unsnoc -> Just (T.unsnoc -> Just (xs, ','), ' '), ')')) = xs <> ")"
removeTrailingComma (T.unsnoc -> Just (xs, x)) = T.snoc (removeTrailingComma xs) x
removeTrailingComma x = x
removeHeadingComma (T.stripStart -> s) = case T.uncons s of
Just (',', xs) -> xs
_ -> s
-- | 'matchRegex' combined with 'unifySpaces'
matchRegexUnifySpaces :: T.Text -> T.Text -> Maybe [T.Text]
matchRegexUnifySpaces message = matchRegex (unifySpaces message)
Expand Down
26 changes: 23 additions & 3 deletions test/exe/Main.hs
Original file line number Diff line number Diff line change
Expand Up @@ -996,7 +996,7 @@ extendImportTests = testGroup "extend import actions"
, "main = print (stuffB .* stuffB)"
])
(Range (Position 3 17) (Position 3 18))
["Add .* to the import list of ModuleA"]
["Add (.*) to the import list of ModuleA"]
(T.unlines
[ "module ModuleB where"
, "import ModuleA as A ((.*), stuffB)"
Expand All @@ -1021,7 +1021,7 @@ extendImportTests = testGroup "extend import actions"
, "b :: A"
, "b = 0"
])
, (`xfail` "known broken") $ testSession "extend single line import with constructor" $ template
, testSession "extend single line import with constructor" $ template
[("ModuleA.hs", T.unlines
[ "module ModuleA where"
, "data A = Constructor"
Expand All @@ -1033,12 +1033,32 @@ extendImportTests = testGroup "extend import actions"
, "b = Constructor"
])
(Range (Position 2 5) (Position 2 5))
["Add Constructor to the import list of ModuleA"]
["Add A(Constructor) to the import list of ModuleA"]
(T.unlines
[ "module ModuleB where"
, "import ModuleA (A(Constructor))"
, "b :: A"
, "b = Constructor"
])
, testSession "extend single line import with mixed constructors" $ template
[("ModuleA.hs", T.unlines
[ "module ModuleA where"
, "data A = ConstructorFoo | ConstructorBar"
, "a = 1"
])]
("ModuleB.hs", T.unlines
[ "module ModuleB where"
, "import ModuleA (A(ConstructorBar),a)"
, "b :: A"
, "b = ConstructorFoo"
])
(Range (Position 2 5) (Position 2 5))
["Add A(ConstructorFoo) to the import list of ModuleA"]
(T.unlines
[ "module ModuleB where"
, "import ModuleA (A(ConstructorFoo,ConstructorBar), a)"
, "b :: A"
, "b = ConstructorFoo"
])
, testSession "extend single line qualified import with value" $ template
[("ModuleA.hs", T.unlines
Expand Down