GNU ELPA - lisp-ts-mode

lisp-ts-mode Atom Feed

Description
lisp-mode with tree-sitter support
Latest
lisp-ts-mode-0.2.0.tar (.sig), 2026-Jun-28, 160 KiB
Maintainer
zach shaftel <zach@shaf.tel>
Website
https://codeberg.org/zshaftel/lisp-ts-mode
Browse ELPA's repository
CGit or Gitweb
All Dependencies
cond-star (.tar), compat (.tar)
Badge

To install this package from Emacs, use package-install or list-packages.

Full description

A tree-sitter alternative to lisp-mode which uses this grammar.

1. Why?

The killer feature of this mode is the FORMAT string support. Using a specialized embedded grammar for format strings enables format directive font-lock and indentation. Trust me, the font-lock helps a lot when writing intricate format strings. The indentation is more niche and can be customized or simply disabled, see 3.

A sample, showing off both indentation and font-lock:

format-screenshot.png

Figure 1: I actually wrote that before I made this package 😂

If this makes your eyes bleed, you can customize all those faces to look as boring as you'd like.

The highlighted ~​{​~​} directive after the cursor is from show-paren-mode, because lisp-ts-mode's syntax-propertize-function adds delimiter syntax to format directives. This also means forward-sexp moves across those directives like it does on regular lists.

Aside from that, the truth is tree-sitter doesn't add nearly as much to most Lisp languages on its own compared to other languages. Emacs' syntax system is (unsurprisingly) extremely well suited to handling Lisp syntax (notably, it even has first class support for nested comments unlike tree-sitter). But the CST generated by tree-sitter enables some very fancy semantic font-lock, see the sister package gaudy-cl.

2. Getting started

This package is available on GNU ELPA, so here are some examples of how to activate:

(use-package lisp-ts-mode
  :ensure t)

If you use elpaca, you can use the following recipe:

(elpaca lisp-ts-mode)

For straight.el:

(straight-use-package 'lisp-ts-mode)

To automatically activate lisp-ts-mode wherever lisp-mode normally activates, use

(setf (alist-get 'lisp-mode major-mode-remap-alist) 'lisp-ts-mode)

You can add this to the :init section of use-package.

I also recommend this snippet:

(setf (alist-get 'lisp-ts-mode font-lock-ignore)
      lisp-ts-mode-font-lock-ignore-keywords)

You can place this in the :config section of use-package. This disables font-lock keywords which are already handled by the tree-sitter based font-lock rules.

Lastly, the FORMAT string support is a separate minor mode, lisp-ts-format-support-mode. You can enable it with

(add-hook 'lisp-ts-mode-hook #'lisp-ts-format-support-mode)

But if you use gaudy-cl, it will apply the grammar on its own (and much more).

3. FORMAT directive indentation

There is optional (but currently enabled by default) auto-indentation of format directives. Still somewhat experimental. This is mainly to indent relative to the paired directives (~[, ~{​, ~( and ~<). Example:

"~<
~A
 ~:>"

After pressing TAB on the second line:

"~<~@
   ~A
 ~:>"

The directive is indented to the column after the ~<, and the newline is converted to a ~@<newline> directive so that the indentation of the output isn't affected. The behavior depends on a few customizable variables:

lisp-ts-mode-format-indent-predicate
A predicate for treesit-node-match-p (usually a function or regexp matching a treesit node's type) to determine which nodes should have their contents indented. By default it matches the four paired directives, but you can also set it to indent relative to the start of the entire format string. To disable format indentation entirely, set it to nil.
lisp-ts-mode-format-indent-auto-escape-eol
Controls the behavior of the newline directive. It can be set to nil (don't indent if there isn't already a ~<newline>), t (don't add it but indent anyway), a string specifying the directive prefix to add, or a cons pair (LOGICAL-BLOCK . DEFAULT) to specify a different string (like ~:@_~) to use within ~<​~:>.
lisp-ts-mode-format-indent-tilde-relative

Determines which column is used as an anchor for calculating indentation (using the following two variables), and the indentation of the end of a paired directive when it begins a line. If nil (the default), the directive characters themselves are aligned, like

"~:@{
     ~A
   ~}"

If set to non-nil, the ~s are aligned:

"~:@{
  ~A
 ~}"
lisp-ts-mode-format-group-indent-offset
Number of columns added to indentation relative to the start of a paired directive.
lisp-ts-mode-format-string-indent-offset
Like the above but controls indentation relative to the start of the string, if that's enabled by lisp-ts-mode-format-indent-predicate.
lisp-ts-mode-format-indent-function
This variable isn't customizable because it's meant to be used with add-function. This is the function called to perform the indentation, so you can wrap it to control if, when or how the indentation is performed.

For more details see each variable's documentation.

4. Font lock

There are distinct faces for every component of format directives I could think of: ~, numeric, character, V and # parameters, the , separator between parameters, : and @ modifiers and the directive character itself all have specific faces. The paired directives (~[, ~{​, ~( and ~<) have a separate face from other directives, and you can customize lisp-ts-mode-format-rainbow-delimiters to use rainbow-delimiters faces to highlight those directives based on nesting level. There are also three faces for each level of #||# comment nesting (the top level uses font-lock-comment-face). See M-x customize-group RET lisp-ts-mode.