From bbc6c8ad315d6a8830d41f1eac59ddc8b31e799c Mon Sep 17 00:00:00 2001 From: JJ Date: Sat, 15 Jul 2023 19:00:54 -0700 Subject: [PATCH 1/2] Add rainbow tree-sitter highlights ref: https://github.com/helix-editor/helix/pull/2857 --- book/src/SUMMARY.md | 1 + book/src/configuration.md | 1 + book/src/guides/README.md | 2 +- book/src/guides/rainbow_bracket_queries.md | 132 +++++++ book/src/languages.md | 2 + book/src/themes.md | 11 + helix-core/src/syntax.rs | 417 +++++++++++++++++---- helix-term/src/health.rs | 11 +- helix-term/src/ui/editor.rs | 53 +++ helix-view/src/editor.rs | 6 +- helix-view/src/theme.rs | 113 +++++- xtask/src/querycheck.rs | 1 + 12 files changed, 669 insertions(+), 81 deletions(-) create mode 100644 book/src/guides/rainbow_bracket_queries.md diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index ba330cf7..dcd128de 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -17,3 +17,4 @@ # Summary - [Adding textobject queries](./guides/textobject.md) - [Adding indent queries](./guides/indent.md) - [Adding injection queries](./guides/injection.md) + - [Adding rainbow bracket queries](./guides/rainbow_bracket_queries.md) diff --git a/book/src/configuration.md b/book/src/configuration.md index bed20b28..32a4aac4 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -65,6 +65,7 @@ ### `[editor]` Section | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` | | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | | `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` | +| `rainbow-brackets` | Whether to render rainbow colors for matching brackets. Requires tree-sitter `rainbows.scm` queries for the language. | `false` | ### `[editor.statusline]` Section diff --git a/book/src/guides/README.md b/book/src/guides/README.md index c25768e6..e53983d6 100644 --- a/book/src/guides/README.md +++ b/book/src/guides/README.md @@ -1,4 +1,4 @@ # Guides This section contains guides for adding new language server configurations, -tree-sitter grammars, textobject queries, and other similar items. +tree-sitter grammars, textobject and rainbow bracket queries, and other similar items. diff --git a/book/src/guides/rainbow_bracket_queries.md b/book/src/guides/rainbow_bracket_queries.md new file mode 100644 index 00000000..1cba6a99 --- /dev/null +++ b/book/src/guides/rainbow_bracket_queries.md @@ -0,0 +1,132 @@ +# 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: + +```tsq +["(" ")" "[" "]"] @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: +> +> ```js +> { +> // ... +> 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][tsq grammar.js]. 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`. + +```tsq +[ + (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 `link`, the syntax tree is: + +```tsq +(element ; link + (start_tag ; + (tag_name)) ; a + (text) ; link + (end_tag ; + (tag_name))) ; a +``` + +If we want to highlight the `<`, `>` and `" "` and `` and ``, and `>, + + /// If set, overrides rainbow brackets for a language. + pub rainbow_brackets: Option, } #[derive(Debug, PartialEq, Eq, Hash)] @@ -624,6 +627,8 @@ fn initialize_highlight(&self, scopes: &[String]) -> Option Option( + query: &'a Query, + root: Node<'tree>, + source: RopeSlice<'a>, + range: Option>, +) -> (QueryCursor, QueryCaptures<'a, 'tree, RopeProvider<'a>>) { + // Reuse a cursor from the pool if available. + let mut cursor = PARSER.with(|ts_parser| { + let highlighter = &mut ts_parser.borrow_mut(); + highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) + }); + + // This is the unsafe line: + // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which + // prevents them from being moved. But both of these values are really just + // pointers, so it's actually ok to move them. + let cursor_ref = mem::transmute::<_, &'static mut QueryCursor>(&mut cursor); + + // if reusing cursors & no range this resets to whole range + cursor_ref.set_byte_range(range.unwrap_or(0..usize::MAX)); + cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT); + + let captures = cursor_ref.captures(query, root, RopeProvider(source)); + + (cursor, captures) +} + #[derive(Debug)] pub struct Syntax { layers: HopSlotMap, @@ -1248,6 +1284,46 @@ pub fn tree(&self) -> &Tree { self.layers[self.root].tree() } + /// Iterate over all captures for a query across injection layers. + fn query_iter<'a, F>( + &'a self, + query_fn: F, + source: RopeSlice<'a>, + range: Option>, + ) -> impl Iterator, usize)> + where + F: Fn(&'a HighlightConfiguration) -> &'a Query, + { + let mut layers: Vec<_> = self + .layers + .iter() + .filter_map(|(_, layer)| { + let (cursor, captures) = unsafe { + query_captures( + query_fn(&layer.config), + layer.tree().root_node(), + source, + range.clone(), + ) + }; + let mut captures = captures.peekable(); + + // If there aren't any captures for this layer, skip the layer. + captures.peek()?; + + Some(QueryIterLayer { + cursor, + captures: RefCell::new(captures), + layer, + }) + }) + .collect(); + + layers.sort_unstable_by_key(|layer| layer.sort_key()); + + QueryIter { layers } + } + /// Iterate over the highlighted regions for a given slice of source code. pub fn highlight_iter<'a>( &'a self, @@ -1255,37 +1331,23 @@ pub fn highlight_iter<'a>( range: Option>, cancellation_flag: Option<&'a AtomicUsize>, ) -> impl Iterator> + 'a { - let mut layers = self + let mut layers: Vec<_> = self .layers .iter() .filter_map(|(_, layer)| { // TODO: if range doesn't overlap layer range, skip it - // Reuse a cursor from the pool if available. - let mut cursor = PARSER.with(|ts_parser| { - let highlighter = &mut ts_parser.borrow_mut(); - highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) - }); - - // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which - // prevents them from being moved. But both of these values are really just - // pointers, so it's actually ok to move them. - let cursor_ref = - unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; - - // if reusing cursors & no range this resets to whole range - cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); - cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT); - - let mut captures = cursor_ref - .captures( + let (cursor, captures) = unsafe { + query_captures( &layer.config.query, layer.tree().root_node(), - RopeProvider(source), + source, + range.clone(), ) - .peekable(); + }; + let mut captures = captures.peekable(); - // If there's no captures, skip the layer + // If there are no captures, skip the layer captures.peek()?; Some(HighlightIterLayer { @@ -1302,11 +1364,13 @@ pub fn highlight_iter<'a>( depth: layer.depth, // TODO: just reuse `layer` }) }) - .collect::>(); + .collect(); layers.sort_unstable_by_key(|layer| layer.sort_key()); - let mut result = HighlightIter { + sort_layers(&mut layers); + + HighlightIter { source, byte_offset: range.map_or(0, |r| r.start), cancellation_flag, @@ -1314,9 +1378,95 @@ pub fn highlight_iter<'a>( layers, next_event: None, last_highlight_range: None, - }; - result.sort_layers(); - result + } + } + + /// Queries for rainbow highlights in the given range. + pub fn rainbow_spans<'a>( + &'a self, + source: RopeSlice<'a>, + query_range: Option>, + rainbow_length: usize, + ) -> Vec<(usize, std::ops::Range)> { + struct RainbowScope { + end: usize, + node_id: Option, + highlight: usize, + } + + let mut spans = Vec::new(); + let mut scope_stack: Vec = Vec::new(); + + // Calculating rainbow highlights is similar to determining local highlights + // in the highlight iterator. We iterate over the query captures for + // `@rainbow.scope` and `@rainbow.bracket`: + // + // * `@rainbow.scope`: pushes a new `RainbowScope` onto the `scope_stack` + // stack. The number of `RainbowScope`s is the level of nesting within + // brackets and determines which color of the rainbow should be used as + // a highlight: `scope_stack.len() % rainbow_length`. + // + // * `@rainbow.bracket`: adds a new highlight span to the `spans` Vec. + // A `@rainbow.bracket` capture only creates a new highlight if that node + // is a child node of the latest node captured with `@rainbow.scope`, + // or if the last `RainbowScope` on the `scope_stack` was captured with + // the `(set! rainbow.include-children)` property. + // + // The iterator over the query captures returns captures across injection + // layers sorted by the earliest captures in the document first, so + // highlight colors are calculated correctly across injection layers. + + // Iterate over all of the captures for rainbow queries across injections. + for (layer, match_, capture_index) in + self.query_iter(|config| &config.rainbow_query, source, query_range) + { + let capture = match_.captures[capture_index]; + let range = capture.node.byte_range(); + + // If any scope in the stack ends before this new capture begins, + // pop the scope out of the scope stack. + while let Some(scope) = scope_stack.last() { + if range.start >= scope.end { + scope_stack.pop(); + } else { + break; + } + } + + if Some(capture.index) == layer.config.rainbow_scope_capture_index { + // If the capture is a "rainbow.scope", push it onto the scope stack. + let mut scope = RainbowScope { + end: range.end, + node_id: Some(capture.node.id()), + highlight: scope_stack.len() % rainbow_length, + }; + for prop in layer + .config + .rainbow_query + .property_settings(match_.pattern_index) + { + if prop.key.as_ref() == "rainbow.include-children" { + scope.node_id = None; + } + } + scope_stack.push(scope); + } else if Some(capture.index) == layer.config.rainbow_bracket_capture_index { + // If the capture is a "rainbow.bracket", check that the top of the scope stack + // is a valid scope for the bracket. The scope is valid if: + // * The scope's node is the direct parent of the captured node. + // * The scope has the "rainbow.include-children" property set. This allows the + // scope to match all descendant nodes in its range. + if let Some(scope) = scope_stack.last() { + if scope.node_id.is_none() + || capture.node.parent().map(|p| p.id()) == scope.node_id + { + spans.push((scope.highlight, range)); + } + } + } + } + + spans } // Commenting @@ -1331,6 +1481,18 @@ pub fn highlight_iter<'a>( // TODO: Folding } +/// Finds the child of `node` which contains the given byte range `range`. +pub fn child_for_byte_range(node: Node, range: std::ops::Range) -> Option { + for child in node.children(&mut node.walk()) { + let child_range = child.byte_range(); + if range.start >= child_range.start && range.end <= child_range.end { + return Some(child); + } + } + + None +} + bitflags! { /// Flags that track the status of a layer /// in the `Sytaxn::update` function @@ -1558,7 +1720,8 @@ pub enum HighlightEvent { #[derive(Debug)] pub struct HighlightConfiguration { pub language: Grammar, - pub query: Query, + query: Query, + rainbow_query: Query, injections_query: Query, combined_injections_query: Option, highlights_pattern_index: usize, @@ -1572,6 +1735,8 @@ pub struct HighlightConfiguration { local_def_capture_index: Option, local_def_value_capture_index: Option, local_ref_capture_index: Option, + rainbow_scope_capture_index: Option, + rainbow_bracket_capture_index: Option, } #[derive(Debug)] @@ -1656,6 +1821,7 @@ impl HighlightConfiguration { pub fn new( language: Grammar, highlights_query: &str, + rainbow_query: &str, injection_query: &str, locals_query: &str, ) -> Result { @@ -1675,6 +1841,7 @@ pub fn new( highlights_pattern_index += 1; } } + let rainbow_query = Query::new(language, rainbow_query)?; let mut injections_query = Query::new(language, injection_query)?; @@ -1717,6 +1884,8 @@ pub fn new( let mut local_def_value_capture_index = None; let mut local_ref_capture_index = None; let mut local_scope_capture_index = None; + let mut rainbow_scope_capture_index = None; + let mut rainbow_bracket_capture_index = None; for (i, name) in query.capture_names().iter().enumerate() { let i = Some(i as u32); match name.as_str() { @@ -1728,6 +1897,15 @@ pub fn new( } } + for (i, name) in rainbow_query.capture_names().iter().enumerate() { + let i = Some(i as u32); + match name.as_str() { + "rainbow.scope" => rainbow_scope_capture_index = i, + "rainbow.bracket" => rainbow_bracket_capture_index = i, + _ => {} + } + } + for (i, name) in injections_query.capture_names().iter().enumerate() { let i = Some(i as u32); match name.as_str() { @@ -1743,6 +1921,7 @@ pub fn new( Ok(Self { language, query, + rainbow_query, injections_query, combined_injections_query, highlights_pattern_index, @@ -1756,6 +1935,8 @@ pub fn new( local_def_capture_index, local_def_value_capture_index, local_ref_capture_index, + rainbow_scope_capture_index, + rainbow_bracket_capture_index, }) } @@ -1896,11 +2077,21 @@ fn injection_for_match<'a>( } } -impl<'a> HighlightIterLayer<'a> { - // First, sort scope boundaries by their byte offset in the document. At a - // given position, emit scope endings before scope beginnings. Finally, emit - // scope boundaries from deeper layers first. - fn sort_key(&self) -> Option<(usize, bool, isize)> { +trait IterLayer { + type SortKey: PartialOrd; + + fn sort_key(&self) -> Option; + + fn cursor(self) -> QueryCursor; +} + +impl<'a> IterLayer for HighlightIterLayer<'a> { + type SortKey = (usize, bool, isize); + + fn sort_key(&self) -> Option { + // First, sort scope boundaries by their byte offset in the document. At a + // given position, emit scope endings before scope beginnings. Finally, emit + // scope boundaries from deeper layers first. let depth = -(self.depth as isize); let next_start = self .captures @@ -1921,6 +2112,82 @@ fn sort_key(&self) -> Option<(usize, bool, isize)> { _ => None, } } + + fn cursor(self) -> QueryCursor { + self.cursor + } +} + +impl<'a> IterLayer for QueryIterLayer<'a> { + type SortKey = (usize, isize); + + fn sort_key(&self) -> Option { + // Sort the layers so that the first layer in the Vec has the next + // capture ordered by start byte and depth (descending). + let depth = -(self.layer.depth as isize); + let mut captures = self.captures.borrow_mut(); + let (match_, capture_index) = captures.peek()?; + let start = match_.captures[*capture_index].node.start_byte(); + + Some((start, depth)) + } + + fn cursor(self) -> QueryCursor { + self.cursor + } +} + +/// Re-sort the given layers so that the next capture for the `layers[0]` is +/// the earliest capture in the document for all layers. +/// +/// This function assumes that `layers` is already sorted except for the +/// first layer in the `Vec`. This function shifts the first layer later in +/// the `Vec` after any layers with earlier captures. +/// +/// This is quicker than a regular full sort: it can only take as many +/// iterations as the number of layers and usually takes many fewer than +/// the full number of layers. The case when `layers[0]` is already the +/// layer with the earliest capture and the sort is a no-op is a fast-lane +/// which only takes one comparison operation. +/// +/// This function also removes any layers which have no more query captures +/// to emit. +fn sort_layers(layers: &mut Vec) { + while !layers.is_empty() { + // If `Layer::sort_key` returns `None`, the layer has no more captures + // to emit and can be removed. + if let Some(sort_key) = layers[0].sort_key() { + let mut i = 0; + while i + 1 < layers.len() { + if let Some(next_offset) = layers[i + 1].sort_key() { + // Compare `0`'s sort key to `i + 1`'s. If `i + 1` comes + // before `0`, shift the `0` layer so it comes after the + // `i + 1` layers. + if next_offset < sort_key { + i += 1; + continue; + } + } else { + let layer = layers.remove(i + 1); + PARSER.with(|ts_parser| { + let highlighter = &mut ts_parser.borrow_mut(); + highlighter.cursors.push(layer.cursor()); + }); + } + break; + } + if i > 0 { + layers[0..(i + 1)].rotate_left(1); + } + break; + } else { + let layer = layers.remove(0); + PARSER.with(|ts_parser| { + let highlighter = &mut ts_parser.borrow_mut(); + highlighter.cursors.push(layer.cursor()); + }); + } + } } #[derive(Clone)] @@ -2051,42 +2318,9 @@ fn emit_event( } else { result = event.map(Ok); } - self.sort_layers(); + sort_layers(&mut self.layers); result } - - fn sort_layers(&mut self) { - while !self.layers.is_empty() { - if let Some(sort_key) = self.layers[0].sort_key() { - let mut i = 0; - while i + 1 < self.layers.len() { - if let Some(next_offset) = self.layers[i + 1].sort_key() { - if next_offset < sort_key { - i += 1; - continue; - } - } else { - let layer = self.layers.remove(i + 1); - PARSER.with(|ts_parser| { - let highlighter = &mut ts_parser.borrow_mut(); - highlighter.cursors.push(layer.cursor); - }); - } - break; - } - if i > 0 { - self.layers[0..(i + 1)].rotate_left(1); - } - break; - } else { - let layer = self.layers.remove(0); - PARSER.with(|ts_parser| { - let highlighter = &mut ts_parser.borrow_mut(); - highlighter.cursors.push(layer.cursor); - }); - } - } - } } impl<'a> Iterator for HighlightIter<'a> { @@ -2238,7 +2472,7 @@ fn next(&mut self) -> Option { } } - self.sort_layers(); + sort_layers(&mut self.layers); continue 'main; } @@ -2247,7 +2481,7 @@ fn next(&mut self) -> Option { // a different layer, then skip over this one. if let Some((last_start, last_end, last_depth)) = self.last_highlight_range { if range.start == last_start && range.end == last_end && layer.depth < last_depth { - self.sort_layers(); + sort_layers(&mut self.layers); continue 'main; } } @@ -2265,7 +2499,7 @@ fn next(&mut self) -> Option { } } - self.sort_layers(); + sort_layers(&mut self.layers); continue 'main; } } @@ -2300,7 +2534,7 @@ fn next(&mut self) -> Option { .emit_event(range.start, Some(HighlightEvent::HighlightStart(highlight))); } - self.sort_layers(); + sort_layers(&mut self.layers); } } } @@ -2510,6 +2744,42 @@ fn pretty_print_tree_impl( Ok(()) } +struct QueryIterLayer<'a> { + cursor: QueryCursor, + captures: RefCell>>>, + layer: &'a LanguageLayer, +} + +impl<'a> fmt::Debug for QueryIterLayer<'a> { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + f.debug_struct("QueryIterLayer").finish() + } +} + +#[derive(Debug)] +pub struct QueryIter<'a> { + layers: Vec>, +} + +impl<'a> Iterator for QueryIter<'a> { + type Item = (&'a LanguageLayer, QueryMatch<'a, 'a>, usize); + + fn next(&mut self) -> Option { + // Sort the layers so that the first layer contains the next capture. + sort_layers(&mut self.layers); + + // Emit the next capture from the lowest layer. If there are no more + // layers, terminate. + let layer = self.layers.get_mut(0)?; + let inner = layer.layer; + layer + .captures + .borrow_mut() + .next() + .map(|(match_, index)| (inner, match_, index)) + } +} + #[cfg(test)] mod test { use super::*; @@ -2539,7 +2809,7 @@ fn test_textobject_queries() { let textobject = TextObjectQuery { query }; let mut cursor = QueryCursor::new(); - let config = HighlightConfiguration::new(language, "", "", "").unwrap(); + let config = HighlightConfiguration::new(language, "", "", "", "").unwrap(); let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap(); let root = syntax.tree().root_node(); @@ -2601,6 +2871,7 @@ fn test_parser() { language, &std::fs::read_to_string("../runtime/grammars/sources/rust/queries/highlights.scm") .unwrap(), + "", // rainbows.scm &std::fs::read_to_string("../runtime/grammars/sources/rust/queries/injections.scm") .unwrap(), "", // locals.scm @@ -2703,7 +2974,7 @@ fn assert_pretty_print( }); let language = get_language(language_name).unwrap(); - let config = HighlightConfiguration::new(language, "", "", "").unwrap(); + let config = HighlightConfiguration::new(language, "", "", "", "").unwrap(); let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader)).unwrap(); let root = syntax diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 8f921877..462cdf3f 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -12,11 +12,17 @@ pub enum TsFeature { Highlight, TextObject, AutoIndent, + RainbowBrackets, } impl TsFeature { pub fn all() -> &'static [Self] { - &[Self::Highlight, Self::TextObject, Self::AutoIndent] + &[ + Self::Highlight, + Self::TextObject, + Self::AutoIndent, + Self::RainbowBrackets, + ] } pub fn runtime_filename(&self) -> &'static str { @@ -24,6 +30,7 @@ pub fn runtime_filename(&self) -> &'static str { Self::Highlight => "highlights.scm", Self::TextObject => "textobjects.scm", Self::AutoIndent => "indents.scm", + Self::RainbowBrackets => "rainbows.scm", } } @@ -32,6 +39,7 @@ pub fn long_title(&self) -> &'static str { Self::Highlight => "Syntax Highlighting", Self::TextObject => "Treesitter Textobjects", Self::AutoIndent => "Auto Indent", + Self::RainbowBrackets => "Rainbow Brackets", } } @@ -40,6 +48,7 @@ pub fn short_title(&self) -> &'static str { Self::Highlight => "Highlight", Self::TextObject => "Textobject", Self::AutoIndent => "Indent", + Self::RainbowBrackets => "Rainbow", } } } diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 81f8fe22..63590e94 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -94,6 +94,11 @@ pub fn render_view( let theme = &editor.theme; let config = editor.config(); + let should_render_rainbow_brackets = doc + .language_config() + .and_then(|lang_config| lang_config.rainbow_brackets) + .unwrap_or(config.rainbow_brackets); + let text_annotations = view.text_annotations(doc, Some(theme)); let mut line_decorations: Vec> = Vec::new(); let mut translated_positions: Vec = Vec::new(); @@ -125,6 +130,12 @@ pub fn render_view( let mut highlights = Self::doc_syntax_highlights(doc, view.offset.anchor, inner.height, theme); + if should_render_rainbow_brackets { + highlights = Box::new(syntax::merge( + highlights, + Self::doc_rainbow_highlights(doc, view.offset.anchor, inner.height, theme), + )); + } let overlay_highlights = Self::overlay_syntax_highlights( doc, view.offset.anchor, @@ -330,6 +341,48 @@ pub fn doc_syntax_highlights<'doc>( } } + pub fn doc_rainbow_highlights( + doc: &Document, + anchor: usize, + height: u16, + theme: &Theme, + ) -> Vec<(usize, std::ops::Range)> { + let syntax = match doc.syntax() { + Some(syntax) => syntax, + None => return Vec::new(), + }; + + let text = doc.text().slice(..); + let row = text.char_to_line(anchor.min(text.len_chars())); + + // calculate viewport byte ranges + let last_line = doc.text().len_lines().saturating_sub(1); + let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line); + let visible_start = text.line_to_byte(row.min(last_line)); + let visible_end = text.line_to_byte(last_visible_line + 1); + + // The calculation for the current nesting level for rainbow highlights + // depends on where we start the iterator from. For accuracy, we start + // the iterator further back than the viewport: at the start of the containing + // non-root syntax-tree node. Any spans that are off-screen are truncated when + // the spans are merged via [syntax::merge]. + let syntax_node_start = + syntax::child_for_byte_range(syntax.tree().root_node(), visible_start..visible_start) + .map_or(visible_start, |node| node.byte_range().start); + let syntax_node_range = syntax_node_start..visible_end; + + let mut spans = syntax.rainbow_spans(text, Some(syntax_node_range), theme.rainbow_length()); + + for (_highlight, range) in spans.iter_mut() { + let start = text.byte_to_char(ensure_grapheme_boundary_next_byte(text, range.start)); + let end = text.byte_to_char(ensure_grapheme_boundary_next_byte(text, range.end)); + + *range = start..end; + } + + spans + } + /// Get highlight spans for document diagnostics pub fn doc_diagnostics_highlights( doc: &Document, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 1ab5f976..c0410f7d 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -316,6 +316,8 @@ pub struct Config { pub workspace_lsp_roots: Vec, /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`. pub default_line_ending: LineEndingConfig, + /// Whether to render rainbow highlights. Defaults to `false`. + pub rainbow_brackets: bool, } #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] @@ -851,6 +853,7 @@ fn default() -> Self { completion_replace: false, workspace_lsp_roots: Vec::new(), default_line_ending: LineEndingConfig::default(), + rainbow_brackets: false, } } } @@ -1181,8 +1184,7 @@ fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) { return; } - let scopes = theme.scopes(); - self.syn_loader.set_scopes(scopes.to_vec()); + self.syn_loader.set_scopes(theme.scopes().to_vec()); match preview { ThemeAction::Preview => { diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index bf3379ca..2b024384 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -216,17 +216,19 @@ pub struct Theme { // tree-sitter highlight styles are stored in a Vec to optimize lookups scopes: Vec, highlights: Vec