From eaf8d6d30da5014a3c475c4187b9dccfe621afd5 Mon Sep 17 00:00:00 2001 From: JJ Date: Tue, 31 Oct 2023 18:00:19 -0700 Subject: Add rainbow tree-sitter highlights ref: https://github.com/helix-editor/helix/issues/695 ref: https://github.com/helix-editor/helix/pull/2857 Co-authored-by: Michael Davis --- book/src/SUMMARY.md | 1 + book/src/configuration.md | 1 + book/src/generated/lang-support.md | 370 ++++++++++++------------- 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 +++++++- runtime/queries/bash/rainbows.scm | 21 ++ 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/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 | 10 + 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/tsq/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 + runtime/queries/zig/rainbows.scm | 42 +++ xtask/src/querycheck.rs | 1 + 44 files changed, 1453 insertions(+), 266 deletions(-) create mode 100644 book/src/guides/rainbow_bracket_queries.md 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/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/tsq/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 create mode 100644 runtime/queries/zig/rainbows.scm 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 @@ - [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 d223b6e9..9b0ff36a 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -65,6 +65,7 @@ Its settings will be merged with the configuration directory `config.toml` and t | `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` | | `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` | +| `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 6fdc6f11..04627c8e 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -1,185 +1,185 @@ -| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | 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` | -| gas | ✓ | ✓ | | | -| 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 | ✓ | | | `graphql-lsp` | -| 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` | -| json5 | ✓ | | | | -| 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 | ✓ | | | | -| templ | ✓ | | | `templ` | -| 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`, `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` | +| gas | ✓ | ✓ | | | | +| 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 | ✓ | | | | `graphql-lsp` | +| 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` | +| json5 | ✓ | | | | | +| 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 | ✓ | | | | | +| templ | ✓ | | | | `templ` | +| 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`, `ansible-language-server` | +| yuck | ✓ | | | | | +| zig | ✓ | ✓ | ✓ | ✓ | `zls` | 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 @@ 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"); @@ -642,6 +647,7 @@ impl LanguageConfiguration { let config = HighlightConfiguration::new( language, &highlights_query, + &rainbows_query, &injections_query, &locals_query, ) @@ -918,6 +924,36 @@ 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>>) { + // 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, @@ -1251,6 +1287,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, @@ -1258,37 +1334,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 { @@ -1305,11 +1367,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, @@ -1317,9 +1381,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 @@ -1334,6 +1484,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 @@ -1561,7 +1723,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, @@ -1575,6 +1738,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)] @@ -1659,6 +1824,7 @@ impl HighlightConfiguration { pub fn new( language: Grammar, highlights_query: &str, + rainbow_query: &str, injection_query: &str, locals_query: &str, ) -> Result { @@ -1678,6 +1844,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()) @@ -1709,6 +1876,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() { @@ -1720,6 +1889,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() { @@ -1735,6 +1913,7 @@ impl HighlightConfiguration { Ok(Self { language, query, + rainbow_query, injections_query, combined_injections_patterns, highlights_pattern_index, @@ -1748,6 +1927,8 @@ impl HighlightConfiguration { local_def_capture_index, local_def_value_capture_index, local_ref_capture_index, + rainbow_scope_capture_index, + rainbow_bracket_capture_index, }) } @@ -1888,11 +2069,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 @@ -1913,6 +2104,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)] @@ -2043,42 +2310,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> { @@ -2230,7 +2464,7 @@ impl<'a> Iterator for HighlightIter<'a> { } } - self.sort_layers(); + sort_layers(&mut self.layers); continue 'main; } @@ -2239,7 +2473,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; } } @@ -2257,7 +2491,7 @@ impl<'a> Iterator for HighlightIter<'a> { } } - self.sort_layers(); + sort_layers(&mut self.layers); continue 'main; } } @@ -2292,7 +2526,7 @@ impl<'a> Iterator for HighlightIter<'a> { .emit_event(range.start, Some(HighlightEvent::HighlightStart(highlight))); } - self.sort_layers(); + sort_layers(&mut self.layers); } } } @@ -2502,6 +2736,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::*; @@ -2531,7 +2801,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(); @@ -2593,6 +2863,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 @@ -2695,7 +2966,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 dff90319..978c9c49 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 a305907b..9e30cea4 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -97,6 +97,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 line_decorations: Vec> = Vec::new(); let mut translated_positions: Vec = Vec::new(); @@ -128,6 +133,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, @@ -336,6 +347,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 f285aa99..fbfed656 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -308,6 +308,8 @@ pub struct Config { pub explorer: ExplorerConfig, /// Whether to color modes with different colors. Defaults to `false`. pub color_modes: bool, + /// Whether to render rainbow highlights. Defaults to `false`. + pub rainbow_brackets: bool, pub soft_wrap: SoftWrap, /// Workspace specific lsp ceiling dirs pub workspace_lsp_roots: Vec, @@ -863,6 +865,7 @@ impl Default for Config { indent_guides: IndentGuidesConfig::default(), explorer: ExplorerConfig::default(), color_modes: false, + rainbow_brackets: false, soft_wrap: SoftWrap { enable: Some(false), ..SoftWrap::default() @@ -1203,8 +1206,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 4acc5664..16da247f 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