Compare commits

...

404 Commits

Author SHA1 Message Date
Blaž Hrastnik
44566ea812 Release 0.3.0 2021-06-27 13:27:47 +09:00
Blaž Hrastnik
cad14c6b46 Address nightly clippy warnings 2021-06-27 13:27:47 +09:00
Ivan Tham
ed1a745442 Trait width method use refactor 2021-06-26 08:52:22 -07:00
Nathan Vegdahl
a6cadddef4 Fix silly mistake in previous phantom line bug fix.
Fixes #381.  I was trying to change an index value that... wasn't
even an index value.
2021-06-26 14:26:18 +09:00
Nathan Vegdahl
2dba228c76 Fix highlight code splitting graphemes.
This resulted in phantom blank lines in files with CRLF line
endings, but could potentially have manifested with other
graphemes as well.
2021-06-26 11:09:50 +09:00
Wojciech Kępka
eb6fb63e74 Sort files in file picker by access, modification and creation date (#336)
* Sort files in file picker by access date

* Fallback file time to modified then created then UNIX_EPOCH

* Use `sort_by_key`

* Refactor
2021-06-26 11:09:17 +09:00
Nathan Vegdahl
d534d6470f Detect file language before file indent style.
Fixes #378.  The issue was that because indent style detection
ran before language detection, there was no language indent
style to fall back on if indent style detection failed, so it
would just default to 2 spaces.
2021-06-25 20:27:43 -04:00
Gokul Soumya
e8d2f3612f Use unicode_width to correctly truncate picker chars 2021-06-25 16:09:05 -07:00
teenjuna
c688288881 Move helix-view/tests/*txt files to txts subdirectory (#372)
* Move helix-view/tests/*txt files to txts subdirectory

* Rename tests/txts to tests/encoding
2021-06-25 15:59:06 +09:00
Blaž Hrastnik
f2d8ce3415 Use a deadline when eagerly processing notifications 2021-06-25 13:22:50 +09:00
Blaž Hrastnik
503ca112ae fix: jumping to location did not convert the URI correctly
thus breaking Windows
2021-06-25 13:20:15 +09:00
Blaž Hrastnik
8e277ad8ba fix: crossterm -> input key conversion 2021-06-25 13:13:15 +09:00
Keith Simmons
4418e17547 reverse the dependency between helix-tui and helix-view (#366)
* reverse the dependency between helix-tui and helix-view by moving a fiew types to view

* fix tests

* clippy and format fixes

Co-authored-by: Keith Simmons <keithsim@microsoft.com>
2021-06-25 12:58:15 +09:00
Ivan Tham
74cc4b4a49 Add default color for cursor match (#370)
* Add default color for cursor match

Not all terminals support dim, for those terminal that does not support
this (konsole, item2, wezterm), users cannot differentiate between match
and primary cursor. So set a color for this.

* Use alacritty dim color for match
2021-06-25 11:18:38 +09:00
Ivan Tham
c2b937481f Fix goto line end
Should not goto newline.
2021-06-24 18:34:23 -07:00
Gokul Soumya
18beda38ac Add … when chars are truncated in picker 2021-06-25 09:28:24 +09:00
Ivan Tham
10548bf0e3 Fix previous broken refactor key into helix-view
Need to be used for autoinfo

Revert "Revert "Refactor key into helix-view""

This reverts commit 10f9f72232.
2021-06-25 00:39:03 +09:00
Kirawi
15ae2e7ef1 Update helix-term/src/commands.rs
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-24 22:38:38 +09:00
Kirawi
7ae21b98ce Update helix-term/src/commands.rs
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-24 22:38:38 +09:00
Shafkath Shuhan
629df6124d Blocking :wq 2021-06-24 22:38:38 +09:00
Nathan Vegdahl
8935e7a879 Fix open-new-line command for CRLF, as well as other bugs.
Fixes #363.

I set out to fix issue #363, but after fixing it discovered some
other things were wrong with the command while testing.  In
summary:
- #363 was because it was still assuming a line ending width
  of 1 char in its indexing calculations, even when actually
  inserting CRLF.
- Aside from #363, it actually needed to set `line_end_index`
  to zero for *all* calculations that use it when line == 0,
  but it was only doing so for a single calculation.
2021-06-24 18:25:56 +09:00
Gokul Soumya
394629ab73 Skip enclosed pairs in surround
Surround operations previously ignored other pairs that are
enclosed within which should be skipped. For example if the
cursor is on the `,` in `{{a},{b}}`, doing `md{` previously
would delete the `{` on the left of `a` and `}` on the right
of `b` instead of the outermost braces. This commit corrects
this behavior.
2021-06-24 13:02:56 +09:00
Gokul Soumya
fb8e7dc25b Fix picker item width overflow
Fixes #352
2021-06-24 12:00:08 +09:00
Alex Ryapolov
2924522aea Remove duplicate properties from theme.toml 2021-06-24 09:23:23 +08:00
Benoît CORTIER
14f61fb6ac Fix lsp config deserialization case
It should have been in kebab-case, but it was the default snake_case.
2021-06-24 10:04:18 +09:00
Nathan Vegdahl
9cbf564d08 Handle erroneously ignored case in RopeGraphemes iterator. 2021-06-24 10:04:03 +09:00
Blaž Hrastnik
7f6265ecf3 fix: crash with ctrl-c on empty file 2021-06-24 01:38:02 +09:00
Blaž Hrastnik
0f55e67576 fix: ok, needs to be the end of the previous line 2021-06-24 01:35:36 +09:00
Blaž Hrastnik
7366fe81e0 open: Use the correct function
Still not correct but at least it doesn't append at EOF
2021-06-24 01:06:17 +09:00
Blaž Hrastnik
4ad7b61c69 fix: Better fix that also fixes crashes on o 2021-06-24 00:58:14 +09:00
Blaž Hrastnik
655c1aeb73 fix: panic on O at the start of the file (fixes #354) 2021-06-24 00:50:52 +09:00
Blaž Hrastnik
ea8cd4765d Adjust default theme colors (insert/extend cursor) 2021-06-23 22:11:46 +09:00
Blaž Hrastnik
39dc09e6c4 ui: Paginate prompt completion 2021-06-23 21:55:13 +09:00
wojciechkepka
3606d8bd24 Patch the primary cursor with insert and select styles 2021-06-23 21:55:02 +09:00
wojciechkepka
c534fdefdc Refactor, add ui.cursor.primary 2021-06-23 21:55:02 +09:00
wojciechkepka
d70be55f70 Add ability to theme primary selecition 2021-06-23 21:55:02 +09:00
wojciechkepka
ac1e98d088 Add ability to theme cursor 2021-06-23 21:55:02 +09:00
wojciechkepka
f09ccbc891 Update docs 2021-06-23 21:55:02 +09:00
wojciechkepka
ed6528b9a6 fix: Docs, select_line -> extend_line 2021-06-23 16:23:15 +09:00
Shafkath Shuhan
6564257a7b add missing doc 2021-06-23 15:40:27 +09:00
nobody
7896eefd73 add tests 2021-06-23 15:40:27 +09:00
Shafkath Shuhan
fd98e743e8 Handle non-UTF8 files 2021-06-23 15:40:27 +09:00
Blaž Hrastnik
9706f1121d Fix small screen panics 2021-06-23 13:13:56 +09:00
Nathan Vegdahl
2ff9b362fb Update to Ropey v1.3.1 with needed bugfix. 2021-06-23 12:43:09 +09:00
Nathan Vegdahl
848cc1b438 Fix extend_line() behavior.
It would always extend to the next line if the cursor was at the
end of the current line, even if the current line wasn't fully
selected yet.
2021-06-23 12:43:09 +09:00
Nathan Vegdahl
481c4ba044 Increment char_index by grapheme char count.
It was just assuming single-char graphemes before.
2021-06-23 12:43:09 +09:00
Nathan Vegdahl
0cbaa998ce Fix flipped condition where Helix adds a line ending on open. 2021-06-23 12:43:09 +09:00
Nathan Vegdahl
38bf9c2576 Missed some items in the CRLF PR. 2021-06-23 12:43:09 +09:00
Blaž Hrastnik
9c53461429 fix: Select matching at the start of the doc could crash. Fixes #346 2021-06-23 12:27:38 +09:00
Blaž Hrastnik
7511110d82 Fix build on master 2021-06-23 10:15:57 +09:00
Joe Neeman
fd1ae35051 Make the prompt callback take a Context. 2021-06-23 10:03:11 +09:00
Lionel Flandrin
16883e7543 Implement show_current_directory command 2021-06-22 19:20:51 -04:00
Lionel Flandrin
b56174d738 Implement change_current_directory command 2021-06-22 19:20:51 -04:00
Blaž Hrastnik
866b32b5d7 Add repology.org packaging status 2021-06-23 01:05:22 +09:00
Blaž Hrastnik
39d59216e4 Fix link to good first issue 2021-06-23 00:51:30 +09:00
Blaž Hrastnik
20f33ead67 minor: Remove old TODOs 2021-06-22 23:26:34 +09:00
Gokul Soumya
e0fd08d6df Rename surround to match_mode 2021-06-22 14:27:51 +09:00
Gokul Soumya
753ed4cbc5 Add documentation for surround 2021-06-22 14:27:51 +09:00
Gokul Soumya
892c80771a Correctly identify pairs when cursor on pair 2021-06-22 14:27:51 +09:00
Gokul Soumya
b00e9fc227 Handle line endings correctly in surround 2021-06-22 14:27:51 +09:00
Gokul Soumya
b79b5e66f2 Move match_bracket to mm 2021-06-22 14:27:51 +09:00
Gokul Soumya
86271bac18 Refactor and add tests for surround 2021-06-22 14:27:51 +09:00
Gokul Soumya
4754b2e5ae Add more surround pair characters 2021-06-22 14:27:51 +09:00
Gokul Soumya
13648d28b9 Add surround keybinds 2021-06-22 14:27:51 +09:00
Blaž Hrastnik
2f321b9335 lsp: Eagerly process notifications/server calls to avoid re-rendering 2021-06-22 13:47:57 +09:00
Yusuf Bera Ertan
6dddd5cd1d build(nix): fetch submodules lazily 2021-06-22 13:29:13 +09:00
Blaž Hrastnik
a70de6e980 Merge pull request #224 from helix-editor/line_ending_detection
Line ending detection
2021-06-22 11:09:19 +09:00
dependabot[bot]
c704970fd7 Bump tokio from 1.6.1 to 1.7.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.6.1 to 1.7.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.6.1...tokio-1.7.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-22 09:26:14 +09:00
dependabot[bot]
05bf9edebd Bump actions/upload-artifact from 2.2.3 to 2.2.4
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 2.2.3 to 2.2.4.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v2.2.3...v2.2.4)

---
updated-dependencies:
- dependency-name: actions/upload-artifact
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-22 09:25:29 +09:00
Nathan Vegdahl
f2954fa153 Flesh out the line ending utility unit tests. 2021-06-21 12:56:42 -07:00
Nathan Vegdahl
a18d50b777 Add command to set the document's default line ending. 2021-06-21 12:36:01 -07:00
Nathan Vegdahl
7c4fa18764 Fix clippy warnings. 2021-06-21 12:02:44 -07:00
Nathan Vegdahl
d33355650f Convert remaining commands to use the document's line ending setting. 2021-06-21 11:59:03 -07:00
Nathan Vegdahl
e436c30ed7 Make split_selection_on_newline command handle all line endings. 2021-06-21 11:22:07 -07:00
Nathan Vegdahl
23d6188535 Update replace command to use document line ending setting. 2021-06-21 11:08:05 -07:00
Nathan Vegdahl
07e28802f6 Add function to get the line ending of a str slice.
This is needed in some places.
2021-06-21 10:29:29 -07:00
Nathan Vegdahl
714002048c Don't need getters/setters for line_ending property.
It's plain-old-data.  If we want to do fancier things later, it's
easy to switch back.
2021-06-21 09:52:21 -07:00
Ivan Tham
9fd17d4ff5 Use pep 8 indentation for python
Change the default spaces of python indentation to follow pep8 which is the standard.
2021-06-21 08:50:51 -07:00
Blaž Hrastnik
994ff4b269 Don't run wl-copy with --foreground
It stalls the hx process
2021-06-21 19:06:57 +09:00
Wojciech Kępka
ee80fa8ea9 Cleanup spinners and messages on progress end 2021-06-21 18:51:04 +09:00
wojciechkepka
aca9d73fe4 Hold Config in Application, expect at least one editor view 2021-06-21 12:59:06 +09:00
wojciechkepka
cc357d5096 Add progress spinners to status line 2021-06-21 12:59:06 +09:00
wojciechkepka
b2804b14b1 Add a Spinner 2021-06-21 12:59:06 +09:00
wojciechkepka
618ad55dc1 Update docs 2021-06-21 12:59:06 +09:00
wojciechkepka
d39a764399 Fix typo in feature request template 2021-06-21 10:54:18 +09:00
Nathan Vegdahl
3d3149e0d5 Silence clippy warning. 2021-06-20 16:13:59 -07:00
Nathan Vegdahl
e686c3e462 Merge branch 'master' of github.com:helix-editor/helix into line_ending_detection
Rebasing was making me manually fix conflicts on every commit, so
merging instead.
2021-06-20 16:09:14 -07:00
Nathan Vegdahl
4efd6713c5 Work on moving code over to LineEnding instead of assuming '\n'.
Also some general cleanup and some minor fixes along the way.
2021-06-20 15:33:02 -07:00
Ivan Tham
985625763a Fix doc warnings 2021-06-20 23:13:36 +08:00
Blaž Hrastnik
eaf259f8aa Fix build.. 2021-06-20 23:40:48 +09:00
Blaž Hrastnik
f41688d960 Merge x and X 2021-06-20 23:37:11 +09:00
Benoît CORTIER
ffb54b4eac book: document new system clipboard mappings 2021-06-20 23:25:53 +09:00
Benoît CORTIER
f50261c944 Add mappable commands for system clipboard
System clipboard integration exists now in two favors: typable and
mappable.

Default mappings are:
- SPC p: paste clipboard after
- SPC P: paste clipboard before
- SPC y: join and yank selection to clipboard
- SPC Y: yank main selection to clipboard
- SPC R: replace selections by clipboard contents
2021-06-20 23:25:53 +09:00
Benoît CORTIER
a2b8cfca34 Add system clipboard yank and paste commands
This commit adds six new commands to interact with system clipboard:
- clipboard-yank
- clipboard-yank-join
- clipboard-paste-after
- clipboard-paste-before
- clipboard-paste-replace
- show-clipboard-provider

System clipboard provider is detected by checking a few environment
variables and executables. Currently only built-in detection is
supported.

`clipboard-yank` will only yank the "main" selection, which is currently the first
one. This will need to be revisited later.

Closes https://github.com/helix-editor/helix/issues/76
2021-06-20 23:25:53 +09:00
wojciechkepka
d59c9f3baf Add a blank issue template 2021-06-20 22:32:55 +09:00
wojciechkepka
82018af609 Add a template for a feature request 2021-06-20 22:32:55 +09:00
wojciechkepka
fc39a6c40d Add comment, statusline + commandline = 2 2021-06-20 19:59:26 +09:00
wojciechkepka
0882712b45 Use full screen size 2021-06-20 19:59:26 +09:00
wojciechkepka
980e602352 Make completion window move to top when cursor is below half 2021-06-20 19:59:26 +09:00
Nathan Vegdahl
5d22e3c4e5 Misc fixes and clean up of line ending detect code. 2021-06-20 00:40:41 -07:00
Blaž Hrastnik
34ebe82654 ui: prompt: Add more keymappings 2021-06-20 16:38:58 +09:00
Blaž Hrastnik
e9a3245aae Re-export unicode crates from helix_core 2021-06-20 16:38:58 +09:00
Blaž Hrastnik
9275021497 ui: prompt: Better unicode support
We copied over eval_movement from wezterm, that already solves most of
our problems. self.cursor is now byte-based.
2021-06-20 16:38:58 +09:00
wojciechkepka
59c59deb46 Add missing theme to toml config 2021-06-20 13:05:08 +09:00
Gokul Soumya
29f77b9c5f Fix docx formatting and links 2021-06-20 13:04:30 +09:00
Blaž Hrastnik
4b7276ddd6 ci: Test with --release on releases 2021-06-20 10:48:42 +09:00
Blaž Hrastnik
4f108ab1b2 Fix tests failing on cargo test --release 2021-06-20 10:44:00 +09:00
Jan Hrastnik
8634e04a31 added the line_end helper function 2021-06-20 02:22:10 +02:00
Jan Hrastnik
701eb0dd68 changed some hardcoded newlines, removed a else if in line_ending.rs 2021-06-20 01:24:36 +02:00
wojciechkepka
2d629a880c Fix overflow 2021-06-19 16:49:20 -04:00
Blaž Hrastnik
28d9673a8e Fix compilation 2021-06-20 00:19:48 +09:00
wojciechkepka
6825e19509 Only reconfiure highlights when setting theme 2021-06-20 00:07:13 +09:00
wojciechkepka
42e13bd542 Add :theme <name> command 2021-06-20 00:07:13 +09:00
wojciechkepka
b1a41c4cc8 Add theme to global configuration 2021-06-20 00:07:13 +09:00
wojciechkepka
a2db161d5a Add theme completer 2021-06-20 00:07:13 +09:00
wojciechkepka
ce97a2f05f Add ability to change theme on editor 2021-06-20 00:07:13 +09:00
wojciechkepka
f424a61054 Add themes loader 2021-06-20 00:07:13 +09:00
wojciechkepka
3b534e17f4 Move themes to runtime/themes, add link from contrib/themes 2021-06-20 00:07:13 +09:00
wojciechkepka
cd0ecded1f Update docs 2021-06-20 00:07:13 +09:00
Blaž Hrastnik
10f9f72232 Revert "Refactor key into helix-view"
Did not use defaults when custom keymap was used

This reverts commit ca806d4f85.
2021-06-19 23:59:19 +09:00
wojciechkepka
11f20af25f Make home and end work in insert mode 2021-06-19 23:16:13 +09:00
Jan Hrastnik
1e80fbb602 fix merge issue 2021-06-19 14:58:49 +02:00
Jan Hrastnik
cdd9347457 Merge remote-tracking branch 'origin/master' into line_ending_detection 2021-06-19 14:51:53 +02:00
Jan Hrastnik
97323dc2f9 ran cargo fmt 2021-06-19 14:05:11 +02:00
Jan Hrastnik
ecb884db98 added get_line_ending from pr comment 2021-06-19 14:03:14 +02:00
Malte Voos
2cbec2b047 Update flake.lock
Closes #302
2021-06-19 17:16:33 +09:00
Ivan Tham
ca806d4f85 Refactor key into helix-view
Now also make use of Deserialize for Config.
2021-06-19 16:37:15 +09:00
wojciechkepka
1c25852021 Make arrow keys and page up/down work in insert mode 2021-06-18 21:37:30 -07:00
wojciechkepka
c5a2fd5da3 Add close_language_servers method on Editor 2021-06-19 13:02:56 +09:00
wojciechkepka
dd0af78079 Fix unwraps in lsp::transport 2021-06-19 13:02:56 +09:00
wojciechkepka
c2aad859b1 Handle language server shutdown with timeout 2021-06-19 13:02:56 +09:00
Benoît CORTIER
03d1ca7b0a cargo: add more metadata to manifests 2021-06-19 10:04:59 +09:00
Benoît CORTIER
db5bdf4f2d Run cargo-diet
cargo-diet is a helper for computing the optimal `include` directives
for Cargo.toml manifests.
https://github.com/the-lean-crate/cargo-diet
2021-06-19 10:04:59 +09:00
Benoît CORTIER
b48054f3ee cargo: add version to local dependencies
First step towards enabling us to publish on crates.io.

See: https://github.com/helix-editor/helix/issues/42
2021-06-19 10:04:59 +09:00
wojciechkepka
1c1474c3b8 Add ui.statusline.inactive, use ui.statusline for statusline text 2021-06-18 15:18:58 -04:00
Benoît CORTIER
b0522239e7 Update ropey dependency to 1.3 2021-06-18 22:56:36 +09:00
rypervenche
0151826233 Removed unneeded escaping in Markdown docs (#299) 2021-06-18 09:42:25 -04:00
Wojciech Kępka
1bb3b778ad Don't derive Default for GlobalConfig (#297)
We shouldn't derive Default because `lsp_progress` by default should be turned on (opt out).
2021-06-18 09:41:49 -04:00
Gokul Soumya
b1cb98283d Fix indent regression issue with o, O
Indents were no longer respected with `o` and `O`. Using counts resulted
in multiple cursors in the same line instead of cursors on each line.

Introduced by 47d2e3ae
2021-06-18 21:30:58 +09:00
wojciechkepka
a3cb79ebaa Use kebab-case for config 2021-06-18 17:42:38 +09:00
wojciechkepka
bbefc1db63 Add an option to disable display of progress in status bar 2021-06-18 17:42:38 +09:00
wojciechkepka
d095ec15d4 Reenable work_done_progress capability 2021-06-18 17:42:38 +09:00
wojciechkepka
612511dc98 Handle workDoneProgress/create request 2021-06-18 17:42:38 +09:00
wojciechkepka
e1109a5a01 Update handling of progress notification 2021-06-18 17:42:38 +09:00
wojciechkepka
38cb934d8f Add unique id to each lsp client/server pair 2021-06-18 17:42:38 +09:00
wojciechkepka
80b4a69053 Update client::reply to be non async 2021-06-18 17:42:38 +09:00
wojciechkepka
a6d39585d8 Add work_done_token as parameter to lsp methods 2021-06-18 17:42:38 +09:00
wojciechkepka
52fb90b81e Add MethodCall, ProgressStatus, LspProgressMap 2021-06-18 17:42:38 +09:00
Wojciech Kępka
41b07486ad Fix expansion of ~ (#284)
* Fix expansion of `~`, dont use directory relative to cwd.

* Add `expand_tilde`

* Bring back `canonicalize_path`, use `expand_tilde` to `normalize`

* Make `:open ~` completion work

* Fix clippy

* Fold home dir into tilde in Document `realitve_path`
2021-06-18 15:19:34 +09:00
Benoît CORTIER
42142cf680 Fix panic when entering unicode in command prompt
It was attempted to use `String::insert` and `String::remove` to insert
without taking care of unicodes.

Fixes https://github.com/helix-editor/helix/issues/282
2021-06-18 10:08:32 +09:00
Benoît CORTIER
8664d70e73 Replace Editor::current by a macro
This is necessary to workaround ownership issues across function calls.
The issue notably arised when implementing the registers into `Editor`
and I was getting annoyed again when implementing copy/pasting into
system clipboard.
The problem is addressed by using macro calls instead of function calls.
There is no notable side effect.
2021-06-18 09:38:10 +09:00
Perry Thompson
f65db9397a Fix typos in Markdown documentation 2021-06-17 18:39:29 -04:00
Blaž Hrastnik
14db2cc68b Add homebrew tap instructions again 2021-06-17 23:21:33 +09:00
Jan Hrastnik
8bccd6df30 applied changes from pr review 2021-06-17 13:49:50 +02:00
PabloMansanet
f7e00cf720 Configurable keys 2 (Mapping keys to commands) (#268)
* Add convenience/clarity wrapper for Range initialization

* Add keycode parse and display methods

* Add remapping functions and tests

* Implement key remapping

* Add remapping book entry

* Use raw string literal for toml

* Add command constants

* Make command functions private

* Map directly to commands

* Match key parsing/displaying to Kakoune

* Formatting pass

* Update documentation

* Formatting

* Fix example in the book

* Refactor into single config file

* Formatting

* Refactor configuration and add keymap newtype wrappers

* Address first batch of PR comments

* Replace FromStr with custom deserialize
2021-06-17 20:08:05 +09:00
Gokul Soumya
47d2e3aefa Let o, O take counts for multiple cursors 2021-06-17 18:54:07 +09:00
Gokul Soumya
20d6b202d5 Fix cursor position bugs related to o and O
- `O` at the beginning of file didn't move cursor
- `o` and `O` messed up cursor position with multiple cursors

Fixes #127
2021-06-17 18:54:07 +09:00
Jan Hrastnik
9c3eadb2e4 fixed some problems from rebasing 2021-06-16 17:22:55 +02:00
Jan Hrastnik
7cf0fa05a4 doc.line_ending() now returns &'static str 2021-06-16 17:13:44 +02:00
Jan Hrastnik
a4f5a0134e trying out line ending helper functions in commands.rs 2021-06-16 17:13:41 +02:00
Jan Hrastnik
a9a718c3ca added some tests and a line_ending helper function in document.rs 2021-06-16 17:11:16 +02:00
Jan Hrastnik
e4849f41be fix typo 2021-06-16 17:09:03 +02:00
Jan Hrastnik
9c419fe05c added more changes from pr review for line_ending_detection 2021-06-16 17:08:46 +02:00
Jan Hrastnik
5eb6918392 resolved conflict in rebase 2021-06-16 17:05:14 +02:00
Jan Hrastnik
17f69a03e0 ran cargo clippy and cargo fmt 2021-06-16 17:00:30 +02:00
Jan Hrastnik
3756c21bae rebase on branch line_ending_detection 2021-06-16 17:00:21 +02:00
Ivan Tham
a364d6c383 Add latex 2021-06-16 21:42:05 +09:00
Gokul Soumya
d1c8a74771 Add theme key for selected line number
Adds `ui.linenr.selected` which controls highlight of linu numbes which
have cursors on.

- Fallback to linenr if linenr.selected is missing

- Update docs and themes

- Add TODOs for themes with temporary linenr.selected
2021-06-16 15:00:14 +09:00
Ivan Tham
33a35b7589 Add other cursor shape 2021-06-15 23:46:21 +08:00
Ivan Tham
124514aa70 Add cursor kind to separate hidden cursor from pos
Now IME cursor position should be correct since we can still set cursor
position without drawing the cursor.
2021-06-15 23:46:21 +08:00
Benoît CORTIER
6bdf609caa Remove RwLock for registers
Registers are stored inside `Editor` and accessed without `RwLock`.
To work around ownership, I added a sister method to `Editor::current`:
`Editor::current_with_context`. I tried to modify `Editor::current`
directly but it's used at a lot of places so I reverted into this for
now at least.
2021-06-15 23:01:56 +08:00
Benoît CORTIER
6fb2d2679d Use _impl suffix instead of _ prefix
Helpers / internal implementations where using the `_` prefix.
However, this prefix also suppress unused warnings.
I suggest we use the `_impl` suffix instead.
2021-06-15 02:33:12 -04:00
Gokul Soumya
eb77de6a51 Format docs for better readability
- Wrapped appropriate table elements in inline code blocks
- Added links to different modes
- Capitalised table elements
2021-06-15 00:20:22 -04:00
Ivan Tham
05ed3e8fb8 Remove unused variables 2021-06-15 00:17:04 -04:00
Ivan Tham
002f1ad397 Add filter ability to picker
Inspired by doom emacs. Able to filter picker options multiple times.
2021-06-15 12:00:31 +08:00
Nathan Vegdahl
7c2fb92c91 Report indent style when calling indent-style with no arguments.
Also print an error message when the argument is malformed.
2021-06-14 20:33:42 -07:00
Nathan Vegdahl
d415a666fe Address PR comments.
* Clean up "indent-style" command argument parsing.
* Adjust command's name to match the style of other commands.
* Add a "0" alias to the command, for tabs indent style.
2021-06-14 18:32:23 -07:00
Nathan Vegdahl
ecb39da3e0 Cosmetic changes and better comments for the indent auto-detect code. 2021-06-14 18:32:23 -07:00
Nathan Vegdahl
4faf1d3bf4 Remove indent style status-line display for now. 2021-06-14 18:32:23 -07:00
Nathan Vegdahl
0a5580aa21 Address PR comments.
- Move char functions into their own module under helix_core.
- Use matches!() macro where appropriate.
- Use a static lifetime on indent_unit() now that we can.
2021-06-14 18:32:23 -07:00
Nathan Vegdahl
358ea6a37c Implement command to change the indent-style setting of a document. 2021-06-14 18:32:23 -07:00
Nathan Vegdahl
8648e483f7 Render indent-style status in status line.
Also cleaned up the status line code a little.
2021-06-14 18:32:23 -07:00
Nathan Vegdahl
5ca043c17a Fix clippy warnings. 2021-06-14 18:32:23 -07:00
Nathan Vegdahl
2329512122 Attempt to auto-detect indentation style on document load.
This also moves the primary indentation-style setting into Document.
2021-06-14 18:32:23 -07:00
Ivan Tham
1bda454149 Add ctrl-w for prompt 2021-06-15 01:06:53 +09:00
Blaž Hrastnik
e819121f6e fix: wq/wqa functions need to wait for save to finish before closing 2021-06-15 01:02:32 +09:00
Gokul Soumya
f33aaba53f Add ui.selection to theme.toml
Enables changing the color of the selection which was previously
hard coded.
2021-06-15 00:06:53 +09:00
Gokul Soumya
9cfa163370 Refactor keymap definitions using macros
Adds a macro rule to the `key!` macro so that keymaps using `Left`,
`Home`, `Esc`, etc. will also be accepted.
2021-06-14 20:31:20 +09:00
Gokul Soumya
6b8c6ed535 Correct onedark theme file location 2021-06-14 18:24:12 +09:00
dependabot[bot]
e4b3a666d2 Bump once_cell from 1.7.2 to 1.8.0 (#255)
Bumps [once_cell](https://github.com/matklad/once_cell) from 1.7.2 to 1.8.0.
- [Release notes](https://github.com/matklad/once_cell/releases)
- [Changelog](https://github.com/matklad/once_cell/blob/master/CHANGELOG.md)
- [Commits](https://github.com/matklad/once_cell/compare/v1.7.2...v1.8.0)

---
updated-dependencies:
- dependency-name: once_cell
  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>
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2021-06-14 17:46:12 +09:00
Gokul Soumya
43e3173231 Add onedark theme 2021-06-14 17:39:01 +09:00
dependabot[bot]
8c1edd22af Bump anyhow from 1.0.40 to 1.0.41
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.40 to 1.0.41.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.40...1.0.41)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-14 17:36:56 +09:00
dependabot[bot]
9b352ceefd Bump crossterm from 0.19.0 to 0.20.0
Bumps [crossterm](https://github.com/crossterm-rs/crossterm) from 0.19.0 to 0.20.0.
- [Release notes](https://github.com/crossterm-rs/crossterm/releases)
- [Changelog](https://github.com/crossterm-rs/crossterm/blob/master/CHANGELOG.md)
- [Commits](https://github.com/crossterm-rs/crossterm/compare/0.19...0.20)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-14 17:36:47 +09:00
dependabot[bot]
f882ea92b6 Bump lsp-types from 0.89.1 to 0.89.2
Bumps [lsp-types](https://github.com/gluon-lang/lsp-types) from 0.89.1 to 0.89.2.
- [Release notes](https://github.com/gluon-lang/lsp-types/releases)
- [Changelog](https://github.com/gluon-lang/lsp-types/blob/master/CHANGELOG.md)
- [Commits](https://github.com/gluon-lang/lsp-types/compare/v0.89.1...v0.89.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-14 17:36:40 +09:00
dependabot[bot]
98485524c8 Bump ignore from 0.4.17 to 0.4.18
Bumps [ignore](https://github.com/BurntSushi/ripgrep) from 0.4.17 to 0.4.18.
- [Release notes](https://github.com/BurntSushi/ripgrep/releases)
- [Changelog](https://github.com/BurntSushi/ripgrep/blob/master/CHANGELOG.md)
- [Commits](https://github.com/BurntSushi/ripgrep/compare/ignore-0.4.17...ignore-0.4.18)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-06-14 17:36:31 +09:00
Wojciech Kępka
f47da891db Fix a typo in theme name 2021-06-14 14:41:59 +09:00
Andreas Liljeqvist
5d23667a26 fix offset by one problem in replace_with_yanked 2021-06-14 09:58:40 +09:00
Wojciech Kępka
b6e363ef0e Add bogster theme 2021-06-14 09:43:15 +09:00
Yusuf Bera Ertan
ca02024199 chore(nix): update nixCargoIntegration input 2021-06-13 23:38:14 +09:00
Blaž Hrastnik
ae5ecfdf66 Release v0.2.0 2021-06-13 22:35:13 +09:00
Blaž Hrastnik
d545e61644 ui: Prompt should figure out a reasonable column width
Fixes #192
Refs #225
2021-06-13 22:28:18 +09:00
Wojciech Kępka
df217f71c1 Fix wq 2021-06-13 20:48:18 +09:00
Wojciech Kępka
d008e86037 Document::is_modified should not check if path is set
If there is a new document we still want to know if there are unsaved changes
2021-06-13 20:48:18 +09:00
Wojciech Kępka
b9100fbd44 Fix clippy 2021-06-13 20:48:18 +09:00
Wojciech Kępka
52d3c29244 Deduplicate code 2021-06-13 20:48:18 +09:00
Wojciech Kępka
17c9a8499e Add qa and qa! 2021-06-13 20:48:18 +09:00
Wojciech Kępka
62e6232a32 Update write_all 2021-06-13 20:48:18 +09:00
Wojciech Kępka
d8b5d1181f Add Copy derive to PromptEvent 2021-06-13 20:48:18 +09:00
Wojciech Kępka
b500a2a138 commands: Add more write commands 2021-06-13 20:48:18 +09:00
Yusuf Bera Ertan
a3f01503e2 build(nix): use nix-cargo-integration, make shell.nix use flake devshell 2021-06-13 14:46:51 +09:00
Ivan Tham
9640ed1425 Add clarification to last buffer 2021-06-13 09:58:50 +09:00
Robin
9baf1ecc90 add symbol picker (#230)
* add symbol picker

use the lsp document_symbol request

* fix errors from merging in master

* add docs for symbol picker
2021-06-12 21:45:21 +09:00
Robin
44cc0d8eb0 add alternate file (#223)
* add alternate file

inspired by vim ctrl-6/kak ga commands. the alternate file is kept per view

* apply feedback from #223

* rename to last_accessed

* add ga doc

* add fail message for ga
2021-06-12 21:21:06 +09:00
Ivan Tham
1953588873 Change picker horizontal split to h
Follow window mode and vim behavior, x seemed weird.
2021-06-12 21:17:48 +09:00
Wojciech Kępka
45793d7c09 Update README 2021-06-12 17:26:41 +08:00
Wojciech Kępka
4b6aff8c66 Use runtime dir when defaulting to executable location 2021-06-12 17:26:41 +08:00
Wojciech Kępka
4a40e935de Make runtime_dir private 2021-06-12 17:26:41 +08:00
Wojciech Kępka
716067ba05 Add more ways to detect runtime directory 2021-06-12 17:26:41 +08:00
Wojciech Kępka
c754df12b3 lsp: Check bounds when converting lsp positions (#204)
* lsp: Make position conversion funcs return `Option`

* Add tests

* Fixes

* Revert pos_to_lsp_pos to panic
2021-06-12 16:04:30 +09:00
Blaž Hrastnik
1bf5b103b0 Add bug report template 2021-06-12 10:39:46 +09:00
Blaž Hrastnik
1665bac1b6 Fix broken test 2021-06-12 10:24:48 +09:00
Blaž Hrastnik
278361a086 Only auto-format for certain languages
Fixes #53
Fixes #207
2021-06-12 10:20:37 +09:00
Jakub Bartodziej
69fe46a122 Add :earlier and :later commands that can be used to navigate the full edit history. (#194)
* Disable deleting from an empty buffer which can cause a crash.

* Improve on the fix for deleting from the end of the buffer.

* Clean up leftover log.

* Avoid theoretical underflow.

* Implement :before which accepts a time interval and moves the editor to
the closest history state to the commit of the current time minus that
interval. Current time is now by default, or the commit time if :before
has just been used.

* Add :earlier an :later commands that can move through
the edit history and retrieve changes hidded by undoing
and commiting new changes. The commands accept a number
of steps or a time period relative to the currrent change.

* Fix clippy lint error.

* Remove the dependency on parse_duration, add a custom parser instead.

* Fix clippy errors.

* Make helix_core::history a public module.

* Use the helper for getting the current document and view.

* Handled some PR comments.

* Fix the logic in :later n.

Co-authored-by: Ivan Tham <pickfire@riseup.net>

* Add an alias for :earlier.

Co-authored-by: Ivan Tham <pickfire@riseup.net>

* Add an alias for later.

Co-authored-by: Ivan Tham <pickfire@riseup.net>

* Run cargo fmt.

* Add some tests for earlier and later.

* Add more tests and restore the fix for later that diappeared somehow.

* Use ? instead of a match on an option.

Co-authored-by: Ivan Tham <pickfire@riseup.net>

* Rename to UndoKind.

* Remove the leftover match.

* Handle a bunch of review comments.

* More systemd.time compliant time units and additional description for the new commands.

* A more concise rewrite of the time span parser using ideas from PR discussion.

* Replace a match with map_err().

Co-authored-by: Ivan Tham <pickfire@riseup.net>

Co-authored-by: Jakub Bartodziej <jqb@google.com>
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-11 22:06:13 +09:00
PabloMansanet
86af55c379 Movement fixes, refactor and unit test suite (#217)
* Add convenience/clarity wrapper for Range initialization

* Test horizontal moves

* Add column jumping tests

* Add failing movement conditions for multi-word moves

* Refactor skip_over_next

* Add complex forward movement unit tests

* Add strict whitespace checks and edge case tests

* Restore formatting

* Remove unused function

* Add empty test case for deletion and fix nth_prev_word_boundary

* Add tests for backward motion

* Refactor word movement

* Address review comments and finish refactoring backwards move

* Finish unit test suite

* Fmt pass

* Fix lint erors

* Clean up diff restoring bad 'cargo fmt' actions

* Simplify movement closures (thanks Pickfire)

* Fmt pass

* Replace index-based movement with iterator based movement, ensuring that each move incurs a single call to the RopeSlice API

* Break down tuple function

* Extract common logic to all movement functions

* Split iterator helpers away into their own module

* WIP reducing clones

* Operate on spans

* WIP simplifying iterators

* Simplify motion helpers

* Fix iterator

* Fix all unit tests

* Refactor and simplify

* Simplify fold
2021-06-11 21:57:07 +09:00
Wojciech Kępka
0c2b99327a commands: Handle t<ENTER> as till newline 2021-06-11 18:34:46 +09:00
Blaž Hrastnik
a8a5bcd13d Temporarily disable workDone
Blows up on gopls because we don't handle receiving window/workDoneProgress/create method calls
2021-06-11 13:30:21 +09:00
Wojciech Kępka
098806ce2a lsp: Display LSP progress messages (#216) 2021-06-11 12:42:16 +09:00
Robin van Dijk
c0d32707d0 move to first nonwhitespace on shift-i
This matches the behaviour in vim and kak
2021-06-10 22:02:38 +09:00
Timothy DeHerrera
d8df10f295 Add Nix runtime 2021-06-10 22:01:48 +09:00
Timothy DeHerrera
38073fd64c Add Nix syntax 2021-06-10 22:01:48 +09:00
Timothy DeHerrera
01760c3845 embed runtime 2021-06-10 22:00:53 +09:00
Timothy DeHerrera
8590f6a912 ignore Nix outputs 2021-06-10 22:00:53 +09:00
Timothy DeHerrera
69378382c3 add overlay 2021-06-10 22:00:53 +09:00
Timothy DeHerrera
1a774d61bb Fix flake package 2021-06-10 22:00:53 +09:00
notoria
1b14e9a19a Downgrade unicode-segmentation 2021-06-10 22:00:08 +09:00
notoria
e46346c907 Correct tree-sitter-haskell submodule 2021-06-10 22:00:08 +09:00
notoria
9887b1275a Implement missing Debug and update Cargo.lock 2021-06-10 22:00:08 +09:00
Ivan Tham
7cc13fefe9 Derive debug without feature
Note that this also removed those `finish_non_exhaustive()`.
2021-06-10 22:00:08 +09:00
notoria
1a3a924634 Implement Debug for data structure as a feature 2021-06-10 22:00:08 +09:00
Blaž Hrastnik
aebdef8257 Reuse a cursor from the pool if available (fixes #202) 2021-06-10 12:49:34 +09:00
Ivan Tham
6b3c9d8ed3 Fix jump behavior, goto_implementation now jump
Better jump behavior since we override the first jump if it's on the
first document. At the same time, ctrl-i is now working with gd jumps.
2021-06-10 11:08:18 +08:00
wojciechkepka
4dbc23ff1c Fix documentation popup panic 2021-06-10 11:26:03 +09:00
Kevin Sjöberg
b20e4a108c Only enforce limit outside of .git 2021-06-09 10:06:31 +09:00
Kevin Sjöberg
1bb9977faf Match keybindings of menu 2021-06-09 09:54:22 +09:00
Kevin Sjöberg
29962a5bd9 Fix Shift-Tab for moving upwards in menu 2021-06-09 09:53:40 +09:00
Kevin Sjöberg
7ef0e2cab6 Don't panic on empty document 2021-06-09 09:43:21 +09:00
Corey Powell
35feb614b6 Updated elixir queries to fix crash 2021-06-09 00:07:44 +09:00
Ivan Tham
5e2ba28e0e Fix panic on ctrl-w empty document 2021-06-08 23:08:08 +09:00
Blaž Hrastnik
83723957fe Fix crash when too many completions available
Refs #81
2021-06-08 21:58:26 +09:00
Zheming Li
ae51065213 Support go to line 1 2021-06-08 17:27:21 +09:00
Wojciech Kępka
4e3a343602 Make r<ENTER> work 2021-06-08 17:23:38 +09:00
Wojciech Kępka
81e02e1ba4 Remove unwanted as_str 2021-06-08 17:23:38 +09:00
Wojciech Kępka
c349ceb61f Don't replace newlines 2021-06-08 17:23:38 +09:00
Wojciech Kępka
2e4a338944 Add bounds checks to replace 2021-06-08 17:23:38 +09:00
Wojciech Kępka
9c83a98469 commands: Replace all characters in selection 2021-06-08 17:23:38 +09:00
Wojciech Kępka
1bffb34350 Make matching bracket dimmed, prevent out of bounds rendering 2021-06-08 17:23:05 +09:00
Wojciech Kępka
c978d811d9 Cleanup find_first_non_whitespace_char funcs 2021-06-08 17:22:37 +09:00
Wojciech Kępka
48df05b16d commands: Add goto first non-whitespace char of line 2021-06-08 17:22:37 +09:00
Kirawi
b873fb9897 Fix Unicode (#135)
* init

* wip

* wip

* fix unicode break

* fix unicode break

* Update helix-core/src/transaction.rs

Co-authored-by: Benoît Cortier <benoit.cortier@fried-world.eu>

* clippy

* fix

* add changes

* added test

* wip

* wip

* wip

* wip

* fix

* fix view

* fix #88

Co-authored-by: Benoît Cortier <benoit.cortier@fried-world.eu>
2021-06-08 13:20:15 +09:00
Kelly Thomas Kline
8f1eb7b2b0 Add trace log primer to the Contributing section 2021-06-08 12:21:25 +09:00
Ivan Tham
82fdfdc38e Add missing newline to end of file on load
Fix #152
2021-06-08 11:38:56 +09:00
Egor Karavaev
ea6667070f helix-lsp cleanup 2021-06-08 10:56:46 +09:00
Egor Karavaev
960bc9f134 Don't panic on LSP not starting 2021-06-08 10:02:41 +09:00
Kevin Sjöberg
08f50310bd Bump file picker limit 2021-06-08 09:51:50 +09:00
Wojciech Kępka
4bec87ad18 Update keymap 2021-06-08 09:50:14 +09:00
Wojciech Kępka
c65b4dea09 commands: Add replace with yanked as R 2021-06-08 09:50:14 +09:00
Wojciech Kępka
6fc0e0b5fb completion: Fix unimplemented autocomplete 2021-06-08 09:38:53 +09:00
Blaž Hrastnik
0201ef9205 ui: completion: Use the correct type_name
Fixes #166
2021-06-08 01:38:57 +09:00
Wojciech Kępka
037f45f24e Create all parent directories for config and cache 2021-06-08 01:07:30 +09:00
Blaž Hrastnik
9821beb5c4 Make gh/gl extend selection in select mode 2021-06-07 23:32:44 +09:00
Blaž Hrastnik
3cee0bf200 Address clippy lint 2021-06-07 23:08:51 +09:00
Blaž Hrastnik
4fd38f82a3 Disable failing doctest 2021-06-07 23:05:39 +09:00
Ivan Tham
b5682f984b Separate helix-term as a library
helix-term stuff will now be documented in rustdoc.
2021-06-07 21:35:31 +08:00
Benoît CORTIER
68affa3c59 Implement register selection
User can select register to yank into with the " command.
A new state is added to `Editor` and `commands::Context` structs.
This state is managed by leveraging a new struct `RegisterSelection`.
2021-06-07 21:52:09 +09:00
Blaž Hrastnik
d5de9183ef Use upstream jsonrpc again 2021-06-07 21:33:17 +09:00
Blaž Hrastnik
8d6fad4cac lsp: Provide workspace root on client.initialize() 2021-06-07 21:32:01 +09:00
Blaž Hrastnik
14830e75ff Revert the line number rendering change, we were correct before 2021-06-07 13:24:03 +09:00
Blaž Hrastnik
59f94d13b8 Disable haskell grammar until build issues are resolved 2021-06-07 10:17:25 +09:00
Blaž Hrastnik
b3eeac7bbf Disable aarch64-macos, it fails to build on macos-latest 2021-06-07 09:50:15 +09:00
Blaž Hrastnik
f48a60b8e2 Release 0.0.10 2021-06-07 09:42:15 +09:00
Blaž Hrastnik
4f561e93b8 View mode: Use saturating_sub when calculating first_col 2021-06-07 09:29:21 +09:00
Blaž Hrastnik
01b1bd15a1 commands: use chars().count() over .len() on strings 2021-06-07 09:26:49 +09:00
Blaž Hrastnik
ff8a031cb2 Add diagnostics keys to keymap.md 2021-06-07 09:24:23 +09:00
Blaž Hrastnik
d9b2f6feac Only test on stable rust
Shorter CI times, and it should be good enough.
2021-06-07 09:20:36 +09:00
Blaž Hrastnik
582f1ee9d8 Add aarch64-macos (M1) to the release build matrix 2021-06-07 09:19:51 +09:00
ahkrr
e2d780f993 fix: 2 panics while setting style + off by 1
The panics would occur because set_style 
would draw outside of the the surface. 
Both occured using `find_prev` or `till_prev`
In my case the first panic! would appear
in a terminal with around 80 columns 
in helix/README.md going to the end of the file
with `geglf(`
the second with `geglfX`
The off by one fix ensures that `find_nth_prev` 
starts at the first character to the left
2021-06-07 09:15:08 +09:00
Ethan Bodzioney
843c2cdebd Install instructions and version number corrections (#148)
* Add MacOS install instructions

* Change version name argument

When using the -V command to get the version you are given 'helix-term x.x.x', I changed this to just helix as it makes more sense.

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number
2021-06-07 09:14:06 +09:00
Benoît CORTIER
8a29086c1a Fix panic when moving over unicode punctuation
`is_ascii_punctuation` will only work for ASCII punctuations, and when
we have unicode punctuation (or other) we jump into the `unreachable`.
This patch fallback into categorizing everything in this branch as
`Unknown`.

Fixes https://github.com/helix-editor/helix/issues/123

https://github.com/helix-editor/helix/pull/135: add better support for
unicode categories.
2021-06-07 09:12:01 +09:00
Wojciech Kępka
16b1cfa3be Add diagnostics keybindings 2021-06-07 09:11:52 +09:00
Ivan Tham
2066e866c7 Add spc w w for window mode 2021-06-07 09:08:08 +09:00
Kevin Sjöberg
3494bb8ef0 Refactor index assignment
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-06 21:48:19 +09:00
Kevin Sjöberg
a4ff8cdd8a Allow moving backwards in completions 2021-06-06 21:48:19 +09:00
Kevin Sjöberg
145bc1970a Trigger directory completion upon pressing Enter 2021-06-06 21:48:19 +09:00
Ingrid
54f3548d54 theme: Enable style modifiers in theme.toml, add Ingrid's theme (#113)
* theme: Enable style modifiers in theme.toml

* docs: theme documentation

* fixup: parse modifiers with filter_map

* theme: tests for parse_style

* theme: Log invalid cases in theme.toml parse

* docs: theme documentation fixup

* docs: Blaz's theming comments

* docs: Theme doc fixes from pickfire

Co-authored-by: Ivan Tham <pickfire@riseup.net>

* theme: More context in logs, TODO for alerting users

* contrib: Ingrid's theme

* docs: Theme subsection fixes

Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-06 21:45:59 +09:00
Ivan Tham
3280510d5b Fix unused import 2021-06-06 21:30:18 +09:00
Ivan Tham
df80f3c966 Add test for prev word 2021-06-06 21:30:18 +09:00
Ivan Tham
40744ce835 Add ctrl-w in insert mode
It seemed to panic when I pressed too many times, but that is from
lsp side.
2021-06-06 21:30:18 +09:00
Kevin Sjöberg
aa8a8baeeb Calculate offset when moving picker cursor 2021-06-06 19:18:09 +09:00
Wojciech Kępka
bcb1afeb4c Add a comment to canonicalize_path 2021-06-06 17:28:09 +08:00
Wojciech Kępka
de946d2357 Add a TODO 2021-06-06 17:28:09 +08:00
Wojciech Kępka
14f511da93 Create document if it doesn't exist on save 2021-06-06 17:28:09 +08:00
Blaž Hrastnik
392631b21d Update build.yml 2021-06-06 18:22:18 +09:00
Ivan Tham
ce99ecc7a2 Add more coverage for CI
Runs every day as cron. Add matrix for test, includes windows and macos.
2021-06-06 18:10:02 +09:00
Kevin Sjöberg
2ac496f919 Do not move past number of matches 2021-06-06 18:04:45 +09:00
Brian Dawn
5463a436a8 Return an error if we request an embedded file that does not exist.
This makes the load_runtime_file function behave like the non-embedded
one.
2021-06-06 10:49:17 +09:00
Brian Dawn
e09b0f4eff Add a smoke test around loading runtime files.
This test makes sure we can read some amount of data from the runtime folder.
2021-06-06 10:49:17 +09:00
Brian Dawn
f3db12e240 Simplify the load_runtime_file code.
Reduce the number of feature switches for the embed_runtime feature.
2021-06-06 10:49:17 +09:00
Brian Dawn
676719b361 Simplify creating pathbufs.
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-06 10:49:17 +09:00
Brian Dawn
ae105812d6 Apply suggestions from code review
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-06-06 10:49:17 +09:00
Brian Dawn
255598a2cb Make rust-embed optionally included based on the embed_runtime feature. 2021-06-06 10:49:17 +09:00
Brian Dawn
62d181de78 Provide a feature flag to be able to embed the runtime folder.
These changes provide a new feature flag "embed_runtime" that when
enabled and built in release mode will embed the runtime folder into the
resulting binary.
2021-06-06 10:49:17 +09:00
Ivan Tham
8c2fa12ffc Add window mode
Fix #93
2021-06-06 10:12:35 +09:00
Jan Hrastnik
212f6bc372 changed flag in build_cpp '/std:c++14' to '/std:c++17' due to tree_sitter_haskell not compiling on msvc without it 2021-06-06 09:27:58 +09:00
ahkrr
c5c3ec07f4 fix: panicked at 'attempt to subtract with overflow'
helix-term/src/ui/editor.rs:275:29
This would happen when the window-size was to small to display the entire width and one would start jumping forwards with f<some_char> and the beginning of the highlighted area would end up outside of the window
2021-06-06 00:01:16 +09:00
ahkrr
444cd0b068 fix: make find_prev_char and till_prev_char work
Bevore this PR `commands::find_prev_char` and `commands::till_prev_char` were triggerable through keys 
but `seach::find_nth_next()` was hardcoded in `_find_char`. 
The passed `fn` was nerver used. With this PR the passed `fn` is used.
The change in search.rs resolves an off by one error in the behivor of `find_nth_prev`
2021-06-06 00:01:16 +09:00
Blaž Hrastnik
f6a900fee1 syntax: Use a different C++ flag for MSVC 2021-06-06 00:00:18 +09:00
Ivan Tham
6254720f53 Add unreachable context
Better error for #123
2021-06-05 20:18:27 +08:00
Blaž Hrastnik
407b37c327 Better link to Matrix 2021-06-05 13:04:13 +09:00
notoria
2bb71a829e Don't panic on empty file/buffer (#108) 2021-06-05 13:00:43 +09:00
Kirawi
c17dcb8633 Fixing Multiple Panics (#121)
* init

* wip

* wip
2021-06-05 12:49:19 +09:00
Blaž Hrastnik
5a344a3ae5 Address clippy lint 2021-06-05 09:28:13 +09:00
Antoni Stevenet
a1f4b8f92b Add home-end keymaps, (as kakoune/vim do) (#83)
* add home-end keymaps

* implement extend methods for extend_line_start, extend_line_end

* add home-end mappings to keymaps.md

* add ^-$ extend mappings for extend mode

* pass cargo linter
2021-06-05 09:25:46 +09:00
Blaž Hrastnik
72eaaaac99 syntax: Build C++ grammars as c++14
The haskell grammar requires at last c++14 to build.

Fixes #117
2021-06-05 09:21:33 +09:00
Blaž Hrastnik
8f78c0c612 syntax: Disable explicit debug/opt_level passing
cc-rs will already do the right thing and figure out the flags.

Fixes #34
2021-06-05 09:20:33 +09:00
Corey Powell
01dd7b570a Restored haskell syntax
It seems to work
2021-06-05 01:17:44 +08:00
notoria
f3a243c6cb Rust: Highlight crate namespace, categorize mut 2021-06-04 23:16:33 +09:00
notoria
adcfcf9044 Replace ^/$ with gh/gl 2021-06-04 17:26:16 +09:00
Blaž Hrastnik
4f0e3aa948 Implement gt/gm/gb, remap goto tYpe to gy 2021-06-04 15:47:29 +09:00
Blaž Hrastnik
f2e554d761 matchbrackets: Needs to render with the viewport offset 2021-06-04 15:11:55 +09:00
Blaž Hrastnik
bd4552cd2b scroll: Fix the clamping 2021-06-04 11:36:28 +09:00
Blaž Hrastnik
06d8d3f55f Try to detect language when document file path is set
Fixes #91
2021-06-04 11:03:40 +09:00
Blaž Hrastnik
8afd4e1bc2 Exit select mode on delete_selection 2021-06-04 11:03:40 +09:00
wojciechkepka
43b92b24d2 Show file picker when directory passed as first arg 2021-06-04 11:02:06 +09:00
notoria
b2b2d430ae Rust: Add keyword async, match the entire macro 2021-06-04 10:57:17 +09:00
notoria
8af5a9a5cf Remove swapfile 2021-06-04 10:30:14 +09:00
notoria
f76f44c8af Convert byte index to char index for find 2021-06-04 10:00:22 +09:00
Egor Karavaev
d55419604c Remove select_all implementation 2021-06-04 09:25:30 +09:00
Ivan Tham
29b9eed33c Fix panic paint mysterious matching pair
When the matching pair is out of bounds it still paints it causing an
out of bound panic. A dirty fix since it still have some issue, at least
it does not panic now.
2021-06-04 09:25:03 +09:00
Kevin Sjöberg
fdb5bfafae Limit goto count
Giving a goto count greater than the number of lines in the buffer
would cause Helix to panic.
2021-06-04 01:35:52 +09:00
Ivan Tham
e6132f0acd Fix undo redo
I missed the fast return.

Fix #89
2021-06-04 01:27:09 +09:00
Antoni Stevent
3071339cbc update keymap.md to include arrow keys for movement 2021-06-03 23:24:24 +09:00
Antoni Stevent
27aee705e0 use correct _extend methods, also remove unnecessary casts 2021-06-03 23:24:24 +09:00
Antoni Stevent
f0fe558f38 Add up/right/left/down arrow keymaps, similar to kakoune 2021-06-03 23:24:24 +09:00
Jakub Bartodziej
09a7db637e Avoid theoretical underflow. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
31ed4db153 Clean up leftover log. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
3c5dfb0633 Improve on the fix for deleting from the end of the buffer. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
6cbc0aea92 Disable deleting from an empty buffer which can cause a crash. 2021-06-03 23:23:23 +09:00
Jan Hrastnik
c1c3750d38 key is now modified in place at start of handle_event 2021-06-03 23:16:04 +09:00
Jan Hrastnik
daad8ebe12 key_canonicalization now only matches chars 2021-06-03 23:16:04 +09:00
Jan Hrastnik
68abc67ec6 put the key canonicalization in a seperate function. only chars now get stripped of Shift modifier 2021-06-03 23:16:04 +09:00
Jan Hrastnik
712f25c2b9 removed shift matching 2021-06-03 23:16:04 +09:00
Blaž Hrastnik
abe8a83d8e Merge pull request #92 from bfredl/clangd
LSP: add clangd as server for c/c++
2021-06-03 22:23:20 +09:00
Blaž Hrastnik
a05fb95769 Merge pull request #80 from notoria/highlight
Highlight matching brackets
2021-06-03 22:14:37 +09:00
Blaž Hrastnik
74e4ac8d49 Merge pull request #77 from notoria/match_brackets
Fix match_brackets::find
2021-06-03 22:13:48 +09:00
Björn Linse
0e6f007028 LSP: add clangd as server for c/c++ 2021-06-03 15:07:50 +02:00
notoria
c3a98b6a3e Highlight matching brackets 2021-06-03 11:40:46 +02:00
notoria
4fe654cf9a Fix match_brackets::find 2021-06-03 10:35:17 +02:00
Blaž Hrastnik
661dbdca57 Fix cursor not showing on (0, 0) 2021-06-03 13:34:00 +09:00
Blaž Hrastnik
5773bd6a40 Merge pull request #64 from pickfire/log
Default log file to cache
2021-06-03 12:58:31 +09:00
Ivan Tham
d664d1dec0 Default log file to cache 2021-06-03 10:15:17 +08:00
Blaž Hrastnik
7e8603247d Merge pull request #66 from IceDragon200/replaced-args-parser
Drop pico-args in favour of a hand rolled parser
2021-06-03 10:32:42 +09:00
Blaž Hrastnik
7140908f6e Nix: add lldb to shell 2021-06-03 10:31:33 +09:00
Blaž Hrastnik
6dba1e7ec7 Clippy lint 2021-06-03 10:31:14 +09:00
Blaž Hrastnik
c0332bd935 Fix split sizes getting out of sync with the terminal size, refs #69 2021-06-03 10:28:49 +09:00
Blaž Hrastnik
3c7729906c Merge pull request #70 from RLHerbert/master
Fix panic when buffer larger than terminal width
2021-06-03 10:28:14 +09:00
Rowan Herbert
1b67fae9f4 Fix panic when buffer larger than terminal width 2021-06-02 16:30:40 -07:00
Corey Powell
f0018280cb Refactored parse_args loop
Thanks @PabloMansanet
2021-06-02 14:26:20 -05:00
Corey Powell
7202953e69 Dropped pico-args in favour of a simpler hand roller parser
Not the greatest looking, but it gets the job done
2021-06-02 14:26:13 -05:00
Corey Powell
7761c88d61 Merge pull request #62 from pickfire/cell
Separate document history into Cell
2021-06-02 13:27:35 -05:00
Corey Powell
68f5031dcc Merge pull request #49 from eleijonmarck/patch-1
Update README.md to include shortcuts
2021-06-02 13:15:32 -05:00
Corey Powell
83031564db Merge pull request #57 from pickfire/fix-panic
Fix panic opening rust file
2021-06-02 13:14:19 -05:00
Ivan Tham
eab6e53511 Fix panic opening rust file
Application::new will use stuff that requires tokio runtime.
2021-06-02 23:49:26 +08:00
Ivan Tham
f5f46b1fed Separate document history into Cell
As history is used separately from the rest of the edits, separating it
can avoid needless borrowing and cloning. But one need to be aware later.
2021-06-02 23:47:50 +08:00
Eric Leijonmarck
5f49bafbe8 Update README.md 2021-06-02 17:05:15 +02:00
Blaž Hrastnik
2719a35123 Merge pull request #55 from helix-editor/autoresize
autoresize terminal in compositor render
2021-06-02 22:45:43 +09:00
Blaž Hrastnik
0a6672c626 Merge pull request #50 from wojciechkepka/config
Use config_dir for logging, create config_dir
2021-06-02 22:43:28 +09:00
Blaž Hrastnik
b51111a364 Merge pull request #21 from IceDragon200/elixir-syntax
Added elixir syntax
2021-06-02 22:41:51 +09:00
Jan Hrastnik
78980f575b autoresize terminal in compositor render 2021-06-02 15:40:08 +02:00
Corey Powell
0bb375bafa Added missing tree-sitter-elixir submodule 2021-06-02 06:43:22 -05:00
Eric Leijonmarck
c960bcfc24 Update README.md 2021-06-02 13:15:31 +02:00
Wojciech Kępka
e88383d990 Use config_dir for logging, create config_dir 2021-06-02 12:25:25 +02:00
Eric Leijonmarck
312b29f712 Update README.md 2021-06-02 12:05:39 +02:00
Blaž Hrastnik
f4560cb68a Better fix for w/e that also covers ia<esc>we/ia<esc>wb 2021-06-02 14:57:43 +09:00
Blaž Hrastnik
cbb3ebafdc Support ctrl-f and ctrl-b to page up/down, fixes #41 2021-06-02 13:20:36 +09:00
Blaž Hrastnik
0851110d10 f/t: Check if at bounds before searching, refs #43, closes #37 2021-06-02 13:20:27 +09:00
Blaž Hrastnik
3ace581191 Fix panics when triggering w or e on the last char of the line
Closes #32
2021-06-02 13:19:40 +09:00
Blaž Hrastnik
c0264b9f7f fix: Don't allow moving past last line, fixes #30, #24
Off by 1 error
2021-06-02 13:19:40 +09:00
Blaž Hrastnik
22dad592b8 Merge pull request #40 from data0x200/fix-empty-command
Fix empty command cause panic
2021-06-02 13:06:57 +09:00
Corey Powell
ca042a4bde Added elixir syntax
Using custom fork for now to get around generating the source files
2021-06-01 21:59:16 -05:00
Blaž Hrastnik
67b1cd32c7 Update install notes 2021-06-02 11:14:46 +09:00
Daichi Takamiya
4d12c7c3cf Fix empty command cause panic 2021-06-02 10:55:32 +09:00
Blaž Hrastnik
4f56a8e248 book: Always generate the CNAME file 2021-06-02 10:24:00 +09:00
Blaž Hrastnik
dbc392d92c Run fmt 2021-06-02 09:56:50 +09:00
Blaž Hrastnik
7967d312c0 Merge pull request #38 from nathom/master
Add .DS_Store to ignored directories
2021-06-02 09:37:09 +09:00
Blaž Hrastnik
db48d22384 Merge pull request #19 from wojciechkepka/archinstall
Add Arch Linux installation instructions to README
2021-06-02 09:30:41 +09:00
Blaž Hrastnik
533ff61d0e Merge pull request #34 from DanySpin97/improve-error
Improve errors handling in main by adding context
2021-06-02 09:30:16 +09:00
nathom
b1ce969d80 Add .DS_Store to ignored directories 2021-06-01 17:29:37 -07:00
Blaž Hrastnik
01bf363446 Merge pull request #31 from wullewutz/patch-1
Fixed c/p error in keymap doc
2021-06-02 09:26:29 +09:00
Blaž Hrastnik
cc323f7665 Merge pull request #36 from swdunlop/patch-1
Make HELIX_RUNTIME depend on pwd, not speed's HOME
2021-06-02 09:26:04 +09:00
Scott Dunlop
60caaf7fc4 Make HELIX_RUNTIME depend on pwd, not speed's HOME 2021-06-01 15:03:57 -07:00
Danilo Spinella
ea824ed05d Improve errors handling in main by adding context
Return a anyhow::Result in main function so that Context can be used
there too.
2021-06-01 23:27:16 +02:00
wullewutz
cfae07e7ba Fixed c/p error in keymap doc
Go to definition mapping is "gd" not "ge"
2021-06-01 22:36:42 +02:00
wojciechkepka
56dbc60840 Add Arch Linux installation instructions to README 2021-06-01 21:08:09 +02:00
Blaž Hrastnik
c2e6b9f506 Add typescript support & ts/js indentation queries 2021-06-01 17:55:11 +09:00
150 changed files with 376676 additions and 3249 deletions

5
.envrc
View File

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

1
.gitattributes vendored Normal file
View File

@@ -0,0 +1 @@
helix-view/tests/* linguist-generated

4
.github/ISSUE_TEMPLATE/blank_issue.md vendored Normal file
View File

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

28
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,28 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: C-bug
assignees: ''
---
<!-- Your issue may already be reported!
Please search on the issue tracker before creating one. -->
### Reproduction steps
<!-- Ideally provide a key sequence and/or asciinema.org recording. -->
### Environment
- Platform: <!-- macOS / Windows / Linux -->
- Helix version: <!-- 'hx -v' if using a release, 'git describe' if building from master -->
<details><summary>~/.cache/helix/helix.log</summary>
```
please provide a copy of `~/.cache/helix/helix.log` here if possible, you may need to redact some of the lines
```
</details>

View File

@@ -0,0 +1,13 @@
---
name: Feature request
about: Suggest a new feature or improvement
title: ''
labels: C-enhancement
assignees: ''
---
<!-- Your feature may already be reported!
Please search on the issue tracker before creating one. -->
#### Describe your feature request

View File

@@ -1,11 +1,11 @@
name: Build
on:
pull_request:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
- cron: '00 01 * * *'
jobs:
check:
@@ -49,7 +49,7 @@ jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
@@ -60,7 +60,7 @@ jobs:
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: ${{ matrix.rust }}
override: true
- name: Cache cargo registry
@@ -86,6 +86,11 @@ jobs:
with:
command: test
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
lints:
name: Lints
runs-on: ubuntu-latest

View File

@@ -37,6 +37,10 @@ jobs:
rust: stable
target: x86_64-pc-windows-msvc
cross: false
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu
@@ -64,7 +68,7 @@ jobs:
uses: actions-rs/cargo@v1
with:
command: test
args: --locked
args: --release --locked
- name: Build release binary
uses: actions-rs/cargo@v1
@@ -96,7 +100,7 @@ jobs:
cp "target/${{ matrix.target }}/release/hx" "dist/"
fi
- uses: actions/upload-artifact@v2.2.3
- uses: actions/upload-artifact@v2.2.4
with:
name: bins-${{ matrix.build }}
path: dist

1
.gitignore vendored
View File

@@ -2,3 +2,4 @@ target
.direnv
helix-term/rustfmt.toml
helix-syntax/languages/
result

12
.gitmodules vendored
View File

@@ -82,3 +82,15 @@
path = helix-syntax/languages/tree-sitter-toml
url = https://github.com/ikatyang/tree-sitter-toml
shallow = true
[submodule "helix-syntax/languages/tree-sitter-elixir"]
path = helix-syntax/languages/tree-sitter-elixir
url = https://github.com/IceDragon200/tree-sitter-elixir
shallow = true
[submodule "helix-syntax/languages/tree-sitter-nix"]
path = helix-syntax/languages/tree-sitter-nix
url = https://github.com/cstrahan/tree-sitter-nix
shallow = true
[submodule "helix-syntax/languages/tree-sitter-latex"]
path = helix-syntax/languages/tree-sitter-latex
url = https://github.com/latex-lsp/tree-sitter-latex
shallow = true

87
CHANGELOG.md Normal file
View File

@@ -0,0 +1,87 @@
# 0.3.0
Another big release.
Highlights:
- Indentation is now automatically detected from file heuristics. ([#245](https://github.com/helix-editor/helix/pull/245))
- Support for other line endings (CRLF). Significantly improved Windows support. ([#224](https://github.com/helix-editor/helix/pull/224))
- Encodings other than UTF-8 are now supported! ([#228](https://github.com/helix-editor/helix/pull/228))
- Key bindings can now be configured via a `config.toml` file ([#268](https://github.com/helix-editor/helix/pull/268))
- Theme can now be configured and changed at runtime ([please feel free to contribute more themes!](https://github.com/helix-editor/helix/tree/master/runtime/themes)) ([#267](https://github.com/helix-editor/helix/pull/267))
- System clipboard yank/paste is now supported! ([#310](https://github.com/helix-editor/helix/pull/310))
- Surround commands were implemented ([#320](https://github.com/helix-editor/helix/pull/320))
Features:
- File picker can now be repeatedly filtered ([#232](https://github.com/helix-editor/helix/pull/232))
- LSP progress is now received and rendered as a spinner ([#234](https://github.com/helix-editor/helix/pull/234))
- Current line number can now be themed ([#260](https://github.com/helix-editor/helix/pull/260))
- Arrow keys & home/end now work in insert mode ([#305](https://github.com/helix-editor/helix/pull/305))
- Cursors and selections can now be themed ([#325](https://github.com/helix-editor/helix/pull/325))
- Language servers are now gracefully shut down before `hx` exits ([#287](https://github.com/helix-editor/helix/pull/287))
- `:show-directory`/`:change-directory` ([#335](https://github.com/helix-editor/helix/pull/335))
- File picker is now sorted by access time (before filtering) ([#336](https://github.com/helix-editor/helix/pull/336))
- Code is being migrated from helix-term to helix-view (prerequisite for
alternative frontends) ([#366](https://github.com/helix-editor/helix/pull/366))
- `x` and `X` merged
([f41688d9](https://github.com/helix-editor/helix/commit/f41688d960ef89c29c4a51c872b8406fb8f81a85))
Fixes:
- The IME popup is now correctly positioned ([#273](https://github.com/helix-editor/helix/pull/273))
- A bunch of bugs regarding `o`/`O` behavior ([#281](https://github.com/helix-editor/helix/pull/281))
- `~` expansion now works in file completion ([#284](https://github.com/helix-editor/helix/pull/284))
- Several UI related overflow crashes ([#318](https://github.com/helix-editor/helix/pull/318))
- Fix a test failure occuring only on `test --release` ([4f108ab1](https://github.com/helix-editor/helix/commit/4f108ab1b2197809506bd7305ad903a3525eabfa))
- Prompts now support unicode input ([#295](https://github.com/helix-editor/helix/pull/295))
- Completion documentation no longer overlaps the popup ([#322](https://github.com/helix-editor/helix/pull/322))
- Fix a crash when trying to select `^` ([9c534614](https://github.com/helix-editor/helix/commit/9c53461429a3e72e3b1fb87d7ca490e168d7dee2))
- Prompt completions are now paginated ([39dc09e6](https://github.com/helix-editor/helix/commit/39dc09e6c4172299bc79de4c1c52288d3f624bd7))
- Goto did not work on Windows ([503ca112](https://github.com/helix-editor/helix/commit/503ca112ae57ebdf3ea323baf8940346204b46d2))
# 0.2.1
Includes a fix where wq/wqa could exit before file saving completed.
# 0.2.0
Enough has changed to bump the version. We're skipping 0.1.x because
previously the CLI would always report version as 0.1.0, and we'd like
to distinguish it in bug reports..
- The `runtime/` directory is now properly detected on binary releases and
on cargo run. `~/.config/helix/runtime` can also be used.
- Registers can now be selected via " (for example `"ay`)
- Support for Nix files was added
- Movement is now fully tested and matches kakoune implementation
- A per-file LSP symbol picker was added to space+s
- Selection can be replaced with yanked text via R
- `1g` now correctly goes to line 1
- `ctrl-i` now correctly jumps backwards in history
- A small memory leak was fixed, where we tried to reuse tree-sitter
query cursors, but always allocated a new one
- Auto-formatting is now only on for certain languages
- The root directory is now provided in LSP initialization, fixing
certain language servers (typescript)
- LSP failing to start no longer panics
- Elixir language queries were fixed
# 0.0.10
Keymaps:
- Add mappings to jump to diagnostics
- Add gt/gm/gb mappings to jump to top/middle/bottom of screen
- ^ and $ are now gh, gl
- The runtime/ can now optionally be embedded in the binary
- Haskell syntax added
- Window mode (ctrl-w) added
- Show matching bracket (vim's matchbrackets)
- Themes now support style modifiers
- First user contributed theme
- Create a document if it doesn't exist yet on save
- Detect language on a new file on save
- Panic fixes, lots of them

237
Cargo.lock generated
View File

@@ -13,9 +13,15 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.40"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
[[package]]
name = "arc-swap"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
[[package]]
name = "autocfg"
@@ -59,12 +65,29 @@ dependencies = [
"jobserver",
]
[[package]]
name = "cfg-if"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4785bdd1c96b2a846b2bd7cc02e86b6b3dbf14e7e53446c4f54c92a361040822"
[[package]]
name = "cfg-if"
version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chardetng"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "81a81b0d8f8ee23417182818b4f06312c5f535c2b04eef1773f7c24bbdf8c500"
dependencies = [
"cfg-if 0.1.10",
"encoding_rs",
"memchr",
]
[[package]]
name = "chrono"
version = "0.4.19"
@@ -79,37 +102,36 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
"autocfg",
"cfg-if",
"cfg-if 1.0.0",
"lazy_static",
]
[[package]]
name = "crossterm"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c"
checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
dependencies = [
"bitflags",
"crossterm_winapi",
"futures-core",
"lazy_static",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9"
checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
dependencies = [
"winapi",
]
@@ -120,7 +142,7 @@ version = "2.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b98cf8ebf19c3d1b223e151f99a4f9f0690dca41414773390fc824184ac833e1"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"dirs-sys-next",
]
@@ -135,13 +157,28 @@ dependencies = [
"winapi",
]
[[package]]
name = "either"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]]
name = "encoding_rs"
version = "0.8.28"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
dependencies = [
"cfg-if 1.0.0",
]
[[package]]
name = "etcetera"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "016b04fd1e94fb833d432634245c9bb61cf1c7409668a0e7d4c3ab00c5172dec"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"dirs-next",
"thiserror",
]
@@ -233,22 +270,16 @@ version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"libc",
"wasi",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "globset"
version = "0.4.6"
version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a"
checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [
"aho-corasick",
"bstr",
@@ -259,47 +290,46 @@ dependencies = [
[[package]]
name = "helix-core"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"arc-swap",
"etcetera",
"helix-syntax",
"once_cell",
"regex",
"ropey",
"rust-embed",
"serde",
"smallvec",
"tendril",
"toml",
"tree-sitter",
"unicode-general-category",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "helix-lsp"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"anyhow",
"futures-executor",
"futures-util",
"glob",
"helix-core",
"jsonrpc-core",
"log",
"lsp-types",
"once_cell",
"pathdiff",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"url",
]
[[package]]
name = "helix-syntax"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"cc",
"serde",
@@ -309,7 +339,7 @@ dependencies = [
[[package]]
name = "helix-term"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"anyhow",
"chrono",
@@ -326,7 +356,6 @@ dependencies = [
"log",
"num_cpus",
"once_cell",
"pico-args",
"pulldown-cmark",
"serde",
"serde_json",
@@ -336,11 +365,13 @@ dependencies = [
[[package]]
name = "helix-tui"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"bitflags",
"cassowary",
"crossterm",
"helix-core",
"helix-view",
"serde",
"unicode-segmentation",
"unicode-width",
@@ -348,19 +379,25 @@ dependencies = [
[[package]]
name = "helix-view"
version = "0.1.0"
version = "0.3.0"
dependencies = [
"anyhow",
"bitflags",
"chardetng",
"crossterm",
"encoding_rs",
"futures-util",
"helix-core",
"helix-lsp",
"helix-tui",
"log",
"once_cell",
"serde",
"slotmap",
"tokio",
"toml",
"url",
"which",
]
[[package]]
@@ -385,9 +422,9 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.17"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
dependencies = [
"crossbeam-utils",
"globset",
@@ -407,7 +444,7 @@ version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
@@ -428,7 +465,8 @@ dependencies = [
[[package]]
name = "jsonrpc-core"
version = "17.1.0"
source = "git+https://github.com/paritytech/jsonrpc#609d7a6cc160742d035510fa89fb424ccf077660"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4467ab6dfa369b69e52bd0692e480c4d117410538526a57a304a0f2250fd95e"
dependencies = [
"futures-util",
"log",
@@ -445,9 +483,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.95"
version = "0.2.97"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
checksum = "12b8adadd720df158f4d70dfe7ccc6adb0472d7c55ca83445f6a5ab3e36f8fb6"
[[package]]
name = "lock_api"
@@ -464,14 +502,14 @@ version = "0.4.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51b9bbe6c47d51fc3e1a9b945965946b4c44142ab8792c50835a980d362c2710"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
]
[[package]]
name = "lsp-types"
version = "0.89.1"
version = "0.89.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48b8a871b0a450bcec0e26d74a59583c8173cb9fb7d7f98889e18abb84838e0f"
checksum = "852e0dedfd52cc32325598b2631e0eba31b7b708959676a9f837042f276b09a2"
dependencies = [
"bitflags",
"serde",
@@ -500,9 +538,9 @@ checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]]
name = "mio"
version = "0.7.11"
version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956"
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
dependencies = [
"libc",
"log",
@@ -566,9 +604,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.7.2"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "parking_lot"
@@ -587,7 +625,7 @@ version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018"
dependencies = [
"cfg-if",
"cfg-if 1.0.0",
"instant",
"libc",
"redox_syscall",
@@ -595,24 +633,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "pathdiff"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34"
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pico-args"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7afeb98c5a10e0bffcc7fc16e105b04d06729fac5fd6384aebf7ff5cb5a67d"
[[package]]
name = "pin-project-lite"
version = "0.2.6"
@@ -656,9 +682,9 @@ dependencies = [
[[package]]
name = "redox_syscall"
version = "0.2.8"
version = "0.2.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc"
checksum = "5ab49abadf3f9e1c4bc499e8845e152ad87d2ad2d30371841171169e9d75feee"
dependencies = [
"bitflags",
]
@@ -692,13 +718,46 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]]
name = "ropey"
version = "1.2.0"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f3ef16589fdbb3e8fbce3dca944c08e61f39c7f16064b21a257d68ea911a83"
checksum = "9150aff6deb25b20ed110889f070a678bcd1033e46e5e9d6fb1abeab17947f28"
dependencies = [
"smallvec",
]
[[package]]
name = "rust-embed"
version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d"
dependencies = [
"walkdir",
]
[[package]]
name = "ryu"
version = "1.0.5"
@@ -764,20 +823,30 @@ dependencies = [
[[package]]
name = "signal-hook"
version = "0.1.17"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729"
checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
dependencies = [
"libc",
"mio",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.3.0"
name = "signal-hook-mio"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
@@ -805,9 +874,9 @@ checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]]
name = "syn"
version = "1.0.72"
version = "1.0.73"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82"
checksum = "f71489ff30030d2ae598524f61326b902466f72a0fb1a8564c001cc63425bcc7"
dependencies = [
"proc-macro2",
"quote",
@@ -880,9 +949,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.6.1"
version = "1.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975"
checksum = "5fb2ed024293bb19f7a5dc54fe83bf86532a44c12a2bb8ba40d64a4509395ca2"
dependencies = [
"autocfg",
"bytes",
@@ -958,10 +1027,16 @@ dependencies = [
]
[[package]]
name = "unicode-normalization"
version = "0.1.17"
name = "unicode-general-category"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
"tinyvec",
]
@@ -1026,6 +1101,16 @@ version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "which"
version = "4.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b55551e42cbdf2ce2bedd2203d0cc08dba002c27510f86dab6d0ce304cba3dfe"
dependencies = [
"either",
"libc",
]
[[package]]
name = "winapi"
version = "0.3.9"

View File

@@ -13,6 +13,8 @@ myself agreeing with most of kakoune's design decisions.
For more information, see the [website](https://helix-editor.com) or
[documentation](https://docs.helix-editor.com/).
All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html)
# Features
- Vim-like modal editing
@@ -25,7 +27,8 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
# Installation
Note: Only Rust and Golang have indentation definitions at the moment.
Note: Only certain languages have indentation definitions at the moment. Check
`runtime/<lang>/` for `indents.toml`.
We provide packaging for various distributions, but here's a quick method to
build from source.
@@ -38,18 +41,41 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`.
Now copy the `runtime/` directory somewhere. Helix will by default look for the
runtime inside the same folder as the executable, but that can be overriden via
the `HELIX_RUNTIME` environment variable.
Now copy the `runtime/` directory somewhere. Helix will by default look for the runtime
inside the config directory or the same directory as executable, but that can be overriden
via the `HELIX_RUNTIME` environment variable.
> NOTE: running via cargo doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
> detect the `runtime` directory in the project root.
If you want to embed the `runtime/` directory into the Helix binary you can build
it with:
```
cargo install --path helix-term --features "embed_runtime"
```
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## MacOS
Helix can be installed on MacOS through homebrew via:
```
brew tap helix-editor/helix
brew install helix
```
# Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
Some suggestions to get started:
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/good%20first%20issue) label on the issue tracker.
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
- Help with packaging on various distributions needed!
- To use print debugging to the `~/.cache/helix/helix.log` file, you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
- If your preferred language is missing, integrating a tree-sitter grammar for
it and defining syntax highlight queries for it is straight forward and
doesn't require much knowledge of the internals.
@@ -59,5 +85,4 @@ a good overview of the internals.
# Getting help
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-community:matrix.org).
Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet).

19
TODO.md
View File

@@ -1,8 +1,3 @@
- Refactor tree-sitter-highlight to work like the atom one, recomputing partial tree updates.
------
as you type completion!
- tree sitter:
- lua
@@ -15,20 +10,16 @@ as you type completion!
- clojure
- erlang
as you type completion!
- [ ] use signature_help_provider and completion_provider trigger characters in
a hook to trigger signature help text / autocompletion
- [ ] document.on_type provider triggers
- [ ] completion isIncomplete support
- [ ] scroll wheel support
- [ ] matching bracket highlight
1
- [ ] respect view fullscreen flag
- [ ] Implement marks (superset of Selection/Range)
- [ ] nixos packaging
- [ ] = for auto indent line/selection
- [ ] :x for closing buffers
@@ -37,25 +28,19 @@ as you type completion!
- [] jump to alt buffer
- [ ] lsp: signature help
- [x] lsp: hover
- [ ] lsp: document symbols (nested/vec)
- [ ] lsp: code actions
- [ ] lsp: formatting
- [x] lsp: goto
- [ ] search: smart case by default: insensitive unless upper detected
- [ ] move Compositor into tui
2
- [ ] surround bindings (select + surround ( wraps selection in parens )
- [ ] macro recording
- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
- [x] bracket pairs
- [x] comment block (gcc)
- [ ] selection align
- [ ] store some state between restarts: file positions, prompt history
- [ ] highlight matched characters in completion
- [ ] highlight matched characters in picker
3
- [ ] diff mode with highlighting?

View File

@@ -4,3 +4,6 @@ language = "en"
multilingual = false
src = "src"
theme = "colibri"
[output.html]
cname = "docs.helix-editor.com"

View File

@@ -3,5 +3,7 @@
- [Installation](./install.md)
- [Usage](./usage.md)
- [Configuration](./configuration.md)
- [Themes](./themes.md)
- [Keymap](./keymap.md)
- [Key Remapping](./remapping.md)
- [Hooks](./hooks.md)

View File

@@ -1 +1,11 @@
# Configuration
To override global configuration parameters create a `config.toml` file located in your config directory (i.e `~/.config/helix/config.toml`).
## LSP
To display all language server messages in the status line add the following to your `config.toml`:
```toml
[lsp]
display-messages = true
```

View File

@@ -2,13 +2,15 @@
We provide pre-built binaries on the [GitHub Releases page](https://github.com/helix-editor/helix/releases).
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## OSX
TODO: brew tap
A Homebrew tap is available:
```
$ brew tap helix-editor/helix
$ brew install helix
brew tap helix-editor/helix
brew install helix
```
## Linux
@@ -21,7 +23,9 @@ shell for working on Helix.
### Arch Linux
A binary package is available on AUR as [helix-bin](https://aur.archlinux.org/packages/helix-bin/).
Binary packages are available on AUR:
- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
## Build from source
@@ -36,3 +40,10 @@ This will install the `hx` binary to `$HOME/.cargo/bin`.
Now copy the `runtime/` directory somewhere. Helix will by default look for the
runtime inside the same folder as the executable, but that can be overriden via
the `HELIX_RUNTIME` environment variable.
If you want to embed the `runtime/` directory into the Helix binary you can build
it with:
```
cargo install --path helix-term --features "embed_runtime"
```

View File

@@ -4,87 +4,101 @@
### Movement
| Key | Description |
|-----|-----------|
| h | move left |
| j | move down |
| k | move up |
| l | move right |
| w | move next word start |
| b | move previous word start |
| e | move next word end |
| t | find 'till next char |
| f | find next char |
| T | find 'till previous char |
| F | find previous char |
| ^ | move to the start of the line |
| $ | move to the end of the line |
| m | Jump to matching bracket |
| PageUp | Move page up |
| PageDown | Move page down |
| ctrl-u | Move half page up |
| ctrl-d | Move half page down |
| Tab | Switch to next view |
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab |
| ctrl-o | Jump backward on the jumplist |
| v | Enter select (extend) mode |
| g | Enter goto mode |
| : | Enter command mode |
| z | Enter view mode |
| space | Enter space mode |
| K | Show documentation for the item under the cursor |
> NOTE: `f`, `F`, `t` and `T` are not confined to the current line.
| Key | Description |
| ----- | ----------- |
| `h`, `Left` | Move left |
| `j`, `Down` | Move down |
| `k`, `Up` | Move up |
| `l`, `Right` | Move right |
| `w` | Move next word start |
| `b` | Move previous word start |
| `e` | Move next word end |
| `t` | Find 'till next char |
| `f` | Find next char |
| `T` | Find 'till previous char |
| `F` | Find previous char |
| `Home` | Move to the start of the line |
| `End` | Move to the end of the line |
| `PageUp` | Move page up |
| `PageDown` | Move page down |
| `Ctrl-u` | Move half page up |
| `Ctrl-d` | Move half page down |
| `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab |
| `Ctrl-o` | Jump backward on the jumplist |
| `v` | Enter [select (extend) mode](#select--extend-mode) |
| `g` | Enter [goto mode](#goto-mode) |
| `m` | Enter [match mode](#match-mode)
| `:` | Enter command mode |
| `z` | Enter [view mode](#view-mode) |
| `Ctrl-w` | Enter [window mode](#window-mode) (maybe will be remove for spc w w later) |
| `Space` | Enter [space mode](#space-mode) |
| `K` | Show documentation for the item under the cursor |
### Changes
| Key | Description |
|-----|-----------|
| r | replace (single character change) |
| i | Insert before selection |
| a | Insert after selection (append) |
| I | Insert at the start of the line |
| A | Insert at the end of the line |
| o | Open new line below selection |
| o | Open new line above selection |
| u | Undo change |
| U | Redo change |
| y | Yank selection |
| p | Paste after selection |
| P | Paste before selection |
| > | Indent selection |
| < | Unindent selection |
| = | Format selection |
| d | Delete selection |
| c | Change selection (delete and enter insert mode) |
| Key | Description |
| ----- | ----------- |
| `r` | Replace with a character |
| `R` | Replace with yanked text |
| `i` | Insert before selection |
| `a` | Insert after selection (append) |
| `I` | Insert at the start of the line |
| `A` | Insert at the end of the line |
| `o` | Open new line below selection |
| `o` | Open new line above selection |
| `u` | Undo change |
| `U` | Redo change |
| `y` | Yank selection |
| `p` | Paste after selection |
| `P` | Paste before selection |
| `>` | Indent selection |
| `<` | Unindent selection |
| `=` | Format selection |
| `d` | Delete selection |
| `c` | Change selection (delete and enter insert mode) |
### Selection manipulation
| Key | Description |
|-----|-----------|
| s | Select all regex matches inside selections |
| S | Split selection into subselections on regex matches |
| alt-s | Split selection on newlines |
| ; | Collapse selection onto a single cursor |
| alt-; | Flip selection cursor and anchor |
| % | Select entire file |
| x | Select current line |
| X | Extend to next line |
| [ | Expand selection to parent syntax node TODO: pick a key |
| J | join lines inside selection |
| K | keep selections matching the regex TODO: overlapped by hover help |
| space | keep only the primary selection TODO: overlapped by space mode |
| ctrl-c | Comment/uncomment the selections |
| Key | Description |
| ----- | ----------- |
| `s` | Select all regex matches inside selections |
| `S` | Split selection into subselections on regex matches |
| `Alt-s` | Split selection on newlines |
| `;` | Collapse selection onto a single cursor |
| `Alt-;` | Flip selection cursor and anchor |
| `%` | Select entire file |
| `x` | Select current line, if already selected, extend to next line |
| | Expand selection to parent syntax node TODO: pick a key |
| `J` | join lines inside selection |
| `K` | keep selections matching the regex TODO: overlapped by hover help |
| `Space` | keep only the primary selection TODO: overlapped by space mode |
| `Ctrl-c` | Comment/uncomment the selections |
### Search
> TODO: The search implementation isn't ideal yet -- we don't support searching
in reverse, or searching via smartcase.
| Key | Description |
|-----|-----------|
| / | Search for regex pattern |
| n | Select next search match |
| N | Add next search match to selection |
| * | Use current selection as the search pattern |
| Key | Description |
| ----- | ----------- |
| `/` | Search for regex pattern |
| `n` | Select next search match |
| `N` | Add next search match to selection |
| `*` | Use current selection as the search pattern |
### Diagnostics
> NOTE: `[` and `]` will likely contain more pair mappings in the style of
> [vim-unimpaired](https://github.com/tpope/vim-unimpaired)
| Key | Description |
| ----- | ----------- |
| `[d` | Go to previous diagnostic |
| `]d` | Go to next diagnostic |
| `[D` | Go to first diagnostic in document |
| `]D` | Go to last diagnostic in document |
## Select / extend mode
@@ -99,14 +113,14 @@ commands to extend the existing selection instead of replacing it.
View mode is intended for scrolling and manipulating the view without changing
the selection.
| Key | Description |
|-----|-----------|
| z , c | Vertically center the line |
| t | Align the line to the top of the screen |
| b | Align the line to the bottom of the screen |
| m | Align the line to the middle of the screen (horizontally) |
| j | Scroll the view downwards |
| k | Scroll the view upwards |
| Key | Description |
| ----- | ----------- |
| `z` , `c` | Vertically center the line |
| `t` | Align the line to the top of the screen |
| `b` | Align the line to the bottom of the screen |
| `m` | Align the line to the middle of the screen (horizontally) |
| `j` | Scroll the view downwards |
| `k` | Scroll the view upwards |
## Goto mode
@@ -114,28 +128,76 @@ Jumps to various locations.
> NOTE: Some of these features are only available with the LSP present.
| Key | Description |
|-----|-----------|
| g | Go to the start of the file |
| e | Go to the end of the file |
| e | Go to definition |
| t | Go to type definition |
| r | Go to references |
| i | Go to implementation |
| Key | Description |
| ----- | ----------- |
| `g` | Go to the start of the file |
| `e` | Go to the end of the file |
| `h` | Go to the start of the line |
| `l` | Go to the end of the line |
| `s` | Go to first non-whitespace character of the line |
| `t` | Go to the top of the screen |
| `m` | Go to the middle of the screen |
| `b` | Go to the bottom of the screen |
| `d` | Go to definition |
| `y` | Go to type definition |
| `r` | Go to references |
| `i` | Go to implementation |
| `a` | Go to the last accessed/alternate file |
## Match mode
Enter this mode using `m` from normal mode. See the relavant section
in [Usage](./usage.md#surround) for an explanation about surround usage.
| Key | Description |
| ----- | ----------- |
| `m` | Goto matching bracket |
| `s` `<char>` | Surround current selection with `<char>` |
| `r` `<from><to>` | Replace surround character `<from>` with `<to>` |
| `d` `<char>` | Delete surround character `<char>` |
## Object mode
TODO: Mappings for selecting syntax nodes (a superset of `[`).
## Window mode
This layer is similar to vim keybindings as kakoune does not support window.
| Key | Description |
| ----- | ------------- |
| `w`, `Ctrl-w` | Switch to next window |
| `v`, `Ctrl-v` | Vertical right split |
| `h`, `Ctrl-h` | Horizontal bottom split |
| `q`, `Ctrl-q` | Close current window |
## Space mode
This layer is a kludge of mappings I had under leader key in neovim.
| Key | Description |
|-----|-----------|
| f | Open file picker |
| b | Open buffer picker |
| v | Open a new vertical split into the current file |
| w | Save changes to file |
| c | Close the current split |
| space | Keep primary selection TODO: it's here because space mode replaced it |
| Key | Description |
| ----- | ----------- |
| `f` | Open file picker |
| `b` | Open buffer picker |
| `s` | Open symbol picker (current document) |
| `w` | Enter [window mode](#window-mode) |
| `space` | Keep primary selection TODO: it's here because space mode replaced it |
| `p` | paste system clipboard after selections |
| `P` | paste system clipboard before selections |
| `y` | join and yank selections to clipboard |
| `Y` | yank main selection to clipboard |
| `R` | replace selections by clipboard contents |
# Picker
Keys to use within picker.
| Key | Description |
| ----- | ------------- |
| `Up`, `Ctrl-p` | Previous entry |
| `Down`, `Ctrl-n` | Next entry |
| `Ctrl-space` | Filter options |
| `Enter` | Open selected |
| `Ctrl-h` | Open horizontally |
| `Ctrl-v` | Open vertically |
| `Escape`, `Ctrl-c` | Close picker |

50
book/src/remapping.md Normal file
View File

@@ -0,0 +1,50 @@
# Key Remapping
One-way key remapping is temporarily supported via a simple TOML configuration
file. (More powerful solutions such as rebinding via commands will be
available in the feature).
To remap keys, write a `config.toml` file in your `helix` configuration
directory (default `~/.config/helix` in Linux systems) with a structure like
this:
```toml
# At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
[keys.normal]
a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
C-S-esc = "extend_line" # Maps Control-Shift-Escape to extend_line
[keys.insert]
A-x = "normal_mode" # Maps Alt-X to enter normal mode
```
Control, Shift and Alt modifiers are encoded respectively with the prefixes
`C-`, `S-` and `A-`. Special keys are encoded as follows:
| Key name | Representation |
| --- | --- |
| Backspace | `"backspace"` |
| Space | `"space"` |
| Return/Enter | `"ret"` |
| < | `"lt"` |
| \> | `"gt"` |
| \+ | `"plus"` |
| \- | `"minus"` |
| ; | `"semicolon"` |
| % | `"percent"` |
| Left | `"left"` |
| Right | `"right"` |
| Up | `"up"` |
| Home | `"home"` |
| End | `"end"` |
| Page | `"pageup"` |
| Page | `"pagedown"` |
| Tab | `"tab"` |
| Back | `"backtab"` |
| Delete | `"del"` |
| Insert | `"ins"` |
| Null | `"null"` |
| Escape | `"esc"` |
Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)

101
book/src/themes.md Normal file
View File

@@ -0,0 +1,101 @@
# Themes
First you'll need to place selected themes in your `themes` directory (i.e `~/.config/helix/themes`), the directory might have to be created beforehand.
To use a custom theme add `theme = <name>` to your [`config.toml`](./configuration.md) or override it during runtime using `:theme <name>`.
The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes).
## Creating a theme
First create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`).
Each line in the theme file is specified as below:
```toml
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
```
where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
To specify only the foreground color:
```toml
key = "#ffffff"
```
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
```toml
"key.key" = "#ffffff"
```
Possible modifiers:
| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `slow\_blink` |
| `rapid\_blink` |
| `reversed` |
| `hidden` |
| `crossed\_out` |
Possible keys:
| Key | Notes |
| --- | --- |
| `attribute` | |
| `keyword` | |
| `keyword.directive` | Preprocessor directives (\#if in C) |
| `namespace` | |
| `punctuation` | |
| `punctuation.delimiter` | |
| `operator` | |
| `special` | |
| `property` | |
| `variable` | |
| `variable.parameter` | |
| `type` | |
| `type.builtin` | |
| `constructor` | |
| `function` | |
| `function.macro` | |
| `function.builtin` | |
| `comment` | |
| `variable.builtin` | |
| `constant` | |
| `constant.builtin` | |
| `string` | |
| `number` | |
| `escape` | Escaped characters |
| `label` | For lifetimes |
| `module` | |
| `ui.background` | |
| `ui.cursor` | |
| `ui.cursor.insert` | |
| `ui.cursor.select` | |
| `ui.cursor.match` | Matching bracket etc. |
| `ui.cursor.primary` | Cursor with primary selection |
| `ui.linenr` | |
| `ui.statusline` | |
| `ui.statusline.inactive` | |
| `ui.popup` | |
| `ui.window` | |
| `ui.help` | |
| `ui.text` | |
| `ui.text.focus` | |
| `ui.menu.selected` | |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `warning` | LSP warning |
| `error` | LSP error |
| `info` | LSP info |
| `hint` | LSP hint |
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.

View File

@@ -1 +1,26 @@
# Usage
(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
## Surround
Functionality similar to [vim-surround](https://github.com/tpope/vim-surround) is built into
helix. The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich):
![surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif)
- `ms` - Add surround characters
- `mr` - Replace surround characters
- `md` - Delete surround characters
`ms` acts on a selection, so select the text first and use `ms<char>`. `mr` and `md` work
on the closest pairs found and selections are not required; use counts to act in outer pairs.
It can also act on multiple seletions (yay!). For example, to change every occurance of `(use)` to `[use]`:
- `%` to select the whole file
- `s` to split the selections on a search term
- Input `use` and hit Enter
- `mr([` to replace the parens with square brackets
Multiple characters are currently not supported, but planned.

1
contrib/themes Symbolic link
View File

@@ -0,0 +1 @@
../runtime/themes

105
flake.lock generated
View File

@@ -1,73 +1,65 @@
{
"nodes": {
"flake-utils": {
"devshell": {
"locked": {
"lastModified": 1620759905,
"narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=",
"lastModified": 1622711433,
"narHash": "sha256-rGjXz7FA7HImAT3TtoqwecByLO5yhVPSwPdaYPBFRQw=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8",
"repo": "devshell",
"rev": "1f4fb67b662b65fa7cfe696fc003fcc1e8f7cc36",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"repo": "devshell",
"type": "github"
}
},
"flake-utils_2": {
"flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1614513358,
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3",
"lastModified": 1606424373,
"narHash": "sha256-oq8d4//CJOrVj+EcOaSXvMebvuTkmBJuT5tzlfewUnQ=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "99f1c2157fba4bfe6211a321fd0ee43199025dbf",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"naersk": {
"nixCargoIntegration": {
"inputs": {
"nixpkgs": "nixpkgs"
"devshell": "devshell",
"nixpkgs": [
"nixpkgs"
],
"rustOverlay": "rustOverlay"
},
"locked": {
"lastModified": 1620316130,
"narHash": "sha256-sU0VS5oJS1FsHsZsLELAXc7G2eIelVuucRw+q5B1x9k=",
"owner": "nmattia",
"repo": "naersk",
"rev": "a3f40fe42cc6d267ff7518fa3199e99ff1444ac4",
"lastModified": 1624244973,
"narHash": "sha256-h+b4CwPjyibgwMYAeBaT5qBnxI0fsmGf66k23FqEH5Y=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "00f5df6d8e7eeeac2764b7fa2c57e2e81f5d47cd",
"type": "github"
},
"original": {
"owner": "nmattia",
"repo": "naersk",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1622059058,
"narHash": "sha256-t1/ZMtyxClVSfcV4Pt5C1YpkeJ/UwFF3oitLD7Ch/UA=",
"path": "/nix/store/2gam4i1fa1v19k3n5rc9vgvqac1c2xj5-source",
"rev": "84aa23742f6c72501f9cc209f29c438766f5352d",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1622194753,
"narHash": "sha256-76qtvFp/vFEz46lz5iZMJ0mnsWQYmuGYlb0fHgKqqMg=",
"lastModified": 1624024598,
"narHash": "sha256-X++38oH5MKEmPW4/2WdMaHQvwJzO8pJfbnzMD7DbG1E=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "540dccb2aeaffa9dc69bfdc41c55abd7ccc6baa3",
"rev": "33d42ad7cf2769ce6364ed4e52afa8e9d1439d58",
"type": "github"
},
"original": {
@@ -77,40 +69,21 @@
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1617325113,
"narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "54c1e44240d8a527a8f4892608c4bce5440c3ecb",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay"
"flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3"
},
"rustOverlay": {
"flake": false,
"locked": {
"lastModified": 1622257069,
"narHash": "sha256-+QVnS/es9JCRZXphoHL0fOIUhpGqB4/wreBsXWArVck=",
"lastModified": 1624242197,
"narHash": "sha256-J0+j4DYFaE0O0marb4QN/S1bUhpGwAjQ4O04kIYKcb8=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8aa5f93c0b665e5357af19c5631a3450bff4aba5",
"rev": "df5d330f34b64194d64dcbafb91e82e01a89a229",
"type": "github"
},
"original": {

View File

@@ -3,31 +3,61 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
naersk.url = "github:nmattia/naersk";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
};
flakeCompat = {
url = "github:edolstra/flake-compat";
flake = false;
};
};
outputs = inputs@{ self, nixpkgs, naersk, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlay ]; };
rust = (pkgs.rustChannelOf {
date = "2021-05-01";
channel = "nightly";
}).minimal; # cargo, rustc and rust-std
naerskLib = naersk.lib."${system}".override {
# naersk can't build with stable?!
# inherit (pkgs.rust-bin.stable.latest) rustc cargo;
rustc = rust;
cargo = rust;
outputs = inputs@{ self, nixCargoIntegration, ... }:
nixCargoIntegration.lib.makeOutputs {
root = ./.;
buildPlatform = "crate2nix";
renameOutputs = { "helix-term" = "helix"; };
# Set default app to hx (binary is from helix-term release build)
# Set default package to helix-term release build
defaultOutputs = { app = "hx"; package = "helix"; };
overrides = {
crateOverrides = common: _: {
helix-term = prev: {
# link languages and theme toml files since helix-term expects them (for tests)
preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} ..";
buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
};
# link runtime since helix-core expects it because of embed_runtime feature
helix-core = _: { preConfigure = "ln -s ${common.root + "/runtime"} ../runtime"; };
# link languages and theme toml files since helix-view expects them
helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
helix-syntax = prev:
let
helix = common.pkgs.fetchgit {
url = "https://github.com/helix-editor/helix.git";
rev = "9fd17d4ff5b81211317da1a28d2b30442a512ffc";
fetchSubmodules = true;
sha256 = "sha256-y652sn/tCc1XoKr3YxDZv6bS2Cmr6+9K/wzzNAMFZJw=";
};
in
{
src = common.pkgs.runCommand prev.src.name { } ''
mkdir -p $out
ln -s ${prev.src}/* $out
ln -sf ${helix}/helix-syntax/languages $out
'';
};
};
in rec {
packages.helix = naerskLib.buildPackage {
pname = "helix";
root = ./.;
shell = common: prev: {
packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb ]);
env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; }
{ name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
];
};
defaultPackage = packages.helix;
devShell = pkgs.callPackage ./shell.nix {};
});
build = _: prev: { rootFeatures = prev.rootFeatures ++ [ "embed_runtime" ]; };
};
};
}

View File

@@ -1,26 +1,35 @@
[package]
name = "helix-core"
version = "0.1.0"
version = "0.3.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
description = "Helix editor core editing primitives"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
embed_runtime = ["rust-embed"]
[dependencies]
helix-syntax = { path = "../helix-syntax" }
helix-syntax = { version = "0.3", path = "../helix-syntax" }
ropey = "1.2"
ropey = "1.3"
smallvec = "1.4"
tendril = "0.4.2"
unicode-segmentation = "1.6"
unicode-segmentation = "1.7"
unicode-width = "0.1"
unicode-general-category = "0.4"
# slab = "0.4.2"
tree-sitter = "0.19"
once_cell = "1.4"
once_cell = "1.8"
arc-swap = "1"
regex = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
etcetera = "0.3"
rust-embed = { version = "5.9.0", optional = true }

View File

@@ -12,7 +12,7 @@ pub const PAIRS: &[(char, char)] = &[
('`', '`'),
];
const CLOSE_BEFORE: &str = ")]}'\":;> \n"; // includes space and newline
const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines
// insert hook:
// Fn(doc, selection, char) => Option<Transaction>
@@ -67,7 +67,7 @@ fn handle_open(
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);
@@ -109,7 +109,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);

133
helix-core/src/chars.rs Normal file
View File

@@ -0,0 +1,133 @@
use crate::LineEnding;
#[derive(Debug, Eq, PartialEq)]
pub enum CharCategory {
Whitespace,
Eol,
Word,
Punctuation,
Unknown,
}
#[inline]
pub fn categorize_char(ch: char) -> CharCategory {
if char_is_line_ending(ch) {
CharCategory::Eol
} else if ch.is_whitespace() {
CharCategory::Whitespace
} else if char_is_word(ch) {
CharCategory::Word
} else if char_is_punctuation(ch) {
CharCategory::Punctuation
} else {
CharCategory::Unknown
}
}
/// Determine whether a character is a line ending.
#[inline]
pub fn char_is_line_ending(ch: char) -> bool {
LineEnding::from_char(ch).is_some()
}
/// Determine whether a character qualifies as (non-line-break)
/// whitespace.
#[inline]
pub fn char_is_whitespace(ch: char) -> bool {
// TODO: this is a naive binary categorization of whitespace
// characters. For display, word wrapping, etc. we'll need a better
// categorization based on e.g. breaking vs non-breaking spaces
// and whether they're zero-width or not.
match ch {
//'\u{1680}' | // Ogham Space Mark (here for completeness, but usually displayed as a dash, not as whitespace)
'\u{0009}' | // Character Tabulation
'\u{0020}' | // Space
'\u{00A0}' | // No-break Space
'\u{180E}' | // Mongolian Vowel Separator
'\u{202F}' | // Narrow No-break Space
'\u{205F}' | // Medium Mathematical Space
'\u{3000}' | // Ideographic Space
'\u{FEFF}' // Zero Width No-break Space
=> true,
// En Quad, Em Quad, En Space, Em Space, Three-per-em Space,
// Four-per-em Space, Six-per-em Space, Figure Space,
// Punctuation Space, Thin Space, Hair Space, Zero Width Space.
ch if ('\u{2000}' ..= '\u{200B}').contains(&ch) => true,
_ => false,
}
}
#[inline]
pub fn char_is_punctuation(ch: char) -> bool {
use unicode_general_category::{get_general_category, GeneralCategory};
matches!(
get_general_category(ch),
GeneralCategory::OtherPunctuation
| GeneralCategory::OpenPunctuation
| GeneralCategory::ClosePunctuation
| GeneralCategory::InitialPunctuation
| GeneralCategory::FinalPunctuation
| GeneralCategory::ConnectorPunctuation
| GeneralCategory::DashPunctuation
| GeneralCategory::MathSymbol
| GeneralCategory::CurrencySymbol
| GeneralCategory::ModifierSymbol
)
}
#[inline]
pub fn char_is_word(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn test_categorize() {
const EOL_TEST_CASE: &'static str = "\n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}";
const WORD_TEST_CASE: &'static str =
"_hello_world_あいうえおー1234567890";
const PUNCTUATION_TEST_CASE: &'static str =
"!\"#$%&\'()*+,-./:;<=>?@[\\]^`{|}~!”#$%&’()*+、。:;<=>?@「」^`{|}~";
const WHITESPACE_TEST_CASE: &'static str = "  ";
for ch in EOL_TEST_CASE.chars() {
assert_eq!(CharCategory::Eol, categorize_char(ch));
}
for ch in WHITESPACE_TEST_CASE.chars() {
assert_eq!(
CharCategory::Whitespace,
categorize_char(ch),
"Testing '{}', but got `{:?}` instead of `Category::Whitespace`",
ch,
categorize_char(ch)
);
}
for ch in WORD_TEST_CASE.chars() {
assert_eq!(
CharCategory::Word,
categorize_char(ch),
"Testing '{}', but got `{:?}` instead of `Category::Word`",
ch,
categorize_char(ch)
);
}
for ch in PUNCTUATION_TEST_CASE.chars() {
assert_eq!(
CharCategory::Punctuation,
categorize_char(ch),
"Testing '{}', but got `{:?}` instead of `Category::Punctuation`",
ch,
categorize_char(ch)
);
}
}
}

View File

@@ -1,5 +1,5 @@
use crate::{
find_first_non_whitespace_char2, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
};
use core::ops::Range;
use std::borrow::Cow;
@@ -14,7 +14,7 @@ fn find_line_comment(
let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
for line in lines {
let line_slice = text.line(line);
if let Some(pos) = find_first_non_whitespace_char2(line_slice) {
if let Some(pos) = find_first_non_whitespace_char(line_slice) {
let len = line_slice.len_chars();
if pos < min {
@@ -51,7 +51,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection) -> Transaction {
let lines = start..end + 1;
let (commented, skipped, min) = find_line_comment(token, text, lines.clone());
changes.reserve(end - start - skipped.len());
changes.reserve((end - start).saturating_sub(skipped.len()));
for line in lines {
if skipped.contains(&line) {

View File

@@ -1,4 +1,4 @@
#[derive(Eq, PartialEq)]
#[derive(Debug, Eq, PartialEq)]
pub enum Severity {
Error,
Warning,
@@ -6,10 +6,13 @@ pub enum Severity {
Hint,
}
#[derive(Debug)]
pub struct Range {
pub start: usize,
pub end: usize,
}
#[derive(Debug)]
pub struct Diagnostic {
pub range: Range,
pub line: usize,

View File

@@ -3,6 +3,8 @@ use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
use std::fmt;
#[must_use]
pub fn grapheme_width(g: &str) -> usize {
if g.as_bytes()[0] <= 127 {
@@ -119,6 +121,16 @@ pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
nth_next_grapheme_boundary(slice, char_idx, 1)
}
/// Returns the passed char index if it's already a grapheme boundary,
/// or the next grapheme boundary char index if not.
pub fn ensure_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
if char_idx == 0 {
0
} else {
next_grapheme_boundary(slice, char_idx - 1)
}
}
/// Returns whether the given char position is a grapheme boundary.
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
// Bounds check
@@ -156,6 +168,18 @@ pub struct RopeGraphemes<'a> {
cursor: GraphemeCursor,
}
impl<'a> fmt::Debug for RopeGraphemes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
// .field("cursor", &self.cursor)
.finish()
}
}
impl<'a> RopeGraphemes<'a> {
#[must_use]
pub fn new(slice: RopeSlice) -> RopeGraphemes {
@@ -193,6 +217,10 @@ impl<'a> Iterator for RopeGraphemes<'a> {
self.cur_chunk_start += self.cur_chunk.len();
self.cur_chunk = self.chunks.next().unwrap_or("");
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}

View File

@@ -1,19 +1,61 @@
use crate::{ChangeSet, Rope, State, Transaction};
use once_cell::sync::Lazy;
use regex::Regex;
use smallvec::{smallvec, SmallVec};
use std::num::NonZeroUsize;
use std::time::{Duration, Instant};
/// Undo-tree style history store.
// Stores the history of changes to a buffer.
//
// Currently the history is represented as a vector of revisions. The vector
// always has at least one element: the empty root revision. Each revision
// with the exception of the root has a parent revision, a [Transaction]
// that can be applied to its parent to transition from the parent to itself,
// and an inversion of that transaction to transition from the parent to its
// latest child.
//
// When using `u` to undo a change, an inverse of the stored transaction will
// be applied which will transition the buffer to the parent state.
//
// Each revision with the exception of the last in the vector also has a
// last child revision. When using `U` to redo a change, the last child transaction
// will be applied to the current state of the buffer.
//
// The current revision is the one currently displayed in the buffer.
//
// Commiting a new revision to the history will update the last child of the
// current revision, and push a new revision to the end of the vector.
//
// Revisions are commited with a timestamp. :earlier and :later can be used
// to jump to the closest revision to a moment in time relative to the timestamp
// of the current revision plus (:later) or minus (:earlier) the duration
// given to the command. If a single integer is given, the editor will instead
// jump the given number of revisions in the vector.
//
// Limitations:
// * Changes in selections currently don't commit history changes. The selection
// will only be updated to the state after a commited buffer change.
// * The vector of history revisions is currently unbounded. This might
// cause the memory consumption to grow significantly large during long
// editing sessions.
// * Because delete transactions currently don't store the text that they
// delete, we also store an inversion of the transaction.
#[derive(Debug)]
pub struct History {
revisions: Vec<Revision>,
cursor: usize,
current: usize,
}
// A single point in history. See [History] for more information.
#[derive(Debug)]
struct Revision {
parent: usize,
children: SmallVec<[(usize, Transaction); 1]>,
/// The transaction to revert to previous state.
revert: Transaction,
// selection before, selection after?
last_child: Option<NonZeroUsize>,
transaction: Transaction,
// We need an inversion for undos because delete transactions don't store
// the deleted text.
inversion: Transaction,
timestamp: Instant,
}
impl Default for History {
@@ -22,74 +64,253 @@ impl Default for History {
Self {
revisions: vec![Revision {
parent: 0,
children: SmallVec::new(),
revert: Transaction::from(ChangeSet::new(&Rope::new())),
last_child: None,
transaction: Transaction::from(ChangeSet::new(&Rope::new())),
inversion: Transaction::from(ChangeSet::new(&Rope::new())),
timestamp: Instant::now(),
}],
cursor: 0,
current: 0,
}
}
}
impl History {
pub fn commit_revision(&mut self, transaction: &Transaction, original: &State) {
// TODO: could store a single transaction, if deletes also stored the text they delete
let revert = transaction
self.commit_revision_at_timestamp(transaction, original, Instant::now());
}
pub fn commit_revision_at_timestamp(
&mut self,
transaction: &Transaction,
original: &State,
timestamp: Instant,
) {
let inversion = transaction
.invert(&original.doc)
// Store the current cursor position
.with_selection(original.selection.clone());
let new_cursor = self.revisions.len();
let new_current = self.revisions.len();
self.revisions[self.current].last_child = NonZeroUsize::new(new_current);
self.revisions.push(Revision {
parent: self.cursor,
children: SmallVec::new(),
revert,
parent: self.current,
last_child: None,
transaction: transaction.clone(),
inversion,
timestamp,
});
// add a reference to the parent
self.revisions
.get_mut(self.cursor)
.unwrap() // TODO: get_unchecked_mut
.children
.push((new_cursor, transaction.clone()));
self.cursor = new_cursor;
self.current = new_current;
}
#[inline]
pub fn current_revision(&self) -> usize {
self.cursor
self.current
}
#[inline]
pub const fn at_root(&self) -> bool {
self.cursor == 0
self.current == 0
}
// TODO: I'd like to pass Transaction by reference but it fights with the borrowck
pub fn undo(&mut self) -> Option<Transaction> {
pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() {
// We're at the root of undo, nothing to do.
return None;
}
let current_revision = &self.revisions[self.cursor];
self.cursor = current_revision.parent;
Some(current_revision.revert.clone())
let current_revision = &self.revisions[self.current];
self.current = current_revision.parent;
Some(&current_revision.inversion)
}
pub fn redo(&mut self) -> Option<Transaction> {
let current_revision = &self.revisions[self.cursor];
pub fn redo(&mut self) -> Option<&Transaction> {
let current_revision = &self.revisions[self.current];
let last_child = current_revision.last_child?;
self.current = last_child.get();
// for now, simply pick the latest child (linear undo / redo)
if let Some((index, transaction)) = current_revision.children.last() {
self.cursor = *index;
let last_child_revision = &self.revisions[last_child.get()];
Some(&self.revisions[last_child.get()].transaction)
}
return Some(transaction.clone());
fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
use std::collections::HashSet;
let mut a_path_set = HashSet::new();
let mut b_path_set = HashSet::new();
loop {
a_path_set.insert(a);
b_path_set.insert(b);
if a_path_set.contains(&b) {
return b;
}
if b_path_set.contains(&a) {
return a;
}
a = self.revisions[a].parent; // Relies on the parent of 0 being 0.
b = self.revisions[b].parent; // Same as above.
}
}
// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
let mut path = Vec::new();
while n != a {
path.push(n);
n = self.revisions[n].parent;
}
path
}
fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
let lca = self.lowest_common_ancestor(self.current, to);
let up = self.path_up(self.current, lca);
let down = self.path_up(to, lca);
self.current = to;
let up_txns = up.iter().map(|&n| self.revisions[n].inversion.clone());
let down_txns = down
.iter()
.rev()
.map(|&n| self.revisions[n].transaction.clone());
up_txns.chain(down_txns).collect()
}
fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
self.jump_to(self.current.saturating_sub(delta))
}
fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
self.jump_to(
self.current
.saturating_add(delta)
.min(self.revisions.len() - 1),
)
}
// Helper for a binary search case below.
fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
let dur_i = self.revisions[i].timestamp.duration_since(instant);
use std::cmp::Ordering::*;
match dur_im1.cmp(&dur_i) {
Less => i - 1,
Equal | Greater => i,
}
}
fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
let search_result = self
.revisions
.binary_search_by(|rev| rev.timestamp.cmp(&instant));
let revision = match search_result {
Ok(revision) => revision,
Err(insert_point) => match insert_point {
0 => 0,
n if n == self.revisions.len() => n - 1,
i => self.revision_closer_to_instant(i, instant),
},
};
self.jump_to(revision)
}
fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
match self.revisions[self.current].timestamp.checked_sub(duration) {
Some(instant) => self.jump_instant(instant),
None => self.jump_to(0),
}
}
fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
match self.revisions[self.current].timestamp.checked_add(duration) {
Some(instant) => self.jump_instant(instant),
None => self.jump_to(self.revisions.len() - 1),
}
}
pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
use UndoKind::*;
match uk {
Steps(n) => self.jump_backward(n),
TimePeriod(d) => self.jump_duration_backward(d),
}
}
pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
use UndoKind::*;
match uk {
Steps(n) => self.jump_forward(n),
TimePeriod(d) => self.jump_duration_forward(d),
}
}
}
#[derive(Debug, PartialEq)]
pub enum UndoKind {
Steps(usize),
TimePeriod(std::time::Duration),
}
// A subset of sytemd.time time span syntax units.
const TIME_UNITS: &[(&[&str], &str, u64)] = &[
(&["seconds", "second", "sec", "s"], "seconds", 1),
(&["minutes", "minute", "min", "m"], "minutes", 60),
(&["hours", "hour", "hr", "h"], "hours", 60 * 60),
(&["days", "day", "d"], "days", 24 * 60 * 60),
];
static DURATION_VALIDATION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
fn parse_human_duration(s: &str) -> Result<Duration, String> {
if !DURATION_VALIDATION_REGEX.is_match(s) {
return Err("duration should be composed \
of positive integers followed by time units"
.to_string());
}
let mut specified = [false; TIME_UNITS.len()];
let mut seconds = 0u64;
for cap in NUMBER_UNIT_REGEX.captures_iter(s) {
let (n, unit_str) = (&cap[1], &cap[2]);
let n: u64 = n.parse().map_err(|_| format!("integer too large: {}", n))?;
let time_unit = TIME_UNITS
.iter()
.enumerate()
.find(|(_, (forms, _, _))| forms.iter().any(|f| f == &unit_str));
if let Some((i, (_, unit, mul))) = time_unit {
if specified[i] {
return Err(format!("{} specified more than once", unit));
}
specified[i] = true;
let new_seconds = n.checked_mul(*mul).and_then(|s| seconds.checked_add(s));
match new_seconds {
Some(ns) => seconds = ns,
None => return Err("duration too large".to_string()),
}
} else {
return Err(format!("incorrect time unit: {}", unit_str));
}
}
Ok(Duration::from_secs(seconds))
}
impl std::str::FromStr for UndoKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
Ok(Self::Steps(1usize))
} else if let Ok(n) = s.parse::<usize>() {
Ok(UndoKind::Steps(n))
} else {
Ok(Self::TimePeriod(parse_human_duration(s)?))
}
None
}
}
@@ -145,4 +366,191 @@ mod test {
undo(&mut history, &mut state);
assert_eq!("hello", state.doc);
}
#[test]
fn test_earlier_later() {
let mut history = History::default();
let doc = Rope::from("a\n");
let mut state = State::new(doc);
fn undo(history: &mut History, state: &mut State) {
if let Some(transaction) = history.undo() {
transaction.apply(&mut state.doc);
}
};
fn earlier(history: &mut History, state: &mut State, uk: UndoKind) {
let txns = history.earlier(uk);
for txn in txns {
txn.apply(&mut state.doc);
}
};
fn later(history: &mut History, state: &mut State, uk: UndoKind) {
let txns = history.later(uk);
for txn in txns {
txn.apply(&mut state.doc);
}
};
fn commit_change(
history: &mut History,
state: &mut State,
change: crate::transaction::Change,
instant: Instant,
) {
let txn = Transaction::change(&state.doc, vec![change.clone()].into_iter());
history.commit_revision_at_timestamp(&txn, &state, instant);
txn.apply(&mut state.doc);
};
let t0 = Instant::now();
let t = |n| t0.checked_add(Duration::from_secs(n)).unwrap();
commit_change(&mut history, &mut state, (1, 1, Some(" b".into())), t(0));
assert_eq!("a b\n", state.doc);
commit_change(&mut history, &mut state, (3, 3, Some(" c".into())), t(10));
assert_eq!("a b c\n", state.doc);
commit_change(&mut history, &mut state, (5, 5, Some(" d".into())), t(20));
assert_eq!("a b c d\n", state.doc);
undo(&mut history, &mut state);
assert_eq!("a b c\n", state.doc);
commit_change(&mut history, &mut state, (5, 5, Some(" e".into())), t(30));
assert_eq!("a b c e\n", state.doc);
undo(&mut history, &mut state);
undo(&mut history, &mut state);
assert_eq!("a b\n", state.doc);
commit_change(&mut history, &mut state, (1, 3, None), t(40));
assert_eq!("a\n", state.doc);
commit_change(&mut history, &mut state, (1, 1, Some(" f".into())), t(50));
assert_eq!("a f\n", state.doc);
use UndoKind::*;
earlier(&mut history, &mut state, Steps(3));
assert_eq!("a b c d\n", state.doc);
later(&mut history, &mut state, TimePeriod(Duration::new(20, 0)));
assert_eq!("a\n", state.doc);
earlier(&mut history, &mut state, TimePeriod(Duration::new(19, 0)));
assert_eq!("a b c d\n", state.doc);
earlier(
&mut history,
&mut state,
TimePeriod(Duration::new(10000, 0)),
);
assert_eq!("a\n", state.doc);
later(&mut history, &mut state, Steps(50));
assert_eq!("a f\n", state.doc);
earlier(&mut history, &mut state, Steps(4));
assert_eq!("a b c\n", state.doc);
later(&mut history, &mut state, TimePeriod(Duration::new(1, 0)));
assert_eq!("a b c\n", state.doc);
later(&mut history, &mut state, TimePeriod(Duration::new(5, 0)));
assert_eq!("a b c d\n", state.doc);
later(&mut history, &mut state, TimePeriod(Duration::new(6, 0)));
assert_eq!("a b c e\n", state.doc);
later(&mut history, &mut state, Steps(1));
assert_eq!("a\n", state.doc);
}
#[test]
fn test_parse_undo_kind() {
use UndoKind::*;
// Default is one step.
assert_eq!("".parse(), Ok(Steps(1)));
// An integer means the number of steps.
assert_eq!("1".parse(), Ok(Steps(1)));
assert_eq!(" 16 ".parse(), Ok(Steps(16)));
// Duration has a strict format.
let validation_err = Err("duration should be composed \
of positive integers followed by time units"
.to_string());
assert_eq!(" 16 33".parse::<UndoKind>(), validation_err);
assert_eq!(" seconds 22 ".parse::<UndoKind>(), validation_err);
assert_eq!(" -4 m".parse::<UndoKind>(), validation_err);
assert_eq!("5s 3".parse::<UndoKind>(), validation_err);
// Units are u64.
assert_eq!(
"18446744073709551616minutes".parse::<UndoKind>(),
Err("integer too large: 18446744073709551616".to_string())
);
// Units are validated.
assert_eq!(
"1 millenium".parse::<UndoKind>(),
Err("incorrect time unit: millenium".to_string())
);
// Units can't be specified twice.
assert_eq!(
"2 seconds 6s".parse::<UndoKind>(),
Err("seconds specified more than once".to_string())
);
// Various formats are correctly handled.
assert_eq!(
"4s".parse::<UndoKind>(),
Ok(TimePeriod(Duration::from_secs(4)))
);
assert_eq!(
"2m".parse::<UndoKind>(),
Ok(TimePeriod(Duration::from_secs(120)))
);
assert_eq!(
"5h".parse::<UndoKind>(),
Ok(TimePeriod(Duration::from_secs(5 * 60 * 60)))
);
assert_eq!(
"3d".parse::<UndoKind>(),
Ok(TimePeriod(Duration::from_secs(3 * 24 * 60 * 60)))
);
assert_eq!(
"1m30s".parse::<UndoKind>(),
Ok(TimePeriod(Duration::from_secs(90)))
);
assert_eq!(
"1m 20 seconds".parse::<UndoKind>(),
Ok(TimePeriod(Duration::from_secs(80)))
);
assert_eq!(
" 2 minute 1day".parse::<UndoKind>(),
Ok(TimePeriod(Duration::from_secs(24 * 60 * 60 + 2 * 60)))
);
assert_eq!(
"3 d 2hour 5 minutes 30sec".parse::<UndoKind>(),
Ok(TimePeriod(Duration::from_secs(
3 * 24 * 60 * 60 + 2 * 60 * 60 + 5 * 60 + 30
)))
);
// Sum overflow is handled.
assert_eq!(
"18446744073709551615minutes".parse::<UndoKind>(),
Err("duration too large".to_string())
);
assert_eq!(
"1 minute 18446744073709551615 seconds".parse::<UndoKind>(),
Err("duration too large".to_string())
);
}
}

View File

@@ -105,11 +105,14 @@ fn suggested_indent_for_line(
line_num: usize,
tab_width: usize,
) -> usize {
let line = text.line(line_num);
let current = indent_level_for_line(line, tab_width);
if let Some(start) = find_first_non_whitespace_char(text, line_num) {
return suggested_indent_for_pos(Some(language_config), syntax, text, start, false);
if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
return suggested_indent_for_pos(
Some(language_config),
syntax,
text,
start + text.line_to_char(line_num),
false,
);
};
// if the line is blank, indent should be zero
@@ -259,6 +262,7 @@ where
highlight_config: OnceCell::new(),
//
roots: vec![],
auto_format: false,
language_server: None,
indent: Some(IndentationConfiguration {
tab_width: 4,

View File

@@ -1,10 +1,12 @@
#![allow(unused)]
pub mod auto_pairs;
pub mod chars;
pub mod comment;
pub mod diagnostic;
pub mod graphemes;
mod history;
pub mod history;
pub mod indent;
pub mod line_ending;
pub mod macros;
pub mod match_brackets;
pub mod movement;
@@ -14,71 +16,106 @@ pub mod register;
pub mod search;
pub mod selection;
mod state;
pub mod surround;
pub mod syntax;
mod transaction;
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
// find first non-whitespace char
for (start, ch) in line.chars().enumerate() {
// TODO: could use memchr with chunks?
if ch != ' ' && ch != '\t' && ch != '\n' {
return Some(start);
}
}
None
pub mod unicode {
pub use unicode_general_category as category;
pub use unicode_segmentation as segmentation;
pub use unicode_width as width;
}
pub(crate) fn find_first_non_whitespace_char(text: RopeSlice, line_num: usize) -> Option<usize> {
let line = text.line(line_num);
let mut start = text.line_to_char(line_num);
// find first non-whitespace char
for ch in line.chars() {
// TODO: could use memchr with chunks?
if ch != ' ' && ch != '\t' && ch != '\n' {
return Some(start);
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
once_cell::sync::Lazy::new(runtime_dir);
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}
pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let root = match root {
Some(root) => {
let root = std::path::Path::new(root);
if root.is_absolute() {
root.to_path_buf()
} else {
current_dir.join(root)
}
}
start += 1;
}
None => current_dir,
};
for ancestor in root.ancestors() {
// TODO: also use defined roots if git isn't found
if ancestor.join(".git").is_dir() {
return Some(ancestor.to_path_buf());
}
}
None
}
#[cfg(not(embed_runtime))]
pub fn runtime_dir() -> std::path::PathBuf {
// runtime env var || dir where binary is located
std::env::var("HELIX_RUNTIME")
.map(|path| path.into())
.unwrap_or_else(|_| {
std::env::current_exe()
.ok()
.and_then(|path| path.parent().map(|path| path.to_path_buf()))
.unwrap()
})
if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
return dir.into();
}
const RT_DIR: &str = "runtime";
let conf_dir = config_dir().join(RT_DIR);
if conf_dir.exists() {
return conf_dir;
}
if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") {
// this is the directory of the crate being run by cargo, we need the workspace path so we take the parent
return std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR);
}
// fallback to location of the executable being run
std::env::current_exe()
.ok()
.and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
.unwrap()
}
pub fn config_dir() -> std::path::PathBuf {
// TODO: allow env var override
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let mut path = strategy.config_dir();
path.push("helix");
path
}
pub use ropey::{Rope, RopeSlice};
pub fn cache_dir() -> std::path::PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let mut path = strategy.cache_dir();
path.push("helix");
path
}
pub use etcetera::home_dir;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
pub use ropey::{Rope, RopeBuilder, RopeSlice};
pub use tendril::StrTendril as Tendril;
#[doc(inline)]
pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes;
pub use position::{coords_at_pos, pos_at_coords, Position};
pub use selection::{Range, Selection};
pub use smallvec::SmallVec;
pub use syntax::Syntax;
pub use diagnostic::Diagnostic;
pub use history::History;
pub use state::State;
pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};

View File

@@ -0,0 +1,258 @@
use crate::{Rope, RopeGraphemes, RopeSlice};
#[cfg(target_os = "windows")]
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::Crlf;
#[cfg(not(target_os = "windows"))]
pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::LF;
/// Represents one of the valid Unicode line endings.
#[derive(PartialEq, Copy, Clone, Debug)]
pub enum LineEnding {
Crlf, // CarriageReturn followed by LineFeed
LF, // U+000A -- LineFeed
VT, // U+000B -- VerticalTab
FF, // U+000C -- FormFeed
CR, // U+000D -- CarriageReturn
Nel, // U+0085 -- NextLine
LS, // U+2028 -- Line Separator
PS, // U+2029 -- ParagraphSeparator
}
impl LineEnding {
#[inline]
pub fn len_chars(&self) -> usize {
match self {
Self::Crlf => 2,
_ => 1,
}
}
#[inline]
pub fn as_str(&self) -> &'static str {
match self {
Self::Crlf => "\u{000D}\u{000A}",
Self::LF => "\u{000A}",
Self::VT => "\u{000B}",
Self::FF => "\u{000C}",
Self::CR => "\u{000D}",
Self::Nel => "\u{0085}",
Self::LS => "\u{2028}",
Self::PS => "\u{2029}",
}
}
#[inline]
pub fn from_char(ch: char) -> Option<LineEnding> {
match ch {
'\u{000A}' => Some(LineEnding::LF),
'\u{000B}' => Some(LineEnding::VT),
'\u{000C}' => Some(LineEnding::FF),
'\u{000D}' => Some(LineEnding::CR),
'\u{0085}' => Some(LineEnding::Nel),
'\u{2028}' => Some(LineEnding::LS),
'\u{2029}' => Some(LineEnding::PS),
// Not a line ending
_ => None,
}
}
// Normally we'd want to implement the FromStr trait, but in this case
// that would force us into a different return type than from_char or
// or from_rope_slice, which would be weird.
#[allow(clippy::should_implement_trait)]
#[inline]
pub fn from_str(g: &str) -> Option<LineEnding> {
match g {
"\u{000D}\u{000A}" => Some(LineEnding::Crlf),
"\u{000A}" => Some(LineEnding::LF),
"\u{000B}" => Some(LineEnding::VT),
"\u{000C}" => Some(LineEnding::FF),
"\u{000D}" => Some(LineEnding::CR),
"\u{0085}" => Some(LineEnding::Nel),
"\u{2028}" => Some(LineEnding::LS),
"\u{2029}" => Some(LineEnding::PS),
// Not a line ending
_ => None,
}
}
#[inline]
pub fn from_rope_slice(g: &RopeSlice) -> Option<LineEnding> {
if let Some(text) = g.as_str() {
LineEnding::from_str(text)
} else {
// Non-contiguous, so it can't be a line ending.
// Specifically, Ropey guarantees that CRLF is always
// contiguous. And the remaining line endings are all
// single `char`s, and therefore trivially contiguous.
None
}
}
}
#[inline]
pub fn str_is_line_ending(s: &str) -> bool {
LineEnding::from_str(s).is_some()
}
/// Attempts to detect what line ending the passed document uses.
pub fn auto_detect_line_ending(doc: &Rope) -> Option<LineEnding> {
// Return first matched line ending. Not all possible line endings
// are being matched, as they might be special-use only
for line in doc.lines().take(100) {
match get_line_ending(&line) {
None | Some(LineEnding::VT) | Some(LineEnding::FF) | Some(LineEnding::PS) => {}
ending => return ending,
}
}
None
}
/// Returns the passed line's line ending, if any.
pub fn get_line_ending(line: &RopeSlice) -> Option<LineEnding> {
// Last character as str.
let g1 = line
.slice(line.len_chars().saturating_sub(1)..)
.as_str()
.unwrap();
// Last two characters as str, or empty str if they're not contiguous.
// It's fine to punt on the non-contiguous case, because Ropey guarantees
// that CRLF is always contiguous.
let g2 = line
.slice(line.len_chars().saturating_sub(2)..)
.as_str()
.unwrap_or("");
// First check the two-character case for CRLF, then check the single-character case.
LineEnding::from_str(g2).or_else(|| LineEnding::from_str(g1))
}
/// Returns the passed line's line ending, if any.
pub fn get_line_ending_of_str(line: &str) -> Option<LineEnding> {
if line.ends_with("\u{000D}\u{000A}") {
Some(LineEnding::Crlf)
} else if line.ends_with('\u{000A}') {
Some(LineEnding::LF)
} else if line.ends_with('\u{000B}') {
Some(LineEnding::VT)
} else if line.ends_with('\u{000C}') {
Some(LineEnding::FF)
} else if line.ends_with('\u{000D}') {
Some(LineEnding::CR)
} else if line.ends_with('\u{0085}') {
Some(LineEnding::Nel)
} else if line.ends_with('\u{2028}') {
Some(LineEnding::LS)
} else if line.ends_with('\u{2029}') {
Some(LineEnding::PS)
} else {
None
}
}
/// Returns the char index of the end of the given line, not including its line ending.
pub fn line_end_char_index(slice: &RopeSlice, line: usize) -> usize {
slice.line_to_char(line + 1)
- get_line_ending(&slice.line(line))
.map(|le| le.len_chars())
.unwrap_or(0)
}
/// Returns the char index of the end of the given RopeSlice, not including
/// any final line ending.
pub fn rope_end_without_line_ending(slice: &RopeSlice) -> usize {
slice.len_chars() - get_line_ending(slice).map(|le| le.len_chars()).unwrap_or(0)
}
#[cfg(test)]
mod line_ending_tests {
use super::*;
#[test]
fn line_ending_autodetect() {
assert_eq!(
auto_detect_line_ending(&Rope::from_str("\n")),
Some(LineEnding::LF)
);
assert_eq!(
auto_detect_line_ending(&Rope::from_str("\r\n")),
Some(LineEnding::Crlf)
);
assert_eq!(auto_detect_line_ending(&Rope::from_str("hello")), None);
assert_eq!(auto_detect_line_ending(&Rope::from_str("")), None);
assert_eq!(
auto_detect_line_ending(&Rope::from_str("hello\nhelix\r\n")),
Some(LineEnding::LF)
);
assert_eq!(
auto_detect_line_ending(&Rope::from_str("a formfeed\u{000C}")),
None
);
assert_eq!(
auto_detect_line_ending(&Rope::from_str("\n\u{000A}\n \u{000A}")),
Some(LineEnding::LF)
);
assert_eq!(
auto_detect_line_ending(&Rope::from_str(
"a formfeed\u{000C} with a\u{000C} linefeed\u{000A}"
)),
Some(LineEnding::LF)
);
assert_eq!(auto_detect_line_ending(&Rope::from_str("a formfeed\u{000C} with a\u{000C} carriage return linefeed\u{000D}\u{000A} and a linefeed\u{000A}")), Some(LineEnding::Crlf));
}
#[test]
fn str_to_line_ending() {
assert_eq!(LineEnding::from_str("\r"), Some(LineEnding::CR));
assert_eq!(LineEnding::from_str("\n"), Some(LineEnding::LF));
assert_eq!(LineEnding::from_str("\r\n"), Some(LineEnding::Crlf));
assert_eq!(LineEnding::from_str("hello\n"), None);
}
#[test]
fn rope_slice_to_line_ending() {
let r = Rope::from_str("hello\r\n");
assert_eq!(
LineEnding::from_rope_slice(&r.slice(5..6)),
Some(LineEnding::CR)
);
assert_eq!(
LineEnding::from_rope_slice(&r.slice(6..7)),
Some(LineEnding::LF)
);
assert_eq!(
LineEnding::from_rope_slice(&r.slice(5..7)),
Some(LineEnding::Crlf)
);
assert_eq!(LineEnding::from_rope_slice(&r.slice(..)), None);
}
#[test]
fn get_line_ending_rope_slice() {
let r = Rope::from_str("Hello\rworld\nhow\r\nare you?");
assert_eq!(get_line_ending(&r.slice(..6)), Some(LineEnding::CR));
assert_eq!(get_line_ending(&r.slice(..12)), Some(LineEnding::LF));
assert_eq!(get_line_ending(&r.slice(..17)), Some(LineEnding::Crlf));
assert_eq!(get_line_ending(&r.slice(..)), None);
}
#[test]
fn get_line_ending_str() {
let text = "Hello\rworld\nhow\r\nare you?";
assert_eq!(get_line_ending_of_str(&text[..6]), Some(LineEnding::CR));
assert_eq!(get_line_ending_of_str(&text[..12]), Some(LineEnding::LF));
assert_eq!(get_line_ending_of_str(&text[..17]), Some(LineEnding::Crlf));
assert_eq!(get_line_ending_of_str(&text[..]), None);
}
#[test]
fn line_end_char_index_rope_slice() {
let r = Rope::from_str("Hello\rworld\nhow\r\nare you?");
let s = &r.slice(..);
assert_eq!(line_end_char_index(s, 0), 5);
assert_eq!(line_end_char_index(s, 1), 11);
assert_eq!(line_end_char_index(s, 2), 15);
assert_eq!(line_end_char_index(s, 3), 25);
}
}

View File

@@ -1,6 +1,6 @@
use crate::{Range, Rope, Selection, Syntax};
// const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']')];
const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')];
// limit matching pairs to only ( ) { } [ ] < >
#[must_use]
@@ -20,15 +20,27 @@ pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
None => return None,
};
let start_byte = node.start_byte();
let end_byte = node.end_byte() - 1; // it's end exclusive
if start_byte == byte_pos {
return Some(doc.byte_to_char(end_byte));
if node.is_error() {
return None;
}
if end_byte == byte_pos {
return Some(doc.byte_to_char(start_byte));
let start_byte = node.start_byte();
let len = doc.len_bytes();
if start_byte >= len {
return None;
}
let end_byte = node.end_byte() - 1; // it's end exclusive
let start_char = doc.byte_to_char(start_byte);
let end_char = doc.byte_to_char(end_byte);
if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
if start_byte == byte_pos {
return Some(end_char);
}
if end_byte == byte_pos {
return Some(start_char);
}
}
None

View File

@@ -1,240 +1,274 @@
use crate::graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary, RopeGraphemes};
use crate::{coords_at_pos, pos_at_coords, ChangeSet, Position, Range, Rope, RopeSlice, Selection};
use std::iter::{self, from_fn, Peekable, SkipWhile};
#[derive(Copy, Clone, PartialEq, Eq)]
use ropey::iter::Chars;
use crate::{
chars::{
categorize_char, char_is_line_ending, char_is_punctuation, char_is_whitespace,
char_is_word, CharCategory,
},
coords_at_pos,
graphemes::{nth_next_grapheme_boundary, nth_prev_grapheme_boundary},
line_ending::{get_line_ending, line_end_char_index},
pos_at_coords, Position, Range, RopeSlice,
};
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Direction {
Forward,
Backward,
}
#[derive(Copy, Clone, PartialEq, Eq)]
pub enum Movement {
Extend,
Move,
}
pub fn move_horizontally(
text: RopeSlice,
slice: RopeSlice,
range: Range,
dir: Direction,
count: usize,
extend: bool,
behaviour: Movement,
) -> Range {
let pos = range.head;
let line = text.char_to_line(pos);
let line = slice.char_to_line(pos);
// TODO: we can optimize clamping by passing in RopeSlice limited to current line. that way
// we stop calculating past start/end of line.
let pos = match dir {
Direction::Backward => {
let start = text.line_to_char(line);
nth_prev_grapheme_boundary(text, pos, count).max(start)
let start = slice.line_to_char(line);
nth_prev_grapheme_boundary(slice, pos, count).max(start)
}
Direction::Forward => {
// Line end is pos at the start of next line - 1
let end = text.line_to_char(line + 1).saturating_sub(1);
nth_next_grapheme_boundary(text, pos, count).min(end)
let end_char_idx = line_end_char_index(&slice, line);
nth_next_grapheme_boundary(slice, pos, count).min(end_char_idx)
}
};
Range::new(if extend { range.anchor } else { pos }, pos)
let anchor = match behaviour {
Movement::Extend => range.anchor,
Movement::Move => pos,
};
Range::new(anchor, pos)
}
pub fn move_vertically(
text: RopeSlice,
slice: RopeSlice,
range: Range,
dir: Direction,
count: usize,
extend: bool,
behaviour: Movement,
) -> Range {
let Position { row, col } = coords_at_pos(text, range.head);
let Position { row, col } = coords_at_pos(slice, range.head);
let horiz = range.horiz.unwrap_or(col as u32);
let new_line = match dir {
Direction::Backward => row.saturating_sub(count),
Direction::Forward => std::cmp::min(row.saturating_add(count), text.len_lines() - 1),
Direction::Forward => std::cmp::min(
row.saturating_add(count),
slice.len_lines().saturating_sub(2),
),
};
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
let new_line_len = text.line(new_line).len_chars().saturating_sub(2);
// Length of the line sans line-ending.
let new_line_len = {
let line = slice.line(new_line);
line.len_chars() - get_line_ending(&line).map(|le| le.len_chars()).unwrap_or(0)
};
let new_col = std::cmp::min(horiz as usize, new_line_len);
let pos = pos_at_coords(text, Position::new(new_line, new_col));
let pos = pos_at_coords(slice, Position::new(new_line, new_col));
let mut range = Range::new(if extend { range.anchor } else { pos }, pos);
let anchor = match behaviour {
Movement::Extend => range.anchor,
Movement::Move => pos,
};
let mut range = Range::new(anchor, pos);
range.horiz = Some(horiz);
range
}
pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
let mut end = begin;
for _ in 0..count {
if begin + 1 == slice.len_chars() {
return None;
}
let mut ch = slice.char(begin);
let next = slice.char(begin + 1);
// if we're at the end of a word, or on whitespce right before new one
if categorize(ch) != categorize(next) {
begin += 1;
}
// return if not skip while?
skip_over_next(slice, &mut begin, |ch| ch == '\n');
ch = slice.char(begin);
end = begin + 1;
if is_word(ch) {
skip_over_next(slice, &mut end, is_word);
} else if ch.is_ascii_punctuation() {
skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
}
skip_over_next(slice, &mut end, is_horiz_blank);
}
Some(Range::new(begin, end - 1))
pub fn move_next_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::NextWordStart)
}
pub fn move_prev_word_start(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
let mut with_end = false;
let mut end = begin;
for _ in 0..count {
if begin == 0 {
return None;
}
let ch = slice.char(begin);
let prev = slice.char(begin - 1);
if categorize(ch) != categorize(prev) {
begin -= 1;
}
// return if not skip while?
skip_over_prev(slice, &mut begin, |ch| ch == '\n');
end = begin;
with_end = skip_over_prev(slice, &mut end, is_horiz_blank);
// refetch
let ch = slice.char(end);
if is_word(ch) {
with_end = skip_over_prev(slice, &mut end, is_word);
} else if ch.is_ascii_punctuation() {
with_end = skip_over_prev(slice, &mut end, |ch| ch.is_ascii_punctuation());
}
}
Some(Range::new(begin, if with_end { end } else { end + 1 }))
pub fn move_next_word_end(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::NextWordEnd)
}
pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> Option<Range> {
let mut end = begin;
pub fn move_prev_word_start(slice: RopeSlice, range: Range, count: usize) -> Range {
word_move(slice, range, count, WordMotionTarget::PrevWordStart)
}
for _ in 0..count {
if begin + 1 == slice.len_chars() {
return None;
}
let ch = slice.char(begin);
let next = slice.char(begin + 1);
if categorize(ch) != categorize(next) {
begin += 1;
}
// return if not skip while?
skip_over_next(slice, &mut begin, |ch| ch == '\n');
end = begin;
skip_over_next(slice, &mut end, is_horiz_blank);
// refetch
let ch = slice.char(end);
if is_word(ch) {
skip_over_next(slice, &mut end, is_word);
} else if ch.is_ascii_punctuation() {
skip_over_next(slice, &mut end, |ch| ch.is_ascii_punctuation());
}
}
Some(Range::new(begin, end - 1))
fn word_move(slice: RopeSlice, mut range: Range, count: usize, target: WordMotionTarget) -> Range {
(0..count).fold(range, |range, _| {
slice.chars_at(range.head).range_to_target(target, range)
})
}
// ---- util ------------
// used for by-word movement
fn is_word(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}
fn is_horiz_blank(ch: char) -> bool {
matches!(ch, ' ' | '\t')
}
#[derive(Debug, Eq, PartialEq)]
enum Category {
Whitespace,
Eol,
Word,
Punctuation,
}
fn categorize(ch: char) -> Category {
if ch == '\n' {
Category::Eol
} else if ch.is_ascii_whitespace() {
Category::Whitespace
} else if is_word(ch) {
Category::Word
} else if ch.is_ascii_punctuation() {
Category::Punctuation
} else {
unreachable!()
}
}
#[inline]
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
/// Returns first index that doesn't satisfy a given predicate when
/// advancing the character index.
///
/// Returns none if all characters satisfy the predicate.
pub fn skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<usize>
where
F: Fn(char) -> bool,
{
let mut chars = slice.chars_at(*pos);
for ch in chars {
if !fun(ch) {
break;
}
*pos += 1;
}
let mut chars = slice.chars_at(pos).enumerate();
chars.find_map(|(i, c)| if !fun(c) { Some(pos + i) } else { None })
}
#[inline]
/// Returns true if the final pos matches the predicate.
pub fn skip_over_prev<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
/// Returns first index that doesn't satisfy a given predicate when
/// retreating the character index, saturating if all elements satisfy
/// the condition.
pub fn backwards_skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<usize>
where
F: Fn(char) -> bool,
{
// need to +1 so that prev() includes current char
let mut chars = slice.chars_at(*pos + 1);
while let Some(ch) = chars.prev() {
if !fun(ch) {
break;
let mut chars_starting_from_next = slice.chars_at(pos + 1);
let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
backwards.find_map(|(i, c)| {
if !fun(c) {
Some(pos.saturating_sub(i))
} else {
None
}
})
}
/// Possible targets of a word motion
#[derive(Copy, Clone, Debug)]
pub enum WordMotionTarget {
NextWordStart,
NextWordEnd,
PrevWordStart,
}
pub trait CharHelpers {
fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range;
}
enum WordMotionPhase {
Start,
SkipNewlines,
ReachTarget,
}
impl CharHelpers for Chars<'_> {
fn range_to_target(&mut self, target: WordMotionTarget, origin: Range) -> Range {
let range = origin;
// Characters are iterated forward or backwards depending on the motion direction.
let characters: Box<dyn Iterator<Item = char>> = match target {
WordMotionTarget::PrevWordStart => {
self.next();
Box::new(from_fn(|| self.prev()))
}
_ => Box::new(self),
};
// Index advancement also depends on the direction.
let advance: &dyn Fn(&mut usize) = match target {
WordMotionTarget::PrevWordStart => &|u| *u = u.saturating_sub(1),
_ => &|u| *u += 1,
};
let mut characters = characters.peekable();
let mut phase = WordMotionPhase::Start;
let mut head = origin.head;
let mut anchor: Option<usize> = None;
let is_boundary =
|a: char, b: Option<char>| categorize_char(a) != categorize_char(b.unwrap_or(a));
while let Some(peek) = characters.peek().copied() {
phase = match phase {
WordMotionPhase::Start => {
characters.next();
if characters.peek().is_none() {
break; // We're at the end, so there's nothing to do.
}
// Anchor may remain here if the head wasn't at a boundary
if !is_boundary(peek, characters.peek().copied()) && !char_is_line_ending(peek)
{
anchor = Some(head);
}
// First character is always skipped by the head
advance(&mut head);
WordMotionPhase::SkipNewlines
}
WordMotionPhase::SkipNewlines => {
if char_is_line_ending(peek) {
characters.next();
if characters.peek().is_some() {
advance(&mut head);
}
WordMotionPhase::SkipNewlines
} else {
WordMotionPhase::ReachTarget
}
}
WordMotionPhase::ReachTarget => {
characters.next();
anchor = anchor.or(Some(head));
if reached_target(target, peek, characters.peek()) {
break;
} else {
advance(&mut head);
}
WordMotionPhase::ReachTarget
}
}
}
Range::new(anchor.unwrap_or(origin.anchor), head)
}
}
fn reached_target(target: WordMotionTarget, peek: char, next_peek: Option<&char>) -> bool {
let next_peek = match next_peek {
Some(next_peek) => next_peek,
None => return true,
};
match target {
WordMotionTarget::NextWordStart => {
((categorize_char(peek) != categorize_char(*next_peek))
&& (char_is_line_ending(*next_peek) || !next_peek.is_whitespace()))
}
WordMotionTarget::NextWordEnd | WordMotionTarget::PrevWordStart => {
((categorize_char(peek) != categorize_char(*next_peek))
&& (!peek.is_whitespace() || char_is_line_ending(*next_peek)))
}
*pos = pos.saturating_sub(1);
}
fun(slice.char(*pos))
}
#[cfg(test)]
mod test {
use std::array::{self, IntoIter};
use ropey::Rope;
use super::*;
const SINGLE_LINE_SAMPLE: &str = "This is a simple alphabetic line";
const MULTILINE_SAMPLE: &str = "\
Multiline\n\
text sample\n\
which\n\
is merely alphabetic\n\
and whitespaced\n\
";
const MULTIBYTE_CHARACTER_SAMPLE: &str = "\
パーティーへ行かないか\n\
The text above is Japanese\n\
";
#[test]
fn test_vertical_move() {
let text = Rope::from("abcd\nefg\nwrs");
@@ -245,9 +279,436 @@ mod test {
assert_eq!(
coords_at_pos(
slice,
move_vertically(slice, range, Direction::Forward, 1, false).head
move_vertically(slice, range, Direction::Forward, 1, Movement::Move).head
),
(1, 2).into()
(1, 3).into()
);
}
#[test]
fn horizontal_moves_through_single_line_in_single_line_text() {
let text = Rope::from(SINGLE_LINE_SAMPLE);
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into());
let mut range = Range::point(position);
let moves_and_expected_coordinates = [
((Direction::Forward, 1usize), (0, 1)), // T|his is a simple alphabetic line
((Direction::Forward, 2usize), (0, 3)), // Thi|s is a simple alphabetic line
((Direction::Forward, 0usize), (0, 3)), // Thi|s is a simple alphabetic line
((Direction::Forward, 999usize), (0, 32)), // This is a simple alphabetic line|
((Direction::Forward, 999usize), (0, 32)), // This is a simple alphabetic line|
((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line
];
for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) {
range = move_horizontally(slice, range, direction, amount, Movement::Move);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into())
}
}
#[test]
fn horizontal_moves_through_single_line_in_multiline_text() {
let text = Rope::from(MULTILINE_SAMPLE);
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into());
let mut range = Range::point(position);
let moves_and_expected_coordinates = IntoIter::new([
((Direction::Forward, 1usize), (0, 1)), // M|ultiline\n
((Direction::Forward, 2usize), (0, 3)), // Mul|tiline\n
((Direction::Backward, 6usize), (0, 0)), // |Multiline\n
((Direction::Backward, 999usize), (0, 0)), // |Multiline\n
((Direction::Forward, 3usize), (0, 3)), // Mul|tiline\n
((Direction::Forward, 0usize), (0, 3)), // Mul|tiline\n
((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\n
((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
((Direction::Forward, 999usize), (0, 9)), // Multiline|\n
]);
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_horizontally(slice, range, direction, amount, Movement::Move);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
assert_eq!(range.head, range.anchor);
}
}
#[test]
fn selection_extending_moves_in_single_line_text() {
let text = Rope::from(SINGLE_LINE_SAMPLE);
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into());
let mut range = Range::point(position);
let original_anchor = range.anchor;
let moves = IntoIter::new([
(Direction::Forward, 1usize),
(Direction::Forward, 5usize),
(Direction::Backward, 3usize),
]);
for (direction, amount) in moves {
range = move_horizontally(slice, range, direction, amount, Movement::Extend);
assert_eq!(range.anchor, original_anchor);
}
}
#[test]
fn vertical_moves_in_single_column() {
let text = Rope::from(MULTILINE_SAMPLE);
let slice = dbg!(&text).slice(..);
let position = pos_at_coords(slice, (0, 0).into());
let mut range = Range::point(position);
let moves_and_expected_coordinates = IntoIter::new([
((Direction::Forward, 1usize), (1, 0)),
((Direction::Forward, 2usize), (3, 0)),
((Direction::Backward, 999usize), (0, 0)),
((Direction::Forward, 3usize), (3, 0)),
((Direction::Forward, 0usize), (3, 0)),
((Direction::Backward, 0usize), (3, 0)),
((Direction::Forward, 5), (4, 0)),
((Direction::Forward, 999usize), (4, 0)),
]);
for ((direction, amount), coordinates) in moves_and_expected_coordinates {
range = move_vertically(slice, range, direction, amount, Movement::Move);
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
assert_eq!(range.head, range.anchor);
}
}
#[test]
fn vertical_moves_jumping_column() {
let text = Rope::from(MULTILINE_SAMPLE);
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into());
let mut range = Range::point(position);
enum Axis {
H,
V,
}
let moves_and_expected_coordinates = IntoIter::new([
// Places cursor at the end of line
((Axis::H, Direction::Forward, 8usize), (0, 8)),
// First descent preserves column as the target line is wider
((Axis::V, Direction::Forward, 1usize), (1, 8)),
// Second descent clamps column as the target line is shorter
((Axis::V, Direction::Forward, 1usize), (2, 5)),
// Third descent restores the original column
((Axis::V, Direction::Forward, 1usize), (3, 8)),
// Behaviour is preserved even through long jumps
((Axis::V, Direction::Backward, 999usize), (0, 8)),
((Axis::V, Direction::Forward, 999usize), (4, 8)),
]);
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis {
Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move),
Axis::V => move_vertically(slice, range, direction, amount, Movement::Move),
};
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
assert_eq!(range.head, range.anchor);
}
}
#[test]
fn multibyte_character_column_jumps() {
let text = Rope::from(MULTIBYTE_CHARACTER_SAMPLE);
let slice = text.slice(..);
let position = pos_at_coords(slice, (0, 0).into());
let mut range = Range::point(position);
// FIXME: The behaviour captured in this test diverges from both Kakoune and Vim. These
// will attempt to preserve the horizontal position of the cursor, rather than
// placing it at the same character index.
enum Axis {
H,
V,
}
let moves_and_expected_coordinates = IntoIter::new([
// Places cursor at the fourth kana
((Axis::H, Direction::Forward, 4), (0, 4)),
// Descent places cursor at the fourth character.
((Axis::V, Direction::Forward, 1usize), (1, 4)),
]);
for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates {
range = match axis {
Axis::H => move_horizontally(slice, range, direction, amount, Movement::Move),
Axis::V => move_vertically(slice, range, direction, amount, Movement::Move),
};
assert_eq!(coords_at_pos(slice, range.head), coordinates.into());
assert_eq!(range.head, range.anchor);
}
}
#[test]
#[should_panic]
fn nonsensical_ranges_panic_on_forward_movement_attempt_in_debug_mode() {
move_next_word_start(Rope::from("Sample").slice(..), Range::point(99999999), 1);
}
#[test]
#[should_panic]
fn nonsensical_ranges_panic_on_forward_to_end_movement_attempt_in_debug_mode() {
move_next_word_end(Rope::from("Sample").slice(..), Range::point(99999999), 1);
}
#[test]
#[should_panic]
fn nonsensical_ranges_panic_on_backwards_movement_attempt_in_debug_mode() {
move_prev_word_start(Rope::from("Sample").slice(..), Range::point(99999999), 1);
}
#[test]
fn test_behaviour_when_moving_to_start_of_next_words() {
let tests = array::IntoIter::new([
("Basic forward motion stops at the first space",
vec![(1, Range::new(0, 0), Range::new(0, 5))]),
(" Starting from a boundary advances the anchor",
vec![(1, Range::new(0, 0), Range::new(1, 9))]),
("Long whitespace gap is bridged by the head",
vec![(1, Range::new(0, 0), Range::new(0, 10))]),
("Previous anchor is irrelevant for forward motions",
vec![(1, Range::new(12, 0), Range::new(0, 8))]),
(" Starting from whitespace moves to last space in sequence",
vec![(1, Range::new(0, 0), Range::new(0, 3))]),
("Starting from mid-word leaves anchor at start position and moves head",
vec![(1, Range::new(3, 3), Range::new(3, 8))]),
("Identifiers_with_underscores are considered a single word",
vec![(1, Range::new(0, 0), Range::new(0, 28))]),
("Jumping\n into starting whitespace selects the spaces before 'into'",
vec![(1, Range::new(0, 6), Range::new(8, 11))]),
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
vec![
(1, Range::new(0, 0), Range::new(0, 11)),
(1, Range::new(0, 11), Range::new(12, 14)),
(1, Range::new(12, 14), Range::new(15, 17))
]),
("... ... punctuation and spaces behave as expected",
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
(1, Range::new(0, 5), Range::new(6, 9)),
]),
(".._.._ punctuation is not joined by underscores into a single block",
vec![(1, Range::new(0, 0), Range::new(0, 1))]),
("Newlines\n\nare bridged seamlessly.",
vec![
(1, Range::new(0, 0), Range::new(0, 7)),
(1, Range::new(0, 7), Range::new(10, 13)),
]),
("Jumping\n\n\n\n\n\n from newlines to whitespace selects whitespace.",
vec![
(1, Range::new(0, 8), Range::new(13, 15)),
]),
("A failed motion does not modify the range",
vec![
(3, Range::new(37, 41), Range::new(37, 41)),
]),
("oh oh oh two character words!",
vec![
(1, Range::new(0, 0), Range::new(0, 2)),
(1, Range::new(0, 2), Range::new(3, 5)),
(1, Range::new(0, 1), Range::new(2, 2)),
]),
("Multiple motions at once resolve correctly",
vec![
(3, Range::new(0, 0), Range::new(17, 19)),
]),
("Excessive motions are performed partially",
vec![
(999, Range::new(0, 0), Range::new(32, 40)),
]),
("", // Edge case of moving forward in empty string
vec![
(1, Range::new(0, 0), Range::new(0, 0)),
]),
("\n\n\n\n\n", // Edge case of moving forward in all newlines
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
]),
("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
vec![
(1, Range::new(0, 0), Range::new(1, 3)),
(1, Range::new(1, 3), Range::new(5, 7)),
]),
("ヒーリクス multibyte characters behave as normal characters",
vec![
(1, Range::new(0, 0), Range::new(0, 5)),
]),
]);
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_next_word_start(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
#[test]
fn test_behaviour_when_moving_to_start_of_previous_words() {
let tests = array::IntoIter::new([
("Basic backward motion from the middle of a word",
vec![(1, Range::new(3, 3), Range::new(3, 0))]),
("Starting from after boundary retreats the anchor",
vec![(1, Range::new(0, 8), Range::new(7, 0))]),
(" Jump to start of a word preceded by whitespace",
vec![(1, Range::new(5, 5), Range::new(5, 4))]),
(" Jump to start of line from start of word preceded by whitespace",
vec![(1, Range::new(4, 4), Range::new(3, 0))]),
("Previous anchor is irrelevant for backward motions",
vec![(1, Range::new(12, 5), Range::new(5, 0))]),
(" Starting from whitespace moves to first space in sequence",
vec![(1, Range::new(0, 3), Range::new(3, 0))]),
("Identifiers_with_underscores are considered a single word",
vec![(1, Range::new(0, 20), Range::new(20, 0))]),
("Jumping\n \nback through a newline selects whitespace",
vec![(1, Range::new(0, 13), Range::new(11, 8))]),
("Jumping to start of word from the end selects the word",
vec![(1, Range::new(6, 6), Range::new(6, 0))]),
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
vec![
(1, Range::new(30, 30), Range::new(30, 21)),
(1, Range::new(30, 21), Range::new(20, 18)),
(1, Range::new(20, 18), Range::new(17, 15))
]),
("... ... punctuation and spaces behave as expected",
vec![
(1, Range::new(0, 10), Range::new(9, 6)),
(1, Range::new(9, 6), Range::new(5, 0)),
]),
(".._.._ punctuation is not joined by underscores into a single block",
vec![(1, Range::new(0, 5), Range::new(4, 3))]),
("Newlines\n\nare bridged seamlessly.",
vec![
(1, Range::new(0, 10), Range::new(7, 0)),
]),
("Jumping \n\n\n\n\nback from within a newline group selects previous block",
vec![
(1, Range::new(0, 13), Range::new(10, 0)),
]),
("Failed motions do not modify the range",
vec![
(0, Range::new(3, 0), Range::new(3, 0)),
]),
("Multiple motions at once resolve correctly",
vec![
(3, Range::new(18, 18), Range::new(8, 0)),
]),
("Excessive motions are performed partially",
vec![
(999, Range::new(40, 40), Range::new(9, 0)),
]),
("", // Edge case of moving backwards in empty string
vec![
(1, Range::new(0, 0), Range::new(0, 0)),
]),
("\n\n\n\n\n", // Edge case of moving backwards in all newlines
vec![
(1, Range::new(0, 0), Range::new(0, 0)),
]),
(" \n \nJumping back through alternated space blocks and newlines selects the space blocks",
vec![
(1, Range::new(0, 7), Range::new(6, 4)),
(1, Range::new(6, 4), Range::new(2, 0)),
]),
("ヒーリクス multibyte characters behave as normal characters",
vec![
(1, Range::new(0, 5), Range::new(4, 0)),
]),
]);
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_prev_word_start(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
#[test]
fn test_behaviour_when_moving_to_end_of_next_words() {
let tests = array::IntoIter::new([
("Basic forward motion from the start of a word to the end of it",
vec![(1, Range::new(0, 0), Range::new(0, 4))]),
("Basic forward motion from the end of a word to the end of the next",
vec![(1, Range::new(0, 4), Range::new(5, 12))]),
("Basic forward motion from the middle of a word to the end of it",
vec![(1, Range::new(2, 2), Range::new(2, 4))]),
(" Jumping to end of a word preceded by whitespace",
vec![(1, Range::new(0, 0), Range::new(0, 10))]),
(" Starting from a boundary advances the anchor",
vec![(1, Range::new(0, 0), Range::new(1, 8))]),
("Previous anchor is irrelevant for end of word motion",
vec![(1, Range::new(12, 2), Range::new(2, 7))]),
("Identifiers_with_underscores are considered a single word",
vec![(1, Range::new(0, 0), Range::new(0, 27))]),
("Jumping\n into starting whitespace selects up to the end of next word",
vec![(1, Range::new(0, 6), Range::new(8, 15))]),
("alphanumeric.!,and.?=punctuation are considered 'words' for the purposes of word motion",
vec![
(1, Range::new(0, 0), Range::new(0, 11)),
(1, Range::new(0, 11), Range::new(12, 14)),
(1, Range::new(12, 14), Range::new(15, 17))
]),
("... ... punctuation and spaces behave as expected",
vec![
(1, Range::new(0, 0), Range::new(0, 2)),
(1, Range::new(0, 2), Range::new(3, 8)),
]),
(".._.._ punctuation is not joined by underscores into a single block",
vec![(1, Range::new(0, 0), Range::new(0, 1))]),
("Newlines\n\nare bridged seamlessly.",
vec![
(1, Range::new(0, 0), Range::new(0, 7)),
(1, Range::new(0, 7), Range::new(10, 12)),
]),
("Jumping\n\n\n\n\n\n from newlines to whitespace selects to end of next word.",
vec![
(1, Range::new(0, 8), Range::new(13, 19)),
]),
("A failed motion does not modify the range",
vec![
(3, Range::new(37, 41), Range::new(37, 41)),
]),
("Multiple motions at once resolve correctly",
vec![
(3, Range::new(0, 0), Range::new(16, 18)),
]),
("Excessive motions are performed partially",
vec![
(999, Range::new(0, 0), Range::new(31, 40)),
]),
("", // Edge case of moving forward in empty string
vec![
(1, Range::new(0, 0), Range::new(0, 0)),
]),
("\n\n\n\n\n", // Edge case of moving forward in all newlines
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
]),
("\n \n \n Jumping through alternated space blocks and newlines selects the space blocks",
vec![
(1, Range::new(0, 0), Range::new(1, 3)),
(1, Range::new(1, 3), Range::new(5, 7)),
]),
("ヒーリクス multibyte characters behave as normal characters",
vec![
(1, Range::new(0, 0), Range::new(0, 4)),
]),
]);
for (sample, scenario) in tests {
for (count, begin, expected_end) in scenario.into_iter() {
let range = move_next_word_end(Rope::from(sample).slice(..), begin, count);
assert_eq!(range, expected_end, "Case failed: [{}]", sample);
}
}
}
}

View File

@@ -1,4 +1,5 @@
use crate::{
chars::char_is_line_ending,
graphemes::{nth_next_grapheme_boundary, RopeGraphemes},
Rope, RopeSlice,
};
@@ -23,8 +24,9 @@ impl Position {
pub fn traverse(self, text: &crate::Tendril) -> Self {
let Self { mut row, mut col } = self;
// TODO: there should be a better way here
for ch in text.chars() {
if ch == '\n' {
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
row += 1;
col = 0;
} else {

View File

@@ -1,21 +1,63 @@
use crate::Tendril;
use once_cell::sync::Lazy;
use std::{collections::HashMap, sync::RwLock};
use std::collections::HashMap;
// TODO: could be an instance on Editor
static REGISTRY: Lazy<RwLock<HashMap<char, Vec<String>>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
pub fn get(register: char) -> Option<Vec<String>> {
let registry = REGISTRY.read().unwrap();
// TODO: no cloning
registry.get(&register).cloned()
#[derive(Debug)]
pub struct Register {
name: char,
values: Vec<String>,
}
// restoring: bool
pub fn set(register: char, values: Vec<String>) {
let mut registry = REGISTRY.write().unwrap();
impl Register {
pub fn new(name: char) -> Self {
Self {
name,
values: Vec::new(),
}
}
registry.insert(register, values);
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
Self { name, values }
}
pub fn name(&self) -> char {
self.name
}
pub fn read(&self) -> &Vec<String> {
&self.values
}
pub fn write(&mut self, values: Vec<String>) {
self.values = values;
}
}
/// Currently just wraps a `HashMap` of `Register`s
#[derive(Debug, Default)]
pub struct Registers {
inner: HashMap<char, Register>,
}
impl Registers {
pub fn get(&self, name: char) -> Option<&Register> {
self.inner.get(&name)
}
pub fn get_mut(&mut self, name: char) -> Option<&mut Register> {
self.inner.get_mut(&name)
}
pub fn get_or_insert(&mut self, name: char) -> &mut Register {
self.inner
.entry(name)
.or_insert_with(|| Register::new(name))
}
pub fn write(&mut self, name: char, values: Vec<String>) {
self.inner
.insert(name, Register::new_with_values(name, values));
}
pub fn read(&self, name: char) -> Option<&Vec<String>> {
self.get(name).map(|reg| reg.read())
}
}

View File

@@ -7,6 +7,10 @@ pub fn find_nth_next(
n: usize,
inclusive: bool,
) -> Option<usize> {
if pos >= text.len_chars() {
return None;
}
// start searching right after pos
let mut chars = text.chars_at(pos + 1);
@@ -37,7 +41,7 @@ pub fn find_nth_prev(
inclusive: bool,
) -> Option<usize> {
// start searching right before pos
let mut chars = text.chars_at(pos.saturating_sub(1));
let mut chars = text.chars_at(pos);
for _ in 0..n {
loop {
@@ -52,7 +56,7 @@ pub fn find_nth_prev(
}
if !inclusive {
pos -= 1;
pos += 1;
}
Some(pos)

View File

@@ -35,6 +35,10 @@ impl Range {
}
}
pub fn point(head: usize) -> Self {
Self::new(head, head)
}
/// Start of the range.
#[inline]
#[must_use]
@@ -348,7 +352,7 @@ pub fn select_on_matches(
let start = text.byte_to_char(start_byte + mat.start());
let end = text.byte_to_char(start_byte + mat.end());
result.push(Range::new(start, end - 1));
result.push(Range::new(start, end.saturating_sub(1)));
}
}
@@ -383,7 +387,7 @@ pub fn split_on_matches(
// TODO: retain range direction
let end = text.byte_to_char(start_byte + mat.start());
result.push(Range::new(start, end - 1));
result.push(Range::new(start, end.saturating_sub(1)));
start = text.byte_to_char(start_byte + mat.end());
}

View File

@@ -1,7 +1,7 @@
use crate::{Rope, Selection};
/// A state represents the current editor state of a single buffer.
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct State {
pub doc: Rope,
pub selection: Selection,

266
helix-core/src/surround.rs Normal file
View File

@@ -0,0 +1,266 @@
use crate::{search, Selection};
use ropey::RopeSlice;
pub const PAIRS: &[(char, char)] = &[
('(', ')'),
('[', ']'),
('{', '}'),
('<', '>'),
('«', '»'),
('「', '」'),
('', ''),
];
/// Given any char in [PAIRS], return the open and closing chars. If not found in
/// [PAIRS] return (ch, ch).
///
/// ```
/// use helix_core::surround::get_pair;
///
/// assert_eq!(get_pair('['), ('[', ']'));
/// assert_eq!(get_pair('}'), ('{', '}'));
/// assert_eq!(get_pair('"'), ('"', '"'));
/// ```
pub fn get_pair(ch: char) -> (char, char) {
PAIRS
.iter()
.find(|(open, close)| *open == ch || *close == ch)
.copied()
.unwrap_or((ch, ch))
}
/// Find the position of surround pairs of `ch` which can be either a closing
/// or opening pair. `n` will skip n - 1 pairs (eg. n=2 will discard (only)
/// the first pair found and keep looking)
pub fn find_nth_pairs_pos(
text: RopeSlice,
ch: char,
pos: usize,
n: usize,
) -> Option<(usize, usize)> {
let (open, close) = get_pair(ch);
let (open_pos, close_pos) = if open == close {
// find_nth* do not consider current character; +1/-1 to include them
(
search::find_nth_prev(text, open, pos + 1, n, true)?,
search::find_nth_next(text, close, pos - 1, n, true)?,
)
} else {
(
find_nth_open_pair(text, open, close, pos, n)?,
find_nth_close_pair(text, open, close, pos, n)?,
)
};
Some((open_pos, close_pos))
}
fn find_nth_open_pair(
text: RopeSlice,
open: char,
close: char,
mut pos: usize,
n: usize,
) -> Option<usize> {
let mut chars = text.chars_at(pos + 1);
// Adjusts pos for the first iteration, and handles the case of the
// cursor being *on* the close character which will get falsely stepped over
// if not skipped here
if chars.prev()? == open {
return Some(pos);
}
for _ in 0..n {
let mut step_over: usize = 0;
loop {
let c = chars.prev()?;
pos = pos.saturating_sub(1);
// ignore other surround pairs that are enclosed *within* our search scope
if c == close {
step_over += 1;
} else if c == open {
if step_over == 0 {
break;
}
step_over = step_over.saturating_sub(1);
}
}
}
Some(pos)
}
fn find_nth_close_pair(
text: RopeSlice,
open: char,
close: char,
mut pos: usize,
n: usize,
) -> Option<usize> {
if pos >= text.len_chars() {
return None;
}
let mut chars = text.chars_at(pos);
if chars.next()? == close {
return Some(pos);
}
for _ in 0..n {
let mut step_over: usize = 0;
loop {
let c = chars.next()?;
pos += 1;
if c == open {
step_over += 1;
} else if c == close {
if step_over == 0 {
break;
}
step_over = step_over.saturating_sub(1);
}
}
}
Some(pos)
}
/// Find position of surround characters around every cursor. Returns None
/// if any positions overlap. Note that the positions are in a flat Vec.
/// Use get_surround_pos().chunks(2) to get matching pairs of surround positions.
/// `ch` can be either closing or opening pair.
pub fn get_surround_pos(
text: RopeSlice,
selection: &Selection,
ch: char,
skip: usize,
) -> Option<Vec<usize>> {
let mut change_pos = Vec::new();
for range in selection {
let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return None;
}
change_pos.extend_from_slice(&[open_pos, close_pos]);
}
Some(change_pos)
}
#[cfg(test)]
mod test {
use super::*;
use crate::Range;
use ropey::Rope;
use smallvec::SmallVec;
#[test]
fn test_find_nth_pairs_pos() {
let doc = Rope::from("some (text) here");
let slice = doc.slice(..);
// cursor on [t]ext
assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
// cursor on so[m]e
assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
// cursor on bracket itself
assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
}
#[test]
fn test_find_nth_pairs_pos_skip() {
let doc = Rope::from("(so (many (good) text) here)");
let slice = doc.slice(..);
// cursor on go[o]d
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
}
#[test]
fn test_find_nth_pairs_pos_same() {
let doc = Rope::from("'so 'many 'good' text' here'");
let slice = doc.slice(..);
// cursor on go[o]d
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
}
#[test]
fn test_find_nth_pairs_pos_step() {
let doc = Rope::from("((so)((many) good (text))(here))");
let slice = doc.slice(..);
// cursor on go[o]d
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
}
#[test]
fn test_find_nth_pairs_pos_mixed() {
let doc = Rope::from("(so [many {good} text] here)");
let slice = doc.slice(..);
// cursor on go[o]d
assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
}
#[test]
fn test_get_surround_pos() {
let doc = Rope::from("(some) (chars)\n(newline)");
let slice = doc.slice(..);
let selection = Selection::new(
SmallVec::from_slice(&[Range::point(2), Range::point(9), Range::point(20)]),
0,
);
// cursor on s[o]me, c[h]ars, newl[i]ne
assert_eq!(
get_surround_pos(slice, &selection, '(', 1)
.unwrap()
.as_slice(),
&[0, 5, 7, 13, 15, 23]
);
}
#[test]
fn test_get_surround_pos_bail() {
let doc = Rope::from("[some]\n(chars)xx\n(newline)");
let slice = doc.slice(..);
let selection =
Selection::new(SmallVec::from_slice(&[Range::point(2), Range::point(9)]), 0);
// cursor on s[o]me, c[h]ars
assert_eq!(
get_surround_pos(slice, &selection, '(', 1),
None // different surround chars
);
let selection = Selection::new(
SmallVec::from_slice(&[Range::point(14), Range::point(24)]),
0,
);
// cursor on [x]x, newli[n]e
assert_eq!(
get_surround_pos(slice, &selection, '(', 1),
None // overlapping surround chars
);
}
}

View File

@@ -1,10 +1,13 @@
use crate::{regex::Regex, Change, Rope, RopeSlice, Transaction};
use crate::{chars::char_is_line_ending, regex::Regex, Change, Rope, RopeSlice, Transaction};
pub use helix_syntax::{get_language, get_language_name, Lang};
use arc_swap::ArcSwap;
use std::{
borrow::Cow,
cell::RefCell,
collections::{HashMap, HashSet},
fmt,
path::{Path, PathBuf},
sync::Arc,
};
@@ -12,13 +15,13 @@ use std::{
use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
}
// largely based on tree-sitter/cli/src/loader.rs
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
@@ -27,8 +30,8 @@ pub struct LanguageConfiguration {
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
// pub path: PathBuf,
// root_path for tree-sitter (^)
#[serde(default)]
pub auto_format: bool,
// content_regex
// injection_regex
@@ -46,7 +49,7 @@ pub struct LanguageConfiguration {
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
pub command: String,
@@ -55,14 +58,14 @@ pub struct LanguageServerConfiguration {
pub args: Vec<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndentationConfiguration {
pub tab_width: usize,
pub unit: String,
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndentQuery {
#[serde(default)]
@@ -73,16 +76,48 @@ pub struct IndentQuery {
pub outdent: HashSet<String>,
}
#[cfg(not(feature = "embed_runtime"))]
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let path = crate::RUNTIME_DIR
.join("queries")
.join(language)
.join(filename);
std::fs::read_to_string(&path)
}
#[cfg(feature = "embed_runtime")]
fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> {
use std::fmt;
#[derive(rust_embed::RustEmbed)]
#[folder = "../runtime/"]
struct Runtime;
#[derive(Debug)]
struct EmbeddedFileNotFoundError {
path: PathBuf,
}
impl std::error::Error for EmbeddedFileNotFoundError {}
impl fmt::Display for EmbeddedFileNotFoundError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "failed to load embedded file {}", self.path.display())
}
}
let path = PathBuf::from("queries").join(language).join(filename);
if let Some(query_bytes) = Runtime::get(&path.display().to_string()) {
String::from_utf8(query_bytes.to_vec()).map_err(|err| err.into())
} else {
Err(Box::new(EmbeddedFileNotFoundError { path }))
}
}
fn read_query(language: &str, filename: &str) -> String {
static INHERITS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
let root = crate::runtime_dir();
// let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let path = root.join("queries").join(language).join(filename);
let query = std::fs::read_to_string(&path).unwrap_or_default();
let query = load_runtime_file(language, filename).unwrap_or_default();
// TODO: the collect() is not ideal
let inherits = INHERITS_REGEX
@@ -110,47 +145,56 @@ fn read_query(language: &str, filename: &str) -> String {
}
impl LanguageConfiguration {
fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let highlights_query = read_query(&language, "highlights.scm");
// always highlight syntax errors
// highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm");
let locals_query = "";
if highlights_query.is_empty() {
None
} else {
let language = get_language(self.language_id);
let mut config = HighlightConfiguration::new(
language,
&highlights_query,
&injections_query,
locals_query,
)
.unwrap(); // TODO: no unwrap
config.configure(scopes);
Some(Arc::new(config))
}
}
pub fn reconfigure(&self, scopes: &[String]) {
if let Some(Some(config)) = self.highlight_config.get() {
config.configure(scopes);
}
}
pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
self.highlight_config
.get_or_init(|| {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let highlights_query = read_query(&language, "highlights.scm");
// always highlight syntax errors
// highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm");
let locals_query = "";
if highlights_query.is_empty() {
None
} else {
let language = get_language(self.language_id);
let mut config = HighlightConfiguration::new(
language,
&highlights_query,
&injections_query,
locals_query,
)
.unwrap(); // TODO: no unwrap
config.configure(scopes);
Some(Arc::new(config))
}
})
.get_or_init(|| self.initialize_highlight(scopes))
.clone()
}
pub fn is_highlight_initialized(&self) -> bool {
self.highlight_config.get().is_some()
}
pub fn indent_query(&self) -> Option<&IndentQuery> {
self.indent_query
.get_or_init(|| {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let root = crate::runtime_dir();
let path = root.join("queries").join(language).join("indents.toml");
let toml = std::fs::read(&path).ok()?;
toml::from_slice(&toml).ok()
let toml = load_runtime_file(&language, "indents.toml").ok()?;
toml::from_slice(toml.as_bytes()).ok()
})
.as_ref()
}
@@ -160,8 +204,7 @@ impl LanguageConfiguration {
}
}
pub static LOADER: OnceCell<Loader> = OnceCell::new();
#[derive(Debug)]
pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
@@ -216,6 +259,10 @@ impl Loader {
.find(|config| config.scope == scope)
.cloned()
}
pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter()
}
}
pub struct TsParser {
@@ -223,6 +270,12 @@ pub struct TsParser {
cursors: Vec<QueryCursor>,
}
impl fmt::Debug for TsParser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TsParser").finish()
}
}
// could also just use a pool, or a single instance?
thread_local! {
pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
@@ -231,6 +284,7 @@ thread_local! {
})
}
#[derive(Debug)]
pub struct Syntax {
config: Arc<HighlightConfiguration>,
@@ -333,7 +387,11 @@ impl Syntax {
// prevents them from being moved. But both of these values are really just
// pointers, so it's actually ok to move them.
let mut cursor = QueryCursor::new(); // reuse a pool
// reuse a cursor from the pool if possible
let mut cursor = PARSER.with(|ts_parser| {
let highlighter = &mut ts_parser.borrow_mut();
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
});
let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
@@ -407,6 +465,7 @@ impl Syntax {
// buffer_range_for_scope_at_pos
}
#[derive(Debug)]
pub struct LanguageLayer {
// mode
// grammar
@@ -530,9 +589,10 @@ impl LanguageLayer {
mut column,
} = point;
// TODO: there should be a better way here
for ch in text.bytes() {
if ch == b'\n' {
// TODO: there should be a better way here.
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
row += 1;
column = 0;
} else {
@@ -715,13 +775,14 @@ pub enum HighlightEvent {
/// Contains the data neeeded to higlight code written in a particular language.
///
/// This struct is immutable and can be shared between threads.
#[derive(Debug)]
pub struct HighlightConfiguration {
pub language: Grammar,
pub query: Query,
combined_injections_query: Option<Query>,
locals_pattern_index: usize,
highlights_pattern_index: usize,
highlight_indices: Vec<Option<Highlight>>,
highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
non_local_variable_patterns: Vec<bool>,
injection_content_capture_index: Option<u32>,
injection_language_capture_index: Option<u32>,
@@ -745,6 +806,7 @@ struct LocalScope<'a> {
local_defs: Vec<LocalDef<'a>>,
}
#[derive(Debug)]
struct HighlightIter<'a, 'tree: 'a, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
@@ -770,6 +832,12 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
depth: usize,
}
impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HighlightIterLayer").finish()
}
}
impl HighlightConfiguration {
/// Creates a `HighlightConfiguration` for a given `Grammar` and set of highlighting
/// queries.
@@ -866,7 +934,7 @@ impl HighlightConfiguration {
}
}
let highlight_indices = vec![None; query.capture_names().len()];
let highlight_indices = ArcSwap::from_pointee(vec![None; query.capture_names().len()]);
Ok(Self {
language,
query,
@@ -899,17 +967,20 @@ impl HighlightConfiguration {
///
/// When highlighting, results are returned as `Highlight` values, which contain the index
/// of the matched highlight this list of highlight names.
pub fn configure(&mut self, recognized_names: &[String]) {
pub fn configure(&self, recognized_names: &[String]) {
let mut capture_parts = Vec::new();
self.highlight_indices.clear();
self.highlight_indices
.extend(self.query.capture_names().iter().map(move |capture_name| {
let indices: Vec<_> = self
.query
.capture_names()
.iter()
.map(move |capture_name| {
capture_parts.clear();
capture_parts.extend(capture_name.split('.'));
let mut best_index = None;
let mut best_match_len = 0;
for (i, recognized_name) in recognized_names.iter().enumerate() {
let recognized_name = recognized_name;
let mut len = 0;
let mut matches = true;
for part in recognized_name.split('.') {
@@ -925,7 +996,10 @@ impl HighlightConfiguration {
}
}
best_index.map(Highlight)
}));
})
.collect();
self.highlight_indices.store(Arc::new(indices));
}
}
@@ -1504,7 +1578,7 @@ where
}
}
let current_highlight = layer.config.highlight_indices[capture.index as usize];
let current_highlight = layer.config.highlight_indices.load()[capture.index as usize];
// If this node represents a local definition, then store the current
// highlight value on the local scope entry representing this node.
@@ -1700,3 +1774,13 @@ fn test_input_edits() {
}]
);
}
#[test]
fn test_load_runtime_file() {
// Test to make sure we can load some data from the runtime directory.
let contents = load_runtime_file("rust", "indents.toml").unwrap();
assert!(!contents.is_empty());
let results = load_runtime_file("rust", "does-not-exist");
assert!(results.is_err());
}

View File

@@ -15,7 +15,7 @@ pub enum Operation {
Insert(Tendril),
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Assoc {
Before,
After,
@@ -90,7 +90,8 @@ impl ChangeSet {
return;
}
self.len_after += fragment.len();
// Avoiding std::str::len() to account for UTF-8 characters.
self.len_after += fragment.chars().count();
let new_last = match self.changes.as_mut_slice() {
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
@@ -415,7 +416,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,
@@ -754,4 +755,21 @@ mod test {
use Operation::*;
assert_eq!(changes.changes, &[Insert("a".into())]);
}
#[test]
fn combine_with_utf8() {
const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
let empty = Rope::from("");
let mut a = ChangeSet::new(&empty);
let mut b = ChangeSet::new(&empty);
b.insert(TEST_CASE.into());
let changes = a.compose(b);
use Operation::*;
assert_eq!(changes.changes, &[Insert(TEST_CASE.into())]);
assert_eq!(changes.len_after, TEST_CASE.chars().count());
}
}

View File

@@ -1,29 +1,27 @@
[package]
name = "helix-lsp"
version = "0.1.0"
version = "0.3.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
description = "LSP client implementation for Helix project"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helix-core = { path = "../helix-core" }
helix-core = { version = "0.3", path = "../helix-core" }
once_cell = "1.4"
lsp-types = { version = "0.89", features = ["proposed"] }
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1.5"
futures-executor = { version = "0.3" }
url = "2"
pathdiff = "0.2"
glob = "0.3"
anyhow = "1"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
# jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc", default-features = false } # don't pull in all of futures
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
log = "0.4"
lsp-types = { version = "0.89", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
log = "~0.4"
tokio = { version = "1.7", features = ["full"] }
tokio-stream = "0.1.6"

View File

@@ -3,37 +3,35 @@ use crate::{
Call, Error, OffsetEncoding, Result,
};
use helix_core::{ChangeSet, Rope};
// use std::collections::HashMap;
use std::future::Future;
use std::sync::atomic::{AtomicU64, Ordering};
use helix_core::{chars::char_is_line_ending, find_root, ChangeSet, Rope};
use jsonrpc_core as jsonrpc;
use lsp_types as lsp;
use serde_json::Value;
use std::future::Future;
use std::process::Stdio;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::{
io::{BufReader, BufWriter},
// prelude::*,
process::{Child, Command},
sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
};
#[derive(Debug)]
pub struct Client {
id: usize,
_process: Child,
outgoing: UnboundedSender<Payload>,
// pub incoming: Receiver<Call>,
pub request_counter: AtomicU64,
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
capabilities: Option<lsp::ServerCapabilities>,
offset_encoding: OffsetEncoding,
}
impl Client {
pub fn start(cmd: &str, args: &[String]) -> Result<(Self, UnboundedReceiver<Call>)> {
pub fn start(
cmd: &str,
args: &[String],
id: usize,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> {
let process = Command::new(cmd)
.args(args)
.stdin(Stdio::piped())
@@ -43,40 +41,32 @@ impl Client {
.kill_on_drop(true)
.spawn();
// use std::io::ErrorKind;
let mut process = match process {
Ok(process) => process,
Err(err) => match err.kind() {
// ErrorKind::NotFound | ErrorKind::PermissionDenied => {
// return Err(Error::Other(err.into()))
// }
_kind => return Err(Error::Other(err.into())),
},
};
let mut process = process?;
// TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
let (incoming, outgoing) = Transport::start(reader, writer, stderr);
let (server_rx, server_tx) = Transport::start(reader, writer, stderr, id);
let client = Self {
id,
_process: process,
outgoing,
// incoming,
server_tx,
request_counter: AtomicU64::new(0),
capabilities: None,
// diagnostics: HashMap::new(),
offset_encoding: OffsetEncoding::Utf8,
};
// TODO: async client.initialize()
// maybe use an arc<atomic> flag
Ok((client, incoming))
Ok((client, server_rx))
}
pub fn id(&self) -> usize {
self.id
}
fn next_request_id(&self) -> jsonrpc::Id {
@@ -106,7 +96,7 @@ impl Client {
}
/// Execute a RPC request on the language server.
pub async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
where
R::Params: serde::Serialize,
R::Result: core::fmt::Debug, // TODO: temporary
@@ -118,17 +108,20 @@ impl Client {
}
/// Execute a RPC request on the language server.
pub fn call<R: lsp::request::Request>(
fn call<R: lsp::request::Request>(
&self,
params: R::Params,
) -> impl Future<Output = Result<Value>>
where
R::Params: serde::Serialize,
{
let outgoing = self.outgoing.clone();
let server_tx = self.server_tx.clone();
let id = self.next_request_id();
async move {
use std::time::Duration;
use tokio::time::timeout;
let params = serde_json::to_value(params)?;
let request = jsonrpc::MethodCall {
@@ -140,32 +133,29 @@ impl Client {
let (tx, mut rx) = channel::<Result<Value>>(1);
outgoing
server_tx
.send(Payload::Request {
chan: tx,
value: request,
})
.map_err(|e| Error::Other(e.into()))?;
use std::time::Duration;
use tokio::time::timeout;
timeout(Duration::from_secs(2), rx.recv())
.await
.map_err(|_| Error::Timeout)? // return Timeout
.unwrap() // TODO: None if channel closed
.ok_or(Error::StreamClosed)?
}
}
/// Send a RPC notification to the language server.
pub fn notify<R: lsp::notification::Notification>(
fn notify<R: lsp::notification::Notification>(
&self,
params: R::Params,
) -> impl Future<Output = Result<()>>
where
R::Params: serde::Serialize,
{
let outgoing = self.outgoing.clone();
let server_tx = self.server_tx.clone();
async move {
let params = serde_json::to_value(params)?;
@@ -176,7 +166,7 @@ impl Client {
params: Self::value_into_params(params),
};
outgoing
server_tx
.send(Payload::Notification(notification))
.map_err(|e| Error::Other(e.into()))?;
@@ -185,46 +175,51 @@ impl Client {
}
/// Reply to a language server RPC call.
pub async fn reply(
pub fn reply(
&self,
id: jsonrpc::Id,
result: core::result::Result<Value, jsonrpc::Error>,
) -> Result<()> {
) -> impl Future<Output = Result<()>> {
use jsonrpc::{Failure, Output, Success, Version};
let output = match result {
Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2),
id,
result,
}),
Err(error) => Output::Failure(Failure {
jsonrpc: Some(Version::V2),
id,
error,
}),
};
let server_tx = self.server_tx.clone();
self.outgoing
.send(Payload::Response(output))
.map_err(|e| Error::Other(e.into()))?;
async move {
let output = match result {
Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2),
id,
result,
}),
Err(error) => Output::Failure(Failure {
jsonrpc: Some(Version::V2),
id,
error,
}),
};
Ok(())
server_tx
.send(Payload::Response(output))
.map_err(|e| Error::Other(e.into()))?;
Ok(())
}
}
// -------------------------------------------------------------------------------------------
// General messages
// -------------------------------------------------------------------------------------------
pub async fn initialize(&mut self) -> Result<()> {
pub(crate) async fn initialize(&mut self) -> Result<()> {
// TODO: delay any requests that are triggered prior to initialize
let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
#[allow(deprecated)]
let params = lsp::InitializeParams {
process_id: Some(std::process::id()),
// root_path is obsolete, use root_uri
root_path: None,
// root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
root_uri: None, // set to project root in the future
root_uri: root,
initialization_options: None,
capabilities: lsp::ClientCapabilities {
text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -247,6 +242,10 @@ impl Client {
}),
..Default::default()
}),
window: Some(lsp::WindowClientCapabilities {
work_done_progress: Some(true),
..Default::default()
}),
..Default::default()
},
trace: None,
@@ -273,6 +272,21 @@ impl Client {
self.notify::<lsp::notification::Exit>(())
}
/// Tries to shut down the language server but returns
/// early if server responds with an error.
pub async fn shutdown_and_exit(&self) -> Result<()> {
self.shutdown().await?;
self.exit().await
}
/// Forcefully shuts down the language server ignoring any errors.
pub async fn force_shutdown(&self) -> Result<()> {
if let Err(e) = self.shutdown().await {
log::warn!("language server failed to terminate gracefully - {}", e);
}
self.exit().await
}
// -------------------------------------------------------------------------------------------
// Text document
// -------------------------------------------------------------------------------------------
@@ -323,8 +337,9 @@ impl Client {
mut character,
} = pos;
for ch in text.chars() {
if ch == '\n' {
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
line += 1;
character = 0;
} else {
@@ -479,6 +494,7 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
// ) -> Result<Vec<lsp::CompletionItem>> {
let params = lsp::CompletionParams {
@@ -487,9 +503,7 @@ impl Client {
position,
},
// TODO: support these tokens by async receiving and updating the choice list
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
@@ -504,15 +518,14 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::SignatureHelpParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
position,
},
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
context: None,
// lsp::SignatureHelpContext
};
@@ -524,15 +537,14 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::HoverParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
position,
},
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
// lsp::SignatureHelpContext
};
@@ -545,6 +557,7 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> anyhow::Result<Vec<lsp::TextEdit>> {
let capabilities = self.capabilities.as_ref().unwrap();
@@ -559,9 +572,7 @@ impl Client {
let params = lsp::DocumentFormattingParams {
text_document,
options,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let response = self.request::<lsp::request::Formatting>(params).await?;
@@ -574,6 +585,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> anyhow::Result<Vec<lsp::TextEdit>> {
let capabilities = self.capabilities.as_ref().unwrap();
@@ -589,9 +601,7 @@ impl Client {
text_document,
range,
options,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let response = self
@@ -610,15 +620,14 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::GotoDefinitionParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
position,
},
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
@@ -631,30 +640,42 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoDefinition>(text_document, position)
self.goto_request::<lsp::request::GotoDefinition>(text_document, position, work_done_token)
}
pub fn goto_type_definition(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoTypeDefinition>(text_document, position)
self.goto_request::<lsp::request::GotoTypeDefinition>(
text_document,
position,
work_done_token,
)
}
pub fn goto_implementation(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoImplementation>(text_document, position)
self.goto_request::<lsp::request::GotoImplementation>(
text_document,
position,
work_done_token,
)
}
pub fn goto_reference(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::ReferenceParams {
text_document_position: lsp::TextDocumentPositionParams {
@@ -664,9 +685,7 @@ impl Client {
context: lsp::ReferenceContext {
include_declaration: true,
},
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
@@ -674,4 +693,17 @@ impl Client {
self.call::<lsp::request::References>(params)
}
pub fn document_symbols(
&self,
text_document: lsp::TextDocumentIdentifier,
) -> impl Future<Output = Result<Value>> {
let params = lsp::DocumentSymbolParams {
text_document,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};
self.call::<lsp::request::DocumentSymbolRequest>(params)
}
}

View File

@@ -1,26 +1,30 @@
mod client;
mod select_all;
mod transport;
pub use client::Client;
pub use futures_executor::block_on;
pub use jsonrpc::Call;
pub use jsonrpc_core as jsonrpc;
pub use lsp::{Position, Url};
pub use lsp_types as lsp;
pub use client::Client;
pub use lsp::{Position, Url};
pub type Result<T> = core::result::Result<T, Error>;
use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::LanguageConfiguration;
use thiserror::Error;
use std::{collections::HashMap, sync::Arc};
use std::{
collections::{hash_map::Entry, HashMap},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream;
pub use futures_executor::block_on;
pub type Result<T> = core::result::Result<T, Error>;
type LanguageId = String;
#[derive(Error, Debug)]
pub enum Error {
@@ -28,8 +32,14 @@ pub enum Error {
Rpc(#[from] jsonrpc::Error),
#[error("failed to parse: {0}")]
Parse(#[from] serde_json::Error),
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
#[error("request timed out")]
Timeout,
#[error("server closed the stream")]
StreamClosed,
#[error("LSP not defined")]
LspNotDefined,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
@@ -48,23 +58,54 @@ pub mod util {
use super::*;
use helix_core::{Range, Rope, Transaction};
/// Converts [`lsp::Position`] to a position in the document.
///
/// Returns `None` if position exceeds document length or an operation overflows.
pub fn lsp_pos_to_pos(
doc: &Rope,
pos: lsp::Position,
offset_encoding: OffsetEncoding,
) -> usize {
) -> Option<usize> {
let max_line = doc.lines().count().saturating_sub(1);
let pos_line = pos.line as usize;
let pos_line = if pos_line > max_line {
return None;
} else {
pos_line
};
match offset_encoding {
OffsetEncoding::Utf8 => {
let line = doc.line_to_char(pos.line as usize);
line + pos.character as usize
let max_char = doc
.line_to_char(max_line)
.checked_add(doc.line(max_line).len_chars())?;
let line = doc.line_to_char(pos_line);
let pos = line.checked_add(pos.character as usize)?;
if pos <= max_char {
Some(pos)
} else {
None
}
}
OffsetEncoding::Utf16 => {
let line = doc.line_to_char(pos.line as usize);
let max_char = doc
.line_to_char(max_line)
.checked_add(doc.line(max_line).len_chars())?;
let max_cu = doc.char_to_utf16_cu(max_char);
let line = doc.line_to_char(pos_line);
let line_start = doc.char_to_utf16_cu(line);
doc.utf16_cu_to_char(line_start + pos.character as usize)
let pos = line_start.checked_add(pos.character as usize)?;
if pos <= max_cu {
Some(doc.utf16_cu_to_char(pos))
} else {
None
}
}
}
}
/// Converts position in the document to [`lsp::Position`].
///
/// Panics when `pos` is out of `doc` bounds or operation overflows.
pub fn pos_to_lsp_pos(
doc: &Rope,
pos: usize,
@@ -88,6 +129,7 @@ pub mod util {
}
}
/// Converts a range in the document to [`lsp::Range`].
pub fn range_to_lsp_range(
doc: &Rope,
range: Range,
@@ -99,6 +141,17 @@ pub mod util {
lsp::Range::new(start, end)
}
pub fn lsp_range_to_range(
doc: &Rope,
range: lsp::Range,
offset_encoding: OffsetEncoding,
) -> Option<Range> {
let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?;
let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?;
Some(Range::new(start, end))
}
pub fn generate_transaction_from_edits(
doc: &Rope,
edits: Vec<lsp::TextEdit>,
@@ -114,14 +167,45 @@ pub mod util {
None
};
let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding);
let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding);
let start =
if let Some(start) = lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
start
} else {
return (0, 0, None);
};
let end = if let Some(end) = lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
end
} else {
return (0, 0, None);
};
(start, end, replacement)
}),
)
}
}
// apply_insert_replace_edit
#[derive(Debug, PartialEq, Clone)]
pub enum MethodCall {
WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams),
}
impl MethodCall {
pub fn parse(method: &str, params: jsonrpc::Params) -> Option<MethodCall> {
use lsp::request::Request;
let request = match method {
lsp::request::WorkDoneProgressCreate::METHOD => {
let params: lsp::WorkDoneProgressCreateParams = params
.parse()
.expect("Failed to parse WorkDoneCreate params");
Self::WorkDoneProgressCreate(params)
}
_ => {
log::warn!("unhandled lsp request: {}", method);
return None;
}
};
Some(request)
}
}
#[derive(Debug, PartialEq, Clone)]
@@ -129,6 +213,7 @@ pub enum Notification {
PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams),
ProgressMessage(lsp::ProgressParams),
}
impl Notification {
@@ -146,17 +231,20 @@ impl Notification {
}
lsp::notification::ShowMessage::METHOD => {
let params: lsp::ShowMessageParams =
params.parse().expect("Failed to parse ShowMessage params");
let params: lsp::ShowMessageParams = params.parse().ok()?;
Self::ShowMessage(params)
}
lsp::notification::LogMessage::METHOD => {
let params: lsp::LogMessageParams =
params.parse().expect("Failed to parse ShowMessage params");
let params: lsp::LogMessageParams = params.parse().ok()?;
Self::LogMessage(params)
}
lsp::notification::Progress::METHOD => {
let params: lsp::ProgressParams = params.parse().ok()?;
Self::ProgressMessage(params)
}
_ => {
log::error!("unhandled LSP notification: {}", method);
return None;
@@ -167,16 +255,12 @@ impl Notification {
}
}
pub use jsonrpc::Call;
type LanguageId = String;
use crate::select_all::SelectAll;
#[derive(Debug)]
pub struct Registry {
inner: HashMap<LanguageId, Option<Arc<Client>>>,
inner: HashMap<LanguageId, (usize, Arc<Client>)>,
pub incoming: SelectAll<UnboundedReceiverStream<Call>>,
counter: AtomicUsize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
}
impl Default for Registry {
@@ -189,39 +273,124 @@ impl Registry {
pub fn new() -> Self {
Self {
inner: HashMap::new(),
counter: AtomicUsize::new(0),
incoming: SelectAll::new(),
}
}
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Option<Arc<Client>> {
// TODO: propagate the error
pub fn get_by_id(&mut self, id: usize) -> Option<&Client> {
self.inner
.values()
.find(|(client_id, _)| client_id == &id)
.map(|(_, client)| client.as_ref())
}
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
if let Some(config) = &language_config.language_server {
// avoid borrow issues
let inner = &mut self.inner;
let s_incoming = &self.incoming;
let language_server = inner
.entry(language_config.scope.clone()) // can't use entry with Borrow keys: https://github.com/rust-lang/rfcs/pull/1769
.or_insert_with(|| {
// TODO: lookup defaults for id (name, args)
let s_incoming = &mut self.incoming;
match inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(entry.get().1.clone()),
Entry::Vacant(entry) => {
// initialize a new client
let (mut client, incoming) =
Client::start(&config.command, &config.args).ok()?;
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let (mut client, incoming) = Client::start(&config.command, &config.args, id)?;
// TODO: run this async without blocking
futures_executor::block_on(client.initialize()).unwrap();
futures_executor::block_on(client.initialize())?;
s_incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client);
Some(Arc::new(client))
})
.clone();
return language_server;
entry.insert((id, client.clone()));
Ok(client)
}
}
} else {
Err(Error::LspNotDefined)
}
}
None
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().map(|(_, client)| client)
}
}
#[derive(Debug)]
pub enum ProgressStatus {
Created,
Started(lsp::WorkDoneProgress),
}
impl ProgressStatus {
pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
match &self {
ProgressStatus::Created => None,
ProgressStatus::Started(progress) => Some(progress),
}
}
}
#[derive(Default, Debug)]
/// 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>>);
impl LspProgressMap {
pub fn new() -> Self {
Self::default()
}
/// Returns a map of all tokens coresponding to the lanaguage server with `id`.
pub fn progress_map(&self, id: usize) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
self.0.get(&id)
}
pub fn is_progressing(&self, id: usize) -> 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> {
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 {
self.0
.get(&id)
.map(|values| values.get(token).is_some())
.unwrap_or_default()
}
pub fn create(&mut self, id: usize, token: lsp::ProgressToken) {
self.0
.entry(id)
.or_default()
.insert(token, ProgressStatus::Created);
}
/// Ends the progress by removing the `token` from server with `id`, if removed returns the value.
pub fn end_progress(
&mut self,
id: usize,
token: &lsp::ProgressToken,
) -> Option<ProgressStatus> {
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
}
/// Updates the progess of `token` for server with `id` to `status`, returns the value replaced or `None`.
pub fn update(
&mut self,
id: usize,
token: lsp::ProgressToken,
status: lsp::WorkDoneProgress,
) -> Option<ProgressStatus> {
self.0
.entry(id)
.or_default()
.insert(token, ProgressStatus::Started(status))
}
}
@@ -250,3 +419,34 @@ impl Registry {
// there needs to be a way to process incoming lsp messages from all clients.
// -> notifications need to be dispatched to wherever
// -> requests need to generate a reply and travel back to the same lsp!
#[cfg(test)]
mod tests {
use super::{lsp, util::*, OffsetEncoding};
use helix_core::Rope;
#[test]
fn converts_lsp_pos_to_pos() {
macro_rules! test_case {
($doc:expr, ($x:expr, $y:expr) => $want:expr) => {
let doc = Rope::from($doc);
let pos = lsp::Position::new($x, $y);
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf16));
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf8))
};
}
test_case!("", (0, 0) => Some(0));
test_case!("", (0, 1) => None);
test_case!("", (1, 0) => None);
test_case!("\n\n", (0, 0) => Some(0));
test_case!("\n\n", (1, 0) => Some(1));
test_case!("\n\n", (1, 1) => Some(2));
test_case!("\n\n", (2, 0) => Some(2));
test_case!("\n\n", (3, 0) => None);
test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
test_case!("test\n\n\n\ncase", (4, 5) => None);
test_case!("", (u32::MAX, u32::MAX) => None);
}
}

View File

@@ -1,143 +0,0 @@
//! An unbounded set of streams
use core::{
fmt::{self, Debug},
iter::FromIterator,
pin::Pin,
};
use std::task::{Context, Poll};
use futures_util::stream::{FusedStream, FuturesUnordered, StreamExt, StreamFuture};
use futures_util::{ready, stream::Stream};
/// An unbounded set of streams
///
/// This "combinator" provides the ability to maintain a set of streams
/// and drive them all to completion.
///
/// Streams are pushed into this set and their realized values are
/// yielded as they become ready. Streams will only be polled when they
/// generate notifications. This allows to coordinate a large number of streams.
///
/// Note that you can create a ready-made `SelectAll` via the
/// `select_all` function in the `stream` module, or you can start with an
/// empty set with the `SelectAll::new` constructor.
#[must_use = "streams do nothing unless polled"]
pub struct SelectAll<St> {
inner: FuturesUnordered<StreamFuture<St>>,
}
impl<St: Debug> Debug for SelectAll<St> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "SelectAll {{ ... }}")
}
}
impl<St: Stream + Unpin> SelectAll<St> {
/// Constructs a new, empty `SelectAll`
///
/// The returned `SelectAll` does not contain any streams and, in this
/// state, `SelectAll::poll` will return `Poll::Ready(None)`.
pub fn new() -> Self {
Self {
inner: FuturesUnordered::new(),
}
}
/// Returns the number of streams contained in the set.
///
/// This represents the total number of in-flight streams.
pub fn len(&self) -> usize {
self.inner.len()
}
/// Returns `true` if the set contains no streams
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
/// Push a stream into the set.
///
/// This function submits the given stream to the set for managing. This
/// function will not call `poll` on the submitted stream. The caller must
/// ensure that `SelectAll::poll` is called in order to receive task
/// notifications.
pub fn push(&self, stream: St) {
self.inner.push(stream.into_future());
}
}
impl<St: Stream + Unpin> Default for SelectAll<St> {
fn default() -> Self {
Self::new()
}
}
impl<St: Stream + Unpin> Stream for SelectAll<St> {
type Item = St::Item;
fn poll_next(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
loop {
match ready!(self.inner.poll_next_unpin(cx)) {
Some((Some(item), remaining)) => {
self.push(remaining);
return Poll::Ready(Some(item));
}
Some((None, _)) => {
// `FuturesUnordered` thinks it isn't terminated
// because it yielded a Some.
// We do not return, but poll `FuturesUnordered`
// in the next loop iteration.
}
None => return Poll::Ready(None),
}
}
}
}
impl<St: Stream + Unpin> FusedStream for SelectAll<St> {
fn is_terminated(&self) -> bool {
self.inner.is_terminated()
}
}
/// Convert a list of streams into a `Stream` of results from the streams.
///
/// This essentially takes a list of streams (e.g. a vector, an iterator, etc.)
/// and bundles them together into a single stream.
/// The stream will yield items as they become available on the underlying
/// streams internally, in the order they become available.
///
/// Note that the returned set can also be used to dynamically push more
/// futures into the set as they become available.
///
/// This function is only available when the `std` or `alloc` feature of this
/// library is activated, and it is activated by default.
pub fn select_all<I>(streams: I) -> SelectAll<I::Item>
where
I: IntoIterator,
I::Item: Stream + Unpin,
{
let set = SelectAll::new();
for stream in streams {
set.push(stream);
}
set
}
impl<St: Stream + Unpin> FromIterator<St> for SelectAll<St> {
fn from_iter<T: IntoIterator<Item = St>>(iter: T) -> Self {
select_all(iter)
}
}
impl<St: Stream + Unpin> Extend<St> for SelectAll<St> {
fn extend<T: IntoIterator<Item = St>>(&mut self, iter: T) {
for st in iter {
self.push(st)
}
}
}

View File

@@ -1,15 +1,10 @@
use std::collections::HashMap;
use std::io;
use log::{error, info};
use crate::Error;
type Result<T> = core::result::Result<T, Error>;
use crate::Result;
use anyhow::Context;
use jsonrpc_core as jsonrpc;
use log::{error, info};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use tokio::{
io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
process::{ChildStderr, ChildStdin, ChildStdout},
@@ -26,47 +21,51 @@ pub enum Payload {
Response(jsonrpc::Output),
}
use serde::{Deserialize, Serialize};
/// A type representing all possible values sent from the server to the client.
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
enum Message {
enum ServerMessage {
/// A regular JSON-RPC request output (single response).
Output(jsonrpc::Output),
/// A JSON-RPC request or notification.
Call(jsonrpc::Call),
}
#[derive(Debug)]
pub struct Transport {
incoming: UnboundedSender<jsonrpc::Call>,
outgoing: UnboundedReceiver<Payload>,
id: usize,
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
client_rx: UnboundedReceiver<Payload>,
pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
headers: HashMap<String, String>,
writer: BufWriter<ChildStdin>,
reader: BufReader<ChildStdout>,
stderr: BufReader<ChildStderr>,
server_stdin: BufWriter<ChildStdin>,
server_stdout: BufReader<ChildStdout>,
server_stderr: BufReader<ChildStderr>,
}
impl Transport {
pub fn start(
reader: BufReader<ChildStdout>,
writer: BufWriter<ChildStdin>,
stderr: BufReader<ChildStderr>,
) -> (UnboundedReceiver<jsonrpc::Call>, UnboundedSender<Payload>) {
let (incoming, rx) = unbounded_channel();
let (tx, outgoing) = unbounded_channel();
server_stdout: BufReader<ChildStdout>,
server_stdin: BufWriter<ChildStdin>,
server_stderr: BufReader<ChildStderr>,
id: usize,
) -> (
UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedSender<Payload>,
) {
let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel();
let transport = Self {
reader,
writer,
stderr,
incoming,
outgoing,
id,
server_stdout,
server_stdin,
server_stderr,
client_tx,
client_rx,
pending_requests: HashMap::default(),
headers: HashMap::default(),
};
tokio::spawn(transport.duplex());
@@ -74,105 +73,106 @@ impl Transport {
(rx, tx)
}
async fn recv(
async fn recv_server_message(
reader: &mut (impl AsyncBufRead + Unpin + Send),
headers: &mut HashMap<String, String>,
) -> core::result::Result<Message, std::io::Error> {
// read headers
buffer: &mut String,
) -> Result<ServerMessage> {
let mut content_length = None;
loop {
let mut header = String::new();
// detect pipe closed if 0
reader.read_line(&mut header).await?;
let header = header.trim();
buffer.truncate(0);
reader.read_line(buffer).await?;
let header = buffer.trim();
if header.is_empty() {
break;
}
let parts: Vec<&str> = header.split(": ").collect();
if parts.len() != 2 {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to parse header",
));
let mut parts = header.split(": ");
match (parts.next(), parts.next(), parts.next()) {
(Some("Content-Length"), Some(value), None) => {
content_length = Some(value.parse().context("invalid content length")?);
}
(Some(_), Some(_), None) => {}
_ => {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to parse header",
)
.into());
}
}
headers.insert(parts[0].to_string(), parts[1].to_string());
}
// find content-length
let content_length = headers.get("Content-Length").unwrap().parse().unwrap();
let content_length = content_length.context("missing content length")?;
//TODO: reuse vector
let mut content = vec![0; content_length];
reader.read_exact(&mut content).await?;
let msg = String::from_utf8(content).unwrap();
// read data
let msg = String::from_utf8(content).context("invalid utf8 from server")?;
info!("<- {}", msg);
// try parsing as output (server response) or call (server request)
let output: serde_json::Result<Message> = serde_json::from_str(&msg);
let output: serde_json::Result<ServerMessage> = serde_json::from_str(&msg);
Ok(output?)
}
async fn err(
async fn recv_server_error(
err: &mut (impl AsyncBufRead + Unpin + Send),
) -> core::result::Result<(), std::io::Error> {
let mut line = String::new();
err.read_line(&mut line).await?;
error!("err <- {}", line);
buffer: &mut String,
) -> Result<()> {
buffer.truncate(0);
err.read_line(buffer).await?;
error!("err <- {}", buffer);
Ok(())
}
pub async fn send_payload(&mut self, payload: Payload) -> io::Result<()> {
match payload {
async fn send_payload_to_server(&mut self, payload: Payload) -> Result<()> {
//TODO: reuse string
let json = match payload {
Payload::Request { chan, value } => {
self.pending_requests.insert(value.id.clone(), chan);
let json = serde_json::to_string(&value)?;
self.send(json).await
serde_json::to_string(&value)?
}
Payload::Notification(value) => {
let json = serde_json::to_string(&value)?;
self.send(json).await
}
Payload::Response(error) => {
let json = serde_json::to_string(&error)?;
self.send(json).await
}
}
Payload::Notification(value) => serde_json::to_string(&value)?,
Payload::Response(error) => serde_json::to_string(&error)?,
};
self.send_string_to_server(json).await
}
pub async fn send(&mut self, request: String) -> io::Result<()> {
async fn send_string_to_server(&mut self, request: String) -> Result<()> {
info!("-> {}", request);
// send the headers
self.writer
self.server_stdin
.write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes())
.await?;
// send the body
self.writer.write_all(request.as_bytes()).await?;
self.server_stdin.write_all(request.as_bytes()).await?;
self.writer.flush().await?;
self.server_stdin.flush().await?;
Ok(())
}
async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> {
async fn process_server_message(&mut self, msg: ServerMessage) -> Result<()> {
match msg {
Message::Output(output) => self.recv_response(output).await?,
Message::Call(call) => {
self.incoming.send(call).unwrap();
ServerMessage::Output(output) => self.process_request_response(output).await?,
ServerMessage::Call(call) => {
self.client_tx
.send((self.id, call))
.context("failed to send a message to server")?;
// let notification = Notification::parse(&method, params);
}
};
Ok(())
}
async fn recv_response(&mut self, output: jsonrpc::Output) -> io::Result<()> {
async fn process_request_response(&mut self, output: jsonrpc::Output) -> Result<()> {
let (id, result) = match output {
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
info!("<- {}", result);
@@ -200,29 +200,33 @@ impl Transport {
Ok(())
}
pub async fn duplex(mut self) {
async fn duplex(mut self) {
let mut recv_buffer = String::new();
let mut err_buffer = String::new();
loop {
tokio::select! {
// client -> server
msg = self.outgoing.recv() => {
if msg.is_none() {
break;
msg = self.client_rx.recv() => {
match msg {
Some(msg) => {
self.send_payload_to_server(msg).await.unwrap()
},
None => break
}
let msg = msg.unwrap();
self.send_payload(msg).await.unwrap();
}
// server <- client
msg = Self::recv(&mut self.reader, &mut self.headers) => {
if msg.is_err() {
error!("err: <- {:?}", msg);
break;
// server -> client
msg = Self::recv_server_message(&mut self.server_stdout, &mut recv_buffer) => {
match msg {
Ok(msg) => {
self.process_server_message(msg).await.unwrap();
}
Err(_) => {
error!("err: <- {:?}", msg);
break;
},
}
let msg = msg.unwrap();
self.recv_msg(msg).await.unwrap();
}
_msg = Self::err(&mut self.stderr) => {}
_msg = Self::recv_server_error(&mut self.server_stderr, &mut err_buffer) => {}
}
}
}

View File

@@ -1,11 +1,14 @@
[package]
name = "helix-syntax"
version = "0.1.0"
version = "0.3.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "Tree-sitter grammars support"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"]
[dependencies]
tree-sitter = "0.19"

View File

@@ -1,16 +1,8 @@
use std::fs;
use std::path::PathBuf;
use std::{env, fs};
use std::sync::mpsc::channel;
fn get_opt_level() -> u32 {
env::var("OPT_LEVEL").unwrap().parse::<u32>().unwrap()
}
fn get_debug() -> bool {
env::var("DEBUG").unwrap() == "true"
}
fn collect_tree_sitter_dirs(ignore: &[String]) -> Vec<String> {
let mut dirs = Vec::new();
for entry in fs::read_dir("languages").unwrap().flatten() {
@@ -58,25 +50,28 @@ fn build_c(files: Vec<String>, language: &str) {
.file(&file)
.include(PathBuf::from(file).parent().unwrap())
.pic(true)
.opt_level(get_opt_level())
.debug(get_debug())
.warnings(false)
.flag_if_supported("-std=c99");
.warnings(false);
}
build.compile(&format!("tree-sitter-{}-c", language));
}
fn build_cpp(files: Vec<String>, language: &str) {
let mut build = cc::Build::new();
let flag = if build.get_compiler().is_like_msvc() {
"/std:c++17"
} else {
"-std=c++14"
};
for file in files {
build
.file(&file)
.include(PathBuf::from(file).parent().unwrap())
.pic(true)
.opt_level(get_opt_level())
.debug(get_debug())
.warnings(false)
.cpp(true);
.cpp(true)
.flag_if_supported(flag);
}
build.compile(&format!("tree-sitter-{}-cpp", language));
}
@@ -107,7 +102,11 @@ fn build_dir(dir: &str, language: &str) {
}
fn main() {
let ignore = vec!["tree-sitter-typescript".to_string()];
let ignore = vec![
"tree-sitter-typescript".to_string(),
"tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34
".DS_Store".to_string(),
];
let dirs = collect_tree_sitter_dirs(&ignore);
let mut n_jobs = 0;

View File

@@ -68,17 +68,20 @@ mk_langs!(
// 2) tree-sitter function to call to get a Language
(Agda, tree_sitter_agda),
(Bash, tree_sitter_bash),
(C, tree_sitter_c),
(CSharp, tree_sitter_c_sharp),
(Cpp, tree_sitter_cpp),
(CSharp, tree_sitter_c_sharp),
(Css, tree_sitter_css),
(C, tree_sitter_c),
(Elixir, tree_sitter_elixir),
(Go, tree_sitter_go),
// (Haskell, tree_sitter_haskell),
(Html, tree_sitter_html),
(Java, tree_sitter_java),
(Javascript, tree_sitter_javascript),
(Java, tree_sitter_java),
(Json, tree_sitter_json),
(Julia, tree_sitter_julia),
(Latex, tree_sitter_latex),
(Nix, tree_sitter_nix),
(Php, tree_sitter_php),
(Python, tree_sitter_python),
(Ruby, tree_sitter_ruby),

View File

@@ -1,30 +1,38 @@
[package]
name = "helix-term"
version = "0.1.0"
version = "0.3.0"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[package.metadata.nix]
build = true
app = true
[features]
embed_runtime = ["helix-core/embed_runtime"]
[[bin]]
name = "hx"
path = "src/main.rs"
[dependencies]
helix-core = { path = "../helix-core" }
helix-view = { path = "../helix-view", features = ["term"]}
helix-lsp = { path = "../helix-lsp"}
helix-core = { version = "0.3", path = "../helix-core" }
helix-view = { version = "0.3", path = "../helix-view" }
helix-lsp = { version = "0.3", path = "../helix-lsp" }
anyhow = "1"
once_cell = "1.4"
once_cell = "1.8"
tokio = { version = "1", features = ["full"] }
num_cpus = "1"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.19", features = ["event-stream"] }
pico-args = "0.4"
crossterm = { version = "0.20", features = ["event-stream"] }
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }

View File

@@ -1,7 +0,0 @@
window -> buffer -> text
\-> contains "view", a viewport into the buffer
view
\-> selections etc
-> cursor

View File

@@ -1,28 +1,35 @@
use helix_view::{document::Mode, Document, Editor, Theme, View};
use helix_core::syntax;
use helix_lsp::{lsp, LspProgressMap};
use helix_view::{document::Mode, graphics::Rect, theme, Document, Editor, Theme, View};
use crate::{compositor::Compositor, ui, Args};
use crate::{
args::Args,
compositor::Compositor,
config::Config,
keymap::Keymaps,
ui::{self, Spinner},
};
use log::{error, info};
use std::{
collections::HashMap,
future::Future,
io::{self, stdout, Stdout, Write},
path::PathBuf,
pin::Pin,
sync::Arc,
time::Duration,
};
use anyhow::Error;
use anyhow::{Context, Error};
use crossterm::{
event::{Event, EventStream},
execute, terminal,
};
use tui::layout::Rect;
use futures_util::stream::FuturesUnordered;
use std::pin::Pin;
use futures_util::{future, stream::FuturesUnordered};
type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
pub type LspCallback =
@@ -35,31 +42,86 @@ pub struct Application {
compositor: Compositor,
editor: Editor,
// TODO should be separate to take only part of the config
config: Config,
theme_loader: Arc<theme::Loader>,
syn_loader: Arc<syntax::Loader>,
callbacks: LspCallbacks,
lsp_progress: LspProgressMap,
}
impl Application {
pub fn new(mut args: Args) -> Result<Self, Error> {
pub fn new(mut args: Args, mut config: Config) -> Result<Self, Error> {
use helix_view::editor::Action;
let mut compositor = Compositor::new()?;
let size = compositor.size();
let mut editor = Editor::new(size);
let conf_dir = helix_core::config_dir();
let theme_loader =
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
// load $HOME/.config/helix/languages.toml, fallback to default config
let lang_conf = std::fs::read(conf_dir.join("languages.toml"));
let lang_conf = lang_conf
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
let theme = if let Some(theme) = &config.theme {
match theme_loader.load(theme) {
Ok(theme) => theme,
Err(e) => {
log::warn!("failed to load theme `{}` - {}", theme, e);
theme_loader.default()
}
}
} else {
theme_loader.default()
};
let syn_loader_conf = toml::from_slice(lang_conf).expect("Could not parse languages.toml");
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut editor = Editor::new(size, theme_loader.clone(), syn_loader.clone());
let mut editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
compositor.push(editor_view);
if !args.files.is_empty() {
for file in args.files {
editor.open(file, Action::VerticalSplit)?;
let first = &args.files[0]; // we know it's not empty
if first.is_dir() {
editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(first.clone())));
} else {
for file in args.files {
if file.is_dir() {
return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
));
} else {
editor.open(file, Action::VerticalSplit)?;
}
}
}
} else {
editor.new_file(Action::VerticalSplit);
}
compositor.push(Box::new(ui::EditorView::new()));
editor.set_theme(theme);
let mut app = Self {
compositor,
editor,
config,
theme_loader,
syn_loader,
callbacks: FuturesUnordered::new(),
lsp_progress: LspProgressMap::new(),
};
Ok(app)
@@ -89,14 +151,26 @@ impl Application {
break;
}
use futures_util::StreamExt;
use futures_util::{FutureExt, StreamExt};
tokio::select! {
event = reader.next() => {
self.handle_terminal_events(event)
}
Some(call) = self.editor.language_servers.incoming.next() => {
self.handle_language_server_message(call).await
Some((id, call)) = self.editor.language_servers.incoming.next() => {
self.handle_language_server_message(call, id).await;
// eagerly process any other available notifications/calls
let now = std::time::Instant::now();
let deadline = std::time::Duration::from_millis(10);
while let Some(Some((id, call))) = self.editor.language_servers.incoming.next().now_or_never() {
self.handle_language_server_message(call, id).await;
if now.elapsed() > deadline { // use a deadline so we don't block too long
break;
}
}
self.render();
}
Some(callback) = &mut self.callbacks.next() => {
self.handle_language_server_callback(callback)
@@ -139,8 +213,21 @@ impl Application {
}
}
pub async fn handle_language_server_message(&mut self, call: helix_lsp::Call) {
use helix_lsp::{Call, Notification};
pub async fn handle_language_server_message(
&mut self,
call: helix_lsp::Call,
server_id: usize,
) {
use helix_lsp::{Call, MethodCall, Notification};
let editor_view = self
.compositor
.find(std::any::type_name::<ui::EditorView>())
.expect("expected at least one EditorView");
let editor_view = editor_view
.as_any_mut()
.downcast_mut::<ui::EditorView>()
.unwrap();
match call {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) {
@@ -148,7 +235,6 @@ impl Application {
None => return,
};
// TODO: parse should return Result/Option
match notification {
Notification::PublishDiagnostics(params) => {
let path = Some(params.uri.to_file_path().unwrap());
@@ -165,7 +251,7 @@ impl Application {
let diagnostics = params
.diagnostics
.into_iter()
.map(|diagnostic| {
.filter_map(|diagnostic| {
use helix_core::{
diagnostic::{Range, Severity, Severity::*},
Diagnostic,
@@ -176,18 +262,29 @@ impl Application {
let language_server = doc.language_server().unwrap();
// TODO: convert inside server
let start = lsp_pos_to_pos(
let start = if let Some(start) = lsp_pos_to_pos(
text,
diagnostic.range.start,
language_server.offset_encoding(),
);
let end = lsp_pos_to_pos(
) {
start
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
let end = if let Some(end) = lsp_pos_to_pos(
text,
diagnostic.range.end,
language_server.offset_encoding(),
);
) {
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
Diagnostic {
Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
message: diagnostic.message,
@@ -201,13 +298,11 @@ impl Application {
),
// code
// source
}
})
})
.collect();
doc.diagnostics = diagnostics;
// TODO: we want to process all the events in queue, then render. publishDiagnostic tends to send a whole bunch of events
self.render();
doc.set_diagnostics(diagnostics);
}
}
Notification::ShowMessage(params) => {
@@ -216,12 +311,144 @@ impl Application {
Notification::LogMessage(params) => {
log::warn!("unhandled window/logMessage: {:?}", params);
}
Notification::ProgressMessage(params) => {
let lsp::ProgressParams { token, value } = params;
let lsp::ProgressParamsValue::WorkDone(work) = value;
let parts = match &work {
lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
title,
message,
percentage,
..
}) => (Some(title), message, percentage),
lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
message,
percentage,
..
}) => (None, message, percentage),
lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd { message }) => {
if message.is_some() {
(None, message, &None)
} else {
self.lsp_progress.end_progress(server_id, &token);
if !self.lsp_progress.is_progressing(server_id) {
editor_view.spinners_mut().get_or_create(server_id).stop();
}
self.editor.clear_status();
// we want to render to clear any leftover spinners or messages
return;
}
}
};
let token_d: &dyn std::fmt::Display = match &token {
lsp::NumberOrString::Number(n) => n,
lsp::NumberOrString::String(s) => s,
};
let status = match parts {
(Some(title), Some(message), Some(percentage)) => {
format!("[{}] {}% {} - {}", token_d, percentage, title, message)
}
(Some(title), None, Some(percentage)) => {
format!("[{}] {}% {}", token_d, percentage, title)
}
(Some(title), Some(message), None) => {
format!("[{}] {} - {}", token_d, title, message)
}
(None, Some(message), Some(percentage)) => {
format!("[{}] {}% {}", token_d, percentage, message)
}
(Some(title), None, None) => {
format!("[{}] {}", token_d, title)
}
(None, Some(message), None) => {
format!("[{}] {}", token_d, message)
}
(None, None, Some(percentage)) => {
format!("[{}] {}%", token_d, percentage)
}
(None, None, None) => format!("[{}]", token_d),
};
if let lsp::WorkDoneProgress::End(_) = work {
self.lsp_progress.end_progress(server_id, &token);
if !self.lsp_progress.is_progressing(server_id) {
editor_view.spinners_mut().get_or_create(server_id).stop();
}
} else {
self.lsp_progress.update(server_id, token, work);
}
if self.config.lsp.display_messages {
self.editor.set_status(status);
}
}
_ => unreachable!(),
}
}
Call::MethodCall(call) => {
error!("Method not found {}", call.method);
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method,
params,
jsonrpc,
id,
}) => {
let call = match MethodCall::parse(&method, params) {
Some(call) => call,
None => {
error!("Method not found {}", method);
return;
}
};
match call {
MethodCall::WorkDoneProgressCreate(params) => {
self.lsp_progress.create(server_id, params.token);
let spinner = editor_view.spinners_mut().get_or_create(server_id);
if spinner.is_stopped() {
spinner.start();
}
let doc = self.editor.documents().find(|doc| {
doc.language_server()
.map(|server| server.id() == server_id)
.unwrap_or_default()
});
match doc {
Some(doc) => {
// it's ok to unwrap, we check for the language server before
let server = doc.language_server().unwrap();
tokio::spawn(server.reply(id, Ok(serde_json::Value::Null)));
}
None => {
if let Some(server) =
self.editor.language_servers.get_by_id(server_id)
{
log::warn!(
"missing document with language server id `{}`",
server_id
);
tokio::spawn(server.reply(
id,
Err(helix_lsp::jsonrpc::Error {
code: helix_lsp::jsonrpc::ErrorCode::InternalError,
message: "document missing".to_string(),
data: None,
}),
));
} else {
log::warn!(
"can't find language server with id `{}`",
server_id
);
}
}
}
}
}
// self.language_server.reply(
// call.id,
// // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
@@ -253,6 +480,8 @@ impl Application {
self.event_loop().await;
self.editor.close_language_servers(None).await;
// reset cursor shape
write!(stdout, "\x1B[2 q");

53
helix-term/src/args.rs Normal file
View File

@@ -0,0 +1,53 @@
use anyhow::{Error, Result};
use std::path::PathBuf;
#[derive(Default)]
pub struct Args {
pub display_help: bool,
pub display_version: bool,
pub verbosity: u64,
pub files: Vec<PathBuf>,
}
impl Args {
pub fn parse_args() -> Result<Args> {
let mut args = Args::default();
let argv: Vec<String> = std::env::args().collect();
let mut iter = argv.iter();
iter.next(); // skip the program, we don't care about that
for arg in &mut iter {
match arg.as_str() {
"--" => break, // stop parsing at this point treat the remaining as files
"--version" => args.display_version = true,
"--help" => args.display_help = true,
arg if arg.starts_with("--") => {
return Err(Error::msg(format!(
"unexpected double dash argument: {}",
arg
)))
}
arg if arg.starts_with('-') => {
let arg = arg.get(1..).unwrap().chars();
for chr in arg {
match chr {
'v' => args.verbosity += 1,
'V' => args.display_version = true,
'h' => args.display_help = true,
_ => return Err(Error::msg(format!("unexpected short arg {}", chr))),
}
}
}
arg => args.files.push(PathBuf::from(arg)),
}
}
// push the remaining args, if any to the files
for filename in iter {
args.files.push(PathBuf::from(filename));
}
Ok(args)
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,10 +1,12 @@
// Each component declares it's own size constraints and gets fitted based on it's parent.
// Q: how does this work with popups?
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
use helix_core::Position;
use helix_lsp::LspProgressMap;
use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event;
use helix_core::Position;
use tui::{buffer::Buffer as Surface, layout::Rect};
use tui::buffer::Buffer as Surface;
pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
@@ -47,8 +49,9 @@ pub trait Component: Any + AnyComponent {
/// Render the component onto the provided surface.
fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context);
fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option<Position> {
None
/// Get cursor position and cursor kind.
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
(None, CursorKind::Hidden)
}
/// May be used by the parent component to compute the child area.
@@ -122,27 +125,34 @@ impl Compositor {
}
pub fn render(&mut self, cx: &mut Context) {
let area = self.size();
let area = self
.terminal
.autoresize()
.expect("Unable to determine terminal size");
// TODO: need to recalculate view tree if necessary
let surface = self.terminal.current_buffer_mut();
let area = *surface.area();
for layer in &self.layers {
layer.render(area, surface, cx)
}
let pos = self
.cursor_position(area, cx.editor)
.map(|pos| (pos.col as u16, pos.row as u16));
let (pos, kind) = self.cursor(area, cx.editor);
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos);
self.terminal.draw(pos, kind);
}
pub fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
pub fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
for layer in self.layers.iter().rev() {
if let Some(pos) = layer.cursor_position(area, editor) {
return Some(pos);
if let (Some(pos), kind) = layer.cursor(area, editor) {
return (Some(pos), kind);
}
}
None
(None, CursorKind::Hidden)
}
pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> {
@@ -170,15 +180,13 @@ pub trait AnyComponent {
/// Returns a boxed any from a boxed self.
///
/// Can be used before `Box::downcast()`.
///
/// # Examples
///
/// ```rust
/// # use cursive_core::views::TextComponent;
/// # use cursive_core::view::Component;
/// let boxed: Box<Component> = Box::new(TextComponent::new("text"));
/// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
/// ```
//
// # Examples
//
// ```rust
// let boxed: Box<Component> = Box::new(TextComponent::new("text"));
// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
// ```
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
}

65
helix-term/src/config.rs Normal file
View File

@@ -0,0 +1,65 @@
use anyhow::{Error, Result};
use serde::Deserialize;
use std::collections::HashMap;
use crate::commands::Command;
use crate::keymap::Keymaps;
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
pub struct Config {
pub theme: Option<String>,
#[serde(default)]
pub lsp: LspConfig,
#[serde(default)]
pub keys: Keymaps,
}
#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LspConfig {
pub display_messages: bool,
}
#[test]
fn parsing_keymaps_config_file() {
use helix_core::hashmap;
use helix_view::{
document::Mode,
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
};
let sample_keymaps = r#"
[keys.insert]
y = "move_line_down"
S-C-a = "delete_selection"
[keys.normal]
A-F12 = "move_next_word_end"
"#;
assert_eq!(
toml::from_str::<Config>(sample_keymaps).unwrap(),
Config {
keys: Keymaps(hashmap! {
Mode::Insert => hashmap! {
KeyEvent {
code: KeyCode::Char('y'),
modifiers: KeyModifiers::NONE,
} => Command::move_line_down,
KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL,
} => Command::delete_selection,
},
Mode::Normal => hashmap! {
KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::ALT,
} => Command::move_next_word_end,
},
}),
..Default::default()
}
);
}

View File

@@ -1,7 +1,20 @@
use crate::commands::{self, Command};
use crate::commands;
pub use crate::commands::Command;
use crate::config::Config;
use anyhow::{anyhow, Error, Result};
use helix_core::hashmap;
use helix_view::document::Mode;
use std::collections::HashMap;
use helix_view::{
document::Mode,
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
};
use serde::Deserialize;
use std::{
collections::HashMap,
fmt::Display,
ops::{Deref, DerefMut},
str::FromStr,
};
// Kakoune-inspired:
// mode = {
@@ -11,7 +24,8 @@ use std::collections::HashMap;
// W = next WORD
// e = end of word
// E = end of WORD
// r =
// r = replace
// R = replace with yanked
// t = 'till char
// y = yank
// u = undo
@@ -35,10 +49,10 @@ use std::collections::HashMap;
// f = find_char()
// g = goto (gg, G, gc, gd, etc)
//
// h = move_char_left(n)
// j = move_line_down(n)
// k = move_line_up(n)
// l = move_char_right(n)
// h = move_char_left(n) || arrow-left = move_char_left(n)
// j = move_line_down(n) || arrow-down = move_line_down(n)
// k = move_line_up(n) || arrow_up = move_line_up(n)
// l = move_char_right(n) || arrow-right = move_char_right(n)
// : = command line
// ; = collapse selection to cursor
// " = use register
@@ -61,8 +75,8 @@ use std::collections::HashMap;
// in kakoune these are alt-h alt-l / gh gl
// select from curs to begin end / move curs to begin end
// 0 = start of line
// ^ = start of line (first non blank char)
// $ = end of line
// ^ = start of line(first non blank char) || Home = start of line(first non blank char)
// $ = end of line || End = end of line
//
// z = save selections
// Z = restore selections
@@ -85,16 +99,20 @@ use std::collections::HashMap;
//
// gd = goto definition
// gr = goto reference
// [d = previous diagnostic
// d] = next diagnostic
// [D = first diagnostic
// D] = last diagnostic
// }
// #[cfg(feature = "term")]
pub use crossterm::event::{KeyCode, KeyEvent, KeyModifiers};
pub type Keymap = HashMap<KeyEvent, Command>;
pub type Keymaps = HashMap<Mode, Keymap>;
#[macro_export]
macro_rules! key {
($key:ident) => {
KeyEvent {
code: KeyCode::$key,
modifiers: KeyModifiers::NONE,
}
};
($($ch:tt)*) => {
KeyEvent {
code: KeyCode::Char($($ch)*),
@@ -103,15 +121,6 @@ macro_rules! key {
};
}
macro_rules! shift {
($($ch:tt)*) => {
KeyEvent {
code: KeyCode::Char($($ch)*),
modifiers: KeyModifiers::SHIFT,
}
};
}
macro_rules! ctrl {
($($ch:tt)*) => {
KeyEvent {
@@ -130,197 +139,262 @@ macro_rules! alt {
};
}
pub fn default() -> Keymaps {
let normal = hashmap!(
key!('h') => commands::move_char_left as Command,
key!('j') => commands::move_line_down,
key!('k') => commands::move_line_up,
key!('l') => commands::move_char_right,
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(transparent)]
pub struct Keymaps(pub HashMap<Mode, HashMap<KeyEvent, Command>>);
key!('t') => commands::find_till_char,
key!('f') => commands::find_next_char,
shift!('T') => commands::till_prev_char,
shift!('F') => commands::find_prev_char,
// and matching set for select mode (extend)
//
key!('r') => commands::replace,
impl Deref for Keymaps {
type Target = HashMap<Mode, HashMap<KeyEvent, Command>>;
key!('^') => commands::move_line_start,
key!('$') => commands::move_line_end,
key!('w') => commands::move_next_word_start,
key!('b') => commands::move_prev_word_start,
key!('e') => commands::move_next_word_end,
key!('v') => commands::select_mode,
key!('g') => commands::goto_mode,
key!(':') => commands::command_mode,
key!('i') => commands::insert_mode,
shift!('I') => commands::prepend_to_line,
key!('a') => commands::append_mode,
shift!('A') => commands::append_to_line,
key!('o') => commands::open_below,
shift!('O') => commands::open_above,
// [<space> ]<space> equivalents too (add blank new line, no edit)
key!('d') => commands::delete_selection,
// TODO: also delete without yanking
key!('c') => commands::change_selection,
// TODO: also change delete without yanking
// key!('r') => commands::replace_with_char,
key!('s') => commands::select_regex,
alt!('s') => commands::split_selection_on_newline,
shift!('S') => commands::split_selection,
key!(';') => commands::collapse_selection,
alt!(';') => commands::flip_selections,
key!('%') => commands::select_all,
key!('x') => commands::select_line,
shift!('X') => commands::extend_line,
// or select mode X?
// extend_to_whole_line, crop_to_whole_line
key!('m') => commands::match_brackets,
// TODO: refactor into
// key!('m') => commands::select_to_matching,
// key!('M') => commands::back_select_to_matching,
// select mode extend equivalents
// key!('.') => commands::repeat_insert,
// repeat_select
// TODO: figure out what key to use
key!('[') => commands::expand_selection,
key!('/') => commands::search,
// ? for search_reverse
key!('n') => commands::search_next,
shift!('N') => commands::extend_search_next,
// N for search_prev
key!('*') => commands::search_selection,
key!('u') => commands::undo,
shift!('U') => commands::redo,
key!('y') => commands::yank,
// yank_all
key!('p') => commands::paste_after,
// paste_all
shift!('P') => commands::paste_before,
key!('>') => commands::indent,
key!('<') => commands::unindent,
key!('=') => commands::format_selections,
shift!('J') => commands::join_selections,
// TODO: conflicts hover/doc
shift!('K') => commands::keep_selections,
// TODO: and another method for inverse
// TODO: clashes with space mode
key!(' ') => commands::keep_primary_selection,
// key!('q') => commands::record_macro,
// key!('Q') => commands::replay_macro,
// ~ / apostrophe => change case
// & align selections
// _ trim selections
// C / altC = copy (repeat) selections on prev/next lines
KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE
} => commands::normal_mode,
KeyEvent {
code: KeyCode::PageUp,
modifiers: KeyModifiers::NONE
} => commands::page_up,
KeyEvent {
code: KeyCode::PageDown,
modifiers: KeyModifiers::NONE
} => commands::page_down,
ctrl!('u') => commands::half_page_up,
ctrl!('d') => commands::half_page_down,
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE
} => commands::next_view,
// move under <space>c
ctrl!('c') => commands::toggle_comments,
shift!('K') => commands::hover,
// z family for save/restore/combine from/to sels from register
ctrl!('i') => commands::jump_forward, // TODO: ctrl-i conflicts tab
ctrl!('o') => commands::jump_backward,
// ctrl!('s') => commands::save_selection,
key!(' ') => commands::space_mode,
key!('z') => commands::view_mode,
);
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
// because some selection operations can now be done from normal mode, some from select mode.
let mut select = normal.clone();
select.extend(
hashmap!(
key!('h') => commands::extend_char_left as Command,
key!('j') => commands::extend_line_down,
key!('k') => commands::extend_line_up,
key!('l') => commands::extend_char_right,
key!('w') => commands::extend_next_word_start,
key!('b') => commands::extend_prev_word_start,
key!('e') => commands::extend_next_word_end,
key!('t') => commands::extend_till_char,
key!('f') => commands::extend_next_char,
shift!('T') => commands::extend_till_prev_char,
shift!('F') => commands::extend_prev_char,
KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE
} => commands::exit_select_mode as Command,
)
.into_iter(),
);
hashmap!(
// as long as you cast the first item, rust is able to infer the other cases
// TODO: select could be normal mode with some bindings merged over
Mode::Normal => normal,
Mode::Select => select,
Mode::Insert => hashmap!(
KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE
} => commands::normal_mode as Command,
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE
} => commands::insert::delete_char_backward,
KeyEvent {
code: KeyCode::Delete,
modifiers: KeyModifiers::NONE
} => commands::insert::delete_char_forward,
KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE
} => commands::insert::insert_newline,
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE
} => commands::insert::insert_tab,
ctrl!('x') => commands::completion,
),
)
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for Keymaps {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl Default for Keymaps {
fn default() -> Keymaps {
let normal = hashmap!(
key!('h') => Command::move_char_left,
key!('j') => Command::move_line_down,
key!('k') => Command::move_line_up,
key!('l') => Command::move_char_right,
key!(Left) => Command::move_char_left,
key!(Down) => Command::move_line_down,
key!(Up) => Command::move_line_up,
key!(Right) => Command::move_char_right,
key!('t') => Command::find_till_char,
key!('f') => Command::find_next_char,
key!('T') => Command::till_prev_char,
key!('F') => Command::find_prev_char,
// and matching set for select mode (extend)
//
key!('r') => Command::replace,
key!('R') => Command::replace_with_yanked,
key!(Home) => Command::move_line_start,
key!(End) => Command::move_line_end,
key!('w') => Command::move_next_word_start,
key!('b') => Command::move_prev_word_start,
key!('e') => Command::move_next_word_end,
key!('v') => Command::select_mode,
key!('g') => Command::goto_mode,
key!(':') => Command::command_mode,
key!('i') => Command::insert_mode,
key!('I') => Command::prepend_to_line,
key!('a') => Command::append_mode,
key!('A') => Command::append_to_line,
key!('o') => Command::open_below,
key!('O') => Command::open_above,
// [<space> ]<space> equivalents too (add blank new line, no edit)
key!('d') => Command::delete_selection,
// TODO: also delete without yanking
key!('c') => Command::change_selection,
// TODO: also change delete without yanking
// key!('r') => Command::replace_with_char,
key!('s') => Command::select_regex,
alt!('s') => Command::split_selection_on_newline,
key!('S') => Command::split_selection,
key!(';') => Command::collapse_selection,
alt!(';') => Command::flip_selections,
key!('%') => Command::select_all,
key!('x') => Command::extend_line,
// extend_to_whole_line, crop_to_whole_line
key!('m') => Command::match_mode,
key!('[') => Command::left_bracket_mode,
key!(']') => Command::right_bracket_mode,
key!('/') => Command::search,
// ? for search_reverse
key!('n') => Command::search_next,
key!('N') => Command::extend_search_next,
// N for search_prev
key!('*') => Command::search_selection,
key!('u') => Command::undo,
key!('U') => Command::redo,
key!('y') => Command::yank,
// yank_all
key!('p') => Command::paste_after,
// paste_all
key!('P') => Command::paste_before,
key!('>') => Command::indent,
key!('<') => Command::unindent,
key!('=') => Command::format_selections,
key!('J') => Command::join_selections,
// TODO: conflicts hover/doc
key!('K') => Command::keep_selections,
// TODO: and another method for inverse
// TODO: clashes with space mode
key!(' ') => Command::keep_primary_selection,
// key!('q') => Command::record_macro,
// key!('Q') => Command::replay_macro,
// ~ / apostrophe => change case
// & align selections
// _ trim selections
// C / altC = copy (repeat) selections on prev/next lines
key!(Esc) => Command::normal_mode,
key!(PageUp) => Command::page_up,
key!(PageDown) => Command::page_down,
ctrl!('b') => Command::page_up,
ctrl!('f') => Command::page_down,
ctrl!('u') => Command::half_page_up,
ctrl!('d') => Command::half_page_down,
ctrl!('w') => Command::window_mode,
// move under <space>c
ctrl!('c') => Command::toggle_comments,
key!('K') => Command::hover,
// z family for save/restore/combine from/to sels from register
// supposedly ctrl!('i') but did not work
key!(Tab) => Command::jump_forward,
ctrl!('o') => Command::jump_backward,
// ctrl!('s') => Command::save_selection,
key!(' ') => Command::space_mode,
key!('z') => Command::view_mode,
key!('"') => Command::select_register,
);
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
// because some selection operations can now be done from normal mode, some from select mode.
let mut select = normal.clone();
select.extend(
hashmap!(
key!('h') => Command::extend_char_left,
key!('j') => Command::extend_line_down,
key!('k') => Command::extend_line_up,
key!('l') => Command::extend_char_right,
key!(Left) => Command::extend_char_left,
key!(Down) => Command::extend_line_down,
key!(Up) => Command::extend_line_up,
key!(Right) => Command::extend_char_right,
key!('w') => Command::extend_next_word_start,
key!('b') => Command::extend_prev_word_start,
key!('e') => Command::extend_next_word_end,
key!('t') => Command::extend_till_char,
key!('f') => Command::extend_next_char,
key!('T') => Command::extend_till_prev_char,
key!('F') => Command::extend_prev_char,
key!(Home) => Command::extend_line_start,
key!(End) => Command::extend_line_end,
key!(Esc) => Command::exit_select_mode,
)
.into_iter(),
);
Keymaps(hashmap!(
// as long as you cast the first item, rust is able to infer the other cases
// TODO: select could be normal mode with some bindings merged over
Mode::Normal => normal,
Mode::Select => select,
Mode::Insert => hashmap!(
key!(Esc) => Command::normal_mode as Command,
key!(Backspace) => Command::delete_char_backward,
key!(Delete) => Command::delete_char_forward,
key!(Enter) => Command::insert_newline,
key!(Tab) => Command::insert_tab,
key!(Left) => Command::move_char_left,
key!(Down) => Command::move_line_down,
key!(Up) => Command::move_line_up,
key!(Right) => Command::move_char_right,
key!(PageUp) => Command::page_up,
key!(PageDown) => Command::page_down,
key!(Home) => Command::move_line_start,
key!(End) => Command::move_line_end,
ctrl!('x') => Command::completion,
ctrl!('w') => Command::delete_word_backward,
),
))
}
}
/// Merge default config keys with user overwritten keys for custom
/// user config.
pub fn merge_keys(mut config: Config) -> Config {
let mut delta = std::mem::take(&mut config.keys);
for (mode, keys) in &mut *config.keys {
keys.extend(delta.remove(mode).unwrap_or_default());
}
config
}
#[test]
fn merge_partial_keys() {
let config = Config {
keys: Keymaps(hashmap! {
Mode::Normal => hashmap! {
KeyEvent {
code: KeyCode::Char('i'),
modifiers: KeyModifiers::NONE,
} => Command::normal_mode,
KeyEvent { // key that does not exist
code: KeyCode::Char('无'),
modifiers: KeyModifiers::NONE,
} => Command::insert_mode,
},
}),
..Default::default()
};
let merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config);
assert_eq!(
*merged_config
.keys
.0
.get(&Mode::Normal)
.unwrap()
.get(&KeyEvent {
code: KeyCode::Char('i'),
modifiers: KeyModifiers::NONE
})
.unwrap(),
Command::normal_mode
);
assert_eq!(
*merged_config
.keys
.0
.get(&Mode::Normal)
.unwrap()
.get(&KeyEvent {
code: KeyCode::Char('无'),
modifiers: KeyModifiers::NONE
})
.unwrap(),
Command::insert_mode
);
assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
}

12
helix-term/src/lib.rs Normal file
View File

@@ -0,0 +1,12 @@
#![allow(unused)]
#[macro_use]
extern crate helix_view;
pub mod application;
pub mod args;
pub mod commands;
pub mod compositor;
pub mod config;
pub mod keymap;
pub mod ui;

View File

@@ -1,25 +1,13 @@
#![allow(unused)]
mod application;
mod commands;
mod compositor;
mod keymap;
mod ui;
use application::Application;
use anyhow::{Context, Error, Result};
use helix_term::application::Application;
use helix_term::args::Args;
use helix_term::config::Config;
use helix_term::keymap::merge_keys;
use std::path::PathBuf;
use anyhow::Error;
fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new();
// Let's say we depend on something which whose "info" level messages are too
// verbose to include in end-user output. If we don't need them,
// let's not include them.
// .level_for("overly-verbose-target", log::LevelFilter::Warn)
base_config = match verbosity {
0 => base_config.level(log::LevelFilter::Warn),
1 => base_config.level(log::LevelFilter::Info),
@@ -27,8 +15,6 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
_3_or_more => base_config.level(log::LevelFilter::Trace),
};
let home = dirs_next::home_dir().expect("can't find the home directory");
// Separate file config so we can include year, month and day in file logs
let file_config = fern::Dispatch::new()
.format(|out, message, record| {
@@ -40,18 +26,21 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
message
))
})
.chain(fern::log_file(home.join("helix.log"))?);
.chain(fern::log_file(logpath)?);
base_config.chain(file_config).apply()?;
Ok(())
}
pub struct Args {
files: Vec<PathBuf>,
}
#[tokio::main]
async fn main() -> Result<()> {
let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() {
std::fs::create_dir_all(&cache_dir).ok();
}
fn main() {
let logpath = cache_dir.join("helix.log");
let help = format!(
"\
{} {}
@@ -67,55 +56,45 @@ ARGS:
FLAGS:
-h, --help Prints help information
-v Increases logging verbosity each use for up to 3 times
(default file: {})
-V, --version Prints version information
",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
logpath.display(),
);
let mut pargs = pico_args::Arguments::from_env();
let args = Args::parse_args().context("could not parse arguments")?;
// Help has a higher priority and should be handled separately.
if pargs.contains(["-h", "--help"]) {
if args.display_help {
print!("{}", help);
std::process::exit(0);
}
let mut verbosity: u64 = 0;
if pargs.contains("-v") {
verbosity = 1;
if args.display_version {
println!("helix {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
setup_logging(verbosity).expect("failed to initialize logging.");
let conf_dir = helix_core::config_dir();
if !conf_dir.exists() {
std::fs::create_dir_all(&conf_dir).ok();
}
let args = Args {
files: pargs.finish().into_iter().map(|arg| arg.into()).collect(),
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
Ok(config) => merge_keys(toml::from_str(&config)?),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
};
// initialize language registry
use helix_core::config_dir;
use helix_core::syntax::{Loader, LOADER};
// load $HOME/.config/helix/languages.toml, fallback to default config
let config = std::fs::read(config_dir().join("languages.toml"));
let toml = config
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
LOADER.get_or_init(|| {
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
Loader::new(config)
});
let runtime = tokio::runtime::Runtime::new().unwrap();
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
// TODO: use the thread local executor to spawn the application task separately from the work pool
runtime.block_on(async move {
let mut app = Application::new(args).unwrap();
let mut app = Application::new(args, config).context("unable to create new application")?;
app.run().await.unwrap();
app.run().await;
});
Ok(())
}

View File

@@ -1,15 +1,14 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use tui::buffer::Buffer as Surface;
use std::borrow::Cow;
use helix_core::{Position, Transaction};
use helix_view::Editor;
use helix_view::{
graphics::{Color, Rect, Style},
Editor,
};
use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
@@ -68,7 +67,7 @@ impl menu::Item for CompletionItem {
/// Wraps a Menu.
pub struct Completion {
popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
popup: Popup<Menu<CompletionItem>>,
trigger_offset: usize,
// TODO: maintain a completioncontext with trigger kind & trigger char
}
@@ -82,46 +81,14 @@ impl Completion {
// let items: Vec<CompletionItem> = Vec::new();
let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| {
match event {
PromptEvent::Abort => {
// revert state
// let id = editor.view().doc;
// let doc = &mut editor.documents[id];
// doc.state = snapshot.clone();
}
PromptEvent::Abort => {}
PromptEvent::Validate => {
let (view, doc) = editor.current();
// revert state to what it was before the last update
// doc.state = snapshot.clone();
// extract as fn(doc, item):
// TODO: need to apply without composing state...
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
// the final state?
// -> on update simply update the snapshot, then on accept redo the call,
// finally updating doc.changes + notifying lsp.
//
// or we could simply use doc.undo + apply when changing between options
let (view, doc) = current!(editor);
// always present here
let item = item.unwrap();
use helix_lsp::{lsp, util};
// determine what to insert: text_edit | insert_text | label
let edit = if let Some(edit) = &item.text_edit {
match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
}
} else {
item.insert_text.as_ref().unwrap_or(&item.label);
unimplemented!();
// lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
// and we insert at position.
};
// if more text was entered, remove it
let cursor = doc.selection(view.id).cursor();
@@ -134,14 +101,29 @@ impl Completion {
}
use helix_lsp::OffsetEncoding;
let transaction = util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
);
let transaction = if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
};
util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
)
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
let cursor = doc.selection(view.id).cursor();
Transaction::change(
doc.text(),
vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
)
};
doc.apply(&transaction, view.id);
// TODO: merge edit with additional_text_edits
if let Some(additional_edits) = &item.additional_text_edits {
// gopls uses this to add extra imports
if !additional_edits.is_empty() {
@@ -167,7 +149,7 @@ impl Completion {
pub fn update(&mut self, cx: &mut commands::Context) {
// recompute menu based on matches
let menu = self.popup.contents_mut();
let (view, doc) = cx.editor.current();
let (view, doc) = current!(cx.editor);
// cx.hooks()
// cx.add_hook(enum type, ||)
@@ -224,18 +206,21 @@ impl Component for Completion {
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.popup.render(area, surface, cx);
// TODO: if we have a selection, render a markdown popup on top/below with info
// if we have a selection, render a markdown popup on top/below with info
if let Some(option) = self.popup.contents().selection() {
// need to render:
// option.detail
// ---
// option.documentation
let (view, doc) = cx.editor.current();
let (view, doc) = current!(cx.editor);
let language = doc
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
let cursor_pos = doc.selection(view.id).cursor();
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.first_line) as u16;
let doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
@@ -244,42 +229,60 @@ impl Component for Completion {
value: contents,
})) => {
// TODO: convert to wrapped text
Markdown::new(format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
))
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
}
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: contents,
})) => {
// TODO: set language based on doc scope
Markdown::new(format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
))
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
}
None if option.detail.is_some() => {
// TODO: copied from above
// TODO: set language based on doc scope
Markdown::new(format!(
"```{}\n{}\n```",
language,
option.detail.as_deref().unwrap_or_default(),
))
Markdown::new(
format!(
"```{}\n{}\n```",
language,
option.detail.as_deref().unwrap_or_default(),
),
cx.editor.syn_loader.clone(),
)
}
None => return,
};
let half = area.height / 2;
let height = 15.min(half);
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
let area = Rect::new(0, area.height - height - 2, area.width, height);
// we want to make sure the cursor is visible (not hidden behind the documentation)
let y = if cursor_pos + view.area.y
>= (cx.editor.tree.area().height - height - 2/* statusline + commandline */)
{
0
} else {
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
area.height.saturating_sub(height).saturating_sub(2)
};
let area = Rect::new(0, y, area.width, height);
// clear area
let background = cx.editor.theme.get("ui.popup");

View File

@@ -3,47 +3,63 @@ use crate::{
compositor::{Component, Compositor, Context, EventResult},
key,
keymap::{self, Keymaps},
ui::Completion,
ui::{Completion, ProgressSpinners},
};
use helix_core::{
coords_at_pos,
graphemes::ensure_grapheme_boundary,
syntax::{self, HighlightEvent},
Position, Range,
LineEnding, Position, Range,
};
use helix_lsp::LspProgressMap;
use helix_view::{
document::Mode,
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::KeyEvent,
keyboard::{KeyCode, KeyModifiers},
Document, Editor, Theme, View,
};
use helix_view::{document::Mode, Document, Editor, Theme, View};
use std::borrow::Cow;
use crossterm::{
cursor,
event::{read, Event, EventStream, KeyCode, KeyEvent, KeyModifiers},
};
use tui::{
backend::CrosstermBackend,
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Modifier, Style},
event::{read, Event, EventStream},
};
use tui::{backend::CrosstermBackend, buffer::Buffer as Surface};
pub struct EditorView {
keymap: Keymaps,
keymaps: Keymaps,
on_next_key: Option<Box<dyn FnOnce(&mut commands::Context, KeyEvent)>>,
last_insert: (commands::Command, Vec<KeyEvent>),
completion: Option<Completion>,
spinners: ProgressSpinners,
}
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
impl Default for EditorView {
fn default() -> Self {
Self::new(Keymaps::default())
}
}
impl EditorView {
pub fn new() -> Self {
pub fn new(keymaps: Keymaps) -> Self {
Self {
keymap: keymap::default(),
keymaps,
on_next_key: None,
last_insert: (commands::normal_mode, Vec::new()),
last_insert: (commands::Command::normal_mode, Vec::new()),
completion: None,
spinners: ProgressSpinners::default(),
}
}
pub fn spinners_mut(&mut self) -> &mut ProgressSpinners {
&mut self.spinners
}
#[allow(clippy::too_many_arguments)]
pub fn render_view(
&self,
doc: &Document,
@@ -108,8 +124,6 @@ impl EditorView {
};
// TODO: range doesn't actually restrict source, just highlight range
// TODO: cache highlight results
// TODO: only recalculate when state.doc is actually modified
let highlights: Vec<_> = match doc.syntax() {
Some(syntax) => {
syntax
@@ -122,13 +136,13 @@ impl EditorView {
})],
};
let mut spans = Vec::new();
let mut visual_x = 0;
let mut visual_x = 0u16;
let mut line = 0u16;
let tab_width = doc.tab_width();
'outer: for event in highlights {
match event.unwrap() {
HighlightEvent::HighlightStart(span) => {
HighlightEvent::HighlightStart(mut span) => {
spans.push(span);
}
HighlightEvent::HighlightEnd => {
@@ -138,8 +152,8 @@ impl EditorView {
// TODO: filter out spans out of viewport for now..
// TODO: do these before iterating
let start = text.byte_to_char(start);
let end = text.byte_to_char(end);
let start = ensure_grapheme_boundary(text, text.byte_to_char(start));
let end = ensure_grapheme_boundary(text, text.byte_to_char(end));
let text = text.slice(start..end);
@@ -169,7 +183,7 @@ impl EditorView {
// iterate over range char by char
for grapheme in RopeGraphemes::new(text) {
if grapheme == "\n" {
if LineEnding::from_rope_slice(&grapheme).is_some() {
visual_x = 0;
line += 1;
@@ -178,7 +192,7 @@ impl EditorView {
break 'outer;
}
} else if grapheme == "\t" {
visual_x += (tab_width as u16);
visual_x = visual_x.saturating_add(tab_width as u16);
} else {
let out_of_bounds = visual_x < view.first_col as u16
|| visual_x >= viewport.width + view.first_col as u16;
@@ -190,12 +204,12 @@ impl EditorView {
if out_of_bounds {
// if we're offscreen just keep going until we hit a new line
visual_x += width;
visual_x = visual_x.saturating_add(width);
continue;
}
// ugh,interleave highlight spans with diagnostic spans
let is_diagnostic = doc.diagnostics.iter().any(|diagnostic| {
let is_diagnostic = doc.diagnostics().iter().any(|diagnostic| {
diagnostic.range.start <= char_index
&& diagnostic.range.end > char_index
});
@@ -216,13 +230,51 @@ impl EditorView {
visual_x += width;
}
char_index += 1;
char_index += grapheme.chars().count();
}
}
}
}
// render selections
// render gutters
let linenr: Style = theme.get("ui.linenr");
let warning: Style = theme.get("warning");
let error: Style = theme.get("error");
let info: Style = theme.get("info");
let hint: Style = theme.get("hint");
for (i, line) in (view.first_line..last_line).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
surface.set_stringn(
viewport.x - OFFSET,
viewport.y + i as u16,
"",
1,
match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
},
);
}
// line numbers having selections are rendered differently
surface.set_stringn(
viewport.x + 1 - OFFSET,
viewport.y + i as u16,
format!("{:>5}", line + 1),
5,
linenr,
);
}
// render selections and selected linenr(s)
let linenr_select: Style = theme
.try_get("ui.linenr.selected")
.unwrap_or_else(|| theme.get("ui.linenr"));
if is_focused {
let screen = {
@@ -230,22 +282,53 @@ impl EditorView {
let end = text.line_to_char(last_line + 1);
Range::new(start, end)
};
let cursor_style = Style::default()
// .bg(Color::Rgb(255, 255, 255))
.add_modifier(Modifier::REVERSED);
// let selection_style = Style::default().bg(Color::Rgb(94, 0, 128));
let selection_style = Style::default().bg(Color::Rgb(84, 0, 153));
let mode = doc.mode();
let base_cursor_style = theme
.try_get("ui.cursor")
.unwrap_or_else(|| Style::default().add_modifier(Modifier::REVERSED));
let cursor_style = match mode {
Mode::Insert => theme.try_get("ui.cursor.insert"),
Mode::Select => theme.try_get("ui.cursor.select"),
Mode::Normal => Some(base_cursor_style),
}
.unwrap_or(base_cursor_style);
let primary_cursor_style = theme
.try_get("ui.cursor.primary")
.map(|style| {
if mode != Mode::Normal {
// we want to make sure that the insert and select highlights
// also affect the primary cursor if set
style.patch(cursor_style)
} else {
style
}
})
.unwrap_or(cursor_style);
for selection in doc
.selection(view.id)
let selection_style = theme.get("ui.selection");
let primary_selection_style = theme
.try_get("ui.selection.primary")
.unwrap_or(selection_style);
let selection = doc.selection(view.id);
let primary_idx = selection.primary_index();
for (i, selection) in selection
.iter()
.filter(|range| range.overlaps(&screen))
.enumerate()
.filter(|(_, range)| range.overlaps(&screen))
{
// TODO: render also if only one of the ranges is in viewport
let mut start = view.screen_coords_at_pos(doc, text, selection.anchor);
let mut end = view.screen_coords_at_pos(doc, text, selection.head);
let (cursor_style, selection_style) = if i == primary_idx {
(primary_cursor_style, primary_selection_style)
} else {
(cursor_style, selection_style)
};
let head = end;
if selection.head < selection.anchor {
@@ -261,7 +344,16 @@ impl EditorView {
Rect::new(
viewport.x + start.col as u16,
viewport.y + start.row as u16,
(end.col - start.col) as u16 + 1,
// .min is important, because set_style does a
// for i in area.left()..area.right() and
// area.right = x + width !!! which shouldn't be > then surface.area.right()
// This is checked by a debug_assert! in Buffer::index_of
((end.col - start.col) as u16 + 1).min(
surface
.area
.width
.saturating_sub(viewport.x + start.col as u16),
),
1,
),
selection_style,
@@ -272,7 +364,7 @@ impl EditorView {
viewport.x + start.col as u16,
viewport.y + start.row as u16,
// text.line(view.first_line).len_chars() as u16 - start.col as u16,
viewport.width - start.col as u16,
viewport.width.saturating_sub(start.col as u16),
1,
),
selection_style,
@@ -290,7 +382,12 @@ impl EditorView {
);
}
surface.set_style(
Rect::new(viewport.x, viewport.y + end.row as u16, end.col as u16, 1),
Rect::new(
viewport.x,
viewport.y + end.row as u16,
(end.col as u16).min(viewport.width),
1,
),
selection_style,
);
}
@@ -306,43 +403,44 @@ impl EditorView {
),
cursor_style,
);
surface.set_stringn(
viewport.x + 1 - OFFSET,
viewport.y + head.row as u16,
format!("{:>5}", view.first_line + head.row + 1),
5,
linenr_select,
);
// TODO: set cursor position for IME
if let Some(syntax) = doc.syntax() {
use helix_core::match_brackets;
let pos = doc.selection(view.id).cursor();
let pos = match_brackets::find(syntax, doc.text(), pos);
if let Some(pos) = pos {
let pos = view.screen_coords_at_pos(doc, text, pos);
if let Some(pos) = pos {
if (pos.col as u16) < viewport.width + view.first_col as u16
&& pos.col >= view.first_col
{
let style =
theme.try_get("ui.cursor.match").unwrap_or_else(|| {
Style::default()
.add_modifier(Modifier::REVERSED)
.add_modifier(Modifier::DIM)
});
surface
.get_mut(
viewport.x + pos.col as u16,
viewport.y + pos.row as u16,
)
.set_style(style);
}
}
}
}
}
}
}
// render gutters
let style: Style = theme.get("ui.linenr");
let warning: Style = theme.get("warning");
let error: Style = theme.get("error");
let info: Style = theme.get("info");
let hint: Style = theme.get("hint");
for (i, line) in (view.first_line..last_line).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics.iter().find(|d| d.line == line) {
surface.set_stringn(
viewport.x - OFFSET,
viewport.y + i as u16,
"",
1,
match diagnostic.severity {
Some(Severity::Error) => error,
Some(Severity::Warning) | None => warning,
Some(Severity::Info) => info,
Some(Severity::Hint) => hint,
},
);
}
surface.set_stringn(
viewport.x + 1 - OFFSET,
viewport.y + i as u16,
format!("{:>5}", line + 1),
5,
style,
);
}
}
pub fn render_diagnostics(
@@ -364,7 +462,7 @@ impl EditorView {
let cursor = doc.selection(view.id).cursor();
let line = doc.text().char_to_line(cursor);
let diagnostics = doc.diagnostics.iter().filter(|diagnostic| {
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
});
@@ -402,6 +500,7 @@ impl EditorView {
);
}
#[allow(clippy::too_many_arguments)]
pub fn render_statusline(
&self,
doc: &Document,
@@ -411,63 +510,91 @@ impl EditorView {
theme: &Theme,
is_focused: bool,
) {
//-------------------------------
// Left side of the status line.
//-------------------------------
let mode = match doc.mode() {
Mode::Insert => "INS",
Mode::Select => "SEL",
Mode::Normal => "NOR",
};
let text_color = if is_focused {
theme.get("ui.text.focus")
let progress = doc
.language_server()
.and_then(|srv| {
self.spinners
.get(srv.id())
.and_then(|spinner| spinner.frame())
})
.unwrap_or("");
let style = if is_focused {
theme.get("ui.statusline")
} else {
theme.get("ui.text")
theme.get("ui.statusline.inactive")
};
// statusline
surface.set_style(
Rect::new(viewport.x, viewport.y, viewport.width, 1),
theme.get("ui.statusline"),
);
surface.set_style(Rect::new(viewport.x, viewport.y, viewport.width, 1), style);
if is_focused {
surface.set_string(viewport.x + 1, viewport.y, mode, text_color);
surface.set_string(viewport.x + 1, viewport.y, mode, style);
}
surface.set_string(viewport.x + 5, viewport.y, progress, style);
if let Some(path) = doc.relative_path() {
let path = path.to_string_lossy();
let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
surface.set_stringn(
viewport.x + 6,
viewport.x + 8,
viewport.y,
title,
viewport.width.saturating_sub(6) as usize,
text_color,
style,
);
}
surface.set_stringn(
viewport.x + viewport.width.saturating_sub(15),
viewport.y,
format!("{}", doc.diagnostics.len()),
4,
text_color,
//-------------------------------
// Right side of the status line.
//-------------------------------
// Compute the individual info strings.
let diag_count = format!("{}", doc.diagnostics().len());
// let indent_info = match doc.indent_style {
// IndentStyle::Tabs => "tabs",
// IndentStyle::Spaces(1) => "spaces:1",
// IndentStyle::Spaces(2) => "spaces:2",
// IndentStyle::Spaces(3) => "spaces:3",
// IndentStyle::Spaces(4) => "spaces:4",
// IndentStyle::Spaces(5) => "spaces:5",
// IndentStyle::Spaces(6) => "spaces:6",
// IndentStyle::Spaces(7) => "spaces:7",
// IndentStyle::Spaces(8) => "spaces:8",
// _ => "indent:ERROR",
// };
let position_info = {
let pos = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor());
format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
};
// Render them to the status line together.
let right_side_text = format!(
"{} {} ",
&diag_count[..diag_count.len().min(4)],
// indent_info,
position_info
);
// render line:col
let pos = coords_at_pos(doc.text().slice(..), doc.selection(view.id).cursor());
let text = format!("{}:{}", pos.row + 1, pos.col + 1); // convert to 1-indexing
let len = text.len();
let text_len = right_side_text.len() as u16;
surface.set_string(
viewport.x + viewport.width.saturating_sub(len as u16 + 1),
viewport.x + viewport.width.saturating_sub(text_len),
viewport.y,
text,
text_color,
right_side_text,
style,
);
}
fn insert_mode(&self, cx: &mut commands::Context, event: KeyEvent) {
if let Some(command) = self.keymap[&Mode::Insert].get(&event) {
command(cx);
if let Some(command) = self.keymaps[&Mode::Insert].get(&event) {
command.execute(cx);
} else if let KeyEvent {
code: KeyCode::Char(ch),
..
@@ -482,12 +609,13 @@ impl EditorView {
// count handling
key!(i @ '0'..='9') => {
let i = i.to_digit(10).unwrap() as usize;
cxt.editor.count = Some(cxt.editor.count.map_or(i, |c| c * 10 + i));
cxt.editor.count =
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
}
// special handling for repeat operator
key!('.') => {
// first execute whatever put us into insert mode
(self.last_insert.0)(cxt);
self.last_insert.0.execute(cxt);
// then replay the inputs
for key in &self.last_insert.1 {
self.insert_mode(cxt, *key)
@@ -495,13 +623,16 @@ impl EditorView {
}
_ => {
// set the count
cxt.count = cxt.editor.count.take().unwrap_or(1);
cxt.count = cxt.editor.count.take();
// TODO: edge case: 0j -> reset to 1
// if this fails, count was Some(0)
// debug_assert!(cxt.count != 0);
if let Some(command) = self.keymap[&mode].get(&event) {
command(cxt);
// set the register
cxt.selected_register = cxt.editor.selected_register.take();
if let Some(command) = self.keymaps[&mode].get(&event) {
command.execute(cxt);
}
}
}
@@ -530,18 +661,21 @@ impl Component for EditorView {
EventResult::Consumed(None)
}
Event::Key(key) => {
let mut key = KeyEvent::from(key);
canonicalize_key(&mut key);
// clear status
cx.editor.status_msg = None;
let (view, doc) = cx.editor.current();
let (view, doc) = current!(cx.editor);
let mode = doc.mode();
let mut cxt = commands::Context {
selected_register: helix_view::RegisterSelection::default(),
editor: &mut cx.editor,
count: 1,
count: None,
callback: None,
callbacks: cx.callbacks,
on_next_key_callback: None,
callbacks: cx.callbacks,
};
if let Some(on_next_key) = self.on_next_key.take() {
@@ -601,7 +735,7 @@ impl Component for EditorView {
return EventResult::Ignored;
}
let (view, doc) = cx.editor.current();
let (view, doc) = current!(cx.editor);
view.ensure_cursor_in_view(doc);
// mode transitions
@@ -613,7 +747,7 @@ impl Component for EditorView {
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
self.last_insert.0 = self.keymap[&mode][&key];
self.last_insert.0 = self.keymaps[&mode][&key];
self.last_insert.1.clear();
}
(Mode::Insert, Mode::Normal) => {
@@ -633,6 +767,10 @@ impl Component for EditorView {
// clear with background color
surface.set_style(area, cx.editor.theme.get("ui.background"));
// if the terminal size suddenly changed, we need to trigger a resize
cx.editor
.resize(Rect::new(area.x, area.y, area.width, area.height - 1)); // - 1 to account for commandline
for (view, is_focused) in cx.editor.tree.views() {
let doc = cx.editor.document(view.doc).unwrap();
self.render_view(doc, view, area, surface, &cx.editor.theme, is_focused);
@@ -661,14 +799,21 @@ impl Component for EditorView {
}
}
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
// match view.doc.mode() {
// Mode::Insert => write!(stdout, "\x1B[6 q"),
// mode => write!(stdout, "\x1B[2 q"),
// };
// return editor.cursor_position()
// It's easier to just not render the cursor and use selection rendering instead.
None
editor.cursor()
}
}
fn canonicalize_key(key: &mut KeyEvent) {
if let KeyEvent {
code: KeyCode::Char(_),
modifiers: _,
} = key
{
key.modifiers.remove(KeyModifiers::SHIFT)
}
}

View File

@@ -1,31 +1,38 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
text::Text,
use tui::{buffer::Buffer as Surface, text::Text};
use std::{borrow::Cow, sync::Arc};
use helix_core::{syntax, Position};
use helix_view::{
graphics::{Color, Rect, Style},
Editor, Theme,
};
use std::borrow::Cow;
use helix_core::Position;
use helix_view::{Editor, Theme};
pub struct Markdown {
contents: String,
config_loader: Arc<syntax::Loader>,
}
// TODO: pre-render and self reference via Pin
// better yet, just use Tendril + subtendril for references
impl Markdown {
pub fn new(contents: String) -> Self {
Self { contents }
pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
Self {
contents,
config_loader,
}
}
}
fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
fn parse<'a>(
contents: &'a str,
theme: Option<&Theme>,
loader: &syntax::Loader,
) -> tui::text::Text<'a> {
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use tui::text::{Span, Spans, Text};
@@ -79,9 +86,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
use helix_core::Rope;
let rope = Rope::from(text.as_ref());
let syntax = syntax::LOADER
.get()
.unwrap()
let syntax = loader
.language_config_for_scope(&format!("source.{}", language))
.and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config));
@@ -101,15 +106,15 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
}
HighlightEvent::Source { start, end } => {
let style = match highlights.first() {
Some(span) => {
theme.get(theme.scopes()[span.0].as_str())
}
Some(span) => theme.get(&theme.scopes()[span.0]),
None => text_style,
};
// TODO: replace tabs with indentation
let mut slice = &text[start..end];
// TODO: do we need to handle all unicode line endings
// here, or is just '\n' okay?
while let Some(end) = slice.find('\n') {
// emit span up to newline
let text = &slice[..end];
@@ -157,7 +162,6 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
}
}
Event::Code(text) | Event::Html(text) => {
log::warn!("code {:?}", text);
let mut span = to_span(text);
span.style = code_style;
spans.push(span);
@@ -196,7 +200,7 @@ impl Component for Markdown {
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
use tui::widgets::{Paragraph, Widget, Wrap};
let text = parse(&self.contents, Some(&cx.editor.theme));
let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
let par = Paragraph::new(text)
.wrap(Wrap { trim: false })
@@ -207,7 +211,7 @@ impl Component for Markdown {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let contents = parse(&self.contents, None);
let contents = parse(&self.contents, None, &self.config_loader);
let padding = 2;
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);

View File

@@ -1,11 +1,6 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
widgets::Table,
};
use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row};
@@ -15,7 +10,10 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_core::Position;
use helix_view::Editor;
use helix_view::{
graphics::{Color, Rect, Style},
Editor,
};
pub trait Item {
// TODO: sort_text
@@ -82,7 +80,7 @@ impl<T: Item> Menu<T> {
let text = option.filter_text();
// TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher
.fuzzy_match(&text, pattern)
.fuzzy_match(text, pattern)
.map(|score| (index, score))
}),
);
@@ -166,8 +164,8 @@ impl<T: Item + 'static> Component for Menu<T> {
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::SHIFT,
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Up, ..

View File

@@ -5,6 +5,7 @@ mod menu;
mod picker;
mod popup;
mod prompt;
mod spinner;
mod text;
pub use completion::Completion;
@@ -14,32 +15,34 @@ pub use menu::Menu;
pub use picker::Picker;
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
pub use tui::layout::Rect;
pub use tui::style::{Color, Modifier, Style};
use helix_core::regex::Regex;
use helix_view::{Document, Editor, View};
use helix_core::register::Registers;
use helix_view::{
graphics::{Color, Modifier, Rect, Style},
Document, Editor, View,
};
use std::path::{Path, PathBuf};
pub fn regex_prompt(
cx: &mut crate::commands::Context,
prompt: String,
fun: impl Fn(&mut View, &mut Document, Regex) + 'static,
fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static,
) -> Prompt {
let view_id = cx.view().id;
let snapshot = cx.doc().selection(view_id).clone();
let (view, doc) = current!(cx.editor);
let view_id = view.id;
let snapshot = doc.selection(view_id).clone();
Prompt::new(
prompt,
|input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
move |editor: &mut Editor, input: &str, event: PromptEvent| {
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event {
PromptEvent::Abort => {
// TODO: also revert text
let (view, doc) = editor.current();
let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, snapshot.clone());
}
PromptEvent::Validate => {
@@ -53,13 +56,13 @@ pub fn regex_prompt(
match Regex::new(input) {
Ok(regex) => {
let (view, doc) = editor.current();
let (view, doc) = current!(cx.editor);
let registers = &mut cx.editor.registers;
// revert state to what it was before the last update
// TODO: also revert text
doc.set_selection(view.id, snapshot.clone());
fun(view, doc, regex);
fun(view, doc, registers, regex);
view.ensure_cursor_in_view(doc);
}
@@ -73,11 +76,22 @@ pub fn regex_prompt(
pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
use ignore::Walk;
use std::time;
let files = Walk::new(root.clone()).filter_map(|entry| match entry {
Ok(entry) => {
// filter dirs, but we might need special handling for symlinks!
if !entry.file_type().map_or(false, |entry| entry.is_dir()) {
Some(entry.into_path())
let time = if let Ok(metadata) = entry.metadata() {
metadata
.accessed()
.or_else(|_| metadata.modified())
.or_else(|_| metadata.created())
.unwrap_or(time::UNIX_EPOCH)
} else {
time::UNIX_EPOCH
};
Some((entry.into_path(), time))
} else {
None
}
@@ -85,10 +99,19 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
Err(_err) => None,
});
const MAX: usize = 2048;
let mut files: Vec<_> = if root.join(".git").is_dir() {
files.collect()
} else {
const MAX: usize = 8192;
files.take(MAX).collect()
};
files.sort_by_key(|file| file.1);
let files = files.into_iter().map(|(path, _)| path).collect();
Picker::new(
files.take(MAX).collect(),
files,
move |path: &PathBuf| {
// format_fn
path.strip_prefix(&root)
@@ -107,21 +130,91 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
pub mod completers {
use crate::ui::prompt::Completion;
use std::borrow::Cow;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_view::theme;
use std::cmp::Reverse;
use std::{borrow::Cow, sync::Arc};
pub type Completer = fn(&str) -> Vec<Completion>;
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
pub fn theme(input: &str) -> Vec<Completion> {
let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
names.extend(theme::Loader::read_names(
&helix_core::config_dir().join("themes"),
));
names.push("default".into());
let mut names: Vec<_> = names
.into_iter()
.map(|name| ((0..), Cow::from(name)))
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
.into_iter()
.filter_map(|(range, name)| {
matcher.fuzzy_match(&name, input).map(|score| (name, score))
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names
}
pub fn filename(input: &str) -> Vec<Completion> {
filename_impl(input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
if is_dir {
FileMatch::AcceptIncomplete
} else {
FileMatch::Accept
}
})
}
pub fn directory(input: &str) -> Vec<Completion> {
filename_impl(input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
if is_dir {
FileMatch::Accept
} else {
FileMatch::Reject
}
})
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum FileMatch {
/// Entry should be ignored
Reject,
/// Entry is usable but can't be the end (for instance if the entry is a directory and we
/// try to match a file)
AcceptIncomplete,
/// Entry is usable and can be the end of the match
Accept,
}
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
fn filename_impl<F>(input: &str, filter_fn: F) -> Vec<Completion>
where
F: Fn(&ignore::DirEntry) -> FileMatch,
{
// Rust's filename handling is really annoying.
use ignore::WalkBuilder;
use std::path::{Path, PathBuf};
let path = Path::new(input);
let is_tilde = input.starts_with('~') && input.len() == 1;
let path = helix_view::document::expand_tilde(Path::new(input));
let (dir, file_name) = if input.ends_with('/') {
(path.into(), None)
(path, None)
} else {
let file_name = path
.file_name()
@@ -142,17 +235,33 @@ pub mod completers {
.max_depth(Some(1))
.build()
.filter_map(|file| {
file.ok().map(|entry| {
file.ok().and_then(|entry| {
let fmatch = filter_fn(&entry);
if fmatch == FileMatch::Reject {
return None;
}
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
let path = entry.path();
let mut path = path.strip_prefix(&dir).unwrap_or(path).to_path_buf();
let mut path = if is_tilde {
// if it's a single tilde an absolute path is displayed so that when `TAB` is pressed on
// one of the directories the tilde will be replaced with a valid path not with a relative
// home directory name.
// ~ -> <TAB> -> /home/user
// ~/ -> <TAB> -> ~/first_entry
path.to_path_buf()
} else {
path.strip_prefix(&dir).unwrap_or(path).to_path_buf()
};
if is_dir {
if fmatch == FileMatch::AcceptIncomplete {
path.push("");
}
let path = path.to_str().unwrap().to_owned();
(end.clone(), Cow::from(path))
Some((end.clone(), Cow::from(path)))
})
}) // TODO: unwrap or skip
.filter(|(_, path)| !path.is_empty()) // TODO
@@ -160,10 +269,6 @@ pub mod completers {
// if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name {
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use std::cmp::Reverse;
let matcher = Matcher::default();
// inefficient, but we need to calculate the scores, filter out None, then sort.
@@ -176,7 +281,7 @@ pub mod completers {
})
.collect();
let range = ((input.len() - file_name.len())..);
let range = ((input.len().saturating_sub(file_name.len()))..);
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
files = matches

View File

@@ -2,8 +2,6 @@ use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
widgets::{Block, BorderType, Borders},
};
@@ -14,8 +12,11 @@ use std::borrow::Cow;
use crate::ui::{Prompt, PromptEvent};
use helix_core::Position;
use helix_view::editor::Action;
use helix_view::Editor;
use helix_view::{
editor::Action,
graphics::{Color, CursorKind, Rect, Style},
Editor,
};
pub struct Picker<T> {
options: Vec<T>,
@@ -23,6 +24,8 @@ pub struct Picker<T> {
matcher: Box<Matcher>,
/// (index, score)
matches: Vec<(usize, i64)>,
/// Filter over original options.
filters: Vec<usize>, // could be optimized into bit but not worth it now
cursor: usize,
// pattern: String,
@@ -41,7 +44,7 @@ impl<T> Picker<T> {
let prompt = Prompt::new(
"".to_string(),
|pattern: &str| Vec::new(),
|editor: &mut Editor, pattern: &str, event: PromptEvent| {
|editor: &mut Context, pattern: &str, event: PromptEvent| {
//
},
);
@@ -50,6 +53,7 @@ impl<T> Picker<T> {
options,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
filters: Vec::new(),
cursor: 0,
prompt,
format_fn: Box::new(format_fn),
@@ -68,6 +72,7 @@ impl<T> Picker<T> {
ref mut options,
ref mut matcher,
ref mut matches,
ref filters,
ref format_fn,
..
} = *self;
@@ -81,6 +86,10 @@ impl<T> Picker<T> {
.iter()
.enumerate()
.filter_map(|(index, option)| {
// filter options first before matching
if !filters.is_empty() {
filters.binary_search(&index).ok()?;
}
// TODO: maybe using format_fn isn't the best idea here
let text = (format_fn)(option);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
@@ -100,8 +109,11 @@ impl<T> Picker<T> {
}
pub fn move_down(&mut self) {
// TODO: len - 1
if self.cursor < self.options.len() {
if self.matches.is_empty() {
return;
}
if self.cursor < self.matches.len() - 1 {
self.cursor += 1;
}
}
@@ -111,6 +123,14 @@ impl<T> Picker<T> {
.get(self.cursor)
.map(|(index, _score)| &self.options[*index])
}
pub fn save_filter(&mut self) {
self.filters.clear();
self.filters
.extend(self.matches.iter().map(|(index, _)| *index));
self.filters.sort_unstable(); // used for binary search later
self.prompt.clear();
}
}
// process:
@@ -148,7 +168,11 @@ impl<T: 'static> Component for Picker<T> {
code: KeyCode::Up, ..
}
| KeyEvent {
code: KeyCode::Char('k'),
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => self.move_up(),
KeyEvent {
@@ -156,11 +180,18 @@ impl<T: 'static> Component for Picker<T> {
..
}
| KeyEvent {
code: KeyCode::Char('j'),
code: KeyCode::Tab, ..
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => self.move_down(),
KeyEvent {
code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => {
return close_fn;
}
@@ -174,7 +205,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn;
}
KeyEvent {
code: KeyCode::Char('x'),
code: KeyCode::Char('h'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() {
@@ -191,6 +222,12 @@ impl<T: 'static> Component for Picker<T> {
}
return close_fn;
}
KeyEvent {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::CONTROL,
} => {
self.save_filter();
}
_ => {
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
// TODO: recalculate only if pattern changed
@@ -219,8 +256,6 @@ impl<T: 'static> Component for Picker<T> {
let inner = block.inner(area);
block.render(area, surface);
// TODO: abstract into a clear(area) fn
// surface.set_style(inner, Style::default().bg(Color::Rgb(150, 50, 0)));
// -- Render the input bar:
@@ -243,27 +278,33 @@ impl<T: 'static> Component for Picker<T> {
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
let rows = inner.height - 2; // -1 for search bar
let offset = self.cursor / (rows as usize) * (rows as usize);
let files = self.matches.iter().map(|(index, _score)| {
let files = self.matches.iter().skip(offset).map(|(index, _score)| {
(index, self.options.get(*index).unwrap()) // get_unchecked
});
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
if i == self.cursor {
if i == (self.cursor - offset) {
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
}
surface.set_stringn(
surface.set_string_truncated(
inner.x + 3,
inner.y + 2 + i as u16,
(self.format_fn)(option),
inner.width as usize - 1,
if i == self.cursor { selected } else { style },
(inner.width as usize).saturating_sub(3), // account for the " > "
if i == (self.cursor - offset) {
selected
} else {
style
},
true,
);
}
}
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
// TODO: this is mostly duplicate code
let area = inner_rect(area);
let block = Block::default().borders(Borders::ALL);
@@ -273,6 +314,6 @@ impl<T: 'static> Component for Picker<T> {
// prompt area
let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1);
self.prompt.cursor_position(area, editor)
self.prompt.cursor(area, editor)
}
}

View File

@@ -1,15 +1,14 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use tui::buffer::Buffer as Surface;
use std::borrow::Cow;
use helix_core::Position;
use helix_view::Editor;
use helix_view::{
graphics::{Color, Rect, Style},
Editor,
};
// 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>)
@@ -116,7 +115,7 @@ impl<T: Component> Component for Popup<T> {
let position = self
.position
.or_else(|| cx.editor.cursor_position())
.or_else(|| cx.editor.cursor().0)
.unwrap_or_default();
let (width, height) = self.size;
@@ -125,13 +124,13 @@ impl<T: Component> Component for Popup<T> {
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 + width) - viewport.width)
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
};
// TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete.
if height <= rel_y {
rel_y -= height // position above point
rel_y = rel_y.saturating_sub(height) // position above point
} else {
rel_y += 1 // position below point
}

View File

@@ -1,9 +1,18 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crate::ui;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use helix_core::Position;
use helix_view::{Editor, Theme};
use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface;
use helix_core::{
unicode::segmentation::{GraphemeCursor, GraphemeIncomplete},
unicode::width::UnicodeWidthStr,
Position,
};
use helix_view::{
graphics::{Color, CursorKind, Margin, Modifier, Rect, Style},
Editor, Theme,
};
pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
@@ -14,11 +23,11 @@ pub struct Prompt {
completion: Vec<Completion>,
selection: Option<usize>,
completion_fn: Box<dyn FnMut(&str) -> Vec<Completion>>,
callback_fn: Box<dyn FnMut(&mut Editor, &str, PromptEvent)>,
callback_fn: Box<dyn FnMut(&mut Context, &str, PromptEvent)>,
pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
}
#[derive(PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum PromptEvent {
/// The prompt input has been updated.
Update,
@@ -28,11 +37,27 @@ pub enum PromptEvent {
Abort,
}
pub enum CompletionDirection {
Forward,
Backward,
}
#[derive(Debug, Clone, Copy)]
pub enum Movement {
BackwardChar(usize),
BackwardWord(usize),
ForwardChar(usize),
ForwardWord(usize),
StartOfLine,
EndOfLine,
None,
}
impl Prompt {
pub fn new(
prompt: String,
mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static,
callback_fn: impl FnMut(&mut Editor, &str, PromptEvent) + 'static,
callback_fn: impl FnMut(&mut Context, &str, PromptEvent) + 'static,
) -> Self {
Self {
prompt,
@@ -46,21 +71,120 @@ impl Prompt {
}
}
/// Compute the cursor position after applying movement
/// Taken from: https://github.com/wez/wezterm/blob/e0b62d07ca9bf8ce69a61e30a3c20e7abc48ce7e/termwiz/src/lineedit/mod.rs#L516-L611
fn eval_movement(&self, movement: Movement) -> usize {
match movement {
Movement::BackwardChar(rep) => {
let mut position = self.cursor;
for _ in 0..rep {
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
if let Ok(Some(pos)) = cursor.prev_boundary(&self.line, 0) {
position = pos;
} else {
break;
}
}
position
}
Movement::BackwardWord(rep) => {
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
if char_indices.is_empty() {
return self.cursor;
}
let mut char_position = char_indices
.iter()
.position(|(idx, _)| *idx == self.cursor)
.unwrap_or(char_indices.len() - 1);
for _ in 0..rep {
if char_position == 0 {
break;
}
let mut found = None;
for prev in (0..char_position - 1).rev() {
if char_indices[prev].1.is_whitespace() {
found = Some(prev + 1);
break;
}
}
char_position = found.unwrap_or(0);
}
char_indices[char_position].0
}
Movement::ForwardWord(rep) => {
let char_indices: Vec<(usize, char)> = self.line.char_indices().collect();
if char_indices.is_empty() {
return self.cursor;
}
let mut char_position = char_indices
.iter()
.position(|(idx, _)| *idx == self.cursor)
.unwrap_or_else(|| char_indices.len());
for _ in 0..rep {
// Skip any non-whitespace characters
while char_position < char_indices.len()
&& !char_indices[char_position].1.is_whitespace()
{
char_position += 1;
}
// Skip any whitespace characters
while char_position < char_indices.len()
&& char_indices[char_position].1.is_whitespace()
{
char_position += 1;
}
// We are now on the start of the next word
}
char_indices
.get(char_position)
.map(|(i, _)| *i)
.unwrap_or_else(|| self.line.len())
}
Movement::ForwardChar(rep) => {
let mut position = self.cursor;
for _ in 0..rep {
let mut cursor = GraphemeCursor::new(position, self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
position = pos;
} else {
break;
}
}
position
}
Movement::StartOfLine => 0,
Movement::EndOfLine => {
let mut cursor =
GraphemeCursor::new(self.line.len().saturating_sub(1), self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
pos
} else {
self.cursor
}
}
Movement::None => self.cursor,
}
}
pub fn insert_char(&mut self, c: char) {
self.line.insert(self.cursor, c);
self.cursor += 1;
let mut cursor = GraphemeCursor::new(self.cursor, self.line.len(), false);
if let Ok(Some(pos)) = cursor.next_boundary(&self.line, 0) {
self.cursor = pos;
}
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
}
pub fn move_char_left(&mut self) {
self.cursor = self.cursor.saturating_sub(1)
}
pub fn move_char_right(&mut self) {
if self.cursor < self.line.len() {
self.cursor += 1;
}
pub fn move_cursor(&mut self, movement: Movement) {
let pos = self.eval_movement(movement);
self.cursor = pos
}
pub fn move_start(&mut self) {
@@ -72,19 +196,50 @@ impl Prompt {
}
pub fn delete_char_backwards(&mut self) {
if self.cursor > 0 {
self.line.remove(self.cursor - 1);
self.cursor -= 1;
self.completion = (self.completion_fn)(&self.line);
}
let pos = self.eval_movement(Movement::BackwardChar(1));
self.line.replace_range(pos..self.cursor, "");
self.cursor = pos;
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn delete_word_backwards(&mut self) {
let pos = self.eval_movement(Movement::BackwardWord(1));
self.line.replace_range(pos..self.cursor, "");
self.cursor = pos;
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn kill_to_end_of_line(&mut self) {
let pos = self.eval_movement(Movement::EndOfLine);
self.line.replace_range(self.cursor..pos, "");
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn clear(&mut self) {
self.line.clear();
self.cursor = 0;
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
}
pub fn change_completion_selection(&mut self) {
pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
if self.completion.is_empty() {
return;
}
let index = self.selection.map_or(0, |i| i + 1) % self.completion.len();
let index = match direction {
CompletionDirection::Forward => self.selection.map_or(0, |i| i + 1),
CompletionDirection::Backward => {
self.selection.unwrap_or(0) + self.completion.len() - 1
}
} % self.completion.len();
self.selection = Some(index);
let (range, item) = &self.completion[index];
@@ -92,19 +247,13 @@ impl Prompt {
self.line.replace_range(range.clone(), item);
self.move_end();
// TODO: recalculate completion when completion item is accepted, (Enter)
}
pub fn exit_selection(&mut self) {
self.selection = None;
}
}
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Modifier, Style},
};
const BASE_WIDTH: u16 = 30;
impl Prompt {
@@ -114,8 +263,21 @@ impl Prompt {
let selected_color = theme.get("ui.menu.selected");
// completion
let max_col = area.width / BASE_WIDTH;
let height = ((self.completion.len() as u16 + max_col - 1) / max_col);
let max_len = self
.completion
.iter()
.map(|(_, completion)| completion.len() as u16)
.max()
.unwrap_or(BASE_WIDTH)
.max(BASE_WIDTH);
let cols = std::cmp::max(1, area.width / max_len);
let col_width = (area.width - (cols)) / cols;
let height = ((self.completion.len() as u16 + cols - 1) / cols)
.min(10) // at most 10 rows (or less)
.min(area.height.saturating_sub(1));
let completion_area = Rect::new(
area.x,
(area.height - height).saturating_sub(1),
@@ -127,12 +289,22 @@ impl Prompt {
let area = completion_area;
let background = theme.get("ui.statusline");
let items = height as usize * cols as usize;
let offset = self
.selection
.map(|selection| selection / items * items)
.unwrap_or_default();
surface.clear_with(area, background);
let mut row = 0;
let mut col = 0;
for (i, (_range, completion)) in self.completion.iter().enumerate() {
// TODO: paginate
for (i, (_range, completion)) in
self.completion.iter().enumerate().skip(offset).take(items)
{
let color = if Some(i) == self.selection {
// Style::default().bg(Color::Rgb(104, 60, 232))
selected_color // TODO: just invert bg
@@ -140,10 +312,10 @@ impl Prompt {
text_color
};
surface.set_stringn(
area.x + 1 + col * BASE_WIDTH,
area.x + col * (1 + col_width),
area.y + row,
&completion,
BASE_WIDTH as usize - 1,
col_width.saturating_sub(1) as usize,
color,
);
row += 1;
@@ -151,21 +323,23 @@ impl Prompt {
row = 0;
col += 1;
}
if col > max_col {
break;
}
}
}
if let Some(doc) = (self.doc_fn)(&self.line) {
let text = ui::Text::new(doc.to_string());
let area = Rect::new(completion_area.x, completion_area.y - 3, BASE_WIDTH * 3, 3);
let viewport = area;
let area = viewport.intersection(Rect::new(
completion_area.x,
completion_area.y.saturating_sub(3),
BASE_WIDTH * 3,
3,
));
let background = theme.get("ui.help");
surface.clear_with(area, background);
use tui::layout::Margin;
text.render(
area.inner(&Margin {
vertical: 1,
@@ -212,47 +386,100 @@ impl Component for Prompt {
modifiers: KeyModifiers::SHIFT,
} => {
self.insert_char(c);
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Esc, ..
} => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort);
(self.callback_fn)(cx, &self.line, PromptEvent::Abort);
return close_fn;
}
KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Right,
..
} => self.move_char_right(),
} => self.move_cursor(Movement::ForwardChar(1)),
KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Left,
..
} => self.move_char_left(),
} => self.move_cursor(Movement::BackwardChar(1)),
KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
} => self.move_end(),
KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
} => self.move_start(),
KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::ALT,
}
| KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::ALT,
} => self.move_cursor(Movement::BackwardWord(1)),
KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::ALT,
}
| KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::ALT,
} => self.move_cursor(Movement::ForwardWord(1)),
KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::CONTROL,
} => self.delete_word_backwards(),
KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => self.kill_to_end_of_line(),
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE,
} => {
self.delete_char_backwards();
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
KeyEvent {
code: KeyCode::Enter,
..
} => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
return close_fn;
if self.line.ends_with('/') {
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
} else {
(self.callback_fn)(cx, &self.line, PromptEvent::Validate);
return close_fn;
}
}
KeyEvent {
code: KeyCode::Tab, ..
} => self.change_completion_selection(),
} => self.change_completion_selection(CompletionDirection::Forward),
KeyEvent {
code: KeyCode::BackTab,
..
} => self.change_completion_selection(CompletionDirection::Backward),
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
@@ -267,11 +494,16 @@ impl Component for Prompt {
self.render_prompt(area, surface, cx)
}
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
let line = area.height as usize - 1;
Some(Position::new(
area.y as usize + line,
area.x as usize + self.prompt.len() + self.cursor,
))
(
Some(Position::new(
area.y as usize + line,
area.x as usize
+ self.prompt.len()
+ UnicodeWidthStr::width(&self.line[..self.cursor]),
)),
CursorKind::Block,
)
}
}

View File

@@ -0,0 +1,75 @@
use std::{collections::HashMap, time::SystemTime};
#[derive(Default, Debug)]
pub struct ProgressSpinners {
inner: HashMap<usize, Spinner>,
}
impl ProgressSpinners {
pub fn get(&self, id: usize) -> Option<&Spinner> {
self.inner.get(&id)
}
pub fn get_or_create(&mut self, id: usize) -> &mut Spinner {
self.inner.entry(id).or_insert_with(Spinner::default)
}
}
impl Default for Spinner {
fn default() -> Self {
Self::dots(80)
}
}
#[derive(Debug)]
pub struct Spinner {
frames: Vec<&'static str>,
count: usize,
start: Option<SystemTime>,
interval: u64,
}
impl Spinner {
/// Creates a new spinner with `frames` and `interval`.
/// Expects the frames count and interval to be greater than 0.
pub fn new(frames: Vec<&'static str>, interval: u64) -> Self {
let count = frames.len();
assert!(count > 0);
assert!(interval > 0);
Self {
frames,
count,
interval,
start: None,
}
}
pub fn dots(interval: u64) -> Self {
Self::new(vec!["", "", "", "", "", "", "", ""], interval)
}
pub fn start(&mut self) {
self.start = Some(SystemTime::now());
}
pub fn frame(&self) -> Option<&str> {
let idx = (self
.start
.map(|time| SystemTime::now().duration_since(time))?
.ok()?
.as_millis()
/ self.interval as u128) as usize
% self.count;
self.frames.get(idx).copied()
}
pub fn stop(&mut self) {
self.start = None;
}
pub fn is_stopped(&self) -> bool {
self.start.is_none()
}
}

View File

@@ -1,15 +1,14 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use tui::buffer::Buffer as Surface;
use std::borrow::Cow;
use helix_core::Position;
use helix_view::Editor;
use helix_view::{
graphics::{Color, Rect, Style},
Editor,
};
pub struct Text {
contents: String,

View File

@@ -1,12 +1,16 @@
[package]
name = "helix-tui"
version = "0.1.0"
version = "0.3.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
description = """
A library to build rich terminal user interfaces or dashboards
"""
edition = "2018"
license = "MPL-2.0"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
[features]
default = ["crossterm"]
@@ -16,5 +20,7 @@ bitflags = "1.0"
cassowary = "0.3"
unicode-segmentation = "1.2"
unicode-width = "0.1"
crossterm = { version = "0.19", optional = true }
crossterm = { version = "0.20", optional = true }
serde = { version = "1", "optional" = true, features = ["derive"]}
helix-view = { version = "0.3", path = "../helix-view", features = ["term"] }
helix-core = { version = "0.3", path = "../helix-core" }

View File

@@ -1,11 +1,6 @@
use crate::{
backend::Backend,
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use crate::{backend::Backend, buffer::Cell};
use crossterm::{
cursor::{Hide, MoveTo, Show},
cursor::{CursorShape, Hide, MoveTo, SetCursorShape, Show},
execute, queue,
style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
@@ -13,6 +8,7 @@ use crossterm::{
},
terminal::{self, Clear, ClearType},
};
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
use std::io::{self, Write};
pub struct CrosstermBackend<W: Write> {
@@ -93,8 +89,14 @@ where
map_error(execute!(self.buffer, Hide))
}
fn show_cursor(&mut self) -> io::Result<()> {
map_error(execute!(self.buffer, Show))
fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
let shape = match kind {
CursorKind::Block => CursorShape::Block,
CursorKind::Bar => CursorShape::Line,
CursorKind::Underline => CursorShape::UnderScore,
CursorKind::Hidden => unreachable!(),
};
map_error(execute!(self.buffer, Show, SetCursorShape(shape)))
}
fn get_cursor(&mut self) -> io::Result<(u16, u16)> {
@@ -126,32 +128,6 @@ fn map_error(error: crossterm::Result<()>) -> io::Result<()> {
error.map_err(|e| io::Error::new(io::ErrorKind::Other, e.to_string()))
}
impl From<Color> for CColor {
fn from(color: Color) -> Self {
match color {
Color::Reset => CColor::Reset,
Color::Black => CColor::Black,
Color::Red => CColor::DarkRed,
Color::Green => CColor::DarkGreen,
Color::Yellow => CColor::DarkYellow,
Color::Blue => CColor::DarkBlue,
Color::Magenta => CColor::DarkMagenta,
Color::Cyan => CColor::DarkCyan,
Color::Gray => CColor::Grey,
Color::DarkGray => CColor::DarkGrey,
Color::LightRed => CColor::Red,
Color::LightGreen => CColor::Green,
Color::LightBlue => CColor::Blue,
Color::LightYellow => CColor::Yellow,
Color::LightMagenta => CColor::Magenta,
Color::LightCyan => CColor::Cyan,
Color::White => CColor::White,
Color::Indexed(i) => CColor::AnsiValue(i),
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
}
}
}
#[derive(Debug)]
struct ModifierDiff {
pub from: Modifier,

View File

@@ -1,7 +1,8 @@
use std::io;
use crate::buffer::Cell;
use crate::layout::Rect;
use helix_view::graphics::{CursorKind, Rect};
#[cfg(feature = "crossterm")]
mod crossterm;
@@ -16,7 +17,7 @@ pub trait Backend {
where
I: Iterator<Item = (u16, u16, &'a Cell)>;
fn hide_cursor(&mut self) -> Result<(), io::Error>;
fn show_cursor(&mut self) -> Result<(), io::Error>;
fn show_cursor(&mut self, kind: CursorKind) -> Result<(), io::Error>;
fn get_cursor(&mut self) -> Result<(u16, u16), io::Error>;
fn set_cursor(&mut self, x: u16, y: u16) -> Result<(), io::Error>;
fn clear(&mut self) -> Result<(), io::Error>;

View File

@@ -1,8 +1,8 @@
use crate::{
backend::Backend,
buffer::{Buffer, Cell},
layout::Rect,
};
use helix_view::graphics::{CursorKind, Rect};
use std::{fmt::Write, io};
use unicode_width::UnicodeWidthStr;
@@ -122,7 +122,7 @@ impl Backend for TestBackend {
Ok(())
}
fn show_cursor(&mut self) -> Result<(), io::Error> {
fn show_cursor(&mut self, _kind: CursorKind) -> Result<(), io::Error> {
self.cursor = true;
Ok(())
}

View File

@@ -1,12 +1,10 @@
use crate::{
layout::Rect,
style::{Color, Modifier, Style},
text::{Span, Spans},
};
use crate::text::{Span, Spans};
use std::cmp::min;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
use helix_view::graphics::{Color, Modifier, Rect, Style};
/// A buffer cell
#[derive(Debug, Clone, PartialEq)]
pub struct Cell {
@@ -89,8 +87,7 @@ impl Default for Cell {
///
/// ```
/// use helix_tui::buffer::{Buffer, Cell};
/// use helix_tui::layout::Rect;
/// use helix_tui::style::{Color, Style, Modifier};
/// use helix_view::graphics::{Rect, Color, Style, Modifier};
///
/// let mut buf = Buffer::empty(Rect{x: 0, y: 0, width: 10, height: 5});
/// buf.get_mut(0, 2).set_symbol("x");
@@ -193,7 +190,7 @@ impl Buffer {
///
/// ```
/// # use helix_tui::buffer::Buffer;
/// # use helix_tui::layout::Rect;
/// # use helix_view::graphics::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Global coordinates to the top corner of this buffer's area
@@ -203,16 +200,6 @@ impl Buffer {
/// # Panics
///
/// Panics when given an coordinate that is outside of this Buffer's area.
///
/// ```should_panic
/// # use helix_tui::buffer::Buffer;
/// # use helix_tui::layout::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// // Top coordinate is outside of the buffer in global coordinate space, as the Buffer's area
/// // starts at (200, 100).
/// buffer.index_of(0, 0); // Panics
/// ```
pub fn index_of(&self, x: u16, y: u16) -> usize {
debug_assert!(
x >= self.area.left()
@@ -235,7 +222,7 @@ impl Buffer {
///
/// ```
/// # use helix_tui::buffer::Buffer;
/// # use helix_tui::layout::Rect;
/// # use helix_view::graphics::Rect;
/// let rect = Rect::new(200, 100, 10, 10);
/// let buffer = Buffer::empty(rect);
/// assert_eq!(buffer.pos_of(0), (200, 100));
@@ -245,15 +232,6 @@ impl Buffer {
/// # Panics
///
/// Panics when given an index that is outside the Buffer's content.
///
/// ```should_panic
/// # use helix_tui::buffer::Buffer;
/// # use helix_tui::layout::Rect;
/// let rect = Rect::new(0, 0, 10, 10); // 100 cells in total
/// let buffer = Buffer::empty(rect);
/// // Index 100 is the 101th cell, which lies outside of the area of this Buffer.
/// buffer.pos_of(100); // Panics
/// ```
pub fn pos_of(&self, i: usize) -> (u16, u16) {
debug_assert!(
i < self.content.len(),
@@ -285,11 +263,30 @@ impl Buffer {
width: usize,
style: Style,
) -> (u16, u16)
where
S: AsRef<str>,
{
self.set_string_truncated(x, y, string, width, style, false)
}
/// Print at most the first `width` characters of a string if enough space is available
/// until the end of the line. If `markend` is true appends a `…` at the end of
/// truncated lines.
pub fn set_string_truncated<S>(
&mut self,
x: u16,
y: u16,
string: S,
width: usize,
style: Style,
ellipsis: bool,
) -> (u16, u16)
where
S: AsRef<str>,
{
let mut index = self.index_of(x, y);
let mut x_offset = x as usize;
let width = if ellipsis { width - 1 } else { width };
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
for s in graphemes {
@@ -312,6 +309,9 @@ impl Buffer {
index += width;
x_offset += width;
}
if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
self.content[index].set_symbol("");
}
(x_offset as u16, y)
}
@@ -510,6 +510,7 @@ mod tests {
#[test]
#[should_panic(expected = "outside the buffer")]
#[cfg(debug_assertions)]
fn pos_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect);
@@ -520,6 +521,7 @@ mod tests {
#[test]
#[should_panic(expected = "outside the buffer")]
#[cfg(debug_assertions)]
fn index_of_panics_on_out_of_bounds() {
let rect = Rect::new(0, 0, 10, 10);
let buf = Buffer::empty(rect);

View File

@@ -1,11 +1,12 @@
use std::cell::RefCell;
use std::cmp::{max, min};
use std::collections::HashMap;
use cassowary::strength::{REQUIRED, WEAK};
use cassowary::WeightedRelation::*;
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
use helix_view::graphics::{Margin, Rect};
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
pub enum Corner {
TopLeft,
@@ -45,12 +46,6 @@ impl Constraint {
}
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub vertical: u16,
pub horizontal: u16,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment {
Left,
@@ -119,7 +114,8 @@ impl Layout {
///
/// # Examples
/// ```
/// # use helix_tui::layout::{Rect, Constraint, Direction, Layout};
/// # use helix_tui::layout::{Constraint, Direction, Layout};
/// # use helix_view::graphics::Rect;
/// let chunks = Layout::default()
/// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref())
@@ -348,117 +344,6 @@ impl Element {
}
}
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl Default for Rect {
fn default() -> Rect {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
let max_area = u16::max_value();
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
Rect {
x,
y,
width: clipped_width,
height: clipped_height,
}
}
pub fn area(self) -> u16 {
self.width * self.height
}
pub fn left(self) -> u16 {
self.x
}
pub fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
pub fn top(self) -> u16 {
self.y
}
pub fn bottom(self) -> u16 {
self.y.saturating_add(self.height)
}
pub fn inner(self, margin: &Margin) -> Rect {
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
Rect::default()
} else {
Rect {
x: self.x + margin.horizontal,
y: self.y + margin.vertical,
width: self.width - 2 * margin.horizontal,
height: self.height - 2 * margin.vertical,
}
}
}
pub fn union(self, other: Rect) -> Rect {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.x + self.width, other.x + other.width);
let y2 = max(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersection(self, other: Rect) -> Rect {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.x + self.width, other.x + other.width);
let y2 = min(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersects(self, other: Rect) -> bool {
self.x < other.x + other.width
&& self.x + self.width > other.x
&& self.y < other.y + other.height
&& self.y + self.height > other.y
}
}
#[cfg(test)]
mod tests {
use super::*;
@@ -487,48 +372,4 @@ mod tests {
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>());
chunks.windows(2).for_each(|w| assert!(w[0].y <= w[1].y));
}
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
)
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}
}

View File

@@ -44,7 +44,7 @@
//! implement your own.
//!
//! Each widget follows a builder pattern API providing a default configuration along with methods
//! to customize them. The widget is then rendered using the [`Frame::render_widget`] which take
//! to customize them. The widget is then rendered using the `Frame::render_widget` which take
//! your widget instance an area to draw to.
//!
//! The following example renders a block of the size of the terminal:
@@ -125,7 +125,6 @@
pub mod backend;
pub mod buffer;
pub mod layout;
pub mod style;
pub mod symbols;
pub mod terminal;
pub mod text;

View File

@@ -1,4 +1,5 @@
use crate::{backend::Backend, buffer::Buffer, layout::Rect};
use crate::{backend::Backend, buffer::Buffer};
use helix_view::graphics::{CursorKind, Rect};
use std::io;
#[derive(Debug, Clone, PartialEq)]
@@ -57,7 +58,7 @@ where
fn drop(&mut self) {
// Attempt to restore the cursor state
if self.hidden_cursor {
if let Err(err) = self.show_cursor() {
if let Err(err) = self.show_cursor(CursorKind::Block) {
eprintln!("Failed to show the cursor: {}", err);
}
}
@@ -137,19 +138,21 @@ where
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
if self.viewport.resize_behavior == ResizeBehavior::Auto {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
}
pub fn autoresize(&mut self) -> io::Result<Rect> {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
};
Ok(())
Ok(size)
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call.
pub fn draw(&mut self, cursor_position: Option<(u16, u16)>) -> io::Result<()> {
pub fn draw(
&mut self,
cursor_position: Option<(u16, u16)>,
cursor_kind: CursorKind,
) -> io::Result<()> {
// // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// // and the terminal (if growing), which may OOB.
// self.autoresize()?;
@@ -164,12 +167,13 @@ where
// Draw to stdout
self.flush()?;
match cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?;
}
if let Some((x, y)) = cursor_position {
self.set_cursor(x, y)?;
}
match cursor_kind {
CursorKind::Hidden => self.hide_cursor()?,
kind => self.show_cursor(kind)?,
}
// Swap buffers
@@ -187,8 +191,8 @@ where
Ok(())
}
pub fn show_cursor(&mut self) -> io::Result<()> {
self.backend.show_cursor()?;
pub fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
self.backend.show_cursor(kind)?;
self.hidden_cursor = false;
Ok(())
}

View File

@@ -21,7 +21,7 @@
//! ```rust
//! # use helix_tui::widgets::Block;
//! # use helix_tui::text::{Span, Spans};
//! # use helix_tui::style::{Color, Style};
//! # use helix_view::graphics::{Color, Style};
//! // A simple string with no styling.
//! // Converted to Spans(vec![
//! // Span { content: Cow::Borrowed("My title"), style: Style { .. } }
@@ -46,7 +46,8 @@
//! Span::raw(" title"),
//! ]);
//! ```
use crate::style::Style;
use helix_core::line_ending::str_is_line_ending;
use helix_view::graphics::Style;
use std::borrow::Cow;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
@@ -91,7 +92,7 @@ impl<'a> Span<'a> {
///
/// ```rust
/// # use helix_tui::text::Span;
/// # use helix_tui::style::{Color, Modifier, Style};
/// # use helix_view::graphics::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Span::styled("My text", style);
/// Span::styled(String::from("My text"), style);
@@ -120,7 +121,7 @@ impl<'a> Span<'a> {
///
/// ```rust
/// # use helix_tui::text::{Span, StyledGrapheme};
/// # use helix_tui::style::{Color, Modifier, Style};
/// # use helix_view::graphics::{Color, Modifier, Style};
/// # use std::iter::Iterator;
/// let style = Style::default().fg(Color::Yellow);
/// let span = Span::styled("Text", style);
@@ -177,7 +178,7 @@ impl<'a> Span<'a> {
symbol: g,
style: base_style.patch(self.style),
})
.filter(|s| s.symbol != "\n")
.filter(|s| !str_is_line_ending(s.symbol))
}
}
@@ -210,7 +211,7 @@ impl<'a> Spans<'a> {
///
/// ```rust
/// # use helix_tui::text::{Span, Spans};
/// # use helix_tui::style::{Color, Style};
/// # use helix_view::graphics::{Color, Style};
/// let spans = Spans::from(vec![
/// Span::styled("My", Style::default().fg(Color::Yellow)),
/// Span::raw(" text"),
@@ -264,7 +265,7 @@ impl<'a> From<Spans<'a>> for String {
///
/// ```rust
/// # use helix_tui::text::Text;
/// # use helix_tui::style::{Color, Modifier, Style};
/// # use helix_view::graphics::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
///
/// // An initial two lines of `Text` built from a `&str`
@@ -318,7 +319,7 @@ impl<'a> Text<'a> {
///
/// ```rust
/// # use helix_tui::text::Text;
/// # use helix_tui::style::{Color, Modifier, Style};
/// # use helix_view::graphics::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// Text::styled("The first line\nThe second line", style);
/// Text::styled(String::from("The first line\nThe second line"), style);
@@ -368,7 +369,7 @@ impl<'a> Text<'a> {
///
/// ```rust
/// # use helix_tui::text::Text;
/// # use helix_tui::style::{Color, Modifier, Style};
/// # use helix_view::graphics::{Color, Modifier, Style};
/// let style = Style::default().fg(Color::Yellow).add_modifier(Modifier::ITALIC);
/// let mut raw_text = Text::raw("The first line\nThe second line");
/// let styled_text = Text::styled(String::from("The first line\nThe second line"), style);

View File

@@ -1,11 +1,10 @@
use crate::{
buffer::Buffer,
layout::Rect,
style::Style,
symbols::line,
text::{Span, Spans},
widgets::{Borders, Widget},
};
use helix_view::graphics::{Rect, Style};
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderType {
@@ -33,7 +32,7 @@ impl BorderType {
///
/// ```
/// # use helix_tui::widgets::{Block, BorderType, Borders};
/// # use helix_tui::style::{Style, Color};
/// # use helix_view::graphics::{Style, Color};
/// Block::default()
/// .title("Block")
/// .borders(Borders::LEFT | Borders::RIGHT)
@@ -212,7 +211,6 @@ impl<'a> Widget for Block<'a> {
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::Rect;
#[test]
fn inner_takes_into_account_the_borders() {

View File

@@ -1,4 +1,4 @@
//! `widgets` is a collection of types that implement [`Widget`] or [`StatefulWidget`] or both.
//! `widgets` is a collection of types that implement [`Widget`].
//!
//! All widgets are implemented using the builder pattern and are consumable objects. They are not
//! meant to be stored but used as *commands* to draw common figures in the UI.
@@ -20,9 +20,11 @@ pub use self::block::{Block, BorderType};
pub use self::paragraph::{Paragraph, Wrap};
pub use self::table::{Cell, Row, Table, TableState};
use crate::{buffer::Buffer, layout::Rect};
use crate::buffer::Buffer;
use bitflags::bitflags;
use helix_view::graphics::Rect;
bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget.
pub struct Borders: u32 {

View File

@@ -1,13 +1,13 @@
use crate::{
buffer::Buffer,
layout::{Alignment, Rect},
style::Style,
layout::Alignment,
text::{StyledGrapheme, Text},
widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper},
Block, Widget,
},
};
use helix_view::graphics::{Rect, Style};
use std::iter;
use unicode_width::UnicodeWidthStr;
@@ -26,8 +26,8 @@ fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment)
/// ```
/// # use helix_tui::text::{Text, Spans, Span};
/// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use helix_tui::style::{Style, Color, Modifier};
/// # use helix_tui::layout::{Alignment};
/// # use helix_view::graphics::{Style, Color, Modifier};
/// let text = vec![
/// Spans::from(vec![
/// Span::raw("First"),

View File

@@ -1,4 +1,5 @@
use crate::text::StyledGrapheme;
use helix_core::line_ending::str_is_line_ending;
use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
@@ -62,13 +63,13 @@ impl<'a, 'b> LineComposer<'a> for WordWrapper<'a, 'b> {
// Ignore characters wider that the total max width.
if symbol.width() as u16 > self.max_line_width
// Skip leading whitespace when trim is enabled.
|| self.trim && symbol_whitespace && symbol != "\n" && current_line_width == 0
|| self.trim && symbol_whitespace && !str_is_line_ending(symbol) && current_line_width == 0
{
continue;
}
// Break on newline and discard it.
if symbol == "\n" {
if str_is_line_ending(symbol) {
if prev_whitespace {
current_line_width = width_to_last_word_end;
self.current_line.truncate(symbols_to_last_word_end);
@@ -170,7 +171,7 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
}
// Break on newline and discard it.
if symbol == "\n" {
if str_is_line_ending(symbol) {
break;
}
@@ -199,7 +200,7 @@ impl<'a, 'b> LineComposer<'a> for LineTruncator<'a, 'b> {
if skip_rest {
for StyledGrapheme { symbol, .. } in &mut self.symbols {
if symbol == "\n" {
if str_is_line_ending(symbol) {
break;
}
}

View File

@@ -1,7 +1,6 @@
use crate::{
buffer::Buffer,
layout::{Constraint, Rect},
style::Style,
layout::Constraint,
text::Text,
widgets::{Block, Widget},
};
@@ -10,10 +9,8 @@ use cassowary::{
WeightedRelation::*,
{Expression, Solver},
};
use std::{
collections::HashMap,
iter::{self, Iterator},
};
use helix_view::graphics::{Rect, Style};
use std::collections::HashMap;
use unicode_width::UnicodeWidthStr;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
@@ -21,8 +18,8 @@ use unicode_width::UnicodeWidthStr;
/// It can be created from anything that can be converted to a [`Text`].
/// ```rust
/// # use helix_tui::widgets::Cell;
/// # use helix_tui::style::{Style, Modifier};
/// # use helix_tui::text::{Span, Spans, Text};
/// # use helix_view::graphics::{Style, Modifier};
/// Cell::from("simple string");
///
/// Cell::from(Span::from("span"));
@@ -74,7 +71,7 @@ where
/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
/// ```rust
/// # use helix_tui::widgets::{Row, Cell};
/// # use helix_tui::style::{Style, Color};
/// # use helix_view::graphics::{Style, Color};
/// Row::new(vec![
/// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
@@ -137,7 +134,7 @@ impl<'a> Row<'a> {
/// ```rust
/// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell};
/// # use helix_tui::layout::Constraint;
/// # use helix_tui::style::{Style, Color, Modifier};
/// # use helix_view::graphics::{Style, Color, Modifier};
/// # use helix_tui::text::{Text, Spans, Span};
/// Table::new(vec![
/// // Row can be created from simple strings.
@@ -415,9 +412,7 @@ impl<'a> Table<'a> {
let has_selection = state.selected.is_some();
let columns_widths = self.get_columns_widths(table_area.width, has_selection);
let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ")
.take(highlight_symbol.width())
.collect::<String>();
let blank_symbol = " ".repeat(highlight_symbol.width());
let mut current_height = 0;
let mut rows_height = table_area.height;

View File

@@ -1,10 +1,7 @@
use helix_tui::{
backend::{Backend, TestBackend},
layout::Rect,
widgets::Paragraph,
Terminal,
};
use std::error::Error;
#[test]
fn terminal_buffer_size_should_be_limited() {

View File

@@ -1,30 +1,42 @@
[package]
name = "helix-view"
version = "0.1.0"
version = "0.3.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "UI abstractions for use in backends"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
[features]
term = ["tui", "crossterm"]
default = ["term"]
default = []
term = ["crossterm"]
[dependencies]
bitflags = "1.0"
anyhow = "1"
helix-core = { path = "../helix-core" }
helix-lsp = { path = "../helix-lsp"}
helix-core = { version = "0.3", path = "../helix-core" }
helix-lsp = { version = "0.3", path = "../helix-lsp"}
crossterm = { version = "0.20", optional = true }
# Conversion traits
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"], optional = true }
crossterm = { version = "0.19", features = ["event-stream"], optional = true }
once_cell = "1.4"
once_cell = "1.8"
url = "2"
tokio = { version = "1", features = ["full"] }
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
slotmap = "1"
encoding_rs = "0.8"
chardetng = "0.1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
log = "~0.4"
which = "4.1"
[dev-dependencies]
helix-tui = { path = "../helix-tui" }

193
helix-view/src/clipboard.rs Normal file
View File

@@ -0,0 +1,193 @@
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
use anyhow::Result;
use std::borrow::Cow;
pub trait ClipboardProvider: std::fmt::Debug {
fn name(&self) -> Cow<str>;
fn get_contents(&self) -> Result<String>;
fn set_contents(&self, contents: String) -> Result<()>;
}
macro_rules! command_provider {
(paste => $get_prg:literal $( , $get_arg:literal )* ; copy => $set_prg:literal $( , $set_arg:literal )* ; ) => {{
Box::new(provider::CommandProvider {
get_cmd: provider::CommandConfig {
prg: $get_prg,
args: &[ $( $get_arg ),* ],
},
set_cmd: provider::CommandConfig {
prg: $set_prg,
args: &[ $( $set_arg ),* ],
},
})
}};
}
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
// TODO: support for user-defined provider, probably when we have plugin support by setting a
// variable?
if exists("pbcopy") && exists("pbpaste") {
command_provider! {
paste => "pbpaste";
copy => "pbcopy";
}
} else if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") {
command_provider! {
paste => "wl-paste", "--no-newline";
copy => "wl-copy", "--type", "text/plain";
}
} else if env_var_is_set("DISPLAY") && exists("xclip") {
command_provider! {
paste => "xclip", "-o", "-selection", "clipboard";
copy => "xclip", "-i", "-selection", "clipboard";
}
} else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"])
{
// FIXME: check performance of is_exit_success
command_provider! {
paste => "xsel", "-o", "-b";
copy => "xsel", "--nodetach", "-i", "-b";
}
} else if exists("lemonade") {
command_provider! {
paste => "lemonade", "paste";
copy => "lemonade", "copy";
}
} else if exists("doitclient") {
command_provider! {
paste => "doitclient", "wclip", "-r";
copy => "doitclient", "wclip";
}
} else if exists("win32yank.exe") {
// FIXME: does it work within WSL?
command_provider! {
paste => "win32yank.exe", "-o", "--lf";
copy => "win32yank.exe", "-i", "--crlf";
}
} else if exists("termux-clipboard-set") && exists("termux-clipboard-get") {
command_provider! {
paste => "termux-clipboard-get";
copy => "termux-clipboard-set";
}
} else if env_var_is_set("TMUX") && exists("tmux") {
command_provider! {
paste => "tmux", "save-buffer", "-";
copy => "tmux", "load-buffer", "-";
}
} else {
Box::new(provider::NopProvider)
}
}
fn exists(executable_name: &str) -> bool {
which::which(executable_name).is_ok()
}
fn env_var_is_set(env_var_name: &str) -> bool {
std::env::var_os(env_var_name).is_some()
}
fn is_exit_success(program: &str, args: &[&str]) -> bool {
std::process::Command::new(program)
.args(args)
.output()
.ok()
.and_then(|out| out.status.success().then(|| ())) // TODO: use then_some when stabilized
.is_some()
}
mod provider {
use super::ClipboardProvider;
use anyhow::{bail, Context as _, Result};
use std::borrow::Cow;
#[derive(Debug)]
pub struct NopProvider;
impl ClipboardProvider for NopProvider {
fn name(&self) -> Cow<str> {
Cow::Borrowed("none")
}
fn get_contents(&self) -> Result<String> {
Ok(String::new())
}
fn set_contents(&self, _: String) -> Result<()> {
Ok(())
}
}
#[derive(Debug)]
pub struct CommandConfig {
pub prg: &'static str,
pub args: &'static [&'static str],
}
impl CommandConfig {
fn execute(&self, input: Option<&str>, pipe_output: bool) -> Result<Option<String>> {
use std::io::Write;
use std::process::{Command, Stdio};
let stdin = input.map(|_| Stdio::piped()).unwrap_or_else(Stdio::null);
let stdout = pipe_output.then(Stdio::piped).unwrap_or_else(Stdio::null);
let mut child = Command::new(self.prg)
.args(self.args)
.stdin(stdin)
.stdout(stdout)
.stderr(Stdio::null())
.spawn()?;
if let Some(input) = input {
let mut stdin = child.stdin.take().context("stdin is missing")?;
stdin
.write_all(input.as_bytes())
.context("couldn't write in stdin")?;
}
// TODO: add timer?
let output = child.wait_with_output()?;
if !output.status.success() {
bail!("clipboard provider {} failed", self.prg);
}
if pipe_output {
Ok(Some(String::from_utf8(output.stdout)?))
} else {
Ok(None)
}
}
}
#[derive(Debug)]
pub struct CommandProvider {
pub get_cmd: CommandConfig,
pub set_cmd: CommandConfig,
}
impl ClipboardProvider for CommandProvider {
fn name(&self) -> Cow<str> {
if self.get_cmd.prg != self.set_cmd.prg {
Cow::Owned(format!("{}+{}", self.get_cmd.prg, self.set_cmd.prg))
} else {
Cow::Borrowed(self.get_cmd.prg)
}
}
fn get_contents(&self) -> Result<String> {
let output = self
.get_cmd
.execute(None, true)?
.context("output is missing")?;
Ok(output)
}
fn set_contents(&self, value: String) -> Result<()> {
self.set_cmd.execute(Some(&value), false).map(|_| ())
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +1,41 @@
use crate::{theme::Theme, tree::Tree, Document, DocumentId, View, ViewId};
use tui::layout::Rect;
use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
graphics::{CursorKind, Rect},
theme::{self, Theme},
tree::Tree,
Document, DocumentId, RegisterSelection, View, ViewId,
};
use std::path::PathBuf;
use futures_util::future;
use std::{path::PathBuf, sync::Arc, time::Duration};
use slotmap::SlotMap;
use anyhow::Error;
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
use helix_core::syntax;
use helix_core::Position;
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
pub documents: SlotMap<DocumentId, Document>,
pub count: Option<usize>,
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: RegisterSelection,
pub registers: Registers,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
pub clipboard_provider: Box<dyn ClipboardProvider>,
pub syn_loader: Arc<syntax::Loader>,
pub theme_loader: Arc<theme::Loader>,
pub status_msg: Option<(String, Severity)>,
}
#[derive(Copy, Clone)]
#[derive(Debug, Copy, Clone)]
pub enum Action {
Replace,
HorizontalSplit,
@@ -27,15 +43,11 @@ pub enum Action {
}
impl Editor {
pub fn new(mut area: tui::layout::Rect) -> Self {
use helix_core::config_dir;
let config = std::fs::read(config_dir().join("theme.toml"));
// load $HOME/.config/helix/theme.toml, fallback to default config
let toml = config
.as_deref()
.unwrap_or(include_bytes!("../../theme.toml"));
let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
pub fn new(
mut area: Rect,
themes: Arc<theme::Loader>,
config_loader: Arc<syntax::Loader>,
) -> Self {
let language_servers = helix_lsp::Registry::new();
// HAXX: offset the render area height by 1 to account for prompt/commandline
@@ -45,12 +57,21 @@ impl Editor {
tree: Tree::new(area),
documents: SlotMap::with_key(),
count: None,
theme,
selected_register: RegisterSelection::default(),
theme: themes.default(),
language_servers,
syn_loader: config_loader,
theme_loader: themes,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
}
}
pub fn clear_status(&mut self) {
self.status_msg = None;
}
pub fn set_status(&mut self, status: String) {
self.status_msg = Some((status, Severity::Info));
}
@@ -59,6 +80,32 @@ impl Editor {
self.status_msg = Some((error, Severity::Error));
}
pub fn set_theme(&mut self, theme: Theme) {
let scopes = theme.scopes();
for config in self
.syn_loader
.language_configs_iter()
.filter(|cfg| cfg.is_highlight_initialized())
{
config.reconfigure(scopes);
}
self.theme = theme;
self._refresh();
}
pub fn set_theme_from_name(&mut self, theme: &str) {
let theme = match self.theme_loader.load(theme.as_ref()) {
Ok(theme) => theme,
Err(e) => {
log::warn!("failed setting theme `{}` - {}", theme, e);
return;
}
};
self.set_theme(theme);
}
fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() {
let doc = &self.documents[view.doc];
@@ -69,20 +116,27 @@ impl Editor {
pub fn switch(&mut self, id: DocumentId, action: Action) {
use crate::tree::Layout;
use helix_core::Selection;
if !self.documents.contains_key(id) {
log::error!("cannot switch to document that does not exist (anymore)");
return;
}
match action {
Action::Replace => {
let view = self.view();
let view = view!(self);
let jump = (
view.doc,
self.documents[view.doc].selection(view.id).clone(),
);
let view = self.view_mut();
let view = view_mut!(self);
view.jumps.push(jump);
view.last_accessed_doc = Some(view.doc);
view.doc = id;
view.first_line = 0;
let (view, doc) = self.current();
let (view, doc) = current!(self);
// initialize selection for view
let selection = doc
@@ -116,8 +170,7 @@ impl Editor {
}
pub fn new_file(&mut self, action: Action) -> DocumentId {
use helix_core::Rope;
let doc = Document::new(Rope::from("\n"));
let doc = Document::default();
let id = self.documents.insert(doc);
self.documents[id].id = id;
self.switch(id, action);
@@ -125,7 +178,7 @@ impl Editor {
}
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = std::fs::canonicalize(path)?;
let path = crate::document::canonicalize_path(&path)?;
let id = self
.documents()
@@ -135,13 +188,13 @@ impl Editor {
let id = if let Some(id) = id {
id
} else {
let mut doc = Document::load(path, self.theme.scopes())?;
let mut doc = Document::open(path, None, Some(&self.theme), Some(&self.syn_loader))?;
// try to find a language server based on the language name
let language_server = doc
.language
.as_ref()
.and_then(|language| self.language_servers.get(language));
.and_then(|language| self.language_servers.get(language).ok());
if let Some(language_server) = language_server {
doc.set_language_server(Some(language_server.clone()));
@@ -182,7 +235,7 @@ impl Editor {
let language_server = doc
.language
.as_ref()
.and_then(|language| language_servers.get(language));
.and_then(|language| language_servers.get(language).ok());
if let Some(language_server) = language_server {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
@@ -194,8 +247,9 @@ impl Editor {
}
pub fn resize(&mut self, area: Rect) {
self.tree.resize(area);
self._refresh();
if self.tree.resize(area) {
self._refresh();
};
}
pub fn focus_next(&mut self) {
@@ -206,20 +260,6 @@ impl Editor {
self.tree.is_empty()
}
pub fn current(&mut self) -> (&mut View, &mut Document) {
let view = self.tree.get_mut(self.tree.focus);
let doc = &mut self.documents[view.doc];
(view, doc)
}
pub fn view(&self) -> &View {
self.tree.get(self.tree.focus)
}
pub fn view_mut(&mut self) -> &mut View {
self.tree.get_mut(self.tree.focus)
}
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
let view = self.tree.get_mut(id);
let doc = &self.documents[view.doc];
@@ -234,21 +274,44 @@ impl Editor {
self.documents.iter().map(|(_id, doc)| doc)
}
pub fn documents_mut(&mut self) -> impl Iterator<Item = &mut Document> {
self.documents.iter_mut().map(|(_id, doc)| doc)
}
// pub fn current_document(&self) -> Document {
// let id = self.view().doc;
// let doc = &mut editor.documents[id];
// }
pub fn cursor_position(&self) -> Option<helix_core::Position> {
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
let view = self.view();
let view = view!(self);
let doc = &self.documents[view.doc];
let cursor = doc.selection(view.id).cursor();
if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), cursor) {
pos.col += view.area.x as usize + OFFSET as usize;
pos.row += view.area.y as usize;
return Some(pos);
(Some(pos), CursorKind::Hidden)
} else {
(None, CursorKind::Hidden)
}
None
}
/// Closes language servers with timeout. The default timeout is 500 ms, use
/// `timeout` parameter to override this.
pub async fn close_language_servers(
&self,
timeout: Option<u64>,
) -> Result<(), tokio::time::error::Elapsed> {
tokio::time::timeout(
Duration::from_millis(timeout.unwrap_or(500)),
future::join_all(
self.language_servers
.iter_clients()
.map(|client| client.force_shutdown()),
),
)
.await
.map(|_| ())
}
}

View File

@@ -1,281 +1,481 @@
//! `style` contains the primitives used to control how your user interface will look.
use bitflags::bitflags;
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
Gray,
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
White,
Rgb(u8, u8, u8),
Indexed(u8),
}
bitflags! {
/// Modifier changes the way a piece of text is displayed.
///
/// They are bitflags so they can easily be composed.
///
/// ## Examples
///
/// ```rust
/// # use helix_tui::style::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
const ITALIC = 0b0000_0000_0100;
const UNDERLINED = 0b0000_0000_1000;
const SLOW_BLINK = 0b0000_0001_0000;
const RAPID_BLINK = 0b0000_0010_0000;
const REVERSED = 0b0000_0100_0000;
const HIDDEN = 0b0000_1000_0000;
const CROSSED_OUT = 0b0001_0000_0000;
}
}
/// Style let you control the main characteristics of the displayed elements.
///
/// ```rust
/// # use helix_tui::style::{Color, Modifier, Style};
/// Style::default()
/// .fg(Color::Black)
/// .bg(Color::Green)
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
/// ```
///
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
/// just S3.
///
/// ```rust
/// # use helix_tui::style::{Color, Modifier, Style};
/// # use helix_tui::buffer::Buffer;
/// # use helix_tui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default().bg(Color::Red),
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// }
/// assert_eq!(
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Red),
/// add_modifier: Modifier::BOLD,
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// );
/// ```
///
/// The default implementation returns a `Style` that does not modify anything. If you wish to
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// # use helix_tui::style::{Color, Modifier, Style};
/// # use helix_tui::buffer::Buffer;
/// # use helix_tui::layout::Rect;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::reset().fg(Color::Yellow),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// }
/// assert_eq!(
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Reset),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
impl Default for Style {
fn default() -> Style {
Style {
fg: None,
bg: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
}
}
impl Style {
/// Returns a `Style` resetting all properties.
pub fn reset() -> Style {
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(),
}
}
/// Changes the foreground color.
///
/// ## Examples
///
/// ```rust
/// # use helix_tui::style::{Color, Style};
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
pub fn fg(mut self, color: Color) -> Style {
self.fg = Some(color);
self
}
/// Changes the background color.
///
/// ## Examples
///
/// ```rust
/// # use helix_tui::style::{Color, Style};
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
pub fn bg(mut self, color: Color) -> Style {
self.bg = Some(color);
self
}
/// Changes the text emphasis.
///
/// When applied, it adds the given modifier to the `Style` modifiers.
///
/// ## Examples
///
/// ```rust
/// # use helix_tui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
self.sub_modifier.remove(modifier);
self.add_modifier.insert(modifier);
self
}
/// Changes the text emphasis.
///
/// When applied, it removes the given modifier from the `Style` modifiers.
///
/// ## Examples
///
/// ```rust
/// # use helix_tui::style::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
self.add_modifier.remove(modifier);
self.sub_modifier.insert(modifier);
self
}
/// Results in a combined style that is equivalent to applying the two individual styles to
/// a style one after the other.
///
/// ## Examples
/// ```
/// # use helix_tui::style::{Color, Modifier, Style};
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
/// assert_eq!(
/// Style::default().patch(style_1).patch(style_2),
/// Style::default().patch(combined));
/// ```
pub fn patch(mut self, other: Style) -> Style {
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
self.add_modifier.remove(other.sub_modifier);
self.add_modifier.insert(other.add_modifier);
self.sub_modifier.remove(other.add_modifier);
self.sub_modifier.insert(other.sub_modifier);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
fn styles() -> Vec<Style> {
vec![
Style::default(),
Style::default().fg(Color::Yellow),
Style::default().bg(Color::Yellow),
Style::default().add_modifier(Modifier::BOLD),
Style::default().remove_modifier(Modifier::BOLD),
Style::default().add_modifier(Modifier::ITALIC),
Style::default().remove_modifier(Modifier::ITALIC),
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
]
}
#[test]
fn combined_patch_gives_same_result_as_individual_patch() {
let styles = styles();
for &a in &styles {
for &b in &styles {
for &c in &styles {
for &d in &styles {
let combined = a.patch(b.patch(c.patch(d)));
assert_eq!(
Style::default().patch(a).patch(b).patch(c).patch(d),
Style::default().patch(combined)
);
}
}
}
}
}
}
use bitflags::bitflags;
use std::cmp::{max, min};
#[derive(Debug)]
/// UNSTABLE
pub enum CursorKind {
/// █
Block,
/// |
Bar,
/// _
Underline,
/// Hidden cursor, can set cursor position with this to let IME have correct cursor position.
Hidden,
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Margin {
pub vertical: u16,
pub horizontal: u16,
}
/// A simple rectangle used in the computation of the layout and to give widgets an hint about the
/// area they are supposed to render to.
#[derive(Debug, Clone, Copy, Hash, PartialEq, Eq)]
pub struct Rect {
pub x: u16,
pub y: u16,
pub width: u16,
pub height: u16,
}
impl Default for Rect {
fn default() -> Rect {
Rect {
x: 0,
y: 0,
width: 0,
height: 0,
}
}
}
impl Rect {
/// Creates a new rect, with width and height limited to keep the area under max u16.
/// If clipped, aspect ratio will be preserved.
pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect {
let max_area = u16::max_value();
let (clipped_width, clipped_height) =
if u32::from(width) * u32::from(height) > u32::from(max_area) {
let aspect_ratio = f64::from(width) / f64::from(height);
let max_area_f = f64::from(max_area);
let height_f = (max_area_f / aspect_ratio).sqrt();
let width_f = height_f * aspect_ratio;
(width_f as u16, height_f as u16)
} else {
(width, height)
};
Rect {
x,
y,
width: clipped_width,
height: clipped_height,
}
}
pub fn area(self) -> u16 {
self.width * self.height
}
pub fn left(self) -> u16 {
self.x
}
pub fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
pub fn top(self) -> u16 {
self.y
}
pub fn bottom(self) -> u16 {
self.y.saturating_add(self.height)
}
pub fn inner(self, margin: &Margin) -> Rect {
if self.width < 2 * margin.horizontal || self.height < 2 * margin.vertical {
Rect::default()
} else {
Rect {
x: self.x + margin.horizontal,
y: self.y + margin.vertical,
width: self.width - 2 * margin.horizontal,
height: self.height - 2 * margin.vertical,
}
}
}
pub fn union(self, other: Rect) -> Rect {
let x1 = min(self.x, other.x);
let y1 = min(self.y, other.y);
let x2 = max(self.x + self.width, other.x + other.width);
let y2 = max(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersection(self, other: Rect) -> Rect {
let x1 = max(self.x, other.x);
let y1 = max(self.y, other.y);
let x2 = min(self.x + self.width, other.x + other.width);
let y2 = min(self.y + self.height, other.y + other.height);
Rect {
x: x1,
y: y1,
width: x2 - x1,
height: y2 - y1,
}
}
pub fn intersects(self, other: Rect) -> bool {
self.x < other.x + other.width
&& self.x + self.width > other.x
&& self.y < other.y + other.height
&& self.y + self.height > other.y
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub enum Color {
Reset,
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
Gray,
DarkGray,
LightRed,
LightGreen,
LightYellow,
LightBlue,
LightMagenta,
LightCyan,
White,
Rgb(u8, u8, u8),
Indexed(u8),
}
#[cfg(feature = "term")]
impl From<Color> for crossterm::style::Color {
fn from(color: Color) -> Self {
use crossterm::style::Color as CColor;
match color {
Color::Reset => CColor::Reset,
Color::Black => CColor::Black,
Color::Red => CColor::DarkRed,
Color::Green => CColor::DarkGreen,
Color::Yellow => CColor::DarkYellow,
Color::Blue => CColor::DarkBlue,
Color::Magenta => CColor::DarkMagenta,
Color::Cyan => CColor::DarkCyan,
Color::Gray => CColor::Grey,
Color::DarkGray => CColor::DarkGrey,
Color::LightRed => CColor::Red,
Color::LightGreen => CColor::Green,
Color::LightBlue => CColor::Blue,
Color::LightYellow => CColor::Yellow,
Color::LightMagenta => CColor::Magenta,
Color::LightCyan => CColor::Cyan,
Color::White => CColor::White,
Color::Indexed(i) => CColor::AnsiValue(i),
Color::Rgb(r, g, b) => CColor::Rgb { r, g, b },
}
}
}
bitflags! {
/// Modifier changes the way a piece of text is displayed.
///
/// They are bitflags so they can easily be composed.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::Modifier;
///
/// let m = Modifier::BOLD | Modifier::ITALIC;
/// ```
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Modifier: u16 {
const BOLD = 0b0000_0000_0001;
const DIM = 0b0000_0000_0010;
const ITALIC = 0b0000_0000_0100;
const UNDERLINED = 0b0000_0000_1000;
const SLOW_BLINK = 0b0000_0001_0000;
const RAPID_BLINK = 0b0000_0010_0000;
const REVERSED = 0b0000_0100_0000;
const HIDDEN = 0b0000_1000_0000;
const CROSSED_OUT = 0b0001_0000_0000;
}
}
/// Style let you control the main characteristics of the displayed elements.
///
/// ```rust
/// # use helix_view::graphics::{Color, Modifier, Style};
/// Style::default()
/// .fg(Color::Black)
/// .bg(Color::Green)
/// .add_modifier(Modifier::ITALIC | Modifier::BOLD);
/// ```
///
/// It represents an incremental change. If you apply the styles S1, S2, S3 to a cell of the
/// terminal buffer, the style of this cell will be the result of the merge of S1, S2 and S3, not
/// just S3.
///
/// ```rust
/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
/// # use helix_tui::buffer::Buffer;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::default().bg(Color::Red),
/// Style::default().fg(Color::Yellow).remove_modifier(Modifier::ITALIC),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// }
/// assert_eq!(
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Red),
/// add_modifier: Modifier::BOLD,
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// );
/// ```
///
/// The default implementation returns a `Style` that does not modify anything. If you wish to
/// reset all properties until that point use [`Style::reset`].
///
/// ```
/// # use helix_view::graphics::{Rect, Color, Modifier, Style};
/// # use helix_tui::buffer::Buffer;
/// let styles = [
/// Style::default().fg(Color::Blue).add_modifier(Modifier::BOLD | Modifier::ITALIC),
/// Style::reset().fg(Color::Yellow),
/// ];
/// let mut buffer = Buffer::empty(Rect::new(0, 0, 1, 1));
/// for style in &styles {
/// buffer.get_mut(0, 0).set_style(*style);
/// }
/// assert_eq!(
/// Style {
/// fg: Some(Color::Yellow),
/// bg: Some(Color::Reset),
/// add_modifier: Modifier::empty(),
/// sub_modifier: Modifier::empty(),
/// },
/// buffer.get(0, 0).style(),
/// );
/// ```
#[derive(Debug, Clone, Copy, PartialEq)]
#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
pub struct Style {
pub fg: Option<Color>,
pub bg: Option<Color>,
pub add_modifier: Modifier,
pub sub_modifier: Modifier,
}
impl Default for Style {
fn default() -> Style {
Style {
fg: None,
bg: None,
add_modifier: Modifier::empty(),
sub_modifier: Modifier::empty(),
}
}
}
impl Style {
/// Returns a `Style` resetting all properties.
pub fn reset() -> Style {
Style {
fg: Some(Color::Reset),
bg: Some(Color::Reset),
add_modifier: Modifier::empty(),
sub_modifier: Modifier::all(),
}
}
/// Changes the foreground color.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::{Color, Style};
/// let style = Style::default().fg(Color::Blue);
/// let diff = Style::default().fg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().fg(Color::Red));
/// ```
pub fn fg(mut self, color: Color) -> Style {
self.fg = Some(color);
self
}
/// Changes the background color.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::{Color, Style};
/// let style = Style::default().bg(Color::Blue);
/// let diff = Style::default().bg(Color::Red);
/// assert_eq!(style.patch(diff), Style::default().bg(Color::Red));
/// ```
pub fn bg(mut self, color: Color) -> Style {
self.bg = Some(color);
self
}
/// Changes the text emphasis.
///
/// When applied, it adds the given modifier to the `Style` modifiers.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD);
/// let diff = Style::default().add_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD | Modifier::ITALIC);
/// assert_eq!(patched.sub_modifier, Modifier::empty());
/// ```
pub fn add_modifier(mut self, modifier: Modifier) -> Style {
self.sub_modifier.remove(modifier);
self.add_modifier.insert(modifier);
self
}
/// Changes the text emphasis.
///
/// When applied, it removes the given modifier from the `Style` modifiers.
///
/// ## Examples
///
/// ```rust
/// # use helix_view::graphics::{Color, Modifier, Style};
/// let style = Style::default().add_modifier(Modifier::BOLD | Modifier::ITALIC);
/// let diff = Style::default().remove_modifier(Modifier::ITALIC);
/// let patched = style.patch(diff);
/// assert_eq!(patched.add_modifier, Modifier::BOLD);
/// assert_eq!(patched.sub_modifier, Modifier::ITALIC);
/// ```
pub fn remove_modifier(mut self, modifier: Modifier) -> Style {
self.add_modifier.remove(modifier);
self.sub_modifier.insert(modifier);
self
}
/// Results in a combined style that is equivalent to applying the two individual styles to
/// a style one after the other.
///
/// ## Examples
/// ```
/// # use helix_view::graphics::{Color, Modifier, Style};
/// let style_1 = Style::default().fg(Color::Yellow);
/// let style_2 = Style::default().bg(Color::Red);
/// let combined = style_1.patch(style_2);
/// assert_eq!(
/// Style::default().patch(style_1).patch(style_2),
/// Style::default().patch(combined));
/// ```
pub fn patch(mut self, other: Style) -> Style {
self.fg = other.fg.or(self.fg);
self.bg = other.bg.or(self.bg);
self.add_modifier.remove(other.sub_modifier);
self.add_modifier.insert(other.add_modifier);
self.sub_modifier.remove(other.add_modifier);
self.sub_modifier.insert(other.sub_modifier);
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_rect_size_truncation() {
for width in 256u16..300u16 {
for height in 256u16..300u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert!(rect.width < width || rect.height < height);
// The target dimensions are rounded down so the math will not be too precise
// but let's make sure the ratios don't diverge crazily.
assert!(
(f64::from(rect.width) / f64::from(rect.height)
- f64::from(width) / f64::from(height))
.abs()
< 1.0
)
}
}
// One dimension below 255, one above. Area above max u16.
let width = 900;
let height = 100;
let rect = Rect::new(0, 0, width, height);
assert_ne!(rect.width, 900);
assert_ne!(rect.height, 100);
assert!(rect.width < width || rect.height < height);
}
#[test]
fn test_rect_size_preservation() {
for width in 0..256u16 {
for height in 0..256u16 {
let rect = Rect::new(0, 0, width, height);
rect.area(); // Should not panic.
assert_eq!(rect.width, width);
assert_eq!(rect.height, height);
}
}
// One dimension below 255, one above. Area below max u16.
let rect = Rect::new(0, 0, 300, 100);
assert_eq!(rect.width, 300);
assert_eq!(rect.height, 100);
}
fn styles() -> Vec<Style> {
vec![
Style::default(),
Style::default().fg(Color::Yellow),
Style::default().bg(Color::Yellow),
Style::default().add_modifier(Modifier::BOLD),
Style::default().remove_modifier(Modifier::BOLD),
Style::default().add_modifier(Modifier::ITALIC),
Style::default().remove_modifier(Modifier::ITALIC),
Style::default().add_modifier(Modifier::ITALIC | Modifier::BOLD),
Style::default().remove_modifier(Modifier::ITALIC | Modifier::BOLD),
]
}
#[test]
fn combined_patch_gives_same_result_as_individual_patch() {
let styles = styles();
for &a in &styles {
for &b in &styles {
for &c in &styles {
for &d in &styles {
let combined = a.patch(b.patch(c.patch(d)));
assert_eq!(
Style::default().patch(a).patch(b).patch(c).patch(d),
Style::default().patch(combined)
);
}
}
}
}
}
}

231
helix-view/src/input.rs Normal file
View File

@@ -0,0 +1,231 @@
//! Input event handling, currently backed by crossterm.
use anyhow::{anyhow, Error};
use serde::de::{self, Deserialize, Deserializer};
use std::fmt;
use crate::keyboard::{KeyCode, KeyModifiers};
/// Represents a key event.
// We use a newtype here because we want to customize Deserialize and Display.
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
pub struct KeyEvent {
pub code: KeyCode,
pub modifiers: KeyModifiers,
}
impl fmt::Display for KeyEvent {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!(
"{}{}{}",
if self.modifiers.contains(KeyModifiers::SHIFT) {
"S-"
} else {
""
},
if self.modifiers.contains(KeyModifiers::ALT) {
"A-"
} else {
""
},
if self.modifiers.contains(KeyModifiers::CONTROL) {
"C-"
} else {
""
},
))?;
match self.code {
KeyCode::Backspace => f.write_str("backspace")?,
KeyCode::Enter => f.write_str("ret")?,
KeyCode::Left => f.write_str("left")?,
KeyCode::Right => f.write_str("right")?,
KeyCode::Up => f.write_str("up")?,
KeyCode::Down => f.write_str("down")?,
KeyCode::Home => f.write_str("home")?,
KeyCode::End => f.write_str("end")?,
KeyCode::PageUp => f.write_str("pageup")?,
KeyCode::PageDown => f.write_str("pagedown")?,
KeyCode::Tab => f.write_str("tab")?,
KeyCode::BackTab => f.write_str("backtab")?,
KeyCode::Delete => f.write_str("del")?,
KeyCode::Insert => f.write_str("ins")?,
KeyCode::Null => f.write_str("null")?,
KeyCode::Esc => f.write_str("esc")?,
KeyCode::Char('<') => f.write_str("lt")?,
KeyCode::Char('>') => f.write_str("gt")?,
KeyCode::Char('+') => f.write_str("plus")?,
KeyCode::Char('-') => f.write_str("minus")?,
KeyCode::Char(';') => f.write_str("semicolon")?,
KeyCode::Char('%') => f.write_str("percent")?,
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
};
Ok(())
}
}
impl std::str::FromStr for KeyEvent {
type Err = Error;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut tokens: Vec<_> = s.split('-').collect();
let code = match tokens.pop().ok_or_else(|| anyhow!("Missing key code"))? {
"backspace" => KeyCode::Backspace,
"space" => KeyCode::Char(' '),
"ret" => KeyCode::Enter,
"lt" => KeyCode::Char('<'),
"gt" => KeyCode::Char('>'),
"plus" => KeyCode::Char('+'),
"minus" => KeyCode::Char('-'),
"semicolon" => KeyCode::Char(';'),
"percent" => KeyCode::Char('%'),
"left" => KeyCode::Left,
"right" => KeyCode::Right,
"up" => KeyCode::Down,
"home" => KeyCode::Home,
"end" => KeyCode::End,
"pageup" => KeyCode::PageUp,
"pagedown" => KeyCode::PageDown,
"tab" => KeyCode::Tab,
"backtab" => KeyCode::BackTab,
"del" => KeyCode::Delete,
"ins" => KeyCode::Insert,
"null" => KeyCode::Null,
"esc" => KeyCode::Esc,
single if single.len() == 1 => KeyCode::Char(single.chars().next().unwrap()),
function if function.len() > 1 && function.starts_with('F') => {
let function: String = function.chars().skip(1).collect();
let function = str::parse::<u8>(&function)?;
(function > 0 && function < 13)
.then(|| KeyCode::F(function))
.ok_or_else(|| anyhow!("Invalid function key '{}'", function))?
}
invalid => return Err(anyhow!("Invalid key code '{}'", invalid)),
};
let mut modifiers = KeyModifiers::empty();
for token in tokens {
let flag = match token {
"S" => KeyModifiers::SHIFT,
"A" => KeyModifiers::ALT,
"C" => KeyModifiers::CONTROL,
_ => return Err(anyhow!("Invalid key modifier '{}-'", token)),
};
if modifiers.contains(flag) {
return Err(anyhow!("Repeated key modifier '{}-'", token));
}
modifiers.insert(flag);
}
Ok(KeyEvent { code, modifiers })
}
}
impl<'de> Deserialize<'de> for KeyEvent {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let s = String::deserialize(deserializer)?;
s.parse().map_err(de::Error::custom)
}
}
#[cfg(feature = "term")]
impl From<crossterm::event::KeyEvent> for KeyEvent {
fn from(
crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent,
) -> KeyEvent {
KeyEvent {
code: code.into(),
modifiers: modifiers.into(),
}
}
}
#[cfg(test)]
mod test {
use super::*;
#[test]
fn parsing_unmodified_keys() {
assert_eq!(
str::parse::<KeyEvent>("backspace").unwrap(),
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE
}
);
assert_eq!(
str::parse::<KeyEvent>("left").unwrap(),
KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE
}
);
assert_eq!(
str::parse::<KeyEvent>(",").unwrap(),
KeyEvent {
code: KeyCode::Char(','),
modifiers: KeyModifiers::NONE
}
);
assert_eq!(
str::parse::<KeyEvent>("w").unwrap(),
KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::NONE
}
);
assert_eq!(
str::parse::<KeyEvent>("F12").unwrap(),
KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::NONE
}
);
}
#[test]
fn parsing_modified_keys() {
assert_eq!(
str::parse::<KeyEvent>("S-minus").unwrap(),
KeyEvent {
code: KeyCode::Char('-'),
modifiers: KeyModifiers::SHIFT
}
);
assert_eq!(
str::parse::<KeyEvent>("C-A-S-F12").unwrap(),
KeyEvent {
code: KeyCode::F(12),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL | KeyModifiers::ALT
}
);
assert_eq!(
str::parse::<KeyEvent>("S-C-2").unwrap(),
KeyEvent {
code: KeyCode::Char('2'),
modifiers: KeyModifiers::SHIFT | KeyModifiers::CONTROL
}
);
}
#[test]
fn parsing_nonsensical_keys_fails() {
assert!(str::parse::<KeyEvent>("F13").is_err());
assert!(str::parse::<KeyEvent>("F0").is_err());
assert!(str::parse::<KeyEvent>("aaa").is_err());
assert!(str::parse::<KeyEvent>("S-S-a").is_err());
assert!(str::parse::<KeyEvent>("C-A-S-C-1").is_err());
assert!(str::parse::<KeyEvent>("FU").is_err());
assert!(str::parse::<KeyEvent>("123").is_err());
assert!(str::parse::<KeyEvent>("S--").is_err());
}
}

156
helix-view/src/keyboard.rs Normal file
View File

@@ -0,0 +1,156 @@
use bitflags::bitflags;
bitflags! {
/// Represents key modifiers (shift, control, alt).
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct KeyModifiers: u8 {
const SHIFT = 0b0000_0001;
const CONTROL = 0b0000_0010;
const ALT = 0b0000_0100;
const NONE = 0b0000_0000;
}
}
#[cfg(feature = "term")]
impl From<KeyModifiers> for crossterm::event::KeyModifiers {
fn from(key_modifiers: KeyModifiers) -> Self {
use crossterm::event::KeyModifiers as CKeyModifiers;
let mut result = CKeyModifiers::NONE;
if key_modifiers.contains(KeyModifiers::SHIFT) {
result.insert(CKeyModifiers::SHIFT);
}
if key_modifiers.contains(KeyModifiers::CONTROL) {
result.insert(CKeyModifiers::CONTROL);
}
if key_modifiers.contains(KeyModifiers::ALT) {
result.insert(CKeyModifiers::ALT);
}
result
}
}
#[cfg(feature = "term")]
impl From<crossterm::event::KeyModifiers> for KeyModifiers {
fn from(val: crossterm::event::KeyModifiers) -> Self {
use crossterm::event::KeyModifiers as CKeyModifiers;
let mut result = KeyModifiers::NONE;
if val.contains(CKeyModifiers::SHIFT) {
result.insert(KeyModifiers::SHIFT);
}
if val.contains(CKeyModifiers::CONTROL) {
result.insert(KeyModifiers::CONTROL);
}
if val.contains(CKeyModifiers::ALT) {
result.insert(KeyModifiers::ALT);
}
result
}
}
/// Represents a key.
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum KeyCode {
/// Backspace key.
Backspace,
/// Enter key.
Enter,
/// Left arrow key.
Left,
/// Right arrow key.
Right,
/// Up arrow key.
Up,
/// Down arrow key.
Down,
/// Home key.
Home,
/// End key.
End,
/// Page up key.
PageUp,
/// Page dow key.
PageDown,
/// Tab key.
Tab,
/// Shift + Tab key.
BackTab,
/// Delete key.
Delete,
/// Insert key.
Insert,
/// F key.
///
/// `KeyCode::F(1)` represents F1 key, etc.
F(u8),
/// A character.
///
/// `KeyCode::Char('c')` represents `c` character, etc.
Char(char),
/// Null.
Null,
/// Escape key.
Esc,
}
#[cfg(feature = "term")]
impl From<KeyCode> for crossterm::event::KeyCode {
fn from(key_code: KeyCode) -> Self {
use crossterm::event::KeyCode as CKeyCode;
match key_code {
KeyCode::Backspace => CKeyCode::Backspace,
KeyCode::Enter => CKeyCode::Enter,
KeyCode::Left => CKeyCode::Left,
KeyCode::Right => CKeyCode::Right,
KeyCode::Up => CKeyCode::Up,
KeyCode::Down => CKeyCode::Down,
KeyCode::Home => CKeyCode::Home,
KeyCode::End => CKeyCode::End,
KeyCode::PageUp => CKeyCode::PageUp,
KeyCode::PageDown => CKeyCode::PageDown,
KeyCode::Tab => CKeyCode::Tab,
KeyCode::BackTab => CKeyCode::BackTab,
KeyCode::Delete => CKeyCode::Delete,
KeyCode::Insert => CKeyCode::Insert,
KeyCode::F(f_number) => CKeyCode::F(f_number),
KeyCode::Char(character) => CKeyCode::Char(character),
KeyCode::Null => CKeyCode::Null,
KeyCode::Esc => CKeyCode::Esc,
}
}
}
#[cfg(feature = "term")]
impl From<crossterm::event::KeyCode> for KeyCode {
fn from(val: crossterm::event::KeyCode) -> Self {
use crossterm::event::KeyCode as CKeyCode;
match val {
CKeyCode::Backspace => KeyCode::Backspace,
CKeyCode::Enter => KeyCode::Enter,
CKeyCode::Left => KeyCode::Left,
CKeyCode::Right => KeyCode::Right,
CKeyCode::Up => KeyCode::Up,
CKeyCode::Down => KeyCode::Down,
CKeyCode::Home => KeyCode::Home,
CKeyCode::End => KeyCode::End,
CKeyCode::PageUp => KeyCode::PageUp,
CKeyCode::PageDown => KeyCode::PageDown,
CKeyCode::Tab => KeyCode::Tab,
CKeyCode::BackTab => KeyCode::BackTab,
CKeyCode::Delete => KeyCode::Delete,
CKeyCode::Insert => KeyCode::Insert,
CKeyCode::F(f_number) => KeyCode::F(f_number),
CKeyCode::Char(character) => KeyCode::Char(character),
CKeyCode::Null => KeyCode::Null,
CKeyCode::Esc => KeyCode::Esc,
}
}
}

View File

@@ -1,14 +1,24 @@
#[macro_use]
pub mod macros;
pub mod clipboard;
pub mod document;
pub mod editor;
pub mod graphics;
pub mod input;
pub mod keyboard;
pub mod register_selection;
pub mod theme;
pub mod tree;
pub mod view;
use slotmap::new_key_type;
new_key_type! { pub struct DocumentId; }
new_key_type! { pub struct ViewId; }
slotmap::new_key_type! {
pub struct DocumentId;
pub struct ViewId;
}
pub use document::Document;
pub use editor::Editor;
pub use register_selection::RegisterSelection;
pub use theme::Theme;
pub use view::View;

29
helix-view/src/macros.rs Normal file
View File

@@ -0,0 +1,29 @@
#[macro_export]
macro_rules! current {
( $( $editor:ident ).+ ) => {{
let view = $crate::view_mut!( $( $editor ).+ );
let doc = &mut $( $editor ).+ .documents[view.doc];
(view, doc)
}};
}
#[macro_export]
macro_rules! doc_mut {
( $( $editor:ident ).+ ) => {{
$crate::current!( $( $editor ).+ ).1
}};
}
#[macro_export]
macro_rules! view_mut {
( $( $editor:ident ).+ ) => {{
$( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus)
}};
}
#[macro_export]
macro_rules! view {
( $( $editor:ident ).+ ) => {{
$( $editor ).+ .tree.get($( $editor ).+ .tree.focus)
}};
}

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