Skip to content

Commit ac83ca4

Browse files
santiweightSantiago Weight
and
Santiago Weight
authored
Cleanup Development.IDE.CodeAction (#3360)
* refact: extract ImportUtils * refact: extract FillTypeWildcard * refact: Extract FillHole * remove partial hlint warnings * fix import Co-authored-by: Santiago Weight <[email protected]>
1 parent 1e88d16 commit ac83ca4

File tree

5 files changed

+273
-222
lines changed

5 files changed

+273
-222
lines changed

plugins/hls-refactor-plugin/hls-refactor-plugin.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,9 @@ library
3636
Development.IDE.Plugin.CodeAction.PositionIndexed
3737
Development.IDE.Plugin.Plugins.AddArgument
3838
Development.IDE.Plugin.Plugins.Diagnostic
39+
Development.IDE.Plugin.Plugins.FillHole
40+
Development.IDE.Plugin.Plugins.FillTypeWildcard
41+
Development.IDE.Plugin.Plugins.ImportUtils
3942
default-extensions:
4043
BangPatterns
4144
CPP

plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs

Lines changed: 5 additions & 222 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,9 @@ import Development.IDE.Plugin.CodeAction.Util
5858
import Development.IDE.Plugin.Completions.Types
5959
import qualified Development.IDE.Plugin.Plugins.AddArgument
6060
import Development.IDE.Plugin.Plugins.Diagnostic
61+
import Development.IDE.Plugin.Plugins.FillHole (suggestFillHole)
62+
import Development.IDE.Plugin.Plugins.FillTypeWildcard (suggestFillTypeWildcard)
63+
import Development.IDE.Plugin.Plugins.ImportUtils
6164
import Development.IDE.Plugin.TypeLenses (suggestSignature)
6265
import Development.IDE.Types.Exports
6366
import Development.IDE.Types.Location
@@ -72,7 +75,7 @@ import qualified Language.LSP.Server as LSP
7275
import Language.LSP.Types (ApplyWorkspaceEditParams (..),
7376
CodeAction (..),
7477
CodeActionContext (CodeActionContext, _diagnostics),
75-
CodeActionKind (CodeActionQuickFix, CodeActionUnknown),
78+
CodeActionKind (CodeActionQuickFix),
7679
CodeActionParams (CodeActionParams),
7780
Command,
7881
Diagnostic (..),
@@ -90,8 +93,7 @@ import Language.LSP.Types (ApplyWorkspa
9093
import Language.LSP.VFS (VirtualFile,
9194
_file_text)
9295
import qualified Text.Fuzzy.Parallel as TFP
93-
import Text.Regex.TDFA (mrAfter,
94-
(=~), (=~~))
96+
import Text.Regex.TDFA ((=~), (=~~))
9597
#if MIN_VERSION_ghc(9,2,0)
9698
import GHC (AddEpAnn (AddEpAnn),
9799
Anchor (anchor_op),
@@ -915,17 +917,6 @@ newDefinitionAction IdeOptions {..} parsedModule Range {_start} name typ
915917
ParsedModule {pm_parsed_source = L _ HsModule {hsmodDecls}} = parsedModule
916918

917919

918-
suggestFillTypeWildcard :: Diagnostic -> [(T.Text, TextEdit)]
919-
suggestFillTypeWildcard Diagnostic{_range=_range,..}
920-
-- Foo.hs:3:8: error:
921-
-- * Found type wildcard `_' standing for `p -> p1 -> p'
922-
923-
| "Found type wildcard" `T.isInfixOf` _message
924-
, " standing for " `T.isInfixOf` _message
925-
, typeSignature <- extractWildCardTypeSignature _message
926-
= [("Use type signature: ‘" <> typeSignature <> "", TextEdit _range typeSignature)]
927-
| otherwise = []
928-
929920
{- Handles two variants with different formatting
930921
931922
1. Could not find module ‘Data.Cha’
@@ -953,88 +944,6 @@ suggestModuleTypo Diagnostic{_range=_range,..}
953944
_ -> Nothing
954945

955946

956-
suggestFillHole :: Diagnostic -> [(T.Text, TextEdit)]
957-
suggestFillHole Diagnostic{_range=_range,..}
958-
| Just holeName <- extractHoleName _message
959-
, (holeFits, refFits) <- processHoleSuggestions (T.lines _message) =
960-
let isInfixHole = _message =~ addBackticks holeName :: Bool in
961-
map (proposeHoleFit holeName False isInfixHole) holeFits
962-
++ map (proposeHoleFit holeName True isInfixHole) refFits
963-
| otherwise = []
964-
where
965-
extractHoleName = fmap head . flip matchRegexUnifySpaces "Found hole: ([^ ]*)"
966-
addBackticks text = "`" <> text <> "`"
967-
addParens text = "(" <> text <> ")"
968-
proposeHoleFit holeName parenthise isInfixHole name =
969-
let isInfixOperator = T.head name == '('
970-
name' = getOperatorNotation isInfixHole isInfixOperator name in
971-
( "replace " <> holeName <> " with " <> name
972-
, TextEdit _range (if parenthise then addParens name' else name')
973-
)
974-
getOperatorNotation True False name = addBackticks name
975-
getOperatorNotation True True name = T.drop 1 (T.dropEnd 1 name)
976-
getOperatorNotation _isInfixHole _isInfixOperator name = name
977-
978-
processHoleSuggestions :: [T.Text] -> ([T.Text], [T.Text])
979-
processHoleSuggestions mm = (holeSuggestions, refSuggestions)
980-
{-
981-
• Found hole: _ :: LSP.Handlers
982-
983-
Valid hole fits include def
984-
Valid refinement hole fits include
985-
fromMaybe (_ :: LSP.Handlers) (_ :: Maybe LSP.Handlers)
986-
fromJust (_ :: Maybe LSP.Handlers)
987-
haskell-lsp-types-0.22.0.0:Language.LSP.Types.Window.$sel:_value:ProgressParams (_ :: ProgressParams
988-
LSP.Handlers)
989-
T.foldl (_ :: LSP.Handlers -> Char -> LSP.Handlers)
990-
(_ :: LSP.Handlers)
991-
(_ :: T.Text)
992-
T.foldl' (_ :: LSP.Handlers -> Char -> LSP.Handlers)
993-
(_ :: LSP.Handlers)
994-
(_ :: T.Text)
995-
-}
996-
where
997-
t = id @T.Text
998-
holeSuggestions = do
999-
-- get the text indented under Valid hole fits
1000-
validHolesSection <-
1001-
getIndentedGroupsBy (=~ t " *Valid (hole fits|substitutions) include") mm
1002-
-- the Valid hole fits line can contain a hole fit
1003-
holeFitLine <-
1004-
mapHead
1005-
(mrAfter . (=~ t " *Valid (hole fits|substitutions) include"))
1006-
validHolesSection
1007-
let holeFit = T.strip $ T.takeWhile (/= ':') holeFitLine
1008-
guard (not $ T.null holeFit)
1009-
return holeFit
1010-
refSuggestions = do -- @[]
1011-
-- get the text indented under Valid refinement hole fits
1012-
refinementSection <-
1013-
getIndentedGroupsBy (=~ t " *Valid refinement hole fits include") mm
1014-
-- get the text for each hole fit
1015-
holeFitLines <- getIndentedGroups (tail refinementSection)
1016-
let holeFit = T.strip $ T.unwords holeFitLines
1017-
guard $ not $ holeFit =~ t "Some refinement hole fits suppressed"
1018-
return holeFit
1019-
1020-
mapHead f (a:aa) = f a : aa
1021-
mapHead _ [] = []
1022-
1023-
-- > getIndentedGroups [" H1", " l1", " l2", " H2", " l3"] = [[" H1,", " l1", " l2"], [" H2", " l3"]]
1024-
getIndentedGroups :: [T.Text] -> [[T.Text]]
1025-
getIndentedGroups [] = []
1026-
getIndentedGroups ll@(l:_) = getIndentedGroupsBy ((== indentation l) . indentation) ll
1027-
-- |
1028-
-- > getIndentedGroupsBy (" H" `isPrefixOf`) [" H1", " l1", " l2", " H2", " l3"] = [[" H1", " l1", " l2"], [" H2", " l3"]]
1029-
getIndentedGroupsBy :: (T.Text -> Bool) -> [T.Text] -> [[T.Text]]
1030-
getIndentedGroupsBy pred inp = case dropWhile (not.pred) inp of
1031-
(l:ll) -> case span (\l' -> indentation l < indentation l') ll of
1032-
(indented, rest) -> (l:indented) : getIndentedGroupsBy pred rest
1033-
_ -> []
1034-
1035-
indentation :: T.Text -> Int
1036-
indentation = T.length . T.takeWhile isSpace
1037-
1038947
#if !MIN_VERSION_ghc(9,3,0)
1039948
suggestExtendImport :: ExportsMap -> ParsedSource -> Diagnostic -> [(T.Text, CodeActionKind, Rewrite)]
1040949
suggestExtendImport exportsMap (L _ HsModule {hsmodImports}) Diagnostic{_range=_range,..}
@@ -1845,64 +1754,6 @@ mkRenameEdit contents range name
18451754
curr <- textInRange range <$> contents
18461755
pure $ "'" `T.isPrefixOf` curr
18471756

1848-
-- | Extract the type and surround it in parentheses except in obviously safe cases.
1849-
--
1850-
-- Inferring when parentheses are actually needed around the type signature would
1851-
-- require understanding both the precedence of the context of the hole and of
1852-
-- the signature itself. Inserting them (almost) unconditionally is ugly but safe.
1853-
extractWildCardTypeSignature :: T.Text -> T.Text
1854-
extractWildCardTypeSignature msg
1855-
| enclosed || not isApp || isToplevelSig = sig
1856-
| otherwise = "(" <> sig <> ")"
1857-
where
1858-
msgSigPart = snd $ T.breakOnEnd "standing for " msg
1859-
(sig, rest) = T.span (/='') . T.dropWhile (=='') . T.dropWhile (/='') $ msgSigPart
1860-
-- If we're completing something like ‘foo :: _’ parens can be safely omitted.
1861-
isToplevelSig = errorMessageRefersToToplevelHole rest
1862-
-- Parenthesize type applications, e.g. (Maybe Char).
1863-
isApp = T.any isSpace sig
1864-
-- Do not add extra parentheses to lists, tuples and already parenthesized types.
1865-
enclosed = not (T.null sig) && (T.head sig, T.last sig) `elem` [('(', ')'), ('[', ']')]
1866-
1867-
-- | Detect whether user wrote something like @foo :: _@ or @foo :: (_, Int)@.
1868-
-- The former is considered toplevel case for which the function returns 'True',
1869-
-- the latter is not toplevel and the returned value is 'False'.
1870-
--
1871-
-- When type hole is at toplevel then there’s a line starting with
1872-
-- "• In the type signature" which ends with " :: _" like in the
1873-
-- following snippet:
1874-
--
1875-
-- source/library/Language/Haskell/Brittany/Internal.hs:131:13: error:
1876-
-- • Found type wildcard ‘_’ standing for ‘HsDecl GhcPs’
1877-
-- To use the inferred type, enable PartialTypeSignatures
1878-
-- • In the type signature: decl :: _
1879-
-- In an equation for ‘splitAnnots’:
1880-
-- splitAnnots m@HsModule {hsmodAnn, hsmodDecls}
1881-
-- = undefined
1882-
-- where
1883-
-- ann :: SrcSpanAnnA
1884-
-- decl :: _
1885-
-- L ann decl = head hsmodDecls
1886-
-- • Relevant bindings include
1887-
-- [REDACTED]
1888-
--
1889-
-- When type hole is not at toplevel there’s a stack of where
1890-
-- the hole was located ending with "In the type signature":
1891-
--
1892-
-- source/library/Language/Haskell/Brittany/Internal.hs:130:20: error:
1893-
-- • Found type wildcard ‘_’ standing for ‘GhcPs’
1894-
-- To use the inferred type, enable PartialTypeSignatures
1895-
-- • In the first argument of ‘HsDecl’, namely ‘_’
1896-
-- In the type ‘HsDecl _’
1897-
-- In the type signature: decl :: HsDecl _
1898-
-- • Relevant bindings include
1899-
-- [REDACTED]
1900-
errorMessageRefersToToplevelHole :: T.Text -> Bool
1901-
errorMessageRefersToToplevelHole msg =
1902-
not (T.null prefix) && " :: _" `T.isSuffixOf` T.takeWhile (/= '\n') rest
1903-
where
1904-
(prefix, rest) = T.breakOn "• In the type signature:" msg
1905-
19061757
extractRenamableTerms :: T.Text -> [T.Text]
19071758
extractRenamableTerms msg
19081759
-- Account for both "Variable not in scope" and "Not in scope"
@@ -2054,71 +1905,3 @@ matchRegExMultipleImports message = do
20541905
imps <- regExImports imports
20551906
return (binding, imps)
20561907

2057-
-- | Possible import styles for an 'IdentInfo'.
2058-
--
2059-
-- The first 'Text' parameter corresponds to the 'rendered' field of the
2060-
-- 'IdentInfo'.
2061-
data ImportStyle
2062-
= ImportTopLevel T.Text
2063-
-- ^ Import a top-level export from a module, e.g., a function, a type, a
2064-
-- class.
2065-
--
2066-
-- > import M (?)
2067-
--
2068-
-- Some exports that have a parent, like a type-class method or an
2069-
-- associated type/data family, can still be imported as a top-level
2070-
-- import.
2071-
--
2072-
-- Note that this is not the case for constructors, they must always be
2073-
-- imported as part of their parent data type.
2074-
2075-
| ImportViaParent T.Text T.Text
2076-
-- ^ Import an export (first parameter) through its parent (second
2077-
-- parameter).
2078-
--
2079-
-- import M (P(?))
2080-
--
2081-
-- @P@ and @?@ can be a data type and a constructor, a class and a method,
2082-
-- a class and an associated type/data family, etc.
2083-
2084-
| ImportAllConstructors T.Text
2085-
-- ^ Import all constructors for a specific data type.
2086-
--
2087-
-- import M (P(..))
2088-
--
2089-
-- @P@ can be a data type or a class.
2090-
deriving Show
2091-
2092-
importStyles :: IdentInfo -> NonEmpty ImportStyle
2093-
importStyles IdentInfo {parent, rendered, isDatacon}
2094-
| Just p <- parent
2095-
-- Constructors always have to be imported via their parent data type, but
2096-
-- methods and associated type/data families can also be imported as
2097-
-- top-level exports.
2098-
= ImportViaParent rendered p
2099-
:| [ImportTopLevel rendered | not isDatacon]
2100-
<> [ImportAllConstructors p]
2101-
| otherwise
2102-
= ImportTopLevel rendered :| []
2103-
2104-
-- | Used for adding new imports
2105-
renderImportStyle :: ImportStyle -> T.Text
2106-
renderImportStyle (ImportTopLevel x) = x
2107-
renderImportStyle (ImportViaParent x p@(T.uncons -> Just ('(', _))) = "type " <> p <> "(" <> x <> ")"
2108-
renderImportStyle (ImportViaParent x p) = p <> "(" <> x <> ")"
2109-
renderImportStyle (ImportAllConstructors p) = p <> "(..)"
2110-
2111-
-- | Used for extending import lists
2112-
unImportStyle :: ImportStyle -> (Maybe String, String)
2113-
unImportStyle (ImportTopLevel x) = (Nothing, T.unpack x)
2114-
unImportStyle (ImportViaParent x y) = (Just $ T.unpack y, T.unpack x)
2115-
unImportStyle (ImportAllConstructors x) = (Just $ T.unpack x, wildCardSymbol)
2116-
2117-
2118-
quickFixImportKind' :: T.Text -> ImportStyle -> CodeActionKind
2119-
quickFixImportKind' x (ImportTopLevel _) = CodeActionUnknown $ "quickfix.import." <> x <> ".list.topLevel"
2120-
quickFixImportKind' x (ImportViaParent _ _) = CodeActionUnknown $ "quickfix.import." <> x <> ".list.withParent"
2121-
quickFixImportKind' x (ImportAllConstructors _) = CodeActionUnknown $ "quickfix.import." <> x <> ".list.allConstructors"
2122-
2123-
quickFixImportKind :: T.Text -> CodeActionKind
2124-
quickFixImportKind x = CodeActionUnknown $ "quickfix.import." <> x
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
module Development.IDE.Plugin.Plugins.FillHole
2+
( suggestFillHole
3+
) where
4+
5+
import Control.Monad (guard)
6+
import Data.Char
7+
import qualified Data.Text as T
8+
import Development.IDE.Plugin.Plugins.Diagnostic
9+
import Language.LSP.Types (Diagnostic (..),
10+
TextEdit (TextEdit))
11+
import Text.Regex.TDFA (MatchResult (..),
12+
(=~))
13+
14+
suggestFillHole :: Diagnostic -> [(T.Text, TextEdit)]
15+
suggestFillHole Diagnostic{_range=_range,..}
16+
| Just holeName <- extractHoleName _message
17+
, (holeFits, refFits) <- processHoleSuggestions (T.lines _message) =
18+
let isInfixHole = _message =~ addBackticks holeName :: Bool in
19+
map (proposeHoleFit holeName False isInfixHole) holeFits
20+
++ map (proposeHoleFit holeName True isInfixHole) refFits
21+
| otherwise = []
22+
where
23+
extractHoleName = fmap (headOrThrow "impossible") . flip matchRegexUnifySpaces "Found hole: ([^ ]*)"
24+
addBackticks text = "`" <> text <> "`"
25+
addParens text = "(" <> text <> ")"
26+
proposeHoleFit holeName parenthise isInfixHole name =
27+
case T.uncons name of
28+
Nothing -> error "impossible: empty name provided by ghc"
29+
Just (firstChr, _) ->
30+
let isInfixOperator = firstChr == '('
31+
name' = getOperatorNotation isInfixHole isInfixOperator name in
32+
( "replace " <> holeName <> " with " <> name
33+
, TextEdit _range (if parenthise then addParens name' else name')
34+
)
35+
getOperatorNotation True False name = addBackticks name
36+
getOperatorNotation True True name = T.drop 1 (T.dropEnd 1 name)
37+
getOperatorNotation _isInfixHole _isInfixOperator name = name
38+
headOrThrow msg = \case
39+
[] -> error msg
40+
(x:_) -> x
41+
42+
processHoleSuggestions :: [T.Text] -> ([T.Text], [T.Text])
43+
processHoleSuggestions mm = (holeSuggestions, refSuggestions)
44+
{-
45+
• Found hole: _ :: LSP.Handlers
46+
47+
Valid hole fits include def
48+
Valid refinement hole fits include
49+
fromMaybe (_ :: LSP.Handlers) (_ :: Maybe LSP.Handlers)
50+
fromJust (_ :: Maybe LSP.Handlers)
51+
haskell-lsp-types-0.22.0.0:Language.LSP.Types.Window.$sel:_value:ProgressParams (_ :: ProgressParams
52+
LSP.Handlers)
53+
T.foldl (_ :: LSP.Handlers -> Char -> LSP.Handlers)
54+
(_ :: LSP.Handlers)
55+
(_ :: T.Text)
56+
T.foldl' (_ :: LSP.Handlers -> Char -> LSP.Handlers)
57+
(_ :: LSP.Handlers)
58+
(_ :: T.Text)
59+
-}
60+
where
61+
t = id @T.Text
62+
holeSuggestions = do
63+
-- get the text indented under Valid hole fits
64+
validHolesSection <-
65+
getIndentedGroupsBy (=~ t " *Valid (hole fits|substitutions) include") mm
66+
-- the Valid hole fits line can contain a hole fit
67+
holeFitLine <-
68+
mapHead
69+
(mrAfter . (=~ t " *Valid (hole fits|substitutions) include"))
70+
validHolesSection
71+
let holeFit = T.strip $ T.takeWhile (/= ':') holeFitLine
72+
guard (not $ T.null holeFit)
73+
return holeFit
74+
refSuggestions = do -- @[]
75+
-- get the text indented under Valid refinement hole fits
76+
refinementSection <-
77+
getIndentedGroupsBy (=~ t " *Valid refinement hole fits include") mm
78+
case refinementSection of
79+
[] -> error "GHC provided invalid hole fit options"
80+
(_:refinementSection) -> do
81+
-- get the text for each hole fit
82+
holeFitLines <- getIndentedGroups refinementSection
83+
let holeFit = T.strip $ T.unwords holeFitLines
84+
guard $ not $ holeFit =~ t "Some refinement hole fits suppressed"
85+
return holeFit
86+
87+
mapHead f (a:aa) = f a : aa
88+
mapHead _ [] = []
89+
90+
-- > getIndentedGroups [" H1", " l1", " l2", " H2", " l3"] = [[" H1,", " l1", " l2"], [" H2", " l3"]]
91+
getIndentedGroups :: [T.Text] -> [[T.Text]]
92+
getIndentedGroups [] = []
93+
getIndentedGroups ll@(l:_) = getIndentedGroupsBy ((== indentation l) . indentation) ll
94+
-- |
95+
-- > getIndentedGroupsBy (" H" `isPrefixOf`) [" H1", " l1", " l2", " H2", " l3"] = [[" H1", " l1", " l2"], [" H2", " l3"]]
96+
getIndentedGroupsBy :: (T.Text -> Bool) -> [T.Text] -> [[T.Text]]
97+
getIndentedGroupsBy pred inp = case dropWhile (not.pred) inp of
98+
(l:ll) -> case span (\l' -> indentation l < indentation l') ll of
99+
(indented, rest) -> (l:indented) : getIndentedGroupsBy pred rest
100+
_ -> []
101+
102+
indentation :: T.Text -> Int
103+
indentation = T.length . T.takeWhile isSpace
104+

0 commit comments

Comments
 (0)