curry: a modern Emacs mode for Haskell
November 15, 2026Back in April I wrote about a month of Elisp and finished by mentioning that, having experienced neocaml (a modern Emacs mode for OCaml), I'd started writing a tree-sitter powered Emacs package for Haskell. That package is curry-mode, and I'm releasing version 0.1.0.
curry-mode is a major mode for Haskell built on Emacs's built-in tree-sitter support. It provides font-locking (Emacs-speak for syntax highlighting), indentation, navigation, imenu (the buffer index you navigate with), a REPL (read-eval-print loop) using GHCi, the interactive environment of the Glasgow Haskell Compiler (GHC), and Cabal integration, and hands everything semantic (completion, go-to-definition, types on hover, code actions) to the Haskell Language Server (HLS) via Eglot, the client for the Language Server Protocol (LSP) built into Emacs.
Why another Haskell mode?
I started out using Emacs at University on SunOS SPARC machines. At the time I learnt both of the viable editors, vi and Emacs, but leant towards Emacs because of how far you could customise it. Over the years I've mostly used other people's packages and configuration, with detours into IDEs when Java or Scala demanded it. I'd never built a large mode from scratch, and the best way I know to learn something is by doing it.
There is already an official haskell-mode with many users and many features, so why build a new one? haskell-mode has grown complex over the years, bundling everything from indentation to a REPL to HLS interaction in one monolith. It has to, since it predates both tree-sitter in Emacs and language servers. With tree-sitter built into Emacs 29+ and HLS providing the semantic features over LSP, a modern Haskell mode can be dramatically simpler.
I'm not the first person to think this. haskell-mode issue #1804 collects the attempts at a tree-sitter powered major mode for Haskell, including two separate haskell-ts-mode packages. There is erikbackman/haskell-ts-mode, which is now archived, and pranshu/haskell-ts-mode. Neither attempt became popular enough to sustain the effort, and I thought I could do a better job by following the architecture of bbatsov's neocaml.
That architecture is a mode built in layers. At the bottom, curry-mode provides the syntax layer with highlighting, indentation, navigation and imenu, all from the tree-sitter AST, and works with nothing else installed. On top of that, HLS via Eglot adds the semantic layer with completion, types, diagnostics, code actions and formatting. All the good things you expect from an LSP.
Each layer should be useful on its own and each one above improves on, or replaces, what the layer below provides. You shouldn't need a running language server before Emacs can indent your code. If things go well, perhaps some of this can be folded back into haskell-mode once it no longer needs to support Emacs versions before 29.
What's in a name
Naming is hard, and realistically this mode should be haskell-ts-mode the third, but that is a really terrible choice. Instead I named it after Haskell Curry, after whom both the language and the concept of currying are named. A delightfully intertwined name, I think.
Standing on neocaml's shoulders
I closely modelled curry-mode on neocaml (and its sibling fsharp-ts-mode), so the packages share plenty of structural and stylistic similarities. Perhaps that's not surprising, since both are statically typed languages with an ML heritage. Borrowing his good ideas let me quickly tick off the basics, like a base mode with language-specific derived modes, the REPL integration pattern, and the build tool interaction pattern. Even imenu, code folding and navigation from tree-sitter started as fairly direct translations of the neocaml code. Hopefully bbatsov is fine with that.
bbatsov has also written up Building Emacs major modes with tree-sitter: lessons learned, which covers many of the same lessons I learnt the hard way. If you're thinking of writing a tree-sitter mode, read that first. If only I'd checked his blog sooner it would have saved me some time.
So let's skip the similarities and talk about what's different about Haskell.
What you get
The 0.1.0 release covers the most common things you'd need in day to day Haskell work:
- Font-locking with 4 levels of detail for
.hs,.hscand.hs-bootfiles. - Indentation from tree-sitter rules, with region shifting (
C-c >/C-c <) and the indentation offset guessed from the existing code. - Navigation by definition (
C-M-a/C-M-e), by expression (C-M-f/C-M-b), by top-level declaration (M-a/M-eon Emacs 30+), and a jump to the import block and back (M-g i). - Imenu grouped into functions, types, classes, instances and imports.
- A GHCi REPL (
curry-repl) with tree-sitter highlighting of what you type, and commands to send definitions, regions or the whole buffer, reload, and ask for:typeand:infoat point. - Cabal integration with
curry-cabal-modefor.cabalfiles, formatting with the cabal-fmt or cabal-gild formatters, and commands to run cabal build, test, run, repl and haddock (Haskell's documentation generator) from any Haskell buffer. - GHC errors in
M-x compileoutput, soM-g n/M-g pjump to the source location. - Eglot registration for haskell-language-server.
- Debugging with dape, an Emacs debugger client, and Well-Typed's haskell-debugger (experimental, needs GHC 9.14+).
curry-mode doesn't provide completion, type lookup, Hoogle search (the Haskell API search engine), documentation browsing or code actions. HLS does those better than any Elisp I could write.
How much simpler?
I was curious whether "dramatically simpler" was true, so I counted lines. curry-mode is about 2,100 lines of Elisp against just over 20,000 for haskell-mode, roughly 10 times smaller. That number is a bit misleading, since haskell-mode includes plenty that curry-mode hands to HLS. Comparing the features both modes actually implement is a fairer comparison:
| Concern | haskell-mode | curry-mode | Ratio |
|---|---|---|---|
| Font-locking | 1,219 | 122 | ~10x |
| Indentation | 2,870 | 190 | ~15x |
| REPL | 2,019 | 357 | ~6x |
| Cabal | 1,228 | 611 | ~2x |
| Mode, navigation, imenu | 1,874 | 837 | ~2x |
Tree-sitter shows up clearly in the first two rows. haskell-mode includes a hand-written lexer (haskell-lexeme.el) that copes with the layout rule, string gaps, character literals and pragmas, and adds font-lock keywords on top of it. curry-mode asks the tree-sitter syntax tree instead, and around 120 lines of treesit-font-lock-rules cover all four levels. For indentation, haskell-mode ships two indenters (haskell-indent and haskell-indentation), because Haskell layout is hard enough that neither one alone was enough. curry-mode has about 190 lines of treesit-simple-indent-rules. Again there is a gap in functionality in the indentation but really I think most developers would be better served by an external tool.
The REPL and Cabal savings come from leaning on what Emacs already provides (comint, compile) and on external tools like cabal-fmt, rather than from tree-sitter.
Working with tree-sitter
Most of the size difference comes from tree-sitter. Once you have a syntax tree, font-locking is a set of queries (patterns matched against nodes in the tree) rather than a pile of regular expressions, and the same tree gives you navigation, imenu, folding and Combobulate support with little extra code. Combobulate is a package for structured navigation and editing that works across any language with a tree-sitter grammar. It also makes new features easier to add. Want to find all the tests in a file using the Tasty test framework? That's a query over the tree, not a parser you write yourself.
The Haskell grammar story is a little fragmented, though. The official tree-sitter/tree-sitter-haskell is missing many bug fixes and newer GHC features. tek/tree-sitter-haskell applies most of those fixes and is active, but doesn't commit the generated C parser that Emacs needs to build the grammar. So curry-mode uses my fork of tek's, which adds the generated parser files, and installs it for you. I chose tek's grammar because it has the best language coverage, but I'd love for the fork to become unnecessary.
That leads to my bigger point, which is that a tree-sitter grammar is no longer optional for a language. GitHub code navigation, Emacs, Neovim, Helix, Zed and plenty of other tools are built on tree-sitter, so a good grammar matters as much as a good standard library or build tool. Ideally the grammar would track each GHC release, or even be mechanically derived from GHC's own parser. GHC Haskell keeps growing new syntax, and keeping editors working across versions raises the same problems that Trees that Grow, the extensible AST design used inside GHC, tackles for the compiler.
I hit a few rough edges along the way. Tree-sitter ABI (application binary interface) versions are confusing. Emacs often supports ABI 14 as the lowest version, but you can build grammars with ABI 15, and since ABI 15 isn't backwards compatible with 14, loading the wrong one will SEGV your Emacs. curry-mode now checks the version of libtree-sitter (the tree-sitter C library Emacs is linked against) and gives a clear error instead. Grammar releases also tend to follow the tree-sitter tooling version rather than semver, so a minor version bump can bring major headaches. Finally, with tree-sitter the AST is your API, and an AST is never going to be particularly stable, so curry-mode warns you when the installed grammar is older than the one it expects.
Indentation
Haskell is whitespace sensitive, which makes indentation the hardest part of any Haskell mode. Unlike most languages, there is often more than one valid indentation for a line, and they mean different things. The first rule for curry-mode is to do no harm, so indenting a line should never break a program that was already correct. Beyond that, the tree-sitter rules aim to cover the common layouts like do, where, let, case, guards, records and the rest of the everyday code I write. Where the rules get it wrong there is an escape hatch. curry-cycle-indent switches to indent-relative, which lines up with the previous line, and region shifting moves a block by one level.
Some cases are still hard (NondecreasingIndentation, CPP blocks, and edits that change the parse tree as you type), and the roadmap lists them as known issues. Getting a whole file into the right shape is a job for a formatter like ormolu or fourmolu, which you can already run through HLS with eglot-format-buffer. This is another place where curry-mode provides a usable base and leaves the heavy lifting to the tool that's best at it.
Trying it out
curry-mode needs Emacs 29.1 or newer, built against libtree-sitter 0.25 or newer. On Emacs 30 you can install it from GitHub with use-package:
(use-package curry-mode
:vc (:url "https://github.com/tmcgilchrist/curry-mode" :rev :newest))
On Emacs 29, use M-x package-vc-install RET https://github.com/tmcgilchrist/curry-mode.
The Haskell tree-sitter grammar is installed automatically the first time you open a Haskell file, or you can run M-x curry-install-grammars yourself. For the semantic features, install HLS (for example with ghcup install hls) and run M-x eglot in a Haskell buffer.
The README has the details on key bindings, font-lock levels and configuration options.
What's next?
How mature is curry-mode? It covers what I need for my Haskell work, and I've been using it daily. The grammar could use polish to keep up with the ever-growing syntax of GHC Haskell, the font-locking needs tuning to look just right, and I'm sure there are corners of Haskell I haven't touched. The roadmap collects ideas for where it could go next, from formatter integration to running the right test from point. I also want to build out the higher layers. I have a branch adding Combobulate support for Haskell (PR #12), which adds structured editing on top of the same syntax tree, and debugging with dape needs more use before it moves past experimental.
Most of the hacking for this release is done, and I'm going to spend time on other projects for a while. Perhaps people will find curry-mode useful and borrow the code for haskell-mode, or enough people will use it that it lives on as a modern Haskell mode for Emacs. Please try it out and report issues for anything that breaks.
Contributions, bug reports, suggestions and feedback are most welcome. Remember, Emacs can be whatever you make of it.