Compare commits

...

361 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
369 changed files with 14437 additions and 4282 deletions

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
@@ -31,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
@@ -65,6 +67,7 @@ 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
@@ -92,6 +95,7 @@ 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

View File

@@ -14,7 +14,7 @@ jobs:
uses: actions/checkout@v4
- name: Install nix
uses: cachix/install-nix-action@v25
uses: cachix/install-nix-action@v26
- name: Authenticate with Cachix
uses: cachix/cachix-action@v14

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'
@@ -27,14 +27,14 @@ jobs:
echo "OUTDIR=$OUTDIR" >> $GITHUB_ENV
- 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@v3
uses: peaceiris/actions-gh-pages@v4
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book

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.

661
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -37,11 +37,12 @@ package.helix-tui.opt-level = 2
package.helix-term.opt-level = 2
[workspace.dependencies]
tree-sitter = { version = "0.20", git = "https://github.com/helix-editor/tree-sitter", rev = "660481dbf71413eba5a928b0b0ab8da50c1109e0" }
tree-sitter = { version = "0.22" }
nucleo = "0.2.0"
slotmap = "1.0.7"
[workspace.package]
version = "23.10.0"
version = "24.3.0"
edition = "2021"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
categories = ["editor"]

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

@@ -68,6 +68,7 @@ Its settings will be merged with the configuration directory `config.toml` and t
| `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
@@ -75,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:
@@ -108,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 |
@@ -177,7 +179,7 @@ All git related options are only enabled in a git repository.
|`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 | Defaults to `None`.
|`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.
@@ -223,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]]
@@ -253,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
@@ -265,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)
@@ -340,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
@@ -370,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,7 @@
| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP |
| --- | --- | --- | --- | --- |
| ada | ✓ | ✓ | | `ada_language_server`, `ada_language_server` |
| adl | ✓ | ✓ | ✓ | |
| agda | ✓ | | | |
| astro | ✓ | | | |
| awk | ✓ | ✓ | | `awk-language-server` |
@@ -8,12 +10,15 @@
| beancount | ✓ | | | |
| bibtex | ✓ | | | `texlab` |
| bicep | ✓ | | | `bicep-langserver` |
| bitbake | ✓ | | | `bitbake-language-server` |
| blade | ✓ | | | |
| blueprint | ✓ | | | `blueprint-compiler` |
| c | ✓ | ✓ | ✓ | `clangd` |
| c-sharp | ✓ | ✓ | | `OmniSharp` |
| cabal | | | | `haskell-language-server-wrapper` |
| cairo | ✓ | ✓ | ✓ | `cairo-language-server` |
| capnp | ✓ | | ✓ | |
| cel | ✓ | | | |
| clojure | ✓ | | | `clojure-lsp` |
| cmake | ✓ | ✓ | ✓ | `cmake-language-server` |
| comment | ✓ | | | |
@@ -29,9 +34,11 @@
| 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 | ✓ | | | |
@@ -42,6 +49,7 @@
| erb | ✓ | | | |
| erlang | ✓ | ✓ | | `erlang_ls` |
| esdl | ✓ | | | |
| fidl | ✓ | | | |
| fish | ✓ | ✓ | ✓ | |
| forth | ✓ | | | `forth-lsp` |
| fortran | ✓ | | ✓ | `fortls` |
@@ -55,6 +63,7 @@
| git-ignore | ✓ | | | |
| git-rebase | ✓ | | | |
| gleam | ✓ | ✓ | | `gleam` |
| glimmer | ✓ | | | `ember-language-server` |
| glsl | ✓ | ✓ | ✓ | |
| gn | ✓ | | | |
| go | ✓ | ✓ | ✓ | `gopls`, `golangci-lint-langserver` |
@@ -62,16 +71,20 @@
| 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 | ✓ | | | |
@@ -80,15 +93,19 @@
| 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 | ✓ | ✓ | ✓ | |
| koka | ✓ | | ✓ | `koka` |
| kotlin | ✓ | | | `kotlin-language-server` |
| latex | ✓ | ✓ | | `texlab` |
| ld | ✓ | | ✓ | |
| ldif | ✓ | | | |
| lean | ✓ | | | `lean` |
| ledger | ✓ | | | |
| llvm | ✓ | ✓ | ✓ | |
@@ -99,22 +116,24 @@
| lua | ✓ | ✓ | ✓ | `lua-language-server` |
| 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` |
| nix | ✓ | | | `nil` |
| nu | ✓ | | | `nu` |
| nunjucks | ✓ | | | |
| ocaml | ✓ | | ✓ | `ocamllsp` |
| ocaml-interface | ✓ | | | `ocamllsp` |
| odin | ✓ | | ✓ | `ols` |
| ohm | ✓ | ✓ | ✓ | |
| opencl | ✓ | ✓ | ✓ | `clangd` |
| openscad | ✓ | | | `openscad-lsp` |
| org | ✓ | | | |
@@ -123,9 +142,13 @@
| 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` |
@@ -148,35 +171,39 @@
| 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 | ✓ | | | `typst-lsp` |
| 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 | ✓ | | | |
@@ -189,6 +216,7 @@
| wren | ✓ | ✓ | ✓ | |
| xit | ✓ | | | |
| xml | ✓ | | ✓ | |
| xtc | ✓ | | | |
| yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` |
| yuck | ✓ | | | |
| zig | ✓ | ✓ | ✓ | `zls` |

View File

@@ -86,3 +86,5 @@
| `: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

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

@@ -76,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.

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,6 +24,8 @@
> 💡 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. You can return to it from other modes by pressing the `Escape` key.
@@ -48,13 +51,13 @@ Normal mode is the default mode when you launch helix. You can return to it from
| `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
@@ -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
@@ -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,7 +61,8 @@ 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`) |
@@ -78,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
@@ -120,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).
@@ -146,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
@@ -251,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
@@ -314,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 |
@@ -333,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

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

24
flake.lock generated
View File

@@ -7,11 +7,11 @@
]
},
"locked": {
"lastModified": 1701025348,
"narHash": "sha256-42GHmYH+GF7VjwGSt+fVT1CQuNpGanJbNgVHTAZppUM=",
"lastModified": 1709610799,
"narHash": "sha256-5jfLQx0U9hXbi2skYMGodDJkIgffrjIOgMRjZqms2QE=",
"owner": "ipetkov",
"repo": "crane",
"rev": "42afaeb1a0325194a7cdb526332d2cb92fddd07b",
"rev": "81c393c776d5379c030607866afef6406ca1be57",
"type": "github"
},
"original": {
@@ -25,11 +25,11 @@
"systems": "systems"
},
"locked": {
"lastModified": 1694529238,
"narHash": "sha256-zsNZZGTGnMOf9YpHKJqMSsa0dXbfmxeoJ7xHlrt+xmY=",
"lastModified": 1709126324,
"narHash": "sha256-q6EQdSeUZOG26WelxqkmR7kArjgWCdw5sfJVHPH/7j8=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "ff7b65b44d01cf9ba6a71320833626af21126384",
"rev": "d465f4819400de7c8d874d50b982301f28a84605",
"type": "github"
},
"original": {
@@ -40,11 +40,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1700794826,
"narHash": "sha256-RyJTnTNKhO0yqRpDISk03I/4A67/dp96YRxc86YOPgU=",
"lastModified": 1709479366,
"narHash": "sha256-n6F0n8UV6lnTZbYPl1A9q1BS0p4hduAv1mGAP17CVd0=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "5a09cb4b393d58f9ed0d9ca1555016a8543c2ac8",
"rev": "b8697e57f10292a6165a20f03d2f42920dfaf973",
"type": "github"
},
"original": {
@@ -72,11 +72,11 @@
]
},
"locked": {
"lastModified": 1701137803,
"narHash": "sha256-0LcPAdql5IhQSUXJx3Zna0dYTgdIoYO7zUrsKgiBd04=",
"lastModified": 1709604635,
"narHash": "sha256-le4fwmWmjGRYWwkho0Gr7mnnZndOOe4XGbLw68OvF40=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "9dd940c967502f844eacea52a61e9596268d4f70",
"rev": "e86c0fb5d3a22a5f30d7f64ecad88643fe26449d",
"type": "github"
},
"original": {

View File

@@ -22,24 +22,23 @@ helix-loader = { path = "../helix-loader" }
ropey = { version = "1.6.1", default-features = false, features = ["simd"] }
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.19"
arc-swap = "1"
regex = "1"
bitflags = "2.4"
ahash = "0.8.6"
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"
@@ -48,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`].
@@ -47,8 +49,25 @@ pub struct Diagnostic {
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,10 +1,10 @@
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},
find_first_non_whitespace_char,
graphemes::{grapheme_width, tab_width_at},
syntax::{IndentationHeuristic, LanguageConfiguration, RopeProvider, Syntax},
tree_sitter::Node,
@@ -247,41 +247,18 @@ fn add_indent_level(
}
}
/// 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));
} 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;
/// 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;
}
}
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);
}
}
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.
@@ -852,7 +829,6 @@ pub fn treesitter_indent_for_pos<'a>(
byte_pos,
new_line_byte_pos,
)?;
let mut first_in_line = get_first_in_line(node, new_line.then_some(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
@@ -861,9 +837,7 @@ pub fn treesitter_indent_for_pos<'a>(
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
@@ -906,7 +880,6 @@ pub fn treesitter_indent_for_pos<'a>(
}
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.
@@ -970,7 +943,7 @@ pub fn indent_for_newline(
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 find_first_non_whitespace_char(line) {
let first_non_whitespace_char = match line.first_non_whitespace_char() {
Some(i) => i,
None => {
continue;

View File

@@ -37,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 + 1)?;
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,9 +118,7 @@ fn find_pair(
};
node = parent;
}
let node = tree
.root_node()
.named_descendant_for_byte_range(pos, pos + 1)?;
let node = root.named_descendant_for_byte_range(pos, pos + 1)?;
if node.child_count() != 0 {
return None;
}

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

@@ -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)]
@@ -103,7 +103,19 @@ pub struct LanguageConfiguration {
pub shebangs: Vec<String>, // interpreter(s) associated with language
#[serde(default)]
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>,
#[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>,
@@ -164,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 {
@@ -178,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()
}
}
@@ -213,9 +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({
suffix.replace('/', std::path::MAIN_SEPARATOR_STR)
})),
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 {
@@ -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)]
@@ -709,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!(
@@ -752,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)]
@@ -759,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>,
@@ -767,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())
@@ -938,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> {
@@ -949,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,
@@ -962,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
@@ -993,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))
};
@@ -1231,6 +1365,7 @@ impl Syntax {
depth,
ranges,
flags: LayerUpdateFlags::empty(),
parent: Some(layer_id),
};
// Find an identical existing layer
@@ -1338,7 +1473,7 @@ impl Syntax {
result
}
pub fn descendant_for_byte_range(&self, start: usize, end: usize) -> Option<Node<'_>> {
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() {
@@ -1349,12 +1484,27 @@ impl Syntax {
}
}
self.layers[container_id]
.tree()
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
@@ -1387,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
@@ -1406,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);
}
}
@@ -1429,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) };
@@ -1588,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;
@@ -1729,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);
@@ -1738,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
@@ -1889,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);
}
@@ -2306,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 {
@@ -2516,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();
@@ -2582,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| {
@@ -2608,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]
@@ -2644,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(
@@ -2664,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");
@@ -2750,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()
@@ -2770,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:
@@ -2789,7 +2958,8 @@ mod test {
" (macro_invocation\n",
" macro: (identifier)\n",
" (token_tree\n",
" (string_literal))))))",
" (string_literal\n",
" (string_content)))))))",
),
0,
source.len(),
@@ -2808,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,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,7 +212,7 @@ 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,

View File

@@ -2,7 +2,7 @@ use crate::{
requests::DisconnectArguments,
transport::{Payload, Request, Response, Transport},
types::*,
Error, Result, ThreadId,
Error, Result,
};
use helix_core::syntax::DebuggerQuirks;

View File

@@ -12,7 +12,7 @@ homepage.workspace = true
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
ahash = "0.8.3"
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

View File

@@ -19,7 +19,7 @@ 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.19"
@@ -30,7 +30,7 @@ log = "0.4"
# cloning/compiling tree-sitter grammars
cc = { version = "1" }
threadpool = { version = "1.0" }
tempfile = "3.9.0"
tempfile = "3.10.1"
dunce = "1.0.4"
[target.'cfg(not(target_arch = "wasm32"))'.dependencies]

View File

@@ -53,7 +53,7 @@ fn prioritize_runtime_dirs() -> Vec<PathBuf> {
rt_dirs.push(conf_rt_dir);
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
let dir = path::expand_tilde(dir);
let dir = path::expand_tilde(Path::new(&dir));
rt_dirs.push(path::normalize(dir));
}
@@ -126,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

View File

@@ -27,6 +27,8 @@ lsp-types = { version = "0.95" }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
tokio = { version = "1.35", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.14"
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,28 +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, syntax::LanguageServerFeature, ChangeSet, Rope};
use helix_loader::{self, VERSION_AND_GIT_HASH};
use helix_loader::VERSION_AND_GIT_HASH;
use helix_stdx::path;
use lsp::{
notification::DidChangeWorkspaceFolders, CodeActionCapabilityResolveSupport,
DidChangeWorkspaceFoldersParams, OneOf, PositionEncodingKind, SignatureHelp, 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},
@@ -45,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>,
@@ -175,13 +177,16 @@ 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 = helix_stdx::env::which(cmd)?;
@@ -204,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::normalize(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()
@@ -233,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,
@@ -248,7 +238,7 @@ impl Client {
&self.name
}
pub fn id(&self) -> usize {
pub fn id(&self) -> LanguageServerId {
self.id
}
@@ -278,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 {
@@ -402,6 +397,16 @@ 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,
{
@@ -410,7 +415,7 @@ impl Client {
fn call_with_timeout<R: lsp::request::Request>(
&self,
params: R::Params,
params: &R::Params,
timeout_secs: u64,
) -> impl Future<Output = Result<Value>>
where
@@ -419,17 +424,16 @@ impl Client {
let server_tx = self.server_tx.clone();
let id = self.next_request_id();
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);
@@ -640,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 {
@@ -717,30 +727,30 @@ impl Client {
})
}
pub fn prepare_file_rename(
pub fn will_rename(
&self,
old_uri: &lsp::Url,
new_uri: &lsp::Url,
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = Result<lsp::WorkspaceEdit>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support willRename feature
match &capabilities.workspace {
Some(workspace) => match &workspace.file_operations {
Some(op) => {
op.will_rename.as_ref()?;
}
_ => return None,
},
_ => return None,
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: old_uri.to_string(),
new_uri: new_uri.to_string(),
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 },
&lsp::RenameFilesParams { files },
5,
);
@@ -751,27 +761,28 @@ impl Client {
})
}
pub fn did_file_rename(
pub fn did_rename(
&self,
old_uri: &lsp::Url,
new_uri: &lsp::Url,
old_path: &Path,
new_path: &Path,
is_dir: bool,
) -> Option<impl Future<Output = std::result::Result<(), Error>>> {
let capabilities = self.capabilities.get().unwrap();
// Return early if the server does not support DidRename feature
match &capabilities.workspace {
Some(workspace) => match &workspace.file_operations {
Some(op) => {
op.did_rename.as_ref()?;
}
_ => return None,
},
_ => return None,
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: old_uri.to_string(),
new_uri: new_uri.to_string(),
old_uri: url_from_path(old_path)?,
new_uri: url_from_path(new_path)?,
}];
Some(self.notify::<lsp::notification::DidRenameFiles>(lsp::RenameFilesParams { files }))
}
@@ -1024,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(

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;
@@ -15,6 +17,7 @@ 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 {
@@ -282,7 +286,8 @@ pub mod util {
.chars_at(cursor)
.skip(1)
.take_while(|ch| chars::char_is_word(*ch))
.count();
.count()
+ 1;
}
(start, end)
}
@@ -537,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)
}),
)
@@ -638,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(
@@ -679,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,
@@ -714,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,
@@ -723,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;
});
@@ -746,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;
});
@@ -763,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(
@@ -780,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()
}
}
@@ -816,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 {
@@ -824,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()
@@ -855,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))
@@ -864,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> {
@@ -875,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);

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

@@ -16,6 +16,14 @@ 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.9"
tempfile = "3.10"

View File

@@ -42,8 +42,9 @@ pub fn binary_exists<T: AsRef<OsStr>>(binary_name: T) -> bool {
pub fn which<T: AsRef<OsStr>>(
binary_name: T,
) -> Result<std::path::PathBuf, ExecutableNotFoundError> {
which::which(binary_name.as_ref()).map_err(|err| ExecutableNotFoundError {
command: binary_name.as_ref().to_string_lossy().into_owned(),
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,
})
}

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)
}

View File

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

View File

@@ -1,37 +1,52 @@
pub use etcetera::home_dir;
use std::path::{Component, Path, PathBuf};
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(path: &Path) -> PathBuf {
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) {
return PathBuf::from("~").join(stripped);
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.to_path_buf()
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(path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
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());
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.to_path_buf()
path
}
/// Normalize a path without resolving symlinks.
@@ -109,9 +124,9 @@ pub fn normalize(path: impl AsRef<Path>) -> PathBuf {
/// This function is used instead of [`std::fs::canonicalize`] because we don't want to verify
/// here if the path exists, just normalize it's components.
pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
let path = expand_tilde(path);
let path = expand_tilde(path.as_ref());
let path = if path.is_relative() {
current_working_dir().join(path)
Cow::Owned(current_working_dir().join(path))
} else {
path
};
@@ -119,18 +134,21 @@ pub fn canonicalize(path: impl AsRef<Path>) -> PathBuf {
normalize(path)
}
pub fn get_relative_path(path: impl AsRef<Path>) -> PathBuf {
let path = PathBuf::from(path.as_ref());
let path = if path.is_absolute() {
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());
normalize(&path)
.strip_prefix(cwdir)
.map(PathBuf::from)
.unwrap_or(path)
} else {
path
};
fold_home_dir(&path)
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
@@ -164,22 +182,50 @@ pub fn get_relative_path(path: impl AsRef<Path>) -> PathBuf {
///
pub fn get_truncated_path(path: impl AsRef<Path>) -> PathBuf {
let cwd = current_working_dir();
let path = path
.as_ref()
.strip_prefix(cwd)
.unwrap_or_else(|_| path.as_ref());
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::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 {
ret.push(
d.to_string_lossy()
.chars()
.next()
.unwrap_or_default()
.to_string(),
);
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);
}
}
}

View File

@@ -1,11 +1,42 @@
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: Sized {
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 RopeSliceExt for RopeSlice<'_> {
impl<'a> RopeSliceExt<'a> for RopeSlice<'a> {
fn ends_with(self, text: &str) -> bool {
let len = self.len_bytes();
if len < text.len() {
@@ -23,4 +54,87 @@ impl RopeSliceExt for RopeSlice<'_> {
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
);
}
}
}
}

View File

