Compare commits

...

544 Commits

Author SHA1 Message Date
dependabot[bot] ccabfee381
build(deps): bump rustix from 0.38.11 to 0.38.13 (#8249)
Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.38.11 to 0.38.13.
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.11...v0.38.13)

---
updated-dependencies:
- dependency-name: rustix
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] e3d537cee5
build(deps): bump url from 2.4.0 to 2.4.1 (#8250)
Bumps [url](https://github.com/servo/rust-url) from 2.4.0 to 2.4.1.
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.4.0...v2.4.1)

---
updated-dependencies:
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] 719ef3f879
build(deps): bump chrono from 0.4.26 to 0.4.30 (#8247)
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.26 to 0.4.30.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.26...v0.4.30)

---
updated-dependencies:
- dependency-name: chrono
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] d46127fb2e
build(deps): bump which from 4.4.0 to 4.4.1 (#8251)
Bumps [which](https://github.com/harryfei/which-rs) from 4.4.0 to 4.4.1.
- [Changelog](https://github.com/harryfei/which-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harryfei/which-rs/compare/4.4.0...4.4.1)

---
updated-dependencies:
- dependency-name: which
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] a8449afbfe
build(deps): bump thiserror from 1.0.47 to 1.0.48 (#8252)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.47 to 1.0.48.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.47...1.0.48)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
Em Zhan 7090555dab
Add `insert-final-newline` config option (#8157)
Co-authored-by: Xalfer <64538944+Xalfer@users.noreply.github.com>
8 months ago
Blaž Hrastnik ef23847957
scheme: Highlight abbreviations 8 months ago
Blaž Hrastnik 95e994ab38
Add more shebangs to languages 8 months ago
Blaž Hrastnik 060e73a711
Lower idle-timeout to 250ms
The aim is to make it slow enough it only triggers during a typing
pause, but not too slow. Initial value was chosen as a safe slow
default but I've been using 250 for a while.
8 months ago
Galen Abell acef759a5e
Add additional YAML injections (#8217) 8 months ago
Alexis Mousset 83ac53a109
Fix various typos (#8233) 8 months ago
Yhya 6f3a6575dc
add material theme collection (#8211)
* Create material theme files

* Add material deep ocean pallete

* Add primary theme properties to material deep ocean theme

* Fix material deep ocean theme

* Ad syntax highlighting to material deep ocean theme

* Make material oceanic theme

* Make material darker theme

* Remove material lighter theme

* Make material palenight theme

* Make other material themes inherit material deep ocean theme

* Add virtual ruler background to the material theme collection
8 months ago
Jesse Luehrs 81d6d3ff0e
re-add indent and textobject queries for perl (#7947)
* bump tree-sitter-perl version

need some grammar tweaks for the indent queries to function properly

* add indent queries for perl

* add textobject queries for perl
8 months ago
Alexis Mousset 829db76563
Add feed-related formats as xml (#8232) 8 months ago
Ross Manchester 0d986fce76
chore: add additional ignore file highlights (#8220)
* chore: add additional ignore file highlights

Various files use the same syntax highlighting as `.gitignore` and
similarly tell different tools what files/folders to ignore. Update the
languages file so that other ignore type files use the same highlighting
as gitignore. The files added are:

- `.ignore`
- `.prettierignore`
- `.eslintignore`
- `.npmignore`

* chore: add highlighting for codeowners files

Add `CODEOWNERS` as an additional file type for `git-ignore` in the
language file. `CODEOWNERS`'s grammar is close enough to that of
`.gitignore`, this can be used to avoid making a new grammar
specifically for `CODEOWNERS` files.
8 months ago
Luke Halasy b959162ceb
Add tree-sitter-highlight-name command (#8170)
* adds treesitter-highlight-name command

* commit documentation changes

* moves the get_highlight_name function into core/syntax

* rename get_highlight_name function to get_highlight_for_node_at_position

* addresses pr comments: moves fn into helper fn, simplifies a lot

* commit updated documentation changes

* changes scope method to return &str so that callers can decide whether or not to own
8 months ago
Jaden 528a5e3aff
Update EdgedDB (ESDL) grammar (#8222) 8 months ago
Pascal Kuthe eb9c37844c
fix syntax highlights in dynamic picker (#8206) 8 months ago
Theodore Gregory 14401ff75b
docs: fix link to document formatting requests (#8166) 8 months ago
Weiyuan Wu 8017bb2999
add redraw command (#6949)
Co-authored-by: Roberto Vidal <vidal.roberto.j@gmail.com>
8 months ago
Michael Davis c0fd8bc61b
Fix Clone definition for Injector (#8194) 8 months ago
Pascal Kuthe e6cdc5f9d3
Don't use word splitting during fuzzy matching (#8192) 8 months ago
Pascal Kuthe 0cfd46c14f
Do not show (running) when opening picker (#8148)
* only stream from background thread if necessary

If the file transversal is longer shorter 30ms it will now be performed
on the main thread. Spawning a thread can take a while (or rather it
takes a while until that thread is scheduled) so the files can actually
take a while to show up. This prevents the `(running)` indicator from
briefly showing up when opening the file picker in a small directory.

* run partial cargo update
8 months ago
Jonathan LEI 8778083b5a
Detect tmux clipboard provider on macOS (#8182) 8 months ago
dependabot[bot] 48b7520bca
build(deps): bump actions/checkout from 3 to 4 (#8173)
Bumps [actions/checkout](https://github.com/actions/checkout) from 3 to 4.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] 65c3cca3cc
build(deps): bump serde_json from 1.0.104 to 1.0.105 (#8177)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] 4dbdcaebba
build(deps): bump regex from 1.9.4 to 1.9.5 (#8175)
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.4 to 1.9.5.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.4...1.9.5)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] 761cdf124a
build(deps): bump serde from 1.0.185 to 1.0.188 (#8176)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.185 to 1.0.188.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.185...v1.0.188)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] 10e7ca819b
build(deps): bump cachix/install-nix-action from 22 to 23 (#8172)
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 22 to 23.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Commits](https://github.com/cachix/install-nix-action/compare/v22...v23)

---
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
dependabot[bot] 9b397c9e68
build(deps): bump rustix from 0.38.8 to 0.38.11 (#8174)
Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.38.8 to 0.38.11.
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.8...v0.38.11)

---
updated-dependencies:
- dependency-name: rustix
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
8 months ago
John Scarrott 61814fea7f
Nord Theme: Fix missing ui text focus, use undercurls for diagnostics (#8165) 8 months ago
Ivan Molodetskikh 9d7f66574d
Update tree-sitter-blueprint (#8161) 8 months ago
Lorenzo Bellina e8fc77fe98
Maintain the current cursor's position and view in the vsplit/hsplit commands too (#8109)
Co-authored-by: Benjamin Bouvier <public@benj.me>
8 months ago
woojiq bb3e6998e6
Fix find commands for buffers with non-LF line-endings (#8111) 8 months ago
Pascal Kuthe a38ec6d6ca
avoid excessive memory consumption in picker (#8127)
* avoid excessive memory consumption from file picker

* fix typos

Co-authored-by: Chris <75008413+cd-a@users.noreply.github.com>

---------

Co-authored-by: Chris <75008413+cd-a@users.noreply.github.com>
8 months ago
Michael Davis 7cf775d512
Build flake packages with stable Rust (#8133)
We can continue to use the MSRV for local development and checks while
building release executables with the latest stable Rust, as we do in
CI.
8 months ago
Michael Davis 48373d4a2b
Clear completion when switching windows via click (#8118)
The completion component assumes that it operates on the same View but
it's possible to break this assumption by switching windows through
left-clicking. I believe we should clear the completion menu when
switching windows to fix this.

This change fixes a panic for this scenario:

* Open a buffer with LSP completion available
* Split the window (for example '<C-w>v')
* Enter insert mode and trigger the completion menu
* Select a completion candidate (for example with '<C-n>')
* Switch to the original window by left-clicking in its area
* Enter insert mode and make edits (for example 'o<backspace>')

This will trip the 'assert_eq' in Document::restore.
8 months ago
Michael Davis a2767269d0
crossterm: Handle 'hidden' modifier (#8120)
Crossterm supports the 'hidden' SGR parameter but we previously didn't
set the attribute when the "hidden" modifier was specified in a theme.
8 months ago
Ezekiel Warren 6bef982f2d
use which on formatter command (#8064) 8 months ago
chtenb 7fffc0a5d1
Rename reset to default (#8114)
Use `default` instead of `reset`, as this is the conventional name for ANSI codes 39/49. The word `reset` should be reserved for ANSI code `0`, which resets both fg and bg colors at once, while also removing all modifiers. While the code uses the value name `Reset`, this is misleading and should not leak into the user space.
9 months ago
Pascal Kuthe 0cb595e226
transition to nucleo for fuzzy matching (#7814)
* transition to nucleo for fuzzy matching

* drop flakey test case

since the picker streams in results now any test that relies
on the picker containing results is potentially flakely

* use crates.io version of nucleo

* Fix typo in commands.rs

Co-authored-by: Skyler Hawthorne <skyler@dead10ck.com>

---------

Co-authored-by: Skyler Hawthorne <skyler@dead10ck.com>
9 months ago
Tanguy 40d7e6c9c8
Copy desktop and icon files to Nix output (#7979) 9 months ago
woojiq b67d2c3a68
fix: line numbers remain relative when helix loses focus (#7955)
* fix: line numbers remain relative when helix loses focus

If `line number = relative` and a new window is opened in helix, lines inside unfocused windows will be `absolute`. This commit adds the same thing when helix becomes unfocused in a terminal emulator.

* partial rebase
9 months ago
West 82cd445715
add `reset` to the color palette (#8083) 9 months ago
dependabot[bot] 3ac2ac6dd6
build(deps): bump log from 0.4.19 to 0.4.20 (#8103)
Bumps [log](https://github.com/rust-lang/log) from 0.4.19 to 0.4.20.
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.19...0.4.20)

---
updated-dependencies:
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] d71e58c1ba
build(deps): bump encoding_rs from 0.8.32 to 0.8.33 (#8104)
Bumps [encoding_rs](https://github.com/hsivonen/encoding_rs) from 0.8.32 to 0.8.33.
- [Commits](https://github.com/hsivonen/encoding_rs/compare/v0.8.32...v0.8.33)

---
updated-dependencies:
- dependency-name: encoding_rs
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] 92652638be
build(deps): bump tokio from 1.31.0 to 1.32.0 (#8105)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.31.0 to 1.32.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.31.0...tokio-1.32.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] bcbad25e78
build(deps): bump regex from 1.9.3 to 1.9.4 (#8106)
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.3 to 1.9.4.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.3...1.9.4)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
Damir Vandic 79c0425154
Sync latest catppuccin theme changes (#8102) 9 months ago
Michael Davis 072e1eae92
Update tree-sitter-gleam, enable auto-format (#8085) 9 months ago
sigmaSd 992c858369
chore: update strace tree sitter grammar (#8087) 9 months ago
Álan Crístoffer 9f843e4f56
highlight(matlab): bumps grammar after some improvements (#8040) 9 months ago
Sol Fisher Romanoff aeaeb09f48
add gemini language support (#8070) 9 months ago
David Else c9694f680f
Add ltex-ls language server (#7838) 9 months ago
arslee07 75c5a33028
Highlight Dart 3 `sealed` and `base` keywords (#7974) 9 months ago
Michael Davis 546c8ca344 Handle switch from crossterm::Result to io::Result 9 months ago
Michael Davis 050c019ccb Translate new ScrollLeft/ScrollRight crossterm mouse events 9 months ago
Michael Davis e8fef6b6fc Bump crossterm to 0.27 in helix-term on macos 9 months ago
dependabot[bot] c3442f3a18 build(deps): bump crossterm from 0.26.1 to 0.27.0
Bumps [crossterm](https://github.com/crossterm-rs/crossterm) from 0.26.1 to 0.27.0.
- [Release notes](https://github.com/crossterm-rs/crossterm/releases)
- [Changelog](https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossterm-rs/crossterm/compare/0.26.1...0.27.0)

---
updated-dependencies:
- dependency-name: crossterm
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
9 months ago
dependabot[bot] 4ac4055fd5
build(deps): bump serde from 1.0.183 to 1.0.185 (#8034)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] 2123b993fc
build(deps): bump thiserror from 1.0.44 to 1.0.47 (#8039)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.44 to 1.0.47.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.44...1.0.47)

---
updated-dependencies:
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] 10f75ac67a
build(deps): bump tempfile from 3.7.1 to 3.8.0 (#8038)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.7.1 to 3.8.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.7.1...v3.8.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] 52d5bc0eef
build(deps): bump cc from 1.0.79 to 1.0.83 (#8037)
Bumps [cc](https://github.com/rust-lang/cc-rs) from 1.0.79 to 1.0.83.
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.79...1.0.83)

---
updated-dependencies:
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] d7e4d07943
build(deps): bump anyhow from 1.0.72 to 1.0.75 (#8035)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.72 to 1.0.75.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.72...1.0.75)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
Pascal Kuthe e5f8d8ef04
create separate timer for redraw requests (#8023)
* create separate timer for redraw requests

* Update helix-view/src/editor.rs

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
9 months ago
David Else 454b61cb21
Update pyright config to avoid time-outs (#8032) 9 months ago
Mike 0cc94cd87a
goto_file_impl: use relative path to open file (#7965)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
9 months ago
kaashyapan 75342968e2
update fsharp tree-sitter (#8024) 9 months ago
nkitsaini 22f4f313f1
Remove unnecessary `Err` from `get_canonicalized_path` (#8009)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
9 months ago
nkitsaini 2767459f89
Remove path completions for `:new` command (#8010) 9 months ago
Michael Davis 01a1e5ec2a
Update tree-sitter to latest master (#7998) 9 months ago
dastrukar e4c95f65a6
Nord theme: Update ruler to set bg instead of fg (#7995) 9 months ago
Tomas Sandven 18a79aa3bf
Update tree-sitter-robot (#7970)
* Update tree-sitter-robot

* Update Robot highlights query for Helix

* Change @comment.single to @comment

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
9 months ago
Jack Allison 2b7e7c80eb
Update Monokai bufferline theming to be distinguish active buffers (#7983)
* Update OneDark theme to use light-gray for inlay hints.

* fix monokai tab themeing to be more distinguishing
9 months ago
Pham Huy Hoang 56ccaedffb
markdown.inline: Add `injection.combined` to html tag (#7960)
Problem: Closing tags for markdown is sometimes not highlighted
Solution: Add `injection.combined` to create a valid syntax tree for
highlighting
9 months ago
Bjorn Ove Hay Andersen 0a45fb4371
document a-ret picker keybinding (#7884) 9 months ago
Jens Getreu 61ccf4eded
autumn theme: improve readability of comments (#7939)
* Improve readability of comments

* Rename color

* Rename color

* Sort variables
9 months ago
David Bell 567eda88ef
add .star as starlark file extension (#7922)
In addition to the other defined extensions, `.star` is a frequently used extension for starlark files. This can be demonstrated through a cursory search of github for files ending in `.star` here: https://github.com/search?q=path%3A%2F.star%24%2F&type=code
9 months ago
sigmaSd cbfe8eef89
add strace highlighting (#7928)
* feat: add strace tree sitter

* f
9 months ago
Andrés Cabero 090a225f28
goto_file: open picker if a directory is selected (#7909)
* feat: open file picker on directories using goto_file (gf)

* remove helper and call to canonicalize
9 months ago
N 7b2f3f533c
Recognize more filenames for zsh (#7930)
Including `zshrc` et al. since this is convention in dotfiles repos
9 months ago
Dillard Robertson ea88677394
Stop Terminal::drop from overriding work of Terminal::restore. (#7931)
When Application::run is exiting, either Terminal::restore or
Terminal::force_restore will be called depending
on if a panic occured or not.
Both of these functions will reset the cursor to terminal's default.

After this is done, Terminal::drop will be called.
If terminal.cursor_kind == Hidden, then
the cursor will be reset to a CursorKind::Block,
undoing the work of restore or force_restore.

This commit just removes the drop implementation,
as its job is already better handled in restore and force_restore.
9 months ago
dependabot[bot] 2756f70dfc
build(deps): bump rustix from 0.38.4 to 0.38.8 (#7946)
Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.38.4 to 0.38.8.
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.4...v0.38.8)

---
updated-dependencies:
- dependency-name: rustix
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] a1c50056bf
build(deps): bump regex from 1.9.1 to 1.9.3 (#7945)
Bumps [regex](https://github.com/rust-lang/regex) from 1.9.1 to 1.9.3.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.9.1...1.9.3)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] e9a807a48f
build(deps): bump bitflags from 2.3.3 to 2.4.0 (#7943)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.3.3 to 2.4.0.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.3.3...2.4.0)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] 0b2e96885f
build(deps): bump tokio from 1.29.1 to 1.31.0 (#7944)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.29.1 to 1.31.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.29.1...tokio-1.31.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
Jonathan LEI 3a162e2bef
Make editor remember the latest search register (#5244) 9 months ago
quantonganh 085706e0cd
Include completions for git-ignored files in debugger prompt (#7936) 9 months ago
theteachr d6bb1092c7 Update stale comments
Obsoleted by https://github.com/helix-editor/helix/pull/4731
9 months ago
Jesse Luehrs d56638ba9a fix formatting in the rust textobject query file
looks like two lines were unintentionally joined - it doesn't appear to
affect the functionality, but it's confusing to read
9 months ago
Erasin Wang 19dff5c3a4
Update slint grammar (#7893) 9 months ago
Noob Zhang b0c270f8e6
Added some LSP servers and updated python's roots (#7897)
* Add csharp-ls for possible c-sharp LSP

See https://github.com/razzmatazz/csharp-language-server for more info
about it.

* Add pyright for possible python LSP

It may be prefered than pylsp by someone.
According to https://github.com/helix-editor/helix/issues/5479, I don't
make it default for everyone. Just for people who need this.

* Update roots of python

Using some known filenames to detect correct project root.

* Add pylyzer for possible python LSP

Co-authored-by: zetashift <rskaraya@gmail.com>

---------

Co-authored-by: zetashift <rskaraya@gmail.com>
9 months ago
Jesse Luehrs 2caca1c4e9
Add pod highlighting (#7907) 9 months ago
Dillard Robertson 01776e6851
Prevent GraphemeStrs created from Strings from leaking (#7920) 9 months ago
Daniel Ebert ee3171cc54 Document @align indent capture. 9 months ago
Daniel Ebert b315901cbb Run indentation tests on a part of the Helix source code.
Add C++ indent test file.
9 months ago
Daniel Ebert 155cedc5c8 Fix broken indentation that causes the indentation tests to fail.
For some reason, `cargo fmt` does not change the indentation in
these places (maybe it isn't sure about what the correct formatting
should be).
9 months ago
Daniel Ebert 36a59e4482 Improve C, Rust & Python indent queries & add @align captures. 9 months ago
Daniel Ebert eab0d4fa4b Implement @align (and @anchor) indent query. 9 months ago
Skyler Hawthorne 929eb0c39e expand indents guide 9 months ago
Skyler Hawthorne 7078e84007 Fix YAML auto indent
YAML indents queries are tweaked to fix auto indent behavior.

A new capture type `indent.always` is introduced to address use cases
where combining indent captures on a single line is desired.

Fixes #6661
9 months ago
Ivan Isekeev 57f093d836
Jinja language family syntax support (#7233)
* feat: add jinja language support

* feat: add nunjucks language support

* feat: add to lang support book jinja and nunjucks languages
9 months ago
Jan9103 c0eae84073
feat: add todo.txt tree-sitter (#7835) 9 months ago
Gaël cb9b08d650
Add new Yellowed theme to default themes (#7849)
* added new Yellowed theme to default themes

* syntax typo and missing color fix
9 months ago
Artemiy 1077630834
Update tree-sitter grammar for nu (#7873)
* Update tree-sitter grammar for nu

Change tree-sitter grammar for nushell to 'officially' maintained
by nushell project https://github.com/nushell/tree-sitter-nu. Update
to the latest version. Replace queries with supported

* Restore injection queries for nu

Restore injection.scm queries for nushell tree-sitter grammar
9 months ago
Alex Vinyals 48eb0d4792
Enhance :toggle to support cycling numbers (#7877) 9 months ago
zetashift 294aa669a2
Add Unison support (#7724) 9 months ago
Michael Davis f01ca107fb
Detect non-existent files as non-readonly (#7875) 9 months ago
Tshepang Mbambo cefc33e3df
use AND operator when searching (#7839)
This makes search results less surprising, because it is how major web search engines behave
9 months ago
dependabot[bot] 1c1df42cc0
build(deps): bump globset from 0.4.12 to 0.4.13 (#7864)
Bumps [globset](https://github.com/BurntSushi/ripgrep) from 0.4.12 to 0.4.13.
- [Release notes](https://github.com/BurntSushi/ripgrep/releases)
- [Changelog](https://github.com/BurntSushi/ripgrep/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/ripgrep/compare/globset-0.4.12...globset-0.4.13)

---
updated-dependencies:
- dependency-name: globset
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] f7c0ca7e0c
build(deps): bump tempfile from 3.7.0 to 3.7.1 (#7862)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.7.0 to 3.7.1.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.7.0...v3.7.1)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] 942852b933
build(deps): bump lsp-types from 0.94.0 to 0.94.1 (#7861)
Bumps [lsp-types](https://github.com/gluon-lang/lsp-types) from 0.94.0 to 0.94.1.
- [Changelog](https://github.com/gluon-lang/lsp-types/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gluon-lang/lsp-types/compare/v0.94.0...v0.94.1)

---
updated-dependencies:
- dependency-name: lsp-types
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
woojiq aa4d84a0b3
Align view for background buffer opened with `alt-ret` (#7691)
* fix(picker): `alt-ret' changes cursor pos of current file, not new one

Closes #7673

* fix other pickers

* symbol pickers
* diagnostick pickers

This is done using the already patched `jump_to_location` method.

* fix global and jumplist pickers

* use `view` as old_id; make `align_view` method of `Action`

* test(picker): basic <alt-ret> functionality

* fix: picker integrational test

* fix nit

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
9 months ago
dependabot[bot] c1c71bb90e
build(deps): bump serde from 1.0.180 to 1.0.183 (#7860)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
Connortsui20 fcbac485f8
Show whether file readonly in statusline (#7740) 9 months ago
Jesse Luehrs a7a145ad3d
Center the picker preview selection using visual lines (#7837)
this way the preview always shows the selection even if lines were
wrapped
9 months ago
Anshul Dalal c7e9e94f00
Skip rendering gutters when gutter width exceeds view width (#7821) 9 months ago
woojiq 7cda5b8592
build(tree-sitter): update javascript, typescript and tsx (#7852)
* build(tree-sitter): update javascript, typescript and tsx

* update revision of tree-sitter parsers for these languages.
* rename `?.` to `optional_chain`, introduced in tree-sitter/tree-sitter-javascript@186f2adbf7.

* fix(highlight): change jsx queries to match latest tree-sitter

Latest tree-sitter/tree-sitter-javascript@bb1f97b643 added some breaking changes that broke highlighting.
* Remove some queries with `nested_identifier`.
* Remove deprecated `jsx_fragment` from indent query.
* Count `</` and `/>` as a single token.
9 months ago
Petr Gajdůšek 7af37bb3b9
Add tree-sitter textobjects queries for bash (#7764)
This implements function, (calling) argument and comment captures for use
in the textobject selections in bash.

This also updates the generated docs after adding the textobjects for bash.
9 months ago
Michael Davis d6c799fb30
Update tree-sitter-git-commit (#7831)
This fixes a problem parsing the "On branch _branch_" part of the
commit comment when the branch contains a slash.
9 months ago
Michael Davis 86fc203197
CI: Remove the aarch64 appimage build steps from release (#7832)
The steps mistakenly produce a x86_64 appimage and call it aarch64.
linuxdeploy doesn't currently support producing aarch64 appimages so
we should just remove these steps for aarch64-linux.
9 months ago
Blaž Hrastnik 57071513a8 Only use tsq for tsq files
This makes our highlight files more plain but it correctly highlights
scheme :/
9 months ago
Blaž Hrastnik 979933b514 Update tree-sitter-scheme 9 months ago
woojiq 1d189820a1
feat(indent): add basic java indentation queries (#7844) 9 months ago
Mohamed Imrane Chehabi 80d2599f9c
Add new moon theme to default themes (#7834)
* Add new moon theme to default themes

* Remove .DS_Store
9 months ago
Jummit f19793c2f8
Improve wren support (#7819) 9 months ago
Christoph Sax 5a51036bc1
Update t32 language queries to version 2.2.1 (#7811)
Version 2.2.1 of the grammar adds extended support for HLL (C, C++,..)
expressions. Quite a few node types were added, renamed or removed in
the process.

This change brings the highlight queries in sync with the ones found in
the repository of the grammar. The highlighting tests "look" okay after
updating the queries.

Recently, Codeberg had some reliability issues. That is why the language
is now using the mirror repository on GitLab as source instead.

Co-authored-by: Christoph Sax <christoph.sax@mailbox.org>
9 months ago
J. Brock 286e44050d
Bump the version of Hare's grammar (#7784) 9 months ago
dependabot[bot] d5af4ae6b3
build(deps): bump serde from 1.0.175 to 1.0.180 (#7794)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.175 to 1.0.180.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.175...v1.0.180)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
Austin L Wolfgram 5535ba8b7d
fix range formatting error message typo (#7823) 9 months ago
voroskoi bc737404e8
Update tree-sitter-zig (#7803) 9 months ago
Jimmy Zelinskie 325692a154
languages: add protobuf language servers (#7796)
* languages: add bufls protobuf language server

* languages: add pbkit protobuf language server
9 months ago
Skyler Hawthorne 15e07d4db8 feat: smart_tab
Implement `smart_tab`, which optionally makes the tab key run the
`move_parent_node_start` command when the cursor has non- whitespace to
its left.
9 months ago
Skyler Hawthorne 93acb53812 add node boundary movement 9 months ago
Zoey Hewll 1d702ea191
update yanked dependency (#7800) 9 months ago
dependabot[bot] 99413558b9
build(deps): bump serde_json from 1.0.103 to 1.0.104 (#7793)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
dependabot[bot] 0eea76c415
build(deps): bump globset from 0.4.11 to 0.4.12 (#7795)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
9 months ago
Michael Davis d4f9716fbc Add yank_to_clipboard commands, bind to `<space>y` by default
The clipboard special registers are able to retain multiple selections
and also join the value when copying it to the clipboard. So by default
we should yank regularly to the '*' and '+' registers. That will have
the same behavior for the clipboards but will allow pasting multiple
selections if the clipboard doesn't change between yanks.
10 months ago
Michael Davis 4555a6b433 Reimplement clipboard commands in terms of special regs
Since the clipboard provider now lives on the Registers type, we want
to eliminate it from the Editor. We can do that and clean up the
commands that interact with the clipboard by calling regular yank,
paste and replace impls on the clipboard special registers.

Eventually the clipboard commands could be removed once macro keybinding
is supported.
10 months ago
Michael Davis 2d838d729c Preview the latest value for regular registers
This fixes a discrepancy between regular registers which are used for
yanking multiple values (for example via `"ay`) and regular registers
that store a history of values (for example `"a*`).

Previously, the preview shown in `select_register`'s infobox would show
the oldest value in history. It's intuitive and useful to see the most
recent value pushed to the history though.

We cannot simply switch the preview line from `values.first()`
to `values.last()`: that would fix the preview for registers
used for history but break the preview for registers used to yank
multiple values. We could push to the beginning of the values with
`Registers::push` but this is wasteful from a performance perspective.
Instead we can have `Registers::read` return an iterator that
returns elements in the reverse order and reverse the values in
`Register::write`. This effectively means that `push` adds elements to
the beginning of the register's values. For the sake of the preview, we
can switch to `values.last()` and that is then correct for both usage-
styles. This also needs a change to call-sites that read the latest
history value to switch from `last` to `first`.
10 months ago
Michael Davis 86a1f0177c book: Document default and special registers 10 months ago
Michael Davis a23b70182c commands: Allow using selected register where hardcoded
These snippets use hardcoded registers but it can be useful to be able
to specify a register for these commands.
10 months ago
Michael Davis baceb02a09 Use refactored Registers type
This is an unfortunately noisy change: we need to update virtually all
callsites that access the registers. For reads this means passing in the
Editor and for writes this means handling potential failure when we
can't write to a clipboard register.
10 months ago
Michael Davis 0f19f282cf Add system & primary clipboards as special registers
These special registers join and copy the values to the clipboards with
'*' corresponding to the system clipboard and '+' to the primary as
they are in Vim. This also uses the trick from PR6889 to save the values
in the register and re-use them without joining into one value when
pasting a value which was yanked and not changed.

These registers are not implemented in Kakoune but Kakoune also does
not have a built-in clipboard integration.

Co-authored-by: CcydtN <51289140+CcydtN@users.noreply.github.com>
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
10 months ago
Michael Davis 32d071a392 Add the '%' (current filename) register
This register also comes from Kakoune. It's read-only and produces the
current document's name, defaulting to the scratch buffer name
constant.

(Also see PR5577.)

Co-authored-by: Ivan Tham <pickfire@riseup.net>
10 months ago
Michael Davis da2afe7353 Add '#' and '.' special registers
These come from Kakoune:

* '#' is the selection index register. It's read-only and produces the
  selection index numbers, 1-indexed.
* '.' is the selection contents register. It is also read-only and
  mirrors the contents of the current selections when read.

We switch the iterators returned from Selection's `fragments` and
`slices` methods to ExactSizeIterators because:

* The selection contents register can simply return the fragments
  iterator.
* ExactSizeIterator is already implemented for iterators over Vecs, so
  it's essentially free.
* The `len` method can be useful on its own.
10 months ago
Michael Davis 5eb1a25d8a Refactor Registers to take Editor
This sets up a new Registers type that will allow us to expand support
for special registers. (See the child commits.)

We start simple with the regular (`Vec<String>`) registers and the
simplest special register, the black hole. In the child commits we
will expand these match arms with more special registers.

The upcoming special registers will need a few things that aren't
possible with the current Registers type in helix-core:

* Access to the `Editor`. This is only necessary when reading from
  registers, so the `&Editor` parameter is only added to
  `Registers::read`.
* Returning owned values. Registers in helix-core returns references
  to the values backed by the `Vec<String>` but future special registers
  will need to return owned values. We refactor the return value of the
  read operations to give `Cow<str>`s and iterators over those.
* Returning a `Result` for write/push functions. This will be used by
  the clipboard special registers.
10 months ago
lydiandy 57952c46a4
replace new lsp for vlang (#7760)
* fix vlang grammar fetch and build fail

* update highlights.scm for v-analyzer

* Update languages.toml

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

* Update runtime/queries/v/highlights.scm

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

* update scm for new lsp

* gen doc lang-support.md

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
10 months ago
Jesse Luehrs d5571968fa
add new theme based on the default vim dark theme (#7785) 10 months ago
Mateusz S. Szczygieł 13e7edab19
Register .gltf file type for JSON (#7781) 10 months ago
Matthias Q 224fd5fa29
feat: update prql parser (#7771) 10 months ago
Jummit 9a4890f62b
Add wren support (#7765) 10 months ago
Michael Davis d6856cfeec
Refactor Nix flake to use crane (#7763)
This resolves a build issue with nci/dream2nix and git dependencies.
We can keep most of the helix-specific parts of the flake, we just need
to switch from configuring nci to calling craneLib functions.

We also switch to flake-utils from flake-parts:

* Using rust-overlay with flake-parts directly is unergonomic
  (see https://github.com/hercules-ci/flake-parts/discussions/83).
* Removing flake-parts reduces the overall dependencies: rust-overlay
  already depends on flake-utils.
10 months ago
Philipp Mildenberger 8a28f30593
Reformat with nightly rustfmt for better let-else formatting (#7721) 10 months ago
Pascal Kuthe 262a595e53
pin TS to unreleased git revision to fix freezes (#7737) 10 months ago
Michael Davis 953073a679 highlighted_code_block: Take input text as &str
This removes a handful of allocations for functions calling into the
function, which is nice because the prompt may call this function on
every keypress.
10 months ago
Michael Davis 98ef05d768 Prefer RopeSlice to &Rope in helix_core::syntax
Pascal and I discussed this and we think it's generally better to
take a 'RopeSlice' rather than a '&Rope'. The code block rendering
function in the markdown component module is a good example for how
this can be useful: we can remove an allocation of a rope and instead
directly turn a '&str' into a 'RopeSlice' which is very cheap.

A change to prefer 'RopeSlice' to '&Rope' whenever the rope isn't
modified would be nice, but it would be a very large diff (around 500+
500-). Starting off with just the syntax functions seems like a nice
middle-ground, and we can remove a Rope allocation because of it.

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
10 months ago
Michael Davis f0b877e258 Tune regex highlights for usage in prompts
Since regex is almost always injected into other languages,
`pattern_character`s will inherit the highlight for the structure that
injects them (for example `/foo/` in JavaScript or `~r/foo/` in Elixir).
This removes the string highlight when used in the prompt.

We also add `ERROR` node highlighting so that errors in regex syntax
appear in the prompt. This resolves a TODO in the `regex_prompt`
function about highlighting errors in the regex.
10 months ago
Michael Davis 0dc3753eb2 Syntax-highlight regex prompts
We can use tree-sitter-regex highlighting in prompts for entering
regexes, like `search` or `global_search`. The `highlighted_code_block`
function from the markdown component makes this a very small change.

This could be improved in the future by leaving the parsed syntax tree
on the prompt, allowing incremental updates. Prompt lines are usually so
short though and tree-sitter-regex is rather small and uncomplicated,
so that improvement probably wouldn't make a big difference.
10 months ago
Michael Davis 6a431afc4e
Save an undo checkpoint before accepting completion (#7747) 10 months ago
eh dea6894f92
Theme Pop-Dark: Increase Diagnostics clarity (#7702) 10 months ago
saltlakrits 5a52897014
Update everforest_dark.toml to add missing color definitions (#7739) 10 months ago
Pham Huy Hoang 75239a938f
fix incorrect predicate in comment highlights (#7732) 10 months ago
Federico Stra a188282b37
Update soft-wrap indicator in gruvbox themes (#7736) 10 months ago
dependabot[bot] b266628c17
build(deps): bump signal-hook from 0.3.16 to 0.3.17 (#7728)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 00dc205108
build(deps): bump thiserror from 1.0.43 to 1.0.44 (#7730)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 7295340119
build(deps): bump serde from 1.0.171 to 1.0.175 (#7727)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 46251a1411
build(deps): bump tempfile from 3.6.0 to 3.7.0 (#7726)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
sigmaSd 5ec126d3e2
Fix docs for default statusline config (#7720) 10 months ago
Ravi Shekhar Jethani 48d57dad47
Fix selection highlighting in remaining gruvbox derived themes (#7717) 10 months ago
Jonas Tepe 505213d41b
Drop mut from variable to silence linter (#7704) 10 months ago
Ryan Fowler 5c41f22c2a
Add support for LSP DidChangeWatchedFiles (#7665)
* Add initial support for LSP DidChangeWatchedFiles

* Move file event Handler to helix-lsp

* Simplify file event handling

* Refactor file event handling

* Block on future within LSP file event handler

* Fully qualify uses of the file_event::Handler type

* Rename ops field to options

* Revert newline removal from helix-view/Cargo.toml

* Ensure file event Handler is cleaned up when lsp client is shutdown
10 months ago
sigmaSd 8977123f25
feat: resolve code action (#7677) 10 months ago
Thales Ramos d52b790379
Add Kaolin Dark, Light and Valley Dark themes (#7151)
Add some missing keys

Inherit themes from kaolin-dark and override diverging keys
10 months ago
Christian Holman 579f68b52d
allow for higher F keys to be used (#7672) 10 months ago
dependabot[bot] b47519ab11
build(deps): bump signal-hook from 0.3.15 to 0.3.16 (#7664)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] b87858b7b4
build(deps): bump indoc from 2.0.2 to 2.0.3 (#7663)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 1478a0d3a6
build(deps): bump anyhow from 1.0.71 to 1.0.72 (#7662)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 6d4fd77315
build(deps): bump serde_json from 1.0.100 to 1.0.103 (#7661)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
J. Brock 2fa576b177
Remove snap aliasing instructions (#7657)
The helix snap now gets aliased to hx by default at installation time,
so manual aliasing should no longer be required.

Signed-off-by: Joseph Brock <joseph.brock@protonmail.com>
10 months ago
Jesse Luehrs e7f60611ac
switch to https://github.com/tree-sitter-perl/tree-sitter-perl (#7644) 10 months ago
Jake Langford ad2061bab6
Update my name README.md (#7656) 10 months ago
Pascal Kuthe 8f1c6456f3
Clear statusline while prompt is visible (#7646) 10 months ago
Pascal Kuthe 68a98ac36b use a single query for injections
In the past we used two separate queries for combined and normal injections. There was no real reason for this (except historical/slightly easier implementation). Instead, we now use a single query and simply check if an injection corresponds to a combined injection or not.
10 months ago
Pascal Kuthe 2d5ff9ec8f fix crash when encountering overlapping injections 10 months ago
Doug Kelkhoff 79a8fd6249
Add a more file types for R (#7633) 10 months ago
kaashyapan 2ace6032e7
Add fsharp language support (#7619) 10 months ago
Michael Goodness 86bf0e00fe
feat: add Brewfile to Ruby file-types (#7629) 10 months ago
woojiq ab819ede9a
docs(install): add how to install helix from snap (#7625) 10 months ago
Erasin Wang 2cb00bcbc4
Support inlay-hints for svelteserver. (#7622) 10 months ago
Jeppe Christiansen bc4f08febf
Update Typescript, TSX and Svelte grammar, to latest tag (#6874)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
10 months ago
Alex Vinyals 843ae97120
enhanced `surround_replace` to provide visual feedback (#7588) 10 months ago
Nick Saika 9551e4e111
runtime/themes: Add "naysayer" theme (#7570) 10 months ago
Jonathan 0e0501c510
Fix piping to Helix on macOS (#5468) 10 months ago
Karim Mk e86bb64b63
Change dark_plus inlay-hints colors to more pleasant colors (#7611)
* Changing code_dark inlay-hints colors.

* Using dark_plus_experimental inlay hints is better ;)
10 months ago
Arian Dehghani a5f7190614
Register systemd files as ini (#7592) 10 months ago
Jorge Santiago 9259c52606
Add shebang for nushell files (#7606) 10 months ago
Tudyx 9893a1fbcc
Auto indent change if selection is linewise (#7316) 10 months ago
Yomain 8afc0282f2
Fix crash when cwd is deleted (#7185) 10 months ago
Pascal Kuthe 1adb19464f
search buffer contents during global search (#5652) 10 months ago
dependabot[bot] 541d2b76d6
build(deps): bump serde_json from 1.0.99 to 1.0.100 (#7598)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] ac57e93583
build(deps): bump smallvec from 1.10.0 to 1.11.0 (#7597)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 1790097d59
build(deps): bump regex from 1.8.4 to 1.9.1 (#7596)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 57babd9456
build(deps): bump serde from 1.0.166 to 1.0.171 (#7595)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 62b2b6360d
build(deps): bump toml from 0.7.5 to 0.7.6 (#7594)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 66a0b64853
build(deps): bump thiserror from 1.0.40 to 1.0.43 (#7593)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
Queyrouzec f68956a306
Update dart commit in languages.toml (#7576) 10 months ago
Gabriel Hansson c1488267e5
(Updated) Apply motion API refinements (#6078)
* _apply_motion generalization where possible

API encourages users to not forget setting `editor.last_motion` when
applying a motion. But also not setting `last_motion` without applying a
motion first.

* (rename) will_find_char -> find_char

method name makes it sound like it would be returning a boolean.

* use _apply_motion in find_char

Feature that falls out from this is that repetitions of t,T,f,F are
saved with the context extention/move and count. (Not defaulting to extend
by 1 count).

* Finalize apply_motion API

last_motion is now a private field and can only be set by calling
Editor.apply_motion(). Removing need (and possibility) of writing:

`motion(editor); editor.last_motion = motion`

Now it's just: `editor.apply_motion(motion)`

* editor.last_message: rm Box wrap around Arc

* Use pre-existing `Direction` rather than custom `SearchDirection`.

* `LastMotion` type alias for `Option<Arc<dyn Fn(&mut Editor)>>`

* Take motion rather than cloning it.

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

* last_motion as Option<Motion>.

* Use `Box` over `Arc` for `last_motion`.

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
10 months ago
Em Zhan 9a324f337a
docs: Update mdBook theme and improve maintainability (#7524) 10 months ago
Borys Lykah 7c338429f8
Add language support for persistent library syntax (#7261) 10 months ago
Sharpened Blade c33795e770
Update the Nord theme to follow the Nord style guide (#7490) 10 months ago
Gammut 607b426e26
Refactor queries for ecma based languages (#7207) 10 months ago
Alex Vinyals 28452e1f2a
Initialize log and config files right after parsing arguments (#7585) 10 months ago
Álan Crístoffer 550192826b
highlight(matlab): Better UTF-8 handling. (#7532) 10 months ago
Ryan Fowler 828c7432e3
Implement the wa! command (#7577) 10 months ago
Alex Vinyals 1698992de6
Fix `:log-open` when `--log` is specified (#7573) 10 months ago
Alberto Romero 507dd50860
Add filename completer for shell prompt (#7569) 10 months ago
Pascal Kuthe 618620b369
use redraw handle for debouncing LSP messages (#7538) 10 months ago
Tom Taylor dc50263ed0
Fix incorrect gutter bail message (#7534) 10 months ago
Erin van der Veen 3fb430257e
Update Nickel grammar (#7551) 10 months ago
zer0-x 9ccca81305
book: Rename Arch Linux's repository from `community` to `extra` (#7543) 10 months ago
dependabot[bot] 57e538d07b
build(deps): bump gix from 0.47.0 to 0.48.0 (#7531)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] c091f9a37c
build(deps): bump serde from 1.0.164 to 1.0.166 (#7527)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.164 to 1.0.166.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.164...v1.0.166)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 9840422281
build(deps): bump bitflags from 2.3.2 to 2.3.3 (#7530)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] 83e59197ac
build(deps): bump indoc from 2.0.1 to 2.0.2 (#7529)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
dependabot[bot] d908a6ed19
build(deps): bump tokio from 1.28.2 to 1.29.1 (#7528)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
10 months ago
Álan Crístoffer 457b389395
highlight(matlab): Many bug fixes and improvements (#7511) 10 months ago
Álan Crístoffer a9849ebee4
highlight(matlab): Fix string's single-quote's color (#7493) 11 months ago
Chris Heyes aec1b997dd
Add .cppm file type to cpp language configuration (#7492) 11 months ago
Tshepang Mbambo 9546e0c0a7
docs: align content with parent paragraph (#7488) 11 months ago
Álan Crístoffer 78505e0149
Update tree-sitter-matlab (#7491) 11 months ago
Michael Davis e0bb032f0e
LSP: Forcefully shutdown uninitialized servers (#7449)
The LSP spec has this to say about initialize:

> Until the server has responded to the `initialize` request with an
> `InitializeResult`, the client must not send any additional requests
> or notifications to the server.

(https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)

The spec is not really explicit about how to handle this scenario.
Before a client sends the 'initialize' request we are allowed to send an
'exit' notification, but after 'initialize' we can't send any requests
(like shutdown) or notifications (like exit). So my intepretation is
that we should forcefully close the server in this state.

This matches the behavior of Neovim's built-in LSP client:
5ceb2238d3/runtime/lua/vim/lsp.lua (L1610-L1628)
11 months ago
gobraves b745fb2551
update OneDarker theme to use light-gray for inlay hints. (#7433) 11 months ago
Michael Davis 4fab60030f
LSP: Use negotiated position encoding for workspace edits (#7469)
Previously this was hard-coded to UTF-8 but we might have negotiated
another position encoding.
11 months ago
Michael Davis d3f8e0592b
LSP: Discard publishDiagnostic from uninitialized servers (#7467)
The spec explicitly disallows publishDiagnostic to be sent before
the initialize response:

> ... the server is not allowed to send any requests or notifications to
> the client until it has responded with an InitializeResult ...

(https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#initialize)

But if a non-compliant server sends this we currently panic because we
'.expect()' the server capabilities to be known to fetch the position
encoding. Instead of panicking we can discard the notification and log
the non-compliant behavior.
11 months ago
Pascal Kuthe 4a2337d828
correctly map unsorted positions (#7471)
* correctly map unsorted positions

* Fix typo

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
11 months ago
dependabot[bot] d8f9b901dd
build(deps): bump libc from 0.2.146 to 0.2.147 (#7463)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.146 to 0.2.147.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.146...0.2.147)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] b68b3608a6
build(deps): bump serde_json from 1.0.97 to 1.0.99 (#7462)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.97 to 1.0.99.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.97...v1.0.99)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] dcad5cddd1
build(deps): bump toml from 0.7.4 to 0.7.5 (#7461)
Bumps [toml](https://github.com/toml-rs/toml) from 0.7.4 to 0.7.5.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.4...toml-v0.7.5)

---
updated-dependencies:
- dependency-name: toml
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] ad04b9125d
build(deps): bump gix from 0.46.0 to 0.47.0 (#7460)
Bumps [gix](https://github.com/Byron/gitoxide) from 0.46.0 to 0.47.0.
- [Release notes](https://github.com/Byron/gitoxide/releases)
- [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Byron/gitoxide/compare/gix-v0.46.0...gix-v0.47.0)

---
updated-dependencies:
- dependency-name: gix
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
Michael Davis 636c91c76b
Mark buffers created from stdin as modified (#7431)
This resolves some confusing behavior where a scratch document created
by piping into hx is discarded when navigating away from that document.

We discard any scratch documents that are not modified and the original
`Editor::new_file_from_stdin` would create unmodified documents. We
refactor this function to create an empty document first and then to
apply the text from stdin as a change.
11 months ago
Blaž Hrastnik 8d39a81aa8 fix: Regression from d491e234f4 11 months ago
Pascal Kuthe b33516fb16 move normalize fastpath into normalize function 11 months ago
Pascal Kuthe d491e234f4 map positions through changes in O(N) 11 months ago
Branch Vincent 6c6923c39e
feat(toml): highlight table headers (#7441) 11 months ago
Álan Crístoffer 08c0a23b79
highlight(matlab): Bumps tree-sitter-matlab commit. (#7442) 11 months ago
Scott Driggers 93ac706844
Allow any indent size from 1 to 16 (#7429) 11 months ago
Álan Crístoffer 2c5288dafb
Replace MATLAB grammar (#7388) 11 months ago
eh 842518ccb7
Theme: port of Zed's OneDark and OneLight themes (#7250) 11 months ago
Pascal Kuthe a0359f7f22 make TS matching fallback to plaintext 11 months ago
Pascal Kuthe 5dba649d81 Avoid false positives in non-fuzzy bracket match 11 months ago
Pascal Kuthe 37058e0401 match pairs which don't form a standalone TS node 11 months ago
A-Walrus eb81cf3c01
Fix tree sitter chunking (#7417)
Call as bytes before slicing, that way you can take bytes that aren't
aligned to chars. Should technically also be slightly faster since you
don't have to check alignment...
11 months ago
dependabot[bot] 18160a667b
build(deps): bump bitflags from 2.3.1 to 2.3.2 (#7394)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] 00e819cfd4
build(deps): bump serde_json from 1.0.96 to 1.0.97 (#7393)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.96 to 1.0.97.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.96...v1.0.97)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] 293a55ad2b
build(deps): bump cachix/install-nix-action from 21 to 22 (#7392)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
Kirawi b0cdc2d6e1
remove mention of Wasm from `vision.md` (#7395) 11 months ago
LeoniePhiline 48ad9ae1bb
highlight(sql): Update tree-sitter-sql to `98a7fc9` (#7387)
* highlight(sql): Update tree-sitter-sql to 92018a3

* highlight(sql): Update tree-sitter-sql to 98a7fc9
11 months ago
Michael Davis 9690e0b733
Fix Component implementations for Picker (#7382) 11 months ago
Pascal Kuthe 06d63d6ac1
Merge pull request #7264 from the-mikedavis/merge-picker-and-filepicker
Merge FilePicker into Picker
11 months ago
tomleb 29638babea
Allow ANSI colors in themes (#5119) 11 months ago
Gokul Soumya f18acadbd0
Completely remove old Picker and rename FilePicker to Picker 11 months ago
Michael Davis 545acfda88
Make file preview callback optional
When Picker and FilePicker are merged, not all Pickers will be able to
show a preview.

Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
11 months ago
Gokul Soumya fc111213b5
Move FilePicker struct def closer to impl block 11 months ago
Gokul Soumya 15cc09fc81
Render the preview in FilePicker 11 months ago
Gokul Soumya 34c8f9ab73
Move Picker::render into FilePicker::render 11 months ago
Gokul Soumya 49fbf8df53
Move Component methods except render() to FilePicker 11 months ago
Gokul Soumya 1e66e9198c
Move handle_event methods from Picker to FilePicker 11 months ago
Gokul Soumya 8516f43837
Move navigation methods from Picker to FilePicker 11 months ago
Gokul Soumya 7a058c7361
Move scoring functions from Picker to FilePicker 11 months ago
Gokul Soumya 104036bd7f
Copy struct fields and new() from Picker to FilePicker 11 months ago
Gokul Soumya 40916dff63
Move FilePicker::render from Component impl to normal impl
Merges the code for the Picker and FilePicker into a single Picker that
can show a file preview if a preview callback is provided. This change
was mainly made to facilitate refactoring out a simple skeleton of a
picker that does not do any filtering to be reused in a normal Picker
and a DynamicPicker (see #5714; in particular [mikes-comment] and
[gokuls-comment]).

The crux of the issue is that a picker maintains a list of predefined
options (eg. list of files in the directory) and (re-)filters them every
time the picker prompt changes, while a dynamic picker (eg. interactive
global search, #4687) recalculates the full list of options on every
prompt change. Using a filtering picker to drive a dynamic picker hence
does duplicate work of filtering thousands of matches for no reason. It
could also cause problems like interfering with the regex pattern in the
global search.

I tried to directly extract a PickerBase to be reused in Picker and
FilePicker and DynamicPicker, but the problem is that DynamicPicker is
actually a DynamicFilePicker (i.e. it can preview file contents) which
means we would need PickerBase, Picker, FilePicker, DynamicPicker and
DynamicFilePicker and then another way of sharing the previewing code
between a FilePicker and a DynamicFilePicker. By merging Picker and
FilePicker into Picker, we only need PickerBase, Picker and
DynamicPicker.

[gokuls-comment]: https://github.com/helix-editor/helix/issues/5714#issuecomment-1410949578
[mikes-comment]: https://github.com/helix-editor/helix/issues/5714#issuecomment-1407451963
11 months ago
Alex d5af6031f6
Use default line ending in open command (#7357) 11 months ago
Alexander Brevig b4fe31cad6
Add forth-lsp and update tree-sitter-forth (#7334)
* feat: add forth lsp and update tree sitter

* fix: update highlights
11 months ago
spectre256 d8b7232a47
Add yank_joined command (#7195)
Resolves issue #6888 by adding a command to join all selections and yank
them to the specified register. The typed command takes an argument as
the separator to use when joining the selections.
11 months ago
Alex 3fb9fafb2a
Add config for default line ending (#5621) 11 months ago
Ivan Svoboda df094909d1
Webc language support (#7290)
* feat: add webc language support

* feat: add to lang-support book webc
11 months ago
Thomas Aarholt cbe2308210
Explain how to enter Select mode in keymap (#7333)
* Explain how to enter Select mode

Also added an equivalent explanation that Insert mode is the default, with how to return to it.

* Fix glarious typo
11 months ago
A-Walrus 4d8c9a394e
Preview scratch buffers in jumplist picker (#7331) 11 months ago
A-Walrus a4a86120e8
Fix next/prev tree-sitter inconsistency (#7332)
* Fix next/prev tree-sitter inconsistency

Before there where different results going to next or previous due to
sorting not dealing with multiple captures that start/end at the same
pos. I chose to prefer longer matches.

* Revert unnecessary change
11 months ago
Jorge Santiago d249639114
Add merionette theme (#7186) 11 months ago
Zisulin Morbrot fbd22afe29
Add rl and rla aliasses for reload and reload-all commands (#7158) 11 months ago
Robert 015c079acc
Add reverse_selection_contents (#7329) 11 months ago
kazimir malevich 37fcd160db
Clarify the runtime directory location in installation docs (#6624)
* runtime config made clearer

* following Unix FHS

* we probably want to install Helix as a regular user without sudo access

* suggestions adopted from @the-mikedavis

* attempted to synthesise comments given

* capitalisation of second header

* required changes hopefully made

* we should have a match now

* Linux windows dir match
11 months ago
dependabot[bot] 0e42e4e193
build(deps): bump libc from 0.2.145 to 0.2.146 (#7327)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] 66ed5bd013
build(deps): bump gix from 0.44.1 to 0.46.0 (#7325)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] 0d998c99d4
build(deps): bump tempfile from 3.5.0 to 3.6.0 (#7326)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] 3c23ea5d15
build(deps): bump serde from 1.0.163 to 1.0.164 (#7328)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] a6fd32eef4
build(deps): bump log from 0.4.18 to 0.4.19 (#7324)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
Philipp Mildenberger 2a11fb485d
Fix underflow when repeating a completion that has a negative shift position (#7322) 11 months ago
Michael Davis 25ad534d64
Check for 'git' before fetching/building grammars (#7320)
Previously the error message for this potential failure-case was
confusing: "no such file or directory". `hx -g fetch`, `hx -g build` and
the helix-term builder should bail early if the git binary is not
available.
11 months ago
Selwyn 18e07addfd
Use Vue language server based on Volar (#7312) 11 months ago
0xHiro / ヒロ 8c6eb88c6a
fix: add file argument in help text (#7307) 11 months ago
lefp a197ce869a
Add Fortran comment injections (#7305) 11 months ago
Vitalii Solodilov a981a2525d
Support core mode for delve debugger (#7300) 11 months ago
Luca Schlecker dbd248fdfa add config option for instant completion entry preview (defaulting to true).
Signed-off-by: Luca Schlecker <luca.schlecker@hotmail.com>
11 months ago
spectre256 00b152facd
Add register statusline element (#7222) 11 months ago
Alex 993c68ad6f
Auto indent on `insert_at_line_start` (#5837) 11 months ago
Tshepang Mbambo e2a1678436
Fix book configuration for edit template (#7278) 11 months ago
Michael Davis b0129b552d
Fix style overwriting in table rows with multiple cells (#7281) 11 months ago
Tshepang Mbambo 27891cdc8d
misc doc fixes/improvements (#7282) 11 months ago
Michael Davis d4427125eb
Bail if no language servers support workspace symbols (#7286) 11 months ago
Dimitri Sabadie 31b8b728a2
Fix C++ queries and update tree-sitter-cpp (#7285) 11 months ago
broke ef5bcd5060
theme: added gruvbox dark soft variant (#7139) 11 months ago
Christoph Sax d324feb072
Add support for language t32 (#7140)
Co-authored-by: Christoph Sax <christoph.sax@mailbox.org>
11 months ago
vwkd 352d1574a6
add move_prev_long_word_end and extend_prev_long_word_end (#6905) 11 months ago
Tshepang Mbambo 2f9b63999f
Break long sentence in book configuration footnote (#7279) 11 months ago
Michael Davis b3949979ae Propagate the count and register to command palette commands
Previously a count or register selection would be lost while opening
the command palette. This change allows using a register selection or
count in any command chosen from the command palette.
11 months ago
Michael Davis 0e083497a5 Persist register selection in pending keymaps
Previously the register selection (via `"`) would be lost in the middle
of any key sequence longer than one key. For example, `<space>f` would
clear the register selection after the `<space>` making it inaccessible
for the `file_picker` command.

This behavior does not currently have any effect in the default keymap
but might affect custom keymaps. This change aligns the behavior of the
register with count. Making this change allows propagating the register
to the `command_palette` (see the child commit) or other pickers should
we decide to use registers in those in the future. (Interactive global
search for example.)
11 months ago
Ilya Sovtsov 77e9a22aff
Add check for a non-zero value for tab width (#7178) 11 months ago
Alex Vinyals 204bac1706
commands(toggle): use pattern matching on the Value enum (#7240) 11 months ago
blt__ ba691f4fb0
Fix verilog grammar source repo and revision (#7262) 11 months ago
Jens Getreu 3e927ac578
Autumn theme: style "soft-wrap indicator" + maintenance (#7229)
Co-authored-by: Jens Getreu <getreu@getreu@web.de>
11 months ago
gibbz00 9926c2d292 Remove Keymap(KeyTrie) and simply use KeyTrie. 11 months ago
gibbz00 b8563685ec Move `keymap.reverse_keymap()` to `Keytrie`:
The plan is let `Keymaps` simply store `KeyTrie`s, as the `Keymap(Keytrie)` wrapping serves little to no purpose.
11 months ago
gibbz00 eda4c79f2f Remove pub keymap.name();
`keymap.name` is only used internally.
11 months ago
gibbz00 3d0bc72099 Place `Info::from_keymap()` contents in `keymap.infobox()`:
This makes it easier later control the order in which the key events
are presented.
11 months ago
gibbz00 19326d23d1 Keymap infobox: Idiomatic body tuple.
Does not change any behavior other than making the tuple slightly
more idiomatic.  Keymap infobox shows key events, then the respective
description. This commit makes sure that order is used from the get go,
rather than flipping it midway.
11 months ago
gibbz00 3a0892f793 Exclude config no_op bindings in command palette. 11 months ago
gibbz00 39773e48d3 Remove superfluous command description pruning for keymap infobox:
Exist under the wrong (possibly just outdated) assumption that command
descriptions are written with their `KeyTrie` name prefixed
11 months ago
gibbz00 d20c1632a7 `helix_term::keymap`: Remove one-liner solely used for a test. 11 months ago
gibbz00 f7df53c948 Make `Keymap` a tuple struct. 11 months ago
gibbz00 daea97a89f keymap: Rename KeyTrie::Leaf -> KeyTrie::MapppableCommand
The variant Sequence is technically also a leaf.
11 months ago
gibbz00 a56af221d7 keymap: Derive `Default` for KeyTrieNode 11 months ago
Alexander Brevig 78e8695420
Add support for Forth (#7256) 11 months ago
Tobias Clasen 71688a387b
Recognize 'make' file-type as Makefile (#7212) 11 months ago
dependabot[bot] 9f5b965627
build(deps): bump regex from 1.8.3 to 1.8.4 (#7249)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] 6deb0e4ef7
build(deps): bump once_cell from 1.17.2 to 1.18.0 (#7248)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] a56222cd0e
build(deps): bump url from 2.3.1 to 2.4.0 (#7247)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] a8c99fb24c
build(deps): bump hashbrown from 0.13.2 to 0.14.0 (#7246)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] 1d114ecb6e
build(deps): bump chrono from 0.4.25 to 0.4.26 (#7245)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
dependabot[bot] cd01dc886a
build(deps): bump libc from 0.2.144 to 0.2.145 (#7244)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
11 months ago
Alex Vinyals a2b8cfdb8c
feat(core): add plaintext matching fallback to tree-sitter matching (#4288) 11 months ago
Michael Davis 428d33ab50
Exit gracefully on termination signals (#7236) 11 months ago
Alex Vinyals d5707a4696
feat(commands): allows cycling option values at runtime (#4411) 11 months ago
Ivan Tkachuk 2022e6175b
Add blueprint language (#7213)
* Add blueprint tree-sitter support

* Add blueprint lsp support

* Run cargo xtask docgen
11 months ago
Rich Seymour 751da01303
Update install.md instructions regarding symlinks (#7231)
* Update install.md

Fixes `ln` command line bug that could hit users moving from packaged to source builds.

* Remove extra 'how to' command example
11 months ago
avaunit02 232d9f96a0
Fix textobject keybindings in usage docs (#7197) 11 months ago
Pascal Kuthe de0ef8af15
fix UB in diff gutter (#7227) 11 months ago
Zisulin Morbrot d511122279
Add description for 'ui.text.focus' in theme docs (#7177) 12 months ago
zetashift 92380540b8
chore: update scala tree-sitter grammar (#7147)
* chore: update scala tree-sitter grammar

* Add locals.scm for Scala
12 months ago
dependabot[bot] a726799a4e
build(deps): bump log from 0.4.17 to 0.4.18 (#7175)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
dependabot[bot] 8bd841136a
build(deps): bump once_cell from 1.17.1 to 1.17.2 (#7174)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
dependabot[bot] 825e7c4976
build(deps): bump tokio from 1.28.1 to 1.28.2 (#7173)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
dependabot[bot] f694c24ac8
build(deps): bump regex from 1.8.2 to 1.8.3 (#7172)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
dependabot[bot] d62fb8c8d7
build(deps): bump chrono from 0.4.24 to 0.4.25 (#7171)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
dependabot[bot] a367a713ef
build(deps): bump cachix/install-nix-action from 20 to 21 (#7170)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
Ethan Wilkes 3334e7e4b2
fixed repeat_last_motion extends selection (#7159) 12 months ago
yvt 3a8592abdb
feat(languages): recognise `.gir` as XML (#7152) 12 months ago
Jimmy Zelinskie b107814e39
languages: add .zimrc to bash filetypes (#7146) 12 months ago
Gammut 86ef389c0d
Highlight query improvements for java and typescript (#7145)
* Add missing queries to java highlights

* Fix generic type bracket highlights in typescript
12 months ago
Martin Weinelt 61a89956e3
book: Mention Helix package in nixpkgs (#7064) 12 months ago
Ravi Shekhar Jethani 6e647e83ed
Update spacebones_light.toml (#7125)
Better contrast between type variable and its members
12 months ago
Casper Rogild Storm 1dd1735909
Correct selection color (#7138) 12 months ago
Ikko Eltociear Ashimine 349e2b5f0e
Fix typo in syntax.rs (#7114)
seperator -> separator
12 months ago
William Etheredge a2e11af608
Support hidden justfiles (#7088) 12 months ago
Brandon Sprague 16375cd94a
Treat 'Vagrantfile' files as Ruby code (#7112)
`Vagrantfile`s are configuration files for the [vagrant](https://www.vagrantup.com/) tool, and they are written in Ruby. Even as vagrant [is ported to Go](https://www.hashicorp.com/blog/toward-vagrant-3-0), `Vagrantfile`s [will be written in Ruby](https://developer.hashicorp.com/vagrant/docs/experimental/vagrant_go)

When a Vangrantfile is created, it starts with these lines:

```
```

But since Helix (quite reasonably) doesn't change editor behavior based on file contents, adding this to `languages.toml` seems like a reasonable way to get this functionality.
12 months ago
Viktor Kleen 0c21c227e1
Update languages.toml for Nickel (#7059) 12 months ago
Blaž Hrastnik 0266ec6ba3
Update language support doc 12 months ago
Blaž Hrastnik 591629ec11
fix compilation 12 months ago
Blaž Hrastnik c6d1430243
Drop old cairo grammar, alias to rust for now 12 months ago
Matthew Toohey 207829eefe
Fix off-by-one in select symbol references (#7132) 12 months ago
Poliorcetics 8e2660b5cc
Update diagnostics correctly on LSP exit (#7111)
* chore: avoid format! call with argument when useless

* feat: also clear diagnostics for unopened documents when exiting an LSP

* feat: we already worked on `self.editor.diagnostics` no need to redo the checks
12 months ago
dependabot[bot] 6043c3c3db
build(deps): bump bitflags from 2.2.1 to 2.3.1 (#7117)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
dependabot[bot] a66e287654
build(deps): bump toml from 0.7.3 to 0.7.4 (#7120)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
dependabot[bot] 16275db817
build(deps): bump regex from 1.8.1 to 1.8.2 (#7119)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
dependabot[bot] d765132426
build(deps): bump pulldown-cmark from 0.9.2 to 0.9.3 (#7118)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
12 months ago
yvt 16e0340b4a feat(languages): recognise `Cargo.lock` as TOML 12 months ago
Chris d3cca0f357
Recognise XAML files as XML (#7083) 12 months ago
LeoniePhiline 41f52d74fe
fix(docs): Remove `config` from language configuration docs (#7082) 12 months ago
Szabin 70e4cdbd8e
Add command to merge non-consecutive ranges (#7053)
* Add command for merging non-consecutive ranges

* Add `merge_selections` command to book

* Simplify `merge_ranges`

Heeded the advice of @the-mikedavis to stop iterating over all ranges and simply merge the first and the last range, as the invariants of `Selection` guarantee that the list of ranges is always sorted and never empty.

* Clarify doc comment of `merge_ranges`
12 months ago
Charles Hall e4a9bec562
fix off-by-one git-commit rulers (#7072)
Characters should maximally reside *inside* the ruler, not on top of it.
12 months ago
Yt ff442eecd0
nix: add overlay for convenient package usage (#7078) 12 months ago
Blaž Hrastnik 53f47bc477
Merge pull request #2507 from Philipp-M/multiple-language-servers
Add support for multiple language servers per language
12 months ago
Philipp Mildenberger 2a512f7c48 Rebase cleanup/fixes and use lsp::CompletionItem in item_to_transaction directly 12 months ago
Philipp Mildenberger f8fa0d8a10 Clarify language-servers documentation for mergeable LSP features (`diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols`) 12 months ago
Philipp Mildenberger b6d0e26814 Sort language servers table in languages.toml and rename language server 'R' to 'r' 12 months ago
Philipp Mildenberger f45bbf165e Apply all review suggestions (doc_id -> id, error message, unnecessary if)
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
12 months ago
Philipp Mildenberger 656ee24966 Simplify gutter diagnostics rendering by using partition_point instead of binary search
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
12 months ago
Philipp Mildenberger 2b746ea6fa Some minor clarity/cosmetic improvements
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
12 months ago
Philipp Mildenberger 39b9a4bba2 Add function `Editor::language_server_by_id` and refactor/simplify related code, also don't 'crash' in completion menu if language_server somehow disappeared 12 months ago
Philipp Mildenberger 521cdec5a1 Remove TODO comment in helix_lsp::Registry::restart and add doc-comment on top of function instead 12 months ago
Philipp Mildenberger dcb07673f8 Reorder id generation for Clients to stay close to the old behavior 12 months ago
Philipp Mildenberger 56748509bd Reduce boilerplate by 'use lsp::*' in Client::supports_feature, and remove TODO comment 12 months ago
Philipp Mildenberger 3e4bac1d96 Fix lsp_restart across multiple different document scopes (language servers weren't restarted, if not of the same scope id), and fix some smaller rebase issues 12 months ago
Philipp Mildenberger 2a21b939c4 Fix crash with filtered diagnostics in gutter (e.g. when diagnostics aren't visible) 12 months ago
Philipp Mildenberger 93fd79a949 Remove offset_encoding in CodeActionOrCommandItem, as it can be retrieved on demand 12 months ago
Philipp Mildenberger 073000e54d Maintain language servers TOML array order in `doc.language_servers` 12 months ago
Philipp Mildenberger ff26208427 Filter language servers also by capabilities in `doc.language_servers_with_feature`
* Add `helix_lsp::client::Client::supports_feature(&self, LanguageServerFeature)`
* Extend `doc.language_servers_with_feature` to use this method as filter as well
* Add macro `language_server_with_feature!` to reduce boilerplate for non-mergeable language server requests (like goto-definition)
* Refactored most of the `find_map` code to use the either the macro or filter directly via `doc.language_servers_with_feature`
12 months ago
Philipp Mildenberger 9d089c27c7 Fix docgen again 12 months ago
Philipp Mildenberger 451fe528bb Filter out already seen language servers in requests that can be sent to multiple language servers (code-action, completion, symbol pickers) 12 months ago
Philipp Mildenberger 8ee599942a Optimize gutter diagnostics and simplify shown_diagnostics 12 months ago
Philipp Mildenberger 8ab6d7be5e Use let else instead of variable and fix some error messages
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
12 months ago
Philipp Mildenberger 1d5d5dab47 Remove offset_encoding in CompletionItem 12 months ago
Philipp Mildenberger 2eeac10755 Refactor doc language servers to a HashMap, and the config to use a Vec to retain order 12 months ago
Philipp Mildenberger b1199c552b Remove symbol picker is_empty check 12 months ago
Philipp Mildenberger 58c913ce45 Simplify 'lsp_stop' command 12 months ago
Philipp Mildenberger 7d20740b5b Fix docgen and lsp-stop documentation 12 months ago
Philipp Mildenberger 60a6af1fea Remove boilerplate in the goto methods by generically composing functions 12 months ago
Philipp Mildenberger d963050621 Format/fix language docs a bit 12 months ago
Philipp Mildenberger 9639f42766 Refactor doc.shown_diagnostics to avoid an extra HashSet
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
12 months ago
Philipp Mildenberger 1122928c2a Add method doc.supports_language_server for better readability 12 months ago
Philipp Mildenberger ec2f9091a0 Simplify Display implementation for LanguageServerFeature 12 months ago
Philipp Mildenberger 76b5cab524 Refactored doc.language_servers and doc.language_servers_with_feature to return an iterator and refactor LanguageServerFeature handling to a HashMap (language server name maps to features)
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
12 months ago
Philipp Mildenberger 0637691eb1 Use DoubleEndedIterator instead of collect to Vec for reversing
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
12 months ago
Philipp Mildenberger dd2f74794a Fix error messages when no language server is available
Co-authored-by: Skyler Hawthorne <skyler@dead10ck.com>
12 months ago
Philipp Mildenberger 19f88fc577 Simplify Display implementation of LanguageServerFeature
Co-authored-by: Ivan Tham <pickfire@riseup.net>
12 months ago
Philipp Mildenberger f9b08656f4 Fix sorting issues of the editor wide diagnostics and apply diagnostics related review suggestions
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
12 months ago
Philipp Mildenberger 4da6d8ccc7 str instead of String 12 months ago
Philipp Mildenberger b6c60beb2d Remove unnecessary completion support check (likely an artifact) 12 months ago
Philipp Mildenberger 44b2b40190 Fix issue with ltex-ls, filtering params is not what we want here 12 months ago
Philipp Mildenberger 05583f8cc9 Fix hardcoded offset_encoding 12 months ago
Philipp Mildenberger 74e21e1b25 Fix some lints/docgen hints 12 months ago
Philipp Mildenberger 7d4f7eb4bd Fix 'WorkspaceConfiguration' request with empty configuration section strings 12 months ago
Philipp Mildenberger 71551d395b Adds support for multiple language servers per language.
Language Servers are now configured in a separate table in `languages.toml`:

```toml
[langauge-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }

[language-server.efm-lsp-prettier]
command = "efm-langserver"

[language-server.efm-lsp-prettier.config]
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
```

The language server for a language is configured like this (`typescript-language-server` is configured by default):

```toml
[[language]]
name = "typescript"
language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]
```

or equivalent:

```toml
[[language]]
name = "typescript"
language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]
```

Each requested LSP feature is priorized in the order of the `language-servers` array.
For example the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).

If no `except-features` or `only-features` is given all features for the language server are enabled, as long as the language server supports these. If it doesn't the next language server which supports the feature is tried.

The list of supported features are:

- `format`
- `goto-definition`
- `goto-declaration`
- `goto-type-definition`
- `goto-reference`
- `goto-implementation`
- `signature-help`
- `hover`
- `document-highlight`
- `completion`
- `code-action`
- `workspace-command`
- `document-symbols`
- `workspace-symbols`
- `diagnostics`
- `rename-symbol`
- `inlay-hints`

Another side-effect/difference that comes with this PR, is that only one language server instance is started if different languages use the same language server.
12 months ago
Michael Davis 7f5940be80 Add 23.05 to the AppData 12 months ago
Michael Davis 59f8f5e6d4 Add changelog notes for the 23.05 release 12 months ago
Michael Davis 04fbf30488 Bump the version to 23.05 12 months ago
Ivan Gulakov 2cccb3f09c
Fix completion on paths containing spaces (#6779)
There was an issue with autocompletion of a path with a space in it.

Before:

:o test\ dir -> <TAB> -> test\ dirfile1

After:

:o test\ dir -> <TAB> -> test\ dir\file1
12 months ago
Pascal Kuthe b0705337be automatically disable TS when parsing takes longer than 500ms 12 months ago
Pascal Kuthe 2f2306475c async picker syntax highlighting 12 months ago
Pascal Kuthe c6f169b1f8 cleanup integration tests 12 months ago
Pascal Kuthe 25d4ebe30d don't move cursor while forward deleting in append mode
Currently, when forward deleting (`delete_char_forward` bound to `del`,
`delete_word_forward`, `kill_to_line_end`) the cursor is moved to the
left in append mode (or generally when the cursor is at the end of the
selection). For example in a document `|abc|def`  (|indicates selection)
if enter append mode the cursor is moved to `c` and the selection
becomes: `|abcd|ef`. When deleting forward (`del`) `d` is deleted. The
expectation would be that the selection doesn't shrink so that `del`
again deletes `e` and then `f`. This would look as follows:

`|abcd|ef`
`|abce|f`
`|abcf|`
`|abc |`

This is inline with how other editors like kakoune work.
However, helix currently moves the selection backwards leading to the
following behavior:

`|abcd|ef`
`|abc|ef`
`|ab|ef`
`ef`

This means that `delete_char_forward` essentially acts like
`delete_char_backward` after deleting the first character in append
mode.

To fix the problem the cursor must be moved to the right while deleting
forward (first fix in this commit). Furthermore, when the EOF char is
reached a newline char must be inserted (just like when entering
appendmode) to prevent the cursor from moving to the right
12 months ago
Pascal Kuthe 2c3ccc3e8b cleanup delete_by_selection_insert_mode function 12 months ago
Pascal Kuthe f8225ed921 fix panic when deleting overlapping ranges
Some deletion operations (especially those that use indentation)
can generate overlapping deletion ranges when using multiple cursors.
To fix that problem a new `Transaction::delete` and
`Transaction:delete_by_selection` function were added. These functions
merge overlapping deletion ranges instead of generating an invalid
transaction. This merging of changes is only possible for deletions
and not for other changes and therefore require its own function.

The function has been used in all commands that currently delete
text by using `Transaction::change_by_selection`.
12 months ago
Pascal Kuthe 6842fd4c36 clarify comments about completion savepoints
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
12 months ago
Pascal Kuthe 28b730381c only resolve completion items once 12 months ago
Pascal Kuthe bcb8c3d34d deduplicate savepoints 12 months ago
Pascal Kuthe 9c558fc470 ensure correct trigger/start completion offset
When re requesting a completion that already has a selected item we
reuse that selections savepoint. However, the selection has likely
changed since that savepoint which requires us to use the selection
from that savepoint
12 months ago
Pascal Kuthe 30ff7f8db2 resolve completions before applying transactions 12 months ago
Pascal Kuthe 5406e9f629 correctly handle completion rerequest 12 months ago
Jan Scheer 91da0dc172
Update nightfox theme (#7061)
* theme: nightfox - fix subselection highlighting

This fixes an issue with subselect highlighting on the same line as
reported here: https://github.com/helix-editor/helix/discussions/5158

* theme: nightfox - update bufferline colors

This uses `ui.bufferline` to make it easier to distinguish
between (in-)active tabs/buffers.
12 months ago
gibbz00 6f135e58a3
languages.toml: recognize `gml` files. (#7055) 1 year ago
gibbz00 e9efcebdb0
languages.toml: recognize `geojson` files. (#7054) 1 year ago
dependabot[bot] 4b9dba8217
build(deps): bump serde from 1.0.162 to 1.0.163 (#7056)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.162 to 1.0.163.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.162...v1.0.163)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] b50032ee85
build(deps): bump tokio from 1.28.0 to 1.28.1 (#7057)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.28.0 to 1.28.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.28.0...tokio-1.28.1)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
A-Walrus d5fe08ddb8
Replace DAP vars popup, instead of adding new (#7034) 1 year ago
lefp 06d7f5d100
Add comment injections for Odin (#7027) 1 year ago
Vitalii Solodilov 5938e2c0dc
fix: update upstream tree-sitter-dockerfile (#6895)
* fix: update upstream tree-sitter-dockerfile

Fixes: #6797

* fix: review

* fix: review
1 year ago
ZJPzjp 3b8c15618f
Fix warnings from clippy (#7013)
* Fix warnings from clippy

* revert MAIN_SEPARATOR_STR
1 year ago
Blaž Hrastnik 1e5997dc98
nix: Update flake dependencies 1 year ago
John Kelly 87da58578f
Add better error message for file save failure due to parent not existing (#7014)
Fixes #5229
1 year ago
Kitsu 92c328c088
Add wbc and wbc! commands (#6947) 1 year ago
Ollie Charles 8424f387b5
Remove `tree-sitter-cabal` (#6996) 1 year ago
David Else 69332ae3b2
Add Flathub as third party repository (#6994) 1 year ago
dependabot[bot] 7089bc20f0
build(deps): bump serde from 1.0.160 to 1.0.162 (#7002)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.160 to 1.0.162.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.160...1.0.162)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 9cca80bd53
build(deps): bump libc from 0.2.142 to 0.2.144 (#7000)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.142 to 0.2.144.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.142...0.2.144)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 301d45b34b
build(deps): bump termini from 0.1.4 to 1.0.0 (#7001)
Bumps [termini](https://github.com/pascalkuthe/termini) from 0.1.4 to 1.0.0.
- [Release notes](https://github.com/pascalkuthe/termini/releases)
- [Commits](https://github.com/pascalkuthe/termini/compare/v0.1.4...v1.0.0)

---
updated-dependencies:
- dependency-name: termini
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
Chirikumbrah 3fb0562e7b
Update dracula menu colors (#6987) 1 year ago
Andrius Pukšta 3a9e77934b
Treat .sty and .cls files as latex (#6986) 1 year ago
Dave Powers 5ae30f1993
Fix keymap select / extend mode anchor link (#6974) 1 year ago
Diego Pontoriero fc1e9a6ff9
languages: add build.gradle.kts to java and scala roots (#6970)
Gradle scripts written in kotlin use a .kts extension:

https://docs.gradle.org/current/userguide/kotlin_dsl.html#sec:scripts
1 year ago
Gagan Janjua c6228825fd
Add Amberwood theme (#6924) 1 year ago
taupiqueur c49faf5681
Add language server command for Crystal (#6948) 1 year ago
dependabot[bot] d1a4bd876b
build(deps): bump tokio from 1.27.0 to 1.28.0 (#6935)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.27.0 to 1.28.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.27.0...tokio-1.28.0)

---
updated-dependencies:
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 7ea5965bd5
build(deps): bump tokio-stream from 0.1.12 to 0.1.14 (#6934)
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.12 to 0.1.14.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Changelog](https://github.com/tokio-rs/tokio/blob/tokio-0.1.14/CHANGELOG.md)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.12...tokio-0.1.14)

---
updated-dependencies:
- dependency-name: tokio-stream
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] d3e535b697
build(deps): bump gix from 0.44.0 to 0.44.1 (#6933)
Bumps [gix](https://github.com/Byron/gitoxide) from 0.44.0 to 0.44.1.
- [Release notes](https://github.com/Byron/gitoxide/releases)
- [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Byron/gitoxide/compare/gix-v0.44.0...gix-v0.44.1)

---
updated-dependencies:
- dependency-name: gix
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] d9a7bd3bc1
build(deps): bump anyhow from 1.0.70 to 1.0.71 (#6932)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.70 to 1.0.71.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.70...1.0.71)

---
updated-dependencies:
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 1b3923374a
build(deps): bump etcetera from 0.7.1 to 0.8.0 (#6931)
Bumps [etcetera](https://github.com/lunacookies/etcetera) from 0.7.1 to 0.8.0.
- [Release notes](https://github.com/lunacookies/etcetera/releases)
- [Commits](https://github.com/lunacookies/etcetera/compare/v0.7.1...v0.8.0)

---
updated-dependencies:
- dependency-name: etcetera
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
Chirikumbrah 2701781db7
Dracula theme: change variable and menu colors (#6855) 1 year ago
Scott Stevenson 015623720c
Recognise Jupyter notebooks as JSON documents (#6927) 1 year ago
Scott Stevenson d7878238c1
Recognise poetry.lock as TOML (#6928) 1 year ago
Pascal Kuthe 75f62bc86f fix didChange notifaction offset encoding 1 year ago
Alexis-Lapierre b0b3f45b80
Conserve BOM and properly support UTF16 (#6497) 1 year ago
Rafael Madriz efd09b6c7c
tutor: Delete space between shorthand (#6920) 1 year ago
Rafael Madriz 5dcc891996
tutor: Trim trailing white space (#6919) 1 year ago
sscheele f52e81b1f3
Update docs for `move_visual_line_*` (#6918) 1 year ago
Pascal Kuthe 77da0ae8ce
downgrade gix log level to info (#6915) 1 year ago
0rphee f2ccc03332 [Theme - noctis] Change color for whitespace rendering and indent guides 1 year ago
0rphee 96720e7b84 [Theme - noctis] Refine multiple cursor highlighting
Improve clarity when using multiple cursors
1 year ago
Evgeniy Tatarkin 6a1bb81f10
Sort the buffer picker by most recent access (#2980) 1 year ago
Timothy DeHerrera 9c6c63a2be
inject language based on file extension & shebang (#3970)
* inject language based on file extension

Nodes can now be captured with "injection.filename". If this capture
contains a valid file extension known to Helix, then the content will
be highlighted as that language.

* inject language by shebang

Nodes can now be captured with "injection.shebang". If this capture
contains a valid shebang line known to Helix, then the content will
be highlighted as the language the shebang calls for.

* add documentation for language injection

* nix: fix highlights

The `@` is now highlighted properly on either side of the function arg.

Also, extending the phases with `buildPhase = prev.buildPhase + ''''`
is now highlighted properly.

Fix highlighting of `''$` style escapes (requires tree-sitter-nix bump)

Fix `inherit` highlighting.

* simplify injection_for_match

Split out injection pair logic into its own method to make the overall
flow easier to follow.

Also transform the top-level function into a method on a
HighlightConfiguration.

* markdown: add shebang injection query
1 year ago
jorge 204d1eba4b
feat(commands): add clear-register typable command (#5695)
Co-authored-by: Jorge <chorcheus@tutanota.com>
1 year ago
ap29600 9cdc6b2e8a
Change Odin grammar to `ap29600/tree-sitter-odin` (#6766)
* Change Odin grammar to `ap29600/tree-sitter-odin`

The previously adopted grammar, `MineBill/tree-sitter-odin`, is unmaintained and mentions my repository as an alternative source.

* update queries

* docgen

* fix queries

* Update runtime/queries/odin/highlights.scm

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

* remove `ERROR` query for `odin`

* track the latest rev in `ap29600/tree-sitter-odin`

* runtime/queries/odin/highlights.scm: update rune highlight class

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
1 year ago
Vitalii Solodilov 2836ea2ac4
feat: add a config option to exclude declaration from LSP references (#6886)
* feat: added the config option to exclude declaration from reference query

Fixes: #5344

* fix: review

* fix: review
1 year ago
Sebastian Thiel 222be0f1e7
upgrade gitoxide to v0.44 and change repository discovery to worktree-only (#6867) (#6890) 1 year ago
Cole Helbling 6dd5054da4
Highlight sqlx's `query_scalar{,_unchecked}` macros as SQL (#6793)
* Highlight sqlx's `query_scalar{,_unchecked}` macros as SQL

* Update injections.scm

* fixup copy-pasta
1 year ago
Pascal Kuthe 8f1671eaaa
fix panic in inlay hint computation when view anchor is out of bounds (#6883) 1 year ago
Wesley 903bdaae87
Set PerlNavigator as the language server for Perl (#6860) 1 year ago
Daniel Martí 488707c7c8
add go.work and remove Gopkg.toml as Go roots (#6884)
Gopkg.toml was used by dep, Go's original dependency management tool.
It was an experiment that culminated in official and built-in support
for Go modules in mid 2018, and dep was deprecated and archived
in mid 2020 per https://github.com/golang/go/issues/38158.

Now, in 2023, Gopkg.toml files are incredibly rare in actively developed
Go projects, as people use go.mod with Go modules instead.

While here, also add go.work as a root file, since that is used by
Go module workspaces, added in Go 1.18 in early 2022.
gopls or commands like `go build` work inside either go.work or go.mod.
These two root files are the same ones used by gopls integrations with
other editors like vim or neovim.
1 year ago
Timothy DeHerrera 080f85c34f
fix(nix): fix superflous `/` that breaks the build (#6880)
Resolves #1779
1 year ago
Daniel Martí e7f25d88ca
Avoid extra indentation on Go switches (#6817)
Unlike other languages, in Go, switches themselves are not indented;
it's just each case body which is indented by one level:

   switch foo {
   case "bar":
      baz()
   }

As such, we shouldn't `@indent` for type_switch_statement nor
expression_switch_statement, as otherwise inserted lines show up as:

   switch foo {
      // inserted with "o"
   case "bar":
         // inserted with "o"
      baz()
   }

With the fix, the inserted lines are indented properly:

   switch foo {
   // inserted with "o"
   case "bar":
      // inserted with "o"
      baz()
   }

I also verified that indentation on selects similarly works well.

Thanks to Triton171 for helping with this fix.
1 year ago
Dimitri Sabadie 096ed0ced4
Add extend_to_first_nonwhitespace (#6837)
Closes #6836
1 year ago
dependabot[bot] 0097e191bb
build(deps): bump etcetera from 0.5.0 to 0.7.1 (#6873)
Bumps [etcetera](https://github.com/lunacookies/etcetera) from 0.5.0 to 0.7.1.
- [Release notes](https://github.com/lunacookies/etcetera/releases)
- [Commits](https://github.com/lunacookies/etcetera/compare/v0.5.0...v0.7.1)

---
updated-dependencies:
- dependency-name: etcetera
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] d45af96fc4
build(deps): bump libc from 0.2.141 to 0.2.142 (#6872)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.141 to 0.2.142.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.141...0.2.142)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] f47f8d538d
build(deps): bump regex from 1.7.3 to 1.8.1 (#6871)
Bumps [regex](https://github.com/rust-lang/regex) from 1.7.3 to 1.8.1.
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.7.3...1.8.1)

---
updated-dependencies:
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 11e3fdd153
build(deps): bump dunce from 1.0.3 to 1.0.4 (#6870)
Bumps [dunce](https://gitlab.com/kornelski/dunce) from 1.0.3 to 1.0.4.
- [Release notes](https://gitlab.com/kornelski/dunce/tags)
- [Commits](https://gitlab.com/kornelski/dunce/compare/v1.0.3...v1.0.4)

---
updated-dependencies:
- dependency-name: dunce
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] fd1b3dc499
build(deps): bump bitflags from 2.1.0 to 2.2.1 (#6869)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.1.0 to 2.2.1.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.1.0...2.2.1)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
ymgyt 61ff2bc094
return early if soft wrap is invalid (#6856) 1 year ago
Atticus Sebastiani 228a4af35f
make `:u` alias `:update` (#6835)
* Gave the command update the alias u

* Re added trailing newline

* generated docs
1 year ago
Pascal Kuthe b7c62e200e
fix windows builds (#6845) 1 year ago
Michael T. Mordowanec a1044a6c68
Change soft-wrap.wrap_at_text_width to kebab case in documentation (#6842)
Solves #6803.
1 year ago
Michael Davis ee7413a3fd
tui: Allow toggling mouse capture at runtime (#6675)
This picks up changes to the `editor.mouse` option at runtime - either
through `:set-option` or `:config-reload`. When the value changes, we
tell the terminal to enable or disable mouse capture sequences.
1 year ago
Paul Lange 78088ac185
Ayu dark improvements (#6622)
* theme(ayu_dark): Differentiate primary cursor

* theme(ayu_dark): Color statusline mode field

* theme(ayu): Adapt other ayu themes as well
1 year ago
Chirikumbrah 2342807364
another refactoring of dracula theme (#6767) 1 year ago
Ollie Charles 5aedf11493
Replace `*.cabal` in Haskell `roots` with `cabal.project` (#6828)
`roots` doesn't support wildcards. As such this root is dropped, and `cabal.project` is added, which is probably the best we can do for Cabal-based projects for now.
1 year ago
Pascal Kuthe c3e2db77f7
flip symbol range in LSP goto commands (#6794) 1 year ago
Pascal Kuthe 13cc44c65e
fix windows build (#6834) 1 year ago
Pascal Kuthe b336cd76a7
update dependencies (#6808) 1 year ago
Pascal Kuthe f5d38cee17
produce error when grammar build fails (#6795)
* produce error when grammar build fails

* print which grammar build failed
1 year ago
David Else 83c307a66a
Improve and update the installation documentation (#6809) 1 year ago
Lakshya Singh 4f03c03556
update tree-sitter-markdown (#6785)
* chore: fix bump tree-sitter-markdown

Signed-off-by: Lakshya Singh <lakshay.singh1108@gmail.com>

* chore: bump markdown_inline to latest rev

Signed-off-by: Lakshya Singh <lakshay.singh1108@gmail.com>

---------

Signed-off-by: Lakshya Singh <lakshay.singh1108@gmail.com>
1 year ago
Pascal Kuthe ca65d312e7
always build grammars with c++14 and c11 (#6792) 1 year ago
dependabot[bot] 2ccb5b75d0
build(deps): bump libloading from 0.7.4 to 0.8.0 (#6801)
Bumps [libloading](https://github.com/nagisa/rust_libloading) from 0.7.4 to 0.8.0.
- [Release notes](https://github.com/nagisa/rust_libloading/releases)
- [Commits](https://github.com/nagisa/rust_libloading/compare/0.7.4...0.8.0)

---
updated-dependencies:
- dependency-name: libloading
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 6f972e5da2
build(deps): bump serde from 1.0.159 to 1.0.160 (#6800)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.159 to 1.0.160.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.159...v1.0.160)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] 3f72792033
build(deps): bump serde_json from 1.0.95 to 1.0.96 (#6799)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.95 to 1.0.96.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.95...v1.0.96)

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] a333f6fed7
build(deps): bump etcetera from 0.4.0 to 0.5.0 (#6798)
Bumps [etcetera](https://github.com/lunacookies/etcetera) from 0.4.0 to 0.5.0.
- [Release notes](https://github.com/lunacookies/etcetera/releases)
- [Commits](https://github.com/lunacookies/etcetera/commits)

---
updated-dependencies:
- dependency-name: etcetera
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
A-Walrus 8839eb0af4
Fix unwrap bug in DAP (#6786) 1 year ago
shem 1b016a89d5
Fix crash on opening from suspend state (#6764)
* Fix crash on opening from suspend state (#6725)

* Fix code style

* revert using of the imperative code style. Add panic if couldn't set terminal raw mode

* remove redundant import of core::panic macros

* small refactoring
1 year ago
Aleksey Kuznetsov 7706ff77eb
make :toggle-option print the new value (#6774)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
1 year ago
karei 7607727483 Add `inlay-hint` colours to Kanagawa
Makes the LSP hints more legible against the background colour
and more in line with the normal colours in the theme.
1 year ago
dylrich 523e9e4573 ferra: fix ruler overriding foreground style
Ferra's current ruler styling overrides the foreground style, which is
an odd look only for characters in the ruler column. This commit removes
the foreground styling for the ruler rule. This is more in line with
what other themes do for the ruler.
1 year ago
Matthew Toohey 99a8b6159c Fix go locals query for var_spec identifiers 1 year ago
VuiMuich 4cdba7cccd
Add standalone language support for `just` (#6453)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
1 year ago
Pascal Kuthe 896404c7ea
emit cargo metadata duiring build scripts to avoid outdated buildscript outputs (#6743)
* rebuild on revision change
* rerun grammar build if grammars change
1 year ago
John Careaga 78e8066c3e Replace 'Visual' (mode) with 'Select' in tutor 1 year ago
zetashift cd7ef57818
Change smithy language server to the one from AWS (#6572) 1 year ago
Farzin bb27542199
Fix python highlights to support single character and alphanumeric constant identifier (#6751) 1 year ago
Pascal Kuthe 5ec41195a0
ensure :toggle soft-wrap.enable works by default (#6742) 1 year ago
Jesús González bff7fc8695
Add inlay hints styling to darcula theme (#6732)
* Add inlay hints coloring for darcula theme

* Add whitespace

---------

Co-authored-by: Ivan Tham <pickfire@riseup.net>
1 year ago
shem 9f680c69f4
Fix #6669: Theme preview doesn't return theme to normal (#6694)
* Fix #6669: Theme preview doesn't return theme to normal when delete name with Alt-Backspace

* Fix #6669: Return theme preview to normal theme for all remaining keybinds that change the promt text
1 year ago
Michael 161fef2166
Add DTD language support (#6644)
- Added syntax highlighting for the DTD subset of the XML spec.
- Included .dtd and .ent as common file extensions
1 year ago
lefp fff8543b58
Add OpenCL tree-sitter and LSP config (#6473) 1 year ago
Matouš Dzivjak 70de5026b8
feat(languages): Support markdoc (#6432)
Add basic support for [markdoc](https://markdoc.dev/).

For language server, see: https://github.com/markdoc-extra/markdoc-ls
For tree sitter, see: https://github.com/markdoc-extra/tree-sitter-markdoc
1 year ago
Farzin fed5e6e646
Highlight C/C++ escape sequences (#6724) 1 year ago
Chirikumbrah f6d65cf089
Refactor dracula theme (#6552) 1 year ago
Clément Delafargue 3e2eca0db8
tree-sitter(haskell): use quasiquoters as an injection point (#6474)
Similar to tagged templates in JS, quasiquoters allow to embed external
languages in haskell, so it makes sense to treat them as an injection
point.
1 year ago
Ollie Charles deab323c28
Update tree-sitter-haskell (#6317) 1 year ago
basbebe 1421b67a4c Everforest theme: Style inlay-hints and wrap indicator 1 year ago
basbebe bfcc4c3795 Everforest: Fix keywords and types 1 year ago
basbebe 9248de8780 Adjust everforest to resemble original more closely
Adjust palettes and assignments:

Color palettes of upstream everforest where tweaked since creation of this port:
- https://github.com/sainnhe/everforest/pull/108
- https://github.com/sainnhe/everforest/pull/109

These adjustments move the helix everforest theme closer to the dcocumented
 upstream vim theme
1 year ago
Ollie Charles 92c5f5f18c
Initial Cabal language support (#6485) 1 year ago
Constantin Angheloiu d5fec302c9
base16_transparent: Highlight selected item state (#6716)
Increase visibility of selected file in file picker
 
https://asciinema.org/a/DBaZ5zjaYrKlXDSAVH8THVZg4
1 year ago
EsfoNL 2f82bc13e8
Fix crash on opening jumplist (#6672)
Co-authored-by: Esra Fokker <esra.fokker@finalist.nl>
1 year ago
dependabot[bot] b9b4ed5c6c
build(deps): bump bitflags from 2.0.2 to 2.1.0 (#6699)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 2.0.2 to 2.1.0.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.0.2...2.1.0)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
dependabot[bot] ed67aa864d
build(deps): bump libc from 0.2.140 to 0.2.141 (#6700)
Bumps [libc](https://github.com/rust-lang/libc) from 0.2.140 to 0.2.141.
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.140...0.2.141)

---
updated-dependencies:
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
1 year ago
Konstantin Keller f8b7e95481
LSP: Gracefully handle 'workspace/configuration' from a stopped server (#6693) 1 year ago
Michael Davis 686a1e2f31
Update tree-sitter-git-commit (#6692)
Trailers are now supported, for example 'Co-authored-by' or
'Signed-off-by'. Commits are also now recognized in message bodies.
1 year ago
tippfehlr 5106a124ed
Fix `file-picker.follow-symlinks` config option name in docs (#6685) 1 year ago
not e72ad1e731
theme(rose-pine): Improve whitespace and search matches colors (#6679)
* feat: make whitespace characters more readable

* feat: make search matches more discernable

* fix: reduce importance of whitespace characters

* feat: add background to ui.text.focus

https://github.com/rose-pine/helix/pull/4

Co-authored-by: Iorvethe <58810330+Iorvethe@users.noreply.github.com>
1 year ago
Nico Bos 894c2b9edb
Add Windows install instructions using Winget (#6602) 1 year ago
Ivan Tham 76825c7b66
Fix ayu theme markup unreadable bg (#6538)
* Fix ayu theme markup unreadable bg

* Add modifiers for markup
1 year ago
Alireza Alavi 78b516430a
Fix minor grammar issue in 'from-vim' docs (#6667) 1 year ago
Arnar 6dabd36491
Update gleam grammar (#6641) 1 year ago

@ -14,7 +14,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install stable toolchain
uses: helix-editor/rust-toolchain@v1
with:
@ -34,7 +34,7 @@ jobs:
HELIX_LOG_LEVEL: info
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.65
@ -63,7 +63,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.65
@ -88,7 +88,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.65

@ -11,10 +11,10 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install nix
uses: cachix/install-nix-action@v20
uses: cachix/install-nix-action@v23
- name: Authenticate with Cachix
uses: cachix/cachix-action@v12

@ -11,7 +11,7 @@ jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1

@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@stable
@ -103,7 +103,7 @@ jobs:
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Download grammars
uses: actions/download-artifact@v3
@ -160,7 +160,7 @@ jobs:
- name: Build AppImage
shell: bash
if: matrix.build == 'aarch64-linux' || matrix.build == 'x86_64-linux'
if: matrix.build == 'x86_64-linux'
run: |
# Required as of 22.x https://github.com/AppImage/AppImageKit/wiki/FUSE
sudo add-apt-repository universe
@ -231,7 +231,7 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Checkout sources
uses: actions/checkout@v3
uses: actions/checkout@v4
- uses: actions/download-artifact@v3
@ -263,7 +263,7 @@ jobs:
mv bins-$platform/hx$exe $pkgname
chmod +x $pkgname/hx$exe
if [[ "$platform" = "aarch64-linux" || "$platform" = "x86_64-linux" ]]; then
if [[ "$platform" = "x86_64-linux" ]]; then
mv bins-$platform/helix-*.AppImage* dist/
fi

@ -1,5 +1,2 @@
# 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
# Minified JS vendored from mdbook
book/theme/highlight.js

@ -1,3 +1,120 @@
# 23.05 (2023-05-18)
23.05 is a smaller release focusing on fixes. There were 88 contributors in this release. Thank you all!
Features:
- Add a config option to exclude declaration from LSP references request ([#6886](https://github.com/helix-editor/helix/pull/6886))
- Enable injecting languages based on their file extension and shebang ([#3970](https://github.com/helix-editor/helix/pull/3970))
- Sort the buffer picker by most recent access ([#2980](https://github.com/helix-editor/helix/pull/2980))
- Perform syntax highlighting in the picker asynchronously ([#7028](https://github.com/helix-editor/helix/pull/7028))
Commands:
- `:update` is now aliased as `:u` ([#6835](https://github.com/helix-editor/helix/pull/6835))
- Add `extend_to_first_nonwhitespace` which acts the same as `goto_first_nonwhitespace` but always extends ([#6837](https://github.com/helix-editor/helix/pull/6837))
- Add `:clear-register` for clearing the given register or all registers ([#5695](https://github.com/helix-editor/helix/pull/5695))
- Add `:write-buffer-close` and `:write-buffer-close!` ([#6947](https://github.com/helix-editor/helix/pull/6947))
Fixes:
- Normalize LSP workspace paths ([#6517](https://github.com/helix-editor/helix/pull/6517))
- Robustly handle invalid LSP ranges ([#6512](https://github.com/helix-editor/helix/pull/6512))
- Fix line number display for LSP goto pickers ([#6559](https://github.com/helix-editor/helix/pull/6559))
- Fix toggling of `soft-wrap.enable` option ([#6656](https://github.com/helix-editor/helix/pull/6656), [58e457a](https://github.com/helix-editor/helix/commit/58e457a), [#6742](https://github.com/helix-editor/helix/pull/6742))
- Handle `workspace/configuration` requests from stopped language servers ([#6693](https://github.com/helix-editor/helix/pull/6693))
- Fix possible crash from opening the jumplist picker ([#6672](https://github.com/helix-editor/helix/pull/6672))
- Fix theme preview returning to current theme on line and word deletions ([#6694](https://github.com/helix-editor/helix/pull/6694))
- Re-run crate build scripts on changes to revision and grammar repositories ([#6743](https://github.com/helix-editor/helix/pull/6743))
- Fix crash on opening from suspended state ([#6764](https://github.com/helix-editor/helix/pull/6764))
- Fix unwrap bug in DAP ([#6786](https://github.com/helix-editor/helix/pull/6786))
- Always build tree-sitter parsers with C++14 and C11 ([#6792](https://github.com/helix-editor/helix/pull/6792), [#6834](https://github.com/helix-editor/helix/pull/6834), [#6845](https://github.com/helix-editor/helix/pull/6845))
- Exit with a non-zero statuscode when tree-sitter parser builds fail ([#6795](https://github.com/helix-editor/helix/pull/6795))
- Flip symbol range in LSP goto commands ([#6794](https://github.com/helix-editor/helix/pull/6794))
- Fix runtime toggling of the `mouse` option ([#6675](https://github.com/helix-editor/helix/pull/6675))
- Fix panic in inlay hint computation when view anchor is out of bounds ([#6883](https://github.com/helix-editor/helix/pull/6883))
- Significantly improve performance of git discovery on slow file systems ([#6890](https://github.com/helix-editor/helix/pull/6890))
- Downgrade gix log level to info ([#6915](https://github.com/helix-editor/helix/pull/6915))
- Conserve BOM and properly support saving UTF16 files ([#6497](https://github.com/helix-editor/helix/pull/6497))
- Correctly handle completion re-request ([#6594](https://github.com/helix-editor/helix/pull/6594))
- Fix offset encoding in LSP `didChange` notifications ([#6921](https://github.com/helix-editor/helix/pull/6921))
- Change `gix` logging level to info ([#6915](https://github.com/helix-editor/helix/pull/6915))
- Improve error message when writes fail because parent directories do not exist ([#7014](https://github.com/helix-editor/helix/pull/7014))
- Replace DAP variables popup instead of pushing more popups ([#7034](https://github.com/helix-editor/helix/pull/7034))
- Disable tree-sitter for files after parsing for 500ms ([#7028](https://github.com/helix-editor/helix/pull/7028))
- Fix crash when deleting with multiple cursors ([#6024](https://github.com/helix-editor/helix/pull/6024))
- Fix selection sliding when deleting forwards in append mode ([#6024](https://github.com/helix-editor/helix/pull/6024))
- Fix completion on paths containing spaces ([#6779](https://github.com/helix-editor/helix/pull/6779))
Themes:
- Style inlay hints in `dracula` theme ([#6515](https://github.com/helix-editor/helix/pull/6515))
- Style inlay hints in `onedark` theme ([#6503](https://github.com/helix-editor/helix/pull/6503))
- Style inlay hints and the soft-wrap indicator in `varua` ([#6568](https://github.com/helix-editor/helix/pull/6568), [#6589](https://github.com/helix-editor/helix/pull/6589))
- Style inlay hints in `emacs` theme ([#6569](https://github.com/helix-editor/helix/pull/6569))
- Update `base16_transparent` and `dark_high_contrast` themes ([#6577](https://github.com/helix-editor/helix/pull/6577))
- Style inlay hints for `mellow` and `rasmus` themes ([#6583](https://github.com/helix-editor/helix/pull/6583))
- Dim pane divider for `base16_transparent` theme ([#6534](https://github.com/helix-editor/helix/pull/6534))
- Style inlay hints in `zenburn` theme ([#6593](https://github.com/helix-editor/helix/pull/6593))
- Style inlay hints in `boo_berry` theme ([#6625](https://github.com/helix-editor/helix/pull/6625))
- Add `ferra` theme ([#6619](https://github.com/helix-editor/helix/pull/6619), [#6776](https://github.com/helix-editor/helix/pull/6776))
- Style inlay hints in `nightfox` theme ([#6655](https://github.com/helix-editor/helix/pull/6655))
- Fix `ayu` theme family markup code block background ([#6538](https://github.com/helix-editor/helix/pull/6538))
- Improve whitespace and search match colors in `rose_pine` theme ([#6679](https://github.com/helix-editor/helix/pull/6679))
- Highlight selected items in `base16_transparent` theme ([#6716](https://github.com/helix-editor/helix/pull/6716))
- Adjust everforest to resemble original more closely ([#5866](https://github.com/helix-editor/helix/pull/5866))
- Refactor `dracula` theme ([#6552](https://github.com/helix-editor/helix/pull/6552), [#6767](https://github.com/helix-editor/helix/pull/6767), [#6855](https://github.com/helix-editor/helix/pull/6855), [#6987](https://github.com/helix-editor/helix/pull/6987))
- Style inlay hints in `darcula` theme ([#6732](https://github.com/helix-editor/helix/pull/6732))
- Style inlay hints in `kanagawa` theme ([#6773](https://github.com/helix-editor/helix/pull/6773))
- Improve `ayu_dark` theme ([#6622](https://github.com/helix-editor/helix/pull/6622))
- Refactor `noctis` theme multiple cursor highlighting ([96720e7](https://github.com/helix-editor/helix/commit/96720e7))
- Refactor `noctis` theme whitespace rendering and indent guides ([f2ccc03](https://github.com/helix-editor/helix/commit/f2ccc03))
- Add `amberwood` theme ([#6924](https://github.com/helix-editor/helix/pull/6924))
- Update `nightfox` theme ([#7061](https://github.com/helix-editor/helix/pull/7061))
Language support:
- R language server: use the `--no-echo` flag to silence output ([#6570](https://github.com/helix-editor/helix/pull/6570))
- Recognize CUDA files as C++ ([#6521](https://github.com/helix-editor/helix/pull/6521))
- Add support for Hurl ([#6450](https://github.com/helix-editor/helix/pull/6450))
- Add textobject queries for Julia ([#6588](https://github.com/helix-editor/helix/pull/6588))
- Update Ruby highlight queries ([#6587](https://github.com/helix-editor/helix/pull/6587))
- Add xsd to XML file-types ([#6631](https://github.com/helix-editor/helix/pull/6631))
- Support Robot Framework ([#6611](https://github.com/helix-editor/helix/pull/6611))
- Update Gleam tree-sitter parser ([#6641](https://github.com/helix-editor/helix/pull/6641))
- Update git-commit tree-sitter parser ([#6692](https://github.com/helix-editor/helix/pull/6692))
- Update Haskell tree-sitter parser ([#6317](https://github.com/helix-editor/helix/pull/6317))
- Add injection queries for Haskell quasiquotes ([#6474](https://github.com/helix-editor/helix/pull/6474))
- Highlight C/C++ escape sequences ([#6724](https://github.com/helix-editor/helix/pull/6724))
- Support Markdoc ([#6432](https://github.com/helix-editor/helix/pull/6432))
- Support OpenCL ([#6473](https://github.com/helix-editor/helix/pull/6473))
- Support DTD ([#6644](https://github.com/helix-editor/helix/pull/6644))
- Fix constant highlighting in Python queries ([#6751](https://github.com/helix-editor/helix/pull/6751))
- Support Just ([#6453](https://github.com/helix-editor/helix/pull/6453))
- Fix Go locals query for `var_spec` identifiers ([#6763](https://github.com/helix-editor/helix/pull/6763))
- Update Markdown tree-sitter parser ([#6785](https://github.com/helix-editor/helix/pull/6785))
- Fix Haskell workspace root for cabal projects ([#6828](https://github.com/helix-editor/helix/pull/6828))
- Avoid extra indentation in Go switches ([#6817](https://github.com/helix-editor/helix/pull/6817))
- Fix Go workspace roots ([#6884](https://github.com/helix-editor/helix/pull/6884))
- Set PerlNavigator as the default Perl language server ([#6860](https://github.com/helix-editor/helix/pull/6860))
- Highlight more sqlx macros in Rust ([#6793](https://github.com/helix-editor/helix/pull/6793))
- Switch Odin tree-sitter grammar ([#6766](https://github.com/helix-editor/helix/pull/6766))
- Recognize `poetry.lock` as TOML ([#6928](https://github.com/helix-editor/helix/pull/6928))
- Recognize Jupyter notebooks as JSON ([#6927](https://github.com/helix-editor/helix/pull/6927))
- Add language server configuration for Crystal ([#6948](https://github.com/helix-editor/helix/pull/6948))
- Add `build.gradle.kts` to Java and Scala roots ([#6970](https://github.com/helix-editor/helix/pull/6970))
- Recognize `sty` and `cls` files as latex ([#6986](https://github.com/helix-editor/helix/pull/6986))
- Update Dockerfile tree-sitter grammar ([#6895](https://github.com/helix-editor/helix/pull/6895))
- Add comment injections for Odin ([#7027](https://github.com/helix-editor/helix/pull/7027))
- Recognize `gml` as XML ([#7055](https://github.com/helix-editor/helix/pull/7055))
- Recognize `geojson` as JSON ([#7054](https://github.com/helix-editor/helix/pull/7054))
Packaging:
- Update the Nix flake dependencies, remove a deprecated option ([#6546](https://github.com/helix-editor/helix/pull/6546))
- Fix and re-enable aarch64-macos release binary builds ([#6504](https://github.com/helix-editor/helix/pull/6504))
- The git dependency on `tree-sitter` has been replaced with a regular crates.io dependency ([#6608](https://github.com/helix-editor/helix/pull/6608))
# 23.03 (2023-03-31)
23.03 brings some long-awaited and exciting features. Thank you to everyone involved! This release saw changes from 102 contributors.
@ -1479,7 +1596,7 @@ to distinguish it in bug reports..
- The `runtime/` directory is now properly detected on binary releases and
on cargo run. `~/.config/helix/runtime` can also be used.
- Registers can now be selected via " (for example `"ay`)
- Registers can now be selected via " (for example, `"ay`)
- Support for Nix files was added
- Movement is now fully tested and matches Kakoune implementation
- A per-file LSP symbol picker was added to space+s

1044
Cargo.lock generated

File diff suppressed because it is too large Load Diff

@ -5,6 +5,7 @@ members = [
"helix-term",
"helix-tui",
"helix-lsp",
"helix-event",
"helix-dap",
"helix-loader",
"helix-vcs",
@ -32,3 +33,7 @@ inherits = "test"
package.helix-core.opt-level = 2
package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" }
nucleo = "0.2.0"

@ -61,4 +61,4 @@ Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-c
# Credits
Thanks to [@JakeHL](https://github.com/JakeHL) for designing the logo!
Thanks to [@jakenvac](https://github.com/jakenvac) for designing the logo!

@ -1 +1 @@
23.03
23.05

@ -3,10 +3,14 @@ authors = ["Blaž Hrastnik"]
language = "en"
multilingual = false
src = "src"
edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
[output.html]
cname = "docs.helix-editor.com"
default-theme = "colibri"
preferred-dark-theme = "colibri"
git-repository-url = "https://github.com/helix-editor/helix"
edit-url-template = "https://github.com/helix-editor/helix/edit/master/book/{path}"
additional-css = ["custom.css"]
[output.html.search]
use-boolean-and = true

@ -0,0 +1,231 @@
html {
font-family: "Inter", sans-serif;
}
.sidebar .sidebar-scrollbox {
padding: 0;
}
.chapter {
margin: 0.25rem 0;
}
.chapter li.chapter-item {
line-height: initial;
margin: 0;
padding: 1rem 1.5rem;
}
.chapter .section li.chapter-item {
line-height: inherit;
padding: .5rem .5rem 0 .5rem;
}
.content {
overflow-y: auto;
padding: 0 15px;
padding-bottom: 50px;
}
/* 2 1.75 1.5 1.25 1 .875 */
.content h1 { font-size: 2em }
.content h2 { font-size: 1.75em }
.content h3 { font-size: 1.5em }
.content h4 { font-size: 1.25em }
.content h5 { font-size: 1em }
.content h6 { font-size: .875em }
.content h1,
.content h2,
.content h3,
.content h4 {
font-weight: 500;
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p,
.content ol,
.content ul,
.content table {
margin-top: 0;
margin-bottom: .875em;
}
.content ul li {
margin-bottom: .25rem;
}
.content ul {
list-style-type: square;
}
.content ul ul,
.content ol ul {
margin-bottom: .5rem;
}
.content li p {
margin-bottom: .5em;
}
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-left: 4px solid var(--quote-border);
border-top: none;
border-bottom: none;
}
blockquote *:last-child {
margin-bottom: 0;
}
table {
width: 100%;
}
table thead th {
padding: .75rem;
text-align: left;
font-weight: 500;
line-height: 1.5;
width: auto;
}
table td {
padding: .75rem;
border: none;
}
table thead tr {
border: none;
border-bottom: 2px var(--table-border-color) solid;
}
table tbody tr {
border-bottom: 1px var(--table-border-line) solid;
}
table tbody tr:nth-child(2n) {
background: unset;
}
pre code.hljs {
display: block;
overflow-x: auto;
padding: 1em;
}
code.hljs {
padding: 3px 5px;
}
.colibri {
--bg: #3b224c;
--fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
/* --links: #a4a0e8; */
--links: #ECCDBA;
--inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0, 0, 0, .2);
--quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #acff5;
}
.colibri .content .header {
color: #fff;
}
/* highlight.js theme, :where() is used to avoid increasing specificity */
:where(.colibri) .hljs {
background: #2f1e2e;
color: #a39e9b;
}
:where(.colibri) .hljs-comment,
:where(.colibri) .hljs-quote {
color: #8d8687;
}
:where(.colibri) .hljs-link,
:where(.colibri) .hljs-meta,
:where(.colibri) .hljs-name,
:where(.colibri) .hljs-regexp,
:where(.colibri) .hljs-selector-class,
:where(.colibri) .hljs-selector-id,
:where(.colibri) .hljs-tag,
:where(.colibri) .hljs-template-variable,
:where(.colibri) .hljs-variable {
color: #ef6155;
}
:where(.colibri) .hljs-built_in,
:where(.colibri) .hljs-deletion,
:where(.colibri) .hljs-literal,
:where(.colibri) .hljs-number,
:where(.colibri) .hljs-params,
:where(.colibri) .hljs-type {
color: #f99b15;
}
:where(.colibri) .hljs-attribute,
:where(.colibri) .hljs-section,
:where(.colibri) .hljs-title {
color: #fec418;
}
:where(.colibri) .hljs-addition,
:where(.colibri) .hljs-bullet,
:where(.colibri) .hljs-string,
:where(.colibri) .hljs-symbol {
color: #48b685;
}
:where(.colibri) .hljs-keyword,
:where(.colibri) .hljs-selector-tag {
color: #815ba4;
}
:where(.colibri) .hljs-emphasis {
font-style: italic;
}
:where(.colibri) .hljs-strong {
font-weight: 700;
}

@ -16,3 +16,4 @@
- [Adding languages](./guides/adding_languages.md)
- [Adding textobject queries](./guides/textobject.md)
- [Adding indent queries](./guides/indent.md)
- [Adding injection queries](./guides/injection.md)

@ -52,6 +52,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `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 | `400` |
| `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` |
@ -60,8 +61,10 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `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` |
| `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` |
### `[editor.statusline]` Section
@ -87,9 +90,9 @@ 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"]` |
| `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", "position", "file-encoding"]` |
| `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"` |
@ -106,6 +109,7 @@ The following statusline elements can be configured:
| `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 |
@ -117,6 +121,7 @@ The following statusline elements can be configured:
| `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
@ -128,10 +133,11 @@ The following statusline elements can be configured:
| `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!
[^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
@ -161,7 +167,7 @@ All git related options are only enabled in a git repository.
| Key | Description | Default |
|--|--|---------|
|`hidden` | Enables ignoring hidden files | true
|`follow-links` | Follow symlinks instead of ignoring them | 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
@ -343,3 +349,11 @@ 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` |

@ -2,9 +2,9 @@
Helix's editing model is strongly inspired from Vim and Kakoune, and a notable
difference from Vim (and the most striking similarity to Kakoune) is that Helix
follows the `selection → action` model. This means that the 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
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.
See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim) and Helix's [Migrating from Vim](https://github.com/helix-editor/helix/wiki/Migrating-from-Vim).

@ -2,14 +2,16 @@
| --- | --- | --- | --- | --- |
| astro | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | | ✓ | `bash-language-server` |
| bash | ✓ | | ✓ | `bash-language-server` |
| bass | ✓ | | | `bass` |
| beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` |
| blueprint | ✓ | | | `blueprint-compiler` |
| c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` |
| cairo | ✓ | | | |
| cabal | | | | |
| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
| capnp | ✓ | | ✓ | |
| clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
@ -17,7 +19,7 @@
| common-lisp | ✓ | | | `cl-lsp` |
| cpon | ✓ | | ✓ | |
| cpp | ✓ | ✓ | ✓ | `clangd` |
| crystal | ✓ | ✓ | | |
| crystal | ✓ | ✓ | | `crystalline` |
| css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` |
| d | ✓ | ✓ | ✓ | `serve-d` |
@ -27,6 +29,7 @@
| diff | ✓ | | | |
| dockerfile | ✓ | | | `docker-langserver` |
| dot | ✓ | | | `dot-language-server` |
| dtd | ✓ | | | |
| edoc | ✓ | | | |
| eex | ✓ | | | |
| ejs | ✓ | | | |
@ -38,8 +41,11 @@
| erlang | ✓ | ✓ | | `erlang_ls` |
| esdl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | `forth-lsp` |
| fortran | ✓ | | ✓ | `fortls` |
| fsharp | ✓ | | | `fsautocomplete` |
| gdscript | ✓ | ✓ | ✓ | |
| gemini | ✓ | | | |
| git-attributes | ✓ | | | |
| git-commit | ✓ | ✓ | | |
| git-config | ✓ | | | |
@ -55,6 +61,7 @@
| graphql | ✓ | | | |
| hare | ✓ | | | |
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | |
| hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` |
| hosts | ✓ | | | |
@ -63,13 +70,15 @@
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
| java | ✓ | ✓ | | `jdtls` |
| java | ✓ | ✓ | | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | |
| jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` |
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| julia | ✓ | ✓ | ✓ | `julia` |
| just | ✓ | ✓ | ✓ | |
| kdl | ✓ | | | |
| kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | ✓ | | `texlab` |
@ -80,9 +89,10 @@
| llvm-mir-yaml | ✓ | | ✓ | |
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
| make | ✓ | | | |
| markdoc | ✓ | | | `markdoc-ls` |
| markdown | ✓ | | | `marksman` |
| markdown.inline | ✓ | | | |
| matlab | ✓ | | | |
| matlab | ✓ | | | |
| mermaid | ✓ | | | |
| meson | ✓ | | ✓ | |
| mint | | | | `mint` |
@ -92,21 +102,24 @@
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
| nix | ✓ | | | `nil` |
| nu | ✓ | | | |
| nunjucks | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | | `ols` |
| odin | ✓ | | ✓ | `ols` |
| opencl | ✓ | ✓ | ✓ | `clangd` |
| openscad | ✓ | | | `openscad-lsp` |
| org | ✓ | | | |
| pascal | ✓ | ✓ | | `pasls` |
| passwd | ✓ | | | |
| pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | |
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
| php | ✓ | ✓ | ✓ | `intelephense` |
| po | ✓ | ✓ | | |
| pod | ✓ | | | |
| ponylang | ✓ | ✓ | ✓ | |
| prisma | ✓ | | | `prisma-language-server` |
| prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | |
| protobuf | ✓ | | ✓ | `bufls`, `pb` |
| prql | ✓ | | | |
| purescript | ✓ | | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `pylsp` |
@ -133,29 +146,35 @@
| sql | ✓ | | | |
| sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | |
| svelte | ✓ | | | `svelteserver` |
| strace | ✓ | | | |
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | | | `sourcekit-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | |
| task | ✓ | | | |
| tfvars | ✓ | | ✓ | `terraform-ls` |
| todotxt | ✓ | | | |
| toml | ✓ | | | `taplo` |
| tsq | ✓ | | | |
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| twig | ✓ | | | |
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
| ungrammar | ✓ | | | |
| unison | ✓ | | | |
| uxntal | ✓ | | | |
| v | ✓ | ✓ | ✓ | `v` |
| v | ✓ | ✓ | ✓ | `v-analyzer` |
| vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | |
| vue | ✓ | | | `vls` |
| vue | ✓ | | | `vue-language-server` |
| wast | ✓ | | | |
| wat | ✓ | | | |
| webc | ✓ | | | |
| wgsl | ✓ | | | `wgsl_analyzer` |
| wit | ✓ | | ✓ | |
| wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | |
| xml | ✓ | | ✓ | |
| yaml | ✓ | | ✓ | `yaml-language-server` |

@ -12,7 +12,9 @@
| `:buffer-next`, `:bn`, `:bnext` | Goto next buffer. |
| `:buffer-previous`, `:bp`, `:bprev` | Goto previous buffer. |
| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) |
| `:write!`, `:w!` | Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write some/path.txt) |
| `:write!`, `:w!` | Force write changes to disk creating necessary subdirectories. Accepts an optional path (:write! some/path.txt) |
| `:write-buffer-close`, `:wbc` | Write changes to disk and closes the buffer. Accepts an optional path (:write-buffer-close some/path.txt) |
| `:write-buffer-close!`, `:wbc!` | Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt) |
| `:new`, `:n` | Create a new scratch buffer. |
| `:format`, `:fmt` | Format the file using the LSP formatter. |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
@ -22,6 +24,7 @@
| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) |
| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) |
| `:write-all`, `:wa` | Write changes from all buffers to disk. |
| `:write-all!`, `:wa!` | Forcefully write changes from all buffers to disk creating necessary subdirectories. |
| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all buffers to disk and close all views. |
| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all buffers to disk and close all views forcefully (ignoring unsaved changes). |
| `:quit-all`, `:qa` | Close all views. |
@ -29,6 +32,7 @@
| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). |
| `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). |
| `:theme` | Change the editor theme (show current theme if no name specified). |
| `:yank-join` | Yank joined selections. A separator can be provided as first argument. Default value is newline. |
| `:clipboard-yank` | Yank main selection into system clipboard. |
| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. |
| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. |
@ -44,13 +48,14 @@
| `:show-directory`, `:pwd` | Show the current working directory. |
| `:encoding` | Set encoding. Based on `https://encoding.spec.whatwg.org`. |
| `:character-info`, `:char` | Get info about the character under the primary cursor. |
| `:reload` | Discard changes and reload from the source file. |
| `:reload-all` | Discard changes and reload all documents from the source files. |
| `:update` | Write changes only if the file has been modified. |
| `:reload`, `:rl` | Discard changes and reload from the source file. |
| `:reload-all`, `:rla` | Discard changes and reload all documents from the source files. |
| `:update`, `:u` | Write changes only if the file has been modified. |
| `:lsp-workspace-command` | Open workspace command picker |
| `:lsp-restart` | Restarts the Language Server that is in use by the current doc |
| `:lsp-stop` | Stops the Language Server that is in use by the current doc |
| `:lsp-restart` | Restarts the language servers used by the current doc |
| `:lsp-stop` | Stops the language servers that are used by the current doc |
| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. |
| `:tree-sitter-highlight-name` | Display name of tree-sitter highlight scope under the cursor. |
| `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. |
| `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. |
| `:debug-eval` | Evaluate expression in current debug context. |
@ -78,3 +83,5 @@
| `:pipe-to` | Pipe each selection to the shell command, ignoring output. |
| `:run-shell-command`, `:sh` | Run a shell command |
| `: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. |
| `:redraw` | Clear and re-render the whole UI |

@ -9,6 +9,7 @@ below.
necessary configuration for the new language. For more information on
language configuration, refer to the
[language configuration section](../languages.md) of the documentation.
A new language server can be added by extending the `[language-server]` table in the same file.
2. If you are adding a new language or updating an existing language server
configuration, run the command `cargo xtask docgen` to update the
[Language Support](../lang-support.md) documentation.

@ -1,76 +1,299 @@
# Adding indent queries
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/{language}/indents.scm`. The indentation for a line
is calculated by traversing the syntax tree from the lowest node at the
beginning of the new line. Each of these nodes contributes to the total
indent when it is captured by the query (in what way depends on the name
of the capture).
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/
{language}/indents.scm`. The indentation for a line is calculated by traversing
the syntax tree from the lowest node at the beginning of the new line (see
[Indent queries](#indent-queries)). Each of these nodes contributes to the total
indent when it is captured by the query (in what way depends on the name of
the capture.
Note that it matters where these added indents begin. For example,
multiple indent level increases that start on the same line only increase
the total indent level by 1.
the total indent level by 1. See [Capture types](#capture-types).
## Scopes
## Indent queries
Added indents don't always apply to the whole node. For example, in most
cases when a node should be indented, we actually only want everything
except for its first line to be indented. For this, there are several
scopes (more scopes may be added in the future if required):
When Helix is inserting a new line through `o`, `O`, or `<ret>`, to determine
the indent level for the new line, the query in `indents.scm` is run on the
document. The starting position of the query is the end of the line above where
a new line will be inserted.
- `all`:
This scope applies to the whole captured node. This is only different from
`tail` when the captured node is the first node on its line.
For `o`, the inserted line is the line below the cursor, so that starting
position of the query is the end of the current line.
- `tail`:
This scope applies to everything except for the first line of the
captured node.
```rust
fn need_hero(some_hero: Hero, life: Life) -> {
matches!(some_hero, Hero { // ←─────────────────╮
strong: true,//←╮ ↑ ↑ │
fast: true, // │ │ ╰── query start │
sure: true, // │ ╰───── cursor ├─ traversal
soon: true, // ╰──────── new line inserted │ start node
}) && // │
// ↑ │
// ╰───────────────────────────────────────────────╯
some_hero > life
}
```
Every capture type has a default scope which should do the right thing
in most situations. When a different scope is required, this can be
changed by using a `#set!` declaration anywhere in the pattern:
```scm
(assignment_expression
right: (_) @indent
(#set! "scope" "all"))
For `O`, the newly inserted line is the *current* line, so the starting position
of the query is the end of the line above the cursor.
```rust
fn need_hero(some_hero: Hero, life: Life) -> { // ←─╮
matches!(some_hero, Hero { // ←╮ ↑ │
strong: true,// ↑ ╭───╯ │ │
fast: true, // │ │ query start ─╯ │
sure: true, // ╰───┼ cursor ├─ traversal
soon: true, // ╰ new line inserted │ start node
}) && // │
some_hero > life // │
} // ←──────────────────────────────────────────────╯
```
## Capture types
From this starting node, the syntax tree is traversed up until the root node.
Each indent capture is collected along the way, and then combined according to
their [capture types](#capture-types) and [scopes](#scopes) to a final indent
level for the line.
- `@indent` (default scope `tail`):
Increase the indent level by 1. Multiple occurrences in the same line
don't stack. If there is at least one `@indent` and one `@outdent`
capture on the same line, the indent level isn't changed at all.
### Capture types
- `@indent` (default scope `tail`):
Increase the indent level by 1. Multiple occurrences in the same line *do not*
stack. If there is at least one `@indent` and one `@outdent` capture on the
same line, the indent level isn't changed at all.
- `@outdent` (default scope `all`):
Decrease the indent level by 1. The same rules as for `@indent` apply.
Decrease the indent level by 1. The same rules as for `@indent` apply.
- `@indent.always` (default scope `tail`):
Increase the indent level by 1. Multiple occurrences on the same line *do*
stack. The final indent level is `@indent.always` `@outdent.always`. If
an `@indent` and an `@indent.always` are on the same line, the `@indent` is
ignored.
- `@outdent.always` (default scope `all`):
Decrease the indent level by 1. The same rules as for `@indent.always` apply.
- `@align` (default scope `all`):
Align everything inside this node to some anchor. The anchor is given
by the start of the node captured by `@anchor` in the same pattern.
Every pattern with an `@align` should contain exactly one `@anchor`.
Indent (and outdent) for nodes below (in terms of their starting line)
the `@align` node is added to the indentation required for alignment.
- `@extend`:
Extend the range of this node to the end of the line and to lines that
are indented more than the line that this node starts on. This is useful
for languages like Python, where for the purpose of indentation some nodes
(like functions or classes) should also contain indented lines that follow them.
Extend the range of this node to the end of the line and to lines that are
indented more than the line that this node starts on. This is useful for
languages like Python, where for the purpose of indentation some nodes (like
functions or classes) should also contain indented lines that follow them.
- `@extend.prevent-once`:
Prevents the first extension of an ancestor of this node. For example, in Python
a return expression always ends the block that it is in. Note that this only stops the
extension of the next `@extend` capture. If multiple ancestors are captured,
only the extension of the innermost one is prevented. All other ancestors are unaffected
(regardless of whether the innermost ancestor would actually have been extended).
Prevents the first extension of an ancestor of this node. For example, in Python
a return expression always ends the block that it is in. Note that this only
stops the extension of the next `@extend` capture. If multiple ancestors are
captured, only the extension of the innermost one is prevented. All other
ancestors are unaffected (regardless of whether the innermost ancestor would
actually have been extended).
#### `@indent` / `@outdent`
Consider this example:
```rust
fn shout(things: Vec<Thing>) {
// ↑
// ├───────────────────────╮ indent level
// @indent ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// │
let it_all = |out| { things.filter(|thing| { // │ 1
// ↑ ↑ │
// ├───────────────────────┼─────┼┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// @indent @indent
// │ 2
thing.can_do_with(out) // │
})}; // ├┄┄┄┄┄┄┄┄┄┄┄┄┄┄
//↑↑↑ │ 1
} //╰┼┴──────────────────────────────────────────────┴┄┄┄┄┄┄┄┄┄┄┄┄┄┄
// 3x @outdent
```
```scm
((block) @indent)
["}" ")"] @outdent
```
Note how on the second line, we have two blocks begin on the same line. In this
case, since both captures occur on the same line, they are combined and only
result in a net increase of 1. Also note that the closing `}`s are part of the
`@indent` captures, but the 3 `@outdent`s also combine into 1 and result in that
line losing one indent level.
#### `@extend` / `@extend.prevent-once`
For an example of where `@extend` can be useful, consider Python, which is
whitespace-sensitive.
```scm
]
(parenthesized_expression)
(function_definition)
(class_definition)
] @indent
```
```python
class Hero:
def __init__(self, strong, fast, sure, soon):# ←─╮
self.is_strong = strong # │
self.is_fast = fast # ╭─── query start │
self.is_sure = sure # │ ╭─ cursor │
self.is_soon = soon # │ │ │
# ↑ ↑ │ │ │
# │ ╰──────╯ │ │
# ╰─────────────────────╯ │
# ├─ traversal
def need_hero(self, life): # │ start node
return ( # │
self.is_strong # │
and self.is_fast # │
and self.is_sure # │
and self.is_soon # │
and self > life # │
) # ←─────────────────────────────────────────╯
```
Without braces to catch the scope of the function, the smallest descendant of
the cursor on a line feed ends up being the entire inside of the class. Because
of this, it will miss the entire function node and its indent capture, leading
to an indent level one too small.
To address this case, `@extend` tells helix to "extend" the captured node's span
to the line feed and every consecutive line that has a greater indent level than
the line of the node.
```scm
(parenthesized_expression) @indent
]
(function_definition)
(class_definition)
] @indent @extend
```
```python
class Hero:
def __init__(self, strong, fast, sure, soon):# ←─╮
self.is_strong = strong # │
self.is_fast = fast # ╭─── query start ├─ traversal
self.is_sure = sure # │ ╭─ cursor │ start node
self.is_soon = soon # │ │ ←───────────────╯
# ↑ ↑ │ │
# │ ╰──────╯ │
# ╰─────────────────────╯
def need_hero(self, life):
return (
self.is_strong
and self.is_fast
and self.is_sure
and self.is_soon
and self > life
)
```
Furthermore, there are some cases where extending to everything with a greater
indent level may not be desirable. Consider the `need_hero` function above. If
our cursor is on the last line of the returned expression.
```python
class Hero:
def __init__(self, strong, fast, sure, soon):
self.is_strong = strong
self.is_fast = fast
self.is_sure = sure
self.is_soon = soon
def need_hero(self, life):
return (
self.is_strong
and self.is_fast
and self.is_sure
and self.is_soon
and self > life
) # ←─── cursor
#←────────── where cursor should go on new line
```
In Python, the are a few tokens that will always end a scope, such as a return
statement. Since the scope ends, so should the indent level. But because the
function span is extended to every line with a greater indent level, a new line
would just continue on the same level. And an `@outdent` would not help us here
either, since it would cause everything in the parentheses to become outdented
as well.
To help, we need to signal an end to the extension. We can do this with
`@extend.prevent-once`.
```scm
(parenthesized_expression) @indent
]
(function_definition)
(class_definition)
] @indent @extend
(return_statement) @extend.prevent-once
```
#### `@indent.always` / `@outdent.always`
As mentioned before, normally if there is more than one `@indent` or `@outdent`
capture on the same line, they are combined.
Sometimes, there are cases when you may want to ensure that every indent capture
is additive, regardless of how many occur on the same line. Consider this
example in YAML.
```yaml
- foo: bar
# ↑ ↑
# │ ╰─────────────── start of map
# ╰───────────────── start of list element
baz: quux # ←─── cursor
# ←───────────── where the cursor should go on a new line
garply: waldo
- quux:
bar: baz
xyzzy: thud
fred: plugh
```
In YAML, you often have lists of maps. In these cases, the syntax is such that
the list element and the map both start on the same line. But we really do want
to start an indentation for each of these so that subsequent keys in the map
hang over the list and align properly. This is where `@indent.always` helps.
```scm
((block_sequence_item) @item @indent.always @extend
(#not-one-line? @item))
((block_mapping_pair
key: (_) @key
value: (_) @val
(#not-same-line? @key @val)
) @indent.always @extend
)
```
## Predicates
In some cases, an S-expression cannot express exactly what pattern should be matched.
For that, tree-sitter allows for predicates to appear anywhere within a pattern,
similar to how `#set!` declarations work:
```scm
(some_kind
(child_kind) @indent
(#predicate? arg1 arg2 ...)
)
```
The number of arguments depends on the predicate that's used.
Each argument is either a capture (`@name`) or a string (`"some string"`).
The following predicates are supported by tree-sitter:
@ -91,3 +314,47 @@ argument (a string).
- `#same-line?`/`#not-same-line?`:
The captures given by the 2 arguments must/must not start on the same line.
- `#one-line?`/`#not-one-line?`:
The captures given by the fist argument must/must span a total of one line.
### Scopes
Added indents don't always apply to the whole node. For example, in most
cases when a node should be indented, we actually only want everything
except for its first line to be indented. For this, there are several
scopes (more scopes may be added in the future if required):
- `tail`:
This scope applies to everything except for the first line of the
captured node.
- `all`:
This scope applies to the whole captured node. This is only different from
`tail` when the captured node is the first node on its line.
For example, imagine we have the following function
```rust
fn aha() { // ←─────────────────────────────────────╮
let take = "on me"; // ←──────────────╮ scope: │
let take = "me on"; // ├─ "tail" ├─ (block) @indent
let ill = be_gone_days(1 || 2); // │ │
} // ←───────────────────────────────────┴──────────┴─ "}" @outdent
// scope: "all"
```
We can write the following query with the `#set!` declaration:
```scm
((block) @indent
(#set! "scope" "tail"))
("}" @outdent
(#set! "scope" "all"))
```
As we can see, the "tail" scope covers the node, except for the first line.
Everything up to and including the closing brace gets an indent level of 1.
Then, on the closing brace, we encounter an outdent with a scope of "all", which
means the first line is included, and the indent level is cancelled out on this
line. (Note these scopes are the defaults for `@indent` and `@outdent`—they are
written explicitly for demonstration.)

@ -0,0 +1,57 @@
# Adding Injection Queries
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
are a few Helix specific extensions that allow for more control.
And example of a simple query that would highlight all strings as bash in Nix:
```scm
((string_expression (string_fragment) @injection.content)
(#set! injection.language "bash"))
```
## Capture Types
- `@injection.language` (standard):
The captured node may contain the language name used to highlight the node captured by
`@injection.content`.
- `@injection.content` (standard):
Marks the content to be highlighted as the language captured with `@injection.language` _et al_.
- `@injection.filename` (extension):
The captured node may contain a filename with a file-extension known to Helix,
highlighting `@injection.content` as that language. This uses the language extensions defined in
both the default languages.toml distributed with Helix, as well as user defined languages.
- `@injection.shebang` (extension):
The captured node may contain a shebang used to choose a language to highlight as. This also uses
the shebangs defined in the default and user `languages.toml`.
## Settings
- `injection.combined` (standard):
Indicates that all the matching nodes in the tree should have their content parsed as one
nested document.
- `injection.language` (standard):
Forces the captured content to be highlighted as the given language
- `injection.include-children` (standard):
Indicates that the content nodes entire text should be re-parsed, including the text of its child
nodes. By default, child nodes text will be excluded from the injected document.
- `injection.include-unnamed-children` (extension):
Same as `injection.include-children` but only for unnamed child nodes.
## Predicates
- `#eq?` (standard):
The first argument (a capture) must be equal to the second argument
(a capture or a string).
- `#match?` (standard):
The first argument (a capture) must match the regex given in the
second argument (a string).
[upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection

@ -6,17 +6,23 @@
- [Linux](#linux)
- [Ubuntu](#ubuntu)
- [Fedora/RHEL](#fedorarhel)
- [Arch Linux community](#arch-linux-community)
- [Arch Linux extra](#arch-linux-extra)
- [NixOS](#nixos)
- [Flatpak](#flatpak)
- [Snap](#snap)
- [AppImage](#appimage)
- [macOS](#macos)
- [Homebrew Core](#homebrew-core)
- [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-->
@ -40,8 +46,6 @@ line.
## Linux, macOS, Windows and OpenBSD packaging status
Helix is available for Linux, macOS and Windows via the official repositories listed below.
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## Linux
@ -50,7 +54,7 @@ The following third party repositories are available:
### Ubuntu
Helix is available via [Maveonair's PPA](https://launchpad.net/~maveonair/+archive/ubuntu/helix-editor):
Add the `PPA` for Helix:
```sh
sudo add-apt-repository ppa:maveonair/helix-editor
@ -60,16 +64,16 @@ sudo apt install helix
### Fedora/RHEL
Helix is available via `copr`:
Enable the `COPR` repository for Helix:
```sh
sudo dnf copr enable varlad/helix
sudo dnf install helix
```
### Arch Linux community
### Arch Linux extra
Releases are available in the `community` repository:
Releases are available in the `extra` repository:
```sh
sudo pacman -S helix
@ -79,7 +83,10 @@ in the AUR, which builds the master branch.
### NixOS
Helix is available as a [flake](https://nixos.wiki/wiki/Flakes) in the project
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
@ -89,10 +96,29 @@ 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 [AppImage](https://appimage.org/).
Download Helix AppImage from the [latest releases](https://github.com/helix-editor/helix/releases/latest) page.
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
@ -109,9 +135,17 @@ brew install helix
## Windows
Install on Windows using [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/)
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
@ -134,33 +168,41 @@ pacman -S mingw-w64-ucrt-x86_64-helix
## Building from source
Clone the repository:
Requirements:
```sh
git clone https://github.com/helix-editor/helix
cd helix
```
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
Compile from source:
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
cargo install --path helix-term --locked
RUSTFLAGS="-C target-feature=-crt-static"
```
This command will create the `hx` executable and construct the tree-sitter
grammars in the local `runtime` folder. To build the tree-sitter grammars requires
a c++ compiler to be installed, for example `gcc-c++`.
1. Clone the repository:
```sh
git clone https://github.com/helix-editor/helix
cd helix
```
> 💡 If you are using the musl-libc 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"
> ```
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` (requires `git`) and compile them with
> `hx --grammar build` (requires a C++ compiler). This will install them in
> 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)).
@ -168,18 +210,22 @@ a c++ compiler to be installed, for example `gcc-c++`.
#### Linux and macOS
Either set the `HELIX_RUNTIME` environment variable to point to the runtime files and add it to your `~/.bashrc` or equivalent:
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=/home/user-name/src/helix/runtime
HELIX_RUNTIME=~/src/helix/runtime
```
Or, create a symlink in `~/.config/helix` that links to the source code directory:
Or, create a symbolic link:
```sh
ln -s $PWD/runtime ~/.config/helix/runtime
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

@ -15,7 +15,7 @@
- [Popup](#popup)
- [Unimpaired](#unimpaired)
- [Insert mode](#insert-mode)
- [Select / extend mode](#select-extend-mode)
- [Select / extend mode](#select--extend-mode)
- [Picker](#picker)
- [Prompt](#prompt)
@ -25,6 +25,8 @@
## Normal mode
Normal mode is the default mode when you launch helix. Return to it from other modes by typing `Escape`.
### Movement
> NOTE: Unlike Vim, `f`, `F`, `t` and `T` are not confined to the current line.
@ -32,8 +34,8 @@
| Key | Description | Command |
| ----- | ----------- | ------- |
| `h`, `Left` | Move left | `move_char_left` |
| `j`, `Down` | Move down | `move_line_down` |
| `k`, `Up` | Move up | `move_line_up` |
| `j`, `Down` | Move down | `move_visual_line_down` |
| `k`, `Up` | Move up | `move_visual_line_up` |
| `l`, `Right` | Move right | `move_char_right` |
| `w` | Move next word start | `move_next_word_start` |
| `b` | Move previous word start | `move_prev_word_start` |
@ -111,7 +113,8 @@
| `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into sub selections on regex matches | `split_selection` |
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `Alt-_ ` | Merge consecutive selections | `merge_consecutive_selections` |
| `Alt-minus` | Merge selections | `merge_selections` |
| `Alt-_` | Merge consecutive selections | `merge_consecutive_selections` |
| `&` | Align selection in columns | `align_selections` |
| `_` | Trim whitespace from the selection | `trim_selections` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
@ -218,6 +221,8 @@ Jumps to various locations.
| `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | Go to last modification in current file | `goto_last_modification` |
| `j` | Move down textual (instead of visual) line | `move_line_down` |
| `k` | Move up textual (instead of visual) line | `move_line_up` |
#### Match mode
@ -286,7 +291,7 @@ This layer is a kludge of mappings, mostly pickers.
| `w` | Enter [window mode](#window-mode) | N/A |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `y` | Yank selections to clipboard | `yank_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
@ -334,6 +339,8 @@ These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim
## Insert mode
Accessed by typing `i` in [normal mode](#normal-mode).
Insert mode bindings are minimal by default. Helix is designed to
be a modal editor, and this is reflected in the user experience and internal
mechanics. Changes to the text are only saved for undos when
@ -387,9 +394,11 @@ end = "no_op"
## Select / extend mode
Accessed by typing `v` in [normal mode](#normal-mode).
Select mode echoes Normal mode, but changes any movements to extend
selections rather than replace them. Goto motions are also changed to
extend, so that `vgl` for example extends the selection to the end of
extend, so that `vgl`, for example, extends the selection to the end of
the line.
Search is also affected. By default, `n` and `N` will remove the current
@ -402,19 +411,20 @@ you to selectively add search terms to your selections.
Keys to use within picker. Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry |
| `Tab`, `Down`, `Ctrl-n` | Next entry |
| `PageUp`, `Ctrl-u` | Page up |
| `PageDown`, `Ctrl-d` | Page down |
| `Home` | Go to first entry |
| `End` | Go to last entry |
| `Enter` | Open selected |
| `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically |
| `Ctrl-t` | Toggle preview |
| `Escape`, `Ctrl-c` | Close picker |
| Key | Description |
| ----- | ------------- |
| `Shift-Tab`, `Up`, `Ctrl-p` | Previous entry |
| `Tab`, `Down`, `Ctrl-n` | Next entry |
| `PageUp`, `Ctrl-u` | Page up |
| `PageDown`, `Ctrl-d` | Page down |
| `Home` | Go to first entry |
| `End` | Go to last entry |
| `Enter` | Open selected |
| `Alt-Enter` | Open selected in the background without closing the picker |
| `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically |
| `Ctrl-t` | Toggle preview |
| `Escape`, `Ctrl-c` | Close picker |
## Prompt

@ -7,21 +7,24 @@ in `languages.toml` files.
There are three possible locations for a `languages.toml` file:
1. In the Helix source code, this lives in the
1. In the Helix source code, which lives in the
[Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml).
It provides the default configurations for languages and language servers.
2. In your [configuration directory](./configuration.md). This overrides values
from the built-in language configuration. For example to disable
from the built-in language configuration. For example, to disable
auto-LSP-formatting in Rust:
```toml
# in <config_dir>/helix/languages.toml
```toml
# in <config_dir>/helix/languages.toml
[[language]]
name = "rust"
auto-format = false
```
[language-server.mylang-lsp]
command = "mylang-lsp"
[[language]]
name = "rust"
auto-format = false
```
3. In a `.helix` folder in your project. Language configuration may also be
overridden local to a project by creating a `languages.toml` file in a
@ -41,8 +44,8 @@ injection-regex = "mylang"
file-types = ["mylang", "myl"]
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "mylang-lsp", args = ["--stdio"], environment = { "ENV1" = "value1", "ENV2" = "value2" } }
formatter = { command = "mylang-formatter" , args = ["--stdin"] }
language-servers = [ "mylang-lsp" ]
```
These configuration keys are available:
@ -50,6 +53,7 @@ These configuration keys are available:
| Key | Description |
| ---- | ----------- |
| `name` | The name of the language |
| `language-id` | The language-id for language servers, checkout the table at [TextDocumentItem](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem) for the right id |
| `scope` | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
| `injection-regex` | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
| `file-types` | The filetypes of the language, for example `["yml", "yaml"]`. See the file-type detection section below. |
@ -59,11 +63,10 @@ These configuration keys are available:
| `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) |
| `comment-token` | The token to use as a comment-token |
| `indent` | The indent to use. Has sub keys `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-server` | The Language Server to run. See the Language Server configuration section below. |
| `config` | Language Server configuration |
| `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`) |
| `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout |
| `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. |
### File-type detection and the `file-types` key
@ -92,31 +95,102 @@ with the following priorities:
replaced at runtime with the appropriate path separator for the operating
system, so this rule would match against `.git\config` files on Windows.
### Language Server configuration
## Language Server configuration
The `language-server` field takes the following keys:
Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`
For example:
```toml
[language-server.mylang-lsp]
command = "mylang-lsp"
args = ["--stdio"]
config = { provideFormatter = true }
environment = { "ENV1" = "value1", "ENV2" = "value2" }
[language-server.efm-lsp-prettier]
command = "efm-langserver"
[language-server.efm-lsp-prettier.config]
documentFormatting = true
languages = { typescript = [ { formatCommand ="prettier --stdin-filepath ${INPUT}", formatStdin = true } ] }
```
| Key | Description |
| --- | ----------- |
| `command` | The name of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `timeout` | The maximum time a request to the language server may take, in seconds. Defaults to `20` |
| `language-id` | The language name to pass to the language server. Some language servers support multiple languages and use this field to determine which one is being served in a buffer |
| `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` |
These are the available options for a language server.
The top-level `config` field is used to configure the LSP initialization options. A `format`
sub-table within `config` can be used to pass extra formatting options to
[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-16.md#document-formatting-request--leftwards_arrow_with_hook).
For example with typescript:
| Key | Description |
| ---- | ----------- |
| `command` | The name or path of the language server binary to execute. Binaries must be in `$PATH` |
| `args` | A list of arguments to pass to the language server binary |
| `config` | LSP initialization options |
| `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" }` |
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).
For example, with typescript:
```toml
[[language]]
name = "typescript"
auto-format = true
[language-server.typescript-language-server]
# pass format options according to https://github.com/typescript-language-server/typescript-language-server#workspacedidchangeconfiguration omitting the "[language].format." prefix.
config = { format = { "semicolons" = "insert", "insertSpaceBeforeFunctionParenthesis" = true } }
```
### Configuring Language Servers for a language
The `language-servers` attribute in a language tells helix which language servers are used for this language.
They have to be defined in the `[language-server]` table as described in the previous section.
Different languages can use the same language server instance, e.g. `typescript-language-server` is used for javascript, jsx, tsx and typescript by default.
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.
As an example, `efm-lsp-prettier` of the previous example is used only with a formatting command `prettier`,
so everything else should be handled by the `typescript-language-server` (which is configured by default).
The language configuration for typescript could look like this:
```toml
[[language]]
name = "typescript"
language-servers = [ { name = "efm-lsp-prettier", only-features = [ "format" ] }, "typescript-language-server" ]
```
or equivalent:
```toml
[[language]]
name = "typescript"
language-servers = [ { name = "typescript-language-server", except-features = [ "format" ] }, "efm-lsp-prettier" ]
```
Each requested LSP feature is prioritized in the order of the `language-servers` array.
For example, the first `goto-definition` supported language server (in this case `typescript-language-server`) will be taken for the relevant LSP request (command `goto_definition`).
The features `diagnostics`, `code-action`, `completion`, `document-symbols` and `workspace-symbols` are an exception to that rule, as they are working for all language servers at the same time and are merged together, if enabled for the language.
If no `except-features` or `only-features` is given, all features for the language server are enabled.
If a language server itself doesn't support a feature, the next language server array entry will be tried (and so on).
The list of supported features is:
- `format`
- `goto-definition`
- `goto-declaration`
- `goto-type-definition`
- `goto-reference`
- `goto-implementation`
- `signature-help`
- `hover`
- `document-highlight`
- `completion`
- `code-action`
- `workspace-command`
- `document-symbols`
- `workspace-symbols`
- `diagnostics`
- `rename-symbol`
- `inlay-hints`
## Tree-sitter grammar configuration
The source for a language's tree-sitter grammar is specified in a `[[grammar]]`

@ -70,6 +70,7 @@ over it and is merged into the default palette.
| Color Name |
| --- |
| `default` |
| `black` |
| `red` |
| `green` |
@ -296,7 +297,7 @@ These scopes are used for theming the editor interface:
| `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. |
| `ui.text.focus` | |
| `ui.text.focus` | The currently selected line in the picker |
| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
| `ui.virtual.ruler` | Ruler columns (see the [`editor.rulers` config][editor-section]) |

@ -37,19 +37,35 @@ If a register is selected before invoking a change or delete command, the select
- `"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.
### Special registers
### 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 |
| `_` | Black hole |
| `@` | Last recorded macro |
### Special registers
Some registers have special behavior when read from and written to.
The system clipboard is not directly supported by a special register. Instead, special commands and keybindings are provided. Refer to the
[key map](keymap.md#space-mode) for more details.
| 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 |
The black hole register is a no-op register, meaning that no data will be read or written to it.
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
@ -96,13 +112,13 @@ function or block of code.
| `(`, `[`, `'`, etc. | Specified surround pairs |
| `m` | The closest surround pair |
| `f` | Function |
| `c` | Class |
| `t` | Type (or Class) |
| `a` | Argument/parameter |
| `o` | Comment |
| `t` | Test |
| `c` | Comment |
| `T` | Test |
| `g` | Change |
> 💡 `f`, `c`, etc. need a tree-sitter grammar active for the current
> 💡 `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!
@ -112,7 +128,7 @@ Contributions are welcome!
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
class use `[c`, and so on.
type use `[t`, and so on.
![Tree-sitter-nav-demo][tree-sitter-nav-demo]

@ -1,660 +0,0 @@
"use strict";
// Fix back button cache problem
window.onunload = function () { };
// Global variable, shared between modules
function playground_text(playground) {
let code_block = playground.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
return editor.getValue();
} else {
return code_block.textContent;
}
}
(function codeSnippets() {
function fetch_with_timeout(url, options, timeout = 6000) {
return Promise.race([
fetch(url, options),
new Promise((_, reject) => setTimeout(() => reject(new Error('timeout')), timeout))
]);
}
var playgrounds = Array.from(document.querySelectorAll(".playground"));
if (playgrounds.length > 0) {
fetch_with_timeout("https://play.rust-lang.org/meta/crates", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
})
.then(response => response.json())
.then(response => {
// get list of crates available in the rust playground
let playground_crates = response.crates.map(item => item["id"]);
playgrounds.forEach(block => handle_crate_list_update(block, playground_crates));
});
}
function handle_crate_list_update(playground_block, playground_crates) {
// update the play buttons after receiving the response
update_play_button(playground_block, playground_crates);
// and install on change listener to dynamically update ACE editors
if (window.ace) {
let code_block = playground_block.querySelector("code");
if (code_block.classList.contains("editable")) {
let editor = window.ace.edit(code_block);
editor.addEventListener("change", function (e) {
update_play_button(playground_block, playground_crates);
});
// add Ctrl-Enter command to execute rust code
editor.commands.addCommand({
name: "run",
bindKey: {
win: "Ctrl-Enter",
mac: "Ctrl-Enter"
},
exec: _editor => run_rust_code(playground_block)
});
}
}
}
// updates the visibility of play button based on `no_run` class and
// used crates vs ones available on http://play.rust-lang.org
function update_play_button(pre_block, playground_crates) {
var play_button = pre_block.querySelector(".play-button");
// skip if code is `no_run`
if (pre_block.querySelector('code').classList.contains("no_run")) {
play_button.classList.add("hidden");
return;
}
// get list of `extern crate`'s from snippet
var txt = playground_text(pre_block);
var re = /extern\s+crate\s+([a-zA-Z_0-9]+)\s*;/g;
var snippet_crates = [];
var item;
while (item = re.exec(txt)) {
snippet_crates.push(item[1]);
}
// check if all used crates are available on play.rust-lang.org
var all_available = snippet_crates.every(function (elem) {
return playground_crates.indexOf(elem) > -1;
});
if (all_available) {
play_button.classList.remove("hidden");
} else {
play_button.classList.add("hidden");
}
}
function run_rust_code(code_block) {
var result_block = code_block.querySelector(".result");
if (!result_block) {
result_block = document.createElement('code');
result_block.className = 'result hljs language-bash';
code_block.append(result_block);
}
let text = playground_text(code_block);
let classes = code_block.querySelector('code').classList;
let has_2018 = classes.contains("edition2018");
let edition = has_2018 ? "2018" : "2015";
var params = {
version: "stable",
optimize: "0",
code: text,
edition: edition
};
if (text.indexOf("#![feature") !== -1) {
params.version = "nightly";
}
result_block.innerText = "Running...";
fetch_with_timeout("https://play.rust-lang.org/evaluate.json", {
headers: {
'Content-Type': "application/json",
},
method: 'POST',
mode: 'cors',
body: JSON.stringify(params)
})
.then(response => response.json())
.then(response => result_block.innerText = response.result)
.catch(error => result_block.innerText = "Playground Communication: " + error.message);
}
// Syntax highlighting Configuration
hljs.configure({
tabReplace: ' ', // 4 spaces
languages: [], // Languages used for auto-detection
});
let code_nodes = Array
.from(document.querySelectorAll('code'))
// Don't highlight `inline code` blocks in headers.
.filter(function (node) {return !node.parentElement.classList.contains("header"); });
if (window.ace) {
// language-rust class needs to be removed for editable
// blocks or highlightjs will capture events
Array
.from(document.querySelectorAll('code.editable'))
.forEach(function (block) { block.classList.remove('language-rust'); });
Array
.from(document.querySelectorAll('code:not(.editable)'))
.forEach(function (block) { hljs.highlightBlock(block); });
} else {
code_nodes.forEach(function (block) { hljs.highlightBlock(block); });
}
// Adding the hljs class gives code blocks the color css
// even if highlighting doesn't apply
code_nodes.forEach(function (block) { block.classList.add('hljs'); });
Array.from(document.querySelectorAll("code.language-rust")).forEach(function (block) {
var lines = Array.from(block.querySelectorAll('.boring'));
// If no lines were hidden, return
if (!lines.length) { return; }
block.classList.add("hide-boring");
var buttons = document.createElement('div');
buttons.className = 'buttons';
buttons.innerHTML = "<button class=\"fa fa-eye\" title=\"Show hidden lines\" aria-label=\"Show hidden lines\"></button>";
// add expand button
var pre_block = block.parentNode;
pre_block.insertBefore(buttons, pre_block.firstChild);
pre_block.querySelector('.buttons').addEventListener('click', function (e) {
if (e.target.classList.contains('fa-eye')) {
e.target.classList.remove('fa-eye');
e.target.classList.add('fa-eye-slash');
e.target.title = 'Hide lines';
e.target.setAttribute('aria-label', e.target.title);
block.classList.remove('hide-boring');
} else if (e.target.classList.contains('fa-eye-slash')) {
e.target.classList.remove('fa-eye-slash');
e.target.classList.add('fa-eye');
e.target.title = 'Show hidden lines';
e.target.setAttribute('aria-label', e.target.title);
block.classList.add('hide-boring');
}
});
});
if (window.playground_copyable) {
Array.from(document.querySelectorAll('pre code')).forEach(function (block) {
var pre_block = block.parentNode;
if (!pre_block.classList.contains('playground')) {
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var clipButton = document.createElement('button');
clipButton.className = 'fa fa-copy clip-button';
clipButton.title = 'Copy to clipboard';
clipButton.setAttribute('aria-label', clipButton.title);
clipButton.innerHTML = '<i class=\"tooltiptext\"></i>';
buttons.insertBefore(clipButton, buttons.firstChild);
}
});
}
// Process playground code blocks
Array.from(document.querySelectorAll(".playground")).forEach(function (pre_block) {
// Add play button
var buttons = pre_block.querySelector(".buttons");
if (!buttons) {
buttons = document.createElement('div');
buttons.className = 'buttons';
pre_block.insertBefore(buttons, pre_block.firstChild);
}
var runCodeButton = document.createElement('button');
runCodeButton.className = 'fa fa-play play-button';
runCodeButton.hidden = true;
runCodeButton.title = 'Run this code';
runCodeButton.setAttribute('aria-label', runCodeButton.title);
buttons.insertBefore(runCodeButton, buttons.firstChild);
runCodeButton.addEventListener('click', function (e) {
run_rust_code(pre_block);
});
if (window.playground_copyable) {
var copyCodeClipboardButton = document.createElement('button');
copyCodeClipboardButton.className = 'fa fa-copy clip-button';
copyCodeClipboardButton.innerHTML = '<i class="tooltiptext"></i>';
copyCodeClipboardButton.title = 'Copy to clipboard';
copyCodeClipboardButton.setAttribute('aria-label', copyCodeClipboardButton.title);
buttons.insertBefore(copyCodeClipboardButton, buttons.firstChild);
}
let code_block = pre_block.querySelector("code");
if (window.ace && code_block.classList.contains("editable")) {
var undoChangesButton = document.createElement('button');
undoChangesButton.className = 'fa fa-history reset-button';
undoChangesButton.title = 'Undo changes';
undoChangesButton.setAttribute('aria-label', undoChangesButton.title);
buttons.insertBefore(undoChangesButton, buttons.firstChild);
undoChangesButton.addEventListener('click', function () {
let editor = window.ace.edit(code_block);
editor.setValue(editor.originalCode);
editor.clearSelection();
});
}
});
})();
(function themes() {
var html = document.querySelector('html');
var themeToggleButton = document.getElementById('theme-toggle');
var themePopup = document.getElementById('theme-list');
var themeColorMetaTag = document.querySelector('meta[name="theme-color"]');
var stylesheets = {
ayuHighlight: document.querySelector("[href$='ayu-highlight.css']"),
tomorrowNight: document.querySelector("[href$='tomorrow-night.css']"),
highlight: document.querySelector("[href$='highlight.css']"),
};
function showThemes() {
themePopup.style.display = 'block';
themeToggleButton.setAttribute('aria-expanded', true);
themePopup.querySelector("button#" + get_theme()).focus();
}
function hideThemes() {
themePopup.style.display = 'none';
themeToggleButton.setAttribute('aria-expanded', false);
themeToggleButton.focus();
}
function get_theme() {
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch (e) { }
if (theme === null || theme === undefined) {
return default_theme;
} else {
return theme;
}
}
function set_theme(theme, store = true) {
let ace_theme;
if (theme == 'coal' || theme == 'navy') {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = false;
stylesheets.highlight.disabled = true;
ace_theme = "ace/theme/tomorrow_night";
} else if (theme == 'ayu') {
stylesheets.ayuHighlight.disabled = false;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = true;
ace_theme = "ace/theme/tomorrow_night";
} else {
stylesheets.ayuHighlight.disabled = true;
stylesheets.tomorrowNight.disabled = true;
stylesheets.highlight.disabled = false;
ace_theme = "ace/theme/dawn";
}
setTimeout(function () {
themeColorMetaTag.content = getComputedStyle(document.body).backgroundColor;
}, 1);
if (window.ace && window.editors) {
window.editors.forEach(function (editor) {
editor.setTheme(ace_theme);
});
}
var previousTheme = get_theme();
if (store) {
try { localStorage.setItem('mdbook-theme', theme); } catch (e) { }
}
html.classList.remove(previousTheme);
html.classList.add(theme);
}
// Set theme
var theme = get_theme();
set_theme(theme, false);
themeToggleButton.addEventListener('click', function () {
if (themePopup.style.display === 'block') {
hideThemes();
} else {
showThemes();
}
});
themePopup.addEventListener('click', function (e) {
var theme = e.target.id || e.target.parentElement.id;
set_theme(theme);
});
themePopup.addEventListener('focusout', function(e) {
// e.relatedTarget is null in Safari and Firefox on macOS (see workaround below)
if (!!e.relatedTarget && !themeToggleButton.contains(e.relatedTarget) && !themePopup.contains(e.relatedTarget)) {
hideThemes();
}
});
// Should not be needed, but it works around an issue on macOS & iOS: https://github.com/rust-lang/mdBook/issues/628
document.addEventListener('click', function(e) {
if (themePopup.style.display === 'block' && !themeToggleButton.contains(e.target) && !themePopup.contains(e.target)) {
hideThemes();
}
});
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (!themePopup.contains(e.target)) { return; }
switch (e.key) {
case 'Escape':
e.preventDefault();
hideThemes();
break;
case 'ArrowUp':
e.preventDefault();
var li = document.activeElement.parentElement;
if (li && li.previousElementSibling) {
li.previousElementSibling.querySelector('button').focus();
}
break;
case 'ArrowDown':
e.preventDefault();
var li = document.activeElement.parentElement;
if (li && li.nextElementSibling) {
li.nextElementSibling.querySelector('button').focus();
}
break;
case 'Home':
e.preventDefault();
themePopup.querySelector('li:first-child button').focus();
break;
case 'End':
e.preventDefault();
themePopup.querySelector('li:last-child button').focus();
break;
}
});
})();
(function sidebar() {
var html = document.querySelector("html");
var sidebar = document.getElementById("sidebar");
var sidebarLinks = document.querySelectorAll('#sidebar a');
var sidebarToggleButton = document.getElementById("sidebar-toggle");
var sidebarResizeHandle = document.getElementById("sidebar-resize-handle");
var firstContact = null;
function showSidebar() {
html.classList.remove('sidebar-hidden')
html.classList.add('sidebar-visible');
Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', 0);
});
sidebarToggleButton.setAttribute('aria-expanded', true);
sidebar.setAttribute('aria-hidden', false);
try { localStorage.setItem('mdbook-sidebar', 'visible'); } catch (e) { }
}
var sidebarAnchorToggles = document.querySelectorAll('#sidebar a.toggle');
function toggleSection(ev) {
ev.currentTarget.parentElement.classList.toggle('expanded');
}
Array.from(sidebarAnchorToggles).forEach(function (el) {
el.addEventListener('click', toggleSection);
});
function hideSidebar() {
html.classList.remove('sidebar-visible')
html.classList.add('sidebar-hidden');
Array.from(sidebarLinks).forEach(function (link) {
link.setAttribute('tabIndex', -1);
});
sidebarToggleButton.setAttribute('aria-expanded', false);
sidebar.setAttribute('aria-hidden', true);
try { localStorage.setItem('mdbook-sidebar', 'hidden'); } catch (e) { }
}
// Toggle sidebar
sidebarToggleButton.addEventListener('click', function sidebarToggle() {
if (html.classList.contains("sidebar-hidden")) {
var current_width = parseInt(
document.documentElement.style.getPropertyValue('--sidebar-width'), 10);
if (current_width < 150) {
document.documentElement.style.setProperty('--sidebar-width', '150px');
}
showSidebar();
} else if (html.classList.contains("sidebar-visible")) {
hideSidebar();
} else {
if (getComputedStyle(sidebar)['transform'] === 'none') {
hideSidebar();
} else {
showSidebar();
}
}
});
sidebarResizeHandle.addEventListener('mousedown', initResize, false);
function initResize(e) {
window.addEventListener('mousemove', resize, false);
window.addEventListener('mouseup', stopResize, false);
html.classList.add('sidebar-resizing');
}
function resize(e) {
var pos = (e.clientX - sidebar.offsetLeft);
if (pos < 20) {
hideSidebar();
} else {
if (html.classList.contains("sidebar-hidden")) {
showSidebar();
}
pos = Math.min(pos, window.innerWidth - 100);
document.documentElement.style.setProperty('--sidebar-width', pos + 'px');
}
}
//on mouseup remove windows functions mousemove & mouseup
function stopResize(e) {
html.classList.remove('sidebar-resizing');
window.removeEventListener('mousemove', resize, false);
window.removeEventListener('mouseup', stopResize, false);
}
document.addEventListener('touchstart', function (e) {
firstContact = {
x: e.touches[0].clientX,
time: Date.now()
};
}, { passive: true });
document.addEventListener('touchmove', function (e) {
if (!firstContact)
return;
var curX = e.touches[0].clientX;
var xDiff = curX - firstContact.x,
tDiff = Date.now() - firstContact.time;
if (tDiff < 250 && Math.abs(xDiff) >= 150) {
if (xDiff >= 0 && firstContact.x < Math.min(document.body.clientWidth * 0.25, 300))
showSidebar();
else if (xDiff < 0 && curX < 300)
hideSidebar();
firstContact = null;
}
}, { passive: true });
// Scroll sidebar to current active section
var activeSection = document.getElementById("sidebar").querySelector(".active");
if (activeSection) {
// https://developer.mozilla.org/en-US/docs/Web/API/Element/scrollIntoView
activeSection.scrollIntoView({ block: 'center' });
}
})();
(function chapterNavigation() {
document.addEventListener('keydown', function (e) {
if (e.altKey || e.ctrlKey || e.metaKey || e.shiftKey) { return; }
if (window.search && window.search.hasFocus()) { return; }
switch (e.key) {
case 'ArrowRight':
e.preventDefault();
var nextButton = document.querySelector('.nav-chapters.next');
if (nextButton) {
window.location.href = nextButton.href;
}
break;
case 'ArrowLeft':
e.preventDefault();
var previousButton = document.querySelector('.nav-chapters.previous');
if (previousButton) {
window.location.href = previousButton.href;
}
break;
}
});
})();
(function clipboard() {
var clipButtons = document.querySelectorAll('.clip-button');
function hideTooltip(elem) {
elem.firstChild.innerText = "";
elem.className = 'fa fa-copy clip-button';
}
function showTooltip(elem, msg) {
elem.firstChild.innerText = msg;
elem.className = 'fa fa-copy tooltipped';
}
var clipboardSnippets = new ClipboardJS('.clip-button', {
text: function (trigger) {
hideTooltip(trigger);
let playground = trigger.closest("pre");
return playground_text(playground);
}
});
Array.from(clipButtons).forEach(function (clipButton) {
clipButton.addEventListener('mouseout', function (e) {
hideTooltip(e.currentTarget);
});
});
clipboardSnippets.on('success', function (e) {
e.clearSelection();
showTooltip(e.trigger, "Copied!");
});
clipboardSnippets.on('error', function (e) {
showTooltip(e.trigger, "Clipboard error!");
});
})();
(function scrollToTop () {
var menuTitle = document.querySelector('.menu-title');
menuTitle.addEventListener('click', function () {
document.scrollingElement.scrollTo({ top: 0, behavior: 'smooth' });
});
})();
(function controlMenu() {
var menu = document.getElementById('menu-bar');
(function controlPosition() {
var scrollTop = document.scrollingElement.scrollTop;
var prevScrollTop = scrollTop;
var minMenuY = -menu.clientHeight - 50;
// When the script loads, the page can be at any scroll (e.g. if you reforesh it).
menu.style.top = scrollTop + 'px';
// Same as parseInt(menu.style.top.slice(0, -2), but faster
var topCache = menu.style.top.slice(0, -2);
menu.classList.remove('sticky');
var stickyCache = false; // Same as menu.classList.contains('sticky'), but faster
document.addEventListener('scroll', function () {
scrollTop = Math.max(document.scrollingElement.scrollTop, 0);
// `null` means that it doesn't need to be updated
var nextSticky = null;
var nextTop = null;
var scrollDown = scrollTop > prevScrollTop;
var menuPosAbsoluteY = topCache - scrollTop;
if (scrollDown) {
nextSticky = false;
if (menuPosAbsoluteY > 0) {
nextTop = prevScrollTop;
}
} else {
if (menuPosAbsoluteY > 0) {
nextSticky = true;
} else if (menuPosAbsoluteY < minMenuY) {
nextTop = prevScrollTop + minMenuY;
}
}
if (nextSticky === true && stickyCache === false) {
menu.classList.add('sticky');
stickyCache = true;
} else if (nextSticky === false && stickyCache === true) {
menu.classList.remove('sticky');
stickyCache = false;
}
if (nextTop !== null) {
menu.style.top = nextTop + 'px';
topCache = nextTop;
}
prevScrollTop = scrollTop;
}, { passive: true });
})();
(function controlBorder() {
menu.classList.remove('bordered');
document.addEventListener('scroll', function () {
if (menu.offsetTop === 0) {
menu.classList.remove('bordered');
} else {
menu.classList.add('bordered');
}
}, { passive: true });
})();
})();

@ -1,499 +0,0 @@
/* CSS for UI elements (a.k.a. chrome) */
@import 'variables.css';
::-webkit-scrollbar {
background: var(--bg);
}
::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
html {
scrollbar-color: var(--scrollbar) var(--bg);
}
#searchresults a,
.content a:link,
a:visited,
a > .hljs {
color: var(--links);
}
.content a:hover {
text-decoration: underline;
}
/* Menu Bar */
#menu-bar,
#menu-bar-hover-placeholder {
z-index: 101;
margin: auto calc(0px - var(--page-padding));
}
#menu-bar {
position: relative;
display: flex;
flex-wrap: wrap;
background-color: var(--bg);
border-bottom-color: var(--bg);
border-bottom-width: 1px;
border-bottom-style: solid;
}
#menu-bar.sticky,
.js #menu-bar-hover-placeholder:hover + #menu-bar,
.js #menu-bar:hover,
.js.sidebar-visible #menu-bar {
position: -webkit-sticky;
position: sticky;
top: 0 !important;
}
#menu-bar-hover-placeholder {
position: sticky;
position: -webkit-sticky;
top: 0;
height: var(--menu-bar-height);
}
#menu-bar.bordered {
border-bottom-color: var(--table-border-color);
}
#menu-bar i, #menu-bar .icon-button {
position: relative;
padding: 0 8px;
z-index: 10;
line-height: var(--menu-bar-height);
cursor: pointer;
transition: color 0.5s;
}
@media only screen and (max-width: 420px) {
#menu-bar i, #menu-bar .icon-button {
padding: 0 5px;
}
}
.icon-button {
border: none;
background: none;
padding: 0;
color: inherit;
}
.icon-button i {
margin: 0;
}
.right-buttons {
margin: 0 15px;
}
.right-buttons a {
text-decoration: none;
}
.left-buttons {
display: flex;
margin: 0 5px;
}
.no-js .left-buttons {
display: none;
}
.menu-title {
display: inline-block;
font-weight: 200;
font-size: 2.4rem;
line-height: var(--menu-bar-height);
text-align: center;
margin: 0;
flex: 1;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.js .menu-title {
cursor: pointer;
}
.menu-bar,
.menu-bar:visited,
.nav-chapters,
.nav-chapters:visited,
.mobile-nav-chapters,
.mobile-nav-chapters:visited,
.menu-bar .icon-button,
.menu-bar a i {
color: var(--icons);
}
.menu-bar i:hover,
.menu-bar .icon-button:hover,
.nav-chapters:hover,
.mobile-nav-chapters i:hover {
color: var(--icons-hover);
}
/* Nav Icons */
.nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
position: fixed;
top: 0;
bottom: 0;
margin: 0;
max-width: 150px;
min-width: 90px;
display: flex;
justify-content: center;
align-content: center;
flex-direction: column;
transition: color 0.5s, background-color 0.5s;
}
.nav-chapters:hover {
text-decoration: none;
background-color: var(--theme-hover);
transition: background-color 0.15s, color 0.15s;
}
.nav-wrapper {
margin-top: 50px;
display: none;
}
.mobile-nav-chapters {
font-size: 2.5em;
text-align: center;
text-decoration: none;
width: 90px;
border-radius: 5px;
background-color: var(--sidebar-bg);
}
.previous {
float: left;
}
.next {
float: right;
right: var(--page-padding);
}
@media only screen and (max-width: 1080px) {
.nav-wide-wrapper { display: none; }
.nav-wrapper { display: block; }
}
@media only screen and (max-width: 1380px) {
.sidebar-visible .nav-wide-wrapper { display: none; }
.sidebar-visible .nav-wrapper { display: block; }
}
/* Inline code */
:not(pre) > .hljs {
display: inline;
padding: 0.1em 0.3em;
border-radius: 3px;
}
:not(pre):not(a):not(td):not(p) > .hljs {
color: var(--inline-code-color);
overflow-x: initial;
}
a:hover > .hljs {
text-decoration: underline;
}
pre {
position: relative;
}
pre > .buttons {
position: absolute;
z-index: 100;
right: 5px;
top: 5px;
color: var(--sidebar-fg);
cursor: pointer;
}
pre > .buttons :hover {
color: var(--sidebar-active);
}
pre > .buttons i {
margin-left: 8px;
}
pre > .buttons button {
color: inherit;
background: transparent;
border: none;
cursor: inherit;
}
pre > .result {
margin-top: 10px;
}
/* Search */
#searchresults a {
text-decoration: none;
}
mark {
border-radius: 2px;
padding: 0 3px 1px 3px;
margin: 0 -3px -1px -3px;
background-color: var(--search-mark-bg);
transition: background-color 300ms linear;
cursor: pointer;
}
mark.fade-out {
background-color: rgba(0,0,0,0) !important;
cursor: auto;
}
.searchbar-outer {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
}
#searchbar {
width: 100%;
margin: 5px auto 0px auto;
padding: 10px 16px;
transition: box-shadow 300ms ease-in-out;
border: 1px solid var(--searchbar-border-color);
border-radius: 3px;
background-color: var(--searchbar-bg);
color: var(--searchbar-fg);
}
#searchbar:focus,
#searchbar.active {
box-shadow: 0 0 3px var(--searchbar-shadow-color);
}
.searchresults-header {
font-weight: bold;
font-size: 1em;
padding: 18px 0 0 5px;
color: var(--searchresults-header-fg);
}
.searchresults-outer {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
border-bottom: 1px dashed var(--searchresults-border-color);
}
ul#searchresults {
list-style: none;
padding-left: 20px;
}
ul#searchresults li {
margin: 10px 0px;
padding: 2px;
border-radius: 2px;
}
ul#searchresults li.focus {
background-color: var(--searchresults-li-bg);
}
ul#searchresults span.teaser {
display: block;
clear: both;
margin: 5px 0 0 20px;
font-size: 0.8em;
}
ul#searchresults span.teaser em {
font-weight: bold;
font-style: normal;
}
/* Sidebar */
.sidebar {
position: fixed;
left: 0;
top: 0;
bottom: 0;
width: var(--sidebar-width);
font-size: 0.875em;
box-sizing: border-box;
-webkit-overflow-scrolling: touch;
overscroll-behavior-y: contain;
background-color: var(--sidebar-bg);
color: var(--sidebar-fg);
}
.sidebar-resizing {
-moz-user-select: none;
-webkit-user-select: none;
-ms-user-select: none;
user-select: none;
}
.js:not(.sidebar-resizing) .sidebar {
transition: transform 0.3s; /* Animation: slide away */
}
.sidebar code {
line-height: 2em;
}
.sidebar .sidebar-scrollbox {
overflow-y: auto;
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
}
.sidebar .sidebar-resize-handle {
position: absolute;
cursor: col-resize;
width: 0;
right: 0;
top: 0;
bottom: 0;
}
.js .sidebar .sidebar-resize-handle {
cursor: col-resize;
width: 5px;
}
.sidebar-hidden .sidebar {
transform: translateX(calc(0px - var(--sidebar-width)));
}
.sidebar::-webkit-scrollbar {
background: var(--sidebar-bg);
}
.sidebar::-webkit-scrollbar-thumb {
background: var(--scrollbar);
}
.sidebar-visible .page-wrapper {
transform: translateX(var(--sidebar-width));
}
@media only screen and (min-width: 620px) {
.sidebar-visible .page-wrapper {
transform: none;
margin-left: var(--sidebar-width);
}
}
.chapter {
list-style: none outside none;
padding-left: 0;
margin: .25rem 0;
}
.chapter ol {
width: 100%;
}
.chapter li {
display: flex;
color: var(--sidebar-non-existent);
}
.chapter li a {
display: block;
text-decoration: none;
color: var(--sidebar-fg);
}
.chapter li a:hover {
color: var(--sidebar-active);
}
.chapter li a.active {
color: var(--sidebar-active);
}
.chapter li > a.toggle {
cursor: pointer;
display: block;
margin-left: auto;
padding: 0 10px;
user-select: none;
opacity: 0.68;
}
.chapter li > a.toggle div {
transition: transform 0.5s;
}
/* collapse the section */
.chapter li:not(.expanded) + li > ol {
display: none;
}
.chapter li.chapter-item {
padding: 1rem 1.5rem;
}
.chapter .section li.chapter-item {
padding: .5rem .5rem 0 .5rem;
}
.chapter li.expanded > a.toggle div {
transform: rotate(90deg);
}
.spacer {
width: 100%;
height: 3px;
margin: 5px 0px;
}
.chapter .spacer {
background-color: var(--sidebar-spacer);
}
@media (-moz-touch-enabled: 1), (pointer: coarse) {
.chapter li a { padding: 5px 0; }
.spacer { margin: 10px 0; }
}
.section {
list-style: none outside none;
padding-left: 2rem;
line-height: 1.9em;
}
/* Theme Menu Popup */
.theme-popup {
position: absolute;
left: 10px;
top: var(--menu-bar-height);
z-index: 1000;
border-radius: 4px;
font-size: 0.7em;
color: var(--fg);
background: var(--theme-popup-bg);
border: 1px solid var(--theme-popup-border);
margin: 0;
padding: 0;
list-style: none;
display: none;
}
.theme-popup .default {
color: var(--icons);
}
.theme-popup .theme {
width: 100%;
border: 0;
margin: 0;
padding: 2px 10px;
line-height: 25px;
white-space: nowrap;
text-align: left;
cursor: pointer;
color: inherit;
background: inherit;
font-size: inherit;
}
.theme-popup .theme:hover {
background-color: var(--theme-hover);
}
.theme-popup .theme:hover:first-child,
.theme-popup .theme:hover:last-child {
border-top-left-radius: inherit;
border-top-right-radius: inherit;
}

@ -1,233 +0,0 @@
/* Base styles and content styles */
@import 'variables.css';
:root {
/* Browser default font-size is 16px, this way 1 rem = 10px */
font-size: 62.5%;
}
/* TODO: replace with self hosted fonts */
html {
font-family: "Inter", sans-serif;
color: var(--fg);
background-color: var(--bg);
text-size-adjust: none;
}
/* @supports (font-variation-settings: normal) { */
/* html { font-family: 'Inter var', sans-serif; } */
/* } */
body {
margin: 0;
font-size: 1.6rem;
overflow-x: hidden;
}
code {
font-family: "Source Code Pro", Consolas, "Ubuntu Mono", Menlo, "DejaVu Sans Mono", monospace, monospace !important;
font-size: 0.875em; /* please adjust the ace font size accordingly in editor.js */
}
/* Don't change font size in headers. */
h1 code, h2 code, h3 code, h4 code, h5 code, h6 code {
font-size: unset;
}
.left { float: left; }
.right { float: right; }
.boring { opacity: 0.6; }
.hide-boring .boring { display: none; }
.hidden { display: none !important; }
h2, h3 { margin-top: 2.5em; }
h4, h5 { margin-top: 2em; }
.header + .header h3,
.header + .header h4,
.header + .header h5 {
margin-top: 1em;
}
h1:target::before,
h2:target::before,
h3:target::before,
h4:target::before,
h5:target::before,
h6:target::before {
display: inline-block;
content: "»";
margin-left: -30px;
width: 30px;
}
/* This is broken on Safari as of version 14, but is fixed
in Safari Technology Preview 117 which I think will be Safari 14.2.
https://bugs.webkit.org/show_bug.cgi?id=218076
*/
:target {
scroll-margin-top: calc(var(--menu-bar-height) + 0.5em);
}
.page {
outline: 0;
padding: 0 var(--page-padding);
margin-top: calc(0px - var(--menu-bar-height)); /* Compensate for the #menu-bar-hover-placeholder */
}
.page-wrapper {
box-sizing: border-box;
}
.js:not(.sidebar-resizing) .page-wrapper {
transition: margin-left 0.3s ease, transform 0.3s ease; /* Animation: slide away */
}
.content {
overflow-y: auto;
padding: 0 15px;
padding-bottom: 50px;
}
.content main {
margin-left: auto;
margin-right: auto;
max-width: var(--content-max-width);
}
/* 2 1.75 1.5 1.25 1 .875 */
.content h1 { font-size: 2em }
.content h2 { font-size: 1.75em }
.content h3 { font-size: 1.5em }
.content h4 { font-size: 1.25em }
.content h5 { font-size: 1em }
.content h6 { font-size: .875em }
.content h1, .content h2, .content h3, .content h4 {
font-weight: 500;
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p, .content ol, .content ul, .content table {
margin-top: 0;
margin-bottom: .875em;
}
.content ul li {
margin-bottom: .25rem;
}
.content ul {
list-style-type: square;
}
.content ul ul, .content ol ul {
margin-bottom: .5rem;
}
.content li p {
margin-bottom: .5em;
}
.content p { line-height: 1.45em; }
.content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; }
.content a { text-decoration: none; }
.content a:hover { text-decoration: underline; }
.content img { max-width: 100%; }
.content .header:link,
.content .header:visited {
color: var(--fg);
color: var(--heading-fg);
}
.content .header:link,
.content .header:visited:hover {
text-decoration: none;
}
table {
margin: 0 auto;
border-collapse: collapse;
width: 100%;
}
table td {
padding: .75rem;
width: auto;
}
table thead {
background: var(--table-header-bg);
}
table thead td {
font-weight: 700;
border: none;
}
table thead th {
padding: .75rem;
text-align: left;
font-weight: 500;
line-height: 1.5;
width: auto;
}
table thead tr {
border-bottom: 2px var(--table-border-color) solid;
}
table tbody tr {
border-bottom: 1px var(--table-border-line) solid;
}
/* Alternate background colors for rows */
table tbody tr:nth-child(2n) {
/* background: var(--table-alternate-bg); */
}
blockquote {
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-left: 4px solid var(--quote-border);
}
blockquote *:last-child {
margin-bottom: 0;
}
:not(.footnote-definition) + .footnote-definition,
.footnote-definition + :not(.footnote-definition) {
margin-top: 2em;
}
.footnote-definition {
font-size: 0.9em;
margin: 0.5em 0;
}
.footnote-definition p {
display: inline;
}
.tooltiptext {
position: absolute;
visibility: hidden;
color: #fff;
background-color: #333;
transform: translateX(-50%); /* Center by moving tooltip 50% of its width left */
left: -8px; /* Half of the width of the icon */
top: -35px;
font-size: 0.8em;
text-align: center;
border-radius: 6px;
padding: 5px 8px;
margin: 5px;
z-index: 1000;
}
.tooltipped .tooltiptext {
visibility: visible;
}
.chapter li.part-title {
color: var(--sidebar-fg);
margin: 5px 0px;
font-weight: bold;
}
.result-no-output {
font-style: italic;
}

@ -1,54 +0,0 @@
#sidebar,
#menu-bar,
.nav-chapters,
.mobile-nav-chapters {
display: none;
}
#page-wrapper.page-wrapper {
transform: none;
margin-left: 0px;
overflow-y: initial;
}
#content {
max-width: none;
margin: 0;
padding: 0;
}
.page {
overflow-y: initial;
}
code {
background-color: #666666;
border-radius: 5px;
/* Force background to be printed in Chrome */
-webkit-print-color-adjust: exact;
}
pre > .buttons {
z-index: 2;
}
a, a:visited, a:active, a:hover {
color: #4183c4;
text-decoration: none;
}
h1, h2, h3, h4, h5, h6 {
page-break-inside: avoid;
page-break-after: avoid;
}
pre, code {
page-break-inside: avoid;
white-space: pre-wrap;
}
.fa {
display: none !important;
}

@ -1,411 +0,0 @@
/* Globals */
:root {
--sidebar-width: 300px;
--page-padding: 15px;
--content-max-width: 750px;
--menu-bar-height: 50px;
}
/* Themes */
.ayu {
--bg: hsl(210, 25%, 8%);
--fg: #c5c5c5;
--sidebar-bg: #14191f;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #5c6773;
--sidebar-active: #ffb454;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #0096cf;
--inline-code-color: #ffb454;
--theme-popup-bg: #14191f;
--theme-popup-border: #5c6773;
--theme-hover: #191f26;
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(210, 25%, 13%);
--table-header-bg: hsl(210, 25%, 28%);
--table-alternate-bg: hsl(210, 25%, 11%);
--searchbar-border-color: #848484;
--searchbar-bg: #424242;
--searchbar-fg: #fff;
--searchbar-shadow-color: #d4c89f;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #252932;
--search-mark-bg: #e3b171;
--hljs-background: #191f26;
--hljs-color: #e6e1cf;
--hljs-quote: #5c6773;
--hljs-variable: #ff7733;
--hljs-type: #ffee99;
--hljs-title: #b8cc52;
--hljs-symbol: #ffb454;
--hljs-selector-tag: #ff7733;
--hljs-selector-tag: #36a3d9;
--hljs-selector-tag: #00568d;
--hljs-selector-tag: #91b362;
--hljs-selector-tag: #d96c75;
}
.coal {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
--sidebar-non-existent: #505254;
--sidebar-active: #3473ad;
--sidebar-spacer: #393939;
--scrollbar: var(--sidebar-fg);
--icons: #43484d;
--icons-hover: #b3c0cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%);
--table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
--hljs-background: #969896;
--hljs-color: #cc6666;
--hljs-quote: #de935f;
--hljs-variable: #f0c674;
--hljs-type: #b5bd68;
--hljs-title: #8abeb7;
--hljs-symbol: #81a2be;
--hljs-selector-tag: #b294bb;
--hljs-selector-tag: #1d1f21;
--hljs-selector-tag: #c5c8c6;
--hljs-selector-tag: #718c00;
--hljs-selector-tag: #c82829;
}
.light {
--bg: hsl(0, 0%, 100%);
--fg: hsl(0, 0%, 0%);
--sidebar-bg: #fafafa;
--sidebar-fg: hsl(0, 0%, 0%);
--sidebar-non-existent: #aaaaaa;
--sidebar-active: #1f1fff;
--sidebar-spacer: #f4f4f4;
--scrollbar: #8F8F8F;
--icons: #747474;
--icons-hover: #000000;
--links: #20609f;
--inline-code-color: #301900;
--theme-popup-bg: #fafafa;
--theme-popup-border: #cccccc;
--theme-hover: #e6e6e6;
--quote-bg: hsl(197, 37%, 96%);
--quote-border: hsl(197, 37%, 91%);
--table-border-color: hsl(0, 0%, 95%);
--table-header-bg: hsl(0, 0%, 80%);
--table-alternate-bg: hsl(0, 0%, 97%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #e4f2fe;
--search-mark-bg: #a2cff5;
--hljs-background: #f6f7f6;
--hljs-color: #000;
--hljs-quote: #575757;
--hljs-variable: #d70025;
--hljs-type: #b21e00;
--hljs-title: #0030f2;
--hljs-symbol: #008200;
--hljs-selector-tag: #9d00ec;
}
.navy {
--bg: hsl(226, 23%, 11%);
--fg: #bcbdd0;
--sidebar-bg: #282d3f;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #2b79a2;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: #282e40;
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 16%);
--table-header-bg: hsl(226, 23%, 31%);
--table-alternate-bg: hsl(226, 23%, 14%);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
--hljs-background: #969896;
--hljs-color: #cc6666;
--hljs-quote: #de935f;
--hljs-variable: #f0c674;
--hljs-type: #b5bd68;
--hljs-title: #8abeb7;
--hljs-symbol: #81a2be;
--hljs-selector-tag: #b294bb;
--hljs-selector-tag: #1d1f21;
--hljs-selector-tag: #c5c8c6;
--hljs-selector-tag: #718c00;
--hljs-selector-tag: #c82829;
}
.rust {
--bg: hsl(60, 9%, 87%);
--fg: #262625;
--sidebar-bg: #3b2e2a;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505254;
--sidebar-active: #e69f67;
--sidebar-spacer: #45373a;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #262625;
--links: #2b79a2;
--inline-code-color: #6e6b5e;
--theme-popup-bg: #e1e1db;
--theme-popup-border: #b38f6b;
--theme-hover: #99908a;
--quote-bg: hsl(60, 5%, 75%);
--quote-border: hsl(60, 5%, 70%);
--table-border-color: hsl(60, 9%, 82%);
--table-header-bg: #b3a497;
--table-alternate-bg: hsl(60, 9%, 84%);
--searchbar-border-color: #aaa;
--searchbar-bg: #fafafa;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #888;
--searchresults-li-bg: #dec2a2;
--search-mark-bg: #e69f67;
--hljs-background: #f6f7f6;
--hljs-color: #000;
--hljs-quote: #575757;
--hljs-variable: #d70025;
--hljs-type: #b21e00;
--hljs-title: #0030f2;
--hljs-symbol: #008200;
--hljs-selector-tag: #9d00ec;
}
@media (prefers-color-scheme: dark) {
.light.no-js {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
--sidebar-non-existent: #505254;
--sidebar-active: #3473ad;
--sidebar-spacer: #393939;
--scrollbar: var(--sidebar-fg);
--icons: #43484d;
--icons-hover: #b3c0cc;
--links: #2b79a2;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
--theme-hover: #1f2124;
--quote-bg: hsl(234, 21%, 18%);
--quote-border: hsl(234, 21%, 23%);
--table-border-color: hsl(200, 7%, 13%);
--table-header-bg: hsl(200, 7%, 28%);
--table-alternate-bg: hsl(200, 7%, 11%);
--searchbar-border-color: #aaa;
--searchbar-bg: #b7b7b7;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #666;
--searchresults-border-color: #98a3ad;
--searchresults-li-bg: #2b2b2f;
--search-mark-bg: #355c7d;
}
}
.colibri {
--bg: #3b224c;
--fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
/* --links: #a4a0e8; */
--links: #ECCDBA;
--inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #acff5;
--hljs-background: #2f1e2e;
--hljs-color: #a39e9b;
--hljs-quote: #8d8687;
--hljs-variable: #ef6155;
--hljs-type: #f99b15;
--hljs-title: #fec418;
--hljs-symbol: #48b685;
--hljs-selector-tag: #815ba4;
}
.colibri {
/*
--bg: #ffffff;
--fg: #452859;
--fg: #5a5977;
--heading-fg: #281733;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
--sidebar-non-existent: #505274;
--sidebar-active: #a4a0e8;
--sidebar-spacer: #2d334f;
--scrollbar: var(--sidebar-fg);
--icons: #737480;
--icons-hover: #b7b9cc;
--links: #6F44F0;
--inline-code-color: #a39e9b;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: rgba(0, 0, 0, 0);
--quote-border: hsl(226, 15%, 75%);
--table-border-color: #5a5977;
--table-border-color: hsl(201deg 10% 67%);
--table-header-bg: hsl(0, 0%, 100%);
--table-alternate-bg: hsl(0, 0%, 97%);
--table-border-line: hsl(201deg, 20%, 92%);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
--searchbar-fg: #000;
--searchbar-shadow-color: #aaa;
--searchresults-header-fg: #5f5f71;
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
--hljs-background: #TODO;
--hljs-color: #TODO;
--hljs-quote: #TODO;
--hljs-variable: #TODO;
--hljs-type: #TODO;
--hljs-title: #TODO;
--hljs-symbol: #TODO;
--hljs-selector-tag: #TODO;
*/
}

@ -1,56 +0,0 @@
pre code.hljs {
display:block;
overflow-x:auto;
padding:1em
}
code.hljs {
padding:3px 5px
}
.hljs {
background: var(--hljs-background);
color: var(--hljs-color);
}
.hljs-comment,
.hljs-quote {
color: var(--hljs-quote)
}
.hljs-link,
.hljs-meta,
.hljs-name,
.hljs-regexp,
.hljs-selector-class,
.hljs-selector-id,
.hljs-tag,
.hljs-template-variable,
.hljs-variable {
color: var(--hljs-variable)
}
.hljs-built_in,
.hljs-deletion,
.hljs-literal,
.hljs-number,
.hljs-params,
.hljs-type {
color: var(--hljs-type)
}
.hljs-attribute,
.hljs-section,
.hljs-title {
color: var(--hljs-title)
}
.hljs-addition,
.hljs-bullet,
.hljs-string,
.hljs-symbol {
color: var(--hljs-symbol)
}
.hljs-keyword,
.hljs-selector-tag {
color: var(--hljs-selector-tag)
}
.hljs-emphasis {
font-style:italic
}
.hljs-strong {
font-weight:700
}

File diff suppressed because one or more lines are too long

@ -15,7 +15,6 @@
<!-- Custom HTML head -->
{{> head}}
<meta content="text/html; charset=utf-8" http-equiv="Content-Type">
<meta name="description" content="{{ description }}">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="theme-color" content="#ffffff" />
@ -53,18 +52,19 @@
{{#if mathjax_support}}
<!-- MathJax -->
<script async type="text/javascript" src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
<script async src="https://cdnjs.cloudflare.com/ajax/libs/mathjax/2.7.1/MathJax.js?config=TeX-AMS-MML_HTMLorMML"></script>
{{/if}}
</head>
<body>
<div id="body-container">
<!-- Provide site root to javascript -->
<script type="text/javascript">
<script>
var path_to_root = "{{ path_to_root }}";
var default_theme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "{{ preferred_dark_theme }}" : "{{ default_theme }}";
</script>
<!-- Work around some values being stored in localStorage wrapped in quotes -->
<script type="text/javascript">
<script>
try {
var theme = localStorage.getItem('mdbook-theme');
var sidebar = localStorage.getItem('mdbook-sidebar');
@ -80,7 +80,7 @@
</script>
<!-- Set the theme before any content is loaded, prevents flash -->
<script type="text/javascript">
<script>
var theme;
try { theme = localStorage.getItem('mdbook-theme'); } catch(e) { }
if (theme === null || theme === undefined) { theme = default_theme; }
@ -92,12 +92,14 @@
</script>
<!-- Hide / unhide sidebar before it is displayed -->
<script type="text/javascript">
<script>
var html = document.querySelector('html');
var sidebar = 'hidden';
var sidebar = null;
if (document.body.clientWidth >= 1080) {
try { sidebar = localStorage.getItem('mdbook-sidebar'); } catch(e) { }
sidebar = sidebar || 'visible';
} else {
sidebar = 'hidden';
}
html.classList.remove('sidebar-visible');
html.classList.add("sidebar-" + sidebar);
@ -110,12 +112,34 @@
<div id="sidebar-resize-handle" class="sidebar-resize-handle"></div>
</nav>
<!-- Track and set sidebar scroll position -->
<script>
var sidebarScrollbox = document.querySelector('#sidebar .sidebar-scrollbox');
sidebarScrollbox.addEventListener('click', function(e) {
if (e.target.tagName === 'A') {
sessionStorage.setItem('sidebar-scroll', sidebarScrollbox.scrollTop);
}
}, { passive: true });
var sidebarScrollTop = sessionStorage.getItem('sidebar-scroll');
sessionStorage.removeItem('sidebar-scroll');
if (sidebarScrollTop) {
// preserve sidebar scroll position when navigating via links within sidebar
sidebarScrollbox.scrollTop = sidebarScrollTop;
} else {
// scroll sidebar to current active section when navigating via "next/previous chapter" buttons
var activeSection = document.querySelector('#sidebar .active');
if (activeSection) {
activeSection.scrollIntoView({ block: 'center' });
}
}
</script>
<div id="page-wrapper" class="page-wrapper">
<div class="page">
{{> header}}
<div id="menu-bar-hover-placeholder"></div>
<div id="menu-bar" class="menu-bar sticky bordered">
<div id="menu-bar" class="menu-bar sticky">
<div class="left-buttons">
<button id="sidebar-toggle" class="icon-button" type="button" title="Toggle Table of Contents" aria-label="Toggle Table of Contents" aria-controls="sidebar">
<i class="fa fa-bars"></i>
@ -124,12 +148,12 @@
<i class="fa fa-paint-brush"></i>
</button>
<ul id="theme-list" class="theme-popup" aria-label="Themes" role="menu">
<li role="none"><button role="menuitem" class="theme" id="light">{{ theme_option "Light" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">{{ theme_option "Rust" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">{{ theme_option "Coal" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">{{ theme_option "Navy" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">{{ theme_option "Ayu" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="colibri">{{ theme_option "Colibri" }}</button></li>
<li role="none"><button role="menuitem" class="theme" id="light">Light</button></li>
<li role="none"><button role="menuitem" class="theme" id="rust">Rust</button></li>
<li role="none"><button role="menuitem" class="theme" id="coal">Coal</button></li>
<li role="none"><button role="menuitem" class="theme" id="navy">Navy</button></li>
<li role="none"><button role="menuitem" class="theme" id="ayu">Ayu</button></li>
<li role="none"><button role="menuitem" class="theme" id="colibri">Colibri</button></li>
</ul>
{{#if search_enabled}}
<button id="search-toggle" class="icon-button" type="button" title="Search. (Shortkey: s)" aria-label="Toggle Searchbar" aria-expanded="false" aria-keyshortcuts="S" aria-controls="searchbar">
@ -151,13 +175,19 @@
<i id="git-repository-button" class="fa {{git_repository_icon}}"></i>
</a>
{{/if}}
{{#if git_repository_edit_url}}
<a href="{{git_repository_edit_url}}" title="Suggest an edit" aria-label="Suggest an edit">
<i id="git-edit-button" class="fa fa-edit"></i>
</a>
{{/if}}
</div>
</div>
{{#if search_enabled}}
<div id="search-wrapper" class="hidden">
<form id="searchbar-outer" class="searchbar-outer">
<input type="search" name="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
<input type="search" id="searchbar" name="searchbar" placeholder="Search this book ..." aria-controls="searchresults-outer" aria-describedby="searchresults-header">
</form>
<div id="searchresults-outer" class="searchresults-outer hidden">
<div id="searchresults-header" class="searchresults-header"></div>
@ -168,7 +198,7 @@
{{/if}}
<!-- Apply ARIA attributes after the sidebar and the sidebar toggle button are added to the DOM -->
<script type="text/javascript">
<script>
document.getElementById('sidebar-toggle').setAttribute('aria-expanded', sidebar === 'visible');
document.getElementById('sidebar').setAttribute('aria-hidden', sidebar !== 'visible');
Array.from(document.querySelectorAll('#sidebar a')).forEach(function(link) {
@ -216,10 +246,12 @@
</div>
{{#if livereload}}
{{#if live_reload_endpoint}}
<!-- Livereload script (if served using the cli tool) -->
<script type="text/javascript">
var socket = new WebSocket("{{{livereload}}}");
<script>
const wsProtocol = location.protocol === 'https:' ? 'wss:' : 'ws:';
const wsAddress = wsProtocol + "//" + location.host + "/" + "{{{live_reload_endpoint}}}";
const socket = new WebSocket(wsAddress);
socket.onmessage = function (event) {
if (event.data === "reload") {
socket.close();
@ -235,7 +267,7 @@
{{#if google_analytics}}
<!-- Google Analytics Tag -->
<script type="text/javascript">
<script>
var localAddrs = ["localhost", "127.0.0.1", ""];
// make sure we don't activate google analytics if the developer is
@ -253,43 +285,43 @@
{{/if}}
{{#if playground_line_numbers}}
<script type="text/javascript">
<script>
window.playground_line_numbers = true;
</script>
{{/if}}
{{#if playground_copyable}}
<script type="text/javascript">
<script>
window.playground_copyable = true;
</script>
{{/if}}
{{#if playground_js}}
<script src="{{ path_to_root }}ace.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}editor.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mode-rust.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-dawn.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}ace.js"></script>
<script src="{{ path_to_root }}editor.js"></script>
<script src="{{ path_to_root }}mode-rust.js"></script>
<script src="{{ path_to_root }}theme-dawn.js"></script>
<script src="{{ path_to_root }}theme-tomorrow_night.js"></script>
{{/if}}
{{#if search_js}}
<script src="{{ path_to_root }}elasticlunr.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}mark.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}searcher.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}elasticlunr.min.js"></script>
<script src="{{ path_to_root }}mark.min.js"></script>
<script src="{{ path_to_root }}searcher.js"></script>
{{/if}}
<script src="{{ path_to_root }}clipboard.min.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}highlight.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}book.js" type="text/javascript" charset="utf-8"></script>
<script src="{{ path_to_root }}clipboard.min.js"></script>
<script src="{{ path_to_root }}highlight.js"></script>
<script src="{{ path_to_root }}book.js"></script>
<!-- Custom JS scripts -->
{{#each additional_js}}
<script type="text/javascript" src="{{ ../path_to_root }}{{this}}"></script>
<script src="{{ ../path_to_root }}{{this}}"></script>
{{/each}}
{{#if is_print}}
{{#if mathjax_support}}
<script type="text/javascript">
<script>
window.addEventListener('load', function() {
MathJax.Hub.Register.StartupHook('End', function() {
window.setTimeout(window.print, 100);
@ -297,7 +329,7 @@
});
</script>
{{else}}
<script type="text/javascript">
<script>
window.addEventListener('load', function() {
window.setTimeout(window.print, 100);
});
@ -305,5 +337,6 @@
{{/if}}
{{/if}}
</div>
</body>
</html>

@ -36,6 +36,9 @@
<content_rating type="oars-1.1" />
<releases>
<release version="23.05" date="2023-05-18">
<url>https://github.com/helix-editor/helix/releases/tag/23.05</url>
</release>
<release version="23.03" date="2023-03-31">
<url>https://helix-editor.com/news/release-23-03-highlights/</url>
</release>

@ -1,7 +1,7 @@
## Checklist
Helix releases are versioned in the Calendar Versioning scheme:
`YY.0M(.MICRO)`, for example `22.05` for May of 2022. In these instructions
`YY.0M(.MICRO)`, for example, `22.05` for May of 2022. In these instructions
we'll use `<tag>` as a placeholder for the tag being published.
* Merge the changelog PR
@ -30,7 +30,7 @@ we'll use `<tag>` as a placeholder for the tag being published.
The changelog is currently created manually by reading through commits in the
log since the last release. GitHub's compare view is a nice way to approach
this. For example when creating the 22.07 release notes, this compare link
this. For example, when creating the 22.07 release notes, this compare link
may be used
```

@ -20,5 +20,5 @@ Vision statements are all well and good, but are also vague and subjective. Her
* **Built-in tools** for working with code bases efficiently. Most projects aren't a single file, and an editor should handle that as a first-class use case. In Helix's case, this means (among other things) a fuzzy-search file navigator and LSP support.
* **Edit anything** that comes up when coding, within reason. Whether it's a 200 MB XML file, a megabyte of minified javascript on a single line, or Japanese text encoded in ShiftJIS, you should be able to open it and edit it without problems. (Note: this doesn't mean handle every esoteric use case. Sometimes you do just need a specialized tool, and Helix isn't that.)
* **Configurable**, within reason. Although the defaults should be good, not everyone will agree on what "good" is. Within the bounds of Helix's core interaction models, it should be reasonably configurable so that it can be "good" for more people. This means, for example, custom key maps among other things.
* **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed. Right now we're thinking Wasm-based plugins.
* **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed.
* **Clean code base.** Sometimes other factors (e.g. significant performance gains, important features, correctness, etc.) will trump strict readability, but we nevertheless want to keep the code base straightforward and easy to understand to the extent we can.

@ -1,109 +1,29 @@
{
"nodes": {
"crane": {
"flake": false,
"locked": {
"lastModified": 1670900067,
"narHash": "sha256-VXVa+KBfukhmWizaiGiHRVX/fuk66P8dgSFfkVN4/MY=",
"owner": "ipetkov",
"repo": "crane",
"rev": "59b31b41a589c0a65e4a1f86b0e5eac68081468b",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"dream2nix": {
"inputs": {
"all-cabal-json": [
"nci"
],
"crane": "crane",
"devshell": [
"nci"
],
"drv-parts": "drv-parts",
"flake-compat": "flake-compat",
"flake-parts": [
"nci",
"parts"
],
"flake-utils-pre-commit": [
"nci"
],
"ghc-utils": [
"nci"
],
"gomod2nix": [
"nci"
],
"mach-nix": [
"nci"
],
"nix-pypi-fetcher": [
"nci"
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nci",
"nixpkgs"
],
"nixpkgsV1": "nixpkgsV1",
"poetry2nix": [
"nci"
],
"pre-commit-hooks": [
"nci"
],
"pruned-racket-catalog": [
"nci"
]
},
"locked": {
"lastModified": 1680258209,
"narHash": "sha256-lEo50RXI/17/a9aCIun8Hz62ZJ5JM5RGeTgclIP+Lgc=",
"owner": "nix-community",
"repo": "dream2nix",
"rev": "6f512b5a220fdb26bd3c659f7b55e4f052ec8b35",
"type": "github"
},
"original": {
"owner": "nix-community",
"repo": "dream2nix",
"type": "github"
}
},
"drv-parts": {
"inputs": {
"flake-compat": [
"nci",
"dream2nix",
"flake-compat"
],
"flake-parts": [
"nci",
"dream2nix",
"flake-parts"
],
"nixpkgs": [
"nci",
"dream2nix",
"nixpkgs"
"rust-overlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1680172861,
"narHash": "sha256-QMyI338xRxaHFDlCXdLCtgelGQX2PdlagZALky4ZXJ8=",
"owner": "davhau",
"repo": "drv-parts",
"rev": "ced8a52f62b0a94244713df2225c05c85b416110",
"lastModified": 1688772518,
"narHash": "sha256-ol7gZxwvgLnxNSZwFTDJJ49xVY5teaSvF7lzlo3YQfM=",
"owner": "ipetkov",
"repo": "crane",
"rev": "8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e",
"type": "github"
},
"original": {
"owner": "davhau",
"repo": "drv-parts",
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
@ -124,12 +44,15 @@
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1659877975,
"narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=",
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"type": "github"
},
"original": {
@ -138,55 +61,13 @@
"type": "github"
}
},
"mk-naked-shell": {
"flake": false,
"locked": {
"lastModified": 1676572903,
"narHash": "sha256-oQoDHHUTxNVSURfkFcYLuAK+btjs30T4rbEUtCUyKy8=",
"owner": "yusdacra",
"repo": "mk-naked-shell",
"rev": "aeca9f8aa592f5e8f71f407d081cb26fd30c5a57",
"type": "github"
},
"original": {
"owner": "yusdacra",
"repo": "mk-naked-shell",
"type": "github"
}
},
"nci": {
"inputs": {
"dream2nix": "dream2nix",
"mk-naked-shell": "mk-naked-shell",
"nixpkgs": [
"nixpkgs"
],
"parts": "parts",
"rust-overlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1680329418,
"narHash": "sha256-+KN0eQLSZvL1J0kDO8/fxv0UCHTyZCADLmpIfeeiSGo=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "98c1d2ff5155f0fee5d290f6b982cb990839d540",
"type": "github"
},
"original": {
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1680213900,
"narHash": "sha256-cIDr5WZIj3EkKyCgj/6j3HBH4Jj1W296z7HTcWj1aMA=",
"lastModified": 1690272529,
"narHash": "sha256-MakzcKXEdv/I4qJUtq/k/eG+rVmyOZLnYNC2w1mB59Y=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "e3652e0735fbec227f342712f180f4f21f0594f2",
"rev": "ef99fa5c5ed624460217c31ac4271cfb5cb2502c",
"type": "github"
},
"original": {
@ -196,99 +77,29 @@
"type": "github"
}
},
"nixpkgs-lib": {
"locked": {
"dir": "lib",
"lastModified": 1678375444,
"narHash": "sha256-XIgHfGvjFvZQ8hrkfocanCDxMefc/77rXeHvYdzBMc8=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "130fa0baaa2b93ec45523fdcde942f6844ee9f6e",
"type": "github"
},
"original": {
"dir": "lib",
"owner": "NixOS",
"ref": "nixos-unstable",
"repo": "nixpkgs",
"type": "github"
}
},
"nixpkgsV1": {
"locked": {
"lastModified": 1678500271,
"narHash": "sha256-tRBLElf6f02HJGG0ZR7znMNFv/Uf7b2fFInpTHiHaSE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "5eb98948b66de29f899c7fe27ae112a47964baf8",
"type": "github"
},
"original": {
"id": "nixpkgs",
"ref": "nixos-22.11",
"type": "indirect"
}
},
"parts": {
"inputs": {
"nixpkgs-lib": [
"nci",
"nixpkgs"
]
},
"locked": {
"lastModified": 1679737941,
"narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"parts_2": {
"inputs": {
"nixpkgs-lib": "nixpkgs-lib"
},
"locked": {
"lastModified": 1679737941,
"narHash": "sha256-srSD9CwsVPnUMsIZ7Kt/UegkKUEBcTyU1Rev7mO45S0=",
"owner": "hercules-ci",
"repo": "flake-parts",
"rev": "3502ee99d6dade045bdeaf7b0cd8ec703484c25c",
"type": "github"
},
"original": {
"owner": "hercules-ci",
"repo": "flake-parts",
"type": "github"
}
},
"root": {
"inputs": {
"nci": "nci",
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"parts": "parts_2",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
]
},
"locked": {
"lastModified": 1680315536,
"narHash": "sha256-0AsBuKssJMbcRcw4HJQwJsUHhZxR5+gaf6xPQayhR44=",
"lastModified": 1690424156,
"narHash": "sha256-Bpml+L280tHTQpwpC5/BJbU4HSvEzMvW8IZ4gAXimhE=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "5c8c151bdd639074a0051325c16df1a64ee23497",
"rev": "f335a0213504c7e6481c359dc1009be9cf34432c",
"type": "github"
},
"original": {
@ -296,6 +107,21 @@
"repo": "rust-overlay",
"type": "github"
}
},
"systems": {
"locked": {
"lastModified": 1681028828,
"narHash": "sha256-Vy1rq5AaRuLzOxct8nz4T6wlgyUR7zLU309k9mBC768=",
"owner": "nix-systems",
"repo": "default",
"rev": "da67096a3b9bf56a91d16901293e51ba5b49a27e",
"type": "github"
},
"original": {
"owner": "nix-systems",
"repo": "default",
"type": "github"
}
}
},
"root": "root",

@ -3,162 +3,195 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay = {
url = "github:oxalica/rust-overlay";
inputs.nixpkgs.follows = "nixpkgs";
inputs = {
nixpkgs.follows = "nixpkgs";
flake-utils.follows = "flake-utils";
};
};
nci = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
crane = {
url = "github:ipetkov/crane";
inputs.rust-overlay.follows = "rust-overlay";
inputs.flake-utils.follows = "flake-utils";
inputs.nixpkgs.follows = "nixpkgs";
};
parts.url = "github:hercules-ci/flake-parts";
};
outputs = inp: let
mkRootPath = rel:
builtins.path {
path = "${toString ./.}/${rel}";
name = rel;
outputs = {
self,
nixpkgs,
crane,
flake-utils,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
filteredSource = let
pathsToIgnore = [
".envrc"
".ignore"
".github"
".gitignore"
"logo.svg"
"logo_dark.svg"
"logo_light.svg"
"rust-toolchain.toml"
"rustfmt.toml"
"runtime"
"screenshot.png"
"book"
"contrib"
"docs"
"README.md"
"CHANGELOG.md"
"shell.nix"
"default.nix"
"grammars.nix"
"flake.nix"
"flake.lock"
];
ignorePaths = path: type: let
inherit (inp.nixpkgs) lib;
# split the nix store path into its components
components = lib.splitString "/" path;
# drop off the `/nix/hash-source` section from the path
relPathComponents = lib.drop 4 components;
# reassemble the path components
relPath = lib.concatStringsSep "/" relPathComponents;
in
lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
in
builtins.path {
name = "helix-source";
path = toString ./.;
# filter out unnecessary paths
filter = ignorePaths;
};
in
inp.parts.lib.mkFlake {inputs = inp;} {
imports = [inp.nci.flakeModule];
systems = [
"x86_64-linux"
"x86_64-darwin"
"aarch64-linux"
"aarch64-darwin"
"i686-linux"
];
perSystem = {
config,
pkgs,
lib,
...
}: let
makeOverridableHelix = old: config: let
grammars = pkgs.callPackage ./grammars.nix config;
runtimeDir = pkgs.runCommand "helix-runtime" {} ''
mkdir -p $out
ln -s ${mkRootPath "runtime"}/* $out
rm -r $out/grammars
ln -s ${grammars} $out/grammars
'';
helix-wrapped =
pkgs.runCommand
old.name
{
inherit (old) pname version;
meta = old.meta or {};
passthru =
(old.passthru or {})
// {
unwrapped = old;
};
nativeBuildInputs = [pkgs.makeWrapper];
makeWrapperArgs = config.makeWrapperArgs or [];
}
''
cp -rs --no-preserve=mode,ownership ${old} $out
wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
'';
mkRootPath = rel:
builtins.path {
path = "${toString ./.}/${rel}";
name = rel;
};
filteredSource = let
pathsToIgnore = [
".envrc"
".ignore"
".github"
".gitignore"
"logo_dark.svg"
"logo_light.svg"
"rust-toolchain.toml"
"rustfmt.toml"
"runtime"
"screenshot.png"
"book"
"docs"
"README.md"
"CHANGELOG.md"
"shell.nix"
"default.nix"
"grammars.nix"
"flake.nix"
"flake.lock"
];
ignorePaths = path: type: let
inherit (nixpkgs) lib;
# split the nix store path into its components
components = lib.splitString "/" path;
# drop off the `/nix/hash-source` section from the path
relPathComponents = lib.drop 4 components;
# reassemble the path components
relPath = lib.concatStringsSep "/" relPathComponents;
in
helix-wrapped
// {
override = makeOverridableHelix old;
lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore;
in
builtins.path {
name = "helix-source";
path = toString ./.;
# filter out unnecessary paths
filter = ignorePaths;
};
makeOverridableHelix = old: config: let
grammars = pkgs.callPackage ./grammars.nix config;
runtimeDir = pkgs.runCommand "helix-runtime" {} ''
mkdir -p $out
ln -s ${mkRootPath "runtime"}/* $out
rm -r $out/grammars
ln -s ${grammars} $out/grammars
'';
helix-wrapped =
pkgs.runCommand
old.name
{
inherit (old) pname version;
meta = old.meta or {};
passthru =
helix-wrapped.passthru
(old.passthru or {})
// {
wrapper = old: makeOverridableHelix old config;
unwrapped = old;
};
};
stdenv =
if pkgs.stdenv.isLinux
then pkgs.stdenv
else pkgs.clangStdenv;
rustFlagsEnv =
if stdenv.isLinux
then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
else "$RUSTFLAGS";
in {
nci.projects."helix-project".relPath = "";
nci.crates."helix-term" = {
overrides = {
add-meta.override = _: {meta.mainProgram = "hx";};
add-inputs.overrideAttrs = prev: {
buildInputs = (prev.buildInputs or []) ++ [stdenv.cc.cc.lib];
};
disable-grammar-builds = {
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
nativeBuildInputs = [pkgs.makeWrapper];
makeWrapperArgs = config.makeWrapperArgs or [];
}
''
cp -rs --no-preserve=mode,ownership ${old} $out
wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}"
'';
in
helix-wrapped
// {
override = makeOverridableHelix old;
passthru =
helix-wrapped.passthru
// {
wrapper = old: makeOverridableHelix old config;
};
disable-tests = {checkPhase = ":";};
set-stdenv.override = _: {inherit stdenv;};
set-filtered-src.override = _: {src = filteredSource;};
};
};
stdenv =
if pkgs.stdenv.isLinux
then pkgs.stdenv
else pkgs.clangStdenv;
rustFlagsEnv =
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;
craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain;
craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default;
commonArgs =
{
inherit stdenv;
src = filteredSource;
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
buildInputs = [stdenv.cc.cc.lib];
# disable tests
doCheck = false;
meta.mainProgram = "hx";
}
// craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;};
cargoArtifacts = craneLibMSRV.buildDepsOnly commonArgs;
in {
packages = {
helix-unwrapped = craneLibStable.buildPackage (commonArgs
// {
cargoArtifacts = craneLibStable.buildDepsOnly commonArgs;
postInstall = ''
mkdir -p $out/share/applications $out/share/icons/hicolor/scalable/apps $out/share/icons/hicolor/256x256/apps
cp contrib/Helix.desktop $out/share/applications
cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg
cp contrib/helix.png $out/share/icons/hicolor/256x256/apps
'';
});
helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {};
default = self.packages.${system}.helix;
};
packages.helix-unwrapped = config.nci.outputs."helix-term".packages.release;
packages.helix-unwrapped-dev = config.nci.outputs."helix-term".packages.dev;
packages.helix = makeOverridableHelix config.packages.helix-unwrapped {};
packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {};
packages.default = config.packages.helix;
checks = {
# Build the crate itself
inherit (self.packages.${system}) helix;
devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: {
nativeBuildInputs =
(old.nativeBuildInputs or [])
++ (with pkgs; [lld_13 cargo-flamegraph rust-analyzer])
++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
++ (lib.optional stdenv.isLinux pkgs.lldb)
++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
shellHook = ''
export HELIX_RUNTIME="$PWD/runtime"
export RUST_BACKTRACE="1"
export RUSTFLAGS="${rustFlagsEnv}"
'';
});
clippy = craneLibMSRV.cargoClippy (commonArgs
// {
inherit cargoArtifacts;
cargoClippyExtraArgs = "--all-targets -- --deny warnings";
});
fmt = craneLibMSRV.cargoFmt commonArgs;
doc = craneLibMSRV.cargoDoc (commonArgs
// {
inherit cargoArtifacts;
});
test = craneLibMSRV.cargoTest (commonArgs
// {
inherit cargoArtifacts;
});
};
devShells.default = pkgs.mkShell {
inputsFrom = builtins.attrValues self.checks.${system};
nativeBuildInputs = with pkgs;
[lld_13 cargo-flamegraph rust-analyzer]
++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin)
++ (lib.optional stdenv.isLinux pkgs.lldb)
++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation);
shellHook = ''
export HELIX_RUNTIME="$PWD/runtime"
export RUST_BACKTRACE="1"
export RUSTFLAGS="${rustFlagsEnv}"
'';
};
})
// {
overlays.default = final: prev: {
inherit (self.packages.${final.system}) helix;
};
};

@ -18,20 +18,20 @@ integration = []
helix-loader = { version = "0.6", path = "../helix-loader" }
ropey = { version = "1.6.0", default-features = false, features = ["simd"] }
smallvec = "1.10"
smallvec = "1.11"
smartstring = "1.0.1"
unicode-segmentation = "1.10"
unicode-width = "0.1"
unicode-general-category = "0.6"
# slab = "0.4.2"
slotmap = "1.0"
tree-sitter = "0.20"
once_cell = "1.17"
tree-sitter.workspace = true
once_cell = "1.18"
arc-swap = "1"
regex = "1"
bitflags = "2.0"
bitflags = "2.4"
ahash = "0.8.3"
hashbrown = { version = "0.13.2", features = ["raw"] }
hashbrown = { version = "0.14.0", features = ["raw"] }
dunce = "1.0"
log = "0.4"
@ -45,9 +45,12 @@ encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
etcetera = "0.4"
etcetera = "0.8"
textwrap = "0.16.0"
nucleo.workspace = true
parking_lot = "0.12"
[dev-dependencies]
quickcheck = { version = "1", default-features = false }
indoc = "2.0.1"
indoc = "2.0.3"

@ -43,6 +43,7 @@ pub struct Diagnostic {
pub message: String,
pub severity: Option<Severity>,
pub code: Option<NumberOrString>,
pub language_server_id: usize,
pub tags: Vec<DiagnosticTag>,
pub source: Option<String>,
pub data: Option<serde_json::Value>,

@ -0,0 +1,43 @@
use std::ops::DerefMut;
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
use nucleo::Config;
use parking_lot::Mutex;
pub struct LazyMutex<T> {
inner: Mutex<Option<T>>,
init: fn() -> T,
}
impl<T> LazyMutex<T> {
pub const fn new(init: fn() -> T) -> Self {
Self {
inner: Mutex::new(None),
init,
}
}
pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
parking_lot::MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
}
}
pub static MATCHER: LazyMutex<nucleo::Matcher> = LazyMutex::new(nucleo::Matcher::default);
/// convenience function to easily fuzzy match
/// on a (relatively small list of inputs). This is not recommended for building a full tui
/// application that can match large numbers of matches as all matching is done on the current
/// thread, effectively blocking the UI
pub fn fuzzy_match<T: AsRef<str>>(
pattern: &str,
items: impl IntoIterator<Item = T>,
path: bool,
) -> Vec<(T, u16)> {
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
if path {
matcher.config.set_match_paths();
}
let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false);
pattern.match_list(items, &mut matcher)
}

@ -481,7 +481,7 @@ impl<'a> From<String> for GraphemeStr<'a> {
let ptr = Box::into_raw(g.into_bytes().into_boxed_slice()) as *mut u8;
GraphemeStr {
ptr: unsafe { NonNull::new_unchecked(ptr) },
len: i32::try_from(len).unwrap() as u32,
len: (i32::try_from(len).unwrap() as u32) | Self::MASK_OWNED,
phantom: PhantomData,
}
}

@ -72,8 +72,8 @@ impl Default for History {
revisions: vec![Revision {
parent: 0,
last_child: None,
transaction: Transaction::from(ChangeSet::new(&Rope::new())),
inversion: Transaction::from(ChangeSet::new(&Rope::new())),
transaction: Transaction::from(ChangeSet::new("".into())),
inversion: Transaction::from(ChangeSet::new("".into())),
timestamp: Instant::now(),
}],
current: 0,

@ -1,13 +1,13 @@
use std::collections::HashMap;
use std::{borrow::Cow, collections::HashMap};
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
graphemes::tab_width_at,
graphemes::{grapheme_width, tab_width_at},
syntax::{LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node,
Rope, RopeSlice,
Rope, RopeGraphemes, RopeSlice,
};
/// Enum representing indentation style.
@ -19,6 +19,10 @@ pub enum IndentStyle {
Spaces(u8),
}
// 16 spaces
const INDENTS: &str = " ";
const MAX_INDENT: u8 = 16;
impl IndentStyle {
/// Creates an `IndentStyle` from an indentation string.
///
@ -27,10 +31,10 @@ impl IndentStyle {
#[inline]
pub fn from_str(indent: &str) -> Self {
// XXX: do we care about validating the input more than this? Probably not...?
debug_assert!(!indent.is_empty() && indent.len() <= 8);
debug_assert!(!indent.is_empty() && indent.len() <= MAX_INDENT as usize);
if indent.starts_with(' ') {
IndentStyle::Spaces(indent.len() as u8)
IndentStyle::Spaces(indent.len().clamp(1, MAX_INDENT as usize) as u8)
} else {
IndentStyle::Tabs
}
@ -40,20 +44,13 @@ impl IndentStyle {
pub fn as_str(&self) -> &'static str {
match *self {
IndentStyle::Tabs => "\t",
IndentStyle::Spaces(1) => " ",
IndentStyle::Spaces(2) => " ",
IndentStyle::Spaces(3) => " ",
IndentStyle::Spaces(4) => " ",
IndentStyle::Spaces(5) => " ",
IndentStyle::Spaces(6) => " ",
IndentStyle::Spaces(7) => " ",
IndentStyle::Spaces(8) => " ",
// Unsupported indentation style. This should never happen,
// but just in case fall back to two spaces.
IndentStyle::Spaces(n) => {
debug_assert!(n > 0 && n <= 8); // Always triggers. `debug_panic!()` wanted.
" "
// Unsupported indentation style. This should never happen,
debug_assert!(n > 0 && n <= MAX_INDENT);
// Either way, clamp to the nearest supported value
let closest_n = n.clamp(1, MAX_INDENT) as usize;
&INDENTS[0..closest_n]
}
}
}
@ -75,9 +72,9 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
// Build a histogram of the indentation *increases* between
// subsequent lines, ignoring lines that are all whitespace.
//
// Index 0 is for tabs, the rest are 1-8 spaces.
let histogram: [usize; 9] = {
let mut histogram = [0; 9];
// Index 0 is for tabs, the rest are 1-MAX_INDENT spaces.
let histogram: [usize; MAX_INDENT as usize + 1] = {
let mut histogram = [0; MAX_INDENT as usize + 1];
let mut prev_line_is_tabs = false;
let mut prev_line_leading_count = 0usize;
@ -136,7 +133,7 @@ pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
histogram[0] += 1;
} else {
let amount = leading_count - prev_line_leading_count;
if amount <= 8 {
if amount <= MAX_INDENT as usize {
histogram[amount] += 1;
}
}
@ -240,68 +237,117 @@ fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bo
/// This is usually constructed in one of 2 ways:
/// - Successively add indent captures to get the (added) indent from a single line
/// - Successively add the indent results for each line
#[derive(Default)]
/// The string that this indentation defines starts with the string contained in the align field (unless it is None), followed by:
/// - max(0, indent - outdent) tabs, if tabs are used for indentation
/// - max(0, indent - outdent)*indent_width spaces, if spaces are used for indentation
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct Indentation {
/// The total indent (the number of indent levels) is defined as max(0, indent-outdent).
/// The string that this results in depends on the indent style (spaces or tabs, etc.)
indent: usize,
indent_always: usize,
outdent: usize,
outdent_always: usize,
/// The alignment, as a string containing only tabs & spaces. Storing this as a string instead of e.g.
/// the (visual) width ensures that the alignment is preserved even if the tab width changes.
align: Option<String>,
}
impl Indentation {
/// Add some other [Indentation] to this.
/// The added indent should be the total added indent from one line
fn add_line(&mut self, added: &Indentation) {
if added.indent > 0 && added.outdent == 0 {
self.indent += 1;
} else if added.outdent > 0 && added.indent == 0 {
self.outdent += 1;
/// The added indent should be the total added indent from one line.
/// Indent should always be added starting from the bottom (or equivalently, the innermost tree-sitter node).
fn add_line(&mut self, added: Indentation) {
// Align overrides the indent from outer scopes.
if self.align.is_some() {
return;
}
if added.align.is_some() {
self.align = added.align;
return;
}
self.indent += added.indent;
self.indent_always += added.indent_always;
self.outdent += added.outdent;
self.outdent_always += added.outdent_always;
}
/// Add an indent capture to this indent.
/// All the captures that are added in this way should be on the same line.
fn add_capture(&mut self, added: IndentCaptureType) {
match added {
IndentCaptureType::Indent => {
self.indent = 1;
if self.indent_always == 0 {
self.indent = 1;
}
}
IndentCaptureType::IndentAlways => {
// any time we encounter an `indent.always` on the same line, we
// want to cancel out all regular indents
self.indent_always += 1;
self.indent = 0;
}
IndentCaptureType::Outdent => {
self.outdent = 1;
if self.outdent_always == 0 {
self.outdent = 1;
}
}
IndentCaptureType::OutdentAlways => {
self.outdent_always += 1;
self.outdent = 0;
}
IndentCaptureType::Align(align) => {
self.align = Some(align);
}
}
}
fn as_string(&self, indent_style: &IndentStyle) -> String {
let indent_level = if self.indent >= self.outdent {
self.indent - self.outdent
fn into_string(self, indent_style: &IndentStyle) -> String {
let indent = self.indent_always + self.indent;
let outdent = self.outdent_always + self.outdent;
let indent_level = if indent >= outdent {
indent - outdent
} else {
log::warn!("Encountered more outdent than indent nodes while calculating indentation: {} outdent, {} indent", self.outdent, self.indent);
0
};
indent_style.as_str().repeat(indent_level)
let mut indent_string = if let Some(align) = self.align {
align
} else {
String::new()
};
indent_string.push_str(&indent_style.as_str().repeat(indent_level));
indent_string
}
}
/// An indent definition which corresponds to a capture from the indent query
#[derive(Debug)]
struct IndentCapture {
capture_type: IndentCaptureType,
scope: IndentScope,
}
#[derive(Clone, Copy)]
#[derive(Debug, Clone, PartialEq)]
enum IndentCaptureType {
Indent,
IndentAlways,
Outdent,
OutdentAlways,
/// Alignment given as a string of whitespace
Align(String),
}
impl IndentCaptureType {
fn default_scope(&self) -> IndentScope {
match self {
IndentCaptureType::Indent => IndentScope::Tail,
IndentCaptureType::Outdent => IndentScope::All,
IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail,
IndentCaptureType::Outdent | IndentCaptureType::OutdentAlways => IndentScope::All,
IndentCaptureType::Align(_) => IndentScope::All,
}
}
}
/// This defines which part of a node an [IndentCapture] applies to.
/// Each [IndentCaptureType] has a default scope, but the scope can be changed
/// with `#set!` property declarations.
#[derive(Clone, Copy)]
#[derive(Debug, Clone, Copy)]
enum IndentScope {
/// The indent applies to the whole node
All,
@ -311,6 +357,7 @@ enum IndentScope {
/// A capture from the indent query which does not define an indent but extends
/// the range of a node. This is used before the indent is calculated.
#[derive(Debug)]
enum ExtendCapture {
Extend,
PreventOnce,
@ -319,24 +366,41 @@ enum ExtendCapture {
/// The result of running a tree-sitter indent query. This stores for
/// each node (identified by its ID) the relevant captures (already filtered
/// by predicates).
#[derive(Debug)]
struct IndentQueryResult {
indent_captures: HashMap<usize, Vec<IndentCapture>>,
extend_captures: HashMap<usize, Vec<ExtendCapture>>,
}
fn get_node_start_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
let mut node_line = node.start_position().row;
// Adjust for the new line that will be inserted
if new_line_byte_pos.map_or(false, |pos| node.start_byte() >= pos) {
node_line += 1;
}
node_line
}
fn get_node_end_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
let mut node_line = node.end_position().row;
// Adjust for the new line that will be inserted (with a strict inequality since end_byte is exclusive)
if new_line_byte_pos.map_or(false, |pos| node.end_byte() > pos) {
node_line += 1;
}
node_line
}
fn query_indents(
query: &Query,
syntax: &Syntax,
cursor: &mut QueryCursor,
text: RopeSlice,
range: std::ops::Range<usize>,
// Position of the (optional) newly inserted line break.
// Given as (line, byte_pos)
new_line_break: Option<(usize, usize)>,
new_line_byte_pos: Option<usize>,
) -> IndentQueryResult {
let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
cursor.set_byte_range(range);
// Iterate over all captures from the query
for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
// Skip matches where not all custom predicates are fulfilled
@ -363,21 +427,13 @@ fn query_indents(
Some(QueryPredicateArg::Capture(capt1)),
Some(QueryPredicateArg::Capture(capt2))
) => {
let get_line_num = |node: Node| {
let mut node_line = node.start_position().row;
// Adjust for the new line that will be inserted
if let Some((line, byte)) = new_line_break {
if node_line==line && node.start_byte()>=byte {
node_line += 1;
}
}
node_line
};
let n1 = m.nodes_for_capture_index(*capt1).next();
let n2 = m.nodes_for_capture_index(*capt2).next();
match (n1, n2) {
(Some(n1), Some(n2)) => {
let same_line = get_line_num(n1)==get_line_num(n2);
let n1_line = get_node_start_line(n1, new_line_byte_pos);
let n2_line = get_node_start_line(n2, new_line_byte_pos);
let same_line = n1_line == n2_line;
same_line==(pred.operator.as_ref()=="same-line?")
}
_ => true,
@ -388,6 +444,23 @@ fn query_indents(
}
}
}
"one-line?" | "not-one-line?" => match pred.args.get(0) {
Some(QueryPredicateArg::Capture(capture_idx)) => {
let node = m.nodes_for_capture_index(*capture_idx).next();
match node {
Some(node) => {
let (start_line, end_line) = (get_node_start_line(node,new_line_byte_pos), get_node_end_line(node, new_line_byte_pos));
let one_line = end_line == start_line;
one_line != (pred.operator.as_ref() == "not-one-line?")
},
_ => true,
}
}
_ => {
panic!("Invalid indent query: Arguments to \"not-kind-eq?\" must be a capture and a string");
}
},
_ => {
panic!(
"Invalid indent query: Unknown predicate (\"{}\")",
@ -398,11 +471,28 @@ fn query_indents(
}) {
continue;
}
// A list of pairs (node_id, indent_capture) that are added by this match.
// They cannot be added to indent_captures immediately since they may depend on other captures (such as an @anchor).
let mut added_indent_captures: Vec<(usize, IndentCapture)> = Vec::new();
// The row/column position of the optional anchor in this query
let mut anchor: Option<tree_sitter::Node> = None;
for capture in m.captures {
let capture_name = query.capture_names()[capture.index as usize].as_str();
let capture_type = match capture_name {
"indent" => IndentCaptureType::Indent,
"indent.always" => IndentCaptureType::IndentAlways,
"outdent" => IndentCaptureType::Outdent,
"outdent.always" => IndentCaptureType::OutdentAlways,
// The alignment will be updated to the correct value at the end, when the anchor is known.
"align" => IndentCaptureType::Align(String::from("")),
"anchor" => {
if anchor.is_some() {
log::error!("Invalid indent query: Encountered more than one @anchor in the same match.")
} else {
anchor = Some(capture.node);
}
continue;
}
"extend" => {
extend_captures
.entry(capture.node.id())
@ -452,17 +542,52 @@ fn query_indents(
}
}
}
added_indent_captures.push((capture.node.id(), indent_capture))
}
for (node_id, mut capture) in added_indent_captures {
// Set the anchor for all align queries.
if let IndentCaptureType::Align(_) = capture.capture_type {
let anchor = match anchor {
None => {
log::error!(
"Invalid indent query: @align requires an accompanying @anchor."
);
continue;
}
Some(anchor) => anchor,
};
// Create a string of tabs & spaces that should have the same width
// as the string that precedes the anchor (independent of the tab width).
let mut align = String::new();
for grapheme in RopeGraphemes::new(
text.line(anchor.start_position().row)
.byte_slice(0..anchor.start_position().column),
) {
if grapheme == "\t" {
align.push('\t');
} else {
align.extend(
std::iter::repeat(' ').take(grapheme_width(&Cow::from(grapheme))),
);
}
}
capture.capture_type = IndentCaptureType::Align(align);
}
indent_captures
.entry(capture.node.id())
// Most entries only need to contain a single IndentCapture
.entry(node_id)
.or_insert_with(|| Vec::with_capacity(1))
.push(indent_capture);
.push(capture);
}
}
IndentQueryResult {
let result = IndentQueryResult {
indent_captures,
extend_captures,
}
};
log::trace!("indent result = {:?}", result);
result
}
/// Handle extend queries. deepest_preceding is the deepest descendant of node that directly precedes the cursor position.
@ -581,12 +706,14 @@ pub fn treesitter_indent_for_pos(
new_line: bool,
) -> Option<String> {
let byte_pos = text.char_to_byte(pos);
let new_line_byte_pos = new_line.then_some(byte_pos);
// The innermost tree-sitter node which is considered for the indent
// computation. It may change if some predeceding node is extended
let mut node = syntax
.tree()
.root_node()
.descendant_for_byte_range(byte_pos, byte_pos)?;
let (query_result, deepest_preceding) = {
// The query range should intersect with all nodes directly preceding
// the position of the indent query in case one of them is extended.
@ -617,13 +744,13 @@ pub fn treesitter_indent_for_pos(
&mut cursor,
text,
query_range,
new_line.then_some((line, byte_pos)),
new_line_byte_pos,
);
ts_parser.cursors.push(cursor);
(query_result, deepest_preceding)
})
};
let indent_captures = query_result.indent_captures;
let mut indent_captures = query_result.indent_captures;
let extend_captures = query_result.extend_captures;
// Check for extend captures, potentially changing the node that the indent calculation starts with
@ -645,12 +772,16 @@ pub fn treesitter_indent_for_pos(
// even if there are multiple "indent" nodes on the same line
let mut indent_for_line = Indentation::default();
let mut indent_for_line_below = Indentation::default();
loop {
// This can safely be unwrapped because `first_in_line` contains
// 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
if let Some(definitions) = indent_captures.get(&node.id()) {
// Apply all indent definitions for this node.
// Since we only iterate over each node once, we can remove the
// corresponding captures from the HashMap to avoid cloning them.
if let Some(definitions) = indent_captures.remove(&node.id()) {
for definition in definitions {
match definition.scope {
IndentScope::All => {
@ -668,28 +799,22 @@ pub fn treesitter_indent_for_pos(
}
if let Some(parent) = node.parent() {
let mut node_line = node.start_position().row;
let mut parent_line = parent.start_position().row;
if node_line == line && new_line {
// Also consider the line that will be inserted
if node.start_byte() >= byte_pos {
node_line += 1;
}
if parent.start_byte() >= byte_pos {
parent_line += 1;
}
};
let node_line = get_node_start_line(node, new_line_byte_pos);
let parent_line = get_node_start_line(parent, new_line_byte_pos);
if node_line != parent_line {
// Don't add indent for the line below the line of the query
if node_line < line + (new_line as usize) {
// Don't add indent for the line below the line of the query
result.add_line(&indent_for_line_below);
result.add_line(indent_for_line_below);
}
if node_line == parent_line + 1 {
indent_for_line_below = indent_for_line;
} else {
result.add_line(&indent_for_line);
result.add_line(indent_for_line);
indent_for_line_below = Indentation::default();
}
indent_for_line = Indentation::default();
}
@ -701,13 +826,13 @@ pub fn treesitter_indent_for_pos(
if (node.start_position().row < line)
|| (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
{
result.add_line(&indent_for_line_below);
result.add_line(indent_for_line_below);
}
result.add_line(&indent_for_line);
result.add_line(indent_for_line);
break;
}
}
Some(result.as_string(indent_style))
Some(result.into_string(indent_style))
}
/// Returns the indentation for a new line.
@ -797,4 +922,138 @@ mod test {
3
);
}
#[test]
fn test_large_indent_level() {
let tab_width = 16;
let indent_width = 16;
let line = Rope::from(" fn new"); // 16 spaces
assert_eq!(
indent_level_for_line(line.slice(..), tab_width, indent_width),
1
);
let line = Rope::from(" fn new"); // 32 spaces
assert_eq!(
indent_level_for_line(line.slice(..), tab_width, indent_width),
2
);
}
#[test]
fn add_capture() {
let indent = || Indentation {
indent: 1,
..Default::default()
};
let indent_always = || Indentation {
indent_always: 1,
..Default::default()
};
let outdent = || Indentation {
outdent: 1,
..Default::default()
};
let outdent_always = || Indentation {
outdent_always: 1,
..Default::default()
};
let add_capture = |mut indent: Indentation, capture| {
indent.add_capture(capture);
indent
};
// adding an indent to no indent makes an indent
assert_eq!(
indent(),
add_capture(Indentation::default(), IndentCaptureType::Indent)
);
assert_eq!(
indent_always(),
add_capture(Indentation::default(), IndentCaptureType::IndentAlways)
);
assert_eq!(
outdent(),
add_capture(Indentation::default(), IndentCaptureType::Outdent)
);
assert_eq!(
outdent_always(),
add_capture(Indentation::default(), IndentCaptureType::OutdentAlways)
);
// adding an indent to an already indented has no effect
assert_eq!(indent(), add_capture(indent(), IndentCaptureType::Indent));
assert_eq!(
outdent(),
add_capture(outdent(), IndentCaptureType::Outdent)
);
// adding an always to a regular makes it always
assert_eq!(
indent_always(),
add_capture(indent(), IndentCaptureType::IndentAlways)
);
assert_eq!(
outdent_always(),
add_capture(outdent(), IndentCaptureType::OutdentAlways)
);
// adding an always to an always is additive
assert_eq!(
Indentation {
indent_always: 2,
..Default::default()
},
add_capture(indent_always(), IndentCaptureType::IndentAlways)
);
assert_eq!(
Indentation {
outdent_always: 2,
..Default::default()
},
add_capture(outdent_always(), IndentCaptureType::OutdentAlways)
);
// adding regular to always should be associative
assert_eq!(
Indentation {
indent_always: 1,
..Default::default()
},
add_capture(
add_capture(indent(), IndentCaptureType::Indent),
IndentCaptureType::IndentAlways
)
);
assert_eq!(
Indentation {
indent_always: 1,
..Default::default()
},
add_capture(
add_capture(indent(), IndentCaptureType::IndentAlways),
IndentCaptureType::Indent
)
);
assert_eq!(
Indentation {
outdent_always: 1,
..Default::default()
},
add_capture(
add_capture(outdent(), IndentCaptureType::Outdent),
IndentCaptureType::OutdentAlways
)
);
assert_eq!(
Indentation {
outdent_always: 1,
..Default::default()
},
add_capture(
add_capture(outdent(), IndentCaptureType::OutdentAlways),
IndentCaptureType::Outdent
)
);
}
}

@ -7,6 +7,7 @@ pub mod config;
pub mod diagnostic;
pub mod diff;
pub mod doc_formatter;
pub mod fuzzy;
pub mod graphemes;
pub mod history;
pub mod increment;
@ -18,7 +19,6 @@ pub mod movement;
pub mod object;
pub mod path;
mod position;
pub mod register;
pub mod search;
pub mod selection;
pub mod shellwords;
@ -41,7 +41,9 @@ 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;
pub use rope_reader::RopeReader;
pub use ropey::{self, str_utils, Rope, RopeBuilder, RopeSlice};
// pub use tendril::StrTendril as Tendril;
@ -66,5 +68,5 @@ pub use syntax::Syntax;
pub use diagnostic::Diagnostic;
pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};

@ -1,9 +1,9 @@
use crate::{Rope, RopeSlice};
#[cfg(target_os = "windows")]
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::Crlf;
pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::Crlf;
#[cfg(not(target_os = "windows"))]
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::LF;
pub const NATIVE_LINE_ENDING: LineEnding = LineEnding::LF;
/// Represents one of the valid Unicode line endings.
#[derive(PartialEq, Eq, Copy, Clone, Debug)]

@ -1,7 +1,15 @@
use std::iter;
use ropey::RopeSlice;
use tree_sitter::Node;
use crate::{Rope, Syntax};
use crate::movement::Direction::{self, Backward, Forward};
use crate::Syntax;
const MAX_PLAINTEXT_SCAN: usize = 10000;
const MATCH_LIMIT: usize = 16;
// Limit matching pairs to only ( ) { } [ ] < > ' ' " "
const PAIRS: &[(char, char)] = &[
('(', ')'),
('{', '}'),
@ -11,17 +19,17 @@ const PAIRS: &[(char, char)] = &[
('\"', '\"'),
];
// limit matching pairs to only ( ) { } [ ] < > ' ' " "
// Returns the position of the matching bracket under cursor.
//
// If the cursor is one the opening bracket, the position of
// the closing bracket is returned. If the cursor in the closing
// bracket, the position of the opening bracket is returned.
//
// If the cursor is not on a bracket, `None` is returned.
/// Returns the position of the matching bracket under cursor.
///
/// If the cursor is on the opening bracket, the position of
/// the closing bracket is returned. If the cursor on the closing
/// bracket, the position of the opening bracket is returned.
///
/// If the cursor is not on a bracket, `None` is returned.
///
/// If no matching bracket is found, `None` is returned.
#[must_use]
pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, 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)) {
return None;
}
@ -39,46 +47,159 @@ pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<
//
// If no surrounding scope is found, the function returns `None`.
#[must_use]
pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option<usize> {
find_pair(syntax, doc, pos, true)
}
fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option<usize> {
fn find_pair(
syntax: &Syntax,
doc: RopeSlice,
pos_: usize,
traverse_parents: bool,
) -> 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().named_descendant_for_byte_range(pos, pos)?;
let mut node = tree.root_node().descendant_for_byte_range(pos, pos)?;
loop {
let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
if node.is_named() {
let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
if is_valid_pair(doc, start_char, end_char) {
if end_byte == pos {
return Some(start_char);
}
if is_valid_pair(doc, start_char, end_char) {
if end_byte == pos {
return Some(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.
if traverse_parents || start_byte == pos {
return Some(end_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.
return Some(end_char);
}
// this node itselt wasn't a pair but maybe its siblings are
if traverse_parents {
node = node.parent()?;
} else {
return None;
// check if we are *on* the pair (special cased so we don't look
// at the current node twice and to jump to the start on that case)
if let Some(open) = as_close_pair(doc, &node) {
if let Some(pair_start) = find_pair_end(doc, node.prev_sibling(), open, Backward) {
return Some(pair_start);
}
}
if !traverse_parents {
// check if we are *on* the opening pair (special cased here as
// 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);
}
}
if node.is_named() {
break;
}
}
for close in
iter::successors(node.next_sibling(), |node| node.next_sibling()).take(MATCH_LIMIT)
{
let Some(open) = as_close_pair(doc, &close) else {
continue;
};
if find_pair_end(doc, Some(node), open, Backward).is_some() {
return doc.try_byte_to_char(close.start_byte()).ok();
}
}
let Some(parent) = node.parent() else {
break;
};
node = parent;
}
let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
if node.child_count() != 0 {
return None;
}
let node_start = doc.byte_to_char(node.start_byte());
find_matching_bracket_plaintext(doc.byte_slice(node.byte_range()), pos_ - node_start)
.map(|pos| pos + node_start)
}
/// Returns the position of the matching bracket under cursor.
/// This function works on plain text and ignores tree-sitter grammar.
/// The search is limited to `MAX_PLAINTEXT_SCAN` characters
///
/// If the cursor is on the opening bracket, the position of
/// the closing bracket is returned. If the cursor on the closing
/// bracket, the position of the opening bracket is returned.
///
/// If the cursor is not on a bracket, `None` is returned.
///
/// If no matching bracket is found, `None` is returned.
#[must_use]
pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> {
// 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) {
return None;
}
// Determine the direction of the matching.
let is_fwd = is_forward_bracket(bracket);
let chars_iter = if is_fwd {
doc.chars_at(cursor_pos + 1)
} else {
doc.chars_at(cursor_pos).reversed()
};
let mut open_cnt = 1;
for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() {
if candidate == bracket {
open_cnt += 1;
} else if is_valid_pair(
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.
if open_cnt == 1 {
return Some(if is_fwd {
cursor_pos + i + 1
} else {
cursor_pos - i - 1
});
}
open_cnt -= 1;
}
}
None
}
fn is_valid_bracket(c: char) -> bool {
PAIRS.iter().any(|(l, r)| *l == c || *r == c)
}
fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool {
fn is_forward_bracket(c: char) -> bool {
PAIRS.iter().any(|(l, _)| *l == c)
}
fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool {
PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
}
fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
fn surrounding_bytes(doc: RopeSlice, node: &Node) -> Option<(usize, usize)> {
let len = doc.len_bytes();
let start_byte = node.start_byte();
@ -90,3 +211,85 @@ fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
Some((start_byte, end_byte))
}
/// 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> {
let close = as_char(doc, node)?.1;
PAIRS
.iter()
.find_map(|&(open, close_)| (close_ == close).then_some(open))
}
/// Checks if `node` or its siblings (at most MATCH_LIMIT nodes) is the specified closing char
///
/// # Returns
///
/// The position of the found node or `None` otherwise
fn find_pair_end(
doc: RopeSlice,
node: Option<Node>,
end_char: char,
direction: Direction,
) -> Option<usize> {
let advance = match direction {
Forward => Node::next_sibling,
Backward => Node::prev_sibling,
};
iter::successors(node, advance)
.take(MATCH_LIMIT)
.find_map(|node| {
let (pos, c) = as_char(doc, &node)?;
(end_char == c).then_some(pos)
})
}
/// Tests if this node is a pair close char and returns the expected open char
fn as_open_pair(doc: RopeSlice, node: &Node) -> Option<char> {
let open = as_char(doc, node)?.1;
PAIRS
.iter()
.find_map(|&(open_, close)| (open_ == open).then_some(close))
}
/// If node is a single char return it (and its char position)
fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> {
// TODO: multi char/non ASCII pairs
if node.byte_range().len() != 1 {
return None;
}
let pos = doc.try_byte_to_char(node.start_byte()).ok()?;
Some((pos, doc.char(pos)))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_find_matching_bracket_current_line_plaintext() {
let assert = |input: &str, pos, expected| {
let input = RopeSlice::from(input);
let actual = find_matching_bracket_plaintext(input, pos);
assert_eq!(expected, actual.unwrap());
let actual = find_matching_bracket_plaintext(input, expected);
assert_eq!(pos, actual.unwrap(), "expected symmetrical behaviour");
};
assert("(hello)", 0, 6);
assert("((hello))", 0, 8);
assert("((hello))", 1, 7);
assert("(((hello)))", 2, 8);
assert("key: ${value}", 6, 12);
assert("key: ${value} # (some comment)", 16, 29);
assert("(paren (paren {bracket}))", 0, 24);
assert("(paren (paren {bracket}))", 7, 23);
assert("(paren (paren {bracket}))", 14, 22);
assert("(prev line\n ) (middle) ( \n next line)", 0, 12);
assert("(prev line\n ) (middle) ( \n next line)", 14, 21);
assert("(prev line\n ) (middle) ( \n next line)", 23, 36);
}
}

@ -1,4 +1,4 @@
use std::iter;
use std::{cmp::Reverse, iter};
use ropey::iter::Chars;
use tree_sitter::{Node, QueryCursor};
@ -16,7 +16,7 @@ use crate::{
syntax::LanguageConfiguration,
text_annotations::TextAnnotations,
textobject::TextObject,
visual_offset_from_block, Range, RopeSlice,
visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
@ -62,7 +62,7 @@ pub fn move_vertically_visual(
annotations: &mut TextAnnotations,
) -> Range {
if !text_fmt.soft_wrap {
move_vertically(slice, range, dir, count, behaviour, text_fmt, annotations);
return move_vertically(slice, range, dir, count, behaviour, text_fmt, annotations);
}
annotations.clear_line_annotations();
let pos = range.cursor(slice);
@ -177,6 +177,10 @@ pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Ran
word_move(slice, range, count, WordMotionTarget::PrevWordStart)
}
pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevWordEnd)
}
pub fn move_next_long_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::NextLongWordStart)
}
@ -189,8 +193,8 @@ pub fn move_prev_long_word_start(slice: RopeSlice, range: Range, count: usize) -
word_move(slice, range, count, WordMotionTarget::PrevLongWordStart)
}
pub fn move_prev_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevWordEnd)
pub fn move_prev_long_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevLongWordEnd)
}
fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTarget) -> Range {
@ -199,6 +203,7 @@ fn word_move(slice: RopeSlice, range: Range, count: usize, target: WordMotionTar
WordMotionTarget::PrevWordStart
| WordMotionTarget::PrevLongWordStart
| WordMotionTarget::PrevWordEnd
| WordMotionTarget::PrevLongWordEnd
);
// Special-case early-out.
@ -377,6 +382,7 @@ pub enum WordMotionTarget {
NextLongWordStart,
NextLongWordEnd,
PrevLongWordStart,
PrevLongWordEnd,
}
pub trait CharHelpers {
@ -393,6 +399,7 @@ impl CharHelpers for Chars<'_> {
WordMotionTarget::PrevWordStart
| WordMotionTarget::PrevLongWordStart
| WordMotionTarget::PrevWordEnd
| WordMotionTarget::PrevLongWordEnd
);
// Reverse the iterator if needed for the motion direction.
@ -479,7 +486,7 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
is_word_boundary(prev_ch, next_ch)
&& (!prev_ch.is_whitespace() || char_is_line_ending(next_ch))
}
WordMotionTarget::NextLongWordStart => {
WordMotionTarget::NextLongWordStart | WordMotionTarget::PrevLongWordEnd => {
is_long_word_boundary(prev_ch, next_ch)
&& (char_is_line_ending(next_ch) || !next_ch.is_whitespace())
}
@ -520,10 +527,10 @@ pub fn goto_treesitter_object(
let node = match dir {
Direction::Forward => nodes
.filter(|n| n.start_byte() > byte_pos)
.min_by_key(|n| n.start_byte())?,
.min_by_key(|n| (n.start_byte(), Reverse(n.end_byte())))?,
Direction::Backward => nodes
.filter(|n| n.end_byte() < byte_pos)
.max_by_key(|n| n.end_byte())?,
.max_by_key(|n| (n.end_byte(), Reverse(n.start_byte())))?,
};
let len = slice.len_bytes();
@ -549,6 +556,85 @@ pub fn goto_treesitter_object(
last_range
}
fn find_parent_start(mut node: Node) -> Option<Node> {
let start = node.start_byte();
while node.start_byte() >= start || !node.is_named() {
node = node.parent()?;
}
Some(node)
}
pub fn move_parent_node_end(
syntax: &Syntax,
text: RopeSlice,
selection: Selection,
dir: Direction,
movement: Movement,
) -> Selection {
let tree = syntax.tree();
selection.transform(|range| {
let start_from = text.char_to_byte(range.from());
let start_to = text.char_to_byte(range.to());
let mut node = match tree
.root_node()
.named_descendant_for_byte_range(start_from, start_to)
{
Some(node) => node,
None => {
log::debug!(
"no descendant found for byte range: {} - {}",
start_from,
start_to
);
return range;
}
};
let mut end_head = match dir {
// moving forward, we always want to move one past the end of the
// current node, so use the end byte of the current node, which is an exclusive
// end of the range
Direction::Forward => text.byte_to_char(node.end_byte()),
// moving backward, we want the cursor to land on the start char of
// the current node, or if it is already at the start of a node, to traverse up to
// the parent
Direction::Backward => {
let end_head = text.byte_to_char(node.start_byte());
// if we're already on the beginning, look up to the parent
if end_head == range.cursor(text) {
node = find_parent_start(node).unwrap_or(node);
text.byte_to_char(node.start_byte())
} else {
end_head
}
}
};
if movement == Movement::Move {
// preserve direction of original range
if range.direction() == Direction::Forward {
Range::new(end_head, end_head + 1)
} else {
Range::new(end_head + 1, end_head)
}
} else {
// if we end up with a forward range, then adjust it to be one past
// where we want
if end_head >= range.anchor {
end_head += 1;
}
Range::new(range.anchor, end_head)
}
})
}
#[cfg(test)]
mod test {
use ropey::Rope;
@ -1445,6 +1531,100 @@ mod test {
}
}
#[test]
fn test_behaviour_when_moving_to_end_of_prev_long_words() {
let tests = [
(
"Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(4, 0))],
),
("Starting from after boundary retreats the anchor",
vec![(1, Range::new(0, 9), Range::new(8, 0))],
),
(
"Jump to end of a word succeeded by whitespace",
vec![(1, Range::new(10, 10), Range::new(10, 4))],
),
(
" Jump to start of line from end of word preceded by whitespace",
vec![(1, Range::new(3, 4), Range::new(4, 0))],
),
("Previous anchor is irrelevant for backward motions",
vec![(1, Range::new(12, 5), Range::new(6, 0))]),
(
" Starting from whitespace moves to first space in sequence",
vec![(1, Range::new(0, 4), Range::new(4, 0))],
),
("Identifiers_with_underscores are considered a single word",
vec![(1, Range::new(0, 20), Range::new(20, 0))]),
(
"Jumping\n \nback through a newline selects whitespace",
vec![(1, Range::new(0, 13), Range::new(12, 8))],
),
(
"Jumping to start of word from the end selects the word",
vec![(1, Range::new(6, 7), Range::new(7, 0))],
),
(
"alphanumeric.!,and.?=punctuation are treated exactly the same",
vec![(1, Range::new(29, 30), Range::new(30, 0))],
),
(
"... ... punctuation and spaces behave as expected",
vec![
(1, Range::new(0, 10), Range::new(9, 3)),
(1, Range::new(10, 6), Range::new(7, 3)),
],
),
(".._.._ punctuation is joined by underscores into a single block",
vec![(1, Range::new(0, 6), Range::new(6, 0))]),
(
"Newlines\n\nare bridged seamlessly.",
vec![(1, Range::new(0, 10), Range::new(8, 0))],
),
(
"Jumping \n\n\n\n\nback from within a newline group selects previous block",
vec![(1, Range::new(0, 13), Range::new(11, 7))],
),
(
"Failed motions do not modify the range",
vec![(0, Range::new(3, 0), Range::new(3, 0))],
),
(
"Multiple motions at once resolve correctly",
vec![(3, Range::new(19, 19), Range::new(8, 0))],
),
(
"Excessive motions are performed partially",
vec![(999, Range::new(40, 40), Range::new(9, 0))],
),
(
"", // Edge case of moving backwards in empty string
vec![(1, Range::new(0, 0), Range::new(0, 0))],
),
(
"\n\n\n\n\n", // Edge case of moving backwards in all newlines
vec![(1, Range::new(5, 5), Range::new(0, 0))],
),
(" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
vec![
(1, Range::new(0, 8), Range::new(7, 4)),
(1, Range::new(7, 4), Range::new(3, 0)),
]),
("ヒーリ..クス multibyte characters behave as normal characters, including when interacting with punctuation",
vec![
(1, Range::new(0, 8), Range::new(7, 0)),
]),
];
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_prev_long_word_end(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
#[test]
fn test_behaviour_when_moving_to_prev_paragraph_single() {
let tests = [

@ -85,23 +85,21 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
///
/// 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) -> std::io::Result<PathBuf> {
pub fn get_canonicalized_path(path: &Path) -> PathBuf {
let path = expand_tilde(path);
let path = if path.is_relative() {
std::env::current_dir().map(|current_dir| current_dir.join(path))?
helix_loader::current_working_dir().join(path)
} else {
path
};
Ok(get_normalized_path(path.as_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 = std::env::current_dir()
.map(|path| get_normalized_path(&path))
.expect("couldn't determine current directory");
let cwdir = get_normalized_path(&helix_loader::current_working_dir());
get_normalized_path(&path)
.strip_prefix(cwdir)
.map(PathBuf::from)
@ -142,7 +140,7 @@ pub fn get_relative_path(path: &Path) -> PathBuf {
/// ```
///
pub fn get_truncated_path<P: AsRef<Path>>(path: P) -> PathBuf {
let cwd = std::env::current_dir().unwrap_or_default();
let cwd = helix_loader::current_working_dir();
let path = path
.as_ref()
.strip_prefix(cwd)

@ -1,81 +0,0 @@
use std::collections::HashMap;
#[derive(Debug)]
pub struct Register {
name: char,
values: Vec<String>,
}
impl Register {
pub const fn new(name: char) -> Self {
Self {
name,
values: Vec::new(),
}
}
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
Self { name, values }
}
pub const fn name(&self) -> char {
self.name
}
pub fn read(&self) -> &[String] {
&self.values
}
pub fn write(&mut self, values: Vec<String>) {
self.values = values;
}
pub fn push(&mut self, value: String) {
self.values.push(value);
}
}
/// Currently just wraps a `HashMap` of `Register`s
#[derive(Debug, Default)]
pub struct Registers {
inner: HashMap<char, Register>,
}
impl Registers {
pub fn get(&self, name: char) -> Option<&Register> {
self.inner.get(&name)
}
pub fn read(&self, name: char) -> Option<&[String]> {
self.get(name).map(|reg| reg.read())
}
pub fn write(&mut self, name: char, values: Vec<String>) {
if name != '_' {
self.inner
.insert(name, Register::new_with_values(name, values));
}
}
pub fn push(&mut self, name: char, value: String) {
if name != '_' {
if let Some(r) = self.inner.get_mut(&name) {
r.push(value);
} else {
self.write(name, vec![value]);
}
}
}
pub fn first(&self, name: char) -> Option<&String> {
self.read(name).and_then(|entries| entries.first())
}
pub fn last(&self, name: char) -> Option<&String> {
self.read(name).and_then(|entries| entries.last())
}
pub fn inner(&self) -> &HashMap<char, Register> {
&self.inner
}
}

@ -0,0 +1,37 @@
use std::io;
use ropey::iter::Chunks;
use ropey::RopeSlice;
pub struct RopeReader<'a> {
current_chunk: &'a [u8],
chunks: Chunks<'a>,
}
impl<'a> RopeReader<'a> {
pub fn new(rope: RopeSlice<'a>) -> RopeReader<'a> {
RopeReader {
current_chunk: &[],
chunks: rope.chunks(),
}
}
}
impl io::Read for RopeReader<'_> {
fn read(&mut self, mut buf: &mut [u8]) -> io::Result<usize> {
let buf_len = buf.len();
loop {
let read_bytes = self.current_chunk.read(buf)?;
buf = &mut buf[read_bytes..];
if buf.is_empty() {
return Ok(buf_len);
}
if let Some(next_chunk) = self.chunks.next() {
self.current_chunk = next_chunk.as_bytes();
} else {
return Ok(buf_len - buf.len());
}
}
}
}

@ -161,34 +161,35 @@ impl Range {
self.from() <= pos && pos < self.to()
}
/// Map a range through a set of changes. Returns a new range representing the same position
/// after the changes are applied.
pub fn map(self, changes: &ChangeSet) -> Self {
/// Map a range through a set of changes. Returns a new range representing
/// the same position after the changes are applied. Note that this
/// 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
/// complexity is then O(MN) (for multicuror M=N usually). Instead use
/// [Selection::map] or [ChangeSet::update_positions] instead
pub fn map(mut self, changes: &ChangeSet) -> Self {
use std::cmp::Ordering;
let (anchor, head) = match self.anchor.cmp(&self.head) {
Ordering::Equal => (
changes.map_pos(self.anchor, Assoc::After),
changes.map_pos(self.head, Assoc::After),
),
Ordering::Less => (
changes.map_pos(self.anchor, Assoc::After),
changes.map_pos(self.head, Assoc::Before),
),
Ordering::Greater => (
changes.map_pos(self.anchor, Assoc::Before),
changes.map_pos(self.head, Assoc::After),
),
};
// We want to return a new `Range` with `horiz == None` every time,
// even if the anchor and head haven't changed, because we don't
// know if the *visual* position hasn't changed due to
// character-width or grapheme changes earlier in the text.
Self {
anchor,
head,
old_visual_position: None,
if changes.is_empty() {
return self;
}
let positions_to_map = match self.anchor.cmp(&self.head) {
Ordering::Equal => [
(&mut self.anchor, Assoc::After),
(&mut self.head, Assoc::After),
],
Ordering::Less => [
(&mut self.anchor, Assoc::After),
(&mut self.head, Assoc::Before),
],
Ordering::Greater => [
(&mut self.head, Assoc::After),
(&mut self.anchor, Assoc::Before),
],
};
changes.update_positions(positions_to_map.into_iter());
self.old_visual_position = None;
self
}
/// Extend the range to cover at least `from` `to`.
@ -451,17 +452,36 @@ impl Selection {
/// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document.
pub fn map(self, changes: &ChangeSet) -> Self {
self.map_no_normalize(changes).normalize()
}
/// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document. Doesn't normalize the selection
pub fn map_no_normalize(mut self, changes: &ChangeSet) -> Self {
if changes.is_empty() {
return self;
}
Self::new(
self.ranges
.into_iter()
.map(|range| range.map(changes))
.collect(),
self.primary_index,
)
let positions_to_map = self.ranges.iter_mut().flat_map(|range| {
use std::cmp::Ordering;
range.old_visual_position = None;
match range.anchor.cmp(&range.head) {
Ordering::Equal => [
(&mut range.anchor, Assoc::After),
(&mut range.head, Assoc::After),
],
Ordering::Less => [
(&mut range.anchor, Assoc::After),
(&mut range.head, Assoc::Before),
],
Ordering::Greater => [
(&mut range.head, Assoc::After),
(&mut range.anchor, Assoc::Before),
],
}
});
changes.update_positions(positions_to_map);
self
}
pub fn ranges(&self) -> &[Range] {
@ -497,6 +517,9 @@ impl Selection {
/// Normalizes a `Selection`.
fn normalize(mut self) -> Self {
if self.len() < 2 {
return self;
}
let mut primary = self.ranges[self.primary_index];
self.ranges.sort_unstable_by_key(Range::from);
@ -522,7 +545,14 @@ impl Selection {
self
}
// Merges all ranges that are consecutive
/// Replaces ranges with one spanning from first to last range.
pub fn merge_ranges(self) -> Self {
let first = self.ranges.first().unwrap();
let last = self.ranges.last().unwrap();
Selection::new(smallvec![first.merge(*last)], 0)
}
/// Merges all ranges that are consecutive.
pub fn merge_consecutive_ranges(mut self) -> Self {
let mut primary = self.ranges[self.primary_index];
@ -554,17 +584,12 @@ impl Selection {
assert!(!ranges.is_empty());
debug_assert!(primary_index < ranges.len());
let mut selection = Self {
let selection = Self {
ranges,
primary_index,
};
if selection.ranges.len() > 1 {
// TODO: only normalize if needed (any ranges out of order)
selection = selection.normalize();
}
selection
selection.normalize()
}
/// Takes a closure and maps each `Range` over the closure.
@ -605,11 +630,19 @@ impl Selection {
self.transform(|range| Range::point(range.cursor(text)))
}
pub fn fragments<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = Cow<str>> + 'a {
pub fn fragments<'a>(
&'a self,
text: RopeSlice<'a>,
) -> impl DoubleEndedIterator<Item = Cow<'a, str>> + ExactSizeIterator<Item = Cow<str>> + 'a
{
self.ranges.iter().map(move |range| range.fragment(text))
}
pub fn slices<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = RopeSlice> + 'a {
pub fn slices<'a>(
&'a self,
text: RopeSlice<'a>,
) -> impl DoubleEndedIterator<Item = RopeSlice<'a>> + ExactSizeIterator<Item = RopeSlice<'a>> + 'a
{
self.ranges.iter().map(move |range| range.slice(text))
}

@ -397,15 +397,10 @@ mod test {
let selections: SmallVec<[Range; 1]> = spec
.match_indices('^')
.into_iter()
.map(|(i, _)| Range::point(i))
.collect();
let expectations: Vec<usize> = spec
.match_indices('_')
.into_iter()
.map(|(i, _)| i)
.collect();
let expectations: Vec<usize> = spec.match_indices('_').map(|(i, _)| i).collect();
(rope, Selection::new(selections, 0), expectations)
}

@ -4,7 +4,7 @@ use crate::{
diagnostic::Severity,
regex::Regex,
transaction::{ChangeSet, Operation},
Rope, RopeSlice, Tendril,
RopeSlice, Tendril,
};
use ahash::RandomState;
@ -16,8 +16,8 @@ use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{
borrow::Cow,
cell::RefCell,
collections::{HashMap, VecDeque},
fmt,
collections::{HashMap, HashSet, VecDeque},
fmt::{self, Display},
hash::{Hash, Hasher},
mem::{replace, transmute},
path::{Path, PathBuf},
@ -26,7 +26,7 @@ use std::{
};
use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize};
use serde::{ser::SerializeSeq, Deserialize, Serialize};
use helix_loader::grammar::{get_language, load_runtime_file};
@ -48,6 +48,21 @@ where
.transpose()
}
fn deserialize_tab_width<'de, D>(deserializer: D) -> Result<usize, D::Error>
where
D: serde::Deserializer<'de>,
{
usize::deserialize(deserializer).and_then(|n| {
if n > 0 && n <= 16 {
Ok(n)
} else {
Err(serde::de::Error::custom(
"tab width must be a value from 1 to 16 inclusive",
))
}
})
}
pub fn deserialize_auto_pairs<'de, D>(deserializer: D) -> Result<Option<AutoPairs>, D::Error>
where
D: serde::Deserializer<'de>,
@ -60,8 +75,11 @@ fn default_timeout() -> u64 {
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
#[serde(default)]
pub language_server: HashMap<String, LanguageServerConfiguration>,
}
impl Default for Configuration {
@ -75,7 +93,10 @@ impl Default for Configuration {
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
pub language_id: String, // c-sharp, rust
pub language_id: String, // c-sharp, rust, tsx
#[serde(rename = "language-id")]
// see the table under https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocumentItem
pub language_server_language_id: Option<String>, // csharp, rust, typescriptreact, for the language-server
pub scope: String, // source.rust
pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
#[serde(default)]
@ -85,9 +106,6 @@ pub struct LanguageConfiguration {
pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default)]
pub auto_format: bool,
@ -107,8 +125,13 @@ pub struct LanguageConfiguration {
#[serde(skip)]
pub(crate) highlight_config: OnceCell<Option<Arc<HighlightConfiguration>>>,
// tags_config OnceCell<> https://github.com/tree-sitter/tree-sitter/pull/583
#[serde(skip_serializing_if = "Option::is_none")]
pub language_server: Option<LanguageServerConfiguration>,
#[serde(
default,
skip_serializing_if = "Vec::is_empty",
serialize_with = "serialize_lang_features",
deserialize_with = "deserialize_lang_features"
)]
pub language_servers: Vec<LanguageServerFeatures>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indent: Option<IndentationConfiguration>,
@ -187,9 +210,12 @@ impl<'de> Deserialize<'de> for FileType {
M: serde::de::MapAccess<'de>,
{
match map.next_entry::<String, String>()? {
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix(
suffix.replace('/', &std::path::MAIN_SEPARATOR.to_string()),
)),
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
// FIXME: use `suffix.replace('/', std::path::MAIN_SEPARATOR_STR)`
// if MSRV is updated to 1.68
let mut separator = [0; 1];
suffix.replace('/', std::path::MAIN_SEPARATOR.encode_utf8(&mut separator))
})),
Some((key, _value)) => Err(serde::de::Error::custom(format!(
"unknown key in `file-types` list: {}",
key
@ -205,6 +231,133 @@ impl<'de> Deserialize<'de> for FileType {
}
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum LanguageServerFeature {
Format,
GotoDeclaration,
GotoDefinition,
GotoTypeDefinition,
GotoReference,
GotoImplementation,
// Goto, use bitflags, combining previous Goto members?
SignatureHelp,
Hover,
DocumentHighlight,
Completion,
CodeAction,
WorkspaceCommand,
DocumentSymbols,
WorkspaceSymbols,
// Symbols, use bitflags, see above?
Diagnostics,
RenameSymbol,
InlayHints,
}
impl Display for LanguageServerFeature {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
use LanguageServerFeature::*;
let feature = match self {
Format => "format",
GotoDeclaration => "goto-declaration",
GotoDefinition => "goto-definition",
GotoTypeDefinition => "goto-type-definition",
GotoReference => "goto-type-definition",
GotoImplementation => "goto-implementation",
SignatureHelp => "signature-help",
Hover => "hover",
DocumentHighlight => "document-highlight",
Completion => "completion",
CodeAction => "code-action",
WorkspaceCommand => "workspace-command",
DocumentSymbols => "document-symbols",
WorkspaceSymbols => "workspace-symbols",
Diagnostics => "diagnostics",
RenameSymbol => "rename-symbol",
InlayHints => "inlay-hints",
};
write!(f, "{feature}",)
}
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(untagged, rename_all = "kebab-case", deny_unknown_fields)]
enum LanguageServerFeatureConfiguration {
#[serde(rename_all = "kebab-case")]
Features {
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
only_features: HashSet<LanguageServerFeature>,
#[serde(default, skip_serializing_if = "HashSet::is_empty")]
except_features: HashSet<LanguageServerFeature>,
name: String,
},
Simple(String),
}
#[derive(Debug, Default)]
pub struct LanguageServerFeatures {
pub name: String,
pub only: HashSet<LanguageServerFeature>,
pub excluded: HashSet<LanguageServerFeature>,
}
impl LanguageServerFeatures {
pub fn has_feature(&self, feature: LanguageServerFeature) -> bool {
(self.only.is_empty() || self.only.contains(&feature)) && !self.excluded.contains(&feature)
}
}
fn deserialize_lang_features<'de, D>(
deserializer: D,
) -> Result<Vec<LanguageServerFeatures>, D::Error>
where
D: serde::Deserializer<'de>,
{
let raw: Vec<LanguageServerFeatureConfiguration> = Deserialize::deserialize(deserializer)?;
let res = raw
.into_iter()
.map(|config| match config {
LanguageServerFeatureConfiguration::Simple(name) => LanguageServerFeatures {
name,
..Default::default()
},
LanguageServerFeatureConfiguration::Features {
only_features,
except_features,
name,
} => LanguageServerFeatures {
name,
only: only_features,
excluded: except_features,
},
})
.collect();
Ok(res)
}
fn serialize_lang_features<S>(
map: &Vec<LanguageServerFeatures>,
serializer: S,
) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let mut serializer = serializer.serialize_seq(Some(map.len()))?;
for features in map {
let features = if features.only.is_empty() && features.excluded.is_empty() {
LanguageServerFeatureConfiguration::Simple(features.name.to_owned())
} else {
LanguageServerFeatureConfiguration::Features {
only_features: features.only.clone(),
except_features: features.excluded.clone(),
name: features.name.to_owned(),
}
};
serializer.serialize_element(&features)?;
}
serializer.end()
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
@ -214,9 +367,10 @@ pub struct LanguageServerConfiguration {
pub args: Vec<String>,
#[serde(default, skip_serializing_if = "HashMap::is_empty")]
pub environment: HashMap<String, String>,
#[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
pub config: Option<serde_json::Value>,
#[serde(default = "default_timeout")]
pub timeout: u64,
pub language_id: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@ -285,6 +439,7 @@ pub struct DebuggerQuirks {
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndentationConfiguration {
#[serde(deserialize_with = "deserialize_tab_width")]
pub tab_width: usize,
pub unit: String,
}
@ -591,6 +746,8 @@ pub struct Loader {
language_config_ids_by_suffix: HashMap<String, usize>,
language_config_ids_by_shebang: HashMap<String, usize>,
language_server_configs: HashMap<String, LanguageServerConfiguration>,
scopes: ArcSwap<Vec<String>>,
}
@ -598,6 +755,7 @@ impl Loader {
pub fn new(config: Configuration) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_server_configs: config.language_server,
language_config_ids_by_extension: HashMap::new(),
language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
@ -660,11 +818,13 @@ impl Loader {
// TODO: content_regex handling conflict resolution
}
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
pub fn language_config_for_shebang(
&self,
source: RopeSlice,
) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0));
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)").unwrap()
});
static SHEBANG_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(&["^", SHEBANG].concat()).unwrap());
let configuration_id = SHEBANG_REGEX
.captures(&line)
.and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
@ -686,15 +846,14 @@ impl Loader {
.cloned()
}
pub fn language_configuration_for_injection_string(
&self,
string: &str,
) -> Option<Arc<LanguageConfiguration>> {
/// Unlike language_config_for_language_id, which only returns Some for an exact id, this
/// function will perform a regex match on the given string to find the closest language match.
pub fn language_config_for_name(&self, name: &str) -> Option<Arc<LanguageConfiguration>> {
let mut best_match_length = 0;
let mut best_match_position = None;
for (i, configuration) in self.language_configs.iter().enumerate() {
if let Some(injection_regex) = &configuration.injection_regex {
if let Some(mat) = injection_regex.find(string) {
if let Some(mat) = injection_regex.find(name) {
let length = mat.end() - mat.start();
if length > best_match_length {
best_match_position = Some(i);
@ -704,17 +863,30 @@ impl Loader {
}
}
if let Some(i) = best_match_position {
let configuration = &self.language_configs[i];
return Some(configuration.clone());
best_match_position.map(|i| self.language_configs[i].clone())
}
pub fn language_configuration_for_injection_string(
&self,
capture: &InjectionLanguageMarker,
) -> Option<Arc<LanguageConfiguration>> {
match capture {
InjectionLanguageMarker::Name(string) => self.language_config_for_name(string),
InjectionLanguageMarker::Filename(file) => self.language_config_for_file_name(file),
InjectionLanguageMarker::Shebang(shebang) => {
self.language_config_for_language_id(shebang)
}
}
None
}
pub fn language_configs(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter()
}
pub fn language_server_configs(&self) -> &HashMap<String, LanguageServerConfiguration> {
&self.language_server_configs
}
pub fn set_scopes(&self, scopes: Vec<String>) {
self.scopes.store(Arc::new(scopes));
@ -758,7 +930,11 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
}
impl Syntax {
pub fn new(source: &Rope, config: Arc<HighlightConfiguration>, loader: Arc<Loader>) -> Self {
pub fn new(
source: RopeSlice,
config: Arc<HighlightConfiguration>,
loader: Arc<Loader>,
) -> Option<Self> {
let root_layer = LanguageLayer {
tree: None,
config,
@ -783,24 +959,26 @@ impl Syntax {
loader,
};
syntax
.update(source, source, &ChangeSet::new(source))
.unwrap();
let res = syntax.update(source, source, &ChangeSet::new(source));
syntax
if res.is_err() {
log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
return None;
}
Some(syntax)
}
pub fn update(
&mut self,
old_source: &Rope,
source: &Rope,
old_source: RopeSlice,
source: RopeSlice,
changeset: &ChangeSet,
) -> Result<(), Error> {
let mut queue = VecDeque::new();
queue.push_back(self.root);
let scopes = self.loader.scopes.load();
let injection_callback = |language: &str| {
let injection_callback = |language: &InjectionLanguageMarker| {
self.loader
.language_configuration_for_injection_string(language)
.and_then(|language_config| language_config.highlight_config(&scopes))
@ -915,6 +1093,7 @@ impl Syntax {
PARSER.with(|ts_parser| {
let ts_parser = &mut ts_parser.borrow_mut();
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);
// TODO: might need to set cursor range
cursor.set_byte_range(0..usize::MAX);
@ -959,14 +1138,37 @@ impl Syntax {
layer.tree().root_node(),
RopeProvider(source_slice),
);
let mut combined_injections = vec![
(None, Vec::new(), IncludedChildren::default());
layer.config.combined_injections_patterns.len()
];
let mut injections = Vec::new();
let mut last_injection_end = 0;
for mat in matches {
let (language_name, content_node, included_children) = injection_for_match(
&layer.config,
&layer.config.injections_query,
&mat,
source_slice,
);
let (injection_capture, content_node, included_children) = layer
.config
.injection_for_match(&layer.config.injections_query, &mat, source_slice);
// in case this is a combined injection save it for more processing later
if let Some(combined_injection_idx) = layer
.config
.combined_injections_patterns
.iter()
.position(|&pattern| pattern == mat.pattern_index)
{
let entry = &mut combined_injections[combined_injection_idx];
if injection_capture.is_some() {
entry.0 = injection_capture;
}
if let Some(content_node) = content_node {
if content_node.start_byte() >= last_injection_end {
entry.1.push(content_node);
last_injection_end = content_node.end_byte();
}
}
entry.2 = included_children;
continue;
}
// Explicitly remove this match so that none of its other captures will remain
// in the stream of captures.
@ -974,59 +1176,31 @@ impl Syntax {
// If a language is found with the given name, then add a new language layer
// to the highlighted document.
if let (Some(language_name), Some(content_node)) = (language_name, content_node)
if let (Some(injection_capture), Some(content_node)) =
(injection_capture, content_node)
{
if let Some(config) = (injection_callback)(&language_name) {
if let Some(config) = (injection_callback)(&injection_capture) {
let ranges =
intersect_ranges(&layer.ranges, &[content_node], included_children);
if !ranges.is_empty() {
if content_node.start_byte() < last_injection_end {
continue;
}
last_injection_end = content_node.end_byte();
injections.push((config, ranges));
}
}
}
}
// Process combined injections.
if let Some(combined_injections_query) = &layer.config.combined_injections_query {
let mut injections_by_pattern_index =
vec![
(None, Vec::new(), IncludedChildren::default());
combined_injections_query.pattern_count()
];
let matches = cursor.matches(
combined_injections_query,
layer.tree().root_node(),
RopeProvider(source_slice),
);
for mat in matches {
let entry = &mut injections_by_pattern_index[mat.pattern_index];
let (language_name, content_node, included_children) = injection_for_match(
&layer.config,
combined_injections_query,
&mat,
source_slice,
);
if language_name.is_some() {
entry.0 = language_name;
}
if let Some(content_node) = content_node {
entry.1.push(content_node);
}
entry.2 = included_children;
}
for (lang_name, content_nodes, included_children) in injections_by_pattern_index
{
if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
if let Some(config) = (injection_callback)(&lang_name) {
let ranges = intersect_ranges(
&layer.ranges,
&content_nodes,
included_children,
);
if !ranges.is_empty() {
injections.push((config, ranges));
}
for (lang_name, content_nodes, included_children) in combined_injections {
if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
if let Some(config) = (injection_callback)(&lang_name) {
let ranges =
intersect_ranges(&layer.ranges, &content_nodes, included_children);
if !ranges.is_empty() {
injections.push((config, ranges));
}
}
}
@ -1216,7 +1390,7 @@ impl LanguageLayer {
self.tree.as_ref().unwrap()
}
fn parse(&mut self, parser: &mut Parser, source: &Rope) -> Result<(), Error> {
fn parse(&mut self, parser: &mut Parser, source: RopeSlice) -> Result<(), Error> {
parser
.set_included_ranges(&self.ranges)
.map_err(|_| Error::InvalidRanges)?;
@ -1231,7 +1405,7 @@ impl LanguageLayer {
&mut |byte, _| {
if byte <= source.len_bytes() {
let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
chunk[byte - start_byte..].as_bytes()
&chunk.as_bytes()[byte - start_byte..]
} else {
// out of range
&[]
@ -1247,7 +1421,7 @@ impl LanguageLayer {
}
pub(crate) fn generate_edits(
old_text: &Rope,
old_text: RopeSlice,
changeset: &ChangeSet,
) -> Vec<tree_sitter::InputEdit> {
use Operation::*;
@ -1263,7 +1437,7 @@ pub(crate) fn generate_edits(
// TODO; this is a lot easier with Change instead of Operation.
fn point_at_pos(text: &Rope, pos: usize) -> (usize, Point) {
fn point_at_pos(text: RopeSlice, pos: usize) -> (usize, Point) {
let byte = text.char_to_byte(pos); // <- attempted to index past end
let line = text.char_to_line(pos);
let line_start_byte = text.line_to_byte(line);
@ -1389,12 +1563,14 @@ pub struct HighlightConfiguration {
pub language: Grammar,
pub query: Query,
injections_query: Query,
combined_injections_query: Option<Query>,
combined_injections_patterns: Vec<usize>,
highlights_pattern_index: usize,
highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
non_local_variable_patterns: Vec<bool>,
injection_content_capture_index: Option<u32>,
injection_language_capture_index: Option<u32>,
injection_filename_capture_index: Option<u32>,
injection_shebang_capture_index: Option<u32>,
local_scope_capture_index: Option<u32>,
local_def_capture_index: Option<u32>,
local_def_value_capture_index: Option<u32>,
@ -1438,7 +1614,7 @@ impl<'a> Iterator for ChunksBytes<'a> {
}
pub struct RopeProvider<'a>(pub RopeSlice<'a>);
impl<'a> TextProvider<'a> for RopeProvider<'a> {
impl<'a> TextProvider<&'a [u8]> for RopeProvider<'a> {
type I = ChunksBytes<'a>;
fn text(&mut self, node: Node) -> Self::I {
@ -1452,7 +1628,7 @@ impl<'a> TextProvider<'a> for RopeProvider<'a> {
struct HighlightIterLayer<'a> {
_tree: Option<Tree>,
cursor: QueryCursor,
captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>>,
captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>>>,
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
@ -1503,26 +1679,15 @@ impl HighlightConfiguration {
}
}
let mut injections_query = Query::new(language, injection_query)?;
// Construct a separate query just for dealing with the 'combined injections'.
// Disable the combined injection patterns in the main query.
let mut combined_injections_query = Query::new(language, injection_query)?;
let mut has_combined_queries = false;
for pattern_index in 0..injections_query.pattern_count() {
let settings = injections_query.property_settings(pattern_index);
if settings.iter().any(|s| &*s.key == "injection.combined") {
has_combined_queries = true;
injections_query.disable_pattern(pattern_index);
} else {
combined_injections_query.disable_pattern(pattern_index);
}
}
let combined_injections_query = if has_combined_queries {
Some(combined_injections_query)
} else {
None
};
let injections_query = Query::new(language, injection_query)?;
let combined_injections_patterns = (0..injections_query.pattern_count())
.filter(|&i| {
injections_query
.property_settings(i)
.iter()
.any(|s| &*s.key == "injection.combined")
})
.collect();
// Find all of the highlighting patterns that are disabled for nodes that
// have been identified as local variables.
@ -1538,6 +1703,8 @@ impl HighlightConfiguration {
// Store the numeric ids for all of the special captures.
let mut injection_content_capture_index = None;
let mut injection_language_capture_index = None;
let mut injection_filename_capture_index = None;
let mut injection_shebang_capture_index = None;
let mut local_def_capture_index = None;
let mut local_def_value_capture_index = None;
let mut local_ref_capture_index = None;
@ -1558,6 +1725,8 @@ impl HighlightConfiguration {
match name.as_str() {
"injection.content" => injection_content_capture_index = i,
"injection.language" => injection_language_capture_index = i,
"injection.filename" => injection_filename_capture_index = i,
"injection.shebang" => injection_shebang_capture_index = i,
_ => {}
}
}
@ -1567,12 +1736,14 @@ impl HighlightConfiguration {
language,
query,
injections_query,
combined_injections_query,
combined_injections_patterns,
highlights_pattern_index,
highlight_indices,
non_local_variable_patterns,
injection_content_capture_index,
injection_language_capture_index,
injection_filename_capture_index,
injection_shebang_capture_index,
local_scope_capture_index,
local_def_capture_index,
local_def_value_capture_index,
@ -1631,6 +1802,90 @@ impl HighlightConfiguration {
self.highlight_indices.store(Arc::new(indices));
}
fn injection_pair<'a>(
&self,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (Option<InjectionLanguageMarker<'a>>, Option<Node<'a>>) {
let mut injection_capture = None;
let mut content_node = None;
for capture in query_match.captures {
let index = Some(capture.index);
if index == self.injection_language_capture_index {
let name = byte_range_to_str(capture.node.byte_range(), source);
injection_capture = Some(InjectionLanguageMarker::Name(name));
} else if index == self.injection_filename_capture_index {
let name = byte_range_to_str(capture.node.byte_range(), source);
let path = Path::new(name.as_ref()).to_path_buf();
injection_capture = Some(InjectionLanguageMarker::Filename(path.into()));
} else if index == self.injection_shebang_capture_index {
let node_slice = source.byte_slice(capture.node.byte_range());
// some languages allow space and newlines before the actual string content
// so a shebang could be on either the first or second line
let lines = if let Ok(end) = node_slice.try_line_to_byte(2) {
node_slice.byte_slice(..end)
} else {
node_slice
};
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(SHEBANG).unwrap());
injection_capture = SHEBANG_REGEX
.captures(&Cow::from(lines))
.map(|cap| InjectionLanguageMarker::Shebang(cap[1].to_owned()))
} else if index == self.injection_content_capture_index {
content_node = Some(capture.node);
}
}
(injection_capture, content_node)
}
fn injection_for_match<'a>(
&self,
query: &'a Query,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (
Option<InjectionLanguageMarker<'a>>,
Option<Node<'a>>,
IncludedChildren,
) {
let (mut injection_capture, content_node) = self.injection_pair(query_match, source);
let mut included_children = IncludedChildren::default();
for prop in query.property_settings(query_match.pattern_index) {
match prop.key.as_ref() {
// In addition to specifying the language name via the text of a
// captured node, it can also be hard-coded via a `#set!` predicate
// that sets the injection.language key.
"injection.language" if injection_capture.is_none() => {
injection_capture = prop
.value
.as_ref()
.map(|s| InjectionLanguageMarker::Name(s.as_ref().into()));
}
// By default, injections do not include the *children* of an
// `injection.content` node - only the ranges that belong to the
// node itself. This can be changed using a `#set!` predicate that
// sets the `injection.include-children` key.
"injection.include-children" => included_children = IncludedChildren::All,
// Some queries might only exclude named children but include unnamed
// children in their `injection.content` node. This can be enabled using
// a `#set!` predicate that sets the `injection.include-unnamed-children` key.
"injection.include-unnamed-children" => {
included_children = IncludedChildren::Unnamed
}
_ => {}
}
}
(injection_capture, content_node, included_children)
}
}
impl<'a> HighlightIterLayer<'a> {
@ -2042,56 +2297,15 @@ impl<'a> Iterator for HighlightIter<'a> {
}
}
fn injection_for_match<'a>(
config: &HighlightConfiguration,
query: &'a Query,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (Option<Cow<'a, str>>, Option<Node<'a>>, IncludedChildren) {
let content_capture_index = config.injection_content_capture_index;
let language_capture_index = config.injection_language_capture_index;
let mut language_name = None;
let mut content_node = None;
for capture in query_match.captures {
let index = Some(capture.index);
if index == language_capture_index {
let name = byte_range_to_str(capture.node.byte_range(), source);
language_name = Some(name);
} else if index == content_capture_index {
content_node = Some(capture.node);
}
}
let mut included_children = IncludedChildren::default();
for prop in query.property_settings(query_match.pattern_index) {
match prop.key.as_ref() {
// In addition to specifying the language name via the text of a
// captured node, it can also be hard-coded via a `#set!` predicate
// that sets the injection.language key.
"injection.language" => {
if language_name.is_none() {
language_name = prop.value.as_ref().map(|s| s.as_ref().into())
}
}
// By default, injections do not include the *children* of an
// `injection.content` node - only the ranges that belong to the
// node itself. This can be changed using a `#set!` predicate that
// sets the `injection.include-children` key.
"injection.include-children" => included_children = IncludedChildren::All,
// Some queries might only exclude named children but include unnamed
// children in their `injection.content` node. This can be enabled using
// a `#set!` predicate that sets the `injection.include-unnamed-children` key.
"injection.include-unnamed-children" => included_children = IncludedChildren::Unnamed,
_ => {}
}
}
(language_name, content_node, included_children)
#[derive(Debug, Clone)]
pub enum InjectionLanguageMarker<'a> {
Name(Cow<'a, str>),
Filename(Cow<'a, Path>),
Shebang(String),
}
const SHEBANG: &str = r"#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)";
pub struct Merge<I> {
iter: I,
spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,
@ -2307,7 +2521,10 @@ mod test {
"#,
);
let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap();
@ -2315,7 +2532,7 @@ mod test {
let mut cursor = QueryCursor::new();
let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let root = syntax.tree().root_node();
let mut test = |capture, range| {
@ -2366,7 +2583,10 @@ mod test {
.map(String::from)
.collect();
let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new(
@ -2386,7 +2606,7 @@ mod test {
fn main() {}
",
);
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let tree = syntax.tree();
let root = tree.root_node();
assert_eq!(root.kind(), "source_file");
@ -2413,7 +2633,7 @@ mod test {
&doc,
vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
);
let edits = generate_edits(&doc, transaction.changes());
let edits = generate_edits(doc.slice(..), transaction.changes());
// transaction.apply(&mut state);
assert_eq!(
@ -2442,7 +2662,7 @@ mod test {
let mut doc = Rope::from("fn test() {}");
let transaction =
Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter());
let edits = generate_edits(&doc, transaction.changes());
let edits = generate_edits(doc.slice(..), transaction.changes());
transaction.apply(&mut doc);
assert_eq!(doc, "fn test(a: u32) {}");
@ -2469,11 +2689,14 @@ mod test {
) {
let source = Rope::from_str(source);
let loader = Loader::new(Configuration { language: vec![] });
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
let language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(&source, Arc::new(config), Arc::new(loader));
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let root = syntax
.tree()

@ -1,10 +1,12 @@
use ropey::RopeSlice;
use smallvec::SmallVec;
use crate::{Range, Rope, Selection, Tendril};
use std::borrow::Cow;
use std::{borrow::Cow, iter::once};
/// (from, to, replacement)
pub type Change = (usize, usize, Option<Tendril>);
pub type Deletion = (usize, usize);
// TODO: pub(crate)
#[derive(Debug, Clone, PartialEq, Eq)]
@ -41,7 +43,7 @@ impl ChangeSet {
}
#[must_use]
pub fn new(doc: &Rope) -> Self {
pub fn new(doc: RopeSlice) -> Self {
let len = doc.len_chars();
Self {
changes: Vec::new(),
@ -325,20 +327,75 @@ impl ChangeSet {
self.changes.is_empty() || self.changes == [Operation::Retain(self.len)]
}
/// Map a position through the changes.
/// Map a (mostly) *sorted* list of positions through the changes.
///
/// `assoc` indicates which size to associate the position with. `Before` will keep the
/// position close to the character before, and will place it before insertions over that
/// range, or at that point. `After` will move it forward, placing it at the end of such
/// insertions.
pub fn map_pos(&self, pos: usize, assoc: Assoc) -> usize {
/// This is equivalent to updating each position with `map_pos`:
///
/// ``` no-compile
/// for (pos, assoc) in positions {
/// *pos = changes.map_pos(*pos, assoc);
/// }
/// ```
/// However this function is significantly faster for sorted lists running
/// in `O(N+M)` instead of `O(NM)`. This function also handles unsorted/
/// partially sorted lists. However, in that case worst case complexity is
/// again `O(MN)`. For lists that are often/mostly sorted (like the end of diagnostic ranges)
/// performance is usally close to `O(N + M)`
pub fn update_positions<'a>(&self, positions: impl Iterator<Item = (&'a mut usize, Assoc)>) {
use Operation::*;
let mut positions = positions.peekable();
let mut old_pos = 0;
let mut new_pos = 0;
let mut iter = self.changes.iter().enumerate().peekable();
'outer: loop {
macro_rules! map {
($map: expr, $i: expr) => {
loop {
let Some((pos, assoc)) = positions.peek_mut() else { return; };
if **pos < old_pos {
// Positions are not sorted, revert to the last Operation that
// contains this position and continue iterating from there.
// We can unwrap here since `pos` can not be negative
// (unsigned integer) and iterating backwards to the start
// should always move us back to the start
for (i, change) in self.changes[..$i].iter().enumerate().rev() {
match change {
Retain(i) => {
old_pos -= i;
new_pos -= i;
}
Delete(i) => {
old_pos -= i;
}
Insert(ins) => {
new_pos -= ins.chars().count();
}
}
if old_pos <= **pos {
iter = self.changes[i..].iter().enumerate().peekable();
}
}
debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works");
continue 'outer;
}
let Some(new_pos) = $map(**pos, *assoc) else { break; };
**pos = new_pos;
positions.next();
}
};
}
let mut iter = self.changes.iter().peekable();
let Some((i, change)) = iter.next() else {
map!(
|pos, _| (old_pos == pos).then_some(new_pos),
self.changes.len()
);
break;
};
while let Some(change) = iter.next() {
let len = match change {
Delete(i) | Retain(i) => *i,
Insert(_) => 0,
@ -347,46 +404,51 @@ impl ChangeSet {
match change {
Retain(_) => {
if old_end > pos {
return new_pos + (pos - old_pos);
}
map!(
|pos, _| (old_end > pos).then_some(new_pos + (pos - old_pos)),
i
);
new_pos += len;
}
Delete(_) => {
// in range
if old_end > pos {
return new_pos;
}
map!(|pos, _| (old_end > pos).then_some(new_pos), i);
}
Insert(s) => {
let ins = s.chars().count();
// a subsequent delete means a replace, consume it
if let Some(Delete(len)) = iter.peek() {
if let Some((_, Delete(len))) = iter.peek() {
iter.next();
old_end = old_pos + len;
// in range of replaced text
if old_end > pos {
// at point or tracking before
if pos == old_pos || assoc == Assoc::Before {
return new_pos;
} else {
// place to end of insert
return new_pos + ins;
}
}
map!(
|pos, assoc| (old_end > pos).then(|| {
// at point or tracking before
if pos == old_pos || assoc == Assoc::Before {
new_pos
} else {
// place to end of insert
new_pos + ins
}
}),
i
);
} else {
// at insert point
if old_pos == pos {
// return position before inserted text
if assoc == Assoc::Before {
return new_pos;
} else {
// after text
return new_pos + ins;
}
}
map!(
|pos, assoc| (old_pos == pos).then(|| {
// return position before inserted text
if assoc == Assoc::Before {
new_pos
} else {
// after text
new_pos + ins
}
}),
i
);
}
new_pos += ins;
@ -394,14 +456,20 @@ impl ChangeSet {
}
old_pos = old_end;
}
let out_of_bounds: Vec<_> = positions.collect();
if pos > old_pos {
panic!(
"Position {} is out of range for changeset len {}!",
pos, old_pos
)
}
new_pos
panic!("Positions {out_of_bounds:?} are out of range for changeset len {old_pos}!",)
}
/// Map a position through the changes.
///
/// `assoc` indicates which side to associate the position with. `Before` will keep the
/// position close to the character before, and will place it before insertions over that
/// range, or at that point. `After` will move it forward, placing it at the end of such
/// insertions.
pub fn map_pos(&self, mut pos: usize, assoc: Assoc) -> usize {
self.update_positions(once((&mut pos, assoc)));
pos
}
pub fn changes_iter(&self) -> ChangeIterator {
@ -421,7 +489,7 @@ impl Transaction {
/// Create a new, empty transaction.
pub fn new(doc: &Rope) -> Self {
Self {
changes: ChangeSet::new(doc),
changes: ChangeSet::new(doc.slice(..)),
selection: None,
}
}
@ -534,6 +602,46 @@ impl Transaction {
Self::from(changeset)
}
/// Generate a transaction from a set of potentially overlapping deletions
/// by merging overlapping deletions together.
pub fn delete<I>(doc: &Rope, deletions: I) -> Self
where
I: Iterator<Item = Deletion>,
{
let len = doc.len_chars();
let (lower, upper) = deletions.size_hint();
let size = upper.unwrap_or(lower);
let mut changeset = ChangeSet::with_capacity(2 * size + 1); // rough estimate
let mut last = 0;
for (mut from, to) in deletions {
if last > to {
continue;
}
if last > from {
from = last
}
debug_assert!(
from <= to,
"Edit end must end before it starts (should {from} <= {to})"
);
// Retain from last "to" to current "from"
changeset.retain(from - last);
changeset.delete(to - from);
last = to;
}
changeset.retain(len - last);
Self::from(changeset)
}
pub fn insert_at_eof(mut self, text: Tendril) -> Transaction {
self.changes.insert(text);
self
}
/// Generate a transaction with a change per selection range.
pub fn change_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
where
@ -580,6 +688,16 @@ impl Transaction {
)
}
/// Generate a transaction with a deletion per selection range.
/// Compared to using `change_by_selection` directly these ranges may overlap.
/// In that case they are merged
pub fn delete_by_selection<F>(doc: &Rope, selection: &Selection, f: F) -> Self
where
F: FnMut(&Range) -> Deletion,
{
Self::delete(doc, selection.iter().map(f))
}
/// Insert text at each selection head.
pub fn insert(doc: &Rope, selection: &Selection, text: Tendril) -> Self {
Self::change_by_selection(doc, selection, |range| {
@ -752,6 +870,20 @@ mod test {
};
assert_eq!(cs.map_pos(2, Assoc::Before), 2);
assert_eq!(cs.map_pos(2, Assoc::After), 2);
// unsorted selection
let cs = ChangeSet {
changes: vec![
Insert("ab".into()),
Delete(2),
Insert("cd".into()),
Delete(2),
],
len: 4,
len_after: 4,
};
let mut positions = [4, 2];
cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After)));
assert_eq!(positions, [4, 2]);
}
#[test]
@ -818,9 +950,9 @@ mod test {
#[test]
fn combine_with_empty() {
let empty = Rope::from("");
let a = ChangeSet::new(&empty);
let a = ChangeSet::new(empty.slice(..));
let mut b = ChangeSet::new(&empty);
let mut b = ChangeSet::new(empty.slice(..));
b.insert("a".into());
let changes = a.compose(b);
@ -834,9 +966,9 @@ mod test {
const TEST_CASE: &str = "Hello, これはヘリックスエディターです!";
let empty = Rope::from("");
let a = ChangeSet::new(&empty);
let a = ChangeSet::new(empty.slice(..));
let mut b = ChangeSet::new(&empty);
let mut b = ChangeSet::new(empty.slice(..));
b.insert(TEST_CASE.into());
let changes = a.compose(b);

@ -0,0 +1,48 @@
std::vector<std::string>
fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
char* parm5, bool parm6);
std::vector<std::string>
fn_with_many_parameters(int parm1, long parm2, float parm3, double parm4,
char* parm5, bool parm6) {
auto lambda = []() {
return 0;
};
auto lambda_with_a_really_long_name_that_uses_a_whole_line
= [](int some_more_aligned_parameters,
std::string parm2) {
do_smth();
};
if (brace_on_same_line) {
do_smth();
} else if (brace_on_next_line)
{
do_smth();
} else if (another_condition) {
do_smth();
}
else {
do_smth();
}
if (inline_if_statement)
do_smth();
if (another_inline_if_statement)
return [](int parm1, char* parm2) {
this_is_a_really_pointless_lambda();
};
switch (var) {
case true:
return -1;
case false:
return 42;
}
}
class MyClass : public MyBaseClass {
public:
MyClass();
void public_fn();
private:
super_secret_private_fn();
}

@ -1 +0,0 @@
../../../src/indent.rs

@ -11,3 +11,16 @@ indent = { tab-width = 4, unit = " " }
[[grammar]]
name = "rust"
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
[[language]]
name = "cpp"
scope = "source.cpp"
injection-regex = "cpp"
file-types = ["cc", "hh", "c++", "cpp", "hpp", "h", "ipp", "tpp", "cxx", "hxx", "ixx", "txx", "ino", "C", "H"]
roots = []
comment-token = "//"
indent = { tab-width = 2, unit = " " }
[[grammar]]
name = "cpp"
source = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "2d2c4aee8672af4c7c8edff68e7dd4c07e88d2b1" }

@ -1,20 +1,122 @@
use helix_core::{
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
syntax::Loader,
syntax::{Configuration, Loader},
Syntax,
};
use std::path::PathBuf;
use ropey::Rope;
use std::{ops::Range, path::PathBuf, process::Command};
#[test]
fn test_treesitter_indent_rust() {
test_treesitter_indent("rust.rs", "source.rust");
standard_treesitter_test("rust.rs", "source.rust");
}
#[test]
fn test_treesitter_indent_cpp() {
standard_treesitter_test("cpp.cpp", "source.cpp");
}
#[test]
fn test_treesitter_indent_rust_2() {
test_treesitter_indent("indent.rs", "source.rust");
// TODO Use commands.rs as indentation test.
// Currently this fails because we can't align the parameters of a closure yet
// test_treesitter_indent("commands.rs", "source.rust");
fn test_treesitter_indent_rust_helix() {
// We pin a specific git revision to prevent unrelated changes from causing the indent tests to fail.
// Ideally, someone updates this once in a while and fixes any errors that occur.
let rev = "af382768cdaf89ff547dbd8f644a1bddd90e7c8f";
let files = Command::new("git")
.args([
"ls-tree",
"-r",
"--name-only",
"--full-tree",
rev,
"helix-term/src",
])
.output()
.unwrap();
let files = String::from_utf8(files.stdout).unwrap();
let ignored_files = vec![
// Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting
"helix-term/src/health.rs",
];
for file in files.split_whitespace() {
if ignored_files.contains(&file) {
continue;
}
let ignored_lines: Vec<Range<usize>> = match file {
"helix-term/src/application.rs" => vec![
// We can't handle complicated indent rules inside macros (`json!` in this case) since
// the tree-sitter grammar only parses them as `token_tree` and `identifier` nodes.
1045..1051,
],
"helix-term/src/commands.rs" => vec![
// This is broken because of the current handling of `call_expression`
// (i.e. having an indent query for it but outdenting again in specific cases).
// The indent query is needed to correctly handle multi-line arguments in function calls
// inside indented `field_expression` nodes (which occurs fairly often).
//
// Once we have the `@indent.always` capture type, it might be possible to just have an indent
// capture for the `arguments` field of a call expression. That could enable us to correctly
// handle this.
2226..2230,
],
"helix-term/src/commands/dap.rs" => vec![
// Complex `format!` macro
46..52,
],
"helix-term/src/commands/lsp.rs" => vec![
// Macro
624..627,
// Return type declaration of a closure. `cargo fmt` adds an additional space here,
// which we cannot (yet) model with our indent queries.
878..879,
// Same as in `helix-term/src/commands.rs`
1335..1343,
],
"helix-term/src/config.rs" => vec![
// Multiline string
146..152,
],
"helix-term/src/keymap.rs" => vec![
// Complex macro (see above)
456..470,
// Multiline string without indent
563..567,
],
"helix-term/src/main.rs" => vec![
// Multiline string
44..70,
],
"helix-term/src/ui/completion.rs" => vec![
// Macro
218..232,
],
"helix-term/src/ui/editor.rs" => vec![
// The chained function calls here are not indented, probably because of the comment
// in between. Since `cargo fmt` doesn't even attempt to format it, there's probably
// no point in trying to indent this correctly.
342..350,
],
"helix-term/src/ui/lsp.rs" => vec![
// Macro
56..61,
],
"helix-term/src/ui/statusline.rs" => vec![
// Same as in `helix-term/src/commands.rs`
436..442,
450..456,
],
_ => Vec::new(),
};
let git_object = rev.to_string() + ":" + file;
let content = Command::new("git")
.args(["cat-file", "blob", &git_object])
.output()
.unwrap();
let doc = Rope::from_reader(&mut content.stdout.as_slice()).unwrap();
test_treesitter_indent(file, doc, "source.rust", ignored_lines);
}
}
#[test]
@ -50,20 +152,41 @@ fn test_indent_level_for_line_with_spaces_and_tabs() {
assert_eq!(indent_level, 2)
}
fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
fn indent_tests_dir() -> PathBuf {
let mut test_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
test_dir.push("tests/data/indent");
test_dir
}
let mut test_file = test_dir.clone();
test_file.push(file_name);
let test_file = std::fs::File::open(test_file).unwrap();
fn indent_test_path(name: &str) -> PathBuf {
let mut path = indent_tests_dir();
path.push(name);
path
}
fn indent_tests_config() -> Configuration {
let mut config_path = indent_tests_dir();
config_path.push("languages.toml");
let config = std::fs::read_to_string(config_path).unwrap();
toml::from_str(&config).unwrap()
}
fn standard_treesitter_test(file_name: &str, lang_scope: &str) {
let test_path = indent_test_path(file_name);
let test_file = std::fs::File::open(test_path).unwrap();
let doc = ropey::Rope::from_reader(test_file).unwrap();
test_treesitter_indent(file_name, doc, lang_scope, Vec::new())
}
let mut config_file = test_dir;
config_file.push("languages.toml");
let config = std::fs::read_to_string(config_file).unwrap();
let config = toml::from_str(&config).unwrap();
let loader = Loader::new(config);
/// Test that all the lines in the given file are indented as expected.
/// ignored_lines is a list of (1-indexed) line ranges that are excluded from this test.
fn test_treesitter_indent(
test_name: &str,
doc: Rope,
lang_scope: &str,
ignored_lines: Vec<std::ops::Range<usize>>,
) {
let loader = Loader::new(indent_tests_config());
// set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@ -71,21 +194,25 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
let language_config = loader.language_config_for_scope(lang_scope).unwrap();
let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit);
let highlight_config = language_config.highlight_config(&[]).unwrap();
let syntax = Syntax::new(&doc, highlight_config, std::sync::Arc::new(loader));
let indent_query = language_config.indent_query().unwrap();
let text = doc.slice(..);
let syntax = Syntax::new(text, highlight_config, std::sync::Arc::new(loader)).unwrap();
let indent_query = language_config.indent_query().unwrap();
for i in 0..doc.len_lines() {
let line = text.line(i);
if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
continue;
}
if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
let tab_and_indent_width: usize = 4;
let tab_width: usize = 4;
let suggested_indent = treesitter_indent_for_pos(
indent_query,
&syntax,
&IndentStyle::Spaces(tab_and_indent_width as u8),
tab_and_indent_width,
tab_and_indent_width,
&indent_style,
tab_width,
indent_style.indent_width(tab_width),
text,
i,
text.line_to_char(i) + pos,
@ -94,7 +221,8 @@ fn test_treesitter_indent(file_name: &str, lang_scope: &str) {
.unwrap();
assert!(
line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
"Wrong indentation on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
"Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",
test_name,
i+1,
line.slice(..line.len_chars()-1),
suggested_indent,

@ -62,12 +62,10 @@ impl Client {
if command.is_empty() {
return Result::Err(Error::Other(anyhow!("Command not provided")));
}
if transport == "tcp" && port_arg.is_some() {
Self::tcp_process(command, args, port_arg.unwrap(), id).await
} else if transport == "stdio" {
Self::stdio(command, args, id)
} else {
Result::Err(Error::Other(anyhow!("Incorrect transport {}", transport)))
match (transport, port_arg) {
("tcp", Some(port_arg)) => Self::tcp_process(command, args, port_arg, id).await,
("stdio", _) => Self::stdio(command, args, id),
_ => Result::Err(Error::Other(anyhow!("Incorrect transport {}", transport))),
}
}

@ -230,38 +230,48 @@ impl Transport {
}
}
async fn recv(
async fn recv_inner(
transport: Arc<Self>,
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
client_tx: UnboundedSender<Payload>,
) {
) -> Result<()> {
let mut recv_buffer = String::new();
loop {
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
Ok(msg) => {
transport
.process_server_message(&client_tx, msg)
.await
.unwrap();
}
Err(err) => {
error!("err: <- {:?}", err);
break;
}
}
let msg = Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?;
transport.process_server_message(&client_tx, msg).await?;
}
}
async fn send(
async fn recv(
transport: Arc<Self>,
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
client_tx: UnboundedSender<Payload>,
) {
if let Err(err) = Self::recv_inner(transport, server_stdout, client_tx).await {
error!("err: <- {:?}", err);
}
}
async fn send_inner(
transport: Arc<Self>,
mut server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
mut client_rx: UnboundedReceiver<Payload>,
) {
) -> Result<()> {
while let Some(payload) = client_rx.recv().await {
transport
.send_payload_to_server(&mut server_stdin, payload)
.await
.unwrap()
.await?;
}
Ok(())
}
async fn send(
transport: Arc<Self>,
server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
client_rx: UnboundedReceiver<Payload>,
) {
if let Err(err) = Self::send_inner(transport, server_stdin, client_rx).await {
error!("err: <- {:?}", err);
}
}

@ -0,0 +1,15 @@
[package]
name = "helix-event"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
parking_lot = { version = "0.12", features = ["send_guard"] }

@ -0,0 +1,8 @@
//! `helix-event` contains systems that allow (often async) communication between
//! different editor components without strongly coupling them. Currently this
//! crate only contains some smaller facilities but the intend is to add more
//! functionality in the future ( like a generic hook system)
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
mod redraw;

@ -0,0 +1,49 @@
//! Signals that control when/if the editor redraws
use std::future::Future;
use parking_lot::{RwLock, RwLockReadGuard};
use tokio::sync::Notify;
/// A `Notify` instance that can be used to (asynchronously) request
/// the editor the render a new frame.
static REDRAW_NOTIFY: Notify = Notify::const_new();
/// A `RwLock` that prevents the next frame from being
/// drawn until an exclusive (write) lock can be acquired.
/// This allows asynchsonous tasks to acquire `non-exclusive`
/// locks (read) to prevent the next frame from being drawn
/// until a certain computation has finished.
static RENDER_LOCK: RwLock<()> = RwLock::new(());
pub type RenderLockGuard = RwLockReadGuard<'static, ()>;
/// Requests that the editor is redrawn. The redraws are debounced (currently to
/// 30FPS) so this can be called many times without causing a ton of frames to
/// be rendered.
pub fn request_redraw() {
REDRAW_NOTIFY.notify_one();
}
/// Returns a future that will yield once a redraw has been asynchronously
/// requested using [`request_redraw`].
pub fn redraw_requested() -> impl Future<Output = ()> {
REDRAW_NOTIFY.notified()
}
/// Wait until all locks acquired with [`lock_frame`] have been released.
/// This function is called before rendering and is intended to allow the frame
/// to wait for async computations that should be included in the current frame.
pub fn start_frame() {
drop(RENDER_LOCK.write());
// exhaust any leftover redraw notifications
let notify = REDRAW_NOTIFY.notified();
tokio::pin!(notify);
notify.enable();
}
/// Acquires the render lock which will prevent the next frame from being drawn
/// until the returned guard is dropped.
pub fn lock_frame() -> RenderLockGuard {
RENDER_LOCK.read()
}

@ -17,16 +17,19 @@ path = "src/main.rs"
anyhow = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.7"
etcetera = "0.4"
tree-sitter = "0.20"
once_cell = "1.17"
etcetera = "0.8"
tree-sitter.workspace = true
once_cell = "1.18"
log = "0.4"
which = "4.4"
# TODO: these two should be on !wasm32 only
# cloning/compiling tree-sitter grammars
cc = { version = "1" }
threadpool = { version = "1.0" }
tempfile = "3.8.0"
dunce = "1.0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
libloading = "0.7"
libloading = "0.8"

@ -1,4 +1,5 @@
use std::borrow::Cow;
use std::path::Path;
use std::process::Command;
const VERSION: &str = include_str!("../VERSION");
@ -11,7 +12,7 @@ fn main() {
.filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok());
let version: Cow<_> = match git_hash {
let version: Cow<_> = match &git_hash {
Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
None => VERSION.into(),
};
@ -23,4 +24,44 @@ fn main() {
println!("cargo:rerun-if-changed=../VERSION");
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
if git_hash.is_none() {
return;
}
// we need to revparse because the git dir could be anywhere if you are
// using detached worktrees but there is no good way to obtain an OsString
// from command output so for now we can't accept non-utf8 paths here
// probably rare enouch where it doesn't matter tough we could use gitoxide
// here but that would be make it a hard dependency and slow compile times
let Some(git_dir): Option<String> = Command::new("git")
.args(["rev-parse", "--git-dir"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok())
else {
return;
};
// If heads starts pointing at something else (different branch)
// we need to return
let head = Path::new(&git_dir).join("HEAD");
if head.exists() {
println!("cargo:rerun-if-changed={}", head.display());
}
// if the thing head points to (branch) itself changes
// we need to return
let Some(head_ref): Option<String> = Command::new("git")
.args(["symbolic-ref", "HEAD"])
.output()
.ok()
.filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok())
else {
return;
};
let head_ref = Path::new(&git_dir).join(head_ref);
if head_ref.exists() {
println!("cargo:rerun-if-changed={}", head_ref.display());
}
}

@ -1,4 +1,4 @@
use anyhow::{anyhow, Context, Result};
use anyhow::{anyhow, bail, Context, Result};
use serde::{Deserialize, Serialize};
use std::fs;
use std::time::SystemTime;
@ -8,6 +8,7 @@ use std::{
process::Command,
sync::mpsc::channel,
};
use tempfile::TempPath;
use tree_sitter::Language;
#[cfg(unix)]
@ -84,7 +85,16 @@ pub fn get_language(name: &str) -> Result<Language> {
Ok(language)
}
fn ensure_git_is_available() -> Result<()> {
match which::which("git") {
Ok(_cmd) => Ok(()),
Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")),
}
}
pub fn fetch_grammars() -> Result<()> {
ensure_git_is_available()?;
// We do not need to fetch local grammars.
let mut grammars = get_grammar_configs()?;
grammars.retain(|grammar| !matches!(grammar.source, GrammarSource::Local { .. }));
@ -97,15 +107,12 @@ pub fn fetch_grammars() -> Result<()> {
let mut git_up_to_date = 0;
let mut non_git = Vec::new();
for res in results {
for (grammar_id, res) in results {
match res {
Ok(FetchStatus::GitUpToDate) => git_up_to_date += 1,
Ok(FetchStatus::GitUpdated {
grammar_id,
revision,
}) => git_updated.push((grammar_id, revision)),
Ok(FetchStatus::NonGit { grammar_id }) => non_git.push(grammar_id),
Err(e) => errors.push(e),
Ok(FetchStatus::GitUpdated { revision }) => git_updated.push((grammar_id, revision)),
Ok(FetchStatus::NonGit) => non_git.push(grammar_id),
Err(e) => errors.push((grammar_id, e)),
}
}
@ -137,16 +144,18 @@ pub fn fetch_grammars() -> Result<()> {
if !errors.is_empty() {
let len = errors.len();
println!("{} grammars failed to fetch", len);
for (i, error) in errors.into_iter().enumerate() {
println!("\tFailure {}/{}: {}", i + 1, len, error);
for (i, (grammar, error)) in errors.into_iter().enumerate() {
println!("Failure {}/{len}: {grammar} {error}", i + 1);
}
bail!("{len} grammars failed to fetch");
}
Ok(())
}
pub fn build_grammars(target: Option<String>) -> Result<()> {
ensure_git_is_available()?;
let grammars = get_grammar_configs()?;
println!("Building {} grammars", grammars.len());
let results = run_parallel(grammars, move |grammar| {
@ -157,11 +166,11 @@ pub fn build_grammars(target: Option<String>) -> Result<()> {
let mut already_built = 0;
let mut built = Vec::new();
for res in results {
for (grammar_id, res) in results {
match res {
Ok(BuildStatus::AlreadyBuilt) => already_built += 1,
Ok(BuildStatus::Built { grammar_id }) => built.push(grammar_id),
Err(e) => errors.push(e),
Ok(BuildStatus::Built) => built.push(grammar_id),
Err(e) => errors.push((grammar_id, e)),
}
}
@ -178,10 +187,10 @@ pub fn build_grammars(target: Option<String>) -> Result<()> {
if !errors.is_empty() {
let len = errors.len();
println!("{} grammars failed to build", len);
for (i, error) in errors.into_iter().enumerate() {
println!("\tFailure {}/{}: {}", i, len, error);
for (i, (grammar_id, error)) in errors.into_iter().enumerate() {
println!("Failure {}/{len}: {grammar_id} {error}", i + 1);
}
bail!("{len} grammars failed to build");
}
Ok(())
@ -213,7 +222,7 @@ fn get_grammar_configs() -> Result<Vec<GrammarConfiguration>> {
Ok(grammars)
}
fn run_parallel<F, Res>(grammars: Vec<GrammarConfiguration>, job: F) -> Vec<Result<Res>>
fn run_parallel<F, Res>(grammars: Vec<GrammarConfiguration>, job: F) -> Vec<(String, Result<Res>)>
where
F: Fn(GrammarConfiguration) -> Result<Res> + Send + 'static + Clone,
Res: Send + 'static,
@ -228,7 +237,7 @@ where
pool.execute(move || {
// Ignore any SendErrors, if any job in another thread has encountered an
// error the Receiver will be closed causing this send to fail.
let _ = tx.send(job(grammar));
let _ = tx.send((grammar.grammar_id.clone(), job(grammar)));
});
}
@ -239,13 +248,8 @@ where
enum FetchStatus {
GitUpToDate,
GitUpdated {
grammar_id: String,
revision: String,
},
NonGit {
grammar_id: String,
},
GitUpdated { revision: String },
NonGit,
}
fn fetch_grammar(grammar: GrammarConfiguration) -> Result<FetchStatus> {
@ -286,17 +290,12 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<FetchStatus> {
)?;
git(&grammar_dir, ["checkout", &revision])?;
Ok(FetchStatus::GitUpdated {
grammar_id: grammar.grammar_id,
revision,
})
Ok(FetchStatus::GitUpdated { revision })
} else {
Ok(FetchStatus::GitUpToDate)
}
} else {
Ok(FetchStatus::NonGit {
grammar_id: grammar.grammar_id,
})
Ok(FetchStatus::NonGit)
}
}
@ -346,7 +345,7 @@ where
enum BuildStatus {
AlreadyBuilt,
Built { grammar_id: String },
Built,
}
fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result<BuildStatus> {
@ -413,6 +412,18 @@ fn build_tree_sitter_library(
let mut library_path = parser_lib_path.join(&grammar.grammar_id);
library_path.set_extension(DYLIB_EXTENSION);
// if we are running inside a buildscript emit cargo metadata
// to detect if we are running from a buildscript check some env variables
// that cargo only sets for build scripts
if std::env::var("OUT_DIR").is_ok() && std::env::var("CARGO").is_ok() {
if let Some(scanner_path) = scanner_path.as_ref().and_then(|path| path.to_str()) {
println!("cargo:rerun-if-changed={scanner_path}");
}
if let Some(parser_path) = parser_path.to_str() {
println!("cargo:rerun-if-changed={parser_path}");
}
}
let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
.context("Failed to compare source and binary timestamps")?;
@ -433,16 +444,53 @@ fn build_tree_sitter_library(
for (key, value) in compiler.env() {
command.env(key, value);
}
command.args(compiler.args());
// used to delay dropping the temporary object file until after the compilation is complete
let _path_guard;
if cfg!(all(windows, target_env = "msvc")) {
if compiler.is_like_msvc() {
command
.args(["/nologo", "/LD", "/I"])
.arg(header_path)
.arg("/Od")
.arg("/utf-8");
.arg("/utf-8")
.arg("/std:c11");
if let Some(scanner_path) = scanner_path.as_ref() {
command.arg(scanner_path);
if scanner_path.extension() == Some("c".as_ref()) {
command.arg(scanner_path);
} else {
let mut cpp_command = Command::new(compiler.path());
cpp_command.current_dir(src_path);
for (key, value) in compiler.env() {
cpp_command.env(key, value);
}
cpp_command.args(compiler.args());
let object_file =
library_path.with_file_name(format!("{}_scanner.obj", &grammar.grammar_id));
cpp_command
.args(["/nologo", "/LD", "/I"])
.arg(header_path)
.arg("/Od")
.arg("/utf-8")
.arg("/std:c++14")
.arg(format!("/Fo{}", object_file.display()))
.arg("/c")
.arg(scanner_path);
let output = cpp_command
.output()
.context("Failed to execute C++ compiler")?;
if !output.status.success() {
return Err(anyhow!(
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
));
}
command.arg(&object_file);
_path_guard = TempPath::from_path(object_file);
}
}
command
@ -454,20 +502,49 @@ fn build_tree_sitter_library(
.arg("-shared")
.arg("-fPIC")
.arg("-fno-exceptions")
.arg("-g")
.arg("-I")
.arg(header_path)
.arg("-o")
.arg(&library_path)
.arg("-O3");
.arg(&library_path);
if let Some(scanner_path) = scanner_path.as_ref() {
if scanner_path.extension() == Some("c".as_ref()) {
command.arg("-xc").arg("-std=c99").arg(scanner_path);
command.arg("-xc").arg("-std=c11").arg(scanner_path);
} else {
command.arg(scanner_path);
let mut cpp_command = Command::new(compiler.path());
cpp_command.current_dir(src_path);
for (key, value) in compiler.env() {
cpp_command.env(key, value);
}
cpp_command.args(compiler.args());
let object_file =
library_path.with_file_name(format!("{}_scanner.o", &grammar.grammar_id));
cpp_command
.arg("-fPIC")
.arg("-fno-exceptions")
.arg("-I")
.arg(header_path)
.arg("-o")
.arg(&object_file)
.arg("-std=c++14")
.arg("-c")
.arg(scanner_path);
let output = cpp_command
.output()
.context("Failed to execute C++ compiler")?;
if !output.status.success() {
return Err(anyhow!(
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
));
}
command.arg(&object_file);
_path_guard = TempPath::from_path(object_file);
}
}
command.arg("-xc").arg(parser_path);
command.arg("-xc").arg("-std=c11").arg(parser_path);
if cfg!(all(
unix,
not(any(target_os = "macos", target_os = "illumos"))
@ -487,9 +564,7 @@ fn build_tree_sitter_library(
));
}
Ok(BuildStatus::Built {
grammar_id: grammar.grammar_id,
})
Ok(BuildStatus::Built)
}
fn needs_recompile(

@ -3,29 +3,56 @@ pub mod grammar;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::{Path, PathBuf};
use std::sync::RwLock;
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>> =
once_cell::sync::Lazy::new(prioritize_runtime_dirs);
static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
pub fn initialize_config_file(specified_file: Option<PathBuf>) {
let config_file = specified_file.unwrap_or_else(|| {
let config_dir = config_dir();
static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).ok();
}
// 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
}
config_dir.join("config.toml")
});
pub fn set_current_working_dir(path: PathBuf) -> std::io::Result<()> {
let path = dunce::canonicalize(path)?;
std::env::set_current_dir(path.clone())?;
let mut cwd = CWD.write().unwrap();
*cwd = Some(path);
Ok(())
}
// We should only initialize this value once.
pub fn initialize_config_file(specified_file: Option<PathBuf>) {
let config_file = specified_file.unwrap_or_else(default_config_file);
ensure_parent_dir(&config_file);
CONFIG_FILE.set(config_file).ok();
}
pub fn initialize_log_file(specified_file: Option<PathBuf>) {
let log_file = specified_file.unwrap_or_else(default_log_file);
ensure_parent_dir(&log_file);
LOG_FILE.set(log_file).ok();
}
/// A list of runtime directories from highest to lowest priority
///
/// The priority is:
@ -122,10 +149,11 @@ pub fn cache_dir() -> PathBuf {
}
pub fn config_file() -> PathBuf {
CONFIG_FILE
.get()
.map(|path| path.to_path_buf())
.unwrap_or_else(|| config_dir().join("config.toml"))
CONFIG_FILE.get().map(|path| path.to_path_buf()).unwrap()
}
pub fn log_file() -> PathBuf {
LOG_FILE.get().map(|path| path.to_path_buf()).unwrap()
}
pub fn workspace_config_file() -> PathBuf {
@ -136,7 +164,7 @@ pub fn lang_config_file() -> PathBuf {
config_dir().join("languages.toml")
}
pub fn log_file() -> PathBuf {
pub fn default_log_file() -> PathBuf {
cache_dir().join("helix.log")
}
@ -209,13 +237,55 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
}
}
/// Finds the current workspace folder.
/// 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
/// and returns the first directory that contains either `.git` or `.helix`.
/// If no workspace was found returns (CWD, true).
/// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = current_working_dir();
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return (ancestor.to_owned(), false);
}
}
(current_dir, true)
}
fn default_config_file() -> PathBuf {
config_dir().join("config.toml")
}
fn ensure_parent_dir(path: &Path) {
if let Some(parent) = path.parent() {
if !parent.exists() {
std::fs::create_dir_all(parent).ok();
}
}
}
#[cfg(test)]
mod merge_toml_tests {
use std::str;
use super::merge_toml_values;
use super::{current_working_dir, merge_toml_values, set_current_working_dir};
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.clone()).expect("Couldn't set new path");
let cwd = current_working_dir();
assert_eq!(cwd, new_path);
}
#[test]
fn language_toml_map_merges() {
const USER: &str = r#"
@ -281,21 +351,3 @@ mod merge_toml_tests {
)
}
}
/// Finds the current workspace folder.
/// 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
/// and returns the first directory that contains either `.git` or `.helix`.
/// If no workspace was found returns (CWD, true).
/// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
for ancestor in current_dir.ancestors() {
if ancestor.join(".git").exists() || ancestor.join(".helix").exists() {
return (ancestor.to_owned(), false);
}
}
(current_dir, true)
}

@ -19,12 +19,13 @@ helix-parsec = { version = "0.6", path = "../helix-parsec" }
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
globset = "0.4.13"
log = "0.4"
lsp-types = { version = "0.94" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.27", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.12"
tokio = { version = "1.32", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.14"
which = "4.4"
parking_lot = "0.12.1"

@ -4,11 +4,12 @@ use crate::{
Call, Error, OffsetEncoding, Result,
};
use helix_core::{find_workspace, path, ChangeSet, Rope};
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
use lsp::{
notification::DidChangeWorkspaceFolders, DidChangeWorkspaceFoldersParams, OneOf,
PositionEncodingKind, WorkspaceFolder, WorkspaceFoldersChangeEvent,
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
WorkspaceFoldersChangeEvent,
};
use lsp_types as lsp;
use parking_lot::Mutex;
@ -44,6 +45,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
#[derive(Debug)]
pub struct Client {
id: usize,
name: String,
_process: Child,
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
@ -166,8 +168,7 @@ impl Client {
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
}
#[allow(clippy::type_complexity)]
#[allow(clippy::too_many_arguments)]
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
pub fn start(
cmd: &str,
args: &[String],
@ -176,6 +177,7 @@ impl Client {
root_markers: &[String],
manual_roots: &[PathBuf],
id: usize,
name: String,
req_timeout: u64,
doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
@ -200,7 +202,7 @@ impl Client {
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id);
Transport::start(reader, writer, stderr, id, name.clone());
let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::get_normalized_path(&workspace);
let root = find_lsp_workspace(
@ -225,6 +227,7 @@ impl Client {
let client = Self {
id,
name,
_process: process,
server_tx,
request_counter: AtomicU64::new(0),
@ -240,6 +243,10 @@ impl Client {
Ok((client, server_rx, initialize_notify))
}
pub fn name(&self) -> &str {
&self.name
}
pub fn id(&self) -> usize {
self.id
}
@ -270,6 +277,87 @@ impl Client {
.expect("language server not yet initialized!")
}
/// Client has to be initialized otherwise this function panics
#[inline]
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
let capabilities = self.capabilities();
use lsp::*;
match feature {
LanguageServerFeature::Format => matches!(
capabilities.document_formatting_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoDeclaration => matches!(
capabilities.declaration_provider,
Some(
DeclarationCapability::Simple(true)
| DeclarationCapability::RegistrationOptions(_)
| DeclarationCapability::Options(_),
)
),
LanguageServerFeature::GotoDefinition => matches!(
capabilities.definition_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoTypeDefinition => matches!(
capabilities.type_definition_provider,
Some(
TypeDefinitionProviderCapability::Simple(true)
| TypeDefinitionProviderCapability::Options(_),
)
),
LanguageServerFeature::GotoReference => matches!(
capabilities.references_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::GotoImplementation => matches!(
capabilities.implementation_provider,
Some(
ImplementationProviderCapability::Simple(true)
| ImplementationProviderCapability::Options(_),
)
),
LanguageServerFeature::SignatureHelp => capabilities.signature_help_provider.is_some(),
LanguageServerFeature::Hover => matches!(
capabilities.hover_provider,
Some(HoverProviderCapability::Simple(true) | HoverProviderCapability::Options(_),)
),
LanguageServerFeature::DocumentHighlight => matches!(
capabilities.document_highlight_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::Completion => capabilities.completion_provider.is_some(),
LanguageServerFeature::CodeAction => matches!(
capabilities.code_action_provider,
Some(
CodeActionProviderCapability::Simple(true)
| CodeActionProviderCapability::Options(_),
)
),
LanguageServerFeature::WorkspaceCommand => {
capabilities.execute_command_provider.is_some()
}
LanguageServerFeature::DocumentSymbols => matches!(
capabilities.document_symbol_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::WorkspaceSymbols => matches!(
capabilities.workspace_symbol_provider,
Some(OneOf::Left(true) | OneOf::Right(_))
),
LanguageServerFeature::Diagnostics => true, // there's no extra server capability
LanguageServerFeature::RenameSymbol => matches!(
capabilities.rename_provider,
Some(OneOf::Left(true)) | Some(OneOf::Right(_))
),
LanguageServerFeature::InlayHints => matches!(
capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
),
}
}
pub fn offset_encoding(&self) -> OffsetEncoding {
self.capabilities()
.position_encoding
@ -456,6 +544,10 @@ impl Client {
normalizes_line_endings: Some(false),
change_annotation_support: None,
}),
did_change_watched_files: Some(lsp::DidChangeWatchedFilesClientCapabilities {
dynamic_registration: Some(true),
relative_pattern_support: Some(false),
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@ -522,6 +614,12 @@ impl Client {
.collect(),
},
}),
is_preferred_support: Some(true),
disabled_support: Some(true),
data_support: Some(true),
resolve_support: Some(CodeActionCapabilityResolveSupport {
properties: vec!["edit".to_owned(), "command".to_owned()],
}),
..Default::default()
}),
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
@ -645,7 +743,11 @@ impl Client {
// Calculation is therefore a bunch trickier.
use helix_core::RopeSlice;
fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position {
fn traverse(
pos: lsp::Position,
text: RopeSlice,
offset_encoding: OffsetEncoding,
) -> lsp::Position {
let lsp::Position {
mut line,
mut character,
@ -662,7 +764,11 @@ impl Client {
line += 1;
character = 0;
} else {
character += ch.len_utf16() as u32;
character += match offset_encoding {
OffsetEncoding::Utf8 => ch.len_utf8() as u32,
OffsetEncoding::Utf16 => ch.len_utf16() as u32,
OffsetEncoding::Utf32 => 1,
};
}
}
lsp::Position { line, character }
@ -683,7 +789,7 @@ impl Client {
}
Delete(_) => {
let start = pos_to_lsp_pos(new_text, new_pos, offset_encoding);
let end = traverse(start, old_text.slice(old_pos..old_end));
let end = traverse(start, old_text.slice(old_pos..old_end), offset_encoding);
// deletion
changes.push(lsp::TextDocumentContentChangeEvent {
@ -700,7 +806,8 @@ impl Client {
// a subsequent delete means a replace, consume it
let end = if let Some(Delete(len)) = iter.peek() {
old_end = old_pos + len;
let end = traverse(start, old_text.slice(old_pos..old_end));
let end =
traverse(start, old_text.slice(old_pos..old_end), offset_encoding);
iter.next();
@ -858,6 +965,24 @@ impl Client {
Some(self.call::<lsp::request::ResolveCompletionItem>(completion_item))
}
pub fn resolve_code_action(
&self,
code_action: lsp::CodeAction,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support resolving code action.
match capabilities.completion_provider {
Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..
}) => (),
_ => return None,
}
Some(self.call::<lsp::request::CodeActionResolveRequest>(code_action))
}
pub fn text_document_signature_help(
&self,
text_document: lsp::TextDocumentIdentifier,
@ -1167,6 +1292,7 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
include_declaration: bool,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
@ -1183,7 +1309,7 @@ impl Client {
position,
},
context: lsp::ReferenceContext {
include_declaration: true,
include_declaration,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
@ -1285,21 +1411,13 @@ impl Client {
Some(self.call::<lsp::request::CodeActionRequest>(params))
}
pub fn supports_rename(&self) -> bool {
let capabilities = self.capabilities.get().unwrap();
matches!(
capabilities.rename_provider,
Some(lsp::OneOf::Left(true) | lsp::OneOf::Right(_))
)
}
pub fn rename_symbol(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
new_name: String,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
if !self.supports_rename() {
if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
return None;
}
@ -1339,4 +1457,13 @@ impl Client {
Some(self.call::<lsp::request::ExecuteCommand>(params))
}
pub fn did_change_watched_files(
&self,
changes: Vec<lsp::FileEvent>,
) -> impl Future<Output = std::result::Result<(), Error>> {
self.notify::<lsp::notification::DidChangeWatchedFiles>(lsp::DidChangeWatchedFilesParams {
changes,
})
}
}

@ -0,0 +1,193 @@
use std::{collections::HashMap, path::PathBuf, sync::Weak};
use globset::{GlobBuilder, GlobSetBuilder};
use tokio::sync::mpsc;
use crate::{lsp, Client};
enum Event {
FileChanged {
path: PathBuf,
},
Register {
client_id: usize,
client: Weak<Client>,
registration_id: String,
options: lsp::DidChangeWatchedFilesRegistrationOptions,
},
Unregister {
client_id: usize,
registration_id: String,
},
RemoveClient {
client_id: usize,
},
}
#[derive(Default)]
struct ClientState {
client: Weak<Client>,
registered: HashMap<String, globset::GlobSet>,
}
/// The Handler uses a dedicated tokio task to respond to file change events by
/// forwarding changes to LSPs that have registered for notifications with a
/// matching glob.
///
/// When an LSP registers for the DidChangeWatchedFiles notification, the
/// Handler is notified by sending the registration details in addition to a
/// weak reference to the LSP client. This is done so that the Handler can have
/// access to the client without preventing the client from being dropped if it
/// is closed and the Handler isn't properly notified.
#[derive(Clone, Debug)]
pub struct Handler {
tx: mpsc::UnboundedSender<Event>,
}
impl Default for Handler {
fn default() -> Self {
Self::new()
}
}
impl Handler {
pub fn new() -> Self {
let (tx, rx) = mpsc::unbounded_channel();
tokio::spawn(Self::run(rx));
Self { tx }
}
pub fn register(
&self,
client_id: usize,
client: Weak<Client>,
registration_id: String,
options: lsp::DidChangeWatchedFilesRegistrationOptions,
) {
let _ = self.tx.send(Event::Register {
client_id,
client,
registration_id,
options,
});
}
pub fn unregister(&self, client_id: usize, registration_id: String) {
let _ = self.tx.send(Event::Unregister {
client_id,
registration_id,
});
}
pub fn file_changed(&self, path: PathBuf) {
let _ = self.tx.send(Event::FileChanged { path });
}
pub fn remove_client(&self, client_id: usize) {
let _ = self.tx.send(Event::RemoveClient { client_id });
}
async fn run(mut rx: mpsc::UnboundedReceiver<Event>) {
let mut state: HashMap<usize, ClientState> = HashMap::new();
while let Some(event) = rx.recv().await {
match event {
Event::FileChanged { path } => {
log::debug!("Received file event for {:?}", &path);
state.retain(|id, client_state| {
if !client_state
.registered
.values()
.any(|glob| glob.is_match(&path))
{
return true;
}
let Some(client) = client_state.client.upgrade() else {
log::warn!("LSP client was dropped: {id}");
return false;
};
let Ok(uri) = lsp::Url::from_file_path(&path) else {
return true;
};
log::debug!(
"Sending didChangeWatchedFiles notification to client '{}'",
client.name()
);
if let Err(err) = crate::block_on(client
.did_change_watched_files(vec![lsp::FileEvent {
uri,
// We currently always send the CHANGED state
// since we don't actually have more context at
// the moment.
typ: lsp::FileChangeType::CHANGED,
}]))
{
log::warn!("Failed to send didChangeWatchedFiles notification to client: {err}");
}
true
});
}
Event::Register {
client_id,
client,
registration_id,
options: ops,
} => {
log::debug!(
"Registering didChangeWatchedFiles for client '{}' with id '{}'",
client_id,
registration_id
);
let entry = state.entry(client_id).or_insert_with(ClientState::default);
entry.client = client;
let mut builder = GlobSetBuilder::new();
for watcher in ops.watchers {
if let lsp::GlobPattern::String(pattern) = watcher.glob_pattern {
if let Ok(glob) = GlobBuilder::new(&pattern).build() {
builder.add(glob);
}
}
}
match builder.build() {
Ok(globset) => {
entry.registered.insert(registration_id, globset);
}
Err(err) => {
// Remove any old state for that registration id and
// remove the entire client if it's now empty.
entry.registered.remove(&registration_id);
if entry.registered.is_empty() {
state.remove(&client_id);
}
log::warn!(
"Unable to build globset for LSP didChangeWatchedFiles {err}"
)
}
}
}
Event::Unregister {
client_id,
registration_id,
} => {
log::debug!(
"Unregistering didChangeWatchedFiles with id '{}' for client '{}'",
registration_id,
client_id
);
if let Some(client_state) = state.get_mut(&client_id) {
client_state.registered.remove(&registration_id);
if client_state.registered.is_empty() {
state.remove(&client_id);
}
}
}
Event::RemoveClient { client_id } => {
log::debug!("Removing LSP client: {client_id}");
state.remove(&client_id);
}
}
}
}
}

@ -1,4 +1,5 @@
mod client;
pub mod file_event;
pub mod jsonrpc;
pub mod snippet;
mod transport;
@ -12,24 +13,21 @@ pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll;
use helix_core::{
path,
syntax::{LanguageConfiguration, LanguageServerConfiguration},
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
};
use tokio::sync::mpsc::UnboundedReceiver;
use std::{
collections::{hash_map::Entry, HashMap},
collections::HashMap,
path::{Path, PathBuf},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
sync::Arc,
};
use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream;
pub type Result<T> = core::result::Result<T, Error>;
type LanguageId = String;
pub type LanguageServerName = String;
#[derive(Error, Debug)]
pub enum Error {
@ -49,7 +47,7 @@ pub enum Error {
Other(#[from] anyhow::Error),
}
#[derive(Clone, Copy, Debug, Default)]
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum OffsetEncoding {
/// UTF-8 code units aka bytes
Utf8,
@ -380,7 +378,7 @@ pub mod util {
.expect("transaction must be valid for primary selection");
let removed_text = text.slice(removed_start..removed_end);
let (transaction, selection) = Transaction::change_by_selection_ignore_overlapping(
let (transaction, mut selection) = Transaction::change_by_selection_ignore_overlapping(
doc,
selection,
|range| {
@ -423,6 +421,11 @@ pub mod util {
return transaction;
}
// Don't normalize to avoid merging/reording selections which would
// break the association between tabstops and selections. Most ranges
// will be replaced by tabstops anyways and the final selection will be
// normalized anyways
selection = selection.map_no_normalize(changes);
let mut mapped_selection = SmallVec::with_capacity(selection.len());
let mut mapped_primary_idx = 0;
let primary_range = selection.primary();
@ -431,9 +434,8 @@ pub mod util {
mapped_primary_idx = mapped_selection.len()
}
let range = range.map(changes);
let tabstops = tabstops.first().filter(|tabstops| !tabstops.is_empty());
let Some(tabstops) = tabstops else{
let Some(tabstops) = tabstops else {
// no tabstop normal mapping
mapped_selection.push(range);
continue;
@ -546,6 +548,7 @@ pub enum MethodCall {
WorkspaceFolders,
WorkspaceConfiguration(lsp::ConfigurationParams),
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
}
impl MethodCall {
@ -569,6 +572,10 @@ impl MethodCall {
let params: lsp::RegistrationParams = params.parse()?;
Self::RegisterCapability(params)
}
lsp::request::UnregisterCapability::METHOD => {
let params: lsp::UnregistrationParams = params.parse()?;
Self::UnregisterCapability(params)
}
_ => {
return Err(Error::Unhandled);
}
@ -624,24 +631,21 @@ impl Notification {
#[derive(Debug)]
pub struct Registry {
inner: HashMap<LanguageId, Vec<(usize, Arc<Client>)>>,
counter: AtomicUsize,
inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
syn_loader: Arc<helix_core::syntax::Loader>,
counter: usize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
pub file_event_handler: file_event::Handler,
}
impl Registry {
pub fn new() -> Self {
pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
Self {
inner: HashMap::new(),
counter: AtomicUsize::new(0),
syn_loader,
counter: 0,
incoming: SelectAll::new(),
file_event_handler: file_event::Handler::new(),
}
}
@ -649,65 +653,95 @@ impl Registry {
self.inner
.values()
.flatten()
.find(|(client_id, _)| client_id == &id)
.map(|(_, client)| client.as_ref())
.find(|client| client.id() == id)
.map(|client| &**client)
}
pub fn remove_by_id(&mut self, id: usize) {
self.inner.retain(|_, clients| {
clients.retain(|&(client_id, _)| client_id != id);
!clients.is_empty()
})
self.file_event_handler.remove_client(id);
self.inner.retain(|_, language_servers| {
language_servers.retain(|ls| id != ls.id());
!language_servers.is_empty()
});
}
fn start_client(
&mut self,
name: String,
ls_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Arc<Client>> {
let config = self
.syn_loader
.language_server_configs()
.get(&name)
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
let id = self.counter;
self.counter += 1;
let NewClient(client, incoming) = start_client(
id,
name,
ls_config,
config,
doc_path,
root_dirs,
enable_snippets,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(client)
}
/// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
/// as it could be that language servers of these documents were stopped by this method.
/// See helix_view::editor::Editor::refresh_language_servers
pub fn restart(
&mut self,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
None => return Ok(None),
};
let scope = language_config.scope.clone();
match self.inner.entry(scope) {
Entry::Vacant(_) => Ok(None),
Entry::Occupied(mut entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) = start_client(
id,
language_config,
config,
doc_path,
root_dirs,
enable_snippets,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
let old_clients = entry.insert(vec![(id, client.clone())]);
for (_, old_client) in old_clients {
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
) -> Result<Vec<Arc<Client>>> {
language_config
.language_servers
.iter()
.filter_map(|LanguageServerFeatures { name, .. }| {
if self.inner.contains_key(name) {
let client = match self.start_client(
name.clone(),
language_config,
doc_path,
root_dirs,
enable_snippets,
) {
Ok(client) => client,
error => return Some(error),
};
let old_clients = self
.inner
.insert(name.clone(), vec![client.clone()])
.unwrap();
for old_client in old_clients {
self.file_event_handler.remove_client(old_client.id());
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
}
Some(Ok(client))
} else {
None
}
Ok(Some(client))
}
}
})
.collect()
}
pub fn stop(&mut self, language_config: &LanguageConfiguration) {
let scope = language_config.scope.clone();
if let Some(clients) = self.inner.remove(&scope) {
for (_, client) in clients {
pub fn stop(&mut self, name: &str) {
if let Some(clients) = self.inner.remove(name) {
for client in clients {
self.file_event_handler.remove_client(client.id());
tokio::spawn(async move {
let _ = client.force_shutdown().await;
});
@ -721,37 +755,34 @@ impl Registry {
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Option<Arc<Client>>> {
let config = match &language_config.language_server {
Some(config) => config,
None => return Ok(None),
};
let clients = self.inner.entry(language_config.scope.clone()).or_default();
// check if we already have a client for this documents root that we can reuse
if let Some((_, client)) = clients.iter_mut().enumerate().find(|(i, (_, client))| {
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
}) {
return Ok(Some(client.1.clone()));
}
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let NewClientResult(client, incoming) = start_client(
id,
language_config,
config,
doc_path,
root_dirs,
enable_snippets,
)?;
clients.push((id, client.clone()));
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(Some(client))
) -> Result<HashMap<LanguageServerName, Arc<Client>>> {
language_config
.language_servers
.iter()
.map(|LanguageServerFeatures { name, .. }| {
if let Some(clients) = self.inner.get(name) {
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
}) {
return Ok((name.to_owned(), client.clone()));
}
}
let client = self.start_client(
name.clone(),
language_config,
doc_path,
root_dirs,
enable_snippets,
)?;
let clients = self.inner.entry(name.clone()).or_default();
clients.push(client.clone());
Ok((name.clone(), client))
})
.collect()
}
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().flatten().map(|(_, client)| client)
self.inner.values().flatten()
}
}
@ -833,26 +864,28 @@ impl LspProgressMap {
}
}
struct NewClientResult(Arc<Client>, UnboundedReceiver<(usize, Call)>);
struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
/// it is only called when it makes sense.
fn start_client(
id: usize,
name: String,
config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<NewClientResult> {
) -> Result<NewClient> {
let (client, incoming, initialize_notify) = Client::start(
&ls_config.command,
&ls_config.args,
config.config.clone(),
ls_config.config.clone(),
ls_config.environment.clone(),
&config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
id,
name,
ls_config.timeout,
doc_path,
)?;
@ -886,7 +919,7 @@ fn start_client(
initialize_notify.notify_one();
});
Ok(NewClientResult(client, incoming))
Ok(NewClient(client, incoming))
}
/// Find an LSP workspace of a file using the following mechanism:
@ -909,7 +942,7 @@ pub fn find_lsp_workspace(
let mut file = if file.is_absolute() {
file.to_path_buf()
} else {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let current_dir = helix_loader::current_working_dir();
current_dir.join(file)
};
file = path::get_normalized_path(&file);

@ -38,6 +38,7 @@ enum ServerMessage {
#[derive(Debug)]
pub struct Transport {
id: usize,
name: String,
pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
}
@ -47,6 +48,7 @@ impl Transport {
server_stdin: BufWriter<ChildStdin>,
server_stderr: BufReader<ChildStderr>,
id: usize,
name: String,
) -> (
UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedSender<Payload>,
@ -58,6 +60,7 @@ impl Transport {
let transport = Self {
id,
name,
pending_requests: Mutex::new(HashMap::default()),
};
@ -83,6 +86,7 @@ impl Transport {
async fn recv_server_message(
reader: &mut (impl AsyncBufRead + Unpin + Send),
buffer: &mut String,
language_server_name: &str,
) -> Result<ServerMessage> {
let mut content_length = None;
loop {
@ -124,7 +128,7 @@ impl Transport {
reader.read_exact(&mut content).await?;
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
info!("<- {}", msg);
info!("{language_server_name} <- {msg}");
// try parsing as output (server response) or call (server request)
let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
@ -135,12 +139,13 @@ impl Transport {
async fn recv_server_error(
err: &mut (impl AsyncBufRead + Unpin + Send),
buffer: &mut String,
language_server_name: &str,
) -> Result<()> {
buffer.truncate(0);
if err.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed);
};
error!("err <- {:?}", buffer);
error!("{language_server_name} err <- {buffer:?}");
Ok(())
}
@ -162,15 +167,17 @@ impl Transport {
Payload::Notification(value) => serde_json::to_string(&value)?,
Payload::Response(error) => serde_json::to_string(&error)?,
};
self.send_string_to_server(server_stdin, json).await
self.send_string_to_server(server_stdin, json, &self.name)
.await
}
async fn send_string_to_server(
&self,
server_stdin: &mut BufWriter<ChildStdin>,
request: String,
language_server_name: &str,
) -> Result<()> {
info!("-> {}", request);
info!("{language_server_name} -> {request}");
// send the headers
server_stdin
@ -189,9 +196,13 @@ impl Transport {
&self,
client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
msg: ServerMessage,
language_server_name: &str,
) -> Result<()> {
match msg {
ServerMessage::Output(output) => self.process_request_response(output).await?,
ServerMessage::Output(output) => {
self.process_request_response(output, language_server_name)
.await?
}
ServerMessage::Call(call) => {
client_tx
.send((self.id, call))
@ -202,14 +213,18 @@ impl Transport {
Ok(())
}
async fn process_request_response(&self, output: jsonrpc::Output) -> Result<()> {
async fn process_request_response(
&self,
output: jsonrpc::Output,
language_server_name: &str,
) -> Result<()> {
let (id, result) = match output {
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
info!("<- {}", result);
info!("{language_server_name} <- {}", result);
(id, Ok(result))
}
jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
error!("<- {}", error);
error!("{language_server_name} <- {error}");
(id, Err(error.into()))
}
};
@ -240,12 +255,17 @@ impl Transport {
) {
let mut recv_buffer = String::new();
loop {
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer, &transport.name)
.await
{
Ok(msg) => {
match transport.process_server_message(&client_tx, msg).await {
match transport
.process_server_message(&client_tx, msg, &transport.name)
.await
{
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
error!("{} err: <- {err:?}", transport.name);
break;
}
};
@ -270,7 +290,7 @@ impl Transport {
params: jsonrpc::Params::None,
}));
match transport
.process_server_message(&client_tx, notification)
.process_server_message(&client_tx, notification, &transport.name)
.await
{
Ok(_) => {}
@ -281,20 +301,22 @@ impl Transport {
break;
}
Err(err) => {
error!("err: <- {:?}", err);
error!("{} err: <- {err:?}", transport.name);
break;
}
}
}
}
async fn err(_transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
async fn err(transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
let mut recv_buffer = String::new();
loop {
match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await {
match Self::recv_server_error(&mut server_stderr, &mut recv_buffer, &transport.name)
.await
{
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
error!("{} err: <- {err:?}", transport.name);
break;
}
}
@ -331,6 +353,11 @@ impl Transport {
}
}
fn is_shutdown(payload: &Payload) -> bool {
use lsp_types::request::{Request, Shutdown};
matches!(payload, Payload::Request { value: jsonrpc::MethodCall { method, .. }, .. } if method == Shutdown::METHOD)
}
// TODO: events that use capabilities need to do the right thing
loop {
@ -348,10 +375,11 @@ impl Transport {
method: lsp_types::notification::Initialized::METHOD.to_string(),
params: jsonrpc::Params::None,
}));
match transport.process_server_message(&client_tx, notification).await {
let language_server_name = &transport.name;
match transport.process_server_message(&client_tx, notification, language_server_name).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
error!("{language_server_name} err: <- {err:?}");
}
}
@ -361,14 +389,17 @@ impl Transport {
match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
error!("{language_server_name} err: <- {err:?}");
}
}
}
}
msg = client_rx.recv() => {
if let Some(msg) = msg {
if is_pending && !is_initialize(&msg) {
if is_pending && is_shutdown(&msg) {
log::info!("Language server not initialized, shutting down");
break;
} else if is_pending && !is_initialize(&msg) {
// ignore notifications
if let Payload::Notification(_) = msg {
continue;
@ -380,7 +411,7 @@ impl Transport {
match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
error!("{} err: <- {err:?}", transport.name);
}
}
}

@ -24,6 +24,7 @@ path = "src/main.rs"
[dependencies]
helix-core = { version = "0.6", path = "../helix-core" }
helix-event = { version = "0.6", path = "../helix-event" }
helix-view = { version = "0.6", path = "../helix-view" }
helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" }
@ -31,13 +32,13 @@ helix-vcs = { version = "0.6", path = "../helix-vcs" }
helix-loader = { version = "0.6", path = "../helix-loader" }
anyhow = "1"
once_cell = "1.17"
once_cell = "1.18"
which = "4.4"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.26", features = ["event-stream"] }
crossterm = { version = "0.27", features = ["event-stream"] }
signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@ -49,7 +50,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] }
log = "0.4"
# File picker
fuzzy-matcher = "0.3"
nucleo.workspace = true
ignore = "0.4"
# markdown doc rendering
pulldown-cmark = { version = "0.9", default-features = false }
@ -68,12 +69,15 @@ grep-searcher = "0.1.11"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.140"
libc = "0.2.147"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
[build-dependencies]
helix-loader = { version = "0.6", path = "../helix-loader" }
[dev-dependencies]
smallvec = "1.10"
indoc = "2.0.1"
tempfile = "3.4.0"
smallvec = "1.11"
indoc = "2.0.3"
tempfile = "3.8.0"

@ -5,7 +5,11 @@ use helix_core::{
path::get_relative_path,
pos_at_coords, syntax, Selection,
};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_lsp::{
lsp::{self, notification::Notification},
util::lsp_pos_to_pos,
LspProgressMap,
};
use helix_view::{
align_view,
document::DocumentSavedEventResult,
@ -29,12 +33,9 @@ use crate::{
};
use log::{debug, error, warn};
use std::{
io::{stdin, stdout},
path::Path,
sync::Arc,
time::{Duration, Instant},
};
#[cfg(not(feature = "integration"))]
use std::io::stdout;
use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
use anyhow::{Context, Error};
@ -44,8 +45,6 @@ use {signal_hook::consts::signal, signal_hook_tokio::Signals};
#[cfg(windows)]
type Signals = futures_util::stream::Empty<()>;
const LSP_DEADLINE: Duration = Duration::from_millis(16);
#[cfg(not(feature = "integration"))]
use tui::backend::CrosstermBackend;
@ -75,7 +74,6 @@ pub struct Application {
signals: Signals,
jobs: Jobs,
lsp_progress: LspProgressMap,
last_render: Instant,
}
#[cfg(feature = "integration")]
@ -162,11 +160,11 @@ impl Application {
let path = helix_loader::runtime_file(Path::new("tutor"));
editor.open(&path, Action::VerticalSplit)?;
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
doc_mut!(editor).set_path(None);
} else if !args.files.is_empty() {
let first = &args.files[0].0; // we know it's not empty
if first.is_dir() {
std::env::set_current_dir(first).context("set current dir")?;
helix_loader::set_current_working_dir(first.clone())?;
editor.new_file(Action::VerticalSplit);
let picker = ui::file_picker(".".into(), &config.load().editor);
compositor.push(Box::new(overlaid(picker)));
@ -214,11 +212,6 @@ impl Application {
}
} else if stdin().is_tty() || cfg!(feature = "integration") {
editor.new_file(Action::VerticalSplit);
} else if cfg!(target_os = "macos") {
// On Linux and Windows, we allow the output of a command to be piped into the new buffer.
// This doesn't currently work on macOS because of the following issue:
// https://github.com/crossterm-rs/crossterm/issues/500
anyhow::bail!("Piping into helix-term is currently not supported on macOS");
} else {
editor
.new_file_from_stdin(Action::VerticalSplit)
@ -230,8 +223,14 @@ impl Application {
#[cfg(windows)]
let signals = futures_util::stream::empty();
#[cfg(not(windows))]
let signals = Signals::new([signal::SIGTSTP, signal::SIGCONT, signal::SIGUSR1])
.context("build signal handler")?;
let signals = Signals::new([
signal::SIGTSTP,
signal::SIGCONT,
signal::SIGUSR1,
signal::SIGTERM,
signal::SIGINT,
])
.context("build signal handler")?;
let app = Self {
compositor,
@ -246,29 +245,25 @@ impl Application {
signals,
jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(),
last_render: Instant::now(),
};
Ok(app)
}
async fn render(&mut self) {
if self.compositor.full_redraw {
self.terminal.clear().expect("Cannot clear the terminal");
self.compositor.full_redraw = false;
}
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
scroll: None,
};
// Acquire mutable access to the redraw_handle lock
// to ensure that there are no tasks running that want to block rendering
drop(cx.editor.redraw_handle.1.write().await);
helix_event::start_frame();
cx.editor.needs_redraw = false;
{
// exhaust any leftover redraw notifications
let notify = cx.editor.redraw_handle.0.notified();
tokio::pin!(notify);
notify.enable();
}
let area = self
.terminal
@ -290,10 +285,9 @@ impl Application {
pub async fn event_loop<S>(&mut self, input_stream: &mut S)
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{
self.render().await;
self.last_render = Instant::now();
loop {
if !self.event_loop_until_idle(input_stream).await {
@ -304,7 +298,7 @@ impl Application {
pub async fn event_loop_until_idle<S>(&mut self, input_stream: &mut S) -> bool
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{
loop {
if self.editor.should_close() {
@ -317,7 +311,9 @@ impl Application {
biased;
Some(signal) = self.signals.next() => {
self.handle_signals(signal).await;
if !self.handle_signals(signal).await {
return false;
};
}
Some(event) = input_stream.next() => {
self.handle_terminal_events(event).await;
@ -361,6 +357,9 @@ impl Application {
ConfigEvent::Update(editor_config) => {
let mut app_config = (*self.config.load().clone()).clone();
app_config.editor = *editor_config;
if let Err(err) = self.terminal.reconfigure(app_config.editor.clone().into()) {
self.editor.set_error(err.to_string());
};
self.config.store(Arc::new(app_config));
}
}
@ -419,6 +418,8 @@ impl Application {
.map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?;
self.refresh_language_config()?;
self.refresh_theme(&default_config)?;
self.terminal
.reconfigure(default_config.editor.clone().into())?;
// Store new config
self.config.store(Arc::new(default_config));
Ok(())
@ -436,10 +437,12 @@ impl Application {
#[cfg(windows)]
// no signal handling available on windows
pub async fn handle_signals(&mut self, _signal: ()) {}
pub async fn handle_signals(&mut self, _signal: ()) -> bool {
true
}
#[cfg(not(windows))]
pub async fn handle_signals(&mut self, signal: i32) {
pub async fn handle_signals(&mut self, signal: i32) -> bool {
match signal {
signal::SIGTSTP => {
self.restore_term().unwrap();
@ -471,7 +474,17 @@ impl Application {
}
}
signal::SIGCONT => {
self.claim_term().await.unwrap();
// Copy/Paste from same issue from neovim:
// https://github.com/neovim/neovim/issues/12322
// https://github.com/neovim/neovim/pull/13084
for retries in 1..=10 {
match self.claim_term().await {
Ok(()) => break,
Err(err) if retries == 10 => panic!("Failed to claim terminal: {}", err),
Err(_) => continue,
}
}
// redraw the terminal
let area = self.terminal.size().expect("couldn't get terminal size");
self.compositor.resize(area);
@ -483,8 +496,14 @@ impl Application {
self.refresh_config();
self.render().await;
}
signal::SIGTERM | signal::SIGINT => {
self.restore_term().unwrap();
return false;
}
_ => unreachable!(),
}
true
}
pub async fn handle_idle_timeout(&mut self) {
@ -532,16 +551,7 @@ impl Application {
let bytes = doc_save_event.text.len_bytes();
if doc.path() != Some(&doc_save_event.path) {
if let Err(err) = doc.set_path(Some(&doc_save_event.path)) {
log::error!(
"error setting path for doc '{:?}': {}",
doc.path(),
err.to_string(),
);
self.editor.set_error(err.to_string());
return;
}
doc.set_path(Some(&doc_save_event.path));
let loader = self.editor.syn_loader.clone();
@ -549,7 +559,7 @@ impl Application {
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let id = doc.id();
doc.detect_language(loader);
let _ = self.editor.refresh_language_server(id);
self.editor.refresh_language_servers(id);
}
// TODO: fix being overwritten by lsp
@ -577,12 +587,7 @@ impl Application {
EditorEvent::LanguageServerMessage((id, call)) => {
self.handle_language_server_message(call, id).await;
// limit render calls for fast language server messages
let last = self.editor.language_servers.incoming.is_empty();
if last || self.last_render.elapsed() > LSP_DEADLINE {
self.render().await;
self.last_render = Instant::now();
}
helix_event::request_redraw();
}
EditorEvent::DebuggerEvent(payload) => {
let needs_render = self.editor.handle_debugger_message(payload).await;
@ -590,6 +595,9 @@ impl Application {
self.render().await;
}
}
EditorEvent::Redraw => {
self.render().await;
}
EditorEvent::IdleTimer => {
self.editor.clear_idle_timer();
self.handle_idle_timeout().await;
@ -604,10 +612,7 @@ impl Application {
false
}
pub async fn handle_terminal_events(
&mut self,
event: Result<CrosstermEvent, crossterm::ErrorKind>,
) {
pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermEvent>) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
jobs: &mut self.jobs,
@ -647,6 +652,18 @@ impl Application {
) {
use helix_lsp::{Call, MethodCall, Notification};
macro_rules! language_server {
() => {
match self.editor.language_server_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
}
};
}
match call {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) {
@ -662,14 +679,7 @@ impl Application {
match notification {
Notification::Initialized => {
let language_server =
match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
let language_server = language_server!();
// Trigger a workspace/didChangeConfiguration notification after initialization.
// This might not be required by the spec but Neovim does this as well, so it's
@ -678,9 +688,10 @@ impl Application {
tokio::spawn(language_server.did_change_configuration(config.clone()));
}
let docs = self.editor.documents().filter(|doc| {
doc.language_server().map(|server| server.id()) == Some(server_id)
});
let docs = self
.editor
.documents()
.filter(|doc| doc.supports_language_server(server_id));
// trigger textDocument/didOpen for docs that are already open
for doc in docs {
@ -700,7 +711,7 @@ impl Application {
));
}
}
Notification::PublishDiagnostics(mut params) => {
Notification::PublishDiagnostics(params) => {
let path = match params.uri.to_file_path() {
Ok(path) => path,
Err(_) => {
@ -708,6 +719,12 @@ impl Application {
return;
}
};
let language_server = language_server!();
if !language_server.is_initialized() {
log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name());
return;
}
let offset_encoding = language_server.offset_encoding();
let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
if let Some(version) = params.version {
if version != doc.version() {
@ -730,18 +747,11 @@ impl Application {
use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
use lsp::DiagnosticSeverity;
let language_server = if let Some(language_server) = doc.language_server() {
language_server
} else {
log::warn!("Discarding diagnostic because language server is not initialized: {:?}", diagnostic);
return None;
};
// TODO: convert inside server
let start = if let Some(start) = lsp_pos_to_pos(
text,
diagnostic.range.start,
language_server.offset_encoding(),
offset_encoding,
) {
start
} else {
@ -749,11 +759,9 @@ impl Application {
return None;
};
let end = if let Some(end) = lsp_pos_to_pos(
text,
diagnostic.range.end,
language_server.offset_encoding(),
) {
let end = if let Some(end) =
lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
{
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
@ -792,14 +800,19 @@ impl Application {
None => None,
};
let tags = if let Some(ref tags) = diagnostic.tags {
let new_tags = tags.iter().filter_map(|tag| {
match *tag {
lsp::DiagnosticTag::DEPRECATED => Some(DiagnosticTag::Deprecated),
lsp::DiagnosticTag::UNNECESSARY => Some(DiagnosticTag::Unnecessary),
_ => None
}
}).collect();
let tags = if let Some(tags) = &diagnostic.tags {
let new_tags = tags
.iter()
.filter_map(|tag| match *tag {
lsp::DiagnosticTag::DEPRECATED => {
Some(DiagnosticTag::Deprecated)
}
lsp::DiagnosticTag::UNNECESSARY => {
Some(DiagnosticTag::Unnecessary)
}
_ => None,
})
.collect();
new_tags
} else {
@ -815,25 +828,40 @@ impl Application {
tags,
source: diagnostic.source.clone(),
data: diagnostic.data.clone(),
language_server_id: server_id,
})
})
.collect();
doc.set_diagnostics(diagnostics);
doc.replace_diagnostics(diagnostics, server_id);
}
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
params
let mut diagnostics = params
.diagnostics
.sort_unstable_by_key(|d| (d.severity, d.range.start));
.into_iter()
.map(|d| (d, server_id))
.collect();
// Insert the original lsp::Diagnostics here because we may have no open document
// for diagnosic message and so we can't calculate the exact position.
// When using them later in the diagnostics picker, we calculate them on-demand.
self.editor
.diagnostics
.insert(params.uri, params.diagnostics);
match self.editor.diagnostics.entry(params.uri) {
Entry::Occupied(o) => {
let current_diagnostics = o.into_mut();
// there may entries of other language servers, which is why we can't overwrite the whole entry
current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
current_diagnostics.append(&mut diagnostics);
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
current_diagnostics
.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
}
Entry::Vacant(v) => {
diagnostics
.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
v.insert(diagnostics);
}
};
}
Notification::ShowMessage(params) => {
log::warn!("unhandled window/showMessage: {:?}", params);
@ -930,24 +958,18 @@ impl Application {
Notification::Exit => {
self.editor.set_status("Language server exited");
// Clear any diagnostics for documents with this server open.
let urls: Vec<_> = self
.editor
.documents_mut()
.filter_map(|doc| {
if doc.language_server().map(|server| server.id())
== Some(server_id)
{
doc.set_diagnostics(Vec::new());
doc.url()
} else {
None
}
})
.collect();
// LSPs may produce diagnostics for files that haven't been opened in helix,
// we need to clear those and remove the entries from the list if this leads to
// an empty diagnostic list for said files
for diags in self.editor.diagnostics.values_mut() {
diags.retain(|(_, lsp_id)| *lsp_id != server_id);
}
self.editor.diagnostics.retain(|_, diags| !diags.is_empty());
for url in urls {
self.editor.diagnostics.remove(&url);
// Clear any diagnostics for documents with this server open.
for doc in self.editor.documents_mut() {
doc.clear_diagnostics(server_id);
}
// Remove the language server from the registry.
@ -998,48 +1020,48 @@ impl Application {
Ok(serde_json::Value::Null)
}
Ok(MethodCall::ApplyWorkspaceEdit(params)) => {
let res = apply_workspace_edit(
&mut self.editor,
helix_lsp::OffsetEncoding::Utf8,
&params.edit,
);
Ok(json!(lsp::ApplyWorkspaceEditResponse {
applied: res.is_ok(),
failure_reason: res.as_ref().err().map(|err| err.kind.to_string()),
failed_change: res
.as_ref()
.err()
.map(|err| err.failed_change_idx as u32),
}))
let language_server = language_server!();
if language_server.is_initialized() {
let offset_encoding = language_server.offset_encoding();
let res = apply_workspace_edit(
&mut self.editor,
offset_encoding,
&params.edit,
);
Ok(json!(lsp::ApplyWorkspaceEditResponse {
applied: res.is_ok(),
failure_reason: res.as_ref().err().map(|err| err.kind.to_string()),
failed_change: res
.as_ref()
.err()
.map(|err| err.failed_change_idx as u32),
}))
} else {
Err(helix_lsp::jsonrpc::Error {
code: helix_lsp::jsonrpc::ErrorCode::InvalidRequest,
message: "Server must be initialized to request workspace edits"
.to_string(),
data: None,
})
}
}
Ok(MethodCall::WorkspaceFolders) => {
let language_server =
self.editor.language_servers.get_by_id(server_id).unwrap();
Ok(json!(&*language_server.workspace_folders().await))
Ok(json!(&*language_server!().workspace_folders().await))
}
Ok(MethodCall::WorkspaceConfiguration(params)) => {
let language_server = language_server!();
let result: Vec<_> = params
.items
.iter()
.map(|item| {
let mut config = match &item.scope_uri {
Some(scope) => {
let path = scope.to_file_path().ok()?;
let doc = self.editor.document_by_path(path)?;
doc.language_config()?.config.as_ref()?
}
None => self
.editor
.language_servers
.get_by_id(server_id)
.unwrap()
.config()?,
};
let mut config = language_server.config()?;
if let Some(section) = item.section.as_ref() {
for part in section.split('.') {
config = config.get(part)?;
// for some reason some lsps send an empty string (observed in 'vscode-eslint-language-server')
if !section.is_empty() {
for part in section.split('.') {
config = config.get(part)?;
}
}
}
Some(config)
@ -1047,28 +1069,68 @@ impl Application {
.collect();
Ok(json!(result))
}
Ok(MethodCall::RegisterCapability(_params)) => {
log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
// Language Servers based on the `vscode-languageserver-node` library often send
// client/registerCapability even though we do not enable dynamic registration
// for any capabilities. We should send a MethodNotFound JSONRPC error in this
// case but that rejects the registration promise in the server which causes an
// exit. So we work around this by ignoring the request and sending back an OK
// response.
Ok(MethodCall::RegisterCapability(params)) => {
if let Some(client) = self
.editor
.language_servers
.iter_clients()
.find(|client| client.id() == server_id)
{
for reg in params.registrations {
match reg.method.as_str() {
lsp::notification::DidChangeWatchedFiles::METHOD => {
let Some(options) = reg.register_options else {
continue;
};
let ops: lsp::DidChangeWatchedFilesRegistrationOptions =
match serde_json::from_value(options) {
Ok(ops) => ops,
Err(err) => {
log::warn!("Failed to deserialize DidChangeWatchedFilesRegistrationOptions: {err}");
continue;
}
};
self.editor.language_servers.file_event_handler.register(
client.id(),
Arc::downgrade(client),
reg.id,
ops,
)
}
_ => {
// Language Servers based on the `vscode-languageserver-node` library often send
// client/registerCapability even though we do not enable dynamic registration
// for most capabilities. We should send a MethodNotFound JSONRPC error in this
// case but that rejects the registration promise in the server which causes an
// exit. So we work around this by ignoring the request and sending back an OK
// response.
log::warn!("Ignoring a client/registerCapability request because dynamic capability registration is not enabled. Please report this upstream to the language server");
}
}
}
}
Ok(serde_json::Value::Null)
}
};
let language_server = match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
Ok(MethodCall::UnregisterCapability(params)) => {
for unreg in params.unregisterations {
match unreg.method.as_str() {
lsp::notification::DidChangeWatchedFiles::METHOD => {
self.editor
.language_servers
.file_event_handler
.unregister(server_id, unreg.id);
}
_ => {
log::warn!("Received unregistration request for unsupported method: {}", unreg.method);
}
}
}
Ok(serde_json::Value::Null)
}
};
tokio::spawn(language_server.reply(id, reply));
tokio::spawn(language_server!().reply(id, reply));
}
Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
}
@ -1091,7 +1153,7 @@ impl Application {
pub async fn run<S>(&mut self, input_stream: &mut S) -> Result<i32, Error>
where
S: Stream<Item = crossterm::Result<crossterm::event::Event>> + Unpin,
S: Stream<Item = std::io::Result<crossterm::event::Event>> + Unpin,
{
self.claim_term().await?;

File diff suppressed because it is too large Load Diff

@ -2,7 +2,7 @@ use super::{Context, Editor};
use crate::{
compositor::{self, Compositor},
job::{Callback, Jobs},
ui::{self, overlay::overlaid, FilePicker, Picker, Popup, Prompt, PromptEvent, Text},
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent, Text},
};
use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
@ -73,21 +73,19 @@ fn thread_picker(
let debugger = debugger!(editor);
let thread_states = debugger.thread_states.clone();
let picker = FilePicker::new(
threads,
thread_states,
move |cx, thread, _action| callback_fn(cx.editor, thread),
move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
let frame = frames.get(0)?;
let path = frame.source.as_ref()?.path.clone()?;
let pos = Some((
frame.line.saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
));
Some((path.into(), pos))
},
);
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
callback_fn(cx.editor, thread)
})
.with_preview(move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
let frame = frames.get(0)?;
let path = frame.source.as_ref()?.path.clone()?;
let pos = Some((
frame.line.saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
));
Some((path.into(), pos))
});
compositor.push(Box::new(picker));
},
);
@ -219,7 +217,7 @@ pub fn dap_start_impl(
}
}
args.insert("cwd", to_value(std::env::current_dir().unwrap())?);
args.insert("cwd", to_value(helix_loader::current_working_dir())?);
let args = to_value(args).unwrap();
@ -341,8 +339,12 @@ fn debug_parameter_prompt(
.to_owned();
let completer = match field_type {
"filename" => ui::completers::filename,
"directory" => ui::completers::directory,
"filename" => |editor: &Editor, input: &str| {
ui::completers::filename_with_git_ignore(editor, input, false)
},
"directory" => |editor: &Editor, input: &str| {
ui::completers::directory_with_git_ignore(editor, input, false)
},
_ => ui::completers::none,
};
@ -580,7 +582,7 @@ pub fn dap_variables(cx: &mut Context) {
let contents = Text::from(tui::text::Text::from(variables));
let popup = Popup::new("dap-variables", contents);
cx.push_layer(Box::new(popup));
cx.replace_or_push_layer("dap-variables", popup);
}
pub fn dap_terminate(cx: &mut Context) {
@ -728,39 +730,35 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let frames = debugger.stack_frames[&thread_id].clone();
let picker = FilePicker::new(
frames,
(),
move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find
let pos = debugger.stack_frames[&thread_id]
.iter()
.position(|f| f.id == frame.id);
debugger.active_frame = pos;
let frame = debugger.stack_frames[&thread_id]
.get(pos.unwrap_or(0))
.cloned();
if let Some(frame) = &frame {
jump_to_stack_frame(cx.editor, frame);
}
},
move |_editor, frame| {
frame
.source
.as_ref()
.and_then(|source| source.path.clone())
.map(|path| {
(
path.into(),
Some((
frame.line.saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
)),
)
})
},
);
let picker = Picker::new(frames, (), move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find
let pos = debugger.stack_frames[&thread_id]
.iter()
.position(|f| f.id == frame.id);
debugger.active_frame = pos;
let frame = debugger.stack_frames[&thread_id]
.get(pos.unwrap_or(0))
.cloned();
if let Some(frame) = &frame {
jump_to_stack_frame(cx.editor, frame);
}
})
.with_preview(move |_editor, frame| {
frame
.source
.as_ref()
.and_then(|source| source.path.clone())
.map(|path| {
(
path.into(),
Some((
frame.line.saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
)),
)
})
});
cx.push_layer(Box::new(picker))
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

@ -16,6 +16,7 @@ pub enum EventResult {
}
use crate::job::Jobs;
use crate::ui::picker;
use helix_view::Editor;
pub use helix_view::input::Event;
@ -79,6 +80,7 @@ pub struct Compositor {
area: Rect,
pub(crate) last_picker: Option<Box<dyn Component>>,
pub(crate) full_redraw: bool,
}
impl Compositor {
@ -87,6 +89,7 @@ impl Compositor {
layers: Vec::new(),
area,
last_picker: None,
full_redraw: false,
}
}
@ -100,6 +103,11 @@ impl Compositor {
/// Add a layer to be rendered in front of all existing layers.
pub fn push(&mut self, mut layer: Box<dyn Component>) {
// immediately clear last_picker field to avoid excessive memory
// consumption for picker with many items
if layer.id() == Some(picker::ID) {
self.last_picker = None;
}
let size = self.size();
// trigger required_size on init
layer.required_size((size.width, size.height));
@ -200,6 +208,10 @@ impl Compositor {
.find(|component| component.id() == Some(id))
.and_then(|component| component.as_any_mut().downcast_mut())
}
pub fn need_full_redraw(&mut self) {
self.full_redraw = true;
}
}
// View casting, taken straight from Cursive

@ -1,5 +1,5 @@
use crate::keymap;
use crate::keymap::{merge_keys, Keymap};
use crate::keymap::{merge_keys, KeyTrie};
use helix_loader::merge_toml_values;
use helix_view::document::Mode;
use serde::Deserialize;
@ -12,7 +12,7 @@ use toml::de::Error as TomlError;
#[derive(Debug, Clone, PartialEq)]
pub struct Config {
pub theme: Option<String>,
pub keys: HashMap<Mode, Keymap>,
pub keys: HashMap<Mode, KeyTrie>,
pub editor: helix_view::editor::Config,
}
@ -20,7 +20,7 @@ pub struct Config {
#[serde(deny_unknown_fields)]
pub struct ConfigRaw {
pub theme: Option<String>,
pub keys: Option<HashMap<Mode, Keymap>>,
pub keys: Option<HashMap<Mode, KeyTrie>>,
pub editor: Option<toml::Value>,
}
@ -109,6 +109,7 @@ impl Config {
)?,
}
}
// these are just two io errors return the one for the global config
(Err(err), Err(_)) => return Err(err),
};
@ -138,7 +139,6 @@ mod tests {
#[test]
fn parsing_keymaps_config_file() {
use crate::keymap;
use crate::keymap::Keymap;
use helix_core::hashmap;
use helix_view::document::Mode;
@ -155,13 +155,13 @@ mod tests {
merge_keys(
&mut keys,
hashmap! {
Mode::Insert => Keymap::new(keymap!({ "Insert mode"
Mode::Insert => keymap!({ "Insert mode"
"y" => move_line_down,
"S-C-a" => delete_selection,
})),
Mode::Normal => Keymap::new(keymap!({ "Normal mode"
}),
Mode::Normal => keymap!({ "Normal mode"
"A-F12" => move_next_word_end,
})),
}),
},
);

@ -192,10 +192,14 @@ pub fn languages_all() -> std::io::Result<()> {
for lang in &syn_loader_conf.language {
column(&lang.language_id, Color::Reset);
let lsp = lang
.language_server
.as_ref()
.map(|lsp| lsp.command.to_string());
// TODO multiple language servers (check binary for each supported language server, not just the first)
let lsp = lang.language_servers.first().and_then(|ls| {
syn_loader_conf
.language_server
.get(&ls.name)
.map(|config| config.command.clone())
});
check_binary(lsp);
let dap = lang.debugger.as_ref().map(|dap| dap.command.to_string());
@ -264,11 +268,15 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
}
};
// TODO multiple language servers
probe_protocol(
"language server",
lang.language_server
.as_ref()
.map(|lsp| lsp.command.to_string()),
lang.language_servers.first().and_then(|ls| {
syn_loader_conf
.language_server
.get(&ls.name)
.map(|config| config.command.clone())
}),
)?;
probe_protocol(

@ -18,7 +18,7 @@ use std::{
pub use default::default;
use macros::key;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, Default)]
pub struct KeyTrieNode {
/// A label for keys coming under this node, like "Goto mode"
name: String,
@ -52,10 +52,6 @@ impl KeyTrieNode {
}
}
pub fn name(&self) -> &str {
&self.name
}
/// Merge another Node in. Leaves and subnodes from the other node replace
/// corresponding keyevent in self, except when both other and self have
/// subnodes for same key. In that case the merge is recursive.
@ -77,49 +73,40 @@ impl KeyTrieNode {
}
pub fn infobox(&self) -> Info {
let mut body: Vec<(&str, BTreeSet<KeyEvent>)> = Vec::with_capacity(self.len());
let mut body: Vec<(BTreeSet<KeyEvent>, &str)> = Vec::with_capacity(self.len());
for (&key, trie) in self.iter() {
let desc = match trie {
KeyTrie::Leaf(cmd) => {
KeyTrie::MappableCommand(cmd) => {
if cmd.name() == "no_op" {
continue;
}
cmd.doc()
}
KeyTrie::Node(n) => n.name(),
KeyTrie::Node(n) => &n.name,
KeyTrie::Sequence(_) => "[Multiple commands]",
};
match body.iter().position(|(d, _)| d == &desc) {
match body.iter().position(|(_, d)| d == &desc) {
Some(pos) => {
body[pos].1.insert(key);
body[pos].0.insert(key);
}
None => body.push((desc, BTreeSet::from([key]))),
None => body.push((BTreeSet::from([key]), desc)),
}
}
body.sort_unstable_by_key(|(_, keys)| {
body.sort_unstable_by_key(|(keys, _)| {
self.order
.iter()
.position(|&k| k == *keys.iter().next().unwrap())
.unwrap()
});
let prefix = format!("{} ", self.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
body = body
.into_iter()
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect();
}
Info::from_keymap(self.name(), body)
}
/// Get a reference to the key trie node's order.
pub fn order(&self) -> &[KeyEvent] {
self.order.as_slice()
}
}
impl Default for KeyTrieNode {
fn default() -> Self {
Self::new("", HashMap::new(), Vec::new())
let body: Vec<_> = body
.into_iter()
.map(|(events, desc)| {
let events = events.iter().map(ToString::to_string).collect::<Vec<_>>();
(events.join(", "), desc)
})
.collect();
Info::new(&self.name, &body)
}
}
@ -145,7 +132,7 @@ impl DerefMut for KeyTrieNode {
#[derive(Debug, Clone, PartialEq)]
pub enum KeyTrie {
Leaf(MappableCommand),
MappableCommand(MappableCommand),
Sequence(Vec<MappableCommand>),
Node(KeyTrieNode),
}
@ -174,7 +161,7 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
{
command
.parse::<MappableCommand>()
.map(KeyTrie::Leaf)
.map(KeyTrie::MappableCommand)
.map_err(E::custom)
}
@ -208,17 +195,43 @@ impl<'de> serde::de::Visitor<'de> for KeyTrieVisitor {
}
impl KeyTrie {
pub fn reverse_map(&self) -> ReverseKeymap {
// recursively visit all nodes in keymap
fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
match node {
KeyTrie::MappableCommand(cmd) => {
let name = cmd.name();
if name != "no_op" {
cmd_map.entry(name.into()).or_default().push(keys.clone())
}
}
KeyTrie::Node(next) => {
for (key, trie) in &next.map {
keys.push(*key);
map_node(cmd_map, trie, keys);
keys.pop();
}
}
KeyTrie::Sequence(_) => {}
};
}
let mut res = HashMap::new();
map_node(&mut res, self, &mut Vec::new());
res
}
pub fn node(&self) -> Option<&KeyTrieNode> {
match *self {
KeyTrie::Node(ref node) => Some(node),
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
}
}
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
match *self {
KeyTrie::Node(ref mut node) => Some(node),
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
}
}
@ -235,7 +248,7 @@ impl KeyTrie {
trie = match trie {
KeyTrie::Node(map) => map.get(key),
// leaf encountered while keys left to process
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
KeyTrie::MappableCommand(_) | KeyTrie::Sequence(_) => None,
}?
}
Some(trie)
@ -256,75 +269,11 @@ pub enum KeymapResult {
Cancelled(Vec<KeyEvent>),
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(transparent)]
pub struct Keymap {
/// Always a Node
root: KeyTrie,
}
/// A map of command names to keybinds that will execute the command.
pub type ReverseKeymap = HashMap<String, Vec<Vec<KeyEvent>>>;
impl Keymap {
pub fn new(root: KeyTrie) -> Self {
Keymap { root }
}
pub fn reverse_map(&self) -> ReverseKeymap {
// recursively visit all nodes in keymap
fn map_node(cmd_map: &mut ReverseKeymap, node: &KeyTrie, keys: &mut Vec<KeyEvent>) {
match node {
KeyTrie::Leaf(cmd) => match cmd {
MappableCommand::Typable { name, .. } => {
cmd_map.entry(name.into()).or_default().push(keys.clone())
}
MappableCommand::Static { name, .. } => cmd_map
.entry(name.to_string())
.or_default()
.push(keys.clone()),
},
KeyTrie::Node(next) => {
for (key, trie) in &next.map {
keys.push(*key);
map_node(cmd_map, trie, keys);
keys.pop();
}
}
KeyTrie::Sequence(_) => {}
};
}
let mut res = HashMap::new();
map_node(&mut res, &self.root, &mut Vec::new());
res
}
pub fn root(&self) -> &KeyTrie {
&self.root
}
pub fn merge(&mut self, other: Self) {
self.root.merge_nodes(other.root);
}
}
impl Deref for Keymap {
type Target = KeyTrieNode;
fn deref(&self) -> &Self::Target {
self.root.node().unwrap()
}
}
impl Default for Keymap {
fn default() -> Self {
Self::new(KeyTrie::Node(KeyTrieNode::default()))
}
}
pub struct Keymaps {
pub map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>,
pub map: Box<dyn DynAccess<HashMap<Mode, KeyTrie>>>,
/// Stores pending keys waiting for the next key. This is relative to a
/// sticky node if one is in use.
state: Vec<KeyEvent>,
@ -333,7 +282,7 @@ pub struct Keymaps {
}
impl Keymaps {
pub fn new(map: Box<dyn DynAccess<HashMap<Mode, Keymap>>>) -> Self {
pub fn new(map: Box<dyn DynAccess<HashMap<Mode, KeyTrie>>>) -> Self {
Self {
map,
state: Vec::new(),
@ -341,7 +290,7 @@ impl Keymaps {
}
}
pub fn map(&self) -> DynGuard<HashMap<Mode, Keymap>> {
pub fn map(&self) -> DynGuard<HashMap<Mode, KeyTrie>> {
self.map.load()
}
@ -373,11 +322,11 @@ impl Keymaps {
let first = self.state.get(0).unwrap_or(&key);
let trie_node = match self.sticky {
Some(ref trie) => Cow::Owned(KeyTrie::Node(trie.clone())),
None => Cow::Borrowed(&keymap.root),
None => Cow::Borrowed(keymap),
};
let trie = match trie_node.search(&[*first]) {
Some(KeyTrie::Leaf(ref cmd)) => {
Some(KeyTrie::MappableCommand(ref cmd)) => {
return KeymapResult::Matched(cmd.clone());
}
Some(KeyTrie::Sequence(ref cmds)) => {
@ -396,7 +345,7 @@ impl Keymaps {
}
KeymapResult::Pending(map.clone())
}
Some(KeyTrie::Leaf(cmd)) => {
Some(KeyTrie::MappableCommand(cmd)) => {
self.state.clear();
KeymapResult::Matched(cmd.clone())
}
@ -416,9 +365,13 @@ impl Default for Keymaps {
}
/// Merge default config keys with user overwritten keys for custom user config.
pub fn merge_keys(dst: &mut HashMap<Mode, Keymap>, mut delta: HashMap<Mode, Keymap>) {
pub fn merge_keys(dst: &mut HashMap<Mode, KeyTrie>, mut delta: HashMap<Mode, KeyTrie>) {
for (mode, keys) in dst {
keys.merge(delta.remove(mode).unwrap_or_default())
keys.merge_nodes(
delta
.remove(mode)
.unwrap_or_else(|| KeyTrie::Node(KeyTrieNode::default())),
)
}
}
@ -447,17 +400,15 @@ mod tests {
#[test]
fn merge_partial_keys() {
let keymap = hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
"i" => normal_mode,
"无" => insert_mode,
"z" => jump_backward,
"g" => { "Merge into goto mode"
"$" => goto_line_end,
"g" => delete_char_forward,
},
})
)
Mode::Normal => keymap!({ "Normal mode"
"i" => normal_mode,
"无" => insert_mode,
"z" => jump_backward,
"g" => { "Merge into goto mode"
"$" => goto_line_end,
"g" => delete_char_forward,
},
})
};
let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone());
@ -484,40 +435,52 @@ mod tests {
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_line_end),
keymap.search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::delete_char_forward),
keymap.search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::Leaf(MappableCommand::goto_last_line),
keymap.search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::goto_last_line),
"Old leaves in subnode should be present in merged node"
);
assert!(merged_keyamp.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_keyamp.get(&Mode::Insert).unwrap().len() > 0);
assert!(
merged_keyamp
.get(&Mode::Normal)
.and_then(|key_trie| key_trie.node())
.unwrap()
.len()
> 1
);
assert!(
merged_keyamp
.get(&Mode::Insert)
.and_then(|key_trie| key_trie.node())
.unwrap()
.len()
> 0
);
}
#[test]
fn order_should_be_set() {
let keymap = hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
"space" => { ""
"s" => { ""
"v" => vsplit,
"c" => hsplit,
},
Mode::Normal => keymap!({ "Normal mode"
"space" => { ""
"s" => { ""
"v" => vsplit,
"c" => hsplit,
},
})
)
},
})
};
let mut merged_keyamp = default();
merge_keys(&mut merged_keyamp, keymap.clone());
@ -525,22 +488,19 @@ mod tests {
let keymap = merged_keyamp.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works
assert_eq!(
keymap
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
&KeyTrie::Leaf(MappableCommand::vsplit),
keymap.search(&[key!(' '), key!('s'), key!('v')]).unwrap(),
&KeyTrie::MappableCommand(MappableCommand::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty())
let node = keymap.search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order.as_slice().is_empty())
}
#[test]
fn aliased_modes_are_same_in_default_keymap() {
let keymaps = Keymaps::default().map();
let root = keymaps.get(&Mode::Normal).unwrap().root();
let root = keymaps.get(&Mode::Normal).unwrap();
assert_eq!(
root.search(&[key!(' '), key!('w')]).unwrap(),
root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
@ -563,7 +523,7 @@ mod tests {
},
"j" | "k" => move_line_down,
});
let keymap = Keymap::new(normal_mode);
let keymap = normal_mode;
let mut reverse_map = keymap.reverse_map();
// sort keybindings in order to have consistent tests
@ -611,7 +571,7 @@ mod tests {
modifiers: KeyModifiers::NONE,
};
let expectation = Keymap::new(KeyTrie::Node(KeyTrieNode::new(
let expectation = KeyTrie::Node(KeyTrieNode::new(
"",
hashmap! {
key => KeyTrie::Sequence(vec!{
@ -628,7 +588,7 @@ mod tests {
})
},
vec![key],
)));
));
assert_eq!(toml::from_str(keys), Ok(expectation));
}

@ -1,10 +1,10 @@
use std::collections::HashMap;
use super::macros::keymap;
use super::{Keymap, Mode};
use super::{KeyTrie, Mode};
use helix_core::hashmap;
pub fn default() -> HashMap<Mode, Keymap> {
pub fn default() -> HashMap<Mode, KeyTrie> {
let normal = keymap!({ "Normal mode"
"h" | "left" => move_char_left,
"j" | "down" => move_visual_line_down,
@ -79,6 +79,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"s" => select_regex,
"A-s" => split_selection_on_newline,
"A-minus" => merge_selections,
"A-_" => merge_consecutive_selections,
"S" => split_selection,
";" => collapse_selection,
@ -87,6 +88,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"A-i" | "A-down" => shrink_selection,
"A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling,
"A-e" => move_parent_node_end,
"A-b" => move_parent_node_start,
"%" => select_all,
"x" => extend_line_below,
@ -264,7 +267,7 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-v" | "v" => vsplit_new,
},
},
"y" => yank_joined_to_clipboard,
"y" => yank_to_clipboard,
"Y" => yank_main_selection_to_clipboard,
"p" => paste_clipboard_after,
"P" => paste_clipboard_before,
@ -335,6 +338,9 @@ pub fn default() -> HashMap<Mode, Keymap> {
"B" => extend_prev_long_word_start,
"E" => extend_next_long_word_end,
"A-e" => extend_parent_node_end,
"A-b" => extend_parent_node_start,
"n" => extend_search_next,
"N" => extend_search_prev,
@ -367,7 +373,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"C-h" | "backspace" | "S-backspace" => delete_char_backward,
"C-d" | "del" => delete_char_forward,
"C-j" | "ret" => insert_newline,
"tab" => insert_tab,
"tab" => smart_tab,
"S-tab" => insert_tab,
"up" => move_visual_line_up,
"down" => move_visual_line_down,
@ -379,8 +386,8 @@ pub fn default() -> HashMap<Mode, Keymap> {
"end" => goto_line_end_newline,
});
hashmap!(
Mode::Normal => Keymap::new(normal),
Mode::Select => Keymap::new(select),
Mode::Insert => Keymap::new(insert),
Mode::Normal => normal,
Mode::Select => select,
Mode::Insert => insert,
)
}

@ -62,12 +62,11 @@ macro_rules! alt {
};
}
/// Macro for defining the root of a `Keymap` object. Example:
/// Macro for defining a `KeyTrie`. Example:
///
/// ```
/// # use helix_core::hashmap;
/// # use helix_term::keymap;
/// # use helix_term::keymap::Keymap;
/// let normal_mode = keymap!({ "Normal mode"
/// "i" => insert_mode,
/// "g" => { "Goto"
@ -76,12 +75,12 @@ macro_rules! alt {
/// },
/// "j" | "down" => move_line_down,
/// });
/// let keymap = Keymap::new(normal_mode);
/// let keymap = normal_mode;
/// ```
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::MappableCommand::$cmd)
$crate::keymap::KeyTrie::MappableCommand($crate::commands::MappableCommand::$cmd)
};
(@trie

@ -4,9 +4,8 @@ use helix_loader::VERSION_AND_GIT_HASH;
use helix_term::application::Application;
use helix_term::args::Args;
use helix_term::config::{Config, ConfigLoadError};
use std::path::PathBuf;
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
fn setup_logging(verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new();
base_config = match verbosity {
@ -27,7 +26,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message
))
})
.chain(fern::log_file(logpath)?);
.chain(fern::log_file(helix_loader::log_file())?);
base_config.chain(file_config).apply()?;
@ -41,12 +40,6 @@ fn main() -> Result<()> {
#[tokio::main]
async fn main_impl() -> Result<i32> {
let logpath = helix_loader::log_file();
let parent = logpath.parent().unwrap();
if !parent.exists() {
std::fs::create_dir_all(parent).ok();
}
let help = format!(
"\
{} {}
@ -68,7 +61,7 @@ FLAGS:
-g, --grammar {{fetch|build}} Fetches or builds tree-sitter grammars listed in languages.toml
-c, --config <file> Specifies a file to use for configuration
-v Increases logging verbosity each use for up to 3 times
--log Specifies a file to use for logging
--log <file> Specifies a file to use for logging
(default file: {})
-V, --version Prints version information
--vsplit Splits all given files vertically into different windows
@ -78,11 +71,14 @@ FLAGS:
VERSION_AND_GIT_HASH,
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
logpath.display(),
helix_loader::default_log_file().display(),
);
let args = Args::parse_args().context("could not parse arguments")?;
helix_loader::initialize_config_file(args.config_file.clone());
helix_loader::initialize_log_file(args.log_file.clone());
// Help has a higher priority and should be handled separately.
if args.display_help {
print!("{}", help);
@ -116,15 +112,7 @@ FLAGS:
return Ok(0);
}
let logpath = args.log_file.as_ref().cloned().unwrap_or(logpath);
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
let config_dir = helix_loader::config_dir();
if !config_dir.exists() {
std::fs::create_dir_all(&config_dir).ok();
}
helix_loader::initialize_config_file(args.config_file.clone());
setup_logging(args.verbosity).context("failed to initialize logging")?;
let config = match Config::load_default() {
Ok(config) => config,

@ -15,8 +15,7 @@ use helix_view::{graphics::Rect, Document, Editor};
use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::{lsp, util};
use lsp::CompletionItem;
use helix_lsp::{lsp, util, OffsetEncoding};
impl menu::Item for CompletionItem {
type Data = ();
@ -26,28 +25,30 @@ impl menu::Item for CompletionItem {
#[inline]
fn filter_text(&self, _data: &Self::Data) -> Cow<str> {
self.filter_text
self.item
.filter_text
.as_ref()
.unwrap_or(&self.label)
.unwrap_or(&self.item.label)
.as_str()
.into()
}
fn format(&self, _data: &Self::Data) -> menu::Row {
let deprecated = self.deprecated.unwrap_or_default()
|| self.tags.as_ref().map_or(false, |tags| {
let deprecated = self.item.deprecated.unwrap_or_default()
|| self.item.tags.as_ref().map_or(false, |tags| {
tags.contains(&lsp::CompletionItemTag::DEPRECATED)
});
menu::Row::new(vec![
menu::Cell::from(Span::styled(
self.label.as_str(),
self.item.label.as_str(),
if deprecated {
Style::default().add_modifier(Modifier::CROSSED_OUT)
} else {
Style::default()
},
)),
menu::Cell::from(match self.kind {
menu::Cell::from(match self.item.kind {
Some(lsp::CompletionItemKind::TEXT) => "text",
Some(lsp::CompletionItemKind::METHOD) => "method",
Some(lsp::CompletionItemKind::FUNCTION) => "function",
@ -79,15 +80,17 @@ impl menu::Item for CompletionItem {
}
None => "",
}),
// self.detail.as_deref().unwrap_or("")
// self.label_details
// .as_ref()
// .or(self.detail())
// .as_str(),
])
}
}
#[derive(Debug, PartialEq, Default, Clone)]
pub struct CompletionItem {
pub item: lsp::CompletionItem,
pub language_server_id: usize,
pub resolved: bool,
}
/// Wraps a Menu.
pub struct Completion {
popup: Popup<Menu<CompletionItem>>,
@ -104,21 +107,21 @@ impl Completion {
editor: &Editor,
savepoint: Arc<SavePoint>,
mut items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize,
) -> Self {
let preview_completion_insert = editor.config().preview_completion_insert;
let replace_mode = editor.config().completion_replace;
// Sort completion items according to their preselect status (given by the LSP server)
items.sort_by_key(|item| !item.preselect.unwrap_or(false));
items.sort_by_key(|item| !item.item.preselect.unwrap_or(false));
// Then create the menu
let menu = Menu::new(items, (), move |editor: &mut Editor, item, event| {
fn item_to_transaction(
doc: &Document,
view_id: ViewId,
item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding,
item: &lsp::CompletionItem,
offset_encoding: OffsetEncoding,
trigger_offset: usize,
include_placeholder: bool,
replace_mode: bool,
@ -141,7 +144,9 @@ impl Completion {
}
};
let Some(range) = util::lsp_range_to_range(doc.text(), edit.range, offset_encoding) else{
let Some(range) =
util::lsp_range_to_range(doc.text(), edit.range, offset_encoding)
else {
return Transaction::new(doc.text());
};
@ -209,77 +214,110 @@ impl Completion {
let (view, doc) = current!(editor);
// if more text was entered, remove it
doc.restore(view, &savepoint);
macro_rules! language_server {
($item:expr) => {
match editor
.language_servers
.get_by_id($item.language_server_id)
{
Some(ls) => ls,
None => {
editor.set_error("completions are outdated");
// TODO close the completion menu somehow,
// currently there is no trivial way to access the EditorView to close the completion menu
return;
}
}
};
}
match event {
PromptEvent::Abort => {
editor.last_completion = None;
}
PromptEvent::Update => {
PromptEvent::Abort => {}
PromptEvent::Update if preview_completion_insert => {
// Update creates "ghost" transactions which are not sent to the
// lsp server to avoid messing up re-requesting completions. Once a
// completion has been selected (with tab, c-n or c-p) it's always accepted whenever anything
// is typed. The only way to avoid that is to explicitly abort the completion
// with c-c. This will remove the "ghost" transaction.
//
// The ghost transaction is modeled with a transaction that is not sent to the LS.
// (apply_temporary) and a savepoint. It's extremely important this savepoint is restored
// (also without sending the transaction to the LS) *before any further transaction is applied*.
// Otherwise incremental sync breaks (since the state of the LS doesn't match the state the transaction
// is applied to).
if editor.last_completion.is_none() {
editor.last_completion = Some(CompleteAction::Selected {
savepoint: doc.savepoint(view),
})
}
// if more text was entered, remove it
doc.restore(view, &savepoint, false);
// always present here
let item = item.unwrap();
let transaction = item_to_transaction(
doc,
view.id,
item,
offset_encoding,
&item.item,
language_server!(item).offset_encoding(),
trigger_offset,
true,
replace_mode,
);
// initialize a savepoint
doc.apply(&transaction, view.id);
editor.last_completion = Some(CompleteAction {
trigger_offset,
changes: completion_changes(&transaction, trigger_offset),
});
doc.apply_temporary(&transaction, view.id);
}
PromptEvent::Update => {}
PromptEvent::Validate => {
if let Some(CompleteAction::Selected { savepoint }) =
editor.last_completion.take()
{
doc.restore(view, &savepoint, false);
}
// always present here
let item = item.unwrap();
let mut item = item.unwrap().clone();
let language_server = language_server!(item);
let offset_encoding = language_server.offset_encoding();
let language_server = editor
.language_servers
.get_by_id(item.language_server_id)
.unwrap();
// resolve item if not yet resolved
if !item.resolved {
if let Some(resolved) =
Self::resolve_completion_item(language_server, item.item.clone())
{
item.item = resolved;
}
};
// if more text was entered, remove it
doc.restore(view, &savepoint, true);
// save an undo checkpoint before the completion
doc.append_changes_to_history(view);
let transaction = item_to_transaction(
doc,
view.id,
item,
&item.item,
offset_encoding,
trigger_offset,
false,
replace_mode,
);
doc.apply(&transaction, view.id);
editor.last_completion = Some(CompleteAction {
editor.last_completion = Some(CompleteAction::Applied {
trigger_offset,
changes: completion_changes(&transaction, trigger_offset),
});
// apply additional edits, mostly used to auto import unqualified types
let resolved_item = if item
.additional_text_edits
.as_ref()
.map(|edits| !edits.is_empty())
.unwrap_or(false)
{
None
} else {
Self::resolve_completion_item(doc, item.clone())
};
if let Some(additional_edits) = resolved_item
.as_ref()
.and_then(|item| item.additional_text_edits.as_ref())
.or(item.additional_text_edits.as_ref())
{
// TODO: add additional _edits to completion_changes?
if let Some(additional_edits) = item.item.additional_text_edits {
if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits(
doc.text(),
additional_edits.clone(),
additional_edits,
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
@ -304,11 +342,9 @@ impl Completion {
}
fn resolve_completion_item(
doc: &Document,
language_server: &helix_lsp::Client,
completion_item: lsp::CompletionItem,
) -> Option<CompletionItem> {
let language_server = doc.language_server()?;
) -> Option<lsp::CompletionItem> {
let future = language_server.resolve_completion_item(completion_item)?;
let response = helix_lsp::block_on(future);
match response {
@ -359,7 +395,7 @@ impl Completion {
self.popup.contents().is_empty()
}
fn replace_item(&mut self, old_item: lsp::CompletionItem, new_item: lsp::CompletionItem) {
fn replace_item(&mut self, old_item: CompletionItem, new_item: CompletionItem) {
self.popup.contents_mut().replace_option(old_item, new_item);
}
@ -375,19 +411,21 @@ impl Completion {
// > The returned completion item should have the documentation property filled in.
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
let current_item = match self.popup.contents().selection() {
Some(item) if item.documentation.is_none() => item.clone(),
Some(item) if !item.resolved => item.clone(),
_ => return false,
};
let language_server = match doc!(cx.editor).language_server() {
Some(language_server) => language_server,
None => return false,
let Some(language_server) = cx
.editor
.language_server_by_id(current_item.language_server_id)
else {
return false;
};
// This method should not block the compositor so we handle the response asynchronously.
let future = match language_server.resolve_completion_item(current_item.clone()) {
Some(future) => future,
None => return false,
let Some(future) = language_server.resolve_completion_item(current_item.item.clone())
else {
return false;
};
cx.callback(
@ -403,6 +441,12 @@ impl Completion {
.unwrap()
.completion
{
let resolved_item = CompletionItem {
item: resolved_item,
language_server_id: current_item.language_server_id,
resolved: true,
};
completion.replace_item(current_item, resolved_item);
}
},
@ -457,25 +501,25 @@ impl Component for Completion {
Markdown::new(md, cx.editor.syn_loader.clone())
};
let mut markdown_doc = match &option.documentation {
let mut markdown_doc = match &option.item.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: contents,
})) => {
// TODO: convert to wrapped text
markdowned(language, option.detail.as_deref(), Some(contents))
markdowned(language, option.item.detail.as_deref(), Some(contents))
}
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: contents,
})) => {
// TODO: set language based on doc scope
markdowned(language, option.detail.as_deref(), Some(contents))
markdowned(language, option.item.detail.as_deref(), Some(contents))
}
None if option.detail.is_some() => {
None if option.item.detail.is_some() => {
// TODO: set language based on doc scope
markdowned(language, option.detail.as_deref(), None)
markdowned(language, option.item.detail.as_deref(), None)
}
None => return,
};

@ -19,7 +19,7 @@ use helix_core::{
syntax::{self, HighlightEvent},
text_annotations::TextAnnotations,
unicode::width::UnicodeWidthStr,
visual_offset_from_block, Position, Range, Selection, Transaction,
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
};
use helix_view::{
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
@ -33,7 +33,7 @@ use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc};
use tui::{buffer::Buffer as Surface, text::Span};
use super::statusline;
use super::{completion::CompletionItem, statusline};
use super::{document::LineDecoration, lsp::SignatureHelp};
pub struct EditorView {
@ -43,12 +43,17 @@ pub struct EditorView {
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
spinners: ProgressSpinners,
/// Tracks if the terminal window is focused by reaction to terminal focus events
terminal_focused: bool,
}
#[derive(Debug, Clone)]
pub enum InsertEvent {
Key(KeyEvent),
CompletionApply(CompleteAction),
CompletionApply {
trigger_offset: usize,
changes: Vec<Change>,
},
TriggerCompletion,
RequestCompletion,
}
@ -68,6 +73,7 @@ impl EditorView {
last_insert: (commands::MappableCommand::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
terminal_focused: true,
}
}
@ -103,7 +109,7 @@ impl EditorView {
// Set DAP highlights, if needed.
if let Some(frame) = editor.current_stack_frame() {
let dap_line = frame.line.saturating_sub(1) as usize;
let dap_line = frame.line.saturating_sub(1);
let style = theme.get("ui.highlight.frameline");
let line_decoration = move |renderer: &mut TextRenderer, pos: LinePos| {
if pos.doc_line != dap_line {
@ -160,15 +166,18 @@ impl EditorView {
Box::new(highlights)
};
Self::render_gutter(
editor,
doc,
view,
view.area,
theme,
is_focused,
&mut line_decorations,
);
let gutter_overflow = view.gutter_offset(doc) == 0;
if !gutter_overflow {
Self::render_gutter(
editor,
doc,
view,
view.area,
theme,
is_focused & self.terminal_focused,
&mut line_decorations,
);
}
if is_focused {
let cursor = doc
@ -498,7 +507,9 @@ impl EditorView {
use helix_core::match_brackets;
let pos = doc.selection(view.id).primary().cursor(text);
if let Some(pos) = match_brackets::find_matching_bracket(syntax, doc.text(), pos) {
if let Some(pos) =
match_brackets::find_matching_bracket(syntax, doc.text().slice(..), pos)
{
// ensure col is on screen
if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") {
return vec![(highlight, pos..pos + 1)];
@ -647,7 +658,7 @@ impl EditorView {
.primary()
.cursor(doc.text().slice(..));
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
let diagnostics = doc.shown_diagnostics().filter(|diagnostic| {
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
});
@ -813,7 +824,7 @@ impl EditorView {
}
(Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion
self.completion = None;
self.clear_completion(cxt.editor);
cxt.editor.completion_request_handle = None;
// TODO: Use an on_mode_change hook to remove signature help
@ -891,22 +902,26 @@ impl EditorView {
for key in self.last_insert.1.clone() {
match key {
InsertEvent::Key(key) => self.insert_mode(cxt, key),
InsertEvent::CompletionApply(compl) => {
InsertEvent::CompletionApply {
trigger_offset,
changes,
} => {
let (view, doc) = current!(cxt.editor);
if let Some(last_savepoint) = last_savepoint.as_deref() {
doc.restore(view, last_savepoint);
doc.restore(view, last_savepoint, true);
}
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let shift_position =
|pos: usize| -> usize { pos + cursor - compl.trigger_offset };
let shift_position = |pos: usize| -> usize {
(pos + cursor).saturating_sub(trigger_offset)
};
let tx = Transaction::change(
doc.text(),
compl.changes.iter().cloned().map(|(start, end, t)| {
changes.iter().cloned().map(|(start, end, t)| {
(shift_position(start), shift_position(end), t)
}),
);
@ -937,6 +952,8 @@ impl EditorView {
self.handle_keymap_event(mode, cxt, event);
if self.keymaps.pending().is_empty() {
cxt.editor.count = None
} else {
cxt.editor.selected_register = cxt.register.take();
}
}
}
@ -947,20 +964,13 @@ impl EditorView {
&mut self,
editor: &mut Editor,
savepoint: Arc<SavePoint>,
items: Vec<helix_lsp::lsp::CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
items: Vec<CompletionItem>,
start_offset: usize,
trigger_offset: usize,
size: Rect,
) -> Option<Rect> {
let mut completion = Completion::new(
editor,
savepoint,
items,
offset_encoding,
start_offset,
trigger_offset,
);
let mut completion =
Completion::new(editor, savepoint, items, start_offset, trigger_offset);
if completion.is_empty() {
// skip if we got no completion results
@ -979,6 +989,21 @@ impl EditorView {
pub fn clear_completion(&mut self, editor: &mut Editor) {
self.completion = None;
if let Some(last_completion) = editor.last_completion.take() {
match last_completion {
CompleteAction::Applied {
trigger_offset,
changes,
} => self.last_insert.1.push(InsertEvent::CompletionApply {
trigger_offset,
changes,
}),
CompleteAction::Selected { savepoint } => {
let (view, doc) = current!(editor);
doc.restore(view, &savepoint, false);
}
}
}
// Clear any savepoints
editor.clear_idle_timer(); // don't retrigger
@ -1048,6 +1073,7 @@ impl EditorView {
let editor = &mut cxt.editor;
if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) {
let prev_view_id = view!(editor).id;
let doc = doc_mut!(editor, &view!(editor, view_id).doc);
if modifiers == KeyModifiers::ALT {
@ -1057,6 +1083,10 @@ impl EditorView {
doc.set_selection(view_id, Selection::point(pos));
}
if view_id != prev_view_id {
self.clear_completion(editor);
}
editor.focus(view_id);
editor.ensure_cursor_in_view(view_id);
@ -1265,12 +1295,22 @@ impl Component for EditorView {
jobs: cx.jobs,
scroll: None,
};
completion.handle_event(event, &mut cx)
};
if let EventResult::Consumed(callback) = res {
consumed = true;
if let EventResult::Consumed(callback) =
completion.handle_event(event, &mut cx)
{
consumed = true;
Some(callback)
} else if let EventResult::Consumed(callback) =
completion.handle_event(&Event::Key(key!(Enter)), &mut cx)
{
Some(callback)
} else {
None
}
};
if let Some(callback) = res {
if callback.is_some() {
// assume close_fn
self.clear_completion(cx.editor);
@ -1286,10 +1326,6 @@ impl Component for EditorView {
// if completion didn't take the event, we pass it onto commands
if !consumed {
if let Some(compl) = cx.editor.last_completion.take() {
self.last_insert.1.push(InsertEvent::CompletionApply(compl));
}
self.insert_mode(&mut cx, key);
// record last_insert key
@ -1344,13 +1380,17 @@ impl Component for EditorView {
Event::Mouse(event) => self.handle_mouse_event(event, &mut cx),
Event::IdleTimeout => self.handle_idle_timeout(&mut cx),
Event::FocusGained => EventResult::Ignored(None),
Event::FocusGained => {
self.terminal_focused = true;
EventResult::Consumed(None)
}
Event::FocusLost => {
if context.editor.config().auto_save {
if let Err(e) = commands::typed::write_all_impl(context, false, false) {
context.editor.set_error(format!("{}", e));
}
}
self.terminal_focused = false;
EventResult::Consumed(None)
}
}

@ -1,239 +0,0 @@
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
#[cfg(test)]
mod test;
struct QueryAtom {
kind: QueryAtomKind,
atom: String,
ignore_case: bool,
inverse: bool,
}
impl QueryAtom {
fn new(atom: &str) -> Option<QueryAtom> {
let mut atom = atom.to_string();
let inverse = atom.starts_with('!');
if inverse {
atom.remove(0);
}
let mut kind = match atom.chars().next() {
Some('^') => QueryAtomKind::Prefix,
Some('\'') => QueryAtomKind::Substring,
_ if inverse => QueryAtomKind::Substring,
_ => QueryAtomKind::Fuzzy,
};
if atom.starts_with(['^', '\'']) {
atom.remove(0);
}
if atom.is_empty() {
return None;
}
if atom.ends_with('$') && !atom.ends_with("\\$") {
atom.pop();
kind = if kind == QueryAtomKind::Prefix {
QueryAtomKind::Exact
} else {
QueryAtomKind::Postfix
}
}
Some(QueryAtom {
kind,
atom: atom.replace('\\', ""),
// not ideal but fuzzy_matches only knows ascii uppercase so more consistent
// to behave the same
ignore_case: kind != QueryAtomKind::Fuzzy
&& atom.chars().all(|c| c.is_ascii_lowercase()),
inverse,
})
}
fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec<usize>) -> bool {
// for inverse there are no indices to return
// just return whether we matched
if self.inverse {
return self.matches(matcher, item);
}
let buf;
let item = if self.ignore_case {
buf = item.to_ascii_lowercase();
&buf
} else {
item
};
let off = match self.kind {
QueryAtomKind::Fuzzy => {
if let Some((_, fuzzy_indices)) = matcher.fuzzy_indices(item, &self.atom) {
indices.extend_from_slice(&fuzzy_indices);
return true;
} else {
return false;
}
}
QueryAtomKind::Substring => {
if let Some(off) = item.find(&self.atom) {
off
} else {
return false;
}
}
QueryAtomKind::Prefix if item.starts_with(&self.atom) => 0,
QueryAtomKind::Postfix if item.ends_with(&self.atom) => item.len() - self.atom.len(),
QueryAtomKind::Exact if item == self.atom => 0,
_ => return false,
};
indices.extend(off..(off + self.atom.len()));
true
}
fn matches(&self, matcher: &Matcher, item: &str) -> bool {
let buf;
let item = if self.ignore_case {
buf = item.to_ascii_lowercase();
&buf
} else {
item
};
let mut res = match self.kind {
QueryAtomKind::Fuzzy => matcher.fuzzy_match(item, &self.atom).is_some(),
QueryAtomKind::Substring => item.contains(&self.atom),
QueryAtomKind::Prefix => item.starts_with(&self.atom),
QueryAtomKind::Postfix => item.ends_with(&self.atom),
QueryAtomKind::Exact => item == self.atom,
};
if self.inverse {
res = !res;
}
res
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum QueryAtomKind {
/// Item is a fuzzy match of this behaviour
///
/// Usage: `foo`
Fuzzy,
/// Item contains query atom as a continuous substring
///
/// Usage `'foo`
Substring,
/// Item starts with query atom
///
/// Usage: `^foo`
Prefix,
/// Item ends with query atom
///
/// Usage: `foo$`
Postfix,
/// Item is equal to query atom
///
/// Usage `^foo$`
Exact,
}
#[derive(Default)]
pub struct FuzzyQuery {
first_fuzzy_atom: Option<String>,
query_atoms: Vec<QueryAtom>,
}
fn query_atoms(query: &str) -> impl Iterator<Item = &str> + '_ {
let mut saw_backslash = false;
query.split(move |c| {
saw_backslash = match c {
' ' if !saw_backslash => return true,
'\\' => true,
_ => false,
};
false
})
}
impl FuzzyQuery {
pub fn refine(&self, query: &str, old_query: &str) -> (FuzzyQuery, bool) {
// TODO: we could be a lot smarter about this
let new_query = Self::new(query);
let mut is_refinement = query.starts_with(old_query);
// if the last atom is an inverse atom adding more text to it
// will actually increase the number of matches and we can not refine
// the matches.
if is_refinement && !self.query_atoms.is_empty() {
let last_idx = self.query_atoms.len() - 1;
if self.query_atoms[last_idx].inverse
&& self.query_atoms[last_idx].atom != new_query.query_atoms[last_idx].atom
{
is_refinement = false;
}
}
(new_query, is_refinement)
}
pub fn new(query: &str) -> FuzzyQuery {
let mut first_fuzzy_query = None;
let query_atoms = query_atoms(query)
.filter_map(|atom| {
let atom = QueryAtom::new(atom)?;
if atom.kind == QueryAtomKind::Fuzzy && first_fuzzy_query.is_none() {
first_fuzzy_query = Some(atom.atom);
None
} else {
Some(atom)
}
})
.collect();
FuzzyQuery {
first_fuzzy_atom: first_fuzzy_query,
query_atoms,
}
}
pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option<i64> {
// use the rank of the first fuzzzy query for the rank, because merging ranks is not really possible
// this behaviour matches fzf and skim
let score = self
.first_fuzzy_atom
.as_ref()
.map_or(Some(0), |atom| matcher.fuzzy_match(item, atom))?;
if self
.query_atoms
.iter()
.any(|atom| !atom.matches(matcher, item))
{
return None;
}
Some(score)
}
pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec<usize>)> {
let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else(
|| Some((0, Vec::new())),
|atom| matcher.fuzzy_indices(item, atom),
)?;
// fast path for the common case of just a single atom
if self.query_atoms.is_empty() {
return Some((score, indices));
}
for atom in &self.query_atoms {
if !atom.indices(matcher, item, &mut indices) {
return None;
}
}
// deadup and remove duplicate matches
indices.sort_unstable();
indices.dedup();
Some((score, indices))
}
}

@ -1,47 +0,0 @@
use crate::ui::fuzzy_match::FuzzyQuery;
use crate::ui::fuzzy_match::Matcher;
fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec<String> {
let query = FuzzyQuery::new(query);
let matcher = Matcher::default();
items
.iter()
.filter_map(|item| {
let (_, indices) = query.fuzzy_indices(item, &matcher)?;
let matched_string = indices
.iter()
.map(|&pos| item.chars().nth(pos).unwrap())
.collect();
Some(matched_string)
})
.collect()
}
#[test]
fn match_single_value() {
let matches = run_test("foo", &["foobar", "foo", "bar"]);
assert_eq!(matches, &["foo", "foo"])
}
#[test]
fn match_multiple_values() {
let matches = run_test(
"foo bar",
&["foo bar", "foo bar", "bar foo", "bar", "foo"],
);
assert_eq!(matches, &["foobar", "foobar", "barfoo"])
}
#[test]
fn space_escape() {
let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["foo bar"])
}
#[test]
fn trim() {
let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["barfoo", "foobar", "foobar"]);
let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]);
assert_eq!(matches, &["bar foo"])
}

@ -62,7 +62,7 @@ impl Component for SignatureHelp {
});
let sig_text = crate::ui::markdown::highlighted_code_block(
self.signature.clone(),
&self.signature,
&self.language,
Some(&cx.editor.theme),
Arc::clone(&self.config_loader),
@ -109,7 +109,7 @@ impl Component for SignatureHelp {
let max_text_width = (viewport.0 - PADDING).min(120);
let signature_text = crate::ui::markdown::highlighted_code_block(
self.signature.clone(),
&self.signature,
&self.language,
None,
Arc::clone(&self.config_loader),

@ -9,15 +9,15 @@ use std::sync::Arc;
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use helix_core::{
syntax::{self, HighlightEvent, Syntax},
Rope,
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
RopeSlice,
};
use helix_view::{
graphics::{Margin, Rect, Style},
Theme,
};
fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
fn styled_multiline_text<'a>(text: &str, style: Style) -> Text<'a> {
let spans: Vec<_> = text
.lines()
.map(|line| Span::styled(line.to_string(), style))
@ -27,7 +27,7 @@ fn styled_multiline_text<'a>(text: String, style: Style) -> Text<'a> {
}
pub fn highlighted_code_block<'a>(
text: String,
text: &str,
language: &str,
theme: Option<&Theme>,
config_loader: Arc<syntax::Loader>,
@ -45,11 +45,13 @@ pub fn highlighted_code_block<'a>(
None => return styled_multiline_text(text, code_style),
};
let rope = Rope::from(text.as_ref());
let ropeslice = RopeSlice::from(text);
let syntax = config_loader
.language_configuration_for_injection_string(language)
.language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
language.into(),
))
.and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config, Arc::clone(&config_loader)));
.and_then(|config| Syntax::new(ropeslice, config, Arc::clone(&config_loader)));
let syntax = match syntax {
Some(s) => s,
@ -57,7 +59,7 @@ pub fn highlighted_code_block<'a>(
};
let highlight_iter = syntax
.highlight_iter(rope.slice(..), None, None)
.highlight_iter(ropeslice, None, None)
.map(|e| e.unwrap());
let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> =
if let Some(spans) = additional_highlight_spans {
@ -265,7 +267,7 @@ impl Markdown {
CodeBlockKind::Indented => "",
};
let tui_text = highlighted_code_block(
text.to_string(),
&text,
language,
theme,
Arc::clone(&self.config_loader),

@ -1,22 +1,22 @@
use std::{borrow::Cow, path::PathBuf};
use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
use crate::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
};
use helix_core::fuzzy::MATCHER;
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
use nucleo::{Config, Utf32Str};
use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_view::{graphics::Rect, Editor};
use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
use tui::layout::Constraint;
pub trait Item {
pub trait Item: Sync + Send + 'static {
/// Additional editor state that is used for label calculation.
type Data;
type Data: Sync + Send + 'static;
fn format(&self, data: &Self::Data) -> Row;
@ -51,9 +51,8 @@ pub struct Menu<T: Item> {
cursor: Option<usize>,
matcher: Box<Matcher>,
/// (index, score)
matches: Vec<(usize, i64)>,
matches: Vec<(u32, u32)>,
widths: Vec<Constraint>,
@ -75,11 +74,10 @@ impl<T: Item> Menu<T> {
editor_data: <T as Item>::Data,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let matches = (0..options.len()).map(|i| (i, 0)).collect();
let matches = (0..options.len() as u32).map(|i| (i, 0)).collect();
Self {
options,
editor_data,
matcher: Box::new(Matcher::default().ignore_case()),
matches,
cursor: None,
widths: Vec::new(),
@ -94,20 +92,19 @@ impl<T: Item> Menu<T> {
pub fn score(&mut self, pattern: &str) {
// reuse the matches allocation
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
let text = option.filter_text(&self.editor_data);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
// Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority
self.matches.sort_by_key(|(_, score)| -score);
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
let mut buf = Vec::new();
let matches = self.options.iter().enumerate().filter_map(|(i, option)| {
let text = option.filter_text(&self.editor_data);
pattern
.score(Utf32Str::new(&text, &mut buf), &mut matcher)
.map(|score| (i as u32, score as u32))
});
self.matches.extend(matches);
self.matches
.sort_unstable_by_key(|&(i, score)| (Reverse(score), i));
// reset cursor position
self.cursor = None;
@ -201,7 +198,7 @@ impl<T: Item> Menu<T> {
self.cursor.and_then(|cursor| {
self.matches
.get(cursor)
.map(|(index, _score)| &self.options[*index])
.map(|(index, _score)| &self.options[*index as usize])
})
}
@ -209,7 +206,7 @@ impl<T: Item> Menu<T> {
self.cursor.and_then(|cursor| {
self.matches
.get(cursor)
.map(|(index, _score)| &mut self.options[*index])
.map(|(index, _score)| &mut self.options[*index as usize])
})
}
@ -247,6 +244,21 @@ impl<T: Item + 'static> Component for Menu<T> {
compositor.pop();
}));
// Ignore tab key when supertab is turned on in order not to interfere
// with it. (Is there a better way to do this?)
if (event == key!(Tab) || event == shift!(Tab))
&& cx.editor.config().auto_completion
&& matches!(
cx.editor.config().smart_tab,
Some(SmartTabConfig {
enable: true,
supersede_menu: true,
})
)
{
return EventResult::Ignored(None);
}
match event {
// esc or ctrl-c aborts the completion and closes the menu
key!(Esc) | ctrl!('c') => {
@ -317,7 +329,7 @@ impl<T: Item + 'static> Component for Menu<T> {
.iter()
.map(|(index, _score)| {
// (index, self.options.get(*index).unwrap()) // get_unchecked
&self.options[*index] // get_unchecked
&self.options[*index as usize] // get_unchecked
})
.collect();

@ -1,13 +1,12 @@
mod completion;
mod document;
pub(crate) mod editor;
mod fuzzy_match;
mod info;
pub mod lsp;
mod markdown;
pub mod menu;
pub mod overlay;
mod picker;
pub mod picker;
pub mod popup;
mod prompt;
mod spinner;
@ -17,11 +16,11 @@ mod text;
use crate::compositor::{Component, Compositor};
use crate::filter_picker_entry;
use crate::job::{self, Callback};
pub use completion::Completion;
pub use completion::{Completion, CompletionItem};
pub use editor::EditorView;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, FilePicker, Picker};
pub use picker::{DynamicPicker, FileLocation, Picker};
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
@ -64,7 +63,7 @@ pub fn regex_prompt(
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static,
fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static,
) {
let (view, doc) = current!(cx.editor);
let doc_id = view.doc;
@ -111,7 +110,7 @@ pub fn regex_prompt(
view.jumps.push((doc_id, snapshot.clone()));
}
fun(cx.editor, regex, event);
fun(cx, regex, event);
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);
@ -142,23 +141,21 @@ pub fn regex_prompt(
};
cx.jobs.callback(callback);
} else {
// Update
// TODO: mark command line as error
}
}
}
}
}
},
);
)
.with_language("regex", std::sync::Arc::clone(&cx.editor.syn_loader));
// Calculate initial completion
prompt.recalculate_completion(cx.editor);
// prompt
cx.push_layer(Box::new(prompt));
}
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant;
@ -176,6 +173,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
.git_ignore(config.file_picker.git_ignore)
.git_global(config.file_picker.git_global)
.git_exclude(config.file_picker.git_exclude)
.sort_by_file_name(|name1, name2| name1.cmp(name2))
.max_depth(config.file_picker.max_depth)
.filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks));
@ -192,58 +190,60 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePi
.build()
.expect("failed to build excluded_types");
walk_builder.types(excluded_types);
// We want files along with their modification date for sorting
let files = walk_builder.build().filter_map(|entry| {
let mut files = walk_builder.build().filter_map(|entry| {
let entry = entry.ok()?;
// This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir
if entry.file_type()?.is_file() {
Some(entry.into_path())
} else {
None
if !entry.file_type()?.is_file() {
return None;
}
Some(entry.into_path())
});
// Cap the number of files if we aren't in a git project, preventing
// hangs when using the picker in your home directory
let mut files: Vec<PathBuf> = if root.join(".git").exists() {
files.collect()
} else {
// const MAX: usize = 8192;
const MAX: usize = 100_000;
files.take(MAX).collect()
};
files.sort();
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
FilePicker::new(
files,
root,
move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
format!("{}", err)
} else {
format!("unable to open \"{}\"", path.display())
};
cx.editor.set_error(err);
let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
format!("{}", err)
} else {
format!("unable to open \"{}\"", path.display())
};
cx.editor.set_error(err);
}
})
.with_preview(|_editor, path| Some((path.clone().into(), None)));
let injector = picker.injector();
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
let mut hit_timeout = false;
for file in &mut files {
if injector.push(file).is_err() {
break;
}
if std::time::Instant::now() >= timeout {
hit_timeout = true;
break;
}
}
if hit_timeout {
std::thread::spawn(move || {
for file in files {
if injector.push(file).is_err() {
break;
}
}
},
|_editor, path| Some((path.clone().into(), None)),
)
});
}
picker
}
pub mod completers {
use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_core::fuzzy::fuzzy_match;
use helix_core::syntax::LanguageServerFeature;
use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme;
use helix_view::{editor::Config, Editor};
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::cmp::Reverse;
pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
@ -252,31 +252,16 @@ pub mod completers {
}
pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
let mut names: Vec<_> = editor
.documents
.values()
.map(|doc| {
let name = doc
.relative_path()
.map(|p| p.display().to_string())
.unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME));
((0..), Cow::from(name))
})
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
.into_iter()
.filter_map(|(_range, name)| {
matcher.fuzzy_match(&name, input).map(|score| (name, score))
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
let names = editor.documents.values().map(|doc| {
doc.relative_path()
.map(|p| p.display().to_string().into())
.unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME))
});
names
fuzzy_match(input, names, true)
.into_iter()
.map(|(name, _)| ((0..), name))
.collect()
}
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@ -289,26 +274,10 @@ pub mod completers {
names.sort();
names.dedup();
let mut names: Vec<_> = names
.into_iter()
.map(|name| ((0..), Cow::from(name)))
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
fuzzy_match(input, names, false)
.into_iter()
.filter_map(|(_range, name)| {
matcher.fuzzy_match(&name, input).map(|score| (name, score))
})
.collect();
matches.sort_unstable_by(|(name1, score1), (name2, score2)| {
(Reverse(*score1), name1).cmp(&(Reverse(*score2), name2))
});
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names
.map(|(name, _)| ((0..), name.into()))
.collect()
}
/// Recursive function to get all keys from this value and add them to vec
@ -335,22 +304,22 @@ pub mod completers {
keys
});
let matcher = Matcher::default();
let mut matches: Vec<_> = KEYS
.iter()
.filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score)))
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
matches
fuzzy_match(input, &*KEYS, false)
.into_iter()
.map(|(name, _)| ((0..), name.into()))
.collect()
}
pub fn filename(editor: &Editor, input: &str) -> Vec<Completion> {
filename_impl(editor, input, |entry| {
filename_with_git_ignore(editor, input, true)
}
pub fn filename_with_git_ignore(
editor: &Editor,
input: &str,
git_ignore: bool,
) -> Vec<Completion> {
filename_impl(editor, input, git_ignore, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
if is_dir {
@ -362,8 +331,6 @@ pub mod completers {
}
pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default();
let text: String = "text".into();
let language_ids = editor
@ -372,65 +339,36 @@ pub mod completers {
.map(|config| &config.language_id)
.chain(std::iter::once(&text));
let mut matches: Vec<_> = language_ids
.filter_map(|language_id| {
matcher
.fuzzy_match(language_id, input)
.map(|score| (language_id, score))
})
.collect();
matches.sort_unstable_by(|(language1, score1), (language2, score2)| {
(Reverse(*score1), language1).cmp(&(Reverse(*score2), language2))
});
matches
fuzzy_match(input, language_ids, false)
.into_iter()
.map(|(language, _score)| ((0..), language.clone().into()))
.map(|(name, _)| ((0..), name.to_owned().into()))
.collect()
}
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
let matcher = Matcher::default();
let (_, doc) = current_ref!(editor);
let language_server = match doc.language_server() {
Some(language_server) => language_server,
None => {
return vec![];
}
let Some(options) = doc!(editor)
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
.find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
else {
return vec![];
};
let options = match &language_server.capabilities().execute_command_provider {
Some(options) => options,
None => {
return vec![];
}
};
let mut matches: Vec<_> = options
.commands
.iter()
.filter_map(|command| {
matcher
.fuzzy_match(command, input)
.map(|score| (command, score))
})
.collect();
matches.sort_unstable_by(|(command1, score1), (command2, score2)| {
(Reverse(*score1), command1).cmp(&(Reverse(*score2), command2))
});
matches
fuzzy_match(input, &options.commands, false)
.into_iter()
.map(|(command, _score)| ((0..), command.clone().into()))
.map(|(name, _)| ((0..), name.to_owned().into()))
.collect()
}
pub fn directory(editor: &Editor, input: &str) -> Vec<Completion> {
filename_impl(editor, input, |entry| {
directory_with_git_ignore(editor, input, true)
}
pub fn directory_with_git_ignore(
editor: &Editor,
input: &str,
git_ignore: bool,
) -> Vec<Completion> {
filename_impl(editor, input, git_ignore, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
if is_dir {
@ -453,7 +391,12 @@ pub mod completers {
}
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
fn filename_impl<F>(_editor: &Editor, input: &str, filter_fn: F) -> Vec<Completion>
fn filename_impl<F>(
_editor: &Editor,
input: &str,
git_ignore: bool,
filter_fn: F,
) -> Vec<Completion>
where
F: Fn(&ignore::DirEntry) -> FileMatch,
{
@ -484,7 +427,7 @@ pub mod completers {
match path.parent() {
Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
// Path::new("h")'s parent is Some("")...
_ => std::env::current_dir().expect("couldn't determine current directory"),
_ => helix_loader::current_working_dir(),
}
};
@ -493,9 +436,10 @@ pub mod completers {
let end = input.len()..;
let mut files: Vec<_> = WalkBuilder::new(&dir)
let files = WalkBuilder::new(&dir)
.hidden(false)
.follow_links(false) // We're scanning over depth 1
.git_ignore(git_ignore)
.max_depth(Some(1))
.build()
.filter_map(|file| {
@ -524,43 +468,25 @@ pub mod completers {
path.push("");
}
let path = path.to_str()?.to_owned();
Some((end.clone(), Cow::from(path)))
let path = path.into_os_string().into_string().ok()?;
Some(Cow::from(path))
})
}) // TODO: unwrap or skip
.filter(|(_, path)| !path.is_empty()) // TODO
.collect();
.filter(|path| !path.is_empty());
// if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name {
let matcher = Matcher::default();
// inefficient, but we need to calculate the scores, filter out None, then sort.
let mut matches: Vec<_> = files
.into_iter()
.filter_map(|(_range, file)| {
matcher
.fuzzy_match(&file, &file_name)
.map(|score| (file, score))
})
.collect();
let range = (input.len().saturating_sub(file_name.len()))..;
matches.sort_unstable_by(|(file1, score1), (file2, score2)| {
(Reverse(*score1), file1).cmp(&(Reverse(*score2), file2))
});
files = matches
fuzzy_match(&file_name, files, true)
.into_iter()
.map(|(file, _)| (range.clone(), file))
.collect();
.map(|(name, _)| (range.clone(), name))
.collect()
// TODO: complete to longest common match
} else {
let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect();
files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2));
files
}
files
}
}

File diff suppressed because it is too large Load Diff

@ -1,7 +1,9 @@
use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui};
use helix_core::syntax;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
use std::sync::Arc;
use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Widget};
@ -32,6 +34,7 @@ pub struct Prompt {
callback_fn: CallbackFn,
pub doc_fn: DocFn,
next_char_handler: Option<PromptCharHandler>,
language: Option<(&'static str, Arc<syntax::Loader>)>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
@ -83,6 +86,7 @@ impl Prompt {
callback_fn: Box::new(callback_fn),
doc_fn: Box::new(|_| None),
next_char_handler: None,
language: None,
}
}
@ -94,6 +98,11 @@ impl Prompt {
self
}
pub fn with_language(mut self, language: &'static str, loader: Arc<syntax::Loader>) -> Self {
self.language = Some((language, loader));
self
}
pub fn line(&self) -> &String {
&self.line
}
@ -297,8 +306,8 @@ impl Prompt {
direction: CompletionDirection,
) {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
let values = match cx.editor.registers.read(register) {
Some(values) if !values.is_empty() => values,
let mut values = match cx.editor.registers.read(register, cx.editor) {
Some(values) if values.len() > 0 => values.rev(),
_ => return,
};
@ -306,13 +315,16 @@ impl Prompt {
let index = match direction {
CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
CompletionDirection::Backward => {
self.history_pos.unwrap_or(values.len()).saturating_sub(1)
}
CompletionDirection::Backward => self
.history_pos
.unwrap_or_else(|| values.len())
.saturating_sub(1),
}
.min(end);
self.line = values[index].clone();
self.line = values.nth(index).unwrap().to_string();
// Appease the borrow checker.
drop(values);
self.history_pos = Some(index);
@ -356,6 +368,7 @@ impl Prompt {
let completion_color = theme.get("ui.menu");
let selected_color = theme.get("ui.menu.selected");
let suggestion_color = theme.get("ui.text.inactive");
let background = theme.get("ui.background");
// completion
let max_len = self
@ -451,33 +464,32 @@ impl Prompt {
}
let line = area.height - 1;
surface.clear_with(area.clip_top(line), background);
// render buffer text
surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
let (input, is_suggestion): (Cow<str>, bool) = if self.line.is_empty() {
// latest value in the register list
match self
let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
if self.line.is_empty() {
// Show the most recently entered value as a suggestion.
if let Some(suggestion) = self
.history_register
.and_then(|reg| cx.editor.registers.last(reg))
.map(|entry| entry.into())
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
{
Some(value) => (value, true),
None => (Cow::from(""), false),
surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
}
} else if let Some((language, loader)) = self.language.as_ref() {
let mut text: ui::text::Text = crate::ui::markdown::highlighted_code_block(
&self.line,
language,
Some(&cx.editor.theme),
loader.clone(),
None,
)
.into();
text.render(line_area, surface, cx);
} else {
(self.line.as_str().into(), false)
};
surface.set_string(
area.x + self.prompt.len() as u16,
area.y + line,
&input,
if is_suggestion {
suggestion_color
} else {
prompt_color
},
);
surface.set_string(line_area.x, line_area.y, self.line.clone(), prompt_color);
}
}
}
@ -511,11 +523,21 @@ impl Component for Prompt {
ctrl!('e') | key!(End) => self.move_end(),
ctrl!('a') | key!(Home) => self.move_start(),
ctrl!('w') | alt!(Backspace) | ctrl!(Backspace) => {
self.delete_word_backwards(cx.editor)
self.delete_word_backwards(cx.editor);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
alt!('d') | alt!(Delete) | ctrl!(Delete) => {
self.delete_word_forwards(cx.editor);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
ctrl!('k') => {
self.kill_to_end_of_line(cx.editor);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
ctrl!('u') => {
self.kill_to_start_of_line(cx.editor);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
alt!('d') | alt!(Delete) | ctrl!(Delete) => self.delete_word_forwards(cx.editor),
ctrl!('k') => self.kill_to_end_of_line(cx.editor),
ctrl!('u') => self.kill_to_start_of_line(cx.editor),
ctrl!('h') | key!(Backspace) | shift!(Backspace) => {
self.delete_char_backwards(cx.editor);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
@ -548,25 +570,29 @@ impl Component for Prompt {
} else {
let last_item = self
.history_register
.and_then(|reg| cx.editor.registers.last(reg).cloned())
.map(|entry| entry.into())
.unwrap_or_else(|| Cow::from(""));
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
.map(|entry| entry.to_string())
.unwrap_or_else(|| String::from(""));
// handle executing with last command in history if nothing entered
let input: Cow<str> = if self.line.is_empty() {
last_item
let input = if self.line.is_empty() {
&last_item
} else {
if last_item != self.line {
// store in history
if let Some(register) = self.history_register {
cx.editor.registers.push(register, self.line.clone());
if let Err(err) =
cx.editor.registers.push(register, self.line.clone())
{
cx.editor.set_error(err.to_string());
}
};
}
self.line.as_str().into()
&self.line
};
(self.callback_fn)(cx, &input, PromptEvent::Validate);
(self.callback_fn)(cx, input, PromptEvent::Validate);
return close_fn;
}
@ -598,25 +624,16 @@ impl Component for Prompt {
self.completion = cx
.editor
.registers
.inner()
.iter()
.map(|(ch, reg)| {
let content = reg
.read()
.get(0)
.and_then(|s| s.lines().next().to_owned())
.unwrap_or_default();
(0.., format!("{} {}", ch, &content).into())
})
.iter_preview()
.map(|(ch, preview)| (0.., format!("{} {}", ch, &preview).into()))
.collect();
self.next_char_handler = Some(Box::new(|prompt, c, context| {
prompt.insert_str(
context
&context
.editor
.registers
.read(c)
.and_then(|r| r.first())
.map_or("", |r| r.as_str()),
.first(c, context.editor)
.unwrap_or_default(),
context.editor,
);
}));

@ -145,6 +145,7 @@ where
helix_view::editor::StatusLineElement::FileModificationIndicator => {
render_file_modification_indicator
}
helix_view::editor::StatusLineElement::ReadOnlyIndicator => render_read_only_indicator,
helix_view::editor::StatusLineElement::FileEncoding => render_file_encoding,
helix_view::editor::StatusLineElement::FileLineEnding => render_file_line_ending,
helix_view::editor::StatusLineElement::FileType => render_file_type,
@ -160,6 +161,7 @@ where
helix_view::editor::StatusLineElement::Separator => render_separator,
helix_view::editor::StatusLineElement::Spacer => render_spacer,
helix_view::editor::StatusLineElement::VersionControl => render_version_control,
helix_view::editor::StatusLineElement::Register => render_register,
}
}
@ -197,15 +199,15 @@ where
);
}
// TODO think about handling multiple language servers
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let language_server = context.doc.language_servers().next();
write(
context,
context
.doc
.language_server()
language_server
.and_then(|srv| {
context
.spinners
@ -225,8 +227,7 @@ where
{
let (warnings, errors) = context
.doc
.diagnostics()
.iter()
.shown_diagnostics()
.fold((0, 0), |mut counts, diag| {
use helix_core::diagnostic::Severity;
match diag.severity {
@ -266,7 +267,7 @@ where
.diagnostics
.values()
.flatten()
.fold((0, 0), |mut counts, diag| {
.fold((0, 0), |mut counts, (diag, _)| {
match diag.severity {
Some(DiagnosticSeverity::WARNING) => counts.0 += 1,
Some(DiagnosticSeverity::ERROR) | None => counts.1 += 1,
@ -276,7 +277,7 @@ where
});
if warnings > 0 || errors > 0 {
write(context, format!(" {} ", "W"), None);
write(context, " W ".into(), None);
}
if warnings > 0 {
@ -442,6 +443,19 @@ where
write(context, title, None);
}
fn render_read_only_indicator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = if context.doc.readonly {
" [readonly] "
} else {
""
}
.to_string();
write(context, title, None);
}
fn render_file_base_name<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
@ -490,3 +504,12 @@ where
write(context, head, None);
}
fn render_register<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
if let Some(reg) = context.editor.selected_register {
write(context, format!(" reg={} ", reg), None)
}
}

@ -18,6 +18,7 @@ mod test {
mod auto_indent;
mod auto_pairs;
mod commands;
mod languages;
mod movement;
mod prompt;
mod splits;

@ -2,7 +2,7 @@ use helix_core::{auto_pairs::DEFAULT_PAIRS, hashmap};
use super::*;
const LINE_END: &str = helix_core::DEFAULT_LINE_ENDING.as_str();
const LINE_END: &str = helix_core::NATIVE_LINE_ENDING.as_str();
fn differing_pairs() -> impl Iterator<Item = &'static (char, char)> {
DEFAULT_PAIRS.iter().filter(|(open, close)| open != close)

@ -2,6 +2,7 @@ use helix_term::application::Application;
use super::*;
mod movement;
mod write;
#[tokio::test(flavor = "multi_thread")]
@ -12,15 +13,13 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
#[lo|]#rem
ipsum
dolor
"})
.as_str(),
"}),
"CC",
platform_line(indoc! {"\
#(lo|)#rem
#(ip|)#sum
#[do|]#lor
"})
.as_str(),
"}),
))
.await?;
@ -30,15 +29,13 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
#[|lo]#rem
ipsum
dolor
"})
.as_str(),
"}),
"CC",
platform_line(indoc! {"\
#(|lo)#rem
#(|ip)#sum
#[|do]#lor
"})
.as_str(),
"}),
))
.await?;
@ -47,14 +44,12 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
platform_line(indoc! {"\
test
#[testitem|]#
"})
.as_str(),
"}),
"<A-C>",
platform_line(indoc! {"\
test
#[testitem|]#
"})
.as_str(),
"}),
))
.await?;
@ -63,14 +58,12 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
platform_line(indoc! {"\
test
#[test|]#
"})
.as_str(),
"}),
"<A-C>",
platform_line(indoc! {"\
#[test|]#
#(test|)#
"})
.as_str(),
"}),
))
.await?;
@ -79,14 +72,12 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
platform_line(indoc! {"\
#[testitem|]#
test
"})
.as_str(),
"}),
"C",
platform_line(indoc! {"\
#[testitem|]#
test
"})
.as_str(),
"}),
))
.await?;
@ -95,14 +86,12 @@ async fn test_selection_duplication() -> anyhow::Result<()> {
platform_line(indoc! {"\
#[test|]#
test
"})
.as_str(),
"}),
"C",
platform_line(indoc! {"\
#(test|)#
#[test|]#
"})
.as_str(),
"}),
))
.await?;
Ok(())
@ -174,15 +163,13 @@ async fn test_multi_selection_paste() -> anyhow::Result<()> {
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"})
.as_str(),
"}),
"yp",
platform_line(indoc! {"\
lorem#[|lorem]#
ipsum#(|ipsum)#
dolor#(|dolor)#
"})
.as_str(),
"}),
))
.await?;
@ -197,8 +184,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"})
.as_str(),
"}),
"|echo foo<ret>",
platform_line(indoc! {"\
#[|foo\n]#
@ -207,8 +193,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
#(|foo\n)#
"})
.as_str(),
"}),
))
.await?;
@ -218,8 +203,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"})
.as_str(),
"}),
"!echo foo<ret>",
platform_line(indoc! {"\
#[|foo\n]#
@ -228,8 +212,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
ipsum
#(|foo\n)#
dolor
"})
.as_str(),
"}),
))
.await?;
@ -239,8 +222,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"})
.as_str(),
"}),
"<A-!>echo foo<ret>",
platform_line(indoc! {"\
lorem#[|foo\n]#
@ -249,8 +231,7 @@ async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
dolor#(|foo\n)#
"})
.as_str(),
"}),
))
.await?;
@ -294,16 +275,14 @@ async fn test_extend_line() -> anyhow::Result<()> {
ipsum
dolor
"})
.as_str(),
"}),
"x2x",
platform_line(indoc! {"\
#[lorem
ipsum
dolor\n|]#
"})
.as_str(),
"}),
))
.await?;
@ -313,15 +292,13 @@ async fn test_extend_line() -> anyhow::Result<()> {
#[l|]#orem
ipsum
"})
.as_str(),
"}),
"2x",
platform_line(indoc! {"\
#[lorem
ipsum\n|]#
"})
.as_str(),
"}),
))
.await?;
@ -385,3 +362,121 @@ async fn test_character_info() -> anyhow::Result<()> {
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_char_backward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test((
platform_line("#(x|)# #[x|]#"),
"c<space><backspace><esc>",
platform_line("#[\n|]#"),
))
.await?;
test((
platform_line("#( |)##( |)#a#( |)#axx#[x|]#a"),
"li<backspace><esc>",
platform_line("#(a|)##(|a)#xx#[|a]#"),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_word_backward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test((
platform_line("fo#[o|]#ba#(r|)#"),
"a<C-w><esc>",
platform_line("#[\n|]#"),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_word_forward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test((
platform_line("fo#[o|]#b#(|ar)#"),
"i<A-d><esc>",
platform_line("fo#[\n|]#"),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_char_forward() -> anyhow::Result<()> {
test((
platform_line(indoc! {"\
#[abc|]#def
#(abc|)#ef
#(abc|)#f
#(abc|)#
"}),
"a<del><esc>",
platform_line(indoc! {"\
#[abc|]#ef
#(abc|)#f
#(abc|)#
#(abc|)#
"}),
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_insert_with_indent() -> anyhow::Result<()> {
const INPUT: &str = "\
#[f|]#n foo() {
if let Some(_) = None {
}
\x20
}
fn bar() {
}";
// insert_at_line_start
test((
INPUT,
":lang rust<ret>%<A-s>I",
"\
#[f|]#n foo() {
#(i|)#f let Some(_) = None {
#(\n|)#\
\x20 #(}|)#
#(\x20|)#
#(}|)#
#(\n|)#\
#(f|)#n bar() {
#(\n|)#\
#(}|)#",
))
.await?;
// insert_at_line_end
test((
INPUT,
":lang rust<ret>%<A-s>A",
"\
fn foo() {#[\n|]#\
\x20 if let Some(_) = None {#(\n|)#\
\x20 #(\n|)#\
\x20 }#(\n|)#\
\x20#(\n|)#\
}#(\n|)#\
#(\n|)#\
fn bar() {#(\n|)#\
\x20 #(\n|)#\
}#(|)#",
))
.await?;
Ok(())
}

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

Loading…
Cancel
Save