Compare commits

...

505 Commits

Author SHA1 Message Date
Michael Davis
5ea92d1713 Use syntax symbol pickers for Erlang
Neither language server robustly supports workspace symbol search.
`erlang-ls`'s symbol picker takes a long time to open successfully on
boot. `elp`'s is faster but not faster than the tags query.
2025-07-14 19:52:38 -04:00
Michael Davis
ece331855e Document tags.scm queries, commands and language support 2025-07-14 19:52:38 -04:00
Michael Davis
4d49513c9e Add syntax symbol pickers based on tags.scm queries 2025-07-14 19:52:38 -04:00
Michael Davis
b24024f93f Add initial tags.scm queries
Co-authored-by: cgahr <26804763+cgahr@users.noreply.github.com>
Co-authored-by: eh <correia.eh@gmail.com>
2025-07-14 19:52:38 -04:00
Michael Davis
6fd1efd1c2 Gracefully handle highlighter bugs in the markdown component
Since tree-house is young and we've seen a few bugs that make it go
backwards, we should handle this case gracefully and just give up on
syntax highlighting with an error log.
2025-07-13 13:12:14 -04:00
Michael Davis
86f10ae24c Add a language to fix Rust highlights in format-args macros
This is a bit hacky. Injections cannot stack on each other like
highlights because layers can have their own injections. So this new
language `rust-format-args-macro` emulates that. It unconditionally
injects `rust-format-args` into all strings. Rust injects this new
language into known format-args macros like `println!`.

The downside is that this can cause false-positive highlights within
these macros for strings which happen to contain format-args syntax

    println!("Hello, {}!", "{}");
    //               ^ format args syntax
    //                      ^ not format args syntax, but highlighted
    //                        as if it were :(

This false-positive case is expected to be rare.

Injecting this fake language fixes regular non-string highlights in
macro invocations: macro invocations need to inject the entire token
tree and use `injection.include-children` for proper highlighting.
2025-07-13 12:01:18 -04:00
Michael Davis
d2f37b1559 deps: Update tree-house-bindings to v0.2.1 2025-07-12 18:21:28 -04:00
Nik Revenco
e844a4365d fix: bitwise representation for RGB highlight (#13188)
Co-authored-by: Nik Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-07-12 11:11:31 -04:00
CalebLarsen
ca7479ca88 Add docs to helix-stdx, helix-tui, helix-vcs (#13765) 2025-07-12 09:48:00 -04:00
Matthew Toohey
7e1fbb05fd feat: add :buffer-close-others --skip-visible flag (#5393) 2025-07-11 11:17:50 -05:00
Michael Davis
2f560914fb Add a '--no-format' flag for :write commands 2025-07-11 12:00:41 -04:00
spentbliss
636cbe58e3 feat(theme): add doom-one theme (#13933)
Co-authored-by: spentbliss <spentbliss@users.noreply.github.com>
2025-07-11 10:34:45 -04:00
Maikel Martens
43187f2ed3 Add Django language support (#13935) 2025-07-11 10:33:44 -04:00
Michael Davis
532f241287 Allow symlinks in shell program completions
Co-authored-by: thort <thort@compass-vm>
2025-07-11 10:30:58 -04:00
Gabriel Lopes Rodrigues
ba04f53830 languages: consider compose.yaml/.yml as docker compose language (#13930) 2025-07-10 20:14:45 -05:00
StratusFearMe21
242353b2ba Add ability to configure atomic saving (#13656)
Co-authored-by: Łukasz Langa <lukasz@langa.pl>
2025-07-10 19:12:59 -04:00
belowm
de898460b8 Allow :move command to accept directories as target (#13922)
Co-authored-by: Martin Below <martin@below.cologne>
2025-07-10 14:28:53 -05:00
Kristoffer Plagborg Bak Sørensen
8e0f326ebb languages: create dedicated language for json-ld (#13925) 2025-07-10 08:49:33 -05:00
connnnal
16d06643a4 queries: Odin or_break+or_continue keywords, struct indents
from https://github.com/tree-sitter-grammars/tree-sitter-odin/pull/25
2025-07-08 19:05:48 -04:00
connnnal
9447a9cc93 chore: Bump Odin grammar 2025-07-08 19:05:48 -04:00
Jonas Köhnen
febc3d03b3 queries/gomod: add "tool", "toolchain" to keywords (#13913) 2025-07-08 08:19:11 -05:00
dependabot[bot]
06047808eb build(deps): bump the rust-dependencies group with 2 updates (#13909)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-08 08:18:37 -05:00
Andrew Davis
02fe437622 Fix off by one error when opening multiple new lines with CRLF line endings (#13905) 2025-07-07 17:41:16 -05:00
Daniel Fortes
e88e48f41c Rose pine theme: improve contrast of selected menu item (#13908) 2025-07-07 16:09:16 -05:00
Val Packett
fc53af9f4e Add systemd-lsp and dts-lsp (#13907) 2025-07-07 15:55:19 -05:00
Kristoffer Plagborg Bak Sørensen
3e5bb392fa languages: add comment tokens for DTD language (#13904) 2025-07-07 14:02:26 -04:00
Remo Senekowitsch
479c3b5584 Add highlighting for git notes editmsg (#13885) 2025-07-05 10:23:53 -04:00
Kristoffer Plagborg Bak Sørensen
9789b27461 languages: add Java .properties file support (#13874) 2025-07-04 09:17:22 -05:00
Kristoffer Plagborg Bak Sørensen
6c6607ef62 queries: add textobjects for qml (#13855) 2025-07-03 22:19:22 -04:00
CalebLarsen
bcb6c20a84 queries: Add locals.scm for C. Improve C parameter highlights (#13876) 2025-07-02 17:41:28 -05:00
CalebLarsen
f7ab5ec4a1 queries: Update highlights for Odin (#13877) 2025-07-02 17:38:05 -05:00
Tino
6a090471a8 Fix panic in goto_word when editor.jump-label-alphabet is empty (#13863) 2025-07-01 09:06:54 -05:00
Michael Davis
6081a5df81 queries: Fix precedence of Fennel highlights 2025-07-01 09:40:40 -04:00
dependabot[bot]
0043c16506 build(deps): bump indexmap in the rust-dependencies group (#13872)
Bumps the rust-dependencies group with 1 update: [indexmap](https://github.com/indexmap-rs/indexmap).


Updates `indexmap` from 2.9.0 to 2.10.0
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.9.0...2.10.0)

---
updated-dependencies:
- dependency-name: indexmap
  dependency-version: 2.10.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-07-01 17:06:54 +09:00
David Crespo
e5f9937c1d docs: escape pipe in typeable command name (#13869) 2025-06-30 11:45:47 -04:00
Kristoffer Plagborg Bak Sørensen
91dff9393d languages: add Caddyfile support (#13859)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-06-30 09:45:22 -05:00
Sean Barag
0ca12250bc helix-view: expand primary selection line range in shell commands (#13840) 2025-06-30 09:44:55 -05:00
Peter Retzlaff
4d782bbd18 Add "Dark Synthwave" theme (#13857) 2025-06-30 09:32:32 -05:00
Nik Revenco
b036fa0b9b fix: Highlight 'x' as a character in Go 2025-06-30 10:29:20 -04:00
Nik Revenco
0d799235f6 feat: Inject markdown into Go's block documentation comments
more

chore: capture unlimited number of comments

Each commen
2025-06-30 10:29:20 -04:00
CalebLarsen
305f8bc165 Re-detect .editorconfig on :config-reload (#13443) 2025-06-30 09:21:37 -05:00
Luka Krmpotić
e03f100187 Fix typo in helix-tui text module docs (#13860) 2025-06-30 09:11:53 -05:00
Kristoffer Plagborg Bak Sørensen
f75d71844f queries: inject nix into lib.literalExression string contents (#13851) 2025-06-28 18:00:58 -04:00
Jonas Köhnen
d654a07d3d queries/dockerfile: injections for heredocs (#13852) 2025-06-28 17:59:32 -04:00
Ricardo Fernández Serrata
930340e646 feat: recognize mimeapps.list as INI (#13850) 2025-06-28 13:04:34 -04:00
Michael Davis
44293dfd22 Change tree-sitter parser for Git commit message files
The gbprod grammar is more complete and featureful than mine, and more
actively maintained. I will archive my tree-sitter-git-commit in favor
of gbprod's.

The new queries are based on the ones in the repo upstream but I
modified them to look similar to the highlights before this commit.

Also I've updated the tab-width so that change nodes in the generated
message are indented correctly.
2025-06-27 10:55:00 -04:00
Kristoffer Plagborg Bak Sørensen
a9d51ef258 queries: inject all lines in a newline escaped RUN Dockerfile instruction as bash (#13845) 2025-06-26 13:51:10 -04:00
Kristoffer Plagborg Bak Sørensen
b9f980f567 languages: detect ~/.gem/credentials as yaml (#13843) 2025-06-26 13:06:50 -04:00
CalebLarsen
c3c4895179 fix: Make code-action popup auto close like other popups (#13832) 2025-06-25 08:21:24 -05:00
Md Atiquz Zaman
974ac9eaf3 Sidra Theme - A super customizable, balanced dark theme for Helix (#13575) 2025-06-24 08:09:14 -05:00
CalebLarsen
60fce357fb queries: Fix Rust function parameter locals tracking (#13828) 2025-06-24 08:08:25 -05:00
dependabot[bot]
4f985832bf build(deps): bump libc in the rust-dependencies group (#13827)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-23 21:41:00 -04:00
Michael Davis
43963473e3 Add a ConfigDidChange event
This is meant to be minimal for now and is expected to change as the
config system evolves.

Features like word completion should be able to hook into this to
initialize or clear the word index when the toggle for the feature is
turned on or off (respectively).
2025-06-23 11:32:51 -04:00
Jason Williams
2338b44909 DAP: Support the startDebugging reverse request (#13403) 2025-06-23 09:48:05 -05:00
Ricardo Fernández Serrata
58dfa158c2 feat(langs): acknowledge jsconfig (#13822) 2025-06-23 09:07:10 -05:00
Nik Revenco
171dfc60e5 fix: highlight floor in |> float.floor as function in Gleam (#13813) 2025-06-21 14:50:52 -04:00
Harald Atteneder
250af462cd Update tree-sitter-twig rev to latest commit (#13689) 2025-06-21 14:24:54 -04:00
Nik Revenco
40a3fb9b92 feat: Improved syntax highlighting for Gleam (#13807) 2025-06-21 12:24:19 -05:00
Margret Riegert
c96642125f Update Crystal tree sitter and add support for ameba-ls (#13805)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-06-21 12:02:16 -05:00
Michael Davis
4a85171907 Show all active scopes in :tree-sitter-highlight-name
Previously the command only showed the top of the stack of highlights.
Now it shows all active highlights under the cursor, comma separated.
2025-06-21 12:29:13 -04:00
Yesudeep Mangalapilly
472a27e4f2 feat(languages): detect BUCK files as starlark (#13810)
Co-authored-by: Yesudeep Mangalapilly <yesudeep@vikata.local>
2025-06-21 09:00:55 -05:00
Michael Davis
036729211a Remove blank_issue template
GitHub has an option for a blank issue by default.
2025-06-19 10:52:16 -04:00
Michael Davis
d3fb8fc9b8 Fix prompt truncation for non-ASCII lines
The prompt was previously assuming that each grapheme cluster in the
line was single-width and single-byte. Lines like the one in the new
integration test would cause panics because the anchor attempted to
slice into a character.

This change rewrites the anchor and truncation code in the prompt to
account for Unicode segmentation and width. Now multi-width graphemes
can be hidden by multiple consecutive elipses - for example "十" is
hidden by "……" (2-width).

Co-authored-by: Narazaki, Shuji <shujinarazaki@protonmail.com>
2025-06-19 10:44:06 -04:00
yuri
684e108fd0 Fix goto_file on Windows (#13770) 2025-06-18 19:32:14 -05:00
Damir Vandic
3c6c221d45 Update gleam tree-sitter grammar (#13793) 2025-06-18 09:32:41 -05:00
Alan
6b94d70f20 chore: bump purescript grammar (#13782) 2025-06-18 09:11:28 -05:00
Roald Storm
1491cbc8f3 Add diagnostic source to diagnostic pickers (#13758) 2025-06-17 09:17:46 -05:00
Saheed Adeleye
e11794be37 use human-readable sizes for file size on save/write (#13627) 2025-06-17 09:10:14 -05:00
Nik Revenco
fba644f2b4 fix: inconsistent error messages (#12577)
Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-06-17 09:04:50 -05:00
dependabot[bot]
24fe989596 build(deps): bump the rust-dependencies group with 3 updates (#13777)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-17 08:54:41 -05:00
Michael Davis
fed3edcab7 queries: Fix highlighting of '#' in CSS color hex codes
This was previously highlighted as `punctuation` because the capture
applied to the whole `(color_value)` node rather than the `"#"` child
node specifically.
2025-06-16 13:43:41 -04:00
Michael Davis
4099465632 stdx: Add an iterator over grapheme indices in a rope slice 2025-06-16 13:12:54 -04:00
Michael Davis
9100bce9aa stdx: Unify RopeSlice grapheme cluster iterators
This style for RopeGraphemes is identical to Ropey's Chars and Bytes
iterators. Being able to move the iterator types like cursors over the
bytes/chars/graphemes is useful in some cases. For example see
`helix_core::movement::<Chars as CharHelpers>::range_to_target`.

This change also adds `RopeSliceExt::graphemes_at` for flexibility.
`graphemes` and `graphemes_rev` are now implemented in terms of
`graphemes_at` and `RopeGraphemes::reversed`.
2025-06-16 13:12:13 -04:00
Michael Davis
f5dc8245ea stdx: Add RopeSliceExt::(nth_){next,prev}_grapheme_boundary
These functions mirror those in `helix_core::graphemes` but operate
directly on byte indices rather than character indices. These are meant
to be used as we transition to Ropey v2 and always use byte indices.
2025-06-16 13:10:30 -04:00
Michael Davis
362e97e927 Update tree-house to v0.3.0
This release contains some fixes to highlight ordering which could cause
panics in the markdown component for highlights arriving out of order.
2025-06-16 10:27:19 -04:00
Michael Davis
ba54b6afe4 LSP: Short-circuit documentColors request for no servers
This fixes a deadlock when starting Helix with very many files, like
`hx runtime/queries/*/*.scm`. The tree-sitter query files don't have
an active language server on my machine and yet we were spawning a tokio
task to collect documentColors responses. We can skip that entirely.
Further debugging is needed to figure out why this lead to a deadlock
previously.
2025-06-16 09:42:48 -04:00
Tatesa Uradnik
837627dd8a feat: allow moving nonexistent file (#13748) 2025-06-16 08:19:28 -05:00
CalebLarsen
1246549afd Fix: update c++ highlights (#13772) 2025-06-16 08:04:22 -05:00
uncenter
ada8004ea5 Highlight HTML entities (#13753) 2025-06-16 08:03:02 -05:00
Pascal Kuthe
205e7ece70 update imara-diff (#13722) 2025-06-14 16:14:27 -04:00
yuri
1315b7e2b1 Feat: inlay hint length limit (#13742) 2025-06-13 11:09:21 -05:00
Lens0021 / Leslie
52192ae29e Add Amber language server (#13763) 2025-06-13 10:05:21 -05:00
Jonas Köhnen
fba1a6188a Picker: Detect language before rendering preview (#13761) 2025-06-13 09:29:26 -05:00
polyface
b90d8960a8 added crystal formatter in languages.toml (#13759)
Co-authored-by: polyface <polyface@proton.me>
2025-06-13 09:20:07 -05:00
uncenter
6e4ec96101 Highlight Ecma escape sequences (#13762) 2025-06-13 09:15:40 -05:00
CalebLarsen
62f270e5d2 chore: updated c,c++ highlights (#13747) 2025-06-12 09:28:09 -05:00
idealseal
3b7aaddb13 feat: add neocmakelsp language server (#13740) 2025-06-11 09:00:46 -05:00
Nguyễn Đức Toàn
ab97585b69 feat: add tombi language server (#13723) 2025-06-10 10:35:11 -05:00
yuri
9dbfb9b4eb Merge formatting options and config.format in range format request (#13734) 2025-06-10 09:55:46 -05:00
Caleb Larsen
091f19f67c chore: updated themes using 'comment.block.documentation' to also use 'comment.line.documentation' 2025-06-10 10:54:05 -04:00
Caleb Larsen
ae3eac8aeb fix: Rust now correctly highlights doc comments as @comment.{block,line}.documentation instead of @comment 2025-06-10 10:54:05 -04:00
dependabot[bot]
fbe9785613 build(deps): bump the rust-dependencies group with 4 updates (#13730)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 09:45:32 -05:00
spentbliss
7092e30f8d feat(colorscheme): add lapis_aquamarine colorscheme (#13726)
Co-authored-by: spentbliss <spentbliss@users.noreply.github.com>
2025-06-10 09:44:30 -05:00
Samuel Ibarra
705d467932 Update languages.toml for Mojo (#13648) 2025-06-10 09:43:00 -05:00
dependabot[bot]
05a4d05646 build(deps): bump which from 7.0.3 to 8.0.0 (#13729)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-10 09:42:04 -05:00
CalebLarsen
2b26d27416 fix: re-ordered and updated python highlights (#13715) 2025-06-08 21:34:37 -04:00
Grey
e773d6cc92 feat: add luau grammars and lsp (#13702)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-06-08 15:21:08 -04:00
Michael Davis
633c5fbf0f Update language loader before refreshing theme in :config-reload
Since locals are handled during parsing instead of highlighting with
tree-house, we need to call `helix_core::syntax::Loader::set_scopes`
before parsing any documents. During `:config-reload` we previously
reloaded the `Loader` and re-parsed documents and _then_ updated the
theme. So documents were parsed before `Loader::set_scopes` was called
on the new loader.

With this change the `refresh_language_config` helper is inlined into
`refresh_config`. Updating the `Editor`'s `ArcSwap` of the loader is
done before updating the theme so that the `load_configured_theme`
helper can call `set_scopes` with on the new loader.
2025-06-08 15:05:22 -04:00
Tan Kian
b75e95862c Add Pyrefly language server (#13713) 2025-06-08 15:01:01 -04:00
Michael Davis
f4b488e380 Remove unused helix_core::graphemes::is_grapheme_boundary
This function was never used and will be superseded by
`RopeSliceExt::is_grapheme_boundary` (which accepts a byte index rather
than a character index) once we transition to Ropey v2. In the meantime
any callers should convert to byte index and use the `RopeSliceExt`
extension rather than form new dependencies on this.
2025-06-06 18:24:44 -04:00
Michael Davis
7410fe35a3 Update tree-house and bindings to v0.2.0 2025-06-06 17:14:08 -04:00
Axlefublr
637274c4d4 Add rotate_selections_{first,last} commands (#13615) 2025-06-06 15:08:41 -05:00
Michael Davis
01341cbbf6 minor: Add missing call to Vec::clear for a buffer 2025-06-06 12:23:22 -04:00
Michael Davis
b1f4717356 Allow multiple #not-kind-eq? predicates in indent queries
This fixes a regression from the switch to tree-house with one of the
custom predicates in indent queries: `#not-kind-eq?`. This predicate
should be allowed to be written multiple times in a pattern. For example
in the Go indents:

    ; Switches and selects aren't indented, only their case bodies are.
    ; Outdent all closing braces except those closing switches or selects.
    (
        (_ "}" @outdent) @outer
        (#not-kind-eq? @outer "select_statement")
        (#not-kind-eq? @outer "type_switch_statement")
        (#not-kind-eq? @outer "expression_switch_statement")
    )

So instead of an `Option<T>` of one we need a `Vec<T>` and we need to
check that all of these predicates are individually satisfied (basically
`iter().all(/* node kind is not expected kind for that capture */)`).
2025-06-06 12:14:43 -04:00
Michael Davis
25b299abc5 queries: Recognize methods as a locals scope
This fixes a bug in highlighting parameter variables in Go methods.
See <https://redirect.github.com/helix-editor/helix/issues/13674#issuecomment-2935514099>
2025-06-06 11:48:18 -04:00
Michael Davis
4dd4ba798c Update tree-sitter-rust-format-args
Update repo name and move to latest commit which includes a license
2025-06-06 11:21:26 -04:00
Skyler Hawthorne
ca4ae7f287 set working dir of the lsp command to workspace root (#13691) 2025-06-06 10:16:36 -05:00
Matt Conway
d375f1e7f4 syntax: add grammar and highlighting for the alloy config lang (#13660) 2025-06-06 10:02:30 -05:00
Michael Davis
8d2870b94a Reuse content buffer in JSONRPC recv for LSP and DAP
Previously `recv` for new messages from the language server or debug
adapter allocated a fresh Vec for each message. Instead we can reuse
the buffer. This resolves TODO comments.

Co-authored-by: Rolo <roloedits@gmail.com>
2025-06-06 10:19:33 -04:00
Michael Davis
f6878f62f7 Set enableDiagnostics in prisma-language-server config
Without this key `prisma-language-server` currently crashes during
initialization. See #13662.
2025-06-03 10:08:49 -04:00
Ryan Mehri
6c43dc4962 fix: trim whitespace up to the last selection on insert_newline (#13673) 2025-06-03 08:45:19 -05:00
dependabot[bot]
1ea9050a5e build(deps): bump the rust-dependencies group with 2 updates (#13676)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-06-02 18:27:22 -05:00
Nik Revenco
2baff46b25 fix: Some Incorrect Rust highlights (#13657)
Co-authored-by: Nik Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-05-31 09:43:19 -05:00
Jonas Köhnen
921ca08e1b highlights/go: fix locals clashing with variable.other.member (#13644) 2025-05-31 09:41:19 -05:00
Jeff Bencin
17fb12bcf3 feat: Add Clarity language support (#13647) 2025-05-31 09:11:16 -05:00
Michael Davis
67f1fe20c3 Fix command line completion replacement for quoted items
With a directory with spaces in the name (for example
`mkdir -p 'Temp/Abc Def'`), completing `Temp/Ab` would create a
completion item `'Temp/AbAbc Def'`. Now it correctly completes
`'Temp/Abc Def'`
2025-05-31 09:56:54 -04:00
Michael Davis
8961ae1dc6 Consistently use helix_core::config::default_{lang,config}_loader
This avoids using any custom configuration in a user-defined
`languages.toml` config for the syntax test cases. The test cases should
only use the builtin `languages.toml` config.

Also the xtask crate reimplemented `default_lang_loader` and
`default_lang_config`. These functions are replaced with calls into
`helix_core`.
2025-05-29 09:55:26 -04:00
Tino
3366db0afb Add Ty language server (#13643) 2025-05-29 08:34:23 -05:00
Erasin Wang
733ebcdaeb Add file indentation style for statusline (#13632) 2025-05-29 08:20:22 -05:00
CalebLarsen
2bd7452fe0 Make signature_help more like hover, fix overflow and lack of scrolling in signature_help (#13566)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-05-27 10:18:12 -05:00
CalebLarsen
7dcddf98c6 Append changes to history on jumps (#13619)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-05-27 09:44:47 -05:00
dependabot[bot]
c9e7b0f84f build(deps): bump the rust-dependencies group with 2 updates (#13621)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-27 08:39:15 -05:00
Axlefublr
2fbe7fc5b5 fix(doc): missing capitalization of goto_{next,prev}_tabstop (#13616) 2025-05-26 08:48:32 -05:00
Zhaith Izaliel
12523cd126 fix: fix support for grammar overlays and include predicate (#13603) (#13613) 2025-05-26 08:47:14 -05:00
Binh Tran
8d58f6ce8d fix(highlights/ungrammar): improve UX (#13607) 2025-05-25 08:47:10 -05:00
Michael Davis
702a961517 Fix try_restore_indent on non-LF documents
On Windows for example the behavior of this function typically diverges
from the usual behavior on Unix. Instead of checking that the inserted
string starts with `'\n'` (untrue for for CRLF line endings) we need to
check that the first grapheme cluster in the string is a line ending.
(All line endings are single grapheme clusters.)
2025-05-24 11:42:29 -04:00
Nik Revenco
1023e8f964 feat: highlight rust string interpolation macros that use format_args! (#13533)
Co-authored-by: Nik Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-05-24 10:02:32 -05:00
Saheed Adeleye
223ceec10a Add an --insensitive/-i flag for :sort (#13560)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-05-24 09:55:48 -05:00
Christian Fredrik Johnsen
cb1ec1b27e fix(tutor): replace unexisting and non existing with nonexistent
Sounds more natural to me.
Just a spelling change.
2025-05-24 10:29:46 -04:00
Christian Fredrik Johnsen
4098151591 fix(tutor): fix typos in section 11.2
Minor grammar fix and add missing word.
2025-05-24 10:29:46 -04:00
Christian Fredrik Johnsen
1edf98262c fix(tutor): recommend e instead of w for selecting word
I was doing the tutorial to learn Helix, and it's more sensible to use
`e` than `w`.

If you're using `w`, you will need to add an extra space.
Example, assuming cursor is at |:

This sentence has incorrect words |behind it.

If you use `wc`, then you will get:
This sentence has incorrect words |it.

while `ec` will give you
This sentence has incorrect words | it.

Which enables you to drop removing and adding a space for no reason.
2025-05-24 10:29:46 -04:00
Ricardo Fernández Serrata
237d875e7d docs(building-from-source): suggest optimized install cmd as alternative (#13553) 2025-05-24 09:24:09 -05:00
Kris Warner
b70b8df916 Add matching cursor to nord theme (#13574) 2025-05-24 09:22:23 -05:00
Binh Tran
ae0dd313bd fix(highlights/ini): make consistency with toml (#13589) 2025-05-22 08:52:51 -05:00
Jérôme Tamba
76029e5840 health: Use lsp name in output wherever possible (#13573)
Also match the DAP/Formatter output style to the LSP style
2025-05-21 09:38:16 -05:00
dependabot[bot]
3a6c9747b8 build(deps): bump the rust-dependencies group with 3 updates (#13576)
Bumps the rust-dependencies group with 3 updates: [bitflags](https://github.com/bitflags/bitflags), [hashbrown](https://github.com/rust-lang/hashbrown) and [cc](https://github.com/rust-lang/cc-rs).


Updates `bitflags` from 2.9.0 to 2.9.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.9.0...2.9.1)

Updates `hashbrown` from 0.15.2 to 0.15.3
- [Release notes](https://github.com/rust-lang/hashbrown/releases)
- [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/hashbrown/commits/v0.15.3)

Updates `cc` from 1.2.22 to 1.2.23
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.22...cc-v1.2.23)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-version: 2.9.1
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: hashbrown
  dependency-version: 0.15.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.23
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-20 21:03:55 +09:00
CalebLarsen
ebf96bd469 Updated python/locals.scm to label self and cls as variable.buitin (#13552) 2025-05-17 09:43:21 -05:00
Michael Davis
5a1dcc2429 syntax: Reset query cursor byte range for textobjects
`InactiveQueryCursor::new` might reuse a query cursor from a
thread-local cache if one is available, rather than create a new cursor.
Currently tree-house does not reset cached cursors back to defaults
(i.e. byte range and match limit). For now we can patch around this here
but eventually this should be fixed in `tree-house` upstream. Then this
patch can be reverted.

In practice this caused textobjects like `]f` to get "stuck" trying to
move to the next function if it was out of the current view. This is
because the highlight query cursor sets the range of the cursor to the
current viewport. We can reset the byte range to defaults to fix the
textobject behavior.
2025-05-17 10:30:30 -04:00
Michael Davis
be1bf2f909 CI: Set a timeout for the test suite workflow
The integration tests have recently become a bit flaky in that
individual tests can seem to hang forever. This needs further
investigation. In the meantime we should limit the total allowed time
for the test workflow to something reasonably low. The default timeout
is a very high 360min.
2025-05-17 09:16:14 -04:00
Michael Davis
05ae617e1c queries: Reorder Slint and HTML injections in Rust
This fixes injections of Slint and HTML in Rust macros. These patterns
must be moved after the generic `(macro_invocation (token_tree))`
pattern since they are more specific, and later patterns now take
priority.

See <https://redirect.github.com/helix-editor/helix/pull/12972#issuecomment-2888300442>.
2025-05-17 08:48:01 -04:00
CalebLarsen
e606652a96 Removed unnecessary apostrophe in keymap.md (#13551) 2025-05-17 07:43:27 -05:00
CalebLarsen
df02ef6a99 Update tree-sitter-haskell (#13475) 2025-05-17 07:42:52 -05:00
Michael Davis
3ceae88c3a Use 'ui.text' as a base style for the syntax highlighter
This fixes a regression from the refactor of the highlighters when
switching to tree-house. The old `StyleIter` used `renderer.text_style`
as the base style rather than `Style::default()` for syntax highlights.
The result was that any text not captured by a syntax highlight query
was styled with no foreground or background, defaulting to the
terminal's foreground/background. This could cause text in markdown
files to look off-colored depending on your terminal configuration.
(Though you wouldn't notice if your 'ui.text' theming matches your
terminal's theming.)
2025-05-16 10:52:47 -04:00
Nguyễn Hồng Quân
b4e51ef895 More glob to detect gitattributes file (#13540) 2025-05-16 09:32:29 -05:00
Rock Boynton
f157a918a3 Show the primary selection index on statusline (#12326)
Co-authored-by: Rock Boynton <rboynton@anduril.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-05-15 07:53:02 -05:00
Michael Davis
a7c3a43069 Bump tree-house-bindings to v0.1.1 to fix IllumOS build
See <https://redirect.github.com/helix-editor/tree-house/issues/8>.
2025-05-15 08:33:10 -04:00
Michael Davis
702b1d0a0f statusline: Avoid unnecessary allocations for &'static str spans
Previously the statusline `write` function only accepted a string
and optional Style, so all rendering functions converted text to
strings. Some elements write spans with `&'static str`s, however, making
this unnecessary since `Span<'a>` is a wrapper around `Cow<'a, str>` and
style, and a `Span<'static>` would outlive all required lifetimes.

Moreover many elements could produce `Span<'a>` according to the
lifetime in `RenderContext` in the future, potentially re-borrowing from
the Editor borrow, so this change could save allocations for many
file-type elements (with more future changes). This is not explored in
this patch since the statusline functions currently add bespoke padding
per-element, but with a future refactor to make spacing consistent this
could be possible.

This change refactors the write function to accept a `Span<'a>` and
rewrites some related code to fit the codebase better (preferring `for`
to iterator's `for_each` for example). The new code is more complicated
lifetime-wise but avoids allocations in these cases:

* spacer for mode name when a pane is not focused
* LSP spinner frames
* '●' (workspace) diagnostic indicators
* " W " workspace diagnostic prefix
* file modification indicators
* read-only indicators
* spacer element

... and opens the door to avoid allocation for file name elements in the
future.
2025-05-15 08:33:02 -04:00
Michael Davis
b0528bbac4 statusline: Avoid showing only 'W' in workspace-diagnostics element
If you configure a subset of severities to show in the workspace
diagnostics statusline element you can see the 'W' (and surrounding
space) without any diagnostic indicators. This is the case by default
as it's configured to show warnings and errors only - if you have only
hints in your workspace like if you open `application.rs` in Helix for
example then you would see the 'W' and no indicators.

This change checks that any of the configured diagnostics are non-zero
and bails early if there are none.
2025-05-14 17:33:21 -04:00
Michael Davis
09bc67ad6d syntax: Fix language detection by shebang
The switch to tree-house accidentally dropped some shebang parsing code
from the loader's function to detect by shebang. This change restores
that. The new code is slightly different as it's using a `regex_cursor`
regex on the Rope rather than eagerly converting the text to a
`Cow<str>` and running a regular regex across it.
2025-05-14 16:30:29 -04:00
zoey
6be38642f4 Add nyxvamp themes (#12185) 2025-05-14 08:52:00 -05:00
Michael Davis
f46222ced3 Revert "feat: Highlight Rust String interpolation macros"
This reverts commit 9bb80a74e1.
2025-05-13 19:52:48 -04:00
Nikita Revenco
9bb80a74e1 feat: Highlight Rust String interpolation macros
Fixes #5845
2025-05-13 19:31:42 -04:00
Michael Davis
be1cf090c3 queries: Inject markdown into Rust doc comments
Co-authored-by: Nik Revenco <154856872+nik-rev@users.noreply.github.com>
2025-05-13 19:01:10 -04:00
Michael Davis
a3b64b6da2 queries: Rewrite all locals 2025-05-13 19:01:10 -04:00
Michael Davis
aea53523dd Replace tree-sitter with tree-house 2025-05-13 18:43:43 -04:00
Michael Davis
24e3ccc31b Add the syn_loader to Document
This type also exists on `Editor`. This change brings it to the
`Document` as well because the replacement for `Syntax` in the child
commits will eliminate `Syntax`'s copy of `syn_loader`. `Syntax` will
also be responsible for returning the highlighter and query iterators
(which will borrow the loader), so the loader must be separated from
that type.

In the long run, when we make a larger refactor to have
`Document::apply` be a function of the `Editor` instead of the
`Document`, we will be able to drop this field on `Document` - it is
currently only necessary for `Document::apply`. Once we make that
refactor, we will be able to eliminate the surrounding `Arc` in
`Arc<ArcSwap<syntax::Loader>>` and use the `ArcSwap` directly instead.
2025-05-13 18:30:21 -04:00
Michael Davis
c94fde8d1c syntax: Move config types to a separate module 2025-05-13 18:30:21 -04:00
Jengamon
84e95d35ee Add sld to Scheme file type extensions (#13528) 2025-05-13 17:21:51 -05:00
dependabot[bot]
447a6d3299 build(deps): bump the rust-dependencies group with 6 updates (#13518)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-13 08:26:46 -05:00
RoloEdits
47547e94ad perf(statusline): reorder match and specify u32 for workspace_diagnostics (#13512) 2025-05-12 08:27:04 -05:00
Aitor
908b9edf28 add support for tree-sitter custom template languages on vue (#13511)
Co-authored-by: TheSylex <TheSylex@users.noreply.github.com>
2025-05-12 08:23:59 -05:00
Bekobi
63a1a94d92 Changed rust-analyzer configuration to use server-side file watching (#13432)
Co-authored-by: Bekobi <coding@bekobi.eu>
2025-05-10 14:48:46 -04:00
omahs
bfd2c72715 fix: typos (#13505) 2025-05-10 10:36:17 -05:00
Bryce Berger
4c8600967c verilog: add highlighting for the "var" keyword (#13493) 2025-05-10 08:08:34 -05:00
Jules Wiriath
313ef30f64 bump: tree-sitter-cpp (#13504) 2025-05-10 08:00:05 -05:00
bloxx12
9bb370c91e chore: clean up grammars.nix (#13506) 2025-05-10 07:58:48 -05:00
Bloxx12
cb1ecc9128 flake: checks: build the release package 2025-05-09 11:57:48 -04:00
Bloxx12
7a6bc53528 default.nix: remove ellipsis in argument attrset 2025-05-09 11:57:48 -04:00
Bloxx12
60a03a35c6 flake: drop flake-utils dependency 2025-05-09 11:57:48 -04:00
David Else
e4ef096945 Update Bash highlights (#13477) 2025-05-08 08:22:14 -05:00
Michael Davis
9e7a6a5dbd xtask: Allow passing languages and themes to check
This is for convenience when testing a few languages or themes, for
example while updating a language's parser and queries. query-check in
particular can take a while since parser initialization and query
analysis can each take some time and there are now many many languages.
Specifying the exact language makes the feedback loop much faster.
2025-05-08 09:04:22 -04:00
Nik Revenco
1460a086df feat: add wgsl built-in functions to @function.builtin (#13479)
Co-authored-by: Nik Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-05-08 07:56:36 -05:00
Sean Russell
51d3b15557 Update tree-sitter-v (#13469)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-05-07 17:38:07 -05:00
Remo Senekowitsch
e53462c78c verilog: separate highlighting of keyword operators (#13473) 2025-05-06 08:24:14 -05:00
dependabot[bot]
fb45017a26 build(deps): bump the rust-dependencies group with 3 updates (#13474)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-05-06 08:16:18 -05:00
Rotem Horesh
cbac427383 feat: add a tree-sitter grammar and highlights for dunst's config (#13458) 2025-05-05 09:08:33 -05:00
CalebLarsen
46cb177792 feat: add basic Slang support (#13449)
Co-authored-by: uncenter <uncenter@uncenter.dev>
2025-05-05 09:03:40 -05:00
Spenser Black
4784650ccf Add support for Pug language (#13435) 2025-05-05 08:57:00 -05:00
uncenter
ece12dd74d docs(guides/textobject): list parameter.around capture (#13470) 2025-05-05 09:20:57 -04:00
Spenser Black
72932a391b Support .git-blame-ignore-revs (#13460) 2025-05-04 08:52:28 -05:00
RoloEdits
4c630c148a feat(commands): add selection variable expansion (#13467) 2025-05-04 08:43:09 -05:00
RoloEdits
ac3c6ebaff feat(commands): add language variable expansion (#13466) 2025-05-04 08:35:58 -05:00
Michael Davis
12139a4c30 queries: Fix comment.unused highlights in Erlang
* Other languages use `comment.unused` instead of `comment.discard`.
* Fix the precedence of discarded variables - previously the parameter
  highlight was higher precedence so discarded variables in the
  parameter list were highlighted as parameters instead of
  `comment.unused`.
2025-05-02 09:46:46 -04:00
RoloEdits
aa3fad84ef build(grammar): remove explicit opt out of optimizations for MSVC (#13451)
`cc` should pick correct defaults when `debug` or `release`.
2025-05-01 22:54:45 +02:00
Daniel Bowring
69b9db2fbb Add goto_column and extend_to_column commands (#13440) 2025-05-01 09:12:30 -05:00
Spenser Black
1c32fb2d4d Help Linguist identify Tree-sitter queries (#13436) 2025-04-29 08:28:20 -05:00
dependabot[bot]
b42e1d20d2 build(deps): bump the rust-dependencies group with 3 updates (#13437) 2025-04-29 10:48:45 +09:00
Joffrey Bluthé
949d9e4433 feat: give formatters access to filename (#13429)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-04-28 17:34:05 -05:00
RoloEdits
fbc4e5798e chore(msrv): bump from 1.76 to 1.82 (#13275)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-04-28 09:48:54 -05:00
Michael Davis
47cdd23e50 LSP: Fix Client::supports_feature check for 'colorProvider'
The old check would allow sending textDocument/documentColor requests
when the server declared `{"colorProvider": false}` in its capabilities
as this was `Some`:

    Some(ColorProviderCapability::Simple(false))

The Julia language server sets this explicitly to `false` in the wild.
2025-04-28 09:44:25 -04:00
Michael Davis
9f3b193743 queries: Fix precedence for zero-arity erlang macros 2025-04-27 15:06:11 -04:00
Sumandora
99b57181d5 Improve auto completion for shell commands (#12883)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-04-27 14:04:56 -05:00
Jonas Köhnen
448e3c2ef5 fix(highlights/go): defer, go, goto are keywords (#13425) 2025-04-27 13:23:47 -05:00
Tobias Tschinkowitz
95c8d9c9e9 Add highlighting for lua methods (#13401) 2025-04-27 08:41:16 -05:00
dastrukar
8580d35de9 sonokai: fix constant.builtin highlights (#13410) 2025-04-27 08:39:05 -05:00
one
c5e9dda269 feat(theme): add kinda_nvim theme (#13406) 2025-04-27 08:35:23 -05:00
Queyrouzec
01f7e636d5 Update svelte tree sitter commit (#13423) 2025-04-27 08:33:29 -05:00
IwantHappiness
2ec59f8ff6 feat(theme): add vesper theme (#13394) 2025-04-26 12:16:18 -04:00
Mazrine
23e16264b3 feat(theme): add ataraxia theme (#13390) 2025-04-26 12:15:38 -04:00
Matouš Dzivjak
0354ceb95f chore(languages): bump tree-sitter-go-mod (#13395) 2025-04-26 11:57:37 -04:00
Erasin Wang
626407395a fix theme onelight: ui.picker.header (#13413) 2025-04-26 11:50:01 -04:00
Erasin Wang
52d4d775ce Update scss and highlights (#13414) 2025-04-26 11:35:31 -04:00
dependabot[bot]
0815b52e09 build(deps): bump libc in the rust-dependencies group (#13389) 2025-04-22 15:10:48 +09:00
Daichi Tamaki
523e8aa781 Fix tokyonight_* theme color keys (#13375)
Co-authored-by: tamakiii <tamakiii@users.noreply.github.com>
2025-04-18 09:59:53 -05:00
Kristoffer Plagborg Bak Sørensen
3bfec18a45 Make goto_word jump-labels easier to distinguish for zed themes (#13370) 2025-04-17 15:26:52 -05:00
Daniel Fichtinger
0f1af75f76 feat: added ashen theme (#13366) 2025-04-17 14:40:06 -05:00
James Turk
37b5d8ba99 feat: add basic Quarto support (#13339) 2025-04-16 08:03:21 -05:00
chtenb
795040910b Update other-software.md (#13360) 2025-04-16 07:58:26 -05:00
Jonas Köhnen
0ca01a9649 feat(themes): add gruvbox-material (#13311) 2025-04-15 18:19:02 -05:00
VuiMuich
8f30f39c6a update serika themes (#13341) 2025-04-15 18:17:04 -05:00
Carter Watson
31cc2110ec add: ui.text.focus to gruvbox configs (#13315) 2025-04-15 18:09:07 -05:00
icefoxen
bce166290a Basic language support for Fennel. Might even work. (#13260)
Co-authored-by: uncenter <uncenter@uncenter.dev>
Co-authored-by: Simon Heath <simon.heath@nearearth.aero>
2025-04-15 18:05:44 -05:00
dependabot[bot]
b1345b302d build(deps): bump the rust-dependencies group with 3 updates (#13351)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-15 08:23:27 -05:00
Niklas Gruhn
340934db92 Basic injection queries for Quint (#13322) 2025-04-11 10:31:57 -05:00
Gábor Szabó
d0275a554a remove the multilingual field from book.toml (#13328) 2025-04-11 09:23:39 -05:00
RoloEdits
4a7939928e chore(jjdescription): bump rev to latest (#13329) 2025-04-11 09:06:20 -05:00
Niklas Gruhn
8b952bb1d5 docs: another injection query example (#13312) 2025-04-09 07:30:11 -05:00
RoloEdits
d24e4fcf0f feat(config): add [workspace-]diagnostics fields to statusline (#13288) 2025-04-08 13:58:14 -05:00
RoloEdits
34aa4d41c6 ci: add arm64 runner (#13273) 2025-04-08 11:54:12 -05:00
PL Pery
032c7b897d flake: fix Helix.desktop and helix.png output paths (#13305) 2025-04-08 10:18:17 -05:00
dependabot[bot]
5d16aae58e build(deps): bump the rust-dependencies group across 1 directory with 5 updates (#13301)
Bumps the rust-dependencies group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [smallvec](https://github.com/servo/rust-smallvec) | `1.14.0` | `1.15.0` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.44.1` | `1.44.2` |
| [rustix](https://github.com/bytecodealliance/rustix) | `1.0.3` | `1.0.5` |
| [indexmap](https://github.com/indexmap-rs/indexmap) | `2.8.0` | `2.9.0` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.2.17` | `1.2.18` |



Updates `smallvec` from 1.14.0 to 1.15.0
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.14.0...v1.15.0)

Updates `tokio` from 1.44.1 to 1.44.2
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.44.1...tokio-1.44.2)

Updates `rustix` from 1.0.3 to 1.0.5
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Changelog](https://github.com/bytecodealliance/rustix/blob/main/CHANGES.md)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v1.0.3...v1.0.5)

Updates `indexmap` from 2.8.0 to 2.9.0
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.8.0...2.9.0)

Updates `cc` from 1.2.17 to 1.2.18
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.17...cc-v1.2.18)

---
updated-dependencies:
- dependency-name: smallvec
  dependency-version: 1.15.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: tokio
  dependency-version: 1.44.2
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustix
  dependency-version: 1.0.5
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: indexmap
  dependency-version: 2.9.0
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-version: 1.2.18
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-07 21:33:46 -05:00
Julien THILLARD
a799794623 Change highlights order (#13293) 2025-04-08 09:16:31 +09:00
Multirious
0609b06638 Book: Mentions zsh-helix-mode in other-softwares (#13294) 2025-04-08 09:15:54 +09:00
James Rogers
4130b162a7 Update docs link to Commands.md in Remapping.md (#13284) 2025-04-07 11:08:09 -05:00
Jason Fuchs
46f7cdb5a9 feat: add ! alias for sh and | for pipe (#13263) 2025-04-08 01:00:45 +09:00
Mykyta
9cfb8afa99 Added: missing highlighting to queries/_typescript (#13250)
* Added: missing highlighting
* moved: delete, typeof, instanceof, void - from keywords to keyword.operator
2025-04-07 10:00:03 -05:00
Dimitri Sabadie
29789f2a9f Add support for extend_file_{start,end} (#11767) 2025-04-06 13:18:47 -05:00
Austin L Wolfgram
efdcf34b79 Fix out of date comments on merge_toml_values (#13253)
merge_toplevel_arrays is no longer a parameter for this method, and the
example fits better in a doc comment.
2025-04-06 13:00:18 -05:00
Weihnachtsbaum
e9a3dcd858 Add WESL language (#13267)
Co-authored-by: uncenter <uncenter@uncenter.dev>
2025-04-06 12:23:34 -05:00
Mykyta
effe849cf4 Update themes.md (#13247)
added small description to `label` and `special` - because for me it was not clear what it was for, maybe I am not the one
2025-04-06 12:22:06 -05:00
Jorge Gomez
130f725026 Update mint lsp command args (#13248) 2025-04-06 12:21:33 -05:00
Nik Revenco
42de785779 feat: sync catppuccin theme changes (#13262)
Co-authored-by: Nik Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-04-06 12:06:21 -05:00
Wesley Hershberger
6c42ed1bd5 Debian control file syntax highlighting (#13245)
File format described at
https://www.debian.org/doc/debian-policy/ch-controlfields.html

Co-authored-by: Wesley Hershberger <wesley.hershberger@gmail.com>
2025-04-06 12:05:43 -05:00
Rolo
5b72b59448 perf: use next_back on DoubleEndedIterator 2025-04-06 12:03:14 -05:00
Rolo
1bc45c8b3a refactor: change empty check to is_empty instead of len > 0 2025-04-06 12:03:14 -05:00
Rolo
f857a98671 refactor: uneeded string conversion for Display type 2025-04-06 12:03:14 -05:00
dependabot[bot]
5a671e65fd build(deps): bump gix from 0.70.0 to 0.71.0 (#13269)
* build(deps): bump gix from 0.70.0 to 0.71.0

Bumps [gix](https://github.com/GitoxideLabs/gitoxide) from 0.70.0 to 0.71.0.
- [Release notes](https://github.com/GitoxideLabs/gitoxide/releases)
- [Changelog](https://github.com/GitoxideLabs/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/GitoxideLabs/gitoxide/compare/gix-v0.70.0...gix-v0.71.0)

---
updated-dependencies:
- dependency-name: gix
  dependency-version: 0.71.0
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>

* minor: Migrate from Repository::work_dir to Repository::workdir

The former was deprecated in favor of the latter

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-04-06 12:02:20 -05:00
Mykyta
994b750dd4 fixed: typo in capture group, removed duplicate query (#13251) 2025-04-06 11:49:21 -05:00
Max
1fc19c6d8e Add Prolog tree-sitter grammar (#11611) 2025-04-06 11:43:53 -05:00
Daniel Fichtinger
d0c5a2044d feat: added lsp for just (#13276) 2025-04-06 11:41:53 -05:00
Tshepang Mbambo
2bb0d52f3e book: avoid uncomfortable flow disruption (#13271) 2025-04-06 11:41:10 -05:00
dependabot[bot]
7ebf650029 build(deps): bump once_cell in the rust-dependencies group (#13244)
Bumps the rust-dependencies group with 1 update: [once_cell](https://github.com/matklad/once_cell).


Updates `once_cell` from 1.21.1 to 1.21.3
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.21.1...v1.21.3)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-version: 1.21.3
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-04-01 13:55:16 +09:00
Sri Senthil Balaji J
db187c4870 feat: add ConsoleOnly to desktop entry categories (#13236) 2025-03-31 09:28:50 -05:00
Michael Davis
e148d8b311 editor: Remove closed Document after updating Views
When closing a document we must wait until all views have been updated
first - either replacing their current document or closing the view -
before we remove the document from the `documents` map. The
`Editor::_refresh` helper is called by `Editor::close`. It accesses each
View's Document to sync changes and ensure that the cursor is in view.
When closing multiple Views at once, `Editor::_refresh` will attempt
to access the closing Document while refreshing a to-be-closed View.
2025-03-30 11:01:17 -04:00
Andrea Novellini
fb815e2c6f Add peachpuff theme (#13225) 2025-03-29 14:44:55 -05:00
Gavin Morrow
e735485277 Remove bg from tokyonight text (#13216) 2025-03-29 14:43:33 -05:00
Keir Lawson
bb96a535fc Add ui.text.directory to spacebones (#13213) 2025-03-29 14:41:43 -05:00
RoloEdits
01fce51c45 fix(keymap): point to proper MappableCommand instead of Command (#13214) 2025-03-28 08:51:36 -05:00
Keir Lawson
7929c0719d Track progress title an display in place of internal token (#13180)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-03-27 12:00:23 -05:00
Alexandre Legent
68d7308e25 Update golangci-lint command for v2 (#13204) 2025-03-27 11:27:25 -05:00
Daniel Fichtinger
a2c580c4ae feat: add tmTheme as XML filetype (#13202) 2025-03-27 08:39:10 -05:00
Steven Vancoillie
cf7eb5517f Add earl_grey theme (#13203) 2025-03-27 08:37:32 -05:00
Michael Davis
388a3b78e3 Avoid removing modified documents in Editor::close_document
This fixes a regression from 6da1a79d80. `:buffer-close` on an
unmodified document would cause later panics since the document should
not have been removed. Instead of eagerly removing the document on the
first line we need to wait until we've checked that it's unmodified.
2025-03-25 09:03:32 -04:00
Michael Davis
d43de14807 LSP: Avoid requesting document colors for ghost transactions
The point of ghost transactions is to avoid notifying language servers
about changes since the change is meant to be temporary. This is used
for completion while selecting items in the menu: updating the language
server would mess up incomplete completions.

When a document is changed by a ghost transaction the language server
will not be notified so its understanding of the document will not be
synchronized and any positions it sends may be out-of-date. So we should
avoid triggering a request for new document color information when a
document is changed by a ghost transaction.
2025-03-25 08:52:47 -04:00
dependabot[bot]
04d1180a0c build(deps): bump the rust-dependencies group with 4 updates (#13190)
Bumps the rust-dependencies group with 4 updates: [tempfile](https://github.com/Stebalien/tempfile), [log](https://github.com/rust-lang/log), [rustix](https://github.com/bytecodealliance/rustix) and [cc](https://github.com/rust-lang/cc-rs).


Updates `tempfile` from 3.19.0 to 3.19.1
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.19.0...v3.19.1)

Updates `log` from 0.4.26 to 0.4.27
- [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.26...0.4.27)

Updates `rustix` from 1.0.2 to 1.0.3
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Changelog](https://github.com/bytecodealliance/rustix/blob/main/CHANGES.md)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v1.0.2...v1.0.3)

Updates `cc` from 1.2.16 to 1.2.17
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.16...cc-v1.2.17)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: log
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustix
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-25 07:40:40 -05:00
Nick
5adb4b7413 Allow :theme to show current theme (#13192)
Updates the signature for the command to take 0 arguments. This probably
regressed during 0efa8207d8.
2025-03-25 18:43:26 +09:00
Sebastian Dörner
13b2dc31f5 Book: Add a section with links to "Helix mode" in non-Helix software. (#12258) 2025-03-24 08:21:37 -05:00
Nik Revenco
0ee5850016 Color swatches ( 🟩 green 🟥 #ffaaaa ) (#12308) 2025-03-23 16:07:02 -05:00
Asta Halkjær From
8ff544757f Make goto_word highlights visible (same fix as #12904) (#13174) 2025-03-23 10:27:58 -05:00
Branch Vincent
f07c1c1b29 add ui.text.directory to onedark (#13166) 2025-03-23 09:33:23 -05:00
RoloEdits
8ad6e53b1f build(grammar): remove -fPIC flag from windows build (#13169)
Even though there is a check for `is_like_msvc`, when setting `CXX` to
`clang++` this will miss that check and try to use `-fPIC`, which is an
invlaid flag for the target.
2025-03-23 09:32:56 -05:00
Ahmir Postell
6bedca8064 Add focus_nova theme (#13144) 2025-03-22 16:24:16 -05:00
Michael Davis
7e7a98560e LSP: Eagerly decode request results in the client
Previously the `call` helper (and its related functions) returned a
`serde_json::Value` which was then decoded either later in the client
(see signature help and hover) or by the client's caller. This led to
some unnecessary boilerplate in the client:

    let resp = self.call::<MyRequest>(params);
    Some(async move { Ok(serde_json::from_value(resp.await?)?) })

and in the caller. It also allowed for mistakes with the types. The
workspace symbol request's calling code for example mistakenly decoded a
`lsp::WorkspaceSymbolResponse` as `Vec<lsp::SymbolInformation>` - one of
the untagged enum members (so it parsed successfully) but not the
correct type.

With this change, the `call` helper eagerly decodes the response to a
request as the `lsp::request::Request::Result` trait item. This is
similar to the old helper `request` (which has become redundant and has
been eliminated) but all work is done within the same async block which
avoids some awkward lifetimes. The return types of functions like
`Client::text_document_range_inlay_hints` are now more verbose but it is
no longer possible to accidentally decode as an incorrect type.

Additionally `Client::resolve_code_action` now uses the `call_with_ref`
helper to avoid an unnecessary clone.
2025-03-22 14:40:29 -04:00
Michael Davis
6da1a79d80 Add document and LSP lifecycle events, move some callbacks into hooks
This adds events for:

* a document being opened
* a document being closed
* a language server sending the initialized notification
* a language server exiting

and also moves some handling done for these scenarios into hooks,
generally moving more into helix-view. A hook is also added on
`DocumentDidChange` which sends the `text_document_did_change`
notification - this resolves a TODO in `document`.
2025-03-22 11:41:50 -04:00
Michael Davis
2cc33b5c47 Add pull diagnostics identifier to LSP diagnostic provider
This includes a change to lsp-types to store the identifier as an Arc
since it will be cloned for each diagnostic.
2025-03-22 09:25:29 -04:00
Michael Davis
683fac65e7 Refactor DiagnosticProvider as an enum
This resolves a TODO in the core diagnostic module to refactor this
type. It was originally an alias of `LanguageServerId` for simplicity.
Refactoring as an enum is a necessary step towards introducing
"internal" diagnostics - diagnostics emitted by core features such as
a spell checker. Fully supporting this use-case will require further
larger changes to the diagnostic type, but the change to the provider
can be made first.

Note that `Copy` is not derived for `DiagnosticProvider` (as it was
previously because `LanguageServerId` is `Copy`). In the child commits
we will add the `identifier` used in LSP pull diagnostics which is a
string - not `Copy`.
2025-03-22 09:25:29 -04:00
Michael Davis
2d4c2a170c commands: Allow any number of arguments in :bc, :bc!
Limiting to zero arguments was incorrect - a set of buffers can be
specified.
2025-03-22 09:17:30 -04:00
Michael Davis
14cab4ba62 LSP: Gracefully handle partial failures in multi-server LSP requests
This is the same change as 1c9a5bd366 but for:

* document symbols
* workspace symbols
* goto definition/declaration/.../references
* hover

Instead of bailing when one server fails, we log an error and continue
gathering items from the other responses.
2025-03-22 08:54:22 -04:00
Michael Davis
3a63e85b6a Support EditorConfig (#13056) 2025-03-22 16:06:41 +09:00
RoloEdits
f6cb90593d chore(worker): remove unused lifetime on EventAccumulator (#13158) 2025-03-22 16:00:39 +09:00
Ian Hobson
1c9a5bd366 Show successfully requested code actions after a failed request (#13156)
When requesting code actions from multiple LSP servers,
rather than bailing as soon as an error is encountered,
instead log the error and then keep going so that successful
requests can be presented to the user.
2025-03-21 09:10:24 -05:00
Michael Davis
1dee64f7ec minor: Accept impl AsRef<Path> in loader's runtime_file helper
This is purely for ergonomics: we should be able to pass strings for
example

    crate::runtime_file(format!("namespace/{foo}/{bar}.txt"))

(Note that this works on Windows, see the `Path` documentation.)
2025-03-20 22:05:23 -04:00
Jan
b7d735ffe6 Switch from reddish-orange to orangeish-yellow for Solarized diff.delta (#13121) 2025-03-20 08:59:23 -05:00
Jan
7ca916ab73 Switch from reddish-orange to orangeish-yellow for Solarized diff.delta (#13121) 2025-03-20 08:55:06 -05:00
may
8e65077065 queries(scheme): consider the first argument of λ to be a variable (#13143) 2025-03-20 08:54:26 -05:00
Freddie Gilbraith
d6cacb2731 add werk language and highlights (#13136) 2025-03-20 08:04:52 -05:00
Rishikanth Chandrasekaran
ccf9564123 Adds Carbon theme for helix editor (#13067) 2025-03-19 08:54:10 -05:00
Max Milton
e7c82a34a5 language: Extend ini with more systemd file-types (#13139) 2025-03-19 08:44:37 -05:00
Michael Davis
33c17d48ff minor: Move 'execute_lsp_command' helper into helix-view
This is a minor move that will make future refactors of code actions
simpler. We should be able to move nearly all code action functionality
into `helix-view`, save UI stuff like the `menu::Item` implementation
and dealings with the compositor.
2025-03-19 09:41:18 -04:00
Zeger Van de Vannet
6f463dbeb3 feat: add indents for starlark (#13126) 2025-03-18 09:18:45 -05:00
dependabot[bot]
70a60efcbe build(deps): bump the rust-dependencies group with 5 updates (#13131) 2025-03-18 12:52:22 +09:00
trevershick
9d31e4df11 fix: adjust spelling of simlink->symlink (#13128) 2025-03-18 08:03:25 +09:00
Lens0021 / Leslie
27ca9d2c33 Add '///' to Amber comment-token configuration (#13122) 2025-03-16 11:03:03 -05:00
Ben Brown
e56d3abb0a languages: Also include gitconfig as an extension (#13115)
This is useful for maintaining syntax highlighting when editing git
config files which have been included via `include` or `includeIf`.
2025-03-15 13:10:24 -05:00
Michael Davis
9574e551cf commands: Allow zero or one arguments in :reflow
`:reflow` can optionally be passed a width.
2025-03-14 10:36:02 -04:00
Michael Davis
44bddf51b7 minor: Use parking_lot workspace dependency in helix-vcs
This is a follow-up from the parent commit - I accidentally didn't write
the buffer with this change before committing.
2025-03-13 12:43:33 -04:00
Michael Davis
b47c9da3a1 minor: Use a workspace dependency for parking_lot 2025-03-13 12:34:40 -04:00
VESSE Léo
fdaf12a35d feat(tlaplus) : added tlaplus config + grammar (#13081) 2025-03-13 08:59:17 -05:00
SadMachinesP86
0d84bd563c Fix Ruby highlights (#13055) 2025-03-13 08:48:13 -05:00
Michael Davis
1bd7a3901c queries: Add JSON injection for Rust json!({..}) macros
Note that this injection doesn't work currently because precedence is
not handled by the current syntax highlighter. The switch to tree-house
will properly handle the precedence of this pattern.
2025-03-12 17:46:17 -04:00
may
694b61514f queries: Inject into string content in Rust injections
This change also recognizes `RegexBuilder::new` calls for the regex
injection.
2025-03-12 17:31:50 -04:00
Michael Davis
7f416704b1 Fix precedence of JSON highlight queries for keys 2025-03-12 17:28:11 -04:00
Mikhail Katychev
430ce9c46b chore: Point OpenSCAD grammar to official repo (#13033) 2025-03-12 16:10:38 -05:00
iximeow
d1e0891260 warn when configured theme is unusable for color reasons (#13058)
if `config.toml` either does not have `editor.true-color` or sets
it to false, many (most?) themes stop being usable. when loading such a
theme, Helix falls back to the default theme, but didn't mention this
anywhere - even in `~/.cache/helix/helix.log` when run with `-v`.

if this occurs when reloading a theme at runtime with `:theme`, there's
a fairly helpful error about

> `theme requires true color support`

seems worth logging about this if it happens during startup too.
2025-03-12 15:52:07 -05:00
Michael Davis
e74956fa4d minor: Add a helper function for setting the configured theme
This block was duplicated in `Application::new` and in another helper
`Application::refresh_theme`. This change adds a helper to cover both
cases.
2025-03-12 16:32:52 -04:00
Chris44442
8d590e8aee update vhdl tree-sitter (#13091) 2025-03-12 09:47:37 -05:00
Egor Afanasin
63ed85bc62 Sunset theme: version 2.0 (#13086) 2025-03-12 09:39:55 -05:00
Constantin Angheloiu
9bd3cecd49 Update base16_transparent.toml ui.linenr (#13080) 2025-03-12 09:18:40 -05:00
Daniel Fichtinger
8df58b2e17 feat(ini): bumped grammar version to include support for global parameters (#13088) 2025-03-11 15:28:53 -05:00
dependabot[bot]
f9360fb27e build(deps): bump rustix from 0.38.44 to 1.0.2 (#13071)
* build(deps): bump rustix from 0.38.44 to 1.0.2

Bumps [rustix](https://github.com/bytecodealliance/rustix) from 0.38.44 to 1.0.2.
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Changelog](https://github.com/bytecodealliance/rustix/blob/main/CHANGES.md)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.44...v1.0.2)

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

Signed-off-by: dependabot[bot] <support@github.com>

* Drop unnecessary unsafe blocks for rustix Uid and Gid types

* Revert spurious downgrade of windows-sys

---------

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-03-11 09:34:08 -05:00
dependabot[bot]
88a254d8bf build(deps): bump cachix/install-nix-action from 30 to 31 (#13073)
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 30 to 31.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Commits](https://github.com/cachix/install-nix-action/compare/v30...v31)

---
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>
2025-03-11 08:49:18 -05:00
Jonathan Davies
c5c9e65cc4 Update install instructions (#13079) 2025-03-11 08:41:35 -05:00
dependabot[bot]
9db6c534a3 build(deps): bump cachix/cachix-action from 15 to 16 (#13074)
Bumps [cachix/cachix-action](https://github.com/cachix/cachix-action) from 15 to 16.
- [Release notes](https://github.com/cachix/cachix-action/releases)
- [Commits](https://github.com/cachix/cachix-action/compare/v15...v16)

---
updated-dependencies:
- dependency-name: cachix/cachix-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>
2025-03-11 08:30:17 -05:00
dependabot[bot]
ff558f9105 build(deps): bump the rust-dependencies group with 5 updates (#13070)
Bumps the rust-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [tempfile](https://github.com/Stebalien/tempfile) | `3.17.1` | `3.18.0` |
| [once_cell](https://github.com/matklad/once_cell) | `1.20.3` | `1.21.0` |
| [serde](https://github.com/serde-rs/serde) | `1.0.218` | `1.0.219` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.43.0` | `1.44.0` |
| [indexmap](https://github.com/indexmap-rs/indexmap) | `2.7.1` | `2.8.0` |


Updates `tempfile` from 3.17.1 to 3.18.0
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.17.1...v3.18.0)

Updates `once_cell` from 1.20.3 to 1.21.0
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.20.3...v1.21.0)

Updates `serde` from 1.0.218 to 1.0.219
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.218...v1.0.219)

Updates `tokio` from 1.43.0 to 1.44.0
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.43.0...tokio-1.44.0)

Updates `indexmap` from 2.7.1 to 2.8.0
- [Changelog](https://github.com/indexmap-rs/indexmap/blob/main/RELEASES.md)
- [Commits](https://github.com/indexmap-rs/indexmap/compare/2.7.1...2.8.0)

---
updated-dependencies:
- dependency-name: tempfile
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: indexmap
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-11 08:29:38 -05:00
Michael Davis
b38eae1f98 minor: Fix native line-ending handling in trimming integration tests 2025-03-10 11:09:46 -04:00
Michael Davis
67879a1e5b Avoid inserting final newlines in empty files
This matches the behavior described by the EditorConfig spec for its
`insert_final_newline` option:

> Editors must not insert newlines in empty files when saving those
> files, even if `insert_final_newline = true`.

Co-authored-by: Axlefublr <101342105+Axlefublr@users.noreply.github.com>
2025-03-10 10:38:11 -04:00
Michael Davis
aa20eb8e7f Add config for trimming trailing whitespace and newlines on write
These match the equivalent options in VSCode. `trim_trailing_whitespace`
is also the name used by EditorConfig.

* `trim-final-newlines` trims any extra line endings after the final one
* `trim-trailing-whitespace` trims any trailing whitespace (but not
  empty lines)
2025-03-10 10:18:55 -04:00
Michael Davis
ee9db440ce minor: Trim trailing whitespace in languages.toml 2025-03-10 10:18:55 -04:00
Lauri Gustafsson
296eb9be83 languages.toml: Change wgsl_analyzer to wgsl-analyzer (#13063)
The binary name was changed in wgsl-analyzer commit
4c56b1435d30cd45d8aee52297bbf68ed5bb3beb and released in 0.9.7.
2025-03-10 08:22:49 -05:00
suza
dc4761ad3a feat: Add SourcePawn language support (#13028) 2025-03-07 11:55:48 -06:00
Noel Cower
2d3b75a8c5 fix: render rulers before the cursor
Render rulers before the cursor to ensure that the cursor, when over
a ruler, is not hidden from view. Without this, you typically end up
with 1) foreground text that is the same as the background if the
ruler doesn't already have a foreground and 2) no visible cursor,
because the ruler's background color took precedence. By moving the
rulers before the cursor, this ensures that the theme is still rendered
more or less the way one would visually expect things to turn out.
2025-03-07 12:44:28 -05:00
Michael Davis
8da226f0b4 flake: Revert devShell linker to lld
`mold` does not appear to work on macOS as stated in the parent commit.
2025-03-07 12:06:33 -05:00
Michael Davis
fab08c0981 flake: Use mold for linking in devShell
Our `lld` was a bit out of date. Mold seems to be slightly faster
anyways and seems to work well on both Linux & macOS.
2025-03-07 09:41:48 -05:00
Michael Davis
b6e58c0fa4 flake: Split platform and common RUSTFLAGS in devShell
The `--no-rosegment` is not supported on macOS but the other flag
configurations can be used on both macOS and Linux.
2025-03-07 09:36:58 -05:00
Michael Davis
19558839b7 flake: Avoid setting HELIX_RUNTIME in devShell
The runtime directory should be correctly set without the need to set
HELIX_RUNTIME manually because we check for a runtime directory within
CARGO_MANIFEST_DIR.

This change also filters the runtime directory out of the source file
set passed to buildRustPackage since the runtime directory is not needed
at compilation time.
2025-03-06 18:40:35 -05:00
Michael Davis
c4d314d7ba LSP: Fix offset encoding test case
Co-authored-by: Isaac Mills <rooster0055@protonmail.com>
2025-03-06 17:13:38 -05:00
Daniel Fichtinger
b423ed42f1 feat: add harper-ls LSP configuration (#13029) 2025-03-06 15:59:22 -06:00
Michael Davis
a3fa65880e flake: Copy logo.svg in postInstall hook 2025-03-04 14:52:17 -05:00
Christopher Smyth
b1ee4ab5c6 Fix the git hash missing and add some more comments. (#13024) 2025-03-04 13:12:48 -06:00
Michael Davis
fbc0f956b3 minor: Move json deserialization into text_document_hover future
This follows a pattern used in the signature help request for example.
Moving the json deserialization into the return future of
`text_document_hover` makes the types easier for callers to work with.
2025-03-04 12:01:07 -05:00
Michael Davis
486f4297b7 Set cargoLock.allowBuiltinFetchGit in Nix package 2025-03-04 11:47:17 -05:00
Michael Davis
28e69f09fc direnv: Watch changes to default.nix
Now that the package definition lives in default.nix we need direnv to
watch that file to get automatic reloads.
2025-03-04 11:31:54 -05:00
Michael Davis
ab56f9e26b minor: Tweak some verbose LSP logs
The info log within `process_request_response` duplicated the body of
the JSON message printed earlier by the transport which was confusing.

The error log in the completion handler was easy to hit during normal
use and is not actually an error - dropping is the graceful way to
handle changes occurring while completion requests are in flight.
2025-03-04 11:25:11 -05:00
Christopher Smyth
1d453785e5 Clean up Nix Flake & make it easier to customize (#12831) 2025-03-04 10:23:28 -06:00
Erwin de Keijzer
671a6036b3 Add beans theme (#12963) 2025-03-04 10:03:46 -06:00
Alexander Brassel
82f8ac208f Improve %% escaping error message (#13018) 2025-03-04 10:03:11 -06:00
dependabot[bot]
9440feae7c build(deps): bump the rust-dependencies group with 11 updates (#13017)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-03-03 18:09:43 -06:00
luetage
1a28999002 Kanagawa: fix palette and attempt at a markdown compromise (#12895) 2025-03-02 11:06:59 -06:00
Michael Davis
0efa8207d8 Rewrite command line parsing, add flags and expansions (#12527)
Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2025-02-26 19:50:15 -06:00
may
e1c7a1ed77 remove unnecessary allocations in switch_case (#12786) 2025-02-26 19:03:29 -06:00
Michael Davis
7bebe0a70e Highlight file picker directories with 'ui.text.directory'
This applies the same styling as the parent commit to the file pickers.
2025-02-26 19:19:37 -05:00
Nik Revenco
682967d328 feat: Improve look of Global Search Picker (#12855)
Co-authored-by: Poliorcetics <poliorcetics@users.noreply.github.com>
Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-02-26 18:09:57 -06:00
Mykyta
1e8774a030 Added missing CSS highlight Tree Sitter Scopes (#12497) 2025-02-26 17:51:36 -06:00
Daniel Fichtinger
c36408457a feat(kdl): add kdlfmt as formatter for kdl (#12967) 2025-02-26 17:50:36 -06:00
Branch Vincent
1dd8a19ad6 Add pkl-lsp (#12962) 2025-02-26 17:45:10 -06:00
tshaynik
43eab10a4c languages.toml: add starpls as Starlark language server (#12958) 2025-02-26 17:43:16 -06:00
Daniel Fichtinger
83d4ca41cc feat: added comment textobject to toml (#12952) 2025-02-26 17:40:34 -06:00
SofusA
534d0907d3 Update c-sharp queries (#12948) 2025-02-26 17:40:16 -06:00
Daniel Fichtinger
bb3af143f1 feat: language support for mail files (#12945) 2025-02-26 17:33:36 -06:00
wcampbell
26cb3c20e7 Accept more scons as python language (#12943) 2025-02-26 17:31:23 -06:00
Bang Lee
69f25a85da Update languages.toml to add astro-ls (#12939) 2025-02-26 17:30:55 -06:00
Dmitriy Sokolov
8cb0d869e6 feat(lsp): add protobuf language servers (#12936) 2025-02-26 17:30:26 -06:00
David Vogt
c98302a543 feat(lsp): add container name as a column in the symbol pickers (#12930) 2025-02-26 17:28:34 -06:00
Roberto Vidal
0ba2e05a6f fix: escape percent character when yanking to search register (#12886)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-02-26 17:22:41 -06:00
Michael Davis
e1060a2785 queries: Fix precedence in Erlang highlights 2025-02-26 10:55:00 -05:00
Michael Davis
fcddd50325 Set theme before opening documents
This is not consequential now but when we switch to the new highlighter
we will want the theme to be set (and the loader's `scopes` to be set
based on the theme) before parsing a document. Previously `set_theme`
came after the loading of documents, so documents would be missing
locals highlights after being loaded and before the first edit.
2025-02-26 10:49:36 -05:00
dependabot[bot]
35575b0b0f build(deps): bump the rust-dependencies group with 6 updates (#12956)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-24 18:05:24 -06:00
Michael Davis
7e87a36e93 queries: Fix module highlights in koto 2025-02-24 12:49:43 -05:00
Michael Davis
6182bdc860 xtask: Inline query and theme checks into main module
This reverts cosmetic changes from <https://redirect.github.com/helix-editor/helix/pull/3234>:
that PR split the tasks into separate modules but the query and theme
check tasks are small enough that splitting them into separate files (or
modules) adds unnecessary friction.

This change also adds `theme-check` to the help message for the xtask
crate.
2025-02-24 10:51:28 -05:00
Michael Davis
e1c26ebfc7 queries: Reverse precedence for git-config highlights 2025-02-24 10:41:09 -05:00
Michael Davis
3683cd9ea3 queries: Remove unknown predicates 2025-02-22 14:26:41 -05:00
Sebastian Dörner
0deb8bbce6 Add indents and textobjects for Kotlin (#12925) 2025-02-20 11:58:27 -06:00
Michael Davis
3d7e2730e7 Read language servers from config in :lsp-restart
`:lsp-stop` should consider only the set of active language servers for
a document. `:lsp-restart` though may be used to start up a language
server that crashed or was manually stopped, so it needs to consider the
language servers in config instead.

This change inlines the `valid_lang_servers` function into `:lsp-stop`
and `:lsp-restart` and changes `:lsp-restart` to check the doc's config
rather than active language servers. `:lsp-restart` now also does not
need to clone the language server names as strings since it borrows from
the config and arguments rather than `Document`. The completer has also
been split into two - one matching active language servers, used by
`:lsp-stop`, and the other matching configured language servers, used by
`:lsp-restart`.

This also removes the part of `:lsp-restart` which bailed if a language
server failed to be restarted (for example because it is not installed).
There might be multiple language servers configured for a language and
only one installed. In that case the `:lsp-restart` should be considered
successful even if not all servers could be started. Bailing prevented
any language servers which could start from being attached to the
document. Instead errors are collected and emitted at the end.
2025-02-20 12:50:09 -05:00
J. Dekker
6304e7b2a7 languages/xml: add mpd & smil extensions (#12916) 2025-02-19 10:58:29 -06:00
Michael Davis
d031260180 Avoid cloning configured env vars when starting a language server
The clone of the hashmap can be avoided by passing a ref instead. This
commit also changes the `server_environment` type to match the bounds
expected by `Command::envs` - this will avoid future refactoring if the
underlying type changes (for example switching to `hashbrown::HashMap`).
2025-02-19 10:30:06 -05:00
Michael Davis
e0da129727 Use custom titles for register select info boxes
Previously all register selection info boxes had "Registers" as the
title. That was particularly confusing for `copy_between_registers`
which presents two info boxes back-to-back.
2025-02-19 10:29:15 -05:00
Michael Davis
b8912adbbf Use a Cow<'static, str> for the Info component title
Some uses of the component (like for register) provide a static title.
We can trivially avoid the title allocation in those cases.
2025-02-19 10:10:55 -05:00
oxcrow
1c0b36b1b4 Improve jump label colors for github_light theme (#12907) 2025-02-18 08:40:42 -06:00
Michael Davis
e35d420199 application: Eliminate duplicate theme and syntax loader clones
The application held onto these since their introduction in ce97a2f0 but
the Arcs are duplicated between Application and Editor - we can store it
only on Editor without issue.
2025-02-17 19:01:54 -05:00
dependabot[bot]
48194825b9 build(deps): bump the rust-dependencies group with 3 updates (#12903)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-17 17:36:50 -06:00
Michael Davis
82f07fe6d1 Migrate helix-event to foldhash
This is following `hashbrown`'s switch in v0.15 from ahash to foldhash
for its `default-haster` feature, applied only to helix-event for now.

I don't have a strong preference between the two. Benchmarks in
Spellbook, which is particularly sensitive to hashers and hash table
performance, show no perceptible difference. Foldhash is dependency-free
though.

Once we migrate to the new tree-sitter bindings and highlighter we
should be able to eliminate the remaining dependencies on ahash.
2025-02-17 17:35:00 -05:00
Mike Boutin
1c47aec30c Improve onedarker theme contrast cursorline/selection (#12833) 2025-02-17 13:04:15 -06:00
Nik Revenco
ef375d690e feat: highlight rust repetition pattern (#12871)
Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-02-17 12:54:49 -06:00
Axlefublr
0445062d27 fix :yank-joined disrespecting default-yank-register option (#12890) 2025-02-17 10:06:29 -06:00
Abderrahmane TAHRI JOUTI
46728046fd Cyan Theme : fix popup not having any background (#12891) 2025-02-17 10:05:15 -06:00
dependabot[bot]
7275b7f850 build(deps): bump pulldown-cmark from 0.12.2 to 0.13.0 in the rust-dependencies group (#12865)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-13 08:31:38 -06:00
Michael Davis
ed3bc2b294 Remove unused dependencies
The dependabot file was matching on tree-sitter crates - that's a relic
from v0.6.0 and lower where grammars were regular dependencies.

The remaining changes are unused crates that were forgotten about during
shuffles like moving path canonicalization from helix-core to
helix-loader (and then again from helix-loader to helix-stdx).
2025-02-13 08:41:46 -05:00
Abderrahmane TAHRI JOUTI
3ccf8d58de Cyan light UI grays and directory prompt (#12864) 2025-02-13 07:21:37 -06:00
RoloEdits
efb44e0b22 feat(sql): update tree-sitter files (#12837) 2025-02-13 07:16:27 -06:00
Michael Davis
144a4f402f queries: Fix html highlight precedence ordering 2025-02-12 20:58:09 -05:00
Harishankar G
df752bbd45 Prevent auto-format in auto-save (#12817)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-02-12 12:11:01 -06:00
T. Li
d8c4c7c26f fix: check and print remaining language servers (#12841) 2025-02-12 09:57:25 -06:00
Michael Davis
c3e9a0d607 Replace 'pkgbuild-language-server' with 'termux-language-server'
`pkgbuild-language-server` no longer exists and the PKGBUILD
functionality has moved to `termux-language-server`.
2025-02-12 10:51:07 -05:00
Jean-Louis Fuchs
258e3e1136 feat: Add support for the Ink programming language (#12773) 2025-02-12 09:46:50 -06:00
Michael Davis
5a66270c00 Remove typst-lsp config
typst-lsp has been deprecated in favor of tinymist.
2025-02-12 10:39:10 -05:00
Roberto Vidal
6aa82bb3f8 mark xsl files as XML (#12834) 2025-02-11 09:09:53 -06:00
dependabot[bot]
518d054fcb build(deps): bump the rust-dependencies group with 4 updates (#12832)
Bumps the rust-dependencies group with 4 updates: [once_cell](https://github.com/matklad/once_cell), [toml](https://github.com/toml-rs/toml), [cc](https://github.com/rust-lang/cc-rs) and [which](https://github.com/harryfei/which-rs).


Updates `once_cell` from 1.20.2 to 1.20.3
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.20.2...v1.20.3)

Updates `toml` from 0.8.19 to 0.8.20
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.19...toml-v0.8.20)

Updates `cc` from 1.2.11 to 1.2.13
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Changelog](https://github.com/rust-lang/cc-rs/blob/main/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/cc-rs/compare/cc-v1.2.11...cc-v1.2.13)

Updates `which` from 7.0.1 to 7.0.2
- [Release notes](https://github.com/harryfei/which-rs/releases)
- [Changelog](https://github.com/harryfei/which-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/harryfei/which-rs/compare/7.0.1...7.0.2)

---
updated-dependencies:
- dependency-name: once_cell
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: which
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-11 14:53:29 +09:00
Matthew Mark Ibbetson
35faa73be1 Add Djot support (#12562) 2025-02-10 15:36:01 -06:00
Abhi
7a3470c48d Add support for yara language (#12753) 2025-02-10 15:32:28 -06:00
Nikita Revenco
199dc05a04 fix: Align Markdown styles with tree sitter highlights (#12696)
Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-02-10 14:58:39 -06:00
Michael Davis
5e2501da30 Reapply "Re-enable Hare by default (#11507)"
This reverts commit 151caeacc6.
2025-02-10 15:51:50 -05:00
Milo Moisson
a03becf021 nix: add indent TS query (#12829)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-02-10 14:47:31 -06:00
Kristoffer Plagborg Bak Sørensen
a6f94e04e6 feat: add mising pkgs.writers.write* nix tree-sitter injections (#12774) 2025-02-10 14:46:53 -06:00
Kristoffer Plagborg Bak Sørensen
2197b3cfa0 feat: add mising builtins.fromTOML nix tree-sitter injection (#12776) 2025-02-10 14:46:23 -06:00
Jaakko Paju
a19c95a0a7 Add CSV language and syntax highlighting (#11973) 2025-02-10 10:51:06 -06:00
Xubai Wang
ff012e844f Fix Bash completion space regression (#12828) 2025-02-10 10:42:45 -06:00
Poliorcetics
fcfa70e66c just: bump grammar support to handle more kind of shebang injections (#12818) 2025-02-10 09:55:27 -06:00
Nikita Revenco
1b89f998e8 fix: Rust highlights (regression from the reverse-query-precedence PR) (#12795)
Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-02-07 08:51:22 -06:00
Jonas Köhnen
c36ed6ad92 feat(themes): add ui.text.directory to gruber-darker (#12797) 2025-02-06 09:31:36 -06:00
Michael Davis
18b9eb9e06 Update tree-sitter-erlang
This is for packaging reasons, previously the license identifier in
`package.json` accidentally listed MIT instead of Apache-2.0
2025-02-05 20:22:38 -05:00
Gabriel Dinner-David
b0e1eaf50d reverse zig highlights (#12777) 2025-02-04 20:38:15 -06:00
Drew Zemke
a36730cb21 add support for the FGA language (#12763) 2025-02-04 11:06:22 -06:00
uncenter
75abc23428 Add Tera templating language support (#12756) 2025-02-04 10:56:36 -06:00
Michael Davis
313a6479b1 LSP: Properly discard out-of-date diagnostics
Previously the `filter` caused the diagnostics to not be attached to the
document - which is good - but the out-of-date diagnostics were still
inserted into the global (editor-wide) diagnostic set. Instead we should
completely discard out-of-date diagnostics.
2025-02-04 10:39:49 -05:00
Michael Davis
62625eda46 LSP: Move diagnostic handling from Application to Editor
There is no functional change to the move - it's just moving the code
into helix-view under a new method `Editor::handle_lsp_diagnostics` -
thought there is a typo fix, the removal of an unnecessary clone (for
the document's language config) and the removal of some nesting.

Co-authored-by: Sofus Addington <sofus@addington.dk>
2025-02-04 10:39:42 -05:00
Michael Davis
16ff06370f queries: Remove (ERROR) from all highlights
We do not highlight `(ERROR)` nodes since the highlighting is quite
noisy while typing. Also see todo comments in `syntax.rs` - we could
introduce configuration in the future to prepend `(ERROR)` to a
language's highlights query.
2025-02-04 09:35:38 -05:00
Robin Heggelund Hansen
ee33a84489 Update highlights.scm for Gren language (#12769) 2025-02-04 08:34:59 -06:00
Niklas Wallgren
1258111394 Print full error chain when failing to load grammar (#12744) 2025-02-04 08:18:54 -06:00
Michael Davis
26db54155e DAP: Drain pending requests on recv failure
This matches <https://redirect.github.com/helix-editor/helix/pull/4852>
for the DAP transport: when there is a failure to receive a message from
the debugger we should drain all pending requests and respond to them
with the StreamClosed error.

This improves the behavior when a debugger fails to initialize, for
example starting debugpy without debugpy installed. Previously the UI
would freeze until the request timed out. Now it instantly prints a
statusline error saying that the debugger failed to start up.
2025-02-04 09:09:54 -05:00
Michael Davis
d456377821 minor: Remove double BufReader wrapper in DAP client
`reader` is already a `BufReader` so there's no need to wrap it in
another `BufReader`. This is a typo/mistake made possible by the type
erasure (a `Box<BufReader<BufReader<T: Read>>>` is also a boxed reader).
2025-02-04 08:52:09 -05:00
Michael Davis
d0d16931e3 DAP: Configure child process stderr as piped
By default this is `Stdio::inherit` which sends stderr from the child
process to Helix. Instead we should use `Stdio::piped` which allows us
to read the piped output.

We can also expect that the stderr opens now (it should similarly to
stdout), so that we always start a reader for stderr like the LSP
client.
2025-02-04 08:51:46 -05:00
dependabot[bot]
8995ccaae2 build(deps): bump the rust-dependencies group with 4 updates (#12766)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-02-03 17:37:46 -06:00
Michael Davis
066e938ba0 Add copy_between_registers for interactive register copying 2025-02-02 20:49:25 -05:00
Michael Davis
e882a750ea commands: Eagerly clear autoinfo in select_register, insert_register
This causes the infobox to disappear even when you type a non-character
key like escape. For example `"<esc>` now clears the infobox where
before it was left hanging.
2025-02-02 20:45:25 -05:00
Michael Davis
ebdab86ce6 minor: Prefer stable core::num::abs_diff to polyfill
This function was made stable in Rust 1.60.0 so we no longer need to
polyfill.
2025-02-02 20:42:55 -05:00
jack
ab6a92ed49 update(theme): add virtual-inlay hint highlight to snazzy theme (#11089) 2025-02-02 19:02:48 -06:00
Viktor Szépe
e22bbf5489 Fix typos (#12690) 2025-02-02 18:58:29 -06:00
Leo Unglaub
0ab403d428 Add block comment configuration for PHP 2025-02-02 19:37:13 -05:00
Michael Davis
b8bfc44e42 queries: Improve Rust const generic and '_' type highlighting
You may pass constants as type arguments the const generics feature.
This is used in spellbook for example as a poor man's enum, for example
`self.strip_suffix_only::<FULL_WORD>(word, hidden_homonym)`. With this
change that `FULL_WORD` part is highlighted as a constant instead of
a type.

This change also highlight the underscore in type placeholders - this
is similar to the highlighting done for bindings in Elixir or Erlang
for example. In `Vec<_>` the underscore is highlighted the same as a
comment.
2025-02-02 19:28:01 -05:00
Michael Davis
5952d564d1 Reverse highlight precedence ordering (#9458)
Co-authored-by: postsolar <120750161+postsolar@users.noreply.github.com>
Co-authored-by: Iorvethe <58810330+Iorvethe@users.noreply.github.com>
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
Co-authored-by: gabydd <gabydinnerdavid@gmail.com>
2025-02-02 18:17:10 -06:00
Michael Davis
382401020c queries: Add 'not-eq' and 'not-match' predicates to TSQ highlights 2025-02-02 18:39:07 -05:00
Michael Davis
93fa990e56 queries: Fix module/namespace highlight in Unison 2025-02-02 18:39:07 -05:00
Michael Davis
70d452db3e core: Make completion item documentation optional
Path completion items always have documentation but future core (i.e.
non-LSP) completions may not always have documentation - for example
word completion from the current buffer.
2025-02-01 21:24:25 -05:00
Michael Davis
369f2bb93d ui: Expose the 'prompt' module
The prompt Completion type alias is otherwise private. This will be
used in <https://redirect.github.com/helix-editor/helix/pull/12527>
to refactor some functions to return prompt completions.
2025-02-01 21:12:20 -05:00
Doug Kelkhoff
0f594c35f2 feat(lang:r): Add roxygen header comment token (#12748) 2025-02-01 19:44:07 -06:00
Remo Senekowitsch
de11273857 Document installation of rust-analyzer via rustup (#12618) 2025-02-01 19:43:20 -06:00
Pascal Kuthe
5c1f3f814f implement incomplete completion requests 2025-02-01 19:36:10 -05:00
Pascal Kuthe
4e0fc0efc6 Add a completion handler type in helix-view for tracking responses
This will replace the `Sender<CompletionEvent>` in the child commits.
It tracks sender alongside extra metadata about the responses received
from providers - namely whether a request is incomplete or not - which
can be reused between subsequent requests to the provider.
2025-02-01 19:35:58 -05:00
Michael Davis
1ab35ade2d minor: Move CompletionEvent to a new completion handler module
Completions are not specific to LSP anymore. In the child commits we
will expand on the types in this module so this refactor is done
eagerly to minimize changes later.
2025-02-01 19:32:37 -05:00
Pascal Kuthe
018081a5b1 core: Add a provider type to track the origin of a completion 2025-02-01 19:32:37 -05:00
Michael Davis
f0fa905622 LSP: Eagerly send requests in Client::request
This is a similar change to the parent commit but for `request`. The
requests should be sent eagerly so that the ordering stays consistent.

Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2025-02-01 19:32:37 -05:00
Michael Davis
5532ef35d9 LSP: Remove future wrapper from Client::notify, Client::reply
Previously LSP notifications were sent within a future and most callers
used a `tokio::spawn` to send the notification, silently discarding any
failures like problems serializing parameters or sending on the channel.
It's possible that tokio could schedule futures out of intended order
though which could cause notifications where order is important, like
document synchronization, to become partially shuffled. This change
removes the future wrapper and logs all internal failures.

Also included in this commit is the same change for `Client::reply`
which was also unnecessarily wrapped in a future.

Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2025-02-01 19:32:37 -05:00
Pascal Kuthe
0ea401d2d7 Use the slotmap workspace dependency in helix-view
This workspace dependency is already used in `helix-core` and
`helix-lsp`. This change uses it in `helix-view` as well for
consistency.
2025-02-01 19:32:23 -05:00
uncenter
e70f8833e2 Highlight $ template literals as shell commands (#12751) 2025-02-01 18:18:08 -06:00
Kristoffer Plagborg Bak Sørensen
30616344d7 Recognize .sublime-* files (#12750) 2025-02-01 17:44:06 -06:00
rhogenson
17ffa38a5a Use the first char in a grapheme for classification (#12483)
Co-authored-by: Rose Hogenson <rosehogenson@posteo.net>
2025-02-01 17:09:45 -06:00
Michael Davis
c3620b7116 Join input and wait tasks in external formatter Tokio command
This matches the layout of `shell_impl_async` in `commands.rs` and
avoids a hang or maybe deadlock in `to_writer`'s calls to
`tokio::io::AsyncWriteExt::write_all`. I don't really understand the
underlying cause of the hang but it seems it's necessary to spawn a
new tokio task to provide input to stdin. This is shown in an example
in `tokio::process::Child::wait` but not documented explicitly.
2025-02-01 10:58:03 -05:00
Michael Davis
e9c16b7fc5 Use typable command doc when keybind provides no arguments
This improves the display of the keymap popup for example, so that if
you bind a key like `C-x = ":buffer-close"` under the `<space>` menu,
the infobox shows "Close the current buffer." rather than `:buffer-close
[]`.
2025-02-01 09:10:04 -05:00
Michael Davis
8439ce5683 Hover UI: Eagerly convert hover response to Markdown
This simplifies the hover component by eagerly converting all
`lsp::Hover` responses into `Markdown`s. Previously we cached the
current `Markdown` and created a new `Markdown` when switching the
active response. Instead we can consume the `lsp::Hover` and avoid some
clones of its inner types.
2025-01-31 17:34:56 -05:00
Nikita Revenco
6edff24c81 fix: add comment token for svelte files (#12743)
Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-01-31 15:13:09 -06:00
Michael Davis
47f84d04ea Set a statusline error for formatter errors in :format 2025-01-31 14:07:22 -05:00
Michael Davis
2367b20318 Remove popup_border calculations from LSP Hover UI component
The Hover component is used as the inner contents of a Popup. The Popup
should be doing calculations based on whether popup_borders is
configured and not Hover. This fixes an issue with hover rendering when
the popup border option is enabled for popups.

Fixes #12742
2025-01-31 12:01:33 -05:00
Michael Davis
28047fed7f config: Deny unknown fields in [editor.smart-tab]
Previously a typo like "enabled" would silently be discarded. Instead
we should error when a field is configured which doesn't exist.

Fixes #12739
2025-01-31 08:34:30 -05:00
RoloEdits
025719c1d8 perf(ropey): enable simd feature for stdx (#12735) 2025-01-30 19:51:34 -05:00
renshyle
80dbe030a1 Do not record keys pressed by macros while recording a macro (#12733)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-01-30 18:14:04 -06:00
John Kelly
6906164177 Properly prevent crossterm features being used when feature = "term" not enabled in helix-view (#12734) 2025-01-30 17:38:07 -06:00
Michael Davis
d285a8a9e5 LSP: Fix option handling in goto definition/references commands
The language server may return `None` for a definition/reference
request. The parent commits introduced a regression for these commands
when a server did not provide locations. With this change a server may
respond with `null` and its locations will instead not be considered.

Fixes #12732
2025-01-30 11:59:02 -05:00
Michael Davis
1a821ac726 LSP: Support multiple langauge servers for goto references
This refactors goto_reference like the parent commit to query all
language servers supporting the feature.
2025-01-30 10:37:01 -05:00
Michael Davis
f7394d53fd LSP: Support multiple language servers for goto definition
This covers all goto-definition-like commands: declaration, definition,
type definition and implementation.

Closes #11689

Co-authored-by: j <junglerobba@jngl.one>
2025-01-30 10:36:03 -05:00
Michael Davis
ba116b47a0 LSP commands: Move offset encoding onto the Location type
<https://github.com/helix-editor/helix/pull/11486> introduced a Location
type in the LSP commands module which unified helpers like
`jump_to_location`. This change moves `OffsetEncoding` onto that type.

`SymbolInformationItem` and `PickerDiagnostic` already had fields for
carrying the offset encoding. We would want a similar setup for goto
definition/references as well (for supporting multiple language servers
with that feature) but those use the `Location` type. By moving
`OffsetEncoding` onto `Location` we make future changes to allow
mulitple language servers possible for goto definition/references
features and also simplify some calls for symbols and diagnostics.
2025-01-29 17:25:12 -05:00
Michael Davis
c9dc940428 Fix byte/char indexing mix-up in path completion
The positions passed to `Transaction::change_by_selection` should be
character indexes. `edit_diff` is meant to track the number of
characters that should be deleted to erase the file name that has been
typed so far (if any). Mistakenly this was using `str::len` which is
the byte count. This fixes a bug that could cause more text to be
deleted than intended or a panic when completing a directory with
multi-byte characters like 'éclair'.

This change also moves the `edit_diff` binding out of the loop since
it's now performing some non-trivial work (counting characters, where
before it was just accessing the pre-computed number of bytes).
2025-01-29 10:05:21 -05:00
Gareth Widlansky
8328c422b7 Add ghostty configuration support (#12703) 2025-01-29 08:56:08 -06:00
0xLucqs
6049f2035b chore(grammar): update cairo + queries (#12712) 2025-01-28 08:19:33 -06:00
Gabriel Dinner-David
8d6efaf350 fix zig highlight query use of #lua-match (#12708) 2025-01-28 00:19:43 -05:00
dependabot[bot]
98ddbf0086 build(deps): bump the rust-dependencies group with 2 updates (#12707)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2025-01-27 17:19:26 -06:00
Zi How Poh
0c8f0c0334 goto_diag: avoid wraparound by default (#12704) 2025-01-27 14:41:34 -06:00
Michael Davis
fec5101a41 DAP: Refactor handling of Event and Request protocol types
This change refactors the DAP `Event` type, the `helix_dap` module and
the `helix_dap::transport` module to be closer to the equivalent
implementations in `helix_lsp`. The DAP `Event` type is similar to LSP's
`Notification` so this change rewrites the enum as a trait which is
implemented by empty types (for example `enum Initialized {}`).

`Event` is then reintroduced as an enum in `helix_dap` with a helper
function to convert from the `Event` as the transport knows it. By
separating the definitions of `Event` between lib and transport, we can
handle incoming events which are not known to our `Event` enum. For
example debugpy sends `"debugpySockets"` which is unknown. With this
change, unknown events are discarded with an info-level log message.

The `Request` type was already a trait but did not have an enum with the
known values. This change also introduces a `helix_dap::Request` enum
similar to `helix_dap::Event`. This resolves a TODO comment about
avoiding `unwrap` when parsing the request's arguments.
2025-01-27 15:27:35 -05:00
Michael Davis
9bc63c1c59 DAP: Move module ID tests out of events module
These were mistakenly added to the events module, they should be part of
the types module.
2025-01-27 15:25:22 -05:00
Michael Davis
20151a5594 Move rope grapheme iterators from core to stdx 2025-01-27 09:24:40 -05:00
Michael Davis
51832b02c9 core: Remove unused byte index grapheme functions 2025-01-27 09:24:40 -05:00
Michael Davis
39b72329b4 stdx: Add floor/ceil/is grapheme boundary functions to RopeSliceExt
These functions are the equivalent of 23b424a46 for grapheme clusters.
In order to add the `is_grapheme_boundary` function we also need to
query whether a byte index lies on a character boundary, so this change
also adds `is_char_boundary`.
2025-01-27 09:24:40 -05:00
Michael Davis
0364521dca goto_word: Skip keys with modifiers in both on-next-key blocks 2025-01-27 09:24:40 -05:00
Michael Davis
f5f9f499cf goto_word: Reject jump label characters with modifiers
Previously you could use `<A-a><A-b>` to jump to a label "ab". We should
not treat characters with modifiers the same as characters without.
With this change the `<A-a>` input exits out of the jumping on-next-key.

Fixes #12695
2025-01-27 08:49:03 -05:00
Poliorcetics
b00b475dfe just: bump grammar support to Just 1.39.0 (#12692) 2025-01-26 20:10:27 -06:00
kyfanc
9829ac0c02 Cycle through hover results from multiple language servers (#10122)
Co-authored-by: Vladyslav Karasov <36513243+cotneit@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-01-26 11:24:50 -06:00
Kristoffer Plagborg Bak Sørensen
7c907e66f4 feat: print helpful suggestion when using :{,r}sort incorrectly (#12585) 2025-01-26 10:39:19 -06:00
Kristoffer Plagborg Bak Sørensen
259be07f05 feat: add asm-lsp for assembly dialects (#12684) 2025-01-26 10:28:40 -06:00
Michael Davis
360c6bb061 stdx: Replace RopeSliceExt::byte_to_next_char with ceil_char_boundary
The new `RopeSliceExt::ceil_char_boundary` from the parent commits can
be used to implement `RopeSliceExt::byte_to_next_char` when used with
`RopeSlice::byte_to_char`. That function had only one caller and that
caller will eventually disappear when we switch to Ropey v2 and drop
character indexing, so we can drop `byte_to_next_char` now and replace
its caller with `byte_to_char` plus `ceil_char_boundary`.

This change keeps the unit tests for `byte_to_next_char` and checks them
against a polyfill of `byte_to_char` plus `ceil_char_boundary` to ensure
that `byte_to_next_char`'s intended behavior is not changed.
2025-01-26 11:11:53 -05:00
Michael Davis
4919058e90 Use RopeSliceExt floor/ceil functions for goto_file_impl search cap
This is a good example use-case of the `floor_char_boundary` and
`ceil_char_boundary` functions added in the parent commit. In the
single-width, single-selection case in `goto_file` we cap the search
to either the current line or 1000 bytes before or after the cursor
(whichever case comes earlier). That byte index might not lie on a
character boundary so it needs to be fixed to either the prior or
later boundary.
2025-01-26 11:10:50 -05:00
Michael Davis
23b424a46d stdx: Add floor/ceil char boundary functions to RopeSliceExt
These functions mimic `str::floor_char_boundary` and
`str::floor_char_boundary` (currently unstable under
`round_char_boundary`). They're useful for correcting a byte index
which may not lie on a character boundary. For example you might limit
a search within a slice to some fixed number of bytes. The fixed number
might not lie on a boundary though so it needs to be corrected to
either the earlier (floor) or later (ceil) boundary.
2025-01-26 11:10:24 -05:00
Joel Dueck
aac0ce5fd1 Update install.md: fix link to lang server install instructions (#12675) 2025-01-26 14:21:13 +09:00
Michael Davis
899afad4a6 flake: Revert update of nixpkgs 2025-01-25 13:52:14 -05:00
magackame
3fdd98979c fix: goto_file_impl incorrect use of slice instead of byte_slice (#12673) 2025-01-25 12:38:35 -06:00
Alexis Mousset
de738bac6a Small refinements for modus themes (#12670) 2025-01-25 20:30:43 +09:00
Darshan Kumawat
81708b70e6 doc: Document mdm and mrm for popup help (#12650) 2025-01-24 11:46:19 -06:00
Michael Davis
8bf9adf7b6 Update tree-sitter-elixir 2025-01-24 12:37:48 -05:00
Kevin Danne
9088f8a599 fix: HELIX_RUNTIME environment path for windows on building-from-source book page (#12658)
Co-authored-by: Kevin Danne <kevin.danne@triluxds.com>
2025-01-24 09:03:49 -06:00
Nikita Revenco
a63a2ad281 feat: specify custom lang server(s) for :lsp-stop and :lsp-restart (#12578)
Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-01-23 18:14:35 -06:00
RoloEdits
4ded712dbd perf(syntax): short-circuit if name matches language_id (#12407) 2025-01-23 17:49:14 -06:00
Michael Davis
151caeacc6 Revert "Re-enable Hare by default (#11507)"
This reverts commit 2c09a35ccf.

Temporarily reverting this to unblock any builds during SourceHut's
ongoing DDoS: <https://status.sr.ht/issues/2025-01-23-git.sr.ht-ddos/>
2025-01-23 18:43:13 -05:00
Michael Davis
d4ade40983 Rename "file browser" => "file explorer"
Connects #11285
2025-01-23 18:17:56 -05:00
Michael Davis
0b9701e899 tui buffer: Handle multi-width graphemes in set_string_anchored
We should skip zero-width graphemes and reset only long (more than
1-width) graphemes.

Connects #12036
2025-01-23 18:12:20 -05:00
Michael Davis
9d6ea773e9 prompt: Cap anchor to line length in cursor calculation
This prevents a panic when using `C-w` on a long single-word prompt line
for example.

Connects #12036
2025-01-23 17:33:28 -05:00
Denys Rybalka
6b044aeb29 Add file browser (#11285) 2025-01-23 16:28:18 -06:00
Yomain
8af33108f6 fix: better display of prompts on long inputs (#12036) 2025-01-23 15:56:34 -06:00
Michael Davis
1afa63d457 rust: Highlight / and ! within comments as comments 2025-01-23 16:17:44 -05:00
Khang Nguyen Duy
5f62c5c24c Update to more up-to-date zig tree-sitter repo (#11980)
Co-authored-by: Khang Nguyen Duy <iceghost@users.noreply.github.com>
Co-authored-by: Khang Nguyen Duy <os@ndykhang.net>
2025-01-23 14:36:24 -06:00
TornaxO7
fa27ae16a7 Add path completion for multiple cursors (#12550) 2025-01-23 14:31:12 -06:00
Michael Davis
8986f8b953 Update nix flake inputs
$ nix flake update
    • Updated input 'crane':
        'github:ipetkov/crane/37e4f9f0976cb9281cd3f0c70081e5e0ecaee93f?narHash=sha256-WD0//20h%2B2/yPGkO88d2nYbb23WMWYvnRyDQ9Dx4UHg%3D' (2024-10-03)
      → 'github:ipetkov/crane/849376434956794ebc7a6b487d31aace395392ba?narHash=sha256-GLJvkOG29XCynQm8XWPyykMRqIhxKcBARVu7Ydrz02M%3D' (2025-01-22)
    • Updated input 'flake-utils':
        'github:numtide/flake-utils/c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a?narHash=sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ%3D' (2024-09-17)
      → 'github:numtide/flake-utils/11707dc2f618dd54ca8739b309ec4fc024de578b?narHash=sha256-l0KFg5HjrsfsO/JpG%2Br7fRrqm12kzFHyUHqHCVpMMbI%3D' (2024-11-13)
    • Updated input 'nixpkgs':
        'github:nixos/nixpkgs/bc947f541ae55e999ffdb4013441347d83b00feb?narHash=sha256-NOiTvBbRLIOe5F6RbHaAh6%2B%2BBNjsb149fGZd1T4%2BKBg%3D' (2024-10-04)
      → 'github:nixos/nixpkgs/9e4d5190a9482a1fb9d18adf0bdb83c6e506eaab?narHash=sha256-nmKOgAU48S41dTPIXAq0AHZSehWUn6ZPrUKijHAMmIk%3D' (2025-01-21)
    • Updated input 'rust-overlay':
        'github:oxalica/rust-overlay/25685cc2c7054efc31351c172ae77b21814f2d42?narHash=sha256-lJMFnMO4maJuNO6PQ5fZesrTmglze3UFTTBuKGwR1Nw%3D' (2024-10-07)
      → 'github:oxalica/rust-overlay/38374302ae9edf819eac666d1f276d62c712dd06?narHash=sha256-S2rHCrQWCDVp63XxL/AQbGr1g5M8Zx14C7Jooa4oM8o%3D' (2025-01-23)
2025-01-23 15:18:37 -05:00
Rolo
650af50c13 fix: typos 2025-01-23 15:18:16 -05:00
Rolo
c1d382a532 fix(lints): clippy 1.84 2025-01-23 15:18:16 -05:00
Nikita Revenco
168b11e091 feat: passing multile of the same files in the arguments places a cursor at each position (#12192)
Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-01-23 14:04:02 -06:00
uncenter
f70923c448 Restrict tagged template language injections for ecma (#12217) 2025-01-23 13:45:35 -06:00
Branch Vincent
8f1585a097 Add ghostty/config and hgrc file types 2025-01-23 14:23:48 -05:00
Branch Vincent
0d5f6f04c9 Add fish-lsp and bump tree-sitter-fish 2025-01-23 14:23:48 -05:00
Roman Frołow
122bbea7cf tutor: flips selections -> flips direction of selection (#12638) 2025-01-23 13:22:28 -06:00
Roman Frołow
088ba58af5 docs: force creating symbolic link if it exists (#12637) 2025-01-23 13:17:06 -06:00
Remo Senekowitsch
ce348d84f6 book: Document language.rulers config option (#12627) 2025-01-23 13:16:37 -06:00
Remo Senekowitsch
def6139abd docs: Fix broken link (#12623) 2025-01-23 13:15:54 -06:00
Fraser Li
cf7b36f0bf Add beancount-language-server (#12610) 2025-01-23 13:14:10 -06:00
robb
c81e0136c5 Update languages.toml to fix cl-lsp (#12615) 2025-01-23 13:13:30 -06:00
Rock Boynton
dca235c5c8 Update tree-sitter-rust (#12607)
Co-authored-by: Rock Boynton <rboynton@anduril.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-01-23 13:11:07 -06:00
Michael Davis
fcf981bbd7 Recognize bun.lock as JSONC
Fixes #12651
2025-01-23 14:03:10 -05:00
Jose Alvarez
d123193902 Replace current file when loading in background from picker (#12605) 2025-01-23 12:49:20 -06:00
Pig Fang
430414979d add language server for wat (#12581) 2025-01-23 12:46:15 -06:00
FITAHIANA Nomeniavo joe
6593969f8d add ruby-lsp as candidate for the ruby lsp (#12511) 2025-01-23 12:09:46 -06:00
Michael Davis
cb0f201d0e snippets: Discard placeholder text for the $0 tabstop 2025-01-23 09:50:19 -05:00
Michael Davis
032dadaf37 snippets: Add a test case for parsing ${0:placeholder}
This is an example snippet sent by older versions of clangd.
2025-01-23 09:50:19 -05:00
Michael Davis
7dea2b0ddd CI: Cache tree-sitter grammars in all jobs
This change adds tree-sitter grammar caching to Check, Lints and Docs
jobs which all previously downloaded grammars in the `helix-term` build
script fresh per job. This should increase reliability and mitigate the
effects of an ongoing SourceHut outage
(<https://status.sr.ht/issues/2025-01-23-git.sr.ht-ddos/>).

This is also a nice speed boost for these jobs:

| Job name | Example time before | Example time after |
|---       |---                  |---                 |
| Check    | 2m20s               | 47s                |
| Lints    | 2m56s               | 1m10s              |
| Docs     | 4m56s               | 2m35s              |
2025-01-23 09:47:51 -05:00
Michael Davis
76a8682c4d syntax: Prefer RopeSlice for non-id language injection markers
The `Name` variant's inner type can be switched to `RopeSlice` since
the parent commit removed the usage of `&str`. In doing this we need to
switch from a regular `Regex` to a `rope::Regex`, which is mostly a
matter of renaming the type.

The `Filename` and `Shebang` variants can also switch to `RopeSlice`
which avoids allocations in cases where the text doesn't reside on
different chunks of the rope. Previously `Filename`'s `Cow` was always
the owned variant because of the conversion to a `PathBuf`.
2025-01-23 11:01:35 +09:00
Michael Davis
060255344c syntax: Lookup up (#set! injection.language "name") props by ID
This splits the `InjectionLanguageMarker::Name` into two: one that
preforms the previous behavior (using the language configurations'
`injection_regex` fields and performing a match) and a new variant that
looks up directly by `language_id` with equality.

The old variant is used when capturing the injection language like we
do in the markdown queries for codefences. That captured text is part of
the document being highlighted so we might need a regex to recognize a
language like JavaScript as either "js" or "javascript". But the text
passed in the `(#set! injection.language "name")` property can be
looked up directly. This property is in the query code so there's no
need to be flexible in what we accept: we can require that the
`(#set! injection.language ..)` properties refer to languages by their
configured ID. This should save a noticeable amount of work for the
common case of injections: `(#set! injection.language)` is used much
more often than `@injection.language`.
2025-01-23 11:01:35 +09:00
dependabot[bot]
09b2f6ab5f build(deps): bump the rust-dependencies group with 5 updates (#12614)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-01-21 08:59:05 -06:00
Remo Senekowitsch
64aca8b350 Fix indent unit of git-rebase config (#12617) 2025-01-21 08:14:04 -06:00
Michael Davis
ccdb710431 minor: Rename '*' at eof integration test to be more specific 2025-01-21 09:08:36 -05:00
Nikita Revenco
ba4793fca0 fix: panic when pressing * after the end of the file (#12611)
* fix: panic when pressing `*` at the end of the file

chore: remove incorrect additions

* docs: add info comment

* test: add new syntax to add a selection at the final character

* test: `*` panics when after the last char

* test: move into a more appopriate module

* test: fix failing

* test: account for Windows test suite

* test: choose a different strategy for custom syntax

* test: do not modify the syntax

* style: remove newline

---------

Co-authored-by: Nikita Revenco <154856872+NikitaRevenco@users.noreply.github.com>
2025-01-21 16:05:15 +09:00
560 changed files with 27272 additions and 13110 deletions

3
.envrc
View File

@@ -1,7 +1,8 @@
watch_file shell.nix
watch_file default.nix
watch_file flake.lock
watch_file rust-toolchain.toml
# try to use flakes, if it fails use normal nix (ie. shell.nix)
use flake || use nix
eval "$shellHook"
eval "$shellHook"

1
.gitattributes vendored
View File

@@ -8,4 +8,5 @@
*.md text diff=markdown
book/theme/highlight.js linguist-vendored
runtime/queries/**/*.scm linguist-language=Tree-sitter-Query
Cargo.lock text

View File

@@ -1,4 +0,0 @@
---
name: Blank Issue
about: Create a blank issue.
---

View File

@@ -8,9 +8,6 @@ updates:
schedule:
interval: "weekly"
groups:
tree-sitter:
patterns:
- "tree-sitter*"
rust-dependencies:
update-types:
- "minor"

View File

@@ -9,7 +9,9 @@ on:
- cron: "00 01 * * *"
env:
MSRV: "1.76"
MSRV: "1.82"
# This key can be changed to bust the cache of tree-sitter grammars.
GRAMMAR_CACHE_VERSION: ""
jobs:
check:
@@ -29,6 +31,13 @@ jobs:
with:
shared-key: "build"
- name: Cache tree-sitter grammars
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-
- name: Run cargo check
run: cargo check
@@ -36,6 +45,7 @@ jobs:
name: Test Suite
runs-on: ${{ matrix.os }}
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
timeout-minutes: 30
env:
RUST_BACKTRACE: 1
HELIX_LOG_LEVEL: info
@@ -52,12 +62,12 @@ jobs:
with:
shared-key: "build"
- name: Cache test tree-sitter grammar
- name: Cache tree-sitter grammars
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-
key: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-
- name: Run cargo test
run: cargo test --workspace
@@ -67,7 +77,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
os: [ubuntu-latest, macos-latest, windows-latest, ubuntu-24.04-arm]
lints:
name: Lints
@@ -87,6 +97,13 @@ jobs:
with:
shared-key: "build"
- name: Cache tree-sitter grammars
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-
- name: Run cargo fmt
run: cargo fmt --all --check
@@ -115,6 +132,13 @@ jobs:
with:
shared-key: "build"
- name: Cache tree-sitter grammars
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-${{ runner.arch }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-
- name: Validate queries
run: cargo xtask query-check

View File

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

View File

@@ -1,4 +1,4 @@
name: Github Pages
name: GitHub Pages
on:
push:

View File

@@ -58,18 +58,18 @@ jobs:
strategy:
fail-fast: false # don't fail other jobs if one fails
matrix:
build: [x86_64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
build: [x86_64-linux, aarch64-linux, x86_64-macos, x86_64-windows] #, x86_64-win-gnu, win32-msvc
include:
- build: x86_64-linux
os: ubuntu-22.04
os: ubuntu-24.04
rust: stable
target: x86_64-unknown-linux-gnu
cross: false
- build: aarch64-linux
os: ubuntu-22.04
os: ubuntu-24.04-arm
rust: stable
target: aarch64-unknown-linux-gnu
cross: true
cross: false
# - build: riscv64-linux
# os: ubuntu-22.04
# rust: stable

View File

@@ -735,7 +735,7 @@ Updated languages and queries:
- Recognize common Dockerfile file types ([#9772](https://github.com/helix-editor/helix/pull/9772))
- Recognize NUON files as Nu ([#9839](https://github.com/helix-editor/helix/pull/9839))
- Add textobjects for Java native functions and constructors ([#9806](https://github.com/helix-editor/helix/pull/9806))
- Fix "braket" typeo in JSX highlights ([#9910](https://github.com/helix-editor/helix/pull/9910))
- Fix "braket" typo in JSX highlights ([#9910](https://github.com/helix-editor/helix/pull/9910))
- Update tree-sitter-hurl ([#9775](https://github.com/helix-editor/helix/pull/9775))
- Add textobjects queries for Vala ([#8541](https://github.com/helix-editor/helix/pull/8541))
- Update tree-sitter-git-config ([9610254](https://github.com/helix-editor/helix/commit/9610254))
@@ -942,7 +942,7 @@ Updated languages and queries:
- Add Fortran comment injections ([#7305](https://github.com/helix-editor/helix/pull/7305))
- Switch Vue language server to `vue-language-server` ([#7312](https://github.com/helix-editor/helix/pull/7312))
- Update tree-sitter-sql ([#7387](https://github.com/helix-editor/helix/pull/7387), [#8464](https://github.com/helix-editor/helix/pull/8464))
- Replace the MATLAB tre-sitter grammar ([#7388](https://github.com/helix-editor/helix/pull/7388), [#7442](https://github.com/helix-editor/helix/pull/7442), [#7491](https://github.com/helix-editor/helix/pull/7491), [#7493](https://github.com/helix-editor/helix/pull/7493), [#7511](https://github.com/helix-editor/helix/pull/7511), [#7532](https://github.com/helix-editor/helix/pull/7532), [#8040](https://github.com/helix-editor/helix/pull/8040))
- Replace the MATLAB tree-sitter grammar ([#7388](https://github.com/helix-editor/helix/pull/7388), [#7442](https://github.com/helix-editor/helix/pull/7442), [#7491](https://github.com/helix-editor/helix/pull/7491), [#7493](https://github.com/helix-editor/helix/pull/7493), [#7511](https://github.com/helix-editor/helix/pull/7511), [#7532](https://github.com/helix-editor/helix/pull/7532), [#8040](https://github.com/helix-editor/helix/pull/8040))
- Highlight TOML table headers ([#7441](https://github.com/helix-editor/helix/pull/7441))
- Recognize `cppm` file-type as C++ ([#7492](https://github.com/helix-editor/helix/pull/7492))
- Refactor ecma language queries into private and public queries ([#7207](https://github.com/helix-editor/helix/pull/7207))
@@ -1429,7 +1429,7 @@ Features:
- Support underline styles and colors ([#4061](https://github.com/helix-editor/helix/pull/4061), [98c121c](https://github.com/helix-editor/helix/commit/98c121c))
- Inheritance for themes ([#3067](https://github.com/helix-editor/helix/pull/3067), [#4096](https://github.com/helix-editor/helix/pull/4096))
- Cursorcolumn ([#4084](https://github.com/helix-editor/helix/pull/4084))
- Overhauled system for writing files and quiting ([#2267](https://github.com/helix-editor/helix/pull/2267), [#4397](https://github.com/helix-editor/helix/pull/4397))
- Overhauled system for writing files and quitting ([#2267](https://github.com/helix-editor/helix/pull/2267), [#4397](https://github.com/helix-editor/helix/pull/4397))
- Autosave when terminal loses focus ([#3178](https://github.com/helix-editor/helix/pull/3178))
- Use OSC52 as a fallback for the system clipboard ([#3220](https://github.com/helix-editor/helix/pull/3220))
- Show git diffs in the gutter ([#3890](https://github.com/helix-editor/helix/pull/3890), [#5012](https://github.com/helix-editor/helix/pull/5012), [#4995](https://github.com/helix-editor/helix/pull/4995))
@@ -1584,7 +1584,7 @@ Themes:
- Update `pop-dark` ([#4323](https://github.com/helix-editor/helix/pull/4323))
- Update `rose_pine` ([#4221](https://github.com/helix-editor/helix/pull/4221))
- Add `kanagawa` ([#4300](https://github.com/helix-editor/helix/pull/4300))
- Add `hex_steel`, `hex_toxic` and `hex_lavendar` ([#4367](https://github.com/helix-editor/helix/pull/4367), [#4990](https://github.com/helix-editor/helix/pull/4990))
- Add `hex_steel`, `hex_toxic` and `hex_lavender` ([#4367](https://github.com/helix-editor/helix/pull/4367), [#4990](https://github.com/helix-editor/helix/pull/4990))
- Update `tokyonight` and `tokyonight_storm` ([#4415](https://github.com/helix-editor/helix/pull/4415))
- Update `gruvbox` ([#4626](https://github.com/helix-editor/helix/pull/4626))
- Update `dark_plus` ([#4661](https://github.com/helix-editor/helix/pull/4661), [#4678](https://github.com/helix-editor/helix/pull/4678))
@@ -1751,7 +1751,7 @@ Usability improvements and fixes:
- Introduce `keyword.storage` highlight scope ([#2731](https://github.com/helix-editor/helix/pull/2731))
- Handle symlinks more consistently ([#2718](https://github.com/helix-editor/helix/pull/2718))
- Improve markdown list rendering ([#2687](https://github.com/helix-editor/helix/pull/2687))
- Update auto-pairs and idle-timout settings when the config is reloaded ([#2736](https://github.com/helix-editor/helix/pull/2736))
- Update auto-pairs and idle-timeout settings when the config is reloaded ([#2736](https://github.com/helix-editor/helix/pull/2736))
- Fix panic on closing last buffer ([#2658](https://github.com/helix-editor/helix/pull/2658))
- Prevent modifying jumplist until jumping to a reference ([#2670](https://github.com/helix-editor/helix/pull/2670))
- Ensure `:quit` and `:quit!` take no arguments ([#2654](https://github.com/helix-editor/helix/pull/2654))

944
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,12 +37,19 @@ package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.22" }
tree-house = { version = "0.3.0", default-features = false }
nucleo = "0.5.0"
slotmap = "1.0.7"
thiserror = "2.0"
tempfile = "3.15.0"
bitflags = "2.7"
tempfile = "3.20.0"
bitflags = "2.9"
unicode-segmentation = "1.2"
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
foldhash = "0.1"
parking_lot = "0.12"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
tokio-stream = "0.1.17"
[workspace.package]
version = "25.1.1"
@@ -52,4 +59,4 @@ categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
license = "MPL-2.0"
rust-version = "1.76"
rust-version = "1.82"

View File

@@ -1,7 +1,6 @@
[book]
authors = ["Blaž Hrastnik"]
language = "en"
multilingual = false
src = "src"
[output.html]

View File

@@ -12,9 +12,12 @@
- [Syntax aware motions](./syntax-aware-motions.md)
- [Pickers](./pickers.md)
- [Keymap](./keymap.md)
- [Command line](./command-line.md)
- [Commands](./commands.md)
- [Language support](./lang-support.md)
- [Migrating from Vim](./from-vim.md)
- [Ecosystem](./ecosystem.md)
- [Migrating from Vim](./from-vim.md)
- [Helix mode in other software](./other-software.md)
- [Configuration](./configuration.md)
- [Editor](./editor.md)
- [Themes](./themes.md)
@@ -25,3 +28,4 @@
- [Adding textobject queries](./guides/textobject.md)
- [Adding indent queries](./guides/indent.md)
- [Adding injection queries](./guides/injection.md)
- [Adding tags queries](./guides/tags.md)

View File

@@ -35,10 +35,19 @@ RUSTFLAGS="-C target-feature=-crt-static"
2. Compile from source:
```sh
# Reproducible
cargo install --path helix-term --locked
```
```sh
# Optimized
cargo install \
--profile opt \
--config 'build.rustflags="-C target-cpu=native"' \
--path helix-term \
--locked
```
This command will create the `hx` executable and construct the tree-sitter
Either command will create the `hx` executable and construct the tree-sitter
grammars in the local `runtime` folder.
> 💡 If you do not want to fetch or build grammars, set an environment variable `HELIX_DISABLE_AUTO_GRAMMAR_BUILD`
@@ -64,11 +73,9 @@ export HELIX_RUNTIME=~/src/helix/runtime
Or, create a symbolic link:
```sh
ln -Ts $PWD/runtime ~/.config/helix/runtime
ln -Tsf $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
@@ -76,7 +83,7 @@ Either set the `HELIX_RUNTIME` environment variable to point to the runtime file
Cmd:
```sh
setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime"
setx HELIX_RUNTIME "%userprofile%\src\helix\runtime"
```
> 💡 `%userprofile%` resolves to your user directory like
@@ -184,13 +191,13 @@ cargo deb -- --locked
```
> 💡 This locks you into the `--release` profile. But you can also build helix in any way you like.
> As long as you leave a `target/release/hx` file, it will get packaged with `cargo deb --no-build`
> As long as you leave a `target/release/hx` file, it will get packaged with `cargo deb --no-build`
> 💡 Don't worry about the repeated
> 💡 Don't worry about the following:
> ```
> warning: Failed to find dependency specification
> ```
> warnings. Cargo deb just reports which packaged files it didn't derive dependencies for. But
> Cargo deb just reports which packaged files it didn't derive dependencies for. But
> so far the dependency deriving seams very good, even if some of the grammar files are skipped.
You can find the resulted `.deb` in `target/debian/`. It should contain everything it needs, including the

86
book/src/command-line.md Normal file
View File

@@ -0,0 +1,86 @@
# Command line
- [Quoting](#quoting)
- [Flags](#flags)
- [Expansions](#expansions)
- [Exceptions](#exceptions)
The command line is used for executing [typable commands](./commands.md#typable-commands) like `:write` or `:quit`. Press `:` to activate the command line.
Typable commands optionally accept arguments. `:write` for example accepts an optional path to write the file contents. The command line also supports a quoting syntax for arguments, flags to modify command behaviors, and _expansions_ - a way to insert values from the editor. Most commands support these features but some have custom parsing rules (see the [exceptions](#exceptions) below).
## Quoting
By default, command arguments are split on tabs and space characters. `:open README.md CHANGELOG.md` for example should open two files, `README.md` and `CHANGELOG.md`. Arguments that contain spaces can be surrounded in single quotes (`'`) or backticks (`` ` ``) to prevent the space from separating the argument, like `:open 'a b.txt'`.
Double quotes may be used the same way, but double quotes _expand_ their inner content. `:echo "%{cursor_line}"` for example may print `1` because of the expansion for the `cursor_line` variable. `:echo '%{cursor_line}'` though prints `%{cursor_line}` literally: content within single quotes or backticks is interpreted as-is.
On Unix systems the backslash character may be used to escape certain characters depending on where it is used. Within an argument which isn't surround in quotes, the backslash can be used to escape the space or tab characters: `:open a\ b.txt` is equivalent to `:open 'a b.txt'`. The backslash may also be used to escape quote characters (`'`, `` ` ``, `"`) or the percent token (`%`) when used at the beginning of an argument. `:echo \%%sh{foo}` for example prints `%sh{foo}` instead of invoking a `foo` shell command and `:echo \"quote` prints `"quote`. The backslash character is treated literally in any other situation on Unix systems and always on Windows: `:echo \n` always prints `\n`.
## Flags
Command flags are optional switches that can be used to alter the behavior of a command. For example the `:sort` command accepts an optional `--reverse` (or `-r` for short) flag which causes the sort command to reverse the sorting direction. Typing the `-` character shows completions for the current command's flags, if any.
The `--` flag specifies the end of flags. All arguments after `--` are treated as positional arguments: `:open -- -a.txt` opens a file called `-a.txt`.
## Expansions
Expansions are patterns that Helix recognizes and replaces within the command line. Helix recognizes anything starting with a percent token (`%`) as an expansion, for example `%sh{echo hi!}`. Expansions are particularly useful when used in commands like `:echo` or `:noop` for executing simple scripts. For example:
```toml
[keys.normal]
# Print the current line's git blame information to the statusline.
space.B = ":echo %sh{git blame -L %{cursor_line},+1 %{buffer_name}}"
```
Expansions take the form `%[<kind>]<open><contents><close>`. In `%sh{echo hi!}`, for example, the kind is `sh` - the shell expansion - and the contents are "echo hi!", with `{` and `}` acting as opening and closing delimiters. The following open/close characters are recognized as expansion delimiter pairs: `(`/`)`, `[`/`]`, `{`/`}` and `<`/`>`. Plus the single characters `'`, `"` or `|` may be used instead: `%{cursor_line}` is equivalent to `%<cursor_line>`, `%[cursor_line]` or `%|cursor_line|`.
To escape a percent character instead of treating it as an expansion, use two percent characters consecutively. To execute a shell command like `date -u +'%Y-%m-%d'`, double the percent characters: `:echo %sh{date -u +'%%Y-%%m-%%d'}`.
When no `<kind>` is provided, Helix will expand a **variable**. For example `%{cursor_line}` can be used as in argument to insert the line number. `:echo %{cursor_line}` for instance may print `1` to the statusline.
The following variables are supported:
| Name | Description |
|--- |--- |
| `cursor_line` | The line number of the primary cursor in the currently focused document, starting at 1. |
| `cursor_column` | The column number of the primary cursor in the currently focused document, starting at 1. This is counted as the number of grapheme clusters from the start of the line rather than bytes or codepoints. |
| `buffer_name` | The relative path of the currently focused document. `[scratch]` is expanded instead for scratch buffers. |
| `line_ending` | A string containing the line ending of the currently focused document. For example on Unix systems this is usually a line-feed character (`\n`) but on Windows systems this may be a carriage-return plus a line-feed (`\r\n`). The line ending kind of the currently focused document can be inspected with the `:line-ending` command. |
| `language` | A string containing the language name of the currently focused document.|
| `selection` | A string containing the contents of the primary selection of the currently focused document. |
| `selection_line_start` | The line number of the start of the primary selection in the currently focused document, starting at 1. |
| `selection_line_end` | The line number of the end of the primary selection in the currently focused document, starting at 1. |
Aside from editor variables, the following expansions may be used:
* Unicode `%u{..}`. The contents may contain up to six hexadecimal numbers corresponding to a Unicode codepoint value. For example `:echo %u{25CF}` prints `●` to the statusline.
* Shell `%sh{..}`. The contents are passed to the configured shell command. For example `:echo %sh{echo "20 * 5" | bc}` may print `100` on the statusline on when using a shell with `echo` and the `bc` calculator installed. Shell expansions are evaluated recursively. `%sh{echo '%{buffer_name}:%{cursor_line}'}` for example executes a command like `echo 'README.md:1'`: the variables within the `%sh{..}` expansion are evaluated before executing the shell command.
As mentioned above, double quotes can be used to surround arguments containing spaces but also support expansions within the quoted content unlike singe quotes or backticks. For example `:echo "circle: %u{25CF}"` prints `circle: ●` to the statusline while `:echo 'circle: %u{25CF}'` prints `circle: %u{25CF}`.
Note that expansions are only evaluated once the Enter key is pressed in command mode.
## Exceptions
The following commands support expansions but otherwise pass the given argument directly to the shell program without interpreting quotes:
* `:insert-output`
* `:append-output`
* `:pipe`
* `:pipe-to`
* `:run-shell-command`
For example executing `:sh echo "%{buffer_name}:%{cursor_column}"` would pass text like `echo "README.md:1"` as an argument to the shell program: the expansions are evaluated but not the quotes. As mentioned above, percent characters can be used in shell commands by doubling the percent character. To insert the output of a command like `date -u +'%Y-%m-%d'` use `:insert-output date -u +'%%Y-%%m-%%d'`.
The `:set-option` and `:toggle-option` commands use regular parsing for the first argument - the config option name - and parse the rest depending on the config option's type. `:set-option` interprets the second argument as a string for string config options and parses everything else as JSON.
`:toggle-option`'s behavior depends on the JSON type of the config option supplied as the first argument:
* Booleans: only the config option name should be provided. For example `:toggle-option auto-format` will flip the `auto-format` option.
* Strings: the rest of the command line is parsed with regular quoting rules. For example `:toggle-option indent-heuristic hybrid tree-sitter simple` cycles through "hybrid", "tree-sitter" and "simple" values on each invocation of the command.
* Numbers, arrays and objects: the rest of the command line is parsed as a stream of JSON values. For example `:toggle-option rulers [81] [51, 73]` cycles through `[81]` and `[51, 73]`.
When providing multiple values to `:toggle-option` there should be no duplicates. `:toggle-option indent-heuristic hybrid simple tree-sitter simple` for example would only toggle between "hybrid" and "tree-sitter" values.
`:lsp-workspace-command` works similarly to `:toggle-option`. The first argument (if present) is parsed according to normal rules. The rest of the line is parsed as JSON values. Unlike `:toggle-option`, string arguments for a command must be quoted. For example `:lsp-workspace-command lsp.Command "foo" "bar"`.

3
book/src/ecosystem.md Normal file
View File

@@ -0,0 +1,3 @@
# Ecosystem
This section has information related to the wider Helix ecosystem.

View File

@@ -53,11 +53,15 @@
| `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` |
| `atomic-save` | Whether to use atomic operations to write documents to disk. This prevents data loss if the editor is interrupted while writing the file, but may confuse some file watching/hot reloading programs. | `true` |
| `trim-final-newlines` | Whether to automatically remove line-endings after the final one on write | `false` |
| `trim-trailing-whitespace` | Whether to automatically remove whitespace preceding line endings on write | `false` |
| `popup-border` | Draw border around `popup`, `menu`, `all`, or `none` | `none` |
| `indent-heuristic` | How the indentation for a newly inserted line is computed: `simple` just copies the indentation level from the previous line, `tree-sitter` computes the indentation based on the syntax tree and `hybrid` combines both approaches. If the chosen heuristic is not available, a different one will be used as a fallback (the fallback order being `hybrid` -> `tree-sitter` -> `simple`). | `hybrid`
| `jump-label-alphabet` | The characters that are used to generate two character jump labels. Characters at the start of the alphabet are used first. | `"abcdefghijklmnopqrstuvwxyz"`
| `end-of-line-diagnostics` | Minimum severity of diagnostics to render at the end of the line. Set to `disable` to disable entirely. Refer to the setting about `inline-diagnostics` for more details | "disable"
| `clipboard-provider` | Which API to use for clipboard interaction. One of `pasteboard` (MacOS), `wayland`, `x-clip`, `x-sel`, `win-32-yank`, `termux`, `tmux`, `windows`, `termcode`, `none`, or a custom command set. | Platform and environment specific. |
| `editor-config` | Whether to read settings from [EditorConfig](https://editorconfig.org) files | `true` |
### `[editor.clipboard-provider]` Section
@@ -70,7 +74,7 @@ For instance, setting it to use OSC 52 termcodes, the configuration would be:
clipboard-provider = "termcode"
```
Alternatively, Helix can be configured to use arbitary commands for clipboard integration:
Alternatively, Helix can be configured to use arbitrary commands for clipboard integration:
```toml
[editor.clipboard-provider.custom]
@@ -101,6 +105,8 @@ separator = "│"
mode.normal = "NORMAL"
mode.insert = "INSERT"
mode.select = "SELECT"
diagnostics = ["warning", "error"]
workspace-diagnostics = ["warning", "error"]
```
The `[editor.statusline]` key takes the following sub-keys:
@@ -113,6 +119,8 @@ The `[editor.statusline]` key takes the following sub-keys:
| `mode.normal` | The text shown in the `mode` element for normal mode | `"NOR"` |
| `mode.insert` | The text shown in the `mode` element for insert mode | `"INS"` |
| `mode.select` | The text shown in the `mode` element for select mode | `"SEL"` |
| `diagnostics` | A list of severities which are displayed for the current buffer | `["warning", "error"]` |
| `workspace-diagnostics` | A list of severities which are displayed for the workspace | `["warning", "error"]` |
The following statusline elements can be configured:
@@ -126,12 +134,13 @@ 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) |
| `file-indent-style` | The file indentation style |
| `read-only-indicator` | An indicator that shows `[readonly]` when a file cannot be written |
| `total-line-numbers` | The total line numbers of the opened file |
| `file-type` | The type of the opened file |
| `diagnostics` | The number of warnings and/or errors |
| `workspace-diagnostics` | The number of warnings and/or errors on workspace |
| `selections` | The number of active selections |
| `selections` | The primary selection index out of the number of active selections |
| `primary-selection-length` | The number of characters currently in primary selection |
| `position` | The cursor position |
| `position-percentage` | The cursor position as a percentage of the total number of lines |
@@ -149,6 +158,8 @@ The following statusline elements can be configured:
| `display-progress-messages` | Display LSP progress messages below statusline[^1] | `false` |
| `auto-signature-help` | Enable automatic popup of signature help (parameter hints) | `true` |
| `display-inlay-hints` | Display inlay hints[^2] | `false` |
| `inlay-hints-length-limit` | Maximum displayed length (non-zero number) of inlay hints | Unset by default |
| `display-color-swatches` | Show color swatches next to colors | `true` |
| `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` |

View File

@@ -1,249 +1,279 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default language servers |
| --- | --- | --- | --- | --- |
| ada | ✓ | ✓ | | `ada_language_server` |
| adl | ✓ | ✓ | ✓ | |
| agda | ✓ | | | |
| amber | ✓ | | | |
| astro | ✓ | | | |
| awk | ✓ | | | `awk-language-server` |
| bash | ✓ | ✓ | | `bash-language-server` |
| bass | ✓ | | | `bass` |
| beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` |
| bitbake | ✓ | | | `bitbake-language-server` |
| blade | ✓ | | | |
| blueprint | ✓ | | | `blueprint-compiler` |
| c | ✓ | | | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` |
| cabal | | | | `haskell-language-server-wrapper` |
| cairo | | | | `cairo-language-server` |
| capnp | ✓ | | ✓ | |
| cel | ✓ | | | |
| circom | ✓ | | | `circom-lsp` |
| clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | | | `cmake-language-server` |
| codeql | ✓ | | | `codeql` |
| comment | ✓ | | | |
| common-lisp | ✓ | | ✓ | `cl-lsp` |
| cpon | ✓ | | | |
| cpp | ✓ | | | `clangd` |
| crystal | ✓ | ✓ | | `crystalline` |
| css | ✓ | | ✓ | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` |
| cylc | ✓ | ✓ | ✓ | |
| d | ✓ | ✓ | | `serve-d` |
| dart | ✓ | | | `dart` |
| dbml | ✓ | | | |
| devicetree | ✓ | | | |
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
| diff | ✓ | | | |
| docker-compose | ✓ | | | `docker-compose-langserver`, `yaml-language-server` |
| dockerfile | ✓ | ✓ | | `docker-langserver` |
| dot | ✓ | | | `dot-language-server` |
| dtd | ✓ | | | |
| dune | ✓ | | | |
| earthfile | ✓ | | | `earthlyls` |
| edoc | ✓ | | | |
| eex | ✓ | | | |
| ejs | ✓ | | | |
| elisp | ✓ | | | |
| elixir | ✓ | ✓ | ✓ | `elixir-ls` |
| elm | ✓ | | | `elm-language-server` |
| elvish | ✓ | | | `elvish` |
| env | ✓ | | | |
| erb | ✓ | | | |
| erlang | ✓ | | | `erlang_ls`, `elp` |
| esdl | ✓ | | | |
| fidl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | `forth-lsp` |
| fortran | ✓ | | | `fortls` |
| fsharp | ✓ | | | `fsautocomplete` |
| gas | ✓ | ✓ | | |
| gdscript | ✓ | ✓ | | |
| gemini | ✓ | | | |
| gherkin | ✓ | | | |
| git-attributes | ✓ | | | |
| git-commit | ✓ | ✓ | | |
| git-config | ✓ | ✓ | | |
| git-ignore | ✓ | | | |
| git-rebase | ✓ | | | |
| gjs | ✓ | ✓ | | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| gleam | ✓ | ✓ | | `gleam` |
| glimmer | ✓ | | | `ember-language-server` |
| glsl | ✓ | | | `glsl_analyzer` |
| gn | ✓ | | | |
| go | ✓ | | | `gopls`, `golangci-lint-langserver` |
| godot-resource | ✓ | ✓ | | |
| gomod | ✓ | | | `gopls` |
| gotmpl | ✓ | | | `gopls` |
| gowork | ✓ | | | `gopls` |
| gpr | ✓ | | | `ada_language_server` |
| graphql | ✓ | ✓ | | `graphql-lsp` |
| gren | ✓ | ✓ | | |
| groovy | ✓ | | | |
| gts | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| hare | ✓ | | | |
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | |
| hcl | ✓ | | | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` |
| helm | ✓ | | | `helm_ls` |
| hocon | ✓ | | | |
| hoon | ✓ | | | |
| hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server`, `superhtml` |
| hurl | ✓ | ✓ | ✓ | |
| hyprlang | ✓ | | | `hyprls` |
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
| inko | ✓ | ✓ | | |
| janet | ✓ | | | |
| java | ✓ | ✓ | ✓ | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | |
| jjdescription | ✓ | | | |
| jq | ✓ | ✓ | | `jq-lsp` |
| jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` |
| json5 | ✓ | | | |
| jsonc | ✓ | | | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` |
| jsx | ✓ | | | `typescript-language-server` |
| julia | ✓ | ✓ | ✓ | `julia` |
| just | ✓ | | | |
| kdl | ✓ | ✓ | ✓ | |
| koka | ✓ | | ✓ | `koka` |
| kotlin | ✓ | | | `kotlin-language-server` |
| koto | ✓ | | | `koto-ls` |
| latex | ✓ | ✓ | | `texlab` |
| ld | ✓ | | | |
| ldif | ✓ | | | |
| lean | ✓ | | | `lean` |
| ledger | ✓ | | | |
| llvm | ✓ | | ✓ | |
| llvm-mir | ✓ | | | |
| llvm-mir-yaml | ✓ | | ✓ | |
| log | ✓ | | | |
| lpf | ✓ | | | |
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
| make | ✓ | | ✓ | |
| markdoc | ✓ | | | `markdoc-ls` |
| markdown | ✓ | | | `marksman`, `markdown-oxide` |
| markdown.inline | ✓ | | | |
| matlab | ✓ | ✓ | | |
| mermaid | ✓ | | | |
| meson | ✓ | | | `mesonlsp` |
| mint | | | | `mint` |
| mojo | ✓ | ✓ | ✓ | `magic` |
| move | ✓ | | | |
| msbuild | ✓ | | ✓ | |
| nasm | ✓ | | | |
| nestedtext | ✓ | | | |
| nginx | ✓ | | | |
| nickel | ✓ | | ✓ | `nls` |
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
| nix | ✓ | ✓ | | `nil`, `nixd` |
| nu | ✓ | | | `nu` |
| nunjucks | ✓ | | | |
| ocaml | ✓ | | | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | ✓ | ✓ | `ols` |
| ohm | ✓ | | | |
| opencl | ✓ | | ✓ | `clangd` |
| openscad | | | | `openscad-lsp` |
| org | ✓ | | | |
| pascal | ✓ | | | `pasls` |
| passwd | ✓ | | | |
| pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
| pest | ✓ | | | `pest-language-server` |
| php | ✓ | | ✓ | `intelephense` |
| php-only | ✓ | | | |
| pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` |
| pkl | ✓ | | | |
| po | ✓ | | | |
| pod | ✓ | | | |
| ponylang | ✓ | | | |
| powershell | ✓ | | | |
| prisma | ✓ | ✓ | | `prisma-language-server` |
| prolog | | | | `swipl` |
| protobuf | ✓ | | | `bufls`, `pb` |
| prql | ✓ | | | |
| purescript | ✓ | ✓ | | `purescript-language-server` |
| python | ✓ | | | `ruff`, `jedi-language-server`, `pylsp` |
| qml | ✓ | | ✓ | `qmlls` |
| quint | ✓ | | | `quint-language-server` |
| r | ✓ | | | `R` |
| racket | ✓ | | ✓ | `racket` |
| regex | ✓ | | | |
| rego | ✓ | | | `regols` |
| rescript | ✓ | ✓ | | `rescript-language-server` |
| rmarkdown | ✓ | | | `R` |
| robot | ✓ | | | `robotframework_ls` |
| ron | ✓ | | ✓ | |
| rst | ✓ | | | |
| ruby | ✓ | ✓ | | `solargraph` |
| rust | ✓ | | ✓ | `rust-analyzer` |
| sage | ✓ | ✓ | | |
| scala | ✓ | ✓ | ✓ | `metals` |
| scheme | ✓ | | | |
| scss | ✓ | | | `vscode-css-language-server` |
| slint | ✓ | ✓ | | `slint-lsp` |
| smali | ✓ | | ✓ | |
| smithy | ✓ | | | `cs` |
| sml | ✓ | | | |
| snakemake | ✓ | | | `pylsp` |
| solidity | ✓ | | | `solc` |
| spade | ✓ | | ✓ | `spade-language-server` |
| spicedb | ✓ | | | |
| sql | ✓ | | | |
| sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | |
| strace | ✓ | | | |
| supercollider | ✓ | | | |
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | ✓ | | `sourcekit-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | |
| tact | ✓ | ✓ | | |
| task | ✓ | | | |
| tcl | ✓ | | ✓ | |
| teal | ✓ | | | `teal-language-server` |
| templ | ✓ | | | `templ` |
| textproto | ✓ | ✓ | ✓ | |
| tfvars | ✓ | | ✓ | `terraform-ls` |
| thrift | ✓ | | | |
| todotxt | ✓ | | | |
| toml | ✓ | ✓ | | `taplo` |
| tsq | ✓ | | | `ts_query_ls` |
| tsx | ✓ | ✓ | | `typescript-language-server` |
| twig | ✓ | | | |
| typescript | ✓ | | ✓ | `typescript-language-server` |
| typespec | ✓ | ✓ | ✓ | `tsp-server` |
| typst | ✓ | | | `tinymist`, `typst-lsp` |
| ungrammar | ✓ | | | |
| unison | ✓ | ✓ | | |
| uxntal | ✓ | | | |
| v | ✓ | | ✓ | `v-analyzer` |
| vala | ✓ | ✓ | | `vala-language-server` |
| vento | ✓ | | | |
| verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | |
| vue | ✓ | | | `vue-language-server` |
| wast | ✓ | | | |
| wat | ✓ | | | |
| webc | ✓ | | | |
| wgsl | ✓ | | | `wgsl_analyzer` |
| wit | ✓ | | | |
| wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | |
| xml | ✓ | | | |
| xtc | ✓ | | | |
| yaml | ✓ | ✓ | ✓ | `yaml-language-server`, `ansible-language-server` |
| yuck | ✓ | | | |
| zig | ✓ | | | `zls` |
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Code Navigation Tags | Default language servers |
| --- | --- | --- | --- | --- | --- |
| ada | ✓ | ✓ | | | `ada_language_server` |
| adl | ✓ | ✓ | ✓ | | |
| agda | ✓ | | | | |
| alloy | ✓ | | | | |
| amber | ✓ | | | | `amber-lsp` |
| astro | ✓ | | | | `astro-ls` |
| awk | ✓ | ✓ | | | `awk-language-server` |
| bash | ✓ | ✓ | ✓ | | `bash-language-server` |
| bass | ✓ | | | | `bass` |
| beancount | ✓ | | | | `beancount-language-server` |
| bibtex | ✓ | | | | `texlab` |
| bicep | ✓ | | | | `bicep-langserver` |
| bitbake | ✓ | | | | `bitbake-language-server` |
| blade | ✓ | | | | |
| blueprint | ✓ | | | | `blueprint-compiler` |
| c | ✓ | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | | | | ✓ | `OmniSharp` |
| cabal | | | | | `haskell-language-server-wrapper` |
| caddyfile | ✓ | | ✓ | | |
| cairo | ✓ | | | | `cairo-language-server` |
| capnp | ✓ | | ✓ | | |
| cel | ✓ | | | | |
| circom | ✓ | | | | `circom-lsp` |
| clarity | ✓ | | | | `clarinet` |
| clojure | ✓ | | | | `clojure-lsp` |
| cmake | ✓ | | ✓ | | `neocmakelsp`, `cmake-language-server` |
| codeql | ✓ | ✓ | | | `codeql` |
| comment | ✓ | | | | |
| common-lisp | ✓ | | ✓ | | `cl-lsp` |
| cpon | ✓ | | ✓ | | |
| cpp | ✓ | | ✓ | ✓ | `clangd` |
| crystal | ✓ | ✓ | ✓ | ✓ | `crystalline`, `ameba-ls` |
| css | ✓ | | ✓ | | `vscode-css-language-server` |
| csv | ✓ | | | | |
| cue | ✓ | | | | `cuelsp` |
| cylc | ✓ | ✓ | | | |
| d | ✓ | ✓ | ✓ | | `serve-d` |
| dart | ✓ | ✓ | ✓ | | `dart` |
| dbml | ✓ | | | | |
| debian | ✓ | | | | |
| devicetree | ✓ | | | | `dts-lsp` |
| dhall | ✓ | ✓ | | | `dhall-lsp-server` |
| diff | ✓ | | | | |
| djot | ✓ | | | | |
| docker-compose | ✓ | | | | `docker-compose-langserver`, `yaml-language-server` |
| dockerfile | ✓ | ✓ | | | `docker-langserver` |
| dot | ✓ | | | | `dot-language-server` |
| dtd | ✓ | | | | |
| dune | ✓ | | | | |
| dunstrc | ✓ | | | | |
| earthfile | ✓ | ✓ | ✓ | | `earthlyls` |
| edoc | ✓ | | | | |
| eex | ✓ | | | | |
| ejs | ✓ | | | | |
| elisp | ✓ | | | ✓ | |
| elixir | ✓ | ✓ | ✓ | ✓ | `elixir-ls` |
| elm | ✓ | ✓ | | ✓ | `elm-language-server` |
| elvish | ✓ | | | | `elvish` |
| env | ✓ | ✓ | | | |
| erb | ✓ | | | | |
| erlang | ✓ | ✓ | | ✓ | `erlang_ls`, `elp` |
| esdl | ✓ | | | | |
| fennel | ✓ | | | | `fennel-ls` |
| fga | ✓ | ✓ | | | |
| fidl | ✓ | | | | |
| fish | ✓ | ✓ | ✓ | | `fish-lsp` |
| forth | ✓ | | | | `forth-lsp` |
| fortran | ✓ | | | | `fortls` |
| fsharp | ✓ | | | | `fsautocomplete` |
| gas | ✓ | ✓ | | | `asm-lsp` |
| gdscript | ✓ | ✓ | ✓ | | |
| gemini | ✓ | | | | |
| gherkin | ✓ | | | | |
| ghostty | ✓ | | | | |
| git-attributes | ✓ | | | | |
| git-commit | ✓ | ✓ | | | |
| git-config | ✓ | ✓ | | | |
| git-ignore | ✓ | | | | |
| git-notes | ✓ | | | | |
| git-rebase | ✓ | | | | |
| gjs | ✓ | ✓ | ✓ | | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| gleam | ✓ | ✓ | | | `gleam` |
| glimmer | ✓ | | | | `ember-language-server` |
| glsl | ✓ | ✓ | ✓ | | `glsl_analyzer` |
| gn | ✓ | | | | |
| go | ✓ | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
| godot-resource | ✓ | ✓ | | | |
| gomod | ✓ | | | | `gopls` |
| gotmpl | ✓ | | | | `gopls` |
| gowork | ✓ | | | | `gopls` |
| gpr | ✓ | | | | `ada_language_server` |
| graphql | ✓ | ✓ | | | `graphql-lsp` |
| gren | ✓ | ✓ | | | |
| groovy | ✓ | | | | |
| gts | ✓ | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| hare | ✓ | | | | |
| haskell | | | | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | | |
| hcl | ✓ | | | | `terraform-ls` |
| heex | ✓ | ✓ | | | `elixir-ls` |
| helm | ✓ | | | | `helm_ls` |
| hocon | ✓ | ✓ | ✓ | | |
| hoon | ✓ | | | | |
| hosts | ✓ | | | | |
| html | ✓ | | | | `vscode-html-language-server`, `superhtml` |
| htmldjango | ✓ | | | | `djlsp`, `vscode-html-language-server`, `superhtml` |
| hurl | ✓ | ✓ | ✓ | | |
| hyprlang | ✓ | | ✓ | | `hyprls` |
| idris | | | | | `idris2-lsp` |
| iex | ✓ | | | | |
| ini | ✓ | | | | |
| ink | ✓ | | | | |
| inko | ✓ | ✓ | ✓ | | |
| janet | ✓ | | | | |
| java | ✓ | ✓ | ✓ | | `jdtls` |
| javascript | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | | |
| jjdescription | ✓ | | | | |
| jq | ✓ | ✓ | | | `jq-lsp` |
| jsdoc | ✓ | | | | |
| json | ✓ | | | | `vscode-json-language-server` |
| json-ld | ✓ | ✓ | ✓ | | `vscode-json-language-server` |
| json5 | ✓ | | | | |
| jsonc | ✓ | | ✓ | | `vscode-json-language-server` |
| jsonnet | ✓ | | | | `jsonnet-language-server` |
| jsx | ✓ | | ✓ | ✓ | `typescript-language-server` |
| julia | ✓ | ✓ | ✓ | | `julia` |
| just | ✓ | ✓ | ✓ | | `just-lsp` |
| kdl | ✓ | ✓ | ✓ | | |
| koka | ✓ | | ✓ | | `koka` |
| kotlin | ✓ | ✓ | ✓ | | `kotlin-language-server` |
| koto | ✓ | ✓ | ✓ | | `koto-ls` |
| latex | ✓ | ✓ | | | `texlab` |
| ld | ✓ | | ✓ | | |
| ldif | ✓ | | | | |
| lean | ✓ | | | | `lean` |
| ledger | ✓ | | | | |
| llvm | ✓ | ✓ | ✓ | | |
| llvm-mir | ✓ | ✓ | | | |
| llvm-mir-yaml | ✓ | | ✓ | | |
| log | ✓ | | | | |
| lpf | ✓ | | | | |
| lua | ✓ | | | | `lua-language-server` |
| luau | ✓ | | ✓ | | `luau-lsp` |
| mail | ✓ | ✓ | | | |
| make | ✓ | | ✓ | | |
| markdoc | ✓ | | | | `markdoc-ls` |
| markdown | ✓ | | | ✓ | `marksman`, `markdown-oxide` |
| markdown-rustdoc | ✓ | | | | |
| markdown.inline | ✓ | | | | |
| matlab | ✓ | ✓ | ✓ | | |
| mermaid | ✓ | | | | |
| meson | ✓ | | ✓ | | `mesonlsp` |
| mint | | | | | `mint` |
| mojo | ✓ | ✓ | ✓ | | `pixi` |
| move | ✓ | | | | |
| msbuild | ✓ | | ✓ | | |
| nasm | ✓ | ✓ | | | `asm-lsp` |
| nestedtext | ✓ | ✓ | ✓ | | |
| nginx | ✓ | | | | |
| nickel | ✓ | | ✓ | | `nls` |
| nim | ✓ | | | | `nimlangserver` |
| nix | ✓ | ✓ | ✓ | | `nil`, `nixd` |
| nu | ✓ | | | | `nu` |
| nunjucks | ✓ | | | | |
| ocaml | ✓ | | | | `ocamllsp` |
| ocaml-interface | ✓ | | | | `ocamllsp` |
| odin | ✓ | ✓ | ✓ | | `ols` |
| ohm | ✓ | ✓ | ✓ | | |
| opencl | | ✓ | ✓ | | `clangd` |
| openscad | ✓ | | | | `openscad-lsp` |
| org | ✓ | | | | |
| pascal | ✓ | ✓ | | | `pasls` |
| passwd | ✓ | | | | |
| pem | ✓ | | | | |
| perl | ✓ | ✓ | ✓ | | `perlnavigator` |
| pest | ✓ | ✓ | ✓ | | `pest-language-server` |
| php | ✓ | ✓ | | ✓ | `intelephense` |
| php-only | ✓ | | | ✓ | |
| pkgbuild | ✓ | ✓ | | | `termux-language-server`, `bash-language-server` |
| pkl | ✓ | | ✓ | | `pkl-lsp` |
| po | ✓ | ✓ | | | |
| pod | ✓ | | | | |
| ponylang | ✓ | ✓ | ✓ | | |
| powershell | ✓ | | | | |
| prisma | ✓ | ✓ | | | `prisma-language-server` |
| prolog | ✓ | | ✓ | | `swipl` |
| properties | ✓ | ✓ | | | |
| protobuf | ✓ | ✓ | ✓ | | `buf`, `pb`, `protols` |
| prql | ✓ | | | | |
| pug | ✓ | | | | |
| purescript | ✓ | ✓ | | | `purescript-language-server` |
| python | ✓ | | ✓ | ✓ | `ty`, `ruff`, `jedi-language-server`, `pylsp` |
| qml | ✓ | ✓ | ✓ | | `qmlls` |
| quarto | ✓ | | ✓ | | |
| quint | ✓ | | | | `quint-language-server` |
| r | ✓ | | | | `R` |
| racket | ✓ | | ✓ | | `racket` |
| regex | ✓ | | | | |
| rego | ✓ | | | | `regols` |
| rescript | ✓ | ✓ | | | `rescript-language-server` |
| rmarkdown | ✓ | | ✓ | | `R` |
| robot | ✓ | | | | `robotframework_ls` |
| ron | ✓ | | ✓ | | |
| rst | ✓ | | | | |
| ruby | ✓ | ✓ | ✓ | ✓ | `ruby-lsp`, `solargraph` |
| rust | ✓ | ✓ | ✓ | ✓ | `rust-analyzer` |
| rust-format-args | ✓ | | | | |
| rust-format-args-macro | ✓ | ✓ | ✓ | | |
| sage | ✓ | ✓ | | | |
| scala | ✓ | ✓ | ✓ | | `metals` |
| scheme | ✓ | | ✓ | | |
| scss | ✓ | | | | `vscode-css-language-server` |
| slang | ✓ | ✓ | ✓ | | `slangd` |
| slint | ✓ | ✓ | ✓ | | `slint-lsp` |
| smali | ✓ | | ✓ | | |
| smithy | ✓ | | | | `cs` |
| sml | ✓ | | | | |
| snakemake | ✓ | | ✓ | | `pylsp` |
| solidity | ✓ | ✓ | | | `solc` |
| sourcepawn | ✓ | ✓ | | | `sourcepawn-studio` |
| spade | ✓ | | | | `spade-language-server` |
| spicedb | ✓ | | | ✓ | |
| sql | ✓ | ✓ | | | |
| sshclientconfig | ✓ | | | | |
| starlark | ✓ | | | | `starpls` |
| strace | ✓ | | | | |
| supercollider | ✓ | | | | |
| svelte | ✓ | | ✓ | | `svelteserver` |
| sway | ✓ | ✓ | ✓ | | `forc` |
| swift | ✓ | ✓ | | | `sourcekit-lsp` |
| systemd | ✓ | | | | `systemd-lsp` |
| t32 | ✓ | | | | |
| tablegen | ✓ | ✓ | ✓ | | |
| tact | ✓ | ✓ | ✓ | | |
| task | ✓ | | | | |
| tcl | ✓ | | ✓ | | |
| teal | ✓ | | | | `teal-language-server` |
| templ | ✓ | | | | `templ` |
| tera | ✓ | | | | |
| textproto | ✓ | ✓ | ✓ | | |
| tfvars | ✓ | | | | `terraform-ls` |
| thrift | ✓ | | | | |
| tlaplus | ✓ | | | | |
| todotxt | ✓ | | | | |
| toml | ✓ | ✓ | | | `taplo`, `tombi` |
| tsq | ✓ | | | | `ts_query_ls` |
| tsx | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
| twig | ✓ | | | | |
| typescript | ✓ | ✓ | ✓ | ✓ | `typescript-language-server` |
| typespec | ✓ | ✓ | ✓ | | `tsp-server` |
| typst | ✓ | | | ✓ | `tinymist` |
| ungrammar | ✓ | | | | |
| unison | ✓ | ✓ | ✓ | | |
| uxntal | ✓ | | | | |
| v | ✓ | ✓ | ✓ | | `v-analyzer` |
| vala | ✓ | ✓ | | | `vala-language-server` |
| vento | ✓ | | | | |
| verilog | ✓ | ✓ | | | `svlangserver` |
| vhdl | ✓ | | | | `vhdl_ls` |
| vhs | ✓ | | | | |
| vue | ✓ | | | | `vue-language-server` |
| wast | ✓ | | | | |
| wat | ✓ | | | | `wat_server` |
| webc | ✓ | | | | |
| werk | ✓ | | | | |
| wesl | ✓ | ✓ | | | |
| wgsl | ✓ | | | | `wgsl-analyzer` |
| wit | ✓ | | ✓ | | |
| wren | ✓ | ✓ | ✓ | | |
| xit | ✓ | | | | |
| xml | ✓ | | ✓ | | |
| xtc | ✓ | | | | |
| yaml | ✓ | ✓ | ✓ | | `yaml-language-server`, `ansible-language-server` |
| yara | ✓ | | | | `yls` |
| yuck | ✓ | | | | |
| zig | ✓ | ✓ | ✓ | | `zls` |

View File

@@ -100,13 +100,16 @@
| `file_picker` | Open file picker | normal: `` <space>f ``, select: `` <space>f `` |
| `file_picker_in_current_buffer_directory` | Open file picker at current buffer's directory | |
| `file_picker_in_current_directory` | Open file picker at current working directory | normal: `` <space>F ``, select: `` <space>F `` |
| `file_explorer` | Open file explorer in workspace root | normal: `` <space>e ``, select: `` <space>e `` |
| `file_explorer_in_current_buffer_directory` | Open file explorer at current buffer's directory | normal: `` <space>E ``, select: `` <space>E `` |
| `file_explorer_in_current_directory` | Open file explorer at current working directory | |
| `code_action` | Perform code action | normal: `` <space>a ``, select: `` <space>a `` |
| `buffer_picker` | Open buffer picker | normal: `` <space>b ``, select: `` <space>b `` |
| `jumplist_picker` | Open jumplist picker | normal: `` <space>j ``, select: `` <space>j `` |
| `symbol_picker` | Open symbol picker | normal: `` <space>s ``, select: `` <space>s `` |
| `symbol_picker` | Open symbol picker | |
| `changed_file_picker` | Open changed file picker | normal: `` <space>g ``, select: `` <space>g `` |
| `select_references_to_symbol_under_cursor` | Select symbol references | normal: `` <space>h ``, select: `` <space>h `` |
| `workspace_symbol_picker` | Open workspace symbol picker | normal: `` <space>S ``, select: `` <space>S `` |
| `workspace_symbol_picker` | Open workspace symbol picker | |
| `diagnostics_picker` | Open diagnostic picker | normal: `` <space>d ``, select: `` <space>d `` |
| `workspace_diagnostics_picker` | Open workspace diagnostic picker | normal: `` <space>D ``, select: `` <space>D `` |
| `last_picker` | Open last picker | normal: `` <space>' ``, select: `` <space>' `` |
@@ -123,8 +126,10 @@
| `add_newline_below` | Add newline below | normal: `` ]<space> ``, select: `` ]<space> `` |
| `goto_type_definition` | Goto type definition | normal: `` gy ``, select: `` gy `` |
| `goto_implementation` | Goto implementation | normal: `` gi ``, select: `` gi `` |
| `goto_file_start` | Goto line number <n> else file start | normal: `` gg ``, select: `` gg `` |
| `goto_file_start` | Goto line number <n> else file start | normal: `` gg `` |
| `goto_file_end` | Goto file end | |
| `extend_to_file_start` | Extend to line number<n> else file start | select: `` gg `` |
| `extend_to_file_end` | Extend to file end | |
| `goto_file` | Goto files/URLs in selections | normal: `` gf ``, select: `` gf `` |
| `goto_file_hsplit` | Goto files in selections (hsplit) | normal: `` <C-w>f ``, `` <space>wf ``, select: `` <C-w>f ``, `` <space>wf `` |
| `goto_file_vsplit` | Goto files in selections (vsplit) | normal: `` <C-w>F ``, `` <space>wF ``, select: `` <C-w>F ``, `` <space>wF `` |
@@ -136,7 +141,8 @@
| `goto_last_modified_file` | Goto last modified file | normal: `` gm ``, select: `` gm `` |
| `goto_last_modification` | Goto last modification | normal: `` g. ``, select: `` g. `` |
| `goto_line` | Goto line | normal: `` G ``, select: `` G `` |
| `goto_last_line` | Goto last line | normal: `` ge ``, select: `` ge `` |
| `goto_last_line` | Goto last line | normal: `` ge `` |
| `extend_to_last_line` | Extend to last line | select: `` ge `` |
| `goto_first_diag` | Goto first diagnostic | normal: `` [D ``, select: `` [D `` |
| `goto_last_diag` | Goto last diagnostic | normal: `` ]D ``, select: `` ]D `` |
| `goto_next_diag` | Goto next diagnostic | normal: `` ]d ``, select: `` ]d `` |
@@ -147,6 +153,8 @@
| `goto_last_change` | Goto last change | normal: `` ]G ``, select: `` ]G `` |
| `goto_line_start` | Goto line start | normal: `` gh ``, `` <home> ``, select: `` gh ``, insert: `` <home> `` |
| `goto_line_end` | Goto line end | normal: `` gl ``, `` <end> ``, select: `` gl `` |
| `goto_column` | Goto column | normal: `` g\| `` |
| `extend_to_column` | Extend to column | select: `` g\| `` |
| `goto_next_buffer` | Goto next buffer | normal: `` gn ``, select: `` gn `` |
| `goto_previous_buffer` | Goto previous buffer | normal: `` gp ``, select: `` gp `` |
| `goto_line_end_newline` | Goto newline at line end | insert: `` <end> `` |
@@ -236,6 +244,7 @@
| `wonly` | Close windows except current | normal: `` <C-w>o ``, `` <space>wo ``, `` <C-w><C-o> ``, `` <space>w<C-o> ``, select: `` <C-w>o ``, `` <space>wo ``, `` <C-w><C-o> ``, `` <space>w<C-o> `` |
| `select_register` | Select register | normal: `` " ``, select: `` " `` |
| `insert_register` | Insert register | insert: `` <C-r> `` |
| `copy_between_registers` | Copy between two registers | |
| `align_view_middle` | Align view middle | normal: `` Zm ``, `` zm ``, select: `` Zm ``, `` zm `` |
| `align_view_top` | Align view top | normal: `` Zt ``, `` zt ``, select: `` Zt ``, `` zt `` |
| `align_view_center` | Align view center | normal: `` Zc ``, `` Zz ``, `` zc ``, `` zz ``, select: `` Zc ``, `` Zz ``, `` zc ``, `` zz `` |
@@ -292,5 +301,11 @@
| `command_palette` | Open command palette | normal: `` <space>? ``, select: `` <space>? `` |
| `goto_word` | Jump to a two-character label | normal: `` gw `` |
| `extend_to_word` | Extend to a two-character label | select: `` gw `` |
| `goto_next_tabstop` | goto next snippet placeholder | |
| `goto_prev_tabstop` | goto next snippet placeholder | |
| `goto_next_tabstop` | Goto next snippet placeholder | |
| `goto_prev_tabstop` | Goto next snippet placeholder | |
| `rotate_selections_first` | Make the first selection your primary one | |
| `rotate_selections_last` | Make the last selection your primary one | |
| `syntax_symbol_picker` | Open symbol picker from syntax information | |
| `syntax_workspace_symbol_picker` | Open workspace symbol picker from syntax information | |
| `lsp_or_syntax_symbol_picker` | Open symbol picker from LSP or syntax information | normal: `` <space>s ``, select: `` <space>s `` |
| `lsp_or_syntax_workspace_symbol_picker` | Open workspace symbol picker from LSP or syntax information | normal: `` <space>S ``, select: `` <space>S `` |

View File

@@ -52,8 +52,8 @@
| `: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 servers used by the current doc |
| `:lsp-stop` | Stops the language servers that are used by the current doc |
| `:lsp-restart` | Restarts the given language servers, or all language servers that are used by the current file if no arguments are supplied |
| `:lsp-stop` | Stops the given language servers, or all language servers that are used by the current file if no arguments are supplied |
| `: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. |
@@ -67,10 +67,9 @@
| `:goto`, `:g` | Goto line number. |
| `:set-language`, `:lang` | Set the language of current buffer (show current language if no value specified). |
| `:set-option`, `:set` | Set a config option at runtime.<br>For example to disable smart case search, use `:set search.smart-case false`. |
| `:toggle-option`, `:toggle` | Toggle a boolean config option at runtime.<br>For example to toggle smart case search, use `:toggle search.smart-case`. |
| `:toggle-option`, `:toggle` | Toggle a config option at runtime.<br>For example to toggle smart case search, use `:toggle search.smart-case`. |
| `:get-option`, `:get` | Get the current value of a config option. |
| `:sort` | Sort ranges in selection. |
| `:rsort` | Sort ranges in selection in reverse order. |
| `:reflow` | Hard-wrap the current selection of lines to a given width. |
| `:tree-sitter-subtree`, `:ts-subtree` | Display the smallest tree-sitter subtree that spans the primary selection, primarily for debugging queries. |
| `:config-reload` | Refresh user config. |
@@ -79,12 +78,14 @@
| `:log-open` | Open the helix log file. |
| `:insert-output` | Run shell command, inserting output before each selection. |
| `:append-output` | Run shell command, appending output after each selection. |
| `:pipe` | Pipe each selection to the shell command. |
| `:pipe`, `:\|` | Pipe each selection to the shell command. |
| `:pipe-to` | Pipe each selection to the shell command, ignoring output. |
| `:run-shell-command`, `:sh` | Run a shell command |
| `: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 |
| `:move`, `:mv` | Move the current buffer and its corresponding file to a different path |
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
| `:read`, `:r` | Load a file into buffer |
| `:echo` | Prints the given arguments to the statusline. |
| `:noop` | Does nothing. |

View File

@@ -38,12 +38,6 @@ below.
for more information on writing queries.
4. A list of highlight captures can be found [on the themes page](https://docs.helix-editor.com/themes.html#scopes).
> 💡 In Helix, the first matching query takes precedence when evaluating
> queries, which is different from other editors such as Neovim where the last
> matching query supersedes the ones before it. See
> [this issue](https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090)
> for an example.
## Common issues
- If you encounter errors when running Helix after switching branches, you may

View File

@@ -4,11 +4,16 @@ Writing language injection queries allows one to highlight a specific node as a
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:
An 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"))
```
Another example is this query, which highlights links in comments and keywords like "TODO", by reusing the dedicated "comment" language:
```
((comment) @injection.content
(#set! injection.language "comment"))
```
## Capture Types

34
book/src/guides/tags.md Normal file
View File

@@ -0,0 +1,34 @@
## Adding tags queries
See tree-sitter's documentation on [Code Navigation Systems] for more
background on tags queries.
Helix provides LSP-like features such as document and workspace symbol pickers
out-of-the-box for languages with `tags.scm` queries based on syntax trees. To
be analyzed a language must have a tree-sitter grammar and a `tags.scm` query
file which pattern matches interesting nodes from syntax trees.
Query files should be placed in `runtime/queries/{language}/tags.scm`
when contributing to Helix. You may place these under your local runtime
directory (`~/.config/helix/runtime` in Linux for example) for the sake of
testing.
The following [captures][tree-sitter-captures] are recognized:
| Capture name |
|--- |
| `definition.class` |
| `definition.constant` |
| `definition.function` |
| `definition.interface` |
| `definition.macro` |
| `definition.module` |
| `definition.struct` |
| `definition.type` |
[Example query files][example-queries] can be found in the Helix GitHub
repository.
[Code Navigation Systems]: https://tree-sitter.github.io/tree-sitter/4-code-navigation.html
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers/queries/index.html
[example-queries]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+path%3A%2A%2A/tags.scm&type=Code

View File

@@ -23,6 +23,7 @@ The following [captures][tree-sitter-captures] are recognized:
| `test.inside` |
| `test.around` |
| `parameter.inside` |
| `parameter.around` |
| `comment.inside` |
| `comment.around` |
| `entry.inside` |

View File

@@ -1,6 +1,7 @@
# Installing Helix
To install Helix, follow the instructions specific to your operating system.
The typical way to install Helix is via [your operating system's package manager](./package-managers.md).
Note that:
- To get the latest nightly version of Helix, you need to
@@ -8,7 +9,7 @@ Note that:
- To take full advantage of Helix, install the language servers for your
preferred programming languages. See the
[wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers)
[wiki](https://github.com/helix-editor/helix/wiki/Language-Server-Configurations)
for instructions.
## Pre-built binaries

View File

@@ -47,9 +47,9 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `W` | Move next WORD start | `move_next_long_word_start` |
| `B` | Move previous WORD start | `move_prev_long_word_start` |
| `E` | Move next WORD end | `move_next_long_word_end` |
| `t` | Find 'till next char | `find_till_char` |
| `t` | Find till next char | `find_till_char` |
| `f` | Find next char | `find_next_char` |
| `T` | Find 'till previous char | `till_prev_char` |
| `T` | Find till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
| `G` | Go to line number `<n>` | `goto_line` |
| `Alt-.` | Repeat last motion (`f`, `t`, `m`, `[` or `]`) | `repeat_last_motion` |
@@ -213,6 +213,7 @@ Jumps to various locations.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `g` | Go to line number `<n>` else start of file | `goto_file_start` |
| <code>&#124;</code> | Go to column number `<n>` else start of line | `goto_column` |
| `e` | Go to the end of the file | `goto_last_line` |
| `f` | Go to files in the selections | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` |

View File

@@ -66,9 +66,10 @@ These configuration keys are available:
| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) |
| `language-servers` | The Language Servers used for this language. See below for more information in the section [Configuring Language Servers for a language](#configuring-language-servers-for-a-language) |
| `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 |
| `soft-wrap` | [editor.softwrap](./configuration.md#editorsoft-wrap-section)
| `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. The filename of the current buffer can be passed as argument by using the `%{buffer_name}` expansion variable. See below for more information in the [Configuring the formatter command](#configuring-the-formatter-command) |
| `soft-wrap` | [editor.softwrap](./editor.md#editorsoft-wrap-section)
| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set, defaults to `editor.text-width` |
| `rulers` | Overrides the `editor.rulers` config key for the language. |
| `path-completion` | Overrides the `editor.path-completion` config key for the language. |
| `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml`. Overwrites the setting of the same name in `config.toml` if set. |
| `persistent-diagnostic-sources` | An array of LSP diagnostic sources assumed unchanged when the language server resends the same set of diagnostics. Helix can track the position for these diagnostics internally instead. Useful for diagnostics that are recomputed on save.
@@ -101,6 +102,16 @@ with the following priorities:
the file extension of a given file wins. In the example above, the `"toml"`
config matches files like `Cargo.toml` or `languages.toml`.
### Configuring the formatter command
[Command line expansions](./command-line.md#expansions) are supported in the arguments
of the formatter command. In particular, the `%{buffer_name}` variable can be passed as
argument to the formatter:
```toml
formatter = { command = "mylang-formatter" , args = ["--stdin", "--stdin-filename %{buffer_name}"] }
```
## Language Server configuration
Language servers are configured separately in the table `language-server` in the same file as the languages `languages.toml`

View File

@@ -0,0 +1,33 @@
# Helix mode in other software
Helix' keymap and interaction model ([Using Helix](#usage.md)) is easier to adopt if it can be used consistently in many editing contexts. Yet, certain use cases cannot easily be addressed directly in Helix. Similar to vim, this leads to the creation of "Helix mode" in various other software products, allowing Helix-style editing for a greater variety of use cases.
"Helix mode" is frequently still in early stages or missing entirely. For such cases, we also link to relevant bugs or discussions.
## Other editors
| Editor | Plugin or feature providing Helix editing | Comments
| --- | --- | --- |
| [Vim](https://www.vim.org/) | [helix.vim](https://github.com/chtenb/helix.vim) config |
| [IntelliJ IDEA](https://www.jetbrains.com/idea/) / [Android Studio](https://developer.android.com/studio)| [IdeaVim](https://plugins.jetbrains.com/plugin/164-ideavim) plugin + [helix.idea.vim](https://github.com/chtenb/helix.vim) config | Minimum recommended version is IdeaVim 2.19.0.
| [Visual Studio](https://visualstudio.microsoft.com/) | [VsVim](https://marketplace.visualstudio.com/items?itemName=JaredParMSFT.VsVim) plugin + [helix.vs.vim](https://github.com/chtenb/helix.vim) config |
| [Visual Studio Code](https://code.visualstudio.com/) | [Dance](https://marketplace.visualstudio.com/items?itemName=gregoire.dance) extension, or its [Helix fork](https://marketplace.visualstudio.com/items?itemName=kend.dancehelixkey) | The Helix fork has diverged. You can also use the original Dance and tweak its keybindings directly (try [this config](https://github.com/71/dance/issues/299#issuecomment-1655509531)).
| [Visual Studio Code](https://code.visualstudio.com/) | [Helix for VS Code](https://marketplace.visualstudio.com/items?itemName=jasew.vscode-helix-emulation) extension|
| [Zed](https://zed.dev/) | native via keybindings ([Bug](https://github.com/zed-industries/zed/issues/4642)) |
| [CodeMirror](https://codemirror.net/) | [codemirror-helix](https://gitlab.com/_rvidal/codemirror-helix) |
## Shells
| Shell | Plugin or feature providing Helix editing
| --- | ---
| Fish | [Feature Request](https://github.com/fish-shell/fish-shell/issues/7748)
| Fish | [fish-helix](https://github.com/sshilovsky/fish-helix/tree/main)
| Zsh | [helix-zsh](https://github.com/john-h-k/helix-zsh) or [zsh-helix-mode](https://github.com/Multirious/zsh-helix-mode)
| Nushell | [Feature Request](https://github.com/nushell/reedline/issues/639)
## Other software
| Software | Plugin or feature providing Helix editing. | Comments
| --- | --- | --- |
| [Obsidian](https://obsidian.md/) | [Obsidian-Helix](https://github.com/Sinono3/obsidian-helix) | Uses `codemirror-helix` listed above.

View File

@@ -12,7 +12,7 @@ There are three kinds of commands that can be used in keymaps:
in [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)
at the invocation of `static_commands!` macro.
* Typable commands: commands that can be executed from command mode (`:`), for
example `:write!`. See the [Commands](./commands.html) documentation for a
example `:write!`. See the [Commands](./commands.md) documentation for a
list of available typeable commands or the `TypableCommandList` declaration in
the source code at [`helix-term/src/commands/typed.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands/typed.rs).
* Macros: sequences of keys that are executed in order. These keybindings

View File

@@ -171,8 +171,10 @@ We use a similar set of scopes as
- `comment` - Code comments
- `line` - Single line comments (`//`)
- `documentation` - Line documentation comments (e.g. `///` in Rust)
- `block` - Block comments (e.g. (`/* */`)
- `documentation` - Documentation comments (e.g. `///` in Rust)
- `documentation` - Block documentation comments (e.g. `/** */` in Rust)
- `unused` - Unused variables and patterns, e.g. `_` and `_foo`
- `variable` - Variables
- `builtin` - Reserved language variables (`self`, `this`, `super`, etc.)
@@ -181,7 +183,7 @@ We use a similar set of scopes as
- `member` - Fields of composite data types (e.g. structs, unions)
- `private` - Private fields that use a unique syntax (currently just ECMAScript-based languages)
- `label`
- `label` - `.class`, `#id` in CSS, etc.
- `punctuation`
- `delimiter` - Commas, colons
@@ -216,7 +218,7 @@ We use a similar set of scopes as
- `namespace`
- `special`
- `special` - `derive` in Rust, etc.
- `markup`
- `heading`

View File

@@ -86,6 +86,6 @@ Keywords[ru]=текст;текстовый редактор;
Keywords[sr]=Текст;едитор;
Keywords[tr]=Metin;düzenleyici;
Icon=helix
Categories=Utility;TextEditor;
Categories=Utility;TextEditor;ConsoleOnly
StartupNotify=false
MimeType=text/english;text/plain;text/x-makefile;text/x-c++hdr;text/x-c++src;text/x-chdr;text/x-csrc;text/x-java;text/x-moc;text/x-pascal;text/x-tcl;text/x-tex;application/x-shellscript;text/x-c;text/x-c++;

View File

@@ -9,23 +9,23 @@ _hx() {
case "$prev" in
-g | --grammar)
COMPREPLY=($(compgen -W 'fetch build' -- "$cur"))
mapfile -t COMPREPLY < <(compgen -W 'fetch build' -- "$cur")
return 0
;;
--health)
languages=$(hx --health | tail -n '+7' | awk '{print $1}' | sed 's/\x1b\[[0-9;]*m//g')
COMPREPLY=($(compgen -W """$languages""" -- "$cur"))
mapfile -t COMPREPLY < <(compgen -W """$languages""" -- "$cur")
return 0
;;
esac
case "$2" in
-*)
COMPREPLY=($(compgen -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- """$2"""))
mapfile -t COMPREPLY < <(compgen -W "-h --help --tutor -V --version -v -vv -vvv --health -g --grammar --vsplit --hsplit -c --config --log" -- """$2""")
return 0
;;
*)
COMPREPLY=($(compgen -fd -- """$2"""))
mapfile -t COMPREPLY < <(compgen -fd -- """$2""")
return 0
;;
esac

View File

@@ -20,7 +20,7 @@ var config = [ "--config" "-c" ]
set edit:completion:arg-completer[hx] = {|@args|
var n = (count $args)
if (>= $n 3) {
# Stop completions if passed arg will take presedence
# Stop completions if passed arg will take precedence
# and invalidate further input
if (has-value $skips $args[-2]) {
return

View File

@@ -2,7 +2,7 @@
#
# NOTE: the `+N` syntax is not supported in Nushell (https://github.com/nushell/nushell/issues/13418)
# so it has not been specified here and will not be proposed in the autocompletion of Nushell.
# The help message won't be overriden though, so it will still be present here
# The help message won't be overridden though, so it will still be present here
def health_categories [] {
let languages = ^hx --health languages | detect columns | get Language | filter { $in != null }

View File

@@ -1,8 +1,85 @@
# Flake's default package for non-flake-enabled nix instances
let
compat = builtins.fetchTarball {
url = "https://github.com/edolstra/flake-compat/archive/b4a34015c698c7793d592d66adbab377907a2be8.tar.gz";
sha256 = "sha256:1qc703yg0babixi6wshn5wm2kgl5y1drcswgszh4xxzbrwkk9sv7";
};
{
lib,
rustPlatform,
callPackage,
runCommand,
installShellFiles,
git,
gitRev ? null,
grammarOverlays ? [],
includeGrammarIf ? _: true,
}: let
fs = lib.fileset;
src = fs.difference (fs.gitTracked ./.) (fs.unions [
./.envrc
./rustfmt.toml
./screenshot.png
./book
./docs
./runtime
./flake.lock
(fs.fileFilter (file: lib.strings.hasInfix ".git" file.name) ./.)
(fs.fileFilter (file: file.hasExt "svg") ./.)
(fs.fileFilter (file: file.hasExt "md") ./.)
(fs.fileFilter (file: file.hasExt "nix") ./.)
]);
# Next we actually need to build the grammars and the runtime directory
# that they reside in. It is built by calling the derivation in the
# grammars.nix file, then taking the runtime directory in the git repo
# and hooking symlinks up to it.
grammars = callPackage ./grammars.nix {inherit grammarOverlays includeGrammarIf;};
runtimeDir = runCommand "helix-runtime" {} ''
mkdir -p $out
ln -s ${./runtime}/* $out
rm -r $out/grammars
ln -s ${grammars} $out/grammars
'';
in
(import compat {src = ./.;}).defaultNix
rustPlatform.buildRustPackage (self: {
cargoLock = {
lockFile = ./Cargo.lock;
# This is not allowed in nixpkgs but is very convenient here: it allows us to
# avoid specifying `outputHashes` here for any git dependencies we might take
# on temporarily.
allowBuiltinFetchGit = true;
};
nativeBuildInputs = [
installShellFiles
git
];
buildType = "release";
name = with builtins; (fromTOML (readFile ./helix-term/Cargo.toml)).package.name;
src = fs.toSource {
root = ./.;
fileset = src;
};
# Helix attempts to reach out to the network and get the grammars. Nix doesn't allow this.
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
# So Helix knows what rev it is.
HELIX_NIX_BUILD_REV = gitRev;
doCheck = false;
strictDeps = true;
# Sets the Helix runtime dir to the grammars
env.HELIX_DEFAULT_RUNTIME = "${runtimeDir}";
# Get all the application stuff in the output directory.
postInstall = ''
mkdir -p $out/lib
installShellCompletion ${./contrib/completion}/hx.{bash,fish,zsh}
mkdir -p $out/share/{applications,icons/hicolor/{256x256,scalable}/apps}
cp ${./contrib/Helix.desktop} $out/share/applications/Helix.desktop
cp ${./logo.svg} $out/share/icons/hicolor/scalable/apps/helix.svg
cp ${./contrib/helix.png} $out/share/icons/hicolor/256x256/apps/helix.png
'';
meta.mainProgram = "hx";
})

View File

@@ -13,8 +13,10 @@ Some suggestions to get started:
- Instead of running a release version of Helix, while developing you may want to run in debug mode with `cargo run` which is way faster to compile
- Looking for even faster compile times? Give a try to [mold](https://github.com/rui314/mold)
- If your preferred language is missing, integrating a tree-sitter grammar for
it and defining syntax highlight queries for it is straight forward and
it and defining syntax highlight queries for it is straightforward and
doesn't require much knowledge of the internals.
- If you don't use the Nix development shell and are getting your rust-analyzer binary from rustup, you may need to run `rustup component add rust-analyzer`.
This is because `rust-toolchain.toml` selects our MSRV for the development toolchain but doesn't download the matching rust-analyzer automatically.
We provide an [architecture.md][architecture.md] that should give you
a good overview of the internals.

62
flake.lock generated
View File

@@ -1,45 +1,12 @@
{
"nodes": {
"crane": {
"locked": {
"lastModified": 1727974419,
"narHash": "sha256-WD0//20h+2/yPGkO88d2nYbb23WMWYvnRyDQ9Dx4UHg=",
"owner": "ipetkov",
"repo": "crane",
"rev": "37e4f9f0976cb9281cd3f0c70081e5e0ecaee93f",
"type": "github"
},
"original": {
"owner": "ipetkov",
"repo": "crane",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1726560853,
"narHash": "sha256-X6rJYSESBVr3hBoH0WbKE5KvhPU5bloyZ2L4K60/fPQ=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "c1dfcf08411b08f6b8615f7d8971a2bfa81d5e8a",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1728018373,
"narHash": "sha256-NOiTvBbRLIOe5F6RbHaAh6++BNjsb149fGZd1T4+KBg=",
"lastModified": 1740560979,
"narHash": "sha256-Vr3Qi346M+8CjedtbyUevIGDZW8LcA1fTG0ugPY/Hic=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "bc947f541ae55e999ffdb4013441347d83b00feb",
"rev": "5135c59491985879812717f4c9fea69604e7f26f",
"type": "github"
},
"original": {
@@ -51,8 +18,6 @@
},
"root": {
"inputs": {
"crane": "crane",
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
@@ -64,11 +29,11 @@
]
},
"locked": {
"lastModified": 1728268235,
"narHash": "sha256-lJMFnMO4maJuNO6PQ5fZesrTmglze3UFTTBuKGwR1Nw=",
"lastModified": 1740623427,
"narHash": "sha256-3SdPQrZoa4odlScFDUHd4CUPQ/R1gtH4Mq9u8CBiK8M=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "25685cc2c7054efc31351c172ae77b21814f2d42",
"rev": "d342e8b5fd88421ff982f383c853f0fc78a847ab",
"type": "github"
},
"original": {
@@ -76,21 +41,6 @@
"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",

243
flake.nix
View File

@@ -3,191 +3,98 @@
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";
};
crane.url = "github:ipetkov/crane";
};
outputs = {
self,
nixpkgs,
crane,
flake-utils,
rust-overlay,
...
}:
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
mkRootPath = rel:
builtins.path {
path = "${toString ./.}/${rel}";
name = rel;
}: let
inherit (nixpkgs) lib;
systems = [
"x86_64-linux"
"aarch64-linux"
"x86_64-darwin"
"aarch64-darwin"
];
eachSystem = lib.genAttrs systems;
pkgsFor = eachSystem (system:
import nixpkgs {
localSystem.system = system;
overlays = [(import rust-overlay) self.overlays.helix];
});
gitRev = self.rev or self.dirtyRev or null;
in {
packages = eachSystem (system: {
inherit (pkgsFor.${system}) helix;
/*
The default Helix build. Uses the latest stable Rust toolchain, and unstable
nixpkgs.
The build inputs can be overridden with the following:
packages.${system}.default.override { rustPlatform = newPlatform; };
Overriding a derivation attribute can be done as well:
packages.${system}.default.overrideAttrs { buildType = "debug"; };
*/
default = self.packages.${system}.helix;
});
checks =
lib.mapAttrs (system: pkgs: let
# Get Helix's MSRV toolchain to build with by default.
msrvToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
msrvPlatform = pkgs.makeRustPlatform {
cargo = msrvToolchain;
rustc = msrvToolchain;
};
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 = self.packages.${system}.helix.override {
rustPlatform = msrvPlatform;
};
})
pkgsFor;
# Devshell behavior is preserved.
devShells =
lib.mapAttrs (system: pkgs: {
default = let
commonRustFlagsEnv = "-C link-arg=-fuse-ld=lld -C target-cpu=native --cfg tokio_unstable";
platformRustFlagsEnv = lib.optionalString pkgs.stdenv.isLinux "-Clink-arg=-Wl,--no-rosegment";
in
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 =
(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}"
'';
in
helix-wrapped
// {
override = makeOverridableHelix old;
passthru =
helix-wrapped.passthru
// {
wrapper = old: makeOverridableHelix old config;
};
};
stdenv =
if pkgs.stdenv.isLinux
then pkgs.stdenv
else pkgs.clangStdenv;
rustFlagsEnv = pkgs.lib.optionalString stdenv.isLinux "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment --cfg tokio_unstable";
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;
inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;}) pname;
inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./Cargo.toml;}) version;
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];
nativeBuildInputs = [pkgs.installShellFiles];
# disable tests
doCheck = false;
meta.mainProgram = "hx";
};
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
installShellCompletion contrib/completion/hx.{bash,fish,zsh}
pkgs.mkShell {
inputsFrom = [self.checks.${system}.helix];
nativeBuildInputs = with pkgs;
[
lld
cargo-flamegraph
rust-bin.nightly.latest.rust-analyzer
]
++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin)
++ (lib.optional stdenv.isLinux lldb)
++ (lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.CoreFoundation);
shellHook = ''
export RUST_BACKTRACE="1"
export RUSTFLAGS="''${RUSTFLAGS:-""} ${commonRustFlagsEnv} ${platformRustFlagsEnv}"
'';
# set git revision for nix flake builds, see 'git_hash' in helix-loader/build.rs
HELIX_NIX_BUILD_REV = self.rev or self.dirtyRev or null;
});
helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {};
default = self.packages.${system}.helix;
};
})
pkgsFor;
overlays = {
helix = final: prev: {
helix = final.callPackage ./default.nix {inherit gitRev;};
};
checks = {
# Build the crate itself
inherit (self.packages.${system}) helix;
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="''${RUSTFLAGS:-""} ${rustFlagsEnv}"
'';
};
})
// {
overlays.default = final: prev: {
inherit (self.packages.${final.system}) helix;
};
default = self.overlays.helix;
};
};
nixConfig = {
extra-substituters = ["https://helix.cachix.org"];
extra-trusted-public-keys = ["helix.cachix.org-1:ejp9KQpR1FBI2onstMQ34yogDm4OgU2ru6lIwPvuCVs="];

View File

@@ -1,22 +1,13 @@
{
stdenv,
lib,
runCommandLocal,
runCommand,
yj,
includeGrammarIf ? _: true,
grammarOverlays ? [],
...
}: let
# HACK: nix < 2.6 has a bug in the toml parser, so we convert to JSON
# before parsing
languages-json = runCommandLocal "languages-toml-to-json" {} ''
${yj}/bin/yj -t < ${./languages.toml} > $out
'';
languagesConfig =
if lib.versionAtLeast builtins.nixVersion "2.6.0"
then builtins.fromTOML (builtins.readFile ./languages.toml)
else builtins.fromJSON (builtins.readFile (builtins.toPath languages-json));
builtins.fromTOML (builtins.readFile ./languages.toml);
isGitGrammar = grammar:
builtins.hasAttr "source" grammar
&& builtins.hasAttr "git" grammar.source
@@ -32,10 +23,10 @@
# If `use-grammars.except` is set, use all other grammars.
# Otherwise use all grammars.
useGrammar = grammar:
if languagesConfig?use-grammars.only then
builtins.elem grammar.name languagesConfig.use-grammars.only
else if languagesConfig?use-grammars.except then
!(builtins.elem grammar.name languagesConfig.use-grammars.except)
if languagesConfig ? use-grammars.only
then builtins.elem grammar.name languagesConfig.use-grammars.only
else if languagesConfig ? use-grammars.except
then !(builtins.elem grammar.name languagesConfig.use-grammars.except)
else true;
grammarsToUse = builtins.filter useGrammar languagesConfig.grammar;
gitGrammars = builtins.filter isGitGrammar grammarsToUse;
@@ -66,10 +57,10 @@
version = grammar.source.rev;
src = source;
sourceRoot = if builtins.hasAttr "subpath" grammar.source then
"source/${grammar.source.subpath}"
else
"source";
sourceRoot =
if builtins.hasAttr "subpath" grammar.source
then "source/${grammar.source.subpath}"
else "source";
dontConfigure = true;
@@ -116,17 +107,21 @@
'';
};
grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
builtGrammars = builtins.map (grammar: {
inherit (grammar) name;
value = buildGrammar grammar;
}) grammarsToBuild;
builtGrammars =
builtins.map (grammar: {
inherit (grammar) name;
value = buildGrammar grammar;
})
grammarsToBuild;
extensibleGrammars =
lib.makeExtensible (self: builtins.listToAttrs builtGrammars);
overlayedGrammars = lib.pipe extensibleGrammars
overlaidGrammars =
lib.pipe extensibleGrammars
(builtins.map (overlay: grammar: grammar.extend overlay) grammarOverlays);
grammarLinks = lib.mapAttrsToList
grammarLinks =
lib.mapAttrsToList
(name: artifact: "ln -s ${artifact}/${name}.so $out/${name}.so")
(lib.filterAttrs (n: v: lib.isDerivation v) overlayedGrammars);
(lib.filterAttrs (n: v: lib.isDerivation v) overlaidGrammars);
in
runCommand "consolidated-helix-grammars" {} ''
mkdir -p $out

View File

@@ -20,10 +20,10 @@ helix-stdx = { path = "../helix-stdx" }
helix-loader = { path = "../helix-loader" }
helix-parsec = { path = "../helix-parsec" }
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
smallvec = "1.13"
ropey.workspace = true
smallvec = "1.15"
smartstring = "1.0.1"
unicode-segmentation = "1.12"
unicode-segmentation.workspace = true
# unicode-width is changing width definitions
# that both break our logic and disagree with common
# width definitions in terminals, we need to replace it.
@@ -32,14 +32,12 @@ unicode-segmentation = "1.12"
unicode-width = "=0.1.12"
unicode-general-category = "1.0"
slotmap.workspace = true
tree-sitter.workspace = true
once_cell = "1.20"
tree-house.workspace = true
once_cell = "1.21"
arc-swap = "1"
regex = "1"
bitflags.workspace = true
ahash = "0.8.11"
hashbrown = { version = "0.14.5", features = ["raw"] }
dunce = "1.0"
foldhash.workspace = true
url = "2.5.4"
log = "0.4"
@@ -48,20 +46,18 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
imara-diff = "0.1.7"
imara-diff = "0.2.0"
encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
etcetera = "0.8"
textwrap = "0.16.1"
textwrap = "0.16.2"
nucleo.workspace = true
parking_lot = "0.12"
globset = "0.4.15"
regex-cursor = "0.1.4"
parking_lot.workspace = true
globset = "0.4.16"
regex-cursor = "0.1.5"
[dev-dependencies]
quickcheck = { version = "1", default-features = false }
indoc = "2.0.5"
indoc = "2.0.6"

File diff suppressed because it is too large Load Diff

View File

@@ -4,7 +4,8 @@
use smallvec::SmallVec;
use crate::{
syntax::BlockCommentToken, Change, Range, Rope, RopeSlice, Selection, Tendril, Transaction,
syntax::config::BlockCommentToken, Change, Range, Rope, RopeSlice, Selection, Tendril,
Transaction,
};
use helix_stdx::rope::RopeSliceExt;
use std::borrow::Cow;
@@ -204,13 +205,9 @@ pub fn find_block_comments(
range: *range,
start_pos,
end_pos,
start_margin: selection_slice
.get_char(after_start)
.map_or(false, |c| c == ' '),
start_margin: selection_slice.get_char(after_start) == Some(' '),
end_margin: after_start != before_end
&& selection_slice
.get_char(before_end)
.map_or(false, |c| c == ' '),
&& (selection_slice.get_char(before_end) == Some(' ')),
start_token: start_token.to_string(),
end_token: end_token.to_string(),
});

View File

@@ -1,6 +1,6 @@
use std::borrow::Cow;
use crate::Transaction;
use crate::{diagnostic::LanguageServerId, Transaction};
#[derive(Debug, PartialEq, Clone)]
pub struct CompletionItem {
@@ -8,5 +8,18 @@ pub struct CompletionItem {
pub label: Cow<'static, str>,
pub kind: Cow<'static, str>,
/// Containing Markdown
pub documentation: String,
pub documentation: Option<String>,
pub provider: CompletionProvider,
}
#[derive(Debug, PartialEq, Eq, Hash, Clone, Copy)]
pub enum CompletionProvider {
Lsp(LanguageServerId),
Path,
}
impl From<LanguageServerId> for CompletionProvider {
fn from(id: LanguageServerId) -> Self {
CompletionProvider::Lsp(id)
}
}

View File

@@ -1,4 +1,4 @@
use crate::syntax::{Configuration, Loader, LoaderError};
use crate::syntax::{config::Configuration, Loader, LoaderError};
/// Language configuration based on built-in languages.toml.
pub fn default_lang_config() -> Configuration {

View File

@@ -1,5 +1,5 @@
//! LSP diagnostic utility types.
use std::fmt;
use std::{fmt, sync::Arc};
pub use helix_stdx::range::Range;
use serde::{Deserialize, Serialize};
@@ -50,8 +50,35 @@ pub struct Diagnostic {
pub data: Option<serde_json::Value>,
}
// TODO turn this into an enum + feature flag when lsp becomes optional
pub type DiagnosticProvider = LanguageServerId;
/// The source of a diagnostic.
///
/// This type is cheap to clone: all data is either `Copy` or wrapped in an `Arc`.
#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum DiagnosticProvider {
Lsp {
/// The ID of the language server which sent the diagnostic.
server_id: LanguageServerId,
/// An optional identifier under which diagnostics are managed by the client.
///
/// `identifier` is a field from the LSP "Pull Diagnostics" feature meant to provide an
/// optional "namespace" for diagnostics: a language server can respond to a diagnostics
/// pull request with an identifier and these diagnostics should be treated as separate
/// from push diagnostics. Rust-analyzer uses this feature for example to provide Cargo
/// diagnostics with push and internal diagnostics with pull. The push diagnostics should
/// not clear the pull diagnostics and vice-versa.
identifier: Option<Arc<str>>,
},
// Future internal features can go here...
}
impl DiagnosticProvider {
pub fn language_server_id(&self) -> Option<LanguageServerId> {
match self {
Self::Lsp { server_id, .. } => Some(*server_id),
// _ => None,
}
}
}
// while I would prefer having this in helix-lsp that necessitates a bunch of
// conversions I would rather not add. I think its fine since this just a very

View File

@@ -1,51 +1,22 @@
use std::ops::Range;
use std::time::Instant;
use imara_diff::intern::InternedInput;
use imara_diff::Algorithm;
use imara_diff::{Algorithm, Diff, Hunk, IndentHeuristic, IndentLevel, InternedInput};
use ropey::RopeSlice;
use crate::{ChangeSet, Rope, Tendril, Transaction};
/// A `imara_diff::Sink` that builds a `ChangeSet` for a character diff of a hunk
struct CharChangeSetBuilder<'a> {
res: &'a mut ChangeSet,
hunk: &'a InternedInput<char>,
pos: u32,
}
impl imara_diff::Sink for CharChangeSetBuilder<'_> {
type Out = ();
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
self.res.retain((before.start - self.pos) as usize);
self.res.delete(before.len());
self.pos = before.end;
let res = self.hunk.after[after.start as usize..after.end as usize]
.iter()
.map(|&token| self.hunk.interner[token])
.collect();
self.res.insert(res);
}
fn finish(self) -> Self::Out {
self.res.retain(self.hunk.before.len() - self.pos as usize);
}
}
struct LineChangeSetBuilder<'a> {
struct ChangeSetBuilder<'a> {
res: ChangeSet,
after: RopeSlice<'a>,
file: &'a InternedInput<RopeSlice<'a>>,
current_hunk: InternedInput<char>,
char_diff: Diff,
pos: u32,
}
impl imara_diff::Sink for LineChangeSetBuilder<'_> {
type Out = ChangeSet;
fn process_change(&mut self, before: Range<u32>, after: Range<u32>) {
impl ChangeSetBuilder<'_> {
fn process_hunk(&mut self, before: Range<u32>, after: Range<u32>) {
let len = self.file.before[self.pos as usize..before.start as usize]
.iter()
.map(|&it| self.file.interner[it].len_chars())
@@ -109,25 +80,36 @@ impl imara_diff::Sink for LineChangeSetBuilder<'_> {
.flat_map(|&it| self.file.interner[it].chars());
self.current_hunk.update_before(hunk_before);
self.current_hunk.update_after(hunk_after);
// the histogram heuristic does not work as well
// for characters because the same characters often reoccur
// use myer diff instead
imara_diff::diff(
self.char_diff.compute_with(
Algorithm::Myers,
&self.current_hunk,
CharChangeSetBuilder {
res: &mut self.res,
hunk: &self.current_hunk,
pos: 0,
},
&self.current_hunk.before,
&self.current_hunk.after,
self.current_hunk.interner.num_tokens(),
);
let mut pos = 0;
for Hunk { before, after } in self.char_diff.hunks() {
self.res.retain((before.start - pos) as usize);
self.res.delete(before.len());
pos = before.end;
let res = self.current_hunk.after[after.start as usize..after.end as usize]
.iter()
.map(|&token| self.current_hunk.interner[token])
.collect();
self.res.insert(res);
}
self.res
.retain(self.current_hunk.before.len() - pos as usize);
// reuse allocations
self.current_hunk.clear();
}
}
fn finish(mut self) -> Self::Out {
fn finish(mut self) -> ChangeSet {
let len = self.file.before[self.pos as usize..]
.iter()
.map(|&it| self.file.interner[it].len_chars())
@@ -140,7 +122,7 @@ impl imara_diff::Sink for LineChangeSetBuilder<'_> {
struct RopeLines<'a>(RopeSlice<'a>);
impl<'a> imara_diff::intern::TokenSource for RopeLines<'a> {
impl<'a> imara_diff::TokenSource for RopeLines<'a> {
type Token = RopeSlice<'a>;
type Tokenizer = ropey::iter::Lines<'a>;
@@ -161,15 +143,23 @@ pub fn compare_ropes(before: &Rope, after: &Rope) -> Transaction {
let res = ChangeSet::with_capacity(32);
let after = after.slice(..);
let file = InternedInput::new(RopeLines(before.slice(..)), RopeLines(after));
let builder = LineChangeSetBuilder {
let mut builder = ChangeSetBuilder {
res,
file: &file,
after,
pos: 0,
current_hunk: InternedInput::default(),
char_diff: Diff::default(),
};
let res = imara_diff::diff(Algorithm::Histogram, &file, builder).into();
let mut diff = Diff::compute(Algorithm::Histogram, &file);
diff.postprocess_with_heuristic(
&file,
IndentHeuristic::new(|token| IndentLevel::for_ascii_line(file.interner[token].bytes(), 4)),
);
for hunk in diff.hunks() {
builder.process_hunk(hunk.before, hunk.after)
}
let res = builder.finish().into();
log::debug!(
"rope diff took {}s",

View File

@@ -19,10 +19,12 @@ mod test;
use unicode_segmentation::{Graphemes, UnicodeSegmentation};
use helix_stdx::rope::{RopeGraphemes, RopeSliceExt};
use crate::graphemes::{Grapheme, GraphemeStr};
use crate::syntax::Highlight;
use crate::text_annotations::TextAnnotations;
use crate::{Position, RopeGraphemes, RopeSlice};
use crate::{Position, RopeSlice};
/// TODO make Highlight a u32 to reduce the size of this enum to a single word.
#[derive(Debug, Clone, Copy)]
@@ -219,7 +221,7 @@ impl<'t> DocumentFormatter<'t> {
text_fmt,
annotations,
visual_pos: Position { row: 0, col: 0 },
graphemes: RopeGraphemes::new(text.slice(block_char_idx..)),
graphemes: text.slice(block_char_idx..).graphemes(),
char_pos: block_char_idx,
exhausted: false,
indent_level: None,
@@ -370,8 +372,8 @@ impl<'t> DocumentFormatter<'t> {
match col.cmp(&(self.text_fmt.viewport_width as usize)) {
// The EOF char and newline chars are always selectable in helix. That means
// that wrapping happens "too-early" if a word fits a line perfectly. This
// is intentional so that all selectable graphemes are always visisble (and
// therefore the cursor never dissapears). However if the user manually set a
// is intentional so that all selectable graphemes are always visible (and
// therefore the cursor never disappears). However if the user manually set a
// lower softwrap width then this is undesirable. Just increasing the viewport-
// width by one doesn't work because if a line is wrapped multiple times then
// some words may extend past the specified width.
@@ -380,9 +382,10 @@ impl<'t> DocumentFormatter<'t> {
// by a newline/eof character here.
Ordering::Equal
if self.text_fmt.soft_wrap_at_text_width
&& self.peek_grapheme(col, char_pos).map_or(false, |grapheme| {
grapheme.is_newline() || grapheme.is_eof()
}) => {}
&& self
.peek_grapheme(col, char_pos)
.is_some_and(|grapheme| grapheme.is_newline() || grapheme.is_eof()) => {
}
Ordering::Equal if word_width > self.text_fmt.max_wrap as usize => return,
Ordering::Greater if word_width > self.text_fmt.max_wrap as usize => {
self.peeked_grapheme = self.word_buf.pop();

View File

@@ -102,6 +102,14 @@ fn long_word_softwrap() {
);
}
#[test]
fn softwrap_multichar_grapheme() {
assert_eq!(
softwrap_text("xxxx xxxx xxx a\u{0301}bc\n"),
"xxxx xxxx xxx \n.ábc \n "
)
}
fn softwrap_text_at_text_width(text: &str) -> String {
let mut text_fmt = TextFormat::new_test(true);
text_fmt.soft_wrap_at_text_width = true;

View File

@@ -0,0 +1,333 @@
//! Support for [EditorConfig](https://EditorConfig.org) configuration loading.
//!
//! EditorConfig is an editor-agnostic format for specifying configuration in an INI-like, human
//! friendly syntax in `.editorconfig` files (which are intended to be checked into VCS). This
//! module provides functions to search for all `.editorconfig` files that apply to a given path
//! and returns an `EditorConfig` type containing any specified configuration options.
//!
//! At time of writing, this module follows the [spec](https://spec.editorconfig.org/) at
//! version 0.17.2.
use std::{
collections::HashMap,
fs,
num::{NonZeroU16, NonZeroU8},
path::Path,
str::FromStr,
};
use encoding_rs::Encoding;
use globset::{GlobBuilder, GlobMatcher};
use crate::{
indent::{IndentStyle, MAX_INDENT},
LineEnding,
};
/// Configuration declared for a path in `.editorconfig` files.
#[derive(Debug, Default, PartialEq, Eq)]
pub struct EditorConfig {
pub indent_style: Option<IndentStyle>,
pub tab_width: Option<NonZeroU8>,
pub line_ending: Option<LineEnding>,
pub encoding: Option<&'static Encoding>,
// pub spelling_language: Option<SpellingLanguage>,
pub trim_trailing_whitespace: Option<bool>,
pub insert_final_newline: Option<bool>,
pub max_line_length: Option<NonZeroU16>,
}
impl EditorConfig {
/// Finds any configuration in `.editorconfig` files which applies to the given path.
///
/// If no configuration applies then `EditorConfig::default()` is returned.
pub fn find(path: &Path) -> Self {
let mut configs = Vec::new();
// <https://spec.editorconfig.org/#file-processing>
for ancestor in path.ancestors() {
let editor_config_file = ancestor.join(".editorconfig");
let Ok(contents) = fs::read_to_string(&editor_config_file) else {
continue;
};
let ini = match contents.parse::<Ini>() {
Ok(ini) => ini,
Err(err) => {
log::warn!("Ignoring EditorConfig file at '{editor_config_file:?}' because a glob failed to compile: {err}");
continue;
}
};
let is_root = ini.pairs.get("root").map(AsRef::as_ref) == Some("true");
configs.push((ini, ancestor));
// > The search shall stop if an EditorConfig file is found with the `root` key set to
// > `true` in the preamble or when reaching the root filesystem directory.
if is_root {
break;
}
}
let mut pairs = Pairs::new();
// Reverse the configuration stack so that the `.editorconfig` files closest to `path`
// are applied last and overwrite settings in files closer to the search ceiling.
//
// > If multiple EditorConfig files have matching sections, the pairs from the closer
// > EditorConfig file are read last, so pairs in closer files take precedence.
for (config, dir) in configs.into_iter().rev() {
let relative_path = path.strip_prefix(dir).expect("dir is an ancestor of path");
for section in config.sections {
if section.glob.is_match(relative_path) {
log::info!(
"applying EditorConfig from section '{}' in file {:?}",
section.glob.glob(),
dir.join(".editorconfig")
);
pairs.extend(section.pairs);
}
}
}
Self::from_pairs(pairs)
}
fn from_pairs(pairs: Pairs) -> Self {
enum IndentSize {
Tab,
Spaces(NonZeroU8),
}
// <https://spec.editorconfig.org/#supported-pairs>
let indent_size = pairs.get("indent_size").and_then(|value| {
if value.as_ref() == "tab" {
Some(IndentSize::Tab)
} else if let Ok(spaces) = value.parse::<NonZeroU8>() {
Some(IndentSize::Spaces(spaces))
} else {
None
}
});
let tab_width = pairs
.get("tab_width")
.and_then(|value| value.parse::<NonZeroU8>().ok())
.or(match indent_size {
Some(IndentSize::Spaces(spaces)) => Some(spaces),
_ => None,
});
let indent_style = pairs
.get("indent_style")
.and_then(|value| match value.as_ref() {
"tab" => Some(IndentStyle::Tabs),
"space" => {
let spaces = match indent_size {
Some(IndentSize::Spaces(spaces)) => spaces.get(),
Some(IndentSize::Tab) => tab_width.map(|n| n.get()).unwrap_or(4),
None => 4,
};
Some(IndentStyle::Spaces(spaces.clamp(1, MAX_INDENT)))
}
_ => None,
});
let line_ending = pairs
.get("end_of_line")
.and_then(|value| match value.as_ref() {
"lf" => Some(LineEnding::LF),
"crlf" => Some(LineEnding::Crlf),
#[cfg(feature = "unicode-lines")]
"cr" => Some(LineEnding::CR),
_ => None,
});
let encoding = pairs.get("charset").and_then(|value| match value.as_ref() {
"latin1" => Some(encoding_rs::WINDOWS_1252),
"utf-8" => Some(encoding_rs::UTF_8),
// `utf-8-bom` is intentionally ignored.
// > `utf-8-bom` is discouraged.
"utf-16le" => Some(encoding_rs::UTF_16LE),
"utf-16be" => Some(encoding_rs::UTF_16BE),
_ => None,
});
let trim_trailing_whitespace =
pairs
.get("trim_trailing_whitespace")
.and_then(|value| match value.as_ref() {
"true" => Some(true),
"false" => Some(false),
_ => None,
});
let insert_final_newline = pairs
.get("insert_final_newline")
.and_then(|value| match value.as_ref() {
"true" => Some(true),
"false" => Some(false),
_ => None,
});
// This option is not in the spec but is supported by some editors.
// <https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties#max_line_length>
let max_line_length = pairs
.get("max_line_length")
.and_then(|value| value.parse::<NonZeroU16>().ok());
Self {
indent_style,
tab_width,
line_ending,
encoding,
trim_trailing_whitespace,
insert_final_newline,
max_line_length,
}
}
}
type Pairs = HashMap<Box<str>, Box<str>>;
#[derive(Debug)]
struct Section {
glob: GlobMatcher,
pairs: Pairs,
}
#[derive(Debug, Default)]
struct Ini {
pairs: Pairs,
sections: Vec<Section>,
}
impl FromStr for Ini {
type Err = globset::Error;
fn from_str(source: &str) -> Result<Self, Self::Err> {
// <https://spec.editorconfig.org/#file-format>
let mut ini = Ini::default();
// > EditorConfig files are in an INI-like file format. To read an EditorConfig file, take
// > one line at a time, from beginning to end. For each line:
for full_line in source.lines() {
// > 1. Remove all leading and trailing whitespace.
let line = full_line.trim();
// > 2. Process the remaining text as specified for its type below.
// > The types of lines are:
// > * Blank: contains nothing. Blank lines are ignored.
if line.is_empty() {
continue;
}
// > * Comment: starts with a ';' or '#'. Comment lines are ignored.
if line.starts_with([';', '#']) {
continue;
}
if let Some(section) = line.strip_prefix('[').and_then(|s| s.strip_suffix(']')) {
// > * Section Header: starts with a `[` and ends with a `]`. These lines define
// > globs...
// <https://spec.editorconfig.org/#glob-expressions>
// We need to modify the glob string slightly since EditorConfig's glob flavor
// doesn't match `globset`'s exactly. `globset` only allows '**' at the beginning
// or end of a glob or between two '/'s. (This replacement is not very fancy but
// should cover most practical cases.)
let mut glob_str = section.replace("**.", "**/*.");
if !is_glob_relative(section) {
glob_str.insert_str(0, "**/");
}
let glob = GlobBuilder::new(&glob_str)
.literal_separator(true)
.backslash_escape(true)
.build()?;
ini.sections.push(Section {
glob: glob.compile_matcher(),
pairs: Pairs::new(),
});
} else if let Some((key, value)) = line.split_once('=') {
// > * Key-Value Pair (or Pair): contains a key and a value, separated by an `=`.
// > * Key: The part before the first `=` on the line.
// > * Value: The part, if any, after the first `=` on the line.
// > * Keys and values are trimmed of leading and trailing whitespace, but
// > include any whitespace that is between non-whitespace characters.
// > * If a value is not provided, then the value is an empty string.
let key = key.trim().to_lowercase().into_boxed_str();
let value = value.trim().to_lowercase().into_boxed_str();
if let Some(section) = ini.sections.last_mut() {
section.pairs.insert(key, value);
} else {
ini.pairs.insert(key, value);
}
}
}
Ok(ini)
}
}
/// Determines whether a glob is relative to the directory of the config file.
fn is_glob_relative(source: &str) -> bool {
// > If the glob contains a path separator (a `/` not inside square brackets), then the
// > glob is relative to the directory level of the particular `.editorconfig` file itself.
let mut idx = 0;
while let Some(open) = source[idx..].find('[').map(|open| idx + open) {
if source[..open].contains('/') {
return true;
}
idx = source[open..]
.find(']')
.map_or(source.len(), |close| idx + close);
}
source[idx..].contains('/')
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn is_glob_relative_test() {
assert!(is_glob_relative("subdir/*.c"));
assert!(!is_glob_relative("*.txt"));
assert!(!is_glob_relative("[a/b].c"));
}
fn editor_config(path: impl AsRef<Path>, source: &str) -> EditorConfig {
let path = path.as_ref();
let ini = source.parse::<Ini>().unwrap();
let pairs = ini
.sections
.into_iter()
.filter(|section| section.glob.is_match(path))
.fold(Pairs::new(), |mut acc, section| {
acc.extend(section.pairs);
acc
});
EditorConfig::from_pairs(pairs)
}
#[test]
fn parse_test() {
let source = r#"
[*]
indent_style = space
[Makefile]
indent_style = tab
[docs/**.txt]
insert_final_newline = true
"#;
assert_eq!(
editor_config("a.txt", source),
EditorConfig {
indent_style: Some(IndentStyle::Spaces(4)),
..Default::default()
}
);
assert_eq!(
editor_config("pkg/Makefile", source),
EditorConfig {
indent_style: Some(IndentStyle::Tabs),
..Default::default()
}
);
assert_eq!(
editor_config("docs/config/editor.txt", source),
EditorConfig {
indent_style: Some(IndentStyle::Spaces(4)),
insert_final_newline: Some(true),
..Default::default()
}
);
}
}

View File

@@ -1,7 +1,7 @@
//! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents.
//!
//! Based on <https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs>
use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use ropey::{str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
@@ -64,7 +64,7 @@ impl<'a> Grapheme<'a> {
}
pub fn is_whitespace(&self) -> bool {
!matches!(&self, Grapheme::Other { g } if !g.chars().all(char_is_whitespace))
!matches!(&self, Grapheme::Other { g } if !g.chars().next().is_some_and(char_is_whitespace))
}
// TODO currently word boundaries are used for softwrapping.
@@ -72,7 +72,7 @@ impl<'a> Grapheme<'a> {
// This could however be improved in the future by considering unicode
// character classes but
pub fn is_word_boundary(&self) -> bool {
!matches!(&self, Grapheme::Other { g,.. } if g.chars().all(char_is_word))
!matches!(&self, Grapheme::Other { g,.. } if g.chars().next().is_some_and(char_is_word))
}
}
@@ -119,6 +119,9 @@ pub fn grapheme_width(g: &str) -> usize {
}
}
// NOTE: for byte indexing versions of these functions see `RopeSliceExt`'s
// `floor_grapheme_boundary` and `ceil_grapheme_boundary` and the rope grapheme iterators.
#[must_use]
pub fn nth_prev_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -> usize {
// Bounds check
@@ -208,43 +211,6 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
chunk_char_idx + tmp
}
#[must_use]
pub fn nth_next_grapheme_boundary_byte(slice: RopeSlice, mut byte_idx: usize, n: usize) -> usize {
// Bounds check
debug_assert!(byte_idx <= slice.len_bytes());
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, mut _chunk_char_idx, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Find the nth next grapheme cluster boundary.
for _ in 0..n {
loop {
match gc.next_boundary(chunk, chunk_byte_idx) {
Ok(None) => return slice.len_bytes(),
Ok(Some(n)) => {
byte_idx = n;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
chunk_byte_idx += chunk.len();
let (a, _, _c, _) = slice.chunk_at_byte(chunk_byte_idx);
chunk = a;
// chunk_char_idx = c;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = slice.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
byte_idx
}
/// Finds the next grapheme boundary after the given char position.
#[must_use]
#[inline(always)]
@@ -252,13 +218,6 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
nth_next_grapheme_boundary(slice, char_idx, 1)
}
/// Finds the next grapheme boundary after the given byte position.
#[must_use]
#[inline(always)]
pub fn next_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> usize {
nth_next_grapheme_boundary_byte(slice, byte_idx, 1)
}
/// Returns the passed char index if it's already a grapheme boundary,
/// or the next grapheme boundary char index if not.
#[must_use]
@@ -283,215 +242,6 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize
}
}
/// Returns whether the given char position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
// Bounds check
debug_assert!(char_idx <= slice.len_chars());
// We work with bytes for this, so convert.
let byte_idx = slice.char_to_byte(char_idx);
// Get the chunk with our byte index in it.
let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Determine if the given position is a grapheme cluster boundary.
loop {
match gc.is_boundary(chunk, chunk_byte_idx) {
Ok(n) => return n,
Err(GraphemeIncomplete::PreContext(n)) => {
let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
gc.provide_context(ctx_chunk, ctx_byte_start);
}
Err(_) => unreachable!(),
}
}
}
/// Returns whether the given byte position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary_byte(slice: RopeSlice, byte_idx: usize) -> bool {
// Bounds check
debug_assert!(byte_idx <= slice.len_bytes());
// Get the chunk with our byte index in it.
let (chunk, chunk_byte_idx, _, _) = slice.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, slice.len_bytes(), true);
// Determine if the given position is a grapheme cluster boundary.
loop {
match gc.is_boundary(chunk, chunk_byte_idx) {
Ok(n) => return n,
Err(GraphemeIncomplete::PreContext(n)) => {
let (ctx_chunk, ctx_byte_start, _, _) = slice.chunk_at_byte(n - 1);
gc.provide_context(ctx_chunk, ctx_byte_start);
}
Err(_) => unreachable!(),
}
}
}
/// An iterator over the graphemes of a `RopeSlice`.
#[derive(Clone)]
pub struct RopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
cursor: GraphemeCursor,
}
impl fmt::Debug for RopeGraphemes<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
// .field("cursor", &self.cursor)
.finish()
}
}
impl RopeGraphemes<'_> {
#[must_use]
pub fn new(slice: RopeSlice) -> RopeGraphemes {
let mut chunks = slice.chunks();
let first_chunk = chunks.next().unwrap_or("");
RopeGraphemes {
text: slice,
chunks,
cur_chunk: first_chunk,
cur_chunk_start: 0,
cursor: GraphemeCursor::new(0, slice.len_bytes(), true),
}
}
}
impl<'a> Iterator for RopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.next_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
self.cur_chunk_start += self.cur_chunk.len();
self.cur_chunk = self.chunks.next().unwrap_or("");
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a < self.cur_chunk_start {
Some(self.text.byte_slice(a..b))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[a2..b2]).into())
}
}
}
/// An iterator over the graphemes of a `RopeSlice` in reverse.
#[derive(Clone)]
pub struct RevRopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
cursor: GraphemeCursor,
}
impl fmt::Debug for RevRopeGraphemes<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RevRopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
// .field("cursor", &self.cursor)
.finish()
}
}
impl RevRopeGraphemes<'_> {
#[must_use]
pub fn new(slice: RopeSlice) -> RevRopeGraphemes {
let (mut chunks, mut cur_chunk_start, _, _) = slice.chunks_at_byte(slice.len_bytes());
chunks.reverse();
let first_chunk = chunks.next().unwrap_or("");
cur_chunk_start -= first_chunk.len();
RevRopeGraphemes {
text: slice,
chunks,
cur_chunk: first_chunk,
cur_chunk_start,
cursor: GraphemeCursor::new(slice.len_bytes(), slice.len_bytes(), true),
}
}
}
impl<'a> Iterator for RevRopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
self.cur_chunk = self.chunks.next().unwrap_or("");
self.cur_chunk_start -= self.cur_chunk.len();
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a >= self.cur_chunk_start + self.cur_chunk.len() {
Some(self.text.byte_slice(b..a))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[b2..a2]).into())
}
}
}
/// A highly compressed Cow<'a, str> that holds
/// atmost u31::MAX bytes and is readonly
pub struct GraphemeStr<'a> {

View File

@@ -1,14 +1,18 @@
use std::{borrow::Cow, collections::HashMap, iter};
use helix_stdx::rope::RopeSliceExt;
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use tree_house::TREE_SITTER_MATCH_LIMIT;
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
graphemes::{grapheme_width, tab_width_at},
syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node,
Position, Rope, RopeGraphemes, RopeSlice, Tendril,
syntax::{self, config::IndentationHeuristic},
tree_sitter::{
self,
query::{InvalidPredicateError, UserPredicate},
Capture, Grammar, InactiveQueryCursor, Node, Pattern, Query, QueryMatch, RopeInput,
},
Position, Rope, RopeSlice, Syntax, Tendril,
};
/// Enum representing indentation style.
@@ -200,7 +204,7 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: us
/// Create a string of tabs & spaces that has the same visual width as the given RopeSlice (independent of the tab width).
fn whitespace_with_same_width(text: RopeSlice) -> String {
let mut s = String::new();
for grapheme in RopeGraphemes::new(text) {
for grapheme in text.graphemes() {
if grapheme == "\t" {
s.push('\t');
} else {
@@ -279,18 +283,164 @@ fn add_indent_level(
/// Return true if only whitespace comes before the node on its line.
/// If given, new_line_byte_pos is treated the same way as any existing newline.
fn is_first_in_line(node: Node, text: RopeSlice, new_line_byte_pos: Option<usize>) -> bool {
let mut line_start_byte_pos = text.line_to_byte(node.start_position().row);
fn is_first_in_line(node: &Node, text: RopeSlice, new_line_byte_pos: Option<u32>) -> bool {
let line = text.byte_to_line(node.start_byte() as usize);
let mut line_start_byte_pos = text.line_to_byte(line) as u32;
if let Some(pos) = new_line_byte_pos {
if line_start_byte_pos < pos && pos <= node.start_byte() {
line_start_byte_pos = pos;
}
}
text.byte_slice(line_start_byte_pos..node.start_byte())
text.byte_slice(line_start_byte_pos as usize..node.start_byte() as usize)
.chars()
.all(|c| c.is_whitespace())
}
#[derive(Debug, Default)]
pub struct IndentQueryPredicates {
not_kind_eq: Vec<(Capture, Box<str>)>,
same_line: Option<(Capture, Capture, bool)>,
one_line: Option<(Capture, bool)>,
}
impl IndentQueryPredicates {
fn are_satisfied(
&self,
match_: &QueryMatch,
text: RopeSlice,
new_line_byte_pos: Option<u32>,
) -> bool {
for (capture, not_expected_kind) in self.not_kind_eq.iter() {
let node = match_.nodes_for_capture(*capture).next();
if node.is_some_and(|n| n.kind() == not_expected_kind.as_ref()) {
return false;
}
}
if let Some((capture1, capture2, negated)) = self.same_line {
let n1 = match_.nodes_for_capture(capture1).next();
let n2 = match_.nodes_for_capture(capture2).next();
let satisfied = n1.zip(n2).is_some_and(|(n1, n2)| {
let n1_line = get_node_start_line(text, n1, new_line_byte_pos);
let n2_line = get_node_start_line(text, n2, new_line_byte_pos);
let same_line = n1_line == n2_line;
same_line != negated
});
if !satisfied {
return false;
}
}
if let Some((capture, negated)) = self.one_line {
let node = match_.nodes_for_capture(capture).next();
let satisfied = node.is_some_and(|node| {
let start_line = get_node_start_line(text, node, new_line_byte_pos);
let end_line = get_node_end_line(text, node, new_line_byte_pos);
let one_line = end_line == start_line;
one_line != negated
});
if !satisfied {
return false;
}
}
true
}
}
#[derive(Debug)]
pub struct IndentQuery {
query: Query,
properties: HashMap<Pattern, IndentScope>,
predicates: HashMap<Pattern, IndentQueryPredicates>,
indent_capture: Option<Capture>,
indent_always_capture: Option<Capture>,
outdent_capture: Option<Capture>,
outdent_always_capture: Option<Capture>,
align_capture: Option<Capture>,
anchor_capture: Option<Capture>,
extend_capture: Option<Capture>,
extend_prevent_once_capture: Option<Capture>,
}
impl IndentQuery {
pub fn new(grammar: Grammar, source: &str) -> Result<Self, tree_sitter::query::ParseError> {
let mut properties = HashMap::new();
let mut predicates: HashMap<Pattern, IndentQueryPredicates> = HashMap::new();
let query = Query::new(grammar, source, |pattern, predicate| match predicate {
UserPredicate::SetProperty { key: "scope", val } => {
let scope = match val {
Some("all") => IndentScope::All,
Some("tail") => IndentScope::Tail,
Some(other) => {
return Err(format!("unknown scope (#set! scope \"{other}\")").into())
}
None => return Err("missing scope value (#set! scope ...)".into()),
};
properties.insert(pattern, scope);
Ok(())
}
UserPredicate::Other(predicate) => {
let name = predicate.name();
match name {
"not-kind-eq?" => {
predicate.check_arg_count(2)?;
let capture = predicate.capture_arg(0)?;
let not_expected_kind = predicate.str_arg(1)?;
predicates
.entry(pattern)
.or_default()
.not_kind_eq
.push((capture, not_expected_kind.into()));
Ok(())
}
"same-line?" | "not-same-line?" => {
predicate.check_arg_count(2)?;
let capture1 = predicate.capture_arg(0)?;
let capture2 = predicate.capture_arg(1)?;
let negated = name == "not-same-line?";
predicates.entry(pattern).or_default().same_line =
Some((capture1, capture2, negated));
Ok(())
}
"one-line?" | "not-one-line?" => {
predicate.check_arg_count(1)?;
let capture = predicate.capture_arg(0)?;
let negated = name == "not-one-line?";
predicates.entry(pattern).or_default().one_line = Some((capture, negated));
Ok(())
}
_ => Err(InvalidPredicateError::unknown(UserPredicate::Other(
predicate,
))),
}
}
_ => Err(InvalidPredicateError::unknown(predicate)),
})?;
Ok(Self {
properties,
predicates,
indent_capture: query.get_capture("indent"),
indent_always_capture: query.get_capture("indent.always"),
outdent_capture: query.get_capture("outdent"),
outdent_always_capture: query.get_capture("outdent.always"),
align_capture: query.get_capture("align"),
anchor_capture: query.get_capture("anchor"),
extend_capture: query.get_capture("extend"),
extend_prevent_once_capture: query.get_capture("extend.prevent-once"),
query,
})
}
}
/// The total indent for some line of code.
/// This is usually constructed in one of 2 ways:
/// - Successively add indent captures to get the (added) indent from a single line
@@ -453,193 +603,114 @@ struct IndentQueryResult<'a> {
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;
fn get_node_start_line(text: RopeSlice, node: &Node, new_line_byte_pos: Option<u32>) -> usize {
let mut node_line = text.byte_to_line(node.start_byte() as usize);
// Adjust for the new line that will be inserted
if new_line_byte_pos.map_or(false, |pos| node.start_byte() >= pos) {
if new_line_byte_pos.is_some_and(|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;
fn get_node_end_line(text: RopeSlice, node: &Node, new_line_byte_pos: Option<u32>) -> usize {
let mut node_line = text.byte_to_line(node.end_byte() as usize);
// 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) {
if new_line_byte_pos.is_some_and(|pos| node.end_byte() > pos) {
node_line += 1;
}
node_line
}
fn query_indents<'a>(
query: &Query,
query: &IndentQuery,
syntax: &Syntax,
cursor: &mut QueryCursor,
text: RopeSlice<'a>,
range: std::ops::Range<usize>,
new_line_byte_pos: Option<usize>,
range: std::ops::Range<u32>,
new_line_byte_pos: Option<u32>,
) -> IndentQueryResult<'a> {
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);
let mut cursor = InactiveQueryCursor::new(range, TREE_SITTER_MATCH_LIMIT).execute_query(
&query.query,
&syntax.tree().root_node(),
RopeInput::new(text),
);
// Iterate over all captures from the query
for m in cursor.matches(query, syntax.tree().root_node(), RopeProvider(text)) {
while let Some(m) = cursor.next_match() {
// Skip matches where not all custom predicates are fulfilled
if !query.general_predicates(m.pattern_index).iter().all(|pred| {
match pred.operator.as_ref() {
"not-kind-eq?" => match (pred.args.first(), pred.args.get(1)) {
(
Some(QueryPredicateArg::Capture(capture_idx)),
Some(QueryPredicateArg::String(kind)),
) => {
let node = m.nodes_for_capture_index(*capture_idx).next();
match node {
Some(node) => node.kind()!=kind.as_ref(),
_ => true,
}
}
_ => {
panic!("Invalid indent query: Arguments to \"not-kind-eq?\" must be a capture and a string");
}
},
"same-line?" | "not-same-line?" => {
match (pred.args.first(), pred.args.get(1)) {
(
Some(QueryPredicateArg::Capture(capt1)),
Some(QueryPredicateArg::Capture(capt2))
) => {
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 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,
}
}
_ => {
panic!("Invalid indent query: Arguments to \"{}\" must be 2 captures", pred.operator);
}
}
}
"one-line?" | "not-one-line?" => match pred.args.first() {
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 (\"{}\")",
pred.operator
);
}
}
}) {
if query
.predicates
.get(&m.pattern())
.is_some_and(|preds| !preds.are_satisfied(&m, text, new_line_byte_pos))
{
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];
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(RopeSlice::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())
.or_insert_with(|| Vec::with_capacity(1))
.push(ExtendCapture::Extend);
continue;
}
"extend.prevent-once" => {
extend_captures
.entry(capture.node.id())
.or_insert_with(|| Vec::with_capacity(1))
.push(ExtendCapture::PreventOnce);
continue;
}
_ => {
// Ignore any unknown captures (these may be needed for predicates such as #match?)
continue;
let mut anchor: Option<&Node> = None;
for matched_node in m.matched_nodes() {
let node_id = matched_node.node.id();
let capture = Some(matched_node.capture);
let capture_type = if capture == query.indent_capture {
IndentCaptureType::Indent
} else if capture == query.indent_always_capture {
IndentCaptureType::IndentAlways
} else if capture == query.outdent_capture {
IndentCaptureType::Outdent
} else if capture == query.outdent_always_capture {
IndentCaptureType::OutdentAlways
} else if capture == query.align_capture {
IndentCaptureType::Align(RopeSlice::from(""))
} else if capture == query.anchor_capture {
if anchor.is_some() {
log::error!("Invalid indent query: Encountered more than one @anchor in the same match.")
} else {
anchor = Some(&matched_node.node);
}
continue;
} else if capture == query.extend_capture {
extend_captures
.entry(node_id)
.or_insert_with(|| Vec::with_capacity(1))
.push(ExtendCapture::Extend);
continue;
} else if capture == query.extend_prevent_once_capture {
extend_captures
.entry(node_id)
.or_insert_with(|| Vec::with_capacity(1))
.push(ExtendCapture::PreventOnce);
continue;
} else {
// Ignore any unknown captures (these may be needed for predicates such as #match?)
continue;
};
let scope = capture_type.default_scope();
let mut indent_capture = IndentCapture {
// Apply additional settings for this capture
let scope = query
.properties
.get(&m.pattern())
.copied()
.unwrap_or_else(|| capture_type.default_scope());
let indent_capture = IndentCapture {
capture_type,
scope,
};
// Apply additional settings for this capture
for property in query.property_settings(m.pattern_index) {
match property.key.as_ref() {
"scope" => {
indent_capture.scope = match property.value.as_deref() {
Some("all") => IndentScope::All,
Some("tail") => IndentScope::Tail,
Some(s) => {
panic!("Invalid indent query: Unknown value for \"scope\" property (\"{}\")", s);
}
None => {
panic!(
"Invalid indent query: Missing value for \"scope\" property"
);
}
}
}
_ => {
panic!(
"Invalid indent query: Unknown property \"{}\"",
property.key
);
}
}
}
added_indent_captures.push((capture.node.id(), indent_capture))
added_indent_captures.push((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,
let Some(anchor) = anchor else {
log::error!("Invalid indent query: @align requires an accompanying @anchor.");
continue;
};
let line = text.byte_to_line(anchor.start_byte() as usize);
let line_start = text.line_to_byte(line);
capture.capture_type = IndentCaptureType::Align(
text.line(anchor.start_position().row)
.byte_slice(0..anchor.start_position().column),
text.byte_slice(line_start..anchor.start_byte() as usize),
);
}
indent_captures
@@ -691,13 +762,15 @@ fn extend_nodes<'a>(
// - the cursor is on the same line as the end of the node OR
// - the line that the cursor is on is more indented than the
// first line of the node
if deepest_preceding.end_position().row == line {
if text.byte_to_line(deepest_preceding.end_byte() as usize) == line {
extend_node = true;
} else {
let cursor_indent =
indent_level_for_line(text.line(line), tab_width, indent_width);
let node_indent = indent_level_for_line(
text.line(deepest_preceding.start_position().row),
text.line(
text.byte_to_line(deepest_preceding.start_byte() as usize),
),
tab_width,
indent_width,
);
@@ -714,7 +787,7 @@ fn extend_nodes<'a>(
if node_captured && stop_extend {
stop_extend = false;
} else if extend_node && !stop_extend {
*node = deepest_preceding;
*node = deepest_preceding.clone();
break;
}
// If the tree contains a syntax error, `deepest_preceding` may not
@@ -731,17 +804,17 @@ fn extend_nodes<'a>(
/// - The indent captures for all relevant nodes.
#[allow(clippy::too_many_arguments)]
fn init_indent_query<'a, 'b>(
query: &Query,
query: &IndentQuery,
syntax: &'a Syntax,
text: RopeSlice<'b>,
tab_width: usize,
indent_width: usize,
line: usize,
byte_pos: usize,
new_line_byte_pos: Option<usize>,
byte_pos: u32,
new_line_byte_pos: Option<u32>,
) -> Option<(Node<'a>, HashMap<usize, Vec<IndentCapture<'b>>>)> {
// The innermost tree-sitter node which is considered for the indent
// computation. It may change if some predeceding node is extended
// computation. It may change if some preceding node is extended
let mut node = syntax
.tree()
.root_node()
@@ -751,37 +824,25 @@ fn init_indent_query<'a, 'b>(
// The query range should intersect with all nodes directly preceding
// the position of the indent query in case one of them is extended.
let mut deepest_preceding = None; // The deepest node preceding the indent query position
let mut tree_cursor = node.walk();
for child in node.children(&mut tree_cursor) {
for child in node.children() {
if child.byte_range().end <= byte_pos {
deepest_preceding = Some(child);
deepest_preceding = Some(child.clone());
}
}
deepest_preceding = deepest_preceding.map(|mut prec| {
// Get the deepest directly preceding node
while prec.child_count() > 0 {
prec = prec.child(prec.child_count() - 1).unwrap();
prec = prec.child(prec.child_count() - 1).unwrap().clone();
}
prec
});
let query_range = deepest_preceding
.as_ref()
.map(|prec| prec.byte_range().end - 1..byte_pos + 1)
.unwrap_or(byte_pos..byte_pos + 1);
crate::syntax::PARSER.with(|ts_parser| {
let mut ts_parser = ts_parser.borrow_mut();
let mut cursor = ts_parser.cursors.pop().unwrap_or_default();
let query_result = query_indents(
query,
syntax,
&mut cursor,
text,
query_range,
new_line_byte_pos,
);
ts_parser.cursors.push(cursor);
(query_result, deepest_preceding)
})
let query_result = query_indents(query, syntax, text, query_range, new_line_byte_pos);
(query_result, deepest_preceding)
};
let extend_captures = query_result.extend_captures;
@@ -839,7 +900,7 @@ fn init_indent_query<'a, 'b>(
/// ```
#[allow(clippy::too_many_arguments)]
pub fn treesitter_indent_for_pos<'a>(
query: &Query,
query: &IndentQuery,
syntax: &Syntax,
tab_width: usize,
indent_width: usize,
@@ -848,7 +909,7 @@ pub fn treesitter_indent_for_pos<'a>(
pos: usize,
new_line: bool,
) -> Option<Indentation<'a>> {
let byte_pos = text.char_to_byte(pos);
let byte_pos = text.char_to_byte(pos) as u32;
let new_line_byte_pos = new_line.then_some(byte_pos);
let (mut node, mut indent_captures) = init_indent_query(
query,
@@ -868,7 +929,7 @@ pub fn treesitter_indent_for_pos<'a>(
let mut indent_for_line_below = Indentation::default();
loop {
let is_first = is_first_in_line(node, text, new_line_byte_pos);
let is_first = is_first_in_line(&node, text, new_line_byte_pos);
// Apply all indent definitions for this node.
// Since we only iterate over each node once, we can remove the
@@ -891,8 +952,8 @@ pub fn treesitter_indent_for_pos<'a>(
}
if let Some(parent) = node.parent() {
let node_line = get_node_start_line(node, new_line_byte_pos);
let parent_line = get_node_start_line(parent, new_line_byte_pos);
let node_line = get_node_start_line(text, &node, new_line_byte_pos);
let parent_line = get_node_start_line(text, &parent, new_line_byte_pos);
if node_line != parent_line {
// Don't add indent for the line below the line of the query
@@ -914,8 +975,9 @@ pub fn treesitter_indent_for_pos<'a>(
} else {
// Only add the indentation for the line below if that line
// is not after the line that the indentation is calculated for.
if (node.start_position().row < line)
|| (new_line && node.start_position().row == line && node.start_byte() < byte_pos)
let node_start_line = text.byte_to_line(node.start_byte() as usize);
if node_start_line < line
|| (new_line && node_start_line == line && node.start_byte() < byte_pos)
{
result.add_line(indent_for_line_below);
}
@@ -930,7 +992,7 @@ pub fn treesitter_indent_for_pos<'a>(
/// This is done either using treesitter, or if that's not available by copying the indentation from the current line
#[allow(clippy::too_many_arguments)]
pub fn indent_for_newline(
language_config: Option<&LanguageConfiguration>,
loader: &syntax::Loader,
syntax: Option<&Syntax>,
indent_heuristic: &IndentationHeuristic,
indent_style: &IndentStyle,
@@ -947,7 +1009,7 @@ pub fn indent_for_newline(
Some(syntax),
) = (
indent_heuristic,
language_config.and_then(|config| config.indent_query()),
syntax.and_then(|syntax| loader.indent_query(syntax.root_language())),
syntax,
) {
if let Some(indent) = treesitter_indent_for_pos(
@@ -1015,10 +1077,10 @@ pub fn indent_for_newline(
indent_style.as_str().repeat(indent_level)
}
pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> {
pub fn get_scopes<'a>(syntax: Option<&'a Syntax>, text: RopeSlice, pos: usize) -> Vec<&'a str> {
let mut scopes = Vec::new();
if let Some(syntax) = syntax {
let pos = text.char_to_byte(pos);
let pos = text.char_to_byte(pos) as u32;
let mut node = match syntax
.tree()
.root_node()

View File

@@ -3,12 +3,14 @@ pub use encoding_rs as encoding;
pub mod auto_pairs;
pub mod case_conversion;
pub mod chars;
pub mod command_line;
pub mod comment;
pub mod completion;
pub mod config;
pub mod diagnostic;
pub mod diff;
pub mod doc_formatter;
pub mod editor_config;
pub mod fuzzy;
pub mod graphemes;
pub mod history;
@@ -22,7 +24,6 @@ pub mod object;
mod position;
pub mod search;
pub mod selection;
pub mod shellwords;
pub mod snippets;
pub mod surround;
pub mod syntax;
@@ -52,9 +53,8 @@ pub use smartstring::SmartString;
pub type Tendril = SmartString<smartstring::LazyCompact>;
#[doc(inline)]
pub use {regex, tree_sitter};
pub use {regex, tree_house::tree_sitter};
pub use graphemes::RopeGraphemes;
pub use position::{
char_idx_at_visual_offset, coords_at_pos, pos_at_coords, softwrapped_dimensions,
visual_offset_from_anchor, visual_offset_from_block, Position, VisualOffsetError,
@@ -73,3 +73,5 @@ pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};
pub use uri::Uri;
pub use tree_house::Language;

View File

@@ -1,7 +1,7 @@
use std::iter;
use crate::tree_sitter::Node;
use ropey::RopeSlice;
use tree_sitter::Node;
use crate::movement::Direction::{self, Backward, Forward};
use crate::Syntax;
@@ -75,7 +75,7 @@ fn find_pair(
pos_: usize,
traverse_parents: bool,
) -> Option<usize> {
let pos = doc.char_to_byte(pos_);
let pos = doc.char_to_byte(pos_) as u32;
let root = syntax.tree_for_byte_range(pos, pos).root_node();
let mut node = root.descendant_for_byte_range(pos, pos)?;
@@ -128,7 +128,7 @@ fn find_pair(
if find_pair_end(doc, sibling.prev_sibling(), start_char, end_char, Backward)
.is_some()
{
return doc.try_byte_to_char(sibling.start_byte()).ok();
return doc.try_byte_to_char(sibling.start_byte() as usize).ok();
}
}
} else if node.is_named() {
@@ -144,9 +144,9 @@ fn find_pair(
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)
let node_start = doc.byte_to_char(node.start_byte() as usize);
let node_text = doc.byte_slice(node.start_byte() as usize..node.end_byte() as usize);
find_matching_bracket_plaintext(node_text, pos_ - node_start).map(|pos| pos + node_start)
}
/// Returns the position of the matching bracket under cursor.
@@ -304,7 +304,7 @@ fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> {
if node.byte_range().len() != 1 {
return None;
}
let pos = doc.try_byte_to_char(node.start_byte()).ok()?;
let pos = doc.try_byte_to_char(node.start_byte() as usize).ok()?;
Some((pos, doc.char(pos)))
}

View File

@@ -1,7 +1,6 @@
use std::{cmp::Reverse, iter};
use std::{borrow::Cow, cmp::Reverse, iter};
use ropey::iter::Chars;
use tree_sitter::{Node, QueryCursor};
use crate::{
char_idx_at_visual_offset,
@@ -13,9 +12,10 @@ use crate::{
},
line_ending::rope_is_line_ending,
position::char_idx_at_visual_block_offset,
syntax::LanguageConfiguration,
syntax,
text_annotations::TextAnnotations,
textobject::TextObject,
tree_sitter::Node,
visual_offset_from_block, Range, RopeSlice, Selection, Syntax,
};
@@ -560,21 +560,23 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo
/// Finds the range of the next or previous textobject in the syntax sub-tree of `node`.
/// Returns the range in the forwards direction.
#[allow(clippy::too_many_arguments)]
pub fn goto_treesitter_object(
slice: RopeSlice,
range: Range,
object_name: &str,
dir: Direction,
slice_tree: Node,
lang_config: &LanguageConfiguration,
slice_tree: &Node,
syntax: &Syntax,
loader: &syntax::Loader,
count: usize,
) -> Range {
let textobject_query = loader.textobject_query(syntax.root_language());
let get_range = move |range: Range| -> Option<Range> {
let byte_pos = slice.char_to_byte(range.cursor(slice));
let cap_name = |t: TextObject| format!("{}.{}", object_name, t);
let mut cursor = QueryCursor::new();
let nodes = lang_config.textobject_query()?.capture_nodes_any(
let nodes = textobject_query?.capture_nodes_any(
&[
&cap_name(TextObject::Movement),
&cap_name(TextObject::Around),
@@ -582,7 +584,6 @@ pub fn goto_treesitter_object(
],
slice_tree,
slice,
&mut cursor,
)?;
let node = match dir {
@@ -617,14 +618,15 @@ pub fn goto_treesitter_object(
last_range
}
fn find_parent_start(mut node: Node) -> Option<Node> {
fn find_parent_start<'tree>(node: &Node<'tree>) -> Option<Node<'tree>> {
let start = node.start_byte();
let mut node = Cow::Borrowed(node);
while node.start_byte() >= start || !node.is_named() {
node = node.parent()?;
node = Cow::Owned(node.parent()?);
}
Some(node)
Some(node.into_owned())
}
pub fn move_parent_node_end(
@@ -635,8 +637,8 @@ pub fn move_parent_node_end(
movement: Movement,
) -> Selection {
selection.transform(|range| {
let start_from = text.char_to_byte(range.from());
let start_to = text.char_to_byte(range.to());
let start_from = text.char_to_byte(range.from()) as u32;
let start_to = text.char_to_byte(range.to()) as u32;
let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) {
Some(node) => node,
@@ -654,18 +656,18 @@ pub fn move_parent_node_end(
// 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()),
Direction::Forward => text.byte_to_char(node.end_byte() as usize),
// 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());
let end_head = text.byte_to_char(node.start_byte() as usize);
// 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())
node = find_parent_start(&node).unwrap_or(node);
text.byte_to_char(node.start_byte() as usize)
} else {
end_head
}

View File

@@ -4,8 +4,8 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection)
let cursor = &mut syntax.walk();
selection.transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());
let from = text.char_to_byte(range.from()) as u32;
let to = text.char_to_byte(range.to()) as u32;
let byte_range = from..to;
cursor.reset_to_byte_range(from, to);
@@ -17,8 +17,8 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection)
}
let node = cursor.node();
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
let from = text.byte_to_char(node.start_byte() as usize);
let to = text.byte_to_char(node.end_byte() as usize);
Range::new(to, from).with_direction(range.direction())
})
@@ -53,10 +53,10 @@ pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selectio
}
pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
selection.transform_iter(|range| {
let mut cursor = syntax.walk();
let mut cursor = syntax.walk();
selection.transform_iter(move |range| {
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from, to);
cursor.reset_to_byte_range(from as u32, to as u32);
if !cursor.goto_parent_with(|parent| parent.child_count() > 1) {
return vec![range].into_iter();
@@ -67,21 +67,18 @@ pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selectio
}
pub fn select_all_children(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
selection.transform_iter(|range| {
let mut cursor = syntax.walk();
let mut cursor = syntax.walk();
selection.transform_iter(move |range| {
let (from, to) = range.into_byte_range(text);
cursor.reset_to_byte_range(from, to);
cursor.reset_to_byte_range(from as u32, to as u32);
select_children(&mut cursor, text, range).into_iter()
})
}
fn select_children<'n>(
cursor: &'n mut TreeCursor<'n>,
text: RopeSlice,
range: Range,
) -> Vec<Range> {
fn select_children(cursor: &mut TreeCursor, text: RopeSlice, range: Range) -> Vec<Range> {
let children = cursor
.named_children()
.children()
.filter(|child| child.is_named())
.map(|child| Range::from_node(child, text, range.direction()))
.collect::<Vec<_>>();
@@ -98,7 +95,7 @@ pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selectio
text,
selection,
|cursor| {
while !cursor.goto_prev_sibling() {
while !cursor.goto_previous_sibling() {
if !cursor.goto_parent() {
break;
}
@@ -121,16 +118,16 @@ where
let cursor = &mut syntax.walk();
selection.transform(|range| {
let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to());
let from = text.char_to_byte(range.from()) as u32;
let to = text.char_to_byte(range.to()) as u32;
cursor.reset_to_byte_range(from, to);
motion(cursor);
let node = cursor.node();
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
let from = text.byte_to_char(node.start_byte() as usize);
let to = text.byte_to_char(node.end_byte() as usize);
Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
})

View File

@@ -4,10 +4,12 @@ use std::{
ops::{Add, AddAssign, Sub, SubAssign},
};
use helix_stdx::rope::RopeSliceExt;
use crate::{
chars::char_is_line_ending,
doc_formatter::{DocumentFormatter, TextFormat},
graphemes::{ensure_grapheme_boundary_prev, grapheme_width, RopeGraphemes},
graphemes::{ensure_grapheme_boundary_prev, grapheme_width},
line_ending::line_end_char_index,
text_annotations::TextAnnotations,
RopeSlice,
@@ -87,11 +89,6 @@ impl From<(usize, usize)> for Position {
}
}
impl From<Position> for tree_sitter::Point {
fn from(pos: Position) -> Self {
Self::new(pos.row, pos.col)
}
}
/// Convert a character index to (line, column) coordinates.
///
/// column in `char` count which can be used for row:column display in
@@ -101,7 +98,7 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
let line_start = text.line_to_char(line);
let pos = ensure_grapheme_boundary_prev(text, pos);
let col = RopeGraphemes::new(text.slice(line_start..pos)).count();
let col = text.slice(line_start..pos).graphemes().count();
Position::new(line, col)
}
@@ -126,7 +123,7 @@ pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Po
let mut col = 0;
for grapheme in RopeGraphemes::new(text.slice(line_start..pos)) {
for grapheme in text.slice(line_start..pos).graphemes() {
if grapheme == "\t" {
col += tab_width - (col % tab_width);
} else {
@@ -275,7 +272,7 @@ pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending
};
let mut col_char_offset = 0;
for (i, g) in RopeGraphemes::new(text.slice(line_start..line_end)).enumerate() {
for (i, g) in text.slice(line_start..line_end).graphemes().enumerate() {
if i == col {
break;
}
@@ -306,7 +303,7 @@ pub fn pos_at_visual_coords(text: RopeSlice, coords: Position, tab_width: usize)
let mut col_char_offset = 0;
let mut cols_remaining = col;
for grapheme in RopeGraphemes::new(text.slice(line_start..line_end)) {
for grapheme in text.slice(line_start..line_end).graphemes() {
let grapheme_width = if grapheme == "\t" {
tab_width - ((col - cols_remaining) % tab_width)
} else {

View File

@@ -9,13 +9,13 @@ use crate::{
},
line_ending::get_line_ending,
movement::Direction,
Assoc, ChangeSet, RopeGraphemes, RopeSlice,
tree_sitter::Node,
Assoc, ChangeSet, RopeSlice,
};
use helix_stdx::range::is_subset;
use helix_stdx::rope::{self, RopeSliceExt};
use smallvec::{smallvec, SmallVec};
use std::{borrow::Cow, iter, slice};
use tree_sitter::Node;
/// A single selection range.
///
@@ -76,8 +76,8 @@ impl Range {
}
pub fn from_node(node: Node, text: RopeSlice, direction: Direction) -> Self {
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
let from = text.byte_to_char(node.start_byte() as usize);
let to = text.byte_to_char(node.end_byte() as usize);
Range::new(from, to).with_direction(direction)
}
@@ -379,7 +379,7 @@ impl Range {
/// Returns true if this Range covers a single grapheme in the given text
pub fn is_single_grapheme(&self, doc: RopeSlice) -> bool {
let mut graphemes = RopeGraphemes::new(doc.slice(self.from()..self.to()));
let mut graphemes = doc.slice(self.from()..self.to()).graphemes();
let first = graphemes.next();
let second = graphemes.next();
first.is_some() && second.is_none()
@@ -619,7 +619,6 @@ impl Selection {
self
}
// TODO: consume an iterator or a vec to reduce allocations?
#[must_use]
pub fn new(ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Self {
assert!(!ranges.is_empty());
@@ -721,6 +720,12 @@ impl IntoIterator for Selection {
}
}
impl FromIterator<Range> for Selection {
fn from_iter<T: IntoIterator<Item = Range>>(ranges: T) -> Self {
Self::new(ranges.into_iter().collect(), 0)
}
}
impl From<Range> for Selection {
fn from(range: Range) -> Self {
Self {

View File

@@ -1,350 +0,0 @@
use std::borrow::Cow;
/// Auto escape for shellwords usage.
pub fn escape(input: Cow<str>) -> Cow<str> {
if !input.chars().any(|x| x.is_ascii_whitespace()) {
input
} else if cfg!(unix) {
Cow::Owned(input.chars().fold(String::new(), |mut buf, c| {
if c.is_ascii_whitespace() {
buf.push('\\');
}
buf.push(c);
buf
}))
} else {
Cow::Owned(format!("\"{}\"", input))
}
}
enum State {
OnWhitespace,
Unquoted,
UnquotedEscaped,
Quoted,
QuoteEscaped,
Dquoted,
DquoteEscaped,
}
pub struct Shellwords<'a> {
state: State,
/// Shellwords where whitespace and escapes has been resolved.
words: Vec<Cow<'a, str>>,
/// The parts of the input that are divided into shellwords. This can be
/// used to retrieve the original text for a given word by looking up the
/// same index in the Vec as the word in `words`.
parts: Vec<&'a str>,
}
impl<'a> From<&'a str> for Shellwords<'a> {
fn from(input: &'a str) -> Self {
use State::*;
let mut state = Unquoted;
let mut words = Vec::new();
let mut parts = Vec::new();
let mut escaped = String::with_capacity(input.len());
let mut part_start = 0;
let mut unescaped_start = 0;
let mut end = 0;
for (i, c) in input.char_indices() {
state = match state {
OnWhitespace => match c {
'"' => {
end = i;
Dquoted
}
'\'' => {
end = i;
Quoted
}
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
OnWhitespace
}
}
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
_ => Unquoted,
},
Unquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
UnquotedEscaped
} else {
Unquoted
}
}
c if c.is_ascii_whitespace() => {
end = i;
OnWhitespace
}
_ => Unquoted,
},
UnquotedEscaped => Unquoted,
Quoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
QuoteEscaped
} else {
Quoted
}
}
'\'' => {
end = i;
OnWhitespace
}
_ => Quoted,
},
QuoteEscaped => Quoted,
Dquoted => match c {
'\\' => {
if cfg!(unix) {
escaped.push_str(&input[unescaped_start..i]);
unescaped_start = i + 1;
DquoteEscaped
} else {
Dquoted
}
}
'"' => {
end = i;
OnWhitespace
}
_ => Dquoted,
},
DquoteEscaped => Dquoted,
};
let c_len = c.len_utf8();
if i == input.len() - c_len && end == 0 {
end = i + c_len;
}
if end > 0 {
let esc_trim = escaped.trim();
let inp = &input[unescaped_start..end];
if !(esc_trim.is_empty() && inp.trim().is_empty()) {
if esc_trim.is_empty() {
words.push(inp.into());
parts.push(inp);
} else {
words.push([escaped, inp.into()].concat().into());
parts.push(&input[part_start..end]);
escaped = "".to_string();
}
}
unescaped_start = i + 1;
part_start = i + 1;
end = 0;
}
}
debug_assert!(words.len() == parts.len());
Self {
state,
words,
parts,
}
}
}
impl<'a> Shellwords<'a> {
/// Checks that the input ends with a whitespace character which is not escaped.
///
/// # Examples
///
/// ```rust
/// use helix_core::shellwords::Shellwords;
/// assert_eq!(Shellwords::from(" ").ends_with_whitespace(), true);
/// assert_eq!(Shellwords::from(":open ").ends_with_whitespace(), true);
/// assert_eq!(Shellwords::from(":open foo.txt ").ends_with_whitespace(), true);
/// assert_eq!(Shellwords::from(":open").ends_with_whitespace(), false);
/// #[cfg(unix)]
/// assert_eq!(Shellwords::from(":open a\\ ").ends_with_whitespace(), false);
/// #[cfg(unix)]
/// assert_eq!(Shellwords::from(":open a\\ b.txt").ends_with_whitespace(), false);
/// ```
pub fn ends_with_whitespace(&self) -> bool {
matches!(self.state, State::OnWhitespace)
}
/// Returns the list of shellwords calculated from the input string.
pub fn words(&self) -> &[Cow<'a, str>] {
&self.words
}
/// Returns a list of strings which correspond to [`Self::words`] but represent the original
/// text in the input string - including escape characters - without separating whitespace.
pub fn parts(&self) -> &[&'a str] {
&self.parts
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
#[cfg(windows)]
fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó"),
Cow::from("wörds"),
Cow::from("\\three\\"),
Cow::from("\\"),
Cow::from("with\\ escaping\\\\"),
];
// TODO test is_owned and is_borrowed, once they get stabilized.
assert_eq!(expected, result);
}
#[test]
#[cfg(unix)]
fn test_normal() {
let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#;
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó"),
Cow::from("wörds"),
Cow::from(r#"three "with escaping\"#),
];
// TODO test is_owned and is_borrowed, once they get stabilized.
assert_eq!(expected, result);
}
#[test]
#[cfg(unix)]
fn test_quoted() {
let quoted =
r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#;
let shellwords = Shellwords::from(quoted);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó wörds"),
Cow::from(r#"three' "with escaping\"#),
Cow::from("quote incomplete"),
];
assert_eq!(expected, result);
}
#[test]
#[cfg(unix)]
fn test_dquoted() {
let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#;
let shellwords = Shellwords::from(dquoted);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó wörds"),
Cow::from(r#"three' "with escaping\"#),
Cow::from("dquote incomplete"),
];
assert_eq!(expected, result);
}
#[test]
#[cfg(unix)]
fn test_mixed() {
let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#;
let shellwords = Shellwords::from(dquoted);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":o"),
Cow::from("single_word"),
Cow::from("twó wörds"),
Cow::from("three' \"with escaping\\"),
Cow::from("no space before"),
Cow::from("and after"),
Cow::from("$#%^@"),
Cow::from("%^&(%^"),
Cow::from(")(*&^%"),
Cow::from(r#"a\\b"#),
//last ' just changes to quoted but since we dont have anything after it, it should be ignored
];
assert_eq!(expected, result);
}
#[test]
fn test_lists() {
let input =
r#":set statusline.center ["file-type","file-encoding"] '["list", "in", "quotes"]'"#;
let shellwords = Shellwords::from(input);
let result = shellwords.words().to_vec();
let expected = vec![
Cow::from(":set"),
Cow::from("statusline.center"),
Cow::from(r#"["file-type","file-encoding"]"#),
Cow::from(r#"["list", "in", "quotes"]"#),
];
assert_eq!(expected, result);
}
#[test]
#[cfg(unix)]
fn test_escaping_unix() {
assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar".into()), Cow::Borrowed("foo\\ bar"));
assert_eq!(escape("foo\tbar".into()), Cow::Borrowed("foo\\\tbar"));
}
#[test]
#[cfg(windows)]
fn test_escaping_windows() {
assert_eq!(escape("foobar".into()), Cow::Borrowed("foobar"));
assert_eq!(escape("foo bar".into()), Cow::Borrowed("\"foo bar\""));
}
#[test]
#[cfg(unix)]
fn test_parts() {
assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]);
assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\ "]);
}
#[test]
#[cfg(windows)]
fn test_parts() {
assert_eq!(Shellwords::from(":o a").parts(), &[":o", "a"]);
assert_eq!(Shellwords::from(":o a\\ ").parts(), &[":o", "a\\"]);
}
#[test]
fn test_multibyte_at_end() {
assert_eq!(Shellwords::from("𒀀").parts(), &["𒀀"]);
assert_eq!(
Shellwords::from(":sh echo 𒀀").parts(),
&[":sh", "echo", "𒀀"]
);
assert_eq!(
Shellwords::from(":sh echo 𒀀 hello world𒀀").parts(),
&[":sh", "echo", "𒀀", "hello", "world𒀀"]
);
}
}

View File

@@ -1,6 +1,6 @@
use std::ops::{Index, IndexMut};
use hashbrown::HashSet;
use foldhash::HashSet;
use helix_stdx::range::{is_exact_subset, is_subset};
use helix_stdx::Range;
use ropey::Rope;
@@ -35,7 +35,7 @@ impl ActiveSnippet {
let snippet = Self {
ranges: snippet.ranges,
tabstops: snippet.tabstops,
active_tabstops: HashSet::new(),
active_tabstops: HashSet::default(),
current_tabstop: TabstopIdx(0),
};
(snippet.tabstops.len() != 1).then_some(snippet)
@@ -252,4 +252,21 @@ mod tests {
snippet.map(edit.changes());
assert!(!snippet.is_valid(&Selection::point(4)))
}
#[test]
fn tabstop_zero_with_placeholder() {
// The `$0` tabstop should not have placeholder text. When we receive a snippet like this
// (from older versions of clangd for example) we should discard the placeholder text.
let snippet = Snippet::parse("sizeof(${0:expression-or-type})").unwrap();
let mut doc = Rope::from("\n");
let (transaction, _, snippet) = snippet.render(
&doc,
&Selection::point(0),
|_| (0, 0),
&mut SnippetRenderCtx::test_ctx(),
);
assert!(transaction.apply(&mut doc));
assert_eq!(doc, "sizeof()\n");
assert!(ActiveSnippet::new(snippet).is_none());
}
}

View File

@@ -178,9 +178,16 @@ impl Snippet {
&mut self,
idx: usize,
parent: Option<TabstopIdx>,
default: Vec<parser::SnippetElement>,
mut default: Vec<parser::SnippetElement>,
) -> TabstopIdx {
let idx = TabstopIdx::elaborate(idx);
if idx == LAST_TABSTOP_IDX && !default.is_empty() {
// Older versions of clangd for example may send a snippet like `${0:placeholder}`
// which is considered by VSCode to be a misuse of the `$0` tabstop.
log::warn!("Discarding placeholder text for the `$0` tabstop ({default:?}). \
The `$0` tabstop signifies the final cursor position and should not include placeholder text.");
default.clear();
}
let default = self.elaborate(default, Some(idx));
self.tabstops.push(Tabstop {
idx,

View File

@@ -361,7 +361,20 @@ mod test {
Text(")".into()),
]),
parse("match(${1:Arg1})")
)
);
// The `$0` tabstop should not have placeholder text. The parser should handle this case
// normally and then the placeholder text should be discarded during elaboration.
assert_eq!(
Ok(vec![
Text("sizeof(".into()),
Placeholder {
tabstop: 0,
value: vec![Text("expression-or-type".into())],
},
Text(")".into()),
]),
parse("sizeof(${0:expression-or-type})")
);
}
#[test]

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,617 @@
use crate::{auto_pairs::AutoPairs, diagnostic::Severity, Language};
use globset::GlobSet;
use helix_stdx::rope;
use serde::{ser::SerializeSeq as _, Deserialize, Serialize};
use std::{
collections::{HashMap, HashSet},
fmt::{self, Display},
path::PathBuf,
str::FromStr,
};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
#[serde(default)]
pub language_server: HashMap<String, LanguageServerConfiguration>,
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(skip)]
pub(super) language: Option<Language>,
#[serde(rename = "name")]
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)]
pub shebangs: Vec<String>, // interpreter(s) associated with language
#[serde(default)]
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
#[serde(
default,
skip_serializing,
deserialize_with = "from_comment_tokens",
alias = "comment-token"
)]
pub comment_tokens: Option<Vec<String>>,
#[serde(
default,
skip_serializing,
deserialize_with = "from_block_comment_tokens"
)]
pub block_comment_tokens: Option<Vec<BlockCommentToken>>,
pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,
#[serde(default)]
pub auto_format: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub formatter: Option<FormatterConfiguration>,
/// If set, overrides `editor.path-completion`.
pub path_completion: Option<bool>,
#[serde(default)]
pub diagnostic_severity: Severity,
pub grammar: Option<String>, // tree-sitter grammar name, defaults to language_id
// content_regex
#[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
pub injection_regex: Option<rope::Regex>,
// first_line_regex
//
#[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>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debugger: Option<DebugAdapterConfig>,
/// Automatic insertion of pairs to parentheses, brackets,
/// etc. Defaults to true. Optionally, this can be a list of 2-tuples
/// to specify a list of characters to pair. This overrides the
/// global setting.
#[serde(default, skip_serializing, deserialize_with = "deserialize_auto_pairs")]
pub auto_pairs: Option<AutoPairs>,
pub rulers: Option<Vec<u16>>, // if set, override editor's rulers
/// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
/// Falling back to the current working directory if none are configured.
pub workspace_lsp_roots: Option<Vec<PathBuf>>,
#[serde(default)]
pub persistent_diagnostic_sources: Vec<String>,
}
impl LanguageConfiguration {
pub fn language(&self) -> Language {
// This value must be set by `super::Loader::new`.
self.language.unwrap()
}
}
#[derive(Debug, PartialEq, Eq, Hash)]
pub enum FileType {
/// The extension of the file, either the `Path::extension` or the full
/// filename if the file does not have an extension.
Extension(String),
/// A Unix-style path glob. This is compared to the file's absolute path, so
/// it can be used to detect files based on their directories. If the glob
/// is not an absolute path and does not already start with a glob pattern,
/// a glob pattern will be prepended to it.
Glob(globset::Glob),
}
impl Serialize for FileType {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
use serde::ser::SerializeMap;
match self {
FileType::Extension(extension) => serializer.serialize_str(extension),
FileType::Glob(glob) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("glob", glob.glob())?;
map.end()
}
}
}
}
impl<'de> Deserialize<'de> for FileType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct FileTypeVisitor;
impl<'de> serde::de::Visitor<'de> for FileTypeVisitor {
type Value = FileType;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("string or table")
}
fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(FileType::Extension(value.to_string()))
}
fn visit_map<M>(self, mut map: M) -> Result<Self::Value, M::Error>
where
M: serde::de::MapAccess<'de>,
{
match map.next_entry::<String, String>()? {
Some((key, mut glob)) if key == "glob" => {
// If the glob isn't an absolute path or already starts
// with a glob pattern, add a leading glob so we
// properly match relative paths.
if !glob.starts_with('/') && !glob.starts_with("*/") {
glob.insert_str(0, "*/");
}
globset::Glob::new(glob.as_str())
.map(FileType::Glob)
.map_err(|err| {
serde::de::Error::custom(format!("invalid `glob` pattern: {}", err))
})
}
Some((key, _value)) => Err(serde::de::Error::custom(format!(
"unknown key in `file-types` list: {}",
key
))),
None => Err(serde::de::Error::custom(
"expected a `suffix` key in the `file-types` entry",
)),
}
}
}
deserializer.deserialize_any(FileTypeVisitor)
}
}
fn from_comment_tokens<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum CommentTokens {
Multiple(Vec<String>),
Single(String),
}
Ok(
Option::<CommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
CommentTokens::Single(val) => vec![val],
CommentTokens::Multiple(vals) => vals,
}),
)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BlockCommentToken {
pub start: String,
pub end: String,
}
impl Default for BlockCommentToken {
fn default() -> Self {
BlockCommentToken {
start: "/*".to_string(),
end: "*/".to_string(),
}
}
}
fn from_block_comment_tokens<'de, D>(
deserializer: D,
) -> Result<Option<Vec<BlockCommentToken>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum BlockCommentTokens {
Multiple(Vec<BlockCommentToken>),
Single(BlockCommentToken),
}
Ok(
Option::<BlockCommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
BlockCommentTokens::Single(val) => vec![val],
BlockCommentTokens::Multiple(vals) => vals,
}),
)
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[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,
DocumentColors,
}
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-reference",
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",
DocumentColors => "document-colors",
};
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()
}
fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
where
D: serde::Deserializer<'de>,
{
let patterns = Vec::<String>::deserialize(deserializer)?;
if patterns.is_empty() {
return Ok(None);
}
let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns {
let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
builder.add(glob);
}
builder.build().map(Some).map_err(serde::de::Error::custom)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
pub command: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
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,
#[serde(
default,
skip_serializing,
deserialize_with = "deserialize_required_root_patterns"
)]
pub required_root_patterns: Option<GlobSet>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct FormatterConfiguration {
pub command: String,
#[serde(default)]
#[serde(skip_serializing_if = "Vec::is_empty")]
pub args: Vec<String>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct AdvancedCompletion {
pub name: Option<String>,
pub completion: Option<String>,
pub default: Option<String>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case", untagged)]
pub enum DebugConfigCompletion {
Named(String),
Advanced(AdvancedCompletion),
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum DebugArgumentValue {
String(String),
Array(Vec<String>),
Boolean(bool),
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DebugTemplate {
pub name: String,
pub request: String,
#[serde(default)]
pub completion: Vec<DebugConfigCompletion>,
pub args: HashMap<String, DebugArgumentValue>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "kebab-case")]
pub struct DebugAdapterConfig {
pub name: String,
pub transport: String,
#[serde(default)]
pub command: String,
#[serde(default)]
pub args: Vec<String>,
pub port_arg: Option<String>,
pub templates: Vec<DebugTemplate>,
#[serde(default)]
pub quirks: DebuggerQuirks,
}
// Different workarounds for adapters' differences
#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)]
pub struct DebuggerQuirks {
#[serde(default)]
pub absolute_paths: bool,
}
#[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,
}
/// How the indentation for a newly inserted line should be determined.
/// If the selected heuristic is not available (e.g. because the current
/// language has no tree-sitter indent queries), a simpler one will be used.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IndentationHeuristic {
/// Just copy the indentation of the line that the cursor is currently on.
Simple,
/// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line.
TreeSitter,
/// Use tree-sitter indent queries to compute the expected difference in indentation between the new line
/// and the line before. Add this to the actual indentation level of the line before.
#[default]
Hybrid,
}
/// Configuration for auto pairs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
pub enum AutoPairConfig {
/// Enables or disables auto pairing. False means disabled. True means to use the default pairs.
Enable(bool),
/// The mappings of pairs.
Pairs(HashMap<char, char>),
}
impl Default for AutoPairConfig {
fn default() -> Self {
AutoPairConfig::Enable(true)
}
}
impl From<&AutoPairConfig> for Option<AutoPairs> {
fn from(auto_pair_config: &AutoPairConfig) -> Self {
match auto_pair_config {
AutoPairConfig::Enable(false) => None,
AutoPairConfig::Enable(true) => Some(AutoPairs::default()),
AutoPairConfig::Pairs(pairs) => Some(AutoPairs::new(pairs.iter())),
}
}
}
impl From<AutoPairConfig> for Option<AutoPairs> {
fn from(auto_pairs_config: AutoPairConfig) -> Self {
(&auto_pairs_config).into()
}
}
impl FromStr for AutoPairConfig {
type Err = std::str::ParseBoolError;
// only do bool parsing for runtime setting
fn from_str(s: &str) -> Result<Self, Self::Err> {
let enable: bool = s.parse()?;
Ok(AutoPairConfig::Enable(enable))
}
}
#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct SoftWrap {
/// Soft wrap lines that exceed viewport width. Default to off
// NOTE: Option on purpose because the struct is shared between language config and global config.
// By default the option is None so that the language config falls back to the global config unless explicitly set.
pub enable: Option<bool>,
/// Maximum space left free at the end of the line.
/// This space is used to wrap text at word boundaries. If that is not possible within this limit
/// the word is simply split at the end of the line.
///
/// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
///
/// Default to 20
pub max_wrap: Option<u16>,
/// Maximum number of indentation that can be carried over from the previous line when softwrapping.
/// If a line is indented further then this limit it is rendered at the start of the viewport instead.
///
/// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views.
///
/// Default to 40
pub max_indent_retain: Option<u16>,
/// Indicator placed at the beginning of softwrapped lines
///
/// Defaults to ↪
pub wrap_indicator: Option<String>,
/// Softwrap at `text_width` instead of viewport width if it is shorter
pub wrap_at_text_width: Option<bool>,
}
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<rope::Regex>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?
.map(|buf| rope::Regex::new(&buf).map_err(serde::de::Error::custom))
.transpose()
}
fn deserialize_lsp_config<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<toml::Value>::deserialize(deserializer)?
.map(|toml| toml.try_into().map_err(serde::de::Error::custom))
.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>,
{
Ok(Option::<AutoPairConfig>::deserialize(deserializer)?.and_then(AutoPairConfig::into))
}
fn default_timeout() -> u64 {
20
}

View File

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

View File

@@ -65,7 +65,7 @@ pub fn print(s: &str) -> (String, Selection) {
let head_at_beg = iter.next_if_eq(&"|").is_some();
let last_grapheme = |s: &str| {
UnicodeSegmentation::graphemes(s, true)
.last()
.next_back()
.map(String::from)
};

View File

@@ -5,7 +5,7 @@ use std::ops::Range;
use std::ptr::NonNull;
use crate::doc_formatter::FormattedGrapheme;
use crate::syntax::Highlight;
use crate::syntax::{Highlight, OverlayHighlights};
use crate::{Position, Tendril};
/// An inline annotation is continuous text shown
@@ -300,10 +300,7 @@ impl<'a> TextAnnotations<'a> {
}
}
pub fn collect_overlay_highlights(
&self,
char_range: Range<usize>,
) -> Vec<(usize, Range<usize>)> {
pub fn collect_overlay_highlights(&self, char_range: Range<usize>) -> OverlayHighlights {
let mut highlights = Vec::new();
self.reset_pos(char_range.start);
for char_idx in char_range {
@@ -311,11 +308,11 @@ impl<'a> TextAnnotations<'a> {
// we don't know the number of chars the original grapheme takes
// however it doesn't matter as highlight boundaries are automatically
// aligned to grapheme boundaries in the rendering code
highlights.push((highlight.0, char_idx..char_idx + 1))
highlights.push((highlight, char_idx..char_idx + 1));
}
}
highlights
OverlayHighlights::Heterogenous { highlights }
}
/// Add new inline annotations.

View File

@@ -1,13 +1,12 @@
use std::fmt::Display;
use ropey::RopeSlice;
use tree_sitter::{Node, QueryCursor};
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
use crate::line_ending::rope_is_line_ending;
use crate::movement::Direction;
use crate::syntax::LanguageConfiguration;
use crate::syntax;
use crate::Range;
use crate::{surround, Syntax};
@@ -260,18 +259,18 @@ pub fn textobject_treesitter(
range: Range,
textobject: TextObject,
object_name: &str,
slice_tree: Node,
lang_config: &LanguageConfiguration,
syntax: &Syntax,
loader: &syntax::Loader,
_count: usize,
) -> Range {
let root = syntax.tree().root_node();
let textobject_query = loader.textobject_query(syntax.root_language());
let get_range = move || -> Option<Range> {
let byte_pos = slice.char_to_byte(range.cursor(slice));
let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
let mut cursor = QueryCursor::new();
let node = lang_config
.textobject_query()?
.capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
let node = textobject_query?
.capture_nodes(&capture_name, &root, slice)?
.filter(|node| node.byte_range().contains(&byte_pos))
.min_by_key(|node| node.byte_range().len())?;

View File

@@ -1,12 +1,11 @@
use arc_swap::ArcSwap;
use helix_core::{
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
syntax::{Configuration, Loader},
syntax::{config::Configuration, Loader},
Syntax,
};
use helix_stdx::rope::RopeSliceExt;
use ropey::Rope;
use std::{ops::Range, path::PathBuf, process::Command, sync::Arc};
use std::{ops::Range, path::PathBuf, process::Command};
#[test]
fn test_treesitter_indent_rust() {
@@ -196,17 +195,12 @@ fn test_treesitter_indent(
runtime.push("../runtime");
std::env::set_var("HELIX_RUNTIME", runtime.to_str().unwrap());
let language_config = loader.language_config_for_scope(lang_scope).unwrap();
let language = loader.language_for_scope(lang_scope).unwrap();
let language_config = loader.language(language).config();
let indent_style = IndentStyle::from_str(&language_config.indent.as_ref().unwrap().unit);
let highlight_config = language_config.highlight_config(&[]).unwrap();
let text = doc.slice(..);
let syntax = Syntax::new(
text,
highlight_config,
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let indent_query = language_config.indent_query().unwrap();
let syntax = Syntax::new(text, language, &loader).unwrap();
let indent_query = loader.indent_query(language).unwrap();
for i in 0..doc.len_lines() {
let line = text.line(i);

View File

@@ -22,6 +22,11 @@ serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
thiserror.workspace = true
slotmap.workspace = true
futures-executor.workspace = true
futures-util.workspace = true
tokio-stream.workspace = true
[dev-dependencies]
fern = "0.7"

View File

@@ -1,10 +1,11 @@
use crate::{
requests::DisconnectArguments,
registry::DebugAdapterId,
requests::{DisconnectArguments, TerminateArguments},
transport::{Payload, Request, Response, Transport},
types::*,
Error, Result,
};
use helix_core::syntax::DebuggerQuirks;
use helix_core::syntax::config::{DebugAdapterConfig, DebuggerQuirks};
use serde_json::Value;
@@ -27,12 +28,14 @@ use tokio::{
#[derive(Debug)]
pub struct Client {
id: usize,
id: DebugAdapterId,
_process: Option<Child>,
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
connection_type: Option<ConnectionType>,
starting_request_args: Option<Value>,
/// The socket address of the debugger, if using TCP transport.
pub socket: Option<SocketAddr>,
pub caps: Option<DebuggerCapabilities>,
// thread_id -> frames
pub stack_frames: HashMap<ThreadId, Vec<StackFrame>>,
@@ -41,23 +44,20 @@ pub struct Client {
/// Currently active frame for the current thread.
pub active_frame: Option<usize>,
pub quirks: DebuggerQuirks,
}
#[derive(Clone, Copy, Debug)]
pub enum ConnectionType {
Launch,
Attach,
/// The config which was used to start this debugger.
pub config: Option<DebugAdapterConfig>,
}
impl Client {
// Spawn a process and communicate with it by either TCP or stdio
// The returned stream includes the Client ID so consumers can differentiate between multiple clients
pub async fn process(
transport: &str,
command: &str,
args: Vec<&str>,
port_arg: Option<&str>,
id: usize,
) -> Result<(Self, UnboundedReceiver<Payload>)> {
id: DebugAdapterId,
) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
if command.is_empty() {
return Result::Err(Error::Other(anyhow!("Command not provided")));
}
@@ -72,9 +72,9 @@ impl Client {
rx: Box<dyn AsyncBufRead + Unpin + Send>,
tx: Box<dyn AsyncWrite + Unpin + Send>,
err: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
id: usize,
id: DebugAdapterId,
process: Option<Child>,
) -> Result<(Self, UnboundedReceiver<Payload>)> {
) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let (server_rx, server_tx) = Transport::start(rx, tx, err, id);
let (client_tx, client_rx) = unbounded_channel();
@@ -86,22 +86,24 @@ impl Client {
caps: None,
connection_type: None,
starting_request_args: None,
socket: None,
stack_frames: HashMap::new(),
thread_states: HashMap::new(),
thread_id: None,
active_frame: None,
quirks: DebuggerQuirks::default(),
config: None,
};
tokio::spawn(Self::recv(server_rx, client_tx));
tokio::spawn(Self::recv(id, server_rx, client_tx));
Ok((client, client_rx))
}
pub async fn tcp(
addr: std::net::SocketAddr,
id: usize,
) -> Result<(Self, UnboundedReceiver<Payload>)> {
id: DebugAdapterId,
) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let stream = TcpStream::connect(addr).await?;
let (rx, tx) = stream.into_split();
Self::streams(Box::new(BufReader::new(rx)), Box::new(tx), None, id, None)
@@ -110,8 +112,8 @@ impl Client {
pub fn stdio(
cmd: &str,
args: Vec<&str>,
id: usize,
) -> Result<(Self, UnboundedReceiver<Payload>)> {
id: DebugAdapterId,
) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
// Resolve path to the binary
let cmd = helix_stdx::env::which(cmd)?;
@@ -119,6 +121,7 @@ impl Client {
.args(args)
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
// make sure the process is reaped on drop
.kill_on_drop(true)
.spawn();
@@ -128,16 +131,12 @@ impl Client {
// TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
let errors = process.stderr.take().map(BufReader::new);
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
Self::streams(
Box::new(BufReader::new(reader)),
Box::new(reader),
Box::new(writer),
// errors.map(|errors| Box::new(BufReader::new(errors))),
match errors {
Some(errors) => Some(Box::new(BufReader::new(errors))),
None => None,
},
Some(Box::new(stderr)),
id,
Some(process),
)
@@ -165,8 +164,8 @@ impl Client {
cmd: &str,
args: Vec<&str>,
port_format: &str,
id: usize,
) -> Result<(Self, UnboundedReceiver<Payload>)> {
id: DebugAdapterId,
) -> Result<(Self, UnboundedReceiver<(DebugAdapterId, Payload)>)> {
let port = Self::get_port().await.unwrap();
let process = Command::new(cmd)
@@ -181,40 +180,49 @@ impl Client {
// Wait for adapter to become ready for connection
time::sleep(time::Duration::from_millis(500)).await;
let stream = TcpStream::connect(SocketAddr::new(
IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)),
port,
))
.await?;
let socket = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), port);
let stream = TcpStream::connect(socket).await?;
let (rx, tx) = stream.into_split();
Self::streams(
let mut result = Self::streams(
Box::new(BufReader::new(rx)),
Box::new(tx),
None,
id,
Some(process),
)
);
// Set the socket address for the client
if let Ok((client, _)) = &mut result {
client.socket = Some(socket);
}
result
}
async fn recv(mut server_rx: UnboundedReceiver<Payload>, client_tx: UnboundedSender<Payload>) {
async fn recv(
id: DebugAdapterId,
mut server_rx: UnboundedReceiver<Payload>,
client_tx: UnboundedSender<(DebugAdapterId, Payload)>,
) {
while let Some(msg) = server_rx.recv().await {
match msg {
Payload::Event(ev) => {
client_tx.send(Payload::Event(ev)).expect("Failed to send");
client_tx
.send((id, Payload::Event(ev)))
.expect("Failed to send");
}
Payload::Response(_) => unreachable!(),
Payload::Request(req) => {
client_tx
.send(Payload::Request(req))
.send((id, Payload::Request(req)))
.expect("Failed to send");
}
}
}
}
pub fn id(&self) -> usize {
pub fn id(&self) -> DebugAdapterId {
self.id
}
@@ -357,6 +365,14 @@ impl Client {
self.call::<requests::Disconnect>(args)
}
pub fn terminate(
&mut self,
args: Option<TerminateArguments>,
) -> impl Future<Output = Result<Value>> {
self.connection_type = None;
self.call::<requests::Terminate>(args)
}
pub fn launch(&mut self, args: serde_json::Value) -> impl Future<Output = Result<Value>> {
self.connection_type = Some(ConnectionType::Launch);
self.starting_request_args = Some(args.clone());

View File

@@ -1,12 +1,14 @@
mod client;
pub mod registry;
mod transport;
mod types;
pub use client::{Client, ConnectionType};
pub use events::Event;
pub use client::Client;
pub use transport::{Payload, Response, Transport};
pub use types::*;
use serde::de::DeserializeOwned;
use thiserror::Error;
#[derive(Error, Debug)]
pub enum Error {
@@ -18,9 +20,86 @@ pub enum Error {
Timeout(u64),
#[error("server closed the stream")]
StreamClosed,
#[error("Unhandled")]
Unhandled,
#[error(transparent)]
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
pub type Result<T> = core::result::Result<T, Error>;
#[derive(Debug)]
pub enum Request {
RunInTerminal(<requests::RunInTerminal as types::Request>::Arguments),
StartDebugging(<requests::StartDebugging as types::Request>::Arguments),
}
impl Request {
pub fn parse(command: &str, arguments: Option<serde_json::Value>) -> Result<Self> {
use crate::types::Request as _;
let arguments = arguments.unwrap_or_default();
let request = match command {
requests::RunInTerminal::COMMAND => Self::RunInTerminal(parse_value(arguments)?),
requests::StartDebugging::COMMAND => Self::StartDebugging(parse_value(arguments)?),
_ => return Err(Error::Unhandled),
};
Ok(request)
}
}
#[derive(Debug)]
pub enum Event {
Initialized(<events::Initialized as events::Event>::Body),
Stopped(<events::Stopped as events::Event>::Body),
Continued(<events::Continued as events::Event>::Body),
Exited(<events::Exited as events::Event>::Body),
Terminated(<events::Terminated as events::Event>::Body),
Thread(<events::Thread as events::Event>::Body),
Output(<events::Output as events::Event>::Body),
Breakpoint(<events::Breakpoint as events::Event>::Body),
Module(<events::Module as events::Event>::Body),
LoadedSource(<events::LoadedSource as events::Event>::Body),
Process(<events::Process as events::Event>::Body),
Capabilities(<events::Capabilities as events::Event>::Body),
// ProgressStart(),
// ProgressUpdate(),
// ProgressEnd(),
// Invalidated(),
Memory(<events::Memory as events::Event>::Body),
}
impl Event {
pub fn parse(event: &str, body: Option<serde_json::Value>) -> Result<Self> {
use crate::events::Event as _;
let body = body.unwrap_or_default();
let event = match event {
events::Initialized::EVENT => Self::Initialized(parse_value(body)?),
events::Stopped::EVENT => Self::Stopped(parse_value(body)?),
events::Continued::EVENT => Self::Continued(parse_value(body)?),
events::Exited::EVENT => Self::Exited(parse_value(body)?),
events::Terminated::EVENT => Self::Terminated(parse_value(body)?),
events::Thread::EVENT => Self::Thread(parse_value(body)?),
events::Output::EVENT => Self::Output(parse_value(body)?),
events::Breakpoint::EVENT => Self::Breakpoint(parse_value(body)?),
events::Module::EVENT => Self::Module(parse_value(body)?),
events::LoadedSource::EVENT => Self::LoadedSource(parse_value(body)?),
events::Process::EVENT => Self::Process(parse_value(body)?),
events::Capabilities::EVENT => Self::Capabilities(parse_value(body)?),
events::Memory::EVENT => Self::Memory(parse_value(body)?),
_ => return Err(Error::Unhandled),
};
Ok(event)
}
}
fn parse_value<T>(value: serde_json::Value) -> Result<T>
where
T: DeserializeOwned,
{
serde_json::from_value(value).map_err(|err| err.into())
}

114
helix-dap/src/registry.rs Normal file
View File

@@ -0,0 +1,114 @@
use crate::{Client, Payload, Result, StackFrame};
use futures_executor::block_on;
use futures_util::stream::SelectAll;
use helix_core::syntax::config::DebugAdapterConfig;
use slotmap::SlotMap;
use std::fmt;
use tokio_stream::wrappers::UnboundedReceiverStream;
/// The resgistry is a struct that manages and owns multiple debugger clients
/// This holds the responsibility of managing the lifecycle of each client
/// plus showing the heirarcihical nature betweeen them
pub struct Registry {
inner: SlotMap<DebugAdapterId, Client>,
/// The active debugger client
///
/// TODO: You can have multiple active debuggers, so the concept of a single active debugger
/// may need to be changed
current_client_id: Option<DebugAdapterId>,
/// A stream of incoming messages from all debuggers
pub incoming: SelectAll<UnboundedReceiverStream<(DebugAdapterId, Payload)>>,
}
impl Registry {
/// Creates a new DebuggerService instance
pub fn new() -> Self {
Self {
inner: SlotMap::with_key(),
current_client_id: None,
incoming: SelectAll::new(),
}
}
pub fn start_client(
&mut self,
socket: Option<std::net::SocketAddr>,
config: &DebugAdapterConfig,
) -> Result<DebugAdapterId> {
self.inner.try_insert_with_key(|id| {
let result = match socket {
Some(socket) => block_on(Client::tcp(socket, id)),
None => block_on(Client::process(
&config.transport,
&config.command,
config.args.iter().map(|arg| arg.as_str()).collect(),
config.port_arg.as_deref(),
id,
)),
};
let (mut client, receiver) = result?;
self.incoming.push(UnboundedReceiverStream::new(receiver));
client.config = Some(config.clone());
block_on(client.initialize(config.name.clone()))?;
client.quirks = config.quirks.clone();
Ok(client)
})
}
pub fn remove_client(&mut self, id: DebugAdapterId) {
self.inner.remove(id);
}
pub fn get_client(&self, id: DebugAdapterId) -> Option<&Client> {
self.inner.get(id)
}
pub fn get_client_mut(&mut self, id: DebugAdapterId) -> Option<&mut Client> {
self.inner.get_mut(id)
}
pub fn get_active_client(&self) -> Option<&Client> {
self.current_client_id.and_then(|id| self.get_client(id))
}
pub fn get_active_client_mut(&mut self) -> Option<&mut Client> {
self.current_client_id
.and_then(|id| self.get_client_mut(id))
}
pub fn set_active_client(&mut self, id: DebugAdapterId) {
if self.get_client(id).is_some() {
self.current_client_id = Some(id);
} else {
self.current_client_id = None;
}
}
pub fn unset_active_client(&mut self) {
self.current_client_id = None;
}
pub fn current_stack_frame(&self) -> Option<&StackFrame> {
self.get_active_client()
.and_then(|debugger| debugger.current_stack_frame())
}
}
impl Default for Registry {
fn default() -> Self {
Self::new()
}
}
slotmap::new_key_type! {
pub struct DebugAdapterId;
}
impl fmt::Display for DebugAdapterId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.0)
}
}

View File

@@ -1,10 +1,10 @@
use crate::{Error, Event, Result};
use crate::{registry::DebugAdapterId, Error, Result};
use anyhow::Context;
use log::{error, info, warn};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use std::{collections::HashMap, fmt::Debug};
use tokio::{
io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWrite, AsyncWriteExt},
sync::{
@@ -32,11 +32,17 @@ pub struct Response {
pub body: Option<Value>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
pub struct Event {
pub event: String,
pub body: Option<Value>,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
#[serde(tag = "type", rename_all = "camelCase")]
pub enum Payload {
// type = "event"
Event(Box<Event>),
Event(Event),
// type = "response"
Response(Response),
// type = "request"
@@ -46,7 +52,7 @@ pub enum Payload {
#[derive(Debug)]
pub struct Transport {
#[allow(unused)]
id: usize,
id: DebugAdapterId,
pending_requests: Mutex<HashMap<u64, Sender<Result<Response>>>>,
}
@@ -55,7 +61,7 @@ impl Transport {
server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
server_stdin: Box<dyn AsyncWrite + Unpin + Send>,
server_stderr: Option<Box<dyn AsyncBufRead + Unpin + Send>>,
id: usize,
id: DebugAdapterId,
) -> (UnboundedReceiver<Payload>, UnboundedSender<Payload>) {
let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel();
@@ -67,7 +73,7 @@ impl Transport {
let transport = Arc::new(transport);
tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx));
tokio::spawn(Self::recv(id, transport.clone(), server_stdout, client_tx));
tokio::spawn(Self::send(transport, server_stdin, client_rx));
if let Some(stderr) = server_stderr {
tokio::spawn(Self::err(stderr));
@@ -77,12 +83,14 @@ impl Transport {
}
async fn recv_server_message(
id: DebugAdapterId,
reader: &mut Box<dyn AsyncBufRead + Unpin + Send>,
buffer: &mut String,
content: &mut Vec<u8>,
) -> Result<Payload> {
let mut content_length = None;
loop {
buffer.truncate(0);
buffer.clear();
if reader.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed);
};
@@ -111,17 +119,17 @@ impl Transport {
}
let content_length = content_length.context("missing content length")?;
content.resize(content_length, 0);
reader.read_exact(content).await?;
let msg = std::str::from_utf8(content).context("invalid utf8 from server")?;
//TODO: reuse vector
let mut content = vec![0; content_length];
reader.read_exact(&mut content).await?;
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
info!("<- DAP {}", msg);
info!("[{}] <- DAP {}", id, msg);
// try parsing as output (server response) or call (server request)
let output: serde_json::Result<Payload> = serde_json::from_str(msg);
content.clear();
Ok(output?)
}
@@ -157,7 +165,7 @@ impl Transport {
server_stdin: &mut Box<dyn AsyncWrite + Unpin + Send>,
request: String,
) -> Result<()> {
info!("-> DAP {}", request);
info!("[{}] -> DAP {}", self.id, request);
// send the headers
server_stdin
@@ -172,15 +180,18 @@ impl Transport {
Ok(())
}
fn process_response(res: Response) -> Result<Response> {
fn process_response(&self, res: Response) -> Result<Response> {
if res.success {
info!("<- DAP success in response to {}", res.request_seq);
info!(
"[{}] <- DAP success in response to {}",
self.id, res.request_seq
);
Ok(res)
} else {
error!(
"<- DAP error {:?} ({:?}) for command #{} {}",
res.message, res.body, res.request_seq, res.command
"[{}] <- DAP error {:?} ({:?}) for command #{} {}",
self.id, res.message, res.body, res.request_seq, res.command
);
Err(Error::Other(anyhow::format_err!("{:?}", res.body)))
@@ -198,7 +209,7 @@ impl Transport {
let tx = self.pending_requests.lock().await.remove(&request_seq);
match tx {
Some(tx) => match tx.send(Self::process_response(res)).await {
Some(tx) => match tx.send(self.process_response(res)).await {
Ok(_) => (),
Err(_) => error!(
"Tried sending response into a closed channel (id={:?}), original request likely timed out",
@@ -218,37 +229,58 @@ impl Transport {
ref seq,
..
}) => {
info!("<- DAP request {} #{}", command, seq);
info!("[{}] <- DAP request {} #{}", self.id, command, seq);
client_tx.send(msg).expect("Failed to send");
Ok(())
}
Payload::Event(ref event) => {
info!("<- DAP event {:?}", event);
info!("[{}] <- DAP event {:?}", self.id, event);
client_tx.send(msg).expect("Failed to send");
Ok(())
}
}
}
async fn recv_inner(
async fn recv(
id: DebugAdapterId,
transport: Arc<Self>,
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
client_tx: UnboundedSender<Payload>,
) -> Result<()> {
let mut recv_buffer = String::new();
loop {
let msg = Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await?;
transport.process_server_message(&client_tx, msg).await?;
}
}
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);
let mut recv_buffer = String::new();
let mut content_buffer = Vec::new();
loop {
match Self::recv_server_message(
id,
&mut server_stdout,
&mut recv_buffer,
&mut content_buffer,
)
.await
{
Ok(msg) => match transport.process_server_message(&client_tx, msg).await {
Ok(_) => (),
Err(err) => {
error!(" [{id}] err: <- {err:?}");
break;
}
},
Err(err) => {
if !matches!(err, Error::StreamClosed) {
error!("Exiting after unexpected error: {err:?}");
}
// Close any outstanding requests.
for (id, tx) in transport.pending_requests.lock().await.drain() {
match tx.send(Err(Error::StreamClosed)).await {
Ok(_) => (),
Err(_) => {
error!("Could not close request on a closed channel (id={id})");
}
}
}
}
}
}
}

View File

@@ -438,6 +438,21 @@ pub mod requests {
const COMMAND: &'static str = "disconnect";
}
#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TerminateArguments {
pub restart: Option<bool>,
}
#[derive(Debug)]
pub enum Terminate {}
impl Request for Terminate {
type Arguments = Option<TerminateArguments>;
type Result = ();
const COMMAND: &'static str = "terminate";
}
#[derive(Debug)]
pub enum ConfigurationDone {}
@@ -752,6 +767,21 @@ pub mod requests {
type Result = RunInTerminalResponse;
const COMMAND: &'static str = "runInTerminal";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct StartDebuggingArguments {
pub request: ConnectionType,
pub configuration: Value,
}
#[derive(Debug)]
pub enum StartDebugging {}
impl Request for StartDebugging {
type Arguments = StartDebuggingArguments;
type Result = ();
const COMMAND: &'static str = "startDebugging";
}
}
// Events
@@ -759,33 +789,30 @@ pub mod requests {
pub mod events {
use super::*;
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
#[serde(tag = "event", content = "body")]
// seq is omitted as unused and is not sent by some implementations
pub enum Event {
Initialized(Option<DebuggerCapabilities>),
Stopped(Stopped),
Continued(Continued),
Exited(Exited),
Terminated(Option<Terminated>),
Thread(Thread),
Output(Output),
Breakpoint(Breakpoint),
Module(Module),
LoadedSource(LoadedSource),
Process(Process),
Capabilities(Capabilities),
// ProgressStart(),
// ProgressUpdate(),
// ProgressEnd(),
// Invalidated(),
Memory(Memory),
pub trait Event {
type Body: serde::de::DeserializeOwned + serde::Serialize;
const EVENT: &'static str;
}
#[derive(Debug)]
pub enum Initialized {}
impl Event for Initialized {
type Body = Option<DebuggerCapabilities>;
const EVENT: &'static str = "initialized";
}
#[derive(Debug)]
pub enum Stopped {}
impl Event for Stopped {
type Body = StoppedBody;
const EVENT: &'static str = "stopped";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Stopped {
pub struct StoppedBody {
pub reason: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
@@ -801,37 +828,77 @@ pub mod events {
pub hit_breakpoint_ids: Option<Vec<usize>>,
}
#[derive(Debug)]
pub enum Continued {}
impl Event for Continued {
type Body = ContinuedBody;
const EVENT: &'static str = "continued";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Continued {
pub struct ContinuedBody {
pub thread_id: ThreadId,
#[serde(skip_serializing_if = "Option::is_none")]
pub all_threads_continued: Option<bool>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Exited {
pub exit_code: usize,
#[derive(Debug)]
pub enum Exited {}
impl Event for Exited {
type Body = ExitedBody;
const EVENT: &'static str = "exited";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Terminated {
pub struct ExitedBody {
pub exit_code: usize,
}
#[derive(Debug)]
pub enum Terminated {}
impl Event for Terminated {
type Body = Option<TerminatedBody>;
const EVENT: &'static str = "terminated";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct TerminatedBody {
#[serde(skip_serializing_if = "Option::is_none")]
pub restart: Option<Value>,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Thread {
pub reason: String,
pub thread_id: ThreadId,
#[derive(Debug)]
pub enum Thread {}
impl Event for Thread {
type Body = ThreadBody;
const EVENT: &'static str = "thread";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Output {
pub struct ThreadBody {
pub reason: String,
pub thread_id: ThreadId,
}
#[derive(Debug)]
pub enum Output {}
impl Event for Output {
type Body = OutputBody;
const EVENT: &'static str = "output";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct OutputBody {
pub output: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub category: Option<String>,
@@ -849,30 +916,62 @@ pub mod events {
pub data: Option<Value>,
}
#[derive(Debug)]
pub enum Breakpoint {}
impl Event for Breakpoint {
type Body = BreakpointBody;
const EVENT: &'static str = "breakpoint";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Breakpoint {
pub struct BreakpointBody {
pub reason: String,
pub breakpoint: super::Breakpoint,
}
#[derive(Debug)]
pub enum Module {}
impl Event for Module {
type Body = ModuleBody;
const EVENT: &'static str = "module";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Module {
pub struct ModuleBody {
pub reason: String,
pub module: super::Module,
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct LoadedSource {
pub reason: String,
pub source: super::Source,
#[derive(Debug)]
pub enum LoadedSource {}
impl Event for LoadedSource {
type Body = LoadedSourceBody;
const EVENT: &'static str = "loadedSource";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Process {
pub struct LoadedSourceBody {
pub reason: String,
pub source: super::Source,
}
#[derive(Debug)]
pub enum Process {}
impl Event for Process {
type Body = ProcessBody;
const EVENT: &'static str = "process";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct ProcessBody {
pub name: String,
#[serde(skip_serializing_if = "Option::is_none")]
pub system_process_id: Option<usize>,
@@ -884,39 +983,62 @@ pub mod events {
pub pointer_size: Option<usize>,
}
#[derive(Debug)]
pub enum Capabilities {}
impl Event for Capabilities {
type Body = CapabilitiesBody;
const EVENT: &'static str = "capabilities";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Capabilities {
pub struct CapabilitiesBody {
pub capabilities: super::DebuggerCapabilities,
}
// #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
// #[serde(rename_all = "camelCase")]
// pub struct Invalidated {
// pub struct InvalidatedBody {
// pub areas: Vec<InvalidatedArea>,
// pub thread_id: Option<ThreadId>,
// pub stack_frame_id: Option<usize>,
// }
#[derive(Debug)]
pub enum Memory {}
impl Event for Memory {
type Body = MemoryBody;
const EVENT: &'static str = "memory";
}
#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct Memory {
pub struct MemoryBody {
pub memory_reference: String,
pub offset: usize,
pub count: usize,
}
#[test]
fn test_deserialize_module_id_from_number() {
let raw = r#"{"id": 0, "name": "Name"}"#;
let module: super::Module = serde_json::from_str(raw).expect("Error!");
assert_eq!(module.id, "0");
}
#[test]
fn test_deserialize_module_id_from_string() {
let raw = r#"{"id": "0", "name": "Name"}"#;
let module: super::Module = serde_json::from_str(raw).expect("Error!");
assert_eq!(module.id, "0");
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ConnectionType {
Launch,
Attach,
}
#[test]
fn test_deserialize_module_id_from_number() {
let raw = r#"{"id": 0, "name": "Name"}"#;
let module: Module = serde_json::from_str(raw).expect("Error!");
assert_eq!(module.id, "0");
}
#[test]
fn test_deserialize_module_id_from_string() {
let raw = r#"{"id": "0", "name": "Name"}"#;
let module: Module = serde_json::from_str(raw).expect("Error!");
assert_eq!(module.id, "0");
}

View File

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

View File

@@ -14,8 +14,8 @@ use crate::hook::ErasedHook;
use crate::runtime_local;
pub struct Registry {
events: HashMap<&'static str, TypeId, ahash::RandomState>,
handlers: HashMap<&'static str, Vec<ErasedHook>, ahash::RandomState>,
events: HashMap<&'static str, TypeId, foldhash::fast::FixedState>,
handlers: HashMap<&'static str, Vec<ErasedHook>, foldhash::fast::FixedState>,
}
impl Registry {
@@ -105,8 +105,8 @@ runtime_local! {
static REGISTRY: RwLock<Registry> = RwLock::new(Registry {
// hardcoded random number is good enough here we don't care about DOS resistance
// and avoids the additional complexity of `Option<Registry>`
events: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 9978, 38322, 3280080)),
handlers: HashMap::with_hasher(ahash::RandomState::with_seeds(423, 99078, 382322, 3282938)),
events: HashMap::with_hasher(foldhash::fast::FixedState::with_seed(72536814787)),
handlers: HashMap::with_hasher(foldhash::fast::FixedState::with_seed(72536814787)),
});
}

View File

@@ -41,8 +41,9 @@ macro_rules! runtime_local {
#[cfg(feature = "integration_test")]
pub struct RuntimeLocal<T: 'static> {
data:
parking_lot::RwLock<hashbrown::HashMap<tokio::runtime::Id, &'static T, ahash::RandomState>>,
data: parking_lot::RwLock<
hashbrown::HashMap<tokio::runtime::Id, &'static T, foldhash::fast::FixedState>,
>,
init: fn() -> T,
}
@@ -53,7 +54,7 @@ impl<T> RuntimeLocal<T> {
pub const fn __new(init: fn() -> T) -> Self {
Self {
data: parking_lot::RwLock::new(hashbrown::HashMap::with_hasher(
ahash::RandomState::with_seeds(423, 9978, 38322, 3280080),
foldhash::fast::FixedState::with_seed(12345678910),
)),
init,
}

View File

@@ -56,6 +56,7 @@ fn smoke_test() {
}
#[test]
#[allow(dead_code)]
fn dynamic() {
events! {
Event3 {}

View File

@@ -20,9 +20,8 @@ helix-stdx = { path = "../helix-stdx" }
anyhow = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
etcetera = "0.8"
tree-sitter.workspace = true
once_cell = "1.20"
etcetera = "0.10"
once_cell = "1.21"
log = "0.4"
# TODO: these two should be on !wasm32 only
@@ -31,7 +30,5 @@ log = "0.4"
cc = { version = "1" }
threadpool = { version = "1.0" }
tempfile.workspace = true
dunce = "1.0.5"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
libloading = "0.8"
tree-house.workspace = true

View File

@@ -23,22 +23,6 @@ pub fn user_lang_config() -> Result<toml::Value, toml::de::Error> {
.collect::<Result<Vec<_>, _>>()?
.into_iter()
.fold(default_lang_config(), |a, b| {
// combines for example
// b:
// [[language]]
// name = "toml"
// language-server = { command = "taplo", args = ["lsp", "stdio"] }
//
// a:
// [[language]]
// language-server = { command = "/usr/bin/taplo" }
//
// into:
// [[language]]
// name = "toml"
// language-server = { command = "/usr/bin/taplo" }
//
// thus it overrides the third depth-level of b with values of a if they exist, but otherwise merges their values
crate::merge_toml_values(a, b, 3)
});

View File

@@ -9,7 +9,7 @@ use std::{
sync::mpsc::channel,
};
use tempfile::TempPath;
use tree_sitter::Language;
use tree_house::tree_sitter::Grammar;
#[cfg(unix)]
const DYLIB_EXTENSION: &str = "so";
@@ -61,28 +61,21 @@ const BUILD_TARGET: &str = env!("BUILD_TARGET");
const REMOTE_NAME: &str = "origin";
#[cfg(target_arch = "wasm32")]
pub fn get_language(name: &str) -> Result<Language> {
pub fn get_language(name: &str) -> Result<Option<Grammar>> {
unimplemented!()
}
#[cfg(not(target_arch = "wasm32"))]
pub fn get_language(name: &str) -> Result<Language> {
use libloading::{Library, Symbol};
pub fn get_language(name: &str) -> Result<Option<Grammar>> {
let mut rel_library_path = PathBuf::new().join("grammars").join(name);
rel_library_path.set_extension(DYLIB_EXTENSION);
let library_path = crate::runtime_file(&rel_library_path);
if !library_path.exists() {
return Ok(None);
}
let library = unsafe { Library::new(&library_path) }
.with_context(|| format!("Error opening dynamic library {:?}", library_path))?;
let language_fn_name = format!("tree_sitter_{}", name.replace('-', "_"));
let language = unsafe {
let language_fn: Symbol<unsafe extern "C" fn() -> Language> = library
.get(language_fn_name.as_bytes())
.with_context(|| format!("Failed to load symbol {}", language_fn_name))?;
language_fn()
};
std::mem::forget(library);
Ok(language)
let grammar = unsafe { Grammar::new(name, &library_path) }?;
Ok(Some(grammar))
}
fn ensure_git_is_available() -> Result<()> {
@@ -273,12 +266,12 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result<FetchStatus> {
}
// ensure the remote matches the configured remote
if get_remote_url(&grammar_dir).map_or(true, |s| s != remote) {
if get_remote_url(&grammar_dir).as_ref() != Some(&remote) {
set_remote(&grammar_dir, &remote)?;
}
// ensure the revision matches the configured revision
if get_revision(&grammar_dir).map_or(true, |s| s != revision) {
if get_revision(&grammar_dir).as_ref() != Some(&revision) {
// Fetch the exact revision from the remote.
// Supported by server-side git since v2.5.0 (July 2015),
// enabled by default on major git hosts.
@@ -451,7 +444,6 @@ fn build_tree_sitter_library(
command
.args(["/nologo", "/LD", "/I"])
.arg(header_path)
.arg("/Od")
.arg("/utf-8")
.arg("/std:c11");
if let Some(scanner_path) = scanner_path.as_ref() {
@@ -469,7 +461,6 @@ fn build_tree_sitter_library(
cpp_command
.args(["/nologo", "/LD", "/I"])
.arg(header_path)
.arg("/Od")
.arg("/utf-8")
.arg("/std:c++14")
.arg(format!("/Fo{}", object_file.display()))
@@ -496,9 +487,11 @@ fn build_tree_sitter_library(
.arg("/link")
.arg(format!("/out:{}", library_path.to_str().unwrap()));
} else {
#[cfg(not(windows))]
command.arg("-fPIC");
command
.arg("-shared")
.arg("-fPIC")
.arg("-fno-exceptions")
.arg("-I")
.arg(header_path)
@@ -517,8 +510,11 @@ fn build_tree_sitter_library(
cpp_command.args(compiler.args());
let object_file =
library_path.with_file_name(format!("{}_scanner.o", &grammar.grammar_id));
#[cfg(not(windows))]
cpp_command.arg("-fPIC");
cpp_command
.arg("-fPIC")
.arg("-fno-exceptions")
.arg("-I")
.arg(header_path)
@@ -592,6 +588,6 @@ fn mtime(path: &Path) -> Result<SystemTime> {
/// Gives the contents of a file from a language's `runtime/queries/<lang>`
/// directory
pub fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let path = crate::runtime_file(&PathBuf::new().join("queries").join(language).join(filename));
let path = crate::runtime_file(PathBuf::new().join("queries").join(language).join(filename));
std::fs::read_to_string(path)
}

View File

@@ -107,8 +107,8 @@ fn find_runtime_file(rel_path: &Path) -> Option<PathBuf> {
/// The valid runtime directories are searched in priority order and the first
/// file found to exist is returned, otherwise the path to the final attempt
/// that failed.
pub fn runtime_file(rel_path: &Path) -> PathBuf {
find_runtime_file(rel_path).unwrap_or_else(|| {
pub fn runtime_file(rel_path: impl AsRef<Path>) -> PathBuf {
find_runtime_file(rel_path.as_ref()).unwrap_or_else(|| {
RUNTIME_DIRS
.last()
.map(|dir| dir.join(rel_path))
@@ -154,17 +154,36 @@ pub fn default_log_file() -> PathBuf {
/// Merge two TOML documents, merging values from `right` onto `left`
///
/// When an array exists in both `left` and `right`, `right`'s array is
/// used. When a table exists in both `left` and `right`, the merged table
/// consists of all keys in `left`'s table unioned with all keys in `right`
/// with the values of `right` being merged recursively onto values of
/// `left`.
/// `merge_depth` sets the nesting depth up to which values are merged instead
/// of overridden.
///
/// `merge_toplevel_arrays` controls whether a top-level array in the TOML
/// document is merged instead of overridden. This is useful for TOML
/// documents that use a top-level array of values like the `languages.toml`,
/// where one usually wants to override or add to the array instead of
/// replacing it altogether.
/// When a table exists in both `left` and `right`, the merged table consists of
/// all keys in `left`'s table unioned with all keys in `right` with the values
/// of `right` being merged recursively onto values of `left`.
///
/// `crate::merge_toml_values(a, b, 3)` combines, for example:
///
/// b:
/// ```toml
/// [[language]]
/// name = "toml"
/// language-server = { command = "taplo", args = ["lsp", "stdio"] }
/// ```
/// a:
/// ```toml
/// [[language]]
/// language-server = { command = "/usr/bin/taplo" }
/// ```
///
/// into:
/// ```toml
/// [[language]]
/// name = "toml"
/// language-server = { command = "/usr/bin/taplo" }
/// ```
///
/// thus it overrides the third depth-level of b with values of a if they exist,
/// but otherwise merges their values
pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usize) -> toml::Value {
use toml::Value;
@@ -174,11 +193,6 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
match (left, right) {
(Value::Array(mut left_items), Value::Array(right_items)) => {
// The top-level arrays should be merged but nested arrays should
// act as overrides. For the `languages.toml` config, this means
// that you can specify a sub-set of languages in an overriding
// `languages.toml` but that nested arrays like Language Server
// arguments are replaced instead of merged.
if merge_depth > 0 {
left_items.reserve(right_items.len());
for rvalue in right_items {
@@ -230,7 +244,12 @@ pub fn merge_toml_values(left: toml::Value, right: toml::Value, merge_depth: usi
/// Otherwise (workspace, false) is returned
pub fn find_workspace() -> (PathBuf, bool) {
let current_dir = current_working_dir();
for ancestor in current_dir.ancestors() {
find_workspace_in(current_dir)
}
pub fn find_workspace_in(dir: impl AsRef<Path>) -> (PathBuf, bool) {
let dir = dir.as_ref();
for ancestor in dir.ancestors() {
if ancestor.join(".git").exists()
|| ancestor.join(".svn").exists()
|| ancestor.join(".jj").exists()
@@ -240,7 +259,7 @@ pub fn find_workspace() -> (PathBuf, bool) {
}
}
(current_dir, true)
(dir.to_owned(), true)
}
fn default_config_file() -> PathBuf {

View File

@@ -22,9 +22,8 @@ license = "MIT"
[dependencies]
bitflags.workspace = true
serde = { version = "1.0.217", features = ["derive"] }
serde_json = "1.0.135"
serde_repr = "0.1"
serde = { version = "1.0.219", features = ["derive"] }
serde_json = "1.0.140"
url = {version = "2.5.4", features = ["serde"]}
[features]

View File

@@ -1,4 +1,4 @@
use std::collections::HashMap;
use std::{collections::HashMap, sync::Arc};
use serde::{Deserialize, Serialize};
@@ -33,8 +33,13 @@ pub struct DiagnosticClientCapabilities {
pub struct DiagnosticOptions {
/// An optional identifier under which the diagnostics are
/// managed by the client.
#[serde(skip_serializing_if = "Option::is_none")]
pub identifier: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_option_arc_str",
deserialize_with = "deserialize_option_arc_str"
)]
pub identifier: Option<Arc<str>>,
/// Whether the language has inter file dependencies, meaning that editing code in one file can
/// result in a different diagnostic set in another file. Inter file dependencies are common
@@ -48,6 +53,19 @@ pub struct DiagnosticOptions {
pub work_done_progress_options: WorkDoneProgressOptions,
}
fn serialize_option_arc_str<S: serde::Serializer>(
val: &Option<Arc<str>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
serializer.serialize_str(val.as_ref().unwrap())
}
fn deserialize_option_arc_str<'de, D: serde::Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Arc<str>>, D::Error> {
Option::<String>::deserialize(deserializer).map(|opt| opt.map(|s| s.into()))
}
/// Diagnostic registration options.
///
/// @since 3.17.0
@@ -81,7 +99,13 @@ pub struct DocumentDiagnosticParams {
pub text_document: TextDocumentIdentifier,
/// The additional identifier provided during registration.
pub identifier: Option<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_option_arc_str",
deserialize_with = "deserialize_option_arc_str"
)]
pub identifier: Option<Arc<str>>,
/// The result ID of a previous response if provided.
pub previous_result_id: Option<String>,

View File

@@ -2568,9 +2568,9 @@ pub enum Documentation {
///
/// The pair of a language and a value is an equivalent to markdown:
///
/// ```${language}
/// <pre><code>```${language}
/// ${value}
/// ```
/// ```</code></pre>
#[derive(Debug, Eq, PartialEq, Clone, Deserialize, Serialize)]
#[serde(untagged)]
pub enum MarkedString {

View File

@@ -16,19 +16,18 @@ homepage.workspace = true
helix-stdx = { path = "../helix-stdx" }
helix-core = { path = "../helix-core" }
helix-loader = { path = "../helix-loader" }
helix-parsec = { path = "../helix-parsec" }
helix-lsp-types = { path = "../helix-lsp-types" }
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
globset = "0.4.15"
futures-executor.workspace = true
futures-util.workspace = true
globset = "0.4.16"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
tokio = { version = "1.43", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.17"
parking_lot = "0.12.3"
tokio = { version = "1.46", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream.workspace = true
parking_lot.workspace = true
arc-swap = "1"
slotmap.workspace = true
thiserror.workspace = true

View File

@@ -10,17 +10,20 @@ use crate::lsp::{
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url,
WorkspaceFolder, WorkspaceFoldersChangeEvent,
};
use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_core::{find_workspace, syntax::config::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::VERSION_AND_GIT_HASH;
use helix_stdx::path;
use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use std::{collections::HashMap, path::PathBuf};
use std::{
ffi::OsStr,
sync::{
atomic::{AtomicU64, Ordering},
Arc,
},
};
use std::{future::Future, sync::OnceLock};
use std::{path::Path, process::Stdio};
use tokio::{
@@ -36,7 +39,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
lsp::WorkspaceFolder {
name: uri
.path_segments()
.and_then(|segments| segments.last())
.and_then(|mut segments| segments.next_back())
.map(|basename| basename.to_string())
.unwrap_or_default(),
uri,
@@ -85,7 +88,7 @@ impl Client {
.and_then(|root| lsp::Url::from_file_path(root).ok());
if self.root_path == root.unwrap_or(workspace)
|| root_uri.as_ref().map_or(false, |root_uri| {
|| root_uri.as_ref().is_some_and(|root_uri| {
self.workspace_folders
.lock()
.iter()
@@ -170,7 +173,30 @@ impl Client {
// and that we can therefore reuse the client (but are done now)
return;
}
tokio::spawn(self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new()));
self.did_change_workspace(vec![workspace_for_uri(root_uri)], Vec::new())
}
/// Merge FormattingOptions with 'config.format' and return it
fn get_merged_formatting_options(
&self,
options: lsp::FormattingOptions,
) -> lsp::FormattingOptions {
let config_format = self
.config
.as_ref()
.and_then(|cfg| cfg.get("format"))
.and_then(|fmt| HashMap::<String, lsp::FormattingProperty>::deserialize(fmt).ok());
if let Some(mut properties) = config_format {
// passed in options take precedence over 'config.format'
properties.extend(options.properties);
lsp::FormattingOptions {
properties,
..options
}
} else {
options
}
}
#[allow(clippy::type_complexity, clippy::too_many_arguments)]
@@ -178,7 +204,7 @@ impl Client {
cmd: &str,
args: &[String],
config: Option<Value>,
server_environment: HashMap<String, String>,
server_environment: impl IntoIterator<Item = (impl AsRef<OsStr>, impl AsRef<OsStr>)>,
root_path: PathBuf,
root_uri: Option<lsp::Url>,
id: LanguageServerId,
@@ -198,6 +224,7 @@ impl Client {
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.current_dir(&root_path)
// make sure the process is reaped on drop
.kill_on_drop(true)
.spawn();
@@ -353,6 +380,14 @@ impl Client {
capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
),
LanguageServerFeature::DocumentColors => matches!(
capabilities.color_provider,
Some(
ColorProviderCapability::Simple(true)
| ColorProviderCapability::ColorProvider(_)
| ColorProviderCapability::Options(_)
)
),
}
}
@@ -382,23 +417,11 @@ impl Client {
self.workspace_folders.lock()
}
/// Execute a RPC request on the language server.
async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
where
R::Params: serde::Serialize,
R::Result: core::fmt::Debug, // TODO: temporary
{
// a future that resolves into the response
let json = self.call::<R>(params).await?;
let response = serde_json::from_value(json)?;
Ok(response)
}
/// Execute a RPC request on the language server.
fn call<R: lsp::request::Request>(
&self,
params: R::Params,
) -> impl Future<Output = Result<Value>>
) -> impl Future<Output = Result<R::Result>>
where
R::Params: serde::Serialize,
{
@@ -408,7 +431,7 @@ impl Client {
fn call_with_ref<R: lsp::request::Request>(
&self,
params: &R::Params,
) -> impl Future<Output = Result<Value>>
) -> impl Future<Output = Result<R::Result>>
where
R::Params: serde::Serialize,
{
@@ -419,66 +442,77 @@ impl Client {
&self,
params: &R::Params,
timeout_secs: u64,
) -> impl Future<Output = Result<Value>>
) -> impl Future<Output = Result<R::Result>>
where
R::Params: serde::Serialize,
{
let server_tx = self.server_tx.clone();
let id = self.next_request_id();
let params = serde_json::to_value(params);
// It's important that this is not part of the future so that it gets executed right away
// and the request order stays consistent.
let rx = serde_json::to_value(params)
.map_err(Error::from)
.and_then(|params| {
let request = jsonrpc::MethodCall {
jsonrpc: Some(jsonrpc::Version::V2),
id: id.clone(),
method: R::METHOD.to_string(),
params: Self::value_into_params(params),
};
let (tx, rx) = channel::<Result<Value>>(1);
server_tx
.send(Payload::Request {
chan: tx,
value: request,
})
.map_err(|e| Error::Other(e.into()))?;
Ok(rx)
});
async move {
use std::time::Duration;
use tokio::time::timeout;
let request = jsonrpc::MethodCall {
jsonrpc: Some(jsonrpc::Version::V2),
id: id.clone(),
method: R::METHOD.to_string(),
params: Self::value_into_params(params?),
};
let (tx, mut rx) = channel::<Result<Value>>(1);
server_tx
.send(Payload::Request {
chan: tx,
value: request,
})
.map_err(|e| Error::Other(e.into()))?;
// TODO: delay other calls until initialize success
timeout(Duration::from_secs(timeout_secs), rx.recv())
timeout(Duration::from_secs(timeout_secs), rx?.recv())
.await
.map_err(|_| Error::Timeout(id))? // return Timeout
.ok_or(Error::StreamClosed)?
.and_then(|value| serde_json::from_value(value).map_err(Into::into))
}
}
/// Send a RPC notification to the language server.
pub fn notify<R: lsp::notification::Notification>(
&self,
params: R::Params,
) -> impl Future<Output = Result<()>>
pub fn notify<R: lsp::notification::Notification>(&self, params: R::Params)
where
R::Params: serde::Serialize,
{
let server_tx = self.server_tx.clone();
async move {
let params = serde_json::to_value(params)?;
let params = match serde_json::to_value(params) {
Ok(params) => params,
Err(err) => {
log::error!(
"Failed to serialize params for notification '{}' for server '{}': {err}",
R::METHOD,
self.name,
);
return;
}
};
let notification = jsonrpc::Notification {
jsonrpc: Some(jsonrpc::Version::V2),
method: R::METHOD.to_string(),
params: Self::value_into_params(params),
};
let notification = jsonrpc::Notification {
jsonrpc: Some(jsonrpc::Version::V2),
method: R::METHOD.to_string(),
params: Self::value_into_params(params),
};
server_tx
.send(Payload::Notification(notification))
.map_err(|e| Error::Other(e.into()))?;
Ok(())
if let Err(err) = server_tx.send(Payload::Notification(notification)) {
log::error!(
"Failed to send notification '{}' to server '{}': {err}",
R::METHOD,
self.name
);
}
}
@@ -487,31 +521,29 @@ impl Client {
&self,
id: jsonrpc::Id,
result: core::result::Result<Value, jsonrpc::Error>,
) -> impl Future<Output = Result<()>> {
) -> Result<()> {
use jsonrpc::{Failure, Output, Success, Version};
let server_tx = self.server_tx.clone();
async move {
let output = match result {
Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2),
id,
result: serde_json::to_value(result)?,
}),
Err(error) => Output::Failure(Failure {
jsonrpc: Some(Version::V2),
id,
error,
}),
};
let output = match result {
Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2),
id,
result,
}),
Err(error) => Output::Failure(Failure {
jsonrpc: Some(Version::V2),
id,
error,
}),
};
server_tx
.send(Payload::Response(output))
.map_err(|e| Error::Other(e.into()))?;
server_tx
.send(Payload::Response(output))
.map_err(|e| Error::Other(e.into()))?;
Ok(())
}
Ok(())
}
// -------------------------------------------------------------------------------------------
@@ -686,14 +718,14 @@ impl Client {
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
};
self.request::<lsp::request::Initialize>(params).await
self.call::<lsp::request::Initialize>(params).await
}
pub async fn shutdown(&self) -> Result<()> {
self.request::<lsp::request::Shutdown>(()).await
self.call::<lsp::request::Shutdown>(()).await
}
pub fn exit(&self) -> impl Future<Output = Result<()>> {
pub fn exit(&self) {
self.notify::<lsp::notification::Exit>(())
}
@@ -701,7 +733,8 @@ impl Client {
/// early if server responds with an error.
pub async fn shutdown_and_exit(&self) -> Result<()> {
self.shutdown().await?;
self.exit().await
self.exit();
Ok(())
}
/// Forcefully shuts down the language server ignoring any errors.
@@ -709,24 +742,21 @@ impl Client {
if let Err(e) = self.shutdown().await {
log::warn!("language server failed to terminate gracefully - {}", e);
}
self.exit().await
self.exit();
Ok(())
}
// -------------------------------------------------------------------------------------------
// Workspace
// -------------------------------------------------------------------------------------------
pub fn did_change_configuration(&self, settings: Value) -> impl Future<Output = Result<()>> {
pub fn did_change_configuration(&self, settings: Value) {
self.notify::<lsp::notification::DidChangeConfiguration>(
lsp::DidChangeConfigurationParams { settings },
)
}
pub fn did_change_workspace(
&self,
added: Vec<WorkspaceFolder>,
removed: Vec<WorkspaceFolder>,
) -> impl Future<Output = Result<()>> {
pub fn did_change_workspace(&self, added: Vec<WorkspaceFolder>, removed: Vec<WorkspaceFolder>) {
self.notify::<DidChangeWorkspaceFolders>(DidChangeWorkspaceFoldersParams {
event: WorkspaceFoldersChangeEvent { added, removed },
})
@@ -737,7 +767,7 @@ impl Client {
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceEdit>>>> {
let capabilities = self.file_operations_intests();
if !capabilities.will_rename.has_interest(old_path, is_dir) {
return None;
@@ -754,24 +784,13 @@ impl Client {
old_uri: url_from_path(old_path)?,
new_uri: url_from_path(new_path)?,
}];
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
Some(self.call_with_timeout::<lsp::request::WillRenameFiles>(
&lsp::RenameFilesParams { files },
5,
);
Some(async move {
let json = request.await?;
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
))
}
pub fn did_rename(
&self,
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
pub fn did_rename(&self, old_path: &Path, new_path: &Path, is_dir: bool) -> Option<()> {
let capabilities = self.file_operations_intests();
if !capabilities.did_rename.has_interest(new_path, is_dir) {
return None;
@@ -789,7 +808,8 @@ impl Client {
old_uri: url_from_path(old_path)?,
new_uri: url_from_path(new_path)?,
}];
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files });
Some(())
}
// -------------------------------------------------------------------------------------------
@@ -802,7 +822,7 @@ impl Client {
version: i32,
doc: &Rope,
language_id: String,
) -> impl Future<Output = Result<()>> {
) {
self.notify::<lsp::notification::DidOpenTextDocument>(lsp::DidOpenTextDocumentParams {
text_document: lsp::TextDocumentItem {
uri,
@@ -929,7 +949,7 @@ impl Client {
old_text: &Rope,
new_text: &Rope,
changes: &ChangeSet,
) -> Option<impl Future<Output = Result<()>>> {
) -> Option<()> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document sync.
@@ -961,18 +981,14 @@ impl Client {
kind => unimplemented!("{:?}", kind),
};
Some(self.notify::<lsp::notification::DidChangeTextDocument>(
lsp::DidChangeTextDocumentParams {
text_document,
content_changes: changes,
},
))
self.notify::<lsp::notification::DidChangeTextDocument>(lsp::DidChangeTextDocumentParams {
text_document,
content_changes: changes,
});
Some(())
}
pub fn text_document_did_close(
&self,
text_document: lsp::TextDocumentIdentifier,
) -> impl Future<Output = Result<()>> {
pub fn text_document_did_close(&self, text_document: lsp::TextDocumentIdentifier) {
self.notify::<lsp::notification::DidCloseTextDocument>(lsp::DidCloseTextDocumentParams {
text_document,
})
@@ -984,7 +1000,7 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
text: &Rope,
) -> Option<impl Future<Output = Result<()>>> {
) -> Option<()> {
let capabilities = self.capabilities.get().unwrap();
let include_text = match &capabilities.text_document_sync.as_ref()? {
@@ -1002,12 +1018,11 @@ impl Client {
lsp::TextDocumentSyncCapability::Kind(..) => false,
};
Some(self.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document,
text: include_text.then_some(text.into()),
},
))
self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams {
text_document,
text: include_text.then_some(text.into()),
});
Some(())
}
pub fn completion(
@@ -1016,7 +1031,7 @@ impl Client {
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
context: lsp::CompletionContext,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::CompletionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support completion.
@@ -1042,14 +1057,13 @@ impl Client {
&self,
completion_item: &lsp::CompletionItem,
) -> impl Future<Output = Result<lsp::CompletionItem>> {
let res = self.call_with_ref::<lsp::request::ResolveCompletionItem>(completion_item);
async move { Ok(serde_json::from_value(res.await?)?) }
self.call_with_ref::<lsp::request::ResolveCompletionItem>(completion_item)
}
pub fn resolve_code_action(
&self,
code_action: lsp::CodeAction,
) -> Option<impl Future<Output = Result<Value>>> {
code_action: &lsp::CodeAction,
) -> Option<impl Future<Output = Result<lsp::CodeAction>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support resolving code actions.
@@ -1061,7 +1075,7 @@ impl Client {
_ => return None,
}
Some(self.call::<lsp::request::CodeActionResolveRequest>(code_action))
Some(self.call_with_ref::<lsp::request::CodeActionResolveRequest>(code_action))
}
pub fn text_document_signature_help(
@@ -1085,8 +1099,7 @@ impl Client {
// lsp::SignatureHelpContext
};
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
Some(async move { Ok(serde_json::from_value(res.await?)?) })
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
}
pub fn text_document_range_inlay_hints(
@@ -1094,7 +1107,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::InlayHint>>>>> {
let capabilities = self.capabilities.get().unwrap();
match capabilities.inlay_hint_provider {
@@ -1114,12 +1127,31 @@ impl Client {
Some(self.call::<lsp::request::InlayHintRequest>(params))
}
pub fn text_document_document_color(
&self,
text_document: lsp::TextDocumentIdentifier,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Vec<lsp::ColorInformation>>>> {
self.capabilities.get().unwrap().color_provider.as_ref()?;
let params = lsp::DocumentColorParams {
text_document,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: work_done_token.clone(),
},
partial_result_params: helix_lsp_types::PartialResultParams {
partial_result_token: work_done_token,
},
};
Some(self.call::<lsp::request::DocumentColor>(params))
}
pub fn text_document_hover(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::Hover>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support hover.
@@ -1150,7 +1182,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::TextEdit>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support formatting.
@@ -1159,23 +1191,7 @@ impl Client {
_ => return None,
};
// merge FormattingOptions with 'config.format'
let config_format = self
.config
.as_ref()
.and_then(|cfg| cfg.get("format"))
.and_then(|fmt| HashMap::<String, lsp::FormattingProperty>::deserialize(fmt).ok());
let options = if let Some(mut properties) = config_format {
// passed in options take precedence over 'config.format'
properties.extend(options.properties);
lsp::FormattingOptions {
properties,
..options
}
} else {
options
};
let options = self.get_merged_formatting_options(options);
let params = lsp::DocumentFormattingParams {
text_document,
@@ -1183,13 +1199,7 @@ impl Client {
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let request = self.call::<lsp::request::Formatting>(params);
Some(async move {
let json = request.await?;
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
Some(self.call::<lsp::request::Formatting>(params))
}
pub fn text_document_range_formatting(
@@ -1198,7 +1208,7 @@ impl Client {
range: lsp::Range,
options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::TextEdit>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support range formatting.
@@ -1207,6 +1217,8 @@ impl Client {
_ => return None,
};
let options = self.get_merged_formatting_options(options);
let params = lsp::DocumentRangeFormattingParams {
text_document,
range,
@@ -1214,13 +1226,7 @@ impl Client {
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let request = self.call::<lsp::request::RangeFormatting>(params);
Some(async move {
let json = request.await?;
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
Some(self.call::<lsp::request::RangeFormatting>(params))
}
pub fn text_document_document_highlight(
@@ -1228,7 +1234,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::DocumentHighlight>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document highlight.
@@ -1261,7 +1267,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
) -> impl Future<Output = Result<T::Result>> {
let params = lsp::GotoDefinitionParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
@@ -1281,7 +1287,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-definition.
@@ -1302,7 +1308,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-declaration.
@@ -1327,7 +1333,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-type-definition.
@@ -1351,7 +1357,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::GotoDefinitionResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-definition.
@@ -1376,7 +1382,7 @@ impl Client {
position: lsp::Position,
include_declaration: bool,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::Location>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support goto-reference.
@@ -1405,7 +1411,7 @@ impl Client {
pub fn document_symbols(
&self,
text_document: lsp::TextDocumentIdentifier,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::DocumentSymbolResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support document symbols.
@@ -1427,7 +1433,7 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<lsp::PrepareRenameResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
match capabilities.rename_provider {
@@ -1447,7 +1453,10 @@ impl Client {
}
// empty string to get all symbols
pub fn workspace_symbols(&self, query: String) -> Option<impl Future<Output = Result<Value>>> {
pub fn workspace_symbols(
&self,
query: String,
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceSymbolResponse>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support workspace symbols.
@@ -1470,7 +1479,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
context: lsp::CodeActionContext,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<Vec<lsp::CodeActionOrCommand>>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support code actions.
@@ -1498,7 +1507,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
new_name: String,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
) -> Option<impl Future<Output = Result<Option<lsp::WorkspaceEdit>>>> {
if !self.supports_feature(LanguageServerFeature::RenameSymbol) {
return None;
}
@@ -1514,16 +1523,13 @@ impl Client {
},
};
let request = self.call::<lsp::request::Rename>(params);
Some(async move {
let json = request.await?;
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
Some(self.call::<lsp::request::Rename>(params))
}
pub fn command(&self, command: lsp::Command) -> Option<impl Future<Output = Result<Value>>> {
pub fn command(
&self,
command: lsp::Command,
) -> Option<impl Future<Output = Result<Option<Value>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the language server does not support executing commands.
@@ -1540,10 +1546,7 @@ 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>> {
pub fn did_change_watched_files(&self, changes: Vec<lsp::FileEvent>) {
self.notify::<lsp::notification::DidChangeWatchedFiles>(lsp::DidChangeWatchedFilesParams {
changes,
})

View File

@@ -113,17 +113,13 @@ impl Handler {
"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}");
}
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,
}]);
true
});
}

View File

@@ -12,7 +12,7 @@ pub use jsonrpc::Call;
pub use lsp::{Position, Url};
use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::{
use helix_core::syntax::config::{
LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures,
};
use helix_stdx::path;
@@ -618,51 +618,45 @@ impl Registry {
Ok(self.inner[id].clone())
}
/// 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.
/// If this method is called, all documents that have a reference to the language server have to refresh their language servers,
/// See helix_view::editor::Editor::refresh_language_servers
pub fn restart(
pub fn restart_server(
&mut self,
name: &str,
language_config: &LanguageConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Vec<Arc<Client>>> {
language_config
.language_servers
.iter()
.filter_map(|LanguageServerFeatures { name, .. }| {
if let Some(old_clients) = self.inner_by_name.remove(name) {
if old_clients.is_empty() {
log::info!("restarting client for '{name}' which was manually stopped");
} else {
log::info!("stopping existing clients for '{name}'");
}
for old_client in old_clients {
self.file_event_handler.remove_client(old_client.id());
self.inner.remove(old_client.id());
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
}
}
let client = match self.start_client(
name.clone(),
language_config,
doc_path,
root_dirs,
enable_snippets,
) {
Ok(client) => client,
Err(StartupError::NoRequiredRootFound) => return None,
Err(StartupError::Error(err)) => return Some(Err(err)),
};
self.inner_by_name
.insert(name.to_owned(), vec![client.clone()]);
) -> Option<Result<Arc<Client>>> {
if let Some(old_clients) = self.inner_by_name.remove(name) {
if old_clients.is_empty() {
log::info!("restarting client for '{name}' which was manually stopped");
} else {
log::info!("stopping existing clients for '{name}'");
}
for old_client in old_clients {
self.file_event_handler.remove_client(old_client.id());
self.inner.remove(old_client.id());
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
}
}
let client = match self.start_client(
name.to_string(),
language_config,
doc_path,
root_dirs,
enable_snippets,
) {
Ok(client) => client,
Err(StartupError::NoRequiredRootFound) => return None,
Err(StartupError::Error(err)) => return Some(Err(err)),
};
self.inner_by_name
.insert(name.to_owned(), vec![client.clone()]);
Some(Ok(client))
})
.collect()
Some(Ok(client))
}
pub fn stop(&mut self, name: &str) {
@@ -739,14 +733,17 @@ impl Registry {
#[derive(Debug)]
pub enum ProgressStatus {
Created,
Started(lsp::WorkDoneProgress),
Started {
title: String,
progress: lsp::WorkDoneProgress,
},
}
impl ProgressStatus {
pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
match &self {
ProgressStatus::Created => None,
ProgressStatus::Started(progress) => Some(progress),
ProgressStatus::Started { title: _, progress } => Some(progress),
}
}
}
@@ -783,6 +780,13 @@ impl LspProgressMap {
self.0.get(&id).and_then(|values| values.get(token))
}
pub fn title(&self, id: LanguageServerId, token: &lsp::ProgressToken) -> Option<&String> {
self.progress(id, token).and_then(|p| match p {
ProgressStatus::Created => None,
ProgressStatus::Started { title, .. } => Some(title),
})
}
/// Checks if progress `token` for server with `id` is created.
pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool {
self.0
@@ -807,17 +811,39 @@ impl LspProgressMap {
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
}
/// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`.
/// Updates the progress of `token` for server with `id` to begin state `status`
pub fn begin(
&mut self,
id: LanguageServerId,
token: lsp::ProgressToken,
status: lsp::WorkDoneProgressBegin,
) {
self.0.entry(id).or_default().insert(
token,
ProgressStatus::Started {
title: status.title.clone(),
progress: lsp::WorkDoneProgress::Begin(status),
},
);
}
/// Updates the progress of `token` for server with `id` to report state `status`.
pub fn update(
&mut self,
id: LanguageServerId,
token: lsp::ProgressToken,
status: lsp::WorkDoneProgress,
) -> Option<ProgressStatus> {
status: lsp::WorkDoneProgressReport,
) {
self.0
.entry(id)
.or_default()
.insert(token, ProgressStatus::Started(status))
.entry(token)
.and_modify(|e| match e {
ProgressStatus::Created => (),
ProgressStatus::Started { progress, .. } => {
*progress = lsp::WorkDoneProgress::Report(status)
}
});
}
}
@@ -877,7 +903,7 @@ fn start_client(
&ls_config.command,
&ls_config.args,
ls_config.config.clone(),
ls_config.environment.clone(),
&ls_config.environment,
root_path,
root_uri,
id,
@@ -906,17 +932,7 @@ fn start_client(
}
// next up, notify<initialized>
let notification_result = _client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await;
if let Err(e) = notification_result {
log::error!(
"failed to notify language server of its initialization: {}",
e
);
return;
}
_client.notify::<lsp::notification::Initialized>(lsp::InitializedParams {});
initialize_notify.notify_one();
});
@@ -1048,7 +1064,8 @@ mod tests {
let mut source = Rope::from_str("[\n\"🇺🇸\",\n\"🎄\",\n]");
let transaction = generate_transaction_from_edits(&source, edits, OffsetEncoding::Utf8);
let transaction = generate_transaction_from_edits(&source, edits, OffsetEncoding::Utf16);
assert!(transaction.apply(&mut source));
assert_eq!(source, "[\n \"🇺🇸\",\n \"🎄\",\n]");
}
}

View File

@@ -90,11 +90,12 @@ impl Transport {
async fn recv_server_message(
reader: &mut (impl AsyncBufRead + Unpin + Send),
buffer: &mut String,
content: &mut Vec<u8>,
language_server_name: &str,
) -> Result<ServerMessage> {
let mut content_length = None;
loop {
buffer.truncate(0);
buffer.clear();
if reader.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed);
};
@@ -126,17 +127,17 @@ impl Transport {
}
let content_length = content_length.context("missing content length")?;
//TODO: reuse vector
let mut content = vec![0; content_length];
reader.read_exact(&mut content).await?;
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
content.resize(content_length, 0);
reader.read_exact(content).await?;
let msg = std::str::from_utf8(content).context("invalid utf8 from server")?;
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);
content.clear();
Ok(output?)
}
@@ -223,10 +224,7 @@ impl Transport {
language_server_name: &str,
) -> Result<()> {
let (id, result) = match output {
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
info!("{language_server_name} <- {}", result);
(id, Ok(result))
}
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => (id, Ok(result)),
jsonrpc::Output::Failure(jsonrpc::Failure { id, error, .. }) => {
error!("{language_server_name} <- {error}");
(id, Err(error.into()))
@@ -258,9 +256,15 @@ impl Transport {
client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>,
) {
let mut recv_buffer = String::new();
let mut content_buffer = Vec::new();
loop {
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer, &transport.name)
.await
match Self::recv_server_message(
&mut server_stdout,
&mut recv_buffer,
&mut content_buffer,
&transport.name,
)
.await
{
Ok(msg) => {
match transport

View File

@@ -13,19 +13,20 @@ homepage.workspace = true
[dependencies]
dunce = "1.0"
etcetera = "0.8"
ropey = { version = "1.6.1", default-features = false }
which = "7.0"
regex-cursor = "0.1.4"
etcetera = "0.10"
ropey.workspace = true
which = "8.0"
regex-cursor = "0.1.5"
bitflags.workspace = true
once_cell = "1.19"
once_cell = "1.21"
regex-automata = "0.4.9"
unicode-segmentation.workspace = true
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.59", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
windows-sys = { version = "0.60", features = ["Win32_Foundation", "Win32_Security", "Win32_Security_Authorization", "Win32_Storage_FileSystem", "Win32_System_Threading"] }
[target.'cfg(unix)'.dependencies]
rustix = { version = "0.38", features = ["fs"] }
rustix = { version = "1.0", features = ["fs"] }
[dev-dependencies]
tempfile.workspace = true

View File

@@ -1,3 +1,4 @@
//! Functions for working with the host environment.
use std::{
borrow::Cow,
ffi::{OsStr, OsString},
@@ -10,9 +11,9 @@ use once_cell::sync::Lazy;
// We keep the CWD as a static so that we can access it in places where we don't have access to the Editor
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
// 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.
/// 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();
@@ -37,6 +38,7 @@ pub fn current_working_dir() -> PathBuf {
cwd
}
/// Update the current working directory.
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<Option<PathBuf>> {
let path = crate::path::canonicalize(path);
std::env::set_current_dir(&path)?;
@@ -45,14 +47,17 @@ pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<Option
Ok(cwd.replace(path))
}
/// Checks if the given environment variable is set.
pub fn env_var_is_set(env_var_name: &str) -> bool {
std::env::var_os(env_var_name).is_some()
}
/// Checks if a binary with the given name exists.
pub fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
which::which(binary_name).is_ok()
}
/// Attempts to find a binary of the given name. See [which](https://linux.die.net/man/1/which).
pub fn which<T: AsRef<OsStr>>(
binary_name: T,
) -> Result<std::path::PathBuf, ExecutableNotFoundError> {

View File

@@ -1,3 +1,4 @@
//! Functions for managine file metadata.
//! From <https://github.com/Freaky/faccess>
use std::io;
@@ -51,8 +52,8 @@ mod imp {
}
fn chown(p: &Path, uid: Option<u32>, gid: Option<u32>) -> io::Result<()> {
let uid = uid.map(|n| unsafe { rustix::fs::Uid::from_raw(n) });
let gid = gid.map(|n| unsafe { rustix::fs::Gid::from_raw(n) });
let uid = uid.map(rustix::fs::Uid::from_raw);
let gid = gid.map(rustix::fs::Gid::from_raw);
rustix::fs::chown(p, uid, gid)?;
Ok(())
}

View File

@@ -1,3 +1,6 @@
//! Extensions to the standard library. A collection of helper functions
//! used throughout helix.
pub mod env;
pub mod faccess;
pub mod path;

View File

@@ -1,3 +1,5 @@
//! Functions for working with [Path].
pub use etcetera::home_dir;
use once_cell::sync::Lazy;
use regex_cursor::{engines::meta::Regex, Input};
@@ -140,6 +142,7 @@ pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
normalize(path)
}
/// Convert path into a relative path
pub fn get_relative_path<'a, P>(path: P) -> Cow<'a, Path>
where
P: Into<Cow<'a, Path>>,

View File

@@ -1,3 +1,5 @@
//! Provides [Range] type expanding on [RangeBounds].
use std::ops::{self, RangeBounds};
/// A range of `char`s within the text.
@@ -66,6 +68,7 @@ pub fn is_subset<const ALLOW_EMPTY: bool>(
}
}
/// Similar to is_subset but requires each element of `super_set` to be matched
pub fn is_exact_subset(
mut super_set: impl Iterator<Item = Range>,
mut sub_set: impl Iterator<Item = Range>,

View File

@@ -1,11 +1,15 @@
//! Functions and types for working with [RopeSlice]
use std::fmt;
use std::ops::{Bound, RangeBounds};
pub use regex_cursor::engines::meta::{Builder as RegexBuilder, Regex};
pub use regex_cursor::regex_automata::util::syntax::Config;
use regex_cursor::{Input as RegexInput, RopeyCursor};
use ropey::str_utils::byte_to_char_idx;
use ropey::iter::Chunks;
use ropey::RopeSlice;
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
/// Additional utility functions for [RopeSlice]
pub trait RopeSliceExt<'a>: Sized {
fn ends_with(self, text: &str) -> bool;
fn starts_with(self, text: &str) -> bool;
@@ -17,23 +21,260 @@ pub trait RopeSliceExt<'a>: Sized {
fn regex_input_at<R: RangeBounds<usize>>(self, char_range: R) -> RegexInput<RopeyCursor<'a>>;
fn first_non_whitespace_char(self) -> Option<usize>;
fn last_non_whitespace_char(self) -> Option<usize>;
/// returns the char idx of `byte_idx`, if `byte_idx` is a char boundary
/// this function behaves the same as `byte_to_char` but if `byte_idx` is
/// not a valid char boundary (so within a char) this will return the next
/// char index.
/// Finds the closest byte index not exceeding `byte_idx` which lies on a character boundary.
///
/// If `byte_idx` already lies on a character boundary then it is returned as-is. When
/// `byte_idx` lies between two character boundaries, this function returns the byte index of
/// the lesser / earlier / left-hand-side boundary.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("😆");
/// for i in 1..text.len_bytes() {
/// assert_eq!(text.byte_to_char(i), 0);
/// assert_eq!(text.byte_to_next_char(i), 1);
/// }
/// let text = RopeSlice::from(""); // three bytes: e2 8c 9a
/// assert_eq!(text.floor_char_boundary(0), 0);
/// assert_eq!(text.floor_char_boundary(1), 0);
/// assert_eq!(text.floor_char_boundary(2), 0);
/// assert_eq!(text.floor_char_boundary(3), 3);
/// ```
fn byte_to_next_char(self, byte_idx: usize) -> usize;
fn floor_char_boundary(self, byte_idx: usize) -> usize;
/// Finds the closest byte index not below `byte_idx` which lies on a character boundary.
///
/// If `byte_idx` already lies on a character boundary then it is returned as-is. When
/// `byte_idx` lies between two character boundaries, this function returns the byte index of
/// the greater / later / right-hand-side boundary.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("⌚"); // three bytes: e2 8c 9a
/// assert_eq!(text.ceil_char_boundary(0), 0);
/// assert_eq!(text.ceil_char_boundary(1), 3);
/// assert_eq!(text.ceil_char_boundary(2), 3);
/// assert_eq!(text.ceil_char_boundary(3), 3);
/// ```
fn ceil_char_boundary(self, byte_idx: usize) -> usize;
/// Checks whether the given `byte_idx` lies on a character boundary.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("⌚"); // three bytes: e2 8c 9a
/// assert!(text.is_char_boundary(0));
/// assert!(!text.is_char_boundary(1));
/// assert!(!text.is_char_boundary(2));
/// assert!(text.is_char_boundary(3));
/// ```
#[allow(clippy::wrong_self_convention)]
fn is_char_boundary(self, byte_idx: usize) -> bool;
/// Finds the closest byte index not exceeding `byte_idx` which lies on a grapheme cluster
/// boundary.
///
/// If `byte_idx` already lies on a grapheme cluster boundary then it is returned as-is. When
/// `byte_idx` lies between two grapheme cluster boundaries, this function returns the byte
/// index of the lesser / earlier / left-hand-side boundary.
///
/// `byte_idx` does not need to be aligned to a character boundary.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("\r\n"); // U+000D U+000A, hex: 0d 0a
/// assert_eq!(text.floor_grapheme_boundary(0), 0);
/// assert_eq!(text.floor_grapheme_boundary(1), 0);
/// assert_eq!(text.floor_grapheme_boundary(2), 2);
/// ```
fn floor_grapheme_boundary(self, byte_idx: usize) -> usize;
/// Finds the closest byte index not exceeding `byte_idx` which lies on a grapheme cluster
/// boundary.
///
/// If `byte_idx` already lies on a grapheme cluster boundary then it is returned as-is. When
/// `byte_idx` lies between two grapheme cluster boundaries, this function returns the byte
/// index of the greater / later / right-hand-side boundary.
///
/// `byte_idx` does not need to be aligned to a character boundary.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("\r\n"); // U+000D U+000A, hex: 0d 0a
/// assert_eq!(text.ceil_grapheme_boundary(0), 0);
/// assert_eq!(text.ceil_grapheme_boundary(1), 2);
/// assert_eq!(text.ceil_grapheme_boundary(2), 2);
/// ```
fn ceil_grapheme_boundary(self, byte_idx: usize) -> usize;
/// Checks whether the `byte_idx` lies on a grapheme cluster boundary.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("\r\n"); // U+000D U+000A, hex: 0d 0a
/// assert!(text.is_grapheme_boundary(0));
/// assert!(!text.is_grapheme_boundary(1));
/// assert!(text.is_grapheme_boundary(2));
/// ```
#[allow(clippy::wrong_self_convention)]
fn is_grapheme_boundary(self, byte_idx: usize) -> bool;
/// Returns an iterator over the grapheme clusters in the slice.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("😶‍🌫️🏴‍☠️🖼️");
/// let graphemes: Vec<_> = text.graphemes().collect();
/// assert_eq!(graphemes.as_slice(), &["😶‍🌫️", "🏴‍☠️", "🖼️"]);
/// ```
fn graphemes(self) -> RopeGraphemes<'a> {
self.graphemes_at(0)
}
/// Returns an iterator over the grapheme clusters in the slice, reversed.
///
/// The returned iterator starts at the end of the slice and ends at the beginning of the
/// slice.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("😶‍🌫️🏴‍☠️🖼️");
/// let graphemes: Vec<_> = text.graphemes_rev().collect();
/// assert_eq!(graphemes.as_slice(), &["🖼️", "🏴‍☠️", "😶‍🌫️"]);
/// ```
fn graphemes_rev(self) -> RopeGraphemes<'a>;
/// Returns an iterator over the grapheme clusters in the slice at the given byte index.
///
/// # Example
///
/// ```
/// # use ropey::Rope;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = Rope::from_str("😶‍🌫️🏴‍☠️🖼️");
/// // 14 is the byte index of the pirate flag's starting cluster boundary.
/// let graphemes: Vec<_> = text.slice(..).graphemes_at(14).collect();
/// assert_eq!(graphemes.as_slice(), &["🏴‍☠️", "🖼️"]);
/// // 27 is the byte index of the pirate flag's ending cluster boundary.
/// let graphemes: Vec<_> = text.slice(..).graphemes_at(27).reversed().collect();
/// assert_eq!(graphemes.as_slice(), &["🏴‍☠️", "😶‍🌫️"]);
/// ```
fn graphemes_at(self, byte_idx: usize) -> RopeGraphemes<'a>;
/// Returns an iterator over the grapheme clusters in a rope and the byte index where each
/// grapheme cluster starts.
///
/// # Example
///
/// ```
/// # use ropey::Rope;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = Rope::from_str("😶‍🌫️🏴‍☠️🖼️");
/// let slice = text.slice(..);
/// let graphemes: Vec<_> = slice.grapheme_indices_at(0).collect();
/// assert_eq!(
/// graphemes.as_slice(),
/// &[(0, "😶‍🌫️".into()), (14, "🏴‍☠️".into()), (27, "🖼️".into())]
/// );
/// let graphemes: Vec<_> = slice.grapheme_indices_at(slice.len_bytes()).reversed().collect();
/// assert_eq!(
/// graphemes.as_slice(),
/// &[(27, "🖼️".into()), (14, "🏴‍☠️".into()), (0, "😶‍🌫️".into())]
/// );
/// ```
fn grapheme_indices_at(self, byte_idx: usize) -> RopeGraphemeIndices<'a>;
/// Finds the byte index of the next grapheme boundary after `byte_idx`.
///
/// If the byte index lies on the last grapheme cluster in the slice then this function
/// returns `RopeSlice::len_bytes`.
///
/// # Example
///
/// ```
/// # use ropey::Rope;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = Rope::from_str("😶‍🌫️🏴‍☠️🖼️");
/// let slice = text.slice(..);
/// let mut byte_idx = 0;
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("😶‍🌫️".into()));
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("🏴‍☠️".into()));
///
/// // If `byte_idx` does not lie on a character or grapheme boundary then this function is
/// // functionally the same as `ceil_grapheme_boundary`.
/// assert_eq!(slice.next_grapheme_boundary(byte_idx - 1), byte_idx);
/// assert_eq!(slice.next_grapheme_boundary(byte_idx - 2), byte_idx);
/// assert_eq!(slice.next_grapheme_boundary(byte_idx + 1), slice.next_grapheme_boundary(byte_idx));
/// assert_eq!(slice.next_grapheme_boundary(byte_idx + 2), slice.next_grapheme_boundary(byte_idx));
///
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).next(), Some("🖼️".into()));
/// byte_idx = slice.next_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).next(), None);
/// assert_eq!(byte_idx, slice.len_bytes());
/// ```
fn next_grapheme_boundary(self, byte_idx: usize) -> usize {
self.nth_next_grapheme_boundary(byte_idx, 1)
}
/// Finds the byte index of the `n`th grapheme cluster after the given `byte_idx`.
///
/// If there are fewer than `n` grapheme clusters after `byte_idx` in the rope then this
/// function returns `RopeSlice::len_bytes`.
///
/// This is functionally equivalent to calling `next_grapheme_boundary` `n` times but is more
/// efficient.
fn nth_next_grapheme_boundary(self, byte_idx: usize, n: usize) -> usize;
/// Finds the byte index of the previous grapheme boundary before `byte_idx`.
///
/// If the byte index lies on the first grapheme cluster in the slice then this function
/// returns zero.
///
/// # Example
///
/// ```
/// # use ropey::Rope;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = Rope::from_str("😶‍🌫️🏴‍☠️🖼️");
/// let slice = text.slice(..);
/// let mut byte_idx = text.len_bytes();
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("🖼️".into()));
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("🏴‍☠️".into()));
///
/// // If `byte_idx` does not lie on a character or grapheme boundary then this function is
/// // functionally the same as `floor_grapheme_boundary`.
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx + 1), byte_idx);
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx + 2), byte_idx);
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx - 1), slice.prev_grapheme_boundary(byte_idx));
/// assert_eq!(slice.prev_grapheme_boundary(byte_idx - 2), slice.prev_grapheme_boundary(byte_idx));
///
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), Some("😶‍🌫️".into()));
/// byte_idx = slice.prev_grapheme_boundary(byte_idx);
/// assert_eq!(slice.graphemes_at(byte_idx).prev(), None);
/// assert_eq!(byte_idx, 0);
/// ```
fn prev_grapheme_boundary(self, byte_idx: usize) -> usize {
self.nth_prev_grapheme_boundary(byte_idx, 1)
}
/// Finds the byte index of the `n`th grapheme cluster before the given `byte_idx`.
///
/// If there are fewer than `n` grapheme clusters before `byte_idx` in the rope then this
/// function returns zero.
///
/// This is functionally equivalent to calling `prev_grapheme_boundary` `n` times but is more
/// efficient.
fn nth_prev_grapheme_boundary(self, byte_idx: usize, n: usize) -> usize;
}
impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
@@ -43,7 +284,7 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
return false;
}
self.get_byte_slice(len - text.len()..)
.map_or(false, |end| end == text)
.is_some_and(|end| end == text)
}
fn starts_with(self, text: &str) -> bool {
@@ -52,7 +293,7 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
return false;
}
self.get_byte_slice(..text.len())
.map_or(false, |start| start == text)
.is_some_and(|start| start == text)
}
fn regex_input(self) -> RegexInput<RopeyCursor<'a>> {
@@ -94,14 +335,234 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
.map(|pos| self.len_chars() - pos - 1)
}
/// returns the char idx of `byte_idx`, if `byte_idx` is
/// a char boundary this function behaves the same as `byte_to_char`
fn byte_to_next_char(self, mut byte_idx: usize) -> usize {
let (chunk, chunk_byte_off, chunk_char_off, _) = self.chunk_at_byte(byte_idx);
byte_idx -= chunk_byte_off;
let is_char_boundary =
is_utf8_char_boundary(chunk.as_bytes().get(byte_idx).copied().unwrap_or(0));
chunk_char_off + byte_to_char_idx(chunk, byte_idx) + !is_char_boundary as usize
// These three are adapted from std:
fn floor_char_boundary(self, byte_idx: usize) -> usize {
if byte_idx >= self.len_bytes() {
self.len_bytes()
} else {
let offset = self
.bytes_at(byte_idx + 1)
.reversed()
.take(4)
.position(is_utf8_char_boundary)
// A char can only be four bytes long so we are guaranteed to find a boundary.
.unwrap();
byte_idx - offset
}
}
fn ceil_char_boundary(self, byte_idx: usize) -> usize {
if byte_idx > self.len_bytes() {
self.len_bytes()
} else {
let upper_bound = self.len_bytes().min(byte_idx + 4);
self.bytes_at(byte_idx)
.position(is_utf8_char_boundary)
.map_or(upper_bound, |pos| pos + byte_idx)
}
}
fn is_char_boundary(self, byte_idx: usize) -> bool {
if byte_idx == 0 {
return true;
}
if byte_idx >= self.len_bytes() {
byte_idx == self.len_bytes()
} else {
is_utf8_char_boundary(self.bytes_at(byte_idx).next().unwrap())
}
}
fn floor_grapheme_boundary(self, mut byte_idx: usize) -> usize {
if byte_idx >= self.len_bytes() {
return self.len_bytes();
}
byte_idx = self.ceil_char_boundary(byte_idx + 1);
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
let mut cursor = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
loop {
match cursor.prev_boundary(chunk, chunk_byte_idx) {
Ok(None) => return 0,
Ok(Some(boundary)) => return boundary,
Err(GraphemeIncomplete::PrevChunk) => {
let (ch, ch_byte_idx, _, _) = self.chunk_at_byte(chunk_byte_idx - 1);
chunk = ch;
chunk_byte_idx = ch_byte_idx;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = self.chunk_at_byte(n - 1).0;
cursor.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
fn ceil_grapheme_boundary(self, mut byte_idx: usize) -> usize {
if byte_idx >= self.len_bytes() {
return self.len_bytes();
}
if byte_idx == 0 {
return 0;
}
byte_idx = self.floor_char_boundary(byte_idx - 1);
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
let mut cursor = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
loop {
match cursor.next_boundary(chunk, chunk_byte_idx) {
Ok(None) => return self.len_bytes(),
Ok(Some(boundary)) => return boundary,
Err(GraphemeIncomplete::NextChunk) => {
chunk_byte_idx += chunk.len();
chunk = self.chunk_at_byte(chunk_byte_idx).0;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = self.chunk_at_byte(n - 1).0;
cursor.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
fn is_grapheme_boundary(self, byte_idx: usize) -> bool {
// The byte must lie on a character boundary to lie on a grapheme cluster boundary.
if !self.is_char_boundary(byte_idx) {
return false;
}
let (chunk, chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
let mut cursor = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
loop {
match cursor.is_boundary(chunk, chunk_byte_idx) {
Ok(n) => return n,
Err(GraphemeIncomplete::PreContext(n)) => {
let (ctx_chunk, ctx_byte_start, _, _) = self.chunk_at_byte(n - 1);
cursor.provide_context(ctx_chunk, ctx_byte_start);
}
Err(_) => unreachable!(),
}
}
}
fn graphemes_rev(self) -> RopeGraphemes<'a> {
self.graphemes_at(self.len_bytes()).reversed()
}
fn graphemes_at(self, byte_idx: usize) -> RopeGraphemes<'a> {
// Bounds check
assert!(byte_idx <= self.len_bytes());
let (mut chunks, chunk_byte_idx, _, _) = self.chunks_at_byte(byte_idx);
let current_chunk = chunks.next().unwrap_or("");
RopeGraphemes {
text: self,
chunks,
current_chunk,
chunk_byte_idx,
cursor: GraphemeCursor::new(byte_idx, self.len_bytes(), true),
is_reversed: false,
}
}
fn grapheme_indices_at(self, byte_idx: usize) -> RopeGraphemeIndices<'a> {
// Bounds check
assert!(byte_idx <= self.len_bytes());
RopeGraphemeIndices {
front_offset: byte_idx,
iter: self.graphemes_at(byte_idx),
is_reversed: false,
}
}
fn nth_next_grapheme_boundary(self, mut byte_idx: usize, n: usize) -> usize {
// Bounds check
assert!(byte_idx <= self.len_bytes());
byte_idx = self.floor_char_boundary(byte_idx);
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
// Find the nth next grapheme cluster boundary.
for _ in 0..n {
loop {
match gc.next_boundary(chunk, chunk_byte_idx) {
Ok(None) => return self.len_bytes(),
Ok(Some(boundary)) => {
byte_idx = boundary;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
chunk_byte_idx += chunk.len();
let (a, _, _, _) = self.chunk_at_byte(chunk_byte_idx);
chunk = a;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = self.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
byte_idx
}
fn nth_prev_grapheme_boundary(self, mut byte_idx: usize, n: usize) -> usize {
// Bounds check
assert!(byte_idx <= self.len_bytes());
byte_idx = self.ceil_char_boundary(byte_idx);
// Get the chunk with our byte index in it.
let (mut chunk, mut chunk_byte_idx, _, _) = self.chunk_at_byte(byte_idx);
// Set up the grapheme cursor.
let mut gc = GraphemeCursor::new(byte_idx, self.len_bytes(), true);
for _ in 0..n {
loop {
match gc.prev_boundary(chunk, chunk_byte_idx) {
Ok(None) => return 0,
Ok(Some(boundary)) => {
byte_idx = boundary;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
let (a, b, _, _) = self.chunk_at_byte(chunk_byte_idx - 1);
chunk = a;
chunk_byte_idx = b;
}
Err(GraphemeIncomplete::PreContext(n)) => {
let ctx_chunk = self.chunk_at_byte(n - 1).0;
gc.provide_context(ctx_chunk, n - ctx_chunk.len());
}
_ => unreachable!(),
}
}
}
byte_idx
}
}
@@ -112,32 +573,208 @@ const fn is_utf8_char_boundary(b: u8) -> bool {
(b as i8) >= -0x40
}
/// An iterator over the graphemes of a `RopeSlice`.
///
/// This iterator is cursor-like: rather than implementing DoubleEndedIterator it can be reversed
/// like a cursor. This style matches `Bytes` and `Chars` iterator types in Ropey and is more
/// natural and useful for wrapping `GraphemeCursor`.
#[derive(Clone)]
pub struct RopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
current_chunk: &'a str,
/// Byte index of the start of the current chunk.
chunk_byte_idx: usize,
cursor: GraphemeCursor,
is_reversed: bool,
}
impl fmt::Debug for RopeGraphemes<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("current_chunk", &self.current_chunk)
.field("chunk_byte_idx", &self.chunk_byte_idx)
// .field("cursor", &self.cursor)
.field("is_reversed", &self.is_reversed)
.finish()
}
}
impl<'a> RopeGraphemes<'a> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<RopeSlice<'a>> {
if self.is_reversed {
self.prev_impl()
} else {
self.next_impl()
}
}
pub fn prev(&mut self) -> Option<RopeSlice<'a>> {
if self.is_reversed {
self.next_impl()
} else {
self.prev_impl()
}
}
pub fn reverse(&mut self) {
self.is_reversed = !self.is_reversed;
}
#[must_use]
pub fn reversed(mut self) -> Self {
self.reverse();
self
}
fn next_impl(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.next_boundary(self.current_chunk, self.chunk_byte_idx)
{
Ok(None) => return None,
Ok(Some(boundary)) => {
b = boundary;
break;
}
Err(GraphemeIncomplete::NextChunk) => {
self.chunk_byte_idx += self.current_chunk.len();
self.current_chunk = self.chunks.next().unwrap_or("");
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a < self.chunk_byte_idx {
Some(self.text.byte_slice(a..b))
} else {
let a2 = a - self.chunk_byte_idx;
let b2 = b - self.chunk_byte_idx;
Some((&self.current_chunk[a2..b2]).into())
}
}
fn prev_impl(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.prev_boundary(self.current_chunk, self.chunk_byte_idx)
{
Ok(None) => return None,
Ok(Some(boundary)) => {
b = boundary;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
self.current_chunk = self.chunks.prev().unwrap_or("");
self.chunk_byte_idx -= self.current_chunk.len();
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a >= self.chunk_byte_idx + self.current_chunk.len() {
Some(self.text.byte_slice(b..a))
} else {
let a2 = a - self.chunk_byte_idx;
let b2 = b - self.chunk_byte_idx;
Some((&self.current_chunk[b2..a2]).into())
}
}
}
impl<'a> Iterator for RopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<Self::Item> {
RopeGraphemes::next(self)
}
}
/// An iterator over the grapheme clusters in a rope and the byte indices where each grapheme
/// cluster starts.
///
/// This iterator wraps `RopeGraphemes` and is also cursor-like. Use `reverse` or `reversed` to
/// toggle the direction of the iterator. See [RopeGraphemes].
#[derive(Debug, Clone)]
pub struct RopeGraphemeIndices<'a> {
front_offset: usize,
iter: RopeGraphemes<'a>,
is_reversed: bool,
}
impl<'a> RopeGraphemeIndices<'a> {
#[allow(clippy::should_implement_trait)]
pub fn next(&mut self) -> Option<(usize, RopeSlice<'a>)> {
if self.is_reversed {
self.prev_impl()
} else {
self.next_impl()
}
}
pub fn prev(&mut self) -> Option<(usize, RopeSlice<'a>)> {
if self.is_reversed {
self.next_impl()
} else {
self.prev_impl()
}
}
pub fn reverse(&mut self) {
self.is_reversed = !self.is_reversed;
}
#[must_use]
pub fn reversed(mut self) -> Self {
self.reverse();
self
}
fn next_impl(&mut self) -> Option<(usize, RopeSlice<'a>)> {
let slice = self.iter.next()?;
let idx = self.front_offset;
self.front_offset += slice.len_bytes();
Some((idx, slice))
}
fn prev_impl(&mut self) -> Option<(usize, RopeSlice<'a>)> {
let slice = self.iter.prev()?;
self.front_offset -= slice.len_bytes();
Some((self.front_offset, slice))
}
}
impl<'a> Iterator for RopeGraphemeIndices<'a> {
type Item = (usize, RopeSlice<'a>);
fn next(&mut self) -> Option<Self::Item> {
RopeGraphemeIndices::next(self)
}
}
#[cfg(test)]
mod tests {
use ropey::RopeSlice;
use crate::rope::RopeSliceExt;
#[test]
fn next_char_at_byte() {
for i in 0..=6 {
assert_eq!(RopeSlice::from("foobar").byte_to_next_char(i), i);
}
for char_idx in 0..10 {
let len = "😆".len();
assert_eq!(
RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len),
char_idx
);
for i in 1..=len {
assert_eq!(
RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len + i),
char_idx + 1
);
}
}
}
#[test]
fn starts_with() {
assert!(RopeSlice::from("asdf").starts_with("a"));
@@ -147,4 +784,79 @@ mod tests {
fn ends_with() {
assert!(RopeSlice::from("asdf").ends_with("f"));
}
#[test]
fn char_boundaries() {
let ascii = RopeSlice::from("ascii");
// When the given index lies on a character boundary, the index should not change.
for byte_idx in 0..=ascii.len_bytes() {
assert_eq!(ascii.floor_char_boundary(byte_idx), byte_idx);
assert_eq!(ascii.ceil_char_boundary(byte_idx), byte_idx);
assert!(ascii.is_char_boundary(byte_idx));
}
// This is a polyfill of a method of this trait which was replaced by ceil_char_boundary.
// It returns the _character index_ of the given byte index, rounding up if it does not
// already lie on a character boundary.
fn byte_to_next_char(slice: RopeSlice, byte_idx: usize) -> usize {
slice.byte_to_char(slice.ceil_char_boundary(byte_idx))
}
for i in 0..=6 {
assert_eq!(byte_to_next_char(RopeSlice::from("foobar"), i), i);
}
for char_idx in 0..10 {
let len = "😆".len();
assert_eq!(
byte_to_next_char(RopeSlice::from("😆😆😆😆😆😆😆😆😆😆"), char_idx * len),
char_idx
);
for i in 1..=len {
assert_eq!(
byte_to_next_char(RopeSlice::from("😆😆😆😆😆😆😆😆😆😆"), char_idx * len + i),
char_idx + 1
);
}
}
}
#[test]
fn grapheme_boundaries() {
let ascii = RopeSlice::from("ascii");
// When the given index lies on a grapheme boundary, the index should not change.
for byte_idx in 0..=ascii.len_bytes() {
assert_eq!(ascii.floor_char_boundary(byte_idx), byte_idx);
assert_eq!(ascii.ceil_char_boundary(byte_idx), byte_idx);
assert!(ascii.is_grapheme_boundary(byte_idx));
}
// 🏴‍☠️: U+1F3F4 U+200D U+2620 U+FE0F
// 13 bytes, hex: f0 9f 8f b4 + e2 80 8d + e2 98 a0 + ef b8 8f
let g = RopeSlice::from("🏴‍☠️\r\n");
let emoji_len = "🏴‍☠️".len();
let end = g.len_bytes();
for byte_idx in 0..emoji_len {
assert_eq!(g.floor_grapheme_boundary(byte_idx), 0);
}
for byte_idx in emoji_len..end {
assert_eq!(g.floor_grapheme_boundary(byte_idx), emoji_len);
}
assert_eq!(g.floor_grapheme_boundary(end), end);
assert_eq!(g.ceil_grapheme_boundary(0), 0);
for byte_idx in 1..=emoji_len {
assert_eq!(g.ceil_grapheme_boundary(byte_idx), emoji_len);
}
for byte_idx in emoji_len + 1..=end {
assert_eq!(g.ceil_grapheme_boundary(byte_idx), end);
}
assert!(g.is_grapheme_boundary(0));
assert!(g.is_grapheme_boundary(emoji_len));
assert!(g.is_grapheme_boundary(end));
for byte_idx in (1..emoji_len).chain(emoji_len + 1..end) {
assert!(!g.is_grapheme_boundary(byte_idx));
}
}
}

View File

@@ -1,10 +1,6 @@
#![cfg(windows)]
use std::{
env::set_current_dir,
error::Error,
path::{Component, Path, PathBuf},
};
use std::{env::set_current_dir, error::Error, path::Component};
use helix_stdx::path;
use tempfile::Builder;
@@ -100,7 +96,7 @@ fn test_normalize_path() -> Result<(), Box<dyn Error>> {
assert_eq!(
path::normalize(&path),
expected,
"input {:?} and \"..\" should not erase the simlink that goes ahead",
"input {:?} and \"..\" should not erase the symlink that goes ahead",
&path
);

View File

@@ -51,7 +51,7 @@ helix-vcs = { path = "../helix-vcs" }
helix-loader = { path = "../helix-loader" }
anyhow = "1"
once_cell = "1.20"
once_cell = "1.21"
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"] }
@@ -61,6 +61,7 @@ tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
arc-swap = { version = "1.7.1" }
termini = "1"
indexmap = "2.10"
# Logging
fern = "0.7"
@@ -71,7 +72,7 @@ log = "0.4"
nucleo.workspace = true
ignore = "0.4"
# markdown doc rendering
pulldown-cmark = { version = "0.12", default-features = false }
pulldown-cmark = { version = "0.13", default-features = false }
# file type detection
content_inspector = "0.2.4"
thiserror.workspace = true
@@ -90,9 +91,11 @@ serde = { version = "1.0", features = ["derive"] }
grep-regex = "0.1.13"
grep-searcher = "0.1.14"
dashmap = "6.0"
[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.169"
libc = "0.2.174"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }
@@ -101,7 +104,7 @@ crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc
helix-loader = { path = "../helix-loader" }
[dev-dependencies]
smallvec = "1.13"
indoc = "2.0.5"
smallvec = "1.15"
indoc = "2.0.6"
tempfile.workspace = true
same-file = "1.0.1"

View File

@@ -1,6 +1,6 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Range, Selection};
use helix_lsp::{
lsp::{self, notification::Notification},
util::lsp_range_to_range,
@@ -11,7 +11,6 @@ use helix_view::{
align_view,
document::{DocumentOpenError, DocumentSavedEventResult},
editor::{ConfigEvent, EditorEvent},
events::DiagnosticsDidChange,
graphics::Rect,
theme,
tree::Layout,
@@ -33,7 +32,7 @@ use crate::{
use log::{debug, error, info, warn};
#[cfg(not(feature = "integration"))]
use std::io::stdout;
use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
use std::{io::stdin, path::Path, sync::Arc};
#[cfg(not(windows))]
use anyhow::Context;
@@ -66,11 +65,6 @@ pub struct Application {
config: Arc<ArcSwap<Config>>,
#[allow(dead_code)]
theme_loader: Arc<theme::Loader>,
#[allow(dead_code)]
syn_loader: Arc<ArcSwap<syntax::Loader>>,
signals: Signals,
jobs: Jobs,
lsp_progress: LspProgressMap,
@@ -107,25 +101,7 @@ impl Application {
let mut theme_parent_dirs = vec![helix_loader::config_dir()];
theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned());
let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs));
let true_color = config.editor.true_color || crate::true_color();
let theme = config
.theme
.as_ref()
.and_then(|theme| {
theme_loader
.load(theme)
.map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, e);
e
})
.ok()
.filter(|theme| (true_color || theme.is_16_color()))
})
.unwrap_or_else(|| theme_loader.default_theme(true_color));
let syn_loader = Arc::new(ArcSwap::from_pointee(lang_loader));
let theme_loader = theme::Loader::new(&theme_parent_dirs);
#[cfg(not(feature = "integration"))]
let backend = CrosstermBackend::new(stdout(), &config.editor);
@@ -140,13 +116,14 @@ impl Application {
let handlers = handlers::setup(config.clone());
let mut editor = Editor::new(
area,
theme_loader.clone(),
syn_loader.clone(),
Arc::new(theme_loader),
Arc::new(ArcSwap::from_pointee(lang_loader)),
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.editor
})),
handlers,
);
Self::load_configured_theme(&mut editor, &config.load());
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.keys
@@ -164,7 +141,7 @@ impl Application {
// If the first file is a directory, skip it and open a picker
if let Some((first, _)) = files_it.next_if(|(p, _)| p.is_dir()) {
let picker = ui::file_picker(first, &config.load().editor);
let picker = ui::file_picker(&editor, first);
compositor.push(Box::new(overlaid(picker)));
}
@@ -210,8 +187,13 @@ impl Application {
// opened last is focused on.
let view_id = editor.tree.focus;
let doc = doc_mut!(editor, &doc_id);
let pos = Selection::point(pos_at_coords(doc.text().slice(..), pos, true));
doc.set_selection(view_id, pos);
let selection = pos
.into_iter()
.map(|coords| {
Range::point(pos_at_coords(doc.text().slice(..), coords, true))
})
.collect();
doc.set_selection(view_id, selection);
}
}
@@ -240,8 +222,6 @@ impl Application {
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
}
editor.set_theme(theme);
#[cfg(windows)]
let signals = futures_util::stream::empty();
#[cfg(not(windows))]
@@ -258,12 +238,7 @@ impl Application {
compositor,
terminal,
editor,
config,
theme_loader,
syn_loader,
signals,
jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(),
@@ -381,6 +356,8 @@ impl Application {
}
pub fn handle_config_events(&mut self, config_event: ConfigEvent) {
let old_editor_config = self.editor.config();
match config_event {
ConfigEvent::Refresh => self.refresh_config(),
@@ -399,7 +376,7 @@ impl Application {
// Update all the relevant members in the editor after updating
// the configuration.
self.editor.refresh_config();
self.editor.refresh_config(&old_editor_config);
// reset view position in case softwrap was enabled/disabled
let scrolloff = self.editor.config().scrolloff;
@@ -409,53 +386,32 @@ impl Application {
}
}
/// refresh language config after config change
fn refresh_language_config(&mut self) -> Result<(), Error> {
let lang_loader = helix_core::config::user_lang_loader()?;
self.syn_loader.store(Arc::new(lang_loader));
self.editor.syn_loader = self.syn_loader.clone();
for document in self.editor.documents.values_mut() {
document.detect_language(self.syn_loader.clone());
let diagnostics = Editor::doc_diagnostics(
&self.editor.language_servers,
&self.editor.diagnostics,
document,
);
document.replace_diagnostics(diagnostics, &[], None);
}
Ok(())
}
/// Refresh theme after config change
fn refresh_theme(&mut self, config: &Config) -> Result<(), Error> {
let true_color = config.editor.true_color || crate::true_color();
let theme = config
.theme
.as_ref()
.and_then(|theme| {
self.theme_loader
.load(theme)
.map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, e);
e
})
.ok()
.filter(|theme| (true_color || theme.is_16_color()))
})
.unwrap_or_else(|| self.theme_loader.default_theme(true_color));
self.editor.set_theme(theme);
Ok(())
}
fn refresh_config(&mut self) {
let mut refresh_config = || -> Result<(), Error> {
let default_config = Config::load_default()
.map_err(|err| anyhow::anyhow!("Failed to load config: {}", err))?;
self.refresh_language_config()?;
self.refresh_theme(&default_config)?;
// Update the syntax language loader before setting the theme. Setting the theme will
// call `Loader::set_scopes` which must be done before the documents are re-parsed for
// the sake of locals highlighting.
let lang_loader = helix_core::config::user_lang_loader()?;
self.editor.syn_loader.store(Arc::new(lang_loader));
Self::load_configured_theme(&mut self.editor, &default_config);
// Re-parse any open documents with the new language config.
let lang_loader = self.editor.syn_loader.load();
for document in self.editor.documents.values_mut() {
// Re-detect .editorconfig
document.detect_editor_config();
document.detect_language(&lang_loader);
let diagnostics = Editor::doc_diagnostics(
&self.editor.language_servers,
&self.editor.diagnostics,
document,
);
document.replace_diagnostics(diagnostics, &[], None);
}
self.terminal
.reconfigure(default_config.editor.clone().into())?;
// Store new config
@@ -473,6 +429,37 @@ impl Application {
}
}
/// Load the theme set in configuration
fn load_configured_theme(editor: &mut Editor, config: &Config) {
let true_color = config.editor.true_color || crate::true_color();
let theme = config
.theme
.as_ref()
.and_then(|theme| {
editor
.theme_loader
.load(theme)
.map_err(|e| {
log::warn!("failed to load theme `{}` - {}", theme, e);
e
})
.ok()
.filter(|theme| {
let colors_ok = true_color || theme.is_16_color();
if !colors_ok {
log::warn!(
"loaded theme `{}` but cannot use it because true color \
support is not enabled",
theme.name()
);
}
colors_ok
})
})
.unwrap_or_else(|| editor.theme_loader.default_theme(true_color));
editor.set_theme(theme);
}
#[cfg(windows)]
// no signal handling available on windows
pub async fn handle_signals(&mut self, _signal: ()) -> bool {
@@ -586,16 +573,24 @@ impl Application {
doc.set_last_saved_revision(doc_save_event.revision, doc_save_event.save_time);
let lines = doc_save_event.text.len_lines();
let bytes = doc_save_event.text.len_bytes();
let mut sz = doc_save_event.text.len_bytes() as f32;
const SUFFIX: [&str; 4] = ["B", "KiB", "MiB", "GiB"];
let mut i = 0;
while i < SUFFIX.len() - 1 && sz >= 1024.0 {
sz /= 1024.0;
i += 1;
}
self.editor
.set_doc_path(doc_save_event.doc_id, &doc_save_event.path);
// TODO: fix being overwritten by lsp
self.editor.set_status(format!(
"'{}' written, {}L {}B",
"'{}' written, {}L {:.1}{}",
get_relative_path(&doc_save_event.path).to_string_lossy(),
lines,
bytes
sz,
SUFFIX[i],
));
}
@@ -617,8 +612,8 @@ impl Application {
// limit render calls for fast language server messages
helix_event::request_redraw();
}
EditorEvent::DebuggerEvent(payload) => {
let needs_render = self.editor.handle_debugger_message(payload).await;
EditorEvent::DebuggerEvent((id, payload)) => {
let needs_render = self.editor.handle_debugger_message(id, payload).await;
if needs_render {
self.render().await;
}
@@ -717,33 +712,15 @@ impl Application {
// This might not be required by the spec but Neovim does this as well, so it's
// probably a good idea for compatibility.
if let Some(config) = language_server.config() {
tokio::spawn(language_server.did_change_configuration(config.clone()));
language_server.did_change_configuration(config.clone());
}
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 {
let url = match doc.url() {
Some(url) => url,
None => continue, // skip documents with no path
};
let language_id =
doc.language_id().map(ToOwned::to_owned).unwrap_or_default();
tokio::spawn(language_server.text_document_did_open(
url,
doc.version(),
doc.text(),
language_id,
));
}
helix_event::dispatch(helix_view::events::LanguageServerInitialized {
editor: &mut self.editor,
server_id,
});
}
Notification::PublishDiagnostics(mut params) => {
Notification::PublishDiagnostics(params) => {
let uri = match helix_core::Uri::try_from(params.uri) {
Ok(uri) => uri,
Err(err) => {
@@ -756,100 +733,16 @@ impl Application {
log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name());
return;
}
// have to inline the function because of borrow checking...
let doc = self.editor.documents.values_mut()
.find(|doc| doc.uri().is_some_and(|u| u == uri))
.filter(|doc| {
if let Some(version) = params.version {
if version != doc.version() {
log::info!("Version ({version}) is out of date for {uri:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
return false;
}
}
true
});
let mut unchanged_diag_sources = Vec::new();
if let Some(doc) = &doc {
let lang_conf = doc.language.clone();
if let Some(lang_conf) = &lang_conf {
if let Some(old_diagnostics) = self.editor.diagnostics.get(&uri) {
if !lang_conf.persistent_diagnostic_sources.is_empty() {
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
params
.diagnostics
.sort_by_key(|d| (d.severity, d.range.start));
}
for source in &lang_conf.persistent_diagnostic_sources {
let new_diagnostics = params
.diagnostics
.iter()
.filter(|d| d.source.as_ref() == Some(source));
let old_diagnostics = old_diagnostics
.iter()
.filter(|(d, d_server)| {
*d_server == server_id
&& d.source.as_ref() == Some(source)
})
.map(|(d, _)| d);
if new_diagnostics.eq(old_diagnostics) {
unchanged_diag_sources.push(source.clone())
}
}
}
}
}
let diagnostics = params.diagnostics.into_iter().map(|d| (d, server_id));
// 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.
let diagnostics = match self.editor.diagnostics.entry(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.extend(diagnostics);
current_diagnostics
// Sort diagnostics first by severity and then by line numbers.
}
Entry::Vacant(v) => v.insert(diagnostics.collect()),
let provider = helix_core::diagnostic::DiagnosticProvider::Lsp {
server_id,
identifier: None,
};
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
diagnostics
.sort_by_key(|(d, server_id)| (d.severity, d.range.start, *server_id));
if let Some(doc) = doc {
let diagnostic_of_language_server_and_not_in_unchanged_sources =
|diagnostic: &lsp::Diagnostic, ls_id| {
ls_id == server_id
&& diagnostic.source.as_ref().map_or(true, |source| {
!unchanged_diag_sources.contains(source)
})
};
let diagnostics = Editor::doc_diagnostics_with_filter(
&self.editor.language_servers,
&self.editor.diagnostics,
doc,
diagnostic_of_language_server_and_not_in_unchanged_sources,
);
doc.replace_diagnostics(
diagnostics,
&unchanged_diag_sources,
Some(server_id),
);
let doc = doc.id();
helix_event::dispatch(DiagnosticsDidChange {
editor: &mut self.editor,
doc,
});
}
self.editor.handle_lsp_diagnostics(
&provider,
uri,
params.version,
params.diagnostics,
);
}
Notification::ShowMessage(params) => {
if self.config.load().editor.lsp.display_messages {
@@ -874,10 +767,11 @@ impl Application {
.compositor
.find::<ui::EditorView>()
.expect("expected at least one EditorView");
let lsp::ProgressParams { token, value } = params;
let lsp::ProgressParamsValue::WorkDone(work) = value;
let parts = match &work {
let lsp::ProgressParams {
token,
value: lsp::ProgressParamsValue::WorkDone(work),
} = params;
let (title, message, percentage) = match &work {
lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
title,
message,
@@ -905,47 +799,43 @@ impl Application {
}
};
let token_d: &dyn std::fmt::Display = match &token {
lsp::NumberOrString::Number(n) => n,
lsp::NumberOrString::String(s) => s,
};
let status = match parts {
(Some(title), Some(message), Some(percentage)) => {
format!("[{}] {}% {} - {}", token_d, percentage, title, message)
if self.editor.config().lsp.display_progress_messages {
let title =
title.or_else(|| self.lsp_progress.title(server_id, &token));
if title.is_some() || percentage.is_some() || message.is_some() {
use std::fmt::Write as _;
let mut status = format!("{}: ", language_server!().name());
if let Some(percentage) = percentage {
write!(status, "{percentage:>2}% ").unwrap();
}
if let Some(title) = title {
status.push_str(title);
}
if title.is_some() && message.is_some() {
status.push_str("");
}
if let Some(message) = message {
status.push_str(message);
}
self.editor.set_status(status);
}
(Some(title), None, Some(percentage)) => {
format!("[{}] {}% {}", token_d, percentage, title)
}
(Some(title), Some(message), None) => {
format!("[{}] {} - {}", token_d, title, message)
}
(None, Some(message), Some(percentage)) => {
format!("[{}] {}% {}", token_d, percentage, message)
}
(Some(title), None, None) => {
format!("[{}] {}", token_d, title)
}
(None, Some(message), None) => {
format!("[{}] {}", token_d, message)
}
(None, None, Some(percentage)) => {
format!("[{}] {}%", token_d, percentage)
}
(None, None, None) => format!("[{}]", token_d),
};
if let lsp::WorkDoneProgress::End(_) = work {
self.lsp_progress.end_progress(server_id, &token);
if !self.lsp_progress.is_progressing(server_id) {
editor_view.spinners_mut().get_or_create(server_id).stop();
}
} else {
self.lsp_progress.update(server_id, token, work);
}
if self.config.load().editor.lsp.display_progress_messages {
self.editor.set_status(status);
match work {
lsp::WorkDoneProgress::Begin(begin_status) => {
self.lsp_progress
.begin(server_id, token.clone(), begin_status);
}
lsp::WorkDoneProgress::Report(report_status) => {
self.lsp_progress
.update(server_id, token.clone(), report_status);
}
lsp::WorkDoneProgress::End(_) => {
self.lsp_progress.end_progress(server_id, &token);
if !self.lsp_progress.is_progressing(server_id) {
editor_view.spinners_mut().get_or_create(server_id).stop();
};
}
}
}
Notification::ProgressMessage(_params) => {
@@ -958,16 +848,23 @@ impl Application {
// 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);
diags.retain(|(_, provider)| {
provider.language_server_id() != Some(server_id)
});
}
self.editor.diagnostics.retain(|_, diags| !diags.is_empty());
// Clear any diagnostics for documents with this server open.
for doc in self.editor.documents_mut() {
doc.clear_diagnostics(Some(server_id));
doc.clear_diagnostics_for_language_server(server_id);
}
helix_event::dispatch(helix_view::events::LanguageServerExited {
editor: &mut self.editor,
server_id,
});
// Remove the language server from the registry.
self.editor.language_servers.remove_by_id(server_id);
}
@@ -1126,7 +1023,13 @@ impl Application {
}
};
tokio::spawn(language_server!().reply(id, reply));
let language_server = language_server!();
if let Err(err) = language_server.reply(id.clone(), reply) {
log::error!(
"Failed to send reply to server '{}' request {id}: {err}",
language_server.name()
);
}
}
Call::Invalid { id } => log::error!("LSP invalid method call id={:?}", id),
}

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