Skip to content

Commit 306aaa0

Browse files
sergvpepeiborra
andauthored
Improve name export code action (#2847)
* Improve code action that exports a name to reuse export list style * Remove outdated comment * Prefer comparisons on Text to comparisons on String Co-authored-by: Pepe Iborra <[email protected]>
1 parent c14cbdb commit 306aaa0

File tree

2 files changed

+137
-32
lines changed

2 files changed

+137
-32
lines changed

Diff for: ghcide/src/Development/IDE/Plugin/CodeAction.hs

+59-32
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,6 @@
55
{-# LANGUAGE DuplicateRecordFields #-}
66
{-# LANGUAGE GADTs #-}
77

8-
-- | Go to the definition of a variable.
9-
108
module Development.IDE.Plugin.CodeAction
119
(
1210
iePluginDescriptor,
@@ -21,7 +19,8 @@ module Development.IDE.Plugin.CodeAction
2119

2220
import Control.Applicative ((<|>))
2321
import Control.Arrow (second,
24-
(>>>))
22+
(>>>),
23+
(&&&))
2524
import Control.Concurrent.STM.Stats (atomically)
2625
import Control.Monad (guard, join,
2726
msum)
@@ -37,6 +36,7 @@ import Data.List.NonEmpty (NonEmpty ((:
3736
import qualified Data.List.NonEmpty as NE
3837
import qualified Data.Map as M
3938
import Data.Maybe
39+
import Data.Ord (comparing)
4040
import qualified Data.Rope.UTF16 as Rope
4141
import qualified Data.Set as S
4242
import qualified Data.Text as T
@@ -107,7 +107,7 @@ iePluginDescriptor :: PluginId -> PluginDescriptor IdeState
107107
iePluginDescriptor plId =
108108
let old =
109109
mkGhcideCAsPlugin [
110-
wrap suggestExtendImport
110+
wrap suggestExtendImport
111111
, wrap suggestImportDisambiguation
112112
, wrap suggestNewOrExtendImportForClassMethod
113113
, wrap suggestNewImport
@@ -227,8 +227,10 @@ findInstanceHead df instanceHead decls =
227227

228228
#if MIN_VERSION_ghc(9,2,0)
229229
findDeclContainingLoc :: Foldable t => Position -> t (GenLocated (SrcSpanAnn' a) e) -> Maybe (GenLocated (SrcSpanAnn' a) e)
230+
#elif MIN_VERSION_ghc(8,10,0)
231+
findDeclContainingLoc :: Foldable t => Position -> t (GenLocated SrcSpan e) -> Maybe (GenLocated SrcSpan e)
230232
#else
231-
-- TODO populate this type signature for GHC versions <9.2
233+
-- TODO populate this type signature for GHC versions <8.10
232234
#endif
233235
findDeclContainingLoc loc = find (\(L l _) -> loc `isInsideSrcSpan` locA l)
234236

@@ -549,48 +551,73 @@ suggestDeleteUnusedBinding
549551
data ExportsAs = ExportName | ExportPattern | ExportFamily | ExportAll
550552
deriving (Eq)
551553

552-
getLocatedRange :: Located a -> Maybe Range
554+
getLocatedRange :: HasSrcSpan a => a -> Maybe Range
553555
getLocatedRange = srcSpanToRange . getLoc
554556

555-
suggestExportUnusedTopBinding :: Maybe T.Text -> ParsedModule -> Diagnostic -> [(T.Text, TextEdit)]
557+
suggestExportUnusedTopBinding :: Maybe T.Text -> ParsedModule -> Diagnostic -> Maybe (T.Text, TextEdit)
556558
suggestExportUnusedTopBinding srcOpt ParsedModule{pm_parsed_source = L _ HsModule{..}} Diagnostic{..}
557559
-- Foo.hs:4:1: warning: [-Wunused-top-binds] Defined but not used: ‘f’
558560
-- Foo.hs:5:1: warning: [-Wunused-top-binds] Defined but not used: type constructor or class ‘F’
559561
-- Foo.hs:6:1: warning: [-Wunused-top-binds] Defined but not used: data constructor ‘Bar’
560562
| Just source <- srcOpt
561-
, Just [name] <- matchRegexUnifySpaces _message ".*Defined but not used: ‘([^ ]+)’"
562-
<|> matchRegexUnifySpaces _message ".*Defined but not used: type constructor or class ‘([^ ]+)’"
563-
<|> matchRegexUnifySpaces _message ".*Defined but not used: data constructor ‘([^ ]+)’"
564-
, Just (exportType, _) <- find (matchWithDiagnostic _range . snd)
565-
. mapMaybe
566-
(\(L (locA -> l) b) -> if maybe False isTopLevel $ srcSpanToRange l
567-
then exportsAs b else Nothing)
568-
$ hsmodDecls
569-
, Just pos <- (fmap _end . getLocatedRange) . reLoc =<< hsmodExports
570-
, Just needComma <- needsComma source <$> fmap reLoc hsmodExports
571-
, let exportName = (if needComma then ", " else "") <> printExport exportType name
572-
insertPos = pos {_character = pred $ _character pos}
573-
= [("Export ‘" <> name <> "", TextEdit (Range insertPos insertPos) exportName)]
574-
| otherwise = []
563+
, Just [_, name] <-
564+
matchRegexUnifySpaces
565+
_message
566+
".*Defined but not used: (type constructor or class |data constructor )?‘([^ ]+)’"
567+
, Just (exportType, _) <-
568+
find (matchWithDiagnostic _range . snd)
569+
. mapMaybe (\(L l b) -> if isTopLevel (locA l) then exportsAs b else Nothing)
570+
$ hsmodDecls
571+
, Just exports <- fmap (fmap reLoc) . reLoc <$> hsmodExports
572+
, Just exportsEndPos <- _end <$> getLocatedRange exports
573+
, let name' = printExport exportType name
574+
sep = exportSep source $ map getLocatedRange <$> exports
575+
exportName = case sep of
576+
Nothing -> (if needsComma source exports then ", " else "") <> name'
577+
Just s -> s <> name'
578+
exportsEndPos' = exportsEndPos { _character = pred $ _character exportsEndPos }
579+
insertPos = fromMaybe exportsEndPos' $ case (sep, unLoc exports) of
580+
(Just _, exports'@(_:_)) -> fmap _end . getLocatedRange $ last exports'
581+
_ -> Nothing
582+
= Just ("Export ‘" <> name <> "", TextEdit (Range insertPos insertPos) exportName)
583+
| otherwise = Nothing
575584
where
576-
-- we get the last export and the closing bracket and check for comma in that range
577-
needsComma :: T.Text -> Located [LIE GhcPs] -> Bool
585+
exportSep :: T.Text -> Located [Maybe Range] -> Maybe T.Text
586+
exportSep src (L (RealSrcSpan _ _) xs@(_ : tl@(_ : _))) =
587+
case mapMaybe (\(e, s) -> (,) <$> e <*> s) $ zip (fmap _end <$> xs) (fmap _start <$> tl) of
588+
[] -> Nothing
589+
bounds -> Just smallestSep
590+
where
591+
smallestSep
592+
= snd
593+
$ minimumBy (comparing fst)
594+
$ map (T.length &&& id)
595+
$ nubOrd
596+
$ map (\(prevEnd, nextStart) -> textInRange (Range prevEnd nextStart) src) bounds
597+
exportSep _ _ = Nothing
598+
599+
-- We get the last export and the closing bracket and check for comma in that range.
600+
needsComma :: T.Text -> Located [Located (IE GhcPs)] -> Bool
578601
needsComma _ (L _ []) = False
579602
needsComma source (L (RealSrcSpan l _) exports) =
580-
let closeParan = _end $ realSrcSpanToRange l
581-
lastExport = fmap _end . getLocatedRange $ last $ fmap reLoc exports
582-
in case lastExport of
583-
Just lastExport -> not $ T.isInfixOf "," $ textInRange (Range lastExport closeParan) source
603+
let closeParen = _end $ realSrcSpanToRange l
604+
lastExport = fmap _end . getLocatedRange $ last exports
605+
in
606+
case lastExport of
607+
Just lastExport ->
608+
not $ T.any (== ',') $ textInRange (Range lastExport closeParen) source
584609
_ -> False
585610
needsComma _ _ = False
586611

587-
opLetter :: String
612+
opLetter :: T.Text
588613
opLetter = ":!#$%&*+./<=>?@\\^|-~"
589614

590615
parenthesizeIfNeeds :: Bool -> T.Text -> T.Text
591616
parenthesizeIfNeeds needsTypeKeyword x
592-
| T.head x `elem` opLetter = (if needsTypeKeyword then "type " else "") <> "(" <> x <>")"
617+
| T.any (c ==) opLetter = (if needsTypeKeyword then "type " else "") <> "(" <> x <> ")"
593618
| otherwise = x
619+
where
620+
c = T.head x
594621

595622
matchWithDiagnostic :: Range -> Located (IdP GhcPs) -> Bool
596623
matchWithDiagnostic Range{_start=l,_end=r} x =
@@ -603,8 +630,8 @@ suggestExportUnusedTopBinding srcOpt ParsedModule{pm_parsed_source = L _ HsModul
603630
printExport ExportFamily x = parenthesizeIfNeeds True x
604631
printExport ExportAll x = parenthesizeIfNeeds True x <> "(..)"
605632

606-
isTopLevel :: Range -> Bool
607-
isTopLevel l = (_character . _start) l == 0
633+
isTopLevel :: SrcSpan -> Bool
634+
isTopLevel span = fmap (_character . _start) (srcSpanToRange span) == Just 0
608635

609636
exportsAs :: HsDecl GhcPs -> Maybe (ExportsAs, Located (IdP GhcPs))
610637
exportsAs (ValD _ FunBind {fun_id}) = Just (ExportName, reLoc fun_id)
@@ -1218,7 +1245,7 @@ removeRedundantConstraints df (L _ HsModule {hsmodDecls}) Diagnostic{..}
12181245
= [(actionTitle redundantConstraintList typeSignatureName, rewrite)]
12191246
| otherwise = []
12201247
where
1221-
toRemove df list a = showSDoc df (ppr a) `elem` (T.unpack <$> list)
1248+
toRemove df list a = T.pack (showSDoc df (ppr a)) `elem` list
12221249

12231250
parseConstraints :: T.Text -> [T.Text]
12241251
parseConstraints t = t

Diff for: ghcide/test/exe/Main.hs

+78
Original file line numberDiff line numberDiff line change
@@ -3653,6 +3653,84 @@ exportUnusedTests = testGroup "export unused actions"
36533653
, " bar) where"
36543654
, "foo = id"
36553655
, "bar = foo"])
3656+
, testSession "style of multiple exports is preserved 1" $ template
3657+
(T.unlines
3658+
[ "{-# OPTIONS_GHC -Wunused-top-binds #-}"
3659+
, "module A"
3660+
, " ( foo"
3661+
, " , bar"
3662+
, " ) where"
3663+
, "foo = id"
3664+
, "bar = foo"
3665+
, "baz = bar"
3666+
])
3667+
(R 7 0 7 3)
3668+
"Export ‘baz’"
3669+
(Just $ T.unlines
3670+
[ "{-# OPTIONS_GHC -Wunused-top-binds #-}"
3671+
, "module A"
3672+
, " ( foo"
3673+
, " , bar"
3674+
, " , baz"
3675+
, " ) where"
3676+
, "foo = id"
3677+
, "bar = foo"
3678+
, "baz = bar"
3679+
])
3680+
, testSession "style of multiple exports is preserved 2" $ template
3681+
(T.unlines
3682+
[ "{-# OPTIONS_GHC -Wunused-top-binds #-}"
3683+
, "module A"
3684+
, " ( foo,"
3685+
, " bar"
3686+
, " ) where"
3687+
, "foo = id"
3688+
, "bar = foo"
3689+
, "baz = bar"
3690+
])
3691+
(R 7 0 7 3)
3692+
"Export ‘baz’"
3693+
(Just $ T.unlines
3694+
[ "{-# OPTIONS_GHC -Wunused-top-binds #-}"
3695+
, "module A"
3696+
, " ( foo,"
3697+
, " bar,"
3698+
, " baz"
3699+
, " ) where"
3700+
, "foo = id"
3701+
, "bar = foo"
3702+
, "baz = bar"
3703+
])
3704+
, testSession "style of multiple exports is preserved and selects smallest export separator" $ template
3705+
(T.unlines
3706+
[ "{-# OPTIONS_GHC -Wunused-top-binds #-}"
3707+
, "module A"
3708+
, " ( foo"
3709+
, " , bar"
3710+
, " -- * For testing"
3711+
, " , baz"
3712+
, " ) where"
3713+
, "foo = id"
3714+
, "bar = foo"
3715+
, "baz = bar"
3716+
, "quux = bar"
3717+
])
3718+
(R 10 0 10 4)
3719+
"Export ‘quux’"
3720+
(Just $ T.unlines
3721+
[ "{-# OPTIONS_GHC -Wunused-top-binds #-}"
3722+
, "module A"
3723+
, " ( foo"
3724+
, " , bar"
3725+
, " -- * For testing"
3726+
, " , baz"
3727+
, " , quux"
3728+
, " ) where"
3729+
, "foo = id"
3730+
, "bar = foo"
3731+
, "baz = bar"
3732+
, "quux = bar"
3733+
])
36563734
, testSession "unused pattern synonym" $ template
36573735
(T.unlines
36583736
[ "{-# OPTIONS_GHC -Wunused-top-binds #-}"

0 commit comments

Comments
 (0)