Skip to content

Commit cf59dc2

Browse files
authored
Splice export modifications precisely when using CPP (#4981)
* Extract shared line helpers into Development.IDE.Core.Text * Add export-list CPP detection to hls-exactprint-utils * Splice text when using CPP in export lists * Update CODEOWNERS for hls-export-plugin
1 parent 66cbd3c commit cf59dc2

25 files changed

Lines changed: 612 additions & 212 deletions

File tree

CODEOWNERS

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
/plugins/hls-explicit-fixity-plugin
2424
/plugins/hls-explicit-imports-plugin
2525
/plugins/hls-explicit-record-fields-plugin @ozkutuk
26+
/plugins/hls-export-plugin @crtschin
2627
/plugins/hls-fourmolu-plugin @georgefst
2728
/plugins/hls-gadt-plugin @July541
2829
/plugins/hls-hlint-plugin @eddiemundo

ghcide/ghcide.cabal

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ library
142142
Development.IDE.Core.RuleTypes
143143
Development.IDE.Core.Service
144144
Development.IDE.Core.Shake
145+
Development.IDE.Core.Text
145146
Development.IDE.Core.Tracing
146147
Development.IDE.Core.UseStale
147148
Development.IDE.Core.WorkerThread
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
module Development.IDE.Core.Text
2+
( takeLineRange
3+
, lineAt
4+
) where
5+
6+
import Data.Maybe (listToMaybe)
7+
import Data.Text (Text)
8+
import Data.Text.Utf16.Rope.Mixed (Rope)
9+
import qualified Data.Text.Utf16.Rope.Mixed as Rope
10+
11+
-- | The lines of @rope@ over the 0-based inclusive line range @[from, to]@.
12+
takeLineRange :: Word -> Word -> Rope -> [Text]
13+
takeLineRange from to rope
14+
| to < from = []
15+
| otherwise = Rope.lines $ fst $ Rope.splitAtLine (to - from + 1) $ snd $ Rope.splitAtLine from rope
16+
17+
-- | The 0-based line @n@ of @rope@, if it has one.
18+
lineAt :: Word -> Rope -> Maybe Text
19+
lineAt n = listToMaybe . takeLineRange n n

ghcide/src/Development/IDE/Plugin/Completions/Logic.hs

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ import qualified Data.HashSet as HashSet
4040
import Data.Ord (Down (Down))
4141
import qualified Data.Set as Set
4242
import Development.IDE.Core.PositionMapping
43+
import Development.IDE.Core.Text (lineAt)
4344
import Development.IDE.GHC.Compat hiding (isQual, ppr)
4445
import qualified Development.IDE.GHC.Compat as GHC
4546
import Development.IDE.GHC.Compat.Util
@@ -876,8 +877,7 @@ getCompletionPrefixFromRope pos@(Position l c) ropetext =
876877
lastMaybe = headMaybe . reverse
877878

878879
-- grab the entire line the cursor is at
879-
curLine <- headMaybe $ Rope.lines
880-
$ fst $ Rope.splitAtLine 1 $ snd $ Rope.splitAtLine (fromIntegral l) ropetext
880+
curLine <- lineAt (fromIntegral l) ropetext
881881
let beforePos = T.take (fromIntegral c) curLine
882882
-- the word getting typed, after previous space and before cursor
883883
curWord <-

haskell-language-server.cabal

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1771,11 +1771,15 @@ library hls-exactprint-utils
17711771
import: defaults, pedantic, warnings
17721772
exposed-modules:
17731773
Development.IDE.GHC.ExactPrint.Annotation
1774+
Development.IDE.GHC.ExactPrint.CPP
17741775
hs-source-dirs: hls-exactprint-utils/src
17751776
build-depends:
17761777
, ghc
17771778
, ghc-exactprint
17781779
, ghcide == 2.14.0.0
1780+
, lsp >=2.8
1781+
, text
1782+
, text-rope
17791783
default-extensions:
17801784
CPP
17811785

@@ -1805,6 +1809,7 @@ library hls-export-plugin
18051809
build-depends:
18061810
, containers
18071811
, ghc
1812+
, ghc-boot-th
18081813
, ghc-exactprint
18091814
, ghcide == 2.14.0.0
18101815
, haskell-language-server:hls-exactprint-utils
@@ -1813,6 +1818,7 @@ library hls-export-plugin
18131818
, lsp >=2.8
18141819
, stm
18151820
, text
1821+
, text-rope
18161822
default-extensions:
18171823
, DataKinds
18181824
, LambdaCase
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
{-# LANGUAGE OverloadedStrings #-}
2+
3+
module Development.IDE.GHC.ExactPrint.CPP
4+
( spanHasCpp
5+
, isCppDirective
6+
) where
7+
8+
import Data.Text (Text)
9+
import qualified Data.Text as T
10+
import Data.Text.Utf16.Rope.Mixed (Rope)
11+
import Development.IDE.Core.Text (takeLineRange)
12+
import Language.LSP.Protocol.Types (Position (..), Range (..))
13+
14+
-- | Whether the source over @range@ holds a CPP directive.
15+
spanHasCpp :: Maybe Rope -> Range -> Bool
16+
spanHasCpp Nothing _ = False
17+
spanHasCpp (Just rope) (Range (Position l0 _) (Position l1 _)) =
18+
any isCppDirective (takeLineRange (fromIntegral l0) (fromIntegral l1) rope)
19+
20+
-- | Whether a line is a CPP directive. In a source compiled with CPP a directive
21+
-- is the only line whose first non-space character is @#@.
22+
isCppDirective :: Text -> Bool
23+
isCppDirective = T.isPrefixOf "#" . T.stripStart

plugins/hls-export-plugin/src/Ide/Plugin/Export.hs

Lines changed: 22 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,17 @@ module Ide.Plugin.Export (descriptor) where
55
import Control.Concurrent.STM (atomically)
66
import Control.Lens
77
import Control.Monad.IO.Class (liftIO)
8+
import Data.Maybe (isJust, isNothing)
89
import Data.Text (Text)
910
import qualified Data.Text as T
11+
import Data.Text.Utf16.Rope.Mixed (Rope)
1012
import Development.IDE
1113
import Development.IDE.Core.PluginUtils (runActionE, useE)
1214
import Development.IDE.Core.Shake (getDiagnostics)
1315
import Development.IDE.GHC.Compat
1416
import Development.IDE.GHC.Compat.Error (_TcRnUnusedTopBind,
1517
msgEnvelopeErrorL)
18+
import qualified GHC.LanguageExtensions.Type as LangExt (Extension (..))
1619
import Ide.Plugin.Error (getNormalizedFilePathE)
1720
import Ide.Plugin.Export.Cursor
1821
import Ide.Plugin.Export.ExactPrint
@@ -35,17 +38,27 @@ quickCodeActionHandlers :: PluginMethodHandler IdeState Method_TextDocumentCodeA
3538
quickCodeActionHandlers state _plId (CodeActionParams _ _ doc range _) = do
3639
let uri = doc ^. L.uri
3740
nfp <- getNormalizedFilePathE uri
38-
pm <- runActionE "Export.GetParsedModuleWithComments" state (useE GetParsedModuleWithComments nfp)
39-
let ps = pm_parsed_source pm
40-
case (isExplicit ps, locateUnderCursor (range ^. L.start) ps) of
41-
(True, Just under) -> do
41+
(ps, isCpp, mUnder, msrc) <- runActionE "Export.getInputs" state $ do
42+
pm <- useE GetParsedModuleWithComments nfp
43+
let ps = pm_parsed_source pm
44+
isCpp = xopt LangExt.Cpp (ms_hspp_opts (pm_mod_summary pm))
45+
mUnder = if isExplicit ps then locateUnderCursor (range ^. L.start) ps else Nothing
46+
-- Only a CPP module about to be offered an action needs the buffer (to find
47+
-- directives in the export list), so skip the fetch otherwise.
48+
msrc <- if isJust mUnder && isCpp then snd <$> useE GetFileContents nfp else pure Nothing
49+
pure (ps, isCpp, mUnder, msrc)
50+
case mUnder of
51+
-- A CPP module whose buffer we could not read may have directives in the
52+
-- export list that a reprint would silently erase. Withhold rather than risk
53+
-- it.
54+
Just under | not (isCpp && isNothing msrc) -> do
4255
-- The names GHC flags as defined-but-unused. Attach the action to the
4356
-- unused diagnostics as well.
4457
unusedDiags <- liftIO $ unusedTopBindDiagnostics state nfp
4558
pure . InL . map InR $
4659
[ ca
4760
| Just (verb, title, edits) <-
48-
[ addAction under ps
61+
[ addAction msrc under ps
4962
]
5063
, let fixes = [ d | d <- unusedDiags, locateUnderCursor (d ^. L.range . L.start) ps == Just under ]
5164
ca = mkAction (verb <> " `" <> title <> "`")
@@ -63,14 +76,14 @@ unusedTopBindDiagnostics state nfp = do
6376
isUnusedTopBind =
6477
has (fdStructuredMessageL . _SomeStructuredMessage . msgEnvelopeErrorL . _TcRnUnusedTopBind)
6578

66-
addAction :: UnderCursor -> ParsedSource -> Maybe (Text, Text, [TextEdit])
67-
addAction under ps = case under of
79+
addAction :: Maybe Rope -> UnderCursor -> ParsedSource -> Maybe (Text, Text, [TextEdit])
80+
addAction msrc under ps = case under of
6881
Decl flavor n
6982
| n `isExported` ps -> Nothing
70-
| otherwise -> ("Export", T.pack (printRdrName n),) <$> addExport ps (mkExportIE flavor n)
83+
| otherwise -> ("Export", T.pack (printRdrName n),) <$> addExport msrc ps (mkExportIE flavor n)
7184
Constructor t c
7285
| c `isExported` ps -> Nothing
7386
| otherwise ->
7487
("Export", T.pack (printRdrName t) <> "(" <> T.pack (printRdrName c) <> ")",)
75-
<$> addConstructorExport t c ps
88+
<$> addConstructorExport msrc t c ps
7689
Header -> Nothing

plugins/hls-export-plugin/src/Ide/Plugin/Export/ExactPrint.hs

Lines changed: 58 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,13 @@ module Ide.Plugin.Export.ExactPrint
99
, appendIE
1010
, addCtorUnderParent
1111
, printExportList
12-
, toDeltaExportList
12+
, printIE
13+
, freshCtorEntry
1314
) where
1415

1516
import Control.Lens (_last, over)
1617
import Data.Bifunctor (first)
1718
import Data.List.NonEmpty (NonEmpty (..))
18-
import qualified Data.List.NonEmpty as NE
1919
import Data.Text (Text)
2020
import qualified Data.Text as T
2121
import Development.IDE.GHC.Compat
@@ -40,7 +40,6 @@ import GHC (DeltaPos (..),
4040

4141
import Language.Haskell.GHC.ExactPrint (addComma,
4242
exactPrint,
43-
makeDeltaAst,
4443
setEntryDP)
4544

4645
#if MIN_VERSION_ghc(9,11,0)
@@ -167,9 +166,8 @@ mkTypeWithIE parent ctors =
167166
Nothing
168167
#endif
169168
where
170-
children = case NE.toList ctors of
171-
[] -> [] -- impossible
172-
(c:cs) -> mkIEName c : map (\x -> first addComma (mkIEName x)) cs
169+
children = mkIEName c : map (first addComma . mkIEName) cs
170+
c :| cs = ctors
173171

174172
-- | Map over an @IEThingWith@'s listed constructors, a no-op for any other item.
175173
overThingWithChildren :: ([LIEWrappedName GhcPs] -> [LIEWrappedName GhcPs]) -> IE GhcPs -> IE GhcPs
@@ -226,58 +224,70 @@ separatorComma items =
226224

227225
-- | 'Nothing' iff @ctor@ is already exported (via @T(..)@ or @T(...,ctor,...)@).
228226
addCtorUnderParent ::
229-
RdrName {- ^ parent -} ->
230-
RdrName {- ^ ctor -} ->
227+
-- | parent
228+
RdrName ->
229+
-- | ctor
230+
RdrName ->
231231
LExportList ->
232232
Maybe LExportList
233233
addCtorUnderParent parent ctor lst@(L l items) =
234-
case findParent items of
235-
ParentNotFound -> Just $ appendIE (mkTypeWithIE parent (ctor :| [])) lst
236-
FoundIEThingAll -> Nothing
237-
FoundIEThingWith CtorPresent -> Nothing
238-
FoundIEThingWith CtorAbsent -> Just (L l (map (transformParent extendThingWith) items))
239-
FoundIEThingAbs ->
240-
let upgraded = unLoc (mkTypeWithIE parent (ctor :| []))
241-
in Just (L l (map (transformParent (const upgraded)) items))
234+
case ctorExportEdit parent ctor items of
235+
AlreadyExported -> Nothing
236+
AppendParent -> Just (appendIE newThing lst)
237+
UpgradeBare -> Just (L l (map (transformParent (const (unLoc newThing))) items))
238+
AddChild -> Just (L l (map (transformParent (addCtorChildren ctor)) items))
242239
where
243-
parentFS = rdrNameFS parent
244-
ctorFS = rdrNameFS ctor
245-
246-
ctorPresence cs
247-
| any ((== ctorFS) . lieWrappedNameFS) cs = CtorPresent
248-
| otherwise = CtorAbsent
249-
250-
findParent [] = ParentNotFound
251-
findParent (L _ ie : rest)
252-
| parentNameIs parentFS ie =
253-
case ie of
254-
IEThingAll{} -> FoundIEThingAll
255-
IEThingAbs{} -> FoundIEThingAbs
256-
_ | Just cs <- ieThingWithChildren ie -> FoundIEThingWith (ctorPresence cs)
257-
| otherwise -> findParent rest
258-
| otherwise = findParent rest
259-
240+
newThing = mkTypeWithIE parent (ctor :| [])
260241
transformParent f (L itemLoc ie)
261-
| parentNameIs parentFS ie = L itemLoc (f ie)
242+
| parentNameIs (rdrNameFS parent) ie = L itemLoc (f ie)
262243
| otherwise = L itemLoc ie
263244

264-
extendThingWith :: IE GhcPs -> IE GhcPs
265-
extendThingWith = overThingWithChildren $ \cs ->
266-
let hasSibling = not (null cs)
267-
newChild = setEntryDP (mkIEName ctor) (SameLine (if hasSibling then 1 else 0))
268-
in (if hasSibling then map (first ensureTrailingComma) cs else cs) ++ [newChild]
245+
-- | Append @ctor@ to an @IEThingWith@'s children, reusing the sibling separator
246+
-- comma. No-op for other items.
247+
addCtorChildren :: RdrName -> IE GhcPs -> IE GhcPs
248+
addCtorChildren ctor = overThingWithChildren $ \cs ->
249+
let hasSibling = not (null cs)
250+
newChild = setEntryDP (mkIEName ctor) (SameLine (if hasSibling then 1 else 0))
251+
in (if hasSibling then map (first ensureTrailingComma) cs else cs) ++ [newChild]
269252

270253
printExportList :: LExportList -> Text
271254
printExportList l = T.pack (exactPrint (setEntryDP l (SameLine 0)))
272255

273-
toDeltaExportList :: LExportList -> LExportList
274-
toDeltaExportList = makeDeltaAst
256+
-- | Exactprint a single item, without the surrounding list layout. The
257+
-- trailing separator comma counts as layout: dropping it keeps a spliced item
258+
-- from carrying a stray comma into text that already supplies its own.
259+
printIE :: LIE GhcPs -> Text
260+
printIE item = T.pack (exactPrint (setEntryDP (first removeTrailingCommaAnn item) (SameLine 0)))
275261

276-
data FindParentResult
277-
= ParentNotFound
278-
| FoundIEThingAll
279-
| FoundIEThingWith CtorPresence
280-
| FoundIEThingAbs
262+
-- | A fresh @T(ctor)@ export entry rendered as text, or 'Nothing' if @ctor@ is
263+
-- already exported in the parsed list. Under CPP this adds a standalone entry so
264+
-- the splice never reprints an existing @T(...)@ span, which can straddle a
265+
-- directive.
266+
freshCtorEntry :: RdrName -> RdrName -> [LIE GhcPs] -> Maybe Text
267+
freshCtorEntry parent ctor items = case ctorExportEdit parent ctor items of
268+
AlreadyExported -> Nothing
269+
_ -> Just (printIE (mkTypeWithIE parent (ctor :| [])))
281270

282-
data CtorPresence = CtorAbsent | CtorPresent
283-
deriving Eq
271+
-- | How to add @ctor@ to an export list so its parent type @T@ exports it.
272+
data CtorEdit
273+
= AlreadyExported -- ^ @T(..)@ or @T(..., ctor, ...)@, nothing to do
274+
| AppendParent -- ^ no entry for @T@ yet, add a fresh @T(ctor)@
275+
| UpgradeBare -- ^ replace the bare @T@ entry with @T(ctor)@
276+
| AddChild -- ^ add @ctor@ to the existing @T(...)@ entry
277+
278+
-- | Decide how @ctor@ should be added under @parent@, classifying the first
279+
-- matching export item by its constructor-carrying shape.
280+
ctorExportEdit :: RdrName -> RdrName -> [LIE GhcPs] -> CtorEdit
281+
ctorExportEdit parent ctor = go
282+
where
283+
parentFS = rdrNameFS parent
284+
ctorFS = rdrNameFS ctor
285+
go [] = AppendParent
286+
go (L _ ie : rest)
287+
| parentNameIs parentFS ie = case ie of
288+
IEThingAll {} -> AlreadyExported
289+
IEThingAbs {} -> UpgradeBare
290+
_ | Just cs <- ieThingWithChildren ie ->
291+
if any ((== ctorFS) . lieWrappedNameFS) cs then AlreadyExported else AddChild
292+
| otherwise -> go rest
293+
| otherwise = go rest

plugins/hls-export-plugin/src/Ide/Plugin/Export/Exports.hs

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,15 @@ module Ide.Plugin.Export.Exports
55
, addConstructorExport
66
) where
77

8-
import Data.Maybe (isJust)
8+
import Data.Maybe (isJust)
9+
import Data.Text (Text)
10+
import Data.Text.Utf16.Rope.Mixed (Rope)
911
import Development.IDE.GHC.Compat
10-
import Development.IDE.GHC.Error (srcSpanToRange)
12+
import Development.IDE.GHC.Error (srcSpanToRange)
13+
import Development.IDE.GHC.ExactPrint.CPP (spanHasCpp)
1114
import Ide.Plugin.Export.ExactPrint
1215
import Ide.Plugin.Export.Utils
16+
import Language.Haskell.GHC.ExactPrint (makeDeltaAst)
1317
import Language.LSP.Protocol.Types
1418

1519
isExplicit :: ParsedSource -> Bool
@@ -24,15 +28,39 @@ isExported n ps = case hsmodExports (unLoc ps) of
2428
nFS = rdrNameFS n
2529
covers ie = parentNameIs nFS ie || isInIE nFS ie
2630

27-
replaceExportList :: ParsedSource -> (LExportList -> Maybe LExportList) -> Maybe [TextEdit]
28-
replaceExportList ps f = do
31+
-- | Extract the export list and pick an edit strategy: splice surgically when
32+
-- the span holds a CPP directive, otherwise reprint the whole transformed list.
33+
withExportList
34+
:: Maybe Rope
35+
-> ParsedSource
36+
-> (LExportList -> Maybe LExportList) -- ^ reprint transform
37+
-> (Range -> LExportList -> Maybe [TextEdit]) -- ^ list holds a directive
38+
-> Maybe [TextEdit]
39+
withExportList msrc ps reprint onCpp = do
2940
exports <- hsmodExports (unLoc ps)
30-
newList <- f (toDeltaExportList exports)
31-
r <- srcSpanToRange (getLoc exports)
32-
Just [TextEdit r (printExportList newList)]
41+
full <- srcSpanToRange (getLoc exports)
42+
if spanHasCpp msrc full
43+
then onCpp full exports
44+
else do
45+
newList <- reprint (makeDeltaAst exports)
46+
Just [TextEdit full (printExportList newList)]
3347

34-
addExport :: ParsedSource -> LIE GhcPs -> Maybe [TextEdit]
35-
addExport ps item = replaceExportList ps (Just . appendIE item)
48+
addExport :: Maybe Rope -> ParsedSource -> LIE GhcPs -> Maybe [TextEdit]
49+
addExport msrc ps item =
50+
withExportList msrc ps (Just . appendIE item) $ \full _ ->
51+
Just [insertAfterOpen full (printIE item)]
3652

37-
addConstructorExport :: RdrName -> RdrName -> ParsedSource -> Maybe [TextEdit]
38-
addConstructorExport parent ctor ps = replaceExportList ps (addCtorUnderParent parent ctor)
53+
addConstructorExport :: Maybe Rope -> RdrName -> RdrName -> ParsedSource -> Maybe [TextEdit]
54+
addConstructorExport msrc parent ctor ps =
55+
withExportList msrc ps (addCtorUnderParent parent ctor) $ \full exports ->
56+
(\txt -> [insertAfterOpen full txt]) <$> freshCtorEntry parent ctor (unLoc exports)
57+
58+
-- | Splice @itemTxt@ in right after the opening paren with a trailing comma,
59+
-- @( <itemTxt>, <existing> )@. Valid in every CPP branch: a first item needs no
60+
-- leading separator and a trailing comma is always legal.
61+
insertAfterOpen :: Range -> Text -> TextEdit
62+
insertAfterOpen (Range (Position sl sc) _) itemTxt =
63+
TextEdit (Range pos pos) (" " <> itemTxt <> ",")
64+
where
65+
-- `sc` is the column of `(`, so insert just past it.
66+
pos = Position sl (sc + 1)

0 commit comments

Comments
 (0)