@@ -41,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"
@@ -52,16 +53,16 @@ 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.0.1"
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"] }
@@ -72,7 +73,7 @@ grep-searcher = "0.1.13"
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }
libc = "0.2.152"
libc = "0.2.153"
[target.'cfg(target_os = "macos")'.dependencies]
crossterm = { version = "0.27", features = ["event-stream", "use-dev-tty"] }
@@ -82,5 +83,5 @@ helix-loader = { path = "../helix-loader" }
[dev-dependencies]
smallvec = "1.13"
indoc = "2.0.4"
tempfile = "3.9.0"
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

@@ -4,7 +4,7 @@ use helix_core::{diagnostic::Severity, pos_at_coords, syntax, Selection};
use helix_lsp::{
lsp::{self, notification::Notification},
util::lsp_range_to_range,
LspProgressMap,
LanguageServerId, LspProgressMap,
};
use helix_stdx::path::get_relative_path;
use helix_view::{
@@ -21,7 +21,6 @@ use tui::backend::Backend;
use crate::{
args::Args,
commands::apply_workspace_edit,
compositor::{Compositor, Event},
config::Config,
handlers,
@@ -67,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,
@@ -97,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();
@@ -127,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);
@@ -395,10 +390,9 @@ 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());
@@ -573,26 +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);
// and again a borrow checker workaround...
let doc = doc_mut!(self.editor, &doc_save_event.doc_id);
let diagnostics = Editor::doc_diagnostics(
&self.editor.language_servers,
&self.editor.diagnostics,
doc,
);
doc.replace_diagnostics(diagnostics, &[], None);
}
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",
@@ -679,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};
@@ -748,7 +724,7 @@ impl Application {
}
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;
@@ -777,9 +753,7 @@ impl Application {
let lang_conf = doc.language.clone();
if let Some(lang_conf) = &lang_conf {
if let Some(old_diagnostics) =
self.editor.diagnostics.get(&params.uri)
{
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
@@ -812,7 +786,7 @@ impl Application {
// Insert the original lsp::Diagnostics here because we may have no open document
// for diagnosic message and so we can't calculate the exact position.
// When using them later in the diagnostics picker, we calculate them on-demand.
let diagnostics = match self.editor.diagnostics.entry(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
@@ -1011,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(),
@@ -1058,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 => {

File diff suppressed because it is too large Load Diff

View File

@@ -1,4 +1,4 @@
use futures_util::{stream::FuturesUnordered, FutureExt};
use futures_util::{stream::FuturesOrdered, FutureExt};
use helix_lsp::{
block_on,
lsp::{
@@ -6,7 +6,7 @@ use helix_lsp::{
NumberOrString,
},
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
Client, OffsetEncoding,
Client, LanguageServerId, OffsetEncoding,
};
use tokio_stream::StreamExt;
use tui::{
@@ -38,7 +38,7 @@ use std::{
collections::{BTreeMap, HashSet},
fmt::Write,
future::Future,
path::PathBuf,
path::{Path, PathBuf},
};
/// Gets the first language server that is attached to a document which supports a specific feature.
@@ -134,7 +134,7 @@ struct DiagnosticStyles {
}
struct PickerDiagnostic {
url: lsp::Url,
path: PathBuf,
diag: lsp::Diagnostic,
offset_encoding: OffsetEncoding,
}
@@ -167,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())
}
};
@@ -208,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));
@@ -258,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(),
});
@@ -292,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)
}
@@ -339,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| {
@@ -414,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| {
@@ -470,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)));
@@ -488,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 {
@@ -580,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"
@@ -726,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
@@ -764,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
@@ -787,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,
@@ -871,142 +811,7 @@ 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 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!(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 view_id = editor.get_synced_view_id(doc_id);
let doc = doc_mut!(editor, &doc_id);
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,
@@ -1019,9 +824,7 @@ fn goto_impl(
[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)
@@ -1063,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);
}
},
);
}
@@ -1123,7 +930,11 @@ 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);
}
},
);
}
@@ -1227,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(),
@@ -1256,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()),
}
@@ -1508,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;
@@ -7,10 +8,9 @@ use super::*;
use helix_core::fuzzy::fuzzy_match;
use helix_core::indent::MAX_INDENT;
use helix_core::{encoding, line_ending, shellwords::Shellwords};
use helix_lsp::{OffsetEncoding, Url};
use helix_view::document::DEFAULT_LANGUAGE_NAME;
use helix_view::editor::{Action, CloseError, ConfigEvent};
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};
@@ -111,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_stdx::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)));
},
));
@@ -310,7 +310,7 @@ fn buffer_next(
return Ok(());
}
goto_buffer(cx.editor, Direction::Forward);
goto_buffer(cx.editor, Direction::Forward, 1);
Ok(())
}
@@ -323,7 +323,7 @@ fn buffer_previous(
return Ok(());
}
goto_buffer(cx.editor, Direction::Backward);
goto_buffer(cx.editor, Direction::Backward, 1);
Ok(())
}
@@ -1079,11 +1079,11 @@ fn change_current_directory(
return Ok(());
}
let dir = helix_stdx::path::expand_tilde(
args.first()
.context("target directory not provided")?
.as_ref(),
);
let dir = args
.first()
.context("target directory not provided")?
.as_ref();
let dir = helix_stdx::path::expand_tilde(Path::new(dir));
helix_stdx::env::set_current_working_dir(dir)?;
@@ -1269,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
@@ -1319,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
@@ -1546,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:
@@ -2028,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(..);
@@ -2053,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(())
}
@@ -2068,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);
@@ -2094,8 +2088,6 @@ fn reflow(
});
doc.apply(&transaction, view.id);
doc.append_changes_to_history(view);
view.ensure_cursor_in_view(doc, scrolloff);
Ok(())
}
@@ -2261,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() {
@@ -2274,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)
@@ -2299,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 {
@@ -2337,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(())
}
@@ -2407,66 +2392,83 @@ fn move_buffer(
ensure!(args.len() == 1, format!(":move takes one argument"));
let doc = doc!(cx.editor);
let new_path =
helix_stdx::path::canonicalize(&PathBuf::from(args.first().unwrap().to_string()));
let old_path = doc
.path()
.ok_or_else(|| anyhow!("Scratch buffer cannot be moved. Use :write instead"))?
.context("Scratch buffer cannot be moved. Use :write instead")?
.clone();
let old_path_as_url = doc.url().unwrap();
let new_path_as_url = Url::from_file_path(&new_path).unwrap();
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(())
}
let edits: Vec<(
helix_lsp::Result<helix_lsp::lsp::WorkspaceEdit>,
OffsetEncoding,
String,
)> = doc
.language_servers()
.map(|lsp| {
(
lsp.prepare_file_rename(&old_path_as_url, &new_path_as_url),
lsp.offset_encoding(),
lsp.name().to_owned(),
)
})
.filter(|(f, _, _)| f.is_some())
.map(|(f, encoding, name)| (helix_lsp::block_on(f.unwrap()), encoding, name))
.collect();
for (lsp_reply, encoding, name) in edits {
match lsp_reply {
Ok(edit) => {
if let Err(e) = apply_workspace_edit(cx.editor, encoding, &edit) {
log::error!(
":move command failed to apply edits from lsp {}: {:?}",
name,
e
);
};
}
Err(e) => {
log::error!("LSP {} failed to treat willRename request: {:?}", name, e);
}
};
fn yank_diagnostic(
cx: &mut compositor::Context,
args: &[Cow<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
if event != PromptEvent::Validate {
return Ok(());
}
let doc = doc_mut!(cx.editor);
doc.set_path(Some(new_path.as_path()));
if let Err(e) = std::fs::rename(&old_path, &new_path) {
doc.set_path(Some(old_path.as_path()));
bail!("Could not move file: {}", e);
let reg = match args.first() {
Some(s) => {
ensure!(s.chars().count() == 1, format!("Invalid register {s}"));
s.chars().next().unwrap()
}
None => '+',
};
doc.language_servers().for_each(|lsp| {
lsp.did_file_rename(&old_path_as_url, &new_path_as_url);
});
let (view, doc) = current_ref!(cx.editor);
let primary = doc.selection(view.id).primary();
cx.editor
.language_servers
.file_event_handler
.file_changed(new_path);
// 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(())
}
@@ -3062,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",
@@ -3078,6 +3080,20 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
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>> =
@@ -3159,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]));

View File

@@ -9,10 +9,9 @@ use crate::handlers::completion::CompletionHandler;
use crate::handlers::signature_help::SignatureHelpHandler;
pub use completion::trigger_auto_completion;
pub use helix_view::handlers::lsp::SignatureHelpInvoked;
pub use helix_view::handlers::Handlers;
mod completion;
pub mod completion;
mod signature_help;
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {

View File

@@ -30,6 +30,8 @@ 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 {
@@ -221,9 +223,17 @@ fn request_completion(
.iter()
.find(|&trigger| trigger_text.ends_with(trigger))
});
lsp::CompletionContext {
trigger_kind: lsp::CompletionTriggerKind::TRIGGER_CHARACTER,
trigger_character: trigger_char.cloned(),
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,
}
}
};
@@ -243,7 +253,7 @@ fn request_completion(
.into_iter()
.map(|item| CompletionItem {
item,
language_server_id,
provider: language_server_id,
resolved: false,
})
.collect();

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

@@ -5,7 +5,7 @@ use helix_core::syntax::LanguageServerFeature;
use helix_event::{
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
};
use helix_lsp::lsp;
use helix_lsp::lsp::{self, SignatureInformation};
use helix_stdx::rope::RopeSliceExt;
use helix_view::document::Mode;
use helix_view::events::{DocumentDidChange, SelectionDidChange};
@@ -18,7 +18,7 @@ use crate::commands::Open;
use crate::compositor::Compositor;
use crate::events::{OnModeSwitch, PostInsertChar};
use crate::handlers::Handlers;
use crate::ui::lsp::SignatureHelp;
use crate::ui::lsp::{Signature, SignatureHelp};
use crate::ui::Popup;
use crate::{job, ui};
@@ -82,6 +82,7 @@ impl helix_event::AsyncHook for SignatureHelpHandler {
}
}
self.state = if open { State::Open } else { State::Closed };
return timeout;
}
}
@@ -138,6 +139,31 @@ pub fn request_signature_help(
});
}
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,
@@ -184,54 +210,50 @@ pub fn show_signature_help(
let doc = doc!(editor);
let language = doc.language_name().unwrap_or("");
let signature = match response
if response.signatures.is_empty() {
return;
}
let signatures: Vec<Signature> = 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),
);
.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 {
signature.documentation.as_ref().map(|doc| match doc {
lsp::Documentation::String(s) => s.clone(),
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
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,
}
})
} 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());
.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)

View File

