Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Rainbow tree-sitter matches 🌈 #2857

Draft
wants to merge 13 commits into
base: master
Choose a base branch
from

Conversation

the-mikedavis
Copy link
Member

@the-mikedavis the-mikedavis commented Jun 22, 2022

Configurable, themable, nesting rainbow highlights:

demo

This approach uses the syntax tree to guide nesting levels and highlights so it ends up being much more flexible than a naive pair-matching implementation. For example, we can highlight </> when used in Rust type parameters like struct MyStruct<'a> { .. } and not mistakenly highlight </> comparison operators. We can also capture multiple nodes, for example the #, [ and ] characters in a Rust attribute like #[cfg(windows)] and none of these characters are required to be adjacent.

The cherry on top is that we can highlight according to the syntax tree's nesting information rather than whether characters surround each other. For example, in this TOML table, characters is a sub-table of editor.whitespace and the rainbow highlights reflect that fact.

theme = "default"

[editor.whitespace]
# <- red          ^ red
render = "all"
characters = { tab = "", newline = "" }
#            ^ yellow                   ^ yellow (comes after red in base16)

It also works across injected languages like javascript injected into HTML script tags.

(You can see each of these things in action in the gif above.)

How does it work?...

This implementation leverages tree-sitter queries. We use a QueryCaptures Iterator very similar to the existing one for syntax highlights to query the parsed syntax tree for new rainbows.scm patterns.

Two new captures - @rainbow.scope and @rainbow.bracket - track nesting level and capture syntax nodes for highlighting with rainbows. Specifically: @rainbow.scope pushes a RainbowScope on a stack. When @rainbow.bracket matches, we test that the captured node is a direct descendant of the current scope. If it is, we emit highlights for it according to the scope's level of nesting.


Configuration...

In the theme we add a rainbow key that takes a list of Styles. You can have however many you want and you can re-use colors from the palette or even throw modifiers in there:

# ~/.config/helix/themes/my_theme.toml

rainbow = ["#fc6b59", { fg = "lavender", bg = "comet" }, "lilac", { modifiers = ["reversed"] }]

[palette]
lavender = "#a4a0e8"
comet = "#5a5977"
lilac = "#dbbfef"

A default rainbow using the red, yellow, green, blue, cyan, and magenta terminal colors is used as a fallback.

Enable the rendering of rainbow brackets in your config.toml

# ~/.config/helix/config.toml
[editor]
rainbow-brackets = true

Or enable rendering per-language in languages.toml (this overrides the config.toml setting for any languages that set this value):

# ~/.config/helix/languages.toml
[[language]]
name = "scheme"
rainbow-brackets = true

[[language]]
name = "commonlisp"
rainbow-brackets = true

When the rainbow-brackets option is disabled, rainbow brackets are not rendered or computed.


Status...

The current implementation is working well. Take it for a spin and let me know if you find any bugs!

@the-mikedavis the-mikedavis added the S-experimental Status: Ongoing experiment that does not require reviewing and won't be merged in its current state. label Jun 22, 2022
@archseer
Copy link
Member

Surprisingly little code to get this working 👍🏻

@SoraTenshi
Copy link
Contributor

Would it make sense to use the color palette colors as default values if the rainbow feature is enabled?
That was my idea when i tried to work on that.

@the-mikedavis
Copy link
Member Author

That could work but usually the palette has a bunch of different ui colors too, so some deeply nested brackets could end up using backgrounds / grays which may look bad. I think a default rainbow palette not based on the current theme could work ok

@archseer
Copy link
Member

You could cheat and ignore any scopes that start_with("ui.")

@the-mikedavis
Copy link
Member Author

I suspect theme colors may not be good for a theme's rainbow because they might blend in and end up looking confusing. I'll give it a try though - that would take all the guesswork out of having to come up with a default that looks good on all themes 😄

@archseer
Copy link
Member

You could get fancy and calculate the color contrast and only pick colors that are distant enough :D

