- Emacs Lisp 96.2%
- Nix 2.1%
- Makefile 1.7%
- Haskell mode based on Tree-sitter
- Usage
- Features
- REPL
- Comparison with
haskell-mode - Motivation
- Structural navigation
- Installation
- Customization
- Changelog
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.
The above screenshot is colored using haskell-ts-mode, with
prettify-symbols-mode enabled.
Usage
C-c C-rOpen REPLC-c C-cSend region to REPL (or reload with:rwhen no region is active)C-c C-lLoad the current file into the REPL (starts one if needed)C-c C-eSend the current line to the REPLC-M-xSend the definition at point to the REPL
Features
Overview of features:
- Syntax highlighting
- Structural navigation
- Comment-aware editing: prose motion, paragraph text objects, and
RETcontinuation inside =–=/Haddock comments - Imenu support
- REPL (
C-c C-rin the mode to run) - Prettify Symbols mode support
M-x alignsupport (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-csends the active region to the REPL (or:rreloads when no region is active).C-c C-lsaves 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-esends the current line to the REPL, verbatim.C-M-xsends the definition at point to the REPL – the same definitiontreesit-defun-name-functionand imenu use, so it is either a top-level binding or, from inside awhere=/=letblock, 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:[]))onlyais 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.
- Only arguments that can be used in functions are highlighted, e.g.,
in
- More performant, this is especially seen in longer files.
- Much, much less code,
haskell-modehas accumulated 30,000 lines of code and features to do with all things Haskell related.haskell-ts-modejust 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?
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:
- The official tree-sitter/tree-sitter-haskell grammar (unmaintained);
- A community fork (somewhat maintained);
- A fork by @tek (well-maintained);
- My fork of @tek's grammar (well-maintained; contains some features not in the fork by @tek).
@tek's grammar is actively maintained and fixes node-type names that the
official grammar gets wrong (for example type_synomym → type_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:
- Consult Hoogle with consult-hoogle
- Format code with Apheleia
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.