@@ -2,7 +2,7 @@ use crossterm::{
style::{Color, Print, Stylize},
tty::IsTty,
};
use helix_core::config::{default_syntax_loader, user_syntax_loader};
use helix_core::config::{default_lang_config, user_lang_config};
use helix_loader::grammar::load_runtime_file;
use helix_view::clipboard::get_clipboard_provider;
use std::io::Write;
@@ -128,7 +128,7 @@ pub fn languages_all() -> std::io::Result<()> {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let mut syn_loader_conf = match user_syntax_loader() {
let mut syn_loader_conf = match user_lang_config() {
Ok(conf) => conf,
Err(err) => {
let stderr = std::io::stderr();
@@ -141,7 +141,7 @@ pub fn languages_all() -> std::io::Result<()> {
err
)?;
writeln!(stderr, "{}", "Using default language config".yellow())?;
default_syntax_loader()
default_lang_config()
}
};
@@ -234,7 +234,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
let stdout = std::io::stdout();
let mut stdout = stdout.lock();
let syn_loader_conf = match user_syntax_loader() {
let syn_loader_conf = match user_lang_config() {
Ok(conf) => conf,
Err(err) => {
let stderr = std::io::stderr();
@@ -247,7 +247,7 @@ pub fn language(lang_str: String) -> std::io::Result<()> {
err
)?;
writeln!(stderr, "{}", "Using default language config".yellow())?;
default_syntax_loader()
default_lang_config()
}
};

View File

@@ -303,6 +303,15 @@ impl Keymaps {
self.sticky.as_ref()
}
pub fn contains_key(&self, mode: Mode, key: KeyEvent) -> bool {
let keymaps = &*self.map();
let keymap = &keymaps[&mode];
keymap
.search(self.pending())
.and_then(KeyTrie::node)
.is_some_and(|node| node.contains_key(&key))
}
/// Lookup `key` in the keymap to try and find a command to execute. Escape
/// key cancels pending keystrokes. If there are no pending keystrokes but a
/// sticky node is in use, it will be cleared.

View File

@@ -58,6 +58,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"k" => move_line_up,
"j" => move_line_down,
"." => goto_last_modification,
"w" => goto_word,
},
":" => command_mode,
@@ -86,10 +87,12 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"A-;" => flip_selections,
"A-o" | "A-up" => expand_selection,
"A-i" | "A-down" => shrink_selection,
"A-I" | "A-S-down" => select_all_children,
"A-p" | "A-left" => select_prev_sibling,
"A-n" | "A-right" => select_next_sibling,
"A-e" => move_parent_node_end,
"A-b" => move_parent_node_start,
"A-a" => select_all_siblings,
"%" => select_all,
"x" => extend_line_below,
@@ -113,6 +116,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"t" => goto_prev_class,
"a" => goto_prev_parameter,
"c" => goto_prev_comment,
"e" => goto_prev_entry,
"T" => goto_prev_test,
"p" => goto_prev_paragraph,
"space" => add_newline_above,
@@ -126,6 +130,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"t" => goto_next_class,
"a" => goto_next_parameter,
"c" => goto_next_comment,
"e" => goto_next_entry,
"T" => goto_next_test,
"p" => goto_next_paragraph,
"space" => add_newline_below,
@@ -178,8 +183,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"esc" => normal_mode,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
"C-u" => half_page_up,
"C-d" => half_page_down,
"C-u" => page_cursor_half_up,
"C-d" => page_cursor_half_down,
"C-w" => { "Window"
"C-w" | "w" => rotate_view,
@@ -222,9 +227,10 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"S" => workspace_symbol_picker,
"d" => diagnostics_picker,
"D" => workspace_diagnostics_picker,
"g" => changed_file_picker,
"a" => code_action,
"'" => last_picker,
"g" => { "Debug (experimental)" sticky=true
"G" => { "Debug (experimental)" sticky=true
"l" => dap_launch,
"r" => dap_restart,
"b" => dap_toggle_breakpoint,
@@ -276,6 +282,9 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"k" => hover,
"r" => rename_symbol,
"h" => select_references_to_symbol_under_cursor,
"c" => toggle_comments,
"C" => toggle_block_comments,
"A-c" => toggle_line_comments,
"?" => command_palette,
},
"z" => { "View"
@@ -287,8 +296,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"j" | "down" => scroll_down,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
"C-u" | "backspace" => half_page_up,
"C-d" | "space" => half_page_down,
"C-u" | "backspace" => page_cursor_half_up,
"C-d" | "space" => page_cursor_half_down,
"/" => search,
"?" => rsearch,
@@ -304,8 +313,8 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"j" | "down" => scroll_down,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
"C-u" | "backspace" => half_page_up,
"C-d" | "space" => half_page_down,
"C-u" | "backspace" => page_cursor_half_up,
"C-d" | "space" => page_cursor_half_down,
"/" => search,
"?" => rsearch,
@@ -357,6 +366,7 @@ pub fn default() -> HashMap<Mode, KeyTrie> {
"g" => { "Goto"
"k" => extend_line_up,
"j" => extend_line_down,
"w" => extend_to_word,
},
}));
let insert = keymap!({ "Insert mode"

View File

@@ -20,25 +20,39 @@ mod handlers;
use ignore::DirEntry;
use url::Url;
pub use keymap::macros::*;
#[cfg(not(windows))]
fn true_color() -> bool {
std::env::var("COLORTERM")
.map(|v| matches!(v.as_str(), "truecolor" | "24bit"))
.unwrap_or(false)
}
#[cfg(windows)]
fn true_color() -> bool {
true
}
#[cfg(not(windows))]
fn true_color() -> bool {
if matches!(
std::env::var("COLORTERM").map(|v| matches!(v.as_str(), "truecolor" | "24bit")),
Ok(true)
) {
return true;
}
match termini::TermInfo::from_env() {
Ok(t) => {
t.extended_cap("RGB").is_some()
|| t.extended_cap("Tc").is_some()
|| (t.extended_cap("setrgbf").is_some() && t.extended_cap("setrgbb").is_some())
}
Err(_) => false,
}
}
/// Function used for filtering dir entries in the various file pickers.
fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> bool {
// We always want to ignore the .git directory, otherwise if
// We always want to ignore popular VCS directories, otherwise if
// `ignore` is turned off, we end up with a lot of noise
// in our picker.
if entry.file_name() == ".git" {
if matches!(
entry.file_name().to_str(),
Some(".git" | ".pijul" | ".jj" | ".hg" | ".svn")
) {
return false;
}

View File

@@ -145,18 +145,18 @@ FLAGS:
}
};
let syn_loader_conf = helix_core::config::user_syntax_loader().unwrap_or_else(|err| {
eprintln!("Bad language config: {}", err);
let lang_loader = helix_core::config::user_lang_loader().unwrap_or_else(|err| {
eprintln!("{}", err);
eprintln!("Press <ENTER> to continue with default language config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
helix_core::config::default_syntax_loader()
helix_core::config::default_lang_loader()
});
// TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args, config, syn_loader_conf)
.context("unable to create new application")?;
let mut app =
Application::new(args, config, lang_loader).context("unable to create new application")?;
let exit_code = app.run(&mut EventStream::new()).await?;

View File

@@ -1,6 +1,6 @@
use crate::{
compositor::{Component, Context, Event, EventResult},
handlers::trigger_auto_completion,
handlers::{completion::ResolveHandler, trigger_auto_completion},
};
use helix_view::{
document::SavePoint,
@@ -17,10 +17,9 @@ use std::{borrow::Cow, sync::Arc};
use helix_core::{chars, Change, Transaction};
use helix_view::{graphics::Rect, Document, Editor};
use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::{lsp, util, OffsetEncoding};
use helix_lsp::{lsp, util, LanguageServerId, OffsetEncoding};
impl menu::Item for CompletionItem {
type Data = ();
@@ -92,7 +91,7 @@ impl menu::Item for CompletionItem {
#[derive(Debug, PartialEq, Default, Clone)]
pub struct CompletionItem {
pub item: lsp::CompletionItem,
pub language_server_id: usize,
pub provider: LanguageServerId,
pub resolved: bool,
}
@@ -102,6 +101,7 @@ pub struct Completion {
#[allow(dead_code)]
trigger_offset: usize,
filter: String,
resolve_handler: ResolveHandler,
}
impl Completion {
@@ -221,7 +221,7 @@ impl Completion {
($item:expr) => {
match editor
.language_servers
.get_by_id($item.language_server_id)
.get_by_id($item.provider)
{
Some(ls) => ls,
None => {
@@ -282,12 +282,6 @@ impl Completion {
let language_server = language_server!(item);
let offset_encoding = language_server.offset_encoding();
let language_server = editor
.language_servers
.get_by_id(item.language_server_id)
.unwrap();
// resolve item if not yet resolved
if !item.resolved {
if let Some(resolved) =
Self::resolve_completion_item(language_server, item.item.clone())
@@ -368,6 +362,7 @@ impl Completion {
// TODO: expand nucleo api to allow moving straight to a Utf32String here
// and avoid allocation during matching
filter: String::from(fragment),
resolve_handler: ResolveHandler::new(),
};
// need to recompute immediately in case start_offset != trigger_offset
@@ -379,14 +374,25 @@ impl Completion {
completion
}
/// Synchronously resolve the given completion item. This is used when
/// accepting a completion.
fn resolve_completion_item(
language_server: &helix_lsp::Client,
completion_item: lsp::CompletionItem,
) -> Option<lsp::CompletionItem> {
let future = language_server.resolve_completion_item(completion_item)?;
if !matches!(
language_server.capabilities().completion_provider,
Some(lsp::CompletionOptions {
resolve_provider: Some(true),
..
})
) {
return None;
}
let future = language_server.resolve_completion_item(&completion_item);
let response = helix_lsp::block_on(future);
match response {
Ok(value) => serde_json::from_value(value).ok(),
Ok(item) => Some(item),
Err(err) => {
log::error!("Failed to resolve completion item: {}", err);
None
@@ -416,66 +422,10 @@ impl Completion {
self.popup.contents().is_empty()
}
fn replace_item(&mut self, old_item: CompletionItem, new_item: CompletionItem) {
pub fn replace_item(&mut self, old_item: &CompletionItem, new_item: CompletionItem) {
self.popup.contents_mut().replace_option(old_item, new_item);
}
/// Asynchronously requests that the currently selection completion item is
/// resolved through LSP `completionItem/resolve`.
pub fn ensure_item_resolved(&mut self, cx: &mut commands::Context) -> bool {
// > 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.
// https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion
let current_item = match self.popup.contents().selection() {
Some(item) if !item.resolved => item.clone(),
_ => return false,
};
let Some(language_server) = cx
.editor
.language_server_by_id(current_item.language_server_id)
else {
return false;
};
// This method should not block the compositor so we handle the response asynchronously.
let Some(future) = language_server.resolve_completion_item(current_item.item.clone())
else {
return false;
};
cx.callback(
future,
move |_editor, compositor, response: Option<lsp::CompletionItem>| {
let resolved_item = match response {
Some(item) => item,
None => return,
};
if let Some(completion) = &mut compositor
.find::<crate::ui::EditorView>()
.unwrap()
.completion
{
let resolved_item = CompletionItem {
item: resolved_item,
language_server_id: current_item.language_server_id,
resolved: true,
};
completion.replace_item(current_item, resolved_item);
}
},
);
true
}
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
self.popup.area(viewport, editor)
}
@@ -494,10 +444,13 @@ impl Component for Completion {
self.popup.render(area, surface, cx);
// if we have a selection, render a markdown popup on top/below with info
let option = match self.popup.contents().selection() {
let option = match self.popup.contents_mut().selection_mut() {
Some(option) => option,
None => return,
};
if !option.resolved {
self.resolve_handler.ensure_item_resolved(cx.editor, option);
}
// need to render:
// option.detail
// ---
@@ -545,12 +498,7 @@ impl Component for Completion {
None => return,
};
let popup_area = {
let (popup_x, popup_y) = self.popup.get_rel_position(area, cx.editor);
let (popup_width, popup_height) = self.popup.get_size();
Rect::new(popup_x, popup_y, popup_width, popup_height)
};
let popup_area = self.popup.area(area, cx.editor);
let doc_width_available = area.width.saturating_sub(popup_area.right());
let doc_area = if doc_width_available > 30 {
let mut doc_width = doc_width_available;

View File

@@ -7,6 +7,7 @@ use helix_core::syntax::Highlight;
use helix_core::syntax::HighlightEvent;
use helix_core::text_annotations::TextAnnotations;
use helix_core::{visual_offset_from_block, Position, RopeSlice};
use helix_stdx::rope::RopeSliceExt;
use helix_view::editor::{WhitespaceConfig, WhitespaceRenderValue};
use helix_view::graphics::Rect;
use helix_view::theme::Style;
@@ -32,14 +33,27 @@ impl<F: FnMut(&mut TextRenderer, LinePos)> LineDecoration for F {
}
}
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
enum StyleIterKind {
/// base highlights (usually emitted by TS), byte indices (potentially not codepoint aligned)
BaseHighlights,
/// overlay highlights (emitted by custom code from selections), char indices
Overlay,
}
/// A wrapper around a HighlightIterator
/// that merges the layered highlights to create the final text style
/// and yields the active text style and the char_idx where the active
/// style will have to be recomputed.
///
/// TODO(ropey2): hopefully one day helix and ropey will operate entirely
/// on byte ranges and we can remove this
struct StyleIter<'a, H: Iterator<Item = HighlightEvent>> {
text_style: Style,
active_highlights: Vec<Highlight>,
highlight_iter: H,
kind: StyleIterKind,
text: RopeSlice<'a>,
theme: &'a Theme,
}
@@ -54,7 +68,7 @@ impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> {
HighlightEvent::HighlightEnd => {
self.active_highlights.pop();
}
HighlightEvent::Source { start, end } => {
HighlightEvent::Source { start, mut end } => {
if start == end {
continue;
}
@@ -64,6 +78,9 @@ impl<H: Iterator<Item = HighlightEvent>> Iterator for StyleIter<'_, H> {
.fold(self.text_style, |acc, span| {
acc.patch(self.theme.highlight(span.0))
});
if self.kind == StyleIterKind::BaseHighlights {
end = self.text.byte_to_next_char(end);
}
return Some((style, end));
}
}
@@ -185,13 +202,17 @@ pub fn render_text<'t>(
text_style: renderer.text_style,
active_highlights: Vec::with_capacity(64),
highlight_iter: syntax_highlight_iter,
kind: StyleIterKind::BaseHighlights,
theme,
text,
};
let mut overlay_styles = StyleIter {
text_style: Style::default(),
active_highlights: Vec::with_capacity(64),
highlight_iter: overlay_highlight_iter,
kind: StyleIterKind::Overlay,
theme,
text,
};
let mut last_line_pos = LinePos {
@@ -341,6 +362,7 @@ pub struct TextRenderer<'a> {
pub indent_guide_style: Style,
pub newline: String,
pub nbsp: String,
pub nnbsp: String,
pub space: String,
pub tab: String,
pub virtual_tab: String,
@@ -395,6 +417,11 @@ impl<'a> TextRenderer<'a> {
} else {
" ".to_owned()
};
let nnbsp = if ws_render.nnbsp() == WhitespaceRenderValue::All {
ws_chars.nnbsp.into()
} else {
" ".to_owned()
};
let text_style = theme.get("ui.text");
@@ -405,6 +432,7 @@ impl<'a> TextRenderer<'a> {
indent_guide_char: editor_config.indent_guides.character.into(),
newline,
nbsp,
nnbsp,
space,
tab,
virtual_tab,
@@ -448,6 +476,7 @@ impl<'a> TextRenderer<'a> {
let width = grapheme.width();
let space = if is_virtual { " " } else { &self.space };
let nbsp = if is_virtual { " " } else { &self.nbsp };
let nnbsp = if is_virtual { " " } else { &self.nnbsp };
let tab = if is_virtual {
&self.virtual_tab
} else {
@@ -461,6 +490,7 @@ impl<'a> TextRenderer<'a> {
// TODO special rendering for other whitespaces?
Grapheme::Other { ref g } if g == " " => space,
Grapheme::Other { ref g } if g == "\u{00A0}" => nbsp,
Grapheme::Other { ref g } if g == "\u{202F}" => nnbsp,
Grapheme::Other { ref g } => g,
Grapheme::Newline => &self.newline,
};

View File

@@ -1,7 +1,6 @@
use crate::{
commands::{self, OnKeyCallback},
compositor::{Component, Context, Event, EventResult},
events::{OnModeSwitch, PostCommand},
key,
keymap::{KeymapResult, Keymaps},
ui::{
@@ -12,9 +11,7 @@ use crate::{
use helix_core::{
diagnostic::NumberOrString,
graphemes::{
ensure_grapheme_boundary_next_byte, next_grapheme_boundary, prev_grapheme_boundary,
},
graphemes::{next_grapheme_boundary, prev_grapheme_boundary},
movement::Direction,
syntax::{self, HighlightEvent},
text_annotations::TextAnnotations,
@@ -315,26 +312,14 @@ impl EditorView {
let iter = syntax
// TODO: range doesn't actually restrict source, just highlight range
.highlight_iter(text.slice(..), Some(range), None)
.map(|event| event.unwrap())
.map(move |event| match event {
// TODO: use byte slices directly
// convert byte offsets to char offset
HighlightEvent::Source { start, end } => {
let start =
text.byte_to_char(ensure_grapheme_boundary_next_byte(text, start));
let end =
text.byte_to_char(ensure_grapheme_boundary_next_byte(text, end));
HighlightEvent::Source { start, end }
}
event => event,
});
.map(|event| event.unwrap());
Box::new(iter)
}
None => Box::new(
[HighlightEvent::Source {
start: text.byte_to_char(range.start),
end: text.byte_to_char(range.end),
start: range.start,
end: range.end,
}]
.into_iter(),
),
@@ -350,7 +335,8 @@ impl EditorView {
let text = doc.text().slice(..);
let row = text.char_to_line(anchor.min(text.len_chars()));
let range = Self::viewport_byte_range(text, row, height);
let mut range = Self::viewport_byte_range(text, row, height);
range = text.byte_to_char(range.start)..text.byte_to_char(range.end);
text_annotations.collect_overlay_highlights(range)
}
@@ -359,8 +345,8 @@ impl EditorView {
pub fn doc_diagnostics_highlights(
doc: &Document,
theme: &Theme,
) -> [Vec<(usize, std::ops::Range<usize>)>; 5] {
use helix_core::diagnostic::Severity;
) -> [Vec<(usize, std::ops::Range<usize>)>; 7] {
use helix_core::diagnostic::{DiagnosticTag, Range, Severity};
let get_scope_of = |scope| {
theme
.find_scope_index_exact(scope)
@@ -380,11 +366,34 @@ impl EditorView {
let error = get_scope_of("diagnostic.error");
let r#default = get_scope_of("diagnostic"); // this is a bit redundant but should be fine
// Diagnostic tags
let unnecessary = theme.find_scope_index_exact("diagnostic.unnecessary");
let deprecated = theme.find_scope_index_exact("diagnostic.deprecated");
let mut default_vec: Vec<(usize, std::ops::Range<usize>)> = Vec::new();
let mut info_vec = Vec::new();
let mut hint_vec = Vec::new();
let mut warning_vec = Vec::new();
let mut error_vec = Vec::new();
let mut unnecessary_vec = Vec::new();
let mut deprecated_vec = Vec::new();
let push_diagnostic =
|vec: &mut Vec<(usize, std::ops::Range<usize>)>, scope, range: Range| {
// If any diagnostic overlaps ranges with the prior diagnostic,
// merge the two together. Otherwise push a new span.
match vec.last_mut() {
Some((_, existing_range)) if range.start <= existing_range.end => {
// This branch merges overlapping diagnostics, assuming that the current
// diagnostic starts on range.start or later. If this assertion fails,
// we will discard some part of `diagnostic`. This implies that
// `doc.diagnostics()` is not sorted by `diagnostic.range`.
debug_assert!(existing_range.start <= range.start);
existing_range.end = range.end.max(existing_range.end)
}
_ => vec.push((scope, range.start..range.end)),
}
};
for diagnostic in doc.diagnostics() {
// Separate diagnostics into different Vecs by severity.
@@ -396,22 +405,44 @@ impl EditorView {
_ => (&mut default_vec, r#default),
};
// If any diagnostic overlaps ranges with the prior diagnostic,
// merge the two together. Otherwise push a new span.
match vec.last_mut() {
Some((_, range)) if diagnostic.range.start <= range.end => {
// This branch merges overlapping diagnostics, assuming that the current
// diagnostic starts on range.start or later. If this assertion fails,
// we will discard some part of `diagnostic`. This implies that
// `doc.diagnostics()` is not sorted by `diagnostic.range`.
debug_assert!(range.start <= diagnostic.range.start);
range.end = diagnostic.range.end.max(range.end)
// If the diagnostic has tags and a non-warning/error severity, skip rendering
// the diagnostic as info/hint/default and only render it as unnecessary/deprecated
// instead. For warning/error diagnostics, render both the severity highlight and
// the tag highlight.
if diagnostic.tags.is_empty()
|| matches!(
diagnostic.severity,
Some(Severity::Warning | Severity::Error)
)
{
push_diagnostic(vec, scope, diagnostic.range);
}
for tag in &diagnostic.tags {
match tag {
DiagnosticTag::Unnecessary => {
if let Some(scope) = unnecessary {
push_diagnostic(&mut unnecessary_vec, scope, diagnostic.range)
}
}
DiagnosticTag::Deprecated => {
if let Some(scope) = deprecated {
push_diagnostic(&mut deprecated_vec, scope, diagnostic.range)
}
}
}
_ => vec.push((scope, diagnostic.range.start..diagnostic.range.end)),
}
}
[default_vec, info_vec, hint_vec, warning_vec, error_vec]
[
default_vec,
unnecessary_vec,
deprecated_vec,
info_vec,
hint_vec,
warning_vec,
error_vec,
]
}
/// Get highlight spans for selections in a document view.
@@ -716,7 +747,8 @@ impl EditorView {
}
}
let paragraph = Paragraph::new(lines)
let text = Text::from(lines);
let paragraph = Paragraph::new(&text)
.alignment(Alignment::Right)
.wrap(Wrap { trim: true });
let width = 100.min(viewport.width);
@@ -834,17 +866,10 @@ impl EditorView {
cxt.editor.autoinfo = self.keymaps.sticky().map(|node| node.infobox());
let mut execute_command = |command: &commands::MappableCommand| {
command.execute(cxt);
helix_event::dispatch(PostCommand { command, cx: cxt });
cxt.execute_command(command);
let current_mode = cxt.editor.mode();
if current_mode != last_mode {
helix_event::dispatch(OnModeSwitch {
old_mode: last_mode,
new_mode: current_mode,
cx: cxt,
});
// HAXX: if we just entered insert mode from normal, clear key buf
// and record the command that got us into this mode.
if current_mode == Mode::Insert {
@@ -854,7 +879,6 @@ impl EditorView {
self.last_insert.1.clear();
}
}
last_mode = current_mode;
};
@@ -878,18 +902,18 @@ impl EditorView {
match keyresult {
KeymapResult::NotFound => {
if let Some(ch) = event.char() {
commands::insert::insert_char(cx, ch)
cx.execute(|cx| commands::insert::insert_char(cx, ch))
}
}
KeymapResult::Cancelled(pending) => {
for ev in pending {
match ev.char() {
Some(ch) => commands::insert::insert_char(cx, ch),
Some(ch) => cx.execute(|cx| commands::insert::insert_char(cx, ch)),
None => {
if let KeymapResult::Matched(command) =
self.keymaps.get(Mode::Insert, ev)
{
command.execute(cx);
cx.execute_command(&command);
}
}
}
@@ -902,17 +926,21 @@ impl EditorView {
fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEvent) {
match (event, cxt.editor.count) {
// count handling
(key!(i @ '0'), Some(_)) | (key!(i @ '1'..='9'), _) => {
// If the count is already started and the input is a number, always continue the count.
(key!(i @ '0'..='9'), Some(count)) => {
let i = i.to_digit(10).unwrap() as usize;
cxt.editor.count =
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
cxt.editor.count = NonZeroUsize::new(count.get() * 10 + i);
}
// A non-zero digit will start the count if that number isn't used by a keymap.
(key!(i @ '1'..='9'), None) if !self.keymaps.contains_key(mode, event) => {
let i = i.to_digit(10).unwrap() as usize;
cxt.editor.count = NonZeroUsize::new(i);
}
// special handling for repeat operator
(key!('.'), _) if self.keymaps.pending().is_empty() => {
for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) {
// first execute whatever put us into insert mode
self.last_insert.0.execute(cxt);
cxt.execute_command(&self.last_insert.0);
let mut last_savepoint = None;
let mut last_request_savepoint = None;
// then replay the inputs
@@ -997,7 +1025,6 @@ impl EditorView {
self.last_insert.1.push(InsertEvent::TriggerCompletion);
// TODO : propagate required size on resize to completion too
completion.required_size((size.width, size.height));
self.completion = Some(completion);
Some(area)
}
@@ -1025,26 +1052,38 @@ impl EditorView {
pub fn handle_idle_timeout(&mut self, cx: &mut commands::Context) -> EventResult {
commands::compute_inlay_hints_for_all_views(cx.editor, cx.jobs);
if let Some(completion) = &mut self.completion {
return if completion.ensure_item_resolved(cx) {
EventResult::Consumed(None)
} else {
EventResult::Ignored(None)
};
}
EventResult::Ignored(None)
}
}
impl EditorView {
/// must be called whenever the editor processed input that
/// is not a `KeyEvent`. In these cases any pending keys/on next
/// key callbacks must be canceled.
fn handle_non_key_input(&mut self, cxt: &mut commands::Context) {
cxt.editor.status_msg = None;
cxt.editor.reset_idle_timer();
// HACKS: create a fake key event that will never trigger any actual map
// and therefore simply acts as "dismiss"
let null_key_event = KeyEvent {
code: KeyCode::Null,
modifiers: KeyModifiers::empty(),
};
// dismiss any pending keys
if let Some(on_next_key) = self.on_next_key.take() {
on_next_key(cxt, null_key_event);
}
self.handle_keymap_event(cxt.editor.mode, cxt, null_key_event);
self.pseudo_pending.clear();
}
fn handle_mouse_event(
&mut self,
event: &MouseEvent,
cxt: &mut commands::Context,
) -> EventResult {
if event.kind != MouseEventKind::Moved {
cxt.editor.reset_idle_timer();
self.handle_non_key_input(cxt)
}
let config = cxt.editor.config();
@@ -1086,6 +1125,15 @@ impl EditorView {
if modifiers == KeyModifiers::ALT {
let selection = doc.selection(view_id).clone();
doc.set_selection(view_id, selection.push(Range::point(pos)));
} else if editor.mode == Mode::Select {
// Discards non-primary selections for consistent UX with normal mode
let primary = doc.selection(view_id).primary().put_cursor(
doc.text().slice(..),
pos,
true,
);
editor.mouse_down_range = Some(primary);
doc.set_selection(view_id, Selection::single(primary.anchor, primary.head));
} else {
doc.set_selection(view_id, Selection::point(pos));
}
@@ -1154,7 +1202,7 @@ impl EditorView {
}
let offset = config.scroll_lines.unsigned_abs();
commands::scroll(cxt, offset, direction);
commands::scroll(cxt, offset, direction, false);
cxt.editor.tree.focus = current_view;
cxt.editor.ensure_cursor_in_view(current_view);
@@ -1169,40 +1217,54 @@ impl EditorView {
let (view, doc) = current!(cxt.editor);
if doc
.selection(view.id)
.primary()
.slice(doc.text().slice(..))
.len_chars()
<= 1
{
return EventResult::Ignored(None);
let should_yank = match cxt.editor.mouse_down_range.take() {
Some(down_range) => doc.selection(view.id).primary() != down_range,
None => {
// This should not happen under normal cases. We fall back to the original
// behavior of yanking on non-single-char selections.
doc.selection(view.id)
.primary()
.slice(doc.text().slice(..))
.len_chars()
> 1
}
};
if should_yank {
cxt.execute_command(
&commands::MappableCommand::yank_main_selection_to_primary_clipboard,
);
EventResult::Consumed(None)
} else {
EventResult::Ignored(None)
}
commands::MappableCommand::yank_main_selection_to_primary_clipboard.execute(cxt);
EventResult::Consumed(None)
}
MouseEventKind::Up(MouseButton::Right) => {
if let Some((coords, view_id)) = gutter_coords_and_view(cxt.editor, row, column) {
if let Some((pos, view_id)) = gutter_coords_and_view(cxt.editor, row, column) {
cxt.editor.focus(view_id);
let (view, doc) = current!(cxt.editor);
if let Some(pos) =
view.pos_at_visual_coords(doc, coords.row as u16, coords.col as u16, true)
{
doc.set_selection(view_id, Selection::point(pos));
if modifiers == KeyModifiers::ALT {
commands::MappableCommand::dap_edit_log.execute(cxt);
} else {
commands::MappableCommand::dap_edit_condition.execute(cxt);
if let Some((pos, _)) = pos_and_view(cxt.editor, row, column, true) {
doc_mut!(cxt.editor).set_selection(view_id, Selection::point(pos));
} else {
let (view, doc) = current!(cxt.editor);
if let Some(pos) = view.pos_at_visual_coords(doc, pos.row as u16, 0, true) {
doc.set_selection(view_id, Selection::point(pos));
match modifiers {
KeyModifiers::ALT => {
cxt.execute_command(&commands::MappableCommand::dap_edit_log)
}
_ => cxt.execute_command(
&commands::MappableCommand::dap_edit_condition,
),
};
}
return EventResult::Consumed(None);
}
}
cxt.editor.ensure_cursor_in_view(view_id);
return EventResult::Consumed(None);
}
EventResult::Ignored(None)
}
@@ -1213,8 +1275,9 @@ impl EditorView {
}
if modifiers == KeyModifiers::ALT {
commands::MappableCommand::replace_selections_with_primary_clipboard
.execute(cxt);
cxt.execute_command(
&commands::MappableCommand::replace_selections_with_primary_clipboard,
);
return EventResult::Consumed(None);
}
@@ -1223,7 +1286,7 @@ impl EditorView {
let doc = doc_mut!(editor, &view!(editor, view_id).doc);
doc.set_selection(view_id, Selection::point(pos));
cxt.editor.focus(view_id);
commands::MappableCommand::paste_primary_clipboard_before.execute(cxt);
cxt.execute_command(&commands::MappableCommand::paste_primary_clipboard_before);
return EventResult::Consumed(None);
}
@@ -1253,21 +1316,11 @@ impl Component for EditorView {
match event {
Event::Paste(contents) => {
self.handle_non_key_input(&mut cx);
cx.count = cx.editor.count;
commands::paste_bracketed_value(&mut cx, contents.clone());
cx.execute(|cx| commands::paste_bracketed_value(cx, contents.clone()));
cx.editor.count = None;
let config = cx.editor.config();
let mode = cx.editor.mode();
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);
// Store a history state if not in insert mode. Otherwise wait till we exit insert
// to include any edits to the paste in the history state.
if mode != Mode::Insert {
doc.append_changes_to_history(view);
}
EventResult::Consumed(None)
}
Event::Resize(_width, _height) => {
@@ -1286,7 +1339,7 @@ impl Component for EditorView {
if let Some(on_next_key) = self.on_next_key.take() {
// if there's a command waiting input, do that first
on_next_key(&mut cx, key);
cx.execute(|cx| on_next_key(cx, key));
} else {
match mode {
Mode::Insert => {
@@ -1350,17 +1403,6 @@ impl Component for EditorView {
return EventResult::Ignored(None);
}
let config = cx.editor.config();
let mode = cx.editor.mode();
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);
// Store a history state if not in insert mode. This also takes care of
// committing changes when leaving insert mode.
if mode != Mode::Insert {
doc.append_changes_to_history(view);
}
let callback = if callbacks.is_empty() {
None
} else {

View File

@@ -2,6 +2,7 @@ use crate::compositor::{Component, Context};
use helix_view::graphics::{Margin, Rect};
use helix_view::info::Info;
use tui::buffer::Buffer as Surface;
use tui::text::Text;
use tui::widgets::{Block, Borders, Paragraph, Widget};
impl Component for Info {
@@ -31,7 +32,7 @@ impl Component for Info {
let inner = block.inner(area).inner(&margin);
block.render(area, surface);
Paragraph::new(self.text.as_str())
Paragraph::new(&Text::from(self.text.as_str()))
.style(text_style)
.render(inner, surface);
}

View File

@@ -1,57 +1,97 @@
use std::sync::Arc;
use arc_swap::ArcSwap;
use helix_core::syntax;
use helix_view::graphics::{Margin, Rect, Style};
use helix_view::input::Event;
use tui::buffer::Buffer;
use tui::layout::Alignment;
use tui::text::Text;
use tui::widgets::{BorderType, Paragraph, Widget, Wrap};
use crate::compositor::{Component, Compositor, Context};
use crate::compositor::{Component, Compositor, Context, EventResult};
use crate::alt;
use crate::ui::Markdown;
use super::Popup;
pub struct SignatureHelp {
signature: String,
signature_doc: Option<String>,
pub struct Signature {
pub signature: String,
pub signature_doc: Option<String>,
/// Part of signature text
active_param_range: Option<(usize, usize)>,
pub active_param_range: Option<(usize, usize)>,
}
pub struct SignatureHelp {
language: String,
config_loader: Arc<syntax::Loader>,
config_loader: Arc<ArcSwap<syntax::Loader>>,
active_signature: usize,
signatures: Vec<Signature>,
}
impl SignatureHelp {
pub const ID: &'static str = "signature-help";
pub fn new(signature: String, language: String, config_loader: Arc<syntax::Loader>) -> Self {
pub fn new(
language: String,
config_loader: Arc<ArcSwap<syntax::Loader>>,
active_signature: usize,
signatures: Vec<Signature>,
) -> Self {
Self {
signature,
signature_doc: None,
active_param_range: None,
language,
config_loader,
active_signature,
signatures,
}
}
pub fn set_signature_doc(&mut self, signature_doc: Option<String>) {
self.signature_doc = signature_doc;
}
pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) {
self.active_param_range = offset;
pub fn active_signature(&self) -> usize {
self.active_signature
}
pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> {
compositor.find_id::<Popup<Self>>(Self::ID)
}
fn signature_index(&self) -> String {
format!("({}/{})", self.active_signature + 1, self.signatures.len())
}
}
impl Component for SignatureHelp {
fn handle_event(&mut self, event: &Event, _cx: &mut Context) -> EventResult {
let Event::Key(event) = event else {
return EventResult::Ignored(None);
};
if self.signatures.len() <= 1 {
return EventResult::Ignored(None);
}
match event {
alt!('p') => {
self.active_signature = self
.active_signature
.checked_sub(1)
.unwrap_or(self.signatures.len() - 1);
EventResult::Consumed(None)
}
alt!('n') => {
self.active_signature = (self.active_signature + 1) % self.signatures.len();
EventResult::Consumed(None)
}
_ => EventResult::Ignored(None),
}
}
fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
let margin = Margin::horizontal(1);
let active_param_span = self.active_param_range.map(|(start, end)| {
let signature = &self.signatures[self.active_signature];
let active_param_span = signature.active_param_range.map(|(start, end)| {
vec![(
cx.editor
.theme
@@ -61,21 +101,29 @@ impl Component for SignatureHelp {
)]
});
let sig = &self.signatures[self.active_signature];
let sig_text = crate::ui::markdown::highlighted_code_block(
&self.signature,
sig.signature.as_str(),
&self.language,
Some(&cx.editor.theme),
Arc::clone(&self.config_loader),
active_param_span,
);
if self.signatures.len() > 1 {
let signature_index = self.signature_index();
let text = Text::from(signature_index);
let paragraph = Paragraph::new(&text).alignment(Alignment::Right);
paragraph.render(area.clip_top(1).with_height(1).clip_right(1), surface);
}
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area);
let sig_text_para = Paragraph::new(sig_text).wrap(Wrap { trim: false });
let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false });
sig_text_para.render(sig_text_area, surface);
if self.signature_doc.is_none() {
if sig.signature_doc.is_none() {
return;
}
@@ -87,7 +135,7 @@ impl Component for SignatureHelp {
}
}
let sig_doc = match &self.signature_doc {
let sig_doc = match &sig.signature_doc {
None => return,
Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
};
@@ -95,7 +143,7 @@ impl Component for SignatureHelp {
let sig_doc_area = area
.clip_top(sig_text_area.height + 2)
.clip_bottom(u16::from(cx.editor.popup_border()));
let sig_doc_para = Paragraph::new(sig_doc)
let sig_doc_para = Paragraph::new(&sig_doc)
.wrap(Wrap { trim: false })
.scroll((cx.scroll.unwrap_or_default() as u16, 0));
sig_doc_para.render(sig_doc_area.inner(&margin), surface);
@@ -105,13 +153,15 @@ impl Component for SignatureHelp {
const PADDING: u16 = 2;
const SEPARATOR_HEIGHT: u16 = 1;
let sig = &self.signatures[self.active_signature];
if PADDING >= viewport.1 || PADDING >= viewport.0 {
return None;
}
let max_text_width = (viewport.0 - PADDING).min(120);
let signature_text = crate::ui::markdown::highlighted_code_block(
&self.signature,
sig.signature.as_str(),
&self.language,
None,
Arc::clone(&self.config_loader),
@@ -120,7 +170,7 @@ impl Component for SignatureHelp {
let (sig_width, sig_height) =
crate::ui::text::required_size(&signature_text, max_text_width);
let (width, height) = match self.signature_doc {
let (width, height) = match sig.signature_doc {
Some(ref doc) => {
let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader));
let doc_text = doc_md.parse(None);
@@ -134,6 +184,12 @@ impl Component for SignatureHelp {
None => (sig_width, sig_height),
};
Some((width + PADDING, height + PADDING))
let sig_index_width = if self.signatures.len() > 1 {
self.signature_index().len() + 1
} else {
0
};
Some((width + PADDING + sig_index_width as u16, height + PADDING))
}
}

View File

@@ -1,4 +1,5 @@
use crate::compositor::{Component, Context};
use arc_swap::ArcSwap;
use tui::{
buffer::Buffer as Surface,
text::{Span, Spans, Text},
@@ -6,7 +7,7 @@ use tui::{
use std::sync::Arc;
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag};
use pulldown_cmark::{CodeBlockKind, Event, HeadingLevel, Options, Parser, Tag, TagEnd};
use helix_core::{
syntax::{self, HighlightEvent, InjectionLanguageMarker, Syntax},
@@ -31,7 +32,7 @@ pub fn highlighted_code_block<'a>(
text: &str,
language: &str,
theme: Option<&Theme>,
config_loader: Arc<syntax::Loader>,
config_loader: Arc<ArcSwap<syntax::Loader>>,
additional_highlight_spans: Option<Vec<(usize, std::ops::Range<usize>)>>,
) -> Text<'a> {
let mut spans = Vec::new();
@@ -48,6 +49,7 @@ pub fn highlighted_code_block<'a>(
let ropeslice = RopeSlice::from(text);
let syntax = config_loader
.load()
.language_configuration_for_injection_string(&InjectionLanguageMarker::Name(
language.into(),
))
@@ -121,7 +123,7 @@ pub fn highlighted_code_block<'a>(
pub struct Markdown {
contents: String,
config_loader: Arc<syntax::Loader>,
config_loader: Arc<ArcSwap<syntax::Loader>>,
}
// TODO: pre-render and self reference via Pin
@@ -140,7 +142,7 @@ impl Markdown {
];
const INDENT: &'static str = " ";
pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
pub fn new(contents: String, config_loader: Arc<ArcSwap<syntax::Loader>>) -> Self {
Self {
contents,
config_loader,
@@ -209,7 +211,7 @@ impl Markdown {
list_stack.push(list);
}
Event::End(Tag::List(_)) => {
Event::End(TagEnd::List(_)) => {
list_stack.pop();
// whenever top-level list closes, empty line
@@ -249,7 +251,10 @@ impl Markdown {
Event::End(tag) => {
tags.pop();
match tag {
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) | Tag::Item => {
TagEnd::Heading(_)
| TagEnd::Paragraph
| TagEnd::CodeBlock
| TagEnd::Item => {
push_line(&mut spans, &mut lines);
}
_ => (),
@@ -257,7 +262,7 @@ impl Markdown {
// whenever heading, code block or paragraph closes, empty line
match tag {
Tag::Heading(_, _, _) | Tag::Paragraph | Tag::CodeBlock(_) => {
TagEnd::Heading(_) | TagEnd::Paragraph | TagEnd::CodeBlock => {
lines.push(Spans::default());
}
_ => (),
@@ -279,7 +284,7 @@ impl Markdown {
lines.extend(tui_text.lines.into_iter());
} else {
let style = match tags.last() {
Some(Tag::Heading(level, ..)) => match level {
Some(Tag::Heading { level, .. }) => match level {
HeadingLevel::H1 => heading_styles[0],
HeadingLevel::H2 => heading_styles[1],
HeadingLevel::H3 => heading_styles[2],
@@ -341,7 +346,7 @@ impl Component for Markdown {
let text = self.parse(Some(&cx.editor.theme));
let par = Paragraph::new(text)
let par = Paragraph::new(&text)
.wrap(Wrap { trim: false })
.scroll((cx.scroll.unwrap_or_default() as u16, 0));

View File

@@ -241,9 +241,9 @@ impl<T: Item> Menu<T> {
}
impl<T: Item + PartialEq> Menu<T> {
pub fn replace_option(&mut self, old_option: T, new_option: T) {
pub fn replace_option(&mut self, old_option: &T, new_option: T) {
for option in &mut self.options {
if old_option == *option {
if old_option == option {
*option = new_option;
break;
}
@@ -427,6 +427,7 @@ impl<T: Item + 'static> Component for Menu<T> {
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
} else if !render_borders {
// Draw scroll track
cell.set_symbol(half_block);
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
}

View File

@@ -13,11 +13,12 @@ mod spinner;
mod statusline;
mod text;
use crate::compositor::{Component, Compositor};
use crate::compositor::Compositor;
use crate::filter_picker_entry;
use crate::job::{self, Callback};
pub use completion::{Completion, CompletionItem};
pub use editor::EditorView;
use helix_stdx::rope;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::{DynamicPicker, FileLocation, Picker};
@@ -26,8 +27,6 @@ pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;
use helix_view::Editor;
use std::path::PathBuf;
@@ -63,7 +62,22 @@ pub fn regex_prompt(
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static,
fun: impl Fn(&mut crate::compositor::Context, rope::Regex, PromptEvent) + 'static,
) {
raw_regex_prompt(
cx,
prompt,
history_register,
completion_fn,
move |cx, regex, _, event| fun(cx, regex, event),
);
}
pub fn raw_regex_prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut crate::compositor::Context, rope::Regex, &str, PromptEvent) + 'static,
) {
let (view, doc) = current!(cx.editor);
let doc_id = view.doc;
@@ -94,10 +108,13 @@ pub fn regex_prompt(
false
};
match RegexBuilder::new(input)
.case_insensitive(case_insensitive)
.multi_line(true)
.build()
match rope::RegexBuilder::new()
.syntax(
rope::Config::new()
.case_insensitive(case_insensitive)
.multi_line(true),
)
.build(input)
{
Ok(regex) => {
let (view, doc) = current!(cx.editor);
@@ -110,7 +127,7 @@ pub fn regex_prompt(
view.jumps.push((doc_id, snapshot.clone()));
}
fun(cx, regex, event);
fun(cx, regex, input, event);
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc, config.scrolloff);
@@ -126,14 +143,12 @@ pub fn regex_prompt(
move |_editor: &mut Editor, compositor: &mut Compositor| {
let contents = Text::new(format!("{}", err));
let size = compositor.size();
let mut popup = Popup::new("invalid-regex", contents)
let popup = Popup::new("invalid-regex", contents)
.position(Some(helix_core::Position::new(
size.height as usize - 2, // 2 = statusline + commandline
0,
)))
.auto_close(true);
popup.required_size((size.width, size.height));
compositor.replace_or_push("invalid-regex", popup);
},
));
@@ -336,8 +351,8 @@ pub mod completers {
pub fn language(editor: &Editor, input: &str) -> Vec<Completion> {
let text: String = "text".into();
let language_ids = editor
.syn_loader
let loader = editor.syn_loader.load();
let language_ids = loader
.language_configs()
.map(|config| &config.language_id)
.chain(std::iter::once(&text));
@@ -428,9 +443,9 @@ pub mod completers {
path
} else {
match path.parent() {
Some(path) if !path.as_os_str().is_empty() => path.to_path_buf(),
Some(path) if !path.as_os_str().is_empty() => Cow::Borrowed(path),
// Path::new("h")'s parent is Some("")...
_ => helix_stdx::env::current_working_dir(),
_ => Cow::Owned(helix_stdx::env::current_working_dir()),
}
};
@@ -492,4 +507,18 @@ pub mod completers {
files
}
}
pub fn register(editor: &Editor, input: &str) -> Vec<Completion> {
let iter = editor
.registers
.iter_preview()
// Exclude special registers that shouldn't be written to
.filter(|(ch, _)| !matches!(ch, '%' | '#' | '.'))
.map(|(ch, _)| ch.to_string());
fuzzy_match(input, iter, false)
.into_iter()
.map(|(name, _)| ((0..), name.into()))
.collect()
}
}

View File

@@ -461,14 +461,17 @@ impl<T: Item + 'static> Picker<T> {
// Then attempt to highlight it if it has no language set
if doc.language_config().is_none() {
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader) {
if let Some(language_config) = doc.detect_language_config(&cx.editor.syn_loader.load())
{
doc.language = Some(language_config.clone());
let text = doc.text().clone();
let loader = cx.editor.syn_loader.clone();
let job = tokio::task::spawn_blocking(move || {
let syntax = language_config.highlight_config(&loader.scopes()).and_then(
|highlight_config| Syntax::new(text.slice(..), highlight_config, loader),
);
let syntax = language_config
.highlight_config(&loader.load().scopes())
.and_then(|highlight_config| {
Syntax::new(text.slice(..), highlight_config, loader)
});
let callback = move |editor: &mut Editor, compositor: &mut Compositor| {
let Some(syntax) = syntax else {
log::info!("highlighting picker item failed");

View File

@@ -11,9 +11,12 @@ use tui::{
use helix_core::Position;
use helix_view::{
graphics::{Margin, Rect},
input::{MouseEvent, MouseEventKind},
Editor,
};
const MIN_HEIGHT: u16 = 4;
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
// a width/height hint. maybe Popup(Box<Component>)
@@ -21,10 +24,9 @@ pub struct Popup<T: Component> {
contents: T,
position: Option<Position>,
margin: Margin,
size: (u16, u16),
child_size: (u16, u16),
area: Rect,
position_bias: Open,
scroll: usize,
scroll_half_pages: usize,
auto_close: bool,
ignore_escape_key: bool,
id: &'static str,
@@ -37,10 +39,9 @@ impl<T: Component> Popup<T> {
contents,
position: None,
margin: Margin::none(),
size: (0, 0),
position_bias: Open::Below,
child_size: (0, 0),
scroll: 0,
area: Rect::new(0, 0, 0, 0),
scroll_half_pages: 0,
auto_close: false,
ignore_escape_key: false,
id,
@@ -92,58 +93,12 @@ impl<T: Component> Popup<T> {
self
}
/// Calculate the position where the popup should be rendered and return the coordinates of the
/// top left corner.
pub fn get_rel_position(&mut self, viewport: Rect, editor: &Editor) -> (u16, u16) {
let position = self
.position
.get_or_insert_with(|| editor.cursor().0.unwrap_or_default());
let (width, height) = self.size;
// if there's a orientation preference, use that
// if we're on the top part of the screen, do below
// if we're on the bottom part, do above
// -- make sure frame doesn't stick out of bounds
let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width {
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
}
let can_put_below = viewport.height > rel_y + height;
let can_put_above = rel_y.checked_sub(height).is_some();
let final_pos = match self.position_bias {
Open::Below => match can_put_below {
true => Open::Below,
false => Open::Above,
},
Open::Above => match can_put_above {
true => Open::Above,
false => Open::Below,
},
};
rel_y = match final_pos {
Open::Above => rel_y.saturating_sub(height),
Open::Below => rel_y + 1,
};
(rel_x, rel_y)
pub fn scroll_half_page_down(&mut self) {
self.scroll_half_pages += 1;
}
pub fn get_size(&self) -> (u16, u16) {
(self.size.0, self.size.1)
}
pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction {
let max_offset = self.child_size.1.saturating_sub(self.size.1);
self.scroll = (self.scroll + offset).min(max_offset as usize);
} else {
self.scroll = self.scroll.saturating_sub(offset);
}
pub fn scroll_half_page_up(&mut self) {
self.scroll_half_pages = self.scroll_half_pages.saturating_sub(1);
}
/// Toggles the Popup's scrollbar.
@@ -163,13 +118,93 @@ impl<T: Component> Popup<T> {
}
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
// trigger required_size so we recalculate if the child changed
self.required_size((viewport.width, viewport.height));
let child_size = self
.contents
.required_size((viewport.width, viewport.height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
let (rel_x, rel_y) = self.get_rel_position(viewport, editor);
self.area_internal(viewport, editor, child_size)
}
// clip to viewport
viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1))
pub fn area_internal(
&mut self,
viewport: Rect,
editor: &Editor,
child_size: (u16, u16),
) -> Rect {
let width = child_size.0.min(viewport.width);
let height = child_size.1.min(viewport.height.saturating_sub(2)); // add some spacing in the viewport
let position = self
.position
.get_or_insert_with(|| editor.cursor().0.unwrap_or_default());
// if there's a orientation preference, use that
// if we're on the top part of the screen, do below
// if we're on the bottom part, do above
// -- make sure frame doesn't stick out of bounds
let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width {
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
}
let can_put_below = viewport.height > rel_y + MIN_HEIGHT;
let can_put_above = rel_y.checked_sub(MIN_HEIGHT).is_some();
let final_pos = match self.position_bias {
Open::Below => match can_put_below {
true => Open::Below,
false => Open::Above,
},
Open::Above => match can_put_above {
true => Open::Above,
false => Open::Below,
},
};
match final_pos {
Open::Above => {
rel_y = rel_y.saturating_sub(height);
Rect::new(rel_x, rel_y, width, position.row as u16 - rel_y)
}
Open::Below => {
rel_y += 1;
let y_max = viewport.bottom().min(height + rel_y);
Rect::new(rel_x, rel_y, width, y_max - rel_y)
}
}
}
fn handle_mouse_event(
&mut self,
&MouseEvent {
kind,
column: x,
row: y,
..
}: &MouseEvent,
) -> EventResult {
let mouse_is_within_popup = x >= self.area.left()
&& x < self.area.right()
&& y >= self.area.top()
&& y < self.area.bottom();
if !mouse_is_within_popup {
return EventResult::Ignored(None);
}
match kind {
MouseEventKind::ScrollDown if self.has_scrollbar => {
self.scroll_half_page_down();
EventResult::Consumed(None)
}
MouseEventKind::ScrollUp if self.has_scrollbar => {
self.scroll_half_page_up();
EventResult::Consumed(None)
}
_ => EventResult::Ignored(None),
}
}
}
@@ -177,6 +212,7 @@ impl<T: Component> Component for Popup<T> {
fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
let key = match event {
Event::Key(event) => *event,
Event::Mouse(event) => return self.handle_mouse_event(event),
Event::Resize(_, _) => {
// TODO: calculate inner area, call component's handle_event with that area
return EventResult::Ignored(None);
@@ -200,11 +236,11 @@ impl<T: Component> Component for Popup<T> {
EventResult::Consumed(Some(close_fn))
}
ctrl!('d') => {
self.scroll(self.size.1 as usize / 2, true);
self.scroll_half_page_down();
EventResult::Consumed(None)
}
ctrl!('u') => {
self.scroll(self.size.1 as usize / 2, false);
self.scroll_half_page_up();
EventResult::Consumed(None)
}
_ => {
@@ -223,33 +259,41 @@ impl<T: Component> Component for Popup<T> {
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let max_width = 120.min(viewport.0);
let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
const MAX_WIDTH: u16 = 120;
const MAX_HEIGHT: u16 = 26;
let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin);
let inner = Rect::new(0, 0, MAX_WIDTH, MAX_HEIGHT).inner(&self.margin);
let (width, height) = self
.contents
.required_size((inner.width, inner.height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
self.child_size = (width, height);
self.size = (
(width + self.margin.width()).min(max_width),
(height + self.margin.height()).min(max_height),
let size = (
(width + self.margin.width()).min(MAX_WIDTH),
(height + self.margin.height()).min(MAX_HEIGHT),
);
// re-clamp scroll offset
let max_offset = self.child_size.1.saturating_sub(self.size.1);
self.scroll = self.scroll.min(max_offset as usize);
Some(self.size)
Some(size)
}
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
let area = self.area(viewport, cx.editor);
cx.scroll = Some(self.scroll);
let child_size = self
.contents
.required_size((viewport.width, viewport.height))
.expect("Component needs required_size implemented in order to be embedded in a popup");
let area = self.area_internal(viewport, cx.editor, child_size);
self.area = area;
let max_offset = child_size.1.saturating_sub(area.height) as usize;
let half_page_size = (area.height / 2) as usize;
let scroll = max_offset.min(self.scroll_half_pages * half_page_size);
if half_page_size > 0 {
self.scroll_half_pages = scroll / half_page_size;
}
cx.scroll = Some(scroll);
// clear area
let background = cx.editor.theme.get("ui.popup");
@@ -277,9 +321,8 @@ impl<T: Component> Component for Popup<T> {
// render scrollbar if contents do not fit
if self.has_scrollbar {
let win_height = (inner.height as usize).saturating_sub(2 * border);
let len = (self.child_size.1 as usize).saturating_sub(2 * border);
let len = (child_size.1 as usize).saturating_sub(2 * border);
let fits = len <= win_height;
let scroll = self.scroll;
let scroll_style = cx.editor.theme.get("ui.menu.scroll");
const fn div_ceil(a: usize, b: usize) -> usize {
@@ -303,6 +346,7 @@ impl<T: Component> Component for Popup<T> {
cell.set_fg(scroll_style.fg.unwrap_or(helix_view::theme::Color::Reset));
} else if !render_borders {
// Draw scroll track
cell.set_symbol(half_block);
cell.set_fg(scroll_style.bg.unwrap_or(helix_view::theme::Color::Reset));
}
}

View File

@@ -1,5 +1,6 @@
use crate::compositor::{Component, Compositor, Context, Event, EventResult};
use crate::{alt, ctrl, key, shift, ui};
use arc_swap::ArcSwap;
use helix_core::syntax;
use helix_view::input::KeyEvent;
use helix_view::keyboard::KeyCode;
@@ -34,7 +35,7 @@ pub struct Prompt {
callback_fn: CallbackFn,
pub doc_fn: DocFn,
next_char_handler: Option<PromptCharHandler>,
language: Option<(&'static str, Arc<syntax::Loader>)>,
language: Option<(&'static str, Arc<ArcSwap<syntax::Loader>>)>,
}
#[derive(Clone, Copy, PartialEq, Eq)]
@@ -98,7 +99,11 @@ impl Prompt {
self
}
pub fn with_language(mut self, language: &'static str, loader: Arc<syntax::Loader>) -> Self {
pub fn with_language(
mut self,
language: &'static str,
loader: Arc<ArcSwap<syntax::Loader>>,
) -> Self {
self.language = Some((language, loader));
self
}
@@ -393,7 +398,7 @@ impl Prompt {
height,
);
if !self.completion.is_empty() {
if completion_area.height > 0 && !self.completion.is_empty() {
let area = completion_area;
let background = theme.get("ui.menu");

View File

@@ -1,16 +1,18 @@
use std::{collections::HashMap, time::Instant};
use helix_lsp::LanguageServerId;
#[derive(Default, Debug)]
pub struct ProgressSpinners {
inner: HashMap<usize, Spinner>,
inner: HashMap<LanguageServerId, Spinner>,
}
impl ProgressSpinners {
pub fn get(&self, id: usize) -> Option<&Spinner> {
pub fn get(&self, id: LanguageServerId) -> Option<&Spinner> {
self.inner.get(&id)
}
pub fn get_or_create(&mut self, id: usize) -> &mut Spinner {
pub fn get_or_create(&mut self, id: LanguageServerId) -> &mut Spinner {
self.inner.entry(id).or_default()
}
}

View File

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

View File

@@ -33,7 +33,7 @@ impl Component for Text {
fn render(&mut self, area: Rect, surface: &mut Surface, _cx: &mut Context) {
use tui::widgets::{Paragraph, Widget, Wrap};
let par = Paragraph::new(self.contents.clone()).wrap(Wrap { trim: false });
let par = Paragraph::new(&self.contents).wrap(Wrap { trim: false });
// .scroll(x, y) offsets
par.render(area, surface);

View File

@@ -6,13 +6,13 @@ async fn auto_indent_c() -> anyhow::Result<()> {
AppBuilder::new().with_file("foo.c", None),
// switches to append mode?
(
helpers::platform_line("void foo() {#[|}]#"),
"void foo() {#[|}]#",
"i<ret><esc>",
helpers::platform_line(indoc! {"\
indoc! {"\
void foo() {
#[|\n]#\
}
"}),
"},
),
)
.await?;

View File

@@ -19,6 +19,7 @@ async fn insert_basic() -> anyhow::Result<()> {
format!("#[{}|]#", LINE_END),
format!("i{}", pair.0),
format!("{}#[|{}]#{}", pair.0, pair.1, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -46,6 +47,7 @@ async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> {
format!("#[{}|]#", LINE_END),
format!("i{}", open),
format!("{}#[|{}]#{}", open, close, LINE_END),
LineFeedHandling::AsIs,
),
)
.await?;
@@ -56,6 +58,7 @@ async fn insert_configured_multi_byte_chars() -> anyhow::Result<()> {
format!("{}#[{}|]#{}", open, close, LINE_END),
format!("i{}", close),
format!("{}{}#[|{}]#", open, close, LINE_END),
LineFeedHandling::AsIs,
),
)
.await?;
@@ -71,6 +74,7 @@ async fn insert_after_word() -> anyhow::Result<()> {
format!("foo#[{}|]#", LINE_END),
format!("i{}", pair.0),
format!("foo{}#[|{}]#{}", pair.0, pair.1, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -80,6 +84,7 @@ async fn insert_after_word() -> anyhow::Result<()> {
format!("foo#[{}|]#", LINE_END),
format!("i{}", pair.0),
format!("foo{}#[|{}]#", pair.0, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -94,6 +99,7 @@ async fn insert_before_word() -> anyhow::Result<()> {
format!("#[f|]#oo{}", LINE_END),
format!("i{}", pair.0),
format!("{}#[|f]#oo{}", pair.0, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -108,6 +114,7 @@ async fn insert_before_word_selection() -> anyhow::Result<()> {
format!("#[foo|]#{}", LINE_END),
format!("i{}", pair.0),
format!("{}#[|foo]#{}", pair.0, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -122,6 +129,7 @@ async fn insert_before_word_selection_trailing_word() -> anyhow::Result<()> {
format!("foo#[ wor|]#{}", LINE_END),
format!("i{}", pair.0),
format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -136,6 +144,7 @@ async fn insert_closer_selection_trailing_word() -> anyhow::Result<()> {
format!("foo{}#[|{} wor]#{}", pair.0, pair.1, LINE_END),
format!("i{}", pair.1),
format!("foo{}{}#[| wor]#{}", pair.0, pair.1, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -155,6 +164,7 @@ async fn insert_before_eol() -> anyhow::Result<()> {
open = pair.0,
close = pair.1
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -177,6 +187,7 @@ async fn insert_auto_pairs_disabled() -> anyhow::Result<()> {
format!("#[{}|]#", LINE_END),
format!("i{}", pair.0),
format!("{}#[|{}]#", pair.0, LINE_END),
LineFeedHandling::AsIs,
),
)
.await?;
@@ -197,6 +208,7 @@ async fn insert_multi_range() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -211,6 +223,7 @@ async fn insert_before_multi_code_point_graphemes() -> anyhow::Result<()> {
format!("hello #[👨‍👩‍👧‍👦|]# goodbye{}", LINE_END),
format!("i{}", pair.1),
format!("hello {}#[|👨‍👩‍👧‍👦]# goodbye{}", pair.1, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -226,6 +239,7 @@ async fn insert_at_end_of_document() -> anyhow::Result<()> {
in_keys: format!("i{}", pair.0),
out_text: format!("{}{}{}", LINE_END, pair.0, pair.1),
out_selection: Selection::single(LINE_END.len() + 1, LINE_END.len() + 2),
line_feed_handling: LineFeedHandling::AsIs,
})
.await?;
@@ -235,6 +249,7 @@ async fn insert_at_end_of_document() -> anyhow::Result<()> {
in_keys: format!("i{}", pair.0),
out_text: format!("foo{}{}{}", LINE_END, pair.0, pair.1),
out_selection: Selection::single(LINE_END.len() + 4, LINE_END.len() + 5),
line_feed_handling: LineFeedHandling::AsIs,
})
.await?;
}
@@ -259,6 +274,7 @@ async fn insert_close_inside_pair() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -283,6 +299,7 @@ async fn insert_close_inside_pair_multi() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -307,6 +324,7 @@ async fn insert_nested_open_inside_pair() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -338,6 +356,7 @@ async fn insert_nested_open_inside_pair_multi() -> anyhow::Result<()> {
inner_close = inner_pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -358,6 +377,7 @@ async fn append_basic() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -377,6 +397,7 @@ async fn append_multi_range() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -401,6 +422,7 @@ async fn append_close_inside_pair() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -425,6 +447,7 @@ async fn append_close_inside_pair_multi() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -444,6 +467,7 @@ async fn append_end_of_word() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -458,6 +482,7 @@ async fn append_middle_of_word() -> anyhow::Result<()> {
format!("#[wo|]#rd{}", LINE_END),
format!("a{}", pair.1),
format!("#[wo{}r|]#d{}", pair.1, LINE_END),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -477,6 +502,7 @@ async fn append_end_of_word_multi() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -501,6 +527,7 @@ async fn append_inside_nested_pair() -> anyhow::Result<()> {
close = pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}
@@ -532,6 +559,7 @@ async fn append_inside_nested_pair_multi() -> anyhow::Result<()> {
inner_close = inner_pair.1,
eol = LINE_END
),
LineFeedHandling::AsIs,
))
.await?;
}

View File

@@ -9,89 +9,89 @@ mod write;
async fn test_selection_duplication() -> anyhow::Result<()> {
// Forward
test((
platform_line(indoc! {"\
indoc! {"\
#[lo|]#rem
ipsum
dolor
"}),
"},
"CC",
platform_line(indoc! {"\
indoc! {"\
#(lo|)#rem
#(ip|)#sum
#[do|]#lor
"}),
"},
))
.await?;
// Backward
test((
platform_line(indoc! {"\
indoc! {"\
#[|lo]#rem
ipsum
dolor
"}),
"},
"CC",
platform_line(indoc! {"\
indoc! {"\
#(|lo)#rem
#(|ip)#sum
#[|do]#lor
"}),
"},
))
.await?;
// Copy the selection to previous line, skipping the first line in the file
test((
platform_line(indoc! {"\
indoc! {"\
test
#[testitem|]#
"}),
"},
"<A-C>",
platform_line(indoc! {"\
indoc! {"\
test
#[testitem|]#
"}),
"},
))
.await?;
// Copy the selection to previous line, including the first line in the file
test((
platform_line(indoc! {"\
indoc! {"\
test
#[test|]#
"}),
"},
"<A-C>",
platform_line(indoc! {"\
indoc! {"\
#[test|]#
#(test|)#
"}),
"},
))
.await?;
// Copy the selection to next line, skipping the last line in the file
test((
platform_line(indoc! {"\
indoc! {"\
#[testitem|]#
test
"}),
"},
"C",
platform_line(indoc! {"\
indoc! {"\
#[testitem|]#
test
"}),
"},
))
.await?;
// Copy the selection to next line, including the last line in the file
test((
platform_line(indoc! {"\
indoc! {"\
#[test|]#
test
"}),
"},
"C",
platform_line(indoc! {"\
indoc! {"\
#(test|)#
#[test|]#
"}),
"},
))
.await?;
Ok(())
@@ -153,23 +153,45 @@ async fn test_goto_file_impl() -> anyhow::Result<()> {
)
.await?;
// ';' is behind the path
test_key_sequence(
&mut AppBuilder::new().with_file(file.path(), None).build()?,
Some("iimport 'one.js';<esc>B;gf"),
Some(&|app| {
assert_eq!(1, match_paths(app, vec!["one.js"]));
}),
false,
)
.await?;
// allow numeric values in path
test_key_sequence(
&mut AppBuilder::new().with_file(file.path(), None).build()?,
Some("iimport 'one123.js'<esc>B;gf"),
Some(&|app| {
assert_eq!(1, match_paths(app, vec!["one123.js"]));
}),
false,
)
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_multi_selection_paste() -> anyhow::Result<()> {
test((
platform_line(indoc! {"\
indoc! {"\
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"}),
"},
"yp",
platform_line(indoc! {"\
indoc! {"\
lorem#[|lorem]#
ipsum#(|ipsum)#
dolor#(|dolor)#
"}),
"},
))
.await?;
@@ -180,58 +202,58 @@ async fn test_multi_selection_paste() -> anyhow::Result<()> {
async fn test_multi_selection_shell_commands() -> anyhow::Result<()> {
// pipe
test((
platform_line(indoc! {"\
indoc! {"\
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"}),
"},
"|echo foo<ret>",
platform_line(indoc! {"\
indoc! {"\
#[|foo\n]#
#(|foo\n)#
#(|foo\n)#
"}),
"},
))
.await?;
// insert-output
test((
platform_line(indoc! {"\
indoc! {"\
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"}),
"},
"!echo foo<ret>",
platform_line(indoc! {"\
indoc! {"\
#[|foo\n]#
lorem
#(|foo\n)#
ipsum
#(|foo\n)#
dolor
"}),
"},
))
.await?;
// append-output
test((
platform_line(indoc! {"\
indoc! {"\
#[|lorem]#
#(|ipsum)#
#(|dolor)#
"}),
"},
"<A-!>echo foo<ret>",
platform_line(indoc! {"\
indoc! {"\
lorem#[|foo\n]#
ipsum#(|foo\n)#
dolor#(|foo\n)#
"}),
"},
))
.await?;
@@ -247,7 +269,13 @@ async fn test_undo_redo() -> anyhow::Result<()> {
// * u Undo the two newlines. We're now on line 1.
// * <C-o><C-i> Jump forward an back again in the jumplist. This would panic
// if the jumplist were not being updated correctly.
test(("#[|]#", "2[<space><C-s>u<C-o><C-i>", "#[|]#")).await?;
test((
"#[|]#",
"2[<space><C-s>u<C-o><C-i>",
"#[|]#",
LineFeedHandling::AsIs,
))
.await?;
// A jumplist selection is passed through an edit and then an undo and then a redo.
//
@@ -258,10 +286,22 @@ async fn test_undo_redo() -> anyhow::Result<()> {
// * <C-o> Jump back in the jumplist. This would panic if the jumplist were not being
// updated correctly.
// * <C-i> Jump forward to line 1.
test(("#[|]#", "[<space><C-s>kduU<C-o><C-i>", "#[|]#")).await?;
test((
"#[|]#",
"[<space><C-s>kduU<C-o><C-i>",
"#[|]#",
LineFeedHandling::AsIs,
))
.await?;
// In this case we 'redo' manually to ensure that the transactions are composing correctly.
test(("#[|]#", "[<space>u[<space>u", "#[|]#")).await?;
test((
"#[|]#",
"[<space>u[<space>u",
"#[|]#",
LineFeedHandling::AsIs,
))
.await?;
Ok(())
}
@@ -270,35 +310,35 @@ async fn test_undo_redo() -> anyhow::Result<()> {
async fn test_extend_line() -> anyhow::Result<()> {
// extend with line selected then count
test((
platform_line(indoc! {"\
indoc! {"\
#[l|]#orem
ipsum
dolor
"}),
"},
"x2x",
platform_line(indoc! {"\
indoc! {"\
#[lorem
ipsum
dolor\n|]#
"}),
"},
))
.await?;
// extend with count on partial selection
test((
platform_line(indoc! {"\
indoc! {"\
#[l|]#orem
ipsum
"}),
"},
"2x",
platform_line(indoc! {"\
indoc! {"\
#[lorem
ipsum\n|]#
"}),
"},
))
.await?;
@@ -366,16 +406,11 @@ async fn test_character_info() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_char_backward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test(("#(x|)# #[x|]#", "c<space><backspace><esc>", "#[\n|]#")).await?;
test((
platform_line("#(x|)# #[x|]#"),
"c<space><backspace><esc>",
platform_line("#[\n|]#"),
))
.await?;
test((
platform_line("#( |)##( |)#a#( |)#axx#[x|]#a"),
"#( |)##( |)#a#( |)#axx#[x|]#a",
"li<backspace><esc>",
platform_line("#(a|)##(|a)#xx#[|a]#"),
"#(a|)##(|a)#xx#[|a]#",
))
.await?;
@@ -385,43 +420,33 @@ async fn test_delete_char_backward() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_word_backward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test((
platform_line("fo#[o|]#ba#(r|)#"),
"a<C-w><esc>",
platform_line("#[\n|]#"),
))
.await?;
test(("fo#[o|]#ba#(r|)#", "a<C-w><esc>", "#[\n|]#")).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_word_forward() -> anyhow::Result<()> {
// don't panic when deleting overlapping ranges
test((
platform_line("fo#[o|]#b#(|ar)#"),
"i<A-d><esc>",
platform_line("fo#[\n|]#"),
))
.await?;
test(("fo#[o|]#b#(|ar)#", "i<A-d><esc>", "fo#[\n|]#")).await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_delete_char_forward() -> anyhow::Result<()> {
test((
platform_line(indoc! {"\
indoc! {"\
#[abc|]#def
#(abc|)#ef
#(abc|)#f
#(abc|)#
"}),
"},
"a<del><esc>",
platform_line(indoc! {"\
indoc! {"\
#[abc|]#ef
#(abc|)#f
#(abc|)#
#(abc|)#
"}),
"},
))
.await?;
@@ -430,33 +455,37 @@ async fn test_delete_char_forward() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")]
async fn test_insert_with_indent() -> anyhow::Result<()> {
const INPUT: &str = "\
#[f|]#n foo() {
if let Some(_) = None {
const INPUT: &str = indoc! { "
#[f|]#n foo() {
if let Some(_) = None {
}
\x20
}
}
}
fn bar() {
fn bar() {
}";
}
"
};
// insert_at_line_start
test((
INPUT,
":lang rust<ret>%<A-s>I",
"\
#[f|]#n foo() {
#(i|)#f let Some(_) = None {
#(\n|)#\
\x20 #(}|)#
#(\x20|)#
#(}|)#
#(\n|)#\
#(f|)#n bar() {
#(\n|)#\
#(}|)#",
indoc! { "
#[f|]#n foo() {
#(i|)#f let Some(_) = None {
#(\n|)#
#(}|)#
#( |)#
#(}|)#
#(\n|)#
#(f|)#n bar() {
#(\n|)#
#(}|)#
"
},
))
.await?;
@@ -464,17 +493,19 @@ fn bar() {
test((
INPUT,
":lang rust<ret>%<A-s>A",
"\
fn foo() {#[\n|]#\
\x20 if let Some(_) = None {#(\n|)#\
\x20 #(\n|)#\
\x20 }#(\n|)#\
\x20#(\n|)#\
}#(\n|)#\
#(\n|)#\
fn bar() {#(\n|)#\
\x20 #(\n|)#\
}#(|)#",
indoc! { "
fn foo() {#[\n|]#
if let Some(_) = None {#(\n|)#
#(\n|)#
}#(\n|)#
#(\n|)#
}#(\n|)#
#(\n|)#
fn bar() {#(\n|)#
#(\n|)#
}#(\n|)#
"
},
))
.await?;
@@ -485,44 +516,151 @@ fn bar() {#(\n|)#\
async fn test_join_selections() -> anyhow::Result<()> {
// normal join
test((
platform_line(indoc! {"\
indoc! {"\
#[a|]#bc
def
"}),
"},
"J",
platform_line(indoc! {"\
indoc! {"\
#[a|]#bc def
"}),
"},
))
.await?;
// join with empty line
test((
platform_line(indoc! {"\
indoc! {"\
#[a|]#bc
def
"}),
"},
"JJ",
platform_line(indoc! {"\
indoc! {"\
#[a|]#bc def
"}),
"},
))
.await?;
// join with additional space in non-empty line
test((
platform_line(indoc! {"\
indoc! {"\
#[a|]#bc
def
"}),
"},
"JJ",
platform_line(indoc! {"\
indoc! {"\
#[a|]#bc def
"}),
"},
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_join_selections_space() -> anyhow::Result<()> {
// join with empty lines panic
test((
indoc! {"\
#[a
b
c
d
e|]#
"},
"<A-J>",
indoc! {"\
a#[ |]#b#( |)#c#( |)#d#( |)#e
"},
))
.await?;
// normal join
test((
indoc! {"\
#[a|]#bc
def
"},
"<A-J>",
indoc! {"\
abc#[ |]#def
"},
))
.await?;
// join with empty line
test((
indoc! {"\
#[a|]#bc
def
"},
"<A-J>",
indoc! {"\
#[a|]#bc
def
"},
))
.await?;
// join with additional space in non-empty line
test((
indoc! {"\
#[a|]#bc
def
"},
"<A-J><A-J>",
indoc! {"\
abc#[ |]#def
"},
))
.await?;
// join with retained trailing spaces
test((
indoc! {"\
#[aaa
bb
c |]#
"},
"<A-J>",
indoc! {"\
aaa #[ |]#bb #( |)#c
"},
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_read_file() -> anyhow::Result<()> {
let mut file = tempfile::NamedTempFile::new()?;
let contents_to_read = "some contents";
let output_file = helpers::temp_file_with_contents(contents_to_read)?;
test_key_sequence(
&mut helpers::AppBuilder::new()
.with_file(file.path(), None)
.build()?,
Some(&format!(":r {:?}<ret><esc>:w<ret>", output_file.path())),
Some(&|app| {
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
}),
false,
)
.await?;
let expected_contents = LineFeedHandling::Native.apply(contents_to_read);
helpers::assert_file_has_content(&mut file, &expected_contents)?;
Ok(())
}

View File

@@ -6,7 +6,7 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> {
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
fn foo() {
let result = if true {
"yes"
@@ -14,9 +14,9 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> {
"no#["|]#
}
}
"##}),
"##},
"<A-e>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -24,10 +24,10 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> {
\"no\"#[\n|]#
}
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -35,9 +35,9 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> {
\"no\"#[\n|]#
}
}
"}),
"},
"<A-e>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -45,11 +45,11 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> {
\"no\"
}#[\n|]#
}
"}),
"},
),
// select mode extends
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
fn foo() {
let result = if true {
"yes"
@@ -57,9 +57,9 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> {
#["no"|]#
}
}
"##}),
"##},
"v<A-e><A-e>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -67,7 +67,7 @@ async fn test_move_parent_node_end() -> anyhow::Result<()> {
#[\"no\"
}\n|]#
}
"}),
"},
),
];
@@ -84,7 +84,7 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
fn foo() {
let result = if true {
"yes"
@@ -92,9 +92,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
"no#["|]#
}
}
"##}),
"##},
"<A-b>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -102,10 +102,10 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
#[\"|]#no\"
}
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -113,9 +113,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
\"no\"#[\n|]#
}
}
"}),
"},
"<A-b>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -123,10 +123,10 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
\"no\"
}
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -134,9 +134,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
\"no\"
}
}
"}),
"},
"<A-b>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -144,11 +144,11 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
\"no\"
}
}
"}),
"},
),
// select mode extends
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
fn foo() {
let result = if true {
"yes"
@@ -156,9 +156,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
#["no"|]#
}
}
"##}),
"##},
"v<A-b><A-b>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -166,10 +166,10 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
]#\"no\"
}
}
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
fn foo() {
let result = if true {
"yes"
@@ -177,9 +177,9 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
#["no"|]#
}
}
"##}),
"##},
"v<A-b><A-b><A-b>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -187,7 +187,7 @@ async fn test_move_parent_node_start() -> anyhow::Result<()> {
]#\"no\"
}
}
"}),
"},
),
];
@@ -204,7 +204,7 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
// single cursor stays single cursor, first goes to end of current
// node, then parent
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
fn foo() {
let result = if true {
"yes"
@@ -212,9 +212,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
"no#["|]#
}
}
"##}),
"##},
"i<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -222,10 +222,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"#[|\n]#
}
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -233,9 +233,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"#[\n|]#
}
}
"}),
"},
"i<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -243,12 +243,12 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"
}#[|\n]#
}
"}),
"},
),
// appending to the end of a line should still look at the current
// line, not the next one
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -256,9 +256,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no#[\"|]#
}
}
"}),
"},
"a<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -266,11 +266,11 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"
}#[\n|]#
}
"}),
"},
),
// before cursor is all whitespace, so insert tab
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -278,9 +278,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
#[\"no\"|]#
}
}
"}),
"},
"i<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -288,12 +288,12 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
#[|\"no\"]#
}
}
"}),
"},
),
// if selection spans multiple lines, it should still only look at the
// line on which the head is
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"
@@ -301,9 +301,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"|]#
}
}
"}),
"},
"a<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -311,10 +311,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"
}#[\n|]#
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"
@@ -322,9 +322,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"|]#
}
}
"}),
"},
"i<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
#[|\"yes\"
@@ -332,10 +332,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"]#
}
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
#[l|]#et result = if true {
#(\"yes\"
@@ -343,9 +343,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"|)#
}
}
"}),
"},
"i<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
#[|l]#et result = if true {
#(|\"yes\"
@@ -353,10 +353,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\")#
}
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"#[\n|]#
@@ -364,9 +364,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"#(\n|)#
}
}
"}),
"},
"i<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -374,10 +374,10 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"
}#(|\n)#
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"|]#
@@ -385,9 +385,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
#(\"no\"|)#
}
}
"}),
"},
"i<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
#[|\"yes\"]#
@@ -395,12 +395,12 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
#(|\"no\")#
}
}
"}),
"},
),
// if any cursors are not preceded by all whitespace, then do the
// smart_tab action
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"\n|]#
@@ -408,9 +408,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no#(\"\n|)#
}
}
"}),
"},
"i<tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
\"yes\"
@@ -418,11 +418,11 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no\"
}#(|\n)#
}
"}),
"},
),
// Ctrl-tab always inserts a tab
(
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
#[\"yes\"\n|]#
@@ -430,9 +430,9 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no#(\"\n|)#
}
}
"}),
"},
"i<S-tab>",
helpers::platform_line(indoc! {"\
indoc! {"\
fn foo() {
let result = if true {
#[|\"yes\"\n]#
@@ -440,7 +440,363 @@ async fn test_smart_tab_move_parent_node_end() -> anyhow::Result<()> {
\"no #(|\"\n)#
}
}
"}),
"},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn select_all_siblings() -> anyhow::Result<()> {
let tests = vec![
// basic tests
(
indoc! {r##"
let foo = bar(#[a|]#, b, c);
"##},
"<A-a>",
indoc! {r##"
let foo = bar(#[a|]#, #(b|)#, #(c|)#);
"##},
),
(
indoc! {r##"
let a = [
#[1|]#,
2,
3,
4,
5,
];
"##},
"<A-a>",
indoc! {r##"
let a = [
#[1|]#,
#(2|)#,
#(3|)#,
#(4|)#,
#(5|)#,
];
"##},
),
// direction is preserved
(
indoc! {r##"
let a = [
#[|1]#,
2,
3,
4,
5,
];
"##},
"<A-a>",
indoc! {r##"
let a = [
#[|1]#,
#(|2)#,
#(|3)#,
#(|4)#,
#(|5)#,
];
"##},
),
// can't pick any more siblings - selection stays the same
(
indoc! {r##"
let a = [
#[1|]#,
#(2|)#,
#(3|)#,
#(4|)#,
#(5|)#,
];
"##},
"<A-a>",
indoc! {r##"
let a = [
#[1|]#,
#(2|)#,
#(3|)#,
#(4|)#,
#(5|)#,
];
"##},
),
// each cursor does the sibling select independently
(
indoc! {r##"
let a = [
#[1|]#,
2,
3,
4,
5,
];
let b = [
#("one"|)#,
"two",
"three",
"four",
"five",
];
"##},
"<A-a>",
indoc! {r##"
let a = [
#[1|]#,
#(2|)#,
#(3|)#,
#(4|)#,
#(5|)#,
];
let b = [
#("one"|)#,
#("two"|)#,
#("three"|)#,
#("four"|)#,
#("five"|)#,
];
"##},
),
// conflicting sibling selections get normalized. Here, the primary
// selection would choose every list item, but because the secondary
// range covers more than one item, the descendent is the entire list,
// which means the sibling is the assignment. The list item ranges just
// get normalized out since the list itself becomes selected.
(
indoc! {r##"
let a = [
#[1|]#,
2,
#(3,
4|)#,
5,
];
"##},
"<A-a>",
indoc! {r##"
let #(a|)# = #[[
1,
2,
3,
4,
5,
]|]#;
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn select_all_children() -> anyhow::Result<()> {
let tests = vec![
// basic tests
(
indoc! {r##"
let foo = bar#[(a, b, c)|]#;
"##},
"<A-I>",
indoc! {r##"
let foo = bar(#[a|]#, #(b|)#, #(c|)#);
"##},
),
(
indoc! {r##"
let a = #[[
1,
2,
3,
4,
5,
]|]#;
"##},
"<A-I>",
indoc! {r##"
let a = [
#[1|]#,
#(2|)#,
#(3|)#,
#(4|)#,
#(5|)#,
];
"##},
),
// direction is preserved
(
indoc! {r##"
let a = #[|[
1,
2,
3,
4,
5,
]]#;
"##},
"<A-I>",
indoc! {r##"
let a = [
#[|1]#,
#(|2)#,
#(|3)#,
#(|4)#,
#(|5)#,
];
"##},
),
// can't pick any more children - selection stays the same
(
indoc! {r##"
let a = [
#[1|]#,
#(2|)#,
#(3|)#,
#(4|)#,
#(5|)#,
];
"##},
"<A-I>",
indoc! {r##"
let a = [
#[1|]#,
#(2|)#,
#(3|)#,
#(4|)#,
#(5|)#,
];
"##},
),
// each cursor does the sibling select independently
(
indoc! {r##"
let a = #[|[
1,
2,
3,
4,
5,
]]#;
let b = #([
"one",
"two",
"three",
"four",
"five",
]|)#;
"##},
"<A-I>",
indoc! {r##"
let a = [
#[|1]#,
#(|2)#,
#(|3)#,
#(|4)#,
#(|5)#,
];
let b = [
#("one"|)#,
#("two"|)#,
#("three"|)#,
#("four"|)#,
#("five"|)#,
];
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_select_next_sibling() -> anyhow::Result<()> {
let tests = vec![
// basic test
(
indoc! {r##"
fn inc(x: usize) -> usize { x + 1 #[}|]#
fn dec(x: usize) -> usize { x - 1 }
fn ident(x: usize) -> usize { x }
"##},
"<A-n>",
indoc! {r##"
fn inc(x: usize) -> usize { x + 1 }
#[fn dec(x: usize) -> usize { x - 1 }|]#
fn ident(x: usize) -> usize { x }
"##},
),
// direction is not preserved and is always forward.
(
indoc! {r##"
fn inc(x: usize) -> usize { x + 1 #[}|]#
fn dec(x: usize) -> usize { x - 1 }
fn ident(x: usize) -> usize { x }
"##},
"<A-n><A-;><A-n>",
indoc! {r##"
fn inc(x: usize) -> usize { x + 1 }
fn dec(x: usize) -> usize { x - 1 }
#[fn ident(x: usize) -> usize { x }|]#
"##},
),
];
for test in tests {
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
}
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_select_prev_sibling() -> anyhow::Result<()> {
let tests = vec![
// basic test
(
indoc! {r##"
fn inc(x: usize) -> usize { x + 1 }
fn dec(x: usize) -> usize { x - 1 }
#[|f]#n ident(x: usize) -> usize { x }
"##},
"<A-p>",
indoc! {r##"
fn inc(x: usize) -> usize { x + 1 }
#[|fn dec(x: usize) -> usize { x - 1 }]#
fn ident(x: usize) -> usize { x }
"##},
),
// direction is not preserved and is always backward.
(
indoc! {r##"
fn inc(x: usize) -> usize { x + 1 }
fn dec(x: usize) -> usize { x - 1 }
#[|f]#n ident(x: usize) -> usize { x }
"##},
"<A-p><A-;><A-p>",
indoc! {r##"
#[|fn inc(x: usize) -> usize { x + 1 }]#
fn dec(x: usize) -> usize { x - 1 }
fn ident(x: usize) -> usize { x }
"##},
),
];

View File

@@ -94,7 +94,10 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> {
)
.await?;
helpers::assert_file_has_content(file.as_file_mut(), &platform_line(&RANGE.end().to_string()))?;
helpers::assert_file_has_content(
&mut file,
&LineFeedHandling::Native.apply(&RANGE.end().to_string()),
)?;
Ok(())
}
@@ -114,14 +117,12 @@ async fn test_write() -> anyhow::Result<()> {
)
.await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
reload_file(&mut file).unwrap();
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(
helpers::platform_line("the gostak distims the doshes"),
LineFeedHandling::Native.apply("the gostak distims the doshes"),
file_content
);
@@ -138,24 +139,18 @@ async fn test_overwrite_protection() -> anyhow::Result<()> {
helpers::run_event_loop_until_idle(&mut app).await;
file.as_file_mut()
.write_all(helpers::platform_line("extremely important content").as_bytes())?;
.write_all("extremely important content".as_bytes())?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
test_key_sequence(&mut app, Some(":x<ret>"), None, false).await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
file.rewind()?;
reload_file(&mut file).unwrap();
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
file.read_to_string(&mut file_content)?;
assert_eq!(
helpers::platform_line("extremely important content"),
file_content
);
assert_eq!("extremely important content", file_content);
Ok(())
}
@@ -175,14 +170,13 @@ async fn test_write_quit() -> anyhow::Result<()> {
)
.await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
reload_file(&mut file).unwrap();
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
file.read_to_string(&mut file_content)?;
assert_eq!(
helpers::platform_line("the gostak distims the doshes"),
LineFeedHandling::Native.apply("the gostak distims the doshes"),
file_content
);
@@ -205,12 +199,13 @@ async fn test_write_concurrent() -> anyhow::Result<()> {
test_key_sequence(&mut app, Some(&command), None, false).await?;
file.as_file_mut().flush()?;
file.as_file_mut().sync_all()?;
reload_file(&mut file).unwrap();
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(platform_line(&RANGE.end().to_string()), file_content);
file.read_to_string(&mut file_content)?;
assert_eq!(
LineFeedHandling::Native.apply(&RANGE.end().to_string()),
file_content
);
Ok(())
}
@@ -276,7 +271,7 @@ async fn test_write_scratch_to_new_path() -> anyhow::Result<()> {
)
.await?;
helpers::assert_file_has_content(file.as_file_mut(), &helpers::platform_line("hello"))?;
helpers::assert_file_has_content(&mut file, &LineFeedHandling::Native.apply("hello"))?;
Ok(())
}
@@ -315,13 +310,13 @@ async fn test_write_auto_format_fails_still_writes() -> anyhow::Result<()> {
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.with_input_text("#[l|]#et foo = 0;\n")
.with_lang_config(helpers::test_syntax_conf(Some(lang_conf.into())))
.with_lang_loader(helpers::test_syntax_loader(Some(lang_conf.into())))
.build()?;
test_key_sequences(&mut app, vec![(Some(":w<ret>"), None)], false).await?;
// file still saves
helpers::assert_file_has_content(file.as_file_mut(), "let foo = 0;\n")?;
helpers::assert_file_has_content(&mut file, "let foo = 0;\n")?;
Ok(())
}
@@ -360,13 +355,13 @@ async fn test_write_new_path() -> anyhow::Result<()> {
.await?;
helpers::assert_file_has_content(
file1.as_file_mut(),
&helpers::platform_line("i can eat glass, it will not hurt me\n"),
&mut file1,
&LineFeedHandling::Native.apply("i can eat glass, it will not hurt me\n"),
)?;
helpers::assert_file_has_content(
file2.as_file_mut(),
&helpers::platform_line("i can eat glass, it will not hurt me\n"),
&mut file2,
&LineFeedHandling::Native.apply("i can eat glass, it will not hurt me\n"),
)?;
Ok(())
@@ -436,8 +431,8 @@ async fn test_write_insert_final_newline_added_if_missing() -> anyhow::Result<()
test_key_sequence(&mut app, Some(":w<ret>"), None, false).await?;
helpers::assert_file_has_content(
file.as_file_mut(),
&helpers::platform_line("have you tried chamomile tea?\n"),
&mut file,
&LineFeedHandling::Native.apply("have you tried chamomile tea?\n"),
)?;
Ok(())
@@ -448,14 +443,14 @@ async fn test_write_insert_final_newline_unchanged_if_not_missing() -> anyhow::R
let mut file = tempfile::NamedTempFile::new()?;
let mut app = helpers::AppBuilder::new()
.with_file(file.path(), None)
.with_input_text(&helpers::platform_line("#[t|]#en minutes, please\n"))
.with_input_text(LineFeedHandling::Native.apply("#[t|]#en minutes, please\n"))
.build()?;
test_key_sequence(&mut app, Some(":w<ret>"), None, false).await?;
helpers::assert_file_has_content(
file.as_file_mut(),
&helpers::platform_line("ten minutes, please\n"),
&mut file,
&LineFeedHandling::Native.apply("ten minutes, please\n"),
)?;
Ok(())
@@ -478,10 +473,8 @@ async fn test_write_insert_final_newline_unchanged_if_missing_and_false() -> any
test_key_sequence(&mut app, Some(":w<ret>"), None, false).await?;
helpers::assert_file_has_content(
file.as_file_mut(),
"the quiet rain continued through the night",
)?;
reload_file(&mut file).unwrap();
helpers::assert_file_has_content(&mut file, "the quiet rain continued through the night")?;
Ok(())
}
@@ -507,13 +500,13 @@ async fn test_write_all_insert_final_newline_add_if_missing_and_modified() -> an
.await?;
helpers::assert_file_has_content(
file1.as_file_mut(),
&helpers::platform_line("we don't serve time travelers here\n"),
&mut file1,
&LineFeedHandling::Native.apply("we don't serve time travelers here\n"),
)?;
helpers::assert_file_has_content(
file2.as_file_mut(),
&helpers::platform_line("a time traveler walks into a bar\n"),
&mut file2,
&LineFeedHandling::Native.apply("a time traveler walks into a bar\n"),
)?;
Ok(())
@@ -531,7 +524,82 @@ async fn test_write_all_insert_final_newline_do_not_add_if_unmodified() -> anyho
test_key_sequence(&mut app, Some(":wa<ret>"), None, false).await?;
helpers::assert_file_has_content(file.as_file_mut(), "i lost on Jeopardy!")?;
helpers::assert_file_has_content(&mut file, "i lost on Jeopardy!")?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_symlink_write() -> anyhow::Result<()> {
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(not(unix))]
use std::os::windows::fs::symlink_file as symlink;
let dir = tempfile::tempdir()?;
let mut file = tempfile::NamedTempFile::new_in(&dir)?;
let symlink_path = dir.path().join("linked");
symlink(file.path(), &symlink_path)?;
let mut app = helpers::AppBuilder::new()
.with_file(&symlink_path, None)
.build()?;
test_key_sequence(
&mut app,
Some("ithe gostak distims the doshes<ret><esc>:w<ret>"),
None,
false,
)
.await?;
reload_file(&mut file).unwrap();
let mut file_content = String::new();
file.as_file_mut().read_to_string(&mut file_content)?;
assert_eq!(
LineFeedHandling::Native.apply("the gostak distims the doshes"),
file_content
);
assert!(symlink_path.is_symlink());
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_symlink_write_fail() -> anyhow::Result<()> {
#[cfg(unix)]
use std::os::unix::fs::symlink;
#[cfg(not(unix))]
use std::os::windows::fs::symlink_file as symlink;
let dir = tempfile::tempdir()?;
let file = helpers::new_readonly_tempfile_in_dir(&dir)?;
let symlink_path = dir.path().join("linked");
symlink(file.path(), &symlink_path)?;
let mut app = helpers::AppBuilder::new()
.with_file(&symlink_path, None)
.build()?;
test_key_sequence(
&mut app,
Some("ihello<esc>:wq<ret>"),
Some(&|app| {
let mut docs: Vec<_> = app.editor.documents().collect();
assert_eq!(1, docs.len());
let doc = docs.pop().unwrap();
assert_eq!(Some(&path::normalize(&symlink_path)), doc.path());
assert_eq!(&Severity::Error, app.editor.get_status().unwrap().1);
}),
false,
)
.await?;
assert!(symlink_path.is_symlink());
Ok(())
}
@@ -557,7 +625,7 @@ async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> {
)
.await?;
file.rewind()?;
reload_file(&mut file).unwrap();
let mut new_file_content: Vec<u8> = Vec::new();
file.read_to_end(&mut new_file_content)?;

View File

@@ -1,5 +1,4 @@
use std::{
fs::File,
io::{Read, Write},
mem::replace,
path::PathBuf,
@@ -14,6 +13,46 @@ use helix_view::{current_ref, doc, editor::LspConfig, input::parse_macro, Editor
use tempfile::NamedTempFile;
use tokio_stream::wrappers::UnboundedReceiverStream;
/// Specify how to set up the input text with line feeds
#[derive(Clone, Debug)]
pub enum LineFeedHandling {
/// Replaces all LF chars with the system's appropriate line feed character,
/// and if one doesn't exist already, appends the system's appropriate line
/// ending to the end of a string.
Native,
/// Do not modify the input text in any way. What you give is what you test.
AsIs,
}
impl LineFeedHandling {
/// Apply the line feed handling to the input string, yielding a set of
/// resulting texts with the appropriate line feed substitutions.
pub fn apply(&self, text: &str) -> String {
let line_end = match self {
LineFeedHandling::Native => helix_core::NATIVE_LINE_ENDING,
LineFeedHandling::AsIs => return text.into(),
}
.as_str();
// we can assume that the source files in this code base will always
// be LF, so indoc strings will always insert LF
let mut output = text.replace('\n', line_end);
if !output.ends_with(line_end) {
output.push_str(line_end);
}
output
}
}
impl Default for LineFeedHandling {
fn default() -> Self {
Self::Native
}
}
#[derive(Clone, Debug)]
pub struct TestCase {
pub in_text: String,
@@ -21,6 +60,8 @@ pub struct TestCase {
pub in_keys: String,
pub out_text: String,
pub out_selection: Selection,
pub line_feed_handling: LineFeedHandling,
}
impl<S, R, V> From<(S, R, V)> for TestCase
@@ -30,8 +71,19 @@ where
V: Into<String>,
{
fn from((input, keys, output): (S, R, V)) -> Self {
let (in_text, in_selection) = test::print(&input.into());
let (out_text, out_selection) = test::print(&output.into());
TestCase::from((input, keys, output, LineFeedHandling::default()))
}
}
impl<S, R, V> From<(S, R, V, LineFeedHandling)> for TestCase
where
S: Into<String>,
R: Into<String>,
V: Into<String>,
{
fn from((input, keys, output, line_feed_handling): (S, R, V, LineFeedHandling)) -> Self {
let (in_text, in_selection) = test::print(&line_feed_handling.apply(&input.into()));
let (out_text, out_selection) = test::print(&line_feed_handling.apply(&output.into()));
TestCase {
in_text,
@@ -39,6 +91,7 @@ where
in_keys: keys.into(),
out_text,
out_selection,
line_feed_handling,
}
}
}
@@ -137,9 +190,10 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
should_exit: bool,
) -> anyhow::Result<()> {
let test_case = test_case.into();
let mut app = match app {
Some(app) => app,
None => Application::new(Args::default(), test_config(), test_syntax_conf(None))?,
None => Application::new(Args::default(), test_config(), test_syntax_loader(None))?,
};
let (view, doc) = helix_view::current!(app.editor);
@@ -162,9 +216,9 @@ pub async fn test_key_sequence_with_input_text<T: Into<TestCase>>(
.await
}
/// Generates language configs that merge in overrides, like a user language
/// Generates language config loader that merge in overrides, like a user language
/// config. The argument string must be a raw TOML document.
pub fn test_syntax_conf(overrides: Option<String>) -> helix_core::syntax::Configuration {
pub fn test_syntax_loader(overrides: Option<String>) -> helix_core::syntax::Loader {
let mut lang = helix_loader::config::default_lang_config();
if let Some(overrides) = overrides {
@@ -172,7 +226,7 @@ pub fn test_syntax_conf(overrides: Option<String>) -> helix_core::syntax::Config
lang = helix_loader::merge_toml_values(lang, override_toml, 3);
}
lang.try_into().unwrap()
helix_core::syntax::Loader::new(lang.try_into().unwrap()).unwrap()
}
/// Use this for very simple test cases where there is one input
@@ -240,23 +294,6 @@ pub fn test_editor_config() -> helix_view::editor::Config {
}
}
/// Replaces all LF chars with the system's appropriate line feed
/// character, and if one doesn't exist already, appends the system's
/// appropriate line ending to the end of a string.
pub fn platform_line(input: &str) -> String {
let line_end = helix_core::NATIVE_LINE_ENDING.as_str();
// we can assume that the source files in this code base will always
// be LF, so indoc strings will always insert LF
let mut output = input.replace('\n', line_end);
if !output.ends_with(line_end) {
output.push_str(line_end);
}
output
}
/// Creates a new temporary file that is set to read only. Useful for
/// testing write failures.
pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
@@ -268,10 +305,22 @@ pub fn new_readonly_tempfile() -> anyhow::Result<NamedTempFile> {
Ok(file)
}
/// Creates a new temporary file in the directory that is set to read only. Useful for
/// testing write failures.
pub fn new_readonly_tempfile_in_dir(
dir: impl AsRef<std::path::Path>,
) -> anyhow::Result<NamedTempFile> {
let mut file = tempfile::NamedTempFile::new_in(dir)?;
let metadata = file.as_file().metadata()?;
let mut perms = metadata.permissions();
perms.set_readonly(true);
file.as_file_mut().set_permissions(perms)?;
Ok(file)
}
pub struct AppBuilder {
args: Args,
config: Config,
syn_conf: helix_core::syntax::Configuration,
syn_loader: helix_core::syntax::Loader,
input: Option<(String, Selection)>,
}
@@ -280,7 +329,7 @@ impl Default for AppBuilder {
Self {
args: Args::default(),
config: test_config(),
syn_conf: test_syntax_conf(None),
syn_loader: test_syntax_loader(None),
input: None,
}
}
@@ -314,8 +363,8 @@ impl AppBuilder {
self
}
pub fn with_lang_config(mut self, syn_conf: helix_core::syntax::Configuration) -> Self {
self.syn_conf = syn_conf;
pub fn with_lang_loader(mut self, syn_loader: helix_core::syntax::Loader) -> Self {
self.syn_loader = syn_loader;
self
}
@@ -328,7 +377,7 @@ impl AppBuilder {
bail!("Having the directory {path:?} in args.files[0] is not yet supported for integration tests");
}
let mut app = Application::new(self.args, self.config, self.syn_conf)?;
let mut app = Application::new(self.args, self.config, self.syn_loader)?;
if let Some((text, selection)) = self.input {
let (view, doc) = helix_view::current!(app.editor);
@@ -352,9 +401,8 @@ pub async fn run_event_loop_until_idle(app: &mut Application) {
app.event_loop_until_idle(&mut rx_stream).await;
}
pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result<()> {
file.flush()?;
file.sync_all()?;
pub fn assert_file_has_content(file: &mut NamedTempFile, content: &str) -> anyhow::Result<()> {
reload_file(file)?;
let mut file_content = String::new();
file.read_to_string(&mut file_content)?;
@@ -368,3 +416,13 @@ pub fn assert_status_not_error(editor: &Editor) {
assert_ne!(&Severity::Error, sev);
}
}
pub fn reload_file(file: &mut NamedTempFile) -> anyhow::Result<()> {
let path = file.path();
let f = std::fs::OpenOptions::new()
.write(true)
.read(true)
.open(&path)?;
*file.as_file_mut() = f;
Ok(())
}

View File

@@ -6,30 +6,30 @@ async fn auto_indent() -> anyhow::Result<()> {
let enter_tests = [
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
type Test struct {#[}|]#
"##}),
"##},
"i<ret>",
helpers::platform_line(indoc! {"\
indoc! {"\
type Test struct {
\t#[|\n]#
}
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
func main() {
\tswitch nil {#[}|]#
}
"}),
"},
"i<ret>",
helpers::platform_line(indoc! {"\
indoc! {"\
func main() {
\tswitch nil {
\t\t#[|\n]#
\t}
}
"}),
"},
),
];

View File

@@ -6,7 +6,7 @@ async fn auto_indent() -> anyhow::Result<()> {
let below_tests = [
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
#[t|]#op:
baz: foo
bazi:
@@ -17,9 +17,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
#[\n|]#
baz: foo
@@ -31,10 +31,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
top:
b#[a|]#z: foo
bazi:
@@ -45,9 +45,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
#[\n|]#
@@ -59,10 +59,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
top:
baz: foo
bazi#[:|]#
@@ -73,9 +73,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -87,10 +87,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
top:
baz: foo
bazi:
@@ -101,9 +101,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -115,10 +115,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
top:
baz: foo
bazi:
@@ -129,9 +129,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -143,10 +143,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -157,9 +157,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -171,10 +171,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -185,9 +185,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -199,10 +199,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -213,9 +213,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:#[\n|]#
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -227,10 +227,10 @@ async fn auto_indent() -> anyhow::Result<()> {
bax: foox
fook:
#[\n|]#
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: |
@@ -239,9 +239,9 @@ async fn auto_indent() -> anyhow::Result<()> {
line
string#[\n|]#
fook:
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: |
@@ -251,10 +251,10 @@ async fn auto_indent() -> anyhow::Result<()> {
string
#[\n|]#
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: >
@@ -263,9 +263,9 @@ async fn auto_indent() -> anyhow::Result<()> {
line#[\n|]#
string
fook:
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: >
@@ -275,74 +275,74 @@ async fn auto_indent() -> anyhow::Result<()> {
#[\n|]#
string
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: >#[\n|]#
fook:
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: >
#[\n|]#
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
- top:#[\n|]#
baz: foo
bax: foox
fook:
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
#[\n|]#
baz: foo
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
baz: foo#[\n|]#
bax: foox
fook:
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
baz: foo
#[\n|]#
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
baz: foo
bax: foox#[\n|]#
fook:
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
baz: foo
bax: foox
#[\n|]#
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz:
- one: two#[\n|]#
@@ -350,9 +350,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- top:
baz: foo
bax: foox
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz:
- one: two
@@ -361,42 +361,42 @@ async fn auto_indent() -> anyhow::Result<()> {
- top:
baz: foo
bax: foox
"}),
"},
),
// yaml map without a key
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:#[\n|]#
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
#[\n|]#
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top#[:|]#
bottom: withvalue
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
#[\n|]#
bottom: withvalue
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
bottom: withvalue
top#[:|]#
"}),
"},
"o",
helpers::platform_line(indoc! {"\
indoc! {"\
bottom: withvalue
top:
#[\n|]#
"}),
"},
),
];
@@ -406,7 +406,7 @@ async fn auto_indent() -> anyhow::Result<()> {
let above_tests = [
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
#[t|]#op:
baz: foo
bazi:
@@ -417,9 +417,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
#[\n|]#
top:
baz: foo
@@ -431,10 +431,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
top:
b#[a|]#z: foo
bazi:
@@ -445,9 +445,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
#[\n|]#
baz: foo
@@ -459,10 +459,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
top:
baz: foo
bazi#[:|]#
@@ -473,9 +473,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
#[\n|]#
@@ -487,10 +487,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
top:
baz: foo
bazi:
@@ -501,9 +501,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -515,10 +515,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
top:
baz: foo
bazi:
@@ -529,9 +529,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"##}),
"##},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -543,10 +543,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -557,9 +557,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -571,10 +571,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -585,9 +585,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -599,10 +599,10 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -613,9 +613,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- 2
bax: foox
fook:#[\n|]#
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bazi:
@@ -627,10 +627,10 @@ async fn auto_indent() -> anyhow::Result<()> {
bax: foox
#[\n|]#
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: |
@@ -639,9 +639,9 @@ async fn auto_indent() -> anyhow::Result<()> {
line
string#[\n|]#
fook:
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: |
@@ -651,10 +651,10 @@ async fn auto_indent() -> anyhow::Result<()> {
#[\n|]#
string
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: >
@@ -663,9 +663,9 @@ async fn auto_indent() -> anyhow::Result<()> {
line
string
fook:
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: >
@@ -675,58 +675,58 @@ async fn auto_indent() -> anyhow::Result<()> {
line
string
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: >
fook:#[\n|]#
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz: foo
bax: >
#[\n|]#
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
baz: foo#[\n|]#
bax: foox
fook:
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
#[\n|]#
baz: foo
bax: foox
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
baz: foo
bax: foox
fook:#[\n|]#
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
- top:
baz: foo
bax: foox
#[\n|]#
fook:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz:
- one: two#[\n|]#
@@ -734,9 +734,9 @@ async fn auto_indent() -> anyhow::Result<()> {
- top:
baz: foo
bax: foox
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
baz:
#[\n|]#
@@ -745,42 +745,42 @@ async fn auto_indent() -> anyhow::Result<()> {
- top:
baz: foo
bax: foox
"}),
"},
),
// yaml map without a key
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:#[\n|]#
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
#[\n|]#
top:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
bottom: withvalue
top#[:|]#
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
bottom: withvalue
#[\n|]#
top:
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
top:
bottom:#[ |]#withvalue
"}),
"},
"O",
helpers::platform_line(indoc! {"\
indoc! {"\
top:
#[\n|]#
bottom: withvalue
"}),
"},
),
];
@@ -790,24 +790,24 @@ async fn auto_indent() -> anyhow::Result<()> {
let enter_tests = [
(
helpers::platform_line(indoc! {r##"
indoc! {r##"
foo: #[b|]#ar
"##}),
"##},
"i<ret>",
helpers::platform_line(indoc! {"\
indoc! {"\
foo:
#[|b]#ar
"}),
"},
),
(
helpers::platform_line(indoc! {"\
indoc! {"\
foo:#[\n|]#
"}),
"},
"i<ret>",
helpers::platform_line(indoc! {"\
indoc! {"\
foo:
#[|\n]#
"}),
"},
),
];

View File

@@ -8,6 +8,7 @@ async fn insert_mode_cursor_position() -> anyhow::Result<()> {
in_keys: "i".into(),
out_text: String::new(),
out_selection: Selection::single(0, 0),
line_feed_handling: LineFeedHandling::AsIs,
})
.await?;
@@ -392,20 +393,10 @@ async fn cursor_position_newly_opened_file() -> anyhow::Result<()> {
#[tokio::test(flavor = "multi_thread")]
async fn cursor_position_append_eof() -> anyhow::Result<()> {
// Selection is forwards
test((
"#[foo|]#",
"abar<esc>",
helpers::platform_line("#[foobar|]#\n"),
))
.await?;
test(("#[foo|]#", "abar<esc>", "#[foobar|]#\n")).await?;
// Selection is backwards
test((
"#[|foo]#",
"abar<esc>",
helpers::platform_line("#[foobar|]#\n"),
))
.await?;
test(("#[|foo]#", "abar<esc>", "#[foobar|]#\n")).await?;
Ok(())
}
@@ -415,19 +406,19 @@ async fn select_mode_tree_sitter_next_function_is_union_of_objects() -> anyhow::
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(indoc! {"\
indoc! {"\
#[/|]#// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
"}),
"},
"]fv]f",
helpers::platform_line(indoc! {"\
indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }|]#
"}),
"},
),
)
.await?;
@@ -440,19 +431,19 @@ async fn select_mode_tree_sitter_prev_function_unselects_object() -> anyhow::Res
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(indoc! {"\
indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }|]#
"}),
"},
"v[f",
helpers::platform_line(indoc! {"\
indoc! {"\
/// Increments
#[fn inc(x: usize) -> usize { x + 1 }|]#
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
"}),
"},
),
)
.await?;
@@ -466,23 +457,23 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(indoc! {"\
indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
#[fn ident(x: usize) -> usize { x }|]#
"}),
"},
"v[f",
helpers::platform_line(indoc! {"\
indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
#[|fn dec(x: usize) -> usize { x - 1 }
/// Identity
]#fn ident(x: usize) -> usize { x }
"}),
"},
),
)
.await?;
@@ -490,23 +481,23 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
test_with_config(
AppBuilder::new().with_file("foo.rs", None),
(
helpers::platform_line(indoc! {"\
indoc! {"\
/// Increments
fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
#[fn ident(x: usize) -> usize { x }|]#
"}),
"},
"v[f[f",
helpers::platform_line(indoc! {"\
indoc! {"\
/// Increments
#[|fn inc(x: usize) -> usize { x + 1 }
/// Decrements
fn dec(x: usize) -> usize { x - 1 }
/// Identity
]#fn ident(x: usize) -> usize { x }
"}),
"},
),
)
.await?;
@@ -517,38 +508,178 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any
#[tokio::test(flavor = "multi_thread")]
async fn find_char_line_ending() -> anyhow::Result<()> {
test((
helpers::platform_line(indoc! {
indoc! {
"\
one
#[|t]#wo
three"
}),
},
"T<ret>gll2f<ret>",
helpers::platform_line(indoc! {
indoc! {
"\
one
two#[
|]#three"
}),
},
))
.await?;
test((
helpers::platform_line(indoc! {
indoc! {
"\
#[|o]#ne
two
three"
}),
},
"f<ret>2t<ret>ghT<ret>F<ret>",
helpers::platform_line(indoc! {
indoc! {
"\
one#[|
t]#wo
three"
}),
},
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_surround_replace() -> anyhow::Result<()> {
test((
indoc! {"\
(#[|a]#)
"},
"mrm{",
indoc! {"\
{#[|a]#}
"},
))
.await?;
test((
indoc! {"\
(#[a|]#)
"},
"mrm{",
indoc! {"\
{#[a|]#}
"},
))
.await?;
test((
indoc! {"\
{{
#(}|)#
#[}|]#
"},
"mrm)",
indoc! {"\
((
#()|)#
#[)|]#
"},
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn test_surround_delete() -> anyhow::Result<()> {
test((
indoc! {"\
(#[|a]#)
"},
"mdm",
indoc! {"\
#[|a]#
"},
))
.await?;
test((
indoc! {"\
(#[a|]#)
"},
"mdm",
indoc! {"\
#[a|]#
"},
))
.await?;
test((
indoc! {"\
{{
#(}|)#
#[}|]#
"},
"mdm",
"\n\n#(\n|)##[\n|]#",
))
.await?;
Ok(())
}
#[tokio::test(flavor = "multi_thread")]
async fn tree_sitter_motions_work_across_injections() -> anyhow::Result<()> {
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-o>",
"<script>let #[|x = 1]#;</script>",
),
)
.await?;
// When the full injected layer is selected, expand_selection jumps to
// a more shallow layer.
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>#[|let x = 1;]#</script>",
"<A-o>",
"#[|<script>let x = 1;</script>]#",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x = 1]#;</script>",
"<A-i>",
"<script>let #[|x]# = 1;</script>",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-n>",
"<script>let x #[=|]# 1;</script>",
),
)
.await?;
test_with_config(
AppBuilder::new().with_file("foo.html", None),
(
"<script>let #[|x]# = 1;</script>",
"<A-p>",
"<script>#[|let]# x = 1;</script>",
),
)
.await?;
Ok(())
}

View File

@@ -62,9 +62,9 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> {
)
.await?;
helpers::assert_file_has_content(file1.as_file_mut(), &platform_line("hello1"))?;
helpers::assert_file_has_content(file2.as_file_mut(), &platform_line("hello2"))?;
helpers::assert_file_has_content(file3.as_file_mut(), &platform_line("hello3"))?;
helpers::assert_file_has_content(&mut file1, &LineFeedHandling::Native.apply("hello1"))?;
helpers::assert_file_has_content(&mut file2, &LineFeedHandling::Native.apply("hello2"))?;
helpers::assert_file_has_content(&mut file3, &LineFeedHandling::Native.apply("hello3"))?;
Ok(())
}
@@ -91,7 +91,7 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
let doc = docs.pop().unwrap();
assert_eq!(
helpers::platform_line("hello\ngoodbye"),
LineFeedHandling::Native.apply("hello\ngoodbye"),
doc.text().to_string()
);
@@ -110,7 +110,7 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
let doc = docs.pop().unwrap();
assert_eq!(
helpers::platform_line("hello\ngoodbye"),
LineFeedHandling::Native.apply("hello\ngoodbye"),
doc.text().to_string()
);
@@ -122,10 +122,7 @@ async fn test_split_write_quit_same_file() -> anyhow::Result<()> {
)
.await?;
helpers::assert_file_has_content(
file.as_file_mut(),
&helpers::platform_line("hello\ngoodbye"),
)?;
helpers::assert_file_has_content(&mut file, &LineFeedHandling::Native.apply("hello\ngoodbye"))?;
Ok(())
}
@@ -151,7 +148,13 @@ async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> {
//
// This panicked in the past because the jumplist entry on line 2 of window 2
// was not updated and after the `kd` step, pointed outside of the document.
test(("#[|]#", "<C-w>v[<space><C-s><C-w>wkd<C-w>qd", "#[|]#")).await?;
test((
"#[|]#",
"<C-w>v[<space><C-s><C-w>wkd<C-w>qd",
"#[|]#",
LineFeedHandling::AsIs,
))
.await?;
// Transactions are applied to the views for windows lazily when they are focused.
// This case panics if the transactions and inversions are not applied in the
@@ -160,6 +163,7 @@ async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> {
"#[|]#",
"[<space>[<space>[<space><C-w>vuuu<C-w>wUUU<C-w>quuu",
"#[|]#",
LineFeedHandling::AsIs,
))
.await?;
@@ -185,6 +189,7 @@ async fn test_changes_in_splits_apply_to_all_views() -> anyhow::Result<()> {
"#[|]#",
"3[<space><C-w>v<C-s><C-w>wuu3[<space><C-w>q%d",
"#[|]#",
LineFeedHandling::AsIs,
))
.await?;

View File

@@ -18,9 +18,9 @@ default = ["crossterm"]
helix-view = { path = "../helix-view", features = ["term"] }
helix-core = { path = "../helix-core" }
bitflags = "2.4"
bitflags = "2.5"
cassowary = "0.3"
unicode-segmentation = "1.10"
unicode-segmentation = "1.11"
crossterm = { version = "0.27", optional = true }
termini = "1.0"
serde = { version = "1", "optional" = true, features = ["derive"]}

View File

@@ -52,10 +52,14 @@ impl Default for Capabilities {
impl Capabilities {
/// Detect capabilities from the terminfo database located based
/// on the $TERM environment variable. If detection fails, returns
/// a default value where no capability is supported.
/// a default value where no capability is supported, or just undercurl
/// if config.undercurl is set.
pub fn from_env_or_default(config: &EditorConfig) -> Self {
match termini::TermInfo::from_env() {
Err(_) => Capabilities::default(),
Err(_) => Capabilities {
has_extended_underlines: config.undercurl,
..Capabilities::default()
},
Ok(t) => Capabilities {
// Smulx, VTE: https://unix.stackexchange.com/a/696253/246284
// Su (used by kitty): https://sw.kovidgoyal.net/kitty/underlines
@@ -87,6 +91,10 @@ where
W: Write,
{
pub fn new(buffer: W, config: &EditorConfig) -> CrosstermBackend<W> {
// helix is not usable without colors, but crossterm will disable
// them by default if NO_COLOR is set in the environment. Override
// this behaviour.
crossterm::style::force_color_output(true);
CrosstermBackend {
buffer,
capabilities: Capabilities::from_env_or_default(config),

View File

@@ -28,15 +28,15 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use helix_tui::layout::{Alignment};
/// # use helix_view::graphics::{Style, Color, Modifier};
/// let text = vec![
/// let text = Text::from(vec![
/// Spans::from(vec![
/// Span::raw("First"),
/// Span::styled("line",Style::default().add_modifier(Modifier::ITALIC)),
/// Span::raw("."),
/// ]),
/// Spans::from(Span::styled("Second line", Style::default().fg(Color::Red))),
/// ];
/// Paragraph::new(text)
/// ]);
/// Paragraph::new(&text)
/// .block(Block::default().title("Paragraph").borders(Borders::ALL))
/// .style(Style::default().fg(Color::White).bg(Color::Black))
/// .alignment(Alignment::Center)
@@ -51,7 +51,7 @@ pub struct Paragraph<'a> {
/// How to wrap the text
wrap: Option<Wrap>,
/// The text to display
text: Text<'a>,
text: &'a Text<'a>,
/// Scroll
scroll: (u16, u16),
/// Alignment of the text
@@ -70,7 +70,7 @@ pub struct Paragraph<'a> {
/// - Here is another point that is long enough to wrap"#);
///
/// // With leading spaces trimmed (window width of 30 chars):
/// Paragraph::new(bullet_points.clone()).wrap(Wrap { trim: true });
/// Paragraph::new(&bullet_points).wrap(Wrap { trim: true });
/// // Some indented points:
/// // - First thing goes here and is
/// // long so that it wraps
@@ -78,7 +78,7 @@ pub struct Paragraph<'a> {
/// // is long enough to wrap
///
/// // But without trimming, indentation is preserved:
/// Paragraph::new(bullet_points).wrap(Wrap { trim: false });
/// Paragraph::new(&bullet_points).wrap(Wrap { trim: false });
/// // Some indented points:
/// // - First thing goes here
/// // and is long so that it wraps
@@ -92,15 +92,12 @@ pub struct Wrap {
}
impl<'a> Paragraph<'a> {
pub fn new<T>(text: T) -> Paragraph<'a>
where
T: Into<Text<'a>>,
{
pub fn new(text: &'a Text) -> Paragraph<'a> {
Paragraph {
block: None,
style: Default::default(),
wrap: None,
text: text.into(),
text,
scroll: (0, 0),
alignment: Alignment::Left,
}

View File

@@ -4,6 +4,7 @@ use helix_core::unicode::width::UnicodeWidthStr;
use unicode_segmentation::UnicodeSegmentation;
const NBSP: &str = "\u{00a0}";
const NNBSP: &str = "\u{202f}";
/// A state machine to pack styled symbols into lines.
/// Cannot implement it as Iterator since it yields slices of the internal buffer (need streaming
@@ -58,7 +59,8 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
let mut symbols_exhausted = true;
for StyledGrapheme { symbol, style } in &mut self.symbols {
symbols_exhausted = false;
let symbol_whitespace = symbol.chars().all(&char::is_whitespace) && symbol != NBSP;
let symbol_whitespace =
symbol.chars().all(&char::is_whitespace) && symbol != NBSP && symbol != NNBSP;
// Ignore characters wider that the total max width.
if symbol.width() as u16 > self.max_line_width
@@ -496,6 +498,20 @@ mod test {
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
}
#[test]
fn line_composer_word_wrapper_nnbsp() {
let width = 20;
let text = "AAAAAAAAAAAAAAA AAAA\u{202f}AAA";
let (word_wrapper, _) = run_composer(Composer::WordWrapper { trim: true }, text, width);
assert_eq!(word_wrapper, vec!["AAAAAAAAAAAAAAA", "AAAA\u{202f}AAA",]);
// Ensure that if the character was a regular space, it would be wrapped differently.
let text_space = text.replace('\u{202f}', " ");
let (word_wrapper_space, _) =
run_composer(Composer::WordWrapper { trim: true }, &text_space, width);
assert_eq!(word_wrapper_space, vec!["AAAAAAAAAAAAAAA AAAA", "AAA",]);
}
#[test]
fn line_composer_word_wrapper_preserve_indentation() {
let width = 20;

View File

@@ -17,14 +17,16 @@ fn terminal_buffer_size_should_not_be_limited() {
// let backend = TestBackend::new(10, 10);
// let mut terminal = Terminal::new(backend)?;
// let frame = terminal.draw(|f| {
// let paragraph = Paragraph::new("Test");
// let text = Text::from("Test");
// let paragraph = Paragraph::new(&text);
// f.render_widget(paragraph, f.size());
// })?;
// assert_eq!(frame.buffer.get(0, 0).symbol, "T");
// assert_eq!(frame.area, Rect::new(0, 0, 10, 10));
// terminal.backend_mut().resize(8, 8);
// let frame = terminal.draw(|f| {
// let paragraph = Paragraph::new("test");
// let text = Text::from("test");
// let paragraph = Paragraph::new(&text);
// f.render_widget(paragraph, f.size());
// })?;
// assert_eq!(frame.buffer.get(0, 0).symbol, "t");

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