Skip to content

Commit f92eb82

Browse files
committed
Eval plugin: add exception configuration
1 parent 0ea3ec6 commit f92eb82

File tree

6 files changed

+92
-30
lines changed

6 files changed

+92
-30
lines changed

plugins/hls-eval-plugin/README.md

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -242,6 +242,11 @@ On the contrary, if the test were into a plain comment, the result would simply
242242
-}
243243
```
244244

245+
If you find this WAS/NOW behaviour does not fit your needs, you can turn it off with toggling the configuration option:
246+
```json
247+
"haskell.plugin.eval.config.diff": false
248+
```
249+
245250
# Multiline Output
246251

247252
By default, the output of every expression is returned as a single line.
@@ -274,6 +279,8 @@ To display it properly, we can exploit the fact that the output of an error is d
274279
]
275280
```
276281

282+
This assumes you did not turn on exception marking (see [Marking exceptions](#marking-exceptions) below).
283+
277284
# Differences with doctest
278285

279286
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.
@@ -287,6 +294,24 @@ Only the value of an IO expression is spliced in, not its output:
287294
()
288295
```
289296

297+
### Marking exceptions
298+
299+
When an exception is thrown it is not prefixed:
300+
301+
```
302+
>>> 1 `div` 0
303+
divide by zero
304+
```
305+
306+
If you want to get the doctest/GHCi behaviour, you can toggle the configuration option:
307+
```json
308+
"haskell.plugin.eval.config.exception": true
309+
```
310+
```
311+
>>> 1 `div` 0
312+
*** Exception: divide by zero
313+
```
314+
290315
### Pattern Matching
291316

292317
The arbitrary content matcher __...__ is unsupported.

plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs

Lines changed: 18 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@ import Ide.Plugin.Eval.Code (Statement, asStatements,
9090
evalSetup, myExecStmt,
9191
propSetup, resultRange,
9292
testCheck, testRanges)
93-
import Ide.Plugin.Eval.Config (getDiffProperty)
93+
import Ide.Plugin.Eval.Config (getEvalConfig, EvalConfig(..))
9494
import Ide.Plugin.Eval.GHC (addImport, addPackages,
9595
hasPackage, showDynFlags)
9696
import Ide.Plugin.Eval.Parse.Comments (commentsToSections)
@@ -292,13 +292,13 @@ runEvalCmd plId st EvalParams{..} =
292292
-- Evaluation takes place 'inside' the module
293293
setContext [Compat.IIModule modName]
294294
Right <$> getSession
295-
diff <- lift $ getDiffProperty plId
295+
evalCfg <- lift $ getEvalConfig plId
296296
edits <-
297297
perf "edits" $
298298
liftIO $
299299
evalGhcEnv hscEnv' $
300300
runTests
301-
diff
301+
evalCfg
302302
(st, fp)
303303
tests
304304

@@ -340,8 +340,8 @@ testsBySection sections =
340340

341341
type TEnv = (IdeState, String)
342342

343-
runTests :: Bool -> TEnv -> [(Section, Test)] -> Ghc [TextEdit]
344-
runTests diff e@(_st, _) tests = do
343+
runTests :: EvalConfig -> TEnv -> [(Section, Test)] -> Ghc [TextEdit]
344+
runTests EvalConfig{..} e@(_st, _) tests = do
345345
df <- getInteractiveDynFlags
346346
evalSetup
347347
when (hasQuickCheck df && needsQuickCheck tests) $ void $ evals True e df propSetup
@@ -356,7 +356,7 @@ runTests diff e@(_st, _) tests = do
356356
rs <- runTest e df test
357357
dbg "TEST RESULTS" rs
358358

359-
let checkedResult = testCheck diff (section, test) rs
359+
let checkedResult = testCheck eval_cfg_diff (section, test) rs
360360

361361
let edit = asEdit (sectionFormat section) test (map pad checkedResult)
362362
dbg "TEST EDIT" edit
@@ -368,7 +368,7 @@ runTests diff e@(_st, _) tests = do
368368
return $
369369
singleLine
370370
"Add QuickCheck to your cabal dependencies to run this test."
371-
runTest e df test = evals (isProperty test) e df (asStatements test)
371+
runTest e df test = evals (eval_cfg_exception && not (isProperty test)) e df (asStatements test)
372372

373373
asEdit :: Format -> Test -> [Text] -> TextEdit
374374
asEdit (MultiLine commRange) test resultLines
@@ -419,29 +419,33 @@ Nothing is returned for an empty line:
419419
A, possibly multi line, error is returned for a wrong declaration, directive or value or an exception thrown by the evaluated code:
420420
421421
>>>:set -XNonExistent
422-
Unknown extension: "NonExistent"
422+
Some flags have not been recognized: -XNonExistent
423423
424424
>>> cls C
425-
Variable not in scope: cls :: t0 -> ()
425+
Variable not in scope: cls :: t0 -> t
426426
Data constructor not in scope: C
427427
428428
>>> "A
429429
lexical error in string/character literal at end of input
430430
431+
Exceptions are shown as if printed, but it can be configured to include prefix like
432+
in GHCi or doctest. This allows it to be used as a hack to simulate print until we
433+
get proper IO support. See #1977
434+
431435
>>> 3 `div` 0
432-
*** Exception: divide by zero
436+
divide by zero
433437
434438
>>> error "Something went wrong\nbad times" :: E.SomeException
435-
*** Exception: Something went wrong
439+
Something went wrong
436440
bad times
437441
438442
Or for a value that does not have a Show instance and can therefore not be displayed:
439443
>>> data V = V
440444
>>> V
441-
No instance for (Show V)
445+
No instance for (Show V) arising from a use of ‘evalPrint’
442446
-}
443447
evals :: Bool -> TEnv -> DynFlags -> [Statement] -> Ghc [Text]
444-
evals property (st, fp) df stmts = do
448+
evals mark_exception (st, fp) df stmts = do
445449
er <- gStrictTry $ mapM eval stmts
446450
return $ case er of
447451
Left err -> errorLines err
@@ -489,7 +493,7 @@ evals property (st, fp) df stmts = do
489493
dbg "{STMT " stmt
490494
res <- exec stmt l
491495
let r = case res of
492-
Left err -> Just . (if property then errorLines else exceptionLines) $ err
496+
Left err -> Just . (if mark_exception then exceptionLines else errorLines) $ err
493497
Right x -> singleLine <$> x
494498
dbg "STMT} -> " r
495499
return r

plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Config.hs

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,8 @@
33
{-# LANGUAGE OverloadedStrings #-}
44
module Ide.Plugin.Eval.Config
55
( properties
6-
, getDiffProperty
6+
, getEvalConfig
7+
, EvalConfig(..)
78
) where
89

910
import Ide.Plugin.Config (Config)
@@ -12,10 +13,25 @@ import Ide.PluginUtils (usePropertyLsp)
1213
import Ide.Types (PluginId)
1314
import Language.LSP.Server (MonadLsp)
1415

15-
properties :: Properties '[ 'PropertyKey "diff" 'TBoolean]
16+
-- | The Eval plugin configuration. (see 'properties')
17+
data EvalConfig = EvalConfig
18+
{ eval_cfg_diff :: Bool
19+
, eval_cfg_exception :: Bool
20+
}
21+
deriving (Eq, Ord, Show)
22+
23+
properties :: Properties
24+
'[ 'PropertyKey "exception" 'TBoolean
25+
, 'PropertyKey "diff" 'TBoolean
26+
]
1627
properties = emptyProperties
1728
& defineBooleanProperty #diff
1829
"Enable the diff output (WAS/NOW) of eval lenses" True
30+
& defineBooleanProperty #exception
31+
"Enable marking exceptions with `*** Exception:` similarly to doctest and GHCi." False
1932

20-
getDiffProperty :: (MonadLsp Config m) => PluginId -> m Bool
21-
getDiffProperty plId = usePropertyLsp #diff plId properties
33+
getEvalConfig :: (MonadLsp Config m) => PluginId -> m EvalConfig
34+
getEvalConfig plId =
35+
EvalConfig
36+
<$> usePropertyLsp #diff plId properties
37+
<*> usePropertyLsp #exception plId properties

plugins/hls-eval-plugin/test/Main.hs

Lines changed: 23 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import Control.Lens (_Just, folded, preview, toListOf,
1212
view, (^..))
1313
import Data.Aeson (Value (Object), fromJSON, object,
1414
toJSON, (.=))
15-
import Data.Aeson.Types (Result (Success))
15+
import Data.Aeson.Types (Result (Success), Pair)
1616
import Data.List (isInfixOf)
1717
import Data.List.Extra (nubOrdOn)
1818
import qualified Data.Map as Map
@@ -76,8 +76,7 @@ tests =
7676
| ghcVersion == GHC90 -> "-- No instance for (Num String) arising from a use of ‘+’"
7777
| otherwise -> "-- No instance for (Num [Char]) arising from a use of ‘+’"
7878
evalInFile "T8.hs" "-- >>> \"" "-- lexical error in string/character literal at end of input"
79-
evalInFile "T8.hs" "-- >>> 3 `div` 0" "-- *** Exception: divide by zero"
80-
, goldenWithEval "Evaluates to exception" "TException" "hs"
79+
evalInFile "T8.hs" "-- >>> 3 `div` 0" "-- divide by zero" -- The default for marking exceptions is False
8180
, goldenWithEval "Applies file LANGUAGE extensions" "T9" "hs"
8281
, goldenWithEval' "Evaluate a type with :kind!" "T10" "hs" (if ghcVersion == GHC92 then "ghc92.expected" else "expected")
8382
, goldenWithEval' "Reports an error for an incorrect type with :kind!" "T11" "hs" (if ghcVersion == GHC92 then "ghc92.expected" else "expected")
@@ -150,12 +149,12 @@ tests =
150149
]
151150
, goldenWithEval "Works with NoImplicitPrelude" "TNoImplicitPrelude" "hs"
152151
, goldenWithEval "Variable 'it' works" "TIt" "hs"
153-
154-
, goldenWithHaskellDoc evalPlugin "Give 'WAS' by default" testDataDir "TDiff" "expected.default" "hs" executeLensesBackwards
155-
, goldenWithHaskellDoc evalPlugin "Give the result only if diff is off" testDataDir "TDiff" "expected.no-diff" "hs" $ \doc -> do
156-
sendConfigurationChanged (toJSON diffOffConfig)
157-
executeLensesBackwards doc
158-
152+
, testGroup "configuration"
153+
[ goldenWithEval' "Give 'WAS' by default" "TDiff" "hs" "expected.default"
154+
, goldenWithEvalConfig' "Give the result only if diff is off" "TDiff" "hs" "expected.no-diff" diffOffConfig
155+
, goldenWithEvalConfig' "Evaluates to exception (not marked)" "TException" "hs" "expected.nomark" (exceptionConfig False)
156+
, goldenWithEvalConfig' "Evaluates to exception (with mark)" "TException" "hs" "expected.marked" (exceptionConfig True)
157+
]
159158
, testGroup ":info command"
160159
[ testCase ":info reports type, constructors and instances" $ do
161160
[output] <- map (unlines . codeLensTestOutput) <$> evalLenses "TInfo.hs"
@@ -265,16 +264,28 @@ codeLensTestOutput codeLens = do
265264
testDataDir :: FilePath
266265
testDataDir = "test" </> "testdata"
267266

268-
diffOffConfig :: Config
269-
diffOffConfig =
267+
changeConfig :: [Pair] -> Config
268+
changeConfig conf =
270269
def
271270
{ Plugin.plugins = Map.fromList [("eval",
272-
def { Plugin.plcGlobalOn = True, Plugin.plcConfig = unObject $ object ["diff" .= False] }
271+
def { Plugin.plcGlobalOn = True, Plugin.plcConfig = unObject $ object conf }
273272
)] }
274273
where
275274
unObject (Object obj) = obj
276275
unObject _ = undefined
277276

277+
diffOffConfig :: Config
278+
diffOffConfig = changeConfig ["diff" .= False]
279+
280+
exceptionConfig :: Bool -> Config
281+
exceptionConfig exCfg = changeConfig ["exception" .= exCfg]
282+
283+
goldenWithEvalConfig' :: TestName -> FilePath -> FilePath -> FilePath -> Config -> TestTree
284+
goldenWithEvalConfig' title path ext expected cfg =
285+
goldenWithHaskellDoc evalPlugin title testDataDir path expected ext $ \doc -> do
286+
sendConfigurationChanged (toJSON cfg)
287+
executeLensesBackwards doc
288+
278289
evalInFile :: HasCallStack => FilePath -> T.Text -> T.Text -> IO ()
279290
evalInFile fp e expected = runSessionWithServer evalPlugin testDataDir $ do
280291
doc <- openDoc fp "haskell"
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
module TException where
2+
3+
-- >>> exceptionalCode
4+
-- I am exceptional!
5+
exceptionalCode :: Int
6+
exceptionalCode = error "I am exceptional!"

0 commit comments

Comments
 (0)