emacs/admin/notes/tree-sitter/starter-guide

465 lines
15 KiB
Text
Raw Normal View History

STARTER GUIDE ON WRITTING MAJOR MODE WITH TREE-SITTER -*- org -*-
This document guides you on adding tree-sitter support to a major
mode.
TOC:
- Building Emacs with tree-sitter
- Install language definitions
- Setup
- Naming convention
- Font-lock
- Indent
- Imenu
- Navigation
- Which-func
- More features?
- Common tasks (code snippets)
- Manual
* Building Emacs with tree-sitter
You can either install tree-sitter by your package manager, or from
source:
git clone https://github.com/tree-sitter/tree-sitter.git
cd tree-sitter
make
make install
Then pull the tree-sitter branch (or the master branch, if it has
merged) and rebuild Emacs.
* Install language definitions
Tree-sitter by itself doesnt know how to parse any particular
language. We need to install language definitions (or “grammars”) for
a language to be able to parse it. There are a couple of ways to get
them.
You can use this script that I put together here:
https://github.com/casouri/tree-sitter-module
You can also find them under this directory in /build-modules.
This script automatically pulls and builds language definitions for C,
C++, Rust, JSON, Go, HTML, Javascript, CSS, Python, Typescript,
and C#. Better yet, I pre-built these language definitions for
GNU/Linux and macOS, they can be downloaded here:
https://github.com/casouri/tree-sitter-module/releases/tag/v2.1
To build them yourself, run
git clone git@github.com:casouri/tree-sitter-module.git
cd tree-sitter-module
./batch.sh
and language definitions will be in the /dist directory. You can
either copy them to standard dynamic library locations of your system,
eg, /usr/local/lib, or leave them in /dist and later tell Emacs where
to find language definitions by setting treesit-extra-load-path.
Language definition sources can be found on GitHub under
tree-sitter/xxx, like tree-sitter/tree-sitter-python. The tree-sitter
organization has all the "official" language definitions:
https://github.com/tree-sitter
* Setting up for adding major mode features
Start Emacs, and load tree-sitter with
(require 'treesit)
Now check if Emacs is built with tree-sitter library
(treesit-available-p)
Users toggle tree-sitter for each major mode with a central variable,
treesit-settings. You can check whether to enable tree-sitter with
treesit-ready-p, which takes a major-mode symbol and one or more
language symbol. The major mode body should use a branch like this:
#+begin_src emacs-lisp
(cond
;; Tree-sitter setup.
((treesit-ready-p 'python-mode 'python)
...)
(t
;; Non-tree-sitter setup.
...))
#+end_src
* Naming convention
Use tree-sitter for text (documentation, comment), use treesit for
symbol (variable, function).
* Font-lock
Tree-sitter works like this: You provide a query made of patterns and
capture names, tree-sitter finds the nodes that match these patterns,
tag the corresponding capture names onto the nodes and return them to
you. The query function returns a list of (capture-name . node). For
font-lock, we use face names as capture names. And the captured node
will be fontified in their capture name.
The capture name could also be a function, in which case (NODE
OVERRIDE START END) is passed to the function for fontification. START
and END is the start and end of the region to be fontified. The
function should only fontify within that region. The function should
also allow more optional arguments with (&rest _), for future
extensibility. For OVERRIDE check out the docstring of
treesit-font-lock-rules.
Contextual syntax like multi-line comments and multi-line strings,
needs special care. Because change in this type of things can affect
a large portion of the buffer. Think of inserting a closing comment
delimeter, it causes all the text before it (to the opening comment
delimeter) to change to comment face. These things needs to be
captured in a special name “contextual”, so that Emacs can give them
special treatment. Se the example below for how it looks like.
** Query syntax
There are two types of nodes, named, like (identifier),
(function_definition), and anonymous, like "return", "def", "(",
"}". Parent-child relationship is expressed as
(parent (child) (child) (child (grand_child)))
Eg, an argument list (1, "3", 1) could be:
(argument_list "(" (number) (string) (number) ")")
Children could have field names in its parent:
(function_definition name: (identifier) type: (identifier))
Match any of the list:
["true" "false" "none"]
Capture names can come after any node in the pattern:
(parent (child) @child) @parent
The query above captures both parent and child.
["return" "continue" "break"] @keyword
The query above captures all the keywords with capture name
"keyword".
These are the common syntax, see all of them in the manual
("Parsing Program Source" section).
** Query references
But how do one come up with the queries? Take python for an
example, open any python source file, evaluate
(treesit-parser-create 'python)
so there is a parser available, then enable treesit-inspect-mode.
Now you should see information of the node under point in
mode-line. Move around and you should be able to get a good
picture. Besides this, you can consult the grammar of the language
definition. For example, Pythons grammar file is at
https://github.com/tree-sitter/tree-sitter-python/blob/master/grammar.js
Neovim also has a bunch of queries to reference:
https://github.com/nvim-treesitter/nvim-treesitter/tree/master/queries
The manual explains how to read grammar files in the bottom of section
"Tree-sitter Language Definitions".
** Debugging queires
If your query has problems, use treesit-query-validate to debug the
query. It will pop a buffer containing the query (in text format) and
mark the offending part in red.
** Code
To enable tree-sitter font-lock, set treesit-font-lock-settings and
treesit-font-lock-feature-list buffer-locally and call
treesit-major-mode-setup. For example, see
python--treesit-settings in python.el. Below I paste a snippet of
it.
Note that like the current font-lock, if the to-be-fontified region
already has a face (ie, an earlier match fontified part/all of the
region), the new face is discarded rather than applied. If you want
later matches always override earlier matches, use the :override
keyword.
Each rule should have a :feature, like function-name,
string-interpolation, builtin, etc. Users can then enable/disable each
feature individually.
#+begin_src elisp
(defvar python--treesit-settings
(treesit-font-lock-rules
:feature 'comment
:language 'python
'((comment) @font-lock-comment-face)
:feature 'string
:language 'python
'((string) @font-lock-string-face
(string) @contextual) ; Contextual special treatment.
:feature 'function-name
:language 'python
'((function_definition
name: (identifier) @font-lock-function-name-face))
:feature 'class-name
:language 'python
'((class_definition
name: (identifier) @font-lock-type-face))
...))
#+end_src
Then in python-mode, enable tree-sitter font-lock:
#+begin_src elisp
(treesit-parser-create 'python)
(setq-local treesit-font-lock-settings python--treesit-settings)
(setq-local treesit-font-lock-feature-list
'((comment string function-name)
(class-name keyword builtin)
(string-interpolation decorator)))
...
(treesit-major-mode-setup)
#+end_src
Concretely, something like this:
#+begin_src elisp
(define-derived-mode python-mode prog-mode "Python"
...
(cond
;; Tree-sitter.
((treesit-ready-p 'python-mode 'python)
(treesit-parser-create 'python)
(setq-local treesit-font-lock-settings python--treesit-settings)
(setq-local treesit-font-lock-feature-list
'((comment string function-name)
(class-name keyword builtin)
(string-interpolation decorator)))
(treesit-major-mode-setup))
(t
;; No tree-sitter
(setq-local font-lock-defaults ...)
...)))
#+end_src
* Indent
Indent works like this: We have a bunch of rules that look like
(MATCHER ANCHOR OFFSET)
When the indentation process starts, point is at the BOL of a line, we
want to know which column to indent this line to. Let NODE be the node
at point, we pass this node to the MATCHER of each rule, one of them
will match the node (eg, "this node is a closing bracket!"). Then we pass
the node to the ANCHOR, which returns a point, eg, the BOL of the
previous line. We find the column number of that point (eg, 4), add
OFFSET to it (eg, 0), and that is the column we want to indent the
current line to (4 + 0 = 4).
Matchers and anchors are functions that takes (NODE PARENT BOL &rest
_). Matches return nil/non-nil for no match/match, and anchors return
the anchor point. Below are some convenient builtin matchers and anchors.
For MATHCER we have
(parent-is TYPE) => matches if PARENTs type matches TYPE as regexp
(node-is TYPE) => mathces NODEs type
(query QUERY) => matches if querying PARENT with QUERY
captures NODE.
(match NODE-TYPE PARENT-TYPE NODE-FIELD
NODE-INDEX-MIN NODE-INDEX-MAX)
=> checks everything. If an argument is nil, dont match that. Eg,
(match nil nil TYPE) is the same as (parent-is TYPE)
For ANCHOR we have
first-sibling => start of the first sibling
parent => start of parent
parent-bol => BOL of the line parent is on.
prev-sibling => start of previous sibling
no-indent => current position (dont indent)
prev-line => start of previous line
There is also a manual section for indent: "Parser-based Indentation".
When writing indent rules, you can use treesit-check-indent to
check if your indentation is correct. To debug what went wrong, set
treesit--indent-verboase to non-nil. Then when you indent, Emacs
tells you which rule is applied in the echo area.
#+begin_src elisp
(defvar typescript-mode-indent-rules
(let ((offset typescript-indent-offset))
`((typescript
;; This rule matches if node at point is "}", ANCHOR is the
;; parent nodes BOL, and offset is 0.
((node-is "}") parent-bol 0)
((node-is ")") parent-bol 0)
((node-is "]") parent-bol 0)
((node-is ">") parent-bol 0)
((node-is "\\.") parent-bol ,offset)
((parent-is "ternary_expression") parent-bol ,offset)
((parent-is "named_imports") parent-bol ,offset)
((parent-is "statement_block") parent-bol ,offset)
((parent-is "type_arguments") parent-bol ,offset)
((parent-is "variable_declarator") parent-bol ,offset)
((parent-is "arguments") parent-bol ,offset)
((parent-is "array") parent-bol ,offset)
((parent-is "formal_parameters") parent-bol ,offset)
((parent-is "template_substitution") parent-bol ,offset)
((parent-is "object_pattern") parent-bol ,offset)
((parent-is "object") parent-bol ,offset)
((parent-is "object_type") parent-bol ,offset)
((parent-is "enum_body") parent-bol ,offset)
((parent-is "arrow_function") parent-bol ,offset)
((parent-is "parenthesized_expression") parent-bol ,offset)
...))))
#+end_src
Then you set treesit-simple-indent-rules to your rules, and call
treesit-major-mode-setup:
#+begin_src elisp
(setq-local treesit-simple-indent-rules typescript-mode-indent-rules)
(treesit-major-mode-setup)
#+end_src
* Imenu
Not much to say except for utilizing treesit-induce-sparse-tree.
See js--treesit-imenu-1 in js.el for an example.
Once you have the index builder, set imenu-create-index-function to
it.
* Navigation
Mainly beginning-of-defun-function and end-of-defun-function.
You can find the end of a defun with something like
(treesit-search-forward-goto "function_definition" 'end)
where "function_definition" matches the node type of a function
definition node, and end means we want to go to the end of that node.
Tree-sitter has default implementations for
beginning-of-defun-function and end-of-defun-function. So for
ordinary languages, it is suffice to set treesit-defun-type-regexp
to something that matches all the defun struct types in the language,
and call treesit-major-mode-setup. For example,
#+begin_src emacs-lisp
(setq-local treesit-defun-type-regexp (rx bol
(or "function" "class")
"_definition"
eol))
(treesit-major-mode-setup)
#+end_src>
* Which-func
If you have an imenu implementation, set which-func-functions to
nil, and which-func will automatically use imenus data.
If you want independent implementation for which-func, you can find
the current function by going up the tree and looking for the
function_definition node. See the function below for an example.
Since Python allows nested function definitions, that function keeps
going until it reaches the root node, and records all the function
names along the way.
#+begin_src elisp
(defun python-info-treesit-current-defun (&optional include-type)
"Identical to `python-info-current-defun' but use tree-sitter.
For INCLUDE-TYPE see `python-info-current-defun'."
(let ((node (treesit-node-at (point)))
(name-list ())
(type nil))
(cl-loop while node
if (pcase (treesit-node-type node)
("function_definition"
(setq type 'def))
("class_definition"
(setq type 'class))
(_ nil))
do (push (treesit-node-text
(treesit-node-child-by-field-name node "name")
t)
name-list)
do (setq node (treesit-node-parent node))
finally return (concat (if include-type
(format "%s " type)
"")
(string-join name-list ".")))))
#+end_src
* More features?
Obviously this list is just a starting point, if there are features in
the major mode that would benefit a parse tree, adding tree-sitter
support for that would be great. But in the minimal case, just adding
font-lock is awesome.
* Common tasks
How to...
** Get the buffer text corresponding to a node?
(treesit-node-text node)
BTW treesit-node-string does different things.
** Scan the whole tree for stuff?
(treesit-search-subtree)
(treesit-search-forward)
(treesit-induce-sparse-tree)
** Move to next node that...?
(treesit-search-forward-goto)
** Get the root node?
(treesit-buffer-root-node)
** Get the node at point?
(treesit-node-at (point))
* Manual
I suggest you read the manual section for tree-sitter in Info. The
section is Parsing Program Source. Typing
C-h i d m elisp RET g Parsing Program Source RET
will bring you to that section. You can also read the HTML version
under /html-manual in this directory. I find the HTML version easier
to read. You dont need to read through every sentence, just read the
text paragraphs and glance over function names.