cpg

Code folding in Doom Emacs

2024-10-21 #emacs

Tree-sitter-based code folding in Doom Emacs with ts-fold

Introduction

After reading matklad’s A Missing IDE Feature post, I realized that syntax-aware code folding was a great editor feature I did not know I needed.


Example of a source file (from sqlsonnet) with folds closed, in Doom Emacs.

Here is how to get it working in Doom Emacs, with a focus on Rust. See also the Emacs subreddit discussion.

Base configuration

We will use the ts-fold package for tree-sitter-based folding.

  1. Make sure your ~/.doom.d/init.el contains:

    :editor
    fold
    :tools
    tree-sitter
    

    Caveat: ts-fold seems to conflict with indent-guides (resulting in characters being replaced by |), make sure to comment the latter out in the :ui section of the file.

  2. Add the following to your ~/.doom.d/config.el:

    (after! rustic
      (add-hook 'rustic-mode-hook
                (lambda ()
                  (tree-sitter-mode)
                  ; Comment out the next line if you do not want to auto-fold.
                  (ts-fold-close-all)
                )
      )
    )
    (map!
        :n "z r" #'ts-fold-open-all
        :n "z m" #'ts-fold-close-all
        :n "z o" #'ts-fold-open
        :n "z O" #'ts-fold-open-recursively
        :n "z c" #'ts-fold-close
        :n "z a" #'ts-fold-toggle
    )
    

    Caveat: Doom should be seamlessly combining multiple folding packages, but in my experiments, the folds types were not recognized and they did not open or close. Thus, we instead rebind the keys to force using ts-fold.

  3. Run doom sync, restart your emacs server if needed.

The keybindings are then as follows:

ActionKeybinding
Open/close all foldsz r / z m
Open/close a foldz o / z c
Open a fold recursivelyz O
Toggle a foldz a

To support other languages, simply add similar hooks to the respective modes. Look at the ts-fold README for languages supported out-of-the-box.

Customizing the folding rules

The per-language folding rules for ts-fold are defined in ts-fold-parsers.el. For Rust, they are:

; Default Rust folding rules from ts-fold
(defun ts-fold-parsers-rust ()
  "Rule set for Rust."
  '((declaration_list       . ts-fold-range-seq)
    (enum_variant_list      . ts-fold-range-seq)
    (field_declaration_list . ts-fold-range-seq)
    (use_list               . ts-fold-range-seq)
    (field_initializer_list . ts-fold-range-seq)
    (match_block            . ts-fold-range-seq)
    (macro_definition       . (ts-fold-range-rust-macro 1 -1))
    (block                  . ts-fold-range-seq)
    (token_tree             . ts-fold-range-seq)
    (line_comment
     . (lambda (node offset)
         (ts-fold-range-line-comment node
                                     (ts-fold--cons-add offset '(0 . -1))
                                     "///")))
    (block_comment          . ts-fold-range-block-comment)))

This is a fairly aggressive folding, resulting in something like this (where we opened the order_by module and impl Expr folds):

Folding only method bodies

Matklad’s article advocates for only folding method bodies. This can be achieved with the following configuration (again in your config.el):

(after! ts-fold
  (defun ts-fold-rust-fn-body (node offset)
    (let* ((body-node (tsc-get-child-by-field node :body))
           (beg (1+ (tsc-node-start-position body-node)))
           (end (1- (tsc-node-end-position body-node))))
      (ts-fold--cons-add (cons beg end) offset)))
  (defun ts-fold-parsers-rust ()
    '(
      (function_item . ts-fold-rust-fn-body)
      ))
  (setf (alist-get 'rustic-mode ts-fold-range-alist) (ts-fold-parsers-rust))
  )

The result is then as shown on the top screenshot.

See the ts-fold README for more instructions on customization.