Adding Rainbow Bracket Queries
Helix uses rainbows.scm
tree-sitter query files to provide rainbow bracket
functionality.
Tree-sitter queries are documented in the tree-sitter online documentation. If you're writing queries for the first time, be sure to check out the section on syntax highlighting queries and on query syntax.
Rainbow queries have two captures: @rainbow.scope
and @rainbow.bracket
.
@rainbow.scope
should capture any node that increases the nesting level
while @rainbow.bracket
should capture any bracket nodes. Put another way:
@rainbow.scope
switches to the next rainbow color for all nodes in the tree
under it while @rainbow.bracket
paints captured nodes with the current
rainbow color.
For an example, let's add rainbow queries for the tree-sitter query (TSQ)
language itself. These queries will go into a
runtime/queries/tsq/rainbows.scm
file in the repository root.
First we'll add the @rainbow.bracket
captures. TSQ only has parentheses and
square brackets:
["(" ")" "[" "]"] @rainbow.bracket
The ordering of the nodes within the alternation (square brackets) is not taken into consideration.
Note: Why are these nodes quoted? Most syntax highlights capture text surrounded by parentheses. These are named nodes and correspond to the names of rules in the grammar. Brackets are usually written in tree-sitter grammars as literal strings, for example:
{ // ... arguments: seq("(", repeat($.argument), ")"), // ... }
Nodes written as literal strings in tree-sitter grammars may be captured in queries with those same literal strings.
Then we'll add @rainbow.scope
captures. The easiest way to do this is to
view the grammar.js
file in the tree-sitter grammar's repository. For TSQ,
that file is here. As we scroll down the grammar.js
, we
see that the (alternation)
, (L36) (group)
(L57), (named_node)
(L59),
(predicate)
(L87) and (wildcard_node)
(L97) nodes all contain literal
parentheses or square brackets in their definitions. These nodes are all
direct parents of brackets and happen to also be the nodes we want to change
to the next rainbow color, so we capture them as @rainbow.scope
.
[
(group)
(named_node)
(wildcard_node)
(predicate)
(alternation)
] @rainbow.scope
This strategy works as a rule of thumb for most programming and configuration languages. Markup languages can be trickier and may take additional experimentation to find the correct nodes to use for scopes and brackets.
The :tree-sitter-subtree
command shows the syntax tree under the primary
selection in S-expression format and can be a useful tool for determining how
to write a query.
Properties
The rainbow.include-children
property may be applied to @rainbow.scope
captures. By default, all @rainbow.bracket
captures must be direct descendant
of a node captured with @rainbow.scope
in a syntax tree in order to be
highlighted. The rainbow.include-children
property disables that check and
allows @rainbow.bracket
captures to be highlighted if they are direct or
indirect descendants of some node captured with @rainbow.scope
.
For example, this property is used in the HTML rainbow queries.
For a document like <a>link</a>
, the syntax tree is:
(element ; <a>link</a>
(start_tag ; <a>
(tag_name)) ; a
(text) ; link
(end_tag ; </a>
(tag_name))) ; a
If we want to highlight the <
, >
and </
nodes with rainbow colors, we
capture them as @rainbow.bracket
:
["<" ">" "</"] @rainbow.bracket
And we capture (element)
as @rainbow.scope
because (element)
nodes nest
within each other: they increment the nesting level and switch to the next
color in the rainbow.
(element) @rainbow.scope
But this combination of @rainbow.scope
and @rainbow.bracket
will not
highlight any nodes. <
, >
and </
are children of the (start_tag)
and
(end_tag)
nodes. We can't capture (start_tag)
and (end_tag)
as
@rainbow.scope
because they don't nest other elements. We can fix this case
by removing the requirement that <
, >
and </
are direct descendants of
(element)
using the rainbow.include-children
property.
((element) @rainbow.scope
(#set! rainbow.include-children))
With this property set, <
, >
, and </
will highlight with rainbow colors
even though they aren't direct descendents of the (element)
node.
rainbow.include-children
is not necessary for the vast majority of programming
languages. It is only necessary when the node that increments the nesting level
(changes rainbow color) is not the direct parent of the bracket node.