1
0
Fork
You've already forked haskell-ts-mode
0
Emacs Haskell mode using Tree-sitter
  • Emacs Lisp 96.2%
  • Nix 2.1%
  • Makefile 1.7%
2026年07月14日 17:44:04 +02:00
.ai TODO items 2026年07月14日 17:44:04 +02:00
.github/workflows Add make format and CI 2026年07月10日 11:09:10 +02:00
tests Fix three comment/code navigation bugs 2026年07月14日 17:35:00 +02:00
.dir-locals.el Add dirlocal to disable tab indentation 2025年05月31日 18:11:27 +10:00
.elpaignore More changes 2024年08月29日 17:22:31 +10:00
.gitignore Track AI stuff with git 2026年07月12日 23:46:47 +02:00
CHANGELOG.org Fix three comment/code navigation bugs 2026年07月14日 17:35:00 +02:00
CLAUDE.md Remember cabal component; add prefix-arg component override 2026年07月13日 10:54:55 +02:00
flake.lock Add test suite, dev Nix Flake, and document the @tek grammar 2026年06月15日 17:41:50 +02:00
flake.nix Add relint, gating make check 2026年07月10日 18:45:03 +02:00
haskell-ts-mode.el Release 1.5: condense changelog, advertise nav features 2026年07月13日 10:54:57 +02:00
haskell-ts-navigation.el Fix three comment/code navigation bugs 2026年07月14日 17:35:00 +02:00
haskell-ts-repl.el Remember cabal component; add prefix-arg component override 2026年07月13日 10:54:55 +02:00
LICENSE Added licence 2024年08月17日 23:04:04 +10:00
Makefile Split REPL support into haskell-ts-repl.el; send line/defun to REPL 2026年07月10日 19:33:54 +02:00
NOTES.org Confine evil's }/{ paragraph motions to a glued comment 2026年07月12日 23:19:48 +02:00
README.org Release 1.5: condense changelog, advertise nav features 2026年07月13日 10:54:57 +02:00
ss.png Change image 2025年05月16日 23:41:38 +10:00
TODO.org TODO items 2026年07月14日 17:44:04 +02:00

NOTICE: This is a fork and an experiment based on Pranshu's haskell-ts-mode and under active development. The API may change without notice.

Haskell mode based on Tree-sitter

A Haskell mode that uses Tree-sitter.

/dschrempf/haskell-ts-mode/media/branch/main/ss.png
Image

The above screenshot is colored using haskell-ts-mode, with prettify-symbols-mode enabled.

Usage

  • C-c C-r Open REPL
  • C-c C-c Send region to REPL (or reload with :r when no region is active)
  • C-c C-l Load the current file into the REPL (starts one if needed)
  • C-c C-e Send the current line to the REPL
  • C-M-x Send the definition at point to the REPL

Features

Overview of features:

  • Syntax highlighting
  • Structural navigation
  • Comment-aware editing: prose motion, paragraph text objects, and RET continuation inside =–=/Haddock comments
  • Imenu support
  • REPL (C-c C-r in the mode to run)
  • Prettify Symbols mode support
  • M-x align support (align the ​=​ signs in a region)

REPL

C-c C-r (haskell-ts-run) starts an inferior Haskell process. When the current buffer is inside a cabal project (a cabal.project or *.cabal file is found by walking up the directory tree), the process is started with cabal repl, so the project's dependencies, default language extensions and GHC options are available in the session. Outside a cabal project it falls back to plain ghci. This is controlled by haskell-ts-use-cabal (default auto; set it to nil to always use ghci, or t to always use cabal).

The process is started in the project root when one is found, so relative import's and the module search path resolve from there. When the current file belongs to several cabal components, haskell-ts-run prompts for one and remembers the choice per buffer; a prefix argument (C-u C-c C-r) forces the pick even for an unambiguous file.

  • C-c C-c sends the active region to the REPL (or :r reloads when no region is active).
  • C-c C-l saves the buffer and loads its file with :load, starting a session if none is running. The REPL is displayed without leaving the current buffer.
  • C-c C-e sends the current line to the REPL, verbatim.
  • C-M-x sends the definition at point to the REPL – the same definition treesit-defun-name-function and imenu use, so it is either a top-level binding or, from inside a where=/=let block, the enclosing local one.

