Compare commits

...

594 Commits

Author SHA1 Message Date
Michael Davis
faf0cdebc1 Append changes to history before & after each command
Related to <https://redirect.github.com/helix-editor/helix/pull/7226>
2024-04-24 08:08:48 -04:00
Yoav Lavi
50c90cb47c Add support for highlighting any .*ignore file (#10579) 2024-04-24 07:06:19 -05:00
David Else
22960e0d70 Refactor Dark Plus and add new maintainer (#10574)
* Make dark_plus.toml more accurate to VSCode

* theme(dark_plus): make type.builtin blue

* Refactor dark_plus and add myself as new maintainer

Co-authored-by: NAME <NAME@EXAMPLE.COM>

---------

Co-authored-by: Luca Saccarola <96259932+saccarosium@users.noreply.github.com>
Co-authored-by: Luca Saccarola <github.e41mv@aleeas.com>
Co-authored-by: NAME <NAME@EXAMPLE.COM>
2024-04-24 11:40:04 +02:00
Krishan
89a9f2be78 specify direction for select_prev_sibling and select_next_sibling (#10542)
* specify direction for select_prev_sibling and select_next_sibling

* fix failing integration-test
2024-04-23 08:41:03 -05:00
Kirawi
e18b772654 Remove kirawi from dark_plus maintainer list (#10543) 2024-04-21 23:53:31 -04:00
Pascal Kuthe
38ee845b05 don't overload LS with completion resolve requests
While moving completion resolve to the event system in #9668 we introduced what
is essentially a "DOS attack" on slow LSPs. Completion resolve requests were
made in the render loop and debounced with a timeout. Once the timeout expired
the resolve request was made. The problem is the next frame would immediately
request a new completion resolve request (and mark the old one as obsolete but
because LSP has no notion of cancelation the server would still process it). So
we were in essence sending one completion request to the server every 150ms and
only stopped if the server managed to respond before we rendered a new frame.
This caused overload on slower machines/with slower LS.

In this PR I revamped the resolve handler so that a request is only ever
resolved once. Both by checking if a request is already in-flight and by marking
failed resolve requests as resolved.
2024-04-22 12:27:47 +09:00
Pascal Kuthe
b834806dbc use newtype parttern for langauge server id 2024-04-22 12:27:47 +09:00
Matouš Dzivjak
d140072fdc feat(themes): jump-label for modus themes (#10538)
Add styling for jump-labels for modus themes. I couldn't find
any official approach here so picking `yello-cooler`. `cooler` is
used for other meta highlights by modus and yellow seems to be
used the least - only warnings, so there's little chance of colliding
with other highlights.
2024-04-21 09:42:50 -05:00
Simran Kedia
26d9610e78 Ignore .svn version control files (#10536)
Co-authored-by: Simran Kedia <simk@fb.com>
2024-04-21 12:00:30 +09:00
Triton171
efae85ec20 Simplify first-in-line computation for indent queries. (#10527) 2024-04-20 18:58:54 -04:00
dependabot[bot]
35b6aef5fb build(deps): bump the rust-dependencies group with 8 updates (#10532)
Bumps the rust-dependencies group with 8 updates:

| Package | From | To |
| --- | --- | --- |
| [tree-sitter](https://github.com/tree-sitter/tree-sitter) | `0.22.2` | `0.22.5` |
| [serde](https://github.com/serde-rs/serde) | `1.0.197` | `1.0.198` |
| [serde_json](https://github.com/serde-rs/json) | `1.0.115` | `1.0.116` |
| [encoding_rs](https://github.com/hsivonen/encoding_rs) | `0.8.33` | `0.8.34` |
| [chrono](https://github.com/chronotope/chrono) | `0.4.37` | `0.4.38` |
| [anyhow](https://github.com/dtolnay/anyhow) | `1.0.81` | `1.0.82` |
| [clipboard-win](https://github.com/DoumanAsh/clipboard-win) | `5.3.0` | `5.3.1` |
| [cc](https://github.com/rust-lang/cc-rs) | `1.0.90` | `1.0.95` |


Updates `tree-sitter` from 0.22.2 to 0.22.5
- [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.2...v0.22.5)

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

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

Updates `encoding_rs` from 0.8.33 to 0.8.34
- [Commits](https://github.com/hsivonen/encoding_rs/compare/v0.8.33...v0.8.34)

Updates `chrono` from 0.4.37 to 0.4.38
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.37...v0.4.38)

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

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

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

---
updated-dependencies:
- dependency-name: tree-sitter
  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: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: encoding_rs
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: chrono
  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: clipboard-win
  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-04-20 23:49:01 +09:00
Chris Sergienko
345e687573 feat: update bash grammar to latest tree-sitter-bash rev (#10526) 2024-04-20 07:41:55 -05:00
Ben Fekih, Hichem
4b8bcd2773 popup: call required_size only once while rendering
to speed up the rendering a little

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2024-04-20 08:39:12 -04:00
Ben Fekih, Hichem
af4ff80524 Improve popup position
Make the popup positions more consistent.
Improvements:
1. if the signature popup content is bigger than the available space,
   then the popup is always shown under the cursor, even if there more
   space above the cursor than below
2. There is no mutation anymore inside required_size. Maybe in the future
   we can update all widgets to have no mutations and change the trait

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2024-04-20 08:39:12 -04:00
Michael Davis
211f368064 Respect mode when starting a search (#10505)
Currently the editor mode has no effect on the behavior of `search` and
`rsearch`. We can pass in the right movement for the editor mode to make
the behavior or `search` and `rsearch` match `search_next` and
`search_prev` in select mode.
2024-04-20 10:25:11 +09:00
Kevin Vigor
18d5cacea6 Override crossterm's support for NO_COLOR. (#10514)
Since helix isn't usable without color support, honoring this does
nobody any good.
2024-04-20 10:19:12 +09:00
RoloEdits
94405f3d07 refactor(themes): gruvbox warnings to yellow1 (#10506) 2024-04-19 23:22:55 +09:00
urly3
98b4df23a3 theme: everblush (#10394)
changed the statusline colors for SELECT mode
  the previous colours seem to be incorrect and quite ugly (sorry).
  I chose the magenta over the cyan that (colors that were already present) as it has
  more contrast with the existing INSERT colour.
  the statusline colours are now inline with eachother, all having the background be the 'background'
  colour, with varying foregrounds.

Co-authored-by: urly3 <u@rl.yyy>
2024-04-19 10:29:49 +09:00
Nuke
2209effb02 Update lang-support.md for new wiki page name (#10508)
The prior URL invites you to create a new wiki page. I think https://github.com/helix-editor/helix/wiki/Language-Server-Configurations is the correct place to point to now.

There might be more issues related to changes in wiki structure that are not caught by some CI link check because of this (it's a valid URL, just not what you want to direct to lol)
2024-04-19 10:26:04 +09:00
Rolo
34291f0f3b feat(themes): add ui.virtual.ruler for base16_default_dark 2024-04-18 11:50:23 -04:00
Rolo
4e16956007 feat(themes): add ui.virtual.ruler for base16_default_light 2024-04-18 11:50:23 -04:00
Rolo
bb57686854 feat(themes): add ui.virtual.ruler for base16_terminal 2024-04-18 11:50:23 -04:00
Rolo
ccb0c40b5e feat(themes): add ui.virtual.ruler for mellow 2024-04-18 11:50:23 -04:00
Rolo
785d09e38f feat(themes): add ui.virtual.ruler for poimandres
This change also propagates to `poimandres-storm`
2024-04-18 11:50:23 -04:00
Rolo
6fdc1d6a95 feat(themes): add ui.virtual.ruler for varua 2024-04-18 11:50:23 -04:00
Rolo
c0aadfd4ce feat(themes): add ui.virtual.ruler for vim_dark_high_contrast 2024-04-18 11:50:23 -04:00
Rolo
368b29ca72 feat(themes): add ui.virtual.ruler for base16_default 2024-04-18 11:50:23 -04:00
Rolo
be8dc22272 feat(themes): add ui.virtual.ruler for horizon-dark 2024-04-18 11:50:23 -04:00
Rolo
a5a9827f32 fix(themes): correct typo in theme.toml 2024-04-18 11:50:23 -04:00
Rolo
88da9e857c feat(themes): add ui.virtual.ruler for default 2024-04-18 11:50:23 -04:00
Rolo
4713eb06b1 refactor(themes): change solarized_* ruler to bg
Also changed the colors to better blend with the theme.
2024-04-18 11:50:23 -04:00
Rolo
6bdc6f460e refactor(themes): removed ui.highlight effects from solarized_light
This now matches `solarized_dark` changes from #10261
2024-04-18 11:50:23 -04:00
ves
97f683b336 Improve HTML highlighting (#10503) 2024-04-18 15:57:26 +09:00
Blaž Hrastnik
8924691c5d minor: Update docs 2024-04-18 14:59:46 +09:00
Blaž Hrastnik
f06a166962 Add Move language support 2024-04-18 14:57:23 +09:00
Daniel O'Brien
1d23796ad1 Fix kanagawa theme when using cursorline (#10500) 2024-04-17 17:58:33 -05:00
Sean Perry
30baff907d Implement read command (#10447)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
Co-authored-by: Ibrahim Dursun <ibrahim@dursun.cc>
2024-04-17 17:57:57 -05:00
ath3
521accaf00 Include "change" in textobject autoinfo (#10496) 2024-04-17 17:29:28 +02:00
Gaëtan Lehmann
ab203b5f53 update earthfile grammar and highlight queries (#10489) 2024-04-17 20:15:16 +09:00
Pascal Kuthe
1cce693bef correctly describe behavior of C in tutor (#10465) 2024-04-17 09:06:05 +09:00
Clara Smyth
43dff1c772 Fix: Svelte queries (#10487) 2024-04-16 23:56:43 +02:00
Jonathan Lebon
36ee9ba7d6 languages/rust: add rust-script and cargo shebangs (#10484)
The former is one of the more popular forks of the original idea:

https://rust-script.org/

The latter is an RFC for folding that functionality into cargo itself,
available on nightly:

https://doc.rust-lang.org/nightly/cargo/reference/unstable.html#script
2024-04-16 22:47:18 +02:00
Hichem
69e08d9e91 allow cycling through function signatures/overloads (#9974)
implement handle_event to cycle through the function signatures.

To change the signature press alt+p/n .

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2024-04-16 20:57:22 +02:00
Rowan Lovejoy
7775b35cba Add a warning in docs about conflicts with terminal default key bindings (#10380)
Add a warning about conflicts with terminal default key bindings.
2024-04-16 09:13:02 -05:00
Kieran Moy
50470f755f Add missing hyprlang support (#10383) 2024-04-16 16:11:01 +02:00
Idobenhamo
68765f51c9 Support Typst 0.11 (#10321)
* Update the tree sitter to support Typst 0.11 and changed the lsp to Tinymist

* Fixed

* Added typst-lsp & tinymist

---------

Co-authored-by: Idobenhamo <idobenhamo@users.noreply.github.com>
2024-04-16 16:00:13 +02:00
Matthew Bourke
8e161723ee Enabled traversing multiple buffers at once (#10463)
* Enable traversing multiple buffers at once

* run cargo fmt

* simplify iterator call
2024-04-16 15:59:45 +02:00
Alexis-Lapierre
8256ca7bc3 Add support for Xena OpenAutomation files (#10448)
Add support for .xtc/.xoa/.xpc files

* XTC stand for Xena Traffic Configuration
* XOA stand for Xena OpenAutomation
* XPC stand for Xena Port Configuration

Theses three file time seems to be the most common file extension I
encountered in the wild
2024-04-16 15:33:50 +02:00
blinxen
70459b2b66 Update gix to version 0.62 (#10451)
This update contains a security fix for
https://rustsec.org/advisories/RUSTSEC-2024-0335.html
2024-04-15 14:44:00 -05:00
Pedro Fedricci
0546273570 chore: update tree-sitter-rust to v0.21.0 (#10365)
* chore: update tree-sitter-rust to 0.21.0

* fix: pretty print and textobject tests
2024-04-15 18:07:15 +02:00
Sufian
1245760595 Add bufferline and cursorline colors to vim dark theme (#10444) 2024-04-15 10:08:55 -05:00
Christopher Kaster
9df1266376 Add lldb-dap debugger support for Odin (#10175) 2024-04-14 02:34:57 +02:00
Matthew Toohey
081f7d0bd8 Update tree-sitter-ld and highlights (#10379) 2024-04-13 02:32:24 +02:00
Kirawi
6d363a978d Read symlink when writing files (#10339)
Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2024-04-11 20:49:16 -05:00
Arthur
c9ae694aff fix(languages/helm): recognize _*.tpl as helm (#10344) 2024-04-11 17:59:00 +02:00
Alexander Brevig
009a5498ca add LDIF support (#10330)
* feat(lang): add LDIF support

* style: no unnecessary glob

* Update runtime/queries/ldif/highlights.scm

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

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-04-11 17:17:44 +02:00
Blaž Hrastnik
4fc0a4dafc Improve solidity highlighting 2024-04-11 16:16:47 +09:00
Josh Robson Chase
34c7eb4bd4 themes/monokai: add inlay-hint style (#10334)
Matching comment styling so that it doesn't get confused for actual
code.
2024-04-10 10:36:37 -05:00
Pascal Kuthe
73d26d0d97 don't manually grapheme align ts highlights (#10310) 2024-04-11 00:14:08 +09:00
blt-r
b974716b92 Hightlight meson.options as meson file (#10323) 2024-04-09 20:17:52 +02:00
Pascal Kuthe
f601b7c278 fix char/byte index mixup in overlay rendering (#10317) 2024-04-10 01:49:30 +09:00
VKondakoff
c5e257b81c Update nord.toml (#10307)
When "nord2" color is used in ui.selection it is almost invisible
if cursorline highlighting is enabled. Changing the color to "nord3" fixes the issue.
2024-04-09 16:43:21 +02:00
Valentin B
b8ddb2f114 feat(solidity): add textobject queries for solidity (#10318)
* feat: add textobject queries for solidity

* feat(solidity): add parameter textobject query for call expressions

* feat(solidity): add more textobject queries for parameters

* feat(solidity): add yul function textobject query

* feat(solidity): add textobject query for emit statement arguments

* feat(solidity): add textobject query for revert call arguments

* feat(solidity): update tree-sitter grammar and fix typo

* docs: update auto-generated docs

* fix(solidity): fix identifiers highlight query priority

* feat(solidity): add "abstract" to keywords list

* feat(solidity): add highlight query for type alias

* feat(solidity): add variable builtin highlight queries
2024-04-09 16:42:51 +02:00
Skyler Hawthorne
c99c333337 Use new in-crate TreeCursor 2024-04-09 10:33:15 -04:00
Skyler Hawthorne
fa67c5c474 feat(command): select_all_children 2024-04-09 10:33:15 -04:00
Skyler Hawthorne
87c4161732 feat(command): select_all_siblings 2024-04-09 10:33:15 -04:00
Skyler Hawthorne
cf9b88f9bd add Range::{from_node,into_byte_range} 2024-04-09 10:33:15 -04:00
Evgeniy Tatarkin
07cb24abdd Respect lsp definition order for code actions (#9590) 2024-04-09 09:28:54 -05:00
Yomain
cf99615b43 material theme: fix statusline color (#10308) 2024-04-09 09:44:49 +09:00
dependabot[bot]
55346f32b5 build(deps): bump peaceiris/actions-mdbook from 1 to 2 (#10314)
Bumps [peaceiris/actions-mdbook](https://github.com/peaceiris/actions-mdbook) from 1 to 2.
- [Release notes](https://github.com/peaceiris/actions-mdbook/releases)
- [Changelog](https://github.com/peaceiris/actions-mdbook/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-mdbook/compare/v1...v2)

---
updated-dependencies:
- dependency-name: peaceiris/actions-mdbook
  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-04-09 09:41:41 +09:00
dependabot[bot]
cf259e37c8 build(deps): bump peaceiris/actions-gh-pages from 3 to 4 (#10313)
Bumps [peaceiris/actions-gh-pages](https://github.com/peaceiris/actions-gh-pages) from 3 to 4.
- [Release notes](https://github.com/peaceiris/actions-gh-pages/releases)
- [Changelog](https://github.com/peaceiris/actions-gh-pages/blob/main/CHANGELOG.md)
- [Commits](https://github.com/peaceiris/actions-gh-pages/compare/v3...v4)

---
updated-dependencies:
- dependency-name: peaceiris/actions-gh-pages
  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-04-09 09:41:11 +09:00
RoloEdits
ae85f5ff71 refactor(themes): removed ui.highlight effects from solarized_dark (#10261) 2024-04-08 20:02:02 +02:00
RoloEdits
c499f9aa55 feat(themes): add ruler for adwaita-dark (#10260) 2024-04-08 20:01:40 +02:00
Alexander Brevig
be8afe1bfe chore: remove unused xtask themelint (#10294)
* chore: remove unused xtask themelint

* chore(clippy): remove unused themes
2024-04-08 18:15:31 +02:00
Hichem
ea2a4858b7 Fix scrolling to the end within a popup (#10181)
when the available height for the popup is low/small, then it is not
possible to scroll until the end

Signed-off-by: Ben Fekih, Hichem <hichem.f@live.de>
2024-04-08 10:41:03 -05:00
Diego
92338bc207 fix mouse right click selection (#10067) 2024-04-08 10:20:53 -05:00
Bertrand Bousquet
d180395461 Varua theme jump label support (#10299) 2024-04-08 22:53:10 +09:00
Yomain
1e7c01d75b material theme: add diagnostics and other missing stuff (#10290) 2024-04-08 14:47:44 +09:00
Pascal Kuthe
6f5ea6be58 fix off by one error for completion-replace option (#10279) 2024-04-08 12:31:23 +09:00
Gary Miller
0da809c981 feat: Add ADL language support (#10029)
* feat: Add ADL language support

* removed error match & change captures to match https://docs.helix-editor.com/master/themes.html\#syntax-highlighting

* fixes to grammar, highlight changes based on PR and grammar fixes
2024-04-08 04:45:20 +02:00
Pebrianz
e663dafcd8 Add angular language server (#10166) 2024-04-08 04:32:29 +02:00
TornaxO7
e69292e5eb Improve goto_file_impl (#9065) 2024-04-06 16:27:29 -05:00
Slug
97afd67fca Add jump-label style for dark high contrast (#10102) 2024-04-06 16:49:14 +09:00
Michael Davis
3f2de21342 Handle partial failure when sending textDocument/didSave (#10168) 2024-04-06 13:38:51 +09:00
Eduardo Farinati
d3bfa3e063 Fix crash on lsp text edits with invalid ranges (#9649) 2024-04-06 04:17:22 +02:00
Ricardo Fernández Serrata
1ba5763a0c recognize more files and shebangs (#10120)
* recognize `.node_repl_history` as JS

* recognize `bun` shebang

also add comments explaining Deno & Bun history files

* recognize `.python_history` file and `python3` shebang

* recognize more shells

* rm Py3 shebang

Co-authored-by: ath3 <45574139+ath3@users.noreply.github.com>

* rm non-standard deno/bun hist recognition

---------

Co-authored-by: ath3 <45574139+ath3@users.noreply.github.com>
2024-04-05 22:44:39 +02:00
Matthew Toohey
a2ee2e66f2 Add koka language server and update grammar (#10119) 2024-04-05 18:53:07 +09:00
Michael Davis
f240d896a4 Merge unnecessary/deprecated diagnostic highlights separately (#10084)
Previously unnecessary/deprecated diagnostic tags replaced the highlight
for the severity of a diagnostic. This could cause either the severity
or unnecessary/deprecated scopes to disappear when diagnostic ranges
overlapped though. Plus the severity highlight can be interesting in
addition to the unnecessary/deprecated highlight.

So this change separates the unnecessary and deprecated highlights from
the severity highlights, so each is merged separately and when they
overlap, the highlights are combined.
2024-04-05 14:50:41 +09:00
Karem Abdul-Samad
d053886fe3 Add new keyword as per PEP 695 (#10165) 2024-04-05 02:22:41 +02:00
Aito Stukas
5f44890176 Remove duplicate keyword highlight for Python 'and' (#10160) 2024-04-04 19:12:19 -05:00
Harishankar G
962c714185 Use an enum to represent yank decision during deletion of a selection (#10148) 2024-04-05 00:18:48 +09:00
Tshepang Mbambo
3e2b85c61b remove redundant imports (#10154) 2024-04-04 23:28:44 +09:00
Michael Davis
1539312558 Add a serializer for the jump alphabet config option (#10156)
Without a serializer, we drop the custom alphabet when making config
modifications like with `:set`. For example before this commit,
`:set mouse false` would reset a custom alphabet to the default.
2024-04-04 23:28:31 +09:00
Blaž Hrastnik
5fece0054d Disable todotxt grammar for files named "todo.txt"
The file name is too common and can unintentionally conflict. Users
should manually opt in.

Fixes #10113
2024-04-04 10:35:44 +09:00
Eloi Torrents
88f2931153 Add tcl highlighting (#9837)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-04-04 03:31:00 +02:00
André Sá
f16380d418 Add jumb label color to ayu themes (#10133) 2024-04-04 10:17:51 +09:00
Jens Getreu
fa79ce59a0 Autumn theme: add jump label (#10134) 2024-04-04 10:17:40 +09:00
Harishankar G
f773972241 Use a boolean to indicate whether to yank during deletion of a selection (#10132) 2024-04-03 12:40:55 -05:00
Takumi Matsuura
84cc42e5ee typofix (#10125) 2024-04-03 23:05:48 +09:00
Evgeniy Tatarkin
e54e3f07cb feat(languages): Tiltfile as starlark language (#10072) 2024-04-03 10:32:12 +09:00
Angus Dippenaar
e0ec34421b languages: update templ (#10114) 2024-04-03 10:31:44 +09:00
Gaëtan Lehmann
226b2755fb add earthfile language configuration (#10111) 2024-04-03 10:30:15 +09:00
Sampo Siltanen
54844241ce Add fsharp block comment tokens to languages.toml (#10108) 2024-04-03 10:29:30 +09:00
Jesús González
96f7d73f3d Add jump label support to Darcula themes (#10116) 2024-04-02 16:38:22 -05:00
Luis Useche
190fbf66d4 reconfigure tree properly after closing view (#10004)
This patch merges the last child of a container node to the parent. This
avoids the bug where uneven spaced Views would be created. To reproduce:
1. `vsplit`
2. `split`
3. `quit`
4. `vsplit`

With this patch the bug cannot be seen anymore.
2024-04-02 17:29:57 +02:00
woojiq
cc8290f393 show newest options in jumplist picker first, fix docs (#10095)
* docs: mention `<space>g` changed file picker

* picker(jumplist): show jumps in order from latest to oldest
2024-04-02 16:39:44 +02:00
Henrik Tjäder
e63ed0579d Theme: PaperColor: Tune jump-label (#10104) 2024-04-02 15:59:29 +02:00
Clément Delafargue
545ff1aac6 add support for jump labels in tokyonight themes (#10106) 2024-04-02 08:15:12 -05:00
dependabot[bot]
bb0bfa23f3 build(deps): bump the rust-dependencies group with 3 updates (#10099)
Bumps the rust-dependencies group with 3 updates: [serde_json](https://github.com/serde-rs/json), [chrono](https://github.com/chronotope/chrono) and [tokio](https://github.com/tokio-rs/tokio).


Updates `serde_json` from 1.0.114 to 1.0.115
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.114...v1.0.115)

Updates `chrono` from 0.4.35 to 0.4.37
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.35...v0.4.37)

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

---
updated-dependencies:
- dependency-name: serde_json
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: chrono
  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
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-02 11:26:28 +09:00
RoloEdits
b24c465a08 uswd lldb-dab instead of lldb-dap in the default dap configuration (#10091)
`LLVM v18.0.0` changed the name here: llvm/llvm-project#69264

Current users of `lldb-vscode` not able to upgrade will have to create a
symlink so that `lldb-dap` links to `lldb-vscode`.

BREAKING CHANGE: debugger looks for `lldb-dap` rather than `lldb-vscode`

closes: #9964
2024-04-02 02:49:26 +02:00
Manuel Mendez
4b236f30db Add Buildkite command/commands support to yaml injections (#10090)
* Use any-of instead of match in yaml injections

Go back to original intent when this code was first written now that the
tree-sitter version used supports any-of?.

* yaml/injections: Add injection for Buildkite

Buildkite uses `command` (`commands` is an alias) to define the
command(s) to run for the step.
2024-04-02 02:48:08 +02:00
Maksym Pavlenko
8635913f0e Dim primary selection in kanagawa (#10094)
* [kanagawa] Change selection color

Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>

* [kanagawa] Use less brigh selection color

Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>

---------

Signed-off-by: Maksym Pavlenko <pavlenko.maksym@gmail.com>
2024-04-02 00:40:40 +02:00
Pawel Grzybek
d56f3fb755 Add jump-label styling for base16 themes (#10076)
* add jump-label styling for base16_transparent theme

* keep the change consistent with other themes from base16 family

* update jump-labels color to blue and make them bold
2024-04-01 18:45:55 +02:00
Kirawi
88d455afeb Use a temporary file for writes (#9236)
Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2024-03-31 17:43:09 -05:00
Jonathan LEI
a224ee5079 Changed file picker (#5645)
Co-authored-by: WJH <hou32hou@gmail.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2024-03-31 17:36:28 -05:00
Daniel S Poulin
1abb64e48d Add textobject for entries/elements of list-like things (#8150) 2024-03-31 11:53:15 -05:00
Krishan
47ef9f0ca2 add new theme keys for LSP diagnostic tags (#10064) 2024-03-31 16:00:14 +02:00
ftqo
2f4572d91e add flexoki theme (#10002)
* add flexoki theme

* change ruler color to bg-2
2024-03-31 14:50:37 +02:00
Ni
0c3bc46ac4 added ../gleam/injections.scm (#10062)
Co-authored-by: Ni <ni@superboring.net>
2024-03-31 14:48:29 +02:00
Gokul Soumya
957d030be9 Support scrolling popup contents using mouse (#10053)
* Extract popup scrolling code into named functions

* Scroll popup contents on mouse scroll event

* Ignore mouse events outside the popup

* Remove unneeded return statement
2024-03-31 14:47:13 +02:00
Erasin Wang
0da5865695 feat(themes): add jump-label for oneLight theme (#10061) 2024-03-31 14:16:48 +02:00
Pascal Eberhard
9a80f7203d add initial support for bitbake language (#10010)
Signed-off-by: Pascal Eberhard <pascal.eberhard@se.com>
2024-03-31 14:16:19 +02:00
Skyler Hawthorne
f5991657f4 Factor out line ending handling in integration tests (#9921)
Prior to this change, every integration test which wanted its line
endings to be handled transparently across platforms, i.e. test with
the same input that has its platform's line feed characters, converting
the line endings was up to each individual test by calling the
`platform_line` helper function. This significantly increases the amount
of boilerplate one has to copy between all the tests.

However, there are some test cases that need to exert strict control
over the exact input text without being manipulated behind the scenes by
the test framework.

So, with this change, the line feed conversions are factored into
the `TestCase` struct. By default, line endings of the input text
are converted to the platform's native line feed ending, but one can
explicitly specify in their test case when the input text should be left
alone and tested as is.
2024-03-31 07:12:17 -05:00
Matouš Dzivjak
2533b08f64 feat(languages): .mkdn as markdown extension (#10065)
GitHub recognizes `.mkdn` as markdown files. Add `.mkdn` as a valid
file type for Markdown.
2024-03-31 07:03:05 -05:00
Tshepang Mbambo
8b7cbf5280 typo (#10060) 2024-03-31 13:59:46 +02:00
Abderrahmane TAHRI JOUTI
25d8e9445c Add jump-label (#10058) 2024-03-31 00:49:49 +01:00
Dawid Ciężarkiewicz
825ae74b9e chore: add "ui.virtual.jump-label" to solarized colorschemes (#10056) 2024-03-31 00:49:16 +01:00
Pedro Fedricci
96220cb632 feat(themes): add jump-label style for nightfox (#10052) 2024-03-30 22:58:16 +01:00
blt-r
3d6f34c270 Make Helix.appdata.xml spec-compliant (#10051)
- The <description> tag shouldn't be translated, but instead the
  individual paragraphs.
- The <developer> tag is required.
- The <requires> tag actually means that the app can't even be installed
  without that control. What this should be is <recommends>.
2024-03-30 19:51:46 +01:00
Michael Davis
2cadec0b11 Add changelog notes for 24.03 (#9946)
* changelog: Checkpoint 2024-03-20

commit: b8e79c0ef5

* Bump the version to 24.3.0

* changelog: Checkpoint 2024-03-25

commit: 614a744d24

* Address review comments

* changelog: Checkpoint 2024-03-30

commit: 64c1de1988

* Adjust release day to 2024-03-30

* Linkify
2024-03-30 22:28:02 +09:00
Chris
64c1de1988 Add main.odin to Odin roots (#9968) 2024-03-30 07:40:38 -05:00
Yannic Nuwenhof
787a237f8b Add JetBrains default dark theme (#9967) 2024-03-30 19:45:38 +09:00
Yomain
381976a04c fix: cursor line on material themes (#10043) 2024-03-30 19:43:53 +09:00
Daniel Flanagan
527cbe656c chore: update tree-sitter-markdown (#10039) 2024-03-29 08:31:27 -05:00
Joe
7ec509e2f9 Add injections for GraphQL and ERB in Ruby (#10036) 2024-03-29 10:45:17 +09:00
mlemesle
c69ad7b3bf Add jump-label to Kanagawa theme + format (#10030)
* Add jump-label to Kanagawa theme + format

* Remove bg color for jump-label

* Remove values alignment
2024-03-28 09:58:50 -05:00
Vladyslav Karasov
f21efc7a2f fix: don't highlight colons (:) in comments (#10014) 2024-03-27 14:31:35 +01:00
Michael Davis
cb286b7a5d Fix rustc warnings about unused 'pub use's (#10011) 2024-03-27 14:34:47 +09:00
RoloEdits
a6c6fa4c8e feat(themes): add jump-label for gruvbox themes (#10012) 2024-03-26 20:48:08 +01:00
Ben Lee-Cohen
cef839e477 feat: add jump format for monokai family (#10009)
Co-authored-by: Ben Lee-Cohen <ben@lee-cohen.com>
2024-03-26 17:18:13 +01:00
Ben Lee-Cohen
1e50fb8b55 Adding jump styles for Nord and Nord Light (#10008)
Co-authored-by: Ben Lee-Cohen <ben@lee-cohen.com>
2024-03-26 15:51:53 +01:00
Michael Davis
07e21a10f0 Revert "helix-term: allow to backspace out-of the command prompt (#9828)" (#10005)
This reverts commit 0dc67ff885.

See the post-merge discussion in #9828. The old behavior was less
surprising and we have other ways to abort from a prompt, so let's
revert the behavior change.
2024-03-26 14:49:39 +09:00
Blaž Hrastnik
a0d6566140 Fix regression in color-modes
Fixes #10006
2024-03-26 14:46:56 +09:00
dependabot[bot]
3205bc4132 build(deps): bump the rust-dependencies group with 6 updates (#10007)
Bumps the rust-dependencies group with 6 updates:

| Package | From | To |
| --- | --- | --- |
| [smallvec](https://github.com/servo/rust-smallvec) | `1.13.1` | `1.13.2` |
| [arc-swap](https://github.com/vorner/arc-swap) | `1.7.0` | `1.7.1` |
| [regex](https://github.com/rust-lang/regex) | `1.10.3` | `1.10.4` |
| [indoc](https://github.com/dtolnay/indoc) | `2.0.4` | `2.0.5` |
| [rustix](https://github.com/bytecodealliance/rustix) | `0.38.31` | `0.38.32` |
| [which](https://github.com/harryfei/which-rs) | `6.0.0` | `6.0.1` |


Updates `smallvec` from 1.13.1 to 1.13.2
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.13.1...v1.13.2)

Updates `arc-swap` from 1.7.0 to 1.7.1
- [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/arc-swap/commits)

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

Updates `indoc` from 2.0.4 to 2.0.5
- [Release notes](https://github.com/dtolnay/indoc/releases)
- [Commits](https://github.com/dtolnay/indoc/compare/2.0.4...2.0.5)

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

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

---
updated-dependencies:
- dependency-name: smallvec
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: arc-swap
  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: indoc
  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: which
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-26 12:55:58 +09:00
Yomain
d4b467090b doc: small formatting corrections (#9986) 2024-03-26 01:08:42 +01:00
Mo
1d1087822a Use the OS path separator instead of / (#10000) 2024-03-26 00:59:33 +09:00
Quentin
614a744d24 Add narrow no-break space support (#9604) 2024-03-25 10:29:36 +09:00
Novus Nota
2d9e336f64 feat: Add Ohm language support (#9991)
* feat: Add `Ohm` language support

Hope this commit makes it into release :)

* Update runtime/queries/ohm/highlights.scm

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

* chore: final newline

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-03-24 23:30:59 +01:00
Armando Pérez Marqués
47995bfb0c Add jump label color ("rosewater") to catppuccin themes (#9983) 2024-03-24 16:02:26 +09:00
Tobias Brunner
7bce9530d3 Add jump label color to rose-pine themes (#9981) 2024-03-24 11:42:57 +09:00
Florent Nuttens
da2dec174a feat: add jump label color to onedark theme 2024-03-24 11:42:35 +09:00
Florent Nuttens
628dcd5c56 feat: add jump label color to dark plus theme 2024-03-24 11:42:35 +09:00
Florent Nuttens
2178adfe93 feat: add jump label color to catppuccin themes 2024-03-24 11:42:35 +09:00
ves
54fab657be Add jump label color to horizon-dark theme (#9984) 2024-03-24 11:42:01 +09:00
Yomain
8f65bfe089 feat: add jump label color to dracula themes (#9973) 2024-03-23 13:40:19 +01:00
Pascal Kuthe
b46064b8c4 Add an Amp-like jump command
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-03-23 15:35:25 +09:00
Pascal Kuthe
3001f22b31 add reverse rope grapheme iterator 2024-03-23 15:35:25 +09:00
Pascal Kuthe
66b9ff1d2a dismiss pending keys properly for mouse/paste 2024-03-23 15:35:25 +09:00
Pascal Kuthe
69e07ab61e use slices instead of Rc for virtual text 2024-03-23 15:35:25 +09:00
Michael Davis
68b21578ac Reimplement tree motions in terms of syntax::TreeCursor
This uses the new TreeCursor type from the parent commit to reimplement
the tree-sitter motions (`A-p/o/i/n`). Other tree-sitter related
features like textobjects are not touched with this change and will
need a different, unrelated approach to solve.
2024-03-23 15:32:34 +09:00
Michael Davis
b1222f0664 Add a TreeCursor type that travels over injection layers
This uses the layer parentage information from the parent commit to
traverse the layers. It's a similar API to `tree_sitter:TreeCursor`
but internally it does not use a `tree_sitter::TreeCursor` currently
because that interface is behaving very unexpectedly. Using the
`next_sibling`/`prev_sibling`/`parent` API on `tree_sitter::Node`
reflects the previous code's behavior so this should result in no
surprising changes.
2024-03-23 15:32:34 +09:00
Michael Davis
6dd46bfe1c syntax: Track parent LanguageLayer IDs
This commit adds a `parent` field to the `LanguageLayer`. This
information is conveniently already available when we parse injections.
This will be used in the child commit to create a type that can
traverse injection layers using this information.
2024-03-23 15:32:34 +09:00
Jaakko Paju
d5c2973cd1 Fix repeat last motion for goto next/prev diagnostic (#9966) 2024-03-23 15:25:08 +09:00
Carter Watson
be307a4204 fix: undefined bufferline colors (#9960) 2024-03-21 23:07:47 +01:00
David Else
c9e34c556b Add rclone.conf as a glob to make it behave as an ini file (#9959) 2024-03-21 16:10:16 -05:00
Michael Davis
f5d95de227 C++: Improve parameter highlighting
This adds parameter highlighting for reference parameters and defaulted
parameters. For example:

```cpp
auto strip_prefix_only(std::string& s,
                       Hidden_Homonym skip_hidden_homonym = {}) const
    -> Affixing_Result<Prefix>;
```

Previously both parameters were only highlighted as variables.
2024-03-22 00:20:29 +09:00
Michael Davis
c099dde2a7 Rust: Highlight extern crate aliases
For example `extern crate alloc as myalloc;`
2024-03-22 00:20:29 +09:00
Michael Davis
9ceeea5a83 Update tree-sitter-gleam and highlights
This contains a few syntax fixes. The highlights have been updated as
well for reserved identifiers and escape sequences
2024-03-22 00:20:29 +09:00
Michael Davis
fdcd461e65 Update tree-sitter-erlang and highlights
A few changes:

* 0-arity type specs like the following previously would not have the
  expected 'variable.parameter' highlighting for the return type:

    -spec foo() -> Value when Value :: term().

* Highlight module, type and function docs as documentation comments
  and inject markdown into them.

* Replace `#match?` predicates with `#any-of?` where possible.

* Remove custom auto-pairs. Now that Erlang uses markdown for
  documentation, the asciidoc-style backtick-singlequote pair is no
  longer useful.
2024-03-22 00:20:29 +09:00
Michael Davis
459eb9a4c1 Recognize 'mmd' as mermaid 2024-03-22 00:20:29 +09:00
Michael Davis
961025433d Update tree-sitter-git-commit
This commit has partial support for escapes within strings.
2024-03-22 00:20:29 +09:00
JR
51739f1290 Create a tutor entry for using splits (#9417)
* WIP

* WIP

* WIP

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* WIP

* WIP

* WIP

* Fix typos

* Fix typos

* Minor updates

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Remove mention of arrows in split tutorial

* Do not mention starting in normal mode

* Fix right drift of titles

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Reflow paragraphs

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

* Update runtime/tutor

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

---------

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-03-21 14:37:08 +01:00
Jordan Reger
5ba36fe9b3 Update rasmus.toml (#9939)
* Update rasmus.toml

Remove "ui.virtual" setting as it selects seemingly random characters to highlight.

* Add ui.virtual.ruler
2024-03-21 12:35:34 +09:00
Michael Davis
7f5fd63835 Evenly space statusline areas when there isn't space to align middle (#9950)
The refactor in bcf7b263 introduced a possible subtraction with overflow
when the statusline is layed out so that the left or right sides are
larger than the padding it would take to align the center area to the
middle.

When the left or right areas are too large, we can evenly space the
elements rather than trying to align the center area to the middle.
This prevents possible underflows and makes sense visually - it's
still easy to tell the areas apart at a glance.
2024-03-21 09:16:58 +09:00
David Else
52a0734120 Add new theme keys for LSP diagnostic tags for dark_plus (#9949) 2024-03-21 00:31:48 +01:00
Hendrik Norkowski
b8e79c0ef5 fix(languages): specify correct comment-token for PKGBUILD files (#9943) 2024-03-20 08:09:45 -04:00
Szabin
bcf7b26393 Refactor statusline elements to build Spans (#9122)
* Refactor statusline elements to return Spans

* Split render fn to build Spans and blit to Surface
2024-03-19 17:31:39 +01:00
Arthur Deierlein
427dd2f383 Add support for ember.js templates (#9902)
* feat: add support for ember .hbs (glimmer) templates

* adjust highlights to helix

* highlight this correctly in block statements

* correctly highlight attributes

* correctly highlight hash_pair

* add newline to highlights.scm

* refactor: use #any-of and #eq instead of #match

* chore: add newline to languages.toml
2024-03-19 17:26:50 +01:00
Kirawi
d9de809a57 add register completion (#9936) 2024-03-19 17:26:32 +01:00
Tobias Hunger
1d1806c85a Ignore more version control systems (#9935)
Ignore `.pijul` and `.jj` as well as `.git`. This makes hx so much more
usable with VCSes other than git!
2024-03-19 15:08:50 +01:00
Arthur Deierlein
4b4947639a feat: add suport for helm charts (#9900) 2024-03-19 15:07:44 +01:00
Cyrill Schenkel
5b8bfc5476 Prevent shell_keep_pipe from stopping on nonzero exit status code (#9817)
The `shell_impl` and `shell_impl_async` functions no longer return
`success` because it was always `true`. If the command didn't succeed
both functions would return an `Err`.

This was also the reason, why `shell_keep_pipe` didn't work. It relied
upon the value of `success` and aborted in case of an `Err`.
It now removes any selection for which `shell_impl` returns `Err`.

If the command always fails, the selections are preserved and an error
message is displayed in the status bar.
2024-03-19 07:30:34 -05:00
Damian Zaręba
485c5cf0b8 Initial Ada language support (after stale) (#9908)
* Adding initial support for ada language, based off #7790 PR from tomekw

* More translation to helix-specific tree-sitter scm labels, add ada gpr switch to ada LSP

* Generate ada in lang-support.md using cargo xtask docgen

* Update tree-sitter definitions according to comments

* Remove .gpr glob from languages.toml

* Fix unit in languages.toml for ada, update locals.scm to helix needs
2024-03-19 13:28:15 +01:00
Arthur Deierlein
0b6dea6dc2 Enhance support for PKGBUILDS (#9909)
* enhance support for PKGBUILDS

* run cargo xtask docgen
2024-03-19 13:27:46 +01:00
Luis Useche
6a22d7d1ca Do not stop reloading docs on error (#9870)
In the `reload-all` command, we should not stop reloading the documents
if one error is found. Instead, we should report the error and continue
trying to reload the current open documents. This is useful in cases
where a backing file does not exist temporarily (e.g. when editing a git
patch in the outstanding chain that doesn't have a file just yet).

This change also remove the error messages in the cases where the
backing is `None`, like in new docs or `tutor`.
2024-03-19 12:16:30 +01:00
Khang Nguyen Duy
4d2282cbcc Respect undercurl config even with no terminfo (#9897)
I have just found out that my recent Windows Terminal version
supported rendering undercurl (see
https://devblogs.microsoft.com/commandline/windows-terminal-preview-1-20-release
). However, looking at the source, terminfo is required for helix to
emit the undercurl control code, which isn't available on Windows AFAIK.

This commit make helix respects the `editor.undercurl` option when
there is no terminfo.

Tested on Windows Terminal Preview 1.20

Signed-off-by: Khang Nguyen <khang.nguyenduycse@hcmut.edu.vn>
2024-03-19 12:12:37 +01:00
dependabot[bot]
3d4889ce9a build(deps): bump the rust-dependencies group with 3 updates (#9929)
Bumps the rust-dependencies group with 3 updates: [bitflags](https://github.com/bitflags/bitflags), [toml](https://github.com/toml-rs/toml) and [lsp-types](https://github.com/gluon-lang/lsp-types).


Updates `bitflags` from 2.4.2 to 2.5.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.4.2...2.5.0)

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

Updates `lsp-types` from 0.95.0 to 0.95.1
- [Changelog](https://github.com/gluon-lang/lsp-types/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gluon-lang/lsp-types/compare/v0.95.0...v0.95.1)

---
updated-dependencies:
- dependency-name: bitflags
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: rust-dependencies
- dependency-name: toml
  dependency-type: direct:production
  update-type: version-update:semver-patch
  dependency-group: rust-dependencies
- dependency-name: lsp-types
  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-03-19 15:47:05 +09:00
mo8it
0f5430ab9e Optimize get_truncated_path 2024-03-19 14:39:46 +09:00
mo8it
e91ec8e880 Optimize getting a relative path 2024-03-19 14:39:46 +09:00
mo8it
6ed93b6e49 Optimize fold_home_dir 2024-03-19 14:39:46 +09:00
mo8it
6607938bf8 Call as_ref only once 2024-03-19 14:39:46 +09:00
Blaž Hrastnik
13533cb99c ci: Group minor rust dependency dependabot updates
https://docs.github.com/en/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file#groups
2024-03-19 14:28:15 +09:00
dependabot[bot]
df8352ec05 build(deps): bump tokio-stream from 0.1.14 to 0.1.15 (#9926)
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.14 to 0.1.15.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.14...tokio-stream-0.1.15)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 14:14:09 +09:00
dependabot[bot]
ef797acf0d build(deps): bump anyhow from 1.0.80 to 1.0.81 (#9925)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.80 to 1.0.81.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.80...1.0.81)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 14:13:35 +09:00
dependabot[bot]
7ef583bea7 build(deps): bump gix from 0.58.0 to 0.61.0 (#9924)
Bumps [gix](https://github.com/Byron/gitoxide) from 0.58.0 to 0.61.0.
- [Release notes](https://github.com/Byron/gitoxide/releases)
- [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Byron/gitoxide/compare/gix-v0.58.0...gix-v0.61.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 14:13:28 +09:00
dependabot[bot]
326c078356 build(deps): bump thiserror from 1.0.57 to 1.0.58 (#9923)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.57 to 1.0.58.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.57...1.0.58)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 14:08:43 +09:00
dependabot[bot]
8dc50bded9 build(deps): bump clipboard-win from 5.2.0 to 5.3.0 (#9922)
Bumps [clipboard-win](https://github.com/DoumanAsh/clipboard-win) from 5.2.0 to 5.3.0.
- [Commits](https://github.com/DoumanAsh/clipboard-win/commits)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-19 14:08:36 +09:00
Jaakko Paju
58022586a0 Add yaml LSP for docker compose (#9916)
* Add yaml LSP for docker compose

* Change docs
2024-03-18 20:39:32 +01:00
Matthew Toohey
2e4653ea31 add koka language support (#8727)
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
2024-03-18 13:14:10 +01:00
Phil
94d210c9bf Add initial support for SuperCollider (#9329) 2024-03-18 13:13:23 +01:00
Michael Davis
4ffe993533 Fix malformed predicates in highlighting queries 2024-03-18 10:34:53 +09:00
Michael Davis
38af99f05f Bump tree-sitter to 0.22.2 2024-03-18 10:34:53 +09:00
Joey Hain
476e6baf8f Add textobject queries for vala (#8541) 2024-03-18 00:11:28 +01:00
Leonardo Eugênio
d99b6177c2 Add blade support (#9513)
* Add php-only language config and queries

php-only is required enabling php injections like in blade templates

* Add blade templates support
2024-03-18 00:07:09 +01:00
Carsten Führmann
eead105f94 Fix selected text background in toykonight (#9789)
Before the fix, the color that the original (Neovim) tokyonight uses for
Neovim's visual mode was used in Helix for highlighting the selected
item in the picker. But for the highlighting of selected _text_ in
Helix (corresponding to Neovim's visual mode), a much darker background
was used. This made it very hard to pick out selected text, in
particular in suboptimal lighting conditions.

I swapped the two colors, so that text selection now looks like in
Neovim, while selected items in the picker are highlighted a bit less
strongly. (But still easy to see because the whole line is highlighted.)
2024-03-18 00:06:23 +01:00
Erasin Wang
64389f97fe Updated grammar for hurl 4 (#9775) 2024-03-18 00:01:25 +01:00
Benedikt Ritter
f7913c1a3b Extend groovy support (#9677)
* Extend groovy support

Use more complete parser introduced in nvm-treesitter in
d4dac523d2

* Update runtime/queries/groovy/locals.scm

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

* Drop indent.scm for groovy

It was copied from the tree-sitter repository but is not
compatiblw with the way indent queries are implemented
in Helix.

* Adapt groovy highlights to helix syntax

* Update documentation

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-03-18 00:01:11 +01:00
Alexis Mousset
8457652da1 Update to modus-themes 4.4.0 (#9912) 2024-03-17 23:56:33 +01:00
Jonas De Vuyst
0301d01e78 Fix URL in doc chapter about Textobject queries (#5773)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-03-17 23:56:17 +01:00
Dan Cardamore
3890376a23 add 'file-absolute-path' to statusline (#4535)
* feat: add 'file-abs-path' to statusline (#4434)

* cleanup implementation

* rename to be non-abbreviated names

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-03-17 23:55:49 +01:00
George "Riye" Hollister
e36774c2c8 Add Support for JSONC (#9906)
* Added `jsonc` language with support for comments

The `vscode-json-language-server` accepts `jsonc` as a language id.
Allowing the use of comments within JSON files.

* fix: Update `injdection-rejex` to be unique

* fix: use includes to remove redundant queries

* ci: Generate language-support docs
2024-03-17 23:54:05 +01:00
Arthur Deierlein
9ec0271873 Add support for hyprland config (#9899)
* feat: add hyprland config language

* adjust indents to helix

* adjust highlights to helix
2024-03-17 23:53:30 +01:00
Arthur Deierlein
61f7d9ce2f fix typo "braket" in jsx highlights (#9910) 2024-03-17 17:36:54 -05:00
Emi
761df60077 Keybind for Extend/shrink selection up and down (#9080)
* implement another selection modifying command

* Selection feels more ergonomic in case of swapping the direction. This also fixes a problem when starting at an empty line.

* rename select_line_up/down to select_line_above/below

* apply clippy suggestion of using cmp instead of if-chain

* revert `Extent` implementing `Clone/Copy`

* move select_line functions below extend_line implementations

* implement help add function, which saturates at the number of text lines

---------

Co-authored-by: Emi <emanuel.boehm@gmail.com>
2024-03-17 23:06:24 +01:00
Nick
6fea7876a4 Fix comment key bind behaviour in OCaml (#9894) 2024-03-16 08:50:47 -04:00
Michael Davis
9282f1b8e5 Handle starting and continuing the count separately (#9887) 2024-03-16 08:52:57 +09:00
Mike Trinkala
b961acf746 Update regex-cursor (#9891) 2024-03-15 14:44:08 -05:00
Kirawi
0c51ab16d0 Add a yank diagnostic command (#9640)
* yank diagnostic command

* improve success message

* move to a typed command

* docgen
2024-03-15 17:38:22 +01:00
Michael Davis
6c4d986c1b Use non-deprecated chrono Duration functions 2024-03-14 14:13:56 +09:00
dependabot[bot]
b44b627b14 build(deps): bump chrono from 0.4.34 to 0.4.35
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.34 to 0.4.35.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.34...v0.4.35)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-14 14:13:56 +09:00
dependabot[bot]
e01a558294 build(deps): bump log from 0.4.20 to 0.4.21 (#9856)
Bumps [log](https://github.com/rust-lang/log) from 0.4.20 to 0.4.21.
- [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.20...0.4.21)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-14 14:13:36 +09:00
fnuttens
3915b04bd9 fix(themes-catppuccin): make inlay hints more legible (#9859) 2024-03-13 13:47:55 +09:00
dependabot[bot]
2d15acdf60 build(deps): bump libloading from 0.8.2 to 0.8.3 (#9857)
Bumps [libloading](https://github.com/nagisa/rust_libloading) from 0.8.2 to 0.8.3.
- [Commits](https://github.com/nagisa/rust_libloading/compare/0.8.2...0.8.3)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 14:45:44 +09:00
dependabot[bot]
ab61874efb build(deps): bump cc from 1.0.88 to 1.0.90 (#9855)
Bumps [cc](https://github.com/rust-lang/cc-rs) from 1.0.88 to 1.0.90.
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.88...1.0.90)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 14:41:48 +09:00
dependabot[bot]
2e2a1d6f61 build(deps): bump open from 5.0.1 to 5.1.2 (#9854)
Bumps [open](https://github.com/Byron/open-rs) from 5.0.1 to 5.1.2.
- [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.0.1...v5.1.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-03-12 14:41:40 +09:00
dependabot[bot]
2d589e74f0 build(deps): bump cachix/install-nix-action from 25 to 26 (#9851)
Bumps [cachix/install-nix-action](https://github.com/cachix/install-nix-action) from 25 to 26.
- [Release notes](https://github.com/cachix/install-nix-action/releases)
- [Commits](https://github.com/cachix/install-nix-action/compare/v25...v26)

---
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-03-12 14:38:19 +09:00
Kalpaj Chaudhari
c145999bff treesitter: Add textobjects for native funcs and constructors (#9806)
This allows native functions and constructors to be accessible as part
of goto_{next,prev}_func.

Change-Id: Ia1234004e8b38e1c5871331a38fcf4f267da935e
2024-03-10 12:23:33 +09:00
Aidan Gauland
3bd493299f Use Nu language for NUON files (#9839) 2024-03-10 12:22:04 +09:00
Markus F.X.J. Oberhumer
0dc67ff885 helix-term: allow to backspace out-of the command prompt (#9828) 2024-03-09 17:02:43 +09:00
Matthew Toohey
e3c6c82828 add linker script language (#9835) 2024-03-09 16:59:56 +09:00
Alexander Brevig
fd89c3c833 fix: close #9771 fix comments with ( and ) (#9800)
* fix: close #9771 update OCaml

* fix: no longer match on ( ) as the underlying grammar handles these

* fix: implement excellent corrections from review

* fix: module -> namespace to match theme scopes
2024-03-08 10:54:17 +09:00
Chris
301dfb07cc Add PowerShell highlighting (#9827) 2024-03-07 23:39:00 +01:00
Mike Trinkala
e27b04735c Fix panic in select_textobject_around (#9832)
Test Document
-------------
```
a)b
```

Steps to Reproduce
------------------
1. %   # select_all
1. ms( # surround_add
1. mam # select_textobject_around

Debug and Release
-----------------
`thread 'main' panicked at 'Attempt to index past end of RopeSlice:
char index 7, RopeSlice char length 6', ropey-1.6.1/src/slice.rs:796:13`

Description
-----------
An index was selected beyond the end of the slice with chars_at. The fix
adds a guard check to `find_nth_open_pair`, like in the other find_nth*
functions.
2024-03-07 13:37:01 -06:00
Mike Trinkala
cb01e52cd8 Fix panic in surround_replace/delete nested multi-cursor (#9815)
Test Document
-------------
```
{{

}
}

```

Steps To Reproduce
------------------
1. 2j  # move_visual_line_down
1. C   # copy_selection_on_next_line
1. mdm # surround_delete

Debug
-----
`assertion failed: last <= from', transaction.rs:597:13`

Release
-------
`called `Result::unwrap()` on an `Err` value: Char range out of bounds:
char range 18446744073709551614..18446744073709551615,
Rope/RopeSlice char length 7', ropey-1.6.1/src/rope.rs:546:37`

Description
-----------

Processing the surrounding pairs in order violates the assertion the
ranges are ordered. To handle nested surrounds all positions have to
be sorted. Also surround_replace has to track the proper replacement
character for each position.
2024-03-08 02:20:07 +09:00
dependabot[bot]
b93fae9c8b build(deps): bump mio from 0.8.9 to 0.8.11 (#9808) 2024-03-06 11:10:59 +09:00
dependabot[bot]
4e5f19df53 build(deps): bump clipboard-win from 5.1.0 to 5.2.0 (#9811) 2024-03-06 11:10:24 +09:00
dependabot[bot]
7a473c7494 build(deps): bump ahash from 0.8.9 to 0.8.11 (#9813) 2024-03-06 11:09:13 +09:00
dependabot[bot]
ea0b244644 build(deps): bump libloading from 0.8.1 to 0.8.2 (#9810) 2024-03-06 11:08:40 +09:00
dependabot[bot]
7d8c86e403 build(deps): bump arc-swap from 1.6.0 to 1.7.0 (#9809) 2024-03-06 11:08:07 +09:00
varris1
3f98891e79 flake.lock: Bump flake inputs to prevent a warning message (#9816) 2024-03-06 00:00:34 +09:00
RoloEdits
cc43e3521e feat(languages): add support for *.Dockerfile file-types naming convention (#9772)
Current `file-types` only supports up to a `Dockerfile.frontend` naming scheme.

With these changes `frontend.Dockerfile` now gives proper highlights and lsp actions.
2024-03-03 18:56:18 +01:00
Mike Trinkala
9267343830 Fix panic when using surround_replace/delete (#9796)
1. Create a document containing `{A}`
1. C-w v # vsplit
1. gl    # goto_line_end
1. b     # move_prev_word_start
1. `     # switch_to_lowercase
1. mrm(  # surround replace
1. C-w v # vsplit

In the debug build surround_replace/delete will immedately assert with
`assertion failed: last <= from', transaction.rs:597:13`. The splits and
lowercase conversion are not needed to trigger the bug.

In the release build the surround becomes `)a(` and the last vsplit
causes the transaction to panic.
`internal error: entered unreachable code:
(Some(Retain(18446744073709551573)))', transaction.rs:185:46`

Since the selection direction is backwards get_surround_pos returns the
pairs reversed but the downstream code assumes they are in the forward
direction.
2024-03-03 18:55:09 +01:00
Mike Trinkala
5bd007266a Fix panic when using join_selections_space (#9783)
Joining lines with Alt-J does not properly select the inserted spaces
when the selection contains blank lines. In the worst case it panics
with an out of bounds index.

thread 'main' panicked at 'called `Result::unwrap()` on an `Err` value:
Char index out of bounds: char index 11, Rope/RopeSlice char length 10'

Steps to reproduce:
* Create a new document
    ```
    a

    b

    c

    d

    e
    ```
* % (Select all)
* Alt-J (join and select the spaces)
2024-03-02 15:05:58 +01:00
Michael Davis
d769fadde0 Fix precedence of svelte typescript injection (#9777) 2024-03-02 16:47:24 +09:00
Malpha
f04dafa2e2 languages.toml: add elvish shebang (#9779) 2024-03-02 16:47:10 +09:00
Michael Davis
5ca6a448e9 Support LSP diagnostic tags (#9780) 2024-03-02 13:37:11 +09:00
Marcin Drzymala
1d6db30acf Fix bug 9703 by commenting out the wrong command (#9778)
* Fix bug 9703 by commenting out the wrong command

This fixes issue https://github.com/helix-editor/helix/issues/9703 by removing the wrong formatting command for justfiles.

* Fix indentation width for justfile
2024-03-02 11:05:17 +09:00
Felix Zeller
062fb819a2 feat: Add markdown-oxide language server (#9758) 2024-03-01 09:10:49 -06:00
Dawid Ciężarkiewicz
44db25939c Document embracing smart-tab navigation. (#9762)
Re #4443
2024-03-01 02:57:31 +01:00
Keir Lawson
d0bb774471 Mark GTK builder ui files as XML (#9754) 2024-02-29 19:09:29 +09:00
JJ
e51a1e4e2a Switch Nim tree-sitter queries to alaviss/tree-sitter-nim (#9722) 2024-02-29 10:49:10 +09:00
Pascal Kuthe
1143f47954 fix split_on_newline (#9756) 2024-02-29 10:47:41 +09:00
Brian Dorsey
f03b91d1b7 update languages.toml: tree-sitter-lua grammar (#9727)
* update languages.toml: tree-sitter-lua grammar

repo has moved, use new URL and the rev of the latest release (v0.0.19)

* update highlight queries

a novice attempt to port query updates from the
source repo to Helix captures and ordering

* Apply suggestions from code review

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

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-02-28 20:55:17 +01:00
Chris
083a9e775d Add support for pde files (#9741) 2024-02-28 13:18:00 +01:00
Mo
00653c772e Avoid cloning the whole paragraph content just for rendering (#9739)
* Avoid cloning the whole paragraph content just for rendering

* Fix tests
2024-02-27 18:24:05 +01:00
Gabriel Dinner-David
26b3dc29be toggling of block comments (#4718) 2024-02-27 22:36:25 +09:00
dependabot[bot]
f46a09ab4f build(deps): bump cc from 1.0.85 to 1.0.88 (#9734)
Bumps [cc](https://github.com/rust-lang/cc-rs) from 1.0.85 to 1.0.88.
- [Release notes](https://github.com/rust-lang/cc-rs/releases)
- [Commits](https://github.com/rust-lang/cc-rs/compare/1.0.85...1.0.88)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 02:06:37 +01:00
巢鹏
358ac6bc1f add fidl support (#9713) 2024-02-27 01:41:50 +01:00
dependabot[bot]
1a82aeeae9 build(deps): bump serde from 1.0.196 to 1.0.197 (#9736)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.196 to 1.0.197.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.196...v1.0.197)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 01:26:24 +01:00
dependabot[bot]
ea95c68775 build(deps): bump serde_json from 1.0.113 to 1.0.114 (#9735)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.113 to 1.0.114.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.113...v1.0.114)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 01:25:52 +01:00
dependabot[bot]
d0f8261141 build(deps): bump tempfile from 3.10.0 to 3.10.1 (#9733)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.10.0 to 3.10.1.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.10.0...v3.10.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 01:25:17 +01:00
dependabot[bot]
b43d9aa306 build(deps): bump ahash from 0.8.6 to 0.8.9 (#9737)
Bumps [ahash](https://github.com/tkaitchuck/ahash) from 0.8.6 to 0.8.9.
- [Release notes](https://github.com/tkaitchuck/ahash/releases)
- [Commits](https://github.com/tkaitchuck/ahash/commits/v0.8.9)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-27 01:22:00 +01:00
Pascal Kuthe
cd02976fa3 switch to regex-cursor (#9422) 2024-02-26 16:45:20 +09:00
Tobias Hunger
c68ec92c5e slint: Update SHA of tree-sitter parser (#9698) 2024-02-26 16:08:31 +09:00
Carsten Führmann
a876148585 Fix colors of tokyonight diagnostic undercurls (#9724) 2024-02-26 10:53:59 +09:00
Michael Davis
928bf80d9a LSP: Normalize diagnostic file paths 2024-02-26 00:12:43 +09:00
Michael Davis
8141a4a1ab LSP: Key diagnostics off file path instead of URI
URIs need to be normalized to be comparable. For example a language
server could send a URI for a path containing '+' as '%2B' but we might
encode this in something like 'Document::url' as just '+'. We can
normalize the URI straight into a PathBuf though since this is the only
value we compare these diagnostics URIs against. This also covers
edge-cases like windows drive letter capitalization.
2024-02-26 00:12:43 +09:00
Ryan Mehri
dfa5382c51 Don't run scheduled builds on forks (#9718) 2024-02-25 19:37:54 +09:00
Mo
6db666fce1 Optimization of tilde expansion (#9709)
* Use next and avoid a redundant prefix strip

* Avoid allocations

Especially when `expand_tilde` is claled on a path
that doesn't contain a tilde.

* Add a test

* Use Into<Cow<…>>

* Put the expand_tilde test at the end of the file

* Remove unused importsw
2024-02-24 16:59:11 +01:00
DS/Charlie
ec9efdef3b Bump tree-sitter-sql (#9634) 2024-02-24 12:28:25 +09:00
wr7
38484f33e5 Completely fix bash autocomplete handling of filenames with spaces (#9708) 2024-02-24 11:54:40 +09:00
Jake Low
03623f2f40 Add osm and osc extensions to xml language filetypes (#9697) 2024-02-24 11:53:16 +09:00
wr7
7100ed4efc Properly handle spaces in filenames in bash autocomplete (#9702) 2024-02-23 16:46:41 +01:00
Michael Davis
b7b6f30084 Use a hook for resolving completion items
Previously we used the IdleTimeout event to trigger LSP
`completion/resolveItem` requests. We can now refactor this to use an
event system hook instead and lower the timeout.
2024-02-22 22:37:23 -05:00
Abderrahmane TAHRI JOUTI
98ebeeebd8 Cyan Light theme : Add License and Author (#9688)
* Cyan Light theme : Add License and Author

* Add License

Copy  license from https://github.com/OlyaB/CyanTheme/blob/master/LICENSE

* better credits to original author
2024-02-21 23:47:14 +09:00
Benedikt Ritter
eca537615a Use groovy support when editing Gradle files (#9681)
The Gradle build tool provides two DSLs for configuring builds. On is
based on Groovy and Gradle build files written in Gradle Groovy DSL use
*.gradle file ending.

This change adds `gradle` to the supported file types of the groovy
language configuration.
2024-02-21 23:46:31 +09:00
dependabot[bot]
cad0209e20 build(deps): bump textwrap from 0.16.0 to 0.16.1 (#9674)
Bumps [textwrap](https://github.com/mgeisler/textwrap) from 0.16.0 to 0.16.1.
- [Release notes](https://github.com/mgeisler/textwrap/releases)
- [Changelog](https://github.com/mgeisler/textwrap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/mgeisler/textwrap/compare/0.16.0...0.16.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-20 17:07:16 +01:00
dependabot[bot]
d2aacb3e36 build(deps): bump anyhow from 1.0.79 to 1.0.80 (#9675)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.79 to 1.0.80.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.79...1.0.80)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-20 17:06:58 +01:00
dependabot[bot]
27335476ed build(deps): bump chrono from 0.4.33 to 0.4.34 (#9673)
Bumps [chrono](https://github.com/chronotope/chrono) from 0.4.33 to 0.4.34.
- [Release notes](https://github.com/chronotope/chrono/releases)
- [Changelog](https://github.com/chronotope/chrono/blob/main/CHANGELOG.md)
- [Commits](https://github.com/chronotope/chrono/compare/v0.4.33...v0.4.34)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-20 17:06:40 +01:00
Volodymyr Chernetskyi
990378a46b Add Groovy grammar (#9350)
* Add Groovy grammar

* Rewrite Neovim captures into Helix for Groovy

* Simplify Groovy injections

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

* Remove Neovim's spell from Groovy highlights

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

* Apply suggestions to languages.toml

* Escape backslash in groovy highlights.scm

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-02-19 18:37:02 +01:00
Jonathan LEI
cdef4f8a70 Make mouse click extend selection in select mode (#5436)
* Make mouse click extend selection in select mode

* chore: better readability with `Option::take()`
2024-02-19 14:08:26 +01:00
nkitsaini
787cc36092 fix LSP ComplitionTriggerKind value for TriggerKind::Auto (#9660) 2024-02-19 13:58:17 +01:00
Jaakko Paju
ebf155d635 Add textobject queries for HCL (#9658)
* Add textobject queries for HCL

* Add to lang-support.md
2024-02-19 00:46:32 +01:00
Jaakko Paju
2dc9ce68ec Add textobject queries for Nix (#9659)
* Add textobject queries for Nix

* Add to lang-support.md
2024-02-19 00:46:13 +01:00
Malpha
c72426cc87 Add docker-compose language (#9661)
* languages: add docker-compose language

it uses docker-compose-langserver as lsp
And yaml for syntax highlighting, indents and injections

* languages: add luajit as a shebang of lua

This helps to provide syntax highlighting and
other lua goodies when writing luajit

* book(update): run cargo xtask docgen

* since #8006 full filenames uses glob
2024-02-19 00:19:44 +01:00
Jimmy Zelinskie
b950dea003 add monokai soda theme (#9651) 2024-02-19 00:19:26 +01:00
AlexanderDickie
9ab3f9d01a Scroll cursor and page together (neovim-like scrolling) (#8015)
* neovim like scroll function

* clear line annotations outside of move_vertically/_visual

* add nvim scroll function to commands

* assign nvim-scroll to C-d and C-u (half page scrolls)

* dont remove backspace and space mapping

* move non-softwrap logic to seperate function, call this in nvim-scroll fn

* Revert "move non-softwrap logic to seperate function, call this in nvim-scroll fn"

This reverts commit e4905729c3.

* Revert "clear line annotations outside of move_vertically/_visual"

This reverts commit 1df3fefe55.

* add TODO for when inline diagnostics gets merged

* move nvim-scroll logic into scroll(), dont respect scrolloff

* run cargo fmt

* run cargo clippy

* update documenation for Ctrl-d and Ctrl-u remap
2024-02-19 00:13:04 +01:00
Vince Varga
6432669822 Add tmux.conf as a bash file type (#9653)
* Add conf as a bash file type

Tmux and tmux.conf is used widely in software developer circles.

Having the tmux.conf file not have any syntax highlighting by default is (IMO) not ideal for an editor that otherwise "just works".

* Use tmux.conf glob instead of simply conf for tmux

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

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-02-18 15:55:21 +01:00
melted-brownie
78c34194b5 Improve textobjects for parameter/argument for Dart (#9644)
Co-authored-by: Sébastien Blondiau <sebastien.blondiau@iot-d.com>
2024-02-17 17:09:21 +01:00
Vivek Kethineni
3e963b3c1b Add Rust fields as argument textobject (#9637)
* added field_declaration_list and field_initializer_list as parameter textobjects

* removed field_declaration_listt from textobjects.scm
2024-02-17 17:08:44 +01:00
Nick Condron
6ffe09e873 Fix Sonokai theme to better match original (#5379)
* Make sonokai palette perfectfully faithful

* Amend theme to better match original sonokai

Changes based on the following references:
(1) https://www.sainnhe.dev/post/contributing-guide/#sonokai
(2) https://github.com/sainnhe/sonokai/blob/master/colors/sonokai.vim

* Make constants white (1)
* Make builtin variables purple (1)
* Make members orange (1)
* Make labels red (2)
* Make operators red (1)
* Make all punctuation grey (2)
* Make builtin functions and macros green (2)
* Make diff delta blue (2)
* Make cursor match bg4 (2)
* Make visible whitespace bg4 (2)

* Make Sonokai special punctuation yellow
2024-02-17 17:08:18 +01:00
Matt
76e512f944 Remove unwrap on line option, preventing DAP crash (#9632)
* Remove unwrap on line option, preventing DAP crash, ref #4683

* Update to fall back to existing values for option fields
2024-02-16 15:43:14 +01:00
Michael Davis
59369d99e2 Bump tree-sitter-erlang, add *.app.src file-type (#9627) 2024-02-15 01:55:55 +09:00
Mehdi Abedi
4df08ddbe0 Allow numbers as second input event (#8471)
* Make sure pending key list is empty when count handling

This will allow using numbers as second key event.

* count handling; add an exception for 'g'

* Lookup the key event before considering a number as count

* Avoid the allocation of another vec for the pending keys

---------

Co-authored-by: x <x@torrent>
2024-02-14 11:53:15 +01:00
Mihir Gadgil
c59f29921d Add changes to history when paste-replacing (#9625) 2024-02-13 10:16:23 -06:00
kyfanc
fe869e5dc7 fix lsp config reload (#9415)
`syn_loader` was replaced rather than interior value being replace,
old value was still being referenced and not updated after `:config-refresh`.
By using `ArcSwap` like for `config`, each `.load()` call will return the most
updated value.

Co-authored-by: kyfan <kyfan@email>
2024-02-13 11:58:53 +01:00
dependabot[bot]
7934ac7714 build(deps): bump cc from 1.0.83 to 1.0.85 (#9618)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 09:58:23 +09:00
dependabot[bot]
36ad70e4c7 build(deps): bump unicode-segmentation from 1.10.1 to 1.11.0 (#9617)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 09:57:37 +09:00
dependabot[bot]
c55bf72d25 build(deps): bump clipboard-win from 5.0.0 to 5.1.0 (#9616)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 09:57:22 +09:00
dependabot[bot]
caad87e999 build(deps): bump thiserror from 1.0.56 to 1.0.57 (#9615)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 09:57:12 +09:00
ves
fd7b722dfb theme: horizon-dark fix constructor color (#9493)
* theme: add horizon-dark

* fix whitespace color and gutter selected

* taplo fmt

* markup and color tweaks

markup colors
diff colors
better comment and menu colors

* horizon-dark: fix constructor color

makes Some and None to look better and more like the vscode theme
2024-02-13 01:49:42 +09:00
Chris
661e123152 Add support for Odin's formatter (#9537) 2024-02-13 01:49:04 +09:00
dependabot[bot]
c111e28a6f build(deps): bump toml from 0.7.6 to 0.8.10 (#9539)
Bumps [toml](https://github.com/toml-rs/toml) from 0.7.6 to 0.8.10.
- [Commits](https://github.com/toml-rs/toml/compare/toml-v0.7.6...toml-v0.8.10)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-13 01:48:45 +09:00
Gagan Janjua
d232e7a985 Adding Curzon theme (#9553)
* adding Curzon theme

* Adding cursive strings
2024-02-13 01:48:18 +09:00
Mark Stosberg
0516337abb docs: Document that what the diff gutter symbol does (#9587)
Before there was no document about what the `diff` gutter is displaying
or what the colors mean.

These docs clarify it's a `git` diff and makes it easier to
cross-reference the theme if you aren't sure what the colors mean or
want to change them.
2024-02-13 01:48:10 +09:00
Tom Manner
bb1e2ddcd8 Added "zon" file type to zig language section. (#9582)
`build.zig.zon` is what 0.11.0 uses for external dependencies.  The syntax is a subset of zig and can be highlighted and formatted like normal zig code.
2024-02-13 01:47:52 +09:00
kpbaks
78ed3aded3 feat(languages): use fish_indent as the formatter for fish 2024-02-13 01:47:36 +09:00
kpbaks
0eec518ff6 feat(languages): add formatter for just 2024-02-13 01:47:36 +09:00
Chirikumbrah
45ee568b98 Dracula whitespace color changes (#9588)
* added color to render whitespace characters

* made some alphabet sort
2024-02-13 01:47:08 +09:00
iko
d7c7589fd5 Add Hoon (#9190)
* Added Hoon

* Added highlights.scm

* Updated docs

* Update runtime/queries/hoon/highlights.scm

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

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-02-12 16:36:14 +01:00
Blaž Hrastnik
ad7b7bc804 minor: Fix compilation 2024-02-12 12:01:17 +09:00
ontley
6a90166d0a Add required-root-patterns for situational lsp activation (#8696)
* Added required-root-patterns for situational lsp activation using globbing

* Replaced filter_map with flatten

* updated book to include required-root-patterns option

* fixed wrong function name for path

* Added globset to helix-core. Moved globset building to config parsing.

* Normalize implements AsRef

* cargo fmt

* Revert "cargo fmt"

This reverts commit ca8ce123e8.
2024-02-12 02:35:25 +01:00
HumanEntity
ac8d1f62a1 sonokai: Add color modes support and change contrast between ruler and bg (#9376)
* Added `editor.color-modes` option support

* Less contrast between bg and ruler
2024-02-12 02:29:49 +01:00
Jimmy Zelinskie
d9f7aaacaf languages: add CEL, SpiceDB schema language (#9296)
* languages: add CEL language and grammar

* languages: add spicedb schema language

* chore: docgen

* runtime/queries: refine spicedb & cel highlights

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

* languages: update spicedb

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-02-12 02:28:52 +01:00
dependabot[bot]
7d8ce1a400 build(deps): bump tempfile from 3.9.0 to 3.10.0 (#9538)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.9.0 to 3.10.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.9.0...v3.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 02:23:24 +01:00
7ombie
204c3707b0 Updated Swift grammar, adding 'any' and 'await' keywords. (#9586) 2024-02-12 02:17:44 +01:00
dependabot[bot]
13b9885084 build(deps): bump tokio from 1.35.1 to 1.36.0 (#9540)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.35.1 to 1.36.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.35.1...tokio-1.36.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-12 02:17:02 +01:00
Kirawi
786b5c533e follow neovim's truecolor detection (#9577) 2024-02-11 18:38:09 +01:00
Galen Abell
581a1ebf5d Add glob file type support (#8006)
* Replace FileType::Suffix with FileType::Glob

Suffix is rather limited and cannot be used to match files which have
semantic meaning based on location + file type (for example, Github
Action workflow files). This patch adds support for a Glob FileType to
replace Suffix, which encompasses the existing behavior & adds
additional file matching functionality.

Globs are standard Unix-style path globs, which are matched against the
absolute path of the file. If the configured glob for a language is a
relative glob (that is, it isn't an absolute path or already starts with
a glob pattern), a glob pattern will be prepended to allow matching
relative paths from any directory.

The order of file type matching is also updated to first match on globs
and then on extension. This is necessary as most cases where
glob-matching is useful will have already been matched by an extension
if glob matching is done last.

* Convert file-types suffixes to globs

* Use globs for filename matching

Trying to match the file-type raw strings against both filename and
extension leads to files with the same name as the extension having the
incorrect syntax.

* Match dockerfiles with suffixes

It's common practice to add a suffix to dockerfiles based on their
context, e.g. `Dockerfile.dev`, `Dockerfile.prod`, etc.

* Make env filetype matching more generic

Match on `.env` or any `.env.*` files.

* Update docs

* Use GlobSet to match all file type globs at once

* Update todo.txt glob patterns

* Consolidate language Configuration and Loader creation

This is a refactor that improves the error handling for creating
the `helix_core::syntax::Loader` from the default and user language
configuration.

* Fix integration tests

* Add additional starlark file-type glob

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-02-11 18:24:20 +01:00
Android789515
d570c29ce3 expand upon the arch linux install instructions (#9574)
Signed-off-by: Android789515 <derflug@mailfence.com>
2024-02-09 08:29:11 -06:00
Matouš Dzivjak
d137a08231 feat(languages): pkl (#9515)
* feat(languages): pkl

Add [pkl](https://github.com/apple/pkl) language.
Official documentation: https://pkl-lang.org/

* remove branch indent
2024-02-09 11:44:46 +01:00
ath3
f8e2d822ba Fix scroll track (#9508) 2024-02-09 03:24:45 +01:00
Tobias Hunger
a1272bdb17 slint: Update treesitter parser and queries (#9551)
* slint: Update treesitter parser and queries

* slint: Port over suggestions from nvim review
2024-02-07 19:36:29 +01:00
Michael Davis
bbcc89241f Fix pulldown_cmark breaking changes to tag types
* Tags and TagEnd are now separate enums since
  <https://redirect.github.com/raphlinus/pulldown-cmark/pull/517>.
* The `Tag::Heading` member has been changed from a tuple variant to a
  struct variant.
2024-02-07 23:53:51 +09:00
dependabot[bot]
630d91168a build(deps): bump pulldown-cmark from 0.9.6 to 0.10.0
Bumps [pulldown-cmark](https://github.com/raphlinus/pulldown-cmark) from 0.9.6 to 0.10.0.
- [Release notes](https://github.com/raphlinus/pulldown-cmark/releases)
- [Commits](https://github.com/raphlinus/pulldown-cmark/compare/v0.9.6...v0.10.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-07 23:53:51 +09:00
dependabot[bot]
72c508de24 build(deps): bump libc from 0.2.152 to 0.2.153 (#9541)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-02-07 17:34:49 +09:00
Bogdan Agica
c64a0e615b Revert "build(deps): bump cc from 1.0.83 to 1.0.84 (#8809)" (#9548) 2024-02-07 16:50:14 +09:00
eh
28a39e6efc Fix cursorline Zed OneDark (#9549)
Co-authored-by: e4 <eric.correia@dronesense.com>
2024-02-07 01:47:13 +09:00
Diego
a37af2dcbf fix division by zero when prompt completion area is too small (#9524) 2024-02-06 22:45:57 +09:00
eh
1d87c6a999 Update colors used for zed themes (#9544)
Official colors used is now publically available: https://github.com/zed-industries/zed/blob/main/assets/themes/one/one.json
Modified the theme to more accurately reflect the actual colors being used.

Co-authored-by: e4 <eric.correia@dronesense.com>
2024-02-06 15:13:20 +09:00
Matouš Dzivjak
0975d9c5e7 feat(languages): golang comments and numeric types (#9525) 2024-02-06 09:55:56 +09:00
Doug Kelkhoff
5c567f31e2 Adding two themes using only colors from 16-color terminal themes (#9477)
* adding 16-color terminal themes

* minor consistency update

* minor consistency update

* rename to be more consistent with other helix theme name conventions

* fixing improper theme inherits name
2024-02-04 02:11:20 +01:00
zetashift
6e3ed7f0fa Update Unison tree-sitter grammar for type changes and add indent queries (#9505)
* Update Unison tree-sitter grammar for type changes

* Add indent queries for Unison

* Improve Unison indent queries
2024-02-04 02:10:20 +01:00
Jaakko Paju
75d61d8149 Improve tree-sitter queries for Scala (#9475)
- Simplify function highlighting
- Highlight extension methods
- Textobject query (mia/maa) for class/trait constructor parameters/arguments
- Textobject query (mif/maf) for Scala 3 braceless lambdas
2024-02-04 02:09:42 +01:00
Novus Nota
d1054de3ce feat: Add Tact language support (#9512)
Re-submitting
2024-02-04 02:09:11 +01:00
Devyn Cairns
3f380722fb Update grammars for Nushell to rev 358c4f50 (#9502) 2024-02-04 02:04:51 +01:00
Matouš Dzivjak
d545452819 feat(queries): regex injection for golang (#9510) 2024-02-04 00:27:40 +01:00
Shoyu Vanilla
81ae768a4e Use gix pipeline filter instead of manual crlf implementation (#9503) 2024-02-03 12:23:33 -06:00
dependabot[bot]
70cea93bff build(deps): bump serde_json from 1.0.111 to 1.0.113 (#9471) 2024-02-02 10:34:49 -07:00
dependabot[bot]
aa4241c973 build(deps): bump chrono from 0.4.32 to 0.4.33 (#9472) 2024-02-02 10:34:24 -07:00
dependabot[bot]
5ec9565ddb build(deps): bump serde from 1.0.195 to 1.0.196 (#9473) 2024-02-02 10:34:05 -07:00
dependabot[bot]
dd59602809 build(deps): bump pulldown-cmark from 0.9.3 to 0.9.6 (#9474) 2024-02-02 10:33:46 -07:00
Andrew Carter
dbac78bb3c Set ui.virtual.ruler background for GitHub themes (#9487)
Turning on a ruler does not show a visible ruler
line for the GitHub themes. This change renders
rulers using the `canvas.subtle` color. This
matches the color used for the `cursorline` and
creates a visible ruler that fits the theme.
2024-02-01 02:18:53 +01:00
Waleed Dahshan
cf4492174d Use range positions to determine insert_newline motion (#9448)
* use anchor and head positions to determine motion

* use range cursor to decide extending or shifting

* add condition to cursor moving back on normal
2024-01-30 06:58:33 +09:00
Pascal Kuthe
87a720c3a1 make path changes LSP spec conform (#8949)
Currently, helix implements operations which change the paths of files
incorrectly and inconsistently. This PR ensures that we do the following
whenever a buffer is renamed (`:move` and workspace edits)

* always send did_open/did_close notifications
* send will_rename/did_rename requests correctly
  * send them to all LSP servers not just those that are active for a
    buffer
  * also send these requests for paths that are not yet open in a buffer (if
    triggered from workspace edit).
  * only send these if the server registered interests in the path
* autodetect language, indent, line ending, ..

This PR also centralizes the infrastructure for path setting and
therefore `:w <path>` benefits from similar fixed (but without didRename)
2024-01-29 01:34:45 +09:00
sogaiu
f5b67d9acb Use janet-simple grammar for Janet (#9247)
* Use janet-simple grammar for Janet

* Update book

* Tweak language name and related

* Rename janet-simple to janet in book

* Remove spurious language section for janet

* Drop quote_lit and qq_lit related highlighting

---------

Co-authored-by: sogaiu <983021772@users.noreply.github.com>
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2024-01-28 19:17:46 +09:00
NitinKM
4ab7029535 Add icon to Windows executable (#9104)
* injecting the icon through a resource file, no extra deps

* formatted

* scripted rc compilation

* formatted and restructured

* simplified conditional func call
2024-01-28 18:31:10 +09:00
Twinkle
eb3c4e9f00 feat: add hard/soft contrast for gruvbox light mode (#9266) 2024-01-28 18:20:51 +09:00
Ryan Roden-Corrent
fe44391016 Add argument to textobject in gdscript. (#9288)
Currently `maa` only selects parameters in a function definition.
Allow it to also select arguments inside a function call.
2024-01-28 18:19:25 +09:00
Jaakko Paju
ee68fd09ac highlight(scala): highlight abstract methods in traits and classes (#9340) 2024-01-28 18:18:35 +09:00
Abderrahmane TAHRI JOUTI
0328fa4d02 adjust color darkness on ruler & inlay-hints (#9375) 2024-01-28 18:14:12 +09:00
Michael Davis
035b8eabdb Respect injections in movement::move_parent_node_end 2024-01-28 18:13:33 +09:00
Michael Davis
8b6565c839 Respect injections in :tree-sitter-highlight-name 2024-01-28 18:13:33 +09:00
Michael Davis
5e0b3cc28b Use injection syntax trees for bracket matching
Previously we used the root syntax tree for bracket matching. We can use
the new functionality in `Syntax` for finding the correct syntax tree
for a given byte range though so we use the correct syntax tree within
injections. This improves bracket matching behavior within HTML
injections like script or style tags for example.
2024-01-28 18:13:33 +09:00
Jaakko Paju
9978d421fe Include interpolated SQL strings in Scala injection queries (#9428)
* Change Scala injection queries to include SQL strings

* Include block comments in comment injection

* Change #match predicate to #any-of

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

---------

Co-authored-by: Kirawi <67773714+kirawi@users.noreply.github.com>
2024-01-28 18:12:07 +09:00
Travis Harmon
f0be0c6d4a Make status line modes bold for theme onedark (#9435)
* Make status line modes bold

* Revert change to onedarker
2024-01-28 18:11:31 +09:00
Chirikumbrah
1616021a5a Make status line modes bold (#9449) 2024-01-28 18:11:05 +09:00
Poliorcetics
0d09fb4f55 lang(git-ignore): add helix/ignore to git-ignore file types (#9447) 2024-01-27 13:17:37 -06:00
blinxen
2661e05b34 Update some grammars to a commit where the license file is included (#9279)
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-01-25 14:12:17 +09:00
Nick
d8b8d2fda6 Fix error message shown for goto references (#9382) 2024-01-25 14:11:12 +09:00
Michael Davis
cda8ea991e highlighting: Gate multiple captures behind #is-not? local predicates (#9390) 2024-01-25 14:10:58 +09:00
Blaž Hrastnik
83f09ecbff minor: Silence noisy language server not found error in log 2024-01-24 16:00:21 +09:00
Michael Davis
cb25d13028 Improve error handling for which::which failures
Co-authored-by: Pascal Kuthe <pascalkuthe@pm.me>
2024-01-24 15:47:49 +09:00
Michael Davis
6d724a8f33 Re-export which from helix-stdx::env
We use `which::which` in many crates, so `which` was a separate
dependency across all of them. We can centralize `which` into the
stdx crate so it's easy for all crates to depend on it.

I also moved the rest of `helix-view/src/env.rs` into helix-stdx's
`env` module since it only contained a thin wrapper around `which`
and `std::env`.
2024-01-24 15:47:49 +09:00
Blaž Hrastnik
6bfe1ddc53 minor: Silence noisy set_error log
Outside of debugging tests, it makes no sense to log this.
2024-01-24 15:31:16 +09:00
dependabot[bot]
b606c05246 build(deps): bump actions/cache from 3 to 4 (#9402)
Bumps [actions/cache](https://github.com/actions/cache) from 3 to 4.
- [Release notes](https://github.com/actions/cache/releases)
- [Changelog](https://github.com/actions/cache/blob/main/RELEASES.md)
- [Commits](https://github.com/actions/cache/compare/v3...v4)

---
updated-dependencies:
- dependency-name: actions/cache
  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-01-23 20:23:39 +01:00
Idobenhamo
299bcce481 Update Typst Tree-Sitter grammar (#9403)
Co-authored-by: Idobenhamo <idobenhamo@users.noreply.github.com>
2024-01-23 18:27:46 +01:00
melted-brownie
2058b3732c Add text object queries for dart (#9411)
* Add text object queries for dart

* Update runtime/queries/dart/textobjects.scm

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

* Clean up internal capture name

---------

Co-authored-by: Sébastien Blondiau <sebastien.blondiau@iot-d.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-01-23 18:27:09 +01:00
dependabot[bot]
7caae13465 build(deps): bump gix from 0.57.1 to 0.58.0 (#9407)
Bumps [gix](https://github.com/Byron/gitoxide) from 0.57.1 to 0.58.0.
- [Release notes](https://github.com/Byron/gitoxide/releases)
- [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Byron/gitoxide/compare/gix-v0.57.1...gix-v0.58.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 16:56:16 +01:00
dependabot[bot]
cbd8602018 build(deps): bump smallvec from 1.12.0 to 1.13.1 (#9405)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 23:19:36 +09:00
Jaakko Paju
4168864572 Add bufferline config for onedark & onedarker themes (#9397) 2024-01-23 23:19:22 +09:00
dependabot[bot]
ae8042bb83 build(deps): bump bitflags from 2.4.1 to 2.4.2 (#9404)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 11:24:38 +09:00
dependabot[bot]
e85507ccac build(deps): bump regex from 1.10.2 to 1.10.3 (#9408)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 11:24:29 +09:00
dependabot[bot]
960cda60ab build(deps): bump chrono from 0.4.31 to 0.4.32 (#9409)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-23 11:22:42 +09:00
Pascal Kuthe
8e592a151f refactor completion and signature help using hooks 2024-01-23 11:20:19 +09:00
Pascal Kuthe
13ed4f6c47 Add hook/event system 2024-01-23 11:20:19 +09:00
Boris Verkhovskiy
7d7ace551c Highlight .bash_history as bash (#9401) 2024-01-23 00:19:28 +01:00
Jaakko Paju
9ed3dc52e0 Update Scala tree-sitter grammar (#9348)
* Update Scala tree-sitter grammar

* Support block comments

Modify comment handling in textobjects and highlights to support new TS-scala node type 'block_comment'
2024-01-22 19:51:56 +01:00
woojiq
52a43bcdfc bash, make, css: highlight and indent queries improvement (#9393)
* highlights(bash): rework keywords section

* Use more specified scope when possible for keywords like @keyword.repeat.
* Add more keywords like "local" or "unsetenv".

Limitation:
* Bash doesn't allow you to have a local variable outside of a function, so maybe we need to have better queries to not highlight the local in this case.
* If we name a function with a keyword (such as unset or local), it will use the highlight scope "keyword" instead of "function".

* indents(css, make): add basic queries

* Despite the fact that queries look simple, they improve indentation in some edge cases that helix couldn't handle correctly by default.
2024-01-22 19:51:12 +01:00
Matthew Toohey
f5f08becef Fix typo in string representation of GotoReference (#9395) 2024-01-22 23:06:20 +09:00
Ahmed Hagi
9c56afeff3 Handle failure when enabling bracketed paste (#9353)
* match instead of crash

* pulling bracketedpaste out, refactor, tracking for bracketed paste

* sending disable bracketed paste only when supports true

* move disable bracketed paste to throwaway
2024-01-18 13:34:06 +01:00
Michael Davis
1bc7aac780 Use helix-stdx tilde expansion and normalization for HELIX_RUNTIME paths
Previously this wasn't possible since helix-core depends on helix-loader,
so helix-loader couldn't use helix-core's path extensions.

We use the path normalization/canonicalization for the runtime directory
provided by the HELIX_RUNTIME environment variable. This improves a
scenario where you set a path containing a tilde. Now that path will be
expanded and normalized.
2024-01-18 10:57:53 +09:00
Michael Davis
1f916e65cf Create helix-stdx crate for stdlib extensions
helix-stdx is meant to carry extensions to the stdlib or low-level
dependencies that are useful in all other crates. This commit starts
with all of the path functions from helix-core and the CWD tracking that
lived in helix-loader.

The CWD tracking in helix-loader was previously unable to call the
canonicalization functions in helix-core. Switching to our custom
canonicalization code should make no noticeable difference though
since `std::env::current_dir` returns a canonicalized path with
symlinks resolved (at least on unix).
2024-01-18 10:57:53 +09:00
Daniel Sedlak
af8e524a7d Address clippy lints (#9371) 2024-01-17 12:40:45 -06:00
Matouš Dzivjak
c60ba4ba04 feat(lsp): implement show document request (#8865)
* feat(lsp): implement show document request

Implement [window.showDocument](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#window_showDocument)
LSP server-sent request.

This PR builds on top of helix-editor#5820,
moves the external-URL opening functionality into shared crate-level
function that returns a callback that is now used by both the
`open_file` command as well as the window.showDocument handler if
the URL is marked as external.

* add return

* use vertical split

* refactor

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-01-17 18:24:38 +01:00
HumanEntity
6754acd83f Made inlay-hints not look like normal code (#9370)
Added `"ui.virtual.whitespace" = { fg = "grey_dim" }` line
2024-01-17 18:23:46 +01:00
Ben Dennis
dcdecaab22 Exit a language server if it sends a message with invalid json (#9332)
* Keep lsp event listener thread alive when malformed json is encountered from the lsp server

* Update unexpected error flow in recv() to close outstanding requests and close the language server

* Log malformed notifications as info instead of error

* Make close_language_server a nested function inside recv, similar to what's done in send

* Update malformed notification log text

* Clean up new log text a bit

* Initialize recv_buffer closer to where it's used

* Use "exit" instead of "close"

* Remove whitespace

* Remove the need for a helper method to exit the language server

* Match on Unhandled error explicitly and keep catch-all error case around
2024-01-17 15:49:25 +01:00
Jeremy Brudvik
f41727cc9c Support PureScript's new spago.yaml configs (#9362) 2024-01-17 23:15:38 +09:00
Kirawi
6339a8c95a Delete .ignore (#9363)
This isn't being used for anything anymore.
2024-01-17 15:15:09 +01:00
Kirawi
44cb8e5475 update tempfile dev-dependency to 3.9 (#9359) 2024-01-16 18:32:05 -06:00
dependabot[bot]
eef46b1aed build(deps): bump cachix/cachix-action from 13 to 14 (#9347)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 14:13:12 +09:00
dependabot[bot]
bd9eef1f90 build(deps): bump cachix/install-nix-action from 24 to 25 (#9346)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 14:13:04 +09:00
dependabot[bot]
0e7f5d604e build(deps): bump thiserror from 1.0.52 to 1.0.56 (#9345)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 14:12:39 +09:00
dependabot[bot]
fa1d8dfabc build(deps): bump anyhow from 1.0.78 to 1.0.79 (#9344)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 14:12:32 +09:00
dependabot[bot]
2d8d16ff5e build(deps): bump rustix from 0.38.28 to 0.38.30 (#9342)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 14:12:25 +09:00
dependabot[bot]
8a00620a71 build(deps): bump smallvec from 1.11.2 to 1.12.0 (#9341)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-16 14:12:15 +09:00
JR
6ce57b7924 Fix export instructions in installation doc (#9306) 2024-01-15 21:43:23 +01:00
Michael Davis
eca3ccff76 Select subtree within injections in :tree-sitter-subtree (#9309)
`:tree-sitter-subtree` could previously only print subtrees of nodes
in the root injection layer. We can improve on that by finding the layer
that contains the given byte range and printing the subtree within that
layer. That gives more useful results when a selection is within an
injection layer.
2024-01-15 15:34:38 +09:00
Michael Davis
3011df4f35 Bump tree-sitter to latest master (#9317)
* query capture names now return `&str`s rather than `String`s
* the `#any-of?` predicate is now supported
2024-01-15 15:33:26 +09:00
Pascal Kuthe
445f7a273a ignore empty TS nodes in match bracket 2024-01-15 15:32:07 +09:00
Pascal Kuthe
2fb7e50b54 don't crash in plaintext bracket match (mm) on empty file 2024-01-15 15:32:07 +09:00
woojiq
3f88a3f4e6 Change path normalization strategy to not resolve symlinks (#9330) 2024-01-14 08:46:32 -06:00
Sebastian Zivota
a0b02106c3 Make nix flake respect unused grammars (#9326)
* Make nix flake respect unused grammars

* Use default value

* Refactor

* Take use-grammars.only into account

---------

Co-authored-by: Sebastian Zivota <sebastian.zivota@mailbox.org>
2024-01-14 15:11:40 +01:00
jw013
054ce3961a Fallback to filename for +arg (#9333) 2024-01-14 15:11:18 +01:00
Michael Davis
17dd102e5c Remove sourcehut tree-sitter grammars from default build (#9316)
Sourcehut has outages occasionally that cause the CI and from-source
builds to fail. It also doesn't setup redirects when a user renames
themselves, so if a user that publishes a tree-sitter grammar we use
changes their sourcehut name then it breaks the build and any prior
builds using that grammar.

For now let's remove them from the default build. It's a bandaid over
a larger reliability and trust problem with the grammar repositories
but it should fix the build for now.
2024-01-11 09:26:25 -06:00
Pascal Kuthe
7739d3ece1 Revert "build(deps): bump ahash from 0.8.6 to 0.8.7" (#9294) 2024-01-11 22:13:39 +09:00
Gabriel Dinner-David
84e24b33dc make sure to sync views when applying edits to unfocused views (#9173) 2024-01-09 10:21:16 +09:00
Sammo98
65d0412880 health - add formatter to display (#7986) 2024-01-09 10:15:50 +09:00
Michael Davis
305d6e9c89 Normalize S-<lower-ascii> keymaps to uppercase ascii (#9213) 2024-01-09 10:04:34 +09:00
Philipp Mildenberger
41ca46cf8c Initialize diagnostics when opening a document (#8873) 2024-01-09 10:01:04 +09:00
rojebd
46ecc102ba added voxed theme (#9164) 2024-01-09 09:57:14 +09:00
Kirawi
7af78c7788 update comment grammar (#9253) 2024-01-09 09:56:51 +09:00
Pascal Kuthe
48c49f0227 update history of a newly focused view (#9271) 2024-01-09 09:56:09 +09:00
Michael Davis
0cbd8d3df1 Check for rename support before showing LSP rename prompt (#9277) 2024-01-09 09:55:11 +09:00
Pascal Kuthe
4da6191a1c don't automatically dismiss zero width diagnostics (#9280) 2024-01-09 09:54:55 +09:00
dependabot[bot]
20b91fd99a build(deps): bump serde_json from 1.0.109 to 1.0.111 (#9284)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 09:52:36 +09:00
dependabot[bot]
97145eaae6 build(deps): bump ignore from 0.4.21 to 0.4.22 (#9283)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 09:52:25 +09:00
dependabot[bot]
bad10a5ddd build(deps): bump libc from 0.2.151 to 0.2.152 (#9282)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 09:51:58 +09:00
dependabot[bot]
f4212421da build(deps): bump ahash from 0.8.6 to 0.8.7 (#9281)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 09:51:38 +09:00
dependabot[bot]
e2e8d2739a build(deps): bump serde from 1.0.193 to 1.0.195 (#9285)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-09 09:51:25 +09:00
Tomas
77ab792ac7 runtime/themes: adding "ttox" theme (#8524)
* runtime/themes: adding 'ttox' theme

* Improving primary selections
2024-01-08 03:57:04 +01:00
Greedwolf DSS
918bd9c2b0 feat: update wren tree-sitter grammar (#8544)
Co-authored-by: masai.dss <masai.dss@bytedance.com>
2024-01-08 03:54:16 +01:00
NitinKM
e46fb58595 Info on how to skip grammar build when building from source (#8698)
* info: no grammar compile

Added instructions on how to compile without compiling grammars

* Update book/src/install.md

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

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-01-08 03:46:53 +01:00
jw013
00d681cc69 Update goto_file docs (#8563) (#9001)
Make the pluralization of files and selections consistent to emphasize
the 1-to-1 relation between files and selections. The prior wording
with plural "files" and singular "selection" can mislead users into
thinking the command can open multiple files from a single selection.
2024-01-08 03:11:18 +01:00
Ryan Roden-Corrent
c8e58304bf Add textobject queries for protobuf grammar. (#9184)
Given `message Foo {string s = 1;}`
- `mat` selects `message Foo {string s = 1}`
- `mit` selects `{string s = 1;}`

Given `service SearchService { rpc Search(Req) returns (Resp); }
- `mit` or `mat` selects `Req` or `Resp`
- `mif` or `maf` selects `rpc Search(Req) returns (Resp);`
- `mit` selects { rpc Search(Req) returns (Resp); }`
- `mat` selects `service SearchService { rpc Search(Req) returns (Resp); }`
2024-01-08 03:08:41 +01:00
Paul Graydon
154d9b6ed1 Update tokyonight themes (#9099) 2024-01-08 03:08:20 +01:00
Jaakko Paju
73deba7044 Add textobject queries for Scala (#9191) 2024-01-08 03:05:10 +01:00
Jaakko Paju
a32d537d0a Add HOCON language support (#9203)
* Add HOCON language support

* Remove error query

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

* Change include query

* Fix query error

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2024-01-08 03:04:43 +01:00
DuckDuckWhale
f8ae2bc61b Fix: misleading active tab color in monokai_pro* (#9148) 2024-01-08 03:03:56 +01:00
petrak@
7e389b67c2 Add auto-pairs to scheme language support (#9232)
Currently, typing a single quote in a `.scm` file "helpfully" auto-
completes a closing quote. This is because there is no auto-pairs
section in the languages.toml. This commit adds that.
2024-01-04 14:49:50 -06:00
Tshepang Mbambo
da4afaf3da remove build warnings (#9180) 2024-01-04 15:51:00 +09:00
dependabot[bot]
8f2e611b7e build(deps): bump serde_json from 1.0.108 to 1.0.109 (#9201)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.108 to 1.0.109.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.108...v1.0.109)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 15:38:12 +09:00
dependabot[bot]
2e3f330b12 build(deps): bump tempfile from 3.8.1 to 3.9.0 (#9199)
Bumps [tempfile](https://github.com/Stebalien/tempfile) from 3.8.1 to 3.9.0.
- [Changelog](https://github.com/Stebalien/tempfile/blob/master/CHANGELOG.md)
- [Commits](https://github.com/Stebalien/tempfile/compare/v3.8.1...v3.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 15:38:05 +09:00
dependabot[bot]
b908abae2d build(deps): bump anyhow from 1.0.76 to 1.0.78 (#9200)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.76 to 1.0.78.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.76...1.0.78)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 15:37:49 +09:00
dependabot[bot]
78d85eb13f build(deps): bump futures-executor from 0.3.29 to 0.3.30 (#9168)
Bumps [futures-executor](https://github.com/rust-lang/futures-rs) from 0.3.29 to 0.3.30.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.29...0.3.30)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 15:37:41 +09:00
dependabot[bot]
5876b763e1 build(deps): bump gix from 0.57.0 to 0.57.1 (#9202)
Bumps [gix](https://github.com/Byron/gitoxide) from 0.57.0 to 0.57.1.
- [Release notes](https://github.com/Byron/gitoxide/releases)
- [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Byron/gitoxide/compare/gix-v0.57.0...gix-v0.57.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-01-04 15:37:22 +09:00
Jaakko Paju
a6ed104ea2 Add .prettierrc to json file types (#9214) 2024-01-02 17:47:59 -05:00
Manuel Mendez
efc4865c78 Reduce logo sizes even more (#9211)
* Reduce logo.svg even more

While reading through commits to helix I saw
https://github.com/helix-editor/helix/pull/9106 and wondered if the
relatively-new-to-me svgo would do better than -95B diff, indeed it
does. Seeing as this file is "a minified file we're basically treating
as binary" anyway I figured might as well minify it further.

* Minimize all the svg logos
2024-01-02 23:16:37 +01:00
Rose Hudson
a680b2e409 rust highlights: clean up constructor logic (#8957)
Enum variants and (tuple) structs are indistinguishable in general, so we
mark any PascalCase pattern or expression as a "constructor", which
covers all three.
2024-01-02 16:38:13 +01:00
Gabriel Lopes Rodrigues
7fd266efa9 Avoid crashing with 2 instances of the same LSP (#9134) 2024-01-02 09:29:22 -06:00
Sebastian Thiel
85fce2f5b6 build(deps): bump gix from 0.56.0 to 0.57.0 (#9188) 2023-12-29 13:46:01 -06:00
Pascal Kuthe
783ff27b1b consistent diagnostic sorting 2023-12-27 15:28:14 +09:00
Pascal Kuthe
515ef17207 make diagnostics stick to word boundaries
Diagnostics are currently extended if text is inserted at their end. This is
desirable when inserting text after an identifier. For example consider:

let foo = 2;
    --- unused variable

Renaming the identifier should extend the diagnostic:

let foobar = 2;
    ------ unused variable

This is currently implemented in helix but as a consequence adding whitespaces
or a type hint also extends the diagnostic:

let foo      = 2;
    -------- unused variable
let foo: Bar = 2;
    -------- unused variable

In these cases the diagnostic should remain unchanged:

let foo      = 2;
    --- unused variable
let foo: Bar = 2;
    --- unused variable

As a heuristic helix will now only extend diagnostics that end on a word char
if new chars are appended to the word (so not for punctuation/ whitespace).
The idea for this mapping was inspired for the word level tracking vscode uses
for many positions. While VSCode doesn't currently update diagnostics after
receiving publishDiagnostic it does use this system for inlay hints for example.
Similarly, the new association mechanism implemented here can be used for word
level tracking of inlay hints.

A similar mapping function is implemented for word starts. Together
these can be used to make a diagnostic stick to a word. If that word
is removed that diagnostic is automatically removed too. This is the exact
same behavior VSCode inlay hints eixibit.
2023-12-27 15:28:14 +09:00
Pascal Kuthe
8653e1b02f Add config to mark diagnostic sources as persistent 2023-12-27 15:28:14 +09:00
dependabot[bot]
c874a896a5 build(deps): bump tokio from 1.35.0 to 1.35.1 (#9169)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.35.0 to 1.35.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.35.0...tokio-1.35.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-27 13:34:58 +09:00
dependabot[bot]
0036782059 build(deps): bump anyhow from 1.0.75 to 1.0.76 (#9170)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.75 to 1.0.76.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.75...1.0.76)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-27 13:33:46 +09:00
dependabot[bot]
2ec4d5004d build(deps): bump futures-util from 0.3.29 to 0.3.30 (#9171)
Bumps [futures-util](https://github.com/rust-lang/futures-rs) from 0.3.29 to 0.3.30.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.29...0.3.30)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-27 13:33:23 +09:00
dependabot[bot]
1fc20cd02b build(deps): bump thiserror from 1.0.51 to 1.0.52 (#9172)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.51 to 1.0.52.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.51...1.0.52)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-27 13:33:08 +09:00
goyalyashpal
d5e6749fa2 Reduce logo.svg size (-93B) (#9106)
* chore(logo): PrettyPrint to reduce size by 93B ...

  * Current size:  2,755 B
  * Original size: 2,848 B
  * Add file's final newline (linux convention)
  * Remove tag separator spaces
  * Add newlines
  * Add tab indentation (instead of 2/4 spaces)
  * Prettify root svg's attribs

* style(logo): Bring style attrb to front

* chore(logo): Remove tab characters

* chore(logo): Remove \n, use LF as final newline

* chore(logo): Minify logo.svg ...

  * Remove final newline too
2023-12-22 13:52:53 +09:00
Sharpened Blade
7b0f92bb3a Add markup styling to nord theme (#9135)
Fixes https://github.com/helix-editor/helix/issues/9131
2023-12-22 13:52:26 +09:00
romi
ab50299efa Add .glif to XML file-types (#9130)
`.glif` files are standard files in the type design industry. From the
Unified Font Object specification website:

The Glyph Interchange Format (GLIF) is a simple and clear XML
representation of a single glyph. GLIF files typically have a .glif
extension.
https://unifiedfontobject.org/versions/ufo3/glyphs/glif/
2023-12-22 13:51:21 +09:00
Michael Davis
585402d9ff Update upload/download-artifact actions to v4 (#9120) 2023-12-21 14:09:26 +09:00
0rphee
5f04d09f03 theme: update noctis (#9123) 2023-12-20 17:09:35 +09:00
Evan Richter
a98b8ddd1a add smali language support (#9089) 2023-12-20 00:31:27 +01:00
dependabot[bot]
63218a5126 build(deps): bump lsp-types from 0.94.1 to 0.95.0 (#9117)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2023-12-20 00:30:46 +09:00
Passw
8736ce3889 Update README.md to add link to Kakoune and Neovim (#9119) 2023-12-19 15:50:39 +01:00
ath3
9ba691cd3a Support drawing popup frame (#4313)
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2023-12-19 10:17:12 +09:00
ves
06d7dc628e theme: add horizon-dark (#9008) 2023-12-19 10:06:20 +09:00
Lucas Wagler
970f9e6333 Add Avro schema file support (#9113) 2023-12-19 10:05:58 +09:00
0rphee
0a83d85124 Add haskell-language-server as lsp for cabal files (#9111) 2023-12-19 10:05:55 +09:00
Matouš Dzivjak
80dd585966 feat(themes): add modus vivendi theme(s) (#8894) 2023-12-19 10:03:26 +09:00
dependabot[bot]
c1ab94bbef build(deps): bump thiserror from 1.0.50 to 1.0.51 (#9116)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-19 10:01:10 +09:00
Gabriel Dinner-David
f27fdb2bf4 when text document sync capability is only kind send didSave without text (#9101)
see https://github.com/microsoft/language-server-protocol/issues/288 for details
2023-12-17 15:59:04 -06:00
Novus Nota
a1a20d231f book: Describe usage of .ignore and helix-specific ignore files in [editor.file-picker] section (#9102) 2023-12-17 22:40:29 +01:00
JJ
c56cd6ee8b Add support for Agda (#8285)
* agda language support (wip)

* improve highlights

* disable agda-language-server

* minor addendum to documentation

* cargo xtask docgen

* oh i can just do this neat

* minor comment cleanup

* upstream updated

* imports: missed a spot

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2023-12-16 05:29:26 +01:00
TornaxO7
914c83420b fix :indent-style crash (#9087)
* removing unreachable statement in `:indent-style`

* update checks when setting indent line and update docs

* `cargo xtask docgen`
2023-12-15 19:05:04 +01:00
Daniel Ebert
33d85606cf Add alignment indent queries for binary & ternary expressions in C. 2023-12-15 15:59:54 +09:00
Daniel Ebert
723a132bdf Simplify implementation of add_indent_level.
Increase hybrid indent heuristic attempt limit to 4.
Clarify the fallback logic in indent heuristic docs.
2023-12-15 15:59:54 +09:00
Daniel Ebert
3e79a35656 Align arguments in a function call in C.
Since the tree-sitter grammar is not very good
at parsing function calls while they're being written,
this is not yet super useful.
However, it prevents the new `hybrid` indent heuristic
from choosing these lines as a baseline, making it
more robust.
2023-12-15 15:59:54 +09:00
Daniel Ebert
a5acfdbf10 Add documentation for new indent computation 2023-12-15 15:59:54 +09:00
Daniel Ebert
938a710904 Make the indent heuristic configurable 2023-12-15 15:59:54 +09:00
Daniel Ebert
559bfc1f5e Improve relative indent computation.
Add tests to ensure that relative & absolute indent computation are consistent.
2023-12-15 15:59:54 +09:00
Daniel Ebert
d29a66f267 Implement relative indent queries,
i.e. also take into account the indentation of a previous
line when computing the indentation for a new line.
2023-12-15 15:59:54 +09:00
Susheel Thapa
23fd145a56 fix: typo in scm files inside runtime/queries/ (#8630) 2023-12-15 15:58:27 +09:00
JR
e332c7d875 Add tutor for match mode (#8751)
* Add tutor for match mode

* Improve the surround tutor

* Add missing == in header

* Reflow

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Update runtime/tutor

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

* Apply feedback

---------

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>
2023-12-15 15:57:28 +09:00
Jesús González
f1e34ce5a2 Specify BG and FG cursor colors in Darcula themes (#9002) 2023-12-15 15:54:59 +09:00
Matthew Toohey
11856329bf Change R markdown language name to fix language server detection (#9012) 2023-12-15 15:54:43 +09:00
Phil
b4571c292e Add initial support for janet-lang (#9081)
* Add initial support for janet-lang

* Use default roots for janet-lang
2023-12-15 15:54:25 +09:00
dependabot[bot]
437fbee425 build(deps): bump zerocopy from 0.7.20 to 0.7.31 (#9092)
Bumps [zerocopy](https://github.com/google/zerocopy) from 0.7.20 to 0.7.31.
- [Release notes](https://github.com/google/zerocopy/releases)
- [Changelog](https://github.com/google/zerocopy/blob/main/CHANGELOG.md)
- [Commits](https://github.com/google/zerocopy/compare/v0.7.20...v0.7.31)

---
updated-dependencies:
- dependency-name: zerocopy
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-15 15:52:58 +09:00
Valerii Petryniak
7c55190806 Update keymap.md: improve grammar (#9069)
* Update keymap.md: improve grammar

* Update keymap.md

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>

---------

Co-authored-by: David Else <12832280+David-Else@users.noreply.github.com>
2023-12-13 17:37:12 +01:00
Michal Rostecki
c2591445c9 chore: Update tree-sitter-d (#9021)
One of the included changes is gdamore/tree-sitter-d#22 which
fixes the build of Helix when using clang as `CC`.
2023-12-13 02:37:03 +01:00
dependabot[bot]
49dffa7d24 build(deps): bump gix from 0.55.2 to 0.56.0 (#9055)
* build(deps): bump gix from 0.55.2 to 0.56.0

Bumps [gix](https://github.com/Byron/gitoxide) from 0.55.2 to 0.56.0.
- [Release notes](https://github.com/Byron/gitoxide/releases)
- [Changelog](https://github.com/Byron/gitoxide/blob/main/CHANGELOG.md)
- [Commits](https://github.com/Byron/gitoxide/compare/gix-v0.55.2...gix-v0.56.0)

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

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

* Adapt to changes in gix EntryMode/EntryKind

The rest of the gix codebase now calls `.kind()` on the mode and uses
the renamed `EntryKind` enum.

---------

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>
2023-12-13 02:29:43 +01:00
dependabot[bot]
f036451a0e build(deps): bump rustix from 0.38.26 to 0.38.28 (#9054)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-12 22:46:21 +09:00
dependabot[bot]
7f44a6ad50 build(deps): bump once_cell from 1.18.0 to 1.19.0 (#9053)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-12 22:46:11 +09:00
dependabot[bot]
3e249829ee build(deps): bump libc from 0.2.150 to 0.2.151 (#9056)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-12 11:05:20 +09:00
dependabot[bot]
53ad0f72a5 build(deps): bump tokio from 1.34.0 to 1.35.0 (#9057)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-12 11:05:10 +09:00
Tanguy
510928618d Fix version of Nix package (#9013) 2023-12-09 03:04:09 +09:00
TornaxO7
b81aacc5e1 Join empty lines with only one space in join_selections (#8989)
* fix: #8977

fixes the issue that lines with only spaces are getting
joined as well

* reverting some renamings

* improve empty line check

* adding integration test

* reverting code block

* fix conditon check for line end

* applying suggested style
2023-12-06 16:19:54 +01:00
Frederick Schwalbe
c3cb1795bf Update gleam grammar and queries (#9003) 2023-12-05 22:54:00 +09:00
dependabot[bot]
4c2bd4905e build(deps): bump open from 5.0.0 to 5.0.1 (#8992)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 12:28:32 +09:00
Skyler Hawthorne
bf7c4e1659 use workspace inheritance for common version (#8925) 2023-12-05 10:54:18 +09:00
Jesús González
44c3d48a94 Add more accurate to official theme type highlighting to Darcula themes (#8738) 2023-12-05 10:46:23 +09:00
Niklas Alexander Shern
ab763b3111 fix: update rose_pine to be identical to main repo (#8946) 2023-12-05 10:45:50 +09:00
Manuel Mendez
9fcfb88132 Add .envrc.local and .envrc.private to env file-types (#8988) 2023-12-05 10:45:41 +09:00
dependabot[bot]
8532cec01c build(deps): bump grep-searcher from 0.1.12 to 0.1.13 (#8998)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 10:45:18 +09:00
dependabot[bot]
fa7a8ffc50 build(deps): bump cachix/cachix-action from 12 to 13 (#8997)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 10:45:01 +09:00
dependabot[bot]
0d890ef0f7 build(deps): bump ignore from 0.4.20 to 0.4.21 (#8996)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 10:44:51 +09:00
dependabot[bot]
00d565bf74 build(deps): bump cachix/install-nix-action from 23 to 24 (#8995)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 10:43:43 +09:00
dependabot[bot]
58daa31523 build(deps): bump slotmap from 1.0.6 to 1.0.7 (#8994)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 10:43:35 +09:00
dependabot[bot]
aad44f6dd0 build(deps): bump rustix from 0.38.25 to 0.38.26 (#8993)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 10:41:50 +09:00
dependabot[bot]
79965a238d build(deps): bump url from 2.4.1 to 2.5.0 (#8991)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-12-05 10:41:30 +09:00
Skyler Hawthorne
fcd564fddf upgrade tree-sitter-python (#8976)
supports new syntaxes from Python 3.12
2023-12-04 17:26:11 +09:00
Evan Richter
455b206a8c nix: update flake inputs (#8943)
* removed non-existent crane flake input overrides
2023-12-04 01:46:17 +01:00
Nan Zhong
466b87c8e5 languages: update rescript grammar (#8962)
This bump fixes a build failure of the grammer with clang.
2023-12-04 01:46:00 +01:00
Tshepang Mbambo
86023cf1e6 use canonical name (#8960)
I do not find anywhere where the option omits the 's'
2023-12-01 14:07:31 +09:00
Michael Davis
0c81ef73e1 direnv: Watch the rust-toolchain file (#8921) 2023-11-29 11:01:12 +09:00
Tudyx
f8d261cd20 add log tree-sitter (#8916)
* add log tree-sitter

* better highlight queries
2023-11-29 02:42:59 +01:00
A-Walrus
0739d13b03 Add musicxml to xml extensions (#8935) 2023-11-29 02:41:35 +01:00
dependabot[bot]
0d9145a1bf build(deps): bump grep-regex from 0.1.11 to 0.1.12 (#8930)
Bumps [grep-regex](https://github.com/BurntSushi/ripgrep) from 0.1.11 to 0.1.12.
- [Release notes](https://github.com/BurntSushi/ripgrep/releases)
- [Changelog](https://github.com/BurntSushi/ripgrep/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/ripgrep/compare/grep-regex-0.1.11...0.1.12)

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

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-29 02:41:10 +01:00
Hendrik Norkowski
b023faacf8 fix(ui): use crossterm cursor when at the end of the rope (#8934) 2023-11-29 00:11:23 +09:00
Blaž Hrastnik
6d168eda27 fix CI: tree-sitter-gemini user renamed 2023-11-28 14:38:15 +09:00
dependabot[bot]
3e451f0d53 build(deps): bump serde from 1.0.192 to 1.0.193 (#8931)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 09:54:35 +09:00
dependabot[bot]
008208fcfb build(deps): bump grep-searcher from 0.1.11 to 0.1.12 (#8929)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 09:54:16 +09:00
dependabot[bot]
b30451f776 build(deps): bump hashbrown from 0.14.2 to 0.14.3 (#8928)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 09:53:53 +09:00
dependabot[bot]
4bc43347a1 build(deps): bump clipboard-win from 4.5.0 to 5.0.0 (#8927)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 09:52:46 +09:00
dependabot[bot]
8de8a66182 build(deps): bump globset from 0.4.13 to 0.4.14 (#8926)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-28 09:51:45 +09:00
Hendrik Norkowski
71fd85894b use crossterm cursor in editor when out of focus (#6858)
Use crossterm cursor in the editor when the terminal is out of focus to achieve consistent out-of-focus cursor behaviour
2023-11-27 20:11:16 +01:00
Fomalhaut Weisszwerg
b7f98d1d99 set Cargo feature resolver to v2 (#8917)
* fix: version of Cargo feature resolver.

This commit solve the ambiguity to determin the version of resolver.
To get more detail, see the following two documents:

- https://doc.rust-lang.org/cargo/reference/resolver.html#resolver-versions
- https://doc.rust-lang.org/edition-guide/rust-2021/default-cargo-resolver.html

* unified: Rust edition in all workspaces.

Now, the Rust 2021 is available in all workspaces.

* fined up: Cargo.toml by using workspace inheritance.

To get more detail of the `workspace.package` table, see a following document:

- https://doc.rust-lang.org/cargo/reference/workspaces.html#the-package-table
2023-11-27 13:24:57 +01:00
Davide Ferrero
3f9788daaa update which crate to 5.0.0 (#8902)
* update which crate to 5.0.0

* update which crate to 5.0.0
2023-11-26 01:37:00 +09:00
chtenb
8c68074fa6 Fix precedence of ui.virtual.whitespace (#8879)
* Revert "Revert "Fix precedence of ui.virtual.whitespace (#8750)""

This reverts commit 811d62d3b3.

* Fix ui.text overwriting the syntax highlighting

Adjust ui.text description
2023-11-25 14:27:31 +01:00
Cole Helbling
8b0ae3d279 bump MSRV to 1.70.0 (#8877)
* rust-toolchain.toml: bump MSRV to 1.70.0

With Firefox 120 released on 21 November 2023, the MSRV is now 1.70.0.

* Fix cargo fmt with Rust 1.70.0

* Fix cargo clippy with Rust 1.70.0

* Fix cargo doc with Rust 1.70.0

* rust-toolchain.toml: add clippy component

* .github: bump dtolnay/rust-toolchain to 1.70

* helix-term: bump rust-version to 1.70

* helix-view/gutter: use checked_ilog10 to count digits

* helix-core/syntax: use MAIN_SEPARATOR_STR constant

* helix-view/handlers/dap: use Display impl for displaying process spawn error

* WIP: helix-term/commands: use checked math to assert ranges cannot overlap
2023-11-25 13:55:49 +01:00
Alexis Mousset
090ed97e00 Add modus operandi themes (#8728) 2023-11-22 19:04:10 +01:00
ghashy
ff095ebd9b DBML Language support (#8860)
* DBML language support

* DBML language support, highlights.scm added

* DBML support

* Update runtime/queries/dbml/highlights.scm

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

* Update runtime/queries/dbml/highlights.scm

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

* Update runtime/queries/dbml/highlights.scm

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

* remove unnecessary block highlight

* remove unnecessary line

* remove index_block query

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2023-11-22 18:27:25 +01:00
Ethan Brierley
f1b9c19fa9 add LSP for nushell (#8878) 2023-11-22 18:24:34 +01:00
chtenb
db83eb0c50 Document the bufferline scopes (#8880)
* Document the bufferline scopes

* Update book/src/themes.md

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

---------

Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2023-11-22 14:07:26 +01:00
Blaž Hrastnik
811d62d3b3 Revert "Fix precedence of ui.virtual.whitespace (#8750)"
This reverts commit 41b307b673.
2023-11-22 16:00:28 +09:00
chtenb
41b307b673 Fix precedence of ui.virtual.whitespace (#8750) 2023-11-22 11:42:40 +09:00
Bjorn Ove Hay Andersen
47b6c4bc78 Resolve args.files before changing directory (#8676)
* Resolve args.files before changing directory

* Removed the open_cwd work-around now that the path is full

* If -w is specified, use that as the working directory

* Open the remaining files in the argument list, also when the first is a directory

* Use an iterator access the files argument
2023-11-21 12:07:00 +01:00
Matouš Dzivjak
3052050ee0 open urls with goto_file command (#5820)
* feat(commands): open urls with goto_file command

Add capability for `goto_file` command to open an URL under cursor.

Fixes: https://github.com/helix-editor/helix/issues/1472
Superseds: https://github.com/helix-editor/helix/pull/4398

* open files inside helix

* address code review

* bump deps

* fix based on code review comments
2023-11-21 12:04:20 +01:00
dependabot[bot]
bfd60a5b39 build(deps): bump rustix from 0.38.22 to 0.38.25 (#8874)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-21 09:31:12 +09:00
blinxen
a0e5bb8520 [themes] Add missing license files for recently added themes 2023-11-19 14:44:02 -06:00
blinxen
b16752306c [themes] Mention license files in README 2023-11-19 14:44:02 -06:00
Dan Field
b306b25e82 GN language support (#6969)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2023-11-18 17:11:18 +09:00
Ryan Mehri
6bf5548dbd make increment/decrement exit select mode 2023-11-18 01:07:57 -06:00
Ryan Mehri
09c78e8b4e make switch case commands exit select mode 2023-11-18 01:07:57 -06:00
Ryan Mehri
d4a0eba1a7 make align exit select mode 2023-11-18 01:07:57 -06:00
Ryan Mehri
5913073733 make replace with clipboard commands exit select mode 2023-11-18 01:07:57 -06:00
Ryan Mehri
1271a50a82 make paste commands exit select mode 2023-11-18 01:07:57 -06:00
Ryan Mehri
34de1cab62 make indent/unindent exit select mode 2023-11-18 01:07:57 -06:00
Niklas Alexander Shern
2acf5e365e theme: show active selection and buffer for naysayer theme (#8838) 2023-11-18 10:44:01 +09:00
blinxen
39aa6fa646 Update some grammars to a commit where the license file is included (#8691) 2023-11-18 10:24:59 +09:00
WuerfelDev
2579bca21c Book: fix formatting of some default values (#8837) 2023-11-18 00:34:17 +01:00
crozbo
7868136a18 Theme: Papercolor: Add inlay-hint style (#8827) 2023-11-18 00:32:06 +01:00
Luca Saccarola
5889b81fc7 docs: add docs for soft-wrap in languages.toml (#8836) 2023-11-17 15:57:15 -06:00
Seth Maurice-Brant
73ca2d5f84 Change Fedora installation to the new official Helix package (#8762)
Remove COPR install in favour of official Helix package
2023-11-17 15:54:30 -06:00
Eemil Haapanen
3c8bf9df4a theme: add starlight (#8787) 2023-11-17 01:34:56 +01:00
dependabot[bot]
2040444da9 build(deps): bump rustix from 0.38.21 to 0.38.22 (#8807)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-15 20:21:00 +01:00
dependabot[bot]
40959bb449 build(deps): bump cc from 1.0.83 to 1.0.84 (#8809)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-14 12:15:50 +09:00
dependabot[bot]
e5d02cd4bd build(deps): bump serde from 1.0.191 to 1.0.192 (#8810)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-14 12:15:37 +09:00
Michael Davis
69a0df929a Remove 'roots' keys with default value from languages.toml 2023-11-14 11:56:53 +09:00
Michael Davis
8b2d97eb56 Default 'roots' field of language config
Previously roots needed to be specified by every language and `[]` was
used as an explicit default. Root files don't make sense for every
language (for example TOML) so I think we should allow languages to
not explicitly mention the key and have the `[]` default automatically.
2023-11-14 11:56:53 +09:00
dependabot[bot]
13386a4786 build(deps): bump smallvec from 1.11.1 to 1.11.2 (#8808)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-14 11:56:13 +09:00
dependabot[bot]
5ec53c0222 build(deps): bump tokio from 1.33.0 to 1.34.0 (#8811)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-14 11:55:57 +09:00
Ambuj Singh
23fea46815 theme: Add Theme poimandres (#8759)
* theme: Add Theme poimandres

* theme: inherit `poimandres_storm` from `poimandres` with minor tweaks

* fix(theme): rename `crossed-out` to `crossed_out`

* fix(theme:poimandres): improve contrast of selection color for regular variant
2023-11-12 23:04:36 +01:00
blt__
172ef2fa9f Highlight meson_options.txt as a meson file (#8794) 2023-11-12 23:04:03 +01:00
mydumpfire
6ab774da0b grammars.nix: allow the user to apply overlays (#8749)
You can now apply overlays to the grammar derivations via
`grammarOverlays`. Also, the `src` in the derivation is now properly
unpacked to the build directory, allowing the user to mutate the source
files if they want to.
2023-11-09 09:56:57 +01:00
dependabot[bot]
4229583631 build(deps): bump futures-executor from 0.3.28 to 0.3.29 (#8743)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-09 11:13:32 +09:00
dependabot[bot]
aac7bd9b08 build(deps): bump libc from 0.2.149 to 0.2.150 (#8741)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-09 11:13:03 +09:00
dependabot[bot]
91bdceb8b6 build(deps): bump serde_json from 1.0.107 to 1.0.108 (#8744)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-09 11:12:54 +09:00
dependabot[bot]
676ab0c1f3 build(deps): bump serde from 1.0.190 to 1.0.191 (#8740)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-11-09 11:12:45 +09:00
Triton171
cb0bc25a9f Add indent queries for scheme (and reuse them for common-lisp & racket). (#8720) 2023-11-08 20:53:07 +01:00
Yomain
e868678139 Add command to move files with LSP support (#8584)
* Added rename command

* Added an error if the new path already exists

* Fixed wrong command name being used

* fixed clippy suggestions

* removed didRenameFiles call, fixed early return due to path Err

* added ':rnm' alias to ':rename'

* code cleanup

* formatting

* removed debug line

* cargo fmt

* Improved new buffer error message

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Removed unnecessary path normalizing

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update helix-term/src/commands/typed.rs

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update helix-term/src/commands/typed.rs

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update helix-term/src/commands/typed.rs

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* Update helix-term/src/commands/typed.rs

Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>

* feat: change `rename` command to `move`

* feat: add multi lsp support when moving files

* feat: allow lsp calls with a custom timeout

* feat: sending lsp file_changed event once file has moved

---------

Co-authored-by: ontley <theontley@gmail.com>
Co-authored-by: ontley <67148677+ontley@users.noreply.github.com>
Co-authored-by: Pascal Kuthe <pascal.kuthe@semimod.de>
2023-11-08 12:38:17 -06:00
Henrik Tjäder
7bc564d3dc Theme: Papercolor: Add type.parameter (#8735) 2023-11-06 15:33:18 -06:00
Joey Hain
f73e9a8d15 highlights: add type.parameter scope to several more languages (#8718)
* typescript

* go

* haskell

* ocaml

* kotlin (+ bugfix)
2023-11-06 02:54:25 +01:00
postsolar
a98ad137f9 Update PureScript grammar (#8712) 2023-11-05 14:16:25 +01:00
MDeiml
2fddc2a4fc Update markdown grammar to v0.1.6 2023-11-05 07:31:19 -05:00
Skyler Hawthorne
10b178e94b swap yank command registers (#8708)
#8703 swapped the `+` and `*` registers, but did not swap them in the
corresponding yank commands.
2023-11-04 09:35:38 +09:00
Michael Davis
8dc197721b Add an installation method field to the bug report template (#8711)
We can guess the installation method from the version tag and platform
in some cases but it would be useful to have this be explicit for the
sake of debugging packager-specific problems.
2023-11-04 09:34:54 +09:00
cgahr
5c325fe342 replace kdl tree-sitter to fix highlighting (#8652)
* replace kdl tree-sitter

* kdl: adopt highlights for new tree-sitter

* kdl: add indent queries

* kdl: add textobjects

* kdl: improve syntax highlighting

* kdl: update lang-support

* kdl: make indents more concise

---------

Co-authored-by: Constantin Gahr <constantin.gahr@ipp.mpg.de>
2023-11-03 22:21:54 +01:00
Joey Hain
70bbbd7d19 add highlight scope for type parameters (#8660)
* rust: add highlight scope for type parameters

* handle optional type parameters
2023-11-03 22:21:01 +01:00
Arkady Rost
ae6a0a9cfd Adjusted ui.virtual.inlay-hint color for everblush theme (#8705)
Co-authored-by: Arkady Rost <1239844+arkrost@users.noreply.github.com>
2023-11-03 09:51:22 +09:00
Omnikar
1755c61d08 Swap system and primary clipboard registers (#8703) 2023-11-03 09:51:10 +09:00
blinxen
a069b92897 Add missing license files for themes (#8684) 2023-10-31 17:09:48 -05:00
dependabot[bot]
53bb62b318 build(deps): bump tempfile from 3.8.0 to 3.8.1 (#8672)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-31 16:57:03 +09:00
dependabot[bot]
d32e052e0e build(deps): bump ahash from 0.8.5 to 0.8.6 (#8669)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-31 16:56:23 +09:00
dependabot[bot]
d171e23f72 build(deps): bump futures-util from 0.3.28 to 0.3.29 (#8673)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-31 12:27:15 +09:00
dependabot[bot]
403a1739cf build(deps): bump serde from 1.0.189 to 1.0.190 (#8670)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-31 12:27:07 +09:00
Blaž Hrastnik
ccc3085ad0 ci: Use a shared cache across build workflow steps 2023-10-31 11:55:34 +09:00
dependabot[bot]
566f41635e build(deps): bump rustix from 0.38.20 to 0.38.21 (#8671)
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2023-10-31 11:38:41 +09:00
Ryan Mehri
d0430f1c81 Only render preview if picker has a preview function (#8667) 2023-10-30 15:03:38 -05:00
Jeffrey Gelens
7d7ed78681 Add MacPorts as installation option for MacOS (#8663)
* Added MacPorts as installation option for MacOS

* Added macports to ToC
2023-10-30 19:28:55 +01:00
John Careaga
992b7a0c39 Update idle-timeout in docs (#8661) 2023-10-29 19:28:06 -05:00
Angus Dippenaar
44e03fa414 add golangci-lint-langserver (#8656)
* languages add golangci-lint-langserver

* update docs
2023-10-29 17:53:15 +01:00
Triton171
ef0c31db02 Fix precedence order of @align captures in indent computation (#8659)
precedence when multiple occur on the same line in an indent query.
2023-10-29 17:48:58 +01:00
RoloEdits
f992c3b597 feat(highlights): add more comment highlights (#8564) 2023-10-27 01:41:09 +02:00
blt__
ab266b99e6 Say "unindent" instead of "outdent" in tutor (#8623) 2023-10-27 01:40:49 +02:00
Mehedi Rifat
99bf62a560 Theme: Add gruber-darker theme (#8598) 2023-10-27 01:40:33 +02:00
Gabriel Dinner-David
4f1d414d9c switch to tree-sitter-ron (#8624) 2023-10-27 01:40:16 +02:00
Ryan Mehri
553ffbcaa0 Use terminfo to reset terminal cursor style (#8591) 2023-10-26 18:36:34 -05:00
Frans Skarman
9eec9adb8f Add LPF tree sitter (#8536)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2023-10-27 01:32:49 +02:00
Alexander Brevig
2906660119 Add typst language and lsp (#7474)
Co-authored-by: Michael Davis <mcarsondavis@gmail.com>
2023-10-27 01:27:42 +02:00
Michael Davis
ef1f4f31b6 CI: Publish stable book before master (#8621) 2023-10-26 23:07:36 +09:00
Blaž Hrastnik
5ce1c30f77 Revert "Pin tree-sitter to the 0.20.10 release (#8396)"
We only reverted so that the latest release would use a stable
tree-sitter version hosted on crates.io. We do want the improvements
on nightly.

This reverts commit 2ebcc4dbeb.
2023-10-26 15:58:10 +09:00
Ryan Mehri
c24a67c0e4 Add rust html injection query (#8603) 2023-10-26 11:39:22 +09:00
467 changed files with 22620 additions and 6457 deletions

View File

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

1
.envrc
View File

@@ -1,5 +1,6 @@
watch_file shell.nix
watch_file flake.lock
watch_file rust-toolchain.toml
# try to use flakes, if it fails use normal nix (ie. shell.nix)
use flake || use nix

View File

@@ -55,6 +55,16 @@ body:
placeholder: wezterm 20220101-133340-7edc5b5a
validations:
required: true
- type: input
id: installation-method
attributes:
label: Installation Method
description: >
How you installed Helix - from a package manager like Homebrew or the
AUR, built from source, downloaded a binary from the releases page, etc.
placeholder: "source / brew / nixpkgs / flake / releases page"
validations:
required: true
- type: input
id: helix-version
attributes:

View File

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

View File

@@ -12,6 +12,7 @@ jobs:
check:
name: Check (msrv)
runs-on: ubuntu-latest
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
steps:
- name: Checkout sources
uses: actions/checkout@v4
@@ -22,6 +23,8 @@ jobs:
override: true
- uses: Swatinem/rust-cache@v2
with:
shared-key: "build"
- name: Run cargo check
run: cargo check
@@ -29,6 +32,7 @@ jobs:
test:
name: Test Suite
runs-on: ${{ matrix.os }}
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
env:
RUST_BACKTRACE: 1
HELIX_LOG_LEVEL: info
@@ -37,12 +41,14 @@ jobs:
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.65
uses: dtolnay/rust-toolchain@1.70
- uses: Swatinem/rust-cache@v2
with:
shared-key: "build"
- name: Cache test tree-sitter grammar
uses: actions/cache@v3
uses: actions/cache@v4
with:
path: runtime/grammars
key: ${{ runner.os }}-stable-v${{ env.CACHE_VERSION }}-tree-sitter-grammars-${{ hashFiles('languages.toml') }}
@@ -61,16 +67,19 @@ jobs:
lints:
name: Lints
runs-on: ubuntu-latest
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.65
uses: dtolnay/rust-toolchain@1.70
with:
components: rustfmt, clippy
- uses: Swatinem/rust-cache@v2
with:
shared-key: "build"
- name: Run cargo fmt
run: cargo fmt --all --check
@@ -86,14 +95,17 @@ jobs:
docs:
name: Docs
runs-on: ubuntu-latest
if: github.repository == 'helix-editor/helix' || github.event_name != 'schedule'
steps:
- name: Checkout sources
uses: actions/checkout@v4
- name: Install stable toolchain
uses: dtolnay/rust-toolchain@1.65
uses: dtolnay/rust-toolchain@1.70
- uses: Swatinem/rust-cache@v2
with:
shared-key: "build"
- name: Validate queries
run: cargo xtask query-check

View File

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

View File

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

View File

@@ -36,7 +36,7 @@ jobs:
- name: Bundle grammars
run: tar cJf grammars.tar.xz -C runtime/grammars/sources .
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: grammars
path: grammars.tar.xz
@@ -106,7 +106,7 @@ jobs:
uses: actions/checkout@v4
- name: Download grammars
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
- name: Move grammars under runtime
if: "!startsWith(matrix.os, 'windows')"
@@ -220,7 +220,7 @@ jobs:
fi
cp -r runtime dist
- uses: actions/upload-artifact@v3
- uses: actions/upload-artifact@v4
with:
name: bins-${{ matrix.build }}
path: dist
@@ -233,7 +233,7 @@ jobs:
- name: Checkout sources
uses: actions/checkout@v4
- uses: actions/download-artifact@v3
- uses: actions/download-artifact@v4
- name: Build archive
shell: bash
@@ -288,7 +288,7 @@ jobs:
overwrite: true
- name: Upload binaries as artifact
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
if: env.preview == 'true'
with:
name: release

View File

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

View File

@@ -1,3 +1,256 @@
# 24.03 (2024-03-30)
As always, a big thank you to all of the contributors! This release saw changes from 125 contributors.
Breaking changes:
- `suffix` file-types in the `file-types` key in language configuration have been removed ([#8006](https://github.com/helix-editor/helix/pull/8006))
- The `file-types` key in language configuration no longer matches full filenames without a glob pattern ([#8006](https://github.com/helix-editor/helix/pull/8006))
Features:
- Open URLs with the `goto_file` command ([#5820](https://github.com/helix-editor/helix/pull/5820))
- Support drawing a border around popups and menus ([#4313](https://github.com/helix-editor/helix/pull/4313), [#9508](https://github.com/helix-editor/helix/pull/9508))
- Track long lived diagnostic sources like Clippy or `rustc` ([#6447](https://github.com/helix-editor/helix/pull/6447), [#9280](https://github.com/helix-editor/helix/pull/9280))
- This improves the handling of diagnostics from sources that only update the diagnostic positions on save.
- Add support for LSP `window/showDocument` requests ([#8865](https://github.com/helix-editor/helix/pull/8865))
- Refactor ad-hoc hooks to use a new generic event system ([#8021](https://github.com/helix-editor/helix/pull/8021), [#9668](https://github.com/helix-editor/helix/pull/9668), [#9660](https://github.com/helix-editor/helix/pull/9660))
- This improves the behavior of autocompletions. For example navigating in insert mode no longer automatically triggers completions.
- Allow using globs in the language configuration `file-types` key ([#8006](https://github.com/helix-editor/helix/pull/8006))
- Allow specifying required roots for situational LSP activation ([#8696](https://github.com/helix-editor/helix/pull/8696))
- Extend selections using mouse clicks in select mode ([#5436](https://github.com/helix-editor/helix/pull/5436))
- Toggle block comments ([#4718](https://github.com/helix-editor/helix/pull/4718), [#9894](https://github.com/helix-editor/helix/pull/9894))
- Support LSP diagnostic tags ([#9780](https://github.com/helix-editor/helix/pull/9780))
- Add a `file-absolute-path` statusline element ([#4535](https://github.com/helix-editor/helix/pull/4535))
- Cross injection layers in tree-sitter motions (`A-p`/`A-o`/`A-i`/`A-n`) ([#5176](https://github.com/helix-editor/helix/pull/5176))
- Add a Amp-editor-like jumping command ([#8875](https://github.com/helix-editor/helix/pull/8875))
Commands:
- `:move` - move buffers with LSP support ([#8584](https://github.com/helix-editor/helix/pull/8584))
- Also see [#8949](https://github.com/helix-editor/helix/pull/8949) which made path changes conform to the LSP spec and fixed the behavior of this command.
- `page_cursor_up`, `page_cursor_down`, `page_cursor_half_up`, `page_cursor_half_down` - commands for scrolling the cursor and page together ([#8015](https://github.com/helix-editor/helix/pull/8015))
- `:yank-diagnostic` - yank the diagnostic(s) under the primary cursor ([#9640](https://github.com/helix-editor/helix/pull/9640))
- `select_line_above` / `select_line_below` - extend or shrink a selection based on the direction and anchor ([#9080](https://github.com/helix-editor/helix/pull/9080))
Usability improvements:
- Make `roots` key of `[[language]]` entries in `languages.toml` configuration optional ([#8803](https://github.com/helix-editor/helix/pull/8803))
- Exit select mode in commands that modify the buffer ([#8689](https://github.com/helix-editor/helix/pull/8689))
- Use crossterm cursor when out of focus ([#6858](https://github.com/helix-editor/helix/pull/6858), [#8934](https://github.com/helix-editor/helix/pull/8934))
- Join empty lines with only one space in `join_selections` ([#8989](https://github.com/helix-editor/helix/pull/8989))
- Introduce a hybrid tree-sitter and contextual indentation heuristic ([#8307](https://github.com/helix-editor/helix/pull/8307))
- Allow configuring the indentation heuristic ([#8307](https://github.com/helix-editor/helix/pull/8307))
- Check for LSP rename support before showing rename prompt ([#9277](https://github.com/helix-editor/helix/pull/9277))
- Normalize `S-<lower-ascii>` keymaps to uppercase ascii ([#9213](https://github.com/helix-editor/helix/pull/9213))
- Add formatter status to `--health` output ([#7986](https://github.com/helix-editor/helix/pull/7986))
- Change path normalization strategy to not resolve symlinks ([#9330](https://github.com/helix-editor/helix/pull/9330))
- Select subtree within injections in `:tree-sitter-subtree` ([#9309](https://github.com/helix-editor/helix/pull/9309))
- Use tilde expansion and normalization for `$HELIX_RUNTIME` paths ([1bc7aac](https://github.com/helix-editor/helix/commit/1bc7aac))
- Improve failure message for LSP goto references ([#9382](https://github.com/helix-editor/helix/pull/9382))
- Use injection syntax trees for bracket matching ([5e0b3cc](https://github.com/helix-editor/helix/commit/5e0b3cc))
- Respect injections in `:tree-sitter-highlight-name` ([8b6565c](https://github.com/helix-editor/helix/commit/8b6565c))
- Respect injections in `move_parent_node_end` ([035b8ea](https://github.com/helix-editor/helix/commit/035b8ea))
- Use `gix` pipeline filter instead of manual CRLF implementation ([#9503](https://github.com/helix-editor/helix/pull/9503))
- Follow Neovim's truecolor detection ([#9577](https://github.com/helix-editor/helix/pull/9577))
- Reload language configuration with `:reload`, SIGHUP ([#9415](https://github.com/helix-editor/helix/pull/9415))
- Allow numbers as bindings ([#8471](https://github.com/helix-editor/helix/pull/8471), [#9887](https://github.com/helix-editor/helix/pull/9887))
- Respect undercurl config when terminfo is not available ([#9897](https://github.com/helix-editor/helix/pull/9897))
- Ignore `.pijul`, `.hg`, `.jj` in addition to `.git` in file pickers configured to show hidden files ([#9935](https://github.com/helix-editor/helix/pull/9935))
- Add completion for registers to `:clear-register` and `:yank-diagnostic` ([#9936](https://github.com/helix-editor/helix/pull/9936))
- Repeat last motion for goto next/prev diagnostic ([#9966](https://github.com/helix-editor/helix/pull/9966))
- Allow configuring a character to use when rendering narrow no-breaking space ([#9604](https://github.com/helix-editor/helix/pull/9604))
- Switch to a streaming regex engine (regex-cursor crate) to significantly speed up regex-based commands and features ([#9422](https://github.com/helix-editor/helix/pull/9422), [#9756](https://github.com/helix-editor/helix/pull/9756), [#9891](https://github.com/helix-editor/helix/pull/9891))
Fixes:
- Swap `*` and `+` registers ([#8703](https://github.com/helix-editor/helix/pull/8703), [#8708](https://github.com/helix-editor/helix/pull/8708))
- Use terminfo to reset terminal cursor style ([#8591](https://github.com/helix-editor/helix/pull/8591))
- Fix precedence of `@align` captures in indentat computation ([#8659](https://github.com/helix-editor/helix/pull/8659))
- Only render the preview if a Picker has a preview function ([#8667](https://github.com/helix-editor/helix/pull/8667))
- Fix the precedence of `ui.virtual.whitespace` ([#8750](https://github.com/helix-editor/helix/pull/8750), [#8879](https://github.com/helix-editor/helix/pull/8879))
- Fix crash in `:indent-style` ([#9087](https://github.com/helix-editor/helix/pull/9087))
- Fix `didSave` text inclusion when sync capability is a kind variant ([#9101](https://github.com/helix-editor/helix/pull/9101))
- Update the history of newly focused views ([#9271](https://github.com/helix-editor/helix/pull/9271))
- Initialize diagnostics when opening a document ([#8873](https://github.com/helix-editor/helix/pull/8873))
- Sync views when applying edits to unfocused views ([#9173](https://github.com/helix-editor/helix/pull/9173))
- This fixes crashes that could occur from LSP workspace edits or `:write-all`.
- Treat non-numeric `+arg`s passed in the CLI args as filenames ([#9333](https://github.com/helix-editor/helix/pull/9333))
- Fix crash when using `mm` on an empty plaintext file ([2fb7e50](https://github.com/helix-editor/helix/commit/2fb7e50))
- Ignore empty tree-sitter nodes in match bracket ([445f7a2](https://github.com/helix-editor/helix/commit/445f7a2))
- Exit a language server if it sends a message with invalid JSON ([#9332](https://github.com/helix-editor/helix/pull/9332))
- Handle failures to enable bracketed paste ([#9353](https://github.com/helix-editor/helix/pull/9353))
- Gate all captures in a pattern behind `#is-not? local` predicates ([#9390](https://github.com/helix-editor/helix/pull/9390))
- Make path changes LSP spec conformant ([#8949](https://github.com/helix-editor/helix/pull/8949))
- Use range positions to determine `insert_newline` motion ([#9448](https://github.com/helix-editor/helix/pull/9448))
- Fix division by zero when prompt completion area is too small ([#9524](https://github.com/helix-editor/helix/pull/9524))
- Add changes to history in clipboard replacement typable commands ([#9625](https://github.com/helix-editor/helix/pull/9625))
- Fix a crash in DAP with an unspecified `line` in breakpoints ([#9632](https://github.com/helix-editor/helix/pull/9632))
- Fix space handling for filenames in bash completion ([#9702](https://github.com/helix-editor/helix/pull/9702), [#9708](https://github.com/helix-editor/helix/pull/9708))
- Key diagnostics off of paths instead of LSP URIs ([#7367](https://github.com/helix-editor/helix/pull/7367))
- Fix panic when using `join_selections_space` ([#9783](https://github.com/helix-editor/helix/pull/9783))
- Fix panic when using `surround_replace`, `surround_delete` ([#9796](https://github.com/helix-editor/helix/pull/9796))
- Fix panic in `surround_replace`, `surround_delete` with nested surrounds and multiple cursors ([#9815](https://github.com/helix-editor/helix/pull/9815))
- Fix panic in `select_textobject_around` ([#9832](https://github.com/helix-editor/helix/pull/9832))
- Don't stop reloading documents when reloading fails in `:reload-all` ([#9870](https://github.com/helix-editor/helix/pull/9870))
- Prevent `shell_keep_pipe` from stopping on nonzero exit status codes ([#9817](https://github.com/helix-editor/helix/pull/9817))
Themes:
- Add `gruber-dark` ([#8598](https://github.com/helix-editor/helix/pull/8598))
- Update `everblush` ([#8705](https://github.com/helix-editor/helix/pull/8705))
- Update `papercolor` ([#8718](https://github.com/helix-editor/helix/pull/8718), [#8827](https://github.com/helix-editor/helix/pull/8827))
- Add `polmandres` ([#8759](https://github.com/helix-editor/helix/pull/8759))
- Add `starlight` ([#8787](https://github.com/helix-editor/helix/pull/8787))
- Update `naysayer` ([#8838](https://github.com/helix-editor/helix/pull/8838))
- Add modus operandi themes ([#8728](https://github.com/helix-editor/helix/pull/8728), [#9912](https://github.com/helix-editor/helix/pull/9912))
- Update `rose_pine` ([#8946](https://github.com/helix-editor/helix/pull/8946))
- Update `darcula` ([#8738](https://github.com/helix-editor/helix/pull/8738), [#9002](https://github.com/helix-editor/helix/pull/9002), [#9449](https://github.com/helix-editor/helix/pull/9449), [#9588](https://github.com/helix-editor/helix/pull/9588))
- Add modus vivendi themes ([#8894](https://github.com/helix-editor/helix/pull/8894), [#9912](https://github.com/helix-editor/helix/pull/9912))
- Add `horizon-dark` ([#9008](https://github.com/helix-editor/helix/pull/9008), [#9493](https://github.com/helix-editor/helix/pull/9493))
- Update `noctis` ([#9123](https://github.com/helix-editor/helix/pull/9123))
- Update `nord` ([#9135](https://github.com/helix-editor/helix/pull/9135))
- Update monokai pro themes ([#9148](https://github.com/helix-editor/helix/pull/9148))
- Update tokyonight themes ([#9099](https://github.com/helix-editor/helix/pull/9099), [#9724](https://github.com/helix-editor/helix/pull/9724), [#9789](https://github.com/helix-editor/helix/pull/9789))
- Add `ttox` ([#8524](https://github.com/helix-editor/helix/pull/8524))
- Add `voxed` ([#9164](https://github.com/helix-editor/helix/pull/9164))
- Update `sonokai` ([#9370](https://github.com/helix-editor/helix/pull/9370), [#9376](https://github.com/helix-editor/helix/pull/9376), [#5379](https://github.com/helix-editor/helix/pull/5379))
- Update `onedark`, `onedarker` ([#9397](https://github.com/helix-editor/helix/pull/9397))
- Update `cyan_light` ([#9375](https://github.com/helix-editor/helix/pull/9375), [#9688](https://github.com/helix-editor/helix/pull/9688))
- Add `gruvbox_light_soft`, `gruvbox_light_hard` ([#9266](https://github.com/helix-editor/helix/pull/9266))
- Update GitHub themes ([#9487](https://github.com/helix-editor/helix/pull/9487))
- Add `term16_dark`, `term16_light` ([#9477](https://github.com/helix-editor/helix/pull/9477))
- Update Zed themes ([#9544](https://github.com/helix-editor/helix/pull/9544), [#9549](https://github.com/helix-editor/helix/pull/9549))
- Add `curzon` ([#9553](https://github.com/helix-editor/helix/pull/9553))
- Add `monokai_soda` ([#9651](https://github.com/helix-editor/helix/pull/9651))
- Update catppuccin themes ([#9859](https://github.com/helix-editor/helix/pull/9859))
- Update `rasmus` ([#9939](https://github.com/helix-editor/helix/pull/9939))
- Update `dark_plus` ([#9949](https://github.com/helix-editor/helix/pull/9949), [628dcd5](https://github.com/helix-editor/helix/commit/628dcd5))
- Update gruvbox themes ([#9960](https://github.com/helix-editor/helix/pull/9960))
- Add jump label theming to `dracula` ([#9973](https://github.com/helix-editor/helix/pull/9973))
- Add jump label theming to `horizon-dark` ([#9984](https://github.com/helix-editor/helix/pull/9984))
- Add jump label theming to catppuccin themes ([2178adf](https://github.com/helix-editor/helix/commit/2178adf), [#9983](https://github.com/helix-editor/helix/pull/9983))
- Add jump label theming to `onedark` themes ([da2dec1](https://github.com/helix-editor/helix/commit/da2dec1))
- Add jump label theming to rose-pine themes ([#9981](https://github.com/helix-editor/helix/pull/9981))
- Add jump label theming to Nord themes ([#10008](https://github.com/helix-editor/helix/pull/10008))
- Add jump label theming to Monokai themes ([#10009](https://github.com/helix-editor/helix/pull/10009))
- Add jump label theming to gruvbox themes ([#10012](https://github.com/helix-editor/helix/pull/10012))
- Add jump label theming to `kanagawa` ([#10030](https://github.com/helix-editor/helix/pull/10030))
- Update material themes ([#10043](https://github.com/helix-editor/helix/pull/10043))
- Add `jetbrains_dark` ([#9967](https://github.com/helix-editor/helix/pull/9967))
New languages:
- Typst ([#7474](https://github.com/helix-editor/helix/pull/7474))
- LPF ([#8536](https://github.com/helix-editor/helix/pull/8536))
- GN ([#6969](https://github.com/helix-editor/helix/pull/6969))
- DBML ([#8860](https://github.com/helix-editor/helix/pull/8860))
- log ([#8916](https://github.com/helix-editor/helix/pull/8916))
- Janet ([#9081](https://github.com/helix-editor/helix/pull/9081), [#9247](https://github.com/helix-editor/helix/pull/9247))
- Agda ([#8285](https://github.com/helix-editor/helix/pull/8285))
- Avro ([#9113](https://github.com/helix-editor/helix/pull/9113))
- Smali ([#9089](https://github.com/helix-editor/helix/pull/9089))
- HOCON ([#9203](https://github.com/helix-editor/helix/pull/9203))
- Tact ([#9512](https://github.com/helix-editor/helix/pull/9512))
- PKL ([#9515](https://github.com/helix-editor/helix/pull/9515))
- CEL ([#9296](https://github.com/helix-editor/helix/pull/9296))
- SpiceDB ([#9296](https://github.com/helix-editor/helix/pull/9296))
- Hoon ([#9190](https://github.com/helix-editor/helix/pull/9190))
- DockerCompose ([#9661](https://github.com/helix-editor/helix/pull/9661), [#9916](https://github.com/helix-editor/helix/pull/9916))
- Groovy ([#9350](https://github.com/helix-editor/helix/pull/9350), [#9681](https://github.com/helix-editor/helix/pull/9681), [#9677](https://github.com/helix-editor/helix/pull/9677))
- FIDL ([#9713](https://github.com/helix-editor/helix/pull/9713))
- Powershell ([#9827](https://github.com/helix-editor/helix/pull/9827))
- ld ([#9835](https://github.com/helix-editor/helix/pull/9835))
- Hyperland config ([#9899](https://github.com/helix-editor/helix/pull/9899))
- JSONC ([#9906](https://github.com/helix-editor/helix/pull/9906))
- PHP Blade ([#9513](https://github.com/helix-editor/helix/pull/9513))
- SuperCollider ([#9329](https://github.com/helix-editor/helix/pull/9329))
- Koka ([#8727](https://github.com/helix-editor/helix/pull/8727))
- PKGBUILD ([#9909](https://github.com/helix-editor/helix/pull/9909), [#9943](https://github.com/helix-editor/helix/pull/9943))
- Ada ([#9908](https://github.com/helix-editor/helix/pull/9908))
- Helm charts ([#9900](https://github.com/helix-editor/helix/pull/9900))
- Ember.js templates ([#9902](https://github.com/helix-editor/helix/pull/9902))
- Ohm ([#9991](https://github.com/helix-editor/helix/pull/9991))
Updated languages and queries:
- Add HTML injection queries for Rust ([#8603](https://github.com/helix-editor/helix/pull/8603))
- Switch to tree-sitter-ron for RON ([#8624](https://github.com/helix-editor/helix/pull/8624))
- Update and improve comment highlighting ([#8564](https://github.com/helix-editor/helix/pull/8564), [#9253](https://github.com/helix-editor/helix/pull/9253), [#9800](https://github.com/helix-editor/helix/pull/9800), [#10014](https://github.com/helix-editor/helix/pull/10014))
- Highlight type parameters in Rust ([#8660](https://github.com/helix-editor/helix/pull/8660))
- Change KDL tree-sitter parsers ([#8652](https://github.com/helix-editor/helix/pull/8652))
- Update tree-sitter-markdown ([#8721](https://github.com/helix-editor/helix/pull/8721), [#10039](https://github.com/helix-editor/helix/pull/10039))
- Update tree-sitter-purescript ([#8712](https://github.com/helix-editor/helix/pull/8712))
- Add type parameter highlighting to TypeScript, Go, Haskell, OCaml and Kotlin ([#8718](https://github.com/helix-editor/helix/pull/8718))
- Add indentation queries for Scheme and lisps using tree-sitter-scheme ([#8720](https://github.com/helix-editor/helix/pull/8720))
- Recognize `meson_options.txt` as Meson ([#8794](https://github.com/helix-editor/helix/pull/8794))
- Add language server configuration for Nushell ([#8878](https://github.com/helix-editor/helix/pull/8878))
- Recognize `musicxml` as XML ([#8935](https://github.com/helix-editor/helix/pull/8935))
- Update tree-sitter-rescript ([#8962](https://github.com/helix-editor/helix/pull/8962))
- Update tree-sitter-python ([#8976](https://github.com/helix-editor/helix/pull/8976))
- Recognize `.envrc.local` and `.envrc.private` as env ([#8988](https://github.com/helix-editor/helix/pull/8988))
- Update tree-sitter-gleam ([#9003](https://github.com/helix-editor/helix/pull/9003), [9ceeea5](https://github.com/helix-editor/helix/commit/9ceeea5))
- Update tree-sitter-d ([#9021](https://github.com/helix-editor/helix/pull/9021))
- Fix R-markdown language name for LSP detection ([#9012](https://github.com/helix-editor/helix/pull/9012))
- Add haskell-language-server LSP configuration ([#9111](https://github.com/helix-editor/helix/pull/9111))
- Recognize `glif` as XML ([#9130](https://github.com/helix-editor/helix/pull/9130))
- Recognize `.prettierrc` as JSON ([#9214](https://github.com/helix-editor/helix/pull/9214))
- Add auto-pairs configuration for scheme ([#9232](https://github.com/helix-editor/helix/pull/9232))
- Add textobject queries for Scala ([#9191](https://github.com/helix-editor/helix/pull/9191))
- Add textobject queries for Protobuf ([#9184](https://github.com/helix-editor/helix/pull/9184))
- Update tree-sitter-wren ([#8544](https://github.com/helix-editor/helix/pull/8544))
- Add `spago.yaml` as an LSP root for PureScript ([#9362](https://github.com/helix-editor/helix/pull/9362))
- Improve highlight and indent queries for Bash, Make and CSS ([#9393](https://github.com/helix-editor/helix/pull/9393))
- Update tree-sitter-scala ([#9348](https://github.com/helix-editor/helix/pull/9348), [#9340](https://github.com/helix-editor/helix/pull/9340), [#9475](https://github.com/helix-editor/helix/pull/9475))
- Recognize `.bash_history` as Bash ([#9401](https://github.com/helix-editor/helix/pull/9401))
- Recognize Helix ignore files as ignore ([#9447](https://github.com/helix-editor/helix/pull/9447))
- Inject SQL into Scala SQL strings ([#9428](https://github.com/helix-editor/helix/pull/9428))
- Update gdscript textobjects ([#9288](https://github.com/helix-editor/helix/pull/9288))
- Update Go queries ([#9510](https://github.com/helix-editor/helix/pull/9510), [#9525](https://github.com/helix-editor/helix/pull/9525))
- Update tree-sitter-nushell ([#9502](https://github.com/helix-editor/helix/pull/9502))
- Update tree-sitter-unison, add indent queries ([#9505](https://github.com/helix-editor/helix/pull/9505))
- Update tree-sitter-slint ([#9551](https://github.com/helix-editor/helix/pull/9551), [#9698](https://github.com/helix-editor/helix/pull/9698))
- Update tree-sitter-swift ([#9586](https://github.com/helix-editor/helix/pull/9586))
- Add `fish_indent` as formatter for fish ([78ed3ad](https://github.com/helix-editor/helix/commit/78ed3ad))
- Recognize `zon` as Zig ([#9582](https://github.com/helix-editor/helix/pull/9582))
- Add a formatter for Odin ([#9537](https://github.com/helix-editor/helix/pull/9537))
- Update tree-sitter-erlang ([#9627](https://github.com/helix-editor/helix/pull/9627), [fdcd461](https://github.com/helix-editor/helix/commit/fdcd461))
- Capture Rust fields as argument textobjects ([#9637](https://github.com/helix-editor/helix/pull/9637))
- Improve Dart textobjects ([#9644](https://github.com/helix-editor/helix/pull/9644))
- Recognize `tmux.conf` as a bash file-type ([#9653](https://github.com/helix-editor/helix/pull/9653))
- Add textobjects queries for Nix ([#9659](https://github.com/helix-editor/helix/pull/9659))
- Add textobjects queries for HCL ([#9658](https://github.com/helix-editor/helix/pull/9658))
- Recognize osm and osc extensions as XML ([#9697](https://github.com/helix-editor/helix/pull/9697))
- Update tree-sitter-sql ([#9634](https://github.com/helix-editor/helix/pull/9634))
- Recognize pde Processing files as Java ([#9741](https://github.com/helix-editor/helix/pull/9741))
- Update tree-sitter-lua ([#9727](https://github.com/helix-editor/helix/pull/9727))
- Switch tree-sitter-nim parsers ([#9722](https://github.com/helix-editor/helix/pull/9722))
- Recognize GTK builder ui files as XML ([#9754](https://github.com/helix-editor/helix/pull/9754))
- Add configuration for markdown-oxide language server ([#9758](https://github.com/helix-editor/helix/pull/9758))
- Add a shebang for elvish ([#9779](https://github.com/helix-editor/helix/pull/9779))
- Fix precedence of Svelte TypeScript injection ([#9777](https://github.com/helix-editor/helix/pull/9777))
- Recognize common Dockerfile file types ([#9772](https://github.com/helix-editor/helix/pull/9772))
- Recognize NUON files as Nu ([#9839](https://github.com/helix-editor/helix/pull/9839))
- Add textobjects for Java native functions and constructors ([#9806](https://github.com/helix-editor/helix/pull/9806))
- Fix "braket" typeo in JSX highlights ([#9910](https://github.com/helix-editor/helix/pull/9910))
- Update tree-sitter-hurl ([#9775](https://github.com/helix-editor/helix/pull/9775))
- Add textobjects queries for Vala ([#8541](https://github.com/helix-editor/helix/pull/8541))
- Update tree-sitter-git-config ([9610254](https://github.com/helix-editor/helix/commit/9610254))
- Recognize 'mmd' as Mermaid ([459eb9a](https://github.com/helix-editor/helix/commit/459eb9a))
- Highlight Rust extern crate aliases ([c099dde](https://github.com/helix-editor/helix/commit/c099dde))
- Improve parameter highlighting in C++ ([f5d95de](https://github.com/helix-editor/helix/commit/f5d95de))
- Recognize 'rclone.conf' as INI ([#9959](https://github.com/helix-editor/helix/pull/9959))
- Add injections for GraphQL and ERB in Ruby heredocs ([#10036](https://github.com/helix-editor/helix/pull/10036))
- Add `main.odin` to Odin LSP roots ([#9968](https://github.com/helix-editor/helix/pull/9968))
Packaging:
- Allow user overlays in Nix grammars build ([#8749](https://github.com/helix-editor/helix/pull/8749))
- Set Cargo feature resolver to v2 ([#8917](https://github.com/helix-editor/helix/pull/8917))
- Use workspace inheritance for common Cargo metadata ([#8925](https://github.com/helix-editor/helix/pull/8925))
- Remove sourcehut-based tree-sitter grammars from default build ([#9316](https://github.com/helix-editor/helix/pull/9316), [#9326](https://github.com/helix-editor/helix/pull/9326))
- Add icon to Windows executable ([#9104](https://github.com/helix-editor/helix/pull/9104))
# 23.10 (2023-10-24)
A big shout out to all the contributors! We had 118 contributors in this release.

1103
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,5 @@
[workspace]
resolver = "2"
members = [
"helix-core",
"helix-view",
@@ -10,6 +11,7 @@ members = [
"helix-loader",
"helix-vcs",
"helix-parsec",
"helix-stdx",
"xtask",
]
@@ -35,5 +37,16 @@ package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.20" }
tree-sitter = { version = "0.22" }
nucleo = "0.2.0"
slotmap = "1.0.7"
[workspace.package]
version = "24.3.0"
edition = "2021"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
license = "MPL-2.0"
rust-version = "1.70"

View File

@@ -18,7 +18,7 @@
![Screenshot](./screenshot.png)
A Kakoune / Neovim inspired editor, written in Rust.
A [Kakoune](https://github.com/mawww/kakoune) / [Neovim](https://github.com/neovim/neovim) inspired editor, written in Rust.
The editing model is very heavily based on Kakoune; during development I found
myself agreeing with most of Kakoune's design decisions.

View File

@@ -1 +0,0 @@
23.10

View File

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

View File

@@ -51,7 +51,8 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `auto-completion` | Enable automatic pop up of auto-completion | `true` |
| `auto-format` | Enable automatic formatting on save | `true` |
| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` |
| `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` |
@@ -65,6 +66,9 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `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
@@ -72,7 +76,7 @@ Allows configuring the statusline at the bottom of the editor.
The configuration distinguishes between three areas of the status line:
`[ ... ... LEFT ... ... | ... ... ... ... CENTER ... ... ... ... | ... ... RIGHT ... ... ]`
`[ ... ... LEFT ... ... | ... ... ... CENTER ... ... ... | ... ... RIGHT ... ... ]`
Statusline elements can be defined as follows:
@@ -105,6 +109,7 @@ The following statusline elements can be configured:
| `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 |
@@ -166,15 +171,30 @@ 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.excludefile` option | true
|`git-exclude` | Enables reading `.git/info/exclude` files | true
|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`.
|`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
@@ -205,7 +225,7 @@ 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 ''
Example `languages.toml` that adds `<>` and removes `''`
```toml
[[language]]
@@ -235,8 +255,8 @@ Options for rendering whitespace with visible characters. Use `:set whitespace.r
| Key | Description | Default |
|-----|-------------|---------|
| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline` | `"none"` |
| `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below |
| `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
@@ -247,11 +267,14 @@ render = "all"
[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)
@@ -322,7 +345,12 @@ Currently unused
#### `[editor.gutters.diff]` Section
Currently unused
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
@@ -352,8 +380,25 @@ 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

@@ -1,5 +1,8 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- |
| ada | ✓ | ✓ | | `ada_language_server`, `ada_language_server` |
| adl | ✓ | ✓ | ✓ | |
| agda | ✓ | | | |
| astro | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` |
| bash | ✓ | ✓ | ✓ | `bash-language-server` |
@@ -7,29 +10,35 @@
| beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` |
| bitbake | ✓ | | | `bitbake-language-server` |
| blade | ✓ | | | |
| blueprint | ✓ | | | `blueprint-compiler` |
| c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` |
| cabal | | | | |
| cabal | | | | `haskell-language-server-wrapper` |
| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
| capnp | ✓ | | ✓ | |
| cel | ✓ | | | |
| clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
| comment | ✓ | | | |
| common-lisp | ✓ | | | `cl-lsp` |
| common-lisp | ✓ | | | `cl-lsp` |
| cpon | ✓ | | ✓ | |
| cpp | ✓ | ✓ | ✓ | `clangd` |
| crystal | ✓ | ✓ | | `crystalline` |
| css | ✓ | | | `vscode-css-language-server` |
| css | ✓ | | | `vscode-css-language-server` |
| cue | ✓ | | | `cuelsp` |
| d | ✓ | ✓ | ✓ | `serve-d` |
| dart | ✓ | | ✓ | `dart` |
| dart | ✓ | | ✓ | `dart` |
| dbml | ✓ | | | |
| devicetree | ✓ | | | |
| dhall | ✓ | ✓ | | `dhall-lsp-server` |
| diff | ✓ | | | |
| docker-compose | ✓ | | ✓ | `docker-compose-langserver`, `yaml-language-server` |
| dockerfile | ✓ | | | `docker-langserver` |
| dot | ✓ | | | `dot-language-server` |
| dtd | ✓ | | | |
| earthfile | ✓ | ✓ | ✓ | `earthlyls` |
| edoc | ✓ | | | |
| eex | ✓ | | | |
| ejs | ✓ | | | |
@@ -40,6 +49,7 @@
| erb | ✓ | | | |
| erlang | ✓ | ✓ | | `erlang_ls` |
| esdl | ✓ | | | |
| fidl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | `forth-lsp` |
| fortran | ✓ | | ✓ | `fortls` |
@@ -53,61 +63,77 @@
| git-ignore | ✓ | | | |
| git-rebase | ✓ | | | |
| gleam | ✓ | ✓ | | `gleam` |
| glimmer | ✓ | | | `ember-language-server` |
| glsl | ✓ | ✓ | ✓ | |
| go | ✓ | ✓ | ✓ | `gopls` |
| gn | ✓ | | | |
| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
| godot-resource | ✓ | | | |
| gomod | ✓ | | | `gopls` |
| gotmpl | ✓ | | | `gopls` |
| gowork | ✓ | | | `gopls` |
| graphql | ✓ | | | `graphql-lsp` |
| graphql | ✓ | | | `graphql-lsp` |
| groovy | ✓ | | | |
| hare | ✓ | | | |
| haskell | ✓ | ✓ | | `haskell-language-server-wrapper` |
| haskell-persistent | ✓ | | | |
| hcl | ✓ | | ✓ | `terraform-ls` |
| hcl | ✓ | | ✓ | `terraform-ls` |
| heex | ✓ | ✓ | | `elixir-ls` |
| helm | ✓ | | | `helm_ls` |
| hocon | ✓ | | ✓ | |
| hoon | ✓ | | | |
| hosts | ✓ | | | |
| html | ✓ | | | `vscode-html-language-server` |
| hurl | ✓ | | ✓ | |
| hyprlang | ✓ | | ✓ | |
| idris | | | | `idris2-lsp` |
| iex | ✓ | | | |
| ini | ✓ | | | |
| janet | ✓ | | | |
| java | ✓ | ✓ | ✓ | `jdtls` |
| javascript | ✓ | ✓ | ✓ | `typescript-language-server` |
| jinja | ✓ | | | |
| jsdoc | ✓ | | | |
| json | ✓ | | ✓ | `vscode-json-language-server` |
| json | ✓ | | ✓ | `vscode-json-language-server` |
| json5 | ✓ | | | |
| jsonc | ✓ | | ✓ | `vscode-json-language-server` |
| jsonnet | ✓ | | | `jsonnet-language-server` |
| jsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| julia | ✓ | ✓ | ✓ | `julia` |
| just | ✓ | ✓ | ✓ | |
| kdl | ✓ | | | |
| kdl | ✓ | | | |
| koka | ✓ | | ✓ | `koka` |
| kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | ✓ | | `texlab` |
| ld | ✓ | | ✓ | |
| ldif | ✓ | | | |
| lean | ✓ | | | `lean` |
| ledger | ✓ | | | |
| llvm | ✓ | ✓ | ✓ | |
| llvm-mir | ✓ | ✓ | ✓ | |
| llvm-mir-yaml | ✓ | | ✓ | |
| log | ✓ | | | |
| lpf | ✓ | | | |
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
| make | ✓ | | | |
| make | ✓ | | | |
| markdoc | ✓ | | | `markdoc-ls` |
| markdown | ✓ | | | `marksman` |
| markdown | ✓ | | | `marksman`, `markdown-oxide` |
| markdown.inline | ✓ | | | |
| matlab | ✓ | ✓ | ✓ | |
| mermaid | ✓ | | | |
| meson | ✓ | | ✓ | |
| mint | | | | `mint` |
| move | ✓ | | | |
| msbuild | ✓ | | ✓ | |
| nasm | ✓ | ✓ | | |
| nickel | ✓ | | ✓ | `nls` |
| nim | ✓ | ✓ | ✓ | `nimlangserver` |
| nix | ✓ | | | `nil` |
| nu | ✓ | | | |
| nix | ✓ | | | `nil` |
| nu | ✓ | | | `nu` |
| nunjucks | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | ✓ | `ols` |
| ohm | ✓ | ✓ | ✓ | |
| opencl | ✓ | ✓ | ✓ | `clangd` |
| openscad | ✓ | | | `openscad-lsp` |
| org | ✓ | | | |
@@ -116,18 +142,22 @@
| pem | ✓ | | | |
| perl | ✓ | ✓ | ✓ | `perlnavigator` |
| php | ✓ | ✓ | ✓ | `intelephense` |
| php-only | ✓ | | | |
| pkgbuild | ✓ | ✓ | ✓ | `pkgbuild-language-server`, `bash-language-server` |
| pkl | ✓ | | ✓ | |
| po | ✓ | ✓ | | |
| pod | ✓ | | | |
| ponylang | ✓ | ✓ | ✓ | |
| powershell | ✓ | | | |
| prisma | ✓ | | | `prisma-language-server` |
| prolog | | | | `swipl` |
| protobuf | ✓ | | ✓ | `bufls`, `pb` |
| protobuf | ✓ | | ✓ | `bufls`, `pb` |
| prql | ✓ | | | |
| purescript | ✓ | ✓ | | `purescript-language-server` |
| python | ✓ | ✓ | ✓ | `pylsp` |
| qml | ✓ | | ✓ | `qmlls` |
| r | ✓ | | | `R` |
| racket | ✓ | | | `racket` |
| racket | ✓ | | | `racket` |
| regex | ✓ | | | |
| rego | ✓ | | | `regols` |
| rescript | ✓ | ✓ | | `rescript-language-server` |
@@ -138,36 +168,42 @@
| ruby | ✓ | ✓ | ✓ | `solargraph` |
| rust | ✓ | ✓ | ✓ | `rust-analyzer` |
| sage | ✓ | ✓ | | |
| scala | ✓ | | ✓ | `metals` |
| scheme | ✓ | | | |
| scala | ✓ | | ✓ | `metals` |
| scheme | ✓ | | | |
| scss | ✓ | | | `vscode-css-language-server` |
| slint | ✓ | | ✓ | `slint-lsp` |
| slint | ✓ | | ✓ | `slint-lsp` |
| smali | ✓ | | ✓ | |
| smithy | ✓ | | | `cs` |
| sml | ✓ | | | |
| solidity | ✓ | | | `solc` |
| solidity | ✓ | | | `solc` |
| spicedb | ✓ | | | |
| sql | ✓ | | | |
| sshclientconfig | ✓ | | | |
| starlark | ✓ | ✓ | | |
| strace | ✓ | | | |
| supercollider | ✓ | | | |
| svelte | ✓ | | ✓ | `svelteserver` |
| sway | ✓ | ✓ | ✓ | `forc` |
| swift | ✓ | | | `sourcekit-lsp` |
| t32 | ✓ | | | |
| tablegen | ✓ | ✓ | ✓ | |
| tact | ✓ | ✓ | ✓ | |
| task | ✓ | | | |
| tcl | ✓ | | ✓ | |
| templ | ✓ | | | `templ` |
| tfvars | ✓ | | ✓ | `terraform-ls` |
| todotxt | ✓ | | | |
| toml | ✓ | | | `taplo` |
| toml | ✓ | | | `taplo` |
| tsq | ✓ | | | |
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
| twig | ✓ | | | |
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
| typst | ✓ | | | `tinymist`, `typst-lsp` |
| ungrammar | ✓ | | | |
| unison | ✓ | | | |
| unison | ✓ | | | |
| uxntal | ✓ | | | |
| v | ✓ | ✓ | ✓ | `v-analyzer` |
| vala | ✓ | | | `vala-language-server` |
| vala | ✓ | | | `vala-language-server` |
| verilog | ✓ | ✓ | | `svlangserver` |
| vhdl | ✓ | | | `vhdl_ls` |
| vhs | ✓ | | | |
@@ -180,6 +216,7 @@
| wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | |
| xml | ✓ | | ✓ | |
| xtc | ✓ | | | |
| yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` |
| yuck | ✓ | | | |
| zig | ✓ | ✓ | ✓ | `zls` |

View File

@@ -17,7 +17,7 @@
| `:write-buffer-close!`, `:wbc!` | Force write changes to disk creating necessary subdirectories and closes the buffer. Accepts an optional path (:write-buffer-close! some/path.txt) |
| `:new`, `:n` | Create a new scratch buffer. |
| `:format`, `:fmt` | Format the file using the LSP formatter. |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) |
| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.) |
| `:line-ending` | Set the document's default line ending. Options: crlf, lf. |
| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. |
| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. |
@@ -85,3 +85,6 @@
| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. |
| `:clear-register` | Clear given register. If no argument is provided, clear all registers. |
| `:redraw` | Clear and re-render the whole UI |
| `:move` | Move the current buffer and its corresponding file to a different path |
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
| `:read`, `:r` | Load a file into buffer |

View File

@@ -36,6 +36,7 @@ below.
3. Refer to the
[tree-sitter website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries)
for more information on writing queries.
4. A list of highlight captures can be found [on the themes page](https://docs.helix-editor.com/themes.html#scopes).
> 💡 In Helix, the first matching query takes precedence when evaluating
> queries, which is different from other editors such as Neovim where the last
@@ -51,3 +52,4 @@ below.
grammars.
- If a parser is causing a segfault, or you want to remove it, make sure to
remove the compiled parser located at `runtime/grammars/<name>.so`.
- If you are attempting to add queries and Helix is unable to locate them, ensure that the environment variable `HELIX_RUNTIME` is set to the location of the `runtime` folder you're developing in.

View File

@@ -12,6 +12,15 @@ Note that it matters where these added indents begin. For example,
multiple indent level increases that start on the same line only increase
the total indent level by 1. See [Capture types](#capture-types).
By default, Helix uses the `hybrid` indentation heuristic. This means that
indent queries are not used to compute the expected absolute indentation of a
line but rather the expected difference in indentation between the new and an
already existing line. This difference is then added to the actual indentation
of the already existing line. Since this makes errors in the indent queries
harder to find, it is recommended to disable it when testing via
`:set indent-heuristic tree-sitter`. The rest of this guide assumes that
the `tree-sitter` heuristic is used.
## Indent queries
When Helix is inserting a new line through `o`, `O`, or `<ret>`, to determine
@@ -306,6 +315,10 @@ The first argument (a capture) must/must not be equal to the second argument
The first argument (a capture) must/must not match the regex given in the
second argument (a string).
- `#any-of?`/`#not-any-of?`:
The first argument (a capture) must/must not be one of the other arguments
(strings).
Additionally, we support some custom predicates for indent queries:
- `#not-kind-eq?`:
@@ -357,4 +370,4 @@ Everything up to and including the closing brace gets an indent level of 1.
Then, on the closing brace, we encounter an outdent with a scope of "all", which
means the first line is included, and the indent level is cancelled out on this
line. (Note these scopes are the defaults for `@indent` and `@outdent`—they are
written explicitly for demonstration.)
written explicitly for demonstration.)

View File

@@ -54,4 +54,7 @@ The first argument (a capture) must be equal to the second argument
The first argument (a capture) must match the regex given in the
second argument (a string).
- `#any-of?` (standard):
The first argument (a capture) must be one of the other arguments (strings).
[upstream-docs]: http://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection

View File

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

View File

@@ -13,6 +13,7 @@
- [AppImage](#appimage)
- [macOS](#macos)
- [Homebrew Core](#homebrew-core)
- [MacPorts](#macports)
- [Windows](#windows)
- [Winget](#winget)
- [Scoop](#scoop)
@@ -64,10 +65,7 @@ sudo apt install helix
### Fedora/RHEL
Enable the `COPR` repository for Helix:
```sh
sudo dnf copr enable varlad/helix
sudo dnf install helix
```
@@ -78,6 +76,15 @@ 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.
@@ -133,6 +140,12 @@ chmod +x helix-*.AppImage # change permission for executable mode
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/)
@@ -200,6 +213,8 @@ RUSTFLAGS="-C target-feature=-crt-static"
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
@@ -210,12 +225,12 @@ RUSTFLAGS="-C target-feature=-crt-static"
#### Linux and macOS
The **runtime** directory is one below the Helix source, so either set a
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
HELIX_RUNTIME=~/src/helix/runtime
export HELIX_RUNTIME=~/src/helix/runtime
```
Or, create a symbolic link:

View File

@@ -12,6 +12,7 @@
- [Match mode](#match-mode)
- [Window mode](#window-mode)
- [Space mode](#space-mode)
- [Comment mode](#comment-mode)
- [Popup](#popup)
- [Unimpaired](#unimpaired)
- [Insert mode](#insert-mode)
@@ -23,9 +24,11 @@
> 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type.
> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the (wiki)[https://github.com/helix-editor/helix/wiki/Terminal-Support] for known conflicts.
## Normal mode
Normal mode is the default mode when you launch helix. Return to it from other modes by typing `Escape`.
Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key.
### Movement
@@ -48,13 +51,13 @@ Normal mode is the default mode when you launch helix. Return to it from other m
| `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
| `G` | Go to line number `<n>` | `goto_line` |
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
| `Alt-.` | Repeat last motion (`f`, `t`, `m`, `[` or `]`) | `repeat_last_motion` |
| `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` |
| `Ctrl-b`, `PageUp` | Move page up | `page_up` |
| `Ctrl-f`, `PageDown` | Move page down | `page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-u` | Move cursor and page half page up | `page_cursor_half_up` |
| `Ctrl-d` | Move cursor and page half page down | `page_cursor_half_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` |
@@ -182,18 +185,18 @@ normal mode) is persistent and can be exited using the escape key. This is
useful when you're simply looking over text and not actively editing it.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `z`, `c` | Vertically center the line | `align_view_center` |
| `t` | Align the line to the top of the screen | `align_view_top` |
| `b` | Align the line to the bottom of the screen | `align_view_bottom` |
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j`, `down` | Scroll the view downwards | `scroll_down` |
| `k`, `up` | Scroll the view upwards | `scroll_up` |
| `Ctrl-f`, `PageDown` | Move page down | `page_down` |
| `Ctrl-b`, `PageUp` | Move page up | `page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `z`, `c` | Vertically center the line | `align_view_center` |
| `t` | Align the line to the top of the screen | `align_view_top` |
| `b` | Align the line to the bottom of the screen | `align_view_bottom` |
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j`, `down` | Scroll the view downwards | `scroll_down` |
| `k`, `up` | Scroll the view upwards | `scroll_up` |
| `Ctrl-f`, `PageDown` | Move page down | `page_down` |
| `Ctrl-b`, `PageUp` | Move page up | `page_up` |
| `Ctrl-u` | Move cursor and page half page up | `page_cursor_half_up` |
| `Ctrl-d` | Move cursor and page half page down | `page_cursor_half_down` |
#### Goto mode
@@ -205,7 +208,7 @@ Jumps to various locations.
| ----- | ----------- | ------- |
| `g` | Go to line number `<n>` else start of file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` |
| `f` | Go to files in the selection | `goto_file` |
| `f` | Go to files in the selections | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
@@ -223,6 +226,7 @@ Jumps to various locations.
| `.` | Go to last modification in current file | `goto_last_modification` |
| `j` | Move down textual (instead of visual) line | `move_line_down` |
| `k` | Move up textual (instead of visual) line | `move_line_up` |
| `w` | Show labels at each word and select the word that belongs to the entered labels | `goto_word` |
#### Match mode
@@ -253,8 +257,8 @@ This layer is similar to Vim keybindings as Kakoune does not support windows.
| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `f` | Go to files in the selection in horizontal splits | `goto_file` |
| `F` | Go to files in the selection in vertical splits | `goto_file` |
| `f` | Go to files in the selections in horizontal splits | `goto_file` |
| `F` | Go to files in the selections in vertical splits | `goto_file` |
| `h`, `Ctrl-h`, `Left` | Move to left split | `jump_view_left` |
| `j`, `Ctrl-j`, `Down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `Up` | Move to split above | `jump_view_up` |
@@ -278,7 +282,8 @@ This layer is a kludge of mappings, mostly pickers.
| `F` | Open file picker at current working directory | `file_picker_in_current_directory` |
| `b` | Open buffer picker | `buffer_picker` |
| `j` | Open jumplist picker | `jumplist_picker` |
| `g` | Debug (experimental) | N/A |
| `g` | Open changed file picker | `changed_file_picker` |
| `G` | Debug (experimental) | N/A |
| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
@@ -289,6 +294,9 @@ This layer is a kludge of mappings, mostly pickers.
| `h` | Select symbol references (**LSP**) | `select_references_to_symbol_under_cursor` |
| `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A |
| `c` | Comment/uncomment selections | `toggle_comments` |
| `C` | Block comment/uncomment selections | `toggle_block_comments` |
| `Alt-c` | Line comment/uncomment selections | `toggle_line_comments` |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Yank selections to clipboard | `yank_to_clipboard` |

View File

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

View File

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

View File

@@ -36,13 +36,6 @@ For inspiration, you can find the default `theme.toml`
user-submitted themes
[here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
### Using the linter
Use the supplied linting tool to check for errors and missing scopes:
```sh
cargo xtask themelint onedark # replace onedark with <name>
```
## The details of theme creation
@@ -155,6 +148,7 @@ We use a similar set of scopes as
- `type` - Types
- `builtin` - Primitive types provided by the language (`int`, `usize`)
- `parameter` - Generic type parameters (`T`)
- `enum`
- `variant`
- `constructor`
@@ -250,6 +244,7 @@ We use a similar set of scopes as
- `gutter` - gutter indicator
- `delta` - modifications
- `moved` - renamed or moved files/changes
- `conflict` - merge conflicts
- `gutter` - gutter indicator
#### Interface
@@ -295,11 +290,14 @@ These scopes are used for theming the editor interface:
| `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline |
| `ui.bufferline` | Style for the buffer line |
| `ui.bufferline.active` | Style for the active buffer in buffer line |
| `ui.bufferline.background` | Style for bufferline background |
| `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.window` | Borderlines separating splits |
| `ui.help` | Description box for commands |
| `ui.text` | Command prompts, popup text, etc. |
| `ui.text` | Default text style, command prompts, popup text, etc. |
| `ui.text.focus` | The currently selected line in the picker |
| `ui.text.inactive` | Same as `ui.text` but when the text is inactive (e.g. suggestions) |
| `ui.text.info` | The key: command text in `ui.popup.info` boxes |
@@ -310,6 +308,7 @@ These scopes are used for theming the editor interface:
| `ui.virtual.inlay-hint.parameter` | Style for inlay hints of kind `parameter` (LSPs are not required to set a kind) |
| `ui.virtual.inlay-hint.type` | Style for inlay hints of kind `type` (LSPs are not required to set a kind) |
| `ui.virtual.wrap` | Soft-wrap indicator (see the [`editor.soft-wrap` config][editor-section]) |
| `ui.virtual.jump-label` | Style for virtual jump labels |
| `ui.menu` | Code and command completion menus |
| `ui.menu.selected` | Selected autocomplete item |
| `ui.menu.scroll` | `fg` sets thumb color, `bg` sets track color of scrollbar |
@@ -329,5 +328,7 @@ These scopes are used for theming the editor interface:
| `diagnostic.info` | Diagnostics info (editing area) |
| `diagnostic.warning` | Diagnostics warning (editing area) |
| `diagnostic.error` | Diagnostics error (editing area) |
| `diagnostic.unnecessary` | Diagnostics with unnecessary tag (editing area) |
| `diagnostic.deprecated` | Diagnostics with deprecated tag (editing area) |
[editor-section]: ./configuration.md#editor-section

View File

@@ -59,8 +59,8 @@ Some registers have special behavior when read from and written to.
| `#` | 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 |
| `+` | 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

View File

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

View File

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

BIN
contrib/helix-256p.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 264 KiB

View File

@@ -1,16 +1,23 @@
## Checklist
Helix releases are versioned in the Calendar Versioning scheme:
`YY.0M(.MICRO)`, for example, `22.05` for May of 2022. In these instructions
we'll use `<tag>` as a placeholder for the tag being published.
`YY.0M(.MICRO)`, for example, `22.05` for May of 2022, or in a patch release,
`22.05.1`. In these instructions we'll use `<tag>` as a placeholder for the tag
being published.
* Merge the changelog PR
* Add new `<release>` entry in `contrib/Helix.appdata.xml` with release information according to the [AppStream spec](https://www.freedesktop.org/software/appstream/docs/sect-Metadata-Releases.html)
* Tag and push
* `git tag -s -m "<tag>" -a <tag> && git push`
* Make sure to switch to master and pull first
* Edit the `VERSION` file and change the date to the next planned release
* Releases are planned to happen every two months, so `22.05` would change to `22.07`
* Edit the `Cargo.toml` file and change the date in the `version` field to the next planned release
* Due to Cargo having a strict requirement on SemVer with 3 or more version
numbers, a `0` is required in the micro version; however, unless we are
publishing a patch release after a major release, the `.0` is dropped in
the user facing version.
* Releases are planned to happen every two months, so `22.05.0` would change to `22.07.0`
* If we are pushing a patch/bugfix release in the same month as the previous
release, bump the micro version, e.g. `22.07.0` to `22.07.1`
* Wait for the Release CI to finish
* It will automatically turn the git tag into a GitHub release when it uploads artifacts
* Edit the new release
@@ -57,4 +64,4 @@ versions for convenience:
> release. For the full log, check out the git log.
Typically, small changes like dependencies or documentation updates, refactors,
or meta changes like GitHub Actions work are left out.
or meta changes like GitHub Actions work are left out.

47
flake.lock generated
View File

@@ -2,23 +2,16 @@
"nodes": {
"crane": {
"inputs": {
"flake-compat": "flake-compat",
"flake-utils": [
"flake-utils"
],
"nixpkgs": [
"nixpkgs"
],
"rust-overlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1688772518,
"narHash": "sha256-ol7gZxwvgLnxNSZwFTDJJ49xVY5teaSvF7lzlo3YQfM=",
"lastModified": 1709610799,
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "8b08e96c9af8c6e3a2b69af5a7fa168750fcf88e",
"rev": "81c393c776d5379c030607866afef6406ca1be57",
"type": "github"
},
"original": {
@@ -27,32 +20,16 @@
"type": "github"
}
},
"flake-compat": {
"flake": false,
"locked": {
"lastModified": 1673956053,
"narHash": "sha256-4gtG9iQuiKITOjNQQeQIpoIB6b16fm+504Ch3sNKLd8=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "35bb57c0c8d8b62bbfd284272c928ceb64ddbde9",
"type": "github"
},
"original": {
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"flake-utils": {
"inputs": {
"systems": "systems"
},
"locked": {
"lastModified": 1689068808,
"narHash": "sha256-6ixXo3wt24N/melDWjq70UuHQLxGV8jZvooRanIHXw0=",
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "919d646de7be200f3bf08cb76ae1f09402b6f9b4",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
@@ -63,11 +40,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1690272529,
"narHash": "sha256-MakzcKXEdv/I4qJUtq/k/eG+rVmyOZLnYNC2w1mB59Y=",
"lastModified": 1709479366,
"narHash": "sha256-n6F0n8UV6lnTZbYPl1A9q1BS0p4hduAv1mGAP17CVd0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "ef99fa5c5ed624460217c31ac4271cfb5cb2502c",
"rev": "b8697e57f10292a6165a20f03d2f42920dfaf973",
"type": "github"
},
"original": {
@@ -95,11 +72,11 @@
]
},
"locked": {
"lastModified": 1690424156,
"narHash": "sha256-Bpml+L280tHTQpwpC5/BJbU4HSvEzMvW8IZ4gAXimhE=",
"lastModified": 1709604635,
"narHash": "sha256-le4fwmWmjGRYWwkho0Gr7mnnZndOOe4XGbLw68OvF40=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "f335a0213504c7e6481c359dc1009be9cf34432c",
"rev": "e86c0fb5d3a22a5f30d7f64ecad88643fe26449d",
"type": "github"
},
"original": {

View File

@@ -13,8 +13,6 @@
};
crane = {
url = "github:ipetkov/crane";
inputs.rust-overlay.follows = "rust-overlay";
inputs.flake-utils.follows = "flake-utils";
inputs.nixpkgs.follows = "nixpkgs";
};
};
@@ -123,18 +121,18 @@
rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml;
craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain;
craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default;
commonArgs =
{
inherit stdenv;
src = filteredSource;
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
buildInputs = [stdenv.cc.cc.lib];
# disable tests
doCheck = false;
meta.mainProgram = "hx";
}
// craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;};
commonArgs = {
inherit stdenv;
inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;}) pname;
inherit (craneLibMSRV.crateNameFromCargoToml {cargoToml = ./Cargo.toml;}) version;
src = filteredSource;
# disable fetching and building of tree-sitter grammars in the helix-term build.rs
HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1";
buildInputs = [stdenv.cc.cc.lib];
# disable tests
doCheck = false;
meta.mainProgram = "hx";
};
cargoArtifacts = craneLibMSRV.buildDepsOnly commonArgs;
in {
packages = {

View File

@@ -5,6 +5,7 @@
runCommand,
yj,
includeGrammarIf ? _: true,
grammarOverlays ? [],
...
}: let
# HACK: nix < 2.6 has a bug in the toml parser, so we convert to JSON
@@ -27,7 +28,17 @@
owner = builtins.elemAt match 0;
repo = builtins.elemAt match 1;
};
gitGrammars = builtins.filter isGitGrammar languagesConfig.grammar;
# If `use-grammars.only` is set, use only those grammars.
# If `use-grammars.except` is set, use all other grammars.
# Otherwise use all grammars.
useGrammar = grammar:
if languagesConfig?use-grammars.only then
builtins.elem grammar.name languagesConfig.use-grammars.only
else if languagesConfig?use-grammars.except then
!(builtins.elem grammar.name languagesConfig.use-grammars.except)
else true;
grammarsToUse = builtins.filter useGrammar languagesConfig.grammar;
gitGrammars = builtins.filter isGitGrammar grammarsToUse;
buildGrammar = grammar: let
gh = toGitHubFetcher grammar.source.git;
sourceGit = builtins.fetchTree {
@@ -48,22 +59,22 @@
then sourceGitHub
else sourceGit;
in
stdenv.mkDerivation rec {
stdenv.mkDerivation {
# see https://github.com/NixOS/nixpkgs/blob/fbdd1a7c0bc29af5325e0d7dd70e804a972eb465/pkgs/development/tools/parsing/tree-sitter/grammar.nix
pname = "helix-tree-sitter-${grammar.name}";
version = grammar.source.rev;
src =
if builtins.hasAttr "subpath" grammar.source
then "${source}/${grammar.source.subpath}"
else source;
src = source;
sourceRoot = if builtins.hasAttr "subpath" grammar.source then
"source/${grammar.source.subpath}"
else
"source";
dontUnpack = true;
dontConfigure = true;
FLAGS = [
"-I${src}/src"
"-Isrc"
"-g"
"-O3"
"-fPIC"
@@ -76,13 +87,13 @@
buildPhase = ''
runHook preBuild
if [[ -e "$src/src/scanner.cc" ]]; then
$CXX -c "$src/src/scanner.cc" -o scanner.o $FLAGS
elif [[ -e "$src/src/scanner.c" ]]; then
$CC -c "$src/src/scanner.c" -o scanner.o $FLAGS
if [[ -e src/scanner.cc ]]; then
$CXX -c src/scanner.cc -o scanner.o $FLAGS
elif [[ -e src/scanner.c ]]; then
$CC -c src/scanner.c -o scanner.o $FLAGS
fi
$CC -c "$src/src/parser.c" -o parser.o $FLAGS
$CC -c src/parser.c -o parser.o $FLAGS
$CXX -shared -o $NAME.so *.o
ls -al
@@ -105,15 +116,17 @@
'';
};
grammarsToBuild = builtins.filter includeGrammarIf gitGrammars;
builtGrammars =
builtins.map (grammar: {
inherit (grammar) name;
artifact = buildGrammar grammar;
})
grammarsToBuild;
grammarLinks =
builtins.map (grammar: "ln -s ${grammar.artifact}/${grammar.name}.so $out/${grammar.name}.so")
builtGrammars;
builtGrammars = builtins.map (grammar: {
inherit (grammar) name;
value = buildGrammar grammar;
}) grammarsToBuild;
extensibleGrammars =
lib.makeExtensible (self: builtins.listToAttrs builtGrammars);
overlayedGrammars = lib.pipe extensibleGrammars
(builtins.map (overlay: grammar: grammar.extend overlay) grammarOverlays);
grammarLinks = lib.mapAttrsToList
(name: artifact: "ln -s ${artifact}/${name}.so $out/${name}.so")
(lib.filterAttrs (n: v: lib.isDerivation v) overlayedGrammars);
in
runCommand "consolidated-helix-grammars" {} ''
mkdir -p $out

View File

@@ -1,43 +1,44 @@
[package]
name = "helix-core"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
description = "Helix editor core editing primitives"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
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]
unicode-lines = ["ropey/unicode_lines"]
integration = []
[dependencies]
helix-loader = { version = "0.6", path = "../helix-loader" }
helix-stdx = { path = "../helix-stdx" }
helix-loader = { path = "../helix-loader" }
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
smallvec = "1.11"
smallvec = "1.13"
smartstring = "1.0.1"
unicode-segmentation = "1.10"
unicode-segmentation = "1.11"
unicode-width = "0.1"
unicode-general-category = "0.6"
# slab = "0.4.2"
slotmap = "1.0"
slotmap.workspace = true
tree-sitter.workspace = true
once_cell = "1.18"
once_cell = "1.19"
arc-swap = "1"
regex = "1"
bitflags = "2.4"
ahash = "0.8.5"
hashbrown = { version = "0.14.2", features = ["raw"] }
bitflags = "2.5"
ahash = "0.8.11"
hashbrown = { version = "0.14.3", features = ["raw"] }
dunce = "1.0"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.7"
toml = "0.8"
imara-diff = "0.1.0"
@@ -46,11 +47,12 @@ encoding_rs = "0.8"
chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] }
etcetera = "0.8"
textwrap = "0.16.0"
textwrap = "0.16.1"
nucleo.workspace = true
parking_lot = "0.12"
globset = "0.4.14"
[dev-dependencies]
quickcheck = { version = "1", default-features = false }
indoc = "2.0.4"
indoc = "2.0.5"

View File

@@ -1,9 +1,12 @@
//! This module contains the functionality toggle comments on lines over the selection
//! using the comment character defined in the user's `languages.toml`
use smallvec::SmallVec;
use crate::{
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
syntax::BlockCommentToken, Change, Range, Rope, RopeSlice, Selection, Tendril, Transaction,
};
use helix_stdx::rope::RopeSliceExt;
use std::borrow::Cow;
/// Given text, a comment token, and a set of line indices, returns the following:
@@ -22,12 +25,12 @@ fn find_line_comment(
) -> (bool, Vec<usize>, usize, usize) {
let mut commented = true;
let mut to_change = Vec::new();
let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
let mut min = usize::MAX; // minimum col for first_non_whitespace_char
let mut margin = 1;
let token_len = token.chars().count();
for line in lines {
let line_slice = text.line(line);
if let Some(pos) = find_first_non_whitespace_char(line_slice) {
if let Some(pos) = line_slice.first_non_whitespace_char() {
let len = line_slice.len_chars();
if pos < min {
@@ -94,6 +97,222 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
Transaction::change(doc, changes.into_iter())
}
#[derive(Debug, PartialEq, Eq)]
pub enum CommentChange {
Commented {
range: Range,
start_pos: usize,
end_pos: usize,
start_margin: bool,
end_margin: bool,
start_token: String,
end_token: String,
},
Uncommented {
range: Range,
start_pos: usize,
end_pos: usize,
start_token: String,
end_token: String,
},
Whitespace {
range: Range,
},
}
pub fn find_block_comments(
tokens: &[BlockCommentToken],
text: RopeSlice,
selection: &Selection,
) -> (bool, Vec<CommentChange>) {
let mut commented = true;
let mut only_whitespace = true;
let mut comment_changes = Vec::with_capacity(selection.len());
let default_tokens = tokens.first().cloned().unwrap_or_default();
// TODO: check if this can be removed on MSRV bump
#[allow(clippy::redundant_clone)]
let mut start_token = default_tokens.start.clone();
#[allow(clippy::redundant_clone)]
let mut end_token = default_tokens.end.clone();
let mut tokens = tokens.to_vec();
// sort the tokens by length, so longer tokens will match first
tokens.sort_by(|a, b| {
if a.start.len() == b.start.len() {
b.end.len().cmp(&a.end.len())
} else {
b.start.len().cmp(&a.start.len())
}
});
for range in selection {
let selection_slice = range.slice(text);
if let (Some(start_pos), Some(end_pos)) = (
selection_slice.first_non_whitespace_char(),
selection_slice.last_non_whitespace_char(),
) {
let mut line_commented = false;
let mut after_start = 0;
let mut before_end = 0;
let len = (end_pos + 1) - start_pos;
for BlockCommentToken { start, end } in &tokens {
let start_len = start.chars().count();
let end_len = end.chars().count();
after_start = start_pos + start_len;
before_end = end_pos.saturating_sub(end_len);
if len >= start_len + end_len {
let start_fragment = selection_slice.slice(start_pos..after_start);
let end_fragment = selection_slice.slice(before_end + 1..end_pos + 1);
// block commented with these tokens
if start_fragment == start.as_str() && end_fragment == end.as_str() {
start_token = start.to_string();
end_token = end.to_string();
line_commented = true;
break;
}
}
}
if !line_commented {
comment_changes.push(CommentChange::Uncommented {
range: *range,
start_pos,
end_pos,
start_token: default_tokens.start.clone(),
end_token: default_tokens.end.clone(),
});
commented = false;
} else {
comment_changes.push(CommentChange::Commented {
range: *range,
start_pos,
end_pos,
start_margin: selection_slice
.get_char(after_start)
.map_or(false, |c| c == ' '),
end_margin: after_start != before_end
&& selection_slice
.get_char(before_end)
.map_or(false, |c| c == ' '),
start_token: start_token.to_string(),
end_token: end_token.to_string(),
});
}
only_whitespace = false;
} else {
comment_changes.push(CommentChange::Whitespace { range: *range });
}
}
if only_whitespace {
commented = false;
}
(commented, comment_changes)
}
#[must_use]
pub fn create_block_comment_transaction(
doc: &Rope,
selection: &Selection,
commented: bool,
comment_changes: Vec<CommentChange>,
) -> (Transaction, SmallVec<[Range; 1]>) {
let mut changes: Vec<Change> = Vec::with_capacity(selection.len() * 2);
let mut ranges: SmallVec<[Range; 1]> = SmallVec::with_capacity(selection.len());
let mut offs = 0;
for change in comment_changes {
if commented {
if let CommentChange::Commented {
range,
start_pos,
end_pos,
start_token,
end_token,
start_margin,
end_margin,
} = change
{
let from = range.from();
changes.push((
from + start_pos,
from + start_pos + start_token.len() + start_margin as usize,
None,
));
changes.push((
from + end_pos - end_token.len() - end_margin as usize + 1,
from + end_pos + 1,
None,
));
}
} else {
// uncommented so manually map ranges through changes
match change {
CommentChange::Uncommented {
range,
start_pos,
end_pos,
start_token,
end_token,
} => {
let from = range.from();
changes.push((
from + start_pos,
from + start_pos,
Some(Tendril::from(format!("{} ", start_token))),
));
changes.push((
from + end_pos + 1,
from + end_pos + 1,
Some(Tendril::from(format!(" {}", end_token))),
));
let offset = start_token.chars().count() + end_token.chars().count() + 2;
ranges.push(
Range::new(from + offs, from + offs + end_pos + 1 + offset)
.with_direction(range.direction()),
);
offs += offset;
}
CommentChange::Commented { range, .. } | CommentChange::Whitespace { range } => {
ranges.push(Range::new(range.from() + offs, range.to() + offs));
}
}
}
}
(Transaction::change(doc, changes.into_iter()), ranges)
}
#[must_use]
pub fn toggle_block_comments(
doc: &Rope,
selection: &Selection,
tokens: &[BlockCommentToken],
) -> Transaction {
let text = doc.slice(..);
let (commented, comment_changes) = find_block_comments(tokens, text, selection);
let (mut transaction, ranges) =
create_block_comment_transaction(doc, selection, commented, comment_changes);
if !commented {
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
}
transaction
}
pub fn split_lines_of_selection(text: RopeSlice, selection: &Selection) -> Selection {
let mut ranges = SmallVec::new();
for range in selection.ranges() {
let (line_start, line_end) = range.line_range(text.slice(..));
let mut pos = text.line_to_char(line_start);
for line in text.slice(pos..text.line_to_char(line_end + 1)).lines() {
let start = pos;
pos += line.len_chars();
ranges.push(Range::new(start, pos));
}
}
Selection::new(ranges, 0)
}
#[cfg(test)]
mod test {
use super::*;
@@ -149,4 +368,49 @@ mod test {
// TODO: account for uncommenting with uneven comment indentation
}
#[test]
fn test_find_block_comments() {
// three lines 5 characters.
let mut doc = Rope::from("1\n2\n3");
// select whole document
let selection = Selection::single(0, doc.len_chars());
let text = doc.slice(..);
let res = find_block_comments(&[BlockCommentToken::default()], text, &selection);
assert_eq!(
res,
(
false,
vec![CommentChange::Uncommented {
range: Range::new(0, 5),
start_pos: 0,
end_pos: 4,
start_token: "/*".to_string(),
end_token: "*/".to_string(),
}]
)
);
// comment
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
transaction.apply(&mut doc);
assert_eq!(doc, "/* 1\n2\n3 */");
// uncomment
let selection = Selection::single(0, doc.len_chars());
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
transaction.apply(&mut doc);
assert_eq!(doc, "1\n2\n3");
// don't panic when there is just a space in comment
doc = Rope::from("/* */");
let selection = Selection::single(0, doc.len_chars());
let transaction = toggle_block_comments(&doc, &selection, &[BlockCommentToken::default()]);
transaction.apply(&mut doc);
assert_eq!(doc, "");
}
}

View File

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

View File

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

View File

@@ -116,7 +116,7 @@ impl Default for TextFormat {
#[derive(Debug)]
pub struct DocumentFormatter<'t> {
text_fmt: &'t TextFormat,
annotations: &'t TextAnnotations,
annotations: &'t TextAnnotations<'t>,
/// The visual position at the end of the last yielded word boundary
visual_pos: Position,

View File

@@ -1,5 +1,3 @@
use std::rc::Rc;
use crate::doc_formatter::{DocumentFormatter, TextFormat};
use crate::text_annotations::{InlineAnnotation, Overlay, TextAnnotations};
@@ -105,7 +103,7 @@ fn overlay_text(text: &str, char_pos: usize, softwrap: bool, overlays: &[Overlay
DocumentFormatter::new_at_prev_checkpoint(
text.into(),
&TextFormat::new_test(softwrap),
TextAnnotations::default().add_overlay(overlays.into(), None),
TextAnnotations::default().add_overlay(overlays, None),
char_pos,
)
.0
@@ -142,7 +140,7 @@ fn annotate_text(text: &str, softwrap: bool, annotations: &[InlineAnnotation]) -
DocumentFormatter::new_at_prev_checkpoint(
text.into(),
&TextFormat::new_test(softwrap),
TextAnnotations::default().add_inline_annotations(annotations.into(), None),
TextAnnotations::default().add_inline_annotations(annotations, None),
0,
)
.0
@@ -164,15 +162,24 @@ fn annotation() {
"foo foo foo foo \n.foo foo foo foo \n.foo foo foo "
);
}
#[test]
fn annotation_and_overlay() {
let annotations = [InlineAnnotation {
char_idx: 0,
text: "fooo".into(),
}];
let overlay = [Overlay {
char_idx: 0,
grapheme: "\t".into(),
}];
assert_eq!(
DocumentFormatter::new_at_prev_checkpoint(
"bbar".into(),
&TextFormat::new_test(false),
TextAnnotations::default()
.add_inline_annotations(Rc::new([InlineAnnotation::new(0, "fooo")]), None)
.add_overlay(Rc::new([Overlay::new(0, "\t")]), None),
.add_inline_annotations(annotations.as_slice(), None)
.add_overlay(overlay.as_slice(), None),
0,
)
.0

View File

@@ -278,23 +278,6 @@ pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize
}
}
/// Returns the passed byte index if it's already a grapheme boundary,
/// or the next grapheme boundary byte index if not.
#[must_use]
#[inline]
pub fn ensure_grapheme_boundary_next_byte(slice: RopeSlice, byte_idx: usize) -> usize {
if byte_idx == 0 {
byte_idx
} else {
// TODO: optimize so we're not constructing grapheme cursor twice
if is_grapheme_boundary_byte(slice, byte_idx) {
byte_idx
} else {
next_grapheme_boundary_byte(slice, byte_idx)
}
}
}
/// Returns whether the given char position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
@@ -425,6 +408,85 @@ impl<'a> Iterator for RopeGraphemes<'a> {
}
}
/// An iterator over the graphemes of a `RopeSlice` in reverse.
#[derive(Clone)]
pub struct RevRopeGraphemes<'a> {
text: RopeSlice<'a>,
chunks: Chunks<'a>,
cur_chunk: &'a str,
cur_chunk_start: usize,
cursor: GraphemeCursor,
}
impl<'a> fmt::Debug for RevRopeGraphemes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RevRopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
// .field("cursor", &self.cursor)
.finish()
}
}
impl<'a> RevRopeGraphemes<'a> {
#[must_use]
pub fn new(slice: RopeSlice) -> RevRopeGraphemes {
let (mut chunks, mut cur_chunk_start, _, _) = slice.chunks_at_byte(slice.len_bytes());
chunks.reverse();
let first_chunk = chunks.next().unwrap_or("");
cur_chunk_start -= first_chunk.len();
RevRopeGraphemes {
text: slice,
chunks,
cur_chunk: first_chunk,
cur_chunk_start,
cursor: GraphemeCursor::new(slice.len_bytes(), slice.len_bytes(), true),
}
}
}
impl<'a> Iterator for RevRopeGraphemes<'a> {
type Item = RopeSlice<'a>;
fn next(&mut self) -> Option<RopeSlice<'a>> {
let a = self.cursor.cur_cursor();
let b;
loop {
match self
.cursor
.prev_boundary(self.cur_chunk, self.cur_chunk_start)
{
Ok(None) => {
return None;
}
Ok(Some(n)) => {
b = n;
break;
}
Err(GraphemeIncomplete::PrevChunk) => {
self.cur_chunk = self.chunks.next().unwrap_or("");
self.cur_chunk_start -= self.cur_chunk.len();
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}
if a >= self.cur_chunk_start + self.cur_chunk.len() {
Some(self.text.byte_slice(b..a))
} else {
let a2 = a - self.cur_chunk_start;
let b2 = b - self.cur_chunk_start;
Some((&self.cur_chunk[b2..a2]).into())
}
}
}
/// A highly compressed Cow<'a, str> that holds
/// atmost u31::MAX bytes and is readonly
pub struct GraphemeStr<'a> {

View File

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

View File

@@ -1,13 +1,14 @@
use std::{borrow::Cow, collections::HashMap};
use helix_stdx::rope::RopeSliceExt;
use tree_sitter::{Query, QueryCursor, QueryPredicateArg};
use crate::{
chars::{char_is_line_ending, char_is_whitespace},
graphemes::{grapheme_width, tab_width_at},
syntax::{LanguageConfiguration, RopeProvider, Syntax},
syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node,
Rope, RopeGraphemes, RopeSlice,
Position, Rope, RopeGraphemes, RopeSlice,
};
/// Enum representing indentation style.
@@ -21,7 +22,7 @@ pub enum IndentStyle {
// 16 spaces
const INDENTS: &str = " ";
const MAX_INDENT: u8 = 16;
pub const MAX_INDENT: u8 = 16;
impl IndentStyle {
/// Creates an `IndentStyle` from an indentation string.
@@ -196,41 +197,68 @@ pub fn indent_level_for_line(line: RopeSlice, tab_width: usize, indent_width: us
len / indent_width
}
/// Computes for node and all ancestors whether they are the first node on their line.
/// The first entry in the return value represents the root node, the last one the node itself
fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> {
let mut first_in_line = Vec::new();
loop {
if let Some(prev) = node.prev_sibling() {
// If we insert a new line, the first node at/after the cursor is considered to be the first in its line
let first = prev.end_position().row != node.start_position().row
|| new_line_byte_pos.map_or(false, |byte_pos| {
node.start_byte() >= byte_pos && prev.start_byte() < byte_pos
});
first_in_line.push(Some(first));
/// Create a string of tabs & spaces that has the same visual width as the given RopeSlice (independent of the tab width).
fn whitespace_with_same_width(text: RopeSlice) -> String {
let mut s = String::new();
for grapheme in RopeGraphemes::new(text) {
if grapheme == "\t" {
s.push('\t');
} else {
// Nodes that have no previous siblings are first in their line if and only if their parent is
// (which we don't know yet)
first_in_line.push(None);
}
if let Some(parent) = node.parent() {
node = parent;
} else {
break;
s.extend(std::iter::repeat(' ').take(grapheme_width(&Cow::from(grapheme))));
}
}
s
}
let mut result = Vec::with_capacity(first_in_line.len());
let mut parent_is_first = true; // The root node is by definition the first node in its line
for first in first_in_line.into_iter().rev() {
if let Some(first) = first {
result.push(first);
parent_is_first = first;
} else {
result.push(parent_is_first);
fn add_indent_level(
mut base_indent: String,
added_indent_level: isize,
indent_style: &IndentStyle,
tab_width: usize,
) -> String {
if added_indent_level >= 0 {
// Adding a non-negative indent is easy, we can simply append the indent string
base_indent.push_str(&indent_style.as_str().repeat(added_indent_level as usize));
base_indent
} else {
// In this case, we want to return a prefix of `base_indent`.
// Since the width of a tab depends on its offset, we cannot simply iterate over
// the chars of `base_indent` in reverse until we have the desired indent reduction,
// instead we iterate over them twice in forward direction.
let base_indent_rope = RopeSlice::from(base_indent.as_str());
#[allow(deprecated)]
let base_indent_width =
crate::visual_coords_at_pos(base_indent_rope, base_indent_rope.len_chars(), tab_width)
.col;
let target_indent_width = base_indent_width
.saturating_sub((-added_indent_level) as usize * indent_style.indent_width(tab_width));
#[allow(deprecated)]
let char_end_idx = crate::pos_at_visual_coords(
base_indent_rope,
Position {
row: 0,
col: target_indent_width,
},
tab_width,
);
let byte_end_idx = base_indent_rope.char_to_byte(char_end_idx);
base_indent.truncate(byte_end_idx);
base_indent
}
}
/// Return true if only whitespace comes before the node on its line.
/// If given, new_line_byte_pos is treated the same way as any existing newline.
fn is_first_in_line(node: Node, text: RopeSlice, new_line_byte_pos: Option<usize>) -> bool {
let mut line_start_byte_pos = text.line_to_byte(node.start_position().row);
if let Some(pos) = new_line_byte_pos {
if line_start_byte_pos < pos && pos <= node.start_byte() {
line_start_byte_pos = pos;
}
}
result
text.byte_slice(line_start_byte_pos..node.start_byte())
.chars()
.all(|c| c.is_whitespace())
}
/// The total indent for some line of code.
@@ -241,21 +269,21 @@ fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bo
/// - max(0, indent - outdent) tabs, if tabs are used for indentation
/// - max(0, indent - outdent)*indent_width spaces, if spaces are used for indentation
#[derive(Default, Debug, PartialEq, Eq, Clone)]
pub struct Indentation {
pub struct Indentation<'a> {
indent: usize,
indent_always: usize,
outdent: usize,
outdent_always: usize,
/// The alignment, as a string containing only tabs & spaces. Storing this as a string instead of e.g.
/// the (visual) width ensures that the alignment is preserved even if the tab width changes.
align: Option<String>,
align: Option<RopeSlice<'a>>,
}
impl Indentation {
impl<'a> Indentation<'a> {
/// Add some other [Indentation] to this.
/// The added indent should be the total added indent from one line.
/// Indent should always be added starting from the bottom (or equivalently, the innermost tree-sitter node).
fn add_line(&mut self, added: Indentation) {
fn add_line(&mut self, added: Indentation<'a>) {
// Align overrides the indent from outer scopes.
if self.align.is_some() {
return;
@@ -271,8 +299,10 @@ impl Indentation {
}
/// Add an indent capture to this indent.
/// All the captures that are added in this way should be on the same line.
fn add_capture(&mut self, added: IndentCaptureType) {
/// Only captures that apply to the same line should be added together in this way (otherwise use `add_line`)
/// and the captures should be added starting from the innermost tree-sitter node (currently this only matters
/// if multiple `@align` patterns occur on the same line).
fn add_capture(&mut self, added: IndentCaptureType<'a>) {
match added {
IndentCaptureType::Indent => {
if self.indent_always == 0 {
@@ -295,47 +325,68 @@ impl Indentation {
self.outdent = 0;
}
IndentCaptureType::Align(align) => {
self.align = Some(align);
if self.align.is_none() {
self.align = Some(align);
}
}
}
}
fn into_string(self, indent_style: &IndentStyle) -> String {
let indent = self.indent_always + self.indent;
let outdent = self.outdent_always + self.outdent;
let indent_level = if indent >= outdent {
indent - outdent
fn net_indent(&self) -> isize {
(self.indent + self.indent_always) as isize
- ((self.outdent + self.outdent_always) as isize)
}
/// Convert `self` into a string, taking into account the computed and actual indentation of some other line.
fn relative_indent(
&self,
other_computed_indent: &Self,
other_leading_whitespace: RopeSlice,
indent_style: &IndentStyle,
tab_width: usize,
) -> Option<String> {
if self.align == other_computed_indent.align {
// If self and baseline are either not aligned to anything or both aligned the same way,
// we can simply take `other_leading_whitespace` and add some indent / outdent to it (in the second
// case, the alignment should already be accounted for in `other_leading_whitespace`).
let indent_diff = self.net_indent() - other_computed_indent.net_indent();
Some(add_indent_level(
String::from(other_leading_whitespace),
indent_diff,
indent_style,
tab_width,
))
} else {
log::warn!("Encountered more outdent than indent nodes while calculating indentation: {} outdent, {} indent", self.outdent, self.indent);
0
};
let mut indent_string = if let Some(align) = self.align {
align
} else {
String::new()
};
indent_string.push_str(&indent_style.as_str().repeat(indent_level));
indent_string
// If the alignment of both lines is different, we cannot compare their indentation in any meaningful way
None
}
}
pub fn to_string(&self, indent_style: &IndentStyle, tab_width: usize) -> String {
add_indent_level(
self.align
.map_or_else(String::new, whitespace_with_same_width),
self.net_indent(),
indent_style,
tab_width,
)
}
}
/// An indent definition which corresponds to a capture from the indent query
#[derive(Debug)]
struct IndentCapture {
capture_type: IndentCaptureType,
struct IndentCapture<'a> {
capture_type: IndentCaptureType<'a>,
scope: IndentScope,
}
#[derive(Debug, Clone, PartialEq)]
enum IndentCaptureType {
enum IndentCaptureType<'a> {
Indent,
IndentAlways,
Outdent,
OutdentAlways,
/// Alignment given as a string of whitespace
Align(String),
Align(RopeSlice<'a>),
}
impl IndentCaptureType {
impl<'a> IndentCaptureType<'a> {
fn default_scope(&self) -> IndentScope {
match self {
IndentCaptureType::Indent | IndentCaptureType::IndentAlways => IndentScope::Tail,
@@ -367,8 +418,8 @@ enum ExtendCapture {
/// each node (identified by its ID) the relevant captures (already filtered
/// by predicates).
#[derive(Debug)]
struct IndentQueryResult {
indent_captures: HashMap<usize, Vec<IndentCapture>>,
struct IndentQueryResult<'a> {
indent_captures: HashMap<usize, Vec<IndentCapture<'a>>>,
extend_captures: HashMap<usize, Vec<ExtendCapture>>,
}
@@ -389,14 +440,14 @@ fn get_node_end_line(node: Node, new_line_byte_pos: Option<usize>) -> usize {
node_line
}
fn query_indents(
fn query_indents<'a>(
query: &Query,
syntax: &Syntax,
cursor: &mut QueryCursor,
text: RopeSlice,
text: RopeSlice<'a>,
range: std::ops::Range<usize>,
new_line_byte_pos: Option<usize>,
) -> IndentQueryResult {
) -> IndentQueryResult<'a> {
let mut indent_captures: HashMap<usize, Vec<IndentCapture>> = HashMap::new();
let mut extend_captures: HashMap<usize, Vec<ExtendCapture>> = HashMap::new();
cursor.set_byte_range(range);
@@ -477,14 +528,14 @@ fn query_indents(
// The row/column position of the optional anchor in this query
let mut anchor: Option<tree_sitter::Node> = None;
for capture in m.captures {
let capture_name = query.capture_names()[capture.index as usize].as_str();
let capture_name = query.capture_names()[capture.index as usize];
let capture_type = match capture_name {
"indent" => IndentCaptureType::Indent,
"indent.always" => IndentCaptureType::IndentAlways,
"outdent" => IndentCaptureType::Outdent,
"outdent.always" => IndentCaptureType::OutdentAlways,
// The alignment will be updated to the correct value at the end, when the anchor is known.
"align" => IndentCaptureType::Align(String::from("")),
"align" => IndentCaptureType::Align(RopeSlice::from("")),
"anchor" => {
if anchor.is_some() {
log::error!("Invalid indent query: Encountered more than one @anchor in the same match.")
@@ -556,22 +607,10 @@ fn query_indents(
}
Some(anchor) => anchor,
};
// Create a string of tabs & spaces that should have the same width
// as the string that precedes the anchor (independent of the tab width).
let mut align = String::new();
for grapheme in RopeGraphemes::new(
capture.capture_type = IndentCaptureType::Align(
text.line(anchor.start_position().row)
.byte_slice(0..anchor.start_position().column),
) {
if grapheme == "\t" {
align.push('\t');
} else {
align.extend(
std::iter::repeat(' ').take(grapheme_width(&Cow::from(grapheme))),
);
}
}
capture.capture_type = IndentCaptureType::Align(align);
);
}
indent_captures
.entry(node_id)
@@ -657,56 +696,20 @@ fn extend_nodes<'a>(
}
}
/// Use the syntax tree to determine the indentation for a given position.
/// This can be used in 2 ways:
///
/// - To get the correct indentation for an existing line (new_line=false), not necessarily equal to the current indentation.
/// - In this case, pos should be inside the first tree-sitter node on that line.
/// In most cases, this can just be the first non-whitespace on that line.
/// - To get the indentation for a new line (new_line=true). This behaves like the first usecase if the part of the current line
/// after pos were moved to a new line.
///
/// The indentation is determined by traversing all the tree-sitter nodes containing the position.
/// Each of these nodes produces some [Indentation] for:
///
/// - The line of the (beginning of the) node. This is defined by the scope `all` if this is the first node on its line.
/// - The line after the node. This is defined by:
/// - The scope `tail`.
/// - The scope `all` if this node is not the first node on its line.
/// Intuitively, `all` applies to everything contained in this node while `tail` applies to everything except for the first line of the node.
/// The indents from different nodes for the same line are then combined.
/// The result [Indentation] is simply the sum of the [Indentation] for all lines.
///
/// Specifying which line exactly an [Indentation] applies to is important because indents on the same line combine differently than indents on different lines:
/// ```ignore
/// some_function(|| {
/// // Both the function parameters as well as the contained block should be indented.
/// // Because they are on the same line, this only yields one indent level
/// });
/// ```
///
/// ```ignore
/// some_function(
/// param1,
/// || {
/// // Here we get 2 indent levels because the 'parameters' and the 'block' node begin on different lines
/// },
/// );
/// ```
/// Prepare an indent query by computing:
/// - The node from which to start the query (this is non-trivial due to `@extend` captures)
/// - The indent captures for all relevant nodes.
#[allow(clippy::too_many_arguments)]
pub fn treesitter_indent_for_pos(
fn init_indent_query<'a, 'b>(
query: &Query,
syntax: &Syntax,
indent_style: &IndentStyle,
syntax: &'a Syntax,
text: RopeSlice<'b>,
tab_width: usize,
indent_width: usize,
text: RopeSlice,
line: usize,
pos: usize,
new_line: bool,
) -> Option<String> {
let byte_pos = text.char_to_byte(pos);
let new_line_byte_pos = new_line.then_some(byte_pos);
byte_pos: usize,
new_line_byte_pos: Option<usize>,
) -> Option<(Node<'a>, HashMap<usize, Vec<IndentCapture<'b>>>)> {
// The innermost tree-sitter node which is considered for the indent
// computation. It may change if some predeceding node is extended
let mut node = syntax
@@ -750,7 +753,6 @@ pub fn treesitter_indent_for_pos(
(query_result, deepest_preceding)
})
};
let mut indent_captures = query_result.indent_captures;
let extend_captures = query_result.extend_captures;
// Check for extend captures, potentially changing the node that the indent calculation starts with
@@ -765,7 +767,68 @@ pub fn treesitter_indent_for_pos(
indent_width,
);
}
let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
Some((node, query_result.indent_captures))
}
/// Use the syntax tree to determine the indentation for a given position.
/// This can be used in 2 ways:
///
/// - To get the correct indentation for an existing line (new_line=false), not necessarily equal to the current indentation.
/// - In this case, pos should be inside the first tree-sitter node on that line.
/// In most cases, this can just be the first non-whitespace on that line.
/// - To get the indentation for a new line (new_line=true). This behaves like the first usecase if the part of the current line
/// after pos were moved to a new line.
///
/// The indentation is determined by traversing all the tree-sitter nodes containing the position.
/// Each of these nodes produces some [Indentation] for:
///
/// - The line of the (beginning of the) node. This is defined by the scope `all` if this is the first node on its line.
/// - The line after the node. This is defined by:
/// - The scope `tail`.
/// - The scope `all` if this node is not the first node on its line.
/// Intuitively, `all` applies to everything contained in this node while `tail` applies to everything except for the first line of the node.
/// The indents from different nodes for the same line are then combined.
/// The result [Indentation] is simply the sum of the [Indentation] for all lines.
///
/// Specifying which line exactly an [Indentation] applies to is important because indents on the same line combine differently than indents on different lines:
/// ```ignore
/// some_function(|| {
/// // Both the function parameters as well as the contained block should be indented.
/// // Because they are on the same line, this only yields one indent level
/// });
/// ```
///
/// ```ignore
/// some_function(
/// param1,
/// || {
/// // Here we get 2 indent levels because the 'parameters' and the 'block' node begin on different lines
/// },
/// );
/// ```
#[allow(clippy::too_many_arguments)]
pub fn treesitter_indent_for_pos<'a>(
query: &Query,
syntax: &Syntax,
tab_width: usize,
indent_width: usize,
text: RopeSlice<'a>,
line: usize,
pos: usize,
new_line: bool,
) -> Option<Indentation<'a>> {
let byte_pos = text.char_to_byte(pos);
let new_line_byte_pos = new_line.then_some(byte_pos);
let (mut node, mut indent_captures) = init_indent_query(
query,
syntax,
text,
tab_width,
indent_width,
line,
byte_pos,
new_line_byte_pos,
)?;
let mut result = Indentation::default();
// We always keep track of all the indent changes on one line, in order to only indent once
@@ -774,9 +837,7 @@ pub fn treesitter_indent_for_pos(
let mut indent_for_line_below = Indentation::default();
loop {
// This can safely be unwrapped because `first_in_line` contains
// one entry for each ancestor of the node (which is what we iterate over)
let is_first = *first_in_line.last().unwrap();
let is_first = is_first_in_line(node, text, new_line_byte_pos);
// Apply all indent definitions for this node.
// Since we only iterate over each node once, we can remove the
@@ -819,7 +880,6 @@ pub fn treesitter_indent_for_pos(
}
node = parent;
first_in_line.pop();
} else {
// Only add the indentation for the line below if that line
// is not after the line that the indentation is calculated for.
@@ -832,7 +892,7 @@ pub fn treesitter_indent_for_pos(
break;
}
}
Some(result.into_string(indent_style))
Some(result)
}
/// Returns the indentation for a new line.
@@ -841,6 +901,7 @@ pub fn treesitter_indent_for_pos(
pub fn indent_for_newline(
language_config: Option<&LanguageConfiguration>,
syntax: Option<&Syntax>,
indent_heuristic: &IndentationHeuristic,
indent_style: &IndentStyle,
tab_width: usize,
text: RopeSlice,
@@ -849,14 +910,18 @@ pub fn indent_for_newline(
current_line: usize,
) -> String {
let indent_width = indent_style.indent_width(tab_width);
if let (Some(query), Some(syntax)) = (
if let (
IndentationHeuristic::TreeSitter | IndentationHeuristic::Hybrid,
Some(query),
Some(syntax),
) = (
indent_heuristic,
language_config.and_then(|config| config.indent_query()),
syntax,
) {
if let Some(indent) = treesitter_indent_for_pos(
query,
syntax,
indent_style,
tab_width,
indent_width,
text,
@@ -864,9 +929,57 @@ pub fn indent_for_newline(
line_before_end_pos,
true,
) {
return indent;
if *indent_heuristic == IndentationHeuristic::Hybrid {
// We want to compute the indentation not only based on the
// syntax tree but also on the actual indentation of a previous
// line. This makes indentation computation more resilient to
// incomplete queries, incomplete source code & differing indentation
// styles for the same language.
// However, using the indent of a previous line as a baseline may not
// make sense, e.g. if it has a different alignment than the new line.
// In order to prevent edge cases with long running times, we only try
// a constant number of (non-empty) lines.
const MAX_ATTEMPTS: usize = 4;
let mut num_attempts = 0;
for line_idx in (0..=line_before).rev() {
let line = text.line(line_idx);
let first_non_whitespace_char = match line.first_non_whitespace_char() {
Some(i) => i,
None => {
continue;
}
};
if let Some(indent) = (|| {
let computed_indent = treesitter_indent_for_pos(
query,
syntax,
tab_width,
indent_width,
text,
line_idx,
text.line_to_char(line_idx) + first_non_whitespace_char,
false,
)?;
let leading_whitespace = line.slice(0..first_non_whitespace_char);
indent.relative_indent(
&computed_indent,
leading_whitespace,
indent_style,
tab_width,
)
})() {
return indent;
}
num_attempts += 1;
if num_attempts == MAX_ATTEMPTS {
break;
}
}
}
return indent.to_string(indent_style, tab_width);
};
}
// Fallback in case we either don't have indent queries or they failed for some reason
let indent_level = indent_level_for_line(text.line(current_line), tab_width, indent_width);
indent_style.as_str().repeat(indent_level)
}
@@ -958,10 +1071,13 @@ mod test {
..Default::default()
};
let add_capture = |mut indent: Indentation, capture| {
fn add_capture<'a>(
mut indent: Indentation<'a>,
capture: IndentCaptureType<'a>,
) -> Indentation<'a> {
indent.add_capture(capture);
indent
};
}
// adding an indent to no indent makes an indent
assert_eq!(
@@ -1056,4 +1172,74 @@ mod test {
)
);
}
#[test]
fn test_relative_indent() {
let indent_style = IndentStyle::Spaces(4);
let tab_width: usize = 4;
let no_align = [
Indentation::default(),
Indentation {
indent: 1,
..Default::default()
},
Indentation {
indent: 5,
outdent: 1,
..Default::default()
},
];
let align = no_align.clone().map(|indent| Indentation {
align: Some(RopeSlice::from("12345")),
..indent
});
let different_align = Indentation {
align: Some(RopeSlice::from("123456")),
..Default::default()
};
// Check that relative and absolute indentation computation are the same when the line we compare to is
// indented as we expect.
let check_consistency = |indent: &Indentation, other: &Indentation| {
assert_eq!(
indent.relative_indent(
other,
RopeSlice::from(other.to_string(&indent_style, tab_width).as_str()),
&indent_style,
tab_width
),
Some(indent.to_string(&indent_style, tab_width))
);
};
for a in &no_align {
for b in &no_align {
check_consistency(a, b);
}
}
for a in &align {
for b in &align {
check_consistency(a, b);
}
}
// Relative indent computation makes no sense if the alignment differs
assert_eq!(
align[0].relative_indent(
&no_align[0],
RopeSlice::from(" "),
&indent_style,
tab_width
),
None
);
assert_eq!(
align[0].relative_indent(
&different_align,
RopeSlice::from(" "),
&indent_style,
tab_width
),
None
);
}
}

View File

@@ -17,7 +17,6 @@ pub mod macros;
pub mod match_brackets;
pub mod movement;
pub mod object;
pub mod path;
mod position;
pub mod search;
pub mod selection;
@@ -38,9 +37,6 @@ pub mod unicode {
pub use helix_loader::find_workspace;
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}
mod rope_reader;
pub use rope_reader::RopeReader;

View File

@@ -57,10 +57,10 @@ fn find_pair(
pos_: usize,
traverse_parents: bool,
) -> Option<usize> {
let tree = syntax.tree();
let pos = doc.char_to_byte(pos_);
let mut node = tree.root_node().descendant_for_byte_range(pos, pos)?;
let root = syntax.tree_for_byte_range(pos, pos + 1).root_node();
let mut node = root.descendant_for_byte_range(pos, pos + 1)?;
loop {
if node.is_named() {
@@ -118,7 +118,7 @@ fn find_pair(
};
node = parent;
}
let node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
let node = root.named_descendant_for_byte_range(pos, pos + 1)?;
if node.child_count() != 0 {
return None;
}
@@ -141,7 +141,7 @@ fn find_pair(
#[must_use]
pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> {
// Don't do anything when the cursor is not on top of a bracket.
let bracket = doc.char(cursor_pos);
let bracket = doc.get_char(cursor_pos)?;
if !is_valid_bracket(bracket) {
return None;
}
@@ -265,6 +265,12 @@ fn as_char(doc: RopeSlice, node: &Node) -> Option<(usize, char)> {
mod tests {
use super::*;
#[test]
fn find_matching_bracket_empty_file() {
let actual = find_matching_bracket_plaintext("".into(), 0);
assert_eq!(actual, None);
}
#[test]
fn test_find_matching_bracket_current_line_plaintext() {
let assert = |input: &str, pos, expected| {

View File

@@ -573,16 +573,11 @@ pub fn move_parent_node_end(
dir: Direction,
movement: Movement,
) -> Selection {
let tree = syntax.tree();
selection.transform(|range| {
let start_from = text.char_to_byte(range.from());
let start_to = text.char_to_byte(range.to());
let mut node = match tree
.root_node()
.named_descendant_for_byte_range(start_from, start_to)
{
let mut node = match syntax.named_descendant_for_byte_range(start_from, start_to) {
Some(node) => node,
None => {
log::debug!(

View File

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

View File

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

View File

@@ -7,11 +7,14 @@ use crate::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary,
},
line_ending::get_line_ending,
movement::Direction,
Assoc, ChangeSet, RopeGraphemes, RopeSlice,
};
use helix_stdx::rope::{self, RopeSliceExt};
use smallvec::{smallvec, SmallVec};
use std::borrow::Cow;
use tree_sitter::Node;
/// A single selection range.
///
@@ -71,6 +74,12 @@ impl Range {
Self::new(head, head)
}
pub fn from_node(node: Node, text: RopeSlice, direction: Direction) -> Self {
let from = text.byte_to_char(node.start_byte());
let to = text.byte_to_char(node.end_byte());
Range::new(from, to).with_direction(direction)
}
/// Start of the range.
#[inline]
#[must_use]
@@ -374,6 +383,12 @@ impl Range {
let second = graphemes.next();
first.is_some() && second.is_none()
}
/// Converts this char range into an in order byte range, discarding
/// direction.
pub fn into_byte_range(&self, text: RopeSlice) -> (usize, usize) {
(text.char_to_byte(self.from()), text.char_to_byte(self.to()))
}
}
impl From<(usize, usize)> for Range {
@@ -703,17 +718,26 @@ impl IntoIterator for Selection {
}
}
impl From<Range> for Selection {
fn from(range: Range) -> Self {
Self {
ranges: smallvec![range],
primary_index: 0,
}
}
}
// TODO: checkSelection -> check if valid for doc length && sorted
pub fn keep_or_remove_matches(
text: RopeSlice,
selection: &Selection,
regex: &crate::regex::Regex,
regex: &rope::Regex,
remove: bool,
) -> Option<Selection> {
let result: SmallVec<_> = selection
.iter()
.filter(|range| regex.is_match(&range.fragment(text)) ^ remove)
.filter(|range| regex.is_match(text.regex_input_at(range.from()..range.to())) ^ remove)
.copied()
.collect();
@@ -724,25 +748,20 @@ pub fn keep_or_remove_matches(
None
}
// TODO: support to split on capture #N instead of whole match
pub fn select_on_matches(
text: RopeSlice,
selection: &Selection,
regex: &crate::regex::Regex,
regex: &rope::Regex,
) -> Option<Selection> {
let mut result = SmallVec::with_capacity(selection.len());
for sel in selection {
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
let fragment = sel.fragment(text);
let sel_start = sel.from();
let start_byte = text.char_to_byte(sel_start);
for mat in regex.find_iter(&fragment) {
for mat in regex.find_iter(text.regex_input_at(sel.from()..sel.to())) {
// TODO: retain range direction
let start = text.byte_to_char(start_byte + mat.start());
let end = text.byte_to_char(start_byte + mat.end());
let start = text.byte_to_char(mat.start());
let end = text.byte_to_char(mat.end());
let range = Range::new(start, end);
// Make sure the match is not right outside of the selection.
@@ -761,12 +780,7 @@ pub fn select_on_matches(
None
}
// TODO: support to split on capture #N instead of whole match
pub fn split_on_matches(
text: RopeSlice,
selection: &Selection,
regex: &crate::regex::Regex,
) -> Selection {
pub fn split_on_newline(text: RopeSlice, selection: &Selection) -> Selection {
let mut result = SmallVec::with_capacity(selection.len());
for sel in selection {
@@ -776,21 +790,49 @@ pub fn split_on_matches(
continue;
}
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
let fragment = sel.fragment(text);
let sel_start = sel.from();
let sel_end = sel.to();
let start_byte = text.char_to_byte(sel_start);
let mut start = sel_start;
for mat in regex.find_iter(&fragment) {
for line in sel.slice(text).lines() {
let Some(line_ending) = get_line_ending(&line) else {
break;
};
let line_end = start + line.len_chars();
// TODO: retain range direction
let end = text.byte_to_char(start_byte + mat.start());
result.push(Range::new(start, line_end - line_ending.len_chars()));
start = line_end;
}
if start < sel_end {
result.push(Range::new(start, sel_end));
}
}
// TODO: figure out a new primary index
Selection::new(result, 0)
}
pub fn split_on_matches(text: RopeSlice, selection: &Selection, regex: &rope::Regex) -> Selection {
let mut result = SmallVec::with_capacity(selection.len());
for sel in selection {
// Special case: zero-width selection.
if sel.from() == sel.to() {
result.push(*sel);
continue;
}
let sel_start = sel.from();
let sel_end = sel.to();
let mut start = sel_start;
for mat in regex.find_iter(text.regex_input_at(sel_start..sel_end)) {
// TODO: retain range direction
let end = text.byte_to_char(mat.start());
result.push(Range::new(start, end));
start = text.byte_to_char(start_byte + mat.end());
start = text.byte_to_char(mat.end());
}
if start < sel_end {
@@ -1021,14 +1063,12 @@ mod test {
#[test]
fn test_select_on_matches() {
use crate::regex::{Regex, RegexBuilder};
let r = Rope::from_str("Nobody expects the Spanish inquisition");
let s = r.slice(..);
let selection = Selection::single(0, r.len_chars());
assert_eq!(
select_on_matches(s, &selection, &Regex::new(r"[A-Z][a-z]*").unwrap()),
select_on_matches(s, &selection, &rope::Regex::new(r"[A-Z][a-z]*").unwrap()),
Some(Selection::new(
smallvec![Range::new(0, 6), Range::new(19, 26)],
0
@@ -1038,8 +1078,14 @@ mod test {
let r = Rope::from_str("This\nString\n\ncontains multiple\nlines");
let s = r.slice(..);
let start_of_line = RegexBuilder::new(r"^").multi_line(true).build().unwrap();
let end_of_line = RegexBuilder::new(r"$").multi_line(true).build().unwrap();
let start_of_line = rope::RegexBuilder::new()
.syntax(rope::Config::new().multi_line(true))
.build(r"^")
.unwrap();
let end_of_line = rope::RegexBuilder::new()
.syntax(rope::Config::new().multi_line(true))
.build(r"$")
.unwrap();
// line without ending
assert_eq!(
@@ -1077,9 +1123,9 @@ mod test {
select_on_matches(
s,
&Selection::single(0, s.len_chars()),
&RegexBuilder::new(r"^[a-z ]*$")
.multi_line(true)
.build()
&rope::RegexBuilder::new()
.syntax(rope::Config::new().multi_line(true))
.build(r"^[a-z ]*$")
.unwrap()
),
Some(Selection::new(
@@ -1171,13 +1217,15 @@ mod test {
#[test]
fn test_split_on_matches() {
use crate::regex::Regex;
let text = Rope::from(" abcd efg wrs xyz 123 456");
let selection = Selection::new(smallvec![Range::new(0, 9), Range::new(11, 20),], 0);
let result = split_on_matches(text.slice(..), &selection, &Regex::new(r"\s+").unwrap());
let result = split_on_matches(
text.slice(..),
&selection,
&rope::Regex::new(r"\s+").unwrap(),
);
assert_eq!(
result.ranges(),

View File

@@ -167,6 +167,10 @@ fn find_nth_open_pair(
mut pos: usize,
n: usize,
) -> Option<usize> {
if pos >= text.len_chars() {
return None;
}
let mut chars = text.chars_at(pos + 1);
// Adjusts pos for the first iteration, and handles the case of the
@@ -260,7 +264,8 @@ pub fn get_surround_pos(
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return Err(Error::CursorOverlap);
}
change_pos.extend_from_slice(&[open_pos, close_pos]);
// ensure the positions are always paired in the forward direction
change_pos.extend_from_slice(&[open_pos.min(close_pos), close_pos.max(open_pos)]);
}
Ok(change_pos)
}
@@ -382,6 +387,21 @@ mod test {
)
}
#[test]
fn test_find_nth_closest_pairs_pos_index_range_panic() {
#[rustfmt::skip]
let (doc, selection, _) =
rope_with_selections_and_expectations(
"(a)c)",
"^^^^^"
);
assert_eq!(
find_nth_closest_pairs_pos(doc.slice(..), selection.primary(), 1),
Err(Error::PairNotFound)
)
}
// Create a Rope and a matching Selection using a specification language.
// ^ is a single-point selection.
// _ is an expected index. These are returned as a Vec<usize> for use in assertions.

View File

@@ -1,3 +1,5 @@
mod tree_cursor;
use crate::{
auto_pairs::AutoPairs,
chars::char_is_line_ending,
@@ -10,7 +12,9 @@ use crate::{
use ahash::RandomState;
use arc_swap::{ArcSwap, Guard};
use bitflags::bitflags;
use globset::GlobSet;
use hashbrown::raw::RawTable;
use helix_stdx::rope::{self, RopeSliceExt};
use slotmap::{DefaultKey as LayerId, HopSlotMap};
use std::{
@@ -19,7 +23,7 @@ use std::{
collections::{HashMap, HashSet, VecDeque},
fmt::{self, Display},
hash::{Hash, Hasher},
mem::{replace, transmute},
mem::replace,
path::{Path, PathBuf},
str::FromStr,
sync::Arc,
@@ -30,6 +34,8 @@ use serde::{ser::SerializeSeq, Deserialize, Serialize};
use helix_loader::grammar::{get_language, load_runtime_file};
pub use tree_cursor::TreeCursor;
fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
where
D: serde::Deserializer<'de>,
@@ -82,12 +88,6 @@ pub struct Configuration {
pub language_server: HashMap<String, LanguageServerConfiguration>,
}
impl Default for Configuration {
fn default() -> Self {
crate::config::default_syntax_loader()
}
}
// largely based on tree-sitter/cli/src/loader.rs
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
@@ -101,8 +101,21 @@ pub struct LanguageConfiguration {
pub file_types: Vec<FileType>, // filename extension or ends_with? <Gemfile, rb, etc>
#[serde(default)]
pub shebangs: Vec<String>, // interpreter(s) associated with language
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>,
#[serde(default)]
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
#[serde(
default,
skip_serializing,
deserialize_with = "from_comment_tokens",
alias = "comment-token"
)]
pub comment_tokens: Option<Vec<String>>,
#[serde(
default,
skip_serializing,
deserialize_with = "from_block_comment_tokens"
)]
pub block_comment_tokens: Option<Vec<BlockCommentToken>>,
pub text_width: Option<usize>,
pub soft_wrap: Option<SoftWrap>,
@@ -154,6 +167,8 @@ pub struct LanguageConfiguration {
/// Hardcoded LSP root directories relative to the workspace root, like `examples` or `tools/fuzz`.
/// Falling back to the current working directory if none are configured.
pub workspace_lsp_roots: Option<Vec<PathBuf>>,
#[serde(default)]
pub persistent_diagnostic_sources: Vec<String>,
}
#[derive(Debug, PartialEq, Eq, Hash)]
@@ -161,9 +176,11 @@ pub enum FileType {
/// The extension of the file, either the `Path::extension` or the full
/// filename if the file does not have an extension.
Extension(String),
/// The suffix of a file. This is compared to a given file's absolute
/// path, so it can be used to detect files based on their directories.
Suffix(String),
/// A Unix-style path glob. This is compared to the file's absolute path, so
/// it can be used to detect files based on their directories. If the glob
/// is not an absolute path and does not already start with a glob pattern,
/// a glob pattern will be prepended to it.
Glob(globset::Glob),
}
impl Serialize for FileType {
@@ -175,9 +192,9 @@ impl Serialize for FileType {
match self {
FileType::Extension(extension) => serializer.serialize_str(extension),
FileType::Suffix(suffix) => {
FileType::Glob(glob) => {
let mut map = serializer.serialize_map(Some(1))?;
map.serialize_entry("suffix", &suffix.replace(std::path::MAIN_SEPARATOR, "/"))?;
map.serialize_entry("glob", glob.glob())?;
map.end()
}
}
@@ -210,12 +227,20 @@ impl<'de> Deserialize<'de> for FileType {
M: serde::de::MapAccess<'de>,
{
match map.next_entry::<String, String>()? {
Some((key, suffix)) if key == "suffix" => Ok(FileType::Suffix({
// FIXME: use `suffix.replace('/', std::path::MAIN_SEPARATOR_STR)`
// if MSRV is updated to 1.68
let mut separator = [0; 1];
suffix.replace('/', std::path::MAIN_SEPARATOR.encode_utf8(&mut separator))
})),
Some((key, mut glob)) if key == "glob" => {
// If the glob isn't an absolute path or already starts
// with a glob pattern, add a leading glob so we
// properly match relative paths.
if !glob.starts_with('/') && !glob.starts_with("*/") {
glob.insert_str(0, "*/");
}
globset::Glob::new(glob.as_str())
.map(FileType::Glob)
.map_err(|err| {
serde::de::Error::custom(format!("invalid `glob` pattern: {}", err))
})
}
Some((key, _value)) => Err(serde::de::Error::custom(format!(
"unknown key in `file-types` list: {}",
key
@@ -231,6 +256,59 @@ impl<'de> Deserialize<'de> for FileType {
}
}
fn from_comment_tokens<'de, D>(deserializer: D) -> Result<Option<Vec<String>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum CommentTokens {
Multiple(Vec<String>),
Single(String),
}
Ok(
Option::<CommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
CommentTokens::Single(val) => vec![val],
CommentTokens::Multiple(vals) => vals,
}),
)
}
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct BlockCommentToken {
pub start: String,
pub end: String,
}
impl Default for BlockCommentToken {
fn default() -> Self {
BlockCommentToken {
start: "/*".to_string(),
end: "*/".to_string(),
}
}
}
fn from_block_comment_tokens<'de, D>(
deserializer: D,
) -> Result<Option<Vec<BlockCommentToken>>, D::Error>
where
D: serde::Deserializer<'de>,
{
#[derive(Deserialize)]
#[serde(untagged)]
enum BlockCommentTokens {
Multiple(Vec<BlockCommentToken>),
Single(BlockCommentToken),
}
Ok(
Option::<BlockCommentTokens>::deserialize(deserializer)?.map(|tokens| match tokens {
BlockCommentTokens::Single(val) => vec![val],
BlockCommentTokens::Multiple(vals) => vals,
}),
)
}
#[derive(Clone, Copy, Debug, Serialize, Deserialize, PartialEq, Eq, Hash)]
#[serde(rename_all = "kebab-case")]
pub enum LanguageServerFeature {
@@ -263,7 +341,7 @@ impl Display for LanguageServerFeature {
GotoDeclaration => "goto-declaration",
GotoDefinition => "goto-definition",
GotoTypeDefinition => "goto-type-definition",
GotoReference => "goto-type-definition",
GotoReference => "goto-reference",
GotoImplementation => "goto-implementation",
SignatureHelp => "signature-help",
Hover => "hover",
@@ -358,6 +436,22 @@ where
serializer.end()
}
fn deserialize_required_root_patterns<'de, D>(deserializer: D) -> Result<Option<GlobSet>, D::Error>
where
D: serde::Deserializer<'de>,
{
let patterns = Vec::<String>::deserialize(deserializer)?;
if patterns.is_empty() {
return Ok(None);
}
let mut builder = globset::GlobSetBuilder::new();
for pattern in patterns {
let glob = globset::Glob::new(&pattern).map_err(serde::de::Error::custom)?;
builder.add(glob);
}
builder.build().map(Some).map_err(serde::de::Error::custom)
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
@@ -371,6 +465,12 @@ pub struct LanguageServerConfiguration {
pub config: Option<serde_json::Value>,
#[serde(default = "default_timeout")]
pub timeout: u64,
#[serde(
default,
skip_serializing,
deserialize_with = "deserialize_required_root_patterns"
)]
pub required_root_patterns: Option<GlobSet>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
@@ -444,6 +544,22 @@ pub struct IndentationConfiguration {
pub unit: String,
}
/// How the indentation for a newly inserted line should be determined.
/// If the selected heuristic is not available (e.g. because the current
/// language has no tree-sitter indent queries), a simpler one will be used.
#[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum IndentationHeuristic {
/// Just copy the indentation of the line that the cursor is currently on.
Simple,
/// Use tree-sitter indent queries to compute the expected absolute indentation level of the new line.
TreeSitter,
/// Use tree-sitter indent queries to compute the expected difference in indentation between the new line
/// and the line before. Add this to the actual indentation level of the line before.
#[default]
Hybrid,
}
/// Configuration for auto pairs
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)]
@@ -693,7 +809,7 @@ impl LanguageConfiguration {
if query_text.is_empty() {
return None;
}
let lang = self.highlight_config.get()?.as_ref()?.language;
let lang = &self.highlight_config.get()?.as_ref()?.language;
Query::new(lang, &query_text)
.map_err(|e| {
log::error!(
@@ -736,6 +852,47 @@ pub struct SoftWrap {
pub wrap_at_text_width: Option<bool>,
}
#[derive(Debug)]
struct FileTypeGlob {
glob: globset::Glob,
language_id: usize,
}
impl FileTypeGlob {
fn new(glob: globset::Glob, language_id: usize) -> Self {
Self { glob, language_id }
}
}
#[derive(Debug)]
struct FileTypeGlobMatcher {
matcher: globset::GlobSet,
file_types: Vec<FileTypeGlob>,
}
impl FileTypeGlobMatcher {
fn new(file_types: Vec<FileTypeGlob>) -> Result<Self, globset::Error> {
let mut builder = globset::GlobSetBuilder::new();
for file_type in &file_types {
builder.add(file_type.glob.clone());
}
Ok(Self {
matcher: builder.build()?,
file_types,
})
}
fn language_id_for_path(&self, path: &Path) -> Option<&usize> {
self.matcher
.matches(path)
.iter()
.filter_map(|idx| self.file_types.get(*idx))
.max_by_key(|file_type| file_type.glob.glob().len())
.map(|file_type| &file_type.language_id)
}
}
// Expose loader as Lazy<> global since it's always static?
#[derive(Debug)]
@@ -743,7 +900,7 @@ pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_extension: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_suffix: HashMap<String, usize>,
language_config_ids_glob_matcher: FileTypeGlobMatcher,
language_config_ids_by_shebang: HashMap<String, usize>,
language_server_configs: HashMap<String, LanguageServerConfiguration>,
@@ -751,66 +908,57 @@ pub struct Loader {
scopes: ArcSwap<Vec<String>>,
}
pub type LoaderError = globset::Error;
impl Loader {
pub fn new(config: Configuration) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_server_configs: config.language_server,
language_config_ids_by_extension: HashMap::new(),
language_config_ids_by_suffix: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
scopes: ArcSwap::from_pointee(Vec::new()),
};
pub fn new(config: Configuration) -> Result<Self, LoaderError> {
let mut language_configs = Vec::new();
let mut language_config_ids_by_extension = HashMap::new();
let mut language_config_ids_by_shebang = HashMap::new();
let mut file_type_globs = Vec::new();
for config in config.language {
// get the next id
let language_id = loader.language_configs.len();
let language_id = language_configs.len();
for file_type in &config.file_types {
// entry().or_insert(Vec::new).push(language_id);
match file_type {
FileType::Extension(extension) => loader
.language_config_ids_by_extension
.insert(extension.clone(), language_id),
FileType::Suffix(suffix) => loader
.language_config_ids_by_suffix
.insert(suffix.clone(), language_id),
FileType::Extension(extension) => {
language_config_ids_by_extension.insert(extension.clone(), language_id);
}
FileType::Glob(glob) => {
file_type_globs.push(FileTypeGlob::new(glob.to_owned(), language_id));
}
};
}
for shebang in &config.shebangs {
loader
.language_config_ids_by_shebang
.insert(shebang.clone(), language_id);
language_config_ids_by_shebang.insert(shebang.clone(), language_id);
}
loader.language_configs.push(Arc::new(config));
language_configs.push(Arc::new(config));
}
loader
Ok(Self {
language_configs,
language_config_ids_by_extension,
language_config_ids_glob_matcher: FileTypeGlobMatcher::new(file_type_globs)?,
language_config_ids_by_shebang,
language_server_configs: config.language_server,
scopes: ArcSwap::from_pointee(Vec::new()),
})
}
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
// Find all the language configurations that match this file name
// or a suffix of the file name.
let configuration_id = path
.file_name()
.and_then(|n| n.to_str())
.and_then(|file_name| self.language_config_ids_by_extension.get(file_name))
let configuration_id = self
.language_config_ids_glob_matcher
.language_id_for_path(path)
.or_else(|| {
path.extension()
.and_then(|extension| extension.to_str())
.and_then(|extension| self.language_config_ids_by_extension.get(extension))
})
.or_else(|| {
self.language_config_ids_by_suffix
.iter()
.find_map(|(file_type, id)| {
if path.to_str()?.ends_with(file_type) {
Some(id)
} else {
None
}
})
});
configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
@@ -922,7 +1070,7 @@ thread_local! {
pub struct Syntax {
layers: HopSlotMap<LayerId, LanguageLayer>,
root: LayerId,
loader: Arc<Loader>,
loader: Arc<ArcSwap<Loader>>,
}
fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<str> {
@@ -933,7 +1081,7 @@ impl Syntax {
pub fn new(
source: RopeSlice,
config: Arc<HighlightConfiguration>,
loader: Arc<Loader>,
loader: Arc<ArcSwap<Loader>>,
) -> Option<Self> {
let root_layer = LanguageLayer {
tree: None,
@@ -946,6 +1094,7 @@ impl Syntax {
start_point: Point::new(0, 0),
end_point: Point::new(usize::MAX, usize::MAX),
}],
parent: None,
};
// track scope_descriptor: a Vec of scopes for item in tree
@@ -977,9 +1126,10 @@ impl Syntax {
let mut queue = VecDeque::new();
queue.push_back(self.root);
let scopes = self.loader.scopes.load();
let loader = self.loader.load();
let scopes = loader.scopes.load();
let injection_callback = |language: &InjectionLanguageMarker| {
self.loader
loader
.language_configuration_for_injection_string(language)
.and_then(|language_config| language_config.highlight_config(&scopes))
};
@@ -1215,6 +1365,7 @@ impl Syntax {
depth,
ranges,
flags: LayerUpdateFlags::empty(),
parent: Some(layer_id),
};
// Find an identical existing layer
@@ -1322,6 +1473,38 @@ impl Syntax {
result
}
pub fn tree_for_byte_range(&self, start: usize, end: usize) -> &Tree {
let mut container_id = self.root;
for (layer_id, layer) in self.layers.iter() {
if layer.depth > self.layers[container_id].depth
&& layer.contains_byte_range(start, end)
{
container_id = layer_id;
}
}
self.layers[container_id].tree()
}
pub fn named_descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
self.tree_for_byte_range(start, end)
.root_node()
.named_descendant_for_byte_range(start, end)
}
pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
self.tree_for_byte_range(start, end)
.root_node()
.descendant_for_byte_range(start, end)
}
pub fn walk(&self) -> TreeCursor<'_> {
// data structure to find the smallest range that contains a point
// when some of the ranges in the structure can overlap.
TreeCursor::new(&self.layers, self.root)
}
// Commenting
// comment_strings_for_pos
// is_commented
@@ -1354,6 +1537,7 @@ pub struct LanguageLayer {
pub ranges: Vec<Range>,
pub depth: u32,
flags: LayerUpdateFlags,
parent: Option<LayerId>,
}
/// This PartialEq implementation only checks if that
@@ -1373,13 +1557,7 @@ impl PartialEq for LanguageLayer {
impl Hash for LanguageLayer {
fn hash<H: Hasher>(&self, state: &mut H) {
self.depth.hash(state);
// The transmute is necessary here because tree_sitter::Language does not derive Hash at the moment.
// However it does use #[repr] transparent so the transmute here is safe
// as `Language` (which `Grammar` is an alias for) is just a newtype wrapper around a (thin) pointer.
// This is also compatible with the PartialEq implementation of language
// as that is just a pointer comparison.
let language: *const () = unsafe { transmute(self.config.language) };
language.hash(state);
self.config.language.hash(state);
self.ranges.hash(state);
}
}
@@ -1396,7 +1574,7 @@ impl LanguageLayer {
.map_err(|_| Error::InvalidRanges)?;
parser
.set_language(self.config.language)
.set_language(&self.config.language)
.map_err(|_| Error::InvalidLanguage)?;
// unsafe { syntax.parser.set_cancellation_flag(cancellation_flag) };
@@ -1418,6 +1596,32 @@ impl LanguageLayer {
self.tree = Some(tree);
Ok(())
}
/// Whether the layer contains the given byte range.
///
/// If the layer has multiple ranges (i.e. combined injections), the
/// given range is considered contained if it is within the start and
/// end bytes of the first and last ranges **and** if the given range
/// starts or ends within any of the layer's ranges.
fn contains_byte_range(&self, start: usize, end: usize) -> bool {
let layer_start = self
.ranges
.first()
.expect("ranges should not be empty")
.start_byte;
let layer_end = self
.ranges
.last()
.expect("ranges should not be empty")
.end_byte;
layer_start <= start
&& layer_end >= end
&& self.ranges.iter().any(|range| {
let byte_range = range.start_byte..range.end_byte;
byte_range.contains(&start) || byte_range.contains(&end)
})
}
}
pub(crate) fn generate_edits(
@@ -1529,7 +1733,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize};
use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
QueryMatch, Range, TextProvider, Tree, TreeCursor,
QueryMatch, Range, TextProvider, Tree,
};
const CANCELLATION_CHECK_INTERVAL: usize = 100;
@@ -1614,7 +1818,7 @@ impl<'a> Iterator for ChunksBytes<'a> {
}
pub struct RopeProvider<'a>(pub RopeSlice<'a>);
impl<'a> TextProvider<'a> for RopeProvider<'a> {
impl<'a> TextProvider<&'a [u8]> for RopeProvider<'a> {
type I = ChunksBytes<'a>;
fn text(&mut self, node: Node) -> Self::I {
@@ -1628,7 +1832,7 @@ impl<'a> TextProvider<'a> for RopeProvider<'a> {
struct HighlightIterLayer<'a> {
_tree: Option<Tree>,
cursor: QueryCursor,
captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>>,
captures: RefCell<iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>, &'a [u8]>>>,
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
@@ -1670,7 +1874,7 @@ impl HighlightConfiguration {
// 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(language, &query_source)?;
let query = Query::new(&language, &query_source)?;
let mut highlights_pattern_index = 0;
for i in 0..(query.pattern_count()) {
let pattern_offset = query.start_byte_for_pattern(i);
@@ -1679,7 +1883,7 @@ impl HighlightConfiguration {
}
}
let injections_query = Query::new(language, injection_query)?;
let injections_query = Query::new(&language, injection_query)?;
let combined_injections_patterns = (0..injections_query.pattern_count())
.filter(|&i| {
injections_query
@@ -1711,7 +1915,7 @@ impl HighlightConfiguration {
let mut local_scope_capture_index = None;
for (i, name) in query.capture_names().iter().enumerate() {
let i = Some(i as u32);
match name.as_str() {
match *name {
"local.definition" => local_def_capture_index = i,
"local.definition-value" => local_def_value_capture_index = i,
"local.reference" => local_ref_capture_index = i,
@@ -1722,7 +1926,7 @@ impl HighlightConfiguration {
for (i, name) in injections_query.capture_names().iter().enumerate() {
let i = Some(i as u32);
match name.as_str() {
match *name {
"injection.content" => injection_content_capture_index = i,
"injection.language" => injection_language_capture_index = i,
"injection.filename" => injection_filename_capture_index = i,
@@ -1752,7 +1956,7 @@ impl HighlightConfiguration {
}
/// Get a slice containing all of the highlight names used in the configuration.
pub fn names(&self) -> &[String] {
pub fn names(&self) -> &[&str] {
self.query.capture_names()
}
@@ -1779,7 +1983,6 @@ impl HighlightConfiguration {
let mut best_index = None;
let mut best_match_len = 0;
for (i, recognized_name) in recognized_names.iter().enumerate() {
let recognized_name = recognized_name;
let mut len = 0;
let mut matches = true;
for (i, part) in recognized_name.split('.').enumerate() {
@@ -1831,11 +2034,16 @@ impl HighlightConfiguration {
node_slice
};
static SHEBANG_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(SHEBANG).unwrap());
static SHEBANG_REGEX: Lazy<rope::Regex> =
Lazy::new(|| rope::Regex::new(SHEBANG).unwrap());
injection_capture = SHEBANG_REGEX
.captures(&Cow::from(lines))
.map(|cap| InjectionLanguageMarker::Shebang(cap[1].to_owned()))
.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 index == self.injection_content_capture_index {
content_node = Some(capture.node);
}
@@ -2248,6 +2456,7 @@ impl<'a> Iterator for HighlightIter<'a> {
// 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 {
@@ -2458,7 +2667,7 @@ pub fn pretty_print_tree<W: fmt::Write>(fmt: &mut W, node: Node) -> fmt::Result
fn pretty_print_tree_impl<W: fmt::Write>(
fmt: &mut W,
cursor: &mut TreeCursor,
cursor: &mut tree_sitter::TreeCursor,
depth: usize,
) -> fmt::Result {
let node = cursor.node();
@@ -2524,15 +2733,21 @@ mod test {
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
})
.unwrap();
let language = get_language("rust").unwrap();
let query = Query::new(language, query_str).unwrap();
let query = Query::new(&language, query_str).unwrap();
let textobject = TextObjectQuery { query };
let mut cursor = QueryCursor::new();
let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let syntax = Syntax::new(
source.slice(..),
Arc::new(config),
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let root = syntax.tree().root_node();
let mut test = |capture, range| {
@@ -2550,10 +2765,10 @@ mod test {
)
};
test("quantified_nodes", 1..36);
test("quantified_nodes", 1..37);
// NOTE: Enable after implementing proper node group capturing
// test("quantified_nodes_grouped", 1..36);
// test("multiple_nodes_grouped", 1..36);
// test("quantified_nodes_grouped", 1..37);
// test("multiple_nodes_grouped", 1..37);
}
#[test]
@@ -2586,7 +2801,8 @@ mod test {
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
})
.unwrap();
let language = get_language("rust").unwrap();
let config = HighlightConfiguration::new(
@@ -2606,7 +2822,12 @@ mod test {
fn main() {}
",
);
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let syntax = Syntax::new(
source.slice(..),
Arc::new(config),
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let tree = syntax.tree();
let root = tree.root_node();
assert_eq!(root.kind(), "source_file");
@@ -2692,11 +2913,17 @@ mod test {
let loader = Loader::new(Configuration {
language: vec![],
language_server: HashMap::new(),
});
})
.unwrap();
let language = get_language(language_name).unwrap();
let config = HighlightConfiguration::new(language, "", "", "").unwrap();
let syntax = Syntax::new(source.slice(..), Arc::new(config), Arc::new(loader)).unwrap();
let syntax = Syntax::new(
source.slice(..),
Arc::new(config),
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let root = syntax
.tree()
@@ -2712,7 +2939,7 @@ mod test {
#[test]
fn test_pretty_print() {
let source = r#"/// Hello"#;
let source = r#"// Hello"#;
assert_pretty_print("rust", source, "(line_comment)", 0, source.len());
// A large tree should be indented with fields:
@@ -2731,7 +2958,8 @@ mod test {
" (macro_invocation\n",
" macro: (identifier)\n",
" (token_tree\n",
" (string_literal))))))",
" (string_literal\n",
" (string_content)))))))",
),
0,
source.len(),
@@ -2750,7 +2978,7 @@ mod test {
// rule but `name` and `body` belong to an unnamed helper `_method_rest`.
// This can cause a bug with a pretty-printing implementation that
// uses `Node::field_name_for_child` to determine field names but is
// fixed when using `TreeCursor::field_name`.
// fixed when using `tree_sitter::TreeCursor::field_name`.
let source = "def self.method_name
true
end";

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

@@ -1,6 +1,5 @@
use std::cell::Cell;
use std::ops::Range;
use std::rc::Rc;
use crate::syntax::Highlight;
use crate::Tendril;
@@ -92,23 +91,23 @@ pub struct LineAnnotation {
}
#[derive(Debug)]
struct Layer<A, M> {
annotations: Rc<[A]>,
struct Layer<'a, A, M> {
annotations: &'a [A],
current_index: Cell<usize>,
metadata: M,
}
impl<A, M: Clone> Clone for Layer<A, M> {
impl<A, M: Clone> Clone for Layer<'_, A, M> {
fn clone(&self) -> Self {
Layer {
annotations: self.annotations.clone(),
annotations: self.annotations,
current_index: self.current_index.clone(),
metadata: self.metadata.clone(),
}
}
}
impl<A, M> Layer<A, M> {
impl<A, M> Layer<'_, A, M> {
pub fn reset_pos(&self, char_idx: usize, get_char_idx: impl Fn(&A) -> usize) {
let new_index = self
.annotations
@@ -128,8 +127,8 @@ impl<A, M> Layer<A, M> {
}
}
impl<A, M> From<(Rc<[A]>, M)> for Layer<A, M> {
fn from((annotations, metadata): (Rc<[A]>, M)) -> Layer<A, M> {
impl<'a, A, M> From<(&'a [A], M)> for Layer<'a, A, M> {
fn from((annotations, metadata): (&'a [A], M)) -> Layer<A, M> {
Layer {
annotations,
current_index: Cell::new(0),
@@ -147,13 +146,13 @@ fn reset_pos<A, M>(layers: &[Layer<A, M>], pos: usize, get_pos: impl Fn(&A) -> u
/// Annotations that change that is displayed when the document is render.
/// Also commonly called virtual text.
#[derive(Default, Debug, Clone)]
pub struct TextAnnotations {
inline_annotations: Vec<Layer<InlineAnnotation, Option<Highlight>>>,
overlays: Vec<Layer<Overlay, Option<Highlight>>>,
line_annotations: Vec<Layer<LineAnnotation, ()>>,
pub struct TextAnnotations<'a> {
inline_annotations: Vec<Layer<'a, InlineAnnotation, Option<Highlight>>>,
overlays: Vec<Layer<'a, Overlay, Option<Highlight>>>,
line_annotations: Vec<Layer<'a, LineAnnotation, ()>>,
}
impl TextAnnotations {
impl<'a> TextAnnotations<'a> {
/// Prepare the TextAnnotations for iteration starting at char_idx
pub fn reset_pos(&self, char_idx: usize) {
reset_pos(&self.inline_annotations, char_idx, |annot| annot.char_idx);
@@ -194,7 +193,7 @@ impl TextAnnotations {
/// the annotations that belong to the layers added first will be shown first.
pub fn add_inline_annotations(
&mut self,
layer: Rc<[InlineAnnotation]>,
layer: &'a [InlineAnnotation],
highlight: Option<Highlight>,
) -> &mut Self {
self.inline_annotations.push((layer, highlight).into());
@@ -211,7 +210,7 @@ impl TextAnnotations {
///
/// If multiple layers contain overlay at the same position
/// the overlay from the layer added last will be show.
pub fn add_overlay(&mut self, layer: Rc<[Overlay]>, highlight: Option<Highlight>) -> &mut Self {
pub fn add_overlay(&mut self, layer: &'a [Overlay], highlight: Option<Highlight>) -> &mut Self {
self.overlays.push((layer, highlight).into());
self
}
@@ -220,7 +219,7 @@ impl TextAnnotations {
///
/// The line annotations **must be sorted** by their `char_idx`.
/// Multiple line annotations with the same `char_idx` **are not allowed**.
pub fn add_line_annotation(&mut self, layer: Rc<[LineAnnotation]>) -> &mut Self {
pub fn add_line_annotation(&mut self, layer: &'a [LineAnnotation]) -> &mut Self {
self.line_annotations.push((layer, ()).into());
self
}

View File

@@ -1,7 +1,7 @@
use ropey::RopeSlice;
use smallvec::SmallVec;
use crate::{Range, Rope, Selection, Tendril};
use crate::{chars::char_is_word, Range, Rope, Selection, Tendril};
use std::{borrow::Cow, iter::once};
/// (from, to, replacement)
@@ -23,6 +23,30 @@ pub enum Operation {
pub enum Assoc {
Before,
After,
/// Acts like `After` if a word character is inserted
/// after the position, otherwise acts like `Before`
AfterWord,
/// Acts like `Before` if a word character is inserted
/// before the position, otherwise acts like `After`
BeforeWord,
}
impl Assoc {
/// Whether to stick to gaps
fn stay_at_gaps(self) -> bool {
!matches!(self, Self::BeforeWord | Self::AfterWord)
}
fn insert_offset(self, s: &str) -> usize {
let chars = s.chars().count();
match self {
Assoc::After => chars,
Assoc::AfterWord => s.chars().take_while(|&c| char_is_word(c)).count(),
// return position before inserted text
Assoc::Before => 0,
Assoc::BeforeWord => chars - s.chars().rev().take_while(|&c| char_is_word(c)).count(),
}
}
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
@@ -415,8 +439,6 @@ impl ChangeSet {
map!(|pos, _| (old_end > pos).then_some(new_pos), i);
}
Insert(s) => {
let ins = s.chars().count();
// a subsequent delete means a replace, consume it
if let Some((_, Delete(len))) = iter.peek() {
iter.next();
@@ -424,13 +446,13 @@ impl ChangeSet {
old_end = old_pos + len;
// in range of replaced text
map!(
|pos, assoc| (old_end > pos).then(|| {
|pos, assoc: Assoc| (old_end > pos).then(|| {
// at point or tracking before
if pos == old_pos || assoc == Assoc::Before {
if pos == old_pos && assoc.stay_at_gaps() {
new_pos
} else {
// place to end of insert
new_pos + ins
new_pos + assoc.insert_offset(s)
}
}),
i
@@ -438,20 +460,15 @@ impl ChangeSet {
} else {
// at insert point
map!(
|pos, assoc| (old_pos == pos).then(|| {
|pos, assoc: Assoc| (old_pos == pos).then(|| {
// return position before inserted text
if assoc == Assoc::Before {
new_pos
} else {
// after text
new_pos + ins
}
new_pos + assoc.insert_offset(s)
}),
i
);
}
new_pos += ins;
new_pos += s.chars().count();
}
}
old_pos = old_end;
@@ -884,6 +901,48 @@ mod test {
let mut positions = [4, 2];
cs.update_positions(positions.iter_mut().map(|pos| (pos, Assoc::After)));
assert_eq!(positions, [4, 2]);
// stays at word boundary
let cs = ChangeSet {
changes: vec![
Retain(2), // <space><space>
Insert(" ab".into()),
Retain(2), // cd
Insert("de ".into()),
],
len: 4,
len_after: 10,
};
assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 3);
assert_eq!(cs.map_pos(4, Assoc::AfterWord), 9);
let cs = ChangeSet {
changes: vec![
Retain(1), // <space>
Insert(" b".into()),
Delete(1), // c
Retain(1), // d
Insert("e ".into()),
Delete(1), // <space>
],
len: 5,
len_after: 7,
};
assert_eq!(cs.map_pos(1, Assoc::BeforeWord), 2);
assert_eq!(cs.map_pos(3, Assoc::AfterWord), 5);
let cs = ChangeSet {
changes: vec![
Retain(1), // <space>
Insert("a".into()),
Delete(2), // <space>b
Retain(1), // d
Insert("e".into()),
Delete(1), // f
Retain(1), // <space>
],
len: 5,
len_after: 7,
};
assert_eq!(cs.map_pos(2, Assoc::BeforeWord), 1);
assert_eq!(cs.map_pos(4, Assoc::AfterWord), 4);
}
#[test]

View File

@@ -1,10 +1,12 @@
use arc_swap::ArcSwap;
use helix_core::{
indent::{indent_level_for_line, treesitter_indent_for_pos, IndentStyle},
syntax::{Configuration, Loader},
Syntax,
};
use helix_stdx::rope::RopeSliceExt;
use ropey::Rope;
use std::{ops::Range, path::PathBuf, process::Command};
use std::{ops::Range, path::PathBuf, process::Command, sync::Arc};
#[test]
fn test_treesitter_indent_rust() {
@@ -186,7 +188,7 @@ fn test_treesitter_indent(
lang_scope: &str,
ignored_lines: Vec<std::ops::Range<usize>>,
) {
let loader = Loader::new(indent_tests_config());
let loader = Loader::new(indent_tests_config()).unwrap();
// set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -197,7 +199,12 @@ 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, std::sync::Arc::new(loader)).unwrap();
let syntax = Syntax::new(
text,
highlight_config,
Arc::new(ArcSwap::from_pointee(loader)),
)
.unwrap();
let indent_query = language_config.indent_query().unwrap();
for i in 0..doc.len_lines() {
@@ -205,12 +212,11 @@ fn test_treesitter_indent(
if ignored_lines.iter().any(|range| range.contains(&(i + 1))) {
continue;
}
if let Some(pos) = helix_core::find_first_non_whitespace_char(line) {
if let Some(pos) = line.first_non_whitespace_char() {
let tab_width: usize = 4;
let suggested_indent = treesitter_indent_for_pos(
indent_query,
&syntax,
&indent_style,
tab_width,
indent_style.indent_width(tab_width),
text,
@@ -218,7 +224,8 @@ fn test_treesitter_indent(
text.line_to_char(i) + pos,
false,
)
.unwrap();
.unwrap()
.to_string(&indent_style, tab_width);
assert!(
line.get_slice(..pos).map_or(false, |s| s == suggested_indent),
"Wrong indentation for file {:?} on line {}:\n\"{}\" (original line)\n\"{}\" (suggested indentation)\n",

View File

@@ -1,25 +1,27 @@
[package]
name = "helix-dap"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
description = "DAP client implementation for Helix project"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
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
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helix-core = { version = "0.6", path = "../helix-core" }
helix-stdx = { path = "../helix-stdx" }
helix-core = { path = "../helix-core" }
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"] }
which = "4.4"
[dev-dependencies]
fern = "0.6"

View File

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

View File

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

View File

@@ -1,15 +1,29 @@
[package]
name = "helix-event"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
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
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] }
parking_lot = { version = "0.12", features = ["send_guard"] }
ahash = "0.8.11"
hashbrown = "0.14.0"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] }
# the event registry is essentially read only but must be an rwlock so we can
# setup new events on initialization, hardware-lock-elision hugely benefits this case
# as it essentially makes the lock entirely free as long as there is no writes
parking_lot = { version = "0.12", features = ["hardware-lock-elision"] }
once_cell = "1.18"
anyhow = "1"
log = "0.4"
futures-executor = "0.3.28"
[features]
integration_test = []

19
helix-event/src/cancel.rs Normal file
View File

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

View File

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

91
helix-event/src/hook.rs Normal file
View File

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

View File

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

View File

@@ -5,16 +5,20 @@ use std::future::Future;
use parking_lot::{RwLock, RwLockReadGuard};
use tokio::sync::Notify;
/// A `Notify` instance that can be used to (asynchronously) request
/// the editor the render a new frame.
static REDRAW_NOTIFY: Notify = Notify::const_new();
use crate::runtime_local;
/// A `RwLock` that prevents the next frame from being
/// drawn until an exclusive (write) lock can be acquired.
/// This allows asynchsonous tasks to acquire `non-exclusive`
/// locks (read) to prevent the next frame from being drawn
/// until a certain computation has finished.
static RENDER_LOCK: RwLock<()> = RwLock::new(());
runtime_local! {
/// A `Notify` instance that can be used to (asynchronously) request
/// the editor to render a new frame.
static REDRAW_NOTIFY: Notify = Notify::const_new();
/// A `RwLock` that prevents the next frame from being
/// drawn until an exclusive (write) lock can be acquired.
/// This allows asynchronous tasks to acquire `non-exclusive`
/// locks (read) to prevent the next frame from being drawn
/// until a certain computation has finished.
static RENDER_LOCK: RwLock<()> = RwLock::new(());
}
pub type RenderLockGuard = RwLockReadGuard<'static, ()>;

131
helix-event/src/registry.rs Normal file
View File

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

View File

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

68
helix-event/src/status.rs Normal file
View File

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

90
helix-event/src/test.rs Normal file
View File

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

View File

@@ -1,34 +1,36 @@
[package]
name = "helix-loader"
version = "0.6.0"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
description = "Build bootstrapping for Helix crates"
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
[[bin]]
name = "hx-loader"
path = "src/main.rs"
[dependencies]
helix-stdx = { path = "../helix-stdx" }
anyhow = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.7"
toml = "0.8"
etcetera = "0.8"
tree-sitter.workspace = true
once_cell = "1.18"
once_cell = "1.19"
log = "0.4"
which = "4.4"
# TODO: these two should be on !wasm32 only
# cloning/compiling tree-sitter grammars
cc = { version = "1" }
threadpool = { version = "1.0" }
tempfile = "3.8.0"
tempfile = "3.10.1"
dunce = "1.0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@@ -2,7 +2,17 @@ use std::borrow::Cow;
use std::path::Path;
use std::process::Command;
const VERSION: &str = include_str!("../VERSION");
const MAJOR: &str = env!("CARGO_PKG_VERSION_MAJOR");
const MINOR: &str = env!("CARGO_PKG_VERSION_MINOR");
const PATCH: &str = env!("CARGO_PKG_VERSION_PATCH");
fn get_calver() -> String {
if PATCH == "0" {
format!("{MAJOR}.{MINOR}")
} else {
format!("{MAJOR}.{MINOR}.{PATCH}")
}
}
fn main() {
let git_hash = Command::new("git")
@@ -12,9 +22,10 @@ fn main() {
.filter(|output| output.status.success())
.and_then(|x| String::from_utf8(x.stdout).ok());
let calver = get_calver();
let version: Cow<_> = match &git_hash {
Some(git_hash) => format!("{} ({})", VERSION, &git_hash[..8]).into(),
None => VERSION.into(),
Some(git_hash) => format!("{} ({})", calver, &git_hash[..8]).into(),
None => calver.into(),
};
println!(
@@ -22,7 +33,6 @@ fn main() {
std::env::var("TARGET").unwrap()
);
println!("cargo:rerun-if-changed=../VERSION");
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", version);
if git_hash.is_none() {

View File

@@ -86,10 +86,8 @@ pub fn get_language(name: &str) -> Result<Language> {
}
fn ensure_git_is_available() -> Result<()> {
match which::which("git") {
Ok(_cmd) => Ok(()),
Err(err) => Err(anyhow::anyhow!("'git' could not be found ({err})")),
}
helix_stdx::env::which("git")?;
Ok(())
}
pub fn fetch_grammars() -> Result<()> {

View File

@@ -1,14 +1,13 @@
pub mod config;
pub mod grammar;
use helix_stdx::{env::current_working_dir, path};
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
use std::path::{Path, PathBuf};
use std::sync::RwLock;
pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH");
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
static RUNTIME_DIRS: once_cell::sync::Lazy<Vec<PathBuf>> =
once_cell::sync::Lazy::new(prioritize_runtime_dirs);
@@ -16,31 +15,6 @@ static CONFIG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCe
static LOG_FILE: once_cell::sync::OnceCell<PathBuf> = once_cell::sync::OnceCell::new();
// Get the current working directory.
// This information is managed internally as the call to std::env::current_dir
// might fail if the cwd has been deleted.
pub fn current_working_dir() -> PathBuf {
if let Some(path) = &*CWD.read().unwrap() {
return path.clone();
}
let path = std::env::current_dir()
.and_then(dunce::canonicalize)
.expect("Couldn't determine current working directory");
let mut cwd = CWD.write().unwrap();
*cwd = Some(path.clone());
path
}
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
let path = dunce::canonicalize(path)?;
std::env::set_current_dir(&path)?;
let mut cwd = CWD.write().unwrap();
*cwd = Some(path);
Ok(())
}
pub fn initialize_config_file(specified_file: Option<PathBuf>) {
let config_file = specified_file.unwrap_or_else(default_config_file);
ensure_parent_dir(&config_file);
@@ -79,7 +53,8 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
rt_dirs.push(conf_rt_dir);
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
rt_dirs.push(dir.into());
let dir = path::expand_tilde(Path::new(&dir));
rt_dirs.push(path::normalize(dir));
}
// If this variable is set during build time, it will always be included
@@ -151,7 +126,7 @@ pub fn config_dir() -> PathBuf {
pub fn cache_dir() -> PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let strategy = choose_base_strategy().expect("Unable to find the cache directory!");
let mut path = strategy.cache_dir();
path.push("helix");
path
@@ -280,21 +255,9 @@ fn ensure_parent_dir(path: &Path) {
mod merge_toml_tests {
use std::str;
use super::{current_working_dir, merge_toml_values, set_current_working_dir};
use super::merge_toml_values;
use toml::Value;
#[test]
fn current_dir_is_set() {
let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
let cwd = current_working_dir();
assert_ne!(cwd, new_path);
set_current_working_dir(&new_path).expect("Couldn't set new path");
let cwd = current_working_dir();
assert_eq!(cwd, new_path);
}
#[test]
fn language_toml_map_merges() {
const USER: &str = r#"

View File

@@ -1,31 +1,34 @@
[package]
name = "helix-lsp"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
description = "LSP client implementation for Helix project"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
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
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helix-core = { version = "0.6", path = "../helix-core" }
helix-loader = { version = "0.6", path = "../helix-loader" }
helix-parsec = { version = "0.6", path = "../helix-parsec" }
helix-stdx = { path = "../helix-stdx" }
helix-core = { path = "../helix-core" }
helix-loader = { path = "../helix-loader" }
helix-parsec = { path = "../helix-parsec" }
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
globset = "0.4.13"
globset = "0.4.14"
log = "0.4"
lsp-types = { version = "0.94" }
lsp-types = { version = "0.95" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.33", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.14"
which = "4.4"
tokio = { version = "1.37", 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"
arc-swap = "1"
slotmap.workspace = true

View File

@@ -1,27 +1,29 @@
use crate::{
file_operations::FileOperationsInterest,
find_lsp_workspace, jsonrpc,
transport::{Payload, Transport},
Call, Error, OffsetEncoding, Result,
Call, Error, LanguageServerId, OffsetEncoding, Result,
};
use helix_core::{find_workspace, path, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::VERSION_AND_GIT_HASH;
use helix_stdx::path;
use lsp::{
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, WorkspaceFolder,
WorkspaceFoldersChangeEvent,
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, Url,
WorkspaceFolder, WorkspaceFoldersChangeEvent,
};
use lsp_types as lsp;
use parking_lot::Mutex;
use serde::Deserialize;
use serde_json::Value;
use std::future::Future;
use std::process::Stdio;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use std::{collections::HashMap, path::PathBuf};
use std::{future::Future, sync::OnceLock};
use std::{path::Path, process::Stdio};
use tokio::{
io::{BufReader, BufWriter},
process::{Child, Command},
@@ -44,12 +46,13 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
#[derive(Debug)]
pub struct Client {
id: usize,
id: LanguageServerId,
name: String,
_process: Child,
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
pub(crate) file_operation_interest: OnceLock<FileOperationsInterest>,
config: Option<Value>,
root_path: std::path::PathBuf,
root_uri: Option<lsp::Url>,
@@ -68,7 +71,7 @@ impl Client {
may_support_workspace: bool,
) -> bool {
let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::get_normalized_path(&workspace);
let workspace = path::normalize(workspace);
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
@@ -174,15 +177,18 @@ impl Client {
args: &[String],
config: Option<Value>,
server_environment: HashMap<String, String>,
root_markers: &[String],
manual_roots: &[PathBuf],
id: usize,
root_path: PathBuf,
root_uri: Option<lsp::Url>,
id: LanguageServerId,
name: String,
req_timeout: u64,
doc_path: Option<&std::path::PathBuf>,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
) -> Result<(
Self,
UnboundedReceiver<(LanguageServerId, Call)>,
Arc<Notify>,
)> {
// Resolve path to the binary
let cmd = which::which(cmd).map_err(|err| anyhow::anyhow!(err))?;
let cmd = helix_stdx::env::which(cmd)?;
let process = Command::new(cmd)
.envs(server_environment)
@@ -203,22 +209,6 @@ impl Client {
let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id, name.clone());
let (workspace, workspace_is_cwd) = find_workspace();
let workspace = path::get_normalized_path(&workspace);
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
root_markers,
manual_roots,
&workspace,
workspace_is_cwd,
);
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
let workspace_folders = root_uri
.clone()
@@ -232,6 +222,7 @@ impl Client {
server_tx,
request_counter: AtomicU64::new(0),
capabilities: OnceCell::new(),
file_operation_interest: OnceLock::new(),
config,
req_timeout,
root_path,
@@ -247,7 +238,7 @@ impl Client {
&self.name
}
pub fn id(&self) -> usize {
pub fn id(&self) -> LanguageServerId {
self.id
}
@@ -277,6 +268,11 @@ impl Client {
.expect("language server not yet initialized!")
}
pub(crate) fn file_operations_intests(&self) -> &FileOperationsInterest {
self.file_operation_interest
.get_or_init(|| FileOperationsInterest::new(self.capabilities()))
}
/// Client has to be initialized otherwise this function panics
#[inline]
pub fn supports_feature(&self, feature: LanguageServerFeature) -> bool {
@@ -401,24 +397,43 @@ impl Client {
&self,
params: R::Params,
) -> impl Future<Output = Result<Value>>
where
R::Params: serde::Serialize,
{
self.call_with_ref::<R>(&params)
}
fn call_with_ref<R: lsp::request::Request>(
&self,
params: &R::Params,
) -> impl Future<Output = Result<Value>>
where
R::Params: serde::Serialize,
{
self.call_with_timeout::<R>(params, self.req_timeout)
}
fn call_with_timeout<R: lsp::request::Request>(
&self,
params: &R::Params,
timeout_secs: u64,
) -> impl Future<Output = Result<Value>>
where
R::Params: serde::Serialize,
{
let server_tx = self.server_tx.clone();
let id = self.next_request_id();
let timeout_secs = self.req_timeout;
let params = serde_json::to_value(params);
async move {
use std::time::Duration;
use tokio::time::timeout;
let params = serde_json::to_value(params)?;
let request = jsonrpc::MethodCall {
jsonrpc: Some(jsonrpc::Version::V2),
id: id.clone(),
method: R::METHOD.to_string(),
params: Self::value_into_params(params),
params: Self::value_into_params(params?),
};
let (tx, mut rx) = channel::<Result<Value>>(1);
@@ -548,6 +563,11 @@ impl Client {
dynamic_registration: Some(true),
relative_pattern_support: Some(false),
}),
file_operations: Some(lsp::WorkspaceFileOperationsClientCapabilities {
will_rename: Some(true),
did_rename: Some(true),
..Default::default()
}),
..Default::default()
}),
text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -624,6 +644,12 @@ impl Client {
}),
publish_diagnostics: Some(lsp::PublishDiagnosticsClientCapabilities {
version_support: Some(true),
tag_support: Some(lsp::TagSupport {
value_set: vec![
lsp::DiagnosticTag::UNNECESSARY,
lsp::DiagnosticTag::DEPRECATED,
],
}),
..Default::default()
}),
inlay_hint: Some(lsp::InlayHintClientCapabilities {
@@ -652,6 +678,7 @@ impl Client {
version: Some(String::from(VERSION_AND_GIT_HASH)),
}),
locale: None, // TODO
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
};
self.request::<lsp::request::Initialize>(params).await
@@ -700,6 +727,66 @@ impl Client {
})
}
pub fn will_rename(
&self,
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
let capabilities = self.file_operations_intests();
if !capabilities.will_rename.has_interest(old_path, is_dir) {
return None;
}
let url_from_path = |path| {
let url = if is_dir {
Url::from_directory_path(path)
} else {
Url::from_file_path(path)
};
Some(url.ok()?.to_string())
};
let files = vec![lsp::FileRename {
old_uri: url_from_path(old_path)?,
new_uri: url_from_path(new_path)?,
}];
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
&lsp::RenameFilesParams { files },
5,
);
Some(async move {
let json = request.await?;
let response: Option<lsp::WorkspaceEdit> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
}
pub fn did_rename(
&self,
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
let capabilities = self.file_operations_intests();
if !capabilities.did_rename.has_interest(new_path, is_dir) {
return None;
}
let url_from_path = |path| {
let url = if is_dir {
Url::from_directory_path(path)
} else {
Url::from_file_path(path)
};
Some(url.ok()?.to_string())
};
let files = vec![lsp::FileRename {
old_uri: url_from_path(old_path)?,
new_uri: url_from_path(new_path)?,
}];
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
}
// -------------------------------------------------------------------------------------------
// Text document
// -------------------------------------------------------------------------------------------
@@ -895,20 +982,19 @@ impl Client {
) -> Option<impl Future<Output = Result<()>>> {
let capabilities = self.capabilities.get().unwrap();
let include_text = match &capabilities.text_document_sync {
Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
save: Some(options),
let include_text = match &capabilities.text_document_sync.as_ref()? {
lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
save: options,
..
})) => match options {
}) => match options.as_ref()? {
lsp::TextDocumentSyncSaveOptions::Supported(true) => false,
lsp::TextDocumentSyncSaveOptions::SaveOptions(lsp_types::SaveOptions {
include_text,
}) => include_text.unwrap_or(false),
// Supported(false)
_ => return None,
lsp::TextDocumentSyncSaveOptions::Supported(false) => return None,
},
// unsupported
_ => return None,
// see: https://github.com/microsoft/language-server-protocol/issues/288
lsp::TextDocumentSyncCapability::Kind(..) => false,
};
Some(self.notify::<lsp::notification::DidSaveTextDocument>(
@@ -924,6 +1010,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
context: lsp::CompletionContext,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
@@ -935,13 +1022,12 @@ impl Client {
text_document,
position,
},
context: Some(context),
// TODO: support these tokens by async receiving and updating the choice list
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
context: None,
// lsp::CompletionContext { trigger_kind: , trigger_character: Some(), }
};
Some(self.call::<lsp::request::Completion>(params))
@@ -949,20 +1035,10 @@ impl Client {
pub fn resolve_completion_item(
&self,
completion_item: lsp::CompletionItem,
) -> Option<impl Future<Output = Result<Value>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support resolving completion items.
match capabilities.completion_provider {
Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..
}) => (),
_ => return None,
}
Some(self.call::<lsp::request::ResolveCompletionItem>(completion_item))
completion_item: &lsp::CompletionItem,
) -> impl Future<Output = Result<lsp::CompletionItem>> {
let res = self.call_with_ref::<lsp::request::ResolveCompletionItem>(completion_item);
async move { Ok(serde_json::from_value(res.await?)?) }
}
pub fn resolve_code_action(
@@ -988,7 +1064,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Value>>> {
) -> Option<impl Future<Output = Result<Option<SignatureHelp>>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support signature help.
@@ -1004,7 +1080,8 @@ impl Client {
// lsp::SignatureHelpContext
};
Some(self.call::<lsp::request::SignatureHelpRequest>(params))
let res = self.call::<lsp::request::SignatureHelpRequest>(params);
Some(async move { Ok(serde_json::from_value(res.await?)?) })
}
pub fn text_document_range_inlay_hints(

View File

@@ -3,24 +3,24 @@ use std::{collections::HashMap, path::PathBuf, sync::Weak};
use globset::{GlobBuilder, GlobSetBuilder};
use tokio::sync::mpsc;
use crate::{lsp, Client};
use crate::{lsp, Client, LanguageServerId};
enum Event {
FileChanged {
path: PathBuf,
},
Register {
client_id: usize,
client_id: LanguageServerId,
client: Weak<Client>,
registration_id: String,
options: lsp::DidChangeWatchedFilesRegistrationOptions,
},
Unregister {
client_id: usize,
client_id: LanguageServerId,
registration_id: String,
},
RemoveClient {
client_id: usize,
client_id: LanguageServerId,
},
}
@@ -59,7 +59,7 @@ impl Handler {
pub fn register(
&self,
client_id: usize,
client_id: LanguageServerId,
client: Weak<Client>,
registration_id: String,
options: lsp::DidChangeWatchedFilesRegistrationOptions,
@@ -72,7 +72,7 @@ impl Handler {
});
}
pub fn unregister(&self, client_id: usize, registration_id: String) {
pub fn unregister(&self, client_id: LanguageServerId, registration_id: String) {
let _ = self.tx.send(Event::Unregister {
client_id,
registration_id,
@@ -83,12 +83,12 @@ impl Handler {
let _ = self.tx.send(Event::FileChanged { path });
}
pub fn remove_client(&self, client_id: usize) {
pub fn remove_client(&self, client_id: LanguageServerId) {
let _ = self.tx.send(Event::RemoveClient { client_id });
}
async fn run(mut rx: mpsc::UnboundedReceiver<Event>) {
let mut state: HashMap<usize, ClientState> = HashMap::new();
let mut state: HashMap<LanguageServerId, ClientState> = HashMap::new();
while let Some(event) = rx.recv().await {
match event {
Event::FileChanged { path } => {

View File

@@ -0,0 +1,105 @@
use std::path::Path;
use globset::{GlobBuilder, GlobSet};
use crate::lsp;
#[derive(Default, Debug)]
pub(crate) struct FileOperationFilter {
dir_globs: GlobSet,
file_globs: GlobSet,
}
impl FileOperationFilter {
fn new(capability: Option<&lsp::FileOperationRegistrationOptions>) -> FileOperationFilter {
let Some(cap) = capability else {
return FileOperationFilter::default();
};
let mut dir_globs = GlobSet::builder();
let mut file_globs = GlobSet::builder();
for filter in &cap.filters {
// TODO: support other url schemes
let is_non_file_schema = filter
.scheme
.as_ref()
.is_some_and(|schema| schema != "file");
if is_non_file_schema {
continue;
}
let ignore_case = filter
.pattern
.options
.as_ref()
.and_then(|opts| opts.ignore_case)
.unwrap_or(false);
let mut glob_builder = GlobBuilder::new(&filter.pattern.glob);
glob_builder.case_insensitive(!ignore_case);
let glob = match glob_builder.build() {
Ok(glob) => glob,
Err(err) => {
log::error!("invalid glob send by LS: {err}");
continue;
}
};
match filter.pattern.matches {
Some(lsp::FileOperationPatternKind::File) => {
file_globs.add(glob);
}
Some(lsp::FileOperationPatternKind::Folder) => {
dir_globs.add(glob);
}
None => {
file_globs.add(glob.clone());
dir_globs.add(glob);
}
};
}
let file_globs = file_globs.build().unwrap_or_else(|err| {
log::error!("invalid globs send by LS: {err}");
GlobSet::empty()
});
let dir_globs = dir_globs.build().unwrap_or_else(|err| {
log::error!("invalid globs send by LS: {err}");
GlobSet::empty()
});
FileOperationFilter {
dir_globs,
file_globs,
}
}
pub(crate) fn has_interest(&self, path: &Path, is_dir: bool) -> bool {
if is_dir {
self.dir_globs.is_match(path)
} else {
self.file_globs.is_match(path)
}
}
}
#[derive(Default, Debug)]
pub(crate) struct FileOperationsInterest {
// TODO: support other notifications
// did_create: FileOperationFilter,
// will_create: FileOperationFilter,
pub did_rename: FileOperationFilter,
pub will_rename: FileOperationFilter,
// did_delete: FileOperationFilter,
// will_delete: FileOperationFilter,
}
impl FileOperationsInterest {
pub fn new(capabilities: &lsp::ServerCapabilities) -> FileOperationsInterest {
let capabilities = capabilities
.workspace
.as_ref()
.and_then(|capabilities| capabilities.file_operations.as_ref());
let Some(capabilities) = capabilities else {
return FileOperationsInterest::default();
};
FileOperationsInterest {
did_rename: FileOperationFilter::new(capabilities.did_rename.as_ref()),
will_rename: FileOperationFilter::new(capabilities.will_rename.as_ref()),
}
}
}

View File

@@ -1,9 +1,11 @@
mod client;
pub mod file_event;
mod file_operations;
pub mod jsonrpc;
pub mod snippet;
mod transport;
use arc_swap::ArcSwap;
pub use client::Client;
pub use futures_executor::block_on;
pub use jsonrpc::Call;
@@ -11,10 +13,11 @@ pub use lsp::{Position, Url};
pub use lsp_types as lsp;
use futures_util::stream::select_all::SelectAll;
use helix_core::{
path,
syntax::{LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures},
use helix_core::syntax::{
LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures,
};
use helix_stdx::path;
use slotmap::SlotMap;
use tokio::sync::mpsc::UnboundedReceiver;
use std::{
@@ -26,8 +29,9 @@ use std::{
use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream;
pub type Result<T> = core::result::Result<T, Error>;
pub type Result<T, E = Error> = core::result::Result<T, E>;
pub type LanguageServerName = String;
pub use helix_core::diagnostic::LanguageServerId;
#[derive(Error, Debug)]
pub enum Error {
@@ -44,6 +48,8 @@ pub enum Error {
#[error("Unhandled")]
Unhandled,
#[error(transparent)]
ExecutableNotFound(#[from] helix_stdx::env::ExecutableNotFoundError),
#[error(transparent)]
Other(#[from] anyhow::Error),
}
@@ -280,7 +286,8 @@ pub mod util {
.chars_at(cursor)
.skip(1)
.take_while(|ch| chars::char_is_word(*ch))
.count();
.count()
+ 1;
}
(start, end)
}
@@ -535,6 +542,16 @@ pub mod util {
} else {
return (0, 0, None);
};
if start > end {
log::error!(
"Invalid LSP text edit start {:?} > end {:?}, discarding",
start,
end
);
return (0, 0, None);
}
(start, end, replacement)
}),
)
@@ -549,6 +566,7 @@ pub enum MethodCall {
WorkspaceConfiguration(lsp::ConfigurationParams),
RegisterCapability(lsp::RegistrationParams),
UnregisterCapability(lsp::UnregistrationParams),
ShowDocument(lsp::ShowDocumentParams),
}
impl MethodCall {
@@ -576,6 +594,10 @@ impl MethodCall {
let params: lsp::UnregistrationParams = params.parse()?;
Self::UnregisterCapability(params)
}
lsp::request::ShowDocument::METHOD => {
let params: lsp::ShowDocumentParams = params.parse()?;
Self::ShowDocument(params)
}
_ => {
return Err(Error::Unhandled);
}
@@ -631,38 +653,42 @@ impl Notification {
#[derive(Debug)]
pub struct Registry {
inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
syn_loader: Arc<helix_core::syntax::Loader>,
counter: usize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
inner: SlotMap<LanguageServerId, Arc<Client>>,
inner_by_name: HashMap<LanguageServerName, Vec<Arc<Client>>>,
syn_loader: Arc<ArcSwap<helix_core::syntax::Loader>>,
pub incoming: SelectAll<UnboundedReceiverStream<(LanguageServerId, Call)>>,
pub file_event_handler: file_event::Handler,
}
impl Registry {
pub fn new(syn_loader: Arc<helix_core::syntax::Loader>) -> Self {
pub fn new(syn_loader: Arc<ArcSwap<helix_core::syntax::Loader>>) -> Self {
Self {
inner: HashMap::new(),
inner: SlotMap::with_key(),
inner_by_name: HashMap::new(),
syn_loader,
counter: 0,
incoming: SelectAll::new(),
file_event_handler: file_event::Handler::new(),
}
}
pub fn get_by_id(&self, id: usize) -> Option<&Client> {
self.inner
.values()
.flatten()
.find(|client| client.id() == id)
.map(|client| &**client)
pub fn get_by_id(&self, id: LanguageServerId) -> Option<&Arc<Client>> {
self.inner.get(id)
}
pub fn remove_by_id(&mut self, id: usize) {
pub fn remove_by_id(&mut self, id: LanguageServerId) {
let Some(client) = self.inner.remove(id) else {
log::error!("client was already removed");
return
};
self.file_event_handler.remove_client(id);
self.inner.retain(|_, language_servers| {
language_servers.retain(|ls| id != ls.id());
!language_servers.is_empty()
});
let instances = self
.inner_by_name
.get_mut(client.name())
.expect("inner and inner_by_name must be synced");
instances.retain(|ls| id != ls.id());
if instances.is_empty() {
self.inner_by_name.remove(client.name());
}
}
fn start_client(
@@ -672,25 +698,28 @@ impl Registry {
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<Arc<Client>> {
let config = self
.syn_loader
) -> Result<Arc<Client>, StartupError> {
let syn_loader = self.syn_loader.load();
let config = syn_loader
.language_server_configs()
.get(&name)
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
let id = self.counter;
self.counter += 1;
let NewClient(client, incoming) = start_client(
id,
name,
ls_config,
config,
doc_path,
root_dirs,
enable_snippets,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
Ok(client)
let id = self.inner.try_insert_with_key(|id| {
start_client(
id,
name,
ls_config,
config,
doc_path,
root_dirs,
enable_snippets,
)
.map(|client| {
self.incoming.push(UnboundedReceiverStream::new(client.1));
client.0
})
})?;
Ok(self.inner[id].clone())
}
/// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
@@ -707,7 +736,7 @@ impl Registry {
.language_servers
.iter()
.filter_map(|LanguageServerFeatures { name, .. }| {
if self.inner.contains_key(name) {
if self.inner_by_name.contains_key(name) {
let client = match self.start_client(
name.clone(),
language_config,
@@ -716,15 +745,17 @@ impl Registry {
enable_snippets,
) {
Ok(client) => client,
error => return Some(error),
Err(StartupError::NoRequiredRootFound) => return None,
Err(StartupError::Error(err)) => return Some(Err(err)),
};
let old_clients = self
.inner
.inner_by_name
.insert(name.clone(), vec![client.clone()])
.unwrap();
for old_client in old_clients {
self.file_event_handler.remove_client(old_client.id());
self.inner.remove(client.id());
tokio::spawn(async move {
let _ = old_client.force_shutdown().await;
});
@@ -739,9 +770,10 @@ impl Registry {
}
pub fn stop(&mut self, name: &str) {
if let Some(clients) = self.inner.remove(name) {
if let Some(clients) = self.inner_by_name.remove(name) {
for client in clients {
self.file_event_handler.remove_client(client.id());
self.inner.remove(client.id());
tokio::spawn(async move {
let _ = client.force_shutdown().await;
});
@@ -756,13 +788,13 @@ impl Registry {
root_dirs: &'a [PathBuf],
enable_snippets: bool,
) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
language_config.language_servers.iter().map(
language_config.language_servers.iter().filter_map(
move |LanguageServerFeatures { name, .. }| {
if let Some(clients) = self.inner.get(name) {
if let Some(clients) = self.inner_by_name.get(name) {
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
}) {
return (name.to_owned(), Ok(client.clone()));
return Some((name.to_owned(), Ok(client.clone())));
}
}
match self.start_client(
@@ -773,20 +805,21 @@ impl Registry {
enable_snippets,
) {
Ok(client) => {
self.inner
self.inner_by_name
.entry(name.to_owned())
.or_default()
.push(client.clone());
(name.clone(), Ok(client))
Some((name.clone(), Ok(client)))
}
Err(err) => (name.to_owned(), Err(err)),
Err(StartupError::NoRequiredRootFound) => None,
Err(StartupError::Error(err)) => Some((name.to_owned(), Err(err))),
}
},
)
}
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().flatten()
self.inner.values()
}
}
@@ -809,7 +842,7 @@ impl ProgressStatus {
/// Acts as a container for progress reported by language servers. Each server
/// has a unique id assigned at creation through [`Registry`]. This id is then used
/// to store the progress in this map.
pub struct LspProgressMap(HashMap<usize, HashMap<lsp::ProgressToken, ProgressStatus>>);
pub struct LspProgressMap(HashMap<LanguageServerId, HashMap<lsp::ProgressToken, ProgressStatus>>);
impl LspProgressMap {
pub fn new() -> Self {
@@ -817,28 +850,35 @@ impl LspProgressMap {
}
/// Returns a map of all tokens corresponding to the language server with `id`.
pub fn progress_map(&self, id: usize) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
pub fn progress_map(
&self,
id: LanguageServerId,
) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
self.0.get(&id)
}
pub fn is_progressing(&self, id: usize) -> bool {
pub fn is_progressing(&self, id: LanguageServerId) -> bool {
self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default()
}
/// Returns last progress status for a given server with `id` and `token`.
pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> {
pub fn progress(
&self,
id: LanguageServerId,
token: &lsp::ProgressToken,
) -> Option<&ProgressStatus> {
self.0.get(&id).and_then(|values| values.get(token))
}
/// Checks if progress `token` for server with `id` is created.
pub fn is_created(&mut self, id: usize, token: &lsp::ProgressToken) -> bool {
pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool {
self.0
.get(&id)
.map(|values| values.get(token).is_some())
.unwrap_or_default()
}
pub fn create(&mut self, id: usize, token: lsp::ProgressToken) {
pub fn create(&mut self, id: LanguageServerId, token: lsp::ProgressToken) {
self.0
.entry(id)
.or_default()
@@ -848,7 +888,7 @@ impl LspProgressMap {
/// Ends the progress by removing the `token` from server with `id`, if removed returns the value.
pub fn end_progress(
&mut self,
id: usize,
id: LanguageServerId,
token: &lsp::ProgressToken,
) -> Option<ProgressStatus> {
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
@@ -857,7 +897,7 @@ impl LspProgressMap {
/// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`.
pub fn update(
&mut self,
id: usize,
id: LanguageServerId,
token: lsp::ProgressToken,
status: lsp::WorkDoneProgress,
) -> Option<ProgressStatus> {
@@ -868,30 +908,68 @@ impl LspProgressMap {
}
}
struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
struct NewClient(Arc<Client>, UnboundedReceiver<(LanguageServerId, Call)>);
enum StartupError {
NoRequiredRootFound,
Error(Error),
}
impl<T: Into<Error>> From<T> for StartupError {
fn from(value: T) -> Self {
StartupError::Error(value.into())
}
}
/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
/// it is only called when it makes sense.
fn start_client(
id: usize,
id: LanguageServerId,
name: String,
config: &LanguageConfiguration,
ls_config: &LanguageServerConfiguration,
doc_path: Option<&std::path::PathBuf>,
root_dirs: &[PathBuf],
enable_snippets: bool,
) -> Result<NewClient> {
) -> Result<NewClient, StartupError> {
let (workspace, workspace_is_cwd) = helix_loader::find_workspace();
let workspace = path::normalize(workspace);
let root = find_lsp_workspace(
doc_path
.and_then(|x| x.parent().and_then(|x| x.to_str()))
.unwrap_or("."),
&config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
&workspace,
workspace_is_cwd,
);
// `root_uri` and `workspace_folder` can be empty in case there is no workspace
// `root_url` can not, use `workspace` as a fallback
let root_path = root.clone().unwrap_or_else(|| workspace.clone());
let root_uri = root.and_then(|root| lsp::Url::from_file_path(root).ok());
if let Some(globset) = &ls_config.required_root_patterns {
if !root_path
.read_dir()?
.flatten()
.map(|entry| entry.file_name())
.any(|entry| globset.is_match(entry))
{
return Err(StartupError::NoRequiredRootFound);
}
}
let (client, incoming, initialize_notify) = Client::start(
&ls_config.command,
&ls_config.args,
ls_config.config.clone(),
ls_config.environment.clone(),
&config.roots,
config.workspace_lsp_roots.as_deref().unwrap_or(root_dirs),
root_path,
root_uri,
id,
name,
ls_config.timeout,
doc_path,
)?;
let client = Arc::new(client);
@@ -915,10 +993,17 @@ fn start_client(
}
// next up, notify<initialized>
_client
let notification_result = _client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await
.unwrap();
.await;
if let Err(e) = notification_result {
log::error!(
"failed to notify language server of its initialization: {}",
e
);
return;
}
initialize_notify.notify_one();
});
@@ -946,10 +1031,10 @@ pub fn find_lsp_workspace(
let mut file = if file.is_absolute() {
file.to_path_buf()
} else {
let current_dir = helix_loader::current_working_dir();
let current_dir = helix_stdx::env::current_working_dir();
current_dir.join(file)
};
file = path::get_normalized_path(&file);
file = path::normalize(&file);
if !file.starts_with(workspace) {
return None;
@@ -966,7 +1051,7 @@ pub fn find_lsp_workspace(
if root_dirs
.iter()
.any(|root_dir| path::get_normalized_path(&workspace.join(root_dir)) == ancestor)
.any(|root_dir| path::normalize(workspace.join(root_dir)) == ancestor)
{
// if the worskapce is the cwd do not search any higher for workspaces
// but specify

View File

@@ -1,4 +1,4 @@
use crate::{jsonrpc, Error, Result};
use crate::{jsonrpc, Error, LanguageServerId, Result};
use anyhow::Context;
use log::{error, info};
use serde::{Deserialize, Serialize};
@@ -37,7 +37,7 @@ enum ServerMessage {
#[derive(Debug)]
pub struct Transport {
id: usize,
id: LanguageServerId,
name: String,
pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
}
@@ -47,10 +47,10 @@ impl Transport {
server_stdout: BufReader<ChildStdout>,
server_stdin: BufWriter<ChildStdin>,
server_stderr: BufReader<ChildStderr>,
id: usize,
id: LanguageServerId,
name: String,
) -> (
UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedReceiver<(LanguageServerId, jsonrpc::Call)>,
UnboundedSender<Payload>,
Arc<Notify>,
) {
@@ -194,7 +194,7 @@ impl Transport {
async fn process_server_message(
&self,
client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
client_tx: &UnboundedSender<(LanguageServerId, jsonrpc::Call)>,
msg: ServerMessage,
language_server_name: &str,
) -> Result<()> {
@@ -251,7 +251,7 @@ impl Transport {
async fn recv(
transport: Arc<Self>,
mut server_stdout: BufReader<ChildStdout>,
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>,
) {
let mut recv_buffer = String::new();
loop {
@@ -270,7 +270,14 @@ impl Transport {
}
};
}
Err(Error::StreamClosed) => {
Err(err) => {
if !matches!(err, Error::StreamClosed) {
error!(
"Exiting {} after unexpected error: {err:?}",
&transport.name
);
}
// Close any outstanding requests.
for (id, tx) in transport.pending_requests.lock().await.drain() {
match tx.send(Err(Error::StreamClosed)).await {
@@ -300,10 +307,6 @@ impl Transport {
}
break;
}
Err(err) => {
error!("{} err: <- {err:?}", transport.name);
break;
}
}
}
}
@@ -326,7 +329,7 @@ impl Transport {
async fn send(
transport: Arc<Self>,
mut server_stdin: BufWriter<ChildStdin>,
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>,
mut client_rx: UnboundedReceiver<Payload>,
initialize_notify: Arc<Notify>,
) {

View File

@@ -1,13 +1,14 @@
[package]
name = "helix-parsec"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
description = "Parser combinators for Helix"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
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
[dependencies]

29
helix-stdx/Cargo.toml Normal file
View File

@@ -0,0 +1,29 @@
[package]
name = "helix-stdx"
description = "Standard library extensions"
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
[dependencies]
dunce = "1.0"
etcetera = "0.8"
ropey = { version = "1.6.1", default-features = false }
which = "6.0"
regex-cursor = "0.1.4"
bitflags = "2.4"
[target.'cfg(windows)'.dependencies]
windows-sys = { version = "0.52", features = ["Win32_Security", "Win32_Security_Authorization", "Win32_System_Threading"] }
[target.'cfg(unix)'.dependencies]
rustix = { version = "0.38", features = ["fs"] }
[dev-dependencies]
tempfile = "3.10"

81
helix-stdx/src/env.rs Normal file
View File

@@ -0,0 +1,81 @@
use std::{
ffi::OsStr,
path::{Path, PathBuf},
sync::RwLock,
};
static CWD: RwLock<Option<PathBuf>> = RwLock::new(None);
// Get the current working directory.
// This information is managed internally as the call to std::env::current_dir
// might fail if the cwd has been deleted.
pub fn current_working_dir() -> PathBuf {
if let Some(path) = &*CWD.read().unwrap() {
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());
path
}
pub fn set_current_working_dir(path: impl AsRef<Path>) -> std::io::Result<()> {
let path = crate::path::canonicalize(path);
std::env::set_current_dir(&path)?;
let mut cwd = CWD.write().unwrap();
*cwd = Some(path);
Ok(())
}
pub fn env_var_is_set(env_var_name: &str) -> bool {
std::env::var_os(env_var_name).is_some()
}
pub fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
which::which(binary_name).is_ok()
}
pub fn which<T: AsRef<OsStr>>(
binary_name: T,
) -> Result<std::path::PathBuf, ExecutableNotFoundError> {
let binary_name = binary_name.as_ref();
which::which(binary_name).map_err(|err| ExecutableNotFoundError {
command: binary_name.to_string_lossy().into_owned(),
inner: err,
})
}
#[derive(Debug)]
pub struct ExecutableNotFoundError {
command: String,
inner: which::Error,
}
impl std::fmt::Display for ExecutableNotFoundError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "command '{}' not found: {}", self.command, self.inner)
}
}
impl std::error::Error for ExecutableNotFoundError {}
#[cfg(test)]
mod tests {
use super::{current_working_dir, set_current_working_dir};
#[test]
fn current_dir_is_set() {
let new_path = dunce::canonicalize(std::env::temp_dir()).unwrap();
let cwd = current_working_dir();
assert_ne!(cwd, new_path);
set_current_working_dir(&new_path).expect("Couldn't set new path");
let cwd = current_working_dir();
assert_eq!(cwd, new_path);
}
}

459
helix-stdx/src/faccess.rs Normal file
View File

@@ -0,0 +1,459 @@
//! From <https://github.com/Freaky/faccess>
use std::io;
use std::path::Path;
use bitflags::bitflags;
// Licensed under MIT from faccess
bitflags! {
/// Access mode flags for `access` function to test for.
pub struct AccessMode: u8 {
/// Path exists
const EXISTS = 0b0001;
/// Path can likely be read
const READ = 0b0010;
/// Path can likely be written to
const WRITE = 0b0100;
/// Path can likely be executed
const EXECUTE = 0b1000;
}
}
#[cfg(unix)]
mod imp {
use super::*;
use rustix::fs::Access;
use std::os::unix::fs::{MetadataExt, PermissionsExt};
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
let mut imode = Access::empty();
if mode.contains(AccessMode::EXISTS) {
imode |= Access::EXISTS;
}
if mode.contains(AccessMode::READ) {
imode |= Access::READ_OK;
}
if mode.contains(AccessMode::WRITE) {
imode |= Access::WRITE_OK;
}
if mode.contains(AccessMode::EXECUTE) {
imode |= Access::EXEC_OK;
}
rustix::fs::access(p, imode)?;
Ok(())
}
fn chown(p: &Path, uid: Option<u32>, gid: Option<u32>) -> io::Result<()> {
let uid = uid.map(|n| unsafe { rustix::fs::Uid::from_raw(n) });
let gid = gid.map(|n| unsafe { rustix::fs::Gid::from_raw(n) });
rustix::fs::chown(p, uid, gid)?;
Ok(())
}
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
let from_meta = std::fs::metadata(from)?;
let to_meta = std::fs::metadata(to)?;
let from_gid = from_meta.gid();
let to_gid = to_meta.gid();
let mut perms = from_meta.permissions();
perms.set_mode(perms.mode() & 0o0777);
if from_gid != to_gid && chown(to, None, Some(from_gid)).is_err() {
let new_perms = (perms.mode() & 0o0707) | ((perms.mode() & 0o07) << 3);
perms.set_mode(new_perms);
}
std::fs::set_permissions(to, perms)?;
Ok(())
}
}
// Licensed under MIT from faccess except for `chown`, `copy_metadata` and `is_acl_inherited`
#[cfg(windows)]
mod imp {
use windows_sys::Win32::Foundation::{CloseHandle, LocalFree, ERROR_SUCCESS, HANDLE, PSID};
use windows_sys::Win32::Security::Authorization::{
GetNamedSecurityInfoW, SetNamedSecurityInfoW, SE_FILE_OBJECT,
};
use windows_sys::Win32::Security::{
AccessCheck, AclSizeInformation, GetAce, GetAclInformation, GetSidIdentifierAuthority,
ImpersonateSelf, IsValidAcl, IsValidSid, MapGenericMask, RevertToSelf,
SecurityImpersonation, ACCESS_ALLOWED_CALLBACK_ACE, ACL, ACL_SIZE_INFORMATION,
DACL_SECURITY_INFORMATION, GENERIC_MAPPING, GROUP_SECURITY_INFORMATION, INHERITED_ACE,
LABEL_SECURITY_INFORMATION, OBJECT_SECURITY_INFORMATION, OWNER_SECURITY_INFORMATION,
PRIVILEGE_SET, PROTECTED_DACL_SECURITY_INFORMATION, PSECURITY_DESCRIPTOR,
SID_IDENTIFIER_AUTHORITY, TOKEN_DUPLICATE, TOKEN_QUERY,
};
use windows_sys::Win32::Storage::FileSystem::{
FILE_ACCESS_RIGHTS, FILE_ALL_ACCESS, FILE_GENERIC_EXECUTE, FILE_GENERIC_READ,
FILE_GENERIC_WRITE,
};
use windows_sys::Win32::System::Threading::{GetCurrentThread, OpenThreadToken};
use super::*;
use std::ffi::c_void;
use std::os::windows::{ffi::OsStrExt, fs::OpenOptionsExt};
struct SecurityDescriptor {
sd: PSECURITY_DESCRIPTOR,
owner: PSID,
group: PSID,
dacl: *mut ACL,
}
impl Drop for SecurityDescriptor {
fn drop(&mut self) {
if !self.sd.is_null() {
unsafe {
LocalFree(self.sd);
}
}
}
}
impl SecurityDescriptor {
fn for_path(p: &Path) -> io::Result<SecurityDescriptor> {
let path = std::fs::canonicalize(p)?;
let pathos = path.into_os_string();
let mut pathw: Vec<u16> = Vec::with_capacity(pathos.len() + 1);
pathw.extend(pathos.encode_wide());
pathw.push(0);
let mut sd = std::ptr::null_mut();
let mut owner = std::ptr::null_mut();
let mut group = std::ptr::null_mut();
let mut dacl = std::ptr::null_mut();
let err = unsafe {
GetNamedSecurityInfoW(
pathw.as_ptr(),
SE_FILE_OBJECT,
OWNER_SECURITY_INFORMATION
| GROUP_SECURITY_INFORMATION
| DACL_SECURITY_INFORMATION
| LABEL_SECURITY_INFORMATION,
&mut owner,
&mut group,
&mut dacl,
std::ptr::null_mut(),
&mut sd,
)
};
if err == ERROR_SUCCESS {
Ok(SecurityDescriptor {
sd,
owner,
group,
dacl,
})
} else {
Err(io::Error::last_os_error())
}
}
fn is_acl_inherited(&self) -> bool {
let mut acl_info: ACL_SIZE_INFORMATION = unsafe { ::core::mem::zeroed() };
let acl_info_ptr: *mut c_void = &mut acl_info as *mut _ as *mut c_void;
let mut ace: ACCESS_ALLOWED_CALLBACK_ACE = unsafe { ::core::mem::zeroed() };
unsafe {
GetAclInformation(
self.dacl,
acl_info_ptr,
std::mem::size_of_val(&acl_info) as u32,
AclSizeInformation,
)
};
for i in 0..acl_info.AceCount {
let mut ptr = &mut ace as *mut _ as *mut c_void;
unsafe { GetAce(self.dacl, i, &mut ptr) };
if (ace.Header.AceFlags as u32 & INHERITED_ACE) != 0 {
return true;
}
}
false
}
fn descriptor(&self) -> &PSECURITY_DESCRIPTOR {
&self.sd
}
fn owner(&self) -> &PSID {
&self.owner
}
}
struct ThreadToken(HANDLE);
impl Drop for ThreadToken {
fn drop(&mut self) {
unsafe {
CloseHandle(self.0);
}
}
}
impl ThreadToken {
fn new() -> io::Result<Self> {
unsafe {
if ImpersonateSelf(SecurityImpersonation) == 0 {
return Err(io::Error::last_os_error());
}
let token: *mut HANDLE = std::ptr::null_mut();
let err =
OpenThreadToken(GetCurrentThread(), TOKEN_DUPLICATE | TOKEN_QUERY, 0, token);
RevertToSelf();
if err == 0 {
return Err(io::Error::last_os_error());
}
Ok(Self(*token))
}
}
fn as_handle(&self) -> &HANDLE {
&self.0
}
}
// Based roughly on Tcl's NativeAccess()
// https://github.com/tcltk/tcl/blob/2ee77587e4dc2150deb06b48f69db948b4ab0584/win/tclWinFile.c
fn eaccess(p: &Path, mut mode: FILE_ACCESS_RIGHTS) -> io::Result<()> {
let md = p.metadata()?;
if !md.is_dir() {
// Read Only is ignored for directories
if mode & FILE_GENERIC_WRITE == FILE_GENERIC_WRITE && md.permissions().readonly() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"File is read only",
));
}
// If it doesn't have the correct extension it isn't executable
if mode & FILE_GENERIC_EXECUTE == FILE_GENERIC_EXECUTE {
if let Some(ext) = p.extension().and_then(|s| s.to_str()) {
match ext {
"exe" | "com" | "bat" | "cmd" => (),
_ => {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
"File not executable",
))
}
}
}
}
return std::fs::OpenOptions::new()
.access_mode(mode)
.open(p)
.map(|_| ());
}
let sd = SecurityDescriptor::for_path(p)?;
// Unmapped Samba users are assigned a top level authority of 22
// ACL tests are likely to be misleading
const SAMBA_UNMAPPED: SID_IDENTIFIER_AUTHORITY = SID_IDENTIFIER_AUTHORITY {
Value: [0, 0, 0, 0, 0, 22],
};
unsafe {
let owner = sd.owner();
if IsValidSid(*owner) != 0
&& (*GetSidIdentifierAuthority(*owner)).Value == SAMBA_UNMAPPED.Value
{
return Ok(());
}
}
let token = ThreadToken::new()?;
let mut privileges: PRIVILEGE_SET = unsafe { std::mem::zeroed() };
let mut granted_access: u32 = 0;
let mut privileges_length = std::mem::size_of::<PRIVILEGE_SET>() as u32;
let mut result = 0;
let mut mapping = GENERIC_MAPPING {
GenericRead: FILE_GENERIC_READ,
GenericWrite: FILE_GENERIC_WRITE,
GenericExecute: FILE_GENERIC_EXECUTE,
GenericAll: FILE_ALL_ACCESS,
};
unsafe { MapGenericMask(&mut mode, &mut mapping) };
if unsafe {
AccessCheck(
*sd.descriptor(),
*token.as_handle(),
mode,
&mut mapping,
&mut privileges,
&mut privileges_length,
&mut granted_access,
&mut result,
)
} != 0
{
if result == 0 {
Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Permission Denied",
))
} else {
Ok(())
}
} else {
Err(io::Error::last_os_error())
}
}
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
let mut imode = 0;
if mode.contains(AccessMode::READ) {
imode |= FILE_GENERIC_READ;
}
if mode.contains(AccessMode::WRITE) {
imode |= FILE_GENERIC_WRITE;
}
if mode.contains(AccessMode::EXECUTE) {
imode |= FILE_GENERIC_EXECUTE;
}
if imode == 0 {
if p.exists() {
Ok(())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "Not Found"))
}
} else {
eaccess(p, imode)
}
}
fn chown(p: &Path, sd: SecurityDescriptor) -> io::Result<()> {
let path = std::fs::canonicalize(p)?;
let pathos = path.as_os_str();
let mut pathw = Vec::with_capacity(pathos.len() + 1);
pathw.extend(pathos.encode_wide());
pathw.push(0);
let mut owner = std::ptr::null_mut();
let mut group = std::ptr::null_mut();
let mut dacl = std::ptr::null();
let mut si = OBJECT_SECURITY_INFORMATION::default();
if unsafe { IsValidSid(sd.owner) } != 0 {
si |= OWNER_SECURITY_INFORMATION;
owner = sd.owner;
}
if unsafe { IsValidSid(sd.group) } != 0 {
si |= GROUP_SECURITY_INFORMATION;
group = sd.group;
}
if unsafe { IsValidAcl(sd.dacl) } != 0 {
si |= DACL_SECURITY_INFORMATION;
if !sd.is_acl_inherited() {
si |= PROTECTED_DACL_SECURITY_INFORMATION;
}
dacl = sd.dacl as *const _;
}
let err = unsafe {
SetNamedSecurityInfoW(
pathw.as_ptr(),
SE_FILE_OBJECT,
si,
owner,
group,
dacl,
std::ptr::null(),
)
};
if err == ERROR_SUCCESS {
Ok(())
} else {
Err(io::Error::last_os_error())
}
}
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
let sd = SecurityDescriptor::for_path(from)?;
chown(to, sd)?;
let meta = std::fs::metadata(from)?;
let perms = meta.permissions();
std::fs::set_permissions(to, perms)?;
Ok(())
}
}
// Licensed under MIT from faccess except for `copy_metadata`
#[cfg(not(any(unix, windows)))]
mod imp {
use super::*;
pub fn access(p: &Path, mode: AccessMode) -> io::Result<()> {
if mode.contains(AccessMode::WRITE) {
if std::fs::metadata(p)?.permissions().readonly() {
return Err(io::Error::new(
io::ErrorKind::PermissionDenied,
"Path is read only",
));
} else {
return Ok(());
}
}
if p.exists() {
Ok(())
} else {
Err(io::Error::new(io::ErrorKind::NotFound, "Path not found"))
}
}
pub fn copy_metadata(from: &path, to: &Path) -> io::Result<()> {
let meta = std::fs::metadata(from)?;
let perms = meta.permissions();
std::fs::set_permissions(to, perms)?;
Ok(())
}
}
pub fn readonly(p: &Path) -> bool {
match imp::access(p, AccessMode::WRITE) {
Ok(_) => false,
Err(err) if err.kind() == std::io::ErrorKind::NotFound => false,
Err(_) => true,
}
}
pub fn copy_metadata(from: &Path, to: &Path) -> io::Result<()> {
imp::copy_metadata(from, to)
}

4
helix-stdx/src/lib.rs Normal file
View File

@@ -0,0 +1,4 @@
pub mod env;
pub mod faccess;
pub mod path;
pub mod rope;

231
helix-stdx/src/path.rs Normal file
View File

@@ -0,0 +1,231 @@
pub use etcetera::home_dir;
use std::{
borrow::Cow,
ffi::OsString,
path::{Component, Path, PathBuf, MAIN_SEPARATOR_STR},
};
use crate::env::current_working_dir;
/// Replaces users home directory from `path` with tilde `~` if the directory
/// is available, otherwise returns the path unchanged.
pub fn fold_home_dir<'a, P>(path: P) -> Cow<'a, Path>
where
P: Into<Cow<'a, Path>>,
{
let path = path.into();
if let Ok(home) = home_dir() {
if let Ok(stripped) = path.strip_prefix(&home) {
let mut path = OsString::with_capacity(2 + stripped.as_os_str().len());
path.push("~");
path.push(MAIN_SEPARATOR_STR);
path.push(stripped);
return Cow::Owned(PathBuf::from(path));
}
}
path
}
/// Expands tilde `~` into users home directory if available, otherwise returns the path
/// unchanged. The tilde will only be expanded when present as the first component of the path
/// and only slash follows it.
pub fn expand_tilde<'a, P>(path: P) -> Cow<'a, Path>
where
P: Into<Cow<'a, Path>>,
{
let path = path.into();
let mut components = path.components();
if let Some(Component::Normal(c)) = components.next() {
if c == "~" {
if let Ok(mut buf) = home_dir() {
buf.push(components);
return Cow::Owned(buf);
}
}
}
path
}
/// Normalize a path without resolving symlinks.
// Strategy: start from the first component and move up. Cannonicalize previous path,
// join component, cannonicalize new path, strip prefix and join to the final result.
pub fn normalize(path: impl AsRef<Path>) -> PathBuf {
let mut components = path.as_ref().components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
#[cfg(not(windows))]
Component::ParentDir => {
ret.pop();
}
#[cfg(windows)]
Component::ParentDir => {
if let Some(head) = ret.components().next_back() {
match head {
Component::Prefix(_) | Component::RootDir => {}
Component::CurDir => unreachable!(),
// If we left previous component as ".." it means we met a symlink before and we can't pop path.
Component::ParentDir => {
ret.push("..");
}
Component::Normal(_) => {
if ret.is_symlink() {
ret.push("..");
} else {
ret.pop();
}
}
}
}
}
#[cfg(not(windows))]
Component::Normal(c) => {
ret.push(c);
}
#[cfg(windows)]
Component::Normal(c) => 'normal: {
use std::fs::canonicalize;
let new_path = ret.join(c);
if new_path.is_symlink() {
ret = new_path;
break 'normal;
}
let (can_new, can_old) = (canonicalize(&new_path), canonicalize(&ret));
match (can_new, can_old) {
(Ok(can_new), Ok(can_old)) => {
let striped = can_new.strip_prefix(can_old);
ret.push(striped.unwrap_or_else(|_| c.as_ref()));
}
_ => ret.push(c),
}
}
}
}
dunce::simplified(&ret).to_path_buf()
}
/// Returns the canonical, absolute form of a path with all intermediate components normalized.
///
/// This function is used instead of [`std::fs::canonicalize`] because we don't want to verify
/// here if the path exists, just normalize it's components.
pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
let path = expand_tilde(path.as_ref());
let path = if path.is_relative() {
Cow::Owned(current_working_dir().join(path))
} else {
path
};
normalize(path)
}
pub fn get_relative_path<'a, P>(path: P) -> Cow<'a, Path>
where
P: Into<Cow<'a, Path>>,
{
let path = path.into();
if path.is_absolute() {
let cwdir = normalize(current_working_dir());
if let Ok(stripped) = normalize(&path).strip_prefix(cwdir) {
return Cow::Owned(PathBuf::from(stripped));
}
return fold_home_dir(path);
}
path
}
/// Returns a truncated filepath where the basepart of the path is reduced to the first
/// char of the folder and the whole filename appended.
///
/// Also strip the current working directory from the beginning of the path.
/// Note that this function does not check if the truncated path is unambiguous.
///
/// ```
/// use helix_stdx::path::get_truncated_path;
/// use std::path::Path;
///
/// assert_eq!(
/// get_truncated_path("/home/cnorris/documents/jokes.txt").as_path(),
/// Path::new("/h/c/d/jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("jokes.txt").as_path(),
/// Path::new("jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("/jokes.txt").as_path(),
/// Path::new("/jokes.txt")
/// );
/// assert_eq!(
/// get_truncated_path("/h/c/d/jokes.txt").as_path(),
/// Path::new("/h/c/d/jokes.txt")
/// );
/// assert_eq!(get_truncated_path("").as_path(), Path::new(""));
/// ```
///
pub fn get_truncated_path(path: impl AsRef<Path>) -> PathBuf {
let cwd = current_working_dir();
let path = path.as_ref();
let path = path.strip_prefix(cwd).unwrap_or(path);
let file = path.file_name().unwrap_or_default();
let base = path.parent().unwrap_or_else(|| Path::new(""));
let mut ret = PathBuf::with_capacity(file.len());
// A char can't be directly pushed to a PathBuf
let mut first_char_buffer = String::new();
for d in base {
let Some(first_char) = d.to_string_lossy().chars().next() else {
break;
};
first_char_buffer.push(first_char);
ret.push(&first_char_buffer);
first_char_buffer.clear();
}
ret.push(file);
ret
}
#[cfg(test)]
mod tests {
use std::{
ffi::OsStr,
path::{Component, Path},
};
use crate::path;
#[test]
fn expand_tilde() {
for path in ["~", "~/foo"] {
let expanded = path::expand_tilde(Path::new(path));
let tilde = Component::Normal(OsStr::new("~"));
let mut component_count = 0;
for component in expanded.components() {
// No tilde left.
assert_ne!(component, tilde);
component_count += 1;
}
// The path was at least expanded to something.
assert_ne!(component_count, 0);
}
}
}

140
helix-stdx/src/rope.rs Normal file
View File

@@ -0,0 +1,140 @@
use std::ops::{Bound, RangeBounds};
pub use regex_cursor::engines::meta::{Builder as RegexBuilder, Regex};
pub use regex_cursor::regex_automata::util::syntax::Config;
use regex_cursor::{Input as RegexInput, RopeyCursor};
use ropey::str_utils::byte_to_char_idx;
use ropey::RopeSlice;
pub trait RopeSliceExt<'a>: Sized {
fn ends_with(self, text: &str) -> bool;
fn starts_with(self, text: &str) -> bool;
fn regex_input(self) -> RegexInput<RopeyCursor<'a>>;
fn regex_input_at_bytes<R: RangeBounds<usize>>(
self,
byte_range: R,
) -> RegexInput<RopeyCursor<'a>>;
fn regex_input_at<R: RangeBounds<usize>>(self, char_range: R) -> RegexInput<RopeyCursor<'a>>;
fn first_non_whitespace_char(self) -> Option<usize>;
fn last_non_whitespace_char(self) -> Option<usize>;
/// returns the char idx of `byte_idx`, if `byte_idx` is a char boundary
/// this function behaves the same as `byte_to_char` but if `byte_idx` is
/// not a valid char boundary (so within a char) this will return the next
/// char index.
///
/// # Example
///
/// ```
/// # use ropey::RopeSlice;
/// # use helix_stdx::rope::RopeSliceExt;
/// let text = RopeSlice::from("😆");
/// for i in 1..text.len_bytes() {
/// assert_eq!(text.byte_to_char(i), 0);
/// assert_eq!(text.byte_to_next_char(i), 1);
/// }
/// ```
fn byte_to_next_char(self, byte_idx: usize) -> usize;
}
impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
fn ends_with(self, text: &str) -> bool {
let len = self.len_bytes();
if len < text.len() {
return false;
}
self.get_byte_slice(len - text.len()..)
.map_or(false, |end| end == text)
}
fn starts_with(self, text: &str) -> bool {
let len = self.len_bytes();
if len < text.len() {
return false;
}
self.get_byte_slice(..len - text.len())
.map_or(false, |start| start == text)
}
fn regex_input(self) -> RegexInput<RopeyCursor<'a>> {
RegexInput::new(self)
}
fn regex_input_at<R: RangeBounds<usize>>(self, char_range: R) -> RegexInput<RopeyCursor<'a>> {
let start_bound = match char_range.start_bound() {
Bound::Included(&val) => Bound::Included(self.char_to_byte(val)),
Bound::Excluded(&val) => Bound::Excluded(self.char_to_byte(val)),
Bound::Unbounded => Bound::Unbounded,
};
let end_bound = match char_range.end_bound() {
Bound::Included(&val) => Bound::Included(self.char_to_byte(val)),
Bound::Excluded(&val) => Bound::Excluded(self.char_to_byte(val)),
Bound::Unbounded => Bound::Unbounded,
};
self.regex_input_at_bytes((start_bound, end_bound))
}
fn regex_input_at_bytes<R: RangeBounds<usize>>(
self,
byte_range: R,
) -> RegexInput<RopeyCursor<'a>> {
let input = match byte_range.start_bound() {
Bound::Included(&pos) | Bound::Excluded(&pos) => {
RegexInput::new(RopeyCursor::at(self, pos))
}
Bound::Unbounded => RegexInput::new(self),
};
input.range(byte_range)
}
fn first_non_whitespace_char(self) -> Option<usize> {
self.chars().position(|ch| !ch.is_whitespace())
}
fn last_non_whitespace_char(self) -> Option<usize> {
self.chars_at(self.len_chars())
.reversed()
.position(|ch| !ch.is_whitespace())
.map(|pos| self.len_chars() - pos - 1)
}
/// returns the char idx of `byte_idx`, if `byte_idx` is
/// a char boundary this function behaves the same as `byte_to_char`
fn byte_to_next_char(self, mut byte_idx: usize) -> usize {
let (chunk, chunk_byte_off, chunk_char_off, _) = self.chunk_at_byte(byte_idx);
byte_idx -= chunk_byte_off;
let is_char_boundary =
is_utf8_char_boundary(chunk.as_bytes().get(byte_idx).copied().unwrap_or(0));
chunk_char_off + byte_to_char_idx(chunk, byte_idx) + !is_char_boundary as usize
}
}
// copied from std
#[inline]
const fn is_utf8_char_boundary(b: u8) -> bool {
// This is bit magic equivalent to: b < 128 || b >= 192
(b as i8) >= -0x40
}
#[cfg(test)]
mod tests {
use ropey::RopeSlice;
use crate::rope::RopeSliceExt;
#[test]
fn next_char_at_byte() {
for i in 0..=6 {
assert_eq!(RopeSlice::from("foobar").byte_to_next_char(i), i);
}
for char_idx in 0..10 {
let len = "😆".len();
assert_eq!(
RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len),
char_idx
);
for i in 1..=len {
assert_eq!(
RopeSlice::from("😆😆😆😆😆😆😆😆😆😆").byte_to_next_char(char_idx * len + i),
char_idx + 1
);
}
}
}
}

124
helix-stdx/tests/path.rs Normal file
View File

@@ -0,0 +1,124 @@
#![cfg(windows)]
use std::{
env::set_current_dir,
error::Error,
path::{Component, Path, PathBuf},
};
use helix_stdx::path;
use tempfile::Builder;
// Paths on Windows are almost always case-insensitive.
// Normalization should return the original path.
// E.g. mkdir `CaSe`, normalize(`case`) = `CaSe`.
#[test]
fn test_case_folding_windows() -> Result<(), Box<dyn Error>> {
// tmp/root/case
let tmp_prefix = std::env::temp_dir();
set_current_dir(&tmp_prefix)?;
let root = Builder::new().prefix("root-").tempdir()?;
let case = Builder::new().prefix("CaSe-").tempdir_in(&root)?;
let root_without_prefix = root.path().strip_prefix(&tmp_prefix)?;
let lowercase_case = format!(
"case-{}",
case.path()
.file_name()
.unwrap()
.to_string_lossy()
.split_at(5)
.1
);
let test_path = root_without_prefix.join(lowercase_case);
assert_eq!(
path::normalize(&test_path),
case.path().strip_prefix(&tmp_prefix)?
);
Ok(())
}
#[test]
fn test_normalize_path() -> Result<(), Box<dyn Error>> {
/*
tmp/root/
link -> dir1/orig_file
dir1/
orig_file
dir2/
dir_link -> ../dir1/
*/
let tmp_prefix = std::env::temp_dir();
set_current_dir(&tmp_prefix)?;
// Create a tree structure as shown above
let root = Builder::new().prefix("root-").tempdir()?;
let dir1 = Builder::new().prefix("dir1-").tempdir_in(&root)?;
let orig_file = Builder::new().prefix("orig_file-").tempfile_in(&dir1)?;
let dir2 = Builder::new().prefix("dir2-").tempdir_in(&root)?;
// Create path and delete existing file
let dir_link = Builder::new()
.prefix("dir_link-")
.tempfile_in(&dir2)?
.path()
.to_owned();
let link = Builder::new()
.prefix("link-")
.tempfile_in(&root)?
.path()
.to_owned();
use std::os::windows;
windows::fs::symlink_dir(&dir1, &dir_link)?;
windows::fs::symlink_file(&orig_file, &link)?;
// root/link
let path = link.strip_prefix(&tmp_prefix)?;
assert_eq!(
path::normalize(path),
path,
"input {:?} and symlink last component shouldn't be resolved",
path
);
// root/dir2/dir_link/orig_file/../..
let path = dir_link
.strip_prefix(&tmp_prefix)
.unwrap()
.join(orig_file.path().file_name().unwrap())
.join(Component::ParentDir)
.join(Component::ParentDir);
let expected = dir_link
.strip_prefix(&tmp_prefix)
.unwrap()
.join(Component::ParentDir);
assert_eq!(
path::normalize(&path),
expected,
"input {:?} and \"..\" should not erase the simlink that goes ahead",
&path
);
// root/link/.././../dir2/../
let path = link
.strip_prefix(&tmp_prefix)
.unwrap()
.join(Component::ParentDir)
.join(Component::CurDir)
.join(Component::ParentDir)
.join(dir2.path().file_name().unwrap())
.join(Component::ParentDir);
let expected = link
.strip_prefix(&tmp_prefix)
.unwrap()
.join(Component::ParentDir)
.join(Component::ParentDir);
assert_eq!(path::normalize(&path), expected, "input {:?}", &path);
Ok(())
}

View File

@@ -1,21 +1,21 @@
[package]
name = "helix-term"
version = "0.6.0"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2021"
license = "MPL-2.0"
categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
default-run = "hx"
rust-version = "1.65"
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]
default = ["git"]
unicode-lines = ["helix-core/unicode-lines"]
integration = []
integration = ["helix-event/integration_test"]
git = ["helix-vcs/git"]
[[bin]]
@@ -23,18 +23,17 @@ name = "hx"
path = "src/main.rs"
[dependencies]
helix-core = { version = "0.6", path = "../helix-core" }
helix-event = { version = "0.6", path = "../helix-event" }
helix-view = { version = "0.6", path = "../helix-view" }
helix-lsp = { version = "0.6", path = "../helix-lsp" }
helix-dap = { version = "0.6", path = "../helix-dap" }
helix-vcs = { version = "0.6", path = "../helix-vcs" }
helix-loader = { version = "0.6", path = "../helix-loader" }
helix-stdx = { path = "../helix-stdx" }
helix-core = { path = "../helix-core" }
helix-event = { path = "../helix-event" }
helix-view = { path = "../helix-view" }
helix-lsp = { path = "../helix-lsp" }
helix-dap = { path = "../helix-dap" }
helix-vcs = { path = "../helix-vcs" }
helix-loader = { path = "../helix-loader" }
anyhow = "1"
once_cell = "1.18"
which = "4.4"
once_cell = "1.19"
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
@@ -42,7 +41,8 @@ crossterm = { version = "0.27", features = ["event-stream"] }
signal-hook = "0.3"
tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
arc-swap = { version = "1.6.0" }
arc-swap = { version = "1.7.1" }
termini = "1"
# Logging
fern = "0.6"
@@ -53,31 +53,35 @@ log = "0.4"
nucleo.workspace = true
ignore = "0.4"
# markdown doc rendering
pulldown-cmark = { version = "0.9", default-features = false }
pulldown-cmark = { version = "0.10", default-features = false }
# file type detection
content_inspector = "0.2.4"
# opening URLs
open = "5.1.2"
url = "2.5.0"
# config
toml = "0.7"
toml = "0.8"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
# ripgrep for global search
grep-regex = "0.1.11"
grep-searcher = "0.1.11"
grep-regex = "0.1.12"
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.149"
libc = "0.2.153"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
[build-dependencies]
helix-loader = { version = "0.6", path = "../helix-loader" }
helix-loader = { path = "../helix-loader" }
[dev-dependencies]
smallvec = "1.11"
indoc = "2.0.4"
tempfile = "3.8.0"
smallvec = "1.13"
indoc = "2.0.5"
tempfile = "3.10.1"

View File

@@ -6,4 +6,150 @@ fn main() {
build_grammars(Some(std::env::var("TARGET").unwrap()))
.expect("Failed to compile tree-sitter grammars");
}
#[cfg(windows)]
windows_rc::link_icon_in_windows_exe("../contrib/helix-256p.ico");
}
#[cfg(windows)]
mod windows_rc {
use std::io::prelude::Write;
use std::{env, io, path::Path, path::PathBuf, process};
pub(crate) fn link_icon_in_windows_exe(icon_path: &str) {
let rc_exe = find_rc_exe().expect("Windows SDK is to be installed along with MSVC");
let output = env::var("OUT_DIR").expect("Env var OUT_DIR should have been set by compiler");
let output_dir = PathBuf::from(output);
let rc_path = output_dir.join("resource.rc");
write_resource_file(&rc_path, icon_path).unwrap();
let resource_file = PathBuf::from(&output_dir).join("resource.lib");
compile_with_toolkit_msvc(rc_exe, resource_file, rc_path);
println!("cargo:rustc-link-search=native={}", output_dir.display());
println!("cargo:rustc-link-lib=dylib=resource");
}
fn compile_with_toolkit_msvc(rc_exe: PathBuf, output: PathBuf, input: PathBuf) {
let mut command = process::Command::new(rc_exe);
let command = command.arg(format!(
"/I{}",
env::var("CARGO_MANIFEST_DIR")
.expect("CARGO_MANIFEST_DIR should have been set by Cargo")
));
let status = command
.arg(format!("/fo{}", output.display()))
.arg(format!("{}", input.display()))
.output()
.unwrap();
println!(
"RC Output:\n{}\n------",
String::from_utf8_lossy(&status.stdout)
);
println!(
"RC Error:\n{}\n------",
String::from_utf8_lossy(&status.stderr)
);
}
fn find_rc_exe() -> io::Result<PathBuf> {
let find_reg_key = process::Command::new("reg")
.arg("query")
.arg(r"HKLM\SOFTWARE\Microsoft\Windows Kits\Installed Roots")
.arg("/reg:32")
.arg("/v")
.arg("KitsRoot10")
.output();
match find_reg_key {
Err(find_reg_key) => {
return Err(io::Error::new(
io::ErrorKind::Other,
format!("Failed to run registry query: {}", find_reg_key),
))
}
Ok(find_reg_key) => {
if find_reg_key.status.code().unwrap() != 0 {
return Err(io::Error::new(
io::ErrorKind::Other,
"Can not find Windows SDK",
));
} else {
let lines = String::from_utf8(find_reg_key.stdout)
.expect("Should be able to parse the output");
let mut lines: Vec<&str> = lines.lines().collect();
let mut rc_exe_paths: Vec<PathBuf> = Vec::new();
lines.reverse();
for line in lines {
if line.trim().starts_with("KitsRoot") {
let kit: String = line
.chars()
.skip(line.find("REG_SZ").unwrap() + 6)
.skip_while(|c| c.is_whitespace())
.collect();
let p = PathBuf::from(&kit);
let rc = if cfg!(target_arch = "x86_64") {
p.join(r"bin\x64\rc.exe")
} else {
p.join(r"bin\x86\rc.exe")
};
if rc.exists() {
println!("{:?}", rc);
rc_exe_paths.push(rc.to_owned());
}
if let Ok(bin) = p.join("bin").read_dir() {
for e in bin.filter_map(|e| e.ok()) {
let p = if cfg!(target_arch = "x86_64") {
e.path().join(r"x64\rc.exe")
} else {
e.path().join(r"x86\rc.exe")
};
if p.exists() {
println!("{:?}", p);
rc_exe_paths.push(p.to_owned());
}
}
}
}
}
if rc_exe_paths.is_empty() {
return Err(io::Error::new(
io::ErrorKind::Other,
"Can not find Windows SDK",
));
}
println!("{:?}", rc_exe_paths);
let rc_path = rc_exe_paths.pop().unwrap();
let rc_exe = if !rc_path.exists() {
if cfg!(target_arch = "x86_64") {
PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x64\rc.exe")
} else {
PathBuf::from(rc_path.parent().unwrap()).join(r"bin\x86\rc.exe")
}
} else {
rc_path
};
println!("Selected RC path: '{}'", rc_exe.display());
Ok(rc_exe)
}
}
}
}
fn write_resource_file(rc_path: &Path, icon_path: &str) -> io::Result<()> {
let mut f = std::fs::File::create(rc_path)?;
writeln!(f, "{} ICON \"{}\"", 1, icon_path)?;
Ok(())
}
}

View File

@@ -1,15 +1,12 @@
use arc_swap::{access::Map, ArcSwap};
use futures_util::Stream;
use helix_core::{
diagnostic::{DiagnosticTag, NumberOrString},
path::get_relative_path,
pos_at_coords, syntax, Selection,
};
use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
use helix_lsp::{
lsp::{self, notification::Notification},
util::lsp_pos_to_pos,
LspProgressMap,
util::lsp_range_to_range,
LanguageServerId, LspProgressMap,
};
use helix_stdx::path::get_relative_path;
use helix_view::{
align_view,
document::DocumentSavedEventResult,
@@ -24,15 +21,15 @@ use tui::backend::Backend;
use crate::{
args::Args,
commands::apply_workspace_edit,
compositor::{Compositor, Event},
config::Config,
handlers,
job::Jobs,
keymap::Keymaps,
ui::{self, overlay::overlaid},
};
use log::{debug, error, warn};
use log::{debug, error, info, warn};
#[cfg(not(feature = "integration"))]
use std::io::stdout;
use std::{collections::btree_map::Entry, io::stdin, path::Path, sync::Arc};
@@ -69,7 +66,7 @@ pub struct Application {
#[allow(dead_code)]
theme_loader: Arc<theme::Loader>,
#[allow(dead_code)]
syn_loader: Arc<syntax::Loader>,
syn_loader: Arc<ArcSwap<syntax::Loader>>,
signals: Signals,
jobs: Jobs,
@@ -99,11 +96,7 @@ fn setup_integration_logging() {
}
impl Application {
pub fn new(
args: Args,
config: Config,
syn_loader_conf: syntax::Configuration,
) -> Result<Self, Error> {
pub fn new(args: Args, config: Config, lang_loader: syntax::Loader) -> Result<Self, Error> {
#[cfg(feature = "integration")]
setup_integration_logging();
@@ -129,7 +122,7 @@ impl Application {
})
.unwrap_or_else(|| theme_loader.default_theme(true_color));
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let syn_loader = Arc::new(ArcSwap::from_pointee(lang_loader));
#[cfg(not(feature = "integration"))]
let backend = CrosstermBackend::new(stdout(), &config.editor);
@@ -141,6 +134,7 @@ impl Application {
let area = terminal.size().expect("couldn't get terminal size");
let mut compositor = Compositor::new(area);
let config = Arc::new(ArcSwap::from_pointee(config));
let handlers = handlers::setup(config.clone());
let mut editor = Editor::new(
area,
theme_loader.clone(),
@@ -148,6 +142,7 @@ impl Application {
Arc::new(Map::new(Arc::clone(&config), |config: &Config| {
&config.editor
})),
handlers,
);
let keys = Box::new(Map::new(Arc::clone(&config), |config: &Config| {
@@ -162,14 +157,19 @@ impl Application {
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None);
} else if !args.files.is_empty() {
if args.open_cwd {
// NOTE: The working directory is already set to args.files[0] in main()
editor.new_file(Action::VerticalSplit);
let picker = ui::file_picker(".".into(), &config.load().editor);
let mut files_it = args.files.into_iter().peekable();
// If the first file is a directory, skip it and open a picker
if let Some((first, _)) = files_it.next_if(|(p, _)| p.is_dir()) {
let picker = ui::file_picker(first, &config.load().editor);
compositor.push(Box::new(overlaid(picker)));
} else {
let nr_of_files = args.files.len();
for (i, (file, pos)) in args.files.into_iter().enumerate() {
}
// If there are any more files specified, open them
if files_it.peek().is_some() {
let mut nr_of_files = 0;
for (file, pos) in files_it {
nr_of_files += 1;
if file.is_dir() {
return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
@@ -181,7 +181,7 @@ impl Application {
// option. If neither of those two arguments are passed
// in, just load the files normally.
let action = match args.split {
_ if i == 0 => Action::VerticalSplit,
_ if nr_of_files == 1 => Action::VerticalSplit,
Some(Layout::Vertical) => Action::VerticalSplit,
Some(Layout::Horizontal) => Action::HorizontalSplit,
None => Action::Load,
@@ -208,6 +208,8 @@ impl Application {
// 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);
}
} else if stdin().is_tty() || cfg!(feature = "integration") {
editor.new_file(Action::VerticalSplit);
@@ -317,10 +319,21 @@ impl Application {
Some(event) = input_stream.next() => {
self.handle_terminal_events(event).await;
}
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
Some(callback) = self.jobs.callbacks.recv() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, Ok(Some(callback)));
self.render().await;
}
Some(msg) = self.jobs.status_messages.recv() => {
let severity = match msg.severity{
helix_event::status::Severity::Hint => Severity::Hint,
helix_event::status::Severity::Info => Severity::Info,
helix_event::status::Severity::Warning => Severity::Warning,
helix_event::status::Severity::Error => Severity::Error,
};
// TODO: show multiple status messages at once to avoid clobbering
self.editor.status_msg = Some((msg.message, severity));
helix_event::request_redraw();
}
Some(callback) = self.jobs.wait_futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render().await;
@@ -377,13 +390,18 @@ impl Application {
/// refresh language config after config change
fn refresh_language_config(&mut self) -> Result<(), Error> {
let syntax_config = helix_core::config::user_syntax_loader()
.map_err(|err| anyhow::anyhow!("Failed to load language config: {}", err))?;
let lang_loader = helix_core::config::user_lang_loader()?;
self.syn_loader = std::sync::Arc::new(syntax::Loader::new(syntax_config));
self.syn_loader.store(Arc::new(lang_loader));
self.editor.syn_loader = self.syn_loader.clone();
for document in self.editor.documents.values_mut() {
document.detect_language(self.syn_loader.clone());
let diagnostics = Editor::doc_diagnostics(
&self.editor.language_servers,
&self.editor.diagnostics,
document,
);
document.replace_diagnostics(diagnostics, &[], None);
}
Ok(())
@@ -549,18 +567,8 @@ impl Application {
let lines = doc_save_event.text.len_lines();
let bytes = doc_save_event.text.len_bytes();
if doc.path() != Some(&doc_save_event.path) {
doc.set_path(Some(&doc_save_event.path));
let loader = self.editor.syn_loader.clone();
// borrowing the same doc again to get around the borrow checker
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let id = doc.id();
doc.detect_language(loader);
self.editor.refresh_language_servers(id);
}
self.editor
.set_doc_path(doc_save_event.doc_id, &doc_save_event.path);
// TODO: fix being overwritten by lsp
self.editor.set_status(format!(
"'{}' written, {}L {}B",
@@ -647,7 +655,7 @@ impl Application {
pub async fn handle_language_server_message(
&mut self,
call: helix_lsp::Call,
server_id: usize,
server_id: LanguageServerId,
) {
use helix_lsp::{Call, MethodCall, Notification};
@@ -667,9 +675,13 @@ impl Application {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) {
Ok(notification) => notification,
Err(helix_lsp::Error::Unhandled) => {
info!("Ignoring Unhandled notification from Language Server");
return;
}
Err(err) => {
log::error!(
"received malformed notification from Language Server: {}",
error!(
"Ignoring unknown notification from Language Server: {}",
err
);
return;
@@ -710,9 +722,9 @@ impl Application {
));
}
}
Notification::PublishDiagnostics(params) => {
Notification::PublishDiagnostics(mut params) => {
let path = match params.uri.to_file_path() {
Ok(path) => path,
Ok(path) => helix_stdx::path::normalize(&path),
Err(_) => {
log::error!("Unsupported file URI: {}", params.uri);
return;
@@ -723,144 +735,95 @@ impl Application {
log::error!("Discarding publishDiagnostic notification sent by an uninitialized server: {}", language_server.name());
return;
}
let offset_encoding = language_server.offset_encoding();
let doc = self.editor.document_by_path_mut(&path).filter(|doc| {
if let Some(version) = params.version {
if version != doc.version() {
log::info!("Version ({version}) is out of date for {path:?} (expected ({}), dropping PublishDiagnostic notification", doc.version());
return false;
// 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))
.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());
return false;
}
}
true
});
let mut unchanged_diag_sources = Vec::new();
if let Some(doc) = &doc {
let lang_conf = doc.language.clone();
if let Some(lang_conf) = &lang_conf {
if let Some(old_diagnostics) = self.editor.diagnostics.get(&path) {
if !lang_conf.persistent_diagnostic_sources.is_empty() {
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
params
.diagnostics
.sort_unstable_by_key(|d| (d.severity, d.range.start));
}
for source in &lang_conf.persistent_diagnostic_sources {
let new_diagnostics = params
.diagnostics
.iter()
.filter(|d| d.source.as_ref() == Some(source));
let old_diagnostics = old_diagnostics
.iter()
.filter(|(d, d_server)| {
*d_server == server_id
&& d.source.as_ref() == Some(source)
})
.map(|(d, _)| d);
if new_diagnostics.eq(old_diagnostics) {
unchanged_diag_sources.push(source.clone())
}
}
}
}
true
});
if let Some(doc) = doc {
let lang_conf = doc.language_config();
let text = doc.text();
let diagnostics = params
.diagnostics
.iter()
.filter_map(|diagnostic| {
use helix_core::diagnostic::{Diagnostic, Range, Severity::*};
use lsp::DiagnosticSeverity;
// TODO: convert inside server
let start = if let Some(start) = lsp_pos_to_pos(
text,
diagnostic.range.start,
offset_encoding,
) {
start
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
let end = if let Some(end) =
lsp_pos_to_pos(text, diagnostic.range.end, offset_encoding)
{
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
let severity =
diagnostic.severity.map(|severity| match severity {
DiagnosticSeverity::ERROR => Error,
DiagnosticSeverity::WARNING => Warning,
DiagnosticSeverity::INFORMATION => Info,
DiagnosticSeverity::HINT => Hint,
severity => unreachable!(
"unrecognized diagnostic severity: {:?}",
severity
),
});
if let Some(lang_conf) = lang_conf {
if let Some(severity) = severity {
if severity < lang_conf.diagnostic_severity {
return None;
}
}
};
let code = match diagnostic.code.clone() {
Some(x) => match x {
lsp::NumberOrString::Number(x) => {
Some(NumberOrString::Number(x))
}
lsp::NumberOrString::String(x) => {
Some(NumberOrString::String(x))
}
},
None => None,
};
let tags = if let Some(tags) = &diagnostic.tags {
let new_tags = tags
.iter()
.filter_map(|tag| match *tag {
lsp::DiagnosticTag::DEPRECATED => {
Some(DiagnosticTag::Deprecated)
}
lsp::DiagnosticTag::UNNECESSARY => {
Some(DiagnosticTag::Unnecessary)
}
_ => None,
})
.collect();
new_tags
} else {
Vec::new()
};
Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
message: diagnostic.message.clone(),
severity,
code,
tags,
source: diagnostic.source.clone(),
data: diagnostic.data.clone(),
language_server_id: server_id,
})
})
.collect();
doc.replace_diagnostics(diagnostics, server_id);
}
let mut diagnostics = params
.diagnostics
.into_iter()
.map(|d| (d, server_id))
.collect();
let diagnostics = params.diagnostics.into_iter().map(|d| (d, server_id));
// Insert the original lsp::Diagnostics here because we may have no open document
// for diagnosic message and so we can't calculate the exact position.
// When using them later in the diagnostics picker, we calculate them on-demand.
match self.editor.diagnostics.entry(params.uri) {
let diagnostics = match self.editor.diagnostics.entry(path) {
Entry::Occupied(o) => {
let current_diagnostics = o.into_mut();
// there may entries of other language servers, which is why we can't overwrite the whole entry
current_diagnostics.retain(|(_, lsp_id)| *lsp_id != server_id);
current_diagnostics.append(&mut diagnostics);
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
current_diagnostics.extend(diagnostics);
current_diagnostics
.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
}
Entry::Vacant(v) => {
diagnostics
.sort_unstable_by_key(|(d, _)| (d.severity, d.range.start));
v.insert(diagnostics);
// Sort diagnostics first by severity and then by line numbers.
}
Entry::Vacant(v) => v.insert(diagnostics.collect()),
};
// Sort diagnostics first by severity and then by line numbers.
// Note: The `lsp::DiagnosticSeverity` enum is already defined in decreasing order
diagnostics.sort_unstable_by_key(|(d, server_id)| {
(d.severity, d.range.start, *server_id)
});
if let Some(doc) = doc {
let diagnostic_of_language_server_and_not_in_unchanged_sources =
|diagnostic: &lsp::Diagnostic, ls_id| {
ls_id == server_id
&& diagnostic.source.as_ref().map_or(true, |source| {
!unchanged_diag_sources.contains(source)
})
};
let diagnostics = Editor::doc_diagnostics_with_filter(
&self.editor.language_servers,
&self.editor.diagnostics,
doc,
diagnostic_of_language_server_and_not_in_unchanged_sources,
);
doc.replace_diagnostics(
diagnostics,
&unchanged_diag_sources,
Some(server_id),
);
}
}
Notification::ShowMessage(params) => {
log::warn!("unhandled window/showMessage: {:?}", params);
@@ -968,7 +931,7 @@ impl Application {
// Clear any diagnostics for documents with this server open.
for doc in self.editor.documents_mut() {
doc.clear_diagnostics(server_id);
doc.clear_diagnostics(Some(server_id));
}
// Remove the language server from the registry.
@@ -1022,11 +985,9 @@ impl Application {
let language_server = language_server!();
if language_server.is_initialized() {
let offset_encoding = language_server.offset_encoding();
let res = apply_workspace_edit(
&mut self.editor,
offset_encoding,
&params.edit,
);
let res = self
.editor
.apply_workspace_edit(offset_encoding, &params.edit);
Ok(json!(lsp::ApplyWorkspaceEditResponse {
applied: res.is_ok(),
@@ -1069,12 +1030,7 @@ impl Application {
Ok(json!(result))
}
Ok(MethodCall::RegisterCapability(params)) => {
if let Some(client) = self
.editor
.language_servers
.iter_clients()
.find(|client| client.id() == server_id)
{
if let Some(client) = self.editor.language_servers.get_by_id(server_id) {
for reg in params.registrations {
match reg.method.as_str() {
lsp::notification::DidChangeWatchedFiles::METHOD => {
@@ -1127,6 +1083,13 @@ impl Application {
}
Ok(serde_json::Value::Null)
}
Ok(MethodCall::ShowDocument(params)) => {
let language_server = language_server!();
let offset_encoding = language_server.offset_encoding();
let result = self.handle_show_document(params, offset_encoding);
Ok(json!(result))
}
};
tokio::spawn(language_server!().reply(id, reply));
@@ -1135,6 +1098,68 @@ impl Application {
}
}
fn handle_show_document(
&mut self,
params: lsp::ShowDocumentParams,
offset_encoding: helix_lsp::OffsetEncoding,
) -> lsp::ShowDocumentResult {
if let lsp::ShowDocumentParams {
external: Some(true),
uri,
..
} = params
{
self.jobs.callback(crate::open_external_url_callback(uri));
return lsp::ShowDocumentResult { success: true };
};
let lsp::ShowDocumentParams {
uri,
selection,
take_focus,
..
} = params;
let path = match uri.to_file_path() {
Ok(path) => path,
Err(err) => {
log::error!("unsupported file URI: {}: {:?}", uri, err);
return lsp::ShowDocumentResult { success: false };
}
};
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) {
Ok(id) => id,
Err(err) => {
log::error!("failed to open path: {:?}: {:?}", uri, err);
return lsp::ShowDocumentResult { success: false };
}
};
let doc = doc_mut!(self.editor, &doc_id);
if let Some(range) = selection {
// TODO: convert inside server
if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding) {
let view = view_mut!(self.editor);
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
if action.align_view(view, doc.id()) {
align_view(doc, view, Align::Center);
}
} else {
log::warn!("lsp position out of bounds - {:?}", range);
};
};
lsp::ShowDocumentResult { success: true }
}
async fn claim_term(&mut self) -> std::io::Result<()> {
let terminal_config = self.config.load().editor.clone().into();
self.terminal.claim(terminal_config)

View File

@@ -17,7 +17,6 @@ pub struct Args {
pub log_file: Option<PathBuf>,
pub config_file: Option<PathBuf>,
pub files: Vec<(PathBuf, Position)>,
pub open_cwd: bool,
pub working_directory: Option<PathBuf>,
}
@@ -91,10 +90,9 @@ impl Args {
}
}
arg if arg.starts_with('+') => {
let arg = &arg[1..];
line_number = match arg.parse::<usize>() {
Ok(n) => n.saturating_sub(1),
_ => anyhow::bail!("bad line number after +"),
match arg[1..].parse::<usize>() {
Ok(n) => line_number = n.saturating_sub(1),
_ => args.files.push(parse_file(arg)),
};
}
arg => args.files.push(parse_file(arg)),

File diff suppressed because it is too large Load Diff

View File

@@ -8,7 +8,7 @@ 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;
use helix_view::{editor::Breakpoint, graphics::Margin};
use serde_json::{to_value, Value};
use tokio_stream::wrappers::UnboundedReceiverStream;
@@ -78,7 +78,7 @@ fn thread_picker(
})
.with_preview(move |editor, thread| {
let frames = editor.debugger.as_ref()?.stack_frames.get(&thread.id)?;
let frame = frames.get(0)?;
let frame = frames.first()?;
let path = frame.source.as_ref()?.path.clone()?;
let pos = Some((
frame.line.saturating_sub(1),
@@ -166,7 +166,7 @@ pub fn dap_start_impl(
// TODO: avoid refetching all of this... pass a config in
let template = match name {
Some(name) => config.templates.iter().find(|t| t.name == name),
None => config.templates.get(0),
None => config.templates.first(),
}
.ok_or_else(|| anyhow!("No debug config with given name"))?;
@@ -217,7 +217,7 @@ pub fn dap_start_impl(
}
}
args.insert("cwd", to_value(helix_loader::current_working_dir())?);
args.insert("cwd", to_value(helix_stdx::env::current_working_dir())?);
let args = to_value(args).unwrap();
@@ -581,7 +581,12 @@ pub fn dap_variables(cx: &mut Context) {
}
let contents = Text::from(tui::text::Text::from(variables));
let popup = Popup::new("dap-variables", contents);
let margin = if cx.editor.popup_border() {
Margin::all(1)
} else {
Margin::none()
};
let popup = Popup::new("dap-variables", contents).margin(margin);
cx.replace_or_push_layer("dap-variables", popup);
}

View File

@@ -1,4 +1,4 @@
use futures_util::{future::BoxFuture, stream::FuturesUnordered, FutureExt};
use futures_util::{stream::FuturesOrdered, FutureExt};
use helix_lsp::{
block_on,
lsp::{
@@ -6,23 +6,23 @@ use helix_lsp::{
NumberOrString,
},
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
Client, OffsetEncoding,
Client, LanguageServerId, OffsetEncoding,
};
use serde_json::Value;
use tokio_stream::StreamExt;
use tui::{
text::{Span, Spans},
widgets::Row,
};
use super::{align_view, push_jump, Align, Context, Editor, Open};
use super::{align_view, push_jump, Align, Context, Editor};
use helix_core::{
path, syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection,
};
use helix_core::{syntax::LanguageServerFeature, text_annotations::InlineAnnotation, Selection};
use helix_stdx::path;
use helix_view::{
document::{DocumentInlayHints, DocumentInlayHintsId, Mode},
document::{DocumentInlayHints, DocumentInlayHintsId},
editor::Action,
graphics::Margin,
handlers::lsp::SignatureHelpInvoked,
theme::Style,
Document, View,
};
@@ -30,10 +30,7 @@ use helix_view::{
use crate::{
compositor::{self, Compositor},
job::Callback,
ui::{
self, lsp::SignatureHelp, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup,
PromptEvent,
},
ui::{self, overlay::overlaid, DynamicPicker, FileLocation, Picker, Popup, PromptEvent},
};
use std::{
@@ -41,15 +38,14 @@ use std::{
collections::{BTreeMap, HashSet},
fmt::Write,
future::Future,
path::PathBuf,
sync::Arc,
path::{Path, PathBuf},
};
/// Gets the first language server that is attached to a document which supports a specific feature.
/// If there is no configured language server that supports the feature, this displays a status message.
/// Using this macro in a context where the editor automatically queries the LSP
/// (instead of when the user explicitly does so via a keybind like `gd`)
/// will spam the "No configured language server supports <feature>" status message confusingly.
/// will spam the "No configured language server supports \<feature>" status message confusingly.
#[macro_export]
macro_rules! language_server_with_feature {
($editor:expr, $doc:expr, $feature:expr) => {{
@@ -138,7 +134,7 @@ struct DiagnosticStyles {
}
struct PickerDiagnostic {
url: lsp::Url,
path: PathBuf,
diag: lsp::Diagnostic,
offset_encoding: OffsetEncoding,
}
@@ -171,8 +167,7 @@ impl ui::menu::Item for PickerDiagnostic {
let path = match format {
DiagnosticsFormat::HideSourcePath => String::new(),
DiagnosticsFormat::ShowSourcePath => {
let file_path = self.url.to_file_path().unwrap();
let path = path::get_truncated_path(file_path);
let path = path::get_truncated_path(&self.path);
format!("{}: ", path.to_string_lossy())
}
};
@@ -212,24 +207,33 @@ fn jump_to_location(
return;
}
};
jump_to_position(editor, &path, location.range, offset_encoding, action);
}
let doc = match editor.open(&path, action) {
fn jump_to_position(
editor: &mut Editor,
path: &Path,
range: lsp::Range,
offset_encoding: OffsetEncoding,
action: Action,
) {
let doc = match editor.open(path, action) {
Ok(id) => doc_mut!(editor, &id),
Err(err) => {
let err = format!("failed to open path: {:?}: {:?}", location.uri, err);
let err = format!("failed to open path: {:?}: {:?}", path, err);
editor.set_error(err);
return;
}
};
let view = view_mut!(editor);
// TODO: convert inside server
let new_range =
if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) {
new_range
} else {
log::warn!("lsp position out of bounds - {:?}", location.range);
return;
};
let new_range = if let Some(new_range) = lsp_range_to_range(doc.text(), range, offset_encoding)
{
new_range
} else {
log::warn!("lsp position out of bounds - {:?}", range);
return;
};
// we flip the range so that the cursor sits on the start of the symbol
// (for example start of the function).
doc.set_selection(view.id, Selection::single(new_range.head, new_range.anchor));
@@ -262,21 +266,20 @@ enum DiagnosticsFormat {
fn diag_picker(
cx: &Context,
diagnostics: BTreeMap<lsp::Url, Vec<(lsp::Diagnostic, usize)>>,
_current_path: Option<lsp::Url>,
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
format: DiagnosticsFormat,
) -> Picker<PickerDiagnostic> {
// 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 (url, diags) in diagnostics {
for (path, 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 {
url: url.clone(),
path: path.clone(),
diag,
offset_encoding: ls.offset_encoding(),
});
@@ -296,22 +299,17 @@ fn diag_picker(
(styles, format),
move |cx,
PickerDiagnostic {
url,
path,
diag,
offset_encoding,
},
action| {
jump_to_location(
cx.editor,
&lsp::Location::new(url.clone(), diag.range),
*offset_encoding,
action,
)
jump_to_position(cx.editor, path, diag.range, *offset_encoding, action)
},
)
.with_preview(move |_editor, PickerDiagnostic { url, diag, .. }| {
let location = lsp::Location::new(url.clone(), diag.range);
Some(location_to_file_location(&location))
.with_preview(move |_editor, PickerDiagnostic { path, diag, .. }| {
let line = Some((diag.range.start.line as usize, diag.range.end.line as usize));
Some((path.clone().into(), line))
})
.truncate_start(false)
}
@@ -343,7 +341,7 @@ pub fn symbol_picker(cx: &mut Context) {
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
let mut futures: FuturesOrdered<_> = doc
.language_servers_with_feature(LanguageServerFeature::DocumentSymbols)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|language_server| {
@@ -418,7 +416,7 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
let get_symbols = move |pattern: String, editor: &mut Editor| {
let doc = doc!(editor);
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
let mut futures: FuturesOrdered<_> = doc
.language_servers_with_feature(LanguageServerFeature::WorkspaceSymbols)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|language_server| {
@@ -474,17 +472,16 @@ pub fn workspace_symbol_picker(cx: &mut Context) {
pub fn diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
if let Some(current_url) = doc.url() {
if let Some(current_path) = doc.path() {
let diagnostics = cx
.editor
.diagnostics
.get(&current_url)
.get(current_path)
.cloned()
.unwrap_or_default();
let picker = diag_picker(
cx,
[(current_url.clone(), diagnostics)].into(),
Some(current_url),
[(current_path.clone(), diagnostics)].into(),
DiagnosticsFormat::HideSourcePath,
);
cx.push_layer(Box::new(overlaid(picker)));
@@ -492,22 +489,15 @@ pub fn diagnostics_picker(cx: &mut Context) {
}
pub fn workspace_diagnostics_picker(cx: &mut Context) {
let doc = doc!(cx.editor);
let current_url = doc.url();
// TODO not yet filtered by LanguageServerFeature, need to do something similar as Document::shown_diagnostics here for all open documents
let diagnostics = cx.editor.diagnostics.clone();
let picker = diag_picker(
cx,
diagnostics,
current_url,
DiagnosticsFormat::ShowSourcePath,
);
let picker = diag_picker(cx, diagnostics, DiagnosticsFormat::ShowSourcePath);
cx.push_layer(Box::new(overlaid(picker)));
}
struct CodeActionOrCommandItem {
lsp_item: lsp::CodeActionOrCommand,
language_server_id: usize,
language_server_id: LanguageServerId,
}
impl ui::menu::Item for CodeActionOrCommandItem {
@@ -584,7 +574,7 @@ pub fn code_action(cx: &mut Context) {
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
let mut futures: FuturesOrdered<_> = doc
.language_servers_with_feature(LanguageServerFeature::CodeAction)
.filter(|ls| seen_language_servers.insert(ls.id()))
// TODO this should probably already been filtered in something like "language_servers_with_feature"
@@ -730,8 +720,7 @@ pub fn code_action(cx: &mut Context) {
resolved_code_action.as_ref().unwrap_or(code_action);
if let Some(ref workspace_edit) = resolved_code_action.edit {
log::debug!("edit: {:?}", workspace_edit);
let _ = apply_workspace_edit(editor, offset_encoding, workspace_edit);
let _ = editor.apply_workspace_edit(offset_encoding, workspace_edit);
}
// if code action provides both edit and command first the edit
@@ -744,7 +733,16 @@ pub fn code_action(cx: &mut Context) {
});
picker.move_down(); // pre-select the first item
let popup = Popup::new("code-action", picker).with_scrollbar(false);
let margin = if editor.menu_border() {
Margin::vertical(1)
} else {
Margin::none()
};
let popup = Popup::new("code-action", picker)
.with_scrollbar(false)
.margin(margin);
compositor.replace_or_push("code-action", popup);
};
@@ -759,7 +757,11 @@ impl ui::menu::Item for lsp::Command {
}
}
pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
pub fn execute_lsp_command(
editor: &mut Editor,
language_server_id: LanguageServerId,
cmd: lsp::Command,
) {
// the command is executed on the server and communicated back
// to the client asynchronously using workspace edits
let future = match editor
@@ -782,63 +784,6 @@ pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd:
});
}
pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
use lsp::ResourceOp;
use std::fs;
match op {
ResourceOp::Create(op) => {
let path = op.uri.to_file_path().unwrap();
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
});
if ignore_if_exists && path.exists() {
Ok(())
} else {
// Create directory if it does not exist
if let Some(dir) = path.parent() {
if !dir.is_dir() {
fs::create_dir_all(dir)?;
}
}
fs::write(&path, [])
}
}
ResourceOp::Delete(op) => {
let path = op.uri.to_file_path().unwrap();
if path.is_dir() {
let recursive = op
.options
.as_ref()
.and_then(|options| options.recursive)
.unwrap_or(false);
if recursive {
fs::remove_dir_all(&path)
} else {
fs::remove_dir(&path)
}
} else if path.is_file() {
fs::remove_file(&path)
} else {
Ok(())
}
}
ResourceOp::Rename(op) => {
let from = op.old_uri.to_file_path().unwrap();
let to = op.new_uri.to_file_path().unwrap();
let ignore_if_exists = op.options.as_ref().map_or(false, |options| {
!options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
});
if ignore_if_exists && to.exists() {
Ok(())
} else {
fs::rename(from, &to)
}
}
}
}
#[derive(Debug)]
pub struct ApplyEditError {
pub kind: ApplyEditErrorKind,
@@ -866,168 +811,20 @@ impl ToString for ApplyEditErrorKind {
}
}
///TODO make this transactional (and set failureMode to transactional)
pub fn apply_workspace_edit(
editor: &mut Editor,
offset_encoding: OffsetEncoding,
workspace_edit: &lsp::WorkspaceEdit,
) -> Result<(), ApplyEditError> {
let mut apply_edits = |uri: &helix_lsp::Url,
version: Option<i32>,
text_edits: Vec<lsp::TextEdit>|
-> Result<(), ApplyEditErrorKind> {
let path = match uri.to_file_path() {
Ok(path) => path,
Err(_) => {
let err = format!("unable to convert URI to filepath: {}", uri);
log::error!("{}", err);
editor.set_error(err);
return Err(ApplyEditErrorKind::UnknownURISchema);
}
};
let current_view_id = view!(editor).id;
let doc_id = match editor.open(&path, Action::Load) {
Ok(doc_id) => doc_id,
Err(err) => {
let err = format!("failed to open document: {}: {}", uri, err);
log::error!("{}", err);
editor.set_error(err);
return Err(ApplyEditErrorKind::FileNotFound);
}
};
let doc = doc_mut!(editor, &doc_id);
if let Some(version) = version {
if version != doc.version() {
let err = format!("outdated workspace edit for {path:?}");
log::error!("{err}, expected {} but got {version}", doc.version());
editor.set_error(err);
return Err(ApplyEditErrorKind::DocumentChanged);
}
}
// Need to determine a view for apply/append_changes_to_history
let selections = doc.selections();
let view_id = if selections.contains_key(&current_view_id) {
// use current if possible
current_view_id
} else {
// Hack: we take the first available view_id
selections
.keys()
.next()
.copied()
.expect("No view_id available")
};
let transaction = helix_lsp::util::generate_transaction_from_edits(
doc.text(),
text_edits,
offset_encoding,
);
let view = view_mut!(editor, view_id);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
Ok(())
};
if let Some(ref document_changes) = workspace_edit.document_changes {
match document_changes {
lsp::DocumentChanges::Edits(document_edits) => {
for (i, document_edit) in document_edits.iter().enumerate() {
let edits = document_edit
.edits
.iter()
.map(|edit| match edit {
lsp::OneOf::Left(text_edit) => text_edit,
lsp::OneOf::Right(annotated_text_edit) => {
&annotated_text_edit.text_edit
}
})
.cloned()
.collect();
apply_edits(
&document_edit.text_document.uri,
document_edit.text_document.version,
edits,
)
.map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
lsp::DocumentChanges::Operations(operations) => {
log::debug!("document changes - operations: {:?}", operations);
for (i, operation) in operations.iter().enumerate() {
match operation {
lsp::DocumentChangeOperation::Op(op) => {
apply_document_resource_op(op).map_err(|io| ApplyEditError {
kind: ApplyEditErrorKind::IoError(io),
failed_change_idx: i,
})?;
}
lsp::DocumentChangeOperation::Edit(document_edit) => {
let edits = document_edit
.edits
.iter()
.map(|edit| match edit {
lsp::OneOf::Left(text_edit) => text_edit,
lsp::OneOf::Right(annotated_text_edit) => {
&annotated_text_edit.text_edit
}
})
.cloned()
.collect();
apply_edits(
&document_edit.text_document.uri,
document_edit.text_document.version,
edits,
)
.map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
}
}
}
return Ok(());
}
if let Some(ref changes) = workspace_edit.changes {
log::debug!("workspace changes: {:?}", changes);
for (i, (uri, text_edits)) in changes.iter().enumerate() {
let text_edits = text_edits.to_vec();
apply_edits(uri, None, text_edits).map_err(|kind| ApplyEditError {
kind,
failed_change_idx: i,
})?;
}
}
Ok(())
}
/// Precondition: `locations` should be non-empty.
fn goto_impl(
editor: &mut Editor,
compositor: &mut Compositor,
locations: Vec<lsp::Location>,
offset_encoding: OffsetEncoding,
) {
let cwdir = helix_loader::current_working_dir();
let cwdir = helix_stdx::env::current_working_dir();
match locations.as_slice() {
[location] => {
jump_to_location(editor, location, offset_encoding, Action::Replace);
}
[] => {
editor.set_error("No definition found.");
}
[] => unreachable!("`locations` should be non-empty for `goto_impl`"),
_locations => {
let picker = Picker::new(locations, cwdir, move |cx, location, action| {
jump_to_location(cx.editor, location, offset_encoding, action)
@@ -1069,7 +866,11 @@ where
future,
move |editor, compositor, response: Option<lsp::GotoDefinitionResponse>| {
let items = to_locations(response);
goto_impl(editor, compositor, items, offset_encoding);
if items.is_empty() {
editor.set_error("No definition found.");
} else {
goto_impl(editor, compositor, items, offset_encoding);
}
},
);
}
@@ -1129,151 +930,19 @@ pub fn goto_reference(cx: &mut Context) {
future,
move |editor, compositor, response: Option<Vec<lsp::Location>>| {
let items = response.unwrap_or_default();
goto_impl(editor, compositor, items, offset_encoding);
if items.is_empty() {
editor.set_error("No references found.");
} else {
goto_impl(editor, compositor, items, offset_encoding);
}
},
);
}
#[derive(PartialEq, Eq, Clone, Copy)]
pub enum SignatureHelpInvoked {
Manual,
Automatic,
}
pub fn signature_help(cx: &mut Context) {
signature_help_impl(cx, SignatureHelpInvoked::Manual)
}
pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) {
let (view, doc) = current!(cx.editor);
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
let future = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.find_map(|language_server| {
let pos = doc.position(view.id, language_server.offset_encoding());
language_server.text_document_signature_help(doc.identifier(), pos, None)
});
let Some(future) = future else {
// Do not show the message if signature help was invoked
// automatically on backspace, trigger characters, etc.
if invoked == SignatureHelpInvoked::Manual {
cx.editor
.set_error("No configured language server supports signature-help");
}
return;
};
signature_help_impl_with_future(cx, future.boxed(), invoked);
}
pub fn signature_help_impl_with_future(
cx: &mut Context,
future: BoxFuture<'static, helix_lsp::Result<Value>>,
invoked: SignatureHelpInvoked,
) {
cx.callback(
future,
move |editor, compositor, response: Option<lsp::SignatureHelp>| {
let config = &editor.config();
if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
|| invoked == SignatureHelpInvoked::Manual)
{
return;
}
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
// it very probably means the server was a little slow to respond and the user has
// already moved on to something else, making a signature help popup will just be an
// annoyance, see https://github.com/helix-editor/helix/issues/3112
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
return;
}
let response = match response {
// According to the spec the response should be None if there
// are no signatures, but some servers don't follow this.
Some(s) if !s.signatures.is_empty() => s,
_ => {
compositor.remove(SignatureHelp::ID);
return;
}
};
let doc = doc!(editor);
let language = doc.language_name().unwrap_or("");
let signature = match response
.signatures
.get(response.active_signature.unwrap_or(0) as usize)
{
Some(s) => s,
None => return,
};
let mut contents = SignatureHelp::new(
signature.label.clone(),
language.to_string(),
Arc::clone(&editor.syn_loader),
);
let signature_doc = if config.lsp.display_signature_help_docs {
signature.documentation.as_ref().map(|doc| match doc {
lsp::Documentation::String(s) => s.clone(),
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
})
} else {
None
};
contents.set_signature_doc(signature_doc);
let active_param_range = || -> Option<(usize, usize)> {
let param_idx = signature
.active_parameter
.or(response.active_parameter)
.unwrap_or(0) as usize;
let param = signature.parameters.as_ref()?.get(param_idx)?;
match &param.label {
lsp::ParameterLabel::Simple(string) => {
let start = signature.label.find(string.as_str())?;
Some((start, start + string.len()))
}
lsp::ParameterLabel::LabelOffsets([start, end]) => {
// LS sends offsets based on utf-16 based string representation
// but highlighting in helix is done using byte offset.
use helix_core::str_utils::char_to_byte_idx;
let from = char_to_byte_idx(&signature.label, *start as usize);
let to = char_to_byte_idx(&signature.label, *end as usize);
Some((from, to))
}
}
};
contents.set_active_param_range(active_param_range());
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
let mut popup = Popup::new(SignatureHelp::ID, contents)
.position(old_popup.and_then(|p| p.get_position()))
.position_bias(Open::Above)
.ignore_escape_key(true);
// Don't create a popup if it intersects the auto-complete menu.
let size = compositor.size();
if compositor
.find::<ui::EditorView>()
.unwrap()
.completion
.as_mut()
.map(|completion| completion.area(size, editor))
.filter(|area| area.intersects(popup.area(size, editor)))
.is_some()
{
return;
}
compositor.replace_or_push(SignatureHelp::ID, popup);
},
);
cx.editor
.handlers
.trigger_signature_help(SignatureHelpInvoked::Manual, cx.editor)
}
pub fn hover(cx: &mut Context) {
@@ -1369,7 +1038,7 @@ pub fn rename_symbol(cx: &mut Context) {
fn create_rename_prompt(
editor: &Editor,
prefill: String,
language_server_id: Option<usize>,
language_server_id: Option<LanguageServerId>,
) -> Box<ui::Prompt> {
let prompt = ui::Prompt::new(
"rename-to:".into(),
@@ -1398,7 +1067,7 @@ pub fn rename_symbol(cx: &mut Context) {
match block_on(future) {
Ok(edits) => {
let _ = apply_workspace_edit(cx.editor, offset_encoding, &edits);
let _ = cx.editor.apply_workspace_edit(offset_encoding, &edits);
}
Err(err) => cx.editor.set_error(err.to_string()),
}
@@ -1411,6 +1080,16 @@ pub fn rename_symbol(cx: &mut Context) {
let (view, doc) = current_ref!(cx.editor);
if doc
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
.next()
.is_none()
{
cx.editor
.set_error("No configured language server supports symbol renaming");
return;
}
let language_server_with_prepare_rename_support = doc
.language_servers_with_feature(LanguageServerFeature::RenameSymbol)
.find(|ls| {
@@ -1640,11 +1319,11 @@ fn compute_inlay_hints_for_view(
view_id,
DocumentInlayHints {
id: new_doc_inlay_hints_id,
type_inlay_hints: type_inlay_hints.into(),
parameter_inlay_hints: parameter_inlay_hints.into(),
other_inlay_hints: other_inlay_hints.into(),
padding_before_inlay_hints: padding_before_inlay_hints.into(),
padding_after_inlay_hints: padding_after_inlay_hints.into(),
type_inlay_hints,
parameter_inlay_hints,
other_inlay_hints,
padding_before_inlay_hints,
padding_after_inlay_hints,
},
);
doc.inlay_hints_oudated = false;

View File

@@ -1,4 +1,5 @@
use std::fmt::Write;
use std::io::BufReader;
use std::ops::Deref;
use crate::job::Job;
@@ -6,9 +7,10 @@ use crate::job::Job;
use super::*;
use helix_core::fuzzy::fuzzy_match;
use helix_core::{encoding, line_ending, shellwords::Shellwords};
use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::editor::{Action, CloseError, ConfigEvent};
use helix_core::indent::MAX_INDENT;
use helix_core::{line_ending, shellwords::Shellwords};
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
use helix_view::editor::{CloseError, ConfigEvent};
use serde_json::Value;
use ui::completers::{self, Completer};
@@ -109,14 +111,14 @@ fn open(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) ->
ensure!(!args.is_empty(), "wrong argument count");
for arg in args {
let (path, pos) = args::parse_file(arg);
let path = helix_core::path::expand_tilde(&path);
let path = helix_stdx::path::expand_tilde(path);
// If the path is a directory, open a file picker on that directory and update the status
// message
if let Ok(true) = std::fs::canonicalize(&path).map(|p| p.is_dir()) {
let callback = async move {
let call: job::Callback = job::Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
let picker = ui::file_picker(path, &editor.config());
let picker = ui::file_picker(path.into_owned(), &editor.config());
compositor.push(Box::new(overlaid(picker)));
},
));
@@ -308,7 +310,7 @@ fn buffer_next(
return Ok(());
}
goto_buffer(cx.editor, Direction::Forward);
goto_buffer(cx.editor, Direction::Forward, 1);
Ok(())
}
@@ -321,7 +323,7 @@ fn buffer_previous(
return Ok(());
}
goto_buffer(cx.editor, Direction::Backward);
goto_buffer(cx.editor, Direction::Backward, 1);
Ok(())
}
@@ -475,20 +477,19 @@ fn set_indent_style(
cx.editor.set_status(match style {
Tabs => "tabs".to_owned(),
Spaces(1) => "1 space".to_owned(),
Spaces(n) if (2..=8).contains(&n) => format!("{} spaces", n),
_ => unreachable!(), // Shouldn't happen.
Spaces(n) => format!("{} spaces", n),
});
return Ok(());
}
// Attempt to parse argument as an indent style.
let style = match args.get(0) {
let style = match args.first() {
Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs),
Some(Cow::Borrowed("0")) => Some(Tabs),
Some(arg) => arg
.parse::<u8>()
.ok()
.filter(|n| (1..=8).contains(n))
.filter(|n| (1..=MAX_INDENT).contains(n))
.map(Spaces),
_ => None,
};
@@ -534,7 +535,7 @@ fn set_line_ending(
}
let arg = args
.get(0)
.first()
.context("argument missing")?
.to_ascii_lowercase();
@@ -573,7 +574,6 @@ fn set_line_ending(
Ok(())
}
fn earlier(
cx: &mut compositor::Context,
args: &[Cow<str>],
@@ -674,13 +674,15 @@ pub fn write_all_impl(
let mut errors: Vec<&'static str> = Vec::new();
let config = cx.editor.config();
let jobs = &mut cx.jobs;
let current_view = view!(cx.editor);
let saves: Vec<_> = cx
.editor
.documents
.values_mut()
.filter_map(|doc| {
.keys()
.cloned()
.collect::<Vec<_>>()
.into_iter()
.filter_map(|id| {
let doc = doc!(cx.editor, &id);
if !doc.is_modified() {
return None;
}
@@ -691,22 +693,9 @@ pub fn write_all_impl(
return None;
}
// Look for a view to apply the formatting change to. If the document
// is in the current view, just use that. Otherwise, since we don't
// have any other metric available for better selection, just pick
// the first view arbitrarily so that we still commit the document
// state for undos. If somehow we have a document that has not been
// initialized with any view, initialize it with the current view.
let target_view = if doc.selections().contains_key(&current_view.id) {
current_view.id
} else if let Some(view) = doc.selections().keys().next() {
*view
} else {
doc.ensure_view_init(current_view.id);
current_view.id
};
Some((doc.id(), target_view))
// Look for a view to apply the formatting change to.
let target_view = cx.editor.get_synced_view_id(doc.id());
Some((id, target_view))
})
.collect();
@@ -921,7 +910,7 @@ fn yank_main_selection_to_clipboard(
return Ok(());
}
yank_primary_selection_impl(cx.editor, '*');
yank_primary_selection_impl(cx.editor, '+');
Ok(())
}
@@ -956,7 +945,7 @@ fn yank_joined_to_clipboard(
let doc = doc!(cx.editor);
let default_sep = Cow::Borrowed(doc.line_ending.as_str());
let separator = args.first().unwrap_or(&default_sep);
yank_joined_impl(cx.editor, separator, '*');
yank_joined_impl(cx.editor, separator, '+');
Ok(())
}
@@ -969,7 +958,7 @@ fn yank_main_selection_to_primary_clipboard(
return Ok(());
}
yank_primary_selection_impl(cx.editor, '+');
yank_primary_selection_impl(cx.editor, '*');
Ok(())
}
@@ -985,7 +974,7 @@ fn yank_joined_to_primary_clipboard(
let doc = doc!(cx.editor);
let default_sep = Cow::Borrowed(doc.line_ending.as_str());
let separator = args.first().unwrap_or(&default_sep);
yank_joined_impl(cx.editor, separator, '+');
yank_joined_impl(cx.editor, separator, '*');
Ok(())
}
@@ -998,7 +987,7 @@ fn paste_clipboard_after(
return Ok(());
}
paste(cx.editor, '*', Paste::After, 1);
paste(cx.editor, '+', Paste::After, 1);
Ok(())
}
@@ -1011,7 +1000,7 @@ fn paste_clipboard_before(
return Ok(());
}
paste(cx.editor, '*', Paste::Before, 1);
paste(cx.editor, '+', Paste::Before, 1);
Ok(())
}
@@ -1024,7 +1013,7 @@ fn paste_primary_clipboard_after(
return Ok(());
}
paste(cx.editor, '+', Paste::After, 1);
paste(cx.editor, '*', Paste::After, 1);
Ok(())
}
@@ -1037,7 +1026,7 @@ fn paste_primary_clipboard_before(
return Ok(());
}
paste(cx.editor, '+', Paste::Before, 1);
paste(cx.editor, '*', Paste::Before, 1);
Ok(())
}
@@ -1050,7 +1039,7 @@ fn replace_selections_with_clipboard(
return Ok(());
}
replace_with_yanked_impl(cx.editor, '*', 1);
replace_with_yanked_impl(cx.editor, '+', 1);
Ok(())
}
@@ -1063,7 +1052,7 @@ fn replace_selections_with_primary_clipboard(
return Ok(());
}
replace_with_yanked_impl(cx.editor, '+', 1);
replace_with_yanked_impl(cx.editor, '*', 1);
Ok(())
}
@@ -1090,18 +1079,17 @@ fn change_current_directory(
return Ok(());
}
let dir = helix_core::path::expand_tilde(
args.first()
.context("target directory not provided")?
.as_ref()
.as_ref(),
);
let dir = args
.first()
.context("target directory not provided")?
.as_ref();
let dir = helix_stdx::path::expand_tilde(Path::new(dir));
helix_loader::set_current_working_dir(dir)?;
helix_stdx::env::set_current_working_dir(dir)?;
cx.editor.set_status(format!(
"Current working directory is now {}",
helix_loader::current_working_dir().display()
helix_stdx::env::current_working_dir().display()
));
Ok(())
}
@@ -1115,7 +1103,7 @@ fn show_current_directory(
return Ok(());
}
let cwd = helix_loader::current_working_dir();
let cwd = helix_stdx::env::current_working_dir();
let message = format!("Current working directory is {}", cwd.display());
if cwd.exists() {
@@ -1281,11 +1269,8 @@ fn reload(
return Ok(());
}
let scrolloff = cx.editor.config().scrolloff;
let (view, doc) = current!(cx.editor);
doc.reload(view, &cx.editor.diff_providers).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})?;
doc.reload(view, &cx.editor.diff_providers)?;
if let Some(path) = doc.path() {
cx.editor
.language_servers
@@ -1331,7 +1316,11 @@ fn reload_all(
// Ensure that the view is synced with the document's history.
view.sync_changes(doc);
doc.reload(view, &cx.editor.diff_providers)?;
if let Err(error) = doc.reload(view, &cx.editor.diff_providers) {
cx.editor.set_error(format!("{}", error));
continue;
}
if let Some(path) = doc.path() {
cx.editor
.language_servers
@@ -1502,7 +1491,7 @@ 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(client.id());
doc.clear_diagnostics(Some(client.id()));
}
}
}
@@ -1558,10 +1547,7 @@ fn tree_sitter_highlight_name(
let text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
let byte = text.char_to_byte(cursor);
let node = syntax
.tree()
.root_node()
.descendant_for_byte_range(byte, byte)?;
let node = syntax.descendant_for_byte_range(byte, byte)?;
// Query the same range as the one used in syntax highlighting.
let range = {
// Calculate viewport byte ranges:
@@ -2008,6 +1994,10 @@ fn language(
let id = doc.id();
cx.editor.refresh_language_servers(id);
let doc = doc_mut!(cx.editor);
let diagnostics =
Editor::doc_diagnostics(&cx.editor.language_servers, &cx.editor.diagnostics, doc);
doc.replace_diagnostics(diagnostics, &[], None);
Ok(())
}
@@ -2036,7 +2026,6 @@ 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(..);
@@ -2061,8 +2050,6 @@ fn sort_impl(
);
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
Ok(())
}
@@ -2076,7 +2063,6 @@ 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);
@@ -2085,7 +2071,7 @@ fn reflow(
// - The configured text-width for this language in languages.toml
// - The configured text-width in the config.toml
let text_width: usize = args
.get(0)
.first()
.map(|num| num.parse::<usize>())
.transpose()?
.or_else(|| doc.language_config().and_then(|config| config.text_width))
@@ -2102,8 +2088,6 @@ fn reflow(
});
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
Ok(())
}
@@ -2124,11 +2108,7 @@ fn tree_sitter_subtree(
let text = doc.text();
let from = text.char_to_byte(primary_selection.from());
let to = text.char_to_byte(primary_selection.to());
if let Some(selected_node) = syntax
.tree()
.root_node()
.descendant_for_byte_range(from, to)
{
if let Some(selected_node) = syntax.descendant_for_byte_range(from, to) {
let mut contents = String::from("```tsq\n");
helix_core::syntax::pretty_print_tree(&mut contents, selected_node)?;
contents.push_str("\n```");
@@ -2273,7 +2253,7 @@ fn run_shell_command(
let args = args.join(" ");
let callback = async move {
let (output, success) = shell_impl_async(&shell, &args, None).await?;
let output = shell_impl_async(&shell, &args, None).await?;
let call: job::Callback = Callback::EditorCompositor(Box::new(
move |editor: &mut Editor, compositor: &mut Compositor| {
if !output.is_empty() {
@@ -2286,11 +2266,7 @@ fn run_shell_command(
));
compositor.replace_or_push("shell", popup);
}
if success {
editor.set_status("Command succeeded");
} else {
editor.set_error("Command failed");
}
editor.set_status("Command succeeded");
},
));
Ok(call)
@@ -2311,7 +2287,6 @@ 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 {
@@ -2349,8 +2324,6 @@ fn reset_diff_change(
// 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);
Ok(())
}
@@ -2408,6 +2381,98 @@ fn redraw(
Ok(())
}
fn move_buffer(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
ensure!(args.len() == 1, format!(":move takes one argument"));
let doc = doc!(cx.editor);
let old_path = doc
.path()
.context("Scratch buffer cannot be moved. Use :write instead")?
.clone();
let new_path = args.first().unwrap().to_string();
if let Err(err) = cx.editor.move_path(&old_path, new_path.as_ref()) {
bail!("Could not move file: {err}");
}
Ok(())
}
fn yank_diagnostic(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let reg = match args.first() {
Some(s) => {
ensure!(s.chars().count() == 1, format!("Invalid register {s}"));
s.chars().next().unwrap()
}
None => '+',
};
let (view, doc) = current_ref!(cx.editor);
let primary = doc.selection(view.id).primary();
// Look only for diagnostics that intersect with the primary selection
let diag: Vec<_> = doc
.diagnostics()
.iter()
.filter(|d| primary.overlaps(&helix_core::Range::new(d.range.start, d.range.end)))
.map(|d| d.message.clone())
.collect();
let n = diag.len();
if n == 0 {
bail!("No diagnostics under primary selection");
}
cx.editor.registers.write(reg, diag)?;
cx.editor.set_status(format!(
"Yanked {n} diagnostic{} to register {reg}",
if n == 1 { "" } else { "s" }
));
Ok(())
}
fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let (view, doc) = current!(cx.editor);
ensure!(!args.is_empty(), "file name is expected");
ensure!(args.len() == 1, "only the file name is expected");
let filename = args.get(0).unwrap();
let path = PathBuf::from(filename.to_string());
ensure!(
path.exists() && path.is_file(),
"path is not a file: {:?}",
path
);
let file = std::fs::File::open(path).map_err(|err| anyhow!("error opening file: {}", err))?;
let mut reader = BufReader::new(file);
let (contents, _, _) = read_to_string(&mut reader, Some(doc.encoding()))
.map_err(|err| anyhow!("error reading file: {}", err))?;
let contents = Tendril::from(contents);
let selection = doc.selection(view.id);
let transaction = Transaction::insert(doc.text(), selection, contents);
doc.apply(&transaction, view.id);
Ok(())
}
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -2531,7 +2596,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "indent-style",
aliases: &[],
doc: "Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.)",
doc: "Set the indentation style for editing. ('t' for tabs or 1-16 for number of spaces.)",
fun: set_indent_style,
signature: CommandSignature::none(),
},
@@ -2999,7 +3064,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
aliases: &[],
doc: "Clear given register. If no argument is provided, clear all registers.",
fun: clear_register,
signature: CommandSignature::none(),
signature: CommandSignature::all(completers::register),
},
TypableCommand {
name: "redraw",
@@ -3008,6 +3073,27 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
fun: redraw,
signature: CommandSignature::none(),
},
TypableCommand {
name: "move",
aliases: &[],
doc: "Move the current buffer and its corresponding file to a different path",
fun: move_buffer,
signature: CommandSignature::positional(&[completers::filename]),
},
TypableCommand {
name: "yank-diagnostic",
aliases: &[],
doc: "Yank diagnostic(s) under primary cursor to register, or clipboard by default",
fun: yank_diagnostic,
signature: CommandSignature::all(completers::register),
},
TypableCommand {
name: "read",
aliases: &["r"],
doc: "Load a file into buffer",
fun: read,
signature: CommandSignature::positional(&[completers::filename]),
},
];
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
@@ -3089,9 +3175,17 @@ 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]));

20
helix-term/src/events.rs Normal file
View File

@@ -0,0 +1,20 @@
use helix_event::{events, register_event};
use helix_view::document::Mode;
use helix_view::events::{DocumentDidChange, SelectionDidChange};
use crate::commands;
use crate::keymap::MappableCommand;
events! {
OnModeSwitch<'a, 'cx> { old_mode: Mode, new_mode: Mode, cx: &'a mut commands::Context<'cx> }
PostInsertChar<'a, 'cx> { c: char, cx: &'a mut commands::Context<'cx> }
PostCommand<'a, 'cx> { command: & 'a MappableCommand, cx: &'a mut commands::Context<'cx> }
}
pub fn register() {
register_event::<OnModeSwitch>();
register_event::<PostInsertChar>();
register_event::<PostCommand>();
register_event::<DocumentDidChange>();
register_event::<SelectionDidChange>();
}

View File

@@ -0,0 +1,29 @@
use std::sync::Arc;
use arc_swap::ArcSwap;
use helix_event::AsyncHook;
use crate::config::Config;
use crate::events;
use crate::handlers::completion::CompletionHandler;
use crate::handlers::signature_help::SignatureHelpHandler;
pub use completion::trigger_auto_completion;
pub use helix_view::handlers::Handlers;
pub mod completion;
mod signature_help;
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
events::register();
let completions = CompletionHandler::new(config).spawn();
let signature_hints = SignatureHelpHandler::new().spawn();
let handlers = Handlers {
completions,
signature_hints,
};
completion::register_hooks(&handlers);
signature_help::register_hooks(&handlers);
handlers
}

View File

@@ -0,0 +1,475 @@
use std::collections::HashSet;
use std::sync::Arc;
use std::time::Duration;
use arc_swap::ArcSwap;
use futures_util::stream::FuturesUnordered;
use helix_core::chars::char_is_word;
use helix_core::syntax::LanguageServerFeature;
use helix_event::{
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
};
use helix_lsp::lsp;
use helix_lsp::util::pos_to_lsp_pos;
use helix_stdx::rope::RopeSliceExt;
use helix_view::document::{Mode, SavePoint};
use helix_view::handlers::lsp::CompletionEvent;
use helix_view::{DocumentId, Editor, ViewId};
use tokio::sync::mpsc::Sender;
use tokio::time::Instant;
use tokio_stream::StreamExt;
use crate::commands;
use crate::compositor::Compositor;
use crate::config::Config;
use crate::events::{OnModeSwitch, PostCommand, PostInsertChar};
use crate::job::{dispatch, dispatch_blocking};
use crate::keymap::MappableCommand;
use crate::ui::editor::InsertEvent;
use crate::ui::lsp::SignatureHelp;
use crate::ui::{self, CompletionItem, Popup};
use super::Handlers;
pub use resolve::ResolveHandler;
mod resolve;
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum TriggerKind {
Auto,
TriggerChar,
Manual,
}
#[derive(Debug, Clone, Copy)]
struct Trigger {
pos: usize,
view: ViewId,
doc: DocumentId,
kind: TriggerKind,
}
#[derive(Debug)]
pub(super) struct CompletionHandler {
/// currently active trigger which will cause a
/// completion request after the timeout
trigger: Option<Trigger>,
/// A handle for currently active completion request.
/// This can be used to determine whether the current
/// request is still active (and new triggers should be
/// ignored) and can also be used to abort the current
/// request (by dropping the handle)
request: Option<CancelTx>,
config: Arc<ArcSwap<Config>>,
}
impl CompletionHandler {
pub fn new(config: Arc<ArcSwap<Config>>) -> CompletionHandler {
Self {
config,
request: None,
trigger: None,
}
}
}
impl helix_event::AsyncHook for CompletionHandler {
type Event = CompletionEvent;
fn handle_event(
&mut self,
event: Self::Event,
_old_timeout: Option<Instant>,
) -> Option<Instant> {
match event {
CompletionEvent::AutoTrigger {
cursor: trigger_pos,
doc,
view,
} => {
// techically it shouldn't be possible to switch views/documents in insert mode
// but people may create weird keymaps/use the mouse so lets be extra careful
if self
.trigger
.as_ref()
.map_or(true, |trigger| trigger.doc != doc || trigger.view != view)
{
self.trigger = Some(Trigger {
pos: trigger_pos,
view,
doc,
kind: TriggerKind::Auto,
});
}
}
CompletionEvent::TriggerChar { cursor, doc, view } => {
// immediately request completions and drop all auto completion requests
self.request = None;
self.trigger = Some(Trigger {
pos: cursor,
view,
doc,
kind: TriggerKind::TriggerChar,
});
}
CompletionEvent::ManualTrigger { cursor, doc, view } => {
// immediately request completions and drop all auto completion requests
self.request = None;
self.trigger = Some(Trigger {
pos: cursor,
view,
doc,
kind: TriggerKind::Manual,
});
// stop debouncing immediately and request the completion
self.finish_debounce();
return None;
}
CompletionEvent::Cancel => {
self.trigger = None;
self.request = None;
}
CompletionEvent::DeleteText { cursor } => {
// if we deleted the original trigger, abort the completion
if matches!(self.trigger, Some(Trigger{ pos, .. }) if cursor < pos) {
self.trigger = None;
self.request = None;
}
}
}
self.trigger.map(|trigger| {
// if the current request was closed forget about it
// otherwise immediately restart the completion request
let cancel = self.request.take().map_or(false, |req| !req.is_closed());
let timeout = if trigger.kind == TriggerKind::Auto && !cancel {
self.config.load().editor.completion_timeout
} else {
// we want almost instant completions for trigger chars
// and restarting completion requests. The small timeout here mainly
// serves to better handle cases where the completion handler
// may fall behind (so multiple events in the channel) and macros
Duration::from_millis(5)
};
Instant::now() + timeout
})
}
fn finish_debounce(&mut self) {
let trigger = self.trigger.take().expect("debounce always has a trigger");
let (tx, rx) = cancelation();
self.request = Some(tx);
dispatch_blocking(move |editor, compositor| {
request_completion(trigger, rx, editor, compositor)
});
}
}
fn request_completion(
mut trigger: Trigger,
cancel: CancelRx,
editor: &mut Editor,
compositor: &mut Compositor,
) {
let (view, doc) = current!(editor);
if compositor
.find::<ui::EditorView>()
.unwrap()
.completion
.is_some()
|| editor.mode != Mode::Insert
{
return;
}
let text = doc.text();
let cursor = doc.selection(view.id).primary().cursor(text.slice(..));
if trigger.view != view.id || trigger.doc != doc.id() || cursor < trigger.pos {
return;
}
// this looks odd... Why are we not using the trigger position from
// the `trigger` here? Won't that mean that the trigger char doesn't get
// send to the LS if we type fast enougn? Yes that is true but it's
// not actually a problem. The LSP will resolve the completion to the identifier
// anyway (in fact sending the later position is necessary to get the right results
// from LSPs that provide incomplete completion list). We rely on trigger offset
// and primary cursor matching for multi-cursor completions so this is definitely
// necessary from our side too.
trigger.pos = cursor;
let trigger_text = text.slice(..cursor);
let mut seen_language_servers = HashSet::new();
let mut futures: FuturesUnordered<_> = doc
.language_servers_with_feature(LanguageServerFeature::Completion)
.filter(|ls| seen_language_servers.insert(ls.id()))
.map(|ls| {
let language_server_id = ls.id();
let offset_encoding = ls.offset_encoding();
let pos = pos_to_lsp_pos(text, cursor, offset_encoding);
let doc_id = doc.identifier();
let context = if trigger.kind == TriggerKind::Manual {
lsp::CompletionContext {
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
trigger_character: None,
}
} else {
let trigger_char =
ls.capabilities()
.completion_provider
.as_ref()
.and_then(|provider| {
provider
.trigger_characters
.as_deref()?
.iter()
.find(|&trigger| trigger_text.ends_with(trigger))
});
if trigger_char.is_some() {
lsp::CompletionContext {
trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: trigger_char.cloned(),
}
} else {
lsp::CompletionContext {
trigger_kind: lsp::CompletionTriggerKind::INVOKED,
trigger_character: None,
}
}
};
let completion_response = ls.completion(doc_id, pos, None, context).unwrap();
async move {
let json = completion_response.await?;
let response: Option<lsp::CompletionResponse> = serde_json::from_value(json)?;
let items = match response {
Some(lsp::CompletionResponse::Array(items)) => items,
// TODO: do something with is_incomplete
Some(lsp::CompletionResponse::List(lsp::CompletionList {
is_incomplete: _is_incomplete,
items,
})) => items,
None => Vec::new(),
}
.into_iter()
.map(|item| CompletionItem {
item,
provider: language_server_id,
resolved: false,
})
.collect();
anyhow::Ok(items)
}
})
.collect();
let future = async move {
let mut items = Vec::new();
while let Some(lsp_items) = futures.next().await {
match lsp_items {
Ok(mut lsp_items) => items.append(&mut lsp_items),
Err(err) => {
log::debug!("completion request failed: {err:?}");
}
};
}
items
};
let savepoint = doc.savepoint(view);
let ui = compositor.find::<ui::EditorView>().unwrap();
ui.last_insert.1.push(InsertEvent::RequestCompletion);
tokio::spawn(async move {
let items = cancelable_future(future, cancel).await.unwrap_or_default();
if items.is_empty() {
return;
}
dispatch(move |editor, compositor| {
show_completion(editor, compositor, items, trigger, savepoint)
})
.await
});
}
fn show_completion(
editor: &mut Editor,
compositor: &mut Compositor,
items: Vec<CompletionItem>,
trigger: Trigger,
savepoint: Arc<SavePoint>,
) {
let (view, doc) = current_ref!(editor);
// check if the completion request is stale.
//
// Completions are completed asynchronously and therefore the user could
//switch document/view or leave insert mode. In all of thoise cases the
// completion should be discarded
if editor.mode != Mode::Insert || view.id != trigger.view || doc.id() != trigger.doc {
return;
}
let size = compositor.size();
let ui = compositor.find::<ui::EditorView>().unwrap();
if ui.completion.is_some() {
return;
}
let completion_area = ui.set_completion(editor, savepoint, items, trigger.pos, size);
let signature_help_area = compositor
.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID)
.map(|signature_help| signature_help.area(size, editor));
// Delete the signature help popup if they intersect.
if matches!((completion_area, signature_help_area),(Some(a), Some(b)) if a.intersects(b)) {
compositor.remove(SignatureHelp::ID);
}
}
pub fn trigger_auto_completion(
tx: &Sender<CompletionEvent>,
editor: &Editor,
trigger_char_only: bool,
) {
let config = editor.config.load();
if !config.auto_completion {
return;
}
let (view, doc): (&helix_view::View, &helix_view::Document) = current_ref!(editor);
let mut text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
text = doc.text().slice(..cursor);
let is_trigger_char = doc
.language_servers_with_feature(LanguageServerFeature::Completion)
.any(|ls| {
matches!(&ls.capabilities().completion_provider, Some(lsp::CompletionOptions {
trigger_characters: Some(triggers),
..
}) if triggers.iter().any(|trigger| text.ends_with(trigger)))
});
if is_trigger_char {
send_blocking(
tx,
CompletionEvent::TriggerChar {
cursor,
doc: doc.id(),
view: view.id,
},
);
return;
}
let is_auto_trigger = !trigger_char_only
&& doc
.text()
.chars_at(cursor)
.reversed()
.take(config.completion_trigger_len as usize)
.all(char_is_word);
if is_auto_trigger {
send_blocking(
tx,
CompletionEvent::AutoTrigger {
cursor,
doc: doc.id(),
view: view.id,
},
);
}
}
fn update_completions(cx: &mut commands::Context, c: Option<char>) {
cx.callback.push(Box::new(move |compositor, cx| {
let editor_view = compositor.find::<ui::EditorView>().unwrap();
if let Some(completion) = &mut editor_view.completion {
completion.update_filter(c);
if completion.is_empty() {
editor_view.clear_completion(cx.editor);
// clearing completions might mean we want to immediately rerequest them (usually
// this occurs if typing a trigger char)
if c.is_some() {
trigger_auto_completion(&cx.editor.handlers.completions, cx.editor, false);
}
}
}
}))
}
fn clear_completions(cx: &mut commands::Context) {
cx.callback.push(Box::new(|compositor, cx| {
let editor_view = compositor.find::<ui::EditorView>().unwrap();
editor_view.clear_completion(cx.editor);
}))
}
fn completion_post_command_hook(
tx: &Sender<CompletionEvent>,
PostCommand { command, cx }: &mut PostCommand<'_, '_>,
) -> anyhow::Result<()> {
if cx.editor.mode == Mode::Insert {
if cx.editor.last_completion.is_some() {
match command {
MappableCommand::Static {
name: "delete_word_forward" | "delete_char_forward" | "completion",
..
} => (),
MappableCommand::Static {
name: "delete_char_backward",
..
} => update_completions(cx, None),
_ => clear_completions(cx),
}
} else {
let event = match command {
MappableCommand::Static {
name: "delete_char_backward" | "delete_word_forward" | "delete_char_forward",
..
} => {
let (view, doc) = current!(cx.editor);
let primary_cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
CompletionEvent::DeleteText {
cursor: primary_cursor,
}
}
// hacks: some commands are handeled elsewhere and we don't want to
// cancel in that case
MappableCommand::Static {
name: "completion" | "insert_mode" | "append_mode",
..
} => return Ok(()),
_ => CompletionEvent::Cancel,
};
send_blocking(tx, event);
}
}
Ok(())
}
pub(super) fn register_hooks(handlers: &Handlers) {
let tx = handlers.completions.clone();
register_hook!(move |event: &mut PostCommand<'_, '_>| completion_post_command_hook(&tx, event));
let tx = handlers.completions.clone();
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
if event.old_mode == Mode::Insert {
send_blocking(&tx, CompletionEvent::Cancel);
clear_completions(event.cx);
} else if event.new_mode == Mode::Insert {
trigger_auto_completion(&tx, event.cx.editor, false)
}
Ok(())
});
let tx = handlers.completions.clone();
register_hook!(move |event: &mut PostInsertChar<'_, '_>| {
if event.cx.editor.last_completion.is_some() {
update_completions(event.cx, Some(event.c))
} else {
trigger_auto_completion(&tx, event.cx.editor, false);
}
Ok(())
});
}

View File

@@ -0,0 +1,153 @@
use std::sync::Arc;
use helix_lsp::lsp;
use tokio::sync::mpsc::Sender;
use tokio::time::{Duration, Instant};
use helix_event::{send_blocking, AsyncHook, CancelRx};
use helix_view::Editor;
use crate::handlers::completion::CompletionItem;
use crate::job;
/// A hook for resolving incomplete completion items.
///
/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion):
///
/// > If computing full completion items is expensive, servers can additionally provide a
/// > handler for the completion item resolve request. ...
/// > A typical use case is for example: the `textDocument/completion` request doesn't fill
/// > in the `documentation` property for returned completion items since it is expensive
/// > to compute. When the item is selected in the user interface then a
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
/// > The returned completion item should have the documentation property filled in.
pub struct ResolveHandler {
last_request: Option<Arc<CompletionItem>>,
resolver: Sender<ResolveRequest>,
}
impl ResolveHandler {
pub fn new() -> ResolveHandler {
ResolveHandler {
last_request: None,
resolver: ResolveTimeout {
next_request: None,
in_flight: None,
}
.spawn(),
}
}
pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut CompletionItem) {
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 {
item.resolved = true;
return;
}
if self.last_request.as_deref().is_some_and(|it| it == item) {
return;
}
let Some(ls) = editor.language_servers.get_by_id(item.provider).cloned() else {
item.resolved = true;
return;
};
if matches!(
ls.capabilities().completion_provider,
Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..
})
) {
let item = Arc::new(item.clone());
self.last_request = Some(item.clone());
send_blocking(&self.resolver, ResolveRequest { item, ls })
} else {
item.resolved = true;
}
}
}
struct ResolveRequest {
item: Arc<CompletionItem>,
ls: Arc<helix_lsp::Client>,
}
#[derive(Default)]
struct ResolveTimeout {
next_request: Option<ResolveRequest>,
in_flight: Option<(helix_event::CancelTx, Arc<CompletionItem>)>,
}
impl AsyncHook for ResolveTimeout {
type Event = ResolveRequest;
fn handle_event(
&mut self,
request: Self::Event,
timeout: Option<tokio::time::Instant>,
) -> Option<tokio::time::Instant> {
if self
.next_request
.as_ref()
.is_some_and(|old_request| old_request.item == request.item)
{
timeout
} else if self
.in_flight
.as_ref()
.is_some_and(|(_, old_request)| old_request.item == request.item.item)
{
self.next_request = None;
None
} else {
self.next_request = Some(request);
Some(Instant::now() + Duration::from_millis(150))
}
}
fn finish_debounce(&mut self) {
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));
}
}
impl ResolveRequest {
async fn execute(self, cancel: CancelRx) {
let future = self.ls.resolve_completion_item(&self.item.item);
let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else {
return;
};
job::dispatch(move |_, compositor| {
if let Some(completion) = &mut compositor
.find::<crate::ui::EditorView>()
.unwrap()
.completion
{
let resolved_item = match resolved_item {
Ok(item) => CompletionItem {
item,
resolved: true,
..*self.item
},
Err(err) => {
log::error!("completion resolve request failed: {err}");
// set item to resolved so we don't request it again
// we could also remove it but that oculd be odd ui
let mut item = (*self.item).clone();
item.resolved = true;
item
}
};
completion.replace_item(&self.item, resolved_item);
};
})
.await
}
}

View File

@@ -0,0 +1,357 @@
use std::sync::Arc;
use std::time::Duration;
use helix_core::syntax::LanguageServerFeature;
use helix_event::{
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
};
use helix_lsp::lsp::{self, SignatureInformation};
use helix_stdx::rope::RopeSliceExt;
use helix_view::document::Mode;
use helix_view::events::{DocumentDidChange, SelectionDidChange};
use helix_view::handlers::lsp::{SignatureHelpEvent, SignatureHelpInvoked};
use helix_view::Editor;
use tokio::sync::mpsc::Sender;
use tokio::time::Instant;
use crate::commands::Open;
use crate::compositor::Compositor;
use crate::events::{OnModeSwitch, PostInsertChar};
use crate::handlers::Handlers;
use crate::ui::lsp::{Signature, SignatureHelp};
use crate::ui::Popup;
use crate::{job, ui};
#[derive(Debug)]
enum State {
Open,
Closed,
Pending { request: CancelTx },
}
/// debounce timeout in ms, value taken from VSCode
/// TODO: make this configurable?
const TIMEOUT: u64 = 120;
#[derive(Debug)]
pub(super) struct SignatureHelpHandler {
trigger: Option<SignatureHelpInvoked>,
state: State,
}
impl SignatureHelpHandler {
pub fn new() -> SignatureHelpHandler {
SignatureHelpHandler {
trigger: None,
state: State::Closed,
}
}
}
impl helix_event::AsyncHook for SignatureHelpHandler {
type Event = SignatureHelpEvent;
fn handle_event(
&mut self,
event: Self::Event,
timeout: Option<tokio::time::Instant>,
) -> Option<Instant> {
match event {
SignatureHelpEvent::Invoked => {
self.trigger = Some(SignatureHelpInvoked::Manual);
self.state = State::Closed;
self.finish_debounce();
return None;
}
SignatureHelpEvent::Trigger => {}
SignatureHelpEvent::ReTrigger => {
// don't retrigger if we aren't open/pending yet
if matches!(self.state, State::Closed) {
return timeout;
}
}
SignatureHelpEvent::Cancel => {
self.state = State::Closed;
return None;
}
SignatureHelpEvent::RequestComplete { open } => {
// don't cancel rerequest that was already triggered
if let State::Pending { request } = &self.state {
if !request.is_closed() {
return timeout;
}
}
self.state = if open { State::Open } else { State::Closed };
return timeout;
}
}
if self.trigger.is_none() {
self.trigger = Some(SignatureHelpInvoked::Automatic)
}
Some(Instant::now() + Duration::from_millis(TIMEOUT))
}
fn finish_debounce(&mut self) {
let invocation = self.trigger.take().unwrap();
let (tx, rx) = cancelation();
self.state = State::Pending { request: tx };
job::dispatch_blocking(move |editor, _| request_signature_help(editor, invocation, rx))
}
}
pub fn request_signature_help(
editor: &mut Editor,
invoked: SignatureHelpInvoked,
cancel: CancelRx,
) {
let (view, doc) = current!(editor);
// TODO merge multiple language server signature help into one instead of just taking the first language server that supports it
let future = doc
.language_servers_with_feature(LanguageServerFeature::SignatureHelp)
.find_map(|language_server| {
let pos = doc.position(view.id, language_server.offset_encoding());
language_server.text_document_signature_help(doc.identifier(), pos, None)
});
let Some(future) = future else {
// 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");
}
return;
};
tokio::spawn(async move {
match cancelable_future(future, cancel).await {
Some(Ok(res)) => {
job::dispatch(move |editor, compositor| {
show_signature_help(editor, compositor, invoked, res)
})
.await
}
Some(Err(err)) => log::error!("signature help request failed: {err}"),
None => (),
}
});
}
fn active_param_range(
signature: &SignatureInformation,
response_active_parameter: Option<u32>,
) -> Option<(usize, usize)> {
let param_idx = signature
.active_parameter
.or(response_active_parameter)
.unwrap_or(0) as usize;
let param = signature.parameters.as_ref()?.get(param_idx)?;
match &param.label {
lsp::ParameterLabel::Simple(string) => {
let start = signature.label.find(string.as_str())?;
Some((start, start + string.len()))
}
lsp::ParameterLabel::LabelOffsets([start, end]) => {
// LS sends offsets based on utf-16 based string representation
// but highlighting in helix is done using byte offset.
use helix_core::str_utils::char_to_byte_idx;
let from = char_to_byte_idx(&signature.label, *start as usize);
let to = char_to_byte_idx(&signature.label, *end as usize);
Some((from, to))
}
}
}
pub fn show_signature_help(
editor: &mut Editor,
compositor: &mut Compositor,
invoked: SignatureHelpInvoked,
response: Option<lsp::SignatureHelp>,
) {
let config = &editor.config();
if !(config.lsp.auto_signature_help
|| SignatureHelp::visible_popup(compositor).is_some()
|| invoked == SignatureHelpInvoked::Manual)
{
return;
}
// If the signature help invocation is automatic, don't show it outside of Insert Mode:
// it very probably means the server was a little slow to respond and the user has
// already moved on to something else, making a signature help popup will just be an
// annoyance, see https://github.com/helix-editor/helix/issues/3112
// For the most part this should not be needed as the request gets canceled automatically now
// but it's technically possible for the mode change to just preempt this callback so better safe than sorry
if invoked == SignatureHelpInvoked::Automatic && editor.mode != Mode::Insert {
return;
}
let response = match response {
// According to the spec the response should be None if there
// are no signatures, but some servers don't follow this.
Some(s) if !s.signatures.is_empty() => s,
_ => {
send_blocking(
&editor.handlers.signature_hints,
SignatureHelpEvent::RequestComplete { open: false },
);
compositor.remove(SignatureHelp::ID);
return;
}
};
send_blocking(
&editor.handlers.signature_hints,
SignatureHelpEvent::RequestComplete { open: true },
);
let doc = doc!(editor);
let language = doc.language_name().unwrap_or("");
if response.signatures.is_empty() {
return;
}
let signatures: Vec<Signature> = response
.signatures
.into_iter()
.map(|s| {
let active_param_range = active_param_range(&s, response.active_parameter);
let signature_doc = if config.lsp.display_signature_help_docs {
s.documentation.map(|doc| match doc {
lsp::Documentation::String(s) => s,
lsp::Documentation::MarkupContent(markup) => markup.value,
})
} else {
None
};
Signature {
signature: s.label,
signature_doc,
active_param_range,
}
})
.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);
if active_signature >= signatures.len() {
active_signature = signatures.len() - 1;
}
let contents = SignatureHelp::new(
language.to_string(),
Arc::clone(&editor.syn_loader),
active_signature,
signatures,
);
let mut popup = Popup::new(SignatureHelp::ID, contents)
.position(old_popup.and_then(|p| p.get_position()))
.position_bias(Open::Above)
.ignore_escape_key(true);
// Don't create a popup if it intersects the auto-complete menu.
let size = compositor.size();
if compositor
.find::<ui::EditorView>()
.unwrap()
.completion
.as_mut()
.map(|completion| completion.area(size, editor))
.filter(|area| area.intersects(popup.area(size, editor)))
.is_some()
{
return;
}
compositor.replace_or_push(SignatureHelp::ID, popup);
}
fn signature_help_post_insert_char_hook(
tx: &Sender<SignatureHelpEvent>,
PostInsertChar { cx, .. }: &mut PostInsertChar<'_, '_>,
) -> anyhow::Result<()> {
if !cx.editor.config().lsp.auto_signature_help {
return Ok(());
}
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(());
};
let capabilities = language_server.capabilities();
if let lsp::ServerCapabilities {
signature_help_provider:
Some(lsp::SignatureHelpOptions {
trigger_characters: Some(triggers),
// TODO: retrigger_characters
..
}),
..
} = capabilities
{
let mut text = doc.text().slice(..);
let cursor = doc.selection(view.id).primary().cursor(text);
text = text.slice(..cursor);
if triggers.iter().any(|trigger| text.ends_with(trigger)) {
send_blocking(tx, SignatureHelpEvent::Trigger)
}
}
Ok(())
}
pub(super) fn register_hooks(handlers: &Handlers) {
let tx = handlers.signature_hints.clone();
register_hook!(move |event: &mut OnModeSwitch<'_, '_>| {
match (event.old_mode, event.new_mode) {
(Mode::Insert, _) => {
send_blocking(&tx, SignatureHelpEvent::Cancel);
event.cx.callback.push(Box::new(|compositor, _| {
compositor.remove(SignatureHelp::ID);
}));
}
(_, Mode::Insert) => {
if event.cx.editor.config().lsp.auto_signature_help {
send_blocking(&tx, SignatureHelpEvent::Trigger);
}
}
_ => (),
}
Ok(())
});
let tx = handlers.signature_hints.clone();
register_hook!(
move |event: &mut PostInsertChar<'_, '_>| signature_help_post_insert_char_hook(&tx, event)
);
let tx = handlers.signature_hints.clone();
register_hook!(move |event: &mut DocumentDidChange<'_>| {
if event.doc.config.load().lsp.auto_signature_help {
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
}
Ok(())
});
let tx = handlers.signature_hints.clone();
register_hook!(move |event: &mut SelectionDidChange<'_>| {
if event.doc.config.load().lsp.auto_signature_help {
send_blocking(&tx, SignatureHelpEvent::ReTrigger);
}
Ok(())
});
}

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