From 71cc25cbeff0676cd7ca2f879d6c63fb64d6b81b Mon Sep 17 00:00:00 2001 From: Michael Davis Date: Thu, 27 Feb 2025 10:27:57 -0500 Subject: [PATCH 1/2] Add rainbow highlights based on tree-sitter queries --- helix-core/src/syntax.rs | 168 ++++++++++++++++++++++- helix-core/src/syntax/config.rs | 2 + helix-term/src/ui/editor.rs | 33 +++++ helix-view/src/editor.rs | 3 + helix-view/src/theme.rs | 67 ++++++++- runtime/queries/bash/rainbows.scm | 20 +++ runtime/queries/c/rainbows.scm | 29 ++++ runtime/queries/clojure/rainbows.scm | 13 ++ runtime/queries/common-lisp/rainbows.scm | 1 + runtime/queries/cpp/rainbows.scm | 49 +++++++ runtime/queries/css/rainbows.scm | 15 ++ runtime/queries/ecma/rainbows.scm | 28 ++++ runtime/queries/elixir/rainbows.scm | 24 ++++ runtime/queries/erlang/rainbows.scm | 24 ++++ runtime/queries/gleam/rainbows.scm | 32 +++++ runtime/queries/go/rainbows.scm | 33 +++++ runtime/queries/html/rainbows.scm | 13 ++ runtime/queries/java/rainbows.scm | 35 +++++ runtime/queries/javascript/rainbows.scm | 1 + runtime/queries/json/rainbows.scm | 9 ++ runtime/queries/jsx/rainbows.scm | 9 ++ runtime/queries/nix/rainbows.scm | 17 +++ runtime/queries/python/rainbows.scm | 30 ++++ runtime/queries/racket/rainbows.scm | 1 + runtime/queries/regex/rainbows.scm | 17 +++ runtime/queries/ruby/rainbows.scm | 28 ++++ runtime/queries/rust/rainbows.scm | 60 ++++++++ runtime/queries/scheme/rainbows.scm | 12 ++ runtime/queries/scss/rainbows.scm | 3 + runtime/queries/starlark/rainbows.scm | 1 + runtime/queries/toml/rainbows.scm | 12 ++ runtime/queries/tsx/rainbows.scm | 2 + runtime/queries/typescript/rainbows.scm | 19 +++ runtime/queries/xml/rainbows.scm | 29 ++++ runtime/queries/yaml/rainbows.scm | 9 ++ xtask/src/main.rs | 1 + 36 files changed, 844 insertions(+), 5 deletions(-) create mode 100644 runtime/queries/bash/rainbows.scm create mode 100644 runtime/queries/c/rainbows.scm create mode 100644 runtime/queries/clojure/rainbows.scm create mode 100644 runtime/queries/common-lisp/rainbows.scm create mode 100644 runtime/queries/cpp/rainbows.scm create mode 100644 runtime/queries/css/rainbows.scm create mode 100644 runtime/queries/ecma/rainbows.scm create mode 100644 runtime/queries/elixir/rainbows.scm create mode 100644 runtime/queries/erlang/rainbows.scm create mode 100644 runtime/queries/gleam/rainbows.scm create mode 100644 runtime/queries/go/rainbows.scm create mode 100644 runtime/queries/html/rainbows.scm create mode 100644 runtime/queries/java/rainbows.scm create mode 100644 runtime/queries/javascript/rainbows.scm create mode 100644 runtime/queries/json/rainbows.scm create mode 100644 runtime/queries/jsx/rainbows.scm create mode 100644 runtime/queries/nix/rainbows.scm create mode 100644 runtime/queries/python/rainbows.scm create mode 100644 runtime/queries/racket/rainbows.scm create mode 100644 runtime/queries/regex/rainbows.scm create mode 100644 runtime/queries/ruby/rainbows.scm create mode 100644 runtime/queries/rust/rainbows.scm create mode 100644 runtime/queries/scheme/rainbows.scm create mode 100644 runtime/queries/scss/rainbows.scm create mode 100644 runtime/queries/starlark/rainbows.scm create mode 100644 runtime/queries/toml/rainbows.scm create mode 100644 runtime/queries/tsx/rainbows.scm create mode 100644 runtime/queries/typescript/rainbows.scm create mode 100644 runtime/queries/xml/rainbows.scm create mode 100644 runtime/queries/yaml/rainbows.scm diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index e232ee69bb86..f3630a29522c 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -13,14 +13,18 @@ use std::{ use anyhow::{Context, Result}; use arc_swap::{ArcSwap, Guard}; use config::{Configuration, FileType, LanguageConfiguration, LanguageServerConfiguration}; +use foldhash::HashSet; use helix_loader::grammar::get_language; use helix_stdx::rope::RopeSliceExt as _; use once_cell::sync::OnceCell; use ropey::RopeSlice; use tree_house::{ highlighter, - query_iter::QueryIter, - tree_sitter::{Grammar, InactiveQueryCursor, InputEdit, Node, Query, RopeInput, Tree}, + query_iter::{QueryIter, QueryIterEvent}, + tree_sitter::{ + query::{InvalidPredicateError, UserPredicate}, + Capture, Grammar, InactiveQueryCursor, InputEdit, Node, Pattern, Query, RopeInput, Tree, + }, Error, InjectionLanguageMarker, LanguageConfig as SyntaxConfig, Layer, }; @@ -37,6 +41,7 @@ pub struct LanguageData { syntax: OnceCell>, indent_query: OnceCell>, textobject_query: OnceCell>, + rainbow_query: OnceCell>, } impl LanguageData { @@ -46,6 +51,7 @@ impl LanguageData { syntax: OnceCell::new(), indent_query: OnceCell::new(), textobject_query: OnceCell::new(), + rainbow_query: OnceCell::new(), } } @@ -154,6 +160,36 @@ impl LanguageData { .as_ref() } + /// Compiles the rainbows.scm query for a language. + /// This function should only be used by this module or the xtask crate. + pub fn compile_rainbow_query( + grammar: Grammar, + config: &LanguageConfiguration, + ) -> Result> { + let name = &config.language_id; + let text = read_query(name, "rainbows.scm"); + if text.is_empty() { + return Ok(None); + } + let rainbow_query = RainbowQuery::new(grammar, &text) + .with_context(|| format!("Failed to compile rainbows.scm query for '{name}'"))?; + Ok(Some(rainbow_query)) + } + + fn rainbow_query(&self, loader: &Loader) -> Option<&RainbowQuery> { + self.rainbow_query + .get_or_init(|| { + let grammar = self.syntax_config(loader)?.grammar; + Self::compile_rainbow_query(grammar, &self.config) + .map_err(|err| { + log::error!("{err}"); + }) + .ok() + .flatten() + }) + .as_ref() + } + fn reconfigure(&self, scopes: &[String]) { if let Some(Some(config)) = self.syntax.get() { reconfigure_highlights(config, scopes); @@ -324,6 +360,10 @@ impl Loader { self.language(lang).textobject_query(self) } + fn rainbow_query(&self, lang: Language) -> Option<&RainbowQuery> { + self.language(lang).rainbow_query(self) + } + pub fn language_server_configs(&self) -> &HashMap { &self.language_server_configs } @@ -496,6 +536,79 @@ impl Syntax { { QueryIter::new(&self.inner, source, loader, range) } + + pub fn rainbow_highlights( + &self, + source: RopeSlice, + rainbow_length: usize, + loader: &Loader, + range: impl RangeBounds, + ) -> OverlayHighlights { + struct RainbowScope<'tree> { + end: u32, + node: Option>, + highlight: Highlight, + } + + let mut scope_stack = Vec::::new(); + let mut highlights = Vec::new(); + let mut query_iter = self.query_iter::<_, (), _>( + source, + |lang| loader.rainbow_query(lang).map(|q| &q.query), + range, + ); + + while let Some(event) = query_iter.next() { + let QueryIterEvent::Match(mat) = event else { + continue; + }; + + let rainbow_query = loader + .rainbow_query(query_iter.current_language()) + .expect("language must have a rainbow query to emit matches"); + + let byte_range = mat.node.byte_range(); + // Pop any scopes that end before this capture begins. + while scope_stack + .last() + .is_some_and(|scope| byte_range.start >= scope.end) + { + scope_stack.pop(); + } + + let capture = Some(mat.capture); + if capture == rainbow_query.scope_capture { + scope_stack.push(RainbowScope { + end: byte_range.end, + node: if rainbow_query + .include_children_patterns + .contains(&mat.pattern) + { + None + } else { + Some(mat.node.clone()) + }, + highlight: Highlight::new((scope_stack.len() % rainbow_length) as u32), + }); + } else if capture == rainbow_query.bracket_capture { + if let Some(scope) = scope_stack.last() { + if !scope + .node + .as_ref() + .is_some_and(|node| mat.node.parent().as_ref() != Some(node)) + { + let start = source + .byte_to_char(source.floor_char_boundary(byte_range.start as usize)); + let end = + source.byte_to_char(source.ceil_char_boundary(byte_range.end as usize)); + highlights.push((scope.highlight, start..end)); + } + } + } + } + + OverlayHighlights::Heterogenous { highlights } + } } pub type Highlighter<'a> = highlighter::Highlighter<'a, 'a, Loader>; @@ -939,6 +1052,57 @@ fn pretty_print_tree_impl( Ok(()) } +/// Finds the child of `node` which contains the given byte range. + +pub fn child_for_byte_range<'a>(node: &Node<'a>, range: ops::Range) -> Option> { + for child in node.children() { + let child_range = child.byte_range(); + + if range.start >= child_range.start && range.end <= child_range.end { + return Some(child); + } + } + + None +} + +#[derive(Debug)] +pub struct RainbowQuery { + query: Query, + include_children_patterns: HashSet, + scope_capture: Option, + bracket_capture: Option, +} + +impl RainbowQuery { + fn new(grammar: Grammar, source: &str) -> Result { + let mut include_children_patterns = HashSet::default(); + + let query = Query::new(grammar, source, |pattern, predicate| match predicate { + UserPredicate::SetProperty { + key: "rainbow.include-children", + val, + } => { + if val.is_some() { + return Err( + "property 'rainbow.include-children' does not take an argument".into(), + ); + } + include_children_patterns.insert(pattern); + Ok(()) + } + _ => Err(InvalidPredicateError::unknown(predicate)), + })?; + + Ok(Self { + include_children_patterns, + scope_capture: query.get_capture("rainbow.scope"), + bracket_capture: query.get_capture("rainbow.bracket"), + query, + }) + } +} + #[cfg(test)] mod test { use once_cell::sync::Lazy; diff --git a/helix-core/src/syntax/config.rs b/helix-core/src/syntax/config.rs index 432611bb0d38..2152a70b0992 100644 --- a/helix-core/src/syntax/config.rs +++ b/helix-core/src/syntax/config.rs @@ -98,6 +98,8 @@ pub struct LanguageConfiguration { pub workspace_lsp_roots: Option>, #[serde(default)] pub persistent_diagnostic_sources: Vec, + /// Overrides the `editor.rainbow-brackets` config key for the language. + pub rainbow_brackets: Option, } impl LanguageConfiguration { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 9343d55d4083..1f0ff4b3ee44 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -127,6 +127,18 @@ impl EditorView { &text_annotations, )); + if doc + .language_config() + .and_then(|config| config.rainbow_brackets) + .unwrap_or(config.rainbow_brackets) + { + if let Some(overlay) = + Self::doc_rainbow_highlights(doc, view_offset.anchor, inner.height, theme, &loader) + { + overlays.push(overlay); + } + } + Self::doc_diagnostics_highlights_into(doc, theme, &mut overlays); if is_focused { @@ -304,6 +316,27 @@ impl EditorView { text_annotations.collect_overlay_highlights(range) } + pub fn doc_rainbow_highlights( + doc: &Document, + anchor: usize, + height: u16, + theme: &Theme, + loader: &syntax::Loader, + ) -> Option { + let syntax = doc.syntax()?; + let text = doc.text().slice(..); + let row = text.char_to_line(anchor.min(text.len_chars())); + let visible_range = Self::viewport_byte_range(text, row, height); + let start = syntax::child_for_byte_range( + &syntax.tree().root_node(), + visible_range.start as u32..visible_range.end as u32, + ) + .map_or(visible_range.start as u32, |node| node.start_byte()); + let range = start..visible_range.end as u32; + + Some(syntax.rainbow_highlights(text, theme.rainbow_length(), loader, range)) + } + /// Get highlight spans for document diagnostics pub fn doc_diagnostics_highlights_into( doc: &Document, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9aa073fcf516..ddad61f71e03 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -373,6 +373,8 @@ pub struct Config { /// Whether to read settings from [EditorConfig](https://editorconfig.org) files. Defaults to /// `true`. pub editor_config: bool, + /// Whether to render rainbow colors for matching brackets. Defaults to `false`. + pub rainbow_brackets: bool, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] @@ -1024,6 +1026,7 @@ impl Default for Config { end_of_line_diagnostics: DiagnosticFilter::Disable, clipboard_provider: ClipboardProvider::default(), editor_config: true, + rainbow_brackets: false, } } } diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 61d490ff3978..6a18d321e19c 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -227,6 +227,7 @@ pub struct Theme { // tree-sitter highlight styles are stored in a Vec to optimize lookups scopes: Vec, highlights: Vec