The inferior buffer runs haskell-ts-inferior-mode (derived from comint-mode): the prompt is read-only and input history is persisted across sessions in haskell-ts-inferior-history-file. Filename completion and history navigation (M-p / M-n, TAB) come from comint.

Comparison with haskell-mode

The more interesting features are:

  • Logical syntax highlighting:

    • Only arguments that can be used in functions are highlighted, e.g., in f (_:(a:[])) only a is highlighted, as it is the only variable that is captured, and that can be used in the body of the function.
    • The return type of a function is highlighted.
    • All new variables are (or should be) highlighted, this includes generators, lambda arguments.
    • Highlighting the = operator in guarded matches correctly, this would be stupidly hard in regexp based syntax.
  • More performant, this is especially seen in longer files.
  • Much, much less code, haskell-mode has accumulated 30,000 lines of code and features to do with all things Haskell related. haskell-ts-mode just keeps the scope to basic major mode functionality, and leaves other tasks to external packages.

Motivation

haskell-mode contains nearly 30k lines of code, and is about 30 years old. A lot of features implemented by haskell-mode are now also available in standard Emacs, and have thus become obsolete.

In 2018, a mode called haskell-tng-mode was made to solve some of these problems. However, because of Haskell's syntax, it too became very complex and required a web of dependencies.

Both these modes ended up practically parsing Haskell's syntax to implement indentation, so I thought why not use Tree-sitter?

Structural navigation

This mode provides structural navigation, for Emacs 30+.

combs (x:xs) = map (x:) c ++ c
 where c = combs xs

In the above code, if the pointer is right in front of the function definition combs, and you press C-M-f (forward-sexp), it will take you to the end of the second line.

Installation

Add this into your init.el:

(use-package haskell-ts-mode
 :ensure t
 :custom
 (haskell-ts-font-lock-level 4)
 (haskell-ts-ghci "ghci"))

You also need the Tree-sitter Haskell grammar; see Installing the grammar below.

Installing the grammar

haskell-ts-mode targets a custom Tree-sitter Haskell grammar. The Haskell Tree-sitter grammar landscape is fragmented. There are three major versions:

@tek's grammar is actively maintained and fixes node-type names that the official grammar gets wrong (for example type_synomymtype_synonym). The font-lock queries in this repository rely on the corrected names, so the official grammar does not work with this mode.

The grammar has to be installed manually. M-x treesit-install-language-grammar cannot do it, for two reasons: it defaults to the official repository, and @tek's and my repository do no ship generated parser (src/parser.c) nor a release branch. That is, the parser must first be produced with tree-sitter generate before it is compiled.

To do so, build the shared library yourself (clone the repository, run tree-sitter generate, compile src/parser.c into a libtree-sitter-haskell shared object and put it on treesit-extra-load-path), or let a package manager build it — the flake.nix in this repository builds the correct grammar for development and for the test suite.

Other recommended packages

Unlike haskell-mode, this mode has limited scope. There are other packages that I find help a lot with development:

Customization

Prettify Symbols mode

prettify-symbols-mode can be used to replace common symbols with unicode alternatives.

Turning on prettify-symbols-mode does stuff like turn -> to . If you want to prettify words, set haskell-ts-prettify-words to non-nil. This will do stuff like prettify forall into and elem to .

(add-hook 'haskell-ts-mode 'prettify-symbols-mode)

Aligning ​=​ signs

Calling M-x align on a region lines up the standalone ​=​ signs of the bindings and equations it contains, for example:

x = 1
foo = 2
ab = 3

Only an ​=​ surrounded by whitespace is aligned, so ​==​, ​=>​, ​<=​, ​>=​ and ​/=​ are left untouched. The rule lives in haskell-ts-align-rules-list.

Adjusting font lock level

Set haskell-ts-font-lock-level accordingly. The default and highest value is 4. You are against vibrancy, you can lower it to match your dreariness.

Language server

haskell-ts-mode works with lsp-mode and, since Emacs 30, with eglot.

To add eglot support on Emacs 29 and earlier, add the following code to your init.el:

(with-eval-after-load 'eglot
 (defvar eglot-server-programs)
 (add-to-list 'eglot-server-programs
 '(haskell-ts-mode . ("haskell-language-server-wrapper" "--lsp"))))

Changelog

See CHANGELOG.org for the list of notable changes.