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 +332/−0
- hls-eval-plugin.cabal +17/−9
- src/Ide/Plugin/Eval.hs +1/−9
- src/Ide/Plugin/Eval/Parse/Token.hs +77/−62
+ 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++++# 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