diff --git a/ghcide/src/Development/IDE/Core/Rules.hs b/ghcide/src/Development/IDE/Core/Rules.hs index ad17c04705..1f8e1cc7d8 100644 --- a/ghcide/src/Development/IDE/Core/Rules.hs +++ b/ghcide/src/Development/IDE/Core/Rules.hs @@ -339,6 +339,9 @@ withOptHaddock = withOption Opt_Haddock withOption :: GeneralFlag -> ModSummary -> ModSummary withOption opt ms = ms{ms_hspp_opts= gopt_set (ms_hspp_opts ms) opt} +withoutOption :: GeneralFlag -> ModSummary -> ModSummary +withoutOption opt ms = ms{ms_hspp_opts= gopt_unset (ms_hspp_opts ms) opt} + -- | Given some normal parse errors (first) and some from Haddock (second), merge them. -- Ignore Haddock errors that are in both. Demote Haddock-only errors to warnings. mergeParseErrorsHaddock :: [FileDiagnostic] -> [FileDiagnostic] -> [FileDiagnostic] @@ -360,7 +363,7 @@ getParsedModuleWithCommentsRule = defineEarlyCutoff $ \GetParsedModuleWithCommen sess <- use_ GhcSession file opt <- getIdeOptions - let ms' = withOption Opt_KeepRawTokenStream ms + let ms' = withoutOption Opt_Haddock $ withOption Opt_KeepRawTokenStream ms liftIO $ getParsedModuleDefinition (hscEnv sess) opt file ms' diff --git a/ghcide/src/Development/IDE/GHC/Orphans.hs b/ghcide/src/Development/IDE/GHC/Orphans.hs index e9a5e91538..be3c830794 100644 --- a/ghcide/src/Development/IDE/GHC/Orphans.hs +++ b/ghcide/src/Development/IDE/GHC/Orphans.hs @@ -22,6 +22,7 @@ import qualified StringBuffer as SB import Data.Text (Text) import Data.String (IsString(fromString)) import Retrie.ExactPrint (Annotated) +import Data.List (foldl') -- Orphan instances for types from the GHC API. diff --git a/plugins/hls-eval-plugin/hls-eval-plugin.cabal b/plugins/hls-eval-plugin/hls-eval-plugin.cabal index cc20233af8..088115efa7 100644 --- a/plugins/hls-eval-plugin/hls-eval-plugin.cabal +++ b/plugins/hls-eval-plugin/hls-eval-plugin.cabal @@ -31,9 +31,7 @@ library Ide.Plugin.Eval.CodeLens Ide.Plugin.Eval.GHC Ide.Plugin.Eval.Parse.Option - Ide.Plugin.Eval.Parse.Parser - Ide.Plugin.Eval.Parse.Section - Ide.Plugin.Eval.Parse.Token + Ide.Plugin.Eval.Parse.Comments Ide.Plugin.Eval.Types Ide.Plugin.Eval.Util @@ -44,6 +42,7 @@ library , deepseq , Diff , directory + , dlist , extra , filepath , ghc @@ -54,6 +53,8 @@ library , haskell-lsp , haskell-lsp-types , hls-plugin-api + , lens + , megaparsec >= 0.9 , parser-combinators , pretty-simple , QuickCheck @@ -63,6 +64,7 @@ library , text , time , transformers + , mtl , unordered-containers ghc-options: -Wall -Wno-name-shadowing diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs index 91208b71c1..35cc58fc5b 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs @@ -15,20 +15,21 @@ import GhcMonad (Ghc, GhcMonad, liftIO) import Ide.Plugin.Eval.Types ( Language (Plain), Loc, - Located (Located), Section (sectionLanguage), - Test (Example, Property, testOutput), + Test (..), Txt, locate, - locate0, + locate0, Located(..) ) import InteractiveEval (runDecls) import Unsafe.Coerce (unsafeCoerce) +import Control.Lens ((^.)) +import Language.Haskell.LSP.Types.Lens (start, line) -- | Return the ranges of the expression and result parts of the given test -testRanges :: Loc Test -> (Range, Range) -testRanges (Located line tst) = - let startLine = line +testRanges :: Test -> (Range, Range) +testRanges tst = + let startLine = testRange tst ^. start.line (exprLines, resultLines) = testLenghts tst resLine = startLine + exprLines in ( Range @@ -44,7 +45,7 @@ testRanges (Located line tst) = -} -- |The document range where the result of the test is defined -resultRange :: Loc Test -> Range +resultRange :: Test -> Range resultRange = snd . testRanges -- TODO: handle BLANKLINE @@ -66,18 +67,18 @@ testCheck (section, test) out | otherwise = showDiffs $ getDiff (map T.pack $ testOutput test) out testLenghts :: Test -> (Int, Int) -testLenghts (Example e r) = (NE.length e, length r) -testLenghts (Property _ r) = (1, length r) +testLenghts (Example e r _) = (NE.length e, length r) +testLenghts (Property _ r _) = (1, length r) -- |A one-line Haskell statement type Statement = Loc String -asStatements :: Loc Test -> [Statement] -asStatements lt = locate (asStmts <$> lt) +asStatements :: Test -> [Statement] +asStatements lt = locate $ Located (testRange lt ^. start.line) (asStmts lt) asStmts :: Test -> [Txt] -asStmts (Example e _) = NE.toList e -asStmts (Property t _) = +asStmts (Example e _ _) = NE.toList e +asStmts (Property t _ _) = ["prop11 = " ++ t, "(propEvaluation prop11 :: IO String)"] -- |Evaluate an expression (either a pure expression or an IO a) diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs index 1bcb9d593b..29f3d3900b 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs @@ -1,3 +1,4 @@ +{-# LANGUAGE LambdaCase #-} {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveGeneric #-} {-# LANGUAGE DuplicateRecordFields #-} @@ -25,258 +26,255 @@ module Ide.Plugin.Eval.CodeLens ( import Control.Applicative (Alternative ((<|>))) import Control.Arrow (second) import qualified Control.Exception as E -import Control.Monad ( - void, - when, - ) +import Control.Monad + ( void, + when, + ) import Control.Monad.IO.Class (MonadIO (liftIO)) -import Control.Monad.Trans.Except ( - ExceptT (..), - runExceptT, - ) -import Data.Aeson ( - FromJSON, - ToJSON, - toJSON, - ) +import Control.Monad.Trans.Except + ( ExceptT (..), + ) +import Data.Aeson + ( FromJSON, + ToJSON, + toJSON, + ) import Data.Char (isSpace) import Data.Either (isRight) -import qualified Data.HashMap.Strict as Map -import Data.List ( - dropWhileEnd, - find, - ) -import Data.Maybe ( - catMaybes, - fromMaybe, - ) +import qualified Data.HashMap.Strict as HashMap +import Data.List + (dropWhileEnd, + find + ) +import qualified Data.Map.Strict as Map +import Data.Maybe + ( catMaybes, + fromMaybe, + ) import Data.String (IsString) import Data.Text (Text) import qualified Data.Text as T -import Data.Text.Encoding (decodeUtf8) import Data.Time (getCurrentTime) import Data.Typeable (Typeable) -import Development.IDE ( - GetModSummary (..), - GhcSession (..), - HscEnvEq (envImportPaths, hscEnv), - IdeState, - List (List), - NormalizedFilePath, - Range (Range), - Uri, - evalGhcEnv, - hscEnvWithImportPaths, - runAction, - stringBufferToByteString, - textToStringBuffer, - toNormalizedFilePath', - toNormalizedUri, - uriToFilePath', - use_, - ) -import Development.IDE.Core.Preprocessor ( - preprocessor, - ) -import Development.IDE.GHC.Compat (HscEnv) +import Development.IDE + (realSrcSpanToRange, GetModSummary (..), + GetParsedModuleWithComments (..), + GhcSession (..), + HscEnvEq (envImportPaths), + IdeState, + List (List), + NormalizedFilePath, + Range (Range), + Uri, + evalGhcEnv, + fromNormalizedFilePath, + hscEnvWithImportPaths, + runAction, + textToStringBuffer, + toNormalizedFilePath', + toNormalizedUri, + uriToFilePath', + useWithStale_, + use_, + ) +import Development.IDE.GHC.Compat (AnnotationComment(AnnBlockComment, AnnLineComment), GenLocated (L), HscEnv, ParsedModule (..), SrcSpan (RealSrcSpan), srcSpanFile) import DynamicLoading (initializePlugins) -import GHC ( - ExecOptions ( - execLineNumber, - execSourceFile - ), - ExecResult (..), - GeneralFlag (..), - Ghc, - GhcLink (LinkInMemory), - GhcMode (CompManager), - GhcMonad (getSession), - HscTarget (HscInterpreted), - LoadHowMuch (LoadAllTargets), - ModSummary (ms_hspp_opts), - Module (moduleName), - SuccessFlag (Failed, Succeeded), - TcRnExprMode (..), - execOptions, - execStmt, - exprType, - getInteractiveDynFlags, - getSessionDynFlags, - isImport, - isStmt, - load, - runDecls, - setContext, - setInteractiveDynFlags, - setLogAction, - setSessionDynFlags, - setTargets, - typeKind, - ) +import FastString (unpackFS) +import GHC + (ExecOptions + ( execLineNumber, + execSourceFile + ), + ExecResult (..), + GeneralFlag (..), + Ghc, + GhcLink (LinkInMemory), + GhcMode (CompManager), + GhcMonad (getSession), + HscTarget (HscInterpreted), + LoadHowMuch (LoadAllTargets), + ModSummary (ms_hspp_opts), + Module (moduleName), + SuccessFlag (Failed, Succeeded), + TcRnExprMode (..), + execOptions, + execStmt, + exprType, + getInteractiveDynFlags, + getSessionDynFlags, + isImport, + isStmt, + load, + runDecls, + setContext, + setInteractiveDynFlags, + setLogAction, + setSessionDynFlags, + setTargets, + typeKind, + ) import GHC.Generics (Generic) import qualified GHC.LanguageExtensions.Type as LangExt -import GhcPlugins ( - DynFlags (..), - defaultLogActionHPutStrDoc, - gopt_set, - gopt_unset, - interpWays, - targetPlatform, - updateWays, - wayGeneralFlags, - wayUnsetGeneralFlags, - xopt_set, - ) -import HscTypes ( - InteractiveImport (IIModule), - ModSummary (ms_mod), - Target (Target), - TargetId (TargetFile), - ) -import Ide.Plugin.Eval.Code ( - Statement, - asStatements, - evalExpr, - evalExtensions, - evalSetup, - propSetup, - resultRange, - testCheck, - testRanges, - ) -import Ide.Plugin.Eval.GHC ( - addExtension, - addImport, - addPackages, - hasPackage, - isExpr, - showDynFlags, - ) +import GhcPlugins + ( DynFlags (..), + defaultLogActionHPutStrDoc, + gopt_set, + gopt_unset, + interpWays, + targetPlatform, + updateWays, + wayGeneralFlags, + wayUnsetGeneralFlags, + xopt_set, + ) +import HscTypes + ( InteractiveImport (IIModule), + ModSummary (ms_mod), + Target (Target), + TargetId (TargetFile), + ) +import Ide.Plugin.Eval.Code + ( Statement, + asStatements, + evalExpr, + evalExtensions, + evalSetup, + propSetup, + resultRange, + testCheck, + testRanges, + ) +import Ide.Plugin.Eval.GHC + ( addExtension, + addImport, + addPackages, + hasPackage, + isExpr, + showDynFlags, + ) +import Ide.Plugin.Eval.Parse.Comments (commentsToSections) import Ide.Plugin.Eval.Parse.Option (langOptions) -import Ide.Plugin.Eval.Parse.Section ( - Section ( - sectionFormat, - sectionTests - ), - allSections, - ) -import Ide.Plugin.Eval.Parse.Token (tokensFrom) -import Ide.Plugin.Eval.Types ( - Format (SingleLine), - Loc, - Located (Located), - Test, - hasTests, - isProperty, - splitSections, - unLoc, - ) -import Ide.Plugin.Eval.Util ( - asS, - gStrictTry, - handleMaybe, - handleMaybeM, - isLiterate, - logWith, - response, - response', - timed, - ) +import Ide.Plugin.Eval.Types +import Ide.Plugin.Eval.Util + ( asS, + gStrictTry, + handleMaybe, + handleMaybeM, + isLiterate, + logWith, + response, + response', + timed, + ) import Ide.PluginUtils (mkLspCommand) -import Ide.Types ( - CodeLensProvider, - CommandFunction, - CommandId, - PluginCommand (PluginCommand), - ) -import Language.Haskell.LSP.Core ( - LspFuncs ( - getVirtualFileFunc, - withIndefiniteProgress - ), - ProgressCancellable ( - Cancellable - ), - ) -import Language.Haskell.LSP.Types ( - ApplyWorkspaceEditParams ( - ApplyWorkspaceEditParams - ), - CodeLens (CodeLens), - CodeLensParams ( - CodeLensParams, - _textDocument - ), - Command (_arguments, _title), - Position (..), - ServerMethod ( - WorkspaceApplyEdit - ), - TextDocumentIdentifier (..), - TextEdit (TextEdit), - WorkspaceEdit (WorkspaceEdit), - ) +import Ide.Types + ( CodeLensProvider, + CommandFunction, + CommandId, + PluginCommand (PluginCommand), + ) +import Language.Haskell.LSP.Core + ( LspFuncs + ( getVirtualFileFunc, + withIndefiniteProgress + ), + ProgressCancellable + ( Cancellable + ), + ) +import Language.Haskell.LSP.Types + ( ApplyWorkspaceEditParams + ( ApplyWorkspaceEditParams + ), + CodeLens (CodeLens), + CodeLensParams + ( CodeLensParams, + _textDocument + ), + Command (_arguments, _title), + Position (..), + ServerMethod + ( WorkspaceApplyEdit + ), + TextDocumentIdentifier (..), + TextEdit (TextEdit), + WorkspaceEdit (WorkspaceEdit), + ) import Language.Haskell.LSP.VFS (virtualFileText) -import Outputable ( - nest, - ppr, - showSDoc, - text, - ($$), - (<+>), - ) +import Outputable + ( nest, + ppr, + showSDoc, + text, + ($$), + (<+>), + ) import System.FilePath (takeFileName) import System.IO (hClose) import System.IO.Temp (withSystemTempFile) import Text.Read (readMaybe) import Util (OverridingBool (Never)) +import Development.IDE.Core.PositionMapping (toCurrentRange) +import qualified Data.DList as DL +import Control.Lens ((^.)) +import Language.Haskell.LSP.Types.Lens (line, end) {- | Code Lens provider NOTE: Invoked every time the document is modified, not just when the document is saved. -} codeLens :: CodeLensProvider IdeState -codeLens lsp st plId CodeLensParams{_textDocument} = +codeLens _lsp st plId CodeLensParams{_textDocument} = let dbg = logWith st perf = timed dbg in perf "codeLens" $ response $ do let TextDocumentIdentifier uri = _textDocument fp <- handleMaybe "uri" $ uriToFilePath' uri + let nfp = toNormalizedFilePath' fp + isLHS = isLiterate fp dbg "fp" fp - mdlText <- moduleText lsp uri - - {- Normalise CPP/LHS files/custom preprocessed files. - Used to extract tests correctly from CPP and LHS (Bird-style). - -} - session :: HscEnvEq <- - runGetSession st $ toNormalizedFilePath' fp - - Right (ppContent, _dflags) <- - perf "preprocessor" $ - liftIO $ - runExceptT $ - preprocessor (hscEnv session) fp (Just $ textToStringBuffer mdlText) - let text = - cleanSource (isLiterate fp) . decodeUtf8 $ - stringBufferToByteString - ppContent - -- dbg "PREPROCESSED CONTENT" text + (ParsedModule{..}, posMap) <- liftIO $ + runAction "parsed" st $ useWithStale_ GetParsedModuleWithComments nfp + let comments = foldMap + ( foldMap $ \case + L (RealSrcSpan real) bdy + | unpackFS (srcSpanFile real) == + fromNormalizedFilePath nfp + , let ran0 = realSrcSpanToRange real + , Just curRan <- toCurrentRange posMap ran0 + -> + + -- since Haddock parsing is unset explicitly in 'getParsedModuleWithComments', + -- we can concentrate on these two + case bdy of + AnnLineComment cmt -> + mempty { lineComments = Map.singleton curRan (RawLineComment cmt) } + AnnBlockComment cmt -> + mempty { blockComments = Map.singleton curRan $ RawBlockComment cmt } + _ -> mempty + _ -> mempty + ) + $ snd pm_annotations + dbg "excluded comments" $ show $ DL.toList $ + foldMap + (foldMap $ \(L a b) -> + case b of + AnnLineComment{} -> mempty + AnnBlockComment{} -> mempty + _ -> DL.singleton (a, b) + ) + $ snd pm_annotations + dbg "comments" $ show comments -- Extract tests from source code - let Right (setups, nonSetups) = - (splitSections . filter hasTests <$>) - . allSections - . tokensFrom - . T.unpack - $ text - let tests = testsBySection nonSetups - + let Sections{..} = commentsToSections isLHS comments + tests = testsBySection nonSetupSections cmd <- liftIO $ mkLspCommand plId evalCommandName "Evaluate=..." (Just []) let lenses = [ CodeLens testRange (Just cmd') Nothing | (section, test) <- tests , let (testRange, resultRange) = testRanges test - args = EvalParams (setups ++ [section]) _textDocument + args = EvalParams (setupSections ++ [section]) _textDocument cmd' = (cmd :: Command) { _arguments = Just (List [toJSON args]) @@ -292,9 +290,9 @@ codeLens lsp st plId CodeLensParams{_textDocument} = unwords [ show (length tests) , "tests in" - , show (length nonSetups) + , show (length nonSetupSections) , "sections" - , show (length setups) + , show (length setupSections) , "setups" , show (length lenses) , "lenses." @@ -310,7 +308,7 @@ evalCommandName = "evalCommand" evalCommand :: PluginCommand IdeState evalCommand = PluginCommand evalCommandName "evaluate" runEvalCmd --- |Specify the test section to execute +-- | Specify the test section to execute data EvalParams = EvalParams { sections :: [Section] , module_ :: !TextDocumentIdentifier @@ -415,7 +413,7 @@ runEvalCmd lsp st EvalParams{..} = (st, fp) tests - let workspaceEditsMap = Map.fromList [(_uri, List $ addFinalReturn mdlText edits)] + let workspaceEditsMap = HashMap.fromList [(_uri, List $ addFinalReturn mdlText edits)] let workspaceEdits = WorkspaceEdit (Just workspaceEditsMap) Nothing return (WorkspaceApplyEdit, ApplyWorkspaceEditParams workspaceEdits) @@ -423,25 +421,6 @@ runEvalCmd lsp st EvalParams{..} = withIndefiniteProgress lsp "Evaluating" Cancellable $ response' cmd -{- ->>> import Language.Haskell.LSP.Types(applyTextEdit) ->>> aTest s = let Right [sec] = allSections (tokensFrom s) in head. sectionTests $ sec ->>> mdl = "module Test where\n-- >>> 2+2" - -To avoid https://github.com/haskell/haskell-language-server/issues/1213, `addFinalReturn` adds, if necessary, a final empty line to the document before inserting the tests' results. - ->>> let [e1,e2] = addFinalReturn mdl [asEdit (aTest mdl) ["4"]] in applyTextEdit e2 (applyTextEdit e1 mdl) -"module Test where\n-- >>> 2+2\n4\n" - ->>> applyTextEdit (head $ addFinalReturn mdl [asEdit (aTest mdl) ["4"]]) mdl -"module Test where\n-- >>> 2+2\n" - ->>> addFinalReturn mdl [asEdit (aTest mdl) ["4"]] -[TextEdit {_range = Range {_start = Position {_line = 1, _character = 10}, _end = Position {_line = 1, _character = 10}}, _newText = "\n"},TextEdit {_range = Range {_start = Position {_line = 2, _character = 0}, _end = Position {_line = 2, _character = 0}}, _newText = "4\n"}] - ->>> asEdit (aTest mdl) ["4"] -TextEdit {_range = Range {_start = Position {_line = 2, _character = 0}, _end = Position {_line = 2, _character = 0}}, _newText = "4\n"} --} addFinalReturn :: Text -> [TextEdit] -> [TextEdit] addFinalReturn mdlText edits | not (null edits) && not (T.null mdlText) && T.last mdlText /= '\n' = @@ -465,13 +444,13 @@ moduleText lsp uri = lsp (toNormalizedUri uri) -testsBySection :: [Section] -> [(Section, Loc Test)] +testsBySection :: [Section] -> [(Section, Test)] testsBySection sections = [(section, test) | section <- sections, test <- sectionTests section] type TEnv = (IdeState, String) -runTests :: TEnv -> [(Section, Loc Test)] -> Ghc [TextEdit] +runTests :: TEnv -> [(Section, Test)] -> Ghc [TextEdit] runTests e@(_st, _) tests = do df <- getInteractiveDynFlags evalSetup @@ -479,7 +458,7 @@ runTests e@(_st, _) tests = do mapM (processTest e df) tests where - processTest :: TEnv -> DynFlags -> (Section, Loc Test) -> Ghc TextEdit + processTest :: TEnv -> DynFlags -> (Section, Test) -> Ghc TextEdit processTest e@(st, fp) df (section, test) = do let dbg = logWith st let pad = pad_ $ (if isLiterate fp then ("> " `T.append`) else id) $ padPrefix (sectionFormat section) @@ -487,22 +466,33 @@ runTests e@(_st, _) tests = do rs <- runTest e df test dbg "TEST RESULTS" rs - let checkedResult = testCheck (section, unLoc test) rs + let checkedResult = testCheck (section, test) rs - let edit = asEdit test (map pad checkedResult) + let edit = asEdit (sectionFormat section) test (map pad checkedResult) dbg "TEST EDIT" edit return edit -- runTest :: String -> DynFlags -> Loc Test -> Ghc [Text] runTest _ df test - | not (hasQuickCheck df) && (isProperty . unLoc $ test) = + | not (hasQuickCheck df) && isProperty test = return $ singleLine "Add QuickCheck to your cabal dependencies to run this test." runTest e df test = evals e df (asStatements test) -asEdit :: Loc Test -> [Text] -> TextEdit -asEdit test resultLines = TextEdit (resultRange test) (T.unlines resultLines) +asEdit :: Format -> Test -> [Text] -> TextEdit +asEdit (MultiLine commRange) test resultLines + -- A test in a block comment, ending with @-\}@ without newline in-between. + | testRange test ^. end.line == commRange ^. end . line + = + TextEdit + (Range + (testRange test ^. end) + (resultRange test ^. end) + ) + ("\n" <> T.unlines (resultLines <> ["-}"])) +asEdit _ test resultLines = + TextEdit (resultRange test) (T.unlines resultLines) {- The result of evaluating a test line can be: @@ -542,7 +532,7 @@ A, possibly multi line, error is returned for a wrong declaration, directive or Unknown extension: "NonExistent" >>> cls C -Variable not in scope: cls :: t0 -> f0 +Variable not in scope: cls :: t0 -> () Data constructor not in scope: C >>> "A @@ -635,8 +625,8 @@ runGetSession st nfp = -- GhcSessionDeps nfp -needsQuickCheck :: [(Section, Loc Test)] -> Bool -needsQuickCheck = any (isProperty . unLoc . snd) +needsQuickCheck :: [(Section, Test)] -> Bool +needsQuickCheck = any (isProperty . snd) hasQuickCheck :: DynFlags -> Bool hasQuickCheck df = hasPackage df "QuickCheck" @@ -689,27 +679,6 @@ padPrefix :: IsString p => Format -> p padPrefix SingleLine = "-- " padPrefix _ = "" -{- -Normalise preprocessed source code (from a CPP/LHS or other processed file) so that tests are on the same lines as in the original source. - ->>> cleanSource True $ T.pack "#line 1 \nA comment\n> module X where" -"comment\nmodule X where\n" - ->>> cleanSource False $ T.pack "#1 \nmodule X where" -"module X where\n" --} -cleanSource :: Bool -> Text -> Text -cleanSource isLit = - T.unlines - . reverse - . (if isLit then map cleanBirdCode else id) - . takeWhile (\t -> T.null t || (T.head t /= '#')) - . reverse - . T.lines - -cleanBirdCode :: Text -> Text -cleanBirdCode = T.drop 2 - {- | Resulting @Text@ MUST NOT prefix each line with @--@ Such comment-related post-process will be taken place solely in 'evalGhciLikeCmd'. diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Comments.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Comments.hs new file mode 100644 index 0000000000..b77d2b7bc9 --- /dev/null +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Comments.hs @@ -0,0 +1,575 @@ +{-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE OverloadedStrings #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE FlexibleInstances #-} +{-# LANGUAGE RankNTypes #-} +{-# LANGUAGE RecordWildCards #-} +{-# LANGUAGE TemplateHaskell #-} +{-# LANGUAGE TupleSections #-} + +module Ide.Plugin.Eval.Parse.Comments where + +import qualified Control.Applicative.Combinators.NonEmpty as NE +import Control.Arrow (first, (&&&), (>>>)) +import Control.Lens (lensField, lensRules, view, (.~), (^.)) +import Control.Lens.Extras (is) +import Control.Lens.TH (makeLensesWith, makePrisms, mappingNamer) +import Control.Monad (guard, void, when) +import Control.Monad.Combinators () +import Control.Monad.Reader (ask) +import Control.Monad.Trans.Reader (Reader, runReader) +import qualified Data.Char as C +import qualified Data.DList as DL +import qualified Data.Foldable as F +import Data.Function ((&)) +import Data.Functor.Identity +import Data.List.NonEmpty (NonEmpty ((:|))) +import qualified Data.List.NonEmpty as NE +import Data.Map.Strict (Map) +import qualified Data.Map.Strict as Map +import Data.Void (Void) +import Development.IDE (Position, Range (Range)) +import Development.IDE.Types.Location (Position (..)) +import GHC.Generics +import Ide.Plugin.Eval.Types +import Language.Haskell.LSP.Types.Lens + ( character, + end, + line, + start, + ) +import Text.Megaparsec +import qualified Text.Megaparsec as P +import Text.Megaparsec.Char + ( alphaNumChar, + char, + eol, + hspace, + letterChar, + ) +import Data.Functor ((<&>)) +import qualified Data.Text as T + +{- +We build parsers combining the following three kinds of them: + + * Line parser - paring a single line into an input, + works both for line- and block-comments. + A line should be a proper content of lines contained in comment: + doesn't include starting @--@ and @{\-@ and no ending @-\}@ + + * Line comment group parser: parses a contiguous group of + tuples of position and line comment into sections of line comments. + Each input MUST start with @--@. + + * Block comment parser: Parsing entire block comment into sections. + Input must be surrounded by @{\-@ and @-\}@. +-} + +-- | Line parser +type LineParser a = forall m. Monad m => ParsecT Void String m a + +-- | Line comment group parser +type LineGroupParser = Parsec Void [(Range, RawLineComment)] + +data BlockEnv = BlockEnv + { isLhs :: Bool + , blockRange :: Range + } + deriving (Read, Show, Eq, Ord) + +makeLensesWith + (lensRules & lensField .~ mappingNamer (pure . (++ "L"))) + ''BlockEnv + +-- | Block comment parser +type BlockCommentParser = ParsecT Void String (Reader BlockEnv) + +-- | Prop line, with "prop>" stripped off +newtype PropLine = PropLine {getPropLine :: String} + deriving (Show) + +-- | Example line, with @>>>@ stripped off +newtype ExampleLine = ExampleLine {getExampleLine :: String} + deriving (Show) + +data TestComment + = AProp + { testCommentRange :: Range + , lineProp :: PropLine + , propResults :: [String] + } + | AnExample + { testCommentRange :: Range + , lineExamples :: NonEmpty ExampleLine + , exampleResults :: [String] + } + deriving (Show) + +-- | Classification of comments +data CommentFlavour = Vanilla | HaddockNext | HaddockPrev | Named String + deriving (Read, Show, Eq, Ord) + +-- | Single line or block comments? +data CommentStyle = Line | Block Range + deriving (Read, Show, Eq, Ord, Generic) + +makePrisms ''CommentStyle + +commentsToSections :: + -- | True if it is literate Haskell + Bool -> + Comments -> + Sections +commentsToSections isLHS Comments {..} = + let (lineSectionSeeds, lineSetupSeeds) = + foldMap + ( \lcs -> + let theRan = + Range + (view start $ fst $ NE.head lcs) + (view end $ fst $ NE.last lcs) + in case parseMaybe lineGroupP $ NE.toList lcs of + Nothing -> mempty + Just (mls, rs) -> + ( maybe mempty (uncurry Map.singleton) ((theRan,) <$> mls) + , -- orders setup sections in ascending order + if null rs + then mempty + else + Map.singleton theRan $ + DL.singleton (Line, rs) + ) + ) + $ groupLineComments $ + Map.filterWithKey + -- FIXME: + -- To comply with the initial behaviour of + -- Extended Eval Plugin; + -- but it also rejects modules with + -- non-zero base indentation level! + ( \pos _ -> + if isLHS + then pos ^. start . character == 2 + else pos ^. start . character == 0 + ) + lineComments + (blockSeed, blockSetupSeeds) = + foldMap + ( \(ran, lcs) -> + case parseBlockMaybe isLHS ran blockCommentBP $ + getRawBlockComment lcs of + Nothing -> mempty + Just (Named "setup", grp) -> + -- orders setup sections in ascending order + ( mempty + , Map.singleton ran $ + DL.singleton (Block ran, grp) + ) + Just grp -> + ( Map.singleton ran grp + , mempty + ) + ) + -- It seems Extended Eval Plugin doesn't constraint + -- starting indentation level for block comments. + -- Rather, it constrains the indentation level /inside/ + -- block comment body. + $ Map.toList blockComments + lineSections = + lineSectionSeeds <&> uncurry (testsToSection Line) + multilineSections = + Map.mapWithKey + (uncurry . testsToSection . Block) + blockSeed + setupSections = + -- Setups doesn't need Dummy position + map + ( \(style, tests) -> + testsToSection + style + (Named "setup") + tests + ) + $ DL.toList $ + F.fold $ + Map.unionWith (<>) lineSetupSeeds blockSetupSeeds + nonSetupSections = F.toList $ lineSections `Map.union` multilineSections + in Sections {..} + +parseBlockMaybe :: Bool -> Range -> BlockCommentParser a -> String -> Maybe a +parseBlockMaybe isLhs blockRange p i = + case runReader (runParserT p' "" i) BlockEnv {..} of + Left {} -> Nothing + Right a -> Just a + where + p' = do + updateParserState $ \st -> + st + { statePosState = + (statePosState st) + { pstateSourcePos = positionToSourcePos $ blockRange ^. start + } + } + p + +type CommentRange = Range + +type SectionRange = Range + +testsToSection :: + CommentStyle -> + CommentFlavour -> + [TestComment] -> + Section +testsToSection style flav tests = + let sectionName + | Named name <- flav = name + | otherwise = "" + sectionLanguage = case flav of + HaddockNext -> Haddock + HaddockPrev -> Haddock + _ -> Plain + sectionTests = map fromTestComment tests + sectionFormat = + case style of + Line -> SingleLine + Block ran -> MultiLine ran + in Section {..} + +fromTestComment :: TestComment -> Test +fromTestComment AProp {..} = + Property + { testline = getPropLine lineProp + , testOutput = propResults + , testRange = testCommentRange + } +fromTestComment AnExample {..} = + Example + { testLines = getExampleLine <$> lineExamples + , testOutput = exampleResults + , testRange = testCommentRange + } + +-- * Block comment parser + +{- $setup +>>> dummyPos = Position 0 0 +>>> parseE p = either (error . errorBundlePretty) id . parse p "" +-} + +-- >>> parseE (blockCommentBP True dummyPos) "{- |\n >>> 5+5\n 11\n -}" +-- (HaddockNext,[AnExample {testCommentRange = Position {_line = 1, _character = 0}, lineExamples = ExampleLine {getExampleLine = " 5+5"} :| [], exampleResults = [" 11"]}]) + +blockCommentBP :: + -- | True if Literate Haskell + BlockCommentParser (CommentFlavour, [TestComment]) +blockCommentBP = do + skipCount 2 anySingle -- "{-" + void $ optional $ char ' ' + flav <- commentFlavourP + hit <- skipNormalCommentBlock + if hit + then do + body <- + many $ + (blockExamples <|> blockProp) + <* skipNormalCommentBlock + void takeRest -- just consume the rest + pure (flav, body) + else pure (flav, []) + +skipNormalCommentBlock :: BlockCommentParser Bool +skipNormalCommentBlock = do + BlockEnv {..} <- ask + skipManyTill (normalLineP isLhs $ Block blockRange) $ + False <$ try (optional (chunk "-}") *> eof) + <|> True <$ lookAhead (try $ testSymbol isLhs $ Block blockRange) + +testSymbol :: Bool -> CommentStyle -> LineParser () +testSymbol isLHS style = + -- FIXME: To comply with existing Extended Eval Plugin Behaviour; + -- it must skip one space after a comment! + -- This prevents Eval Plugin from working on + -- modules with non-standard base indentation-level. + when (isLHS && is _Block style) (void $ count' 0 2 $ char ' ') + *> (exampleSymbol <|> propSymbol) + +eob :: LineParser () +eob = eof <|> try (optional (chunk "-}") *> eof) <|> void eol + +blockExamples + , blockProp :: + BlockCommentParser TestComment +blockExamples = do + BlockEnv {..} <- ask + (ran, examples) <- withRange $ NE.some $ exampleLineStrP isLhs $ Block blockRange + AnExample ran examples <$> resultBlockP +blockProp = do + BlockEnv {..} <- ask + (ran, Identity prop) <- withRange $ fmap Identity $ propLineStrP isLhs $ Block blockRange + AProp ran prop <$> resultBlockP + +withRange :: + (TraversableStream s, Stream s, Monad m, Ord v, Traversable t) => + ParsecT v s m (t (a, Position)) -> + ParsecT v s m (Range, t a) +withRange p = do + beg <- sourcePosToPosition <$> getSourcePos + as <- p + let fin + | null as = beg + | otherwise = snd $ last $ F.toList as + pure (Range beg fin, fst <$> as) + +resultBlockP :: BlockCommentParser [String] +resultBlockP = do + BlockEnv {..} <- ask + many $ + fmap fst . nonEmptyNormalLineP isLhs $ + Block blockRange + +positionToSourcePos :: Position -> SourcePos +positionToSourcePos pos = + P.SourcePos + { sourceName = "" + , sourceLine = P.mkPos $ 1 + pos ^. line + , sourceColumn = P.mkPos $ 1 + pos ^. character + } + +sourcePosToPosition :: SourcePos -> Position +sourcePosToPosition SourcePos {..} = + Position (unPos sourceLine - 1) (unPos sourceColumn - 1) + +-- * Line Group Parser + +{- | +Result: a tuple of ordinary line tests and setting sections. + +TODO: Haddock comment can adjacent to vanilla comment: + + @ + -- Vanilla comment + -- Another vanilla + -- | This parses as Haddock comment as GHC + @ + +This behaviour is not yet handled correctly in Eval Plugin; +but for future extension for this, we use a tuple here instead of 'Either'. +-} +lineGroupP :: + LineGroupParser + (Maybe (CommentFlavour, [TestComment]), [TestComment]) +lineGroupP = do + (_, flav) <- lookAhead $ parseLine (commentFlavourP <* takeRest) + case flav of + Named "setup" -> (Nothing,) <$> lineCommentSectionsP + flav -> (,mempty) . Just . (flav,) <$> lineCommentSectionsP + +-- >>> parse (lineGroupP <*eof) "" $ (dummyPosition, ) . RawLineComment <$> ["-- a", "-- b"] +-- Variable not in scope: dummyPosition :: Position + +commentFlavourP :: LineParser CommentFlavour +commentFlavourP = + P.option + Vanilla + ( HaddockNext <$ char '|' + <|> HaddockPrev <$ char '^' + <|> Named <$ char '$' + <* optional hspace + <*> ((:) <$> letterChar <*> P.many alphaNumChar) + ) + <* optional (char ' ') + +lineCommentHeadP :: LineParser () +lineCommentHeadP = do + -- and no operator symbol character follows. + void $ chunk "--" + skipMany $ char '-' + void $ optional $ char ' ' + +lineCommentSectionsP :: + LineGroupParser [TestComment] +lineCommentSectionsP = do + skipMany normalLineCommentP + many $ + exampleLinesGP + <|> uncurry AProp <$> propLineGP <*> resultLinesP + <* skipMany normalLineCommentP + +lexemeLine :: LineGroupParser a -> LineGroupParser a +lexemeLine p = p <* skipMany normalLineCommentP + +resultLinesP :: LineGroupParser [String] +resultLinesP = many nonEmptyLGP + +normalLineCommentP :: LineGroupParser (Range, String) +normalLineCommentP = + parseLine (fst <$ commentFlavourP <*> normalLineP False Line) + +nonEmptyLGP :: LineGroupParser String +nonEmptyLGP = + try $ + fmap snd $ + parseLine $ + fst <$ commentFlavourP <*> nonEmptyNormalLineP False Line + +exampleLinesGP :: LineGroupParser TestComment +exampleLinesGP = + lexemeLine $ + uncurry AnExample . first convexHullRange . NE.unzip + <$> NE.some exampleLineGP + <*> resultLinesP + +convexHullRange :: NonEmpty Range -> Range +convexHullRange nes = + Range (NE.head nes ^. start) (NE.last nes ^. end) + +exampleLineGP :: LineGroupParser (Range, ExampleLine) +exampleLineGP = + -- In line-comments, indentation-level inside comment doesn't matter. + parseLine (fst <$ commentFlavourP <*> exampleLineStrP False Line) + +propLineGP :: LineGroupParser (Range, PropLine) +propLineGP = + -- In line-comments, indentation-level inside comment doesn't matter. + parseLine (fst <$ commentFlavourP <*> propLineStrP False Line) + +{- | +Turning a line parser into line group parser consuming a single line comment. +Parses a sinlge line comment, skipping prefix "--[-*]" with optional one horizontal space. +fails if the input does not start with "--". + +__N.B.__ We don't strip comment flavours. + +>>> pck = (:[]).(:[]) . RawLineComment + +>>> parseMaybe (parseLine $ takeRest) $ pck "-- >>> A" +Just [">>> A"] + +>>> parseMaybe (parseLine $ takeRest) $ pck "--- >>> A" +Just [" >>> A"] + +>>> parseMaybe (parseLine takeRest) $ pck "" +Nothing +-} +parseLine :: + (Ord (f RawLineComment), Traversable f) => + LineParser a -> + Parsec Void [f RawLineComment] (f a) +parseLine p = + P.token + (mapM $ parseMaybe (lineCommentHeadP *> p) . getRawLineComment) + mempty + +-- * Line Parsers + +-- | Non-empty normal line. +nonEmptyNormalLineP :: + -- | True if Literate Haskell + Bool -> + CommentStyle -> + LineParser (String, Position) +nonEmptyNormalLineP isLHS style = try $ do + (ln, pos) <- normalLineP isLHS style + guard $ + case style of + Block{} -> T.strip (T.pack ln) `notElem` ["{-", "-}", ""] + _ -> not $ all C.isSpace ln + pure (ln, pos) + +{- | Normal line is a line neither a example nor prop. + Empty line is normal. +-} +normalLineP :: + -- | True if Literate Haskell + Bool -> + CommentStyle -> + LineParser (String, Position) +normalLineP isLHS style = do + notFollowedBy + (try $ testSymbol isLHS style) + when (isLHS && is _Block style) $ + void $ count' 0 2 $ char ' ' + consume style + +consume :: CommentStyle -> LineParser (String, Position) +consume style = + case style of + Line -> (,) <$> takeRest <*> getPosition + Block {} -> manyTill_ anySingle (getPosition <* eob) + +getPosition :: (Ord v, TraversableStream s) => ParsecT v s m Position +getPosition = sourcePosToPosition <$> getSourcePos + +-- | Parses example test line. +exampleLineStrP :: + -- | True if Literate Haskell + Bool -> + CommentStyle -> + LineParser (ExampleLine, Position) +exampleLineStrP isLHS style = + try $ + -- FIXME: To comply with existing Extended Eval Plugin Behaviour; + -- it must skip one space after a comment! + -- This prevents Eval Plugin from working on + -- modules with non-standard base indentation-level. + when (isLHS && is _Block style) (void $ count' 0 2 $ char ' ') + *> exampleSymbol + *> (first ExampleLine <$> consume style) + +exampleSymbol :: LineParser () +exampleSymbol = + chunk ">>>" *> P.notFollowedBy (char '>') + +propSymbol :: LineParser () +propSymbol = chunk "prop>" *> P.notFollowedBy (char '>') + +-- | Parses prop test line. +propLineStrP :: + -- | True if Literate HAskell + Bool -> + CommentStyle -> + LineParser (PropLine, Position) +propLineStrP isLHS style = + -- FIXME: To comply with existing Extended Eval Plugin Behaviour; + -- it must skip one space after a comment! + -- This prevents Eval Plugin from working on + -- modules with non-standard base indentation-level. + when (isLHS && is _Block style) (void $ count' 0 2 $ char ' ') + *> chunk "prop>" + *> P.notFollowedBy (char '>') + *> (first PropLine <$> consume style) + +-- * Utilities + +{- | +Given a sequence of tokens increasing in their starting position, +groups them into sublists consisting of contiguous tokens; +Two adjacent tokens are considered to be contiguous if + + * line number increases by 1, and + * they have same starting column. + +>>> contiguousGroupOn id [(1,2),(2,2),(3,4),(4,4),(5,4),(7,0),(8,0)] +[(1,2) :| [(2,2)],(3,4) :| [(4,4),(5,4)],(7,0) :| [(8,0)]] +-} +contiguousGroupOn :: (a -> (Int, Int)) -> [a] -> [NonEmpty a] +contiguousGroupOn toLineCol = foldr step [] + where + step a [] = [pure a] + step a bss0@((b :| bs) : bss) + | let (aLine, aCol) = toLineCol a + , let (bLine, bCol) = toLineCol b + , aLine + 1 == bLine && aCol == bCol = + (a :| b : bs) : bss + | otherwise = pure a : bss0 + +{- | Given a map from positions, divides them into subgroup + with contiguous line and columns. +-} +groupLineComments :: + Map Range a -> [NonEmpty (Range, a)] +groupLineComments = + contiguousGroupOn (fst >>> view start >>> view line &&& view character) + . Map.toList diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Option.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Option.hs index ad75bb4c39..a76dfdeb22 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Option.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Option.hs @@ -6,13 +6,10 @@ module Ide.Plugin.Eval.Parse.Option ( ) where import Control.Monad.Combinators (many) -import Ide.Plugin.Eval.Parse.Parser ( - Parser, - letterChar, - runParser, - space, - string, - ) +import Text.Megaparsec.Char +import Text.Megaparsec +import Data.Void (Void) +import Control.Arrow (left) {- | >>> langOptions ":set -XBinaryLiterals -XOverloadedStrings " @@ -24,10 +21,13 @@ Right [] >>> langOptions "" Left "No match" -} -langOptions :: [Char] -> Either String [[Char]] -langOptions = runParser (many space *> languageOpts <* many space) +langOptions :: String -> Either String [String] +langOptions = + left errorBundlePretty + . parse (space *> languageOpts <* eof) "" --- >>> runParser languageOpts ":set -XBinaryLiterals -XOverloadedStrings" --- Right ["BinaryLiterals","OverloadedStrings"] -languageOpts :: Parser Char [[Char]] -languageOpts = string ":set" *> many (many space *> string "-X" *> many letterChar) +-- >>> parseMaybe languageOpts ":set -XBinaryLiterals -XOverloadedStrings" +-- Just ["BinaryLiterals","OverloadedStrings"] +languageOpts :: Parsec Void String [String] +languageOpts = string ":set" *> space1 + *> many (string "-X" *> many letterChar <* space) diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Parser.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Parser.hs deleted file mode 100644 index a3d533d4e9..0000000000 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Parser.hs +++ /dev/null @@ -1,98 +0,0 @@ -{-# LANGUAGE DeriveFunctor #-} - --- |Simple List Parser, used for both line and test parsing. -module Ide.Plugin.Eval.Parse.Parser ( - Parser, - runParser, - satisfy, - alphaNumChar, - letterChar, - space, - string, - char, - tillEnd, -) where - -import Control.Applicative (Alternative) -import Control.Monad (MonadPlus, (>=>)) -import Control.Monad.Combinators ( - empty, - (<|>), - ) -import Data.Char ( - isAlphaNum, - isLetter, - ) -import Data.List (isPrefixOf) - -type CharParser = Parser Char - -{- $setup - >>> import Control.Monad.Combinators --} - -{- | ->>> runParser (string "aa" <|> string "bb") "bb" -Right "bb" - ->>> runParser (some (string "aa")) "aaaaaa" -Right ["aa","aa","aa"] --} -string :: String -> CharParser String -string t = Parser $ - \s -> if t `isPrefixOf` s then Just (t, drop (length t) s) else Nothing - -letterChar :: Parser Char Char -letterChar = satisfy isLetter - -alphaNumChar :: Parser Char Char -alphaNumChar = satisfy isAlphaNum - -space :: Parser Char Char -space = char ' ' - -{- | - >>> runParser (some $ char 'a') "aa" - Right "aa" --} -char :: Char -> CharParser Char -char ch = satisfy (== ch) - -{- | ->>> runParser tillEnd "abc\ndef" -Right "abc\ndef" --} -tillEnd :: Parser t [t] -tillEnd = Parser $ \s -> Just (s, []) - -satisfy :: (t -> Bool) -> Parser t t -satisfy f = Parser sel - where - sel [] = Nothing - sel (t : ts) - | f t = Just (t, ts) - | otherwise = Nothing - -newtype Parser t a = Parser {parse :: [t] -> Maybe (a, [t])} deriving (Functor) - -instance Applicative (Parser t) where - pure a = Parser (\s -> Just (a, s)) - (Parser p1) <*> (Parser p2) = - Parser (p1 >=> (\(f, s1) -> p2 s1 >>= \(a, s2) -> return (f a, s2))) - -instance Alternative (Parser t) where - empty = Parser (const Nothing) - p <|> q = Parser $ \s -> parse p s <|> parse q s - -instance Monad (Parser t) where - return = pure - (>>=) f g = Parser (parse f >=> (\(a, s') -> parse (g a) s')) - -instance MonadPlus (Parser t) - -runParser :: Show t => Parser t a -> [t] -> Either String a -runParser m s = case parse m s of - Just (res, []) -> Right res - Just (_, ts) -> - Left $ "Parser did not consume entire stream, left: " ++ show ts - Nothing -> Left "No match" diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Section.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Section.hs deleted file mode 100644 index 403f393d22..0000000000 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Section.hs +++ /dev/null @@ -1,141 +0,0 @@ -{-# LANGUAGE RecordWildCards #-} -{-# LANGUAGE ViewPatterns #-} -{-# LANGUAGE NoMonomorphismRestriction #-} -{-# OPTIONS_GHC -Wwarn #-} -{-# OPTIONS_GHC -fno-warn-unused-do-bind #-} - --- |Parse a Section, a group of zero or more tests defined in a multiline comment or a sequence of one line comments. -module Ide.Plugin.Eval.Parse.Section ( - allSections, - validSections, - Section (..), -) where - -import qualified Control.Applicative.Combinators.NonEmpty as NE -import Control.Monad.Combinators ( - many, - optional, - some, - (<|>), - ) -import qualified Data.List.NonEmpty as NE -import Data.Maybe (catMaybes, fromMaybe) -import Ide.Plugin.Eval.Parse.Parser ( - Parser, - runParser, - satisfy, - ) -import Ide.Plugin.Eval.Parse.Token ( - Token (BlockOpen, blockFormat, blockLanguage, blockName), - TokenS, - isBlockClose, - isBlockOpen, - isCodeLine, - isPropLine, - isStatement, - isTextLine, - unsafeContent, - ) -import Ide.Plugin.Eval.Types ( - Format (SingleLine), - Loc, - Located (Located, located, location), - Section (..), - Test (Example, Property), - hasTests, - unLoc, - ) - -type Tk = Loc TokenS - -validSections :: [Tk] -> Either String [Section] -validSections = (filter hasTests <$>) . allSections - -allSections :: [Tk] -> Either String [Section] -allSections = runParser sections - -{- ->>> import Ide.Plugin.Eval.Parse.Token ->>> import System.IO.Extra(readFileUTF8') ->>> testSource_ = runParser sections . tokensFrom ->>> testSource fp = testSource_ <$> readFileUTF8' fp - ->>> testSource "plugins/default/src/Ide/Plugin/Eval/Test/TestGHC.hs" -Right [Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [Located {location = 36, located = Property {testline = " \\(l::[Bool]) -> reverse (reverse l) == l", testOutput = []}}], sectionLanguage = Plain, sectionFormat = MultiLine},Section {sectionName = "", sectionTests = [Located {location = 40, located = Example {testLines = " :set -XScopedTypeVariables -XExplicitForAll" :| [" import qualified Test.QuickCheck as Q11"," runProp11 p = Q11.quickCheckWithResult Q11.stdArgs p >>= return . Q11.output"," prop11 = \\(l::[Int]) -> reverse (reverse l) == l"," runProp11 prop11"], testOutput = []}},Located {location = 46, located = Property {testline = " \\(l::[Int]) -> reverse (reverse l) == l", testOutput = []}}], sectionLanguage = Plain, sectionFormat = MultiLine},Section {sectionName = "", sectionTests = [Located {location = 50, located = Example {testLines = " t" :| [], testOutput = []}}], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [Located {location = 55, located = Example {testLines = " run $ runEval \"3+2\"" :| [], testOutput = []}}], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [Located {location = 125, located = Example {testLines = " isStmt \"\"" :| [], testOutput = ["stmt = let x =33;print x"]}}], sectionLanguage = Haddock, sectionFormat = MultiLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = MultiLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine},Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = SingleLine}] - ->>> testSource "test/testdata/eval/T11.hs" -Right [Section {sectionName = "", sectionTests = [Located {location = 2, located = Example {testLines = " :kind! a" :| [], testOutput = []}}], sectionLanguage = Plain, sectionFormat = SingleLine}] - ->>> testSource "test/testdata/eval/T12.hs" -Right [Section {sectionName = "", sectionTests = [Located {location = 6, located = Example {testLines = " type N = 1" :| [" type M = 40"," :kind N + M + 1"], testOutput = []}}], sectionLanguage = Plain, sectionFormat = SingleLine}] - ->>> testSource_ $ "{"++"-\n -" ++ "}" -Right [Section {sectionName = "", sectionTests = [], sectionLanguage = Plain, sectionFormat = MultiLine}] --} -sections :: Parser Tk [Section] -sections = - catMaybes <$> many (const Nothing <$> some code <|> Just <$> section) - -section :: Parser Tk Section -section = sectionBody >>= sectionEnd - -sectionBody :: Parser Tk Section -sectionBody = - ( \(unLoc -> BlockOpen{..}) ts -> - Section (fromMaybe "" blockName) (catMaybes ts) blockLanguage blockFormat - ) - <$> open <*> many (Just <$> example <|> Just <$> property <|> const Nothing <$> doc) - -sectionEnd :: Section -> Parser Tk Section -sectionEnd s - | sectionFormat s == SingleLine = optional code *> return s - | otherwise = close *> return s - --- section = do --- s <- --- maybe --- (Section "" [] Plain SingleLine) --- ( \(Located _ BlockOpen {..}) -> --- Section (fromMaybe "" blockName) [] blockLanguage blockFormat --- ) --- <$> optional open --- ts <- many (Just <$> example <|> Just <$> property <|> const Nothing <$> doc) --- optional close --- return $ s {sectionTests = catMaybes ts} - --- singleSection :: Parser Tk Section --- singleSection = (\ts -> Section "" (catMaybes ts) Plain SingleLine) <$> tests - --- tests :: Parser Tk [Maybe (Loc Test)] --- tests = some (Just <$> example <|> Just <$> property <|> const Nothing <$> doc) - -doc :: Parser Tk [Tk] -doc = some text - -example, property :: Parser Tk (Loc Test) -property = - ( \(Located l p) rs -> - Located l (Property (unsafeContent p) (unsafeContent . located <$> rs)) - ) - <$> prop - <*> many nonEmptyText -example = - ( \es rs -> - Located - (location (NE.head es)) - (Example (unsafeContent . located <$> es) (unsafeContent . located <$> rs)) - ) - <$> NE.some statement - <*> many nonEmptyText - -open, close, statement, nonEmptyText, text, prop, code :: Parser Tk Tk -statement = is isStatement -text = is isTextLine -prop = is isPropLine -open = is isBlockOpen -close = is isBlockClose -code = is isCodeLine -nonEmptyText = is (\l -> isTextLine l && not (null (unsafeContent l))) - -is :: (b -> Bool) -> Parser (Loc b) (Loc b) -is p = satisfy (p . unLoc) diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Token.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Token.hs deleted file mode 100644 index 614ae02107..0000000000 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Parse/Token.hs +++ /dev/null @@ -1,283 +0,0 @@ -{-# OPTIONS_GHC -Wwarn #-} - --- | Parse source code into a list of line Tokens. -module Ide.Plugin.Eval.Parse.Token ( - Token(..), - TokenS, - tokensFrom, - unsafeContent, - isStatement, - isTextLine, - isPropLine, - isCodeLine, - isBlockOpen, - isBlockClose -) where - -import Control.Monad.Combinators (many, optional, skipManyTill, - (<|>)) -import Data.Functor (($>)) -import Data.List (foldl') -import Ide.Plugin.Eval.Parse.Parser (Parser, alphaNumChar, char, - letterChar, runParser, satisfy, - space, string, tillEnd) -import Ide.Plugin.Eval.Types (Format (..), Language (..), Loc, - Located (Located)) -import Maybes (fromJust, fromMaybe) - -type TParser = Parser Char (State, [TokenS]) - -data State = InCode | InSingleComment | InMultiComment deriving (Eq, Show) - -commentState :: Bool -> State -commentState True = InMultiComment -commentState False = InSingleComment - -type TokenS = Token String - -data Token s - = -- | Text, without prefix "(--)? >>>" - Statement s - | -- | Text, without prefix "(--)? prop>" - PropLine s - | -- | Text inside a comment - TextLine s - | -- | Line of code (outside comments) - CodeLine - | -- | Open of comment - BlockOpen {blockName :: Maybe s, blockLanguage :: Language, blockFormat :: Format} - | -- | Close of multi-line comment - BlockClose - deriving (Eq, Show) - -isStatement :: Token s -> Bool -isStatement (Statement _) = True -isStatement _ = False - -isTextLine :: Token s -> Bool -isTextLine (TextLine _) = True -isTextLine _ = False - -isPropLine :: Token s -> Bool -isPropLine (PropLine _) = True -isPropLine _ = False - -isCodeLine :: Token s -> Bool -isCodeLine CodeLine = True -isCodeLine _ = False - -isBlockOpen :: Token s -> Bool -isBlockOpen (BlockOpen _ _ _) = True -isBlockOpen _ = False - -isBlockClose :: Token s -> Bool -isBlockClose BlockClose = True -isBlockClose _ = False - -unsafeContent :: Token a -> a -unsafeContent = fromJust . contentOf - -contentOf :: Token a -> Maybe a -contentOf (Statement c) = Just c -contentOf (PropLine c) = Just c -contentOf (TextLine c) = Just c -contentOf _ = Nothing - -{- | Parse source code and return a list of located Tokens ->>> import Ide.Plugin.Eval.Types (unLoc) ->>> tks src = map unLoc . tokensFrom <$> readFile src - ->>> tks "test/testdata/eval/T1.hs" -[CodeLine,CodeLine,CodeLine,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = SingleLine},Statement " unwords example",CodeLine,CodeLine] - ->>> tks "test/testdata/eval/TLanguageOptions.hs" -[BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = SingleLine},TextLine "Support for language options",CodeLine,CodeLine,CodeLine,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = SingleLine},TextLine "Language options set in the module source (ScopedTypeVariables)",TextLine "also apply to tests so this works fine",Statement " f = (\\(c::Char) -> [c])",CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine},TextLine "Multiple options can be set with a single `:set`",TextLine "",Statement " :set -XMultiParamTypeClasses -XFlexibleInstances",Statement " class Z a b c",BlockClose,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine},TextLine "",TextLine "",TextLine "Options apply only in the section where they are defined (unless they are in the setup section), so this will fail:",TextLine "",Statement " class L a b c",BlockClose,CodeLine,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine},TextLine "",TextLine "Options apply to all tests in the same section after their declaration.",TextLine "",TextLine "Not set yet:",TextLine "",Statement " class D",TextLine "",TextLine "Now it works:",TextLine "",Statement ":set -XMultiParamTypeClasses",Statement " class C",TextLine "",TextLine "It still works",TextLine "",Statement " class F",BlockClose,CodeLine,BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine},TextLine "Wrong option names are reported.",Statement " :set -XWrong",BlockClose] - --} -tokensFrom :: String -> [Loc (Token String)] -tokensFrom = tokens . lines . filter (/= '\r') - -{- | ->>> tokens ["-- |$setup >>> 4+7","x=11"] -[Located {location = 0, located = BlockOpen {blockName = Just "setup", blockLanguage = Haddock, blockFormat = SingleLine}},Located {location = 0, located = Statement " 4+7"},Located {location = 1, located = CodeLine}] - ->>> tokens ["-- $start"] -[Located {location = 0, located = BlockOpen {blockName = Just "start", blockLanguage = Plain, blockFormat = SingleLine}},Located {location = 0, located = TextLine ""}] - ->>> tokens ["--","-- >>> 4+7"] -[Located {location = 0, located = BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = SingleLine}},Located {location = 0, located = TextLine ""},Located {location = 1, located = Statement " 4+7"}] - ->>> tokens ["-- |$setup 44","-- >>> 4+7"] -[Located {location = 0, located = BlockOpen {blockName = Just "setup", blockLanguage = Haddock, blockFormat = SingleLine}},Located {location = 0, located = TextLine "44"},Located {location = 1, located = Statement " 4+7"}] - ->>> tokens ["{"++"- |$doc",">>> 2+2","4","prop> x-x==0","--minus","-"++"}"] -[Located {location = 0, located = BlockOpen {blockName = Just "doc", blockLanguage = Haddock, blockFormat = MultiLine}},Located {location = 0, located = TextLine ""},Located {location = 1, located = Statement " 2+2"},Located {location = 2, located = TextLine "4"},Located {location = 3, located = PropLine " x-x==0"},Located {location = 4, located = TextLine "--minus"},Located {location = 5, located = BlockClose}] - -Multi lines, closed on following line: - ->>> tokens ["{"++"-","-"++"}"] -[Located {location = 0, located = BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine}},Located {location = 0, located = TextLine ""},Located {location = 1, located = BlockClose}] - ->>> tokens [" {"++"-","-"++"} "] -[Located {location = 0, located = BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine}},Located {location = 0, located = TextLine ""},Located {location = 1, located = BlockClose}] - ->>> tokens ["{"++"- SOME TEXT "," MORE -"++"}"] -[Located {location = 0, located = BlockOpen {blockName = Nothing, blockLanguage = Plain, blockFormat = MultiLine}},Located {location = 0, located = TextLine "SOME TEXT "},Located {location = 1, located = BlockClose}] - -Multi lines, closed on the same line: - ->>> tokens $ ["{--}"] -[Located {location = 0, located = CodeLine}] - ->>> tokens $ [" {- IGNORED -} "] -[Located {location = 0, located = CodeLine}] - ->>> tokens ["{-# LANGUAGE TupleSections","#-}"] -[Located {location = 0, located = CodeLine},Located {location = 1, located = CodeLine}] - ->>> tokens [] -[] --} -tokens :: [String] -> [Loc TokenS] -tokens = concatMap (\(l, vs) -> map (Located l) vs) . zip [0 ..] . reverse . snd . foldl' next (InCode, []) - where - next (st, tokens) ln = case runParser (aline st) ln of - Right (st', tokens') -> (st', tokens' : tokens) - Left err -> error $ unwords ["Tokens.next failed to parse", ln, err] - --- | Parse a line of input -aline :: State -> TParser -aline InCode = optionStart <|> multi <|> singleOpen <|> codeLine -aline InSingleComment = optionStart <|> multi <|> commentLine False <|> codeLine -aline InMultiComment = multiClose <|> commentLine True - -multi :: TParser -multi = multiOpenClose <|> multiOpen - -codeLine :: TParser -codeLine = (InCode, [CodeLine]) <$ tillEnd - -{- | A multi line comment that starts and ends on the same line. - ->>> runParser multiOpenClose $ concat ["{","--","}"] -Right (InCode,[CodeLine]) - ->>> runParser multiOpenClose $ concat [" {","-| >>> IGNORED -","} "] -Right (InCode,[CodeLine]) --} -multiOpenClose :: TParser -multiOpenClose = (multiStart >> multiClose) $> (InCode, [CodeLine]) - -{-| Parses the opening of a multi line comment. ->>> runParser multiOpen $ "{"++"- $longSection this is also parsed" -Right (InMultiComment,[BlockOpen {blockName = Just "longSection", blockLanguage = Plain, blockFormat = MultiLine},TextLine "this is also parsed"]) - ->>> runParser multiOpen $ "{"++"- $longSection >>> 2+3" -Right (InMultiComment,[BlockOpen {blockName = Just "longSection", blockLanguage = Plain, blockFormat = MultiLine},Statement " 2+3"]) --} -multiOpen :: TParser -multiOpen = - ( \() (maybeLanguage, maybeName) tk -> - (InMultiComment, [BlockOpen maybeName (defLang maybeLanguage) MultiLine, tk]) - ) - <$> multiStart - <*> languageAndName - <*> commentRest - -{- | Parse the first line of a sequence of single line comments ->>> runParser singleOpen "-- |$doc >>>11" -Right (InSingleComment,[BlockOpen {blockName = Just "doc", blockLanguage = Haddock, blockFormat = SingleLine},Statement "11"]) --} -singleOpen :: TParser -singleOpen = - ( \() (maybeLanguage, maybeName) tk -> - (InSingleComment, [BlockOpen maybeName (defLang maybeLanguage) SingleLine, tk]) - ) - <$> singleStart - <*> languageAndName - <*> commentRest - -{- | Parse a line in a comment ->>> runParser (commentLine False) "x=11" -Left "No match" - ->>> runParser (commentLine False) "-- >>>11" -Right (InSingleComment,[Statement "11"]) - ->>> runParser (commentLine True) "-- >>>11" -Right (InMultiComment,[TextLine "-- >>>11"]) --} -commentLine :: Bool -> TParser -commentLine noPrefix = - (\tk -> (commentState noPrefix, [tk])) <$> (optLineStart noPrefix *> commentBody) - -commentRest :: Parser Char (Token [Char]) -commentRest = many space *> commentBody - -commentBody :: Parser Char (Token [Char]) -commentBody = stmt <|> prop <|> txt - where - txt = TextLine <$> tillEnd - stmt = Statement <$> (string ">>>" *> tillEnd) - prop = PropLine <$> (string "prop>" *> tillEnd) - --- | Remove comment line prefix, if needed -optLineStart :: Bool -> Parser Char () -optLineStart noPrefix - | noPrefix = pure () - | otherwise = singleStart - -singleStart :: Parser Char () -singleStart = (string "--" *> optional space) $> () - -multiStart :: Parser Char () -multiStart = sstring "{-" $> () - -{- Parse the close of a multi-line comment ->>> runParser multiClose $ "-"++"}" -Right (InCode,[BlockClose]) - ->>> runParser multiClose $ "-"++"} " -Right (InCode,[BlockClose]) - -As there is currently no way of handling tests in the final line of a multi line comment, it ignores anything that precedes the closing marker: - ->>> runParser multiClose $ "IGNORED -"++"} " -Right (InCode,[BlockClose]) --} -multiClose :: TParser -multiClose = skipManyTill (satisfy (const True)) (string "-}" *> many space) >> return (InCode, [BlockClose]) - -optionStart :: Parser Char (State, [Token s]) -optionStart = (string "{-#" *> tillEnd) $> (InCode, [CodeLine]) - -name :: Parser Char [Char] -name = (:) <$> letterChar <*> many (alphaNumChar <|> char '_') - -sstring :: String -> Parser Char [Char] -sstring s = many space *> string s *> many space - -{- | ->>>runParser languageAndName "|$" -Right (Just Haddock,Just "") - ->>>runParser languageAndName "|$start" -Right (Just Haddock,Just "start") - ->>>runParser languageAndName "| $start" -Right (Just Haddock,Just "start") - ->>>runParser languageAndName "^" -Right (Just Haddock,Nothing) - ->>>runParser languageAndName "$start" -Right (Nothing,Just "start") --} -languageAndName :: Parser Char (Maybe Language, Maybe String) -languageAndName = - (,) <$> optional ((char '|' <|> char '^') >> pure Haddock) - <*> optional - (char '$' *> (fromMaybe "" <$> optional name)) - -defLang :: Maybe Language -> Language -defLang = fromMaybe Plain diff --git a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs index fe370566ec..b02fd8ec18 100644 --- a/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs +++ b/plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Types.hs @@ -1,31 +1,42 @@ {-# LANGUAGE DeriveAnyClass #-} {-# LANGUAGE DeriveFunctor #-} {-# LANGUAGE DeriveGeneric #-} +{-# LANGUAGE DerivingStrategies #-} +{-# LANGUAGE GeneralizedNewtypeDeriving #-} +{-# LANGUAGE UndecidableInstances #-} {-# OPTIONS_GHC -Wwarn #-} -module Ide.Plugin.Eval.Types ( - locate, - locate0, - Test (..), - isProperty, - Format (..), - Language (..), - Section (..), - hasTests, - hasPropertyTest, - splitSections, - Loc, - Located (..), - unLoc, - Txt, -) where +module Ide.Plugin.Eval.Types + ( locate, + locate0, + Test (..), + isProperty, + Format (..), + Language (..), + Section (..), + Sections (..), + hasTests, + hasPropertyTest, + splitSections, + Loc, + Located (..), + Comments (..), + RawBlockComment (..), + RawLineComment (..), + unLoc, + Txt, + ) +where import Control.DeepSeq (NFData (rnf), deepseq) import Data.Aeson (FromJSON, ToJSON) import Data.List (partition) import Data.List.NonEmpty (NonEmpty) +import Data.Map.Strict (Map) import Data.String (IsString (..)) +import Development.IDE (Range) import GHC.Generics (Generic) +import qualified Text.Megaparsec as P -- | A thing with a location attached. data Located l a = Located {location :: l, located :: a} @@ -50,9 +61,15 @@ locate0 = locate . Located 0 type Txt = String +data Sections = Sections + { nonSetupSections :: [Section] + , setupSections :: [Section] + } + deriving (Show, Eq, Generic) + data Section = Section { sectionName :: Txt - , sectionTests :: [Loc Test] + , sectionTests :: [Test] , sectionLanguage :: Language , sectionFormat :: Format } @@ -62,22 +79,61 @@ hasTests :: Section -> Bool hasTests = not . null . sectionTests hasPropertyTest :: Section -> Bool -hasPropertyTest = any (isProperty . unLoc) . sectionTests +hasPropertyTest = any isProperty . sectionTests -- |Split setup and normal sections splitSections :: [Section] -> ([Section], [Section]) splitSections = partition ((== "setup") . sectionName) data Test - = Example {testLines :: NonEmpty Txt, testOutput :: [Txt]} - | Property {testline :: Txt, testOutput :: [Txt]} + = Example {testLines :: NonEmpty Txt, testOutput :: [Txt], testRange :: Range} + | Property {testline :: Txt, testOutput :: [Txt], testRange :: Range} deriving (Eq, Show, Generic, FromJSON, ToJSON, NFData) +data Comments = Comments + { lineComments :: Map Range RawLineComment + , blockComments :: Map Range RawBlockComment + } + deriving (Show, Eq, Ord, Generic) + +newtype RawBlockComment = RawBlockComment {getRawBlockComment :: String} + deriving (Show, Eq, Ord) + deriving newtype + ( IsString + , P.Stream + , P.TraversableStream + , P.VisualStream + , Semigroup + , Monoid + ) + +newtype RawLineComment = RawLineComment {getRawLineComment :: String} + deriving (Show, Eq, Ord) + deriving newtype + ( IsString + , P.Stream + , P.TraversableStream + , P.VisualStream + , Semigroup + , Monoid + ) + +instance Semigroup Comments where + Comments ls bs <> Comments ls' bs' = Comments (ls <> ls') (bs <> bs') + +instance Monoid Comments where + mempty = Comments mempty mempty + isProperty :: Test -> Bool -isProperty (Property _ _) = True +isProperty Property {} = True isProperty _ = False -data Format = SingleLine | MultiLine deriving (Eq, Show, Ord, Generic, FromJSON, ToJSON, NFData) +data Format + = SingleLine + | -- | @Range@ is that of surrounding entire block comment, not section. + -- Used for detecting no-newline test commands. + MultiLine Range + deriving (Eq, Show, Ord, Generic, FromJSON, ToJSON, NFData) data Language = Plain | Haddock deriving (Eq, Show, Generic, Ord, FromJSON, ToJSON, NFData) diff --git a/plugins/hls-eval-plugin/test/Eval.hs b/plugins/hls-eval-plugin/test/Eval.hs index f9218e70f0..4e6b65e2ec 100644 --- a/plugins/hls-eval-plugin/test/Eval.hs +++ b/plugins/hls-eval-plugin/test/Eval.hs @@ -1,4 +1,3 @@ -{-# LANGUAGE CPP #-} {-# LANGUAGE OverloadedStrings #-} {-# LANGUAGE ScopedTypeVariables #-} @@ -37,7 +36,7 @@ import System.FilePath ( (<.>), (), ) -import Test.Hls.Util (hlsCommand) +import Test.Hls.Util (hlsCommand, GhcVersion (GHC84, GHC86), knownBrokenForGhcVersions, knownBrokenInEnv, EnvSpec (HostOS, GhcVer), OS (Windows)) import Test.Tasty ( TestTree, testGroup, @@ -127,6 +126,9 @@ tests = , testCase "Multi line comments" $ goldenTest "TMulti.hs" + , testCase + "Multi line comments, with the last test line ends without newline" + $ goldenTest "TEndingMulti.hs" , testCase "Evaluate expressions in Plain comments in both single line and multi line format" $ goldenTest "TPlainComment.hs" @@ -147,18 +149,28 @@ tests = , testCase "Prelude has no special treatment, it is imported as stated in the module" $ goldenTest "TPrelude.hs" + , testCase + "Don't panic on {-# UNPACK #-} pragma" + $ goldenTest "TUNPACK.hs" + , testCase + "Can handle eval inside nested comment properly" + $ goldenTest "TNested.hs" , testCase "Test on last line insert results correctly" $ do runSession hlsCommand fullCaps evalPath $ liftIO $ do let mdl = "TLastLine.hs" -- Write the test file, to make sure that it has no final line return - writeFile (evalPath mdl) $ "module TLastLine where\n\n-- >>> take 3 [1..]" + writeFile (evalPath mdl) "module TLastLine where\n\n-- >>> take 3 [1..]" goldenTest mdl -#if __GLASGOW_HASKELL__ >= 808 - , testCase "CPP support" $ goldenTest "TCPP.hs" - , testCase "Literate Haskell Bird Style" $ goldenTest "TLHS.lhs" -#endif + , testGroup "with preprocessors" + [ knownBrokenInEnv [HostOS Windows, GhcVer GHC84, GhcVer GHC86] + "CPP eval on Windows and/or GHC <= 8.6 fails for some reasons" $ + testCase "CPP support" $ goldenTest "TCPP.hs" + , knownBrokenForGhcVersions [GHC84, GHC86] + "Preprocessor known to fail on GHC <= 8.6" + $ testCase "Literate Haskell Bird Style" $ goldenTest "TLHS.lhs" -- , testCase "Literate Haskell LaTeX Style" $ goldenTest "TLHSLateX.lhs" + ] ] goldenTest :: FilePath -> IO () @@ -175,8 +187,10 @@ goldenTestBy fltr input = runSession hlsCommand fullCaps evalPath $ do codeLenses <- reverse <$> getCodeLensesBy fltr doc -- liftIO $ print codeLenses - -- Execute sequentially - mapM_ executeCmd $ [c | CodeLens{_command = Just c} <- codeLenses] + -- Execute sequentially, waiting for a moment to + -- avoid mis-insertion due to staled location info. + mapM_ executeCmd + [c | CodeLens{_command = Just c} <- codeLenses] edited <- replaceUnicodeQuotes <$> documentContents doc -- liftIO $ T.putStrLn edited diff --git a/plugins/hls-eval-plugin/test/testdata/TEndingMulti.hs b/plugins/hls-eval-plugin/test/testdata/TEndingMulti.hs new file mode 100644 index 0000000000..81d79f5464 --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TEndingMulti.hs @@ -0,0 +1,6 @@ +module TEndingMulti where + +-- Now trailing doctest is allowed: + +{- >>> 42 +>>> 54-} diff --git a/plugins/hls-eval-plugin/test/testdata/TEndingMulti.hs.expected b/plugins/hls-eval-plugin/test/testdata/TEndingMulti.hs.expected new file mode 100644 index 0000000000..2e73d6d7d1 --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TEndingMulti.hs.expected @@ -0,0 +1,9 @@ +module TEndingMulti where + +-- Now trailing doctest is allowed: + +{- >>> 42 +>>> 54 +42 +54 +-} diff --git a/plugins/hls-eval-plugin/test/testdata/THaddock.hs b/plugins/hls-eval-plugin/test/testdata/THaddock.hs index 1a3614f517..57cc5e70f8 100644 --- a/plugins/hls-eval-plugin/test/testdata/THaddock.hs +++ b/plugins/hls-eval-plugin/test/testdata/THaddock.hs @@ -1,8 +1,8 @@ {- | Tests in plain comments in both single line or multi line format, both forward and backward. Tests are ignored if: - * do not start on the first column - * are in multi line comments that open and close on the same line + * do not start on the first column (in Ordinary Haskell) + * do not start on the first or second column (in Literate Haskell) -} module THaddock () where diff --git a/plugins/hls-eval-plugin/test/testdata/THaddock.hs.expected b/plugins/hls-eval-plugin/test/testdata/THaddock.hs.expected index 22edc0fbaf..c7d0a6b5ef 100644 --- a/plugins/hls-eval-plugin/test/testdata/THaddock.hs.expected +++ b/plugins/hls-eval-plugin/test/testdata/THaddock.hs.expected @@ -1,8 +1,8 @@ {- | Tests in plain comments in both single line or multi line format, both forward and backward. Tests are ignored if: - * do not start on the first column - * are in multi line comments that open and close on the same line + * do not start on the first column (in Ordinary Haskell) + * do not start on the first or second column (in Literate Haskell) -} module THaddock () where diff --git a/plugins/hls-eval-plugin/test/testdata/TLastLine.hs b/plugins/hls-eval-plugin/test/testdata/TLastLine.hs new file mode 100644 index 0000000000..779fb1230a --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TLastLine.hs @@ -0,0 +1,3 @@ +module TLastLine where + +-- >>> take 3 [1..] \ No newline at end of file diff --git a/plugins/hls-eval-plugin/test/testdata/TMulti.hs b/plugins/hls-eval-plugin/test/testdata/TMulti.hs index fb5eb2da80..35ea1f9bd8 100644 --- a/plugins/hls-eval-plugin/test/testdata/TMulti.hs +++ b/plugins/hls-eval-plugin/test/testdata/TMulti.hs @@ -1,6 +1,4 @@ {- | Multi line comments are parsed correctly. - - Note that if they open and close on a single line, their content is ignored. -} module TMulti () where @@ -15,7 +13,7 @@ module TMulti () where -- this should work fine if previous multi comment is parsed correctly -- >>> "a"++"b" - {-| >>> IGNORED -} + {-| >>> "NOT IGNORED" -} -- this should work fine if previous multi comment is parsed correctly -- >>> "a"++"b" diff --git a/plugins/hls-eval-plugin/test/testdata/TMulti.hs.expected b/plugins/hls-eval-plugin/test/testdata/TMulti.hs.expected index 13a8105eae..7c35e06d12 100644 --- a/plugins/hls-eval-plugin/test/testdata/TMulti.hs.expected +++ b/plugins/hls-eval-plugin/test/testdata/TMulti.hs.expected @@ -1,6 +1,4 @@ {- | Multi line comments are parsed correctly. - - Note that if they open and close on a single line, their content is ignored. -} module TMulti () where @@ -18,7 +16,9 @@ module TMulti () where -- >>> "a"++"b" -- "ab" - {-| >>> IGNORED -} + {-| >>> "NOT IGNORED" +"NOT IGNORED" +-} -- this should work fine if previous multi comment is parsed correctly -- >>> "a"++"b" diff --git a/plugins/hls-eval-plugin/test/testdata/TNested.hs b/plugins/hls-eval-plugin/test/testdata/TNested.hs new file mode 100644 index 0000000000..7acd11f5bc --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TNested.hs @@ -0,0 +1,13 @@ +module TNseted () where +{- +>>> 54 +{- +Nested +-} +-} + +{- +{- +>>> 42 +-} +-} diff --git a/plugins/hls-eval-plugin/test/testdata/TNested.hs.expected b/plugins/hls-eval-plugin/test/testdata/TNested.hs.expected new file mode 100644 index 0000000000..86369f01c1 --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TNested.hs.expected @@ -0,0 +1,15 @@ +module TNseted () where +{- +>>> 54 +54 +{- +Nested +-} +-} + +{- +{- +>>> 42 +42 +-} +-} diff --git a/plugins/hls-eval-plugin/test/testdata/TUNPACK.hs b/plugins/hls-eval-plugin/test/testdata/TUNPACK.hs new file mode 100644 index 0000000000..a16a07ef1a --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TUNPACK.hs @@ -0,0 +1,13 @@ +{- | Won't panic on UNPACKs -} +module TUNPACK (THStatus(..)) where + +type ByteString = String +type BSEndo = ByteString -> ByteString +type BSEndoList = [ByteString] -> [ByteString] + +data THStatus = THStatus + {-# UNPACK #-} !Int -- running total byte count + BSEndoList -- previously parsed lines + BSEndo -- bytestrings to be prepended + +-- >>> "Yay! UNPACK pragma didn't do bad things!" diff --git a/plugins/hls-eval-plugin/test/testdata/TUNPACK.hs.expected b/plugins/hls-eval-plugin/test/testdata/TUNPACK.hs.expected new file mode 100644 index 0000000000..407202f20f --- /dev/null +++ b/plugins/hls-eval-plugin/test/testdata/TUNPACK.hs.expected @@ -0,0 +1,14 @@ +{- | Won't panic on UNPACKs -} +module TUNPACK (THStatus(..)) where + +type ByteString = String +type BSEndo = ByteString -> ByteString +type BSEndoList = [ByteString] -> [ByteString] + +data THStatus = THStatus + {-# UNPACK #-} !Int -- running total byte count + BSEndoList -- previously parsed lines + BSEndo -- bytestrings to be prepended + +-- >>> "Yay! UNPACK pragma didn't do bad things!" +-- "Yay! UNPACK pragma didn't do bad things!" diff --git a/plugins/hls-eval-plugin/test/testdata/test.cabal b/plugins/hls-eval-plugin/test/testdata/test.cabal index 2d57505ebe..9a9c5104ab 100644 --- a/plugins/hls-eval-plugin/test/testdata/test.cabal +++ b/plugins/hls-eval-plugin/test/testdata/test.cabal @@ -38,6 +38,8 @@ library T24 T25 T26 + T27 + TEndingMulti TMulti TPlainComment THaddock @@ -50,7 +52,9 @@ library TPrelude TCPP TLHS + TSetup Util + TNested build-depends: base >= 4.7 && < 5, QuickCheck default-language: Haskell2010 diff --git a/stack-8.10.1.yaml b/stack-8.10.1.yaml index 3127af1b43..2bf2956342 100644 --- a/stack-8.10.1.yaml +++ b/stack-8.10.1.yaml @@ -40,6 +40,7 @@ extra-deps: - implicit-hie-cradle-0.3.0.2 - implicit-hie-0.1.2.5 - lsp-test-0.12.0.0 + - megaparsec-9.0.1 - monad-dijkstra-0.1.1.2 - opentelemetry-0.6.1 - opentelemetry-extra-0.6.1 diff --git a/stack-8.6.4.yaml b/stack-8.6.4.yaml index de32db67e0..a85ad83394 100644 --- a/stack-8.6.4.yaml +++ b/stack-8.6.4.yaml @@ -58,6 +58,7 @@ extra-deps: - indexed-profunctors-0.1 - lens-4.18 - lsp-test-0.12.0.0 + - megaparsec-9.0.1 - monad-dijkstra-0.1.1.2 - opentelemetry-0.6.1 - opentelemetry-extra-0.6.1 diff --git a/stack-8.6.5.yaml b/stack-8.6.5.yaml index a34abd3bbd..1dfd1bcfaa 100644 --- a/stack-8.6.5.yaml +++ b/stack-8.6.5.yaml @@ -57,6 +57,7 @@ extra-deps: - indexed-profunctors-0.1 - lens-4.18 - lsp-test-0.12.0.0 + - megaparsec-9.0.1 - monad-dijkstra-0.1.1.2 - opentelemetry-0.6.1 - opentelemetry-extra-0.6.1 diff --git a/stack-8.8.2.yaml b/stack-8.8.2.yaml index b6de81ec9e..393940a5d5 100644 --- a/stack-8.8.2.yaml +++ b/stack-8.8.2.yaml @@ -52,6 +52,7 @@ extra-deps: - implicit-hie-cradle-0.3.0.2 - implicit-hie-0.1.2.5 - lsp-test-0.12.0.0 + - megaparsec-9.0.1 - monad-dijkstra-0.1.1.2 - opentelemetry-0.6.1 - opentelemetry-extra-0.6.1 diff --git a/stack-8.8.3.yaml b/stack-8.8.3.yaml index ed136509ce..3002b7aa4e 100644 --- a/stack-8.8.3.yaml +++ b/stack-8.8.3.yaml @@ -47,6 +47,7 @@ extra-deps: - implicit-hie-cradle-0.3.0.2 - implicit-hie-0.1.2.5 - lsp-test-0.12.0.0 + - megaparsec-9.0.1 - monad-dijkstra-0.1.1.2 - opentelemetry-0.6.1 - opentelemetry-extra-0.6.1 diff --git a/stack-8.8.4.yaml b/stack-8.8.4.yaml index ccc2818031..228636de76 100644 --- a/stack-8.8.4.yaml +++ b/stack-8.8.4.yaml @@ -47,6 +47,7 @@ extra-deps: - implicit-hie-cradle-0.3.0.2 - implicit-hie-0.1.2.5 - lsp-test-0.12.0.0 + - megaparsec-9.0.1 - monad-dijkstra-0.1.1.2 - opentelemetry-0.6.1 - opentelemetry-extra-0.6.1 diff --git a/stack.yaml b/stack.yaml index 2a1c43327b..1ae4a1fd92 100644 --- a/stack.yaml +++ b/stack.yaml @@ -58,6 +58,7 @@ extra-deps: - implicit-hie-0.1.2.5 - indexed-profunctors-0.1 - lens-4.18 + - megaparsec-9.0.1 - lsp-test-0.12.0.0 - monad-dijkstra-0.1.1.2 - opentelemetry-0.6.1 diff --git a/test/utils/Test/Hls/Util.hs b/test/utils/Test/Hls/Util.hs index 066cddae98..2e27cc3cd5 100644 --- a/test/utils/Test/Hls/Util.hs +++ b/test/utils/Test/Hls/Util.hs @@ -13,14 +13,19 @@ module Test.Hls.Util , fromCommand , getHspecFormattedConfig , ghcVersion, GhcVersion(..) + , hostOS, OS(..) + , matchesCurrentEnv, EnvSpec(..) , hlsCommand , hlsCommandExamplePlugin , hlsCommandVomit , ignoreForGhcVersions + , ignoreInEnv , inspectCodeAction , inspectCommand , inspectDiagnostic + , knownBrokenOnWindows , knownBrokenForGhcVersions + , knownBrokenInEnv , logFilePath , setupBuildToolFiles , SymbolLocation @@ -61,6 +66,7 @@ import Test.Tasty.ExpectedFailure (ignoreTestBecause, expectFailBecaus import Test.Tasty.HUnit (Assertion, assertFailure, (@?=)) import Text.Blaze.Renderer.String (renderMarkup) import Text.Blaze.Internal hiding (null) +import System.Info.Extra (isWindows, isMac) codeActionSupportCaps :: C.ClientCapabilities codeActionSupportCaps = def { C._textDocument = Just textDocumentCaps } @@ -116,11 +122,44 @@ ghcVersion = GHC86 ghcVersion = GHC84 #endif +data EnvSpec = HostOS OS | GhcVer GhcVersion + deriving (Show, Eq) + +matchesCurrentEnv :: EnvSpec -> Bool +matchesCurrentEnv (HostOS os) = hostOS == os +matchesCurrentEnv (GhcVer ver) = ghcVersion == ver + +data OS = Windows | MacOS | Linux + deriving (Show, Eq) + +hostOS :: OS +hostOS + | isWindows = Windows + | isMac = MacOS + | otherwise = Linux + +-- | Mark as broken if /any/ of environmental spec mathces the current environment. +knownBrokenInEnv :: [EnvSpec] -> String -> TestTree -> TestTree +knownBrokenInEnv envSpecs reason + | any matchesCurrentEnv envSpecs = expectFailBecause reason + | otherwise = id + +knownBrokenOnWindows :: String -> TestTree -> TestTree +knownBrokenOnWindows reason + | isWindows = expectFailBecause reason + | otherwise = id + knownBrokenForGhcVersions :: [GhcVersion] -> String -> TestTree -> TestTree knownBrokenForGhcVersions vers reason | ghcVersion `elem` vers = expectFailBecause reason | otherwise = id +-- | IgnroeTest if /any/ of environmental spec mathces the current environment. +ignoreInEnv :: [EnvSpec] -> String -> TestTree -> TestTree +ignoreInEnv envSpecs reason + | any matchesCurrentEnv envSpecs = ignoreTestBecause reason + | otherwise = id + ignoreForGhcVersions :: [GhcVersion] -> String -> TestTree -> TestTree ignoreForGhcVersions vers reason | ghcVersion `elem` vers = ignoreTestBecause reason