packages feed

hls-eval-plugin 0.1.0.1 → 0.1.0.2

raw patch · 4 files changed

+427/−80 lines, 4 filesPVP ok

version bump matches the API change (PVP)

API changes (from Hackage documentation)

Files

+ README.md view
@@ -0,0 +1,332 @@+# Eval plugin for the [Haskell Language Server](https://github.com/haskell/haskell-language-server#readme)++The Eval plugin evaluates code inserted in comments.++This is mainly useful to test and document functions and to quickly evaluate small expressions.++Every line of code to be evaluated is introduced by __>>>__++A quick calculation:++```+-- >>> 2**4.5/pi+-- 7.202530529256849+```++A little test for the `double` function:++```+{- |+A doubling function.++>>> double 11+22+-}+double = (2*)+```++# Demo++![Eval](demo.gif)++# Test Structure++A test is composed by a sequence of contiguous lines, the result of their evaluation is inserted after the test body:++```+>>> "AB" ++ "CD"+>>> "CD" ++ "AB"+"ABCD"+"CDAB"+```++You execute a test by clicking on the _Evaluate_ code lens that appears above it (or _Refresh_, if the test has been run previously).++All tests in the same comment block are executed together.+++Tests can appear in all kind of comments:+* plain comments (both single and multi line)+```+{-+>>> "ab" ++ "c"+"abc"+-}++-- >>> "ab" ++ "c"+-- "abc"+```+* Haddock commands (both single and multi line, forward and backward)+```+{-+>>> "ab" ++ "c"+"abc"+-}++-- >>> "ab" ++ "c"+-- "abc"++double a = a + a+-- ^ A doubling function+-- >>> double 11+-- 22+```++Both plain Haskell and Literate Haskell (Bird-style only) source files are supported.++# Test Components++In general, a test is a sequence of:+* imports+* directives+* statements+* expressions+* properties++in no particular order, with every line introduced by __>>>__ (or __prop>__ in the case of properties).++### Imports++```+>>> import Data.List+>>> import GHC.TypeNats+```++From any package in scope but currently NOT from modules in the same source directory.++### Language Extensions++```+>>> :set -XScopedTypeVariables -XStandaloneDeriving -XDataKinds -XTypeOperators -XExplicitNamespaces+```++### Statements and Declarations++Function declarations (optionally introduced by __let__):++```+>>> let tuple x = (x,x)+>>> let one=1;two=2+>>> triple x = (x,x,x)+```++Any other declaration:++```+>>> data TertiumDatur = Truly | Falsely | Other deriving Show+>>> class Display a where display :: a -> String+>>> instance Display TertiumDatur where display = show+```++Definitions are available to following tests in the __same__ comment:++```+{-+>>> two = 2++>>> two+2+-}++-- >>> two+-- Variable not in scope: two+```++If you want definitions to be available to all tests in the module, define a setup section:++```+-- $setup+-- >>> eleven = 11++{-+eleven is now available to any test:++>>> eleven*2+22+-}+```+++### Type and Kind directives++```+>>> :type Truly+Truly :: TertiumDatur++>>> :kind TertiumDatur+TertiumDatur :: *++>>> :type 3+3 :: forall p. Num p => p++>>> :type +d 3+3 :: Integer++>>> type N = 1+>>> type M = 40+>>> :kind! N + M + 1+N + M + 1 :: Nat+= 42+```++### Expressions++```+>>> tuple 2+>>> triple 3+>>> display Other+(2,2)+(3,3,3)+"Other"+```++IO expressions can also be evaluated but their output to stdout/stderr is NOT captured:++```+>>> print "foo"+()+```++### Properties++```+prop> \(l::[Int]) -> reverse (reverse l) == l++++ OK, passed 100 tests.+```++# Haddock vs Plain Comments++There is a conceptual difference between Haddock and plain comments:+* Haddock comments constitute the external module's documentation, they state the contract between the implementor and the module users (API)+* Plain comments are internal documentation meant to explain how the code works (implementation).++This conceptual difference is reflected in the way tests results are refreshed by the Eval plugin.++Say that we have defined a `double` function as:++```+double = (*2)+```++And, in an Haddock comment, we run the test:++```+{- |+>>> double 11+22+-}+```++We then change the definition to:++```+double = (*3)+```++When we refresh the test, its current result is compared with the previous one and differences are displayed (as they change the API):++```+{- |+>>> double 11+WAS 22+NOW 33+-}+```++On the contrary, if the test were into a plain comment, the result would simply be replaced:++```+{-+>>> double 11+33+-}+```++# Multiline Output++By default, the output of every expression is returned as a single line.++This is a problem if you want, for example, to pretty print a value (in this case using the [pretty-simple](https://hackage.haskell.org/package/pretty-simple) package):++```+>>> import Text.Pretty.Simple+>>> pShowNoColor [1..3]+"[ 1\n, 2\n, 3\n]"+```++We could try to print the pretty-print output, but stdout is not captured so we get just a ():++```+>>> print $ pShowNoColor [1..7]+()+```++To display it properly, we can exploit the fact that the output of an error is displayed as a multi-line text:++```+>>> import qualified Data.Text.Lazy as TL+>>> import Text.Pretty.Simple+>>> prettyPrint v = error (TL.unpack $ pShowNoColor v) :: IO String+>>> prettyPrint [1..3]+[ 1+, 2+, 3+]+```++# Differences with doctest++Though the Eval plugin functionality is quite similar to that of [doctest](https://hackage.haskell.org/package/doctest), some doctest's features are not supported.++### Capturing Stdout++Only the value of an IO expression is spliced in, not its output:++```+>>> print "foo"+()+```++### Pattern Matching++The arbitrary content matcher __...__ is unsupported.++### Missing lambda abstractions in property tests++Variables are not automatically introduced:++```+prop> reverse (reverse l) == (l::[Int])+Variable not in scope: l :: [Int]+```++This works:++```+prop> \(l::[Int]) -> reverse (reverse l) == l++++ OK, passed 100 tests.+```++### Multiline Expressions++```+ >>> :{+  let+    x = 1+    y = 2+  in x + y + multiline+ :}+```++# Acknowledgments++Design/features derived from:++* [GHCi](https://downloads.haskell.org/~ghc/latest/docs/html/users_guide/ghci.html)++* [Haddock's](https://www.haskell.org/haddock/doc/html/ch03s08.html#idm140354810775744) Examples and Properties++* [Doctest](https://hackage.haskell.org/package/doctest)++* the REPLoid feature of [Dante](https://github.com/jyp/dante)+
hls-eval-plugin.cabal view
@@ -1,16 +1,24 @@ cabal-version:      2.2 name:               hls-eval-plugin-version:            0.1.0.1+version:            0.1.0.2 synopsis:           Eval plugin for Haskell Language Server-description:     Please see the README on GitHub at <https://github.com/haskell/haskell-language-server#readme>-category: Development-bug-reports:   https://github.com/haskell/haskell-language-server/issues+description:+  Please see the README on GitHub at <https://github.com/haskell/haskell-language-server#readme>++category:           Development+bug-reports:        https://github.com/haskell/haskell-language-server/issues license:            Apache-2.0 license-file:       LICENSE-author: https://github.com/haskell/haskell-language-server/contributors-maintainer: https://github.com/haskell/haskell-language-server/contributors+author:+  https://github.com/haskell/haskell-language-server/contributors++maintainer:+  https://github.com/haskell/haskell-language-server/contributors+ build-type:         Simple-extra-source-files: LICENSE+extra-source-files:+  LICENSE+  README.md  flag pedantic   description: Enable -Werror@@ -37,7 +45,7 @@    build-depends:     , aeson-    , base       >=4.12 && <5+    , base                  >=4.12 && <5     , containers     , deepseq     , Diff@@ -63,7 +71,7 @@     , transformers     , unordered-containers -  ghc-options: -Wall -Wno-name-shadowing+  ghc-options:      -Wall -Wno-name-shadowing    if flag(pedantic)     ghc-options: -Werror
src/Ide/Plugin/Eval.hs view
@@ -3,15 +3,7 @@ {-# OPTIONS_GHC -Wwarn #-}  {- |-A plugin inspired by:--* the REPLoid feature of <https://github.com/jyp/dante Dante>--* <https://www.haskell.org/haddock/doc/html/ch03s08.html#idm140354810775744 Haddock>'s Examples and Properties--* <https://hackage.haskell.org/package/doctest Doctest>--See the "Ide.Plugin.Eval.Tutorial" module for a full introduction to the plugin functionality.+Eval Plugin entry point. -} module Ide.Plugin.Eval (     descriptor,
src/Ide/Plugin/Eval/Parse/Token.hs view
@@ -2,7 +2,7 @@  -- | Parse source code into a list of line Tokens. module Ide.Plugin.Eval.Parse.Token (-    Token(..),+    Token (..),     TokenS,     tokensFrom,     unsafeContent,@@ -11,77 +11,93 @@     isPropLine,     isCodeLine,     isBlockOpen,-    isBlockClose+    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)+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 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)+    = -- | 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+isStatement _ = False  isTextLine :: Token s -> Bool isTextLine (TextLine _) = True-isTextLine _            = False+isTextLine _ = False  isPropLine :: Token s -> Bool isPropLine (PropLine _) = True-isPropLine _            = False+isPropLine _ = False  isCodeLine :: Token s -> Bool isCodeLine CodeLine = True-isCodeLine _        = False+isCodeLine _ = False  isBlockOpen :: Token s -> Bool isBlockOpen (BlockOpen _ _ _) = True-isBlockOpen _                 = False+isBlockOpen _ = False  isBlockClose :: Token s -> Bool isBlockClose BlockClose = True-isBlockClose _          = False+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+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)@@ -92,7 +108,6 @@  >>> 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')@@ -126,13 +141,13 @@  Multi lines, closed on the same line: ->>> tokens $ ["{--}"]+>>> tokens $ ["{","--"++"}"] [Located {location = 0, located = CodeLine}] ->>> tokens $ ["  {- IGNORED -}  "]+>>> tokens $ ["  {","- IGNORED -","}  "] [Located {location = 0, located = CodeLine}] ->>> tokens ["{-# LANGUAGE TupleSections","#-}"]+>>> tokens ["{","-# LANGUAGE TupleSections","#-","}"] [Located {location = 0, located = CodeLine},Located {location = 1, located = CodeLine}]  >>> tokens []@@ -142,14 +157,14 @@ 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]+        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 InCode = optionStart <|> multi <|> singleOpen <|> codeLine aline InSingleComment = optionStart <|> multi <|> commentLine False <|> codeLine-aline InMultiComment  = multiClose <|> commentLine True+aline InMultiComment = multiClose <|> commentLine True  multi :: TParser multi = multiOpenClose <|> multiOpen@@ -168,7 +183,7 @@ multiOpenClose :: TParser multiOpenClose = (multiStart >> multiClose) $> (InCode, [CodeLine]) -{-| Parses the opening of a multi line comment.+{- | 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"]) @@ -177,12 +192,12 @@ -} multiOpen :: TParser multiOpen =-  ( \() (maybeLanguage, maybeName) tk ->-      (InMultiComment, [BlockOpen maybeName (defLang maybeLanguage) MultiLine, tk])-  )-    <$> multiStart-    <*> languageAndName-    <*> commentRest+    ( \() (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"@@ -190,12 +205,12 @@ -} singleOpen :: TParser singleOpen =-  ( \() (maybeLanguage, maybeName) tk ->-      (InSingleComment, [BlockOpen maybeName (defLang maybeLanguage) SingleLine, tk])-  )-    <$> singleStart-    <*> languageAndName-    <*> commentRest+    ( \() (maybeLanguage, maybeName) tk ->+        (InSingleComment, [BlockOpen maybeName (defLang maybeLanguage) SingleLine, tk])+    )+        <$> singleStart+        <*> languageAndName+        <*> commentRest  {- | Parse a line in a comment >>> runParser (commentLine False) "x=11"@@ -209,7 +224,7 @@ -} commentLine :: Bool -> TParser commentLine noPrefix =-  (\tk -> (commentState noPrefix, [tk])) <$> (optLineStart noPrefix *> commentBody)+    (\tk -> (commentState noPrefix, [tk])) <$> (optLineStart noPrefix *> commentBody)  commentRest :: Parser Char (Token [Char]) commentRest = many space *> commentBody@@ -224,8 +239,8 @@ -- | Remove comment line prefix, if needed optLineStart :: Bool -> Parser Char () optLineStart noPrefix-  | noPrefix = pure ()-  | otherwise = singleStart+    | noPrefix = pure ()+    | otherwise = singleStart  singleStart :: Parser Char () singleStart = (string "--" *> optional space) $> ()@@ -275,9 +290,9 @@ -} languageAndName :: Parser Char (Maybe Language, Maybe String) languageAndName =-  (,) <$> optional ((char '|' <|> char '^') >> pure Haddock)-    <*> optional-      (char '$' *> (fromMaybe "" <$> optional name))+    (,) <$> optional ((char '|' <|> char '^') >> pure Haddock)+        <*> optional+            (char '$' *> (fromMaybe "" <$> optional name))  defLang :: Maybe Language -> Language defLang = fromMaybe Plain