packages feed

reflex-ghci 0.1.3.1 → 0.1.4.0

raw patch · 7 files changed

+391/−10 lines, 7 filesdep +dependent-sumdep +mtldep +primitivedep ~basedep ~bytestringdep ~directoryPVP: major bump suggested

API removals or changes: PVP suggests a major version bump

Dependencies added: dependent-sum, mtl, primitive, ref-tf, temporary

Dependency ranges changed: base, bytestring, directory, process, reflex, reflex-process, reflex-vty, text, vty

API changes (from Hackage documentation)

+ Reflex.Vty.GHCi: getExitEvent :: (PerformEvent t m, MonadIO (Performable m)) => Ghci t -> Event t a -> VtyWidget t m (Event t ())
+ Reflex.Vty.GHCi: shutdown :: (PerformEvent t m, MonadIO (Performable m)) => Event t (Ghci t) -> m (Event t ())
- Reflex.Vty.GHCi: ghciModuleStatus :: (MonadNodeId m, PostBuild t m, MonadHold t m, MonadFix m) => Ghci t -> VtyWidget t m ()
+ Reflex.Vty.GHCi: ghciModuleStatus :: (MonadNodeId m, PostBuild t m, MonadHold t m, MonadFix m, Adjustable t m) => Ghci t -> VtyWidget t m ()

Files

