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.
Make sure your
~/.doom.d/init.el
contains::editor fold :tools tree-sitter
Caveat:
ts-fold
seems to conflict withindent-guides
(resulting in characters being replaced by|
), make sure to comment the latter out in the:ui
section of the file.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
.Run
doom sync
, restart your emacs server if needed.
The keybindings are then as follows:
Action | Keybinding |
---|---|
Open/close all folds | z r / z m |
Open/close a fold | z o / z c |
Open a fold recursively | z O |
Toggle a fold | z 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.