diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index ba330cf77237a..dcd128dee1038 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -17,3 +17,4 @@ - [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 658ea672a54cd..e5100e4300cc9 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -67,6 +67,7 @@ Its settings will be merged with the configuration directory `config.toml` and t | `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` | | `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` | | `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid` +| `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/generated/lang-support.md b/book/src/generated/lang-support.md index ee01c40309c3a..fd960bf5ebb90 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -192,3 +192,185 @@ | yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` | | yuck | ✓ | | | | | zig | ✓ | ✓ | ✓ | `zls` | +| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Rainbow Brackets | Default LSP | +| --- | --- | --- | --- | --- | --- | +| astro | ✓ | | | | | +| awk | ✓ | ✓ | | | `awk-language-server` | +| bash | ✓ | ✓ | ✓ | ✓ | `bash-language-server` | +| bass | ✓ | | | | `bass` | +| beancount | ✓ | | | | | +| bibtex | ✓ | | | | `texlab` | +| bicep | ✓ | | | | `bicep-langserver` | +| blueprint | ✓ | | | | `blueprint-compiler` | +| c | ✓ | ✓ | ✓ | ✓ | `clangd` | +| c-sharp | ✓ | ✓ | | | `OmniSharp` | +| cabal | | | | | | +| cairo | ✓ | ✓ | ✓ | | `cairo-language-server` | +| capnp | ✓ | | ✓ | | | +| clojure | ✓ | | | ✓ | `clojure-lsp` | +| cmake | ✓ | ✓ | ✓ | | `cmake-language-server` | +| comment | ✓ | | | | | +| common-lisp | ✓ | | | ✓ | `cl-lsp` | +| cpon | ✓ | | ✓ | | | +| cpp | ✓ | ✓ | ✓ | ✓ | `clangd` | +| crystal | ✓ | ✓ | | | `crystalline` | +| css | ✓ | | | ✓ | `vscode-css-language-server` | +| cue | ✓ | | | | `cuelsp` | +| d | ✓ | ✓ | ✓ | | `serve-d` | +| dart | ✓ | | ✓ | | `dart` | +| devicetree | ✓ | | | | | +| dhall | ✓ | ✓ | | | `dhall-lsp-server` | +| diff | ✓ | | | | | +| dockerfile | ✓ | | | | `docker-langserver` | +| dot | ✓ | | | | `dot-language-server` | +| dtd | ✓ | | | | | +| edoc | ✓ | | | | | +| eex | ✓ | | | | | +| ejs | ✓ | | | | | +| elixir | ✓ | ✓ | ✓ | ✓ | `elixir-ls` | +| elm | ✓ | ✓ | | | `elm-language-server` | +| elvish | ✓ | | | | `elvish` | +| env | ✓ | | | | | +| erb | ✓ | | | | | +| erlang | ✓ | ✓ | | ✓ | `erlang_ls` | +| esdl | ✓ | | | | | +| fish | ✓ | ✓ | ✓ | | | +| forth | ✓ | | | | `forth-lsp` | +| fortran | ✓ | | ✓ | | `fortls` | +| fsharp | ✓ | | | | `fsautocomplete` | +| gdscript | ✓ | ✓ | ✓ | | | +| gemini | ✓ | | | | | +| git-attributes | ✓ | | | | | +| git-commit | ✓ | ✓ | | | | +| git-config | ✓ | | | | | +| git-ignore | ✓ | | | | | +| git-rebase | ✓ | | | | | +| gleam | ✓ | ✓ | | ✓ | `gleam` | +| glsl | ✓ | ✓ | ✓ | | | +| go | ✓ | ✓ | ✓ | ✓ | `gopls` | +| godot-resource | ✓ | | | | | +| gomod | ✓ | | | | `gopls` | +| gotmpl | ✓ | | | | `gopls` | +| gowork | ✓ | | | | `gopls` | +| graphql | ✓ | | | | | +| hare | ✓ | | | | | +| haskell | ✓ | ✓ | | | `haskell-language-server-wrapper` | +| haskell-persistent | ✓ | | | | | +| hcl | ✓ | | ✓ | | `terraform-ls` | +| heex | ✓ | ✓ | | | `elixir-ls` | +| hosts | ✓ | | | | | +| html | ✓ | | | ✓ | `vscode-html-language-server` | +| hurl | ✓ | | ✓ | | | +| idris | | | | | `idris2-lsp` | +| iex | ✓ | | | | | +| ini | ✓ | | | | | +| java | ✓ | ✓ | ✓ | ✓ | `jdtls` | +| javascript | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` | +| jinja | ✓ | | | | | +| jsdoc | ✓ | | | | | +| json | ✓ | | ✓ | ✓ | `vscode-json-language-server` | +| jsonnet | ✓ | | | | `jsonnet-language-server` | +| jsx | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` | +| julia | ✓ | ✓ | ✓ | | `julia` | +| just | ✓ | ✓ | ✓ | | | +| kdl | ✓ | | | | | +| kotlin | ✓ | | | | `kotlin-language-server` | +| latex | ✓ | ✓ | | | `texlab` | +| lean | ✓ | | | | `lean` | +| ledger | ✓ | | | | | +| llvm | ✓ | ✓ | ✓ | | | +| llvm-mir | ✓ | ✓ | ✓ | | | +| llvm-mir-yaml | ✓ | | ✓ | | | +| lua | ✓ | ✓ | ✓ | | `lua-language-server` | +| make | ✓ | | | | | +| markdoc | ✓ | | | | `markdoc-ls` | +| markdown | ✓ | | | | `marksman` | +| markdown.inline | ✓ | | | | | +| matlab | ✓ | ✓ | ✓ | | | +| mermaid | ✓ | | | | | +| meson | ✓ | | ✓ | | | +| mint | | | | | `mint` | +| msbuild | ✓ | | ✓ | | | +| nasm | ✓ | ✓ | | | | +| nickel | ✓ | | ✓ | | `nls` | +| nim | ✓ | ✓ | ✓ | | `nimlangserver` | +| nix | ✓ | | | ✓ | `nil` | +| nu | ✓ | | | | | +| nunjucks | ✓ | | | | | +| ocaml | ✓ | | ✓ | | `ocamllsp` | +| ocaml-interface | ✓ | | | | `ocamllsp` | +| odin | ✓ | | ✓ | | `ols` | +| opencl | ✓ | ✓ | ✓ | | `clangd` | +| openscad | ✓ | | | | `openscad-lsp` | +| org | ✓ | | | | | +| pascal | ✓ | ✓ | | | `pasls` | +| passwd | ✓ | | | | | +| pem | ✓ | | | | | +| perl | ✓ | | | | `perlnavigator` | +| php | ✓ | ✓ | ✓ | | `intelephense` | +| po | ✓ | ✓ | | | | +| pod | ✓ | | | | | +| ponylang | ✓ | ✓ | ✓ | | | +| prisma | ✓ | | | | `prisma-language-server` | +| prolog | | | | | `swipl` | +| protobuf | ✓ | | ✓ | | `bufls`, `pb` | +| prql | ✓ | | | | | +| purescript | ✓ | | | | `purescript-language-server` | +| python | ✓ | ✓ | ✓ | ✓ | `pylsp` | +| qml | ✓ | | ✓ | | `qmlls` | +| r | ✓ | | | | `R` | +| racket | ✓ | | | ✓ | `racket` | +| regex | ✓ | | | ✓ | | +| rego | ✓ | | | | `regols` | +| rescript | ✓ | ✓ | | | `rescript-language-server` | +| rmarkdown | ✓ | | ✓ | | `R` | +| robot | ✓ | | | | `robotframework_ls` | +| ron | ✓ | | ✓ | | | +| rst | ✓ | | | | | +| ruby | ✓ | ✓ | ✓ | ✓ | `solargraph` | +| rust | ✓ | ✓ | ✓ | ✓ | `rust-analyzer` | +| sage | ✓ | ✓ | | | | +| scala | ✓ | | ✓ | | `metals` | +| scheme | ✓ | | | ✓ | | +| scss | ✓ | | | ✓ | `vscode-css-language-server` | +| slint | ✓ | | ✓ | | `slint-lsp` | +| smithy | ✓ | | | | `cs` | +| sml | ✓ | | | | | +| solidity | ✓ | | | | `solc` | +| sql | ✓ | | | | | +| sshclientconfig | ✓ | | | | | +| starlark | ✓ | ✓ | | ✓ | | +| strace | ✓ | | | | | +| svelte | ✓ | | ✓ | | `svelteserver` | +| sway | ✓ | ✓ | ✓ | | `forc` | +| swift | ✓ | | | | `sourcekit-lsp` | +| t32 | ✓ | | | | | +| tablegen | ✓ | ✓ | ✓ | | | +| task | ✓ | | | | | +| tfvars | ✓ | | ✓ | | `terraform-ls` | +| todotxt | ✓ | | | | | +| toml | ✓ | | | ✓ | `taplo` | +| tsq | ✓ | | | ✓ | | +| tsx | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` | +| twig | ✓ | | | | | +| typescript | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` | +| ungrammar | ✓ | | | | | +| unison | ✓ | | | | | +| uxntal | ✓ | | | | | +| v | ✓ | ✓ | ✓ | | `v-analyzer` | +| vala | ✓ | | | | `vala-language-server` | +| verilog | ✓ | ✓ | | | `svlangserver` | +| vhdl | ✓ | | | | `vhdl_ls` | +| vhs | ✓ | | | | | +| vue | ✓ | | | | `vue-language-server` | +| wast | ✓ | | | | | +| wat | ✓ | | | | | +| webc | ✓ | | | | | +| wgsl | ✓ | | | | `wgsl_analyzer` | +| wit | ✓ | | ✓ | | | +| wren | ✓ | ✓ | ✓ | | | +| xit | ✓ | | | | | +| xml | ✓ | | ✓ | ✓ | | +| yaml | ✓ | | ✓ | ✓ | `yaml-language-server` | +| yuck | ✓ | | | | | +| zig | ✓ | ✓ | ✓ | ✓ | `zls` | diff --git a/book/src/guides/README.md b/book/src/guides/README.md index c25768e68961d..e53983d60fb1f 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 0000000000000..1cba6a9907d1f --- /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 `>, #[serde(default)] pub persistent_diagnostic_sources: Vec, + + /// If set, overrides rainbow brackets for a language. + pub rainbow_brackets: Option, } #[derive(Debug, PartialEq, Eq, Hash)] @@ -640,6 +643,8 @@ impl LanguageConfiguration { // always highlight syntax errors // highlights_query += "\n(ERROR) @error"; + let rainbows_query = read_query(&self.language_id, "rainbows.scm"); + let injections_query = read_query(&self.language_id, "injections.scm"); let locals_query = read_query(&self.language_id, "locals.scm"); @@ -658,6 +663,7 @@ impl LanguageConfiguration { let config = HighlightConfiguration::new( language, &highlights_query, + &rainbows_query, &injections_query, &locals_query, ) @@ -934,6 +940,39 @@ thread_local! { }) } +/// Creates an iterator over the captures in a query within the given range, +/// re-using a cursor from the pool if available. +/// SAFETY: The `QueryCaptures` must be droped before the `QueryCursor` is dropped. +unsafe fn query_captures<'a, 'tree>( + query: &'a Query, + root: Node<'tree>, + source: RopeSlice<'a>, + range: Option>, +) -> ( + QueryCursor, + QueryCaptures<'a, 'tree, RopeProvider<'a>, &'a [u8]>, +) { + // 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, @@ -1267,6 +1306,46 @@ impl Syntax { 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, @@ -1274,37 +1353,23 @@ impl Syntax { 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 { @@ -1321,11 +1386,13 @@ impl Syntax { 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, @@ -1333,9 +1400,95 @@ impl Syntax { 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 @@ -1350,6 +1503,18 @@ impl Syntax { // 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 @@ -1577,7 +1742,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_patterns: Vec, highlights_pattern_index: usize, @@ -1591,6 +1757,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)] @@ -1675,6 +1843,7 @@ impl HighlightConfiguration { pub fn new( language: Grammar, highlights_query: &str, + rainbow_query: &str, injection_query: &str, locals_query: &str, ) -> Result { @@ -1694,6 +1863,7 @@ impl HighlightConfiguration { highlights_pattern_index += 1; } } + let rainbow_query = Query::new(language, rainbow_query)?; let injections_query = Query::new(language, injection_query)?; let combined_injections_patterns = (0..injections_query.pattern_count()) @@ -1725,6 +1895,8 @@ impl HighlightConfiguration { 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() { @@ -1736,6 +1908,15 @@ impl HighlightConfiguration { } } + 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() { @@ -1751,6 +1932,7 @@ impl HighlightConfiguration { Ok(Self { language, query, + rainbow_query, injections_query, combined_injections_patterns, highlights_pattern_index, @@ -1764,6 +1946,8 @@ impl HighlightConfiguration { local_def_capture_index, local_def_value_capture_index, local_ref_capture_index, + rainbow_scope_capture_index, + rainbow_bracket_capture_index, }) } @@ -1903,11 +2087,21 @@ impl HighlightConfiguration { } } -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 @@ -1928,6 +2122,82 @@ impl<'a> HighlightIterLayer<'a> { _ => 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)] @@ -2058,42 +2328,9 @@ impl<'a> HighlightIter<'a> { } 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> { @@ -2245,7 +2482,7 @@ impl<'a> Iterator for HighlightIter<'a> { } } - self.sort_layers(); + sort_layers(&mut self.layers); continue 'main; } @@ -2254,7 +2491,7 @@ impl<'a> Iterator for HighlightIter<'a> { // 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; } } @@ -2272,7 +2509,7 @@ impl<'a> Iterator for HighlightIter<'a> { } } - self.sort_layers(); + sort_layers(&mut self.layers); continue 'main; } } @@ -2307,7 +2544,7 @@ impl<'a> Iterator for HighlightIter<'a> { .emit_event(range.start, Some(HighlightEvent::HighlightStart(highlight))); } - self.sort_layers(); + sort_layers(&mut self.layers); } } } @@ -2517,6 +2754,42 @@ fn pretty_print_tree_impl( Ok(()) } +struct QueryIterLayer<'a> { + cursor: QueryCursor, + captures: RefCell, &'a [u8]>>>, + 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::*; @@ -2546,7 +2819,7 @@ mod test { 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.slice(..), Arc::new(config), Arc::new(loader)).unwrap(); let root = syntax.tree().root_node(); @@ -2608,6 +2881,7 @@ mod test { 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 @@ -2710,7 +2984,7 @@ mod test { }); let language = get_language(language_name).unwrap(); - let config = HighlightConfiguration::new(language, "", "", "").unwrap(); + let config = HighlightConfiguration::new(language, "", "", "", "").unwrap(); let syntax = Syntax::new(source.slice(..), 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 44ae2a2f780b5..650512bc64405 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 @@ impl TsFeature { Self::Highlight => "highlights.scm", Self::TextObject => "textobjects.scm", Self::AutoIndent => "indents.scm", + Self::RainbowBrackets => "rainbows.scm", } } @@ -32,6 +39,7 @@ impl TsFeature { Self::Highlight => "Syntax Highlighting", Self::TextObject => "Treesitter Textobjects", Self::AutoIndent => "Auto Indent", + Self::RainbowBrackets => "Rainbow Brackets", } } @@ -40,6 +48,7 @@ impl TsFeature { 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 f70de622de656..7a170b00a8df7 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -96,6 +96,11 @@ impl EditorView { 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 decorations = DecorationManager::default(); @@ -123,6 +128,12 @@ impl EditorView { 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, @@ -337,6 +348,48 @@ impl EditorView { } } + 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 39f322b2a10f9..ab515c96c38b3 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -296,6 +296,8 @@ pub struct Config { /// Which indent heuristic to use when a new line is inserted #[serde(default)] pub indent_heuristic: IndentationHeuristic, + /// Whether to render rainbow highlights. Defaults to `false`. + pub rainbow_brackets: bool, } #[derive(Debug, Clone, PartialEq, Deserialize, Serialize, Eq, PartialOrd, Ord)] @@ -863,6 +865,7 @@ impl Default for Config { smart_tab: Some(SmartTabConfig::default()), popup_border: PopupBorderConfig::None, indent_heuristic: IndentationHeuristic::default(), + rainbow_brackets: false, } } } @@ -1191,8 +1194,7 @@ impl Editor { 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 4acc56648aa04..16da247f6db63 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