Compare commits

..

169 Commits

Author SHA1 Message Date
Pascal Kuthe
ae0d4189e1 tmp 2024-07-27 15:43:44 +02:00
Pascal Kuthe
f7686d7af2 getting somewhere 2024-07-20 04:37:47 +02:00
Pascal Kuthe
29ffc20290 progress 2024-07-15 18:26:26 +02:00
Pascal Kuthe
1bd36f528a tmp 2024-07-15 18:25:11 +02:00
Pascal Kuthe
0cd8817d06 tmp 2024-07-15 18:25:08 +02:00
Pascal Kuthe
782a34941e vendor tree-sitter lib 2024-07-15 18:23:41 +02:00
Pascal Kuthe
37397ecc6d tmp 2024-07-15 18:23:41 +02:00
Pascal Kuthe
c4b7b08809 move syntax highlighting to separate crate 2024-07-15 18:23:24 +02:00
Blaž Hrastnik
08ee8b9443 Merge pull request #9647 from helix-editor/pickers-v2
`Picker`s "v2"
2024-07-15 23:30:58 +09:00
Michael Davis
9de5f5cefa Picker: Highlight the currently active column
We can track the ranges in the input text that correspond to each column
and use this information during rendering to apply a new theme key that
makes the "active column" stand out. This makes it easier to tell at
a glance which column you're entering.
2024-07-15 10:03:35 -04:00
FlorianNAdam
6345b78409 Keep editor from switching to normal mode when loading a Document (#11176) 2024-07-15 22:48:37 +09:00
Michael Davis
a7777b3c11 Accept 'IntoIterator<Item = Column<T, D>>' for picker columns
This allows us to replace any `vec![..]`s of columns where all columns
are static with static slices `[..]`.
2024-07-15 09:31:33 -04:00
Michael Davis
009bbdaf8d Picker: Reset the cursor on prompt change 2024-07-15 09:31:33 -04:00
Michael Davis
8555248b01 Accept 'IntoIterator<Item = T>' for Picker::new options
`Picker::new` loops through the input options to inject each of them, so
there's no need to collect into an intermediary Vec. This removes some
unnecessary collections. Also, pickers that start with no initial
options can now pass an empty slice instead of an empty Vec.

Co-authored-by: Luis Useche <useche@gmail.com>
2024-07-15 09:31:33 -04:00
Michael Davis
3906f6605f Avoid allocations in Picker file preview callback
The `FileLocation` and `PathOrId` types can borrow paths rather than
requiring them to be owned. This takes a refactor of the preview
functions and preview internals within `Picker`. With this change we
avoid an unnecessary `PathBuf` clone per render for any picker with a
file preview function (i.e. most pickers).

This refactor is not fully complete. The `PathOrId` is _sometimes_ an
owned `PathBuf`. This is for pragmatic reasons rather than technical
ones. We need a further refactor to introduce more core types like
`Location` in order to eliminate the Cow and only use `&Path`s within
`PathOrId`. This is left for future work as it will be a larger refactor
almost entirely fitting into the LSP commands module and helix-core -
i.e. mostly unrelated to refactoring the `Picker` code itself.

Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2024-07-15 09:31:33 -04:00
Michael Davis
f4a433f855 Convert LSP URIs into custom URIs
This introduces a custom URI type in core meant to be extended later
if we want to support other schemes. For now it's just a wrapper over a
PathBuf. We use this new URI type to firewall `lsp::Url`. This was
previously done in 8141a4a but using a custom URI type is more flexible
and will improve the way Pickers handle paths for previews in the child
commit(s).

Co-authored-by: soqb <cb.setho@gmail.com>
2024-07-15 09:31:33 -04:00
Pascal Kuthe
408282097f avoid collecting columns to a temporary vec 2024-07-15 09:31:33 -04:00
Michael Davis
6ccbfe9bdf Request a UI redraw on Drop of an Injector
This fixes the changed files picker when used against a clean worktree
for example. Without it the running indicator does not disappear. It
also simplifies the dynamic query handler's implementation so that it
doesn't need to request a redraw explicitly.

Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2024-07-15 09:31:33 -04:00
Michael Davis
6492f17e7d Add a hidden column for the global search line contents
We could expand on this in the future to have different preview modes
that you can toggle between with C-t. Currently that binding just hides
the preview but it could switch between different preview modes and in
one mode hide the path and just show the line contents.
2024-07-15 09:31:32 -04:00
Michael Davis
7b1131adf6 global_search: Suggest latest '/' register value 2024-07-15 09:31:32 -04:00
Michael Davis
1d023b05ac Refactor global_search as a dynamic Picker 2024-07-15 09:31:32 -04:00
Michael Davis
5622db6798 Remove sym_picker helper fun
The parent commit split out the workspace symbol picker to an inline
definition so the `workspace` parameter is never passed as `true`. We
should consolidate this picker definition into the symbol_picker
function.
2024-07-15 09:31:32 -04:00
Michael Davis
9e31ba5475 Consolidate DynamicPicker into Picker
DynamicPicker is a thin wrapper over Picker that holds some additional
state, similar to the old FilePicker type. Like with FilePicker, we want
to fold the two types together, having Picker optionally hold that
extra state.

The DynamicPicker is a little more complicated than FilePicker was
though - it holds a query callback and current query string in state and
provides some debounce for queries using the IdleTimeout event.
We can move all of that state and debounce logic into an AsyncHook
implementation, introduced here as `DynamicQueryHandler`. The hook
receives updates to the primary query and debounces those events so
that once a query has been idle for a short time (275ms) we re-run
the query.

A standard Picker created through `new` for example can be promoted into
a Dynamic picker by chaining the new `with_dynamic_query` function, very
similar to FilePicker's replacement `with_preview`.

The workspace symbol picker has been migrated to the new way of writing
dynamic pickers as an example. The child commit will promote global
search into a dynamic Picker as well.
2024-07-15 09:31:32 -04:00
Michael Davis
11f809c177 Bump nucleo to v0.4.1
We will use this in the child commit to improve the picker's running
indicator. Nucleo 0.4.0 includes an `active_injectors` member that we
can use to detect if anything can push to the picker. When that count
drops to zero we can remove the running indicator.

Nucleo 0.4.1 contains a fix for crashes with interactive global search
on a large directory.
2024-07-15 09:31:32 -04:00
Michael Davis
2c9f5b3efb Implement Error for InjectorShutdown 2024-07-15 09:31:32 -04:00
Michael Davis
53ac833efb Replace picker shutdown bool with version number
This works nicely for dynamic pickers: we stop any running jobs like
global search that are pushing to the injector by incrementing the
version number when we start a new request. The boolean only allowed
us to shut the picker down once, but with a usize a picker can have
multiple "sessions" / "life-cycles" where it receives new options
from an injector.
2024-07-15 09:31:32 -04:00
Michael Davis
385b398808 Add column configurations for existing pickers
This removes the menu::Item implementations for picker item types and
adds `Vec<Column<T, D>>` configurations.
2024-07-15 09:31:32 -04:00
Michael Davis
c4c17c693d Add a special query syntax for Pickers to select columns
Now that the picker is defined as a table, we need a way to provide
input for each field in the picker. We introduce a small query syntax
that supports multiple columns without being too verbose. Fields are
specified as `%field pattern`. The default column for a picker doesn't
need the `%field` prefix. The field name may be selected by a prefix
of the field, for example `%p foo.rs` rather than `%path foo.rs`.

Co-authored-by: ItsEthra <107059409+ItsEthra@users.noreply.github.com>
2024-07-15 09:31:31 -04:00
Michael Davis
f40fca88e0 Refactor Picker in terms of columns
`menu::Item` is replaced with column configurations for each picker
which control how a column is displayed and whether it is passed to
nucleo for filtering. (This is used for dynamic pickers so that we can
filter those items with the dynamic picker callback rather than nucleo.)

The picker has a new lucene-like syntax that can be used to filter the
picker only on certain criteria. If a filter is not specified, the text
in the prompt applies to the picker's configured "primary" column.

Adding column configurations for each picker is left for the child
commit.
2024-07-15 09:31:31 -04:00
Michael Davis
dae3841a75 Use an AsyncHook for picker preview highlighting
The picker previously used the IdleTimeout event as a trigger for
syntax-highlighting the currently selected document in the preview pane.
This is a bit ad-hoc now that the event system has landed and we can
refactor towards an AsyncHook (like those used for LSP completion and
signature-help). This should resolve some odd scenarios where the
preview did not highlight because of a race between the idle timeout
and items appearing in the picker.
2024-07-15 09:31:31 -04:00
Emran Ramezan
702a96d417 Update highlights.scm and injections.scm for blade.php files (#11138)
* Update highlights.scm for blade.php files

* Update injections.scm to add tree-sitter-comment injection

* Fixed the injection issues  regarding blade parameters
2024-07-15 22:29:14 +09:00
Masanori Ogino
7f77d95c79 Inject the comment grammar into Hare (#11173) 2024-07-15 22:28:23 +09:00
arcofx
1bad3b0dd4 Make format_selections respect document configuration (#11169) 2024-07-15 22:27:42 +09:00
Blaž Hrastnik
079f544260 Adjust the ruler color of the default theme 2024-07-15 01:19:09 +09:00
Michael Davis
b2cc7d8fea Add changelog notes for 24.07 (#10731)
* Changelog 2024-05-02

checkpoint: 31273c69e0

* Changelog 2024-05-06

checkpoint: 61818996c6

* Changelog 2024-05-11

checkpoint: 00e9e5eade

* Bump version to 24.05

* Add 24.05 release to AppImage metadata

* Fix release number in changelog

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>

* Update release numbers to 24.07

* Changelog 2024-06-15

* Changelog 2024-07-14

checkpoint: c9b484097b

* Linkify

---------

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2024-07-15 01:17:49 +09:00
Masanori Ogino
c9b484097b Exclude EOL repos from the Repology badge (#11159) 2024-07-14 11:31:33 +09:00
eh
e6bf97b843 Theme: Kanagawa Dragon (#10172)
Implements the Dragon variant of the Kanagawa theme.
https://github.com/rebelot/kanagawa.nvim?tab=readme-ov-file
2024-07-14 03:01:58 +09:00
Michael Davis
44d2fc2ab3 Commit an undo checkpoint before each write (#11062)
This fixes the modification indicator when saving from insert mode with
a config such as

    [keys.insert]
    C-s = ":write"

Previously the modification indicator would be stuck showing modified
even if the buffer contents matched the disk contents when writing after
some changes in insert mode with this binding. In insert mode we do not
eagerly write undo checkpoints so that all changes made become one
checkpoint as you exit insert mode. When saving, `Document`s `changes`
`ChangeSet` would be non-empty and when there are changes we show the
buffer as modified. Then switching to normal mode would append the
changes to history, bumping the current revision past what it was when
last saved. Since the last saved revision and current revision were then
unsynced, the modification indicator would always show modified.

This matches [Kakoune's behavior]. Kakoune has a different architecture
for writes but a very similar system for history, transactions and undo
checkpoints (what it calls "undo groups"). Upon saving Kakoune creates
an undo checkpoint if there are any uncommitted changes. It does this
after the write has gone through since its writing system is different.
For our writing system it's cleaner to make the undo checkpoint before
performing the save so that the history revision increments before we
send the save event.

[Kakoune's behavior]: 80fcfebca8/src/buffer.cc (L565-L566)
2024-07-14 02:59:47 +09:00
Lukas Grassauer
35f1c2a55f Update tree-sitter-todotxt (#11097)
Update to latest commit that allows any non-whitespace character for projects, and contexts.
2024-07-14 02:59:21 +09:00
David Else
928e3f0d85 Add regex injections into bash (#11112) 2024-07-14 02:59:07 +09:00
Masanori Ogino
ec0bdb3976 Update Hare grammar (#11130)
This change uses <https://git.sr.ht/~ecs/tree-sitter-hare/> that is
up-to-date and linked from the official documentation.
2024-07-14 02:58:22 +09:00
Antonin
b0f3fe7556 Include .yml files in Helm chart templates (#11135) 2024-07-14 02:58:06 +09:00
RoloEdits
fd7b1a3e37 refactor(commands): trim end of pipe-like output (#10952) 2024-07-13 10:44:48 +09:00
David Else
a75b1cf51e Fix ZSH completions (#11133) 2024-07-12 11:55:10 +09:00
David Else
501af93c92 Documentation: Convert links in the .desktop file to absolute paths (#11115) 2024-07-12 03:16:55 +02:00
Branch Vincent
2d1ac0f699 Add {pdm,uv}.lock, git/ignore, npmrc to languages (#11131) 2024-07-12 03:15:40 +02:00
David Else
8229a40da8 Add space back to main text in the tutor after chapter 11 (#11117) 2024-07-11 15:47:49 +09:00
David Else
9d75385062 Update ZSH completions (#11120) 2024-07-11 09:06:50 +09:00
baiyang1919813
649bd4501e Add basedpyright langserver (#11121) 2024-07-10 19:28:11 +02:00
Yuntao Zhao
86982ab33c feat: improve hx fish completion (#10853)
* feat: improve hx fish completion

- add -w and --working-dir options
- shorten option description
- dynamically call hx --health

* feat: improve health check completion

- remove header
- remove check/x characters

* feat: use hx --health languages in completion
2024-07-10 12:41:12 +09:00
Michael Davis
15a26b87c9 Expand tilde for selected paths in goto_file (#10964)
Previously `gf` on `~/.config/helix` for example would error if the
entire path was selected but succeed and open a picker for the directory
contents if the selection was one one-width cursor. We need to expand
tildes for all paths instead of just the auto-detected paths.

This also refactors the `goto_file` blocks a little so that we construct
`paths` once instead of creating the Vec and immediately clearing it
when the selection is one single-width cursor.
2024-07-10 12:40:43 +09:00
q
bfb7023656 Update fleet_dark.toml (#11046) 2024-07-10 12:40:03 +09:00
kanielrkirby
86e4b51416 Add changes for undo in insert mode (#11090)
* Add changes before insert mode undo
Fixes #11077

* Address edge cases for undo like Kakoune does

---------

Co-authored-by: Kaniel Kirby <pirate7007@runbox.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-07-10 12:38:50 +09:00
David Else
71df2428ee Fix heredoc and add ansi_c_string highlights in bash queries (#11118) 2024-07-10 12:37:25 +09:00
dependabot[bot]
348c0ebeb4 build(deps): bump the rust-dependencies group with 5 updates (#11113)
Bumps the rust-dependencies group with 5 updates:

| Package | From | To |
| --- | --- | --- |
| [serde](https://github.com/serde-rs/serde) | `1.0.203` | `1.0.204` |
| [imara-diff](https://github.com/pascalkuthe/imara-diff) | `0.1.5` | `0.1.6` |
| [clipboard-win](https://github.com/DoumanAsh/clipboard-win) | `5.3.1` | `5.4.0` |
| [open](https://github.com/Byron/open-rs) | `5.1.4` | `5.2.0` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.0.104` | `1.0.106` |


Updates `serde` from 1.0.203 to 1.0.204
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.203...v1.0.204)

Updates `imara-diff` from 0.1.5 to 0.1.6
- [Release notes](https://github.com/pascalkuthe/imara-diff/releases)
- [Changelog](https://github.com/pascalkuthe/imara-diff/blob/master/CHANGELOG.md)
- [Commits](https://github.com/pascalkuthe/imara-diff/compare/v0.1.5...v0.1.6)

Updates `clipboard-win` from 5.3.1 to 5.4.0
- [Commits](https://github.com/DoumanAsh/clipboard-win/commits)

Updates `open` from 5.1.4 to 5.2.0
- [Release notes](https://github.com/Byron/open-rs/releases)
- [Changelog](https://github.com/Byron/open-rs/blob/main/changelog.md)
- [Commits](https://github.com/Byron/open-rs/compare/v5.1.4...v5.2.0)

Updates `cc` from 1.0.104 to 1.0.106
- [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.0.104...cc-v1.0.106)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: imara-diff
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: clipboard-win
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: open
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-09 19:40:22 +09:00
Ricardo Fernández Serrata
6997ee9151 &Option<T> -> Option<&T> (#11091)
* refactor `starting_request_args` to only `ref` non-`Copy`

* refactor `needs_recompile` to only `ref` non-`Copy`

* refactor `add_workspace_folder` to only `ref` `Some`

---------

Co-authored-by: Rudxain <rudxain@localhost.localdomain>
2024-07-09 18:34:28 +09:00
Salman Farooq
0c8d51ee36 add cursorcolumn and cursorline to base16_transparent theme (#11099) 2024-07-07 11:24:04 +09:00
Matt Armstrong
8b7c33d00d Minor improvements to comments in selection.rs (#11101) 2024-07-07 02:40:23 +09:00
Michael Davis
1fb99ec3b2 Fix language server ID type in lsp_workspace_command (#11105) 2024-07-07 02:39:55 +09:00
Schmiddiii
06d8fee048 Allow multiple language server with lsp-workspace-command (#10176)
This fix allows for multiple language servers at once which support
workspace commands. This was previously broken as just the first
language server supporting workspace commands was queried when listing
allowed worspace commands.

The fix is in two parts. Firstly, querying all workspace commands from
all language servers available and using them when actually running the
command in `lsp_workspace_command`. Secondly, doing the same in
`completers::lsp_workspace_command` such that completion still works as
expected.

The fix has one remaining issue, which I am unsure how to handle in the
best way possible, but which I also don't think should happen often:
Multiple language servers may register commands with the same name. This
will lead to that command being listed in the popup menu and in the
completion list multiple times, which can be possibly confusing. One
could disambigue them in the popover menu, but I am not sure the same
can be done for completion. When running `lsp-workspace-command` with
parameters, this behavior is "fixed" by displaying an error in that
case. I am unsure if this is the best fix for this issue in that case,
but could not find a better one.
2024-07-06 10:34:33 -05:00
Charlie Moog
fc97ecc3e3 Add hsc filetype to haskell (#11074) 2024-07-02 17:36:29 +09:00
adriaan
3524060ee8 Override far too dark cursorline (#11071) 2024-07-02 09:56:55 +09:00
Robin
64f8660d3e Tell language servers that Helix can request formatting (#11064)
Without providing the formatting capability, the language server might not advertise its ability to format in return, causing the :format command to be broken.
2024-07-02 09:42:51 +09:00
Christopher Smyth
ed761fbe7c Bump time from broken version (0.3.23) (#11065) 2024-07-02 09:38:30 +09:00
Lucas @ StarkWare
0c6ffe192b chore: update cairo tree sitter + queries (#11067) 2024-07-02 09:37:49 +09:00
dependabot[bot]
9c4c66d417 build(deps): bump the rust-dependencies group with 3 updates (#11072)
Bumps the rust-dependencies group with 3 updates: [log](https://github.com/rust-lang/log), [serde_json](https://github.com/serde-rs/json) and [cc](https://github.com/rust-lang/cc-rs).


Updates `log` from 0.4.21 to 0.4.22
- [Release notes](https://github.com/rust-lang/log/releases)
- [Changelog](https://github.com/rust-lang/log/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/log/compare/0.4.21...0.4.22)

Updates `serde_json` from 1.0.117 to 1.0.120
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.117...v1.0.120)

Updates `cc` from 1.0.100 to 1.0.104
- [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.0.100...cc-v1.0.104)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-07-02 09:37:33 +09:00
Mark Murphy
21fd654cc6 Fix homebrew formula link (#11058)
Co-authored-by: Mark Murphy <mail@markmurphydev.com>
2024-06-30 16:07:57 +09:00
Chris44442
c6dbb9c270 VHDL highlights.scm improvement (#10845) 2024-06-29 15:30:38 +09:00
Michael Davis
dca952c03a Delay auto-save until exiting insert mode (#11047)
Saving while in insert mode causes issues with the modification
indicator and this is very easy to reproduce with the current state of
the auto-save hook. We can tweak the hook slightly to await the mode
switch out of insert mode to perform the save.

The debounce is preserved: if you save and then immediately exit insert
mode the debounce will be respected. If the debounce lapses while you
are in insert mode, the save occurs as you switch out of insert mode
immediately.
2024-06-29 11:08:21 +09:00
Chirikumbrah
b4811f7d2e Large Gruvbox refactoring (#10773)
* gruvbox refactoring

* removed unnecessary lines

* set purple1 for operators

* changed diagnostics colors

* removed some unnecessary lines

* set diff.delta color to yellow

* removed some tag colors
2024-06-26 23:05:01 +02:00
Michael Jones
0e46f56f30 Add new color theme 'iroaseta' (#10381)
* Add new color theme 'iroaseta'

* Update runtime/themes/iroaseta.toml

Co-authored-by: postsolar <120750161+postsolar@users.noreply.github.com>

* Update iroaseta.toml

Add virtual jump label theme setting

* Update runtime/themes/iroaseta.toml

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

* Update iroaseta.toml

update storage. keyword.storage. according to suggestion, and update color.

* Update iroaseta.toml

remove unused palette

* Update iroaseta.toml

add missing setting for bufferline

* Update iroaseta.toml

update diagnostic fg color

* Update iroaseta.toml

I made the config more comprehensive and took all available themes settings from the manual. Some are commented out though.

* Update iroaseta.toml

add missing colors

* Update iroaseta.toml

Made some final adjustments to the color theme to improve visibility and reduce eye strain.

* Update runtime/themes/iroaseta.toml

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

* Update runtime/themes/iroaseta.toml

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

* Update iroaseta.toml

remove redundant settings

* Update iroaseta.toml

update color name

---------

Co-authored-by: postsolar <120750161+postsolar@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-06-26 23:04:17 +02:00
Thomas Schafer
6ed0d0cd39 Only pluralise "buffer" when required (#11018)
* Only pluralise buffer when required

* Use == 1 instead of != 1
2024-06-26 23:03:21 +02:00
dependabot[bot]
b05ed9bf85 build(deps): bump the rust-dependencies group with 4 updates (#11032)
Bumps the rust-dependencies group with 4 updates: [bitflags](https://github.com/bitflags/bitflags), [url](https://github.com/servo/rust-url), [cc](https://github.com/rust-lang/cc-rs) and [libloading](https://github.com/nagisa/rust_libloading).


Updates `bitflags` from 2.5.0 to 2.6.0
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/2.5.0...2.6.0)

Updates `url` from 2.5.1 to 2.5.2
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.1...v2.5.2)

Updates `cc` from 1.0.99 to 1.0.100
- [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/1.0.99...cc-v1.0.100)

Updates `libloading` from 0.8.3 to 0.8.4
- [Commits](https://github.com/nagisa/rust_libloading/compare/0.8.3...0.8.4)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libloading
  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>
2024-06-25 23:22:20 +09:00
Ashley Vaughn
a982e5ce26 add ruler at 101 and text-width at 100 to lean in languages.toml (#10969) 2024-06-23 10:09:39 +09:00
Alejandro Gastón Alvarez
b894cf087b Add "jsonl" as filetype for JSON lang (#11004) 2024-06-23 10:08:20 +09:00
tingerrr
3706c0dc85 Add block comment tokens for typst (#10955) 2024-06-23 10:07:46 +09:00
J. Dekker
3b5f2e66fc base16_default: add styles to newer unthemed features (#10858)
* base16_default: add `ui.statusline` for `color-modes`

Signed-off-by: J. Dekker <jdek@itanimul.li>

* base16_default: add `ui.virtual` default

Previously virtual text such as LSP inlay was impossible to distinguish
from 'real' text by default.

Signed-off-by: J. Dekker <jdek@itanimul.li>

---------

Signed-off-by: J. Dekker <jdek@itanimul.li>
2024-06-23 10:07:13 +09:00
Michael Davis
44e113cb76 tree-sitter: Update parent links on reused injection layers (#10978)
When parsing injections, we skip adding a new layer if there is an
existing layer covering the same range. When doing so we did not update
the parent layer ID, so some layers could have `parent` layer IDs that
pointed to a layer that no longer existed in the `layers` HopSlotMap
which could cause a panic when using `A-o`.

To fix this we update the `parent` pointer for both newly created
injection layers and reused ones.
2024-06-23 10:06:15 +09:00
Kirawi
b55cb3aa11 Revert "Replace unicode-general-category with icu-properties (#10989)" (#11006)
This reverts commit 9b7dffbd61.
2024-06-23 10:05:53 +09:00
Kirawi
9b7dffbd61 Replace unicode-general-category with icu-properties (#10989) 2024-06-18 18:19:05 +02:00
blt-r
0edf60964d Update tree-sitter-rust (#10973)
Update to latest commit on master to include fix for a bug that doesn't
allow spaces in the shebang line.
2024-06-18 18:14:41 +02:00
TiredTumblrina
94a9c81eb0 Prevent improper files (like /dev/random) from being used as file arguments (#10733)
* Implement check before adding path to files

* fix problem where directories were removed from args.files

* Revert "Implement check before adding path to files"

This reverts commit c123944d9b.

* Dissallow opening of irregular non-symlink files

* Fixed issue with creating new file from command line

* Fixed linting error.

* Optimized regularity check as suggested in review

* Created DocumentOpenError Sum Type to switch on in Application

* Forgot cargo fmt

* Update helix-term/src/application.rs

Accept suggestion in review.

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

* Moved thiserror version configuration to the workspace instead of the individual packages.

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-06-18 18:14:17 +02:00
David Else
d70f58da10 Fix multiple broken links in the documentation (#10953)
* Fix multiple broken links in the documentation

* Apply code review suggestion

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

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-06-18 10:43:36 +02:00
Thomas Schafer
668f1239a9 Fix jump_backwards behaviour when jumplist is at capacity (#10968)
* Fix jump_backwards behaviour when jumplist is at capacity

* Decrement self.current while popping from front

* Fix issue with conflicting updates to self.current

* Realised that truncate is intentional

* Use saturating_sub when decrementing current

* Fix naming of previous jump, and remove unneeded comment change

* Remove unnecessary changes in push

* Return num elements removed from front, and use in backward method

* Hide num_removed from public interface and tidy up jump location check
2024-06-18 10:42:46 +02:00
adiabatic
69acf66cd8 Add curly single and double quotes to BRACKETS (#10971) 2024-06-18 10:40:19 +02:00
Meris Bahtijaragic
afe9049a0e improve jump colors for github_dark themes (#10946) 2024-06-18 10:39:56 +02:00
Shaun_Sheep
bc73dd19d3 Make prompt use cursor set for Insert mode (#10945)
* Resolve issue #10874

* cargo fmt
2024-06-18 10:38:56 +02:00
slawomirlech
dbacaaddca DAP: Deserialize number IDs (#10943)
* Fix deserialization of id

* Removing external dependencies

This reverts commit 27962afc16.

* Fix incorrect import

* Adding tests

* Moved tests

---------

Co-authored-by: Sławomir Lech <slawomir.lech@bluesoft.com>
2024-06-15 16:05:04 +09:00
uncenter
43cc30d225 Sync latest Catppuccin themes changes (#10954) 2024-06-15 11:34:33 +09:00
Michael Davis
8eda96de6d Downgrade unicode-width to 0.1.12 (#10963)
unicode-width 0.1.13 contains some fixes that change the widths of line
endings, which breaks some assumptions in helix-tui, causing some
rendering artifacts. We can downgrade to remove the rendering errors
for now.
2024-06-15 10:58:14 +09:00
RoloEdits
9c479e6d2d fix(editor): prevent overflow in count modifier (#10930) 2024-06-13 03:24:24 +02:00
Sebastian Poeplau
62655e97f1 Optional history for rename_symbol (#10932)
Fix #10560 by accepting an optional history register for the
rename_symbol command.
2024-06-12 16:44:47 +02:00
Lucas @ StarkWare
9123d3fbb8 feat(cairo): update tree-sitter grammar and queries (#10919)
* feat(cairo): update tree-sitter grammar and queries

* fix suggestions
2024-06-12 09:20:13 +09:00
dependabot[bot]
8a549b767b build(deps): bump the rust-dependencies group across 1 directory with 5 updates (#10926)
* build(deps): bump the rust-dependencies group across 1 directory with 5 updates

Bumps the rust-dependencies group with 5 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [unicode-width](https://github.com/unicode-rs/unicode-width) | `0.1.12` | `0.1.13` |
| [regex](https://github.com/rust-lang/regex) | `1.10.4` | `1.10.5` |
| [url](https://github.com/servo/rust-url) | `2.5.0` | `2.5.1` |
| [open](https://github.com/Byron/open-rs) | `5.1.3` | `5.1.4` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.0.98` | `1.0.99` |



Updates `unicode-width` from 0.1.12 to 0.1.13
- [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.1.12...v0.1.13)

Updates `regex` from 1.10.4 to 1.10.5
- [Release notes](https://github.com/rust-lang/regex/releases)
- [Changelog](https://github.com/rust-lang/regex/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/regex/compare/1.10.4...1.10.5)

Updates `url` from 2.5.0 to 2.5.1
- [Release notes](https://github.com/servo/rust-url/releases)
- [Commits](https://github.com/servo/rust-url/compare/v2.5.0...v2.5.1)

Updates `open` from 5.1.3 to 5.1.4
- [Release notes](https://github.com/Byron/open-rs/releases)
- [Changelog](https://github.com/Byron/open-rs/blob/main/changelog.md)
- [Commits](https://github.com/Byron/open-rs/compare/v5.1.3...v5.1.4)

Updates `cc` from 1.0.98 to 1.0.99
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.98...1.0.99)

---
updated-dependencies:
- dependency-name: unicode-width
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: regex
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: url
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: open
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

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

* helix-tui: Use zero-width-space for zero-width grapheme test

The update of unicode-width 0.1.13 in the parent commit changed the
width of the U+1 codepoint to 1 from 0, causing the test to fail. We
can switch to a well known zero-width codepoint of U+200B to fix the
behavior.

---------

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>
2024-06-12 01:08:50 +09:00
emilylime
a64dbf825f Improve readability of virtual text with 'noctis' theme (#10910) 2024-06-11 08:16:54 -05:00
Hendrik Wolff
265608a3d8 Auto Save All Buffers After A Delay (#10899)
* auto save after delay

* configable

* clearer names

* init

* working with some odd behaviour

* working with greater consistency

* Apply reviewer suggestions

- Remove unneccessary field
- Remove blocking save

* Improve auto-save configuration

Auto save can be configured to trigger on focus loss:
```toml
auto-save.focus-lost = true|false
```

and after a time delay (in milli seconds) since last keypress:
```toml
auto-save.after-delay.enable = true|false
auto-save.after-delay.timeout = [0, u64::MAX] # default: 3000
```

* Remove boilerplate and unnecessary types

* Remove more useless types

* Update docs for auto-save.after-delay

* Fix wording of (doc) comments relating to auto-save

* book: Move auto-save descriptions to separate section

---------

Co-authored-by: Miguel Perez <miguelvojito@gmail.com>
Co-authored-by: Miguel Perez <perezoji@cs.fsu.edu>
2024-06-11 00:39:06 +02:00
Asger Juul Brunshøj
a1cda3c19e in flake mkShell default RUSTFLAGS to an empty string if unset (#10880) 2024-06-10 17:08:39 +02:00
Chris Pyles
03813bbc2e Remove special handling of line ending characters in selection replacement (#10786)
* Remove special-casing of line ending characters in selection replacement

* Refactor line ending handling and integration test to address code review comments
2024-06-10 17:07:43 +02:00
Arturs Krumins
aa1630a41a Update Swift Grammar and Queries (#10802) 2024-06-07 12:29:42 -05:00
Zoey Hewll
44504b720b add elisp support (#10644)
* add elisp support

* update queries for some constants
2024-06-07 13:02:27 +09:00
tingerrr
80e0e98e45 Add py, hs, rs and typ injection regexes (#10785)
* Add `py` as valid python injection regex

* Add `hs` and `rs` for `haskell` and `rust`

* Add `typ` injection regex for `typst`
2024-06-07 12:58:26 +09:00
Ricardo Fernández Serrata
3a03109a99 "it's" -> "its", in crossterm.rs (#10860) 2024-06-07 12:51:40 +09:00
Ryan Roden-Corrent
886d307b9e Fix logic to update version when HEAD changes. (#10896) 2024-06-07 08:24:33 +09:00
Michael Davis
6f1437e9f3 LSP: Resolve completion items when any info is missing (#10873) 2024-06-06 13:28:10 +09:00
Marty
c39cde8fc2 Flush pending writes before suspend (#10797)
* flush saves before suspending

* review suggestion

Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com>

* review changes

---------

Co-authored-by: PotatoesFall <martyk@tuta.io>
Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com>
2024-06-05 18:47:15 +02:00
dependabot[bot]
31bcde360c build(deps): bump toml in the rust-dependencies group (#10879)
Bumps the rust-dependencies group with 1 update: [toml](https://github.com/toml-rs/toml).


Updates `toml` from 0.8.13 to 0.8.14
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.13...toml-v0.8.14)

---
updated-dependencies:
- dependency-name: toml
  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>
2024-06-04 10:28:17 +09:00
Gaëtan Lehmann
a801044152 update tree-sitter-earthfile to 0.5.3 (#10779) 2024-06-03 08:40:30 -05:00
dependabot[bot]
6dbab51f4a build(deps): bump the rust-dependencies group across 1 directory with 10 updates (#10871)
Bumps the rust-dependencies group with 10 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [serde](https://github.com/serde-rs/serde) | `1.0.201` | `1.0.203` |
| [toml](https://github.com/toml-rs/toml) | `0.8.12` | `0.8.13` |
| [parking_lot](https://github.com/Amanieu/parking_lot) | `0.12.2` | `0.12.3` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.83` | `1.0.86` |
| [tokio](https://github.com/tokio-rs/tokio) | `1.37.0` | `1.38.0` |
| [libc](https://github.com/rust-lang/libc) | `0.2.154` | `0.2.155` |
| [pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) | `0.10.3` | `0.11.0` |
| [open](https://github.com/Byron/open-rs) | `5.1.2` | `5.1.3` |
| [thiserror](https://github.com/dtolnay/thiserror) | `1.0.60` | `1.0.61` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.0.97` | `1.0.98` |



Updates `serde` from 1.0.201 to 1.0.203
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.201...v1.0.203)

Updates `toml` from 0.8.12 to 0.8.13
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.8.12...toml-v0.8.13)

Updates `parking_lot` from 0.12.2 to 0.12.3
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/0.12.2...0.12.3)

Updates `anyhow` from 1.0.83 to 1.0.86
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.83...1.0.86)

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

Updates `libc` from 0.2.154 to 0.2.155
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.154...0.2.155)

Updates `pulldown-cmark` from 0.10.3 to 0.11.0
- [Release notes](https://github.com/raphlinus/pulldown-cmark/releases)
- [Commits](https://github.com/raphlinus/pulldown-cmark/compare/v0.10.3...v0.11.0)

Updates `open` from 5.1.2 to 5.1.3
- [Release notes](https://github.com/Byron/open-rs/releases)
- [Changelog](https://github.com/Byron/open-rs/blob/main/changelog.md)
- [Commits](https://github.com/Byron/open-rs/compare/v5.1.2...v5.1.3)

Updates `thiserror` from 1.0.60 to 1.0.61
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.60...1.0.61)

Updates `cc` from 1.0.97 to 1.0.98
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.97...1.0.98)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: toml
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: parking_lot
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: tokio
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: pulldown-cmark
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: open
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-06-03 17:08:51 +09:00
Poliorcetics
972265640d fix: correctly reset inlay hints when stopping or restarting LSPs for a document (#10741) 2024-06-02 10:39:48 -05:00
Christopher Bayliss
179673568d avoid cnorm on certain terminals (#10769)
using a terminfo's cnorm doesn't reset the cursor for many terminals,
see issue: #10089
2024-05-28 13:15:53 +02:00
Mark Stosberg
730e684d1d Correct typo in "current buffer's directory" (#10814) 2024-05-27 10:22:40 +09:00
Yorick Peterse
f1c9580e4b tree-sitter: update Inko grammar (#10805) 2024-05-24 12:09:56 +09:00
dependabot[bot]
4dbb4eeba1 --- (#10804)
updated-dependencies:
- dependency-name: gix
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-24 12:08:20 +09:00
dependabot[bot]
41dec92b0f --- (#10798)
updated-dependencies:
- dependency-name: cachix/install-nix-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 01:22:06 +09:00
dependabot[bot]
a789f72a88 --- (#10799)
updated-dependencies:
- dependency-name: cachix/cachix-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-22 01:21:53 +09:00
Kirawi
5b9f5f9fdb Handle relative symlinks on write (#10790)
try again

try

wip
2024-05-21 06:46:24 +09:00
Blaž Hrastnik
dfcd814389 tui: Constify functions, shrink Margin representation 2024-05-21 04:34:36 +09:00
Blaž Hrastnik
e94735bbd3 tui: Port https://github.com/ratatui-org/ratatui/pull/1036 2024-05-21 03:50:54 +09:00
Pascal Kuthe
8444f52e9a correctly handle opening helix inside symlinked directory (#10728)
* correctly handle opening helix inside symlinked directory

* Update helix-stdx/src/env.rs

---------

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2024-05-20 21:44:53 +09:00
Michael Davis
ff6aca12b7 Reset all changes overlapped by selections in ':reset-diff-change' (#10178)
This is useful for resetting multiple changes at once. For example you
might use 'maf' or even '%' to select a larger region and reset all
changes within.

The original behavior of resetting the change on the current line is
retained when the primary selection is 1-width since we look for chunks
in the line range of each selection.
2024-05-20 21:40:55 +09:00
Krishan
2301430e37 fix match bracket matching (#10777) 2024-05-16 13:50:01 -05:00
Dmitry Salin
889bbefeb3 Add support for Mojo (#10743)
* Add support for Mojo

* Update grammar

* Fix queries

* Fix docs

* Use inheritance for some files
2024-05-15 16:24:57 +02:00
dependabot[bot]
e76020ddb9 build(deps): bump the rust-dependencies group with 2 updates (#10756)
Bumps the rust-dependencies group with 2 updates: [serde](https://github.com/serde-rs/serde) and [serde_json](https://github.com/serde-rs/json).


Updates `serde` from 1.0.200 to 1.0.201
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.200...v1.0.201)

Updates `serde_json` from 1.0.116 to 1.0.117
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.116...v1.0.117)

---
updated-dependencies:
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde_json
  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>
2024-05-14 17:54:17 +09:00
Valentin B
380c7adcc0 feat(solidity): add missing operators for highlights (#10757)
* feat(solidity): add missing operators for highlights

* fix(solidity): remove invalid operator from highlight queries

* Revert "fix(solidity): remove invalid operator from highlight queries"

This reverts commit 466f38350c.

* feat(solidity): add highlight query for revert statements with custom errors

* feat(solidity): more precise matching for revert statements
2024-05-14 17:53:59 +09:00
Michael Davis
855568fa34 Synchronize files after writing (#10735)
fsync(2) is a somewhat expensive operation that flushes writes to the
underlying disk/SSD. It's typically used by databases to ensure that
writes survive very hard failure scenarios like your cat kicking the
plug out of the wall. Synchronizing isn't automatically done by
`flush`ing (from the `std::io::Write` or `tokio::io::AsyncWriteExt`
traits). From the [`tokio::fs::File`] moduledocs:

> To ensure that a file is closed immediately when it is dropped, you
> should call `flush` before dropping it. Note that this does not ensure
> that the file has been fully written to disk; the operating system
> might keep the changes around in an in-memory buffer. See the
> `sync_all` method for telling the OS to write the data to disk.

[`tokio::fs::File`]: https://docs.rs/tokio/latest/tokio/fs/struct.File.html
2024-05-14 01:37:35 +02:00
Tobias Hunger
5fea7cd0cc tree-sitter: Update slint grammar (#10749) 2024-05-13 18:42:34 +02:00
Felix Richter
00e9e5eade docs/src/package-managers: update nixos link to official wiki (#10718)
This commit updates the the link from the former, unofficial
nixos wiki page to the new https://wiki.nixos.org

ref: https://github.com/NixOS/foundation/issues/113
2024-05-10 06:43:07 +09:00
Aral Balkan
f4a61006bf Remove outdated comment about key binding being disabled (#10717)
I just used the `=` binding to format a selection in a CSS file in helix 23.10 (5931a46c) so removing this comment as outdated.
2024-05-08 21:49:44 +09:00
dependabot[bot]
f656b4f3a7 build(deps): bump the rust-dependencies group across 1 directory with 11 updates (#10715)
Bumps the rust-dependencies group with 11 updates in the / directory:

| Package | From | To |
| --- | --- | --- |
| [tree-sitter](https://github.com/tree-sitter/tree-sitter) | `0.22.5` | `0.22.6` |
| [unicode-width](https://github.com/unicode-rs/unicode-width) | `0.1.11` | `0.1.12` |
| [hashbrown](https://github.com/rust-lang/hashbrown) | `0.14.3` | `0.14.5` |
| [serde](https://github.com/serde-rs/serde) | `1.0.198` | `1.0.200` |
| [parking_lot](https://github.com/Amanieu/parking_lot) | `0.12.1` | `0.12.2` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.82` | `1.0.83` |
| [libc](https://github.com/rust-lang/libc) | `0.2.153` | `0.2.154` |
| [rustix](https://github.com/bytecodealliance/rustix) | `0.38.32` | `0.38.34` |
| [pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) | `0.10.0` | `0.10.3` |
| [thiserror](https://github.com/dtolnay/thiserror) | `1.0.58` | `1.0.60` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.0.95` | `1.0.97` |



Updates `tree-sitter` from 0.22.5 to 0.22.6
- [Release notes](https://github.com/tree-sitter/tree-sitter/releases)
- [Changelog](https://github.com/tree-sitter/tree-sitter/blob/master/CHANGELOG.md)
- [Commits](https://github.com/tree-sitter/tree-sitter/compare/v0.22.5...v0.22.6)

Updates `unicode-width` from 0.1.11 to 0.1.12
- [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.1.11...v0.1.12)

Updates `hashbrown` from 0.14.3 to 0.14.5
- [Changelog](https://github.com/rust-lang/hashbrown/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/hashbrown/compare/v0.14.3...v0.14.5)

Updates `serde` from 1.0.198 to 1.0.200
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.198...v1.0.200)

Updates `parking_lot` from 0.12.1 to 0.12.2
- [Changelog](https://github.com/Amanieu/parking_lot/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Amanieu/parking_lot/compare/0.12.1...0.12.2)

Updates `anyhow` from 1.0.82 to 1.0.83
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.82...1.0.83)

Updates `libc` from 0.2.153 to 0.2.154
- [Release notes](https://github.com/rust-lang/libc/releases)
- [Commits](https://github.com/rust-lang/libc/compare/0.2.153...0.2.154)

Updates `rustix` from 0.38.32 to 0.38.34
- [Release notes](https://github.com/bytecodealliance/rustix/releases)
- [Commits](https://github.com/bytecodealliance/rustix/compare/v0.38.32...v0.38.34)

Updates `pulldown-cmark` from 0.10.0 to 0.10.3
- [Release notes](https://github.com/raphlinus/pulldown-cmark/releases)
- [Commits](https://github.com/raphlinus/pulldown-cmark/compare/v0.10.0...v0.10.3)

Updates `thiserror` from 1.0.58 to 1.0.60
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.58...1.0.60)

Updates `cc` from 1.0.95 to 1.0.97
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.95...1.0.97)

---
updated-dependencies:
- dependency-name: tree-sitter
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: unicode-width
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: hashbrown
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: serde
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: parking_lot
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: anyhow
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: libc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: rustix
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: pulldown-cmark
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: thiserror
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: cc
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-05-07 21:43:40 +02:00
Michael Davis
5c11af1479 cargo fmt 2024-05-07 15:15:52 -04:00
Michael Davis
9dd51e75e0 Resolve new Clippy lints 2024-05-07 15:15:52 -04:00
Michael Davis
3cf2a91a25 Bump MSRV to 1.74.0 2024-05-07 15:15:52 -04:00
Jeremia
2bc0d036e7 Added missing colors to dark_high_contrast (#10705)
I'd been using this theme quite a while because it looks the nicest to
my dyslexia. That said, when I open up XML or HTML documents, they
pretty much always show up in complete white, except for attribute
values.

Finally decided to take a look at why, and added the two colors
(`tag` & `attribute`) needed to make the theme actually usable with the
two formats.
2024-05-07 19:09:34 +02:00
Chris
81d4cbc66e Update Odin tree sitter grammar (#10698)
* Replace Odin grammar source

* Update Odin highlights

* Update Odin indents

* Update Odin injections
2024-05-07 18:14:55 +02:00
Christopher Smyth
5e3de68591 FIx incorrectly spelled cfg options (#10703)
* Fix incorrect cfg key for wasm32

* Fix unicode-lines cfg in helix-view not not being used
2024-05-07 18:14:02 +02:00
Pascal Kuthe
5b8b2f4b9b improve match bracket matching (#10613) 2024-05-07 09:26:04 -05:00
Ryan Roden-Corrent
0432d9cf14 lang(starlark): Add WORKSPACE glob. (#10713)
In addition to BUILD files, bazel repositories contain a WORKSPACE file,
which is also written in starlark.
See https://bazel.build/reference/be/workspace.
2024-05-07 23:10:51 +09:00
Szabin
beb5afcbef Revert "Refactor statusline elements to build Spans (#9122)" (#10642) 2024-05-07 01:51:20 +09:00
Matt Moriarity
e16a4f8a2c Expose all flake outputs through flake-compat (#10673) 2024-05-07 01:33:59 +09:00
Yorick Peterse
b437b8b0ee Add support for Inko (#10656)
This adds formatting and Tree-sitter support for Inko
(https://inko-lang.org/).
2024-05-06 18:04:32 +02:00
Arthur D
295a9a95ce feat: add support for gjs and gts (#9940) 2024-05-06 18:04:08 +02:00
David Else
a959c0ef9b Improve the structure of the documentation (#10619) 2024-05-06 17:39:06 +02:00
François Laignel
f86f350d5d Debugger template: allow missing or empty completion list (#10332)
It can be convenient to define project specific debugger templates, some of
which might not necessitate prompting the user to define completion.

This commit makes completion optional for debugger templates and starts the
dap immediately if undefined or empty.
2024-05-06 17:37:04 +02:00
Vladyslav Karasov
6876f923d5 lang(json): make field key highlighting consistent with toml and yaml (#10676) 2024-05-06 17:11:09 +02:00
Guilherme Salustiano
50b13d1aea docs[install/pre-build binaries]: add runtime setup (#10693) 2024-05-06 10:09:21 -05:00
Silvan Schmidt
7d1e5f18a2 fix: update link in adding_languages.md (#10677)
Previously, the link would point to the now moved "How to install the default language servers" page. The link now directly points to the up-to-date page.
2024-05-06 17:07:34 +02:00
Ashley Vaughn
61818996c6 remove ' and add ⟨⟩ in lean autopairs (#10688) 2024-05-05 22:48:50 +09:00
Matthew Pomes
7e13213e74 Add is not and not in to python syntax (#10647) 2024-05-03 12:39:02 +02:00
Hichem
cfca30887c signature: use the suggested LSP signature when changed (#10655)
some LSPs does update the active signature and some not. To make both
worlds happy, make the active signature more intelligent.

1. SignatureHelp store now the suggested lsp_signature
2. if the lsp_signature changes then use it
3. otherwise use the last signature from the old popup
4. in case the old signature doesn't exist anymore, show the last signature

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2024-05-03 03:53:07 +02:00
Ryan Roden-Corrent
31273c69e0 Add completion/signature bindings to keymap.md (#10654)
* Add completion/signature bindings to keymap.md

PR #9974 added alt-p/alt-n keybindings to scroll through signatures.
This wasn't very discoverable, as it's not in the docs or the command palette.

This also removes a broken link for "comment mode" in the table of contents.

* Update keymap.md
2024-05-02 12:25:15 +02:00
Luv-Ray
752ed8eb15 add try keyword to rust highlights (#10641) 2024-04-30 13:13:27 +09:00
TobiEiss
d8701bfd1e add textobjects queries for hurl (#10594)
* add textobjects queries comment and function

* update doc for hurl lang support

* switch entry.inner to entry.outer

* switch to function.inside

---------

Co-authored-by: Tobias Eiß <te@clarilab.de>
2024-04-29 18:17:46 +02:00
tingerrr
eeb8782c54 Add comment injection to typst queries (#10628) 2024-04-29 17:44:20 +02:00
Erasin Wang
724a96abc8 Add pest support (#10616)
Support [pest-parser](https://github.com/pest-parser)

- [pest-language-server](https://github.com/pest-parser/pest-ide-tools)
- [tree-sitter-pest](https://github.com/pest-parser/tree-sitter-pest)

close #7878
2024-04-29 17:44:03 +02:00
Kitsu
ec224798e7 fix: do not stop at first url at goto_file (#10622) 2024-04-29 08:18:58 -05:00
Kitsu
8db93013fb fix: avoid child area overflow on split (#10620) 2024-04-29 08:18:05 -05:00
Pascal Kuthe
e2594b64c0 move popup when cursor line changes
Co-authored-by: Ben Fekih, Hichem" <hichem.f@live.de>
2024-04-28 11:21:50 -04:00
Pascal Kuthe
93e8c16614 fix required_size implementation of signature help
Trunctation should always be handled by the parent. Returning None is
only supposed to indicate a missing implementation

Co-authored-by: Ben Fekih, Hichem" <hichem.f@live.de>
2024-04-28 11:21:50 -04:00
Pascal Kuthe
2d6d876a23 fix popup size calculation
Co-authored-by: ath3 <ha05190@protonmail.com>
2024-04-28 11:21:50 -04:00
Pascal Kuthe
a1d7997fe3 fix lsp restart (#10614) 2024-04-28 12:11:17 +09:00
Elizabeth
bc03b6b5fe Fixed ECMAScript private member highlighting (#10554) 2024-04-27 21:55:12 +02:00
Alexis (Poliorcetics) Bourget
615d34a237 nit: remove unused ceiling_dir param 2024-04-27 12:51:15 -04:00
Alexis (Poliorcetics) Bourget
f1461b49fa cleanup: remove useless Git struct, using free functions instead 2024-04-27 12:51:15 -04:00
Alexis (Poliorcetics) Bourget
918dd3fa37 cleanup: remove dummy diff provider, it's the exact same as not having one 2024-04-27 12:51:15 -04:00
Diogenesoftoronto
5ee7411450 Change cursor color per mode for default (#10608) 2024-04-26 16:50:29 -05:00
Keir Lawson
31248d4e2f Enable metals inlay hints (#10597) 2024-04-26 16:48:23 -05:00
David Else
109f53fb60 Add debug highlights to the dark plus theme (#10593) 2024-04-25 07:48:14 -05:00
woojiq
839ec4ad39 test: match around closest pair tree-sitter version 2024-04-24 16:36:13 -04:00
woojiq
81dc8e8d6b feat: find closest pair using tree-sitter 2024-04-24 16:36:13 -04:00
254 changed files with 33139 additions and 5812 deletions

View File

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

View File

@@ -1,3 +1,207 @@
# 24.07 (2024-07-14)
Thanks to all of the contributors! This release has changes from 160 contributors.
Breaking changes:
Features:
- Add a textobject for entries/elements of list-like things ([#8150](https://github.com/helix-editor/helix/pull/8150))
- Add a picker showing files changed in VCS ([#5645](https://github.com/helix-editor/helix/pull/5645))
- Use a temporary file for writes ([#9236](https://github.com/helix-editor/helix/pull/9236), [#10339](https://github.com/helix-editor/helix/pull/10339), [#10790](https://github.com/helix-editor/helix/pull/10790))
- Allow cycling through LSP signature-help signatures with `A-n`/`A-p` ([#9974](https://github.com/helix-editor/helix/pull/9974), [#10654](https://github.com/helix-editor/helix/pull/10654), [#10655](https://github.com/helix-editor/helix/pull/10655))
- Use tree-sitter when finding matching brackets and closest pairs ([#8294](https://github.com/helix-editor/helix/pull/8294), [#10613](https://github.com/helix-editor/helix/pull/10613), [#10777](https://github.com/helix-editor/helix/pull/10777))
- Auto-save all buffers after a delay ([#10899](https://github.com/helix-editor/helix/pull/10899), [#11047](https://github.com/helix-editor/helix/pull/11047))
Commands:
- `select_all_siblings` (`A-a`) - select all siblings of each selection ([87c4161](https://github.com/helix-editor/helix/commit/87c4161))
- `select_all_children` (`A-i`) - select all children of each selection ([fa67c5c](https://github.com/helix-editor/helix/commit/fa67c5c))
- `:read` - insert the contents of the given file at each selection ([#10447](https://github.com/helix-editor/helix/pull/10447))
Usability improvements:
- Support scrolling popup contents using the mouse ([#10053](https://github.com/helix-editor/helix/pull/10053))
- Sort the jumplist picker so that most recent items come first ([#10095](https://github.com/helix-editor/helix/pull/10095))
- Improve `goto_file`'s (`gf`) automatic path detection strategy ([#9065](https://github.com/helix-editor/helix/pull/9065))
- Respect language server definition order in code action menu ([#9590](https://github.com/helix-editor/helix/pull/9590))
- Allow using a count with `goto_next_buffer` (`gn`) and `goto_previous_buffer` (`gp`) ([#10463](https://github.com/helix-editor/helix/pull/10463))
- Improve the positioning of popups ([#10257](https://github.com/helix-editor/helix/pull/10257), [#10573](https://github.com/helix-editor/helix/pull/10573))
- Reset all changes overlapped by selections in `:reset-diff-change` ([#10728](https://github.com/helix-editor/helix/pull/10728))
- Await pending writes in the `suspend` command (`C-z`) ([#10797](https://github.com/helix-editor/helix/pull/10797))
- Remove special handling of line ending characters in `replace` (`r`) ([#10786](https://github.com/helix-editor/helix/pull/10786))
- Use the selected register as a history register for `rename_symbol` (`<space>r`) ([#10932](https://github.com/helix-editor/helix/pull/10932))
- Use the configured insert-mode cursor for prompt entry ([#10945](https://github.com/helix-editor/helix/pull/10945))
- Add tilted quotes to the matching brackets list ([#10971](https://github.com/helix-editor/helix/pull/10971))
- Prevent improper files like `/dev/urandom` from being used as file arguments ([#10733](https://github.com/helix-editor/helix/pull/10733))
- Allow multiple language servers to provide `:lsp-workspace-command`s ([#10176](https://github.com/helix-editor/helix/pull/10176), [#11105](https://github.com/helix-editor/helix/pull/11105))
- Trim output of commands executed through `:pipe` ([#10952](https://github.com/helix-editor/helix/pull/10952))
Fixes:
- Use `lldb-dap` instead of `lldb-vscode` in default DAP configuration ([#10091](https://github.com/helix-editor/helix/pull/10091))
- Fix creation of uneven splits when closing windows ([#10004](https://github.com/helix-editor/helix/pull/10004))
- Avoid setting a register in `delete_selection_noyank`, fixing the command's use in command sequences ([#10050](https://github.com/helix-editor/helix/pull/10050), [#10148](https://github.com/helix-editor/helix/pull/10148))
- Fix jump alphabet config resetting when using `:config-reload` ([#10156](https://github.com/helix-editor/helix/pull/10156))
- Overlay LSP unnecessary/deprecated diagnostic tag highlights onto regular diagnostic highlights ([#10084](https://github.com/helix-editor/helix/pull/10084))
- Fix crash on LSP text edits with invalid ranges ([#9649](https://github.com/helix-editor/helix/pull/9649))
- Handle partial failure when sending multiple LSP `textDocument/didSave` notifications ([#10168](https://github.com/helix-editor/helix/pull/10168))
- Fix off-by-one error for completion-replace option ([#10279](https://github.com/helix-editor/helix/pull/10279))
- Fix mouse right-click selection behavior ([#10067](https://github.com/helix-editor/helix/pull/10067))
- Fix scrolling to the end within a popup ([#10181](https://github.com/helix-editor/helix/pull/10181))
- Fix jump label highlight locations when jumping in non-ascii text ([#10317](https://github.com/helix-editor/helix/pull/10317))
- Fix crashes from tree-sitter query captures that return non-grapheme aligned ranges ([#10310](https://github.com/helix-editor/helix/pull/10310))
- Include VCS change in `mi`/`ma` textobject infobox ([#10496](https://github.com/helix-editor/helix/pull/10496))
- Override crossterm's support for `NO_COLOR` ([#10514](https://github.com/helix-editor/helix/pull/10514))
- Respect mode when starting a search ([#10505](https://github.com/helix-editor/helix/pull/10505))
- Simplify first-in-line computation for indent queries ([#10527](https://github.com/helix-editor/helix/pull/10527))
- Ignore .svn version controlled files in file pickers ([#10536](https://github.com/helix-editor/helix/pull/10536))
- Fix overloading language servers with `completionItem/resolve` requests ([38ee845](https://github.com/helix-editor/helix/commit/38ee845), [#10873](https://github.com/helix-editor/helix/pull/10873))
- Specify direction for `select_next_sibling` / `select_prev_sibling` ([#10542](https://github.com/helix-editor/helix/pull/10542))
- Fix restarting language servers ([#10614](https://github.com/helix-editor/helix/pull/10614))
- Don't stop at the first URL in `goto_file` ([#10622](https://github.com/helix-editor/helix/pull/10622))
- Fix overflows in window size calculations for small terminals ([#10620](https://github.com/helix-editor/helix/pull/10620))
- Allow missing or empty completion lists in DAP ([#10332](https://github.com/helix-editor/helix/pull/10332))
- Revert statusline refactor that could cause the statusline to blank out on files with long paths ([#10642](https://github.com/helix-editor/helix/pull/10642))
- Synchronize files after writing ([#10735](https://github.com/helix-editor/helix/pull/10735))
- Avoid `cnorm` for cursor-type detection in certain terminals ([#10769](https://github.com/helix-editor/helix/pull/10769))
- Reset inlay hints when stopping or restarting a language server ([#10741](https://github.com/helix-editor/helix/pull/10741))
- Fix logic for updating `--version` when development VCS HEAD changes ([#10896](https://github.com/helix-editor/helix/pull/10896))
- Set a max value for the count ([#10930](https://github.com/helix-editor/helix/pull/10930))
- Deserialize number IDs in DAP module types ([#10943](https://github.com/helix-editor/helix/pull/10943))
- Fix the behavior of `jump_backwords` when the jumplist is at capacity ([#10968](https://github.com/helix-editor/helix/pull/10968))
- Fix injection layer heritage tracking for reused tree-sitter injection layers ([#1098](https://github.com/helix-editor/helix/pull/1098))
- Fix pluralization of "buffers" in the statusline for `:q`, `:q!`, `:wq` ([#11018](https://github.com/helix-editor/helix/pull/11018))
- Declare LSP formatting client capabilities ([#11064](https://github.com/helix-editor/helix/pull/11064))
- Commit uncommitted changes before attempting undo/earlier ([#11090](https://github.com/helix-editor/helix/pull/11090))
- Expand tilde for selected paths in `goto_file` ([#10964](https://github.com/helix-editor/helix/pull/10964))
- Commit undo checkpoints before `:write[-all]`, fixing the modification indicator ([#11062](https://github.com/helix-editor/helix/pull/11062))
Themes:
- Add jump label styles to `nightfox` ([#10052](https://github.com/helix-editor/helix/pull/10052))
- Add jump label styles to Solarized themes ([#10056](https://github.com/helix-editor/helix/pull/10056))
- Add jump label styles to `cyan_light` ([#10058](https://github.com/helix-editor/helix/pull/10058))
- Add jump label styles to `onelight` ([#10061](https://github.com/helix-editor/helix/pull/10061))
- Add `flexoki-dark` and `flexoki-light` ([#10002](https://github.com/helix-editor/helix/pull/10002))
- Add default theme keys for LSP diagnostics tags to existing themes ([#10064](https://github.com/helix-editor/helix/pull/10064))
- Add jump label styles to base16 themes ([#10076](https://github.com/helix-editor/helix/pull/10076))
- Dim primary selection in `kanagawa` ([#10094](https://github.com/helix-editor/helix/pull/10094), [#10500](https://github.com/helix-editor/helix/pull/10500))
- Add jump label styles to tokyonight themes ([#10106](https://github.com/helix-editor/helix/pull/10106))
- Add jump label styles to papercolor themes ([#10104](https://github.com/helix-editor/helix/pull/10104))
- Add jump label styles to Darcula themes ([#10116](https://github.com/helix-editor/helix/pull/10116))
- Add jump label styles to `autumn` ([#10134](https://github.com/helix-editor/helix/pull/10134))
- Add jump label styles to Ayu themes ([#10133](https://github.com/helix-editor/helix/pull/10133))
- Add jump label styles to `dark_high_contrast` ([#10133](https://github.com/helix-editor/helix/pull/10133))
- Update material themes ([#10290](https://github.com/helix-editor/helix/pull/10290))
- Add jump label styles to `varua` ([#10299](https://github.com/helix-editor/helix/pull/10299))
- Add ruler style to `adwaita-dark` ([#10260](https://github.com/helix-editor/helix/pull/10260))
- Remove `ui.highlight` effects from `solarized_dark` ([#10261](https://github.com/helix-editor/helix/pull/10261))
- Fix statusline color in material themes ([#10308](https://github.com/helix-editor/helix/pull/10308))
- Brighten `nord` selection highlight ([#10307](https://github.com/helix-editor/helix/pull/10307))
- Add inlay-hint styles to monokai themes ([#10334](https://github.com/helix-editor/helix/pull/10334))
- Add bufferline and cursorline colors to `vim_dark_high_contrast` ([#10444](https://github.com/helix-editor/helix/pull/10444))
- Switch themes with foreground rulers to background ([#10309](https://github.com/helix-editor/helix/pull/10309))
- Fix statusline colors for `everblush` ([#10394](https://github.com/helix-editor/helix/pull/10394))
- Use `yellow1` for `gruvbox` warning diagnostics ([#10506](https://github.com/helix-editor/helix/pull/10506))
- Add jump label styles to Modus themes ([#10538](https://github.com/helix-editor/helix/pull/10538))
- Refactor `dark_plus` and switch maintainers ([#10543](https://github.com/helix-editor/helix/pull/10543), [#10574](https://github.com/helix-editor/helix/pull/10574))
- Add debug highlights to `dark_plus` ([#10593](https://github.com/helix-editor/helix/pull/10593))
- Fix per-mode cursor colors in the default theme ([#10608](https://github.com/helix-editor/helix/pull/10608))
- Add `tag` and `attribute` highlights to `dark_high_contrast` ([#10705](https://github.com/helix-editor/helix/pull/10705))
- Improve readability of virtual text with `noctis` theme ([#10910](https://github.com/helix-editor/helix/pull/10910))
- Sync `catppuccin` themes with upstream ([#10954](https://github.com/helix-editor/helix/pull/10954))
- Improve jump colors for `github_dark` themes ([#10946](https://github.com/helix-editor/helix/pull/10946))
- Add modeline and default virtual highlights to `base16_default` ([#10858](https://github.com/helix-editor/helix/pull/10858))
- Add `iroaseta` ([#10381](https://github.com/helix-editor/helix/pull/10381))
- Refactor `gruvbox` ([#10773](https://github.com/helix-editor/helix/pull/10773), [#11071](https://github.com/helix-editor/helix/pull/11071))
- Add cursorcolumn and cursorline to `base16_transparent` ([#11099](https://github.com/helix-editor/helix/pull/11099))
- Update cursorline color for `fleet_dark` ([#11046](https://github.com/helix-editor/helix/pull/11046))
- Add `kanagawa-dragon` ([#10172](https://github.com/helix-editor/helix/pull/10172))
New languages:
- BitBake ([#10010](https://github.com/helix-editor/helix/pull/10010))
- Earthfile ([#10111](https://github.com/helix-editor/helix/pull/10111), [#10489](https://github.com/helix-editor/helix/pull/10489), [#10779](https://github.com/helix-editor/helix/pull/10779))
- TCL ([#9837](https://github.com/helix-editor/helix/pull/9837))
- ADL ([#10029](https://github.com/helix-editor/helix/pull/10029))
- LDIF ([#10330](https://github.com/helix-editor/helix/pull/10330))
- XTC ([#10448](https://github.com/helix-editor/helix/pull/10448))
- Move ([f06a166](https://github.com/helix-editor/helix/commit/f06a166))
- Pest ([#10616](https://github.com/helix-editor/helix/pull/10616))
- GJS/GTS ([#9940](https://github.com/helix-editor/helix/pull/9940))
- Inko ([#10656](https://github.com/helix-editor/helix/pull/10656))
- Mojo ([#10743](https://github.com/helix-editor/helix/pull/10743))
- Elisp ([#10644](https://github.com/helix-editor/helix/pull/10644))
Updated languages and queries:
- Recognize `mkdn` files as markdown ([#10065](https://github.com/helix-editor/helix/pull/10065))
- Add comment injections for Gleam ([#10062](https://github.com/helix-editor/helix/pull/10062))
- Recognize BuildKite commands in YAML injections ([#10090](https://github.com/helix-editor/helix/pull/10090))
- Add F# block comment token configuration ([#10108](https://github.com/helix-editor/helix/pull/10108))
- Update tree-sitter-templ and queries ([#10114](https://github.com/helix-editor/helix/pull/10114))
- Recognize `Tiltfile` as Starlark ([#10072](https://github.com/helix-editor/helix/pull/10072))
- Remove `todo.txt` from files recognized as todotxt ([5fece00](https://github.com/helix-editor/helix/commit/5fece00))
- Highlight `type` keyword in Python from PEP695 ([#10165](https://github.com/helix-editor/helix/pull/10165))
- Update tree-sitter-koka, add language server config ([#10119](https://github.com/helix-editor/helix/pull/10119))
- Recognize node and Python history files ([#10120](https://github.com/helix-editor/helix/pull/10120))
- Recognize more shell files as bash ([#10120](https://github.com/helix-editor/helix/pull/10120))
- Recognize the bun shebang as typescript ([#10120](https://github.com/helix-editor/helix/pull/10120))
- Add a configuration for the angular language server ([#10166](https://github.com/helix-editor/helix/pull/10166))
- Add textobject queries for Solidity ([#10318](https://github.com/helix-editor/helix/pull/10318))
- Recognize `meson.options` as Meson ([#10323](https://github.com/helix-editor/helix/pull/10323))
- Improve Solidity highlighting ([4fc0a4d](https://github.com/helix-editor/helix/commit/4fc0a4d))
- Recognize `_.tpl` files as Helm ([#10344](https://github.com/helix-editor/helix/pull/10344))
- Update tree-sitter-ld and highlights ([#10379](https://github.com/helix-editor/helix/pull/10379))
- Add `lldb-dap` configuration for Odin ([#10175](https://github.com/helix-editor/helix/pull/10175))
- Update tree-sitter-rust ([#10365](https://github.com/helix-editor/helix/pull/10365))
- Update tree-sitter-typst ([#10321](https://github.com/helix-editor/helix/pull/10321))
- Recognize `hyprpaper.conf`, `hypridle.conf` and `hyprlock.conf` as Hyprlang ([#10383](https://github.com/helix-editor/helix/pull/10383))
- Improve HTML highlighting ([#10503](https://github.com/helix-editor/helix/pull/10503))
- Add `rust-script` and `cargo` as shebangs for Rust ([#10484](https://github.com/helix-editor/helix/pull/10484))
- Fix precedence of tag highlights in Svelte ([#10487](https://github.com/helix-editor/helix/pull/10487))
- Update tree-sitter-bash ([#10526](https://github.com/helix-editor/helix/pull/10526))
- Recognize `*.ignore` files as ignore ([#10579](https://github.com/helix-editor/helix/pull/10579))
- Add configuration to enable inlay hints in metals ([#10597](https://github.com/helix-editor/helix/pull/10597))
- Enable highlighting private members in ECMA languages ([#10554](https://github.com/helix-editor/helix/pull/10554))
- Add comment injection to typst queries ([#10628](https://github.com/helix-editor/helix/pull/10628))
- Add textobject queries for Hurl ([#10594](https://github.com/helix-editor/helix/pull/10594))
- Add `try` keyword to Rust ([#10641](https://github.com/helix-editor/helix/pull/10641))
- Add `is not` and `not in` to Python highlights ([#10647](https://github.com/helix-editor/helix/pull/10647))
- Remove ' and ⟨⟩ from Lean autopair configuration ([#10688](https://github.com/helix-editor/helix/pull/10688))
- Match TOML/YAML highlights for JSON keys ([#10676](https://github.com/helix-editor/helix/pull/10676))
- Recognize WORKSPACE files as Starlark ([#10713](https://github.com/helix-editor/helix/pull/10713))
- Switch Odin tree-sitter grammar and highlights ([#10698](https://github.com/helix-editor/helix/pull/10698))
- Update `tree-sitter-slint` ([#10749](https://github.com/helix-editor/helix/pull/10749))
- Add missing operators for Solidity highlights ([#10735](https://github.com/helix-editor/helix/pull/10735))
- Update `tree-sitter-inko` ([#10805](https://github.com/helix-editor/helix/pull/10805))
- Add `py`, `hs`, `rs` and `typ` injection regexes ([#10785](https://github.com/helix-editor/helix/pull/10785))
- Update Swift grammar and queries ([#10802](https://github.com/helix-editor/helix/pull/10802))
- Update Cairo grammar and queries ([#10919](https://github.com/helix-editor/helix/pull/10919), [#11067](https://github.com/helix-editor/helix/pull/11067))
- Update Rust grammar ([#10973](https://github.com/helix-editor/helix/pull/10973))
- Add block comment tokens for typst ([#10955](https://github.com/helix-editor/helix/pull/10955))
- Recognize `jsonl` as JSON ([#11004](https://github.com/helix-editor/helix/pull/11004))
- Add rulers and text-width at 100 columns for Lean language ([#10969](https://github.com/helix-editor/helix/pull/10969))
- Improve VDHL highlights ([#10845](https://github.com/helix-editor/helix/pull/10845))
- Recognize `hsc` as Haskell ([#11074](https://github.com/helix-editor/helix/pull/11074))
- Fix heredoc and `$'<ansi_string>'` highlights in Bash ([#11118](https://github.com/helix-editor/helix/pull/11118))
- Add LSP configuration for `basedpyright` ([#11121](https://github.com/helix-editor/helix/pull/11121))
- Recognize `npmrc` and `.nmprc` files as INI ([#11131](https://github.com/helix-editor/helix/pull/11131))
- Recognize `~/.config/git/ignore` as git-ignore ([#11131](https://github.com/helix-editor/helix/pull/11131))
- Recognize `pdm.lock` and `uv.lock` as TOML ([#11131](https://github.com/helix-editor/helix/pull/11131))
- Recognize `.yml` as well as `.yaml` for Helm chart templates ([#11135](https://github.com/helix-editor/helix/pull/11135))
- Add regex injections for Bash ([#11112](https://github.com/helix-editor/helix/pull/11112))
- Update tree-sitter-todo ([#11097](https://github.com/helix-editor/helix/pull/11097))
Packaging:
- Make `Helix.appdata.xml` spec-compliant ([#10051](https://github.com/helix-editor/helix/pull/10051))
- Expose all flake outputs through flake-compat ([#10673](https://github.com/helix-editor/helix/pull/10673))
- Bump the MSRV to 1.74.0 ([#10714](https://github.com/helix-editor/helix/pull/10714))
- Improve FiSH completions ([#10853](https://github.com/helix-editor/helix/pull/10853))
- Improve ZSH completions ([#10853](https://github.com/helix-editor/helix/pull/10853))
# 24.03 (2024-03-30)
As always, a big thank you to all of the contributors! This release saw changes from 125 contributors.

370
Cargo.lock generated
View File

@@ -62,9 +62,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.82"
version = "1.0.86"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
checksum = "b3d1d046238990b9cf5bcde22a3fb3584ee5cf65fb2765f454ed428c7a0063da"
[[package]]
name = "arc-swap"
@@ -101,9 +101,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]]
name = "bitflags"
version = "2.5.0"
version = "2.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf4b9d6a944f767f8e5e0db018570623c85f3d925ac718db4e06d0187adb21c1"
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
[[package]]
name = "bstr"
@@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.95"
version = "1.0.106"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
checksum = "066fce287b1d4eafef758e89e09d724a24808a9196fe9756b8ca90e86d0719a2"
[[package]]
name = "cfg-if"
@@ -171,9 +171,9 @@ dependencies = [
[[package]]
name = "clipboard-win"
version = "5.3.1"
version = "5.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad"
checksum = "15efe7a882b08f34e38556b14f2fb3daa98769d06c7f0c1b076dfd0d983bc892"
dependencies = [
"error-code",
]
@@ -209,12 +209,6 @@ version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e496a50fda8aacccc86d7529e2c1e0892dbd0f898a6b5645b5561b89c3210efa"
[[package]]
name = "cov-mark"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9ffa3d3e0138386cd4361f63537765cac7ee40698028844635a54495a92f67f3"
[[package]]
name = "crc32fast"
version = "1.3.2"
@@ -273,7 +267,7 @@ version = "0.27.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f476fe445d41c9e991fd07515a6f463074b782242ccf4a5b7b1d1012e70824df"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"crossterm_winapi",
"filedescriptor",
"futures-core",
@@ -351,6 +345,15 @@ dependencies = [
"parking_lot_core",
]
[[package]]
name = "deranged"
version = "0.3.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b42b6fa04a440b495c8b04d0e71b707c585f83cb9cb28cf8cd0d976c315e31b4"
dependencies = [
"powerfmt",
]
[[package]]
name = "dunce"
version = "1.0.4"
@@ -422,9 +425,9 @@ checksum = "a2a2b11eda1d40935b26cf18f6833c526845ae8c41e58d09af6adeb6f0269183"
[[package]]
name = "fastrand"
version = "2.0.1"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5"
checksum = "9fc0510504f03c51ada170672ac806f1f105a88aa97a5281117e1ddc3368e51a"
[[package]]
name = "fern"
@@ -538,9 +541,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
[[package]]
name = "gix"
version = "0.62.0"
version = "0.63.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5631c64fb4cd48eee767bf98a3cbc5c9318ef3bb71074d4c099a2371510282b6"
checksum = "984c5018adfa7a4536ade67990b3ebc6e11ab57b3d6cd9968de0947ca99b4b06"
dependencies = [
"gix-actor",
"gix-attributes",
@@ -588,9 +591,9 @@ dependencies = [
[[package]]
name = "gix-actor"
version = "0.31.1"
version = "0.31.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "45c3a3bde455ad2ee8ba8a195745241ce0b770a8a26faae59fcf409d01b28c46"
checksum = "d69c59d392c7e6c94385b6fd6089d6df0fe945f32b4357687989f3aee253cd7f"
dependencies = [
"bstr",
"gix-date",
@@ -637,9 +640,9 @@ dependencies = [
[[package]]
name = "gix-command"
version = "0.3.6"
version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f90009020dc4b3de47beed28e1334706e0a330ddd17f5cfeb097df3b15a54b77"
checksum = "6c22e086314095c43ffe5cdc5c0922d5439da4fd726f3b0438c56147c34dc225"
dependencies = [
"bstr",
"gix-path",
@@ -663,9 +666,9 @@ dependencies = [
[[package]]
name = "gix-config"
version = "0.36.1"
version = "0.37.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7580e05996e893347ad04e1eaceb92e1c0e6a3ffe517171af99bf6b6df0ca6e5"
checksum = "53fafe42957e11d98e354a66b6bd70aeea00faf2f62dd11164188224a507c840"
dependencies = [
"bstr",
"gix-config-value",
@@ -688,7 +691,7 @@ version = "0.14.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fbd06203b1a9b33a78c88252a625031b094d9e1b647260070c25b09910c0a804"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"bstr",
"gix-path",
"libc",
@@ -697,9 +700,9 @@ dependencies = [
[[package]]
name = "gix-date"
version = "0.8.5"
version = "0.8.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "180b130a4a41870edfbd36ce4169c7090bca70e195da783dea088dd973daa59c"
checksum = "367ee9093b0c2b04fd04c5c7c8b6a1082713534eab537597ae343663a518fa99"
dependencies = [
"bstr",
"itoa",
@@ -709,9 +712,9 @@ dependencies = [
[[package]]
name = "gix-diff"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5fbc24115b957346cd23fb0f47d830eb799c46c89cdcf2f5acc9bf2938c2d01"
checksum = "40b9bd8b2d07b6675a840b56a6c177d322d45fa082672b0dad8f063b25baf0a4"
dependencies = [
"bstr",
"gix-command",
@@ -729,9 +732,9 @@ dependencies = [
[[package]]
name = "gix-dir"
version = "0.4.0"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d6943a1f213ad7a060a0548ece229be53f3c2151534b126446ce3533eaf5f14c"
checksum = "60c99f8c545abd63abe541d20ab6cda347de406c0a3f1c80aadc12d9b0e94974"
dependencies = [
"bstr",
"gix-discover",
@@ -749,9 +752,9 @@ dependencies = [
[[package]]
name = "gix-discover"
version = "0.31.0"
version = "0.32.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "64bab49087ed3710caf77e473dc0efc54ca33d8ccc6441359725f121211482b1"
checksum = "fc27c699b63da66b50d50c00668bc0b7e90c3a382ef302865e891559935f3dbf"
dependencies = [
"bstr",
"dunce",
@@ -765,9 +768,9 @@ dependencies = [
[[package]]
name = "gix-features"
version = "0.38.1"
version = "0.38.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "db4254037d20a247a0367aa79333750146a369719f0c6617fec4f5752cc62b37"
checksum = "ac7045ac9fe5f9c727f38799d002a7ed3583cd777e3322a7c4b43e3cf437dc69"
dependencies = [
"crc32fast",
"flate2",
@@ -784,9 +787,9 @@ dependencies = [
[[package]]
name = "gix-filter"
version = "0.11.1"
version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5c0d1f01af62bfd2fb3dd291acc2b29d4ab3e96ad52a679174626508ce98ef12"
checksum = "00ce6ea5ac8fca7adbc63c48a1b9e0492c222c386aa15f513405f1003f2f4ab2"
dependencies = [
"bstr",
"encoding_rs",
@@ -805,10 +808,11 @@ dependencies = [
[[package]]
name = "gix-fs"
version = "0.10.2"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e2184c40e7910529677831c8b481acf788ffd92427ed21fad65b6aa637e631b8"
checksum = "3f78f7d6dcda7a5809efd73a33b145e3dce7421c460df21f32126f9732736b0c"
dependencies = [
"fastrand",
"gix-features",
"gix-utils",
]
@@ -819,7 +823,7 @@ version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "682bdc43cb3c00dbedfcc366de2a849b582efd8d886215dbad2ea662ec156bb5"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"bstr",
"gix-features",
"gix-path",
@@ -842,7 +846,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ddf80e16f3c19ac06ce415a38b8591993d3f73aede049cb561becb5b3a8e242"
dependencies = [
"gix-hash",
"hashbrown 0.14.3",
"hashbrown 0.14.5",
"parking_lot",
]
@@ -861,11 +865,11 @@ dependencies = [
[[package]]
name = "gix-index"
version = "0.32.0"
version = "0.33.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3383122cf18655ef4c097c0b935bba5eb56983947959aaf3b0ceb1949d4dd371"
checksum = "2d8c5a5f1c58edcbc5692b174cda2703aba82ed17d7176ff4c1752eb48b1b167"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"bstr",
"filetime",
"fnv",
@@ -877,7 +881,8 @@ dependencies = [
"gix-object",
"gix-traverse",
"gix-utils",
"hashbrown 0.14.3",
"gix-validate",
"hashbrown 0.14.5",
"itoa",
"libc",
"memmap2",
@@ -888,9 +893,9 @@ dependencies = [
[[package]]
name = "gix-lock"
version = "13.0.0"
version = "14.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "651e46174dc5e7d18b7b809d31937b6de3681b1debd78618c99162cc30fcf3e1"
checksum = "e3bc7fe297f1f4614774989c00ec8b1add59571dc9b024b4c00acb7dedd4e19d"
dependencies = [
"gix-tempfile",
"gix-utils",
@@ -899,9 +904,9 @@ dependencies = [
[[package]]
name = "gix-macros"
version = "0.1.4"
version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1dff438f14e67e7713ab9332f5fd18c8f20eb7eb249494f6c2bf170522224032"
checksum = "999ce923619f88194171a67fb3e6d613653b8d4d6078b529b15a765da0edcc17"
dependencies = [
"proc-macro2",
"quote",
@@ -910,9 +915,9 @@ dependencies = [
[[package]]
name = "gix-object"
version = "0.42.1"
version = "0.42.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3d4f8efae72030df1c4a81d02dbe2348e748d9b9a11e108ed6efbd846326e051"
checksum = "1fe2dc4a41191c680c942e6ebd630c8107005983c4679214fdb1007dcf5ae1df"
dependencies = [
"bstr",
"gix-actor",
@@ -929,9 +934,9 @@ dependencies = [
[[package]]
name = "gix-odb"
version = "0.60.0"
version = "0.61.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e8bbb43d2fefdc4701ffdf9224844d05b136ae1b9a73c2f90710c8dd27a93503"
checksum = "e92b9790e2c919166865d0825b26cc440a387c175bed1b43a2fa99c0e9d45e98"
dependencies = [
"arc-swap",
"gix-date",
@@ -949,9 +954,9 @@ dependencies = [
[[package]]
name = "gix-pack"
version = "0.50.0"
version = "0.51.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b58bad27c7677fa6b587aab3a1aca0b6c97373bd371a0a4290677c838c9bcaf1"
checksum = "7a8da51212dbff944713edb2141ed7e002eea326b8992070374ce13a6cb610b3"
dependencies = [
"clru",
"gix-chunk",
@@ -994,11 +999,11 @@ dependencies = [
[[package]]
name = "gix-pathspec"
version = "0.7.3"
version = "0.7.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d479789f3abd10f68a709454ce04cd68b54092ee882c8622ae3aa1bb9bf8496c"
checksum = "a76cab098dc10ba2d89f634f66bf196dea4d7db4bf10b75c7a9c201c55a2ee19"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"bstr",
"gix-attributes",
"gix-config-value",
@@ -1020,9 +1025,9 @@ dependencies = [
[[package]]
name = "gix-ref"
version = "0.43.0"
version = "0.44.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd4aba68b925101cb45d6df328979af0681364579db889098a0de75b36c77b65"
checksum = "0b36752b448647acd59c9668fdd830b16d07db1e6d9c3b3af105c1605a6e23d9"
dependencies = [
"gix-actor",
"gix-date",
@@ -1056,9 +1061,9 @@ dependencies = [
[[package]]
name = "gix-revision"
version = "0.27.0"
version = "0.27.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e34196e1969bd5d36e2fbc4467d893999132219d503e23474a8ad2b221cb1e8"
checksum = "63e08f8107ed1f93a83bcfbb4c38084c7cb3f6cd849793f1d5eec235f9b13b2b"
dependencies = [
"bstr",
"gix-date",
@@ -1072,9 +1077,9 @@ dependencies = [
[[package]]
name = "gix-revwalk"
version = "0.13.0"
version = "0.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0a7d393ae814eeaae41a333c0ff684b243121cc61ccdc5bbe9897094588047d"
checksum = "4181db9cfcd6d1d0fd258e91569dbb61f94cb788b441b5294dd7f1167a3e788f"
dependencies = [
"gix-commitgraph",
"gix-date",
@@ -1091,7 +1096,7 @@ version = "0.10.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fddc27984a643b20dd03e97790555804f98cf07404e0e552c0ad8133266a79a1"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"gix-path",
"libc",
"windows-sys 0.52.0",
@@ -1099,9 +1104,9 @@ dependencies = [
[[package]]
name = "gix-status"
version = "0.9.0"
version = "0.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "50c413bfd2952e4ee92e48438dac3c696f3555e586a34d184a427f6bedd1e4f9"
checksum = "2f4373d989713809554d136f51bc7da565adf45c91aa4d86ef6a79801621bfc8"
dependencies = [
"bstr",
"filetime",
@@ -1121,9 +1126,9 @@ dependencies = [
[[package]]
name = "gix-submodule"
version = "0.10.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4fb7ea05666362472fecd44c1fc35fe48a5b9b841b431cc4f85b95e6f20c23ec"
checksum = "921cd49924ac14b6611b22e5fb7bbba74d8780dc7ad26153304b64d1272460ac"
dependencies = [
"bstr",
"gix-config",
@@ -1136,9 +1141,9 @@ dependencies = [
[[package]]
name = "gix-tempfile"
version = "13.0.0"
version = "14.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2d337955b7af00fb87120d053d87cdfb422a80b9ff7a3aa4057a99c79422dc30"
checksum = "d3b0e276cd08eb2a22e9f286a4f13a222a01be2defafa8621367515375644b99"
dependencies = [
"dashmap",
"gix-fs",
@@ -1156,11 +1161,11 @@ checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e"
[[package]]
name = "gix-traverse"
version = "0.39.0"
version = "0.39.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f4029ec209b0cc480d209da3837a42c63801dd8548f09c1f4502c60accb62aeb"
checksum = "f20cb69b63eb3e4827939f42c05b7756e3488ef49c25c412a876691d568ee2a0"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"gix-commitgraph",
"gix-date",
"gix-hash",
@@ -1198,9 +1203,9 @@ dependencies = [
[[package]]
name = "gix-validate"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e39fc6e06044985eac19dd34d474909e517307582e462b2eb4c8fa51b6241545"
checksum = "82c27dd34a49b1addf193c92070bcbf3beaf6e10f16a78544de6372e146a0acf"
dependencies = [
"bstr",
"thiserror",
@@ -1208,9 +1213,9 @@ dependencies = [
[[package]]
name = "gix-worktree"
version = "0.33.0"
version = "0.34.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "359a87dfef695b5f91abb9a424c947edca82768f34acfc269659f66174a510b4"
checksum = "53f6b7de83839274022aff92157d7505f23debf739d257984a300a35972ca94e"
dependencies = [
"bstr",
"gix-attributes",
@@ -1222,6 +1227,7 @@ dependencies = [
"gix-index",
"gix-object",
"gix-path",
"gix-validate",
]
[[package]]
@@ -1282,9 +1288,9 @@ checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888"
[[package]]
name = "hashbrown"
version = "0.14.3"
version = "0.14.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "290f1a1d9242c78d09ce40a5e87e7554ee637af1351968159f4952f028f75604"
checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
dependencies = [
"ahash",
"allocator-api2",
@@ -1292,19 +1298,20 @@ dependencies = [
[[package]]
name = "helix-core"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"ahash",
"arc-swap",
"bitflags 2.5.0",
"bitflags 2.6.0",
"chrono",
"dunce",
"encoding_rs",
"etcetera",
"globset",
"hashbrown 0.14.3",
"hashbrown 0.14.5",
"helix-loader",
"helix-stdx",
"helix-syntax",
"imara-diff",
"indoc",
"log",
@@ -1325,11 +1332,12 @@ dependencies = [
"unicode-general-category",
"unicode-segmentation",
"unicode-width",
"url",
]
[[package]]
name = "helix-dap"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"anyhow",
"fern",
@@ -1344,12 +1352,12 @@ dependencies = [
[[package]]
name = "helix-event"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"ahash",
"anyhow",
"futures-executor",
"hashbrown 0.14.3",
"hashbrown 0.14.5",
"log",
"once_cell",
"parking_lot",
@@ -1358,7 +1366,7 @@ dependencies = [
[[package]]
name = "helix-loader"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"anyhow",
"cc",
@@ -1377,7 +1385,7 @@ dependencies = [
[[package]]
name = "helix-lsp"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -1401,13 +1409,13 @@ dependencies = [
[[package]]
name = "helix-parsec"
version = "24.3.0"
version = "24.7.0"
[[package]]
name = "helix-stdx"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"dunce",
"etcetera",
"regex-cursor",
@@ -1418,9 +1426,30 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "helix-syntax"
version = "24.7.0"
dependencies = [
"ahash",
"arc-swap",
"bitflags 2.6.0",
"cc",
"hashbrown 0.14.5",
"helix-stdx",
"libloading",
"log",
"once_cell",
"regex",
"regex-cursor",
"ropey",
"slotmap",
"thiserror",
"tree-sitter",
]
[[package]]
name = "helix-term"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -1455,6 +1484,7 @@ dependencies = [
"smallvec",
"tempfile",
"termini",
"thiserror",
"tokio",
"tokio-stream",
"toml",
@@ -1463,9 +1493,9 @@ dependencies = [
[[package]]
name = "helix-tui"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"cassowary",
"crossterm",
"helix-core",
@@ -1479,7 +1509,7 @@ dependencies = [
[[package]]
name = "helix-vcs"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"anyhow",
"arc-swap",
@@ -1495,11 +1525,11 @@ dependencies = [
[[package]]
name = "helix-view"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"anyhow",
"arc-swap",
"bitflags 2.5.0",
"bitflags 2.6.0",
"chardetng",
"clipboard-win",
"crossterm",
@@ -1521,6 +1551,7 @@ dependencies = [
"serde_json",
"slotmap",
"tempfile",
"thiserror",
"tokio",
"tokio-stream",
"toml",
@@ -1597,12 +1628,12 @@ dependencies = [
[[package]]
name = "imara-diff"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e98c1d0ad70fc91b8b9654b1f33db55e59579d3b3de2bffdced0fdb810570cb8"
checksum = "af13c8ceb376860ff0c6a66d83a8cdd4ecd9e464da24621bbffcd02b49619434"
dependencies = [
"ahash",
"hashbrown 0.12.3",
"hashbrown 0.14.5",
]
[[package]]
@@ -1612,7 +1643,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
dependencies = [
"equivalent",
"hashbrown 0.14.3",
"hashbrown 0.14.5",
]
[[package]]
@@ -1666,15 +1697,15 @@ dependencies = [
[[package]]
name = "libc"
version = "0.2.153"
version = "0.2.155"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9c198f91728a82281a64e1f4f9eeb25d82cb32a5de251c6bd1b5154d63a8e7bd"
checksum = "97b3888a4aecf77e811145cadf6eef5901f4782c53886191b2f693f24761847c"
[[package]]
name = "libloading"
version = "0.8.3"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0c2a198fb6b0eada2a8df47933734e6d35d350665a33a3593d7164fa52c75c19"
checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
dependencies = [
"cfg-if",
"windows-targets 0.52.0",
@@ -1707,9 +1738,9 @@ dependencies = [
[[package]]
name = "log"
version = "0.4.21"
version = "0.4.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
checksum = "a7a70ba024b9dc04c27ea2f0c0548feb474ec5c54bba33a7f72f873a39d07b24"
[[package]]
name = "lsp-types"
@@ -1771,9 +1802,9 @@ dependencies = [
[[package]]
name = "nucleo"
version = "0.2.1"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ae5331f4bcce475cf28cb29c95366c3091af4b0aa7703f1a6bc858f29718fdf3"
checksum = "5262af4c94921c2646c5ac6ff7900c2af9cbb08dc26a797e18130a7019c039d4"
dependencies = [
"nucleo-matcher",
"parking_lot",
@@ -1782,15 +1813,20 @@ dependencies = [
[[package]]
name = "nucleo-matcher"
version = "0.2.0"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57"
checksum = "bf33f538733d1a5a3494b836ba913207f14d9d4a1d3cd67030c5061bdd2cac85"
dependencies = [
"cov-mark",
"memchr",
"unicode-segmentation",
]
[[package]]
name = "num-conv"
version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9"
[[package]]
name = "num-traits"
version = "0.2.15"
@@ -1836,9 +1872,9 @@ checksum = "3fdb12b2476b595f9358c5161aa467c2438859caa136dec86c26fdd2efe17b92"
[[package]]
name = "open"
version = "5.1.2"
version = "5.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "449f0ff855d85ddbf1edd5b646d65249ead3f5e422aaa86b7d2d0b049b103e32"
checksum = "9d2c909a3fce3bd80efef4cd1c6c056bd9376a8fe06fcfdbebaf32cb485a7e37"
dependencies = [
"is-wsl",
"libc",
@@ -1847,9 +1883,9 @@ dependencies = [
[[package]]
name = "parking_lot"
version = "0.12.1"
version = "0.12.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f"
checksum = "f1bf18183cf54e8d6059647fc3063646a1801cf30896933ec2311622cc4b9a27"
dependencies = [
"lock_api",
"parking_lot_core",
@@ -1892,6 +1928,12 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "powerfmt"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "439ee305def115ba05938db6eb1644ff94165c5ab5e9420d1c1bcedbba909391"
[[package]]
name = "proc-macro2"
version = "1.0.76"
@@ -1909,11 +1951,11 @@ checksum = "744a264d26b88a6a7e37cbad97953fa233b94d585236310bcbc88474b4092d79"
[[package]]
name = "pulldown-cmark"
version = "0.10.0"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
checksum = "8746739f11d39ce5ad5c2520a9b75285310dbfe78c541ccf832d38615765aec0"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"memchr",
"unicase",
]
@@ -1996,9 +2038,9 @@ dependencies = [
[[package]]
name = "regex"
version = "1.10.4"
version = "1.10.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c117dbdfde9c8308975b6a18d71f3f385c89461f7b3fb054288ecf2a2058ba4c"
checksum = "b91213439dad192326a0d7c6ee3955910425f441d7038e0d6933b0aec5c4517f"
dependencies = [
"aho-corasick",
"memchr",
@@ -2054,11 +2096,11 @@ checksum = "d626bb9dae77e28219937af045c257c28bfd3f69333c512553507f5f9798cb76"
[[package]]
name = "rustix"
version = "0.38.32"
version = "0.38.34"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
checksum = "70dc5ec042f7a43c4a73241207cecc9873a06d45debb38b329f8541d85c2730f"
dependencies = [
"bitflags 2.5.0",
"bitflags 2.6.0",
"errno",
"libc",
"linux-raw-sys",
@@ -2094,18 +2136,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
[[package]]
name = "serde"
version = "1.0.198"
version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
checksum = "bc76f558e0cbb2a839d37354c575f1dc3fdc6546b5be373ba43d95f231bf7c12"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.198"
version = "1.0.204"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
checksum = "e0cd7e117be63d3c3678776753929474f3b04a43a080c744d6b0ae2a8c28e222"
dependencies = [
"proc-macro2",
"quote",
@@ -2114,9 +2156,9 @@ dependencies = [
[[package]]
name = "serde_json"
version = "1.0.116"
version = "1.0.120"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5"
dependencies = [
"itoa",
"ryu",
@@ -2136,9 +2178,9 @@ dependencies = [
[[package]]
name = "serde_spanned"
version = "0.6.5"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eb3622f419d1296904700073ea6cc23ad690adbd66f13ea683df73298736f0c1"
checksum = "79e674e01f999af37c49f70a6ede167a8a60b2503e56c5599532a65baa5969a0"
dependencies = [
"serde",
]
@@ -2325,18 +2367,18 @@ dependencies = [
[[package]]
name = "thiserror"
version = "1.0.58"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "03468839009160513471e86a034bb2c5c0e4baae3b43f79ffc55c4a5427b3297"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.58"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c61f3ba182994efc43764a46c018c347bc492c79f024e705f46567b418f6d4f7"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@@ -2354,13 +2396,16 @@ dependencies = [
[[package]]
name = "time"
version = "0.3.23"
version = "0.3.36"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "59e399c068f43a5d116fedaf73b203fa4f9c519f17e2b34f63221d3792f81446"
checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
dependencies = [
"deranged",
"itoa",
"libc",
"num-conv",
"num_threads",
"powerfmt",
"serde",
"time-core",
"time-macros",
@@ -2368,16 +2413,17 @@ dependencies = [
[[package]]
name = "time-core"
version = "0.1.1"
version = "0.1.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb"
checksum = "ef927ca75afb808a4d64dd374f00a2adf8d0fcff8e7b184af886c3c87ec4a3f3"
[[package]]
name = "time-macros"
version = "0.2.10"
version = "0.2.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "96ba15a897f3c86766b757e5ac7221554c6750054d74d5b28844fce5fb36a6c4"
checksum = "3f252a68540fde3a3877aeea552b832b40ab9a69e318efd078774a01ddee1ccf"
dependencies = [
"num-conv",
"time-core",
]
@@ -2398,9 +2444,9 @@ checksum = "1f3ccbac311fea05f86f61904b462b55fb3df8837a366dfc601a0161d0532f20"
[[package]]
name = "tokio"
version = "1.37.0"
version = "1.38.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1adbebffeca75fcfd058afa480fb6c0b81e165a0323f9c9d39c9697e37c46787"
checksum = "ba4f4a02a7a80d6f274636f0aa95c7e383b912d41fe721a31f29e29698585a4a"
dependencies = [
"backtrace",
"bytes",
@@ -2417,9 +2463,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "2.2.0"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b8a1e28f2deaa14e508979454cb3a223b10b938b45af148bc0986de36f1923b"
checksum = "5f5ae998a069d4b5aba8ee9dad856af7d520c3699e6159b185c2acd48155d39a"
dependencies = [
"proc-macro2",
"quote",
@@ -2439,9 +2485,9 @@ dependencies = [
[[package]]
name = "toml"
version = "0.8.12"
version = "0.8.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e9dd1545e8208b4a5af1aa9bbd0b4cf7e9ea08fabc5d0a5c67fcaafa17433aa3"
checksum = "6f49eb2ab21d2f26bd6db7bf383edc527a7ebaee412d17af4d40fdccd442f335"
dependencies = [
"serde",
"serde_spanned",
@@ -2451,18 +2497,18 @@ dependencies = [
[[package]]
name = "toml_datetime"
version = "0.6.5"
version = "0.6.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3550f4e9685620ac18a50ed434eb3aec30db8ba93b0287467bca5826ea25baf1"
checksum = "4badfd56924ae69bcc9039335b2e017639ce3f9b001c393c1b2d1ef846ce2cbf"
dependencies = [
"serde",
]
[[package]]
name = "toml_edit"
version = "0.22.8"
version = "0.22.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c12219811e0c1ba077867254e5ad62ee2c9c190b0d957110750ac0cda1ae96cd"
checksum = "f21c7aaf97f1bd9ca9d4f9e73b0a6c74bd5afef56f2bc931943a6e1c37e04e38"
dependencies = [
"indexmap",
"serde",
@@ -2473,9 +2519,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.22.5"
version = "0.22.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "688200d842c76dd88f9a7719ecb0483f79f5a766fb1c100756d5d8a059abc71b"
checksum = "df7cc499ceadd4dcdf7ec6d4cbc34ece92c3fa07821e287aedecd4416c516dca"
dependencies = [
"cc",
"regex",
@@ -2492,9 +2538,9 @@ dependencies = [
[[package]]
name = "unicode-bidi"
version = "0.3.13"
version = "0.3.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "92888ba5573ff080736b3648696b70cafad7d250551175acbaa4e0385b3e1460"
checksum = "08f95100a766bf4f8f28f90d77e0a5461bbdb219042e7679bebe79004fed8d75"
[[package]]
name = "unicode-bom"
@@ -2537,15 +2583,15 @@ checksum = "d4c87d22b6e3f4a18d4d40ef354e97c90fcb14dd91d7dc0aa9d8a1172ebf7202"
[[package]]
name = "unicode-width"
version = "0.1.11"
version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51733f11c9c4f72aa0c160008246859e340b00807569a0da0e7a1079b27ba85"
checksum = "68f5e5f3158ecfd4b8ff6fe086db7c8467a2dfdac97fe420f2b7c4aa97af66d6"
[[package]]
name = "url"
version = "2.5.0"
version = "2.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "31e6302e3bb753d46e83516cae55ae196fc0c309407cf11ab35cc51a4c2a4633"
checksum = "22784dbdf76fdde8af1aeda5622b546b422b6fc585325248a2bf9f5e41e94d6c"
dependencies = [
"form_urlencoded",
"idna",
@@ -2896,7 +2942,7 @@ checksum = "d135d17ab770252ad95e9a872d365cf3090e3be864a34ab46f48555993efc904"
[[package]]
name = "xtask"
version = "24.3.0"
version = "24.7.0"
dependencies = [
"helix-core",
"helix-loader",

View File

@@ -12,6 +12,7 @@ members = [
"helix-vcs",
"helix-parsec",
"helix-stdx",
"helix-syntax",
"xtask",
]
@@ -38,11 +39,12 @@ package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.22" }
nucleo = "0.2.0"
nucleo = "0.5.0"
slotmap = "1.0.7"
thiserror = "1.0"
[workspace.package]
version = "24.3.0"
version = "24.7.0"
edition = "2021"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
categories = ["editor"]

View File

@@ -47,7 +47,7 @@ Note: Only certain languages have indentation definitions at the moment. Check
[Installation documentation](https://docs.helix-editor.com/install.html).
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg?exclude_unsupported=1)](https://repology.org/project/helix/versions)
# Contributing

View File

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

View File

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

View File

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

View File

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

395
book/src/editor.md Normal file
View File

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

View File

@@ -42,6 +42,7 @@
| edoc | ✓ | | | |
| eex | ✓ | | | |
| ejs | ✓ | | | |
| elisp | ✓ | | | |
| elixir | ✓ | ✓ | ✓ | `elixir-ls` |
| elm | ✓ | ✓ | | `elm-language-server` |
| elvish | ✓ | | | `elvish` |
@@ -62,6 +63,7 @@
| git-config | ✓ | | | |
| git-ignore | ✓ | | | |
| git-rebase | ✓ | | | |
| gjs | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| gleam | ✓ | ✓ | | `gleam` |
| glimmer | ✓ | | | `ember-language-server` |
| glsl | ✓ | ✓ | ✓ | |
@@ -73,6 +75,7 @@
| gowork | ✓ | | | `gopls` |
| graphql | ✓ | ✓ | | `graphql-lsp` |
| groovy | ✓ | | | |
| gts | ✓ | ✓ | ✓ | `typescript-language-server`, `vscode-eslint-language-server`, `ember-language-server` |
| hare | ✓ | | | |
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | |
@@ -83,11 +86,12 @@
| hoon | ✓ | | | |
| hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server` |
| hurl | ✓ | | ✓ | |
| hurl | ✓ | | ✓ | |
| hyprlang | ✓ | | ✓ | |
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
| inko | ✓ | ✓ | ✓ | |
| janet | ✓ | | | |
| java | ✓ | ✓ | ✓ | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
@@ -122,6 +126,7 @@
| mermaid | ✓ | | | |
| meson | ✓ | | ✓ | |
| mint | | | | `mint` |
| mojo | ✓ | ✓ | ✓ | `mojo-lsp-server` |
| move | ✓ | | | |
| msbuild | ✓ | | ✓ | |
| nasm | ✓ | ✓ | | |
@@ -141,6 +146,7 @@
| passwd | ✓ | | | |
| pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
| pest | ✓ | ✓ | ✓ | `pest-language-server` |
| php | ✓ | ✓ | ✓ | `intelephense` |
| php-only | ✓ | | | |
| pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` |
@@ -184,7 +190,7 @@
| supercollider | ✓ | | | |
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | | | `sourcekit-lsp` |
| swift | ✓ | | | `sourcekit-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | |
| tact | ✓ | ✓ | ✓ | |

View File

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

View File

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

View File

@@ -1,4 +1,4 @@
# Keymap
## Keymap
- [Normal mode](#normal-mode)
- [Movement](#movement)
@@ -12,8 +12,9 @@
- [Match mode](#match-mode)
- [Window mode](#window-mode)
- [Space mode](#space-mode)
- [Comment mode](#comment-mode)
- [Popup](#popup)
- [Completion Menu](#completion-menu)
- [Signature-help Popup](#signature-help-popup)
- [Unimpaired](#unimpaired)
- [Insert mode](#insert-mode)
- [Select / extend mode](#select--extend-mode)
@@ -24,7 +25,7 @@
> 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type.
> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the (wiki)[https://github.com/helix-editor/helix/wiki/Terminal-Support] for known conflicts.
> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the [wiki](https://github.com/helix-editor/helix/wiki/Terminal-Support) for known conflicts.
## Normal mode
@@ -88,7 +89,7 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` |
| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` |
| `=` | Format selection (**LSP**) | `format_selections` |
| `d` | Delete selection | `delete_selection` |
| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` |
@@ -232,8 +233,7 @@ Jumps to various locations.
Accessed by typing `m` in [normal mode](#normal-mode).
See the relevant section in [Usage](./usage.md) for an explanation about
[surround](./usage.md#surround) and [textobject](./usage.md#navigating-using-tree-sitter-textobjects) usage.
Please refer to the relevant sections for detailed explanations about [surround](./surround.md) and [textobjects](./textobjects.md).
| Key | Description | Command |
| ----- | ----------- | ------- |
@@ -309,13 +309,31 @@ This layer is a kludge of mappings, mostly pickers.
##### Popup
Displays documentation for item under cursor.
Displays documentation for item under cursor. Remapping currently not supported.
| Key | Description |
| ---- | ----------- |
| `Ctrl-u` | Scroll up |
| `Ctrl-d` | Scroll down |
##### Completion Menu
Displays documentation for the selected completion item. Remapping currently not supported.
| Key | Description |
| ---- | ----------- |
| `Shift-Tab`, `Ctrl-p`, `Up` | Previous entry |
| `Tab`, `Ctrl-n`, `Down` | Next entry |
##### Signature-help Popup
Displays the signature of the selected completion item. Remapping currently not supported.
| Key | Description |
| ---- | ----------- |
| `Alt-p` | Previous signature |
| `Alt-n` | Next signature |
#### Unimpaired
These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).

View File

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

54
book/src/registers.md Normal file
View File

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

24
book/src/surround.md Normal file
View File

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

View File

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

47
book/src/textobjects.md Normal file
View File

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

View File

@@ -179,6 +179,7 @@ We use a similar set of scopes as
- `parameter` - Function parameters
- `other`
- `member` - Fields of composite data types (e.g. structs, unions)
- `private` - Private fields that use a unique syntax (currently just ECMAScript-based languages)
- `label`
@@ -206,6 +207,7 @@ We use a similar set of scopes as
- `function`
- `builtin`
- `method`
- `private` - Private methods that use a unique syntax (currently just ECMAScript-based languages)
- `macro`
- `special` (preprocessor in C)
@@ -295,6 +297,8 @@ These scopes are used for theming the editor interface:
| `ui.bufferline.background` | Style for bufferline background |
| `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.picker.header` | Column names in pickers with multiple columns |
| `ui.picker.header.active` | The column name in pickers with multiple columns where the cursor is entering into. |
| `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Default text style, command prompts, popup text, etc. |

View File

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

View File

@@ -47,6 +47,9 @@
<content_rating type="oars-1.1" />
<releases>
<release version="24.07" date="2024-07-14">
<url>https://github.com/helix-editor/helix/releases/tag/24.07</url>
</release>
<release version="24.03" date="2024-03-30">
<url>https://helix-editor.com/news/release-24-03-highlights/</url>
</release>

View File

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

View File

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

View File

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

View File

@@ -31,7 +31,7 @@ being published.
* Post to reddit
* [Example post](https://www.reddit.com/r/rust/comments/uzp5ze/helix_editor_2205_released/)
[homebrew formula]: https://github.com/Homebrew/homebrew-core/blob/master/Formula/helix.rb
[homebrew formula]: https://github.com/Homebrew/homebrew-core/blob/master/Formula/h/helix.rb
## Changelog Curation

View File

@@ -114,10 +114,7 @@
if pkgs.stdenv.isLinux
then pkgs.stdenv
else pkgs.clangStdenv;
rustFlagsEnv =
if stdenv.isLinux
then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment''
else "$RUSTFLAGS";
rustFlagsEnv = pkgs.lib.optionalString stdenv.isLinux "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment";
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain;
craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default;
@@ -183,7 +180,7 @@
shellHook = ''
export HELIX_RUNTIME="$PWD/runtime"
export RUST_BACKTRACE="1"
export RUSTFLAGS="${rustFlagsEnv}"
export RUSTFLAGS="''${RUSTFLAGS:-""} ${rustFlagsEnv}"
'';
};
})

View File

@@ -17,6 +17,7 @@ integration = []
[dependencies]
helix-stdx = { path = "../helix-stdx" }
helix-syntax = { path = "../helix-syntax" }
helix-loader = { path = "../helix-loader" }
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
@@ -30,17 +31,18 @@ tree-sitter.workspace = true
once_cell = "1.19"
arc-swap = "1"
regex = "1"
bitflags = "2.5"
bitflags = "2.6"
ahash = "0.8.11"
hashbrown = { version = "0.14.3", features = ["raw"] }
hashbrown = { version = "0.14.5", features = ["raw"] }
dunce = "1.0"
url = "2.5.0"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.8"
imara-diff = "0.1.0"
imara-diff = "0.1.6"
encoding_rs = "0.8"

View File

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

View File

@@ -738,18 +738,9 @@ fn init_indent_query<'a, 'b>(
.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_else(QueryCursor::new);
let query_result = query_indents(
query,
syntax,
&mut cursor,
text,
query_range,
new_line_byte_pos,
);
ts_parser.cursors.push(cursor);
crate::syntax::with_cursor(|cursor| {
let query_result =
query_indents(query, syntax, cursor, text, query_range, new_line_byte_pos);
(query_result, deepest_preceding)
})
};

View File

@@ -27,6 +27,7 @@ pub mod test;
pub mod text_annotations;
pub mod textobject;
mod transaction;
pub mod uri;
pub mod wrap;
pub mod unicode {
@@ -66,3 +67,5 @@ pub use diagnostic::Diagnostic;
pub use line_ending::{LineEnding, NATIVE_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Deletion, Operation, Transaction};
pub use uri::Uri;

View File

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

View File

@@ -13,7 +13,7 @@ use crate::{
};
use helix_stdx::rope::{self, RopeSliceExt};
use smallvec::{smallvec, SmallVec};
use std::borrow::Cow;
use std::{borrow::Cow, iter, slice};
use tree_sitter::Node;
/// A single selection range.
@@ -122,7 +122,7 @@ impl Range {
}
/// `Direction::Backward` when head < anchor.
/// `Direction::Backward` otherwise.
/// `Direction::Forward` otherwise.
#[inline]
#[must_use]
pub fn direction(&self) -> Direction {
@@ -175,7 +175,7 @@ impl Range {
/// function runs in O(N) (N is number of changes) and can therefore
/// cause performance problems if run for a large number of ranges as the
/// complexity is then O(MN) (for multicuror M=N usually). Instead use
/// [Selection::map] or [ChangeSet::update_positions] instead
/// [Selection::map] or [ChangeSet::update_positions].
pub fn map(mut self, changes: &ChangeSet) -> Self {
use std::cmp::Ordering;
if changes.is_empty() {
@@ -503,6 +503,16 @@ impl Selection {
&self.ranges
}
/// Returns an iterator over the line ranges of each range in the selection.
///
/// Adjacent and overlapping line ranges of the [Range]s in the selection are merged.
pub fn line_ranges<'a>(&'a self, text: RopeSlice<'a>) -> LineRangeIter<'a> {
LineRangeIter {
ranges: self.ranges.iter().peekable(),
text,
}
}
pub fn primary_index(&self) -> usize {
self.primary_index
}
@@ -531,6 +541,8 @@ impl Selection {
}
/// Normalizes a `Selection`.
///
/// Ranges are sorted by [Range::from], with overlapping ranges merged.
fn normalize(mut self) -> Self {
if self.len() < 2 {
return self;
@@ -727,6 +739,33 @@ impl From<Range> for Selection {
}
}
pub struct LineRangeIter<'a> {
ranges: iter::Peekable<slice::Iter<'a, Range>>,
text: RopeSlice<'a>,
}
impl<'a> Iterator for LineRangeIter<'a> {
type Item = (usize, usize);
fn next(&mut self) -> Option<Self::Item> {
let (start, mut end) = self.ranges.next()?.line_range(self.text);
while let Some((next_start, next_end)) =
self.ranges.peek().map(|range| range.line_range(self.text))
{
// Merge overlapping and adjacent ranges.
// This subtraction cannot underflow because the ranges are sorted.
if next_start - end <= 1 {
end = next_end;
self.ranges.next();
} else {
break;
}
}
Some((start, end))
}
}
// TODO: checkSelection -> check if valid for doc length && sorted
pub fn keep_or_remove_matches(
@@ -1165,6 +1204,32 @@ mod test {
assert_eq!(Range::new(12, 0).line_range(s), (0, 2));
}
#[test]
fn selection_line_ranges() {
let (text, selection) = crate::test::print(
r#" L0
#[|these]# line #(|ranges)# are #(|merged)# L1
L2
single one-line #(|range)# L3
L4
single #(|multiline L5
range)# L6
L7
these #(|multiline L8
ranges)# are #(|also L9
merged)# L10
L11
adjacent #(|ranges)# L12
are merged #(|the same way)# L13
"#,
);
let rope = Rope::from_str(&text);
assert_eq!(
vec![(1, 1), (3, 3), (5, 6), (8, 10), (12, 13)],
selection.line_ranges(rope.slice(..)).collect::<Vec<_>>(),
);
}
#[test]
fn test_cursor() {
let r = Rope::from_str("\r\nHi\r\nthere!");

View File

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

File diff suppressed because it is too large Load Diff

View File

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

View File

@@ -378,7 +378,9 @@ impl ChangeSet {
macro_rules! map {
($map: expr, $i: expr) => {
loop {
let Some((pos, assoc)) = positions.peek_mut() else { return; };
let Some((pos, assoc)) = positions.peek_mut() else {
return;
};
if **pos < old_pos {
// Positions are not sorted, revert to the last Operation that
// contains this position and continue iterating from there.
@@ -405,7 +407,10 @@ impl ChangeSet {
debug_assert!(old_pos <= **pos, "Reverse Iter across changeset works");
continue 'outer;
}
let Some(new_pos) = $map(**pos, *assoc) else { break; };
#[allow(clippy::redundant_closure_call)]
let Some(new_pos) = $map(**pos, *assoc) else {
break;
};
**pos = new_pos;
positions.next();
}

122
helix-core/src/uri.rs Normal file
View File

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

View File

@@ -1,4 +1,3 @@
use arc_swap::ArcSwap;
use helix_core::{
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
syntax::{Configuration, Loader},
@@ -6,7 +5,7 @@ use helix_core::{
};
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() {
@@ -36,7 +35,7 @@ fn test_treesitter_indent_rust_helix() {
.unwrap();
let files = String::from_utf8(files.stdout).unwrap();
let ignored_files = vec![
let ignored_files = [
// Contains many macros that tree-sitter does not parse in a meaningful way and is otherwise not very interesting
"helix-term/src/health.rs",
];
@@ -45,6 +44,7 @@ fn test_treesitter_indent_rust_helix() {
if ignored_files.contains(&file) {
continue;
}
#[allow(clippy::single_range_in_vec_init)]
let ignored_lines: Vec<Range<usize>> = match file {
"helix-term/src/application.rs" => vec![
// We can't handle complicated indent rules inside macros (`json!` in this case) since
@@ -199,12 +199,7 @@ fn test_treesitter_indent(
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 syntax = Syntax::new(text, highlight_config, |_| None).unwrap();
let indent_query = language_config.indent_query().unwrap();
for i in 0..doc.len_lines() {

View File

@@ -20,8 +20,8 @@ anyhow = "1.0"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "net", "sync"] }
thiserror.workspace = true
[dev-dependencies]
fern = "0.6"

View File

@@ -157,8 +157,8 @@ impl Client {
)
}
pub fn starting_request_args(&self) -> &Option<Value> {
&self.starting_request_args
pub fn starting_request_args(&self) -> Option<&Value> {
self.starting_request_args.as_ref()
}
pub async fn tcp_process(

View File

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

View File

@@ -13,7 +13,7 @@ homepage.workspace = true
[dependencies]
ahash = "0.8.11"
hashbrown = "0.14.0"
hashbrown = "0.14.5"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
# the event registry is essentially read only but must be an rwlock so we can
# setup new events on initialization, hardware-lock-elision hugely benefits this case

View File

@@ -34,7 +34,9 @@
use anyhow::Result;
pub use cancel::{cancelable_future, cancelation, CancelRx, CancelTx};
pub use debounce::{send_blocking, AsyncHook};
pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard};
pub use redraw::{
lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard, RequestRedrawOnDrop,
};
pub use registry::Event;
mod cancel;

View File

@@ -51,3 +51,12 @@ pub fn start_frame() {
pub fn lock_frame() -> RenderLockGuard {
RENDER_LOCK.read()
}
/// A zero sized type that requests a redraw via [request_redraw] when the type [Drop]s.
pub struct RequestRedrawOnDrop;
impl Drop for RequestRedrawOnDrop {
fn drop(&mut self) {
request_redraw();
}
}

View File

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

View File

@@ -422,7 +422,7 @@ fn build_tree_sitter_library(
}
}
let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
let recompile = needs_recompile(&library_path, &parser_path, scanner_path.as_ref())
.context("Failed to compare source and binary timestamps")?;
if !recompile {
@@ -568,7 +568,7 @@ fn build_tree_sitter_library(
fn needs_recompile(
lib_path: &Path,
parser_c_path: &Path,
scanner_path: &Option<PathBuf>,
scanner_path: Option<&PathBuf>,
) -> Result<bool> {
if !lib_path.exists() {
return Ok(true);

View File

@@ -26,9 +26,9 @@ log = "0.4"
lsp-types = { version = "0.95" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio = { version = "1.38", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.15"
parking_lot = "0.12.1"
parking_lot = "0.12.3"
arc-swap = "1"
slotmap.workspace = true
thiserror.workspace = true

View File

@@ -123,7 +123,7 @@ impl Client {
{
client.add_workspace_folder(
root_uri,
&workspace_folders_caps.change_notifications,
workspace_folders_caps.change_notifications.as_ref(),
);
}
});
@@ -136,7 +136,10 @@ impl Client {
.and_then(|cap| cap.workspace_folders.as_ref())
.filter(|cap| cap.supported.unwrap_or(false))
{
self.add_workspace_folder(root_uri, &workspace_folders_caps.change_notifications);
self.add_workspace_folder(
root_uri,
workspace_folders_caps.change_notifications.as_ref(),
);
true
} else {
// the server doesn't support multi workspaces, we need a new client
@@ -147,7 +150,7 @@ impl Client {
fn add_workspace_folder(
&self,
root_uri: Option<lsp::Url>,
change_notifications: &Option<OneOf<bool, String>>,
change_notifications: Option<&OneOf<bool, String>>,
) {
// root_uri is None just means that there isn't really any LSP workspace
// associated with this file. For servers that support multiple workspaces
@@ -162,7 +165,7 @@ impl Client {
self.workspace_folders
.lock()
.push(workspace_for_uri(root_uri.clone()));
if &Some(OneOf::Left(false)) == change_notifications {
if Some(&OneOf::Left(false)) == change_notifications {
// server specifically opted out of DidWorkspaceChange notifications
// let's assume the server will request the workspace folders itself
// and that we can therefore reuse the client (but are done now)
@@ -616,6 +619,9 @@ impl Client {
prepare_support_default_behavior: None,
honors_change_annotations: Some(false),
}),
formatting: Some(lsp::DocumentFormattingClientCapabilities {
dynamic_registration: Some(false),
}),
code_action: Some(lsp::CodeActionClientCapabilities {
code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
code_action_kind: lsp::CodeActionKindLiteralSupport {

View File

@@ -139,7 +139,7 @@ impl Handler {
registration_id
);
let entry = state.entry(client_id).or_insert_with(ClientState::default);
let entry = state.entry(client_id).or_default();
entry.client = client;
let mut builder = GlobSetBuilder::new();

View File

@@ -678,7 +678,7 @@ impl Registry {
pub fn remove_by_id(&mut self, id: LanguageServerId) {
let Some(client) = self.inner.remove(id) else {
log::error!("client was already removed");
return
return;
};
self.file_event_handler.remove_client(id);
let instances = self
@@ -736,35 +736,30 @@ impl Registry {
.language_servers
.iter()
.filter_map(|LanguageServerFeatures { name, .. }| {
if self.inner_by_name.contains_key(name) {
let client = match self.start_client(
name.clone(),
language_config,
doc_path,
root_dirs,
enable_snippets,
) {
Ok(client) => client,
Err(StartupError::NoRequiredRootFound) => return None,
Err(StartupError::Error(err)) => return Some(Err(err)),
};
let old_clients = self
.inner_by_name
.insert(name.clone(), vec![client.clone()])
.unwrap();
if let Some(old_clients) = self.inner_by_name.remove(name) {
for old_client in old_clients {
self.file_event_handler.remove_client(old_client.id());
self.inner.remove(client.id());
self.inner.remove(old_client.id());
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
}
Some(Ok(client))
} else {
None
}
let client = match self.start_client(
name.clone(),
language_config,
doc_path,
root_dirs,
enable_snippets,
) {
Ok(client) => client,
Err(StartupError::NoRequiredRootFound) => return None,
Err(StartupError::Error(err)) => return Some(Err(err)),
};
self.inner_by_name
.insert(name.to_owned(), vec![client.clone()]);
Some(Ok(client))
})
.collect()
}

View File

@@ -17,7 +17,7 @@ etcetera = "0.8"
ropey = { version = "1.6.1", default-features = false }
which = "6.0"
regex-cursor = "0.1.4"
bitflags = "2.4"
bitflags = "2.6"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.52", features = ["Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading"] }

View File

@@ -14,13 +14,23 @@ pub fn current_working_dir() -> PathBuf {
return path.clone();
}
let path = std::env::current_dir()
.map(crate::path::normalize)
.expect("Couldn't determine current working directory");
let mut cwd = CWD.write().unwrap();
*cwd = Some(path.clone());
// implementation of crossplatform pwd -L
// we want pwd -L so that symlinked directories are handled correctly
let mut cwd = std::env::current_dir().expect("Couldn't determine current working directory");
path
let pwd = std::env::var_os("PWD");
#[cfg(windows)]
let pwd = pwd.or_else(|| std::env::var_os("CD"));
if let Some(pwd) = pwd.map(PathBuf::from) {
if pwd.canonicalize().ok().as_ref() == Some(&cwd) {
cwd = pwd;
}
}
let mut dst = CWD.write().unwrap();
*dst = Some(cwd.clone());
cwd
}
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {

34
helix-syntax/Cargo.toml Normal file
View File

@@ -0,0 +1,34 @@
[package]
name = "helix-syntax"
description = "Helix syntax highlighting "
include = ["src/**/*", "README.md"]
version.workspace = true
authors.workspace = true
edition.workspace = true
license.workspace = true
rust-version.workspace = true
categories.workspace = true
repository.workspace = true
homepage.workspace = true
[features]
[dependencies]
helix-stdx = { path = "../helix-stdx" }
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
slotmap = "1.0"
tree-sitter.workspace = true
once_cell = "1.19"
arc-swap = "1"
regex = "1"
bitflags = "2.4"
ahash = "0.8.9"
hashbrown = { version = "0.14.3", features = ["raw"] }
log = "0.4"
regex-cursor = "0.1.4"
libloading = "0.8.3"
thiserror = "1.0.59"
[build-dependencies]
cc = "1.0.95"

28
helix-syntax/build.rs Normal file
View File

@@ -0,0 +1,28 @@
use std::path::PathBuf;
use std::{env, fs};
fn main() {
if env::var_os("DISABLED_TS_BUILD").is_some() {
return;
}
let mut config = cc::Build::new();
let manifest_path = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap());
let include_path = manifest_path.join("../vendor/tree-sitter/include");
let src_path = manifest_path.join("../vendor/tree-sitter/src");
for entry in fs::read_dir(&src_path).unwrap() {
let entry = entry.unwrap();
let path = src_path.join(entry.file_name());
println!("cargo:rerun-if-changed={}", path.to_str().unwrap());
}
config
.flag_if_supported("-std=c11")
.flag_if_supported("-fvisibility=hidden")
.flag_if_supported("-Wshadow")
.flag_if_supported("-Wno-unused-parameter")
.include(&src_path)
.include(&include_path)
.file(src_path.join("lib.c"))
.compile("tree-sitter");
}

346
helix-syntax/src/config.rs Normal file
View File

@@ -0,0 +1,346 @@
use std::borrow::Cow;
use std::path::Path;
use std::sync::Arc;
use crate::tree_sitter::query::{Capture, Pattern, QueryStr, UserPredicate};
use crate::tree_sitter::{query, Grammar, Query, QueryMatch, SyntaxTreeNode};
use arc_swap::ArcSwap;
use helix_stdx::rope::{self, RopeSliceExt};
use once_cell::sync::Lazy;
use regex::Regex;
use ropey::RopeSlice;
use crate::byte_range_to_str;
use crate::highlighter::Highlight;
/// Contains the data needed to highlight code written in a particular language.
///
/// This struct is immutable and can be shared between threads.
#[derive(Debug)]
pub struct HighlightConfiguration {
pub grammar: Grammar,
pub query: Query,
pub(crate) injections_query: Query,
pub(crate) combined_injections_patterns: Vec<Pattern>,
first_highlights_pattern: Pattern,
pub(crate) highlight_indices: ArcSwap<Vec<Highlight>>,
pub(crate) non_local_variable_patterns: Vec<bool>,
pub(crate) injection_content_capture: Option<Capture>,
pub(crate) injection_language_capture: Option<Capture>,
pub(crate) injection_filename_capture: Option<Capture>,
pub(crate) injection_shebang_capture: Option<Capture>,
pub(crate) local_scope_capture: Option<Capture>,
pub(crate) local_def_capture: Option<Capture>,
pub(crate) local_def_value_capture: Option<Capture>,
pub(crate) local_ref_capture: Option<Capture>,
}
impl HighlightConfiguration {
/// Creates a `HighlightConfiguration` for a given `Grammar` and set of highlighting
/// queries.
///
/// # Parameters
///
/// * `language` - The Tree-sitter `Grammar` that should be used for parsing.
/// * `highlights_query` - A string containing tree patterns for syntax highlighting. This
/// should be non-empty, otherwise no syntax highlights will be added.
/// * `injections_query` - A string containing tree patterns for injecting other languages
/// into the document. This can be empty if no injections are desired.
/// * `locals_query` - A string containing tree patterns for tracking local variable
/// definitions and references. This can be empty if local variable tracking is not needed.
///
/// Returns a `HighlightConfiguration` that can then be used with the `highlight` method.
pub fn new(
grammar: Grammar,
path: impl AsRef<Path>,
highlights_query: &str,
injection_query: &str,
locals_query: &str,
) -> Result<Self, query::ParseError> {
// Concatenate the query strings, keeping track of the start offset of each section.
let mut query_source = String::new();
query_source.push_str(locals_query);
let highlights_query_offset = query_source.len();
query_source.push_str(highlights_query);
let mut non_local_variable_patterns = Vec::with_capacity(32);
// Construct a single query by concatenating the three query strings, but record the
// range of pattern indices that belong to each individual string.
let query = Query::new(grammar, &query_source, path, |pattern, predicate| {
match predicate {
UserPredicate::IsPropertySet {
negate: true,
key: "local",
val: None,
} => {
if non_local_variable_patterns.len() < pattern.idx() {
non_local_variable_patterns.resize(pattern.idx(), false)
}
non_local_variable_patterns[pattern.idx()] = true;
}
predicate => {
return Err(format!("unsupported predicate {predicate}").into());
}
}
Ok(())
})?;
let mut combined_injections_patterns = Vec::new();
let injections_query = Query::new(grammar, injection_query, path, |pattern, predicate| {
match predicate {
UserPredicate::SetProperty {
key: "injection.combined",
val: None,
} => combined_injections_patterns.push(pattern),
predicate => {
return Err(format!("unsupported predicate {predicate}").into());
}
}
Ok(())
})?;
let first_highlights_pattern = query
.patterns()
.find(|pattern| query.start_byte_for_pattern(*pattern) >= highlights_query_offset)
.unwrap_or(Pattern::SENTINEL);
let injection_content_capture = query.get_capture("injection.content");
let injection_language_capture = query.get_capture("injection.language");
let injection_filename_capture = query.get_capture("injection.filename");
let injection_shebang_capture = query.get_capture("injection.shebang");
let local_def_capture = query.get_capture("local.definition");
let local_def_value_capture = query.get_capture("local.definition-value");
let local_ref_capture = query.get_capture("local.reference");
let local_scope_capture = query.get_capture("local.scope");
let highlight_indices =
ArcSwap::from_pointee(vec![Highlight::NONE; query.num_captures() as usize]);
Ok(Self {
grammar,
query,
injections_query,
combined_injections_patterns,
first_highlights_pattern,
highlight_indices,
non_local_variable_patterns,
injection_content_capture,
injection_language_capture,
injection_filename_capture,
injection_shebang_capture,
local_scope_capture,
local_def_capture,
local_def_value_capture,
local_ref_capture,
})
}
/// Set the list of recognized highlight names.
///
/// Tree-sitter syntax-highlighting queries specify highlights in the form of dot-separated
/// highlight names like `punctuation.bracket` and `function.method.builtin`. Consumers of
/// these queries can choose to recognize highlights with different levels of specificity.
/// For example, the string `function.builtin` will match against `function.builtin.constructor`
/// but will not match `function.method.builtin` and `function.method`.
///
/// When highlighting, results are returned as `Highlight` values, which contain the index
/// of the matched highlight this list of highlight names.
pub fn configure(&self, recognized_names: &[String]) {
let mut capture_parts = Vec::new();
let indices: Vec<_> = self
.query
.captures()
.map(move |(_, capture_name)| {
capture_parts.clear();
capture_parts.extend(capture_name.split('.'));
let mut best_index = u32::MAX;
let mut best_match_len = 0;
for (i, recognized_name) in recognized_names.iter().enumerate() {
let mut len = 0;
let mut matches = true;
for (i, part) in recognized_name.split('.').enumerate() {
match capture_parts.get(i) {
Some(capture_part) if *capture_part == part => len += 1,
_ => {
matches = false;
break;
}
}
}
if matches && len > best_match_len {
best_index = i as u32;
best_match_len = len;
}
}
Highlight(best_index)
})
.collect();
self.highlight_indices.store(Arc::new(indices));
}
fn injection_pair<'a>(
&self,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (
Option<InjectionLanguageMarker<'a>>,
Option<SyntaxTreeNode<'a>>,
) {
let mut injection_capture = None;
let mut content_node = None;
for matched_node in query_match.matched_nodes() {
let capture = Some(matched_node.capture);
if capture == self.injection_language_capture {
let name = byte_range_to_str(matched_node.syntax_node.byte_range(), source);
injection_capture = Some(InjectionLanguageMarker::Name(name));
} else if capture == self.injection_filename_capture {
let name = byte_range_to_str(matched_node.syntax_node.byte_range(), source);
let path = Path::new(name.as_ref()).to_path_buf();
injection_capture = Some(InjectionLanguageMarker::Filename(path.into()));
} else if capture == self.injection_shebang_capture {
let node_slice = source.byte_slice(matched_node.syntax_node.byte_range());
// some languages allow space and newlines before the actual string content
// so a shebang could be on either the first or second line
let lines = if let Ok(end) = node_slice.try_line_to_byte(2) {
node_slice.byte_slice(..end)
} else {
node_slice
};
injection_capture = SHEBANG_REGEX
.captures_iter(lines.regex_input())
.map(|cap| {
let cap = lines.byte_slice(cap.get_group(1).unwrap().range());
InjectionLanguageMarker::Shebang(cap.into())
})
.next()
} else if capture == self.injection_content_capture {
content_node = Some(matched_node.syntax_node.clone());
}
}
(injection_capture, content_node)
}
pub(super) fn injection_for_match<'a>(
&self,
query: &'a Query,
query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>,
) -> (
Option<InjectionLanguageMarker<'a>>,
Option<SyntaxTreeNode<'a>>,
IncludedChildren,
) {
let (mut injection_capture, content_node) = self.injection_pair(query_match, source);
let mut included_children = IncludedChildren::default();
for prop in query.property_settings(query_match.pattern_index) {
match prop.key.as_ref() {
// In addition to specifying the language name via the text of a
// captured node, it can also be hard-coded via a `#set!` predicate
// that sets the injection.language key.
"injection.language" if injection_capture.is_none() => {
injection_capture = prop
.value
.as_ref()
.map(|s| InjectionLanguageMarker::Name(s.as_ref().into()));
}
// By default, injections do not include the *children* of an
// `injection.content` node - only the ranges that belong to the
// node itself. This can be changed using a `#set!` predicate that
// sets the `injection.include-children` key.
"injection.include-children" => included_children = IncludedChildren::All,
// Some queries might only exclude named children but include unnamed
// children in their `injection.content` node. This can be enabled using
// a `#set!` predicate that sets the `injection.include-unnamed-children` key.
"injection.include-unnamed-children" => {
included_children = IncludedChildren::Unnamed
}
_ => {}
}
}
(injection_capture, content_node, included_children)
}
// pub fn load_query(
// &self,
// language: &str,
// filename: &str,
// read_query_text: impl FnMut(&str, &str) -> String,
// ) -> Result<Option<Query>, QueryError> {
// let query_text = read_query(language, filename, read_query_text);
// if query_text.is_empty() {
// return Ok(None);
// }
// Query::new(&self.grammar, &query_text, ).map(Some)
// }
}
/// reads a query by invoking `read_query_text`, handeles any `inherits` directives
pub fn read_query(
language: &str,
filename: &str,
mut read_query_text: impl FnMut(&str, &str) -> String,
) -> String {
fn read_query_impl(
language: &str,
filename: &str,
read_query_text: &mut impl FnMut(&str, &str) -> String,
) -> String {
static INHERITS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()-]+)\s*").unwrap());
let query = read_query_text(language, filename);
// replaces all "; inherits <language>(,<language>)*" with the queries of the given language(s)
INHERITS_REGEX
.replace_all(&query, |captures: &regex::Captures| {
captures[1]
.split(',')
.map(|language| {
format!(
"\n{}\n",
read_query_impl(language, filename, &mut *read_query_text)
)
})
.collect::<String>()
})
.to_string()
}
read_query_impl(language, filename, &mut read_query_text)
}
const SHEBANG: &str = r"#!\s*(?:\S*[/\\](?:env\s+(?:\-\S+\s+)*)?)?([^\s\.\d]+)";
static SHEBANG_REGEX: Lazy<rope::Regex> = Lazy::new(|| rope::Regex::new(SHEBANG).unwrap());
struct InjectionSettings {
include_children: IncludedChildren,
language: Option<QueryStr>,
}
#[derive(Debug, Clone)]
pub enum InjectionLanguageMarker<'a> {
Name(Cow<'a, str>),
Filename(Cow<'a, Path>),
Shebang(String),
}
#[derive(Clone)]
enum IncludedChildren {
None,
All,
Unnamed,
}
impl Default for IncludedChildren {
fn default() -> Self {
Self::None
}
}

View File

@@ -0,0 +1,438 @@
pub use super::highlighter2::*;
// use std::borrow::Cow;
// use std::cell::RefCell;
// use std::sync::atomic::{self, AtomicUsize};
// use std::{fmt, iter, mem, ops};
// use ropey::RopeSlice;
// use tree_sitter::{QueryCaptures, QueryCursor, Tree};
// use crate::{byte_range_to_str, Error, HighlightConfiguration, Syntax, TREE_SITTER_MATCH_LIMIT};
// const CANCELLATION_CHECK_INTERVAL: usize = 100;
// /// Indicates which highlight should be applied to a region of source code.
// #[derive(Copy, Clone, Debug, PartialEq, Eq)]
// pub struct Highlight(pub usize);
// /// Represents a single step in rendering a syntax-highlighted document.
// #[derive(Copy, Clone, Debug)]
// pub enum HighlightEvent {
// Source { start: usize, end: usize },
// HighlightStart(Highlight),
// HighlightEnd,
// }
// #[derive(Debug)]
// struct LocalDef<'a> {
// name: Cow<'a, str>,
// value_range: ops::Range<usize>,
// highlight: Option<Highlight>,
// }
// #[derive(Debug)]
// struct LocalScope<'a> {
// inherits: bool,
// range: ops::Range<usize>,
// local_defs: Vec<LocalDef<'a>>,
// }
// #[derive(Debug)]
// struct HighlightIter<'a> {
// source: RopeSlice<'a>,
// byte_offset: usize,
// cancellation_flag: Option<&'a AtomicUsize>,
// layers: Vec<HighlightIterLayer<'a>>,
// iter_count: usize,
// next_event: Option<HighlightEvent>,
// last_highlight_range: Option<(usize, usize, u32)>,
// }
// struct HighlightIterLayer<'a> {
// _tree: Option<Tree>,
// cursor: QueryCursor,
// captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>>>,
// config: &'a HighlightConfiguration,
// highlight_end_stack: Vec<usize>,
// scope_stack: Vec<LocalScope<'a>>,
// depth: u32,
// }
// impl<'a> fmt::Debug for HighlightIterLayer<'a> {
// fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
// f.debug_struct("HighlightIterLayer").finish()
// }
// }
// impl<'a> HighlightIterLayer<'a> {
// // First, sort scope boundaries by their byte offset in the document. At a
// // given position, emit scope endings before scope beginnings. Finally, emit
// // scope boundaries from deeper layers first.
// fn sort_key(&self) -> Option<(usize, bool, isize)> {
// let depth = -(self.depth as isize);
// let next_start = self
// .captures
// .borrow_mut()
// .peek()
// .map(|(m, i)| m.captures[*i].node.start_byte());
// let next_end = self.highlight_end_stack.last().cloned();
// match (next_start, next_end) {
// (Some(start), Some(end)) => {
// if start < end {
// Some((start, true, depth))
// } else {
// Some((end, false, depth))
// }
// }
// (Some(i), None) => Some((i, true, depth)),
// (None, Some(j)) => Some((j, false, depth)),
// _ => None,
// }
// }
// }
// impl<'a> HighlightIter<'a> {
// fn emit_event(
// &mut self,
// offset: usize,
// event: Option<HighlightEvent>,
// ) -> Option<Result<HighlightEvent, Error>> {
// let result;
// if self.byte_offset < offset {
// result = Some(Ok(HighlightEvent::Source {
// start: self.byte_offset,
// end: offset,
// }));
// self.byte_offset = offset;
// self.next_event = event;
// } else {
// result = event.map(Ok);
// }
// self.sort_layers();
// result
// }
// fn sort_layers(&mut self) {
// while !self.layers.is_empty() {
// if let Some(sort_key) = self.layers[0].sort_key() {
// let mut i = 0;
// while i + 1 < self.layers.len() {
// if let Some(next_offset) = self.layers[i + 1].sort_key() {
// if next_offset < sort_key {
// i += 1;
// continue;
// }
// } else {
// let layer = self.layers.remove(i + 1);
// PARSER.with(|ts_parser| {
// let highlighter = &mut ts_parser.borrow_mut();
// highlighter.cursors.push(layer.cursor);
// });
// }
// break;
// }
// if i > 0 {
// self.layers[0..(i + 1)].rotate_left(1);
// }
// break;
// } else {
// let layer = self.layers.remove(0);
// PARSER.with(|ts_parser| {
// let highlighter = &mut ts_parser.borrow_mut();
// highlighter.cursors.push(layer.cursor);
// });
// }
// }
// }
// }
// impl<'a> Iterator for HighlightIter<'a> {
// type Item = Result<HighlightEvent, Error>;
// fn next(&mut self) -> Option<Self::Item> {
// 'main: loop {
// // If we've already determined the next highlight boundary, just return it.
// if let Some(e) = self.next_event.take() {
// return Some(Ok(e));
// }
// // Periodically check for cancellation, returning `Cancelled` error if the
// // cancellation flag was flipped.
// if let Some(cancellation_flag) = self.cancellation_flag {
// self.iter_count += 1;
// if self.iter_count >= CANCELLATION_CHECK_INTERVAL {
// self.iter_count = 0;
// if cancellation_flag.load(atomic::Ordering::Relaxed) != 0 {
// return Some(Err(Error::Cancelled));
// }
// }
// }
// // If none of the layers have any more highlight boundaries, terminate.
// if self.layers.is_empty() {
// let len = self.source.len_bytes();
// return if self.byte_offset < len {
// let result = Some(Ok(HighlightEvent::Source {
// start: self.byte_offset,
// end: len,
// }));
// self.byte_offset = len;
// result
// } else {
// None
// };
// }
// // Get the next capture from whichever layer has the earliest highlight boundary.
// let range;
// let layer = &mut self.layers[0];
// let captures = layer.captures.get_mut();
// if let Some((next_match, capture_index)) = captures.peek() {
// let next_capture = next_match.captures[*capture_index];
// range = next_capture.node.byte_range();
// // If any previous highlight ends before this node starts, then before
// // processing this capture, emit the source code up until the end of the
// // previous highlight, and an end event for that highlight.
// if let Some(end_byte) = layer.highlight_end_stack.last().cloned() {
// if end_byte <= range.start {
// layer.highlight_end_stack.pop();
// return self.emit_event(end_byte, Some(HighlightEvent::HighlightEnd));
// }
// }
// }
// // If there are no more captures, then emit any remaining highlight end events.
// // And if there are none of those, then just advance to the end of the document.
// else if let Some(end_byte) = layer.highlight_end_stack.last().cloned() {
// layer.highlight_end_stack.pop();
// return self.emit_event(end_byte, Some(HighlightEvent::HighlightEnd));
// } else {
// return self.emit_event(self.source.len_bytes(), None);
// };
// let (mut match_, capture_index) = captures.next().unwrap();
// let mut capture = match_.captures[capture_index];
// // Remove from the local scope stack any local scopes that have already ended.
// while range.start > layer.scope_stack.last().unwrap().range.end {
// layer.scope_stack.pop();
// }
// // If this capture is for tracking local variables, then process the
// // local variable info.
// let mut reference_highlight = None;
// let mut definition_highlight = None;
// while match_.pattern_index < layer.config.highlights_pattern_index {
// // If the node represents a local scope, push a new local scope onto
// // the scope stack.
// if Some(capture.index) == layer.config.local_scope_capture_index {
// definition_highlight = None;
// let mut scope = LocalScope {
// inherits: true,
// range: range.clone(),
// local_defs: Vec::new(),
// };
// for prop in layer.config.query.property_settings(match_.pattern_index) {
// if let "local.scope-inherits" = prop.key.as_ref() {
// scope.inherits =
// prop.value.as_ref().map_or(true, |r| r.as_ref() == "true");
// }
// }
// layer.scope_stack.push(scope);
// }
// // If the node represents a definition, add a new definition to the
// // local scope at the top of the scope stack.
// else if Some(capture.index) == layer.config.local_def_capture_index {
// reference_highlight = None;
// let scope = layer.scope_stack.last_mut().unwrap();
// let mut value_range = 0..0;
// for capture in match_.captures {
// if Some(capture.index) == layer.config.local_def_value_capture_index {
// value_range = capture.node.byte_range();
// }
// }
// let name = byte_range_to_str(range.clone(), self.source);
// scope.local_defs.push(LocalDef {
// name,
// value_range,
// highlight: None,
// });
// definition_highlight = scope.local_defs.last_mut().map(|s| &mut s.highlight);
// }
// // If the node represents a reference, then try to find the corresponding
// // definition in the scope stack.
// else if Some(capture.index) == layer.config.local_ref_capture_index
// && definition_highlight.is_none()
// {
// definition_highlight = None;
// let name = byte_range_to_str(range.clone(), self.source);
// for scope in layer.scope_stack.iter().rev() {
// if let Some(highlight) = scope.local_defs.iter().rev().find_map(|def| {
// if def.name == name && range.start >= def.value_range.end {
// Some(def.highlight)
// } else {
// None
// }
// }) {
// reference_highlight = highlight;
// break;
// }
// if !scope.inherits {
// break;
// }
// }
// }
// // Continue processing any additional matches for the same node.
// if let Some((next_match, next_capture_index)) = captures.peek() {
// let next_capture = next_match.captures[*next_capture_index];
// if next_capture.node == capture.node {
// capture = next_capture;
// match_ = captures.next().unwrap().0;
// continue;
// }
// }
// self.sort_layers();
// continue 'main;
// }
// // Otherwise, this capture must represent a highlight.
// // If this exact range has already been highlighted by an earlier pattern, or by
// // a different layer, then skip over this one.
// if let Some((last_start, last_end, last_depth)) = self.last_highlight_range {
// if range.start == last_start && range.end == last_end && layer.depth < last_depth {
// self.sort_layers();
// continue 'main;
// }
// }
// // If the current node was found to be a local variable, then skip over any
// // highlighting patterns that are disabled for local variables.
// if definition_highlight.is_some() || reference_highlight.is_some() {
// while layer.config.non_local_variable_patterns[match_.pattern_index] {
// match_.remove();
// if let Some((next_match, next_capture_index)) = captures.peek() {
// let next_capture = next_match.captures[*next_capture_index];
// if next_capture.node == capture.node {
// capture = next_capture;
// match_ = captures.next().unwrap().0;
// continue;
// }
// }
// self.sort_layers();
// continue 'main;
// }
// }
// // Once a highlighting pattern is found for the current node, skip over
// // any later highlighting patterns that also match this node. Captures
// // for a given node are ordered by pattern index, so these subsequent
// // captures are guaranteed to be for highlighting, not injections or
// // local variables.
// while let Some((next_match, next_capture_index)) = captures.peek() {
// let next_capture = next_match.captures[*next_capture_index];
// if next_capture.node == capture.node {
// captures.next();
// } else {
// break;
// }
// }
// let current_highlight = layer.config.highlight_indices.load()[capture.index as usize];
// // If this node represents a local definition, then store the current
// // highlight value on the local scope entry representing this node.
// if let Some(definition_highlight) = definition_highlight {
// *definition_highlight = current_highlight;
// }
// // Emit a scope start event and push the node's end position to the stack.
// if let Some(highlight) = reference_highlight.or(current_highlight) {
// self.last_highlight_range = Some((range.start, range.end, layer.depth));
// layer.highlight_end_stack.push(range.end);
// return self
// .emit_event(range.start, Some(HighlightEvent::HighlightStart(highlight)));
// }
// self.sort_layers();
// }
// }
// }
// impl Syntax {
// /// Iterate over the highlighted regions for a given slice of source code.
// pub fn highlight_iter<'a>(
// &'a self,
// source: RopeSlice<'a>,
// range: Option<std::ops::Range<usize>>,
// cancellation_flag: Option<&'a AtomicUsize>,
// ) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a {
// let mut layers = self
// .layers
// .iter()
// .filter_map(|(_, layer)| {
// // TODO: if range doesn't overlap layer range, skip it
// // Reuse a cursor from the pool if available.
// let mut cursor = PARSER.with(|ts_parser| {
// let highlighter = &mut ts_parser.borrow_mut();
// highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
// });
// // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
// // prevents them from being moved. But both of these values are really just
// // pointers, so it's actually ok to move them.
// let cursor_ref =
// unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
// // if reusing cursors & no range this resets to whole range
// cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
// cursor_ref.set_match_limit(TREE_SITTER_MATCH_LIMIT);
// let mut captures = cursor_ref
// .captures(
// &layer.config.query,
// layer.tree().root_node(),
// RopeProvider(source),
// )
// .peekable();
// // If there's no captures, skip the layer
// captures.peek()?;
// Some(HighlightIterLayer {
// highlight_end_stack: Vec::new(),
// scope_stack: vec![LocalScope {
// inherits: false,
// range: 0..usize::MAX,
// local_defs: Vec::new(),
// }],
// cursor,
// _tree: None,
// captures: RefCell::new(captures),
// config: layer.config.as_ref(), // TODO: just reuse `layer`
// depth: layer.depth, // TODO: just reuse `layer`
// })
// })
// .collect::<Vec<_>>();
// layers.sort_unstable_by_key(|layer| layer.sort_key());
// let mut result = HighlightIter {
// source,
// byte_offset: range.map_or(0, |r| r.start),
// cancellation_flag,
// iter_count: 0,
// layers,
// next_event: None,
// last_highlight_range: None,
// };
// result.sort_layers();
// result
// }
// }

View File

@@ -0,0 +1,206 @@
use std::borrow::Cow;
use std::iter::{self, Peekable};
use std::mem::{replace, take};
use std::slice;
use hashbrown::HashMap;
use crate::query_iter::{MatchedNode, QueryIter, QueryIterEvent};
use crate::{Injection, LayerId, Range, Syntax};
/// Indicates which highlight should be applied to a region of source code.
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct Highlight(pub u32);
impl Highlight{
pub(crate) const NONE = Highlight(u32::MAX);
}
#[derive(Debug)]
struct LocalDef<'a> {
name: Cow<'a, str>,
value_range: Range,
highlight: Option<Highlight>,
}
#[derive(Debug)]
struct LocalScope<'a> {
inherits: bool,
range: Range,
local_defs: Vec<LocalDef<'a>>,
}
#[derive(Debug)]
struct HighlightedNode {
end: u32,
highlight: Highlight,
}
#[derive(Debug, Default)]
struct LayerData<'a> {
parent_highlights: usize,
dormant_highlights: Vec<HighlightedNode>,
scope_stack: Vec<LocalDef<'a>>,
}
struct HighlighterConfig<'a> {
new_precedance: bool,
highlight_indices: &'a [Highlight],
}
pub struct Highligther<'a> {
query: QueryIter<'a, LayerData<'a>>,
next_query_event: Option<QueryIterEvent<LayerData<'a>>>,
active_highlights: Vec<HighlightedNode>,
next_highlight_end: u32,
next_highlight_start: u32,
config: HighlighterConfig<'a>,
}
pub struct HighlightList<'a>(slice::Iter<'a, HighlightedNode>);
impl<'a> Iterator for HighlightList<'a> {
type Item = Highlight;
fn next(&mut self) -> Option<Highlight> {
self.0.next().map(|node| node.highlight)
}
}
pub enum HighlighEvent<'a> {
RefreshHiglights(HighlightList<'a>),
PushHighlights(HighlightList<'a>),
}
impl<'a> Highligther<'a> {
pub fn active_highlights(&self) -> HighlightList<'_> {
HighlightList(self.active_highlights.iter())
}
pub fn next_event_offset(&self) -> u32 {
self.next_highlight_start.min(self.next_highlight_end)
}
pub fn advance(&mut self) -> HighlighEvent<'_> {
let mut refresh = false;
let prev_stack_size = self.active_highlights.len();
let pos = self.next_event_offset();
if self.next_highlight_end == pos {
self.process_injection_ends();
self.process_higlight_end();
refresh = true;
}
let mut first_highlight = true;
while self.next_highlight_start == pos {
let Some(query_event) = self.adance_query_iter() else {
break;
};
match query_event {
QueryIterEvent::EnterInjection(_) => self.enter_injection(),
QueryIterEvent::Match(node) => self.start_highlight(node, &mut first_highlight),
QueryIterEvent::ExitInjection { injection, state } => {
// state is returned if the layer is finifhed, if it isn't we have
// a combined injection and need to deactive its highlights
if state.is_none() {
self.deactive_layer(injection.layer);
refresh = true;
}
}
}
}
self.next_highlight_end = self
.active_highlights
.last()
.map_or(u32::MAX, |node| node.end);
if refresh {
HighlighEvent::RefreshHiglights(HighlightList(self.active_highlights.iter()))
} else {
HighlighEvent::PushHighlights(HighlightList(
self.active_highlights[prev_stack_size..].iter(),
))
}
}
fn adance_query_iter(&mut self) -> Option<QueryIterEvent<LayerData<'a>>> {
let event = replace(&mut self.next_query_event, self.query.next());
self.next_highlight_start = self
.next_query_event
.as_ref()
.map_or(u32::MAX, |event| event.start());
event
}
fn process_higlight_end(&mut self) {
let i = self
.active_highlights
.iter()
.rposition(|highlight| highlight.end != self.next_highlight_end)
.unwrap();
self.active_highlights.truncate(i);
}
/// processes injections that end at the same position as highlights first.
fn process_injection_ends(&mut self) {
while self.next_highlight_end == self.next_highlight_start {
match self.next_query_event.as_ref() {
Some(QueryIterEvent::ExitInjection { injection, state }) => {
if state.is_none() {
self.deactive_layer(injection.layer);
}
}
Some(QueryIterEvent::Match(matched_node)) if matched_node.byte_range.is_empty() => {
}
_ => break,
}
}
}
fn enter_injection(&mut self) {
self.query.current_layer_state().parent_highlights = self.active_highlights.len();
}
fn deactive_layer(&mut self, layer: LayerId) {
let LayerData {
parent_highlights,
ref mut dormant_highlights,
..
} = *self.query.layer_state(layer);
let i = self.active_highlights[parent_highlights..]
.iter()
.rposition(|highlight| highlight.end != self.next_highlight_end)
.unwrap();
self.active_highlights.truncate(parent_highlights + i);
dormant_highlights.extend(self.active_highlights.drain(parent_highlights..))
}
fn start_highlight(&mut self, node: MatchedNode, first_highlight: &mut bool) {
if node.byte_range.is_empty() {
return;
}
// if there are multiple matches for the exact same node
// only use one of the (the last with new/nvim precedance)
if !*first_highlight
&& self.active_highlights.last().map_or(false, |prev_node| {
prev_node.end == node.byte_range.end as u32
})
{
if self.config.new_precedance {
self.active_highlights.pop();
} else {
return;
}
}
let highlight = self.config.highlight_indices[node.capture.idx()];
if highlight.0 == u32::MAX {
return;
}
self.active_highlights.push(HighlightedNode {
end: node.byte_range.end as u32,
highlight,
});
*first_highlight = false;
}
}

250
helix-syntax/src/lib.rs Normal file
View File

@@ -0,0 +1,250 @@
use ::ropey::RopeSlice;
use slotmap::{new_key_type, HopSlotMap};
use std::borrow::Cow;
use std::hash::{Hash, Hasher};
use std::path::Path;
use std::str;
use std::sync::Arc;
use crate::parse::LayerUpdateFlags;
pub use crate::config::{read_query, HighlightConfiguration};
use crate::tree_sitter::{SyntaxTree, SyntaxTreeNode};
pub use pretty_print::pretty_print_tree;
pub use tree_cursor::TreeCursor;
mod config;
pub mod highlighter;
pub mod highlighter2;
mod parse;
mod pretty_print;
mod query_iter;
pub mod text_object;
mod tree_cursor;
pub mod tree_sitter;
new_key_type! {
/// The default slot map key type.
pub struct LayerId;
}
/// The maximum number of in-progress matches a TS cursor can consider at once.
/// This is set to a constant in order to avoid performance problems for medium to large files. Set with `set_match_limit`.
/// Using such a limit means that we lose valid captures, so there is fundamentally a tradeoff here.
///
///
/// Old tree sitter versions used a limit of 32 by default until this limit was removed in version `0.19.5` (must now be set manually).
/// However, this causes performance issues for medium to large files.
/// In helix, this problem caused treesitter motions to take multiple seconds to complete in medium-sized rust files (3k loc).
///
///
/// Neovim also encountered this problem and reintroduced this limit after it was removed upstream
/// (see <https://github.com/neovim/neovim/issues/14897> and <https://github.com/neovim/neovim/pull/14915>).
/// The number used here is fundamentally a tradeoff between breaking some obscure edge cases and performance.
///
///
/// Neovim chose 64 for this value somewhat arbitrarily (<https://github.com/neovim/neovim/pull/18397>).
/// 64 is too low for some languages though. In particular, it breaks some highlighting for record fields in Erlang record definitions.
/// This number can be increased if new syntax highlight breakages are found, as long as the performance penalty is not too high.
pub const TREE_SITTER_MATCH_LIMIT: u32 = 256;
// TODO(perf): replace std::ops::Range<usize> with helix_stdx::Range<u32> once added
type Range = std::ops::Range<usize>;
/// The Tree siitter syntax tree for a single language.
/// This is really multipe nested different syntax trees due to tree sitter
/// injections. A single syntax tree/parser is called layer. Each layer
/// is parsed as a single "file" by tree sitter. There can be multiple layers
/// for the same language. A layer corresponds to one of three things:
/// * the root layer
/// * a singular injection limited to a single node in it's parent layer
/// * Multiple injections (multiple disjoint nodes in parent layer) that are
/// parsed as tough they are a single uninterrupted file.
///
/// An injection always refer to a single node into which another layer is
/// injected. As injections only correspond to syntax tree nodes injections in
/// the same layer do not intersect. However, the syntax tree in a an injected
/// layer can have nodes that intersect with nodes from the parent layer. For
/// example:
/// ```
/// layer2: | Sibling A | Sibling B (layer3) | Sibling C |
/// layer1: | Sibling A (layer2) | Sibling B | Sibling C (layer2) |
/// ````
/// In this case Sibling B really spans across a "GAP" in layer2. While the syntax
/// node can not be split up by tree sitter directly, we can treat Sibling B as two
/// seperate injections. That is done while parsing/running the query capture. As
/// a result the injections from a tree. Note that such other queries must account for
/// such multi injection nodes.
#[derive(Debug)]
pub struct Syntax {
layers: HopSlotMap<LayerId, LanguageLayer>,
root: LayerId,
}
impl Syntax {
pub fn new(
source: RopeSlice,
config: Arc<HighlightConfiguration>,
injection_callback: impl Fn(&InjectionLanguageMarker) -> Option<Arc<HighlightConfiguration>>,
) -> Option<Self> {
let root_layer = LanguageLayer {
parse_tree: None,
config,
flags: LayerUpdateFlags::empty(),
ranges: vec![tree_sitter::Range {
start_byte: 0,
end_byte: u32::MAX,
start_point: tree_sitter::Point { row: 0, col: 0 },
end_point: tree_sitter::Point {
row: u32::MAX,
col: u32::MAX,
},
}]
.into_boxed_slice(),
injections: Box::new([]),
parent: None,
};
// track scope_descriptor: a Vec of scopes for item in tree
let mut layers = HopSlotMap::default();
let root = layers.insert(root_layer);
let mut syntax = Self { root, layers };
let res = syntax.update(source, Vec::new(), injection_callback);
if res.is_err() {
log::error!("TS parser failed, disabling TS for the current buffer: {res:?}");
return None;
}
Some(syntax)
}
pub fn tree(&self) -> &SyntaxTree {
self.layers[self.root].tree()
}
pub fn tree_for_byte_range(&self, start: usize, end: usize) -> &SyntaxTree {
let layer = self.layer_for_byte_range(start, end);
self.layers[layer].tree()
}
pub fn named_descendant_for_byte_range(
&self,
start: usize,
end: usize,
) -> Option<SyntaxTreeNode<'_>> {
self.tree_for_byte_range(start, end)
.root_node()
.named_descendant_for_byte_range(start, end)
}
pub fn descendant_for_byte_range(
&self,
start: usize,
end: usize,
) -> Option<SyntaxTreeNode<'_>> {
self.tree_for_byte_range(start, end)
.root_node()
.descendant_for_byte_range(start, end)
}
pub fn layer_for_byte_range(&self, start: usize, end: usize) -> LayerId {
let mut cursor = self.root;
loop {
let layer = &self.layers[cursor];
let Some(start_injection) = layer.injection_at_byte_idx(start) else {
break;
};
let Some(end_injection) = layer.injection_at_byte_idx(end) else {
break;
};
if start_injection.layer == end_injection.layer {
cursor = start_injection.layer;
} else {
break;
}
}
cursor
}
pub fn walk(&self) -> TreeCursor<'_> {
TreeCursor::new(&self.layers, self.root)
}
}
#[derive(Debug, Clone)]
pub(crate) struct Injection {
pub byte_range: Range,
pub layer: LayerId,
}
#[derive(Debug)]
pub struct LanguageLayer {
pub config: Arc<HighlightConfiguration>,
parse_tree: Option<SyntaxTree>,
ranges: Box<[tree_sitter::Range]>,
/// a list of **sorted** non-overlapping injection ranges. Note that
/// injection ranges are not relative to the start of this layer but the
/// start of the root layer
injections: Box<[Injection]>,
/// internal flags used during parsing to track incremental invalidation
flags: LayerUpdateFlags,
parent: Option<LayerId>,
}
/// This PartialEq implementation only checks if that
/// two layers are theoretically identical (meaning they highlight the same text range with the same language).
/// It does not check whether the layers have the same internal treesitter
/// state.
impl PartialEq for LanguageLayer {
fn eq(&self, other: &Self) -> bool {
self.parent == other.parent
&& self.config.grammar == other.config.grammar
&& self.ranges == other.ranges
}
}
/// Hash implementation belongs to PartialEq implementation above.
/// See its documentation for details.
impl Hash for LanguageLayer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.parent.hash(state);
self.config.grammar.hash(state);
self.ranges.hash(state);
}
}
impl LanguageLayer {
pub fn tree(&self) -> &SyntaxTree {
// TODO: no unwrap
self.parse_tree.as_ref().unwrap()
}
/// Returns the injection range **within this layers** that contains `idx`.
/// This function will not descend into nested injections
pub(crate) fn injection_at_byte_idx(&self, idx: usize) -> Option<&Injection> {
let i = self
.injections
.partition_point(|range| range.byte_range.start < idx);
self.injections
.get(i)
.filter(|injection| injection.byte_range.end > idx)
}
}
/// Represents the reason why syntax highlighting failed.
#[derive(Debug, PartialEq, Eq)]
pub enum Error {
Cancelled,
InvalidLanguage,
InvalidRanges,
Unknown,
}
fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> {
Cow::from(source.byte_slice(range))
}

135
helix-syntax/src/merge.rs Normal file
View File

@@ -0,0 +1,135 @@
use crate::highlighter::{Highlight, HighlightEvent};
pub struct Merge<I> {
iter: I,
spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,
next_event: Option<HighlightEvent>,
next_span: Option<(usize, std::ops::Range<usize>)>,
queue: Vec<HighlightEvent>,
}
/// Merge a list of spans into the highlight event stream.
pub fn merge<I: Iterator<Item = HighlightEvent>>(
iter: I,
spans: Vec<(usize, std::ops::Range<usize>)>,
) -> Merge<I> {
let spans = Box::new(spans.into_iter());
let mut merge = Merge {
iter,
spans,
next_event: None,
next_span: None,
queue: Vec::new(),
};
merge.next_event = merge.iter.next();
merge.next_span = merge.spans.next();
merge
}
impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
type Item = HighlightEvent;
fn next(&mut self) -> Option<Self::Item> {
use HighlightEvent::*;
if let Some(event) = self.queue.pop() {
return Some(event);
}
loop {
match (self.next_event, &self.next_span) {
// this happens when range is partially or fully offscreen
(Some(Source { start, .. }), Some((span, range))) if start > range.start => {
if start > range.end {
self.next_span = self.spans.next();
} else {
self.next_span = Some((*span, start..range.end));
};
}
_ => break,
}
}
match (self.next_event, &self.next_span) {
(Some(HighlightStart(i)), _) => {
self.next_event = self.iter.next();
Some(HighlightStart(i))
}
(Some(HighlightEnd), _) => {
self.next_event = self.iter.next();
Some(HighlightEnd)
}
(Some(Source { start, end }), Some((_, range))) if start < range.start => {
let intersect = range.start.min(end);
let event = Source {
start,
end: intersect,
};
if end == intersect {
// the event is complete
self.next_event = self.iter.next();
} else {
// subslice the event
self.next_event = Some(Source {
start: intersect,
end,
});
};
Some(event)
}
(Some(Source { start, end }), Some((span, range))) if start == range.start => {
let intersect = range.end.min(end);
let event = HighlightStart(Highlight(*span));
// enqueue in reverse order
self.queue.push(HighlightEnd);
self.queue.push(Source {
start,
end: intersect,
});
if end == intersect {
// the event is complete
self.next_event = self.iter.next();
} else {
// subslice the event
self.next_event = Some(Source {
start: intersect,
end,
});
};
if intersect == range.end {
self.next_span = self.spans.next();
} else {
self.next_span = Some((*span, intersect..range.end));
}
Some(event)
}
(Some(event), None) => {
self.next_event = self.iter.next();
Some(event)
}
// Can happen if cursor at EOF and/or diagnostic reaches past the end.
// We need to actually emit events for the cursor-at-EOF situation,
// even though the range is past the end of the text. This needs to be
// handled appropriately by the drawing code by not assuming that
// all `Source` events point to valid indices in the rope.
(None, Some((span, range))) => {
let event = HighlightStart(Highlight(*span));
self.queue.push(HighlightEnd);
self.queue.push(Source {
start: range.start,
end: range.end,
});
self.next_span = self.spans.next();
Some(event)
}
(None, None) => None,
e => unreachable!("{:?}", e),
}
}
}

429
helix-syntax/src/parse.rs Normal file
View File

@@ -0,0 +1,429 @@
// use std::collections::VecDeque;
// use std::mem::replace;
// use std::sync::Arc;
// use ahash::RandomState;
use bitflags::bitflags;
// use hashbrown::raw::RawTable;
// use ropey::RopeSlice;
// use tree_sitter::{Node, Parser, Point, QueryCursor, Range};
// use crate::ropey::RopeProvider;
// use crate::{
// Error, HighlightConfiguration, IncludedChildren, InjectionLanguageMarker, LanguageLayer,
// Syntax, PARSER, TREE_SITTER_MATCH_LIMIT,
// };
bitflags! {
/// Flags that track the status of a layer
/// in the `Sytaxn::update` function
#[derive(Debug)]
pub(crate) struct LayerUpdateFlags : u32{
const MODIFIED = 0b001;
const MOVED = 0b010;
const TOUCHED = 0b100;
}
}
// impl Syntax {
// pub fn update(
// &mut self,
// source: RopeSlice,
// edits: Vec<tree_sitter::InputEdit>,
// injection_callback: impl Fn(&InjectionLanguageMarker) -> Option<Arc<HighlightConfiguration>>,
// ) -> Result<(), Error> {
// let mut queue = VecDeque::new();
// queue.push_back(self.root);
// // This table allows inverse indexing of `layers`.
// // That is by hashing a `Layer` you can find
// // the `LayerId` of an existing equivalent `Layer` in `layers`.
// //
// // It is used to determine if a new layer exists for an injection
// // or if an existing layer needs to be updated.
// let mut layers_table = RawTable::with_capacity(self.layers.len());
// let layers_hasher = RandomState::new();
// // Use the edits to update all layers markers
// fn point_add(a: Point, b: Point) -> Point {
// if b.row > 0 {
// Point::new(a.row.saturating_add(b.row), b.column)
// } else {
// Point::new(0, a.column.saturating_add(b.column))
// }
// }
// fn point_sub(a: Point, b: Point) -> Point {
// if a.row > b.row {
// Point::new(a.row.saturating_sub(b.row), a.column)
// } else {
// Point::new(0, a.column.saturating_sub(b.column))
// }
// }
// for (layer_id, layer) in self.layers.iter_mut() {
// // The root layer always covers the whole range (0..usize::MAX)
// if layer.depth == 0 {
// layer.flags = LayerUpdateFlags::MODIFIED;
// continue;
// }
// if !edits.is_empty() {
// for range in &mut layer.ranges {
// // Roughly based on https://github.com/tree-sitter/tree-sitter/blob/ddeaa0c7f534268b35b4f6cb39b52df082754413/lib/src/subtree.c#L691-L720
// for edit in edits.iter().rev() {
// let is_pure_insertion = edit.old_end_byte == edit.start_byte;
// // if edit is after range, skip
// if edit.start_byte > range.end_byte {
// // TODO: || (is_noop && edit.start_byte == range.end_byte)
// continue;
// }
// // if edit is before range, shift entire range by len
// if edit.old_end_byte < range.start_byte {
// range.start_byte =
// edit.new_end_byte + (range.start_byte - edit.old_end_byte);
// range.start_point = point_add(
// edit.new_end_position,
// point_sub(range.start_point, edit.old_end_position),
// );
// range.end_byte = edit
// .new_end_byte
// .saturating_add(range.end_byte - edit.old_end_byte);
// range.end_point = point_add(
// edit.new_end_position,
// point_sub(range.end_point, edit.old_end_position),
// );
// layer.flags |= LayerUpdateFlags::MOVED;
// }
// // if the edit starts in the space before and extends into the range
// else if edit.start_byte < range.start_byte {
// range.start_byte = edit.new_end_byte;
// range.start_point = edit.new_end_position;
// range.end_byte = range
// .end_byte
// .saturating_sub(edit.old_end_byte)
// .saturating_add(edit.new_end_byte);
// range.end_point = point_add(
// edit.new_end_position,
// point_sub(range.end_point, edit.old_end_position),
// );
// layer.flags = LayerUpdateFlags::MODIFIED;
// }
// // If the edit is an insertion at the start of the tree, shift
// else if edit.start_byte == range.start_byte && is_pure_insertion {
// range.start_byte = edit.new_end_byte;
// range.start_point = edit.new_end_position;
// layer.flags |= LayerUpdateFlags::MOVED;
// } else {
// range.end_byte = range
// .end_byte
// .saturating_sub(edit.old_end_byte)
// .saturating_add(edit.new_end_byte);
// range.end_point = point_add(
// edit.new_end_position,
// point_sub(range.end_point, edit.old_end_position),
// );
// layer.flags = LayerUpdateFlags::MODIFIED;
// }
// }
// }
// }
// let hash = layers_hasher.hash_one(layer);
// // Safety: insert_no_grow is unsafe because it assumes that the table
// // has enough capacity to hold additional elements.
// // This is always the case as we reserved enough capacity above.
// unsafe { layers_table.insert_no_grow(hash, layer_id) };
// }
// PARSER.with(|ts_parser| {
// let ts_parser = &mut ts_parser.borrow_mut();
// ts_parser.parser.set_timeout_micros(1000 * 500); // half a second is pretty generours
// let mut cursor = ts_parser.cursors.pop().unwrap_or_else(QueryCursor::new);
// // TODO: might need to set cursor range
// cursor.set_byte_range(0..usize::MAX);
// cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
// let source_slice = source.slice(..);
// while let Some(layer_id) = queue.pop_front() {
// let layer = &mut self.layers[layer_id];
// // Mark the layer as touched
// layer.flags |= LayerUpdateFlags::TOUCHED;
// // If a tree already exists, notify it of changes.
// if let Some(tree) = &mut layer.parse_tree {
// if layer
// .flags
// .intersects(LayerUpdateFlags::MODIFIED | LayerUpdateFlags::MOVED)
// {
// for edit in edits.iter().rev() {
// // Apply the edits in reverse.
// // If we applied them in order then edit 1 would disrupt the positioning of edit 2.
// tree.edit(edit);
// }
// }
// if layer.flags.contains(LayerUpdateFlags::MODIFIED) {
// // Re-parse the tree.
// layer.parse(&mut ts_parser.parser, source)?;
// }
// } else {
// // always parse if this layer has never been parsed before
// layer.parse(&mut ts_parser.parser, source)?;
// }
// // Switch to an immutable borrow.
// let layer = &self.layers[layer_id];
// // Process injections.
// let matches = cursor.matches(
// &layer.config.injections_query,
// layer.tree().root_node(),
// RopeProvider(source_slice),
// );
// let mut combined_injections = vec![
// (None, Vec::new(), IncludedChildren::default());
// layer.config.combined_injections_patterns.len()
// ];
// let mut injections = Vec::new();
// let mut last_injection_end = 0;
// for mat in matches {
// let (injection_capture, content_node, included_children) = layer
// .config
// .injection_for_match(&layer.config.injections_query, &mat, source_slice);
// // in case this is a combined injection save it for more processing later
// if let Some(combined_injection_idx) = layer
// .config
// .combined_injections_patterns
// .iter()
// .position(|&pattern| pattern == mat.pattern_index)
// {
// let entry = &mut combined_injections[combined_injection_idx];
// if injection_capture.is_some() {
// entry.0 = injection_capture;
// }
// if let Some(content_node) = content_node {
// if content_node.start_byte() >= last_injection_end {
// entry.1.push(content_node);
// last_injection_end = content_node.end_byte();
// }
// }
// entry.2 = included_children;
// continue;
// }
// // Explicitly remove this match so that none of its other captures will remain
// // in the stream of captures.
// mat.remove();
// // If a language is found with the given name, then add a new language layer
// // to the highlighted document.
// if let (Some(injection_capture), Some(content_node)) =
// (injection_capture, content_node)
// {
// if let Some(config) = (injection_callback)(&injection_capture) {
// let ranges =
// intersect_ranges(&layer.ranges, &[content_node], included_children);
// if !ranges.is_empty() {
// if content_node.start_byte() < last_injection_end {
// continue;
// }
// last_injection_end = content_node.end_byte();
// injections.push((config, ranges));
// }
// }
// }
// }
// for (lang_name, content_nodes, included_children) in combined_injections {
// if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
// if let Some(config) = (injection_callback)(&lang_name) {
// let ranges =
// intersect_ranges(&layer.ranges, &content_nodes, included_children);
// if !ranges.is_empty() {
// injections.push((config, ranges));
// }
// }
// }
// }
// let depth = layer.depth + 1;
// // TODO: can't inline this since matches borrows self.layers
// for (config, ranges) in injections {
// let parent = Some(layer_id);
// let new_layer = LanguageLayer {
// parse_tree: None,
// config,
// depth,
// ranges,
// flags: LayerUpdateFlags::empty(),
// parent: None,
// };
// // Find an identical existing layer
// let layer = layers_table
// .get(layers_hasher.hash_one(&new_layer), |&it| {
// self.layers[it] == new_layer
// })
// .copied();
// // ...or insert a new one.
// let layer_id = layer.unwrap_or_else(|| self.layers.insert(new_layer));
// self.layers[layer_id].parent = parent;
// queue.push_back(layer_id);
// }
// // TODO: pre-process local scopes at this time, rather than highlight?
// // would solve problems with locals not working across boundaries
// }
// // Return the cursor back in the pool.
// ts_parser.cursors.push(cursor);
// // Reset all `LayerUpdateFlags` and remove all untouched layers
// self.layers.retain(|_, layer| {
// replace(&mut layer.flags, LayerUpdateFlags::empty())
// .contains(LayerUpdateFlags::TOUCHED)
// });
// Ok(())
// })
// }
// }
// /// Compute the ranges that should be included when parsing an injection.
// /// This takes into account three things:
// /// * `parent_ranges` - The ranges must all fall within the *current* layer's ranges.
// /// * `nodes` - Every injection takes place within a set of nodes. The injection ranges
// /// are the ranges of those nodes.
// /// * `includes_children` - For some injections, the content nodes' children should be
// /// excluded from the nested document, so that only the content nodes' *own* content
// /// is reparsed. For other injections, the content nodes' entire ranges should be
// /// reparsed, including the ranges of their children.
// fn intersect_ranges(
// parent_ranges: &[Range],
// nodes: &[Node],
// included_children: IncludedChildren,
// ) -> Vec<Range> {
// let mut cursor = nodes[0].walk();
// let mut result = Vec::new();
// let mut parent_range_iter = parent_ranges.iter();
// let mut parent_range = parent_range_iter
// .next()
// .expect("Layers should only be constructed with non-empty ranges vectors");
// for node in nodes.iter() {
// let mut preceding_range = Range {
// start_byte: 0,
// start_point: Point::new(0, 0),
// end_byte: node.start_byte(),
// end_point: node.start_position(),
// };
// let following_range = Range {
// start_byte: node.end_byte(),
// start_point: node.end_position(),
// end_byte: usize::MAX,
// end_point: Point::new(usize::MAX, usize::MAX),
// };
// for excluded_range in node
// .children(&mut cursor)
// .filter_map(|child| match included_children {
// IncludedChildren::None => Some(child.range()),
// IncludedChildren::All => None,
// IncludedChildren::Unnamed => {
// if child.is_named() {
// Some(child.range())
// } else {
// None
// }
// }
// })
// .chain([following_range].iter().cloned())
// {
// let mut range = Range {
// start_byte: preceding_range.end_byte,
// start_point: preceding_range.end_point,
// end_byte: excluded_range.start_byte,
// end_point: excluded_range.start_point,
// };
// preceding_range = excluded_range;
// if range.end_byte < parent_range.start_byte {
// continue;
// }
// while parent_range.start_byte <= range.end_byte {
// if parent_range.end_byte > range.start_byte {
// if range.start_byte < parent_range.start_byte {
// range.start_byte = parent_range.start_byte;
// range.start_point = parent_range.start_point;
// }
// if parent_range.end_byte < range.end_byte {
// if range.start_byte < parent_range.end_byte {
// result.push(Range {
// start_byte: range.start_byte,
// start_point: range.start_point,
// end_byte: parent_range.end_byte,
// end_point: parent_range.end_point,
// });
// }
// range.start_byte = parent_range.end_byte;
// range.start_point = parent_range.end_point;
// } else {
// if range.start_byte < range.end_byte {
// result.push(range);
// }
// break;
// }
// }
// if let Some(next_range) = parent_range_iter.next() {
// parent_range = next_range;
// } else {
// return result;
// }
// }
// }
// }
// result
// }
// impl LanguageLayer {
// fn parse(&mut self, parser: &mut Parser, source: RopeSlice) -> Result<(), Error> {
// parser
// .set_included_ranges(&self.ranges)
// .map_err(|_| Error::InvalidRanges)?;
// parser
// .set_language(&self.config.language)
// .map_err(|_| Error::InvalidLanguage)?;
// // unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
// let tree = parser
// .parse_with(
// &mut |byte, _| {
// if byte <= source.len_bytes() {
// let (chunk, start_byte, _, _) = source.chunk_at_byte(byte);
// &chunk.as_bytes()[byte - start_byte..]
// } else {
// // out of range
// &[]
// }
// },
// self.parse_tree.as_ref(),
// )
// .ok_or(Error::Cancelled)?;
// // unsafe { ts_parser.parser.set_cancellation_flag(None) };
// self.parse_tree = Some(tree);
// Ok(())
// }
// }

View File

@@ -0,0 +1,65 @@
use std::fmt;
use tree_sitter::{Node, TreeCursor};
pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result {
if node.child_count() == 0 {
if node_is_visible(&node) {
write!(fmt, "({})", node.kind())
} else {
write!(fmt, "\"{}\"", node.kind())
}
} else {
pretty_print_tree_impl(fmt, &mut node.walk(), 0)
}
}
fn pretty_print_tree_impl<W: fmt::Write>(
fmt: &mut W,
cursor: &mut TreeCursor,
depth: usize,
) -> fmt::Result {
let node = cursor.node();
let visible = node_is_visible(&node);
if visible {
let indentation_columns = depth * 2;
write!(fmt, "{:indentation_columns$}", "")?;
if let Some(field_name) = cursor.field_name() {
write!(fmt, "{}: ", field_name)?;
}
write!(fmt, "({}", node.kind())?;
}
// Handle children.
if cursor.goto_first_child() {
loop {
if node_is_visible(&cursor.node()) {
fmt.write_char('\n')?;
}
pretty_print_tree_impl(fmt, cursor, depth + 1)?;
if !cursor.goto_next_sibling() {
break;
}
}
let moved = cursor.goto_parent();
// The parent of the first child must exist, and must be `node`.
debug_assert!(moved);
debug_assert!(cursor.node() == node);
}
if visible {
fmt.write_char(')')?;
}
Ok(())
}
fn node_is_visible(node: &Node) -> bool {
node.is_missing() || (node.is_named() && node.language().node_kind_is_visible(node.kind_id()))
}

View File

@@ -0,0 +1,236 @@
use core::slice;
use std::iter::Peekable;
use std::mem::replace;
use hashbrown::HashMap;
use ropey::RopeSlice;
use crate::tree_sitter::{
Capture, InactiveQueryCursor, Query, QueryCursor, RopeTsInput, SyntaxTreeNode,
};
use crate::{Injection, LayerId, Range, Syntax};
#[derive(Clone)]
pub struct MatchedNode {
pub capture: Capture,
pub byte_range: Range,
}
struct LayerQueryIter<'a> {
cursor: QueryCursor<'a, 'a, RopeTsInput<'a>>,
peeked: Option<MatchedNode>,
}
impl<'a> LayerQueryIter<'a> {
fn peek(&mut self) -> Option<&MatchedNode> {
if self.peeked.is_none() {
let (query_match, node_idx) = self.cursor.next_matched_node()?;
let matched_node = query_match.matched_node(node_idx);
self.peeked = Some(MatchedNode {
capture: matched_node.capture,
byte_range: matched_node.syntax_node.byte_range(),
});
}
self.peeked.as_ref()
}
fn consume(&mut self) -> MatchedNode {
self.peeked.take().unwrap()
}
}
struct ActiveLayer<'a, S> {
state: S,
query_iter: LayerQueryIter<'a>,
injections: Peekable<slice::Iter<'a, Injection>>,
}
// data only needed when entering and exiting injections
// seperate struck to keep the QueryIter reasonably small
struct QueryIterLayerManager<'a, S> {
query: &'a Query,
node: SyntaxTreeNode<'a>,
src: RopeSlice<'a>,
syntax: &'a Syntax,
active_layers: HashMap<LayerId, Box<ActiveLayer<'a, S>>>,
active_injections: Vec<Injection>,
}
impl<'a, S: Default> QueryIterLayerManager<'a, S> {
fn init_layer(&mut self, injection: Injection) -> Box<ActiveLayer<'a, S>> {
self.active_layers
.remove(&injection.layer)
.unwrap_or_else(|| {
let layer = &self.syntax.layers[injection.layer];
let injection_start = layer
.injections
.partition_point(|child| child.byte_range.start < injection.byte_range.start);
let cursor = InactiveQueryCursor::new().execute_query(
self.query,
&self.node,
RopeTsInput::new(self.src),
);
Box::new(ActiveLayer {
state: S::default(),
query_iter: LayerQueryIter {
cursor,
peeked: None,
},
injections: layer.injections[injection_start..].iter().peekable(),
})
})
}
}
pub struct QueryIter<'a, LayerState: Default = ()> {
layer_manager: Box<QueryIterLayerManager<'a, LayerState>>,
current_layer: Box<ActiveLayer<'a, LayerState>>,
current_injection: Injection,
}
impl<'a, LayerState: Default> QueryIter<'a, LayerState> {
pub fn new(syntax: &'a Syntax, src: RopeSlice<'a>, query: &'a Query) -> Self {
Self::at(syntax, src, query, syntax.tree().root_node(), syntax.root)
}
pub fn at(
syntax: &'a Syntax,
src: RopeSlice<'a>,
query: &'a Query,
node: SyntaxTreeNode<'a>,
layer: LayerId,
) -> Self {
// create fake injection for query root
let injection = Injection {
byte_range: node.byte_range(),
layer,
};
let mut layer_manager = Box::new(QueryIterLayerManager {
query,
node,
src,
syntax,
// TODO: reuse allocations with an allocation pool
active_layers: HashMap::with_capacity(8),
active_injections: Vec::with_capacity(8),
});
Self {
current_layer: layer_manager.init_layer(injection),
current_injection: injection,
layer_manager,
}
}
pub fn current_layer_state(&mut self) -> &mut LayerState {
&mut self.current_layer.state
}
pub fn layer_state(&mut self, layer: LayerId) -> &mut LayerState {
if layer == self.current_injection.layer {
self.current_layer_state()
} else {
&mut self
.layer_manager
.active_layers
.get_mut(&layer)
.unwrap()
.state
}
}
fn enter_injection(&mut self, injection: Injection) {
let active_layer = self.layer_manager.init_layer(injection);
let old_injection = replace(&mut self.current_injection, injection);
let old_layer = replace(&mut self.current_layer, active_layer);
self.layer_manager
.active_layers
.insert(old_injection.layer, old_layer);
self.layer_manager.active_injections.push(old_injection);
}
fn exit_injection(&mut self) -> Option<(Injection, Option<LayerState>)> {
let injection = replace(
&mut self.current_injection,
self.layer_manager.active_injections.pop()?,
);
let layer = replace(
&mut self.current_layer,
self.layer_manager
.active_layers
.remove(&self.current_injection.layer)?,
);
let layer_unfinished = layer.query_iter.peeked.is_some();
if layer_unfinished {
self.layer_manager
.active_layers
.insert(injection.layer, layer)
.unwrap();
Some((injection, None))
} else {
Some((injection, Some(layer.state)))
}
}
}
impl<'a, S: Default> Iterator for QueryIter<'a, S> {
type Item = QueryIterEvent<S>;
fn next(&mut self) -> Option<QueryIterEvent<S>> {
loop {
let next_injection = self.current_layer.injections.peek().filter(|injection| {
injection.byte_range.start < self.current_injection.byte_range.end
});
let next_match = self.current_layer.query_iter.peek().filter(|matched_node| {
matched_node.byte_range.start < self.current_injection.byte_range.end
});
match (next_match, next_injection) {
(None, None) => {
return self.exit_injection().map(|(injection, state)| {
QueryIterEvent::ExitInjection { injection, state }
});
}
(Some(_), None) => {
// consume match
let matched_node = self.current_layer.query_iter.consume();
return Some(QueryIterEvent::Match(matched_node));
}
(Some(matched_node), Some(injection))
if matched_node.byte_range.start <= injection.byte_range.end =>
{
// consume match
let matched_node = self.current_layer.query_iter.consume();
// ignore nodes that are overlapped by the injection
if matched_node.byte_range.start <= injection.byte_range.start {
return Some(QueryIterEvent::Match(matched_node));
}
}
(Some(_), Some(_)) | (None, Some(_)) => {
// consume injection
let injection = self.current_layer.injections.next().unwrap();
self.enter_injection(injection.clone());
return Some(QueryIterEvent::EnterInjection(injection.clone()));
}
}
}
}
}
pub enum QueryIterEvent<State = ()> {
EnterInjection(Injection),
Match(MatchedNode),
ExitInjection {
injection: Injection,
state: Option<State>,
},
}
impl<S> QueryIterEvent<S> {
pub fn start(&self) -> u32 {
match self {
QueryIterEvent::EnterInjection(injection) => injection.byte_range.start as u32,
QueryIterEvent::Match(mat) => mat.byte_range.start as u32,
QueryIterEvent::ExitInjection { injection, .. } => injection.byte_range.start as u32,
}
}
}

View File

@@ -0,0 +1 @@

View File

@@ -0,0 +1,93 @@
// TODO: rework using query iter
use std::iter;
use ropey::RopeSlice;
use crate::tree_sitter::{InactiveQueryCursor, Query, RopeTsInput, SyntaxTreeNode};
use crate::TREE_SITTER_MATCH_LIMIT;
#[derive(Debug)]
pub enum CapturedNode<'a> {
Single(SyntaxTreeNode<'a>),
/// Guaranteed to be not empty
Grouped(Vec<SyntaxTreeNode<'a>>),
}
impl<'a> CapturedNode<'a> {
pub fn start_byte(&self) -> usize {
match self {
Self::Single(n) => n.start_byte(),
Self::Grouped(ns) => ns[0].start_byte(),
}
}
pub fn end_byte(&self) -> usize {
match self {
Self::Single(n) => n.end_byte(),
Self::Grouped(ns) => ns.last().unwrap().end_byte(),
}
}
}
#[derive(Debug)]
pub struct TextObjectQuery {
pub query: Query,
}
impl TextObjectQuery {
/// Run the query on the given node and return sub nodes which match given
/// capture ("function.inside", "class.around", etc).
///
/// Captures may contain multiple nodes by using quantifiers (+, *, etc),
/// and support for this is partial and could use improvement.
///
/// ```query
/// (comment)+ @capture
///
/// ; OR
/// (
/// (comment)*
/// .
/// (function)
/// ) @capture
/// ```
pub fn capture_nodes<'a>(
&'a self,
capture_name: &str,
node: SyntaxTreeNode<'a>,
slice: RopeSlice<'a>,
cursor: InactiveQueryCursor,
) -> Option<impl Iterator<Item = CapturedNode<'a>>> {
self.capture_nodes_any(&[capture_name], node, slice, cursor)
}
/// Find the first capture that exists out of all given `capture_names`
/// and return sub nodes that match this capture.
pub fn capture_nodes_any<'a>(
&'a self,
capture_names: &[&str],
node: SyntaxTreeNode<'a>,
slice: RopeSlice<'a>,
mut cursor: InactiveQueryCursor,
) -> Option<impl Iterator<Item = CapturedNode<'a>>> {
let capture = capture_names
.iter()
.find_map(|cap| self.query.get_capture(cap))?;
cursor.set_match_limit(TREE_SITTER_MATCH_LIMIT);
let mut cursor = cursor.execute_query(&self.query, &node, RopeTsInput::new(slice));
let capture_node = iter::from_fn(move || {
let (mat, _) = cursor.next_matched_node()?;
Some(mat.nodes_for_capture(capture).cloned().collect())
})
.filter_map(move |nodes: Vec<_>| {
if nodes.len() > 1 {
Some(CapturedNode::Grouped(nodes))
} else {
nodes.into_iter().map(CapturedNode::Single).next()
}
});
Some(capture_node)
}
}

View File

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

View File

@@ -0,0 +1,45 @@
mod grammar;
mod parser;
pub mod query;
mod query_cursor;
mod query_match;
mod ropey;
mod syntax_tree;
mod syntax_tree_node;
use std::ops;
pub use grammar::Grammar;
pub use parser::{Parser, ParserInputRaw};
pub use query::{Capture, Pattern, Query, QueryStr};
pub use query_cursor::{InactiveQueryCursor, MatchedNode, MatchedNodeIdx, QueryCursor, QueryMatch};
pub use ropey::RopeTsInput;
pub use syntax_tree::{InputEdit, SyntaxTree};
pub use syntax_tree_node::SyntaxTreeNode;
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Point {
pub row: u32,
pub col: u32,
}
#[repr(C)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Range {
pub start_point: Point,
pub end_point: Point,
pub start_byte: u32,
pub end_byte: u32,
}
pub trait TsInput {
type Cursor: regex_cursor::Cursor;
fn cursor_at(&mut self, offset: usize) -> &mut Self::Cursor;
fn eq(&mut self, range1: ops::Range<usize>, range2: ops::Range<usize>) -> bool;
}
pub trait IntoTsInput {
type TsInput: TsInput;
fn into_ts_input(self) -> Self::TsInput;
}

View File

@@ -0,0 +1,107 @@
use std::fmt;
use std::path::{Path, PathBuf};
use std::ptr::NonNull;
use libloading::{Library, Symbol};
/// supported TS versions, WARNING: update when updating vendored c sources
pub const MIN_COMPATIBLE_ABI_VERSION: u32 = 13;
pub const ABI_VERSION: u32 = 14;
// opaque pointer
enum GrammarData {}
#[repr(transparent)]
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Grammar {
ptr: NonNull<GrammarData>,
}
unsafe impl Send for Grammar {}
unsafe impl Sync for Grammar {}
impl std::fmt::Debug for Grammar {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Grammar").finish_non_exhaustive()
}
}
impl Grammar {
/// Loads a shared library containg a tree sitter grammar with name `name`
// from `library_path`.
///
/// # Safety
///
/// `library_path` must be a valid tree sitter grammar
pub unsafe fn new(name: &str, library_path: &Path) -> Result<Grammar, Error> {
let library = unsafe {
Library::new(library_path).map_err(|err| Error::DlOpen {
err,
path: library_path.to_owned(),
})?
};
let language_fn_name = format!("tree_sitter_{}", name.replace('-', "_"));
let grammar = unsafe {
let language_fn: Symbol<unsafe extern "C" fn() -> NonNull<GrammarData>> = library
.get(language_fn_name.as_bytes())
.map_err(|err| Error::DlSym {
err,
symbol: name.to_owned(),
})?;
Grammar { ptr: language_fn() }
};
let version = grammar.version();
if (MIN_COMPATIBLE_ABI_VERSION..=ABI_VERSION).contains(&version) {
std::mem::forget(library);
Ok(grammar)
} else {
Err(Error::IncompatibleVersion { version })
}
}
pub fn version(self) -> u32 {
unsafe { ts_language_version(self) }
}
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("Error opening dynamic library {path:?}")]
DlOpen {
#[source]
err: libloading::Error,
path: PathBuf,
},
#[error("Failed to load symbol {symbol}")]
DlSym {
#[source]
err: libloading::Error,
symbol: String,
},
#[error("Tried to load grammar with incompatible ABI {version}.")]
IncompatibleVersion { version: u32 },
}
/// An error that occurred when trying to assign an incompatible [`Grammar`] to
/// a [`Parser`].
#[derive(Debug, PartialEq, Eq)]
pub struct IncompatibleGrammarError {
version: u32,
}
impl fmt::Display for IncompatibleGrammarError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(
f,
"Tried to load grammar with incompatible ABI {}.",
self.version,
)
}
}
impl std::error::Error for IncompatibleGrammarError {}
extern "C" {
/// Get the ABI version number for this language. This version number
/// is used to ensure that languages were generated by a compatible version of
/// Tree-sitter. See also [`ts_parser_set_language`].
pub fn ts_language_version(grammar: Grammar) -> u32;
}

View File

@@ -0,0 +1,200 @@
use std::os::raw::c_void;
use std::panic::{catch_unwind, AssertUnwindSafe};
use std::ptr::NonNull;
use std::{fmt, ptr};
use regex_cursor::Cursor;
use crate::tree_sitter::syntax_tree::{SyntaxTree, SyntaxTreeData};
use crate::tree_sitter::{Grammar, IntoTsInput, Point, Range, TsInput};
// opaque data
enum ParserData {}
/// A stateful object that this is used to produce a [`Tree`] based on some
/// source code.
pub struct Parser {
ptr: NonNull<ParserData>,
}
impl Parser {
/// Create a new parser.
#[must_use]
pub fn new() -> Parser {
Parser {
ptr: unsafe { ts_parser_new() },
}
}
/// Set the language that the parser should use for parsing.
pub fn set_language(&mut self, grammar: Grammar) {
unsafe { ts_parser_set_language(self.ptr, grammar) };
}
/// Set the ranges of text that the parser should include when parsing. By default, the parser
/// will always include entire documents. This function allows you to parse only a *portion*
/// of a document but still return a syntax tree whose ranges match up with the document as a
/// whole. You can also pass multiple disjoint ranges.
///
/// `ranges` must be non-overlapping and sorted.
pub fn set_included_ranges(&mut self, ranges: &[Range]) -> Result<(), InvalidRangesErrror> {
// TODO: save some memory by only storing byte ranges and converting them to TS ranges in an
// internal buffer here. Points are not used by TS. Alternatively we can path the TS C code
// to accept a simple pair (struct with two fields) of byte positions here instead of a full
// tree sitter range
let success = unsafe {
ts_parser_set_included_ranges(self.ptr, ranges.as_ptr(), ranges.len() as u32)
};
if success {
Ok(())
} else {
Err(InvalidRangesErrror)
}
}
#[must_use]
pub fn parse<I: TsInput>(
&mut self,
input: impl IntoTsInput<TsInput = I>,
old_tree: Option<&SyntaxTree>,
) -> Option<SyntaxTree> {
let mut input = input.into_ts_input();
unsafe extern "C" fn read<C: TsInput>(
payload: NonNull<c_void>,
byte_index: u32,
_position: Point,
bytes_read: *mut u32,
) -> *const u8 {
let cursor = catch_unwind(AssertUnwindSafe(move || {
let input: &mut C = payload.cast().as_mut();
let cursor = input.cursor_at(byte_index as usize);
let slice = cursor.chunk();
(slice.as_ptr(), slice.len().try_into().unwrap())
}));
match cursor {
Ok((ptr, len)) => {
*bytes_read = len;
ptr
}
Err(_) => {
*bytes_read = 0;
ptr::null()
}
}
}
let input = ParserInputRaw {
payload: NonNull::from(&mut input).cast(),
read: read::<I>,
// utf8
encoding: 0,
};
unsafe {
let old_tree = old_tree.map(|tree| tree.as_raw());
let new_tree = ts_parser_parse(self.ptr, old_tree, input);
new_tree.map(|raw| SyntaxTree::from_raw(raw))
}
}
}
impl Default for Parser {
fn default() -> Self {
Self::new()
}
}
unsafe impl Sync for Parser {}
unsafe impl Send for Parser {}
impl Drop for Parser {
fn drop(&mut self) {
unsafe { ts_parser_delete(self.ptr) }
}
}
/// An error that occurred when trying to assign an incompatible [`Grammar`] to
/// a [`Parser`].
#[derive(Debug, PartialEq, Eq)]
pub struct InvalidRangesErrror;
impl fmt::Display for InvalidRangesErrror {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "include ranges are overlap or are not sorted",)
}
}
impl std::error::Error for InvalidRangesErrror {}
type TreeSitterReadFn = unsafe extern "C" fn(
payload: NonNull<c_void>,
byte_index: u32,
position: Point,
bytes_read: *mut u32,
) -> *const u8;
#[repr(C)]
#[derive(Debug)]
pub struct ParserInputRaw {
pub payload: NonNull<c_void>,
pub read: TreeSitterReadFn,
pub encoding: u32,
}
extern "C" {
/// Create a new parser
fn ts_parser_new() -> NonNull<ParserData>;
/// Delete the parser, freeing all of the memory that it used.
fn ts_parser_delete(parser: NonNull<ParserData>);
/// Set the language that the parser should use for parsing. Returns a boolean indicating
/// whether or not the language was successfully assigned. True means assignment
/// succeeded. False means there was a version mismatch: the language was generated with
/// an incompatible version of the Tree-sitter CLI. Check the language's version using
/// [`ts_language_version`] and compare it to this library's [`TREE_SITTER_LANGUAGE_VERSION`]
/// and [`TREE_SITTER_MIN_COMPATIBLE_LANGUAGE_VERSION`] constants.
fn ts_parser_set_language(parser: NonNull<ParserData>, language: Grammar) -> bool;
/// Set the ranges of text that the parser should include when parsing. By default, the parser
/// will always include entire documents. This function allows you to parse only a *portion*
/// of a document but still return a syntax tree whose ranges match up with the document as a
/// whole. You can also pass multiple disjoint ranges. The second and third parameters specify
/// the location and length of an array of ranges. The parser does *not* take ownership of
/// these ranges; it copies the data, so it doesn't matter how these ranges are allocated.
/// If `count` is zero, then the entire document will be parsed. Otherwise, the given ranges
/// must be ordered from earliest to latest in the document, and they must not overlap. That
/// is, the following must hold for all: `i < count - 1`: `ranges[i].end_byte <= ranges[i +
/// 1].start_byte` If this requirement is not satisfied, the operation will fail, the ranges
/// will not be assigned, and this function will return `false`. On success, this function
/// returns `true`
fn ts_parser_set_included_ranges(
parser: NonNull<ParserData>,
ranges: *const Range,
count: u32,
) -> bool;
/// Use the parser to parse some source code and create a syntax tree. If you are parsing this
/// document for the first time, pass `NULL` for the `old_tree` parameter. Otherwise, if you
/// have already parsed an earlier version of this document and the document has since been
/// edited, pass the previous syntax tree so that the unchanged parts of it can be reused.
/// This will save time and memory. For this to work correctly, you must have already edited
/// the old syntax tree using the [`ts_tree_edit`] function in a way that exactly matches
/// the source code changes. The [`TSInput`] parameter lets you specify how to read the text.
/// It has the following three fields: 1. [`read`]: A function to retrieve a chunk of text
/// at a given byte offset and (row, column) position. The function should return a pointer
/// to the text and write its length to the [`bytes_read`] pointer. The parser does not
/// take ownership of this buffer; it just borrows it until it has finished reading it. The
/// function should write a zero value to the [`bytes_read`] pointer to indicate the end of the
/// document. 2. [`payload`]: An arbitrary pointer that will be passed to each invocation of
/// the [`read`] function. 3. [`encoding`]: An indication of how the text is encoded. Either
/// `TSInputEncodingUTF8` or `TSInputEncodingUTF16`. This function returns a syntax tree
/// on success, and `NULL` on failure. There are three possible reasons for failure: 1. The
/// parser does not have a language assigned. Check for this using the [`ts_parser_language`]
/// function. 2. Parsing was cancelled due to a timeout that was set by an earlier call to the
/// [`ts_parser_set_timeout_micros`] function. You can resume parsing from where the parser
/// left out by calling [`ts_parser_parse`] again with the same arguments. Or you can start
/// parsing from scratch by first calling [`ts_parser_reset`]. 3. Parsing was cancelled using
/// a cancellation flag that was set by an earlier call to [`ts_parser_set_cancellation_flag`].
/// You can resume parsing from where the parser left out by calling [`ts_parser_parse`] again
/// with the same arguments. [`read`]: TSInput::read [`payload`]: TSInput::payload [`encoding`]:
/// TSInput::encoding [`bytes_read`]: TSInput::read
fn ts_parser_parse(
parser: NonNull<ParserData>,
old_tree: Option<NonNull<SyntaxTreeData>>,
input: ParserInputRaw,
) -> Option<NonNull<SyntaxTreeData>>;
}

View File

@@ -0,0 +1,451 @@
use std::fmt::{self, Display};
use std::ops::Range;
use std::path::{Path, PathBuf};
use std::ptr::NonNull;
use std::{slice, str};
use crate::tree_sitter::query::predicate::{InvalidPredicateError, Predicate, TextPredicate};
use crate::tree_sitter::Grammar;
mod predicate;
mod property;
pub enum UserPredicate<'a> {
IsPropertySet {
negate: bool,
key: &'a str,
val: Option<&'a str>,
},
SetProperty {
key: &'a str,
val: Option<&'a str>,
},
Other(Predicate<'a>),
}
impl Display for UserPredicate<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match *self {
UserPredicate::IsPropertySet { negate, key, val } => {
let predicate = if negate { "is-not?" } else { "is?" };
write!(f, " ({predicate} {key} {})", val.unwrap_or(""))
}
UserPredicate::SetProperty { key, val } => {
write!(f, "(set! {key} {})", val.unwrap_or(""))
}
UserPredicate::Other(ref predicate) => {
write!(f, "{}", predicate.name())
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Pattern(pub(crate) u32);
impl Pattern {
pub const SENTINEL: Pattern = Pattern(u32::MAX);
pub fn idx(&self) -> usize {
self.0 as usize
}
}
pub enum QueryData {}
#[derive(Debug)]
pub(super) struct PatternData {
text_predicates: Range<u32>,
}
#[derive(Debug)]
pub struct Query {
pub(crate) raw: NonNull<QueryData>,
num_captures: u32,
num_strings: u32,
text_predicates: Vec<TextPredicate>,
patterns: Box<[PatternData]>,
}
impl Query {
/// Create a new query from a string containing one or more S-expression
/// patterns.
///
/// The query is associated with a particular grammar, and can only be run
/// on syntax nodes parsed with that grammar. References to Queries can be
/// shared between multiple threads.
pub fn new(
grammar: Grammar,
source: &str,
path: impl AsRef<Path>,
mut custom_predicate: impl FnMut(Pattern, UserPredicate) -> Result<(), InvalidPredicateError>,
) -> Result<Self, ParseError> {
assert!(
source.len() <= i32::MAX as usize,
"TreeSitter queries must be smaller then 2 GiB (is {})",
source.len() as f64 / 1024.0 / 1024.0 / 1024.0
);
let mut error_offset = 0u32;
let mut error_kind = RawQueryError::None;
let bytes = source.as_bytes();
// Compile the query.
let ptr = unsafe {
ts_query_new(
grammar,
bytes.as_ptr(),
bytes.len() as u32,
&mut error_offset,
&mut error_kind,
)
};
let Some(raw) = ptr else {
let offset = error_offset as usize;
let error_word = || {
source[offset..]
.chars()
.take_while(|&c| c.is_alphanumeric() || matches!(c, '_' | '-'))
.collect()
};
let err = match error_kind {
RawQueryError::NodeType => {
let node: String = error_word();
ParseError::InvalidNodeType {
location: ParserErrorLocation::new(
source,
path.as_ref(),
offset,
node.chars().count(),
),
node,
}
}
RawQueryError::Field => {
let field = error_word();
ParseError::InvalidFieldName {
location: ParserErrorLocation::new(
source,
path.as_ref(),
offset,
field.chars().count(),
),
field,
}
}
RawQueryError::Capture => {
let capture = error_word();
ParseError::InvalidCaptureName {
location: ParserErrorLocation::new(
source,
path.as_ref(),
offset,
capture.chars().count(),
),
capture,
}
}
RawQueryError::Syntax => ParseError::SyntaxError(ParserErrorLocation::new(
source,
path.as_ref(),
offset,
0,
)),
RawQueryError::Structure => ParseError::ImpossiblePattern(
ParserErrorLocation::new(source, path.as_ref(), offset, 0),
),
RawQueryError::None => {
unreachable!("tree-sitter returned a null pointer but did not set an error")
}
RawQueryError::Language => unreachable!("should be handled at grammar load"),
};
return Err(err);
};
// I am not going to bother with safety comments here, all of these are
// safe as long as TS is not buggy because raw is a properly constructed query
let num_captures = unsafe { ts_query_capture_count(raw) };
let num_strings = unsafe { ts_query_string_count(raw) };
let num_patterns = unsafe { ts_query_pattern_count(raw) };
let mut query = Query {
raw,
num_captures,
num_strings,
text_predicates: Vec::new(),
patterns: Box::default(),
};
let patterns: Result<_, ParseError> = (0..num_patterns)
.map(|pattern| {
query
.parse_pattern_predicates(Pattern(pattern), &mut custom_predicate)
.map_err(|err| ParseError::InvalidPredicate {
message: err.msg.into(),
location: ParserErrorLocation::new(
source,
path.as_ref(),
unsafe { ts_query_start_byte_for_pattern(query.raw, pattern) as usize },
0,
),
})
})
.collect();
query.patterns = patterns?;
Ok(query)
}
#[inline]
fn get_string(&self, str: QueryStr) -> &str {
let value_id = str.0;
// need an assertions because the ts c api does not do bounds check
assert!(value_id <= self.num_captures, "invalid value index");
unsafe {
let mut len = 0;
let ptr = ts_query_string_value_for_id(self.raw, value_id, &mut len);
let data = slice::from_raw_parts(ptr, len as usize);
// safety: we only allow passing valid str(ings) as arguments to query::new
// name is always a substring of that. Treesitter does proper utf8 segmentation
// so any substrings it produces are codepoint aligned and therefore valid utf8
str::from_utf8_unchecked(data)
}
}
#[inline]
pub fn capture_name(&self, capture_idx: Capture) -> &str {
let capture_idx = capture_idx.0;
// need an assertions because the ts c api does not do bounds check
assert!(capture_idx <= self.num_captures, "invalid capture index");
let mut length = 0;
unsafe {
let ptr = ts_query_capture_name_for_id(self.raw, capture_idx, &mut length);
let name = slice::from_raw_parts(ptr, length as usize);
// safety: we only allow passing valid str(ings) as arguments to query::new
// name is always a substring of that. Treesitter does proper utf8 segmentation
// so any substrings it produces are codepoint aligned and therefore valid utf8
str::from_utf8_unchecked(name)
}
}
#[inline]
pub fn captures(&self) -> impl ExactSizeIterator<Item = (Capture, &str)> {
(0..self.num_captures).map(|cap| (Capture(cap), self.capture_name(Capture(cap))))
}
#[inline]
pub fn num_captures(&self) -> u32 {
self.num_captures
}
#[inline]
pub fn get_capture(&self, capture_name: &str) -> Option<Capture> {
for capture in 0..self.num_captures {
if capture_name == self.capture_name(Capture(capture)) {
return Some(Capture(capture));
}
}
None
}
pub(crate) fn pattern_text_predicates(&self, pattern_idx: u16) -> &[TextPredicate] {
let range = self.patterns[pattern_idx as usize].text_predicates.clone();
&self.text_predicates[range.start as usize..range.end as usize]
}
/// Get the byte offset where the given pattern starts in the query's
/// source.
#[doc(alias = "ts_query_start_byte_for_pattern")]
#[must_use]
pub fn start_byte_for_pattern(&self, pattern: Pattern) -> usize {
assert!(
pattern.0 < self.text_predicates.len() as u32,
"Pattern index is {pattern_index} but the pattern count is {}",
self.text_predicates.len(),
);
unsafe { ts_query_start_byte_for_pattern(self.raw, pattern.0) as usize }
}
/// Get the number of patterns in the query.
#[must_use]
pub fn pattern_count(&self) -> usize {
unsafe { ts_query_pattern_count(self.raw) as usize }
}
/// Get the number of patterns in the query.
#[must_use]
pub fn patterns(&self) -> impl ExactSizeIterator<Item = Pattern> {
(0..self.pattern_count() as u32).map(Pattern)
}
}
impl Drop for Query {
fn drop(&mut self) {
unsafe { ts_query_delete(self.raw) }
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[repr(transparent)]
pub struct Capture(u32);
impl Capture {
pub fn name(self, query: &Query) -> &str {
query.capture_name(self)
}
pub fn idx(self) -> usize {
self.0 as usize
}
}
/// A reference to a string stroed in a query
#[derive(Clone, Copy, Debug)]
pub struct QueryStr(u32);
impl QueryStr {
pub fn get(self, query: &Query) -> &str {
query.get_string(self)
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct ParserErrorLocation {
pub path: PathBuf,
/// at which line the error occured
pub line: usize,
/// at which codepoints/columns the errors starts in the line
pub column: usize,
/// how many codepoints/columns the error takes up
pub len: usize,
line_content: String,
}
impl ParserErrorLocation {
pub fn new(source: &str, path: &Path, offset: usize, len: usize) -> ParserErrorLocation {
let (line, line_content) = source[..offset]
.split('\n')
.map(|line| line.strip_suffix('\r').unwrap_or(line))
.enumerate()
.last()
.unwrap_or((0, ""));
let column = line_content.chars().count();
ParserErrorLocation {
path: path.to_owned(),
line,
column,
len,
line_content: line_content.to_owned(),
}
}
}
impl Display for ParserErrorLocation {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
writeln!(
f,
" --> {}:{}:{}",
self.path.display(),
self.line,
self.column
)?;
let line = self.line.to_string();
let prefix = format!(" {:width$} |", "", width = line.len());
writeln!(f, "{prefix}")?;
writeln!(f, " {line} | {}", self.line_content)?;
writeln!(
f,
"{prefix}{:width$}{:^<len$}",
"",
"^",
width = self.column,
len = self.len
)?;
writeln!(f, "{prefix}")
}
}
#[derive(thiserror::Error, Debug, PartialEq, Eq)]
pub enum ParseError {
#[error("unexpected EOF")]
UnexpectedEof,
#[error("invalid query syntax\n{0}")]
SyntaxError(ParserErrorLocation),
#[error("invalid node type {node:?}\n{location}")]
InvalidNodeType {
node: String,
location: ParserErrorLocation,
},
#[error("invalid field name {field:?}\n{location}")]
InvalidFieldName {
field: String,
location: ParserErrorLocation,
},
#[error("invalid capture name {capture:?}\n{location}")]
InvalidCaptureName {
capture: String,
location: ParserErrorLocation,
},
#[error("{message}\n{location}")]
InvalidPredicate {
message: String,
location: ParserErrorLocation,
},
#[error("invalid predicate\n{0}")]
ImpossiblePattern(ParserErrorLocation),
}
#[repr(C)]
enum RawQueryError {
None = 0,
Syntax = 1,
NodeType = 2,
Field = 3,
Capture = 4,
Structure = 5,
Language = 6,
}
extern "C" {
/// Create a new query from a string containing one or more S-expression
/// patterns. The query is associated with a particular language, and can
/// only be run on syntax nodes parsed with that language. If all of the
/// given patterns are valid, this returns a [`TSQuery`]. If a pattern is
/// invalid, this returns `NULL`, and provides two pieces of information
/// about the problem: 1. The byte offset of the error is written to
/// the `error_offset` parameter. 2. The type of error is written to the
/// `error_type` parameter.
fn ts_query_new(
grammar: Grammar,
source: *const u8,
source_len: u32,
error_offset: &mut u32,
error_type: &mut RawQueryError,
) -> Option<NonNull<QueryData>>;
/// Delete a query, freeing all of the memory that it used.
fn ts_query_delete(query: NonNull<QueryData>);
/// Get the number of patterns, captures, or string literals in the query.
fn ts_query_pattern_count(query: NonNull<QueryData>) -> u32;
fn ts_query_capture_count(query: NonNull<QueryData>) -> u32;
fn ts_query_string_count(query: NonNull<QueryData>) -> u32;
/// Get the byte offset where the given pattern starts in the query's
/// source. This can be useful when combining queries by concatenating their
/// source code strings.
fn ts_query_start_byte_for_pattern(query: NonNull<QueryData>, pattern_index: u32) -> u32;
// fn ts_query_is_pattern_rooted(query: NonNull<QueryData>, pattern_index: u32) -> bool;
// fn ts_query_is_pattern_non_local(query: NonNull<QueryData>, pattern_index: u32) -> bool;
// fn ts_query_is_pattern_guaranteed_at_step(query: NonNull<QueryData>, byte_offset: u32) -> bool;
/// Get the name and length of one of the query's captures, or one of the
/// query's string literals. Each capture and string is associated with a
/// numeric id based on the order that it appeared in the query's source.
fn ts_query_capture_name_for_id(
query: NonNull<QueryData>,
index: u32,
length: &mut u32,
) -> *const u8;
fn ts_query_string_value_for_id(
self_: NonNull<QueryData>,
index: u32,
length: &mut u32,
) -> *const u8;
}

View File

@@ -0,0 +1,448 @@
use std::error::Error;
use std::iter::zip;
use std::ops::Range;
use std::ptr::NonNull;
use std::{fmt, slice};
use crate::tree_sitter::query::property::QueryProperty;
use crate::tree_sitter::query::{
Capture, Pattern, PatternData, Query, QueryData, QueryStr, UserPredicate,
};
use crate::tree_sitter::query_cursor::MatchedNode;
use crate::tree_sitter::TsInput;
use regex_cursor::engines::meta::Regex;
use regex_cursor::Cursor;
macro_rules! bail {
($($args:tt)*) => {{
return Err(InvalidPredicateError {msg: format!($($args)*).into() })
}}
}
macro_rules! ensure {
($cond: expr, $($args:tt)*) => {{
if !$cond {
return Err(InvalidPredicateError { msg: format!($($args)*).into() })
}
}}
}
#[derive(Debug)]
pub(super) enum TextPredicateKind {
EqString(QueryStr),
EqCapture(Capture),
MatchString(Regex),
AnyString(Box<[QueryStr]>),
}
#[derive(Debug)]
pub(crate) struct TextPredicate {
capture: Capture,
kind: TextPredicateKind,
negated: bool,
match_all: bool,
}
fn input_matches_str<I: TsInput>(str: &str, range: Range<usize>, input: &mut I) -> bool {
if str.len() != range.len() {
return false;
}
let mut str = str.as_bytes();
let cursor = input.cursor_at(range.start);
let start_in_chunk = range.start - cursor.offset();
if range.end - cursor.offset() <= cursor.chunk().len() {
// hotpath
return &cursor.chunk()[start_in_chunk..range.end - cursor.offset()] == str;
}
if cursor.chunk()[start_in_chunk..] != str[..cursor.chunk().len() - start_in_chunk] {
return false;
}
str = &str[..cursor.chunk().len() - start_in_chunk];
while cursor.advance() {
if str.len() <= cursor.chunk().len() {
return &cursor.chunk()[..range.end - cursor.offset()] == str;
}
if &str[..cursor.chunk().len()] != cursor.chunk() {
return false;
}
str = &str[cursor.chunk().len()..]
}
// buggy cursor/invalid range
false
}
fn inputs_match<I: TsInput>(str: &str, range: Range<usize>, input: &mut I) -> bool {
if str.len() != range.len() {
return false;
}
let mut str = str.as_bytes();
let cursor = input.cursor_at(range.start);
let start_in_chunk = range.start - cursor.offset();
if range.end - cursor.offset() <= cursor.chunk().len() {
// hotpath
return &cursor.chunk()[start_in_chunk..range.end - cursor.offset()] == str;
}
if cursor.chunk()[start_in_chunk..] != str[..cursor.chunk().len() - start_in_chunk] {
return false;
}
str = &str[..cursor.chunk().len() - start_in_chunk];
while cursor.advance() {
if str.len() <= cursor.chunk().len() {
return &cursor.chunk()[..range.end - cursor.offset()] == str;
}
if &str[..cursor.chunk().len()] != cursor.chunk() {
return false;
}
str = &str[cursor.chunk().len()..]
}
// buggy cursor/invalid range
false
}
impl TextPredicate {
/// handlers match_all and negated
fn satisfied_helper(&self, mut nodes: impl Iterator<Item = bool>) -> bool {
if self.match_all {
nodes.all(|matched| matched != self.negated)
} else {
nodes.any(|matched| matched != self.negated)
}
}
pub fn satsified<I: TsInput>(
&self,
input: &mut I,
matched_nodes: &[MatchedNode],
query: &Query,
) -> bool {
let mut capture_nodes = matched_nodes
.iter()
.filter(|matched_node| matched_node.capture == self.capture);
match self.kind {
TextPredicateKind::EqString(str) => self.satisfied_helper(capture_nodes.map(|node| {
let range = node.syntax_node.byte_range();
input_matches_str(query.get_string(str), range.clone(), input)
})),
TextPredicateKind::EqCapture(other_capture) => {
let mut other_nodes = matched_nodes
.iter()
.filter(|matched_node| matched_node.capture == other_capture);
let res = self.satisfied_helper(zip(&mut capture_nodes, &mut other_nodes).map(
|(node1, node2)| {
let range1 = node1.syntax_node.byte_range();
let range2 = node2.syntax_node.byte_range();
input.eq(range1, range2)
},
));
let consumed_all = capture_nodes.next().is_none() && other_nodes.next().is_none();
res && (!self.match_all || consumed_all)
}
TextPredicateKind::MatchString(ref regex) => {
self.satisfied_helper(capture_nodes.map(|node| {
let range = node.syntax_node.byte_range();
let input = regex_cursor::Input::new(input.cursor_at(range.start)).range(range);
regex.is_match(input)
}))
}
TextPredicateKind::AnyString(ref strings) => {
let strings = strings.iter().map(|&str| query.get_string(str));
self.satisfied_helper(capture_nodes.map(|node| {
let range = node.syntax_node.byte_range();
strings
.clone()
.filter(|str| str.len() == range.len())
.any(|str| input_matches_str(str, range.clone(), input))
}))
}
}
}
}
impl Query {
pub(super) fn parse_pattern_predicates(
&mut self,
pattern: Pattern,
mut custom_predicate: impl FnMut(Pattern, UserPredicate) -> Result<(), InvalidPredicateError>,
) -> Result<PatternData, InvalidPredicateError> {
let text_predicate_start = self.text_predicates.len() as u32;
let predicate_steps = unsafe {
let mut len = 0u32;
let raw_predicates = ts_query_predicates_for_pattern(self.raw, pattern.0, &mut len);
(len != 0)
.then(|| slice::from_raw_parts(raw_predicates, len as usize))
.unwrap_or_default()
};
let predicates = predicate_steps
.split(|step| step.kind == PredicateStepKind::Done)
.filter(|predicate| !predicate.is_empty());
for predicate in predicates {
let predicate = unsafe { Predicate::new(self, predicate)? };
match predicate.name() {
"eq?" | "not-eq?" | "any-eq?" | "any-not-eq?" => {
predicate.check_arg_count(2)?;
let capture_idx = predicate.capture_arg(0)?;
let arg2 = predicate.arg(1);
let negated = matches!(predicate.name(), "not-eq?" | "not-any-eq?");
let match_all = matches!(predicate.name(), "eq?" | "not-eq?");
let kind = match arg2 {
PredicateArg::Capture(capture) => TextPredicateKind::EqCapture(capture),
PredicateArg::String(str) => TextPredicateKind::EqString(str),
};
self.text_predicates.push(TextPredicate {
capture: capture_idx,
kind,
negated,
match_all,
});
}
"match?" | "not-match?" | "any-match?" | "any-not-match?" => {
predicate.check_arg_count(2)?;
let capture_idx = predicate.capture_arg(0)?;
let regex = predicate.query_str_arg(1)?.get(self);
let negated = matches!(predicate.name(), "not-match?" | "any-not-match?");
let match_all = matches!(predicate.name(), "match?" | "not-match?");
let regex = match Regex::new(regex) {
Ok(regex) => regex,
Err(err) => bail!("invalid regex '{regex}', {err}"),
};
self.text_predicates.push(TextPredicate {
capture: capture_idx,
kind: TextPredicateKind::MatchString(regex),
negated,
match_all,
});
}
"set!" => {
let property = QueryProperty::parse(&predicate)?;
custom_predicate(
pattern,
UserPredicate::SetProperty {
key: property.key.get(&self),
val: property.val.map(|val| val.get(&self)),
},
)?
}
"is-not?" | "is?" => {
let property = QueryProperty::parse(&predicate)?;
custom_predicate(
pattern,
UserPredicate::IsPropertySet {
negate: predicate.name() == "is-not?",
key: property.key.get(&self),
val: property.val.map(|val| val.get(&self)),
},
)?
}
"any-of?" | "not-any-of?" => {
predicate.check_min_arg_count(1)?;
let capture = predicate.capture_arg(0)?;
let negated = predicate.name() == "not-any-of?";
let values: Result<_, InvalidPredicateError> = (1..predicate.num_args())
.map(|i| predicate.query_str_arg(i))
.collect();
self.text_predicates.push(TextPredicate {
capture,
kind: TextPredicateKind::AnyString(values?),
negated,
match_all: false,
});
}
// is and is-not are better handeled as custom predicates since interpreting is context dependent
// "is?" => property_predicates.push((QueryProperty::parse(&predicate), false)),
// "is-not?" => property_predicates.push((QueryProperty::parse(&predicate), true)),
_ => custom_predicate(pattern, UserPredicate::Other(predicate))?,
}
}
Ok(PatternData {
text_predicates: text_predicate_start..self.text_predicates.len() as u32,
})
}
}
pub enum PredicateArg {
Capture(Capture),
String(QueryStr),
}
pub struct Predicate<'a> {
pub name: QueryStr,
args: &'a [PredicateStep],
query: &'a Query,
}
impl<'a> Predicate<'a> {
unsafe fn new(
query: &'a Query,
predicate: &'a [PredicateStep],
) -> Result<Predicate<'a>, InvalidPredicateError> {
ensure!(
predicate[0].kind == PredicateStepKind::String,
"expected predicate to start with a function name. Got @{}.",
Capture(predicate[0].value_id).name(query)
);
let operator_name = QueryStr(predicate[0].value_id);
Ok(Predicate {
name: operator_name,
args: &predicate[1..],
query,
})
}
pub fn name(&self) -> &str {
self.name.get(self.query)
}
pub fn check_arg_count(&self, n: usize) -> Result<(), InvalidPredicateError> {
ensure!(
self.args.len() == n,
"expected {n} arguments for #{}, got {}",
self.name(),
self.args.len()
);
Ok(())
}
pub fn check_min_arg_count(&self, n: usize) -> Result<(), InvalidPredicateError> {
ensure!(
n <= self.args.len(),
"expected at least {n} arguments for #{}, got {}",
self.name(),
self.args.len()
);
Ok(())
}
pub fn check_max_arg_count(&self, n: usize) -> Result<(), InvalidPredicateError> {
ensure!(
self.args.len() <= n,
"expected at most {n} arguments for #{}, got {}",
self.name(),
self.args.len()
);
Ok(())
}
pub fn query_str_arg(&self, i: usize) -> Result<QueryStr, InvalidPredicateError> {
match self.arg(i) {
PredicateArg::String(str) => Ok(str),
PredicateArg::Capture(capture) => bail!(
"{i}. argument to #{} must be a literal, got capture @{:?}",
self.name(),
capture.name(self.query)
),
}
}
pub fn str_arg(&self, i: usize) -> Result<&str, InvalidPredicateError> {
Ok(self.query_str_arg(i)?.get(self.query))
}
pub fn num_args(&self) -> usize {
self.args.len()
}
pub fn capture_arg(&self, i: usize) -> Result<Capture, InvalidPredicateError> {
match self.arg(i) {
PredicateArg::Capture(capture) => Ok(capture),
PredicateArg::String(str) => bail!(
"{i}. argument to #{} expected a capture, got literal {:?}",
self.name(),
str.get(self.query)
),
}
}
pub fn arg(&self, i: usize) -> PredicateArg {
self.args[i].try_into().unwrap()
}
pub fn args(&self) -> impl Iterator<Item = PredicateArg> + '_ {
self.args.iter().map(|&arg| arg.try_into().unwrap())
}
}
#[derive(Debug)]
pub struct InvalidPredicateError {
pub(super) msg: Box<str>,
}
impl From<String> for InvalidPredicateError {
fn from(value: String) -> Self {
InvalidPredicateError {
msg: value.into_boxed_str(),
}
}
}
impl<'a> From<&'a str> for InvalidPredicateError {
fn from(value: &'a str) -> Self {
InvalidPredicateError { msg: value.into() }
}
}
impl fmt::Display for InvalidPredicateError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.msg)
}
}
impl Error for InvalidPredicateError {}
#[repr(C)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum PredicateStepKind {
Done = 0,
Capture = 1,
String = 2,
}
#[repr(C)]
#[derive(Debug, Clone, Copy)]
struct PredicateStep {
kind: PredicateStepKind,
value_id: u32,
}
impl TryFrom<PredicateStep> for PredicateArg {
type Error = ();
fn try_from(step: PredicateStep) -> Result<Self, Self::Error> {
match step.kind {
PredicateStepKind::String => Ok(PredicateArg::String(QueryStr(step.value_id))),
PredicateStepKind::Capture => Ok(PredicateArg::Capture(Capture(step.value_id))),
PredicateStepKind::Done => Err(()),
}
}
}
extern "C" {
/// Get all of the predicates for the given pattern in the query. The
/// predicates are represented as a single array of steps. There are three
/// types of steps in this array, which correspond to the three legal values
/// for the `type` field: - `TSQueryPredicateStepTypeCapture` - Steps with
/// this type represent names of captures. Their `value_id` can be used
/// with the [`ts_query_capture_name_for_id`] function to obtain the name
/// of the capture. - `TSQueryPredicateStepTypeString` - Steps with this
/// type represent literal strings. Their `value_id` can be used with the
/// [`ts_query_string_value_for_id`] function to obtain their string value.
/// - `TSQueryPredicateStepTypeDone` - Steps with this type are *sentinels*
/// that represent the end of an individual predicate. If a pattern has two
/// predicates, then there will be two steps with this `type` in the array.
fn ts_query_predicates_for_pattern(
query: NonNull<QueryData>,
pattern_index: u32,
step_count: &mut u32,
) -> *const PredicateStep;
}

View File

@@ -0,0 +1,20 @@
use crate::tree_sitter::query::predicate::{InvalidPredicateError, Predicate};
use crate::tree_sitter::query::QueryStr;
#[derive(Debug)]
pub struct QueryProperty {
pub key: QueryStr,
pub val: Option<QueryStr>,
}
impl QueryProperty {
pub fn parse(predicate: &Predicate) -> Result<Self, InvalidPredicateError> {
predicate.check_min_arg_count(1)?;
predicate.check_max_arg_count(2)?;
let key = predicate.query_str_arg(0)?;
let val = (predicate.num_args() == 1)
.then(|| predicate.query_str_arg(1))
.transpose()?;
Ok(QueryProperty { key, val })
}
}

View File

@@ -0,0 +1,343 @@
use core::slice;
use std::cell::UnsafeCell;
use std::marker::PhantomData;
use std::mem::replace;
use std::ops::Range;
use std::ptr::{self, NonNull};
use crate::tree_sitter::query::{Capture, Pattern, Query, QueryData};
use crate::tree_sitter::syntax_tree_node::SyntaxTreeNodeRaw;
use crate::tree_sitter::{SyntaxTree, SyntaxTreeNode, TsInput};
enum QueryCursorData {}
thread_local! {
static CURSOR_CACHE: UnsafeCell<Vec<InactiveQueryCursor>> = UnsafeCell::new(Vec::with_capacity(8));
}
/// SAFETY: must not call itself recuresively
unsafe fn with_cache<T>(f: impl FnOnce(&mut Vec<InactiveQueryCursor>) -> T) -> T {
CURSOR_CACHE.with(|cache| f(&mut *cache.get()))
}
pub struct QueryCursor<'a, 'tree, I: TsInput> {
query: &'a Query,
ptr: *mut QueryCursorData,
tree: PhantomData<&'tree SyntaxTree>,
input: I,
}
impl<'tree, I: TsInput> QueryCursor<'_, 'tree, I> {
pub fn next_match(&mut self) -> Option<QueryMatch<'_, 'tree>> {
let mut query_match = TSQueryMatch {
id: 0,
pattern_index: 0,
capture_count: 0,
captures: ptr::null(),
};
loop {
let success = unsafe { ts_query_cursor_next_match(self.ptr, &mut query_match) };
if !success {
return None;
}
let matched_nodes = unsafe {
slice::from_raw_parts(
query_match.captures.cast(),
query_match.capture_count as usize,
)
};
let satisfies_predicates = self
.query
.pattern_text_predicates(query_match.pattern_index)
.iter()
.all(|predicate| predicate.satsified(&mut self.input, matched_nodes, self.query));
if satisfies_predicates {
let res = QueryMatch {
id: query_match.id,
pattern: Pattern(query_match.pattern_index as u32),
matched_nodes,
query_cursor: unsafe { &mut *self.ptr },
_tree: PhantomData,
};
return Some(res);
}
}
}
pub fn next_matched_node(&mut self) -> Option<(QueryMatch<'_, 'tree>, MatchedNodeIdx)> {
let mut query_match = TSQueryMatch {
id: 0,
pattern_index: 0,
capture_count: 0,
captures: ptr::null(),
};
let mut capture_idx = 0;
loop {
let success = unsafe {
ts_query_cursor_next_capture(self.ptr, &mut query_match, &mut capture_idx)
};
if !success {
return None;
}
let matched_nodes = unsafe {
slice::from_raw_parts(
query_match.captures.cast(),
query_match.capture_count as usize,
)
};
let satisfies_predicates = self
.query
.pattern_text_predicates(query_match.pattern_index)
.iter()
.all(|predicate| predicate.satsified(&mut self.input, matched_nodes, self.query));
if satisfies_predicates {
let res = QueryMatch {
id: query_match.id,
pattern: Pattern(query_match.pattern_index as u32),
matched_nodes,
query_cursor: unsafe { &mut *self.ptr },
_tree: PhantomData,
};
return Some((res, capture_idx));
} else {
unsafe {
ts_query_cursor_remove_match(self.ptr, query_match.id);
}
}
}
}
pub fn set_byte_range(&mut self, range: Range<usize>) {
unsafe {
ts_query_cursor_set_byte_range(self.ptr, range.start as u32, range.end as u32);
}
}
pub fn reuse(mut self) -> InactiveQueryCursor {
let ptr = replace(&mut self.ptr, ptr::null_mut());
InactiveQueryCursor {
ptr: unsafe { NonNull::new_unchecked(ptr) },
}
}
}
impl<I: TsInput> Drop for QueryCursor<'_, '_, I> {
fn drop(&mut self) {
// we allow moving the cursor data out so we need the null check here
// would be cleaner with a subtype but doesn't really matter at the end of the day
if let Some(ptr) = NonNull::new(self.ptr) {
unsafe { with_cache(|cache| cache.push(InactiveQueryCursor { ptr })) }
}
}
}
/// A query cursor that is not actively associated with a query
pub struct InactiveQueryCursor {
ptr: NonNull<QueryCursorData>,
}
impl InactiveQueryCursor {
pub fn new() -> Self {
unsafe {
with_cache(|cache| {
cache.pop().unwrap_or_else(|| InactiveQueryCursor {
ptr: NonNull::new_unchecked(ts_query_cursor_new()),
})
})
}
}
/// Return the maximum number of in-progress matches for this cursor.
#[doc(alias = "ts_query_cursor_match_limit")]
#[must_use]
pub fn match_limit(&self) -> u32 {
unsafe { ts_query_cursor_match_limit(self.ptr.as_ptr()) }
}
/// Set the maximum number of in-progress matches for this cursor. The
/// limit must be > 0 and <= 65536.
#[doc(alias = "ts_query_cursor_set_match_limit")]
pub fn set_match_limit(&mut self, limit: u32) {
unsafe {
ts_query_cursor_set_match_limit(self.ptr.as_ptr(), limit);
}
}
/// Check if, on its last execution, this cursor exceeded its maximum number
/// of in-progress matches.
#[doc(alias = "ts_query_cursor_did_exceed_match_limit")]
#[must_use]
pub fn did_exceed_match_limit(&self) -> bool {
unsafe { ts_query_cursor_did_exceed_match_limit(self.ptr.as_ptr()) }
}
pub fn set_byte_range(&mut self, range: Range<usize>) {
unsafe {
ts_query_cursor_set_byte_range(self.ptr.as_ptr(), range.start as u32, range.end as u32);
}
}
pub fn execute_query<'a, 'tree, I: TsInput>(
self,
query: &'a Query,
node: &SyntaxTreeNode<'tree>,
input: I,
) -> QueryCursor<'a, 'tree, I> {
let ptr = self.ptr.as_ptr();
unsafe { ts_query_cursor_exec(self.ptr.as_ptr(), query.raw.as_ref(), node.as_raw()) };
QueryCursor {
query,
ptr,
tree: PhantomData,
input,
}
}
}
impl Drop for InactiveQueryCursor {
fn drop(&mut self) {
unsafe { ts_query_cursor_delete(self.ptr.as_ptr()) }
}
}
pub type MatchedNodeIdx = u32;
#[repr(C)]
#[derive(Clone)]
pub struct MatchedNode<'tree> {
pub syntax_node: SyntaxTreeNode<'tree>,
pub capture: Capture,
}
pub struct QueryMatch<'cursor, 'tree> {
id: u32,
pattern: Pattern,
matched_nodes: &'cursor [MatchedNode<'tree>],
query_cursor: &'cursor mut QueryCursorData,
_tree: PhantomData<&'tree super::SyntaxTree>,
}
impl<'tree> QueryMatch<'_, 'tree> {
pub fn matched_nodes(&self) -> impl Iterator<Item = &MatchedNode<'tree>> {
self.matched_nodes.iter()
}
pub fn nodes_for_capture(
&self,
capture: Capture,
) -> impl Iterator<Item = &SyntaxTreeNode<'tree>> {
self.matched_nodes
.iter()
.filter(move |mat| mat.capture == capture)
.map(|mat| &mat.syntax_node)
}
pub fn matched_node(&self, i: MatchedNodeIdx) -> &MatchedNode {
&self.matched_nodes[i as usize]
}
#[must_use]
pub const fn id(&self) -> u32 {
self.id
}
#[must_use]
pub const fn pattern(&self) -> Pattern {
self.pattern
}
#[doc(alias = "ts_query_cursor_remove_match")]
/// removes this match from the cursor so that further captures
/// from its cursor so that future captures that belong to this match
/// are no longer returned by capture iterators
pub fn remove(self) {
unsafe {
ts_query_cursor_remove_match(self.query_cursor, self.id);
}
}
}
#[repr(C)]
#[derive(Debug)]
struct TSQueryCapture {
node: SyntaxTreeNodeRaw,
index: u32,
}
#[repr(C)]
#[derive(Debug)]
struct TSQueryMatch {
id: u32,
pattern_index: u16,
capture_count: u16,
captures: *const TSQueryCapture,
}
extern "C" {
/// Advance to the next capture of the currently running query.
/// If there is a capture, write its match to `*match` and its index within
/// the matche's capture list to `*capture_index`. Otherwise, return `false`.
fn ts_query_cursor_next_capture(
self_: *mut QueryCursorData,
match_: &mut TSQueryMatch,
capture_index: &mut u32,
) -> bool;
/// Advance to the next match of the currently running query.
///
/// If there is a match, write it to `*match` and return `true`.
/// Otherwise, return `false`.
pub fn ts_query_cursor_next_match(
self_: *mut QueryCursorData,
match_: &mut TSQueryMatch,
) -> bool;
fn ts_query_cursor_remove_match(self_: *mut QueryCursorData, match_id: u32);
/// Delete a query cursor, freeing all of the memory that it used
pub fn ts_query_cursor_delete(self_: *mut QueryCursorData);
/// Create a new cursor for executing a given query.
/// The cursor stores the state that is needed to iteratively search
/// for matches. To use the query cursor, first call [`ts_query_cursor_exec`]
/// to start running a given query on a given syntax node. Then, there are
/// two options for consuming the results of the query:
/// 1. Repeatedly call [`ts_query_cursor_next_match`] to iterate over all of the
/// *matches* in the order that they were found. Each match contains the
/// index of the pattern that matched, and an array of captures. Because
/// multiple patterns can match the same set of nodes, one match may contain
/// captures that appear *before* some of the captures from a previous match.
/// 2. Repeatedly call [`ts_query_cursor_next_capture`] to iterate over all of the
/// individual *captures* in the order that they appear. This is useful if
/// don't care about which pattern matched, and just want a single ordered
/// sequence of captures.
/// If you don't care about consuming all of the results, you can stop calling
/// [`ts_query_cursor_next_match`] or [`ts_query_cursor_next_capture`] at any point.
/// You can then start executing another query on another node by calling
/// [`ts_query_cursor_exec`] again."]
pub fn ts_query_cursor_new() -> *mut QueryCursorData;
/// Start running a given query on a given node.
pub fn ts_query_cursor_exec(
self_: *mut QueryCursorData,
query: &QueryData,
node: SyntaxTreeNodeRaw,
);
/// Manage the maximum number of in-progress matches allowed by this query
/// cursor.
///
/// Query cursors have an optional maximum capacity for storing lists of
/// in-progress captures. If this capacity is exceeded, then the
/// earliest-starting match will silently be dropped to make room for further
/// matches. This maximum capacity is optional — by default, query cursors allow
/// any number of pending matches, dynamically allocating new space for them as
/// needed as the query is executed.
pub fn ts_query_cursor_did_exceed_match_limit(self_: *const QueryCursorData) -> bool;
pub fn ts_query_cursor_match_limit(self_: *const QueryCursorData) -> u32;
pub fn ts_query_cursor_set_match_limit(self_: *mut QueryCursorData, limit: u32);
/// Set the range of bytes or (row, column) positions in which the query
/// will be executed.
pub fn ts_query_cursor_set_byte_range(
self_: *mut QueryCursorData,
start_byte: u32,
end_byte: u32,
);
}

View File

@@ -0,0 +1 @@
pub struct QueryMatch {}

View File

@@ -0,0 +1,54 @@
use std::ops;
use regex_cursor::{Cursor, RopeyCursor};
use ropey::RopeSlice;
use crate::tree_sitter::{IntoTsInput, TsInput};
pub struct RopeTsInput<'a> {
src: RopeSlice<'a>,
cursor: regex_cursor::RopeyCursor<'a>,
}
impl<'a> RopeTsInput<'a> {
pub fn new(src: RopeSlice<'a>) -> Self {
RopeTsInput {
src,
cursor: regex_cursor::RopeyCursor::new(src),
}
}
}
impl<'a> IntoTsInput for RopeSlice<'a> {
type TsInput = RopeTsInput<'a>;
fn into_ts_input(self) -> Self::TsInput {
RopeTsInput {
src: self,
cursor: RopeyCursor::new(self),
}
}
}
impl<'a> TsInput for RopeTsInput<'a> {
type Cursor = RopeyCursor<'a>;
fn cursor_at(&mut self, offset: usize) -> &mut RopeyCursor<'a> {
// this cursor is optimized for contigous reads which are by far the most common during parsing
// very far jumps (like injections at the other end of the document) are handelde
// by starting a new cursor (new chunks iterator)
if offset < self.cursor.offset() || self.cursor.offset() - offset > 4906 {
self.cursor = regex_cursor::RopeyCursor::at(self.src, offset);
} else {
while self.cursor.offset() + self.cursor.chunk().len() >= offset {
if !self.cursor.advance() {
break;
}
}
}
&mut self.cursor
}
fn eq(&mut self, range1: ops::Range<usize>, range2: ops::Range<usize>) -> bool {
self.src.byte_slice(range1) == self.src.byte_slice(range2)
}
}

View File

@@ -0,0 +1,80 @@
use std::fmt;
use std::ptr::NonNull;
use crate::tree_sitter::syntax_tree_node::{SyntaxTreeNode, SyntaxTreeNodeRaw};
use crate::tree_sitter::Point;
// opaque pointers
pub(super) enum SyntaxTreeData {}
pub struct SyntaxTree {
ptr: NonNull<SyntaxTreeData>,
}
impl SyntaxTree {
pub(super) unsafe fn from_raw(raw: NonNull<SyntaxTreeData>) -> SyntaxTree {
SyntaxTree { ptr: raw }
}
pub(super) fn as_raw(&self) -> NonNull<SyntaxTreeData> {
self.ptr
}
pub fn root_node(&self) -> SyntaxTreeNode<'_> {
unsafe { SyntaxTreeNode::from_raw(ts_tree_root_node(self.ptr)).unwrap() }
}
pub fn edit(&mut self, edit: &InputEdit) {
unsafe { ts_tree_edit(self.ptr, edit) }
}
}
impl fmt::Debug for SyntaxTree {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "{{Tree {:?}}}", self.root_node())
}
}
impl Drop for SyntaxTree {
fn drop(&mut self) {
unsafe { ts_tree_delete(self.ptr) }
}
}
impl Clone for SyntaxTree {
fn clone(&self) -> Self {
unsafe {
SyntaxTree {
ptr: ts_tree_copy(self.ptr),
}
}
}
}
#[repr(C)]
#[derive(Debug, Copy, Clone)]
pub struct InputEdit {
pub start_byte: u32,
pub old_end_byte: u32,
pub new_end_byte: u32,
pub start_point: Point,
pub old_end_point: Point,
pub new_end_point: Point,
}
extern "C" {
/// Create a shallow copy of the syntax tree. This is very fast. You need to
/// copy a syntax tree in order to use it on more than one thread at a time,
/// as syntax trees are not thread safe.
fn ts_tree_copy(self_: NonNull<SyntaxTreeData>) -> NonNull<SyntaxTreeData>;
/// Delete the syntax tree, freeing all of the memory that it used.
fn ts_tree_delete(self_: NonNull<SyntaxTreeData>);
/// Get the root node of the syntax tree.
fn ts_tree_root_node<'tree>(self_: NonNull<SyntaxTreeData>) -> SyntaxTreeNodeRaw;
/// Edit the syntax tree to keep it in sync with source code that has been
/// edited.
///
/// You must describe the edit both in terms of byte offsets and in terms of
/// row/column coordinates.
fn ts_tree_edit(self_: NonNull<SyntaxTreeData>, edit: &InputEdit);
}

View File

@@ -0,0 +1,292 @@
use std::ffi::c_void;
use std::marker::PhantomData;
use std::ops::Range;
use std::ptr::NonNull;
use crate::tree_sitter::syntax_tree::SyntaxTree;
use crate::tree_sitter::Grammar;
#[repr(C)]
#[derive(Debug, Clone, Copy)]
pub(super) struct SyntaxTreeNodeRaw {
context: [u32; 4],
id: *const c_void,
tree: *const c_void,
}
impl From<SyntaxTreeNode<'_>> for SyntaxTreeNodeRaw {
fn from(node: SyntaxTreeNode) -> SyntaxTreeNodeRaw {
SyntaxTreeNodeRaw {
context: node.context,
id: node.id.as_ptr(),
tree: node.tree.as_ptr(),
}
}
}
#[derive(Debug, Clone)]
#[repr(C)]
pub struct SyntaxTreeNode<'tree> {
context: [u32; 4],
id: NonNull<c_void>,
tree: NonNull<c_void>,
_phantom: PhantomData<&'tree SyntaxTree>,
}
impl<'tree> SyntaxTreeNode<'tree> {
#[inline]
pub(super) unsafe fn from_raw(raw: SyntaxTreeNodeRaw) -> Option<Self> {
Some(SyntaxTreeNode {
context: raw.context,
id: NonNull::new(raw.id as *mut _)?,
tree: unsafe { NonNull::new_unchecked(raw.tree as *mut _) },
_phantom: PhantomData,
})
}
#[inline]
pub(crate) fn as_raw(&self) -> SyntaxTreeNodeRaw {
SyntaxTreeNodeRaw {
context: self.context,
id: self.id.as_ptr(),
tree: self.tree.as_ptr(),
}
}
/// Get this node's type as a numerical id.
#[inline]
pub fn kind_id(&self) -> u16 {
unsafe { ts_node_symbol(self.as_raw()) }
}
/// Get the [`Language`] that was used to parse this node's syntax tree.
#[inline]
pub fn grammar(&self) -> Grammar {
unsafe { ts_node_language(self.as_raw()) }
}
/// Check if this node is *named*.
///
/// Named nodes correspond to named rules in the grammar, whereas
/// *anonymous* nodes correspond to string literals in the grammar.
#[inline]
pub fn is_named(&self) -> bool {
unsafe { ts_node_is_named(self.as_raw()) }
}
/// Check if this node is *missing*.
///
/// Missing nodes are inserted by the parser in order to recover from
/// certain kinds of syntax errors.
#[inline]
pub fn is_missing(&self) -> bool {
unsafe { ts_node_is_missing(self.as_raw()) }
}
/// Get the byte offsets where this node starts.
#[inline]
pub fn start_byte(&self) -> usize {
unsafe { ts_node_start_byte(self.as_raw()) as usize }
}
/// Get the byte offsets where this node end.
#[inline]
pub fn end_byte(&self) -> usize {
unsafe { ts_node_end_byte(self.as_raw()) as usize }
}
/// Get the byte range of source code that this node represents.
// TODO: use helix_stdx::Range once available
#[inline]
pub fn byte_range(&self) -> Range<usize> {
self.start_byte()..self.end_byte()
}
/// Get the node's child at the given index, where zero represents the first
/// child.
///
/// This method is fairly fast, but its cost is technically log(i), so if
/// you might be iterating over a long list of children, you should use
/// [`SyntaxTreeNode::children`] instead.
#[inline]
pub fn child(&self, i: usize) -> Option<SyntaxTreeNode<'tree>> {
unsafe { SyntaxTreeNode::from_raw(ts_node_child(self.as_raw(), i as u32)) }
}
/// Get this node's number of children.
#[inline]
pub fn child_count(&self) -> usize {
unsafe { ts_node_child_count(self.as_raw()) as usize }
}
/// Get this node's *named* child at the given index.
///
/// See also [`SyntaxTreeNode::is_named`].
/// This method is fairly fast, but its cost is technically log(i), so if
/// you might be iterating over a long list of children, you should use
/// [`SyntaxTreeNode::named_children`] instead.
#[inline]
pub fn named_child(&self, i: usize) -> Option<SyntaxTreeNode<'tree>> {
unsafe { SyntaxTreeNode::from_raw(ts_node_named_child(self.as_raw(), i as u32)) }
}
/// Get this node's number of *named* children.
///
/// See also [`SyntaxTreeNode::is_named`].
#[inline]
pub fn named_child_count(&self) -> usize {
unsafe { ts_node_named_child_count(self.as_raw()) as usize }
}
#[inline]
unsafe fn map(
&self,
f: unsafe extern "C" fn(SyntaxTreeNodeRaw) -> SyntaxTreeNodeRaw,
) -> Option<SyntaxTreeNode<'tree>> {
SyntaxTreeNode::from_raw(f(self.as_raw()))
}
/// Get this node's immediate parent.
#[inline]
pub fn parent(&self) -> Option<Self> {
unsafe { self.map(ts_node_parent) }
}
/// Get this node's next sibling.
#[inline]
pub fn next_sibling(&self) -> Option<Self> {
unsafe { self.map(ts_node_next_sibling) }
}
/// Get this node's previous sibling.
#[inline]
pub fn prev_sibling(&self) -> Option<Self> {
unsafe { self.map(ts_node_prev_sibling) }
}
/// Get this node's next named sibling.
#[inline]
pub fn next_named_sibling(&self) -> Option<Self> {
unsafe { self.map(ts_node_next_named_sibling) }
}
/// Get this node's previous named sibling.
#[inline]
pub fn prev_named_sibling(&self) -> Option<Self> {
unsafe { self.map(ts_node_prev_named_sibling) }
}
/// Get the smallest node within this node that spans the given range.
#[inline]
pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Self> {
unsafe {
Self::from_raw(ts_node_descendant_for_byte_range(
self.as_raw(),
start as u32,
end as u32,
))
}
}
/// Get the smallest named node within this node that spans the given range.
#[inline]
pub fn named_descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Self> {
unsafe {
Self::from_raw(ts_node_named_descendant_for_byte_range(
self.as_raw(),
start as u32,
end as u32,
))
}
}
// /// Iterate over this node's children.
// ///
// /// A [`TreeCursor`] is used to retrieve the children efficiently. Obtain
// /// a [`TreeCursor`] by calling [`Tree::walk`] or [`SyntaxTreeNode::walk`]. To avoid
// /// unnecessary allocations, you should reuse the same cursor for
// /// subsequent calls to this method.
// ///
// /// If you're walking the tree recursively, you may want to use the
// /// [`TreeCursor`] APIs directly instead.
// pub fn children<'cursor>(
// &self,
// cursor: &'cursor mut TreeCursor<'tree>,
// ) -> impl ExactSizeIterator<Item = SyntaxTreeNode<'tree>> + 'cursor {
// cursor.reset(self.to_raw());
// cursor.goto_first_child();
// (0..self.child_count()).map(move |_| {
// let result = cursor.node();
// cursor.goto_next_sibling();
// result
// })
// }
}
unsafe impl Send for SyntaxTreeNode<'_> {}
unsafe impl Sync for SyntaxTreeNode<'_> {}
extern "C" {
/// Get the node's type as a numerical id.
fn ts_node_symbol(node: SyntaxTreeNodeRaw) -> u16;
/// Get the node's language.
fn ts_node_language(node: SyntaxTreeNodeRaw) -> Grammar;
/// Check if the node is *named*. Named nodes correspond to named rules in
/// the grammar, whereas *anonymous* nodes correspond to string literals in
/// the grammar
fn ts_node_is_named(node: SyntaxTreeNodeRaw) -> bool;
/// Check if the node is *missing*. Missing nodes are inserted by the parser
/// in order to recover from certain kinds of syntax errors
fn ts_node_is_missing(node: SyntaxTreeNodeRaw) -> bool;
/// Get the node's immediate parent
fn ts_node_parent(node: SyntaxTreeNodeRaw) -> SyntaxTreeNodeRaw;
/// Get the node's child at the given index, where zero represents the first
/// child
fn ts_node_child(node: SyntaxTreeNodeRaw, child_index: u32) -> SyntaxTreeNodeRaw;
/// Get the node's number of children
fn ts_node_child_count(node: SyntaxTreeNodeRaw) -> u32;
/// Get the node's *named* child at the given index. See also
/// [`ts_node_is_named`]
fn ts_node_named_child(node: SyntaxTreeNodeRaw, child_index: u32) -> SyntaxTreeNodeRaw;
/// Get the node's number of *named* children. See also [`ts_node_is_named`]
fn ts_node_named_child_count(node: SyntaxTreeNodeRaw) -> u32;
/// Get the node's next sibling
fn ts_node_next_sibling(node: SyntaxTreeNodeRaw) -> SyntaxTreeNodeRaw;
fn ts_node_prev_sibling(node: SyntaxTreeNodeRaw) -> SyntaxTreeNodeRaw;
/// Get the node's next *named* sibling
fn ts_node_next_named_sibling(node: SyntaxTreeNodeRaw) -> SyntaxTreeNodeRaw;
fn ts_node_prev_named_sibling(node: SyntaxTreeNodeRaw) -> SyntaxTreeNodeRaw;
/// Get the smallest node within this node that spans the given range of
/// bytes or (row, column) positions
fn ts_node_descendant_for_byte_range(
node: SyntaxTreeNodeRaw,
start: u32,
end: u32,
) -> SyntaxTreeNodeRaw;
/// Get the smallest named node within this node that spans the given range
/// of bytes or (row, column) positions
fn ts_node_named_descendant_for_byte_range(
node: SyntaxTreeNodeRaw,
start: u32,
end: u32,
) -> SyntaxTreeNodeRaw;
/// Get the node's start byte.
fn ts_node_start_byte(self_: SyntaxTreeNodeRaw) -> u32;
/// Get the node's end byte.
fn ts_node_end_byte(node: SyntaxTreeNodeRaw) -> u32;
}

View File

@@ -14,7 +14,7 @@ homepage.workspace = true
[features]
default = ["git"]
unicode-lines = ["helix-core/unicode-lines"]
unicode-lines = ["helix-core/unicode-lines", "helix-view/unicode-lines"]
integration = ["helix-event/integration_test"]
git = ["helix-vcs/git"]
@@ -53,13 +53,14 @@ log = "0.4"
nucleo.workspace = true
ignore = "0.4"
# markdown doc rendering
pulldown-cmark = { version = "0.10", default-features = false }
pulldown-cmark = { version = "0.11", default-features = false }
# file type detection
content_inspector = "0.2.4"
thiserror = "1.0"
# opening URLs
open = "5.1.2"
url = "2.5.0"
open = "5.2.0"
url = "2.5.2"
# config
toml = "0.8"
@@ -73,7 +74,7 @@ grep-searcher = "0.1.13"
[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.153"
libc = "0.2.155"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }

View File

@@ -9,7 +9,7 @@ use helix_lsp::{
use helix_stdx::path::get_relative_path;
use helix_view::{
align_view,
document::DocumentSavedEventResult,
document::{DocumentOpenError, DocumentSavedEventResult},
editor::{ConfigEvent, EditorEvent},
graphics::Rect,
theme,
@@ -186,9 +186,15 @@ impl Application {
Some(Layout::Horizontal) => Action::HorizontalSplit,
None => Action::Load,
};
let doc_id = editor
.open(&file, action)
.context(format!("open '{}'", file.to_string_lossy()))?;
let doc_id = match editor.open(&file, action) {
// Ignore irregular files during application init.
Err(DocumentOpenError::IrregularFile) => {
nr_of_files -= 1;
continue;
}
Err(err) => return Err(anyhow::anyhow!(err)),
Ok(doc_id) => doc_id,
};
// with Action::Load all documents have the same view
// NOTE: this isn't necessarily true anymore. If
// `--vsplit` or `--hsplit` are used, the file which is
@@ -199,15 +205,21 @@ impl Application {
doc.set_selection(view_id, pos);
}
}
editor.set_status(format!(
"Loaded {} file{}.",
nr_of_files,
if nr_of_files == 1 { "" } else { "s" } // avoid "Loaded 1 files." grammo
));
// align the view to center after all files are loaded,
// does not affect views without pos since it is at the top
let (view, doc) = current!(editor);
align_view(doc, view, Align::Center);
// if all files were invalid, replace with empty buffer
if nr_of_files == 0 {
editor.new_file(Action::VerticalSplit);
} else {
editor.set_status(format!(
"Loaded {} file{}.",
nr_of_files,
if nr_of_files == 1 { "" } else { "s" } // avoid "Loaded 1 files." grammo
));
// align the view to center after all files are loaded,
// does not affect views without pos since it is at the top
let (view, doc) = current!(editor);
align_view(doc, view, Align::Center);
}
} else {
editor.new_file(Action::VerticalSplit);
}
@@ -723,10 +735,10 @@ impl Application {
}
}
Notification::PublishDiagnostics(mut params) => {
let path = match params.uri.to_file_path() {
Ok(path) => helix_stdx::path::normalize(&path),
Err(_) => {
log::error!("Unsupported file URI: {}", params.uri);
let uri = match helix_core::Uri::try_from(params.uri) {
Ok(uri) => uri,
Err(err) => {
log::error!("{err}");
return;
}
};
@@ -737,11 +749,11 @@ impl Application {
}
// have to inline the function because of borrow checking...
let doc = self.editor.documents.values_mut()
.find(|doc| doc.path().map(|p| p == &path).unwrap_or(false))
.find(|doc| doc.uri().is_some_and(|u| u == uri))
.filter(|doc| {
if let Some(version) = params.version {
if version != doc.version() {
log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
log::info!("Version ({version}) is out of date for {uri:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
return false;
}
}
@@ -753,7 +765,7 @@ impl Application {
let lang_conf = doc.language.clone();
if let Some(lang_conf) = &lang_conf {
if let Some(old_diagnostics) = self.editor.diagnostics.get(&path) {
if let Some(old_diagnostics) = self.editor.diagnostics.get(&uri) {
if !lang_conf.persistent_diagnostic_sources.is_empty() {
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
@@ -786,7 +798,7 @@ impl Application {
// Insert the original lsp::Diagnostics here because we may have no open document
// for diagnosic message and so we can't calculate the exact position.
// When using them later in the diagnostics picker, we calculate them on-demand.
let diagnostics = match self.editor.diagnostics.entry(path) {
let diagnostics = match self.editor.diagnostics.entry(uri) {
Entry::Occupied(o) => {
let current_diagnostics = o.into_mut();
// there may entries of other language servers, which is why we can't overwrite the whole entry
@@ -1120,20 +1132,22 @@ impl Application {
..
} = params;
let path = match uri.to_file_path() {
Ok(path) => path,
let uri = match helix_core::Uri::try_from(uri) {
Ok(uri) => uri,
Err(err) => {
log::error!("unsupported file URI: {}: {:?}", uri, err);
log::error!("{err}");
return lsp::ShowDocumentResult { success: false };
}
};
// If `Uri` gets another variant other than `Path` this may not be valid.
let path = uri.as_path().expect("URIs are valid paths");
let action = match take_focus {
Some(true) => helix_view::editor::Action::Replace,
_ => helix_view::editor::Action::VerticalSplit,
};
let doc_id = match self.editor.open(&path, action) {
let doc_id = match self.editor.open(path, action) {
Ok(id) => id,
Err(err) => {
log::error!("failed to open path: {:?}: {:?}", uri, err);

File diff suppressed because it is too large Load Diff

View File

@@ -8,11 +8,11 @@ use dap::{StackFrame, Thread, ThreadStates};
use helix_core::syntax::{DebugArgumentValue, DebugConfigCompletion, DebugTemplate};
use helix_dap::{self as dap, Client};
use helix_lsp::block_on;
use helix_view::{editor::Breakpoint, graphics::Margin};
use helix_view::editor::Breakpoint;
use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream;
use tui::{text::Spans, widgets::Row};
use tui::text::Spans;
use std::collections::HashMap;
use std::future::Future;
@@ -22,38 +22,6 @@ use anyhow::{anyhow, bail};
use helix_view::handlers::dap::{breakpoints_changed, jump_to_stack_frame, select_thread_id};
impl ui::menu::Item for StackFrame {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
self.name.as_str().into() // TODO: include thread_states in the label
}
}
impl ui::menu::Item for DebugTemplate {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
self.name.as_str().into()
}
}
impl ui::menu::Item for Thread {
type Data = ThreadStates;
fn format(&self, thread_states: &Self::Data) -> Row {
format!(
"{} ({})",
self.name,
thread_states
.get(&self.id)
.map(|state| state.as_str())
.unwrap_or("unknown")
)
.into()
}
}
fn thread_picker(
cx: &mut Context,
callback_fn: impl Fn(&mut Editor, &dap::Thread) + Send + 'static,
@@ -73,13 +41,27 @@ fn thread_picker(
let debugger = debugger!(editor);
let thread_states = debugger.thread_states.clone();
let picker = Picker::new(threads, thread_states, move |cx, thread, _action| {
callback_fn(cx.editor, thread)
})
let columns = [
ui::PickerColumn::new("name", |item: &Thread, _| item.name.as_str().into()),
ui::PickerColumn::new("state", |item: &Thread, thread_states: &ThreadStates| {
thread_states
.get(&item.id)
.map(|state| state.as_str())
.unwrap_or("unknown")
.into()
}),
];
let picker = Picker::new(
columns,
0,
threads,
thread_states,
move |cx, thread, _action| callback_fn(cx.editor, thread),
)
.with_preview(move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
let frame = frames.first()?;
let path = frame.source.as_ref()?.path.clone()?;
let path = frame.source.as_ref()?.path.as_ref()?.as_path();
let pos = Some((
frame.line.saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1),
@@ -172,9 +154,9 @@ pub fn dap_start_impl(
let mut args: HashMap<&str, Value> = HashMap::new();
if let Some(params) = params {
for (k, t) in &template.args {
let mut value = t.clone();
for (k, t) in &template.args {
let mut value = t.clone();
if let Some(ref params) = params {
for (i, x) in params.iter().enumerate() {
let mut param = x.to_string();
if let Some(DebugConfigCompletion::Advanced(cfg)) = template.completion.get(i) {
@@ -198,22 +180,22 @@ pub fn dap_start_impl(
DebugArgumentValue::Boolean(_) => value,
};
}
}
match value {
DebugArgumentValue::String(string) => {
if let Ok(integer) = string.parse::<usize>() {
args.insert(k, to_value(integer).unwrap());
} else {
args.insert(k, to_value(string).unwrap());
}
}
DebugArgumentValue::Array(arr) => {
args.insert(k, to_value(arr).unwrap());
}
DebugArgumentValue::Boolean(bool) => {
args.insert(k, to_value(bool).unwrap());
match value {
DebugArgumentValue::String(string) => {
if let Ok(integer) = string.parse::<usize>() {
args.insert(k, to_value(integer).unwrap());
} else {
args.insert(k, to_value(string).unwrap());
}
}
DebugArgumentValue::Array(arr) => {
args.insert(k, to_value(arr).unwrap());
}
DebugArgumentValue::Boolean(bool) => {
args.insert(k, to_value(bool).unwrap());
}
}
}
@@ -268,21 +250,34 @@ pub fn dap_launch(cx: &mut Context) {
let templates = config.templates.clone();
let columns = [ui::PickerColumn::new(
"template",
|item: &DebugTemplate, _| item.name.as_str().into(),
)];
cx.push_layer(Box::new(overlaid(Picker::new(
columns,
0,
templates,
(),
|cx, template, _action| {
let completions = template.completion.clone();
let name = template.name.clone();
let callback = Box::pin(async move {
let call: Callback =
Callback::EditorCompositor(Box::new(move |_editor, compositor| {
let prompt = debug_parameter_prompt(completions, name, Vec::new());
compositor.push(Box::new(prompt));
}));
Ok(call)
});
cx.jobs.callback(callback);
if template.completion.is_empty() {
if let Err(err) = dap_start_impl(cx, Some(&template.name), None, None) {
cx.editor.set_error(err.to_string());
}
} else {
let completions = template.completion.clone();
let name = template.name.clone();
let callback = Box::pin(async move {
let call: Callback =
Callback::EditorCompositor(Box::new(move |_editor, compositor| {
let prompt = debug_parameter_prompt(completions, name, Vec::new());
compositor.push(Box::new(prompt));
}));
Ok(call)
});
cx.jobs.callback(callback);
}
},
))));
}
@@ -581,12 +576,7 @@ pub fn dap_variables(cx: &mut Context) {
}
let contents = Text::from(tui::text::Text::from(variables));
let margin = if cx.editor.popup_border() {
Margin::all(1)
} else {
Margin::none()
};
let popup = Popup::new("dap-variables", contents).margin(margin);
let popup = Popup::new("dap-variables", contents);
cx.replace_or_push_layer("dap-variables", popup);
}
@@ -735,7 +725,10 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
let frames = debugger.stack_frames[&thread_id].clone();
let picker = Picker::new(frames, (), move |cx, frame, _action| {
let columns = [ui::PickerColumn::new("frame", |item: &StackFrame, _| {
item.name.as_str().into() // TODO: include thread_states in the label
})];
let picker = Picker::new(columns, 0, frames, (), move |cx, frame, _action| {
let debugger = debugger!(cx.editor);
// TODO: this should be simpler to find
let pos = debugger.stack_frames[&thread_id]
@@ -754,10 +747,10 @@ pub fn dap_switch_stack_frame(cx: &mut Context) {
frame
.source
.as_ref()
.and_then(|source| source.path.clone())
.and_then(|source| source.path.as_ref())
.map(|path| {
(
path.into(),
path.as_path().into(),
Some((
frame.line.saturating_sub(1),
frame.end_line.unwrap_or(frame.line).saturating_sub(1),

View File

@@ -9,19 +9,17 @@ use helix_lsp::{
Client, LanguageServerId, OffsetEncoding,
};
use tokio_stream::StreamExt;
use tui::{
text::{Span, Spans},
widgets::Row,
};
use tui::{text::Span, widgets::Row};
use super::{align_view, push_jump, Align, Context, Editor};
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection};
use helix_core::{
syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection, Uri,
};
use helix_stdx::path;
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId},
editor::Action,
graphics::Margin,
handlers::lsp::SignatureHelpInvoked,
theme::Style,
Document, View,
@@ -30,7 +28,7 @@ use helix_view::{
use crate::{
compositor::{self, Compositor},
job::Callback,
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
ui::{self, overlay::overlaid, FileLocation, Picker, Popup, PromptEvent},
};
use std::{
@@ -38,7 +36,7 @@ use std::{
collections::{BTreeMap, HashSet},
fmt::Write,
future::Future,
path::{Path, PathBuf},
path::Path,
};
/// Gets the first language server that is attached to a document which supports a specific feature.
@@ -63,67 +61,10 @@ macro_rules! language_server_with_feature {
}};
}
impl ui::menu::Item for lsp::Location {
/// Current working directory.
type Data = PathBuf;
fn format(&self, cwdir: &Self::Data) -> Row {
// The preallocation here will overallocate a few characters since it will account for the
// URL's scheme, which is not used most of the time since that scheme will be "file://".
// Those extra chars will be used to avoid allocating when writing the line number (in the
// common case where it has 5 digits or less, which should be enough for a cast majority
// of usages).
let mut res = String::with_capacity(self.uri.as_str().len());
if self.uri.scheme() == "file" {
// With the preallocation above and UTF-8 paths already, this closure will do one (1)
// allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
let mut write_path_to_res = || -> Option<()> {
let path = self.uri.to_file_path().ok()?;
res.push_str(&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy());
Some(())
};
write_path_to_res();
} else {
// Never allocates since we declared the string with this capacity already.
res.push_str(self.uri.as_str());
}
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
// is a simple `/` and not `C:\` (with whatever drive letter)
write!(&mut res, ":{}", self.range.start.line + 1)
.expect("Will only failed if allocating fail");
res.into()
}
}
struct SymbolInformationItem {
symbol: lsp::SymbolInformation,
offset_encoding: OffsetEncoding,
}
impl ui::menu::Item for SymbolInformationItem {
/// Path to currently focussed document
type Data = Option<lsp::Url>;
fn format(&self, current_doc_path: &Self::Data) -> Row {
if current_doc_path.as_ref() == Some(&self.symbol.location.uri) {
self.symbol.name.as_str().into()
} else {
match self.symbol.location.uri.to_file_path() {
Ok(path) => {
let get_relative_path = path::get_relative_path(path.as_path());
format!(
"{} ({})",
&self.symbol.name,
get_relative_path.to_string_lossy()
)
.into()
}
Err(_) => format!("{} ({})", &self.symbol.name, &self.symbol.location.uri).into(),
}
}
}
uri: Uri,
}
struct DiagnosticStyles {
@@ -134,60 +75,15 @@ struct DiagnosticStyles {
}
struct PickerDiagnostic {
path: PathBuf,
uri: Uri,
diag: lsp::Diagnostic,
offset_encoding: OffsetEncoding,
}
impl ui::menu::Item for PickerDiagnostic {
type Data = (DiagnosticStyles, DiagnosticsFormat);
fn format(&self, (styles, format): &Self::Data) -> Row {
let mut style = self
.diag
.severity
.map(|s| match s {
DiagnosticSeverity::HINT => styles.hint,
DiagnosticSeverity::INFORMATION => styles.info,
DiagnosticSeverity::WARNING => styles.warning,
DiagnosticSeverity::ERROR => styles.error,
_ => Style::default(),
})
.unwrap_or_default();
// remove background as it is distracting in the picker list
style.bg = None;
let code = match self.diag.code.as_ref() {
Some(NumberOrString::Number(n)) => format!(" ({n})"),
Some(NumberOrString::String(s)) => format!(" ({s})"),
None => String::new(),
};
let path = match format {
DiagnosticsFormat::HideSourcePath => String::new(),
DiagnosticsFormat::ShowSourcePath => {
let path = path::get_truncated_path(&self.path);
format!("{}: ", path.to_string_lossy())
}
};
Spans::from(vec![
Span::raw(path),
Span::styled(&self.diag.message, style),
Span::styled(code, style),
])
.into()
}
}
fn location_to_file_location(location: &lsp::Location) -> FileLocation {
let path = location.uri.to_file_path().unwrap();
let line = Some((
location.range.start.line as usize,
location.range.end.line as usize,
));
(path.into(), line)
fn uri_to_file_location<'a>(uri: &'a Uri, range: &lsp::Range) -> Option<FileLocation<'a>> {
let path = uri.as_path()?;
let line = Some((range.start.line as usize, range.end.line as usize));
Some((path.into(), line))
}
fn jump_to_location(
@@ -242,20 +138,39 @@ fn jump_to_position(
}
}
type SymbolPicker = Picker<SymbolInformationItem>;
fn sym_picker(symbols: Vec<SymbolInformationItem>, current_path: Option<lsp::Url>) -> SymbolPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
Picker::new(symbols, current_path, move |cx, item, action| {
jump_to_location(
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
})
.with_preview(move |_editor, item| Some(location_to_file_location(&item.symbol.location)))
.truncate_start(false)
fn display_symbol_kind(kind: lsp::SymbolKind) -> &'static str {
match kind {
lsp::SymbolKind::FILE => "file",
lsp::SymbolKind::MODULE => "module",
lsp::SymbolKind::NAMESPACE => "namespace",
lsp::SymbolKind::PACKAGE => "package",
lsp::SymbolKind::CLASS => "class",
lsp::SymbolKind::METHOD => "method",
lsp::SymbolKind::PROPERTY => "property",
lsp::SymbolKind::FIELD => "field",
lsp::SymbolKind::CONSTRUCTOR => "construct",
lsp::SymbolKind::ENUM => "enum",
lsp::SymbolKind::INTERFACE => "interface",
lsp::SymbolKind::FUNCTION => "function",
lsp::SymbolKind::VARIABLE => "variable",
lsp::SymbolKind::CONSTANT => "constant",
lsp::SymbolKind::STRING => "string",
lsp::SymbolKind::NUMBER => "number",
lsp::SymbolKind::BOOLEAN => "boolean",
lsp::SymbolKind::ARRAY => "array",
lsp::SymbolKind::OBJECT => "object",
lsp::SymbolKind::KEY => "key",
lsp::SymbolKind::NULL => "null",
lsp::SymbolKind::ENUM_MEMBER => "enummem",
lsp::SymbolKind::STRUCT => "struct",
lsp::SymbolKind::EVENT => "event",
lsp::SymbolKind::OPERATOR => "operator",
lsp::SymbolKind::TYPE_PARAMETER => "typeparam",
_ => {
log::warn!("Unknown symbol kind: {:?}", kind);
""
}
}
}
#[derive(Copy, Clone, PartialEq)]
@@ -264,22 +179,24 @@ enum DiagnosticsFormat {
HideSourcePath,
}
type DiagnosticsPicker = Picker<PickerDiagnostic, DiagnosticStyles>;
fn diag_picker(
cx: &Context,
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
diagnostics: BTreeMap<Uri, Vec<(lsp::Diagnostic, LanguageServerId)>>,
format: DiagnosticsFormat,
) -> Picker<PickerDiagnostic> {
) -> DiagnosticsPicker {
// TODO: drop current_path comparison and instead use workspace: bool flag?
// flatten the map to a vec of (url, diag) pairs
let mut flat_diag = Vec::new();
for (path, diags) in diagnostics {
for (uri, diags) in diagnostics {
flat_diag.reserve(diags.len());
for (diag, ls) in diags {
if let Some(ls) = cx.editor.language_server_by_id(ls) {
flat_diag.push(PickerDiagnostic {
path: path.clone(),
uri: uri.clone(),
diag,
offset_encoding: ls.offset_encoding(),
});
@@ -294,22 +211,72 @@ fn diag_picker(
error: cx.editor.theme.get("error"),
};
let mut columns = vec![
ui::PickerColumn::new(
"severity",
|item: &PickerDiagnostic, styles: &DiagnosticStyles| {
match item.diag.severity {
Some(DiagnosticSeverity::HINT) => Span::styled("HINT", styles.hint),
Some(DiagnosticSeverity::INFORMATION) => Span::styled("INFO", styles.info),
Some(DiagnosticSeverity::WARNING) => Span::styled("WARN", styles.warning),
Some(DiagnosticSeverity::ERROR) => Span::styled("ERROR", styles.error),
_ => Span::raw(""),
}
.into()
},
),
ui::PickerColumn::new("code", |item: &PickerDiagnostic, _| {
match item.diag.code.as_ref() {
Some(NumberOrString::Number(n)) => n.to_string().into(),
Some(NumberOrString::String(s)) => s.as_str().into(),
None => "".into(),
}
}),
ui::PickerColumn::new("message", |item: &PickerDiagnostic, _| {
item.diag.message.as_str().into()
}),
];
let mut primary_column = 2; // message
if format == DiagnosticsFormat::ShowSourcePath {
columns.insert(
// between message code and message
2,
ui::PickerColumn::new("path", |item: &PickerDiagnostic, _| {
if let Some(path) = item.uri.as_path() {
path::get_truncated_path(path)
.to_string_lossy()
.to_string()
.into()
} else {
Default::default()
}
}),
);
primary_column += 1;
}
Picker::new(
columns,
primary_column,
flat_diag,
(styles, format),
styles,
move |cx,
PickerDiagnostic {
path,
uri,
diag,
offset_encoding,
},
action| {
let Some(path) = uri.as_path() else {
return;
};
jump_to_position(cx.editor, path, diag.range, *offset_encoding, action)
},
)
.with_preview(move |_editor, PickerDiagnostic { path, diag, .. }| {
.with_preview(move |_editor, PickerDiagnostic { uri, diag, .. }| {
let line = Some((diag.range.start.line as usize, diag.range.end.line as usize));
Some((path.clone().into(), line))
Some((uri.as_path()?.into(), line))
})
.truncate_start(false)
}
@@ -318,6 +285,7 @@ pub fn symbol_picker(cx: &mut Context) {
fn nested_to_flat(
list: &mut Vec<SymbolInformationItem>,
file: &lsp::TextDocumentIdentifier,
uri: &Uri,
symbol: lsp::DocumentSymbol,
offset_encoding: OffsetEncoding,
) {
@@ -332,9 +300,10 @@ pub fn symbol_picker(cx: &mut Context) {
container_name: None,
},
offset_encoding,
uri: uri.clone(),
});
for child in symbol.children.into_iter().flatten() {
nested_to_flat(list, file, child, offset_encoding);
nested_to_flat(list, file, uri, child, offset_encoding);
}
}
let doc = doc!(cx.editor);
@@ -348,6 +317,9 @@ pub fn symbol_picker(cx: &mut Context) {
let request = language_server.document_symbols(doc.identifier()).unwrap();
let offset_encoding = language_server.offset_encoding();
let doc_id = doc.identifier();
let doc_uri = doc
.uri()
.expect("docs with active language servers must be backed by paths");
async move {
let json = request.await?;
@@ -362,6 +334,7 @@ pub fn symbol_picker(cx: &mut Context) {
lsp::DocumentSymbolResponse::Flat(symbols) => symbols
.into_iter()
.map(|symbol| SymbolInformationItem {
uri: doc_uri.clone(),
symbol,
offset_encoding,
})
@@ -369,7 +342,13 @@ pub fn symbol_picker(cx: &mut Context) {
lsp::DocumentSymbolResponse::Nested(symbols) => {
let mut flat_symbols = Vec::new();
for symbol in symbols {
nested_to_flat(&mut flat_symbols, &doc_id, symbol, offset_encoding)
nested_to_flat(
&mut flat_symbols,
&doc_id,
&doc_uri,
symbol,
offset_encoding,
)
}
flat_symbols
}
@@ -378,7 +357,6 @@ pub fn symbol_picker(cx: &mut Context) {
}
})
.collect();
let current_url = doc.url();
if futures.is_empty() {
cx.editor
@@ -393,7 +371,37 @@ pub fn symbol_picker(cx: &mut Context) {
symbols.append(&mut lsp_items);
}
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = sym_picker(symbols, current_url);
let columns = [
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
display_symbol_kind(item.symbol.kind).into()
}),
// Some symbols in the document symbol picker may have a URI that isn't
// the current file. It should be rare though, so we concatenate that
// URI in with the symbol name in this picker.
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
item.symbol.name.as_str().into()
}),
];
let picker = Picker::new(
columns,
1, // name column
symbols,
(),
move |cx, item, action| {
jump_to_location(
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
},
)
.with_preview(move |_editor, item| {
uri_to_file_location(&item.uri, &item.symbol.location.range)
})
.truncate_start(false);
compositor.push(Box::new(overlaid(picker)))
};
@@ -402,6 +410,8 @@ pub fn symbol_picker(cx: &mut Context) {
}
pub fn workspace_symbol_picker(cx: &mut Context) {
use crate::ui::picker::Injector;
let doc = doc!(cx.editor);
if doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
@@ -413,25 +423,37 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
return;
}
let get_symbols = move |pattern: String, editor: &mut Editor| {
let get_symbols = |pattern: &str, editor: &mut Editor, _data, injector: &Injector<_, _>| {
let doc = doc!(editor);
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesOrdered<_> = doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|language_server| {
let request = language_server.workspace_symbols(pattern.clone()).unwrap();
let request = language_server
.workspace_symbols(pattern.to_string())
.unwrap();
let offset_encoding = language_server.offset_encoding();
async move {
let json = request.await?;
let response =
let response: Vec<_> =
serde_json::from_value::<Option<Vec<lsp::SymbolInformation>>>(json)?
.unwrap_or_default()
.into_iter()
.map(|symbol| SymbolInformationItem {
symbol,
offset_encoding,
.filter_map(|symbol| {
let uri = match Uri::try_from(&symbol.location.uri) {
Ok(uri) => uri,
Err(err) => {
log::warn!("discarding symbol with invalid URI: {err}");
return None;
}
};
Some(SymbolInformationItem {
symbol,
uri,
offset_encoding,
})
})
.collect();
@@ -444,44 +466,66 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
editor.set_error("No configured language server supports workspace symbols");
}
let injector = injector.clone();
async move {
let mut symbols = Vec::new();
// TODO if one symbol request errors, all other requests are discarded (even if they're valid)
while let Some(mut lsp_items) = futures.try_next().await? {
symbols.append(&mut lsp_items);
while let Some(lsp_items) = futures.try_next().await? {
for item in lsp_items {
injector.push(item)?;
}
}
anyhow::Ok(symbols)
Ok(())
}
.boxed()
};
let columns = [
ui::PickerColumn::new("kind", |item: &SymbolInformationItem, _| {
display_symbol_kind(item.symbol.kind).into()
}),
ui::PickerColumn::new("name", |item: &SymbolInformationItem, _| {
item.symbol.name.as_str().into()
})
.without_filtering(),
ui::PickerColumn::new("path", |item: &SymbolInformationItem, _| {
if let Some(path) = item.uri.as_path() {
path::get_relative_path(path)
.to_string_lossy()
.to_string()
.into()
} else {
item.symbol.location.uri.to_string().into()
}
}),
];
let current_url = doc.url();
let initial_symbols = get_symbols("".to_owned(), cx.editor);
let picker = Picker::new(
columns,
1, // name column
[],
(),
move |cx, item, action| {
jump_to_location(
cx.editor,
&item.symbol.location,
item.offset_encoding,
action,
);
},
)
.with_preview(|_editor, item| uri_to_file_location(&item.uri, &item.symbol.location.range))
.with_dynamic_query(get_symbols, None)
.truncate_start(false);
cx.jobs.callback(async move {
let symbols = initial_symbols.await?;
let call = move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = sym_picker(symbols, current_url);
let dyn_picker = DynamicPicker::new(picker, Box::new(get_symbols));
compositor.push(Box::new(overlaid(dyn_picker)))
};
Ok(Callback::EditorCompositor(Box::new(call)))
});
cx.push_layer(Box::new(overlaid(picker)));
}
pub fn diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
if let Some(current_path) = doc.path() {
let diagnostics = cx
.editor
.diagnostics
.get(current_path)
.cloned()
.unwrap_or_default();
if let Some(uri) = doc.uri() {
let diagnostics = cx.editor.diagnostics.get(&uri).cloned().unwrap_or_default();
let picker = diag_picker(
cx,
[(current_path.clone(), diagnostics)].into(),
[(uri, diagnostics)].into(),
DiagnosticsFormat::HideSourcePath,
);
cx.push_layer(Box::new(overlaid(picker)));
@@ -733,15 +777,7 @@ pub fn code_action(cx: &mut Context) {
});
picker.move_down(); // pre-select the first item
let margin = if editor.menu_border() {
Margin::vertical(1)
} else {
Margin::none()
};
let popup = Popup::new("code-action", picker)
.with_scrollbar(false)
.margin(margin);
let popup = Popup::new("code-action", picker).with_scrollbar(false);
compositor.replace_or_push("code-action", popup);
};
@@ -750,13 +786,6 @@ pub fn code_action(cx: &mut Context) {
});
}
impl ui::menu::Item for lsp::Command {
type Data = ();
fn format(&self, _data: &Self::Data) -> Row {
self.title.as_str().into()
}
}
pub fn execute_lsp_command(
editor: &mut Editor,
language_server_id: LanguageServerId,
@@ -826,10 +855,67 @@ fn goto_impl(
}
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
_locations => {
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
let columns = [ui::PickerColumn::new(
"location",
|item: &lsp::Location, cwdir: &std::path::PathBuf| {
// The preallocation here will overallocate a few characters since it will account for the
// URL's scheme, which is not used most of the time since that scheme will be "file://".
// Those extra chars will be used to avoid allocating when writing the line number (in the
// common case where it has 5 digits or less, which should be enough for a cast majority
// of usages).
let mut res = String::with_capacity(item.uri.as_str().len());
if item.uri.scheme() == "file" {
// With the preallocation above and UTF-8 paths already, this closure will do one (1)
// allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`.
if let Ok(path) = item.uri.to_file_path() {
// We don't convert to a `helix_core::Uri` here because we've already checked the scheme.
// This path won't be normalized but it's only used for display.
res.push_str(
&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy(),
);
}
} else {
// Never allocates since we declared the string with this capacity already.
res.push_str(item.uri.as_str());
}
// Most commonly, this will not allocate, especially on Unix systems where the root prefix
// is a simple `/` and not `C:\` (with whatever drive letter)
write!(&mut res, ":{}", item.range.start.line + 1)
.expect("Will only failed if allocating fail");
res.into()
},
)];
let picker = Picker::new(columns, 0, locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
})
.with_preview(move |_editor, location| Some(location_to_file_location(location)));
.with_preview(move |_editor, location| {
use crate::ui::picker::PathOrId;
let lines = Some((
location.range.start.line as usize,
location.range.end.line as usize,
));
// TODO: we should avoid allocating by doing the Uri conversion ahead of time.
//
// To do this, introduce a `Location` type in `helix-core` that reuses the core
// `Uri` type instead of the LSP `Url` type and replaces the LSP `Range` type.
// Refactor the callers of `goto_impl` to pass iterators that translate the
// LSP location type to the custom one in core, or have them collect and pass
// `Vec<Location>`s. Replace the `uri_to_file_location` function with
// `location_to_file_location` that takes only `&helix_core::Location` as
// parameters.
//
// By doing this we can also eliminate the duplicated URI info in the
// `SymbolInformationItem` type and introduce a custom Symbol type in `helix-core`
// which will be reused in the future for tree-sitter based symbol pickers.
let path = Uri::try_from(&location.uri).ok()?.as_path_buf()?;
#[allow(deprecated)]
Some((PathOrId::from_path_buf(path), lines))
});
compositor.push(Box::new(overlaid(picker)));
}
}
@@ -1038,11 +1124,12 @@ pub fn rename_symbol(cx: &mut Context) {
fn create_rename_prompt(
editor: &Editor,
prefill: String,
history_register: Option<char>,
language_server_id: Option<LanguageServerId>,
) -> Box<ui::Prompt> {
let prompt = ui::Prompt::new(
"rename-to:".into(),
None,
history_register,
ui::completers::none,
move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
if event != PromptEvent::Validate {
@@ -1079,6 +1166,7 @@ pub fn rename_symbol(cx: &mut Context) {
}
let (view, doc) = current_ref!(cx.editor);
let history_register = cx.register;
if doc
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
@@ -1121,14 +1209,14 @@ pub fn rename_symbol(cx: &mut Context) {
}
};
let prompt = create_rename_prompt(editor, prefill, Some(ls_id));
let prompt = create_rename_prompt(editor, prefill, history_register, Some(ls_id));
compositor.push(prompt);
},
);
} else {
let prefill = get_prefill_from_word_boundary(cx.editor);
let prompt = create_rename_prompt(cx.editor, prefill, None);
let prompt = create_rename_prompt(cx.editor, prefill, history_register, None);
cx.push_layer(prompt);
}
}

View File

@@ -164,9 +164,10 @@ fn buffer_close_by_ids_impl(
cx.editor.switch(*first, Action::Replace);
}
bail!(
"{} unsaved buffer(s) remaining: {:?}",
"{} unsaved buffer{} remaining: {:?}",
modified_names.len(),
modified_names
if modified_names.len() == 1 { "" } else { "s" },
modified_names,
);
}
@@ -338,9 +339,12 @@ fn write_impl(
let path = path.map(AsRef::as_ref);
if config.insert_final_newline {
insert_final_newline(doc, view);
insert_final_newline(doc, view.id);
}
// 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 callback = make_format_callback(
@@ -365,13 +369,12 @@ fn write_impl(
Ok(())
}
fn insert_final_newline(doc: &mut Document, view: &mut View) {
fn insert_final_newline(doc: &mut Document, view_id: ViewId) {
let text = doc.text();
if line_ending::get_line_ending(&text.slice(..)).is_none() {
let eof = Selection::point(text.len_chars());
let insert = Transaction::insert(text, &eof, doc.line_ending.as_str().into());
doc.apply(&insert, view.id);
doc.append_changes_to_history(view);
doc.apply(&insert, view_id);
}
}
@@ -658,9 +661,10 @@ pub(super) fn buffers_remaining_impl(editor: &mut Editor) -> anyhow::Result<()>
editor.switch(*first, Action::Replace);
}
bail!(
"{} unsaved buffer(s) remaining: {:?}",
"{} unsaved buffer{} remaining: {:?}",
modified_names.len(),
modified_names
if modified_names.len() == 1 { "" } else { "s" },
modified_names,
);
}
Ok(())
@@ -701,11 +705,15 @@ pub fn write_all_impl(
for (doc_id, target_view) in saves {
let doc = doc_mut!(cx.editor, &doc_id);
let view = view_mut!(cx.editor, target_view);
if config.insert_final_newline {
insert_final_newline(doc, view_mut!(cx.editor, target_view));
insert_final_newline(doc, target_view);
}
// 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 callback = make_format_callback(
@@ -1269,8 +1277,11 @@ fn reload(
return Ok(());
}
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
doc.reload(view, &cx.editor.diff_providers)?;
doc.reload(view, &cx.editor.diff_providers).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})?;
if let Some(path) = doc.path() {
cx.editor
.language_servers
@@ -1365,37 +1376,49 @@ fn lsp_workspace_command(
if event != PromptEvent::Validate {
return Ok(());
}
let doc = doc!(cx.editor);
let Some((language_server_id, options)) = doc
let ls_id_commands = doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
.find_map(|ls| {
.flat_map(|ls| {
ls.capabilities()
.execute_command_provider
.as_ref()
.map(|options| (ls.id(), options))
})
else {
cx.editor
.set_status("No active language servers for this document support workspace commands");
return Ok(());
};
.iter()
.flat_map(|options| options.commands.iter())
.map(|command| (ls.id(), command))
});
if args.is_empty() {
let commands = options
.commands
.iter()
.map(|command| helix_lsp::lsp::Command {
title: command.clone(),
command: command.clone(),
arguments: None,
let commands = ls_id_commands
.map(|(ls_id, command)| {
(
ls_id,
helix_lsp::lsp::Command {
title: command.clone(),
command: command.clone(),
arguments: None,
},
)
})
.collect::<Vec<_>>();
let callback = async move {
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::Picker::new(commands, (), move |cx, command, _action| {
execute_lsp_command(cx.editor, language_server_id, command.clone());
});
let columns = [ui::PickerColumn::new(
"title",
|(_ls_id, command): &(_, helix_lsp::lsp::Command), _| {
command.title.as_str().into()
},
)];
let picker = ui::Picker::new(
columns,
0,
commands,
(),
move |cx, (ls_id, command), _action| {
execute_lsp_command(cx.editor, *ls_id, command.clone());
},
);
compositor.push(Box::new(overlaid(picker)))
},
));
@@ -1404,21 +1427,32 @@ fn lsp_workspace_command(
cx.jobs.callback(callback);
} else {
let command = args.join(" ");
if options.commands.iter().any(|c| c == &command) {
execute_lsp_command(
cx.editor,
language_server_id,
helix_lsp::lsp::Command {
title: command.clone(),
arguments: None,
command,
},
);
} else {
cx.editor.set_status(format!(
"`{command}` is not supported for this language server"
));
return Ok(());
let matches: Vec<_> = ls_id_commands
.filter(|(_ls_id, c)| *c == &command)
.collect();
match matches.as_slice() {
[(ls_id, _command)] => {
execute_lsp_command(
cx.editor,
*ls_id,
helix_lsp::lsp::Command {
title: command.clone(),
arguments: None,
command,
},
);
}
[] => {
cx.editor.set_status(format!(
"`{command}` is not supported for any language server"
));
}
_ => {
cx.editor.set_status(format!(
"`{command}` supported by multiple language servers"
));
}
}
}
Ok(())
@@ -1492,6 +1526,8 @@ fn lsp_stop(
for doc in cx.editor.documents_mut() {
if let Some(client) = doc.remove_language_server_by_name(ls_name) {
doc.clear_diagnostics(Some(client.id()));
doc.reset_all_inlay_hints();
doc.inlay_hints_oudated = true;
}
}
}
@@ -2026,6 +2062,7 @@ fn sort_impl(
_args: &[Cow<str>],
reverse: bool,
) -> anyhow::Result<()> {
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
@@ -2050,6 +2087,8 @@ fn sort_impl(
);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
Ok(())
}
@@ -2063,6 +2102,7 @@ fn reflow(
return Ok(());
}
let scrolloff = cx.editor.config().scrolloff;
let cfg_text_width: usize = cx.editor.config().text_width;
let (view, doc) = current!(cx.editor);
@@ -2088,6 +2128,8 @@ fn reflow(
});
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
Ok(())
}
@@ -2287,6 +2329,7 @@ fn reset_diff_change(
ensure!(args.is_empty(), ":reset-diff-change takes no arguments");
let editor = &mut cx.editor;
let scrolloff = editor.config().scrolloff;
let (view, doc) = current!(editor);
let Some(handle) = doc.diff_handle() else {
@@ -2295,35 +2338,36 @@ fn reset_diff_change(
let diff = handle.load();
let doc_text = doc.text().slice(..);
let line = doc.selection(view.id).primary().cursor_line(doc_text);
let Some(hunk_idx) = diff.hunk_at(line as u32, true) else {
bail!("There is no change at the cursor")
};
let hunk = diff.nth_hunk(hunk_idx);
let diff_base = diff.diff_base();
let before_start = diff_base.line_to_char(hunk.before.start as usize);
let before_end = diff_base.line_to_char(hunk.before.end as usize);
let text: Tendril = diff
.diff_base()
.slice(before_start..before_end)
.chunks()
.collect();
let anchor = doc_text.line_to_char(hunk.after.start as usize);
let mut changes = 0;
let transaction = Transaction::change(
doc.text(),
[(
anchor,
doc_text.line_to_char(hunk.after.end as usize),
(!text.is_empty()).then_some(text),
)]
.into_iter(),
diff.hunks_intersecting_line_ranges(doc.selection(view.id).line_ranges(doc_text))
.map(|hunk| {
changes += 1;
let start = diff_base.line_to_char(hunk.before.start as usize);
let end = diff_base.line_to_char(hunk.before.end as usize);
let text: Tendril = diff_base.slice(start..end).chunks().collect();
(
doc_text.line_to_char(hunk.after.start as usize),
doc_text.line_to_char(hunk.after.end as usize),
(!text.is_empty()).then_some(text),
)
}),
);
if changes == 0 {
bail!("There are no changes under any selection");
}
drop(diff); // make borrow check happy
doc.apply(&transaction, view.id);
// select inserted text
let text_len = before_end - before_start;
doc.set_selection(view.id, Selection::single(anchor, anchor + text_len));
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
cx.editor.set_status(format!(
"Reset {changes} change{}",
if changes == 1 { "" } else { "s" }
));
Ok(())
}
@@ -2448,6 +2492,7 @@ fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
return Ok(());
}
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
ensure!(!args.is_empty(), "file name is expected");
@@ -2469,6 +2514,8 @@ fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
let selection = doc.selection(view.id);
let transaction = Transaction::insert(doc.text(), selection, contents);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
Ok(())
}
@@ -3175,17 +3222,9 @@ pub(super) fn command_mode(cx: &mut Context) {
let shellwords = Shellwords::from(input);
let args = shellwords.words();
let (view, doc) = current!(cx.editor);
doc.append_changes_to_history(view);
if let Err(e) = (cmd.fun)(cx, &args[1..], event) {
cx.editor.set_error(format!("{}", e));
}
if !cx.editor.tree.is_empty() {
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
}
} else if event == PromptEvent::Validate {
cx.editor
.set_error(format!("no such command: '{}'", parts[0]));

View File

@@ -5,12 +5,14 @@ use helix_event::AsyncHook;
use crate::config::Config;
use crate::events;
use crate::handlers::auto_save::AutoSaveHandler;
use crate::handlers::completion::CompletionHandler;
use crate::handlers::signature_help::SignatureHelpHandler;
pub use completion::trigger_auto_completion;
pub use helix_view::handlers::Handlers;
mod auto_save;
pub mod completion;
mod signature_help;
@@ -19,11 +21,16 @@ pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
let completions = CompletionHandler::new(config).spawn();
let signature_hints = SignatureHelpHandler::new().spawn();
let auto_save = AutoSaveHandler::new().spawn();
let handlers = Handlers {
completions,
signature_hints,
auto_save,
};
completion::register_hooks(&handlers);
signature_help::register_hooks(&handlers);
auto_save::register_hooks(&handlers);
handlers
}

View File

@@ -0,0 +1,117 @@
use std::{
sync::{
atomic::{self, AtomicBool},
Arc,
},
time::Duration,
};
use anyhow::Ok;
use arc_swap::access::Access;
use helix_event::{register_hook, send_blocking};
use helix_view::{
document::Mode,
events::DocumentDidChange,
handlers::{AutoSaveEvent, Handlers},
Editor,
};
use tokio::time::Instant;
use crate::{
commands, compositor,
events::OnModeSwitch,
job::{self, Jobs},
};
#[derive(Debug)]
pub(super) struct AutoSaveHandler {
save_pending: Arc<AtomicBool>,
}
impl AutoSaveHandler {
pub fn new() -> AutoSaveHandler {
AutoSaveHandler {
save_pending: Default::default(),
}
}
}
impl helix_event::AsyncHook for AutoSaveHandler {
type Event = AutoSaveEvent;
fn handle_event(
&mut self,
event: Self::Event,
existing_debounce: Option<tokio::time::Instant>,
) -> Option<Instant> {
match event {
Self::Event::DocumentChanged { save_after } => {
Some(Instant::now() + Duration::from_millis(save_after))
}
Self::Event::LeftInsertMode => {
if existing_debounce.is_some() {
// If the change happened more recently than the debounce, let the
// debounce run down before saving.
existing_debounce
} else {
// Otherwise if there is a save pending, save immediately.
if self.save_pending.load(atomic::Ordering::Relaxed) {
self.finish_debounce();
}
None
}
}
}
}
fn finish_debounce(&mut self) {
let save_pending = self.save_pending.clone();
job::dispatch_blocking(move |editor, _| {
if editor.mode() == Mode::Insert {
// Avoid saving while in insert mode since this mixes up
// the modification indicator and prevents future saves.
save_pending.store(true, atomic::Ordering::Relaxed);
} else {
request_auto_save(editor);
save_pending.store(false, atomic::Ordering::Relaxed);
}
})
}
}
fn request_auto_save(editor: &mut Editor) {
let context = &mut compositor::Context {
editor,
scroll: Some(0),
jobs: &mut Jobs::new(),
};
if let Err(e) = commands::typed::write_all_impl(context, false, false) {
context.editor.set_error(format!("{}", e));
}
}
pub(super) fn register_hooks(handlers: &Handlers) {
let tx = handlers.auto_save.clone();
register_hook!(move |event: &mut DocumentDidChange<'_>| {
let config = event.doc.config.load();
if config.auto_save.after_delay.enable {
send_blocking(
&tx,
AutoSaveEvent::DocumentChanged {
save_after: config.auto_save.after_delay.timeout,
},
);
}
Ok(())
});
let tx = handlers.auto_save.clone();
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
if event.old_mode == Mode::Insert {
send_blocking(&tx, AutoSaveEvent::LeftInsertMode)
}
Ok(())
});
}

View File

@@ -42,10 +42,30 @@ impl ResolveHandler {
if item.resolved {
return;
}
let needs_resolve = item.item.documentation.is_none()
|| item.item.detail.is_none()
|| item.item.additional_text_edits.is_none();
if !needs_resolve {
// We consider an item to be fully resolved if it has non-empty, none-`None` details,
// docs and additional text-edits. Ideally we could use `is_some` instead of this
// check but some language servers send values like `Some([])` for additional text
// edits although the items need to be resolved. This is probably a consequence of
// how `null` works in the JavaScript world.
let is_resolved = item
.item
.documentation
.as_ref()
.is_some_and(|docs| match docs {
lsp::Documentation::String(text) => !text.is_empty(),
lsp::Documentation::MarkupContent(markup) => !markup.value.is_empty(),
})
&& item
.item
.detail
.as_ref()
.is_some_and(|detail| !detail.is_empty())
&& item
.item
.additional_text_edits
.as_ref()
.is_some_and(|edits| !edits.is_empty());
if is_resolved {
item.resolved = true;
return;
}
@@ -111,7 +131,9 @@ impl AsyncHook for ResolveTimeout {
}
fn finish_debounce(&mut self) {
let Some(request) = self.next_request.take() else { return };
let Some(request) = self.next_request.take() else {
return;
};
let (tx, rx) = helix_event::cancelation();
self.in_flight = Some((tx, request.item.clone()));
tokio::spawn(request.execute(rx));

View File

@@ -119,8 +119,7 @@ pub fn request_signature_help(
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if invoked == SignatureHelpInvoked::Manual {
editor
.set_error("No configured language server supports signature-help");
editor.set_error("No configured language server supports signature-help");
}
return;
};
@@ -238,19 +237,33 @@ pub fn show_signature_help(
.collect();
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
let mut active_signature = old_popup
.as_ref()
.map(|popup| popup.contents().active_signature())
.unwrap_or_else(|| response.active_signature.unwrap_or_default() as usize);
let lsp_signature = response.active_signature.map(|s| s as usize);
if active_signature >= signatures.len() {
active_signature = signatures.len() - 1;
}
// take the new suggested lsp signature if changed
// otherwise take the old signature if possible
// otherwise the last one (in case there is less signatures than before)
let active_signature = old_popup
.as_ref()
.map(|popup| {
let old_lsp_sig = popup.contents().lsp_signature();
let old_sig = popup
.contents()
.active_signature()
.min(signatures.len() - 1);
if old_lsp_sig != lsp_signature {
lsp_signature.unwrap_or(old_sig)
} else {
old_sig
}
})
.unwrap_or(lsp_signature.unwrap_or_default());
let contents = SignatureHelp::new(
language.to_string(),
Arc::clone(&editor.syn_loader),
active_signature,
lsp_signature,
signatures,
);
@@ -286,11 +299,11 @@ fn signature_help_post_insert_char_hook(
let (view, doc) = current!(cx.editor);
// TODO support multiple language servers (not just the first that is found), likely by merging UI somehow
let Some(language_server) = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.next()
else {
return Ok(());
};
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.next()
else {
return Ok(());
};
let capabilities = language_server.capabilities();

View File

@@ -5,7 +5,6 @@ use crate::{
use helix_view::{
document::SavePoint,
editor::CompleteAction,
graphics::Margin,
handlers::lsp::SignatureHelpInvoked,
theme::{Modifier, Style},
ViewId,
@@ -334,16 +333,9 @@ impl Completion {
}
});
let margin = if editor.menu_border() {
Margin::vertical(1)
} else {
Margin::none()
};
let popup = Popup::new(Self::ID, menu)
.with_scrollbar(false)
.ignore_escape_key(true)
.margin(margin);
.ignore_escape_key(true);
let (view, doc) = current_ref!(editor);
let text = doc.text().slice(..);
@@ -540,8 +532,8 @@ impl Component for Completion {
surface.clear_with(doc_area, background);
if cx.editor.popup_border() {
use tui::widgets::{Block, Borders, Widget};
Widget::render(Block::default().borders(Borders::ALL), doc_area, surface);
use tui::widgets::{Block, Widget};
Widget::render(Block::bordered(), doc_area, surface);
}
markdown_doc.render(doc_area, surface, cx);

View File

@@ -1,6 +1,7 @@
use crate::{
commands::{self, OnKeyCallback},
compositor::{Component, Context, Event, EventResult},
events::{OnModeSwitch, PostCommand},
key,
keymap::{KeymapResult, Keymaps},
ui::{
@@ -866,10 +867,17 @@ impl EditorView {
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
let mut execute_command = |command: &commands::MappableCommand| {
cxt.execute_command(command);
command.execute(cxt);
helix_event::dispatch(PostCommand { command, cx: cxt });
let current_mode = cxt.editor.mode();
if current_mode != last_mode {
helix_event::dispatch(OnModeSwitch {
old_mode: last_mode,
new_mode: current_mode,
cx: cxt,
});
// HAXX: if we just entered insert mode from normal, clear key buf
// and record the command that got us into this mode.
if current_mode == Mode::Insert {
@@ -879,6 +887,7 @@ impl EditorView {
self.last_insert.1.clear();
}
}
last_mode = current_mode;
};
@@ -902,18 +911,18 @@ impl EditorView {
match keyresult {
KeymapResult::NotFound => {
if let Some(ch) = event.char() {
cx.execute(|cx| commands::insert::insert_char(cx, ch))
commands::insert::insert_char(cx, ch)
}
}
KeymapResult::Cancelled(pending) => {
for ev in pending {
match ev.char() {
Some(ch) => cx.execute(|cx| commands::insert::insert_char(cx, ch)),
Some(ch) => commands::insert::insert_char(cx, ch),
None => {
if let KeymapResult::Matched(command) =
self.keymaps.get(Mode::Insert, ev)
{
cx.execute_command(&command);
command.execute(cx);
}
}
}
@@ -929,7 +938,11 @@ impl EditorView {
// If the count is already started and the input is a number, always continue the count.
(key!(i @ '0'..='9'), Some(count)) => {
let i = i.to_digit(10).unwrap() as usize;
cxt.editor.count = NonZeroUsize::new(count.get() * 10 + i);
let count = count.get() * 10 + i;
if count > 100_000_000 {
return;
}
cxt.editor.count = NonZeroUsize::new(count);
}
// A non-zero digit will start the count if that number isn't used by a keymap.
(key!(i @ '1'..='9'), None) if !self.keymaps.contains_key(mode, event) => {
@@ -940,7 +953,7 @@ impl EditorView {
(key!('.'), _) if self.keymaps.pending().is_empty() => {
for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) {
// first execute whatever put us into insert mode
cxt.execute_command(&self.last_insert.0);
self.last_insert.0.execute(cxt);
let mut last_savepoint = None;
let mut last_request_savepoint = None;
// then replay the inputs
@@ -1231,9 +1244,8 @@ impl EditorView {
};
if should_yank {
cxt.execute_command(
&commands::MappableCommand::yank_main_selection_to_primary_clipboard,
);
commands::MappableCommand::yank_main_selection_to_primary_clipboard
.execute(cxt);
EventResult::Consumed(None)
} else {
EventResult::Ignored(None)
@@ -1253,11 +1265,9 @@ impl EditorView {
doc.set_selection(view_id, Selection::point(pos));
match modifiers {
KeyModifiers::ALT => {
cxt.execute_command(&commands::MappableCommand::dap_edit_log)
commands::MappableCommand::dap_edit_log.execute(cxt)
}
_ => cxt.execute_command(
&commands::MappableCommand::dap_edit_condition,
),
_ => commands::MappableCommand::dap_edit_condition.execute(cxt),
};
}
}
@@ -1275,9 +1285,8 @@ impl EditorView {
}
if modifiers == KeyModifiers::ALT {
cxt.execute_command(
&commands::MappableCommand::replace_selections_with_primary_clipboard,
);
commands::MappableCommand::replace_selections_with_primary_clipboard
.execute(cxt);
return EventResult::Consumed(None);
}
@@ -1286,7 +1295,7 @@ impl EditorView {
let doc = doc_mut!(editor, &view!(editor, view_id).doc);
doc.set_selection(view_id, Selection::point(pos));
cxt.editor.focus(view_id);
cxt.execute_command(&commands::MappableCommand::paste_primary_clipboard_before);
commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
return EventResult::Consumed(None);
}
@@ -1318,9 +1327,20 @@ impl Component for EditorView {
Event::Paste(contents) => {
self.handle_non_key_input(&mut cx);
cx.count = cx.editor.count;
cx.execute(|cx| commands::paste_bracketed_value(cx, contents.clone()));
commands::paste_bracketed_value(&mut cx, contents.clone());
cx.editor.count = None;
let config = cx.editor.config();
let mode = cx.editor.mode();
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);
// Store a history state if not in insert mode. Otherwise wait till we exit insert
// to include any edits to the paste in the history state.
if mode != Mode::Insert {
doc.append_changes_to_history(view);
}
EventResult::Consumed(None)
}
Event::Resize(_width, _height) => {
@@ -1339,7 +1359,7 @@ impl Component for EditorView {
if let Some(on_next_key) = self.on_next_key.take() {
// if there's a command waiting input, do that first
cx.execute(|cx| on_next_key(cx, key));
on_next_key(&mut cx, key);
} else {
match mode {
Mode::Insert => {
@@ -1403,6 +1423,17 @@ impl Component for EditorView {
return EventResult::Ignored(None);
}
let config = cx.editor.config();
let mode = cx.editor.mode();
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);
// Store a history state if not in insert mode. This also takes care of
// committing changes when leaving insert mode.
if mode != Mode::Insert {
doc.append_changes_to_history(view);
}
let callback = if callbacks.is_empty() {
None
} else {
@@ -1424,7 +1455,7 @@ impl Component for EditorView {
EventResult::Consumed(None)
}
Event::FocusLost => {
if context.editor.config().auto_save {
if context.editor.config().auto_save.focus_lost {
if let Err(e) = commands::typed::write_all_impl(context, false, false) {
context.editor.set_error(format!("{}", e));
}

View File

@@ -3,7 +3,7 @@ use helix_view::graphics::{Margin, Rect};
use helix_view::info::Info;
use tui::buffer::Buffer as Surface;
use tui::text::Text;
use tui::widgets::{Block, Borders, Paragraph, Widget};
use tui::widgets::{Block, Paragraph, Widget};
impl Component for Info {
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
@@ -23,13 +23,12 @@ impl Component for Info {
));
surface.clear_with(area, popup_style);
let block = Block::default()
let block = Block::bordered()
.title(self.title.as_str())
.borders(Borders::ALL)
.border_style(popup_style);
let margin = Margin::horizontal(1);
let inner = block.inner(area).inner(&margin);
let inner = block.inner(area).inner(margin);
block.render(area, surface);
Paragraph::new(&Text::from(self.text.as_str()))

View File

@@ -27,6 +27,7 @@ pub struct SignatureHelp {
language: String,
config_loader: Arc<ArcSwap<syntax::Loader>>,
active_signature: usize,
lsp_signature: Option<usize>,
signatures: Vec<Signature>,
}
@@ -37,12 +38,14 @@ impl SignatureHelp {
language: String,
config_loader: Arc<ArcSwap<syntax::Loader>>,
active_signature: usize,
lsp_signature: Option<usize>,
signatures: Vec<Signature>,
) -> Self {
Self {
language,
config_loader,
active_signature,
lsp_signature,
signatures,
}
}
@@ -51,6 +54,10 @@ impl SignatureHelp {
self.active_signature
}
pub fn lsp_signature(&self) -> Option<usize> {
self.lsp_signature
}
pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> {
compositor.find_id::<Popup<Self>>(Self::ID)
}
@@ -119,7 +126,7 @@ impl Component for SignatureHelp {
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_area = sig_text_area.inner(margin).intersection(surface.area);
let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false });
sig_text_para.render(sig_text_area, surface);
@@ -146,7 +153,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.inner(margin), surface);
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
@@ -155,10 +162,7 @@ impl Component for SignatureHelp {
let sig = &self.signatures[self.active_signature];
if PADDING >= viewport.1 || PADDING >= viewport.0 {
return None;
}
let max_text_width = (viewport.0 - PADDING).min(120);
let max_text_width = viewport.0.saturating_sub(PADDING).clamp(10, 120);
let signature_text = crate::ui::markdown::highlighted_code_block(
sig.signature.as_str(),

View File

@@ -54,7 +54,14 @@ pub fn highlighted_code_block<'a>(
language.into(),
))
.and_then(|config| config.highlight_config(theme.scopes()))
.and_then(|config| Syntax::new(ropeslice, config, Arc::clone(&config_loader)));
.and_then(|config| {
Syntax::new(ropeslice, config, |injection| {
config_loader
.load()
.language_configuration_for_injection_string(injection)
.and_then(|config| config.get_highlight_config())
})
});
let syntax = match syntax {
Some(s) => s,
@@ -351,7 +358,7 @@ impl Component for Markdown {
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
let margin = Margin::all(1);
par.render(area.inner(&margin), surface);
par.render(area.inner(margin), surface);
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {

View File

@@ -1,24 +1,17 @@
use std::{borrow::Cow, cmp::Reverse, path::PathBuf};
use std::{borrow::Cow, cmp::Reverse};
use crate::{
compositor::{Callback, Component, Compositor, Context, Event, EventResult},
ctrl, key, shift,
};
use helix_core::fuzzy::MATCHER;
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
use nucleo::pattern::{Atom, AtomKind, CaseMatching, Normalization};
use nucleo::{Config, Utf32Str};
use tui::{
buffer::Buffer as Surface,
widgets::{Block, Borders, Table, Widget},
};
use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row};
use helix_view::{
editor::SmartTabConfig,
graphics::{Margin, Rect},
Editor,
};
use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor};
use tui::layout::Constraint;
pub trait Item: Sync + Send + 'static {
@@ -38,18 +31,6 @@ pub trait Item: Sync + Send + 'static {
}
}
impl Item for PathBuf {
/// Root prefix to strip.
type Data = PathBuf;
fn format(&self, root_path: &Self::Data) -> Row {
self.strip_prefix(root_path)
.unwrap_or(self)
.to_string_lossy()
.into()
}
}
pub type MenuCallback<T> = Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>;
pub struct Menu<T: Item> {
@@ -99,7 +80,13 @@ impl<T: Item> Menu<T> {
pub fn score(&mut self, pattern: &str, incremental: bool) {
let mut matcher = MATCHER.lock();
matcher.config = Config::DEFAULT;
let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false);
let pattern = Atom::new(
pattern,
CaseMatching::Ignore,
Normalization::Smart,
AtomKind::Fuzzy,
false,
);
let mut buf = Vec::new();
if incremental {
self.matches.retain_mut(|(index, score)| {
@@ -341,17 +328,9 @@ impl<T: Item + 'static> Component for Menu<T> {
.try_get("ui.menu")
.unwrap_or_else(|| theme.get("ui.text"));
let selected = theme.get("ui.menu.selected");
surface.clear_with(area, style);
let render_borders = cx.editor.menu_border();
let area = if render_borders {
Widget::render(Block::default().borders(Borders::ALL), area, surface);
area.inner(&Margin::vertical(1))
} else {
area
};
let scroll = self.scroll;
let options: Vec<_> = self

View File

@@ -21,7 +21,7 @@ pub use editor::EditorView;
use helix_stdx::rope;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, Picker};
pub use picker::{Column as PickerColumn, FileLocation, Picker};
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
@@ -29,7 +29,7 @@ pub use text::Text;
use helix_view::Editor;
use std::path::PathBuf;
use std::{error::Error, path::PathBuf};
pub fn prompt(
cx: &mut crate::commands::Context,
@@ -170,7 +170,9 @@ pub fn raw_regex_prompt(
cx.push_layer(Box::new(prompt));
}
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker<PathBuf> {
type FilePicker = Picker<PathBuf, PathBuf>;
pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time::Instant;
@@ -217,7 +219,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
});
log::debug!("file_picker init {:?}", Instant::now().duration_since(now));
let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| {
let columns = [PickerColumn::new(
"path",
|item: &PathBuf, root: &PathBuf| {
item.strip_prefix(root)
.unwrap_or(item)
.to_string_lossy()
.into()
},
)];
let picker = Picker::new(columns, 0, [], root, move |cx, path: &PathBuf, action| {
if let Err(e) = cx.editor.open(path, action) {
let err = if let Some(err) = e.source() {
format!("{}", err)
@@ -227,7 +238,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker
cx.editor.set_error(err);
}
})
.with_preview(|_editor, path| Some((path.clone().into(), None)));
.with_preview(|_editor, path| Some((path.as_path().into(), None)));
let injector = picker.injector();
let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30);
@@ -364,14 +375,16 @@ pub mod completers {
}
pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec<Completion> {
let Some(options) = doc!(editor)
let commands = doc!(editor)
.language_servers_with_feature(LanguageServerFeature::WorkspaceCommand)
.find_map(|ls| ls.capabilities().execute_command_provider.as_ref())
else {
return vec![];
};
.flat_map(|ls| {
ls.capabilities()
.execute_command_provider
.iter()
.flat_map(|options| options.commands.iter())
});
fuzzy_match(input, &options.commands, false)
fuzzy_match(input, commands, false)
.into_iter()
.map(|(name, _)| ((0..), name.to_owned().into()))
.collect()

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,187 @@
use std::{
path::Path,
sync::{atomic, Arc},
time::Duration,
};
use helix_event::AsyncHook;
use tokio::time::Instant;
use crate::{job, ui::overlay::Overlay};
use super::{CachedPreview, DynQueryCallback, Picker};
pub(super) struct PreviewHighlightHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
trigger: Option<Arc<Path>>,
phantom_data: std::marker::PhantomData<(T, D)>,
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> Default for PreviewHighlightHandler<T, D> {
fn default() -> Self {
Self {
trigger: None,
phantom_data: Default::default(),
}
}
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook
for PreviewHighlightHandler<T, D>
{
type Event = Arc<Path>;
fn handle_event(
&mut self,
path: Self::Event,
timeout: Option<tokio::time::Instant>,
) -> Option<tokio::time::Instant> {
if self
.trigger
.as_ref()
.is_some_and(|trigger| trigger == &path)
{
// If the path hasn't changed, don't reset the debounce
timeout
} else {
self.trigger = Some(path);
Some(Instant::now() + Duration::from_millis(150))
}
}
fn finish_debounce(&mut self) {
let Some(path) = self.trigger.take() else {
return;
};
job::dispatch_blocking(move |editor, compositor| {
let Some(Overlay {
content: picker, ..
}) = compositor.find::<Overlay<Picker<T, D>>>()
else {
return;
};
let Some(CachedPreview::Document(ref mut doc)) = picker.preview_cache.get_mut(&path)
else {
return;
};
if doc.language_config().is_some() {
return;
}
let Some(language_config) = doc.detect_language_config(&editor.syn_loader.load())
else {
return;
};
doc.language = Some(language_config.clone());
let text = doc.text().clone();
let loader = editor.syn_loader.clone();
tokio::task::spawn_blocking(move || {
let Some(syntax) = language_config
.highlight_config(&loader.load().scopes())
.and_then(|highlight_config| {
helix_core::Syntax::new(text.slice(..), highlight_config, |injection| {
loader
.load()
.language_configuration_for_injection_string(injection)
.and_then(|config| config.get_highlight_config())
})
})
else {
log::info!("highlighting picker item failed");
return;
};
job::dispatch_blocking(move |editor, compositor| {
let Some(Overlay {
content: picker, ..
}) = compositor.find::<Overlay<Picker<T, D>>>()
else {
log::info!("picker closed before syntax highlighting finished");
return;
};
let Some(CachedPreview::Document(ref mut doc)) =
picker.preview_cache.get_mut(&path)
else {
return;
};
let diagnostics = helix_view::Editor::doc_diagnostics(
&editor.language_servers,
&editor.diagnostics,
doc,
);
doc.replace_diagnostics(diagnostics, &[], None);
doc.syntax = Some(syntax);
});
});
});
}
}
pub(super) struct DynamicQueryHandler<T: 'static + Send + Sync, D: 'static + Send + Sync> {
callback: Arc<DynQueryCallback<T, D>>,
// Duration used as a debounce.
// Defaults to 100ms if not provided via `Picker::with_dynamic_query`. Callers may want to set
// this higher if the dynamic query is expensive - for example global search.
debounce: Duration,
last_query: Arc<str>,
query: Option<Arc<str>>,
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> DynamicQueryHandler<T, D> {
pub(super) fn new(callback: DynQueryCallback<T, D>, duration_ms: Option<u64>) -> Self {
Self {
callback: Arc::new(callback),
debounce: Duration::from_millis(duration_ms.unwrap_or(100)),
last_query: "".into(),
query: None,
}
}
}
impl<T: 'static + Send + Sync, D: 'static + Send + Sync> AsyncHook for DynamicQueryHandler<T, D> {
type Event = Arc<str>;
fn handle_event(&mut self, query: Self::Event, _timeout: Option<Instant>) -> Option<Instant> {
if query == self.last_query {
// If the search query reverts to the last one we requested, no need to
// make a new request.
self.query = None;
None
} else {
self.query = Some(query);
Some(Instant::now() + self.debounce)
}
}
fn finish_debounce(&mut self) {
let Some(query) = self.query.take() else {
return;
};
self.last_query = query.clone();
let callback = self.callback.clone();
job::dispatch_blocking(move |editor, compositor| {
let Some(Overlay {
content: picker, ..
}) = compositor.find::<Overlay<Picker<T, D>>>()
else {
return;
};
// Increment the version number to cancel any ongoing requests.
picker.version.fetch_add(1, atomic::Ordering::Relaxed);
picker.matcher.restart(false);
let injector = picker.injector();
let get_options = (callback)(&query, editor, picker.editor_data.clone(), &injector);
tokio::spawn(async move {
if let Err(err) = get_options.await {
log::info!("Dynamic request failed: {err}");
}
// NOTE: the Drop implementation of Injector will request a redraw when the
// injector falls out of scope here, clearing the "running" indicator.
});
})
}
}

View File

@@ -0,0 +1,368 @@
use std::{collections::HashMap, mem, ops::Range, sync::Arc};
#[derive(Debug)]
pub(super) struct PickerQuery {
/// The column names of the picker.
column_names: Box<[Arc<str>]>,
/// The index of the primary column in `column_names`.
/// The primary column is selected by default unless another
/// field is specified explicitly with `%fieldname`.
primary_column: usize,
/// The mapping between column names and input in the query
/// for those columns.
inner: HashMap<Arc<str>, Arc<str>>,
/// The byte ranges of the input text which are used as input for each column.
/// This is calculated at parsing time for use in [Self::active_column].
/// This Vec is naturally sorted in ascending order and ranges do not overlap.
column_ranges: Vec<(Range<usize>, Option<Arc<str>>)>,
}
impl PartialEq<HashMap<Arc<str>, Arc<str>>> for PickerQuery {
fn eq(&self, other: &HashMap<Arc<str>, Arc<str>>) -> bool {
self.inner.eq(other)
}
}
impl PickerQuery {
pub(super) fn new<I: Iterator<Item = Arc<str>>>(
column_names: I,
primary_column: usize,
) -> Self {
let column_names: Box<[_]> = column_names.collect();
let inner = HashMap::with_capacity(column_names.len());
let column_ranges = vec![(0..usize::MAX, Some(column_names[primary_column].clone()))];
Self {
column_names,
primary_column,
inner,
column_ranges,
}
}
pub(super) fn get(&self, column: &str) -> Option<&Arc<str>> {
self.inner.get(column)
}
pub(super) fn parse(&mut self, input: &str) -> HashMap<Arc<str>, Arc<str>> {
let mut fields: HashMap<Arc<str>, String> = HashMap::new();
let primary_field = &self.column_names[self.primary_column];
let mut escaped = false;
let mut in_field = false;
let mut field = None;
let mut text = String::new();
self.column_ranges.clear();
self.column_ranges
.push((0..usize::MAX, Some(primary_field.clone())));
macro_rules! finish_field {
() => {
let key = field.take().unwrap_or(primary_field);
if let Some(pattern) = fields.get_mut(key) {
pattern.push(' ');
pattern.push_str(text.trim());
} else {
fields.insert(key.clone(), text.trim().to_string());
}
text.clear();
};
}
for (idx, ch) in input.char_indices() {
match ch {
// Backslash escaping
_ if escaped => {
// '%' is the only character that is special cased.
// You can escape it to prevent parsing the text that
// follows it as a field name.
if ch != '%' {
text.push('\\');
}
text.push(ch);
escaped = false;
}
'\\' => escaped = !escaped,
'%' => {
if !text.is_empty() {
finish_field!();
}
let (range, _field) = self
.column_ranges
.last_mut()
.expect("column_ranges is non-empty");
range.end = idx;
in_field = true;
}
' ' if in_field => {
text.clear();
in_field = false;
}
_ if in_field => {
text.push(ch);
// Go over all columns and their indices, find all that starts with field key,
// select a column that fits key the most.
field = self
.column_names
.iter()
.filter(|col| col.starts_with(&text))
// select "fittest" column
.min_by_key(|col| col.len());
// Update the column range for this column.
if let Some((_range, current_field)) = self
.column_ranges
.last_mut()
.filter(|(range, _)| range.end == usize::MAX)
{
*current_field = field.cloned();
} else {
self.column_ranges.push((idx..usize::MAX, field.cloned()));
}
}
_ => text.push(ch),
}
}
if !in_field && !text.is_empty() {
finish_field!();
}
let new_inner: HashMap<_, _> = fields
.into_iter()
.map(|(field, query)| (field, query.as_str().into()))
.collect();
mem::replace(&mut self.inner, new_inner)
}
/// Finds the column which the cursor is 'within' in the last parse.
///
/// The cursor is considered to be within a column when it is placed within any
/// of a column's text. See the `active_column_test` unit test below for examples.
///
/// `cursor` is a byte index that represents the location of the prompt's cursor.
pub fn active_column(&self, cursor: usize) -> Option<&Arc<str>> {
let point = self
.column_ranges
.partition_point(|(range, _field)| cursor > range.end);
self.column_ranges
.get(point)
.filter(|(range, _field)| cursor >= range.start && cursor <= range.end)
.and_then(|(_range, field)| field.as_ref())
}
}
#[cfg(test)]
mod test {
use helix_core::hashmap;
use super::*;
#[test]
fn parse_query_test() {
let mut query = PickerQuery::new(
[
"primary".into(),
"field1".into(),
"field2".into(),
"another".into(),
"anode".into(),
]
.into_iter(),
0,
);
// Basic field splitting
query.parse("hello world");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello world".into(),
)
);
query.parse("hello %field1 world %field2 !");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "world".into(),
"field2".into() => "!".into(),
)
);
query.parse("%field1 abc %field2 def xyz");
assert_eq!(
query,
hashmap!(
"field1".into() => "abc".into(),
"field2".into() => "def xyz".into(),
)
);
// Trailing space is trimmed
query.parse("hello ");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
)
);
// Unknown fields are trimmed.
query.parse("hello %foo");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
)
);
// Multiple words in a field
query.parse("hello %field1 a b c");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "a b c".into(),
)
);
// Escaping
query.parse(r#"hello\ world"#);
assert_eq!(
query,
hashmap!(
"primary".into() => r#"hello\ world"#.into(),
)
);
query.parse(r#"hello \%field1 world"#);
assert_eq!(
query,
hashmap!(
"primary".into() => "hello %field1 world".into(),
)
);
query.parse(r#"%field1 hello\ world"#);
assert_eq!(
query,
hashmap!(
"field1".into() => r#"hello\ world"#.into(),
)
);
query.parse(r#"hello %field1 a\"b"#);
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => r#"a\"b"#.into(),
)
);
query.parse(r#"%field1 hello\ world"#);
assert_eq!(
query,
hashmap!(
"field1".into() => r#"hello\ world"#.into(),
)
);
query.parse(r#"\bfoo\b"#);
assert_eq!(
query,
hashmap!(
"primary".into() => r#"\bfoo\b"#.into(),
)
);
query.parse(r#"\\n"#);
assert_eq!(
query,
hashmap!(
"primary".into() => r#"\\n"#.into(),
)
);
// Only the prefix of a field is required.
query.parse("hello %anot abc");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
"another".into() => "abc".into(),
)
);
// The shortest matching the prefix is selected.
query.parse("hello %ano abc");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
"anode".into() => "abc".into()
)
);
// Multiple uses of a column are concatenated with space separators.
query.parse("hello %field1 xyz %fie abc");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "xyz abc".into()
)
);
query.parse("hello %fie abc");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello".into(),
"field1".into() => "abc".into()
)
);
// The primary column can be explicitly qualified.
query.parse("hello %fie abc %prim world");
assert_eq!(
query,
hashmap!(
"primary".into() => "hello world".into(),
"field1".into() => "abc".into()
)
);
}
#[test]
fn active_column_test() {
fn active_column<'a>(query: &'a mut PickerQuery, input: &str) -> Option<&'a str> {
let cursor = input.find('|').expect("cursor must be indicated with '|'");
let input = input.replace('|', "");
query.parse(&input);
query.active_column(cursor).map(AsRef::as_ref)
}
let mut query = PickerQuery::new(
["primary".into(), "foo".into(), "bar".into()].into_iter(),
0,
);
assert_eq!(active_column(&mut query, "|"), Some("primary"));
assert_eq!(active_column(&mut query, "hello| world"), Some("primary"));
assert_eq!(active_column(&mut query, "|%foo hello"), Some("primary"));
assert_eq!(active_column(&mut query, "%foo|"), Some("foo"));
assert_eq!(active_column(&mut query, "%|"), None);
assert_eq!(active_column(&mut query, "%baz|"), None);
assert_eq!(active_column(&mut query, "%quiz%|"), None);
assert_eq!(active_column(&mut query, "%foo hello| world"), Some("foo"));
assert_eq!(active_column(&mut query, "%foo hello world|"), Some("foo"));
assert_eq!(active_column(&mut query, "%foo| hello world"), Some("foo"));
assert_eq!(active_column(&mut query, "%|foo hello world"), Some("foo"));
assert_eq!(active_column(&mut query, "%f|oo hello world"), Some("foo"));
assert_eq!(active_column(&mut query, "hello %f|oo world"), Some("foo"));
assert_eq!(
active_column(&mut query, "hello %f|oo world %bar !"),
Some("foo")
);
assert_eq!(
active_column(&mut query, "hello %foo wo|rld %bar !"),
Some("foo")
);
assert_eq!(
active_column(&mut query, "hello %foo world %bar !|"),
Some("bar")
);
}
}

View File

@@ -5,7 +5,7 @@ use crate::{
};
use tui::{
buffer::Buffer as Surface,
widgets::{Block, Borders, Widget},
widgets::{Block, Widget},
};
use helix_core::Position;
@@ -15,7 +15,16 @@ use helix_view::{
Editor,
};
const MIN_HEIGHT: u16 = 4;
const MIN_HEIGHT: u16 = 6;
const MAX_HEIGHT: u16 = 26;
const MAX_WIDTH: u16 = 120;
struct RenderInfo {
area: Rect,
child_height: u16,
render_borders: bool,
is_menu: bool,
}
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
// a width/height hint. maybe Popup(Box<Component>)
@@ -23,7 +32,6 @@ const MIN_HEIGHT: u16 = 4;
pub struct Popup<T: Component> {
contents: T,
position: Option<Position>,
margin: Margin,
area: Rect,
position_bias: Open,
scroll_half_pages: usize,
@@ -38,7 +46,6 @@ impl<T: Component> Popup<T> {
Self {
contents,
position: None,
margin: Margin::none(),
position_bias: Open::Below,
area: Rect::new(0, 0, 0, 0),
scroll_half_pages: 0,
@@ -71,11 +78,6 @@ impl<T: Component> Popup<T> {
self
}
pub fn margin(mut self, margin: Margin) -> Self {
self.margin = margin;
self
}
pub fn auto_close(mut self, auto_close: bool) -> Self {
self.auto_close = auto_close;
self
@@ -118,38 +120,38 @@ impl<T: Component> Popup<T> {
}
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
let child_size = self
.contents
.required_size((viewport.width, viewport.height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
self.area_internal(viewport, editor, child_size)
self.render_info(viewport, editor).area
}
pub fn area_internal(
&mut self,
viewport: Rect,
editor: &Editor,
child_size: (u16, u16),
) -> Rect {
let width = child_size.0.min(viewport.width);
let height = child_size.1.min(viewport.height.saturating_sub(2)); // add some spacing in the viewport
let position = self
fn render_info(&mut self, viewport: Rect, editor: &Editor) -> RenderInfo {
let mut position = editor.cursor().0.unwrap_or_default();
if let Some(old_position) = self
.position
.get_or_insert_with(|| editor.cursor().0.unwrap_or_default());
.filter(|old_position| old_position.row == position.row)
{
position = old_position;
} else {
self.position = Some(position);
}
// if there's a orientation preference, use that
// if we're on the top part of the screen, do below
// if we're on the bottom part, do above
let is_menu = self
.contents
.type_name()
.starts_with("helix_term::ui::menu::Menu");
let mut render_borders = if is_menu {
editor.menu_border()
} else {
editor.popup_border()
};
// -- make sure frame doesn't stick out of bounds
let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width {
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
}
// if there's a orientation preference, use that
// if we're on the top part of the screen, do below
// if we're on the bottom part, do above
let can_put_below = viewport.height > rel_y + MIN_HEIGHT;
let can_put_above = rel_y.checked_sub(MIN_HEIGHT).is_some();
let final_pos = match self.position_bias {
@@ -163,7 +165,40 @@ impl<T: Component> Popup<T> {
},
};
match final_pos {
// compute maximum space available for child
let mut max_height = match final_pos {
Open::Above => rel_y,
Open::Below => viewport.height.saturating_sub(1 + rel_y),
};
max_height = max_height.min(MAX_HEIGHT);
let mut max_width = viewport.width.saturating_sub(2).min(MAX_WIDTH);
render_borders = render_borders && max_height > 3 && max_width > 3;
if render_borders {
max_width -= 2;
max_height -= 2;
}
// compute required child size and reclamp
let (mut width, child_height) = self
.contents
.required_size((max_width, max_height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
width = width.min(MAX_WIDTH);
let height = if render_borders {
(child_height + 2).min(MAX_HEIGHT)
} else {
child_height.min(MAX_HEIGHT)
};
if render_borders {
width += 2;
}
if viewport.width <= rel_x + width + 2 {
rel_x = viewport.width.saturating_sub(width + 2);
width = viewport.width.saturating_sub(rel_x + 2)
}
let area = match final_pos {
Open::Above => {
rel_y = rel_y.saturating_sub(height);
Rect::new(rel_x, rel_y, width, position.row as u16 - rel_y)
@@ -173,6 +208,12 @@ impl<T: Component> Popup<T> {
let y_max = viewport.bottom().min(height + rel_y);
Rect::new(rel_x, rel_y, width, y_max - rel_y)
}
};
RenderInfo {
area,
child_height,
render_borders,
is_menu,
}
}
@@ -259,69 +300,47 @@ impl<T: Component> Component for Popup<T> {
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
}
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
const MAX_WIDTH: u16 = 120;
const MAX_HEIGHT: u16 = 26;
let inner = Rect::new(0, 0, MAX_WIDTH, MAX_HEIGHT).inner(&self.margin);
let (width, height) = self
.contents
.required_size((inner.width, inner.height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
let size = (
(width + self.margin.width()).min(MAX_WIDTH),
(height + self.margin.height()).min(MAX_HEIGHT),
);
Some(size)
}
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
let child_size = self
.contents
.required_size((viewport.width, viewport.height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
let area = self.area_internal(viewport, cx.editor, child_size);
let RenderInfo {
area,
child_height,
render_borders,
is_menu,
} = self.render_info(viewport, cx.editor);
self.area = area;
let max_offset = child_size.1.saturating_sub(area.height) as usize;
let half_page_size = (area.height / 2) as usize;
// clear area
let background = if is_menu {
// TODO: consistently style menu
cx.editor
.theme
.try_get("ui.menu")
.unwrap_or_else(|| cx.editor.theme.get("ui.text"))
} else {
cx.editor.theme.get("ui.popup")
};
surface.clear_with(area, background);
let mut inner = area;
if render_borders {
inner = area.inner(Margin::all(1));
Widget::render(Block::bordered(), area, surface);
}
let border = usize::from(render_borders);
let max_offset = child_height.saturating_sub(inner.height) as usize;
let half_page_size = (inner.height / 2) as usize;
let scroll = max_offset.min(self.scroll_half_pages * half_page_size);
if half_page_size > 0 {
self.scroll_half_pages = scroll / half_page_size;
}
cx.scroll = Some(scroll);
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
let render_borders = cx.editor.popup_border();
let inner = if self
.contents
.type_name()
.starts_with("helix_term::ui::menu::Menu")
{
area
} else {
area.inner(&self.margin)
};
let border = usize::from(render_borders);
if render_borders {
Widget::render(Block::default().borders(Borders::ALL), area, surface);
}
self.contents.render(inner, surface, cx);
// render scrollbar if contents do not fit
if self.has_scrollbar {
let win_height = (inner.height as usize).saturating_sub(2 * border);
let len = (child_size.1 as usize).saturating_sub(2 * border);
let win_height = inner.height as usize;
let len = child_height as usize;
let fits = len <= win_height;
let scroll_style = cx.editor.theme.get("ui.menu.scroll");
@@ -336,7 +355,8 @@ impl<T: Component> Component for Popup<T> {
let mut cell;
for i in 0..win_height {
cell = &mut surface[(inner.right() - 1, inner.top() + (border + i) as u16)];
cell =
&mut surface[(inner.right() - 1 + border as u16, inner.top() + i as u16)];
let half_block = if render_borders { "" } else { "" };

View File

@@ -2,12 +2,13 @@ use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui};
use arc_swap::ArcSwap;
use helix_core::syntax;
use helix_view::document::Mode;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
use std::sync::Arc;
use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Widget};
use tui::widgets::{Block, Widget};
use helix_core::{
unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position,
@@ -91,12 +92,22 @@ impl Prompt {
}
}
/// Gets the byte index in the input representing the current cursor location.
#[inline]
pub(crate) fn position(&self) -> usize {
self.cursor
}
pub fn with_line(mut self, line: String, editor: &Editor) -> Self {
self.set_line(line, editor);
self
}
pub fn set_line(&mut self, line: String, editor: &Editor) {
let cursor = line.len();
self.line = line;
self.cursor = cursor;
self.recalculate_completion(editor);
self
}
pub fn with_language(
@@ -112,6 +123,19 @@ impl Prompt {
&self.line
}
pub fn with_history_register(&mut self, history_register: Option<char>) -> &mut Self {
self.history_register = history_register;
self
}
pub(crate) fn first_history_completion<'a>(
&'a self,
editor: &'a Editor,
) -> Option<Cow<'a, str>> {
self.history_register
.and_then(|reg| editor.registers.first(reg, editor))
}
pub fn recalculate_completion(&mut self, editor: &Editor) {
self.exit_selection();
self.completion = (self.completion_fn)(editor, &self.line);
@@ -457,12 +481,11 @@ impl Prompt {
let background = theme.get("ui.help");
surface.clear_with(area, background);
let block = Block::default()
let block = Block::bordered()
// .title(self.title.as_str())
.borders(Borders::ALL)
.border_style(background);
let inner = block.inner(area).inner(&Margin::horizontal(1));
let inner = block.inner(area).inner(Margin::horizontal(1));
block.render(area, surface);
text.render(inner, surface, cx);
@@ -476,10 +499,7 @@ impl Prompt {
let line_area = area.clip_left(self.prompt.len() as u16).clip_top(line);
if self.line.is_empty() {
// Show the most recently entered value as a suggestion.
if let Some(suggestion) = self
.history_register
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
{
if let Some(suggestion) = self.first_history_completion(cx.editor) {
surface.set_string(line_area.x, line_area.y, suggestion, suggestion_color);
}
} else if let Some((language, loader)) = self.language.as_ref() {
@@ -574,8 +594,7 @@ impl Component for Prompt {
self.recalculate_completion(cx.editor);
} else {
let last_item = self
.history_register
.and_then(|reg| cx.editor.registers.first(reg, cx.editor))
.first_history_completion(cx.editor)
.map(|entry| entry.to_string())
.unwrap_or_else(|| String::from(""));
@@ -663,7 +682,7 @@ impl Component for Prompt {
self.render_prompt(area, surface, cx)
}
fn cursor(&self, area: Rect, _editor: &Editor) -> (Option<Position>, CursorKind) {
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
let line = area.height as usize - 1;
(
Some(Position::new(
@@ -672,7 +691,7 @@ impl Component for Prompt {
+ self.prompt.len()
+ UnicodeWidthStr::width(&self.line[..self.cursor]),
)),
CursorKind::Block,
editor.config().cursor_shape.from_mode(Mode::Insert),
)
}
}

View File

@@ -4,6 +4,7 @@ use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
graphics::Rect,
theme::Style,
Document, Editor, View,
};
@@ -19,6 +20,7 @@ pub struct RenderContext<'a> {
pub view: &'a View,
pub focused: bool,
pub spinners: &'a ProgressSpinners,
pub parts: RenderBuffer<'a>,
}
impl<'a> RenderContext<'a> {
@@ -35,10 +37,18 @@ impl<'a> RenderContext<'a> {
view,
focused,
spinners,
parts: RenderBuffer::default(),
}
}
}
#[derive(Default)]
pub struct RenderBuffer<'a> {
pub left: Spans<'a>,
pub center: Spans<'a>,
pub right: Spans<'a>,
}
pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface) {
let base_style = if context.focused {
context.editor.theme.get("ui.statusline")
@@ -48,87 +58,85 @@ pub fn render(context: &mut RenderContext, viewport: Rect, surface: &mut Surface
surface.set_style(viewport.with_height(1), base_style);
let statusline = render_statusline(context, viewport.width as usize);
let write_left = |context: &mut RenderContext, text, style| {
append(&mut context.parts.left, text, &base_style, style)
};
let write_center = |context: &mut RenderContext, text, style| {
append(&mut context.parts.center, text, &base_style, style)
};
let write_right = |context: &mut RenderContext, text, style| {
append(&mut context.parts.right, text, &base_style, style)
};
// Left side of the status line.
let config = context.editor.config();
let element_ids = &config.statusline.left;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_left));
surface.set_spans(
viewport.x,
viewport.y,
&statusline,
statusline.width() as u16,
&context.parts.left,
context.parts.left.width() as u16,
);
// Right side of the status line.
let element_ids = &config.statusline.right;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_right));
surface.set_spans(
viewport.x
+ viewport
.width
.saturating_sub(context.parts.right.width() as u16),
viewport.y,
&context.parts.right,
context.parts.right.width() as u16,
);
// Center of the status line.
let element_ids = &config.statusline.center;
element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.for_each(|render| render(context, write_center));
// Width of the empty space between the left and center area and between the center and right area.
let spacing = 1u16;
let edge_width = context.parts.left.width().max(context.parts.right.width()) as u16;
let center_max_width = viewport.width.saturating_sub(2 * edge_width + 2 * spacing);
let center_width = center_max_width.min(context.parts.center.width() as u16);
surface.set_spans(
viewport.x + viewport.width / 2 - center_width / 2,
viewport.y,
&context.parts.center,
center_width,
);
}
pub fn render_statusline<'a>(context: &mut RenderContext, width: usize) -> Spans<'a> {
let config = context.editor.config();
let element_ids = &config.statusline.left;
let mut left = element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.flat_map(|render| render(context).0)
.collect::<Vec<Span>>();
let element_ids = &config.statusline.center;
let mut center = element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.flat_map(|render| render(context).0)
.collect::<Vec<Span>>();
let element_ids = &config.statusline.right;
let mut right = element_ids
.iter()
.map(|element_id| get_render_function(*element_id))
.flat_map(|render| render(context).0)
.collect::<Vec<Span>>();
let left_area_width: usize = left.iter().map(|s| s.width()).sum();
let center_area_width: usize = center.iter().map(|s| s.width()).sum();
let right_area_width: usize = right.iter().map(|s| s.width()).sum();
let min_spacing_between_areas = 1usize;
let sides_space_required = left_area_width + right_area_width + min_spacing_between_areas;
let total_space_required = sides_space_required + center_area_width + min_spacing_between_areas;
let mut statusline: Vec<Span> = vec![];
if center_area_width > 0 && total_space_required <= width {
// SAFETY: this subtraction cannot underflow because `left_area_width + center_area_width + right_area_width`
// is smaller than `total_space_required`, which is smaller than `width` in this branch.
let total_spacers = width - (left_area_width + center_area_width + right_area_width);
// This is how much padding space it would take on either side to align the center area to the middle.
let center_margin = (width - center_area_width) / 2;
let left_spacers = if left_area_width < center_margin && right_area_width < center_margin {
// Align the center area to the middle if there is enough space on both sides.
center_margin - left_area_width
} else {
// Otherwise split the available space evenly and use it as margin.
// The center element won't be aligned to the middle but it will be evenly
// spaced between the left and right areas.
total_spacers / 2
};
let right_spacers = total_spacers - left_spacers;
statusline.append(&mut left);
statusline.push(" ".repeat(left_spacers).into());
statusline.append(&mut center);
statusline.push(" ".repeat(right_spacers).into());
statusline.append(&mut right);
} else if right_area_width > 0 && sides_space_required <= width {
let side_areas_width = left_area_width + right_area_width;
statusline.append(&mut left);
statusline.push(" ".repeat(width - side_areas_width).into());
statusline.append(&mut right);
} else if left_area_width <= width {
statusline.append(&mut left);
}
statusline.into()
fn append(buffer: &mut Spans, text: String, base_style: &Style, style: Option<Style>) {
buffer.0.push(Span::styled(
text,
style.map_or(*base_style, |s| (*base_style).patch(s)),
));
}
fn get_render_function<'a>(
element_id: StatusLineElementID,
) -> impl Fn(&RenderContext) -> Spans<'a> {
fn get_render_function<F>(element_id: StatusLineElementID) -> impl Fn(&mut RenderContext, F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
match element_id {
helix_view::editor::StatusLineElement::Mode => render_mode,
helix_view::editor::StatusLineElement::Spinner => render_lsp_spinner,
@@ -158,40 +166,48 @@ fn get_render_function<'a>(
}
}
fn render_mode<'a>(context: &RenderContext) -> Spans<'a> {
fn render_mode<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let visible = context.focused;
let config = context.editor.config();
let modenames = &config.statusline.mode;
let modename = if visible {
match context.editor.mode() {
Mode::Insert => modenames.insert.clone(),
Mode::Select => modenames.select.clone(),
Mode::Normal => modenames.normal.clone(),
}
} else {
// If not focused, explicitly leave an empty space.
" ".into()
};
let modename = format!(" {} ", modename);
if visible && config.color_modes {
Span::styled(
modename,
write(
context,
format!(
" {} ",
if visible {
match context.editor.mode() {
Mode::Insert => &modenames.insert,
Mode::Select => &modenames.select,
Mode::Normal => &modenames.normal,
}
} else {
// If not focused, explicitly leave an empty space instead of returning None.
" "
}
),
if visible && config.color_modes {
match context.editor.mode() {
Mode::Insert => context.editor.theme.get("ui.statusline.insert"),
Mode::Select => context.editor.theme.get("ui.statusline.select"),
Mode::Normal => context.editor.theme.get("ui.statusline.normal"),
},
)
.into()
} else {
Span::raw(modename).into()
}
Mode::Insert => Some(context.editor.theme.get("ui.statusline.insert")),
Mode::Select => Some(context.editor.theme.get("ui.statusline.select")),
Mode::Normal => Some(context.editor.theme.get("ui.statusline.normal")),
}
} else {
None
},
);
}
// TODO think about handling multiple language servers
fn render_lsp_spinner<'a>(context: &RenderContext) -> Spans<'a> {
fn render_lsp_spinner<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let language_server = context.doc.language_servers().next();
Span::raw(
write(
context,
language_server
.and_then(|srv| {
context
@@ -202,11 +218,14 @@ fn render_lsp_spinner<'a>(context: &RenderContext) -> Spans<'a> {
// Even if there's no spinner; reserve its space to avoid elements frequently shifting.
.unwrap_or(" ")
.to_string(),
)
.into()
None,
);
}
fn render_diagnostics<'a>(context: &RenderContext) -> Spans<'a> {
fn render_diagnostics<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let (warnings, errors) = context
.doc
.diagnostics()
@@ -221,28 +240,29 @@ fn render_diagnostics<'a>(context: &RenderContext) -> Spans<'a> {
counts
});
let mut output = Spans::default();
if warnings > 0 {
output.0.push(Span::styled(
write(
context,
"".to_string(),
context.editor.theme.get("warning"),
));
output.0.push(Span::raw(format!(" {} ", warnings)));
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
}
if errors > 0 {
output.0.push(Span::styled(
write(
context,
"".to_string(),
context.editor.theme.get("error"),
));
output.0.push(Span::raw(format!(" {} ", errors)));
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
}
output
}
fn render_workspace_diagnostics<'a>(context: &RenderContext) -> Spans<'a> {
fn render_workspace_diagnostics<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let (warnings, errors) =
context
.editor
@@ -258,49 +278,51 @@ fn render_workspace_diagnostics<'a>(context: &RenderContext) -> Spans<'a> {
counts
});
let mut output = Spans::default();
if warnings > 0 || errors > 0 {
output.0.push(Span::raw(" W "));
write(context, " W ".into(), None);
}
if warnings > 0 {
output.0.push(Span::styled(
write(
context,
"".to_string(),
context.editor.theme.get("warning"),
));
output.0.push(Span::raw(format!(" {} ", warnings)));
Some(context.editor.theme.get("warning")),
);
write(context, format!(" {} ", warnings), None);
}
if errors > 0 {
output.0.push(Span::styled(
write(
context,
"".to_string(),
context.editor.theme.get("error"),
));
output.0.push(Span::raw(format!(" {} ", errors)));
Some(context.editor.theme.get("error")),
);
write(context, format!(" {} ", errors), None);
}
output
}
fn render_selections<'a>(context: &RenderContext) -> Spans<'a> {
fn render_selections<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let count = context.doc.selection(context.view.id).len();
Span::raw(format!(
" {} sel{} ",
count,
if count == 1 { "" } else { "s" }
))
.into()
write(
context,
format!(" {} sel{} ", count, if count == 1 { "" } else { "s" }),
None,
);
}
fn render_primary_selection_length<'a>(context: &RenderContext) -> Spans<'a> {
fn render_primary_selection_length<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let tot_sel = context.doc.selection(context.view.id).primary().len();
Span::raw(format!(
" {} char{} ",
tot_sel,
if tot_sel == 1 { "" } else { "s" }
))
.into()
write(
context,
format!(" {} char{} ", tot_sel, if tot_sel == 1 { "" } else { "s" }),
None,
);
}
fn get_position(context: &RenderContext) -> Position {
@@ -314,33 +336,55 @@ fn get_position(context: &RenderContext) -> Position {
)
}
fn render_position<'a>(context: &RenderContext) -> Spans<'a> {
fn render_position<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let position = get_position(context);
Span::raw(format!(" {}:{} ", position.row + 1, position.col + 1)).into()
write(
context,
format!(" {}:{} ", position.row + 1, position.col + 1),
None,
);
}
fn render_total_line_numbers<'a>(context: &RenderContext) -> Spans<'a> {
fn render_total_line_numbers<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let total_line_numbers = context.doc.text().len_lines();
Span::raw(format!(" {} ", total_line_numbers)).into()
write(context, format!(" {} ", total_line_numbers), None);
}
fn render_position_percentage<'a>(context: &RenderContext) -> Spans<'a> {
fn render_position_percentage<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let position = get_position(context);
let maxrows = context.doc.text().len_lines();
Span::raw(format!("{}%", (position.row + 1) * 100 / maxrows)).into()
write(
context,
format!("{}%", (position.row + 1) * 100 / maxrows),
None,
);
}
fn render_file_encoding<'a>(context: &RenderContext) -> Spans<'a> {
fn render_file_encoding<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let enc = context.doc.encoding();
if enc != encoding::UTF_8 {
Span::raw(format!(" {} ", enc.name())).into()
} else {
Spans::default()
write(context, format!(" {} ", enc.name()), None);
}
}
fn render_file_line_ending<'a>(context: &RenderContext) -> Spans<'a> {
fn render_file_line_ending<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
use helix_core::LineEnding::*;
let line_ending = match context.doc.line_ending {
Crlf => "CRLF",
@@ -359,16 +403,22 @@ fn render_file_line_ending<'a>(context: &RenderContext) -> Spans<'a> {
PS => "PS", // U+2029 -- ParagraphSeparator
};
Span::raw(format!(" {} ", line_ending)).into()
write(context, format!(" {} ", line_ending), None);
}
fn render_file_type<'a>(context: &RenderContext) -> Spans<'a> {
fn render_file_type<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let file_type = context.doc.language_name().unwrap_or(DEFAULT_LANGUAGE_NAME);
Span::raw(format!(" {} ", file_type)).into()
write(context, format!(" {} ", file_type), None);
}
fn render_file_name<'a>(context: &RenderContext) -> Spans<'a> {
fn render_file_name<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = {
let rel_path = context.doc.relative_path();
let path = rel_path
@@ -378,10 +428,13 @@ fn render_file_name<'a>(context: &RenderContext) -> Spans<'a> {
format!(" {} ", path)
};
Span::raw(title).into()
write(context, title, None);
}
fn render_file_absolute_path<'a>(context: &RenderContext) -> Spans<'a> {
fn render_file_absolute_path<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = {
let path = context.doc.path();
let path = path
@@ -391,10 +444,13 @@ fn render_file_absolute_path<'a>(context: &RenderContext) -> Spans<'a> {
format!(" {} ", path)
};
Span::raw(title).into()
write(context, title, None);
}
fn render_file_modification_indicator<'a>(context: &RenderContext) -> Spans<'a> {
fn render_file_modification_indicator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = (if context.doc.is_modified() {
"[+]"
} else {
@@ -402,20 +458,26 @@ fn render_file_modification_indicator<'a>(context: &RenderContext) -> Spans<'a>
})
.to_string();
Span::raw(title).into()
write(context, title, None);
}
fn render_read_only_indicator<'a>(context: &RenderContext) -> Spans<'a> {
fn render_read_only_indicator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = if context.doc.readonly {
" [readonly] "
} else {
""
}
.to_string();
Span::raw(title).into()
write(context, title, None);
}
fn render_file_base_name<'a>(context: &RenderContext) -> Spans<'a> {
fn render_file_base_name<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let title = {
let rel_path = context.doc.relative_path();
let path = rel_path
@@ -425,37 +487,47 @@ fn render_file_base_name<'a>(context: &RenderContext) -> Spans<'a> {
format!(" {} ", path)
};
Span::raw(title).into()
write(context, title, None);
}
fn render_separator<'a>(context: &RenderContext) -> Spans<'a> {
fn render_separator<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let sep = &context.editor.config().statusline.separator;
Span::styled(
write(
context,
sep.to_string(),
context.editor.theme.get("ui.statusline.separator"),
)
.into()
Some(context.editor.theme.get("ui.statusline.separator")),
);
}
fn render_spacer<'a>(_context: &RenderContext) -> Spans<'a> {
Span::raw(" ").into()
fn render_spacer<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
write(context, String::from(" "), None);
}
fn render_version_control<'a>(context: &RenderContext) -> Spans<'a> {
fn render_version_control<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
let head = context
.doc
.version_control_head()
.unwrap_or_default()
.to_string();
Span::raw(head).into()
write(context, head, None);
}
fn render_register<'a>(context: &RenderContext) -> Spans<'a> {
fn render_register<F>(context: &mut RenderContext, write: F)
where
F: Fn(&mut RenderContext, String, Option<Style>) + Copy,
{
if let Some(reg) = context.editor.selected_register {
Span::raw(format!(" reg={} ", reg)).into()
} else {
Spans::default()
write(context, format!(" reg={} ", reg), None)
}
}

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