diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5d34450ec9..ac585b459b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -262,6 +262,10 @@ jobs: name: Test hls-signature-help-plugin test suite run: cabal test ${CABAL_ARGS} hls-signature-help-plugin-tests || cabal test ${CABAL_ARGS} hls-signature-help-plugin-tests + - if: matrix.test + name: Test hls-export-plugin test suite + run: cabal test ${CABAL_ARGS} hls-export-plugin-tests || cabal test ${CABAL_ARGS} hls-export-plugin-tests + test_post_job: if: always() runs-on: ubuntu-latest diff --git a/ghcide/src/Development/IDE/GHC/Compat/Error.hs b/ghcide/src/Development/IDE/GHC/Compat/Error.hs index 63ec75bfc9..61aef036ef 100644 --- a/ghcide/src/Development/IDE/GHC/Compat/Error.hs +++ b/ghcide/src/Development/IDE/GHC/Compat/Error.hs @@ -32,6 +32,7 @@ module Development.IDE.GHC.Compat.Error ( _TcRnPartialTypeSignatures, _TcRnMissingSignature, _TcRnSolverReport, + _TcRnUnusedTopBind, _TcRnMessageWithInfo, _TypeHole, _ConstraintHole, @@ -82,6 +83,17 @@ _GhcDriverMessage = prism' GhcDriverMessage (\case GhcDriverMessage driverMsg -> Just driverMsg _ -> Nothing) +-- | Focus an unused top-level binding warning (@-Wunused-top-binds@). Structured +-- provenance for this only exists from GHC 9.8 (GHC #20115). +_TcRnUnusedTopBind :: Fold GhcMessage () +#if MIN_VERSION_ghc(9,8,0) +_TcRnUnusedTopBind = _TcRnMessage . folding (\case + TcRnUnusedName _ UnusedNameTopDecl -> Just () + _ -> Nothing) +#else +_TcRnUnusedTopBind = ignored +#endif + -- | Some 'TcRnMessage's are nested in other constructors for additional context. -- For example, 'TcRnWithHsDocContext' and 'TcRnMessageWithInfo'. -- However, in some occasions you don't need the additional context and you just want diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index dd4292df42..ea2bbbcc64 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -1585,6 +1585,7 @@ library hls-refactor-plugin , ghc-boot , regex-tdfa , ghcide == 2.14.0.0 + , haskell-language-server:hls-exactprint-utils , hls-plugin-api == 2.14.0.0 , lsp , text @@ -1762,6 +1763,77 @@ test-suite hls-notes-plugin-tests , hls-test-utils == 2.14.0.0 default-extensions: OverloadedStrings +----------------------------- +-- export plugin +----------------------------- + +library hls-exactprint-utils + import: defaults, pedantic, warnings + exposed-modules: + Development.IDE.GHC.ExactPrint.Annotation + hs-source-dirs: hls-exactprint-utils/src + build-depends: + , ghc + , ghc-exactprint + , ghcide == 2.14.0.0 + default-extensions: + CPP + +flag export + description: Enable export plugin + default: True + manual: True + +common export + if flag(export) + build-depends: haskell-language-server:hls-export-plugin + cpp-options: -Dhls_export + +library hls-export-plugin + import: defaults, pedantic, warnings + if !flag(export) + buildable: False + exposed-modules: + Ide.Plugin.Export + other-modules: + Ide.Plugin.Export.Cursor + Ide.Plugin.Export.ExactPrint + Ide.Plugin.Export.Exports + Ide.Plugin.Export.Utils + hs-source-dirs: plugins/hls-export-plugin/src + default-language: GHC2021 + build-depends: + , containers + , ghc + , ghc-exactprint + , ghcide == 2.14.0.0 + , haskell-language-server:hls-exactprint-utils + , hls-plugin-api == 2.14.0.0 + , lens + , lsp >=2.8 + , stm + , text + default-extensions: + , DataKinds + , LambdaCase + , OverloadedStrings + +test-suite hls-export-plugin-tests + import: defaults, pedantic, test-defaults, warnings + if !flag(export) + buildable: False + type: exitcode-stdio-1.0 + hs-source-dirs: plugins/hls-export-plugin/test + main-is: Main.hs + build-depends: + , filepath + , haskell-language-server:hls-export-plugin + , hls-test-utils == 2.14.0.0 + , lens + , lsp-types + , text + default-extensions: OverloadedStrings + ---------------------------- ---------------------------- -- HLS @@ -1800,6 +1872,7 @@ library , overloadedRecordDot , semanticTokens , notes + , export exposed-modules: Ide.Arguments diff --git a/hls-exactprint-utils/src/Development/IDE/GHC/ExactPrint/Annotation.hs b/hls-exactprint-utils/src/Development/IDE/GHC/ExactPrint/Annotation.hs new file mode 100644 index 0000000000..31129c202b --- /dev/null +++ b/hls-exactprint-utils/src/Development/IDE/GHC/ExactPrint/Annotation.hs @@ -0,0 +1,137 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE RecordWildCards #-} + +-- | Version-agnostic primitives for ghc-exactprint annotations, shared by the +-- refactor and export plugins. +module Development.IDE.GHC.ExactPrint.Annotation + ( epl + , isCommaAnn + , trailingAnns + , overTrailingAnns + , removeTrailingCommaAnn + , ensureTrailingComma + , withTrailingComma + , modifyAnns + , addParens + , parenthesizeName + ) where + +import Data.Bifunctor (first) +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Orphans () +import GHC (LocatedN) +#if MIN_VERSION_ghc(9,11,0) +import GHC (DeltaPos (..), EpAnn (..), + EpToken (..), EpaLocation, + EpaLocation' (..), + NameAdornment (..), + SrcSpanAnnA, TrailingAnn (..)) +import GHC.Types.SrcLoc (UnhelpfulSpanReason (..)) +#elif MIN_VERSION_ghc(9,9,0) +import GHC (DeltaPos (..), EpAnn (..), + EpaLocation, + EpaLocation' (..), + NameAdornment (..), + SrcSpanAnnA, TrailingAnn (..)) +#else +import GHC (Anchor (..), + AnchorOperation (..), + DeltaPos (..), EpAnn (..), + EpaLocation (..), + NameAdornment (NameParens), + SrcSpanAnn' (..), SrcSpanAnnA, + TrailingAnn (..), + emptyComments, realSrcSpan) +import GHC.Types.SrcLoc (generatedSrcSpan) +#endif +import Language.Haskell.GHC.ExactPrint (addComma) + +-- | An entry delta of @n@ spaces on the same line. +epl :: Int -> EpaLocation +#if MIN_VERSION_ghc(9,11,0) +epl n = EpaDelta (UnhelpfulSpan UnhelpfulNoLocationInfo) (SameLine n) [] +#else +epl n = EpaDelta (SameLine n) [] +#endif + +isCommaAnn :: TrailingAnn -> Bool +isCommaAnn AddCommaAnn{} = True +isCommaAnn _ = False + +trailingAnns :: SrcSpanAnnA -> [TrailingAnn] +#if MIN_VERSION_ghc(9,9,0) +trailingAnns (EpAnn _ (AnnListItem as) _) = as +#else +trailingAnns sa = case ann sa of + EpAnn _ (AnnListItem as) _ -> as + _ -> [] +#endif + +-- | Map over an item's trailing annotations, hiding the version-specific 'AnnListItem' shape. +overTrailingAnns :: ([TrailingAnn] -> [TrailingAnn]) -> SrcSpanAnnA -> SrcSpanAnnA +#if MIN_VERSION_ghc(9,9,0) +overTrailingAnns f (EpAnn anc (AnnListItem as) cs) = EpAnn anc (AnnListItem (f as)) cs +#else +overTrailingAnns _ it@(SrcSpanAnn EpAnnNotUsed _) = it +overTrailingAnns f (SrcSpanAnn (EpAnn anc (AnnListItem as) cs) l) = + SrcSpanAnn (EpAnn anc (AnnListItem (f as)) cs) l +#endif + +removeTrailingCommaAnn :: SrcSpanAnnA -> SrcSpanAnnA +removeTrailingCommaAnn = overTrailingAnns (filter (not . isCommaAnn)) + +ensureTrailingComma :: SrcSpanAnnA -> SrcSpanAnnA +ensureTrailingComma ann + | any isCommaAnn (trailingAnns ann) = ann + | otherwise = addComma ann + +-- | Replace an item's trailing comma with @c@, preserving its delta. +withTrailingComma :: TrailingAnn -> SrcSpanAnnA -> SrcSpanAnnA +withTrailingComma c = overTrailingAnns (\as -> filter (not . isCommaAnn) as ++ [c]) + +modifyAnns :: LocatedAn a ast -> (a -> a) -> LocatedAn a ast +#if MIN_VERSION_ghc(9,9,0) +modifyAnns x f = first (fmap f) x +#else +modifyAnns x f = first ((fmap . fmap) f) x +#endif + +addParens :: Bool -> NameAnn -> NameAnn +#if MIN_VERSION_ghc(9,11,0) +addParens True it@NameAnn{} = + it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } +addParens True it@NameAnnCommas{} = + it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } +addParens True it@NameAnnOnly{} = + it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } +addParens True it@NameAnnTrailing{} = + NameAnn{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)), nann_name = epl 0, nann_trailing = nann_trailing it} +#else +addParens True it@NameAnn{} = + it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } +addParens True it@NameAnnCommas{} = + it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } +addParens True it@NameAnnOnly{} = + it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } +addParens True NameAnnTrailing{..} = + NameAnn{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0, nann_name = epl 0, ..} +#endif +addParens _ it = it + +-- | Parenthesize an operator name for an export/import item, e.g. @(<|)@. +parenthesizeName :: LocatedN RdrName -> LocatedN RdrName +#if MIN_VERSION_ghc(9,9,0) +parenthesizeName ln = modifyAnns ln (addParens True) +#else +-- A freshly built name carries EpAnnNotUsed pre-9.9, giving 'addParens' no +-- NameAnn to act on, so install a concrete annotation first. +parenthesizeName (L (SrcSpanAnn ann l) rdr) = + L (SrcSpanAnn (EpAnn anc (addParens True nameAnn) cs) l) rdr + where + (anc, nameAnn, cs) = case ann of + EpAnn a n c -> (a, n, c) + EpAnnNotUsed -> (genAnchor0, NameAnnTrailing [], emptyComments) + +genAnchor0 :: Anchor +genAnchor0 = Anchor (realSrcSpan generatedSrcSpan) (MovedAnchor (SameLine 0)) +#endif diff --git a/plugins/hls-export-plugin/README.md b/plugins/hls-export-plugin/README.md new file mode 100644 index 0000000000..3e71c15412 --- /dev/null +++ b/plugins/hls-export-plugin/README.md @@ -0,0 +1,15 @@ +# Export Plugin + +The export plugin provides code actions for working with a module's export list. It provides code actions for: + +- Export `...`: on top-level declaration symbols. + +Actions are only offered when the module has an explicit export list. + +## Known limitations + +Not yet supported: +- Class methods (`class C where { m :: ... }`) +- Type and data families (standalone or associated) +- Pattern synonyms (`pattern P :: ...`) +- Module re-exports (`module M`) diff --git a/plugins/hls-export-plugin/src/Ide/Plugin/Export.hs b/plugins/hls-export-plugin/src/Ide/Plugin/Export.hs new file mode 100644 index 0000000000..ed7034c907 --- /dev/null +++ b/plugins/hls-export-plugin/src/Ide/Plugin/Export.hs @@ -0,0 +1,76 @@ +{-# LANGUAGE RecordWildCards #-} + +module Ide.Plugin.Export (descriptor) where + +import Control.Concurrent.STM (atomically) +import Control.Lens +import Control.Monad.IO.Class (liftIO) +import Data.Text (Text) +import qualified Data.Text as T +import Development.IDE +import Development.IDE.Core.PluginUtils (runActionE, useE) +import Development.IDE.Core.Shake (getDiagnostics) +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Compat.Error (_TcRnUnusedTopBind, + msgEnvelopeErrorL) +import Ide.Plugin.Error (getNormalizedFilePathE) +import Ide.Plugin.Export.Cursor +import Ide.Plugin.Export.ExactPrint +import Ide.Plugin.Export.Exports +import Ide.Plugin.Export.Utils +import Ide.Types +import qualified Ide.Types as Ide +import qualified Language.LSP.Protocol.Lens as L +import Language.LSP.Protocol.Message (Method (..), SMethod (..)) +import Language.LSP.Protocol.Types + +descriptor :: PluginId -> PluginDescriptor IdeState +descriptor plId = + let exportHandlers = mkPluginHandler SMethod_TextDocumentCodeAction quickCodeActionHandlers + in (defaultPluginDescriptor plId "Code actions for module export lists") + { Ide.pluginHandlers = exportHandlers + } + +quickCodeActionHandlers :: PluginMethodHandler IdeState Method_TextDocumentCodeAction +quickCodeActionHandlers state _plId (CodeActionParams _ _ doc range _) = do + let uri = doc ^. L.uri + nfp <- getNormalizedFilePathE uri + pm <- runActionE "Export.GetParsedModuleWithComments" state (useE GetParsedModuleWithComments nfp) + let ps = pm_parsed_source pm + case (isExplicit ps, locateUnderCursor (range ^. L.start) ps) of + (True, Just under) -> do + -- The names GHC flags as defined-but-unused. Attach the action to the + -- unused diagnostics as well. + unusedDiags <- liftIO $ unusedTopBindDiagnostics state nfp + pure . InL . map InR $ + [ ca + | Just (verb, title, edits) <- + [ addAction under ps + ] + , let fixes = [ d | d <- unusedDiags, locateUnderCursor (d ^. L.range . L.start) ps == Just under ] + ca = mkAction (verb <> " `" <> title <> "`") + & L.edit ?~ singleFileEdit uri edits + & L.diagnostics .~ (if null fixes then Nothing else Just fixes) + ] + _ -> pure (InL []) + +-- | The LSP diagnostics for names GHC reports as unused top-level definitions. +unusedTopBindDiagnostics :: IdeState -> NormalizedFilePath -> IO [Diagnostic] +unusedTopBindDiagnostics state nfp = do + diags <- atomically $ getDiagnostics state + pure [ fdLspDiagnostic d | d <- diags, fdFilePath d == nfp, isUnusedTopBind d ] + where + isUnusedTopBind = + has (fdStructuredMessageL . _SomeStructuredMessage . msgEnvelopeErrorL . _TcRnUnusedTopBind) + +addAction :: UnderCursor -> ParsedSource -> Maybe (Text, Text, [TextEdit]) +addAction under ps = case under of + Decl flavor n + | n `isExported` ps -> Nothing + | otherwise -> ("Export", T.pack (printRdrName n),) <$> addExport ps (mkExportIE flavor n) + Constructor t c + | c `isExported` ps -> Nothing + | otherwise -> + ("Export", T.pack (printRdrName t) <> "(" <> T.pack (printRdrName c) <> ")",) + <$> addConstructorExport t c ps + Header -> Nothing diff --git a/plugins/hls-export-plugin/src/Ide/Plugin/Export/Cursor.hs b/plugins/hls-export-plugin/src/Ide/Plugin/Export/Cursor.hs new file mode 100644 index 0000000000..ff55ca9f44 --- /dev/null +++ b/plugins/hls-export-plugin/src/Ide/Plugin/Export/Cursor.hs @@ -0,0 +1,89 @@ +module Ide.Plugin.Export.Cursor + ( ExportFlavor (..) + , UnderCursor (..) + , locateUnderCursor + ) where + +import Control.Applicative ((<|>)) +import Data.Foldable (toList) +import Data.List (find) +import Data.Maybe +import Development.IDE +import Development.IDE.GHC.Compat + +-- | How a top-level entity is rendered in an export list. +data ExportFlavor + = ExportName -- ^ bare @x@. An operator is parenthesized with no @type@ keyword. Values and type synonyms. + | ExportPattern -- ^ @pattern X@. + | ExportFamily -- ^ bare @T@. An operator becomes @type (:<)@. Type and data families. + | ExportAll -- ^ @T(..)@. An operator becomes @type (:<)(..)@. Data, newtype, and class. + deriving Eq + +data UnderCursor + = Decl ExportFlavor RdrName + | Constructor RdrName RdrName + | Header + deriving Eq + +locateUnderCursor :: Position -> ParsedSource -> Maybe UnderCursor +locateUnderCursor pos ps = classifyHeader pos (unLoc ps) <|> classifyInDecl + where + classifyInDecl = do + L _ decl <- find (\(L l _) -> pos `isInsideSrcSpan` locA l) (hsmodDecls (unLoc ps)) + classifyDecl pos decl + +-- | Match column-free so cursor anywhere on the @module ... where@ line counts. +classifyHeader :: Position -> HsModule GhcPs -> Maybe UnderCursor +classifyHeader pos mod = inName <|> inExports + where + isIn :: HasSrcSpan a => Maybe a -> Maybe UnderCursor + isIn el = el >>= \n -> if pos `isInsideSrcSpanLines` getLoc n then Just Header else Nothing + inName = isIn $ hsmodName mod + inExports = isIn $ hsmodExports mod + +-- | Line-based span containment, column-agnostic. +isInsideSrcSpanLines :: Position -> SrcSpan -> Bool +Position l _ `isInsideSrcSpanLines` r = case srcSpanToRange r of + Just (Range (Position sl _) (Position el _)) -> sl <= l && l <= el + _ -> False + +-- | The exportable entities a top-level declaration defines, each with its +-- export flavor and located name. +declEntities :: HsDecl GhcPs -> [(ExportFlavor, LIdP GhcPs)] +declEntities = \case + ValD _ (PatSynBind _ PSB {psb_id = lname}) -> [(ExportPattern, lname)] + ValD _ FunBind {fun_id = lname} -> [(ExportName, lname)] + TyClD _ DataDecl {tcdLName = lname} -> [(ExportAll, lname)] + TyClD _ ClassDecl {tcdLName = lname} -> [(ExportAll, lname)] + TyClD _ SynDecl {tcdLName = lname} -> [(ExportName, lname)] + TyClD _ FamDecl {tcdFam = fam} -> [(ExportFamily, fdLName fam)] + _ -> [] + +classifyDecl :: Position -> HsDecl GhcPs -> Maybe UnderCursor +classifyDecl pos decl = + listToMaybe [Decl flavor (unLoc n) | (flavor, n) <- declEntities decl, onName n] + <|> typeSigUnderCursor + <|> constructorUnderDecl + where + onName (L l _) = pos `isInsideSrcSpan` locA l + -- A signature is not a definition (so not in 'declEntities'), but its name + -- is still a valid place to invoke the export action from. + typeSigUnderCursor = case decl of + SigD _ (TypeSig _ names _) -> Decl ExportName . unLoc <$> find onName names + _ -> Nothing + constructorUnderDecl = case decl of + TyClD _ DataDecl {tcdLName = lname, tcdDataDefn = HsDataDefn {dd_cons = cons}} + -> Constructor (unLoc lname) <$> constructorUnderCursor pos cons + _ -> Nothing + +constructorUnderCursor :: Position -> DataDefnCons (LConDecl GhcPs) -> Maybe RdrName +constructorUnderCursor pos cons = + listToMaybe . mapMaybe nameAt $ extract_cons cons + where + nameAt (L _ cd) = + listToMaybe [n | L l n <- conDeclNames cd, pos `isInsideSrcSpan` locA l] + + conDeclNames = \case + ConDeclH98 {con_name = lname} -> [lname] + ConDeclGADT {con_names = lnames} -> toList lnames + _ -> [] diff --git a/plugins/hls-export-plugin/src/Ide/Plugin/Export/ExactPrint.hs b/plugins/hls-export-plugin/src/Ide/Plugin/Export/ExactPrint.hs new file mode 100644 index 0000000000..3211f5d7c7 --- /dev/null +++ b/plugins/hls-export-plugin/src/Ide/Plugin/Export/ExactPrint.hs @@ -0,0 +1,283 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE LambdaCase #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE ScopedTypeVariables #-} + +module Ide.Plugin.Export.ExactPrint + ( LExportList + , mkExportIE + , appendIE + , addCtorUnderParent + , printExportList + , toDeltaExportList + ) where + +import Control.Lens (_last, over) +import Data.Bifunctor (first) +import Data.List.NonEmpty (NonEmpty (..)) +import qualified Data.List.NonEmpty as NE +import Data.Text (Text) +import qualified Data.Text as T +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Orphans () +#if MIN_VERSION_ghc(9,11,0) +import GHC (DeltaPos (..), + TrailingAnn (..)) +#elif MIN_VERSION_ghc(9,9,0) +import GHC (DeltaPos (..), + LocatedL, + NoAnn (..), + TrailingAnn (..), + noAnn) +#else +import GHC (DeltaPos (..), + LocatedL, + TrailingAnn (..), + addAnns, + emptyComments, + noAnn) +#endif + +import Language.Haskell.GHC.ExactPrint (addComma, + exactPrint, + makeDeltaAst, + setEntryDP) + +#if MIN_VERSION_ghc(9,11,0) +import GHC (EpToken (..), + LocatedLI) +#else +import GHC (AddEpAnn (..)) +#endif +import Data.Maybe (listToMaybe) +import Development.IDE.GHC.ExactPrint.Annotation (ensureTrailingComma, + epl, isCommaAnn, + parenthesizeName, + removeTrailingCommaAnn, + trailingAnns, + withTrailingComma) +import GHC (LocatedN) +import Ide.Plugin.Export.Cursor (ExportFlavor (..)) +import Ide.Plugin.Export.Utils + +-- | Located @[LIE GhcPs]@, the shape of an export list. Aliases either +-- 'LocatedL' (pre-9.12) or 'LocatedLI'. +#if MIN_VERSION_ghc(9,11,0) +type LExportList = LocatedLI [LIE GhcPs] +#else +type LExportList = LocatedL [LIE GhcPs] +#endif + +mkExportIE :: ExportFlavor -> RdrName -> LIE GhcPs +mkExportIE flavor rdr = case flavor of + ExportName -> ieVar (mkWrappedName WrapPlain rdr) + ExportPattern -> ieVar (mkWrappedName WrapPattern rdr) + ExportFamily -> mkTypeAbsIE' (mkWrappedName keywordWrap rdr) + ExportAll -> mkTypeAllIE' (mkWrappedName keywordWrap rdr) + where + keywordWrap + | isSymOcc (rdrNameOcc rdr) = WrapType + | otherwise = WrapPlain + +ieVar :: LIEWrappedName GhcPs -> LIE GhcPs +ieVar w = + reLocA $ L noSrcSpan $ IEVar +#if MIN_VERSION_ghc(9,8,0) + Nothing +#else + noExtField +#endif + w +#if MIN_VERSION_ghc(9,9,0) + Nothing +#endif + +mkTypeAbsIE' :: LIEWrappedName GhcPs -> LIE GhcPs +mkTypeAbsIE' w = + reLocA $ L noSrcSpan $ IEThingAbs +#if MIN_VERSION_ghc(9,11,0) + Nothing +#elif MIN_VERSION_ghc(9,8,0) + (Nothing, noAnn) +#else + noAnn +#endif + w +#if MIN_VERSION_ghc(9,9,0) + Nothing +#endif + +mkTypeAllIE' :: LIEWrappedName GhcPs -> LIE GhcPs +mkTypeAllIE' w = + reLocA $ L noSrcSpan $ IEThingAll +#if MIN_VERSION_ghc(9,11,0) + (Nothing, (EpTok (epl 1), EpTok (epl 0), EpTok (epl 0))) +#elif MIN_VERSION_ghc(9,9,0) + ( Nothing + , [ AddEpAnn AnnOpenP (epl 1) + , AddEpAnn AnnDotdot (epl 0) + , AddEpAnn AnnCloseP (epl 0) + ] + ) +#elif MIN_VERSION_ghc(9,8,0) + ( Nothing + , addAnns mempty + [ AddEpAnn AnnOpenP (epl 1) + , AddEpAnn AnnDotdot (epl 0) + , AddEpAnn AnnCloseP (epl 0) + ] + emptyComments + ) +#else + (addAnns mempty + [ AddEpAnn AnnOpenP (epl 1) + , AddEpAnn AnnDotdot (epl 0) + , AddEpAnn AnnCloseP (epl 0) + ] + emptyComments) +#endif + w +#if MIN_VERSION_ghc(9,9,0) + Nothing +#endif + +-- | @T(C1, C2, ...)@. The non-empty list is the child constructors. +mkTypeWithIE :: RdrName -> NonEmpty RdrName -> LIE GhcPs +mkTypeWithIE parent ctors = + reLocA $ L noSrcSpan $ IEThingWith +#if MIN_VERSION_ghc(9,11,0) + (Nothing, (EpTok (epl 1), NoEpTok, NoEpTok, EpTok (epl 0))) +#elif MIN_VERSION_ghc(9,9,0) + (Nothing, [AddEpAnn AnnOpenP (epl 1), AddEpAnn AnnCloseP (epl 0)]) +#elif MIN_VERSION_ghc(9,8,0) + ( Nothing + , addAnns mempty + [AddEpAnn AnnOpenP (epl 1), AddEpAnn AnnCloseP (epl 0)] + emptyComments + ) +#else + (addAnns mempty + [AddEpAnn AnnOpenP (epl 1), AddEpAnn AnnCloseP (epl 0)] + emptyComments) +#endif + (mkIEName parent) + NoIEWildcard + children +#if MIN_VERSION_ghc(9,9,0) + Nothing +#endif + where + children = case NE.toList ctors of + [] -> [] -- impossible + (c:cs) -> mkIEName c : map (\x -> first addComma (mkIEName x)) cs + +-- | Map over an @IEThingWith@'s listed constructors, a no-op for any other item. +overThingWithChildren :: ([LIEWrappedName GhcPs] -> [LIEWrappedName GhcPs]) -> IE GhcPs -> IE GhcPs +#if MIN_VERSION_ghc(9,9,0) +overThingWithChildren f (IEThingWith x n w cs docs) = IEThingWith x n w (f cs) docs +#else +overThingWithChildren f (IEThingWith x n w cs) = IEThingWith x n w (f cs) +#endif +overThingWithChildren _ ie = ie + +data WrapKind = WrapPlain | WrapPattern | WrapType + +mkIEName :: RdrName -> LIEWrappedName GhcPs +mkIEName = mkWrappedName WrapPlain + +-- | Wrap an 'RdrName' as an export item. Operators are parenthesized and any +-- @pattern@ or @type@ keyword is followed by a single space. +mkWrappedName :: WrapKind -> RdrName -> LIEWrappedName GhcPs +mkWrappedName kind rdr = + reLocA $ L noSrcSpan $ case kind of + WrapPlain -> IEName noExtField plainName + WrapPattern -> IEPattern keywordTok spacedName + WrapType -> IEType keywordTok spacedName + where + plainName = parenthesizeOperator (reLocA (L noSrcSpan rdr)) + spacedName = setEntryDP plainName (SameLine 1) + keywordTok = +#if MIN_VERSION_ghc(9,11,0) + EpTok (epl 0) +#else + epl 0 +#endif + +parenthesizeOperator :: LocatedN RdrName -> LocatedN RdrName +parenthesizeOperator ln + | isSymOcc (rdrNameOcc (unLoc ln)) = parenthesizeName ln + | otherwise = ln + +appendIE :: LIE GhcPs -> LExportList -> LExportList +appendIE item (L l items) = L l (fixLast items ++ [newItem (not (null items))]) + where + newItem hasSibling = + setEntryDP (first removeTrailingCommaAnn item) (SameLine (if hasSibling then 1 else 0)) + -- Reuse the comma that already separates the list's items. On a multiline + -- leading comma list that comma carries a 'DifferentLine' delta, so the new + -- separator lands on its own line instead of collapsing onto the last item. + fixLast = over _last (first addSep) + addSep = maybe ensureTrailingComma withTrailingComma (separatorComma items) + +-- | The trailing comma that separates existing items, if the list has any. +separatorComma :: [LIE GhcPs] -> Maybe TrailingAnn +separatorComma items = + listToMaybe [c | L ann _ <- items, c <- trailingAnns ann, isCommaAnn c] + +-- | 'Nothing' iff @ctor@ is already exported (via @T(..)@ or @T(...,ctor,...)@). +addCtorUnderParent :: + RdrName {- ^ parent -} -> + RdrName {- ^ ctor -} -> + LExportList -> + Maybe LExportList +addCtorUnderParent parent ctor lst@(L l items) = + case findParent items of + ParentNotFound -> Just $ appendIE (mkTypeWithIE parent (ctor :| [])) lst + FoundIEThingAll -> Nothing + FoundIEThingWith CtorPresent -> Nothing + FoundIEThingWith CtorAbsent -> Just (L l (map (transformParent extendThingWith) items)) + FoundIEThingAbs -> + let upgraded = unLoc (mkTypeWithIE parent (ctor :| [])) + in Just (L l (map (transformParent (const upgraded)) items)) + where + parentFS = rdrNameFS parent + ctorFS = rdrNameFS ctor + + ctorPresence cs + | any ((== ctorFS) . lieWrappedNameFS) cs = CtorPresent + | otherwise = CtorAbsent + + findParent [] = ParentNotFound + findParent (L _ ie : rest) + | parentNameIs parentFS ie = + case ie of + IEThingAll{} -> FoundIEThingAll + IEThingAbs{} -> FoundIEThingAbs + _ | Just cs <- ieThingWithChildren ie -> FoundIEThingWith (ctorPresence cs) + | otherwise -> findParent rest + | otherwise = findParent rest + + transformParent f (L itemLoc ie) + | parentNameIs parentFS ie = L itemLoc (f ie) + | otherwise = L itemLoc ie + + extendThingWith :: IE GhcPs -> IE GhcPs + extendThingWith = overThingWithChildren $ \cs -> + let hasSibling = not (null cs) + newChild = setEntryDP (mkIEName ctor) (SameLine (if hasSibling then 1 else 0)) + in (if hasSibling then map (first ensureTrailingComma) cs else cs) ++ [newChild] + +printExportList :: LExportList -> Text +printExportList l = T.pack (exactPrint (setEntryDP l (SameLine 0))) + +toDeltaExportList :: LExportList -> LExportList +toDeltaExportList = makeDeltaAst + +data FindParentResult + = ParentNotFound + | FoundIEThingAll + | FoundIEThingWith CtorPresence + | FoundIEThingAbs + +data CtorPresence = CtorAbsent | CtorPresent + deriving Eq diff --git a/plugins/hls-export-plugin/src/Ide/Plugin/Export/Exports.hs b/plugins/hls-export-plugin/src/Ide/Plugin/Export/Exports.hs new file mode 100644 index 0000000000..04fd0e339b --- /dev/null +++ b/plugins/hls-export-plugin/src/Ide/Plugin/Export/Exports.hs @@ -0,0 +1,38 @@ +module Ide.Plugin.Export.Exports + ( isExplicit + , isExported + , addExport + , addConstructorExport + ) where + +import Data.Maybe (isJust) +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Error (srcSpanToRange) +import Ide.Plugin.Export.ExactPrint +import Ide.Plugin.Export.Utils +import Language.LSP.Protocol.Types + +isExplicit :: ParsedSource -> Bool +isExplicit = isJust . hsmodExports . unLoc + +-- | Also matches names appearing only as constructor children of an 'IEThingWith' parent. +isExported :: RdrName -> ParsedSource -> Bool +isExported n ps = case hsmodExports (unLoc ps) of + Nothing -> False + Just (L _ items) -> any (covers . unLoc) items + where + nFS = rdrNameFS n + covers ie = parentNameIs nFS ie || isInIE nFS ie + +replaceExportList :: ParsedSource -> (LExportList -> Maybe LExportList) -> Maybe [TextEdit] +replaceExportList ps f = do + exports <- hsmodExports (unLoc ps) + newList <- f (toDeltaExportList exports) + r <- srcSpanToRange (getLoc exports) + Just [TextEdit r (printExportList newList)] + +addExport :: ParsedSource -> LIE GhcPs -> Maybe [TextEdit] +addExport ps item = replaceExportList ps (Just . appendIE item) + +addConstructorExport :: RdrName -> RdrName -> ParsedSource -> Maybe [TextEdit] +addConstructorExport parent ctor ps = replaceExportList ps (addCtorUnderParent parent ctor) diff --git a/plugins/hls-export-plugin/src/Ide/Plugin/Export/Utils.hs b/plugins/hls-export-plugin/src/Ide/Plugin/Export/Utils.hs new file mode 100644 index 0000000000..50cadb9725 --- /dev/null +++ b/plugins/hls-export-plugin/src/Ide/Plugin/Export/Utils.hs @@ -0,0 +1,75 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE RecordWildCards #-} +module Ide.Plugin.Export.Utils where + +import qualified Data.Map.Strict as Map +import Data.Text (Text) +import Development.IDE.GHC.Compat +import Development.IDE.GHC.Compat.Util +import Language.LSP.Protocol.Types + +rdrNameFS :: RdrName -> FastString +rdrNameFS = occNameFS . rdrNameOcc + +ieParentName :: IE GhcPs -> Maybe RdrName +ieParentName e = case e of +#if MIN_VERSION_ghc(9,9,0) + IEVar _ (L _ wn) _ -> Just (ieWrappedRdrName wn) + IEThingAbs _ (L _ wn) _ -> Just (ieWrappedRdrName wn) + IEThingAll _ (L _ wn) _ -> Just (ieWrappedRdrName wn) + IEThingWith _ (L _ wn) _ _ _ -> Just (ieWrappedRdrName wn) +#else + IEVar _ (L _ wn) -> Just (ieWrappedRdrName wn) + IEThingAbs _ (L _ wn) -> Just (ieWrappedRdrName wn) + IEThingAll _ (L _ wn) -> Just (ieWrappedRdrName wn) + IEThingWith _ (L _ wn) _ _ -> Just (ieWrappedRdrName wn) +#endif + _ -> Nothing + +-- | The listed constructors of an @IEThingWith@ (@T(C1, C2)@), or 'Nothing' otherwise. +ieThingWithChildren :: IE GhcPs -> Maybe [LIEWrappedName GhcPs] +#if MIN_VERSION_ghc(9,9,0) +ieThingWithChildren (IEThingWith _ _ _ cs _) = Just cs +#else +ieThingWithChildren (IEThingWith _ _ _ cs) = Just cs +#endif +ieThingWithChildren _ = Nothing + +ieWrappedRdrName :: IEWrappedName GhcPs -> RdrName +ieWrappedRdrName = \case + IEName _ (L _ rdr) -> rdr + IEPattern _ (L _ rdr) -> rdr + IEType _ (L _ rdr) -> rdr +#if MIN_VERSION_ghc(9,11,0) + IEDefault _ (L _ rdr) -> rdr +#endif +#if MIN_VERSION_ghc(9,13,0) + IEData _ (L _ rdr) -> rdr +#endif + +-- | True when the export item's head name is the given 'FastString'. +parentNameIs :: FastString -> IE GhcPs -> Bool +parentNameIs fs = maybe False ((== fs) . rdrNameFS) . ieParentName + +-- | The 'FastString' of a located wrapped name, e.g. an @IEThingWith@ child. +lieWrappedNameFS :: LIEWrappedName GhcPs -> FastString +lieWrappedNameFS = rdrNameFS . ieWrappedRdrName . unLoc + +-- | True when @n@ is listed as a child constructor of an @IEThingWith@. +isInIE :: FastString -> IE GhcPs -> Bool +isInIE n = maybe False (any ((== n) . lieWrappedNameFS)) . ieThingWithChildren + +singleFileEdit :: Uri -> [TextEdit] -> WorkspaceEdit +singleFileEdit uri edits = WorkspaceEdit (Just (Map.singleton uri edits)) Nothing Nothing + +mkAction :: Text -> CodeAction +mkAction title = CodeAction {..} + where + _title = title + _kind = Just CodeActionKind_RefactorRewrite + _diagnostics = Nothing + _isPreferred = Nothing + _disabled = Nothing + _edit = Nothing + _command = Nothing + _data_ = Nothing diff --git a/plugins/hls-export-plugin/test/Main.hs b/plugins/hls-export-plugin/test/Main.hs new file mode 100644 index 0000000000..d5ad6a777d --- /dev/null +++ b/plugins/hls-export-plugin/test/Main.hs @@ -0,0 +1,219 @@ +module Main (main) where + +import Control.Lens ((^.)) +import Data.Either (rights) +import Data.List (sort) +import Data.Maybe (fromMaybe) +import qualified Data.Text as T +import Ide.Plugin.Export (descriptor) +import qualified Language.LSP.Protocol.Lens as L +import System.FilePath (()) +import Test.Hls +import Test.Hls.FileSystem (directProject, mkVirtualFileTree) + +plugin :: PluginTestDescriptor () +plugin = mkPluginTestDescriptor' descriptor "export" + +testDataDir :: FilePath +testDataDir = "plugins" "hls-export-plugin" "test" "testdata" + +-- | Open the named module in its own temporary single-file project, so each +-- test compiles only the file it needs and cannot pick up signals from a +-- sibling module. +runExport :: FilePath -> (TextDocumentIdentifier -> Session a) -> IO a +runExport hsFile act = + runSessionWithTestConfig def + { testDirLocation = Right (mkVirtualFileTree testDataDir (directProject hsFile)) + , testPluginDescriptor = plugin + } $ \_dir -> do + doc <- openDoc hsFile "haskell" + waitForKickDone + act doc + +codeActionTitles :: TextDocumentIdentifier -> Range -> Session [T.Text] +codeActionTitles doc range = + sort . map (^. L.title) . rights . map toEither + <$> getCodeActions doc range + +executeByPrefix :: T.Text -> TextDocumentIdentifier -> Range -> Session () +executeByPrefix prefix doc range = do + actions <- rights . map toEither <$> getCodeActions doc range + case filter (\ca -> prefix `T.isPrefixOf` (ca ^. L.title)) actions of + (ca:_) -> executeCodeAction ca + [] -> liftIO $ assertFailure (T.unpack prefix <> "...` action not offered") + +executeExportAction :: TextDocumentIdentifier -> Range -> Session () +executeExportAction = executeByPrefix "Export `" + +noActionWithPrefix :: T.Text -> TextDocumentIdentifier -> Range -> Session () +noActionWithPrefix prefix doc range = do + titles <- codeActionTitles doc range + liftIO $ not (any (prefix `T.isPrefixOf`) titles) + @? ("Did not expect " <> T.unpack prefix <> " action; saw: " <> show titles) + +noExportOffered :: TextDocumentIdentifier -> Range -> Session () +noExportOffered = noActionWithPrefix "Export `" + +-- | Fail unless some variant is an infix of the text. The message dumps it. +assertAnyInfix :: T.Text -> [T.Text] -> Assertion +assertAnyInfix hay variants = + any (`T.isInfixOf` hay) variants + @? ("Expected one of " <> show variants <> " in:\n" <> T.unpack hay) + +containsAfter :: TextDocumentIdentifier -> [T.Text] -> Session () +containsAfter doc expected = documentContents doc >>= liftIO . (`assertAnyInfix` expected) + +rangeAt :: UInt -> UInt -> Range +rangeAt l c = Range (Position l c) (Position l c) + +main :: IO () +main = defaultTestRunner $ testGroup "Export" + [ testGroup "Add: value bindings" + [ testCase "add value to export list" $ runExport "AddExport.hs" $ \doc -> do + executeExportAction doc (rangeAt 6 0) + containsAfter doc ["module AddExport (foo, Bar, bar)"] + + , testCase "no action when value already exported" $ runExport "AddExport.hs" $ \doc -> + noExportOffered doc (rangeAt 3 0) -- on `foo` + + , testCase "append follows a multi-line leading-comma list" $ runExport "AddExportMultiline.hs" $ \doc -> do + executeExportAction doc (rangeAt 11 0) -- on `baz` + containsAfter doc [" , baz\n ) where"] + ] + + , testGroup "Add: type declarations" + [ testCase "add bare type as T(..)" $ runExport "AddExport.hs" $ \doc -> do + executeExportAction doc (rangeAt 9 5) -- on `Baz` type name + containsAfter doc ["Baz(..)", "Baz (..)"] + ] + + , testGroup "Add: constructors" + [ testCase "constructor with no parent entry appends T (C)" $ runExport "AddExport.hs" $ \doc -> do + executeExportAction doc (rangeAt 9 12) -- on `Baz1`, no Baz entry yet + containsAfter doc ["Baz (Baz1)", "Baz(Baz1)"] + + , testCase "constructor under bare-type parent promotes to T(C)" $ runExport "AddCtor.hs" $ \doc -> do + executeExportAction doc (rangeAt 3 11) -- on `Bar1`, Bar is IEThingAbs + containsAfter doc ["Bar (Bar1)", "Bar(Bar1)"] + + , testCase "constructor merges into existing IEThingWith parent" $ runExport "AddCtor.hs" $ \doc -> do + executeExportAction doc (rangeAt 2 18) -- on `Foo2`, Foo has [Foo1] + containsAfter doc ["Foo (Foo1, Foo2)", "Foo(Foo1, Foo2)"] + + , testCase "constructor already in IEThingWith children suppresses action" $ runExport "AddCtor.hs" $ \doc -> + noExportOffered doc (rangeAt 2 11) -- on `Foo1`, already child of Foo(Foo1) + + , testCase "constructor under IEThingAll T(..) suppresses action" $ runExport "AddCtor.hs" $ \doc -> + noExportOffered doc (rangeAt 4 11) -- on `Baz1`, Baz(..) covers it + + , testCase "constructor exported standalone suppresses action" $ runExport "AddCtor.hs" $ \doc -> + noExportOffered doc (rangeAt 5 11) -- on `Qux1`, Qux1 standalone in list + ] + + , testGroup "Add: type classes" + [ testCase "add class as T(..)" $ runExport "AddClass.hs" $ \doc -> do + executeExportAction doc (rangeAt 8 6) -- on `Baz` class name + containsAfter doc ["module AddClass (Foo (..), Bar, Baz (..))"] + + , testCase "no add action when class exported as T(..)" $ runExport "AddClass.hs" $ \doc -> + noExportOffered doc (rangeAt 2 6) -- on `Foo`, exported as Foo (..) + + , testCase "no add action when class exported as bare T" $ runExport "AddClass.hs" $ \doc -> + noExportOffered doc (rangeAt 5 6) -- on `Bar`, exported as bare + + , testCase "no add action on class method" $ runExport "AddClass.hs" $ \doc -> + noExportOffered doc (rangeAt 9 2) -- on `baz1` inside `class Baz a where` + ] + + , testGroup "Add: layout variants" + [ testCase "add to an empty export list" $ runExport "AddExportEmpty.hs" $ \doc -> do + executeExportAction doc (rangeAt 2 0) -- on `foo` + containsAfter doc ["module AddExportEmpty (foo) where"] + + , testCase "append after a trailing comma" $ runExport "AddExportTrailingComma.hs" $ \doc -> do + executeExportAction doc (rangeAt 7 0) -- on `bar` + containsAfter doc ["( foo, bar"] + + , testCase "preserve a haddock comment between items" $ runExport "AddExportComment.hs" $ \doc -> do + executeExportAction doc (rangeAt 16 0) -- on `quux` + containsAfter doc [" -- * For testing\n , baz\n , quux\n ) where"] + ] + + , testGroup "Add: declaration kinds" + [ testCase "function operator is parenthesized" $ runExport "AddExportKinds.hs" $ \doc -> do + executeExportAction doc (rangeAt 8 1) -- on `(<|)` + containsAfter doc ["(placeholder, (<|))"] + + , testCase "infix function exports bare name" $ runExport "AddExportKinds.hs" $ \doc -> do + executeExportAction doc (rangeAt 11 3) -- on `f` + containsAfter doc ["(placeholder, f)"] + + , testCase "newtype exports as T(..)" $ runExport "AddExportKinds.hs" $ \doc -> do + executeExportAction doc (rangeAt 13 8) -- on `NT` + containsAfter doc ["placeholder, NT(..)", "placeholder, NT (..)"] + + , testCase "type synonym exports bare" $ runExport "AddExportKinds.hs" $ \doc -> do + executeExportAction doc (rangeAt 15 5) -- on `Syn` + containsAfter doc ["(placeholder, Syn)"] + + , testCase "type family exports bare" $ runExport "AddExportKinds.hs" $ \doc -> do + executeExportAction doc (rangeAt 17 12) -- on `TF` + containsAfter doc ["(placeholder, TF)"] + + , testCase "pattern synonym gets a pattern prefix" $ runExport "AddExportKinds.hs" $ \doc -> do + executeExportAction doc (rangeAt 20 9) -- on `Pat` + containsAfter doc ["(placeholder, pattern Pat)"] + + , testCase "data operator gets type keyword and (..)" $ runExport "AddExportKinds.hs" $ \doc -> do + executeExportAction doc (rangeAt 22 7) -- on `(:<)` + containsAfter doc ["placeholder, type (:<)(..)", "placeholder, type (:<) (..)"] + ] + + , testGroup "Add: type-level operators" + [ testCase "type synonym operator has no type keyword" $ runExport "AddExportTypeOps.hs" $ \doc -> do + executeExportAction doc (rangeAt 8 7) -- on `(:<>)` + containsAfter doc ["(placeholder, (:<>))"] + + , testCase "type family operator gets type keyword" $ runExport "AddExportTypeOps.hs" $ \doc -> do + executeExportAction doc (rangeAt 10 14) -- on `(:+:)` + containsAfter doc ["(placeholder, type (:+:))"] + + , testCase "typeclass operator gets type keyword and (..)" $ runExport "AddExportTypeOps.hs" $ \doc -> do + executeExportAction doc (rangeAt 12 8) -- on `(:*:)` + containsAfter doc ["placeholder, type (:*:)(..)", "placeholder, type (:*:) (..)"] + + , testCase "newtype operator gets type keyword and (..)" $ runExport "AddExportTypeOps.hs" $ \doc -> do + executeExportAction doc (rangeAt 14 10) -- on `(:->)` + containsAfter doc ["placeholder, type (:->)(..)", "placeholder, type (:->) (..)"] + + , testCase "pattern synonym operator is parenthesized" $ runExport "AddExportTypeOps.hs" $ \doc -> do + executeExportAction doc (rangeAt 16 11) -- on `(:++)` + containsAfter doc ["(placeholder, pattern (:++))"] + ] + + , testGroup "Add: negative cases" + [ testCase "no action on implicit module" $ runExport "Implicit.hs" $ \doc -> + noExportOffered doc (rangeAt 3 0) + + , testCase "no action when cursor on RHS" $ runExport "AddExport.hs" $ \doc -> + noExportOffered doc (rangeAt 6 6) -- col 6 is on the `2` of `bar = 2` + + , testCase "no action on a where-bound name" $ runExport "AddExportNegatives.hs" $ \doc -> + noExportOffered doc (rangeAt 7 8) -- on `whereBound` + + , testCase "no action on a record field" $ runExport "AddExportNegatives.hs" $ \doc -> + noExportOffered doc (rangeAt 9 18) -- on `recField` + ] + + , testGroup "Export fixes the unused-binding warning" + [ knownBrokenForGhcVersions [GHC96] + "TcRnUnusedName provenance is unstructured before GHC 9.8 (GHC #20115)" $ + testCase "Export action attaches the -Wunused-top-binds diagnostic" $ runExport "ExportUnusedFix.hs" $ \doc -> do + actions <- rights . map toEither <$> getCodeActions doc (rangeAt 6 0) -- on `unused` + case filter ((== "Export `unused`") . (^. L.title)) actions of + (ca:_) -> liftIO $ not (null (fromMaybe [] (ca ^. L.diagnostics))) + @? "Export action should carry the unused-binding diagnostic" + [] -> liftIO $ assertFailure $ + "Export `unused` not offered; saw: " <> show (map (^. L.title) actions) + ] + ] diff --git a/plugins/hls-export-plugin/test/testdata/AddClass.hs b/plugins/hls-export-plugin/test/testdata/AddClass.hs new file mode 100644 index 0000000000..745eb80281 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddClass.hs @@ -0,0 +1,10 @@ +module AddClass (Foo (..), Bar) where + +class Foo a where + foo1 :: a -> Int + +class Bar a where + bar1 :: a -> Int + +class Baz a where + baz1 :: a -> Int diff --git a/plugins/hls-export-plugin/test/testdata/AddCtor.hs b/plugins/hls-export-plugin/test/testdata/AddCtor.hs new file mode 100644 index 0000000000..4d2702c942 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddCtor.hs @@ -0,0 +1,6 @@ +module AddCtor (Foo (Foo1), Bar, Baz (..), Qux1) where + +data Foo = Foo1 | Foo2 | Foo3 +data Bar = Bar1 | Bar2 +data Baz = Baz1 | Baz2 +data Qux = Qux1 | Qux2 diff --git a/plugins/hls-export-plugin/test/testdata/AddExport.hs b/plugins/hls-export-plugin/test/testdata/AddExport.hs new file mode 100644 index 0000000000..e7e1a9cc05 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddExport.hs @@ -0,0 +1,10 @@ +module AddExport (foo, Bar) where + +foo :: Int +foo = 1 + +bar :: Int +bar = 2 + +data Bar = Bar +data Baz = Baz1 | Baz2 diff --git a/plugins/hls-export-plugin/test/testdata/AddExportComment.hs b/plugins/hls-export-plugin/test/testdata/AddExportComment.hs new file mode 100644 index 0000000000..23dc1c9e5e --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddExportComment.hs @@ -0,0 +1,18 @@ +module AddExportComment + ( foo + , bar + -- * For testing + , baz + ) where + +foo :: Int +foo = 1 + +bar :: Int +bar = 2 + +baz :: Int +baz = 3 + +quux :: Int +quux = 4 diff --git a/plugins/hls-export-plugin/test/testdata/AddExportEmpty.hs b/plugins/hls-export-plugin/test/testdata/AddExportEmpty.hs new file mode 100644 index 0000000000..59e06fbd73 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddExportEmpty.hs @@ -0,0 +1,4 @@ +module AddExportEmpty () where + +foo :: Int +foo = 1 diff --git a/plugins/hls-export-plugin/test/testdata/AddExportKinds.hs b/plugins/hls-export-plugin/test/testdata/AddExportKinds.hs new file mode 100644 index 0000000000..7af40d0111 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddExportKinds.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +module AddExportKinds (placeholder) where + +placeholder :: Int +placeholder = 0 + +(<|) :: a -> a -> a +(<|) x _ = x + +a `f` b = b + +newtype NT = NT () + +type Syn = () + +type family TF p + +pattern Pat :: a -> (a, a) +pattern Pat a = (a, a) + +data (:<) = Mk diff --git a/plugins/hls-export-plugin/test/testdata/AddExportMultiline.hs b/plugins/hls-export-plugin/test/testdata/AddExportMultiline.hs new file mode 100644 index 0000000000..83ea6e9a3e --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddExportMultiline.hs @@ -0,0 +1,13 @@ +module AddExportMultiline + ( foo + , bar + ) where + +foo :: Int +foo = 1 + +bar :: Int +bar = 2 + +baz :: Int +baz = 3 diff --git a/plugins/hls-export-plugin/test/testdata/AddExportNegatives.hs b/plugins/hls-export-plugin/test/testdata/AddExportNegatives.hs new file mode 100644 index 0000000000..4822c37faf --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddExportNegatives.hs @@ -0,0 +1,10 @@ +module AddExportNegatives (placeholder) where + +placeholder :: Int +placeholder = 0 + +withWhere :: () +withWhere = whereBound + where whereBound = () + +data Rec = Rec { recField :: () } diff --git a/plugins/hls-export-plugin/test/testdata/AddExportTrailingComma.hs b/plugins/hls-export-plugin/test/testdata/AddExportTrailingComma.hs new file mode 100644 index 0000000000..d6b373ed8c --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddExportTrailingComma.hs @@ -0,0 +1,9 @@ +module AddExportTrailingComma + ( foo, + ) where + +foo :: Int +foo = 1 + +bar :: Int +bar = 2 diff --git a/plugins/hls-export-plugin/test/testdata/AddExportTypeOps.hs b/plugins/hls-export-plugin/test/testdata/AddExportTypeOps.hs new file mode 100644 index 0000000000..c34d1801e3 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/AddExportTypeOps.hs @@ -0,0 +1,17 @@ +{-# LANGUAGE PatternSynonyms #-} +{-# LANGUAGE TypeFamilies #-} +{-# LANGUAGE TypeOperators #-} +module AddExportTypeOps (placeholder) where + +placeholder :: Int +placeholder = 0 + +type (:<>) = () + +type family (:+:) + +class (:*:) a + +newtype (:->) = MkArr () + +pattern x :++ y = (x, y) diff --git a/plugins/hls-export-plugin/test/testdata/ExportUnusedFix.hs b/plugins/hls-export-plugin/test/testdata/ExportUnusedFix.hs new file mode 100644 index 0000000000..28425d25d9 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/ExportUnusedFix.hs @@ -0,0 +1,8 @@ +{-# OPTIONS_GHC -Wunused-top-binds #-} +module ExportUnusedFix (used) where + +used :: Int +used = 1 + +unused :: Int +unused = 2 diff --git a/plugins/hls-export-plugin/test/testdata/Implicit.hs b/plugins/hls-export-plugin/test/testdata/Implicit.hs new file mode 100644 index 0000000000..f0f4a7c1cc --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/Implicit.hs @@ -0,0 +1,4 @@ +module Implicit where + +something :: Int +something = 42 diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Dump.hs b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Dump.hs index 638d14c51d..6f2bd70ab4 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Dump.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/Dump.hs @@ -7,7 +7,7 @@ import Development.IDE.GHC.Compat hiding (LocatedA, import Development.IDE.GHC.Compat.ExactPrint (ExactPrint, exactPrint) import Development.IDE.GHC.Compat.Util import Generics.SYB (ext1Q, ext2Q, extQ) -import GHC.Hs hiding (AnnLet) +import GHC.Hs import GHC.Hs.Dump import GHC.Plugins hiding (AnnLet) import Prelude hiding ((<>)) diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs index 112dc3a6ca..076ba03a06 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/GHC/ExactPrint.hs @@ -24,13 +24,10 @@ module Development.IDE.GHC.ExactPrint modifySigWithM, genAnchor1, setPrecedingLines, - addParens, addParensToCtxt, - modifyAnns, removeComma, -- * Helper function eqSrcSpan, - epl, epAnn, removeTrailingComma, annotateParsedSource, @@ -67,6 +64,10 @@ import Development.IDE.GHC.Compat hiding (parseImport, parsePattern, parseType) import Development.IDE.GHC.Compat.ExactPrint +import Development.IDE.GHC.ExactPrint.Annotation (epl, + isCommaAnn, + modifyAnns, + removeTrailingCommaAnn) import Development.IDE.Graph (RuleResult, Rules) import Development.IDE.Graph.Classes import Generics.SYB @@ -93,16 +94,13 @@ import Data.Default (Default) import GHC ( Anchor (..), AnchorOperation, EpAnn (..), - NameAdornment (NameParens), - NameAnn (..), SrcSpanAnn' (SrcSpanAnn), SrcSpanAnnA, - TrailingAnn (AddCommaAnn), emptyComments, realSrcSpan, spanAsAnchor) import GHC.Parser.Annotation (AnnContext (..), - EpaLocation (EpaDelta), + EpaLocation, deltaPos) import GHC.Types.SrcLoc (generatedSrcSpan) #endif @@ -122,10 +120,7 @@ import GHC ( #if MIN_VERSION_ghc(9,11,0) EpToken (..), #endif - NameAdornment (..), - NameAnn (..), SrcSpanAnnA, - TrailingAnn (..), deltaPos, emptyComments, spanAsAnchor) @@ -783,64 +778,11 @@ addParensToCtxt close_dp = addOpen . addClose #endif | otherwise = it -epl :: Int -> EpaLocation -#if MIN_VERSION_ghc(9,11,0) -epl n = EpaDelta (UnhelpfulSpan UnhelpfulNoLocationInfo) (SameLine n) [] -#else -epl n = EpaDelta (SameLine n) [] -#endif - epAnn :: SrcSpan -> ann -> EpAnn ann epAnn srcSpan anns = EpAnn (spanAsAnchor srcSpan) anns emptyComments -modifyAnns :: LocatedAn a ast -> (a -> a) -> LocatedAn a ast -#if MIN_VERSION_ghc(9,9,0) -modifyAnns x f = first (fmap f) x -#else -modifyAnns x f = first ((fmap.fmap) f) x -#endif - removeComma :: SrcSpanAnnA -> SrcSpanAnnA -#if MIN_VERSION_ghc(9,9,0) -removeComma (EpAnn anc (AnnListItem as) cs) - = EpAnn anc (AnnListItem (filter (not . isCommaAnn) as)) cs - where - isCommaAnn AddCommaAnn{} = True - isCommaAnn _ = False -#else -removeComma it@(SrcSpanAnn EpAnnNotUsed _) = it -removeComma (SrcSpanAnn (EpAnn anc (AnnListItem as) cs) l) - = SrcSpanAnn (EpAnn anc (AnnListItem (filter (not . isCommaAnn) as)) cs) l - where - isCommaAnn AddCommaAnn{} = True - isCommaAnn _ = False -#endif - -addParens :: Bool -> GHC.NameAnn -> GHC.NameAnn -#if MIN_VERSION_ghc(9,11,0) -addParens True it@NameAnn{} = - it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } -addParens True it@NameAnnCommas{} = - it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } -addParens True it@NameAnnOnly{} = - it{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)) } -addParens True it@NameAnnTrailing{} = - NameAnn{nann_adornment = NameParens (EpTok (epl 0)) (EpTok (epl 0)), nann_name = epl 0, nann_trailing = nann_trailing it} -#else -addParens True it@NameAnn{} = - it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } -addParens True it@NameAnnCommas{} = - it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } -addParens True it@NameAnnOnly{} = - it{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0 } -addParens True NameAnnTrailing{..} = - NameAnn{nann_adornment = NameParens, nann_open=epl 0, nann_close=epl 0, nann_name = epl 0, ..} -#endif -addParens _ it = it +removeComma = removeTrailingCommaAnn removeTrailingComma :: GenLocated SrcSpanAnnA ast -> GenLocated SrcSpanAnnA ast removeTrailingComma = flip modifyAnns $ \(AnnListItem l) -> AnnListItem $ filter (not . isCommaAnn) l - -isCommaAnn :: TrailingAnn -> Bool -isCommaAnn AddCommaAnn{} = True -isCommaAnn _ = False diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs index 091b99eb8b..7a28289abb 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction.hs @@ -19,7 +19,6 @@ module Development.IDE.Plugin.CodeAction import Control.Applicative ((<|>)) import Control.Arrow (second, - (&&&), (>>>)) import Control.Concurrent.STM.Stats (atomically) import Control.Monad.Extra @@ -38,7 +37,6 @@ import Data.List.NonEmpty (NonEmpty ((: import qualified Data.List.NonEmpty as NE import qualified Data.Map.Strict as M import Data.Maybe -import Data.Ord (comparing) import qualified Data.Set as S import qualified Data.Text as T import qualified Data.Text.Encoding as T @@ -154,9 +152,8 @@ codeAction state _ (CodeActionParams _ _ (TextDocumentIdentifier uri) range _) = iePluginDescriptor :: Recorder (WithPriority E.Log) -> PluginId -> PluginDescriptor IdeState iePluginDescriptor recorder plId = let old = - mkGhcideCAsPlugin [ - wrap suggestExportUnusedTopBinding - , wrap suggestModuleTypo + mkGhcideCAsPlugin + [ wrap suggestModuleTypo , wrap suggestFixConstructorImport , wrap suggestExtendImport , wrap suggestImportDisambiguation @@ -766,100 +763,9 @@ caDeleteUnusedBindings m contents allDiags contextRange uri headToList (x : _) = [x] subsumesEither (TE._range -> range1) (TE._range -> range2) = subRange range1 range2 || subRange range2 range1 -data ExportsAs = ExportName | ExportPattern | ExportFamily | ExportAll - deriving (Eq) - getLocatedRange :: HasSrcSpan a => a -> Maybe Range getLocatedRange = srcSpanToRange . getLoc -suggestExportUnusedTopBinding :: Maybe T.Text -> ParsedModule -> Diagnostic -> Maybe (T.Text, TextEdit) -suggestExportUnusedTopBinding srcOpt ParsedModule{pm_parsed_source = L _ HsModule{..}} Diagnostic{..} --- Foo.hs:4:1: warning: [-Wunused-top-binds] Defined but not used: ‘f’ --- Foo.hs:5:1: warning: [-Wunused-top-binds] Defined but not used: type constructor or class ‘F’ --- Foo.hs:6:1: warning: [-Wunused-top-binds] Defined but not used: data constructor ‘Bar’ - | Just source <- srcOpt - , Just [_, name] <- - matchRegexUnifySpaces - _message - ".*Defined but not used: (type constructor or class |data constructor )?‘([^ ]+)’" - , Just (exportType, _) <- - find (matchWithDiagnostic _range . snd) - . mapMaybe (\(L l b) -> if isTopLevel (locA l) then exportsAs b else Nothing) - $ hsmodDecls - , Just exports <- fmap (fmap reLoc) . reLoc <$> hsmodExports - , Just exportsEndPos <- _end <$> getLocatedRange exports - , let name' = printExport exportType name - sep = exportSep source $ map getLocatedRange <$> exports - exportName = case sep of - Nothing -> (if needsComma source exports then ", " else "") <> name' - Just s -> s <> name' - exportsEndPos' = exportsEndPos { _character = pred $ _character exportsEndPos } - insertPos = fromMaybe exportsEndPos' $ case (sep, unLoc exports) of - (Just _, exports'@(_:_)) -> fmap _end . getLocatedRange $ last exports' - _ -> Nothing - = Just ("Export ‘" <> name <> "’", TextEdit (Range insertPos insertPos) exportName) - | otherwise = Nothing - where - exportSep :: T.Text -> Located [Maybe Range] -> Maybe T.Text - exportSep src (L (RealSrcSpan _ _) xs@(_ : tl@(_ : _))) = - case mapMaybe (\(e, s) -> (,) <$> e <*> s) $ zip (fmap _end <$> xs) (fmap _start <$> tl) of - [] -> Nothing - bounds -> Just smallestSep - where - smallestSep - = snd - $ minimumBy (comparing fst) - $ map (T.length &&& id) - $ nubOrd - $ map (\(prevEnd, nextStart) -> textInRange (Range prevEnd nextStart) src) bounds - exportSep _ _ = Nothing - - -- We get the last export and the closing bracket and check for comma in that range. - needsComma :: T.Text -> Located [Located (IE GhcPs)] -> Bool - needsComma _ (L _ []) = False - needsComma source (L (RealSrcSpan l _) exports) = - let closeParen = _end $ realSrcSpanToRange l - lastExport = fmap _end . getLocatedRange $ last exports - in - case lastExport of - Just lastExport -> - not $ T.any (== ',') $ textInRange (Range lastExport closeParen) source - _ -> False - needsComma _ _ = False - - opLetter :: T.Text - opLetter = ":!#$%&*+./<=>?@\\^|-~" - - parenthesizeIfNeeds :: Bool -> T.Text -> T.Text - parenthesizeIfNeeds needsTypeKeyword x - | T.any (c ==) opLetter = (if needsTypeKeyword then "type " else "") <> "(" <> x <> ")" - | otherwise = x - where - c = T.head x - - matchWithDiagnostic :: Range -> Located (IdP GhcPs) -> Bool - matchWithDiagnostic Range{_start=l,_end=r} x = - let loc = fmap _start . getLocatedRange $ x - in loc >= Just l && loc <= Just r - - printExport :: ExportsAs -> T.Text -> T.Text - printExport ExportName x = parenthesizeIfNeeds False x - printExport ExportPattern x = "pattern " <> parenthesizeIfNeeds False x - printExport ExportFamily x = parenthesizeIfNeeds True x - printExport ExportAll x = parenthesizeIfNeeds True x <> "(..)" - - isTopLevel :: SrcSpan -> Bool - isTopLevel span = fmap (_character . _start) (srcSpanToRange span) == Just 0 - - exportsAs :: HsDecl GhcPs -> Maybe (ExportsAs, Located (IdP GhcPs)) - exportsAs (ValD _ FunBind {fun_id}) = Just (ExportName, reLoc fun_id) - exportsAs (ValD _ (PatSynBind _ PSB {psb_id})) = Just (ExportPattern, reLoc psb_id) - exportsAs (TyClD _ SynDecl{tcdLName}) = Just (ExportName, reLoc tcdLName) - exportsAs (TyClD _ DataDecl{tcdLName}) = Just (ExportAll, reLoc tcdLName) - exportsAs (TyClD _ ClassDecl{tcdLName}) = Just (ExportAll, reLoc tcdLName) - exportsAs (TyClD _ FamDecl{tcdFam}) = Just (ExportFamily, reLoc $ fdLName tcdFam) - exportsAs _ = Nothing - suggestAddTypeAnnotationToSatisfyConstraints :: Maybe T.Text -> Diagnostic -> [(T.Text, [TextEdit])] suggestAddTypeAnnotationToSatisfyConstraints sourceOpt Diagnostic{_range=_range,..} -- File.hs:52:41: warning: diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs index 6ef6a9d219..6f259fd199 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/CodeAction/ExactPrint.hs @@ -25,6 +25,8 @@ import qualified Data.Text as T import Development.IDE.GHC.Compat hiding (Annotation) import Development.IDE.GHC.Error import Development.IDE.GHC.ExactPrint +import Development.IDE.GHC.ExactPrint.Annotation (addParens, epl, + modifyAnns) import Development.IDE.GHC.Util import Development.IDE.Spans.Common import GHC.Exts (IsList (fromList)) diff --git a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/AddArgument.hs b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/AddArgument.hs index b3ec78c6a4..6ee116da9c 100644 --- a/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/AddArgument.hs +++ b/plugins/hls-refactor-plugin/src/Development/IDE/Plugin/Plugins/AddArgument.hs @@ -26,7 +26,7 @@ import Language.LSP.Protocol.Types -- See Note [Guidelines For Using CPP In GHCIDE Import Statements] #if MIN_VERSION_ghc(9,6,0) && !MIN_VERSION_ghc(9,9,0) -import Development.IDE.GHC.ExactPrint (epl) +import Development.IDE.GHC.ExactPrint.Annotation (epl) import GHC.Parser.Annotation (TokenLocation (..)) #endif diff --git a/plugins/hls-refactor-plugin/test/Main.hs b/plugins/hls-refactor-plugin/test/Main.hs index eccd810c5b..d26eb8349b 100644 --- a/plugins/hls-refactor-plugin/test/Main.hs +++ b/plugins/hls-refactor-plugin/test/Main.hs @@ -317,7 +317,6 @@ codeActionTests = testGroup "code actions" , addFunctionConstraintTests , removeRedundantConstraintsTests , addTypeAnnotationsToLiteralsTest - , exportUnusedTests , addImplicitParamsConstraintTests , removeExportTests , Test.AddArgument.tests @@ -3465,355 +3464,6 @@ addSigActionTests = let , "pattern MkT1' b <- MkT1 42 b\n where MkT1' b = MkT1 42 b" >:: "pattern MkT1' :: (Eq a, Num a) => Show b => b -> T1 a" ] -exportUnusedTests :: TestTree -exportUnusedTests = testGroup "export unused actions" - [ testGroup "don't want suggestion" -- in this test group we check that no code actions are created - [ testSession "implicit exports" $ templateNoAction - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# OPTIONS_GHC -Wmissing-signatures #-}" - , "module A where" - , "foo = id" - ] - (R 3 0 3 3) - "Export ‘foo’" - , testSession "not top-level" $ templateNoAction - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# OPTIONS_GHC -Wunused-binds #-}" - , "module A (foo,bar) where" - , "foo = ()" - , " where bar = ()" - , "bar = ()" - ] - (R 2 0 2 11) - "Export ‘bar’" - , testSession "type is exported but not the constructor of same name" $ templateNoAction - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo) where" - , "data Foo = Foo" - ] - (R 2 0 2 8) - "Export ‘Foo’" - , testSession "unused data field" $ templateNoAction - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo(Foo)) where" - , "data Foo = Foo {foo :: ()}" - ] - (R 2 0 2 20) - "Export ‘foo’" - ] - , testGroup "want suggestion" - [ testSession "empty exports" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (" - , ") where" - , "foo = id" - ] - (R 3 0 3 3) - "Export ‘foo’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (" - , "foo) where" - , "foo = id" - ] - , testSession "single line explicit exports" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (foo) where" - , "foo = id" - , "bar = foo" - ] - (R 3 0 3 3) - "Export ‘bar’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (foo, bar) where" - , "foo = id" - , "bar = foo" - ] - , testSession "multi line explicit exports" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " (" - , " foo) where" - , "foo = id" - , "bar = foo" - ] - (R 5 0 5 3) - "Export ‘bar’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " (" - , " foo, bar) where" - , "foo = id" - , "bar = foo" - ] - , testSession "export list ends in comma" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " (foo," - , " ) where" - , "foo = id" - , "bar = foo" - ] - (R 5 0 5 3) - "Export ‘bar’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " (foo," - , " bar) where" - , "foo = id" - , "bar = foo" - ] - , testSession "style of multiple exports is preserved 1" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo" - , " , bar" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - ] - (R 7 0 7 3) - "Export ‘baz’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo" - , " , bar" - , " , baz" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - ] - , testSession "style of multiple exports is preserved 2" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo," - , " bar" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - ] - (R 7 0 7 3) - "Export ‘baz’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo," - , " bar," - , " baz" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - ] - , testSession "style of multiple exports is preserved and selects smallest export separator" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo" - , " , bar" - , " -- * For testing" - , " , baz" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - , "quux = bar" - ] - (R 10 0 10 4) - "Export ‘quux’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A" - , " ( foo" - , " , bar" - , " -- * For testing" - , " , baz" - , " , quux" - , " ) where" - , "foo = id" - , "bar = foo" - , "baz = bar" - , "quux = bar" - ] - , testSession "unused pattern synonym" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE PatternSynonyms #-}" - , "module A () where" - , "pattern Foo a <- (a, _)" - ] - (R 3 0 3 10) - "Export ‘Foo’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE PatternSynonyms #-}" - , "module A (pattern Foo) where" - , "pattern Foo a <- (a, _)" - ] - , testSession "unused pattern synonym operator" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE PatternSynonyms #-}" - , "module A () where" - , "pattern x :+ y = (x, y)" - ] - (R 3 0 3 12) - "Export ‘:+’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE PatternSynonyms #-}" - , "module A (pattern (:+)) where" - , "pattern x :+ y = (x, y)" - ] - , testSession "unused data type" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "data Foo = Foo" - ] - (R 2 0 2 7) - "Export ‘Foo’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo(..)) where" - , "data Foo = Foo" - ] - , testSession "unused newtype" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "newtype Foo = Foo ()" - ] - (R 2 0 2 10) - "Export ‘Foo’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo(..)) where" - , "newtype Foo = Foo ()" - ] - , testSession "unused type synonym" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "type Foo = ()" - ] - (R 2 0 2 7) - "Export ‘Foo’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo) where" - , "type Foo = ()" - ] - , testSession "unused type family" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeFamilies #-}" - , "module A () where" - , "type family Foo p" - ] - (R 3 0 3 15) - "Export ‘Foo’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeFamilies #-}" - , "module A (Foo) where" - , "type family Foo p" - ] - , testSession "unused typeclass" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "class Foo a" - ] - (R 2 0 2 8) - "Export ‘Foo’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (Foo(..)) where" - , "class Foo a" - ] - , testSession "infix" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "a `f` b = ()" - ] - (R 2 0 2 11) - "Export ‘f’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A (f) where" - , "a `f` b = ()" - ] - , testSession "function operator" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A () where" - , "(<|) = ($)" - ] - (R 2 0 2 9) - "Export ‘<|’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "module A ((<|)) where" - , "(<|) = ($)" - ] - , testSession "type synonym operator" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "type (:<) = ()" - ] - (R 3 0 3 13) - "Export ‘:<’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A ((:<)) where" - , "type (:<) = ()" - ] - , testSession "type family operator" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeFamilies #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "type family (:<)" - ] - (R 4 0 4 15) - "Export ‘:<’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeFamilies #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A (type (:<)) where" - , "type family (:<)" - ] - , testSession "typeclass operator" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "class (:<) a" - ] - (R 3 0 3 11) - "Export ‘:<’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A (type (:<)(..)) where" - , "class (:<) a" - ] - , testSession "newtype operator" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "newtype (:<) = Foo ()" - ] - (R 3 0 3 20) - "Export ‘:<’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A (type (:<)(..)) where" - , "newtype (:<) = Foo ()" - ] - , testSession "data type operator" $ template - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A () where" - , "data (:<) = Foo ()" - ] - (R 3 0 3 17) - "Export ‘:<’" - [ "{-# OPTIONS_GHC -Wunused-top-binds #-}" - , "{-# LANGUAGE TypeOperators #-}" - , "module A (type (:<)(..)) where" - , "data (:<) = Foo ()" - ] - ] - ] - where - template origLines range actionTitle expectedLines = - exportTemplate (Just range) origLines actionTitle (Just expectedLines) - templateNoAction origLines range actionTitle = - exportTemplate (Just range) origLines actionTitle Nothing - exportTemplate :: Maybe Range -> [T.Text] -> T.Text -> Maybe [T.Text] -> Session () exportTemplate mRange initialLines expectedAction expectedLines = do doc <- createDoc "A.hs" "haskell" $ T.unlines initialLines diff --git a/src/HlsPlugins.hs b/src/HlsPlugins.hs index 52dd0699ea..bf0c3ffec6 100644 --- a/src/HlsPlugins.hs +++ b/src/HlsPlugins.hs @@ -97,6 +97,10 @@ import qualified Ide.Plugin.OverloadedRecordDot as OverloadedRecordDot import qualified Ide.Plugin.Notes as Notes #endif +#if hls_export +import qualified Ide.Plugin.Export as Export +#endif + -- formatters #if hls_fourmolu @@ -240,5 +244,8 @@ idePlugins recorder = pluginDescToIdePlugins allPlugins #endif #if hls_notes let pId = "notes" in Notes.descriptor (pluginRecorder pId) pId : +#endif +#if hls_export + let pId = "export" in Export.descriptor pId : #endif GhcIde.descriptors (pluginRecorder "ghcide") diff --git a/test/testdata/schema/ghc910/default-config.golden.json b/test/testdata/schema/ghc910/default-config.golden.json index 7f5693aadf..04374d3948 100644 --- a/test/testdata/schema/ghc910/default-config.golden.json +++ b/test/testdata/schema/ghc910/default-config.golden.json @@ -55,6 +55,9 @@ "explicit-fixity": { "globalOn": true }, + "export": { + "globalOn": true + }, "fourmolu": { "config": { "external": false, diff --git a/test/testdata/schema/ghc910/vscode-extension-schema.golden.json b/test/testdata/schema/ghc910/vscode-extension-schema.golden.json index 383c4ee5fe..21a7532e22 100644 --- a/test/testdata/schema/ghc910/vscode-extension-schema.golden.json +++ b/test/testdata/schema/ghc910/vscode-extension-schema.golden.json @@ -119,6 +119,12 @@ "scope": "resource", "type": "boolean" }, + "haskell.plugin.export.globalOn": { + "default": true, + "description": "Enables export plugin", + "scope": "resource", + "type": "boolean" + }, "haskell.plugin.fourmolu.config.external": { "default": false, "markdownDescription": "Call out to an external \"fourmolu\" executable, rather than using the bundled library.", diff --git a/test/testdata/schema/ghc912/default-config.golden.json b/test/testdata/schema/ghc912/default-config.golden.json index 53313f4f11..aae96a04aa 100644 --- a/test/testdata/schema/ghc912/default-config.golden.json +++ b/test/testdata/schema/ghc912/default-config.golden.json @@ -55,6 +55,9 @@ "explicit-fixity": { "globalOn": true }, + "export": { + "globalOn": true + }, "fourmolu": { "config": { "external": false, diff --git a/test/testdata/schema/ghc912/vscode-extension-schema.golden.json b/test/testdata/schema/ghc912/vscode-extension-schema.golden.json index 9c3c96aba2..a36ce1c524 100644 --- a/test/testdata/schema/ghc912/vscode-extension-schema.golden.json +++ b/test/testdata/schema/ghc912/vscode-extension-schema.golden.json @@ -119,6 +119,12 @@ "scope": "resource", "type": "boolean" }, + "haskell.plugin.export.globalOn": { + "default": true, + "description": "Enables export plugin", + "scope": "resource", + "type": "boolean" + }, "haskell.plugin.fourmolu.config.external": { "default": false, "markdownDescription": "Call out to an external \"fourmolu\" executable, rather than using the bundled library.", diff --git a/test/testdata/schema/ghc914/default-config.golden.json b/test/testdata/schema/ghc914/default-config.golden.json index 37389f7deb..b7b8812154 100644 --- a/test/testdata/schema/ghc914/default-config.golden.json +++ b/test/testdata/schema/ghc914/default-config.golden.json @@ -55,6 +55,9 @@ "explicit-fixity": { "globalOn": true }, + "export": { + "globalOn": true + }, "gadt": { "globalOn": true }, diff --git a/test/testdata/schema/ghc914/vscode-extension-schema.golden.json b/test/testdata/schema/ghc914/vscode-extension-schema.golden.json index 6a8d548967..e67d125d19 100644 --- a/test/testdata/schema/ghc914/vscode-extension-schema.golden.json +++ b/test/testdata/schema/ghc914/vscode-extension-schema.golden.json @@ -119,6 +119,12 @@ "scope": "resource", "type": "boolean" }, + "haskell.plugin.export.globalOn": { + "default": true, + "description": "Enables export plugin", + "scope": "resource", + "type": "boolean" + }, "haskell.plugin.gadt.globalOn": { "default": true, "description": "Enables gadt plugin", diff --git a/test/testdata/schema/ghc96/default-config.golden.json b/test/testdata/schema/ghc96/default-config.golden.json index 8c14e3392c..3bbcb431c3 100644 --- a/test/testdata/schema/ghc96/default-config.golden.json +++ b/test/testdata/schema/ghc96/default-config.golden.json @@ -55,6 +55,9 @@ "explicit-fixity": { "globalOn": true }, + "export": { + "globalOn": true + }, "fourmolu": { "config": { "external": false, diff --git a/test/testdata/schema/ghc96/vscode-extension-schema.golden.json b/test/testdata/schema/ghc96/vscode-extension-schema.golden.json index 61b06899e6..c38a6223be 100644 --- a/test/testdata/schema/ghc96/vscode-extension-schema.golden.json +++ b/test/testdata/schema/ghc96/vscode-extension-schema.golden.json @@ -119,6 +119,12 @@ "scope": "resource", "type": "boolean" }, + "haskell.plugin.export.globalOn": { + "default": true, + "description": "Enables export plugin", + "scope": "resource", + "type": "boolean" + }, "haskell.plugin.fourmolu.config.external": { "default": false, "markdownDescription": "Call out to an external \"fourmolu\" executable, rather than using the bundled library.", diff --git a/test/testdata/schema/ghc98/default-config.golden.json b/test/testdata/schema/ghc98/default-config.golden.json index 8c14e3392c..3bbcb431c3 100644 --- a/test/testdata/schema/ghc98/default-config.golden.json +++ b/test/testdata/schema/ghc98/default-config.golden.json @@ -55,6 +55,9 @@ "explicit-fixity": { "globalOn": true }, + "export": { + "globalOn": true + }, "fourmolu": { "config": { "external": false, diff --git a/test/testdata/schema/ghc98/vscode-extension-schema.golden.json b/test/testdata/schema/ghc98/vscode-extension-schema.golden.json index 61b06899e6..c38a6223be 100644 --- a/test/testdata/schema/ghc98/vscode-extension-schema.golden.json +++ b/test/testdata/schema/ghc98/vscode-extension-schema.golden.json @@ -119,6 +119,12 @@ "scope": "resource", "type": "boolean" }, + "haskell.plugin.export.globalOn": { + "default": true, + "description": "Enables export plugin", + "scope": "resource", + "type": "boolean" + }, "haskell.plugin.fourmolu.config.external": { "default": false, "markdownDescription": "Call out to an external \"fourmolu\" executable, rather than using the bundled library.",