the-mikedavis added a commit to the-mikedavis/tree-sitter-erlang that referenced this pull request Jun 25, 2022
This allows one to match on bracket nodes with named nodes in queries.
Doing so is useful for tree-sitter based rainbow brackets
(helix-editor/helix#2857).
@atagen
Copy link

atagen commented Jun 25, 2022

You could get fancy and calculate the color contrast and only pick colors that are distant enough :D

well, I have a snippet to calculate the L* luminance value (as in Lab/LCH "professional" colour systems) from an sRGB triple. I use it for exactly that, grading and excluding colours with weak contrast in a CLI app 👍

if interested Helix would be most welcome to it, I can PR it to @the-mikedavis rainbow branch?

@the-mikedavis
Copy link
Member Author

Let's keep this branched focused just on the happy path for now (rainbows enabled and themed). If anything, we might want to merge this once cleaned up without any default behavior - i.e. the theme needs to have a rainbow key in order for rainbows to work - and we can find a good default palette or strategy as a follow-up.

@the-mikedavis
Copy link
Member Author

the-mikedavis commented Jun 25, 2022

The reason I lean towards a hardcoded default that uses red/orange?/yellow/green/blue/purple/etc is that it's easy to get a feel for how things are nested when you can predict the ordering of the bracket colors. If I know blue comes after green, I know where a blue tuple stands inside a huge green map or list literal.

I don't want to discourage discussion on automatically determining a good rainbow though, it sounds like there are some cool color math tricks we could use 🙂

@the-mikedavis

This comment was marked as resolved.

@the-mikedavis
Copy link
Member Author

I found an alternate approach for this that ends up being less complicated. Now it works similarly to the locals tracking system in the syntax highlights iterator. The queries end up being more intuitive to write and more flexible (see the updated PR description).

I think this approach is general-purpose enough: I've worked through 27 languages so far with the toughest being HTML. HTML works now so that nested XML elements behave like other nesting scopes rather than just highlighting the < and > within individual tags.

@the-mikedavis the-mikedavis added S-waiting-on-pr Status: This is waiting on another PR to be merged first and removed S-experimental Status: Ongoing experiment that does not require reviewing and won't be merged in its current state. labels Sep 5, 2022
@kirawi kirawi added A-tree-sitter Area: Tree-sitter A-theme Area: Theme and appearence related labels Sep 13, 2022
@the-mikedavis
Copy link
Member Author

the-mikedavis commented Aug 27, 2023

This branch gets a lot of conflicts but they're usually trivial: changes to the autogenerated lang-support.md file cause conflicts when new languages are merged. I'll push up the latest rebase though - it looks like there have been a bunch of smaller changes that caused true conflicts since I last pushed.

#5176 operates on trees while this branch covers queries. We'll need both to be able to work across injection layers for all tree-sitter features but their implementations are separate. It's basically tree_sitter::TreeCursor (5176) vs tree_sitter::QueryCaptures (this branch).

I've been looking at the mechanism for running queries across injections again as a part of a new tree-sitter-based feature and I found some changes that will make it easier to use this for other tree-sitter features and remove any performance penalties when the feature is turned off, so I will set this as a draft for now. I think it will be better to land the query changes as part of a smaller feature or a feature already in-tree so I'm planning on looking at textobjects after the next release before revisiting this.

@the-mikedavis the-mikedavis marked this pull request as draft August 27, 2023 18:12
@the-mikedavis the-mikedavis added S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. and removed S-waiting-on-review Status: Awaiting review from a maintainer. labels Aug 27, 2023
@the-mikedavis the-mikedavis removed this from the next milestone Aug 27, 2023
omentic added a commit to omentic/helix-ext that referenced this pull request Nov 1, 2023
ref: helix-editor/helix#695
ref: helix-editor/helix#2857

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
This change adds a field to the schema of themes which takes a
list of styles.

    rainbow = ["red", "orange", "yellow", { modifiers = ["reversed"] }]
    [palette]
    red = "#ff0000"
    orange = "#ffa500"
    yellow = "#fff000"

Normal style rules apply for each element in `rainbows`: you can
use definitions from the palette and the full fg/bg/modifiers
notation.

Themes written with `rainbow` keys are not backwards compatible.
Parsing errors will be generated for older versions of Helix
attempting to use themes with `rainbow` keys.

A default rainbow is provided with base16 colors.

This change is made with rainbow pair characters (parens, brackets, etc.)
in mind but it could also be used for other rainbow cosmetic elements
like rainbow indent-guides.
This option is similar to the `rulers` config: it can be set no the
editor key in config and also overridden for specific languages, so
you could enable rainbow brackets for lisps for example without
enabling it globally.
This can be used to calculate rainbow highlights (see the child commit)
or indents or textobjects and be accurate to the injected content
rather than just the root layer. This is useful for languages which
use injections heavily like Vue or JavaScript within HTML but are also
useful in common scenarios like within codeblocks in Markdown.

This iterator shares some code with the HighlightIter and
HighlightIterLayer but that iterator emits HighlightEvents, so it cares
about the beginnings and ends of highlight events rather than captures.
This is an example usage of the query_iter introduced in the parent
commit: captures are returned in order across language layers. We
can use this iterator and a stack for the rainbow scopes to calculate
highlight spans that can be merged into the syntax highlights using
syntax::merge.
We call the rainbow_spans function introduced in the parent commits
over the largest node that contains the current viewport: we need to
reach far enough back in the document that we find the absolute
beginning for brackets. If we run rainbow_spans only over the current
viewport, we get a bug where the color of rainbow brackets changes as
we move the viewport.
the-mikedavis and others added 7 commits January 11, 2024 19:18
Co-authored-by: SoraTenshi <dream@neoncity.dev>
This is brings the fix from d5f17d3 to the QueryIter layers. A trait
for getting the cursor and sort-key from each layer helps cut down on
code duplicated between these iterators.
The code in the `sort_layers` function was fully duplicated between
the HighlightIter and the QueryIter. This change adds a common
`sort_layers` function that accepts a layer Vec from both.
This deduplicates some somewhat complex code between the highlight_iter
and query_iter.
It's easy to mistakenly use-after-free the cursor and captures iterator
here because of the transmute. Ideally this could be fixed upstream in
tree-sitter by introducing an API with lifetimes/types that reflect the
lifetimes of the underlying data.

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
@the-mikedavis the-mikedavis added S-experimental Status: Ongoing experiment that does not require reviewing and won't be merged in its current state. and removed S-waiting-on-author Status: This is awaiting some action (such as code changes or more information) from the author. labels Jan 12, 2024
omentic pushed a commit to omentic/helix-ext that referenced this pull request May 1, 2024
omentic pushed a commit to omentic/helix-ext that referenced this pull request May 1, 2024
omentic pushed a commit to omentic/helix-ext that referenced this pull request May 1, 2024
omentic pushed a commit to omentic/helix-ext that referenced this pull request May 1, 2024
omentic pushed a commit to omentic/helix-ext that referenced this pull request May 1, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-theme Area: Theme and appearence related A-tree-sitter Area: Tree-sitter S-experimental Status: Ongoing experiment that does not require reviewing and won't be merged in its current state.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Bracket pair colourization / Rainbow parenthesis