aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.cargo/config2
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md1
-rw-r--r--.github/workflows/build.yml50
-rw-r--r--.github/workflows/release.yml2
-rw-r--r--.gitmodules78
-rw-r--r--CHANGELOG.md119
-rw-r--r--Cargo.lock320
-rw-r--r--Cargo.toml2
-rw-r--r--README.md20
-rw-r--r--TODO.md13
-rw-r--r--base16_theme.toml51
-rw-r--r--book/src/SUMMARY.md4
-rw-r--r--book/src/commands.md5
-rw-r--r--book/src/configuration.md46
-rw-r--r--book/src/generated/lang-support.md63
-rw-r--r--book/src/generated/typable-cmd.md48
-rw-r--r--book/src/guides/adding_languages.md39
-rw-r--r--book/src/install.md9
-rw-r--r--book/src/keymap.md87
-rw-r--r--book/src/lang-support.md10
-rw-r--r--book/src/languages.md1
-rw-r--r--book/src/remapping.md10
-rw-r--r--book/src/themes.md46
-rw-r--r--book/src/usage.md2
-rw-r--r--docs/CONTRIBUTING.md37
-rw-r--r--flake.lock37
-rw-r--r--flake.nix81
-rw-r--r--helix-core/Cargo.toml18
-rw-r--r--helix-core/src/auto_pairs.rs781
-rw-r--r--helix-core/src/chars.rs9
-rw-r--r--helix-core/src/diagnostic.rs15
-rw-r--r--helix-core/src/diff.rs6
-rw-r--r--helix-core/src/graphemes.rs86
-rw-r--r--helix-core/src/history.rs4
-rw-r--r--helix-core/src/increment/date_time.rs490
-rw-r--r--helix-core/src/increment/mod.rs8
-rw-r--r--helix-core/src/increment/number.rs (renamed from helix-core/src/numbers.rs)40
-rw-r--r--helix-core/src/indent.rs147
-rw-r--r--helix-core/src/lib.rs44
-rw-r--r--helix-core/src/line_ending.rs2
-rw-r--r--helix-core/src/match_brackets.rs2
-rw-r--r--helix-core/src/movement.rs52
-rw-r--r--helix-core/src/object.rs73
-rw-r--r--helix-core/src/position.rs12
-rw-r--r--helix-core/src/register.rs4
-rw-r--r--helix-core/src/selection.rs109
-rw-r--r--helix-core/src/shellwords.rs164
-rw-r--r--helix-core/src/surround.rs1
-rw-r--r--helix-core/src/syntax.rs1260
-rw-r--r--helix-core/src/transaction.rs42
-rw-r--r--helix-dap/Cargo.toml4
-rw-r--r--helix-dap/src/transport.rs3
-rw-r--r--helix-lsp/Cargo.toml8
-rw-r--r--helix-lsp/src/client.rs30
-rw-r--r--helix-lsp/src/lib.rs36
-rw-r--r--helix-syntax/Cargo.toml2
-rw-r--r--helix-syntax/README.md13
-rw-r--r--helix-syntax/build.rs1
m---------helix-syntax/languages/tree-sitter-comment0
m---------helix-syntax/languages/tree-sitter-dart0
m---------helix-syntax/languages/tree-sitter-dockerfile0
m---------helix-syntax/languages/tree-sitter-elm0
m---------helix-syntax/languages/tree-sitter-fish0
m---------helix-syntax/languages/tree-sitter-git-commit0
m---------helix-syntax/languages/tree-sitter-git-config0
m---------helix-syntax/languages/tree-sitter-git-diff0
m---------helix-syntax/languages/tree-sitter-git-rebase0
m---------helix-syntax/languages/tree-sitter-go0
m---------helix-syntax/languages/tree-sitter-graphql0
m---------helix-syntax/languages/tree-sitter-haskell0
m---------helix-syntax/languages/tree-sitter-iex0
m---------helix-syntax/languages/tree-sitter-lean0
m---------helix-syntax/languages/tree-sitter-llvm0
m---------helix-syntax/languages/tree-sitter-llvm-mir0
m---------helix-syntax/languages/tree-sitter-make0
m---------helix-syntax/languages/tree-sitter-markdown0
m---------helix-syntax/languages/tree-sitter-php0
m---------helix-syntax/languages/tree-sitter-regex0
m---------helix-syntax/languages/tree-sitter-rescript0
m---------helix-syntax/languages/tree-sitter-scala0
m---------helix-syntax/languages/tree-sitter-tablegen0
m---------helix-syntax/languages/tree-sitter-twig0
m---------helix-syntax/languages/tree-sitter-zig0
-rw-r--r--helix-term/Cargo.toml17
-rw-r--r--helix-term/build.rs15
-rw-r--r--helix-term/src/application.rs139
-rw-r--r--helix-term/src/args.rs45
-rw-r--r--helix-term/src/commands.rs1358
-rw-r--r--helix-term/src/commands/dap.rs8
-rw-r--r--helix-term/src/compositor.rs36
-rw-r--r--helix-term/src/config.rs51
-rw-r--r--helix-term/src/job.rs18
-rw-r--r--helix-term/src/keymap.rs68
-rw-r--r--helix-term/src/lib.rs11
-rw-r--r--helix-term/src/main.rs2
-rw-r--r--helix-term/src/ui/completion.rs55
-rw-r--r--helix-term/src/ui/editor.rs292
-rw-r--r--helix-term/src/ui/markdown.rs310
-rw-r--r--helix-term/src/ui/menu.rs41
-rw-r--r--helix-term/src/ui/mod.rs34
-rw-r--r--helix-term/src/ui/picker.rs41
-rw-r--r--helix-term/src/ui/popup.rs49
-rw-r--r--helix-term/src/ui/prompt.rs10
-rw-r--r--helix-term/src/ui/spinner.rs9
-rw-r--r--helix-tui/Cargo.toml10
-rw-r--r--helix-tui/README.md2
-rw-r--r--helix-tui/src/backend/test.rs3
-rw-r--r--helix-tui/src/buffer.rs107
-rw-r--r--helix-tui/src/text.rs16
-rw-r--r--helix-tui/src/widgets/block.rs26
-rw-r--r--helix-tui/src/widgets/paragraph.rs4
-rw-r--r--helix-tui/src/widgets/reflow.rs4
-rw-r--r--helix-tui/src/widgets/table.rs11
-rw-r--r--helix-view/Cargo.toml15
-rw-r--r--helix-view/src/document.rs246
-rw-r--r--helix-view/src/editor.rs165
-rw-r--r--helix-view/src/graphics.rs31
-rw-r--r--helix-view/src/gutter.rs27
-rw-r--r--helix-view/src/info.rs65
-rw-r--r--helix-view/src/input.rs197
-rw-r--r--helix-view/src/keyboard.rs5
-rw-r--r--helix-view/src/theme.rs136
-rw-r--r--helix-view/src/view.rs13
-rw-r--r--languages.toml230
-rw-r--r--runtime/queries/bash/injections.scm2
-rw-r--r--runtime/queries/c-sharp/injections.scm2
-rw-r--r--runtime/queries/c/indents.toml16
-rw-r--r--runtime/queries/c/injections.scm2
-rw-r--r--runtime/queries/c/textobjects.scm13
-rw-r--r--runtime/queries/cmake/indents.toml12
-rw-r--r--runtime/queries/cmake/injections.scm4
-rw-r--r--runtime/queries/cmake/textobjects.scm3
-rw-r--r--runtime/queries/comment/highlights.scm30
-rw-r--r--runtime/queries/cpp/indents.toml17
-rw-r--r--runtime/queries/cpp/injections.scm1
-rw-r--r--runtime/queries/cpp/textobjects.scm7
-rw-r--r--runtime/queries/css/injections.scm2
-rw-r--r--runtime/queries/dart/highlights.scm237
-rw-r--r--runtime/queries/dart/indents.toml20
-rw-r--r--runtime/queries/dart/injections.scm2
-rw-r--r--runtime/queries/dart/locals.scm20
-rw-r--r--runtime/queries/dockerfile/highlights.scm51
-rw-r--r--runtime/queries/dockerfile/injections.scm6
-rw-r--r--runtime/queries/elixir/injections.scm9
-rw-r--r--runtime/queries/elm/highlights.scm83
-rw-r--r--runtime/queries/elm/injections.scm4
-rw-r--r--runtime/queries/elm/locals.scm14
-rw-r--r--runtime/queries/elm/tags.scm19
-rw-r--r--runtime/queries/fish/highlights.scm156
-rw-r--r--runtime/queries/fish/indents.toml12
-rw-r--r--runtime/queries/fish/injections.scm2
-rw-r--r--runtime/queries/fish/textobjects.scm1
-rw-r--r--runtime/queries/git-commit/highlights.scm14
-rw-r--r--runtime/queries/git-commit/injections.scm8
-rw-r--r--runtime/queries/git-config/highlights.scm27
-rw-r--r--runtime/queries/git-diff/highlights.scm6
-rw-r--r--runtime/queries/git-rebase/highlights.scm11
-rw-r--r--runtime/queries/git-rebase/injections.scm4
-rw-r--r--runtime/queries/glsl/injections.scm5
-rw-r--r--runtime/queries/go/highlights.scm6
-rw-r--r--runtime/queries/go/injections.scm2
-rw-r--r--runtime/queries/graphql/highlights.scm163
-rw-r--r--runtime/queries/haskell/highlights.scm156
-rw-r--r--runtime/queries/haskell/injections.scm2
-rw-r--r--runtime/queries/html/injections.scm3
-rw-r--r--runtime/queries/iex/highlights.scm1
-rw-r--r--runtime/queries/iex/injections.scm6
-rw-r--r--runtime/queries/java/injections.scm2
-rw-r--r--runtime/queries/javascript/injections.scm8
-rw-r--r--runtime/queries/julia/injections.scm8
-rw-r--r--runtime/queries/julia/locals.scm22
-rw-r--r--runtime/queries/latex/highlights.scm28
-rw-r--r--runtime/queries/latex/injections.scm2
-rw-r--r--runtime/queries/lean/folds.scm15
-rw-r--r--runtime/queries/lean/highlights.scm217
-rw-r--r--runtime/queries/lean/injections.scm2
-rw-r--r--runtime/queries/lean/locals.scm5
-rw-r--r--runtime/queries/ledger/highlights.scm2
-rw-r--r--runtime/queries/ledger/injections.scm4
-rw-r--r--runtime/queries/llvm-mir-yaml/highlights.scm1
-rw-r--r--runtime/queries/llvm-mir-yaml/indents.toml3
-rw-r--r--runtime/queries/llvm-mir-yaml/injections.scm9
-rw-r--r--runtime/queries/llvm-mir/highlights.scm136
-rw-r--r--runtime/queries/llvm-mir/indents.toml7
-rw-r--r--runtime/queries/llvm-mir/injections.scm2
-rw-r--r--runtime/queries/llvm-mir/textobjects.scm3
-rw-r--r--runtime/queries/llvm/highlights.scm162
-rw-r--r--runtime/queries/llvm/indents.toml8
-rw-r--r--runtime/queries/llvm/injections.scm2
-rw-r--r--runtime/queries/llvm/locals.scm14
-rw-r--r--runtime/queries/llvm/textobjects.scm16
-rw-r--r--runtime/queries/lua/injections.scm2
-rw-r--r--runtime/queries/make/highlights.scm170
-rw-r--r--runtime/queries/make/injections.scm2
-rw-r--r--runtime/queries/markdown/highlights.scm41
-rw-r--r--runtime/queries/markdown/injections.scm9
-rw-r--r--runtime/queries/nix/highlights.scm7
-rw-r--r--runtime/queries/ocaml-interface/injections.scm2
-rw-r--r--runtime/queries/ocaml/highlights.scm2
-rw-r--r--runtime/queries/ocaml/indents.toml2
-rw-r--r--runtime/queries/ocaml/injections.scm2
-rw-r--r--runtime/queries/perl/indents.toml17
-rw-r--r--runtime/queries/perl/injections.scm2
-rw-r--r--runtime/queries/php/highlights.scm6
-rw-r--r--runtime/queries/php/injections.scm3
-rw-r--r--runtime/queries/php/textobjects.scm30
-rw-r--r--runtime/queries/protobuf/injections.scm2
-rw-r--r--runtime/queries/python/injections.scm2
-rw-r--r--runtime/queries/regex/highlights.scm53
-rw-r--r--runtime/queries/rescript/highlights.scm179
-rw-r--r--runtime/queries/rescript/injections.scm8
-rw-r--r--runtime/queries/rescript/textobjects.scm9
-rw-r--r--runtime/queries/ruby/indents.toml25
-rw-r--r--runtime/queries/ruby/injections.scm2
-rw-r--r--runtime/queries/rust/highlights.scm14
-rw-r--r--runtime/queries/rust/indents.toml1
-rw-r--r--runtime/queries/rust/injections.scm17
-rw-r--r--runtime/queries/scala/highlights.scm203
-rw-r--r--runtime/queries/scala/indents.toml23
-rw-r--r--runtime/queries/scala/injections.scm2
-rw-r--r--runtime/queries/svelte/highlights.scm4
-rw-r--r--runtime/queries/svelte/injections.scm4
-rw-r--r--runtime/queries/tablegen/highlights.scm90
-rw-r--r--runtime/queries/tablegen/indents.toml7
-rw-r--r--runtime/queries/tablegen/injections.scm2
-rw-r--r--runtime/queries/tablegen/textobjects.scm7
-rw-r--r--runtime/queries/toml/injections.scm2
-rw-r--r--runtime/queries/tsq/injections.scm2
-rw-r--r--runtime/queries/tsx/injections.scm1
-rw-r--r--runtime/queries/twig/highlights.scm16
-rw-r--r--runtime/queries/twig/injections.scm3
-rw-r--r--runtime/queries/typescript/injections.scm1
-rw-r--r--runtime/queries/vue/injections.scm3
-rw-r--r--runtime/queries/wgsl/injections.scm2
-rw-r--r--runtime/queries/yaml/highlights.scm16
-rw-r--r--runtime/queries/yaml/injections.scm2
-rw-r--r--runtime/queries/zig/highlights.scm5
-rw-r--r--runtime/queries/zig/indents.toml3
-rw-r--r--runtime/queries/zig/injections.scm2
-rw-r--r--runtime/themes/base16_default_dark.toml50
-rw-r--r--runtime/themes/base16_default_light.toml73
-rw-r--r--runtime/themes/base16_terminal.toml52
-rw-r--r--runtime/themes/bogster.toml17
-rw-r--r--runtime/themes/dark_plus.toml14
-rw-r--r--runtime/themes/dracula.toml63
-rw-r--r--runtime/themes/everforest_dark.toml16
-rw-r--r--runtime/themes/everforest_light.toml100
-rw-r--r--runtime/themes/gruvbox.toml12
-rw-r--r--runtime/themes/gruvbox_light.toml96
-rw-r--r--runtime/themes/ingrid.toml14
-rw-r--r--runtime/themes/monokai.toml16
-rw-r--r--runtime/themes/monokai_pro.toml115
-rw-r--r--runtime/themes/monokai_pro_machine.toml115
-rw-r--r--runtime/themes/monokai_pro_octagon.toml115
-rw-r--r--runtime/themes/monokai_pro_ristretto.toml115
-rw-r--r--runtime/themes/monokai_pro_spectrum.toml115
-rw-r--r--runtime/themes/nord.toml15
-rw-r--r--runtime/themes/onedark.toml23
-rw-r--r--runtime/themes/rose_pine.toml20
-rw-r--r--runtime/themes/rose_pine_dawn.toml73
-rw-r--r--runtime/themes/serika-dark.toml99
-rw-r--r--runtime/themes/serika-light.toml100
-rw-r--r--runtime/themes/solarized_dark.toml44
-rw-r--r--runtime/themes/solarized_light.toml60
-rw-r--r--runtime/themes/spacebones_light.toml14
-rw-r--r--theme.toml11
-rw-r--r--xtask/Cargo.toml11
-rw-r--r--xtask/src/main.rs277
268 files changed, 10621 insertions, 2643 deletions
diff --git a/.cargo/config b/.cargo/config
new file mode 100644
index 00000000..35049cbc
--- /dev/null
+++ b/.cargo/config
@@ -0,0 +1,2 @@
+[alias]
+xtask = "run --package xtask --"
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 958407bb..41b00230 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -17,6 +17,7 @@ Please search on the issue tracker before creating one. -->
### Environment
- Platform: <!-- macOS / Windows / Linux -->
+- Terminal emulator:
- Helix version: <!-- 'hx -V' if using a release, 'git describe' if building from master -->
<details><summary>~/.cache/helix/helix.log</summary>
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 21629180..65c2f949 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -136,4 +136,52 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: clippy
- args: -- -D warnings
+ args: --all-targets -- -D warnings
+
+ docs:
+ name: Docs
+ runs-on: ubuntu-latest
+ steps:
+ - name: Checkout sources
+ uses: actions/checkout@v2
+ with:
+ submodules: true
+
+ - name: Install stable toolchain
+ uses: actions-rs/toolchain@v1
+ with:
+ profile: minimal
+ toolchain: stable
+ override: true
+
+ - name: Cache cargo registry
+ uses: actions/cache@v2.1.6
+ with:
+ path: ~/.cargo/registry
+ key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Cache cargo index
+ uses: actions/cache@v2.1.6
+ with:
+ path: ~/.cargo/git
+ key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Cache cargo target dir
+ uses: actions/cache@v2.1.6
+ with:
+ path: target
+ key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
+
+ - name: Generate docs
+ uses: actions-rs/cargo@v1
+ with:
+ command: xtask
+ args: docgen
+
+ - name: Check uncommitted documentation changes
+ run: |
+ git diff
+ git diff-files --quiet \
+ || (echo "Run 'cargo xtask docgen', commit the changes and push again" \
+ && exit 1)
+
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index b16fa428..7b0b7ee2 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -102,7 +102,7 @@ jobs:
fi
cp -r runtime dist
- - uses: actions/upload-artifact@v2.2.4
+ - uses: actions/upload-artifact@v2.3.1
with:
name: bins-${{ matrix.build }}
path: dist
diff --git a/.gitmodules b/.gitmodules
index 6295b9e9..55ed97b3 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -142,11 +142,87 @@
path = helix-syntax/languages/tree-sitter-perl
url = https://github.com/ganezdragon/tree-sitter-perl
shallow = true
+[submodule "helix-syntax/languages/tree-sitter-comment"]
+ path = helix-syntax/languages/tree-sitter-comment
+ url = https://github.com/stsewd/tree-sitter-comment
+ shallow = true
[submodule "helix-syntax/languages/tree-sitter-wgsl"]
path = helix-syntax/languages/tree-sitter-wgsl
url = https://github.com/szebniok/tree-sitter-wgsl
shallow = true
-[submodule "helix-syntax/tree-sitter-llvm"]
+[submodule "helix-syntax/languages/tree-sitter-llvm"]
path = helix-syntax/languages/tree-sitter-llvm
url = https://github.com/benwilliamgraham/tree-sitter-llvm
shallow = true
+[submodule "helix-syntax/languages/tree-sitter-markdown"]
+ path = helix-syntax/languages/tree-sitter-markdown
+ url = https://github.com/MDeiml/tree-sitter-markdown
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-dart"]
+ path = helix-syntax/languages/tree-sitter-dart
+ url = https://github.com/UserNobody14/tree-sitter-dart.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-dockerfile"]
+ path = helix-syntax/languages/tree-sitter-dockerfile
+ url = https://github.com/camdencheek/tree-sitter-dockerfile.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-fish"]
+ path = helix-syntax/languages/tree-sitter-fish
+ url = https://github.com/ram02z/tree-sitter-fish
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-git-commit"]
+ path = helix-syntax/languages/tree-sitter-git-commit
+ url = https://github.com/the-mikedavis/tree-sitter-git-commit.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-llvm-mir"]
+ path = helix-syntax/languages/tree-sitter-llvm-mir
+ url = https://github.com/Flakebi/tree-sitter-llvm-mir.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-git-diff"]
+ path = helix-syntax/languages/tree-sitter-git-diff
+ url = https://github.com/the-mikedavis/tree-sitter-git-diff.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-tablegen"]
+ path = helix-syntax/languages/tree-sitter-tablegen
+ url = https://github.com/Flakebi/tree-sitter-tablegen
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-git-rebase"]
+ path = helix-syntax/languages/tree-sitter-git-rebase
+ url = https://github.com/the-mikedavis/tree-sitter-git-rebase.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-lean"]
+ path = helix-syntax/languages/tree-sitter-lean
+ url = https://github.com/Julian/tree-sitter-lean
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-regex"]
+ path = helix-syntax/languages/tree-sitter-regex
+ url = https://github.com/tree-sitter/tree-sitter-regex.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-make"]
+ path = helix-syntax/languages/tree-sitter-make
+ url = https://github.com/alemuller/tree-sitter-make
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-git-config"]
+ path = helix-syntax/languages/tree-sitter-git-config
+ url = https://github.com/the-mikedavis/tree-sitter-git-config.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-graphql"]
+ path = helix-syntax/languages/tree-sitter-graphql
+ url = https://github.com/bkegley/tree-sitter-graphql
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-elm"]
+ path = helix-syntax/languages/tree-sitter-elm
+ url = https://github.com/elm-tooling/tree-sitter-elm
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-iex"]
+ path = helix-syntax/languages/tree-sitter-iex
+ url = https://github.com/elixir-lang/tree-sitter-iex
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-twig"]
+ path = helix-syntax/languages/tree-sitter-twig
+ url = https://github.com/eirabben/tree-sitter-twig.git
+ shallow = true
+[submodule "helix-syntax/languages/tree-sitter-rescript"]
+ path = helix-syntax/languages/tree-sitter-rescript
+ url = https://github.com/jaredramirez/tree-sitter-rescript
+ shallow = true
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 52ca2d60..38927991 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,123 @@
+# 0.6.0 (2022-01-04)
+
+Happy new year and a big shout out to all the contributors! We had 55 contributors in this release.
+
+Helix has popped up in DPorts and Fedora Linux via COPR ([#1270](https://github.com/helix-editor/helix/pull/1270))
+
+As usual the following is a brief summary, refer to the git history for a full log:
+
+Breaking changes:
+
+- fix: Normalize backtab into shift-tab
+
+Features:
+
+- Macros ([#1234](https://github.com/helix-editor/helix/pull/1234))
+- Add reverse search functionality ([#958](https://github.com/helix-editor/helix/pull/958))
+- Allow keys to be mapped to sequences of commands ([#589](https://github.com/helix-editor/helix/pull/589))
+- Make it possible to keybind TypableCommands ([#1169](https://github.com/helix-editor/helix/pull/1169))
+- Detect workspace root using language markers ([#1370](https://github.com/helix-editor/helix/pull/1370))
+- Add WORD textobject ([#991](https://github.com/helix-editor/helix/pull/991))
+- Add LSP rename_symbol (space-r) ([#1011](https://github.com/helix-editor/helix/pull/1011))
+- Added workspace_symbol_picker ([#1041](https://github.com/helix-editor/helix/pull/1041))
+- Detect filetype from shebang line ([#1001](https://github.com/helix-editor/helix/pull/1001))
+- Allow piping from stdin into a buffer on startup ([#996](https://github.com/helix-editor/helix/pull/996))
+- Add auto pairs for same-char pairs ([#1219](https://github.com/helix-editor/helix/pull/1219))
+- Update settings at runtime ([#798](https://github.com/helix-editor/helix/pull/798))
+- Enable thin LTO (cccc194)
+
+Commands:
+- :wonly -- window only ([#1057](https://github.com/helix-editor/helix/pull/1057))
+- buffer-close (:bc, :bclose) ([#1035](https://github.com/helix-editor/helix/pull/1035))
+- Add :<line> and :goto <line> commands ([#1128](https://github.com/helix-editor/helix/pull/1128))
+- :sort command ([#1288](https://github.com/helix-editor/helix/pull/1288))
+- Add m textobject for pair under cursor ([#961](https://github.com/helix-editor/helix/pull/961))
+- Implement "Goto next buffer / Goto previous buffer" commands ([#950](https://github.com/helix-editor/helix/pull/950))
+- Implement "Goto last modification" command ([#1067](https://github.com/helix-editor/helix/pull/1067))
+- Add trim_selections command ([#1092](https://github.com/helix-editor/helix/pull/1092))
+- Add movement shortcut for history ([#1088](https://github.com/helix-editor/helix/pull/1088))
+- Add command to inc/dec number under cursor ([#1027](https://github.com/helix-editor/helix/pull/1027))
+ - Add support for dates for increment/decrement
+- Align selections (&) ([#1101](https://github.com/helix-editor/helix/pull/1101))
+- Implement no-yank delete/change ([#1099](https://github.com/helix-editor/helix/pull/1099))
+- Implement black hole register ([#1165](https://github.com/helix-editor/helix/pull/1165))
+- gf as goto_file (gf) ([#1102](https://github.com/helix-editor/helix/pull/1102))
+- Add last modified file (gm) ([#1093](https://github.com/helix-editor/helix/pull/1093))
+- ensure_selections_forward ([#1393](https://github.com/helix-editor/helix/pull/1393))
+- Readline style insert mode ([#1039](https://github.com/helix-editor/helix/pull/1039))
+
+Usability improvements and fixes:
+
+- Detect filetype on :write ([#1141](https://github.com/helix-editor/helix/pull/1141))
+- Add single and double quotes to matching pairs ([#995](https://github.com/helix-editor/helix/pull/995))
+- Launch with defaults upon invalid config/theme (rather than panicking) ([#982](https://github.com/helix-editor/helix/pull/982))
+- If switching away from an empty scratch buffer, remove it ([#935](https://github.com/helix-editor/helix/pull/935))
+- Truncate the starts of file paths instead of the ends in picker ([#951](https://github.com/helix-editor/helix/pull/951))
+- Truncate the start of file paths in the StatusLine ([#1351](https://github.com/helix-editor/helix/pull/1351))
+- Prevent picker from previewing binaries or large file ([#939](https://github.com/helix-editor/helix/pull/939))
+- Inform when reaching undo/redo bounds ([#981](https://github.com/helix-editor/helix/pull/981))
+- search_impl will only align cursor center when it isn't in view ([#959](https://github.com/helix-editor/helix/pull/959))
+- Add <C-h>, <C-u>, <C-d>, Delete in prompt mode ([#1034](https://github.com/helix-editor/helix/pull/1034))
+- Restore screen position when aborting search ([#1047](https://github.com/helix-editor/helix/pull/1047))
+- Buffer picker: show is_modifier flag ([#1020](https://github.com/helix-editor/helix/pull/1020))
+- Add commit hash to version info, if present ([#957](https://github.com/helix-editor/helix/pull/957))
+- Implement indent-aware delete ([#1120](https://github.com/helix-editor/helix/pull/1120))
+- Jump to end char of surrounding pair from any cursor pos ([#1121](https://github.com/helix-editor/helix/pull/1121))
+- File picker configuration ([#988](https://github.com/helix-editor/helix/pull/988))
+- Fix surround cursor position calculation ([#1183](https://github.com/helix-editor/helix/pull/1183))
+- Accept count for goto_window ([#1033](https://github.com/helix-editor/helix/pull/1033))
+- Make kill_to_line_end behave like emacs ([#1235](https://github.com/helix-editor/helix/pull/1235))
+- Only use a single documentation popup ([#1241](https://github.com/helix-editor/helix/pull/1241))
+- ui: popup: Don't allow scrolling past the end of content (3307f44c)
+- Open files with spaces in filename, allow opening multiple files ([#1231](https://github.com/helix-editor/helix/pull/1231))
+- Allow paste commands to take a count ([#1261](https://github.com/helix-editor/helix/pull/1261))
+- Auto pairs selection ([#1254](https://github.com/helix-editor/helix/pull/1254))
+- Use a fuzzy matcher for commands ([#1386](https://github.com/helix-editor/helix/pull/1386))
+- Add c-s to pick word under doc cursor to prompt line & search completion ([#831](https://github.com/helix-editor/helix/pull/831))
+- Fix :earlier/:later missing changeset update ([#1069](https://github.com/helix-editor/helix/pull/1069))
+- Support extend for multiple goto ([#909](https://github.com/helix-editor/helix/pull/909))
+- Add arrow-key bindings for window switching ([#933](https://github.com/helix-editor/helix/pull/933))
+- Implement key ordering for info box ([#952](https://github.com/helix-editor/helix/pull/952))
+
+LSP:
+- Implement MarkedString rendering (e128a8702)
+- Don't panic if init fails (d31bef7)
+- Configurable diagnostic severity ([#1325](https://github.com/helix-editor/helix/pull/1325))
+- Resolve completion item ([#1315](https://github.com/helix-editor/helix/pull/1315))
+- Code action command support ([#1304](https://github.com/helix-editor/helix/pull/1304))
+
+Grammars:
+
+- Adds mint language server ([#974](https://github.com/helix-editor/helix/pull/974))
+- Perl ([#978](https://github.com/helix-editor/helix/pull/978)) ([#1280](https://github.com/helix-editor/helix/pull/1280))
+- GLSL ([#993](https://github.com/helix-editor/helix/pull/993))
+- Racket ([#1143](https://github.com/helix-editor/helix/pull/1143))
+- WGSL ([#1166](https://github.com/helix-editor/helix/pull/1166))
+- LLVM ([#1167](https://github.com/helix-editor/helix/pull/1167)) ([#1388](https://github.com/helix-editor/helix/pull/1388)) ([#1409](https://github.com/helix-editor/helix/pull/1409)) ([#1398](https://github.com/helix-editor/helix/pull/1398))
+- Markdown (49e06787)
+- Scala ([#1278](https://github.com/helix-editor/helix/pull/1278))
+- Dart ([#1250](https://github.com/helix-editor/helix/pull/1250))
+- Fish ([#1308](https://github.com/helix-editor/helix/pull/1308))
+- Dockerfile ([#1303](https://github.com/helix-editor/helix/pull/1303))
+- Git (commit, rebase, diff) ([#1338](https://github.com/helix-editor/helix/pull/1338)) ([#1402](https://github.com/helix-editor/helix/pull/1402)) ([#1373](https://github.com/helix-editor/helix/pull/1373))
+- tree-sitter-comment ([#1300](https://github.com/helix-editor/helix/pull/1300))
+- Highlight comments in c, cpp, cmake and llvm ([#1309](https://github.com/helix-editor/helix/pull/1309))
+- Improve yaml syntax highlighting highlighting ([#1294](https://github.com/helix-editor/helix/pull/1294))
+- Improve rust syntax highlighting ([#1295](https://github.com/helix-editor/helix/pull/1295))
+- Add textobjects and indents to cmake ([#1307](https://github.com/helix-editor/helix/pull/1307))
+- Add textobjects and indents to c and cpp ([#1293](https://github.com/helix-editor/helix/pull/1293))
+
+New themes:
+
+- Solarized dark ([#999](https://github.com/helix-editor/helix/pull/999))
+- Solarized light ([#1010](https://github.com/helix-editor/helix/pull/1010))
+- Spacebones light ([#1131](https://github.com/helix-editor/helix/pull/1131))
+- Monokai Pro ([#1206](https://github.com/helix-editor/helix/pull/1206))
+- Base16 Light and Terminal ([#1078](https://github.com/helix-editor/helix/pull/1078))
+ - and a default 16 color theme, truecolor detection
+- Dracula ([#1258](https://github.com/helix-editor/helix/pull/1258))
+
# 0.5.0 (2021-10-28)
A big shout out to all the contributors! We had 46 contributors in this release.
diff --git a/Cargo.lock b/Cargo.lock
index 89c6388e..4234c3b5 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.51"
+version = "1.0.53"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
+checksum = "94a45b455c14666b85fc40a019e8ab9eb75e3a124e05494f5397122bc9eb06e0"
[[package]]
name = "arc-swap"
@@ -78,9 +78,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chardetng"
-version = "0.1.15"
+version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e"
+checksum = "14b8f0b65b7b08ae3c8187e8d77174de20cb6777864c6b832d8ad365999cf1ea"
dependencies = [
"cfg-if",
"encoding_rs",
@@ -101,9 +101,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
-version = "4.2.2"
+version = "4.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed"
+checksum = "2f3e1238132dc01f081e1cbb9dace14e5ef4c3a51ee244bd982275fb514605db"
dependencies = [
"error-code",
"str-buf",
@@ -121,9 +121,9 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
-version = "0.8.5"
+version = "0.8.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
+checksum = "b5e5bed1f1c269533fa816a0a5492b3545209a205ca1a54842be180eb63a16a6"
dependencies = [
"cfg-if",
"lazy_static",
@@ -131,16 +131,16 @@ dependencies = [
[[package]]
name = "crossterm"
-version = "0.22.1"
+version = "0.23.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
+checksum = "77b75a27dc8d220f1f8521ea69cd55a34d720a200ebb3a624d9aa19193d3b432"
dependencies = [
"bitflags",
"crossterm_winapi",
"futures-core",
"libc",
"mio",
- "parking_lot",
+ "parking_lot 0.12.0",
"signal-hook",
"signal-hook-mio",
"winapi",
@@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encoding_rs"
-version = "0.8.29"
+version = "0.8.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
+checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df"
dependencies = [
"cfg-if",
]
@@ -202,9 +202,9 @@ dependencies = [
[[package]]
name = "error-code"
-version = "2.3.0"
+version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff"
+checksum = "64f18991e7bf11e7ffee451b5318b5c1a73c52d0d0ada6e5a3017c8c1ced6a21"
dependencies = [
"libc",
"str-buf",
@@ -247,26 +247,16 @@ dependencies = [
]
[[package]]
-name = "futf"
-version = "0.1.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7c9c1ce3fa9336301af935ab852c437817d14cd33690446569392e65170aac3b"
-dependencies = [
- "mac",
- "new_debug_unreachable",
-]
-
-[[package]]
name = "futures-core"
-version = "0.3.18"
+version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
+checksum = "0c09fd04b7e4073ac7156a9539b57a484a8ea920f79c7c675d05d289ab6110d3"
[[package]]
name = "futures-executor"
-version = "0.3.18"
+version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
+checksum = "9420b90cfa29e327d0429f19be13e7ddb68fa1cccb09d65e5706b8c7a749b8a6"
dependencies = [
"futures-core",
"futures-task",
@@ -275,15 +265,15 @@ dependencies = [
[[package]]
name = "futures-task"
-version = "0.3.18"
+version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
+checksum = "57c66a976bf5909d801bbef33416c41372779507e7a6b3a5e25e4749c58f776a"
[[package]]
name = "futures-util"
-version = "0.3.18"
+version = "0.3.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
+checksum = "d8b7abd5d659d9b90c8cba917f6ec750a74e2dc23902ef9cd4cc8c8b22e6036a"
dependencies = [
"futures-core",
"futures-task",
@@ -303,9 +293,9 @@ dependencies = [
[[package]]
name = "getrandom"
-version = "0.2.3"
+version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
+checksum = "418d37c8b1d42553c93648be529cb70f920d3baf8ef469b74b9638df426e0b4c"
dependencies = [
"cfg-if",
"libc",
@@ -366,9 +356,11 @@ dependencies = [
[[package]]
name = "helix-core"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"arc-swap",
+ "chrono",
+ "encoding_rs",
"etcetera",
"helix-syntax",
"log",
@@ -379,8 +371,9 @@ dependencies = [
"serde",
"serde_json",
"similar",
+ "slotmap",
"smallvec",
- "tendril",
+ "smartstring",
"toml",
"tree-sitter",
"unicode-general-category",
@@ -390,7 +383,7 @@ dependencies = [
[[package]]
name = "helix-dap"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"anyhow",
"fern",
@@ -404,7 +397,7 @@ dependencies = [
[[package]]
name = "helix-lsp"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"anyhow",
"futures-executor",
@@ -422,7 +415,7 @@ dependencies = [
[[package]]
name = "helix-syntax"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"anyhow",
"cc",
@@ -433,7 +426,7 @@ dependencies = [
[[package]]
name = "helix-term"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"anyhow",
"chrono",
@@ -465,7 +458,7 @@ dependencies = [
[[package]]
name = "helix-tui"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"bitflags",
"cassowary",
@@ -478,14 +471,13 @@ dependencies = [
[[package]]
name = "helix-view"
-version = "0.5.0"
+version = "0.6.0"
dependencies = [
"anyhow",
"bitflags",
"chardetng",
"clipboard-win",
"crossterm",
- "encoding_rs",
"futures-util",
"helix-core",
"helix-dap",
@@ -551,9 +543,9 @@ dependencies = [
[[package]]
name = "itoa"
-version = "0.4.8"
+version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
+checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35"
[[package]]
name = "jsonrpc-core"
@@ -576,15 +568,15 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
-version = "0.2.104"
+version = "0.2.117"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
+checksum = "e74d72e0f9b65b5b4ca49a346af3976df0f9c61d550727f349ecd559f251a26c"
[[package]]
name = "libloading"
-version = "0.7.2"
+version = "0.7.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52"
+checksum = "efbc0f03f9a775e9f6aed295c6a1ba2253c5757a9e03d55c6caa46a681abcddd"
dependencies = [
"cfg-if",
"winapi",
@@ -592,9 +584,9 @@ dependencies = [
[[package]]
name = "lock_api"
-version = "0.4.5"
+version = "0.4.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
+checksum = "88943dd7ef4a2e5a4bfa2753aaab3013e34ce2533d1996fb18ef591e315e2b3b"
dependencies = [
"scopeguard",
]
@@ -610,9 +602,9 @@ dependencies = [
[[package]]
name = "lsp-types"
-version = "0.91.1"
+version = "0.92.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
+checksum = "e8a69d4142d51b208c9fc3cea68b1a7fcef30354e7aa6ccad07250fd8430fc76"
dependencies = [
"bitflags",
"serde",
@@ -622,12 +614,6 @@ dependencies = [
]
[[package]]
-name = "mac"
-version = "0.1.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
-
-[[package]]
name = "matches"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -671,12 +657,6 @@ dependencies = [
]
[[package]]
-name = "new_debug_unreachable"
-version = "1.0.4"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e4a24736216ec316047a1fc4252e27dabb04218aa4a3f37c6e7ddbf1f9782b54"
-
-[[package]]
name = "ntapi"
version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -706,9 +686,9 @@ dependencies = [
[[package]]
name = "num_cpus"
-version = "1.13.0"
+version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3"
+checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1"
dependencies = [
"hermit-abi",
"libc",
@@ -716,9 +696,9 @@ dependencies = [
[[package]]
name = "once_cell"
-version = "1.8.0"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
+checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5"
[[package]]
name = "parking_lot"
@@ -728,7 +708,17 @@ checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [
"instant",
"lock_api",
- "parking_lot_core",
+ "parking_lot_core 0.8.5",
+]
+
+[[package]]
+name = "parking_lot"
+version = "0.12.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87f5ec2493a61ac0506c0f4199f99070cbe83857b0337006a30f3e6719b8ef58"
+dependencies = [
+ "lock_api",
+ "parking_lot_core 0.9.1",
]
[[package]]
@@ -746,6 +736,19 @@ dependencies = [
]
[[package]]
+name = "parking_lot_core"
+version = "0.9.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "28141e0cc4143da2443301914478dc976a61ffdb3f043058310c70df2fed8954"
+dependencies = [
+ "cfg-if",
+ "libc",
+ "redox_syscall",
+ "smallvec",
+ "windows-sys",
+]
+
+[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -753,9 +756,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pin-project-lite"
-version = "0.2.7"
+version = "0.2.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
+checksum = "e280fbe77cc62c91527259e9442153f4688736748d24660126286329742b4c6c"
[[package]]
name = "pin-utils"
@@ -765,18 +768,18 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
-version = "1.0.30"
+version = "1.0.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
+checksum = "c7342d5883fbccae1cc37a2353b09c87c9b0f3afd73f5fb9bba687a1f733b029"
dependencies = [
"unicode-xid",
]
[[package]]
name = "pulldown-cmark"
-version = "0.8.0"
+version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ffade02495f22453cd593159ea2f59827aae7f53fa8323f756799b670881dcf8"
+checksum = "34f197a544b0c9ab3ae46c359a7ec9cbbb5c7bf97054266fecb7ead794a181d6"
dependencies = [
"bitflags",
"memchr",
@@ -794,9 +797,9 @@ dependencies = [
[[package]]
name = "quote"
-version = "1.0.10"
+version = "1.0.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
+checksum = "864d3e96a899863136fc6e99f3d7cae289dafe43bf2c5ac19b70df7210c0a145"
dependencies = [
"proc-macro2",
]
@@ -863,18 +866,18 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "ropey"
-version = "1.3.1"
+version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9150aff6deb25b20ed110889f070a678bcd1033e46e5e9d6fb1abeab17947f28"
+checksum = "e6b9aa65bcd9f308d37c7158b4a1afaaa32b8450213e20c9b98e7d5b3cc2fec3"
dependencies = [
"smallvec",
]
[[package]]
name = "ryu"
-version = "1.0.5"
+version = "1.0.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "71d301d4193d031abdd79ff7e3dd721168a9572ef3fe51a1517aba235bd8f86e"
+checksum = "73b4b750c782965c211b42f022f59af1fbceabdd026623714f104152f1ec149f"
[[package]]
name = "same-file"
@@ -893,18 +896,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
-version = "1.0.130"
+version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
+checksum = "ce31e24b01e1e524df96f1c2fdd054405f8d7376249a5110886fb4b658484789"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
-version = "1.0.130"
+version = "1.0.136"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
+checksum = "08597e7152fcd306f41838ed3e37be9eaeed2b61c42e2117266a554fab4662f9"
dependencies = [
"proc-macro2",
"quote",
@@ -913,9 +916,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.72"
+version = "1.0.78"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
+checksum = "d23c1ba4cf0efd44be32017709280b32d1cea5c3f1275c3b6d9e8bc54f758085"
dependencies = [
"itoa",
"ryu",
@@ -935,9 +938,9 @@ dependencies = [
[[package]]
name = "signal-hook"
-version = "0.3.10"
+version = "0.3.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
+checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d"
dependencies = [
"libc",
"signal-hook-registry",
@@ -965,9 +968,9 @@ dependencies = [
[[package]]
name = "signal-hook-tokio"
-version = "0.3.0"
+version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8"
+checksum = "213241f76fb1e37e27de3b6aa1b068a2c333233b59cca6634f634b80a27ecf1e"
dependencies = [
"futures-core",
"libc",
@@ -998,9 +1001,24 @@ dependencies = [
[[package]]
name = "smallvec"
-version = "1.7.0"
+version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
+checksum = "f2dd574626839106c320a323308629dcb1acfc96e32a8cba364ddc61ac23ee83"
+
+[[package]]
+name = "smartstring"
+version = "0.2.9"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "31aa6a31c0c2b21327ce875f7e8952322acfcfd0c27569a6e18a647281352c9b"
+dependencies = [
+ "static_assertions",
+]
+
+[[package]]
+name = "static_assertions"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f"
[[package]]
name = "str-buf"
@@ -1010,9 +1028,9 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
[[package]]
name = "syn"
-version = "1.0.80"
+version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
+checksum = "8a65b3f4ffa0092e9887669db0eae07941f023991ab58ea44da8fe8e2d511c6b"
dependencies = [
"proc-macro2",
"quote",
@@ -1020,17 +1038,6 @@ dependencies = [
]
[[package]]
-name = "tendril"
-version = "0.4.2"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a9ef557cb397a4f0a5a3a628f06515f78563f2209e64d47055d9dc6052bf5e33"
-dependencies = [
- "futf",
- "mac",
- "utf-8",
-]
-
-[[package]]
name = "thiserror"
version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index"
@@ -1052,9 +1059,9 @@ dependencies = [
[[package]]
name = "thread_local"
-version = "1.1.3"
+version = "1.1.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8018d24e04c95ac8790716a5987d0fec4f8b27249ffa0f7d33f1369bdfb88cbd"
+checksum = "5516c27b78311c50bf42c071425c560ac799b11c30b31f87e3081965fe5e0180"
dependencies = [
"once_cell",
]
@@ -1070,9 +1077,9 @@ dependencies = [
[[package]]
name = "tinyvec"
-version = "1.5.0"
+version = "1.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
+checksum = "2c1c1d5a42b6245520c249549ec267180beaffcc0615401ac8e31853d4b6d8d2"
dependencies = [
"tinyvec_macros",
]
@@ -1085,18 +1092,17 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
-version = "1.14.0"
+version = "1.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
+checksum = "0c27a64b625de6d309e8c57716ba93021dccf1b3b5c97edd6d3dd2d2135afc0a"
dependencies = [
- "autocfg",
"bytes",
"libc",
"memchr",
"mio",
"num_cpus",
"once_cell",
- "parking_lot",
+ "parking_lot 0.11.2",
"pin-project-lite",
"signal-hook-registry",
"tokio-macros",
@@ -1105,9 +1111,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "1.6.0"
+version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
+checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7"
dependencies = [
"proc-macro2",
"quote",
@@ -1136,9 +1142,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
-version = "0.20.1"
+version = "0.20.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "9394e9dbfe967b5f3d6ab79e302e78b5fb7b530c368d634ff3b8d67ede138bf1"
+checksum = "4e34327f8eac545e3f037382471b2b19367725a242bba7bc45edb9efb49fe39a"
dependencies = [
"cc",
"regex",
@@ -1161,9 +1167,9 @@ checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]]
name = "unicode-general-category"
-version = "0.4.0"
+version = "0.5.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
+checksum = "1218098468b8085b19a2824104c70d976491d247ce194bbd9dc77181150cdfd6"
[[package]]
name = "unicode-normalization"
@@ -1176,9 +1182,9 @@ dependencies = [
[[package]]
name = "unicode-segmentation"
-version = "1.8.0"
+version = "1.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
+checksum = "7e8820f5d777f6224dc4be3632222971ac30164d4a258d595640799554ebfd99"
[[package]]
name = "unicode-width"
@@ -1206,16 +1212,10 @@ dependencies = [
]
[[package]]
-name = "utf-8"
-version = "0.7.6"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
-
-[[package]]
name = "version_check"
-version = "0.9.3"
+version = "0.9.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "5fecdca9a5291cc2b8dcf7dc02453fee791a280f3743cb0905f8822ae463b3fe"
+checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f"
[[package]]
name = "walkdir"
@@ -1236,9 +1236,9 @@ checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "which"
-version = "4.2.2"
+version = "4.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9"
+checksum = "2a5a7e487e921cf220206864a94a89b6c6905bfc19f1057fa26a4cb360e5c1d2"
dependencies = [
"either",
"lazy_static",
@@ -1275,3 +1275,55 @@ name = "winapi-x86_64-pc-windows-gnu"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f"
+
+[[package]]
+name = "windows-sys"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "3df6e476185f92a12c072be4a189a0210dcdcf512a1891d6dff9edb874deadc6"
+dependencies = [
+ "windows_aarch64_msvc",
+ "windows_i686_gnu",
+ "windows_i686_msvc",
+ "windows_x86_64_gnu",
+ "windows_x86_64_msvc",
+]
+
+[[package]]
+name = "windows_aarch64_msvc"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d8e92753b1c443191654ec532f14c199742964a061be25d77d7a96f09db20bf5"
+
+[[package]]
+name = "windows_i686_gnu"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6a711c68811799e017b6038e0922cb27a5e2f43a2ddb609fe0b6f3eeda9de615"
+
+[[package]]
+name = "windows_i686_msvc"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "146c11bb1a02615db74680b32a68e2d61f553cc24c4eb5b4ca10311740e44172"
+
+[[package]]
+name = "windows_x86_64_gnu"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "c912b12f7454c6620635bbff3450962753834be2a594819bd5e945af18ec64bc"
+
+[[package]]
+name = "windows_x86_64_msvc"
+version = "0.32.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "504a2476202769977a040c6364301a3f65d0cc9e3fb08600b2bda150a0488316"
+
+[[package]]
+name = "xtask"
+version = "0.6.0"
+dependencies = [
+ "helix-core",
+ "helix-term",
+ "toml",
+]
diff --git a/Cargo.toml b/Cargo.toml
index 6c360ffd..36dcb09f 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -7,6 +7,7 @@ members = [
"helix-syntax",
"helix-lsp",
"helix-dap",
+ "xtask",
]
# Build helix-syntax in release mode to make the code path faster in development.
@@ -18,3 +19,4 @@ split-debuginfo = "unpacked"
[profile.release]
lto = "thin"
+# debug = true
diff --git a/README.md b/README.md
index 3f4087b9..71010cc8 100644
--- a/README.md
+++ b/README.md
@@ -44,8 +44,8 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`.
Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
-config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
-via the `HELIX_RUNTIME` environment variable.
+config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows).
+This location can be overriden via the `HELIX_RUNTIME` environment variable.
Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir.
@@ -65,21 +65,7 @@ brew install helix
# Contributing
-Contributors are very welcome! **No contribution is too small and all contributions are valued.**
-
-Some suggestions to get started:
-
-- You can look at the [good first issue](https://github.com/helix-editor/helix/issues?q=is%3Aopen+label%3AE-easy+label%3AE-good-first-issue) label on the issue tracker.
-- Help with packaging on various distributions needed!
-- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
- * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
- * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
-- If your preferred language is missing, integrating a tree-sitter grammar for
- it and defining syntax highlight queries for it is straight forward and
- doesn't require much knowledge of the internals.
-
-We provide an [architecture.md](./docs/architecture.md) that should give you
-a good overview of the internals.
+Contributing guidelines can be found [here](./docs/CONTRIBUTING.md).
# Getting help
diff --git a/TODO.md b/TODO.md
index 80a9be05..ab94cf9a 100644
--- a/TODO.md
+++ b/TODO.md
@@ -1,25 +1,12 @@
-- tree sitter:
- - markdown
- - regex
- - kotlin
- - clojure
- - erlang
-
- [ ] completion isIncomplete support
-
-1
- [ ] respect view fullscreen flag
- [ ] Implement marks (superset of Selection/Range)
- [ ] = for auto indent line/selection
-- [ ] :x for closing buffers
- [ ] lsp: signature help
2
-- [ ] macro recording
-- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
-- [ ] selection align
- [ ] store some state between restarts: file positions, prompt history
- [ ] highlight matched characters in picker
diff --git a/base16_theme.toml b/base16_theme.toml
new file mode 100644
index 00000000..42e02a98
--- /dev/null
+++ b/base16_theme.toml
@@ -0,0 +1,51 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.menu" = "black"
+"ui.menu.selected" = { modifiers = ["reversed"] }
+"ui.linenr" = { fg = "gray", bg = "black" }
+"ui.popup" = { modifiers = ["reversed"] }
+"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
+"ui.selection" = { fg = "black", bg = "blue" }
+"ui.selection.primary" = { fg = "white", bg = "blue" }
+"comment" = { fg = "gray" }
+"ui.statusline" = { fg = "black", bg = "white" }
+"ui.statusline.inactive" = { fg = "gray", bg = "white" }
+"ui.help" = { modifiers = ["reversed"] }
+"ui.cursor" = { fg = "white", modifiers = ["reversed"] }
+"variable" = "red"
+"constant.numeric" = "yellow"
+"constant" = "yellow"
+"attributes" = "yellow"
+"type" = "yellow"
+"ui.cursor.match" = { fg = "yellow", modifiers = ["underlined"] }
+"string" = "green"
+"variable.other.member" = "green"
+"constant.character.escape" = "cyan"
+"function" = "blue"
+"constructor" = "blue"
+"special" = "blue"
+"keyword" = "magenta"
+"label" = "magenta"
+"namespace" = "magenta"
+"ui.help" = { fg = "white", bg = "black" }
+
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "green"
+"diff.delta" = "yellow"
+"diff.minus" = "red"
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "black" }
+"info" = "blue"
+"hint" = "gray"
+"debug" = "gray"
+"warning" = "yellow"
+"error" = "red"
diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md
index 8cadb663..a8f165c0 100644
--- a/book/src/SUMMARY.md
+++ b/book/src/SUMMARY.md
@@ -2,10 +2,12 @@
- [Installation](./install.md)
- [Usage](./usage.md)
+ - [Keymap](./keymap.md)
+ - [Commands](./commands.md)
+ - [Language Support](./lang-support.md)
- [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md)
- [Themes](./themes.md)
- - [Keymap](./keymap.md)
- [Key Remapping](./remapping.md)
- [Hooks](./hooks.md)
- [Languages](./languages.md)
diff --git a/book/src/commands.md b/book/src/commands.md
new file mode 100644
index 00000000..4c4a5c05
--- /dev/null
+++ b/book/src/commands.md
@@ -0,0 +1,5 @@
+# Commands
+
+Command mode can be activated by pressing `:`, similar to vim. Built-in commands:
+
+{{#include ./generated/typable-cmd.md}}
diff --git a/book/src/configuration.md b/book/src/configuration.md
index 2ed48d51..8048f548 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -5,9 +5,27 @@ To override global configuration parameters, create a `config.toml` file located
* Linux and Mac: `~/.config/helix/config.toml`
* Windows: `%AppData%\helix\config.toml`
+Example config:
+
+```toml
+theme = "onedark"
+
+[editor]
+line-number = "relative"
+mouse = false
+
+[editor.cursor-shape]
+insert = "bar"
+normal = "block"
+select = "underline"
+
+[editor.file-picker]
+hidden = false
+```
+
## Editor
-`[editor]` section of the config.
+### `[editor]` Section
| Key | Description | Default |
|--|--|---------|
@@ -16,15 +34,37 @@ To override global configuration parameters, create a `config.toml` file located
| `middle-click-paste` | Middle click paste support. | `true` |
| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
-| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
+| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` |
| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `auto-info` | Whether to display infoboxes | `true` |
+| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` |
+
+### `[editor.cursor-shape]` Section
+
+Defines the shape of cursor in each mode. Note that due to limitations
+of the terminal environment, only the primary cursor can change shape.
+
+| Key | Description | Default |
+| --- | ----------- | ------- |
+| `normal` | Cursor shape in [normal mode][normal mode] | `block` |
+| `insert` | Cursor shape in [insert mode][insert mode] | `block` |
+| `select` | Cursor shape in [select mode][select mode] | `block` |
+
+[normal mode]: ./keymap.md#normal-mode
+[insert mode]: ./keymap.md#insert-mode
+[select mode]: ./keymap.md#select--extend-mode
+
+### `[editor.file-picker]` Section
-`[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default.
+Sets options for file picker and global search. All but the last key listed in
+the default file-picker configuration below are IgnoreOptions: whether hidden
+files and files listed within ignore files are ignored by (not visible in) the
+helix file picker and global search. There is also one other key, `max-depth`
+available, which is not defined by default.
| Key | Description | Default |
|--|--|---------|
diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md
new file mode 100644
index 00000000..64dab6d3
--- /dev/null
+++ b/book/src/generated/lang-support.md
@@ -0,0 +1,63 @@
+| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
+| --- | --- | --- | --- | --- |
+| bash | ✓ | | | `bash-language-server` |
+| c | ✓ | ✓ | ✓ | `clangd` |
+| c-sharp | ✓ | | | |
+| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
+| comment | ✓ | | | |
+| cpp | ✓ | ✓ | ✓ | `clangd` |
+| css | ✓ | | | |
+| dart | ✓ | | ✓ | `dart` |
+| dockerfile | ✓ | | | `docker-langserver` |
+| elixir | ✓ | | | `elixir-ls` |
+| elm | ✓ | | | `elm-language-server` |
+| fish | ✓ | ✓ | ✓ | |
+| git-commit | ✓ | | | |
+| git-config | ✓ | | | |
+| git-diff | ✓ | | | |
+| git-rebase | ✓ | | | |
+| glsl | ✓ | | ✓ | |
+| go | ✓ | ✓ | ✓ | `gopls` |
+| graphql | ✓ | | | |
+| haskell | ✓ | | | `haskell-language-server-wrapper` |
+| html | ✓ | | | |
+| iex | ✓ | | | |
+| java | ✓ | | | |
+| javascript | ✓ | | ✓ | `typescript-language-server` |
+| json | ✓ | | ✓ | |
+| julia | ✓ | | | `julia` |
+| latex | ✓ | | | |
+| lean | ✓ | | | `lean` |
+| ledger | ✓ | | | |
+| llvm | ✓ | ✓ | ✓ | |
+| llvm-mir | ✓ | ✓ | ✓ | |
+| llvm-mir-yaml | ✓ | | ✓ | |
+| lua | ✓ | | ✓ | |
+| make | ✓ | | | |
+| markdown | ✓ | | | |
+| mint | | | | `mint` |
+| nix | ✓ | | ✓ | `rnix-lsp` |
+| ocaml | ✓ | | ✓ | |
+| ocaml-interface | ✓ | | | |
+| perl | ✓ | ✓ | ✓ | |
+| php | ✓ | ✓ | ✓ | |
+| prolog | | | | `swipl` |
+| protobuf | ✓ | | ✓ | |
+| python | ✓ | ✓ | ✓ | `pylsp` |
+| racket | | | | `racket` |
+| regex | ✓ | | | |
+| rescript | ✓ | ✓ | | `rescript-language-server` |
+| ruby | ✓ | | ✓ | `solargraph` |
+| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
+| scala | ✓ | | ✓ | `metals` |
+| svelte | ✓ | | ✓ | `svelteserver` |
+| tablegen | ✓ | ✓ | ✓ | |
+| toml | ✓ | | | |
+| tsq | ✓ | | | |
+| tsx | ✓ | | | `typescript-language-server` |
+| twig | ✓ | | | |
+| typescript | ✓ | | ✓ | `typescript-language-server` |
+| vue | ✓ | | | |
+| wgsl | ✓ | | | |
+| yaml | ✓ | | ✓ | |
+| zig | ✓ | | ✓ | `zls` |
diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md
new file mode 100644
index 00000000..aed75cbd
--- /dev/null
+++ b/book/src/generated/typable-cmd.md
@@ -0,0 +1,48 @@
+| Name | Description |
+| --- | --- |
+| `:quit`, `:q` | Close the current view. |
+| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). |
+| `:open`, `:o` | Open a file from disk into the current view. |
+| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. |
+| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). |
+| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
+| `:new`, `:n` | Create a new scratch buffer. |
+| `:format`, `:fmt` | Format the file using the LSP formatter. |
+| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
+| `:line-ending` | Set the document's default line ending. Options: crlf, lf, cr, ff, nel. |
+| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
+| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
+| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
+| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
+| `:write-all`, `:wa` | Write changes from all views to disk. |
+| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all views to disk and close all views. |
+| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all views to disk and close all views forcefully (ignoring unsaved changes). |
+| `:quit-all`, `:qa` | Close all views. |
+| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). |
+| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
+| `:cquit!`, `:cq!` | Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2). |
+| `:theme` | Change the editor theme. |
+| `:clipboard-yank` | Yank main selection into system clipboard. |
+| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
+| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
+| `:primary-clipboard-yank-join` | Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. |
+| `:clipboard-paste-after` | Paste system clipboard after selections. |
+| `:clipboard-paste-before` | Paste system clipboard before selections. |
+| `:clipboard-paste-replace` | Replace selections with content of system clipboard. |
+| `:primary-clipboard-paste-after` | Paste primary clipboard after selections. |
+| `:primary-clipboard-paste-before` | Paste primary clipboard before selections. |
+| `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. |
+| `:show-clipboard-provider` | Show clipboard provider name in status bar. |
+| `:change-current-directory`, `:cd` | Change the current working directory. |
+| `:show-directory`, `:pwd` | Show the current working directory. |
+| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` |
+| `:reload` | Discard changes and reload from the source file. |
+| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
+| `:vsplit`, `:vs` | Open the file in a vertical split. |
+| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. |
+| `:tutor` | Open the tutorial. |
+| `:goto`, `:g` | Go to line number. |
+| `:set-option`, `:set` | Set a config option at runtime |
+| `:sort` | Sort ranges in selection. |
+| `:rsort` | Sort ranges in selection in reverse order. |
+| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. |
diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md
index 446eb479..5844a48e 100644
--- a/book/src/guides/adding_languages.md
+++ b/book/src/guides/adding_languages.md
@@ -2,7 +2,7 @@
## Submodules
-To add a new langauge, you should first add a tree-sitter submodule. To do this,
+To add a new language, you should first add a tree-sitter submodule. To do this,
you can run the command
```sh
git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
@@ -27,22 +27,32 @@ directory](../configuration.md).
These are the available keys and descriptions for the file.
-| Key | Description |
-| ---- | ----------- |
-| name | The name of the language |
-| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
-| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
-| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
-| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` |
-| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
-| auto-format | Whether to autoformat this language when saving |
-| comment-token | The token to use as a comment-token |
-| indent | The indent to use. Has sub keys `tab-width` and `unit` |
-| config | Language server configuration |
+| Key | Description |
+| ---- | ----------- |
+| name | The name of the language |
+| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
+| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
+| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
+| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` |
+| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
+| auto-format | Whether to autoformat this language when saving |
+| diagnostic-severity | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
+| comment-token | The token to use as a comment-token |
+| indent | The indent to use. Has sub keys `tab-width` and `unit` |
+| config | Language server configuration |
## Queries
-For a language to have syntax-highlighting and indentation among other things, you have to add queries. Add a directory for your language with the path `runtime/queries/<name>/`. The tree-sitter [website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) gives more info on how to write queries.
+For a language to have syntax-highlighting and indentation among
+other things, you have to add queries. Add a directory for your
+language with the path `runtime/queries/<name>/`. The tree-sitter
+[website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
+gives more info on how to write queries.
+
+> NOTE: When evaluating queries, the first matching query takes
+precedence, which is different from other editors like neovim where
+the last matching query supercedes the ones before it. See
+[this issue][neovim-query-precedence] for an example.
## Common Issues
@@ -58,3 +68,4 @@ For a language to have syntax-highlighting and indentation among other things, y
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
+[neovim-query-precedence]: https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090
diff --git a/book/src/install.md b/book/src/install.md
index d831934c..1a5a9daa 100644
--- a/book/src/install.md
+++ b/book/src/install.md
@@ -27,6 +27,15 @@ Releases are available in the `community` repository.
A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
+### Fedora Linux
+
+You can install the COPR package for Helix via
+
+```
+sudo dnf copr enable varlad/helix
+sudo dnf install helix
+```
+
## Build from source
```
diff --git a/book/src/keymap.md b/book/src/keymap.md
index 865a700b..19fd21bb 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -25,7 +25,9 @@
| `f` | Find next char | `find_next_char` |
| `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
+| `G` | Go to line number `<n>` | `goto_line` |
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
+| `Alt-:` | Ensures the selection is in forward direction | `ensure_selections_forward` |
| `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` |
| `PageUp` | Move page up | `page_up` |
@@ -34,6 +36,7 @@
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
+| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A |
@@ -45,37 +48,39 @@
### Changes
-| Key | Description | Command |
-| ----- | ----------- | ------- |
-| `r` | Replace with a character | `replace` |
-| `R` | Replace with yanked text | `replace_with_yanked` |
-| `~` | Switch case of the selected text | `switch_case` |
-| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
-| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
-| `i` | Insert before selection | `insert_mode` |
-| `a` | Insert after selection (append) | `append_mode` |
-| `I` | Insert at the start of the line | `prepend_to_line` |
-| `A` | Insert at the end of the line | `append_to_line` |
-| `o` | Open new line below selection | `open_below` |
-| `O` | Open new line above selection | `open_above` |
-| `.` | Repeat last change | N/A |
-| `u` | Undo change | `undo` |
-| `U` | Redo change | `redo` |
-| `Alt-u` | Move backward in history | `earlier` |
-| `Alt-U` | Move forward in history | `later` |
-| `y` | Yank selection | `yank` |
-| `p` | Paste after selection | `paste_after` |
-| `P` | Paste before selection | `paste_before` |
-| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
-| `>` | Indent selection | `indent` |
-| `<` | Unindent selection | `unindent` |
-| `=` | Format selection (**LSP**) | `format_selections` |
-| `d` | Delete selection | `delete_selection` |
-| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
-| `c` | Change selection (delete and enter insert mode) | `change_selection` |
-| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
-| `Ctrl-a` | Increment object (number) under cursor | `increment` |
-| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
+| Key | Description | Command |
+| ----- | ----------- | ------- |
+| `r` | Replace with a character | `replace` |
+| `R` | Replace with yanked text | `replace_with_yanked` |
+| `~` | Switch case of the selected text | `switch_case` |
+| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
+| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
+| `i` | Insert before selection | `insert_mode` |
+| `a` | Insert after selection (append) | `append_mode` |
+| `I` | Insert at the start of the line | `prepend_to_line` |
+| `A` | Insert at the end of the line | `append_to_line` |
+| `o` | Open new line below selection | `open_below` |
+| `O` | Open new line above selection | `open_above` |
+| `.` | Repeat last change | N/A |
+| `u` | Undo change | `undo` |
+| `U` | Redo change | `redo` |
+| `Alt-u` | Move backward in history | `earlier` |
+| `Alt-U` | Move forward in history | `later` |
+| `y` | Yank selection | `yank` |
+| `p` | Paste after selection | `paste_after` |
+| `P` | Paste before selection | `paste_before` |
+| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
+| `>` | Indent selection | `indent` |
+| `<` | Unindent selection | `unindent` |
+| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` |
+| `d` | Delete selection | `delete_selection` |
+| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
+| `c` | Change selection (delete and enter insert mode) | `change_selection` |
+| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
+| `Ctrl-a` | Increment object (number) under cursor | `increment` |
+| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
+| `Q` | Start/stop macro recording to the selected register (experimental) | `record_macro` |
+| `q` | Play back a recorded macro from the selected register (experimental) | `replay_macro` |
#### Shell
@@ -85,6 +90,7 @@
| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
+| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
### Selection manipulation
@@ -109,12 +115,14 @@
| `%` | Select entire file | `select_all` |
| `x` | Select current line, if already selected, extend to next line | `extend_line` |
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
-| | Expand selection to parent syntax node TODO: pick a key (**TS**) | `expand_selection` |
| `J` | Join lines inside selection | `join_selections` |
| `K` | Keep selections matching the regex | `keep_selections` |
| `Alt-K` | Remove selections matching the regex | `remove_selections` |
-| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
+| `Alt-k` | Expand selection to parent syntax node (**TS**) | `expand_selection` |
+| `Alt-j` | Shrink syntax tree object selection (**TS**) | `shrink_selection` |
+| `Alt-h` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` |
+| `Alt-l` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` |
### Search
@@ -147,10 +155,10 @@ over text and not actively editing it).
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j` , `down` | Scroll the view downwards | `scroll_down` |
| `k` , `up` | Scroll the view upwards | `scroll_up` |
-| `f` | Move page down | `page_down` |
-| `b` | Move page up | `page_up` |
-| `d` | Move half page down | `half_page_down` |
-| `u` | Move half page up | `half_page_up` |
+| `Ctrl-f` | Move page down | `page_down` |
+| `Ctrl-b` | Move page up | `page_up` |
+| `Ctrl-d` | Move half page down | `half_page_down` |
+| `Ctrl-u` | Move half page up | `half_page_up` |
#### Goto mode
@@ -158,20 +166,21 @@ Jumps to various locations.
| Key | Description | Command |
| ----- | ----------- | ------- |
-| `g` | Go to the start of the file | `goto_file_start` |
+| `g` | Go to line number `<n>` else start of file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` |
| `f` | Go to files in the selection | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
| `t` | Go to the top of the screen | `goto_window_top` |
-| `m` | Go to the middle of the screen | `goto_window_middle` |
+| `c` | Go to the middle of the screen | `goto_window_center` |
| `b` | Go to the bottom of the screen | `goto_window_bottom` |
| `d` | Go to definition (**LSP**) | `goto_definition` |
| `y` | Go to type definition (**LSP**) | `goto_type_definition` |
| `r` | Go to references (**LSP**) | `goto_reference` |
| `i` | Go to implementation (**LSP**) | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
+| `m` | Go to the last modified/alternate file | `goto_last_modified_file` |
| `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | Go to last modification in current file | `goto_last_modification` |
diff --git a/book/src/lang-support.md b/book/src/lang-support.md
new file mode 100644
index 00000000..3920f342
--- /dev/null
+++ b/book/src/lang-support.md
@@ -0,0 +1,10 @@
+# Language Support
+
+For more information like arguments passed to default LSP server,
+extensions assosciated with a filetype, custom LSP settings, filetype
+specific indent settings, etc see the default
+[`languages.toml`][languages.toml] file.
+
+{{#include ./generated/lang-support.md}}
+
+[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
diff --git a/book/src/languages.md b/book/src/languages.md
index cef61501..4c4dc326 100644
--- a/book/src/languages.md
+++ b/book/src/languages.md
@@ -11,4 +11,3 @@ Changes made to the `languages.toml` file in a user's [configuration directory](
name = "rust"
auto-format = false
```
-
diff --git a/book/src/remapping.md b/book/src/remapping.md
index fffd189b..1cdf9b1f 100644
--- a/book/src/remapping.md
+++ b/book/src/remapping.md
@@ -11,6 +11,8 @@ this:
```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal]
+C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file)
+C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file
a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
@@ -21,6 +23,7 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
j = { k = "normal_mode" } # Maps `jk` to exit insert mode
```
+> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command.
Control, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows:
@@ -42,10 +45,9 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
| Down | `"down"` |
| Home | `"home"` |
| End | `"end"` |
-| Page | `"pageup"` |
-| Page | `"pagedown"` |
+| Page Up | `"pageup"` |
+| Page Down | `"pagedown"` |
| Tab | `"tab"` |
-| Back | `"backtab"` |
| Delete | `"del"` |
| Insert | `"ins"` |
| Null | `"null"` |
@@ -54,4 +56,4 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
Keys can be disabled by binding them to the `no_op` command.
Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands.
-> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `commands!` macro.
+> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`.
diff --git a/book/src/themes.md b/book/src/themes.md
index fd3f5b1e..9abcfe8c 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -1,14 +1,14 @@
# Themes
-First you'll need to place selected themes in your `themes` directory (i.e `~/.config/helix/themes`), the directory might have to be created beforehand.
+To use a theme add `theme = "<name>"` to your [`config.toml`](./configuration.md) at the very top of the file before the first section or select it during runtime using `:theme <name>`.
-To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`.
+## Creating a theme
-The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
+Create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`). The directory might have to be created beforehand.
-## Creating a theme
+The names "default" and "base16_default" are reserved for the builtin themes and cannot be overridden by user defined themes.
-First create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`).
+The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
Each line in the theme file is specified as below:
@@ -105,6 +105,7 @@ We use a similar set of scopes as
- `type` - Types
- `builtin` - Primitive types provided by the language (`int`, `usize`)
+- `constructor`
- `constant` (TODO: constant.other.placeholder for %v)
- `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
@@ -146,6 +147,7 @@ We use a similar set of scopes as
- `repeat` - `for`, `while`, `loop`
- `import` - `import`, `export`
- `return`
+ - `exception`
- `operator` - `or`, `in`
- `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func`
@@ -162,10 +164,44 @@ We use a similar set of scopes as
- `namespace`
+- `markup`
+ - `heading`
+ - `list`
+ - `unnumbered`
+ - `numbered`
+ - `bold`
+ - `italic`
+ - `link`
+ - `url` - urls pointed to by links
+ - `label` - non-url link references
+ - `text` - url and image descriptions in links
+ - `quote`
+ - `raw`
+ - `inline`
+ - `block`
+
+- `diff` - version control changes
+ - `plus` - additions
+ - `minus` - deletions
+ - `delta` - modifications
+ - `moved` - renamed or moved files/changes
+
#### Interface
These scopes are used for theming the editor interface.
+- `markup`
+ - `normal`
+ - `completion` - for completion doc popup ui
+ - `hover` - for hover popup ui
+ - `heading`
+ - `completion` - for completion doc popup ui
+ - `hover` - for hover popup ui
+ - `raw`
+ - `inline`
+ - `completion` - for completion doc popup ui
+ - `hover` - for hover popup ui
+
| Key | Notes |
| --- | --- |
diff --git a/book/src/usage.md b/book/src/usage.md
index cf7d9d48..a76bfafc 100644
--- a/book/src/usage.md
+++ b/book/src/usage.md
@@ -42,7 +42,7 @@ helix. The keymappings have been inspired from [vim-sandwich](https://github.com
`ms` acts on a selection, so select the text first and use `ms<char>`. `mr` and `md` work
on the closest pairs found and selections are not required; use counts to act in outer pairs.
-It can also act on multiple seletions (yay!). For example, to change every occurance of `(use)` to `[use]`:
+It can also act on multiple selections (yay!). For example, to change every occurrence of `(use)` to `[use]`:
- `%` to select the whole file
- `s` to split the selections on a search term
diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md
new file mode 100644
index 00000000..bdd771aa
--- /dev/null
+++ b/docs/CONTRIBUTING.md
@@ -0,0 +1,37 @@
+# Contributing
+
+Contributors are very welcome! **No contribution is too small and all contributions are valued.**
+
+Some suggestions to get started:
+
+- You can look at the [good first issue][good-first-issue] label on the issue tracker.
+- Help with packaging on various distributions needed!
+- To use print debugging to the [Helix log file][log-file], you must:
+ * Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
+ * Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
+- If your preferred language is missing, integrating a tree-sitter grammar for
+ it and defining syntax highlight queries for it is straight forward and
+ doesn't require much knowledge of the internals.
+
+We provide an [architecture.md][architecture.md] that should give you
+a good overview of the internals.
+
+# Auto generated documentation
+
+Some parts of [the book][docs] are autogenerated from the code itself,
+like the list of `:commands` and supported languages. To generate these
+files, run
+
+```shell
+cargo xtask docgen
+```
+
+inside the project. We use [xtask][xtask] as an ad-hoc task runner and
+thus do not require any dependencies other than `cargo` (You don't have
+to `cargo install` anything either).
+
+[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy
+[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file
+[architecture.md]: ./architecture.md
+[docs]: https://docs.helix-editor.com/
+[xtask]: https://github.com/matklad/cargo-xtask
diff --git a/flake.lock b/flake.lock
index db0fface..94e443e3 100644
--- a/flake.lock
+++ b/flake.lock
@@ -2,11 +2,11 @@
"nodes": {
"devshell": {
"locked": {
- "lastModified": 1632436039,
- "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
+ "lastModified": 1641980203,
+ "narHash": "sha256-RiWJ3+6V267Ji+P54K1Xrj1Nsah9BfG/aLfIhqgVyBY=",
"owner": "numtide",
"repo": "devshell",
- "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
+ "rev": "d897c1ddb4eab66cc2b783c7868d78555b9880ad",
"type": "github"
},
"original": {
@@ -17,11 +17,11 @@
},
"flake-utils": {
"locked": {
- "lastModified": 1623875721,
- "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
+ "lastModified": 1637014545,
+ "narHash": "sha256-26IZAc5yzlD9FlDT54io1oqG/bBoyka+FJk5guaX4x4=",
"owner": "numtide",
"repo": "flake-utils",
- "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
+ "rev": "bba5dcc8e0b20ab664967ad83d24d64cb64ec4f4",
"type": "github"
},
"original": {
@@ -41,11 +41,11 @@
]
},
"locked": {
- "lastModified": 1634796585,
- "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
+ "lastModified": 1642054253,
+ "narHash": "sha256-kHh9VmaB7gbS6pheheC4x0uT84LEmhfbsbWEQJgU2E4=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
- "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
+ "rev": "f8fa9af990195a3f63fe2dde84aa187e193da793",
"type": "github"
},
"original": {
@@ -56,11 +56,11 @@
},
"nixpkgs": {
"locked": {
- "lastModified": 1634782485,
- "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
+ "lastModified": 1641887635,
+ "narHash": "sha256-kDGpufwzVaiGe5e1sBUBPo9f1YN+nYHJlYqCaVpZTQQ=",
"owner": "nixos",
"repo": "nixpkgs",
- "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
+ "rev": "b2737d4980a17cc2b7d600d7d0b32fd7333aca88",
"type": "github"
},
"original": {
@@ -72,15 +72,16 @@
},
"nixpkgs_2": {
"locked": {
- "lastModified": 1628186154,
- "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
+ "lastModified": 1637453606,
+ "narHash": "sha256-Gy6cwUswft9xqsjWxFYEnx/63/qzaFUwatcbV5GF/GQ=",
"owner": "NixOS",
"repo": "nixpkgs",
- "rev": "06552b72346632b6943c8032e57e702ea12413bf",
+ "rev": "8afc4e543663ca0a6a4f496262cd05233737e732",
"type": "github"
},
"original": {
"owner": "NixOS",
+ "ref": "nixpkgs-unstable",
"repo": "nixpkgs",
"type": "github"
}
@@ -98,11 +99,11 @@
"nixpkgs": "nixpkgs_2"
},
"locked": {
- "lastModified": 1634869268,
- "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
+ "lastModified": 1642128126,
+ "narHash": "sha256-av8JUACdrTfQYl/ftZJvKpZEmZfa0avCq7tt5Usdoq0=",
"owner": "oxalica",
"repo": "rust-overlay",
- "rev": "c02c2d86354327317546501af001886fbb53d374",
+ "rev": "ce4ef6f2d74f2b68f7547df1de22d1b0037ce4ad",
"type": "github"
},
"original": {
diff --git a/flake.nix b/flake.nix
index cbf10c97..0d22c5c1 100644
--- a/flake.nix
+++ b/flake.nix
@@ -20,50 +20,63 @@
# Set default package to helix-term release build
defaultOutputs = { app = "hx"; package = "helix"; };
overrides = {
- crateOverrides = common: _: {
- helix-term = prev: {
- # link languages and theme toml files since helix-term expects them (for tests)
- preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} ..";
- buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
- };
+ crateOverrides = common: _: rec {
# link languages and theme toml files since helix-view expects them
- helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
- helix-syntax = _prev: {
+ helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml,base16_theme.toml} .."; };
+ helix-syntax = prev: {
+ src =
+ let
+ pkgs = common.pkgs;
+ helix = pkgs.fetchgit {
+ url = "https://github.com/helix-editor/helix.git";
+ rev = "a8fd33ac012a79069ef1409503a2edcf3a585153";
+ fetchSubmodules = true;
+ sha256 = "sha256-5AtOC55ttWT+7RYMboaFxpGZML51ix93wAkYJTt+8JI=";
+ };
+ in
+ pkgs.runCommand prev.src.name { } ''
+ mkdir -p $out
+ ln -s ${prev.src}/* $out
+ ln -sf ${helix}/helix-syntax/languages $out
+ '';
preConfigure = "mkdir -p ../runtime/grammars";
postInstall = "cp -r ../runtime $out/runtime";
};
- };
- mainBuild = common: prev:
- let
- inherit (common) pkgs lib;
- helixSyntax = lib.buildCrate {
- root = self;
- memberName = "helix-syntax";
- defaultCrateOverrides = {
- helix-syntax = common.crateOverrides.helix-syntax;
+ helix-term = prev:
+ let
+ inherit (common) pkgs lib;
+ helixSyntax = lib.buildCrate {
+ root = self;
+ memberName = "helix-syntax";
+ defaultCrateOverrides = {
+ helix-syntax = helix-syntax;
+ };
+ release = false;
};
- release = false;
+ runtimeDir = pkgs.runCommand "helix-runtime" { } ''
+ mkdir -p $out
+ ln -s ${common.root}/runtime/* $out
+ ln -sf ${helixSyntax}/runtime/grammars $out
+ '';
+ in
+ {
+ # link languages and theme toml files since helix-term expects them (for tests)
+ preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml,base16_theme.toml} ..";
+ buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
+ nativeBuildInputs = [ pkgs.makeWrapper ];
+ postFixup = ''
+ if [ -f "$out/bin/hx" ]; then
+ wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}"
+ fi
+ '';
};
- runtimeDir = pkgs.runCommand "helix-runtime" { } ''
- mkdir -p $out
- ln -s ${common.root}/runtime/* $out
- ln -sf ${helixSyntax}/runtime/grammars $out
- '';
- in
- lib.optionalAttrs (common.memberName == "helix-term") {
- nativeBuildInputs = [ pkgs.makeWrapper ];
- postFixup = ''
- if [ -f "$out/bin/hx" ]; then
- wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}"
- fi
- '';
- };
+ };
shell = common: prev: {
- packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin ]);
+ packages = prev.packages ++ (with common.pkgs; [ lld_13 lldb cargo-tarpaulin cargo-flamegraph ]);
env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; }
- { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
+ { name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment"; }
];
};
};
diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml
index ea695d34..7ff91cfd 100644
--- a/helix-core/Cargo.toml
+++ b/helix-core/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-core"
-version = "0.5.0"
+version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
@@ -13,17 +13,18 @@ include = ["src/**/*", "README.md"]
[features]
[dependencies]
-helix-syntax = { version = "0.5", path = "../helix-syntax" }
+helix-syntax = { version = "0.6", path = "../helix-syntax" }
ropey = "1.3"
-smallvec = "1.7"
-tendril = "0.4.2"
-unicode-segmentation = "1.8"
+smallvec = "1.8"
+smartstring = "0.2.9"
+unicode-segmentation = "1.9"
unicode-width = "0.1"
-unicode-general-category = "0.4"
+unicode-general-category = "0.5"
# slab = "0.4.2"
+slotmap = "1.0"
tree-sitter = "0.20"
-once_cell = "1.8"
+once_cell = "1.9"
arc-swap = "1"
regex = "1"
@@ -35,6 +36,9 @@ toml = "0.5"
similar = "2.1"
etcetera = "0.3"
+encoding_rs = "0.8"
+
+chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
[dev-dependencies]
quickcheck = { version = "1", default-features = false }
diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs
index cc966852..f4359a34 100644
--- a/helix-core/src/auto_pairs.rs
+++ b/helix-core/src/auto_pairs.rs
@@ -1,7 +1,10 @@
//! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character.
-use crate::{Range, Rope, Selection, Tendril, Transaction};
+use crate::{
+ graphemes, movement::Direction, Range, Rope, RopeGraphemes, Selection, Tendril, Transaction,
+};
+use log::debug;
use smallvec::SmallVec;
// Heavily based on https://github.com/codemirror/closebrackets/
@@ -15,7 +18,9 @@ pub const PAIRS: &[(char, char)] = &[
('`', '`'),
];
-const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
+// [TODO] build this dynamically in language config. see #992
+const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
+const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
@@ -25,14 +30,19 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202
//
// to simplify, maybe return Option<Transaction> and just reimplement the default
-// TODO: delete implementation where it erases the whole bracket (|) -> |
+// [TODO]
+// * delete implementation where it erases the whole bracket (|) -> |
+// * change to multi character pairs to handle cases like placing the cursor in the
+// middle of triple quotes, and more exotic pairs like Jinja's {% %}
#[must_use]
pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
+ debug!("autopairs hook selection: {:#?}", selection);
+
for &(open, close) in PAIRS {
if open == ch {
if open == close {
- return handle_same(doc, selection, open);
+ return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE));
} else {
return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE));
}
@@ -47,18 +57,145 @@ pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction>
None
}
-// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close '
-// for example "&'a mut", or "fn<'a>"
-
-fn next_char(doc: &Rope, pos: usize) -> Option<char> {
- if pos >= doc.len_chars() {
+fn prev_char(doc: &Rope, pos: usize) -> Option<char> {
+ if pos == 0 {
return None;
}
- Some(doc.char(pos))
+
+ doc.get_char(pos - 1)
+}
+
+fn is_single_grapheme(doc: &Rope, range: &Range) -> bool {
+ let mut graphemes = RopeGraphemes::new(doc.slice(range.from()..range.to()));
+ let first = graphemes.next();
+ let second = graphemes.next();
+ debug!("first: {:#?}, second: {:#?}", first, second);
+ first.is_some() && second.is_none()
+}
+
+/// calculate what the resulting range should be for an auto pair insertion
+fn get_next_range(
+ doc: &Rope,
+ start_range: &Range,
+ offset: usize,
+ typed_char: char,
+ len_inserted: usize,
+) -> Range {
+ // When the character under the cursor changes due to complete pair
+ // insertion, we must look backward a grapheme and then add the length
+ // of the insertion to put the resulting cursor in the right place, e.g.
+ //
+ // foo[\r\n] - anchor: 3, head: 5
+ // foo([)]\r\n - anchor: 4, head: 5
+ //
+ // foo[\r\n] - anchor: 3, head: 5
+ // foo'[\r\n] - anchor: 4, head: 6
+ //
+ // foo([)]\r\n - anchor: 4, head: 5
+ // foo()[\r\n] - anchor: 5, head: 7
+ //
+ // [foo]\r\n - anchor: 0, head: 3
+ // [foo(])\r\n - anchor: 0, head: 5
+
+ // inserting at the very end of the document after the last newline
+ if start_range.head == doc.len_chars() && start_range.anchor == doc.len_chars() {
+ return Range::new(
+ start_range.anchor + offset + typed_char.len_utf8(),
+ start_range.head + offset + typed_char.len_utf8(),
+ );
+ }
+
+ let single_grapheme = is_single_grapheme(doc, start_range);
+ let doc_slice = doc.slice(..);
+
+ // just skip over graphemes
+ if len_inserted == 0 {
+ let end_anchor = if single_grapheme {
+ graphemes::next_grapheme_boundary(doc_slice, start_range.anchor) + offset
+
+ // even for backward inserts with multiple grapheme selections,
+ // we want the anchor to stay where it is so that the relative
+ // selection does not change, e.g.:
+ //
+ // foo([) wor]d -> insert ) -> foo()[ wor]d
+ } else {
+ start_range.anchor + offset
+ };
+
+ return Range::new(
+ end_anchor,
+ graphemes::next_grapheme_boundary(doc_slice, start_range.head) + offset,
+ );
+ }
+
+ // trivial case: only inserted a single-char opener, just move the selection
+ if len_inserted == 1 {
+ let end_anchor = if single_grapheme || start_range.direction() == Direction::Backward {
+ start_range.anchor + offset + typed_char.len_utf8()
+ } else {
+ start_range.anchor + offset
+ };
+
+ return Range::new(
+ end_anchor,
+ start_range.head + offset + typed_char.len_utf8(),
+ );
+ }
+
+ // If the head = 0, then we must be in insert mode with a backward
+ // cursor, which implies the head will just move
+ let end_head = if start_range.head == 0 || start_range.direction() == Direction::Backward {
+ start_range.head + offset + typed_char.len_utf8()
+ } else {
+ // We must have a forward cursor, which means we must move to the
+ // other end of the grapheme to get to where the new characters
+ // are inserted, then move the head to where it should be
+ let prev_bound = graphemes::prev_grapheme_boundary(doc_slice, start_range.head);
+ debug!(
+ "prev_bound: {}, offset: {}, len_inserted: {}",
+ prev_bound, offset, len_inserted
+ );
+ prev_bound + offset + len_inserted
+ };
+
+ let end_anchor = match (start_range.len(), start_range.direction()) {
+ // if we have a zero width cursor, it shifts to the same number
+ (0, _) => end_head,
+
+ // If we are inserting for a regular one-width cursor, the anchor
+ // moves with the head. This is the fast path for ASCII.
+ (1, Direction::Forward) => end_head - 1,
+ (1, Direction::Backward) => end_head + 1,
+
+ (_, Direction::Forward) => {
+ if single_grapheme {
+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.head)
+ + typed_char.len_utf8()
+
+ // if we are appending, the anchor stays where it is; only offset
+ // for multiple range insertions
+ } else {
+ start_range.anchor + offset
+ }
+ }
+
+ (_, Direction::Backward) => {
+ if single_grapheme {
+ // if we're backward, then the head is at the first char
+ // of the typed char, so we need to add the length of
+ // the closing char
+ graphemes::prev_grapheme_boundary(doc.slice(..), start_range.anchor) + len_inserted
+ } else {
+ // when we are inserting in front of a selection, we need to move
+ // the anchor over by however many characters were inserted overall
+ start_range.anchor + offset + len_inserted
+ }
+ }
+ };
+
+ Range::new(end_anchor, end_head)
}
-// TODO: selections should be extended if range, moved if point.
-// TODO: if not cursor but selection, wrap on both sides of selection (surround)
fn handle_open(
doc: &Rope,
selection: &Selection,
@@ -66,98 +203,584 @@ fn handle_open(
close: char,
close_before: &str,
) -> Transaction {
- let mut ranges = SmallVec::with_capacity(selection.len());
-
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let pos = range.head;
- let next = next_char(doc, pos);
-
- let head = pos + offs + open.len_utf8();
- // if selection, retain anchor, if cursor, move over
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let cursor = start_range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
+ let len_inserted;
- match next {
+ let change = match next_char {
Some(ch) if !close_before.contains(ch) => {
- offs += 1;
- // TODO: else return (use default handler that inserts open)
- (pos, pos, Some(Tendril::from_char(open)))
+ len_inserted = open.len_utf8();
+ let mut tendril = Tendril::new();
+ tendril.push(open);
+ (cursor, cursor, Some(tendril))
}
// None | Some(ch) if close_before.contains(ch) => {}
_ => {
// insert open & close
- let mut pair = Tendril::with_capacity(2);
- pair.push_char(open);
- pair.push_char(close);
+ let pair = Tendril::from_iter([open, close]);
+ len_inserted = open.len_utf8() + close.len_utf8();
+ (cursor, cursor, Some(pair))
+ }
+ };
- offs += 2;
+ let next_range = get_next_range(doc, start_range, offs, open, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
- (pos, pos, Some(pair))
- }
- }
+ change
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
}
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
- let mut ranges = SmallVec::with_capacity(selection.len());
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
- let transaction = Transaction::change_by_selection(doc, selection, |range| {
- let pos = range.head;
- let next = next_char(doc, pos);
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let cursor = start_range.cursor(doc.slice(..));
+ let next_char = doc.get_char(cursor);
+ let mut len_inserted = 0;
- let head = pos + offs + close.len_utf8();
- // if selection, retain anchor, if cursor, move over
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
+ let change = if next_char == Some(close) {
+ // return transaction that moves past close
+ (cursor, cursor, None) // no-op
+ } else {
+ len_inserted += close.len_utf8();
+ let mut tendril = Tendril::new();
+ tendril.push(close);
+ (cursor, cursor, Some(tendril))
+ };
+
+ let next_range = get_next_range(doc, start_range, offs, close, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
+
+ change
+ });
+
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
+}
+
+/// handle cases where open and close is the same, or in triples ("""docstring""")
+fn handle_same(
+ doc: &Rope,
+ selection: &Selection,
+ token: char,
+ close_before: &str,
+ open_before: &str,
+) -> Transaction {
+ let mut end_ranges = SmallVec::with_capacity(selection.len());
+
+ let mut offs = 0;
- if next == Some(close) {
+ let transaction = Transaction::change_by_selection(doc, selection, |start_range| {
+ let cursor = start_range.cursor(doc.slice(..));
+ let mut len_inserted = 0;
+
+ let next_char = doc.get_char(cursor);
+ let prev_char = prev_char(doc, cursor);
+
+ let change = if next_char == Some(token) {
// return transaction that moves past close
- (pos, pos, None) // no-op
+ (cursor, cursor, None) // no-op
} else {
- offs += close.len_utf8();
+ let mut pair = Tendril::new();
+ pair.push(token);
- // TODO: else return (use default handler that inserts close)
- (pos, pos, Some(Tendril::from_char(close)))
- }
+ // for equal pairs, don't insert both open and close if either
+ // side has a non-pair char
+ if (next_char.is_none() || close_before.contains(next_char.unwrap()))
+ && (prev_char.is_none() || open_before.contains(prev_char.unwrap()))
+ {
+ pair.push(token);
+ }
+
+ len_inserted += pair.len();
+ (cursor, cursor, Some(pair))
+ };
+
+ let next_range = get_next_range(doc, start_range, offs, token, len_inserted);
+ end_ranges.push(next_range);
+ offs += len_inserted;
+
+ change
});
- transaction.with_selection(Selection::new(ranges, selection.primary_index()))
+ let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index()));
+ debug!("auto pair transaction: {:#?}", t);
+ t
}
-// handle cases where open and close is the same, or in triples ("""docstring""")
-fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
- // if not cursor but selection, wrap
- // let next = next char
-
- // if next == bracket {
- // // if start of syntax node, insert token twice (new pair because node is complete)
- // // elseif colsedBracketAt
- // // is_triple == allow triple && next 3 is equal
- // // cursor jump over
- // }
- //} else if allow_triple && followed by triple {
- //}
- //} else if next != word char && prev != bracket && prev != word char {
- // // condition checks for cases like I' where you don't want I'' (or I'm)
- // insert pair ("")
- //}
- None
+#[cfg(test)]
+mod test {
+ use super::*;
+ use smallvec::smallvec;
+
+ const LINE_END: &str = crate::DEFAULT_LINE_ENDING.as_str();
+
+ fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ PAIRS.iter().filter(|(open, close)| open != close)
+ }
+
+ fn matching_pairs() -> impl Iterator<Item = &'static (char, char)> {
+ PAIRS.iter().filter(|(open, close)| open == close)
+ }
+
+ fn test_hooks(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ ch: char,
+ expected_doc: &Rope,
+ expected_sel: &Selection,
+ ) {
+ let trans = hook(in_doc, in_sel, ch).unwrap();
+ let mut actual_doc = in_doc.clone();
+ assert!(trans.apply(&mut actual_doc));
+ assert_eq!(expected_doc, &actual_doc);
+ assert_eq!(expected_sel, trans.selection().unwrap());
+ }
+
+ fn test_hooks_with_pairs<I, F, R>(
+ in_doc: &Rope,
+ in_sel: &Selection,
+ pairs: I,
+ get_expected_doc: F,
+ actual_sel: &Selection,
+ ) where
+ I: IntoIterator<Item = &'static (char, char)>,
+ F: Fn(char, char) -> R,
+ R: Into<Rope>,
+ Rope: From<R>,
+ {
+ pairs.into_iter().for_each(|(open, close)| {
+ test_hooks(
+ in_doc,
+ in_sel,
+ *open,
+ &Rope::from(get_expected_doc(*open, *close)),
+ actual_sel,
+ )
+ });
+ }
+
+ // [] indicates range
+
+ /// [] -> insert ( -> ([])
+ #[test]
+ fn test_insert_blank() {
+ test_hooks_with_pairs(
+ &Rope::from(LINE_END),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, close| format!("{}{}{}", open, close, LINE_END),
+ &Selection::single(2, 1),
+ );
+
+ let empty_doc = Rope::from(format!("{line_end}{line_end}", line_end = LINE_END));
+
+ test_hooks_with_pairs(
+ &empty_doc,
+ &Selection::single(empty_doc.len_chars(), LINE_END.len()),
+ PAIRS,
+ |open, close| {
+ format!(
+ "{line_end}{open}{close}{line_end}",
+ open = open,
+ close = close,
+ line_end = LINE_END
+ )
+ },
+ &Selection::single(LINE_END.len() + 2, LINE_END.len() + 1),
+ );
+ }
+
+ #[test]
+ fn test_insert_before_multi_code_point_graphemes() {
+ test_hooks_with_pairs(
+ &Rope::from(format!("hello 👨‍👩‍👧‍👦 goodbye{}", LINE_END)),
+ &Selection::single(13, 6),
+ PAIRS,
+ |open, _| format!("hello {}👨‍👩‍👧‍👦 goodbye{}", open, LINE_END),
+ &Selection::single(14, 7),
+ );
+ }
+
+ #[test]
+ fn test_insert_at_end_of_document() {
+ test_hooks_with_pairs(
+ &Rope::from(LINE_END),
+ &Selection::single(LINE_END.len(), LINE_END.len()),
+ PAIRS,
+ |open, close| format!("{}{}{}", LINE_END, open, close),
+ &Selection::single(LINE_END.len() + 1, LINE_END.len() + 1),
+ );
+
+ test_hooks_with_pairs(
+ &Rope::from(format!("foo{}", LINE_END)),
+ &Selection::single(3 + LINE_END.len(), 3 + LINE_END.len()),
+ PAIRS,
+ |open, close| format!("foo{}{}{}", LINE_END, open, close),
+ &Selection::single(LINE_END.len() + 4, LINE_END.len() + 4),
+ );
+ }
+
+ /// [] -> append ( -> ([])
+ #[test]
+ fn test_append_blank() {
+ test_hooks_with_pairs(
+ // this is what happens when you have a totally blank document and then append
+ &Rope::from(format!("{line_end}{line_end}", line_end = LINE_END)),
+ // before inserting the pair, the cursor covers all of both empty lines
+ &Selection::single(0, LINE_END.len() * 2),
+ PAIRS,
+ |open, close| {
+ format!(
+ "{line_end}{open}{close}{line_end}",
+ line_end = LINE_END,
+ open = open,
+ close = close
+ )
+ },
+ // after inserting pair, the cursor covers the first new line and the open char
+ &Selection::single(0, LINE_END.len() + 2),
+ );
+ }
+
+ /// [] ([])
+ /// [] -> insert -> ([])
+ /// [] ([])
+ #[test]
+ fn test_insert_blank_multi_cursor() {
+ test_hooks_with_pairs(
+ &Rope::from("\n\n\n"),
+ &Selection::new(
+ smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),),
+ 0,
+ ),
+ PAIRS,
+ |open, close| {
+ format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+ 0,
+ ),
+ );
+ }
+
+ /// fo[o] -> append ( -> fo[o(])
+ #[test]
+ fn test_append() {
+ test_hooks_with_pairs(
+ &Rope::from("foo\n"),
+ &Selection::single(2, 4),
+ differing_pairs(),
+ |open, close| format!("foo{}{}\n", open, close),
+ &Selection::single(2, 5),
+ );
+ }
+
+ /// foo[] -> append to end of line ( -> foo([])
+ #[test]
+ fn test_append_single_cursor() {
+ test_hooks_with_pairs(
+ &Rope::from(format!("foo{}", LINE_END)),
+ &Selection::single(3, 3 + LINE_END.len()),
+ differing_pairs(),
+ |open, close| format!("foo{}{}{}", open, close, LINE_END),
+ &Selection::single(4, 5),
+ );
+ }
+
+ /// fo[o] fo[o(])
+ /// fo[o] -> append ( -> fo[o(])
+ /// fo[o] fo[o(])
+ #[test]
+ fn test_append_multi() {
+ test_hooks_with_pairs(
+ &Rope::from("foo\nfoo\nfoo\n"),
+ &Selection::new(
+ smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)),
+ 0,
+ ),
+ differing_pairs(),
+ |open, close| {
+ format!(
+ "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n",
+ open = open,
+ close = close
+ )
+ },
+ &Selection::new(
+ smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)),
+ 0,
+ ),
+ );
+ }
+
+ /// ([)] -> insert ) -> ()[]
+ #[test]
+ fn test_insert_close_inside_pair() {
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
+
+ test_hooks(
+ &doc,
+ &Selection::single(2, 1),
+ *close,
+ &doc,
+ &Selection::single(2 + LINE_END.len(), 2),
+ );
+ }
+ }
+
+ /// [(]) -> append ) -> [()]
+ #[test]
+ fn test_append_close_inside_pair() {
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!("{}{}{}", open, close, LINE_END));
+
+ test_hooks(
+ &doc,
+ &Selection::single(0, 2),
+ *close,
+ &doc,
+ &Selection::single(0, 2 + LINE_END.len()),
+ );
+ }
+ }
+
+ /// ([]) ()[]
+ /// ([]) -> insert ) -> ()[]
+ /// ([]) ()[]
+ #[test]
+ fn test_insert_close_inside_pair_multi_cursor() {
+ let sel = Selection::new(
+ smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),),
+ 0,
+ );
+
+ let expected_sel = Selection::new(
+ smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),),
+ 0,
+ );
+
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+ }
+ }
+
+ /// [(]) [()]
+ /// [(]) -> append ) -> [()]
+ /// [(]) [()]
+ #[test]
+ fn test_append_close_inside_pair_multi_cursor() {
+ let sel = Selection::new(
+ smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),),
+ 0,
+ );
+
+ let expected_sel = Selection::new(
+ smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),),
+ 0,
+ );
+
+ for (open, close) in PAIRS {
+ let doc = Rope::from(format!(
+ "{open}{close}\n{open}{close}\n{open}{close}\n",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *close, &doc, &expected_sel);
+ }
+ }
+
+ /// ([]) -> insert ( -> (([]))
+ #[test]
+ fn test_insert_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::single(3, 2);
+
+ for (open, close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", open, close));
+ let expected_doc = Rope::from(format!(
+ "{open}{open}{close}{close}",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+ }
+ }
+
+ /// [word(]) -> append ( -> [word((]))
+ #[test]
+ fn test_append_open_inside_pair() {
+ let sel = Selection::single(0, 6);
+ let expected_sel = Selection::single(0, 7);
+
+ for (open, close) in differing_pairs() {
+ let doc = Rope::from(format!("word{}{}", open, close));
+ let expected_doc = Rope::from(format!(
+ "word{open}{open}{close}{close}",
+ open = open,
+ close = close
+ ));
+
+ test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel);
+ }
+ }
+
+ /// ([]) -> insert " -> ("[]")
+ #[test]
+ fn test_insert_nested_open_inside_pair() {
+ let sel = Selection::single(2, 1);
+ let expected_sel = Selection::single(3, 2);
+
+ for (outer_open, outer_close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
+
+ for (inner_open, inner_close) in matching_pairs() {
+ let expected_doc = Rope::from(format!(
+ "{}{}{}{}",
+ outer_open, inner_open, inner_close, outer_close
+ ));
+
+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+ }
+ }
+ }
+
+ /// [(]) -> append " -> [("]")
+ #[test]
+ fn test_append_nested_open_inside_pair() {
+ let sel = Selection::single(0, 2);
+ let expected_sel = Selection::single(0, 3);
+
+ for (outer_open, outer_close) in differing_pairs() {
+ let doc = Rope::from(format!("{}{}", outer_open, outer_close,));
+
+ for (inner_open, inner_close) in matching_pairs() {
+ let expected_doc = Rope::from(format!(
+ "{}{}{}{}",
+ outer_open, inner_open, inner_close, outer_close
+ ));
+
+ test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel);
+ }
+ }
+ }
+
+ /// []word -> insert ( -> ([]word
+ #[test]
+ fn test_insert_open_before_non_pair() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(1, 0),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(2, 1),
+ )
+ }
+
+ /// [wor]d -> insert ( -> ([wor]d
+ #[test]
+ fn test_insert_open_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("word"),
+ &Selection::single(3, 0),
+ PAIRS,
+ |open, _| format!("{}word", open),
+ &Selection::single(4, 1),
+ )
+ }
+
+ /// [wor]d -> append ) -> [wor)]d
+ #[test]
+ fn test_append_close_inside_non_pair_with_selection() {
+ let sel = Selection::single(0, 4);
+ let expected_sel = Selection::single(0, 5);
+
+ for (_, close) in PAIRS {
+ let doc = Rope::from("word");
+ let expected_doc = Rope::from(format!("wor{}d", close));
+ test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel);
+ }
+ }
+
+ /// foo[ wor]d -> insert ( -> foo([) wor]d
+ #[test]
+ fn test_insert_open_trailing_word_with_selection() {
+ test_hooks_with_pairs(
+ &Rope::from("foo word"),
+ &Selection::single(7, 3),
+ differing_pairs(),
+ |open, close| format!("foo{}{} word", open, close),
+ &Selection::single(9, 4),
+ )
+ }
+
+ /// foo([) wor]d -> insert ) -> foo()[ wor]d
+ #[test]
+ fn test_insert_close_inside_pair_trailing_word_with_selection() {
+ for (open, close) in differing_pairs() {
+ test_hooks(
+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
+ &Selection::single(9, 4),
+ *close,
+ &Rope::from(format!("foo{}{} word{}", open, close, LINE_END)),
+ &Selection::single(9, 5),
+ )
+ }
+ }
+
+ /// we want pairs that are *not* the same char to be inserted after
+ /// a non-pair char, for cases like functions, but for pairs that are
+ /// the same char, we want to *not* insert a pair to handle cases like "I'm"
+ ///
+ /// word[] -> insert ( -> word([])
+ /// word[] -> insert ' -> word'[]
+ #[test]
+ fn test_insert_open_after_non_pair() {
+ let doc = Rope::from(format!("word{}", LINE_END));
+ let sel = Selection::single(5, 4);
+ let expected_sel = Selection::single(6, 5);
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ differing_pairs(),
+ |open, close| format!("word{}{}{}", open, close, LINE_END),
+ &expected_sel,
+ );
+
+ test_hooks_with_pairs(
+ &doc,
+ &sel,
+ matching_pairs(),
+ |open, _| format!("word{}{}", open, LINE_END),
+ &expected_sel,
+ );
+ }
}
diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs
index c8e5efbd..54991574 100644
--- a/helix-core/src/chars.rs
+++ b/helix-core/src/chars.rs
@@ -91,12 +91,11 @@ mod test {
#[test]
fn test_categorize() {
- const EOL_TEST_CASE: &'static str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
- const WORD_TEST_CASE: &'static str =
- "_hello_world_あいうえおー12345678901234567890";
- const PUNCTUATION_TEST_CASE: &'static str =
+ const EOL_TEST_CASE: &str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
+ const WORD_TEST_CASE: &str = "_hello_world_あいうえおー12345678901234567890";
+ const PUNCTUATION_TEST_CASE: &str =
"!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
- const WHITESPACE_TEST_CASE: &'static str = "      ";
+ const WHITESPACE_TEST_CASE: &str = "      ";
for ch in EOL_TEST_CASE.chars() {
assert_eq!(CharCategory::Eol, categorize_char(ch));
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index 4fcf51c9..210ad639 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,12 +1,19 @@
//! LSP diagnostic utility types.
+use serde::{Deserialize, Serialize};
/// Describes the severity level of a [`Diagnostic`].
-#[derive(Debug, Clone, Copy, Eq, PartialEq)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)]
pub enum Severity {
- Error,
- Warning,
- Info,
Hint,
+ Info,
+ Warning,
+ Error,
+}
+
+impl Default for Severity {
+ fn default() -> Self {
+ Self::Hint
+ }
}
/// A range of `char`s within the text.
diff --git a/helix-core/src/diff.rs b/helix-core/src/diff.rs
index a83db333..6960c679 100644
--- a/helix-core/src/diff.rs
+++ b/helix-core/src/diff.rs
@@ -11,10 +11,6 @@ pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction {
// A timeout is set so after 1 seconds, the algorithm will start
// approximating. This is especially important for big `Rope`s or
// `Rope`s that are extremely dissimilar to each other.
- //
- // Note: Ignore the clippy warning, as the trait bounds of
- // `Transaction::change()` require an iterator implementing
- // `ExactIterator`.
let mut config = similar::TextDiff::configure();
config.timeout(std::time::Duration::from_secs(1));
@@ -62,7 +58,7 @@ mod tests {
let mut old = Rope::from(a);
let new = Rope::from(b);
compare_ropes(&old, &new).apply(&mut old);
- old.to_string() == new.to_string()
+ old == new
}
}
}
diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs
index c6398875..aa898684 100644
--- a/helix-core/src/graphemes.rs
+++ b/helix-core/src/graphemes.rs
@@ -120,6 +120,43 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
chunk_char_idx + tmp
}
+#[must_use]
+pub fn nth_next_grapheme_boundary_byte(slice: RopeSlice, mut byte_idx: usize, n: usize) -> usize {
+ // Bounds check
+ debug_assert!(byte_idx <= slice.len_bytes());
+
+ // Get the chunk with our byte index in it.
+ let (mut chunk, mut chunk_byte_idx, mut _chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
+
+ // Set up the grapheme cursor.
+ let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
+
+ // Find the nth next grapheme cluster boundary.
+ for _ in 0..n {
+ loop {
+ match gc.next_boundary(chunk, chunk_byte_idx) {
+ Ok(None) => return slice.len_bytes(),
+ Ok(Some(n)) => {
+ byte_idx = n;
+ break;
+ }
+ Err(GraphemeIncomplete::NextChunk) => {
+ chunk_byte_idx += chunk.len();
+ let (a, _, _c, _) = slice.chunk_at_byte(chunk_byte_idx);
+ chunk = a;
+ // chunk_char_idx = c;
+ }
+ Err(GraphemeIncomplete::PreContext(n)) => {
+ let ctx_chunk = slice.chunk_at_byte(n - 1).0;
+ gc.provide_context(ctx_chunk, n - ctx_chunk.len());
+ }
+ _ => unreachable!(),
+ }
+ }
+ }
+ byte_idx
+}
+
/// Finds the next grapheme boundary after the given char position.
#[must_use]
#[inline(always)]
@@ -127,6 +164,13 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
nth_next_grapheme_boundary(slice, char_idx, 1)
}
+/// Finds the next grapheme boundary after the given byte position.
+#[must_use]
+#[inline(always)]
+pub fn next_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> usize {
+ nth_next_grapheme_boundary_byte(slice, byte_idx, 1)
+}
+
/// Returns the passed char index if it's already a grapheme boundary,
/// or the next grapheme boundary char index if not.
#[must_use]
@@ -151,6 +195,23 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize
}
}
+/// Returns the passed byte index if it's already a grapheme boundary,
+/// or the next grapheme boundary byte index if not.
+#[must_use]
+#[inline]
+pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize {
+ if byte_idx == 0 {
+ byte_idx
+ } else {
+ // TODO: optimize so we're not constructing grapheme cursor twice
+ if is_grapheme_boundary_byte(slice, byte_idx) {
+ byte_idx
+ } else {
+ next_grapheme_boundary_byte(slice, byte_idx)
+ }
+ }
+}
+
/// Returns whether the given char position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
@@ -179,6 +240,31 @@ pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
}
}
+/// Returns whether the given byte position is a grapheme boundary.
+#[must_use]
+pub fn is_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> bool {
+ // Bounds check
+ debug_assert!(byte_idx <= slice.len_bytes());
+
+ // Get the chunk with our byte index in it.
+ let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
+
+ // Set up the grapheme cursor.
+ let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
+
+ // Determine if the given position is a grapheme cluster boundary.
+ loop {
+ match gc.is_boundary(chunk, chunk_byte_idx) {
+ Ok(n) => return n,
+ Err(GraphemeIncomplete::PreContext(n)) => {
+ let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
+ gc.provide_context(ctx_chunk, ctx_byte_start);
+ }
+ Err(_) => unreachable!(),
+ }
+ }
+}
+
/// An iterator over the graphemes of a `RopeSlice`.
#[derive(Clone)]
pub struct RopeGraphemes<'a> {
diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs
index 4b1c8d3b..bb95213c 100644
--- a/helix-core/src/history.rs
+++ b/helix-core/src/history.rs
@@ -448,8 +448,8 @@ mod test {
change: crate::transaction::Change,
instant: Instant,
) {
- let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter());
- history.commit_revision_at_timestamp(&txn, &state, instant);
+ let txn = Transaction::change(&state.doc, vec![change].into_iter());
+ history.commit_revision_at_timestamp(&txn, state, instant);
txn.apply(&mut state.doc);
}
diff --git a/helix-core/src/increment/date_time.rs b/helix-core/src/increment/date_time.rs
new file mode 100644
index 00000000..91fa5963
--- /dev/null
+++ b/helix-core/src/increment/date_time.rs
@@ -0,0 +1,490 @@
+use chrono::{Datelike, Duration, NaiveDate, NaiveDateTime, NaiveTime, Timelike};
+use once_cell::sync::Lazy;
+use regex::Regex;
+use ropey::RopeSlice;
+
+use std::borrow::Cow;
+use std::cmp;
+
+use super::Increment;
+use crate::{Range, Tendril};
+
+#[derive(Debug, PartialEq, Eq)]
+pub struct DateTimeIncrementor {
+ date_time: NaiveDateTime,
+ range: Range,
+ fmt: &'static str,
+ field: DateField,
+}
+
+impl DateTimeIncrementor {
+ pub fn from_range(text: RopeSlice, range: Range) -> Option<DateTimeIncrementor> {
+ let range = if range.is_empty() {
+ if range.anchor < text.len_chars() {
+ // Treat empty range as a cursor range.
+ range.put_cursor(text, range.anchor + 1, true)
+ } else {
+ // The range is empty and at the end of the text.
+ return None;
+ }
+ } else {
+ range
+ };
+
+ FORMATS.iter().find_map(|format| {
+ let from = range.from().saturating_sub(format.max_len);
+ let to = (range.from() + format.max_len).min(text.len_chars());
+
+ let (from_in_text, to_in_text) = (range.from() - from, range.to() - from);
+ let text: Cow<str> = text.slice(from..to).into();
+
+ let captures = format.regex.captures(&text)?;
+ if captures.len() - 1 != format.fields.len() {
+ return None;
+ }
+
+ let date_time = captures.get(0)?;
+ let offset = range.from() - from_in_text;
+ let range = Range::new(date_time.start() + offset, date_time.end() + offset);
+
+ let field = captures
+ .iter()
+ .skip(1)
+ .enumerate()
+ .find_map(|(i, capture)| {
+ let capture = capture?;
+ let capture_range = capture.range();
+
+ if capture_range.contains(&from_in_text)
+ && capture_range.contains(&(to_in_text - 1))
+ {
+ Some(format.fields[i])
+ } else {
+ None
+ }
+ })?;
+
+ let has_date = format.fields.iter().any(|f| f.unit.is_date());
+ let has_time = format.fields.iter().any(|f| f.unit.is_time());
+
+ let date_time = &text[date_time.start()..date_time.end()];
+ let date_time = match (has_date, has_time) {
+ (true, true) => NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?,
+ (true, false) => {
+ let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
+
+ date.and_hms(0, 0, 0)
+ }
+ (false, true) => {
+ let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
+
+ NaiveDate::from_ymd(0, 1, 1).and_time(time)
+ }
+ (false, false) => return None,
+ };
+
+ Some(DateTimeIncrementor {
+ date_time,
+ range,
+ fmt: format.fmt,
+ field,
+ })
+ })
+ }
+}
+
+impl Increment for DateTimeIncrementor {
+ fn increment(&self, amount: i64) -> (Range, Tendril) {
+ let date_time = match self.field.unit {
+ DateUnit::Years => add_years(self.date_time, amount),
+ DateUnit::Months => add_months(self.date_time, amount),
+ DateUnit::Days => add_duration(self.date_time, Duration::days(amount)),
+ DateUnit::Hours => add_duration(self.date_time, Duration::hours(amount)),
+ DateUnit::Minutes => add_duration(self.date_time, Duration::minutes(amount)),
+ DateUnit::Seconds => add_duration(self.date_time, Duration::seconds(amount)),
+ DateUnit::AmPm => toggle_am_pm(self.date_time),
+ }
+ .unwrap_or(self.date_time);
+
+ (self.range, date_time.format(self.fmt).to_string().into())
+ }
+}
+
+static FORMATS: Lazy<Vec<Format>> = Lazy::new(|| {
+ vec![
+ Format::new("%Y-%m-%d %H:%M:%S"), // 2021-11-24 07:12:23
+ Format::new("%Y/%m/%d %H:%M:%S"), // 2021/11/24 07:12:23
+ Format::new("%Y-%m-%d %H:%M"), // 2021-11-24 07:12
+ Format::new("%Y/%m/%d %H:%M"), // 2021/11/24 07:12
+ Format::new("%Y-%m-%d"), // 2021-11-24
+ Format::new("%Y/%m/%d"), // 2021/11/24
+ Format::new("%a %b %d %Y"), // Wed Nov 24 2021
+ Format::new("%d-%b-%Y"), // 24-Nov-2021
+ Format::new("%Y %b %d"), // 2021 Nov 24
+ Format::new("%b %d, %Y"), // Nov 24, 2021
+ Format::new("%-I:%M:%S %P"), // 7:21:53 am
+ Format::new("%-I:%M %P"), // 7:21 am
+ Format::new("%-I:%M:%S %p"), // 7:21:53 AM
+ Format::new("%-I:%M %p"), // 7:21 AM
+ Format::new("%H:%M:%S"), // 23:24:23
+ Format::new("%H:%M"), // 23:24
+ ]
+});
+
+#[derive(Debug)]
+struct Format {
+ fmt: &'static str,
+ fields: Vec<DateField>,
+ regex: Regex,
+ max_len: usize,
+}
+
+impl Format {
+ fn new(fmt: &'static str) -> Self {
+ let mut remaining = fmt;
+ let mut fields = Vec::new();
+ let mut regex = String::new();
+ let mut max_len = 0;
+
+ while let Some(i) = remaining.find('%') {
+ let after = &remaining[i + 1..];
+ let mut chars = after.chars();
+ let c = chars.next().unwrap();
+
+ let spec_len = if c == '-' {
+ 1 + chars.next().unwrap().len_utf8()
+ } else {
+ c.len_utf8()
+ };
+
+ let specifier = &after[..spec_len];
+ let field = DateField::from_specifier(specifier).unwrap();
+ fields.push(field);
+ max_len += field.max_len + remaining[..i].len();
+ regex += &remaining[..i];
+ regex += &format!("({})", field.regex);
+ remaining = &after[spec_len..];
+ }
+
+ let regex = Regex::new(&regex).unwrap();
+
+ Self {
+ fmt,
+ fields,
+ regex,
+ max_len,
+ }
+ }
+}
+
+impl PartialEq for Format {
+ fn eq(&self, other: &Self) -> bool {
+ self.fmt == other.fmt && self.fields == other.fields && self.max_len == other.max_len
+ }
+}
+
+impl Eq for Format {}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+struct DateField {
+ regex: &'static str,
+ unit: DateUnit,
+ max_len: usize,
+}
+
+impl DateField {
+ fn from_specifier(specifier: &str) -> Option<Self> {
+ match specifier {
+ "Y" => Some(Self {
+ regex: r"\d{4}",
+ unit: DateUnit::Years,
+ max_len: 5,
+ }),
+ "y" => Some(Self {
+ regex: r"\d\d",
+ unit: DateUnit::Years,
+ max_len: 2,
+ }),
+ "m" => Some(Self {
+ regex: r"[0-1]\d",
+ unit: DateUnit::Months,
+ max_len: 2,
+ }),
+ "d" => Some(Self {
+ regex: r"[0-3]\d",
+ unit: DateUnit::Days,
+ max_len: 2,
+ }),
+ "-d" => Some(Self {
+ regex: r"[1-3]?\d",
+ unit: DateUnit::Days,
+ max_len: 2,
+ }),
+ "a" => Some(Self {
+ regex: r"Sun|Mon|Tue|Wed|Thu|Fri|Sat",
+ unit: DateUnit::Days,
+ max_len: 3,
+ }),
+ "A" => Some(Self {
+ regex: r"Sunday|Monday|Tuesday|Wednesday|Thursday|Friday|Saturday",
+ unit: DateUnit::Days,
+ max_len: 9,
+ }),
+ "b" | "h" => Some(Self {
+ regex: r"Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec",
+ unit: DateUnit::Months,
+ max_len: 3,
+ }),
+ "B" => Some(Self {
+ regex: r"January|February|March|April|May|June|July|August|September|October|November|December",
+ unit: DateUnit::Months,
+ max_len: 9,
+ }),
+ "H" => Some(Self {
+ regex: r"[0-2]\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "M" => Some(Self {
+ regex: r"[0-5]\d",
+ unit: DateUnit::Minutes,
+ max_len: 2,
+ }),
+ "S" => Some(Self {
+ regex: r"[0-5]\d",
+ unit: DateUnit::Seconds,
+ max_len: 2,
+ }),
+ "I" => Some(Self {
+ regex: r"[0-1]\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "-I" => Some(Self {
+ regex: r"1?\d",
+ unit: DateUnit::Hours,
+ max_len: 2,
+ }),
+ "P" => Some(Self {
+ regex: r"am|pm",
+ unit: DateUnit::AmPm,
+ max_len: 2,
+ }),
+ "p" => Some(Self {
+ regex: r"AM|PM",
+ unit: DateUnit::AmPm,
+ max_len: 2,
+ }),
+ _ => None,
+ }
+ }
+}
+
+#[derive(Copy, Clone, Debug, PartialEq, Eq)]
+enum DateUnit {
+ Years,
+ Months,
+ Days,
+ Hours,
+ Minutes,
+ Seconds,
+ AmPm,
+}
+
+impl DateUnit {
+ fn is_date(self) -> bool {
+ matches!(self, DateUnit::Years | DateUnit::Months | DateUnit::Days)
+ }
+
+ fn is_time(self) -> bool {
+ matches!(
+ self,
+ DateUnit::Hours | DateUnit::Minutes | DateUnit::Seconds
+ )
+ }
+}
+
+fn ndays_in_month(year: i32, month: u32) -> u32 {
+ // The first day of the next month...
+ let (y, m) = if month == 12 {
+ (year + 1, 1)
+ } else {
+ (year, month + 1)
+ };
+ let d = NaiveDate::from_ymd(y, m, 1);
+
+ // ...is preceded by the last day of the original month.
+ d.pred().day()
+}
+
+fn add_months(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
+ let month = (date_time.month0() as i64).checked_add(amount)?;
+ let year = date_time.year() + i32::try_from(month / 12).ok()?;
+ let year = if month.is_negative() { year - 1 } else { year };
+
+ // Normalize month
+ let month = month % 12;
+ let month = if month.is_negative() {
+ month + 12
+ } else {
+ month
+ } as u32
+ + 1;
+
+ let day = cmp::min(date_time.day(), ndays_in_month(year, month));
+
+ Some(NaiveDate::from_ymd(year, month, day).and_time(date_time.time()))
+}
+
+fn add_years(date_time: NaiveDateTime, amount: i64) -> Option<NaiveDateTime> {
+ let year = i32::try_from((date_time.year() as i64).checked_add(amount)?).ok()?;
+ let ndays = ndays_in_month(year, date_time.month());
+
+ if date_time.day() > ndays {
+ let d = NaiveDate::from_ymd(year, date_time.month(), ndays);
+ Some(d.succ().and_time(date_time.time()))
+ } else {
+ date_time.with_year(year)
+ }
+}
+
+fn add_duration(date_time: NaiveDateTime, duration: Duration) -> Option<NaiveDateTime> {
+ date_time.checked_add_signed(duration)
+}
+
+fn toggle_am_pm(date_time: NaiveDateTime) -> Option<NaiveDateTime> {
+ if date_time.hour() < 12 {
+ add_duration(date_time, Duration::hours(12))
+ } else {
+ add_duration(date_time, Duration::hours(-12))
+ }
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+ use crate::Rope;
+
+ #[test]
+ fn test_increment_date_times() {
+ let tests = [
+ // (original, cursor, amount, expected)
+ ("2020-02-28", 0, 1, "2021-02-28"),
+ ("2020-02-29", 0, 1, "2021-03-01"),
+ ("2020-01-31", 5, 1, "2020-02-29"),
+ ("2020-01-20", 5, 1, "2020-02-20"),
+ ("2021-01-01", 5, -1, "2020-12-01"),
+ ("2021-01-31", 5, -2, "2020-11-30"),
+ ("2020-02-28", 8, 1, "2020-02-29"),
+ ("2021-02-28", 8, 1, "2021-03-01"),
+ ("2021-02-28", 0, -1, "2020-02-28"),
+ ("2021-03-01", 0, -1, "2020-03-01"),
+ ("2020-02-29", 5, -1, "2020-01-29"),
+ ("2020-02-20", 5, -1, "2020-01-20"),
+ ("2020-02-29", 8, -1, "2020-02-28"),
+ ("2021-03-01", 8, -1, "2021-02-28"),
+ ("1980/12/21", 8, 100, "1981/03/31"),
+ ("1980/12/21", 8, -100, "1980/09/12"),
+ ("1980/12/21", 8, 1000, "1983/09/17"),
+ ("1980/12/21", 8, -1000, "1978/03/27"),
+ ("2021-11-24 07:12:23", 0, 1, "2022-11-24 07:12:23"),
+ ("2021-11-24 07:12:23", 5, 1, "2021-12-24 07:12:23"),
+ ("2021-11-24 07:12:23", 8, 1, "2021-11-25 07:12:23"),
+ ("2021-11-24 07:12:23", 11, 1, "2021-11-24 08:12:23"),
+ ("2021-11-24 07:12:23", 14, 1, "2021-11-24 07:13:23"),
+ ("2021-11-24 07:12:23", 17, 1, "2021-11-24 07:12:24"),
+ ("2021/11/24 07:12:23", 0, 1, "2022/11/24 07:12:23"),
+ ("2021/11/24 07:12:23", 5, 1, "2021/12/24 07:12:23"),
+ ("2021/11/24 07:12:23", 8, 1, "2021/11/25 07:12:23"),
+ ("2021/11/24 07:12:23", 11, 1, "2021/11/24 08:12:23"),
+ ("2021/11/24 07:12:23", 14, 1, "2021/11/24 07:13:23"),
+ ("2021/11/24 07:12:23", 17, 1, "2021/11/24 07:12:24"),
+ ("2021-11-24 07:12", 0, 1, "2022-11-24 07:12"),
+ ("2021-11-24 07:12", 5, 1, "2021-12-24 07:12"),
+ ("2021-11-24 07:12", 8, 1, "2021-11-25 07:12"),
+ ("2021-11-24 07:12", 11, 1, "2021-11-24 08:12"),
+ ("2021-11-24 07:12", 14, 1, "2021-11-24 07:13"),
+ ("2021/11/24 07:12", 0, 1, "2022/11/24 07:12"),
+ ("2021/11/24 07:12", 5, 1, "2021/12/24 07:12"),
+ ("2021/11/24 07:12", 8, 1, "2021/11/25 07:12"),
+ ("2021/11/24 07:12", 11, 1, "2021/11/24 08:12"),
+ ("2021/11/24 07:12", 14, 1, "2021/11/24 07:13"),
+ ("Wed Nov 24 2021", 0, 1, "Thu Nov 25 2021"),
+ ("Wed Nov 24 2021", 4, 1, "Fri Dec 24 2021"),
+ ("Wed Nov 24 2021", 8, 1, "Thu Nov 25 2021"),
+ ("Wed Nov 24 2021", 11, 1, "Thu Nov 24 2022"),
+ ("24-Nov-2021", 0, 1, "25-Nov-2021"),
+ ("24-Nov-2021", 3, 1, "24-Dec-2021"),
+ ("24-Nov-2021", 7, 1, "24-Nov-2022"),
+ ("2021 Nov 24", 0, 1, "2022 Nov 24"),
+ ("2021 Nov 24", 5, 1, "2021 Dec 24"),
+ ("2021 Nov 24", 9, 1, "2021 Nov 25"),
+ ("Nov 24, 2021", 0, 1, "Dec 24, 2021"),
+ ("Nov 24, 2021", 4, 1, "Nov 25, 2021"),
+ ("Nov 24, 2021", 8, 1, "Nov 24, 2022"),
+ ("7:21:53 am", 0, 1, "8:21:53 am"),
+ ("7:21:53 am", 3, 1, "7:22:53 am"),
+ ("7:21:53 am", 5, 1, "7:21:54 am"),
+ ("7:21:53 am", 8, 1, "7:21:53 pm"),
+ ("7:21:53 AM", 0, 1, "8:21:53 AM"),
+ ("7:21:53 AM", 3, 1, "7:22:53 AM"),
+ ("7:21:53 AM", 5, 1, "7:21:54 AM"),
+ ("7:21:53 AM", 8, 1, "7:21:53 PM"),
+ ("7:21 am", 0, 1, "8:21 am"),
+ ("7:21 am", 3, 1, "7:22 am"),
+ ("7:21 am", 5, 1, "7:21 pm"),
+ ("7:21 AM", 0, 1, "8:21 AM"),
+ ("7:21 AM", 3, 1, "7:22 AM"),
+ ("7:21 AM", 5, 1, "7:21 PM"),
+ ("23:24:23", 1, 1, "00:24:23"),
+ ("23:24:23", 3, 1, "23:25:23"),
+ ("23:24:23", 6, 1, "23:24:24"),
+ ("23:24", 1, 1, "00:24"),
+ ("23:24", 3, 1, "23:25"),
+ ];
+
+ for (original, cursor, amount, expected) in tests {
+ let rope = Rope::from_str(original);
+ let range = Range::new(cursor, cursor + 1);
+ assert_eq!(
+ DateTimeIncrementor::from_range(rope.slice(..), range)
+ .unwrap()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
+ );
+ }
+ }
+
+ #[test]
+ fn test_invalid_date_times() {
+ let tests = [
+ "0000-00-00",
+ "1980-2-21",
+ "1980-12-1",
+ "12345",
+ "2020-02-30",
+ "1999-12-32",
+ "19-12-32",
+ "1-2-3",
+ "0000/00/00",
+ "1980/2/21",
+ "1980/12/1",
+ "12345",
+ "2020/02/30",
+ "1999/12/32",
+ "19/12/32",
+ "1/2/3",
+ "123:456:789",
+ "11:61",
+ "2021-55-12 08:12:54",
+ ];
+
+ for invalid in tests {
+ let rope = Rope::from_str(invalid);
+ let range = Range::new(0, 1);
+
+ assert_eq!(DateTimeIncrementor::from_range(rope.slice(..), range), None)
+ }
+ }
+}
diff --git a/helix-core/src/increment/mod.rs b/helix-core/src/increment/mod.rs
new file mode 100644
index 00000000..f5945774
--- /dev/null
+++ b/helix-core/src/increment/mod.rs
@@ -0,0 +1,8 @@
+pub mod date_time;
+pub mod number;
+
+use crate::{Range, Tendril};
+
+pub trait Increment {
+ fn increment(&self, amount: i64) -> (Range, Tendril);
+}
diff --git a/helix-core/src/numbers.rs b/helix-core/src/increment/number.rs
index e9f3c898..57171f67 100644
--- a/helix-core/src/numbers.rs
+++ b/helix-core/src/increment/number.rs
@@ -2,6 +2,8 @@ use std::borrow::Cow;
use ropey::RopeSlice;
+use super::Increment;
+
use crate::{
textobject::{textobject_word, TextObject},
Range, Tendril,
@@ -9,9 +11,9 @@ use crate::{
#[derive(Debug, PartialEq, Eq)]
pub struct NumberIncrementor<'a> {
- pub range: Range,
- pub value: i64,
- pub radix: u32,
+ value: i64,
+ radix: u32,
+ range: Range,
text: RopeSlice<'a>,
}
@@ -71,9 +73,10 @@ impl<'a> NumberIncrementor<'a> {
text,
})
}
+}
- /// Add `amount` to the number and return the formatted text.
- pub fn incremented_text(&self, amount: i64) -> Tendril {
+impl<'a> Increment for NumberIncrementor<'a> {
+ fn increment(&self, amount: i64) -> (Range, Tendril) {
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
let old_length = old_text.len();
let new_value = self.value.wrapping_add(amount);
@@ -144,7 +147,7 @@ impl<'a> NumberIncrementor<'a> {
}
}
- new_text.into()
+ (self.range, new_text.into())
}
}
@@ -366,8 +369,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
@@ -392,8 +396,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
@@ -419,8 +424,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
@@ -464,8 +470,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
@@ -491,8 +498,9 @@ mod test {
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
- .incremented_text(amount),
- expected.into()
+ .increment(amount)
+ .1,
+ Tendril::from(expected)
);
}
}
diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs
index 8ccc0120..5d20edc1 100644
--- a/helix-core/src/indent.rs
+++ b/helix-core/src/indent.rs
@@ -1,6 +1,5 @@
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
- find_first_non_whitespace_char,
syntax::{IndentQuery, LanguageConfiguration, Syntax},
tree_sitter::Node,
Rope, RopeSlice,
@@ -174,8 +173,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
/// To determine indentation of a newly inserted line, figure out the indentation at the last col
/// of the previous line.
-#[allow(dead_code)]
-fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
+pub fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
let mut len = 0;
for ch in line.chars() {
match ch {
@@ -207,10 +205,15 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
Some(node)
}
-fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
- // NOTE: can't use contains() on query because of comparing Vec<String> and &str
- // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
-
+/// Calculate the indentation at a given treesitter node.
+/// If newline is false, then any "indent" nodes on the line are ignored ("outdent" still applies).
+/// This is because the indentation is only increased starting at the second line of the node.
+fn calculate_indentation(
+ query: &IndentQuery,
+ node: Option<Node>,
+ line: usize,
+ newline: bool,
+) -> usize {
let mut increment: isize = 0;
let mut node = match node {
@@ -218,70 +221,45 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
None => return 0,
};
- let mut prev_start = node.start_position().row;
-
- // if we're calculating indentation for a brand new line then the current node will become the
- // parent node. We need to take it's indentation level into account too.
- let node_kind = node.kind();
- if newline && query.indent.contains(node_kind) {
- increment += 1;
- }
-
- while let Some(parent) = node.parent() {
- let parent_kind = parent.kind();
- let start = parent.start_position().row;
-
- // detect deeply nested indents in the same line
- // .map(|a| { <-- ({ is two scopes
- // let len = 1; <-- indents one level
- // }) <-- }) is two scopes
- let starts_same_line = start == prev_start;
-
- if query.outdent.contains(node.kind()) && !starts_same_line {
- // we outdent by skipping the rules for the current level and jumping up
- // node = parent;
- increment -= 1;
- // continue;
+ let mut current_line = line;
+ let mut consider_indent = newline;
+ let mut increment_from_line: isize = 0;
+
+ loop {
+ let node_kind = node.kind();
+ let start = node.start_position().row;
+ if current_line != start {
+ // Indent/dedent by at most one per line:
+ // .map(|a| { <-- ({ is two scopes
+ // let len = 1; <-- indents one level
+ // }) <-- }) is two scopes
+ if consider_indent || increment_from_line < 0 {
+ increment += increment_from_line.signum();
+ }
+ increment_from_line = 0;
+ current_line = start;
+ consider_indent = true;
}
- if query.indent.contains(parent_kind) // && not_first_or_last_sibling
- && !starts_same_line
- {
- // println!("is_scope {}", parent_kind);
- prev_start = start;
- increment += 1
+ if query.outdent.contains(node_kind) {
+ increment_from_line -= 1;
+ }
+ if query.indent.contains(node_kind) {
+ increment_from_line += 1;
}
- // if last_scope && increment > 0 && ...{ ignore }
-
- node = parent;
+ if let Some(parent) = node.parent() {
+ node = parent;
+ } else {
+ break;
+ }
+ }
+ if consider_indent || increment_from_line < 0 {
+ increment += increment_from_line.signum();
}
-
increment.max(0) as usize
}
-#[allow(dead_code)]
-fn suggested_indent_for_line(
- language_config: &LanguageConfiguration,
- syntax: Option<&Syntax>,
- text: RopeSlice,
- line_num: usize,
- _tab_width: usize,
-) -> usize {
- if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
- return suggested_indent_for_pos(
- Some(language_config),
- syntax,
- text,
- start + text.line_to_char(line_num),
- false,
- );
- };
-
- // if the line is blank, indent should be zero
- 0
-}
-
// TODO: two usecases: if we are triggering this for a new, blank line:
// - it should return 0 when mass indenting stuff
// - it should look up the wrapper node and count it too when we press o/O
@@ -290,23 +268,20 @@ pub fn suggested_indent_for_pos(
syntax: Option<&Syntax>,
text: RopeSlice,
pos: usize,
+ line: usize,
new_line: bool,
-) -> usize {
+) -> Option<usize> {
if let (Some(query), Some(syntax)) = (
language_config.and_then(|config| config.indent_query()),
syntax,
) {
let byte_start = text.char_to_byte(pos);
let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
-
- // let config = load indentation query config from Syntax(should contain language_config)
-
// TODO: special case for comments
// TODO: if preserve_leading_whitespace
- calculate_indentation(query, node, new_line)
+ Some(calculate_indentation(query, node, line, new_line))
} else {
- // TODO: heuristics for non-tree sitter grammars
- 0
+ None
}
}
@@ -438,7 +413,8 @@ where
",
);
- let doc = Rope::from(doc);
+ let doc = doc;
+ use crate::diagnostic::Severity;
use crate::syntax::{
Configuration, IndentationConfiguration, LanguageConfiguration, Loader,
};
@@ -456,6 +432,8 @@ where
roots: vec![],
comment_token: None,
auto_format: false,
+ diagnostic_severity: Severity::Warning,
+ tree_sitter_library: None,
language_server: None,
indent: Some(IndentationConfiguration {
tab_width: 4,
@@ -474,20 +452,29 @@ where
let language_config = loader.language_config_for_scope("source.rust").unwrap();
let highlight_config = language_config.highlight_config(&[]).unwrap();
- let syntax = Syntax::new(&doc, highlight_config.clone());
+ let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader));
let text = doc.slice(..);
let tab_width = 4;
for i in 0..doc.len_lines() {
let line = text.line(i);
- let indent = indent_level_for_line(line, tab_width);
- assert_eq!(
- suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
- indent,
- "line {}: {}",
- i,
- line
- );
+ if let Some(pos) = crate::find_first_non_whitespace_char(line) {
+ let indent = indent_level_for_line(line, tab_width);
+ assert_eq!(
+ suggested_indent_for_pos(
+ Some(&language_config),
+ Some(&syntax),
+ text,
+ text.line_to_char(i) + pos,
+ i,
+ false
+ ),
+ Some(indent),
+ "line {}: \"{}\"",
+ i,
+ line
+ );
+ }
}
}
}
diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs
index 7d790406..fa8566ab 100644
--- a/helix-core/src/lib.rs
+++ b/helix-core/src/lib.rs
@@ -1,3 +1,5 @@
+pub use encoding_rs as encoding;
+
pub mod auto_pairs;
pub mod chars;
pub mod comment;
@@ -5,18 +7,19 @@ pub mod diagnostic;
pub mod diff;
pub mod graphemes;
pub mod history;
+pub mod increment;
pub mod indent;
pub mod line_ending;
pub mod macros;
pub mod match_brackets;
pub mod movement;
-pub mod numbers;
pub mod object;
pub mod path;
mod position;
pub mod register;
pub mod search;
pub mod selection;
+pub mod shellwords;
mod state;
pub mod surround;
pub mod syntax;
@@ -36,8 +39,14 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}
-/// Find `.git` root.
-pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
+/// Find project root.
+///
+/// Order of detection:
+/// * Top-most folder containing a root marker in current git repository
+/// * Git repostory root if no marker detected
+/// * Top-most folder containing a root marker if not git repository detected
+/// * Current working directory as fallback
+pub fn find_root(root: Option<&str>, root_markers: &[String]) -> Option<std::path::PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let root = match root {
@@ -49,16 +58,30 @@ pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
current_dir.join(root)
}
}
- None => current_dir,
+ None => current_dir.clone(),
};
+ let mut top_marker = None;
for ancestor in root.ancestors() {
- // TODO: also use defined roots if git isn't found
+ for marker in root_markers {
+ if ancestor.join(marker).exists() {
+ top_marker = Some(ancestor);
+ break;
+ }
+ }
+ // don't go higher than repo
if ancestor.join(".git").is_dir() {
- return Some(ancestor.to_path_buf());
+ // Use workspace if detected from marker
+ return Some(top_marker.unwrap_or(ancestor).to_path_buf());
}
}
- None
+
+ // In absence of git repo, use workspace if detected
+ if top_marker.is_some() {
+ top_marker.map(|a| a.to_path_buf())
+ } else {
+ Some(current_dir)
+ }
}
pub fn runtime_dir() -> std::path::PathBuf {
@@ -158,7 +181,7 @@ mod merge_toml_tests {
";
let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
- .expect("Couldn't parse built-in langauges config");
+ .expect("Couldn't parse built-in languages config");
let user: Value = toml::from_str(USER).unwrap();
let merged = merge_toml_values(base, user);
@@ -189,7 +212,10 @@ use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
pub use ropey::{Rope, RopeBuilder, RopeSlice};
-pub use tendril::StrTendril as Tendril;
+// pub use tendril::StrTendril as Tendril;
+pub use smartstring::SmartString;
+
+pub type Tendril = SmartString<smartstring::LazyCompact>;
#[doc(inline)]
pub use {regex, tree_sitter};
diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs
index 3541305c..8eb426e1 100644
--- a/helix-core/src/line_ending.rs
+++ b/helix-core/src/line_ending.rs
@@ -250,7 +250,7 @@ mod line_ending_tests {
assert_eq!(get_line_ending_of_str(&text[..6]), Some(LineEnding::CR));
assert_eq!(get_line_ending_of_str(&text[..12]), Some(LineEnding::LF));
assert_eq!(get_line_ending_of_str(&text[..17]), Some(LineEnding::Crlf));
- assert_eq!(get_line_ending_of_str(&text[..]), None);
+ assert_eq!(get_line_ending_of_str(text), None);
}
#[test]
diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs
index cd554005..0189dedd 100644
--- a/helix-core/src/match_brackets.rs
+++ b/helix-core/src/match_brackets.rs
@@ -11,7 +11,7 @@ const PAIRS: &[(char, char)] = &[
('\"', '\"'),
];
-// limit matching pairs to only ( ) { } [ ] < >
+// limit matching pairs to only ( ) { } [ ] < > ' ' " "
// Returns the position of the matching bracket under cursor.
//
diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs
index 01a8f890..47fe6827 100644
--- a/helix-core/src/movement.rs
+++ b/helix-core/src/movement.rs
@@ -307,8 +307,6 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
#[cfg(test)]
mod test {
- use std::array::{self, IntoIter};
-
use ropey::Rope;
use super::*;
@@ -360,7 +358,7 @@ mod test {
((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line
];
- for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
+ for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
}
@@ -374,7 +372,7 @@ mod test {
let mut range = Range::point(position);
- let moves_and_expected_coordinates = IntoIter::new([
+ let moves_and_expected_coordinates = [
((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n...
((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n...
((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n...
@@ -384,7 +382,7 @@ mod test {
((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n...
((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n|
- ]);
+ ];
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move);
@@ -402,11 +400,11 @@ mod test {
let mut range = Range::point(position);
let original_anchor = range.anchor;
- let moves = IntoIter::new([
+ let moves = [
(Direction::Forward, 1usize),
(Direction::Forward, 5usize),
(Direction::Backward, 3usize),
- ]);
+ ];
for (direction, amount) in moves {
range = move_horizontally(slice, range, direction, amount, Movement::Extend);
@@ -420,7 +418,7 @@ mod test {
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into(), true);
let mut range = Range::point(position);
- let moves_and_expected_coordinates = IntoIter::new([
+ let moves_and_expected_coordinates = [
((Direction::Forward, 1usize), (1, 0)),
((Direction::Forward, 2usize), (3, 0)),
((Direction::Forward, 1usize), (4, 0)),
@@ -430,7 +428,7 @@ mod test {
((Direction::Backward, 0usize), (4, 0)),
((Direction::Forward, 5), (5, 0)),
((Direction::Forward, 999usize), (5, 0)),
- ]);
+ ];
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_vertically(slice, range, direction, amount, Movement::Move);
@@ -450,7 +448,7 @@ mod test {
H,
V,
}
- let moves_and_expected_coordinates = IntoIter::new([
+ let moves_and_expected_coordinates = [
// Places cursor at the end of line
((Axis::H, Direction::Forward, 8usize), (0, 8)),
// First descent preserves column as the target line is wider
@@ -463,7 +461,7 @@ mod test {
((Axis::V, Direction::Backward, 999usize), (0, 8)),
((Axis::V, Direction::Forward, 4usize), (4, 8)),
((Axis::V, Direction::Forward, 999usize), (5, 0)),
- ]);
+ ];
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis {
@@ -489,7 +487,7 @@ mod test {
H,
V,
}
- let moves_and_expected_coordinates = IntoIter::new([
+ let moves_and_expected_coordinates = [
// Places cursor at the fourth kana.
((Axis::H, Direction::Forward, 4), (0, 4)),
// Descent places cursor at the 4th character.
@@ -498,7 +496,7 @@ mod test {
((Axis::H, Direction::Backward, 1usize), (1, 3)),
// Jumping back up 1 line.
((Axis::V, Direction::Backward, 1usize), (0, 3)),
- ]);
+ ];
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis {
@@ -530,7 +528,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_start_of_next_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic forward motion stops at the first space",
vec![(1, Range::new(0, 0), Range::new(0, 6))]),
(" Starting from a boundary advances the anchor",
@@ -604,7 +602,7 @@ mod test {
vec![
(1, Range::new(0, 0), Range::new(0, 6)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -616,7 +614,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_start_of_next_long_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic forward motion stops at the first space",
vec![(1, Range::new(0, 0), Range::new(0, 6))]),
(" Starting from a boundary advances the anchor",
@@ -688,7 +686,7 @@ mod test {
vec![
(1, Range::new(0, 0), Range::new(0, 8)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -700,7 +698,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_start_of_previous_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(4, 0))]),
@@ -773,7 +771,7 @@ mod test {
vec![
(1, Range::new(0, 6), Range::new(6, 0)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -785,7 +783,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_start_of_previous_long_words() {
- let tests = array::IntoIter::new([
+ let tests = [
(
"Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(4, 0))],
@@ -870,7 +868,7 @@ mod test {
vec![
(1, Range::new(0, 8), Range::new(8, 0)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -882,7 +880,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_end_of_next_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic forward motion from the start of a word to the end of it",
vec![(1, Range::new(0, 0), Range::new(0, 5))]),
("Basic forward motion from the end of a word to the end of the next",
@@ -954,7 +952,7 @@ mod test {
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -966,7 +964,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_end_of_previous_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic backward motion from the middle of a word",
vec![(1, Range::new(9, 9), Range::new(10, 5))]),
("Starting from after boundary retreats the anchor",
@@ -1036,7 +1034,7 @@ mod test {
vec![
(1, Range::new(0, 10), Range::new(10, 4)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
@@ -1048,7 +1046,7 @@ mod test {
#[test]
fn test_behaviour_when_moving_to_end_of_next_long_words() {
- let tests = array::IntoIter::new([
+ let tests = [
("Basic forward motion from the start of a word to the end of it",
vec![(1, Range::new(0, 0), Range::new(0, 5))]),
("Basic forward motion from the end of a word to the end of the next",
@@ -1118,7 +1116,7 @@ mod test {
vec![
(1, Range::new(0, 0), Range::new(0, 7)),
]),
- ]);
+ ];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs
index 717c5994..b06f4144 100644
--- a/helix-core/src/object.rs
+++ b/helix-core/src/object.rs
@@ -1,31 +1,72 @@
use crate::{Range, RopeSlice, Selection, Syntax};
+use tree_sitter::Node;
-// TODO: to contract_selection we'd need to store the previous ranges before expand.
-// Maybe just contract to the first child node?
-pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
+pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+ select_node_impl(syntax, text, selection, |descendant, from, to| {
+ if descendant.start_byte() == from && descendant.end_byte() == to {
+ descendant.parent()
+ } else {
+ Some(descendant)
+ }
+ })
+}
+
+pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
+ select_node_impl(syntax, text, selection, |descendant, _from, _to| {
+ descendant.child(0).or(Some(descendant))
+ })
+}
+
+pub fn select_sibling<F>(
+ syntax: &Syntax,
+ text: RopeSlice,
+ selection: Selection,
+ sibling_fn: &F,
+) -> Selection
+where
+ F: Fn(Node) -> Option<Node>,
+{
+ select_node_impl(syntax, text, selection, |descendant, _from, _to| {
+ find_sibling_recursive(descendant, sibling_fn)
+ })
+}
+
+fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node>
+where
+ F: Fn(Node) -> Option<Node>,
+{
+ sibling_fn(node).or_else(|| {
+ node.parent()
+ .and_then(|node| find_sibling_recursive(node, sibling_fn))
+ })
+}
+
+fn select_node_impl<F>(
+ syntax: &Syntax,
+ text: RopeSlice,
+ selection: Selection,
+ select_fn: F,
+) -> Selection
+where
+ F: Fn(Node, usize, usize) -> Option<Node>,
+{
let tree = syntax.tree();
- selection.clone().transform(|range| {
+ selection.transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());
- // find parent of a descendant that matches the range
- let parent = match tree
+ let node = match tree
.root_node()
.descendant_for_byte_range(from, to)
- .and_then(|node| {
- if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) {
- node.parent()
- } else {
- Some(node)
- }
- }) {
- Some(parent) => parent,
+ .and_then(|node| select_fn(node, from, to))
+ {
+ Some(node) => node,
None => return range,
};
- let from = text.byte_to_char(parent.start_byte());
- let to = text.byte_to_char(parent.end_byte());
+ let from = text.byte_to_char(node.start_byte());
+ let to = text.byte_to_char(node.end_byte());
if range.head < range.anchor {
Range::new(to, from)
diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs
index c6018ce6..93362c77 100644
--- a/helix-core/src/position.rs
+++ b/helix-core/src/position.rs
@@ -109,7 +109,10 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
/// TODO: this should be changed to work in terms of visual row/column, not
/// graphemes.
pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending: bool) -> usize {
- let Position { row, col } = coords;
+ let Position { mut row, col } = coords;
+ if limit_before_line_ending {
+ row = row.min(text.len_lines() - 1);
+ };
let line_start = text.line_to_char(row);
let line_end = if limit_before_line_ending {
line_end_char_index(&text, row)
@@ -290,5 +293,12 @@ mod test {
assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
+
+ // Test out of bounds.
+ let text = Rope::new();
+ let slice = text.slice(..);
+ assert_eq!(pos_at_coords(slice, (10, 0).into(), true), 0);
+ assert_eq!(pos_at_coords(slice, (0, 10).into(), true), 0);
+ assert_eq!(pos_at_coords(slice, (10, 10).into(), true), 0);
}
}
diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs
index b9eb497d..b39e4034 100644
--- a/helix-core/src/register.rs
+++ b/helix-core/src/register.rs
@@ -68,4 +68,8 @@ impl Registers {
pub fn read(&self, name: char) -> Option<&[String]> {
self.get(name).map(|reg| reg.read())
}
+
+ pub fn inner(&self) -> &HashMap<char, Register> {
+ &self.inner
+ }
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index 116a1c7c..c6eceb4b 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -7,6 +7,7 @@ use crate::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary,
},
+ movement::Direction,
Assoc, ChangeSet, RopeSlice,
};
use smallvec::{smallvec, SmallVec};
@@ -82,6 +83,13 @@ impl Range {
std::cmp::max(self.anchor, self.head)
}
+ /// Total length of the range.
+ #[inline]
+ #[must_use]
+ pub fn len(&self) -> usize {
+ self.to() - self.from()
+ }
+
/// The (inclusive) range of lines that the range overlaps.
#[inline]
#[must_use]
@@ -102,6 +110,27 @@ impl Range {
self.anchor == self.head
}
+ /// `Direction::Backward` when head < anchor.
+ /// `Direction::Backward` otherwise.
+ #[inline]
+ #[must_use]
+ pub fn direction(&self) -> Direction {
+ if self.head < self.anchor {
+ Direction::Backward
+ } else {
+ Direction::Forward
+ }
+ }
+
+ // flips the direction of the selection
+ pub fn flip(&self) -> Self {
+ Self {
+ anchor: self.head,
+ head: self.anchor,
+ horiz: self.horiz,
+ }
+ }
+
/// Check two ranges for overlap.
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
@@ -111,6 +140,11 @@ impl Range {
self.from() == other.from() || (self.to() > other.from() && other.to() > self.from())
}
+ #[inline]
+ pub fn contains_range(&self, other: &Self) -> bool {
+ self.from() <= other.from() && self.to() >= other.to()
+ }
+
pub fn contains(&self, pos: usize) -> bool {
self.from() <= pos && pos < self.to()
}
@@ -515,6 +549,39 @@ impl Selection {
pub fn len(&self) -> usize {
self.ranges.len()
}
+
+ // returns true if self ⊇ other
+ pub fn contains(&self, other: &Selection) -> bool {
+ // can't contain other if it is larger
+ if other.len() > self.len() {
+ return false;
+ }
+
+ let (mut iter_self, mut iter_other) = (self.iter(), other.iter());
+ let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next());
+
+ loop {
+ match (ele_self, ele_other) {
+ (Some(ra), Some(rb)) => {
+ if !ra.contains_range(rb) {
+ // `self` doesn't contain next element from `other`, advance `self`, we need to match all from `other`
+ ele_self = iter_self.next();
+ } else {
+ // matched element from `other`, advance `other`
+ ele_other = iter_other.next();
+ };
+ }
+ (None, Some(_)) => {
+ // exhausted `self`, we can't match the reminder of `other`
+ return false;
+ }
+ (_, None) => {
+ // no elements from `other` left to match, `self` contains `other`
+ return true;
+ }
+ }
+ }
+ }
}
impl<'a> IntoIterator for &'a Selection {
@@ -699,16 +766,16 @@ mod test {
fn test_contains() {
let range = Range::new(10, 12);
- assert_eq!(range.contains(9), false);
- assert_eq!(range.contains(10), true);
- assert_eq!(range.contains(11), true);
- assert_eq!(range.contains(12), false);
- assert_eq!(range.contains(13), false);
+ assert!(!range.contains(9));
+ assert!(range.contains(10));
+ assert!(range.contains(11));
+ assert!(!range.contains(12));
+ assert!(!range.contains(13));
let range = Range::new(9, 6);
- assert_eq!(range.contains(9), false);
- assert_eq!(range.contains(7), true);
- assert_eq!(range.contains(6), true);
+ assert!(!range.contains(9));
+ assert!(range.contains(7));
+ assert!(range.contains(6));
}
#[test]
@@ -953,4 +1020,30 @@ mod test {
&["", "abcd", "efg", "rs", "xyz"]
);
}
+ #[test]
+ fn test_selection_contains() {
+ fn contains(a: Vec<(usize, usize)>, b: Vec<(usize, usize)>) -> bool {
+ let sela = Selection::new(a.iter().map(|a| Range::new(a.0, a.1)).collect(), 0);
+ let selb = Selection::new(b.iter().map(|b| Range::new(b.0, b.1)).collect(), 0);
+ sela.contains(&selb)
+ }
+
+ // exact match
+ assert!(contains(vec!((1, 1)), vec!((1, 1))));
+
+ // larger set contains smaller
+ assert!(contains(vec!((1, 1), (2, 2), (3, 3)), vec!((2, 2))));
+
+ // multiple matches
+ assert!(contains(vec!((1, 1), (2, 2)), vec!((1, 1), (2, 2))));
+
+ // smaller set can't contain bigger
+ assert!(!contains(vec!((1, 1)), vec!((1, 1), (2, 2))));
+
+ assert!(contains(
+ vec!((1, 1), (2, 4), (5, 6), (7, 9), (10, 13)),
+ vec!((3, 4), (7, 9))
+ ));
+ assert!(!contains(vec!((1, 1), (5, 6)), vec!((1, 6))));
+ }
}
diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs
new file mode 100644
index 00000000..13f6f3e9
--- /dev/null
+++ b/helix-core/src/shellwords.rs
@@ -0,0 +1,164 @@
+use std::borrow::Cow;
+
+/// Get the vec of escaped / quoted / doublequoted filenames from the input str
+pub fn shellwords(input: &str) -> Vec<Cow<'_, str>> {
+ enum State {
+ Normal,
+ NormalEscaped,
+ Quoted,
+ QuoteEscaped,
+ Dquoted,
+ DquoteEscaped,
+ }
+
+ use State::*;
+
+ let mut state = Normal;
+ let mut args: Vec<Cow<str>> = Vec::new();
+ let mut escaped = String::with_capacity(input.len());
+
+ let mut start = 0;
+ let mut end = 0;
+
+ for (i, c) in input.char_indices() {
+ state = match state {
+ Normal => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ NormalEscaped
+ }
+ '"' => {
+ end = i;
+ Dquoted
+ }
+ '\'' => {
+ end = i;
+ Quoted
+ }
+ c if c.is_ascii_whitespace() => {
+ end = i;
+ Normal
+ }
+ _ => Normal,
+ },
+ NormalEscaped => Normal,
+ Quoted => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ QuoteEscaped
+ }
+ '\'' => {
+ end = i;
+ Normal
+ }
+ _ => Quoted,
+ },
+ QuoteEscaped => Quoted,
+ Dquoted => match c {
+ '\\' => {
+ escaped.push_str(&input[start..i]);
+ start = i + 1;
+ DquoteEscaped
+ }
+ '"' => {
+ end = i;
+ Normal
+ }
+ _ => Dquoted,
+ },
+ DquoteEscaped => Dquoted,
+ };
+
+ if i >= input.len() - 1 && end == 0 {
+ end = i + 1;
+ }
+
+ if end > 0 {
+ let esc_trim = escaped.trim();
+ let inp = &input[start..end];
+
+ if !(esc_trim.is_empty() && inp.trim().is_empty()) {
+ if esc_trim.is_empty() {
+ args.push(inp.into());
+ } else {
+ args.push([escaped, inp.into()].concat().into());
+ escaped = "".to_string();
+ }
+ }
+ start = i + 1;
+ end = 0;
+ }
+ }
+ args
+}
+
+#[cfg(test)]
+mod test {
+ use super::*;
+
+ #[test]
+ fn test_normal() {
+ let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
+ let result = shellwords(input);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twó"),
+ Cow::from("wörds"),
+ Cow::from(r#"three "with escaping\"#),
+ ];
+ // TODO test is_owned and is_borrowed, once they get stabilized.
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_quoted() {
+ let quoted =
+ r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
+ let result = shellwords(quoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twó wörds"),
+ Cow::from(r#"three' "with escaping\"#),
+ Cow::from("quote incomplete"),
+ ];
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_dquoted() {
+ let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
+ let result = shellwords(dquoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twó wörds"),
+ Cow::from(r#"three' "with escaping\"#),
+ Cow::from("dquote incomplete"),
+ ];
+ assert_eq!(expected, result);
+ }
+
+ #[test]
+ fn test_mixed() {
+ let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
+ let result = shellwords(dquoted);
+ let expected = vec![
+ Cow::from(":o"),
+ Cow::from("single_word"),
+ Cow::from("twó wörds"),
+ Cow::from("three' \"with escaping\\"),
+ Cow::from("no space before"),
+ Cow::from("and after"),
+ Cow::from("$#%^@"),
+ Cow::from("%^&(%^"),
+ Cow::from(")(*&^%"),
+ Cow::from(r#"a\\b"#),
+ //last ' just changes to quoted but since we dont have anything after it, it should be ignored
+ ];
+ assert_eq!(expected, result);
+ }
+}
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
index b53b0a78..58eb23cf 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -172,6 +172,7 @@ mod test {
use ropey::Rope;
use smallvec::SmallVec;
+ #[allow(clippy::type_complexity)]
fn check_find_nth_pair_pos(
text: &str,
cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs
index f1c399d2..a5c5e498 100644
--- a/helix-core/src/syntax.rs
+++ b/helix-core/src/syntax.rs
@@ -1,5 +1,6 @@
use crate::{
chars::char_is_line_ending,
+ diagnostic::Severity,
regex::Regex,
transaction::{ChangeSet, Operation},
Rope, RopeSlice, Tendril,
@@ -7,12 +8,13 @@ use crate::{
pub use helix_syntax::get_language;
-use arc_swap::ArcSwap;
+use arc_swap::{ArcSwap, Guard};
+use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{
borrow::Cow,
cell::RefCell,
- collections::{HashMap, HashSet},
+ collections::{HashMap, HashSet, VecDeque},
fmt,
path::Path,
sync::Arc,
@@ -50,7 +52,7 @@ pub struct Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
- pub language_id: String,
+ pub language_id: String, // c-sharp, rust
pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
#[serde(default)]
@@ -63,6 +65,10 @@ pub struct LanguageConfiguration {
#[serde(default)]
pub auto_format: bool,
+ #[serde(default)]
+ pub diagnostic_severity: Severity,
+
+ pub tree_sitter_library: Option<String>, // tree-sitter library name, defaults to language_id
// content_regex
#[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
@@ -92,6 +98,7 @@ pub struct LanguageServerConfiguration {
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
+ pub language_id: Option<String>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
@@ -245,20 +252,22 @@ impl LanguageConfiguration {
if highlights_query.is_empty() {
None
} else {
- let language = get_language(&crate::RUNTIME_DIR, &self.language_id)
- .map_err(|e| log::info!("{}", e))
- .ok()?;
+ let language = get_language(
+ &crate::RUNTIME_DIR,
+ self.tree_sitter_library
+ .as_deref()
+ .unwrap_or(&self.language_id),
+ )
+ .map_err(|e| log::info!("{}", e))
+ .ok()?;
let config = HighlightConfiguration::new(
language,
&highlights_query,
&injections_query,
&locals_query,
- );
+ )
+ .unwrap(); // TODO: avoid panic
- let config = match config {
- Ok(config) => config,
- Err(err) => panic!("{}", err),
- }; // TODO: avoid panic
config.configure(scopes);
Some(Arc::new(config))
}
@@ -308,12 +317,16 @@ impl LanguageConfiguration {
}
}
+// Expose loader as Lazy<> global since it's always static?
+
#[derive(Debug)]
pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_shebang: HashMap<String, usize>,
+
+ scopes: ArcSwap<Vec<String>>,
}
impl Loader {
@@ -322,6 +335,7 @@ impl Loader {
language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
+ scopes: ArcSwap::from_pointee(Vec::new()),
};
for config in config.language {
@@ -366,8 +380,9 @@ impl Loader {
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0));
- static SHEBANG_REGEX: Lazy<Regex> =
- Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
+ static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
+ Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap()
+ });
let configuration_id = SHEBANG_REGEX
.captures(&line)
.and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
@@ -406,8 +421,22 @@ impl Loader {
}
None
}
- pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
- self.language_configs.iter()
+
+ pub fn set_scopes(&self, scopes: Vec<String>) {
+ self.scopes.store(Arc::new(scopes));
+
+ // Reconfigure existing grammars
+ for config in self
+ .language_configs
+ .iter()
+ .filter(|cfg| cfg.is_highlight_initialized())
+ {
+ config.reconfigure(&self.scopes());
+ }
+ }
+
+ pub fn scopes(&self) -> Guard<Arc<Vec<String>>> {
+ self.scopes.load()
}
}
@@ -416,12 +445,6 @@ pub struct TsParser {
cursors: Vec<QueryCursor>,
}
-impl fmt::Debug for TsParser {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- f.debug_struct("TsParser").finish()
- }
-}
-
// could also just use a pool, or a single instance?
thread_local! {
pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
@@ -432,9 +455,9 @@ thread_local! {
#[derive(Debug)]
pub struct Syntax {
- config: Arc<HighlightConfiguration>,
-
- root_layer: LanguageLayer,
+ layers: HopSlotMap<LayerId, LanguageLayer>,
+ root: LayerId,
+ loader: Arc<Loader>,
}
fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> {
@@ -444,38 +467,34 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
}
impl Syntax {
- // buffer, grammar, config, grammars, sync_timeout?
- pub fn new(
- /*language: Lang,*/ source: &Rope,
- config: Arc<HighlightConfiguration>,
- ) -> Self {
- let root_layer = LanguageLayer { tree: None };
+ pub fn new(source: &Rope, config: Arc<HighlightConfiguration>, loader: Arc<Loader>) -> Self {
+ let root_layer = LanguageLayer {
+ tree: None,
+ config,
+ depth: 0,
+ ranges: vec![Range {
+ start_byte: 0,
+ end_byte: usize::MAX,
+ start_point: Point::new(0, 0),
+ end_point: Point::new(usize::MAX, usize::MAX),
+ }],
+ };
- // track markers of injections
// track scope_descriptor: a Vec of scopes for item in tree
+ let mut layers = HopSlotMap::default();
+ let root = layers.insert(root_layer);
+
let mut syntax = Self {
- // grammar,
- config,
- root_layer,
+ root,
+ layers,
+ loader,
};
- // update root layer
- PARSER.with(|ts_parser| {
- // TODO: handle the returned `Result` properly.
- let _ = syntax.root_layer.parse(
- &mut ts_parser.borrow_mut(),
- &syntax.config,
- source,
- 0,
- vec![Range {
- start_byte: 0,
- end_byte: usize::MAX,
- start_point: Point::new(0, 0),
- end_point: Point::new(usize::MAX, usize::MAX),
- }],
- );
- });
+ syntax
+ .update(source, source, &ChangeSet::new(source))
+ .unwrap();
+
syntax
}
@@ -485,32 +504,255 @@ impl Syntax {
source: &Rope,
changeset: &ChangeSet,
) -> Result<(), Error> {
+ let mut queue = VecDeque::new();
+ queue.push_back(self.root);
+
+ let scopes = self.loader.scopes.load();
+ let injection_callback = |language: &str| {
+ self.loader
+ .language_configuration_for_injection_string(language)
+ .and_then(|language_config| language_config.highlight_config(&scopes))
+ };
+
+ // Convert the changeset into tree sitter edits.
+ let edits = generate_edits(old_source, changeset);
+
+ // Use the edits to update all layers markers
+ if !edits.is_empty() {
+ fn point_add(a: Point, b: Point) -> Point {
+ if b.row > 0 {
+ Point::new(a.row.saturating_add(b.row), b.column)
+ } else {
+ Point::new(0, a.column.saturating_add(b.column))
+ }
+ }
+ fn point_sub(a: Point, b: Point) -> Point {
+ if a.row > b.row {
+ Point::new(a.row.saturating_sub(b.row), a.column)
+ } else {
+ Point::new(0, a.column.saturating_sub(b.column))
+ }
+ }
+
+ for layer in &mut self.layers.values_mut() {
+ // The root layer always covers the whole range (0..usize::MAX)
+ if layer.depth == 0 {
+ continue;
+ }
+
+ for range in &mut layer.ranges {
+ // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720
+ for edit in edits.iter().rev() {
+ let is_pure_insertion = edit.old_end_byte == edit.start_byte;
+
+ // if edit is after range, skip
+ if edit.start_byte > range.end_byte {
+ // TODO: || (is_noop && edit.start_byte == range.end_byte)
+ continue;
+ }
+
+ // if edit is before range, shift entire range by len
+ if edit.old_end_byte < range.start_byte {
+ range.start_byte =
+ edit.new_end_byte + (range.start_byte - edit.old_end_byte);
+ range.start_point = point_add(
+ edit.new_end_position,
+ point_sub(range.start_point, edit.old_end_position),
+ );
+
+ range.end_byte = edit
+ .new_end_byte
+ .saturating_add(range.end_byte - edit.old_end_byte);
+ range.end_point = point_add(
+ edit.new_end_position,
+ point_sub(range.end_point, edit.old_end_position),
+ );
+ }
+ // if the edit starts in the space before and extends into the range
+ else if edit.start_byte < range.start_byte {
+ range.start_byte = edit.new_end_byte;
+ range.start_point = edit.new_end_position;
+
+ range.end_byte = range
+ .end_byte
+ .saturating_sub(edit.old_end_byte)
+ .saturating_add(edit.new_end_byte);
+ range.end_point = point_add(
+ edit.new_end_position,
+ point_sub(range.end_point, edit.old_end_position),
+ );
+ }
+ // If the edit is an insertion at the start of the tree, shift
+ else if edit.start_byte == range.start_byte && is_pure_insertion {
+ range.start_byte = edit.new_end_byte;
+ range.start_point = edit.new_end_position;
+ } else {
+ range.end_byte = range
+ .end_byte
+ .saturating_sub(edit.old_end_byte)
+ .saturating_add(edit.new_end_byte);
+ range.end_point = point_add(
+ edit.new_end_position,
+ point_sub(range.end_point, edit.old_end_position),
+ );
+ }
+ }
+ }
+ }
+ }
+
PARSER.with(|ts_parser| {
- self.root_layer.update(
- &mut ts_parser.borrow_mut(),
- &self.config,
- old_source,
- source,
- changeset,
- )
- })
+ let ts_parser = &mut ts_parser.borrow_mut();
+ let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
+ // TODO: might need to set cursor range
+ cursor.set_byte_range(0..usize::MAX);
- // TODO: deal with injections and update them too
- }
+ let source_slice = source.slice(..);
- // fn buffer_changed -> call layer.update(range, new_text) on root layer and then all marker layers
+ let mut touched = HashSet::new();
- // call this on transaction.apply() -> buffer_changed(changes)
- //
- // fn parse(language, old_tree, ranges)
- //
- pub fn tree(&self) -> &Tree {
- self.root_layer.tree()
+ // TODO: we should be able to avoid editing & parsing layers with ranges earlier in the document before the edit
+
+ while let Some(layer_id) = queue.pop_front() {
+ // Mark the layer as touched
+ touched.insert(layer_id);
+
+ let layer = &mut self.layers[layer_id];
+
+ // If a tree already exists, notify it of changes.
+ if let Some(tree) = &mut layer.tree {
+ for edit in edits.iter().rev() {
+ // Apply the edits in reverse.
+ // If we applied them in order then edit 1 would disrupt the positioning of edit 2.
+ tree.edit(edit);
+ }
+ }
+
+ // Re-parse the tree.
+ layer.parse(&mut ts_parser.parser, source)?;
+
+ // Switch to an immutable borrow.
+ let layer = &self.layers[layer_id];
+
+ // Process injections.
+ let matches = cursor.matches(
+ &layer.config.injections_query,
+ layer.tree().root_node(),
+ RopeProvider(source_slice),
+ );
+ let mut injections = Vec::new();
+ for mat in matches {
+ let (language_name, content_node, include_children) = injection_for_match(
+ &layer.config,
+ &layer.config.injections_query,
+ &mat,
+ source_slice,
+ );
+
+ // Explicitly remove this match so that none of its other captures will remain
+ // in the stream of captures.
+ mat.remove();
+
+ // If a language is found with the given name, then add a new language layer
+ // to the highlighted document.
+ if let (Some(language_name), Some(content_node)) = (language_name, content_node)
+ {
+ if let Some(config) = (injection_callback)(&language_name) {
+ let ranges =
+ intersect_ranges(&layer.ranges, &[content_node], include_children);
+
+ if !ranges.is_empty() {
+ injections.push((config, ranges));
+ }
+ }
+ }
+ }
+
+ // Process combined injections.
+ if let Some(combined_injections_query) = &layer.config.combined_injections_query {
+ let mut injections_by_pattern_index =
+ vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
+ let matches = cursor.matches(
+ combined_injections_query,
+ layer.tree().root_node(),
+ RopeProvider(source_slice),
+ );
+ for mat in matches {
+ let entry = &mut injections_by_pattern_index[mat.pattern_index];
+ let (language_name, content_node, include_children) = injection_for_match(
+ &layer.config,
+ combined_injections_query,
+ &mat,
+ source_slice,
+ );
+ if language_name.is_some() {
+ entry.0 = language_name;
+ }
+ if let Some(content_node) = content_node {
+ entry.1.push(content_node);
+ }
+ entry.2 = include_children;
+ }
+ for (lang_name, content_nodes, includes_children) in injections_by_pattern_index
+ {
+ if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
+ if let Some(config) = (injection_callback)(&lang_name) {
+ let ranges = intersect_ranges(
+ &layer.ranges,
+ &content_nodes,
+ includes_children,
+ );
+ if !ranges.is_empty() {
+ injections.push((config, ranges));
+ }
+ }
+ }
+ }
+ }
+
+ let depth = layer.depth + 1;
+ // TODO: can't inline this since matches borrows self.layers
+ for (config, ranges) in injections {
+ // Find an existing layer
+ let layer = self
+ .layers
+ .iter_mut()
+ .find(|(_, layer)| {
+ layer.depth == depth && // TODO: track parent id instead
+ layer.config.language == config.language && layer.ranges == ranges
+ })
+ .map(|(id, _layer)| id);
+
+ // ...or insert a new one.
+ let layer_id = layer.unwrap_or_else(|| {
+ self.layers.insert(LanguageLayer {
+ tree: None,
+ config,
+ depth,
+ ranges,
+ })
+ });
+
+ queue.push_back(layer_id);
+ }
+
+ // TODO: pre-process local scopes at this time, rather than highlight?
+ // would solve problems with locals not working across boundaries
+ }
+
+ // Return the cursor back in the pool.
+ ts_parser.cursors.push(cursor);
+
+ // Remove all untouched layers
+ self.layers.retain(|id, _| touched.contains(&id));
+
+ Ok(())
+ })
}
- //
- // <!--update_for_injection(grammar)-->
- // Highlighting
+ pub fn tree(&self) -> &Tree {
+ self.layers[self.root].tree()
+ }
/// Iterate over the highlighted regions for a given slice of source code.
pub fn highlight_iter<'a>(
@@ -518,65 +760,76 @@ impl Syntax {
source: RopeSlice<'a>,
range: Option<std::ops::Range<usize>>,
cancellation_flag: Option<&'a AtomicUsize>,
- injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a {
- // 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.
-
- // reuse a cursor from the pool if possible
- let mut cursor = PARSER.with(|ts_parser| {
- let highlighter = &mut ts_parser.borrow_mut();
- highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
+ let mut layers = 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));
+
+ let mut captures = cursor_ref
+ .captures(
+ &layer.config.query,
+ layer.tree().root_node(),
+ RopeProvider(source),
+ )
+ .peekable();
+
+ // If there's no captures, skip the layer
+ captures.peek()?;
+
+ Some(HighlightIterLayer {
+ highlight_end_stack: Vec::new(),
+ scope_stack: vec![LocalScope {
+ inherits: false,
+ range: 0..usize::MAX,
+ local_defs: Vec::new(),
+ }],
+ cursor,
+ _tree: None,
+ captures,
+ config: layer.config.as_ref(), // TODO: just reuse `layer`
+ depth: layer.depth, // TODO: just reuse `layer`
+ ranges: &layer.ranges, // TODO: temp
+ })
+ })
+ .collect::<Vec<_>>();
+
+ // HAXX: arrange layers by byte range, with deeper layers positioned first
+ layers.sort_by_key(|layer| {
+ (
+ layer.ranges.first().cloned(),
+ std::cmp::Reverse(layer.depth),
+ )
});
- let tree_ref = self.tree();
- let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
- let query_ref = &self.config.query;
- let config_ref = self.config.as_ref();
-
- // if reusing cursors & no range this resets to whole range
- cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
-
- let captures = cursor_ref
- .captures(query_ref, tree_ref.root_node(), RopeProvider(source))
- .peekable();
-
- // manually craft the root layer based on the existing tree
- let layer = HighlightIterLayer {
- highlight_end_stack: Vec::new(),
- scope_stack: vec![LocalScope {
- inherits: false,
- range: 0..usize::MAX,
- local_defs: Vec::new(),
- }],
- cursor,
- depth: 0,
- _tree: None,
- captures,
- config: config_ref,
- ranges: vec![Range {
- start_byte: 0,
- end_byte: usize::MAX,
- start_point: Point::new(0, 0),
- end_point: Point::new(usize::MAX, usize::MAX),
- }],
- };
let mut result = HighlightIter {
source,
- byte_offset: range.map_or(0, |r| r.start), // TODO: simplify
- injection_callback,
+ byte_offset: range.map_or(0, |r| r.start),
cancellation_flag,
iter_count: 0,
- layers: vec![layer],
+ layers,
next_event: None,
last_highlight_range: None,
};
result.sort_layers();
result
}
- // on_tokenize
- // on_change_highlighting
// Commenting
// comment_strings_for_pos
@@ -588,246 +841,157 @@ impl Syntax {
// indent_level_for_line
// TODO: Folding
-
- // Syntax APIs
- // get_syntax_node_containing_range ->
- // ...
- // get_syntax_node_at_pos
- // buffer_range_for_scope_at_pos
}
#[derive(Debug)]
pub struct LanguageLayer {
// mode
// grammar
- // depth
+ pub config: Arc<HighlightConfiguration>,
pub(crate) tree: Option<Tree>,
+ pub ranges: Vec<Range>,
+ pub depth: usize,
}
impl LanguageLayer {
- // pub fn new() -> Self {
- // Self { tree: None }
- // }
-
pub fn tree(&self) -> &Tree {
// TODO: no unwrap
self.tree.as_ref().unwrap()
}
- fn parse(
- &mut self,
- ts_parser: &mut TsParser,
- config: &HighlightConfiguration,
- source: &Rope,
- _depth: usize,
- ranges: Vec<Range>,
- ) -> Result<(), Error> {
- if ts_parser.parser.set_included_ranges(&ranges).is_ok() {
- ts_parser
- .parser
- .set_language(config.language)
- .map_err(|_| Error::InvalidLanguage)?;
-
- // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
- let tree = ts_parser
- .parser
- .parse_with(
- &mut |byte, _| {
- if byte <= source.len_bytes() {
- let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
- chunk[byte - start_byte..].as_bytes()
- } else {
- // out of range
- &[]
- }
- },
- self.tree.as_ref(),
- )
- .ok_or(Error::Cancelled)?;
+ fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> {
+ parser.set_included_ranges(&self.ranges).unwrap();
- self.tree = Some(tree)
- }
+ parser
+ .set_language(self.config.language)
+ .map_err(|_| Error::InvalidLanguage)?;
+
+ // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
+ let tree = parser
+ .parse_with(
+ &mut |byte, _| {
+ if byte <= source.len_bytes() {
+ let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
+ chunk[byte - start_byte..].as_bytes()
+ } else {
+ // out of range
+ &[]
+ }
+ },
+ self.tree.as_ref(),
+ )
+ .ok_or(Error::Cancelled)?;
+ // unsafe { ts_parser.parser.set_cancellation_flag(None) };
+ self.tree = Some(tree);
Ok(())
}
+}
- pub(crate) fn generate_edits(
- old_text: RopeSlice,
- changeset: &ChangeSet,
- ) -> Vec<tree_sitter::InputEdit> {
- use Operation::*;
- let mut old_pos = 0;
+pub(crate) fn generate_edits(
+ old_text: &Rope,
+ changeset: &ChangeSet,
+) -> Vec<tree_sitter::InputEdit> {
+ use Operation::*;
+ let mut old_pos = 0;
- let mut edits = Vec::new();
+ let mut edits = Vec::new();
- let mut iter = changeset.changes.iter().peekable();
+ if changeset.changes.is_empty() {
+ return edits;
+ }
- // TODO; this is a lot easier with Change instead of Operation.
+ let mut iter = changeset.changes.iter().peekable();
- fn point_at_pos(text: RopeSlice, pos: usize) -> (usize, Point) {
- let byte = text.char_to_byte(pos); // <- attempted to index past end
- let line = text.char_to_line(pos);
- let line_start_byte = text.line_to_byte(line);
- let col = byte - line_start_byte;
+ // TODO; this is a lot easier with Change instead of Operation.
- (byte, Point::new(line, col))
- }
+ fn point_at_pos(text: &Rope, pos: usize) -> (usize, Point) {
+ let byte = text.char_to_byte(pos); // <- attempted to index past end
+ let line = text.char_to_line(pos);
+ let line_start_byte = text.line_to_byte(line);
+ let col = byte - line_start_byte;
- fn traverse(point: Point, text: &Tendril) -> Point {
- let Point {
- mut row,
- mut column,
- } = point;
-
- // TODO: there should be a better way here.
- let mut chars = text.chars().peekable();
- while let Some(ch) = chars.next() {
- if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
- row += 1;
- column = 0;
- } else {
- column += 1;
- }
+ (byte, Point::new(line, col))
+ }
+
+ fn traverse(point: Point, text: &Tendril) -> Point {
+ let Point {
+ mut row,
+ mut column,
+ } = point;
+
+ // TODO: there should be a better way here.
+ let mut chars = text.chars().peekable();
+ while let Some(ch) = chars.next() {
+ if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
+ row += 1;
+ column = 0;
+ } else {
+ column += 1;
}
- Point { row, column }
}
+ Point { row, column }
+ }
- while let Some(change) = iter.next() {
- let len = match change {
- Delete(i) | Retain(i) => *i,
- Insert(_) => 0,
- };
- let mut old_end = old_pos + len;
+ while let Some(change) = iter.next() {
+ let len = match change {
+ Delete(i) | Retain(i) => *i,
+ Insert(_) => 0,
+ };
+ let mut old_end = old_pos + len;
+
+ match change {
+ Retain(_) => {}
+ Delete(_) => {
+ let (start_byte, start_position) = point_at_pos(old_text, old_pos);
+ let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
+
+ // deletion
+ edits.push(tree_sitter::InputEdit {
+ start_byte, // old_pos to byte
+ old_end_byte, // old_end to byte
+ new_end_byte: start_byte, // old_pos to byte
+ start_position, // old pos to coords
+ old_end_position, // old_end to coords
+ new_end_position: start_position, // old pos to coords
+ });
+ }
+ Insert(s) => {
+ let (start_byte, start_position) = point_at_pos(old_text, old_pos);
- match change {
- Retain(_) => {}
- Delete(_) => {
- let (start_byte, start_position) = point_at_pos(old_text, old_pos);
+ // a subsequent delete means a replace, consume it
+ if let Some(Delete(len)) = iter.peek() {
+ old_end = old_pos + len;
let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
- // TODO: Position also needs to be byte based...
- // let byte = char_to_byte(old_pos)
- // let line = char_to_line(old_pos)
- // let line_start_byte = line_to_byte()
- // Position::new(line, line_start_byte - byte)
+ iter.next();
- // deletion
+ // replacement
edits.push(tree_sitter::InputEdit {
- start_byte, // old_pos to byte
- old_end_byte, // old_end to byte
- new_end_byte: start_byte, // old_pos to byte
- start_position, // old pos to coords
- old_end_position, // old_end to coords
- new_end_position: start_position, // old pos to coords
+ start_byte, // old_pos to byte
+ old_end_byte, // old_end to byte
+ new_end_byte: start_byte + s.len(), // old_pos to byte + s.len()
+ start_position, // old pos to coords
+ old_end_position, // old_end to coords
+ new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
+ });
+ } else {
+ // insert
+ edits.push(tree_sitter::InputEdit {
+ start_byte, // old_pos to byte
+ old_end_byte: start_byte, // same
+ new_end_byte: start_byte + s.len(), // old_pos + s.len()
+ start_position, // old pos to coords
+ old_end_position: start_position, // same
+ new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
});
- }
- Insert(s) => {
- let (start_byte, start_position) = point_at_pos(old_text, old_pos);
-
- // a subsequent delete means a replace, consume it
- if let Some(Delete(len)) = iter.peek() {
- old_end = old_pos + len;
- let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
-
- iter.next();
-
- // replacement
- edits.push(tree_sitter::InputEdit {
- start_byte, // old_pos to byte
- old_end_byte, // old_end to byte
- new_end_byte: start_byte + s.len(), // old_pos to byte + s.len()
- start_position, // old pos to coords
- old_end_position, // old_end to coords
- new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
- });
- } else {
- // insert
- edits.push(tree_sitter::InputEdit {
- start_byte, // old_pos to byte
- old_end_byte: start_byte, // same
- new_end_byte: start_byte + s.len(), // old_pos + s.len()
- start_position, // old pos to coords
- old_end_position: start_position, // same
- new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
- });
- }
}
}
- old_pos = old_end;
}
- edits
- }
-
- fn update(
- &mut self,
- ts_parser: &mut TsParser,
- config: &HighlightConfiguration,
- old_source: &Rope,
- source: &Rope,
- changeset: &ChangeSet,
- ) -> Result<(), Error> {
- if changeset.is_empty() {
- return Ok(());
- }
-
- let edits = Self::generate_edits(old_source.slice(..), changeset);
-
- // Notify the tree about all the changes
- for edit in edits.iter().rev() {
- // apply the edits in reverse. If we applied them in order then edit 1 would disrupt
- // the positioning of edit 2
- self.tree.as_mut().unwrap().edit(edit);
- }
-
- self.parse(
- ts_parser,
- config,
- source,
- 0,
- // TODO: what to do about this range on update
- vec![Range {
- start_byte: 0,
- end_byte: usize::MAX,
- start_point: Point::new(0, 0),
- end_point: Point::new(usize::MAX, usize::MAX),
- }],
- )
+ old_pos = old_end;
}
-
- // fn highlight_iter() -> same as Mode but for this layer. Mode composits these
- // fn buffer_changed
- // fn update(range)
- // fn update_injections()
+ edits
}
-// -- refactored from tree-sitter-highlight to be able to retain state
-// TODO: add seek() to iter
-
-// problem: any time a layer is updated it must update it's injections on the parent (potentially
-// removing some from use)
-// can't modify to vec and exist in it at the same time since that would violate borrows
-// maybe we can do with an arena
-// maybe just caching on the top layer and nevermind the injections for now?
-//
-// Grammar {
-// layers: Vec<Box<Layer>> to prevent memory moves when vec is modified
-// }
-// injections tracked by marker:
-// if marker areas match it's fine and update
-// if not found add new layer
-// if length 0 then area got removed, clean up the layer
-//
-// layer update:
-// if range.len = 0 then remove the layer
-// for change in changes { tree.edit(change) }
-// tree = parser.parse(.., tree, ..)
-// calculate affected range and update injections
-// injection update:
-// look for existing injections
-// if present, range = (first injection start, last injection end)
-//
-// For now cheat and just throw out non-root layers if they exist. This should still improve
-// parsing in majority of cases.
-
use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize};
use tree_sitter::{
@@ -864,8 +1028,8 @@ pub enum HighlightEvent {
pub struct HighlightConfiguration {
pub language: Grammar,
pub query: Query,
+ injections_query: Query,
combined_injections_query: Option<Query>,
- locals_pattern_index: usize,
highlights_pattern_index: usize,
highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
non_local_variable_patterns: Vec<bool>,
@@ -892,13 +1056,9 @@ struct LocalScope<'a> {
}
#[derive(Debug)]
-struct HighlightIter<'a, F>
-where
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+struct HighlightIter<'a> {
source: RopeSlice<'a>,
byte_offset: usize,
- injection_callback: F,
cancellation_flag: Option<&'a AtomicUsize>,
layers: Vec<HighlightIterLayer<'a>>,
iter_count: usize,
@@ -938,8 +1098,8 @@ struct HighlightIterLayer<'a> {
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
- ranges: Vec<Range>,
depth: usize,
+ ranges: &'a [Range],
}
impl<'a> fmt::Debug for HighlightIterLayer<'a> {
@@ -971,38 +1131,32 @@ impl HighlightConfiguration {
) -> Result<Self, QueryError> {
// Concatenate the query strings, keeping track of the start offset of each section.
let mut query_source = String::new();
- query_source.push_str(injection_query);
- let locals_query_offset = query_source.len();
query_source.push_str(locals_query);
let highlights_query_offset = query_source.len();
query_source.push_str(highlights_query);
// Construct a single query by concatenating the three query strings, but record the
// range of pattern indices that belong to each individual string.
- let mut query = Query::new(language, &query_source)?;
- let mut locals_pattern_index = 0;
+ let query = Query::new(language, &query_source)?;
let mut highlights_pattern_index = 0;
for i in 0..(query.pattern_count()) {
let pattern_offset = query.start_byte_for_pattern(i);
if pattern_offset < highlights_query_offset {
- if pattern_offset < highlights_query_offset {
- highlights_pattern_index += 1;
- }
- if pattern_offset < locals_query_offset {
- locals_pattern_index += 1;
- }
+ highlights_pattern_index += 1;
}
}
+ let mut injections_query = Query::new(language, injection_query)?;
+
// Construct a separate query just for dealing with the 'combined injections'.
// Disable the combined injection patterns in the main query.
let mut combined_injections_query = Query::new(language, injection_query)?;
let mut has_combined_queries = false;
- for pattern_index in 0..locals_pattern_index {
- let settings = query.property_settings(pattern_index);
+ for pattern_index in 0..injections_query.pattern_count() {
+ let settings = injections_query.property_settings(pattern_index);
if settings.iter().any(|s| &*s.key == "injection.combined") {
has_combined_queries = true;
- query.disable_pattern(pattern_index);
+ injections_query.disable_pattern(pattern_index);
} else {
combined_injections_query.disable_pattern(pattern_index);
}
@@ -1034,8 +1188,6 @@ impl HighlightConfiguration {
for (i, name) in query.capture_names().iter().enumerate() {
let i = Some(i as u32);
match name.as_str() {
- "injection.content" => injection_content_capture_index = i,
- "injection.language" => injection_language_capture_index = i,
"local.definition" => local_def_capture_index = i,
"local.definition-value" => local_def_value_capture_index = i,
"local.reference" => local_ref_capture_index = i,
@@ -1044,12 +1196,21 @@ impl HighlightConfiguration {
}
}
+ for (i, name) in injections_query.capture_names().iter().enumerate() {
+ let i = Some(i as u32);
+ match name.as_str() {
+ "injection.content" => injection_content_capture_index = i,
+ "injection.language" => injection_language_capture_index = i,
+ _ => {}
+ }
+ }
+
let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
Ok(Self {
language,
query,
+ injections_query,
combined_injections_query,
- locals_pattern_index,
highlights_pattern_index,
highlight_indices,
non_local_variable_patterns,
@@ -1114,238 +1275,6 @@ impl HighlightConfiguration {
}
impl<'a> HighlightIterLayer<'a> {
- /// Create a new 'layer' of highlighting for this document.
- ///
- /// In the even that the new layer contains "combined injections" (injections where multiple
- /// disjoint ranges are parsed as one syntax tree), these will be eagerly processed and
- /// added to the returned vector.
- fn new<F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a>(
- source: RopeSlice<'a>,
- cancellation_flag: Option<&'a AtomicUsize>,
- injection_callback: &mut F,
- mut config: &'a HighlightConfiguration,
- mut depth: usize,
- mut ranges: Vec<Range>,
- ) -> Result<Vec<Self>, Error> {
- let mut result = Vec::with_capacity(1);
- let mut queue = Vec::new();
- loop {
- // --> Tree parsing part
-
- PARSER.with(|ts_parser| {
- let highlighter = &mut ts_parser.borrow_mut();
-
- if highlighter.parser.set_included_ranges(&ranges).is_ok() {
- highlighter
- .parser
- .set_language(config.language)
- .map_err(|_| Error::InvalidLanguage)?;
-
- unsafe { highlighter.parser.set_cancellation_flag(cancellation_flag) };
- let tree = highlighter
- .parser
- .parse_with(
- &mut |byte, _| {
- if byte <= source.len_bytes() {
- let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
- chunk[byte - start_byte..].as_bytes()
- } else {
- // out of range
- &[]
- }
- },
- None,
- )
- .ok_or(Error::Cancelled)?;
- unsafe { highlighter.parser.set_cancellation_flag(None) };
- let mut cursor = highlighter.cursors.pop().unwrap_or_else(QueryCursor::new);
-
- // Process combined injections.
- if let Some(combined_injections_query) = &config.combined_injections_query {
- let mut injections_by_pattern_index = vec![
- (None, Vec::new(), false);
- combined_injections_query
- .pattern_count()
- ];
- let matches = cursor.matches(
- combined_injections_query,
- tree.root_node(),
- RopeProvider(source),
- );
- for mat in matches {
- let entry = &mut injections_by_pattern_index[mat.pattern_index];
- let (language_name, content_node, include_children) =
- injection_for_match(
- config,
- combined_injections_query,
- &mat,
- source,
- );
- if language_name.is_some() {
- entry.0 = language_name;
- }
- if let Some(content_node) = content_node {
- entry.1.push(content_node);
- }
- entry.2 = include_children;
- }
- for (lang_name, content_nodes, includes_children) in
- injections_by_pattern_index
- {
- if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty())
- {
- if let Some(next_config) = (injection_callback)(&lang_name) {
- let ranges = Self::intersect_ranges(
- &ranges,
- &content_nodes,
- includes_children,
- );
- if !ranges.is_empty() {
- queue.push((next_config, depth + 1, ranges));
- }
- }
- }
- }
- }
-
- // --> Highlighting query part
-
- // 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 tree_ref = unsafe { mem::transmute::<_, &'static Tree>(&tree) };
- let cursor_ref =
- unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
- let captures = cursor_ref
- .captures(&config.query, tree_ref.root_node(), RopeProvider(source))
- .peekable();
-
- result.push(HighlightIterLayer {
- highlight_end_stack: Vec::new(),
- scope_stack: vec![LocalScope {
- inherits: false,
- range: 0..usize::MAX,
- local_defs: Vec::new(),
- }],
- cursor,
- depth,
- _tree: Some(tree),
- captures,
- config,
- ranges,
- });
- }
-
- Ok(()) // so we can use the try operator
- })?;
-
- if queue.is_empty() {
- break;
- }
-
- let (next_config, next_depth, next_ranges) = queue.remove(0);
- config = next_config;
- depth = next_depth;
- ranges = next_ranges;
- }
-
- Ok(result)
- }
-
- // Compute the ranges that should be included when parsing an injection.
- // This takes into account three things:
- // * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
- // * `nodes` - Every injection takes place within a set of nodes. The injection ranges
- // are the ranges of those nodes.
- // * `includes_children` - For some injections, the content nodes' children should be
- // excluded from the nested document, so that only the content nodes' *own* content
- // is reparsed. For other injections, the content nodes' entire ranges should be
- // reparsed, including the ranges of their children.
- fn intersect_ranges(
- parent_ranges: &[Range],
- nodes: &[Node],
- includes_children: bool,
- ) -> Vec<Range> {
- let mut cursor = nodes[0].walk();
- let mut result = Vec::new();
- let mut parent_range_iter = parent_ranges.iter();
- let mut parent_range = parent_range_iter
- .next()
- .expect("Layers should only be constructed with non-empty ranges vectors");
- for node in nodes.iter() {
- let mut preceding_range = Range {
- start_byte: 0,
- start_point: Point::new(0, 0),
- end_byte: node.start_byte(),
- end_point: node.start_position(),
- };
- let following_range = Range {
- start_byte: node.end_byte(),
- start_point: node.end_position(),
- end_byte: usize::MAX,
- end_point: Point::new(usize::MAX, usize::MAX),
- };
-
- for excluded_range in node
- .children(&mut cursor)
- .filter_map(|child| {
- if includes_children {
- None
- } else {
- Some(child.range())
- }
- })
- .chain([following_range].iter().cloned())
- {
- let mut range = Range {
- start_byte: preceding_range.end_byte,
- start_point: preceding_range.end_point,
- end_byte: excluded_range.start_byte,
- end_point: excluded_range.start_point,
- };
- preceding_range = excluded_range;
-
- if range.end_byte < parent_range.start_byte {
- continue;
- }
-
- while parent_range.start_byte <= range.end_byte {
- if parent_range.end_byte > range.start_byte {
- if range.start_byte < parent_range.start_byte {
- range.start_byte = parent_range.start_byte;
- range.start_point = parent_range.start_point;
- }
-
- if parent_range.end_byte < range.end_byte {
- if range.start_byte < parent_range.end_byte {
- result.push(Range {
- start_byte: range.start_byte,
- start_point: range.start_point,
- end_byte: parent_range.end_byte,
- end_point: parent_range.end_point,
- });
- }
- range.start_byte = parent_range.end_byte;
- range.start_point = parent_range.end_point;
- } else {
- if range.start_byte < range.end_byte {
- result.push(range);
- }
- break;
- }
- }
-
- if let Some(next_range) = parent_range_iter.next() {
- parent_range = next_range;
- } else {
- return result;
- }
- }
- }
- }
- result
- }
-
// 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.
@@ -1371,10 +1300,101 @@ impl<'a> HighlightIterLayer<'a> {
}
}
-impl<'a, F> HighlightIter<'a, F>
-where
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+// Compute the ranges that should be included when parsing an injection.
+// This takes into account three things:
+// * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
+// * `nodes` - Every injection takes place within a set of nodes. The injection ranges
+// are the ranges of those nodes.
+// * `includes_children` - For some injections, the content nodes' children should be
+// excluded from the nested document, so that only the content nodes' *own* content
+// is reparsed. For other injections, the content nodes' entire ranges should be
+// reparsed, including the ranges of their children.
+fn intersect_ranges(
+ parent_ranges: &[Range],
+ nodes: &[Node],
+ includes_children: bool,
+) -> Vec<Range> {
+ let mut cursor = nodes[0].walk();
+ let mut result = Vec::new();
+ let mut parent_range_iter = parent_ranges.iter();
+ let mut parent_range = parent_range_iter
+ .next()
+ .expect("Layers should only be constructed with non-empty ranges vectors");
+ for node in nodes.iter() {
+ let mut preceding_range = Range {
+ start_byte: 0,
+ start_point: Point::new(0, 0),
+ end_byte: node.start_byte(),
+ end_point: node.start_position(),
+ };
+ let following_range = Range {
+ start_byte: node.end_byte(),
+ start_point: node.end_position(),
+ end_byte: usize::MAX,
+ end_point: Point::new(usize::MAX, usize::MAX),
+ };
+
+ for excluded_range in node
+ .children(&mut cursor)
+ .filter_map(|child| {
+ if includes_children {
+ None
+ } else {
+ Some(child.range())
+ }
+ })
+ .chain([following_range].iter().cloned())
+ {
+ let mut range = Range {
+ start_byte: preceding_range.end_byte,
+ start_point: preceding_range.end_point,
+ end_byte: excluded_range.start_byte,
+ end_point: excluded_range.start_point,
+ };
+ preceding_range = excluded_range;
+
+ if range.end_byte < parent_range.start_byte {
+ continue;
+ }
+
+ while parent_range.start_byte <= range.end_byte {
+ if parent_range.end_byte > range.start_byte {
+ if range.start_byte < parent_range.start_byte {
+ range.start_byte = parent_range.start_byte;
+ range.start_point = parent_range.start_point;
+ }
+
+ if parent_range.end_byte < range.end_byte {
+ if range.start_byte < parent_range.end_byte {
+ result.push(Range {
+ start_byte: range.start_byte,
+ start_point: range.start_point,
+ end_byte: parent_range.end_byte,
+ end_point: parent_range.end_point,
+ });
+ }
+ range.start_byte = parent_range.end_byte;
+ range.start_point = parent_range.end_point;
+ } else {
+ if range.start_byte < range.end_byte {
+ result.push(range);
+ }
+ break;
+ }
+ }
+
+ if let Some(next_range) = parent_range_iter.next() {
+ parent_range = next_range;
+ } else {
+ return result;
+ }
+ }
+ }
+ }
+ result
+}
+
+impl<'a> HighlightIter<'a> {
fn emit_event(
&mut self,
offset: usize,
@@ -1405,6 +1425,12 @@ where
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;
}
@@ -1421,30 +1447,9 @@ where
}
}
}
-
- fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
- if let Some(sort_key) = layer.sort_key() {
- let mut i = 1;
- while i < self.layers.len() {
- if let Some(sort_key_i) = self.layers[i].sort_key() {
- if sort_key_i > sort_key {
- self.layers.insert(i, layer);
- return;
- }
- i += 1;
- } else {
- self.layers.remove(i);
- }
- }
- self.layers.push(layer);
- }
- }
}
-impl<'a, F> Iterator for HighlightIter<'a, F>
-where
- F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
-{
+impl<'a> Iterator for HighlightIter<'a> {
type Item = Result<HighlightEvent, Error>;
fn next(&mut self) -> Option<Self::Item> {
@@ -1504,55 +1509,12 @@ where
layer.highlight_end_stack.pop();
return self.emit_event(end_byte, Some(HighlightEvent::HighlightEnd));
} else {
- // return self.emit_event(self.source.len(), None);
- return None;
+ return self.emit_event(self.source.len_bytes(), None);
};
let (mut match_, capture_index) = layer.captures.next().unwrap();
let mut capture = match_.captures[capture_index];
- // If this capture represents an injection, then process the injection.
- if match_.pattern_index < layer.config.locals_pattern_index {
- let (language_name, content_node, include_children) =
- injection_for_match(layer.config, &layer.config.query, &match_, self.source);
-
- // Explicitly remove this match so that none of its other captures will remain
- // in the stream of captures.
- match_.remove();
-
- // If a language is found with the given name, then add a new language layer
- // to the highlighted document.
- if let (Some(language_name), Some(content_node)) = (language_name, content_node) {
- if let Some(config) = (self.injection_callback)(&language_name) {
- let ranges = HighlightIterLayer::intersect_ranges(
- &self.layers[0].ranges,
- &[content_node],
- include_children,
- );
- if !ranges.is_empty() {
- match HighlightIterLayer::new(
- self.source,
- self.cancellation_flag,
- &mut self.injection_callback,
- config,
- self.layers[0].depth + 1,
- ranges,
- ) {
- Ok(layers) => {
- for layer in layers {
- self.insert_layer(layer);
- }
- }
- Err(e) => return Some(Err(e)),
- }
- }
- }
- }
-
- self.sort_layers();
- continue 'main;
- }
-
// Remove from the local scope stack any local scopes that have already ended.
while range.start > layer.scope_stack.last().unwrap().range.end {
layer.scope_stack.pop();
@@ -1747,14 +1709,6 @@ fn injection_for_match<'a>(
(language_name, content_node, include_children)
}
-// fn shrink_and_clear<T>(vec: &mut Vec<T>, capacity: usize) {
-// if vec.len() > capacity {
-// vec.truncate(capacity);
-// vec.shrink_to_fit();
-// }
-// vec.clear();
-// }
-
pub struct Merge<I> {
iter: I,
spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,
@@ -1921,6 +1875,8 @@ mod test {
.map(String::from)
.collect();
+ let loader = Loader::new(Configuration { language: vec![] });
+
let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap();
let config = HighlightConfiguration::new(
language,
@@ -1943,7 +1899,7 @@ mod test {
fn main() {}
",
);
- let syntax = Syntax::new(&source, Arc::new(config));
+ let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
let tree = syntax.tree();
let root = tree.root_node();
assert_eq!(root.kind(), "source_file");
@@ -1970,7 +1926,7 @@ mod test {
&doc,
vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
);
- let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes());
+ let edits = generate_edits(&doc, transaction.changes());
// transaction.apply(&mut state);
assert_eq!(
@@ -1999,7 +1955,7 @@ mod test {
let mut doc = Rope::from("fn test() {}");
let transaction =
Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter());
- let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes());
+ let edits = generate_edits(&doc, transaction.changes());
transaction.apply(&mut doc);
assert_eq!(doc, "fn test(a: u32) {}");
diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs
index dfc18fbe..2e34a986 100644
--- a/helix-core/src/transaction.rs
+++ b/helix-core/src/transaction.rs
@@ -22,7 +22,7 @@ pub enum Assoc {
}
// ChangeSpec = Change | ChangeSet | Vec<Change>
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct ChangeSet {
pub(crate) changes: Vec<Operation>,
/// The required document length. Will refuse to apply changes unless it matches.
@@ -30,16 +30,6 @@ pub struct ChangeSet {
len_after: usize,
}
-impl Default for ChangeSet {
- fn default() -> Self {
- Self {
- changes: Vec::new(),
- len: 0,
- len_after: 0,
- }
- }
-}
-
impl ChangeSet {
pub fn with_capacity(capacity: usize) -> Self {
Self {
@@ -95,7 +85,7 @@ impl ChangeSet {
let new_last = match self.changes.as_mut_slice() {
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
- prev.push_tendril(&fragment);
+ prev.push_str(&fragment);
return;
}
[.., last @ Delete(_)] => std::mem::replace(last, Insert(fragment)),
@@ -199,7 +189,7 @@ impl ChangeSet {
// TODO: cover this with a test
// figure out the byte index of the truncated string end
let (pos, _) = s.char_indices().nth(j).unwrap();
- s.pop_front(pos as u32);
+ s.replace_range(0..pos, "");
head_a = Some(Insert(s));
head_b = changes_b.next();
}
@@ -221,9 +211,11 @@ impl ChangeSet {
Ordering::Greater => {
// figure out the byte index of the truncated string end
let (pos, _) = s.char_indices().nth(j).unwrap();
- let pos = pos as u32;
- changes.insert(s.subtendril(0, pos));
- head_a = Some(Insert(s.subtendril(pos, s.len() as u32 - pos)));
+ let mut before = s;
+ let after = before.split_off(pos);
+
+ changes.insert(before);
+ head_a = Some(Insert(after));
head_b = changes_b.next();
}
}
@@ -287,7 +279,7 @@ impl ChangeSet {
}
Delete(n) => {
let text = Cow::from(original_doc.slice(pos..pos + *n));
- changes.insert(Tendril::from_slice(&text));
+ changes.insert(Tendril::from(text.as_ref()));
pos += n;
}
Insert(s) => {
@@ -330,7 +322,7 @@ impl ChangeSet {
/// `true` when the set is empty.
#[inline]
pub fn is_empty(&self) -> bool {
- self.changes.is_empty()
+ self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
}
/// Map a position through the changes.
@@ -419,7 +411,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
-#[derive(Debug, Default, Clone)]
+#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,
@@ -720,19 +712,19 @@ mod test {
#[test]
fn optimized_composition() {
let mut state = State::new("".into());
- let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('h'));
+ let t1 = Transaction::insert(&state.doc, &state.selection, Tendril::from("h"));
t1.apply(&mut state.doc);
state.selection = state.selection.clone().map(t1.changes());
- let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('e'));
+ let t2 = Transaction::insert(&state.doc, &state.selection, Tendril::from("e"));
t2.apply(&mut state.doc);
state.selection = state.selection.clone().map(t2.changes());
- let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l'));
+ let t3 = Transaction::insert(&state.doc, &state.selection, Tendril::from("l"));
t3.apply(&mut state.doc);
state.selection = state.selection.clone().map(t3.changes());
- let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('l'));
+ let t4 = Transaction::insert(&state.doc, &state.selection, Tendril::from("l"));
t4.apply(&mut state.doc);
state.selection = state.selection.clone().map(t4.changes());
- let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from_char('o'));
+ let t5 = Transaction::insert(&state.doc, &state.selection, Tendril::from("o"));
t5.apply(&mut state.doc);
state.selection = state.selection.clone().map(t5.changes());
@@ -771,7 +763,7 @@ mod test {
#[test]
fn combine_with_utf8() {
- const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
+ const TEST_CASE: &str = "Hello, これはヘリックスエディターです!";
let empty = Rope::from("");
let a = ChangeSet::new(&empty);
diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml
index 42dd29a8..24288697 100644
--- a/helix-dap/Cargo.toml
+++ b/helix-dap/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-dap"
-version = "0.5.0"
+version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
@@ -12,7 +12,7 @@ homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-helix-core = { version = "0.5", path = "../helix-core" }
+helix-core = { version = "0.6", path = "../helix-core" }
anyhow = "1.0"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
diff --git a/helix-dap/src/transport.rs b/helix-dap/src/transport.rs
index 40474e99..783a6f5d 100644
--- a/helix-dap/src/transport.rs
+++ b/helix-dap/src/transport.rs
@@ -36,7 +36,7 @@ pub struct Response {
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Payload {
// type = "event"
- Event(Event),
+ Event(Box<Event>),
// type = "response"
Response(Response),
// type = "request"
@@ -45,6 +45,7 @@ pub enum Payload {
#[derive(Debug)]
pub struct Transport {
+ #[allow(unused)]
id: usize,
pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
}
diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml
index 83b2978d..39b53706 100644
--- a/helix-lsp/Cargo.toml
+++ b/helix-lsp/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-lsp"
-version = "0.5.0"
+version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
@@ -12,16 +12,16 @@ homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
-helix-core = { version = "0.5", path = "../helix-core" }
+helix-core = { version = "0.6", path = "../helix-core" }
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures
log = "0.4"
-lsp-types = { version = "0.91", features = ["proposed"] }
+lsp-types = { version = "0.92", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
-tokio = { version = "1.14", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
+tokio = { version = "1.16", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.8"
diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs
index 271fd9d5..15cbca0e 100644
--- a/helix-lsp/src/client.rs
+++ b/helix-lsp/src/client.rs
@@ -31,6 +31,7 @@ pub struct Client {
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
offset_encoding: OffsetEncoding,
config: Option<Value>,
+ root_markers: Vec<String>,
}
impl Client {
@@ -39,6 +40,7 @@ impl Client {
cmd: &str,
args: &[String],
config: Option<Value>,
+ root_markers: Vec<String>,
id: usize,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
let process = Command::new(cmd)
@@ -68,6 +70,7 @@ impl Client {
capabilities: OnceCell::new(),
offset_encoding: OffsetEncoding::Utf8,
config,
+ root_markers,
};
Ok((client, server_rx, initialize_notify))
@@ -202,7 +205,7 @@ impl Client {
Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2),
id,
- result,
+ result: serde_json::to_value(result)?,
}),
Err(error) => Output::Failure(Failure {
jsonrpc: Some(Version::V2),
@@ -225,7 +228,8 @@ impl Client {
pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
// TODO: delay any requests that are triggered prior to initialize
- let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
+ let root = find_root(None, &self.root_markers)
+ .and_then(|root| lsp::Url::from_file_path(root).ok());
if self.config.is_some() {
log::info!("Using custom LSP config: {}", self.config.as_ref().unwrap());
@@ -434,7 +438,7 @@ impl Client {
changes.push(lsp::TextDocumentContentChangeEvent {
range: Some(lsp::Range::new(start, end)),
- text: s.into(),
+ text: s.to_string(),
range_length: None,
});
}
@@ -556,6 +560,14 @@ impl Client {
self.call::<lsp::request::Completion>(params)
}
+ pub async fn resolve_completion_item(
+ &self,
+ completion_item: lsp::CompletionItem,
+ ) -> Result<lsp::CompletionItem> {
+ self.request::<lsp::request::ResolveCompletionItem>(completion_item)
+ .await
+ }
+
pub fn text_document_signature_help(
&self,
text_document: lsp::TextDocumentIdentifier,
@@ -800,4 +812,16 @@ impl Client {
let response = self.request::<lsp::request::Rename>(params).await?;
Ok(response.unwrap_or_default())
}
+
+ pub fn command(&self, command: lsp::Command) -> impl Future<Output = Result<Value>> {
+ let params = lsp::ExecuteCommandParams {
+ command: command.command,
+ arguments: command.arguments.unwrap_or_default(),
+ work_done_progress_params: lsp::WorkDoneProgressParams {
+ work_done_token: None,
+ },
+ };
+
+ self.call::<lsp::request::ExecuteCommand>(params)
+ }
}
diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs
index 7fa65928..109546d0 100644
--- a/helix-lsp/src/lib.rs
+++ b/helix-lsp/src/lib.rs
@@ -66,39 +66,26 @@ pub mod util {
pos: lsp::Position,
offset_encoding: OffsetEncoding,
) -> Option<usize> {
- let max_line = doc.lines().count().saturating_sub(1);
let pos_line = pos.line as usize;
- let pos_line = if pos_line > max_line {
+ if pos_line > doc.len_lines() - 1 {
return None;
- } else {
- pos_line
- };
+ }
+
match offset_encoding {
OffsetEncoding::Utf8 => {
- let max_char = doc
- .line_to_char(max_line)
- .checked_add(doc.line(max_line).len_chars())?;
let line = doc.line_to_char(pos_line);
let pos = line.checked_add(pos.character as usize)?;
- if pos <= max_char {
+ if pos <= doc.len_chars() {
Some(pos)
} else {
None
}
}
OffsetEncoding::Utf16 => {
- let max_char = doc
- .line_to_char(max_line)
- .checked_add(doc.line(max_line).len_chars())?;
- let max_cu = doc.char_to_utf16_cu(max_char);
let line = doc.line_to_char(pos_line);
let line_start = doc.char_to_utf16_cu(line);
let pos = line_start.checked_add(pos.character as usize)?;
- if pos <= max_cu {
- Some(doc.utf16_cu_to_char(pos))
- } else {
- None
- }
+ doc.try_utf16_cu_to_char(pos).ok()
}
}
}
@@ -203,6 +190,7 @@ pub mod util {
#[derive(Debug, PartialEq, Clone)]
pub enum MethodCall {
WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams),
+ ApplyWorkspaceEdit(lsp::ApplyWorkspaceEditParams),
}
impl MethodCall {
@@ -215,6 +203,12 @@ impl MethodCall {
.expect("Failed to parse WorkDoneCreate params");
Self::WorkDoneProgressCreate(params)
}
+ lsp::request::ApplyWorkspaceEdit::METHOD => {
+ let params: lsp::ApplyWorkspaceEditParams = params
+ .parse()
+ .expect("Failed to parse ApplyWorkspaceEdit params");
+ Self::ApplyWorkspaceEdit(params)
+ }
_ => {
log::warn!("unhandled lsp request: {}", method);
return None;
@@ -319,6 +313,7 @@ impl Registry {
&config.command,
&config.args,
language_config.config.clone(),
+ language_config.roots.clone(),
id,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
@@ -337,7 +332,10 @@ impl Registry {
})
.await;
- value.expect("failed to initialize capabilities");
+ if let Err(e) = value {
+ log::error!("failed to initialize language server: {}", e);
+ return;
+ }
// next up, notify<initialized>
_client
diff --git a/helix-syntax/Cargo.toml b/helix-syntax/Cargo.toml
index cceec412..855839be 100644
--- a/helix-syntax/Cargo.toml
+++ b/helix-syntax/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-syntax"
-version = "0.5.0"
+version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
diff --git a/helix-syntax/README.md b/helix-syntax/README.md
new file mode 100644
index 00000000..bba2197a
--- /dev/null
+++ b/helix-syntax/README.md
@@ -0,0 +1,13 @@
+helix-syntax
+============
+
+Syntax highlighting for helix, (shallow) submodules resides here.
+
+Differences from nvim-treesitter
+--------------------------------
+
+As the syntax are commonly ported from
+<https://github.com/nvim-treesitter/nvim-treesitter>.
+
+Note that we do not support the custom `#any-of` predicate which is
+supported by neovim so one needs to change it to `#match` with regex.
diff --git a/helix-syntax/build.rs b/helix-syntax/build.rs
index 28f85e74..fa8be8b3 100644
--- a/helix-syntax/build.rs
+++ b/helix-syntax/build.rs
@@ -175,7 +175,6 @@ fn build_dir(dir: &str, language: &str) {
fn main() {
let ignore = vec![
"tree-sitter-typescript".to_string(),
- "tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34
"tree-sitter-ocaml".to_string(),
];
let dirs = collect_tree_sitter_dirs(&ignore).unwrap();
diff --git a/helix-syntax/languages/tree-sitter-comment b/helix-syntax/languages/tree-sitter-comment
new file mode 160000
+Subproject 5dd3c62f1bbe378b220fe16b317b85247898639
diff --git a/helix-syntax/languages/tree-sitter-dart b/helix-syntax/languages/tree-sitter-dart
new file mode 160000
+Subproject 6a25376685d1d47968c2cef06d4db8d84a70025
diff --git a/helix-syntax/languages/tree-sitter-dockerfile b/helix-syntax/languages/tree-sitter-dockerfile
new file mode 160000
+Subproject 7af32bc04a66ab196f5b9f92ac471f29372ae2c
diff --git a/helix-syntax/languages/tree-sitter-elm b/helix-syntax/languages/tree-sitter-elm
new file mode 160000
+Subproject bd50ccf66b42c55252ac8efc1086af4ac6bab8c
diff --git a/helix-syntax/languages/tree-sitter-fish b/helix-syntax/languages/tree-sitter-fish
new file mode 160000
+Subproject 04e54ab6585dfd4fee6ddfe5849af56f101b6d4
diff --git a/helix-syntax/languages/tree-sitter-git-commit b/helix-syntax/languages/tree-sitter-git-commit
new file mode 160000
+Subproject 066e395e1107df17183cf3ae4230f1a1406cc97
diff --git a/helix-syntax/languages/tree-sitter-git-config b/helix-syntax/languages/tree-sitter-git-config
new file mode 160000
+Subproject 0e4f0baf90b57e5aeb62dcdbf03062c6315d43e
diff --git a/helix-syntax/languages/tree-sitter-git-diff b/helix-syntax/languages/tree-sitter-git-diff
new file mode 160000
+Subproject c12e6ecb54485f764250556ffd7ccb18f8e2942
diff --git a/helix-syntax/languages/tree-sitter-git-rebase b/helix-syntax/languages/tree-sitter-git-rebase
new file mode 160000
+Subproject 332dc528f27044bc4427024dbb33e6941fc131f
diff --git a/helix-syntax/languages/tree-sitter-go b/helix-syntax/languages/tree-sitter-go
-Subproject 2a83dfdd759a632651f852aa4dc0af2525fae5c
+Subproject 0fa917a7022d1cd2e9b779a6a8fc5dc7fad69c7
diff --git a/helix-syntax/languages/tree-sitter-graphql b/helix-syntax/languages/tree-sitter-graphql
new file mode 160000
+Subproject 5e66e961eee421786bdda8495ed1db045e06b5f
diff --git a/helix-syntax/languages/tree-sitter-haskell b/helix-syntax/languages/tree-sitter-haskell
-Subproject 237f4eb4417c28f643a29d795ed227246afb66f
+Subproject b6ec26f181dd059eedd506fa5fbeae1b8e5556c
diff --git a/helix-syntax/languages/tree-sitter-iex b/helix-syntax/languages/tree-sitter-iex
new file mode 160000
+Subproject 3ec55082cf0be015d03148be8edfdfa8c56e77f
diff --git a/helix-syntax/languages/tree-sitter-lean b/helix-syntax/languages/tree-sitter-lean
new file mode 160000
+Subproject d98426109258b266e1e92358c5f11716d2e8f63
diff --git a/helix-syntax/languages/tree-sitter-llvm b/helix-syntax/languages/tree-sitter-llvm
new file mode 160000
+Subproject 3b213925b9c4f42c1acfe2e10bfbb438d9c6834
diff --git a/helix-syntax/languages/tree-sitter-llvm-mir b/helix-syntax/languages/tree-sitter-llvm-mir
new file mode 160000
+Subproject 06fabca19454b2dc00c1b211a7cb7ad0bc2585f
diff --git a/helix-syntax/languages/tree-sitter-make b/helix-syntax/languages/tree-sitter-make
new file mode 160000
+Subproject a4b9187417d6be349ee5fd4b6e77b4172c6827d
diff --git a/helix-syntax/languages/tree-sitter-markdown b/helix-syntax/languages/tree-sitter-markdown
new file mode 160000
+Subproject ad8c32917a16dfbb387d1da567bf0c3fb6fffde
diff --git a/helix-syntax/languages/tree-sitter-php b/helix-syntax/languages/tree-sitter-php
-Subproject 0d63eaf94e8d6c0694551b016c802787e61b3fb
+Subproject 57f855461aeeca73bd4218754fb26b5ac143f98
diff --git a/helix-syntax/languages/tree-sitter-regex b/helix-syntax/languages/tree-sitter-regex
new file mode 160000
+Subproject e1cfca3c79896ff79842f057ea13e529b66af63
diff --git a/helix-syntax/languages/tree-sitter-rescript b/helix-syntax/languages/tree-sitter-rescript
new file mode 160000
+Subproject 761eb9126b65e078b1b5770ac296b4af8870f93
diff --git a/helix-syntax/languages/tree-sitter-scala b/helix-syntax/languages/tree-sitter-scala
-Subproject fb23ed9a99da012d86b7a5059b9d8928607cce2
+Subproject 0a3dd53a7fc4b352a538397d054380aaa28be54
diff --git a/helix-syntax/languages/tree-sitter-tablegen b/helix-syntax/languages/tree-sitter-tablegen
new file mode 160000
+Subproject 568dd8a937347175fd58db83d4c4cdaeb6069bd
diff --git a/helix-syntax/languages/tree-sitter-twig b/helix-syntax/languages/tree-sitter-twig
new file mode 160000
+Subproject b7444181fb38e603e25ea8fcdac55f9492e49c2
diff --git a/helix-syntax/languages/tree-sitter-zig b/helix-syntax/languages/tree-sitter-zig
-Subproject 1f27fd1dfe7f352408f01b4894c7825f3a1d6c4
+Subproject 93331b8bd8b4ebee2b575490b2758f16ad4e9f3
diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml
index 43268291..e62496f2 100644
--- a/helix-term/Cargo.toml
+++ b/helix-term/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-term"
-version = "0.5.0"
+version = "0.6.0"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
@@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
+default-run = "hx"
[package.metadata.nix]
build = true
@@ -21,18 +22,18 @@ name = "hx"
path = "src/main.rs"
[dependencies]
-helix-core = { version = "0.5", path = "../helix-core" }
-helix-view = { version = "0.5", path = "../helix-view" }
-helix-lsp = { version = "0.5", path = "../helix-lsp" }
-helix-dap = { version = "0.5", path = "../helix-dap" }
+helix-core = { version = "0.6", path = "../helix-core" }
+helix-view = { version = "0.6", path = "../helix-view" }
+helix-lsp = { version = "0.6", path = "../helix-lsp" }
+helix-dap = { version = "0.6", path = "../helix-dap" }
anyhow = "1"
-once_cell = "1.8"
+once_cell = "1.9"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
num_cpus = "1"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
-crossterm = { version = "0.22", features = ["event-stream"] }
+crossterm = { version = "0.23", features = ["event-stream"] }
signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -46,7 +47,7 @@ log = "0.4"
fuzzy-matcher = "0.3"
ignore = "0.4"
# markdown doc rendering
-pulldown-cmark = { version = "0.8", default-features = false }
+pulldown-cmark = { version = "0.9", default-features = false }
# file type detection
content_inspector = "0.2.4"
diff --git a/helix-term/build.rs b/helix-term/build.rs
index 61ffa6f4..21dd5612 100644
--- a/helix-term/build.rs
+++ b/helix-term/build.rs
@@ -1,12 +1,17 @@
+use std::borrow::Cow;
use std::process::Command;
fn main() {
let git_hash = Command::new("git")
- .args(&["describe", "--dirty"])
+ .args(&["rev-parse", "HEAD"])
.output()
- .map(|x| String::from_utf8(x.stdout).ok())
.ok()
- .flatten()
- .unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION")));
- println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash);
+ .and_then(|x| String::from_utf8(x.stdout).ok());
+
+ let version: Cow<_> = match git_hash {
+ Some(git_hash) => format!("{} ({})", env!("CARGO_PKG_VERSION"), &git_hash[..8]).into(),
+ None => env!("CARGO_PKG_VERSION").into(),
+ };
+
+ println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
}
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index 55e4bb03..52a5321f 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -1,10 +1,16 @@
-use helix_core::{merge_toml_values, syntax};
+use helix_core::{merge_toml_values, pos_at_coords, syntax, Selection};
use helix_dap::{self as dap, Payload, Request};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{editor::Breakpoint, theme, Editor};
+use serde_json::json;
use crate::{
- args::Args, commands::fetch_stack_trace, compositor::Compositor, config::Config, job::Jobs, ui,
+ args::Args,
+ commands::{align_view, apply_workspace_edit, fetch_stack_trace, Align},
+ compositor::Compositor,
+ config::Config,
+ job::Jobs,
+ ui,
};
use log::{error, warn};
@@ -78,17 +84,27 @@ impl Application {
None => Ok(def_lang_conf),
};
- let theme = if let Some(theme) = &config.theme {
- match theme_loader.load(theme) {
- Ok(theme) => theme,
- Err(e) => {
- log::warn!("failed to load theme `{}` - {}", theme, e);
+ let true_color = config.editor.true_color || crate::true_color();
+ let theme = config
+ .theme
+ .as_ref()
+ .and_then(|theme| {
+ theme_loader
+ .load(theme)
+ .map_err(|e| {
+ log::warn!("failed to load theme `{}` - {}", theme, e);
+ e
+ })
+ .ok()
+ .filter(|theme| (true_color || theme.is_16_color()))
+ })
+ .unwrap_or_else(|| {
+ if true_color {
theme_loader.default()
+ } else {
+ theme_loader.base16_default()
}
- }
- } else {
- theme_loader.default()
- };
+ });
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.and_then(|conf| conf.try_into())
@@ -118,7 +134,7 @@ impl Application {
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
} else if !args.files.is_empty() {
- let first = &args.files[0]; // we know it's not empty
+ let first = &args.files[0].0; // we know it's not empty
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
@@ -126,16 +142,25 @@ impl Application {
} else {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
- for file in args.files {
+ for (file, pos) in args.files {
if file.is_dir() {
return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
));
} else {
- editor.open(file.to_path_buf(), Action::Load)?;
+ let doc_id = editor.open(file, Action::Load)?;
+ // with Action::Load all documents have the same view
+ let view_id = editor.tree.focus;
+ let doc = editor.document_mut(doc_id).unwrap();
+ let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
+ doc.set_selection(view_id, pos);
}
}
editor.set_status(format!("Loaded {} files.", nr_of_files));
+ // align the view to center after all files are loaded,
+ // does not affect views without pos since it is at the top
+ let (view, doc) = current!(editor);
+ align_view(doc, view, Align::Center);
}
} else if stdin().is_tty() {
editor.new_file(Action::VerticalSplit);
@@ -197,7 +222,6 @@ impl Application {
loop {
if self.editor.should_close() {
- self.jobs.finish();
break;
}
@@ -328,7 +352,7 @@ impl Application {
None => return,
};
match payload {
- Payload::Event(ev) => match ev {
+ Payload::Event(ev) => match *ev {
Event::Stopped(events::Stopped {
thread_id,
description,
@@ -529,12 +553,8 @@ impl Application {
// trigger textDocument/didOpen for docs that are already open
for doc in docs {
- // TODO: extract and share with editor.open
- let language_id = doc
- .language()
- .and_then(|s| s.split('.').last()) // source.rust
- .map(ToOwned::to_owned)
- .unwrap_or_default();
+ let language_id =
+ doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(),
@@ -549,6 +569,7 @@ impl Application {
let doc = self.editor.document_by_path_mut(&path);
if let Some(doc) = doc {
+ let lang_conf = doc.language_config();
let text = doc.text();
let diagnostics = params
@@ -586,19 +607,31 @@ impl Application {
return None;
};
+ let severity =
+ diagnostic.severity.map(|severity| match severity {
+ DiagnosticSeverity::ERROR => Error,
+ DiagnosticSeverity::WARNING => Warning,
+ DiagnosticSeverity::INFORMATION => Info,
+ DiagnosticSeverity::HINT => Hint,
+ severity => unreachable!(
+ "unrecognized diagnostic severity: {:?}",
+ severity
+ ),
+ });
+
+ if let Some(lang_conf) = lang_conf {
+ if let Some(severity) = severity {
+ if severity < lang_conf.diagnostic_severity {
+ return None;
+ }
+ }
+ };
+
Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
message: diagnostic.message,
- severity: diagnostic.severity.map(
- |severity| match severity {
- DiagnosticSeverity::ERROR => Error,
- DiagnosticSeverity::WARNING => Warning,
- DiagnosticSeverity::INFORMATION => Info,
- DiagnosticSeverity::HINT => Hint,
- severity => unimplemented!("{:?}", severity),
- },
- ),
+ severity,
// code
// source
})
@@ -705,14 +738,6 @@ impl Application {
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method, params, id, ..
}) => {
- let language_server = match self.editor.language_servers.get_by_id(server_id) {
- Some(language_server) => language_server,
- None => {
- warn!("can't find language server with id `{}`", server_id);
- return;
- }
- };
-
let call = match MethodCall::parse(&method, params) {
Some(call) => call,
None => {
@@ -742,8 +767,42 @@ impl Application {
if spinner.is_stopped() {
spinner.start();
}
+ let language_server =
+ match self.editor.language_servers.get_by_id(server_id) {
+ Some(language_server) => language_server,
+ None => {
+ warn!("can't find language server with id `{}`", server_id);
+ return;
+ }
+ };
+
tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
}
+ MethodCall::ApplyWorkspaceEdit(params) => {
+ apply_workspace_edit(
+ &mut self.editor,
+ helix_lsp::OffsetEncoding::Utf8,
+ &params.edit,
+ );
+
+ let language_server =
+ match self.editor.language_servers.get_by_id(server_id) {
+ Some(language_server) => language_server,
+ None => {
+ warn!("can't find language server with id `{}`", server_id);
+ return;
+ }
+ };
+
+ tokio::spawn(language_server.reply(
+ id,
+ Ok(json!(lsp::ApplyWorkspaceEditResponse {
+ applied: true,
+ failure_reason: None,
+ failed_change: None,
+ })),
+ ));
+ }
}
}
e => unreachable!("{:?}", e),
@@ -789,6 +848,8 @@ impl Application {
self.event_loop().await;
+ self.jobs.finish().await;
+
if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown");
};
diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs
index 40113db9..247d5b32 100644
--- a/helix-term/src/args.rs
+++ b/helix-term/src/args.rs
@@ -1,5 +1,6 @@
use anyhow::{Error, Result};
-use std::path::PathBuf;
+use helix_core::Position;
+use std::path::{Path, PathBuf};
#[derive(Default)]
pub struct Args {
@@ -7,7 +8,7 @@ pub struct Args {
pub display_version: bool,
pub load_tutor: bool,
pub verbosity: u64,
- pub files: Vec<PathBuf>,
+ pub files: Vec<(PathBuf, Position)>,
}
impl Args {
@@ -41,15 +42,49 @@ impl Args {
}
}
}
- arg => args.files.push(PathBuf::from(arg)),
+ arg => args.files.push(parse_file(arg)),
}
}
// push the remaining args, if any to the files
- for filename in iter {
- args.files.push(PathBuf::from(filename));
+ for arg in iter {
+ args.files.push(parse_file(arg));
}
Ok(args)
}
}
+
+/// Parse arg into [`PathBuf`] and position.
+pub(crate) fn parse_file(s: &str) -> (PathBuf, Position) {
+ let def = || (PathBuf::from(s), Position::default());
+ if Path::new(s).exists() {
+ return def();
+ }
+ split_path_row_col(s)
+ .or_else(|| split_path_row(s))
+ .unwrap_or_else(def)
+}
+
+/// Split file.rs:10:2 into [`PathBuf`], row and col.
+///
+/// Does not validate if file.rs is a file or directory.
+fn split_path_row_col(s: &str) -> Option<(PathBuf, Position)> {
+ let mut s = s.rsplitn(3, ':');
+ let col: usize = s.next()?.parse().ok()?;
+ let row: usize = s.next()?.parse().ok()?;
+ let path = s.next()?.into();
+ let pos = Position::new(row.saturating_sub(1), col.saturating_sub(1));
+ Some((path, pos))
+}
+
+/// Split file.rs:10 into [`PathBuf`] and row.
+///
+/// Does not validate if file.rs is a file or directory.
+fn split_path_row(s: &str) -> Option<(PathBuf, Position)> {
+ let (row, path) = s.rsplit_once(':')?;
+ let row: usize = row.parse().ok()?;
+ let path = path.into();
+ let pos = Position::new(row.saturating_sub(1), 0);
+ Some((path, pos))
+}
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 1871c67e..677943e8 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -5,15 +5,17 @@ pub use dap::*;
use helix_core::{
comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
history::UndoKind,
+ increment::date_time::DateTimeIncrementor,
+ increment::{number::NumberIncrementor, Increment},
indent,
indent::IndentStyle,
line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
match_brackets,
movement::{self, Direction},
- numbers::NumberIncrementor,
object, pos_at_coords,
regex::{self, Regex, RegexBuilder},
- search, selection, surround, textobject,
+ search, selection, shellwords, surround, textobject,
+ tree_sitter::Node,
unicode::width::UnicodeWidthChar,
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
Transaction,
@@ -22,13 +24,15 @@ use helix_view::{
clipboard::ClipboardType,
document::{Mode, SCRATCH_BUFFER_NAME},
editor::{Action, Motion},
+ info::Info,
input::KeyEvent,
keyboard::KeyCode,
view::View,
Document, DocumentId, Editor, ViewId,
};
-use anyhow::{anyhow, bail, Context as _};
+use anyhow::{anyhow, bail, ensure, Context as _};
+use fuzzy_matcher::FuzzyMatcher;
use helix_lsp::{
block_on, lsp,
util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
@@ -38,14 +42,15 @@ use insert::*;
use movement::Movement;
use crate::{
+ args,
compositor::{self, Component, Compositor},
- ui::{self, FilePicker, Picker, Popup, Prompt, PromptEvent},
+ ui::{self, FilePicker, Popup, Prompt, PromptEvent},
};
use crate::job::{self, Job, Jobs};
use futures_util::{FutureExt, StreamExt};
-use std::num::NonZeroUsize;
use std::{collections::HashMap, fmt, future::Future};
+use std::{collections::HashSet, num::NonZeroUsize};
use std::{
borrow::Cow,
@@ -73,7 +78,7 @@ pub struct Context<'a> {
impl<'a> Context<'a> {
/// Push a new component onto the compositor.
pub fn push_layer(&mut self, component: Box<dyn Component>) {
- self.callback = Some(Box::new(|compositor: &mut Compositor| {
+ self.callback = Some(Box::new(|compositor: &mut Compositor, _| {
compositor.push(component)
}));
}
@@ -138,47 +143,76 @@ pub fn align_view(doc: &Document, view: &mut View, align: Align) {
view.offset.row = line.saturating_sub(relative);
}
-/// A command is composed of a static name, and a function that takes the current state plus a count,
-/// and does a side-effect on the state (usually by creating and applying a transaction).
-#[derive(Copy, Clone)]
-pub struct Command {
- name: &'static str,
- fun: fn(cx: &mut Context),
- doc: &'static str,
-}
-
-macro_rules! commands {
+/// A MappableCommand is either a static command like "jump_view_up" or a Typable command like
+/// :format. It causes a side-effect on the state (usually by creating and applying a transaction).
+/// Both of these types of commands can be mapped with keybindings in the config.toml.
+#[derive(Clone)]
+pub enum MappableCommand {
+ Typable {
+ name: String,
+ args: Vec<String>,
+ doc: String,
+ },
+ Static {
+ name: &'static str,
+ fun: fn(cx: &mut Context),
+ doc: &'static str,
+ },
+}
+
+macro_rules! static_commands {
( $($name:ident, $doc:literal,)* ) => {
$(
#[allow(non_upper_case_globals)]
- pub const $name: Self = Self {
+ pub const $name: Self = Self::Static {
name: stringify!($name),
fun: $name,
doc: $doc
};
)*
- pub const COMMAND_LIST: &'static [Self] = &[
+ pub const STATIC_COMMAND_LIST: &'static [Self] = &[
$( Self::$name, )*
];
}
}
-impl Command {
+impl MappableCommand {
pub fn execute(&self, cx: &mut Context) {
- (self.fun)(cx);
+ match &self {
+ Self::Typable { name, args, doc: _ } => {
+ let args: Vec<Cow<str>> = args.iter().map(Cow::from).collect();
+ if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) {
+ let mut cx = compositor::Context {
+ editor: cx.editor,
+ jobs: cx.jobs,
+ scroll: None,
+ };
+ if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) {
+ cx.editor.set_error(format!("{}", e));
+ }
+ }
+ }
+ Self::Static { fun, .. } => (fun)(cx),
+ }
}
- pub fn name(&self) -> &'static str {
- self.name
+ pub fn name(&self) -> &str {
+ match &self {
+ Self::Typable { name, .. } => name,
+ Self::Static { name, .. } => name,
+ }
}
- pub fn doc(&self) -> &'static str {
- self.doc
+ pub fn doc(&self) -> &str {
+ match &self {
+ Self::Typable { doc, .. } => doc,
+ Self::Static { doc, .. } => doc,
+ }
}
#[rustfmt::skip]
- commands!(
+ static_commands!(
no_op, "Do nothing",
move_char_left, "Move left",
move_char_right, "Move right",
@@ -240,6 +274,7 @@ impl Command {
change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
collapse_selection, "Collapse selection onto a single cursor",
flip_selections, "Flip selection cursor and anchor",
+ ensure_selections_forward, "Ensure the selection is in forward direction",
insert_mode, "Insert before selection",
append_mode, "Insert after selection (append)",
command_mode, "Enter command mode",
@@ -261,16 +296,17 @@ impl Command {
add_newline_below, "Add newline below",
goto_type_definition, "Goto type definition",
goto_implementation, "Goto implementation",
- goto_file_start, "Goto file start/line",
+ goto_file_start, "Goto line number <n> else file start",
goto_file_end, "Goto file end",
- goto_file, "Goto files in the selection",
- goto_file_hsplit, "Goto files in the selection in horizontal splits",
- goto_file_vsplit, "Goto files in the selection in vertical splits",
+ goto_file, "Goto files in selection",
+ goto_file_hsplit, "Goto files in selection (hsplit)",
+ goto_file_vsplit, "Goto files in selection (vsplit)",
goto_reference, "Goto references",
goto_window_top, "Goto window top",
- goto_window_middle, "Goto window middle",
+ goto_window_center, "Goto window center",
goto_window_bottom, "Goto window bottom",
goto_last_accessed_file, "Goto last accessed file",
+ goto_last_modified_file, "Goto last modified file",
goto_last_modification, "Goto last modification",
goto_line, "Goto line",
goto_last_line, "Goto last line",
@@ -333,8 +369,12 @@ impl Command {
rotate_selection_contents_forward, "Rotate selection contents forward",
rotate_selection_contents_backward, "Rotate selections contents backward",
expand_selection, "Expand selection to parent syntax node",
+ shrink_selection, "Shrink selection to previously expanded syntax node",
+ select_next_sibling, "Select the next sibling in the syntax tree",
+ select_prev_sibling, "Select the previous sibling in the syntax tree",
jump_forward, "Jump forward on jumplist",
jump_backward, "Jump backward on jumplist",
+ save_selection, "Save the current selection to the jumplist",
jump_view_right, "Jump to the split to the right",
jump_view_left, "Jump to the split to the left",
jump_view_up, "Jump to the split above",
@@ -382,36 +422,56 @@ impl Command {
rename_symbol, "Rename symbol",
increment, "Increment",
decrement, "Decrement",
+ record_macro, "Record macro",
+ replay_macro, "Replay macro",
);
}
-impl fmt::Debug for Command {
+impl fmt::Debug for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command { name, .. } = self;
- f.debug_tuple("Command").field(name).finish()
+ f.debug_tuple("MappableCommand")
+ .field(&self.name())
+ .finish()
}
}
-impl fmt::Display for Command {
+impl fmt::Display for MappableCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
- let Command { name, .. } = self;
- f.write_str(name)
+ f.write_str(self.name())
}
}
-impl std::str::FromStr for Command {
+impl std::str::FromStr for MappableCommand {
type Err = anyhow::Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
- Command::COMMAND_LIST
- .iter()
- .copied()
- .find(|cmd| cmd.name == s)
- .ok_or_else(|| anyhow!("No command named '{}'", s))
+ if let Some(suffix) = s.strip_prefix(':') {
+ let mut typable_command = suffix.split(' ').into_iter().map(|arg| arg.trim());
+ let name = typable_command
+ .next()
+ .ok_or_else(|| anyhow!("Expected typable command name"))?;
+ let args = typable_command
+ .map(|s| s.to_owned())
+ .collect::<Vec<String>>();
+ cmd::TYPABLE_COMMAND_MAP
+ .get(name)
+ .map(|cmd| MappableCommand::Typable {
+ name: cmd.name.to_owned(),
+ doc: format!(":{} {:?}", cmd.name, args),
+ args,
+ })
+ .ok_or_else(|| anyhow!("No TypableCommand named '{}'", s))
+ } else {
+ MappableCommand::STATIC_COMMAND_LIST
+ .iter()
+ .find(|cmd| cmd.name() == s)
+ .cloned()
+ .ok_or_else(|| anyhow!("No command named '{}'", s))
+ }
}
}
-impl<'de> Deserialize<'de> for Command {
+impl<'de> Deserialize<'de> for MappableCommand {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
@@ -421,9 +481,27 @@ impl<'de> Deserialize<'de> for Command {
}
}
-impl PartialEq for Command {
+impl PartialEq for MappableCommand {
fn eq(&self, other: &Self) -> bool {
- self.name() == other.name()
+ match (self, other) {
+ (
+ MappableCommand::Typable {
+ name: first_name, ..
+ },
+ MappableCommand::Typable {
+ name: second_name, ..
+ },
+ ) => first_name == second_name,
+ (
+ MappableCommand::Static {
+ name: first_name, ..
+ },
+ MappableCommand::Static {
+ name: second_name, ..
+ },
+ ) => first_name == second_name,
+ _ => false,
+ }
}
}
@@ -622,8 +700,15 @@ fn kill_to_line_end(cx: &mut Context) {
let selection = doc.selection(view.id).clone().transform(|range| {
let line = range.cursor_line(text);
- let pos = line_end_char_index(&text, line);
- range.put_cursor(text, pos, true)
+ let line_end_pos = line_end_char_index(&text, line);
+ let pos = range.cursor(text);
+
+ let mut new_range = range.put_cursor(text, line_end_pos, true);
+ // don't want to remove the line separator itself if the cursor doesn't reach the end of line.
+ if pos != line_end_pos {
+ new_range.head = line_end_pos;
+ }
+ new_range
});
delete_selection_insert_mode(doc, view, &selection);
}
@@ -736,7 +821,6 @@ fn align_selections(cx: &mut Context) {
});
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String {
@@ -770,8 +854,8 @@ fn goto_window(cx: &mut Context, align: Align) {
Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
Align::Bottom => last_line.saturating_sub(scrolloff + count),
}
- .min(last_line.saturating_sub(scrolloff))
- .max(view.offset.row + scrolloff);
+ .max(view.offset.row + scrolloff)
+ .min(last_line.saturating_sub(scrolloff));
let pos = doc.text().line_to_char(line);
@@ -782,7 +866,7 @@ fn goto_window_top(cx: &mut Context) {
goto_window(cx, Align::Top)
}
-fn goto_window_middle(cx: &mut Context) {
+fn goto_window_center(cx: &mut Context) {
goto_window(cx, Align::Center)
}
@@ -1139,7 +1223,6 @@ fn replace(cx: &mut Context) {
});
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
})
}
@@ -1157,7 +1240,6 @@ where
});
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn switch_case(cx: &mut Context) {
@@ -1222,16 +1304,23 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
.max(view.offset.row + scrolloff)
.min(last_line.saturating_sub(scrolloff));
- let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
+ // If cursor needs moving, replace primary selection
+ if line != cursor.row {
+ let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end
- let anchor = if doc.mode == Mode::Select {
- range.anchor
- } else {
- head
- };
+ let anchor = if doc.mode == Mode::Select {
+ range.anchor
+ } else {
+ head
+ };
- // TODO: only manipulate main selection
- doc.set_selection(view.id, Selection::single(anchor, head));
+ // replace primary selection with an empty selection at cursor pos
+ let prim_sel = Range::new(anchor, head);
+ let mut sel = doc.selection(view.id).clone();
+ let idx = sel.primary_index();
+ sel = sel.replace(idx, prim_sel);
+ doc.set_selection(view.id, sel);
+ }
}
fn page_up(cx: &mut Context) {
@@ -1389,6 +1478,7 @@ fn split_selection_on_newline(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
+#[allow(clippy::too_many_arguments)]
fn search_impl(
doc: &mut Document,
view: &mut View,
@@ -1397,6 +1487,7 @@ fn search_impl(
movement: Movement,
direction: Direction,
scrolloff: usize,
+ wrap_around: bool,
) {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
@@ -1422,16 +1513,22 @@ fn search_impl(
// use find_at to find the next match after the cursor, loop around the end
// Careful, `Regex` uses `bytes` as offsets, not character indices!
- let mat = match direction {
- Direction::Forward => regex
- .find_at(contents, start)
- .or_else(|| regex.find(contents)),
- Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| {
- offset = start;
- regex.find_iter(&contents[start..]).last()
- }),
+ let mut mat = match direction {
+ Direction::Forward => regex.find_at(contents, start),
+ Direction::Backward => regex.find_iter(&contents[..start]).last(),
};
- // TODO: message on wraparound
+
+ if wrap_around && mat.is_none() {
+ mat = match direction {
+ Direction::Forward => regex.find(contents),
+ Direction::Backward => {
+ offset = start;
+ regex.find_iter(&contents[start..]).last()
+ }
+ }
+ // TODO: message on wraparound
+ }
+
if let Some(mat) = mat {
let start = text.byte_to_char(mat.start() + offset);
let end = text.byte_to_char(mat.end() + offset);
@@ -1483,8 +1580,9 @@ fn rsearch(cx: &mut Context) {
fn searcher(cx: &mut Context, direction: Direction) {
let reg = cx.register.unwrap_or('/');
let scrolloff = cx.editor.config.scrolloff;
+ let wrap_around = cx.editor.config.search.wrap_around;
- let (_, doc) = current!(cx.editor);
+ let doc = doc!(cx.editor);
// TODO: could probably share with select_on_matches?
@@ -1516,6 +1614,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
Movement::Move,
direction,
scrolloff,
+ wrap_around,
);
},
);
@@ -1530,16 +1629,27 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
if let Some(query) = registers.read('/') {
let query = query.last().unwrap();
let contents = doc.text().slice(..).to_string();
- let case_insensitive = if cx.editor.config.smart_case {
+ let search_config = &cx.editor.config.search;
+ let case_insensitive = if search_config.smart_case {
!query.chars().any(char::is_uppercase)
} else {
false
};
+ let wrap_around = search_config.wrap_around;
if let Ok(regex) = RegexBuilder::new(query)
.case_insensitive(case_insensitive)
.build()
{
- search_impl(doc, view, &contents, &regex, movement, direction, scrolloff);
+ search_impl(
+ doc,
+ view,
+ &contents,
+ &regex,
+ movement,
+ direction,
+ scrolloff,
+ wrap_around,
+ );
} else {
// get around warning `mutable_borrow_reservation_conflict`
// which will be a hard error in the future
@@ -1571,14 +1681,14 @@ fn search_selection(cx: &mut Context) {
let query = doc.selection(view.id).primary().fragment(contents);
let regex = regex::escape(&query);
cx.editor.registers.get_mut('/').push(regex);
- let msg = format!("register '{}' set to '{}'", '\\', query);
+ let msg = format!("register '{}' set to '{}'", '/', query);
cx.editor.set_status(msg);
}
fn global_search(cx: &mut Context) {
let (all_matches_sx, all_matches_rx) =
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
- let smart_case = cx.editor.config.smart_case;
+ let smart_case = cx.editor.config.search.smart_case;
let file_picker_config = cx.editor.config.file_picker.clone();
let completions = search_completions(cx, None);
@@ -1789,7 +1899,6 @@ fn delete_selection_impl(cx: &mut Context, op: Operation) {
match op {
Operation::Delete => {
- doc.append_changes_to_history(view.id);
// exit select mode, if currently in select mode
exit_select_mode(cx);
}
@@ -1845,7 +1954,21 @@ fn flip_selections(cx: &mut Context) {
let selection = doc
.selection(view.id)
.clone()
- .transform(|range| Range::new(range.head, range.anchor));
+ .transform(|range| range.flip());
+ doc.set_selection(view.id, selection);
+}
+
+fn ensure_selections_forward(cx: &mut Context) {
+ let (view, doc) = current!(cx.editor);
+
+ let selection = doc
+ .selection(view.id)
+ .clone()
+ .transform(|r| match r.direction() {
+ Direction::Forward => r,
+ Direction::Backward => r.flip(),
+ });
+
doc.set_selection(view.id, selection);
}
@@ -1879,7 +2002,7 @@ fn append_mode(cx: &mut Context) {
if !last_range.is_empty() && last_range.head == end {
let transaction = Transaction::change(
doc.text(),
- std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]),
+ [(end, end, Some(doc.line_ending.as_str().into()))].into_iter(),
);
doc.apply(&transaction, view.id);
}
@@ -1893,7 +2016,7 @@ fn append_mode(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
-mod cmd {
+pub mod cmd {
use super::*;
use helix_view::editor::Action;
@@ -1905,13 +2028,13 @@ mod cmd {
pub aliases: &'static [&'static str],
pub doc: &'static str,
// params, flags, helper, completer
- pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>,
+ pub fun: fn(&mut compositor::Context, &[Cow<str>], PromptEvent) -> anyhow::Result<()>,
pub completer: Option<Completer>,
}
fn quit(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
// last view and we have unsaved changes
@@ -1926,7 +2049,7 @@ mod cmd {
fn force_quit(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor.close(view!(cx.editor).id);
@@ -1936,17 +2059,25 @@ mod cmd {
fn open(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let path = args.get(0).context("wrong argument count")?;
- let _ = cx.editor.open(path.into(), Action::Replace)?;
+ ensure!(!args.is_empty(), "wrong argument count");
+ for arg in args {
+ let (path, pos) = args::parse_file(arg);
+ let _ = cx.editor.open(path, Action::Replace)?;
+ let (view, doc) = current!(cx.editor);
+ let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
+ doc.set_selection(view.id, pos);
+ // does not affect opening a buffer without pos
+ align_view(doc, view, Align::Center);
+ }
Ok(())
}
fn buffer_close(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let view = view!(cx.editor);
@@ -1957,7 +2088,7 @@ mod cmd {
fn force_buffer_close(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let view = view!(cx.editor);
@@ -1966,15 +2097,12 @@ mod cmd {
Ok(())
}
- fn write_impl<P: AsRef<Path>>(
- cx: &mut compositor::Context,
- path: Option<P>,
- ) -> anyhow::Result<()> {
+ fn write_impl(cx: &mut compositor::Context, path: Option<&Cow<str>>) -> anyhow::Result<()> {
let jobs = &mut cx.jobs;
- let (_, doc) = current!(cx.editor);
+ let doc = doc_mut!(cx.editor);
if let Some(ref path) = path {
- doc.set_path(Some(path.as_ref()))
+ doc.set_path(Some(path.as_ref().as_ref()))
.context("invalid filepath")?;
}
if doc.path().is_none() {
@@ -2003,7 +2131,7 @@ mod cmd {
fn write(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())
@@ -2011,7 +2139,7 @@ mod cmd {
fn new_file(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor.new_file(Action::Replace);
@@ -2021,11 +2149,10 @@ mod cmd {
fn format(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
-
+ let doc = doc!(cx.editor);
if let Some(format) = doc.format() {
let callback =
make_format_callback(doc.id(), doc.version(), Modified::LeaveModified, format);
@@ -2036,7 +2163,7 @@ mod cmd {
}
fn set_indent_style(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
use IndentStyle::*;
@@ -2056,7 +2183,7 @@ mod cmd {
// Attempt to parse argument as an indent style.
let style = match args.get(0) {
Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
- Some(&"0") => Some(Tabs),
+ Some(Cow::Borrowed("0")) => Some(Tabs),
Some(arg) => arg
.parse::<u8>()
.ok()
@@ -2075,7 +2202,7 @@ mod cmd {
/// Sets or reports the current document's line ending setting.
fn set_line_ending(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
use LineEnding::*;
@@ -2119,7 +2246,7 @@ mod cmd {
fn earlier(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2135,7 +2262,7 @@ mod cmd {
fn later(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
@@ -2150,7 +2277,7 @@ mod cmd {
fn write_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())?;
@@ -2159,7 +2286,7 @@ mod cmd {
fn force_write_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_impl(cx, args.first())?;
@@ -2190,13 +2317,13 @@ mod cmd {
fn write_all_impl(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
quit: bool,
force: bool,
) -> anyhow::Result<()> {
let mut errors = String::new();
-
+ let jobs = &mut cx.jobs;
// save all documents
for doc in &mut cx.editor.documents.values_mut() {
if doc.path().is_none() {
@@ -2204,9 +2331,23 @@ mod cmd {
continue;
}
- // TODO: handle error.
- let handle = doc.save();
- cx.jobs.add(Job::new(handle).wait_before_exiting());
+ if !doc.is_modified() {
+ continue;
+ }
+
+ let fmt = doc.auto_format().map(|fmt| {
+ let shared = fmt.shared();
+ let callback = make_format_callback(
+ doc.id(),
+ doc.version(),
+ Modified::SetUnmodified,
+ shared.clone(),
+ );
+ jobs.callback(callback);
+ shared
+ });
+ let future = doc.format_and_save(fmt);
+ jobs.add(Job::new(future).wait_before_exiting());
}
if quit {
@@ -2226,7 +2367,7 @@ mod cmd {
fn write_all(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, false, false)
@@ -2234,7 +2375,7 @@ mod cmd {
fn write_all_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, true, false)
@@ -2242,18 +2383,13 @@ mod cmd {
fn force_write_all_quit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
write_all_impl(cx, args, event, true, true)
}
- fn quit_all_impl(
- editor: &mut Editor,
- _args: &[&str],
- _event: PromptEvent,
- force: bool,
- ) -> anyhow::Result<()> {
+ fn quit_all_impl(editor: &mut Editor, force: bool) -> anyhow::Result<()> {
if !force {
buffers_remaining_impl(editor)?;
}
@@ -2269,23 +2405,23 @@ mod cmd {
fn quit_all(
cx: &mut compositor::Context,
- args: &[&str],
- event: PromptEvent,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
) -> anyhow::Result<()> {
- quit_all_impl(&mut cx.editor, args, event, false)
+ quit_all_impl(cx.editor, false)
}
fn force_quit_all(
cx: &mut compositor::Context,
- args: &[&str],
- event: PromptEvent,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
) -> anyhow::Result<()> {
- quit_all_impl(&mut cx.editor, args, event, true)
+ quit_all_impl(cx.editor, true)
}
fn cquit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let exit_code = args
@@ -2294,95 +2430,110 @@ mod cmd {
.unwrap_or(1);
cx.editor.exit_code = exit_code;
- let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
- for view_id in views {
- cx.editor.close(view_id);
- }
+ quit_all_impl(cx.editor, false)
+ }
- Ok(())
+ fn force_cquit(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let exit_code = args
+ .first()
+ .and_then(|code| code.parse::<i32>().ok())
+ .unwrap_or(1);
+ cx.editor.exit_code = exit_code;
+
+ quit_all_impl(cx.editor, true)
}
fn theme(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let theme = args.first().context("theme not provided")?;
- cx.editor.set_theme_from_name(theme)
+ let theme = args.first().context("Theme not provided")?;
+ let theme = cx
+ .editor
+ .theme_loader
+ .load(theme)
+ .with_context(|| format!("Failed setting theme {}", theme))?;
+ let true_color = cx.editor.config.true_color || crate::true_color();
+ if !(true_color || theme.is_16_color()) {
+ bail!("Unsupported theme: theme requires true color support");
+ }
+ cx.editor.set_theme(theme);
+ Ok(())
}
fn yank_main_selection_to_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard)
+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard)
}
fn yank_joined_to_clipboard(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
- let separator = args
- .first()
- .copied()
- .unwrap_or_else(|| doc.line_ending.as_str());
- yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Clipboard)
+ let doc = doc!(cx.editor);
+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
+ let separator = args.first().unwrap_or(&default_sep);
+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard)
}
fn yank_main_selection_to_primary_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection)
+ yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection)
}
fn yank_joined_to_primary_clipboard(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
- let separator = args
- .first()
- .copied()
- .unwrap_or_else(|| doc.line_ending.as_str());
- yank_joined_to_clipboard_impl(&mut cx.editor, separator, ClipboardType::Selection)
+ let doc = doc!(cx.editor);
+ let default_sep = Cow::Borrowed(doc.line_ending.as_str());
+ let separator = args.first().unwrap_or(&default_sep);
+ yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection)
}
fn paste_clipboard_after(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
}
fn paste_clipboard_before(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1)
}
fn paste_primary_clipboard_after(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
}
fn paste_primary_clipboard_before(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection)
+ paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1)
}
fn replace_selections_with_clipboard_impl(
@@ -2409,7 +2560,7 @@ mod cmd {
fn replace_selections_with_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard)
@@ -2417,7 +2568,7 @@ mod cmd {
fn replace_selections_with_primary_clipboard(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
replace_selections_with_clipboard_impl(cx, ClipboardType::Selection)
@@ -2425,7 +2576,7 @@ mod cmd {
fn show_clipboard_provider(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
cx.editor
@@ -2435,12 +2586,13 @@ mod cmd {
fn change_current_directory(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let dir = helix_core::path::expand_tilde(
args.first()
.context("target directory not provided")?
+ .as_ref()
.as_ref(),
);
@@ -2458,7 +2610,7 @@ mod cmd {
fn show_current_directory(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let cwd = std::env::current_dir().context("Couldn't get the new working directory")?;
@@ -2470,10 +2622,10 @@ mod cmd {
/// Sets the [`Document`]'s encoding..
fn set_encoding(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- let (_, doc) = current!(cx.editor);
+ let doc = doc_mut!(cx.editor);
if let Some(label) = args.first() {
doc.set_encoding(label)
} else {
@@ -2486,7 +2638,7 @@ mod cmd {
/// Reload the [`Document`] from its source file.
fn reload(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (view, doc) = current!(cx.editor);
@@ -2495,7 +2647,7 @@ mod cmd {
fn tree_sitter_scopes(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let (view, doc) = current!(cx.editor);
@@ -2509,15 +2661,18 @@ mod cmd {
fn vsplit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let id = view!(cx.editor).doc;
- if let Some(path) = args.get(0) {
- cx.editor.open(path.into(), Action::VerticalSplit)?;
- } else {
+ if args.is_empty() {
cx.editor.switch(id, Action::VerticalSplit);
+ } else {
+ for arg in args {
+ cx.editor
+ .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?;
+ }
}
Ok(())
@@ -2525,15 +2680,18 @@ mod cmd {
fn hsplit(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let id = view!(cx.editor).doc;
- if let Some(path) = args.get(0) {
- cx.editor.open(path.into(), Action::HorizontalSplit)?;
- } else {
+ if args.is_empty() {
cx.editor.switch(id, Action::HorizontalSplit);
+ } else {
+ for arg in args {
+ cx.editor
+ .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?;
+ }
}
Ok(())
@@ -2541,7 +2699,7 @@ mod cmd {
fn debug_eval(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
if let Some(debugger) = cx.editor.debugger.as_mut() {
@@ -2563,7 +2721,7 @@ mod cmd {
fn debug_start(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let mut args = args.to_owned();
@@ -2571,12 +2729,12 @@ mod cmd {
0 => None,
_ => Some(args.remove(0)),
};
- dap_start_impl(cx, name, None, Some(args))
+ dap_start_impl(cx, name.as_deref(), None, Some(args))
}
fn debug_remote(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let mut args = args.to_owned();
@@ -2588,12 +2746,12 @@ mod cmd {
0 => None,
_ => Some(args.remove(0)),
};
- dap_start_impl(cx, name, address, Some(args))
+ dap_start_impl(cx, name.as_deref(), address, Some(args))
}
fn tutor(
cx: &mut compositor::Context,
- _args: &[&str],
+ _args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
let path = helix_core::runtime_dir().join("tutor.txt");
@@ -2605,20 +2763,135 @@ mod cmd {
pub(super) fn goto_line_number(
cx: &mut compositor::Context,
- args: &[&str],
+ args: &[Cow<str>],
_event: PromptEvent,
) -> anyhow::Result<()> {
- if args.is_empty() {
- bail!("Line number required");
- }
+ ensure!(!args.is_empty(), "Line number required");
let line = args[0].parse::<usize>()?;
- goto_line_impl(&mut cx.editor, NonZeroUsize::new(line));
+ goto_line_impl(cx.editor, NonZeroUsize::new(line));
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, line);
+ Ok(())
+ }
+
+ fn setting(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let runtime_config = &mut cx.editor.config;
+
+ if args.len() != 2 {
+ anyhow::bail!("Bad arguments. Usage: `:set key field`");
+ }
+
+ let (key, arg) = (&args[0].to_lowercase(), &args[1]);
+
+ match key.as_ref() {
+ "scrolloff" => runtime_config.scrolloff = arg.parse()?,
+ "scroll-lines" => runtime_config.scroll_lines = arg.parse()?,
+ "mouse" => runtime_config.mouse = arg.parse()?,
+ "line-number" => runtime_config.line_number = arg.parse()?,
+ "middle-click_paste" => runtime_config.middle_click_paste = arg.parse()?,
+ "auto-pairs" => runtime_config.auto_pairs = arg.parse()?,
+ "auto-completion" => runtime_config.auto_completion = arg.parse()?,
+ "completion-trigger-len" => runtime_config.completion_trigger_len = arg.parse()?,
+ "auto-info" => runtime_config.auto_info = arg.parse()?,
+ "true-color" => runtime_config.true_color = arg.parse()?,
+ "search.smart-case" => runtime_config.search.smart_case = arg.parse()?,
+ "search.wrap-around" => runtime_config.search.wrap_around = arg.parse()?,
+ _ => anyhow::bail!("Unknown key `{}`.", args[0]),
+ }
+
+ Ok(())
+ }
+
+ fn sort(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ sort_impl(cx, args, false)
+ }
+
+ fn sort_reverse(
+ cx: &mut compositor::Context,
+ args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ sort_impl(cx, args, true)
+ }
+
+ fn sort_impl(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ reverse: bool,
+ ) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+
+ let selection = doc.selection(view.id);
+
+ let mut fragments: Vec<_> = selection
+ .fragments(text)
+ .map(|fragment| Tendril::from(fragment.as_ref()))
+ .collect();
+
+ fragments.sort_by(match reverse {
+ true => |a: &Tendril, b: &Tendril| b.cmp(a),
+ false => |a: &Tendril, b: &Tendril| a.cmp(b),
+ });
+
+ let transaction = Transaction::change(
+ doc.text(),
+ selection
+ .into_iter()
+ .zip(fragments)
+ .map(|(s, fragment)| (s.from(), s.to(), Some(fragment))),
+ );
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+
+ Ok(())
+ }
+
+ fn tree_sitter_subtree(
+ cx: &mut compositor::Context,
+ _args: &[Cow<str>],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ let (view, doc) = current!(cx.editor);
+
+ if let Some(syntax) = doc.syntax() {
+ let primary_selection = doc.selection(view.id).primary();
+ let text = doc.text();
+ let from = text.char_to_byte(primary_selection.from());
+ let to = text.char_to_byte(primary_selection.to());
+ if let Some(selected_node) = syntax
+ .tree()
+ .root_node()
+ .descendant_for_byte_range(from, to)
+ {
+ let contents = format!("```tsq\n{}\n```", selected_node.to_sexp());
+
+ let callback = async move {
+ let call: job::Callback =
+ Box::new(move |editor: &mut Editor, compositor: &mut Compositor| {
+ let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
+ let popup = Popup::new("hover", contents);
+ compositor.replace_or_push("hover", Box::new(popup));
+ });
+ Ok(call)
+ };
+
+ cx.jobs.callback(callback);
+ }
+ }
Ok(())
}
@@ -2646,18 +2919,18 @@ mod cmd {
completer: Some(completers::filename),
},
TypableCommand {
- name: "buffer-close",
- aliases: &["bc", "bclose"],
- doc: "Close the current buffer.",
- fun: buffer_close,
- completer: None, // FIXME: buffer completer
+ name: "buffer-close",
+ aliases: &["bc", "bclose"],
+ doc: "Close the current buffer.",
+ fun: buffer_close,
+ completer: None, // FIXME: buffer completer
},
TypableCommand {
- name: "buffer-close!",
- aliases: &["bc!", "bclose!"],
- doc: "Close the current buffer forcefully (ignoring unsaved changes).",
- fun: force_buffer_close,
- completer: None, // FIXME: buffer completer
+ name: "buffer-close!",
+ aliases: &["bc!", "bclose!"],
+ doc: "Close the current buffer forcefully (ignoring unsaved changes).",
+ fun: force_buffer_close,
+ completer: None, // FIXME: buffer completer
},
TypableCommand {
name: "write",
@@ -2676,7 +2949,7 @@ mod cmd {
TypableCommand {
name: "format",
aliases: &["fmt"],
- doc: "Format the file using a formatter.",
+ doc: "Format the file using the LSP formatter.",
fun: format,
completer: None,
},
@@ -2765,9 +3038,16 @@ mod cmd {
completer: None,
},
TypableCommand {
+ name: "cquit!",
+ aliases: &["cq!"],
+ doc: "Quit with exit code (default 1) forcefully (ignoring unsaved changes). Accepts an optional integer exit code (:cq! 2).",
+ fun: force_cquit,
+ completer: None,
+ },
+ TypableCommand {
name: "theme",
aliases: &[],
- doc: "Change the theme of current view. Requires theme name as argument (:theme <name>)",
+ doc: "Change the editor theme.",
fun: theme,
completer: Some(completers::theme),
},
@@ -2851,7 +3131,7 @@ mod cmd {
TypableCommand {
name: "change-current-directory",
aliases: &["cd"],
- doc: "Change the current working directory (:cd <dir>).",
+ doc: "Change the current working directory.",
fun: change_current_directory,
completer: Some(completers::directory),
},
@@ -2931,18 +3211,47 @@ mod cmd {
doc: "Go to line number.",
fun: goto_line_number,
completer: None,
- }
+ },
+ TypableCommand {
+ name: "set-option",
+ aliases: &["set"],
+ doc: "Set a config option at runtime",
+ fun: setting,
+ completer: Some(completers::setting),
+ },
+ TypableCommand {
+ name: "sort",
+ aliases: &[],
+ doc: "Sort ranges in selection.",
+ fun: sort,
+ completer: None,
+ },
+ TypableCommand {
+ name: "rsort",
+ aliases: &[],
+ doc: "Sort ranges in selection in reverse order.",
+ fun: sort_reverse,
+ completer: None,
+ },
+ TypableCommand {
+ name: "tree-sitter-subtree",
+ aliases: &["ts-subtree"],
+ doc: "Display tree sitter subtree under cursor, primarily for debugging queries.",
+ fun: tree_sitter_subtree,
+ completer: None,
+ },
];
- pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
- TYPABLE_COMMAND_LIST
- .iter()
- .flat_map(|cmd| {
- std::iter::once((cmd.name, cmd))
- .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
- })
- .collect()
- });
+ pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
+ Lazy::new(|| {
+ TYPABLE_COMMAND_LIST
+ .iter()
+ .flat_map(|cmd| {
+ std::iter::once((cmd.name, cmd))
+ .chain(cmd.aliases.iter().map(move |&alias| (alias, cmd)))
+ })
+ .collect()
+ });
}
fn command_mode(cx: &mut Context) {
@@ -2950,17 +3259,28 @@ fn command_mode(cx: &mut Context) {
":".into(),
Some(':'),
|input: &str| {
+ static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
+ Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default);
+
// we use .this over split_whitespace() because we care about empty segments
let parts = input.split(' ').collect::<Vec<&str>>();
// simple heuristic: if there's no just one part, complete command name.
// if there's a space, per command completion kicks in.
if parts.len() <= 1 {
- let end = 0..;
- cmd::TYPABLE_COMMAND_LIST
+ let mut matches: Vec<_> = cmd::TYPABLE_COMMAND_LIST
.iter()
- .filter(|command| command.name.contains(input))
- .map(|command| (end.clone(), Cow::Borrowed(command.name)))
+ .filter_map(|command| {
+ FUZZY_MATCHER
+ .fuzzy_match(command.name, input)
+ .map(|score| (command.name, score))
+ })
+ .collect();
+
+ matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score));
+ matches
+ .into_iter()
+ .map(|(name, _)| (0.., name.into()))
.collect()
} else {
let part = parts.last().unwrap();
@@ -2968,7 +3288,7 @@ fn command_mode(cx: &mut Context) {
if let Some(cmd::TypableCommand {
completer: Some(completer),
..
- }) = cmd::COMMANDS.get(parts[0])
+ }) = cmd::TYPABLE_COMMAND_MAP.get(parts[0])
{
completer(part)
.into_iter()
@@ -2996,15 +3316,25 @@ fn command_mode(cx: &mut Context) {
// If command is numeric, interpret as line number and go there.
if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
- if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) {
+ if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) {
cx.editor.set_error(format!("{}", e));
}
return;
}
// Handle typable commands
- if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
- if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
+ if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) {
+ let args = if cfg!(unix) {
+ shellwords::shellwords(input)
+ } else {
+ // Windows doesn't support POSIX, so fallback for now
+ parts
+ .into_iter()
+ .map(|part| part.into())
+ .collect::<Vec<_>>()
+ };
+
+ if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e));
}
} else {
@@ -3016,7 +3346,7 @@ fn command_mode(cx: &mut Context) {
prompt.doc_fn = Box::new(|input: &str| {
let part = input.split(' ').next().unwrap_or_default();
- if let Some(cmd::TypableCommand { doc, .. }) = cmd::COMMANDS.get(part) {
+ if let Some(cmd::TypableCommand { doc, .. }) = cmd::TYPABLE_COMMAND_MAP.get(part) {
return Some(doc);
}
@@ -3027,7 +3357,8 @@ fn command_mode(cx: &mut Context) {
}
fn file_picker(cx: &mut Context) {
- let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
+ // We don't specify language markers, root will be the root of the current git repo
+ let root = find_root(None, &[]).unwrap_or_else(|| PathBuf::from("./"));
let picker = ui::file_picker(root, &cx.editor.config);
cx.push_layer(Box::new(picker));
}
@@ -3084,8 +3415,8 @@ fn buffer_picker(cx: &mut Context) {
.map(|(_, doc)| new_meta(doc))
.collect(),
BufferMeta::format,
- |cx, meta, _action| {
- cx.editor.switch(meta.id, Action::Replace);
+ |cx, meta, action| {
+ cx.editor.switch(meta.id, action);
},
|editor, meta| {
let doc = &editor.documents.get(&meta.id)?;
@@ -3119,7 +3450,7 @@ fn symbol_picker(cx: &mut Context) {
nested_to_flat(list, file, child);
}
}
- let (_, doc) = current!(cx.editor);
+ let doc = doc!(cx.editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
@@ -3140,7 +3471,7 @@ fn symbol_picker(cx: &mut Context) {
let symbols = match symbols {
lsp::DocumentSymbolResponse::Flat(symbols) => symbols,
lsp::DocumentSymbolResponse::Nested(symbols) => {
- let (_view, doc) = current!(editor);
+ let doc = doc!(editor);
let mut flat_symbols = Vec::new();
for symbol in symbols {
nested_to_flat(&mut flat_symbols, &doc.identifier(), symbol)
@@ -3182,17 +3513,15 @@ fn symbol_picker(cx: &mut Context) {
}
fn workspace_symbol_picker(cx: &mut Context) {
- let (_, doc) = current!(cx.editor);
-
+ let doc = doc!(cx.editor);
+ let current_path = doc.path().cloned();
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => return,
};
let offset_encoding = language_server.offset_encoding();
-
let future = language_server.workspace_symbols("".to_string());
- let current_path = doc_mut!(cx.editor).path().cloned();
cx.callback(
future,
move |_editor: &mut Editor,
@@ -3243,6 +3572,15 @@ fn workspace_symbol_picker(cx: &mut Context) {
)
}
+impl ui::menu::Item for lsp::CodeActionOrCommand {
+ fn label(&self) -> &str {
+ match self {
+ lsp::CodeActionOrCommand::CodeAction(action) => action.title.as_str(),
+ lsp::CodeActionOrCommand::Command(command) => command.title.as_str(),
+ }
+ }
+}
+
pub fn code_action(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
@@ -3262,49 +3600,85 @@ pub fn code_action(cx: &mut Context) {
cx.callback(
future,
- move |_editor: &mut Editor,
+ move |editor: &mut Editor,
compositor: &mut Compositor,
response: Option<lsp::CodeActionResponse>| {
- if let Some(actions) = response {
- let picker = Picker::new(
- true,
- actions,
- |action| match action {
- lsp::CodeActionOrCommand::CodeAction(action) => {
- action.title.as_str().into()
- }
- lsp::CodeActionOrCommand::Command(command) => command.title.as_str().into(),
- },
- move |cx, code_action, _action| match code_action {
- lsp::CodeActionOrCommand::Command(command) => {
- log::debug!("code action command: {:?}", command);
- cx.editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
+ let actions = match response {
+ Some(a) => a,
+ None => return,
+ };
+ if actions.is_empty() {
+ editor.set_status("No code actions available".to_owned());
+ return;
+ }
+
+ let mut picker = ui::Menu::new(actions, move |editor, code_action, event| {
+ if event != PromptEvent::Validate {
+ return;
+ }
+
+ // always present here
+ let code_action = code_action.unwrap();
+
+ match code_action {
+ lsp::CodeActionOrCommand::Command(command) => {
+ log::debug!("code action command: {:?}", command);
+ execute_lsp_command(editor, command.clone());
+ }
+ lsp::CodeActionOrCommand::CodeAction(code_action) => {
+ log::debug!("code action: {:?}", code_action);
+ if let Some(ref workspace_edit) = code_action.edit {
+ log::debug!("edit: {:?}", workspace_edit);
+ apply_workspace_edit(editor, offset_encoding, workspace_edit);
}
- lsp::CodeActionOrCommand::CodeAction(code_action) => {
- log::debug!("code action: {:?}", code_action);
- if let Some(ref workspace_edit) = code_action.edit {
- apply_workspace_edit(cx.editor, offset_encoding, workspace_edit)
- }
+
+ // if code action provides both edit and command first the edit
+ // should be applied and then the command
+ if let Some(command) = &code_action.command {
+ execute_lsp_command(editor, command.clone());
}
- },
- );
- compositor.push(Box::new(picker))
- }
+ }
+ }
+ });
+ picker.move_down(); // pre-select the first item
+
+ let popup = Popup::new("code-action", picker).margin(helix_view::graphics::Margin {
+ vertical: 1,
+ horizontal: 1,
+ });
+ compositor.replace_or_push("code-action", Box::new(popup));
},
)
}
+pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) {
+ let doc = doc!(editor);
+ let language_server = match doc.language_server() {
+ Some(language_server) => language_server,
+ None => return,
+ };
+
+ // the command is executed on the server and communicated back
+ // to the client asynchronously using workspace edits
+ let command_future = language_server.command(cmd);
+ tokio::spawn(async move {
+ let res = command_future.await;
+
+ if let Err(e) = res {
+ log::error!("execute LSP command: {}", e);
+ }
+ });
+}
+
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
use lsp::ResourceOp;
use std::fs;
match op {
ResourceOp::Create(op) => {
let path = op.uri.to_file_path().unwrap();
- let ignore_if_exists = if let Some(options) = &op.options {
+ let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
- } else {
- false
- };
+ });
if ignore_if_exists && path.exists() {
Ok(())
} else {
@@ -3314,11 +3688,12 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
ResourceOp::Delete(op) => {
let path = op.uri.to_file_path().unwrap();
if path.is_dir() {
- let recursive = if let Some(options) = &op.options {
- options.recursive.unwrap_or(false)
- } else {
- false
- };
+ let recursive = op
+ .options
+ .as_ref()
+ .and_then(|options| options.recursive)
+ .unwrap_or(false);
+
if recursive {
fs::remove_dir_all(&path)
} else {
@@ -3333,11 +3708,9 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
ResourceOp::Rename(op) => {
let from = op.old_uri.to_file_path().unwrap();
let to = op.new_uri.to_file_path().unwrap();
- let ignore_if_exists = if let Some(options) = &op.options {
+ let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
- } else {
- false
- };
+ });
if ignore_if_exists && to.exists() {
Ok(())
} else {
@@ -3347,7 +3720,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
}
}
-fn apply_workspace_edit(
+pub fn apply_workspace_edit(
editor: &mut Editor,
offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit,
@@ -3454,7 +3827,7 @@ fn apply_workspace_edit(
fn last_picker(cx: &mut Context) {
// TODO: last picker does not seem to work well with buffer_picker
- cx.callback = Some(Box::new(|compositor: &mut Compositor| {
+ cx.callback = Some(Box::new(|compositor: &mut Compositor, _| {
if let Some(picker) = compositor.last_picker.take() {
compositor.push(picker);
}
@@ -3538,22 +3911,22 @@ fn open(cx: &mut Context, open: Open) {
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
- let line = range.cursor_line(text);
+ let cursor_line = range.cursor_line(text);
- let line = match open {
+ let new_line = match open {
// adjust position to the end of the line (next line - 1)
- Open::Below => line + 1,
+ Open::Below => cursor_line + 1,
// adjust position to the end of the previous line (current line - 1)
- Open::Above => line,
+ Open::Above => cursor_line,
};
// Index to insert newlines after, as well as the char width
// to use to compensate for those inserted newlines.
- let (line_end_index, line_end_offset_width) = if line == 0 {
+ let (line_end_index, line_end_offset_width) = if new_line == 0 {
(0, 0)
} else {
(
- line_end_char_index(&doc.text().slice(..), line.saturating_sub(1)),
+ line_end_char_index(&doc.text().slice(..), new_line.saturating_sub(1)),
doc.line_ending.len_chars(),
)
};
@@ -3564,8 +3937,10 @@ fn open(cx: &mut Context, open: Open) {
doc.syntax(),
text,
line_end_index,
+ new_line.saturating_sub(1),
true,
- );
+ )
+ .unwrap_or_else(|| indent::indent_level_for_line(text.line(cursor_line), doc.tab_width()));
let indent = doc.indent_unit().repeat(indent_level);
let indent_len = indent.len();
let mut text = String::with_capacity(1 + indent_len);
@@ -3611,7 +3986,7 @@ fn normal_mode(cx: &mut Context) {
doc.mode = Mode::Normal;
- doc.append_changes_to_history(view.id);
+ try_restore_indent(doc, view.id);
// if leaving append mode, move cursor back by 1
if doc.restore_cursor {
@@ -3628,6 +4003,40 @@ fn normal_mode(cx: &mut Context) {
}
}
+fn try_restore_indent(doc: &mut Document, view_id: ViewId) {
+ use helix_core::chars::char_is_whitespace;
+ use helix_core::Operation;
+
+ fn inserted_a_new_blank_line(changes: &[Operation], pos: usize, line_end_pos: usize) -> bool {
+ if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] =
+ changes
+ {
+ move_pos + inserted_str.len() == pos
+ && inserted_str.starts_with('\n')
+ && inserted_str.chars().skip(1).all(char_is_whitespace)
+ && pos == line_end_pos // ensure no characters exists after current position
+ } else {
+ false
+ }
+ }
+
+ let doc_changes = doc.changes().changes();
+ let text = doc.text().slice(..);
+ let range = doc.selection(view_id).primary();
+ let pos = range.cursor(text);
+ let line_end_pos = line_end_char_index(&text, range.cursor_line(text));
+
+ if inserted_a_new_blank_line(doc_changes, pos, line_end_pos) {
+ // Removes tailing whitespaces.
+ let transaction =
+ Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
+ let line_start_pos = text.line_to_char(range.cursor_line(text));
+ (line_start_pos, pos, None)
+ });
+ doc.apply(&transaction, view_id);
+ }
+}
+
// Store a jump on the jumplist.
fn push_jump(editor: &mut Editor) {
let (view, doc) = current!(editor);
@@ -3636,7 +4045,7 @@ fn push_jump(editor: &mut Editor) {
}
fn goto_line(cx: &mut Context) {
- goto_line_impl(&mut cx.editor, cx.count)
+ goto_line_impl(cx.editor, cx.count)
}
fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
@@ -3702,6 +4111,20 @@ fn goto_last_modification(cx: &mut Context) {
}
}
+fn goto_last_modified_file(cx: &mut Context) {
+ let view = view!(cx.editor);
+ let alternate_file = view
+ .last_modified_docs
+ .into_iter()
+ .flatten()
+ .find(|&id| id != view.doc);
+ if let Some(alt) = alternate_file {
+ cx.editor.switch(alt, Action::Replace);
+ } else {
+ cx.editor.set_error("no last modified buffer".to_owned())
+ }
+}
+
fn select_mode(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -3979,27 +4402,21 @@ fn goto_pos(editor: &mut Editor, pos: usize) {
}
fn goto_first_diag(cx: &mut Context) {
- let editor = &mut cx.editor;
- let (_, doc) = current!(editor);
-
+ let doc = doc!(cx.editor);
let pos = match doc.diagnostics().first() {
Some(diag) => diag.range.start,
None => return,
};
-
- goto_pos(editor, pos);
+ goto_pos(cx.editor, pos);
}
fn goto_last_diag(cx: &mut Context) {
- let editor = &mut cx.editor;
- let (_, doc) = current!(editor);
-
+ let doc = doc!(cx.editor);
let pos = match doc.diagnostics().last() {
Some(diag) => diag.range.start,
None => return,
};
-
- goto_pos(editor, pos);
+ goto_pos(cx.editor, pos);
}
fn goto_next_diag(cx: &mut Context) {
@@ -4089,7 +4506,6 @@ fn signature_help(cx: &mut Context) {
);
}
-// NOTE: Transactions in this module get appended to history when we switch back to normal mode.
pub mod insert {
use super::*;
pub type Hook = fn(&Rope, &Selection, char) -> Option<Transaction>;
@@ -4184,8 +4600,10 @@ pub mod insert {
// The default insert hook: simply insert the character
#[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature
fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option<Transaction> {
- let t = Tendril::from_char(ch);
- let transaction = Transaction::insert(doc, selection, t);
+ let cursors = selection.clone().cursors(doc.slice(..));
+ let mut t = Tendril::new();
+ t.push(ch);
+ let transaction = Transaction::insert(doc, &cursors, t);
Some(transaction)
}
@@ -4200,11 +4618,11 @@ pub mod insert {
};
let text = doc.text();
- let selection = doc.selection(view.id).clone().cursors(text.slice(..));
+ let selection = doc.selection(view.id);
// run through insert hooks, stopping on the first one that returns Some(t)
for hook in hooks {
- if let Some(transaction) = hook(text, &selection, c) {
+ if let Some(transaction) = hook(text, selection, c) {
doc.apply(&transaction, view.id);
break;
}
@@ -4254,48 +4672,48 @@ pub mod insert {
};
let curr = contents.get_char(pos).unwrap_or(' ');
- // TODO: offset range.head by 1? when calculating?
+ let current_line = text.char_to_line(pos);
let indent_level = indent::suggested_indent_for_pos(
doc.language_config(),
doc.syntax(),
text,
- pos.saturating_sub(1),
+ pos,
+ current_line,
true,
- );
- let indent = doc.indent_unit().repeat(indent_level);
- let mut text = String::with_capacity(1 + indent.len());
- text.push_str(doc.line_ending.as_str());
- text.push_str(&indent);
+ )
+ .unwrap_or_else(|| {
+ indent::indent_level_for_line(text.line(current_line), doc.tab_width())
+ });
- let head = pos + offs + text.chars().count();
+ let indent = doc.indent_unit().repeat(indent_level);
+ let mut text = String::new();
+ // If we are between pairs (such as brackets), we want to insert an additional line which is indented one level more and place the cursor there
+ let new_head_pos = if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
+ let inner_indent = doc.indent_unit().repeat(indent_level + 1);
+ text.reserve_exact(2 + indent.len() + inner_indent.len());
+ text.push_str(doc.line_ending.as_str());
+ text.push_str(&inner_indent);
+ let new_head_pos = pos + offs + text.chars().count();
+ text.push_str(doc.line_ending.as_str());
+ text.push_str(&indent);
+ new_head_pos
+ } else {
+ text.reserve_exact(1 + indent.len());
+ text.push_str(doc.line_ending.as_str());
+ text.push_str(&indent);
+ pos + offs + text.chars().count()
+ };
// TODO: range replace or extend
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
// can be used with cx.mode to do replace or extend on most changes
- ranges.push(Range::new(
- if range.is_empty() {
- head
- } else {
- range.anchor + offs
- },
- head,
- ));
-
- // if between a bracket pair
- if helix_core::auto_pairs::PAIRS.contains(&(prev, curr)) {
- // another newline, indent the end bracket one level less
- let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
- text.push_str(doc.line_ending.as_str());
- text.push_str(&indent);
- }
-
+ ranges.push(Range::new(new_head_pos, new_head_pos));
offs += text.chars().count();
(pos, pos, Some(text.into()))
});
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
- //
doc.apply(&transaction, view.id);
}
@@ -4519,11 +4937,8 @@ fn yank_joined_to_clipboard_impl(
fn yank_joined_to_clipboard(cx: &mut Context) {
let line_ending = doc!(cx.editor).line_ending;
- let _ = yank_joined_to_clipboard_impl(
- &mut cx.editor,
- line_ending.as_str(),
- ClipboardType::Clipboard,
- );
+ let _ =
+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Clipboard);
exit_select_mode(cx);
}
@@ -4548,20 +4963,17 @@ fn yank_main_selection_to_clipboard_impl(
}
fn yank_main_selection_to_clipboard(cx: &mut Context) {
- let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard);
}
fn yank_joined_to_primary_clipboard(cx: &mut Context) {
let line_ending = doc!(cx.editor).line_ending;
- let _ = yank_joined_to_clipboard_impl(
- &mut cx.editor,
- line_ending.as_str(),
- ClipboardType::Selection,
- );
+ let _ =
+ yank_joined_to_clipboard_impl(cx.editor, line_ending.as_str(), ClipboardType::Selection);
}
fn yank_main_selection_to_primary_clipboard(cx: &mut Context) {
- let _ = yank_main_selection_to_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
+ let _ = yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection);
exit_select_mode(cx);
}
@@ -4576,11 +4988,12 @@ fn paste_impl(
doc: &mut Document,
view: &View,
action: Paste,
+ count: usize,
) -> Option<Transaction> {
let repeat = std::iter::repeat(
values
.last()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from(value.repeat(count)))
.unwrap(),
);
@@ -4595,7 +5008,7 @@ fn paste_impl(
let mut values = values
.iter()
.map(|value| REGEX.replace_all(value, doc.line_ending.as_str()))
- .map(|value| Tendril::from(value.as_ref()))
+ .map(|value| Tendril::from(value.as_ref().repeat(count)))
.chain(repeat);
let text = doc.text();
@@ -4615,7 +5028,7 @@ fn paste_impl(
// paste append
(Paste::After, false) => range.to(),
};
- (pos, pos, Some(values.next().unwrap()))
+ (pos, pos, values.next())
});
Some(transaction)
@@ -4625,13 +5038,14 @@ fn paste_clipboard_impl(
editor: &mut Editor,
action: Paste,
clipboard_type: ClipboardType,
+ count: usize,
) -> anyhow::Result<()> {
let (view, doc) = current!(editor);
match editor
.clipboard_provider
.get_contents(clipboard_type)
- .map(|contents| paste_impl(&[contents], doc, view, action))
+ .map(|contents| paste_impl(&[contents], doc, view, action, count))
{
Ok(Some(transaction)) => {
doc.apply(&transaction, view.id);
@@ -4644,22 +5058,43 @@ fn paste_clipboard_impl(
}
fn paste_clipboard_after(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Clipboard);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::After,
+ ClipboardType::Clipboard,
+ cx.count(),
+ );
}
fn paste_clipboard_before(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Clipboard);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::Before,
+ ClipboardType::Clipboard,
+ cx.count(),
+ );
}
fn paste_primary_clipboard_after(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::After, ClipboardType::Selection);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::After,
+ ClipboardType::Selection,
+ cx.count(),
+ );
}
fn paste_primary_clipboard_before(cx: &mut Context) {
- let _ = paste_clipboard_impl(&mut cx.editor, Paste::Before, ClipboardType::Selection);
+ let _ = paste_clipboard_impl(
+ cx.editor,
+ Paste::Before,
+ ClipboardType::Selection,
+ cx.count(),
+ );
}
fn replace_with_yanked(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
@@ -4669,12 +5104,12 @@ fn replace_with_yanked(cx: &mut Context) {
let repeat = std::iter::repeat(
values
.last()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from(&value.repeat(count)))
.unwrap(),
);
let mut values = values
.iter()
- .map(|value| Tendril::from_slice(value))
+ .map(|value| Tendril::from(&value.repeat(count)))
.chain(repeat);
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
@@ -4686,7 +5121,6 @@ fn replace_with_yanked(cx: &mut Context) {
});
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
}
}
@@ -4694,6 +5128,7 @@ fn replace_with_yanked(cx: &mut Context) {
fn replace_selections_with_clipboard_impl(
editor: &mut Editor,
clipboard_type: ClipboardType,
+ count: usize,
) -> anyhow::Result<()> {
let (view, doc) = current!(editor);
@@ -4701,7 +5136,11 @@ fn replace_selections_with_clipboard_impl(
Ok(contents) => {
let selection = doc.selection(view.id);
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
- (range.from(), range.to(), Some(contents.as_str().into()))
+ (
+ range.from(),
+ range.to(),
+ Some(contents.repeat(count).as_str().into()),
+ )
});
doc.apply(&transaction, view.id);
@@ -4713,38 +5152,38 @@ fn replace_selections_with_clipboard_impl(
}
fn replace_selections_with_clipboard(cx: &mut Context) {
- let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Clipboard);
+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count());
}
fn replace_selections_with_primary_clipboard(cx: &mut Context) {
- let _ = replace_selections_with_clipboard_impl(&mut cx.editor, ClipboardType::Selection);
+ let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count());
}
fn paste_after(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
if let Some(transaction) = registers
.read(reg_name)
- .and_then(|values| paste_impl(values, doc, view, Paste::After))
+ .and_then(|values| paste_impl(values, doc, view, Paste::After, count))
{
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
}
fn paste_before(cx: &mut Context) {
+ let count = cx.count();
let reg_name = cx.register.unwrap_or('"');
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
if let Some(transaction) = registers
.read(reg_name)
- .and_then(|values| paste_impl(values, doc, view, Paste::Before))
+ .and_then(|values| paste_impl(values, doc, view, Paste::Before, count))
{
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
}
@@ -4780,7 +5219,6 @@ fn indent(cx: &mut Context) {
}),
);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn unindent(cx: &mut Context) {
@@ -4820,7 +5258,6 @@ fn unindent(cx: &mut Context) {
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn format_selections(cx: &mut Context) {
@@ -4867,8 +5304,6 @@ fn format_selections(cx: &mut Context) {
// doc.apply(&transaction, view.id);
}
-
- doc.append_changes_to_history(view.id);
}
fn join_selections(cx: &mut Context) {
@@ -4911,7 +5346,6 @@ fn join_selections(cx: &mut Context) {
// .with_selection(selection);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
@@ -5039,7 +5473,7 @@ pub fn completion(cx: &mut Context) {
move |editor: &mut Editor,
compositor: &mut Compositor,
response: Option<lsp::CompletionResponse>| {
- let (_, doc) = current!(editor);
+ let doc = doc!(editor);
if doc.mode() != Mode::Insert {
// we're not in insert mode anymore
return;
@@ -5136,9 +5570,10 @@ fn hover(cx: &mut Context) {
// skip if contents empty
- let contents = ui::Markdown::new(contents, editor.syn_loader.clone());
- let popup = Popup::new(contents);
- compositor.push(Box::new(popup));
+ let contents =
+ ui::Markdown::new(contents, editor.syn_loader.clone()).style_group("hover");
+ let popup = Popup::new("hover", contents);
+ compositor.replace_or_push("hover", Box::new(popup));
}
},
);
@@ -5154,7 +5589,6 @@ fn toggle_comments(cx: &mut Context) {
let transaction = comment::toggle_line_comments(doc.text(), doc.selection(view.id), token);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
exit_select_mode(cx);
}
@@ -5185,7 +5619,7 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
let selection = doc.selection(view.id);
let mut fragments: Vec<_> = selection
.fragments(text)
- .map(|fragment| Tendril::from_slice(&fragment))
+ .map(|fragment| Tendril::from(fragment.as_ref()))
.collect();
let group = count
@@ -5211,8 +5645,8 @@ fn rotate_selection_contents(cx: &mut Context, direction: Direction) {
);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
+
fn rotate_selection_contents_forward(cx: &mut Context) {
rotate_selection_contents(cx, Direction::Forward)
}
@@ -5228,14 +5662,73 @@ fn expand_selection(cx: &mut Context) {
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
- let selection = object::expand_selection(syntax, text, doc.selection(view.id));
+
+ let current_selection = doc.selection(view.id);
+
+ // save current selection so it can be restored using shrink_selection
+ view.object_selections.push(current_selection.clone());
+
+ let selection = object::expand_selection(syntax, text, current_selection.clone());
+ doc.set_selection(view.id, selection);
+ }
+ };
+ motion(cx.editor);
+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
+}
+
+fn shrink_selection(cx: &mut Context) {
+ let motion = |editor: &mut Editor| {
+ let (view, doc) = current!(editor);
+ let current_selection = doc.selection(view.id);
+ // try to restore previous selection
+ if let Some(prev_selection) = view.object_selections.pop() {
+ if current_selection.contains(&prev_selection) {
+ // allow shrinking the selection only if current selection contains the previous object selection
+ doc.set_selection(view.id, prev_selection);
+ return;
+ } else {
+ // clear existing selection as they can't be shrinked to anyway
+ view.object_selections.clear();
+ }
+ }
+ // if not previous selection, shrink to first child
+ if let Some(syntax) = doc.syntax() {
+ let text = doc.text().slice(..);
+ let selection = object::shrink_selection(syntax, text, current_selection.clone());
doc.set_selection(view.id, selection);
}
};
- motion(&mut cx.editor);
+ motion(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(motion)));
}
+fn select_sibling_impl<F>(cx: &mut Context, sibling_fn: &'static F)
+where
+ F: Fn(Node) -> Option<Node>,
+{
+ let motion = |editor: &mut Editor| {
+ let (view, doc) = current!(editor);
+
+ if let Some(syntax) = doc.syntax() {
+ let text = doc.text().slice(..);
+ let current_selection = doc.selection(view.id);
+ let selection =
+ object::select_sibling(syntax, text, current_selection.clone(), sibling_fn);
+ doc.set_selection(view.id, selection);
+ }
+ };
+ motion(cx.editor);
+ cx.editor.last_motion = Some(Motion(Box::new(motion)));
+}
+
+fn select_next_sibling(cx: &mut Context) {
+ select_sibling_impl(cx, &|node| Node::next_sibling(&node))
+}
+
+fn select_prev_sibling(cx: &mut Context) {
+ select_sibling_impl(cx, &|node| Node::prev_sibling(&node))
+}
+
fn match_brackets(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
@@ -5288,6 +5781,12 @@ fn jump_backward(cx: &mut Context) {
};
}
+fn save_selection(cx: &mut Context) {
+ push_jump(cx.editor);
+ cx.editor
+ .set_status("Selection saved to jumplist".to_owned());
+}
+
fn rotate_view(cx: &mut Context) {
cx.editor.focus_next()
}
@@ -5358,8 +5857,10 @@ fn wonly(cx: &mut Context) {
}
fn select_register(cx: &mut Context) {
+ cx.editor.autoinfo = Some(Info::from_registers(&cx.editor.registers));
cx.on_next_key(move |cx, event| {
if let Some(ch) = event.char() {
+ cx.editor.autoinfo = None;
cx.editor.selected_register = Some(ch);
}
})
@@ -5464,7 +5965,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
});
doc.set_selection(view.id, selection);
};
- textobject(&mut cx.editor);
+ textobject(cx.editor);
cx.editor.last_motion = Some(Motion(Box::new(textobject)));
}
})
@@ -5479,13 +5980,16 @@ fn surround_add(cx: &mut Context) {
let mut changes = Vec::with_capacity(selection.len() * 2);
for range in selection.iter() {
- changes.push((range.from(), range.from(), Some(Tendril::from_char(open))));
- changes.push((range.to(), range.to(), Some(Tendril::from_char(close))));
+ let mut o = Tendril::new();
+ o.push(open);
+ let mut c = Tendril::new();
+ c.push(close);
+ changes.push((range.from(), range.from(), Some(o)));
+ changes.push((range.to(), range.to(), Some(c)));
}
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
})
}
@@ -5510,15 +6014,12 @@ fn surround_replace(cx: &mut Context) {
let transaction = Transaction::change(
doc.text(),
change_pos.iter().enumerate().map(|(i, &pos)| {
- (
- pos,
- pos + 1,
- Some(Tendril::from_char(if i % 2 == 0 { open } else { close })),
- )
+ let mut t = Tendril::new();
+ t.push(if i % 2 == 0 { open } else { close });
+ (pos, pos + 1, Some(t))
}),
);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
});
}
@@ -5541,7 +6042,6 @@ fn surround_delete(cx: &mut Context) {
let transaction =
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
})
}
@@ -5630,9 +6130,7 @@ fn shell_impl(
) -> anyhow::Result<(Tendril, bool)> {
use std::io::Write;
use std::process::{Command, Stdio};
- if shell.is_empty() {
- bail!("No shell set");
- }
+ ensure!(!shell.is_empty(), "No shell set");
let mut process = match Command::new(&shell[0])
.args(&shell[1..])
@@ -5658,8 +6156,9 @@ fn shell_impl(
log::error!("Shell error: {}", String::from_utf8_lossy(&output.stderr));
}
- let tendril = Tendril::try_from_byte_slice(&output.stdout)
+ let str = std::str::from_utf8(&output.stdout)
.map_err(|_| anyhow!("Process did not output valid UTF-8"))?;
+ let tendril = Tendril::from(str);
Ok((tendril, output.status.success()))
}
@@ -5714,7 +6213,6 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
if behavior != ShellBehavior::Ignore {
let transaction = Transaction::change(doc.text(), changes.into_iter());
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
// after replace cursor may be out of bounds, do this to
@@ -5762,7 +6260,6 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
let transaction = Transaction::change(text, changes);
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
fn rename_symbol(cx: &mut Context) {
@@ -5796,7 +6293,7 @@ fn rename_symbol(cx: &mut Context) {
let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
let edits = block_on(task).unwrap_or_default();
log::debug!("Edits from LSP: {:?}", edits);
- apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
+ apply_workspace_edit(cx.editor, offset_encoding, &edits);
},
);
cx.push_layer(Box::new(prompt));
@@ -5816,16 +6313,45 @@ fn decrement(cx: &mut Context) {
fn increment_impl(cx: &mut Context, amount: i64) {
let (view, doc) = current!(cx.editor);
let selection = doc.selection(view.id);
- let text = doc.text();
+ let text = doc.text().slice(..);
+
+ let changes: Vec<_> = selection
+ .ranges()
+ .iter()
+ .filter_map(|range| {
+ let incrementor: Box<dyn Increment> =
+ if let Some(incrementor) = DateTimeIncrementor::from_range(text, *range) {
+ Box::new(incrementor)
+ } else if let Some(incrementor) = NumberIncrementor::from_range(text, *range) {
+ Box::new(incrementor)
+ } else {
+ return None;
+ };
- let changes = selection.ranges().iter().filter_map(|range| {
- let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
- let new_text = incrementor.incremented_text(amount);
- Some((
- incrementor.range.from(),
- incrementor.range.to(),
- Some(new_text),
- ))
+ let (range, new_text) = incrementor.increment(amount);
+
+ Some((range.from(), range.to(), Some(new_text)))
+ })
+ .collect();
+
+ // Overlapping changes in a transaction will panic, so we need to find and remove them.
+ // For example, if there are cursors on each of the year, month, and day of `2021-11-29`,
+ // incrementing will give overlapping changes, with each change incrementing a different part of
+ // the date. Since these conflict with each other we remove these changes from the transaction
+ // so nothing happens.
+ let mut overlapping_indexes = HashSet::new();
+ for (i, changes) in changes.windows(2).enumerate() {
+ if changes[0].1 > changes[1].0 {
+ overlapping_indexes.insert(i);
+ overlapping_indexes.insert(i + 1);
+ }
+ }
+ let changes = changes.into_iter().enumerate().filter_map(|(i, change)| {
+ if overlapping_indexes.contains(&i) {
+ None
+ } else {
+ Some(change)
+ }
});
if changes.clone().count() > 0 {
@@ -5833,6 +6359,58 @@ fn increment_impl(cx: &mut Context, amount: i64) {
let transaction = transaction.with_selection(selection.clone());
doc.apply(&transaction, view.id);
- doc.append_changes_to_history(view.id);
}
}
+
+fn record_macro(cx: &mut Context) {
+ if let Some((reg, mut keys)) = cx.editor.macro_recording.take() {
+ // Remove the keypress which ends the recording
+ keys.pop();
+ let s = keys
+ .into_iter()
+ .map(|key| {
+ let s = key.to_string();
+ if s.chars().count() == 1 {
+ s
+ } else {
+ format!("<{}>", s)
+ }
+ })
+ .collect::<String>();
+ cx.editor.registers.get_mut(reg).write(vec![s]);
+ cx.editor
+ .set_status(format!("Recorded to register [{}]", reg));
+ } else {
+ let reg = cx.register.take().unwrap_or('@');
+ cx.editor.macro_recording = Some((reg, Vec::new()));
+ cx.editor
+ .set_status(format!("Recording to register [{}]", reg));
+ }
+}
+
+fn replay_macro(cx: &mut Context) {
+ let reg = cx.register.unwrap_or('@');
+ let keys: Vec<KeyEvent> = if let Some([keys_str]) = cx.editor.registers.read(reg) {
+ match helix_view::input::parse_macro(keys_str) {
+ Ok(keys) => keys,
+ Err(err) => {
+ cx.editor.set_error(format!("Invalid macro: {}", err));
+ return;
+ }
+ }
+ } else {
+ cx.editor.set_error(format!("Register [{}] empty", reg));
+ return;
+ };
+
+ let count = cx.count();
+ cx.callback = Some(Box::new(
+ move |compositor: &mut Compositor, cx: &mut compositor::Context| {
+ for _ in 0..count {
+ for &key in keys.iter() {
+ compositor.handle_event(crossterm::event::Event::Key(key.into()), cx);
+ }
+ }
+ },
+ ));
+}
diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs
index 58ef99f5..c73f9611 100644
--- a/helix-term/src/commands/dap.rs
+++ b/helix-term/src/commands/dap.rs
@@ -194,7 +194,7 @@ pub fn dap_start_impl(
cx: &mut compositor::Context,
name: Option<&str>,
socket: Option<std::net::SocketAddr>,
- params: Option<Vec<&str>>,
+ params: Option<Vec<std::borrow::Cow<str>>>,
) -> Result<(), anyhow::Error> {
let doc = doc!(cx.editor);
@@ -242,7 +242,7 @@ pub fn dap_start_impl(
let mut param = x.to_string();
if let Some(DebugConfigCompletion::Advanced(cfg)) = template.completion.get(i) {
if matches!(cfg.completion.as_deref(), Some("filename" | "directory")) {
- param = std::fs::canonicalize(x)
+ param = std::fs::canonicalize(x.as_ref())
.ok()
.and_then(|pb| pb.into_os_string().into_string().ok())
.unwrap_or_else(|| x.to_string());
@@ -408,7 +408,7 @@ fn debug_parameter_prompt(
cx,
Some(&config_name),
None,
- Some(params.iter().map(|x| x.as_str()).collect()),
+ Some(params.iter().map(|x| x.into()).collect()),
) {
cx.editor.set_error(e.to_string());
}
@@ -651,7 +651,7 @@ pub fn dap_variables(cx: &mut Context) {
}
let contents = Text::from(tui::text::Text::from(variables));
- let popup = Popup::new(contents);
+ let popup = Popup::new("dap-variables", contents);
cx.push_layer(Box::new(popup));
}
diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs
index 3a644750..dd7ebe1d 100644
--- a/helix-term/src/compositor.rs
+++ b/helix-term/src/compositor.rs
@@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
-pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
+pub type Callback = Box<dyn FnOnce(&mut Compositor, &mut Context)>;
// --> EventResult should have a callback that takes a context with methods like .popup(),
// .prompt() etc. That way we can abstract it from the renderer.
@@ -55,15 +55,20 @@ pub trait Component: Any + AnyComponent {
/// May be used by the parent component to compute the child area.
/// viewport is the maximum allowed area, and the child should stay within those bounds.
+ ///
+ /// The returned size might be larger than the viewport if the child is too big to fit.
+ /// In this case the parent can use the values to calculate scroll.
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
- // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context
- // that way render can use it
None
}
fn type_name(&self) -> &'static str {
std::any::type_name::<Self>()
}
+
+ fn id(&self) -> Option<&'static str> {
+ None
+ }
}
use anyhow::Error;
@@ -121,17 +126,32 @@ impl Compositor {
self.layers.push(layer);
}
+ /// Replace a component that has the given `id` with the new layer and if
+ /// no component is found, push the layer normally.
+ pub fn replace_or_push(&mut self, id: &'static str, layer: Box<dyn Component>) {
+ if let Some(component) = self.find_id(id) {
+ *component = layer;
+ } else {
+ self.push(layer)
+ }
+ }
+
pub fn pop(&mut self) -> Option<Box<dyn Component>> {
self.layers.pop()
}
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
+ // If it is a key event and a macro is being recorded, push the key event to the recording.
+ if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) {
+ keys.push(key.into());
+ }
+
// propagate events through the layers until we either find a layer that consumes it or we
// run out of layers (event bubbling)
for layer in self.layers.iter_mut().rev() {
match layer.handle_event(event, cx) {
EventResult::Consumed(Some(callback)) => {
- callback(self);
+ callback(self, cx);
return true;
}
EventResult::Consumed(None) => return true,
@@ -184,6 +204,14 @@ impl Compositor {
.find(|component| component.type_name() == type_name)
.and_then(|component| component.as_any_mut().downcast_mut())
}
+
+ pub fn find_id<T: 'static>(&mut self, id: &'static str) -> Option<&mut T> {
+ let type_name = std::any::type_name::<T>();
+ self.layers
+ .iter_mut()
+ .find(|component| component.type_name() == type_name && component.id() == Some(id))
+ .and_then(|component| component.as_any_mut().downcast_mut())
+ }
}
// View casting, taken straight from Cursive
diff --git a/helix-term/src/config.rs b/helix-term/src/config.rs
index 3745f871..6b8bbc1b 100644
--- a/helix-term/src/config.rs
+++ b/helix-term/src/config.rs
@@ -20,14 +20,18 @@ pub struct LspConfig {
pub display_messages: bool,
}
-#[test]
-fn parsing_keymaps_config_file() {
- use crate::keymap;
- use crate::keymap::Keymap;
- use helix_core::hashmap;
- use helix_view::document::Mode;
-
- let sample_keymaps = r#"
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn parsing_keymaps_config_file() {
+ use crate::keymap;
+ use crate::keymap::Keymap;
+ use helix_core::hashmap;
+ use helix_view::document::Mode;
+
+ let sample_keymaps = r#"
[keys.insert]
y = "move_line_down"
S-C-a = "delete_selection"
@@ -36,19 +40,20 @@ fn parsing_keymaps_config_file() {
A-F12 = "move_next_word_end"
"#;
- assert_eq!(
- toml::from_str::<Config>(sample_keymaps).unwrap(),
- Config {
- keys: Keymaps(hashmap! {
- Mode::Insert => Keymap::new(keymap!({ "Insert mode"
- "y" => move_line_down,
- "S-C-a" => delete_selection,
- })),
- Mode::Normal => Keymap::new(keymap!({ "Normal mode"
- "A-F12" => move_next_word_end,
- })),
- }),
- ..Default::default()
- }
- );
+ assert_eq!(
+ toml::from_str::<Config>(sample_keymaps).unwrap(),
+ Config {
+ keys: Keymaps(hashmap! {
+ Mode::Insert => Keymap::new(keymap!({ "Insert mode"
+ "y" => move_line_down,
+ "S-C-a" => delete_selection,
+ })),
+ Mode::Normal => Keymap::new(keymap!({ "Normal mode"
+ "A-F12" => move_next_word_end,
+ })),
+ }),
+ ..Default::default()
+ }
+ );
+ }
}
diff --git a/helix-term/src/job.rs b/helix-term/src/job.rs
index 4fa38174..a6a77021 100644
--- a/helix-term/src/job.rs
+++ b/helix-term/src/job.rs
@@ -22,8 +22,8 @@ pub struct Jobs {
}
impl Job {
- pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Job {
- Job {
+ pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Self {
+ Self {
future: f.map(|r| r.map(|()| None)).boxed(),
wait: false,
}
@@ -31,22 +31,22 @@ impl Job {
pub fn with_callback<F: Future<Output = anyhow::Result<Callback>> + Send + 'static>(
f: F,
- ) -> Job {
- Job {
+ ) -> Self {
+ Self {
future: f.map(|r| r.map(Some)).boxed(),
wait: false,
}
}
- pub fn wait_before_exiting(mut self) -> Job {
+ pub fn wait_before_exiting(mut self) -> Self {
self.wait = true;
self
}
}
impl Jobs {
- pub fn new() -> Jobs {
- Jobs::default()
+ pub fn new() -> Self {
+ Self::default()
}
pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
@@ -93,8 +93,8 @@ impl Jobs {
}
/// Blocks until all the jobs that need to be waited on are done.
- pub fn finish(&mut self) {
+ pub async fn finish(&mut self) {
let wait_futures = std::mem::take(&mut self.wait_futures);
- helix_lsp::block_on(wait_futures.for_each(|_| future::ready(())));
+ wait_futures.for_each(|_| future::ready(())).await
}
}
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index b317242d..e08d7e44 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -1,4 +1,4 @@
-pub use crate::commands::Command;
+pub use crate::commands::MappableCommand;
use crate::config::Config;
use helix_core::hashmap;
use helix_view::{document::Mode, info::Info, input::KeyEvent};
@@ -92,7 +92,7 @@ macro_rules! alt {
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
- $crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
+ $crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
};
(@trie
@@ -120,7 +120,7 @@ macro_rules! keymap {
_key,
keymap!(@trie $value)
);
- debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
+ assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key);
)+
)*
@@ -222,9 +222,8 @@ impl KeyTrieNode {
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect();
}
- Info::new(self.name(), body)
+ Info::from_keymap(self.name(), body)
}
-
/// Get a reference to the key trie node's order.
pub fn order(&self) -> &[KeyEvent] {
self.order.as_slice()
@@ -260,8 +259,8 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(untagged)]
pub enum KeyTrie {
- Leaf(Command),
- Sequence(Vec<Command>),
+ Leaf(MappableCommand),
+ Sequence(Vec<MappableCommand>),
Node(KeyTrieNode),
}
@@ -304,9 +303,9 @@ impl KeyTrie {
pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
- Matched(Command),
+ Matched(MappableCommand),
/// Matched a sequence of commands to execute.
- MatchedSequence(Vec<Command>),
+ MatchedSequence(Vec<MappableCommand>),
/// Key was not found in the root keymap
NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -344,7 +343,7 @@ pub struct Keymap {
impl Keymap {
pub fn new(root: KeyTrie) -> Self {
- Keymap {
+ Self {
root,
state: Vec::new(),
sticky: None,
@@ -368,7 +367,7 @@ impl Keymap {
/// key cancels pending keystrokes. If there are no pending keystrokes but a
/// sticky node is in use, it will be cleared.
pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
- if let key!(Esc) = key {
+ if key!(Esc) == key {
if !self.state.is_empty() {
return KeymapResult::new(
// Note that Esc is not included here
@@ -386,10 +385,10 @@ impl Keymap {
};
let trie = match trie_node.search(&[*first]) {
- Some(&KeyTrie::Leaf(cmd)) => {
- return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
+ Some(KeyTrie::Leaf(ref cmd)) => {
+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky())
}
- Some(&KeyTrie::Sequence(ref cmds)) => {
+ Some(KeyTrie::Sequence(ref cmds)) => {
return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
@@ -408,9 +407,9 @@ impl Keymap {
}
KeymapResult::new(KeymapResultKind::Pending(map.clone()), self.sticky())
}
- Some(&KeyTrie::Leaf(cmd)) => {
+ Some(&KeyTrie::Leaf(ref cmd)) => {
self.state.clear();
- return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
+ return KeymapResult::new(KeymapResultKind::Matched(cmd.clone()), self.sticky());
}
Some(&KeyTrie::Sequence(ref cmds)) => {
self.state.clear();
@@ -477,7 +476,7 @@ impl DerefMut for Keymaps {
}
impl Default for Keymaps {
- fn default() -> Keymaps {
+ fn default() -> Self {
let normal = keymap!({ "Normal mode"
"h" | "left" => move_char_left,
"j" | "down" => move_line_down,
@@ -521,9 +520,10 @@ impl Default for Keymaps {
"r" => goto_reference,
"i" => goto_implementation,
"t" => goto_window_top,
- "m" => goto_window_middle,
+ "c" => goto_window_center,
"b" => goto_window_bottom,
"a" => goto_last_accessed_file,
+ "m" => goto_last_modified_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
"." => goto_last_modification,
@@ -551,6 +551,11 @@ impl Default for Keymaps {
"S" => split_selection,
";" => collapse_selection,
"A-;" => flip_selections,
+ "A-k" => expand_selection,
+ "A-j" => shrink_selection,
+ "A-h" => select_prev_sibling,
+ "A-l" => select_next_sibling,
+
"%" => select_all,
"x" => extend_line,
"X" => extend_to_line_bounds,
@@ -592,6 +597,9 @@ impl Default for Keymaps {
// paste_all
"P" => paste_before,
+ "Q" => record_macro,
+ "q" => replay_macro,
+
">" => indent,
"<" => unindent,
"=" => format_selections,
@@ -613,6 +621,8 @@ impl Default for Keymaps {
"A-(" => rotate_selection_contents_backward,
"A-)" => rotate_selection_contents_forward,
+ "A-:" => ensure_selections_forward,
+
"esc" => normal_mode,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
@@ -640,7 +650,7 @@ impl Default for Keymaps {
"tab" => jump_forward, // tab == <C-i>
"C-o" => jump_backward,
- // "C-s" => save_selection,
+ "C-s" => save_selection,
"space" => { "Space"
"f" => file_picker,
@@ -763,8 +773,10 @@ impl Default for Keymaps {
"del" => delete_char_forward,
"C-d" => delete_char_forward,
"ret" => insert_newline,
+ "C-j" => insert_newline,
"tab" => insert_tab,
"C-w" => delete_word_backward,
+ "A-backspace" => delete_word_backward,
"A-d" => delete_word_forward,
"left" => move_char_left,
@@ -779,6 +791,8 @@ impl Default for Keymaps {
"A-left" => move_prev_word_end,
"A-f" => move_next_word_start,
"A-right" => move_next_word_start,
+ "A-<" => goto_file_start,
+ "A->" => goto_file_end,
"pageup" => page_up,
"pagedown" => page_down,
"home" => goto_line_start,
@@ -792,7 +806,7 @@ impl Default for Keymaps {
"C-x" => completion,
"C-r" => insert_register,
});
- Keymaps(hashmap!(
+ Self(hashmap!(
Mode::Normal => Keymap::new(normal),
Mode::Select => Keymap::new(select),
Mode::Insert => Keymap::new(insert),
@@ -852,36 +866,36 @@ mod tests {
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!(
keymap.get(key!('i')).kind,
- KeymapResultKind::Matched(Command::normal_mode),
+ KeymapResultKind::Matched(MappableCommand::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(key!('无')).kind,
- KeymapResultKind::Matched(Command::insert_mode),
+ KeymapResultKind::Matched(MappableCommand::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
keymap.get(key!('z')).kind,
- KeymapResultKind::Matched(Command::jump_backward),
+ KeymapResultKind::Matched(MappableCommand::jump_backward),
"Leaf should replace node"
);
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
- &KeyTrie::Leaf(Command::goto_line_end),
+ &KeyTrie::Leaf(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
- &KeyTrie::Leaf(Command::delete_char_forward),
+ &KeyTrie::Leaf(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
- &KeyTrie::Leaf(Command::goto_last_line),
+ &KeyTrie::Leaf(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
@@ -915,7 +929,7 @@ mod tests {
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
- &KeyTrie::Leaf(Command::vsplit),
+ &KeyTrie::Leaf(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs
index f5e3a8cd..58cb139c 100644
--- a/helix-term/src/lib.rs
+++ b/helix-term/src/lib.rs
@@ -9,3 +9,14 @@ pub mod config;
pub mod job;
pub mod keymap;
pub mod ui;
+
+#[cfg(not(windows))]
+fn true_color() -> bool {
+ std::env::var("COLORTERM")
+ .map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
+ .unwrap_or(false)
+}
+#[cfg(windows)]
+fn true_color() -> bool {
+ true
+}
diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs
index 88140130..0f504046 100644
--- a/helix-term/src/main.rs
+++ b/helix-term/src/main.rs
@@ -56,7 +56,7 @@ USAGE:
hx [FLAGS] [files]...
ARGS:
- <files>... Sets the input file to use
+ <files>... Sets the input file to use, position can also be specified via file[:row[:col]]
FLAGS:
-h, --help Prints help information
diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs
index dd782d29..35afe81e 100644
--- a/helix-term/src/ui/completion.rs
+++ b/helix-term/src/ui/completion.rs
@@ -154,8 +154,19 @@ impl Completion {
);
doc.apply(&transaction, view.id);
- if let Some(additional_edits) = &item.additional_text_edits {
- // gopls uses this to add extra imports
+ // apply additional edits, mostly used to auto import unqualified types
+ let resolved_additional_text_edits = if item.additional_text_edits.is_some() {
+ None
+ } else {
+ Self::resolve_completion_item(doc, item.clone())
+ .and_then(|item| item.additional_text_edits)
+ };
+
+ if let Some(additional_edits) = item
+ .additional_text_edits
+ .as_ref()
+ .or_else(|| resolved_additional_text_edits.as_ref())
+ {
if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits(
doc.text(),
@@ -168,7 +179,7 @@ impl Completion {
}
};
});
- let popup = Popup::new(menu);
+ let popup = Popup::new("completion", menu);
let mut completion = Self {
popup,
start_offset,
@@ -181,6 +192,31 @@ impl Completion {
completion
}
+ fn resolve_completion_item(
+ doc: &Document,
+ completion_item: lsp::CompletionItem,
+ ) -> Option<CompletionItem> {
+ let language_server = doc.language_server()?;
+ let completion_resolve_provider = language_server
+ .capabilities()
+ .completion_provider
+ .as_ref()?
+ .resolve_provider;
+ if completion_resolve_provider != Some(true) {
+ return None;
+ }
+
+ let future = language_server.resolve_completion_item(completion_item);
+ let response = helix_lsp::block_on(future);
+ match response {
+ Ok(completion_item) => Some(completion_item),
+ Err(err) => {
+ log::error!("execute LSP command: {}", err);
+ None
+ }
+ }
+ }
+
pub fn recompute_filter(&mut self, editor: &Editor) {
// recompute menu based on matches
let menu = self.popup.contents_mut();
@@ -268,6 +304,9 @@ impl Component for Completion {
let cursor_pos = doc.selection(view.id).primary().cursor(text);
let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
let cursor_pos = (coords.row - view.offset.row) as u16;
+
+ let markdown_ui =
+ |content, syn_loader| Markdown::new(content, syn_loader).style_group("completion");
let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
@@ -275,7 +314,7 @@ impl Component for Completion {
value: contents,
})) => {
// TODO: convert to wrapped text
- Markdown::new(
+ markdown_ui(
format!(
"```{}\n{}\n```\n{}",
language,
@@ -290,7 +329,7 @@ impl Component for Completion {
value: contents,
})) => {
// TODO: set language based on doc scope
- Markdown::new(
+ markdown_ui(
format!(
"```{}\n{}\n```\n{}",
language,
@@ -304,7 +343,7 @@ impl Component for Completion {
// TODO: copied from above
// TODO: set language based on doc scope
- Markdown::new(
+ markdown_ui(
format!(
"```{}\n{}\n```",
language,
@@ -328,8 +367,8 @@ impl Component for Completion {
let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
- width = rel_width;
- height = rel_height;
+ width = rel_width.min(width);
+ height = rel_height.min(height);
}
Rect::new(x, y, width, height)
} else {
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index ac11d298..a2131abe 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -7,8 +7,10 @@ use crate::{
};
use helix_core::{
- coords_at_pos,
- graphemes::{ensure_grapheme_boundary_next, next_grapheme_boundary, prev_grapheme_boundary},
+ coords_at_pos, encoding,
+ graphemes::{
+ ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
+ },
movement::Direction,
syntax::{self, HighlightEvent},
unicode::segmentation::UnicodeSegmentation,
@@ -17,8 +19,8 @@ use helix_core::{
};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
+ editor::CursorShapeConfig,
graphics::{CursorKind, Modifier, Rect, Style},
- info::Info,
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
@@ -31,10 +33,9 @@ use tui::buffer::Buffer as Surface;
pub struct EditorView {
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
- last_insert: (commands::Command, Vec<KeyEvent>),
+ last_insert: (commands::MappableCommand, Vec<KeyEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
- autoinfo: Option<Info>,
}
impl Default for EditorView {
@@ -48,10 +49,9 @@ impl EditorView {
Self {
keymaps,
on_next_key: None,
- last_insert: (commands::Command::normal_mode, Vec::new()),
+ last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
- autoinfo: None,
}
}
@@ -106,13 +106,12 @@ impl EditorView {
}
}
- let highlights =
- Self::doc_syntax_highlights(doc, view.offset, inner.height, theme, &editor.syn_loader);
+ let highlights = Self::doc_syntax_highlights(doc, view.offset, inner.height, theme);
let highlights = syntax::merge(highlights, Self::doc_diagnostics_highlights(doc, theme));
let highlights: Box<dyn Iterator<Item = HighlightEvent>> = if is_focused {
Box::new(syntax::merge(
highlights,
- Self::doc_selection_highlights(doc, view, theme),
+ Self::doc_selection_highlights(doc, view, theme, &editor.config.cursor_shape),
))
} else {
Box::new(highlights)
@@ -130,8 +129,7 @@ impl EditorView {
let x = area.right();
let border_style = theme.get("ui.window");
for y in area.top()..area.bottom() {
- surface
- .get_mut(x, y)
+ surface[(x, y)]
.set_symbol(tui::symbols::line::VERTICAL)
//.set_symbol(" ")
.set_style(border_style);
@@ -154,8 +152,7 @@ impl EditorView {
doc: &'doc Document,
offset: Position,
height: u16,
- theme: &Theme,
- loader: &syntax::Loader,
+ _theme: &Theme,
) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
let text = doc.text().slice(..);
let last_line = std::cmp::min(
@@ -172,48 +169,34 @@ impl EditorView {
start..end
};
- // TODO: range doesn't actually restrict source, just highlight range
- let highlights = match doc.syntax() {
+ match doc.syntax() {
Some(syntax) => {
- let scopes = theme.scopes();
- syntax
- .highlight_iter(text.slice(..), Some(range), None, |language| {
- loader.language_configuration_for_injection_string(language)
- .and_then(|language_config| {
- let config = language_config.highlight_config(scopes)?;
- let config_ref = config.as_ref();
- // SAFETY: the referenced `HighlightConfiguration` behind
- // the `Arc` is guaranteed to remain valid throughout the
- // duration of the highlight.
- let config_ref = unsafe {
- std::mem::transmute::<
- _,
- &'static syntax::HighlightConfiguration,
- >(config_ref)
- };
- Some(config_ref)
- })
- })
+ let iter = syntax
+ // TODO: range doesn't actually restrict source, just highlight range
+ .highlight_iter(text.slice(..), Some(range), None)
.map(|event| event.unwrap())
- .collect() // TODO: we collect here to avoid holding the lock, fix later
+ .map(move |event| match event {
+ // convert byte offsets to char offset
+ HighlightEvent::Source { start, end } => {
+ let start =
+ text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start));
+ let end =
+ text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end));
+ HighlightEvent::Source { start, end }
+ }
+ event => event,
+ });
+
+ Box::new(iter)
}
- None => vec![HighlightEvent::Source {
- start: range.start,
- end: range.end,
- }],
+ None => Box::new(
+ [HighlightEvent::Source {
+ start: text.byte_to_char(range.start),
+ end: text.byte_to_char(range.end),
+ }]
+ .into_iter(),
+ ),
}
- .into_iter()
- .map(move |event| match event {
- // convert byte offsets to char offset
- HighlightEvent::Source { start, end } => {
- let start = ensure_grapheme_boundary_next(text, text.byte_to_char(start));
- let end = ensure_grapheme_boundary_next(text, text.byte_to_char(end));
- HighlightEvent::Source { start, end }
- }
- event => event,
- });
-
- Box::new(highlights)
}
/// Get highlight spans for document diagnostics
@@ -245,11 +228,16 @@ impl EditorView {
doc: &Document,
view: &View,
theme: &Theme,
+ cursor_shape_config: &CursorShapeConfig,
) -> Vec<(usize, std::ops::Range<usize>)> {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
let primary_idx = selection.primary_index();
+ let mode = doc.mode();
+ let cursorkind = cursor_shape_config.from_mode(mode);
+ let cursor_is_block = cursorkind == CursorKind::Block;
+
let selection_scope = theme
.find_scope_index("ui.selection")
.expect("could not find `ui.selection` scope in the theme!");
@@ -257,7 +245,7 @@ impl EditorView {
.find_scope_index("ui.cursor")
.unwrap_or(selection_scope);
- let cursor_scope = match doc.mode() {
+ let cursor_scope = match mode {
Mode::Insert => theme.find_scope_index("ui.cursor.insert"),
Mode::Select => theme.find_scope_index("ui.cursor.select"),
Mode::Normal => Some(base_cursor_scope),
@@ -273,7 +261,8 @@ impl EditorView {
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
for (i, range) in selection.iter().enumerate() {
- let (cursor_scope, selection_scope) = if i == primary_idx {
+ let selection_is_primary = i == primary_idx;
+ let (cursor_scope, selection_scope) = if selection_is_primary {
(primary_cursor_scope, primary_selection_scope)
} else {
(cursor_scope, selection_scope)
@@ -281,7 +270,14 @@ impl EditorView {
// Special-case: cursor at end of the rope.
if range.head == range.anchor && range.head == text.len_chars() {
- spans.push((cursor_scope, range.head..range.head + 1));
+ if !selection_is_primary || cursor_is_block {
+ // Bar and underline cursors are drawn by the terminal
+ // BUG: If the editor area loses focus while having a bar or
+ // underline cursor (eg. when a regex prompt has focus) then
+ // the primary cursor will be invisible. This doesn't happen
+ // with block cursors since we manually draw *all* cursors.
+ spans.push((cursor_scope, range.head..range.head + 1));
+ }
continue;
}
@@ -290,11 +286,15 @@ impl EditorView {
// Standard case.
let cursor_start = prev_grapheme_boundary(text, range.head);
spans.push((selection_scope, range.anchor..cursor_start));
- spans.push((cursor_scope, cursor_start..range.head));
+ if !selection_is_primary || cursor_is_block {
+ spans.push((cursor_scope, cursor_start..range.head));
+ }
} else {
// Reverse case.
let cursor_end = next_grapheme_boundary(text, range.head);
- spans.push((cursor_scope, range.head..cursor_end));
+ if !selection_is_primary || cursor_is_block {
+ spans.push((cursor_scope, range.head..cursor_end));
+ }
spans.push((selection_scope, cursor_end..range.anchor));
}
}
@@ -320,6 +320,10 @@ impl EditorView {
let text_style = theme.get("ui.text");
+ // It's slightly more efficient to produce a full RopeSlice from the Rope, then slice that a bunch
+ // of times than it is to always call Rope::slice/get_slice (it will internally always hit RSEnum::Light).
+ let text = text.slice(..);
+
'outer: for event in highlights {
match event {
HighlightEvent::HighlightStart(span) => {
@@ -336,17 +340,16 @@ impl EditorView {
use helix_core::graphemes::{grapheme_width, RopeGraphemes};
- let style = spans.iter().fold(text_style, |acc, span| {
- let style = theme.get(theme.scopes()[span.0].as_str());
- acc.patch(style)
- });
-
for grapheme in RopeGraphemes::new(text) {
let out_of_bounds = visual_x < offset.col as u16
|| visual_x >= viewport.width + offset.col as u16;
if LineEnding::from_rope_slice(&grapheme).is_some() {
if !out_of_bounds {
+ let style = spans.iter().fold(text_style, |acc, span| {
+ acc.patch(theme.highlight(span.0))
+ });
+
// we still want to render an empty cell with the style
surface.set_string(
viewport.x + visual_x - offset.col as u16,
@@ -377,6 +380,10 @@ impl EditorView {
};
if !out_of_bounds {
+ let style = spans.iter().fold(text_style, |acc, span| {
+ acc.patch(theme.highlight(span.0))
+ });
+
// if we're offscreen just keep going until we hit a new line
surface.set_string(
viewport.x + visual_x - offset.col as u16,
@@ -422,8 +429,7 @@ impl EditorView {
.add_modifier(Modifier::DIM)
});
- surface
- .get_mut(viewport.x + pos.col as u16, viewport.y + pos.row as u16)
+ surface[(viewport.x + pos.col as u16, viewport.y + pos.row as u16)]
.set_style(style);
}
}
@@ -453,6 +459,8 @@ impl EditorView {
let mut offset = 0;
+ let gutter_style = theme.get("ui.gutter");
+
// avoid lots of small allocations by reusing a text buffer for each line
let mut text = String::with_capacity(8);
@@ -468,7 +476,7 @@ impl EditorView {
viewport.y + i as u16,
&text,
*width,
- style,
+ gutter_style.patch(style),
);
}
text.clear();
@@ -574,21 +582,6 @@ impl EditorView {
}
surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
- let rel_path = doc.relative_path();
- let path = rel_path
- .as_ref()
- .map(|p| p.to_string_lossy())
- .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
-
- let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
- surface.set_stringn(
- viewport.x + 8,
- viewport.y,
- title,
- viewport.width.saturating_sub(6) as usize,
- base_style,
- );
-
//-------------------------------
// Right side of the status line.
//-------------------------------
@@ -662,6 +655,13 @@ impl EditorView {
base_style,
));
+ let enc = doc.encoding();
+ if enc != encoding::UTF_8 {
+ right_side_text
+ .0
+ .push(Span::styled(format!(" {} ", enc.name()), base_style));
+ }
+
// Render to the statusline.
surface.set_spans(
viewport.x
@@ -672,6 +672,31 @@ impl EditorView {
&right_side_text,
right_side_text.width() as u16,
);
+
+ //-------------------------------
+ // Middle / File path / Title
+ //-------------------------------
+ let title = {
+ let rel_path = doc.relative_path();
+ let path = rel_path
+ .as_ref()
+ .map(|p| p.to_string_lossy())
+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
+ format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" })
+ };
+
+ surface.set_string_truncated(
+ viewport.x + 8, // 8: 1 space + 3 char mode string + 1 space + 1 spinner + 1 space
+ viewport.y,
+ title,
+ viewport
+ .width
+ .saturating_sub(6)
+ .saturating_sub(right_side_text.width() as u16 + 1) as usize, // "+ 1": a space between the title and the selection info
+ base_style,
+ true,
+ true,
+ );
}
/// Handle events by looking them up in `self.keymaps`. Returns None
@@ -684,12 +709,13 @@ impl EditorView {
cxt: &mut commands::Context,
event: KeyEvent,
) -> Option<KeymapResult> {
+ cxt.editor.autoinfo = None;
let key_result = self.keymaps.get_mut(&mode).unwrap().get(event);
- self.autoinfo = key_result.sticky.map(|node| node.infobox());
+ cxt.editor.autoinfo = key_result.sticky.map(|node| node.infobox());
match &key_result.kind {
KeymapResultKind::Matched(command) => command.execute(cxt),
- KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
+ KeymapResultKind::Pending(node) => cxt.editor.autoinfo = Some(node.infobox()),
KeymapResultKind::MatchedSequence(commands) => {
for command in commands {
command.execute(cxt);
@@ -789,8 +815,9 @@ impl EditorView {
pub fn clear_completion(&mut self, editor: &mut Editor) {
self.completion = None;
+
// Clear any savepoints
- let (_, doc) = current!(editor);
+ let doc = doc_mut!(editor);
doc.savepoint = None;
editor.clear_idle_timer(); // don't retrigger
}
@@ -927,7 +954,7 @@ impl EditorView {
return EventResult::Ignored;
}
- commands::Command::yank_main_selection_to_primary_clipboard.execute(cxt);
+ commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
EventResult::Consumed(None)
}
@@ -953,9 +980,9 @@ impl EditorView {
if let Ok(pos) = doc.text().try_line_to_char(line) {
doc.set_selection(view_id, Selection::point(pos));
if modifiers == crossterm::event::KeyModifiers::ALT {
- commands::Command::dap_edit_log.execute(cxt);
+ commands::MappableCommand::dap_edit_log.execute(cxt);
} else {
- commands::Command::dap_edit_condition.execute(cxt);
+ commands::MappableCommand::dap_edit_condition.execute(cxt);
}
return EventResult::Consumed(None);
@@ -977,7 +1004,8 @@ impl EditorView {
}
if modifiers == crossterm::event::KeyModifiers::ALT {
- commands::Command::replace_selections_with_primary_clipboard.execute(cxt);
+ commands::MappableCommand::replace_selections_with_primary_clipboard
+ .execute(cxt);
return EventResult::Consumed(None);
}
@@ -991,7 +1019,7 @@ impl EditorView {
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id;
- commands::Command::paste_primary_clipboard_before.execute(cxt);
+ commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
return EventResult::Consumed(None);
}
@@ -1004,14 +1032,18 @@ impl EditorView {
}
impl Component for EditorView {
- fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
- let mut cxt = commands::Context {
- editor: &mut cx.editor,
+ fn handle_event(
+ &mut self,
+ event: Event,
+ context: &mut crate::compositor::Context,
+ ) -> EventResult {
+ let mut cx = commands::Context {
+ editor: context.editor,
count: None,
register: None,
callback: None,
on_next_key_callback: None,
- jobs: cx.jobs,
+ jobs: context.jobs,
};
match event {
@@ -1021,18 +1053,19 @@ impl Component for EditorView {
EventResult::Consumed(None)
}
Event::Key(key) => {
- cxt.editor.reset_idle_timer();
+ cx.editor.reset_idle_timer();
let mut key = KeyEvent::from(key);
canonicalize_key(&mut key);
+
// clear status
- cxt.editor.status_msg = None;
+ cx.editor.status_msg = None;
- let (_, doc) = current!(cxt.editor);
+ let doc = doc!(cx.editor);
let mode = doc.mode();
if let Some(on_next_key) = self.on_next_key.take() {
// if there's a command waiting input, do that first
- on_next_key(&mut cxt, key);
+ on_next_key(&mut cx, key);
} else {
match mode {
Mode::Insert => {
@@ -1044,8 +1077,8 @@ impl Component for EditorView {
if let Some(completion) = &mut self.completion {
// use a fake context here
let mut cx = Context {
- editor: cxt.editor,
- jobs: cxt.jobs,
+ editor: cx.editor,
+ jobs: cx.jobs,
scroll: None,
};
let res = completion.handle_event(event, &mut cx);
@@ -1055,40 +1088,46 @@ impl Component for EditorView {
if callback.is_some() {
// assume close_fn
- self.clear_completion(cxt.editor);
+ self.clear_completion(cx.editor);
}
}
}
// if completion didn't take the event, we pass it onto commands
if !consumed {
- self.insert_mode(&mut cxt, key);
+ self.insert_mode(&mut cx, key);
// lastly we recalculate completion
if let Some(completion) = &mut self.completion {
- completion.update(&mut cxt);
+ completion.update(&mut cx);
if completion.is_empty() {
- self.clear_completion(cxt.editor);
+ self.clear_completion(cx.editor);
}
}
}
}
- mode => self.command_mode(mode, &mut cxt, key),
+ mode => self.command_mode(mode, &mut cx, key),
}
}
- self.on_next_key = cxt.on_next_key_callback.take();
+ self.on_next_key = cx.on_next_key_callback.take();
// appease borrowck
- let callback = cxt.callback.take();
+ let callback = cx.callback.take();
// if the command consumed the last view, skip the render.
// on the next loop cycle the Application will then terminate.
- if cxt.editor.should_close() {
+ if cx.editor.should_close() {
return EventResult::Ignored;
}
- let (view, doc) = current!(cxt.editor);
- view.ensure_cursor_in_view(doc, cxt.editor.config.scrolloff);
+ let (view, doc) = current!(cx.editor);
+ view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
+
+ // Store a history state if not in insert mode. This also takes care of
+ // commiting changes when leaving insert mode.
+ if doc.mode() != Mode::Insert {
+ doc.append_changes_to_history(view.id);
+ }
// mode transitions
match (mode, doc.mode()) {
@@ -1117,7 +1156,7 @@ impl Component for EditorView {
EventResult::Consumed(callback)
}
- Event::Mouse(event) => self.handle_mouse_event(event, &mut cxt),
+ Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
}
}
@@ -1134,8 +1173,9 @@ impl Component for EditorView {
}
if cx.editor.config.auto_info {
- if let Some(ref mut info) = self.autoinfo {
+ if let Some(mut info) = cx.editor.autoinfo.take() {
info.render(area, surface, cx);
+ cx.editor.autoinfo = Some(info)
}
}
@@ -1173,13 +1213,31 @@ impl Component for EditorView {
disp.push_str(&s);
}
}
+ let style = cx.editor.theme.get("ui.text");
+ let macro_width = if cx.editor.macro_recording.is_some() {
+ 3
+ } else {
+ 0
+ };
surface.set_string(
- area.x + area.width.saturating_sub(key_width),
+ area.x + area.width.saturating_sub(key_width + macro_width),
area.y + area.height.saturating_sub(1),
disp.get(disp.len().saturating_sub(key_width as usize)..)
.unwrap_or(&disp),
- cx.editor.theme.get("ui.text"),
+ style,
);
+ if let Some((reg, _)) = cx.editor.macro_recording {
+ let disp = format!("[{}]", reg);
+ let style = style
+ .fg(helix_view::graphics::Color::Yellow)
+ .add_modifier(Modifier::BOLD);
+ surface.set_string(
+ area.x + area.width.saturating_sub(3),
+ area.y + area.height.saturating_sub(1),
+ &disp,
+ style,
+ );
+ }
}
if let Some(completion) = self.completion.as_mut() {
@@ -1188,11 +1246,11 @@ impl Component for EditorView {
}
fn cursor(&self, _area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
- // match view.doc.mode() {
- // Mode::Insert => write!(stdout, "\x1B[6 q"),
- // mode => write!(stdout, "\x1B[2 q"),
- // };
- editor.cursor()
+ match editor.cursor() {
+ // All block cursors are drawn manually
+ (pos, CursorKind::Block) => (pos, CursorKind::Hidden),
+ cursor => cursor,
+ }
}
}
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index ca8303dd..6a7b641a 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -21,6 +21,9 @@ pub struct Markdown {
contents: String,
config_loader: Arc<syntax::Loader>,
+
+ block_style: String,
+ heading_style: String,
}
// TODO: pre-render and self reference via Pin
@@ -31,120 +34,137 @@ impl Markdown {
Self {
contents,
config_loader,
+ block_style: "markup.raw.inline".into(),
+ heading_style: "markup.heading".into(),
}
}
-}
-fn parse<'a>(
- contents: &'a str,
- theme: Option<&Theme>,
- loader: &syntax::Loader,
-) -> tui::text::Text<'a> {
- // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}}
- // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```";
-
- let mut options = Options::empty();
- options.insert(Options::ENABLE_STRIKETHROUGH);
- let parser = Parser::new_ext(contents, options);
-
- // TODO: if possible, render links as terminal hyperlinks: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
- let mut tags = Vec::new();
- let mut spans = Vec::new();
- let mut lines = Vec::new();
-
- fn to_span(text: pulldown_cmark::CowStr) -> Span {
- use std::ops::Deref;
- Span::raw::<std::borrow::Cow<_>>(match text {
- CowStr::Borrowed(s) => s.into(),
- CowStr::Boxed(s) => s.to_string().into(),
- CowStr::Inlined(s) => s.deref().to_owned().into(),
- })
+ pub fn style_group(mut self, suffix: &str) -> Self {
+ self.block_style = format!("markup.raw.inline.{}", suffix);
+ self.heading_style = format!("markup.heading.{}", suffix);
+ self
}
- let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
-
- // TODO: use better scopes for these, `markup.raw.block`, `markup.heading`
- let code_style = theme
- .map(|theme| theme.get("ui.text.focus"))
- .unwrap_or_default(); // white
- let heading_style = theme
- .map(|theme| theme.get("ui.linenr.selected"))
- .unwrap_or_default(); // lilac
-
- for event in parser {
- match event {
- Event::Start(tag) => tags.push(tag),
- Event::End(tag) => {
- tags.pop();
- match tag {
- Tag::Heading(_) | Tag::Paragraph | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => {
- // whenever code block or paragraph closes, new line
- let spans = std::mem::take(&mut spans);
- if !spans.is_empty() {
- lines.push(Spans::from(spans));
+ fn parse(&self, theme: Option<&Theme>) -> tui::text::Text<'_> {
+ // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}}
+ // let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```";
+
+ let mut options = Options::empty();
+ options.insert(Options::ENABLE_STRIKETHROUGH);
+ let parser = Parser::new_ext(&self.contents, options);
+
+ // TODO: if possible, render links as terminal hyperlinks: https://gist.github.com/egmontkob/eb114294efbcd5adb1944c9f3cb5feda
+ let mut tags = Vec::new();
+ let mut spans = Vec::new();
+ let mut lines = Vec::new();
+
+ fn to_span(text: pulldown_cmark::CowStr) -> Span {
+ use std::ops::Deref;
+ Span::raw::<std::borrow::Cow<_>>(match text {
+ CowStr::Borrowed(s) => s.into(),
+ CowStr::Boxed(s) => s.to_string().into(),
+ CowStr::Inlined(s) => s.deref().to_owned().into(),
+ })
+ }
+
+ macro_rules! get_theme {
+ ($s1: expr) => {
+ theme
+ .map(|theme| theme.try_get($s1.as_str()))
+ .flatten()
+ .unwrap_or_default()
+ };
+ }
+ let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
+ let code_style = get_theme!(self.block_style);
+ let heading_style = get_theme!(self.heading_style);
+
+ for event in parser {
+ match event {
+ Event::Start(tag) => tags.push(tag),
+ Event::End(tag) => {
+ tags.pop();
+ match tag {
+ Tag::Heading(_, _, _)
+ | Tag::Paragraph
+ | Tag::CodeBlock(CodeBlockKind::Fenced(_)) => {
+ // whenever code block or paragraph closes, new line
+ let spans = std::mem::take(&mut spans);
+ if !spans.is_empty() {
+ lines.push(Spans::from(spans));
+ }
+ lines.push(Spans::default());
}
- lines.push(Spans::default());
+ _ => (),
}
- _ => (),
}
- }
- Event::Text(text) => {
- // TODO: temp workaround
- if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
- if let Some(theme) = theme {
- let rope = Rope::from(text.as_ref());
- let syntax = loader
- .language_configuration_for_injection_string(language)
- .and_then(|config| config.highlight_config(theme.scopes()))
- .map(|config| Syntax::new(&rope, config));
-
- if let Some(syntax) = syntax {
- // if we have a syntax available, highlight_iter and generate spans
- let mut highlights = Vec::new();
-
- for event in syntax.highlight_iter(rope.slice(..), None, None, |_| None)
- {
- match event.unwrap() {
- HighlightEvent::HighlightStart(span) => {
- highlights.push(span);
- }
- HighlightEvent::HighlightEnd => {
- highlights.pop();
- }
- HighlightEvent::Source { start, end } => {
- let style = match highlights.first() {
- Some(span) => theme.get(&theme.scopes()[span.0]),
- None => text_style,
- };
-
- // TODO: replace tabs with indentation
-
- let mut slice = &text[start..end];
- // TODO: do we need to handle all unicode line endings
- // here, or is just '\n' okay?
- while let Some(end) = slice.find('\n') {
- // emit span up to newline
- let text = &slice[..end];
- let text = text.replace('\t', " "); // replace tabs
- let span = Span::styled(text, style);
- spans.push(span);
-
- // truncate slice to after newline
- slice = &slice[end + 1..];
-
- // make a new line
- let spans = std::mem::take(&mut spans);
- lines.push(Spans::from(spans));
+ Event::Text(text) => {
+ // TODO: temp workaround
+ if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
+ if let Some(theme) = theme {
+ let rope = Rope::from(text.as_ref());
+ let syntax = self
+ .config_loader
+ .language_configuration_for_injection_string(language)
+ .and_then(|config| config.highlight_config(theme.scopes()))
+ .map(|config| {
+ Syntax::new(&rope, config, self.config_loader.clone())
+ });
+
+ if let Some(syntax) = syntax {
+ // if we have a syntax available, highlight_iter and generate spans
+ let mut highlights = Vec::new();
+
+ for event in syntax.highlight_iter(rope.slice(..), None, None) {
+ match event.unwrap() {
+ HighlightEvent::HighlightStart(span) => {
+ highlights.push(span);
+ }
+ HighlightEvent::HighlightEnd => {
+ highlights.pop();
}
+ HighlightEvent::Source { start, end } => {
+ let style = match highlights.first() {
+ Some(span) => theme.get(&theme.scopes()[span.0]),
+ None => text_style,
+ };
- // if there's anything left, emit it too
- if !slice.is_empty() {
- let span =
- Span::styled(slice.replace('\t', " "), style);
- spans.push(span);
+ // TODO: replace tabs with indentation
+
+ let mut slice = &text[start..end];
+ // TODO: do we need to handle all unicode line endings
+ // here, or is just '\n' okay?
+ while let Some(end) = slice.find('\n') {
+ // emit span up to newline
+ let text = &slice[..end];
+ let text = text.replace('\t', " "); // replace tabs
+ let span = Span::styled(text, style);
+ spans.push(span);
+
+ // truncate slice to after newline
+ slice = &slice[end + 1..];
+
+ // make a new line
+ let spans = std::mem::take(&mut spans);
+ lines.push(Spans::from(spans));
+ }
+
+ // if there's anything left, emit it too
+ if !slice.is_empty() {
+ let span = Span::styled(
+ slice.replace('\t', " "),
+ style,
+ );
+ spans.push(span);
+ }
}
}
}
+ } else {
+ for line in text.lines() {
+ let span = Span::styled(line.to_string(), code_style);
+ lines.push(Spans::from(span));
+ }
}
} else {
for line in text.lines() {
@@ -152,64 +172,60 @@ fn parse<'a>(
lines.push(Spans::from(span));
}
}
+ } else if let Some(Tag::Heading(_, _, _)) = tags.last() {
+ let mut span = to_span(text);
+ span.style = heading_style;
+ spans.push(span);
} else {
- for line in text.lines() {
- let span = Span::styled(line.to_string(), code_style);
- lines.push(Spans::from(span));
- }
+ let mut span = to_span(text);
+ span.style = text_style;
+ spans.push(span);
}
- } else if let Some(Tag::Heading(_)) = tags.last() {
- let mut span = to_span(text);
- span.style = heading_style;
- spans.push(span);
- } else {
+ }
+ Event::Code(text) | Event::Html(text) => {
let mut span = to_span(text);
- span.style = text_style;
+ span.style = code_style;
spans.push(span);
}
+ Event::SoftBreak | Event::HardBreak => {
+ // let spans = std::mem::replace(&mut spans, Vec::new());
+ // lines.push(Spans::from(spans));
+ spans.push(Span::raw(" "));
+ }
+ Event::Rule => {
+ let mut span = Span::raw("---");
+ span.style = code_style;
+ lines.push(Spans::from(span));
+ lines.push(Spans::default());
+ }
+ // TaskListMarker(bool) true if checked
+ _ => {
+ log::warn!("unhandled markdown event {:?}", event);
+ }
}
- Event::Code(text) | Event::Html(text) => {
- let mut span = to_span(text);
- span.style = code_style;
- spans.push(span);
- }
- Event::SoftBreak | Event::HardBreak => {
- // let spans = std::mem::replace(&mut spans, Vec::new());
- // lines.push(Spans::from(spans));
- spans.push(Span::raw(" "));
- }
- Event::Rule => {
- let mut span = Span::raw("---");
- span.style = code_style;
- lines.push(Spans::from(span));
- lines.push(Spans::default());
- }
- // TaskListMarker(bool) true if checked
- _ => {
- log::warn!("unhandled markdown event {:?}", event);
- }
+ // build up a vec of Paragraph tui widgets
}
- // build up a vec of Paragraph tui widgets
- }
- if !spans.is_empty() {
- lines.push(Spans::from(spans));
- }
+ if !spans.is_empty() {
+ lines.push(Spans::from(spans));
+ }
- // if last line is empty, remove it
- if let Some(line) = lines.last() {
- if line.0.is_empty() {
- lines.pop();
+ // if last line is empty, remove it
+ if let Some(line) = lines.last() {
+ if line.0.is_empty() {
+ lines.pop();
+ }
}
- }
- Text::from(lines)
+ Text::from(lines)
+ }
}
+
impl Component for Markdown {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
use tui::widgets::{Paragraph, Widget, Wrap};
- let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
+ let text = self.parse(Some(&cx.editor.theme));
let par = Paragraph::new(text)
.wrap(Wrap { trim: false })
@@ -227,7 +243,8 @@ impl Component for Markdown {
if padding >= viewport.1 || padding >= viewport.0 {
return None;
}
- let contents = parse(&self.contents, None, &self.config_loader);
+ let contents = self.parse(None);
+
// TODO: account for tab width
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
@@ -241,11 +258,6 @@ impl Component for Markdown {
} else if content_width > text_width {
text_width = content_width;
}
-
- if height >= viewport.1 {
- height = viewport.1;
- break;
- }
}
Some((text_width + padding, height))
diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs
index e891c149..f9a0438c 100644
--- a/helix-term/src/ui/menu.rs
+++ b/helix-term/src/ui/menu.rs
@@ -14,11 +14,18 @@ use helix_view::{graphics::Rect, Editor};
use tui::layout::Constraint;
pub trait Item {
- fn sort_text(&self) -> &str;
- fn filter_text(&self) -> &str;
-
fn label(&self) -> &str;
- fn row(&self) -> Row;
+
+ fn sort_text(&self) -> &str {
+ self.label()
+ }
+ fn filter_text(&self) -> &str {
+ self.label()
+ }
+
+ fn row(&self) -> Row {
+ Row::new(vec![Cell::from(self.label())])
+ }
}
pub struct Menu<T: Item> {
@@ -132,7 +139,17 @@ impl<T: Item> Menu<T> {
acc
});
- let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
+
+ let height = self.matches.len().min(10).min(viewport.1 as usize);
+ // do all the matches fit on a single screen?
+ let fits = self.matches.len() <= height;
+
+ let mut len = max_lens.iter().sum::<usize>() + n;
+
+ if !fits {
+ len += 1; // +1: reserve some space for scrollbar
+ }
+
let width = len.min(viewport.0 as usize);
self.widths = max_lens
@@ -140,8 +157,6 @@ impl<T: Item> Menu<T> {
.map(|len| Constraint::Length(len as u16))
.collect();
- let height = self.matches.len().min(10).min(viewport.1 as usize);
-
self.size = (width as u16, height as u16);
// adjust scroll offsets if size changed
@@ -190,7 +205,7 @@ impl<T: Item + 'static> Component for Menu<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -202,7 +217,7 @@ impl<T: Item + 'static> Component for Menu<T> {
return close_fn;
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
- shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None);
@@ -297,12 +312,14 @@ impl<T: Item + 'static> Component for Menu<T> {
},
);
+ let fits = len <= win_height;
+
for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
- if is_marked {
- let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16);
- cell.set_symbol("▐ ");
+ if !fits && is_marked {
+ let cell = &mut surface[(area.x + area.width - 2, area.y + i as u16)];
+ cell.set_symbol("▐");
// cell.set_style(selected);
// cell.set_style(if is_marked { selected } else { style });
}
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 3c203326..49f7b2fa 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -2,7 +2,7 @@ mod completion;
pub(crate) mod editor;
mod info;
mod markdown;
-mod menu;
+pub mod menu;
mod picker;
mod popup;
mod prompt;
@@ -65,7 +65,7 @@ pub fn regex_prompt(
return;
}
- let case_insensitive = if cx.editor.config.smart_case {
+ let case_insensitive = if cx.editor.config.search.smart_case {
!input.chars().any(char::is_uppercase)
} else {
false
@@ -174,7 +174,9 @@ pub mod completers {
use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
+ use helix_view::editor::Config;
use helix_view::theme;
+ use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::cmp::Reverse;
@@ -186,6 +188,7 @@ pub mod completers {
&helix_core::config_dir().join("themes"),
));
names.push("default".into());
+ names.push("base16_default".into());
let mut names: Vec<_> = names
.into_iter()
@@ -207,6 +210,31 @@ pub mod completers {
names
}
+ pub fn setting(input: &str) -> Vec<Completion> {
+ static KEYS: Lazy<Vec<String>> = Lazy::new(|| {
+ serde_json::to_value(Config::default())
+ .unwrap()
+ .as_object()
+ .unwrap()
+ .keys()
+ .cloned()
+ .collect()
+ });
+
+ let matcher = Matcher::default();
+
+ let mut matches: Vec<_> = KEYS
+ .iter()
+ .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score)))
+ .collect();
+
+ matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
+ matches
+ .into_iter()
+ .map(|(name, _)| ((0..), name.into()))
+ .collect()
+ }
+
pub fn filename(input: &str) -> Vec<Completion> {
filename_impl(input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
@@ -255,7 +283,7 @@ pub mod completers {
let is_tilde = input.starts_with('~') && input.len() == 1;
let path = helix_core::path::expand_tilde(Path::new(input));
- let (dir, file_name) = if input.ends_with('/') {
+ let (dir, file_name) = if input.ends_with(std::path::MAIN_SEPARATOR) {
(path, None)
} else {
let file_name = path
diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs
index eaca470e..2c7db7f2 100644
--- a/helix-term/src/ui/picker.rs
+++ b/helix-term/src/ui/picker.rs
@@ -46,7 +46,7 @@ pub struct FilePicker<T> {
}
pub enum CachedPreview {
- Document(Document),
+ Document(Box<Document>),
Binary,
LargeFile,
NotFound,
@@ -139,8 +139,8 @@ impl<T> FilePicker<T> {
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
- Document::open(path, None, Some(&editor.theme), None)
- .map(CachedPreview::Document)
+ Document::open(path, None, None)
+ .map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
},
@@ -159,6 +159,7 @@ impl<T: 'static> Component for FilePicker<T> {
// |picker | | |
// | | | |
// +---------+ +---------+
+
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
let area = inner_rect(area);
// -- Render the frame:
@@ -220,13 +221,8 @@ impl<T: 'static> Component for FilePicker<T> {
let offset = Position::new(first_line, 0);
- let highlights = EditorView::doc_syntax_highlights(
- doc,
- offset,
- area.height,
- &cx.editor.theme,
- &cx.editor.syn_loader,
- );
+ let highlights =
+ EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme);
EditorView::render_text_highlights(
doc,
offset,
@@ -397,6 +393,16 @@ fn inner_rect(area: Rect) -> Rect {
}
impl<T: 'static> Component for Picker<T> {
+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
+ let max_width = 50.min(viewport.0);
+ let max_height = 10.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
+
+ let height = (self.options.len() as u16 + 4) // add some spacing for input + padding
+ .min(max_height);
+ let width = max_width;
+ Some((width, height))
+ }
+
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let key_event = match event {
Event::Key(event) => event,
@@ -404,13 +410,13 @@ impl<T: 'static> Component for Picker<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.last_picker = compositor.pop();
})));
match key_event.into() {
- shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
+ shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
self.move_up();
}
key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
@@ -492,10 +498,9 @@ impl<T: 'static> Component for Picker<T> {
let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
let borders = BorderType::line_symbols(BorderType::Plain);
for x in inner.left()..inner.right() {
- surface
- .get_mut(x, inner.y + 1)
- .set_symbol(borders.horizontal)
- .set_style(sep_style);
+ if let Some(cell) = surface.get_mut(x, inner.y + 1) {
+ cell.set_symbol(borders.horizontal).set_style(sep_style);
+ }
}
// -- Render the contents:
@@ -505,7 +510,7 @@ impl<T: 'static> Component for Picker<T> {
let selected = cx.editor.theme.get("ui.text.focus");
let rows = inner.height;
- let offset = self.cursor / (rows as usize) * (rows as usize);
+ let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize));
let files = self.matches.iter().skip(offset).map(|(index, _score)| {
(index, self.options.get(*index).unwrap()) // get_unchecked
@@ -513,7 +518,7 @@ impl<T: 'static> Component for Picker<T> {
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
if i == (self.cursor - offset) {
- surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected);
+ surface.set_string(inner.x.saturating_sub(2), inner.y + i as u16, ">", selected);
}
surface.set_string_truncated(
diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs
index 8f7921a1..4d319423 100644
--- a/helix-term/src/ui/popup.rs
+++ b/helix-term/src/ui/popup.rs
@@ -6,7 +6,7 @@ use crossterm::event::Event;
use tui::buffer::Buffer as Surface;
use helix_core::Position;
-use helix_view::graphics::Rect;
+use helix_view::graphics::{Margin, Rect};
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
// a width/height hint. maybe Popup(Box<Component>)
@@ -14,17 +14,26 @@ use helix_view::graphics::Rect;
pub struct Popup<T: Component> {
contents: T,
position: Option<Position>,
+ margin: Margin,
size: (u16, u16),
+ child_size: (u16, u16),
scroll: usize,
+ id: &'static str,
}
impl<T: Component> Popup<T> {
- pub fn new(contents: T) -> Self {
+ pub fn new(id: &'static str, contents: T) -> Self {
Self {
contents,
position: None,
+ margin: Margin {
+ vertical: 0,
+ horizontal: 0,
+ },
size: (0, 0),
+ child_size: (0, 0),
scroll: 0,
+ id,
}
}
@@ -32,6 +41,11 @@ impl<T: Component> Popup<T> {
self.position = pos;
}
+ pub fn margin(mut self, margin: Margin) -> Self {
+ self.margin = margin;
+ self
+ }
+
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
let position = self
.position
@@ -68,6 +82,9 @@ impl<T: Component> Popup<T> {
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
self.scroll += offset;
+
+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
+ self.scroll = (self.scroll + offset).min(max_offset as usize);
} else {
self.scroll = self.scroll.saturating_sub(offset);
}
@@ -93,7 +110,7 @@ impl<T: Component> Component for Popup<T> {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -115,13 +132,26 @@ impl<T: Component> Component for Popup<T> {
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
}
- fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
+ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
+ let max_width = 120.min(viewport.0);
+ let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
+
+ let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin);
+
let (width, height) = self
.contents
- .required_size((120, 26)) // max width, max height
+ .required_size((inner.width, inner.height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
- self.size = (width, height);
+ self.child_size = (width, height);
+ self.size = (
+ (width + self.margin.horizontal * 2).min(max_width),
+ (height + self.margin.vertical * 2).min(max_height),
+ );
+
+ // re-clamp scroll offset
+ let max_offset = self.child_size.1.saturating_sub(self.size.1);
+ self.scroll = self.scroll.min(max_offset as usize);
Some(self.size)
}
@@ -141,6 +171,11 @@ impl<T: Component> Component for Popup<T> {
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
- self.contents.render(area, surface, cx);
+ let inner = area.inner(&self.margin);
+ self.contents.render(inner, surface, cx);
+ }
+
+ fn id(&self) -> Option<&'static str> {
+ Some(self.id)
}
}
diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs
index e90b0772..4c4fef26 100644
--- a/helix-term/src/ui/prompt.rs
+++ b/helix-term/src/ui/prompt.rs
@@ -127,7 +127,7 @@ impl Prompt {
let mut char_position = char_indices
.iter()
.position(|(idx, _)| *idx == self.cursor)
- .unwrap_or_else(|| char_indices.len());
+ .unwrap_or(char_indices.len());
for _ in 0..rep {
// Skip any non-whitespace characters
@@ -330,7 +330,7 @@ impl Prompt {
.max(BASE_WIDTH);
let cols = std::cmp::max(1, area.width / max_len);
- let col_width = (area.width - (cols)) / cols;
+ let col_width = (area.width.saturating_sub(cols)) / cols;
let height = ((self.completion.len() as u16 + cols - 1) / cols)
.min(10) // at most 10 rows (or less)
@@ -426,7 +426,7 @@ impl Component for Prompt {
_ => return EventResult::Ignored,
};
- let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
+ let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| {
// remove the layer
compositor.pop();
})));
@@ -473,7 +473,7 @@ impl Component for Prompt {
}
}
key!(Enter) => {
- if self.selection.is_some() && self.line.ends_with('/') {
+ if self.selection.is_some() && self.line.ends_with(std::path::MAIN_SEPARATOR) {
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
} else {
@@ -505,7 +505,7 @@ impl Component for Prompt {
self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
- shift!(BackTab) => {
+ shift!(Tab) => {
self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
diff --git a/helix-term/src/ui/spinner.rs b/helix-term/src/ui/spinner.rs
index e8a43b48..68965469 100644
--- a/helix-term/src/ui/spinner.rs
+++ b/helix-term/src/ui/spinner.rs
@@ -1,4 +1,4 @@
-use std::{collections::HashMap, time::SystemTime};
+use std::{collections::HashMap, time::Instant};
#[derive(Default, Debug)]
pub struct ProgressSpinners {
@@ -25,7 +25,7 @@ impl Default for Spinner {
pub struct Spinner {
frames: Vec<&'static str>,
count: usize,
- start: Option<SystemTime>,
+ start: Option<Instant>,
interval: u64,
}
@@ -50,14 +50,13 @@ impl Spinner {
}
pub fn start(&mut self) {
- self.start = Some(SystemTime::now());
+ self.start = Some(Instant::now());
}
pub fn frame(&self) -> Option<&str> {
let idx = (self
.start
- .map(|time| SystemTime::now().duration_since(time))?
- .ok()?
+ .map(|time| Instant::now().duration_since(time))?
.as_millis()
/ self.interval as u128) as usize
% self.count;
diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml
index 6df65d36..e4cfbe4c 100644
--- a/helix-tui/Cargo.toml
+++ b/helix-tui/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-tui"
-version = "0.5.0"
+version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
description = """
A library to build rich terminal user interfaces or dashboards
@@ -18,8 +18,8 @@ default = ["crossterm"]
[dependencies]
bitflags = "1.3"
cassowary = "0.3"
-unicode-segmentation = "1.8"
-crossterm = { version = "0.22", optional = true }
+unicode-segmentation = "1.9"
+crossterm = { version = "0.23", optional = true }
serde = { version = "1", "optional" = true, features = ["derive"]}
-helix-view = { version = "0.5", path = "../helix-view", features = ["term"] }
-helix-core = { version = "0.5", path = "../helix-core" }
+helix-view = { version = "0.6", path = "../helix-view", features = ["term"] }
+helix-core = { version = "0.6", path = "../helix-core" }
diff --git a/helix-tui/README.md b/helix-tui/README.md
index 97b3d1d9..5cc80aa6 100644
--- a/helix-tui/README.md
+++ b/helix-tui/README.md
@@ -2,5 +2,5 @@
This library is a fork of the great library
[tui-rs](https://github.com/fdehau/tui-rs/). We've mainly relied on the double
-buffer implementation and render diffing, side-stepping it's widget and
+buffer implementation and render diffing, side-stepping its widget and
layouting.
diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs
index 3f56b49c..52474148 100644
--- a/helix-tui/src/backend/test.rs
+++ b/helix-tui/src/backend/test.rs
@@ -111,8 +111,7 @@ impl Backend for TestBackend {
I: Iterator<Item = (u16, u16, &'a Cell)>,
{
for (x, y, c) in content {
- let cell = self.buffer.get_mut(x, y);
- *cell = c.clone();
+ self.buffer[(x, y)] = c.clone();
}
Ok(())
}
diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs
index f480bc2f..f8673e43 100644
--- a/helix-tui/src/buffer.rs
+++ b/helix-tui/src/buffer.rs
@@ -90,19 +90,19 @@ impl Default for Cell {
/// use helix_view::graphics::{Rect, Color, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
-/// buf.get_mut(0, 2).set_symbol("x");
-/// assert_eq!(buf.get(0, 2).symbol, "x");
+/// buf[(0, 2)].set_symbol("x");
+/// assert_eq!(buf[(0, 2)].symbol, "x");
/// buf.set_string(3, 0, "string", Style::default().fg(Color::Red).bg(Color::White));
-/// assert_eq!(buf.get(5, 0), &Cell{
+/// assert_eq!(buf[(5, 0)], Cell{
/// symbol: String::from("r"),
/// fg: Color::Red,
/// bg: Color::White,
/// modifier: Modifier::empty()
/// });
-/// buf.get_mut(5, 0).set_char('x');
-/// assert_eq!(buf.get(5, 0).symbol, "x");
+/// buf[(5, 0)].set_char('x');
+/// assert_eq!(buf[(5, 0)].symbol, "x");
/// ```
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
pub struct Buffer {
/// The area represented by this buffer
pub area: Rect,
@@ -111,15 +111,6 @@ pub struct Buffer {
pub content: Vec<Cell>,
}
-impl Default for Buffer {
- fn default() -> Buffer {
- Buffer {
- area: Default::default(),
- content: Vec::new(),
- }
- }
-}
-
impl Buffer {
/// Returns a Buffer with all cells set to the default one
pub fn empty(area: Rect) -> Buffer {
@@ -171,15 +162,38 @@ impl Buffer {
}
/// Returns a reference to Cell at the given coordinates
- pub fn get(&self, x: u16, y: u16) -> &Cell {
- let i = self.index_of(x, y);
- &self.content[i]
+ pub fn get(&self, x: u16, y: u16) -> Option<&Cell> {
+ self.index_of_opt(x, y).map(|i| &self.content[i])
}
/// Returns a mutable reference to Cell at the given coordinates
- pub fn get_mut(&mut self, x: u16, y: u16) -> &mut Cell {
- let i = self.index_of(x, y);
- &mut self.content[i]
+ pub fn get_mut(&mut self, x: u16, y: u16) -> Option<&mut Cell> {
+ self.index_of_opt(x, y).map(|i| &mut self.content[i])
+ }
+
+ /// Tells whether the global (x, y) coordinates are inside the Buffer's area.
+ ///
+ /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
+ ///
+ /// # Examples
+ ///
+ /// ```
+ /// # use helix_tui::buffer::Buffer;
+ /// # use helix_view::graphics::Rect;
+ /// let rect = Rect::new(200, 100, 10, 10);
+ /// let buffer = Buffer::empty(rect);
+ /// // Global coordinates inside the Buffer's area
+ /// assert!(buffer.in_bounds(209, 100));
+ /// // Global coordinates outside the Buffer's area
+ /// assert!(!buffer.in_bounds(210, 100));
+ /// ```
+ ///
+ /// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
+ pub fn in_bounds(&self, x: u16, y: u16) -> bool {
+ x >= self.area.left()
+ && x < self.area.right()
+ && y >= self.area.top()
+ && y < self.area.bottom()
}
/// Returns the index in the Vec<Cell> for the given global (x, y) coordinates.
@@ -193,7 +207,7 @@ impl Buffer {
/// # use helix_view::graphics::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
- /// // Global coordinates to the top corner of this buffer's area
+ /// // Global coordinates to the top corner of this Buffer's area
/// assert_eq!(buffer.index_of(200, 100), 0);
/// ```
///
@@ -202,10 +216,7 @@ impl Buffer {
/// Panics when given an coordinate that is outside of this Buffer's area.
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
- x >= self.area.left()
- && x < self.area.right()
- && y >= self.area.top()
- && y < self.area.bottom(),
+ self.in_bounds(x, y),
"Trying to access position outside the buffer: x={}, y={}, area={:?}",
x,
y,
@@ -214,6 +225,16 @@ impl Buffer {
((y - self.area.y) * self.area.width + (x - self.area.x)) as usize
}
+ /// Returns the index in the Vec<Cell> for the given global (x, y) coordinates,
+ /// or `None` if the coordinates are outside the buffer's area.
+ fn index_of_opt(&self, x: u16, y: u16) -> Option<usize> {
+ if self.in_bounds(x, y) {
+ Some(self.index_of(x, y))
+ } else {
+ None
+ }
+ }
+
/// Returns the (global) coordinates of a cell given its index
///
/// Global coordinates are offset by the Buffer's area offset (`x`/`y`).
@@ -287,6 +308,11 @@ impl Buffer {
where
S: AsRef<str>,
{
+ // prevent panic if out of range
+ if !self.in_bounds(x, y) || width == 0 {
+ return (x, y);
+ }
+
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let width = if ellipsis { width - 1 } else { width };
@@ -381,7 +407,7 @@ impl Buffer {
pub fn set_background(&mut self, area: Rect, color: Color) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
- self.get_mut(x, y).set_bg(color);
+ self[(x, y)].set_bg(color);
}
}
}
@@ -389,7 +415,7 @@ impl Buffer {
pub fn set_style(&mut self, area: Rect, style: Style) {
for y in area.top()..area.bottom() {
for x in area.left()..area.right() {
- self.get_mut(x, y).set_style(style);
+ self[(x, y)].set_style(style);
}
}
}
@@ -417,7 +443,7 @@ impl Buffer {
pub fn clear(&mut self, area: Rect) {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
- self.get_mut(x, y).reset();
+ self[(x, y)].reset();
}
}
}
@@ -426,7 +452,7 @@ impl Buffer {
pub fn clear_with(&mut self, area: Rect, style: Style) {
for x in area.left()..area.right() {
for y in area.top()..area.bottom() {
- let cell = self.get_mut(x, y);
+ let cell = &mut self[(x, y)];
cell.reset();
cell.set_style(style);
}
@@ -509,15 +535,32 @@ impl Buffer {
updates.push((x, y, &next_buffer[i]));
}
- to_skip = current.symbol.width().saturating_sub(1);
+ let current_width = current.symbol.width();
+ to_skip = current_width.saturating_sub(1);
- let affected_width = std::cmp::max(current.symbol.width(), previous.symbol.width());
+ let affected_width = std::cmp::max(current_width, previous.symbol.width());
invalidated = std::cmp::max(affected_width, invalidated).saturating_sub(1);
}
updates
}
}
+impl std::ops::Index<(u16, u16)> for Buffer {
+ type Output = Cell;
+
+ fn index(&self, (x, y): (u16, u16)) -> &Self::Output {
+ let i = self.index_of(x, y);
+ &self.content[i]
+ }
+}
+
+impl std::ops::IndexMut<(u16, u16)> for Buffer {
+ fn index_mut(&mut self, (x, y): (u16, u16)) -> &mut Self::Output {
+ let i = self.index_of(x, y);
+ &mut self.content[i]
+ }
+}
+
#[cfg(test)]
mod tests {
use super::*;
diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs
index b8e52479..8a974ddb 100644
--- a/helix-tui/src/text.rs
+++ b/helix-tui/src/text.rs
@@ -195,15 +195,9 @@ impl<'a> From<&'a str> for Span<'a> {
}
/// A string composed of clusters of graphemes, each with their own style.
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
pub struct Spans<'a>(pub Vec<Span<'a>>);
-impl<'a> Default for Spans<'a> {
- fn default() -> Spans<'a> {
- Spans(Vec::new())
- }
-}
-
impl<'a> Spans<'a> {
/// Returns the width of the underlying string.
///
@@ -280,17 +274,11 @@ impl<'a> From<Spans<'a>> for String {
/// text.extend(Text::styled("Some more lines\nnow with more style!", style));
/// assert_eq!(6, text.height());
/// ```
-#[derive(Debug, Clone, PartialEq)]
+#[derive(Debug, Default, Clone, PartialEq)]
pub struct Text<'a> {
pub lines: Vec<Spans<'a>>,
}
-impl<'a> Default for Text<'a> {
- fn default() -> Text<'a> {
- Text { lines: Vec::new() }
- }
-}
-
impl<'a> Text<'a> {
/// Create some text (potentially multiple lines) with no style.
///
diff --git a/helix-tui/src/widgets/block.rs b/helix-tui/src/widgets/block.rs
index 648c2d7e..26223c3e 100644
--- a/helix-tui/src/widgets/block.rs
+++ b/helix-tui/src/widgets/block.rs
@@ -15,12 +15,12 @@ pub enum BorderType {
}
impl BorderType {
- pub fn line_symbols(border_type: BorderType) -> line::Set {
+ pub fn line_symbols(border_type: Self) -> line::Set {
match border_type {
- BorderType::Plain => line::NORMAL,
- BorderType::Rounded => line::ROUNDED,
- BorderType::Double => line::DOUBLE,
- BorderType::Thick => line::THICK,
+ Self::Plain => line::NORMAL,
+ Self::Rounded => line::ROUNDED,
+ Self::Double => line::DOUBLE,
+ Self::Thick => line::THICK,
}
}
}
@@ -140,14 +140,14 @@ impl<'a> Widget for Block<'a> {
// Sides
if self.borders.intersects(Borders::LEFT) {
for y in area.top()..area.bottom() {
- buf.get_mut(area.left(), y)
+ buf[(area.left(), y)]
.set_symbol(symbols.vertical)
.set_style(self.border_style);
}
}
if self.borders.intersects(Borders::TOP) {
for x in area.left()..area.right() {
- buf.get_mut(x, area.top())
+ buf[(x, area.top())]
.set_symbol(symbols.horizontal)
.set_style(self.border_style);
}
@@ -155,7 +155,7 @@ impl<'a> Widget for Block<'a> {
if self.borders.intersects(Borders::RIGHT) {
let x = area.right() - 1;
for y in area.top()..area.bottom() {
- buf.get_mut(x, y)
+ buf[(x, y)]
.set_symbol(symbols.vertical)
.set_style(self.border_style);
}
@@ -163,7 +163,7 @@ impl<'a> Widget for Block<'a> {
if self.borders.intersects(Borders::BOTTOM) {
let y = area.bottom() - 1;
for x in area.left()..area.right() {
- buf.get_mut(x, y)
+ buf[(x, y)]
.set_symbol(symbols.horizontal)
.set_style(self.border_style);
}
@@ -171,22 +171,22 @@ impl<'a> Widget for Block<'a> {
// Corners
if self.borders.contains(Borders::RIGHT | Borders::BOTTOM) {
- buf.get_mut(area.right() - 1, area.bottom() - 1)
+ buf[(area.right() - 1, area.bottom() - 1)]
.set_symbol(symbols.bottom_right)
.set_style(self.border_style);
}
if self.borders.contains(Borders::RIGHT | Borders::TOP) {
- buf.get_mut(area.right() - 1, area.top())
+ buf[(area.right() - 1, area.top())]
.set_symbol(symbols.top_right)
.set_style(self.border_style);
}
if self.borders.contains(Borders::LEFT | Borders::BOTTOM) {
- buf.get_mut(area.left(), area.bottom() - 1)
+ buf[(area.left(), area.bottom() - 1)]
.set_symbol(symbols.bottom_left)
.set_style(self.border_style);
}
if self.borders.contains(Borders::LEFT | Borders::TOP) {
- buf.get_mut(area.left(), area.top())
+ buf[(area.left(), area.top())]
.set_symbol(symbols.top_left)
.set_style(self.border_style);
}
diff --git a/helix-tui/src/widgets/paragraph.rs b/helix-tui/src/widgets/paragraph.rs
index fee35d25..4e839162 100644
--- a/helix-tui/src/widgets/paragraph.rs
+++ b/helix-tui/src/widgets/paragraph.rs
@@ -166,7 +166,7 @@ impl<'a> Widget for Paragraph<'a> {
Box::new(WordWrapper::new(&mut styled, text_area.width, trim))
} else {
let mut line_composer = Box::new(LineTruncator::new(&mut styled, text_area.width));
- if let Alignment::Left = self.alignment {
+ if self.alignment == Alignment::Left {
line_composer.set_horizontal_offset(self.scroll.1);
}
line_composer
@@ -176,7 +176,7 @@ impl<'a> Widget for Paragraph<'a> {
if y >= self.scroll.0 {
let mut x = get_line_offset(current_line_width, text_area.width, self.alignment);
for StyledGrapheme { symbol, style } in current_line {
- buf.get_mut(text_area.left() + x, text_area.top() + y - self.scroll.0)
+ buf[(text_area.left() + x, text_area.top() + y - self.scroll.0)]
.set_symbol(if symbol.is_empty() {
// If the symbol is empty, the last char which rendered last time will
// leave on the line. It's a quick fix.
diff --git a/helix-tui/src/widgets/reflow.rs b/helix-tui/src/widgets/reflow.rs
index 21847783..33e52bb4 100644
--- a/helix-tui/src/widgets/reflow.rs
+++ b/helix-tui/src/widgets/reflow.rs
@@ -404,8 +404,8 @@ mod test {
let text = "コンピュータ上で文字を扱う場合、典型的には文字による通信を行う場合にその両端点\
では、";
let (word_wrapper, word_wrapper_width) =
- run_composer(Composer::WordWrapper { trim: true }, &text, width);
- let (line_truncator, _) = run_composer(Composer::LineTruncator, &text, width);
+ run_composer(Composer::WordWrapper { trim: true }, text, width);
+ let (line_truncator, _) = run_composer(Composer::LineTruncator, text, width);
assert_eq!(line_truncator, vec!["コンピュータ上で文字"]);
let wrapped = vec![
"コンピュータ上で文字",
diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs
index d7caa0b0..6aee5988 100644
--- a/helix-tui/src/widgets/table.rs
+++ b/helix-tui/src/widgets/table.rs
@@ -363,21 +363,12 @@ impl<'a> Table<'a> {
}
}
-#[derive(Debug, Clone)]
+#[derive(Debug, Default, Clone)]
pub struct TableState {
pub offset: usize,
pub selected: Option<usize>,
}
-impl Default for TableState {
- fn default() -> TableState {
- TableState {
- offset: 0,
- selected: None,
- }
- }
-}
-
impl TableState {
pub fn selected(&self) -> Option<usize> {
self.selected
diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml
index ffe6a111..932c3321 100644
--- a/helix-view/Cargo.toml
+++ b/helix-view/Cargo.toml
@@ -1,6 +1,6 @@
[package]
name = "helix-view"
-version = "0.5.0"
+version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
@@ -16,13 +16,13 @@ term = ["crossterm"]
[dependencies]
bitflags = "1.3"
anyhow = "1"
-helix-core = { version = "0.5", path = "../helix-core" }
-helix-lsp = { version = "0.5", path = "../helix-lsp"}
-helix-dap = { version = "0.5", path = "../helix-dap"}
-crossterm = { version = "0.22", optional = true }
+helix-core = { version = "0.6", path = "../helix-core" }
+helix-lsp = { version = "0.6", path = "../helix-lsp"}
+helix-dap = { version = "0.6", path = "../helix-dap"}
+crossterm = { version = "0.23", optional = true }
# Conversion traits
-once_cell = "1.8"
+once_cell = "1.9"
url = "2"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
@@ -31,7 +31,6 @@ futures-util = { version = "0.3", features = ["std", "async-await"], default-fea
slotmap = "1"
-encoding_rs = "0.8"
chardetng = "0.1"
serde = { version = "1.0", features = ["derive"] }
@@ -41,7 +40,7 @@ log = "~0.4"
which = "4.2"
[target.'cfg(windows)'.dependencies]
-clipboard-win = { version = "4.2", features = ["std"] }
+clipboard-win = { version = "4.4", features = ["std"] }
[dev-dependencies]
helix-tui = { path = "../helix-tui" }
diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs
index 76b19a07..c0186ee5 100644
--- a/helix-view/src/document.rs
+++ b/helix-view/src/document.rs
@@ -1,5 +1,6 @@
-use anyhow::{anyhow, Context, Error};
+use anyhow::{anyhow, bail, Context, Error};
use serde::de::{self, Deserialize, Deserializer};
+use serde::Serialize;
use std::cell::Cell;
use std::collections::HashMap;
use std::fmt::Display;
@@ -9,7 +10,8 @@ use std::str::FromStr;
use std::sync::Arc;
use helix_core::{
- history::History,
+ encoding,
+ history::{History, UndoKind},
indent::{auto_detect_indent_style, IndentStyle},
line_ending::auto_detect_line_ending,
syntax::{self, LanguageConfiguration},
@@ -18,7 +20,7 @@ use helix_core::{
};
use helix_lsp::util::LspFormatting;
-use crate::{DocumentId, Theme, ViewId};
+use crate::{DocumentId, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192;
@@ -29,9 +31,9 @@ pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode {
- Normal,
- Select,
- Insert,
+ Normal = 0,
+ Select = 1,
+ Insert = 2,
}
impl Display for Mode {
@@ -52,7 +54,7 @@ impl FromStr for Mode {
"normal" => Ok(Mode::Normal),
"select" => Ok(Mode::Select),
"insert" => Ok(Mode::Insert),
- _ => Err(anyhow!("Invalid mode '{}'", s)),
+ _ => bail!("Invalid mode '{}'", s),
}
}
}
@@ -68,13 +70,22 @@ impl<'de> Deserialize<'de> for Mode {
}
}
+impl Serialize for Mode {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ serializer.collect_str(self)
+ }
+}
+
pub struct Document {
pub(crate) id: DocumentId,
text: Rope,
pub(crate) selections: HashMap<ViewId, Selection>,
path: Option<PathBuf>,
- encoding: &'static encoding_rs::Encoding,
+ encoding: &'static encoding::Encoding,
/// Current editing mode.
pub mode: Mode,
@@ -104,6 +115,7 @@ pub struct Document {
last_saved_revision: usize,
version: i32, // should be usize?
+ pub(crate) modified_since_accessed: bool,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
@@ -127,6 +139,7 @@ impl fmt::Debug for Document {
// .field("history", &self.history)
.field("last_saved_revision", &self.last_saved_revision)
.field("version", &self.version)
+ .field("modified_since_accessed", &self.modified_since_accessed)
.field("diagnostics", &self.diagnostics)
// .field("language_server", &self.language_server)
.finish()
@@ -141,8 +154,8 @@ impl fmt::Debug for Document {
/// be used to override encoding auto-detection.
pub fn from_reader<R: std::io::Read + ?Sized>(
reader: &mut R,
- encoding: Option<&'static encoding_rs::Encoding>,
-) -> Result<(Rope, &'static encoding_rs::Encoding), Error> {
+ encoding: Option<&'static encoding::Encoding>,
+) -> Result<(Rope, &'static encoding::Encoding), Error> {
// These two buffers are 8192 bytes in size each and are used as
// intermediaries during the decoding process. Text read into `buf`
// from `reader` is decoded into `buf_out` as UTF-8. Once either
@@ -210,11 +223,11 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
total_read += read;
total_written += written;
match result {
- encoding_rs::CoderResult::InputEmpty => {
+ encoding::CoderResult::InputEmpty => {
debug_assert_eq!(slice.len(), total_read);
break;
}
- encoding_rs::CoderResult::OutputFull => {
+ encoding::CoderResult::OutputFull => {
debug_assert!(slice.len() > total_read);
builder.append(&buf_str[..total_written]);
total_written = 0;
@@ -249,7 +262,7 @@ pub fn from_reader<R: std::io::Read + ?Sized>(
/// replacement characters may appear in the encoded text.
pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
writer: &'a mut W,
- encoding: &'static encoding_rs::Encoding,
+ encoding: &'static encoding::Encoding,
rope: &'a Rope,
) -> Result<(), Error> {
// Text inside a `Rope` is stored as non-contiguous blocks of data called
@@ -284,12 +297,12 @@ pub async fn to_writer<'a, W: tokio::io::AsyncWriteExt + Unpin + ?Sized>(
total_read += read;
total_written += written;
match result {
- encoding_rs::CoderResult::InputEmpty => {
+ encoding::CoderResult::InputEmpty => {
debug_assert_eq!(chunk.len(), total_read);
debug_assert!(buf.len() >= total_written);
break;
}
- encoding_rs::CoderResult::OutputFull => {
+ encoding::CoderResult::OutputFull => {
debug_assert!(chunk.len() > total_read);
writer.write_all(&buf[..total_written]).await?;
total_written = 0;
@@ -320,8 +333,8 @@ use helix_lsp::lsp;
use url::Url;
impl Document {
- pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Self {
- let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
+ pub fn from(text: Rope, encoding: Option<&'static encoding::Encoding>) -> Self {
+ let encoding = encoding.unwrap_or(encoding::UTF_8);
let changes = ChangeSet::new(&text);
let old_state = None;
@@ -344,6 +357,7 @@ impl Document {
history: Cell::new(History::default()),
savepoint: None,
last_saved_revision: 0,
+ modified_since_accessed: false,
language_server: None,
}
}
@@ -353,9 +367,8 @@ impl Document {
/// overwritten with the `encoding` parameter.
pub fn open(
path: &Path,
- encoding: Option<&'static encoding_rs::Encoding>,
- theme: Option<&Theme>,
- config_loader: Option<&syntax::Loader>,
+ encoding: Option<&'static encoding::Encoding>,
+ config_loader: Option<Arc<syntax::Loader>>,
) -> Result<Self, Error> {
// Open the file if it exists, otherwise assume it is a new file (and thus empty).
let (rope, encoding) = if path.exists() {
@@ -363,7 +376,7 @@ impl Document {
std::fs::File::open(path).context(format!("unable to open {:?}", path))?;
from_reader(&mut file, encoding)?
} else {
- let encoding = encoding.unwrap_or(encoding_rs::UTF_8);
+ let encoding = encoding.unwrap_or(encoding::UTF_8);
(Rope::from(DEFAULT_LINE_ENDING.as_str()), encoding)
};
@@ -372,7 +385,7 @@ impl Document {
// set the path and try detecting the language
doc.set_path(Some(path))?;
if let Some(loader) = config_loader {
- doc.detect_language(theme, loader);
+ doc.detect_language(loader);
}
doc.detect_indent_and_line_ending();
@@ -383,7 +396,7 @@ impl Document {
/// The same as [`format`], but only returns formatting changes if auto-formatting
/// is configured.
pub fn auto_format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
- if self.language_config().map(|c| c.auto_format) == Some(true) {
+ if self.language_config()?.auto_format {
self.format()
} else {
None
@@ -393,30 +406,27 @@ impl Document {
/// If supported, returns the changes that should be applied to this document in order
/// to format it nicely.
pub fn format(&self) -> Option<impl Future<Output = LspFormatting> + 'static> {
- if let Some(language_server) = self.language_server() {
- let text = self.text.clone();
- let offset_encoding = language_server.offset_encoding();
- let request = language_server.text_document_formatting(
- self.identifier(),
- lsp::FormattingOptions::default(),
- None,
- )?;
-
- let fut = async move {
- let edits = request.await.unwrap_or_else(|e| {
- log::warn!("LSP formatting failed: {}", e);
- Default::default()
- });
- LspFormatting {
- doc: text,
- edits,
- offset_encoding,
- }
- };
- Some(fut)
- } else {
- None
- }
+ let language_server = self.language_server()?;
+ let text = self.text.clone();
+ let offset_encoding = language_server.offset_encoding();
+ let request = language_server.text_document_formatting(
+ self.identifier(),
+ lsp::FormattingOptions::default(),
+ None,
+ )?;
+
+ let fut = async move {
+ let edits = request.await.unwrap_or_else(|e| {
+ log::warn!("LSP formatting failed: {}", e);
+ Default::default()
+ });
+ LspFormatting {
+ doc: text,
+ edits,
+ offset_encoding,
+ }
+ };
+ Some(fut)
}
pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
@@ -460,9 +470,7 @@ impl Document {
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
- return Err(Error::msg(
- "can't save file, parent directory does not exist",
- ));
+ bail!("can't save file, parent directory does not exist");
}
}
@@ -494,12 +502,12 @@ impl Document {
}
/// Detect the programming language based on the file type.
- pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
+ pub fn detect_language(&mut self, config_loader: Arc<syntax::Loader>) {
if let Some(path) = &self.path {
let language_config = config_loader
.language_config_for_file_name(path)
.or_else(|| config_loader.language_config_for_shebang(self.text()));
- self.set_language(theme, language_config);
+ self.set_language(language_config, Some(config_loader));
}
}
@@ -509,8 +517,7 @@ impl Document {
/// line ending.
pub fn detect_indent_and_line_ending(&mut self) {
self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
- self.language
- .as_ref()
+ self.language_config()
.and_then(|config| config.indent.as_ref())
.map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
});
@@ -524,7 +531,7 @@ impl Document {
// If there is no path or the path no longer exists.
if path.is_none() {
- return Err(anyhow!("can't find file to reload from"));
+ bail!("can't find file to reload from");
}
let mut file = std::fs::File::open(path.unwrap())?;
@@ -545,15 +552,13 @@ impl Document {
/// Sets the [`Document`]'s encoding with the encoding correspondent to `label`.
pub fn set_encoding(&mut self, label: &str) -> Result<(), Error> {
- match encoding_rs::Encoding::for_label(label.as_bytes()) {
- Some(encoding) => self.encoding = encoding,
- None => return Err(anyhow::anyhow!("unknown encoding")),
- }
+ self.encoding = encoding::Encoding::for_label(label.as_bytes())
+ .ok_or_else(|| anyhow!("unknown encoding"))?;
Ok(())
}
/// Returns the [`Document`]'s current encoding.
- pub fn encoding(&self) -> &'static encoding_rs::Encoding {
+ pub fn encoding(&self) -> &'static encoding::Encoding {
self.encoding
}
@@ -573,15 +578,13 @@ impl Document {
/// if it exists.
pub fn set_language(
&mut self,
- theme: Option<&Theme>,
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
+ loader: Option<Arc<helix_core::syntax::Loader>>,
) {
- if let Some(language_config) = language_config {
- let scopes = theme.map(|theme| theme.scopes()).unwrap_or(&[]);
- if let Some(highlight_config) = language_config.highlight_config(scopes) {
- let syntax = Syntax::new(&self.text, highlight_config);
+ if let (Some(language_config), Some(loader)) = (language_config, loader) {
+ if let Some(highlight_config) = language_config.highlight_config(&loader.scopes()) {
+ let syntax = Syntax::new(&self.text, highlight_config, loader);
self.syntax = Some(syntax);
- // TODO: config.configure(scopes) is now delayed, is that ok?
}
self.language = Some(language_config);
@@ -593,15 +596,10 @@ impl Document {
/// Set the programming language for the file if you know the name (scope) but don't have the
/// [`syntax::LanguageConfiguration`] for it.
- pub fn set_language2(
- &mut self,
- scope: &str,
- theme: Option<&Theme>,
- config_loader: Arc<syntax::Loader>,
- ) {
+ pub fn set_language2(&mut self, scope: &str, config_loader: Arc<syntax::Loader>) {
let language_config = config_loader.language_config_for_scope(scope);
- self.set_language(theme, language_config);
+ self.set_language(language_config, Some(config_loader));
}
/// Set the LSP.
@@ -639,6 +637,8 @@ impl Document {
selection.clone().ensure_invariants(self.text.slice(..)),
);
}
+
+ self.modified_since_accessed = true;
}
if !transaction.changes().is_empty() {
@@ -680,7 +680,7 @@ impl Document {
if let Some(notify) = notify {
tokio::spawn(notify);
- } //.expect("failed to emit textDocument/didChange");
+ }
}
}
success
@@ -708,11 +708,11 @@ impl Document {
success
}
- /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
- pub fn undo(&mut self, view_id: ViewId) -> bool {
+ fn undo_redo_impl(&mut self, view_id: ViewId, undo: bool) -> bool {
let mut history = self.history.take();
- let success = if let Some(transaction) = history.undo() {
- self.apply_impl(transaction, view_id)
+ let txn = if undo { history.undo() } else { history.redo() };
+ let success = if let Some(txn) = txn {
+ self.apply_impl(txn, view_id)
} else {
false
};
@@ -725,21 +725,14 @@ impl Document {
success
}
+ /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
+ pub fn undo(&mut self, view_id: ViewId) -> bool {
+ self.undo_redo_impl(view_id, true)
+ }
+
/// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
pub fn redo(&mut self, view_id: ViewId) -> bool {
- let mut history = self.history.take();
- let success = if let Some(transaction) = history.redo() {
- self.apply_impl(transaction, view_id)
- } else {
- false
- };
- self.history.set(history);
-
- if success {
- // reset changeset to fix len
- self.changes = ChangeSet::new(self.text());
- }
- success
+ self.undo_redo_impl(view_id, false)
}
pub fn savepoint(&mut self) {
@@ -752,9 +745,12 @@ impl Document {
}
}
- /// Undo modifications to the [`Document`] according to `uk`.
- pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
- let txns = self.history.get_mut().earlier(uk);
+ fn earlier_later_impl(&mut self, view_id: ViewId, uk: UndoKind, earlier: bool) -> bool {
+ let txns = if earlier {
+ self.history.get_mut().earlier(uk)
+ } else {
+ self.history.get_mut().later(uk)
+ };
let mut success = false;
for txn in txns {
if self.apply_impl(&txn, view_id) {
@@ -768,20 +764,14 @@ impl Document {
success
}
+ /// Undo modifications to the [`Document`] according to `uk`.
+ pub fn earlier(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+ self.earlier_later_impl(view_id, uk, true)
+ }
+
/// Redo modifications to the [`Document`] according to `uk`.
- pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
- let txns = self.history.get_mut().later(uk);
- let mut success = false;
- for txn in txns {
- if self.apply_impl(&txn, view_id) {
- success = true;
- }
- }
- if success {
- // reset changeset to fix len
- self.changes = ChangeSet::new(self.text());
- }
- success
+ pub fn later(&mut self, view_id: ViewId, uk: UndoKind) -> bool {
+ self.earlier_later_impl(view_id, uk, false)
}
/// Commit pending changes to history
@@ -837,6 +827,16 @@ impl Document {
.map(|language| language.scope.as_str())
}
+ /// Language ID for the document. Either the `language-id` from the
+ /// `language-server` configuration, or the document language if no
+ /// `language-id` has been specified.
+ pub fn language_id(&self) -> Option<&str> {
+ self.language_config()
+ .and_then(|config| config.language_server.as_ref())
+ .and_then(|lsp_config| lsp_config.language_id.as_deref())
+ .or_else(|| Some(self.language()?.rsplit_once('.')?.1))
+ }
+
/// Corresponding [`LanguageConfiguration`].
pub fn language_config(&self) -> Option<&LanguageConfiguration> {
self.language.as_deref()
@@ -847,18 +847,10 @@ impl Document {
self.version
}
+ /// Language server if it has been initialized.
pub fn language_server(&self) -> Option<&helix_lsp::Client> {
- let server = self.language_server.as_deref();
- let initialized = server
- .map(|server| server.is_initialized())
- .unwrap_or(false);
-
- // only resolve language_server if it's initialized
- if initialized {
- server
- } else {
- None
- }
+ let server = self.language_server.as_deref()?;
+ server.is_initialized().then(|| server)
}
#[inline]
@@ -869,8 +861,7 @@ impl Document {
/// Tab size in columns.
pub fn tab_width(&self) -> usize {
- self.language
- .as_ref()
+ self.language_config()
.and_then(|config| config.indent.as_ref())
.map_or(4, |config| config.tab_width) // fallback to 4 columns
}
@@ -883,6 +874,10 @@ impl Document {
self.indent_style.as_str()
}
+ pub fn changes(&self) -> &ChangeSet {
+ &self.changes
+ }
+
#[inline]
/// File path on disk.
pub fn path(&self) -> Option<&PathBuf> {
@@ -891,7 +886,7 @@ impl Document {
/// File path as a URL.
pub fn url(&self) -> Option<Url> {
- self.path().map(|path| Url::from_file_path(path).unwrap())
+ Url::from_file_path(self.path()?).ok()
}
#[inline]
@@ -914,10 +909,6 @@ impl Document {
.map(helix_core::path::get_relative_path)
}
- // pub fn slice<R>(&self, range: R) -> RopeSlice where R: RangeBounds {
- // self.state.doc.slice
- // }
-
// transact(Fn) ?
// -- LSP methods
@@ -938,7 +929,6 @@ impl Document {
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
self.diagnostics = diagnostics;
- // sort by range
self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range);
}
@@ -1113,7 +1103,7 @@ mod test {
macro_rules! test_decode {
($label:expr, $label_override:expr) => {
- let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+ let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
let path = base_path.join(format!("{}_in.txt", $label));
let ref_path = base_path.join(format!("{}_in_ref.txt", $label));
@@ -1132,7 +1122,7 @@ mod test {
macro_rules! test_encode {
($label:expr, $label_override:expr) => {
- let encoding = encoding_rs::Encoding::for_label($label_override.as_bytes()).unwrap();
+ let encoding = encoding::Encoding::for_label($label_override.as_bytes()).unwrap();
let base_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/encoding");
let path = base_path.join(format!("{}_out.txt", $label));
let ref_path = base_path.join(format!("{}_out_ref.txt", $label));
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index c7b3baef..2e6121bc 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -1,7 +1,9 @@
use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
- document::SCRATCH_BUFFER_NAME,
+ document::{Mode, SCRATCH_BUFFER_NAME},
graphics::{CursorKind, Rect},
+ info::Info,
+ input::KeyEvent,
theme::{self, Theme},
tree::{self, Tree},
Document, DocumentId, View, ViewId,
@@ -22,7 +24,7 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep};
-use anyhow::{bail, Context, Error};
+use anyhow::{bail, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
@@ -30,7 +32,7 @@ use helix_core::syntax;
use helix_core::{Position, Selection};
use helix_dap as dap;
-use serde::Deserialize;
+use serde::{ser::SerializeMap, Deserialize, Deserializer, Serialize};
fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::Error>
where
@@ -40,7 +42,7 @@ where
Ok(Duration::from_millis(millis))
}
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct FilePickerConfig {
/// IgnoreOptions
@@ -80,7 +82,7 @@ impl Default for FilePickerConfig {
}
}
-#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config {
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
@@ -95,8 +97,6 @@ pub struct Config {
pub line_number: LineNumber,
/// Middle click paste support. Defaults to true.
pub middle_click_paste: bool,
- /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
- pub smart_case: bool,
/// Automatic insertion of pairs to parentheses, brackets, etc. Defaults to true.
pub auto_pairs: bool,
/// Automatic auto-completion, automatically pop up without user trigger. Defaults to true.
@@ -108,18 +108,101 @@ pub struct Config {
/// Whether to display infoboxes. Defaults to true.
pub auto_info: bool,
pub file_picker: FilePickerConfig,
+ /// Shape for cursor in each mode
+ pub cursor_shape: CursorShapeConfig,
+ /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`.
+ pub true_color: bool,
+ /// Search configuration.
+ #[serde(default)]
+ pub search: SearchConfig,
+}
+
+#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+pub struct SearchConfig {
+ /// Smart case: Case insensitive searching unless pattern contains upper case characters. Defaults to true.
+ pub smart_case: bool,
+ /// Whether the search should wrap after depleting the matches. Default to true.
+ pub wrap_around: bool,
+}
+
+// Cursor shape is read and used on every rendered frame and so needs
+// to be fast. Therefore we avoid a hashmap and use an enum indexed array.
+#[derive(Debug, Clone, PartialEq)]
+pub struct CursorShapeConfig([CursorKind; 3]);
+
+impl CursorShapeConfig {
+ pub fn from_mode(&self, mode: Mode) -> CursorKind {
+ self.get(mode as usize).copied().unwrap_or_default()
+ }
}
-#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
+impl<'de> Deserialize<'de> for CursorShapeConfig {
+ fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
+ where
+ D: Deserializer<'de>,
+ {
+ let m = HashMap::<Mode, CursorKind>::deserialize(deserializer)?;
+ let into_cursor = |mode: Mode| m.get(&mode).copied().unwrap_or_default();
+ Ok(CursorShapeConfig([
+ into_cursor(Mode::Normal),
+ into_cursor(Mode::Select),
+ into_cursor(Mode::Insert),
+ ]))
+ }
+}
+
+impl Serialize for CursorShapeConfig {
+ fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
+ where
+ S: serde::Serializer,
+ {
+ let mut map = serializer.serialize_map(Some(self.len()))?;
+ let modes = [Mode::Normal, Mode::Select, Mode::Insert];
+ for mode in modes {
+ map.serialize_entry(&mode, &self.from_mode(mode))?;
+ }
+ map.end()
+ }
+}
+
+impl std::ops::Deref for CursorShapeConfig {
+ type Target = [CursorKind; 3];
+
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
+
+impl Default for CursorShapeConfig {
+ fn default() -> Self {
+ Self([CursorKind::Block; 3])
+ }
+}
+
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
Absolute,
- /// Show relative line number to the primary cursor
+ /// If focused and in normal/select mode, show relative line number to the primary cursor.
+ /// If unfocused or in insert mode, show absolute line number.
Relative,
}
+impl std::str::FromStr for LineNumber {
+ type Err = anyhow::Error;
+
+ fn from_str(s: &str) -> Result<Self, Self::Err> {
+ match s.to_lowercase().as_str() {
+ "absolute" | "abs" => Ok(Self::Absolute),
+ "relative" | "rel" => Ok(Self::Relative),
+ _ => anyhow::bail!("Line number can only be `absolute` or `relative`."),
+ }
+ }
+}
+
impl Default for Config {
fn default() -> Self {
Self {
@@ -133,13 +216,24 @@ impl Default for Config {
},
line_number: LineNumber::Absolute,
middle_click_paste: true,
- smart_case: true,
auto_pairs: true,
auto_completion: true,
idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2,
auto_info: true,
file_picker: FilePickerConfig::default(),
+ cursor_shape: CursorShapeConfig::default(),
+ true_color: false,
+ search: SearchConfig::default(),
+ }
+ }
+}
+
+impl Default for SearchConfig {
+ fn default() -> Self {
+ Self {
+ wrap_around: true,
+ smart_case: true,
}
}
}
@@ -177,6 +271,7 @@ pub struct Editor {
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
+ pub macro_recording: Option<(char, Vec<KeyEvent>)>,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
@@ -190,6 +285,7 @@ pub struct Editor {
pub theme_loader: Arc<theme::Loader>,
pub status_msg: Option<(String, Severity)>,
+ pub autoinfo: Option<Info>,
pub config: Config,
@@ -225,6 +321,7 @@ impl Editor {
documents: BTreeMap::new(),
count: None,
selected_register: None,
+ macro_recording: None,
theme: theme_loader.default(),
language_servers,
debugger: None,
@@ -235,6 +332,7 @@ impl Editor {
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
+ autoinfo: None,
idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
config,
@@ -275,31 +373,16 @@ impl Editor {
}
let scopes = theme.scopes();
- for config in self
- .syn_loader
- .language_configs_iter()
- .filter(|cfg| cfg.is_highlight_initialized())
- {
- config.reconfigure(scopes);
- }
+ self.syn_loader.set_scopes(scopes.to_vec());
self.theme = theme;
self._refresh();
}
- pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
- let theme = self
- .theme_loader
- .load(theme.as_ref())
- .with_context(|| format!("failed setting theme `{}`", theme))?;
- self.set_theme(theme);
- Ok(())
- }
-
/// Refreshes the language server for a given document
pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
let doc = self.documents.get_mut(&doc_id)?;
- doc.detect_language(Some(&self.theme), &self.syn_loader);
+ doc.detect_language(self.syn_loader.clone());
Self::launch_language_server(&mut self.language_servers, doc)
}
@@ -323,11 +406,8 @@ impl Editor {
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
- let language_id = doc
- .language()
- .and_then(|s| s.split('.').last()) // source.rust
- .map(ToOwned::to_owned)
- .unwrap_or_default();
+
+ let language_id = doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
// TODO: this now races with on_init code if the init happens too quickly
tokio::spawn(language_server.text_document_did_open(
@@ -394,7 +474,8 @@ impl Editor {
.tree
.traverse()
.any(|(_, v)| v.doc == doc.id && v.id != view.id);
- let view = view_mut!(self);
+
+ let (view, doc) = current!(self);
if remove_empty_scratch {
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
// borrow, invalidating direct access to `doc.id`.
@@ -403,7 +484,16 @@ impl Editor {
} else {
let jump = (view.doc, doc.selection(view.id).clone());
view.jumps.push(jump);
- view.last_accessed_doc = Some(view.doc);
+ // Set last accessed doc if it is a different document
+ if doc.id != id {
+ view.last_accessed_doc = Some(view.doc);
+ // Set last modified doc if modified and last modified doc is different
+ if std::mem::take(&mut doc.modified_since_accessed)
+ && view.last_modified_docs[0] != Some(view.doc)
+ {
+ view.last_modified_docs = [Some(view.doc), view.last_modified_docs[0]];
+ }
+ }
}
let view_id = view.id;
@@ -471,7 +561,7 @@ impl Editor {
let id = if let Some(id) = id {
id
} else {
- let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
+ let mut doc = Document::open(&path, None, Some(self.syn_loader.clone()))?;
let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
@@ -629,9 +719,10 @@ impl Editor {
let inner = view.inner_area();
pos.col += inner.x as usize;
pos.row += inner.y as usize;
- (Some(pos), CursorKind::Hidden)
+ let cursorkind = self.config.cursor_shape.from_mode(doc.mode());
+ (Some(pos), cursorkind)
} else {
- (None, CursorKind::Hidden)
+ (None, CursorKind::default())
}
}
diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs
index 0bfca04a..6d0a9292 100644
--- a/helix-view/src/graphics.rs
+++ b/helix-view/src/graphics.rs
@@ -1,10 +1,12 @@
use bitflags::bitflags;
+use serde::{Deserialize, Serialize};
use std::{
cmp::{max, min},
str::FromStr,
};
-#[derive(Debug, Clone, Copy, PartialEq)]
+#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)]
+#[serde(rename_all = "lowercase")]
/// UNSTABLE
pub enum CursorKind {
/// █
@@ -17,6 +19,12 @@ pub enum CursorKind {
Hidden,
}
+impl Default for CursorKind {
+ fn default() -> Self {
+ Self::Block
+ }
+}
+
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub vertical: u16,
@@ -25,7 +33,7 @@ pub struct Margin {
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to. (x, y) = (0, 0) is at the top left corner of the screen.
-#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
+#[derive(Debug, Default, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
@@ -33,17 +41,6 @@ pub struct Rect {
pub height: u16,
}
-impl Default for Rect {
- fn default() -> Rect {
- Rect {
- x: 0,
- y: 0,
- width: 0,
- height: 0,
- }
- }
-}
-
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
@@ -334,7 +331,7 @@ impl FromStr for Modifier {
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
-/// buffer.get_mut(0, 0).set_style(*style);
+/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -343,7 +340,7 @@ impl FromStr for Modifier {
/// add_modifier: Modifier::BOLD,
/// sub_modifier: Modifier::empty(),
/// },
-/// buffer.get(0, 0).style(),
+/// buffer[(0, 0)].style(),
/// );
/// ```
///
@@ -359,7 +356,7 @@ impl FromStr for Modifier {
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
-/// buffer.get_mut(0, 0).set_style(*style);
+/// buffer[(0, 0)].set_style(*style);
/// }
/// assert_eq!(
/// Style {
@@ -368,7 +365,7 @@ impl FromStr for Modifier {
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
-/// buffer.get(0, 0).style(),
+/// buffer[(0, 0)].style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
index e156b9e5..6a77c41f 100644
--- a/helix-view/src/gutter.rs
+++ b/helix-view/src/gutter.rs
@@ -25,7 +25,8 @@ pub fn diagnostic<'doc>(
Box::new(move |line: usize, _selected: bool, out: &mut String| {
use helix_core::diagnostic::Severity;
- if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) {
+ if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) {
+ let diagnostic = &diagnostics[index];
write!(out, "●").unwrap();
return Some(match diagnostic.severity {
Some(Severity::Error) => error,
@@ -60,29 +61,31 @@ pub fn line_number<'doc>(
.char_to_line(doc.selection(view.id).primary().cursor(text));
let config = editor.config.line_number;
+ let mode = doc.mode;
Box::new(move |line: usize, selected: bool, out: &mut String| {
if line == last_line && !draw_last {
write!(out, "{:>1$}", '~', width).unwrap();
Some(linenr)
} else {
- use crate::editor::LineNumber;
- let line = match config {
- LineNumber::Absolute => line + 1,
- LineNumber::Relative => {
- if current_line == line {
- line + 1
- } else {
- abs_diff(current_line, line)
- }
- }
+ use crate::{document::Mode, editor::LineNumber};
+
+ let relative = config == LineNumber::Relative
+ && mode != Mode::Insert
+ && is_focused
+ && current_line != line;
+
+ let display_num = if relative {
+ abs_diff(current_line, line)
+ } else {
+ line + 1
};
let style = if selected && is_focused {
linenr_select
} else {
linenr
};
- write!(out, "{:>1$}", line, width).unwrap();
+ write!(out, "{:>1$}", display_num, width).unwrap();
Some(style)
}
})
diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs
index b5a002fa..5ad6a60c 100644
--- a/helix-view/src/info.rs
+++ b/helix-view/src/info.rs
@@ -1,5 +1,5 @@
use crate::input::KeyEvent;
-use helix_core::unicode::width::UnicodeWidthStr;
+use helix_core::{register::Registers, unicode::width::UnicodeWidthStr};
use std::{collections::BTreeSet, fmt::Write};
#[derive(Debug)]
@@ -16,33 +16,60 @@ pub struct Info {
}
impl Info {
- pub fn new(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Info {
- let body = body
- .into_iter()
- .map(|(desc, events)| {
- let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
- (desc, events.join(", "))
- })
- .collect::<Vec<_>>();
+ pub fn new(title: &str, body: Vec<(String, String)>) -> Self {
+ if body.is_empty() {
+ return Self {
+ title: title.to_string(),
+ height: 1,
+ width: title.len() as u16,
+ text: "".to_string(),
+ };
+ }
- let keymaps_width = body.iter().map(|r| r.1.len()).max().unwrap();
+ let item_width = body.iter().map(|(item, _)| item.width()).max().unwrap();
let mut text = String::new();
- for (desc, keyevents) in &body {
- let _ = writeln!(
- text,
- "{:width$} {}",
- keyevents,
- desc,
- width = keymaps_width
- );
+ for (item, desc) in &body {
+ let _ = writeln!(text, "{:width$} {}", item, desc, width = item_width);
}
- Info {
+ Self {
title: title.to_string(),
width: text.lines().map(|l| l.width()).max().unwrap() as u16,
height: body.len() as u16,
text,
}
}
+
+ pub fn from_keymap(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Self {
+ let body = body
+ .into_iter()
+ .map(|(desc, events)| {
+ let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
+ (events.join(", "), desc.to_string())
+ })
+ .collect();
+
+ Self::new(title, body)
+ }
+
+ pub fn from_registers(registers: &Registers) -> Self {
+ let body = registers
+ .inner()
+ .iter()
+ .map(|(ch, reg)| {
+ let content = reg
+ .read()
+ .get(0)
+ .and_then(|s| s.lines().next())
+ .map(String::from)
+ .unwrap_or_default();
+ (ch.to_string(), content)
+ })
+ .collect();
+
+ let mut infobox = Self::new("Registers", body);
+ infobox.width = 30; // copied content could be very long
+ infobox
+ }
}
diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs
index 580204cc..14dadc3b 100644
--- a/helix-view/src/input.rs
+++ b/helix-view/src/input.rs
@@ -36,7 +36,6 @@ pub(crate) mod keys {
pub(crate) const PAGEUP: &str = "pageup";
pub(crate) const PAGEDOWN: &str = "pagedown";
pub(crate) const TAB: &str = "tab";
- pub(crate) const BACKTAB: &str = "backtab";
pub(crate) const DELETE: &str = "del";
pub(crate) const INSERT: &str = "ins";
pub(crate) const NULL: &str = "null";
@@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent {
KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
KeyCode::Tab => f.write_str(keys::TAB)?,
- KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
KeyCode::Delete => f.write_str(keys::DELETE)?,
KeyCode::Insert => f.write_str(keys::INSERT)?,
KeyCode::Null => f.write_str(keys::NULL)?,
@@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent {
KeyCode::PageUp => keys::PAGEUP.len(),
KeyCode::PageDown => keys::PAGEDOWN.len(),
KeyCode::Tab => keys::TAB.len(),
- KeyCode::BackTab => keys::BACKTAB.len(),
KeyCode::Delete => keys::DELETE.len(),
KeyCode::Insert => keys::INSERT.len(),
KeyCode::Null => keys::NULL.len(),
@@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent {
keys::PAGEUP => KeyCode::PageUp,
keys::PAGEDOWN => KeyCode::PageDown,
keys::TAB => KeyCode::Tab,
- keys::BACKTAB => KeyCode::BackTab,
keys::DELETE => KeyCode::Delete,
keys::INSERT => KeyCode::Insert,
keys::NULL => KeyCode::Null,
@@ -220,14 +216,79 @@ impl<'de> Deserialize<'de> for KeyEvent {
#[cfg(feature = "term")]
impl From<crossterm::event::KeyEvent> for KeyEvent {
- fn from(
- crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent,
- ) -> KeyEvent {
- KeyEvent {
- code: code.into(),
- modifiers: modifiers.into(),
+ fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self {
+ if code == crossterm::event::KeyCode::BackTab {
+ // special case for BackTab -> Shift-Tab
+ let mut modifiers: KeyModifiers = modifiers.into();
+ modifiers.insert(KeyModifiers::SHIFT);
+ Self {
+ code: KeyCode::Tab,
+ modifiers,
+ }
+ } else {
+ Self {
+ code: code.into(),
+ modifiers: modifiers.into(),
+ }
+ }
+ }
+}
+
+#[cfg(feature = "term")]
+impl From<KeyEvent> for crossterm::event::KeyEvent {
+ fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self {
+ if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) {
+ // special case for Shift-Tab -> BackTab
+ let mut modifiers = modifiers;
+ modifiers.remove(KeyModifiers::SHIFT);
+ crossterm::event::KeyEvent {
+ code: crossterm::event::KeyCode::BackTab,
+ modifiers: modifiers.into(),
+ }
+ } else {
+ crossterm::event::KeyEvent {
+ code: code.into(),
+ modifiers: modifiers.into(),
+ }
+ }
+ }
+}
+
+pub fn parse_macro(keys_str: &str) -> anyhow::Result<Vec<KeyEvent>> {
+ use anyhow::Context;
+ let mut keys_res: anyhow::Result<_> = Ok(Vec::new());
+ let mut i = 0;
+ while let Ok(keys) = &mut keys_res {
+ if i >= keys_str.len() {
+ break;
+ }
+ if !keys_str.is_char_boundary(i) {
+ i += 1;
+ continue;
+ }
+
+ let s = &keys_str[i..];
+ let mut end_i = 1;
+ while !s.is_char_boundary(end_i) {
+ end_i += 1;
+ }
+ let c = &s[..end_i];
+ if c == ">" {
+ keys_res = Err(anyhow!("Unmatched '>'"));
+ } else if c != "<" {
+ keys.push(c);
+ i += end_i;
+ } else {
+ match s.find('>').context("'>' expected") {
+ Ok(end_i) => {
+ keys.push(&s[1..end_i]);
+ i += end_i + 1;
+ }
+ Err(err) => keys_res = Err(err),
+ }
}
}
+ keys_res.and_then(|keys| keys.into_iter().map(str::parse).collect())
}
#[cfg(test)]
@@ -315,4 +376,120 @@ mod test {
assert!(str::parse::<KeyEvent>("123").is_err());
assert!(str::parse::<KeyEvent>("S--").is_err());
}
+
+ #[test]
+ fn parsing_valid_macros() {
+ assert_eq!(
+ parse_macro("xdo").ok(),
+ Some(vec![
+ KeyEvent {
+ code: KeyCode::Char('x'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('d'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::NONE,
+ },
+ ]),
+ );
+
+ assert_eq!(
+ parse_macro("<C-w>v<C-w>h<C-o>xx<A-s>").ok(),
+ Some(vec![
+ KeyEvent {
+ code: KeyCode::Char('w'),
+ modifiers: KeyModifiers::CONTROL,
+ },
+ KeyEvent {
+ code: KeyCode::Char('v'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('w'),
+ modifiers: KeyModifiers::CONTROL,
+ },
+ KeyEvent {
+ code: KeyCode::Char('h'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::CONTROL,
+ },
+ KeyEvent {
+ code: KeyCode::Char('x'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('x'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('s'),
+ modifiers: KeyModifiers::ALT,
+ },
+ ])
+ );
+
+ assert_eq!(
+ parse_macro(":o foo.bar<ret>").ok(),
+ Some(vec![
+ KeyEvent {
+ code: KeyCode::Char(':'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char(' '),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('f'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('o'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('.'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('b'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('a'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Char('r'),
+ modifiers: KeyModifiers::NONE,
+ },
+ KeyEvent {
+ code: KeyCode::Enter,
+ modifiers: KeyModifiers::NONE,
+ },
+ ])
+ );
+ }
+
+ #[test]
+ fn parsing_invalid_macros_fails() {
+ assert!(parse_macro("abc<C-").is_err());
+ assert!(parse_macro("abc>123").is_err());
+ assert!(parse_macro("wd<foo>").is_err());
+ }
}
diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs
index 810aa063..f1717209 100644
--- a/helix-view/src/keyboard.rs
+++ b/helix-view/src/keyboard.rs
@@ -79,8 +79,6 @@ pub enum KeyCode {
PageDown,
/// Tab key.
Tab,
- /// Shift + Tab key.
- BackTab,
/// Delete key.
Delete,
/// Insert key.
@@ -116,7 +114,6 @@ impl From<KeyCode> for crossterm::event::KeyCode {
KeyCode::PageUp => CKeyCode::PageUp,
KeyCode::PageDown => CKeyCode::PageDown,
KeyCode::Tab => CKeyCode::Tab,
- KeyCode::BackTab => CKeyCode::BackTab,
KeyCode::Delete => CKeyCode::Delete,
KeyCode::Insert => CKeyCode::Insert,
KeyCode::F(f_number) => CKeyCode::F(f_number),
@@ -144,7 +141,7 @@ impl From<crossterm::event::KeyCode> for KeyCode {
CKeyCode::PageUp => KeyCode::PageUp,
CKeyCode::PageDown => KeyCode::PageDown,
CKeyCode::Tab => KeyCode::Tab,
- CKeyCode::BackTab => KeyCode::BackTab,
+ CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"),
CKeyCode::Delete => KeyCode::Delete,
CKeyCode::Insert => KeyCode::Insert,
CKeyCode::F(f_number) => KeyCode::F(f_number),
diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs
index 757316bd..00c1bbbd 100644
--- a/helix-view/src/theme.rs
+++ b/helix-view/src/theme.rs
@@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style};
pub static DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme")
});
+pub static BASE16_DEFAULT_THEME: Lazy<Theme> = Lazy::new(|| {
+ toml::from_slice(include_bytes!("../../base16_theme.toml"))
+ .expect("Failed to parse base 16 default theme")
+});
#[derive(Clone, Debug)]
pub struct Loader {
@@ -35,6 +39,9 @@ impl Loader {
if name == "default" {
return Ok(self.default());
}
+ if name == "base16_default" {
+ return Ok(self.base16_default());
+ }
let filename = format!("{}.toml", name);
let user_path = self.user_dir.join(&filename);
@@ -74,12 +81,20 @@ impl Loader {
pub fn default(&self) -> Theme {
DEFAULT_THEME.clone()
}
+
+ /// Returns the alternative 16-color default theme
+ pub fn base16_default(&self) -> Theme {
+ BASE16_DEFAULT_THEME.clone()
+ }
}
#[derive(Clone, Debug)]
pub struct Theme {
- scopes: Vec<String>,
+ // UI styles are stored in a HashMap
styles: HashMap<String, Style>,
+ // tree-sitter highlight styles are stored in a Vec to optimize lookups
+ scopes: Vec<String>,
+ highlights: Vec<Style>,
}
impl<'de> Deserialize<'de> for Theme {
@@ -88,6 +103,8 @@ impl<'de> Deserialize<'de> for Theme {
D: Deserializer<'de>,
{
let mut styles = HashMap::new();
+ let mut scopes = Vec::new();
+ let mut highlights = Vec::new();
if let Ok(mut colors) = HashMap::<String, Value>::deserialize(deserializer) {
// TODO: alert user of parsing failures in editor
@@ -102,24 +119,38 @@ impl<'de> Deserialize<'de> for Theme {
.unwrap_or_default();
styles.reserve(colors.len());
+ scopes.reserve(colors.len());
+ highlights.reserve(colors.len());
+
for (name, style_value) in colors {
let mut style = Style::default();
if let Err(err) = palette.parse_style(&mut style, style_value) {
warn!("{}", err);
}
- styles.insert(name, style);
+
+ // these are used both as UI and as highlights
+ styles.insert(name.clone(), style);
+ scopes.push(name);
+ highlights.push(style);
}
}
- let scopes = styles.keys().map(ToString::to_string).collect();
- Ok(Self { scopes, styles })
+ Ok(Self {
+ scopes,
+ styles,
+ highlights,
+ })
}
}
impl Theme {
+ #[inline]
+ pub fn highlight(&self, index: usize) -> Style {
+ self.highlights[index]
+ }
+
pub fn get(&self, scope: &str) -> Style {
- self.try_get(scope)
- .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255)))
+ self.try_get(scope).unwrap_or_default()
}
pub fn try_get(&self, scope: &str) -> Option<Style> {
@@ -134,6 +165,14 @@ impl Theme {
pub fn find_scope_index(&self, scope: &str) -> Option<usize> {
self.scopes().iter().position(|s| s == scope)
}
+
+ pub fn is_16_color(&self) -> bool {
+ self.styles.iter().all(|(_, style)| {
+ [style.fg, style.bg]
+ .into_iter()
+ .all(|color| !matches!(color, Some(Color::Rgb(..))))
+ })
+ }
}
struct ThemePalette {
@@ -257,53 +296,58 @@ impl TryFrom<Value> for ThemePalette {
}
}
-#[test]
-fn test_parse_style_string() {
- let fg = Value::String("#ffffff".to_string());
+#[cfg(test)]
+mod tests {
+ use super::*;
- let mut style = Style::default();
- let palette = ThemePalette::default();
- palette.parse_style(&mut style, fg).unwrap();
+ #[test]
+ fn test_parse_style_string() {
+ let fg = Value::String("#ffffff".to_string());
- assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
-}
+ let mut style = Style::default();
+ let palette = ThemePalette::default();
+ palette.parse_style(&mut style, fg).unwrap();
-#[test]
-fn test_palette() {
- use helix_core::hashmap;
- let fg = Value::String("my_color".to_string());
+ assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
+ }
- let mut style = Style::default();
- let palette =
- ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
- palette.parse_style(&mut style, fg).unwrap();
+ #[test]
+ fn test_palette() {
+ use helix_core::hashmap;
+ let fg = Value::String("my_color".to_string());
- assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
-}
+ let mut style = Style::default();
+ let palette =
+ ThemePalette::new(hashmap! { "my_color".to_string() => Color::Rgb(255, 255, 255) });
+ palette.parse_style(&mut style, fg).unwrap();
-#[test]
-fn test_parse_style_table() {
- let table = toml::toml! {
- "keyword" = {
- fg = "#ffffff",
- bg = "#000000",
- modifiers = ["bold"],
- }
- };
+ assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
+ }
- let mut style = Style::default();
- let palette = ThemePalette::default();
- if let Value::Table(entries) = table {
- for (_name, value) in entries {
- palette.parse_style(&mut style, value).unwrap();
+ #[test]
+ fn test_parse_style_table() {
+ let table = toml::toml! {
+ "keyword" = {
+ fg = "#ffffff",
+ bg = "#000000",
+ modifiers = ["bold"],
+ }
+ };
+
+ let mut style = Style::default();
+ let palette = ThemePalette::default();
+ if let Value::Table(entries) = table {
+ for (_name, value) in entries {
+ palette.parse_style(&mut style, value).unwrap();
+ }
}
- }
- assert_eq!(
- style,
- Style::default()
- .fg(Color::Rgb(255, 255, 255))
- .bg(Color::Rgb(0, 0, 0))
- .add_modifier(Modifier::BOLD)
- );
+ assert_eq!(
+ style,
+ Style::default()
+ .fg(Color::Rgb(255, 255, 255))
+ .bg(Color::Rgb(0, 0, 0))
+ .add_modifier(Modifier::BOLD)
+ );
+ }
}
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 9336742b..6bc9435c 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -78,6 +78,13 @@ pub struct View {
pub jumps: JumpList,
/// the last accessed file before the current one
pub last_accessed_doc: Option<DocumentId>,
+ /// the last modified files before the current one
+ /// ordered from most frequent to least frequent
+ // uses two docs because we want to be able to swap between the
+ // two last modified docs which we need to manually keep track of
+ pub last_modified_docs: [Option<DocumentId>; 2],
+ /// used to store previous selections of tree-sitter objecs
+ pub object_selections: Vec<Selection>,
}
impl View {
@@ -89,6 +96,8 @@ impl View {
area: Rect::default(), // will get calculated upon inserting into tree
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
last_accessed_doc: None,
+ last_modified_docs: [None, None],
+ object_selections: Vec::new(),
}
}
@@ -370,7 +379,7 @@ mod tests {
let text = rope.slice(..);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4),
+ view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
Some(0)
);
@@ -403,7 +412,7 @@ mod tests {
let text = rope.slice(..);
assert_eq!(
- view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET + 0, 4),
+ view.text_pos_at_screen_coords(&text, 40, 40 + OFFSET, 4),
Some(0)
);
diff --git a/languages.toml b/languages.toml
index 2b501651..b6ab1d0e 100644
--- a/languages.toml
+++ b/languages.toml
@@ -3,14 +3,11 @@ name = "rust"
scope = "source.rust"
injection-regex = "rust"
file-types = ["rs"]
-roots = []
+roots = ["Cargo.toml", "Cargo.lock"]
auto-format = true
comment-token = "//"
language-server = { command = "rust-analyzer" }
indent = { tab-width = 4, unit = " " }
-[language.config]
-cargo = { loadOutDirsFromCheck = true }
-procMacro = { enable = false }
[language.debugger]
name = "lldb-vscode"
@@ -74,6 +71,17 @@ language-server = { command = "elixir-ls" }
indent = { tab-width = 2, unit = " " }
[[language]]
+name = "fish"
+scope = "source.fish"
+injection-regex = "fish"
+file-types = ["fish"]
+shebangs = ["fish"]
+roots = []
+comment-token = "#"
+
+indent = { tab-width = 4, unit = " " }
+
+[[language]]
name = "mint"
scope = "source.mint"
injection-regex = "mint"
@@ -226,6 +234,7 @@ roots = []
comment-token = "//"
# TODO: highlights-jsx, highlights-params
+language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "javascript" }
indent = { tab-width = 2, unit = " " }
[language.debugger]
@@ -249,7 +258,7 @@ shebangs = []
roots = []
# TODO: highlights-jsx, highlights-params
-language-server = { command = "typescript-language-server", args = ["--stdio"] }
+language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescript"}
indent = { tab-width = 2, unit = " " }
[[language]]
@@ -260,14 +269,14 @@ file-types = ["tsx"]
roots = []
# TODO: highlights-jsx, highlights-params
-language-server = { command = "typescript-language-server", args = ["--stdio"] }
+language-server = { command = "typescript-language-server", args = ["--stdio"], language-id = "typescriptreact" }
indent = { tab-width = 2, unit = " " }
[[language]]
name = "css"
scope = "source.css"
injection-regex = "css"
-file-types = ["css"]
+file-types = ["css", "scss"]
roots = []
indent = { tab-width = 2, unit = " " }
@@ -322,7 +331,7 @@ indent = { tab-width = 2, unit = " " }
name = "bash"
scope = "source.bash"
injection-regex = "bash"
-file-types = ["sh", "bash"]
+file-types = ["sh", "bash", "zsh", ".bash_login", ".bash_logout", ".bash_profile", ".bashrc", ".profile", ".zshenv", ".zlogin", ".zlogout", ".zprofile", ".zshrc"]
shebangs = ["sh", "bash", "dash"]
roots = []
comment-token = "#"
@@ -341,6 +350,15 @@ roots = []
indent = { tab-width = 4, unit = " " }
[[language]]
+name = "twig"
+scope = "source.twig"
+injection-regex = "twig"
+file-types = ["twig"]
+roots = []
+
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
name = "latex"
scope = "source.tex"
injection-regex = "tex"
@@ -351,6 +369,17 @@ comment-token = "%"
indent = { tab-width = 4, unit = "\t" }
[[language]]
+name = "lean"
+scope = "source.lean"
+injection-regex = "lean"
+file-types = ["lean"]
+roots = [ "lakefile.lean" ]
+comment-token = "--"
+language-server = { command = "lean", args = [ "--server" ] }
+
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
name = "julia"
scope = "source.julia"
injection-regex = "julia"
@@ -367,7 +396,6 @@ language-server = { command = "julia", args = [
using Pkg;
import StaticLint;
env_path = dirname(Pkg.Types.Context().env.project_file);
-
server = LanguageServer.LanguageServerInstance(stdin, stdout, env_path, "");
server.runlinter = true;
run(server);
@@ -380,7 +408,7 @@ name = "java"
scope = "source.java"
injection-regex = "java"
file-types = ["java"]
-roots = []
+roots = ["pom.xml"]
indent = { tab-width = 4, unit = " " }
[[language]]
@@ -445,16 +473,17 @@ file-types = ["yml", "yaml"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
+injection-regex = "yml|yaml"
-# [[language]]
-# name = "haskell"
-# scope = "source.haskell"
-# injection-regex = "haskell"
-# file-types = ["hs"]
-# roots = []
-# comment-token = "--"
-#
-# indent = { tab-width = 2, unit = " " }
+[[language]]
+name = "haskell"
+scope = "source.haskell"
+injection-regex = "haskell"
+file-types = ["hs"]
+roots = []
+comment-token = "--"
+language-server = { command = "haskell-language-server-wrapper", args = ["--lsp"] }
+indent = { tab-width = 2, unit = " " }
[[language]]
name = "zig"
@@ -487,6 +516,7 @@ scope = "source.tsq"
file-types = ["scm"]
roots = []
comment-token = ";"
+injection-regex = "tsq"
indent = { tab-width = 2, unit = " " }
[[language]]
@@ -497,6 +527,14 @@ roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "cmake-language-server" }
+injection-regex = "cmake"
+
+[[language]]
+name = "make"
+scope = "source.make"
+file-types = ["Makefile", "makefile", "justfile", ".justfile"]
+roots =[]
+comment-token = "#"
[[language]]
name = "glsl"
@@ -505,6 +543,7 @@ file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
roots = []
comment-token = "//"
indent = { tab-width = 4, unit = " " }
+injection-regex = "glsl"
[[language]]
name = "perl"
@@ -525,6 +564,13 @@ comment-token = ";"
language-server = { command = "racket", args = ["-l", "racket-langserver"] }
[[language]]
+name = "comment"
+scope = "scope.comment"
+roots = []
+file-types = []
+injection-regex = "comment"
+
+[[language]]
name = "wgsl"
scope = "source.wgsl"
file-types = ["wgsl"]
@@ -539,3 +585,149 @@ roots = []
file-types = ["ll"]
comment-token = ";"
indent = { tab-width = 2, unit = " " }
+injection-regex = "llvm"
+
+[[language]]
+name = "llvm-mir"
+scope = "source.llvm_mir"
+roots = []
+file-types = []
+comment-token = ";"
+indent = { tab-width = 2, unit = " " }
+injection-regex = "mir"
+
+[[language]]
+name = "llvm-mir-yaml"
+tree-sitter-library = "yaml"
+scope = "source.yaml"
+roots = []
+file-types = ["mir"]
+comment-token = "#"
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "tablegen"
+scope = "source.tablegen"
+roots = []
+file-types = ["td"]
+comment-token = "//"
+indent = { tab-width = 2, unit = " " }
+injection-regex = "tablegen"
+
+[[language]]
+name = "markdown"
+scope = "source.md"
+injection-regex = "md|markdown"
+file-types = ["md"]
+roots = []
+
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "dart"
+scope = "source.dart"
+file-types = ["dart"]
+roots = ["pubspec.yaml"]
+auto-format = true
+comment-token = "//"
+language-server = { command = "dart", args = ["language-server", "--client-id=helix"] }
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "scala"
+scope = "source.scala"
+roots = ["build.sbt", "pom.xml"]
+file-types = ["scala", "sbt"]
+comment-token = "//"
+indent = { tab-width = 2, unit = " " }
+language-server = { command = "metals" }
+
+[[language]]
+name = "dockerfile"
+scope = "source.dockerfile"
+injection-regex = "docker|dockerfile"
+roots = ["Dockerfile"]
+file-types = ["Dockerfile", "dockerfile"]
+comment-token = "#"
+indent = { tab-width = 2, unit = " " }
+language-server = { command = "docker-langserver", args = ["--stdio"] }
+
+[[language]]
+name = "git-commit"
+scope = "git.commitmsg"
+roots = []
+file-types = ["COMMIT_EDITMSG"]
+comment-token = "#"
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "git-diff"
+scope = "source.diff"
+roots = []
+file-types = ["diff"]
+injection-regex = "diff"
+comment-token = "#"
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "git-rebase"
+scope = "source.gitrebase"
+roots = []
+file-types = ["git-rebase-todo"]
+injection-regex = "git-rebase"
+comment-token = "#"
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "regex"
+scope = "source.regex"
+injection-regex = "regex"
+file-types = ["regex"]
+roots = []
+
+[[language]]
+name = "git-config"
+scope = "source.gitconfig"
+roots = []
+# TODO: allow specifying file-types as a regex so we can read directory names (e.g. `.git/config`)
+file-types = [".gitmodules", ".gitconfig"]
+injection-regex = "git-config"
+comment-token = "#"
+indent = { tab-width = 4, unit = "\t" }
+
+[[language]]
+name = "graphql"
+scope = "source.graphql"
+injection-regex = "graphql"
+file-types = ["gql", "graphql"]
+roots = []
+indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "elm"
+scope = "source.elm"
+injection-regex = "elm"
+file-types = ["elm"]
+roots = ["elm.json"]
+auto-format = true
+comment-token = "--"
+language-server = { command = "elm-language-server" }
+indent = { tab-width = 4, unit = " " }
+
+[[language]]
+name = "iex"
+scope = "source.iex"
+injection-regex = "iex"
+file-types = ["iex"]
+roots = []
+
+[[language]]
+name = "rescript"
+scope = "source.rescript"
+injection-regex = "rescript"
+file-types = ["res"]
+roots = ["bsconfig.json"]
+auto-format = true
+comment-token = "//"
+language-server = { command = "rescript-language-server", args = ["--stdio"] }
+indent = { tab-width = 2, unit = " " }
diff --git a/runtime/queries/bash/injections.scm b/runtime/queries/bash/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/bash/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/c-sharp/injections.scm b/runtime/queries/c-sharp/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/c-sharp/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/c/indents.toml b/runtime/queries/c/indents.toml
new file mode 100644
index 00000000..f4076e17
--- /dev/null
+++ b/runtime/queries/c/indents.toml
@@ -0,0 +1,16 @@
+indent = [
+ "compound_statement",
+ "field_declaration_list",
+ "enumerator_list",
+ "parameter_list",
+ "init_declarator",
+ "case_statement",
+ "condition_clause",
+ "expression_statement",
+]
+
+outdent = [
+ "case",
+ "}",
+ "]",
+]
diff --git a/runtime/queries/c/injections.scm b/runtime/queries/c/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/c/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/c/textobjects.scm b/runtime/queries/c/textobjects.scm
new file mode 100644
index 00000000..b0f03668
--- /dev/null
+++ b/runtime/queries/c/textobjects.scm
@@ -0,0 +1,13 @@
+(function_definition
+ body: (_) @function.inside) @function.around
+
+(struct_specifier
+ body: (_) @class.inside) @class.around
+
+(enum_specifier
+ body: (_) @class.inside) @class.around
+
+(union_specifier
+ body: (_) @class.inside) @class.around
+
+(parameter_declaration) @parameter.inside
diff --git a/runtime/queries/cmake/indents.toml b/runtime/queries/cmake/indents.toml
new file mode 100644
index 00000000..8b886a4f
--- /dev/null
+++ b/runtime/queries/cmake/indents.toml
@@ -0,0 +1,12 @@
+indent = [
+ "if_condition",
+ "foreach_loop",
+ "while_loop",
+ "function_def",
+ "macro_def",
+ "normal_command",
+]
+
+outdent = [
+ ")"
+]
diff --git a/runtime/queries/cmake/injections.scm b/runtime/queries/cmake/injections.scm
new file mode 100644
index 00000000..6cb6c254
--- /dev/null
+++ b/runtime/queries/cmake/injections.scm
@@ -0,0 +1,4 @@
+((line_comment) @injection.content
+ (#set! injection.language "comment"))
+((bracket_comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/cmake/textobjects.scm b/runtime/queries/cmake/textobjects.scm
new file mode 100644
index 00000000..b0d1b108
--- /dev/null
+++ b/runtime/queries/cmake/textobjects.scm
@@ -0,0 +1,3 @@
+(macro_def) @function.around
+
+(argument) @parameter.inside
diff --git a/runtime/queries/comment/highlights.scm b/runtime/queries/comment/highlights.scm
new file mode 100644
index 00000000..88685d59
--- /dev/null
+++ b/runtime/queries/comment/highlights.scm
@@ -0,0 +1,30 @@
+[
+ "("
+ ")"
+] @punctuation.bracket
+
+":" @punctuation.delimiter
+
+((tag (name) @warning)
+ (#match? @warning "^(TODO|HACK|WARNING)$"))
+
+("text" @warning
+ (#match? @warning "^(TODO|HACK|WARNING)$"))
+
+((tag (name) @error)
+ (match? @error "^(FIXME|XXX|BUG)$"))
+
+("text" @error
+ (match? @error "^(FIXME|XXX|BUG)$"))
+
+(tag
+ (name) @ui.text
+ (user)? @constant)
+
+; Issue number (#123)
+("text" @constant.numeric
+ (#match? @constant.numeric "^#[0-9]+$"))
+
+; User mention (@user)
+("text" @tag
+ (#match? @tag "^[@][a-zA-Z0-9_-]+$"))
diff --git a/runtime/queries/cpp/indents.toml b/runtime/queries/cpp/indents.toml
new file mode 100644
index 00000000..0ca2ed8b
--- /dev/null
+++ b/runtime/queries/cpp/indents.toml
@@ -0,0 +1,17 @@
+indent = [
+ "compound_statement",
+ "field_declaration_list",
+ "enumerator_list",
+ "parameter_list",
+ "init_declarator",
+ "case_statement",
+ "condition_clause",
+ "expression_statement",
+]
+
+outdent = [
+ "case",
+ "access_specifier",
+ "}",
+ "]",
+]
diff --git a/runtime/queries/cpp/injections.scm b/runtime/queries/cpp/injections.scm
new file mode 100644
index 00000000..a5a5208c
--- /dev/null
+++ b/runtime/queries/cpp/injections.scm
@@ -0,0 +1 @@
+; inherits: c
diff --git a/runtime/queries/cpp/textobjects.scm b/runtime/queries/cpp/textobjects.scm
new file mode 100644
index 00000000..6e3de1a2
--- /dev/null
+++ b/runtime/queries/cpp/textobjects.scm
@@ -0,0 +1,7 @@
+; inherits: c
+
+(lambda_expression
+ body: (_) @function.inside) @function.around
+
+(class_specifier
+ body: (_) @class.inside) @class.around
diff --git a/runtime/queries/css/injections.scm b/runtime/queries/css/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/css/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/dart/highlights.scm b/runtime/queries/dart/highlights.scm
new file mode 100644
index 00000000..9f667d6b
--- /dev/null
+++ b/runtime/queries/dart/highlights.scm
@@ -0,0 +1,237 @@
+(dotted_identifier_list) @string
+
+; Methods
+; --------------------
+(super) @function.builtin
+
+(function_expression_body (identifier) @function.method)
+((identifier)(selector (argument_part)) @function.method)
+
+; Annotations
+; --------------------
+(annotation
+ name: (identifier) @attribute)
+(marker_annotation
+ name: (identifier) @attribute)
+
+; Types
+; --------------------
+(class_definition
+ name: (identifier) @type)
+
+(constructor_signature
+ name: (identifier) @function.method)
+
+(function_signature
+ name: (identifier) @function.method)
+
+(getter_signature
+ (identifier) @function.builtin)
+
+(setter_signature
+ name: (identifier) @function.builtin)
+
+(enum_declaration
+ name: (identifier) @type)
+
+(enum_constant
+ name: (identifier) @type.builtin)
+
+(void_type) @type.builtin
+
+((scoped_identifier
+ scope: (identifier) @type)
+ (#match? @type "^[a-zA-Z]"))
+
+((scoped_identifier
+ scope: (identifier) @type
+ name: (identifier) @type)
+ (#match? @type "^[a-zA-Z]"))
+
+; the DisabledDrawerButtons in : const DisabledDrawerButtons(history: true),
+(type_identifier) @type.builtin
+
+; Variables
+; --------------------
+; the "File" in var file = File();
+((identifier) @namespace
+ (#match? @namespace "^_?[A-Z].*[a-z]")) ; catch Classes or IClasses not CLASSES
+
+("Function" @type.builtin)
+(inferred_type) @type.builtin
+
+; properties
+(unconditional_assignable_selector
+ (identifier) @variable.other.member)
+
+(conditional_assignable_selector
+ (identifier) @variable.other.member)
+
+; assignments
+; --------------------
+; the "strings" in : strings = "some string"
+(assignment_expression
+ left: (assignable_expression) @variable)
+
+(this) @variable.builtin
+
+; Parameters
+; --------------------
+(formal_parameter
+ name: (identifier) @variable)
+
+(named_argument
+ (label (identifier) @variable))
+
+; Literals
+; --------------------
+[
+ (hex_integer_literal)
+ (decimal_integer_literal)
+ (decimal_floating_point_literal)
+ ;(octal_integer_literal)
+ ;(hex_floating_point_literal)
+] @constant.numeric.integer
+
+(symbol_literal) @string.special.symbol
+(string_literal) @string
+
+[
+ (const_builtin)
+ (final_builtin)
+] @variable.builtin
+
+[
+ (true)
+ (false)
+] @constant.builtin.boolean
+
+(null_literal) @constant.builtin
+
+(comment) @comment.line
+(documentation_comment) @comment.block.documentation
+
+; Tokens
+; --------------------
+(template_substitution
+ "$" @punctuation.special
+ "{" @punctuation.special
+ "}" @punctuation.special
+) @embedded
+
+(template_substitution
+ "$" @punctuation.special
+ (identifier_dollar_escaped) @variable
+) @embedded
+
+(escape_sequence) @constant.character.escape
+
+; Punctuation
+;---------------------
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ ";"
+ "."
+ ","
+ ":"
+] @punctuation.delimiter
+
+; Operators
+;---------------------
+[
+ "@"
+ "?"
+ "=>"
+ ".."
+ "=="
+ "&&"
+ "%"
+ "<"
+ ">"
+ "="
+ ">="
+ "<="
+ "||"
+ (multiplicative_operator)
+ (increment_operator)
+ (is_operator)
+ (prefix_operator)
+ (equality_operator)
+ (additive_operator)
+] @operator
+
+; Keywords
+; --------------------
+["import" "library" "export"] @keyword.control.import
+["do" "while" "continue" "for"] @keyword.control.repeat
+["return" "yield"] @keyword.control.return
+["as" "in" "is"] @keyword.operator
+
+[
+ "?."
+ "??"
+ "if"
+ "else"
+ "switch"
+ "default"
+ "late"
+] @keyword.control.conditional
+
+[
+ "try"
+ "throw"
+ "catch"
+ "finally"
+ (break_statement)
+] @keyword.control.exception
+
+; Reserved words (cannot be used as identifiers)
+[
+ (case_builtin)
+ "abstract"
+ "async"
+ "async*"
+ "await"
+ "class"
+ "covariant"
+ "deferred"
+ "dynamic"
+ "enum"
+ "extends"
+ "extension"
+ "external"
+ "factory"
+ "Function"
+ "get"
+ "implements"
+ "interface"
+ "mixin"
+ "new"
+ "on"
+ "operator"
+ "part"
+ "required"
+ "set"
+ "show"
+ "static"
+ "super"
+ "sync*"
+ "typedef"
+ "with"
+] @keyword
+
+; when used as an identifier:
+((identifier) @variable.builtin
+ (#match? @variable.builtin "^(abstract|as|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|set|static|typedef)$"))
+
+; Error
+(ERROR) @error
+
diff --git a/runtime/queries/dart/indents.toml b/runtime/queries/dart/indents.toml
new file mode 100644
index 00000000..5c11e05d
--- /dev/null
+++ b/runtime/queries/dart/indents.toml
@@ -0,0 +1,20 @@
+indent = [
+ "class_body",
+ "function_body",
+ "function_expression_body",
+ "declaration",
+ "initializers",
+ "switch_block",
+ "if_statement",
+ "formal_parameter_list",
+ "formal_parameter",
+ "list_literal",
+ "return_statement",
+ "arguments"
+]
+
+outdent = [
+ "}",
+ "]",
+ ")"
+]
diff --git a/runtime/queries/dart/injections.scm b/runtime/queries/dart/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/dart/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/dart/locals.scm b/runtime/queries/dart/locals.scm
new file mode 100644
index 00000000..629838e5
--- /dev/null
+++ b/runtime/queries/dart/locals.scm
@@ -0,0 +1,20 @@
+; Scopes
+;-------
+
+[
+ (block)
+ (try_statement)
+ (catch_clause)
+ (finally_clause)
+] @local.scope
+
+; Definitions
+;------------
+
+(class_definition
+ body: (_) @local.definition)
+
+; References
+;------------
+
+(identifier) @local.reference
diff --git a/runtime/queries/dockerfile/highlights.scm b/runtime/queries/dockerfile/highlights.scm
new file mode 100644
index 00000000..5a945fb9
--- /dev/null
+++ b/runtime/queries/dockerfile/highlights.scm
@@ -0,0 +1,51 @@
+[
+ "FROM"
+ "AS"
+ "RUN"
+ "CMD"
+ "LABEL"
+ "EXPOSE"
+ "ENV"
+ "ADD"
+ "COPY"
+ "ENTRYPOINT"
+ "VOLUME"
+ "USER"
+ "WORKDIR"
+ "ARG"
+ "ONBUILD"
+ "STOPSIGNAL"
+ "HEALTHCHECK"
+ "SHELL"
+ "MAINTAINER"
+ "CROSS_BUILD"
+] @keyword
+
+[
+ ":"
+ "@"
+] @operator
+
+(comment) @comment
+
+
+(image_spec
+ (image_tag
+ ":" @punctuation.special)
+ (image_digest
+ "@" @punctuation.special))
+
+(double_quoted_string) @string
+
+(expansion
+ [
+ "$"
+ "{"
+ "}"
+ ] @punctuation.special
+) @none
+
+((variable) @constant
+ (#match? @constant "^[A-Z][A-Z_0-9]*$"))
+
+
diff --git a/runtime/queries/dockerfile/injections.scm b/runtime/queries/dockerfile/injections.scm
new file mode 100644
index 00000000..20396f1a
--- /dev/null
+++ b/runtime/queries/dockerfile/injections.scm
@@ -0,0 +1,6 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
+([(shell_command) (shell_fragment)] @injection.content
+ (#set! injection.language "bash"))
+
diff --git a/runtime/queries/elixir/injections.scm b/runtime/queries/elixir/injections.scm
new file mode 100644
index 00000000..8370a0d8
--- /dev/null
+++ b/runtime/queries/elixir/injections.scm
@@ -0,0 +1,9 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
+((sigil
+ (sigil_name) @_sigil_name
+ (quoted_content) @injection.content)
+ (#match? @_sigil_name "^(r|R)$")
+ (#set! injection.language "regex")
+ (#set! injection.combined))
diff --git a/runtime/queries/elm/highlights.scm b/runtime/queries/elm/highlights.scm
new file mode 100644
index 00000000..3c8fd12d
--- /dev/null
+++ b/runtime/queries/elm/highlights.scm
@@ -0,0 +1,83 @@
+; Keywords
+[
+ "if"
+ "then"
+ "else"
+ "let"
+ "in"
+ ] @keyword.control
+(case) @keyword.control
+(of) @keyword.control
+
+(colon) @keyword.operator
+(backslash) @keyword
+(as) @keyword
+(port) @keyword
+(exposing) @keyword
+(alias) @keyword
+(infix) @keyword
+
+(arrow) @keyword.operator
+(dot) @keyword.operator
+
+(port) @keyword
+
+(type_annotation(lower_case_identifier) @function)
+(port_annotation(lower_case_identifier) @function)
+(file (value_declaration (function_declaration_left(lower_case_identifier) @function)))
+
+(field name: (lower_case_identifier) @attribute)
+(field_access_expr(lower_case_identifier) @attribute)
+
+(operator_identifier) @keyword.operator
+(eq) @keyword.operator.assignment
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+"|" @keyword
+"," @punctuation.delimiter
+
+[
+ "|>"
+] @keyword
+
+
+(import) @keyword.contol.import
+(module) @keyword.other
+
+(number_constant_expr) @constant.numeric
+
+(type) @type
+
+(type_declaration(upper_case_identifier) @type)
+(type_ref) @type
+(type_alias_declaration name: (upper_case_identifier) @type)
+
+(union_pattern constructor: (upper_case_qid (upper_case_identifier) @label (dot) (upper_case_identifier) @variable.other.member))
+(union_pattern constructor: (upper_case_qid (upper_case_identifier) @variable.other.member))
+
+(union_variant(upper_case_identifier) @variable.other.member)
+(value_expr name: (value_qid (upper_case_identifier) @label))
+(value_expr (upper_case_qid (upper_case_identifier) @label (dot) (upper_case_identifier) @variable.other.member))
+(value_expr(upper_case_qid(upper_case_identifier)) @variable.other.member)
+
+; comments
+(line_comment) @comment
+(block_comment) @comment
+
+; strings
+(string_escape) @constant.character.escape
+
+(open_quote) @string
+(close_quote) @string
+(regular_string_part) @string
+
+(open_char) @constant.character
+(close_char) @constant.character
diff --git a/runtime/queries/elm/injections.scm b/runtime/queries/elm/injections.scm
new file mode 100644
index 00000000..83f8245c
--- /dev/null
+++ b/runtime/queries/elm/injections.scm
@@ -0,0 +1,4 @@
+; Parse glsl where defined
+
+((glsl_content) @injection.content
+ (#set! injection.language "glsl"))
diff --git a/runtime/queries/elm/locals.scm b/runtime/queries/elm/locals.scm
new file mode 100644
index 00000000..ab103115
--- /dev/null
+++ b/runtime/queries/elm/locals.scm
@@ -0,0 +1,14 @@
+(value_declaration) @local.scope
+(type_alias_declaration) @local.scope
+(type_declaration) @local.scope
+(type_annotation) @local.scope
+(port_annotation) @local.scope
+(infix_declaration) @local.scope
+(let_in_expr) @local.scope
+
+(function_declaration_left (lower_pattern (lower_case_identifier)) @local.definition)
+(function_declaration_left (lower_case_identifier) @local.definition)
+
+(value_expr(value_qid(upper_case_identifier)) @local.reference)
+(value_expr(value_qid(lower_case_identifier)) @local.reference)
+(type_ref (upper_case_qid) @local.reference)
diff --git a/runtime/queries/elm/tags.scm b/runtime/queries/elm/tags.scm
new file mode 100644
index 00000000..03999fb1
--- /dev/null
+++ b/runtime/queries/elm/tags.scm
@@ -0,0 +1,19 @@
+(value_declaration (function_declaration_left (lower_case_identifier) @name)) @definition.function
+
+(function_call_expr (value_expr (value_qid) @name)) @reference.function
+(exposed_value (lower_case_identifier) @name) @reference.function
+(type_annotation ((lower_case_identifier) @name) (colon)) @reference.function
+
+(type_declaration ((upper_case_identifier) @name) ) @definition.type
+
+(type_ref (upper_case_qid (upper_case_identifier) @name)) @reference.type
+(exposed_type (upper_case_identifier) @name) @reference.type
+
+(type_declaration (union_variant (upper_case_identifier) @name)) @definition.union
+
+(value_expr (upper_case_qid (upper_case_identifier) @name)) @reference.union
+
+
+(module_declaration
+ (upper_case_qid (upper_case_identifier)) @name
+) @definition.module
diff --git a/runtime/queries/fish/highlights.scm b/runtime/queries/fish/highlights.scm
new file mode 100644
index 00000000..def53931
--- /dev/null
+++ b/runtime/queries/fish/highlights.scm
@@ -0,0 +1,156 @@
+;; Operators
+
+[
+ "&&"
+ "||"
+ "|"
+ "&"
+ "="
+ "!="
+ ".."
+ "!"
+ (direction)
+ (stream_redirect)
+ (test_option)
+] @operator
+
+[
+ "not"
+ "and"
+ "or"
+] @keyword.operator
+
+;; Conditionals
+
+(if_statement
+[
+ "if"
+ "end"
+] @keyword.control.conditional)
+
+(switch_statement
+[
+ "switch"
+ "end"
+] @keyword.control.conditional)
+
+(case_clause
+[
+ "case"
+] @keyword.control.conditional)
+
+(else_clause
+[
+ "else"
+] @keyword.control.conditional)
+
+(else_if_clause
+[
+ "else"
+ "if"
+] @keyword.control.conditional)
+
+;; Loops/Blocks
+
+(while_statement
+[
+ "while"
+ "end"
+] @keyword.control.repeat)
+
+(for_statement
+[
+ "for"
+ "end"
+] @keyword.control.repeat)
+
+(begin_statement
+[
+ "begin"
+ "end"
+] @keyword.control.repeat)
+
+;; Keywords
+
+[
+ "in"
+ (break)
+ (continue)
+] @keyword
+
+"return" @keyword.control.return
+
+;; Punctuation
+
+[
+ "["
+ "]"
+ "{"
+ "}"
+ "("
+ ")"
+] @punctuation.bracket
+
+"," @punctuation.delimiter
+
+;; Commands
+
+(command
+ argument: [
+ (word) @variable.parameter (#match? @variable.parameter "^-")
+ ]
+)
+
+; non-bultin command names
+(command name: (word) @function)
+
+; derived from builtin -n (fish 3.2.2)
+(command
+ name: [
+ (word) @function.builtin
+ (#match? @function.builtin "^(\.|:|_|alias|argparse|bg|bind|block|breakpoint|builtin|cd|command|commandline|complete|contains|count|disown|echo|emit|eval|exec|exit|fg|functions|history|isatty|jobs|math|printf|pwd|random|read|realpath|set|set_color|source|status|string|test|time|type|ulimit|wait)$")
+ ]
+)
+
+(test_command "test" @function.builtin)
+
+;; Functions
+
+(function_definition ["function" "end"] @keyword.function)
+
+(function_definition
+ name: [
+ (word) (concatenation)
+ ]
+@function)
+
+(function_definition
+ option: [
+ (word)
+ (concatenation (word))
+ ] @variable.parameter (#match? @variable.parameter "^-")
+)
+
+;; Strings
+
+[(double_quote_string) (single_quote_string)] @string
+(escape_sequence) @constant.character.escape
+
+;; Variables
+
+(variable_name) @variable
+(variable_expansion) @constant
+
+;; Nodes
+
+(integer) @constant.numeric.integer
+(float) @constant.numeric.float
+(comment) @comment
+(test_option) @string
+
+((word) @constant.builtin.boolean
+(#match? @constant.builtin.boolean "^(true|false)$"))
+
+;; Error
+
+(ERROR) @error
diff --git a/runtime/queries/fish/indents.toml b/runtime/queries/fish/indents.toml
new file mode 100644
index 00000000..6f1e563a
--- /dev/null
+++ b/runtime/queries/fish/indents.toml
@@ -0,0 +1,12 @@
+indent = [
+ "function_definition",
+ "while_statement",
+ "for_statement",
+ "if_statement",
+ "begin_statement",
+ "switch_statement",
+]
+
+outdent = [
+ "end"
+]
diff --git a/runtime/queries/fish/injections.scm b/runtime/queries/fish/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/fish/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/fish/textobjects.scm b/runtime/queries/fish/textobjects.scm
new file mode 100644
index 00000000..67fd6614
--- /dev/null
+++ b/runtime/queries/fish/textobjects.scm
@@ -0,0 +1 @@
+(function_definition) @function.around
diff --git a/runtime/queries/git-commit/highlights.scm b/runtime/queries/git-commit/highlights.scm
new file mode 100644
index 00000000..0b50d419
--- /dev/null
+++ b/runtime/queries/git-commit/highlights.scm
@@ -0,0 +1,14 @@
+(subject) @markup.heading
+(path) @string.special.path
+(branch) @string.special.symbol
+(commit) @constant
+(item) @markup.link.url
+(header) @tag
+
+(change kind: "new file" @diff.plus)
+(change kind: "deleted" @diff.minus)
+(change kind: "modified" @diff.delta)
+(change kind: "renamed" @diff.delta.moved)
+
+[":" "->"] @punctuation.delimeter
+(comment) @comment
diff --git a/runtime/queries/git-commit/injections.scm b/runtime/queries/git-commit/injections.scm
new file mode 100644
index 00000000..cf0657f7
--- /dev/null
+++ b/runtime/queries/git-commit/injections.scm
@@ -0,0 +1,8 @@
+((comment (scissors))
+ (message) @injection.content
+ (#set! injection.include-children)
+ (#set! injection.language "diff"))
+
+((rebase_command) @injection.content
+ (#set! injection.include-children)
+ (#set! injection.language "git-rebase"))
diff --git a/runtime/queries/git-config/highlights.scm b/runtime/queries/git-config/highlights.scm
new file mode 100644
index 00000000..84767edc
--- /dev/null
+++ b/runtime/queries/git-config/highlights.scm
@@ -0,0 +1,27 @@
+((section_name) @keyword.directive
+ (#eq? @keyword.directive "include"))
+
+((section_header
+ (section_name) @keyword.directive
+ (subsection_name))
+ (#eq? @keyword.directive "includeIf"))
+
+(section_name) @markup.heading
+(variable (name) @variable.other.member)
+[(true) (false)] @constant.builtin.boolean
+(integer) @constant.numeric.integer
+
+((string) @string.special.path
+ (#match? @string.special.path "^(~|./|/)"))
+
+[(string) (subsection_name)] @string
+
+[
+ "["
+ "]"
+ "\""
+] @punctuation.bracket
+
+"=" @punctuation.delimiter
+
+(comment) @comment
diff --git a/runtime/queries/git-diff/highlights.scm b/runtime/queries/git-diff/highlights.scm
new file mode 100644
index 00000000..1c1a8829
--- /dev/null
+++ b/runtime/queries/git-diff/highlights.scm
@@ -0,0 +1,6 @@
+[(addition) (new_file)] @diff.plus
+[(deletion) (old_file)] @diff.minus
+
+(commit) @constant
+(location) @attribute
+(command) @markup.bold
diff --git a/runtime/queries/git-rebase/highlights.scm b/runtime/queries/git-rebase/highlights.scm
new file mode 100644
index 00000000..4f007037
--- /dev/null
+++ b/runtime/queries/git-rebase/highlights.scm
@@ -0,0 +1,11 @@
+(operation operator: ["p" "pick" "r" "reword" "e" "edit" "s" "squash" "m" "merge" "d" "drop" "b" "break" "x" "exec"] @keyword)
+(operation operator: ["l" "label" "t" "reset"] @function)
+(operation operator: ["f" "fixup"] @function.special)
+
+(option) @operator
+(label) @string.special.symbol
+(commit) @constant
+"#" @punctuation.delimiter
+(comment) @comment
+
+(ERROR) @error
diff --git a/runtime/queries/git-rebase/injections.scm b/runtime/queries/git-rebase/injections.scm
new file mode 100644
index 00000000..070129b6
--- /dev/null
+++ b/runtime/queries/git-rebase/injections.scm
@@ -0,0 +1,4 @@
+((operation
+ operator: ["x" "exec"]
+ (command) @injection.content)
+ (#set! injection.language "bash"))
diff --git a/runtime/queries/glsl/injections.scm b/runtime/queries/glsl/injections.scm
index 7d3323b1..6330ea3e 100644
--- a/runtime/queries/glsl/injections.scm
+++ b/runtime/queries/glsl/injections.scm
@@ -1,3 +1,4 @@
-(preproc_arg) @glsl
+; inherits: c
-(comment) @comment
+((preproc_arg) @injection.content
+ (#set! injection.language "glsl"))
diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm
index 56384d4d..4ff8675b 100644
--- a/runtime/queries/go/highlights.scm
+++ b/runtime/queries/go/highlights.scm
@@ -69,6 +69,7 @@
"|"
"|="
"||"
+ "~"
] @operator
; Keywords
@@ -143,6 +144,9 @@
(false)
] @constant.builtin.boolean
-(nil) @constant.builtin
+[
+ (nil)
+ (iota)
+] @constant.builtin
(comment) @comment
diff --git a/runtime/queries/go/injections.scm b/runtime/queries/go/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/go/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/graphql/highlights.scm b/runtime/queries/graphql/highlights.scm
new file mode 100644
index 00000000..9fab4051
--- /dev/null
+++ b/runtime/queries/graphql/highlights.scm
@@ -0,0 +1,163 @@
+; Types
+;------
+
+(scalar_type_definition
+ (name) @type)
+
+(object_type_definition
+ (name) @type)
+
+(interface_type_definition
+ (name) @type)
+
+(union_type_definition
+ (name) @type)
+
+(enum_type_definition
+ (name) @type)
+
+(input_object_type_definition
+ (name) @type)
+
+(directive_definition
+ (name) @type)
+
+(directive_definition
+ "@" @type)
+
+(scalar_type_extension
+ (name) @type)
+
+(object_type_extension
+ (name) @type)
+
+(interface_type_extension
+ (name) @type)
+
+(union_type_extension
+ (name) @type)
+
+(enum_type_extension
+ (name) @type)
+
+(input_object_type_extension
+ (name) @type)
+
+(named_type
+ (name) @type)
+
+(directive) @type
+
+; Properties
+;-----------
+
+(field
+ (name) @variable.other.member)
+
+(field
+ (alias
+ (name) @variable.other.member))
+
+(field_definition
+ (name) @variable.other.member)
+
+(object_value
+ (object_field
+ (name) @variable.other.member))
+
+(enum_value
+ (name) @variable.other.member)
+
+; Variable Definitions and Arguments
+;-----------------------------------
+
+(operation_definition
+ (name) @variable)
+
+(fragment_name
+ (name) @variable)
+
+(input_fields_definition
+ (input_value_definition
+ (name) @variable.parameter))
+
+(argument
+ (name) @variable.parameter)
+
+(arguments_definition
+ (input_value_definition
+ (name) @variable.parameter))
+
+(variable_definition
+ (variable) @variable.parameter)
+
+(argument
+ (value
+ (variable) @variable))
+
+; Constants
+;----------
+
+(string_value) @string
+
+(int_value) @constants.numeric.integer
+
+(float_value) @constants.numeric.float
+
+(boolean_value) @constants.builtin.boolean
+
+; Literals
+;---------
+
+(description) @comment
+
+(comment) @comment
+
+(directive_location
+ (executable_directive_location) @type.builtin)
+
+(directive_location
+ (type_system_directive_location) @type.builtin)
+
+; Keywords
+;----------
+
+[
+ "query"
+ "mutation"
+ "subscription"
+ "fragment"
+ "scalar"
+ "type"
+ "interface"
+ "union"
+ "enum"
+ "input"
+ "extend"
+ "directive"
+ "schema"
+ "on"
+ "repeatable"
+ "implements"
+] @keyword
+
+; Punctuation
+;------------
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+"=" @operator
+
+"|" @punctuation.delimiter
+"&" @punctuation.delimiter
+":" @punctuation.delimiter
+
+"..." @punctuation.special
+"!" @punctuation.special
diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm
index 72187876..5009f3b5 100644
--- a/runtime/queries/haskell/highlights.scm
+++ b/runtime/queries/haskell/highlights.scm
@@ -1,45 +1,125 @@
-(variable) @variable
-(operator) @operator
-(exp_name (constructor) @constructor)
-(constructor_operator) @operator
-(module) @namespace
-(type) @type
-(type) @class
-(constructor) @constructor
-(pragma) @pragma
-(comment) @comment
-(signature name: (variable) @fun_type_name)
-(function name: (variable) @function)
-(constraint class: (class_name (type)) @class)
-(class (class_head class: (class_name (type)) @class))
-(instance (instance_head class: (class_name (type)) @class))
+;; ----------------------------------------------------------------------------
+;; Literals and comments
+
(integer) @constant.numeric.integer
+(exp_negation) @constant.numeric.integer
(exp_literal (float)) @constant.numeric.float
(char) @constant.character
-(con_unit) @literal
-(con_list) @literal
-(tycon_arrow) @operator
-(where) @keyword
-"module" @keyword
-"let" @keyword
-"in" @keyword
-"class" @keyword
-"instance" @keyword
-"data" @keyword
-"newtype" @keyword
-"family" @keyword
-"type" @keyword
-"import" @keyword
-"qualified" @keyword
-"as" @keyword
-"deriving" @keyword
-"via" @keyword
-"stock" @keyword
-"anyclass" @keyword
-"do" @keyword
-"mdo" @keyword
-"rec" @keyword
+(string) @string
+
+(con_unit) @constant.builtin ; unit, as in ()
+
+(comment) @comment
+
+
+;; ----------------------------------------------------------------------------
+;; Punctuation
+
[
"("
")"
+ "{"
+ "}"
+ "["
+ "]"
] @punctuation.bracket
+
+[
+ (comma)
+ ";"
+] @punctuation.delimiter
+
+
+;; ----------------------------------------------------------------------------
+;; Keywords, operators, includes
+
+(pragma) @constant.macro
+
+[
+ "if"
+ "then"
+ "else"
+ "case"
+ "of"
+] @keyword.control.conditional
+
+[
+ "import"
+ "qualified"
+ "module"
+] @keyword.control.import
+
+[
+ (operator)
+ (constructor_operator)
+ (type_operator)
+ (tycon_arrow)
+ (qualified_module) ; grabs the `.` (dot), ex: import System.IO
+ (all_names)
+ (wildcard)
+ "="
+ "|"
+ "::"
+ "=>"
+ "->"
+ "<-"
+ "\\"
+ "`"
+ "@"
+] @operator
+
+(qualified_module (module) @constructor)
+(qualified_type (module) @namespace)
+(qualified_variable (module) @namespace)
+(import (module) @namespace)
+
+[
+ (where)
+ "let"
+ "in"
+ "class"
+ "instance"
+ "data"
+ "newtype"
+ "family"
+ "type"
+ "as"
+ "hiding"
+ "deriving"
+ "via"
+ "stock"
+ "anyclass"
+ "do"
+ "mdo"
+ "rec"
+ "forall"
+ "∀"
+ "infix"
+ "infixl"
+ "infixr"
+] @keyword
+
+
+;; ----------------------------------------------------------------------------
+;; Functions and variables
+
+(signature name: (variable) @type)
+(function name: (variable) @function)
+
+(variable) @variable
+"_" @variable.builtin
+
+(exp_infix (variable) @operator) ; consider infix functions as operators
+
+("@" @namespace) ; "as" pattern operator, e.g. x@Constructor
+
+
+;; ----------------------------------------------------------------------------
+;; Types
+
+(type) @type
+
+(constructor) @constructor
+
+; True or False
+((constructor) @_bool (#match? @_bool "(True|False)")) @constant.builtin.boolean
diff --git a/runtime/queries/haskell/injections.scm b/runtime/queries/haskell/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/haskell/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/html/injections.scm b/runtime/queries/html/injections.scm
index 71e7c3ae..ef58f415 100644
--- a/runtime/queries/html/injections.scm
+++ b/runtime/queries/html/injections.scm
@@ -1,3 +1,6 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
((script_element
(raw_text) @injection.content)
(#set! injection.language "javascript"))
diff --git a/runtime/queries/iex/highlights.scm b/runtime/queries/iex/highlights.scm
new file mode 100644
index 00000000..2847fbff
--- /dev/null
+++ b/runtime/queries/iex/highlights.scm
@@ -0,0 +1 @@
+(prompt) @comment
diff --git a/runtime/queries/iex/injections.scm b/runtime/queries/iex/injections.scm
new file mode 100644
index 00000000..48863d9d
--- /dev/null
+++ b/runtime/queries/iex/injections.scm
@@ -0,0 +1,6 @@
+((evaluation_block (prompt_line (expression) @injection.content))
+ (#set! injection.language "elixir")
+ (#set! injection.combined))
+
+((result) @injection.content
+ (#set! injection.language "elixir"))
diff --git a/runtime/queries/java/injections.scm b/runtime/queries/java/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/java/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/javascript/injections.scm b/runtime/queries/javascript/injections.scm
index 5539241a..e8429111 100644
--- a/runtime/queries/javascript/injections.scm
+++ b/runtime/queries/javascript/injections.scm
@@ -9,6 +9,14 @@
]
arguments: (template_string) @injection.content)
+; Parse the contents of gql template literals
+
+((call_expression
+ function: (identifier) @_template_function_name
+ arguments: (template_string) @injection.content)
+ (#eq? @_template_function_name "gql")
+ (#set! injection.language "graphql"))
+
; Parse regex syntax within regex literals
((regex_pattern) @injection.content
diff --git a/runtime/queries/julia/injections.scm b/runtime/queries/julia/injections.scm
index be2412c0..1c1e804e 100644
--- a/runtime/queries/julia/injections.scm
+++ b/runtime/queries/julia/injections.scm
@@ -1,5 +1,5 @@
-; TODO: re-add when markdown is added.
-; ((triple_string) @markdown
-; (#offset! @markdown 0 3 0 -3))
+((triple_string) @injection.content
+ (#set! injection.language "markdown"))
-(comment) @comment
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/julia/locals.scm b/runtime/queries/julia/locals.scm
index f8b34f71..d5ac794e 100644
--- a/runtime/queries/julia/locals.scm
+++ b/runtime/queries/julia/locals.scm
@@ -2,24 +2,24 @@
(import_statement
(identifier) @definition.import)
(variable_declaration
- (identifier) @definition.var)
+ (identifier) @local.definition)
(variable_declaration
(tuple_expression
- (identifier) @definition.var))
+ (identifier) @local.definition))
(for_binding
- (identifier) @definition.var)
+ (identifier) @local.definition)
(for_binding
(tuple_expression
- (identifier) @definition.var))
+ (identifier) @local.definition))
(assignment_expression
(tuple_expression
- (identifier) @definition.var))
+ (identifier) @local.definition))
(assignment_expression
(bare_tuple_expression
- (identifier) @definition.var))
+ (identifier) @local.definition))
(assignment_expression
- (identifier) @definition.var)
+ (identifier) @local.definition)
(type_parameter_list
(identifier) @definition.type)
@@ -43,11 +43,11 @@
(identifier) @definition.parameter)
(function_definition
- name: (identifier) @definition.function) @scope
+ name: (identifier) @definition.function) @local.scope
(macro_definition
- name: (identifier) @definition.macro) @scope
+ name: (identifier) @definition.macro) @local.scope
-(identifier) @reference
+(identifier) @local.reference
[
(try_statement)
@@ -56,4 +56,4 @@
(let_statement)
(compound_expression)
(for_statement)
-] @scope
+] @local.scope
diff --git a/runtime/queries/latex/highlights.scm b/runtime/queries/latex/highlights.scm
index f045c82d..0a030b31 100644
--- a/runtime/queries/latex/highlights.scm
+++ b/runtime/queries/latex/highlights.scm
@@ -278,7 +278,7 @@
"\\includeinkscape"
"\\usepgflibrary"
"\\usetikzlibrary"
-] @include
+] @keyword.control.import
[
"\\part"
@@ -318,60 +318,60 @@
["[" "]" "{" "}"] @punctuation.bracket ;"(" ")" is has no special meaning in LaTeX
(chapter
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(part
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(section
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(subsection
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(subsubsection
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(paragraph
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
(subparagraph
- text: (brace_group) @text.title)
+ text: (brace_group) @markup.heading)
((environment
(begin
name: (word) @_frame)
(brace_group
- child: (text) @text.title))
+ child: (text) @markup.heading))
(#eq? @_frame "frame"))
((generic_command
name:(generic_command_name) @_name
arg: (brace_group
- (text) @text.title))
+ (text) @markup.heading))
(#eq? @_name "\\frametitle"))
;; Formatting
((generic_command
name:(generic_command_name) @_name
- arg: (_) @text.emphasis)
+ arg: (_) @markup.italic)
(#eq? @_name "\\emph"))
((generic_command
name:(generic_command_name) @_name
- arg: (_) @text.emphasis)
+ arg: (_) @markup.italic)
(#match? @_name "^(\\\\textit|\\\\mathit)$"))
((generic_command
name:(generic_command_name) @_name
- arg: (_) @text.strong)
+ arg: (_) @markup.bold)
(#match? @_name "^(\\\\textbf|\\\\mathbf)$"))
((generic_command
name:(generic_command_name) @_name
.
- arg: (_) @text.uri)
+ arg: (_) @markup.link.url)
(#match? @_name "^(\\\\url|\\\\href)$"))
(ERROR) @error
diff --git a/runtime/queries/latex/injections.scm b/runtime/queries/latex/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/latex/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/lean/folds.scm b/runtime/queries/lean/folds.scm
new file mode 100644
index 00000000..2c2bbb33
--- /dev/null
+++ b/runtime/queries/lean/folds.scm
@@ -0,0 +1,15 @@
+[
+ (namespace)
+ (section)
+
+ (instance)
+ (def)
+ (theorem)
+ (example)
+
+ (product)
+ (array)
+ (list)
+
+ (string)
+] @fold
diff --git a/runtime/queries/lean/highlights.scm b/runtime/queries/lean/highlights.scm
new file mode 100644
index 00000000..a64feb1d
--- /dev/null
+++ b/runtime/queries/lean/highlights.scm
@@ -0,0 +1,217 @@
+(open
+ namespace: (identifier) @namespace)
+(namespace
+ name: (identifier) @namespace)
+(section
+ name: (identifier) @namespace)
+
+;; Identifier naming conventions
+((identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(arrow) @type
+(product) @type
+
+;; Declarations
+
+[
+ "abbrev"
+ "def"
+ "theorem"
+ "constant"
+ "instance"
+ "axiom"
+ "example"
+ "inductive"
+ "structure"
+ "class"
+
+ "deriving"
+
+ "section"
+ "namespace"
+] @keyword
+
+(attributes
+ (identifier) @function)
+
+(abbrev
+ name: (identifier) @type)
+(def
+ name: (identifier) @function)
+(theorem
+ name: (identifier) @function)
+(constant
+ name: (identifier) @type)
+(instance
+ name: (identifier) @function)
+(instance
+ type: (identifier) @type)
+(axiom
+ name: (identifier) @function)
+(structure
+ name: (identifier) @type)
+(structure
+ extends: (identifier) @type)
+
+(where_decl
+ type: (identifier) @type)
+
+(proj
+ name: (identifier) @field)
+
+(binders
+ type: (identifier) @type)
+
+["if" "then" "else"] @keyword.control.conditional
+
+["for" "in" "do"] @keyword.control.repeat
+
+(import) @include
+
+; Tokens
+
+[
+ "!"
+ "$"
+ "%"
+ "&&"
+ "*"
+ "*>"
+ "+"
+ "++"
+ "-"
+ "/"
+ "::"
+ ":="
+ "<"
+ "<$>"
+ "<*"
+ "<*>"
+ "<="
+ "<|"
+ "<|>"
+ "="
+ "=="
+ "=>"
+ ">"
+ ">"
+ ">="
+ ">>"
+ ">>="
+ "@"
+ "^"
+ "|>"
+ "|>."
+ "||"
+ "←"
+ "→"
+ "↔"
+ "∘"
+ "∧"
+ "∨"
+ "≠"
+ "≤"
+ "≥"
+] @operator
+
+[
+ "@&"
+] @operator
+
+[
+ "attribute"
+ "by"
+ "end"
+ "export"
+ "extends"
+ "fun"
+ "let"
+ "have"
+ "match"
+ "open"
+ "return"
+ "universe"
+ "variable"
+ "where"
+ "with"
+ "λ"
+ (hash_command)
+ (prelude)
+ (sorry)
+] @keyword
+
+[
+ "prefix"
+ "infix"
+ "infixl"
+ "infixr"
+ "postfix"
+ "notation"
+ "macro_rules"
+ "syntax"
+ "elab"
+ "builtin_initialize"
+] @keyword
+
+[
+ "noncomputable"
+ "partial"
+ "private"
+ "protected"
+ "unsafe"
+] @keyword
+
+[
+ "apply"
+ "exact"
+ "rewrite"
+ "rw"
+ "simp"
+ (trivial)
+] @keyword
+
+[
+ "catch"
+ "finally"
+ "try"
+] @exception
+
+((apply
+ name: (identifier) @exception)
+ (#match? @exception "throw"))
+
+[
+ "unless"
+ "mut"
+] @keyword
+
+[(true) (false)] @boolean
+
+(number) @constant.numeric.integer
+(float) @constant.numeric.float
+
+(comment) @comment
+(char) @character
+(string) @string
+(interpolated_string) @string
+; (escape_sequence) @string.escape
+
+; Reset highlighing in string interpolation
+(interpolation) @none
+
+(interpolation
+ "{" @punctuation.special
+ "}" @punctuation.special)
+
+["(" ")" "[" "]" "{" "}" "⟨" "⟩"] @punctuation.bracket
+
+["|" "," "." ":" ";"] @punctuation.delimiter
+
+(sorry) @error
+
+;; Error
+(ERROR) @error
+
+; Variables
+(identifier) @variable
diff --git a/runtime/queries/lean/injections.scm b/runtime/queries/lean/injections.scm
new file mode 100644
index 00000000..030714f1
--- /dev/null
+++ b/runtime/queries/lean/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "markdown"))
diff --git a/runtime/queries/lean/locals.scm b/runtime/queries/lean/locals.scm
new file mode 100644
index 00000000..dd6c2036
--- /dev/null
+++ b/runtime/queries/lean/locals.scm
@@ -0,0 +1,5 @@
+[
+ (module)
+ (namespace)
+ (section)
+] @local.scope
diff --git a/runtime/queries/ledger/highlights.scm b/runtime/queries/ledger/highlights.scm
index bdf5f2db..02a9ea9a 100644
--- a/runtime/queries/ledger/highlights.scm
+++ b/runtime/queries/ledger/highlights.scm
@@ -12,7 +12,7 @@
((account) @variable.other.member)
((commodity) @text.literal)
-"include" @include
+"include" @keyword.local.import
[
"account"
diff --git a/runtime/queries/ledger/injections.scm b/runtime/queries/ledger/injections.scm
index 2d948141..c1714786 100644
--- a/runtime/queries/ledger/injections.scm
+++ b/runtime/queries/ledger/injections.scm
@@ -1,2 +1,2 @@
-(comment) @comment
-(note) @comment
+([(comment) (note)] @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/llvm-mir-yaml/highlights.scm b/runtime/queries/llvm-mir-yaml/highlights.scm
new file mode 100644
index 00000000..4ba254e8
--- /dev/null
+++ b/runtime/queries/llvm-mir-yaml/highlights.scm
@@ -0,0 +1 @@
+; inherits: yaml
diff --git a/runtime/queries/llvm-mir-yaml/indents.toml b/runtime/queries/llvm-mir-yaml/indents.toml
new file mode 100644
index 00000000..ddc3578b
--- /dev/null
+++ b/runtime/queries/llvm-mir-yaml/indents.toml
@@ -0,0 +1,3 @@
+indent = [
+ "block_mapping_pair",
+]
diff --git a/runtime/queries/llvm-mir-yaml/injections.scm b/runtime/queries/llvm-mir-yaml/injections.scm
new file mode 100644
index 00000000..b3243022
--- /dev/null
+++ b/runtime/queries/llvm-mir-yaml/injections.scm
@@ -0,0 +1,9 @@
+; inherits: yaml
+
+((document (block_node (block_scalar) @injection.content))
+ (#set! injection.language "llvm"))
+
+((document (block_node (block_mapping (block_mapping_pair
+ key: (flow_node (plain_scalar (string_scalar))) ; "body"
+ value: (block_node (block_scalar) @injection.content)))))
+ (#set! injection.language "mir"))
diff --git a/runtime/queries/llvm-mir/highlights.scm b/runtime/queries/llvm-mir/highlights.scm
new file mode 100644
index 00000000..79234612
--- /dev/null
+++ b/runtime/queries/llvm-mir/highlights.scm
@@ -0,0 +1,136 @@
+[
+ (label)
+ (bb_ref)
+] @label
+
+[
+ (comment)
+ (multiline_comment)
+] @comment
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+ "<"
+ ">"
+] @punctuation.bracket
+
+[
+ ","
+ ":"
+ "|"
+ "*"
+] @punctuation.delimiter
+
+[
+ "="
+ "x"
+] @operator
+
+[
+ "true"
+ "false"
+] @constant.builtin.boolean
+
+[
+ "null"
+ "_"
+ "unknown-address"
+] @constant.builtin
+
+[
+ (stack_object)
+ (constant_pool_index)
+ (jump_table_index)
+ (var)
+ (physical_register)
+ (ir_block)
+ (external_symbol)
+ (global_var)
+ (ir_local_var)
+ (metadata_ref)
+ (mnemonic)
+] @variable
+
+(low_level_type) @type
+
+[
+ (immediate_type)
+ (primitive_type)
+] @type.builtin
+
+(number) @constant.numeric.integer
+(float) @constant.numeric.float
+(string) @string
+
+(instruction name: _ @keyword.operator)
+
+[
+ "successors"
+ "liveins"
+ "pre-instr-symbol"
+ "post-instr-symbol"
+ "heap-alloc-marker"
+ "debug-instr-number"
+ "debug-location"
+ "mcsymbol"
+ "tied-def"
+ "target-flags"
+ "CustomRegMask"
+ "same_value"
+ "def_cfa_register"
+ "restore"
+ "undefined"
+ "offset"
+ "rel_offset"
+ "def_cfa"
+ "llvm_def_aspace_cfa"
+ "register"
+ "escape"
+ "remember_state"
+ "restore_state"
+ "window_save"
+ "negate_ra_sign_state"
+ "intpred"
+ "floatpred"
+ "shufflemask"
+ "liveout"
+ "target-index"
+ "blockaddress"
+ "intrinsic"
+ "load"
+ "store"
+ "unknown-size"
+ "on"
+ "from"
+ "into"
+ "align"
+ "basealign"
+ "addrspace"
+ "call-entry"
+ "custom"
+ "constant-pool"
+ "stack"
+ "got"
+ "jump-table"
+ "syncscope"
+ "address-taken"
+ "landing-pad"
+ "inlineasm-br-indirect-target"
+ "ehfunclet-entry"
+ "bbsections"
+
+ (intpred)
+ (floatpred)
+ (memory_operand_flag)
+ (atomic_ordering)
+ (register_flag)
+ (instruction_flag)
+ (float_keyword)
+] @keyword
+
+(ERROR) @error
diff --git a/runtime/queries/llvm-mir/indents.toml b/runtime/queries/llvm-mir/indents.toml
new file mode 100644
index 00000000..6a70e5ad
--- /dev/null
+++ b/runtime/queries/llvm-mir/indents.toml
@@ -0,0 +1,7 @@
+indent = [
+ "basic_block",
+]
+
+outdent = [
+ "label",
+]
diff --git a/runtime/queries/llvm-mir/injections.scm b/runtime/queries/llvm-mir/injections.scm
new file mode 100644
index 00000000..0b476f86
--- /dev/null
+++ b/runtime/queries/llvm-mir/injections.scm
@@ -0,0 +1,2 @@
+([ (comment) (multiline_comment)] @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/llvm-mir/textobjects.scm b/runtime/queries/llvm-mir/textobjects.scm
new file mode 100644
index 00000000..73f6f772
--- /dev/null
+++ b/runtime/queries/llvm-mir/textobjects.scm
@@ -0,0 +1,3 @@
+(basic_block) @function.around
+
+(argument) @parameter.inside
diff --git a/runtime/queries/llvm/highlights.scm b/runtime/queries/llvm/highlights.scm
index 73afe85e..cb705197 100644
--- a/runtime/queries/llvm/highlights.scm
+++ b/runtime/queries/llvm/highlights.scm
@@ -1,14 +1,158 @@
(type) @type
-(statement) @keyword.operator
+(type_keyword) @type.builtin
+
+(type [
+ (local_var)
+ (global_var)
+ ] @type)
+
+(argument) @variable.parameter
+
+(_ inst_name: _ @keyword.operator)
+
+[
+ "catch"
+ "filter"
+] @keyword.operator
+
+[
+ "to"
+ "nuw"
+ "nsw"
+ "exact"
+ "unwind"
+ "from"
+ "cleanup"
+ "swifterror"
+ "volatile"
+ "inbounds"
+ "inrange"
+ (icmp_cond)
+ (fcmp_cond)
+ (fast_math)
+] @keyword.control
+
+(_ callee: _ @function)
+(function_header name: _ @function)
+
+[
+ "declare"
+ "define"
+ (calling_conv)
+] @keyword.function
+
+[
+ "target"
+ "triple"
+ "datalayout"
+ "source_filename"
+ "addrspace"
+ "blockaddress"
+ "align"
+ "syncscope"
+ "within"
+ "uselistorder"
+ "uselistorder_bb"
+ "module"
+ "asm"
+ "sideeffect"
+ "alignstack"
+ "inteldialect"
+ "unwind"
+ "type"
+ "global"
+ "constant"
+ "externally_initialized"
+ "alias"
+ "ifunc"
+ "section"
+ "comdat"
+ "thread_local"
+ "localdynamic"
+ "initialexec"
+ "localexec"
+ "any"
+ "exactmatch"
+ "largest"
+ "nodeduplicate"
+ "samesize"
+ "distinct"
+ "attributes"
+ "vscale"
+ "no_cfi"
+ (linkage_aux)
+ (dso_local)
+ (visibility)
+ (dll_storage_class)
+ (unnamed_addr)
+ (attribute_name)
+] @keyword
+
+
+(function_header [
+ (linkage)
+ (calling_conv)
+ (unnamed_addr)
+ ] @keyword.function)
+
+[
+ (string)
+ (cstring)
+] @string
+
(number) @constant.numeric.integer
(comment) @comment
-(string) @string
(label) @label
-(keyword) @keyword
-"ret" @keyword.control.return
-(boolean) @constant.builtin.boolean
+(_ inst_name: "ret" @keyword.control.return)
(float) @constant.numeric.float
-(constant) @constant
-(identifier) @variable
-(symbol) @punctuation.delimiter
-(bracket) @punctuation.bracket
+
+[
+ (local_var)
+ (global_var)
+] @variable
+
+[
+ (struct_value)
+ (array_value)
+ (vector_value)
+] @constructor
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+ "<"
+ ">"
+ "<{"
+ "}>"
+] @punctuation.bracket
+
+[
+ ","
+ ":"
+] @punctuation.delimiter
+
+[
+ "="
+ "|"
+ "x"
+ "..."
+] @operator
+
+[
+ "true"
+ "false"
+] @constant.builtin.boolean
+
+[
+ "undef"
+ "poison"
+ "null"
+ "none"
+ "zeroinitializer"
+] @constant.builtin
+
+(ERROR) @error
diff --git a/runtime/queries/llvm/indents.toml b/runtime/queries/llvm/indents.toml
new file mode 100644
index 00000000..8cd603c8
--- /dev/null
+++ b/runtime/queries/llvm/indents.toml
@@ -0,0 +1,8 @@
+indent = [
+ "function_body",
+ "instruction",
+]
+
+outdent = [
+ "}",
+]
diff --git a/runtime/queries/llvm/injections.scm b/runtime/queries/llvm/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/llvm/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/llvm/locals.scm b/runtime/queries/llvm/locals.scm
new file mode 100644
index 00000000..1946c287
--- /dev/null
+++ b/runtime/queries/llvm/locals.scm
@@ -0,0 +1,14 @@
+; Scopes
+
+(function_body) @local.scope
+
+; Definitions
+
+(argument
+ (value (var (local_var) @local.definition)))
+
+(instruction
+ (local_var) @local.definition)
+
+; References
+(local_var) @local.reference
diff --git a/runtime/queries/llvm/textobjects.scm b/runtime/queries/llvm/textobjects.scm
new file mode 100644
index 00000000..3738a3bb
--- /dev/null
+++ b/runtime/queries/llvm/textobjects.scm
@@ -0,0 +1,16 @@
+(define
+ body: (_) @function.inside) @function.around
+
+(struct_type
+ (struct_body) @class.inside) @class.around
+
+(packed_struct_type
+ (struct_body) @class.inside) @class.around
+
+(array_type
+ (array_vector_body) @class.inside) @class.around
+
+(vector_type
+ (array_vector_body) @class.inside) @class.around
+
+(argument) @parameter.inside
diff --git a/runtime/queries/lua/injections.scm b/runtime/queries/lua/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/lua/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/make/highlights.scm b/runtime/queries/make/highlights.scm
new file mode 100644
index 00000000..50380baf
--- /dev/null
+++ b/runtime/queries/make/highlights.scm
@@ -0,0 +1,170 @@
+[
+ "("
+ ")"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ ":"
+ "&:"
+ "::"
+ "|"
+ ";"
+ "\""
+ "'"
+ ","
+] @punctuation.delimiter
+
+[
+ "$"
+ "$$"
+] @punctuation.special
+
+(automatic_variable
+ [ "@" "%" "<" "?" "^" "+" "/" "*" "D" "F"] @punctuation.special)
+
+(automatic_variable
+ "/" @error . ["D" "F"])
+
+[
+ "="
+ ":="
+ "::="
+ "?="
+ "+="
+ "!="
+ "@"
+ "-"
+ "+"
+] @operator
+
+[
+ (text)
+ (string)
+ (raw_text)
+] @string
+
+(variable_assignment (word) @string)
+
+[
+ "ifeq"
+ "ifneq"
+ "ifdef"
+ "ifndef"
+ "else"
+ "endif"
+ "if"
+ "or" ; boolean functions are conditional in make grammar
+ "and"
+] @keyword.control.conditional
+
+"foreach" @keyword.control.repeat
+
+[
+ "define"
+ "endef"
+ "vpath"
+ "undefine"
+ "export"
+ "unexport"
+ "override"
+ "private"
+; "load"
+] @keyword
+
+[
+ "include"
+ "sinclude"
+ "-include"
+] @keyword.control.import
+
+[
+ "subst"
+ "patsubst"
+ "strip"
+ "findstring"
+ "filter"
+ "filter-out"
+ "sort"
+ "word"
+ "words"
+ "wordlist"
+ "firstword"
+ "lastword"
+ "dir"
+ "notdir"
+ "suffix"
+ "basename"
+ "addsuffix"
+ "addprefix"
+ "join"
+ "wildcard"
+ "realpath"
+ "abspath"
+ "call"
+ "eval"
+ "file"
+ "value"
+ "shell"
+] @keyword.function
+
+[
+ "error"
+ "warning"
+ "info"
+] @keyword.control.exception
+
+;; Variable
+(variable_assignment
+ name: (word) @variable)
+
+(variable_reference
+ (word) @variable)
+
+(comment) @comment
+
+((word) @clean @string.regexp
+ (#match? @clean "[%\*\?]"))
+
+(function_call
+ function: "error"
+ (arguments (text) @error))
+
+(function_call
+ function: "warning"
+ (arguments (text) @warning))
+
+(function_call
+ function: "info"
+ (arguments (text) @info))
+
+;; Install Command Categories
+;; Others special variables
+;; Variables Used by Implicit Rules
+[
+ "VPATH"
+ ".RECIPEPREFIX"
+] @constant.builtin
+
+(variable_assignment
+ name: (word) @clean @constant.builtin
+ (#match? @clean "^(AR|AS|CC|CXX|CPP|FC|M2C|PC|CO|GET|LEX|YACC|LINT|MAKEINFO|TEX|TEXI2DVI|WEAVE|CWEAVE|TANGLE|CTANGLE|RM|ARFLAGS|ASFLAGS|CFLAGS|CXXFLAGS|COFLAGS|CPPFLAGS|FFLAGS|GFLAGS|LDFLAGS|LDLIBS|LFLAGS|YFLAGS|PFLAGS|RFLAGS|LINTFLAGS|PRE_INSTALL|POST_INSTALL|NORMAL_INSTALL|PRE_UNINSTALL|POST_UNINSTALL|NORMAL_UNINSTALL|MAKEFILE_LIST|MAKE_RESTARTS|MAKE_TERMOUT|MAKE_TERMERR|\.DEFAULT_GOAL|\.RECIPEPREFIX|\.EXTRA_PREREQS)$"))
+
+(variable_reference
+ (word) @clean @constant.builtin
+ (#match? @clean "^(AR|AS|CC|CXX|CPP|FC|M2C|PC|CO|GET|LEX|YACC|LINT|MAKEINFO|TEX|TEXI2DVI|WEAVE|CWEAVE|TANGLE|CTANGLE|RM|ARFLAGS|ASFLAGS|CFLAGS|CXXFLAGS|COFLAGS|CPPFLAGS|FFLAGS|GFLAGS|LDFLAGS|LDLIBS|LFLAGS|YFLAGS|PFLAGS|RFLAGS|LINTFLAGS|PRE_INSTALL|POST_INSTALL|NORMAL_INSTALL|PRE_UNINSTALL|POST_UNINSTALL|NORMAL_UNINSTALL|MAKEFILE_LIST|MAKE_RESTARTS|MAKE_TERMOUT|MAKE_TERMERR|\.DEFAULT_GOAL|\.RECIPEPREFIX|\.EXTRA_PREREQS\.VARIABLES|\.FEATURES|\.INCLUDE_DIRS|\.LOADED)$"))
+
+;; Standart targets
+(targets
+ (word) @constant.macro
+ (#match? @constant.macro "^(all|install|install-html|install-dvi|install-pdf|install-ps|uninstall|install-strip|clean|distclean|mostlyclean|maintainer-clean|TAGS|info|dvi|html|pdf|ps|dist|check|installcheck|installdirs)$"))
+
+(targets
+ (word) @constant.macro
+ (#match? @constant.macro "^(all|install|install-html|install-dvi|install-pdf|install-ps|uninstall|install-strip|clean|distclean|mostlyclean|maintainer-clean|TAGS|info|dvi|html|pdf|ps|dist|check|installcheck|installdirs)$"))
+
+;; Builtin targets
+(targets
+ (word) @constant.macro
+ (#match? @constant.macro "^\.(PHONY|SUFFIXES|DEFAULT|PRECIOUS|INTERMEDIATE|SECONDARY|SECONDEXPANSION|DELETE_ON_ERROR|IGNORE|LOW_RESOLUTION_TIME|SILENT|EXPORT_ALL_VARIABLES|NOTPARALLEL|ONESHELL|POSIX)$"))
diff --git a/runtime/queries/make/injections.scm b/runtime/queries/make/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/make/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm
new file mode 100644
index 00000000..f12254e9
--- /dev/null
+++ b/runtime/queries/markdown/highlights.scm
@@ -0,0 +1,41 @@
+[
+ (atx_heading)
+ (setext_heading)
+] @markup.heading
+
+(code_fence_content) @none
+
+[
+ (indented_code_block)
+ (fenced_code_block)
+] @markup.raw.block
+
+(block_quote) @markup.quote
+
+(code_span) @markup.raw.inline
+
+(emphasis) @markup.italic
+
+(strong_emphasis) @markup.bold
+
+(link_destination) @markup.link.url
+(link_label) @markup.link.label
+
+[
+ (link_text)
+ (image_description)
+] @markup.link.text
+
+[
+ (list_marker_plus)
+ (list_marker_minus)
+ (list_marker_star)
+ (list_marker_dot)
+ (list_marker_parenthesis)
+] @punctuation.special
+
+[
+ (backslash_escape)
+ (hard_line_break)
+] @string.character.escape
+
diff --git a/runtime/queries/markdown/injections.scm b/runtime/queries/markdown/injections.scm
new file mode 100644
index 00000000..10dcab0b
--- /dev/null
+++ b/runtime/queries/markdown/injections.scm
@@ -0,0 +1,9 @@
+(fenced_code_block
+ (info_string) @injection.language
+ (code_fence_content) @injection.content
+ (#set! injection.include-children))
+
+((html_block) @injection.content
+ (#set! injection.language "html"))
+((html_tag) @injection.content
+ (#set! injection.language "html"))
diff --git a/runtime/queries/nix/highlights.scm b/runtime/queries/nix/highlights.scm
index 66719e87..f6682065 100644
--- a/runtime/queries/nix/highlights.scm
+++ b/runtime/queries/nix/highlights.scm
@@ -13,7 +13,7 @@
] @keyword
((identifier) @variable.builtin
- (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins|false|null|true)$")
+ (#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins)$")
(#is-not? local))
((identifier) @function.builtin
@@ -33,6 +33,11 @@
(uri) @string.special.uri
+; boolean
+((identifier) @constant.builtin.boolean (#match? @constant.builtin.boolean "^(true|false)$")) @constant.builtin.boolean
+; null
+((identifier) @constant.builtin (#eq? @constant.builtin "null")) @constant.builtin
+
(integer) @constant.numeric.integer
(float) @constant.numeric.float
diff --git a/runtime/queries/ocaml-interface/injections.scm b/runtime/queries/ocaml-interface/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/ocaml-interface/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm
index 15f46cc1..a08b1267 100644
--- a/runtime/queries/ocaml/highlights.scm
+++ b/runtime/queries/ocaml/highlights.scm
@@ -90,7 +90,7 @@
["exception" "try"] @keyword.control.exception
-["include" "open"] @include
+["include" "open"] @keyword.control.import
["for" "to" "downto" "while" "do" "done"] @keyword.control.repeat
diff --git a/runtime/queries/ocaml/indents.toml b/runtime/queries/ocaml/indents.toml
index 9b6462d8..7586b83a 100644
--- a/runtime/queries/ocaml/indents.toml
+++ b/runtime/queries/ocaml/indents.toml
@@ -8,6 +8,6 @@ indent = [
"match_case",
]
-oudent = [
+outdent = [
"}",
]
diff --git a/runtime/queries/ocaml/injections.scm b/runtime/queries/ocaml/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/ocaml/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/perl/indents.toml b/runtime/queries/perl/indents.toml
new file mode 100644
index 00000000..365e0663
--- /dev/null
+++ b/runtime/queries/perl/indents.toml
@@ -0,0 +1,17 @@
+indent = [
+ "function",
+ "identifier",
+ "method_invocation",
+ "if_statement",
+ "unless_statement",
+ "if_simple_statement",
+ "unless_simple_statement",
+ "variable_declaration",
+ "block",
+ "list_item",
+ "word_list_qw"
+]
+
+outdent = [
+ "}"
+]
diff --git a/runtime/queries/perl/injections.scm b/runtime/queries/perl/injections.scm
new file mode 100644
index 00000000..cab5f53d
--- /dev/null
+++ b/runtime/queries/perl/injections.scm
@@ -0,0 +1,2 @@
+((comments) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/php/highlights.scm b/runtime/queries/php/highlights.scm
index 46b5d26c..5379fa1e 100644
--- a/runtime/queries/php/highlights.scm
+++ b/runtime/queries/php/highlights.scm
@@ -5,7 +5,8 @@
(primitive_type) @type.builtin
(cast_type) @type.builtin
-(type_name (name) @type)
+(named_type (name) @type) @type
+(named_type (qualified_name) @type) @type
; Functions
@@ -85,10 +86,12 @@
"endif" @keyword
"endswitch" @keyword
"endwhile" @keyword
+"enum" @keyword
"extends" @keyword
"final" @keyword
"finally" @keyword
"foreach" @keyword
+"fn" @keyword
"function" @keyword
"global" @keyword
"if" @keyword
@@ -97,6 +100,7 @@
"include" @keyword
"insteadof" @keyword
"interface" @keyword
+"match" @keyword
"namespace" @keyword
"new" @keyword
"private" @keyword
diff --git a/runtime/queries/php/injections.scm b/runtime/queries/php/injections.scm
index 16d5736b..614a3850 100644
--- a/runtime/queries/php/injections.scm
+++ b/runtime/queries/php/injections.scm
@@ -1,3 +1,6 @@
((text) @injection.content
(#set! injection.language "html")
(#set! injection.combined))
+
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/php/textobjects.scm b/runtime/queries/php/textobjects.scm
new file mode 100644
index 00000000..04ffefd2
--- /dev/null
+++ b/runtime/queries/php/textobjects.scm
@@ -0,0 +1,30 @@
+(class_declaration
+ body: (_) @class.inside) @class.around
+
+(interface_declaration
+ body: (_) @class.inside) @class.around
+
+(trait_declaration
+ body: (_) @class.inside) @class.around
+
+(enum_declaration
+ body: (_) @class.inside) @class.around
+
+(function_definition
+ body: (_) @function.inside) @function.around
+
+(method_declaration
+ body: (_) @function.inside) @function.around
+
+(arrow_function
+ body: (_) @function.inside) @function.around
+
+(anonymous_function_creation_expression
+ body: (_) @function.inside) @function.around
+
+(formal_parameters
+ [
+ (simple_parameter)
+ (variadic_parameter)
+ (property_promotion_parameter)
+ ] @parameter.inside)
diff --git a/runtime/queries/protobuf/injections.scm b/runtime/queries/protobuf/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/protobuf/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/python/injections.scm b/runtime/queries/python/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/python/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/regex/highlights.scm b/runtime/queries/regex/highlights.scm
new file mode 100644
index 00000000..9376caa9
--- /dev/null
+++ b/runtime/queries/regex/highlights.scm
@@ -0,0 +1,53 @@
+; upstream: https://github.com/tree-sitter/tree-sitter-regex/blob/e1cfca3c79896ff79842f057ea13e529b66af636/queries/highlights.scm
+
+[
+ "("
+ ")"
+ "(?"
+ "(?:"
+ "(?<"
+ ">"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "*"
+ "+"
+ "|"
+ "="
+ "<="
+ "!"
+ "<!"
+ "?"
+] @operator
+
+[
+ (identity_escape)
+ (control_letter_escape)
+ (character_class_escape)
+ (control_escape)
+ (start_assertion)
+ (end_assertion)
+ (boundary_assertion)
+ (non_boundary_assertion)
+] @constant.character.escape
+
+(group_name) @property
+
+(count_quantifier
+ [
+ (decimal_digits) @constant.numeric
+ "," @punctuation.delimiter
+ ])
+
+(character_class
+ [
+ "^" @operator
+ (class_range "-" @operator)
+ ])
+
+(class_character) @constant.character
+(pattern_character) @string
diff --git a/runtime/queries/rescript/highlights.scm b/runtime/queries/rescript/highlights.scm
new file mode 100644
index 00000000..b9ab8ea6
--- /dev/null
+++ b/runtime/queries/rescript/highlights.scm
@@ -0,0 +1,179 @@
+(comment) @comment
+
+; Identifiers
+;------------
+
+; Escaped identifiers like \"+."
+((value_identifier) @function.macro
+ (#match? @function.macro "^\\.*$"))
+
+[
+ (type_identifier)
+ (unit_type)
+ "list"
+] @type
+
+[
+ (variant_identifier)
+ (polyvar_identifier)
+] @constant
+
+(property_identifier) @variable.other.member
+(module_identifier) @namespace
+
+(jsx_identifier) @tag
+(jsx_attribute (property_identifier) @variable.parameter)
+
+; Parameters
+;----------------
+
+(list_pattern (value_identifier) @variable.parameter)
+(spread_pattern (value_identifier) @variable.parameter)
+
+; String literals
+;----------------
+
+[
+ (string)
+ (template_string)
+] @string
+
+(template_substitution
+ "${" @punctuation.bracket
+ "}" @punctuation.bracket) @embedded
+
+(character) @constant.character
+(escape_sequence) @constant.character.escape
+
+; Other literals
+;---------------
+
+[
+ (true)
+ (false)
+] @constant.builtin
+
+(number) @constant.numeric
+(polyvar) @constant
+(polyvar_string) @constant
+
+; Functions
+;----------
+
+[
+ (formal_parameters (value_identifier))
+ (positional_parameter (value_identifier))
+ (labeled_parameter (value_identifier))
+] @variable.parameter
+
+(function parameter: (value_identifier) @variable.parameter)
+
+; Meta
+;-----
+
+[
+ "@"
+ "@@"
+ (decorator_identifier)
+] @label
+
+(extension_identifier) @keyword
+("%") @keyword
+
+; Misc
+;-----
+
+(subscript_expression index: (string) @variable.other.member)
+(polyvar_type_pattern "#" @constant)
+
+[
+ ("include")
+ ("open")
+] @keyword
+
+[
+ "as"
+ "export"
+ "external"
+ "let"
+ "module"
+ "mutable"
+ "private"
+ "rec"
+ "type"
+ "and"
+] @keyword
+
+[
+ "if"
+ "else"
+ "switch"
+] @keyword
+
+[
+ "exception"
+ "try"
+ "catch"
+ "raise"
+] @keyword
+
+[
+ "."
+ ","
+ "|"
+] @punctuation.delimiter
+
+[
+ "++"
+ "+"
+ "+."
+ "-"
+ "-."
+ "*"
+ "*."
+ "/"
+ "/."
+ "<"
+ "<="
+ "=="
+ "==="
+ "!"
+ "!="
+ "!=="
+ ">"
+ ">="
+ "&&"
+ "||"
+ "="
+ ":="
+ "->"
+ "|>"
+ ":>"
+ (uncurry)
+] @operator
+
+[
+ "("
+ ")"
+ "{"
+ "}"
+ "["
+ "]"
+] @punctuation.bracket
+
+(polyvar_type
+ [
+ "["
+ "[>"
+ "[<"
+ "]"
+ ] @punctuation.bracket)
+
+[
+ "~"
+ "?"
+ "=>"
+ "..."
+] @punctuation
+
+(ternary_expression ["?" ":"] @operator)
diff --git a/runtime/queries/rescript/injections.scm b/runtime/queries/rescript/injections.scm
new file mode 100644
index 00000000..201cce75
--- /dev/null
+++ b/runtime/queries/rescript/injections.scm
@@ -0,0 +1,8 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
+
+((raw_js) @injection.content
+ (#set! injection.language "javascript"))
+
+((raw_gql) @injection.content
+ (#set! injection.language "graphql"))
diff --git a/runtime/queries/rescript/textobjects.scm b/runtime/queries/rescript/textobjects.scm
new file mode 100644
index 00000000..7ee8cd1a
--- /dev/null
+++ b/runtime/queries/rescript/textobjects.scm
@@ -0,0 +1,9 @@
+; Classes (modules)
+;------------------
+
+(module_declaration definition: ((_) @class.inside)) @class.around
+
+; Functions
+;----------
+
+(function body: (_) @function.inside) @function.around
diff --git a/runtime/queries/ruby/indents.toml b/runtime/queries/ruby/indents.toml
new file mode 100644
index 00000000..b417751f
--- /dev/null
+++ b/runtime/queries/ruby/indents.toml
@@ -0,0 +1,25 @@
+indent = [
+ "argument_list",
+ "array",
+ "begin",
+ "block",
+ "call",
+ "class",
+ "case",
+ "do_block",
+ "elsif",
+ "if",
+ "hash",
+ "method",
+ "module",
+ "singleton_class",
+ "singleton_method",
+]
+
+outdent = [
+ ")",
+ "}",
+ "]",
+ "end",
+ "when",
+]
diff --git a/runtime/queries/ruby/injections.scm b/runtime/queries/ruby/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/ruby/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm
index 539d9550..26496c66 100644
--- a/runtime/queries/rust/highlights.scm
+++ b/runtime/queries/rust/highlights.scm
@@ -127,11 +127,16 @@
"await"
] @keyword.control
+"use" @keyword.control.import
+(mod_item "mod" @keyword.control.import !body)
+(use_as_clause "as" @keyword.control.import)
+
+(type_cast_expression "as" @keyword.operator)
+
[
(crate)
(super)
"as"
- "use"
"pub"
"mod"
"extern"
@@ -242,10 +247,9 @@
; ---
; Macros
; ---
-
(meta_item
- (identifier) @attribute)
-(attribute_item) @attribute
+ (identifier) @function.macro)
+
(inner_attribute_item) @attribute
(macro_definition
@@ -259,7 +263,7 @@
"!" @function.macro)
(metavariable) @variable.parameter
-(fragment_specifier) @variable.parameter
+(fragment_specifier) @type
diff --git a/runtime/queries/rust/indents.toml b/runtime/queries/rust/indents.toml
index 3900f0b9..51a0ceea 100644
--- a/runtime/queries/rust/indents.toml
+++ b/runtime/queries/rust/indents.toml
@@ -9,6 +9,7 @@ indent = [
"field_initializer_list",
"struct_pattern",
"tuple_pattern",
+ "unit_expression",
"enum_variant_list",
"call_expression",
"binary_expression",
diff --git a/runtime/queries/rust/injections.scm b/runtime/queries/rust/injections.scm
index 6035d418..77c70805 100644
--- a/runtime/queries/rust/injections.scm
+++ b/runtime/queries/rust/injections.scm
@@ -1,3 +1,6 @@
+([(line_comment) (block_comment)] @injection.content
+ (#set! injection.language "comment"))
+
((macro_invocation
(token_tree) @injection.content)
(#set! injection.language "rust")
@@ -7,3 +10,17 @@
(token_tree) @injection.content)
(#set! injection.language "rust")
(#set! injection.include-children))
+
+(call_expression
+ function: (scoped_identifier
+ path: (identifier) @_regex (#eq? @_regex "Regex")
+ name: (identifier) @_new (#eq? @_new "new"))
+ arguments: (arguments (raw_string_literal) @injection.content)
+ (#set! injection.language "regex"))
+
+(call_expression
+ function: (scoped_identifier
+ path: (scoped_identifier (identifier) @_regex (#eq? @_regex "Regex") .)
+ name: (identifier) @_new (#eq? @_new "new"))
+ arguments: (arguments (raw_string_literal) @injection.content)
+ (#set! injection.language "regex"))
diff --git a/runtime/queries/scala/highlights.scm b/runtime/queries/scala/highlights.scm
new file mode 100644
index 00000000..50a6e18a
--- /dev/null
+++ b/runtime/queries/scala/highlights.scm
@@ -0,0 +1,203 @@
+; CREDITS @stumash (stuart.mashaal@gmail.com)
+
+;; variables
+
+
+((identifier) @variable.builtin
+ (#match? @variable.builtin "^this$"))
+
+(interpolation) @none
+
+; Assume other uppercase names constants.
+; NOTE: In order to distinguish constants we highlight
+; all the identifiers that are uppercased. But this solution
+; is not suitable for all occurrences e.g. it will highlight
+; an uppercased method as a constant if used with no params.
+; Introducing highlighting for those specific cases, is probably
+; best way to resolve the issue.
+((identifier) @constant (#match? @constant "^[A-Z]"))
+
+;; types
+
+(type_identifier) @type
+
+(class_definition
+ name: (identifier) @type)
+
+(object_definition
+ name: (identifier) @type)
+
+(trait_definition
+ name: (identifier) @type)
+
+(type_definition
+ name: (type_identifier) @type)
+
+; method definition
+
+(class_definition
+ body: (template_body
+ (function_definition
+ name: (identifier) @function.method)))
+(object_definition
+ body: (template_body
+ (function_definition
+ name: (identifier) @function.method)))
+(trait_definition
+ body: (template_body
+ (function_definition
+ name: (identifier) @function.method)))
+
+; imports
+
+(import_declaration
+ path: (identifier) @namespace)
+((stable_identifier (identifier) @namespace))
+
+((import_declaration
+ path: (identifier) @type) (#match? @type "^[A-Z]"))
+((stable_identifier (identifier) @type) (#match? @type "^[A-Z]"))
+
+((import_selectors (identifier) @type) (#match? @type "^[A-Z]"))
+
+; method invocation
+
+
+(call_expression
+ function: (identifier) @function)
+
+(call_expression
+ function: (field_expression
+ field: (identifier) @function.method))
+
+((call_expression
+ function: (identifier) @variable.other.member)
+ (#match? @variable.other.member "^[A-Z]"))
+
+(generic_function
+ function: (identifier) @function)
+
+(
+ (identifier) @function.builtin
+ (#match? @function.builtin "^super$")
+)
+
+; function definitions
+
+(function_definition
+ name: (identifier) @function)
+
+(parameter
+ name: (identifier) @variable.parameter)
+
+; expressions
+
+
+(field_expression field: (identifier) @variable.other.member)
+(field_expression value: (identifier) @type
+ (#match? @type "^[A-Z]"))
+
+(infix_expression operator: (identifier) @operator)
+(infix_expression operator: (operator_identifier) @operator)
+(infix_type operator: (operator_identifier) @operator)
+(infix_type operator: (operator_identifier) @operator)
+
+; literals
+(boolean_literal) @constant.builtin.boolean
+(integer_literal) @constant.numeric.integer
+(floating_point_literal) @constant.numeric.float
+
+
+(symbol_literal) @string.special.symbol
+
+[
+(string)
+(character_literal)
+(interpolated_string_expression)
+] @string
+
+(interpolation "$" @punctuation.special)
+
+;; keywords
+
+[
+ "abstract"
+ "case"
+ "class"
+ "extends"
+ "final"
+ "finally"
+;; `forSome` existential types not implemented yet
+ "implicit"
+ "lazy"
+;; `macro` not implemented yet
+ "object"
+ "override"
+ "package"
+ "private"
+ "protected"
+ "sealed"
+ "trait"
+ "type"
+ "val"
+ "var"
+ "with"
+] @keyword
+
+(null_literal) @constant.builtin
+(wildcard) @keyword
+
+;; special keywords
+
+"new" @keyword.operator
+
+[
+ "else"
+ "if"
+ "match"
+ "try"
+ "catch"
+ "throw"
+] @keyword.control.conditional
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "."
+ ","
+] @punctuation.delimiter
+
+[
+ "do"
+ "for"
+ "while"
+ "yield"
+] @keyword.control.repeat
+
+"def" @keyword.function
+
+[
+ "=>"
+ "<-"
+ "@"
+] @keyword.operator
+
+"import" @keyword.control.import
+
+"return" @keyword.control.return
+
+(comment) @comment
+
+;; `case` is a conditional keyword in case_block
+
+(case_block
+ (case_clause ("case") @keyword.control.conditional))
+
+(identifier) @variable \ No newline at end of file
diff --git a/runtime/queries/scala/indents.toml b/runtime/queries/scala/indents.toml
new file mode 100644
index 00000000..6de54844
--- /dev/null
+++ b/runtime/queries/scala/indents.toml
@@ -0,0 +1,23 @@
+
+indent = [
+ "block",
+ "arguments",
+ "parameter",
+ "class_definition",
+ "trait_definition",
+ "object_definition",
+ "function_definition",
+ "val_definition",
+ "import_declaration",
+ "while_expression",
+ "do_while_expression",
+ "for_expression",
+ "try_expression",
+ "match_expression"
+]
+
+outdent = [
+ "}",
+ "]",
+ ")"
+]
diff --git a/runtime/queries/scala/injections.scm b/runtime/queries/scala/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/scala/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm
index 4fcdfd66..22b0c551 100644
--- a/runtime/queries/svelte/highlights.scm
+++ b/runtime/queries/svelte/highlights.scm
@@ -20,12 +20,12 @@
((element (start_tag (tag_name) @_tag) (text) @markup.inline)
(#match? @_tag "^(code|kbd)$"))
-((element (start_tag (tag_name) @_tag) (text) @markup.underline.link)
+((element (start_tag (tag_name) @_tag) (text) @markup.link.url)
(#eq? @_tag "a"))
((attribute
(attribute_name) @_attr
- (quoted_attribute_value (attribute_value) @markup.undeline.link))
+ (quoted_attribute_value (attribute_value) @markup.link.url))
(#match? @_attr "^(href|src)$"))
(tag_name) @tag
diff --git a/runtime/queries/svelte/injections.scm b/runtime/queries/svelte/injections.scm
index 266f4701..04e860cf 100644
--- a/runtime/queries/svelte/injections.scm
+++ b/runtime/queries/svelte/injections.scm
@@ -26,5 +26,5 @@
(#set! injection.language "typescript")
)
-(comment) @comment
-
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/tablegen/highlights.scm b/runtime/queries/tablegen/highlights.scm
new file mode 100644
index 00000000..8ade5ba9
--- /dev/null
+++ b/runtime/queries/tablegen/highlights.scm
@@ -0,0 +1,90 @@
+[
+ (comment)
+ (multiline_comment)
+] @comment
+
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+ "<"
+ ">"
+] @punctuation.bracket
+
+[
+ ","
+ ";"
+ "."
+] @punctuation.delimiter
+
+[
+ "#"
+ "-"
+ "..."
+ ":"
+] @operator
+
+[
+ "="
+ "!cond"
+ (operator_keyword)
+] @function
+
+[
+ "true"
+ "false"
+] @constant.builtin.boolean
+
+[
+ "?"
+] @constant.builtin
+
+(var) @variable
+
+(template_arg (identifier) @variable.parameter)
+
+(_ argument: (value (identifier) @variable.parameter))
+
+(type) @type
+
+"code" @type.builtin
+
+(number) @constant.numeric.integer
+[
+ (string_string)
+ (code_string)
+] @string
+
+(preprocessor) @keyword.directive
+
+[
+ "class"
+ "field"
+ "let"
+ "defvar"
+ "def"
+ "defset"
+ "defvar"
+ "assert"
+] @keyword
+
+[
+ "let"
+ "in"
+ "foreach"
+ "if"
+ "then"
+ "else"
+] @keyword.operator
+
+"include" @keyword.control.import
+
+[
+ "multiclass"
+ "defm"
+] @namespace
+
+(ERROR) @error
diff --git a/runtime/queries/tablegen/indents.toml b/runtime/queries/tablegen/indents.toml
new file mode 100644
index 00000000..43532f4d
--- /dev/null
+++ b/runtime/queries/tablegen/indents.toml
@@ -0,0 +1,7 @@
+indent = [
+ "statement",
+]
+
+outdent = [
+ "}",
+]
diff --git a/runtime/queries/tablegen/injections.scm b/runtime/queries/tablegen/injections.scm
new file mode 100644
index 00000000..0b476f86
--- /dev/null
+++ b/runtime/queries/tablegen/injections.scm
@@ -0,0 +1,2 @@
+([ (comment) (multiline_comment)] @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/tablegen/textobjects.scm b/runtime/queries/tablegen/textobjects.scm
new file mode 100644
index 00000000..2cb80268
--- /dev/null
+++ b/runtime/queries/tablegen/textobjects.scm
@@ -0,0 +1,7 @@
+(class
+ body: (_) @class.inside) @class.around
+
+(multiclass
+ body: (_) @class.inside) @class.around
+
+(_ argument: _ @parameter.inside)
diff --git a/runtime/queries/toml/injections.scm b/runtime/queries/toml/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/toml/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/tsq/injections.scm b/runtime/queries/tsq/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/tsq/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/tsx/injections.scm b/runtime/queries/tsx/injections.scm
new file mode 100644
index 00000000..1b61e36d
--- /dev/null
+++ b/runtime/queries/tsx/injections.scm
@@ -0,0 +1 @@
+; inherits: typescript
diff --git a/runtime/queries/twig/highlights.scm b/runtime/queries/twig/highlights.scm
new file mode 100644
index 00000000..2c95ab63
--- /dev/null
+++ b/runtime/queries/twig/highlights.scm
@@ -0,0 +1,16 @@
+(comment_directive) @comment
+
+[
+ "{%"
+ "{%-"
+ "{%~"
+ "%}"
+ "-%}"
+ "~%}"
+ "{{"
+ "{{-"
+ "{{~"
+ "}}"
+ "-}}"
+ "~}}"
+] @keyword
diff --git a/runtime/queries/twig/injections.scm b/runtime/queries/twig/injections.scm
new file mode 100644
index 00000000..f0822734
--- /dev/null
+++ b/runtime/queries/twig/injections.scm
@@ -0,0 +1,3 @@
+((content) @injection.content
+ (#set! injection.language "html")
+ (#set! injection.combined))
diff --git a/runtime/queries/typescript/injections.scm b/runtime/queries/typescript/injections.scm
new file mode 100644
index 00000000..ff0ddfac
--- /dev/null
+++ b/runtime/queries/typescript/injections.scm
@@ -0,0 +1 @@
+; inherits: javascript
diff --git a/runtime/queries/vue/injections.scm b/runtime/queries/vue/injections.scm
index 8ee34ffb..73df868b 100644
--- a/runtime/queries/vue/injections.scm
+++ b/runtime/queries/vue/injections.scm
@@ -15,3 +15,6 @@
((style_element
(raw_text) @injection.content)
(#set! injection.language "css"))
+
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/wgsl/injections.scm b/runtime/queries/wgsl/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/wgsl/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/yaml/highlights.scm b/runtime/queries/yaml/highlights.scm
index a7efb5e7..e4fed27a 100644
--- a/runtime/queries/yaml/highlights.scm
+++ b/runtime/queries/yaml/highlights.scm
@@ -1,9 +1,19 @@
-(block_mapping_pair key: (_) @variable.other.member)
-(flow_mapping (_ key: (_) @variable.other.member))
+(block_mapping_pair
+ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member))
+(block_mapping_pair
+ key: (flow_node (plain_scalar (string_scalar) @variable.other.member)))
+
+(flow_mapping
+ (_ key: (flow_node [(double_quote_scalar) (single_quote_scalar)] @variable.other.member)))
+(flow_mapping
+ (_ key: (flow_node (plain_scalar (string_scalar) @variable.other.member))))
+
(boolean_scalar) @constant.builtin.boolean
(null_scalar) @constant.builtin
(double_quote_scalar) @string
(single_quote_scalar) @string
+(block_scalar) @string
+(string_scalar) @string
(escape_sequence) @constant.character.escape
(integer_scalar) @constant.numeric.integer
(float_scalar) @constant.numeric.float
@@ -30,4 +40,4 @@
"}"
] @punctuation.bracket
-["*" "&"] @punctuation.special
+["*" "&" "---" "..."] @punctuation.special
diff --git a/runtime/queries/yaml/injections.scm b/runtime/queries/yaml/injections.scm
new file mode 100644
index 00000000..321c90ad
--- /dev/null
+++ b/runtime/queries/yaml/injections.scm
@@ -0,0 +1,2 @@
+((comment) @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/queries/zig/highlights.scm b/runtime/queries/zig/highlights.scm
index 34dbeacd..62c99acc 100644
--- a/runtime/queries/zig/highlights.scm
+++ b/runtime/queries/zig/highlights.scm
@@ -144,7 +144,6 @@ field_constant: (IDENTIFIER) @constant
; VarDecl
[
- "comptime"
"threadlocal"
"fn"
] @keyword.function
@@ -178,6 +177,7 @@ field_constant: (IDENTIFIER) @constant
; PrecProc
[
+ "comptime"
"inline"
"noinline"
"asm"
@@ -195,15 +195,14 @@ field_constant: (IDENTIFIER) @constant
(BitwiseOp)
(BitShiftOp)
(AdditionOp)
+ (AssignOp)
(MultiplyOp)
(PrefixOp)
"*"
"**"
"->"
- "=>"
".?"
".*"
- "="
"?"
] @operator
diff --git a/runtime/queries/zig/indents.toml b/runtime/queries/zig/indents.toml
index 88f88e16..36ba8e55 100644
--- a/runtime/queries/zig/indents.toml
+++ b/runtime/queries/zig/indents.toml
@@ -3,6 +3,9 @@ indent = [
"BlockExpr",
"ContainerDecl",
"SwitchExpr",
+ "AssignExpr",
+ "ErrorUnionExpr",
+ "Statement",
"InitList"
]
diff --git a/runtime/queries/zig/injections.scm b/runtime/queries/zig/injections.scm
new file mode 100644
index 00000000..3df95897
--- /dev/null
+++ b/runtime/queries/zig/injections.scm
@@ -0,0 +1,2 @@
+([(line_comment) (doc_comment)] @injection.content
+ (#set! injection.language "comment"))
diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml
index d65995c0..d19863e0 100644
--- a/runtime/themes/base16_default_dark.toml
+++ b/runtime/themes/base16_default_dark.toml
@@ -1,27 +1,28 @@
-# Author: RayGervais<raygervais@hotmail.ca>
+# Author: RayGervais <raygervais@hotmail.ca>
"ui.background" = { bg = "base00" }
"ui.menu" = "base01"
-"ui.menu.selected" = { fg = "base04", bg = "base01" }
-"ui.linenr" = {fg = "base01" }
+"ui.menu.selected" = { fg = "base01", bg = "base04" }
+"ui.linenr" = { fg = "base03", bg = "base01" }
"ui.popup" = { bg = "base01" }
"ui.window" = { bg = "base01" }
-"ui.liner.selected" = "base02"
-"ui.selection" = "base02"
-"comment" = "base03"
-"ui.statusline" = {fg = "base04", bg = "base01" }
+"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
+"ui.selection" = { bg = "base02" }
+"comment" = { fg = "base03", modifiers = ["italic"] }
+"ui.statusline" = { fg = "base04", bg = "base01" }
"ui.help" = { fg = "base04", bg = "base01" }
-"ui.cursor" = { fg = "base05", modifiers = ["reversed"] }
-"ui.text" = { fg = "base05" }
+"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
+"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
+"ui.text" = "base05"
"operator" = "base05"
-"ui.text.focus" = { fg = "base05" }
+"ui.text.focus" = "base05"
"variable" = "base08"
"constant.numeric" = "base09"
"constant" = "base09"
-"attributes" = "base09"
+"attributes" = "base09"
"type" = "base0A"
"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
-"strings" = "base0B"
+"string" = "base0B"
"variable.other.member" = "base0B"
"constant.character.escape" = "base0C"
"function" = "base0D"
@@ -30,15 +31,28 @@
"keyword" = "base0E"
"label" = "base0E"
"namespace" = "base0E"
-"ui.popup" = { bg = "base01" }
-"ui.window" = { bg = "base00" }
-"ui.help" = { bg = "base01", fg = "base06" }
+"ui.help" = { fg = "base06", bg = "base01" }
+
+"markup.heading" = "base0D"
+"markup.list" = "base08"
+"markup.bold" = { fg = "base0A", modifiers = ["bold"] }
+"markup.italic" = { fg = "base0E", modifiers = ["italic"] }
+"markup.link.url" = { fg = "base09", modifiers = ["underlined"] }
+"markup.link.text" = "base08"
+"markup.quote" = "base0C"
+"markup.raw" = "base0B"
+
+"diff.plus" = "base0B"
+"diff.delta" = "base09"
+"diff.minus" = "base08"
-"info" = "base03"
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "base01" }
+"info" = "base0D"
"hint" = "base03"
"debug" = "base03"
-"diagnostic" = "base03"
-"error" = "base0E"
+"warning" = "base09"
+"error" = "base08"
[palette]
base00 = "#181818" # Default Background
diff --git a/runtime/themes/base16_default_light.toml b/runtime/themes/base16_default_light.toml
new file mode 100644
index 00000000..483e87cc
--- /dev/null
+++ b/runtime/themes/base16_default_light.toml
@@ -0,0 +1,73 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.background" = { bg = "base00" }
+"ui.menu" = "base01"
+"ui.menu.selected" = { fg = "base01", bg = "base04" }
+"ui.linenr" = { fg = "base03", bg = "base01" }
+"ui.popup" = { bg = "base01" }
+"ui.window" = { bg = "base01" }
+"ui.linenr.selected" = { fg = "base04", bg = "base01", modifiers = ["bold"] }
+"ui.selection" = { bg = "base02" }
+"comment" = { fg = "base03", modifiers = ["italic"] }
+"ui.statusline" = { fg = "base04", bg = "base01" }
+"ui.help" = { fg = "base04", bg = "base01" }
+"ui.cursor" = { fg = "base04", modifiers = ["reversed"] }
+"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
+"ui.text" = "base05"
+"operator" = "base05"
+"ui.text.focus" = "base05"
+"variable" = "base08"
+"constant.numeric" = "base09"
+"constant" = "base09"
+"attributes" = "base09"
+"type" = "base0A"
+"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] }
+"string" = "base0B"
+"variable.other.member" = "base0B"
+"constant.character.escape" = "base0C"
+"function" = "base0D"
+"constructor" = "base0D"
+"special" = "base0D"
+"keyword" = "base0E"
+"label" = "base0E"
+"namespace" = "base0E"
+"ui.help" = { fg = "base06", bg = "base01" }
+
+"markup.heading" = "base0D"
+"markup.list" = "base08"
+"markup.bold" = { fg = "base0A", modifiers = ["bold"] }
+"markup.italic" = { fg = "base0E", modifiers = ["italic"] }
+"markup.link.url" = { fg = "base09", modifiers = ["underlined"] }
+"markup.link.text" = "base08"
+"markup.quote" = "base0C"
+"markup.raw" = "base0B"
+
+"diff.plus" = "base0B"
+"diff.delta" = "base09"
+"diff.minus" = "base08"
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "base01" }
+"info" = "base0D"
+"hint" = "base03"
+"debug" = "base03"
+"warning" = "base09"
+"error" = "base08"
+
+[palette]
+base00 = "#f8f8f8" # Default Background
+base01 = "#e8e8e8" # Lighter Background (Used for status bars, line number and folding marks)
+base02 = "#d8d8d8" # Selection Background
+base03 = "#b8b8b8" # Comments, Invisibles, Line Highlighting
+base04 = "#585858" # Dark Foreground (Used for status bars)
+base05 = "#383838" # Default Foreground, Caret, Delimiters, Operators
+base06 = "#282828" # Light Foreground (Not often used)
+base07 = "#181818" # Light Background (Not often used)
+base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted
+base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url
+base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background
+base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted
+base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes
+base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings
+base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed
+base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. <?php ?>
diff --git a/runtime/themes/base16_terminal.toml b/runtime/themes/base16_terminal.toml
new file mode 100644
index 00000000..23240d8d
--- /dev/null
+++ b/runtime/themes/base16_terminal.toml
@@ -0,0 +1,52 @@
+# Author: NNB <nnbnh@protonmail.com>
+
+"ui.menu" = "black"
+"ui.menu.selected" = { modifiers = ["reversed"] }
+"ui.linenr" = { fg = "light-gray", bg = "black" }
+"ui.popup" = { bg = "black" }
+"ui.window" = { bg = "black" }
+"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] }
+"ui.selection" = { fg = "gray", modifiers = ["reversed"] }
+"comment" = { fg = "light-gray", modifiers = ["italic"] }
+"ui.statusline" = { fg = "white", bg = "black" }
+"ui.statusline.inactive" = { fg = "gray", bg = "black" }
+"ui.help" = { fg = "white", bg = "black" }
+"ui.cursor" = { fg = "light-gray", modifiers = ["reversed"] }
+"ui.cursor.primary" = { fg = "light-white", modifiers = ["reversed"] }
+"variable" = "light-red"
+"constant.numeric" = "yellow"
+"constant" = "yellow"
+"attributes" = "yellow"
+"type" = "light-yellow"
+"ui.cursor.match" = { fg = "light-yellow", modifiers = ["underlined"] }
+"string" = "light-green"
+"variable.other.member" = "light-green"
+"constant.character.escape" = "light-cyan"
+"function" = "light-blue"
+"constructor" = "light-blue"
+"special" = "light-blue"
+"keyword" = "light-magenta"
+"label" = "light-magenta"
+"namespace" = "light-magenta"
+"ui.help" = { fg = "white", bg = "black" }
+
+"markup.heading" = "light-blue"
+"markup.list" = "light-red"
+"markup.bold" = { fg = "light-yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "light-magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "light-red"
+"markup.quote" = "light-cyan"
+"markup.raw" = "light-green"
+
+"diff.plus" = "light-green"
+"diff.delta" = "yellow"
+"diff.minus" = "light-red"
+
+"diagnostic" = { modifiers = ["underlined"] }
+"ui.gutter" = { bg = "black" }
+"info" = "light-blue"
+"hint" = "gray"
+"debug" = "gray"
+"warning" = "yellow"
+"error" = "light-red"
diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml
index 86a6c34b..32b58d0a 100644
--- a/runtime/themes/bogster.toml
+++ b/runtime/themes/bogster.toml
@@ -28,6 +28,20 @@
"module" = "#d32c5d"
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "#59dcb7"
+"diff.delta" = "#dc7759"
+"diff.minus" = "#dc597f"
+
"ui.background" = { bg = "#161c23" }
"ui.linenr" = { fg = "#415367" }
"ui.linenr.selected" = { fg = "#e5ded6" } # TODO
@@ -49,3 +63,6 @@
"error" = "#dc597f"
"info" = "#59dcb7"
"hint" = "#59c0dc"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml
index 0554f827..ab7c16ec 100644
--- a/runtime/themes/dark_plus.toml
+++ b/runtime/themes/dark_plus.toml
@@ -39,6 +39,20 @@
"constant.numeric" = { fg = "pale_green" }
"constant.character.escape" = { fg = "gold" }
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = { fg = "pale_green" }
+"diff.delta" = { fg = "gold" }
+"diff.minus" = { fg = "red" }
+
"ui.background" = { fg = "light_gray", bg = "dark_gray2" }
"ui.window" = { bg = "widget" }
diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml
new file mode 100644
index 00000000..1db25d8f
--- /dev/null
+++ b/runtime/themes/dracula.toml
@@ -0,0 +1,63 @@
+# Author : Sebastian Zivota <loewenheim@mailbox.org>
+"comment" = { fg = "comment" }
+"constant" = { fg = "purple" }
+"constant.character.escape" = { fg = "pink" }
+"function" = { fg = "green" }
+"keyword" = { fg = "pink" }
+"operator" = { fg = "pink" }
+"punctuation" = { fg = "foreground" }
+"string" = { fg = "yellow" }
+"string.regexp" = { fg = "red" }
+"tag" = { fg = "pink" }
+"type" = { fg = "cyan", modifiers = ["italic"] }
+"type.enum.variant" = { fg = "foreground", modifiers = ["italic"] }
+"variable" = { fg = "foreground" }
+"variable.builtin" = { fg = "cyan", modifiers = ["italic"] }
+"variable.parameter" = { fg ="orange", modifiers = ["italic"] }
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
+"ui.background" = { fg = "foreground", bg = "background" }
+"ui.cursor" = { fg = "background", bg = "orange", modifiers = ["dim"] }
+"ui.cursor.match" = { fg = "green", modifiers = ["underlined"] }
+"ui.cursor.primary" = { fg = "background", bg = "cyan", modifier = ["dim"] }
+"ui.help" = { fg = "foreground", bg = "background_dark" }
+"ui.linenr" = { fg = "comment" }
+"ui.linenr.selected" = { fg = "foreground" }
+"ui.menu" = { fg = "foreground", bg = "background_dark" }
+"ui.menu.selected" = { fg = "cyan", bg = "background_dark" }
+"ui.popup" = { fg = "foreground", bg = "background_dark" }
+"ui.selection" = { fg = "background", bg = "purple", modifiers = ["dim"] }
+"ui.selection.primary" = { fg = "background", bg = "pink" }
+"ui.statusline" = { fg = "foreground", bg = "background_dark" }
+"ui.statusline.inactive" = { fg = "comment", bg = "background_dark" }
+"ui.text" = { fg = "foreground" }
+"ui.text.focus" = { fg = "cyan" }
+"ui.window" = { fg = "foreground" }
+
+"error" = { fg = "red" }
+"warning" = { fg = "cyan" }
+
+"markup.heading" = { fg = "purple", modifiers = ["bold"] }
+"markup.list" = "cyan"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
+"markup.link.url" = "cyan"
+"markup.link.text" = "pink"
+"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
+"markup.raw" = { fg = "foreground" }
+
+[palette]
+background = "#282a36"
+background_dark = "#21222c"
+foreground = "#f8f8f2"
+comment = "#6272a4"
+red = "#ff5555"
+orange = "#ffb86c"
+yellow = "#f1fa8c"
+green = "#50fa7b"
+purple = "#bd93f9"
+cyan = "#8be9fd"
+pink = "#ff79c6"
diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml
index bbd005e6..a6389da2 100644
--- a/runtime/themes/everforest_dark.toml
+++ b/runtime/themes/everforest_dark.toml
@@ -12,7 +12,7 @@
"type" = "yellow"
"constant" = "purple"
"constant.numeric" = "purple"
-"string" = "grey2"
+"string" = "green"
"comment" = "grey0"
"variable" = "fg"
"variable.builtin" = "blue"
@@ -34,6 +34,20 @@
"module" = "blue"
"special" = "orange"
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
"ui.background" = { bg = "bg0" }
"ui.cursor" = { fg = "bg0", bg = "fg" }
"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" }
diff --git a/runtime/themes/everforest_light.toml b/runtime/themes/everforest_light.toml
new file mode 100644
index 00000000..5490adb3
--- /dev/null
+++ b/runtime/themes/everforest_light.toml
@@ -0,0 +1,100 @@
+# Everforest (Dark Hard)
+# Author: CptPotato
+
+# Original Author:
+# URL: https://github.com/sainnhe/everforest
+# Filename: autoload/everforest.vim
+# Author: sainnhe
+# Email: sainnhe@gmail.com
+# License: MIT License
+
+"constant.character.escape" = "orange"
+"type" = "yellow"
+"constant" = "purple"
+"constant.numeric" = "purple"
+"string" = "green"
+"comment" = "grey0"
+"variable" = "fg"
+"variable.builtin" = "blue"
+"variable.parameter" = "fg"
+"variable.other.member" = "fg"
+"label" = "aqua"
+"punctuation" = "grey2"
+"punctuation.delimiter" = "grey2"
+"punctuation.bracket" = "fg"
+"keyword" = "red"
+"operator" = "orange"
+"function" = "green"
+"function.builtin" = "blue"
+"function.macro" = "aqua"
+"tag" = "yellow"
+"namespace" = "aqua"
+"attribute" = "aqua"
+"constructor" = "yellow"
+"module" = "blue"
+"special" = "orange"
+
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+"ui.background" = { bg = "bg0" }
+"ui.cursor" = { fg = "bg0", bg = "fg" }
+"ui.cursor.match" = { fg = "orange", bg = "bg_yellow" }
+"ui.cursor.insert" = { fg = "bg0", bg = "grey1" }
+"ui.cursor.select" = { fg = "bg0", bg = "blue" }
+"ui.linenr" = "grey0"
+"ui.linenr.selected" = "fg"
+"ui.statusline" = { fg = "grey2", bg = "bg2" }
+"ui.statusline.inactive" = { fg = "grey0", bg = "bg1" }
+"ui.popup" = { fg = "grey2", bg = "bg1" }
+"ui.window" = { fg = "grey2", bg = "bg1" }
+"ui.help" = { fg = "fg", bg = "bg1" }
+"ui.text" = "fg"
+"ui.text.focus" = "fg"
+"ui.menu" = { fg = "fg", bg = "bg2" }
+"ui.menu.selected" = { fg = "bg0", bg = "green" }
+"ui.selection" = { bg = "bg3" }
+
+"hint" = "blue"
+"info" = "aqua"
+"warning" = "yellow"
+"error" = "red"
+"diagnostic" = { modifiers = ["underlined"] }
+
+
+[palette]
+
+bg0 = "#fff9e8"
+bg1 = "#f7f4e0"
+bg2 = "#f0eed9"
+bg3 = "#e9e8d2"
+bg4 = "#e1ddcb"
+bg5 = "#bec5b2"
+bg_visual = "#edf0cd"
+bg_red = "#fce5dc"
+bg_green = "#f1f3d4"
+bg_blue = "#eaf2eb"
+bg_yellow = "#fbefd0"
+
+fg = "#5c6a72"
+red = "#f85552"
+orange = "#f57d26"
+yellow = "#dfa000"
+green = "#8da101"
+aqua = "#35a77c"
+blue = "#3a94c5"
+purple = "#df69ba"
+grey0 = "#a6b0a0"
+grey1 = "#939f91"
+grey2 = "#829181"
diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml
index 0ff039ea..a976a9bd 100644
--- a/runtime/themes/gruvbox.toml
+++ b/runtime/themes/gruvbox.toml
@@ -19,6 +19,7 @@
"function" = { fg = "green1", modifiers = ["bold"] }
"function.macro" = "aqua1"
"function.builtin" = "yellow1"
+"tag" = "red1"
"comment" = { fg = "gray1", modifiers = ["italic"] }
"constant" = { fg = "purple1" }
"constant.builtin" = { fg = "purple1", modifiers = ["bold"] }
@@ -28,6 +29,10 @@
"label" = "aqua1"
"module" = "aqua1"
+"diff.plus" = "green1"
+"diff.delta" = "orange1"
+"diff.minus" = "red1"
+
"warning" = { fg = "orange1", bg = "bg1" }
"error" = { fg = "red1", bg = "bg1" }
"info" = { fg = "aqua1", bg = "bg1" }
@@ -51,6 +56,13 @@
"diagnostic" = { modifiers = ["underlined"] }
+"markup.heading" = "aqua1"
+"markup.bold" = { modifiers = ["bold"] }
+"markup.italic" = { modifiers = ["italic"] }
+"markup.link.url" = { fg = "green1", modifiers = ["underlined"] }
+"markup.link.text" = "red1"
+"markup.raw" = "red1"
+
[palette]
bg0 = "#282828" # main background
bg1 = "#3c3836"
diff --git a/runtime/themes/gruvbox_light.toml b/runtime/themes/gruvbox_light.toml
new file mode 100644
index 00000000..81ea7fd1
--- /dev/null
+++ b/runtime/themes/gruvbox_light.toml
@@ -0,0 +1,96 @@
+# Author : Rohan Jain <crodjer@pm.me>
+# Author : Jakub Bartodziej <kubabartodziej@gmail.com>
+# The theme uses the gruvbox light palette with standard contrast: github.com/morhetz/gruvbox
+
+"attribute" = "aqua1"
+"keyword" = { fg = "red1" }
+"keyword.directive" = "red0"
+"namespace" = "aqua1"
+"punctuation" = "orange1"
+"punctuation.delimiter" = "orange1"
+"operator" = "purple1"
+"special" = "purple0"
+"variable.other.member" = "blue1"
+"variable" = "fg1"
+"variable.builtin" = "orange1"
+"variable.parameter" = "fg2"
+"type" = "yellow1"
+"type.builtin" = "yellow1"
+"constructor" = { fg = "purple1", modifiers = ["bold"] }
+"function" = { fg = "green1", modifiers = ["bold"] }
+"function.macro" = "aqua1"
+"function.builtin" = "yellow1"
+"tag" = "red1"
+"comment" = { fg = "gray1", modifiers = ["italic"] }
+"constant" = { fg = "purple1" }
+"constant.builtin" = { fg = "purple1", modifiers = ["bold"] }
+"string" = "green1"
+"constant.numeric" = "purple1"
+"constant.character.escape" = { fg = "fg2", modifiers = ["bold"] }
+"label" = "aqua1"
+"module" = "aqua1"
+
+"diff.plus" = "green1"
+"diff.delta" = "orange1"
+"diff.minus" = "red1"
+
+"warning" = { fg = "orange1", bg = "bg1" }
+"error" = { fg = "red1", bg = "bg1" }
+"info" = { fg = "aqua1", bg = "bg1" }
+"hint" = { fg = "blue1", bg = "bg1" }
+
+"ui.background" = { bg = "bg0" }
+"ui.linenr" = { fg = "bg4" }
+"ui.linenr.selected" = { fg = "yellow1" }
+"ui.statusline" = { fg = "fg1", bg = "bg2" }
+"ui.statusline.inactive" = { fg = "fg4", bg = "bg1" }
+"ui.popup" = { bg = "bg1" }
+"ui.window" = { bg = "bg1" }
+"ui.help" = { bg = "bg1", fg = "fg1" }
+"ui.text" = { fg = "fg1" }
+"ui.text.focus" = { fg = "fg1" }
+"ui.selection" = { bg = "bg3", modifiers = ["reversed"] }
+"ui.cursor.primary" = { modifiers = ["reversed"] }
+"ui.cursor.match" = { modifiers = ["reversed"] }
+"ui.menu" = { fg = "fg1", bg = "bg2" }
+"ui.menu.selected" = { fg = "bg2", bg = "blue1", modifiers = ["bold"] }
+
+"diagnostic" = { modifiers = ["underlined"] }
+
+"markup.heading" = "aqua1"
+"markup.bold" = { modifiers = ["bold"] }
+"markup.italic" = { modifiers = ["italic"] }
+"markup.link.url" = { fg = "green1", modifiers = ["underlined"] }
+"markup.link.text" = "red1"
+"markup.raw" = "red1"
+
+[palette]
+bg0 = "#fbf1c7" # main background
+bg1 = "#ebdbb2"
+bg2 = "#d5c4a1"
+bg3 = "#bdae93"
+bg4 = "#a89984"
+
+fg0 = "#282828" # main foreground
+fg1 = "#3c3836"
+fg2 = "#504945"
+fg3 = "#665c54"
+fg4 = "#7c6f64" # gray0
+
+gray0 = "#7c6f64"
+gray1 = "#928374"
+
+red0 = "#cc241d" # neutral
+red1 = "#9d0006" # bright
+green0 = "#98971a"
+green1 = "#79740e"
+yellow0 = "#d79921"
+yellow1 = "#b57614"
+blue0 = "#458588"
+blue1 = "#076678"
+purple0 = "#b16286"
+purple1 = "#8f3f71"
+aqua0 = "#689d6a"
+aqua1 = "#427b58"
+orange0 = "#d65d0e"
+orange1 = "#af3a03"
diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml
index 30829475..a7c33e2d 100644
--- a/runtime/themes/ingrid.toml
+++ b/runtime/themes/ingrid.toml
@@ -28,6 +28,20 @@
"module" = "#839A53"
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "#839A53"
+"diff.delta" = "#D4A520"
+"diff.minus" = "#D74E50"
+
"ui.background" = { bg = "#FFFCFD" }
"ui.linenr" = { fg = "#bbbbbb" }
"ui.linenr.selected" = { fg = "#F3EAE9" } # TODO
diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml
index 38f9f170..e6ff0a5e 100644
--- a/runtime/themes/monokai.toml
+++ b/runtime/themes/monokai.toml
@@ -39,6 +39,20 @@
"constant.numeric" = { fg = "#ae81ff" }
"constant.character.escape" = { fg = "#ae81ff" }
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = { fg = "#a6e22e" }
+"diff.delta" = { fg = "#fd971f" }
+"diff.minus" = { fg = "#f92672" }
+
"ui.background" = { fg = "text", bg = "background" }
"ui.window" = { bg = "widget" }
@@ -65,7 +79,7 @@
"warning" = { fg = "#cca700" }
"error" = { fg = "#f48771" }
"info" = { fg = "#75beff" }
-"hint" = { fg = "#eeeeeeb3" }
+"hint" = { fg = "#eeeeeb3" }
diagnostic = { modifiers = ["underlined"] }
diff --git a/runtime/themes/monokai_pro.toml b/runtime/themes/monokai_pro.toml
new file mode 100644
index 00000000..8de9994c
--- /dev/null
+++ b/runtime/themes/monokai_pro.toml
@@ -0,0 +1,115 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[palette]
+# primary colors
+"red" = "#ff6188"
+"orange" = "#fc9867"
+"yellow" = "#ffd866"
+"green" = "#a9dc76"
+"blue" = "#78dce8"
+"purple" = "#ab9df2"
+# base colors, sorted from darkest to lightest
+"base0" = "#19181a"
+"base1" = "#221f22"
+"base2" = "#2d2a2e"
+"base3" = "#403e41"
+"base4" = "#5b595c"
+"base5" = "#727072"
+"base6" = "#939293"
+"base7" = "#c1c0c0"
+"base8" = "#fcfcfa"
+# variants (for when transparency isn't supported)
+"base8x0c" = "#363337" # using base2 as bg
diff --git a/runtime/themes/monokai_pro_machine.toml b/runtime/themes/monokai_pro_machine.toml
new file mode 100644
index 00000000..c5890042
--- /dev/null
+++ b/runtime/themes/monokai_pro_machine.toml
@@ -0,0 +1,115 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[palette]
+# primary colors
+"red" = "#ff6d7e"
+"orange" = "#ffb270"
+"yellow" = "#ffed72"
+"green" = "#a2e57b"
+"blue" = "#7cd5f1"
+"purple" = "#baa0f8"
+# base colors
+"base0" = "#161b1e"
+"base1" = "#1d2528"
+"base2" = "#273136"
+"base3" = "#3a4449"
+"base4" = "#545f62"
+"base5" = "#6b7678"
+"base6" = "#798384"
+"base7" = "#b8c4c3"
+"base8" = "#f2fffc"
+# variants
+"base8x0c" = "#303a3e"
diff --git a/runtime/themes/monokai_pro_octagon.toml b/runtime/themes/monokai_pro_octagon.toml
new file mode 100644
index 00000000..d9badf3c
--- /dev/null
+++ b/runtime/themes/monokai_pro_octagon.toml
@@ -0,0 +1,115 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[palette]
+# primary colors
+"red" = "#ff657a"
+"orange" = "#ff9b5e"
+"yellow" = "#ffd76d"
+"green" = "#bad761"
+"blue" = "#9cd1bb"
+"purple" = "#c39ac9"
+# base colors
+"base0" = "#161821"
+"base1" = "#1e1f2b"
+"base2" = "#282a3a"
+"base3" = "#3a3d4b"
+"base4" = "#535763"
+"base5" = "#696d77"
+"base6" = "#767b81"
+"base7" = "#b2b9bd"
+"base8" = "#eaf2f1"
+# variants
+"base8x0c" = "#303342"
diff --git a/runtime/themes/monokai_pro_ristretto.toml b/runtime/themes/monokai_pro_ristretto.toml
new file mode 100644
index 00000000..ed7ebeae
--- /dev/null
+++ b/runtime/themes/monokai_pro_ristretto.toml
@@ -0,0 +1,115 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[palette]
+# primary colors
+"red" = "#fd6883"
+"orange" = "#f38d70"
+"yellow" = "#f9cc6c"
+"green" = "#adda78"
+"blue" = "#85dacc"
+"purple" = "#a8a9eb"
+# base colors
+"base0" = "#191515"
+"base1" = "#211c1c"
+"base2" = "#2c2525"
+"base3" = "#403838"
+"base4" = "#5b5353"
+"base5" = "#72696a"
+"base6" = "#8c8384"
+"base7" = "#c3b7b8"
+"base8" = "#fff1f3"
+# variants
+"base8x0c" = "#352e2e"
diff --git a/runtime/themes/monokai_pro_spectrum.toml b/runtime/themes/monokai_pro_spectrum.toml
new file mode 100644
index 00000000..da06e597
--- /dev/null
+++ b/runtime/themes/monokai_pro_spectrum.toml
@@ -0,0 +1,115 @@
+# Author : WindSoilder<WindSoilder@outlook.com>
+# The unofficial Monokai Pro theme, simply migrate from jetbrains monokai pro theme: https://github.com/subtheme-dev/monokai-pro
+# Credit goes to the original creator: https://monokai.pro
+
+"ui.linenr.selected" = { bg = "base3" }
+"ui.text.focus" = { fg = "yellow", modifiers= ["bold"] }
+"ui.menu.selected" = { fg = "base2", bg = "yellow" }
+
+"info" = "base8"
+"hint" = "base8"
+
+# background color
+"ui.background" = { bg = "base2" }
+"ui.statusline.inactive" = { fg = "base8", bg = "base8x0c" }
+
+# status bars, panels, modals, autocompletion
+"ui.statusline" = { bg = "base4" }
+"ui.popup" = { bg = "base3" }
+"ui.window" = { bg = "base3" }
+"ui.help" = { bg = "base3" }
+
+# active line, highlighting
+"ui.selection" = { bg = "base4" }
+"ui.cursor.match" = { bg = "base4" }
+
+# comments, nord3 based lighter color
+"comment" = { fg = "base5", modifiers = ["italic"] }
+"ui.linenr" = { fg = "base5" }
+
+# cursor, variables, constants, attributes, fields
+"ui.cursor.primary" = { fg = "base7", modifiers = ["reversed"] }
+"attribute" = "blue"
+"variable" = "base8"
+"constant" = "orange"
+"variable.builtin" = "red"
+"constant.builtin" = "red"
+"namespace" = "base8"
+
+# base text, punctuation
+"ui.text" = { fg = "base8" }
+"punctuation" = "base6"
+
+# classes, types, primiatives
+"type" = "green"
+"type.builtin" = { fg = "red"}
+"label" = "base8"
+
+# declaration, methods, routines
+"constructor" = "blue"
+"function" = "green"
+"function.macro" = { fg = "blue" }
+"function.builtin" = { fg = "cyan" }
+
+# operator, tags, units, punctuations
+"operator" = "red"
+"variable.other.member" = "base8"
+
+# keywords, special
+"keyword" = { fg = "red" }
+"keyword.directive" = "blue"
+"variable.parameter" = "#f59762"
+
+# error
+"error" = "red"
+
+# annotations, decorators
+"special" = "#f59762"
+"module" = "#f59762"
+
+# warnings, escape characters, regex
+"warning" = "orange"
+"constant.character.escape" = { fg = "base8" }
+
+# strings
+"string" = "yellow"
+
+# integer, floating point
+"constant.numeric" = "purple"
+
+# vcs
+"diff.plus" = "green"
+"diff.delta" = "orange"
+"diff.minus" = "red"
+
+# make diagnostic underlined, to distinguish with selection text.
+diagnostic = { modifiers = ["underlined"] }
+
+# markup highlight, no need for `markup.raw` and `markup.list`, make them to be default
+"markup.heading" = "green"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "orange", modifiers = ["italic"] }
+"markup.link.url" = { fg = "orange", modifiers = ["underlined"] }
+"markup.link.text" = "yellow"
+"markup.quote" = "green"
+
+[palette]
+# primary colors
+"red" = "#fc618d"
+"orange" = "#fd9353"
+"yellow" = "#fce566"
+"green" = "#7bd88f"
+"blue" = "#5ad4e6"
+"purple" = "#948ae3"
+# base colors
+"base0" = "#131313"
+"base1" = "#191919"
+"base2" = "#222222"
+"base3" = "#363537"
+"base4" = "#525053"
+"base5" = "#69676c"
+"base6" = "#8b888f"
+"base7" = "#bab6c0"
+"base8" = "#f7f1ff"
+# variants
+"base8x0c" = "#2b2b2b"
diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml
index a619f902..deb90452 100644
--- a/runtime/themes/nord.toml
+++ b/runtime/themes/nord.toml
@@ -84,6 +84,21 @@
# nord15 - integer, floating point
"constant.numeric" = "nord15"
+# TODO markup
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+# vcs
+"diff.plus" = "nord14"
+"diff.delta" = "nord12"
+"diff.minus" = "nord11"
+
[palette]
nord0 = "#2e3440"
nord1 = "#3b4252"
diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml
index 40ed1abe..acdaf99c 100644
--- a/runtime/themes/onedark.toml
+++ b/runtime/themes/onedark.toml
@@ -3,27 +3,42 @@
"attribute" = { fg = "yellow" }
"comment" = { fg = "light-gray", modifiers = ["italic"] }
"constant" = { fg = "cyan" }
-"constant.builtin" = { fg = "blue" }
+"constant.numeric" = { fg = "gold" }
+"constant.builtin" = { fg = "gold" }
+"constant.character.escape" = { fg = "gold" }
"constructor" = { fg = "blue" }
-"escape" = { fg = "gold" }
"function" = { fg = "blue" }
"function.builtin" = { fg = "blue" }
"function.macro" = { fg = "purple" }
"keyword" = { fg = "red" }
"keyword.control" = { fg = "purple" }
+"keyword.control.import" = { fg = "red" }
"keyword.directive" = { fg = "purple" }
"label" = { fg = "purple" }
"namespace" = { fg = "blue" }
-"number" = { fg = "gold" }
"operator" = { fg = "purple" }
+"keyword.operator" = { fg = "purple" }
"property" = { fg = "red" }
"special" = { fg = "blue" }
"string" = { fg = "green" }
"type" = { fg = "yellow" }
-"type.builtin" = { fg = "yellow" }
# "variable" = { fg = "blue" }
"variable.builtin" = { fg = "blue" }
"variable.parameter" = { fg = "red" }
+"variable.other.member" = { fg = "red" }
+
+"markup.heading" = { fg = "red" }
+"markup.raw.inline" = { fg = "green" }
+"markup.bold" = { fg = "gold", modifiers = ["bold"] }
+"markup.italic" = { fg = "purple", modifiers = ["italic"] }
+"markup.list" = { fg = "red" }
+"markup.quote" = { fg = "yellow" }
+"markup.link.url" = { fg = "cyan", modifiers = ["underlined"]}
+"markup.link.text" = { fg = "purple" }
+
+"diff.plus" = "green"
+"diff.delta" = "gold"
+"diff.minus" = "red"
diagnostic = { modifiers = ["underlined"] }
"info" = { fg = "blue", modifiers = ["bold"] }
diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml
index 53777008..66717bb2 100644
--- a/runtime/themes/rose_pine.toml
+++ b/runtime/themes/rose_pine.toml
@@ -1,15 +1,15 @@
# Author: RayGervais<raygervais@hotmail.ca>
+# Author: ChrisHa<chunghha@users.noreply.github.com>
"ui.background" = { bg = "base" }
-"ui.menu" = "surface"
+"ui.menu" = { fg = "text", bg = "overlay" }
"ui.menu.selected" = { fg = "iris", bg = "surface" }
"ui.linenr" = {fg = "subtle" }
-"ui.popup" = { bg = "overlay" }
-"ui.window" = { bg = "overlay" }
"ui.liner.selected" = "highlightOverlay"
"ui.selection" = "highlight"
"comment" = "subtle"
"ui.statusline" = {fg = "foam", bg = "surface" }
+"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
"ui.help" = { fg = "foam", bg = "surface" }
"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
"ui.text" = { fg = "text" }
@@ -32,10 +32,13 @@
"keyword" = "pine"
"label" = "iris"
"namespace" = "pine"
-"ui.popup" = { bg = "overlay" }
+"ui.popup" = { bg = "surface" }
"ui.window" = { bg = "base" }
"ui.help" = { bg = "overlay", fg = "foam" }
"text" = "text"
+"diff.plus" = "foam"
+"diff.delta" = "rose"
+"diff.minus" = "love"
"info" = "gold"
"hint" = "gold"
@@ -43,6 +46,15 @@
"diagnostic" = "rose"
"error" = "love"
+"markup.heading" = { fg = "rose" }
+"markup.raw.inline" = { fg = "foam" }
+"markup.bold" = { fg = "gold", modifiers = ["bold"] }
+"markup.italic" = { fg = "iris", modifiers = ["italic"] }
+"markup.list" = { fg = "love" }
+"markup.quote" = { fg = "rose" }
+"markup.link.url" = { fg = "pine", modifiers = ["underlined"]}
+"markup.link.text" = { fg = "foam" }
+
[palette]
base = "#191724"
surface = "#1f1d2e"
diff --git a/runtime/themes/rose_pine_dawn.toml b/runtime/themes/rose_pine_dawn.toml
new file mode 100644
index 00000000..bec77506
--- /dev/null
+++ b/runtime/themes/rose_pine_dawn.toml
@@ -0,0 +1,73 @@
+# Author: RayGervais<raygervais@hotmail.ca>
+# Author: ChrisHa<chunghha@users.noreply.github.com>
+
+"ui.background" = { bg = "surface" }
+"ui.menu" = { fg = "text", bg = "overlay" }
+"ui.menu.selected" = { fg = "iris", bg = "surface" }
+"ui.linenr" = {fg = "subtle" }
+"ui.liner.selected" = "highlightOverlay"
+"ui.selection" = "highlight"
+"comment" = "subtle"
+"ui.statusline" = {fg = "foam", bg = "surface" }
+"ui.statusline.inactive" = { fg = "iris", bg = "surface" }
+"ui.help" = { fg = "foam", bg = "surface" }
+"ui.cursor" = { fg = "rose", modifiers = ["reversed"] }
+"ui.text" = { fg = "text" }
+"operator" = "rose"
+"ui.text.focus" = { fg = "base05" }
+"variable" = "text"
+"number" = "iris"
+"constant" = "gold"
+"attributes" = "gold"
+"type" = "foam"
+"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] }
+"string" = "gold"
+"property" = "foam"
+"escape" = "subtle"
+"function" = "rose"
+"function.builtin" = "rose"
+"function.method" = "foam"
+"constructor" = "gold"
+"special" = "gold"
+"keyword" = "pine"
+"label" = "iris"
+"namespace" = "pine"
+"ui.popup" = { bg = "surface" }
+"ui.window" = { bg = "base" }
+"ui.help" = { bg = "overlay", fg = "foam" }
+"text" = "text"
+"diff.plus" = "foam"
+"diff.delta" = "rose"
+"diff.minus" = "love"
+
+"info" = "gold"
+"hint" = "gold"
+"debug" = "rose"
+"diagnostic" = "rose"
+"error" = "love"
+
+"markup.heading" = { fg = "rose" }
+"markup.raw.inline" = { fg = "foam" }
+"markup.bold" = { fg = "gold", modifiers = ["bold"] }
+"markup.italic" = { fg = "iris", modifiers = ["italic"] }
+"markup.list" = { fg = "love" }
+"markup.quote" = { fg = "rose" }
+"markup.link.url" = { fg = "pine", modifiers = ["underlined"]}
+"markup.link.test" = { fg = "foam" }
+
+[palette]
+base = "#faf4ed"
+surface = "#fffaf3"
+overlay = "#f2e9de"
+inactive = "#9893a5"
+subtle = "#6e6a86"
+text = "#575279"
+love = "#b4637a"
+gold = "#ea9d34"
+rose = "#d7827e"
+pine = "#286983"
+foam = "#56949f"
+iris = "#907aa9"
+highlight = "#eee9e6"
+highlightInactive = "#f2ede9"
+highlightOverlay = "#e4dfde"
diff --git a/runtime/themes/serika-dark.toml b/runtime/themes/serika-dark.toml
new file mode 100644
index 00000000..da145780
--- /dev/null
+++ b/runtime/themes/serika-dark.toml
@@ -0,0 +1,99 @@
+# Serika (Dark)
+# Author: VuiMuich
+
+# Original Author:
+# URL: https://github.com/arturoalviar/serika-syntax
+# Author: arturoalviar
+# License: MIT License
+
+"escape" = "orange"
+"type" = "yellow"
+"constant" = "purple"
+"number" = "purple"
+"string" = "fg"
+"comment" = "grey2"
+"variable" = "yellow"
+"variable.builtin" = "blue"
+"variable.parameter" = "yellow"
+"variable.property" = "yellow"
+"label" = "aqua"
+"punctuation" = "grey0"
+"punctuation.delimiter" = "grey2"
+"punctuation.bracket" = "fg"
+"keyword" = "red"
+"operator" = "grey0"
+"function" = "green"
+"function.builtin" = "blue"
+"function.macro" = "aqua"
+"tag" = "yellow"
+"namespace" = "fg"
+"attribute" = "aqua"
+"constructor" = "yellow"
+"module" = "blue"
+"property" = "yellow"
+"special" = "orange"
+
+"ui.background" = { bg = "bg0" }
+"ui.cursor" = { fg = "bg0", bg = "fg" }
+"ui.cursor.match" = { fg = "grey3", bg = "grey2" }
+"ui.cursor.insert" = { fg = "bg0", bg = "bg_yellow" }
+"ui.cursor.select" = { fg = "bg0", bg = "bg_yellow" }
+"ui.linenr" = "yellow"
+"ui.linenr.selected" = { fg = "fg", modifiers = ["bold", "underlined"] }
+"ui.statusline" = { fg = "grey1", bg = "bg2" }
+"ui.statusline.inactive" = { fg = "grey2", bg = "bg1" }
+"ui.popup" = { fg = "grey2", bg = "bg1" }
+"ui.window" = { fg = "grey2", bg = "bg1" }
+"ui.help" = { fg = "fg", bg = "bg1" }
+"ui.text" = "fg"
+"ui.text.focus" = "yellow"
+"ui.menu" = { fg = "fg", bg = "bg2" }
+"ui.menu.selected" = { fg = "bg0", bg = "bg_yellow" }
+"ui.selection" = { bg = "bg3" }
+
+"hint" = "blue"
+"info" = "aqua"
+"warning" = "yellow"
+"error" = "nasty-red"
+"diagnostic" = { fg = "dark-red", Modifiers = ["underlined"] }
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
+"markup.heading" = { fg = "purple", modifiers = ["bold"] }
+"markup.list" = "cyan"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
+"markup.link.url" = "cyan"
+"markup.link.text" = "pink"
+"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
+"markup.raw" = { fg = "foreground" }
+
+[palette]
+
+bg0 = "#323437"
+bg1 = "#494c50"
+bg2 = "#55585e"
+bg3 = "#61656b"
+bg4 = "#6d7278"
+bg5 = "#797e86"
+bg_visual = "#646669"
+bg_red = "#7e2a33"
+bg_green = "#86b365"
+bg_blue = "#6a89af"
+bg_yellow = "#e2b714"
+
+fg = "#d1d0c5"
+red = "#f9ebed"
+nasty-red = "#ca4754"
+dark-red = "#7e2a33"
+orange = "#dd8a3c"
+yellow = "#e2b714"
+green = "#e5eae1"
+aqua = "#b9c2c6"
+blue = "#bdcadb"
+purple = "#d0c4d4"
+grey0 = "#aaaeb3"
+grey1 = "#e1e1e3"
+grey2 = "#646669"
diff --git a/runtime/themes/serika-light.toml b/runtime/themes/serika-light.toml
new file mode 100644
index 00000000..edde9044
--- /dev/null
+++ b/runtime/themes/serika-light.toml
@@ -0,0 +1,100 @@
+# Serika (Light)
+# Author: VuiMuich
+
+# Original Author:
+# URL: https://github.com/arturoalviar/serika-syntax
+# Author: arturoalviar
+# License: MIT License
+
+"escape" = "orange"
+"type" = "yellow"
+"constant" = "purple"
+"number" = "purple"
+"string" = "fg"
+"comment" = "grey2"
+"variable" = "yellow"
+"variable.builtin" = "blue"
+"variable.parameter" = "yellow"
+"variable.property" = "yellow"
+"label" = "aqua"
+"punctuation" = "grey0"
+"punctuation.delimiter" = "grey2"
+"punctuation.bracket" = "fg"
+"keyword" = "red"
+"operator" = "grey0"
+"function" = "green"
+"function.builtin" = "blue"
+"function.macro" = "aqua"
+"tag" = "yellow"
+"namespace" = "fg"
+"attribute" = "aqua"
+"constructor" = "yellow"
+"module" = "blue"
+"property" = "yellow"
+"special" = "orange"
+
+"ui.background" = { bg = "bg0" }
+"ui.cursor" = { fg = "bg0", bg = "fg" }
+"ui.cursor.match" = { fg = "grey1", bg = "grey2" }
+"ui.cursor.insert" = { fg = "bg0", bg = "bg_yellow" }
+"ui.cursor.select" = { fg = "bg0", bg = "bg_yellow" }
+"ui.linenr" = "yellow"
+"ui.linenr.selected" = { fg = "fg", modifiers = ["bold", "underlined"] }
+"ui.statusline" = { fg = "grey1", bg = "bg5" }
+"ui.statusline.inactive" = { fg = "grey2", bg = "bg1" }
+"ui.popup" = { fg = "bg0", bg = "bg5" }
+"ui.window" = { fg = "bg0", bg = "bg5" }
+"ui.help" = { fg = "bg0", bg = "bg5" }
+"ui.text" = "fg"
+"ui.text.focus" = "yellow"
+"ui.menu" = { fg = "bg0", bg = "bg3" }
+"ui.menu.selected" = { fg = "bg0", bg = "bg_yellow" }
+"ui.selection" = { fg = "bg0", bg = "bg3" }
+
+"hint" = "blue"
+"info" = "aqua"
+"warning" = "yellow"
+"error" = "nasty-red"
+"diagnostic" = { fg = "dark-red", Modifiers = ["underlined"] }
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
+"markup.heading" = { fg = "purple", modifiers = ["bold"] }
+"markup.list" = "cyan"
+"markup.bold" = { fg = "orange", modifiers = ["bold"] }
+"markup.italic" = { fg = "yellow", modifiers = ["italic"] }
+"markup.link.url" = "cyan"
+"markup.link.text" = "pink"
+"markup.quote" = { fg = "yellow", modifiers = ["italic"] }
+"markup.raw" = { fg = "foreground" }
+
+
+[palette]
+
+bg0 = "#e1e1e3"
+bg1 = "#494c50"
+bg2 = "#55585e"
+bg3 = "#61656b"
+bg4 = "#6d7278"
+bg5 = "#797e86"
+bg_visual = "#646669"
+bg_red = "#7e2a33"
+bg_green = "#86b365"
+bg_blue = "#6a89af"
+bg_yellow = "#e2b714"
+
+fg = "#323437"
+red = "#621d28"
+nasty-red = "#da3333"
+dark-red = "#791717"
+orange = "#57320f"
+yellow = "#e2b714"
+green = "#3f4b34"
+aqua = "#455054"
+blue = "#3f5673"
+purple = "#534059"
+grey0 = "#aaaeb3"
+grey1 = "#e1e1e3"
+grey2 = "#646669"
diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml
index 984c86ee..dfaa104a 100644
--- a/runtime/themes/solarized_dark.toml
+++ b/runtime/themes/solarized_dark.toml
@@ -22,6 +22,20 @@
"module" = { fg = "violet" }
"tag" = { fg = "magenta" }
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
# 背景
"ui.background" = { bg = "base03" }
@@ -58,13 +72,13 @@
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
-"ui.cursor.primary" = {fg = "base03", bg = "base1"}
-"ui.selection.primary" = { fg = "base03", bg = "base01" }
-"ui.cursor.select" = {fg = "base02", bg = "green"}
-"ui.selection" = { fg = "base02", bg = "yellow" }
+"ui.cursor.primary" = { fg = "base03", bg = "base1" }
+"ui.cursor.select" = { fg = "base02", bg = "cyan" }
+"ui.selection" = { bg = "base0175" }
+"ui.selection.primary" = { bg = "base015" }
# normal模式的光标
-"ui.cursor" = {fg = "base03", bg = "green"}
+"ui.cursor" = {fg = "base02", bg = "cyan"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
@@ -73,18 +87,20 @@
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
-"diagnostic" = { mdifiers = ["underlined"] }
+"diagnostic" = { modifiers = ["underlined"] }
[palette]
# 深色 越来越深
-base03 = "#002b36"
-base02 = "#073642"
-base01 = "#586e75"
-base00 = "#657b83"
-base0 = "#839496"
-base1 = "#93a1a1"
-base2 = "#eee8d5"
-base3 = "#fdf6e3"
+base03 = "#002b36"
+base02 = "#073642"
+base0175 = "#16404b"
+base015 = "#2c4f59"
+base01 = "#586e75"
+base00 = "#657b83"
+base0 = "#839496"
+base1 = "#93a1a1"
+base2 = "#eee8d5"
+base3 = "#fdf6e3"
# 浅色 越來越浅
yellow = "#b58900"
diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml
index 0ab1b962..c8a3dee1 100644
--- a/runtime/themes/solarized_light.toml
+++ b/runtime/themes/solarized_light.toml
@@ -22,6 +22,20 @@
"module" = { fg = "violet" }
"tag" = { fg = "magenta" }
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = { fg = "green" }
+"diff.delta" = { fg = "orange" }
+"diff.minus" = { fg = "red" }
+
# 背景
"ui.background" = { bg = "base03" }
@@ -58,13 +72,13 @@
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
-"ui.cursor.primary" = {fg = "base03", bg = "base1"}
-"ui.selection.primary" = { fg = "base03", bg = "base01" }
-"ui.cursor.select" = {fg = "base02", bg = "green"}
-"ui.selection" = { fg = "base02", bg = "yellow" }
+"ui.cursor.primary" = { fg = "base03", bg = "base1" }
+"ui.cursor.select" = { fg = "base02", bg = "cyan" }
+"ui.selection" = { bg = "base0175" }
+"ui.selection.primary" = { bg = "base015" }
# normal模式的光标
-"ui.cursor" = {fg = "base03", bg = "green"}
+"ui.cursor" = {fg = "base02", bg = "cyan"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
@@ -73,26 +87,28 @@
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
-"diagnostic" = { mdifiers = ["underlined"] }
+"diagnostic" = { modifiers = ["underlined"] }
[palette]
-red = '#dc322f'
-green = '#859900'
-yellow = '#b58900'
-blue = '#268bd2'
-magenta = '#d33682'
-cyan = '#2aa198'
-orange = '#cb4b16'
-violet = '#6c71c4'
+red = '#dc322f'
+green = '#859900'
+yellow = '#b58900'
+blue = '#268bd2'
+magenta = '#d33682'
+cyan = '#2aa198'
+orange = '#cb4b16'
+violet = '#6c71c4'
# 深色 越来越深
-base0 = '#657b83'
-base1 = '#586e75'
-base2 = '#073642'
-base3 = '#002b36'
+base0 = '#657b83'
+base1 = '#586e75'
+base2 = '#073642'
+base3 = '#002b36'
## 浅色 越來越浅
-base00 = '#839496'
-base01 = '#93a1a1'
-base02 = '#eee8d5'
-base03 = '#fdf6e3'
+base00 = '#839496'
+base01 = '#93a1a1'
+base015 = '#c5c8bd'
+base0175 = '#dddbcc'
+base02 = '#eee8d5'
+base03 = '#fdf6e3'
diff --git a/runtime/themes/spacebones_light.toml b/runtime/themes/spacebones_light.toml
index 92f116ab..5318dc2d 100644
--- a/runtime/themes/spacebones_light.toml
+++ b/runtime/themes/spacebones_light.toml
@@ -30,6 +30,20 @@
"label" = "#b1951d"
"module" = "#b1951d"
+# TODO
+"markup.heading" = "blue"
+"markup.list" = "red"
+"markup.bold" = { fg = "yellow", modifiers = ["bold"] }
+"markup.italic" = { fg = "magenta", modifiers = ["italic"] }
+"markup.link.url" = { fg = "yellow", modifiers = ["underlined"] }
+"markup.link.text" = "red"
+"markup.quote" = "cyan"
+"markup.raw" = "green"
+
+"diff.plus" = "#2d9574"
+"diff.delta" = "#715ab1"
+"diff.minus" = "#ba2f59"
+
"warning" = { fg = "#da8b55" }
"error" = { fg = "#e0211d" }
"info" = { fg = "#b1951d" }
diff --git a/theme.toml b/theme.toml
index 8c0d1f6c..d2c1fc32 100644
--- a/theme.toml
+++ b/theme.toml
@@ -28,6 +28,17 @@ string = "silver"
# used for lifetimes
label = "honey"
+"markup.heading" = "lilac"
+"markup.bold" = { modifiers = ["bold"] }
+"markup.italic" = { modifiers = ["italic"] }
+"markup.link.url" = { fg = "silver", modifiers = ["underlined"] }
+"markup.link.text" = "almond"
+"markup.raw" = "almond"
+
+"diff.plus" = "#35bf86"
+"diff.minus" = "#f22c86"
+"diff.delta" = "#6f44f0"
+
# TODO: diferentiate doc comment
# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors
diff --git a/xtask/Cargo.toml b/xtask/Cargo.toml
new file mode 100644
index 00000000..717530d0
--- /dev/null
+++ b/xtask/Cargo.toml
@@ -0,0 +1,11 @@
+[package]
+name = "xtask"
+version = "0.6.0"
+edition = "2021"
+
+# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
+
+[dependencies]
+helix-term = { version = "0.6", path = "../helix-term" }
+helix-core = { version = "0.6", path = "../helix-core" }
+toml = "0.5"
diff --git a/xtask/src/main.rs b/xtask/src/main.rs
new file mode 100644
index 00000000..d24a29cc
--- /dev/null
+++ b/xtask/src/main.rs
@@ -0,0 +1,277 @@
+use std::{env, error::Error};
+
+type DynError = Box<dyn Error>;
+
+pub mod helpers {
+ use std::{
+ fmt::Display,
+ path::{Path, PathBuf},
+ };
+
+ use crate::path;
+ use helix_core::syntax::Configuration as LangConfig;
+
+ #[derive(Copy, Clone)]
+ pub enum TsFeature {
+ Highlight,
+ TextObjects,
+ AutoIndent,
+ }
+
+ impl TsFeature {
+ pub fn all() -> &'static [Self] {
+ &[Self::Highlight, Self::TextObjects, Self::AutoIndent]
+ }
+
+ pub fn runtime_filename(&self) -> &'static str {
+ match *self {
+ Self::Highlight => "highlights.scm",
+ Self::TextObjects => "textobjects.scm",
+ Self::AutoIndent => "indents.toml",
+ }
+ }
+ }
+
+ impl Display for TsFeature {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match *self {
+ Self::Highlight => "Syntax Highlighting",
+ Self::TextObjects => "Treesitter Textobjects",
+ Self::AutoIndent => "Auto Indent",
+ }
+ )
+ }
+ }
+
+ /// Get the list of languages that support a particular tree-sitter
+ /// based feature.
+ pub fn ts_lang_support(feat: TsFeature) -> Vec<String> {
+ let queries_dir = path::ts_queries();
+
+ find_files(&queries_dir, feat.runtime_filename())
+ .iter()
+ .map(|f| {
+ // .../helix/runtime/queries/python/highlights.scm
+ let tail = f.strip_prefix(&queries_dir).unwrap(); // python/highlights.scm
+ let lang = tail.components().next().unwrap(); // python
+ lang.as_os_str().to_string_lossy().to_string()
+ })
+ .collect()
+ }
+
+ /// Get the list of languages that have any form of tree-sitter
+ /// queries defined in the runtime directory.
+ pub fn langs_with_ts_queries() -> Vec<String> {
+ std::fs::read_dir(path::ts_queries())
+ .unwrap()
+ .filter_map(|entry| {
+ let entry = entry.ok()?;
+ entry
+ .file_type()
+ .ok()?
+ .is_dir()
+ .then(|| entry.file_name().to_string_lossy().to_string())
+ })
+ .collect()
+ }
+
+ // naive implementation, but suffices for our needs
+ pub fn find_files(dir: &Path, filename: &str) -> Vec<PathBuf> {
+ std::fs::read_dir(dir)
+ .unwrap()
+ .filter_map(|entry| {
+ let path = entry.ok()?.path();
+ if path.is_dir() {
+ Some(find_files(&path, filename))
+ } else {
+ (path.file_name()?.to_string_lossy() == filename).then(|| vec![path])
+ }
+ })
+ .flatten()
+ .collect()
+ }
+
+ pub fn lang_config() -> LangConfig {
+ let bytes = std::fs::read(path::lang_config()).unwrap();
+ toml::from_slice(&bytes).unwrap()
+ }
+}
+
+pub mod md_gen {
+ use crate::DynError;
+
+ use crate::helpers;
+ use crate::path;
+ use std::fs;
+
+ use helix_term::commands::cmd::TYPABLE_COMMAND_LIST;
+
+ pub const TYPABLE_COMMANDS_MD_OUTPUT: &str = "typable-cmd.md";
+ pub const LANG_SUPPORT_MD_OUTPUT: &str = "lang-support.md";
+
+ fn md_table_heading(cols: &[String]) -> String {
+ let mut header = String::new();
+ header += &md_table_row(cols);
+ header += &md_table_row(&vec!["---".to_string(); cols.len()]);
+ header
+ }
+
+ fn md_table_row(cols: &[String]) -> String {
+ format!("| {} |\n", cols.join(" | "))
+ }
+
+ fn md_mono(s: &str) -> String {
+ format!("`{}`", s)
+ }
+
+ pub fn typable_commands() -> Result<String, DynError> {
+ let mut md = String::new();
+ md.push_str(&md_table_heading(&[
+ "Name".to_owned(),
+ "Description".to_owned(),
+ ]));
+
+ let cmdify = |s: &str| format!("`:{}`", s);
+
+ for cmd in TYPABLE_COMMAND_LIST {
+ let names = std::iter::once(&cmd.name)
+ .chain(cmd.aliases.iter())
+ .map(|a| cmdify(a))
+ .collect::<Vec<_>>()
+ .join(", ");
+
+ md.push_str(&md_table_row(&[names.to_owned(), cmd.doc.to_owned()]));
+ }
+
+ Ok(md)
+ }
+
+ pub fn lang_features() -> Result<String, DynError> {
+ let mut md = String::new();
+ let ts_features = helpers::TsFeature::all();
+
+ let mut cols = vec!["Language".to_owned()];
+ cols.append(
+ &mut ts_features
+ .iter()
+ .map(|t| t.to_string())
+ .collect::<Vec<_>>(),
+ );
+ cols.push("Default LSP".to_owned());
+
+ md.push_str(&md_table_heading(&cols));
+ let config = helpers::lang_config();
+
+ let mut langs = config
+ .language
+ .iter()
+ .map(|l| l.language_id.clone())
+ .collect::<Vec<_>>();
+ langs.sort_unstable();
+
+ let mut ts_features_to_langs = Vec::new();
+ for &feat in ts_features {
+ ts_features_to_langs.push((feat, helpers::ts_lang_support(feat)));
+ }
+
+ let mut row = Vec::new();
+ for lang in langs {
+ let lc = config
+ .language
+ .iter()
+ .find(|l| l.language_id == lang)
+ .unwrap(); // lang comes from config
+ row.push(lc.language_id.clone());
+
+ for (_feat, support_list) in &ts_features_to_langs {
+ row.push(
+ if support_list.contains(&lang) {
+ "✓"
+ } else {
+ ""
+ }
+ .to_owned(),
+ );
+ }
+ row.push(
+ lc.language_server
+ .as_ref()
+ .map(|s| s.command.clone())
+ .map(|c| md_mono(&c))
+ .unwrap_or_default(),
+ );
+
+ md.push_str(&md_table_row(&row));
+ row.clear();
+ }
+
+ Ok(md)
+ }
+
+ pub fn write(filename: &str, data: &str) {
+ let error = format!("Could not write to {}", filename);
+ let path = path::book_gen().join(filename);
+ fs::write(path, data).expect(&error);
+ }
+}
+
+pub mod path {
+ use std::path::{Path, PathBuf};
+
+ pub fn project_root() -> PathBuf {
+ Path::new(env!("CARGO_MANIFEST_DIR"))
+ .parent()
+ .unwrap()
+ .to_path_buf()
+ }
+
+ pub fn book_gen() -> PathBuf {
+ project_root().join("book/src/generated/")
+ }
+
+ pub fn ts_queries() -> PathBuf {
+ project_root().join("runtime/queries")
+ }
+
+ pub fn lang_config() -> PathBuf {
+ project_root().join("languages.toml")
+ }
+}
+
+pub mod tasks {
+ use crate::md_gen;
+ use crate::DynError;
+
+ pub fn docgen() -> Result<(), DynError> {
+ use md_gen::*;
+ write(TYPABLE_COMMANDS_MD_OUTPUT, &typable_commands()?);
+ write(LANG_SUPPORT_MD_OUTPUT, &lang_features()?);
+ Ok(())
+ }
+
+ pub fn print_help() {
+ println!(
+ "
+Usage: Run with `cargo xtask <task>`, eg. `cargo xtask docgen`.
+
+ Tasks:
+ docgen: Generate files to be included in the mdbook output.
+"
+ );
+ }
+}
+
+fn main() -> Result<(), DynError> {
+ let task = env::args().nth(1);
+ match task {
+ None => tasks::print_help(),
+ Some(t) => match t.as_str() {
+ "docgen" => tasks::docgen()?,
+ invalid => return Err(format!("Invalid task name: {}", invalid).into()),
+ },
+ };
+ Ok(())
+}