Merge branch 'master' into master

pull/9328/head
rojebd 3 weeks ago committed by GitHub
commit f76941d0e8
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194

@ -1,3 +1,17 @@
# we use tokio_unstable to enable runtime::Handle::id so we can separate
# globals from multiple parallel tests. If that function ever does get removed
# its possible to replace (with some additional overhead and effort)
# Annoyingly build.rustflags doesn't work here because it gets overwritten
# if people have their own global target.<..> config (for example to enable mold)
# specifying flags this way is more robust as they get merged
# This still gets overwritten by RUST_FLAGS though, luckily it shouldn't be necessary
# to set those most of the time. If downstream does overwrite this its not a huge
# deal since it will only break tests anyway
[target."cfg(all())"]
rustflags = ["--cfg", "tokio_unstable", "-C", "target-feature=-crt-static"]
[alias] [alias]
xtask = "run --package xtask --" xtask = "run --package xtask --"
integration-test = "test --features integration --profile integration --workspace --test integration" integration-test = "test --features integration --profile integration --workspace --test integration"

@ -7,6 +7,14 @@ updates:
directory: "/" directory: "/"
schedule: schedule:
interval: "weekly" interval: "weekly"
groups:
tree-sitter:
patterns:
- "tree-sitter*"
rust-dependencies:
update-types:
- "minor"
- "patch"
- package-ecosystem: "github-actions" - package-ecosystem: "github-actions"
directory: "/" directory: "/"

@ -6,20 +6,19 @@ on:
- master - master
merge_group: merge_group:
schedule: schedule:
- cron: '00 01 * * *' - cron: "00 01 * * *"
jobs: jobs:
check: check:
name: Check (msrv) name: Check (msrv)
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install stable toolchain - name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1 uses: dtolnay/rust-toolchain@1.70
with:
profile: minimal
override: true
- uses: Swatinem/rust-cache@v2 - uses: Swatinem/rust-cache@v2
with: with:
@ -31,6 +30,7 @@ jobs:
test: test:
name: Test Suite name: Test Suite
runs-on: ${{ matrix.os }} runs-on: ${{ matrix.os }}
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
env: env:
RUST_BACKTRACE: 1 RUST_BACKTRACE: 1
HELIX_LOG_LEVEL: info HELIX_LOG_LEVEL: info
@ -46,7 +46,7 @@ jobs:
shared-key: "build" shared-key: "build"
- name: Cache test tree-sitter grammar - name: Cache test tree-sitter grammar
uses: actions/cache@v3 uses: actions/cache@v4
with: with:
path: runtime/grammars path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }} key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
@ -65,6 +65,7 @@ jobs:
lints: lints:
name: Lints name: Lints
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -92,6 +93,7 @@ jobs:
docs: docs:
name: Docs name: Docs
runs-on: ubuntu-latest runs-on: ubuntu-latest
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v4 uses: actions/checkout@v4
@ -106,6 +108,9 @@ jobs:
- name: Validate queries - name: Validate queries
run: cargo xtask query-check run: cargo xtask query-check
- name: Validate themes
run: cargo xtask theme-check
- name: Generate docs - name: Generate docs
run: cargo xtask docgen run: cargo xtask docgen
@ -115,4 +120,3 @@ jobs:
git diff-files --quiet \ git diff-files --quiet \
|| (echo "Run 'cargo xtask docgen', commit the changes and push again" \ || (echo "Run 'cargo xtask docgen', commit the changes and push again" \
&& exit 1) && exit 1)

@ -14,10 +14,10 @@ jobs:
uses: actions/checkout@v4 uses: actions/checkout@v4
- name: Install nix - name: Install nix
uses: cachix/install-nix-action@v24 uses: cachix/install-nix-action@v30
- name: Authenticate with Cachix - name: Authenticate with Cachix
uses: cachix/cachix-action@v13 uses: cachix/cachix-action@v15
with: with:
name: helix name: helix
authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} authToken: ${{ secrets.CACHIX_AUTH_TOKEN }}

@ -14,7 +14,7 @@ jobs:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
- name: Setup mdBook - name: Setup mdBook
uses: peaceiris/actions-mdbook@v1 uses: peaceiris/actions-mdbook@v2
with: with:
mdbook-version: 'latest' mdbook-version: 'latest'
# mdbook-version: '0.4.8' # mdbook-version: '0.4.8'
@ -27,14 +27,14 @@ jobs:
echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV
- name: Deploy stable - name: Deploy stable
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v4
if: startswith(github.ref, 'refs/tags/') if: startswith(github.ref, 'refs/tags/')
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book publish_dir: ./book/book
- name: Deploy - name: Deploy
uses: peaceiris/actions-gh-pages@v3 uses: peaceiris/actions-gh-pages@v4
with: with:
github_token: ${{ secrets.GITHUB_TOKEN }} github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book publish_dir: ./book/book

@ -1,2 +0,0 @@
# Things that we don't want ripgrep to search that we do want in git
# https://github.com/BurntSushi/ripgrep/blob/master/GUIDE.md#automatic-filtering

@ -1,3 +1,460 @@
# 24.07 (2024-07-14)
Thanks to all of the contributors! This release has changes from 160 contributors.
Breaking changes:
Features:
- Add a textobject for entries/elements of list-like things ([#8150](https://github.com/helix-editor/helix/pull/8150))
- Add a picker showing files changed in VCS ([#5645](https://github.com/helix-editor/helix/pull/5645))
- Use a temporary file for writes ([#9236](https://github.com/helix-editor/helix/pull/9236), [#10339](https://github.com/helix-editor/helix/pull/10339), [#10790](https://github.com/helix-editor/helix/pull/10790))
- Allow cycling through LSP signature-help signatures with `A-n`/`A-p` ([#9974](https://github.com/helix-editor/helix/pull/9974), [#10654](https://github.com/helix-editor/helix/pull/10654), [#10655](https://github.com/helix-editor/helix/pull/10655))
- Use tree-sitter when finding matching brackets and closest pairs ([#8294](https://github.com/helix-editor/helix/pull/8294), [#10613](https://github.com/helix-editor/helix/pull/10613), [#10777](https://github.com/helix-editor/helix/pull/10777))
- Auto-save all buffers after a delay ([#10899](https://github.com/helix-editor/helix/pull/10899), [#11047](https://github.com/helix-editor/helix/pull/11047))
Commands:
- `select_all_siblings` (`A-a`) - select all siblings of each selection ([87c4161](https://github.com/helix-editor/helix/commit/87c4161))
- `select_all_children` (`A-I`) - select all children of each selection ([fa67c5c](https://github.com/helix-editor/helix/commit/fa67c5c))
- `:read` - insert the contents of the given file at each selection ([#10447](https://github.com/helix-editor/helix/pull/10447))
Usability improvements:
- Support scrolling popup contents using the mouse ([#10053](https://github.com/helix-editor/helix/pull/10053))
- Sort the jumplist picker so that most recent items come first ([#10095](https://github.com/helix-editor/helix/pull/10095))
- Improve `goto_file`'s (`gf`) automatic path detection strategy ([#9065](https://github.com/helix-editor/helix/pull/9065))
- Respect language server definition order in code action menu ([#9590](https://github.com/helix-editor/helix/pull/9590))
- Allow using a count with `goto_next_buffer` (`gn`) and `goto_previous_buffer` (`gp`) ([#10463](https://github.com/helix-editor/helix/pull/10463))
- Improve the positioning of popups ([#10257](https://github.com/helix-editor/helix/pull/10257), [#10573](https://github.com/helix-editor/helix/pull/10573))
- Reset all changes overlapped by selections in `:reset-diff-change` ([#10728](https://github.com/helix-editor/helix/pull/10728))
- Await pending writes in the `suspend` command (`C-z`) ([#10797](https://github.com/helix-editor/helix/pull/10797))
- Remove special handling of line ending characters in `replace` (`r`) ([#10786](https://github.com/helix-editor/helix/pull/10786))
- Use the selected register as a history register for `rename_symbol` (`<space>r`) ([#10932](https://github.com/helix-editor/helix/pull/10932))
- Use the configured insert-mode cursor for prompt entry ([#10945](https://github.com/helix-editor/helix/pull/10945))
- Add tilted quotes to the matching brackets list ([#10971](https://github.com/helix-editor/helix/pull/10971))
- Prevent improper files like `/dev/urandom` from being used as file arguments ([#10733](https://github.com/helix-editor/helix/pull/10733))
- Allow multiple language servers to provide `:lsp-workspace-command`s ([#10176](https://github.com/helix-editor/helix/pull/10176), [#11105](https://github.com/helix-editor/helix/pull/11105))
- Trim output of commands executed through `:pipe` ([#10952](https://github.com/helix-editor/helix/pull/10952))
Fixes:
- Use `lldb-dap` instead of `lldb-vscode` in default DAP configuration ([#10091](https://github.com/helix-editor/helix/pull/10091))
- Fix creation of uneven splits when closing windows ([#10004](https://github.com/helix-editor/helix/pull/10004))
- Avoid setting a register in `delete_selection_noyank`, fixing the command's use in command sequences ([#10050](https://github.com/helix-editor/helix/pull/10050), [#10148](https://github.com/helix-editor/helix/pull/10148))
- Fix jump alphabet config resetting when using `:config-reload` ([#10156](https://github.com/helix-editor/helix/pull/10156))
- Overlay LSP unnecessary/deprecated diagnostic tag highlights onto regular diagnostic highlights ([#10084](https://github.com/helix-editor/helix/pull/10084))
- Fix crash on LSP text edits with invalid ranges ([#9649](https://github.com/helix-editor/helix/pull/9649))
- Handle partial failure when sending multiple LSP `textDocument/didSave` notifications ([#10168](https://github.com/helix-editor/helix/pull/10168))
- Fix off-by-one error for completion-replace option ([#10279](https://github.com/helix-editor/helix/pull/10279))
- Fix mouse right-click selection behavior ([#10067](https://github.com/helix-editor/helix/pull/10067))
- Fix scrolling to the end within a popup ([#10181](https://github.com/helix-editor/helix/pull/10181))
- Fix jump label highlight locations when jumping in non-ascii text ([#10317](https://github.com/helix-editor/helix/pull/10317))
- Fix crashes from tree-sitter query captures that return non-grapheme aligned ranges ([#10310](https://github.com/helix-editor/helix/pull/10310))
- Include VCS change in `mi`/`ma` textobject infobox ([#10496](https://github.com/helix-editor/helix/pull/10496))
- Override crossterm's support for `NO_COLOR` ([#10514](https://github.com/helix-editor/helix/pull/10514))
- Respect mode when starting a search ([#10505](https://github.com/helix-editor/helix/pull/10505))
- Simplify first-in-line computation for indent queries ([#10527](https://github.com/helix-editor/helix/pull/10527))
- Ignore .svn version controlled files in file pickers ([#10536](https://github.com/helix-editor/helix/pull/10536))
- Fix overloading language servers with `completionItem/resolve` requests ([38ee845](https://github.com/helix-editor/helix/commit/38ee845), [#10873](https://github.com/helix-editor/helix/pull/10873))
- Specify direction for `select_next_sibling` / `select_prev_sibling` ([#10542](https://github.com/helix-editor/helix/pull/10542))
- Fix restarting language servers ([#10614](https://github.com/helix-editor/helix/pull/10614))
- Don't stop at the first URL in `goto_file` ([#10622](https://github.com/helix-editor/helix/pull/10622))
- Fix overflows in window size calculations for small terminals ([#10620](https://github.com/helix-editor/helix/pull/10620))
- Allow missing or empty completion lists in DAP ([#10332](https://github.com/helix-editor/helix/pull/10332))
- Revert statusline refactor that could cause the statusline to blank out on files with long paths ([#10642](https://github.com/helix-editor/helix/pull/10642))
- Synchronize files after writing ([#10735](https://github.com/helix-editor/helix/pull/10735))
- Avoid `cnorm` for cursor-type detection in certain terminals ([#10769](https://github.com/helix-editor/helix/pull/10769))
- Reset inlay hints when stopping or restarting a language server ([#10741](https://github.com/helix-editor/helix/pull/10741))
- Fix logic for updating `--version` when development VCS HEAD changes ([#10896](https://github.com/helix-editor/helix/pull/10896))
- Set a max value for the count ([#10930](https://github.com/helix-editor/helix/pull/10930))
- Deserialize number IDs in DAP module types ([#10943](https://github.com/helix-editor/helix/pull/10943))
- Fix the behavior of `jump_backwords` when the jumplist is at capacity ([#10968](https://github.com/helix-editor/helix/pull/10968))
- Fix injection layer heritage tracking for reused tree-sitter injection layers ([#1098](https://github.com/helix-editor/helix/pull/1098))
- Fix pluralization of "buffers" in the statusline for `:q`, `:q!`, `:wq` ([#11018](https://github.com/helix-editor/helix/pull/11018))
- Declare LSP formatting client capabilities ([#11064](https://github.com/helix-editor/helix/pull/11064))
- Commit uncommitted changes before attempting undo/earlier ([#11090](https://github.com/helix-editor/helix/pull/11090))
- Expand tilde for selected paths in `goto_file` ([#10964](https://github.com/helix-editor/helix/pull/10964))
- Commit undo checkpoints before `:write[-all]`, fixing the modification indicator ([#11062](https://github.com/helix-editor/helix/pull/11062))
Themes:
- Add jump label styles to `nightfox` ([#10052](https://github.com/helix-editor/helix/pull/10052))
- Add jump label styles to Solarized themes ([#10056](https://github.com/helix-editor/helix/pull/10056))
- Add jump label styles to `cyan_light` ([#10058](https://github.com/helix-editor/helix/pull/10058))
- Add jump label styles to `onelight` ([#10061](https://github.com/helix-editor/helix/pull/10061))
- Add `flexoki-dark` and `flexoki-light` ([#10002](https://github.com/helix-editor/helix/pull/10002))
- Add default theme keys for LSP diagnostics tags to existing themes ([#10064](https://github.com/helix-editor/helix/pull/10064))
- Add jump label styles to base16 themes ([#10076](https://github.com/helix-editor/helix/pull/10076))
- Dim primary selection in `kanagawa` ([#10094](https://github.com/helix-editor/helix/pull/10094), [#10500](https://github.com/helix-editor/helix/pull/10500))
- Add jump label styles to tokyonight themes ([#10106](https://github.com/helix-editor/helix/pull/10106))
- Add jump label styles to papercolor themes ([#10104](https://github.com/helix-editor/helix/pull/10104))
- Add jump label styles to Darcula themes ([#10116](https://github.com/helix-editor/helix/pull/10116))
- Add jump label styles to `autumn` ([#10134](https://github.com/helix-editor/helix/pull/10134))
- Add jump label styles to Ayu themes ([#10133](https://github.com/helix-editor/helix/pull/10133))
- Add jump label styles to `dark_high_contrast` ([#10133](https://github.com/helix-editor/helix/pull/10133))
- Update material themes ([#10290](https://github.com/helix-editor/helix/pull/10290))
- Add jump label styles to `varua` ([#10299](https://github.com/helix-editor/helix/pull/10299))
- Add ruler style to `adwaita-dark` ([#10260](https://github.com/helix-editor/helix/pull/10260))
- Remove `ui.highlight` effects from `solarized_dark` ([#10261](https://github.com/helix-editor/helix/pull/10261))
- Fix statusline color in material themes ([#10308](https://github.com/helix-editor/helix/pull/10308))
- Brighten `nord` selection highlight ([#10307](https://github.com/helix-editor/helix/pull/10307))
- Add inlay-hint styles to monokai themes ([#10334](https://github.com/helix-editor/helix/pull/10334))
- Add bufferline and cursorline colors to `vim_dark_high_contrast` ([#10444](https://github.com/helix-editor/helix/pull/10444))
- Switch themes with foreground rulers to background ([#10309](https://github.com/helix-editor/helix/pull/10309))
- Fix statusline colors for `everblush` ([#10394](https://github.com/helix-editor/helix/pull/10394))
- Use `yellow1` for `gruvbox` warning diagnostics ([#10506](https://github.com/helix-editor/helix/pull/10506))
- Add jump label styles to Modus themes ([#10538](https://github.com/helix-editor/helix/pull/10538))
- Refactor `dark_plus` and switch maintainers ([#10543](https://github.com/helix-editor/helix/pull/10543), [#10574](https://github.com/helix-editor/helix/pull/10574))
- Add debug highlights to `dark_plus` ([#10593](https://github.com/helix-editor/helix/pull/10593))
- Fix per-mode cursor colors in the default theme ([#10608](https://github.com/helix-editor/helix/pull/10608))
- Add `tag` and `attribute` highlights to `dark_high_contrast` ([#10705](https://github.com/helix-editor/helix/pull/10705))
- Improve readability of virtual text with `noctis` theme ([#10910](https://github.com/helix-editor/helix/pull/10910))
- Sync `catppuccin` themes with upstream ([#10954](https://github.com/helix-editor/helix/pull/10954))
- Improve jump colors for `github_dark` themes ([#10946](https://github.com/helix-editor/helix/pull/10946))
- Add modeline and default virtual highlights to `base16_default` ([#10858](https://github.com/helix-editor/helix/pull/10858))
- Add `iroaseta` ([#10381](https://github.com/helix-editor/helix/pull/10381))
- Refactor `gruvbox` ([#10773](https://github.com/helix-editor/helix/pull/10773), [#11071](https://github.com/helix-editor/helix/pull/11071))
- Add cursorcolumn and cursorline to `base16_transparent` ([#11099](https://github.com/helix-editor/helix/pull/11099))
- Update cursorline color for `fleet_dark` ([#11046](https://github.com/helix-editor/helix/pull/11046))
- Add `kanagawa-dragon` ([#10172](https://github.com/helix-editor/helix/pull/10172))
New languages:
- BitBake ([#10010](https://github.com/helix-editor/helix/pull/10010))
- Earthfile ([#10111](https://github.com/helix-editor/helix/pull/10111), [#10489](https://github.com/helix-editor/helix/pull/10489), [#10779](https://github.com/helix-editor/helix/pull/10779))
- TCL ([#9837](https://github.com/helix-editor/helix/pull/9837))
- ADL ([#10029](https://github.com/helix-editor/helix/pull/10029))
- LDIF ([#10330](https://github.com/helix-editor/helix/pull/10330))
- XTC ([#10448](https://github.com/helix-editor/helix/pull/10448))
- Move ([f06a166](https://github.com/helix-editor/helix/commit/f06a166))
- Pest ([#10616](https://github.com/helix-editor/helix/pull/10616))
- GJS/GTS ([#9940](https://github.com/helix-editor/helix/pull/9940))
- Inko ([#10656](https://github.com/helix-editor/helix/pull/10656))
- Mojo ([#10743](https://github.com/helix-editor/helix/pull/10743))
- Elisp ([#10644](https://github.com/helix-editor/helix/pull/10644))
Updated languages and queries:
- Recognize `mkdn` files as markdown ([#10065](https://github.com/helix-editor/helix/pull/10065))
- Add comment injections for Gleam ([#10062](https://github.com/helix-editor/helix/pull/10062))
- Recognize BuildKite commands in YAML injections ([#10090](https://github.com/helix-editor/helix/pull/10090))
- Add F# block comment token configuration ([#10108](https://github.com/helix-editor/helix/pull/10108))
- Update tree-sitter-templ and queries ([#10114](https://github.com/helix-editor/helix/pull/10114))
- Recognize `Tiltfile` as Starlark ([#10072](https://github.com/helix-editor/helix/pull/10072))
- Remove `todo.txt` from files recognized as todotxt ([5fece00](https://github.com/helix-editor/helix/commit/5fece00))
- Highlight `type` keyword in Python from PEP695 ([#10165](https://github.com/helix-editor/helix/pull/10165))
- Update tree-sitter-koka, add language server config ([#10119](https://github.com/helix-editor/helix/pull/10119))
- Recognize node and Python history files ([#10120](https://github.com/helix-editor/helix/pull/10120))
- Recognize more shell files as bash ([#10120](https://github.com/helix-editor/helix/pull/10120))
- Recognize the bun shebang as typescript ([#10120](https://github.com/helix-editor/helix/pull/10120))
- Add a configuration for the angular language server ([#10166](https://github.com/helix-editor/helix/pull/10166))
- Add textobject queries for Solidity ([#10318](https://github.com/helix-editor/helix/pull/10318))
- Recognize `meson.options` as Meson ([#10323](https://github.com/helix-editor/helix/pull/10323))
- Improve Solidity highlighting ([4fc0a4d](https://github.com/helix-editor/helix/commit/4fc0a4d))
- Recognize `_.tpl` files as Helm ([#10344](https://github.com/helix-editor/helix/pull/10344))
- Update tree-sitter-ld and highlights ([#10379](https://github.com/helix-editor/helix/pull/10379))
- Add `lldb-dap` configuration for Odin ([#10175](https://github.com/helix-editor/helix/pull/10175))
- Update tree-sitter-rust ([#10365](https://github.com/helix-editor/helix/pull/10365))
- Update tree-sitter-typst ([#10321](https://github.com/helix-editor/helix/pull/10321))
- Recognize `hyprpaper.conf`, `hypridle.conf` and `hyprlock.conf` as Hyprlang ([#10383](https://github.com/helix-editor/helix/pull/10383))
- Improve HTML highlighting ([#10503](https://github.com/helix-editor/helix/pull/10503))
- Add `rust-script` and `cargo` as shebangs for Rust ([#10484](https://github.com/helix-editor/helix/pull/10484))
- Fix precedence of tag highlights in Svelte ([#10487](https://github.com/helix-editor/helix/pull/10487))
- Update tree-sitter-bash ([#10526](https://github.com/helix-editor/helix/pull/10526))
- Recognize `*.ignore` files as ignore ([#10579](https://github.com/helix-editor/helix/pull/10579))
- Add configuration to enable inlay hints in metals ([#10597](https://github.com/helix-editor/helix/pull/10597))
- Enable highlighting private members in ECMA languages ([#10554](https://github.com/helix-editor/helix/pull/10554))
- Add comment injection to typst queries ([#10628](https://github.com/helix-editor/helix/pull/10628))
- Add textobject queries for Hurl ([#10594](https://github.com/helix-editor/helix/pull/10594))
- Add `try` keyword to Rust ([#10641](https://github.com/helix-editor/helix/pull/10641))
- Add `is not` and `not in` to Python highlights ([#10647](https://github.com/helix-editor/helix/pull/10647))
- Remove ' and ⟨⟩ from Lean autopair configuration ([#10688](https://github.com/helix-editor/helix/pull/10688))
- Match TOML/YAML highlights for JSON keys ([#10676](https://github.com/helix-editor/helix/pull/10676))
- Recognize WORKSPACE files as Starlark ([#10713](https://github.com/helix-editor/helix/pull/10713))
- Switch Odin tree-sitter grammar and highlights ([#10698](https://github.com/helix-editor/helix/pull/10698))
- Update `tree-sitter-slint` ([#10749](https://github.com/helix-editor/helix/pull/10749))
- Add missing operators for Solidity highlights ([#10735](https://github.com/helix-editor/helix/pull/10735))
- Update `tree-sitter-inko` ([#10805](https://github.com/helix-editor/helix/pull/10805))
- Add `py`, `hs`, `rs` and `typ` injection regexes ([#10785](https://github.com/helix-editor/helix/pull/10785))
- Update Swift grammar and queries ([#10802](https://github.com/helix-editor/helix/pull/10802))
- Update Cairo grammar and queries ([#10919](https://github.com/helix-editor/helix/pull/10919), [#11067](https://github.com/helix-editor/helix/pull/11067))
- Update Rust grammar ([#10973](https://github.com/helix-editor/helix/pull/10973))
- Add block comment tokens for typst ([#10955](https://github.com/helix-editor/helix/pull/10955))
- Recognize `jsonl` as JSON ([#11004](https://github.com/helix-editor/helix/pull/11004))
- Add rulers and text-width at 100 columns for Lean language ([#10969](https://github.com/helix-editor/helix/pull/10969))
- Improve VDHL highlights ([#10845](https://github.com/helix-editor/helix/pull/10845))
- Recognize `hsc` as Haskell ([#11074](https://github.com/helix-editor/helix/pull/11074))
- Fix heredoc and `$'<ansi_string>'` highlights in Bash ([#11118](https://github.com/helix-editor/helix/pull/11118))
- Add LSP configuration for `basedpyright` ([#11121](https://github.com/helix-editor/helix/pull/11121))
- Recognize `npmrc` and `.nmprc` files as INI ([#11131](https://github.com/helix-editor/helix/pull/11131))
- Recognize `~/.config/git/ignore` as git-ignore ([#11131](https://github.com/helix-editor/helix/pull/11131))
- Recognize `pdm.lock` and `uv.lock` as TOML ([#11131](https://github.com/helix-editor/helix/pull/11131))
- Recognize `.yml` as well as `.yaml` for Helm chart templates ([#11135](https://github.com/helix-editor/helix/pull/11135))
- Add regex injections for Bash ([#11112](https://github.com/helix-editor/helix/pull/11112))
- Update tree-sitter-todo ([#11097](https://github.com/helix-editor/helix/pull/11097))
Packaging:
- Make `Helix.appdata.xml` spec-compliant ([#10051](https://github.com/helix-editor/helix/pull/10051))
- Expose all flake outputs through flake-compat ([#10673](https://github.com/helix-editor/helix/pull/10673))
- Bump the MSRV to 1.74.0 ([#10714](https://github.com/helix-editor/helix/pull/10714))
- Improve FiSH completions ([#10853](https://github.com/helix-editor/helix/pull/10853))
- Improve ZSH completions ([#10853](https://github.com/helix-editor/helix/pull/10853))
# 24.03 (2024-03-30)
As always, a big thank you to all of the contributors! This release saw changes from 125 contributors.
Breaking changes:
- `suffix` file-types in the `file-types` key in language configuration have been removed ([#8006](https://github.com/helix-editor/helix/pull/8006))
- The `file-types` key in language configuration no longer matches full filenames without a glob pattern ([#8006](https://github.com/helix-editor/helix/pull/8006))
Features:
- Open URLs with the `goto_file` command ([#5820](https://github.com/helix-editor/helix/pull/5820))
- Support drawing a border around popups and menus ([#4313](https://github.com/helix-editor/helix/pull/4313), [#9508](https://github.com/helix-editor/helix/pull/9508))
- Track long lived diagnostic sources like Clippy or `rustc` ([#6447](https://github.com/helix-editor/helix/pull/6447), [#9280](https://github.com/helix-editor/helix/pull/9280))
- This improves the handling of diagnostics from sources that only update the diagnostic positions on save.
- Add support for LSP `window/showDocument` requests ([#8865](https://github.com/helix-editor/helix/pull/8865))
- Refactor ad-hoc hooks to use a new generic event system ([#8021](https://github.com/helix-editor/helix/pull/8021), [#9668](https://github.com/helix-editor/helix/pull/9668), [#9660](https://github.com/helix-editor/helix/pull/9660))
- This improves the behavior of autocompletions. For example navigating in insert mode no longer automatically triggers completions.
- Allow using globs in the language configuration `file-types` key ([#8006](https://github.com/helix-editor/helix/pull/8006))
- Allow specifying required roots for situational LSP activation ([#8696](https://github.com/helix-editor/helix/pull/8696))
- Extend selections using mouse clicks in select mode ([#5436](https://github.com/helix-editor/helix/pull/5436))
- Toggle block comments ([#4718](https://github.com/helix-editor/helix/pull/4718), [#9894](https://github.com/helix-editor/helix/pull/9894))
- Support LSP diagnostic tags ([#9780](https://github.com/helix-editor/helix/pull/9780))
- Add a `file-absolute-path` statusline element ([#4535](https://github.com/helix-editor/helix/pull/4535))
- Cross injection layers in tree-sitter motions (`A-p`/`A-o`/`A-i`/`A-n`) ([#5176](https://github.com/helix-editor/helix/pull/5176))
- Add a Amp-editor-like jumping command ([#8875](https://github.com/helix-editor/helix/pull/8875))
Commands:
- `:move` - move buffers with LSP support ([#8584](https://github.com/helix-editor/helix/pull/8584))
- Also see [#8949](https://github.com/helix-editor/helix/pull/8949) which made path changes conform to the LSP spec and fixed the behavior of this command.
- `page_cursor_up`, `page_cursor_down`, `page_cursor_half_up`, `page_cursor_half_down` - commands for scrolling the cursor and page together ([#8015](https://github.com/helix-editor/helix/pull/8015))
- `:yank-diagnostic` - yank the diagnostic(s) under the primary cursor ([#9640](https://github.com/helix-editor/helix/pull/9640))
- `select_line_above` / `select_line_below` - extend or shrink a selection based on the direction and anchor ([#9080](https://github.com/helix-editor/helix/pull/9080))
Usability improvements:
- Make `roots` key of `[[language]]` entries in `languages.toml` configuration optional ([#8803](https://github.com/helix-editor/helix/pull/8803))
- Exit select mode in commands that modify the buffer ([#8689](https://github.com/helix-editor/helix/pull/8689))
- Use crossterm cursor when out of focus ([#6858](https://github.com/helix-editor/helix/pull/6858), [#8934](https://github.com/helix-editor/helix/pull/8934))
- Join empty lines with only one space in `join_selections` ([#8989](https://github.com/helix-editor/helix/pull/8989))
- Introduce a hybrid tree-sitter and contextual indentation heuristic ([#8307](https://github.com/helix-editor/helix/pull/8307))
- Allow configuring the indentation heuristic ([#8307](https://github.com/helix-editor/helix/pull/8307))
- Check for LSP rename support before showing rename prompt ([#9277](https://github.com/helix-editor/helix/pull/9277))
- Normalize `S-<lower-ascii>` keymaps to uppercase ascii ([#9213](https://github.com/helix-editor/helix/pull/9213))
- Add formatter status to `--health` output ([#7986](https://github.com/helix-editor/helix/pull/7986))
- Change path normalization strategy to not resolve symlinks ([#9330](https://github.com/helix-editor/helix/pull/9330))
- Select subtree within injections in `:tree-sitter-subtree` ([#9309](https://github.com/helix-editor/helix/pull/9309))
- Use tilde expansion and normalization for `$HELIX_RUNTIME` paths ([1bc7aac](https://github.com/helix-editor/helix/commit/1bc7aac))
- Improve failure message for LSP goto references ([#9382](https://github.com/helix-editor/helix/pull/9382))
- Use injection syntax trees for bracket matching ([5e0b3cc](https://github.com/helix-editor/helix/commit/5e0b3cc))
- Respect injections in `:tree-sitter-highlight-name` ([8b6565c](https://github.com/helix-editor/helix/commit/8b6565c))
- Respect injections in `move_parent_node_end` ([035b8ea](https://github.com/helix-editor/helix/commit/035b8ea))
- Use `gix` pipeline filter instead of manual CRLF implementation ([#9503](https://github.com/helix-editor/helix/pull/9503))
- Follow Neovim's truecolor detection ([#9577](https://github.com/helix-editor/helix/pull/9577))
- Reload language configuration with `:reload`, SIGHUP ([#9415](https://github.com/helix-editor/helix/pull/9415))
- Allow numbers as bindings ([#8471](https://github.com/helix-editor/helix/pull/8471), [#9887](https://github.com/helix-editor/helix/pull/9887))
- Respect undercurl config when terminfo is not available ([#9897](https://github.com/helix-editor/helix/pull/9897))
- Ignore `.pijul`, `.hg`, `.jj` in addition to `.git` in file pickers configured to show hidden files ([#9935](https://github.com/helix-editor/helix/pull/9935))
- Add completion for registers to `:clear-register` and `:yank-diagnostic` ([#9936](https://github.com/helix-editor/helix/pull/9936))
- Repeat last motion for goto next/prev diagnostic ([#9966](https://github.com/helix-editor/helix/pull/9966))
- Allow configuring a character to use when rendering narrow no-breaking space ([#9604](https://github.com/helix-editor/helix/pull/9604))
- Switch to a streaming regex engine (regex-cursor crate) to significantly speed up regex-based commands and features ([#9422](https://github.com/helix-editor/helix/pull/9422), [#9756](https://github.com/helix-editor/helix/pull/9756), [#9891](https://github.com/helix-editor/helix/pull/9891))
Fixes:
- Swap `*` and `+` registers ([#8703](https://github.com/helix-editor/helix/pull/8703), [#8708](https://github.com/helix-editor/helix/pull/8708))
- Use terminfo to reset terminal cursor style ([#8591](https://github.com/helix-editor/helix/pull/8591))
- Fix precedence of `@align` captures in indentat computation ([#8659](https://github.com/helix-editor/helix/pull/8659))
- Only render the preview if a Picker has a preview function ([#8667](https://github.com/helix-editor/helix/pull/8667))
- Fix the precedence of `ui.virtual.whitespace` ([#8750](https://github.com/helix-editor/helix/pull/8750), [#8879](https://github.com/helix-editor/helix/pull/8879))
- Fix crash in `:indent-style` ([#9087](https://github.com/helix-editor/helix/pull/9087))
- Fix `didSave` text inclusion when sync capability is a kind variant ([#9101](https://github.com/helix-editor/helix/pull/9101))
- Update the history of newly focused views ([#9271](https://github.com/helix-editor/helix/pull/9271))
- Initialize diagnostics when opening a document ([#8873](https://github.com/helix-editor/helix/pull/8873))
- Sync views when applying edits to unfocused views ([#9173](https://github.com/helix-editor/helix/pull/9173))
- This fixes crashes that could occur from LSP workspace edits or `:write-all`.
- Treat non-numeric `+arg`s passed in the CLI args as filenames ([#9333](https://github.com/helix-editor/helix/pull/9333))
- Fix crash when using `mm` on an empty plaintext file ([2fb7e50](https://github.com/helix-editor/helix/commit/2fb7e50))
- Ignore empty tree-sitter nodes in match bracket ([445f7a2](https://github.com/helix-editor/helix/commit/445f7a2))
- Exit a language server if it sends a message with invalid JSON ([#9332](https://github.com/helix-editor/helix/pull/9332))
- Handle failures to enable bracketed paste ([#9353](https://github.com/helix-editor/helix/pull/9353))
- Gate all captures in a pattern behind `#is-not? local` predicates ([#9390](https://github.com/helix-editor/helix/pull/9390))
- Make path changes LSP spec conformant ([#8949](https://github.com/helix-editor/helix/pull/8949))
- Use range positions to determine `insert_newline` motion ([#9448](https://github.com/helix-editor/helix/pull/9448))
- Fix division by zero when prompt completion area is too small ([#9524](https://github.com/helix-editor/helix/pull/9524))
- Add changes to history in clipboard replacement typable commands ([#9625](https://github.com/helix-editor/helix/pull/9625))
- Fix a crash in DAP with an unspecified `line` in breakpoints ([#9632](https://github.com/helix-editor/helix/pull/9632))
- Fix space handling for filenames in bash completion ([#9702](https://github.com/helix-editor/helix/pull/9702), [#9708](https://github.com/helix-editor/helix/pull/9708))
- Key diagnostics off of paths instead of LSP URIs ([#7367](https://github.com/helix-editor/helix/pull/7367))
- Fix panic when using `join_selections_space` ([#9783](https://github.com/helix-editor/helix/pull/9783))
- Fix panic when using `surround_replace`, `surround_delete` ([#9796](https://github.com/helix-editor/helix/pull/9796))
- Fix panic in `surround_replace`, `surround_delete` with nested surrounds and multiple cursors ([#9815](https://github.com/helix-editor/helix/pull/9815))
- Fix panic in `select_textobject_around` ([#9832](https://github.com/helix-editor/helix/pull/9832))
- Don't stop reloading documents when reloading fails in `:reload-all` ([#9870](https://github.com/helix-editor/helix/pull/9870))
- Prevent `shell_keep_pipe` from stopping on nonzero exit status codes ([#9817](https://github.com/helix-editor/helix/pull/9817))
Themes:
- Add `gruber-dark` ([#8598](https://github.com/helix-editor/helix/pull/8598))
- Update `everblush` ([#8705](https://github.com/helix-editor/helix/pull/8705))
- Update `papercolor` ([#8718](https://github.com/helix-editor/helix/pull/8718), [#8827](https://github.com/helix-editor/helix/pull/8827))
- Add `polmandres` ([#8759](https://github.com/helix-editor/helix/pull/8759))
- Add `starlight` ([#8787](https://github.com/helix-editor/helix/pull/8787))
- Update `naysayer` ([#8838](https://github.com/helix-editor/helix/pull/8838))
- Add modus operandi themes ([#8728](https://github.com/helix-editor/helix/pull/8728), [#9912](https://github.com/helix-editor/helix/pull/9912))
- Update `rose_pine` ([#8946](https://github.com/helix-editor/helix/pull/8946))
- Update `darcula` ([#8738](https://github.com/helix-editor/helix/pull/8738), [#9002](https://github.com/helix-editor/helix/pull/9002), [#9449](https://github.com/helix-editor/helix/pull/9449), [#9588](https://github.com/helix-editor/helix/pull/9588))
- Add modus vivendi themes ([#8894](https://github.com/helix-editor/helix/pull/8894), [#9912](https://github.com/helix-editor/helix/pull/9912))
- Add `horizon-dark` ([#9008](https://github.com/helix-editor/helix/pull/9008), [#9493](https://github.com/helix-editor/helix/pull/9493))
- Update `noctis` ([#9123](https://github.com/helix-editor/helix/pull/9123))
- Update `nord` ([#9135](https://github.com/helix-editor/helix/pull/9135))
- Update monokai pro themes ([#9148](https://github.com/helix-editor/helix/pull/9148))
- Update tokyonight themes ([#9099](https://github.com/helix-editor/helix/pull/9099), [#9724](https://github.com/helix-editor/helix/pull/9724), [#9789](https://github.com/helix-editor/helix/pull/9789))
- Add `ttox` ([#8524](https://github.com/helix-editor/helix/pull/8524))
- Add `voxed` ([#9164](https://github.com/helix-editor/helix/pull/9164))
- Update `sonokai` ([#9370](https://github.com/helix-editor/helix/pull/9370), [#9376](https://github.com/helix-editor/helix/pull/9376), [#5379](https://github.com/helix-editor/helix/pull/5379))
- Update `onedark`, `onedarker` ([#9397](https://github.com/helix-editor/helix/pull/9397))
- Update `cyan_light` ([#9375](https://github.com/helix-editor/helix/pull/9375), [#9688](https://github.com/helix-editor/helix/pull/9688))
- Add `gruvbox_light_soft`, `gruvbox_light_hard` ([#9266](https://github.com/helix-editor/helix/pull/9266))
- Update GitHub themes ([#9487](https://github.com/helix-editor/helix/pull/9487))
- Add `term16_dark`, `term16_light` ([#9477](https://github.com/helix-editor/helix/pull/9477))
- Update Zed themes ([#9544](https://github.com/helix-editor/helix/pull/9544), [#9549](https://github.com/helix-editor/helix/pull/9549))
- Add `curzon` ([#9553](https://github.com/helix-editor/helix/pull/9553))
- Add `monokai_soda` ([#9651](https://github.com/helix-editor/helix/pull/9651))
- Update catppuccin themes ([#9859](https://github.com/helix-editor/helix/pull/9859))
- Update `rasmus` ([#9939](https://github.com/helix-editor/helix/pull/9939))
- Update `dark_plus` ([#9949](https://github.com/helix-editor/helix/pull/9949), [628dcd5](https://github.com/helix-editor/helix/commit/628dcd5))
- Update gruvbox themes ([#9960](https://github.com/helix-editor/helix/pull/9960))
- Add jump label theming to `dracula` ([#9973](https://github.com/helix-editor/helix/pull/9973))
- Add jump label theming to `horizon-dark` ([#9984](https://github.com/helix-editor/helix/pull/9984))
- Add jump label theming to catppuccin themes ([2178adf](https://github.com/helix-editor/helix/commit/2178adf), [#9983](https://github.com/helix-editor/helix/pull/9983))
- Add jump label theming to `onedark` themes ([da2dec1](https://github.com/helix-editor/helix/commit/da2dec1))
- Add jump label theming to rose-pine themes ([#9981](https://github.com/helix-editor/helix/pull/9981))
- Add jump label theming to Nord themes ([#10008](https://github.com/helix-editor/helix/pull/10008))
- Add jump label theming to Monokai themes ([#10009](https://github.com/helix-editor/helix/pull/10009))
- Add jump label theming to gruvbox themes ([#10012](https://github.com/helix-editor/helix/pull/10012))
- Add jump label theming to `kanagawa` ([#10030](https://github.com/helix-editor/helix/pull/10030))
- Update material themes ([#10043](https://github.com/helix-editor/helix/pull/10043))
- Add `jetbrains_dark` ([#9967](https://github.com/helix-editor/helix/pull/9967))
New languages:
- Typst ([#7474](https://github.com/helix-editor/helix/pull/7474))
- LPF ([#8536](https://github.com/helix-editor/helix/pull/8536))
- GN ([#6969](https://github.com/helix-editor/helix/pull/6969))
- DBML ([#8860](https://github.com/helix-editor/helix/pull/8860))
- log ([#8916](https://github.com/helix-editor/helix/pull/8916))
- Janet ([#9081](https://github.com/helix-editor/helix/pull/9081), [#9247](https://github.com/helix-editor/helix/pull/9247))
- Agda ([#8285](https://github.com/helix-editor/helix/pull/8285))
- Avro ([#9113](https://github.com/helix-editor/helix/pull/9113))
- Smali ([#9089](https://github.com/helix-editor/helix/pull/9089))
- HOCON ([#9203](https://github.com/helix-editor/helix/pull/9203))
- Tact ([#9512](https://github.com/helix-editor/helix/pull/9512))
- PKL ([#9515](https://github.com/helix-editor/helix/pull/9515))
- CEL ([#9296](https://github.com/helix-editor/helix/pull/9296))
- SpiceDB ([#9296](https://github.com/helix-editor/helix/pull/9296))
- Hoon ([#9190](https://github.com/helix-editor/helix/pull/9190))
- DockerCompose ([#9661](https://github.com/helix-editor/helix/pull/9661), [#9916](https://github.com/helix-editor/helix/pull/9916))
- Groovy ([#9350](https://github.com/helix-editor/helix/pull/9350), [#9681](https://github.com/helix-editor/helix/pull/9681), [#9677](https://github.com/helix-editor/helix/pull/9677))
- FIDL ([#9713](https://github.com/helix-editor/helix/pull/9713))
- Powershell ([#9827](https://github.com/helix-editor/helix/pull/9827))
- ld ([#9835](https://github.com/helix-editor/helix/pull/9835))
- Hyperland config ([#9899](https://github.com/helix-editor/helix/pull/9899))
- JSONC ([#9906](https://github.com/helix-editor/helix/pull/9906))
- PHP Blade ([#9513](https://github.com/helix-editor/helix/pull/9513))
- SuperCollider ([#9329](https://github.com/helix-editor/helix/pull/9329))
- Koka ([#8727](https://github.com/helix-editor/helix/pull/8727))
- PKGBUILD ([#9909](https://github.com/helix-editor/helix/pull/9909), [#9943](https://github.com/helix-editor/helix/pull/9943))
- Ada ([#9908](https://github.com/helix-editor/helix/pull/9908))
- Helm charts ([#9900](https://github.com/helix-editor/helix/pull/9900))
- Ember.js templates ([#9902](https://github.com/helix-editor/helix/pull/9902))
- Ohm ([#9991](https://github.com/helix-editor/helix/pull/9991))
Updated languages and queries:
- Add HTML injection queries for Rust ([#8603](https://github.com/helix-editor/helix/pull/8603))
- Switch to tree-sitter-ron for RON ([#8624](https://github.com/helix-editor/helix/pull/8624))
- Update and improve comment highlighting ([#8564](https://github.com/helix-editor/helix/pull/8564), [#9253](https://github.com/helix-editor/helix/pull/9253), [#9800](https://github.com/helix-editor/helix/pull/9800), [#10014](https://github.com/helix-editor/helix/pull/10014))
- Highlight type parameters in Rust ([#8660](https://github.com/helix-editor/helix/pull/8660))
- Change KDL tree-sitter parsers ([#8652](https://github.com/helix-editor/helix/pull/8652))
- Update tree-sitter-markdown ([#8721](https://github.com/helix-editor/helix/pull/8721), [#10039](https://github.com/helix-editor/helix/pull/10039))
- Update tree-sitter-purescript ([#8712](https://github.com/helix-editor/helix/pull/8712))
- Add type parameter highlighting to TypeScript, Go, Haskell, OCaml and Kotlin ([#8718](https://github.com/helix-editor/helix/pull/8718))
- Add indentation queries for Scheme and lisps using tree-sitter-scheme ([#8720](https://github.com/helix-editor/helix/pull/8720))
- Recognize `meson_options.txt` as Meson ([#8794](https://github.com/helix-editor/helix/pull/8794))
- Add language server configuration for Nushell ([#8878](https://github.com/helix-editor/helix/pull/8878))
- Recognize `musicxml` as XML ([#8935](https://github.com/helix-editor/helix/pull/8935))
- Update tree-sitter-rescript ([#8962](https://github.com/helix-editor/helix/pull/8962))
- Update tree-sitter-python ([#8976](https://github.com/helix-editor/helix/pull/8976))
- Recognize `.envrc.local` and `.envrc.private` as env ([#8988](https://github.com/helix-editor/helix/pull/8988))
- Update tree-sitter-gleam ([#9003](https://github.com/helix-editor/helix/pull/9003), [9ceeea5](https://github.com/helix-editor/helix/commit/9ceeea5))
- Update tree-sitter-d ([#9021](https://github.com/helix-editor/helix/pull/9021))
- Fix R-markdown language name for LSP detection ([#9012](https://github.com/helix-editor/helix/pull/9012))
- Add haskell-language-server LSP configuration ([#9111](https://github.com/helix-editor/helix/pull/9111))
- Recognize `glif` as XML ([#9130](https://github.com/helix-editor/helix/pull/9130))
- Recognize `.prettierrc` as JSON ([#9214](https://github.com/helix-editor/helix/pull/9214))
- Add auto-pairs configuration for scheme ([#9232](https://github.com/helix-editor/helix/pull/9232))
- Add textobject queries for Scala ([#9191](https://github.com/helix-editor/helix/pull/9191))
- Add textobject queries for Protobuf ([#9184](https://github.com/helix-editor/helix/pull/9184))
- Update tree-sitter-wren ([#8544](https://github.com/helix-editor/helix/pull/8544))
- Add `spago.yaml` as an LSP root for PureScript ([#9362](https://github.com/helix-editor/helix/pull/9362))
- Improve highlight and indent queries for Bash, Make and CSS ([#9393](https://github.com/helix-editor/helix/pull/9393))
- Update tree-sitter-scala ([#9348](https://github.com/helix-editor/helix/pull/9348), [#9340](https://github.com/helix-editor/helix/pull/9340), [#9475](https://github.com/helix-editor/helix/pull/9475))
- Recognize `.bash_history` as Bash ([#9401](https://github.com/helix-editor/helix/pull/9401))
- Recognize Helix ignore files as ignore ([#9447](https://github.com/helix-editor/helix/pull/9447))
- Inject SQL into Scala SQL strings ([#9428](https://github.com/helix-editor/helix/pull/9428))
- Update gdscript textobjects ([#9288](https://github.com/helix-editor/helix/pull/9288))
- Update Go queries ([#9510](https://github.com/helix-editor/helix/pull/9510), [#9525](https://github.com/helix-editor/helix/pull/9525))
- Update tree-sitter-nushell ([#9502](https://github.com/helix-editor/helix/pull/9502))
- Update tree-sitter-unison, add indent queries ([#9505](https://github.com/helix-editor/helix/pull/9505))
- Update tree-sitter-slint ([#9551](https://github.com/helix-editor/helix/pull/9551), [#9698](https://github.com/helix-editor/helix/pull/9698))
- Update tree-sitter-swift ([#9586](https://github.com/helix-editor/helix/pull/9586))
- Add `fish_indent` as formatter for fish ([78ed3ad](https://github.com/helix-editor/helix/commit/78ed3ad))
- Recognize `zon` as Zig ([#9582](https://github.com/helix-editor/helix/pull/9582))
- Add a formatter for Odin ([#9537](https://github.com/helix-editor/helix/pull/9537))
- Update tree-sitter-erlang ([#9627](https://github.com/helix-editor/helix/pull/9627), [fdcd461](https://github.com/helix-editor/helix/commit/fdcd461))
- Capture Rust fields as argument textobjects ([#9637](https://github.com/helix-editor/helix/pull/9637))
- Improve Dart textobjects ([#9644](https://github.com/helix-editor/helix/pull/9644))
- Recognize `tmux.conf` as a bash file-type ([#9653](https://github.com/helix-editor/helix/pull/9653))
- Add textobjects queries for Nix ([#9659](https://github.com/helix-editor/helix/pull/9659))
- Add textobjects queries for HCL ([#9658](https://github.com/helix-editor/helix/pull/9658))
- Recognize osm and osc extensions as XML ([#9697](https://github.com/helix-editor/helix/pull/9697))
- Update tree-sitter-sql ([#9634](https://github.com/helix-editor/helix/pull/9634))
- Recognize pde Processing files as Java ([#9741](https://github.com/helix-editor/helix/pull/9741))
- Update tree-sitter-lua ([#9727](https://github.com/helix-editor/helix/pull/9727))
- Switch tree-sitter-nim parsers ([#9722](https://github.com/helix-editor/helix/pull/9722))
- Recognize GTK builder ui files as XML ([#9754](https://github.com/helix-editor/helix/pull/9754))
- Add configuration for markdown-oxide language server ([#9758](https://github.com/helix-editor/helix/pull/9758))
- Add a shebang for elvish ([#9779](https://github.com/helix-editor/helix/pull/9779))
- Fix precedence of Svelte TypeScript injection ([#9777](https://github.com/helix-editor/helix/pull/9777))
- Recognize common Dockerfile file types ([#9772](https://github.com/helix-editor/helix/pull/9772))
- Recognize NUON files as Nu ([#9839](https://github.com/helix-editor/helix/pull/9839))
- Add textobjects for Java native functions and constructors ([#9806](https://github.com/helix-editor/helix/pull/9806))
- Fix "braket" typeo in JSX highlights ([#9910](https://github.com/helix-editor/helix/pull/9910))
- Update tree-sitter-hurl ([#9775](https://github.com/helix-editor/helix/pull/9775))
- Add textobjects queries for Vala ([#8541](https://github.com/helix-editor/helix/pull/8541))
- Update tree-sitter-git-config ([9610254](https://github.com/helix-editor/helix/commit/9610254))
- Recognize 'mmd' as Mermaid ([459eb9a](https://github.com/helix-editor/helix/commit/459eb9a))
- Highlight Rust extern crate aliases ([c099dde](https://github.com/helix-editor/helix/commit/c099dde))
- Improve parameter highlighting in C++ ([f5d95de](https://github.com/helix-editor/helix/commit/f5d95de))
- Recognize 'rclone.conf' as INI ([#9959](https://github.com/helix-editor/helix/pull/9959))
- Add injections for GraphQL and ERB in Ruby heredocs ([#10036](https://github.com/helix-editor/helix/pull/10036))
- Add `main.odin` to Odin LSP roots ([#9968](https://github.com/helix-editor/helix/pull/9968))
Packaging:
- Allow user overlays in Nix grammars build ([#8749](https://github.com/helix-editor/helix/pull/8749))
- Set Cargo feature resolver to v2 ([#8917](https://github.com/helix-editor/helix/pull/8917))
- Use workspace inheritance for common Cargo metadata ([#8925](https://github.com/helix-editor/helix/pull/8925))
- Remove sourcehut-based tree-sitter grammars from default build ([#9316](https://github.com/helix-editor/helix/pull/9316), [#9326](https://github.com/helix-editor/helix/pull/9326))
- Add icon to Windows executable ([#9104](https://github.com/helix-editor/helix/pull/9104))
# 23.10 (2023-10-24) # 23.10 (2023-10-24)
A big shout out to all the contributors! We had 118 contributors in this release. A big shout out to all the contributors! We had 118 contributors in this release.

1551
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -5,12 +5,14 @@ members = [
"helix-view", "helix-view",
"helix-term", "helix-term",
"helix-tui", "helix-tui",
"helix-lsp-types",
"helix-lsp", "helix-lsp",
"helix-event", "helix-event",
"helix-dap", "helix-dap",
"helix-loader", "helix-loader",
"helix-vcs", "helix-vcs",
"helix-parsec", "helix-parsec",
"helix-stdx",
"xtask", "xtask",
] ]
@ -36,11 +38,13 @@ package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2 package.helix-term.opt-level = 2
[workspace.dependencies] [workspace.dependencies]
tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" } tree-sitter = { version = "0.22" }
nucleo = "0.2.0" nucleo = "0.5.0"
slotmap = "1.0.7"
thiserror = "1.0"
[workspace.package] [workspace.package]
version = "23.10.0" version = "24.7.0"
edition = "2021" edition = "2021"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
categories = ["editor"] categories = ["editor"]

@ -37,8 +37,8 @@ All shortcuts/keymaps can be found [in the documentation on the website](https:/
- Built-in language server support - Built-in language server support
- Smart, incremental syntax highlighting and code editing via tree-sitter - Smart, incremental syntax highlighting and code editing via tree-sitter
It's a terminal-based editor first, but I'd like to explore a custom renderer Although it's primarily a terminal-based editor, I am interested in exploring
(similar to Emacs) in wgpu or skulpin. a custom renderer (similar to Emacs) using wgpu or skulpin.
Note: Only certain languages have indentation definitions at the moment. Check Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.scm`. `runtime/queries/<lang>/` for `indents.scm`.
@ -47,7 +47,7 @@ Note: Only certain languages have indentation definitions at the moment. Check
[Installation documentation](https://docs.helix-editor.com/install.html). [Installation documentation](https://docs.helix-editor.com/install.html).
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) [![Packaging status](https://repology.org/badge/vertical-allrepos/helix-editor.svg?exclude_unsupported=1)](https://repology.org/project/helix-editor/versions)
# Contributing # Contributing

@ -28,6 +28,11 @@
"label" = "magenta" "label" = "magenta"
"namespace" = "magenta" "namespace" = "magenta"
"ui.help" = { fg = "white", bg = "black" } "ui.help" = { fg = "white", bg = "black" }
"ui.statusline.insert" = { fg = "black", bg = "green" }
"ui.statusline.select" = { fg = "black", bg = "blue" }
"ui.virtual" = { fg = "gray", modifiers = ["italic"] }
"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] }
"ui.virtual.ruler" = { bg = "black" }
"markup.heading" = "blue" "markup.heading" = "blue"
"markup.list" = "red" "markup.list" = "red"

@ -3,12 +3,20 @@
[Helix](./title-page.md) [Helix](./title-page.md)
- [Installation](./install.md) - [Installation](./install.md)
- [Package Managers](./package-managers.md)
- [Building from source](./building-from-source.md)
- [Usage](./usage.md) - [Usage](./usage.md)
- [Registers](./registers.md)
- [Surround](./surround.md)
- [Textobjects](./textobjects.md)
- [Syntax aware motions](./syntax-aware-motions.md)
- [Pickers](./pickers.md)
- [Keymap](./keymap.md) - [Keymap](./keymap.md)
- [Commands](./commands.md) - [Commands](./commands.md)
- [Language support](./lang-support.md) - [Language support](./lang-support.md)
- [Migrating from Vim](./from-vim.md) - [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md) - [Configuration](./configuration.md)
- [Editor](./editor.md)
- [Themes](./themes.md) - [Themes](./themes.md)
- [Key remapping](./remapping.md) - [Key remapping](./remapping.md)
- [Languages](./languages.md) - [Languages](./languages.md)

@ -0,0 +1,164 @@
## Building from source
- [Configuring Helix's runtime files](#configuring-helixs-runtime-files)
- [Linux and macOS](#linux-and-macos)
- [Windows](#windows)
- [Multiple runtime directories](#multiple-runtime-directories)
- [Note to packagers](#note-to-packagers)
- [Validating the installation](#validating-the-installation)
- [Configure the desktop shortcut](#configure-the-desktop-shortcut)
Requirements:
Clone the Helix GitHub repository into a directory of your choice. The
examples in this documentation assume installation into either `~/src/` on
Linux and macOS, or `%userprofile%\src\` on Windows.
- The [Rust toolchain](https://www.rust-lang.org/tools/install)
- The [Git version control system](https://git-scm.com/)
- A C++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang
If you are using the `musl-libc` standard library instead of `glibc` the following environment variable must be set during the build to ensure tree-sitter grammars can be loaded correctly:
```sh
RUSTFLAGS="-C target-feature=-crt-static"
```
1. Clone the repository:
```sh
git clone https://github.com/helix-editor/helix
cd helix
```
2. Compile from source:
```sh
cargo install --path helix-term --locked
```
This command will create the `hx` executable and construct the tree-sitter
grammars in the local `runtime` folder.
> 💡 If you do not want to fetch or build grammars, set an environment variable `HELIX_DISABLE_AUTO_GRAMMAR_BUILD`
> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
> grammars with `hx --grammar fetch` and compile them with
> `hx --grammar build`. This will install them in
> the `runtime` directory within the user's helix config directory (more
> [details below](#multiple-runtime-directories)).
### Configuring Helix's runtime files
#### Linux and macOS
The **runtime** directory is one below the Helix source, so either export a
`HELIX_RUNTIME` environment variable to point to that directory and add it to
your `~/.bashrc` or equivalent:
```sh
export HELIX_RUNTIME=~/src/helix/runtime
```
Or, create a symbolic link:
```sh
ln -Ts $PWD/runtime ~/.config/helix/runtime
```
If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again.
#### Windows
Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for
`Edit environment variables for your account`) or use the `setx` command in
Cmd:
```sh
setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
```
> 💡 `%userprofile%` resolves to your user directory like
> `C:\Users\Your-Name\` for example.
Or, create a symlink in `%appdata%\helix\` that links to the source code directory:
| Method | Command |
| ---------- | -------------------------------------------------------------------------------------- |
| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
| Cmd | `cd %appdata%\helix` <br/> `mklink /D runtime "%userprofile%\src\helix\runtime"` |
> 💡 On Windows, creating a symbolic link may require running PowerShell or
> Cmd as an administrator.
#### Multiple runtime directories
When Helix finds multiple runtime directories it will search through them for files in the
following order:
1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
developing and testing helix only).
2. `runtime/` subdirectory of OS-dependent helix user config directory.
3. `$HELIX_RUNTIME`
4. Distribution-specific fallback directory (set at compile time—not run time—
with the `HELIX_DEFAULT_RUNTIME` environment variable)
5. `runtime/` subdirectory of path to Helix executable.
This order also sets the priority for selecting which file will be used if multiple runtime
directories have files with the same name.
#### Note to packagers
If you are making a package of Helix for end users, to provide a good out of
the box experience, you should set the `HELIX_DEFAULT_RUNTIME` environment
variable at build time (before invoking `cargo build`) to a directory which
will store the final runtime files after installation. For example, say you want
to package the runtime into `/usr/lib/helix/runtime`. The rough steps a build
script could follow are:
1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime`
1. `cargo build --profile opt --locked --path helix-term`
1. `cp -r runtime $BUILD_DIR/usr/lib/helix/`
1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx`
This way the resulting `hx` binary will always look for its runtime directory in
`/usr/lib/helix/runtime` if the user has no custom runtime in `~/.config/helix`
or `HELIX_RUNTIME`.
### Validating the installation
To make sure everything is set up as expected you should run the Helix health
check:
```sh
hx --health
```
For more information on the health check results refer to
[Health check](https://github.com/helix-editor/helix/wiki/Healthcheck).
### Configure the desktop shortcut
If your desktop environment supports the
[XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html)
you can configure Helix to show up in the application menu by copying the
provided `.desktop` and icon files to their correct folders:
```sh
cp contrib/Helix.desktop ~/.local/share/applications
cp contrib/helix.png ~/.icons # or ~/.local/share/icons
```
It is recommended to convert the links in the `.desktop` file to absolute paths to avoid potential problems:
```sh
sed -i -e "s|Exec=hx %F|Exec=$(readlink -f ~/.cargo/bin/hx) %F|g" \
-e "s|Icon=helix|Icon=$(readlink -f ~/.icons/helix.png)|g" ~/.local/share/applications/Helix.desktop
```
To use another terminal than the system default, you can modify the `.desktop`
file. For example, to use `kitty`:
```sh
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
```

@ -33,344 +33,3 @@ signal to the Helix process on Unix operating systems, such as by using the comm
Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository. Finally, you can have a `config.toml` local to a project by putting it under a `.helix` directory in your repository.
Its settings will be merged with the configuration directory `config.toml` and the built-in configuration. Its settings will be merged with the configuration directory `config.toml` and the built-in configuration.
## Editor
### `[editor]` Section
| Key | Description | Default |
|--|--|---------|
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling | `5` |
| `mouse` | Enable mouse mode | `true` |
| `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` 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` |
| `cursorline` | Highlight all lines with a cursor | `false` |
| `cursorcolumn` | Highlight all columns with a cursor | `false` |
| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
| `auto-format` | Enable automatic formatting on save | `true` |
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `250` |
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
| `auto-info` | Whether to display info boxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
| `undercurl` | Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative | `false` |
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
### `[editor.statusline]` Section
Allows configuring the statusline at the bottom of the editor.
The configuration distinguishes between three areas of the status line:
`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]`
Statusline elements can be defined as follows:
```toml
[editor.statusline]
left = ["mode", "spinner"]
center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
separator = "│"
mode.normal = "NORMAL"
mode.insert = "INSERT"
mode.select = "SELECT"
```
The `[editor.statusline]` key takes the following sub-keys:
| Key | Description | Default |
| --- | --- | --- |
| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
| `center` | A list of elements aligned to the middle of the statusline | `[]` |
| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
| `separator` | The character used to separate elements in the statusline | `"│"` |
| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` |
The following statusline elements can be configured:
| Key | Description |
| ------ | ----------- |
| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) |
| `spinner` | A progress spinner indicating LSP activity |
| `file-name` | The path/name of the opened file |
| `file-base-name` | The basename of the opened file |
| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `file-line-ending` | The file line endings (CRLF or LF) |
| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written |
| `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections |
| `primary-selection-length` | The number of characters currently in primary selection |
| `position` | The cursor position |
| `position-percentage` | The cursor position as a percentage of the total number of lines |
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |
| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) |
| `version-control` | The current branch name or detached commit hash of the opened workspace |
| `register` | The current selected register |
### `[editor.lsp]` Section
| Key | Description | Default |
| --- | ----------- | ------- |
| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them!
### `[editor.cursor-shape]` Section
Defines the shape of cursor in each mode.
Valid values for these options are `block`, `bar`, `underline`, or `hidden`.
> 💡 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
Set options for file picker and global search. Ignoring a file means it is
not visible in the Helix file picker and global search.
All git related options are only enabled in a git repository.
| Key | Description | Default |
|--|--|---------|
|`hidden` | Enables ignoring hidden files | `true`
|`follow-symlinks` | Follow symlinks instead of ignoring them | `true`
|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `true`
|`parents` | Enables reading ignore files from parent directories | `true`
|`ignore` | Enables reading `.ignore` files | `true`
|`git-ignore` | Enables reading `.gitignore` files | `true`
|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true`
|`git-exclude` | Enables reading `.git/info/exclude` files | `true`
|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`.
Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files.
Additionally, you can use Helix-specific ignore files by creating a local `.helix/ignore` file in the current workspace or a global `ignore` file located in your Helix config directory:
- Linux and Mac: `~/.config/helix/ignore`
- Windows: `%AppData%\helix\ignore`
Example:
```ini
# unignore in file picker and global search
!.github/
!.gitignore
!.gitattributes
```
### `[editor.auto-pairs]` Section
Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
simple boolean value, or a specific mapping of pairs of single characters.
To disable auto-pairs altogether, set `auto-pairs` to `false`:
```toml
[editor]
auto-pairs = false # defaults to `true`
```
The default pairs are <code>(){}[]''""``</code>, but these can be customized by
setting `auto-pairs` to a TOML table:
```toml
[editor.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'
```
Additionally, this setting can be used in a language config. Unless
the editor setting is `false`, this will override the editor config in
documents with this language.
Example `languages.toml` that adds <> and removes ''
```toml
[[language]]
name = "rust"
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'
```
### `[editor.search]` Section
Search specific options.
| Key | Description | Default |
|--|--|---------|
| `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` |
| `wrap-around`| Whether the search should wrap after depleting the matches | `true` |
### `[editor.whitespace]` Section
Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace.
| Key | Description | Default |
|-----|-------------|---------|
| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline` | `"none"` |
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below |
Example
```toml
[editor.whitespace]
render = "all"
# or control each character
[editor.whitespace.render]
space = "all"
tab = "all"
newline = "none"
[editor.whitespace.characters]
space = "·"
nbsp = "⍽"
tab = "→"
newline = "⏎"
tabpad = "·" # Tabs will look like "→···" (depending on tab width)
```
### `[editor.indent-guides]` Section
Options for rendering vertical indent guides.
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
| `skip-levels` | Number of indent levels to skip | `0` |
Example:
```toml
[editor.indent-guides]
render = true
character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽"
skip-levels = 1
```
### `[editor.gutters]` Section
For simplicity, `editor.gutters` accepts an array of gutter types, which will
use default settings for all gutter components.
```toml
[editor]
gutters = ["diff", "diagnostics", "line-numbers", "spacer"]
```
To customize the behavior of gutters, the `[editor.gutters]` section must
be used. This section contains top level settings, as well as settings for
specific gutter components as subsections.
| Key | Description | Default |
| --- | --- | --- |
| `layout` | A vector of gutters to display | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
Example:
```toml
[editor.gutters]
layout = ["diff", "diagnostics", "line-numbers", "spacer"]
```
#### `[editor.gutters.line-numbers]` Section
Options for the line number gutter
| Key | Description | Default |
| --- | --- | --- |
| `min-width` | The minimum number of characters to use | `3` |
Example:
```toml
[editor.gutters.line-numbers]
min-width = 1
```
#### `[editor.gutters.diagnostics]` Section
Currently unused
#### `[editor.gutters.diff]` Section
Currently unused
#### `[editor.gutters.spacer]` Section
Currently unused
### `[editor.soft-wrap]` Section
Options for soft wrapping lines that exceed the view width:
| Key | Description | Default |
| --- | --- | --- |
| `enable` | Whether soft wrapping is enabled. | `false` |
| `max-wrap` | Maximum free space left at the end of the line. | `20` |
| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` |
| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` |
| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` |
Example:
```toml
[editor.soft-wrap]
enable = true
max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it
```
### `[editor.smart-tab]` Section
| Key | Description | Default |
|------------|-------------|---------|
| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |

@ -0,0 +1,435 @@
## Editor
- [`[editor]` Section](#editor-section)
- [`[editor.statusline]` Section](#editorstatusline-section)
- [`[editor.lsp]` Section](#editorlsp-section)
- [`[editor.cursor-shape]` Section](#editorcursor-shape-section)
- [`[editor.file-picker]` Section](#editorfile-picker-section)
- [`[editor.auto-pairs]` Section](#editorauto-pairs-section)
- [`[editor.search]` Section](#editorsearch-section)
- [`[editor.whitespace]` Section](#editorwhitespace-section)
- [`[editor.indent-guides]` Section](#editorindent-guides-section)
- [`[editor.gutters]` Section](#editorgutters-section)
- [`[editor.gutters.line-numbers]` Section](#editorguttersline-numbers-section)
- [`[editor.gutters.diagnostics]` Section](#editorguttersdiagnostics-section)
- [`[editor.gutters.diff]` Section](#editorguttersdiff-section)
- [`[editor.gutters.spacer]` Section](#editorguttersspacer-section)
- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section)
- [`[editor.smart-tab]` Section](#editorsmart-tab-section)
- [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section)
### `[editor]` Section
| Key | Description | Default |
|--|--|---------|
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling | `5` |
| `mouse` | Enable mouse mode | `true` |
| `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` 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` |
| `cursorline` | Highlight all lines with a cursor | `false` |
| `cursorcolumn` | Highlight all columns with a cursor | `false` |
| `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
| `auto-format` | Enable automatic formatting on save | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. | `250` |
| `completion-timeout` | Time in milliseconds after typing a word character before completions are shown, set to 5 for instant. | `250` |
| `preview-completion-insert` | Whether to apply completion item instantly when selected | `true` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `completion-replace` | Set to `true` to make completions always replace the entire word and not just the part before the cursor | `false` |
| `auto-info` | Whether to display info boxes | `true` |
| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` |
| `undercurl` | Set to `true` to override automatic detection of terminal undercurl support in the event of a false negative | `false` |
| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` |
| `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` |
| `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` |
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` |
| `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` |
| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` |
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable"
### `[editor.statusline]` Section
Allows configuring the statusline at the bottom of the editor.
The configuration distinguishes between three areas of the status line:
`[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]`
Statusline elements can be defined as follows:
```toml
[editor.statusline]
left = ["mode", "spinner"]
center = ["file-name"]
right = ["diagnostics", "selections", "position", "file-encoding", "file-line-ending", "file-type"]
separator = "│"
mode.normal = "NORMAL"
mode.insert = "INSERT"
mode.select = "SELECT"
```
The `[editor.statusline]` key takes the following sub-keys:
| Key | Description | Default |
| --- | --- | --- |
| `left` | A list of elements aligned to the left of the statusline | `["mode", "spinner", "file-name", "read-only-indicator", "file-modification-indicator"]` |
| `center` | A list of elements aligned to the middle of the statusline | `[]` |
| `right` | A list of elements aligned to the right of the statusline | `["diagnostics", "selections", "register", "position", "file-encoding"]` |
| `separator` | The character used to separate elements in the statusline | `"│"` |
| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` |
The following statusline elements can be configured:
| Key | Description |
| ------ | ----------- |
| `mode` | The current editor mode (`mode.normal`/`mode.insert`/`mode.select`) |
| `spinner` | A progress spinner indicating LSP activity |
| `file-name` | The path/name of the opened file |
| `file-absolute-path` | The absolute path/name of the opened file |
| `file-base-name` | The basename of the opened file |
| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) |
| `file-encoding` | The encoding of the opened file if it differs from UTF-8 |
| `file-line-ending` | The file line endings (CRLF or LF) |
| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written |
| `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections |
| `primary-selection-length` | The number of characters currently in primary selection |
| `position` | The cursor position |
| `position-percentage` | The cursor position as a percentage of the total number of lines |
| `separator` | The string defined in `editor.statusline.separator` (defaults to `"│"`) |
| `spacer` | Inserts a space between elements (multiple/contiguous spacers may be specified) |
| `version-control` | The current branch name or detached commit hash of the opened workspace |
| `register` | The current selected register |
### `[editor.lsp]` Section
| Key | Description | Default |
| --- | ----------- | ------- |
| `enable` | Enables LSP integration. Setting to false will completely disable language servers regardless of language settings.| `true` |
| `display-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `display-signature-help-docs` | Display docs under signature help popup | `true` |
| `snippets` | Enables snippet completions. Requires a server restart (`:lsp-restart`) to take effect after `:config-reload`/`:set`. | `true` |
| `goto-reference-include-declaration` | Include declaration in the goto references popup. | `true` |
[^1]: By default, a progress spinner is shown in the statusline beside the file path.
[^2]: You may also have to activate them in the LSP config for them to appear, not just in Helix. Inlay hints in Helix are still being improved on and may be a little bit laggy/janky under some circumstances. Please report any bugs you see so we can fix them!
### `[editor.cursor-shape]` Section
Defines the shape of cursor in each mode.
Valid values for these options are `block`, `bar`, `underline`, or `hidden`.
> 💡 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
Set options for file picker and global search. Ignoring a file means it is
not visible in the Helix file picker and global search.
All git related options are only enabled in a git repository.
| Key | Description | Default |
|--|--|---------|
|`hidden` | Enables ignoring hidden files | `true`
|`follow-symlinks` | Follow symlinks instead of ignoring them | `true`
|`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | `true`
|`parents` | Enables reading ignore files from parent directories | `true`
|`ignore` | Enables reading `.ignore` files | `true`
|`git-ignore` | Enables reading `.gitignore` files | `true`
|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludesfile` option | `true`
|`git-exclude` | Enables reading `.git/info/exclude` files | `true`
|`max-depth` | Set with an integer value for maximum depth to recurse | Unset by default
Ignore files can be placed locally as `.ignore` or put in your home directory as `~/.ignore`. They support the usual ignore and negative ignore (unignore) rules used in `.gitignore` files.
Additionally, you can use Helix-specific ignore files by creating a local `.helix/ignore` file in the current workspace or a global `ignore` file located in your Helix config directory:
- Linux and Mac: `~/.config/helix/ignore`
- Windows: `%AppData%\helix\ignore`
Example:
```ini
# unignore in file picker and global search
!.github/
!.gitignore
!.gitattributes
```
### `[editor.auto-pairs]` Section
Enables automatic insertion of pairs to parentheses, brackets, etc. Can be a
simple boolean value, or a specific mapping of pairs of single characters.
To disable auto-pairs altogether, set `auto-pairs` to `false`:
```toml
[editor]
auto-pairs = false # defaults to `true`
```
The default pairs are <code>(){}[]''""``</code>, but these can be customized by
setting `auto-pairs` to a TOML table:
```toml
[editor.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'
```
Additionally, this setting can be used in a language config. Unless
the editor setting is `false`, this will override the editor config in
documents with this language.
Example `languages.toml` that adds `<>` and removes `''`
```toml
[[language]]
name = "rust"
[language.auto-pairs]
'(' = ')'
'{' = '}'
'[' = ']'
'"' = '"'
'`' = '`'
'<' = '>'
```
### `[editor.auto-save]` Section
Control auto save behavior.
| Key | Description | Default |
|--|--|---------|
| `focus-lost` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `after-delay.enable` | Enable automatic saving after `auto-save.after-delay.timeout` milliseconds have passed since last edit. | `false` |
| `after-delay.timeout` | Time in milliseconds since last edit before auto save timer triggers. | `3000` |
### `[editor.search]` Section
Search specific options.
| Key | Description | Default |
|--|--|---------|
| `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` |
| `wrap-around`| Whether the search should wrap after depleting the matches | `true` |
### `[editor.whitespace]` Section
Options for rendering whitespace with visible characters. Use `:set whitespace.render all` to temporarily enable visible whitespace.
| Key | Description | Default |
|-----|-------------|---------|
| `render` | Whether to render whitespace. May either be `all` or `none`, or a table with sub-keys `space`, `nbsp`, `nnbsp`, `tab`, and `newline` | `none` |
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `nnbsp`, `newline` or `tabpad` | See example below |
Example
```toml
[editor.whitespace]
render = "all"
# or control each character
[editor.whitespace.render]
space = "all"
tab = "all"
nbsp = "none"
nnbsp = "none"
newline = "none"
[editor.whitespace.characters]
space = "·"
nbsp = "⍽"
nnbsp = "␣"
tab = "→"
newline = "⏎"
tabpad = "·" # Tabs will look like "→···" (depending on tab width)
```
### `[editor.indent-guides]` Section
Options for rendering vertical indent guides.
| Key | Description | Default |
| --- | --- | --- |
| `render` | Whether to render indent guides | `false` |
| `character` | Literal character to use for rendering the indent guide | `│` |
| `skip-levels` | Number of indent levels to skip | `0` |
Example:
```toml
[editor.indent-guides]
render = true
character = "╎" # Some characters that work well: "▏", "┆", "┊", "⸽"
skip-levels = 1
```
### `[editor.gutters]` Section
For simplicity, `editor.gutters` accepts an array of gutter types, which will
use default settings for all gutter components.
```toml
[editor]
gutters = ["diff", "diagnostics", "line-numbers", "spacer"]
```
To customize the behavior of gutters, the `[editor.gutters]` section must
be used. This section contains top level settings, as well as settings for
specific gutter components as subsections.
| Key | Description | Default |
| --- | --- | --- |
| `layout` | A vector of gutters to display | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` |
Example:
```toml
[editor.gutters]
layout = ["diff", "diagnostics", "line-numbers", "spacer"]
```
#### `[editor.gutters.line-numbers]` Section
Options for the line number gutter
| Key | Description | Default |
| --- | --- | --- |
| `min-width` | The minimum number of characters to use | `3` |
Example:
```toml
[editor.gutters.line-numbers]
min-width = 1
```
#### `[editor.gutters.diagnostics]` Section
Currently unused
#### `[editor.gutters.diff]` Section
The `diff` gutter option displays colored bars indicating whether a `git` diff represents that a line was added, removed or changed.
These colors are controlled by the theme attributes `diff.plus`, `diff.minus` and `diff.delta`.
Other diff providers will eventually be supported by a future plugin system.
There are currently no options for this section.
#### `[editor.gutters.spacer]` Section
Currently unused
### `[editor.soft-wrap]` Section
Options for soft wrapping lines that exceed the view width:
| Key | Description | Default |
| --- | --- | --- |
| `enable` | Whether soft wrapping is enabled. | `false` |
| `max-wrap` | Maximum free space left at the end of the line. | `20` |
| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` |
| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` |
| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` |
Example:
```toml
[editor.soft-wrap]
enable = true
max-wrap = 25 # increase value to reduce forced mid-word wrapping
max-indent-retain = 0
wrap-indicator = "" # set wrap-indicator to "" to hide it
```
### `[editor.smart-tab]` Section
Options for navigating and editing using tab key.
| Key | Description | Default |
|------------|-------------|---------|
| `enable` | If set to true, then when the cursor is in a position with non-whitespace to its left, instead of inserting a tab, it will run `move_parent_node_end`. If there is only whitespace to the left, then it inserts a tab as normal. With the default bindings, to explicitly insert a tab character, press Shift-tab. | `true` |
| `supersede-menu` | Normally, when a menu is on screen, such as when auto complete is triggered, the tab key is bound to cycling through the items. This means when menus are on screen, one cannot use the tab key to trigger the `smart-tab` command. If this option is set to true, the `smart-tab` command always takes precedence, which means one cannot use the tab key to cycle through menu items. One of the other bindings must be used instead, such as arrow keys or `C-n`/`C-p`. | `false` |
Due to lack of support for S-tab in some terminals, the default keybindings don't fully embrace smart-tab editing experience. If you enjoy smart-tab navigation and a terminal that supports the [Enhanced Keyboard protocol](https://github.com/helix-editor/helix/wiki/Terminal-Support#enhanced-keyboard-protocol), consider setting extra keybindings:
```
[keys.normal]
tab = "move_parent_node_end"
S-tab = "move_parent_node_start"
[keys.insert]
S-tab = "move_parent_node_start"
[keys.select]
tab = "extend_parent_node_end"
S-tab = "extend_parent_node_start"
```
### `[editor.inline-diagnostics]` Section
Options for rendering diagnostics inside the text like shown below
```
fn main() {
let foo = bar;
└─ no such value in this scope
}
````
| Key | Description | Default |
|------------|-------------|---------|
| `cursor-line` | The minimum severity that a diagnostic must have to be shown inline on the line that contains the primary cursor. Set to `disable` to not show any diagnostics inline. This option does not have any effect when in insert-mode and will only take effect 350ms after moving the cursor to a different line. | `"disable"` |
| `other-lines` | The minimum severity that a diagnostic must have to be shown inline on a line that does not contain the cursor-line. Set to `disable` to not show any diagnostics inline. | `"disable"` |
| `prefix-len` | How many horizontal bars `─` are rendered before the diagnostic text. | `1` |
| `max-wrap` | Equivalent of the `editor.soft-wrap.max-wrap` option for diagnostics. | `20` |
| `max-diagnostics` | Maximum number of diagnostics to render inline for a given line | `10` |
The (first) diagnostic with the highest severity that is not shown inline is rendered at the end of the line (as long as its severity is higher than the `end-of-line-diagnostics` config option):
```
fn main() {
let baz = 1;
let foo = bar; a local variable with a similar name exists: baz
└─ no such value in this scope
}
```
The new diagnostic rendering is not yet enabled by default. As soon as end of line or inline diagnostics are enabled the old diagnostics rendering is automatically disabled. The recommended default setting are:
```
end-of-line-diagnostics = "hint"
[editor.inline-diagnostics]
cursor-line = "warning" # show warnings and errors on the cursorline inline
```

@ -1,5 +1,7 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP | | Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- | | --- | --- | --- | --- | --- |
| ada | ✓ | ✓ | | `ada_language_server` |
| adl | ✓ | ✓ | ✓ | |
| agda | ✓ | | | | | agda | ✓ | | | |
| astro | ✓ | | | | | astro | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` | | awk | ✓ | ✓ | | `awk-language-server` |
@ -8,12 +10,16 @@
| beancount | ✓ | | | | | beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` | | bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` | | bicep | ✓ | | | `bicep-langserver` |
| bitbake | ✓ | | | `bitbake-language-server` |
| blade | ✓ | | | |
| blueprint | ✓ | | | `blueprint-compiler` | | blueprint | ✓ | | | `blueprint-compiler` |
| c | ✓ | ✓ | ✓ | `clangd` | | c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` | | c-sharp | ✓ | ✓ | | `OmniSharp` |
| cabal | | | | `haskell-language-server-wrapper` | | cabal | | | | `haskell-language-server-wrapper` |
| cairo | ✓ | ✓ | ✓ | `cairo-language-server` | | cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
| capnp | ✓ | | ✓ | | | capnp | ✓ | | ✓ | |
| cel | ✓ | | | |
| circom | ✓ | | | `circom-lsp` |
| clojure | ✓ | | | `clojure-lsp` | | clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` | | cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
| comment | ✓ | | | | | comment | ✓ | | | |
@ -21,27 +27,33 @@
| cpon | ✓ | | ✓ | | | cpon | ✓ | | ✓ | |
| cpp | ✓ | ✓ | ✓ | `clangd` | | cpp | ✓ | ✓ | ✓ | `clangd` |
| crystal | ✓ | ✓ | | `crystalline` | | crystal | ✓ | ✓ | | `crystalline` |
| css | ✓ | | | `vscode-css-language-server` | | css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` | | cue | ✓ | | | `cuelsp` |
| cylc | ✓ | ✓ | ✓ | |
| d | ✓ | ✓ | ✓ | `serve-d` | | d | ✓ | ✓ | ✓ | `serve-d` |
| dart | ✓ | | ✓ | `dart` | | dart | ✓ | | ✓ | `dart` |
| dbml | ✓ | | | | | dbml | ✓ | | | |
| devicetree | ✓ | | | | | devicetree | ✓ | | | |
| dhall | ✓ | ✓ | | `dhall-lsp-server` | | dhall | ✓ | ✓ | | `dhall-lsp-server` |
| diff | ✓ | | | | | diff | ✓ | | | |
| dockerfile | ✓ | | | `docker-langserver` | | docker-compose | ✓ | ✓ | ✓ | `docker-compose-langserver`, `yaml-language-server` |
| dockerfile | ✓ | ✓ | | `docker-langserver` |
| dot | ✓ | | | `dot-language-server` | | dot | ✓ | | | `dot-language-server` |
| dtd | ✓ | | | | | dtd | ✓ | | | |
| dune | ✓ | | | |
| earthfile | ✓ | ✓ | ✓ | `earthlyls` |
| edoc | ✓ | | | | | edoc | ✓ | | | |
| eex | ✓ | | | | | eex | ✓ | | | |
| ejs | ✓ | | | | | ejs | ✓ | | | |
| elisp | ✓ | | | |
| elixir | ✓ | ✓ | ✓ | `elixir-ls` | | elixir | ✓ | ✓ | ✓ | `elixir-ls` |
| elm | ✓ | ✓ | | `elm-language-server` | | elm | ✓ | ✓ | | `elm-language-server` |
| elvish | ✓ | | | `elvish` | | elvish | ✓ | | | `elvish` |
| env | ✓ | | | | | env | ✓ | | | |
| erb | ✓ | | | | | erb | ✓ | | | |
| erlang | ✓ | ✓ | | `erlang_ls` | | erlang | ✓ | ✓ | | `erlang_ls`, `elp` |
| esdl | ✓ | | | | | esdl | ✓ | | | |
| fidl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | | | fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | `forth-lsp` | | forth | ✓ | | | `forth-lsp` |
| fortran | ✓ | | ✓ | `fortls` | | fortran | ✓ | | ✓ | `fortls` |
@ -49,45 +61,61 @@
| gas | ✓ | ✓ | | | | gas | ✓ | ✓ | | |
| gdscript | ✓ | ✓ | ✓ | | | gdscript | ✓ | ✓ | ✓ | |
| gemini | ✓ | | | | | gemini | ✓ | | | |
| gherkin | ✓ | | | |
| git-attributes | ✓ | | | | | git-attributes | ✓ | | | |
| git-commit | ✓ | ✓ | | | | git-commit | ✓ | ✓ | | |
| git-config | ✓ | | | | | git-config | ✓ | | | |
| git-ignore | ✓ | | | | | git-ignore | ✓ | | | |
| git-rebase | ✓ | | | | | git-rebase | ✓ | | | |
| gjs | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| gleam | ✓ | ✓ | | `gleam` | | gleam | ✓ | ✓ | | `gleam` |
| glsl | ✓ | ✓ | ✓ | | | glimmer | ✓ | | | `ember-language-server` |
| glsl | ✓ | ✓ | ✓ | `glsl_analyzer` |
| gn | ✓ | | | | | gn | ✓ | | | |
| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` | | go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
| godot-resource | ✓ | | | | | godot-resource | ✓ | | | |
| gomod | ✓ | | | `gopls` | | gomod | ✓ | | | `gopls` |
| gotmpl | ✓ | | | `gopls` | | gotmpl | ✓ | | | `gopls` |
| gowork | ✓ | | | `gopls` | | gowork | ✓ | | | `gopls` |
| graphql | ✓ | | | `graphql-lsp` | | graphql | ✓ | ✓ | | `graphql-lsp` |
| groovy | ✓ | | | |
| gts | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| hare | ✓ | | | | | hare | ✓ | | | |
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` | | haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | | | haskell-persistent | ✓ | | | |
| hcl | ✓ | | ✓ | `terraform-ls` | | hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` | | heex | ✓ | ✓ | | `elixir-ls` |
| helm | ✓ | | | `helm_ls` |
| hocon | ✓ | ✓ | ✓ | |
| hoon | ✓ | | | |
| hosts | ✓ | | | | | hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server` | | html | ✓ | | | `vscode-html-language-server`, `superhtml` |
| hurl | ✓ | | ✓ | | | hurl | ✓ | ✓ | ✓ | |
| hyprlang | ✓ | | ✓ | |
| idris | | | | `idris2-lsp` | | idris | | | | `idris2-lsp` |
| iex | ✓ | | | | | iex | ✓ | | | |
| ini | ✓ | | | | | ini | ✓ | | | |
| inko | ✓ | ✓ | ✓ | |
| janet | ✓ | | | | | janet | ✓ | | | |
| java | ✓ | ✓ | ✓ | `jdtls` | | java | ✓ | ✓ | ✓ | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` | | javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | | | jinja | ✓ | | | |
| jjdescription | ✓ | | | |
| jq | ✓ | ✓ | | `jq-lsp` |
| jsdoc | ✓ | | | | | jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` | | json | ✓ | | ✓ | `vscode-json-language-server` |
| json5 | ✓ | | | | | json5 | ✓ | | | |
| jsonc | ✓ | | ✓ | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` | | jsonnet | ✓ | | | `jsonnet-language-server` |
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` | | jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| julia | ✓ | ✓ | ✓ | `julia` | | julia | ✓ | ✓ | ✓ | `julia` |
| just | ✓ | ✓ | ✓ | | | just | ✓ | ✓ | ✓ | |
| kdl | ✓ | ✓ | ✓ | | | kdl | ✓ | ✓ | ✓ | |
| koka | ✓ | | ✓ | `koka` |
| kotlin | ✓ | | | `kotlin-language-server` | | kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | ✓ | | `texlab` | | latex | ✓ | ✓ | | `texlab` |
| ld | ✓ | | ✓ | |
| ldif | ✓ | | | |
| lean | ✓ | | | `lean` | | lean | ✓ | | | `lean` |
| ledger | ✓ | | | | | ledger | ✓ | | | |
| llvm | ✓ | ✓ | ✓ | | | llvm | ✓ | ✓ | ✓ | |
@ -96,24 +124,27 @@
| log | ✓ | | | | | log | ✓ | | | |
| lpf | ✓ | | | | | lpf | ✓ | | | |
| lua | ✓ | ✓ | ✓ | `lua-language-server` | | lua | ✓ | ✓ | ✓ | `lua-language-server` |
| make | ✓ | | | | | make | ✓ | | | |
| markdoc | ✓ | | | `markdoc-ls` | | markdoc | ✓ | | | `markdoc-ls` |
| markdown | ✓ | | | `marksman` | | markdown | ✓ | | | `marksman`, `markdown-oxide` |
| markdown.inline | ✓ | | | | | markdown.inline | ✓ | | | |
| matlab | ✓ | ✓ | ✓ | | | matlab | ✓ | ✓ | ✓ | |
| mermaid | ✓ | | | | | mermaid | ✓ | | | |
| meson | ✓ | | ✓ | | | meson | ✓ | | ✓ | `mesonlsp` |
| mint | | | | `mint` | | mint | | | | `mint` |
| mojo | ✓ | ✓ | ✓ | `mojo-lsp-server` |
| move | ✓ | | | |
| msbuild | ✓ | | ✓ | | | msbuild | ✓ | | ✓ | |
| nasm | ✓ | ✓ | | | | nasm | ✓ | ✓ | | |
| nickel | ✓ | | ✓ | `nls` | | nickel | ✓ | | ✓ | `nls` |
| nim | ✓ | ✓ | ✓ | `nimlangserver` | | nim | ✓ | ✓ | ✓ | `nimlangserver` |
| nix | ✓ | | | `nil` | | nix | ✓ | | | `nil`, `nixd` |
| nu | ✓ | | | `nu` | | nu | ✓ | | | `nu` |
| nunjucks | ✓ | | | | | nunjucks | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` | | ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` | | ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | ✓ | `ols` | | odin | ✓ | | ✓ | `ols` |
| ohm | ✓ | ✓ | ✓ | |
| opencl | ✓ | ✓ | ✓ | `clangd` | | opencl | ✓ | ✓ | ✓ | `clangd` |
| openscad | ✓ | | | `openscad-lsp` | | openscad | ✓ | | | `openscad-lsp` |
| org | ✓ | | | | | org | ✓ | | | |
@ -121,16 +152,21 @@
| passwd | ✓ | | | | | passwd | ✓ | | | |
| pem | ✓ | | | | | pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | `perlnavigator` | | perl | ✓ | ✓ | ✓ | `perlnavigator` |
| pest | ✓ | ✓ | ✓ | `pest-language-server` |
| php | ✓ | ✓ | ✓ | `intelephense` | | php | ✓ | ✓ | ✓ | `intelephense` |
| php-only | ✓ | | | |
| pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` |
| pkl | ✓ | | ✓ | |
| po | ✓ | ✓ | | | | po | ✓ | ✓ | | |
| pod | ✓ | | | | | pod | ✓ | | | |
| ponylang | ✓ | ✓ | ✓ | | | ponylang | ✓ | ✓ | ✓ | |
| prisma | ✓ | | | `prisma-language-server` | | powershell | ✓ | | | |
| prisma | ✓ | ✓ | | `prisma-language-server` |
| prolog | | | | `swipl` | | prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | `bufls`, `pb` | | protobuf | ✓ | | ✓ | `bufls`, `pb` |
| prql | ✓ | | | | | prql | ✓ | | | |
| purescript | ✓ | ✓ | | `purescript-language-server` | | purescript | ✓ | ✓ | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `pylsp` | | python | ✓ | ✓ | ✓ | `ruff`, `jedi-language-server`, `pylsp` |
| qml | ✓ | | ✓ | `qmlls` | | qml | ✓ | | ✓ | `qmlls` |
| r | ✓ | | | `R` | | r | ✓ | | | `R` |
| racket | ✓ | | ✓ | `racket` | | racket | ✓ | | ✓ | `racket` |
@ -144,38 +180,45 @@
| ruby | ✓ | ✓ | ✓ | `solargraph` | | ruby | ✓ | ✓ | ✓ | `solargraph` |
| rust | ✓ | ✓ | ✓ | `rust-analyzer` | | rust | ✓ | ✓ | ✓ | `rust-analyzer` |
| sage | ✓ | ✓ | | | | sage | ✓ | ✓ | | |
| scala | ✓ | | ✓ | `metals` | | scala | ✓ | | ✓ | `metals` |
| scheme | ✓ | | ✓ | | | scheme | ✓ | | ✓ | |
| scss | ✓ | | | `vscode-css-language-server` | | scss | ✓ | | | `vscode-css-language-server` |
| slint | ✓ | | ✓ | `slint-lsp` | | slint | ✓ | | ✓ | `slint-lsp` |
| smali | ✓ | | ✓ | | | smali | ✓ | | ✓ | |
| smithy | ✓ | | | `cs` | | smithy | ✓ | | | `cs` |
| sml | ✓ | | | | | sml | ✓ | | | |
| solidity | ✓ | | | `solc` | | snakemake | ✓ | | ✓ | `pylsp` |
| sql | ✓ | | | | | solidity | ✓ | ✓ | | `solc` |
| spicedb | ✓ | | | |
| sql | ✓ | ✓ | | |
| sshclientconfig | ✓ | | | | | sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | | | starlark | ✓ | ✓ | | |
| strace | ✓ | | | | | strace | ✓ | | | |
| supercollider | ✓ | | | |
| svelte | ✓ | | ✓ | `svelteserver` | | svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` | | sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | | | `sourcekit-lsp` | | swift | ✓ | | | `sourcekit-lsp` |
| t32 | ✓ | | | | | t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | | | tablegen | ✓ | ✓ | ✓ | |
| tact | ✓ | ✓ | ✓ | |
| task | ✓ | | | | | task | ✓ | | | |
| tcl | ✓ | | ✓ | |
| templ | ✓ | | | `templ` | | templ | ✓ | | | `templ` |
| tfvars | ✓ | | ✓ | `terraform-ls` | | tfvars | ✓ | | ✓ | `terraform-ls` |
| thrift | ✓ | | | |
| todotxt | ✓ | | | | | todotxt | ✓ | | | |
| toml | ✓ | | | `taplo` | | toml | ✓ | | | `taplo` |
| tsq | ✓ | | | | | tsq | ✓ | | | |
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` | | tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| twig | ✓ | | | | | twig | ✓ | | | |
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` | | typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
| typst | ✓ | | | `typst-lsp` | | typespec | ✓ | ✓ | ✓ | `tsp-server` |
| typst | ✓ | | | `tinymist`, `typst-lsp` |
| ungrammar | ✓ | | | | | ungrammar | ✓ | | | |
| unison | ✓ | | | | | unison | ✓ | | | |
| uxntal | ✓ | | | | | uxntal | ✓ | | | |
| v | ✓ | ✓ | ✓ | `v-analyzer` | | v | ✓ | ✓ | ✓ | `v-analyzer` |
| vala | ✓ | | | `vala-language-server` | | vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` | | verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` | | vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | | | vhs | ✓ | | | |
@ -188,6 +231,7 @@
| wren | ✓ | ✓ | ✓ | | | wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | | | xit | ✓ | | | |
| xml | ✓ | | ✓ | | | xml | ✓ | | ✓ | |
| yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` | | xtc | ✓ | | | |
| yaml | ✓ | ✓ | ✓ | `yaml-language-server`, `ansible-language-server` |
| yuck | ✓ | | | | | yuck | ✓ | | | |
| zig | ✓ | ✓ | ✓ | `zls` | | zig | ✓ | ✓ | ✓ | `zls` |

@ -2,7 +2,7 @@
| --- | --- | | --- | --- |
| `:quit`, `:q` | Close the current view. | | `:quit`, `:q` | Close the current view. |
| `:quit!`, `:q!` | Force close the current view, ignoring unsaved changes. | | `:quit!`, `:q!` | Force close the current view, ignoring unsaved changes. |
| `:open`, `:o` | Open a file from disk into the current view. | | `:open`, `:o`, `:edit`, `:e` | 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. |
| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully, ignoring unsaved changes. | | `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully, ignoring unsaved changes. |
| `:buffer-close-others`, `:bco`, `:bcloseother` | Close all buffers but the currently focused one. | | `:buffer-close-others`, `:bco`, `:bcloseother` | Close all buffers but the currently focused one. |
@ -72,7 +72,7 @@
| `:sort` | Sort ranges in selection. | | `:sort` | Sort ranges in selection. |
| `:rsort` | Sort ranges in selection in reverse order. | | `:rsort` | Sort ranges in selection in reverse order. |
| `:reflow` | Hard-wrap the current selection of lines to a given width. | | `:reflow` | Hard-wrap the current selection of lines to a given width. |
| `:tree-sitter-subtree`, `:ts-subtree` | Display tree sitter subtree under cursor, primarily for debugging queries. | | `:tree-sitter-subtree`, `:ts-subtree` | Display the smallest tree-sitter subtree that spans the primary selection, primarily for debugging queries. |
| `:config-reload` | Refresh user config. | | `:config-reload` | Refresh user config. |
| `:config-open` | Open the user config.toml file. | | `:config-open` | Open the user config.toml file. |
| `:config-open-workspace` | Open the workspace config.toml file. | | `:config-open-workspace` | Open the workspace config.toml file. |
@ -85,4 +85,6 @@
| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. | | `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
| `:clear-register` | Clear given register. If no argument is provided, clear all registers. | | `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
| `:redraw` | Clear and re-render the whole UI | | `:redraw` | Clear and re-render the whole UI |
| `:move` | Move the current buffer and its corresponding file to a different path | | `:move`, `:mv` | Move the current buffer and its corresponding file to a different path |
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
| `:read`, `:r` | Load a file into buffer |

@ -1,4 +1,4 @@
# Adding new languages to Helix ## Adding new languages to Helix
In order to add a new language to Helix, you will need to follow the steps In order to add a new language to Helix, you will need to follow the steps
below. below.
@ -16,7 +16,7 @@ below.
> 💡 If you are adding a new Language Server configuration, make sure to update > 💡 If you are adding a new Language Server configuration, make sure to update
> the > the
> [Language Server Wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) > [Language Server Wiki](https://github.com/helix-editor/helix/wiki/Language-Server-Configurations)
> with the installation instructions. > with the installation instructions.
## Grammar configuration ## Grammar configuration

@ -1,4 +1,4 @@
# Adding indent queries ## Adding indent queries
Helix uses tree-sitter to correctly indent new lines. This requires a tree- Helix uses tree-sitter to correctly indent new lines. This requires a tree-
sitter grammar and an `indent.scm` query file placed in `runtime/queries/ sitter grammar and an `indent.scm` query file placed in `runtime/queries/
@ -315,6 +315,10 @@ The first argument (a capture) must/must not be equal to the second argument
The first argument (a capture) must/must not match the regex given in the The first argument (a capture) must/must not match the regex given in the
second argument (a string). second argument (a string).
- `#any-of?`/`#not-any-of?`:
The first argument (a capture) must/must not be one of the other arguments
(strings).
Additionally, we support some custom predicates for indent queries: Additionally, we support some custom predicates for indent queries:
- `#not-kind-eq?`: - `#not-kind-eq?`:

@ -1,4 +1,4 @@
# Adding Injection Queries ## Adding Injection Queries
Writing language injection queries allows one to highlight a specific node as a different language. Writing language injection queries allows one to highlight a specific node as a different language.
In addition to the [standard][upstream-docs] language injection options used by tree-sitter, there In addition to the [standard][upstream-docs] language injection options used by tree-sitter, there
@ -54,4 +54,7 @@ The first argument (a capture) must be equal to the second argument
The first argument (a capture) must match the regex given in the The first argument (a capture) must match the regex given in the
second argument (a string). second argument (a string).
- `#any-of?` (standard):
The first argument (a capture) must be one of the other arguments (strings).
[upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection [upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection

@ -1,4 +1,4 @@
# Adding textobject queries ## Adding textobject queries
Helix supports textobjects that are language specific, such as functions, classes, etc. Helix supports textobjects that are language specific, such as functions, classes, etc.
These textobjects require an accompanying tree-sitter grammar and a `textobjects.scm` query file These textobjects require an accompanying tree-sitter grammar and a `textobjects.scm` query file
@ -25,6 +25,8 @@ The following [captures][tree-sitter-captures] are recognized:
| `parameter.inside` | | `parameter.inside` |
| `comment.inside` | | `comment.inside` |
| `comment.around` | | `comment.around` |
| `entry.inside` |
| `entry.around` |
[Example query files][textobject-examples] can be found in the helix GitHub repository. [Example query files][textobject-examples] can be found in the helix GitHub repository.
@ -44,4 +46,4 @@ doesn't make sense in a navigation context.
[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax [tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes [tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l= [textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+path%3A%2A%2A/textobjects.scm&type=Code&ref=advsearch&l=&l=

@ -1,38 +1,10 @@
# Installing Helix # Installing Helix
<!--toc:start-->
- [Pre-built binaries](#pre-built-binaries)
- [Linux, macOS, Windows and OpenBSD packaging status](#linux-macos-windows-and-openbsd-packaging-status)
- [Linux](#linux)
- [Ubuntu](#ubuntu)
- [Fedora/RHEL](#fedorarhel)
- [Arch Linux extra](#arch-linux-extra)
- [NixOS](#nixos)
- [Flatpak](#flatpak)
- [Snap](#snap)
- [AppImage](#appimage)
- [macOS](#macos)
- [Homebrew Core](#homebrew-core)
- [MacPorts](#macports)
- [Windows](#windows)
- [Winget](#winget)
- [Scoop](#scoop)
- [Chocolatey](#chocolatey)
- [MSYS2](#msys2)
- [Building from source](#building-from-source)
- [Configuring Helix's runtime files](#configuring-helixs-runtime-files)
- [Linux and macOS](#linux-and-macos)
- [Windows](#windows)
- [Multiple runtime directories](#multiple-runtime-directories)
- [Validating the installation](#validating-the-installation)
- [Configure the desktop shortcut](#configure-the-desktop-shortcut)
<!--toc:end-->
To install Helix, follow the instructions specific to your operating system. To install Helix, follow the instructions specific to your operating system.
Note that: Note that:
- To get the latest nightly version of Helix, you need to - To get the latest nightly version of Helix, you need to
[build from source](#building-from-source). [build from source](./building-from-source.md).
- To take full advantage of Helix, install the language servers for your - To take full advantage of Helix, install the language servers for your
preferred programming languages. See the preferred programming languages. See the
@ -41,280 +13,11 @@ Note that:
## Pre-built binaries ## Pre-built binaries
Download pre-built binaries from the Download pre-built binaries from the [GitHub Releases page](https://github.com/helix-editor/helix/releases).
[GitHub Releases page](https://github.com/helix-editor/helix/releases). Add the binary to your system's `$PATH` to use it from the command The tarball contents include an `hx` binary and a `runtime` directory.
line. To set up Helix:
## Linux, macOS, Windows and OpenBSD packaging status
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## Linux
The following third party repositories are available:
### Ubuntu
Add the `PPA` for Helix:
```sh
sudo add-apt-repository ppa:maveonair/helix-editor
sudo apt update
sudo apt install helix
```
### Fedora/RHEL
```sh
sudo dnf install helix
```
### Arch Linux extra
Releases are available in the `extra` repository:
```sh
sudo pacman -S helix
```
Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available
in the AUR, which builds the master branch.
### NixOS
Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute,
the unstable channel usually carries the latest release.
Helix is also available as a [flake](https://nixos.wiki/wiki/Flakes) in the project
root. Use `nix develop` to spin up a reproducible development shell. Outputs are
cached for each push to master using [Cachix](https://www.cachix.org/). The
flake is configured to automatically make use of this cache assuming the user
accepts the new settings on first use.
If you are using a version of Nix without flakes enabled,
[install Cachix CLI](https://docs.cachix.org/installation) and use
`cachix use helix` to configure Nix to use cached outputs when possible.
### Flatpak
Helix is available on [Flathub](https://flathub.org/en-GB/apps/com.helix_editor.Helix):
```sh
flatpak install flathub com.helix_editor.Helix
flatpak run com.helix_editor.Helix
```
### Snap
Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with:
```sh
snap install --classic helix
```
This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`.
### AppImage
Install Helix using the Linux [AppImage](https://appimage.org/) format.
Download the official Helix AppImage from the [latest releases](https://github.com/helix-editor/helix/releases/latest) page.
```sh
chmod +x helix-*.AppImage # change permission for executable mode
./helix-*.AppImage # run helix
```
## macOS
### Homebrew Core
```sh
brew install helix
```
### MacPorts
```sh
port install helix
```
## Windows
Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
or [MSYS2](https://msys2.org/).
### Winget
Windows Package Manager winget command-line tool is by default available on Windows 11 and modern versions of Windows 10 as a part of the App Installer.
You can get [App Installer from the Microsoft Store](https://www.microsoft.com/p/app-installer/9nblggh4nns1#activetab=pivot:overviewtab). If it's already installed, make sure it is updated with the latest version.
```sh
winget install Helix.Helix
```
### Scoop
```sh
scoop install helix
```
### Chocolatey
```sh
choco install helix
```
### MSYS2
For 64-bit Windows 8.1 or above:
```sh
pacman -S mingw-w64-ucrt-x86_64-helix
```
## Building from source
Requirements:
Clone the Helix GitHub repository into a directory of your choice. The
examples in this documentation assume installation into either `~/src/` on
Linux and macOS, or `%userprofile%\src\` on Windows.
- The [Rust toolchain](https://www.rust-lang.org/tools/install)
- The [Git version control system](https://git-scm.com/)
- A C++14 compatible compiler to build the tree-sitter grammars, for example GCC or Clang
If you are using the `musl-libc` standard library instead of `glibc` the following environment variable must be set during the build to ensure tree-sitter grammars can be loaded correctly:
```sh
RUSTFLAGS="-C target-feature=-crt-static"
```
1. Clone the repository:
```sh
git clone https://github.com/helix-editor/helix
cd helix
```
2. Compile from source:
```sh
cargo install --path helix-term --locked
```
This command will create the `hx` executable and construct the tree-sitter
grammars in the local `runtime` folder.
> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch
> grammars with `hx --grammar fetch` and compile them with
> `hx --grammar build`. This will install them in
> the `runtime` directory within the user's helix config directory (more
> [details below](#multiple-runtime-directories)).
### Configuring Helix's runtime files
#### Linux and macOS
The **runtime** directory is one below the Helix source, so either set a
`HELIX_RUNTIME` environment variable to point to that directory and add it to
your `~/.bashrc` or equivalent:
```sh
HELIX_RUNTIME=~/src/helix/runtime
```
Or, create a symbolic link:
```sh
ln -Ts $PWD/runtime ~/.config/helix/runtime
```
If the above command fails to create a symbolic link because the file exists either move `~/.config/helix/runtime` to a new location or delete it, then run the symlink command above again.
#### Windows
Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for
`Edit environment variables for your account`) or use the `setx` command in
Cmd:
```sh
setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
```
> 💡 `%userprofile%` resolves to your user directory like
> `C:\Users\Your-Name\` for example.
Or, create a symlink in `%appdata%\helix\` that links to the source code directory:
| Method | Command |
| ---------- | -------------------------------------------------------------------------------------- |
| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` |
| Cmd | `cd %appdata%\helix` <br/> `mklink /D runtime "%userprofile%\src\helix\runtime"` |
> 💡 On Windows, creating a symbolic link may require running PowerShell or
> Cmd as an administrator.
#### Multiple runtime directories
When Helix finds multiple runtime directories it will search through them for files in the
following order:
1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for
developing and testing helix only).
2. `runtime/` subdirectory of OS-dependent helix user config directory.
3. `$HELIX_RUNTIME`
4. Distribution-specific fallback directory (set at compile time—not run time—
with the `HELIX_DEFAULT_RUNTIME` environment variable)
5. `runtime/` subdirectory of path to Helix executable.
This order also sets the priority for selecting which file will be used if multiple runtime
directories have files with the same name.
#### Note to packagers
If you are making a package of Helix for end users, to provide a good out of
the box experience, you should set the `HELIX_DEFAULT_RUNTIME` environment
variable at build time (before invoking `cargo build`) to a directory which
will store the final runtime files after installation. For example, say you want
to package the runtime into `/usr/lib/helix/runtime`. The rough steps a build
script could follow are:
1. `export HELIX_DEFAULT_RUNTIME=/usr/lib/helix/runtime`
1. `cargo build --profile opt --locked --path helix-term`
1. `cp -r runtime $BUILD_DIR/usr/lib/helix/`
1. `cp target/opt/hx $BUILD_DIR/usr/bin/hx`
This way the resulting `hx` binary will always look for its runtime directory in
`/usr/lib/helix/runtime` if the user has no custom runtime in `~/.config/helix`
or `HELIX_RUNTIME`.
### Validating the installation
To make sure everything is set up as expected you should run the Helix health
check:
```sh
hx --health
```
For more information on the health check results refer to
[Health check](https://github.com/helix-editor/helix/wiki/Healthcheck).
### Configure the desktop shortcut
If your desktop environment supports the
[XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html)
you can configure Helix to show up in the application menu by copying the
provided `.desktop` and icon files to their correct folders:
```sh
cp contrib/Helix.desktop ~/.local/share/applications
cp contrib/helix.png ~/.icons # or ~/.local/share/icons
```
To use another terminal than the system default, you can modify the `.desktop` 1. Add the `hx` binary to your system's `$PATH` to allow it to be used from the command line.
file. For example, to use `kitty`: 2. Copy the `runtime` directory to a location that `hx` searches for runtime files. A typical location on Linux/macOS is `~/.config/helix/runtime`.
```sh To see the runtime directories that `hx` searches, run `hx --health`. If necessary, you can override the default runtime location by setting the `HELIX_RUNTIME` environment variable.
sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop
sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop
```

@ -1,4 +1,4 @@
# Keymap ## Keymap
- [Normal mode](#normal-mode) - [Normal mode](#normal-mode)
- [Movement](#movement) - [Movement](#movement)
@ -13,6 +13,8 @@
- [Window mode](#window-mode) - [Window mode](#window-mode)
- [Space mode](#space-mode) - [Space mode](#space-mode)
- [Popup](#popup) - [Popup](#popup)
- [Completion Menu](#completion-menu)
- [Signature-help Popup](#signature-help-popup)
- [Unimpaired](#unimpaired) - [Unimpaired](#unimpaired)
- [Insert mode](#insert-mode) - [Insert mode](#insert-mode)
- [Select / extend mode](#select--extend-mode) - [Select / extend mode](#select--extend-mode)
@ -23,6 +25,8 @@
> 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type. > 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type.
> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the [wiki](https://github.com/helix-editor/helix/wiki/Terminal-Support) for known conflicts.
## Normal mode ## Normal mode
Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key. Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key.
@ -48,13 +52,13 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `T` | Find 'till previous char | `till_prev_char` | | `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` | | `F` | Find previous char | `find_prev_char` |
| `G` | Go to line number `<n>` | `goto_line` | | `G` | Go to line number `<n>` | `goto_line` |
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` | | `Alt-.` | Repeat last motion (`f`, `t`, `m`, `[` or `]`) | `repeat_last_motion` |
| `Home` | Move to the start of the line | `goto_line_start` | | `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` | | `End` | Move to the end of the line | `goto_line_end` |
| `Ctrl-b`, `PageUp` | Move page up | `page_up` | | `Ctrl-b`, `PageUp` | Move page up | `page_up` |
| `Ctrl-f`, `PageDown` | Move page down | `page_down` | | `Ctrl-f`, `PageDown` | Move page down | `page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` | | `Ctrl-u` | Move cursor and page half page up | `page_cursor_half_up` |
| `Ctrl-d` | Move half page down | `half_page_down` | | `Ctrl-d` | Move cursor and page half page down | `page_cursor_half_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | | `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | | `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` | | `Ctrl-s` | Save the current selection to the jumplist | `save_selection` |
@ -85,7 +89,7 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` | | `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` | | `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` | | `<` | Unindent selection | `unindent` |
| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` | | `=` | Format selection (**LSP**) | `format_selections` |
| `d` | Delete selection | `delete_selection` | | `d` | Delete selection | `delete_selection` |
| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` | | `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` |
@ -141,6 +145,9 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `Alt-i`, `Alt-down` | Shrink syntax tree object selection (**TS**) | `shrink_selection` | | `Alt-i`, `Alt-down` | Shrink syntax tree object selection (**TS**) | `shrink_selection` |
| `Alt-p`, `Alt-left` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` | | `Alt-p`, `Alt-left` | Select previous sibling node in syntax tree (**TS**) | `select_prev_sibling` |
| `Alt-n`, `Alt-right` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` | | `Alt-n`, `Alt-right` | Select next sibling node in syntax tree (**TS**) | `select_next_sibling` |
| `Alt-a` | Select all sibling nodes in syntax tree (**TS**) | `select_all_siblings` |
| `Alt-e` | Move to end of parent node in syntax tree (**TS**) | `move_parent_node_end` |
| `Alt-b` | Move to start of parent node in syntax tree (**TS**) | `move_parent_node_start` |
### Search ### Search
@ -192,8 +199,8 @@ useful when you're simply looking over text and not actively editing it.
| `k`, `up` | Scroll the view upwards | `scroll_up` | | `k`, `up` | Scroll the view upwards | `scroll_up` |
| `Ctrl-f`, `PageDown` | Move page down | `page_down` | | `Ctrl-f`, `PageDown` | Move page down | `page_down` |
| `Ctrl-b`, `PageUp` | Move page up | `page_up` | | `Ctrl-b`, `PageUp` | Move page up | `page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` | | `Ctrl-u` | Move cursor and page half page up | `page_cursor_half_up` |
| `Ctrl-u` | Move half page up | `half_page_up` | | `Ctrl-d` | Move cursor and page half page down | `page_cursor_half_down` |
#### Goto mode #### Goto mode
@ -205,7 +212,7 @@ Jumps to various locations.
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `g` | Go to line number `<n>` else start of 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` | | `e` | Go to the end of the file | `goto_last_line` |
| `f` | Go to files in the selection | `goto_file` | | `f` | Go to files in the selections | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` | | `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` | | `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` | | `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
@ -223,13 +230,13 @@ Jumps to various locations.
| `.` | Go to last modification in current file | `goto_last_modification` | | `.` | Go to last modification in current file | `goto_last_modification` |
| `j` | Move down textual (instead of visual) line | `move_line_down` | | `j` | Move down textual (instead of visual) line | `move_line_down` |
| `k` | Move up textual (instead of visual) line | `move_line_up` | | `k` | Move up textual (instead of visual) line | `move_line_up` |
| `w` | Show labels at each word and select the word that belongs to the entered labels | `goto_word` |
#### Match mode #### Match mode
Accessed by typing `m` in [normal mode](#normal-mode). Accessed by typing `m` in [normal mode](#normal-mode).
See the relevant section in [Usage](./usage.md) for an explanation about Please refer to the relevant sections for detailed explanations about [surround](./surround.md) and [textobjects](./textobjects.md).
[surround](./usage.md#surround) and [textobject](./usage.md#navigating-using-tree-sitter-textobjects) usage.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
@ -253,8 +260,8 @@ This layer is similar to Vim keybindings as Kakoune does not support windows.
| `w`, `Ctrl-w` | Switch to next window | `rotate_view` | | `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
| `v`, `Ctrl-v` | Vertical right split | `vsplit` | | `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` | | `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `f` | Go to files in the selection in horizontal splits | `goto_file` | | `f` | Go to files in the selections in horizontal splits | `goto_file` |
| `F` | Go to files in the selection in vertical splits | `goto_file` | | `F` | Go to files in the selections in vertical splits | `goto_file` |
| `h`, `Ctrl-h`, `Left` | Move to left split | `jump_view_left` | | `h`, `Ctrl-h`, `Left` | Move to left split | `jump_view_left` |
| `j`, `Ctrl-j`, `Down` | Move to split below | `jump_view_down` | | `j`, `Ctrl-j`, `Down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `Up` | Move to split above | `jump_view_up` | | `k`, `Ctrl-k`, `Up` | Move to split above | `jump_view_up` |
@ -278,7 +285,8 @@ This layer is a kludge of mappings, mostly pickers.
| `F` | Open file picker at current working directory | `file_picker_in_current_directory` | | `F` | Open file picker at current working directory | `file_picker_in_current_directory` |
| `b` | Open buffer picker | `buffer_picker` | | `b` | Open buffer picker | `buffer_picker` |
| `j` | Open jumplist picker | `jumplist_picker` | | `j` | Open jumplist picker | `jumplist_picker` |
| `g` | Debug (experimental) | N/A | | `g` | Open changed file picker | `changed_file_picker` |
| `G` | Debug (experimental) | N/A |
| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` | | `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `s` | Open document symbol picker (**LSP**) | `symbol_picker` | | `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` | | `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
@ -289,6 +297,9 @@ This layer is a kludge of mappings, mostly pickers.
| `h` | Select symbol references (**LSP**) | `select_references_to_symbol_under_cursor` | | `h` | Select symbol references (**LSP**) | `select_references_to_symbol_under_cursor` |
| `'` | Open last fuzzy picker | `last_picker` | | `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A | | `w` | Enter [window mode](#window-mode) | N/A |
| `c` | Comment/uncomment selections | `toggle_comments` |
| `C` | Block comment/uncomment selections | `toggle_block_comments` |
| `Alt-c` | Line comment/uncomment selections | `toggle_line_comments` |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` | | `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` | | `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Yank selections to clipboard | `yank_to_clipboard` | | `y` | Yank selections to clipboard | `yank_to_clipboard` |
@ -301,13 +312,35 @@ This layer is a kludge of mappings, mostly pickers.
##### Popup ##### Popup
Displays documentation for item under cursor. Displays documentation for item under cursor. Remapping currently not supported.
| Key | Description | | Key | Description |
| ---- | ----------- | | ---- | ----------- |
| `Ctrl-u` | Scroll up | | `Ctrl-u` | Scroll up |
| `Ctrl-d` | Scroll down | | `Ctrl-d` | Scroll down |
##### Completion Menu
Displays documentation for the selected completion item. Remapping currently not supported.
| Key | Description |
| ---- | ----------- |
| `Shift-Tab`, `Ctrl-p`, `Up` | Previous entry |
| `Tab`, `Ctrl-n`, `Down` | Next entry |
| `Enter` | Close menu and accept completion |
| `Ctrl-c` | Close menu and reject completion |
Any other keypresses result in the completion being accepted.
##### Signature-help Popup
Displays the signature of the selected completion item. Remapping currently not supported.
| Key | Description |
| ---- | ----------- |
| `Alt-p` | Previous signature |
| `Alt-n` | Next signature |
#### Unimpaired #### Unimpaired
These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
@ -410,6 +443,8 @@ you to selectively add search terms to your selections.
## Picker ## Picker
Keys to use within picker. Remapping currently not supported. Keys to use within picker. Remapping currently not supported.
See the documentation page on [pickers](./pickers.md) for more info.
[Prompt](#prompt) keybinds also work in pickers, except where they conflict with picker keybinds.
| Key | Description | | Key | Description |
| ----- | ------------- | | ----- | ------------- |

@ -1,7 +1,7 @@
# Language Support ## Language Support
The following languages and Language Servers are supported. To use The following languages and Language Servers are supported. To use
Language Server features, you must first [install][lsp-install-wiki] the Language Server features, you must first [configure][lsp-config-wiki] the
appropriate Language Server. appropriate Language Server.
You can check the language support in your installed helix version with `hx --health`. You can check the language support in your installed helix version with `hx --health`.
@ -11,6 +11,6 @@ Languages][adding-languages] guide for more language configuration information.
{{#include ./generated/lang-support.md}} {{#include ./generated/lang-support.md}}
[lsp-install-wiki]: https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers [lsp-config-wiki]: https://github.com/helix-editor/helix/wiki/Language-Server-Configurations
[lang-config]: ./languages.md [lang-config]: ./languages.md
[adding-languages]: ./guides/adding_languages.md [adding-languages]: ./guides/adding_languages.md

@ -1,4 +1,4 @@
# Languages ## Languages
Language-specific settings and settings for language servers are configured Language-specific settings and settings for language servers are configured
in `languages.toml` files. in `languages.toml` files.
@ -42,7 +42,7 @@ name = "mylang"
scope = "source.mylang" scope = "source.mylang"
injection-regex = "mylang" injection-regex = "mylang"
file-types = ["mylang", "myl"] file-types = ["mylang", "myl"]
comment-token = "#" comment-tokens = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
formatter = { command = "mylang-formatter" , args = ["--stdin"] } formatter = { command = "mylang-formatter" , args = ["--stdin"] }
language-servers = [ "mylang-lsp" ] language-servers = [ "mylang-lsp" ]
@ -61,7 +61,8 @@ These configuration keys are available:
| `roots` | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` | | `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 | | `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`) | | `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 | | `comment-tokens` | The tokens to use as a comment token, either a single token `"//"` or an array `["//", "///", "//!"]` (the first token will be used for commenting). Also configurable as `comment-token` for backwards compatibility|
| `block-comment-tokens`| The start and end tokens for a multiline comment either an array or single table of `{ start = "/*", end = "*/"}`. The first set of tokens will be used for commenting, any pairs in the array can be uncommented |
| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) | | `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) | | `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
| `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) |
@ -69,6 +70,7 @@ These configuration keys are available:
| `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section) | `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section)
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` | | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. | | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
| `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.
### File-type detection and the `file-types` key ### File-type detection and the `file-types` key
@ -77,24 +79,26 @@ from the above section. `file-types` is a list of strings or tables, for
example: example:
```toml ```toml
file-types = ["Makefile", "toml", { suffix = ".git/config" }] file-types = ["toml", { glob = "Makefile" }, { glob = ".git/config" }, { glob = ".github/workflows/*.yaml" } ]
``` ```
When determining a language configuration to use, Helix searches the file-types When determining a language configuration to use, Helix searches the file-types
with the following priorities: with the following priorities:
1. Exact match: if the filename of a file is an exact match of a string in a 1. Glob: values in `glob` tables are checked against the full path of the given
`file-types` list, that language wins. In the example above, `"Makefile"` file. Globs are standard Unix-style path globs (e.g. the kind you use in Shell)
will match against `Makefile` files. and can be used to match paths for a specific prefix, suffix, directory, etc.
2. Extension: if there are no exact matches, any `file-types` string that In the above example, the `{ glob = "Makefile" }` config would match files
matches the file extension of a given file wins. In the example above, the with the name `Makefile`, the `{ glob = ".git/config" }` config would match
`"toml"` matches files like `Cargo.toml` or `languages.toml`. `config` files in `.git` directories, and the `{ glob = ".github/workflows/*.yaml" }`
3. Suffix: if there are still no matches, any values in `suffix` tables config would match any `yaml` files in `.github/workflow` directories. Note
are checked against the full path of the given file. In the example above, that globs should always use the Unix path separator `/` even on Windows systems;
the `{ suffix = ".git/config" }` would match against any `config` files the matcher will automatically take the machine-specific separators into account.
in `.git` directories. Note: `/` is used as the directory separator but is If the glob isn't an absolute path or doesn't already start with a glob prefix,
replaced at runtime with the appropriate path separator for the operating `*/` will automatically be added to ensure it matches for any subdirectory.
system, so this rule would match against `.git\config` files on Windows. 2. Extension: if there are no glob matches, any `file-types` string that matches
the file extension of a given file wins. In the example above, the `"toml"`
config matches files like `Cargo.toml` or `languages.toml`.
## Language Server configuration ## Language Server configuration
@ -126,6 +130,7 @@ These are the available options for a language server.
| `config` | LSP initialization options | | `config` | LSP initialization options |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` | | `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` | | `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
| `required-root-patterns` | A list of `glob` patterns to look for in the working directory. The language server is started if at least one of them is found. |
A `format` sub-table within `config` can be used to pass extra formatting options to A `format` sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting). [Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting).
@ -145,6 +150,8 @@ They have to be defined in the `[language-server]` table as described in the pre
Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default. Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.
The definition order of language servers affects the order in the results list of code action menu.
In case multiple language servers are specified in the `language-servers` attribute of a `language`, In case multiple language servers are specified in the `language-servers` attribute of a `language`,
it's often useful to only enable/disable certain language-server features for these language servers. it's often useful to only enable/disable certain language-server features for these language servers.

@ -0,0 +1,150 @@
## Package managers
- [Linux](#linux)
- [Ubuntu](#ubuntu)
- [Fedora/RHEL](#fedorarhel)
- [Arch Linux extra](#arch-linux-extra)
- [NixOS](#nixos)
- [Flatpak](#flatpak)
- [Snap](#snap)
- [AppImage](#appimage)
- [macOS](#macos)
- [Homebrew Core](#homebrew-core)
- [MacPorts](#macports)
- [Windows](#windows)
- [Winget](#winget)
- [Scoop](#scoop)
- [Chocolatey](#chocolatey)
- [MSYS2](#msys2)
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix-editor.svg)](https://repology.org/project/helix-editor/versions)
## Linux
The following third party repositories are available:
### Ubuntu
Add the `PPA` for Helix:
```sh
sudo add-apt-repository ppa:maveonair/helix-editor
sudo apt update
sudo apt install helix
```
### Fedora/RHEL
```sh
sudo dnf install helix
```
### Arch Linux extra
Releases are available in the `extra` repository:
```sh
sudo pacman -S helix
```
> 💡 When installed from the `extra` repository, run Helix with `helix` instead of `hx`.
>
> For example:
> ```sh
> helix --health
> ```
> to check health
Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available
in the AUR, which builds the master branch.
### NixOS
Helix is available in [nixpkgs](https://github.com/nixos/nixpkgs) through the `helix` attribute,
the unstable channel usually carries the latest release.
Helix is also available as a [flake](https://wiki.nixos.org/wiki/Flakes) in the project
root. Use `nix develop` to spin up a reproducible development shell. Outputs are
cached for each push to master using [Cachix](https://www.cachix.org/). The
flake is configured to automatically make use of this cache assuming the user
accepts the new settings on first use.
If you are using a version of Nix without flakes enabled,
[install Cachix CLI](https://docs.cachix.org/installation) and use
`cachix use helix` to configure Nix to use cached outputs when possible.
### Flatpak
Helix is available on [Flathub](https://flathub.org/en-GB/apps/com.helix_editor.Helix):
```sh
flatpak install flathub com.helix_editor.Helix
flatpak run com.helix_editor.Helix
```
### Snap
Helix is available on [Snapcraft](https://snapcraft.io/helix) and can be installed with:
```sh
snap install --classic helix
```
This will install Helix as both `/snap/bin/helix` and `/snap/bin/hx`, so make sure `/snap/bin` is in your `PATH`.
### AppImage
Install Helix using the Linux [AppImage](https://appimage.org/) format.
Download the official Helix AppImage from the [latest releases](https://github.com/helix-editor/helix/releases/latest) page.
```sh
chmod +x helix-*.AppImage # change permission for executable mode
./helix-*.AppImage # run helix
```
## macOS
### Homebrew Core
```sh
brew install helix
```
### MacPorts
```sh
port install helix
```
## Windows
Install on Windows using [Winget](https://learn.microsoft.com/en-us/windows/package-manager/winget/), [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
or [MSYS2](https://msys2.org/).
### Winget
Windows Package Manager winget command-line tool is by default available on Windows 11 and modern versions of Windows 10 as a part of the App Installer.
You can get [App Installer from the Microsoft Store](https://www.microsoft.com/p/app-installer/9nblggh4nns1#activetab=pivot:overviewtab). If it's already installed, make sure it is updated with the latest version.
```sh
winget install Helix.Helix
```
### Scoop
```sh
scoop install helix
```
### Chocolatey
```sh
choco install helix
```
### MSYS2
For 64-bit Windows 8.1 or above:
```sh
pacman -S mingw-w64-ucrt-x86_64-helix
```

@ -0,0 +1,11 @@
## Using pickers
Helix has a variety of pickers, which are interactive windows used to select various kinds of items. These include a file picker, global search picker, and more. Most pickers are accessed via keybindings in [space mode](./keymap.md#space-mode). Pickers have their own [keymap](./keymap.md#picker) for navigation.
### Filtering Picker Results
Most pickers perform fuzzy matching using [fzf syntax](https://github.com/junegunn/fzf?tab=readme-ov-file#search-syntax). Two exceptions are the global search picker, which uses regex, and the workspace symbol picker, which passes search terms to the LSP. Note that OR operations (`|`) are not currently supported.
If a picker shows multiple columns, you may apply the filter to a specific column by prefixing the column name with `%`. Column names can be shortened to any prefix, so `%p`, `%pa` or `%pat` all mean the same as `%path`. For example, a query of `helix %p .toml !lang` in the global search picker searches for the term "helix" within files with paths ending in ".toml" but not including "lang".
You can insert the contents of a [register](./registers.md) using `Ctrl-r` followed by a register name. For example, one could insert the currently selected text using `Ctrl-r`-`.`, or the directory of the current file using `Ctrl-r`-`%` followed by `Ctrl-w` to remove the last path section. The global search picker will use the contents of the [search register](./registers.md#default-registers) if you press `Enter` without typing a filter. For example, pressing `*`-`Space-/`-`Enter` will start a global search for the currently selected text.

@ -0,0 +1,54 @@
## Registers
- [User-defined registers](#user-defined-registers)
- [Default registers](#default-registers)
- [Special registers](#special-registers)
In Helix, registers are storage locations for text and other data, such as the
result of a search. Registers can be used to cut, copy, and paste text, similar
to the clipboard in other text editors. Usage is similar to Vim, with `"` being
used to select a register.
### User-defined registers
Helix allows you to create your own named registers for storing text, for
example:
- `"ay` - Yank the current selection to register `a`.
- `"op` - Paste the text in register `o` after the selection.
If a register is selected before invoking a change or delete command, the selection will be stored in the register and the action will be carried out:
- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
- `"md` - Store the selection in register `m` and delete it.
### Default registers
Commands that use registers, like yank (`y`), use a default register if none is specified.
These registers are used as defaults:
| Register character | Contains |
| --- | --- |
| `/` | Last search |
| `:` | Last executed command |
| `"` | Last yanked text |
| `@` | Last recorded macro |
### Special registers
Some registers have special behavior when read from and written to.
| Register character | When read | When written |
| --- | --- | --- |
| `_` | No values are returned | All values are discarded |
| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable |
| `.` | Contents of the current selections | This register is not writable |
| `%` | Name of the current file | This register is not writable |
| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard |
| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard |
When yanking multiple selections to the clipboard registers, the selections
are joined with newlines. Pasting from these registers will paste multiple
selections if the clipboard was last yanked to by the Helix session. Otherwise
the clipboard contents are pasted as one selection.

@ -1,4 +1,4 @@
# Key remapping ## Key remapping
Helix currently supports one-way key remapping through a simple TOML configuration Helix currently supports one-way key remapping through a simple TOML configuration
file. (More powerful solutions such as rebinding via commands will be file. (More powerful solutions such as rebinding via commands will be
@ -75,5 +75,20 @@ Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes
Keys can be disabled by binding them to the `no_op` command. Keys can be disabled by binding them to the `no_op` command.
A list of commands is available in the [Keymap](https://docs.helix-editor.com/keymap.html) documentation ## Commands
and 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`.
There are three kinds of commands that can be used in keymaps:
* Static commands: commands like `move_char_right` which are usually bound to
keys and used for movement and editing. A list of static commands is
available in the [Keymap](./keymap.html) documentation and in the source code
in [`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`.
* Typable commands: commands that can be executed from command mode (`:`), for
example `:write!`. See the [Commands](./commands.html) documentation for a
list of available typeable commands.
* Macros: sequences of keys that are executed in order. These keybindings
start with `@` and then list any number of keys to be executed. For example
`@miw` can be used to select the surrounding word. For now, macro keybindings
are not allowed in keybinding sequences due to limitations in the way that
command sequences are executed.

@ -0,0 +1,24 @@
## Surround
Helix includes built-in functionality similar to [vim-surround](https://github.com/tpope/vim-surround).
The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich):
![Surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif)
| Key Sequence | Action |
| --------------------------------- | --------------------------------------- |
| `ms<char>` (after selecting text) | Add surround characters to selection |
| `mr<char_to_replace><new_char>` | Replace the closest surround characters |
| `md<char_to_delete>` | Delete the closest surround characters |
You can use counts to act on outer pairs.
Surround can also act on multiple selections. For example, to change every occurrence of `(use)` to `[use]`:
1. `%` to select the whole file
2. `s` to split the selections on a search term
3. Input `use` and hit Enter
4. `mr([` to replace the parentheses with square brackets
Multiple characters are currently not supported, but planned for future release.

@ -0,0 +1,66 @@
## Moving the selection with syntax-aware motions
`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the
selection according to its location in the syntax tree. For example, many languages have the
following syntax for function calls:
```js
func(arg1, arg2, arg3);
```
A function call might be parsed by tree-sitter into a tree like the following.
```tsq
(call
function: (identifier) ; func
arguments:
(arguments ; (arg1, arg2, arg3)
(identifier) ; arg1
(identifier) ; arg2
(identifier))) ; arg3
```
Use `:tree-sitter-subtree` to view the syntax tree of the primary selection. In
a more intuitive tree format:
```
┌────┐
│call│
┌─────┴────┴─────┐
│ │
┌─────▼────┐ ┌────▼────┐
│identifier│ │arguments│
│ "func" │ ┌────┴───┬─────┴───┐
└──────────┘ │ │ │
│ │ │
┌─────────▼┐ ┌────▼─────┐ ┌▼─────────┐
│identifier│ │identifier│ │identifier│
│ "arg1" │ │ "arg2" │ │ "arg3" │
└──────────┘ └──────────┘ └──────────┘
```
If you have a selection that wraps `arg1` (see the tree above), and you use
`Alt-n`, it will select the next sibling in the syntax tree: `arg2`.
```js
// before
func([arg1], arg2, arg3)
// after
func(arg1, [arg2], arg3);
```
Similarly, `Alt-o` will expand the selection to the parent node, in this case, the
arguments node.
```js
func[(arg1, arg2, arg3)];
```
There is also some nuanced behavior that prevents you from getting stuck on a
node with no sibling. When using `Alt-p` with a selection on `arg1`, the previous
child node will be selected. In the event that `arg1` does not have a previous
sibling, the selection will move up the syntax tree and select the previous
element. As a result, using `Alt-p` with a selection on `arg1` will move the
selection to the "func" `identifier`.
[lang-support]: ./lang-support.md

@ -0,0 +1,47 @@
## Selecting and manipulating text with textobjects
In Helix, textobjects are a way to select, manipulate and operate on a piece of
text in a structured way. They allow you to refer to blocks of text based on
their structure or purpose, such as a word, sentence, paragraph, or even a
function or block of code.
![Textobject demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
![Textobject tree-sitter demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
- `ma` - Select around the object (`va` in Vim, `<alt-a>` in Kakoune)
- `mi` - Select inside the object (`vi` in Vim, `<alt-i>` in Kakoune)
| Key after `mi` or `ma` | Textobject selected |
| --- | --- |
| `w` | Word |
| `W` | WORD |
| `p` | Paragraph |
| `(`, `[`, `'`, etc. | Specified surround pairs |
| `m` | The closest surround pair |
| `f` | Function |
| `t` | Type (or Class) |
| `a` | Argument/parameter |
| `c` | Comment |
| `T` | Test |
| `g` | Change |
> 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current
document and a special tree-sitter query file to work properly. [Only
some grammars](./lang-support.md) currently have the query file implemented.
Contributions are welcome!
## Navigating using tree-sitter textobjects
Navigating between functions, classes, parameters, and other elements is
possible using tree-sitter and textobject queries. For
example to move to the next function use `]f`, to move to previous
type use `[t`, and so on.
![Tree-sitter-nav-demo](https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif)
For the full reference see the [unimpaired](./keymap.html#unimpaired) section of the key bind
documentation.
> 💡 This feature relies on tree-sitter textobjects
> and requires the corresponding query file to work properly.

@ -1,4 +1,4 @@
# Themes ## Themes
To use a theme add `theme = "<name>"` to the top of your [`config.toml`](./configuration.md) file, or select it during runtime using `:theme <name>`. To use a theme add `theme = "<name>"` to the top of your [`config.toml`](./configuration.md) file, or select it during runtime using `:theme <name>`.
@ -36,13 +36,6 @@ For inspiration, you can find the default `theme.toml`
user-submitted themes user-submitted themes
[here](https://github.com/helix-editor/helix/blob/master/runtime/themes). [here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
### Using the linter
Use the supplied linting tool to check for errors and missing scopes:
```sh
cargo xtask themelint onedark # replace onedark with <name>
```
## The details of theme creation ## The details of theme creation
@ -186,6 +179,7 @@ We use a similar set of scopes as
- `parameter` - Function parameters - `parameter` - Function parameters
- `other` - `other`
- `member` - Fields of composite data types (e.g. structs, unions) - `member` - Fields of composite data types (e.g. structs, unions)
- `private` - Private fields that use a unique syntax (currently just ECMAScript-based languages)
- `label` - `label`
@ -213,6 +207,7 @@ We use a similar set of scopes as
- `function` - `function`
- `builtin` - `builtin`
- `method` - `method`
- `private` - Private methods that use a unique syntax (currently just ECMAScript-based languages)
- `macro` - `macro`
- `special` (preprocessor in C) - `special` (preprocessor in C)
@ -251,6 +246,7 @@ We use a similar set of scopes as
- `gutter` - gutter indicator - `gutter` - gutter indicator
- `delta` - modifications - `delta` - modifications
- `moved` - renamed or moved files/changes - `moved` - renamed or moved files/changes
- `conflict` - merge conflicts
- `gutter` - gutter indicator - `gutter` - gutter indicator
#### Interface #### Interface
@ -287,7 +283,6 @@ These scopes are used for theming the editor interface:
| `ui.debug.active` | Indicator for the line at which debugging execution is paused at, found in the gutter | | `ui.debug.active` | Indicator for the line at which debugging execution is paused at, found in the gutter |
| `ui.gutter` | Gutter | | `ui.gutter` | Gutter |
| `ui.gutter.selected` | Gutter for the line the cursor is on | | `ui.gutter.selected` | Gutter for the line the cursor is on |
| `ui.highlight.frameline` | Line at which debugging execution is paused at |
| `ui.linenr` | Line numbers | | `ui.linenr` | Line numbers |
| `ui.linenr.selected` | Line number for the line the cursor is on | | `ui.linenr.selected` | Line number for the line the cursor is on |
| `ui.statusline` | Statusline | | `ui.statusline` | Statusline |
@ -301,6 +296,9 @@ These scopes are used for theming the editor interface:
| `ui.bufferline.background` | Style for bufferline background | | `ui.bufferline.background` | Style for bufferline background |
| `ui.popup` | Documentation popups (e.g. Space + k) | | `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options | | `ui.popup.info` | Prompt for multiple key options |
| `ui.picker.header` | Header row area in pickers with multiple columns |
| `ui.picker.header.column` | Column names in pickers with multiple columns |
| `ui.picker.header.column.active` | The column name in pickers with multiple columns where the cursor is entering into. |
| `ui.window` | Borderlines separating splits | | `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands | | `ui.help` | Description box for commands |
| `ui.text` | Default text style, command prompts, popup text, etc. | | `ui.text` | Default text style, command prompts, popup text, etc. |
@ -314,12 +312,14 @@ These scopes are used for theming the editor interface:
| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) | | `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) | | `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) | | `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
| `ui.virtual.jump-label` | Style for virtual jump labels |
| `ui.menu` | Code and command completion menus | | `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item | | `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar | | `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
| `ui.selection` | For selections in the editing area | | `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | | | `ui.selection.primary` | |
| `ui.highlight` | Highlighted lines in the picker preview | | `ui.highlight` | Highlighted lines in the picker preview |
| `ui.highlight.frameline` | Line at which debugging execution is paused at |
| `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) | | `ui.cursorline.primary` | The line of the primary cursor ([if cursorline is enabled][editor-section]) |
| `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) | | `ui.cursorline.secondary` | The lines of any other cursors ([if cursorline is enabled][editor-section]) |
| `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) | | `ui.cursorcolumn.primary` | The column of the primary cursor ([if cursorcolumn is enabled][editor-section]) |
@ -333,5 +333,7 @@ These scopes are used for theming the editor interface:
| `diagnostic.info` | Diagnostics info (editing area) | | `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) | | `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) | | `diagnostic.error` | Diagnostics error (editing area) |
| `diagnostic.unnecessary` | Diagnostics with unnecessary tag (editing area) |
| `diagnostic.deprecated` | Diagnostics with deprecated tag (editing area) |
[editor-section]: ./configuration.md#editor-section [editor-section]: ./configuration.md#editor-section

@ -1,15 +1,5 @@
# Using Helix # Using Helix
<!--toc:start-->
- [Registers](#registers)
- [User-defined registers](#user-defined-registers)
- [Special registers](#special-registers)
- [Surround](#surround)
- [Selecting and manipulating text with textobjects](#selecting-and-manipulating-text-with-textobjects)
- [Navigating using tree-sitter textobjects](#navigating-using-tree-sitter-textobjects)
- [Moving the selection with syntax-aware motions](#moving-the-selection-with-syntax-aware-motions)
<!--toc:end-->
For a full interactive introduction to Helix, refer to the For a full interactive introduction to Helix, refer to the
[tutor](https://github.com/helix-editor/helix/blob/master/runtime/tutor) which [tutor](https://github.com/helix-editor/helix/blob/master/runtime/tutor) which
can be accessed via the command `hx --tutor` or `:tutor`. can be accessed via the command `hx --tutor` or `:tutor`.
@ -17,192 +7,27 @@ can be accessed via the command `hx --tutor` or `:tutor`.
> 💡 Currently, not all functionality is fully documented, please refer to the > 💡 Currently, not all functionality is fully documented, please refer to the
> [key mappings](./keymap.md) list. > [key mappings](./keymap.md) list.
## Registers ## Modes
In Helix, registers are storage locations for text and other data, such as the
result of a search. Registers can be used to cut, copy, and paste text, similar
to the clipboard in other text editors. Usage is similar to Vim, with `"` being
used to select a register.
### User-defined registers
Helix allows you to create your own named registers for storing text, for
example:
- `"ay` - Yank the current selection to register `a`.
- `"op` - Paste the text in register `o` after the selection.
If a register is selected before invoking a change or delete command, the selection will be stored in the register and the action will be carried out:
- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
- `"md` - Store the selection in register `m` and delete it.
### Default registers
Commands that use registers, like yank (`y`), use a default register if none is specified.
These registers are used as defaults:
| Register character | Contains |
| --- | --- |
| `/` | Last search |
| `:` | Last executed command |
| `"` | Last yanked text |
| `@` | Last recorded macro |
### Special registers
Some registers have special behavior when read from and written to.
| Register character | When read | When written |
| --- | --- | --- |
| `_` | No values are returned | All values are discarded |
| `#` | Selection indices (first selection is `1`, second is `2`, etc.) | This register is not writable |
| `.` | Contents of the current selections | This register is not writable |
| `%` | Name of the current file | This register is not writable |
| `+` | Reads from the system clipboard | Joins and yanks to the system clipboard |
| `*` | Reads from the primary clipboard | Joins and yanks to the primary clipboard |
When yanking multiple selections to the clipboard registers, the selections
are joined with newlines. Pasting from these registers will paste multiple
selections if the clipboard was last yanked to by the Helix session. Otherwise
the clipboard contents are pasted as one selection.
## Surround
Helix includes built-in functionality similar to [vim-surround](https://github.com/tpope/vim-surround).
The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich):
![Surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif)
| Key Sequence | Action |
| --------------------------------- | --------------------------------------- |
| `ms<char>` (after selecting text) | Add surround characters to selection |
| `mr<char_to_replace><new_char>` | Replace the closest surround characters |
| `md<char_to_delete>` | Delete the closest surround characters |
You can use counts to act on outer pairs.
Surround can also act on multiple selections. For example, to change every occurrence of `(use)` to `[use]`:
1. `%` to select the whole file
2. `s` to split the selections on a search term
3. Input `use` and hit Enter
4. `mr([` to replace the parentheses with square brackets
Multiple characters are currently not supported, but planned for future release.
## Selecting and manipulating text with textobjects
In Helix, textobjects are a way to select, manipulate and operate on a piece of
text in a structured way. They allow you to refer to blocks of text based on
their structure or purpose, such as a word, sentence, paragraph, or even a
function or block of code.
![Textobject demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
![Textobject tree-sitter demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
- `ma` - Select around the object (`va` in Vim, `<alt-a>` in Kakoune)
- `mi` - Select inside the object (`vi` in Vim, `<alt-i>` in Kakoune)
| Key after `mi` or `ma` | Textobject selected |
| --- | --- |
| `w` | Word |
| `W` | WORD |
| `p` | Paragraph |
| `(`, `[`, `'`, etc. | Specified surround pairs |
| `m` | The closest surround pair |
| `f` | Function |
| `t` | Type (or Class) |
| `a` | Argument/parameter |
| `c` | Comment |
| `T` | Test |
| `g` | Change |
> 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current
document and a special tree-sitter query file to work properly. [Only
some grammars][lang-support] currently have the query file implemented.
Contributions are welcome!
## Navigating using tree-sitter textobjects
Navigating between functions, classes, parameters, and other elements is
possible using tree-sitter and textobject queries. For
example to move to the next function use `]f`, to move to previous
type use `[t`, and so on.
![Tree-sitter-nav-demo][tree-sitter-nav-demo]
For the full reference see the [unimpaired][unimpaired-keybinds] section of the key bind
documentation.
> 💡 This feature relies on tree-sitter textobjects
> and requires the corresponding query file to work properly.
## Moving the selection with syntax-aware motions
`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the
selection according to its location in the syntax tree. For example, many languages have the
following syntax for function calls:
```js Helix is a modal editor, meaning it has different modes for different tasks. The main modes are:
func(arg1, arg2, arg3);
```
A function call might be parsed by tree-sitter into a tree like the following. * [Normal mode](./keymap.md#normal-mode): For navigation and editing commands. This is the default mode.
* [Insert mode](./keymap.md#insert-mode): For typing text directly into the document. Access by typing `i` in normal mode.
* [Select/extend mode](./keymap.md#select--extend-mode): For making selections and performing operations on them. Access by typing `v` in normal mode.
```tsq ## Buffers
(call
function: (identifier) ; func
arguments:
(arguments ; (arg1, arg2, arg3)
(identifier) ; arg1
(identifier) ; arg2
(identifier))) ; arg3
```
Use `:tree-sitter-subtree` to view the syntax tree of the primary selection. In Buffers are in-memory representations of files. You can have multiple buffers open at once. Use [pickers](./pickers.md) or commands like `:buffer-next` and `:buffer-previous` to open buffers or switch between them.
a more intuitive tree format:
``` ## Selection-first editing
┌────┐
│call│
┌─────┴────┴─────┐
│ │
┌─────▼────┐ ┌────▼────┐
│identifier│ │arguments│
│ "func" │ ┌────┴───┬─────┴───┐
└──────────┘ │ │ │
│ │ │
┌─────────▼┐ ┌────▼─────┐ ┌▼─────────┐
│identifier│ │identifier│ │identifier│
│ "arg1" │ │ "arg2" │ │ "arg3" │
└──────────┘ └──────────┘ └──────────┘
```
If you have a selection that wraps `arg1` (see the tree above), and you use Inspired by [Kakoune](http://kakoune.org/), Helix follows the `selection → action` model. This means that whatever you are going to act on (a word, a paragraph, a line, etc.) is selected first and the action itself (delete, change, yank, etc.) comes second. A cursor is simply a single width selection.
`Alt-n`, it will select the next sibling in the syntax tree: `arg2`.
```js ## Multiple selections
// before
func([arg1], arg2, arg3)
// after
func(arg1, [arg2], arg3);
```
Similarly, `Alt-o` will expand the selection to the parent node, in this case, the Also inspired by Kakoune, multiple selections are a core mode of interaction in Helix. For example, the standard way of replacing multiple instance of a word is to first select all instances (so there is one selection per instance) and then use the change action (`c`) to edit them all at the same time.
arguments node.
```js ## Motions
func[(arg1, arg2, arg3)];
```
There is also some nuanced behavior that prevents you from getting stuck on a Motions are commands that move the cursor or modify selections. They're used for navigation and text manipulation. Examples include `w` to move to the next word, or `f` to find a character. See the [Movement](./keymap.md#movement) section of the keymap for more motions.
node with no sibling. When using `Alt-p` with a selection on `arg1`, the previous
child node will be selected. In the event that `arg1` does not have a previous
sibling, the selection will move up the syntax tree and select the previous
element. As a result, using `Alt-p` with a selection on `arg1` will move the
selection to the "func" `identifier`.
[lang-support]: ./lang-support.md
[unimpaired-keybinds]: ./keymap.md#unimpaired
[tree-sitter-nav-demo]: https://user-images.githubusercontent.com/23398472/152332550-7dfff043-36a2-4aec-b8f2-77c13eb56d6f.gif

@ -6,27 +6,26 @@
<name>Helix</name> <name>Helix</name>
<summary>A post-modern text editor</summary> <summary>A post-modern text editor</summary>
<summary xml:lang="ar">مُحَرِّرُ نُصُوصٍ سَابِقٌ لِعَهدِه</summary> <summary xml:lang="ar">مُحَرِّرُ نُصُوصٍ سَابِقٌ لِعَهدِه</summary>
<developer id="com.helix_editor">
<name>Blaž Hrastnik</name>
</developer>
<description> <description>
<p> <p>
Helix is a terminal-based text editor inspired by Kakoune / Neovim and written in Rust. Helix is a terminal-based text editor inspired by Kakoune / Neovim and written in Rust.
</p> </p>
<p xml:lang="ar">
مُحَرِّرُ نُصُوصٍ يَعمَلُ فِي الطَّرَفِيَّة، مُستَلهَمٌ مِن Kakoune وَ Neovim وَمَكتُوبٌ بِلُغَةِ رَست البَرمَجِيَّة.
</p>
<ul> <ul>
<li>Vim-like modal editing</li> <li>Vim-like modal editing</li>
<li xml:lang="ar">تَحرِيرٌ وَضعِيٌّ شَبيهٌ بِـVim</li>
<li>Multiple selections</li> <li>Multiple selections</li>
<li xml:lang="ar">تَحدِيدَاتٌ لِلنَّصِ مُتَعَدِّدَة</li>
<li>Built-in language server support</li> <li>Built-in language server support</li>
<li xml:lang="ar">دَعْمٌ مُدمَجٌ لِخَوادِمِ اللُّغَات</li>
<li>Smart, incremental syntax highlighting and code editing via tree-sitter</li> <li>Smart, incremental syntax highlighting and code editing via tree-sitter</li>
</ul> <li xml:lang="ar">تَحرِيرُ التَّعلِيمَاتِ البَّرمَجِيَّةِ مَعَ تَمييزٍ لِلتَّركِيبِ النَّحُويِّ بِواسِطَةِ tree-sitter</li>
</description>
<description xml:lang="ar">
<p>
مُحَرِّرُ نُصُوصٍ يَعمَلُ فِي الطَّرَفِيَّة، مُستَلهَمٌ مِن Kakoune وَ Neovim وَمَكتُوبٌ بِلُغَةِ رَست البَرمَجِيَّة.
</p>
<ul>
<li>تَحرِيرٌ وَضعِيٌّ شَبيهٌ بِـVim</li>
<li>تَحدِيدَاتٌ لِلنَّصِ مُتَعَدِّدَة</li>
<li>دَعْمٌ مُدمَجٌ لِخَوادِمِ اللُّغَات</li>
<li>تَحرِيرُ التَّعلِيمَاتِ البَّرمَجِيَّةِ مَعَ تَمييزٍ لِلتَّركِيبِ النَّحُويِّ بِواسِطَةِ tree-sitter</li>
</ul> </ul>
</description> </description>
@ -48,6 +47,12 @@
<content_rating type="oars-1.1" /> <content_rating type="oars-1.1" />
<releases> <releases>
<release version="24.07" date="2024-07-14">
<url>https://github.com/helix-editor/helix/releases/tag/24.07</url>
</release>
<release version="24.03" date="2024-03-30">
<url>https://helix-editor.com/news/release-24-03-highlights/</url>
</release>
<release version="23.10" date="2023-10-24"> <release version="23.10" date="2023-10-24">
<url>https://helix-editor.com/news/release-23-10-highlights/</url> <url>https://helix-editor.com/news/release-23-10-highlights/</url>
</release> </release>
@ -71,9 +76,9 @@
</release> </release>
</releases> </releases>
<requires> <recommends>
<control>keyboard</control> <control>keyboard</control>
</requires> </recommends>
<categories> <categories>
<category>Utility</category> <category>Utility</category>

@ -2,22 +2,31 @@
# Bash completion script for Helix editor # Bash completion script for Helix editor
_hx() { _hx() {
# $1 command name local cur prev languages
# $2 word being completed
# $3 word preceding
COMPREPLY=() COMPREPLY=()
cur="${COMP_WORDS[COMP_CWORD]}"
prev="${COMP_WORDS[COMP_CWORD - 1]}"
case "$3" in case "$prev" in
-g | --grammar) -g | --grammar)
COMPREPLY=($(compgen -W "fetch build" -- $2)) COMPREPLY=($(compgen -W 'fetch build' -- "$cur"))
return 0
;; ;;
--health) --health)
local languages=$(hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g') languages=$(hx --health | tail -n '+7' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g')
COMPREPLY=($(compgen -W "$languages" -- $2)) COMPREPLY=($(compgen -W """$languages""" -- "$cur"))
return 0
;;
esac
case "$2" in
-*)
COMPREPLY=($(compgen -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- """$2"""))
return 0
;; ;;
*) *)
COMPREPLY=($(compgen -fd -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- $2)) COMPREPLY=($(compgen -fd -- """$2"""))
return 0
;; ;;
esac esac
} && complete -o filenames -F _hx hx } && complete -o filenames -F _hx hx

@ -1,15 +1,18 @@
#!/usr/bin/env fish #!/usr/bin/env fish
# Fish completion script for Helix editor # Fish completion script for Helix editor
set -l langs (hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g')
complete -c hx -s h -l help -d "Prints help information" complete -c hx -s h -l help -d "Prints help information"
complete -c hx -l tutor -d "Loads the tutorial" complete -c hx -l tutor -d "Loads the tutorial"
complete -c hx -l health -x -a "$langs" -d "Checks for errors in editor setup" complete -c hx -l health -xa "(__hx_langs_ops)" -d "Checks for errors"
complete -c hx -s g -l grammar -x -a "fetch build" -d "Fetches or builds tree-sitter grammars" complete -c hx -s g -l grammar -x -a "fetch build" -d "Fetch or build tree-sitter grammars"
complete -c hx -s v -o vv -o vvv -d "Increases logging verbosity" complete -c hx -s v -o vv -o vvv -d "Increases logging verbosity"
complete -c hx -s V -l version -d "Prints version information" complete -c hx -s V -l version -d "Prints version information"
complete -c hx -l vsplit -d "Splits all given files vertically into different windows" complete -c hx -l vsplit -d "Splits all given files vertically"
complete -c hx -l hsplit -d "Splits all given files horizontally into different windows" complete -c hx -l hsplit -d "Splits all given files horizontally"
complete -c hx -s c -l config -r -d "Specifies a file to use for completion" complete -c hx -s c -l config -r -d "Specifies a file to use for config"
complete -c hx -l log -r -d "Specifies a file to write log data into" complete -c hx -l log -r -d "Specifies a file to use for logging"
complete -c hx -s w -l working-dir -d "Specify initial working directory" -xa "(__fish_complete_directories)"
function __hx_langs_ops
hx --health languages | tail -n '+2' | string replace -fr '^(\S+) .*' '$1'
end

@ -0,0 +1,29 @@
# Completions for Helix: <https://github.com/helix-editor/helix>
#
# NOTE: the `+N` syntax is not supported in Nushell (https://github.com/nushell/nushell/issues/13418)
# so it has not been specified here and will not be proposed in the autocompletion of Nushell.
# The help message won't be overriden though, so it will still be present here
def health_categories [] {
let languages = ^hx --health languages | detect columns | get Language | filter { $in != null }
let completions = [ "all", "clipboard", "languages" ] | append $languages
return $completions
}
def grammar_categories [] { ["fetch", "build"] }
# A post-modern text editor.
export extern hx [
--help(-h), # Prints help information
--tutor, # Loads the tutorial
--health: string@health_categories, # Checks for potential errors in editor setup
--grammar(-g): string@grammar_categories, # Fetches or builds tree-sitter grammars listed in `languages.toml`
--config(-c): glob, # Specifies a file to use for configuration
-v, # Increases logging verbosity each use for up to 3 times
--log: glob, # Specifies a file to use for logging
--version(-V), # Prints version information
--vsplit, # Splits all given files vertically into different windows
--hsplit, # Splits all given files horizontally into different windows
--working-dir(-w): glob, # Specify an initial working directory
...files: glob, # Sets the input file to use, position can also be specified via file[:row[:col]]
]

@ -14,16 +14,18 @@ _hx() {
"--health[Checks for errors in editor setup]:language:->health" \ "--health[Checks for errors in editor setup]:language:->health" \
"-g[Fetches or builds tree-sitter grammars]:action:->grammar" \ "-g[Fetches or builds tree-sitter grammars]:action:->grammar" \
"--grammar[Fetches or builds tree-sitter grammars]:action:->grammar" \ "--grammar[Fetches or builds tree-sitter grammars]:action:->grammar" \
"--vsplit[Splits all given files vertically into different windows]" \ "--vsplit[Splits all given files vertically]" \
"--hsplit[Splits all given files horizontally into different windows]" \ "--hsplit[Splits all given files horizontally]" \
"-c[Specifies a file to use for configuration]" \ "-c[Specifies a file to use for configuration]" \
"--config[Specifies a file to use for configuration]" \ "--config[Specifies a file to use for configuration]" \
"--log[Specifies a file to write log data into]" \ "-w[Specify initial working directory]" \
"--working-dir[Specify initial working directory]" \
"--log[Specifies a file to use for logging]" \
"*:file:_files" "*:file:_files"
case "$state" in case "$state" in
health) health)
local languages=($(hx --health |tail -n '+7' |awk '{print $1}' |sed 's/\x1b\[[0-9;]*m//g')) local languages=($(hx --health | tail -n '+11' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g;s/[✘✓]//g'))
_values 'language' $languages _values 'language' $languages
;; ;;
grammar) grammar)
@ -31,4 +33,3 @@ _hx() {
;; ;;
esac esac
} }

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

@ -5,4 +5,4 @@ let
sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7"; sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7";
}; };
in in
(import compat {src = ./.;}).defaultNix.default (import compat {src = ./.;}).defaultNix

@ -53,6 +53,10 @@ Existing tests can be used as examples. Helpers can be found in
[helpers.rs][helpers.rs]. The log level can be set with the `HELIX_LOG_LEVEL` [helpers.rs][helpers.rs]. The log level can be set with the `HELIX_LOG_LEVEL`
environment variable, e.g. `HELIX_LOG_LEVEL=debug cargo integration-test`. environment variable, e.g. `HELIX_LOG_LEVEL=debug cargo integration-test`.
Contributors using MacOS might encounter `Too many open files (os error 24)`
failures while running integration tests. This can be resolved by increasing
the default value (e.g. to `10240` from `256`) by running `ulimit -n 10240`.
## Minimum Stable Rust Version (MSRV) Policy ## Minimum Stable Rust Version (MSRV) Policy
Helix follows the MSRV of Firefox. Helix follows the MSRV of Firefox.

@ -3,6 +3,7 @@
| ----------- | ----------- | | ----------- | ----------- |
| helix-core | Core editing primitives, functional. | | helix-core | Core editing primitives, functional. |
| helix-lsp | Language server client | | helix-lsp | Language server client |
| helix-lsp-types | Language Server Protocol type definitions |
| helix-dap | Debug Adapter Protocol (DAP) client | | helix-dap | Debug Adapter Protocol (DAP) client |
| helix-loader | Functions for building, fetching, and loading external resources | | helix-loader | Functions for building, fetching, and loading external resources |
| helix-view | UI abstractions for use in backends, imperative shell. | | helix-view | UI abstractions for use in backends, imperative shell. |

@ -5,19 +5,18 @@ Helix releases are versioned in the Calendar Versioning scheme:
`22.05.1`. In these instructions we'll use `<tag>` as a placeholder for the tag `22.05.1`. In these instructions we'll use `<tag>` as a placeholder for the tag
being published. being published.
* Merge the changelog PR * Merge the PR with the release updates. That branch should:
* Update the version:
* Update the `workspace.package.version` key in `Cargo.toml`. Cargo only accepts
SemVer versions so a CalVer version of `22.07` for example must be formatted
as `22.7.0`. Patch/bugfix releases should increment the SemVer patch number. A
patch release for 22.07 would be `22.7.1`.
* Run `cargo check` and commit the resulting change to `Cargo.lock`
* Add changelog notes to `CHANGELOG.md`
* Add new `<release>` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html) * Add new `<release>` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html)
* Tag and push * Tag and push
* `git tag -s -m "<tag>" -a <tag> && git push` * Switch to master and pull
* Make sure to switch to master and pull first * `git tag -s -m "<tag>" -a <tag> && git push` (note the `-s` which signs the tag)
* Edit the `Cargo.toml` file and change the date in the `version` field to the next planned release
* Due to Cargo having a strict requirement on SemVer with 3 or more version
numbers, a `0` is required in the micro version; however, unless we are
publishing a patch release after a major release, the `.0` is dropped in
the user facing version.
* Releases are planned to happen every two months, so `22.05.0` would change to `22.07.0`
* If we are pushing a patch/bugfix release in the same month as the previous
release, bump the micro version, e.g. `22.07.0` to `22.07.1`
* Wait for the Release CI to finish * Wait for the Release CI to finish
* It will automatically turn the git tag into a GitHub release when it uploads artifacts * It will automatically turn the git tag into a GitHub release when it uploads artifacts
* Edit the new release * Edit the new release
@ -31,7 +30,7 @@ being published.
* Post to reddit * Post to reddit
* [Example post](https://www.reddit.com/r/rust/comments/uzp5ze/helix_editor_2205_released/) * [Example post](https://www.reddit.com/r/rust/comments/uzp5ze/helix_editor_2205_released/)
[homebrew formula]: https://github.com/Homebrew/homebrew-core/blob/master/Formula/helix.rb [homebrew formula]: https://github.com/Homebrew/homebrew-core/blob/master/Formula/h/helix.rb
## Changelog Curation ## Changelog Curation

@ -1,17 +1,12 @@
{ {
"nodes": { "nodes": {
"crane": { "crane": {
"inputs": {
"nixpkgs": [
"nixpkgs"
]
},
"locked": { "locked": {
"lastModified": 1701025348, "lastModified": 1727974419,
"narHash": "sha256-42GHmYH+GF7VjwGSt+fVT1CQuNpGanJbNgVHTAZppUM=", "narHash": "sha256-WD0//20h+2/yPGkO88d2nYbb23WMWYvnRyDQ9Dx4UHg=",
"owner": "ipetkov", "owner": "ipetkov",
"repo": "crane", "repo": "crane",
"rev": "42afaeb1a0325194a7cdb526332d2cb92fddd07b", "rev": "37e4f9f0976cb9281cd3f0c70081e5e0ecaee93f",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -25,11 +20,11 @@
"systems": "systems" "systems": "systems"
}, },
"locked": { "locked": {
"lastModified": 1694529238, "lastModified": 1726560853,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=", "narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384", "rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -40,11 +35,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1700794826, "lastModified": 1728018373,
"narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=", "narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8", "rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -64,19 +59,16 @@
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": [
"flake-utils"
],
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
] ]
}, },
"locked": { "locked": {
"lastModified": 1701137803, "lastModified": 1728268235,
"narHash": "sha256-0LcPAdql5IhQSUXJx3Zna0dYTgdIoYO7zUrsKgiBd04=", "narHash": "sha256-lJMFnMO4maJuNO6PQ5fZesrTmglze3UFTTBuKGwR1Nw=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "9dd940c967502f844eacea52a61e9596268d4f70", "rev": "25685cc2c7054efc31351c172ae77b21814f2d42",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -6,15 +6,9 @@
flake-utils.url = "github:numtide/flake-utils"; flake-utils.url = "github:numtide/flake-utils";
rust-overlay = { rust-overlay = {
url = "github:oxalica/rust-overlay"; url = "github:oxalica/rust-overlay";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};
crane = {
url = "github:ipetkov/crane";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
}; };
crane.url = "github:ipetkov/crane";
}; };
outputs = { outputs = {
@ -114,10 +108,7 @@
if pkgs.stdenv.isLinux if pkgs.stdenv.isLinux
then pkgs.stdenv then pkgs.stdenv
else pkgs.clangStdenv; else pkgs.clangStdenv;
rustFlagsEnv = rustFlagsEnv = pkgs.lib.optionalString stdenv.isLinux "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment --cfg tokio_unstable";
if stdenv.isLinux
then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
else "$RUSTFLAGS";
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain; craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain;
craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default; craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default;
@ -129,6 +120,7 @@
# disable fetching and building of tree-sitter grammars in the helix-term build.rs # disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1"; HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
buildInputs = [stdenv.cc.cc.lib]; buildInputs = [stdenv.cc.cc.lib];
nativeBuildInputs = [pkgs.installShellFiles];
# disable tests # disable tests
doCheck = false; doCheck = false;
meta.mainProgram = "hx"; meta.mainProgram = "hx";
@ -144,6 +136,7 @@
cp contrib/Helix.desktop $out/share/applications cp contrib/Helix.desktop $out/share/applications
cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg
cp contrib/helix.png $out/share/icons/hicolor/256x256/apps cp contrib/helix.png $out/share/icons/hicolor/256x256/apps
installShellCompletion contrib/completion/hx.{bash,fish,zsh}
''; '';
}); });
helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {}; helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {};
@ -183,7 +176,7 @@
shellHook = '' shellHook = ''
export HELIX_RUNTIME="$PWD/runtime" export HELIX_RUNTIME="$PWD/runtime"
export RUST_BACKTRACE="1" export RUST_BACKTRACE="1"
export RUSTFLAGS="${rustFlagsEnv}" export RUSTFLAGS="''${RUSTFLAGS:-""} ${rustFlagsEnv}"
''; '';
}; };
}) })

@ -28,7 +28,17 @@
owner = builtins.elemAt match 0; owner = builtins.elemAt match 0;
repo = builtins.elemAt match 1; repo = builtins.elemAt match 1;
}; };
gitGrammars = builtins.filter isGitGrammar languagesConfig.grammar; # If `use-grammars.only` is set, use only those grammars.
# If `use-grammars.except` is set, use all other grammars.
# Otherwise use all grammars.
useGrammar = grammar:
if languagesConfig?use-grammars.only then
builtins.elem grammar.name languagesConfig.use-grammars.only
else if languagesConfig?use-grammars.except then
!(builtins.elem grammar.name languagesConfig.use-grammars.except)
else true;
grammarsToUse = builtins.filter useGrammar languagesConfig.grammar;
gitGrammars = builtins.filter isGitGrammar grammarsToUse;
buildGrammar = grammar: let buildGrammar = grammar: let
gh = toGitHubFetcher grammar.source.git; gh = toGitHubFetcher grammar.source.git;
sourceGit = builtins.fetchTree { sourceGit = builtins.fetchTree {

@ -16,42 +16,49 @@ unicode-lines = ["ropey/unicode_lines"]
integration = [] integration = []
[dependencies] [dependencies]
helix-stdx = { path = "../helix-stdx" }
helix-loader = { path = "../helix-loader" } helix-loader = { path = "../helix-loader" }
ropey = { version = "1.6.1", default-features = false, features = ["simd"] } ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
smallvec = "1.11" smallvec = "1.13"
smartstring = "1.0.1" smartstring = "1.0.1"
unicode-segmentation = "1.10" unicode-segmentation = "1.12"
unicode-width = "0.1" # unicode-width is changing width definitions
# that both break our logic and disagree with common
# width definitions in terminals, we need to replace it.
# For now lets lock the version to avoid rendering glitches
# when installing without `--locked`
unicode-width = "=0.1.12"
unicode-general-category = "0.6" unicode-general-category = "0.6"
# slab = "0.4.2" slotmap.workspace = true
slotmap = "1.0"
tree-sitter.workspace = true tree-sitter.workspace = true
once_cell = "1.19" once_cell = "1.20"
arc-swap = "1" arc-swap = "1"
regex = "1" regex = "1"
bitflags = "2.4" bitflags = "2.6"
ahash = "0.8.6" ahash = "0.8.11"
hashbrown = { version = "0.14.3", features = ["raw"] } hashbrown = { version = "0.14.5", features = ["raw"] }
dunce = "1.0" dunce = "1.0"
url = "2.5.0"
log = "0.4" log = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
toml = "0.7" toml = "0.8"
imara-diff = "0.1.0" imara-diff = "0.1.7"
encoding_rs = "0.8" encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
etcetera = "0.8" etcetera = "0.8"
textwrap = "0.16.0" textwrap = "0.16.1"
nucleo.workspace = true nucleo.workspace = true
parking_lot = "0.12" parking_lot = "0.12"
globset = "0.4.15"
[dev-dependencies] [dev-dependencies]
quickcheck = { version = "1", default-features = false } quickcheck = { version = "1", default-features = false }
indoc = "2.0.4" indoc = "2.0.5"

@ -75,9 +75,9 @@ impl From<(&char, &char)> for Pair {
impl AutoPairs { impl AutoPairs {
/// Make a new AutoPairs set with the given pairs and default conditions. /// Make a new AutoPairs set with the given pairs and default conditions.
pub fn new<'a, V: 'a, A>(pairs: V) -> Self pub fn new<'a, V, A>(pairs: V) -> Self
where where
V: IntoIterator<Item = A>, V: IntoIterator<Item = A> + 'a,
A: Into<Pair>, A: Into<Pair>,
{ {
let mut auto_pairs = HashMap::new(); let mut auto_pairs = HashMap::new();

@ -1,11 +1,32 @@
//! This module contains the functionality toggle comments on lines over the selection //! This module contains the functionality toggle comments on lines over the selection
//! using the comment character defined in the user's `languages.toml` //! using the comment character defined in the user's `languages.toml`
use smallvec::SmallVec;
use crate::{ use crate::{
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction, syntax::BlockCommentToken, Change, Range, Rope, RopeSlice, Selection, Tendril, Transaction,
}; };
use helix_stdx::rope::RopeSliceExt;
use std::borrow::Cow; use std::borrow::Cow;
pub const DEFAULT_COMMENT_TOKEN: &str = "//";
/// Returns the longest matching comment token of the given line (if it exists).
pub fn get_comment_token<'a, S: AsRef<str>>(
text: RopeSlice,
tokens: &'a [S],
line_num: usize,
) -> Option<&'a str> {
let line = text.line(line_num);
let start = line.first_non_whitespace_char()?;
tokens
.iter()
.map(AsRef::as_ref)
.filter(|token| line.slice(start..).starts_with(token))
.max_by_key(|token| token.len())
}
/// Given text, a comment token, and a set of line indices, returns the following: /// Given text, a comment token, and a set of line indices, returns the following:
/// - Whether the given lines should be considered commented /// - Whether the given lines should be considered commented
/// - If any of the lines are uncommented, all lines are considered as such. /// - If any of the lines are uncommented, all lines are considered as such.
@ -22,24 +43,23 @@ fn find_line_comment(
) -> (bool, Vec<usize>, usize, usize) { ) -> (bool, Vec<usize>, usize, usize) {
let mut commented = true; let mut commented = true;
let mut to_change = Vec::new(); let mut to_change = Vec::new();
let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char let mut min = usize::MAX; // minimum col for first_non_whitespace_char
let mut margin = 1; let mut margin = 1;
let token_len = token.chars().count(); let token_len = token.chars().count();
for line in lines { for line in lines {
let line_slice = text.line(line); let line_slice = text.line(line);
if let Some(pos) = find_first_non_whitespace_char(line_slice) { if let Some(pos) = line_slice.first_non_whitespace_char() {
let len = line_slice.len_chars(); let len = line_slice.len_chars();
if pos < min { min = std::cmp::min(min, pos);
min = pos;
}
// line can be shorter than pos + token len // line can be shorter than pos + token len
let fragment = Cow::from(line_slice.slice(pos..std::cmp::min(pos + token.len(), len))); let fragment = Cow::from(line_slice.slice(pos..std::cmp::min(pos + token.len(), len)));
if fragment != token {
// as soon as one of the non-blank lines doesn't have a comment, the whole block is // as soon as one of the non-blank lines doesn't have a comment, the whole block is
// considered uncommented. // considered uncommented.
if fragment != token {
commented = false; commented = false;
} }
@ -53,6 +73,7 @@ fn find_line_comment(
to_change.push(line); to_change.push(line);
} }
} }
(commented, to_change, min, margin) (commented, to_change, min, margin)
} }
@ -60,7 +81,7 @@ fn find_line_comment(
pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction { pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction {
let text = doc.slice(..); let text = doc.slice(..);
let token = token.unwrap_or("//"); let token = token.unwrap_or(DEFAULT_COMMENT_TOKEN);
let comment = Tendril::from(format!("{} ", token)); let comment = Tendril::from(format!("{} ", token));
let mut lines: Vec<usize> = Vec::with_capacity(selection.len()); let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
@ -94,59 +115,379 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
Transaction::change(doc, changes.into_iter()) Transaction::change(doc, changes.into_iter())
} }
#[derive(Debug, PartialEq, Eq)]
pub enum CommentChange {
Commented {
range: Range,
start_pos: usize,
end_pos: usize,
start_margin: bool,
end_margin: bool,
start_token: String,
end_token: String,
},
Uncommented {
range: Range,
start_pos: usize,
end_pos: usize,
start_token: String,
end_token: String,
},
Whitespace {
range: Range,
},
}
pub fn find_block_comments(
tokens: &[BlockCommentToken],
text: RopeSlice,
selection: &Selection,
) -> (bool, Vec<CommentChange>) {
let mut commented = true;
let mut only_whitespace = true;
let mut comment_changes = Vec::with_capacity(selection.len());
let default_tokens = tokens.first().cloned().unwrap_or_default();
// TODO: check if this can be removed on MSRV bump
#[allow(clippy::redundant_clone)]
let mut start_token = default_tokens.start.clone();
#[allow(clippy::redundant_clone)]
let mut end_token = default_tokens.end.clone();
let mut tokens = tokens.to_vec();
// sort the tokens by length, so longer tokens will match first
tokens.sort_by(|a, b| {
if a.start.len() == b.start.len() {
b.end.len().cmp(&a.end.len())
} else {
b.start.len().cmp(&a.start.len())
}
});
for range in selection {
let selection_slice = range.slice(text);
if let (Some(start_pos), Some(end_pos)) = (
selection_slice.first_non_whitespace_char(),
selection_slice.last_non_whitespace_char(),
) {
let mut line_commented = false;
let mut after_start = 0;
let mut before_end = 0;
let len = (end_pos + 1) - start_pos;
for BlockCommentToken { start, end } in &tokens {
let start_len = start.chars().count();
let end_len = end.chars().count();
after_start = start_pos + start_len;
before_end = end_pos.saturating_sub(end_len);
if len >= start_len + end_len {
let start_fragment = selection_slice.slice(start_pos..after_start);
let end_fragment = selection_slice.slice(before_end + 1..end_pos + 1);
// block commented with these tokens
if start_fragment == start.as_str() && end_fragment == end.as_str() {
start_token = start.to_string();
end_token = end.to_string();
line_commented = true;
break;
}
}
}
if !line_commented {
comment_changes.push(CommentChange::Uncommented {
range: *range,
start_pos,
end_pos,
start_token: default_tokens.start.clone(),
end_token: default_tokens.end.clone(),
});
commented = false;
} else {
comment_changes.push(CommentChange::Commented {
range: *range,
start_pos,
end_pos,
start_margin: selection_slice
.get_char(after_start)
.map_or(false, |c| c == ' '),
end_margin: after_start != before_end
&& selection_slice
.get_char(before_end)
.map_or(false, |c| c == ' '),
start_token: start_token.to_string(),
end_token: end_token.to_string(),
});
}
only_whitespace = false;
} else {
comment_changes.push(CommentChange::Whitespace { range: *range });
}
}
if only_whitespace {
commented = false;
}
(commented, comment_changes)
}
#[must_use]
pub fn create_block_comment_transaction(
doc: &Rope,
selection: &Selection,
commented: bool,
comment_changes: Vec<CommentChange>,
) -> (Transaction, SmallVec<[Range; 1]>) {
let mut changes: Vec<Change> = Vec::with_capacity(selection.len() * 2);
let mut ranges: SmallVec<[Range; 1]> = SmallVec::with_capacity(selection.len());
let mut offs = 0;
for change in comment_changes {
if commented {
if let CommentChange::Commented {
range,
start_pos,
end_pos,
start_token,
end_token,
start_margin,
end_margin,
} = change
{
let from = range.from();
changes.push((
from + start_pos,
from + start_pos + start_token.len() + start_margin as usize,
None,
));
changes.push((
from + end_pos - end_token.len() - end_margin as usize + 1,
from + end_pos + 1,
None,
));
}
} else {
// uncommented so manually map ranges through changes
match change {
CommentChange::Uncommented {
range,
start_pos,
end_pos,
start_token,
end_token,
} => {
let from = range.from();
changes.push((
from + start_pos,
from + start_pos,
Some(Tendril::from(format!("{} ", start_token))),
));
changes.push((
from + end_pos + 1,
from + end_pos + 1,
Some(Tendril::from(format!(" {}", end_token))),
));
let offset = start_token.chars().count() + end_token.chars().count() + 2;
ranges.push(
Range::new(from + offs, from + offs + end_pos + 1 + offset)
.with_direction(range.direction()),
);
offs += offset;
}
CommentChange::Commented { range, .. } | CommentChange::Whitespace { range } => {
ranges.push(Range::new(range.from() + offs, range.to() + offs));
}
}
}
}
(Transaction::change(doc, changes.into_iter()), ranges)
}
#[must_use]
pub fn toggle_block_comments(
doc: &Rope,
selection: &Selection,
tokens: &[BlockCommentToken],
) -> Transaction {
let text = doc.slice(..);
let (commented, comment_changes) = find_block_comments(tokens, text, selection);
let (mut transaction, ranges) =
create_block_comment_transaction(doc, selection, commented, comment_changes);
if !commented {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
}
transaction
}
pub fn split_lines_of_selection(text: RopeSlice, selection: &Selection) -> Selection {
let mut ranges = SmallVec::new();
for range in selection.ranges() {
let (line_start, line_end) = range.line_range(text.slice(..));
let mut pos = text.line_to_char(line_start);
for line in text.slice(pos..text.line_to_char(line_end + 1)).lines() {
let start = pos;
pos += line.len_chars();
ranges.push(Range::new(start, pos));
}
}
Selection::new(ranges, 0)
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
mod find_line_comment {
use super::*;
#[test] #[test]
fn test_find_line_comment() { fn not_commented() {
// four lines, two space indented, except for line 1 which is blank. // four lines, two space indented, except for line 1 which is blank.
let mut doc = Rope::from(" 1\n\n 2\n 3"); let doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
let mut selection = Selection::single(0, doc.len_chars() - 1);
let text = doc.slice(..); let text = doc.slice(..);
let res = find_line_comment("//", text, 0..3); let res = find_line_comment("//", text, 0..3);
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 0) // (commented = false, to_change = [line 0, line 2], min = col 2, margin = 0)
assert_eq!(res, (false, vec![0, 2], 2, 0)); assert_eq!(res, (false, vec![0, 2], 2, 0));
}
#[test]
fn is_commented() {
// three lines where the second line is empty.
let doc = Rope::from("// hello\n\n// there");
let res = find_line_comment("//", doc.slice(..), 0..3);
// (commented = true, to_change = [line 0, line 2], min = col 0, margin = 1)
assert_eq!(res, (true, vec![0, 2], 0, 1));
}
}
// TODO: account for uncommenting with uneven comment indentation
mod toggle_line_comment {
use super::*;
#[test]
fn comment() {
// four lines, two space indented, except for line 1 which is blank.
let mut doc = Rope::from(" 1\n\n 2\n 3");
// select whole document
let selection = Selection::single(0, doc.len_chars() - 1);
// comment
let transaction = toggle_line_comments(&doc, &selection, None); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc); transaction.apply(&mut doc);
selection = selection.map(transaction.changes());
assert_eq!(doc, " // 1\n\n // 2\n // 3"); assert_eq!(doc, " // 1\n\n // 2\n // 3");
}
#[test]
fn uncomment() {
let mut doc = Rope::from(" // 1\n\n // 2\n // 3");
let mut selection = Selection::single(0, doc.len_chars() - 1);
// uncomment
let transaction = toggle_line_comments(&doc, &selection, None); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc); transaction.apply(&mut doc);
selection = selection.map(transaction.changes()); selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3"); assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning assert!(selection.len() == 1); // to ignore the selection unused warning
}
// 0 margin comments #[test]
doc = Rope::from(" //1\n\n //2\n //3"); fn uncomment_0_margin_comments() {
// reset the selection. let mut doc = Rope::from(" //1\n\n //2\n //3");
selection = Selection::single(0, doc.len_chars() - 1); let mut selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc); transaction.apply(&mut doc);
selection = selection.map(transaction.changes()); selection = selection.map(transaction.changes());
assert_eq!(doc, " 1\n\n 2\n 3"); assert_eq!(doc, " 1\n\n 2\n 3");
assert!(selection.len() == 1); // to ignore the selection unused warning assert!(selection.len() == 1); // to ignore the selection unused warning
}
// 0 margin comments, with no space #[test]
doc = Rope::from("//"); fn uncomment_0_margin_comments_with_no_space() {
// reset the selection. let mut doc = Rope::from("//");
selection = Selection::single(0, doc.len_chars() - 1); let mut selection = Selection::single(0, doc.len_chars() - 1);
let transaction = toggle_line_comments(&doc, &selection, None); let transaction = toggle_line_comments(&doc, &selection, None);
transaction.apply(&mut doc); transaction.apply(&mut doc);
selection = selection.map(transaction.changes()); selection = selection.map(transaction.changes());
assert_eq!(doc, ""); assert_eq!(doc, "");
assert!(selection.len() == 1); // to ignore the selection unused warning assert!(selection.len() == 1); // to ignore the selection unused warning
}
}
// TODO: account for uncommenting with uneven comment indentation #[test]
fn test_find_block_comments() {
// three lines 5 characters.
let mut doc = Rope::from("1\n2\n3");
// select whole document
let selection = Selection::single(0, doc.len_chars());
let text = doc.slice(..);
let res = find_block_comments(&[BlockCommentToken::default()], text, &selection);
assert_eq!(
res,
(
false,
vec![CommentChange::Uncommented {
range: Range::new(0, 5),
start_pos: 0,
end_pos: 4,
start_token: "/*".to_string(),
end_token: "*/".to_string(),
}]
)
);
// comment
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
transaction.apply(&mut doc);
assert_eq!(doc, "/* 1\n2\n3 */");
// uncomment
let selection = Selection::single(0, doc.len_chars());
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
transaction.apply(&mut doc);
assert_eq!(doc, "1\n2\n3");
// don't panic when there is just a space in comment
doc = Rope::from("/* */");
let selection = Selection::single(0, doc.len_chars());
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
transaction.apply(&mut doc);
assert_eq!(doc, "");
}
/// Test, if `get_comment_tokens` works, even if the content of the file includes chars, whose
/// byte size unequal the amount of chars
#[test]
fn test_get_comment_with_char_boundaries() {
let rope = Rope::from("··");
let tokens = ["//", "///"];
assert_eq!(
super::get_comment_token(rope.slice(..), tokens.as_slice(), 0),
None
);
}
/// Test for `get_comment_token`.
///
/// Assuming the comment tokens are stored as `["///", "//"]`, `get_comment_token` should still
/// return `///` instead of `//` if the user is in a doc-comment section.
#[test]
fn test_use_longest_comment() {
let text = Rope::from(" /// amogus");
let tokens = ["///", "//"];
assert_eq!(
super::get_comment_token(text.slice(..), tokens.as_slice(), 0),
Some("///")
);
} }
} }

@ -1,10 +1,45 @@
/// Syntax configuration loader based on built-in languages.toml. use crate::syntax::{Configuration, Loader, LoaderError};
pub fn default_syntax_loader() -> crate::syntax::Configuration {
/// Language configuration based on built-in languages.toml.
pub fn default_lang_config() -> Configuration {
helix_loader::config::default_lang_config() helix_loader::config::default_lang_config()
.try_into() .try_into()
.expect("Could not serialize built-in languages.toml") .expect("Could not deserialize built-in languages.toml")
} }
/// Syntax configuration loader based on user configured languages.toml.
pub fn user_syntax_loader() -> Result<crate::syntax::Configuration, toml::de::Error> { /// Language configuration loader based on built-in languages.toml.
pub fn default_lang_loader() -> Loader {
Loader::new(default_lang_config()).expect("Could not compile loader for default config")
}
#[derive(Debug)]
pub enum LanguageLoaderError {
DeserializeError(toml::de::Error),
LoaderError(LoaderError),
}
impl std::fmt::Display for LanguageLoaderError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::DeserializeError(err) => write!(f, "Failed to parse language config: {err}"),
Self::LoaderError(err) => write!(f, "Failed to compile language config: {err}"),
}
}
}
impl std::error::Error for LanguageLoaderError {}
/// Language configuration based on user configured languages.toml.
pub fn user_lang_config() -> Result<Configuration, toml::de::Error> {
helix_loader::config::user_lang_config()?.try_into() helix_loader::config::user_lang_config()?.try_into()
} }
/// Language configuration loader based on user configured languages.toml.
pub fn user_lang_loader() -> Result<Loader, LanguageLoaderError> {
let config: Configuration = helix_loader::config::user_lang_config()
.map_err(LanguageLoaderError::DeserializeError)?
.try_into()
.map_err(LanguageLoaderError::DeserializeError)?;
Loader::new(config).map_err(LanguageLoaderError::LoaderError)
}

@ -1,8 +1,11 @@
//! LSP diagnostic utility types. //! LSP diagnostic utility types.
use std::fmt;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
/// Describes the severity level of a [`Diagnostic`]. /// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Deserialize, Serialize)] #[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Severity { pub enum Severity {
Hint, Hint,
Info, Info,
@ -23,6 +26,12 @@ pub struct Range {
pub end: usize, pub end: usize,
} }
impl Range {
pub fn contains(self, pos: usize) -> bool {
(self.start..self.end).contains(&pos)
}
}
#[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)] #[derive(Debug, Eq, Hash, PartialEq, Clone, Deserialize, Serialize)]
pub enum NumberOrString { pub enum NumberOrString {
Number(i32), Number(i32),
@ -39,12 +48,40 @@ pub enum DiagnosticTag {
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct Diagnostic { pub struct Diagnostic {
pub range: Range, pub range: Range,
// whether this diagnostic ends at the end of(or inside) a word
pub ends_at_word: bool,
pub starts_at_word: bool,
pub zero_width: bool,
pub line: usize, pub line: usize,
pub message: String, pub message: String,
pub severity: Option<Severity>, pub severity: Option<Severity>,
pub code: Option<NumberOrString>, pub code: Option<NumberOrString>,
pub language_server_id: usize, pub provider: DiagnosticProvider,
pub tags: Vec<DiagnosticTag>, pub tags: Vec<DiagnosticTag>,
pub source: Option<String>, pub source: Option<String>,
pub data: Option<serde_json::Value>, pub data: Option<serde_json::Value>,
} }
// TODO turn this into an enum + feature flag when lsp becomes optional
pub type DiagnosticProvider = LanguageServerId;
// while I would prefer having this in helix-lsp that necessitates a bunch of
// conversions I would rather not add. I think its fine since this just a very
// trivial newtype wrapper and we would need something similar once we define
// completions in core
slotmap::new_key_type! {
pub struct LanguageServerId;
}
impl fmt::Display for LanguageServerId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}
impl Diagnostic {
#[inline]
pub fn severity(&self) -> Severity {
self.severity.unwrap_or(Severity::Warning)
}
}

@ -10,8 +10,9 @@
//! called a "block" and the caller must advance it as needed. //! called a "block" and the caller must advance it as needed.
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Ordering;
use std::fmt::Debug; use std::fmt::Debug;
use std::mem::{replace, take}; use std::mem::replace;
#[cfg(test)] #[cfg(test)]
mod test; mod test;
@ -37,52 +38,104 @@ pub enum GraphemeSource {
}, },
} }
impl GraphemeSource {
/// Returns whether this grapheme is virtual inline text
pub fn is_virtual(self) -> bool {
matches!(self, GraphemeSource::VirtualText { .. })
}
pub fn is_eof(self) -> bool {
// all doc chars except the EOF char have non-zero codepoints
matches!(self, GraphemeSource::Document { codepoints: 0 })
}
pub fn doc_chars(self) -> usize {
match self {
GraphemeSource::Document { codepoints } => codepoints as usize,
GraphemeSource::VirtualText { .. } => 0,
}
}
}
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct FormattedGrapheme<'a> { pub struct FormattedGrapheme<'a> {
pub grapheme: Grapheme<'a>, pub raw: Grapheme<'a>,
pub source: GraphemeSource, pub source: GraphemeSource,
pub visual_pos: Position,
/// Document line at the start of the grapheme
pub line_idx: usize,
/// Document char position at the start of the grapheme
pub char_idx: usize,
}
impl FormattedGrapheme<'_> {
pub fn is_virtual(&self) -> bool {
self.source.is_virtual()
}
pub fn doc_chars(&self) -> usize {
self.source.doc_chars()
}
pub fn is_whitespace(&self) -> bool {
self.raw.is_whitespace()
} }
impl<'a> FormattedGrapheme<'a> { pub fn width(&self) -> usize {
pub fn new( self.raw.width()
}
pub fn is_word_boundary(&self) -> bool {
self.raw.is_word_boundary()
}
}
#[derive(Debug, Clone)]
struct GraphemeWithSource<'a> {
grapheme: Grapheme<'a>,
source: GraphemeSource,
}
impl<'a> GraphemeWithSource<'a> {
fn new(
g: GraphemeStr<'a>, g: GraphemeStr<'a>,
visual_x: usize, visual_x: usize,
tab_width: u16, tab_width: u16,
source: GraphemeSource, source: GraphemeSource,
) -> FormattedGrapheme<'a> { ) -> GraphemeWithSource<'a> {
FormattedGrapheme { GraphemeWithSource {
grapheme: Grapheme::new(g, visual_x, tab_width), grapheme: Grapheme::new(g, visual_x, tab_width),
source, source,
} }
} }
/// Returns whether this grapheme is virtual inline text fn placeholder() -> Self {
pub fn is_virtual(&self) -> bool { GraphemeWithSource {
matches!(self.source, GraphemeSource::VirtualText { .. })
}
pub fn placeholder() -> Self {
FormattedGrapheme {
grapheme: Grapheme::Other { g: " ".into() }, grapheme: Grapheme::Other { g: " ".into() },
source: GraphemeSource::Document { codepoints: 0 }, source: GraphemeSource::Document { codepoints: 0 },
} }
} }
pub fn doc_chars(&self) -> usize { fn doc_chars(&self) -> usize {
match self.source { self.source.doc_chars()
GraphemeSource::Document { codepoints } => codepoints as usize,
GraphemeSource::VirtualText { .. } => 0,
}
} }
pub fn is_whitespace(&self) -> bool { fn is_whitespace(&self) -> bool {
self.grapheme.is_whitespace() self.grapheme.is_whitespace()
} }
pub fn width(&self) -> usize { fn is_newline(&self) -> bool {
matches!(self.grapheme, Grapheme::Newline)
}
fn is_eof(&self) -> bool {
self.source.is_eof()
}
fn width(&self) -> usize {
self.grapheme.width() self.grapheme.width()
} }
pub fn is_word_boundary(&self) -> bool { fn is_word_boundary(&self) -> bool {
self.grapheme.is_word_boundary() self.grapheme.is_word_boundary()
} }
} }
@ -96,6 +149,7 @@ pub struct TextFormat {
pub wrap_indicator: Box<str>, pub wrap_indicator: Box<str>,
pub wrap_indicator_highlight: Option<Highlight>, pub wrap_indicator_highlight: Option<Highlight>,
pub viewport_width: u16, pub viewport_width: u16,
pub soft_wrap_at_text_width: bool,
} }
// test implementation is basically only used for testing or when softwrap is always disabled // test implementation is basically only used for testing or when softwrap is always disabled
@ -109,6 +163,7 @@ impl Default for TextFormat {
wrap_indicator: Box::from(" "), wrap_indicator: Box::from(" "),
viewport_width: 17, viewport_width: 17,
wrap_indicator_highlight: None, wrap_indicator_highlight: None,
soft_wrap_at_text_width: false,
} }
} }
} }
@ -116,7 +171,7 @@ impl Default for TextFormat {
#[derive(Debug)] #[derive(Debug)]
pub struct DocumentFormatter<'t> { pub struct DocumentFormatter<'t> {
text_fmt: &'t TextFormat, text_fmt: &'t TextFormat,
annotations: &'t TextAnnotations, annotations: &'t TextAnnotations<'t>,
/// The visual position at the end of the last yielded word boundary /// The visual position at the end of the last yielded word boundary
visual_pos: Position, visual_pos: Position,
@ -127,10 +182,7 @@ pub struct DocumentFormatter<'t> {
line_pos: usize, line_pos: usize,
exhausted: bool, exhausted: bool,
/// Line breaks to be reserved for virtual text inline_annotation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
/// at the next line break
virtual_lines: usize,
inline_anntoation_graphemes: Option<(Graphemes<'t>, Option<Highlight>)>,
// softwrap specific // softwrap specific
/// The indentation of the current line /// The indentation of the current line
@ -139,9 +191,9 @@ pub struct DocumentFormatter<'t> {
indent_level: Option<usize>, indent_level: Option<usize>,
/// In case a long word needs to be split a single grapheme might need to be wrapped /// In case a long word needs to be split a single grapheme might need to be wrapped
/// while the rest of the word stays on the same line /// while the rest of the word stays on the same line
peeked_grapheme: Option<(FormattedGrapheme<'t>, usize)>, peeked_grapheme: Option<GraphemeWithSource<'t>>,
/// A first-in first-out (fifo) buffer for the Graphemes of any given word /// A first-in first-out (fifo) buffer for the Graphemes of any given word
word_buf: Vec<FormattedGrapheme<'t>>, word_buf: Vec<GraphemeWithSource<'t>>,
/// The index of the next grapheme that will be yielded from the `word_buf` /// The index of the next grapheme that will be yielded from the `word_buf`
word_i: usize, word_i: usize,
} }
@ -157,12 +209,12 @@ impl<'t> DocumentFormatter<'t> {
text_fmt: &'t TextFormat, text_fmt: &'t TextFormat,
annotations: &'t TextAnnotations, annotations: &'t TextAnnotations,
char_idx: usize, char_idx: usize,
) -> (Self, usize) { ) -> Self {
// TODO divide long lines into blocks to avoid bad performance for long lines // TODO divide long lines into blocks to avoid bad performance for long lines
let block_line_idx = text.char_to_line(char_idx.min(text.len_chars())); let block_line_idx = text.char_to_line(char_idx.min(text.len_chars()));
let block_char_idx = text.line_to_char(block_line_idx); let block_char_idx = text.line_to_char(block_line_idx);
annotations.reset_pos(block_char_idx); annotations.reset_pos(block_char_idx);
(
DocumentFormatter { DocumentFormatter {
text_fmt, text_fmt,
annotations, annotations,
@ -170,22 +222,22 @@ impl<'t> DocumentFormatter<'t> {
graphemes: RopeGraphemes::new(text.slice(block_char_idx..)), graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
char_pos: block_char_idx, char_pos: block_char_idx,
exhausted: false, exhausted: false,
virtual_lines: 0,
indent_level: None, indent_level: None,
peeked_grapheme: None, peeked_grapheme: None,
word_buf: Vec::with_capacity(64), word_buf: Vec::with_capacity(64),
word_i: 0, word_i: 0,
line_pos: block_line_idx, line_pos: block_line_idx,
inline_anntoation_graphemes: None, inline_annotation_graphemes: None,
}, }
block_char_idx,
)
} }
fn next_inline_annotation_grapheme(&mut self) -> Option<(&'t str, Option<Highlight>)> { fn next_inline_annotation_grapheme(
&mut self,
char_pos: usize,
) -> Option<(&'t str, Option<Highlight>)> {
loop { loop {
if let Some(&mut (ref mut annotation, highlight)) = if let Some(&mut (ref mut annotation, highlight)) =
self.inline_anntoation_graphemes.as_mut() self.inline_annotation_graphemes.as_mut()
{ {
if let Some(grapheme) = annotation.next() { if let Some(grapheme) = annotation.next() {
return Some((grapheme, highlight)); return Some((grapheme, highlight));
@ -193,9 +245,9 @@ impl<'t> DocumentFormatter<'t> {
} }
if let Some((annotation, highlight)) = if let Some((annotation, highlight)) =
self.annotations.next_inline_annotation_at(self.char_pos) self.annotations.next_inline_annotation_at(char_pos)
{ {
self.inline_anntoation_graphemes = Some(( self.inline_annotation_graphemes = Some((
UnicodeSegmentation::graphemes(&*annotation.text, true), UnicodeSegmentation::graphemes(&*annotation.text, true),
highlight, highlight,
)) ))
@ -205,21 +257,19 @@ impl<'t> DocumentFormatter<'t> {
} }
} }
fn advance_grapheme(&mut self, col: usize) -> Option<FormattedGrapheme<'t>> { fn advance_grapheme(&mut self, col: usize, char_pos: usize) -> Option<GraphemeWithSource<'t>> {
let (grapheme, source) = let (grapheme, source) =
if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme() { if let Some((grapheme, highlight)) = self.next_inline_annotation_grapheme(char_pos) {
(grapheme.into(), GraphemeSource::VirtualText { highlight }) (grapheme.into(), GraphemeSource::VirtualText { highlight })
} else if let Some(grapheme) = self.graphemes.next() { } else if let Some(grapheme) = self.graphemes.next() {
self.virtual_lines += self.annotations.annotation_lines_at(self.char_pos);
let codepoints = grapheme.len_chars() as u32; let codepoints = grapheme.len_chars() as u32;
let overlay = self.annotations.overlay_at(self.char_pos); let overlay = self.annotations.overlay_at(char_pos);
let grapheme = match overlay { let grapheme = match overlay {
Some((overlay, _)) => overlay.grapheme.as_str().into(), Some((overlay, _)) => overlay.grapheme.as_str().into(),
None => Cow::from(grapheme).into(), None => Cow::from(grapheme).into(),
}; };
self.char_pos += codepoints as usize;
(grapheme, GraphemeSource::Document { codepoints }) (grapheme, GraphemeSource::Document { codepoints })
} else { } else {
if self.exhausted { if self.exhausted {
@ -228,19 +278,19 @@ impl<'t> DocumentFormatter<'t> {
self.exhausted = true; self.exhausted = true;
// EOF grapheme is required for rendering // EOF grapheme is required for rendering
// and correct position computations // and correct position computations
return Some(FormattedGrapheme { return Some(GraphemeWithSource {
grapheme: Grapheme::Other { g: " ".into() }, grapheme: Grapheme::Other { g: " ".into() },
source: GraphemeSource::Document { codepoints: 0 }, source: GraphemeSource::Document { codepoints: 0 },
}); });
}; };
let grapheme = FormattedGrapheme::new(grapheme, col, self.text_fmt.tab_width, source); let grapheme = GraphemeWithSource::new(grapheme, col, self.text_fmt.tab_width, source);
Some(grapheme) Some(grapheme)
} }
/// Move a word to the next visual line /// Move a word to the next visual line
fn wrap_word(&mut self, virtual_lines_before_word: usize) -> usize { fn wrap_word(&mut self) -> usize {
// softwrap this word to the next line // softwrap this word to the next line
let indent_carry_over = if let Some(indent) = self.indent_level { let indent_carry_over = if let Some(indent) = self.indent_level {
if indent as u16 <= self.text_fmt.max_indent_retain { if indent as u16 <= self.text_fmt.max_indent_retain {
@ -254,15 +304,17 @@ impl<'t> DocumentFormatter<'t> {
0 0
}; };
let virtual_lines =
self.annotations
.virtual_lines_at(self.char_pos, self.visual_pos, self.line_pos);
self.visual_pos.col = indent_carry_over as usize; self.visual_pos.col = indent_carry_over as usize;
self.virtual_lines -= virtual_lines_before_word; self.visual_pos.row += 1 + virtual_lines;
self.visual_pos.row += 1 + virtual_lines_before_word;
let mut i = 0; let mut i = 0;
let mut word_width = 0; let mut word_width = 0;
let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true) let wrap_indicator = UnicodeSegmentation::graphemes(&*self.text_fmt.wrap_indicator, true)
.map(|g| { .map(|g| {
i += 1; i += 1;
let grapheme = FormattedGrapheme::new( let grapheme = GraphemeWithSource::new(
g.into(), g.into(),
self.visual_pos.col + word_width, self.visual_pos.col + word_width,
self.text_fmt.tab_width, self.text_fmt.tab_width,
@ -282,46 +334,71 @@ impl<'t> DocumentFormatter<'t> {
.change_position(visual_x, self.text_fmt.tab_width); .change_position(visual_x, self.text_fmt.tab_width);
word_width += grapheme.width(); word_width += grapheme.width();
} }
if let Some(grapheme) = &mut self.peeked_grapheme {
let visual_x = self.visual_pos.col + word_width;
grapheme
.grapheme
.change_position(visual_x, self.text_fmt.tab_width);
}
word_width word_width
} }
fn peek_grapheme(&mut self, col: usize, char_pos: usize) -> Option<&GraphemeWithSource<'t>> {
if self.peeked_grapheme.is_none() {
self.peeked_grapheme = self.advance_grapheme(col, char_pos);
}
self.peeked_grapheme.as_ref()
}
fn next_grapheme(&mut self, col: usize, char_pos: usize) -> Option<GraphemeWithSource<'t>> {
self.peek_grapheme(col, char_pos);
self.peeked_grapheme.take()
}
fn advance_to_next_word(&mut self) { fn advance_to_next_word(&mut self) {
self.word_buf.clear(); self.word_buf.clear();
let mut word_width = 0; let mut word_width = 0;
let virtual_lines_before_word = self.virtual_lines; let mut word_chars = 0;
let mut virtual_lines_before_grapheme = self.virtual_lines;
loop { if self.exhausted {
// softwrap word if necessary
if word_width + self.visual_pos.col >= self.text_fmt.viewport_width as usize {
// wrapping this word would move too much text to the next line
// split the word at the line end instead
if word_width > self.text_fmt.max_wrap as usize {
// Usually we stop accomulating graphemes as soon as softwrapping becomes necessary.
// However if the last grapheme is multiple columns wide it might extend beyond the EOL.
// The condition below ensures that this grapheme is not cutoff and instead wrapped to the next line
if word_width + self.visual_pos.col > self.text_fmt.viewport_width as usize {
self.peeked_grapheme = self.word_buf.pop().map(|grapheme| {
(grapheme, self.virtual_lines - virtual_lines_before_grapheme)
});
self.virtual_lines = virtual_lines_before_grapheme;
}
return; return;
} }
word_width = self.wrap_word(virtual_lines_before_word); loop {
let mut col = self.visual_pos.col + word_width;
let char_pos = self.char_pos + word_chars;
match col.cmp(&(self.text_fmt.viewport_width as usize)) {
// The EOF char and newline chars are always selectable in helix. That means
// that wrapping happens "too-early" if a word fits a line perfectly. This
// is intentional so that all selectable graphemes are always visisble (and
// therefore the cursor never dissapears). However if the user manually set a
// lower softwrap width then this is undesirable. Just increasing the viewport-
// width by one doesn't work because if a line is wrapped multiple times then
// some words may extend past the specified width.
//
// So we special case a word that ends exactly at line bounds and is followed
// by a newline/eof character here.
Ordering::Equal
if self.text_fmt.soft_wrap_at_text_width
&& self.peek_grapheme(col, char_pos).map_or(false, |grapheme| {
grapheme.is_newline() || grapheme.is_eof()
}) => {}
Ordering::Equal if word_width > self.text_fmt.max_wrap as usize => return,
Ordering::Greater if word_width > self.text_fmt.max_wrap as usize => {
self.peeked_grapheme = self.word_buf.pop();
return;
}
Ordering::Equal | Ordering::Greater => {
word_width = self.wrap_word();
col = self.visual_pos.col + word_width;
}
Ordering::Less => (),
} }
virtual_lines_before_grapheme = self.virtual_lines; let Some(grapheme) = self.next_grapheme(col, char_pos) else {
let grapheme = if let Some((grapheme, virtual_lines)) = self.peeked_grapheme.take() {
self.virtual_lines += virtual_lines;
grapheme
} else if let Some(grapheme) = self.advance_grapheme(self.visual_pos.col + word_width) {
grapheme
} else {
return; return;
}; };
word_chars += grapheme.doc_chars();
// Track indentation // Track indentation
if !grapheme.is_whitespace() && self.indent_level.is_none() { if !grapheme.is_whitespace() && self.indent_level.is_none() {
@ -340,19 +417,18 @@ impl<'t> DocumentFormatter<'t> {
} }
} }
/// returns the document line pos of the **next** grapheme that will be yielded /// returns the char index at the end of the last yielded grapheme
pub fn line_pos(&self) -> usize { pub fn next_char_pos(&self) -> usize {
self.line_pos self.char_pos
} }
/// returns the visual position at the end of the last yielded grapheme
/// returns the visual pos of the **next** grapheme that will be yielded pub fn next_visual_pos(&self) -> Position {
pub fn visual_pos(&self) -> Position {
self.visual_pos self.visual_pos
} }
} }
impl<'t> Iterator for DocumentFormatter<'t> { impl<'t> Iterator for DocumentFormatter<'t> {
type Item = (FormattedGrapheme<'t>, Position); type Item = FormattedGrapheme<'t>;
fn next(&mut self) -> Option<Self::Item> { fn next(&mut self) -> Option<Self::Item> {
let grapheme = if self.text_fmt.soft_wrap { let grapheme = if self.text_fmt.soft_wrap {
@ -362,23 +438,40 @@ impl<'t> Iterator for DocumentFormatter<'t> {
} }
let grapheme = replace( let grapheme = replace(
self.word_buf.get_mut(self.word_i)?, self.word_buf.get_mut(self.word_i)?,
FormattedGrapheme::placeholder(), GraphemeWithSource::placeholder(),
); );
self.word_i += 1; self.word_i += 1;
grapheme grapheme
} else { } else {
self.advance_grapheme(self.visual_pos.col)? self.advance_grapheme(self.visual_pos.col, self.char_pos)?
}; };
let pos = self.visual_pos; let grapheme = FormattedGrapheme {
if grapheme.grapheme == Grapheme::Newline { raw: grapheme.grapheme,
self.visual_pos.row += 1; source: grapheme.source,
self.visual_pos.row += take(&mut self.virtual_lines); visual_pos: self.visual_pos,
line_idx: self.line_pos,
char_idx: self.char_pos,
};
self.char_pos += grapheme.doc_chars();
if !grapheme.is_virtual() {
self.annotations.process_virtual_text_anchors(&grapheme);
}
if grapheme.raw == Grapheme::Newline {
// move to end of newline char
self.visual_pos.col += 1;
let virtual_lines =
self.annotations
.virtual_lines_at(self.char_pos, self.visual_pos, self.line_pos);
self.visual_pos.row += 1 + virtual_lines;
self.visual_pos.col = 0; self.visual_pos.col = 0;
if !grapheme.is_virtual() {
self.line_pos += 1; self.line_pos += 1;
}
} else { } else {
self.visual_pos.col += grapheme.width(); self.visual_pos.col += grapheme.width();
} }
Some((grapheme, pos)) Some(grapheme)
} }
} }

@ -1,5 +1,3 @@
use std::rc::Rc;
use crate::doc_formatter::{DocumentFormatter, TextFormat}; use crate::doc_formatter::{DocumentFormatter, TextFormat};
use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations}; use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations};
@ -14,6 +12,7 @@ impl TextFormat {
wrap_indicator_highlight: None, wrap_indicator_highlight: None,
// use a prime number to allow lining up too often with repeat // use a prime number to allow lining up too often with repeat
viewport_width: 17, viewport_width: 17,
soft_wrap_at_text_width: false,
} }
} }
} }
@ -23,20 +22,23 @@ impl<'t> DocumentFormatter<'t> {
use std::fmt::Write; use std::fmt::Write;
let mut res = String::new(); let mut res = String::new();
let viewport_width = self.text_fmt.viewport_width; let viewport_width = self.text_fmt.viewport_width;
let soft_wrap_at_text_width = self.text_fmt.soft_wrap_at_text_width;
let mut line = 0; let mut line = 0;
for (grapheme, pos) in self { for grapheme in self {
if pos.row != line { if grapheme.visual_pos.row != line {
line += 1; line += 1;
assert_eq!(pos.row, line); assert_eq!(grapheme.visual_pos.row, line);
write!(res, "\n{}", ".".repeat(pos.col)).unwrap(); write!(res, "\n{}", ".".repeat(grapheme.visual_pos.col)).unwrap();
}
if !soft_wrap_at_text_width {
assert!( assert!(
pos.col <= viewport_width as usize, grapheme.visual_pos.col <= viewport_width as usize,
"softwrapped failed {}<={viewport_width}", "softwrapped failed {}<={viewport_width}",
pos.col grapheme.visual_pos.col
); );
} }
write!(res, "{}", grapheme.grapheme).unwrap(); write!(res, "{}", grapheme.raw).unwrap();
} }
res res
@ -50,7 +52,6 @@ fn softwrap_text(text: &str) -> String {
&TextAnnotations::default(), &TextAnnotations::default(),
0, 0,
) )
.0
.collect_to_str() .collect_to_str()
} }
@ -101,14 +102,29 @@ fn long_word_softwrap() {
); );
} }
fn softwrap_text_at_text_width(text: &str) -> String {
let mut text_fmt = TextFormat::new_test(true);
text_fmt.soft_wrap_at_text_width = true;
let annotations = TextAnnotations::default();
let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text.into(), &text_fmt, &annotations, 0);
formatter.collect_to_str()
}
#[test]
fn long_word_softwrap_text_width() {
assert_eq!(
softwrap_text_at_text_width("xxxxxxxx1xxxx2xxx\nxxxxxxxx1xxxx2xxx"),
"xxxxxxxx1xxxx2xxx \nxxxxxxxx1xxxx2xxx "
);
}
fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String { fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay]) -> String {
DocumentFormatter::new_at_prev_checkpoint( DocumentFormatter::new_at_prev_checkpoint(
text.into(), text.into(),
&TextFormat::new_test(softwrap), &TextFormat::new_test(softwrap),
TextAnnotations::default().add_overlay(overlays.into(), None), TextAnnotations::default().add_overlay(overlays, None),
char_pos, char_pos,
) )
.0
.collect_to_str() .collect_to_str()
} }
@ -142,10 +158,9 @@ fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -
DocumentFormatter::new_at_prev_checkpoint( DocumentFormatter::new_at_prev_checkpoint(
text.into(), text.into(),
&TextFormat::new_test(softwrap), &TextFormat::new_test(softwrap),
TextAnnotations::default().add_inline_annotations(annotations.into(), None), TextAnnotations::default().add_inline_annotations(annotations, None),
0, 0,
) )
.0
.collect_to_str() .collect_to_str()
} }
@ -164,18 +179,26 @@ fn annotation() {
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo " "foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
); );
} }
#[test] #[test]
fn annotation_and_overlay() { fn annotation_and_overlay() {
let annotations = [InlineAnnotation {
char_idx: 0,
text: "fooo".into(),
}];
let overlay = [Overlay {
char_idx: 0,
grapheme: "\t".into(),
}];
assert_eq!( assert_eq!(
DocumentFormatter::new_at_prev_checkpoint( DocumentFormatter::new_at_prev_checkpoint(
"bbar".into(), "bbar".into(),
&TextFormat::new_test(false), &TextFormat::new_test(false),
TextAnnotations::default() TextAnnotations::default()
.add_inline_annotations(Rc::new([InlineAnnotation::new(0, "fooo")]), None) .add_inline_annotations(annotations.as_slice(), None)
.add_overlay(Rc::new([Overlay::new(0, "\t")]), None), .add_overlay(overlay.as_slice(), None),
0, 0,
) )
.0
.collect_to_str(), .collect_to_str(),
"fooo bar " "fooo bar "
); );

@ -1,6 +1,6 @@
use std::ops::DerefMut; use std::ops::DerefMut;
use nucleo::pattern::{Atom, AtomKind, CaseMatching}; use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
use nucleo::Config; use nucleo::Config;
use parking_lot::Mutex; use parking_lot::Mutex;
@ -38,6 +38,12 @@ pub fn fuzzy_match<T: AsRef<str>>(
if path { if path {
matcher.config.set_match_paths(); matcher.config.set_match_paths();
} }
let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false); let pattern = Atom::new(
pattern,
CaseMatching::Smart,
Normalization::Smart,
AtomKind::Fuzzy,
false,
);
pattern.match_list(items, &mut matcher) pattern.match_list(items, &mut matcher)
} }

@ -28,6 +28,11 @@ pub enum Grapheme<'a> {
} }
impl<'a> Grapheme<'a> { impl<'a> Grapheme<'a> {
pub fn new_decoration(g: &'static str) -> Grapheme<'a> {
assert_ne!(g, "\t");
Grapheme::new(g.into(), 0, 0)
}
pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> { pub fn new(g: GraphemeStr<'a>, visual_x: usize, tab_width: u16) -> Grapheme<'a> {
match g { match g {
g if g == "\t" => Grapheme::Tab { g if g == "\t" => Grapheme::Tab {
@ -278,23 +283,6 @@ 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. /// Returns whether the given char position is a grapheme boundary.
#[must_use] #[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool { pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
@ -425,6 +413,85 @@ impl<'a> Iterator for RopeGraphemes<'a> {
} }
} }
/// An iterator over the graphemes of a `RopeSlice` in reverse.
#[derive(Clone)]
pub struct RevRopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
cursor: GraphemeCursor,
}
impl<'a> fmt::Debug for RevRopeGraphemes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RevRopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
// .field("cursor", &self.cursor)
.finish()
}
}
impl<'a> RevRopeGraphemes<'a> {
#[must_use]
pub fn new(slice: RopeSlice) -> RevRopeGraphemes {
let (mut chunks, mut cur_chunk_start, _, _) = slice.chunks_at_byte(slice.len_bytes());
chunks.reverse();
let first_chunk = chunks.next().unwrap_or("");
cur_chunk_start -= first_chunk.len();
RevRopeGraphemes {
text: slice,
chunks,
cur_chunk: first_chunk,
cur_chunk_start,
cursor: GraphemeCursor::new(slice.len_bytes(), slice.len_bytes(), true),
}
}
}
impl<'a> Iterator for RevRopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
self.cur_chunk = self.chunks.next().unwrap_or("");
self.cur_chunk_start -= self.cur_chunk.len();
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a >= self.cur_chunk_start + self.cur_chunk.len() {
Some(self.text.byte_slice(b..a))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[b2..a2]).into())
}
}
}
/// A highly compressed Cow<'a, str> that holds /// A highly compressed Cow<'a, str> that holds
/// atmost u31::MAX bytes and is readonly /// atmost u31::MAX bytes and is readonly
pub struct GraphemeStr<'a> { pub struct GraphemeStr<'a> {

@ -27,7 +27,7 @@ pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
let date_time = NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?; let date_time = NaiveDateTime::parse_from_str(date_time, format.fmt).ok()?;
Some( Some(
date_time date_time
.checked_add_signed(Duration::minutes(amount))? .checked_add_signed(Duration::try_minutes(amount)?)?
.format(format.fmt) .format(format.fmt)
.to_string(), .to_string(),
) )
@ -35,14 +35,15 @@ pub fn increment(selected_text: &str, amount: i64) -> Option<String> {
(true, false) => { (true, false) => {
let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?; let date = NaiveDate::parse_from_str(date_time, format.fmt).ok()?;
Some( Some(
date.checked_add_signed(Duration::days(amount))? date.checked_add_signed(Duration::try_days(amount)?)?
.format(format.fmt) .format(format.fmt)
.to_string(), .to_string(),
) )
} }
(false, true) => { (false, true) => {
let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?; let time = NaiveTime::parse_from_str(date_time, format.fmt).ok()?;
let (adjusted_time, _) = time.overflowing_add_signed(Duration::minutes(amount)); let (adjusted_time, _) =
time.overflowing_add_signed(Duration::try_minutes(amount)?);
Some(adjusted_time.format(format.fmt).to_string()) Some(adjusted_time.format(format.fmt).to_string())
} }
(false, false) => None, (false, false) => None,

@ -1,10 +1,10 @@
use std::{borrow::Cow, collections::HashMap}; use std::{borrow::Cow, collections::HashMap};
use helix_stdx::rope::RopeSliceExt;
use tree_sitter::{Query, QueryCursor, QueryPredicateArg}; use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use crate::{ use crate::{
chars::{char_is_line_ending, char_is_whitespace}, chars::{char_is_line_ending, char_is_whitespace},
find_first_non_whitespace_char,
graphemes::{grapheme_width, tab_width_at}, graphemes::{grapheme_width, tab_width_at},
syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax}, syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node, tree_sitter::Node,
@ -247,41 +247,18 @@ fn add_indent_level(
} }
} }
/// Computes for node and all ancestors whether they are the first node on their line. /// Return true if only whitespace comes before the node on its line.
/// The first entry in the return value represents the root node, the last one the node itself /// If given, new_line_byte_pos is treated the same way as any existing newline.
fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> { fn is_first_in_line(node: Node, text: RopeSlice, new_line_byte_pos: Option<usize>) -> bool {
let mut first_in_line = Vec::new(); let mut line_start_byte_pos = text.line_to_byte(node.start_position().row);
loop { if let Some(pos) = new_line_byte_pos {
if let Some(prev) = node.prev_sibling() { if line_start_byte_pos < pos && pos <= node.start_byte() {
// If we insert a new line, the first node at/after the cursor is considered to be the first in its line line_start_byte_pos = pos;
let first = prev.end_position().row != node.start_position().row
|| new_line_byte_pos.map_or(false, |byte_pos| {
node.start_byte() >= byte_pos && prev.start_byte() < byte_pos
});
first_in_line.push(Some(first));
} else {
// Nodes that have no previous siblings are first in their line if and only if their parent is
// (which we don't know yet)
first_in_line.push(None);
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
}
} }
let mut result = Vec::with_capacity(first_in_line.len());
let mut parent_is_first = true; // The root node is by definition the first node in its line
for first in first_in_line.into_iter().rev() {
if let Some(first) = first {
result.push(first);
parent_is_first = first;
} else {
result.push(parent_is_first);
} }
} text.byte_slice(line_start_byte_pos..node.start_byte())
result .chars()
.all(|c| c.is_whitespace())
} }
/// The total indent for some line of code. /// The total indent for some line of code.
@ -480,7 +457,7 @@ fn query_indents<'a>(
// Skip matches where not all custom predicates are fulfilled // Skip matches where not all custom predicates are fulfilled
if !query.general_predicates(m.pattern_index).iter().all(|pred| { if !query.general_predicates(m.pattern_index).iter().all(|pred| {
match pred.operator.as_ref() { match pred.operator.as_ref() {
"not-kind-eq?" => match (pred.args.get(0), pred.args.get(1)) { "not-kind-eq?" => match (pred.args.first(), pred.args.get(1)) {
( (
Some(QueryPredicateArg::Capture(capture_idx)), Some(QueryPredicateArg::Capture(capture_idx)),
Some(QueryPredicateArg::String(kind)), Some(QueryPredicateArg::String(kind)),
@ -496,7 +473,7 @@ fn query_indents<'a>(
} }
}, },
"same-line?" | "not-same-line?" => { "same-line?" | "not-same-line?" => {
match (pred.args.get(0), pred.args.get(1)) { match (pred.args.first(), pred.args.get(1)) {
( (
Some(QueryPredicateArg::Capture(capt1)), Some(QueryPredicateArg::Capture(capt1)),
Some(QueryPredicateArg::Capture(capt2)) Some(QueryPredicateArg::Capture(capt2))
@ -518,7 +495,7 @@ fn query_indents<'a>(
} }
} }
} }
"one-line?" | "not-one-line?" => match pred.args.get(0) { "one-line?" | "not-one-line?" => match pred.args.first() {
Some(QueryPredicateArg::Capture(capture_idx)) => { Some(QueryPredicateArg::Capture(capture_idx)) => {
let node = m.nodes_for_capture_index(*capture_idx).next(); let node = m.nodes_for_capture_index(*capture_idx).next();
@ -551,7 +528,7 @@ fn query_indents<'a>(
// The row/column position of the optional anchor in this query // The row/column position of the optional anchor in this query
let mut anchor: Option<tree_sitter::Node> = None; let mut anchor: Option<tree_sitter::Node> = None;
for capture in m.captures { for capture in m.captures {
let capture_name = query.capture_names()[capture.index as usize].as_str(); let capture_name = query.capture_names()[capture.index as usize];
let capture_type = match capture_name { let capture_type = match capture_name {
"indent" => IndentCaptureType::Indent, "indent" => IndentCaptureType::Indent,
"indent.always" => IndentCaptureType::IndentAlways, "indent.always" => IndentCaptureType::IndentAlways,
@ -763,7 +740,7 @@ fn init_indent_query<'a, 'b>(
crate::syntax::PARSER.with(|ts_parser| { crate::syntax::PARSER.with(|ts_parser| {
let mut ts_parser = ts_parser.borrow_mut(); let mut ts_parser = ts_parser.borrow_mut();
let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); let mut cursor = ts_parser.cursors.pop().unwrap_or_default();
let query_result = query_indents( let query_result = query_indents(
query, query,
syntax, syntax,
@ -809,6 +786,7 @@ fn init_indent_query<'a, 'b>(
/// - The line after the node. This is defined by: /// - The line after the node. This is defined by:
/// - The scope `tail`. /// - The scope `tail`.
/// - The scope `all` if this node is not the first node on its line. /// - The scope `all` if this node is not the first node on its line.
///
/// Intuitively, `all` applies to everything contained in this node while `tail` applies to everything except for the first line of the node. /// Intuitively, `all` applies to everything contained in this node while `tail` applies to everything except for the first line of the node.
/// The indents from different nodes for the same line are then combined. /// The indents from different nodes for the same line are then combined.
/// The result [Indentation] is simply the sum of the [Indentation] for all lines. /// The result [Indentation] is simply the sum of the [Indentation] for all lines.
@ -852,7 +830,6 @@ pub fn treesitter_indent_for_pos<'a>(
byte_pos, byte_pos,
new_line_byte_pos, new_line_byte_pos,
)?; )?;
let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
let mut result = Indentation::default(); let mut result = Indentation::default();
// We always keep track of all the indent changes on one line, in order to only indent once // We always keep track of all the indent changes on one line, in order to only indent once
@ -861,9 +838,7 @@ pub fn treesitter_indent_for_pos<'a>(
let mut indent_for_line_below = Indentation::default(); let mut indent_for_line_below = Indentation::default();
loop { loop {
// This can safely be unwrapped because `first_in_line` contains let is_first = is_first_in_line(node, text, new_line_byte_pos);
// one entry for each ancestor of the node (which is what we iterate over)
let is_first = *first_in_line.last().unwrap();
// Apply all indent definitions for this node. // Apply all indent definitions for this node.
// Since we only iterate over each node once, we can remove the // Since we only iterate over each node once, we can remove the
@ -906,7 +881,6 @@ pub fn treesitter_indent_for_pos<'a>(
} }
node = parent; node = parent;
first_in_line.pop();
} else { } else {
// Only add the indentation for the line below if that line // Only add the indentation for the line below if that line
// is not after the line that the indentation is calculated for. // is not after the line that the indentation is calculated for.
@ -970,7 +944,7 @@ pub fn indent_for_newline(
let mut num_attempts = 0; let mut num_attempts = 0;
for line_idx in (0..=line_before).rev() { for line_idx in (0..=line_before).rev() {
let line = text.line(line_idx); let line = text.line(line_idx);
let first_non_whitespace_char = match find_first_non_whitespace_char(line) { let first_non_whitespace_char = match line.first_non_whitespace_char() {
Some(i) => i, Some(i) => i,
None => { None => {
continue; continue;

@ -17,7 +17,6 @@ pub mod macros;
pub mod match_brackets; pub mod match_brackets;
pub mod movement; pub mod movement;
pub mod object; pub mod object;
pub mod path;
mod position; mod position;
pub mod search; pub mod search;
pub mod selection; pub mod selection;
@ -28,6 +27,7 @@ pub mod test;
pub mod text_annotations; pub mod text_annotations;
pub mod textobject; pub mod textobject;
mod transaction; mod transaction;
pub mod uri;
pub mod wrap; pub mod wrap;
pub mod unicode { pub mod unicode {
@ -38,9 +38,6 @@ pub mod unicode {
pub use helix_loader::find_workspace; pub use helix_loader::find_workspace;
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}
mod rope_reader; mod rope_reader;
pub use rope_reader::RopeReader; pub use rope_reader::RopeReader;
@ -56,8 +53,8 @@ pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes; pub use graphemes::RopeGraphemes;
pub use position::{ pub use position::{
char_idx_at_visual_offset, coords_at_pos, pos_at_coords, visual_offset_from_anchor, char_idx_at_visual_offset, coords_at_pos, pos_at_coords, softwrapped_dimensions,
visual_offset_from_block, Position, VisualOffsetError, visual_offset_from_anchor, visual_offset_from_block, Position, VisualOffsetError,
}; };
#[allow(deprecated)] #[allow(deprecated)]
pub use position::{pos_at_visual_coords, visual_coords_at_pos}; pub use position::{pos_at_visual_coords, visual_coords_at_pos};
@ -70,3 +67,5 @@ pub use diagnostic::Diagnostic;
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING}; pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction}; pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};
pub use uri::Uri;

@ -9,16 +9,34 @@ use crate::Syntax;
const MAX_PLAINTEXT_SCAN: usize = 10000; const MAX_PLAINTEXT_SCAN: usize = 10000;
const MATCH_LIMIT: usize = 16; const MATCH_LIMIT: usize = 16;
// Limit matching pairs to only ( ) { } [ ] < > ' ' " " pub const BRACKETS: [(char, char); 9] = [
const PAIRS: &[(char, char)] = &[
('(', ')'), ('(', ')'),
('{', '}'), ('{', '}'),
('[', ']'), ('[', ']'),
('<', '>'), ('<', '>'),
('\'', '\''), ('', ''),
('\"', '\"'), ('“', '”'),
('«', '»'),
('「', '」'),
('', ''),
]; ];
// The difference between BRACKETS and PAIRS is that we can find matching
// BRACKETS in a plain text file, but we can't do the same for PAIRs.
// PAIRS also contains all BRACKETS.
pub const PAIRS: [(char, char); BRACKETS.len() + 3] = {
let mut pairs = [(' ', ' '); BRACKETS.len() + 3];
let mut idx = 0;
while idx < BRACKETS.len() {
pairs[idx] = BRACKETS[idx];
idx += 1;
}
pairs[idx] = ('"', '"');
pairs[idx + 1] = ('\'', '\'');
pairs[idx + 2] = ('`', '`');
pairs
};
/// Returns the position of the matching bracket under cursor. /// Returns the position of the matching bracket under cursor.
/// ///
/// If the cursor is on the opening bracket, the position of /// If the cursor is on the opening bracket, the position of
@ -30,7 +48,7 @@ const PAIRS: &[(char, char)] = &[
/// If no matching bracket is found, `None` is returned. /// If no matching bracket is found, `None` is returned.
#[must_use] #[must_use]
pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option<usize> { pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option<usize> {
if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) { if pos >= doc.len_chars() || !is_valid_pair(doc.char(pos)) {
return None; return None;
} }
find_pair(syntax, doc, pos, false) find_pair(syntax, doc, pos, false)
@ -57,68 +75,72 @@ fn find_pair(
pos_: usize, pos_: usize,
traverse_parents: bool, traverse_parents: bool,
) -> Option<usize> { ) -> Option<usize> {
let tree = syntax.tree();
let pos = doc.char_to_byte(pos_); let pos = doc.char_to_byte(pos_);
let mut node = tree.root_node().descendant_for_byte_range(pos, pos)?; let root = syntax.tree_for_byte_range(pos, pos).root_node();
let mut node = root.descendant_for_byte_range(pos, pos)?;
loop { loop {
if node.is_named() { if node.is_named() && node.child_count() >= 2 {
let (start_byte, end_byte) = surrounding_bytes(doc, &node)?; let open = node.child(0).unwrap();
let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte)); let close = node.child(node.child_count() - 1).unwrap();
if is_valid_pair(doc, start_char, end_char) { if let (Some((start_pos, open)), Some((end_pos, close))) =
if end_byte == pos { (as_char(doc, &open), as_char(doc, &close))
return Some(start_char); {
if PAIRS.contains(&(open, close)) {
if end_pos == pos_ {
return Some(start_pos);
} }
// We return the end char if the cursor is either on the start char // We return the end char if the cursor is either on the start char
// or at some arbitrary position between start and end char. // or at some arbitrary position between start and end char.
if traverse_parents || start_byte == pos { if traverse_parents || start_pos == pos_ {
return Some(end_char); return Some(end_pos);
}
} }
} }
} }
// this node itselt wasn't a pair but maybe its siblings are // this node itselt wasn't a pair but maybe its siblings are
// check if we are *on* the pair (special cased so we don't look if let Some((start_char, end_char)) = as_close_pair(doc, &node) {
// at the current node twice and to jump to the start on that case) if let Some(pair_start) =
if let Some(open) = as_close_pair(doc, &node) { find_pair_end(doc, node.prev_sibling(), start_char, end_char, Backward)
if let Some(pair_start) = find_pair_end(doc, node.prev_sibling(), open, Backward) { {
return Some(pair_start); return Some(pair_start);
} }
} }
if let Some((start_char, end_char)) = as_open_pair(doc, &node) {
if !traverse_parents { if let Some(pair_end) =
// check if we are *on* the opening pair (special cased here as find_pair_end(doc, node.next_sibling(), start_char, end_char, Forward)
// an opptimization since we only care about bracket on the cursor {
// here)
if let Some(close) = as_open_pair(doc, &node) {
if let Some(pair_end) = find_pair_end(doc, node.next_sibling(), close, Forward) {
return Some(pair_end); return Some(pair_end);
} }
} }
if node.is_named() {
break;
}
}
for close in if traverse_parents {
for sibling in
iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT) iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT)
{ {
let Some(open) = as_close_pair(doc, &close) else { let Some((start_char, end_char)) = as_close_pair(doc, &sibling) else {
continue; continue;
}; };
if find_pair_end(doc, Some(node), open, Backward).is_some() { if find_pair_end(doc, sibling.prev_sibling(), start_char, end_char, Backward)
return doc.try_byte_to_char(close.start_byte()).ok(); .is_some()
{
return doc.try_byte_to_char(sibling.start_byte()).ok();
} }
} }
} else if node.is_named() {
break;
}
let Some(parent) = node.parent() else { let Some(parent) = node.parent() else {
break; break;
}; };
node = parent; node = parent;
} }
let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?; let node = root.named_descendant_for_byte_range(pos, pos + 1)?;
if node.child_count() != 0 { if node.child_count() != 0 {
return None; return None;
} }
@ -140,14 +162,22 @@ fn find_pair(
/// If no matching bracket is found, `None` is returned. /// If no matching bracket is found, `None` is returned.
#[must_use] #[must_use]
pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> { pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> {
let bracket = doc.get_char(cursor_pos)?;
let matching_bracket = {
let pair = get_pair(bracket);
if pair.0 == bracket {
pair.1
} else {
pair.0
}
};
// Don't do anything when the cursor is not on top of a bracket. // Don't do anything when the cursor is not on top of a bracket.
let bracket = doc.char(cursor_pos);
if !is_valid_bracket(bracket) { if !is_valid_bracket(bracket) {
return None; return None;
} }
// Determine the direction of the matching. // Determine the direction of the matching.
let is_fwd = is_forward_bracket(bracket); let is_fwd = is_open_bracket(bracket);
let chars_iter = if is_fwd { let chars_iter = if is_fwd {
doc.chars_at(cursor_pos + 1) doc.chars_at(cursor_pos + 1)
} else { } else {
@ -159,19 +189,7 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt
for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() { for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() {
if candidate == bracket { if candidate == bracket {
open_cnt += 1; open_cnt += 1;
} else if is_valid_pair( } else if candidate == matching_bracket {
doc,
if is_fwd {
cursor_pos
} else {
cursor_pos - i - 1
},
if is_fwd {
cursor_pos + i + 1
} else {
cursor_pos
},
) {
// Return when all pending brackets have been closed. // Return when all pending brackets have been closed.
if open_cnt == 1 { if open_cnt == 1 {
return Some(if is_fwd { return Some(if is_fwd {
@ -187,37 +205,55 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt
None None
} }
fn is_valid_bracket(c: char) -> bool { /// Returns the open and closing chars pair. If not found in
PAIRS.iter().any(|(l, r)| *l == c || *r == c) /// [`BRACKETS`] returns (ch, ch).
///
/// ```
/// use helix_core::match_brackets::get_pair;
///
/// assert_eq!(get_pair('['), ('[', ']'));
/// assert_eq!(get_pair('}'), ('{', '}'));
/// assert_eq!(get_pair('"'), ('"', '"'));
/// ```
pub fn get_pair(ch: char) -> (char, char) {
PAIRS
.iter()
.find(|(open, close)| *open == ch || *close == ch)
.copied()
.unwrap_or((ch, ch))
} }
fn is_forward_bracket(c: char) -> bool { pub fn is_open_bracket(ch: char) -> bool {
PAIRS.iter().any(|(l, _)| *l == c) BRACKETS.iter().any(|(l, _)| *l == ch)
} }
fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool { pub fn is_close_bracket(ch: char) -> bool {
PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) BRACKETS.iter().any(|(_, r)| *r == ch)
} }
fn surrounding_bytes(doc: RopeSlice, node: &Node) -> Option<(usize, usize)> { pub fn is_valid_bracket(ch: char) -> bool {
let len = doc.len_bytes(); BRACKETS.iter().any(|(l, r)| *l == ch || *r == ch)
}
let start_byte = node.start_byte(); pub fn is_open_pair(ch: char) -> bool {
let end_byte = node.end_byte().saturating_sub(1); PAIRS.iter().any(|(l, _)| *l == ch)
}
if start_byte >= len || end_byte >= len { pub fn is_close_pair(ch: char) -> bool {
return None; PAIRS.iter().any(|(_, r)| *r == ch)
} }
Some((start_byte, end_byte)) pub fn is_valid_pair(ch: char) -> bool {
PAIRS.iter().any(|(l, r)| *l == ch || *r == ch)
} }
/// Tests if this node is a pair close char and returns the expected open char /// Tests if this node is a pair close char and returns the expected open char
fn as_close_pair(doc: RopeSlice, node: &Node) -> Option<char> { /// and close char contained in this node
fn as_close_pair(doc: RopeSlice, node: &Node) -> Option<(char, char)> {
let close = as_char(doc, node)?.1; let close = as_char(doc, node)?.1;
PAIRS PAIRS
.iter() .iter()
.find_map(|&(open, close_)| (close_ == close).then_some(open)) .find_map(|&(open, close_)| (close_ == close).then_some((close, open)))
} }
/// Checks if `node` or its siblings (at most MATCH_LIMIT nodes) is the specified closing char /// Checks if `node` or its siblings (at most MATCH_LIMIT nodes) is the specified closing char
@ -228,6 +264,7 @@ fn as_close_pair(doc: RopeSlice, node: &Node) -> Option<char> {
fn find_pair_end( fn find_pair_end(
doc: RopeSlice, doc: RopeSlice,
node: Option<Node>, node: Option<Node>,
start_char: char,
end_char: char, end_char: char,
direction: Direction, direction: Direction,
) -> Option<usize> { ) -> Option<usize> {
@ -235,20 +272,30 @@ fn find_pair_end(
Forward => Node::next_sibling, Forward => Node::next_sibling,
Backward => Node::prev_sibling, Backward => Node::prev_sibling,
}; };
let mut depth = 0;
iter::successors(node, advance) iter::successors(node, advance)
.take(MATCH_LIMIT) .take(MATCH_LIMIT)
.find_map(|node| { .find_map(|node| {
let (pos, c) = as_char(doc, &node)?; let (pos, c) = as_char(doc, &node)?;
(end_char == c).then_some(pos) if c == end_char {
if depth == 0 {
return Some(pos);
}
depth -= 1;
} else if c == start_char {
depth += 1;
}
None
}) })
} }
/// Tests if this node is a pair close char and returns the expected open char /// Tests if this node is a pair open char and returns the expected close char
fn as_open_pair(doc: RopeSlice, node: &Node) -> Option<char> { /// and open char contained in this node
fn as_open_pair(doc: RopeSlice, node: &Node) -> Option<(char, char)> {
let open = as_char(doc, node)?.1; let open = as_char(doc, node)?.1;
PAIRS PAIRS
.iter() .iter()
.find_map(|&(open_, close)| (open_ == open).then_some(close)) .find_map(|&(open_, close)| (open_ == open).then_some((open, close)))
} }
/// If node is a single char return it (and its char position) /// If node is a single char return it (and its char position)
@ -265,6 +312,12 @@ fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn find_matching_bracket_empty_file() {
let actual = find_matching_bracket_plaintext("".into(), 0);
assert_eq!(actual, None);
}
#[test] #[test]
fn test_find_matching_bracket_current_line_plaintext() { fn test_find_matching_bracket_current_line_plaintext() {
let assert = |input: &str, pos, expected| { let assert = |input: &str, pos, expected| {

@ -79,19 +79,19 @@ pub fn move_vertically_visual(
Direction::Backward => -(count as isize), Direction::Backward => -(count as isize),
}; };
// TODO how to handle inline annotations that span an entire visual line (very unlikely).
// Compute visual offset relative to block start to avoid trasversing the block twice // Compute visual offset relative to block start to avoid trasversing the block twice
row_off += visual_pos.row as isize; row_off += visual_pos.row as isize;
let new_pos = char_idx_at_visual_offset( let (mut new_pos, virtual_rows) = char_idx_at_visual_offset(
slice, slice,
block_off, block_off,
row_off, row_off,
new_col as usize, new_col as usize,
text_fmt, text_fmt,
annotations, annotations,
) );
.0; if dir == Direction::Forward {
new_pos += (virtual_rows != 0) as usize;
}
// Special-case to avoid moving to the end of the last non-empty line. // Special-case to avoid moving to the end of the last non-empty line.
if behaviour == Movement::Extend && slice.line(slice.char_to_line(new_pos)).len_chars() == 0 { if behaviour == Movement::Extend && slice.line(slice.char_to_line(new_pos)).len_chars() == 0 {
@ -197,13 +197,31 @@ pub fn move_prev_long_word_end(slice: RopeSlice, range: Range, count: usize) ->
word_move(slice, range, count, WordMotionTarget::PrevLongWordEnd) word_move(slice, range, count, WordMotionTarget::PrevLongWordEnd)
} }
pub fn move_next_sub_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::NextSubWordStart)
}
pub fn move_next_sub_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::NextSubWordEnd)
}
pub fn move_prev_sub_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevSubWordStart)
}
pub fn move_prev_sub_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevSubWordEnd)
}
fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range { fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range {
let is_prev = matches!( let is_prev = matches!(
target, target,
WordMotionTarget::PrevWordStart WordMotionTarget::PrevWordStart
| WordMotionTarget::PrevLongWordStart | WordMotionTarget::PrevLongWordStart
| WordMotionTarget::PrevSubWordStart
| WordMotionTarget::PrevWordEnd | WordMotionTarget::PrevWordEnd
| WordMotionTarget::PrevLongWordEnd | WordMotionTarget::PrevLongWordEnd
| WordMotionTarget::PrevSubWordEnd
); );
// Special-case early-out. // Special-case early-out.
@ -383,6 +401,12 @@ pub enum WordMotionTarget {
NextLongWordEnd, NextLongWordEnd,
PrevLongWordStart, PrevLongWordStart,
PrevLongWordEnd, PrevLongWordEnd,
// A sub word is similar to a regular word, except it is also delimited by
// underscores and transitions from lowercase to uppercase.
NextSubWordStart,
NextSubWordEnd,
PrevSubWordStart,
PrevSubWordEnd,
} }
pub trait CharHelpers { pub trait CharHelpers {
@ -398,8 +422,10 @@ impl CharHelpers for Chars<'_> {
target, target,
WordMotionTarget::PrevWordStart WordMotionTarget::PrevWordStart
| WordMotionTarget::PrevLongWordStart | WordMotionTarget::PrevLongWordStart
| WordMotionTarget::PrevSubWordStart
| WordMotionTarget::PrevWordEnd | WordMotionTarget::PrevWordEnd
| WordMotionTarget::PrevLongWordEnd | WordMotionTarget::PrevLongWordEnd
| WordMotionTarget::PrevSubWordEnd
); );
// Reverse the iterator if needed for the motion direction. // Reverse the iterator if needed for the motion direction.
@ -476,6 +502,25 @@ fn is_long_word_boundary(a: char, b: char) -> bool {
} }
} }
fn is_sub_word_boundary(a: char, b: char, dir: Direction) -> bool {
match (categorize_char(a), categorize_char(b)) {
(CharCategory::Word, CharCategory::Word) => {
if (a == '_') != (b == '_') {
return true;
}
// Subword boundaries are directional: in 'fooBar', there is a
// boundary between 'o' and 'B', but not between 'B' and 'a'.
match dir {
Direction::Forward => a.is_lowercase() && b.is_uppercase(),
Direction::Backward => a.is_uppercase() && b.is_lowercase(),
}
}
(a, b) if a != b => true,
_ => false,
}
}
fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> bool { fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> bool {
match target { match target {
WordMotionTarget::NextWordStart | WordMotionTarget::PrevWordEnd => { WordMotionTarget::NextWordStart | WordMotionTarget::PrevWordEnd => {
@ -494,6 +539,22 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
is_long_word_boundary(prev_ch, next_ch) is_long_word_boundary(prev_ch, next_ch)
&& (!prev_ch.is_whitespace() || char_is_line_ending(next_ch)) && (!prev_ch.is_whitespace() || char_is_line_ending(next_ch))
} }
WordMotionTarget::NextSubWordStart => {
is_sub_word_boundary(prev_ch, next_ch, Direction::Forward)
&& (char_is_line_ending(next_ch) || !(next_ch.is_whitespace() || next_ch == '_'))
}
WordMotionTarget::PrevSubWordEnd => {
is_sub_word_boundary(prev_ch, next_ch, Direction::Backward)
&& (char_is_line_ending(next_ch) || !(next_ch.is_whitespace() || next_ch == '_'))
}
WordMotionTarget::NextSubWordEnd => {
is_sub_word_boundary(prev_ch, next_ch, Direction::Forward)
&& (!(prev_ch.is_whitespace() || prev_ch == '_') || char_is_line_ending(next_ch))
}
WordMotionTarget::PrevSubWordStart => {
is_sub_word_boundary(prev_ch, next_ch, Direction::Backward)
&& (!(prev_ch.is_whitespace() || prev_ch == '_') || char_is_line_ending(next_ch))
}
} }
} }
@ -573,16 +634,11 @@ pub fn move_parent_node_end(
dir: Direction, dir: Direction,
movement: Movement, movement: Movement,
) -> Selection { ) -> Selection {
let tree = syntax.tree();
selection.transform(|range| { selection.transform(|range| {
let start_from = text.char_to_byte(range.from()); let start_from = text.char_to_byte(range.from());
let start_to = text.char_to_byte(range.to()); let start_to = text.char_to_byte(range.to());
let mut node = match tree let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) {
.root_node()
.named_descendant_for_byte_range(start_from, start_to)
{
Some(node) => node, Some(node) => node,
None => { None => {
log::debug!( log::debug!(
@ -1017,6 +1073,178 @@ mod test {
} }
} }
#[test]
fn test_behaviour_when_moving_to_start_of_next_sub_words() {
let tests = [
(
"NextSubwordStart",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 11)),
],
),
(
"next_subword_start",
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
(1, Range::new(4, 4), Range::new(5, 13)),
],
),
(
"Next_Subword_Start",
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
(1, Range::new(4, 4), Range::new(5, 13)),
],
),
(
"NEXT_SUBWORD_START",
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
(1, Range::new(4, 4), Range::new(5, 13)),
],
),
(
"next subword start",
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
(1, Range::new(4, 4), Range::new(5, 13)),
],
),
(
"Next Subword Start",
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
(1, Range::new(4, 4), Range::new(5, 13)),
],
),
(
"NEXT SUBWORD START",
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
(1, Range::new(4, 4), Range::new(5, 13)),
],
),
(
"next__subword__start",
vec![
(1, Range::new(0, 0), Range::new(0, 6)),
(1, Range::new(4, 4), Range::new(4, 6)),
(1, Range::new(5, 5), Range::new(6, 15)),
],
),
(
"Next__Subword__Start",
vec![
(1, Range::new(0, 0), Range::new(0, 6)),
(1, Range::new(4, 4), Range::new(4, 6)),
(1, Range::new(5, 5), Range::new(6, 15)),
],
),
(
"NEXT__SUBWORD__START",
vec![
(1, Range::new(0, 0), Range::new(0, 6)),
(1, Range::new(4, 4), Range::new(4, 6)),
(1, Range::new(5, 5), Range::new(6, 15)),
],
),
];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_next_sub_word_start(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
#[test]
fn test_behaviour_when_moving_to_end_of_next_sub_words() {
let tests = [
(
"NextSubwordEnd",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 11)),
],
),
(
"next subword end",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 12)),
],
),
(
"Next Subword End",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 12)),
],
),
(
"NEXT SUBWORD END",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 12)),
],
),
(
"next_subword_end",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 12)),
],
),
(
"Next_Subword_End",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 12)),
],
),
(
"NEXT_SUBWORD_END",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 12)),
],
),
(
"next__subword__end",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 13)),
(1, Range::new(5, 5), Range::new(5, 13)),
],
),
(
"Next__Subword__End",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 13)),
(1, Range::new(5, 5), Range::new(5, 13)),
],
),
(
"NEXT__SUBWORD__END",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
(1, Range::new(4, 4), Range::new(4, 13)),
(1, Range::new(5, 5), Range::new(5, 13)),
],
),
];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_next_sub_word_end(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
#[test] #[test]
fn test_behaviour_when_moving_to_start_of_next_long_words() { fn test_behaviour_when_moving_to_start_of_next_long_words() {
let tests = [ let tests = [
@ -1186,6 +1414,92 @@ mod test {
} }
} }
#[test]
fn test_behaviour_when_moving_to_start_of_previous_sub_words() {
let tests = [
(
"PrevSubwordEnd",
vec![
(1, Range::new(13, 13), Range::new(14, 11)),
(1, Range::new(11, 11), Range::new(11, 4)),
],
),
(
"prev subword end",
vec![
(1, Range::new(15, 15), Range::new(16, 13)),
(1, Range::new(12, 12), Range::new(13, 5)),
],
),
(
"Prev Subword End",
vec![
(1, Range::new(15, 15), Range::new(16, 13)),
(1, Range::new(12, 12), Range::new(13, 5)),
],
),
(
"PREV SUBWORD END",
vec![
(1, Range::new(15, 15), Range::new(16, 13)),
(1, Range::new(12, 12), Range::new(13, 5)),
],
),
(
"prev_subword_end",
vec![
(1, Range::new(15, 15), Range::new(16, 13)),
(1, Range::new(12, 12), Range::new(13, 5)),
],
),
(
"Prev_Subword_End",
vec![
(1, Range::new(15, 15), Range::new(16, 13)),
(1, Range::new(12, 12), Range::new(13, 5)),
],
),
(
"PREV_SUBWORD_END",
vec![
(1, Range::new(15, 15), Range::new(16, 13)),
(1, Range::new(12, 12), Range::new(13, 5)),
],
),
(
"prev__subword__end",
vec![
(1, Range::new(17, 17), Range::new(18, 15)),
(1, Range::new(13, 13), Range::new(14, 6)),
(1, Range::new(14, 14), Range::new(15, 6)),
],
),
(
"Prev__Subword__End",
vec![
(1, Range::new(17, 17), Range::new(18, 15)),
(1, Range::new(13, 13), Range::new(14, 6)),
(1, Range::new(14, 14), Range::new(15, 6)),
],
),
(
"PREV__SUBWORD__END",
vec![
(1, Range::new(17, 17), Range::new(18, 15)),
(1, Range::new(13, 13), Range::new(14, 6)),
(1, Range::new(14, 14), Range::new(15, 6)),
],
),
];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_prev_sub_word_start(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
#[test] #[test]
fn test_behaviour_when_moving_to_start_of_previous_long_words() { fn test_behaviour_when_moving_to_start_of_previous_long_words() {
let tests = [ let tests = [
@ -1449,6 +1763,92 @@ mod test {
} }
} }
#[test]
fn test_behaviour_when_moving_to_end_of_previous_sub_words() {
let tests = [
(
"PrevSubwordEnd",
vec![
(1, Range::new(13, 13), Range::new(14, 11)),
(1, Range::new(11, 11), Range::new(11, 4)),
],
),
(
"prev subword end",
vec![
(1, Range::new(15, 15), Range::new(16, 12)),
(1, Range::new(12, 12), Range::new(12, 4)),
],
),
(
"Prev Subword End",
vec![
(1, Range::new(15, 15), Range::new(16, 12)),
(1, Range::new(12, 12), Range::new(12, 4)),
],
),
(
"PREV SUBWORD END",
vec![
(1, Range::new(15, 15), Range::new(16, 12)),
(1, Range::new(12, 12), Range::new(12, 4)),
],
),
(
"prev_subword_end",
vec![
(1, Range::new(15, 15), Range::new(16, 12)),
(1, Range::new(12, 12), Range::new(12, 4)),
],
),
(
"Prev_Subword_End",
vec![
(1, Range::new(15, 15), Range::new(16, 12)),
(1, Range::new(12, 12), Range::new(12, 4)),
],
),
(
"PREV_SUBWORD_END",
vec![
(1, Range::new(15, 15), Range::new(16, 12)),
(1, Range::new(12, 12), Range::new(12, 4)),
],
),
(
"prev__subword__end",
vec![
(1, Range::new(17, 17), Range::new(18, 13)),
(1, Range::new(13, 13), Range::new(13, 4)),
(1, Range::new(14, 14), Range::new(15, 13)),
],
),
(
"Prev__Subword__End",
vec![
(1, Range::new(17, 17), Range::new(18, 13)),
(1, Range::new(13, 13), Range::new(13, 4)),
(1, Range::new(14, 14), Range::new(15, 13)),
],
),
(
"PREV__SUBWORD__END",
vec![
(1, Range::new(17, 17), Range::new(18, 13)),
(1, Range::new(13, 13), Range::new(13, 4)),
(1, Range::new(14, 14), Range::new(15, 13)),
],
),
];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_prev_sub_word_end(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
#[test] #[test]
fn test_behaviour_when_moving_to_end_of_next_long_words() { fn test_behaviour_when_moving_to_end_of_next_long_words() {
let tests = [ let tests = [

@ -1,76 +1,137 @@
use crate::{Range, RopeSlice, Selection, Syntax}; use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
use tree_sitter::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, |mut node, from, to| { let cursor = &mut syntax.walk();
while node.start_byte() == from && node.end_byte() == to {
node = node.parent()?; selection.transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());
let byte_range = from..to;
cursor.reset_to_byte_range(from, to);
while cursor.node().byte_range() == byte_range {
if !cursor.goto_parent() {
break;
} }
Some(node) }
let node = cursor.node();
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
Range::new(to, from).with_direction(range.direction())
}) })
} }
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection { pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(syntax, text, selection, |descendant, _from, _to| { select_node_impl(
descendant.child(0).or(Some(descendant)) syntax,
}) text,
selection,
|cursor| {
cursor.goto_first_child();
},
None,
)
} }
pub fn select_sibling<F>( pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
syntax: &Syntax, select_node_impl(
text: RopeSlice, syntax,
selection: Selection, text,
sibling_fn: &F, selection,
) -> Selection |cursor| {
where while !cursor.goto_next_sibling() {
F: Fn(Node) -> Option<Node>, if !cursor.goto_parent() {
{ break;
select_node_impl(syntax, text, selection, |descendant, _from, _to| { }
find_sibling_recursive(descendant, sibling_fn) }
},
Some(Direction::Forward),
)
}
pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
selection.transform_iter(|range| {
let mut cursor = syntax.walk();
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from, to);
if !cursor.goto_parent_with(|parent| parent.child_count() > 1) {
return vec![range].into_iter();
}
select_children(&mut cursor, text, range).into_iter()
}) })
} }
fn find_sibling_recursive<F>(node: Node, sibling_fn: F) -> Option<Node> pub fn select_all_children(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
where selection.transform_iter(|range| {
F: Fn(Node) -> Option<Node>, let mut cursor = syntax.walk();
{ let (from, to) = range.into_byte_range(text);
sibling_fn(node).or_else(|| { cursor.reset_to_byte_range(from, to);
node.parent() select_children(&mut cursor, text, range).into_iter()
.and_then(|node| find_sibling_recursive(node, sibling_fn))
}) })
} }
fn select_children<'n>(
cursor: &'n mut TreeCursor<'n>,
text: RopeSlice,
range: Range,
) -> Vec<Range> {
let children = cursor
.named_children()
.map(|child| Range::from_node(child, text, range.direction()))
.collect::<Vec<_>>();
if !children.is_empty() {
children
} else {
vec![range]
}
}
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
select_node_impl(
syntax,
text,
selection,
|cursor| {
while !cursor.goto_prev_sibling() {
if !cursor.goto_parent() {
break;
}
}
},
Some(Direction::Backward),
)
}
fn select_node_impl<F>( fn select_node_impl<F>(
syntax: &Syntax, syntax: &Syntax,
text: RopeSlice, text: RopeSlice,
selection: Selection, selection: Selection,
select_fn: F, motion: F,
direction: Option<Direction>,
) -> Selection ) -> Selection
where where
F: Fn(Node, usize, usize) -> Option<Node>, F: Fn(&mut TreeCursor),
{ {
let tree = syntax.tree(); let cursor = &mut syntax.walk();
selection.transform(|range| { selection.transform(|range| {
let from = text.char_to_byte(range.from()); let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to()); let to = text.char_to_byte(range.to());
let node = match tree cursor.reset_to_byte_range(from, to);
.root_node()
.descendant_for_byte_range(from, to) motion(cursor);
.and_then(|node| select_fn(node, from, to))
{
Some(node) => node,
None => return range,
};
let node = cursor.node();
let from = text.byte_to_char(node.start_byte()); let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte()); let to = text.byte_to_char(node.end_byte());
if range.head < range.anchor { Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
Range::new(to, from)
} else {
Range::new(from, to)
}
}) })
} }

@ -1,162 +0,0 @@
use etcetera::home_dir;
use std::path::{Component, Path, PathBuf};
/// Replaces users home directory from `path` with tilde `~` if the directory
/// is available, otherwise returns the path unchanged.
pub fn fold_home_dir(path: &Path) -> PathBuf {
if let Ok(home) = home_dir() {
if let Ok(stripped) = path.strip_prefix(&home) {
return PathBuf::from("~").join(stripped);
}
}
path.to_path_buf()
}
/// Expands tilde `~` into users home directory if available, otherwise returns the path
/// unchanged. The tilde will only be expanded when present as the first component of the path
/// and only slash follows it.
pub fn expand_tilde(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
if let Some(Component::Normal(c)) = components.peek() {
if c == &"~" {
if let Ok(home) = home_dir() {
// it's ok to unwrap, the path starts with `~`
return home.join(path.strip_prefix("~").unwrap());
}
}
}
path.to_path_buf()
}
/// Normalize a path, removing things like `.` and `..`.
///
/// CAUTION: This does not resolve symlinks (unlike
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
/// behavior at times. This should be used carefully. Unfortunately,
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
/// needs to improve on.
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
pub fn get_normalized_path(path: &Path) -> PathBuf {
// normalization strategy is to canonicalize first ancestor path that exists (i.e., canonicalize as much as possible),
// then run handrolled normalization on the non-existent remainder
let (base, path) = path
.ancestors()
.find_map(|base| {
let canonicalized_base = dunce::canonicalize(base).ok()?;
let remainder = path.strip_prefix(base).ok()?.into();
Some((canonicalized_base, remainder))
})
.unwrap_or_else(|| (PathBuf::new(), PathBuf::from(path)));
if path.as_os_str().is_empty() {
return base;
}
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
base.join(ret)
}
/// Returns the canonical, absolute form of a path with all intermediate components normalized.
///
/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
/// here if the path exists, just normalize it's components.
pub fn get_canonicalized_path(path: &Path) -> PathBuf {
let path = expand_tilde(path);
let path = if path.is_relative() {
helix_loader::current_working_dir().join(path)
} else {
path
};
get_normalized_path(path.as_path())
}
pub fn get_relative_path(path: &Path) -> PathBuf {
let path = PathBuf::from(path);
let path = if path.is_absolute() {
let cwdir = get_normalized_path(&helix_loader::current_working_dir());
get_normalized_path(&path)
.strip_prefix(cwdir)
.map(PathBuf::from)
.unwrap_or(path)
} else {
path
};
fold_home_dir(&path)
}
/// Returns a truncated filepath where the basepart of the path is reduced to the first
/// char of the folder and the whole filename appended.
///
/// Also strip the current working directory from the beginning of the path.
/// Note that this function does not check if the truncated path is unambiguous.
///
/// ```
/// use helix_core::path::get_truncated_path;
/// use std::path::Path;
///
/// assert_eq!(
/// get_truncated_path("/home/cnorris/documents/jokes.txt").as_path(),
/// Path::new("/h/c/d/jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("jokes.txt").as_path(),
/// Path::new("jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("/jokes.txt").as_path(),
/// Path::new("/jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("/h/c/d/jokes.txt").as_path(),
/// Path::new("/h/c/d/jokes.txt")
/// );
/// assert_eq!(get_truncated_path("").as_path(), Path::new(""));
/// ```
///
pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
let cwd = helix_loader::current_working_dir();
let path = path
.as_ref()
.strip_prefix(cwd)
.unwrap_or_else(|_| path.as_ref());
let file = path.file_name().unwrap_or_default();
let base = path.parent().unwrap_or_else(|| Path::new(""));
let mut ret = PathBuf::new();
for d in base {
ret.push(
d.to_string_lossy()
.chars()
.next()
.unwrap_or_default()
.to_string(),
);
}
ret.push(file);
ret
}

@ -1,4 +1,8 @@
use std::{borrow::Cow, cmp::Ordering}; use std::{
borrow::Cow,
cmp::Ordering,
ops::{Add, AddAssign, Sub, SubAssign},
};
use crate::{ use crate::{
chars::char_is_line_ending, chars::char_is_line_ending,
@ -16,6 +20,38 @@ pub struct Position {
pub col: usize, pub col: usize,
} }
impl AddAssign for Position {
fn add_assign(&mut self, rhs: Self) {
self.row += rhs.row;
self.col += rhs.col;
}
}
impl SubAssign for Position {
fn sub_assign(&mut self, rhs: Self) {
self.row -= rhs.row;
self.col -= rhs.col;
}
}
impl Sub for Position {
type Output = Position;
fn sub(mut self, rhs: Self) -> Self::Output {
self -= rhs;
self
}
}
impl Add for Position {
type Output = Position;
fn add(mut self, rhs: Self) -> Self::Output {
self += rhs;
self
}
}
impl Position { impl Position {
pub const fn new(row: usize, col: usize) -> Self { pub const fn new(row: usize, col: usize) -> Self {
Self { row, col } Self { row, col }
@ -121,22 +157,31 @@ pub fn visual_offset_from_block(
annotations: &TextAnnotations, annotations: &TextAnnotations,
) -> (Position, usize) { ) -> (Position, usize) {
let mut last_pos = Position::default(); let mut last_pos = Position::default();
let (formatter, block_start) = let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
let mut char_pos = block_start; let block_start = formatter.next_char_pos();
for (grapheme, vpos) in formatter { while let Some(grapheme) = formatter.next() {
last_pos = vpos; last_pos = grapheme.visual_pos;
char_pos += grapheme.doc_chars(); if formatter.next_char_pos() > pos {
return (grapheme.visual_pos, block_start);
if char_pos > pos {
return (last_pos, block_start);
} }
} }
(last_pos, block_start) (last_pos, block_start)
} }
/// Returns the height of the given text when softwrapping
pub fn softwrapped_dimensions(text: RopeSlice, text_fmt: &TextFormat) -> (usize, u16) {
let last_pos =
visual_offset_from_block(text, 0, usize::MAX, text_fmt, &TextAnnotations::default()).0;
if last_pos.row == 0 {
(1, last_pos.col as u16)
} else {
(last_pos.row + 1, text_fmt.viewport_width)
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)] #[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub enum VisualOffsetError { pub enum VisualOffsetError {
PosBeforeAnchorRow, PosBeforeAnchorRow,
@ -153,22 +198,21 @@ pub fn visual_offset_from_anchor(
annotations: &TextAnnotations, annotations: &TextAnnotations,
max_rows: usize, max_rows: usize,
) -> Result<(Position, usize), VisualOffsetError> { ) -> Result<(Position, usize), VisualOffsetError> {
let (formatter, block_start) = let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
let mut char_pos = block_start;
let mut anchor_line = None; let mut anchor_line = None;
let mut found_pos = None; let mut found_pos = None;
let mut last_pos = Position::default(); let mut last_pos = Position::default();
let block_start = formatter.next_char_pos();
if pos < block_start { if pos < block_start {
return Err(VisualOffsetError::PosBeforeAnchorRow); return Err(VisualOffsetError::PosBeforeAnchorRow);
} }
for (grapheme, vpos) in formatter { while let Some(grapheme) = formatter.next() {
last_pos = vpos; last_pos = grapheme.visual_pos;
char_pos += grapheme.doc_chars();
if char_pos > pos { if formatter.next_char_pos() > pos {
if let Some(anchor_line) = anchor_line { if let Some(anchor_line) = anchor_line {
last_pos.row -= anchor_line; last_pos.row -= anchor_line;
return Ok((last_pos, block_start)); return Ok((last_pos, block_start));
@ -176,7 +220,7 @@ pub fn visual_offset_from_anchor(
found_pos = Some(last_pos); found_pos = Some(last_pos);
} }
} }
if char_pos > anchor && anchor_line.is_none() { if formatter.next_char_pos() > anchor && anchor_line.is_none() {
if let Some(mut found_pos) = found_pos { if let Some(mut found_pos) = found_pos {
return if found_pos.row == last_pos.row { return if found_pos.row == last_pos.row {
found_pos.row = 0; found_pos.row = 0;
@ -190,7 +234,7 @@ pub fn visual_offset_from_anchor(
} }
if let Some(anchor_line) = anchor_line { if let Some(anchor_line) = anchor_line {
if vpos.row >= anchor_line + max_rows { if grapheme.visual_pos.row >= anchor_line + max_rows {
return Err(VisualOffsetError::PosAfterMaxRow); return Err(VisualOffsetError::PosAfterMaxRow);
} }
} }
@ -368,39 +412,43 @@ pub fn char_idx_at_visual_block_offset(
text_fmt: &TextFormat, text_fmt: &TextFormat,
annotations: &TextAnnotations, annotations: &TextAnnotations,
) -> (usize, usize) { ) -> (usize, usize) {
let (formatter, mut char_idx) = let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor); DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, annotations, anchor);
let mut last_char_idx = char_idx; let mut last_char_idx = formatter.next_char_pos();
let mut last_char_idx_on_line = None; let mut found_non_virtual_on_row = false;
let mut last_row = 0; let mut last_row = 0;
for (grapheme, grapheme_pos) in formatter { for grapheme in &mut formatter {
match grapheme_pos.row.cmp(&row) { match grapheme.visual_pos.row.cmp(&row) {
Ordering::Equal => { Ordering::Equal => {
if grapheme_pos.col + grapheme.width() > column { if grapheme.visual_pos.col + grapheme.width() > column {
if !grapheme.is_virtual() { if !grapheme.is_virtual() {
return (char_idx, 0); return (grapheme.char_idx, 0);
} else if let Some(char_idx) = last_char_idx_on_line { } else if found_non_virtual_on_row {
return (char_idx, 0); return (last_char_idx, 0);
} }
} else if !grapheme.is_virtual() { } else if !grapheme.is_virtual() {
last_char_idx_on_line = Some(char_idx) found_non_virtual_on_row = true;
last_char_idx = grapheme.char_idx;
} }
} }
Ordering::Greater if found_non_virtual_on_row => return (last_char_idx, 0),
Ordering::Greater => return (last_char_idx, row - last_row), Ordering::Greater => return (last_char_idx, row - last_row),
_ => (), Ordering::Less => {
if !grapheme.is_virtual() {
last_row = grapheme.visual_pos.row;
last_char_idx = grapheme.char_idx;
}
}
} }
last_char_idx = char_idx;
last_row = grapheme_pos.row;
char_idx += grapheme.doc_chars();
} }
(char_idx, 0) (formatter.next_char_pos(), 0)
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::text_annotations::InlineAnnotation;
use crate::Rope; use crate::Rope;
#[test] #[test]
@ -761,6 +809,30 @@ mod test {
assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0); assert_eq!(pos_at_visual_coords(slice, (10, 10).into(), 4), 0);
} }
#[test]
fn test_char_idx_at_visual_row_offset_inline_annotation() {
let text = Rope::from("foo\nbar");
let slice = text.slice(..);
let mut text_fmt = TextFormat::default();
let annotations = [InlineAnnotation {
text: "x".repeat(100).into(),
char_idx: 3,
}];
text_fmt.soft_wrap = true;
assert_eq!(
char_idx_at_visual_offset(
slice,
0,
1,
0,
&text_fmt,
TextAnnotations::default().add_inline_annotations(&annotations, None)
),
(2, 1)
);
}
#[test] #[test]
fn test_char_idx_at_visual_row_offset() { fn test_char_idx_at_visual_row_offset() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ\nfoo"); let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ\nfoo");

@ -7,11 +7,14 @@ use crate::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary, prev_grapheme_boundary,
}, },
line_ending::get_line_ending,
movement::Direction, movement::Direction,
Assoc, ChangeSet, RopeGraphemes, RopeSlice, Assoc, ChangeSet, RopeGraphemes, RopeSlice,
}; };
use helix_stdx::rope::{self, RopeSliceExt};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use std::borrow::Cow; use std::{borrow::Cow, iter, slice};
use tree_sitter::Node;
/// A single selection range. /// A single selection range.
/// ///
@ -71,6 +74,12 @@ impl Range {
Self::new(head, head) Self::new(head, head)
} }
pub fn from_node(node: Node, text: RopeSlice, direction: Direction) -> Self {
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
Range::new(from, to).with_direction(direction)
}
/// Start of the range. /// Start of the range.
#[inline] #[inline]
#[must_use] #[must_use]
@ -113,7 +122,7 @@ impl Range {
} }
/// `Direction::Backward` when head < anchor. /// `Direction::Backward` when head < anchor.
/// `Direction::Backward` otherwise. /// `Direction::Forward` otherwise.
#[inline] #[inline]
#[must_use] #[must_use]
pub fn direction(&self) -> Direction { pub fn direction(&self) -> Direction {
@ -166,7 +175,7 @@ impl Range {
/// function runs in O(N) (N is number of changes) and can therefore /// function runs in O(N) (N is number of changes) and can therefore
/// cause performance problems if run for a large number of ranges as the /// cause performance problems if run for a large number of ranges as the
/// complexity is then O(MN) (for multicuror M=N usually). Instead use /// complexity is then O(MN) (for multicuror M=N usually). Instead use
/// [Selection::map] or [ChangeSet::update_positions] instead /// [Selection::map] or [ChangeSet::update_positions].
pub fn map(mut self, changes: &ChangeSet) -> Self { pub fn map(mut self, changes: &ChangeSet) -> Self {
use std::cmp::Ordering; use std::cmp::Ordering;
if changes.is_empty() { if changes.is_empty() {
@ -175,16 +184,16 @@ impl Range {
let positions_to_map = match self.anchor.cmp(&self.head) { let positions_to_map = match self.anchor.cmp(&self.head) {
Ordering::Equal => [ Ordering::Equal => [
(&mut self.anchor, Assoc::After), (&mut self.anchor, Assoc::AfterSticky),
(&mut self.head, Assoc::After), (&mut self.head, Assoc::AfterSticky),
], ],
Ordering::Less => [ Ordering::Less => [
(&mut self.anchor, Assoc::After), (&mut self.anchor, Assoc::AfterSticky),
(&mut self.head, Assoc::Before), (&mut self.head, Assoc::BeforeSticky),
], ],
Ordering::Greater => [ Ordering::Greater => [
(&mut self.head, Assoc::After), (&mut self.head, Assoc::AfterSticky),
(&mut self.anchor, Assoc::Before), (&mut self.anchor, Assoc::BeforeSticky),
], ],
}; };
changes.update_positions(positions_to_map.into_iter()); changes.update_positions(positions_to_map.into_iter());
@ -374,6 +383,12 @@ impl Range {
let second = graphemes.next(); let second = graphemes.next();
first.is_some() && second.is_none() first.is_some() && second.is_none()
} }
/// Converts this char range into an in order byte range, discarding
/// direction.
pub fn into_byte_range(&self, text: RopeSlice) -> (usize, usize) {
(text.char_to_byte(self.from()), text.char_to_byte(self.to()))
}
} }
impl From<(usize, usize)> for Range { impl From<(usize, usize)> for Range {
@ -467,16 +482,16 @@ impl Selection {
range.old_visual_position = None; range.old_visual_position = None;
match range.anchor.cmp(&range.head) { match range.anchor.cmp(&range.head) {
Ordering::Equal => [ Ordering::Equal => [
(&mut range.anchor, Assoc::After), (&mut range.anchor, Assoc::AfterSticky),
(&mut range.head, Assoc::After), (&mut range.head, Assoc::AfterSticky),
], ],
Ordering::Less => [ Ordering::Less => [
(&mut range.anchor, Assoc::After), (&mut range.anchor, Assoc::AfterSticky),
(&mut range.head, Assoc::Before), (&mut range.head, Assoc::BeforeSticky),
], ],
Ordering::Greater => [ Ordering::Greater => [
(&mut range.head, Assoc::After), (&mut range.head, Assoc::AfterSticky),
(&mut range.anchor, Assoc::Before), (&mut range.anchor, Assoc::BeforeSticky),
], ],
} }
}); });
@ -488,6 +503,16 @@ impl Selection {
&self.ranges &self.ranges
} }
/// Returns an iterator over the line ranges of each range in the selection.
///
/// Adjacent and overlapping line ranges of the [Range]s in the selection are merged.
pub fn line_ranges<'a>(&'a self, text: RopeSlice<'a>) -> LineRangeIter<'a> {
LineRangeIter {
ranges: self.ranges.iter().peekable(),
text,
}
}
pub fn primary_index(&self) -> usize { pub fn primary_index(&self) -> usize {
self.primary_index self.primary_index
} }
@ -516,6 +541,8 @@ impl Selection {
} }
/// Normalizes a `Selection`. /// Normalizes a `Selection`.
///
/// Ranges are sorted by [Range::from], with overlapping ranges merged.
fn normalize(mut self) -> Self { fn normalize(mut self) -> Self {
if self.len() < 2 { if self.len() < 2 {
return self; return self;
@ -703,17 +730,53 @@ impl IntoIterator for Selection {
} }
} }
impl From<Range> for Selection {
fn from(range: Range) -> Self {
Self {
ranges: smallvec![range],
primary_index: 0,
}
}
}
pub struct LineRangeIter<'a> {
ranges: iter::Peekable<slice::Iter<'a, Range>>,
text: RopeSlice<'a>,
}
impl<'a> Iterator for LineRangeIter<'a> {
type Item = (usize, usize);
fn next(&mut self) -> Option<Self::Item> {
let (start, mut end) = self.ranges.next()?.line_range(self.text);
while let Some((next_start, next_end)) =
self.ranges.peek().map(|range| range.line_range(self.text))
{
// Merge overlapping and adjacent ranges.
// This subtraction cannot underflow because the ranges are sorted.
if next_start - end <= 1 {
end = next_end;
self.ranges.next();
} else {
break;
}
}
Some((start, end))
}
}
// TODO: checkSelection -> check if valid for doc length && sorted // TODO: checkSelection -> check if valid for doc length && sorted
pub fn keep_or_remove_matches( pub fn keep_or_remove_matches(
text: RopeSlice, text: RopeSlice,
selection: &Selection, selection: &Selection,
regex: &crate::regex::Regex, regex: &rope::Regex,
remove: bool, remove: bool,
) -> Option<Selection> { ) -> Option<Selection> {
let result: SmallVec<_> = selection let result: SmallVec<_> = selection
.iter() .iter()
.filter(|range| regex.is_match(&range.fragment(text)) ^ remove) .filter(|range| regex.is_match(text.regex_input_at(range.from()..range.to())) ^ remove)
.copied() .copied()
.collect(); .collect();
@ -724,25 +787,20 @@ pub fn keep_or_remove_matches(
None None
} }
// TODO: support to split on capture #N instead of whole match
pub fn select_on_matches( pub fn select_on_matches(
text: RopeSlice, text: RopeSlice,
selection: &Selection, selection: &Selection,
regex: &crate::regex::Regex, regex: &rope::Regex,
) -> Option<Selection> { ) -> Option<Selection> {
let mut result = SmallVec::with_capacity(selection.len()); let mut result = SmallVec::with_capacity(selection.len());
for sel in selection { for sel in selection {
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet for mat in regex.find_iter(text.regex_input_at(sel.from()..sel.to())) {
let fragment = sel.fragment(text);
let sel_start = sel.from();
let start_byte = text.char_to_byte(sel_start);
for mat in regex.find_iter(&fragment) {
// TODO: retain range direction // TODO: retain range direction
let start = text.byte_to_char(start_byte + mat.start()); let start = text.byte_to_char(mat.start());
let end = text.byte_to_char(start_byte + mat.end()); let end = text.byte_to_char(mat.end());
let range = Range::new(start, end); let range = Range::new(start, end);
// Make sure the match is not right outside of the selection. // Make sure the match is not right outside of the selection.
@ -761,12 +819,7 @@ pub fn select_on_matches(
None None
} }
// TODO: support to split on capture #N instead of whole match pub fn split_on_newline(text: RopeSlice, selection: &Selection) -> Selection {
pub fn split_on_matches(
text: RopeSlice,
selection: &Selection,
regex: &crate::regex::Regex,
) -> Selection {
let mut result = SmallVec::with_capacity(selection.len()); let mut result = SmallVec::with_capacity(selection.len());
for sel in selection { for sel in selection {
@ -776,21 +829,49 @@ pub fn split_on_matches(
continue; continue;
} }
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
let fragment = sel.fragment(text);
let sel_start = sel.from(); let sel_start = sel.from();
let sel_end = sel.to(); let sel_end = sel.to();
let start_byte = text.char_to_byte(sel_start); let mut start = sel_start;
for line in sel.slice(text).lines() {
let Some(line_ending) = get_line_ending(&line) else {
break;
};
let line_end = start + line.len_chars();
// TODO: retain range direction
result.push(Range::new(start, line_end - line_ending.len_chars()));
start = line_end;
}
if start < sel_end {
result.push(Range::new(start, sel_end));
}
}
// TODO: figure out a new primary index
Selection::new(result, 0)
}
pub fn split_on_matches(text: RopeSlice, selection: &Selection, regex: &rope::Regex) -> Selection {
let mut result = SmallVec::with_capacity(selection.len());
for sel in selection {
// Special case: zero-width selection.
if sel.from() == sel.to() {
result.push(*sel);
continue;
}
let sel_start = sel.from();
let sel_end = sel.to();
let mut start = sel_start; let mut start = sel_start;
for mat in regex.find_iter(&fragment) { for mat in regex.find_iter(text.regex_input_at(sel_start..sel_end)) {
// TODO: retain range direction // TODO: retain range direction
let end = text.byte_to_char(start_byte + mat.start()); let end = text.byte_to_char(mat.start());
result.push(Range::new(start, end)); result.push(Range::new(start, end));
start = text.byte_to_char(start_byte + mat.end()); start = text.byte_to_char(mat.end());
} }
if start < sel_end { if start < sel_end {
@ -1021,14 +1102,12 @@ mod test {
#[test] #[test]
fn test_select_on_matches() { fn test_select_on_matches() {
use crate::regex::{Regex, RegexBuilder};
let r = Rope::from_str("Nobody expects the Spanish inquisition"); let r = Rope::from_str("Nobody expects the Spanish inquisition");
let s = r.slice(..); let s = r.slice(..);
let selection = Selection::single(0, r.len_chars()); let selection = Selection::single(0, r.len_chars());
assert_eq!( assert_eq!(
select_on_matches(s, &selection, &Regex::new(r"[A-Z][a-z]*").unwrap()), select_on_matches(s, &selection, &rope::Regex::new(r"[A-Z][a-z]*").unwrap()),
Some(Selection::new( Some(Selection::new(
smallvec![Range::new(0, 6), Range::new(19, 26)], smallvec![Range::new(0, 6), Range::new(19, 26)],
0 0
@ -1038,8 +1117,14 @@ mod test {
let r = Rope::from_str("This\nString\n\ncontains multiple\nlines"); let r = Rope::from_str("This\nString\n\ncontains multiple\nlines");
let s = r.slice(..); let s = r.slice(..);
let start_of_line = RegexBuilder::new(r"^").multi_line(true).build().unwrap(); let start_of_line = rope::RegexBuilder::new()
let end_of_line = RegexBuilder::new(r"$").multi_line(true).build().unwrap(); .syntax(rope::Config::new().multi_line(true))
.build(r"^")
.unwrap();
let end_of_line = rope::RegexBuilder::new()
.syntax(rope::Config::new().multi_line(true))
.build(r"$")
.unwrap();
// line without ending // line without ending
assert_eq!( assert_eq!(
@ -1077,9 +1162,9 @@ mod test {
select_on_matches( select_on_matches(
s, s,
&Selection::single(0, s.len_chars()), &Selection::single(0, s.len_chars()),
&RegexBuilder::new(r"^[a-z ]*$") &rope::RegexBuilder::new()
.multi_line(true) .syntax(rope::Config::new().multi_line(true))
.build() .build(r"^[a-z ]*$")
.unwrap() .unwrap()
), ),
Some(Selection::new( Some(Selection::new(
@ -1119,6 +1204,32 @@ mod test {
assert_eq!(Range::new(12, 0).line_range(s), (0, 2)); assert_eq!(Range::new(12, 0).line_range(s), (0, 2));
} }
#[test]
fn selection_line_ranges() {
let (text, selection) = crate::test::print(
r#" L0
#[|these]# line #(|ranges)# are #(|merged)# L1
L2
single one-line #(|range)# L3
L4
single #(|multiline L5
range)# L6
L7
these #(|multiline L8
ranges)# are #(|also L9
merged)# L10
L11
adjacent #(|ranges)# L12
are merged #(|the same way)# L13
"#,
);
let rope = Rope::from_str(&text);
assert_eq!(
vec![(1, 1), (3, 3), (5, 6), (8, 10), (12, 13)],
selection.line_ranges(rope.slice(..)).collect::<Vec<_>>(),
);
}
#[test] #[test]
fn test_cursor() { fn test_cursor() {
let r = Rope::from_str("\r\nHi\r\nthere!"); let r = Rope::from_str("\r\nHi\r\nthere!");
@ -1171,13 +1282,15 @@ mod test {
#[test] #[test]
fn test_split_on_matches() { fn test_split_on_matches() {
use crate::regex::Regex;
let text = Rope::from(" abcd efg wrs xyz 123 456"); let text = Rope::from(" abcd efg wrs xyz 123 456");
let selection = Selection::new(smallvec![Range::new(0, 9), Range::new(11, 20),], 0); let selection = Selection::new(smallvec![Range::new(0, 9), Range::new(11, 20),], 0);
let result = split_on_matches(text.slice(..), &selection, &Regex::new(r"\s+").unwrap()); let result = split_on_matches(
text.slice(..),
&selection,
&rope::Regex::new(r"\s+").unwrap(),
);
assert_eq!( assert_eq!(
result.ranges(), result.ranges(),

@ -1,18 +1,16 @@
use std::fmt::Display; use std::fmt::Display;
use crate::{movement::Direction, search, Range, Selection}; use crate::{
graphemes::next_grapheme_boundary,
match_brackets::{
find_matching_bracket, find_matching_bracket_fuzzy, get_pair, is_close_bracket,
is_open_bracket,
},
movement::Direction,
search, Range, Selection, Syntax,
};
use ropey::RopeSlice; use ropey::RopeSlice;
pub const PAIRS: &[(char, char)] = &[
('(', ')'),
('[', ']'),
('{', '}'),
('<', '>'),
('«', '»'),
('「', '」'),
('', ''),
];
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum Error { pub enum Error {
PairNotFound, PairNotFound,
@ -34,32 +32,68 @@ impl Display for Error {
type Result<T> = std::result::Result<T, Error>; type Result<T> = std::result::Result<T, Error>;
/// Given any char in [PAIRS], return the open and closing chars. If not found in /// Finds the position of surround pairs of any [`crate::match_brackets::PAIRS`]
/// [PAIRS] return (ch, ch). /// using tree-sitter when possible.
/// ///
/// ``` /// # Returns
/// use helix_core::surround::get_pair;
/// ///
/// assert_eq!(get_pair('['), ('[', ']')); /// Tuple `(anchor, head)`, meaning it is not always ordered.
/// assert_eq!(get_pair('}'), ('{', '}')); pub fn find_nth_closest_pairs_pos(
/// assert_eq!(get_pair('"'), ('"', '"')); syntax: Option<&Syntax>,
/// ``` text: RopeSlice,
pub fn get_pair(ch: char) -> (char, char) { range: Range,
PAIRS skip: usize,
.iter() ) -> Result<(usize, usize)> {
.find(|(open, close)| *open == ch || *close == ch) match syntax {
.copied() Some(syntax) => find_nth_closest_pairs_ts(syntax, text, range, skip),
.unwrap_or((ch, ch)) None => find_nth_closest_pairs_plain(text, range, skip),
}
} }
pub fn find_nth_closest_pairs_pos( fn find_nth_closest_pairs_ts(
syntax: &Syntax,
text: RopeSlice, text: RopeSlice,
range: Range, range: Range,
mut skip: usize, mut skip: usize,
) -> Result<(usize, usize)> { ) -> Result<(usize, usize)> {
let is_open_pair = |ch| PAIRS.iter().any(|(open, _)| *open == ch); let mut opening = range.from();
let is_close_pair = |ch| PAIRS.iter().any(|(_, close)| *close == ch); // We want to expand the selection if we are already on the found pair,
// otherwise we would need to subtract "-1" from "range.to()".
let mut closing = range.to();
while skip > 0 {
closing = find_matching_bracket_fuzzy(syntax, text, closing).ok_or(Error::PairNotFound)?;
opening = find_matching_bracket(syntax, text, closing).ok_or(Error::PairNotFound)?;
// If we're already on a closing bracket "find_matching_bracket_fuzzy" will return
// the position of the opening bracket.
if closing < opening {
(opening, closing) = (closing, opening);
}
// In case found brackets are partially inside current selection.
if range.from() < opening || closing < range.to() - 1 {
closing = next_grapheme_boundary(text, closing);
} else {
skip -= 1;
if skip != 0 {
closing = next_grapheme_boundary(text, closing);
}
}
}
// Keep the original direction.
if let Direction::Forward = range.direction() {
Ok((opening, closing))
} else {
Ok((closing, opening))
}
}
fn find_nth_closest_pairs_plain(
text: RopeSlice,
range: Range,
mut skip: usize,
) -> Result<(usize, usize)> {
let mut stack = Vec::with_capacity(2); let mut stack = Vec::with_capacity(2);
let pos = range.from(); let pos = range.from();
let mut close_pos = pos.saturating_sub(1); let mut close_pos = pos.saturating_sub(1);
@ -67,7 +101,7 @@ pub fn find_nth_closest_pairs_pos(
for ch in text.chars_at(pos) { for ch in text.chars_at(pos) {
close_pos += 1; close_pos += 1;
if is_open_pair(ch) { if is_open_bracket(ch) {
// Track open pairs encountered so that we can step over // Track open pairs encountered so that we can step over
// the corresponding close pairs that will come up further // the corresponding close pairs that will come up further
// down the loop. We want to find a lone close pair whose // down the loop. We want to find a lone close pair whose
@ -76,7 +110,7 @@ pub fn find_nth_closest_pairs_pos(
continue; continue;
} }
if !is_close_pair(ch) { if !is_close_bracket(ch) {
// We don't care if this character isn't a brace pair item, // We don't care if this character isn't a brace pair item,
// so short circuit here. // so short circuit here.
continue; continue;
@ -157,7 +191,11 @@ pub fn find_nth_pairs_pos(
) )
}; };
Option::zip(open, close).ok_or(Error::PairNotFound) // preserve original direction
match range.direction() {
Direction::Forward => Option::zip(open, close).ok_or(Error::PairNotFound),
Direction::Backward => Option::zip(close, open).ok_or(Error::PairNotFound),
}
} }
fn find_nth_open_pair( fn find_nth_open_pair(
@ -167,6 +205,10 @@ fn find_nth_open_pair(
mut pos: usize, mut pos: usize,
n: usize, n: usize,
) -> Option<usize> { ) -> Option<usize> {
if pos >= text.len_chars() {
return None;
}
let mut chars = text.chars_at(pos + 1); let mut chars = text.chars_at(pos + 1);
// Adjusts pos for the first iteration, and handles the case of the // Adjusts pos for the first iteration, and handles the case of the
@ -245,6 +287,7 @@ fn find_nth_close_pair(
/// are automatically detected around each cursor (note that this may result /// are automatically detected around each cursor (note that this may result
/// in them selecting different surround characters for each selection). /// in them selecting different surround characters for each selection).
pub fn get_surround_pos( pub fn get_surround_pos(
syntax: Option<&Syntax>,
text: RopeSlice, text: RopeSlice,
selection: &Selection, selection: &Selection,
ch: Option<char>, ch: Option<char>,
@ -253,14 +296,19 @@ pub fn get_surround_pos(
let mut change_pos = Vec::new(); let mut change_pos = Vec::new();
for &range in selection { for &range in selection {
let (open_pos, close_pos) = match ch { let (open_pos, close_pos) = {
let range_raw = match ch {
Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?, Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?,
None => find_nth_closest_pairs_pos(text, range, skip)?, None => find_nth_closest_pairs_pos(syntax, text, range, skip)?,
};
let range = Range::new(range_raw.0, range_raw.1);
(range.from(), range.to())
}; };
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) { if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return Err(Error::CursorOverlap); return Err(Error::CursorOverlap);
} }
change_pos.extend_from_slice(&[open_pos, close_pos]); // ensure the positions are always paired in the forward direction
change_pos.extend_from_slice(&[open_pos.min(close_pos), close_pos.max(open_pos)]);
} }
Ok(change_pos) Ok(change_pos)
} }
@ -283,7 +331,7 @@ mod test {
); );
assert_eq!( assert_eq!(
get_surround_pos(doc.slice(..), &selection, Some('('), 1).unwrap(), get_surround_pos(None, doc.slice(..), &selection, Some('('), 1).unwrap(),
expectations expectations
); );
} }
@ -298,7 +346,7 @@ mod test {
); );
assert_eq!( assert_eq!(
get_surround_pos(doc.slice(..), &selection, Some('('), 1), get_surround_pos(None, doc.slice(..), &selection, Some('('), 1),
Err(Error::PairNotFound) Err(Error::PairNotFound)
); );
} }
@ -313,7 +361,7 @@ mod test {
); );
assert_eq!( assert_eq!(
get_surround_pos(doc.slice(..), &selection, Some('('), 1), get_surround_pos(None, doc.slice(..), &selection, Some('('), 1),
Err(Error::PairNotFound) // overlapping surround chars Err(Error::PairNotFound) // overlapping surround chars
); );
} }
@ -328,7 +376,7 @@ mod test {
); );
assert_eq!( assert_eq!(
get_surround_pos(doc.slice(..), &selection, Some('['), 1), get_surround_pos(None, doc.slice(..), &selection, Some('['), 1),
Err(Error::CursorOverlap) Err(Error::CursorOverlap)
); );
} }
@ -382,6 +430,21 @@ mod test {
) )
} }
#[test]
fn test_find_nth_closest_pairs_pos_index_range_panic() {
#[rustfmt::skip]
let (doc, selection, _) =
rope_with_selections_and_expectations(
"(a)c)",
"^^^^^"
);
assert_eq!(
find_nth_closest_pairs_pos(None, doc.slice(..), selection.primary(), 1),
Err(Error::PairNotFound)
)
}
// Create a Rope and a matching Selection using a specification language. // Create a Rope and a matching Selection using a specification language.
// ^ is a single-point selection. // ^ is a single-point selection.
// _ is an expected index. These are returned as a Vec<usize> for use in assertions. // _ is an expected index. These are returned as a Vec<usize> for use in assertions.

@ -1,3 +1,5 @@
mod tree_cursor;
use crate::{ use crate::{
auto_pairs::AutoPairs, auto_pairs::AutoPairs,
chars::char_is_line_ending, chars::char_is_line_ending,
@ -10,16 +12,18 @@ use crate::{
use ahash::RandomState; use ahash::RandomState;
use arc_swap::{ArcSwap, Guard}; use arc_swap::{ArcSwap, Guard};
use bitflags::bitflags; use bitflags::bitflags;
use globset::GlobSet;
use hashbrown::raw::RawTable; use hashbrown::raw::RawTable;
use helix_stdx::rope::{self, RopeSliceExt};
use slotmap::{DefaultKey as LayerId, HopSlotMap}; use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet, VecDeque}, collections::{HashMap, HashSet, VecDeque},
fmt::{self, Display}, fmt::{self, Display, Write},
hash::{Hash, Hasher}, hash::{Hash, Hasher},
mem::{replace, transmute}, mem::replace,
path::{Path, PathBuf}, path::{Path, PathBuf},
str::FromStr, str::FromStr,
sync::Arc, sync::Arc,
@ -30,6 +34,8 @@ use serde::{ser::SerializeSeq, Deserialize, Serialize};
use helix_loader::grammar::{get_language, load_runtime_file}; use helix_loader::grammar::{get_language, load_runtime_file};
pub use tree_cursor::TreeCursor;
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error> fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
where where
D: serde::Deserializer<'de>, D: serde::Deserializer<'de>,
@ -82,12 +88,6 @@ pub struct Configuration {
pub language_server: HashMap<String, LanguageServerConfiguration>, pub language_server: HashMap<String, LanguageServerConfiguration>,
} }
impl Default for Configuration {
fn default() -> Self {
crate::config::default_syntax_loader()
}
}
// largely based on tree-sitter/cli/src/loader.rs // largely based on tree-sitter/cli/src/loader.rs
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
@ -103,7 +103,19 @@ pub struct LanguageConfiguration {
pub shebangs: Vec<String>, // interpreter(s) associated with language pub shebangs: Vec<String>, // interpreter(s) associated with language
#[serde(default)] #[serde(default)]
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml> pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>, #[serde(
default,
skip_serializing,
deserialize_with = "from_comment_tokens",
alias = "comment-token"
)]
pub comment_tokens: Option<Vec<String>>,
#[serde(
default,
skip_serializing,
deserialize_with = "from_block_comment_tokens"
)]
pub block_comment_tokens: Option<Vec<BlockCommentToken>>,
pub text_width: Option<usize>, pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>, pub soft_wrap: Option<SoftWrap>,
@ -155,6 +167,8 @@ pub struct LanguageConfiguration {
/// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`. /// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
/// Falling back to the current working directory if none are configured. /// Falling back to the current working directory if none are configured.
pub workspace_lsp_roots: Option<Vec<PathBuf>>, pub workspace_lsp_roots: Option<Vec<PathBuf>>,
#[serde(default)]
pub persistent_diagnostic_sources: Vec<String>,
} }
#[derive(Debug, PartialEq, Eq, Hash)] #[derive(Debug, PartialEq, Eq, Hash)]
@ -162,9 +176,11 @@ pub enum FileType {
/// The extension of the file, either the `Path::extension` or the full /// The extension of the file, either the `Path::extension` or the full
/// filename if the file does not have an extension. /// filename if the file does not have an extension.
Extension(String), Extension(String),
/// The suffix of a file. This is compared to a given file's absolute /// A Unix-style path glob. This is compared to the file's absolute path, so
/// path, so it can be used to detect files based on their directories. /// it can be used to detect files based on their directories. If the glob
Suffix(String), /// is not an absolute path and does not already start with a glob pattern,
/// a glob pattern will be prepended to it.
Glob(globset::Glob),
} }
impl Serialize for FileType { impl Serialize for FileType {
@ -176,9 +192,9 @@ impl Serialize for FileType {
match self { match self {
FileType::Extension(extension) => serializer.serialize_str(extension), FileType::Extension(extension) => serializer.serialize_str(extension),
FileType::Suffix(suffix) => { FileType::Glob(glob) => {
let mut map = serializer.serialize_map(Some(1))?; let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?; map.serialize_entry("glob", glob.glob())?;
map.end() map.end()
} }
} }
@ -211,9 +227,20 @@ impl<'de> Deserialize<'de> for FileType {
M: serde::de::MapAccess<'de>, M: serde::de::MapAccess<'de>,
{ {
match map.next_entry::<String, String>()? { match map.next_entry::<String, String>()? {
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({ Some((key, mut glob)) if key == "glob" => {
suffix.replace('/', std::path::MAIN_SEPARATOR_STR) // If the glob isn't an absolute path or already starts
})), // with a glob pattern, add a leading glob so we
// properly match relative paths.
if !glob.starts_with('/') && !glob.starts_with("*/") {
glob.insert_str(0, "*/");
}
globset::Glob::new(glob.as_str())
.map(FileType::Glob)
.map_err(|err| {
serde::de::Error::custom(format!("invalid `glob` pattern: {}", err))
})
}
Some((key, _value)) => Err(serde::de::Error::custom(format!( Some((key, _value)) => Err(serde::de::Error::custom(format!(
"unknown key in `file-types` list: {}", "unknown key in `file-types` list: {}",
key key
@ -229,6 +256,59 @@ impl<'de> Deserialize<'de> for FileType {
} }
} }
fn from_comment_tokens<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum CommentTokens {
Multiple(Vec<String>),
Single(String),
}
Ok(
Option::<CommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
CommentTokens::Single(val) => vec![val],
CommentTokens::Multiple(vals) => vals,
}),
)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BlockCommentToken {
pub start: String,
pub end: String,
}
impl Default for BlockCommentToken {
fn default() -> Self {
BlockCommentToken {
start: "/*".to_string(),
end: "*/".to_string(),
}
}
}
fn from_block_comment_tokens<'de, D>(
deserializer: D,
) -> Result<Option<Vec<BlockCommentToken>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum BlockCommentTokens {
Multiple(Vec<BlockCommentToken>),
Single(BlockCommentToken),
}
Ok(
Option::<BlockCommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
BlockCommentTokens::Single(val) => vec![val],
BlockCommentTokens::Multiple(vals) => vals,
}),
)
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)] #[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub enum LanguageServerFeature { pub enum LanguageServerFeature {
@ -261,7 +341,7 @@ impl Display for LanguageServerFeature {
GotoDeclaration => "goto-declaration", GotoDeclaration => "goto-declaration",
GotoDefinition => "goto-definition", GotoDefinition => "goto-definition",
GotoTypeDefinition => "goto-type-definition", GotoTypeDefinition => "goto-type-definition",
GotoReference => "goto-type-definition", GotoReference => "goto-reference",
GotoImplementation => "goto-implementation", GotoImplementation => "goto-implementation",
SignatureHelp => "signature-help", SignatureHelp => "signature-help",
Hover => "hover", Hover => "hover",
@ -356,6 +436,22 @@ where
serializer.end() serializer.end()
} }
fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
where
D: serde::Deserializer<'de>,
{
let patterns = Vec::<String>::deserialize(deserializer)?;
if patterns.is_empty() {
return Ok(None);
}
let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns {
let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
builder.add(glob);
}
builder.build().map(Some).map_err(serde::de::Error::custom)
}
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration { pub struct LanguageServerConfiguration {
@ -369,6 +465,12 @@ pub struct LanguageServerConfiguration {
pub config: Option<serde_json::Value>, pub config: Option<serde_json::Value>,
#[serde(default = "default_timeout")] #[serde(default = "default_timeout")]
pub timeout: u64, pub timeout: u64,
#[serde(
default,
skip_serializing,
deserialize_with = "deserialize_required_root_patterns"
)]
pub required_root_patterns: Option<GlobSet>,
} }
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
@ -408,6 +510,7 @@ pub enum DebugArgumentValue {
pub struct DebugTemplate { pub struct DebugTemplate {
pub name: String, pub name: String,
pub request: String, pub request: String,
#[serde(default)]
pub completion: Vec<DebugConfigCompletion>, pub completion: Vec<DebugConfigCompletion>,
pub args: HashMap<String, DebugArgumentValue>, pub args: HashMap<String, DebugArgumentValue>,
} }
@ -626,8 +729,11 @@ pub fn read_query(language: &str, filename: &str) -> String {
.replace_all(&query, |captures: &regex::Captures| { .replace_all(&query, |captures: &regex::Captures| {
captures[1] captures[1]
.split(',') .split(',')
.map(|language| format!("\n{}\n", read_query(language, filename))) .fold(String::new(), |mut output, language| {
.collect::<String>() // `write!` to a String cannot fail.
write!(output, "\n{}\n", read_query(language, filename)).unwrap();
output
})
}) })
.to_string() .to_string()
} }
@ -707,7 +813,7 @@ impl LanguageConfiguration {
if query_text.is_empty() { if query_text.is_empty() {
return None; return None;
} }
let lang = self.highlight_config.get()?.as_ref()?.language; let lang = &self.highlight_config.get()?.as_ref()?.language;
Query::new(lang, &query_text) Query::new(lang, &query_text)
.map_err(|e| { .map_err(|e| {
log::error!( log::error!(
@ -750,6 +856,47 @@ pub struct SoftWrap {
pub wrap_at_text_width: Option<bool>, pub wrap_at_text_width: Option<bool>,
} }
#[derive(Debug)]
struct FileTypeGlob {
glob: globset::Glob,
language_id: usize,
}
impl FileTypeGlob {
fn new(glob: globset::Glob, language_id: usize) -> Self {
Self { glob, language_id }
}
}
#[derive(Debug)]
struct FileTypeGlobMatcher {
matcher: globset::GlobSet,
file_types: Vec<FileTypeGlob>,
}
impl FileTypeGlobMatcher {
fn new(file_types: Vec<FileTypeGlob>) -> Result<Self, globset::Error> {
let mut builder = globset::GlobSetBuilder::new();
for file_type in &file_types {
builder.add(file_type.glob.clone());
}
Ok(Self {
matcher: builder.build()?,
file_types,
})
}
fn language_id_for_path(&self, path: &Path) -> Option<&usize> {
self.matcher
.matches(path)
.iter()
.filter_map(|idx| self.file_types.get(*idx))
.max_by_key(|file_type| file_type.glob.glob().len())
.map(|file_type| &file_type.language_id)
}
}
// Expose loader as Lazy<> global since it's always static? // Expose loader as Lazy<> global since it's always static?
#[derive(Debug)] #[derive(Debug)]
@ -757,7 +904,7 @@ pub struct Loader {
// highlight_names ? // highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>, language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_extension: HashMap<String, usize>, // Vec<usize> language_config_ids_by_extension: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_suffix: HashMap<String, usize>, language_config_ids_glob_matcher: FileTypeGlobMatcher,
language_config_ids_by_shebang: HashMap<String, usize>, language_config_ids_by_shebang: HashMap<String, usize>,
language_server_configs: HashMap<String, LanguageServerConfiguration>, language_server_configs: HashMap<String, LanguageServerConfiguration>,
@ -765,66 +912,57 @@ pub struct Loader {
scopes: ArcSwap<Vec<String>>, scopes: ArcSwap<Vec<String>>,
} }
pub type LoaderError = globset::Error;
impl Loader { impl Loader {
pub fn new(config: Configuration) -> Self { pub fn new(config: Configuration) -> Result<Self, LoaderError> {
let mut loader = Self { let mut language_configs = Vec::new();
language_configs: Vec::new(), let mut language_config_ids_by_extension = HashMap::new();
language_server_configs: config.language_server, let mut language_config_ids_by_shebang = HashMap::new();
language_config_ids_by_extension: HashMap::new(), let mut file_type_globs = Vec::new();
language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
scopes: ArcSwap::from_pointee(Vec::new()),
};
for config in config.language { for config in config.language {
// get the next id // get the next id
let language_id = loader.language_configs.len(); let language_id = language_configs.len();
for file_type in &config.file_types { for file_type in &config.file_types {
// entry().or_insert(Vec::new).push(language_id); // entry().or_insert(Vec::new).push(language_id);
match file_type { match file_type {
FileType::Extension(extension) => loader FileType::Extension(extension) => {
.language_config_ids_by_extension language_config_ids_by_extension.insert(extension.clone(), language_id);
.insert(extension.clone(), language_id), }
FileType::Suffix(suffix) => loader FileType::Glob(glob) => {
.language_config_ids_by_suffix file_type_globs.push(FileTypeGlob::new(glob.to_owned(), language_id));
.insert(suffix.clone(), language_id), }
}; };
} }
for shebang in &config.shebangs { for shebang in &config.shebangs {
loader language_config_ids_by_shebang.insert(shebang.clone(), language_id);
.language_config_ids_by_shebang
.insert(shebang.clone(), language_id);
} }
loader.language_configs.push(Arc::new(config)); language_configs.push(Arc::new(config));
} }
loader Ok(Self {
language_configs,
language_config_ids_by_extension,
language_config_ids_glob_matcher: FileTypeGlobMatcher::new(file_type_globs)?,
language_config_ids_by_shebang,
language_server_configs: config.language_server,
scopes: ArcSwap::from_pointee(Vec::new()),
})
} }
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> { pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
// Find all the language configurations that match this file name // Find all the language configurations that match this file name
// or a suffix of the file name. // or a suffix of the file name.
let configuration_id = path let configuration_id = self
.file_name() .language_config_ids_glob_matcher
.and_then(|n| n.to_str()) .language_id_for_path(path)
.and_then(|file_name| self.language_config_ids_by_extension.get(file_name))
.or_else(|| { .or_else(|| {
path.extension() path.extension()
.and_then(|extension| extension.to_str()) .and_then(|extension| extension.to_str())
.and_then(|extension| self.language_config_ids_by_extension.get(extension)) .and_then(|extension| self.language_config_ids_by_extension.get(extension))
})
.or_else(|| {
self.language_config_ids_by_suffix
.iter()
.find_map(|(file_type, id)| {
if path.to_str()?.ends_with(file_type) {
Some(id)
} else {
None
}
})
}); });
configuration_id.and_then(|&id| self.language_configs.get(id).cloned()) configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
@ -887,9 +1025,10 @@ impl Loader {
match capture { match capture {
InjectionLanguageMarker::Name(string) => self.language_config_for_name(string), InjectionLanguageMarker::Name(string) => self.language_config_for_name(string),
InjectionLanguageMarker::Filename(file) => self.language_config_for_file_name(file), InjectionLanguageMarker::Filename(file) => self.language_config_for_file_name(file),
InjectionLanguageMarker::Shebang(shebang) => { InjectionLanguageMarker::Shebang(shebang) => self
self.language_config_for_language_id(shebang) .language_config_ids_by_shebang
} .get(shebang)
.and_then(|&id| self.language_configs.get(id).cloned()),
} }
} }
@ -936,7 +1075,7 @@ thread_local! {
pub struct Syntax { pub struct Syntax {
layers: HopSlotMap<LayerId, LanguageLayer>, layers: HopSlotMap<LayerId, LanguageLayer>,
root: LayerId, root: LayerId,
loader: Arc<Loader>, loader: Arc<ArcSwap<Loader>>,
} }
fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> { fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> {
@ -947,7 +1086,7 @@ impl Syntax {
pub fn new( pub fn new(
source: RopeSlice, source: RopeSlice,
config: Arc<HighlightConfiguration>, config: Arc<HighlightConfiguration>,
loader: Arc<Loader>, loader: Arc<ArcSwap<Loader>>,
) -> Option<Self> { ) -> Option<Self> {
let root_layer = LanguageLayer { let root_layer = LanguageLayer {
tree: None, tree: None,
@ -960,6 +1099,7 @@ impl Syntax {
start_point: Point::new(0, 0), start_point: Point::new(0, 0),
end_point: Point::new(usize::MAX, usize::MAX), end_point: Point::new(usize::MAX, usize::MAX),
}], }],
parent: None,
}; };
// track scope_descriptor: a Vec of scopes for item in tree // track scope_descriptor: a Vec of scopes for item in tree
@ -991,9 +1131,10 @@ impl Syntax {
let mut queue = VecDeque::new(); let mut queue = VecDeque::new();
queue.push_back(self.root); queue.push_back(self.root);
let scopes = self.loader.scopes.load(); let loader = self.loader.load();
let scopes = loader.scopes.load();
let injection_callback = |language: &InjectionLanguageMarker| { let injection_callback = |language: &InjectionLanguageMarker| {
self.loader loader
.language_configuration_for_injection_string(language) .language_configuration_for_injection_string(language)
.and_then(|language_config| language_config.highlight_config(&scopes)) .and_then(|language_config| language_config.highlight_config(&scopes))
}; };
@ -1108,7 +1249,7 @@ impl Syntax {
PARSER.with(|ts_parser| { PARSER.with(|ts_parser| {
let ts_parser = &mut ts_parser.borrow_mut(); let ts_parser = &mut ts_parser.borrow_mut();
ts_parser.parser.set_timeout_micros(1000 * 500); // half a second is pretty generours ts_parser.parser.set_timeout_micros(1000 * 500); // half a second is pretty generours
let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new); let mut cursor = ts_parser.cursors.pop().unwrap_or_default();
// TODO: might need to set cursor range // TODO: might need to set cursor range
cursor.set_byte_range(0..usize::MAX); cursor.set_byte_range(0..usize::MAX);
cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT); cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
@ -1223,12 +1364,14 @@ impl Syntax {
let depth = layer.depth + 1; let depth = layer.depth + 1;
// TODO: can't inline this since matches borrows self.layers // TODO: can't inline this since matches borrows self.layers
for (config, ranges) in injections { for (config, ranges) in injections {
let parent = Some(layer_id);
let new_layer = LanguageLayer { let new_layer = LanguageLayer {
tree: None, tree: None,
config, config,
depth, depth,
ranges, ranges,
flags: LayerUpdateFlags::empty(), flags: LayerUpdateFlags::empty(),
parent: None,
}; };
// Find an identical existing layer // Find an identical existing layer
@ -1240,6 +1383,7 @@ impl Syntax {
// ...or insert a new one. // ...or insert a new one.
let layer_id = layer.unwrap_or_else(|| self.layers.insert(new_layer)); let layer_id = layer.unwrap_or_else(|| self.layers.insert(new_layer));
self.layers[layer_id].parent = parent;
queue.push_back(layer_id); queue.push_back(layer_id);
} }
@ -1281,14 +1425,17 @@ impl Syntax {
// Reuse a cursor from the pool if available. // Reuse a cursor from the pool if available.
let mut cursor = PARSER.with(|ts_parser| { let mut cursor = PARSER.with(|ts_parser| {
let highlighter = &mut ts_parser.borrow_mut(); let highlighter = &mut ts_parser.borrow_mut();
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) highlighter.cursors.pop().unwrap_or_default()
}); });
// The `captures` iterator borrows the `Tree` and the `QueryCursor`, which // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
// prevents them from being moved. But both of these values are really just // prevents them from being moved. But both of these values are really just
// pointers, so it's actually ok to move them. // pointers, so it's actually ok to move them.
let cursor_ref = let cursor_ref = unsafe {
unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; mem::transmute::<&mut tree_sitter::QueryCursor, &mut tree_sitter::QueryCursor>(
&mut cursor,
)
};
// if reusing cursors & no range this resets to whole range // if reusing cursors & no range this resets to whole range
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
@ -1336,6 +1483,38 @@ impl Syntax {
result result
} }
pub fn tree_for_byte_range(&self, start: usize, end: usize) -> &Tree {
let mut container_id = self.root;
for (layer_id, layer) in self.layers.iter() {
if layer.depth > self.layers[container_id].depth
&& layer.contains_byte_range(start, end)
{
container_id = layer_id;
}
}
self.layers[container_id].tree()
}
pub fn named_descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
self.tree_for_byte_range(start, end)
.root_node()
.named_descendant_for_byte_range(start, end)
}
pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
self.tree_for_byte_range(start, end)
.root_node()
.descendant_for_byte_range(start, end)
}
pub fn walk(&self) -> TreeCursor<'_> {
// data structure to find the smallest range that contains a point
// when some of the ranges in the structure can overlap.
TreeCursor::new(&self.layers, self.root)
}
// Commenting // Commenting
// comment_strings_for_pos // comment_strings_for_pos
// is_commented // is_commented
@ -1368,6 +1547,7 @@ pub struct LanguageLayer {
pub ranges: Vec<Range>, pub ranges: Vec<Range>,
pub depth: u32, pub depth: u32,
flags: LayerUpdateFlags, flags: LayerUpdateFlags,
parent: Option<LayerId>,
} }
/// This PartialEq implementation only checks if that /// This PartialEq implementation only checks if that
@ -1387,13 +1567,7 @@ impl PartialEq for LanguageLayer {
impl Hash for LanguageLayer { impl Hash for LanguageLayer {
fn hash<H: Hasher>(&self, state: &mut H) { fn hash<H: Hasher>(&self, state: &mut H) {
self.depth.hash(state); self.depth.hash(state);
// The transmute is necessary here because tree_sitter::Language does not derive Hash at the moment. self.config.language.hash(state);
// However it does use #[repr] transparent so the transmute here is safe
// as `Language` (which `Grammar` is an alias for) is just a newtype wrapper around a (thin) pointer.
// This is also compatible with the PartialEq implementation of language
// as that is just a pointer comparison.
let language: *const () = unsafe { transmute(self.config.language) };
language.hash(state);
self.ranges.hash(state); self.ranges.hash(state);
} }
} }
@ -1410,7 +1584,7 @@ impl LanguageLayer {
.map_err(|_| Error::InvalidRanges)?; .map_err(|_| Error::InvalidRanges)?;
parser parser
.set_language(self.config.language) .set_language(&self.config.language)
.map_err(|_| Error::InvalidLanguage)?; .map_err(|_| Error::InvalidLanguage)?;
// unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) }; // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
@ -1432,6 +1606,32 @@ impl LanguageLayer {
self.tree = Some(tree); self.tree = Some(tree);
Ok(()) Ok(())
} }
/// Whether the layer contains the given byte range.
///
/// If the layer has multiple ranges (i.e. combined injections), the
/// given range is considered contained if it is within the start and
/// end bytes of the first and last ranges **and** if the given range
/// starts or ends within any of the layer's ranges.
fn contains_byte_range(&self, start: usize, end: usize) -> bool {
let layer_start = self
.ranges
.first()
.expect("ranges should not be empty")
.start_byte;
let layer_end = self
.ranges
.last()
.expect("ranges should not be empty")
.end_byte;
layer_start <= start
&& layer_end >= end
&& self.ranges.iter().any(|range| {
let byte_range = range.start_byte..range.end_byte;
byte_range.contains(&start) || byte_range.contains(&end)
})
}
} }
pub(crate) fn generate_edits( pub(crate) fn generate_edits(
@ -1540,10 +1740,10 @@ pub(crate) fn generate_edits(
} }
use std::sync::atomic::{AtomicUsize, Ordering}; use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize}; use std::{iter, mem, ops, str};
use tree_sitter::{ use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError, Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
QueryMatch, Range, TextProvider, Tree, TreeCursor, QueryMatch, Range, TextProvider, Tree,
}; };
const CANCELLATION_CHECK_INTERVAL: usize = 100; const CANCELLATION_CHECK_INTERVAL: usize = 100;
@ -1684,7 +1884,7 @@ impl HighlightConfiguration {
// Construct a single query by concatenating the three query strings, but record the // Construct a single query by concatenating the three query strings, but record the
// range of pattern indices that belong to each individual string. // range of pattern indices that belong to each individual string.
let query = Query::new(language, &query_source)?; let query = Query::new(&language, &query_source)?;
let mut highlights_pattern_index = 0; let mut highlights_pattern_index = 0;
for i in 0..(query.pattern_count()) { for i in 0..(query.pattern_count()) {
let pattern_offset = query.start_byte_for_pattern(i); let pattern_offset = query.start_byte_for_pattern(i);
@ -1693,7 +1893,7 @@ impl HighlightConfiguration {
} }
} }
let injections_query = Query::new(language, injection_query)?; let injections_query = Query::new(&language, injection_query)?;
let combined_injections_patterns = (0..injections_query.pattern_count()) let combined_injections_patterns = (0..injections_query.pattern_count())
.filter(|&i| { .filter(|&i| {
injections_query injections_query
@ -1725,7 +1925,7 @@ impl HighlightConfiguration {
let mut local_scope_capture_index = None; let mut local_scope_capture_index = None;
for (i, name) in query.capture_names().iter().enumerate() { for (i, name) in query.capture_names().iter().enumerate() {
let i = Some(i as u32); let i = Some(i as u32);
match name.as_str() { match *name {
"local.definition" => local_def_capture_index = i, "local.definition" => local_def_capture_index = i,
"local.definition-value" => local_def_value_capture_index = i, "local.definition-value" => local_def_value_capture_index = i,
"local.reference" => local_ref_capture_index = i, "local.reference" => local_ref_capture_index = i,
@ -1736,7 +1936,7 @@ impl HighlightConfiguration {
for (i, name) in injections_query.capture_names().iter().enumerate() { for (i, name) in injections_query.capture_names().iter().enumerate() {
let i = Some(i as u32); let i = Some(i as u32);
match name.as_str() { match *name {
"injection.content" => injection_content_capture_index = i, "injection.content" => injection_content_capture_index = i,
"injection.language" => injection_language_capture_index = i, "injection.language" => injection_language_capture_index = i,
"injection.filename" => injection_filename_capture_index = i, "injection.filename" => injection_filename_capture_index = i,
@ -1766,7 +1966,7 @@ impl HighlightConfiguration {
} }
/// Get a slice containing all of the highlight names used in the configuration. /// Get a slice containing all of the highlight names used in the configuration.
pub fn names(&self) -> &[String] { pub fn names(&self) -> &[&str] {
self.query.capture_names() self.query.capture_names()
} }
@ -1793,7 +1993,6 @@ impl HighlightConfiguration {
let mut best_index = None; let mut best_index = None;
let mut best_match_len = 0; let mut best_match_len = 0;
for (i, recognized_name) in recognized_names.iter().enumerate() { for (i, recognized_name) in recognized_names.iter().enumerate() {
let recognized_name = recognized_name;
let mut len = 0; let mut len = 0;
let mut matches = true; let mut matches = true;
for (i, part) in recognized_name.split('.').enumerate() { for (i, part) in recognized_name.split('.').enumerate() {
@ -1845,11 +2044,16 @@ impl HighlightConfiguration {
node_slice node_slice
}; };
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(SHEBANG).unwrap()); static SHEBANG_REGEX: Lazy<rope::Regex> =
Lazy::new(|| rope::Regex::new(SHEBANG).unwrap());
injection_capture = SHEBANG_REGEX injection_capture = SHEBANG_REGEX
.captures(&Cow::from(lines)) .captures_iter(lines.regex_input())
.map(|cap| InjectionLanguageMarker::Shebang(cap[1].to_owned())) .map(|cap| {
let cap = lines.byte_slice(cap.get_group(1).unwrap().range());
InjectionLanguageMarker::Shebang(cap.into())
})
.next()
} else if index == self.injection_content_capture_index { } else if index == self.injection_content_capture_index {
content_node = Some(capture.node); content_node = Some(capture.node);
} }
@ -2262,6 +2466,7 @@ impl<'a> Iterator for HighlightIter<'a> {
// highlighting patterns that are disabled for local variables. // highlighting patterns that are disabled for local variables.
if definition_highlight.is_some() || reference_highlight.is_some() { if definition_highlight.is_some() || reference_highlight.is_some() {
while layer.config.non_local_variable_patterns[match_.pattern_index] { while layer.config.non_local_variable_patterns[match_.pattern_index] {
match_.remove();
if let Some((next_match, next_capture_index)) = captures.peek() { if let Some((next_match, next_capture_index)) = captures.peek() {
let next_capture = next_match.captures[*next_capture_index]; let next_capture = next_match.captures[*next_capture_index];
if next_capture.node == capture.node { if next_capture.node == capture.node {
@ -2472,7 +2677,7 @@ pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result
fn pretty_print_tree_impl<W: fmt::Write>( fn pretty_print_tree_impl<W: fmt::Write>(
fmt: &mut W, fmt: &mut W,
cursor: &mut TreeCursor, cursor: &mut tree_sitter::TreeCursor,
depth: usize, depth: usize,
) -> fmt::Result { ) -> fmt::Result {
let node = cursor.node(); let node = cursor.node();
@ -2487,6 +2692,8 @@ fn pretty_print_tree_impl<W: fmt::Write>(
} }
write!(fmt, "({}", node.kind())?; write!(fmt, "({}", node.kind())?;
} else {
write!(fmt, " \"{}\"", node.kind())?;
} }
// Handle children. // Handle children.
@ -2538,15 +2745,21 @@ mod test {
let loader = Loader::new(Configuration { let loader = Loader::new(Configuration {
language: vec![], language: vec![],
language_server: HashMap::new(), language_server: HashMap::new(),
}); })
.unwrap();
let language = get_language("rust").unwrap(); let language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap(); let query = Query::new(&language, query_str).unwrap();
let textobject = TextObjectQuery { query }; let textobject = TextObjectQuery { query };
let mut cursor = QueryCursor::new(); let mut cursor = QueryCursor::new();
let config = HighlightConfiguration::new(language, "", "", "").unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap(); let syntax = Syntax::new(
source.slice(..),
Arc::new(config),
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let root = syntax.tree().root_node(); let root = syntax.tree().root_node();
let mut test = |capture, range| { let mut test = |capture, range| {
@ -2564,10 +2777,10 @@ mod test {
) )
}; };
test("quantified_nodes", 1..36); test("quantified_nodes", 1..37);
// NOTE: Enable after implementing proper node group capturing // NOTE: Enable after implementing proper node group capturing
// test("quantified_nodes_grouped", 1..36); // test("quantified_nodes_grouped", 1..37);
// test("multiple_nodes_grouped", 1..36); // test("multiple_nodes_grouped", 1..37);
} }
#[test] #[test]
@ -2600,7 +2813,8 @@ mod test {
let loader = Loader::new(Configuration { let loader = Loader::new(Configuration {
language: vec![], language: vec![],
language_server: HashMap::new(), language_server: HashMap::new(),
}); })
.unwrap();
let language = get_language("rust").unwrap(); let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new( let config = HighlightConfiguration::new(
@ -2620,7 +2834,12 @@ mod test {
fn main() {} fn main() {}
", ",
); );
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap(); let syntax = Syntax::new(
source.slice(..),
Arc::new(config),
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let tree = syntax.tree(); let tree = syntax.tree();
let root = tree.root_node(); let root = tree.root_node();
assert_eq!(root.kind(), "source_file"); assert_eq!(root.kind(), "source_file");
@ -2706,11 +2925,17 @@ mod test {
let loader = Loader::new(Configuration { let loader = Loader::new(Configuration {
language: vec![], language: vec![],
language_server: HashMap::new(), language_server: HashMap::new(),
}); })
.unwrap();
let language = get_language(language_name).unwrap(); let language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap(); let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap(); let syntax = Syntax::new(
source.slice(..),
Arc::new(config),
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let root = syntax let root = syntax
.tree() .tree()
@ -2726,8 +2951,8 @@ mod test {
#[test] #[test]
fn test_pretty_print() { fn test_pretty_print() {
let source = r#"/// Hello"#; let source = r#"// Hello"#;
assert_pretty_print("rust", source, "(line_comment)", 0, source.len()); assert_pretty_print("rust", source, "(line_comment \"//\")", 0, source.len());
// A large tree should be indented with fields: // A large tree should be indented with fields:
let source = r#"fn main() { let source = r#"fn main() {
@ -2737,15 +2962,16 @@ mod test {
"rust", "rust",
source, source,
concat!( concat!(
"(function_item\n", "(function_item \"fn\"\n",
" name: (identifier)\n", " name: (identifier)\n",
" parameters: (parameters)\n", " parameters: (parameters \"(\" \")\")\n",
" body: (block\n", " body: (block \"{\"\n",
" (expression_statement\n", " (expression_statement\n",
" (macro_invocation\n", " (macro_invocation\n",
" macro: (identifier)\n", " macro: (identifier) \"!\"\n",
" (token_tree\n", " (token_tree \"(\"\n",
" (string_literal))))))", " (string_literal \"\"\"\n",
" (string_content) \"\"\") \")\")) \";\") \"}\"))",
), ),
0, 0,
source.len(), source.len(),
@ -2757,14 +2983,14 @@ mod test {
// Error nodes are printed as errors: // Error nodes are printed as errors:
let source = r#"}{"#; let source = r#"}{"#;
assert_pretty_print("rust", source, "(ERROR)", 0, source.len()); assert_pretty_print("rust", source, "(ERROR \"}\" \"{\")", 0, source.len());
// Fields broken under unnamed nodes are determined correctly. // Fields broken under unnamed nodes are determined correctly.
// In the following source, `object` belongs to the `singleton_method` // In the following source, `object` belongs to the `singleton_method`
// rule but `name` and `body` belong to an unnamed helper `_method_rest`. // rule but `name` and `body` belong to an unnamed helper `_method_rest`.
// This can cause a bug with a pretty-printing implementation that // This can cause a bug with a pretty-printing implementation that
// uses `Node::field_name_for_child` to determine field names but is // uses `Node::field_name_for_child` to determine field names but is
// fixed when using `TreeCursor::field_name`. // fixed when using `tree_sitter::TreeCursor::field_name`.
let source = "def self.method_name let source = "def self.method_name
true true
end"; end";
@ -2772,11 +2998,11 @@ mod test {
"ruby", "ruby",
source, source,
concat!( concat!(
"(singleton_method\n", "(singleton_method \"def\"\n",
" object: (self)\n", " object: (self) \".\"\n",
" name: (identifier)\n", " name: (identifier)\n",
" body: (body_statement\n", " body: (body_statement\n",
" (true)))" " (true)) \"end\")"
), ),
0, 0,
source.len(), source.len(),

@ -0,0 +1,264 @@
use std::{cmp::Reverse, ops::Range};
use super::{LanguageLayer, LayerId};
use slotmap::HopSlotMap;
use tree_sitter::Node;
/// The byte range of an injection layer.
///
/// Injection ranges may overlap, but all overlapping parts are subsets of their parent ranges.
/// This allows us to sort the ranges ahead of time in order to efficiently find a range that
/// contains a point with maximum depth.
#[derive(Debug)]
struct InjectionRange {
start: usize,
end: usize,
layer_id: LayerId,
depth: u32,
}
pub struct TreeCursor<'a> {
layers: &'a HopSlotMap<LayerId, LanguageLayer>,
root: LayerId,
current: LayerId,
injection_ranges: Vec<InjectionRange>,
// TODO: Ideally this would be a `tree_sitter::TreeCursor<'a>` but
// that returns very surprising results in testing.
cursor: Node<'a>,
}
impl<'a> TreeCursor<'a> {
pub(super) fn new(layers: &'a HopSlotMap<LayerId, LanguageLayer>, root: LayerId) -> Self {
let mut injection_ranges = Vec::new();
for (layer_id, layer) in layers.iter() {
// Skip the root layer
if layer.parent.is_none() {
continue;
}
for byte_range in layer.ranges.iter() {
let range = InjectionRange {
start: byte_range.start_byte,
end: byte_range.end_byte,
layer_id,
depth: layer.depth,
};
injection_ranges.push(range);
}
}
injection_ranges.sort_unstable_by_key(|range| (range.end, Reverse(range.depth)));
let cursor = layers[root].tree().root_node();
Self {
layers,
root,
current: root,
injection_ranges,
cursor,
}
}
pub fn node(&self) -> Node<'a> {
self.cursor
}
pub fn goto_parent(&mut self) -> bool {
if let Some(parent) = self.node().parent() {
self.cursor = parent;
return true;
}
// If we are already on the root layer, we cannot ascend.
if self.current == self.root {
return false;
}
// Ascend to the parent layer.
let range = self.node().byte_range();
let parent_id = self.layers[self.current]
.parent
.expect("non-root layers have a parent");
self.current = parent_id;
let root = self.layers[self.current].tree().root_node();
self.cursor = root
.descendant_for_byte_range(range.start, range.end)
.unwrap_or(root);
true
}
pub fn goto_parent_with<P>(&mut self, predicate: P) -> bool
where
P: Fn(&Node) -> bool,
{
while self.goto_parent() {
if predicate(&self.node()) {
return true;
}
}
false
}
/// Finds the injection layer that has exactly the same range as the given `range`.
fn layer_id_of_byte_range(&self, search_range: Range<usize>) -> Option<LayerId> {
let start_idx = self
.injection_ranges
.partition_point(|range| range.end < search_range.end);
self.injection_ranges[start_idx..]
.iter()
.take_while(|range| range.end == search_range.end)
.find_map(|range| (range.start == search_range.start).then_some(range.layer_id))
}
fn goto_first_child_impl(&mut self, named: bool) -> bool {
// Check if the current node's range is an exact injection layer range.
if let Some(layer_id) = self
.layer_id_of_byte_range(self.node().byte_range())
.filter(|&layer_id| layer_id != self.current)
{
// Switch to the child layer.
self.current = layer_id;
self.cursor = self.layers[self.current].tree().root_node();
return true;
}
let child = if named {
self.cursor.named_child(0)
} else {
self.cursor.child(0)
};
if let Some(child) = child {
// Otherwise descend in the current tree.
self.cursor = child;
true
} else {
false
}
}
pub fn goto_first_child(&mut self) -> bool {
self.goto_first_child_impl(false)
}
pub fn goto_first_named_child(&mut self) -> bool {
self.goto_first_child_impl(true)
}
fn goto_next_sibling_impl(&mut self, named: bool) -> bool {
let sibling = if named {
self.cursor.next_named_sibling()
} else {
self.cursor.next_sibling()
};
if let Some(sibling) = sibling {
self.cursor = sibling;
true
} else {
false
}
}
pub fn goto_next_sibling(&mut self) -> bool {
self.goto_next_sibling_impl(false)
}
pub fn goto_next_named_sibling(&mut self) -> bool {
self.goto_next_sibling_impl(true)
}
fn goto_prev_sibling_impl(&mut self, named: bool) -> bool {
let sibling = if named {
self.cursor.prev_named_sibling()
} else {
self.cursor.prev_sibling()
};
if let Some(sibling) = sibling {
self.cursor = sibling;
true
} else {
false
}
}
pub fn goto_prev_sibling(&mut self) -> bool {
self.goto_prev_sibling_impl(false)
}
pub fn goto_prev_named_sibling(&mut self) -> bool {
self.goto_prev_sibling_impl(true)
}
/// Finds the injection layer that contains the given start-end range.
fn layer_id_containing_byte_range(&self, start: usize, end: usize) -> LayerId {
let start_idx = self
.injection_ranges
.partition_point(|range| range.end < end);
self.injection_ranges[start_idx..]
.iter()
.take_while(|range| range.start < end || range.depth > 1)
.find_map(|range| (range.start <= start).then_some(range.layer_id))
.unwrap_or(self.root)
}
pub fn reset_to_byte_range(&mut self, start: usize, end: usize) {
self.current = self.layer_id_containing_byte_range(start, end);
let root = self.layers[self.current].tree().root_node();
self.cursor = root.descendant_for_byte_range(start, end).unwrap_or(root);
}
/// Returns an iterator over the children of the node the TreeCursor is on
/// at the time this is called.
pub fn children(&'a mut self) -> ChildIter {
let parent = self.node();
ChildIter {
cursor: self,
parent,
named: false,
}
}
/// Returns an iterator over the named children of the node the TreeCursor is on
/// at the time this is called.
pub fn named_children(&'a mut self) -> ChildIter {
let parent = self.node();
ChildIter {
cursor: self,
parent,
named: true,
}
}
}
pub struct ChildIter<'n> {
cursor: &'n mut TreeCursor<'n>,
parent: Node<'n>,
named: bool,
}
impl<'n> Iterator for ChildIter<'n> {
type Item = Node<'n>;
fn next(&mut self) -> Option<Self::Item> {
// first iteration, just visit the first child
if self.cursor.node() == self.parent {
self.cursor
.goto_first_child_impl(self.named)
.then(|| self.cursor.node())
} else {
self.cursor
.goto_next_sibling_impl(self.named)
.then(|| self.cursor.node())
}
}
}

@ -1,9 +1,12 @@
use std::cell::Cell; use std::cell::Cell;
use std::cmp::Ordering;
use std::fmt::Debug;
use std::ops::Range; use std::ops::Range;
use std::rc::Rc; use std::ptr::NonNull;
use crate::doc_formatter::FormattedGrapheme;
use crate::syntax::Highlight; use crate::syntax::Highlight;
use crate::Tendril; use crate::{Position, Tendril};
/// An inline annotation is continuous text shown /// An inline annotation is continuous text shown
/// on the screen before the grapheme that starts at /// on the screen before the grapheme that starts at
@ -76,39 +79,118 @@ impl Overlay {
} }
} }
/// Line annotations allow for virtual text between normal /// Line annotations allow inserting virtual text lines between normal text
/// text lines. They cause `height` empty lines to be inserted /// lines. These lines can be filled with text in the rendering code as their
/// below the document line that contains `anchor_char_idx`. /// contents have no effect beyond visual appearance.
/// ///
/// These lines can be filled with text in the rendering code /// The height of virtual text is usually not known ahead of time as virtual
/// as their contents have no effect beyond visual appearance. /// text often requires softwrapping. Furthermore the height of some virtual
/// text like side-by-side diffs depends on the height of the text (again
/// influenced by softwrap) and other virtual text. Therefore line annotations
/// are computed on the fly instead of ahead of time like other annotations.
/// ///
/// To insert a line after a document line simply set /// The core of this trait `insert_virtual_lines` function. It is called at the
/// `anchor_char_idx` to `doc.line_to_char(line_idx)` /// end of every visual line and allows the `LineAnnotation` to insert empty
#[derive(Debug, Clone)] /// virtual lines. Apart from that the `LineAnnotation` trait has multiple
pub struct LineAnnotation { /// methods that allow it to track anchors in the document.
pub anchor_char_idx: usize, ///
pub height: usize, /// When a new traversal of a document starts `reset_pos` is called. Afterwards
/// the other functions are called with indices that are larger then the
/// one passed to `reset_pos`. This allows performing a binary search (use
/// `partition_point`) in `reset_pos` once and then to only look at the next
/// anchor during each method call.
///
/// The `reset_pos`, `skip_conceal` and `process_anchor` functions all return a
/// `char_idx` anchor. This anchor is stored when transversing the document and
/// when the grapheme at the anchor is traversed the `process_anchor` function
/// is called.
///
/// # Note
///
/// All functions only receive immutable references to `self`.
/// `LineAnnotation`s that want to store an internal position or
/// state of some kind should use `Cell`. Using interior mutability for
/// caches is preferable as otherwise a lot of lifetimes become invariant
/// which complicates APIs a lot.
pub trait LineAnnotation {
/// Resets the internal position to `char_idx`. This function is called
/// when a new traversal of a document starts.
///
/// All `char_idx` passed to `insert_virtual_lines` are strictly monotonically increasing
/// with the first `char_idx` greater or equal to the `char_idx`
/// passed to this function.
///
/// # Returns
///
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
fn reset_pos(&mut self, _char_idx: usize) -> usize {
usize::MAX
}
/// Called when a text is concealed that contains an anchor registered by this `LineAnnotation`.
/// In this case the line decorations **must** ensure that virtual text anchored within that
/// char range is skipped.
///
/// # Returns
///
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
/// **after the end of conceal_end_char_idx**
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
fn skip_concealed_anchors(&mut self, conceal_end_char_idx: usize) -> usize {
self.reset_pos(conceal_end_char_idx)
}
/// Process an anchor (horizontal position is provided) and returns the next anchor.
///
/// # Returns
///
/// The `char_idx` of the next anchor this `LineAnnotation` is interested in,
/// replaces the currently registered anchor. Return `usize::MAX` to ignore
fn process_anchor(&mut self, _grapheme: &FormattedGrapheme) -> usize {
usize::MAX
}
/// This function is called at the end of a visual line to insert virtual text
///
/// # Returns
///
/// The number of additional virtual lines to reserve
///
/// # Note
///
/// The `line_end_visual_pos` parameter indicates the visual vertical distance
/// from the start of block where the traversal starts. This includes the offset
/// from other `LineAnnotations`. This allows inline annotations to consider
/// the height of the text and "align" two different documents (like for side
/// by side diffs). These annotations that want to "align" two documents should
/// therefore be added last so that other virtual text is also considered while aligning
fn insert_virtual_lines(
&mut self,
line_end_char_idx: usize,
line_end_visual_pos: Position,
doc_line: usize,
) -> Position;
} }
#[derive(Debug)] #[derive(Debug)]
struct Layer<A, M> { struct Layer<'a, A, M> {
annotations: Rc<[A]>, annotations: &'a [A],
current_index: Cell<usize>, current_index: Cell<usize>,
metadata: M, metadata: M,
} }
impl<A, M: Clone> Clone for Layer<A, M> { impl<A, M: Clone> Clone for Layer<'_, A, M> {
fn clone(&self) -> Self { fn clone(&self) -> Self {
Layer { Layer {
annotations: self.annotations.clone(), annotations: self.annotations,
current_index: self.current_index.clone(), current_index: self.current_index.clone(),
metadata: self.metadata.clone(), metadata: self.metadata.clone(),
} }
} }
} }
impl<A, M> Layer<A, M> { impl<A, M> Layer<'_, A, M> {
pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) { pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) {
let new_index = self let new_index = self
.annotations .annotations
@ -128,8 +210,8 @@ impl<A, M> Layer<A, M> {
} }
} }
impl<A, M> From<(Rc<[A]>, M)> for Layer<A, M> { impl<'a, A, M> From<(&'a [A], M)> for Layer<'a, A, M> {
fn from((annotations, metadata): (Rc<[A]>, M)) -> Layer<A, M> { fn from((annotations, metadata): (&'a [A], M)) -> Layer<A, M> {
Layer { Layer {
annotations, annotations,
current_index: Cell::new(0), current_index: Cell::new(0),
@ -144,23 +226,78 @@ fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> u
} }
} }
/// Safety: We store LineAnnotation in a NonNull pointer. This is necessary to work
/// around an unfortunate inconsistency in rusts variance system that unnnecesarily
/// makes the lifetime invariant if implemented with safe code. This makes the
/// DocFormatter API very cumbersome/basically impossible to work with.
///
/// Normally object types `dyn Foo + 'a` are covariant so if we used `Box<dyn LineAnnotation + 'a>` below
/// everything would be alright. However we want to use `Cell<Box<dyn LineAnnotation + 'a>>`
/// to be able to call the mutable function on `LineAnnotation`. The problem is that
/// some types like `Cell` make all their arguments invariant. This is important for soundness
/// normally for the same reasons that `&'a mut T` is invariant over `T`
/// (see <https://doc.rust-lang.org/nomicon/subtyping.html>). However for `&'a mut` (`dyn Foo + 'b`)
/// there is a specical rule in the language to make `'b` covariant (otherwise trait objects would be
/// super annoying to use). See <https://users.rust-lang.org/t/solved-variance-of-dyn-trait-a> for
/// why this is sound. Sadly that rule doesn't apply to `Cell<Box<(dyn Foo + 'a)>`
/// (or other invariant types like `UnsafeCell` or `*mut (dyn Foo + 'a)`).
///
/// We sidestep the problem by using `NonNull` which is covariant. In the
/// special case of trait objects this is sound (easily checked by adding a
/// `PhantomData<&'a mut Foo + 'a)>` field). We don't need an explicit `Cell`
/// type here because we never hand out any refereces to the trait objects. That
/// means any reference to the pointer can create a valid multable reference
/// that is covariant over `'a` (or in other words it's a raw pointer, as long as
/// we don't hand out references we are free to do whatever we want).
struct RawBox<T: ?Sized>(NonNull<T>);
impl<T: ?Sized> RawBox<T> {
/// Safety: Only a single mutable reference
/// created by this function may exist at a given time.
#[allow(clippy::mut_from_ref)]
unsafe fn get(&self) -> &mut T {
&mut *self.0.as_ptr()
}
}
impl<T: ?Sized> From<Box<T>> for RawBox<T> {
fn from(box_: Box<T>) -> Self {
// obviously safe because Box::into_raw never returns null
unsafe { Self(NonNull::new_unchecked(Box::into_raw(box_))) }
}
}
impl<T: ?Sized> Drop for RawBox<T> {
fn drop(&mut self) {
unsafe { drop(Box::from_raw(self.0.as_ptr())) }
}
}
/// Annotations that change that is displayed when the document is render. /// Annotations that change that is displayed when the document is render.
/// Also commonly called virtual text. /// Also commonly called virtual text.
#[derive(Default, Debug, Clone)] #[derive(Default)]
pub struct TextAnnotations { pub struct TextAnnotations<'a> {
inline_annotations: Vec<Layer<InlineAnnotation, Option<Highlight>>>, inline_annotations: Vec<Layer<'a, InlineAnnotation, Option<Highlight>>>,
overlays: Vec<Layer<Overlay, Option<Highlight>>>, overlays: Vec<Layer<'a, Overlay, Option<Highlight>>>,
line_annotations: Vec<Layer<LineAnnotation, ()>>, line_annotations: Vec<(Cell<usize>, RawBox<dyn LineAnnotation + 'a>)>,
}
impl Debug for TextAnnotations<'_> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TextAnnotations")
.field("inline_annotations", &self.inline_annotations)
.field("overlays", &self.overlays)
.finish_non_exhaustive()
}
} }
impl TextAnnotations { impl<'a> TextAnnotations<'a> {
/// Prepare the TextAnnotations for iteration starting at char_idx /// Prepare the TextAnnotations for iteration starting at char_idx
pub fn reset_pos(&self, char_idx: usize) { pub fn reset_pos(&self, char_idx: usize) {
reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx); reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
reset_pos(&self.overlays, char_idx, |annot| annot.char_idx); reset_pos(&self.overlays, char_idx, |annot| annot.char_idx);
reset_pos(&self.line_annotations, char_idx, |annot| { for (next_anchor, layer) in &self.line_annotations {
annot.anchor_char_idx next_anchor.set(unsafe { layer.get().reset_pos(char_idx) });
}); }
} }
pub fn collect_overlay_highlights( pub fn collect_overlay_highlights(
@ -194,10 +331,12 @@ impl TextAnnotations {
/// the annotations that belong to the layers added first will be shown first. /// the annotations that belong to the layers added first will be shown first.
pub fn add_inline_annotations( pub fn add_inline_annotations(
&mut self, &mut self,
layer: Rc<[InlineAnnotation]>, layer: &'a [InlineAnnotation],
highlight: Option<Highlight>, highlight: Option<Highlight>,
) -> &mut Self { ) -> &mut Self {
if !layer.is_empty() {
self.inline_annotations.push((layer, highlight).into()); self.inline_annotations.push((layer, highlight).into());
}
self self
} }
@ -211,8 +350,10 @@ impl TextAnnotations {
/// ///
/// If multiple layers contain overlay at the same position /// If multiple layers contain overlay at the same position
/// the overlay from the layer added last will be show. /// the overlay from the layer added last will be show.
pub fn add_overlay(&mut self, layer: Rc<[Overlay]>, highlight: Option<Highlight>) -> &mut Self { pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option<Highlight>) -> &mut Self {
if !layer.is_empty() {
self.overlays.push((layer, highlight).into()); self.overlays.push((layer, highlight).into());
}
self self
} }
@ -220,8 +361,9 @@ impl TextAnnotations {
/// ///
/// The line annotations **must be sorted** by their `char_idx`. /// The line annotations **must be sorted** by their `char_idx`.
/// Multiple line annotations with the same `char_idx` **are not allowed**. /// Multiple line annotations with the same `char_idx` **are not allowed**.
pub fn add_line_annotation(&mut self, layer: Rc<[LineAnnotation]>) -> &mut Self { pub fn add_line_annotation(&mut self, layer: Box<dyn LineAnnotation + 'a>) -> &mut Self {
self.line_annotations.push((layer, ()).into()); self.line_annotations
.push((Cell::new(usize::MAX), layer.into()));
self self
} }
@ -251,21 +393,35 @@ impl TextAnnotations {
overlay overlay
} }
pub(crate) fn annotation_lines_at(&self, char_idx: usize) -> usize { pub(crate) fn process_virtual_text_anchors(&self, grapheme: &FormattedGrapheme) {
self.line_annotations for (next_anchor, layer) in &self.line_annotations {
.iter() loop {
.map(|layer| { match next_anchor.get().cmp(&grapheme.char_idx) {
let mut lines = 0; Ordering::Less => next_anchor
while let Some(annot) = layer.annotations.get(layer.current_index.get()) { .set(unsafe { layer.get().skip_concealed_anchors(grapheme.char_idx) }),
if annot.anchor_char_idx == char_idx { Ordering::Equal => {
layer.current_index.set(layer.current_index.get() + 1); next_anchor.set(unsafe { layer.get().process_anchor(grapheme) })
lines += annot.height
} else {
break;
} }
Ordering::Greater => break,
};
} }
lines }
}) }
.sum()
pub(crate) fn virtual_lines_at(
&self,
char_idx: usize,
line_end_visual_pos: Position,
doc_line: usize,
) -> usize {
let mut virt_off = Position::new(0, 0);
for (_, layer) in &self.line_annotations {
virt_off += unsafe {
layer
.get()
.insert_virtual_lines(char_idx, line_end_visual_pos + virt_off, doc_line)
};
}
virt_off.row
} }
} }

@ -7,9 +7,9 @@ use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary}; use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
use crate::line_ending::rope_is_line_ending; use crate::line_ending::rope_is_line_ending;
use crate::movement::Direction; use crate::movement::Direction;
use crate::surround;
use crate::syntax::LanguageConfiguration; use crate::syntax::LanguageConfiguration;
use crate::Range; use crate::Range;
use crate::{surround, Syntax};
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize { fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
use CharCategory::{Eol, Whitespace}; use CharCategory::{Eol, Whitespace};
@ -199,25 +199,28 @@ pub fn textobject_paragraph(
} }
pub fn textobject_pair_surround( pub fn textobject_pair_surround(
syntax: Option<&Syntax>,
slice: RopeSlice, slice: RopeSlice,
range: Range, range: Range,
textobject: TextObject, textobject: TextObject,
ch: char, ch: char,
count: usize, count: usize,
) -> Range { ) -> Range {
textobject_pair_surround_impl(slice, range, textobject, Some(ch), count) textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count)
} }
pub fn textobject_pair_surround_closest( pub fn textobject_pair_surround_closest(
syntax: Option<&Syntax>,
slice: RopeSlice, slice: RopeSlice,
range: Range, range: Range,
textobject: TextObject, textobject: TextObject,
count: usize, count: usize,
) -> Range { ) -> Range {
textobject_pair_surround_impl(slice, range, textobject, None, count) textobject_pair_surround_impl(syntax, slice, range, textobject, None, count)
} }
fn textobject_pair_surround_impl( fn textobject_pair_surround_impl(
syntax: Option<&Syntax>,
slice: RopeSlice, slice: RopeSlice,
range: Range, range: Range,
textobject: TextObject, textobject: TextObject,
@ -226,8 +229,7 @@ fn textobject_pair_surround_impl(
) -> Range { ) -> Range {
let pair_pos = match ch { let pair_pos = match ch {
Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count), Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count),
// Automatically find the closest surround pairs None => surround::find_nth_closest_pairs_pos(syntax, slice, range, count),
None => surround::find_nth_closest_pairs_pos(slice, range, count),
}; };
pair_pos pair_pos
.map(|(anchor, head)| match textobject { .map(|(anchor, head)| match textobject {
@ -574,7 +576,8 @@ mod test {
let slice = doc.slice(..); let slice = doc.slice(..);
for &case in scenario { for &case in scenario {
let (pos, objtype, expected_range, ch, count) = case; let (pos, objtype, expected_range, ch, count) = case;
let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count); let result =
textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count);
assert_eq!( assert_eq!(
result, result,
expected_range.into(), expected_range.into(),

@ -1,7 +1,7 @@
use ropey::RopeSlice; use ropey::RopeSlice;
use smallvec::SmallVec; use smallvec::SmallVec;
use crate::{Range, Rope, Selection, Tendril}; use crate::{chars::char_is_word, Range, Rope, Selection, Tendril};
use std::{borrow::Cow, iter::once}; use std::{borrow::Cow, iter::once};
/// (from, to, replacement) /// (from, to, replacement)
@ -23,6 +23,40 @@ pub enum Operation {
pub enum Assoc { pub enum Assoc {
Before, Before,
After, After,
/// Acts like `After` if a word character is inserted
/// after the position, otherwise acts like `Before`
AfterWord,
/// Acts like `Before` if a word character is inserted
/// before the position, otherwise acts like `After`
BeforeWord,
/// Acts like `Before` but if the position is within an exact replacement
/// (exact size) the offset to the start of the replacement is kept
BeforeSticky,
/// Acts like `After` but if the position is within an exact replacement
/// (exact size) the offset to the start of the replacement is kept
AfterSticky,
}
impl Assoc {
/// Whether to stick to gaps
fn stay_at_gaps(self) -> bool {
!matches!(self, Self::BeforeWord | Self::AfterWord)
}
fn insert_offset(self, s: &str) -> usize {
let chars = s.chars().count();
match self {
Assoc::After | Assoc::AfterSticky => chars,
Assoc::AfterWord => s.chars().take_while(|&c| char_is_word(c)).count(),
// return position before inserted text
Assoc::Before | Assoc::BeforeSticky => 0,
Assoc::BeforeWord => chars - s.chars().rev().take_while(|&c| char_is_word(c)).count(),
}
}
pub fn sticky(self) -> bool {
matches!(self, Assoc::BeforeSticky | Assoc::AfterSticky)
}
} }
#[derive(Debug, Default, Clone, PartialEq, Eq)] #[derive(Debug, Default, Clone, PartialEq, Eq)]
@ -354,7 +388,9 @@ impl ChangeSet {
macro_rules! map { macro_rules! map {
($map: expr, $i: expr) => { ($map: expr, $i: expr) => {
loop { loop {
let Some((pos, assoc)) = positions.peek_mut() else { return; }; let Some((pos, assoc)) = positions.peek_mut() else {
return;
};
if **pos < old_pos { if **pos < old_pos {
// Positions are not sorted, revert to the last Operation that // Positions are not sorted, revert to the last Operation that
// contains this position and continue iterating from there. // contains this position and continue iterating from there.
@ -381,7 +417,10 @@ impl ChangeSet {
debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works"); debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works");
continue 'outer; continue 'outer;
} }
let Some(new_pos) = $map(**pos, *assoc) else { break; }; #[allow(clippy::redundant_closure_call)]
let Some(new_pos) = $map(**pos, *assoc) else {
break;
};
**pos = new_pos; **pos = new_pos;
positions.next(); positions.next();
} }
@ -415,8 +454,6 @@ impl ChangeSet {
map!(|pos, _| (old_end > pos).then_some(new_pos), i); map!(|pos, _| (old_end > pos).then_some(new_pos), i);
} }
Insert(s) => { Insert(s) => {
let ins = s.chars().count();
// a subsequent delete means a replace, consume it // a subsequent delete means a replace, consume it
if let Some((_, Delete(len))) = iter.peek() { if let Some((_, Delete(len))) = iter.peek() {
iter.next(); iter.next();
@ -424,13 +461,19 @@ impl ChangeSet {
old_end = old_pos + len; old_end = old_pos + len;
// in range of replaced text // in range of replaced text
map!( map!(
|pos, assoc| (old_end > pos).then(|| { |pos, assoc: Assoc| (old_end > pos).then(|| {
// at point or tracking before // at point or tracking before
if pos == old_pos || assoc == Assoc::Before { if pos == old_pos && assoc.stay_at_gaps() {
new_pos new_pos
} else { } else {
// place to end of insert let ins = assoc.insert_offset(s);
new_pos + ins // if the deleted and inserted text have the exact same size
// keep the relative offset into the new text
if *len == ins && assoc.sticky() {
new_pos + (pos - old_pos)
} else {
new_pos + assoc.insert_offset(s)
}
} }
}), }),
i i
@ -438,20 +481,15 @@ impl ChangeSet {
} else { } else {
// at insert point // at insert point
map!( map!(
|pos, assoc| (old_pos == pos).then(|| { |pos, assoc: Assoc| (old_pos == pos).then(|| {
// return position before inserted text // return position before inserted text
if assoc == Assoc::Before { new_pos + assoc.insert_offset(s)
new_pos
} else {
// after text
new_pos + ins
}
}), }),
i i
); );
} }
new_pos += ins; new_pos += s.chars().count();
} }
} }
old_pos = old_end; old_pos = old_end;
@ -884,6 +922,48 @@ mod test {
let mut positions = [4, 2]; let mut positions = [4, 2];
cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After))); cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After)));
assert_eq!(positions, [4, 2]); assert_eq!(positions, [4, 2]);
// stays at word boundary
let cs = ChangeSet {
changes: vec![
Retain(2), // <space><space>
Insert(" ab".into()),
Retain(2), // cd
Insert("de ".into()),
],
len: 4,
len_after: 10,
};
assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 3);
assert_eq!(cs.map_pos(4, Assoc::AfterWord), 9);
let cs = ChangeSet {
changes: vec![
Retain(1), // <space>
Insert(" b".into()),
Delete(1), // c
Retain(1), // d
Insert("e ".into()),
Delete(1), // <space>
],
len: 5,
len_after: 7,
};
assert_eq!(cs.map_pos(1, Assoc::BeforeWord), 2);
assert_eq!(cs.map_pos(3, Assoc::AfterWord), 5);
let cs = ChangeSet {
changes: vec![
Retain(1), // <space>
Insert("a".into()),
Delete(2), // <space>b
Retain(1), // d
Insert("e".into()),
Delete(1), // f
Retain(1), // <space>
],
len: 5,
len_after: 7,
};
assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 1);
assert_eq!(cs.map_pos(4, Assoc::AfterWord), 4);
} }
#[test] #[test]

@ -0,0 +1,125 @@
use std::{
fmt,
path::{Path, PathBuf},
sync::Arc,
};
/// A generic pointer to a file location.
///
/// Currently this type only supports paths to local files.
///
/// Cloning this type is cheap: the internal representation uses an Arc.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
#[non_exhaustive]
pub enum Uri {
File(Arc<Path>),
}
impl Uri {
// This clippy allow mirrors url::Url::from_file_path
#[allow(clippy::result_unit_err)]
pub fn to_url(&self) -> Result<url::Url, ()> {
match self {
Uri::File(path) => url::Url::from_file_path(path),
}
}
pub fn as_path(&self) -> Option<&Path> {
match self {
Self::File(path) => Some(path),
}
}
}
impl From<PathBuf> for Uri {
fn from(path: PathBuf) -> Self {
Self::File(path.into())
}
}
impl fmt::Display for Uri {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::File(path) => write!(f, "{}", path.display()),
}
}
}
#[derive(Debug)]
pub struct UrlConversionError {
source: url::Url,
kind: UrlConversionErrorKind,
}
#[derive(Debug)]
pub enum UrlConversionErrorKind {
UnsupportedScheme,
UnableToConvert,
}
impl fmt::Display for UrlConversionError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self.kind {
UrlConversionErrorKind::UnsupportedScheme => {
write!(
f,
"unsupported scheme '{}' in URL {}",
self.source.scheme(),
self.source
)
}
UrlConversionErrorKind::UnableToConvert => {
write!(f, "unable to convert URL to file path: {}", self.source)
}
}
}
}
impl std::error::Error for UrlConversionError {}
fn convert_url_to_uri(url: &url::Url) -> Result<Uri, UrlConversionErrorKind> {
if url.scheme() == "file" {
url.to_file_path()
.map(|path| Uri::File(helix_stdx::path::normalize(path).into()))
.map_err(|_| UrlConversionErrorKind::UnableToConvert)
} else {
Err(UrlConversionErrorKind::UnsupportedScheme)
}
}
impl TryFrom<url::Url> for Uri {
type Error = UrlConversionError;
fn try_from(url: url::Url) -> Result<Self, Self::Error> {
convert_url_to_uri(&url).map_err(|kind| Self::Error { source: url, kind })
}
}
impl TryFrom<&url::Url> for Uri {
type Error = UrlConversionError;
fn try_from(url: &url::Url) -> Result<Self, Self::Error> {
convert_url_to_uri(url).map_err(|kind| Self::Error {
source: url.clone(),
kind,
})
}
}
#[cfg(test)]
mod test {
use super::*;
use url::Url;
#[test]
fn unknown_scheme() {
let url = Url::parse("csharp:/metadata/foo/bar/Baz.cs").unwrap();
assert!(matches!(
Uri::try_from(url),
Err(UrlConversionError {
kind: UrlConversionErrorKind::UnsupportedScheme,
..
})
));
}
}

@ -1,10 +1,12 @@
use arc_swap::ArcSwap;
use helix_core::{ use helix_core::{
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle}, indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
syntax::{Configuration, Loader}, syntax::{Configuration, Loader},
Syntax, Syntax,
}; };
use helix_stdx::rope::RopeSliceExt;
use ropey::Rope; use ropey::Rope;
use std::{ops::Range, path::PathBuf, process::Command}; use std::{ops::Range, path::PathBuf, process::Command, sync::Arc};
#[test] #[test]
fn test_treesitter_indent_rust() { fn test_treesitter_indent_rust() {
@ -34,7 +36,7 @@ fn test_treesitter_indent_rust_helix() {
.unwrap(); .unwrap();
let files = String::from_utf8(files.stdout).unwrap(); let files = String::from_utf8(files.stdout).unwrap();
let ignored_files = vec![ let ignored_files = [
// Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting // Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting
"helix-term/src/health.rs", "helix-term/src/health.rs",
]; ];
@ -43,6 +45,7 @@ fn test_treesitter_indent_rust_helix() {
if ignored_files.contains(&file) { if ignored_files.contains(&file) {
continue; continue;
} }
#[allow(clippy::single_range_in_vec_init)]
let ignored_lines: Vec<Range<usize>> = match file { let ignored_lines: Vec<Range<usize>> = match file {
"helix-term/src/application.rs" => vec![ "helix-term/src/application.rs" => vec![
// We can't handle complicated indent rules inside macros (`json!` in this case) since // We can't handle complicated indent rules inside macros (`json!` in this case) since
@ -186,7 +189,7 @@ fn test_treesitter_indent(
lang_scope: &str, lang_scope: &str,
ignored_lines: Vec<std::ops::Range<usize>>, ignored_lines: Vec<std::ops::Range<usize>>,
) { ) {
let loader = Loader::new(indent_tests_config()); let loader = Loader::new(indent_tests_config()).unwrap();
// set runtime path so we can find the queries // set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@ -197,7 +200,12 @@ fn test_treesitter_indent(
let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit); let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit);
let highlight_config = language_config.highlight_config(&[]).unwrap(); let highlight_config = language_config.highlight_config(&[]).unwrap();
let text = doc.slice(..); let text = doc.slice(..);
let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap(); let syntax = Syntax::new(
text,
highlight_config,
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let indent_query = language_config.indent_query().unwrap(); let indent_query = language_config.indent_query().unwrap();
for i in 0..doc.len_lines() { for i in 0..doc.len_lines() {
@ -205,7 +213,7 @@ fn test_treesitter_indent(
if ignored_lines.iter().any(|range| range.contains(&(i + 1))) { if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
continue; continue;
} }
if let Some(pos) = helix_core::find_first_non_whitespace_char(line) { if let Some(pos) = line.first_non_whitespace_char() {
let tab_width: usize = 4; let tab_width: usize = 4;
let suggested_indent = treesitter_indent_for_pos( let suggested_indent = treesitter_indent_for_pos(
indent_query, indent_query,

@ -13,15 +13,15 @@ homepage.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
helix-stdx = { path = "../helix-stdx" }
helix-core = { path = "../helix-core" } helix-core = { path = "../helix-core" }
anyhow = "1.0" anyhow = "1.0"
log = "0.4" log = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
which = "5.0.0" thiserror.workspace = true
[dev-dependencies] [dev-dependencies]
fern = "0.6" fern = "0.7"

@ -2,14 +2,13 @@ use crate::{
requests::DisconnectArguments, requests::DisconnectArguments,
transport::{Payload, Request, Response, Transport}, transport::{Payload, Request, Response, Transport},
types::*, types::*,
Error, Result, ThreadId, Error, Result,
}; };
use helix_core::syntax::DebuggerQuirks; use helix_core::syntax::DebuggerQuirks;
use serde_json::Value; use serde_json::Value;
use anyhow::anyhow; use anyhow::anyhow;
pub use log::{error, info};
use std::{ use std::{
collections::HashMap, collections::HashMap,
future::Future, future::Future,
@ -114,7 +113,7 @@ impl Client {
id: usize, id: usize,
) -> Result<(Self, UnboundedReceiver<Payload>)> { ) -> Result<(Self, UnboundedReceiver<Payload>)> {
// Resolve path to the binary // Resolve path to the binary
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?; let cmd = helix_stdx::env::which(cmd)?;
let process = Command::new(cmd) let process = Command::new(cmd)
.args(args) .args(args)
@ -158,8 +157,8 @@ impl Client {
) )
} }
pub fn starting_request_args(&self) -> &Option<Value> { pub fn starting_request_args(&self) -> Option<&Value> {
&self.starting_request_args self.starting_request_args.as_ref()
} }
pub async fn tcp_process( pub async fn tcp_process(

@ -19,6 +19,8 @@ pub enum Error {
#[error("server closed the stream")] #[error("server closed the stream")]
StreamClosed, StreamClosed,
#[error(transparent)] #[error(transparent)]
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
#[error(transparent)]
Other(#[from] anyhow::Error), Other(#[from] anyhow::Error),
} }
pub type Result<T> = core::result::Result<T, Error>; pub type Result<T> = core::result::Result<T, Error>;

@ -1,4 +1,4 @@
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Deserializer, Serialize};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap; use std::collections::HashMap;
use std::path::PathBuf; use std::path::PathBuf;
@ -311,7 +311,8 @@ pub struct Variable {
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")] #[serde(rename_all = "camelCase")]
pub struct Module { pub struct Module {
pub id: String, // TODO: || number #[serde(deserialize_with = "from_number")]
pub id: String,
pub name: String, pub name: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub path: Option<PathBuf>, pub path: Option<PathBuf>,
@ -331,6 +332,23 @@ pub struct Module {
pub address_range: Option<String>, pub address_range: Option<String>,
} }
fn from_number<'de, D>(deserializer: D) -> Result<String, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum NumberOrString {
Number(i64),
String(String),
}
match NumberOrString::deserialize(deserializer)? {
NumberOrString::Number(n) => Ok(n.to_string()),
NumberOrString::String(s) => Ok(s),
}
}
pub mod requests { pub mod requests {
use super::*; use super::*;
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
@ -887,4 +905,18 @@ pub mod events {
pub offset: usize, pub offset: usize,
pub count: usize, pub count: usize,
} }
#[test]
fn test_deserialize_module_id_from_number() {
let raw = r#"{"id": 0, "name": "Name"}"#;
let module: super::Module = serde_json::from_str(raw).expect("Error!");
assert_eq!(module.id, "0");
}
#[test]
fn test_deserialize_module_id_from_string() {
let raw = r#"{"id": "0", "name": "Name"}"#;
let module: super::Module = serde_json::from_str(raw).expect("Error!");
assert_eq!(module.id, "0");
}
} }

@ -12,5 +12,18 @@ homepage.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] } ahash = "0.8.11"
parking_lot = { version = "0.12", features = ["send_guard"] } hashbrown = "0.14.5"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
# the event registry is essentially read only but must be an rwlock so we can
# setup new events on initialization, hardware-lock-elision hugely benefits this case
# as it essentially makes the lock entirely free as long as there is no writes
parking_lot = { version = "0.12", features = ["hardware-lock-elision"] }
once_cell = "1.20"
anyhow = "1"
log = "0.4"
futures-executor = "0.3.31"
[features]
integration_test = []

@ -0,0 +1,19 @@
use std::future::Future;
pub use oneshot::channel as cancelation;
use tokio::sync::oneshot;
pub type CancelTx = oneshot::Sender<()>;
pub type CancelRx = oneshot::Receiver<()>;
pub async fn cancelable_future<T>(future: impl Future<Output = T>, cancel: CancelRx) -> Option<T> {
tokio::select! {
biased;
_ = cancel => {
None
}
res = future => {
Some(res)
}
}
}

@ -0,0 +1,70 @@
//! Utilities for declaring an async (usually debounced) hook
use std::time::Duration;
use futures_executor::block_on;
use tokio::sync::mpsc::{self, error::TrySendError, Sender};
use tokio::time::Instant;
/// Async hooks provide a convenient framework for implementing (debounced)
/// async event handlers. Most synchronous event hooks will likely need to
/// debounce their events, coordinate multiple different hooks and potentially
/// track some state. `AsyncHooks` facilitate these use cases by running as
/// a background tokio task that waits for events (usually an enum) to be
/// sent through a channel.
pub trait AsyncHook: Sync + Send + 'static + Sized {
type Event: Sync + Send + 'static;
/// Called immediately whenever an event is received, this function can
/// consume the event immediately or debounce it. In case of debouncing,
/// it can either define a new debounce timeout or continue the current one
fn handle_event(&mut self, event: Self::Event, timeout: Option<Instant>) -> Option<Instant>;
/// Called whenever the debounce timeline is reached
fn finish_debounce(&mut self);
fn spawn(self) -> mpsc::Sender<Self::Event> {
// the capacity doesn't matter too much here, unless the cpu is totally overwhelmed
// the cap will never be reached since we always immediately drain the channel
// so it should only be reached in case of total CPU overload.
// However, a bounded channel is much more efficient so it's nice to use here
let (tx, rx) = mpsc::channel(128);
// only spawn worker if we are inside runtime to avoid having to spawn a runtime for unrelated unit tests
if tokio::runtime::Handle::try_current().is_ok() {
tokio::spawn(run(self, rx));
}
tx
}
}
async fn run<Hook: AsyncHook>(mut hook: Hook, mut rx: mpsc::Receiver<Hook::Event>) {
let mut deadline = None;
loop {
let event = match deadline {
Some(deadline_) => {
let res = tokio::time::timeout_at(deadline_, rx.recv()).await;
match res {
Ok(event) => event,
Err(_) => {
hook.finish_debounce();
deadline = None;
continue;
}
}
}
None => rx.recv().await,
};
let Some(event) = event else {
break;
};
deadline = hook.handle_event(event, deadline);
}
}
pub fn send_blocking<T>(tx: &Sender<T>, data: T) {
// block_on has some overhead and in practice the channel should basically
// never be full anyway so first try sending without blocking
if let Err(TrySendError::Full(data)) = tx.try_send(data) {
// set a timeout so that we just drop a message instead of freezing the editor in the worst case
let _ = block_on(tx.send_timeout(data, Duration::from_millis(10)));
}
}

@ -0,0 +1,91 @@
//! rust dynamic dispatch is extremely limited so we have to build our
//! own vtable implementation. Otherwise implementing the event system would not be possible.
//! A nice bonus of this approach is that we can optimize the vtable a bit more. Normally
//! a dyn Trait fat pointer contains two pointers: A pointer to the data itself and a
//! pointer to a global (static) vtable entry which itself contains multiple other pointers
//! (the various functions of the trait, drop, size and align). That makes dynamic
//! dispatch pretty slow (double pointer indirections). However, we only have a single function
//! in the hook trait and don't need a drop implementation (event system is global anyway
//! and never dropped) so we can just store the entire vtable inline.
use anyhow::Result;
use std::ptr::{self, NonNull};
use crate::Event;
/// Opaque handle type that represents an erased type parameter.
///
/// If extern types were stable, this could be implemented as `extern { pub type Opaque; }` but
/// until then we can use this.
///
/// Care should be taken that we don't use a concrete instance of this. It should only be used
/// through a reference, so we can maintain something else's lifetime.
struct Opaque(());
pub(crate) struct ErasedHook {
data: NonNull<Opaque>,
call: unsafe fn(NonNull<Opaque>, NonNull<Opaque>, NonNull<Opaque>),
}
impl ErasedHook {
pub(crate) fn new_dynamic<H: Fn() -> Result<()> + 'static + Send + Sync>(
hook: H,
) -> ErasedHook {
unsafe fn call<F: Fn() -> Result<()> + 'static + Send + Sync>(
hook: NonNull<Opaque>,
_event: NonNull<Opaque>,
result: NonNull<Opaque>,
) {
let hook: NonNull<F> = hook.cast();
let result: NonNull<Result<()>> = result.cast();
let hook: &F = hook.as_ref();
let res = hook();
ptr::write(result.as_ptr(), res)
}
unsafe {
ErasedHook {
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
call: call::<H>,
}
}
}
pub(crate) fn new<E: Event, F: Fn(&mut E) -> Result<()>>(hook: F) -> ErasedHook {
unsafe fn call<E: Event, F: Fn(&mut E) -> Result<()>>(
hook: NonNull<Opaque>,
event: NonNull<Opaque>,
result: NonNull<Opaque>,
) {
let hook: NonNull<F> = hook.cast();
let mut event: NonNull<E> = event.cast();
let result: NonNull<Result<()>> = result.cast();
let hook: &F = hook.as_ref();
let res = hook(event.as_mut());
ptr::write(result.as_ptr(), res)
}
unsafe {
ErasedHook {
data: NonNull::new_unchecked(Box::into_raw(Box::new(hook)) as *mut Opaque),
call: call::<E, F>,
}
}
}
pub(crate) unsafe fn call<E: Event>(&self, event: &mut E) -> Result<()> {
let mut res = Ok(());
unsafe {
(self.call)(
self.data,
NonNull::from(event).cast(),
NonNull::from(&mut res).cast(),
);
}
res
}
}
unsafe impl Sync for ErasedHook {}
unsafe impl Send for ErasedHook {}

@ -1,8 +1,205 @@
//! `helix-event` contains systems that allow (often async) communication between //! `helix-event` contains systems that allow (often async) communication between
//! different editor components without strongly coupling them. Currently this //! different editor components without strongly coupling them. Specifically
//! crate only contains some smaller facilities but the intend is to add more //! it allows defining synchronous hooks that run when certain editor events
//! functionality in the future ( like a generic hook system) //! occur.
//!
//! The core of the event system are hook callbacks and the [`Event`] trait. A
//! hook is essentially just a closure `Fn(event: &mut impl Event) -> Result<()>`
//! that gets called every time an appropriate event is dispatched. The implementation
//! details of the [`Event`] trait are considered private. The [`events`] macro is
//! provided which automatically declares event types. Similarly the `register_hook`
//! macro should be used to (safely) declare event hooks.
//!
//! Hooks run synchronously which can be advantageous since they can modify the
//! current editor state right away (for example to immediately hide the completion
//! popup). However, they can not contain their own state without locking since
//! they only receive immutable references. For handler that want to track state, do
//! expensive background computations or debouncing an [`AsyncHook`] is preferable.
//! Async hooks are based around a channels that receive events specific to
//! that `AsyncHook` (usually an enum). These events can be sent by synchronous
//! hooks. Due to some limitations around tokio channels the [`send_blocking`]
//! function exported in this crate should be used instead of the builtin
//! `blocking_send`.
//!
//! In addition to the core event system, this crate contains some message queues
//! that allow transfer of data back to the main event loop from async hooks and
//! hooks that may not have access to all application data (for example in helix-view).
//! This include the ability to control rendering ([`lock_frame`], [`request_redraw`]) and
//! display status messages ([`status`]).
//!
//! Hooks declared in helix-term can furthermore dispatch synchronous jobs to be run on the
//! main loop (including access to the compositor). Ideally that queue will be moved
//! to helix-view in the future if we manage to detach the compositor from its rendering backend.
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard}; use anyhow::Result;
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
pub use debounce::{send_blocking, AsyncHook};
pub use redraw::{
lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard, RequestRedrawOnDrop,
};
pub use registry::Event;
mod cancel;
mod debounce;
mod hook;
mod redraw; mod redraw;
mod registry;
#[doc(hidden)]
pub mod runtime;
pub mod status;
#[cfg(test)]
mod test;
pub fn register_event<E: Event + 'static>() {
registry::with_mut(|registry| registry.register_event::<E>())
}
/// Registers a hook that will be called when an event of type `E` is dispatched.
/// This function should usually not be used directly, use the [`register_hook`]
/// macro instead.
///
///
/// # Safety
///
/// `hook` must be totally generic over all lifetime parameters of `E`. For
/// example if `E` was a known type `Foo<'a, 'b>`, then the correct trait bound
/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)`, but there is no way to
/// express that kind of constraint for a generic type with the Rust type system
/// as of this writing.
pub unsafe fn register_hook_raw<E: Event>(
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
) {
registry::with_mut(|registry| registry.register_hook(hook))
}
/// Register a hook solely by event name
pub fn register_dynamic_hook(
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
id: &str,
) -> Result<()> {
registry::with_mut(|reg| reg.register_dynamic_hook(hook, id))
}
pub fn dispatch(e: impl Event) {
registry::with(|registry| registry.dispatch(e));
}
/// Macro to declare events
///
/// # Examples
///
/// ``` no-compile
/// events! {
/// FileWrite(&Path)
/// ViewScrolled{ view: View, new_pos: ViewOffset }
/// DocumentChanged<'a> { old_doc: &'a Rope, doc: &'a mut Document, changes: &'a ChangeSet }
/// }
///
/// fn init() {
/// register_event::<FileWrite>();
/// register_event::<ViewScrolled>();
/// register_event::<DocumentChanged>();
/// }
///
/// fn save(path: &Path, content: &str){
/// std::fs::write(path, content);
/// dispatch(FileWrite(path));
/// }
/// ```
#[macro_export]
macro_rules! events {
($name: ident<$($lt: lifetime),*> { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
pub struct $name<$($lt),*> { $(pub $data: $data_ty),* }
unsafe impl<$($lt),*> $crate::Event for $name<$($lt),*> {
const ID: &'static str = stringify!($name);
const LIFETIMES: usize = $crate::events!(@sum $(1, $lt),*);
type Static = $crate::events!(@replace_lt $name, $('static, $lt),*);
}
$crate::events!{ $($rem)* }
};
($name: ident { $($data:ident : $data_ty:ty),* } $($rem:tt)*) => {
pub struct $name { $(pub $data: $data_ty),* }
unsafe impl $crate::Event for $name {
const ID: &'static str = stringify!($name);
const LIFETIMES: usize = 0;
type Static = Self;
}
$crate::events!{ $($rem)* }
};
() => {};
(@replace_lt $name: ident, $($lt1: lifetime, $lt2: lifetime),* ) => {$name<$($lt1),*>};
(@sum $($val: expr, $lt1: lifetime),* ) => {0 $(+ $val)*};
}
/// Safely register statically typed event hooks
#[macro_export]
macro_rules! register_hook {
// Safety: this is safe because we fully control the type of the event here and
// ensure all lifetime arguments are fully generic and the correct number of lifetime arguments
// is present
(move |$event:ident: &mut $event_ty: ident<$($lt: lifetime),*>| $body: expr) => {
let val = move |$event: &mut $event_ty<$($lt),*>| $body;
unsafe {
// Lifetimes are a bit of a pain. We want to allow events being
// non-static. Lifetimes don't actually exist at runtime so its
// fine to essentially transmute the lifetimes as long as we can
// prove soundness. The hook must therefore accept any combination
// of lifetimes. In other words fn(&'_ mut Event<'_, '_>) is ok
// but examples like fn(&'_ mut Event<'_, 'static>) or fn<'a>(&'a
// mut Event<'a, 'a>) are not. To make this safe we use a macro to
// forbid the user from specifying lifetimes manually (all lifetimes
// specified are always function generics and passed to the event so
// lifetimes can't be used multiple times and using 'static causes a
// syntax error).
//
// There is one soundness hole tough: Type Aliases allow
// "accidentally" creating these problems. For example:
//
// type Event2 = Event<'static>.
// type Event2<'a> = Event<'a, a>.
//
// These cases can be caught by counting the number of lifetimes
// parameters at the parameter declaration site and then at the hook
// declaration site. By asserting the number of lifetime parameters
// are equal we can catch all bad type aliases under one assumption:
// There are no unused lifetime parameters. Introducing a static
// would reduce the number of arguments of the alias by one in the
// above example Event2 has zero lifetime arguments while the original
// event has one lifetime argument. Similar logic applies to using
// a lifetime argument multiple times. The ASSERT below performs a
// a compile time assertion to ensure exactly this property.
//
// With unused lifetime arguments it is still one way to cause unsound code:
//
// type Event2<'a, 'b> = Event<'a, 'a>;
//
// However, this case will always emit a compiler warning/cause CI
// failures so a user would have to introduce #[allow(unused)] which
// is easily caught in review (and a very theoretical case anyway).
// If we want to be pedantic we can simply compile helix with
// forbid(unused). All of this is just a safety net to prevent
// very theoretical misuse. This won't come up in real code (and is
// easily caught in review).
#[allow(unused)]
const ASSERT: () = {
if <$event_ty as $crate::Event>::LIFETIMES != 0 + $crate::events!(@sum $(1, $lt),*){
panic!("invalid type alias");
}
};
$crate::register_hook_raw::<$crate::events!(@replace_lt $event_ty, $('static, $lt),*)>(val);
}
};
(move |$event:ident: &mut $event_ty: ident| $body: expr) => {
let val = move |$event: &mut $event_ty| $body;
unsafe {
#[allow(unused)]
const ASSERT: () = {
if <$event_ty as $crate::Event>::LIFETIMES != 0{
panic!("invalid type alias");
}
};
$crate::register_hook_raw::<$event_ty>(val);
}
};
}

@ -5,16 +5,20 @@ use std::future::Future;
use parking_lot::{RwLock, RwLockReadGuard}; use parking_lot::{RwLock, RwLockReadGuard};
use tokio::sync::Notify; use tokio::sync::Notify;
use crate::runtime_local;
runtime_local! {
/// A `Notify` instance that can be used to (asynchronously) request /// A `Notify` instance that can be used to (asynchronously) request
/// the editor the render a new frame. /// the editor to render a new frame.
static REDRAW_NOTIFY: Notify = Notify::const_new(); static REDRAW_NOTIFY: Notify = Notify::const_new();
/// A `RwLock` that prevents the next frame from being /// A `RwLock` that prevents the next frame from being
/// drawn until an exclusive (write) lock can be acquired. /// drawn until an exclusive (write) lock can be acquired.
/// This allows asynchsonous tasks to acquire `non-exclusive` /// This allows asynchronous tasks to acquire `non-exclusive`
/// locks (read) to prevent the next frame from being drawn /// locks (read) to prevent the next frame from being drawn
/// until a certain computation has finished. /// until a certain computation has finished.
static RENDER_LOCK: RwLock<()> = RwLock::new(()); static RENDER_LOCK: RwLock<()> = RwLock::new(());
}
pub type RenderLockGuard = RwLockReadGuard<'static, ()>; pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
@ -47,3 +51,12 @@ pub fn start_frame() {
pub fn lock_frame() -> RenderLockGuard { pub fn lock_frame() -> RenderLockGuard {
RENDER_LOCK.read() RENDER_LOCK.read()
} }
/// A zero sized type that requests a redraw via [request_redraw] when the type [Drop]s.
pub struct RequestRedrawOnDrop;
impl Drop for RequestRedrawOnDrop {
fn drop(&mut self) {
request_redraw();
}
}

@ -0,0 +1,131 @@
//! A global registry where events are registered and can be
//! subscribed to by registering hooks. The registry identifies event
//! types using their type name so multiple event with the same type name
//! may not be registered (will cause a panic to ensure soundness)
use std::any::TypeId;
use anyhow::{bail, Result};
use hashbrown::hash_map::Entry;
use hashbrown::HashMap;
use parking_lot::RwLock;
use crate::hook::ErasedHook;
use crate::runtime_local;
pub struct Registry {
events: HashMap<&'static str, TypeId, ahash::RandomState>,
handlers: HashMap<&'static str, Vec<ErasedHook>, ahash::RandomState>,
}
impl Registry {
pub fn register_event<E: Event + 'static>(&mut self) {
let ty = TypeId::of::<E>();
assert_eq!(ty, TypeId::of::<E::Static>());
match self.events.entry(E::ID) {
Entry::Occupied(entry) => {
if entry.get() == &ty {
// don't warn during tests to avoid log spam
#[cfg(not(feature = "integration_test"))]
panic!("Event {} was registered multiple times", E::ID);
} else {
panic!("Multiple events with ID {} were registered", E::ID);
}
}
Entry::Vacant(ent) => {
ent.insert(ty);
self.handlers.insert(E::ID, Vec::new());
}
}
}
/// # Safety
///
/// `hook` must be totally generic over all lifetime parameters of `E`. For
/// example if `E` was a known type `Foo<'a, 'b> then the correct trait bound
/// would be `F: for<'a, 'b, 'c> Fn(&'a mut Foo<'b, 'c>)` but there is no way to
/// express that kind of constraint for a generic type with the rust type system
/// right now.
pub unsafe fn register_hook<E: Event>(
&mut self,
hook: impl Fn(&mut E) -> Result<()> + 'static + Send + Sync,
) {
// ensure event type ids match so we can rely on them always matching
let id = E::ID;
let Some(&event_id) = self.events.get(id) else {
panic!("Tried to register handler for unknown event {id}");
};
assert!(
TypeId::of::<E::Static>() == event_id,
"Tried to register invalid hook for event {id}"
);
let hook = ErasedHook::new(hook);
self.handlers.get_mut(id).unwrap().push(hook);
}
pub fn register_dynamic_hook(
&mut self,
hook: impl Fn() -> Result<()> + 'static + Send + Sync,
id: &str,
) -> Result<()> {
// ensure event type ids match so we can rely on them always matching
if self.events.get(id).is_none() {
bail!("Tried to register handler for unknown event {id}");
};
let hook = ErasedHook::new_dynamic(hook);
self.handlers.get_mut(id).unwrap().push(hook);
Ok(())
}
pub fn dispatch<E: Event>(&self, mut event: E) {
let Some(hooks) = self.handlers.get(E::ID) else {
log::error!("Dispatched unknown event {}", E::ID);
return;
};
let event_id = self.events[E::ID];
assert_eq!(
TypeId::of::<E::Static>(),
event_id,
"Tried to dispatch invalid event {}",
E::ID
);
for hook in hooks {
// safety: event type is the same
if let Err(err) = unsafe { hook.call(&mut event) } {
log::error!("{} hook failed: {err:#?}", E::ID);
crate::status::report_blocking(err);
}
}
}
}
runtime_local! {
static REGISTRY: RwLock<Registry> = RwLock::new(Registry {
// hardcoded random number is good enough here we don't care about DOS resistance
// and avoids the additional complexity of `Option<Registry>`
events: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 9978, 38322, 3280080)),
handlers: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 99078, 382322, 3282938)),
});
}
pub(crate) fn with<T>(f: impl FnOnce(&Registry) -> T) -> T {
f(&REGISTRY.read())
}
pub(crate) fn with_mut<T>(f: impl FnOnce(&mut Registry) -> T) -> T {
f(&mut REGISTRY.write())
}
/// # Safety
/// The number of specified lifetimes and the static type *must* be correct.
/// This is ensured automatically by the [`events`](crate::events)
/// macro.
pub unsafe trait Event: Sized {
/// Globally unique (case sensitive) string that identifies this type.
/// A good candidate is the events type name
const ID: &'static str;
const LIFETIMES: usize;
type Static: Event + 'static;
}

@ -0,0 +1,88 @@
//! The event system makes use of global to decouple different systems.
//! However, this can cause problems for the integration test system because
//! it runs multiple helix applications in parallel. Making the globals
//! thread-local does not work because a applications can/does have multiple
//! runtime threads. Instead this crate implements a similar notion to a thread
//! local but instead of being local to a single thread, the statics are local to
//! a single tokio-runtime. The implementation requires locking so it's not exactly efficient.
//!
//! Therefore this function is only enabled during integration tests and behaves like
//! a normal static otherwise. I would prefer this module to be fully private and to only
//! export the macro but the macro still need to construct these internals so it's marked
//! `doc(hidden)` instead
use std::ops::Deref;
#[cfg(not(feature = "integration_test"))]
pub struct RuntimeLocal<T: 'static> {
/// inner API used in the macro, not part of public API
#[doc(hidden)]
pub __data: T,
}
#[cfg(not(feature = "integration_test"))]
impl<T> Deref for RuntimeLocal<T> {
type Target = T;
fn deref(&self) -> &Self::Target {
&self.__data
}
}
#[cfg(not(feature = "integration_test"))]
#[macro_export]
macro_rules! runtime_local {
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal {
__data: $init
};)*
};
}
#[cfg(feature = "integration_test")]
pub struct RuntimeLocal<T: 'static> {
data:
parking_lot::RwLock<hashbrown::HashMap<tokio::runtime::Id, &'static T, ahash::RandomState>>,
init: fn() -> T,
}
#[cfg(feature = "integration_test")]
impl<T> RuntimeLocal<T> {
/// inner API used in the macro, not part of public API
#[doc(hidden)]
pub const fn __new(init: fn() -> T) -> Self {
Self {
data: parking_lot::RwLock::new(hashbrown::HashMap::with_hasher(
ahash::RandomState::with_seeds(423, 9978, 38322, 3280080),
)),
init,
}
}
}
#[cfg(feature = "integration_test")]
impl<T> Deref for RuntimeLocal<T> {
type Target = T;
fn deref(&self) -> &T {
let id = tokio::runtime::Handle::current().id();
let guard = self.data.read();
match guard.get(&id) {
Some(res) => res,
None => {
drop(guard);
let data = Box::leak(Box::new((self.init)()));
let mut guard = self.data.write();
guard.insert(id, data);
data
}
}
}
}
#[cfg(feature = "integration_test")]
#[macro_export]
macro_rules! runtime_local {
($($(#[$attr:meta])* $vis: vis static $name:ident: $ty: ty = $init: expr;)*) => {
$($(#[$attr])* $vis static $name: $crate::runtime::RuntimeLocal<$ty> = $crate::runtime::RuntimeLocal::__new(|| $init);)*
};
}

@ -0,0 +1,68 @@
//! A queue of async messages/errors that will be shown in the editor
use std::borrow::Cow;
use std::time::Duration;
use crate::{runtime_local, send_blocking};
use once_cell::sync::OnceCell;
use tokio::sync::mpsc::{Receiver, Sender};
/// Describes the severity level of a [`StatusMessage`].
#[derive(Debug, Clone, Copy, Eq, PartialEq, PartialOrd, Ord)]
pub enum Severity {
Hint,
Info,
Warning,
Error,
}
pub struct StatusMessage {
pub severity: Severity,
pub message: Cow<'static, str>,
}
impl From<anyhow::Error> for StatusMessage {
fn from(err: anyhow::Error) -> Self {
StatusMessage {
severity: Severity::Error,
message: err.to_string().into(),
}
}
}
impl From<&'static str> for StatusMessage {
fn from(msg: &'static str) -> Self {
StatusMessage {
severity: Severity::Info,
message: msg.into(),
}
}
}
runtime_local! {
static MESSAGES: OnceCell<Sender<StatusMessage>> = OnceCell::new();
}
pub async fn report(msg: impl Into<StatusMessage>) {
// if the error channel overflows just ignore it
let _ = MESSAGES
.wait()
.send_timeout(msg.into(), Duration::from_millis(10))
.await;
}
pub fn report_blocking(msg: impl Into<StatusMessage>) {
let messages = MESSAGES.wait();
send_blocking(messages, msg.into())
}
/// Must be called once during editor startup exactly once
/// before any of the messages in this module can be used
///
/// # Panics
/// If called multiple times
pub fn setup() -> Receiver<StatusMessage> {
let (tx, rx) = tokio::sync::mpsc::channel(128);
let _ = MESSAGES.set(tx);
rx
}

@ -0,0 +1,90 @@
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
use std::time::Duration;
use parking_lot::Mutex;
use crate::{dispatch, events, register_dynamic_hook, register_event, register_hook};
#[test]
fn smoke_test() {
events! {
Event1 { content: String }
Event2 { content: usize }
}
register_event::<Event1>();
register_event::<Event2>();
// setup hooks
let res1: Arc<Mutex<String>> = Arc::default();
let acc = Arc::clone(&res1);
register_hook!(move |event: &mut Event1| {
acc.lock().push_str(&event.content);
Ok(())
});
let res2: Arc<AtomicUsize> = Arc::default();
let acc = Arc::clone(&res2);
register_hook!(move |event: &mut Event2| {
acc.fetch_add(event.content, Ordering::Relaxed);
Ok(())
});
// triggers events
let thread = std::thread::spawn(|| {
for i in 0..1000 {
dispatch(Event2 { content: i });
}
});
std::thread::sleep(Duration::from_millis(1));
dispatch(Event1 {
content: "foo".to_owned(),
});
dispatch(Event2 { content: 42 });
dispatch(Event1 {
content: "bar".to_owned(),
});
dispatch(Event1 {
content: "hello world".to_owned(),
});
thread.join().unwrap();
// check output
assert_eq!(&**res1.lock(), "foobarhello world");
assert_eq!(
res2.load(Ordering::Relaxed),
42 + (0..1000usize).sum::<usize>()
);
}
#[test]
fn dynamic() {
events! {
Event3 {}
Event4 { count: usize }
};
register_event::<Event3>();
register_event::<Event4>();
let count = Arc::new(AtomicUsize::new(0));
let count1 = count.clone();
let count2 = count.clone();
register_dynamic_hook(
move || {
count1.fetch_add(2, Ordering::Relaxed);
Ok(())
},
"Event3",
)
.unwrap();
register_dynamic_hook(
move || {
count2.fetch_add(3, Ordering::Relaxed);
Ok(())
},
"Event4",
)
.unwrap();
dispatch(Event3 {});
dispatch(Event4 { count: 0 });
dispatch(Event3 {});
assert_eq!(count.load(Ordering::Relaxed), 7)
}

@ -15,22 +15,23 @@ name = "hx-loader"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
helix-stdx = { path = "../helix-stdx" }
anyhow = "1" anyhow = "1"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
toml = "0.7" toml = "0.8"
etcetera = "0.8" etcetera = "0.8"
tree-sitter.workspace = true tree-sitter.workspace = true
once_cell = "1.19" once_cell = "1.20"
log = "0.4" log = "0.4"
which = "5.0.0"
# TODO: these two should be on !wasm32 only # TODO: these two should be on !wasm32 only
# cloning/compiling tree-sitter grammars # cloning/compiling tree-sitter grammars
cc = { version = "1" } cc = { version = "1" }
threadpool = { version = "1.0" } threadpool = { version = "1.0" }
tempfile = "3.8.1" tempfile = "3.13.0"
dunce = "1.0.4" dunce = "1.0.5"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies] [target.'cfg(not(target_arch = "wasm32"))'.dependencies]
libloading = "0.8" libloading = "0.8"

@ -50,6 +50,7 @@ fn main() {
.ok() .ok()
.filter(|output| output.status.success()) .filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok()) .and_then(|x| String::from_utf8(x.stdout).ok())
.map(|x| x.trim().to_string())
else { else {
return; return;
}; };
@ -67,6 +68,7 @@ fn main() {
.ok() .ok()
.filter(|output| output.status.success()) .filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok()) .and_then(|x| String::from_utf8(x.stdout).ok())
.map(|x| x.trim().to_string())
else { else {
return; return;
}; };

@ -86,10 +86,8 @@ pub fn get_language(name: &str) -> Result<Language> {
} }
fn ensure_git_is_available() -> Result<()> { fn ensure_git_is_available() -> Result<()> {
match which::which("git") { helix_stdx::env::which("git")?;
Ok(_cmd) => Ok(()), Ok(())
Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")),
}
} }
pub fn fetch_grammars() -> Result<()> { pub fn fetch_grammars() -> Result<()> {
@ -424,7 +422,7 @@ fn build_tree_sitter_library(
} }
} }
let recompile = needs_recompile(&library_path, &parser_path, &scanner_path) let recompile = needs_recompile(&library_path, &parser_path, scanner_path.as_ref())
.context("Failed to compare source and binary timestamps")?; .context("Failed to compare source and binary timestamps")?;
if !recompile { if !recompile {
@ -570,7 +568,7 @@ fn build_tree_sitter_library(
fn needs_recompile( fn needs_recompile(
lib_path: &Path, lib_path: &Path,
parser_c_path: &Path, parser_c_path: &Path,
scanner_path: &Option<PathBuf>, scanner_path: Option<&PathBuf>,
) -> Result<bool> { ) -> Result<bool> {
if !lib_path.exists() { if !lib_path.exists() {
return Ok(true); return Ok(true);

@ -1,14 +1,13 @@
pub mod config; pub mod config;
pub mod grammar; pub mod grammar;
use helix_stdx::{env::current_working_dir, path};
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::{Path, PathBuf}; use std::path::{Path, PathBuf};
use std::sync::RwLock;
pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> = static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
once_cell::sync::Lazy::new(prioritize_runtime_dirs); once_cell::sync::Lazy::new(prioritize_runtime_dirs);
@ -16,31 +15,6 @@ static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCe
static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new(); static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
// Get the current working directory.
// This information is managed internally as the call to std::env::current_dir
// might fail if the cwd has been deleted.
pub fn current_working_dir() -> PathBuf {
if let Some(path) = &*CWD.read().unwrap() {
return path.clone();
}
let path = std::env::current_dir()
.and_then(dunce::canonicalize)
.expect("Couldn't determine current working directory");
let mut cwd = CWD.write().unwrap();
*cwd = Some(path.clone());
path
}
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
let path = dunce::canonicalize(path)?;
std::env::set_current_dir(&path)?;
let mut cwd = CWD.write().unwrap();
*cwd = Some(path);
Ok(())
}
pub fn initialize_config_file(specified_file: Option<PathBuf>) { pub fn initialize_config_file(specified_file: Option<PathBuf>) {
let config_file = specified_file.unwrap_or_else(default_config_file); let config_file = specified_file.unwrap_or_else(default_config_file);
ensure_parent_dir(&config_file); ensure_parent_dir(&config_file);
@ -79,7 +53,8 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
rt_dirs.push(conf_rt_dir); rt_dirs.push(conf_rt_dir);
if let Ok(dir) = std::env::var("HELIX_RUNTIME") { if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
rt_dirs.push(dir.into()); let dir = path::expand_tilde(Path::new(&dir));
rt_dirs.push(path::normalize(dir));
} }
// If this variable is set during build time, it will always be included // If this variable is set during build time, it will always be included
@ -151,7 +126,7 @@ pub fn config_dir() -> PathBuf {
pub fn cache_dir() -> PathBuf { pub fn cache_dir() -> PathBuf {
// TODO: allow env var override // TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!"); let strategy = choose_base_strategy().expect("Unable to find the cache directory!");
let mut path = strategy.cache_dir(); let mut path = strategy.cache_dir();
path.push("helix"); path.push("helix");
path path
@ -250,13 +225,17 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
/// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root /// Used as a ceiling dir for LSP root resolution, the filepicker and potentially as a future filewatching root
/// ///
/// This function starts searching the FS upward from the CWD /// This function starts searching the FS upward from the CWD
/// and returns the first directory that contains either `.git` or `.helix`. /// and returns the first directory that contains either `.git`, `.svn`, `.jj` or `.helix`.
/// If no workspace was found returns (CWD, true). /// If no workspace was found returns (CWD, true).
/// Otherwise (workspace, false) is returned /// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) { pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = current_working_dir(); let current_dir = current_working_dir();
for ancestor in current_dir.ancestors() { for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() { if ancestor.join(".git").exists()
|| ancestor.join(".svn").exists()
|| ancestor.join(".jj").exists()
|| ancestor.join(".helix").exists()
{
return (ancestor.to_owned(), false); return (ancestor.to_owned(), false);
} }
} }
@ -280,21 +259,9 @@ fn ensure_parent_dir(path: &Path) {
mod merge_toml_tests { mod merge_toml_tests {
use std::str; use std::str;
use super::{current_working_dir, merge_toml_values, set_current_working_dir}; use super::merge_toml_values;
use toml::Value; use toml::Value;
#[test]
fn current_dir_is_set() {
let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
let cwd = current_working_dir();
assert_ne!(cwd, new_path);
set_current_working_dir(&new_path).expect("Couldn't set new path");
let cwd = current_working_dir();
assert_eq!(cwd, new_path);
}
#[test] #[test]
fn language_toml_map_merges() { fn language_toml_map_merges() {
const USER: &str = r#" const USER: &str = r#"

@ -0,0 +1,176 @@
# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
[[package]]
name = "bitflags"
version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "form_urlencoded"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a9c384f161156f5260c24a097c56119f9be8c798586aecc13afbcbe7b7e26bf8"
dependencies = [
"percent-encoding",
]
[[package]]
name = "idna"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e14ddfc70884202db2244c223200c204c2bda1bc6e0998d11b5e024d657209e6"
dependencies = [
"unicode-bidi",
"unicode-normalization",
]
[[package]]
name = "itoa"
version = "1.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4217ad341ebadf8d8e724e264f13e593e0648f5b3e94b3896a5df283be015ecc"
[[package]]
name = "lsp-types"
version = "0.95.1"
dependencies = [
"bitflags",
"serde",
"serde_json",
"serde_repr",
"url",
]
[[package]]
name = "percent-encoding"
version = "2.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "478c572c3d73181ff3c2539045f6eb99e5491218eae919370993b890cdbdd98e"
[[package]]
name = "proc-macro2"
version = "1.0.47"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5ea3d908b0e36316caf9e9e2c4625cdde190a7e6f440d794667ed17a1855e725"
dependencies = [
"unicode-ident",
]
[[package]]
name = "quote"
version = "1.0.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bbe448f377a7d6961e30f5955f9b8d106c3f5e449d493ee1b125c1d43c2b5179"
dependencies = [
"proc-macro2",
]
[[package]]
name = "ryu"
version = "1.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4501abdff3ae82a1c1b477a17252eb69cee9e66eb915c1abaa4f44d873df9f09"
[[package]]
name = "serde"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.145"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "serde_json"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074"
dependencies = [
"itoa",
"ryu",
"serde",
]
[[package]]
name = "serde_repr"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1fe39d9fbb0ebf5eb2c7cb7e2a47e4f462fad1379f1166b8ae49ad9eae89a7ca"
dependencies = [
"proc-macro2",
"quote",
"syn",
]
[[package]]
name = "syn"
version = "1.0.102"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3fcd952facd492f9be3ef0d0b7032a6e442ee9b361d4acc2b1d0c4aaa5f613a1"
dependencies = [
"proc-macro2",
"quote",
"unicode-ident",
]
[[package]]
name = "tinyvec"
version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "87cc5ceb3875bb20c2890005a4e226a4651264a5c75edb2421b52861a0a0cb50"
dependencies = [
"tinyvec_macros",
]
[[package]]
name = "tinyvec_macros"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "unicode-bidi"
version = "0.3.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "099b7128301d285f79ddd55b9a83d5e6b9e97c92e0ea0daebee7263e932de992"
[[package]]
name = "unicode-ident"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6ceab39d59e4c9499d4e5a8ee0e2735b891bb7308ac83dfb4e80cad195c9f6f3"
[[package]]
name = "unicode-normalization"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c5713f0fc4b5db668a2ac63cdb7bb4469d8c9fed047b1d0292cc7b0ce2ba921"
dependencies = [
"tinyvec",
]
[[package]]
name = "url"
version = "2.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0d68c799ae75762b8c3fe375feb6600ef5602c883c5d21eb51c09f22b83c4643"
dependencies = [
"form_urlencoded",
"idna",
"percent-encoding",
"serde",
]

@ -0,0 +1,34 @@
[package]
name = "helix-lsp-types"
version = "0.95.1"
authors = [
# Original authors
"Markus Westerlind <marwes91@gmail.com>",
"Bruno Medeiros <bruno.do.medeiros@gmail.com>",
# Since forking
"Helix contributors"
]
edition = "2018"
description = "Types for interaction with a language server, using VSCode's Language Server Protocol"
repository = "https://github.com/gluon-lang/lsp-types"
documentation = "https://docs.rs/lsp-types"
readme = "README.md"
keywords = ["language", "server", "lsp", "vscode", "lsif"]
license = "MIT"
[dependencies]
bitflags = "2.6.0"
serde = { version = "1.0.209", features = ["derive"] }
serde_json = "1.0.132"
serde_repr = "0.1"
url = {version = "2.0.0", features = ["serde"]}
[features]
default = []
# Enables proposed LSP extensions.
# NOTE: No semver compatibility is guaranteed for types enabled by this feature.
proposed = []

@ -0,0 +1,22 @@
The MIT License (MIT)
Copyright (c) 2016 Markus Westerlind
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

@ -0,0 +1,3 @@
# Helix's `lsp-types`
This is a fork of the [`lsp-types`](https://crates.io/crates/lsp-types) crate ([`gluon-lang/lsp-types`](https://github.com/gluon-lang/lsp-types)) taken at version v0.95.1 (commit [3e6daee](https://github.com/gluon-lang/lsp-types/commit/3e6daee771d14db4094a554b8d03e29c310dfcbe)). This fork focuses usability improvements that make the types easier to work with for the Helix codebase. For example the URL type - the `uri` crate at this version of `lsp-types` - will be replaced with a wrapper around a string.

@ -0,0 +1,127 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use url::Url;
use crate::{
DynamicRegistrationClientCapabilities, PartialResultParams, Range, SymbolKind, SymbolTag,
TextDocumentPositionParams, WorkDoneProgressOptions, WorkDoneProgressParams,
};
pub type CallHierarchyClientCapabilities = DynamicRegistrationClientCapabilities;
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize, Copy)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyOptions {
#[serde(flatten)]
pub work_done_progress_options: WorkDoneProgressOptions,
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, Copy)]
#[serde(untagged)]
pub enum CallHierarchyServerCapability {
Simple(bool),
Options(CallHierarchyOptions),
}
impl From<CallHierarchyOptions> for CallHierarchyServerCapability {
fn from(from: CallHierarchyOptions) -> Self {
Self::Options(from)
}
}
impl From<bool> for CallHierarchyServerCapability {
fn from(from: bool) -> Self {
Self::Simple(from)
}
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyPrepareParams {
#[serde(flatten)]
pub text_document_position_params: TextDocumentPositionParams,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
}
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyItem {
/// The name of this item.
pub name: String,
/// The kind of this item.
pub kind: SymbolKind,
/// Tags for this item.
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<SymbolTag>>,
/// More detail for this item, e.g. the signature of a function.
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
/// The resource identifier of this item.
pub uri: Url,
/// The range enclosing this symbol not including leading/trailing whitespace but everything else, e.g. comments and code.
pub range: Range,
/// The range that should be selected and revealed when this symbol is being picked, e.g. the name of a function.
/// Must be contained by the [`range`](#CallHierarchyItem.range).
pub selection_range: Range,
/// A data entry field that is preserved between a call hierarchy prepare and incoming calls or outgoing calls requests.
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyIncomingCallsParams {
pub item: CallHierarchyItem,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
#[serde(flatten)]
pub partial_result_params: PartialResultParams,
}
/// Represents an incoming call, e.g. a caller of a method or constructor.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyIncomingCall {
/// The item that makes the call.
pub from: CallHierarchyItem,
/// The range at which at which the calls appears. This is relative to the caller
/// denoted by [`this.from`](#CallHierarchyIncomingCall.from).
pub from_ranges: Vec<Range>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyOutgoingCallsParams {
pub item: CallHierarchyItem,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
#[serde(flatten)]
pub partial_result_params: PartialResultParams,
}
/// Represents an outgoing call, e.g. calling a getter from a method or a method from a constructor etc.
#[derive(Serialize, Deserialize, Debug, PartialEq, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CallHierarchyOutgoingCall {
/// The item that is called.
pub to: CallHierarchyItem,
/// The range at which this item is called. This is the range relative to the caller, e.g the item
/// passed to [`provideCallHierarchyOutgoingCalls`](#CallHierarchyItemProvider.provideCallHierarchyOutgoingCalls)
/// and not [`this.to`](#CallHierarchyOutgoingCall.to).
pub from_ranges: Vec<Range>,
}

@ -0,0 +1,395 @@
use crate::{
Command, Diagnostic, PartialResultParams, Range, TextDocumentIdentifier,
WorkDoneProgressOptions, WorkDoneProgressParams, WorkspaceEdit,
};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::borrow::Cow;
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum CodeActionProviderCapability {
Simple(bool),
Options(CodeActionOptions),
}
impl From<CodeActionOptions> for CodeActionProviderCapability {
fn from(from: CodeActionOptions) -> Self {
Self::Options(from)
}
}
impl From<bool> for CodeActionProviderCapability {
fn from(from: bool) -> Self {
Self::Simple(from)
}
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionClientCapabilities {
///
/// This capability supports dynamic registration.
///
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_registration: Option<bool>,
/// The client support code action literals as a valid
/// response of the `textDocument/codeAction` request.
#[serde(skip_serializing_if = "Option::is_none")]
pub code_action_literal_support: Option<CodeActionLiteralSupport>,
/// Whether code action supports the `isPreferred` property.
///
/// @since 3.15.0
#[serde(skip_serializing_if = "Option::is_none")]
pub is_preferred_support: Option<bool>,
/// Whether code action supports the `disabled` property.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled_support: Option<bool>,
/// Whether code action supports the `data` property which is
/// preserved between a `textDocument/codeAction` and a
/// `codeAction/resolve` request.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub data_support: Option<bool>,
/// Whether the client supports resolving additional code action
/// properties via a separate `codeAction/resolve` request.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub resolve_support: Option<CodeActionCapabilityResolveSupport>,
/// Whether the client honors the change annotations in
/// text edits and resource operations returned via the
/// `CodeAction#edit` property by for example presenting
/// the workspace edit in the user interface and asking
/// for confirmation.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub honors_change_annotations: Option<bool>,
}
/// Whether the client supports resolving additional code action
/// properties via a separate `codeAction/resolve` request.
///
/// @since 3.16.0
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionCapabilityResolveSupport {
/// The properties that a client can resolve lazily.
pub properties: Vec<String>,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionLiteralSupport {
/// The code action kind is support with the following value set.
pub code_action_kind: CodeActionKindLiteralSupport,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionKindLiteralSupport {
/// The code action kind values the client supports. When this
/// property exists the client also guarantees that it will
/// handle values outside its set gracefully and falls back
/// to a default value when unknown.
pub value_set: Vec<String>,
}
/// Params for the CodeActionRequest
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionParams {
/// The document in which the command was invoked.
pub text_document: TextDocumentIdentifier,
/// The range for which the command was invoked.
pub range: Range,
/// Context carrying additional information.
pub context: CodeActionContext,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
#[serde(flatten)]
pub partial_result_params: PartialResultParams,
}
/// response for CodeActionRequest
pub type CodeActionResponse = Vec<CodeActionOrCommand>;
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(untagged)]
pub enum CodeActionOrCommand {
Command(Command),
CodeAction(CodeAction),
}
impl From<Command> for CodeActionOrCommand {
fn from(command: Command) -> Self {
CodeActionOrCommand::Command(command)
}
}
impl From<CodeAction> for CodeActionOrCommand {
fn from(action: CodeAction) -> Self {
CodeActionOrCommand::CodeAction(action)
}
}
#[derive(Debug, Eq, PartialEq, Hash, PartialOrd, Clone, Deserialize, Serialize)]
pub struct CodeActionKind(Cow<'static, str>);
impl CodeActionKind {
/// Empty kind.
pub const EMPTY: CodeActionKind = CodeActionKind::new("");
/// Base kind for quickfix actions: 'quickfix'
pub const QUICKFIX: CodeActionKind = CodeActionKind::new("quickfix");
/// Base kind for refactoring actions: 'refactor'
pub const REFACTOR: CodeActionKind = CodeActionKind::new("refactor");
/// Base kind for refactoring extraction actions: 'refactor.extract'
///
/// Example extract actions:
///
/// - Extract method
/// - Extract function
/// - Extract variable
/// - Extract interface from class
/// - ...
pub const REFACTOR_EXTRACT: CodeActionKind = CodeActionKind::new("refactor.extract");
/// Base kind for refactoring inline actions: 'refactor.inline'
///
/// Example inline actions:
///
/// - Inline function
/// - Inline variable
/// - Inline constant
/// - ...
pub const REFACTOR_INLINE: CodeActionKind = CodeActionKind::new("refactor.inline");
/// Base kind for refactoring rewrite actions: 'refactor.rewrite'
///
/// Example rewrite actions:
///
/// - Convert JavaScript function to class
/// - Add or remove parameter
/// - Encapsulate field
/// - Make method static
/// - Move method to base class
/// - ...
pub const REFACTOR_REWRITE: CodeActionKind = CodeActionKind::new("refactor.rewrite");
/// Base kind for source actions: `source`
///
/// Source code actions apply to the entire file.
pub const SOURCE: CodeActionKind = CodeActionKind::new("source");
/// Base kind for an organize imports source action: `source.organizeImports`
pub const SOURCE_ORGANIZE_IMPORTS: CodeActionKind =
CodeActionKind::new("source.organizeImports");
/// Base kind for a 'fix all' source action: `source.fixAll`.
///
/// 'Fix all' actions automatically fix errors that have a clear fix that
/// do not require user input. They should not suppress errors or perform
/// unsafe fixes such as generating new types or classes.
///
/// @since 3.17.0
pub const SOURCE_FIX_ALL: CodeActionKind = CodeActionKind::new("source.fixAll");
pub const fn new(tag: &'static str) -> Self {
CodeActionKind(Cow::Borrowed(tag))
}
pub fn as_str(&self) -> &str {
&self.0
}
}
impl From<String> for CodeActionKind {
fn from(from: String) -> Self {
CodeActionKind(Cow::from(from))
}
}
impl From<&'static str> for CodeActionKind {
fn from(from: &'static str) -> Self {
CodeActionKind::new(from)
}
}
#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeAction {
/// A short, human-readable, title for this code action.
pub title: String,
/// The kind of the code action.
/// Used to filter code actions.
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<CodeActionKind>,
/// The diagnostics that this code action resolves.
#[serde(skip_serializing_if = "Option::is_none")]
pub diagnostics: Option<Vec<Diagnostic>>,
/// The workspace edit this code action performs.
#[serde(skip_serializing_if = "Option::is_none")]
pub edit: Option<WorkspaceEdit>,
/// A command this code action executes. If a code action
/// provides an edit and a command, first the edit is
/// executed and then the command.
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<Command>,
/// Marks this as a preferred action. Preferred actions are used by the `auto fix` command and can be targeted
/// by keybindings.
/// A quick fix should be marked preferred if it properly addresses the underlying error.
/// A refactoring should be marked preferred if it is the most reasonable choice of actions to take.
///
/// @since 3.15.0
#[serde(skip_serializing_if = "Option::is_none")]
pub is_preferred: Option<bool>,
/// Marks that the code action cannot currently be applied.
///
/// Clients should follow the following guidelines regarding disabled code actions:
///
/// - Disabled code actions are not shown in automatic
/// [lightbulb](https://code.visualstudio.com/docs/editor/editingevolved#_code-action)
/// code action menu.
///
/// - Disabled actions are shown as faded out in the code action menu when the user request
/// a more specific type of code action, such as refactorings.
///
/// - If the user has a keybinding that auto applies a code action and only a disabled code
/// actions are returned, the client should show the user an error message with `reason`
/// in the editor.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub disabled: Option<CodeActionDisabled>,
/// A data entry field that is preserved on a code action between
/// a `textDocument/codeAction` and a `codeAction/resolve` request.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionDisabled {
/// Human readable description of why the code action is currently disabled.
///
/// This is displayed in the code actions UI.
pub reason: String,
}
/// The reason why code actions were requested.
///
/// @since 3.17.0
#[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
#[serde(transparent)]
pub struct CodeActionTriggerKind(i32);
lsp_enum! {
impl CodeActionTriggerKind {
/// Code actions were explicitly requested by the user or by an extension.
pub const INVOKED: CodeActionTriggerKind = CodeActionTriggerKind(1);
/// Code actions were requested automatically.
///
/// This typically happens when current selection in a file changes, but can
/// also be triggered when file content changes.
pub const AUTOMATIC: CodeActionTriggerKind = CodeActionTriggerKind(2);
}
}
/// Contains additional diagnostic information about the context in which
/// a code action is run.
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionContext {
/// An array of diagnostics.
pub diagnostics: Vec<Diagnostic>,
/// Requested kind of actions to return.
///
/// Actions not of this kind are filtered out by the client before being shown. So servers
/// can omit computing them.
#[serde(skip_serializing_if = "Option::is_none")]
pub only: Option<Vec<CodeActionKind>>,
/// The reason why code actions were requested.
///
/// @since 3.17.0
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_kind: Option<CodeActionTriggerKind>,
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, Default)]
#[serde(rename_all = "camelCase")]
pub struct CodeActionOptions {
/// CodeActionKinds that this server may return.
///
/// The list of kinds may be generic, such as `CodeActionKind.Refactor`, or the server
/// may list out every specific kind they provide.
#[serde(skip_serializing_if = "Option::is_none")]
pub code_action_kinds: Option<Vec<CodeActionKind>>,
#[serde(flatten)]
pub work_done_progress_options: WorkDoneProgressOptions,
/// The server provides support to resolve additional
/// information for a code action.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub resolve_provider: Option<bool>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::test_serialization;
#[test]
fn test_code_action_response() {
test_serialization(
&vec![
CodeActionOrCommand::Command(Command {
title: "title".to_string(),
command: "command".to_string(),
arguments: None,
}),
CodeActionOrCommand::CodeAction(CodeAction {
title: "title".to_string(),
kind: Some(CodeActionKind::QUICKFIX),
command: None,
diagnostics: None,
edit: None,
is_preferred: None,
..CodeAction::default()
}),
],
r#"[{"title":"title","command":"command"},{"title":"title","kind":"quickfix"}]"#,
)
}
}

@ -0,0 +1,66 @@
use serde::{Deserialize, Serialize};
use serde_json::Value;
use crate::{
Command, DynamicRegistrationClientCapabilities, PartialResultParams, Range,
TextDocumentIdentifier, WorkDoneProgressParams,
};
pub type CodeLensClientCapabilities = DynamicRegistrationClientCapabilities;
/// Code Lens options.
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize, Copy)]
#[serde(rename_all = "camelCase")]
pub struct CodeLensOptions {
/// Code lens has a resolve provider as well.
#[serde(skip_serializing_if = "Option::is_none")]
pub resolve_provider: Option<bool>,
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeLensParams {
/// The document to request code lens for.
pub text_document: TextDocumentIdentifier,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
#[serde(flatten)]
pub partial_result_params: PartialResultParams,
}
/// A code lens represents a command that should be shown along with
/// source text, like the number of references, a way to run tests, etc.
///
/// A code lens is _unresolved_ when no command is associated to it. For performance
/// reasons the creation of a code lens and resolving should be done in two stages.
#[derive(Debug, Clone, PartialEq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeLens {
/// The range in which this code lens is valid. Should only span a single line.
pub range: Range,
/// The command this code lens represents.
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<Command>,
/// A data entry field that is preserved on a code lens item between
/// a code lens and a code lens resolve request.
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CodeLensWorkspaceClientCapabilities {
/// Whether the client implementation supports a refresh request sent from the
/// server to the client.
///
/// Note that this event is global and will force the client to refresh all
/// code lenses currently shown. It should be used with absolute care and is
/// useful for situation where a server for example detect a project wide
/// change that requires such a calculation.
#[serde(skip_serializing_if = "Option::is_none")]
pub refresh_support: Option<bool>,
}

@ -0,0 +1,122 @@
use crate::{
DocumentSelector, DynamicRegistrationClientCapabilities, PartialResultParams, Range,
TextDocumentIdentifier, TextEdit, WorkDoneProgressParams,
};
use serde::{Deserialize, Serialize};
pub type DocumentColorClientCapabilities = DynamicRegistrationClientCapabilities;
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorProviderOptions {}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StaticTextDocumentColorProviderOptions {
/// A document selector to identify the scope of the registration. If set to null
/// the document selector provided on the client side will be used.
pub document_selector: Option<DocumentSelector>,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum ColorProviderCapability {
Simple(bool),
ColorProvider(ColorProviderOptions),
Options(StaticTextDocumentColorProviderOptions),
}
impl From<ColorProviderOptions> for ColorProviderCapability {
fn from(from: ColorProviderOptions) -> Self {
Self::ColorProvider(from)
}
}
impl From<StaticTextDocumentColorProviderOptions> for ColorProviderCapability {
fn from(from: StaticTextDocumentColorProviderOptions) -> Self {
Self::Options(from)
}
}
impl From<bool> for ColorProviderCapability {
fn from(from: bool) -> Self {
Self::Simple(from)
}
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DocumentColorParams {
/// The text document
pub text_document: TextDocumentIdentifier,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
#[serde(flatten)]
pub partial_result_params: PartialResultParams,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorInformation {
/// The range in the document where this color appears.
pub range: Range,
/// The actual color value for this color range.
pub color: Color,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize, Copy)]
#[serde(rename_all = "camelCase")]
pub struct Color {
/// The red component of this color in the range [0-1].
pub red: f32,
/// The green component of this color in the range [0-1].
pub green: f32,
/// The blue component of this color in the range [0-1].
pub blue: f32,
/// The alpha component of this color in the range [0-1].
pub alpha: f32,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ColorPresentationParams {
/// The text document.
pub text_document: TextDocumentIdentifier,
/// The color information to request presentations for.
pub color: Color,
/// The range where the color would be inserted. Serves as a context.
pub range: Range,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
#[serde(flatten)]
pub partial_result_params: PartialResultParams,
}
#[derive(Debug, PartialEq, Eq, Deserialize, Serialize, Default, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ColorPresentation {
/// The label of this color presentation. It will be shown on the color
/// picker header. By default this is also the text that is inserted when selecting
/// this color presentation.
pub label: String,
/// An [edit](#TextEdit) which is applied to a document when selecting
/// this presentation for the color. When `falsy` the [label](#ColorPresentation.label)
/// is used.
#[serde(skip_serializing_if = "Option::is_none")]
pub text_edit: Option<TextEdit>,
/// An optional array of additional [text edits](#TextEdit) that are applied when
/// selecting this color presentation. Edits must not overlap with the main [edit](#ColorPresentation.textEdit) nor with themselves.
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_text_edits: Option<Vec<TextEdit>>,
}

@ -0,0 +1,622 @@
use serde::{Deserialize, Serialize};
use crate::{
Command, Documentation, MarkupKind, PartialResultParams, TagSupport,
TextDocumentPositionParams, TextDocumentRegistrationOptions, TextEdit, WorkDoneProgressOptions,
WorkDoneProgressParams,
};
use crate::Range;
use serde_json::Value;
use std::fmt::Debug;
/// Defines how to interpret the insert text in a completion item
#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(transparent)]
pub struct InsertTextFormat(i32);
lsp_enum! {
impl InsertTextFormat {
pub const PLAIN_TEXT: InsertTextFormat = InsertTextFormat(1);
pub const SNIPPET: InsertTextFormat = InsertTextFormat(2);
}
}
/// The kind of a completion entry.
#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(transparent)]
pub struct CompletionItemKind(i32);
lsp_enum! {
impl CompletionItemKind {
pub const TEXT: CompletionItemKind = CompletionItemKind(1);
pub const METHOD: CompletionItemKind = CompletionItemKind(2);
pub const FUNCTION: CompletionItemKind = CompletionItemKind(3);
pub const CONSTRUCTOR: CompletionItemKind = CompletionItemKind(4);
pub const FIELD: CompletionItemKind = CompletionItemKind(5);
pub const VARIABLE: CompletionItemKind = CompletionItemKind(6);
pub const CLASS: CompletionItemKind = CompletionItemKind(7);
pub const INTERFACE: CompletionItemKind = CompletionItemKind(8);
pub const MODULE: CompletionItemKind = CompletionItemKind(9);
pub const PROPERTY: CompletionItemKind = CompletionItemKind(10);
pub const UNIT: CompletionItemKind = CompletionItemKind(11);
pub const VALUE: CompletionItemKind = CompletionItemKind(12);
pub const ENUM: CompletionItemKind = CompletionItemKind(13);
pub const KEYWORD: CompletionItemKind = CompletionItemKind(14);
pub const SNIPPET: CompletionItemKind = CompletionItemKind(15);
pub const COLOR: CompletionItemKind = CompletionItemKind(16);
pub const FILE: CompletionItemKind = CompletionItemKind(17);
pub const REFERENCE: CompletionItemKind = CompletionItemKind(18);
pub const FOLDER: CompletionItemKind = CompletionItemKind(19);
pub const ENUM_MEMBER: CompletionItemKind = CompletionItemKind(20);
pub const CONSTANT: CompletionItemKind = CompletionItemKind(21);
pub const STRUCT: CompletionItemKind = CompletionItemKind(22);
pub const EVENT: CompletionItemKind = CompletionItemKind(23);
pub const OPERATOR: CompletionItemKind = CompletionItemKind(24);
pub const TYPE_PARAMETER: CompletionItemKind = CompletionItemKind(25);
}
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionItemCapability {
/// Client supports snippets as insert text.
///
/// A snippet can define tab stops and placeholders with `$1`, `$2`
/// and `${3:foo}`. `$0` defines the final tab stop, it defaults to
/// the end of the snippet. Placeholders with equal identifiers are linked,
/// that is typing in one will update others too.
#[serde(skip_serializing_if = "Option::is_none")]
pub snippet_support: Option<bool>,
/// Client supports commit characters on a completion item.
#[serde(skip_serializing_if = "Option::is_none")]
pub commit_characters_support: Option<bool>,
/// Client supports the follow content formats for the documentation
/// property. The order describes the preferred format of the client.
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation_format: Option<Vec<MarkupKind>>,
/// Client supports the deprecated property on a completion item.
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated_support: Option<bool>,
/// Client supports the preselect property on a completion item.
#[serde(skip_serializing_if = "Option::is_none")]
pub preselect_support: Option<bool>,
/// Client supports the tag property on a completion item. Clients supporting
/// tags have to handle unknown tags gracefully. Clients especially need to
/// preserve unknown tags when sending a completion item back to the server in
/// a resolve call.
#[serde(
default,
skip_serializing_if = "Option::is_none",
deserialize_with = "TagSupport::deserialize_compat"
)]
pub tag_support: Option<TagSupport<CompletionItemTag>>,
/// Client support insert replace edit to control different behavior if a
/// completion item is inserted in the text or should replace text.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_replace_support: Option<bool>,
/// Indicates which properties a client can resolve lazily on a completion
/// item. Before version 3.16.0 only the predefined properties `documentation`
/// and `details` could be resolved lazily.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub resolve_support: Option<CompletionItemCapabilityResolveSupport>,
/// The client supports the `insertTextMode` property on
/// a completion item to override the whitespace handling mode
/// as defined by the client.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_text_mode_support: Option<InsertTextModeSupport>,
/// The client has support for completion item label
/// details (see also `CompletionItemLabelDetails`).
///
/// @since 3.17.0
#[serde(skip_serializing_if = "Option::is_none")]
pub label_details_support: Option<bool>,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionItemCapabilityResolveSupport {
/// The properties that a client can resolve lazily.
pub properties: Vec<String>,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InsertTextModeSupport {
pub value_set: Vec<InsertTextMode>,
}
/// How whitespace and indentation is handled during completion
/// item insertion.
///
/// @since 3.16.0
#[derive(Eq, PartialEq, Clone, Copy, Serialize, Deserialize)]
#[serde(transparent)]
pub struct InsertTextMode(i32);
lsp_enum! {
impl InsertTextMode {
/// The insertion or replace strings is taken as it is. If the
/// value is multi line the lines below the cursor will be
/// inserted using the indentation defined in the string value.
/// The client will not apply any kind of adjustments to the
/// string.
pub const AS_IS: InsertTextMode = InsertTextMode(1);
/// The editor adjusts leading whitespace of new lines so that
/// they match the indentation up to the cursor of the line for
/// which the item is accepted.
///
/// Consider a line like this: `<2tabs><cursor><3tabs>foo`. Accepting a
/// multi line completion item is indented using 2 tabs all
/// following lines inserted will be indented using 2 tabs as well.
pub const ADJUST_INDENTATION: InsertTextMode = InsertTextMode(2);
}
}
#[derive(Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(transparent)]
pub struct CompletionItemTag(i32);
lsp_enum! {
impl CompletionItemTag {
pub const DEPRECATED: CompletionItemTag = CompletionItemTag(1);
}
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionItemKindCapability {
/// The completion item kind values the client supports. When this
/// property exists the client also guarantees that it will
/// handle values outside its set gracefully and falls back
/// to a default value when unknown.
///
/// If this property is not present the client only supports
/// the completion items kinds from `Text` to `Reference` as defined in
/// the initial version of the protocol.
#[serde(skip_serializing_if = "Option::is_none")]
pub value_set: Option<Vec<CompletionItemKind>>,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionListCapability {
/// The client supports the following itemDefaults on
/// a completion list.
///
/// The value lists the supported property names of the
/// `CompletionList.itemDefaults` object. If omitted
/// no properties are supported.
///
/// @since 3.17.0
#[serde(skip_serializing_if = "Option::is_none")]
pub item_defaults: Option<Vec<String>>,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionClientCapabilities {
/// Whether completion supports dynamic registration.
#[serde(skip_serializing_if = "Option::is_none")]
pub dynamic_registration: Option<bool>,
/// The client supports the following `CompletionItem` specific
/// capabilities.
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_item: Option<CompletionItemCapability>,
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_item_kind: Option<CompletionItemKindCapability>,
/// The client supports to send additional context information for a
/// `textDocument/completion` request.
#[serde(skip_serializing_if = "Option::is_none")]
pub context_support: Option<bool>,
/// The client's default when the completion item doesn't provide a
/// `insertTextMode` property.
///
/// @since 3.17.0
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_text_mode: Option<InsertTextMode>,
/// The client supports the following `CompletionList` specific
/// capabilities.
///
/// @since 3.17.0
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_list: Option<CompletionListCapability>,
}
/// A special text edit to provide an insert and a replace operation.
///
/// @since 3.16.0
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct InsertReplaceEdit {
/// The string to be inserted.
pub new_text: String,
/// The range if the insert is requested
pub insert: Range,
/// The range if the replace is requested.
pub replace: Range,
}
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum CompletionTextEdit {
Edit(TextEdit),
InsertAndReplace(InsertReplaceEdit),
}
impl From<TextEdit> for CompletionTextEdit {
fn from(edit: TextEdit) -> Self {
CompletionTextEdit::Edit(edit)
}
}
impl From<InsertReplaceEdit> for CompletionTextEdit {
fn from(edit: InsertReplaceEdit) -> Self {
CompletionTextEdit::InsertAndReplace(edit)
}
}
/// Completion options.
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionOptions {
/// The server provides support to resolve additional information for a completion item.
#[serde(skip_serializing_if = "Option::is_none")]
pub resolve_provider: Option<bool>,
/// Most tools trigger completion request automatically without explicitly
/// requesting it using a keyboard shortcut (e.g. Ctrl+Space). Typically they
/// do so when the user starts to type an identifier. For example if the user
/// types `c` in a JavaScript file code complete will automatically pop up
/// present `console` besides others as a completion item. Characters that
/// make up identifiers don't need to be listed here.
///
/// If code complete should automatically be trigger on characters not being
/// valid inside an identifier (for example `.` in JavaScript) list them in
/// `triggerCharacters`.
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_characters: Option<Vec<String>>,
/// The list of all possible characters that commit a completion. This field
/// can be used if clients don't support individual commit characters per
/// completion item. See client capability
/// `completion.completionItem.commitCharactersSupport`.
///
/// If a server provides both `allCommitCharacters` and commit characters on
/// an individual completion item the ones on the completion item win.
///
/// @since 3.2.0
#[serde(skip_serializing_if = "Option::is_none")]
pub all_commit_characters: Option<Vec<String>>,
#[serde(flatten)]
pub work_done_progress_options: WorkDoneProgressOptions,
/// The server supports the following `CompletionItem` specific
/// capabilities.
///
/// @since 3.17.0
#[serde(skip_serializing_if = "Option::is_none")]
pub completion_item: Option<CompletionOptionsCompletionItem>,
}
#[derive(Debug, Eq, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionOptionsCompletionItem {
/// The server has support for completion item label
/// details (see also `CompletionItemLabelDetails`) when receiving
/// a completion item in a resolve call.
///
/// @since 3.17.0
#[serde(skip_serializing_if = "Option::is_none")]
pub label_details_support: Option<bool>,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
pub struct CompletionRegistrationOptions {
#[serde(flatten)]
pub text_document_registration_options: TextDocumentRegistrationOptions,
#[serde(flatten)]
pub completion_options: CompletionOptions,
}
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)]
#[serde(untagged)]
pub enum CompletionResponse {
Array(Vec<CompletionItem>),
List(CompletionList),
}
impl From<Vec<CompletionItem>> for CompletionResponse {
fn from(items: Vec<CompletionItem>) -> Self {
CompletionResponse::Array(items)
}
}
impl From<CompletionList> for CompletionResponse {
fn from(list: CompletionList) -> Self {
CompletionResponse::List(list)
}
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionParams {
// This field was "mixed-in" from TextDocumentPositionParams
#[serde(flatten)]
pub text_document_position: TextDocumentPositionParams,
#[serde(flatten)]
pub work_done_progress_params: WorkDoneProgressParams,
#[serde(flatten)]
pub partial_result_params: PartialResultParams,
// CompletionParams properties:
#[serde(skip_serializing_if = "Option::is_none")]
pub context: Option<CompletionContext>,
}
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionContext {
/// How the completion was triggered.
pub trigger_kind: CompletionTriggerKind,
/// The trigger character (a single character) that has trigger code complete.
/// Is undefined if `triggerKind !== CompletionTriggerKind.TriggerCharacter`
#[serde(skip_serializing_if = "Option::is_none")]
pub trigger_character: Option<String>,
}
/// How a completion was triggered.
#[derive(Eq, PartialEq, Clone, Copy, Deserialize, Serialize)]
#[serde(transparent)]
pub struct CompletionTriggerKind(i32);
lsp_enum! {
impl CompletionTriggerKind {
pub const INVOKED: CompletionTriggerKind = CompletionTriggerKind(1);
pub const TRIGGER_CHARACTER: CompletionTriggerKind = CompletionTriggerKind(2);
pub const TRIGGER_FOR_INCOMPLETE_COMPLETIONS: CompletionTriggerKind = CompletionTriggerKind(3);
}
}
/// Represents a collection of [completion items](#CompletionItem) to be presented
/// in the editor.
#[derive(Debug, PartialEq, Clone, Default, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct CompletionList {
/// This list it not complete. Further typing should result in recomputing
/// this list.
pub is_incomplete: bool,
/// The completion items.
pub items: Vec<CompletionItem>,
}
#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CompletionItem {
/// The label of this completion item. By default
/// also the text that is inserted when selecting
/// this completion.
pub label: String,
/// Additional details for the label
///
/// @since 3.17.0
#[serde(skip_serializing_if = "Option::is_none")]
pub label_details: Option<CompletionItemLabelDetails>,
/// The kind of this completion item. Based of the kind
/// an icon is chosen by the editor.
#[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<CompletionItemKind>,
/// A human-readable string with additional information
/// about this item, like type or symbol information.
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
/// A human-readable string that represents a doc-comment.
#[serde(skip_serializing_if = "Option::is_none")]
pub documentation: Option<Documentation>,
/// Indicates if this item is deprecated.
#[serde(skip_serializing_if = "Option::is_none")]
pub deprecated: Option<bool>,
/// Select this item when showing.
#[serde(skip_serializing_if = "Option::is_none")]
pub preselect: Option<bool>,
/// A string that should be used when comparing this item
/// with other items. When `falsy` the label is used
/// as the sort text for this item.
#[serde(skip_serializing_if = "Option::is_none")]
pub sort_text: Option<String>,
/// A string that should be used when filtering a set of
/// completion items. When `falsy` the label is used as the
/// filter text for this item.
#[serde(skip_serializing_if = "Option::is_none")]
pub filter_text: Option<String>,
/// A string that should be inserted into a document when selecting
/// this completion. When `falsy` the label is used as the insert text
/// for this item.
///
/// The `insertText` is subject to interpretation by the client side.
/// Some tools might not take the string literally. For example
/// VS Code when code complete is requested in this example
/// `con<cursor position>` and a completion item with an `insertText` of
/// `console` is provided it will only insert `sole`. Therefore it is
/// recommended to use `textEdit` instead since it avoids additional client
/// side interpretation.
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_text: Option<String>,
/// The format of the insert text. The format applies to both the `insertText` property
/// and the `newText` property of a provided `textEdit`. If omitted defaults to `InsertTextFormat.PlainText`.
///
/// @since 3.16.0
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_text_format: Option<InsertTextFormat>,
/// How whitespace and indentation is handled during completion
/// item insertion. If not provided the client's default value depends on
/// the `textDocument.completion.insertTextMode` client capability.
///
/// @since 3.16.0
/// @since 3.17.0 - support for `textDocument.completion.insertTextMode`
#[serde(skip_serializing_if = "Option::is_none")]
pub insert_text_mode: Option<InsertTextMode>,
/// An edit which is applied to a document when selecting
/// this completion. When an edit is provided the value of
/// insertText is ignored.
///
/// Most editors support two different operation when accepting a completion item. One is to insert a
/// completion text and the other is to replace an existing text with a completion text. Since this can
/// usually not predetermined by a server it can report both ranges. Clients need to signal support for
/// `InsertReplaceEdits` via the `textDocument.completion.insertReplaceSupport` client capability
/// property.
///
/// *Note 1:* The text edit's range as well as both ranges from a insert replace edit must be a
/// [single line] and they must contain the position at which completion has been requested.
/// *Note 2:* If an `InsertReplaceEdit` is returned the edit's insert range must be a prefix of
/// the edit's replace range, that means it must be contained and starting at the same position.
///
/// @since 3.16.0 additional type `InsertReplaceEdit`
#[serde(skip_serializing_if = "Option::is_none")]
pub text_edit: Option<CompletionTextEdit>,
/// An optional array of additional text edits that are applied when
/// selecting this completion. Edits must not overlap with the main edit
/// nor with themselves.
#[serde(skip_serializing_if = "Option::is_none")]
pub additional_text_edits: Option<Vec<TextEdit>>,
/// An optional command that is executed *after* inserting this completion. *Note* that
/// additional modifications to the current document should be described with the
/// additionalTextEdits-property.
#[serde(skip_serializing_if = "Option::is_none")]
pub command: Option<Command>,
/// An optional set of characters that when pressed while this completion is
/// active will accept it first and then type that character. *Note* that all
/// commit characters should have `length=1` and that superfluous characters
/// will be ignored.
#[serde(skip_serializing_if = "Option::is_none")]
pub commit_characters: Option<Vec<String>>,
/// An data entry field that is preserved on a completion item between
/// a completion and a completion resolve request.
#[serde(skip_serializing_if = "Option::is_none")]
pub data: Option<Value>,
/// Tags for this completion item.
#[serde(skip_serializing_if = "Option::is_none")]
pub tags: Option<Vec<CompletionItemTag>>,
}
impl CompletionItem {
/// Create a CompletionItem with the minimum possible info (label and detail).
pub fn new_simple(label: String, detail: String) -> CompletionItem {
CompletionItem {
label,
detail: Some(detail),
..Self::default()
}
}
}
/// Additional details for a completion item label.
///
/// @since 3.17.0
#[derive(Debug, PartialEq, Default, Deserialize, Serialize, Clone)]
#[serde(rename_all = "camelCase")]
pub struct CompletionItemLabelDetails {
/// An optional string which is rendered less prominently directly after
/// {@link CompletionItemLabel.label label}, without any spacing. Should be
/// used for function signatures or type annotations.
#[serde(skip_serializing_if = "Option::is_none")]
pub detail: Option<String>,
/// An optional string which is rendered less prominently after
/// {@link CompletionItemLabel.detail}. Should be used for fully qualified
/// names or file path.
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::tests::test_deserialization;
#[test]
fn test_tag_support_deserialization() {
let empty = CompletionItemCapability {
tag_support: None,
..CompletionItemCapability::default()
};
test_deserialization(r#"{}"#, &empty);
test_deserialization(r#"{"tagSupport": false}"#, &empty);
let t = CompletionItemCapability {
tag_support: Some(TagSupport { value_set: vec![] }),
..CompletionItemCapability::default()
};
test_deserialization(r#"{"tagSupport": true}"#, &t);
let t = CompletionItemCapability {
tag_support: Some(TagSupport {
value_set: vec![CompletionItemTag::DEPRECATED],
}),
..CompletionItemCapability::default()
};
test_deserialization(r#"{"tagSupport": {"valueSet": [1]}}"#, &t);
}
#[test]
fn test_debug_enum() {
assert_eq!(format!("{:?}", CompletionItemKind::TEXT), "Text");
assert_eq!(
format!("{:?}", CompletionItemKind::TYPE_PARAMETER),
"TypeParameter"
);
}
#[test]
fn test_try_from_enum() {
use std::convert::TryInto;
assert_eq!("Text".try_into(), Ok(CompletionItemKind::TEXT));
assert_eq!(
"TypeParameter".try_into(),
Ok(CompletionItemKind::TYPE_PARAMETER)
);
}
}

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save