with apologies

Tree-sitting Emacs

Richard Mortier · 3 min read · October 20, 2025 · #tech #linux #emacs #nixos

First, some background that it took me longer than I would’ve hoped to piece together.

Tree-sitter is “a parser generator tool and an incremental parsing library”. What does that mean in pratice? It is a thing which, when paired with a grammar, parses your source code and enables syntax highlighting, code folding, and so on. Syntactic manipulations. It comes with a domain specific language (DSL) for writing grammars and so, happily, many languages are already supported. It is also, as of Emacs 29, included out of the box.

On the other hand, Eglot, also included out of the box since Emacs 29, provides the Language Server Protocol (LSP) interaction. This supports semantic rather than syntactic markup: inline hints, invocation of code formatters, and so on. More on that next.

NixOS packages

I am of course also using NixOS, with flakes and Home Manager, so it took a bit of faffing about to get this all working but I think I got there in the end. First, the NixOS runes needed to install relevant Emacs packages:

programs.emacs = {
  enable = true;
  package = pkgs.emacs-gtk;
  extraPackages =
    epkgs: with epkgs; ([
      # ...
      tree-sitter-langs
      (treesit-grammars.with-grammars (p: [
        p.tree-sitter-bash
        p.tree-sitter-dockerfile
        p.tree-sitter-elisp
        p.tree-sitter-markdown
        p.tree-sitter-markdown-inline
        p.tree-sitter-nix
        p.tree-sitter-ocaml
        p.tree-sitter-python
        p.tree-sitter-rust
        p.tree-sitter-toml
        p.tree-sitter-typst
        p.tree-sitter-yaml
      ]))
    ]);
};

You could use the treesit-grammars.with-all-grammars package instead of the final expression that selects specific grammars, but I found that package did not include all the grammars I actually wanted, hence doing it by hand. It is also in principle possible to allow the Emacs treesit-auto package to download and install grammars on demand. Grammars are first sought in treesit-extra-load-path – in my case a gnarly and ever evolving Nix path, currently /nix/store/4p4rfax61q9z6c5wj7ahnb8hjq8pa11g-emacs-packages-deps/lib/. If they are not found there, they are next looked for in ${user-emacs-directory}/tree-sitter/ with ${user-emacs-directory} resolving in my case to ~/.emacs.d. treesit-auto places them in the latter location.

Additionally, the emacs-lsp-booster package claims to speed things up, so why not:

home.packages = with pkgs; [ emacs-lsp-booster ];

Emacs configuration

This was a bit fiddly but not too bad in the end. First install packages comprising the tree-sitter grammars, an auto-installer for missing grammars, and enabling code folding via tree-sitter:

(use-package tree-sitter-langs ;; grammar bundle
  :after tree-sitter
  :custom (global-tree-sitter-mode t))

(use-package treesit-auto ;; auto-install missing grammars
  :after tree-sitter
  :config (global-treesit-auto-mode))

(use-package treesit-fold ;; enable code folding based on tree-sitter
  :vc (:url "https://github.com/emacs-tree-sitter/treesit-fold")
  :config (treesit-fold-mode)
  :bind
  (("C-`" . treesit-fold-toggle)
   ("C-€" . treesit-fold-close-all) ;; I remap S-` to € using `keyd` so can't use C-S-`
   ("M-C-€" . treesit-fold-open-all)))

Then, configure the -ts- version for each relevant major -mode, e.g., for Dockerfile and Nix:

(use-package dockerfile-ts-mode
  :mode "\\Dockerfile\\'"
  :config (add-to-list 'major-mode-remap-alist '((docker-mode . docker-ts-mode))))

(use-package nix-ts-mode
  :mode "\\.nix\\'"
  :config (add-to-list 'major-mode-remap-alist '((nix-mode . nix-ts-mode))))

…and so on. The examples above do two things: set the file glob pattern to which the major mode will apply, and remap the “default” major mode LANGUAGE-mode to the new tree-sitter major mode, LANGUAGE-ts-mode.

LSP support via Eglot then requires installing any necessary language servers and configuring them. More on that next.