Getting Emacs 29 to Automatically Use Tree-sitter Modes
Robert Enzmann
It's Robb, man!
/apple-touch-icon.png
robbmann
Recently, /u/casouri posted a guide to getting started with the new built-in tree-sitter capabilities for Emacs 29. In that post, they mention that there will be no automatic major-mode fallback for Emacs 29. That means I would have to use M-x python-ts-mode manually, or change the entry in auto-mode-alist to use python-ts-mode, in order to take advantage of the new tree-sitter functionality. Of course, that would still leave the problem of when the Python tree-sitter grammar isn’t installed, in which case python-ts-mode is going to fail.

To solve this issue, I wrote a very small package that adjusts the new major-mode-remap-alist variable based on what grammars are ready on your machine. If a language’s tree-sitter grammar is installed, it will use that mode. If not, it will use the original major mode. Simple as that!

For the impatient: treesit-auto.el #

The package I wound up with is available on GitHub and MELPA as treesit-auto.el. So long as MELPA is on your package-archives list like this:

Then you can use M-x package-refresh-contents followed by M-x package-install RET treesit-auto.

If you also like having a local copy of the git repository itself, then package-vc-install is a better fit:

Then, in your configuration file:

See the README on GitHub for all the goodies you can put in the :config block.

Origins of treesit-auto.el #

The recommendation in Yuan’s article was to use define-derived-mode along with treesit-ready-p. In the NEWS (C-h n), however, I noticed a new variable major-mode-remap-alist, which at a glance appears suitable for a similar cause. For my Emacs configuration, I had two things I wanted to accomplish:

Set all of the URLs for treesit-language-source-alist up front, so that I need only use treesit-install-language-grammar RET python RET, instead of writing out everything interactively

Use the same list of available grammars to remap between tree-sitter modes and their default fallbacks

Initially, I tried Yuan’s suggested approach with define-derived-mode, but I didn’t want to repeat code for every major mode I wanted fallback for. Trying to expand the major mode names correctly in a loop wound up unwieldy, because expanding the names properly for the define-derived-mode macro was too challenging for my current skill level with Emacs lisp, and wound up cluttering the global namespace more than I liked when auto-completing through M-x. Instead, I decided take a two step approach:

Set up treesit-language-source-alist with the grammars I’ll probably use

Loop over the keys in this alist to define the association between a tree-sitter mode and its default fallback through major-mode-remap-alist

This makes the code we need to actually write a little simpler, since an association like python-mode to python-ts-mode can be automatic (since they share a name), and we can use a customizable alist for specifying the edge cases, such as toml-ts-mode falling back to conf-toml-mode.

To start with, I just had this:

At this point, I can just use M-x treesit-install-language-grammar RET bash to get the Bash grammar, and similarly for other languages.

Then, I made an alist of the “weird” cases:

Setting the CDR to nil explicitly means I didn’t want any type of fallback to be attempted whatsoever for a given tree-sitter mode, even if something similarly named might be installed.

Finally, I had a simple loop where I constructed the symbols for the mode and the tree-sitter mode via intern and concat, and check whether the tree-sitter version is available through treesit-ready-p. If it is, we remap the base mode to the tree-sitter one in major-mode-remap-alist. If it isn’t ready, then we do the opposite: remap the tree-sitter mode to the base version.