Compare commits

...

255 Commits

Author SHA1 Message Date
Michael Davis
d4cb822a15 Use KString as the small-string type for the WordIndex
It's already used in gix and tree-house so it does not introduce a new
dependency. It's a small-string type that fits into 16B (like a
`Box<str>`) meant to be primarily used as keys for large maps.
2025-07-16 11:43:55 -04:00
Michael Davis
218ccaac90 Complete words from open buffers 2025-07-16 11:43:55 -04:00
Björn Ganslandt
2ee11a0a9d Add textobjects for XML, HTML and JSX (#11158) 2025-07-16 09:02:52 -05:00
Poliorcetics
9512cb9472 contrib(completions/nushell): switch from deprecated filter to where (#13848) 2025-07-16 08:43:33 -05:00
Bryce Berger
3658e97c2b Add tree-sitter injections for jj config files (#13926) 2025-07-16 08:36:54 -05:00
dependabot[bot]
ab668c2dfc build(deps): bump toml from 0.8.23 to 0.9.2 in the rust-dependencies group (#13955)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2025-07-16 08:18:17 -05:00
Anton Romanov
ef2ebc5f24 [theme] Fix zenburn doc comment color (#13962) 2025-07-16 08:10:57 -05:00
Michael Davis
5cda70e866 Add changelog notes for 25.07 (#13939) 2025-07-15 13:26:52 -05:00
Meiram Shunshalin
c67c3faa78 feat(themes): add soft-wrap style for nightfox (#13957) 2025-07-15 08:42:41 -05: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
353 changed files with 13462 additions and 6648 deletions

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

@@ -9,7 +9,7 @@ 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: ""
@@ -35,8 +35,8 @@ jobs:
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_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 check
run: cargo check
@@ -45,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
@@ -65,8 +66,8 @@ jobs:
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_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
@@ -76,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
@@ -100,8 +101,8 @@ jobs:
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_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 fmt
run: cargo fmt --all --check
@@ -135,8 +136,8 @@ jobs:
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.GRAMMAR_CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
restore-keys: ${{ runner.os }}-stable-v${{ env.GRAMMAR_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: Validate queries
run: cargo xtask query-check

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

@@ -20,6 +20,302 @@ Updated languages and queries:
Packaging:
-->
# 25.07 (2025-07-15)
As always, a big thank you to all of the contributors! This release saw changes from 195 contributors.
Breaking changes:
* The parsing of the command line has been rewritten and now supports flags and expansions ([#12527](https://github.com/helix-editor/helix/pull/12527), [#13018](https://github.com/helix-editor/helix/pull/13018), [9574e55](https://github.com/helix-editor/helix/commit/9574e55), [2d4c2a1](https://github.com/helix-editor/helix/commit/2d4c2a1), [#13192](https://github.com/helix-editor/helix/pull/13192), [67f1fe2](https://github.com/helix-editor/helix/commit/67f1fe2), [#13466](https://github.com/helix-editor/helix/pull/13466), [#13467](https://github.com/helix-editor/helix/pull/13467), [#13840](https://github.com/helix-editor/helix/pull/13840))
* Quoting and spaces are now handled differently. This can break existing keymaps which use typable commands, in particular `:sh`, `:set-option` or `:toggle-option`.
* The `:rsort` command has been removed. Use the reverse flag instead: `:sort --reverse`
Features:
* Add a picker which explores directories ([#11285](https://github.com/helix-editor/helix/pull/11285), [d4aed40](https://github.com/helix-editor/helix/commit/d4aed40))
* Allow cycling through multiple LSP Hover responses with `A-n`/`A-p` ([#10122](https://github.com/helix-editor/helix/pull/10122), [2367b20](https://github.com/helix-editor/helix/commit/2367b20))
* Add support for incomplete LSP completions ([5c1f3f8](https://github.com/helix-editor/helix/commit/5c1f3f8))
* Add support for EditorConfig ([#13056](https://github.com/helix-editor/helix/pull/13056), [#13443](https://github.com/helix-editor/helix/pull/13443))
* Add support for LSP document colors ([#12308](https://github.com/helix-editor/helix/pull/12308), [d43de14](https://github.com/helix-editor/helix/commit/d43de14), [47cdd23](https://github.com/helix-editor/helix/commit/47cdd23), [ba54b6a](https://github.com/helix-editor/helix/commit/ba54b6a), [#13188](https://github.com/helix-editor/helix/pull/13188))
* Support expansions in external formatter arguments ([#13429](https://github.com/helix-editor/helix/pull/13429))
* Switch out the highlighter for the `tree-house` crate ([#12972](https://github.com/helix-editor/helix/pull/12972), [09bc67a](https://github.com/helix-editor/helix/commit/09bc67a), [a7c3a43](https://github.com/helix-editor/helix/commit/a7c3a43), [3ceae88](https://github.com/helix-editor/helix/commit/3ceae88), [05ae617](https://github.com/helix-editor/helix/commit/05ae617), [5a1dcc2](https://github.com/helix-editor/helix/commit/5a1dcc2), [ebf96bd](https://github.com/helix-editor/helix/commit/ebf96bd), [#13644](https://github.com/helix-editor/helix/pull/13644), [b1f4717](https://github.com/helix-editor/helix/commit/b1f4717), [7410fe3](https://github.com/helix-editor/helix/commit/7410fe3), [633c5fb](https://github.com/helix-editor/helix/commit/633c5fb), [362e97e](https://github.com/helix-editor/helix/commit/362e97e), [#13828](https://github.com/helix-editor/helix/pull/13828), [6fd1efd](https://github.com/helix-editor/helix/commit/6fd1efd))
* This fixes a number of highlighter bugs.
* Locals like parameter highlights are now highlighted even when the definition is not in view.
* Markdown is now injected into rust doc comments (`///` and `//!`).
* Add support for the DAP `startDebugging` reverse request ([#13403](https://github.com/helix-editor/helix/pull/13403))
Commands:
* Add `copy_between_registers` for interactive copying between two registers ([066e938](https://github.com/helix-editor/helix/commit/066e938))
* Add `extend_to_file_{start,end}`, select-mode variants of `goto_file_{start,end}` ([#11767](https://github.com/helix-editor/helix/pull/11767))
* Add `:!` alias for `:sh` and `:|` for `:pipe` ([#13263](https://github.com/helix-editor/helix/pull/13263))
* Add `goto_column` and `extend_to_column` ([#13440](https://github.com/helix-editor/helix/pull/13440))
* Add an `--insensitive`/`-i` flag to the `:sort` command ([#13560](https://github.com/helix-editor/helix/pull/13560))
* Add `rotate_selections_first` and `rotate_selections_last` ([#13615](https://github.com/helix-editor/helix/pull/13615))
* Add a `--no-format` flag for all `:write` commands ([2f56091](https://github.com/helix-editor/helix/commit/2f56091))
* Add a `--skip-visible` flag for `:buffer-close-others` and `:buffer-close-others!` ([#5393](https://github.com/helix-editor/helix/pull/5393))
Usability improvements:
* Replace current file using `A-ret` in pickers rather than loading it in the background ([#12605](https://github.com/helix-editor/helix/pull/12605))
* Set multiple selections when passing a file with multiple locations to `hx` ([#12192](https://github.com/helix-editor/helix/pull/12192))
* Add path completion for multiple cursors ([#12550](https://github.com/helix-editor/helix/pull/12550), [c9dc940](https://github.com/helix-editor/helix/commit/c9dc940))
* Truncate long prompt lines with "…" ([#12036](https://github.com/helix-editor/helix/pull/12036), [9d6ea77](https://github.com/helix-editor/helix/commit/9d6ea77), [0b9701e](https://github.com/helix-editor/helix/commit/0b9701e), [d3fb8fc](https://github.com/helix-editor/helix/commit/d3fb8fc))
* Allow specifying languages in `:lsp-stop` and `:lsp-restart` ([#12578](https://github.com/helix-editor/helix/pull/12578), [3d7e273](https://github.com/helix-editor/helix/commit/3d7e273))
* Add `m` (nearest matching pair) to infobox popups for `md` and `mr` ([#12650](https://github.com/helix-editor/helix/pull/12650))
* Add a hint message in the statusline when using `:sort` on a single selection ([#12585](https://github.com/helix-editor/helix/pull/12585))
* Avoid wrapping around in `goto_{next,prev}_diag` ([#12704](https://github.com/helix-editor/helix/pull/12704))
* Support responses from multiple language servers for LSP goto-definition (and declaration, type definition and implementation) and goto-references ([f7394d5](https://github.com/helix-editor/helix/commit/f7394d5), [1a821ac](https://github.com/helix-editor/helix/commit/1a821ac), [d285a8a](https://github.com/helix-editor/helix/commit/d285a8a))
* Show formatter errors in `:format` ([47f84d0](https://github.com/helix-editor/helix/commit/47f84d0))
* Show typable command docs in keybinding infobox popups when the command takes no arguments ([e9c16b7](https://github.com/helix-editor/helix/commit/e9c16b7))
* Add per-command titles to register selection infobox popups for `select_register`, `insert_register` and `copy_between_registers` ([e0da129](https://github.com/helix-editor/helix/commit/e0da129))
* Add container name column to the LSP symbol picker ([#12930](https://github.com/helix-editor/helix/pull/12930))
* Add a theme key for highlighting directories in completions and picker items ([#12855](https://github.com/helix-editor/helix/pull/12855), [7bebe0a](https://github.com/helix-editor/helix/commit/7bebe0a))
* Add `editor.trim-final-newlines` and `editor.trim-trailing-whitespace` config options ([aa20eb8](https://github.com/helix-editor/helix/commit/aa20eb8))
* Warn when the configured theme is unusable because true-color is not available ([#13058](https://github.com/helix-editor/helix/pull/13058))
* Allow configuring `[workspace-]diagnostic` statusline element severities ([#13288](https://github.com/helix-editor/helix/pull/13288), [b0528bb](https://github.com/helix-editor/helix/commit/b0528bb))
* Improve completion for shell commands ([#12883](https://github.com/helix-editor/helix/pull/12883), [532f241](https://github.com/helix-editor/helix/commit/532f241))
* Show the primary selection index in the `selections` statusline element when there are multiple selections ([#12326](https://github.com/helix-editor/helix/pull/12326))
* Use configured language server names when possible in `--health` output ([#13573](https://github.com/helix-editor/helix/pull/13573))
* Add a statusline element for indentation style ([#13632](https://github.com/helix-editor/helix/pull/13632))
* Set the working directory of language server commands to the workspace root ([#13691](https://github.com/helix-editor/helix/pull/13691))
* Avoid jumpiness in the picker preview for languages with non-default tab widths ([#13761](https://github.com/helix-editor/helix/pull/13761))
* Add a config option for limiting LSP inlay hint length ([#13742](https://github.com/helix-editor/helix/pull/13742))
* Improve heuristics used in the diff gutter ([#13722](https://github.com/helix-editor/helix/pull/13722))
* Allow moving a file with `:move` when its old path does not exist ([#13748](https://github.com/helix-editor/helix/pull/13748))
* Allow moving a file into a directory with `:move` ([#13922](https://github.com/helix-editor/helix/pull/13922))
* Show human-readable file sizes in the statusline message for file writes ([#13627](https://github.com/helix-editor/helix/pull/13627))
* Add diagnostic source to the diagnosics pickers ([#13758](https://github.com/helix-editor/helix/pull/13758))
* Show all active scopes under the cursor in `:tree-sitter-highlight-name` ([4a85171](https://github.com/helix-editor/helix/commit/4a85171))
* Auto-close the LSP code-actions popup ([#13832](https://github.com/helix-editor/helix/pull/13832))
* Add a configuration option for controlling atomic writes to disk ([#13656](https://github.com/helix-editor/helix/pull/13656))
Fixes:
* Fix panic from using `search_selection_detect_word_boundaries` (`*`) at the end of the file ([#12611](https://github.com/helix-editor/helix/pull/12611))
* Discard placeholder text for zero tabstop `${0:placeholder}` ([#12647](https://github.com/helix-editor/helix/pull/12647))
* Fix panic in `goto_file` (`gf`) on file names with non-ASCII characters ([#12673](https://github.com/helix-editor/helix/pull/12673))
* Only accept unmodified characters in `goto_word` (`gw`) ([f5f9f49](https://github.com/helix-editor/helix/commit/f5f9f49), [0364521](https://github.com/helix-editor/helix/commit/0364521))
* Skip recording keys pressed by macros while recording a macro ([#12733](https://github.com/helix-editor/helix/pull/12733))
* Deny unknown fields in `editor.smart-tab` config ([28047fe](https://github.com/helix-editor/helix/commit/28047fe))
* Fix soft-wrap word boundary detection for Unicode combining accent characters ([#12483](https://github.com/helix-editor/helix/pull/12483))
* Fix clearing of infobox popups in `select_register` and `insert_register` commands ([e882a75](https://github.com/helix-editor/helix/commit/e882a75))
* Fix handling of `stderr` of DAP child processes ([d0d1693](https://github.com/helix-editor/helix/commit/d0d1693))
* Cancel all pending requests when a DAP session terminates ([26db541](https://github.com/helix-editor/helix/commit/26db541))
* Properly discard out-of-date diagnostics ([313a647](https://github.com/helix-editor/helix/commit/313a647))
* Fix display of multiple language servers in `hx --health` ([#12841](https://github.com/helix-editor/helix/pull/12841))
* Respect `editor.default-yank-register` in `:yank-joined` ([#12890](https://github.com/helix-editor/helix/pull/12890))
* Escape percent character when pasting the history register into the picker ([#12886](https://github.com/helix-editor/helix/pull/12886))
* Render rulers before the cursor ([2d3b75a](https://github.com/helix-editor/helix/commit/2d3b75a))
* Avoid inserting final newlines in empty files ([67879a1](https://github.com/helix-editor/helix/commit/67879a1))
* Gracefully handle partial failure in multi-language-server requests ([#13156](https://github.com/helix-editor/helix/pull/13156), [14cab4b](https://github.com/helix-editor/helix/commit/14cab4b))
* Improve LSP progress message display in the statusline ([#13180](https://github.com/helix-editor/helix/pull/13180))
* Fix behavior of `<esc>` removing added indentation in documents with CRLF line endings ([702a961](https://github.com/helix-editor/helix/commit/702a961))
* Append changes to document history before pushing jumplist jumps ([#13619](https://github.com/helix-editor/helix/pull/13619))
* Fix overflow in the display of large chunks of text in the signature-help component ([#13566](https://github.com/helix-editor/helix/pull/13566))
* Fix panic from clearing whitespace when changing multiple selections on one line ([#13673](https://github.com/helix-editor/helix/pull/13673))
* Include formatting options in LSP range formatting request ([#13734](https://github.com/helix-editor/helix/pull/13734))
* Consistently set statusline errors when LSP features are not available ([#12577](https://github.com/helix-editor/helix/pull/12577))
* Fix `goto_file` on Windows ([#13770](https://github.com/helix-editor/helix/pull/13770))
* Fix crash in `goto_word` (`gw`) when `editor.jump-label-alphabet` is configured to be empty ([#13863](https://github.com/helix-editor/helix/pull/13863))
* Fix `open_above` / `open_below` (`o` / `O`) when using a count on a document with CRLF line-endings ([#13905](https://github.com/helix-editor/helix/pull/13905))
Themes:
* Update `modus` themes ([#12670](https://github.com/helix-editor/helix/pull/12670))
* Update `snazzy` ([#11089](https://github.com/helix-editor/helix/pull/11089))
* Update `gruber-darker` ([#12797](https://github.com/helix-editor/helix/pull/12797))
* Update `cyan_light` ([#12864](https://github.com/helix-editor/helix/pull/12864), [#12891](https://github.com/helix-editor/helix/pull/12891))
* Update `onedarker` ([#12833](https://github.com/helix-editor/helix/pull/12833))
* Update `github_light` ([#12907](https://github.com/helix-editor/helix/pull/12907))
* Update `kanagawa` ([#12895](https://github.com/helix-editor/helix/pull/12895))
* Add `beans` ([#12963](https://github.com/helix-editor/helix/pull/12963))
* Update `base16_transparent` ([#13080](https://github.com/helix-editor/helix/pull/13080))
* Update `sunset` ([#13086](https://github.com/helix-editor/helix/pull/13086))
* Add `carbon` ([#13067](https://github.com/helix-editor/helix/pull/13067))
* Update `soralized` ([#13121](https://github.com/helix-editor/helix/pull/13121))
* Add `focus_nova` ([#13144](https://github.com/helix-editor/helix/pull/13144))
* Update `onedark` ([#13166](https://github.com/helix-editor/helix/pull/13166))
* Update `adwaita-light` ([#13174](https://github.com/helix-editor/helix/pull/13174))
* Add `earl_grey` ([#13203](https://github.com/helix-editor/helix/pull/13203))
* Update `spacebones` ([#13213](https://github.com/helix-editor/helix/pull/13213))
* Add `peachpuff` ([#13225](https://github.com/helix-editor/helix/pull/13225))
* Update catppuccin themes ([#13262](https://github.com/helix-editor/helix/pull/13262))
* Update gruvbox themes ([#13315](https://github.com/helix-editor/helix/pull/13315))
* Update serika themes ([#13341](https://github.com/helix-editor/helix/pull/13341))
* Add `gruvbox-material` ([#13311](https://github.com/helix-editor/helix/pull/13311))
* Add `ashen` ([#13366](https://github.com/helix-editor/helix/pull/13366))
* Update Zed themes ([#13370](https://github.com/helix-editor/helix/pull/13370))
* Update Tokyonight themes ([#13375](https://github.com/helix-editor/helix/pull/13375))
* Update `onelight` ([#13413](https://github.com/helix-editor/helix/pull/13413))
* Add `ataraxia` ([#13390](https://github.com/helix-editor/helix/pull/13390))
* Add `vesper` ([#13394](https://github.com/helix-editor/helix/pull/13394))
* Add `kinda_nvim` and `kinda_nvim_light` ([#13406](https://github.com/helix-editor/helix/pull/13406))
* Update `sonokai` ([#13410](https://github.com/helix-editor/helix/pull/13410))
* Add `nyxvamp` themes ([#12185](https://github.com/helix-editor/helix/pull/12185))
* Update nord themes ([#13574](https://github.com/helix-editor/helix/pull/13574))
* Add `lapis_aquamarine` ([#13726](https://github.com/helix-editor/helix/pull/13726))
* Add `sidra` ([#13575](https://github.com/helix-editor/helix/pull/13575))
* Add `dark-synthwave` ([#13857](https://github.com/helix-editor/helix/pull/13857))
* Update `rose_pine` ([#13908](https://github.com/helix-editor/helix/pull/13908))
* Add `doom-one` ([#13933](https://github.com/helix-editor/helix/pull/13933))
* Update `nightfox` ([#13957](https://github.com/helix-editor/helix/pull/13957))
New languages:
* Ghostty config ([#12703](https://github.com/helix-editor/helix/pull/12703))
* Tera ([#12756](https://github.com/helix-editor/helix/pull/12756))
* FGA ([#12763](https://github.com/helix-editor/helix/pull/12763))
* CSV ([#11973](https://github.com/helix-editor/helix/pull/11973))
* Yara ([#12753](https://github.com/helix-editor/helix/pull/12753))
* Djot ([#12562](https://github.com/helix-editor/helix/pull/12562))
* Ink ([#12773](https://github.com/helix-editor/helix/pull/12773))
* Mail ([#12945](https://github.com/helix-editor/helix/pull/12945))
* SourcePawn ([#13028](https://github.com/helix-editor/helix/pull/13028))
* TLA+ ([#13081](https://github.com/helix-editor/helix/pull/13081))
* Werk ([#13136](https://github.com/helix-editor/helix/pull/13136))
* Debian control file ([#13245](https://github.com/helix-editor/helix/pull/13245))
* WESL ([#13267](https://github.com/helix-editor/helix/pull/13267))
* Fennel ([#13260](https://github.com/helix-editor/helix/pull/13260), [6081a5d](https://github.com/helix-editor/helix/commit/6081a5d))
* Quarto ([#13339](https://github.com/helix-editor/helix/pull/13339))
* Pug ([#13435](https://github.com/helix-editor/helix/pull/13435))
* Slang ([#13449](https://github.com/helix-editor/helix/pull/13449))
* Dunst config ([#13458](https://github.com/helix-editor/helix/pull/13458))
* Luau ([#13702](https://github.com/helix-editor/helix/pull/13702))
* Caddyfile ([#13859](https://github.com/helix-editor/helix/pull/13859))
* Java properties ([#13874](https://github.com/helix-editor/helix/pull/13874))
* Git notes ([#13885](https://github.com/helix-editor/helix/pull/13885))
* systemd (split from INI) ([#13907](https://github.com/helix-editor/helix/pull/13907))
* JSON-LD (split from JSON) ([#13925](https://github.com/helix-editor/helix/pull/13925))
* Django HTML ([#13935](https://github.com/helix-editor/helix/pull/13935))
Updated languages and queries:
* Add `ruby-lsp` for Ruby ([#12511](https://github.com/helix-editor/helix/pull/12511))
* Add `wat_server` for Wat ([#12581](https://github.com/helix-editor/helix/pull/12581))
* Recognize `bun.lock` as JSONC ([fcf981b](https://github.com/helix-editor/helix/commit/fcf981b))
* Update tree-sitter-rust ([#12607](https://github.com/helix-editor/helix/pull/12607), [1afa63d](https://github.com/helix-editor/helix/commit/1afa63d))
* Fix configuration of `cs-lsp` ([#12615](https://github.com/helix-editor/helix/pull/12615))
* Add `beancount-language-server` for Beancount ([#12610](https://github.com/helix-editor/helix/pull/12610))
* Update tree-sitter-fish ([#12456](https://github.com/helix-editor/helix/pull/12456))
* Add `fish-lsp` for Fish ([#12456](https://github.com/helix-editor/helix/pull/12456))
* Update tree-sitter-ini ([#12456](https://github.com/helix-editor/helix/pull/12456), [#13088](https://github.com/helix-editor/helix/pull/13088))
* Recognize `hgrc` as INI ([#12456](https://github.com/helix-editor/helix/pull/12456))
* Restrict tagged template injection languages for ECMA languages ([#12217](https://github.com/helix-editor/helix/pull/12217))
* Update tree-sitter-zig ([#11980](https://github.com/helix-editor/helix/pull/11980), [#12708](https://github.com/helix-editor/helix/pull/12708))
* Update tree-sitter-elixir ([8bf9adf](https://github.com/helix-editor/helix/commit/8bf9adf))
* Add `asm-lsp` for Assembly dialects ([#12684](https://github.com/helix-editor/helix/pull/12684))
* Update tree-sitter-just ([#12692](https://github.com/helix-editor/helix/pull/12692), #)
* Update tree-sitter-cairo ([#12712](https://github.com/helix-editor/helix/pull/12712))
* Configure a comment token for Svelte ([#12743](https://github.com/helix-editor/helix/pull/12743))
* Recognize `.sublime-*` files ([#12750](https://github.com/helix-editor/helix/pull/12750))
* Highlight `$` tagged templates as shell commands in ECMA languages ([#12751](https://github.com/helix-editor/helix/pull/12751))
* Add `#'` comment token for R ([#12748](https://github.com/helix-editor/helix/pull/12748))
* Fix module/namespace highlight in Unison ([93fa990](https://github.com/helix-editor/helix/commit/93fa990))
* Add missing `#not-eq?` and `#not-match?` highlights in TSQ ([3824010](https://github.com/helix-editor/helix/commit/3824010))
* Reverse the precedence order of highlight queries ([#9458](https://github.com/helix-editor/helix/pull/9458), [#12777](https://github.com/helix-editor/helix/pull/12777), [#12795](https://github.com/helix-editor/helix/pull/12795), [144a4f4](https://github.com/helix-editor/helix/commit/144a4f4), [e1c26eb](https://github.com/helix-editor/helix/commit/e1c26eb), [e1060a2](https://github.com/helix-editor/helix/commit/e1060a2), [7f41670](https://github.com/helix-editor/helix/commit/7f41670), [#13293](https://github.com/helix-editor/helix/pull/13293))
* Update Rust highlights ([b8bfc44](https://github.com/helix-editor/helix/commit/b8bfc44), [#12871](https://github.com/helix-editor/helix/pull/12871), [#13664](https://github.com/helix-editor/helix/pull/13664))
* Add block comment configuration for PHP ([0ab403d](https://github.com/helix-editor/helix/commit/0ab403d))
* Update Gren highlights ([#12769](https://github.com/helix-editor/helix/pull/12769))
* Remove `ERROR` node highlighting from all highlight queries ([16ff063](https://github.com/helix-editor/helix/commit/16ff063))
* Update tree-sitter-erlang and highlights ([18b9eb9](https://github.com/helix-editor/helix/commit/18b9eb9), [9f3b193](https://github.com/helix-editor/helix/commit/9f3b193), [12139a4](https://github.com/helix-editor/helix/commit/12139a4))
* Update Nix injections ([#12776](https://github.com/helix-editor/helix/pull/12776), [#12774](https://github.com/helix-editor/helix/pull/12774), [#13851](https://github.com/helix-editor/helix/pull/13851))
* Add indent queries for Nix ([#12829](https://github.com/helix-editor/helix/pull/12829))
* Update Markdown highlights ([#12696](https://github.com/helix-editor/helix/pull/12696))
* Recognize `xsl` as XML ([#12834](https://github.com/helix-editor/helix/pull/12834))
* Remove deprecated `typst-lsp` config ([5a66270](https://github.com/helix-editor/helix/commit/5a66270))
* Replace `pkgbuild-language-server` with `termux-language-server` ([c3c9a0d](https://github.com/helix-editor/helix/commit/c3c9a0d))
* Update SQL highlights ([#12837](https://github.com/helix-editor/helix/pull/12837))
* Recognize `mpd` and `smil` as XML ([#12916](https://github.com/helix-editor/helix/pull/12916))
* Add indents and textojbects for Kotlin ([#12925](https://github.com/helix-editor/helix/pull/12925))
* Fix module highlights in Koto ([7e87a36](https://github.com/helix-editor/helix/commit/7e87a36))
* Update language servers for Protobuf ([#12936](https://github.com/helix-editor/helix/pull/12936))
* Add `astro-ls` for Astro ([#12939](https://github.com/helix-editor/helix/pull/12939))
* Fix recognition of "scons*" files as Python ([#12943](https://github.com/helix-editor/helix/pull/12943))
* Update C# queries ([#12948](https://github.com/helix-editor/helix/pull/12948))
* Add comment textojbect to TOML ([#12952](https://github.com/helix-editor/helix/pull/12952))
* Add `starpls` as Starlark language server ([#12958](https://github.com/helix-editor/helix/pull/12958))
* Add `pkl-lsp` for PKL ([#12962](https://github.com/helix-editor/helix/pull/12962))
* Add `kdlfmt` formatter for KDL ([#12967](https://github.com/helix-editor/helix/pull/12967))
* Update CSS highlights ([#12497](https://github.com/helix-editor/helix/pull/12497), [fed3edc](https://github.com/helix-editor/helix/commit/fed3edc))
* Add `harper-ls` ([#13029](https://github.com/helix-editor/helix/pull/13029))
* Change `wgsl_analyzer` to `wgsl-analyzer` ([#13063](https://github.com/helix-editor/helix/pull/13063))
* Update tree-sitter-vhdl ([#13091](https://github.com/helix-editor/helix/pull/13091))
* Update tree-sitter-openscad ([#13033](https://github.com/helix-editor/helix/pull/13033))
* Update Rust injections ([694b615](https://github.com/helix-editor/helix/commit/694b615), [1bd7a39](https://github.com/helix-editor/helix/commit/1bd7a39))
* Update Ruby highlights ([#13055](https://github.com/helix-editor/helix/pull/13055))
* Recognize `gitconfig` as an extension ([#13115](https://github.com/helix-editor/helix/pull/13115))
* Add `///` comment token for Amber ([#13122](https://github.com/helix-editor/helix/pull/13122))
* Add indent queries for Starlark ([#13126](https://github.com/helix-editor/helix/pull/13126))
* Recognize more systemd file types as INI ([#13139](https://github.com/helix-editor/helix/pull/13139))
* Update scheme queries ([#13143](https://github.com/helix-editor/helix/pull/13143))
* Recognize `tmTheme` as XML ([#13202](https://github.com/helix-editor/helix/pull/13202))
* Update `golangci-lint` command for v2 ([#13204](https://github.com/helix-editor/helix/pull/13204))
* Add `just-lsp` for Just ([#13276](https://github.com/helix-editor/helix/pull/13276))
* Add a tree-sitter-prolog grammar ([#11611](https://github.com/helix-editor/helix/pull/11611))
* Fix typos in Ada queries ([#13251](https://github.com/helix-editor/helix/pull/13251))
* Update mint language server args ([#13248](https://github.com/helix-editor/helix/pull/13248))
* Update typescript highlights ([#13250](https://github.com/helix-editor/helix/pull/13250))
* Update tree-sitter-jjdescription ([#13329](https://github.com/helix-editor/helix/pull/13329))
* Add injection queries for Quint ([#13322](https://github.com/helix-editor/helix/pull/13322))
* Update tree-sitter-scss and highlights ([#13414](https://github.com/helix-editor/helix/pull/13414))
* Update tree-sitter-go-mod ([#13395](https://github.com/helix-editor/helix/pull/13395))
* Update tree-sitter-svelte ([#13423](https://github.com/helix-editor/helix/pull/13423))
* Update Lua highlights ([#13401](https://github.com/helix-editor/helix/pull/13401))
* Update Go highlights ([#13425](https://github.com/helix-editor/helix/pull/13425), [25b299a](https://github.com/helix-editor/helix/commit/25b299a), [#13825](https://github.com/helix-editor/helix/pull/13825))
* Recognize `.git-blame-ignore-revs` as gitignore ([#13460](https://github.com/helix-editor/helix/pull/13460))
* Update Verilog highlights ([#13473](https://github.com/helix-editor/helix/pull/13473), [#13493](https://github.com/helix-editor/helix/pull/13493))
* Update tree-sitter-v ([#13469](https://github.com/helix-editor/helix/pull/13469))
* Update WGSL highlights ([#13479](https://github.com/helix-editor/helix/pull/13479))
* Update Bash highlights ([#13477](https://github.com/helix-editor/helix/pull/13477))
* Update tree-sitter-cpp ([#13504](https://github.com/helix-editor/helix/pull/13504))
* Update rust-analyzer config to use server-side file watching ([#13432](https://github.com/helix-editor/helix/pull/13432))
* Update Vue injections ([#13511](https://github.com/helix-editor/helix/pull/13511))
* Recognize `sld` as Scheme ([#13528](https://github.com/helix-editor/helix/pull/13528))
* Recognize more files as git-attributes ([#13540](https://github.com/helix-editor/helix/pull/13540))
* Update tree-sitter-haskell and queries ([#13475](https://github.com/helix-editor/helix/pull/13475))
* Align INI highlights with TOML ([#13589](https://github.com/helix-editor/helix/pull/13589))
* Add tree-sitter-rust-format-args for `format_args!` injections in Rust ([#13533](https://github.com/helix-editor/helix/pull/13533), [#13657](https://github.com/helix-editor/helix/pull/13657), [4dd4ba7](https://github.com/helix-editor/helix/commit/4dd4ba7), [86f10ae](https://github.com/helix-editor/helix/commit/86f10ae))
* Update Ungrammar highlights ([8d58f6c](https://github.com/helix-editor/helix/commit/8d58f6c))
* Add `ty` language server for Python ([#13643](https://github.com/helix-editor/helix/pull/13643))
* Add `clarinet` language server for Clarity ([#13647](https://github.com/helix-editor/helix/pull/13647))
* Update prisma config to avoid a crash in the language server ([f6878f6](https://github.com/helix-editor/helix/commit/f6878f6))
* Add `pyrefly` for Python ([#13713](https://github.com/helix-editor/helix/pull/13713))
* Update Python highlights ([#13715](https://github.com/helix-editor/helix/pull/13715))
* Update Mojo language server and formatter to `pixi` ([#13648](https://github.com/helix-editor/helix/pull/13648))
* Add `tombi` for TOML ([#13723](https://github.com/helix-editor/helix/pull/13723))
* Add `neocmakelsp` for CMake ([#13740](https://github.com/helix-editor/helix/pull/13740))
* Update C and C++ highlights ([#13747](https://github.com/helix-editor/helix/pull/13747), [#13772](https://github.com/helix-editor/helix/pull/13772))
* Highlight escape sequences in ECMA languages ([#13762](https://github.com/helix-editor/helix/pull/13762))
* Add an external formatter config for Crystal ([#13759](https://github.com/helix-editor/helix/pull/13759))
* Add `amber-lsp` for Amber ([#13763](https://github.com/helix-editor/helix/pull/13763))
* Update HTML highlights ([#13753](https://github.com/helix-editor/helix/pull/13753))
* Update tree-sitter-purescript and highlights ([#13782](https://github.com/helix-editor/helix/pull/13782))
* Update tree-sitter-gleam and highlights ([#13793](https://github.com/helix-editor/helix/pull/13793), [#13807](https://github.com/helix-editor/helix/pull/13807), [#13813](https://github.com/helix-editor/helix/pull/13813))
* Recognize Buck files as Starlark ([#13810](https://github.com/helix-editor/helix/pull/13810))
* Use tree-sitter-crystal instead of tree-sitter-ruby for Crystal and add custom queries ([#13805](https://github.com/helix-editor/helix/pull/13805))
* Update tree-sitter-twig ([#13689](https://github.com/helix-editor/helix/pull/13689))
* Recognize `jsconfig.json` as JSONC, use as JavaScript and JSX roots ([#13822](https://github.com/helix-editor/helix/pull/13822))
* Recognize `.gem/credentials` as YAML ([#13843](https://github.com/helix-editor/helix/pull/13843))
* Update Dockerfile injections ([#13845](https://github.com/helix-editor/helix/pull/13845), 13852)
* Change tree-sitter parser for Git commit message files ([44293df](https://github.com/helix-editor/helix/commit/44293df))
* Recognize `mimeapps.list` as INI ([#13850](https://github.com/helix-editor/helix/pull/13850))
* Update tree-sitter-odin, highlights and indents ([#13877](https://github.com/helix-editor/helix/pull/13877), [#13917](https://github.com/helix-editor/helix/pull/13917))
* Add locals queries for C, improve parameter highlighting ([#13876](https://github.com/helix-editor/helix/pull/13876))
* Add textobjects for QML ([#13855](https://github.com/helix-editor/helix/pull/13855))
* Add comment tokens for DTD ([#13904](https://github.com/helix-editor/helix/pull/13904))
* Add `dts-lsp` for DeviceTree ([#13907](https://github.com/helix-editor/helix/pull/13907))
* Update gomod highlights ([#13913](https://github.com/helix-editor/helix/pull/13913))
* Recognize `compose.yaml` and `compose.yml` as Docker Compose ([#13930](https://github.com/helix-editor/helix/pull/13930))
Packaging:
* Fix handling of spaces in Bash completion ([#12828](https://github.com/helix-editor/helix/pull/12828))
* Refactor Nix flake ([#12831](https://github.com/helix-editor/helix/pull/12831), [#13024](https://github.com/helix-editor/helix/pull/13024), [cb1ecc9](https://github.com/helix-editor/helix/commit/cb1ecc9), [#13305](https://github.com/helix-editor/helix/pull/13305))
* Add `ConsoleOnly` to `Helix.desktop` categories ([#13236](https://github.com/helix-editor/helix/pull/13236))
* Drop Nix flake dependency on flake-utils ([60a03a3](https://github.com/helix-editor/helix/commit/60a03a3))
* Increase the MSRV to 1.82 ([#13275](https://github.com/helix-editor/helix/pull/13275))
# 25.01.1 (2025-01-19)
25.01.1 is a patch release focusing on fixing bugs and panics from changes in 25.01.

759
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,23 +37,27 @@ 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.19.1"
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"
toml = "0.9"
[workspace.package]
version = "25.1.1"
version = "25.7.0"
edition = "2021"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
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

@@ -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`
@@ -182,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

View File

@@ -47,6 +47,10 @@ The following variables are supported:
| `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:

View File

@@ -19,6 +19,7 @@
- [`[editor.soft-wrap]` Section](#editorsoft-wrap-section)
- [`[editor.smart-tab]` Section](#editorsmart-tab-section)
- [`[editor.inline-diagnostics]` Section](#editorinline-diagnostics-section)
- [`[editor.word-completion]` Section](#editorword-completion-section)
### `[editor]` Section
@@ -53,6 +54,7 @@
| `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` |
@@ -104,6 +106,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:
@@ -116,6 +120,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:
@@ -129,12 +135,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 |
@@ -152,6 +159,7 @@ 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` |
@@ -470,3 +478,21 @@ end-of-line-diagnostics = "hint"
[editor.inline-diagnostics]
cursor-line = "warning" # show warnings and errors on the cursorline inline
```
### `[editor.word-completion]` Section
Options for controlling completion of words from open buffers.
| Key | Description | Default |
| --- | --- | --- |
| `enable` | Whether word completion is enabled | `true` |
| `trigger-length` | Number of word characters to type before triggering completion | `7` |
Example:
```toml
[editor.word-completion]
enable = true
# Set the trigger length lower so that words are completed more often
trigger-length = 4
```

View File

@@ -3,7 +3,8 @@
| ada | ✓ | ✓ | | `ada_language_server` |
| adl | ✓ | ✓ | ✓ | |
| agda | ✓ | | | |
| amber | ✓ | | | |
| alloy | ✓ | | | |
| amber | ✓ | | | `amber-lsp` |
| astro | ✓ | | | `astro-ls` |
| awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | ✓ | ✓ | `bash-language-server` |
@@ -17,18 +18,20 @@
| 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 | ✓ | ✓ | ✓ | `cmake-language-server` |
| cmake | ✓ | ✓ | ✓ | `neocmakelsp`, `cmake-language-server` |
| codeql | ✓ | ✓ | | `codeql` |
| comment | ✓ | | | |
| common-lisp | ✓ | | ✓ | `cl-lsp` |
| cpon | ✓ | | ✓ | |
| cpp | ✓ | ✓ | ✓ | `clangd` |
| crystal | ✓ | ✓ | | `crystalline` |
| crystal | ✓ | ✓ | | `crystalline`, `ameba-ls` |
| css | ✓ | | ✓ | `vscode-css-language-server` |
| csv | ✓ | | | |
| cue | ✓ | | | `cuelsp` |
@@ -36,7 +39,8 @@
| d | ✓ | ✓ | ✓ | `serve-d` |
| dart | ✓ | ✓ | ✓ | `dart` |
| dbml | ✓ | | | |
| devicetree | ✓ | | | |
| debian | ✓ | | | |
| devicetree | ✓ | | | `dts-lsp` |
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
| diff | ✓ | | | |
| djot | ✓ | | | |
@@ -45,6 +49,7 @@
| dot | ✓ | | | `dot-language-server` |
| dtd | ✓ | | | |
| dune | ✓ | | | |
| dunstrc | ✓ | | | |
| earthfile | ✓ | ✓ | ✓ | `earthlyls` |
| edoc | ✓ | | | |
| eex | ✓ | | | |
@@ -57,6 +62,7 @@
| erb | ✓ | | | |
| erlang | ✓ | ✓ | | `erlang_ls`, `elp` |
| esdl | ✓ | | | |
| fennel | ✓ | | | `fennel-ls` |
| fga | ✓ | ✓ | ✓ | |
| fidl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | `fish-lsp` |
@@ -72,6 +78,7 @@
| git-commit | ✓ | ✓ | | |
| git-config | ✓ | ✓ | | |
| git-ignore | ✓ | | | |
| git-notes | ✓ | | | |
| git-rebase | ✓ | | | |
| gjs | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| gleam | ✓ | ✓ | | `gleam` |
@@ -97,7 +104,8 @@
| hocon | ✓ | ✓ | ✓ | |
| hoon | ✓ | | | |
| hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server`, `superhtml` |
| html | ✓ | | | `vscode-html-language-server`, `superhtml` |
| htmldjango | ✓ | | | `djlsp`, `vscode-html-language-server`, `superhtml` |
| hurl | ✓ | ✓ | ✓ | |
| hyprlang | ✓ | | ✓ | `hyprls` |
| idris | | | | `idris2-lsp` |
@@ -109,16 +117,20 @@
| java | ✓ | ✓ | ✓ | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | |
| jjconfig | ✓ | ✓ | ✓ | `taplo`, `tombi` |
| jjdescription | ✓ | | | |
| jjrevset | ✓ | | | |
| jjtemplate | ✓ | | | |
| 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 | ✓ | ✓ | ✓ | `just-lsp` |
| kdl | ✓ | ✓ | ✓ | |
| koka | ✓ | | ✓ | `koka` |
| kotlin | ✓ | ✓ | ✓ | `kotlin-language-server` |
@@ -134,16 +146,18 @@
| 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 | ✓ | ✓ | ✓ | `magic` |
| mojo | ✓ | ✓ | ✓ | `pixi` |
| move | ✓ | | | |
| msbuild | ✓ | | ✓ | |
| nasm | ✓ | ✓ | | `asm-lsp` |
@@ -175,12 +189,15 @@
| ponylang | ✓ | ✓ | ✓ | |
| powershell | ✓ | | | |
| prisma | ✓ | ✓ | | `prisma-language-server` |
| prolog | | | | `swipl` |
| prolog | | | | `swipl` |
| properties | ✓ | ✓ | | |
| protobuf | ✓ | ✓ | ✓ | `buf`, `pb`, `protols` |
| prql | ✓ | | | |
| pug | ✓ | | | |
| purescript | ✓ | ✓ | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `ruff`, `jedi-language-server`, `pylsp` |
| qml | ✓ | | ✓ | `qmlls` |
| python | ✓ | ✓ | ✓ | `ty`, `ruff`, `jedi-language-server`, `pylsp` |
| qml | ✓ | | ✓ | `qmlls` |
| quarto | ✓ | | ✓ | |
| quint | ✓ | | | `quint-language-server` |
| r | ✓ | | | `R` |
| racket | ✓ | | ✓ | `racket` |
@@ -193,10 +210,13 @@
| 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` |
@@ -214,6 +234,7 @@
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | ✓ | | `sourcekit-lsp` |
| systemd | ✓ | | | `systemd-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | |
| tact | ✓ | ✓ | ✓ | |
@@ -227,7 +248,7 @@
| thrift | ✓ | | | |
| tlaplus | ✓ | | | |
| todotxt | ✓ | | | |
| toml | ✓ | ✓ | | `taplo` |
| toml | ✓ | ✓ | | `taplo`, `tombi` |
| tsq | ✓ | | | `ts_query_ls` |
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| twig | ✓ | | | |
@@ -248,11 +269,12 @@
| wat | ✓ | | | `wat_server` |
| webc | ✓ | | | |
| werk | ✓ | | | |
| wesl | ✓ | ✓ | | |
| wgsl | ✓ | | | `wgsl-analyzer` |
| wit | ✓ | | ✓ | |
| wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | |
| xml | ✓ | | ✓ | |
| xml | ✓ | | ✓ | |
| xtc | ✓ | | | |
| yaml | ✓ | ✓ | ✓ | `yaml-language-server`, `ansible-language-server` |
| yara | ✓ | | | `yls` |

View File

@@ -126,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 `` |
@@ -139,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 `` |
@@ -150,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> `` |
@@ -262,6 +267,8 @@
| `goto_prev_comment` | Goto previous comment | normal: `` [c ``, select: `` [c `` |
| `goto_next_test` | Goto next test | normal: `` ]T ``, select: `` ]T `` |
| `goto_prev_test` | Goto previous test | normal: `` [T ``, select: `` [T `` |
| `goto_next_xml_element` | Goto next (X)HTML element | normal: `` ]x ``, select: `` ]x `` |
| `goto_prev_xml_element` | Goto previous (X)HTML element | normal: `` [x ``, select: `` [x `` |
| `goto_next_entry` | Goto next pairing | normal: `` ]e ``, select: `` ]e `` |
| `goto_prev_entry` | Goto previous pairing | normal: `` [e ``, select: `` [e `` |
| `goto_next_paragraph` | Goto next paragraph | normal: `` ]p ``, select: `` ]p `` |
@@ -296,5 +303,7 @@
| `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 | |

View File

@@ -78,9 +78,9 @@
| `: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 |

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

View File

@@ -23,10 +23,13 @@ The following [captures][tree-sitter-captures] are recognized:
| `test.inside` |
| `test.around` |
| `parameter.inside` |
| `parameter.around` |
| `comment.inside` |
| `comment.around` |
| `entry.inside` |
| `entry.around` |
| `xml-element.inside` |
| `xml-element.around` |
[Example query files][textobject-examples] can be found in the helix GitHub repository.

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` |
@@ -347,30 +348,32 @@ Displays the signature of the selected completion item. Remapping currently not
These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
| Key | Description | Command |
| ----- | ----------- | ------- |
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
| `]f` | Go to next function (**TS**) | `goto_next_function` |
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]T` | Go to next test (**TS**) | `goto_next_test` |
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
| `]p` | Go to next paragraph | `goto_next_paragraph` |
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
| `]g` | Go to next change | `goto_next_change` |
| `[g` | Go to previous change | `goto_prev_change` |
| `]G` | Go to last change | `goto_last_change` |
| `[G` | Go to first change | `goto_first_change` |
| `]Space` | Add newline below | `add_newline_below` |
| `[Space` | Add newline above | `add_newline_above` |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
| `]f` | Go to next function (**TS**) | `goto_next_function` |
| `[f` | Go to previous function (**TS**) | `goto_prev_function` |
| `]t` | Go to next type definition (**TS**) | `goto_next_class` |
| `[t` | Go to previous type definition (**TS**) | `goto_prev_class` |
| `]a` | Go to next argument/parameter (**TS**) | `goto_next_parameter` |
| `[a` | Go to previous argument/parameter (**TS**) | `goto_prev_parameter` |
| `]c` | Go to next comment (**TS**) | `goto_next_comment` |
| `[c` | Go to previous comment (**TS**) | `goto_prev_comment` |
| `]T` | Go to next test (**TS**) | `goto_next_test` |
| `[T` | Go to previous test (**TS**) | `goto_prev_test` |
| `]p` | Go to next paragraph | `goto_next_paragraph` |
| `[p` | Go to previous paragraph | `goto_prev_paragraph` |
| `]g` | Go to next change | `goto_next_change` |
| `[g` | Go to previous change | `goto_prev_change` |
| `]G` | Go to last change | `goto_last_change` |
| `[G` | Go to first change | `goto_first_change` |
| `[x` | Go to next (X)HTML element | `goto_next_xml_element` |
| `]x` | Go to previous (X)HTML element | `goto_prev_xml_element` |
| `]Space` | Add newline below | `add_newline_below` |
| `[Space` | Add newline above | `add_newline_above` |
## Insert mode

View File

@@ -66,11 +66,12 @@ 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 |
| `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. |
| `word-completion` | Overrides the [`editor.word-completion`](./editor.md#editorword-completion-section) configuration 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.
@@ -102,6 +103,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

@@ -10,6 +10,7 @@ Helix' keymap and interaction model ([Using Helix](#usage.md)) is easier to adop
| --- | --- | --- |
| [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)) |
@@ -22,7 +23,7 @@ Helix' keymap and interaction model ([Using Helix](#usage.md)) is easier to adop
| --- | ---
| 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)
| 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

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

@@ -24,6 +24,7 @@ function or block of code.
| `c` | Comment |
| `T` | Test |
| `g` | Change |
| `x` | (X)HTML element |
> 💡 `f`, `t`, etc. need a tree-sitter grammar active for the current
document and a special tree-sitter query file to work properly. [Only

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

@@ -47,6 +47,9 @@
<content_rating type="oars-1.1" />
<releases>
<release version="25.07" date="2025-07-15">
<url>https://helix-editor.com/news/release-25-07-highlights/</url>
</release>
<release version="25.01.1" date="2025-01-19">
<url>https://github.com/helix-editor/helix/releases/tag/25.01.1</url>
</release>

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

@@ -5,7 +5,7 @@
# 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 }
let languages = ^hx --health languages | detect columns | get Language | where { $in != null }
let completions = [ "all", "clipboard", "languages" ] | append $languages
return $completions
}

View File

@@ -6,7 +6,8 @@
installShellFiles,
git,
gitRev ? null,
...
grammarOverlays ? [],
includeGrammarIf ? _: true,
}: let
fs = lib.fileset;
@@ -28,7 +29,7 @@
# 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 {};
grammars = callPackage ./grammars.nix {inherit grammarOverlays includeGrammarIf;};
runtimeDir = runCommand "helix-runtime" {} ''
mkdir -p $out
ln -s ${./runtime}/* $out
@@ -75,9 +76,9 @@ in
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
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
cp ${./contrib/helix.png} $out/share/icons/hicolor/256x256/apps/helix.png
'';
meta.mainProgram = "hx";

View File

@@ -13,7 +13,7 @@ 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.

34
flake.lock generated
View File

@@ -1,23 +1,5 @@
{
"nodes": {
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1731533236,
"narHash": "sha256-l0KFg5HjrsfsO/JpG+r7fRrqm12kzFHyUHqHCVpMMbI=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "11707dc2f618dd54ca8739b309ec4fc024de578b",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1740560979,
@@ -36,7 +18,6 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
@@ -60,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",

131
flake.nix
View File

@@ -3,7 +3,6 @@
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";
@@ -13,77 +12,89 @@
outputs = {
self,
nixpkgs,
flake-utils,
rust-overlay,
...
}: 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
flake-utils.lib.eachDefaultSystem (system: let
pkgs = import nixpkgs {
inherit system;
overlays = [(import rust-overlay)];
};
in {
packages = eachSystem (system: {
inherit (pkgsFor.${system}) helix;
/*
The default Helix build. Uses the latest stable Rust toolchain, and unstable
nixpkgs.
# 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;
};
in {
packages = rec {
helix = pkgs.callPackage ./default.nix {inherit gitRev;};
The build inputs can be overridden with the following:
/**
The default Helix build. Uses the latest stable Rust toolchain, and unstable
nixpkgs.
The build inputs can be overriden 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 = helix;
};
packages.${system}.default.override { rustPlatform = newPlatform; };
checks.helix = self.outputs.packages.${system}.helix.override {
buildType = "debug";
rustPlatform = msrvPlatform;
};
Overriding a derivation attribute can be done as well:
# Devshell behavior is preserved.
devShells.default = let
commonRustFlagsEnv = "-C link-arg=-fuse-ld=lld -C target-cpu=native --cfg tokio_unstable";
platformRustFlagsEnv = pkgs.lib.optionalString pkgs.stdenv.isLinux "-Clink-arg=-Wl,--no-rosegment";
in
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}"
'';
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;
};
})
// {
overlays.default = final: prev: {
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
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}"
'';
};
})
pkgsFor;
overlays = {
helix = final: prev: {
helix = final.callPackage ./default.nix {inherit gitRev;};
};
};
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

View File

@@ -21,7 +21,7 @@ helix-loader = { path = "../helix-loader" }
helix-parsec = { path = "../helix-parsec" }
ropey.workspace = true
smallvec = "1.14"
smallvec = "1.15"
smartstring = "1.0.1"
unicode-segmentation.workspace = true
# unicode-width is changing width definitions
@@ -32,23 +32,21 @@ unicode-segmentation.workspace = true
unicode-width = "=0.1.12"
unicode-general-category = "1.0"
slotmap.workspace = true
tree-sitter.workspace = true
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"] }
foldhash.workspace = true
url = "2.5.4"
log = "0.4"
anyhow = "1.0"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
imara-diff = "0.1.8"
toml.workspace = true
imara-diff = "0.2.0"
encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }

View File

@@ -357,7 +357,7 @@ pub struct Token<'a> {
pub is_terminated: bool,
}
impl Token<'_> {
impl<'a> Token<'a> {
pub fn empty_at(content_start: usize) -> Self {
Self {
kind: TokenKind::Unquoted,
@@ -366,6 +366,15 @@ impl Token<'_> {
is_terminated: false,
}
}
pub fn expand(content: impl Into<Cow<'a, str>>) -> Self {
Self {
kind: TokenKind::Expand,
content_start: 0,
content: content.into(),
is_terminated: true,
}
}
}
#[derive(Debug)]

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;

View File

@@ -16,6 +16,7 @@ pub struct CompletionItem {
pub enum CompletionProvider {
Lsp(LanguageServerId),
Path,
Word,
}
impl From<LanguageServerId> for CompletionProvider {

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,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

@@ -242,34 +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!(),
}
}
}
/// 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, 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.
@@ -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,16 +603,16 @@ 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.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.is_some_and(|pos| node.end_byte() > pos) {
node_line += 1;
@@ -471,175 +621,96 @@ fn get_node_end_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
}
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

@@ -53,7 +53,7 @@ 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 position::{
char_idx_at_visual_offset, coords_at_pos, pos_at_coords, softwrapped_dimensions,
@@ -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

@@ -89,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

View File

@@ -9,13 +9,13 @@ use crate::{
},
line_ending::get_line_ending,
movement::Direction,
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)
}

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)

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,627 @@
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},
num::NonZeroU8,
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>,
/// If set, overrides `editor.word-completion`.
pub word_completion: Option<WordCompletion>,
#[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>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(default, rename_all = "kebab-case", deny_unknown_fields)]
pub struct WordCompletion {
pub enable: Option<bool>,
pub trigger_length: Option<NonZeroU8>,
}
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

@@ -19,6 +19,16 @@ pub enum Operation {
Insert(Tendril),
}
impl Operation {
/// The number of characters affected by the operation.
pub fn len_chars(&self) -> usize {
match self {
Self::Retain(n) | Self::Delete(n) => *n,
Self::Insert(s) => s.chars().count(),
}
}
}
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Assoc {
Before,

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)?;
@@ -162,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)
@@ -178,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
}
@@ -354,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,8 +1,9 @@
mod client;
pub mod registry;
mod transport;
mod types;
pub use client::{Client, ConnectionType};
pub use client::Client;
pub use transport::{Payload, Response, Transport};
pub use types::*;
@@ -31,6 +32,7 @@ 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 {
@@ -40,6 +42,7 @@ impl Request {
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),
};

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, 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::{
@@ -52,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>>>>,
}
@@ -61,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();
@@ -73,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));
@@ -83,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);
};
@@ -117,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?)
}
@@ -163,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
@@ -178,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)))
@@ -204,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",
@@ -224,12 +229,12 @@ 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(())
}
@@ -237,17 +242,26 @@ impl Transport {
}
async fn recv(
id: DebugAdapterId,
transport: Arc<Self>,
mut server_stdout: Box<dyn AsyncBufRead + Unpin + Send>,
client_tx: UnboundedSender<Payload>,
) {
let mut recv_buffer = String::new();
let mut content_buffer = Vec::new();
loop {
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
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!("err: <- {err:?}");
error!(" [{id}] err: <- {err:?}");
break;
}
},

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
@@ -992,6 +1022,13 @@ pub mod events {
}
}
#[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"}"#;

View File

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

View File

@@ -19,9 +19,8 @@ helix-stdx = { path = "../helix-stdx" }
anyhow = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.8"
toml.workspace = true
etcetera = "0.10"
tree-sitter.workspace = true
once_cell = "1.21"
log = "0.4"
@@ -32,5 +31,4 @@ cc = { version = "1" }
threadpool = { version = "1.0" }
tempfile.workspace = true
[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<()> {
@@ -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()))

View File

@@ -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 {

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

@@ -19,14 +19,14 @@ helix-loader = { path = "../helix-loader" }
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 }
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.44", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.17"
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

View File

@@ -10,7 +10,7 @@ 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;
@@ -39,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,
@@ -176,6 +176,29 @@ impl Client {
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)]
pub fn start(
cmd: &str,
@@ -201,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();
@@ -356,7 +380,14 @@ impl Client {
capabilities.inlay_hint_provider,
Some(OneOf::Left(true) | OneOf::Right(InlayHintServerCapabilities::Options(_)))
),
LanguageServerFeature::DocumentColors => capabilities.color_provider.is_some(),
LanguageServerFeature::DocumentColors => matches!(
capabilities.color_provider,
Some(
ColorProviderCapability::Simple(true)
| ColorProviderCapability::ColorProvider(_)
| ColorProviderCapability::Options(_)
)
),
}
}
@@ -1160,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,
@@ -1202,6 +1217,8 @@ impl Client {
_ => return None,
};
let options = self.get_merged_formatting_options(options);
let params = lsp::DocumentRangeFormattingParams {
text_document,
range,

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;
@@ -733,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),
}
}
}
@@ -777,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
@@ -801,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)
}
});
}
}

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?)
}
@@ -255,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

@@ -15,7 +15,7 @@ homepage.workspace = true
dunce = "1.0"
etcetera = "0.10"
ropey.workspace = true
which = "7.0"
which = "8.0"
regex-cursor = "0.1.5"
bitflags.workspace = true
once_cell = "1.21"
@@ -23,7 +23,7 @@ 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 = "1.0", features = ["fs"] }

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;

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,3 +1,4 @@
//! Functions and types for working with [RopeSlice]
use std::fmt;
use std::ops::{Bound, RangeBounds};
@@ -8,6 +9,7 @@ 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;
@@ -135,7 +137,9 @@ pub trait RopeSliceExt<'a>: Sized {
/// let graphemes: Vec<_> = text.graphemes().collect();
/// assert_eq!(graphemes.as_slice(), &["😶‍🌫️", "🏴‍☠️", "🖼️"]);
/// ```
fn graphemes(self) -> RopeGraphemes<'a>;
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
@@ -150,7 +154,127 @@ pub trait RopeSliceExt<'a>: Sized {
/// let graphemes: Vec<_> = text.graphemes_rev().collect();
/// assert_eq!(graphemes.as_slice(), &["🖼️", "🏴‍☠️", "😶‍🌫️"]);
/// ```
fn graphemes_rev(self) -> RevRopeGraphemes<'a>;
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> {
@@ -335,31 +459,111 @@ impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
}
}
fn graphemes(self) -> RopeGraphemes<'a> {
let mut chunks = self.chunks();
let first_chunk = chunks.next().unwrap_or("");
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,
cur_chunk: first_chunk,
cur_chunk_start: 0,
cursor: GraphemeCursor::new(0, self.len_bytes(), true),
current_chunk,
chunk_byte_idx,
cursor: GraphemeCursor::new(byte_idx, self.len_bytes(), true),
is_reversed: false,
}
}
fn graphemes_rev(self) -> RevRopeGraphemes<'a> {
let (mut chunks, mut cur_chunk_start, _, _) = self.chunks_at_byte(self.len_bytes());
chunks.reverse();
let first_chunk = chunks.next().unwrap_or("");
cur_chunk_start -= first_chunk.len();
RevRopeGraphemes {
text: self,
chunks,
cur_chunk: first_chunk,
cur_chunk_start,
cursor: GraphemeCursor::new(self.len_bytes(), self.len_bytes(), true),
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
}
}
// copied from std
@@ -370,13 +574,19 @@ const fn is_utf8_char_boundary(b: u8) -> bool {
}
/// 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>,
cur_chunk: &'a str,
cur_chunk_start: usize,
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<'_> {
@@ -384,112 +594,178 @@ impl fmt::Debug for RopeGraphemes<'_> {
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("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> {
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!(),
}
}
RopeGraphemes::next(self)
}
}
if a < self.cur_chunk_start {
Some(self.text.byte_slice(a..b))
/// 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 {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[a2..b2]).into())
self.next_impl()
}
}
}
/// 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,
}
pub fn prev(&mut self) -> Option<(usize, RopeSlice<'a>)> {
if self.is_reversed {
self.next_impl()
} else {
self.prev_impl()
}
}
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()
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 RevRopeGraphemes<'a> {
type Item = RopeSlice<'a>;
impl<'a> Iterator for RopeGraphemeIndices<'a> {
type Item = (usize, RopeSlice<'a>);
fn next(&mut self) -> Option<Self::Item> {
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())
}
RopeGraphemeIndices::next(self)
}
}

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;

View File

@@ -61,7 +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.8"
indexmap = "2.10"
# Logging
fern = "0.7"
@@ -82,7 +82,7 @@ open = "5.3.2"
url = "2.5.4"
# config
toml = "0.8"
toml.workspace = true
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
@@ -93,7 +93,7 @@ grep-searcher = "0.1.14"
[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.171"
libc = "0.2.174"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc"] }
@@ -102,7 +102,7 @@ crossterm = { version = "0.28", features = ["event-stream", "use-dev-tty", "libc
helix-loader = { path = "../helix-loader" }
[dev-dependencies]
smallvec = "1.14"
smallvec = "1.15"
indoc = "2.0.6"
tempfile.workspace = true
same-file = "1.0.1"

View File

@@ -356,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(),
@@ -374,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;
@@ -384,31 +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.editor.syn_loader.store(Arc::new(lang_loader));
for document in self.editor.documents.values_mut() {
document.detect_language(self.editor.syn_loader.clone());
let diagnostics = Editor::doc_diagnostics(
&self.editor.language_servers,
&self.editor.diagnostics,
document,
);
document.replace_diagnostics(diagnostics, &[], None);
}
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()?;
// Refresh theme after config change
// 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
@@ -570,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],
));
}
@@ -601,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;
}
@@ -756,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,
@@ -787,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) => {

View File

@@ -34,7 +34,7 @@ use helix_core::{
regex::{self, Regex},
search::{self, CharMatcher},
selection, surround,
syntax::{BlockCommentToken, LanguageServerFeature},
syntax::config::{BlockCommentToken, LanguageServerFeature},
text_annotations::{Overlay, TextAnnotations},
textobject,
unicode::width::UnicodeWidthChar,
@@ -426,6 +426,8 @@ impl MappableCommand {
goto_implementation, "Goto implementation",
goto_file_start, "Goto line number <n> else file start",
goto_file_end, "Goto file end",
extend_to_file_start, "Extend to line number<n> else file start",
extend_to_file_end, "Extend to file end",
goto_file, "Goto files/URLs in selections",
goto_file_hsplit, "Goto files in selections (hsplit)",
goto_file_vsplit, "Goto files in selections (vsplit)",
@@ -438,6 +440,7 @@ impl MappableCommand {
goto_last_modification, "Goto last modification",
goto_line, "Goto line",
goto_last_line, "Goto last line",
extend_to_last_line, "Extend to last line",
goto_first_diag, "Goto first diagnostic",
goto_last_diag, "Goto last diagnostic",
goto_next_diag, "Goto next diagnostic",
@@ -448,6 +451,8 @@ impl MappableCommand {
goto_last_change, "Goto last change",
goto_line_start, "Goto line start",
goto_line_end, "Goto line end",
goto_column, "Goto column",
extend_to_column, "Extend to column",
goto_next_buffer, "Goto next buffer",
goto_previous_buffer, "Goto previous buffer",
goto_line_end_newline, "Goto newline at line end",
@@ -560,6 +565,8 @@ impl MappableCommand {
goto_prev_comment, "Goto previous comment",
goto_next_test, "Goto next test",
goto_prev_test, "Goto previous test",
goto_next_xml_element, "Goto next (X)HTML element",
goto_prev_xml_element, "Goto previous (X)HTML element",
goto_next_entry, "Goto next pairing",
goto_prev_entry, "Goto previous pairing",
goto_next_paragraph, "Goto next paragraph",
@@ -594,8 +601,10 @@ impl MappableCommand {
command_palette, "Open command palette",
goto_word, "Jump to a two-character label",
extend_to_word, "Extend to a two-character label",
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",
);
}
@@ -1253,28 +1262,44 @@ fn goto_next_paragraph(cx: &mut Context) {
}
fn goto_file_start(cx: &mut Context) {
goto_file_start_impl(cx, Movement::Move);
}
fn extend_to_file_start(cx: &mut Context) {
goto_file_start_impl(cx, Movement::Extend);
}
fn goto_file_start_impl(cx: &mut Context, movement: Movement) {
if cx.count.is_some() {
goto_line(cx);
goto_line_impl(cx, movement);
} else {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc
.selection(view.id)
.clone()
.transform(|range| range.put_cursor(text, 0, cx.editor.mode == Mode::Select));
.transform(|range| range.put_cursor(text, 0, movement == Movement::Extend));
push_jump(view, doc);
doc.set_selection(view.id, selection);
}
}
fn goto_file_end(cx: &mut Context) {
goto_file_end_impl(cx, Movement::Move);
}
fn extend_to_file_end(cx: &mut Context) {
goto_file_end_impl(cx, Movement::Extend)
}
fn goto_file_end_impl(cx: &mut Context, movement: Movement) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let pos = doc.text().len_chars();
let selection = doc
.selection(view.id)
.clone()
.transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select));
.transform(|range| range.put_cursor(text, pos, movement == Movement::Extend));
push_jump(view, doc);
doc.set_selection(view.id, selection);
}
@@ -3482,12 +3507,12 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
enter_insert_mode(cx);
let (view, doc) = current!(cx.editor);
let loader = cx.editor.syn_loader.load();
let text = doc.text().slice(..);
let contents = doc.text();
let selection = doc.selection(view.id);
let language_config = doc.language_config();
let syntax = doc.syntax();
let tab_width = doc.tab_width();
@@ -3503,7 +3528,7 @@ fn insert_with_indent(cx: &mut Context, cursor_fallback: IndentFallbackPos) {
let line_end_index = cursor_line_start;
let indent = indent::indent_for_newline(
language_config,
&loader,
syntax,
&doc.config.load().indent_heuristic,
&doc.indent_style,
@@ -3613,6 +3638,7 @@ fn open(cx: &mut Context, open: Open, comment_continuation: CommentContinuation)
enter_insert_mode(cx);
let config = cx.editor.config();
let (view, doc) = current!(cx.editor);
let loader = cx.editor.syn_loader.load();
let text = doc.text().slice(..);
let contents = doc.text();
@@ -3662,7 +3688,7 @@ fn open(cx: &mut Context, open: Open, comment_continuation: CommentContinuation)
let indent = match line.first_non_whitespace_char() {
Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
_ => indent::indent_for_newline(
doc.language_config(),
&loader,
doc.syntax(),
&config.indent_heuristic,
&doc.indent_style,
@@ -3702,11 +3728,13 @@ fn open(cx: &mut Context, open: Open, comment_continuation: CommentContinuation)
.map(|token| token.len() + 1) // `+ 1` for the extra space added
.unwrap_or_default();
for i in 0..count {
// pos -> beginning of reference line,
// + (i * (1+indent_len + comment_len)) -> beginning of i'th line from pos (possibly including comment token)
// pos -> beginning of reference line,
// + (i * (line_ending_len + indent_len + comment_len)) -> beginning of i'th line from pos (possibly including comment token)
// + indent_len + comment_len -> -> indent for i'th line
ranges.push(Range::point(
pos + (i * (1 + indent_len + comment_len)) + indent_len + comment_len,
pos + (i * (doc.line_ending.len_chars() + indent_len + comment_len))
+ indent_len
+ comment_len,
));
}
@@ -3740,21 +3768,30 @@ fn normal_mode(cx: &mut Context) {
}
// Store a jump on the jumplist.
fn push_jump(view: &mut View, doc: &Document) {
fn push_jump(view: &mut View, doc: &mut Document) {
doc.append_changes_to_history(view);
let jump = (doc.id(), doc.selection(view.id).clone());
view.jumps.push(jump);
}
fn goto_line(cx: &mut Context) {
goto_line_impl(cx, Movement::Move);
}
fn goto_line_impl(cx: &mut Context, movement: Movement) {
if cx.count.is_some() {
let (view, doc) = current!(cx.editor);
push_jump(view, doc);
goto_line_without_jumplist(cx.editor, cx.count);
goto_line_without_jumplist(cx.editor, cx.count, movement);
}
}
fn goto_line_without_jumplist(editor: &mut Editor, count: Option<NonZeroUsize>) {
fn goto_line_without_jumplist(
editor: &mut Editor,
count: Option<NonZeroUsize>,
movement: Movement,
) {
if let Some(count) = count {
let (view, doc) = current!(editor);
let text = doc.text().slice(..);
@@ -3769,13 +3806,21 @@ fn goto_line_without_jumplist(editor: &mut Editor, count: Option<NonZeroUsize>)
let selection = doc
.selection(view.id)
.clone()
.transform(|range| range.put_cursor(text, pos, editor.mode == Mode::Select));
.transform(|range| range.put_cursor(text, pos, movement == Movement::Extend));
doc.set_selection(view.id, selection);
}
}
fn goto_last_line(cx: &mut Context) {
goto_last_line_impl(cx, Movement::Move)
}
fn extend_to_last_line(cx: &mut Context) {
goto_last_line_impl(cx, Movement::Extend)
}
fn goto_last_line_impl(cx: &mut Context, movement: Movement) {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let line_idx = if text.line(text.len_lines() - 1).len_chars() == 0 {
@@ -3788,12 +3833,34 @@ fn goto_last_line(cx: &mut Context) {
let selection = doc
.selection(view.id)
.clone()
.transform(|range| range.put_cursor(text, pos, cx.editor.mode == Mode::Select));
.transform(|range| range.put_cursor(text, pos, movement == Movement::Extend));
push_jump(view, doc);
doc.set_selection(view.id, selection);
}
fn goto_column(cx: &mut Context) {
goto_column_impl(cx, Movement::Move);
}
fn extend_to_column(cx: &mut Context) {
goto_column_impl(cx, Movement::Extend);
}
fn goto_column_impl(cx: &mut Context, movement: Movement) {
let count = cx.count();
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
let line = range.cursor_line(text);
let line_start = text.line_to_char(line);
let line_end = line_end_char_index(&text, line);
let pos = graphemes::nth_next_grapheme_boundary(text, line_start, count - 1).min(line_end);
range.put_cursor(text, pos, movement == Movement::Extend)
});
doc.set_selection(view.id, selection);
}
fn goto_last_accessed_file(cx: &mut Context) {
let view = view_mut!(cx.editor);
if let Some(alt) = view.docs_access_history.pop() {
@@ -4126,6 +4193,7 @@ pub mod insert {
pub fn insert_newline(cx: &mut Context) {
let config = cx.editor.config();
let (view, doc) = current_ref!(cx.editor);
let loader = cx.editor.syn_loader.load();
let text = doc.text().slice(..);
let line_ending = doc.line_ending.as_str();
@@ -4144,6 +4212,7 @@ pub mod insert {
None
};
let mut last_pos = 0;
let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
// Tracks the number of trailing whitespace characters deleted by this selection.
let mut chars_deleted = 0;
@@ -4165,13 +4234,14 @@ pub mod insert {
let (from, to, local_offs) = if let Some(idx) =
text.slice(line_start..pos).last_non_whitespace_char()
{
let first_trailing_whitespace_char = (line_start + idx + 1).min(pos);
let first_trailing_whitespace_char = (line_start + idx + 1).clamp(last_pos, pos);
last_pos = pos;
let line = text.line(current_line);
let indent = match line.first_non_whitespace_char() {
Some(pos) if continue_comment_token.is_some() => line.slice(..pos).to_string(),
_ => indent::indent_for_newline(
doc.language_config(),
&loader,
doc.syntax(),
&config.indent_heuristic,
&doc.indent_style,
@@ -5228,6 +5298,21 @@ fn rotate_selections_backward(cx: &mut Context) {
rotate_selections(cx, Direction::Backward)
}
fn rotate_selections_first(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let mut selection = doc.selection(view.id).clone();
selection.set_primary_index(0);
doc.set_selection(view.id, selection);
}
fn rotate_selections_last(cx: &mut Context) {
let (view, doc) = current!(cx.editor);
let mut selection = doc.selection(view.id).clone();
let len = selection.len();
selection.set_primary_index(len - 1);
doc.set_selection(view.id, selection);
}
enum ReorderStrategy {
RotateForward,
RotateBackward,
@@ -5728,19 +5813,14 @@ fn goto_ts_object_impl(cx: &mut Context, object: &'static str, direction: Direct
let count = cx.count();
let motion = move |editor: &mut Editor| {
let (view, doc) = current!(editor);
if let Some((lang_config, syntax)) = doc.language_config().zip(doc.syntax()) {
let loader = editor.syn_loader.load();
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let root = syntax.tree().root_node();
let selection = doc.selection(view.id).clone().transform(|range| {
let new_range = movement::goto_treesitter_object(
text,
range,
object,
direction,
root,
lang_config,
count,
text, range, object, direction, &root, syntax, &loader, count,
);
if editor.mode == Mode::Select {
@@ -5804,6 +5884,14 @@ fn goto_prev_test(cx: &mut Context) {
goto_ts_object_impl(cx, "test", Direction::Backward)
}
fn goto_next_xml_element(cx: &mut Context) {
goto_ts_object_impl(cx, "xml-element", Direction::Forward)
}
fn goto_prev_xml_element(cx: &mut Context) {
goto_ts_object_impl(cx, "xml-element", Direction::Backward)
}
fn goto_next_entry(cx: &mut Context) {
goto_ts_object_impl(cx, "entry", Direction::Forward)
}
@@ -5828,21 +5916,15 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
if let Some(ch) = event.char() {
let textobject = move |editor: &mut Editor| {
let (view, doc) = current!(editor);
let loader = editor.syn_loader.load();
let text = doc.text().slice(..);
let textobject_treesitter = |obj_name: &str, range: Range| -> Range {
let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) {
Some(t) => t,
None => return range,
let Some(syntax) = doc.syntax() else {
return range;
};
textobject::textobject_treesitter(
text,
range,
objtype,
obj_name,
syntax.tree().root_node(),
lang_config,
count,
text, range, objtype, obj_name, syntax, &loader, count,
)
};
@@ -5877,6 +5959,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
'c' => textobject_treesitter("comment", range),
'T' => textobject_treesitter("test", range),
'e' => textobject_treesitter("entry", range),
'x' => textobject_treesitter("xml-element", range),
'p' => textobject::textobject_paragraph(text, range, objtype, count),
'm' => textobject::textobject_pair_surround_closest(
doc.syntax(),
@@ -5921,6 +6004,7 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
("e", "Data structure entry (tree-sitter)"),
("m", "Closest surrounding pair (tree-sitter)"),
("g", "Change"),
("x", "X(HTML) element (tree-sitter)"),
(" ", "... or any character acting as a pair"),
];
@@ -6315,7 +6399,7 @@ fn shell_prompt(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBeha
cx,
prompt,
Some('|'),
ui::completers::filename,
ui::completers::shell,
move |cx, input: &str, event: PromptEvent| {
if event != PromptEvent::Validate {
return;
@@ -6658,6 +6742,10 @@ fn jump_to_word(cx: &mut Context, behaviour: Movement) {
// Calculate the jump candidates: ranges for any visible words with two or
// more characters.
let alphabet = &cx.editor.config().jump_label_alphabet;
if alphabet.is_empty() {
return;
}
let jump_label_limit = alphabet.len() * alphabet.len();
let mut words = Vec::with_capacity(jump_label_limit);
let (view, doc) = current_ref!(cx.editor);

View File

@@ -5,13 +5,12 @@ use crate::{
ui::{self, overlay::overlaid, Picker, Popup, Prompt, PromptEvent, Text},
};
use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
use helix_dap::{self as dap, Client};
use helix_core::syntax::config::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
use helix_dap::{self as dap, requests::TerminateArguments};
use helix_lsp::block_on;
use helix_view::editor::Breakpoint;
use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tui::text::Spans;
use std::collections::HashMap;
@@ -59,7 +58,12 @@ fn thread_picker(
move |cx, thread, _action| callback_fn(cx.editor, thread),
)
.with_preview(move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
let frames = editor
.debug_adapters
.get_active_client()
.as_ref()?
.stack_frames
.get(&thread.id)?;
let frame = frames.first()?;
let path = frame.source.as_ref()?.path.as_ref()?.as_path();
let pos = Some((
@@ -116,34 +120,16 @@ pub fn dap_start_impl(
params: Option<Vec<std::borrow::Cow<str>>>,
) -> Result<(), anyhow::Error> {
let doc = doc!(cx.editor);
let config = doc
.language_config()
.and_then(|config| config.debugger.as_ref())
.ok_or_else(|| anyhow!("No debug adapter available for language"))?;
let result = match socket {
Some(socket) => block_on(Client::tcp(socket, 0)),
None => block_on(Client::process(
&config.transport,
&config.command,
config.args.iter().map(|arg| arg.as_str()).collect(),
config.port_arg.as_deref(),
0,
)),
};
let (mut debugger, events) = match result {
Ok(r) => r,
Err(e) => bail!("Failed to start debug session: {}", e),
};
let request = debugger.initialize(config.name.clone());
if let Err(e) = block_on(request) {
bail!("Failed to initialize debug adapter: {}", e);
}
debugger.quirks = config.quirks.clone();
let id = cx
.editor
.debug_adapters
.start_client(socket, config)
.map_err(|e| anyhow!("Failed to start debug client: {}", e))?;
// TODO: avoid refetching all of this... pass a config in
let template = match name {
@@ -209,6 +195,13 @@ pub fn dap_start_impl(
// }
};
let debugger = match cx.editor.debug_adapters.get_client_mut(id) {
Some(child) => child,
None => {
bail!("Failed to get child debugger.");
}
};
match &template.request[..] {
"launch" => {
let call = debugger.launch(args);
@@ -222,14 +215,12 @@ pub fn dap_start_impl(
};
// TODO: either await "initialized" or buffer commands until event is received
cx.editor.debugger = Some(debugger);
let stream = UnboundedReceiverStream::new(events);
cx.editor.debugger_events.push(stream);
Ok(())
}
pub fn dap_launch(cx: &mut Context) {
if cx.editor.debugger.is_some() {
// TODO: Now that we support multiple Clients, we could run multiple debuggers at once but for now keep this as is
if cx.editor.debug_adapters.get_active_client().is_some() {
cx.editor.set_error("Debugger is already running");
return;
}
@@ -283,7 +274,7 @@ pub fn dap_launch(cx: &mut Context) {
}
pub fn dap_restart(cx: &mut Context) {
let debugger = match &cx.editor.debugger {
let debugger = match cx.editor.debug_adapters.get_active_client() {
Some(debugger) => debugger,
None => {
cx.editor.set_error("Debugger is not running");
@@ -582,12 +573,17 @@ pub fn dap_variables(cx: &mut Context) {
}
pub fn dap_terminate(cx: &mut Context) {
cx.editor.set_status("Terminating debug session...");
let debugger = debugger!(cx.editor);
let request = debugger.disconnect(None);
let terminate_arguments = Some(TerminateArguments {
restart: Some(false),
});
let request = debugger.terminate(terminate_arguments);
dap_callback(cx.jobs, request, |editor, _compositor, _response: ()| {
// editor.set_error(format!("Failed to disconnect: {}", e));
editor.debugger = None;
editor.debug_adapters.unset_active_client();
});
}

View File

@@ -14,7 +14,7 @@ use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor};
use helix_core::{
diagnostic::DiagnosticProvider, syntax::LanguageServerFeature,
diagnostic::DiagnosticProvider, syntax::config::LanguageServerFeature,
text_annotations::InlineAnnotation, Selection, Uri,
};
use helix_stdx::path;
@@ -46,7 +46,7 @@ macro_rules! language_server_with_feature {
match language_server {
Some(language_server) => language_server,
None => {
$editor.set_status(format!(
$editor.set_error(format!(
"No configured language server supports {}",
$feature
));
@@ -252,6 +252,9 @@ fn diag_picker(
.into()
},
),
ui::PickerColumn::new("source", |item: &PickerDiagnostic, _| {
item.diag.source.as_deref().unwrap_or("").into()
}),
ui::PickerColumn::new("code", |item: &PickerDiagnostic, _| {
match item.diag.code.as_ref() {
Some(NumberOrString::Number(n)) => n.to_string().into(),
@@ -263,12 +266,12 @@ fn diag_picker(
item.diag.message.as_str().into()
}),
];
let mut primary_column = 2; // message
let mut primary_column = 3; // message
if format == DiagnosticsFormat::ShowSourcePath {
columns.insert(
// between message code and message
2,
3,
ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| {
if let Some(path) = item.location.uri.as_path() {
path::get_truncated_path(path)
@@ -804,7 +807,9 @@ pub fn code_action(cx: &mut Context) {
});
picker.move_down(); // pre-select the first item
let popup = Popup::new("code-action", picker).with_scrollbar(false);
let popup = Popup::new("code-action", picker)
.with_scrollbar(false)
.auto_close(true);
compositor.replace_or_push("code-action", popup);
};
@@ -1357,6 +1362,7 @@ fn compute_inlay_hints_for_view(
let mut padding_after_inlay_hints = Vec::new();
let doc_text = doc.text();
let inlay_hints_length_limit = doc.config.load().lsp.inlay_hints_length_limit;
for hint in hints {
let char_idx =
@@ -1367,7 +1373,7 @@ fn compute_inlay_hints_for_view(
None => continue,
};
let label = match hint.label {
let mut label = match hint.label {
lsp::InlayHintLabel::String(s) => s,
lsp::InlayHintLabel::LabelParts(parts) => parts
.into_iter()
@@ -1375,6 +1381,31 @@ fn compute_inlay_hints_for_view(
.collect::<Vec<_>>()
.join(""),
};
// Truncate the hint if too long
if let Some(limit) = inlay_hints_length_limit {
// Limit on displayed width
use helix_core::unicode::{
segmentation::UnicodeSegmentation, width::UnicodeWidthStr,
};
let width = label.width();
let limit = limit.get().into();
if width > limit {
let mut floor_boundary = 0;
let mut acc = 0;
for (i, grapheme_cluster) in label.grapheme_indices(true) {
acc += grapheme_cluster.width();
if acc > limit {
floor_boundary = i;
break;
}
}
label.truncate(floor_boundary);
label.push('…');
}
}
let inlay_hints_vec = match hint.kind {
Some(lsp::InlayHintKind::TYPE) => &mut type_inlay_hints,

View File

@@ -230,38 +230,51 @@ fn force_buffer_close(
buffer_close_by_ids_impl(cx, &document_ids, true)
}
fn buffer_gather_others_impl(editor: &mut Editor) -> Vec<DocumentId> {
let current_document = &doc!(editor).id();
editor
.documents()
.map(|doc| doc.id())
.filter(|doc_id| doc_id != current_document)
.collect()
fn buffer_gather_others_impl(editor: &mut Editor, skip_visible: bool) -> Vec<DocumentId> {
if skip_visible {
let visible_document_ids = editor
.tree
.views()
.map(|view| &view.0.doc)
.collect::<HashSet<_>>();
editor
.documents()
.map(|doc| doc.id())
.filter(|doc_id| !visible_document_ids.contains(doc_id))
.collect()
} else {
let current_document = &doc!(editor).id();
editor
.documents()
.map(|doc| doc.id())
.filter(|doc_id| doc_id != current_document)
.collect()
}
}
fn buffer_close_others(
cx: &mut compositor::Context,
_args: Args,
args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let document_ids = buffer_gather_others_impl(cx.editor);
let document_ids = buffer_gather_others_impl(cx.editor, args.has_flag("skip-visible"));
buffer_close_by_ids_impl(cx, &document_ids, false)
}
fn force_buffer_close_others(
cx: &mut compositor::Context,
_args: Args,
args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let document_ids = buffer_gather_others_impl(cx.editor);
let document_ids = buffer_gather_others_impl(cx.editor, args.has_flag("skip-visible"));
buffer_close_by_ids_impl(cx, &document_ids, true)
}
@@ -321,7 +334,11 @@ fn buffer_previous(
Ok(())
}
fn write_impl(cx: &mut compositor::Context, path: Option<&str>, force: bool) -> anyhow::Result<()> {
fn write_impl(
cx: &mut compositor::Context,
path: Option<&str>,
options: WriteOptions,
) -> anyhow::Result<()> {
let config = cx.editor.config();
let jobs = &mut cx.jobs;
let (view, doc) = current!(cx.editor);
@@ -339,14 +356,15 @@ fn write_impl(cx: &mut compositor::Context, path: Option<&str>, force: bool) ->
// Save an undo checkpoint for any outstanding changes.
doc.append_changes_to_history(view);
let fmt = if config.auto_format {
doc.auto_format().map(|fmt| {
let (view, doc) = current_ref!(cx.editor);
let fmt = if config.auto_format && options.auto_format {
doc.auto_format(cx.editor).map(|fmt| {
let callback = make_format_callback(
doc.id(),
doc.version(),
view.id,
fmt,
Some((path.map(Into::into), force)),
Some((path.map(Into::into), options.force)),
);
jobs.add(Job::with_callback(callback).wait_before_exiting());
@@ -357,7 +375,7 @@ fn write_impl(cx: &mut compositor::Context, path: Option<&str>, force: bool) ->
if fmt.is_none() {
let id = doc.id();
cx.editor.save(id, path, force)?;
cx.editor.save(id, path, options.force)?;
}
Ok(())
@@ -422,12 +440,25 @@ fn insert_final_newline(doc: &mut Document, view_id: ViewId) {
}
}
#[derive(Debug, Clone, Copy)]
pub struct WriteOptions {
pub force: bool,
pub auto_format: bool,
}
fn write(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
write_impl(cx, args.first(), false)
write_impl(
cx,
args.first(),
WriteOptions {
force: false,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)
}
fn force_write(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
@@ -435,7 +466,14 @@ fn force_write(cx: &mut compositor::Context, args: Args, event: PromptEvent) ->
return Ok(());
}
write_impl(cx, args.first(), true)
write_impl(
cx,
args.first(),
WriteOptions {
force: true,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)
}
fn write_buffer_close(
@@ -447,7 +485,14 @@ fn write_buffer_close(
return Ok(());
}
write_impl(cx, args.first(), false)?;
write_impl(
cx,
args.first(),
WriteOptions {
force: false,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)?;
let document_ids = buffer_gather_paths_impl(cx.editor, args);
buffer_close_by_ids_impl(cx, &document_ids, false)
@@ -462,7 +507,14 @@ fn force_write_buffer_close(
return Ok(());
}
write_impl(cx, args.first(), true)?;
write_impl(
cx,
args.first(),
WriteOptions {
force: true,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)?;
let document_ids = buffer_gather_paths_impl(cx.editor, args);
buffer_close_by_ids_impl(cx, &document_ids, false)
@@ -483,8 +535,8 @@ fn format(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyh
return Ok(());
}
let (view, doc) = current!(cx.editor);
let format = doc.format().context(
let (view, doc) = current_ref!(cx.editor);
let format = doc.format(cx.editor).context(
"A formatter isn't available, and no language server provides formatting capabilities",
)?;
let callback = make_format_callback(doc.id(), doc.version(), view.id, format, None);
@@ -643,7 +695,14 @@ fn write_quit(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> a
return Ok(());
}
write_impl(cx, args.first(), false)?;
write_impl(
cx,
args.first(),
WriteOptions {
force: false,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)?;
cx.block_try_flush_writes()?;
quit(cx, Args::default(), event)
}
@@ -657,7 +716,14 @@ fn force_write_quit(
return Ok(());
}
write_impl(cx, args.first(), true)?;
write_impl(
cx,
args.first(),
WriteOptions {
force: true,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)?;
cx.block_try_flush_writes()?;
force_quit(cx, Args::default(), event)
}
@@ -752,7 +818,8 @@ pub fn write_all_impl(
doc.append_changes_to_history(view);
let fmt = if options.auto_format && config.auto_format {
doc.auto_format().map(|fmt| {
let doc = doc!(cx.editor, &doc_id);
doc.auto_format(cx.editor).map(|fmt| {
let callback = make_format_callback(
doc_id,
doc.version(),
@@ -778,7 +845,7 @@ pub fn write_all_impl(
Ok(())
}
fn write_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> anyhow::Result<()> {
fn write_all(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
@@ -788,14 +855,14 @@ fn write_all(cx: &mut compositor::Context, _args: Args, event: PromptEvent) -> a
WriteAllOptions {
force: false,
write_scratch: true,
auto_format: true,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)
}
fn force_write_all(
cx: &mut compositor::Context,
_args: Args,
args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -807,14 +874,14 @@ fn force_write_all(
WriteAllOptions {
force: true,
write_scratch: true,
auto_format: true,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)
}
fn write_all_quit(
cx: &mut compositor::Context,
_args: Args,
args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -825,7 +892,7 @@ fn write_all_quit(
WriteAllOptions {
force: false,
write_scratch: true,
auto_format: true,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
)?;
quit_all_impl(cx, false)
@@ -833,7 +900,7 @@ fn write_all_quit(
fn force_write_all_quit(
cx: &mut compositor::Context,
_args: Args,
args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
@@ -844,7 +911,7 @@ fn force_write_all_quit(
WriteAllOptions {
force: true,
write_scratch: true,
auto_format: true,
auto_format: !args.has_flag(WRITE_NO_FORMAT_FLAG.name),
},
);
quit_all_impl(cx, true)
@@ -1670,59 +1737,52 @@ fn tree_sitter_highlight_name(
_args: Args,
event: PromptEvent,
) -> anyhow::Result<()> {
fn find_highlight_at_cursor(
cx: &mut compositor::Context<'_>,
) -> Option<helix_core::syntax::Highlight> {
use helix_core::syntax::HighlightEvent;
let (view, doc) = current!(cx.editor);
let syntax = doc.syntax()?;
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let byte = text.char_to_byte(cursor);
let node = syntax.descendant_for_byte_range(byte, byte)?;
// Query the same range as the one used in syntax highlighting.
let range = {
// Calculate viewport byte ranges:
let row = text.char_to_line(doc.view_offset(view.id).anchor.min(text.len_chars()));
// Saturating subs to make it inclusive zero indexing.
let last_line = text.len_lines().saturating_sub(1);
let height = view.inner_area(doc).height;
let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
let start = text.line_to_byte(row.min(last_line));
let end = text.line_to_byte(last_visible_line + 1);
start..end
};
let mut highlight = None;
for event in syntax.highlight_iter(text, Some(range), None) {
match event.unwrap() {
HighlightEvent::Source { start, end }
if start == node.start_byte() && end == node.end_byte() =>
{
return highlight;
}
HighlightEvent::HighlightStart(hl) => {
highlight = Some(hl);
}
_ => (),
}
}
None
}
if event != PromptEvent::Validate {
return Ok(());
}
let Some(highlight) = find_highlight_at_cursor(cx) else {
let (view, doc) = current_ref!(cx.editor);
let Some(syntax) = doc.syntax() else {
return Ok(());
};
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let byte = text.char_to_byte(cursor) as u32;
// Query the same range as the one used in syntax highlighting.
let range = {
// Calculate viewport byte ranges:
let row = text.char_to_line(doc.view_offset(view.id).anchor.min(text.len_chars()));
// Saturating subs to make it inclusive zero indexing.
let last_line = text.len_lines().saturating_sub(1);
let height = view.inner_area(doc).height;
let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line);
let start = text.line_to_byte(row.min(last_line)) as u32;
let end = text.line_to_byte(last_visible_line + 1) as u32;
let content = cx.editor.theme.scope(highlight.0).to_string();
start..end
};
let loader = cx.editor.syn_loader.load();
let mut highlighter = syntax.highlighter(text, &loader, range);
let mut highlights = Vec::new();
while highlighter.next_event_offset() <= byte {
let (event, new_highlights) = highlighter.advance();
if event == helix_core::syntax::HighlightEvent::Refresh {
highlights.clear();
}
highlights.extend(new_highlights);
}
let content = highlights
.into_iter()
.fold(String::new(), |mut acc, highlight| {
if !acc.is_empty() {
acc.push_str(", ");
}
acc.push_str(cx.editor.theme.scope(highlight));
acc
});
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
@@ -1799,7 +1859,7 @@ fn debug_eval(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> a
return Ok(());
}
if let Some(debugger) = cx.editor.debugger.as_mut() {
if let Some(debugger) = cx.editor.debug_adapters.get_active_client() {
let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
(Some(frame), Some(thread_id)) => (frame, thread_id),
_ => {
@@ -1880,7 +1940,15 @@ fn update_goto_line_number_preview(cx: &mut compositor::Context, args: Args) ->
let scrolloff = cx.editor.config().scrolloff;
let line = args[0].parse::<usize>()?;
goto_line_without_jumplist(cx.editor, NonZeroUsize::new(line));
goto_line_without_jumplist(
cx.editor,
NonZeroUsize::new(line),
if cx.editor.mode == Mode::Select {
Movement::Extend
} else {
Movement::Move
},
);
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, scrolloff);
@@ -2080,10 +2148,11 @@ fn language(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> any
let doc = doc_mut!(cx.editor);
let loader = cx.editor.syn_loader.load();
if &args[0] == DEFAULT_LANGUAGE_NAME {
doc.set_language(None, None)
doc.set_language(None, &loader)
} else {
doc.set_language_by_language_id(&args[0], cx.editor.syn_loader.clone())?;
doc.set_language_by_language_id(&args[0], &loader)?;
}
doc.detect_indent_and_line_ending();
@@ -2101,10 +2170,6 @@ fn sort(cx: &mut compositor::Context, args: Args, event: PromptEvent) -> anyhow:
return Ok(());
}
sort_impl(cx, args.has_flag("reverse"))
}
fn sort_impl(cx: &mut compositor::Context, reverse: bool) -> anyhow::Result<()> {
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -2120,10 +2185,14 @@ fn sort_impl(cx: &mut compositor::Context, reverse: bool) -> anyhow::Result<()>
.map(|fragment| fragment.chunks().collect())
.collect();
fragments.sort_by(match reverse {
true => |a: &Tendril, b: &Tendril| b.cmp(a),
false => |a: &Tendril, b: &Tendril| a.cmp(b),
});
fragments.sort_by(
match (args.has_flag("insensitive"), args.has_flag("reverse")) {
(true, true) => |a: &Tendril, b: &Tendril| b.to_lowercase().cmp(&a.to_lowercase()),
(true, false) => |a: &Tendril, b: &Tendril| a.to_lowercase().cmp(&b.to_lowercase()),
(false, true) => |a: &Tendril, b: &Tendril| b.cmp(a),
(false, false) => |a: &Tendril, b: &Tendril| a.cmp(b),
},
);
let transaction = Transaction::change(
doc.text(),
@@ -2189,8 +2258,8 @@ fn tree_sitter_subtree(
if let Some(syntax) = doc.syntax() {
let primary_selection = doc.selection(view.id).primary();
let text = doc.text();
let from = text.char_to_byte(primary_selection.from());
let to = text.char_to_byte(primary_selection.to());
let from = text.char_to_byte(primary_selection.from()) as u32;
let to = text.char_to_byte(primary_selection.to()) as u32;
if let Some(selected_node) = syntax.descendant_for_byte_range(from, to) {
let mut contents = String::from("```tsq\n");
helix_core::syntax::pretty_print_tree(&mut contents, selected_node)?;
@@ -2459,7 +2528,16 @@ fn move_buffer(cx: &mut compositor::Context, args: Args, event: PromptEvent) ->
.path()
.context("Scratch buffer cannot be moved. Use :write instead")?
.clone();
let new_path = args.first().unwrap().to_string();
let new_path: PathBuf = args.first().unwrap().into();
// if new_path is a directory, append the original file name
// to move the file into that directory.
let new_path = old_path
.file_name()
.filter(|_| new_path.is_dir())
.map(|old_file_name| new_path.join(old_file_name))
.unwrap_or(new_path);
if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) {
bail!("Could not move file: {err}");
}
@@ -2558,6 +2636,21 @@ fn noop(_cx: &mut compositor::Context, _args: Args, _event: PromptEvent) -> anyh
Ok(())
}
/// This command accepts a single boolean --skip-visible flag and no positionals.
const BUFFER_CLOSE_OTHERS_SIGNATURE: Signature = Signature {
positionals: (0, Some(0)),
flags: &[Flag {
name: "skip-visible",
alias: Some('s'),
doc: "don't close buffers that are visible",
..Flag::DEFAULT
}],
..Signature::DEFAULT
};
// TODO: SHELL_SIGNATURE should specify var args for arguments, so that just completers::filename can be used,
// but Signature does not yet allow for var args.
/// This command handles all of its input as-is with no quoting or flags.
const SHELL_SIGNATURE: Signature = Signature {
positionals: (1, Some(2)),
@@ -2566,12 +2659,18 @@ const SHELL_SIGNATURE: Signature = Signature {
};
const SHELL_COMPLETER: CommandCompleter = CommandCompleter::positional(&[
// Command name (TODO: consider a command completer - Kakoune has prior art)
completers::none,
// Command name
completers::program,
// Shell argument(s)
completers::filename,
completers::repeating_filenames,
]);
const WRITE_NO_FORMAT_FLAG: Flag = Flag {
name: "no-format",
doc: "skip auto-formatting",
..Flag::DEFAULT
};
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -2634,10 +2733,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
doc: "Close all buffers but the currently focused one.",
fun: buffer_close_others,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
signature: BUFFER_CLOSE_OTHERS_SIGNATURE,
},
TypableCommand {
name: "buffer-close-others!",
@@ -2645,10 +2741,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
doc: "Force close all buffers but the currently focused one.",
fun: force_buffer_close_others,
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
..Signature::DEFAULT
},
signature: BUFFER_CLOSE_OTHERS_SIGNATURE,
},
TypableCommand {
name: "buffer-close-all",
@@ -2702,6 +2795,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::positional(&[completers::filename]),
signature: Signature {
positionals: (0, Some(1)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2713,6 +2807,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::positional(&[completers::filename]),
signature: Signature {
positionals: (0, Some(1)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2724,6 +2819,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::positional(&[completers::filename]),
signature: Signature {
positionals: (0, Some(1)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2735,6 +2831,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::positional(&[completers::filename]),
signature: Signature {
positionals: (0, Some(1)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2815,6 +2912,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::positional(&[completers::filename]),
signature: Signature {
positionals: (0, Some(1)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2826,6 +2924,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::positional(&[completers::filename]),
signature: Signature {
positionals: (0, Some(1)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2837,6 +2936,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2848,6 +2948,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2859,6 +2960,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -2870,6 +2972,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
completer: CommandCompleter::none(),
signature: Signature {
positionals: (0, Some(0)),
flags: &[WRITE_NO_FORMAT_FLAG],
..Signature::DEFAULT
},
},
@@ -3348,6 +3451,12 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
signature: Signature {
positionals: (0, Some(0)),
flags: &[
Flag {
name: "insensitive",
alias: Some('i'),
doc: "sort the ranges case-insensitively",
..Flag::DEFAULT
},
Flag {
name: "reverse",
alias: Some('r'),
@@ -3442,7 +3551,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
},
TypableCommand {
name: "pipe",
aliases: &[],
aliases: &["|"],
doc: "Pipe each selection to the shell command.",
fun: pipe,
completer: SHELL_COMPLETER,
@@ -3458,7 +3567,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
},
TypableCommand {
name: "run-shell-command",
aliases: &["sh"],
aliases: &["sh", "!"],
doc: "Run a shell command",
fun: run_shell_command,
completer: SHELL_COMPLETER,
@@ -3852,10 +3961,12 @@ fn quote_completion<'a>(
span.content = Cow::Owned(format!(
"'{}{}'",
// Escape any inner single quotes by doubling them.
replace(token.content.as_ref().into(), '\'', "''"),
replace(token.content[..range.start].into(), '\'', "''"),
replace(span.content, '\'', "''")
));
// Ignore `range.start` here since we're replacing the entire token.
// Ignore `range.start` here since we're replacing the entire token. We used
// `range.start` above to emulate the replacement that using `range.start` would have
// done.
((offset + token.content_start).., span)
}
TokenKind::Quoted(quote) => {

View File

@@ -1,8 +1,8 @@
use helix_event::{events, register_event};
use helix_view::document::Mode;
use helix_view::events::{
DiagnosticsDidChange, DocumentDidChange, DocumentDidClose, DocumentDidOpen, DocumentFocusLost,
LanguageServerExited, LanguageServerInitialized, SelectionDidChange,
ConfigDidChange, DiagnosticsDidChange, DocumentDidChange, DocumentDidClose, DocumentDidOpen,
DocumentFocusLost, LanguageServerExited, LanguageServerInitialized, SelectionDidChange,
};
use crate::commands;
@@ -26,4 +26,5 @@ pub fn register() {
register_event::<DiagnosticsDidChange>();
register_event::<LanguageServerInitialized>();
register_event::<LanguageServerExited>();
register_event::<ConfigDidChange>();
}

View File

@@ -8,7 +8,7 @@ use crate::events;
use crate::handlers::auto_save::AutoSaveHandler;
use crate::handlers::signature_help::SignatureHelpHandler;
pub use helix_view::handlers::Handlers;
pub use helix_view::handlers::{word_index, Handlers};
use self::document_colors::DocumentColorsHandler;
@@ -26,12 +26,14 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
let signature_hints = SignatureHelpHandler::new().spawn();
let auto_save = AutoSaveHandler::new().spawn();
let document_colors = DocumentColorsHandler::default().spawn();
let word_index = word_index::Handler::spawn();
let handlers = Handlers {
completions: helix_view::handlers::completion::CompletionHandler::new(event_tx),
signature_hints,
auto_save,
document_colors,
word_index,
};
helix_view::handlers::register_hooks(&handlers);

View File

@@ -2,7 +2,7 @@ use std::collections::HashMap;
use helix_core::chars::char_is_word;
use helix_core::completion::CompletionProvider;
use helix_core::syntax::LanguageServerFeature;
use helix_core::syntax::config::LanguageServerFeature;
use helix_event::{register_hook, TaskHandle};
use helix_lsp::lsp;
use helix_stdx::rope::RopeSliceExt;
@@ -30,6 +30,7 @@ mod item;
mod path;
mod request;
mod resolve;
mod word;
async fn handle_response(
requests: &mut JoinSet<CompletionResponse>,
@@ -82,7 +83,7 @@ async fn replace_completions(
fn show_completion(
editor: &mut Editor,
compositor: &mut Compositor,
items: Vec<CompletionItem>,
mut items: Vec<CompletionItem>,
context: HashMap<CompletionProvider, ResponseContext>,
trigger: Trigger,
) {
@@ -101,6 +102,7 @@ fn show_completion(
if ui.completion.is_some() {
return;
}
word::retain_valid_completions(trigger, doc, view.id, &mut items);
editor.handlers.completions.active_completions = context;
let completion_area = ui.set_completion(editor, items, trigger.pos, size);

View File

@@ -5,7 +5,7 @@ use std::time::Duration;
use arc_swap::ArcSwap;
use futures_util::Future;
use helix_core::completion::CompletionProvider;
use helix_core::syntax::LanguageServerFeature;
use helix_core::syntax::config::LanguageServerFeature;
use helix_event::{cancelable_future, TaskController, TaskHandle};
use helix_lsp::lsp;
use helix_lsp::lsp::{CompletionContext, CompletionTriggerKind};
@@ -28,6 +28,8 @@ use crate::job::{dispatch, dispatch_blocking};
use crate::ui;
use crate::ui::editor::InsertEvent;
use super::word;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
pub(super) enum TriggerKind {
Auto,
@@ -242,10 +244,15 @@ fn request_completions(
doc.selection(view.id).clone(),
doc,
handle.clone(),
savepoint,
savepoint.clone(),
) {
requests.spawn_blocking(path_completion_request);
}
if let Some(word_completion_request) =
word::completion(editor, trigger, handle.clone(), savepoint)
{
requests.spawn_blocking(word_completion_request);
}
let ui = compositor.find::<ui::EditorView>().unwrap();
ui.last_insert.1.push(InsertEvent::RequestCompletion);

View File

@@ -0,0 +1,134 @@
use std::{borrow::Cow, sync::Arc};
use helix_core::{
self as core, chars::char_is_word, completion::CompletionProvider, movement, Transaction,
};
use helix_event::TaskHandle;
use helix_stdx::rope::RopeSliceExt as _;
use helix_view::{
document::SavePoint, handlers::completion::ResponseContext, Document, Editor, ViewId,
};
use super::{request::TriggerKind, CompletionItem, CompletionItems, CompletionResponse, Trigger};
const COMPLETION_KIND: &str = "word";
pub(super) fn completion(
editor: &Editor,
trigger: Trigger,
handle: TaskHandle,
savepoint: Arc<SavePoint>,
) -> Option<impl FnOnce() -> CompletionResponse> {
if !doc!(editor).word_completion_enabled() {
return None;
}
let config = editor.config().word_completion;
let doc_config = doc!(editor)
.language_config()
.and_then(|config| config.word_completion);
let trigger_length = doc_config
.and_then(|c| c.trigger_length)
.unwrap_or(config.trigger_length)
.get() as usize;
let (view, doc) = current_ref!(editor);
let rope = doc.text().clone();
let word_index = editor.handlers.word_index().clone();
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone();
let pos = selection.primary().cursor(text);
let cursor = movement::move_prev_word_start(text, core::Range::point(pos), 1);
if cursor.head == pos {
return None;
}
if trigger.kind != TriggerKind::Manual
&& text
.slice(cursor.head..)
.graphemes()
.take(trigger_length)
.take_while(|g| g.chars().all(char_is_word))
.count()
!= trigger_length
{
return None;
}
let typed_word_range = cursor.head..pos;
let typed_word = text.slice(typed_word_range.clone());
let edit_diff = if typed_word
.char(typed_word.len_chars().saturating_sub(1))
.is_whitespace()
{
0
} else {
typed_word.len_chars()
};
if handle.is_canceled() {
return None;
}
let future = move || {
let text = rope.slice(..);
let typed_word: Cow<_> = text.slice(typed_word_range).into();
let items = word_index
.matches(&typed_word)
.into_iter()
.filter(|word| word.as_str() != typed_word.as_ref())
.map(|word| {
let transaction = Transaction::change_by_selection(&rope, &selection, |range| {
let cursor = range.cursor(text);
(cursor - edit_diff, cursor, Some((&word).into()))
});
CompletionItem::Other(core::CompletionItem {
transaction,
label: word.into(),
kind: Cow::Borrowed(COMPLETION_KIND),
documentation: None,
provider: CompletionProvider::Word,
})
})
.collect();
CompletionResponse {
items: CompletionItems::Other(items),
provider: CompletionProvider::Word,
context: ResponseContext {
is_incomplete: false,
priority: 0,
savepoint,
},
}
};
Some(future)
}
pub(super) fn retain_valid_completions(
trigger: Trigger,
doc: &Document,
view_id: ViewId,
items: &mut Vec<CompletionItem>,
) {
if trigger.kind == TriggerKind::Manual {
return;
}
let text = doc.text().slice(..);
let cursor = doc.selection(view_id).primary().cursor(text);
if text
.get_char(cursor.saturating_sub(1))
.is_some_and(|ch| ch.is_whitespace())
{
items.retain(|item| {
!matches!(
item,
CompletionItem::Other(core::CompletionItem {
provider: CompletionProvider::Word,
..
})
)
});
}
}

View File

@@ -1,7 +1,7 @@
use std::{collections::HashSet, time::Duration};
use futures_util::{stream::FuturesOrdered, StreamExt};
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation};
use helix_core::{syntax::config::LanguageServerFeature, text_annotations::InlineAnnotation};
use helix_event::{cancelable_future, register_hook};
use helix_lsp::lsp;
use helix_view::{
@@ -81,6 +81,10 @@ fn request_document_colors(editor: &mut Editor, doc_id: DocumentId) {
})
.collect();
if futures.is_empty() {
return;
}
tokio::spawn(async move {
let mut all_colors = Vec::new();
loop {

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use std::time::Duration;
use helix_core::syntax::LanguageServerFeature;
use helix_core::syntax::config::LanguageServerFeature;
use helix_event::{cancelable_future, register_hook, send_blocking, TaskController, TaskHandle};
use helix_lsp::lsp::{self, SignatureInformation};
use helix_stdx::rope::RopeSliceExt;

View File

@@ -185,14 +185,16 @@ pub fn languages_all() -> std::io::Result<()> {
.language
.sort_unstable_by_key(|l| l.language_id.clone());
let check_binary = |cmd: Option<&str>| match cmd {
Some(cmd) => match helix_stdx::env::which(cmd) {
Ok(_) => color(fit(&format!("{}", cmd)), Color::Green),
Err(_) => color(fit(&format!("{}", cmd)), Color::Red),
let check_binary_with_name = |cmd: Option<(&str, &str)>| match cmd {
Some((name, cmd)) => match helix_stdx::env::which(cmd) {
Ok(_) => color(fit(&format!("{}", name)), Color::Green),
Err(_) => color(fit(&format!("{}", name)), Color::Red),
},
None => color(fit("None"), Color::Yellow),
};
let check_binary = |cmd: Option<&str>| check_binary_with_name(cmd.map(|cmd| (cmd, cmd)));
for lang in &syn_loader_conf.language {
write!(stdout, "{}", fit(&lang.language_id))?;
@@ -200,9 +202,9 @@ pub fn languages_all() -> std::io::Result<()> {
syn_loader_conf
.language_server
.get(&ls.name)
.map(|config| config.command.as_str())
.map(|config| (ls.name.as_str(), config.command.as_str()))
});
write!(stdout, "{}", check_binary(cmds.next()))?;
write!(stdout, "{}", check_binary_with_name(cmds.next()))?;
let dap = lang.debugger.as_ref().map(|dap| dap.command.as_str());
write!(stdout, "{}", check_binary(dap))?;
@@ -224,7 +226,7 @@ pub fn languages_all() -> std::io::Result<()> {
for cmd in cmds {
write!(stdout, "{}", fit(""))?;
writeln!(stdout, "{}", check_binary(Some(cmd)))?;
writeln!(stdout, "{}", check_binary_with_name(Some(cmd)))?;
}
}
@@ -283,10 +285,12 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
probe_protocols(
"language server",
lang.language_servers
.iter()
.filter_map(|ls| syn_loader_conf.language_server.get(&ls.name))
.map(|config| config.command.as_str()),
lang.language_servers.iter().filter_map(|ls| {
syn_loader_conf
.language_server
.get(&ls.name)
.map(|config| (ls.name.as_str(), config.command.as_str()))
}),
)?;
probe_protocol(
@@ -323,7 +327,7 @@ fn probe_parser(grammar_name: &str) -> std::io::Result<()> {
}
/// Display diagnostics about multiple LSPs and DAPs.
fn probe_protocols<'a, I: Iterator<Item = &'a str> + 'a>(
fn probe_protocols<'a, I: Iterator<Item = (&'a str, &'a str)> + 'a>(
protocol_name: &str,
server_cmds: I,
) -> std::io::Result<()> {
@@ -338,12 +342,12 @@ fn probe_protocols<'a, I: Iterator<Item = &'a str> + 'a>(
}
writeln!(stdout)?;
for cmd in server_cmds {
let (path, icon) = match helix_stdx::env::which(cmd) {
for (name, cmd) in server_cmds {
let (diag, icon) = match helix_stdx::env::which(cmd) {
Ok(path) => (path.display().to_string().green(), "".green()),
Err(_) => (format!("'{}' not found in $PATH", cmd).red(), "".red()),
};
writeln!(stdout, " {} {}: {}", icon, cmd, path)?;
writeln!(stdout, " {} {}: {}", icon, name, diag)?;
}
Ok(())
@@ -354,19 +358,18 @@ fn probe_protocol(protocol_name: &str, server_cmd: Option<String>) -> std::io::R
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let cmd_name = match server_cmd {
Some(ref cmd) => cmd.as_str().green(),
None => "None".yellow(),
write!(stdout, "Configured {}:", protocol_name)?;
let Some(cmd) = server_cmd else {
writeln!(stdout, "{}", " None".yellow())?;
return Ok(());
};
writeln!(stdout, "Configured {}: {}", protocol_name, cmd_name)?;
writeln!(stdout)?;
if let Some(cmd) = server_cmd {
let path = match helix_stdx::env::which(&cmd) {
Ok(path) => path.display().to_string().green(),
Err(_) => format!("'{}' not found in $PATH", cmd).red(),
};
writeln!(stdout, "Binary for {}: {}", protocol_name, path)?;
}
let (diag, icon) = match helix_stdx::env::which(&cmd) {
Ok(path) => (path.display().to_string().green(), "".green()),
Err(_) => (format!("'{}' not found in $PATH", cmd).red(), "".red()),
};
writeln!(stdout, " {} {}", icon, diag)?;
Ok(())
}

View File

@@ -38,6 +38,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"G" => goto_line,
"g" => { "Goto"
"g" => goto_file_start,
"|" => goto_column,
"e" => goto_last_line,
"f" => goto_file,
"h" => goto_line_start,
@@ -119,6 +120,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"e" => goto_prev_entry,
"T" => goto_prev_test,
"p" => goto_prev_paragraph,
"x" => goto_prev_xml_element,
"space" => add_newline_above,
},
"]" => { "Right bracket"
@@ -133,6 +135,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"e" => goto_next_entry,
"T" => goto_next_test,
"p" => goto_next_paragraph,
"x" => goto_next_xml_element,
"space" => add_newline_below,
},
@@ -367,6 +370,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"v" => normal_mode,
"g" => { "Goto"
"g" => extend_to_file_start,
"|" => extend_to_column,
"e" => extend_to_last_line,
"k" => extend_line_up,
"j" => extend_line_down,
"w" => extend_to_word,

View File

@@ -90,7 +90,7 @@ macro_rules! keymap {
};
(@trie [$($cmd:ident),* $(,)?]) => {
$crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
$crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::MappableCommand::$cmd),*])
};
(

View File

@@ -76,8 +76,7 @@ fn open_external_url_callback(
let commands = open::commands(url.as_str());
async {
for cmd in commands {
let mut command = tokio::process::Command::new(cmd.get_program());
command.args(cmd.get_args());
let mut command: tokio::process::Command = cmd.into();
if command.output().await.is_ok() {
return Ok(job::Callback::Editor(Box::new(|_| {})));
}

View File

@@ -3,8 +3,7 @@ use std::cmp::min;
use helix_core::doc_formatter::{DocumentFormatter, GraphemeSource, TextFormat};
use helix_core::graphemes::Grapheme;
use helix_core::str_utils::char_to_byte_idx;
use helix_core::syntax::Highlight;
use helix_core::syntax::HighlightEvent;
use helix_core::syntax::{self, HighlightEvent, Highlighter, OverlayHighlights};
use helix_core::text_annotations::TextAnnotations;
use helix_core::{visual_offset_from_block, Position, RopeSlice};
use helix_stdx::rope::RopeSliceExt;
@@ -17,61 +16,6 @@ use tui::buffer::Buffer as Surface;
use crate::ui::text_decorations::DecorationManager;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum StyleIterKind {
/// base highlights (usually emitted by TS), byte indices (potentially not codepoint aligned)
BaseHighlights,
/// overlay highlights (emitted by custom code from selections), char indices
Overlay,
}
/// A wrapper around a HighlightIterator
/// that merges the layered highlights to create the final text style
/// and yields the active text style and the char_idx where the active
/// style will have to be recomputed.
///
/// TODO(ropey2): hopefully one day helix and ropey will operate entirely
/// on byte ranges and we can remove this
struct StyleIter<'a, H: Iterator<Item = HighlightEvent>> {
text_style: Style,
active_highlights: Vec<Highlight>,
highlight_iter: H,
kind: StyleIterKind,
text: RopeSlice<'a>,
theme: &'a Theme,
}
impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> {
type Item = (Style, usize);
fn next(&mut self) -> Option<(Style, usize)> {
while let Some(event) = self.highlight_iter.next() {
match event {
HighlightEvent::HighlightStart(highlights) => {
self.active_highlights.push(highlights)
}
HighlightEvent::HighlightEnd => {
self.active_highlights.pop();
}
HighlightEvent::Source { mut end, .. } => {
let style = self
.active_highlights
.iter()
.fold(self.text_style, |acc, span| {
acc.patch(self.theme.highlight(span.0))
});
if self.kind == StyleIterKind::BaseHighlights {
// Move the end byte index to the nearest character boundary (rounding up)
// and convert it to a character index.
end = self.text.byte_to_char(self.text.ceil_char_boundary(end));
}
return Some((style, end));
}
}
}
None
}
}
#[derive(Debug, PartialEq, Eq, Copy, Clone)]
pub struct LinePos {
/// Indicates whether the given visual line
@@ -90,8 +34,8 @@ pub fn render_document(
doc: &Document,
offset: ViewPosition,
doc_annotations: &TextAnnotations,
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
syntax_highlighter: Option<Highlighter<'_>>,
overlay_highlights: Vec<syntax::OverlayHighlights>,
theme: &Theme,
decorations: DecorationManager,
) {
@@ -108,8 +52,8 @@ pub fn render_document(
offset.anchor,
&doc.text_format(viewport.width, Some(theme)),
doc_annotations,
syntax_highlight_iter,
overlay_highlight_iter,
syntax_highlighter,
overlay_highlights,
theme,
decorations,
)
@@ -122,8 +66,8 @@ pub fn render_text(
anchor: usize,
text_fmt: &TextFormat,
text_annotations: &TextAnnotations,
syntax_highlight_iter: impl Iterator<Item = HighlightEvent>,
overlay_highlight_iter: impl Iterator<Item = HighlightEvent>,
syntax_highlighter: Option<Highlighter<'_>>,
overlay_highlights: Vec<syntax::OverlayHighlights>,
theme: &Theme,
mut decorations: DecorationManager,
) {
@@ -133,22 +77,9 @@ pub fn render_text(
let mut formatter =
DocumentFormatter::new_at_prev_checkpoint(text, text_fmt, text_annotations, anchor);
let mut syntax_styles = StyleIter {
text_style: renderer.text_style,
active_highlights: Vec::with_capacity(64),
highlight_iter: syntax_highlight_iter,
kind: StyleIterKind::BaseHighlights,
theme,
text,
};
let mut overlay_styles = StyleIter {
text_style: Style::default(),
active_highlights: Vec::with_capacity(64),
highlight_iter: overlay_highlight_iter,
kind: StyleIterKind::Overlay,
theme,
text,
};
let mut syntax_highlighter =
SyntaxHighlighter::new(syntax_highlighter, text, theme, renderer.text_style);
let mut overlay_highlighter = OverlayHighlighter::new(overlay_highlights, theme);
let mut last_line_pos = LinePos {
first_visual_line: false,
@@ -158,12 +89,6 @@ pub fn render_text(
let mut last_line_end = 0;
let mut is_in_indent_area = true;
let mut last_line_indent_level = 0;
let mut syntax_style_span = syntax_styles
.next()
.unwrap_or_else(|| (Style::default(), usize::MAX));
let mut overlay_style_span = overlay_styles
.next()
.unwrap_or_else(|| (Style::default(), usize::MAX));
let mut reached_view_top = false;
loop {
@@ -207,21 +132,17 @@ pub fn render_text(
}
// acquire the correct grapheme style
while grapheme.char_idx >= syntax_style_span.1 {
syntax_style_span = syntax_styles
.next()
.unwrap_or((Style::default(), usize::MAX));
while grapheme.char_idx >= syntax_highlighter.pos {
syntax_highlighter.advance();
}
while grapheme.char_idx >= overlay_style_span.1 {
overlay_style_span = overlay_styles
.next()
.unwrap_or((Style::default(), usize::MAX));
while grapheme.char_idx >= overlay_highlighter.pos {
overlay_highlighter.advance();
}
let grapheme_style = if let GraphemeSource::VirtualText { highlight } = grapheme.source {
let mut style = renderer.text_style;
if let Some(highlight) = highlight {
style = style.patch(theme.highlight(highlight.0));
style = style.patch(theme.highlight(highlight));
}
GraphemeStyle {
syntax_style: style,
@@ -229,8 +150,8 @@ pub fn render_text(
}
} else {
GraphemeStyle {
syntax_style: syntax_style_span.0,
overlay_style: overlay_style_span.0,
syntax_style: syntax_highlighter.style,
overlay_style: overlay_highlighter.style,
}
};
decorations.decorate_grapheme(renderer, &grapheme);
@@ -549,3 +470,105 @@ impl<'a> TextRenderer<'a> {
)
}
}
struct SyntaxHighlighter<'h, 'r, 't> {
inner: Option<Highlighter<'h>>,
text: RopeSlice<'r>,
/// The character index of the next highlight event, or `usize::MAX` if the highlighter is
/// finished.
pos: usize,
theme: &'t Theme,
text_style: Style,
style: Style,
}
impl<'h, 'r, 't> SyntaxHighlighter<'h, 'r, 't> {
fn new(
inner: Option<Highlighter<'h>>,
text: RopeSlice<'r>,
theme: &'t Theme,
text_style: Style,
) -> Self {
let mut highlighter = Self {
inner,
text,
pos: 0,
theme,
style: text_style,
text_style,
};
highlighter.update_pos();
highlighter
}
fn update_pos(&mut self) {
self.pos = self
.inner
.as_ref()
.and_then(|highlighter| {
let next_byte_idx = highlighter.next_event_offset();
(next_byte_idx != u32::MAX).then(|| {
// Move the byte index to the nearest character boundary (rounding up) and
// convert it to a character index.
self.text
.byte_to_char(self.text.ceil_char_boundary(next_byte_idx as usize))
})
})
.unwrap_or(usize::MAX);
}
fn advance(&mut self) {
let Some(highlighter) = self.inner.as_mut() else {
return;
};
let (event, highlights) = highlighter.advance();
let base = match event {
HighlightEvent::Refresh => self.text_style,
HighlightEvent::Push => self.style,
};
self.style = highlights.fold(base, |acc, highlight| {
acc.patch(self.theme.highlight(highlight))
});
self.update_pos();
}
}
struct OverlayHighlighter<'t> {
inner: syntax::OverlayHighlighter,
pos: usize,
theme: &'t Theme,
style: Style,
}
impl<'t> OverlayHighlighter<'t> {
fn new(overlays: Vec<OverlayHighlights>, theme: &'t Theme) -> Self {
let inner = syntax::OverlayHighlighter::new(overlays);
let mut highlighter = Self {
inner,
pos: 0,
theme,
style: Style::default(),
};
highlighter.update_pos();
highlighter
}
fn update_pos(&mut self) {
self.pos = self.inner.next_event_offset();
}
fn advance(&mut self) {
let (event, highlights) = self.inner.advance();
let base = match event {
HighlightEvent::Refresh => Style::default(),
HighlightEvent::Push => self.style,
};
self.style = highlights.fold(base, |acc, highlight| {
acc.patch(self.theme.highlight(highlight))
});
self.update_pos();
}
}

View File

@@ -17,7 +17,7 @@ use helix_core::{
diagnostic::NumberOrString,
graphemes::{next_grapheme_boundary, prev_grapheme_boundary},
movement::Direction,
syntax::{self, HighlightEvent},
syntax::{self, OverlayHighlights},
text_annotations::TextAnnotations,
unicode::width::UnicodeWidthStr,
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
@@ -31,7 +31,7 @@ use helix_view::{
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
};
use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc};
use std::{mem::take, num::NonZeroUsize, ops, path::PathBuf, rc::Rc};
use tui::{buffer::Buffer as Surface, text::Span};
@@ -87,6 +87,7 @@ impl EditorView {
let area = view.area;
let theme = &editor.theme;
let config = editor.config();
let loader = editor.syn_loader.load();
let view_offset = doc.view_offset(view.id);
@@ -115,51 +116,33 @@ impl EditorView {
decorations.add_decoration(line_decoration);
}
let syntax_highlights =
Self::doc_syntax_highlights(doc, view_offset.anchor, inner.height, theme);
let syntax_highlighter =
Self::doc_syntax_highlighter(doc, view_offset.anchor, inner.height, &loader);
let mut overlays = Vec::new();
let mut overlay_highlights =
Self::empty_highlight_iter(doc, view_offset.anchor, inner.height);
let overlay_syntax_highlights = Self::overlay_syntax_highlights(
overlays.push(Self::overlay_syntax_highlights(
doc,
view_offset.anchor,
inner.height,
&text_annotations,
);
if !overlay_syntax_highlights.is_empty() {
overlay_highlights =
Box::new(syntax::merge(overlay_highlights, overlay_syntax_highlights));
}
));
for diagnostic in Self::doc_diagnostics_highlights(doc, theme) {
// Most of the `diagnostic` Vecs are empty most of the time. Skipping
// a merge for any empty Vec saves a significant amount of work.
if diagnostic.is_empty() {
continue;
}
overlay_highlights = Box::new(syntax::merge(overlay_highlights, diagnostic));
}
Self::doc_diagnostics_highlights_into(doc, theme, &mut overlays);
if is_focused {
if let Some(tabstops) = Self::tabstop_highlights(doc, theme) {
overlay_highlights = Box::new(syntax::merge(overlay_highlights, tabstops));
overlays.push(tabstops);
}
let highlights = syntax::merge(
overlay_highlights,
Self::doc_selection_highlights(
editor.mode(),
doc,
view,
theme,
&config.cursor_shape,
self.terminal_focused,
),
);
let focused_view_elements = Self::highlight_focused_view_elements(view, doc, theme);
if focused_view_elements.is_empty() {
overlay_highlights = Box::new(highlights)
} else {
overlay_highlights = Box::new(syntax::merge(highlights, focused_view_elements))
overlays.push(Self::doc_selection_highlights(
editor.mode(),
doc,
view,
theme,
&config.cursor_shape,
self.terminal_focused,
));
if let Some(overlay) = Self::highlight_focused_view_elements(view, doc, theme) {
overlays.push(overlay);
}
}
@@ -207,8 +190,8 @@ impl EditorView {
doc,
view_offset,
&text_annotations,
syntax_highlights,
overlay_highlights,
syntax_highlighter,
overlays,
theme,
decorations,
);
@@ -287,57 +270,23 @@ impl EditorView {
start..end
}
pub fn empty_highlight_iter(
doc: &Document,
anchor: usize,
height: u16,
) -> Box<dyn Iterator<Item = HighlightEvent>> {
let text = doc.text().slice(..);
let row = text.char_to_line(anchor.min(text.len_chars()));
// Calculate viewport byte ranges:
// Saturating subs to make it inclusive zero indexing.
let range = Self::viewport_byte_range(text, row, height);
Box::new(
[HighlightEvent::Source {
start: text.byte_to_char(range.start),
end: text.byte_to_char(range.end),
}]
.into_iter(),
)
}
/// Get syntax highlights for a document in a view represented by the first line
/// Get the syntax highlighter for a document in a view represented by the first line
/// and column (`offset`) and the last line. This is done instead of using a view
/// directly to enable rendering syntax highlighted docs anywhere (eg. picker preview)
pub fn doc_syntax_highlights<'doc>(
doc: &'doc Document,
pub fn doc_syntax_highlighter<'editor>(
doc: &'editor Document,
anchor: usize,
height: u16,
_theme: &Theme,
) -> Box<dyn Iterator<Item = HighlightEvent> + 'doc> {
loader: &'editor syntax::Loader,
) -> Option<syntax::Highlighter<'editor>> {
let syntax = doc.syntax()?;
let text = doc.text().slice(..);
let row = text.char_to_line(anchor.min(text.len_chars()));
let range = Self::viewport_byte_range(text, row, height);
let range = range.start as u32..range.end as u32;
match doc.syntax() {
Some(syntax) => {
let iter = syntax
// TODO: range doesn't actually restrict source, just highlight range
.highlight_iter(text.slice(..), Some(range), None)
.map(|event| event.unwrap());
Box::new(iter)
}
None => Box::new(
[HighlightEvent::Source {
start: range.start,
end: range.end,
}]
.into_iter(),
),
}
let highlighter = syntax.highlighter(text, loader, range);
Some(highlighter)
}
pub fn overlay_syntax_highlights(
@@ -345,7 +294,7 @@ impl EditorView {
anchor: usize,
height: u16,
text_annotations: &TextAnnotations,
) -> Vec<(usize, std::ops::Range<usize>)> {
) -> OverlayHighlights {
let text = doc.text().slice(..);
let row = text.char_to_line(anchor.min(text.len_chars()));
@@ -356,35 +305,29 @@ impl EditorView {
}
/// Get highlight spans for document diagnostics
pub fn doc_diagnostics_highlights(
pub fn doc_diagnostics_highlights_into(
doc: &Document,
theme: &Theme,
) -> [Vec<(usize, std::ops::Range<usize>)>; 7] {
overlay_highlights: &mut Vec<OverlayHighlights>,
) {
use helix_core::diagnostic::{DiagnosticTag, Range, Severity};
let get_scope_of = |scope| {
theme
.find_scope_index_exact(scope)
// get one of the themes below as fallback values
.or_else(|| theme.find_scope_index_exact("diagnostic"))
.or_else(|| theme.find_scope_index_exact("ui.cursor"))
.or_else(|| theme.find_scope_index_exact("ui.selection"))
.expect(
"at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
)
.find_highlight_exact(scope)
// get one of the themes below as fallback values
.or_else(|| theme.find_highlight_exact("diagnostic"))
.or_else(|| theme.find_highlight_exact("ui.cursor"))
.or_else(|| theme.find_highlight_exact("ui.selection"))
.expect(
"at least one of the following scopes must be defined in the theme: `diagnostic`, `ui.cursor`, or `ui.selection`",
)
};
// basically just queries the theme color defined in the config
let hint = get_scope_of("diagnostic.hint");
let info = get_scope_of("diagnostic.info");
let warning = get_scope_of("diagnostic.warning");
let error = get_scope_of("diagnostic.error");
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
// Diagnostic tags
let unnecessary = theme.find_scope_index_exact("diagnostic.unnecessary");
let deprecated = theme.find_scope_index_exact("diagnostic.deprecated");
let unnecessary = theme.find_highlight_exact("diagnostic.unnecessary");
let deprecated = theme.find_highlight_exact("diagnostic.deprecated");
let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
let mut default_vec = Vec::new();
let mut info_vec = Vec::new();
let mut hint_vec = Vec::new();
let mut warning_vec = Vec::new();
@@ -392,31 +335,30 @@ impl EditorView {
let mut unnecessary_vec = Vec::new();
let mut deprecated_vec = Vec::new();
let push_diagnostic =
|vec: &mut Vec<(usize, std::ops::Range<usize>)>, scope, range: Range| {
// If any diagnostic overlaps ranges with the prior diagnostic,
// merge the two together. Otherwise push a new span.
match vec.last_mut() {
Some((_, existing_range)) if range.start <= existing_range.end => {
// This branch merges overlapping diagnostics, assuming that the current
// diagnostic starts on range.start or later. If this assertion fails,
// we will discard some part of `diagnostic`. This implies that
// `doc.diagnostics()` is not sorted by `diagnostic.range`.
debug_assert!(existing_range.start <= range.start);
existing_range.end = range.end.max(existing_range.end)
}
_ => vec.push((scope, range.start..range.end)),
let push_diagnostic = |vec: &mut Vec<ops::Range<usize>>, range: Range| {
// If any diagnostic overlaps ranges with the prior diagnostic,
// merge the two together. Otherwise push a new span.
match vec.last_mut() {
Some(existing_range) if range.start <= existing_range.end => {
// This branch merges overlapping diagnostics, assuming that the current
// diagnostic starts on range.start or later. If this assertion fails,
// we will discard some part of `diagnostic`. This implies that
// `doc.diagnostics()` is not sorted by `diagnostic.range`.
debug_assert!(existing_range.start <= range.start);
existing_range.end = range.end.max(existing_range.end)
}
};
_ => vec.push(range.start..range.end),
}
};
for diagnostic in doc.diagnostics() {
// Separate diagnostics into different Vecs by severity.
let (vec, scope) = match diagnostic.severity {
Some(Severity::Info) => (&mut info_vec, info),
Some(Severity::Hint) => (&mut hint_vec, hint),
Some(Severity::Warning) => (&mut warning_vec, warning),
Some(Severity::Error) => (&mut error_vec, error),
_ => (&mut default_vec, r#default),
let vec = match diagnostic.severity {
Some(Severity::Info) => &mut info_vec,
Some(Severity::Hint) => &mut hint_vec,
Some(Severity::Warning) => &mut warning_vec,
Some(Severity::Error) => &mut error_vec,
_ => &mut default_vec,
};
// If the diagnostic has tags and a non-warning/error severity, skip rendering
@@ -429,34 +371,59 @@ impl EditorView {
Some(Severity::Warning | Severity::Error)
)
{
push_diagnostic(vec, scope, diagnostic.range);
push_diagnostic(vec, diagnostic.range);
}
for tag in &diagnostic.tags {
match tag {
DiagnosticTag::Unnecessary => {
if let Some(scope) = unnecessary {
push_diagnostic(&mut unnecessary_vec, scope, diagnostic.range)
if unnecessary.is_some() {
push_diagnostic(&mut unnecessary_vec, diagnostic.range)
}
}
DiagnosticTag::Deprecated => {
if let Some(scope) = deprecated {
push_diagnostic(&mut deprecated_vec, scope, diagnostic.range)
if deprecated.is_some() {
push_diagnostic(&mut deprecated_vec, diagnostic.range)
}
}
}
}
}
[
default_vec,
unnecessary_vec,
deprecated_vec,
info_vec,
hint_vec,
warning_vec,
error_vec,
]
overlay_highlights.push(OverlayHighlights::Homogeneous {
highlight: get_scope_of("diagnostic"),
ranges: default_vec,
});
if let Some(highlight) = unnecessary {
overlay_highlights.push(OverlayHighlights::Homogeneous {
highlight,
ranges: unnecessary_vec,
});
}
if let Some(highlight) = deprecated {
overlay_highlights.push(OverlayHighlights::Homogeneous {
highlight,
ranges: deprecated_vec,
});
}
overlay_highlights.extend([
OverlayHighlights::Homogeneous {
highlight: get_scope_of("diagnostic.info"),
ranges: info_vec,
},
OverlayHighlights::Homogeneous {
highlight: get_scope_of("diagnostic.hint"),
ranges: hint_vec,
},
OverlayHighlights::Homogeneous {
highlight: get_scope_of("diagnostic.warning"),
ranges: warning_vec,
},
OverlayHighlights::Homogeneous {
highlight: get_scope_of("diagnostic.error"),
ranges: error_vec,
},
]);
}
/// Get highlight spans for selections in a document view.
@@ -467,7 +434,7 @@ impl EditorView {
theme: &Theme,
cursor_shape_config: &CursorShapeConfig,
is_terminal_focused: bool,
) -> Vec<(usize, std::ops::Range<usize>)> {
) -> OverlayHighlights {
let text = doc.text().slice(..);
let selection = doc.selection(view.id);
let primary_idx = selection.primary_index();
@@ -476,34 +443,34 @@ impl EditorView {
let cursor_is_block = cursorkind == CursorKind::Block;
let selection_scope = theme
.find_scope_index_exact("ui.selection")
.find_highlight_exact("ui.selection")
.expect("could not find `ui.selection` scope in the theme!");
let primary_selection_scope = theme
.find_scope_index_exact("ui.selection.primary")
.find_highlight_exact("ui.selection.primary")
.unwrap_or(selection_scope);
let base_cursor_scope = theme
.find_scope_index_exact("ui.cursor")
.find_highlight_exact("ui.cursor")
.unwrap_or(selection_scope);
let base_primary_cursor_scope = theme
.find_scope_index("ui.cursor.primary")
.find_highlight("ui.cursor.primary")
.unwrap_or(base_cursor_scope);
let cursor_scope = match mode {
Mode::Insert => theme.find_scope_index_exact("ui.cursor.insert"),
Mode::Select => theme.find_scope_index_exact("ui.cursor.select"),
Mode::Normal => theme.find_scope_index_exact("ui.cursor.normal"),
Mode::Insert => theme.find_highlight_exact("ui.cursor.insert"),
Mode::Select => theme.find_highlight_exact("ui.cursor.select"),
Mode::Normal => theme.find_highlight_exact("ui.cursor.normal"),
}
.unwrap_or(base_cursor_scope);
let primary_cursor_scope = match mode {
Mode::Insert => theme.find_scope_index_exact("ui.cursor.primary.insert"),
Mode::Select => theme.find_scope_index_exact("ui.cursor.primary.select"),
Mode::Normal => theme.find_scope_index_exact("ui.cursor.primary.normal"),
Mode::Insert => theme.find_highlight_exact("ui.cursor.primary.insert"),
Mode::Select => theme.find_highlight_exact("ui.cursor.primary.select"),
Mode::Normal => theme.find_highlight_exact("ui.cursor.primary.normal"),
}
.unwrap_or(base_primary_cursor_scope);
let mut spans: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
let mut spans = Vec::new();
for (i, range) in selection.iter().enumerate() {
let selection_is_primary = i == primary_idx;
let (cursor_scope, selection_scope) = if selection_is_primary {
@@ -563,7 +530,7 @@ impl EditorView {
}
}
spans
OverlayHighlights::Heterogenous { highlights: spans }
}
/// Render brace match, etc (meant for the focused view only)
@@ -571,41 +538,24 @@ impl EditorView {
view: &View,
doc: &Document,
theme: &Theme,
) -> Vec<(usize, std::ops::Range<usize>)> {
) -> Option<OverlayHighlights> {
// Highlight matching braces
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
use helix_core::match_brackets;
let pos = doc.selection(view.id).primary().cursor(text);
if let Some(pos) =
match_brackets::find_matching_bracket(syntax, doc.text().slice(..), pos)
{
// ensure col is on screen
if let Some(highlight) = theme.find_scope_index_exact("ui.cursor.match") {
return vec![(highlight, pos..pos + 1)];
}
}
}
Vec::new()
let syntax = doc.syntax()?;
let highlight = theme.find_highlight_exact("ui.cursor.match")?;
let text = doc.text().slice(..);
let pos = doc.selection(view.id).primary().cursor(text);
let pos = helix_core::match_brackets::find_matching_bracket(syntax, text, pos)?;
Some(OverlayHighlights::single(highlight, pos..pos + 1))
}
pub fn tabstop_highlights(
doc: &Document,
theme: &Theme,
) -> Option<Vec<(usize, std::ops::Range<usize>)>> {
pub fn tabstop_highlights(doc: &Document, theme: &Theme) -> Option<OverlayHighlights> {
let snippet = doc.active_snippet.as_ref()?;
let highlight = theme.find_scope_index_exact("tabstop")?;
let mut highlights = Vec::new();
let highlight = theme.find_highlight_exact("tabstop")?;
let mut ranges = Vec::new();
for tabstop in snippet.tabstops() {
highlights.extend(
tabstop
.ranges
.iter()
.map(|range| (highlight, range.start..range.end)),
);
ranges.extend(tabstop.ranges.iter().map(|range| range.start..range.end));
}
(!highlights.is_empty()).then_some(highlights)
Some(OverlayHighlights::Homogeneous { highlight, ranges })
}
/// Render bufferline at the top

View File

@@ -1,7 +1,7 @@
use std::sync::Arc;
use arc_swap::ArcSwap;
use helix_core::syntax;
use helix_core::syntax::{self, OverlayHighlights};
use helix_view::graphics::{Margin, Rect, Style};
use helix_view::input::Event;
use tui::buffer::Buffer;
@@ -94,7 +94,8 @@ impl Component for SignatureHelp {
}
fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
let margin = Margin::horizontal(1);
let margin = Margin::all(1);
let area = area.inner(margin);
let signature = self
.signatures
@@ -102,13 +103,12 @@ impl Component for SignatureHelp {
.unwrap_or_else(|| &self.signatures[0]);
let active_param_span = signature.active_param_range.map(|(start, end)| {
vec![(
cx.editor
.theme
.find_scope_index_exact("ui.selection")
.unwrap(),
start..end,
)]
let highlight = cx
.editor
.theme
.find_highlight_exact("ui.selection")
.unwrap();
OverlayHighlights::single(highlight, start..end)
});
let signature = self
@@ -120,7 +120,7 @@ impl Component for SignatureHelp {
signature.signature.as_str(),
&self.language,
Some(&cx.editor.theme),
Arc::clone(&self.config_loader),
&self.config_loader.load(),
active_param_span,
);
@@ -128,13 +128,15 @@ impl Component for SignatureHelp {
let signature_index = self.signature_index();
let text = Text::from(signature_index);
let paragraph = Paragraph::new(&text).alignment(Alignment::Right);
paragraph.render(area.clip_top(1).with_height(1).clip_right(1), surface);
paragraph.render(area.with_height(1).clip_right(1), surface);
}
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
let sig_text_area = sig_text_area.inner(margin).intersection(surface.area);
let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false });
let sig_text_para = Paragraph::new(&sig_text)
.wrap(Wrap { trim: false })
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
let (_, sig_text_height) = sig_text_para.required_size(area.width);
let sig_text_area = area.with_height(sig_text_height.min(area.height));
let sig_text_area = sig_text_area.intersection(surface.area);
sig_text_para.render(sig_text_area, surface);
if signature.signature_doc.is_none() {
@@ -160,7 +162,7 @@ impl Component for SignatureHelp {
let sig_doc_para = Paragraph::new(&sig_doc)
.wrap(Wrap { trim: false })
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
sig_doc_para.render(sig_doc_area.inner(margin), surface);
sig_doc_para.render(sig_doc_area, surface);
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
@@ -178,11 +180,11 @@ impl Component for SignatureHelp {
signature.signature.as_str(),
&self.language,
None,
Arc::clone(&self.config_loader),
&self.config_loader.load(),
None,
);
let (sig_width, sig_height) =
crate::ui::text::required_size(&signature_text, max_text_width);
let sig_text_para = Paragraph::new(&signature_text).wrap(Wrap { trim: false });
let (sig_width, sig_height) = sig_text_para.required_size(max_text_width);
let (width, height) = match signature.signature_doc {
Some(ref doc) => {

View File

@@ -10,8 +10,8 @@ use std::sync::Arc;
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use helix_core::{
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
RopeSlice,
syntax::{self, HighlightEvent, OverlayHighlights},
RopeSlice, Syntax,
};
use helix_view::{
graphics::{Margin, Rect, Style},
@@ -32,8 +32,12 @@ pub fn highlighted_code_block<'a>(
text: &str,
language: &str,
theme: Option<&Theme>,
config_loader: Arc<ArcSwap<syntax::Loader>>,
additional_highlight_spans: Option<Vec<(usize, std::ops::Range<usize>)>>,
loader: &syntax::Loader,
// Optional overlay highlights to mix in with the syntax highlights.
//
// Note that `OverlayHighlights` is typically used with char indexing but the only caller
// which passes this parameter currently passes **byte indices** instead.
additional_highlight_spans: Option<OverlayHighlights>,
) -> Text<'a> {
let mut spans = Vec::new();
let mut lines = Vec::new();
@@ -48,67 +52,80 @@ pub fn highlighted_code_block<'a>(
};
let ropeslice = RopeSlice::from(text);
let syntax = config_loader
.load()
.language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
language.into(),
))
.and_then(|config| config.highlight_config(theme.scopes()))
.and_then(|config| Syntax::new(ropeslice, config, Arc::clone(&config_loader)));
let syntax = match syntax {
Some(s) => s,
None => return styled_multiline_text(text, code_style),
let Some(syntax) = loader
.language_for_match(RopeSlice::from(language))
.and_then(|lang| Syntax::new(ropeslice, lang, loader).ok())
else {
return styled_multiline_text(text, code_style);
};
let highlight_iter = syntax
.highlight_iter(ropeslice, None, None)
.map(|e| e.unwrap());
let highlight_iter: Box<dyn Iterator<Item = HighlightEvent>> =
if let Some(spans) = additional_highlight_spans {
Box::new(helix_core::syntax::merge(highlight_iter, spans))
} else {
Box::new(highlight_iter)
};
let mut syntax_highlighter = syntax.highlighter(ropeslice, loader, ..);
let mut syntax_highlight_stack = Vec::new();
let mut overlay_highlight_stack = Vec::new();
let mut overlay_highlighter = syntax::OverlayHighlighter::new(additional_highlight_spans);
let mut pos = 0;
let mut highlights = Vec::new();
for event in highlight_iter {
match event {
HighlightEvent::HighlightStart(span) => {
highlights.push(span);
while pos < ropeslice.len_bytes() as u32 {
if pos == syntax_highlighter.next_event_offset() {
let (event, new_highlights) = syntax_highlighter.advance();
if event == HighlightEvent::Refresh {
syntax_highlight_stack.clear();
}
HighlightEvent::HighlightEnd => {
highlights.pop();
syntax_highlight_stack.extend(new_highlights);
} else if pos == overlay_highlighter.next_event_offset() as u32 {
let (event, new_highlights) = overlay_highlighter.advance();
if event == HighlightEvent::Refresh {
overlay_highlight_stack.clear();
}
HighlightEvent::Source { start, end } => {
let style = highlights
.iter()
.fold(text_style, |acc, span| acc.patch(theme.highlight(span.0)));
overlay_highlight_stack.extend(new_highlights)
}
let mut slice = &text[start..end];
// TODO: do we need to handle all unicode line endings
// here, or is just '\n' okay?
while let Some(end) = slice.find('\n') {
// emit span up to newline
let text = &slice[..end];
let text = text.replace('\t', " "); // replace tabs
let span = Span::styled(text, style);
spans.push(span);
let start = pos;
pos = syntax_highlighter
.next_event_offset()
.min(overlay_highlighter.next_event_offset() as u32);
if pos == u32::MAX {
pos = ropeslice.len_bytes() as u32;
}
if pos == start {
continue;
}
// The highlighter should always move forward.
// If the highlighter malfunctions, bail on syntax highlighting and log an error.
debug_assert!(pos > start);
if pos < start {
log::error!("Failed to highlight '{language}': {text:?}");
return styled_multiline_text(text, code_style);
}
// truncate slice to after newline
slice = &slice[end + 1..];
let style = syntax_highlight_stack
.iter()
.chain(overlay_highlight_stack.iter())
.fold(text_style, |acc, highlight| {
acc.patch(theme.highlight(*highlight))
});
// make a new line
let spans = std::mem::take(&mut spans);
lines.push(Spans::from(spans));
}
let mut slice = &text[start as usize..pos as usize];
// TODO: do we need to handle all unicode line endings
// here, or is just '\n' okay?
while let Some(end) = slice.find('\n') {
// emit span up to newline
let text = &slice[..end];
let text = text.replace('\t', " "); // replace tabs
let span = Span::styled(text, style);
spans.push(span);
// if there's anything left, emit it too
if !slice.is_empty() {
let span = Span::styled(slice.replace('\t', " "), style);
spans.push(span);
}
}
// truncate slice to after newline
slice = &slice[end + 1..];
// make a new line
let spans = std::mem::take(&mut spans);
lines.push(Spans::from(spans));
}
if !slice.is_empty() {
let span = Span::styled(slice.replace('\t', " "), style);
spans.push(span);
}
}
@@ -286,7 +303,7 @@ impl Markdown {
&text,
language,
theme,
Arc::clone(&self.config_loader),
&self.config_loader.load(),
None,
);
lines.extend(tui_text.lines.into_iter());

View File

@@ -371,13 +371,15 @@ fn directory_content(path: &Path) -> Result<Vec<(PathBuf, bool)>, std::io::Error
pub mod completers {
use super::Utf8PathBuf;
use crate::ui::prompt::Completion;
use helix_core::command_line::{self, Tokenizer};
use helix_core::fuzzy::fuzzy_match;
use helix_core::syntax::LanguageServerFeature;
use helix_core::syntax::config::LanguageServerFeature;
use helix_view::document::SCRATCH_BUFFER_NAME;
use helix_view::theme;
use helix_view::{editor::Config, Editor};
use once_cell::sync::Lazy;
use std::borrow::Cow;
use std::collections::BTreeSet;
use tui::text::Span;
pub type Completer = fn(&Editor, &str) -> Vec<Completion>;
@@ -677,4 +679,64 @@ pub mod completers {
.map(|(name, _)| ((0..), name.into()))
.collect()
}
pub fn program(_editor: &Editor, input: &str) -> Vec<Completion> {
static PROGRAMS_IN_PATH: Lazy<BTreeSet<String>> = Lazy::new(|| {
// Go through the entire PATH and read all files into a set.
let Some(path) = std::env::var_os("PATH") else {
return Default::default();
};
std::env::split_paths(&path)
.filter_map(|path| std::fs::read_dir(path).ok())
.flatten()
.filter_map(|res| {
let entry = res.ok()?;
let metadata = entry.metadata().ok()?;
if metadata.is_file() || metadata.is_symlink() {
entry.file_name().into_string().ok()
} else {
None
}
})
.collect()
});
fuzzy_match(input, PROGRAMS_IN_PATH.iter(), false)
.into_iter()
.map(|(name, _)| ((0..), name.clone().into()))
.collect()
}
/// This expects input to be a raw string of arguments, because this is what Signature's raw_after does.
pub fn repeating_filenames(editor: &Editor, input: &str) -> Vec<Completion> {
let token = match Tokenizer::new(input, false).last() {
Some(token) => token.unwrap(),
None => return filename(editor, input),
};
let offset = token.content_start;
let mut completions = filename(editor, &input[offset..]);
for completion in completions.iter_mut() {
completion.0.start += offset;
}
completions
}
pub fn shell(editor: &Editor, input: &str) -> Vec<Completion> {
let (command, args, complete_command) = command_line::split(input);
if complete_command {
return program(editor, command);
}
let mut completions = repeating_filenames(editor, args);
for completion in completions.iter_mut() {
// + 1 for separator between `command` and `args`
completion.0.start += command.len() + 1;
}
completions
}
}

View File

@@ -585,8 +585,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
// retrieve the `Arc<Path>` key. The `path` in scope here is a `&Path` and
// we can cheaply clone the key for the preview highlight handler.
let (path, preview) = self.preview_cache.get_key_value(path).unwrap();
if matches!(preview, CachedPreview::Document(doc) if doc.language_config().is_none())
{
if matches!(preview, CachedPreview::Document(doc) if doc.syntax().is_none()) {
helix_event::send_blocking(&self.preview_highlight_handler, path.clone());
}
return Some((Preview::Cached(preview), range));
@@ -624,20 +623,27 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
if content_type.is_binary() {
return Ok(CachedPreview::Binary);
}
Document::open(&path, None, None, editor.config.clone()).map_or(
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot open document",
)),
|doc| {
// Asynchronously highlight the new document
helix_event::send_blocking(
&self.preview_highlight_handler,
path.clone(),
);
Ok(CachedPreview::Document(Box::new(doc)))
},
let mut doc = Document::open(
&path,
None,
false,
editor.config.clone(),
editor.syn_loader.clone(),
)
.or(Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
"Cannot open document",
)))?;
let loader = editor.syn_loader.load();
if let Some(language_config) = doc.detect_language_config(&loader) {
doc.language = Some(language_config);
// Asynchronously highlight the new document
helix_event::send_blocking(
&self.preview_highlight_handler,
path.clone(),
);
}
Ok(CachedPreview::Document(Box::new(doc)))
} else {
Err(std::io::Error::new(
std::io::ErrorKind::NotFound,
@@ -933,21 +939,18 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
}
}
let syntax_highlights = EditorView::doc_syntax_highlights(
let loader = cx.editor.syn_loader.load();
let syntax_highlighter =
EditorView::doc_syntax_highlighter(doc, offset.anchor, area.height, &loader);
let mut overlay_highlights = Vec::new();
EditorView::doc_diagnostics_highlights_into(
doc,
offset.anchor,
area.height,
&cx.editor.theme,
&mut overlay_highlights,
);
let mut overlay_highlights =
EditorView::empty_highlight_iter(doc, offset.anchor, area.height);
for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) {
if spans.is_empty() {
continue;
}
overlay_highlights = Box::new(helix_core::syntax::merge(overlay_highlights, spans));
}
let mut decorations = DecorationManager::default();
if let Some((start, end)) = range {
@@ -977,7 +980,7 @@ impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Picker<T, D> {
offset,
// TODO: compute text annotations asynchronously here (like inlay hints)
&TextAnnotations::default(),
syntax_highlights,
syntax_highlighter,
overlay_highlights,
&cx.editor.theme,
decorations,

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