Skip to content

Commit 47cb213

Browse files
Ailrunjneira
andauthored
Add diff option for eval plugin (#2622)
* Add diff option for eval plugin * Add a test for the diff option Co-authored-by: Javier Neira <[email protected]>
1 parent 3f12824 commit 47cb213

File tree

9 files changed

+208
-133
lines changed

9 files changed

+208
-133
lines changed

plugins/hls-eval-plugin/hls-eval-plugin.cabal

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,7 @@ library
4545
other-modules:
4646
Ide.Plugin.Eval.Code
4747
Ide.Plugin.Eval.CodeLens
48+
Ide.Plugin.Eval.Config
4849
Ide.Plugin.Eval.GHC
4950
Ide.Plugin.Eval.Parse.Comments
5051
Ide.Plugin.Eval.Parse.Option
@@ -105,10 +106,12 @@ test-suite tests
105106
build-depends:
106107
, aeson
107108
, base
109+
, containers
108110
, directory
109111
, extra
110112
, filepath
111113
, hls-eval-plugin
114+
, hls-plugin-api
112115
, hls-test-utils ^>=1.2
113116
, lens
114117
, lsp-types

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

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,23 @@ module Ide.Plugin.Eval (
1111

1212
import Development.IDE (IdeState)
1313
import qualified Ide.Plugin.Eval.CodeLens as CL
14+
import Ide.Plugin.Eval.Config
1415
import Ide.Plugin.Eval.Rules (rules)
15-
import Ide.Types (PluginDescriptor (..), PluginId,
16+
import Ide.Types (ConfigDescriptor (..),
17+
PluginDescriptor (..), PluginId,
18+
defaultConfigDescriptor,
1619
defaultPluginDescriptor,
17-
mkPluginHandler)
20+
mkCustomConfig, mkPluginHandler)
1821
import Language.LSP.Types
1922

2023
-- |Plugin descriptor
2124
descriptor :: PluginId -> PluginDescriptor IdeState
2225
descriptor plId =
2326
(defaultPluginDescriptor plId)
2427
{ pluginHandlers = mkPluginHandler STextDocumentCodeLens CL.codeLens
25-
, pluginCommands = [CL.evalCommand]
28+
, pluginCommands = [CL.evalCommand plId]
2629
, pluginRules = rules
30+
, pluginConfigDescriptor = defaultConfigDescriptor
31+
{ configCustomConfig = mkCustomConfig properties
32+
}
2733
}
Lines changed: 120 additions & 120 deletions
Original file line numberDiff line numberDiff line change
@@ -1,120 +1,120 @@
1-
{-# LANGUAGE LambdaCase #-}
2-
{-# LANGUAGE OverloadedStrings #-}
3-
{-# LANGUAGE ViewPatterns #-}
4-
{-# OPTIONS_GHC -Wwarn -fno-warn-orphans #-}
5-
6-
-- | Expression execution
7-
module Ide.Plugin.Eval.Code (Statement, testRanges, resultRange, evalSetup, propSetup, testCheck, asStatements,myExecStmt) where
8-
9-
import Control.Lens ((^.))
10-
import Control.Monad.IO.Class
11-
import Data.Algorithm.Diff (Diff, PolyDiff (..), getDiff)
12-
import qualified Data.List.NonEmpty as NE
13-
import Data.String (IsString)
14-
import qualified Data.Text as T
15-
import Development.IDE.GHC.Compat
16-
import Development.IDE.Types.Location (Position (..), Range (..))
17-
import GHC (ExecOptions, ExecResult (..),
18-
execStmt)
19-
import Ide.Plugin.Eval.Types (Language (Plain), Loc,
20-
Located (..),
21-
Section (sectionLanguage),
22-
Test (..), Txt, locate,
23-
locate0)
24-
import Language.LSP.Types.Lens (line, start)
25-
import System.IO.Extra (newTempFile, readFile')
26-
27-
-- | Return the ranges of the expression and result parts of the given test
28-
testRanges :: Test -> (Range, Range)
29-
testRanges tst =
30-
let startLine = testRange tst ^. start.line
31-
(fromIntegral -> exprLines, fromIntegral -> resultLines) = testLengths tst
32-
resLine = startLine + exprLines
33-
in ( Range
34-
(Position startLine 0)
35-
--(Position (startLine + exprLines + resultLines) 0),
36-
(Position resLine 0)
37-
, Range (Position resLine 0) (Position (resLine + resultLines) 0)
38-
)
39-
40-
{- |The document range where a test is defined
41-
testRange :: Loc Test -> Range
42-
testRange = fst . testRanges
43-
-}
44-
45-
-- |The document range where the result of the test is defined
46-
resultRange :: Test -> Range
47-
resultRange = snd . testRanges
48-
49-
-- TODO: handle BLANKLINE
50-
{-
51-
>>> showDiffs $ getDiff ["abc","def","ghi","end"] ["abc","def","Z","ZZ","end"]
52-
["abc","def","WAS ghi","NOW Z","NOW ZZ","end"]
53-
-}
54-
showDiffs :: (Semigroup a, IsString a) => [Diff a] -> [a]
55-
showDiffs = map showDiff
56-
57-
showDiff :: (Semigroup a, IsString a) => Diff a -> a
58-
showDiff (First w) = "WAS " <> w
59-
showDiff (Second w) = "NOW " <> w
60-
showDiff (Both w _) = w
61-
62-
testCheck :: (Section, Test) -> [T.Text] -> [T.Text]
63-
testCheck (section, test) out
64-
| null (testOutput test) || sectionLanguage section == Plain = out
65-
| otherwise = showDiffs $ getDiff (map T.pack $ testOutput test) out
66-
67-
testLengths :: Test -> (Int, Int)
68-
testLengths (Example e r _) = (NE.length e, length r)
69-
testLengths (Property _ r _) = (1, length r)
70-
71-
-- |A one-line Haskell statement
72-
type Statement = Loc String
73-
74-
asStatements :: Test -> [Statement]
75-
asStatements lt = locate $ Located (fromIntegral $ testRange lt ^. start.line) (asStmts lt)
76-
77-
asStmts :: Test -> [Txt]
78-
asStmts (Example e _ _) = NE.toList e
79-
asStmts (Property t _ _) =
80-
["prop11 = " ++ t, "(propEvaluation prop11 :: IO String)"]
81-
82-
83-
-- |GHC declarations required for expression evaluation
84-
evalSetup :: Ghc ()
85-
evalSetup = do
86-
preludeAsP <- parseImportDecl "import qualified Prelude as P"
87-
context <- getContext
88-
setContext (IIDecl preludeAsP : context)
89-
90-
-- | A wrapper of 'InteractiveEval.execStmt', capturing the execution result
91-
myExecStmt :: String -> ExecOptions -> Ghc (Either String (Maybe String))
92-
myExecStmt stmt opts = do
93-
(temp, purge) <- liftIO newTempFile
94-
evalPrint <- head <$> runDecls ("evalPrint x = P.writeFile "<> show temp <> " (P.show x)")
95-
modifySession $ \hsc -> hsc {hsc_IC = setInteractivePrintName (hsc_IC hsc) evalPrint}
96-
result <- execStmt stmt opts >>= \case
97-
ExecComplete (Left err) _ -> pure $ Left $ show err
98-
ExecComplete (Right _) _ -> liftIO $ Right . (\x -> if null x then Nothing else Just x) <$> readFile' temp
99-
ExecBreak{} -> pure $ Right $ Just "breakpoints are not supported"
100-
liftIO purge
101-
pure result
102-
103-
{- |GHC declarations required to execute test properties
104-
105-
Example:
106-
107-
prop> \(l::[Bool]) -> reverse (reverse l) == l
108-
+++ OK, passed 100 tests.
109-
110-
prop> \(l::[Bool]) -> reverse l == l
111-
*** Failed! Falsified (after 6 tests and 2 shrinks):
112-
[True,False]
113-
-}
114-
propSetup :: [Loc [Char]]
115-
propSetup =
116-
locate0
117-
[ ":set -XScopedTypeVariables -XExplicitForAll"
118-
, "import qualified Test.QuickCheck as Q11"
119-
, "propEvaluation p = Q11.quickCheckWithResult Q11.stdArgs p >>= error . Q11.output" -- uses `error` to get a multi-line display
120-
]
1+
{-# LANGUAGE LambdaCase #-}
2+
{-# LANGUAGE OverloadedStrings #-}
3+
{-# LANGUAGE ViewPatterns #-}
4+
{-# OPTIONS_GHC -Wwarn -fno-warn-orphans #-}
5+
6+
-- | Expression execution
7+
module Ide.Plugin.Eval.Code (Statement, testRanges, resultRange, evalSetup, propSetup, testCheck, asStatements,myExecStmt) where
8+
9+
import Control.Lens ((^.))
10+
import Control.Monad.IO.Class
11+
import Data.Algorithm.Diff (Diff, PolyDiff (..), getDiff)
12+
import qualified Data.List.NonEmpty as NE
13+
import Data.String (IsString)
14+
import qualified Data.Text as T
15+
import Development.IDE.GHC.Compat
16+
import Development.IDE.Types.Location (Position (..), Range (..))
17+
import GHC (ExecOptions, ExecResult (..),
18+
execStmt)
19+
import Ide.Plugin.Eval.Types (Language (Plain), Loc,
20+
Located (..),
21+
Section (sectionLanguage),
22+
Test (..), Txt, locate,
23+
locate0)
24+
import Language.LSP.Types.Lens (line, start)
25+
import System.IO.Extra (newTempFile, readFile')
26+
27+
-- | Return the ranges of the expression and result parts of the given test
28+
testRanges :: Test -> (Range, Range)
29+
testRanges tst =
30+
let startLine = testRange tst ^. start.line
31+
(fromIntegral -> exprLines, fromIntegral -> resultLines) = testLengths tst
32+
resLine = startLine + exprLines
33+
in ( Range
34+
(Position startLine 0)
35+
--(Position (startLine + exprLines + resultLines) 0),
36+
(Position resLine 0)
37+
, Range (Position resLine 0) (Position (resLine + resultLines) 0)
38+
)
39+
40+
{- |The document range where a test is defined
41+
testRange :: Loc Test -> Range
42+
testRange = fst . testRanges
43+
-}
44+
45+
-- |The document range where the result of the test is defined
46+
resultRange :: Test -> Range
47+
resultRange = snd . testRanges
48+
49+
-- TODO: handle BLANKLINE
50+
{-
51+
>>> showDiffs $ getDiff ["abc","def","ghi","end"] ["abc","def","Z","ZZ","end"]
52+
["abc","def","WAS ghi","NOW Z","NOW ZZ","end"]
53+
-}
54+
showDiffs :: (Semigroup a, IsString a) => [Diff a] -> [a]
55+
showDiffs = map showDiff
56+
57+
showDiff :: (Semigroup a, IsString a) => Diff a -> a
58+
showDiff (First w) = "WAS " <> w
59+
showDiff (Second w) = "NOW " <> w
60+
showDiff (Both w _) = w
61+
62+
testCheck :: Bool -> (Section, Test) -> [T.Text] -> [T.Text]
63+
testCheck diff (section, test) out
64+
| not diff || null (testOutput test) || sectionLanguage section == Plain = out
65+
| otherwise = showDiffs $ getDiff (map T.pack $ testOutput test) out
66+
67+
testLengths :: Test -> (Int, Int)
68+
testLengths (Example e r _) = (NE.length e, length r)
69+
testLengths (Property _ r _) = (1, length r)
70+
71+
-- |A one-line Haskell statement
72+
type Statement = Loc String
73+
74+
asStatements :: Test -> [Statement]
75+
asStatements lt = locate $ Located (fromIntegral $ testRange lt ^. start.line) (asStmts lt)
76+
77+
asStmts :: Test -> [Txt]
78+
asStmts (Example e _ _) = NE.toList e
79+
asStmts (Property t _ _) =
80+
["prop11 = " ++ t, "(propEvaluation prop11 :: IO String)"]
81+
82+
83+
-- |GHC declarations required for expression evaluation
84+
evalSetup :: Ghc ()
85+
evalSetup = do
86+
preludeAsP <- parseImportDecl "import qualified Prelude as P"
87+
context <- getContext
88+
setContext (IIDecl preludeAsP : context)
89+
90+
-- | A wrapper of 'InteractiveEval.execStmt', capturing the execution result
91+
myExecStmt :: String -> ExecOptions -> Ghc (Either String (Maybe String))
92+
myExecStmt stmt opts = do
93+
(temp, purge) <- liftIO newTempFile
94+
evalPrint <- head <$> runDecls ("evalPrint x = P.writeFile "<> show temp <> " (P.show x)")
95+
modifySession $ \hsc -> hsc {hsc_IC = setInteractivePrintName (hsc_IC hsc) evalPrint}
96+
result <- execStmt stmt opts >>= \case
97+
ExecComplete (Left err) _ -> pure $ Left $ show err
98+
ExecComplete (Right _) _ -> liftIO $ Right . (\x -> if null x then Nothing else Just x) <$> readFile' temp
99+
ExecBreak{} -> pure $ Right $ Just "breakpoints are not supported"
100+
liftIO purge
101+
pure result
102+
103+
{- |GHC declarations required to execute test properties
104+
105+
Example:
106+
107+
prop> \(l::[Bool]) -> reverse (reverse l) == l
108+
+++ OK, passed 100 tests.
109+
110+
prop> \(l::[Bool]) -> reverse l == l
111+
*** Failed! Falsified (after 6 tests and 2 shrinks):
112+
[True,False]
113+
-}
114+
propSetup :: [Loc [Char]]
115+
propSetup =
116+
locate0
117+
[ ":set -XScopedTypeVariables -XExplicitForAll"
118+
, "import qualified Test.QuickCheck as Q11"
119+
, "propEvaluation p = Q11.quickCheckWithResult Q11.stdArgs p >>= error . Q11.output" -- uses `error` to get a multi-line display
120+
]

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

Lines changed: 13 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import qualified Control.Exception as E
3030
import Control.Lens (_1, _3, (%~), (<&>), (^.))
3131
import Control.Monad (guard, join, void, when)
3232
import Control.Monad.IO.Class (MonadIO (liftIO))
33+
import Control.Monad.Trans (lift)
3334
import Control.Monad.Trans.Except (ExceptT (..))
3435
import Data.Aeson (toJSON)
3536
import Data.Char (isSpace)
@@ -78,10 +79,12 @@ import qualified GHC.LanguageExtensions.Type as LangExt (Extension (..))
7879

7980
import Development.IDE.Core.FileStore (setSomethingModified)
8081
import Development.IDE.Types.Shake (toKey)
82+
import Ide.Plugin.Config (Config)
8183
import Ide.Plugin.Eval.Code (Statement, asStatements,
8284
evalSetup, myExecStmt,
8385
propSetup, resultRange,
8486
testCheck, testRanges)
87+
import Ide.Plugin.Eval.Config (getDiffProperty)
8588
import Ide.Plugin.Eval.GHC (addImport, addPackages,
8689
hasPackage, showDynFlags)
8790
import Ide.Plugin.Eval.Parse.Comments (commentsToSections)
@@ -176,16 +179,16 @@ codeLens st plId CodeLensParams{_textDocument} =
176179
evalCommandName :: CommandId
177180
evalCommandName = "evalCommand"
178181

179-
evalCommand :: PluginCommand IdeState
180-
evalCommand = PluginCommand evalCommandName "evaluate" runEvalCmd
182+
evalCommand :: PluginId -> PluginCommand IdeState
183+
evalCommand plId = PluginCommand evalCommandName "evaluate" (runEvalCmd plId)
181184

182185
type EvalId = Int
183186

184-
runEvalCmd :: CommandFunction IdeState EvalParams
185-
runEvalCmd st EvalParams{..} =
187+
runEvalCmd :: PluginId -> CommandFunction IdeState EvalParams
188+
runEvalCmd plId st EvalParams{..} =
186189
let dbg = logWith st
187190
perf = timed dbg
188-
cmd :: ExceptT String (LspM c) WorkspaceEdit
191+
cmd :: ExceptT String (LspM Config) WorkspaceEdit
189192
cmd = do
190193
let tests = map (\(a,_,b) -> (a,b)) $ testsBySection sections
191194

@@ -300,12 +303,13 @@ runEvalCmd st EvalParams{..} =
300303
-- Evaluation takes place 'inside' the module
301304
setContext [Compat.IIModule modName]
302305
Right <$> getSession
303-
306+
diff <- lift $ getDiffProperty plId
304307
edits <-
305308
perf "edits" $
306309
liftIO $
307310
evalGhcEnv hscEnv' $
308311
runTests
312+
diff
309313
(st, fp)
310314
tests
311315

@@ -347,8 +351,8 @@ testsBySection sections =
347351

348352
type TEnv = (IdeState, String)
349353

350-
runTests :: TEnv -> [(Section, Test)] -> Ghc [TextEdit]
351-
runTests e@(_st, _) tests = do
354+
runTests :: Bool -> TEnv -> [(Section, Test)] -> Ghc [TextEdit]
355+
runTests diff e@(_st, _) tests = do
352356
df <- getInteractiveDynFlags
353357
evalSetup
354358
when (hasQuickCheck df && needsQuickCheck tests) $ void $ evals e df propSetup
@@ -363,7 +367,7 @@ runTests e@(_st, _) tests = do
363367
rs <- runTest e df test
364368
dbg "TEST RESULTS" rs
365369

366-
let checkedResult = testCheck (section, test) rs
370+
let checkedResult = testCheck diff (section, test) rs
367371

368372
let edit = asEdit (sectionFormat section) test (map pad checkedResult)
369373
dbg "TEST EDIT" edit
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{-# LANGUAGE FlexibleContexts #-}
2+
{-# LANGUAGE OverloadedLabels #-}
3+
{-# LANGUAGE OverloadedStrings #-}
4+
module Ide.Plugin.Eval.Config
5+
( properties
6+
, getDiffProperty
7+
) where
8+
9+
import Ide.Plugin.Config (Config)
10+
import Ide.Plugin.Properties
11+
import Ide.PluginUtils (usePropertyLsp)
12+
import Ide.Types (PluginId)
13+
import Language.LSP.Server (MonadLsp)
14+
15+
properties :: Properties '[ 'PropertyKey "diff" 'TBoolean]
16+
properties = emptyProperties
17+
& defineBooleanProperty #diff
18+
"Enable the diff output (WAS/NOW) of eval lenses" True
19+
20+
getDiffProperty :: (MonadLsp Config m) => PluginId -> m Bool
21+
getDiffProperty plId = usePropertyLsp #diff plId properties

0 commit comments

Comments
 (0)