ChangeLog.md view
@@ -1,5 +1,13 @@ # Revision history for reflex-ghci +## 0.1.4.0++* Library: Export `shutdown` and `getExitEvent` to make it easier for library users to cleanly exit+* Library: Fix regex to capture "Failed, one module loaded"+* Tests: Add basic test suite, covering module loading, expression execution, exceptions, and filesystem notification-based reloading.+* Library: Reset module output pane scroll position on reload events (like we do with the expression output pane).+* Executable: Show executable version number (taken from cabal file) in help output+ ## 0.1.3.1  * Update for compatibility with `reflex-process`-0.2.0.0.
reflex-ghci.cabal view
@@ -1,6 +1,6 @@ cabal-version: >=1.10 name: reflex-ghci-version: 0.1.3.1+version: 0.1.4.0 synopsis: A GHCi widget library for use in reflex applications description:   Run GHCi from within a reflex application and interact with it using a functional-reactive interface.@@ -33,11 +33,12 @@     , process >= 1.6 && < 1.7     , reflex >= 0.6.3 && < 0.7     , reflex-fsnotify >= 0.2 && < 0.3-    , reflex-process >= 0.2 && < 0.3+    , reflex-process >= 0.2.1 && < 0.3     , regex-tdfa >= 1.2.3 && < 1.3     , reflex-vty >= 0.1.3 && < 0.2     , text >= 1.2 && < 1.3     , unix >= 2.7 && < 2.8+    , vty >= 5.25 && < 5.26   hs-source-dirs: src   default-language: Haskell2010   ghc-options: -Wall@@ -54,7 +55,31 @@     , reflex-vty     , reflex-process     , text-    , vty >= 5.25 && < 5.26+    , vty+  other-modules: Paths_reflex_ghci+  ghc-options: -threaded -rtsopts+  default-language: Haskell2010++test-suite test+  type: exitcode-stdio-1.0+  main-is: test.hs+  hs-source-dirs: tests+  build-depends:+      base+    , bytestring+    , dependent-sum >= 0.6 && < 0.7+    , directory+    , mtl >= 2.2 && < 2.3+    , primitive >= 0.6.4 && < 0.7+    , process+    , ref-tf >= 0.4 && < 0.5+    , reflex+    , reflex-ghci+    , reflex-process+    , reflex-vty+    , temporary >= 1.3 && < 1.4+    , text+  other-modules: HeadlessHost   ghc-options: -threaded -rtsopts   default-language: Haskell2010 
src-bin/ghci.hs view
@@ -5,12 +5,13 @@  import Control.Concurrent (threadDelay) import Control.Monad.IO.Class (liftIO)-import qualified Graphics.Vty.Input as V import qualified Data.Text as T import qualified Data.Text.Encoding as T-import System.Process (shell, terminateProcess)-+import Data.Version (showVersion)+import qualified Graphics.Vty.Input as V import Options.Applicative+import Paths_reflex_ghci (version)+import System.Process (shell, terminateProcess)  data GhciArg = GhciArg   { _ghciArg_replCommand :: String@@ -39,7 +40,8 @@   let opts = info (ghciArg <**> helper) $ mconcat         [ fullDesc         , progDesc "Run a Haskell REPL that automatically reloads when source files change."-        , header "Welcome to reflex-ghci!"+        , header $ "Welcome to reflex-ghci " <>+            showVersion version         ]   GhciArg { _ghciArg_replCommand = cmd, _ghciArg_execCommand = expr } <- execParser opts   mainWidget $ do
src/Reflex/Process/GHCi.hs view
@@ -95,7 +95,7 @@        -- Define some Regex patterns to use to determine GHCi's state based on output       let okModulesLoaded = "Ok.*module.*loaded." :: ByteString-          failedNoModulesLoaded = "Failed,.*modules loaded." :: ByteString+          failedNoModulesLoaded = "Failed,.*module.*loaded." :: ByteString           -- TODO: Is there a way to distinguish GHCi's actual exception output           -- from someone printing "*** Exception:" to stderr?           -- TODO: Are there other exception patterns to watch out for?
src/Reflex/Vty/GHCi.hs view
@@ -10,6 +10,7 @@  import Control.Monad ((<=<), void) import Control.Monad.Fix (MonadFix)+import Control.Monad.IO.Class (liftIO, MonadIO) import Data.ByteString (ByteString) import qualified Data.Text as T import qualified Data.Text.Encoding as T@@ -17,6 +18,8 @@ import Reflex.Process import Reflex.Process.GHCi import Reflex.Vty+import qualified Graphics.Vty.Input as V+import qualified System.Process as P  -- | Display the overall status of the GHCi session, including exit information in case GHCi has quit statusDisplay@@ -75,9 +78,10 @@ -- | Display the output GHCi produces when it's loading the requested modules (e.g., warnings) ghciModuleStatus   :: ( MonadNodeId m-     , PostBuild t m +     , PostBuild t m      , MonadHold t m      , MonadFix m+     , Adjustable t m      )   => Ghci t   -> VtyWidget t m ()@@ -86,7 +90,9 @@   ghciExited <- hold False $ True <$ ghciExit   fixed 3 $ boxStatic def $ statusDisplay g   out <- moduleOutput (not <$> ghciExited) g-  stretch $ scrollableOutput $ current out+  stretch $ void $+    networkHold (scrollableOutput $ current out) $ ffor (_ghci_reload g) $+      const $ scrollableOutput $ current out  -- | Display the output of the expression GHCi is evaluating ghciExecOutput@@ -118,3 +124,31 @@   (hRule doubleBoxStyle)   (ghciModuleStatus g)   (ghciExecOutput g)++-- | Listen for ctrl-c (and any other provided exit events) and+-- shutdown the Ghci process upon receipt+getExitEvent+  :: ( PerformEvent t m+     , MonadIO (Performable m)+     )+  => Ghci t+  -> Event t a+  -> VtyWidget t m (Event t ())+getExitEvent g externalExitReq = do+  exitReq <- keyCombo (V.KChar 'c', [V.MCtrl])+  let exitReqs = leftmost+        [ g <$ externalExitReq+        , g <$ exitReq+        ]+  shutdown exitReqs++-- | Shut down a given Ghci process+shutdown+  :: ( PerformEvent t m+     , MonadIO (Performable m)+     )+  => Event t (Ghci t)+  -> m (Event t ())+shutdown exitReqs = do+  performEvent $ ffor exitReqs $ \g ->+    liftIO $ P.terminateProcess $ _process_handle $ _ghci_process g
+ tests/HeadlessHost.hs view
@@ -0,0 +1,83 @@+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE RankNTypes #-}+{-# LANGUAGE TypeFamilies #-}+{-# LANGUAGE ConstraintKinds #-}+module HeadlessHost where++import Data.Maybe+import Reflex+import Control.Monad.Fix+import Control.Monad.Primitive+import Reflex.Host.Class+import Control.Monad.IO.Class+import Control.Monad.Ref+import Data.IORef+import Data.Dependent.Sum+import Control.Concurrent.Chan (newChan, readChan)+import Control.Monad (forM, forM_)+import Control.Monad.Identity (Identity(..))++type HeadlessResult t = Event t ()++type MonadHeadlessApp t m =+  ( Reflex t+  , MonadHold t m+  , MonadFix m+  , PrimMonad (HostFrame t)+  , ReflexHost t+  , MonadIO (HostFrame t)+  , Ref m ~ IORef+  , Ref (HostFrame t) ~ IORef+  , MonadRef (HostFrame t)+  , NotReady t m+  , TriggerEvent t m+  , PostBuild t m+  , PerformEvent t m+  , MonadIO m+  , MonadIO (Performable m)+  , Adjustable t m+  )++runHeadlessApp+  :: (forall t m. MonadHeadlessApp t m => m (HeadlessResult t))+  -> IO ()+runHeadlessApp guest =+  (runSpiderHost :: SpiderHost Global a -> IO a) $ do+    (postBuild, postBuildTriggerRef) <- newEventWithTriggerRef+    events <- liftIO newChan+    (vtyResult, fc@(FireCommand fire)) <- do+      hostPerformEventT $+        flip runPostBuildT postBuild $+          flip runTriggerEventT events $+            guest+    mPostBuildTrigger <- readRef postBuildTriggerRef+    forM_ mPostBuildTrigger $ \postBuildTrigger ->+      fire [postBuildTrigger :=> Identity ()] $ return ()+    shutdown <- subscribeEvent vtyResult+    fix $ \loop -> do+      ers <- liftIO $ readChan events+      stop <- fireEventTriggerRefs fc ers $ readEvent shutdown >>= \case+        Nothing -> return False+        Just _ -> return True+      if or stop+        then return ()+        else loop+  where+    -- TODO Some part of this is probably general enough to belong in reflex+    -- | Use the given 'FireCommand' to fire events that have subscribers+    -- and call the callback for the 'TriggerInvocation' of each.+    fireEventTriggerRefs+      :: (Monad (ReadPhase m), MonadIO m)+      => FireCommand t m+      -> [DSum (EventTriggerRef t) TriggerInvocation]+      -> ReadPhase m a+      -> m [a]+    fireEventTriggerRefs (FireCommand fire) ers rcb = do+      mes <- liftIO $+        forM ers $ \(EventTriggerRef er :=> TriggerInvocation a _) -> do+          me <- readIORef er+          return $ fmap (\e -> e :=> Identity a) me+      a <- fire (catMaybes mes) rcb+      liftIO $ forM_ ers $ \(_ :=> TriggerInvocation _ cb) -> cb+      return a
+ tests/test.hs view
@@ -0,0 +1,229 @@+{-# LANGUAGE FlexibleContexts #-}+{-# LANGUAGE LambdaCase #-}+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE ScopedTypeVariables #-}+{-# LANGUAGE TemplateHaskell #-}+import HeadlessHost+import Reflex+import Reflex.Process.GHCi+import Reflex.Vty+import Reflex.Vty.GHCi+import Reflex.Workflow++import Control.Monad (void)+import Control.Monad.Fix (MonadFix)+import Control.Monad.IO.Class (liftIO, MonadIO)+import Data.ByteString (ByteString)+import qualified System.Process as P+import System.Directory+import System.Environment+import System.IO.Temp++ghciExe :: FilePath+ghciExe = "ghci"++data ExitStatus = Succeeded | Failed String+  deriving (Eq, Show)++-- Simple tests with no reloading or interrupts:+--+-- | Test                 | Module Status         | Expression Status         |+-- |----------------------|-----------------------|---------------------------|+-- | testModuleLoad       | Status_LoadSucceeded  | None                      |+-- | testModuleLoadFailed | Status_LoadFailed     | None                      |+-- | testExprErr          | Status_LoadSucceeded  | Status_ExecutionFailed    |+-- | testExprNotFound     | Status_LoadSucceeded  | Status_ExecutionFailed    |+-- | testExprFinished     | Status_LoadSucceeded  | Status_ExecutionSucceeded |++main :: IO ()+main = withSystemTempDirectory "reflex-ghci-test" $ \tmpdir -> do+  src <- getCurrentDirectory+  let cmd path load = P.proc ghciExe ["-i" <> src <> path, load]+  putStrLn "Testing lib-pkg"+  testLoadAndExecute $ cmd "/tests/lib-pkg/src" "MyLib"+  putStrLn "Testing lib-exe"+  testLoadAndExecute $ cmd "/tests/exe-pkg" "Main"+  putStrLn "Testing lib-pkg-err"+  runHeadlessApp $ do+    out <- testModuleLoadFailed $ cmd "/tests/lib-pkg-err/src" "MyLib"+    failOnError out+    exitOnSuccess out+  putStrLn "Testing file watching and reloading"+  watchAndReloadTest++failOnError+  :: ( PerformEvent t m+     , Reflex t+     )+  => Event t ExitStatus -> m ()+failOnError out = performEvent_ $ fforMaybe out $ \case+  Failed err -> Just $ error $ "Failed with: " <> err+  Succeeded -> Nothing++exitOnSuccess+  :: ( Reflex t+     , Monad m+     )+  => Event t ExitStatus+  -> m (Event t ())+exitOnSuccess out =+  return $ () <$ ffilter (==Succeeded) out++testLoadAndExecute+  :: P.CreateProcess+  -> IO ()+testLoadAndExecute cmd = runHeadlessApp $ do+  out <- switch . current <$> workflow (testModuleLoad cmd)+  performEvent_ $ liftIO . print <$> out+  failOnError out+  exitOnSuccess out++type TestWorkflow t m = Workflow t m (Event t ExitStatus)++testModuleLoad+  :: ( MonadIO m+     , TriggerEvent t m+     , PerformEvent t m+     , MonadIO (Performable m)+     , PostBuild t m+     , MonadFix m+     , MonadHold t m+     )+  => P.CreateProcess+  -> TestWorkflow t m+testModuleLoad cmd = Workflow $ do+  liftIO $ putStrLn "testModuleLoad"+  g <- ghciWatch cmd Nothing+  let loaded = fforMaybe (updated $ _ghci_status g) $ \case+        Status_LoadSucceeded -> Just True+        Status_LoadFailed -> Just False+        _ -> Nothing+  void $ shutdown $ g <$ loaded+  return ( Failed . show <$> ffilter not loaded+         , testExprErr cmd <$ ffilter id loaded+         )++testExprErr+  :: ( MonadIO m+     , TriggerEvent t m+     , PerformEvent t m+     , MonadIO (Performable m)+     , PostBuild t m+     , MonadFix m+     , MonadHold t m+     )+  => P.CreateProcess+  -> TestWorkflow t m+testExprErr cmd = Workflow $ do+  liftIO $ putStrLn "testExprErr"+  g <- ghciWatch cmd $ Just "err"+  let exception = fforMaybe (updated $ _ghci_status g) $ \case+        Status_ExecutionFailed -> Just True+        Status_ExecutionSucceeded -> Just False+        _ -> Nothing+  void $ shutdown $ g <$ exception+  return ( Failed . show <$> ffilter not exception+         , testExprNotFound cmd <$ ffilter id exception+         )++testExprNotFound+  :: ( MonadIO m+     , TriggerEvent t m+     , PerformEvent t m+     , MonadIO (Performable m)+     , PostBuild t m+     , MonadFix m+     , MonadHold t m+     )+  => P.CreateProcess+  -> TestWorkflow t m+testExprNotFound cmd = Workflow $ do+  liftIO $ putStrLn "testExprNotFound"+  g <- ghciWatch cmd $ Just "notTheFunctionYoureLookingFor"+  let exception = fforMaybe (updated $ _ghci_status g) $ \case+          Status_ExecutionFailed -> Just True+          Status_ExecutionSucceeded -> Just False+          _ -> Nothing+  void $ shutdown $ g <$ exception+  return ( Failed . show <$> ffilter not exception+         , testExprFinished cmd <$ ffilter id exception+         )++testExprFinished+  :: ( MonadIO m+     , TriggerEvent t m+     , PerformEvent t m+     , MonadIO (Performable m)+     , PostBuild t m+     , MonadFix m+     , MonadHold t m+     )+  => P.CreateProcess+  -> TestWorkflow t m+testExprFinished cmd = Workflow $ do+  liftIO $ putStrLn "testExprFinished"+  g <- ghciWatch cmd $ Just "done"+  let done = fforMaybe (updated $ _ghci_status g) $ \case+          Status_ExecutionFailed -> Just False+          Status_ExecutionSucceeded -> Just True+          _ -> Nothing+  void $ shutdown $ g <$ done+  return ( outcome done+         , never+         )++outcome :: Functor f => f Bool -> f ExitStatus+outcome = fmap (\x -> if x then Succeeded else Failed (show x))++watchAndReloadTest :: IO ()+watchAndReloadTest = withSystemTempDirectory "reflex-ghci-test" $ \p -> do+  src <- getCurrentDirectory+  P.callProcess "cp" ["-r", src <> "/tests/lib-pkg-err", p]+  let cmd = P.proc ghciExe ["-i" <> p <> "/lib-pkg-err/src", "MyLib"]+  withCurrentDirectory p $ runHeadlessApp $ do+    g <- ghciWatch cmd Nothing+    performEvent_ $ ffor (_ghci_moduleOut g) $ liftIO . print+    performEvent_ $ ffor (_ghci_moduleErr g) $ liftIO . print+    performEvent_ $ ffor (updated $ _ghci_status g) $ liftIO . print+    let loadFailed = fforMaybe (updated $ _ghci_status g) $ \case+          Status_LoadFailed -> Just ()+          _ -> Nothing+    numFailures :: Dynamic t Int <- count loadFailed+    performEvent_ $ ffor loadFailed $ \_ -> liftIO $ do+      putStrLn "copying fixed file"+      P.callProcess "cp"+        [ src <> "/tests/lib-pkg/src/MyLib/Three.hs"+        , p <> "/lib-pkg-err/src/MyLib/Three.hs"+        ]+    let loadSucceeded = fforMaybe (updated $ _ghci_status g) $ \case+          Status_LoadSucceeded -> Just ()+          _ -> Nothing+    performEvent_ $ fforMaybe (updated numFailures) $ \x ->+      if x > 1+        then Just $ error "Too many failures."+        else Nothing+    return loadSucceeded++testModuleLoadFailed+  :: ( MonadIO m+     , TriggerEvent t m+     , PerformEvent t m+     , MonadIO (Performable m)+     , PostBuild t m+     , MonadFix m+     , MonadHold t m+     )+  => P.CreateProcess+  -> m (Event t ExitStatus)+testModuleLoadFailed cmd = do+  liftIO $ putStrLn "testModuleLoadFailed"+  g <- ghciWatch cmd Nothing+  let loaded = fforMaybe (updated $ _ghci_status g) $ \case+        Status_LoadSucceeded -> Just False+        Status_LoadFailed -> Just True+        _ -> Nothing+  void $ shutdown $ g <$ loaded+  return $ ffor loaded $ \x ->+    if x+      then Succeeded+      else Failed "testModuleFailed: lib-pkg-err shouldn't have loaded"