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.
  • 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

Packaging status

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 or cabal 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 tests

Tests 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 with hpack.

Can be given multiple times.

--after-startup-ghci <GHCI_CMD>

ghci commands to run after startup

Startup 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 reload

Reload 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 reload

Reload 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 restart

The 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 restart

The 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, where target is a module path, span is a span name, and level 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 errors
  • 1: Display backtraces in errors
  • full: 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 created
  • enter: Log when spans are entered
  • exit: Log when spans are exited
  • close: Log when spans are dropped
  • none: Do not log span events
  • active: Log when spans are entered/exited
  • full: 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 SIGKILLed.

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 :adding 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