Skip to content

Add diff option for eval plugin #2622

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 6 commits into from
Feb 1, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions plugins/hls-eval-plugin/hls-eval-plugin.cabal
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ library
other-modules:
Ide.Plugin.Eval.Code
Ide.Plugin.Eval.CodeLens
Ide.Plugin.Eval.Config
Ide.Plugin.Eval.GHC
Ide.Plugin.Eval.Parse.Comments
Ide.Plugin.Eval.Parse.Option
Expand Down Expand Up @@ -105,10 +106,12 @@ test-suite tests
build-depends:
, aeson
, base
, containers
, directory
, extra
, filepath
, hls-eval-plugin
, hls-plugin-api
, hls-test-utils ^>=1.2
, lens
, lsp-types
Expand Down
12 changes: 9 additions & 3 deletions plugins/hls-eval-plugin/src/Ide/Plugin/Eval.hs
Original file line number Diff line number Diff line change
Expand Up @@ -11,17 +11,23 @@ module Ide.Plugin.Eval (

import Development.IDE (IdeState)
import qualified Ide.Plugin.Eval.CodeLens as CL
import Ide.Plugin.Eval.Config
import Ide.Plugin.Eval.Rules (rules)
import Ide.Types (PluginDescriptor (..), PluginId,
import Ide.Types (ConfigDescriptor (..),
PluginDescriptor (..), PluginId,
defaultConfigDescriptor,
defaultPluginDescriptor,
mkPluginHandler)
mkCustomConfig, mkPluginHandler)
import Language.LSP.Types

-- |Plugin descriptor
descriptor :: PluginId -> PluginDescriptor IdeState
descriptor plId =
(defaultPluginDescriptor plId)
{ pluginHandlers = mkPluginHandler STextDocumentCodeLens CL.codeLens
, pluginCommands = [CL.evalCommand]
, pluginCommands = [CL.evalCommand plId]
, pluginRules = rules
, pluginConfigDescriptor = defaultConfigDescriptor
{ configCustomConfig = mkCustomConfig properties
}
}
240 changes: 120 additions & 120 deletions plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Code.hs
Original file line number Diff line number Diff line change
@@ -1,120 +1,120 @@
{-# LANGUAGE LambdaCase #-}
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This file is completely altered because of \r (it looks like #2597 introducing some \rs. C.C. @bradrn).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh, that’s really bad. The pre-commit hook should not change semantics. This is a stylish-haskell bug, I think.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradrn It didn't change the semantics. It just added \r on each line, which gives this "huge" git diff. (As my setting auto-removes \rs)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for clarifying — I had thought you meant something different by ‘completely altered’.

I’m a bit confused, though. What exactly changed the file? My unrelated PR (merged after this one was submitted) shouldn’t affect this code, unless you merge it… in which case it shouldn’t show up in this commit. Did you run the pre-commit hook by any chance?

Copy link
Member Author

@Ailrun Ailrun Jan 25, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@bradrn I always rebase my PR even after its submission, so this PR is indeed after your PR. (the last was 8 hours ago: #2622 (comment))

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OK, makes sense, thanks!

{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wwarn -fno-warn-orphans #-}
-- | Expression execution
module Ide.Plugin.Eval.Code (Statement, testRanges, resultRange, evalSetup, propSetup, testCheck, asStatements,myExecStmt) where
import Control.Lens ((^.))
import Control.Monad.IO.Class
import Data.Algorithm.Diff (Diff, PolyDiff (..), getDiff)
import qualified Data.List.NonEmpty as NE
import Data.String (IsString)
import qualified Data.Text as T
import Development.IDE.GHC.Compat
import Development.IDE.Types.Location (Position (..), Range (..))
import GHC (ExecOptions, ExecResult (..),
execStmt)
import Ide.Plugin.Eval.Types (Language (Plain), Loc,
Located (..),
Section (sectionLanguage),
Test (..), Txt, locate,
locate0)
import Language.LSP.Types.Lens (line, start)
import System.IO.Extra (newTempFile, readFile')
-- | Return the ranges of the expression and result parts of the given test
testRanges :: Test -> (Range, Range)
testRanges tst =
let startLine = testRange tst ^. start.line
(fromIntegral -> exprLines, fromIntegral -> resultLines) = testLengths tst
resLine = startLine + exprLines
in ( Range
(Position startLine 0)
--(Position (startLine + exprLines + resultLines) 0),
(Position resLine 0)
, Range (Position resLine 0) (Position (resLine + resultLines) 0)
)
{- |The document range where a test is defined
testRange :: Loc Test -> Range
testRange = fst . testRanges
-}
-- |The document range where the result of the test is defined
resultRange :: Test -> Range
resultRange = snd . testRanges
-- TODO: handle BLANKLINE
{-
>>> showDiffs $ getDiff ["abc","def","ghi","end"] ["abc","def","Z","ZZ","end"]
["abc","def","WAS ghi","NOW Z","NOW ZZ","end"]
-}
showDiffs :: (Semigroup a, IsString a) => [Diff a] -> [a]
showDiffs = map showDiff
showDiff :: (Semigroup a, IsString a) => Diff a -> a
showDiff (First w) = "WAS " <> w
showDiff (Second w) = "NOW " <> w
showDiff (Both w _) = w
testCheck :: (Section, Test) -> [T.Text] -> [T.Text]
testCheck (section, test) out
| null (testOutput test) || sectionLanguage section == Plain = out
| otherwise = showDiffs $ getDiff (map T.pack $ testOutput test) out
testLengths :: Test -> (Int, Int)
testLengths (Example e r _) = (NE.length e, length r)
testLengths (Property _ r _) = (1, length r)
-- |A one-line Haskell statement
type Statement = Loc String
asStatements :: Test -> [Statement]
asStatements lt = locate $ Located (fromIntegral $ testRange lt ^. start.line) (asStmts lt)
asStmts :: Test -> [Txt]
asStmts (Example e _ _) = NE.toList e
asStmts (Property t _ _) =
["prop11 = " ++ t, "(propEvaluation prop11 :: IO String)"]
-- |GHC declarations required for expression evaluation
evalSetup :: Ghc ()
evalSetup = do
preludeAsP <- parseImportDecl "import qualified Prelude as P"
context <- getContext
setContext (IIDecl preludeAsP : context)
-- | A wrapper of 'InteractiveEval.execStmt', capturing the execution result
myExecStmt :: String -> ExecOptions -> Ghc (Either String (Maybe String))
myExecStmt stmt opts = do
(temp, purge) <- liftIO newTempFile
evalPrint <- head <$> runDecls ("evalPrint x = P.writeFile "<> show temp <> " (P.show x)")
modifySession $ \hsc -> hsc {hsc_IC = setInteractivePrintName (hsc_IC hsc) evalPrint}
result <- execStmt stmt opts >>= \case
ExecComplete (Left err) _ -> pure $ Left $ show err
ExecComplete (Right _) _ -> liftIO $ Right . (\x -> if null x then Nothing else Just x) <$> readFile' temp
ExecBreak{} -> pure $ Right $ Just "breakpoints are not supported"
liftIO purge
pure result
{- |GHC declarations required to execute test properties
Example:
prop> \(l::[Bool]) -> reverse (reverse l) == l
+++ OK, passed 100 tests.
prop> \(l::[Bool]) -> reverse l == l
*** Failed! Falsified (after 6 tests and 2 shrinks):
[True,False]
-}
propSetup :: [Loc [Char]]
propSetup =
locate0
[ ":set -XScopedTypeVariables -XExplicitForAll"
, "import qualified Test.QuickCheck as Q11"
, "propEvaluation p = Q11.quickCheckWithResult Q11.stdArgs p >>= error . Q11.output" -- uses `error` to get a multi-line display
]
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE OverloadedStrings #-}
{-# LANGUAGE ViewPatterns #-}
{-# OPTIONS_GHC -Wwarn -fno-warn-orphans #-}

-- | Expression execution
module Ide.Plugin.Eval.Code (Statement, testRanges, resultRange, evalSetup, propSetup, testCheck, asStatements,myExecStmt) where

import Control.Lens ((^.))
import Control.Monad.IO.Class
import Data.Algorithm.Diff (Diff, PolyDiff (..), getDiff)
import qualified Data.List.NonEmpty as NE
import Data.String (IsString)
import qualified Data.Text as T
import Development.IDE.GHC.Compat
import Development.IDE.Types.Location (Position (..), Range (..))
import GHC (ExecOptions, ExecResult (..),
execStmt)
import Ide.Plugin.Eval.Types (Language (Plain), Loc,
Located (..),
Section (sectionLanguage),
Test (..), Txt, locate,
locate0)
import Language.LSP.Types.Lens (line, start)
import System.IO.Extra (newTempFile, readFile')

-- | Return the ranges of the expression and result parts of the given test
testRanges :: Test -> (Range, Range)
testRanges tst =
let startLine = testRange tst ^. start.line
(fromIntegral -> exprLines, fromIntegral -> resultLines) = testLengths tst
resLine = startLine + exprLines
in ( Range
(Position startLine 0)
--(Position (startLine + exprLines + resultLines) 0),
(Position resLine 0)
, Range (Position resLine 0) (Position (resLine + resultLines) 0)
)

{- |The document range where a test is defined
testRange :: Loc Test -> Range
testRange = fst . testRanges
-}

-- |The document range where the result of the test is defined
resultRange :: Test -> Range
resultRange = snd . testRanges

-- TODO: handle BLANKLINE
{-
>>> showDiffs $ getDiff ["abc","def","ghi","end"] ["abc","def","Z","ZZ","end"]
["abc","def","WAS ghi","NOW Z","NOW ZZ","end"]
-}
showDiffs :: (Semigroup a, IsString a) => [Diff a] -> [a]
showDiffs = map showDiff

showDiff :: (Semigroup a, IsString a) => Diff a -> a
showDiff (First w) = "WAS " <> w
showDiff (Second w) = "NOW " <> w
showDiff (Both w _) = w

testCheck :: Bool -> (Section, Test) -> [T.Text] -> [T.Text]
testCheck diff (section, test) out
| not diff || null (testOutput test) || sectionLanguage section == Plain = out
| otherwise = showDiffs $ getDiff (map T.pack $ testOutput test) out

testLengths :: Test -> (Int, Int)
testLengths (Example e r _) = (NE.length e, length r)
testLengths (Property _ r _) = (1, length r)

-- |A one-line Haskell statement
type Statement = Loc String

asStatements :: Test -> [Statement]
asStatements lt = locate $ Located (fromIntegral $ testRange lt ^. start.line) (asStmts lt)

asStmts :: Test -> [Txt]
asStmts (Example e _ _) = NE.toList e
asStmts (Property t _ _) =
["prop11 = " ++ t, "(propEvaluation prop11 :: IO String)"]


-- |GHC declarations required for expression evaluation
evalSetup :: Ghc ()
evalSetup = do
preludeAsP <- parseImportDecl "import qualified Prelude as P"
context <- getContext
setContext (IIDecl preludeAsP : context)

-- | A wrapper of 'InteractiveEval.execStmt', capturing the execution result
myExecStmt :: String -> ExecOptions -> Ghc (Either String (Maybe String))
myExecStmt stmt opts = do
(temp, purge) <- liftIO newTempFile
evalPrint <- head <$> runDecls ("evalPrint x = P.writeFile "<> show temp <> " (P.show x)")
modifySession $ \hsc -> hsc {hsc_IC = setInteractivePrintName (hsc_IC hsc) evalPrint}
result <- execStmt stmt opts >>= \case
ExecComplete (Left err) _ -> pure $ Left $ show err
ExecComplete (Right _) _ -> liftIO $ Right . (\x -> if null x then Nothing else Just x) <$> readFile' temp
ExecBreak{} -> pure $ Right $ Just "breakpoints are not supported"
liftIO purge
pure result

{- |GHC declarations required to execute test properties

Example:

prop> \(l::[Bool]) -> reverse (reverse l) == l
+++ OK, passed 100 tests.

prop> \(l::[Bool]) -> reverse l == l
*** Failed! Falsified (after 6 tests and 2 shrinks):
[True,False]
-}
propSetup :: [Loc [Char]]
propSetup =
locate0
[ ":set -XScopedTypeVariables -XExplicitForAll"
, "import qualified Test.QuickCheck as Q11"
, "propEvaluation p = Q11.quickCheckWithResult Q11.stdArgs p >>= error . Q11.output" -- uses `error` to get a multi-line display
]
22 changes: 13 additions & 9 deletions plugins/hls-eval-plugin/src/Ide/Plugin/Eval/CodeLens.hs
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@ import qualified Control.Exception as E
import Control.Lens (_1, _3, (%~), (<&>), (^.))
import Control.Monad (guard, join, void, when)
import Control.Monad.IO.Class (MonadIO (liftIO))
import Control.Monad.Trans (lift)
import Control.Monad.Trans.Except (ExceptT (..))
import Data.Aeson (toJSON)
import Data.Char (isSpace)
Expand Down Expand Up @@ -78,10 +79,12 @@ import qualified GHC.LanguageExtensions.Type as LangExt (Extension (..))

import Development.IDE.Core.FileStore (setSomethingModified)
import Development.IDE.Types.Shake (toKey)
import Ide.Plugin.Config (Config)
import Ide.Plugin.Eval.Code (Statement, asStatements,
evalSetup, myExecStmt,
propSetup, resultRange,
testCheck, testRanges)
import Ide.Plugin.Eval.Config (getDiffProperty)
import Ide.Plugin.Eval.GHC (addImport, addPackages,
hasPackage, showDynFlags)
import Ide.Plugin.Eval.Parse.Comments (commentsToSections)
Expand Down Expand Up @@ -176,16 +179,16 @@ codeLens st plId CodeLensParams{_textDocument} =
evalCommandName :: CommandId
evalCommandName = "evalCommand"

evalCommand :: PluginCommand IdeState
evalCommand = PluginCommand evalCommandName "evaluate" runEvalCmd
evalCommand :: PluginId -> PluginCommand IdeState
evalCommand plId = PluginCommand evalCommandName "evaluate" (runEvalCmd plId)

type EvalId = Int

runEvalCmd :: CommandFunction IdeState EvalParams
runEvalCmd st EvalParams{..} =
runEvalCmd :: PluginId -> CommandFunction IdeState EvalParams
runEvalCmd plId st EvalParams{..} =
let dbg = logWith st
perf = timed dbg
cmd :: ExceptT String (LspM c) WorkspaceEdit
cmd :: ExceptT String (LspM Config) WorkspaceEdit
cmd = do
let tests = map (\(a,_,b) -> (a,b)) $ testsBySection sections

Expand Down Expand Up @@ -300,12 +303,13 @@ runEvalCmd st EvalParams{..} =
-- Evaluation takes place 'inside' the module
setContext [Compat.IIModule modName]
Right <$> getSession

diff <- lift $ getDiffProperty plId
edits <-
perf "edits" $
liftIO $
evalGhcEnv hscEnv' $
runTests
diff
(st, fp)
tests

Expand Down Expand Up @@ -347,8 +351,8 @@ testsBySection sections =

type TEnv = (IdeState, String)

runTests :: TEnv -> [(Section, Test)] -> Ghc [TextEdit]
runTests e@(_st, _) tests = do
runTests :: Bool -> TEnv -> [(Section, Test)] -> Ghc [TextEdit]
runTests diff e@(_st, _) tests = do
df <- getInteractiveDynFlags
evalSetup
when (hasQuickCheck df && needsQuickCheck tests) $ void $ evals e df propSetup
Expand All @@ -363,7 +367,7 @@ runTests e@(_st, _) tests = do
rs <- runTest e df test
dbg "TEST RESULTS" rs

let checkedResult = testCheck (section, test) rs
let checkedResult = testCheck diff (section, test) rs

let edit = asEdit (sectionFormat section) test (map pad checkedResult)
dbg "TEST EDIT" edit
Expand Down
21 changes: 21 additions & 0 deletions plugins/hls-eval-plugin/src/Ide/Plugin/Eval/Config.hs
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE OverloadedLabels #-}
{-# LANGUAGE OverloadedStrings #-}
module Ide.Plugin.Eval.Config
( properties
, getDiffProperty
) where

import Ide.Plugin.Config (Config)
import Ide.Plugin.Properties
import Ide.PluginUtils (usePropertyLsp)
import Ide.Types (PluginId)
import Language.LSP.Server (MonadLsp)

properties :: Properties '[ 'PropertyKey "diff" 'TBoolean]
properties = emptyProperties
& defineBooleanProperty #diff
"Enable the diff output (WAS/NOW) of eval lenses" True

getDiffProperty :: (MonadLsp Config m) => PluginId -> m Bool
getDiffProperty plId = usePropertyLsp #diff plId properties
Loading