Code-folding using tree-sitter
ts-fold
builds on top of elisp-tree-sitter
to provide code folding based on the tree-sitter syntax tree.
Table of Contents
(use-package ts-fold
:straight (ts-fold :type git :host github :repo "emacs-tree-sitter/ts-fold"))
git clone https://github.com/emacs-tree-sitter/ts-fold /path/to/lib
then in Emacs:
(add-to-list 'load-path "/path/to/lib")
(require ts-fold)
or
(use-package ts-fold
:load-path "/path/to/lib")
Commands | Description |
---|---|
ts-fold-close |
fold the current syntax node. |
ts-fold-open |
open all folds inside the current syntax node. |
ts-fold-open-recursively |
open the outmost fold of the current syntax node. Keep the sub-folds close. |
ts-fold-close-all |
close all foldable syntax nodes in the current buffer. |
ts-fold-open-all |
open all folded syntax nodes in the current buffer. |
ts-fold-toggle |
toggle the syntax node at `point'. |
⚠️ Please sort these two lists alphabetically!
These languages are fairly complete:
- Bash
- C / C++ / C# / CSS
- Elixir
- Go
- HTML
- Java / JavaScript / JSX / JSON / Julia
- Nix
- PHP / Python
- R / Ruby / Rust
- Scala / Swift
- TypeScript / TSX
These languages are in development:
- Agda
- Elm
- Emacs Lisp
- OCaml
- XML (upstream)
You need to load ts-fold-indicators-mode
:
-
use-package
(use-package ts-fold-indicators :straight (ts-fold-indicators :type git :host github :repo "emacs-tree-sitter/ts-fold"))
-
(add-to-list 'load-path "/path/to/lib") (require ts-fold)
or
(use-package ts-fold-indicators :load-path "/path/to/lib")
You can then enable this manually by doing the following
M-x ts-fold-indicators-mode
-
To enable this automatically whenever
tree-sitter-mode
is enabled:(add-hook 'tree-sitter-after-on-hook #ts-fold-indicators-mode)
-
To switch to left/right fringe: (Default is
left-fringe
)(setq ts-fold-indicators-fringe 'right-fringe)
-
To lower/higher the fringe overlay's priority: (Default is
30
)(setq ts-fold-indicators-priority 30)
-
To apply different faces depending on some conditions: (Default is
nil
)For example, to coordinate line-reminder with this plugin.
(setq ts-fold-indicators-face-function (lambda (pos &rest _) (let ((ln (line-number-at-pos pos))) (cond ((memq ln line-reminder--change-lines) 'line-reminder-modified-sign-face) ((memq ln line-reminder--saved-lines) 'line-reminder-saved-sign-face) (t nil)))))
This plugin automatically extracts summary from the comment/document string, so you can have a nice way to peek at what's inside the fold range.
-
If you don't want this to happen, do: (Default is
t
)(setq ts-fold-summary-show nil)
-
Summary are truncated by length: (Default is
60
)(setq ts-fold-summary-max-length 60)
-
The exceeding string are replace by: (Default is
"..."
)(setq ts-fold-summary-exceeded-string "...")
-
To change summary format: (Default is
" <S> %s "
)(setq ts-fold-summary-format " <S> %s ")
Enable tree-sitter-mode
first, then tree-sitter-query-builder
is useful to test
out queries that determine what syntax nodes should be foldable and how to fold
them. emacs-tree-sitter
has an excellent documentation on how to write tree-sitter
queries.
Parsers are defined in the ts-fold-parsers.el
file. Parser functions are named
with the prefix ts-fold-parsers-
followed by the language name
. For example, if
you want to create a parser for the C
programming language you should name it
ts-fold-parsers-c
.
Parsers are association lists (alist) whose items consist
of tree-sitter node
and a function that returns the folding range. See
the following example:
(defun ts-fold-parsers-csharp ()
"Rule sets for C#."
'((block . ts-fold-range-seq)
...))
block
is the tree-sitter node and ts-fold-range-seq
is the function
that will return the folding range.
Let's move into details,
To look for the correct node you have three options:
- look at the
tree-sitter-[lang]/grammar.js
implementation. In the above example,block
node is defined in the tree-sitter-c-sharp'sgrammar.js
file - open a file of your language choice in emacs and
M-x tree-sitter-debug-mode
. This will display the whole s-expr representing your file (message "%S" (tsc-node-to-sexp))
in your function to display what your function is seeing
⚠️ WarningMake sure you look into the correct repository. Repositories are managed under tree-sitter-langs's using git submodule. Some tree-sitter module aren't using the latest version!
Function take 2 arguments, node
and offset
.
-
node
- the targeted tree-sitter node; in this example,block
will be the targeting node. -
offset
- (optional) a cons of two integers. This is handy when you have a similar rule with little of positioning adjustment.tree-sitter-[lang]
parsers are generally integrated by different authors, hence their naming and ruling are slightly different (+1/-1 position).Let's look at function
ts-fold-range-seq
for better understanding,(defun ts-fold-range-seq (node offset) "..." (let ((beg (1+ (tsc-node-start-position node))) ; node beginning position (from Rust layer) (end (1- (tsc-node-end-position node)))) ; node end position (from Rust layer) (ts-fold--cons-add (cons beg end) offset))) ; return fold range
Don't forget to add your parser to the entry alist with its corresponding
major-mode
.
(defcustom ts-fold-range-alist
`((agda-mode . ,(ts-fold-parsers-agda))
(sh-mode . ,(ts-fold-parsers-bash))
(c-mode . ,(ts-fold-parsers-c))
(c++-mode . ,(ts-fold-parsers-c++))
...
This variable is defined in package main file, ts-fold.el
.
ts-fold-summary.el
module is used to extract and display a short description
from the comment/docstring.
To create a summary parser, you just have to create a function that could
extract comment syntax correctly then register this function to
ts-fold-summary-parsers-alist
defined in ts-fold-summary.el
.
The display and shortening will be handled by the module itself.
Functions should be named with the prefix ts-fold-summary-
followed by style name
.
For example, to create a summary parser for Javadoc style, then it should be
named ts-fold-summary-javadoc
.
Let's see the implementation,
(defun ts-fold-summary-javadoc (doc-str)
"..."
(ts-fold-summary--generic doc-str "*")) ; strip all asterisks
The above summary parser for Javadoc simply remove *
from any given point.
Like folding parsers, you should register your summary parser to the entry alist
with its corresponding major-mode
.
(defcustom ts-fold-summary-parsers-alist
`((actionscript-mode . ts-fold-summary-javadoc)
(bat-mode . ts-fold-summary-batch)
(c-mode . ts-fold-summary-c)
(c++-mode . ts-fold-summary-c)
...