curry-mode is a modern, tree-sitter-powered Emacs major mode for Haskell. It
provides font-lock, indentation, navigation, imenu, GHCi REPL integration, and
Cabal build commands. Delegating all semantic features (completion,
go-to-definition, type info, code actions) to HLS via Eglot.
Because haskell-mode has grown complex over the years, bundling everything
from indentation to REPL to HLS interaction in one monolith. With tree-sitter
built into Emacs 29+ and HLS providing all semantic features via LSP, a modern
Haskell mode can be dramatically simpler.
The architecture follows neocaml and fsharp-ts-mode: a thin tree-sitter mode for syntax, with everything semantic delegated to the language server.
- Tree-sitter based font-locking (4 levels) for
.hs,.lhs,.hsc, and.hs-bootfiles - Tree-sitter based indentation with cycle-indent and shift-region support
- Auto-detection of indentation offset from existing code
- Navigation (
beginning-of-defun,end-of-defun,forward-sexp, sentence movement withM-a/M-e) - Imenu with categories for Functions, Types, Classes, Instances, and Imports
- Toggling between
.hsand.lhsviaff-find-other-file(C-c C-a) - GHCi REPL integration (
curry-repl) with tree-sitter input fontification - Comment support:
fill-paragraph(M-q), comment continuation (M-j), andcomment-dwim(M-;) - Electric indentation and electric pairs (parentheses, brackets, braces, backticks, quotes)
- Cabal file editing (
curry-cabal-mode) with font-lock, indentation, imenu, andcabal-fmtintegration - Cabal build commands (
curry-cabal-interaction-mode) -- build, test, run, clean, repl, haddock - Easy installation of the Haskell tree-sitter grammar via
M-x curry-install-grammars - Compilation error regexp for GHC output (
M-g n/M-g p) - Eglot integration (auto-configured for
haskell-language-server-wrapper) - Debugging via dape + hdb (experimental, GHC 9.14+)
- Prettify-symbols for common Haskell operators
Requires Emacs 29.1+ (for :vc support in use-package).
(use-package curry-mode
:vc (:url "https://github.com/tmcgilchrist/curry-mode" :rev :newest))curry-mode uses a fork of the Haskell tree-sitter grammar with ABI 14 compatibility and bug fixes from tek/tree-sitter-haskell. The grammar is installed automatically when needed, or manually via:
M-x curry-install-grammars
curry-mode activates automatically for .hs, .lhs, .hsc, and .hs-boot
files. curry-cabal-mode activates for .cabal files.
To use with Eglot, ensure haskell-language-server-wrapper is on your PATH
(install via ghcup):
ghcup install hls
Then start Eglot with M-x eglot in any Haskell buffer.
C-c C-c runs M-x compile, and curry-mode registers a GHC-specific error
regexp so that next-error (M-g n) and previous-error (M-g p) jump
directly to the source locations reported by the compiler.
curry-mode uses tree-sitter to power all structural navigation commands. These are standard Emacs keybindings, backed by the AST rather than heuristics:
| Keybinding | Command | Description |
|---|---|---|
C-M-a |
beginning-of-defun |
Move to the beginning of the current definition |
C-M-e |
end-of-defun |
Move to the end of the current definition |
C-M-f |
forward-sexp |
Move forward over a balanced expression |
C-M-b |
backward-sexp |
Move backward over a balanced expression |
M-a |
backward-sentence |
Move to the previous top-level declaration (Emacs 30+) |
M-e |
forward-sentence |
Move to the next top-level declaration (Emacs 30+) |
M-g i |
curry-jump-to-imports |
Jump to the import block; toggle back on second invocation |
"Definitions" include function bindings, type signatures, data types, newtypes,
type synonyms, classes, instances, and imports. "Statements" (sentences) cover
the same set, enabling M-a/M-e to jump between top-level declarations.
All navigation commands are also available from the Haskell menu.
curry-mode provides 4 levels of font-locking, as is standard for tree-sitter modes. The default level in Emacs is 3, and you can change it like this:
;; this font-locks everything curry-mode supports
(setq treesit-font-lock-level 4)The font-lock features available at each level are:
Level 1 (minimal -- comments and definitions):
comment-- line comments, block comments, haddock comments, pragmas, CPP directivesdefinition-- function declarations, type signatures, binding names, function parameters, lambda parameters
Level 2 (add keywords, strings, numbers):
keyword-- language keywords:let,where,do,case,of,class,instance,data,import, ...string-- string and character literalsnumber-- integer, float, and negation literals
Level 3 (default -- types, constructors, modules):
type-- type constructors, type variables, star kindconstructor-- data constructors,True/False,otherwise, unit()module-- module names in module declarations and imports
Level 4 (maximum detail):
operator-- operators, constructor operators, special symbols (::/->/=>/<-/=/|)variable-- variables and wildcards in patterns, record field namesfunction-- function application, infix function application, builtin functions, quasi-quotersbracket-- parentheses, brackets, bracesdelimiter-- commas and semicolons
You don't have to use the level system. If you want fine-grained control over
what gets highlighted, cherry-pick individual features using
treesit-font-lock-recompute-features:
(defun my-curry-font-lock-setup ()
(treesit-font-lock-recompute-features
;; enable these features
'(comment definition keyword string number
type constructor module
operator variable function)
;; disable these features
'(bracket delimiter)))
(add-hook 'curry-base-mode-hook #'my-curry-font-lock-setup)The faces used are standard font-lock-*-face faces, so any theme applies
automatically. For buffer-local customization (only affects Haskell buffers):
(add-hook 'curry-base-mode-hook
(lambda ()
(face-remap-add-relative 'font-lock-type-face
:foreground "DarkSeaGreen4")))For distinctions that curry-mode doesn't make by default, layer additional tree-sitter font-lock rules via a hook:
(defface my-haskell-keyword-face
'((t :inherit font-lock-keyword-face :weight bold))
"Face for certain Haskell keywords.")
(defun my-curry-extra-keywords ()
(setq treesit-font-lock-settings
(append treesit-font-lock-settings
(treesit-font-lock-rules
:language 'haskell
:override t
:feature 'keyword
'(["where" "let" "in" "do"]
@my-haskell-keyword-face))))
(treesit-font-lock-recompute-features))
(add-hook 'curry-base-mode-hook #'my-curry-extra-keywords)Use M-x treesit-explore-mode to inspect the syntax tree and find the right
node types to match.
You can "prettify" certain Haskell operators by enabling prettify-symbols-mode:
(add-hook 'curry-base-mode-hook #'prettify-symbols-mode)The default replacements are: \ to lambda, -> to arrow, <- to leftarrow,
=> to double arrow, /= to not equal, >=/<= to ge/le, == to equiv,
&&/|| to and/or.
For additional word-level prettification (forall to for all, undefined to bottom):
(setq curry-prettify-words t)Haskell is layout-sensitive: indentation is part of the syntax. This creates a fundamental tension with tree-sitter indentation -- the parser needs correct indentation to produce the right parse tree, but the indenter needs the parse tree to compute correct indentation.
curry-mode handles this pragmatically:
- Newline indentation works well: pressing Enter after
do,where,=,->,case ... of, etc. puts the cursor at the right column. - Re-indenting from scratch (e.g.
indent-regionon fully stripped code) is inherently limited for layout-dependent constructs. - Round-trip preservation works: correctly-indented code stays correctly-indented when re-indented.
For more details on why this is hard, see Batsov's F# indentation analysis.
The default indentation offset is 2 spaces. curry-mode auto-detects the offset
from existing files on open (controlled by curry-guess-indent-offset). You can
also set it explicitly:
(setq curry-indent-offset 4)Or per-project via .dir-locals.el:
((curry-mode (curry-indent-offset . 4)))For manual indentation adjustment in layout-sensitive code:
| Keybinding | Command | Description |
|---|---|---|
C-c > |
curry-shift-region-right |
Indent region by one offset level |
C-c < |
curry-shift-region-left |
Dedent region by one offset level |
You can toggle between tree-sitter indentation and indent-relative using
M-x curry-cycle-indent (also available from the Haskell menu). This is handy
when the tree-sitter indentation doesn't do what you want for a particular piece
of code.
Haskell uses -- line comments and {- ... -} block comments. curry-mode
configures all the necessary variables so Emacs comment commands work out of the
box:
M-;(comment-dwim) -- comments/uncomments regions, inserts inline commentsM-j(default-indent-new-line) -- inside a block comment, inserts a newline and aligns the continuation with the comment body textM-q(fill-paragraph) -- refills the current comment, wrapping text atfill-columnwith proper indentation. Respects{- -},-- |(haddock), and{-# #-}(pragma) delimiters.
On Emacs 30+, outline-minor-mode works out of the box -- it automatically
picks up definition headings from the tree-sitter parse tree. Enable it via a
hook:
(add-hook 'curry-base-mode-hook #'outline-minor-mode)For tree-sitter-aware code folding (fold any node, not just top-level
definitions), treesit-fold
is supported via curry-mode-treesit-fold-setup, which registers curry-mode
with treesit-fold's Haskell fold definitions:
(use-package treesit-fold
:ensure t
:hook (curry-base-mode . treesit-fold-mode)
:config (curry-mode-treesit-fold-setup))expreg provides expand-region-style selection that leverages tree-sitter for language-aware expansion:
(use-package expreg
:ensure t
:bind (("C-=" . expreg-expand)
("C--" . expreg-contract)))curry-repl-minor-mode provides interaction with GHCi from source buffers.
Enable it via a hook:
(add-hook 'curry-base-mode-hook #'curry-repl-minor-mode)The following keybindings are available when curry-repl-minor-mode is active:
Note:
C-c C-cis bound tocompilein the base mode. Whencurry-repl-minor-modeis enabled, it is rebound tocurry-repl-send-definition.
| Keybinding | Command | Description |
|---|---|---|
C-c C-z |
curry-repl-switch-to-repl |
Start GHCi or switch to it |
C-c C-c |
curry-repl-send-definition |
Send the current definition |
C-c C-r |
curry-repl-send-region |
Send the selected region |
C-c C-b |
curry-repl-send-buffer |
Send the entire buffer |
C-c C-l |
curry-repl-load-file |
Load current file (:load) |
C-c C-k |
curry-repl-reload |
Reload current module (:reload) |
C-c C-t |
curry-repl-type-at-point |
Show type (:type) |
C-c C-i |
curry-repl-info-at-point |
Show info (:info) |
By default, code you type in the REPL is fontified using tree-sitter via
comint-fontify-input-mode, giving you the same syntax highlighting as in
regular .hs buffers. To disable this:
(setq curry-repl-fontify-input nil);; Use cabal repl instead of bare ghci
(setq curry-repl-program-name "cabal repl")
;; Or use stack
(setq curry-repl-program-name "stack ghci")
;; Change the REPL buffer name
(setq curry-repl-buffer-name "*My-GHCi*")curry-cabal-mode uses a fork of the
magus/tree-sitter-cabal grammar,
maintained at
tmcgilchrist/tree-sitter-cabal.
The grammar is installed automatically the first time you open a .cabal
file, or manually via:
M-x curry-cabal-install-grammar
curry-cabal-mode activates automatically for .cabal files and provides
tree-sitter based font-lock, indentation, imenu (Library, Executable, Test
Suite, Benchmark, Common, Flag, Source Repository), and formatting via
cabal-fmt.
| Keybinding | Command | Description |
|---|---|---|
C-c C-f |
curry-cabal-format-buffer |
Format with cabal-fmt |
To enable automatic formatting on save:
(setq curry-cabal-format-on-save t)curry-cabal-interaction-mode is a minor mode that provides keybindings for
running common cabal commands from any Haskell buffer. All commands run via
compile, so you get error navigation and clickable source locations.
Enable it in Haskell buffers:
(add-hook 'curry-base-mode-hook #'curry-cabal-interaction-mode)Available commands (all under the C-c C-d prefix):
| Keybinding | Command | Description |
|---|---|---|
C-c C-d b |
curry-cabal-build |
Build default target |
C-c C-d B |
curry-cabal-build-all |
Build all targets |
C-c C-d t |
curry-cabal-test |
Run all tests |
C-c C-d r |
curry-cabal-run |
Run executable (prompts) |
C-c C-d c |
curry-cabal-clean |
Clean build artifacts |
C-c C-d i |
curry-cabal-repl |
Launch cabal repl |
C-c C-d h |
curry-cabal-haddock |
Generate documentation |
C-c C-d f |
curry-cabal-format |
Run cabal-fmt on .cabal file |
C-c C-d d |
curry-cabal-command |
Run arbitrary cabal command |
C-c C-d . |
curry-cabal-find-cabal-file |
Jump to nearest .cabal file |
C-c C-d p |
curry-cabal-find-cabal-project |
Jump to nearest cabal.project |
The project root is determined by walking up from the current file to find
cabal.project or a .cabal file.
curry-mode integrates with dape (a DAP
client, available from GNU ELPA) and
haskell-debugger (hdb) for
step-through debugging. Call curry-mode-dape-setup after dape is loaded to
register the haskell-debugger configuration.
This is experimental and requires GHC 9.14+.
-
Install hdb:
cabal install haskell-debugger \ --allow-newer=base,time,containers,ghc,ghc-bignum,template-haskell \ --enable-executable-dynamic -
Install dape from GNU ELPA (
M-x package-install RET dape RET). -
Register the
haskell-debuggerconfiguration by callingcurry-mode-dape-setupafter dape is loaded:(with-eval-after-load 'dape (curry-mode-dape-setup))
Or with
use-package:(use-package dape :config (curry-mode-dape-setup))
Set breakpoints with M-x dape-breakpoint-toggle, then start a debug session
with M-x dape -- select haskell-debugger and adjust :entryFile and
:entryPoint if needed.
You can set project defaults via .dir-locals.el:
((curry-mode
(dape-command . (haskell-debugger
:entryFile "app/Main.hs"
:entryPoint "main"))))See the Well-Typed blog post for details on debugger features and capabilities.
| Feature | curry-mode | haskell-mode |
|---|---|---|
| Required Emacs version | 29.1+ (30+ recommended) | 25+ |
| Font-lock | Tree-sitter (4 levels) | Regex |
| Indentation | Tree-sitter + cycle-indent | SMIE (haskell-indentation) |
| REPL integration | Yes (comint + tree-sitter) | Yes (interactive-haskell-mode) |
| Navigation (defun, sexp) | Tree-sitter | Regex-based |
| Imenu | Tree-sitter (5 categories) | Regex-based |
| LSP (Eglot) integration | Yes (auto-configured) | Manual |
| Debugger | Yes (dape + hdb) | No |
| Cabal file support | Yes (tree-sitter) | Yes (regex) |
| Cabal build commands | Yes | Yes |
| Compilation commands | Error regexp + C-c C-c |
Yes |
| Prettify symbols | Yes | Yes |
| Code folding (outline) | Yes (Emacs 30+) | No |
| Import management | Delegate to HLS | Built-in |
| Type at point | Delegate to HLS | Built-in (via GHCi) |
Historically, haskell-mode bundled features like type display, completion,
jump-to-definition, import management, and smart suggestions -- driven by GHCi
process communication. Today, HLS provides all of these through the standard
LSP protocol, and Eglot (built into Emacs 29+) acts as the client. There is no
reason for a major mode to reimplement any of this.
HLS uses only standard LSP methods so vanilla Eglot gives you the complete feature set: completions, hover, go-to-definition, code actions (add imports, add pragmas, apply hlint fixes, case splitting), code lenses (type signatures, eval plugin), rename, formatting, and diagnostics.
| Feature | Emacs 29 | Emacs 30+ |
|---|---|---|
| Font-lock | Yes | Yes |
| Indentation | Yes | Yes |
treesit-thing-settings |
No | Yes |
| Sentence navigation (M-a/e) | No | Yes |
Hybrid forward-sexp |
Yes | Yes |
outline-minor-mode folding |
No | Yes |
eldev test
eldev compile --warnings-as-errors
eldev lint
These built-in tools are invaluable when working on font-lock or indentation rules:
M-x treesit-explore-mode-- visualise the full parse treeM-x treesit-inspect-mode-- show node type at point in the mode line(setq treesit--font-lock-verbose t)-- log which font-lock rules fire(setq treesit--indent-verbose t)-- log which indent rule matched
Contributions are welcome. The codebase follows the architecture of neocaml and fsharp-ts-mode -- a thin tree-sitter mode with semantic features delegated to the language server.
Copyright (c) 2026 Tim McGilchrist and contributors.
Distributed under the GNU General Public License, version 3 or later.