diff --git a/CODEOWNERS b/CODEOWNERS index b196037cdf..6553e197cb 100644 --- a/CODEOWNERS +++ b/CODEOWNERS @@ -23,6 +23,7 @@ /plugins/hls-explicit-fixity-plugin /plugins/hls-explicit-imports-plugin /plugins/hls-explicit-record-fields-plugin @ozkutuk +/plugins/hls-export-plugin @crtschin /plugins/hls-fourmolu-plugin @georgefst /plugins/hls-gadt-plugin @July541 /plugins/hls-hlint-plugin @eddiemundo diff --git a/ghcide/ghcide.cabal b/ghcide/ghcide.cabal index 6098498701..01b694ccbc 100644 --- a/ghcide/ghcide.cabal +++ b/ghcide/ghcide.cabal @@ -142,6 +142,7 @@ library Development.IDE.Core.RuleTypes Development.IDE.Core.Service Development.IDE.Core.Shake + Development.IDE.Core.Text Development.IDE.Core.Tracing Development.IDE.Core.UseStale Development.IDE.Core.WorkerThread diff --git a/ghcide/src/Development/IDE/Core/Text.hs b/ghcide/src/Development/IDE/Core/Text.hs new file mode 100644 index 0000000000..2e8265b8d4 --- /dev/null +++ b/ghcide/src/Development/IDE/Core/Text.hs @@ -0,0 +1,19 @@ +module Development.IDE.Core.Text + ( takeLineRange + , lineAt + ) where + +import Data.Maybe (listToMaybe) +import Data.Text (Text) +import Data.Text.Utf16.Rope.Mixed (Rope) +import qualified Data.Text.Utf16.Rope.Mixed as Rope + +-- | The lines of @rope@ over the 0-based inclusive line range @[from, to]@. +takeLineRange :: Word -> Word -> Rope -> [Text] +takeLineRange from to rope + | to < from = [] + | otherwise = Rope.lines $ fst $ Rope.splitAtLine (to - from + 1) $ snd $ Rope.splitAtLine from rope + +-- | The 0-based line @n@ of @rope@, if it has one. +lineAt :: Word -> Rope -> Maybe Text +lineAt n = listToMaybe . takeLineRange n n diff --git a/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs b/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs index 3fe20d24b9..cae32626c7 100644 --- a/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs +++ b/ghcide/src/Development/IDE/Plugin/Completions/Logic.hs @@ -40,6 +40,7 @@ import qualified Data.HashSet as HashSet import Data.Ord (Down (Down)) import qualified Data.Set as Set import Development.IDE.Core.PositionMapping +import Development.IDE.Core.Text (lineAt) import Development.IDE.GHC.Compat hiding (isQual, ppr) import qualified Development.IDE.GHC.Compat as GHC import Development.IDE.GHC.Compat.Util @@ -876,8 +877,7 @@ getCompletionPrefixFromRope pos@(Position l c) ropetext = lastMaybe = headMaybe . reverse -- grab the entire line the cursor is at - curLine <- headMaybe $ Rope.lines - $ fst $ Rope.splitAtLine 1 $ snd $ Rope.splitAtLine (fromIntegral l) ropetext + curLine <- lineAt (fromIntegral l) ropetext let beforePos = T.take (fromIntegral c) curLine -- the word getting typed, after previous space and before cursor curWord <- diff --git a/haskell-language-server.cabal b/haskell-language-server.cabal index ea2bbbcc64..c942e707c8 100644 --- a/haskell-language-server.cabal +++ b/haskell-language-server.cabal @@ -1771,11 +1771,15 @@ library hls-exactprint-utils import: defaults, pedantic, warnings exposed-modules: Development.IDE.GHC.ExactPrint.Annotation + Development.IDE.GHC.ExactPrint.CPP hs-source-dirs: hls-exactprint-utils/src build-depends: , ghc , ghc-exactprint , ghcide == 2.14.0.0 + , lsp >=2.8 + , text + , text-rope default-extensions: CPP @@ -1805,6 +1809,7 @@ library hls-export-plugin build-depends: , containers , ghc + , ghc-boot-th , ghc-exactprint , ghcide == 2.14.0.0 , haskell-language-server:hls-exactprint-utils @@ -1813,6 +1818,7 @@ library hls-export-plugin , lsp >=2.8 , stm , text + , text-rope default-extensions: , DataKinds , LambdaCase diff --git a/hls-exactprint-utils/src/Development/IDE/GHC/ExactPrint/CPP.hs b/hls-exactprint-utils/src/Development/IDE/GHC/ExactPrint/CPP.hs new file mode 100644 index 0000000000..8999dd1fbd --- /dev/null +++ b/hls-exactprint-utils/src/Development/IDE/GHC/ExactPrint/CPP.hs @@ -0,0 +1,23 @@ +{-# LANGUAGE OverloadedStrings #-} + +module Development.IDE.GHC.ExactPrint.CPP + ( spanHasCpp + , isCppDirective + ) where + +import Data.Text (Text) +import qualified Data.Text as T +import Data.Text.Utf16.Rope.Mixed (Rope) +import Development.IDE.Core.Text (takeLineRange) +import Language.LSP.Protocol.Types (Position (..), Range (..)) + +-- | Whether the source over @range@ holds a CPP directive. +spanHasCpp :: Maybe Rope -> Range -> Bool +spanHasCpp Nothing _ = False +spanHasCpp (Just rope) (Range (Position l0 _) (Position l1 _)) = + any isCppDirective (takeLineRange (fromIntegral l0) (fromIntegral l1) rope) + +-- | Whether a line is a CPP directive. In a source compiled with CPP a directive +-- is the only line whose first non-space character is @#@. +isCppDirective :: Text -> Bool +isCppDirective = T.isPrefixOf "#" . T.stripStart diff --git a/plugins/hls-export-plugin/src/Ide/Plugin/Export.hs b/plugins/hls-export-plugin/src/Ide/Plugin/Export.hs index ed7034c907..be94208b7a 100644 --- a/plugins/hls-export-plugin/src/Ide/Plugin/Export.hs +++ b/plugins/hls-export-plugin/src/Ide/Plugin/Export.hs @@ -5,14 +5,17 @@ module Ide.Plugin.Export (descriptor) where import Control.Concurrent.STM (atomically) import Control.Lens import Control.Monad.IO.Class (liftIO) +import Data.Maybe (isJust, isNothing) import Data.Text (Text) import qualified Data.Text as T +import Data.Text.Utf16.Rope.Mixed (Rope) 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 qualified GHC.LanguageExtensions.Type as LangExt (Extension (..)) import Ide.Plugin.Error (getNormalizedFilePathE) import Ide.Plugin.Export.Cursor import Ide.Plugin.Export.ExactPrint @@ -35,17 +38,27 @@ quickCodeActionHandlers :: PluginMethodHandler IdeState Method_TextDocumentCodeA 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 + (ps, isCpp, mUnder, msrc) <- runActionE "Export.getInputs" state $ do + pm <- useE GetParsedModuleWithComments nfp + let ps = pm_parsed_source pm + isCpp = xopt LangExt.Cpp (ms_hspp_opts (pm_mod_summary pm)) + mUnder = if isExplicit ps then locateUnderCursor (range ^. L.start) ps else Nothing + -- Only a CPP module about to be offered an action needs the buffer (to find + -- directives in the export list), so skip the fetch otherwise. + msrc <- if isJust mUnder && isCpp then snd <$> useE GetFileContents nfp else pure Nothing + pure (ps, isCpp, mUnder, msrc) + case mUnder of + -- A CPP module whose buffer we could not read may have directives in the + -- export list that a reprint would silently erase. Withhold rather than risk + -- it. + Just under | not (isCpp && isNothing msrc) -> 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 + [ addAction msrc under ps ] , let fixes = [ d | d <- unusedDiags, locateUnderCursor (d ^. L.range . L.start) ps == Just under ] ca = mkAction (verb <> " `" <> title <> "`") @@ -63,14 +76,14 @@ unusedTopBindDiagnostics state nfp = do isUnusedTopBind = has (fdStructuredMessageL . _SomeStructuredMessage . msgEnvelopeErrorL . _TcRnUnusedTopBind) -addAction :: UnderCursor -> ParsedSource -> Maybe (Text, Text, [TextEdit]) -addAction under ps = case under of +addAction :: Maybe Rope -> UnderCursor -> ParsedSource -> Maybe (Text, Text, [TextEdit]) +addAction msrc under ps = case under of Decl flavor n | n `isExported` ps -> Nothing - | otherwise -> ("Export", T.pack (printRdrName n),) <$> addExport ps (mkExportIE flavor n) + | otherwise -> ("Export", T.pack (printRdrName n),) <$> addExport msrc 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 + <$> addConstructorExport msrc t c ps Header -> Nothing diff --git a/plugins/hls-export-plugin/src/Ide/Plugin/Export/ExactPrint.hs b/plugins/hls-export-plugin/src/Ide/Plugin/Export/ExactPrint.hs index 3211f5d7c7..f4e0639e43 100644 --- a/plugins/hls-export-plugin/src/Ide/Plugin/Export/ExactPrint.hs +++ b/plugins/hls-export-plugin/src/Ide/Plugin/Export/ExactPrint.hs @@ -9,13 +9,13 @@ module Ide.Plugin.Export.ExactPrint , appendIE , addCtorUnderParent , printExportList - , toDeltaExportList + , printIE + , freshCtorEntry ) 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 @@ -40,7 +40,6 @@ import GHC (DeltaPos (..), import Language.Haskell.GHC.ExactPrint (addComma, exactPrint, - makeDeltaAst, setEntryDP) #if MIN_VERSION_ghc(9,11,0) @@ -167,9 +166,8 @@ mkTypeWithIE parent ctors = Nothing #endif where - children = case NE.toList ctors of - [] -> [] -- impossible - (c:cs) -> mkIEName c : map (\x -> first addComma (mkIEName x)) cs + children = mkIEName c : map (first addComma . mkIEName) cs + c :| cs = ctors -- | Map over an @IEThingWith@'s listed constructors, a no-op for any other item. overThingWithChildren :: ([LIEWrappedName GhcPs] -> [LIEWrappedName GhcPs]) -> IE GhcPs -> IE GhcPs @@ -226,58 +224,70 @@ separatorComma items = -- | 'Nothing' iff @ctor@ is already exported (via @T(..)@ or @T(...,ctor,...)@). addCtorUnderParent :: - RdrName {- ^ parent -} -> - RdrName {- ^ ctor -} -> + -- | parent + RdrName -> + -- | ctor + RdrName -> 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)) + case ctorExportEdit parent ctor items of + AlreadyExported -> Nothing + AppendParent -> Just (appendIE newThing lst) + UpgradeBare -> Just (L l (map (transformParent (const (unLoc newThing))) items)) + AddChild -> Just (L l (map (transformParent (addCtorChildren ctor)) 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 - + newThing = mkTypeWithIE parent (ctor :| []) transformParent f (L itemLoc ie) - | parentNameIs parentFS ie = L itemLoc (f ie) + | parentNameIs (rdrNameFS parent) 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] +-- | Append @ctor@ to an @IEThingWith@'s children, reusing the sibling separator +-- comma. No-op for other items. +addCtorChildren :: RdrName -> IE GhcPs -> IE GhcPs +addCtorChildren ctor = 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 +-- | Exactprint a single item, without the surrounding list layout. The +-- trailing separator comma counts as layout: dropping it keeps a spliced item +-- from carrying a stray comma into text that already supplies its own. +printIE :: LIE GhcPs -> Text +printIE item = T.pack (exactPrint (setEntryDP (first removeTrailingCommaAnn item) (SameLine 0))) -data FindParentResult - = ParentNotFound - | FoundIEThingAll - | FoundIEThingWith CtorPresence - | FoundIEThingAbs +-- | A fresh @T(ctor)@ export entry rendered as text, or 'Nothing' if @ctor@ is +-- already exported in the parsed list. Under CPP this adds a standalone entry so +-- the splice never reprints an existing @T(...)@ span, which can straddle a +-- directive. +freshCtorEntry :: RdrName -> RdrName -> [LIE GhcPs] -> Maybe Text +freshCtorEntry parent ctor items = case ctorExportEdit parent ctor items of + AlreadyExported -> Nothing + _ -> Just (printIE (mkTypeWithIE parent (ctor :| []))) -data CtorPresence = CtorAbsent | CtorPresent - deriving Eq +-- | How to add @ctor@ to an export list so its parent type @T@ exports it. +data CtorEdit + = AlreadyExported -- ^ @T(..)@ or @T(..., ctor, ...)@, nothing to do + | AppendParent -- ^ no entry for @T@ yet, add a fresh @T(ctor)@ + | UpgradeBare -- ^ replace the bare @T@ entry with @T(ctor)@ + | AddChild -- ^ add @ctor@ to the existing @T(...)@ entry + +-- | Decide how @ctor@ should be added under @parent@, classifying the first +-- matching export item by its constructor-carrying shape. +ctorExportEdit :: RdrName -> RdrName -> [LIE GhcPs] -> CtorEdit +ctorExportEdit parent ctor = go + where + parentFS = rdrNameFS parent + ctorFS = rdrNameFS ctor + go [] = AppendParent + go (L _ ie : rest) + | parentNameIs parentFS ie = case ie of + IEThingAll {} -> AlreadyExported + IEThingAbs {} -> UpgradeBare + _ | Just cs <- ieThingWithChildren ie -> + if any ((== ctorFS) . lieWrappedNameFS) cs then AlreadyExported else AddChild + | otherwise -> go rest + | otherwise = go rest diff --git a/plugins/hls-export-plugin/src/Ide/Plugin/Export/Exports.hs b/plugins/hls-export-plugin/src/Ide/Plugin/Export/Exports.hs index 04fd0e339b..b64ca48d11 100644 --- a/plugins/hls-export-plugin/src/Ide/Plugin/Export/Exports.hs +++ b/plugins/hls-export-plugin/src/Ide/Plugin/Export/Exports.hs @@ -5,11 +5,15 @@ module Ide.Plugin.Export.Exports , addConstructorExport ) where -import Data.Maybe (isJust) +import Data.Maybe (isJust) +import Data.Text (Text) +import Data.Text.Utf16.Rope.Mixed (Rope) import Development.IDE.GHC.Compat -import Development.IDE.GHC.Error (srcSpanToRange) +import Development.IDE.GHC.Error (srcSpanToRange) +import Development.IDE.GHC.ExactPrint.CPP (spanHasCpp) import Ide.Plugin.Export.ExactPrint import Ide.Plugin.Export.Utils +import Language.Haskell.GHC.ExactPrint (makeDeltaAst) import Language.LSP.Protocol.Types isExplicit :: ParsedSource -> Bool @@ -24,15 +28,39 @@ isExported n ps = case hsmodExports (unLoc ps) of nFS = rdrNameFS n covers ie = parentNameIs nFS ie || isInIE nFS ie -replaceExportList :: ParsedSource -> (LExportList -> Maybe LExportList) -> Maybe [TextEdit] -replaceExportList ps f = do +-- | Extract the export list and pick an edit strategy: splice surgically when +-- the span holds a CPP directive, otherwise reprint the whole transformed list. +withExportList + :: Maybe Rope + -> ParsedSource + -> (LExportList -> Maybe LExportList) -- ^ reprint transform + -> (Range -> LExportList -> Maybe [TextEdit]) -- ^ list holds a directive + -> Maybe [TextEdit] +withExportList msrc ps reprint onCpp = do exports <- hsmodExports (unLoc ps) - newList <- f (toDeltaExportList exports) - r <- srcSpanToRange (getLoc exports) - Just [TextEdit r (printExportList newList)] + full <- srcSpanToRange (getLoc exports) + if spanHasCpp msrc full + then onCpp full exports + else do + newList <- reprint (makeDeltaAst exports) + Just [TextEdit full (printExportList newList)] -addExport :: ParsedSource -> LIE GhcPs -> Maybe [TextEdit] -addExport ps item = replaceExportList ps (Just . appendIE item) +addExport :: Maybe Rope -> ParsedSource -> LIE GhcPs -> Maybe [TextEdit] +addExport msrc ps item = + withExportList msrc ps (Just . appendIE item) $ \full _ -> + Just [insertAfterOpen full (printIE item)] -addConstructorExport :: RdrName -> RdrName -> ParsedSource -> Maybe [TextEdit] -addConstructorExport parent ctor ps = replaceExportList ps (addCtorUnderParent parent ctor) +addConstructorExport :: Maybe Rope -> RdrName -> RdrName -> ParsedSource -> Maybe [TextEdit] +addConstructorExport msrc parent ctor ps = + withExportList msrc ps (addCtorUnderParent parent ctor) $ \full exports -> + (\txt -> [insertAfterOpen full txt]) <$> freshCtorEntry parent ctor (unLoc exports) + +-- | Splice @itemTxt@ in right after the opening paren with a trailing comma, +-- @( , )@. Valid in every CPP branch: a first item needs no +-- leading separator and a trailing comma is always legal. +insertAfterOpen :: Range -> Text -> TextEdit +insertAfterOpen (Range (Position sl sc) _) itemTxt = + TextEdit (Range pos pos) (" " <> itemTxt <> ",") + where + -- `sc` is the column of `(`, so insert just past it. + pos = Position sl (sc + 1) diff --git a/plugins/hls-export-plugin/test/Main.hs b/plugins/hls-export-plugin/test/Main.hs index d5ad6a777d..b76cd83a06 100644 --- a/plugins/hls-export-plugin/test/Main.hs +++ b/plugins/hls-export-plugin/test/Main.hs @@ -1,6 +1,7 @@ module Main (main) where import Control.Lens ((^.)) +import Data.Char (isSpace) import Data.Either (rights) import Data.List (sort) import Data.Maybe (fromMaybe) @@ -9,7 +10,8 @@ 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) +import Test.Hls.FileSystem (copy, directProject, + mkVirtualFileTree) plugin :: PluginTestDescriptor () plugin = mkPluginTestDescriptor' descriptor "export" @@ -21,38 +23,32 @@ testDataDir = "plugins" "hls-export-plugin" "test" "testdata" -- 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 = +runExport = runExportWith [] + +-- | Like 'runExport' but also copies the named extra files into the project, +-- e.g. a header a CPP @#include@ pulls in next to the module. +runExportWith :: [FilePath] -> FilePath -> (TextDocumentIdentifier -> Session a) -> IO a +runExportWith extra hsFile act = runSessionWithTestConfig def - { testDirLocation = Right (mkVirtualFileTree testDataDir (directProject hsFile)) + { testDirLocation = Right (mkVirtualFileTree testDataDir (directProject hsFile <> map copy extra)) , 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 +executeExportAction :: TextDocumentIdentifier -> Range -> Session () +executeExportAction doc range = do actions <- rights . map toEither <$> getCodeActions doc range - case filter (\ca -> prefix `T.isPrefixOf` (ca ^. L.title)) actions of + case filter (\ca -> "Export `" `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) + [] -> liftIO $ assertFailure "Export `...` action not offered" noExportOffered :: TextDocumentIdentifier -> Range -> Session () -noExportOffered = noActionWithPrefix "Export `" +noExportOffered doc range = do + titles <- sort . map (^. L.title) . rights . map toEither <$> getCodeActions doc range + liftIO $ not (any ("Export `" `T.isPrefixOf`) titles) + @? ("Did not expect an Export action; saw: " <> show titles) -- | Fail unless some variant is an infix of the text. The message dumps it. assertAnyInfix :: T.Text -> [T.Text] -> Assertion @@ -63,146 +59,252 @@ assertAnyInfix hay variants = containsAfter :: TextDocumentIdentifier -> [T.Text] -> Session () containsAfter doc expected = documentContents doc >>= liftIO . (`assertAnyInfix` expected) +-- | Fail unless every needle is an infix of the haystack. Used to assert that +-- CPP directives and conditional items survive an edit verbatim. +assertContainsAll :: T.Text -> [T.Text] -> Assertion +assertContainsAll hay = mapM_ $ \needle -> + needle `T.isInfixOf` hay + @? ("Expected " <> show needle <> " in:\n" <> T.unpack hay) + +-- | Lines from the first @(@ through the @) where@ line (included, so an item on +-- the closing line still counts). +exportListRegion :: T.Text -> [T.Text] +exportListRegion txt = + let afterOpen = dropWhile (not . T.isInfixOf "(") (T.lines txt) + (body, close) = break (T.isInfixOf ") where") afterOpen + in body ++ take 1 close + +-- | Does @name@ appear in the export list at CPP nesting level 0, i.e. not +-- guarded by any @#if@/@#ifdef@/@#ifndef@? +exportedUnconditionally :: T.Text -> T.Text -> Bool +exportedUnconditionally name txt = go (0 :: Int) (exportListRegion txt) + where + go _ [] = False + go n (l:ls) + | "#if" `T.isPrefixOf` T.stripStart l = go (n + 1) ls + | "#endif" `T.isPrefixOf` T.stripStart l = go (max 0 (n - 1)) ls + | n == 0, name `T.isInfixOf` l = True + | otherwise = go n ls + +-- | True when the export-list region carries no doubled or leading comma. A +-- trailing comma before @)@ is legal Haskell, so @,)@ is not flagged. +wellFormedExportList :: T.Text -> Bool +wellFormedExportList txt = not (any (`T.isInfixOf` compact) ["(,", ",,"]) + where + compact = T.filter (not . isSpace) (T.unlines (exportListRegion txt)) + +-- | Run the export action, assert the result is well-formed, return its text. +exportAndCheck :: TextDocumentIdentifier -> Range -> Session T.Text +exportAndCheck doc pos = do + executeExportAction doc pos + txt <- documentContents doc + liftIO $ wellFormedExportList txt + @? ("malformed export list, got:\n" <> T.unpack txt) + pure txt + +-- | Crudely re-run CPP for the macro EXAMPLE_FLAG over already-edited text, +-- keeping the branch the given definedness selects. Single level, just enough +-- to inspect the configuration the server did not parse. +preprocessExampleFlag :: Bool -> T.Text -> T.Text +preprocessExampleFlag defined = T.unlines . go Nothing . T.lines + where + -- Nothing outside any conditional. Just b inside one, emitting only when b. + go _ [] = [] + go st (l:ls) + | isDir "#ifdef" = go (Just defined) ls + | isDir "#ifndef" = go (Just (not defined)) ls + | isDir "#else" = go (fmap not st) ls + | isDir "#endif" = go Nothing ls + | fromMaybe True st = l : go st ls + | otherwise = go st ls + where isDir d = d `T.isPrefixOf` T.stripStart l + rangeAt :: UInt -> UInt -> Range rangeAt l c = Range (Position l c) (Position l c) +-- | The CPP block the testdata guards with @#ifdef EXAMPLE_FLAG@. The flag is +-- never defined, so the branch is inactive and must survive an edit verbatim. +flagBlock :: [T.Text] +flagBlock = ["#ifdef EXAMPLE_FLAG", ", flagged", "#endif"] + +-- | The new export of @name@ must sit at CPP nesting level 0, never in a branch. +assertExportedUnconditionally :: T.Text -> T.Text -> Assertion +assertExportedUnconditionally name txt = + exportedUnconditionally name txt + @? (T.unpack name <> " must be exported outside any CPP branch, got:\n" <> T.unpack txt) + +-- | The 'flagBlock' survives verbatim and @name@ lands outside it. +assertFlaggedBlockKept :: T.Text -> T.Text -> Assertion +assertFlaggedBlockKept name txt = + assertContainsAll txt flagBlock >> assertExportedUnconditionally name txt + +-- | Export the binding at the position and assert the result contains one of +-- the @expected@ variants. +addCase :: TestName -> FilePath -> UInt -> UInt -> [T.Text] -> TestTree +addCase name file l c expected = testCase name $ runExport file $ \doc -> do + executeExportAction doc (rangeAt l c) + containsAfter doc expected + +-- | Assert no export action is offered at the position. +noCase :: TestName -> FilePath -> UInt -> UInt -> TestTree +noCase name file l c = testCase name $ runExport file $ \doc -> + noExportOffered doc (rangeAt l c) + +-- | Export the binding at the position, assert the list is well-formed, then +-- run @check@ over the resulting document text. +exportCase :: TestName -> FilePath -> UInt -> UInt -> (T.Text -> Assertion) -> TestTree +exportCase name file l c check = testCase name $ runExport file $ \doc -> do + txt <- exportAndCheck doc (rangeAt l c) + liftIO (check txt) + 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"] + [ addCase "add value to export list" "AddExport.hs" 6 0 + ["module AddExport (foo, Bar, bar)"] + , noCase "no action when value already exported" "AddExport.hs" 3 0 -- on `foo` + , addCase "append follows a multi-line leading-comma list" "AddExportMultiline.hs" 11 0 -- on `baz` + [" , 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 (..)"] + [ addCase "add bare type as T(..)" "AddExport.hs" 9 5 -- on `Baz` type name + ["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 + [ addCase "constructor with no parent entry appends T (C)" "AddExport.hs" 9 12 -- on `Baz1`, no Baz entry yet + ["Baz (Baz1)", "Baz(Baz1)"] + , addCase "constructor under bare-type parent promotes to T(C)" "AddCtor.hs" 3 11 -- on `Bar1`, Bar is IEThingAbs + ["Bar (Bar1)", "Bar(Bar1)"] + , addCase "constructor merges into existing IEThingWith parent" "AddCtor.hs" 2 18 -- on `Foo2`, Foo has [Foo1] + ["Foo (Foo1, Foo2)", "Foo(Foo1, Foo2)"] + , noCase "constructor already in IEThingWith children suppresses action" "AddCtor.hs" 2 11 -- on `Foo1`, already child of Foo(Foo1) + , noCase "constructor under IEThingAll T(..) suppresses action" "AddCtor.hs" 4 11 -- on `Baz1`, Baz(..) covers it + , noCase "constructor exported standalone suppresses action" "AddCtor.hs" 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` + [ addCase "add class as T(..)" "AddClass.hs" 8 6 -- on `Baz` class name + ["module AddClass (Foo (..), Bar, Baz (..))"] + , noCase "no add action when class exported as T(..)" "AddClass.hs" 2 6 -- on `Foo`, exported as Foo (..) + , noCase "no add action when class exported as bare T" "AddClass.hs" 5 6 -- on `Bar`, exported as bare + , noCase "no add action on class method" "AddClass.hs" 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"] + [ addCase "add to an empty export list" "AddExportEmpty.hs" 2 0 -- on `foo` + ["module AddExportEmpty (foo) where"] + , addCase "append after a trailing comma" "AddExportTrailingComma.hs" 7 0 -- on `bar` + ["( foo, bar"] + , addCase "preserve a haddock comment between items" "AddExportComment.hs" 16 0 -- on `quux` + [" -- * 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 (:<) (..)"] + [ addCase "function operator is parenthesized" "AddExportKinds.hs" 8 1 -- on `(<|)` + ["(placeholder, (<|))"] + , addCase "infix function exports bare name" "AddExportKinds.hs" 11 3 -- on `f` + ["(placeholder, f)"] + , addCase "newtype exports as T(..)" "AddExportKinds.hs" 13 8 -- on `NT` + ["placeholder, NT(..)", "placeholder, NT (..)"] + , addCase "type synonym exports bare" "AddExportKinds.hs" 15 5 -- on `Syn` + ["(placeholder, Syn)"] + , addCase "type family exports bare" "AddExportKinds.hs" 17 12 -- on `TF` + ["(placeholder, TF)"] + , addCase "pattern synonym gets a pattern prefix" "AddExportKinds.hs" 20 9 -- on `Pat` + ["(placeholder, pattern Pat)"] + , addCase "data operator gets type keyword and (..)" "AddExportKinds.hs" 22 7 -- on `(:<)` + ["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 (:++))"] + [ addCase "type synonym operator has no type keyword" "AddExportTypeOps.hs" 8 7 -- on `(:<>)` + ["(placeholder, (:<>))"] + , addCase "type family operator gets type keyword" "AddExportTypeOps.hs" 10 14 -- on `(:+:)` + ["(placeholder, type (:+:))"] + , addCase "typeclass operator gets type keyword and (..)" "AddExportTypeOps.hs" 12 8 -- on `(:*:)` + ["placeholder, type (:*:)(..)", "placeholder, type (:*:) (..)"] + , addCase "newtype operator gets type keyword and (..)" "AddExportTypeOps.hs" 14 10 -- on `(:->)` + ["placeholder, type (:->)(..)", "placeholder, type (:->) (..)"] + , addCase "pattern synonym operator is parenthesized" "AddExportTypeOps.hs" 16 11 -- on `(:++)` + ["(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` + [ noCase "no action on implicit module" "Implicit.hs" 3 0 + , noCase "no action when cursor on RHS" "AddExport.hs" 6 6 -- col 6 is on the `2` of `bar = 2` + , noCase "no action on a where-bound name" "AddExportNegatives.hs" 7 8 -- on `whereBound` + , noCase "no action on a record field" "AddExportNegatives.hs" 9 18 -- on `recField` + ] - , testCase "no action on a record field" $ runExport "AddExportNegatives.hs" $ \doc -> - noExportOffered doc (rangeAt 9 18) -- on `recField` + , testGroup "Add: CPP in the export list" + -- EXAMPLE_FLAG is never defined in the test project, so #ifdef branches + -- are inactive and #ifndef branches are active. The edit must preserve + -- every directive verbatim and place the new export outside any branch. + [ exportCase "preserves a trailing #ifdef block" "CppExportTail.hs" 15 0 -- on `baz` + (assertFlaggedBlockKept "baz") + + , exportCase "preserves a leading #ifndef block" "CppExportHead.hs" 12 0 $ \txt -> do -- on `bar` + -- the whole guarded block survives verbatim, not just stray substrings + assertContainsAll txt ["#ifndef EXAMPLE_FLAG\n foo\n#endif"] + assertExportedUnconditionally "bar" txt + + , exportCase "preserves both #if/#else branches" "CppExportElse.hs" 20 0 $ \txt -> do -- on `extra` + assertContainsAll txt + ["#ifdef EXAMPLE_FLAG", ", windows", "#else", ", posix", "#endif"] + assertExportedUnconditionally "extra" txt + + , testCase "preserves an #include directive" $ runExportWith ["CppExportInclude.h"] "CppExportInclude.hs" $ \doc -> do + txt <- exportAndCheck doc (rangeAt 13 0) -- on `extra` + liftIO $ do + assertContainsAll txt ["#include \"CppExportInclude.h\"", "( extra, foo"] + assertExportedUnconditionally "extra" txt + + , exportCase "appends a new T(C) beside a CPP block" "CppCtorAppend.hs" 11 11 $ \txt -> do -- on `Baz1`, no Baz entry yet + assertFlaggedBlockKept "Baz1" txt + txt `assertAnyInfix` ["Baz (Baz1)", "Baz(Baz1)"] + + , exportCase "adds a separate entry beside an IEThingWith parent" "CppCtorExtend.hs" 8 18 $ \txt -> do -- on `Foo2`, Foo has [Foo1] + assertFlaggedBlockKept "Foo2" txt + assertContainsAll txt ["Foo(Foo1)"] + txt `assertAnyInfix` ["Foo (Foo2)", "Foo(Foo2)"] + + , exportCase "adds a separate entry without a double comma" "CppCtorMid.hs" 9 18 $ \txt -> do -- on `Foo2`, Foo(Foo1) precedes `, bar` + assertContainsAll txt (flagBlock <> [", bar", "Foo(Foo1)"]) + txt `assertAnyInfix` ["Foo (Foo2)", "Foo(Foo2)"] + + , exportCase "adds a separate entry beside a bare-type parent" "CppCtorUpgrade.hs" 8 11 $ \txt -> do -- on `Bar1`, Bar is IEThingAbs + assertFlaggedBlockKept "Bar1" txt + txt `assertAnyInfix` ["Bar (Bar1)", "Bar(Bar1)"] + + , exportCase "exports an operator beside a CPP block" "CppExportKinds.hs" 12 1 -- on `(<|)` + (assertFlaggedBlockKept "(<|)") + + , exportCase "exports a pattern synonym beside a CPP block" "CppExportKinds.hs" 16 8 -- on `Zero` + (assertFlaggedBlockKept "pattern Zero") + + , exportCase "adds a separate entry beside a directive inside a constructor list" "CppCtorIntra.hs" 9 24 $ \txt -> do -- on `Foo2` + -- the #ifdef sits inside Foo(...), where an in-place merge would erase it + assertContainsAll txt ["#ifdef EXAMPLE_FLAG\n , Bar\n#endif", "Foo(Foo1"] + txt `assertAnyInfix` ["Foo (Foo2)", "Foo(Foo2)"] + + , exportCase "front-inserts even when the close paren shares a line" "CppExportParenShared.hs" 18 0 $ \txt -> do -- on `baz` + assertContainsAll txt (flagBlock <> [", bar )"]) + assertExportedUnconditionally "baz" txt + + , exportCase "no double comma when the last item already has a trailing comma" "CppExportTrailingComma.hs" 15 0 $ \txt -> do -- on `baz` + -- a doubled `,,` would be caught by exportAndCheck's well-formedness check + assertContainsAll txt ["#ifdef EXAMPLE_FLAG", "flagged,", "#endif"] + assertExportedUnconditionally "baz" txt + + , exportCase "edit stays valid in the unparsed CPP branch" "CppExportOtherBranch.hs" 12 0 $ \txt -> do -- on `bar`, the only item is in the other branch + -- the single item lives under #ifndef, so it is the whole parsed list. + -- The front-insert plus trailing comma stays valid when the flag flips + -- and that item disappears. + let otherBranch = preprocessExampleFlag True txt + wellFormedExportList otherBranch + @? ("edit breaks the EXAMPLE_FLAG-defined configuration:\n" <> T.unpack otherBranch) ] , testGroup "Export fixes the unused-binding warning" diff --git a/plugins/hls-export-plugin/test/testdata/CppCtorAppend.hs b/plugins/hls-export-plugin/test/testdata/CppCtorAppend.hs new file mode 100644 index 0000000000..1cba367b95 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppCtorAppend.hs @@ -0,0 +1,15 @@ +{-# LANGUAGE CPP #-} +module CppCtorAppend + ( foo +#ifdef EXAMPLE_FLAG + , flagged +#endif + ) where + +foo :: Int +foo = 1 + +data Baz = Baz1 | Baz2 + +flagged :: Int +flagged = 0 diff --git a/plugins/hls-export-plugin/test/testdata/CppCtorExtend.hs b/plugins/hls-export-plugin/test/testdata/CppCtorExtend.hs new file mode 100644 index 0000000000..8b485e582e --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppCtorExtend.hs @@ -0,0 +1,12 @@ +{-# LANGUAGE CPP #-} +module CppCtorExtend + ( Foo(Foo1) +#ifdef EXAMPLE_FLAG + , flagged +#endif + ) where + +data Foo = Foo1 | Foo2 + +flagged :: Int +flagged = 0 diff --git a/plugins/hls-export-plugin/test/testdata/CppCtorIntra.hs b/plugins/hls-export-plugin/test/testdata/CppCtorIntra.hs new file mode 100644 index 0000000000..9e685f0b4d --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppCtorIntra.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE CPP #-} +module CppCtorIntra + ( Foo(Foo1 +#ifdef EXAMPLE_FLAG + , Bar +#endif + ) + ) where + +data Foo = Foo1 | Bar | Foo2 + +foo :: Int +foo = 0 diff --git a/plugins/hls-export-plugin/test/testdata/CppCtorMid.hs b/plugins/hls-export-plugin/test/testdata/CppCtorMid.hs new file mode 100644 index 0000000000..79e26db638 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppCtorMid.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE CPP #-} +module CppCtorMid + ( Foo(Foo1) + , bar +#ifdef EXAMPLE_FLAG + , flagged +#endif + ) where + +data Foo = Foo1 | Foo2 + +bar :: Int +bar = 0 + +flagged :: Int +flagged = 0 diff --git a/plugins/hls-export-plugin/test/testdata/CppCtorUpgrade.hs b/plugins/hls-export-plugin/test/testdata/CppCtorUpgrade.hs new file mode 100644 index 0000000000..133947c5d1 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppCtorUpgrade.hs @@ -0,0 +1,12 @@ +{-# LANGUAGE CPP #-} +module CppCtorUpgrade + ( Bar +#ifdef EXAMPLE_FLAG + , flagged +#endif + ) where + +data Bar = Bar1 | Bar2 + +flagged :: Int +flagged = 0 diff --git a/plugins/hls-export-plugin/test/testdata/CppExportElse.hs b/plugins/hls-export-plugin/test/testdata/CppExportElse.hs new file mode 100644 index 0000000000..f7176e12a0 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportElse.hs @@ -0,0 +1,21 @@ +{-# LANGUAGE CPP #-} +module CppExportElse + ( foo +#ifdef EXAMPLE_FLAG + , windows +#else + , posix +#endif + ) where + +foo :: Int +foo = 1 + +windows :: Int +windows = 1 + +posix :: Int +posix = 2 + +extra :: Int +extra = 3 diff --git a/plugins/hls-export-plugin/test/testdata/CppExportHead.hs b/plugins/hls-export-plugin/test/testdata/CppExportHead.hs new file mode 100644 index 0000000000..d8658f58c8 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportHead.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE CPP #-} +module CppExportHead + ( +#ifndef EXAMPLE_FLAG + foo +#endif + ) where + +foo :: Int +foo = 1 + +bar :: Int +bar = 2 diff --git a/plugins/hls-export-plugin/test/testdata/CppExportInclude.h b/plugins/hls-export-plugin/test/testdata/CppExportInclude.h new file mode 100644 index 0000000000..a3e92f720a --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportInclude.h @@ -0,0 +1 @@ + , included diff --git a/plugins/hls-export-plugin/test/testdata/CppExportInclude.hs b/plugins/hls-export-plugin/test/testdata/CppExportInclude.hs new file mode 100644 index 0000000000..4e04f02ff7 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportInclude.hs @@ -0,0 +1,14 @@ +{-# LANGUAGE CPP #-} +module CppExportInclude + ( foo +#include "CppExportInclude.h" + ) where + +foo :: Int +foo = 1 + +included :: Int +included = 2 + +extra :: Int +extra = 3 diff --git a/plugins/hls-export-plugin/test/testdata/CppExportKinds.hs b/plugins/hls-export-plugin/test/testdata/CppExportKinds.hs new file mode 100644 index 0000000000..55da240340 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportKinds.hs @@ -0,0 +1,20 @@ +{-# LANGUAGE CPP #-} +{-# LANGUAGE PatternSynonyms #-} +module CppExportKinds + ( foo +#ifdef EXAMPLE_FLAG + , flagged +#endif + ) where + +foo :: Int +foo = 1 + +(<|) :: Int -> Int -> Int +a <| b = a + b + +pattern Zero :: Int +pattern Zero = 0 + +flagged :: Int +flagged = 2 diff --git a/plugins/hls-export-plugin/test/testdata/CppExportOtherBranch.hs b/plugins/hls-export-plugin/test/testdata/CppExportOtherBranch.hs new file mode 100644 index 0000000000..799d401437 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportOtherBranch.hs @@ -0,0 +1,13 @@ +{-# LANGUAGE CPP #-} +module CppExportOtherBranch + ( +#ifndef EXAMPLE_FLAG + foo +#endif + ) where + +foo :: Int +foo = 1 + +bar :: Int +bar = 2 diff --git a/plugins/hls-export-plugin/test/testdata/CppExportParenShared.hs b/plugins/hls-export-plugin/test/testdata/CppExportParenShared.hs new file mode 100644 index 0000000000..877412091e --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportParenShared.hs @@ -0,0 +1,19 @@ +{-# LANGUAGE CPP #-} +module CppExportParenShared + ( foo +#ifdef EXAMPLE_FLAG + , flagged +#endif + , bar ) where + +foo :: Int +foo = 1 + +flagged :: Int +flagged = 2 + +bar :: Int +bar = 3 + +baz :: Int +baz = 4 diff --git a/plugins/hls-export-plugin/test/testdata/CppExportTail.hs b/plugins/hls-export-plugin/test/testdata/CppExportTail.hs new file mode 100644 index 0000000000..1f9b7360d0 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportTail.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE CPP #-} +module CppExportTail + ( foo +#ifdef EXAMPLE_FLAG + , flagged +#endif + ) where + +foo :: Int +foo = 1 + +flagged :: Int +flagged = 2 + +baz :: Int +baz = 3 diff --git a/plugins/hls-export-plugin/test/testdata/CppExportTrailingComma.hs b/plugins/hls-export-plugin/test/testdata/CppExportTrailingComma.hs new file mode 100644 index 0000000000..aa7006e204 --- /dev/null +++ b/plugins/hls-export-plugin/test/testdata/CppExportTrailingComma.hs @@ -0,0 +1,16 @@ +{-# LANGUAGE CPP #-} +module CppExportTrailingComma + ( foo, +#ifdef EXAMPLE_FLAG + flagged, +#endif + ) where + +foo :: Int +foo = 1 + +flagged :: Int +flagged = 2 + +baz :: Int +baz = 3 diff --git a/plugins/hls-notes-plugin/src/Ide/Plugin/Notes.hs b/plugins/hls-notes-plugin/src/Ide/Plugin/Notes.hs index 2f4faf71b8..a73e958913 100644 --- a/plugins/hls-notes-plugin/src/Ide/Plugin/Notes.hs +++ b/plugins/hls-notes-plugin/src/Ide/Plugin/Notes.hs @@ -19,6 +19,7 @@ import Development.IDE hiding (line) import Development.IDE.Core.PluginUtils (runActionE, useE) import Development.IDE.Core.Shake (toKnownFiles) import qualified Development.IDE.Core.Shake as Shake +import Development.IDE.Core.Text (lineAt) import Development.IDE.Graph.Classes (Hashable, NFData) import GHC.Generics (Generic) import Ide.Plugin.Error (PluginError (..)) @@ -109,8 +110,7 @@ getNote nfp state (Position l c) = do contents <- err "Error getting file contents" =<< liftIO (runAction "notes.getfileContents" state (getFileContents nfp)) - line <- err "Line not found in file" (listToMaybe $ Rope.lines $ fst - (Rope.splitAtLine 1 $ snd $ Rope.splitAtLine (fromIntegral l) contents)) + line <- err "Line not found in file" (lineAt (fromIntegral l) contents) pure $ listToMaybe $ mapMaybe (atPos $ fromIntegral c) $ matchAllText noteRefRegex line where atPos c arr = case arr A.! 0 of @@ -290,7 +290,7 @@ hoverNote state _ params let lineText = case mbRope of Nothing -> "" - Just rope -> fromMaybe "" $ listToMaybe $ drop (fromIntegral line) $ Rope.lines rope + Just rope -> fromMaybe "" $ lineAt (fromIntegral line) rope mbRange = findNoteRange lineText note line @@ -391,8 +391,4 @@ noteSnippet = getLinePrefix :: Rope.Rope -> Position -> Text getLinePrefix rope (Position line col) = - case Rope.splitAtLine (fromIntegral line) rope of - (_, rest) -> - case Rope.lines rest of - (l:_) -> T.take (fromIntegral col) l - _ -> "" + maybe "" (T.take (fromIntegral col)) (lineAt (fromIntegral line) rope)