ghciwatch
Ghciwatch loads a GHCi session for a Haskell project and reloads it when source files change.
Features
- GHCi output is displayed to the user as soon as it’s printed.
- Ghciwatch can handle new modules, removed modules, or moved modules without a hitch
- A variety of lifecycle hooks let you run Haskell code
or shell commands on a variety of events.
- Run a test suite with
--test-ghci TestMain.testMain
. - Refresh your
.cabal
files withhpack
before GHCi starts using--before-startup-shell hpack
. - Format your code asynchronously using
--before-reload-shell async:fourmolu
.
- Run a test suite with
- Custom globs can be supplied to reload or restart the GHCi session when non-Haskell files (like templates or database schema definitions) change.
- Ghciwatch can clear the screen between reloads.
- Compilation errors can be written to a file with
--error-file
, for compatibility with ghcid’s--outputfile
option. - Comments starting with
-- $>
can be evaluated in GHCi.- Eval comments have access to the top-level bindings of the module they’re defined in, including unexported bindings.
- Multi-line eval comments are supported with
{- $> ... <$ -}
.
Demo
Check out an asciinema demo to see how ghciwatch feels in practice:
Installation
Nixpkgs
Ghciwatch is available in nixpkgs
as ghciwatch
:
nix-env -iA ghciwatch
nix profile install nixpkgs#ghciwatch
# Or add to your `/etc/nixos/configuration.nix`.
Statically-linked binaries
Statically-linked binaries for aarch64/x86_64 macOS/Linux can be downloaded from the GitHub releases.
Crates.io
The Rust crate can be downloaded from crates.io:
cargo install ghciwatch
Hackage
Ghciwatch is not yet available on Hackage; see issue #23.
Getting started
To start a ghciwatch session, you’ll need a command to start a GHCi session
(like cabal repl
) and a set of paths and directories to watch for changes.
For example:
ghciwatch --command "cabal repl lib:test-dev" \
--watch src --watch test
Check out the examples and command-line arguments for more information.
Ghciwatch can run test suites after reloads, evaluate
code in comments, log compiler errors to a
file, run startup hooks
like hpack
to generate .cabal
files, and more!
Command-line arguments for ghciwatch
Ghciwatch loads a GHCi session for a Haskell project and reloads it when source files change.
Usage: ghciwatch [--command SHELL_COMMAND] [--watch PATH] [OPTIONS ...]
Examples
Load cabal v2-repl
and watch for changes in src
:
ghciwatch
Load a custom GHCi session and watch for changes in multiple locations:
ghciwatch --command "cabal v2-repl lib:test-dev" \
--watch src --watch test
Run tests after reloads:
ghciwatch --test-ghci TestMain.testMain \
--after-startup-ghci ':set args "--match=/OnlyRunSomeTests/"'
Use hpack
to regenerate .cabal
files:
ghciwatch --before-startup-shell hpack \
--restart-glob '**/package.yaml'
Also reload the session when .persistentmodels
change:
ghciwatch --watch config/modelsFiles \
--reload-glob '**/*.persistentmodels'
Don’t reload for README.md
files:
ghciwatch --reload-glob '!src/**/README.md'
Arguments
<FILE>
-
A Haskell source file to load into a GHCi REPL.
Shortcut for
--command 'ghci PATH'
. Conflicts with--command
.
Options
--command <SHELL_COMMAND>
-
A shell command which starts a GHCi REPL, e.g.
ghci
orcabal v2-repl
or similar.This is used to launch the underlying GHCi session that
ghciwatch
controls.May contain quoted arguments which will be parsed in a
sh
-like manner. --error-file <ERROR_FILE>
-
A file to write compilation errors to.
The output format is compatible with
ghcid
’s--outputfile
option. --enable-eval
-
Evaluate Haskell code in comments.
This parses line commands starting with
-- $>
or multiline commands delimited by{- $>
and<$ -}
and evaluates them after reloads. --clear
-
Clear the screen before reloads and restarts
--no-interrupt-reloads
-
Don’t interrupt reloads when files change.
Depending on your workflow,
ghciwatch
may feel more responsive with this set. --completions <COMPLETIONS>
-
Generate shell completions for the given shell
Possible values:
bash
,elvish
,fish
,powershell
,zsh
Lifecycle hooks
--test-ghci <GHCI_CMD>
-
ghci
commands to run testsTests are run after startup and after reloads.
Example:
TestMain.testMain
.Can be given multiple times.
--test-shell <SHELL_CMD>
-
Shell commands to run tests
Tests are run after startup and after reloads.
Commands starting with
async:
will be run in the background.Can be given multiple times.
--before-startup-shell <SHELL_CMD>
-
Shell commands to run before startup
Startup hooks run when GHCi is started (at
ghciwatch
startup and after GHCi restarts).Commands starting with
async:
will be run in the background.This can be used to regenerate
.cabal
files withhpack
.Can be given multiple times.
--after-startup-ghci <GHCI_CMD>
-
ghci
commands to run after startupStartup hooks run when GHCi is started (at
ghciwatch
startup and after GHCi restarts).Use
:set args ...
to set command-line arguments for test hooks.Can be given multiple times.
--after-startup-shell <SHELL_CMD>
-
Shell commands to run after startup
Startup hooks run when GHCi is started (at
ghciwatch
startup and after GHCi restarts).Commands starting with
async:
will be run in the background.Can be given multiple times.
--before-reload-ghci <GHCI_CMD>
-
ghci
commands to run before reloadReload hooks are run when modules are changed on disk.
Can be given multiple times.
--before-reload-shell <SHELL_CMD>
-
Shell commands to run before reload
Reload hooks are run when modules are changed on disk.
Commands starting with
async:
will be run in the background.Can be given multiple times.
--after-reload-ghci <GHCI_CMD>
-
ghci
commands to run after reloadReload hooks are run when modules are changed on disk.
Can be given multiple times.
--after-reload-shell <SHELL_CMD>
-
Shell commands to run after reload
Reload hooks are run when modules are changed on disk.
Commands starting with
async:
will be run in the background.Can be given multiple times.
--before-restart-ghci <GHCI_CMD>
-
ghci
commands to run before restartThe GHCi session must be restarted when
.cabal
or.ghci
files are modified.Can be given multiple times.
--before-restart-shell <SHELL_CMD>
-
Shell commands to run before restart
The GHCi session must be restarted when
.cabal
or.ghci
files are modified.Commands starting with
async:
will be run in the background.Can be given multiple times.
--after-restart-ghci <GHCI_CMD>
-
ghci
commands to run after restartThe GHCi session must be restarted when
.cabal
or.ghci
files are modified.Can be given multiple times.
--after-restart-shell <SHELL_CMD>
-
Shell commands to run after restart
The GHCi session must be restarted when
.cabal
or.ghci
files are modified.Commands starting with
async:
will be run in the background.Can be given multiple times.
File watching options
--poll <DURATION>
-
Use polling with the given interval rather than notification-based file watching.
Polling tends to be more reliable and less performant. In particular, notification-based watching often misses updates on macOS.
--debounce <DURATION>
-
Debounce file events; wait this duration after receiving an event before attempting to reload.
Defaults to 0.5 seconds.
Default value:
500ms
--watch <PATH>
-
A path to watch for changes.
Directories are watched recursively. Can be given multiple times.
--reload-glob <RELOAD_GLOBS>
-
Reload the GHCi session when paths matching this glob change.
By default, only changes to Haskell source files trigger reloads. If you’d like to exclude some files from that, you can add an ignore glob here, like
!src/my-special-dir/**/*.hs
.Globs provided here have precisely the same semantics as a single line in a
gitignore
file (man gitignore
), where the meaning of!
is inverted: namely,!
at the beginning of a glob will ignore a file.The last matching glob will determine if a reload is triggered.
Can be given multiple times.
--restart-glob <RESTART_GLOBS>
-
Restart the GHCi session when paths matching this glob change.
By default, only changes to
.cabal
or.ghci
files will trigger restarts.See
--reload-globs
for more details.Can be given multiple times.
Logging options
--log-filter <LOG_FILTER>
-
Log message filter.
Can be any of “error”, “warn”, “info”, “debug”, or “trace”. Supports more granular filtering, as well.
The grammar is:
target[span{field=value}]=level
, wheretarget
is a module path,span
is a span name, andlevel
is one of the levels listed above.See documentation in
tracing-subscriber
.A nice value is
ghciwatch=debug
.Default value:
ghciwatch=info
--backtrace <BACKTRACE>
-
How to display backtraces in error messages
Default value:
0
Possible values:
0
: Hide backtraces in errors1
: Display backtraces in errorsfull
: Display backtraces with all stack frames in errors
--trace-spans <TRACE_SPANS>
-
When to log span events, which loosely correspond to tasks being run in the async runtime.
Allows multiple values, comma-separated.
Default value:
new,close
Possible values:
new
: Log when spans are createdenter
: Log when spans are enteredexit
: Log when spans are exitedclose
: Log when spans are droppednone
: Do not log span eventsactive
: Log when spans are entered/exitedfull
: Log all span events
--log-json <PATH>
-
Path to write JSON logs to.
JSON logs are not yet stable and the format may change on any release.
Lifecycle hooks
Ghciwatch supports a number of lifecycle hook options
like --test-ghci
,
--before-startup-shell
, and
--after-restart-ghci
.
Lifecycle hooks can be defined multiple times and run in sequence. For example:
ghciwatch --test-ghci TestMain.testMain \
--test-ghci 'if myGreeting /= "Hello, world!" then error else ()'
This command will first run TestMain.testMain
and then the check for
myGreeting
.
Types of hooks
Lifecycle hooks come in two main variants: shell commands and GHCi commands.
GHCi commands
GHCi lifecycle hook options (like --test-ghci
and
--after-startup-ghci
) end in -ghci
and
define a command to be executed in the GHCi session.
When running a test suite, you can use a hook like --after-startup-ghci ':set args "--match=/MyModule/"'
to filter HSpec items or otherwise
set command-line arguments for the test suite.
Note that any GHCi command is allowed, so there’s nothing to stop you from
setting a hook like :set prompt λ>
that breaks ghciwatch’s ability to detect
when reloads are complete.
Output printed by GHCi, including by GHCi lifecycle hooks, is printed to ghciwatch’s stdout.
Shell commands
Shell lifecycle hook options (like --test-shell
) end
in -shell
and define a shell command to be executed.
Arguments can be quoted with standard sh
syntax as defined in POSIX.1-2008
§2.2 (however, note that no variable expansion is performed).
If a shell lifecycle hook begins with async:
, as in --after-reload-shell 'async:tags'
, the command will be run asynchronously and ghciwatch will
continue to execute as normal.
If a shell lifecycle hook fails (exits with a non-zero status code), a message indicating the command that failed and the contents of its standard output and standard error streams will be printed.
Detecting if code is running in ghciwatch
Before launching the GHCi session, ghciwatch sets the IN_GHCIWATCH
environment variable. GHCi and shell command lifecycle hooks can read this
environment variable to determine if they’re being run inside a ghciwatch session.
This is particularly useful for code which may be compiled, run in a plain
ghci
session, or run in a ghciwatch-managed GHCi session.
List of lifecycle hooks
Before startup
Hook: --before-startup-shell
.
When: Before the --command
is executed to spawn a GHCi
session.
No GHCi session exists when this hook is run, so only a shell hook is available.
Good for running tools like hpack
to generate .cabal
files.
After startup
Hooks: --after-startup-shell
,
--after-startup-ghci
.
When: After the --command
executed to spawn a GHCi
session has finished loading and the error log has been
written, but before eval commands and test
suites are executed.
Test
Hooks: --test-shell
,
--test-ghci
.
When: After the GHCi session starts up or a reload or restart completes.
Note that if compilation fails, test suites and eval commands will not run.
Before reload
Hooks: --before-reload-shell
,
--before-reload-ghci
.
When: After file changes are detected but before a :reload
or :add
command
is sent to the GHCi session.
Note that the before-reload hooks are not executed before a restart.
After reload
Hooks: --after-reload-shell
,
--after-reload-ghci
.
When: After a reload has completed, after the error log has been written, but before eval commands and test suites are executed.
Before restart
Hooks: --before-restart-shell
,
--before-restart-ghci
.
When: After file changes that require a restart are detected but before the
GHCi session is SIGKILL
ed.
The GHCi session is restarted when .cabal
files change, when Haskell modules
are deleted or moved, or when any files specified by
--restart-globs
are changed.
After restart
Hooks: --after-restart-shell
,
--after-restart-ghci
.
When: After the GHCi session has been restarted, the error log has been written, and the after startup hooks have run, but before eval commands and test suites are executed.
Comment evaluation
With the --enable-eval
flag set, ghciwatch will
execute Haskell code in comments which start with $>
in GHCi.
myGreeting :: String
myGreeting = "Hello"
-- $> putStrLn (myGreeting <> " " <> myGreeting)
Prints:
• src/MyLib.hs:9:7: putStrLn (myGreeting <> " " <> myGreeting)
Hello Hello
Running tests with eval comments
Eval comments can be used to run tests in a single file on reload. For large
test suites (thousands of tests), this can be much faster than using Hspec’s
--match
option, because --match
has to load the entire test
suite and perform string matches on [Char]
to determine which tests should be
run. (Combine this with Cabal’s --repl-no-load
option to only
load the modules your test depends on for even faster reloads.)
module MyLibSpec (spec) where
import Test.Hspec
import MyLib (myGreeting)
-- $> import Test.Hspec -- May be necessary for some setups.
-- $> hspec spec
spec :: Spec
spec = do
describe "myGreeting" $ do
it "is hello" $ do
myGreeting `shouldBe` "Hello"
Grammar
Single-line eval comments have the following grammar:
[ \t]* # Leading whitespace
"-- $>" # Eval comment marker
[ \t]* # Optional whitespace
[^\n]+ \n # Rest of line
Multi-line eval comments have the following grammar:
[ \t]* # Leading whitespace
"{- $>" # Eval comment marker
([ \t]* \n)? # Optional newline
([^\n]* \n)* # Lines of Haskell code
[ \t]* # Optional whitespace
"<$ -}" # Eval comment end marker
Performance implications
Note that because each loaded module must be read (and re-read when it changes) to parse eval comments, enabling this feature has some performance overhead. (It’s probably not too bad, because all those files are in your disk cache anyways from being compiled by GHCi.)
Only load modules you need
TL;DR: Use cabal repl --repl-no-load
to start a GHCi
session with no modules loaded. Then, when you edit a module, ghciwatch will
:add
it to the GHCi session, causing only the modules you need (and their
dependencies) to be loaded. In large projects, this can significantly cut down
on reload times.
--repl-no-load
in ghciwatch
Ghciwatch supports --repl-no-load
natively. Add --repl-no-load
to the
ghciwatch --command
option and ghciwatch will start a
GHCi session with no modules loaded. Then, edit a file and ghciwatch will load
it (and its dependencies) into the REPL. (Note that because no modules are
loaded initially, no compilation errors will show up until you start writing
files.)
--repl-no-load
explained
When you load a GHCi session with cabal repl
, Cabal will
interpret and load all the modules in the specified target before presenting a
prompt:
$ cabal repl test-dev
Build profile: -w ghc-9.0.2 -O1
In order, the following will be built (use -v for more details):
- my-simple-package-0.1.0.0 (lib:test-dev) (first run)
Configuring library 'test-dev' for my-simple-package-0.1.0.0..
Preprocessing library 'test-dev' for my-simple-package-0.1.0.0..
GHCi, version 9.0.2: https://www.haskell.org/ghc/ :? for help
[1 of 3] Compiling MyLib ( src/MyLib.hs, interpreted )
[2 of 3] Compiling MyModule ( src/MyModule.hs, interpreted )
[3 of 3] Compiling TestMain ( test/TestMain.hs, interpreted )
Ok, three modules loaded.
ghci>
For this toy project with three modules, that’s not an issue, but it can start to add up with larger projects:
$ echo :quit | time cabal repl
...
Ok, 9194 modules loaded.
ghci> Leaving GHCi.
________________________________________________________
Executed in 161.07 secs
Fortunately, cabal repl
includes a --repl-no-load
option
which instructs Cabal to skip interpreting or loading any modules until it’s
instructed to do so:
$ echo ":quit" | time cabal repl --repl-no-load
...
ghci> Leaving GHCi.
________________________________________________________
Executed in 11.41 secs
Then, you can load modules into the empty GHCi session by :add
ing them, and
only the specified modules and their dependencies will be interpreted. If you
only need to edit a small portion of a library’s total modules, this can
provide a significantly faster workflow than loading every module up-front.
FAQ
Why a reimplementation?
Ghciwatch started out as a reimplementation and reimagination of
ghcid
, a similar tool with smaller scope. When we started working on
ghciwatch, ghcid
suffered from some significant limitations. In particular,
ghcid
couldn’t deal with moved or deleted modules, and wouldn’t detect new
directories because it can’t easily update the set of files being watched at
runtime. We’ve also seen memory leaks requiring multiple restarts
per day. Due to the ghcid
codebase’s relatively small size, a
reimplementation seemed like a more efficient path forward than making
wide-spanning changes to an unfamiliar codebase.
Why not just use watchexec
or similar?
TL;DR: Managing a GHCi session is often faster than recompiling a project with
cabal
or similar.
Recompiling a project when files change is a fairly common development task, so
there’s a bunch of tools with this same rough goal. In particular,
[watchexec
][watchexec] is a nice off-the-shelf solution. Why not just run
watchexec -e hs cabal build
? In truth, ghciwatch doesn’t just recompile the
project when it detects changes. It instead manages an interactive GHCi
session, instructing it to reload modules when relevant. This involves a fairly
complex dance of communicating to GHCi over stdin and parsing its stdout, so
a bespoke tool is useful here.
Tasty
Tips and tricks for using ghciwatch with the Tasty test framework.
Ghciwatch will wait for GHCi to print output, and it can end up waiting forever if the Tasty output is buffered. Something like this works:
module TestMain where
import Control.Exception (bracket)
import System.IO (hGetBuffering, hSetBuffering, stdout)
import Test.Tasty (TestTree, defaultMain, testGroup)
-- | Run an `IO` action, restoring `stdout`\'s buffering mode after the action
-- completes or errors.
protectStdoutBuffering :: IO a -> IO a
protectStdoutBuffering action =
bracket
(hGetBuffering stdout)
(\bufferMode -> hSetBuffering stdout bufferMode)
(const action)
main :: IO ()
main = protectStdoutBuffering $ defaultMain $ mytestgroup
tasty-discover
issues
If you add a new test file, you may need to write the top level
tasty-discover
module to convince ghciwatch to reload it.
tasty-autocollect
relies on a compiler plugin and seems
to avoid this problem.
Multiple Cabal components
Currently, multiple Cabal components don’t work. You can work around this with
the Cabal test-dev
trick as described by the venerable Jade
Lovelace. This works by defining a new component in our .cabal
file which
includes the sources from the library and the tests, which has the added
benefit of speeding up compile times by allowing the compilation of different
components to be interleaved.
You can see this demonstrated in the ghciwatch test sources here. We define four components:
library
tests
- An internal
test-lib
library - An internal
test-dev
library
Then, we can use a command like cabal v2-repl test-dev
to run a GHCi session
containing both the library and test sources.
The package.yaml
should look something like this:
---
spec-version: 0.36.0
name: my-simple-package
version: 0.1.0.0
flags:
local-dev:
description: Turn on development settings, like auto-reload templates.
manual: true
default: false
library:
source-dirs: src
tests:
test:
main: Main.hs
source-dirs:
- test-main
ghc-options: -threaded -rtsopts -with-rtsopts=-N
when:
- condition: flag(local-dev)
then:
dependencies:
- test-dev
else:
dependencies:
- my-simple-package
- test-lib
internal-libraries:
test-lib:
source-dirs:
- test
test-dev:
source-dirs:
- test
- src
when:
- condition: flag(local-dev)
then:
buildable: true
else:
buildable: false
Then, we can set the local-dev
flag in our cabal.project.local
, so that we
use the test-dev
target locally:
package my-simple-package
flags: +local-dev
haskell-language-server
Defining the test-dev
component does tend to confuse
haskell-language-server
, as a single file is now in multiple components. Fix
this by writing an hie.yaml
like this:
cradle:
cabal:
component: test-dev