A modern, tree-sitter-powered Emacs major mode for Haskell.
emacs haskell

Files
README.md

curry-mode #

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.

Why? #

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.

Features #

  • Tree-sitter based font-locking (4 levels) for .hs, .lhs, .hsc, and .hs-boot files
  • 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 with M-a/M-e)
  • Imenu with categories for Functions, Types, Classes, Instances, and Imports
  • Toggling between .hs and .lhs via ff-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), and comment-dwim (M-;)
  • Electric indentation and electric pairs (parentheses, brackets, braces, backticks, quotes)
  • Cabal file editing (curry-cabal-mode) with font-lock, indentation, imenu, and cabal-fmt integration
  • 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

Installation #

From source #

(use-package curry-mode
  :ensure nil
  :load-path "~/code/elisp/curry-mode"
  :mode ("\\.hs\\'" "\\.lhs\\'" "\\.hsc\\'" "\\.hs-boot\\'")
  :config
  (with-eval-after-load 'eglot
    (add-to-list 'eglot-server-programs
                 '((curry-mode :language-id "haskell")
                   "haskell-language-server-wrapper" "--lsp"))))

(use-package curry-cabal-mode
  :ensure nil
  :load-path "~/code/elisp/curry-mode"
  :mode "\\.cabal\\'")

(use-package curry-cabal-interaction
  :ensure nil
  :load-path "~/code/elisp/curry-mode"
  :defer t)

(use-package curry-repl
  :ensure nil
  :load-path "~/code/elisp/curry-mode"
  :defer t)

Grammar #

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

Usage #

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.

Compilation #

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+)

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

Configuration #

Font-locking #

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 directives
  • definition -- 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 literals
  • number -- integer, float, and negation literals

Level 3 (default -- types, constructors, modules):

  • type -- type constructors, type variables, star kind
  • constructor -- 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 names
  • function -- function application, infix function application, builtin functions, quasi-quoters
  • bracket -- parentheses, brackets, braces
  • delimiter -- commas and semicolons

Selecting features #

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)

Customizing faces #

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")))

Adding custom font-lock rules #

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.

Prettify Symbols #

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)

Indentation #

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-region on 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.

Indentation offset #

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)))

Shift region #

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

Cycle indent #

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.

Comments #

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 comments
  • M-j (default-indent-new-line) -- inside a block comment, inserts a newline and aligns the continuation with the comment body text
  • M-q (fill-paragraph) -- refills the current comment, wrapping text at fill-column with proper indentation. Respects {- -}, -- | (haddock), and {-# #-} (pragma) delimiters.

Code Folding #

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 out of the box. curry-mode automatically registers with treesit-fold's Haskell fold definitions when the package is loaded:

(use-package treesit-fold
  :ensure t
  :hook (curry-base-mode . treesit-fold-mode))

Structural Selection #

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)))

GHCi REPL Integration #

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-c is bound to compile in the base mode. When curry-repl-minor-mode is enabled, it is rebound to curry-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)

Input Syntax Highlighting #

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)

Configuration #

;; 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*")

Cabal Support #

Cabal File Editing #

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)

Cabal Build Commands #

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.

Debugging #

curry-mode integrates with dape (a DAP client, available from GNU ELPA) and haskell-debugger (hdb) for step-through debugging. When dape is loaded, curry-mode automatically registers the haskell-debugger configuration.

This is experimental and requires GHC 9.14+.

Setup #

  1. Install hdb:

    cabal install haskell-debugger \
      --allow-newer=base,time,containers,ghc,ghc-bignum,template-haskell \
      --enable-executable-dynamic
    
  2. Install dape from GNU ELPA (M-x package-install RET dape RET).

Usage #

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.

Comparison with haskell-mode #

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)

The impact of LSP on major modes #

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.

Emacs Version Compatibility #

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

Development #

Running tests #

eldev test

Byte-compile #

eldev compile --warnings-as-errors

Lint #

eldev lint

Debugging tree-sitter #

These built-in tools are invaluable when working on font-lock or indentation rules:

  • M-x treesit-explore-mode -- visualise the full parse tree
  • M-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

Contributing #

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.

License #

Copyright (c) 2026 Tim McGilchrist and contributors.

Distributed under the GNU General Public License, version 3 or later.