Compare commits

...

545 Commits

Author SHA1 Message Date
Blaž Hrastnik
1caedc18ca Release v0.4.1 2021-08-14 13:32:29 +09:00
Blaž Hrastnik
dbd1f11311 fix: Cross compile tests as well
We ran the tests first, but did not cross compile them. This step would
also compile all the grammar libraries (but for the host machine). On
the actual release build, the editor would get built for the target, but
the grammar libraries would be detected as present and wouldn't
recompile.

Refs #577
2021-08-14 13:32:29 +09:00
Kirawi
b0acd8c3a6 Update README.md (#581)
* Update README.md

* Update README.md
2021-08-14 13:28:27 +09:00
Ivan Tham
52a848e3c8 Bump chardetng to 0.1.14 (#578) 2021-08-14 13:28:08 +09:00
Blaž Hrastnik
4167201344 ui: picker: Position count according to input bar 2021-08-13 18:00:04 +09:00
Blaž Hrastnik
eb9ac0a743 ui: picker: Use ui.selection instead of ui.selection.primary 2021-08-13 17:59:47 +09:00
Blaž Hrastnik
f20dc1283d ui: picker: Render matches/total counts 2021-08-13 17:56:37 +09:00
Blaž Hrastnik
b635e35818 Appease clippy 2021-08-13 13:16:31 +09:00
Blaž Hrastnik
fd1eaafff5 Add :tree-sitter-scopes, useful when developing indents.toml 2021-08-13 13:15:53 +09:00
Blaž Hrastnik
88cc0f85a5 Clear some TODOs 2021-08-13 13:15:53 +09:00
Blaž Hrastnik
7c834d6506 fix: tree sitter rendering glitches with multiple selection edits 2021-08-13 13:15:53 +09:00
Omnikar
9a39a10ddd Tutorial for Helix akin to vimtutor (#537)
* Create `docs/tutor.txt`

* Create `EXITING HELIX` and `DELETION` sections

* Create Insert mode, saving, and recap sections

* Create `MOTIONS AND SELECTIONS` section

* Add additional notes to `SAVING A FILE` section

* Remove extra blank lines in `SAVING A FILE` section

* Move `tutor.txt` to `runtime/`

* Add WIP message to end of tutorial
2021-08-13 10:13:17 +09:00
Norman Paniagua
8364ceca81 Reverted unintended change? (#576) 2021-08-13 10:12:29 +09:00
Blaž Hrastnik
3de40de0a9 fix build... 2021-08-13 01:28:11 +09:00
Blaž Hrastnik
733ee06b7b Release v0.4.0 2021-08-13 01:24:04 +09:00
Gokul Soumya
d84f8b5fde Show file preview in split pane in fuzzy finder (#534)
* Add preview pane for fuzzy finder

* Fix picker preview lag by caching

* Add picker preview for document symbols

* Cache picker preview per document instead of view

* Use line instead of range for preview doc

* Add picker preview for buffer picker

* Fix render bug and refactor picker

* Refactor picker preview rendering

* Split picker and preview and compose

The current selected item is cloned on every event, which is
undesirable

* Refactor out clones in previewed picker

* Retrieve doc from editor if possible in filepicker

* Disable syntax highlight for picker preview

Files already loaded in memory have syntax highlighting enabled

* Ignore directory symlinks in file picker

* Cleanup unnecessary pubs and derives

* Remove unnecessary highlight from file picker

* Reorganize buffer rendering

* Use normal picker for code actions

* Remove unnecessary generics and trait impls

* Remove prepare_for_render and make render mutable

* Skip picker preview if screen small, less padding
2021-08-12 16:00:42 +09:00
Dmitry Sharshakov
7d51805e94 Support primary clipboard (#548)
* clipboard-none: add in-memory fallback buffer

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* view: add Wayland primary clipboard

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Format

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: copy to primary selection after mouse move stops

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: don't update primary selection if it is a single character

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: discard result of setting primary selection

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: add commands for interaction with primary clipboard

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* editor: implement primary selection copy/paste using commands

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: support xsel for primary selection

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: support xclip for primary selection

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: multiple cursor support for middle click paste

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* rename primary selection to primary clipboard in scope of PR

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: make middle click paste optional

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Format

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Update helix-term/src/ui/editor.rs

* fix formatting

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* config: correct defaults if terminal prop is not set

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* refactor: merge clipboard and primary selection implementations

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Tidy up code

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* view: remove names for different clipboard/selection providers

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Update helix-view/src/clipboard.rs

Co-authored-by: Gokul Soumya <gokulps15@gmail.com>

* helix-view: tidy macros

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: refactor paste-replace commands

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: use new config for middle-click-paste

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: remove memory fallback for command and windows providers

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard-win: fix build

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: return empty string when primary clipboard is missing

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* clipboard: fix errors in Windows build

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
2021-08-12 11:53:48 +09:00
Blaž Hrastnik
d03982ee43 cargo fmt 2021-08-12 10:18:37 +09:00
Gokul Soumya
25a8a475c5 Refactor theme parsing (#570) 2021-08-12 10:00:19 +09:00
Blaž Hrastnik
6d52424303 fix: Adjust scroll offset/padding calculation to prevent wobble
Fixes #324
2021-08-11 13:53:38 +09:00
Blaž Hrastnik
55f1f04717 Highlight (html) tags 2021-08-11 13:23:57 +09:00
Blaž Hrastnik
7b16ae7c4a Bump deps 2021-08-11 13:23:48 +09:00
Blaž Hrastnik
627b899315 ui: completion: Insert suggestions when tabbing over them
Fixes #498
2021-08-11 10:56:32 +09:00
Blaž Hrastnik
6cd77ef380 nix: Update flake 2021-08-11 10:56:32 +09:00
Blaž Hrastnik
f917b5a441 ui: completion: Use sort_text to sort the completions 2021-08-11 10:56:32 +09:00
Nathan Vegdahl
dde2be9395 Fix surround_replace replacing the wrong character on the right. (#571)
Fixes #569.
2021-08-11 09:17:59 +09:00
Dmitry Sharshakov
27b551d345 helix-term: handle scrolling when mouse is enabled (#554)
* helix-term: handle scrolling when mouse is enabled

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: configure scrolling speed

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: use new config for scrolling

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* config: defaults for edtior config

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* config: add scroll-lines property

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: scroll hovered view

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: support inverted scrolling

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: remove duplicating code

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: do not focus view while scrolled

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* helix-term: refactor mouse events and scrolling

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* simplify

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2021-08-10 14:35:20 +09:00
Kirawi
b239f0f45f add java highlighting (#448) 2021-08-10 14:09:57 +09:00
Blaž Hrastnik
86209c93a3 Appease clippy 2021-08-10 10:58:46 +09:00
Omnikar
21e5662125 Make exit_select_mode check current mode (#568)
Change `exit_select_mode` to check that the current mode is select mode
before switching to normal mode
2021-08-10 10:57:07 +09:00
Blaž Hrastnik
a4564adadd fix: Don't crash if language servers time out 2021-08-10 10:52:53 +09:00
Blaž Hrastnik
0a7add4ad4 Only recalculate resize during rendering, this stops flashing on resize 2021-08-10 10:52:52 +09:00
Kirawi
815ee9e334 fix small terminal size panic with info popup (#563)
* fix small terminal size panic with info popup

* remove unused enumerator

* fix subtraction overflow panic
2021-08-09 15:46:58 +09:00
Nathan Vegdahl
b5223618ed Document pos_at_coords better.
Particularly the effect of the `limit_before_line_ending`
parameter.
2021-08-09 11:12:38 +09:00
Blaž Hrastnik
a2ccfffda1 config: Rename [terminal] to [editor] and pass it into Editor 2021-08-08 14:10:01 +09:00
Blaž Hrastnik
f0eb6ed96a Resolve a couple TODOs 2021-08-08 14:08:54 +09:00
Blaž Hrastnik
dbd853a082 Document new keys in book/ 2021-08-08 13:31:09 +09:00
Blaž Hrastnik
e2c3547f26 Improve nix indents 2021-08-08 13:26:30 +09:00
Blaž Hrastnik
02cba2a7f4 Implement alt-( and alt-) to rotate selection contents 2021-08-08 13:26:13 +09:00
Blaž Hrastnik
ba729349b8 languages: Add missing comment token for elixir and nix 2021-08-07 15:04:37 +09:00
Blaž Hrastnik
385a6b5a1a lsp: Refactor duplex to avoid issues with select! + read_exact
read_exact isn't cancellation safe.

Fixes #504
2021-08-07 15:04:03 +09:00
Luctius
8714b71991 Do not shutdown lsp during claim_term
Fixes a bug where the language server is told to shutdown directly after application start.
2021-08-07 10:41:41 +09:00
Blaž Hrastnik
b20a5c4c0e ui: menu: Allow wrapping around on ctrl-p/shift tab 2021-08-06 11:22:23 +09:00
Blaž Hrastnik
66a90130a5 Implement selection rotation with ( and ) 2021-08-06 11:22:01 +09:00
Nathan Vegdahl
953125d3f3 Fix around-word text-object selection. (#546)
* Fix around-word text-object selection.

* Text object around-word: select to the left if no whitespace on the right.

Also only select around when there's whitespace at all.

* Make select-word-around select all white space on a side.

* Update commented-out test case.

* Fix unused import warning from rebase.
2021-08-06 09:32:33 +09:00
Ivan Tham
10c77cdc03 Exit extend after yank
Yank should proceed with normal mode.
2021-08-05 17:25:23 +09:00
Blaž Hrastnik
5342f976d4 Document C/Alt-C in the keymap 2021-08-05 17:04:26 +09:00
Blaž Hrastnik
0793841ac3 Refactor copy selection vertically 2021-08-05 17:04:26 +09:00
Cor
f160008add Vertical Selection 2021-08-05 17:04:26 +09:00
Nathan Vegdahl
c9cbc344fc Fix buggy surround behavior from #376.
Fixes #543.
2021-08-04 09:55:59 +08:00
Nathan Vegdahl
8c3a5b14de Add goto_last_line command, and bind it to ge.
Resolves #529.
2021-08-04 09:47:22 +09:00
Blaž Hrastnik
585793eb46 Use an empty stream on Windows to remove duplication 2021-08-03 09:32:21 +09:00
Ivan Tham
821565e4ef Add ctrl-z to suspend 2021-08-03 09:32:21 +09:00
Blaž Hrastnik
adb5d842ba Use nicer filepaths instead of URIs in goto picker 2021-08-03 09:30:51 +09:00
Blaž Hrastnik
b3aefe18cd nix: Update flake 2021-08-03 09:30:51 +09:00
dependabot[bot]
cfef44e3d2 Bump which from 4.1.0 to 4.2.2
Bumps [which](https://github.com/harryfei/which-rs) from 4.1.0 to 4.2.2.
- [Release notes](https://github.com/harryfei/which-rs/releases)
- [Commits](https://github.com/harryfei/which-rs/compare/4.1.0...4.2.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-03 09:20:42 +09:00
dependabot[bot]
c9b4b48344 Bump serde_json from 1.0.64 to 1.0.66
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.64 to 1.0.66.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.64...v1.0.66)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-03 09:20:16 +09:00
dependabot[bot]
99a4e3fc79 Bump serde from 1.0.126 to 1.0.127
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.126 to 1.0.127.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.126...v1.0.127)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-03 09:20:02 +09:00
Omnikar
bfcc6e2ca5 Update README.md 2021-08-02 17:55:32 +09:00
Ryo Hirayama
ef6a2317b7 Update keymap.md 2021-08-01 09:47:24 -07:00
Blaž Hrastnik
557c63033c fix: Map all selections on transaction.apply 2021-08-01 00:44:14 +09:00
Nathan Vegdahl
ccecda4f66 Add more unit tests for pos_at_screen_coords.
Specifically, test cases for double-width characters and grapheme
clusters.
2021-07-30 19:01:11 -07:00
Gokul Soumya
eec5631140 Update popup and diagnostics in onedark theme 2021-07-30 18:17:48 +09:00
Blaž Hrastnik
62eb8c6b40 mouse: Remove verify_screen_coords, refactor primary selection modification 2021-07-30 16:52:14 +09:00
Blaž Hrastnik
6bb744aeac Remove the jump 2021-07-30 16:52:10 +09:00
Dmitry Sharshakov
8361de45dc Mouse selection support (#509)
* Initial mouse selection support

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Disable mouse event capture if editor crashes

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Translate screen coordinates to view position

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Select full lines by dragging on line numbers

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* editor: don't register dragging as a jump

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Count graphemes correctly

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Do not select lines when dragging on the line number bar

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Split out verify_screen_coords

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Do not iterate over the graphemes twice

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Switch view by clicking on it

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Add disable-mouse config option

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Support multiple selections with mouse

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Remove unnecessary check

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Refactor using match expression

Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Rename local variable

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Rename mouse option

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Refactor code

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Fix dragging selection

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Fix crash when clicking past last line

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Count characters better

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Remove comparison not needed anymore

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Validate coordinates before resolving position

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Tidy up references to editor tree

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Better way to determine line end and avoid overflow

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Fix for last line

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

* Add unit tests for text_pos_at_screen_coords

Signed-off-by: Dmitry Sharshakov <d3dx12.xx@gmail.com>

Co-authored-by: Gokul Soumya <gokulps15@gmail.com>
2021-07-30 16:52:00 +09:00
Blaž Hrastnik
0fdb626c2c Remove embed_runtime feature
It's no longer practical to maintain. Closes #451
2021-07-30 16:27:22 +09:00
Nathan Vegdahl
f88d4c1e20 Move indent-style code into helix_core::indent. 2021-07-30 12:22:59 +09:00
Nathan Vegdahl
e191a75e33 Give default document a single line ending. 2021-07-30 12:20:29 +09:00
Nathan Vegdahl
e6e0d31be0 Fix incorrect behavior of find_char command and friends.
The non-extending variants of the commands weren't selecting from the range head.

Fixes #527.
2021-07-30 09:39:18 +08:00
Nathan Vegdahl
3fda350494 Fixes for new clippy lints in Rust 1.54. 2021-07-29 22:47:18 +02:00
Blaž Hrastnik
05d20e196f Merge pull request #376 from cessen/great_line_ending_and_cursor_range_cleanup
The Great Line Ending & Cursor Range Cleanup
2021-07-29 18:43:20 +09:00
Nathan Vegdahl
e4d41d06e3 Fix typo in comment. 2021-07-28 19:20:23 -07:00
Gokul Soumya
8a2fa692f2 Refactor case where key event is solely a character 2021-07-29 08:39:58 +08:00
Nathan Vegdahl
285aba2de5 Fix bug with / searching after non-ascii characters.
Forgot to convert from char indices to byte indices before passing
to the regex engine.
2021-07-28 16:03:34 -07:00
Nathan Vegdahl
cd7302ffd3 Enforce cursor/selection invariants in one place.
Rather than per-command like before.
2021-07-28 15:57:00 -07:00
Nathan Vegdahl
a873e719d5 Merge branch 'master' into great_line_ending_and_cursor_range_cleanup 2021-07-28 14:11:08 -07:00
Gokul Soumya
b90450b9e8 Fix goto line number
Regression from #454. Go to line 10 with `10gg` or `10G`.
2021-07-28 21:33:18 +08:00
Ivan Tham
013bec407c Quite edit page
Stolen from https://github.com/rust-lang/wg-async-foundations/pull/225
2021-07-28 20:35:47 +09:00
Gokul Soumya
1493313750 Show pending keys in status line (#515)
* Show pending keys and counts in status line

* Refactor pending key display
2021-07-28 13:57:07 +09:00
Rust & Python
581a3d42c8 Update keyboard.rs (#516)
Fix doc comment typo
2021-07-27 16:32:05 +09:00
Nathan Vegdahl
aead4e69a6 Minor cleanup of the vertical movement code. 2021-07-26 23:20:58 -07:00
Nathan Vegdahl
84f8167fd1 Use match for branching on the Direction enum in more places. 2021-07-26 23:09:58 -07:00
Nathan Vegdahl
5229c5387f Add unit tests for some of the new Range methods. 2021-07-26 20:03:12 -07:00
dependabot[bot]
86645c897d Bump futures-executor from 0.3.15 to 0.3.16
Bumps [futures-executor](https://github.com/rust-lang/futures-rs) from 0.3.15 to 0.3.16.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.15...0.3.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-27 09:37:52 +08:00
dependabot[bot]
a6aad3122d Bump tokio from 1.8.2 to 1.9.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.8.2 to 1.9.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.8.2...tokio-1.9.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-27 09:28:34 +08:00
dependabot[bot]
b581c185ba Bump jsonrpc-core from 17.1.0 to 18.0.0
Bumps [jsonrpc-core](https://github.com/paritytech/jsonrpc) from 17.1.0 to 18.0.0.
- [Release notes](https://github.com/paritytech/jsonrpc/releases)
- [Commits](https://github.com/paritytech/jsonrpc/compare/jsonrpc-core-17.1.0...v18.0.0)

---
updated-dependencies:
- dependency-name: jsonrpc-core
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-27 09:26:37 +08:00
dependabot[bot]
13c9d57b2b Bump futures-util from 0.3.15 to 0.3.16
Bumps [futures-util](https://github.com/rust-lang/futures-rs) from 0.3.15 to 0.3.16.
- [Release notes](https://github.com/rust-lang/futures-rs/releases)
- [Changelog](https://github.com/rust-lang/futures-rs/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rust-lang/futures-rs/compare/0.3.15...0.3.16)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-27 09:24:51 +08:00
Nathan Vegdahl
b2c76dc627 Improve Range documentation and organization. 2021-07-26 18:04:03 -07:00
Nathan Vegdahl
f62ec6e51e Merge branch 'master' into great_line_ending_and_cursor_range_cleanup 2021-07-26 11:19:10 -07:00
Nathan Vegdahl
5ee6ba5b38 Address some PR comments. 2021-07-26 10:51:00 -07:00
Gokul Soumya
88d6f65239 Allow multi key remappings in config file (#454)
* Use tree like structure to store keymaps

* Allow multi key keymaps in config file

* Allow multi key keymaps in insert mode

* Make keymap state self contained

* Add keymap! macro for ergonomic declaration

* Add descriptions for editor commands

* Allow keymap! to take multiple keys

* Restore infobox display

* Fix keymap merging and add infobox titles

* Fix and add tests for keymaps

* Clean up comments and apply suggestions

* Allow trailing commas in keymap!

* Remove mode suffixes from keymaps

* Preserve order of keys when showing infobox

* Make command descriptions smaller

* Strip infobox title prefix from items

* Strip infobox title prefix from items
2021-07-27 01:07:13 +09:00
Nathan Vegdahl
01247acf0c Start searches at the right side of the block cursor. 2021-07-26 08:50:26 -07:00
Nathan Vegdahl
0883b4fae0 Collect some common patterns into methods on Range. 2021-07-26 08:40:30 -07:00
gbaranski
a630fb5d20 fix: change primary cursor color in bogster theme 2021-07-26 23:10:24 +09:00
Ivan Tham
f7c8500797 Fix append newline indent
Fix #492
2021-07-26 22:36:40 +09:00
Blaž Hrastnik
63e54e30a7 Implement in-memory prompt history
Implementation is similar to kakoune: we store the entries into
a register.
2021-07-26 11:19:33 +09:00
Blaž Hrastnik
29cefa1be8 rust: Indent multi line call expressions one level deeper 2021-07-26 11:19:33 +09:00
Blaž Hrastnik
f24007b30f Improve rust indentation queries
if/if let are already handled by block, and keeping these scopes would
indent else blocks one level too far.
2021-07-26 11:19:33 +09:00
Omnikar
112ae5cffe Determine whether to use a margin of 0 or 1 when uncommenting (#476)
* Implement `margin` calculation for uncommenting

* Move `margin` calculation to `find_line_comment`

* Fix comment bug with multiple selections on a line

* Fix `find_line_comment` test for new return type

* Generate a single vec of lines for comment toggle

`toggle_line_comments` collects the lines covered by all selections into
a `Vec`, skipping duplicates. `find_line_comment` now returns the lines
to operate on, instead of returning the lines to skip.

* Fix test for `find_line_comment`

* Reserve length of `to_change` instead of `lines`

The length of `lines` includes blank lines which will be skipped, and as
such do not need space for a change reserved for them. `to_change`
includes only the lines which will be changed.

* Use `token.chars().count()` for token char length

* Create `changes` with capacity instead of reserving

* Remove unnecessary clones in `test_find_line_comment`

* Add test case for 0 margin comments

* Add comments explaining `find_line_comment`
2021-07-26 11:00:58 +09:00
Gokul Soumya
e07e42dcfb fix(term): undo-ing code actions 2021-07-25 19:49:05 +09:00
gbaranski
8da58fe44a fix(term): use existing implementation of edits_to_transaction 2021-07-25 19:49:05 +09:00
Yusuf Bera Ertan
41f62c3157 build(nix): fix build issues 2021-07-25 19:47:56 +09:00
Nathan Vegdahl
f96b8b769b Switch to a cleaner range-head moving abstraction.
Also fix a bunch of bugs related to it.
2021-07-24 07:44:11 -07:00
Gokul Soumya
6a8a01df6b Add missing keybinds to docs 2021-07-24 20:11:19 +09:00
Grzegorz Baranski
48e344a2a8 feat: code actions - document edits (#478)
* wip: Code actions

* fix(term): use current macro instead Context::context

* feat(lsp): set code_action capabilities

* feat(term): set SPC-a to code_action

* feat(term): wip on applying code actions

* deps: `cargo update`

* feat(term): applying code actions edits

* fix(term): cleanup of apply_edit

* fix(term): applying edits as a whole thing instead one by one

* refactor(term): move apply_edits below

* fix(term): improve unimplemented messages for further investigation

* fix(term): change code action command comment

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

* fix(term): add matching `}`

* fix(term): cleanup, todo!() on workspace edit

* fix(term): remove unrelated workspace_symbol_picker

* fix(term): apply cargo-clippy suggestions

* fix(term): replace todo!'s with editor.set_error

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-07-24 10:26:43 +09:00
Nathan Vegdahl
20723495d3 Fixed find_till_char and find_char commands.
They worked correctly when extending, but not for normal cursor
movement.
2021-07-23 18:03:40 -07:00
Nathan Vegdahl
8f43dc4039 Fix surround replace command replacing the wrong position on the right. 2021-07-23 17:52:45 -07:00
Nathan Vegdahl
43594049dd Merge branch 'master' into great_line_ending_and_cursor_range_cleanup 2021-07-23 17:23:16 -07:00
Nathan Vegdahl
427ae6ac6c Put selection in separate variable in commands code. 2021-07-23 17:06:14 -07:00
Nathan Vegdahl
ad814b8c2e Fix append mode, and make insertion always happen at head of range. 2021-07-23 14:27:12 -07:00
Ivan Tham
bda4f5c1cd Simplify replace dashes with underscore 2021-07-23 18:37:41 +09:00
Blaž Hrastnik
1789dfabfe fix: ui/menu: Don't allow scrolling past the end of completion
Fixes #472
2021-07-23 18:12:33 +09:00
Blaž Hrastnik
e5d438705b Add rustfmt.toml to force formatting to use rustfmt defaults
Closes #480
2021-07-23 18:11:22 +09:00
Blaž Hrastnik
817a7e0bd6 fix: Only try expanding directory completion if it makes sense
Fixes #487
2021-07-23 18:10:30 +09:00
Blaž Hrastnik
58d08d36ae Simplify ui/menu.rs 2021-07-23 18:10:17 +09:00
Nathan Vegdahl
ffb8057a7f Fix ocassional panic when matching brackets. 2021-07-22 18:47:37 -07:00
Shafkath Shuhan
25103833b2 mark reloaded buffers as unchanged 2021-07-22 17:39:45 -07:00
Nathan Vegdahl
fd684ef693 Revert display-width-based vertical cursor movement.
Still needs to be done, but should be part of a separate PR.
2021-07-22 13:21:44 -07:00
Nathan Vegdahl
5841954f58 Calculate the line that the range head is on correctly. 2021-07-22 11:17:03 -07:00
Nathan Vegdahl
673338bdb6 Use Range::line_range() in some more places I missed. 2021-07-22 10:50:12 -07:00
fossdd
d4bd5b3766 The item fmt was imported redundantly
Fixed warning:

```
warning: the item `fmt` is imported redundantly
  --> helix-core/src/syntax.rs:98:9
   |
16 |     fmt,
   |     --- the item `fmt` is already imported here
...
98 |     use std::fmt;
   |         ^^^^^^^^
   |
```
2021-07-22 22:39:17 +09:00
Ivan Tham
eba0bbda2e Resume last picker
Inspired by space ' in doom emacs.
2021-07-22 11:22:27 +09:00
Nathan Vegdahl
7d07704e6f Fix append mode not editing correctly.
This is currently a bit of a hack, and still doesn't behave quite how we
probably want.  Left a TODO.
2021-07-21 09:56:21 -07:00
Nathan Vegdahl
063aa9452d Fix yank not working with internally zero-width ranges. 2021-07-21 09:32:48 -07:00
Nathan Vegdahl
bc85c85501 Fix selections not being modified quite correctly with text edits. 2021-07-21 09:23:01 -07:00
Kirawi
df0ed80931 Update dark_plus.toml
Corrects primary selection color and makes matching cursor easier to spot.
2021-07-21 11:52:42 +09:00
Nathan Vegdahl
198fe40951 Don't insert a final line ending on file load/reload. 2021-07-20 18:40:41 -07:00
Nathan Vegdahl
c848ed7abc Fixes for misc bugs with view movement. 2021-07-20 18:15:34 -07:00
Nathan Vegdahl
d5534a6d10 Merge branch 'master' into great_line_ending_and_cursor_range_cleanup 2021-07-20 13:35:37 -07:00
Nathan Vegdahl
1194fc842a Use new Range::line_range() method in more places, as appropriate. 2021-07-20 12:40:58 -07:00
Nathan Vegdahl
c9300ec35f Fix comment toggle command also sometimes toggling the next line. 2021-07-20 12:23:40 -07:00
Nathan Vegdahl
1c6b5581f0 Fix various bugs related to goto-end-of-line command.
This also fixes a bug with `Selection::normalize()`, that could
result in an out-of-bounds primary index.
2021-07-20 11:58:56 -07:00
Nathan Vegdahl
e8a3980e46 Fix line-wise p pasting before the current line instead of after. 2021-07-20 10:56:27 -07:00
Nathan Vegdahl
1910fa7723 Fix incorrect line hihglight when a selection is at the end of a line. 2021-07-20 10:26:00 -07:00
Ivan Tham
17f9dfce7e Fix typo 2021-07-20 22:10:43 +09:00
Luctius
585d6f8242 Fixes toggle_comment not finding the correct language comment token 2021-07-20 17:10:17 +09:00
dependabot[bot]
d754c72b4d Bump tokio from 1.8.1 to 1.8.2
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.8.1 to 1.8.2.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.8.1...tokio-1.8.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-20 13:31:14 +08:00
Nathan Vegdahl
1792dc6f93 Make search work a little nicer when there are already selections.
Specifically, if you have text like "aaaaaaaaa" and you search
for "a", the new behavior will actually progress through all of the
"a"s, whereas the previous behavior would be stuck on a single one.
2021-07-19 18:29:26 -07:00
Nathan Vegdahl
c400a60377 Fix Selection::push() to make the pushed range primary.
Apparently I accidentally deleted that behavior in the cleanup.
2021-07-19 18:25:36 -07:00
Nathan Vegdahl
13b0784009 Fix extend line behavior. 2021-07-19 17:44:18 -07:00
Nathan Vegdahl
e98d669c30 Handle edge case in range_to_target() correctly. 2021-07-19 12:30:08 -07:00
Nathan Vegdahl
b0311f4fc2 Fixed primary cursor position calculation to use 1-width semantics.
This had a bunch of knock-on effects that were buggy, such as bracket
match highlighting.
2021-07-19 09:25:10 -07:00
Nathan Vegdahl
079d4ed86d Properly fix last_line view calculation.
Turned out to be simpler than I thought.  Didn't even need to change the
other use-sites.
2021-07-19 08:39:48 -07:00
Nathan Vegdahl
1a9ae72fcb Fix last line number being drawn in the status bar. 2021-07-18 23:09:55 -07:00
Nathan Vegdahl
e462f32723 Merge branch 'master' into great_line_ending_and_cursor_range_cleanup 2021-07-18 22:02:12 -07:00
Nathan Vegdahl
6c038bb015 Update word selection/navigation to work with gap indexing.
Also tweaked some of the existing behavior that seemed inconsistent
and/or buggy.  It's mostly identical, just a few corner cases are
different.
2021-07-18 21:59:31 -07:00
Blaž Hrastnik
5292fe0f7d Calculate completion popup sizing
Fixes #220
2021-07-19 11:29:51 +09:00
Blaž Hrastnik
bf43fabf65 Remove ExactSizeIterator requirement on Transaction::change
Size hint is enough.
2021-07-19 11:29:51 +09:00
Cor Peters
cd65a48635 Made toggle_comments language dependent (#463)
* Made toggle_comments language dependent

* Fixed Test Cases

* Added clippy suggestion

* Small Fixes

* Clippy Suggestion

Co-authored-by: Cor <prive@corpeters.nl>
2021-07-19 01:33:38 +09:00
Cor Peters
0aa43902ca Added option to provide a custom config file to the lsp. (#460)
* Added option to provide a custom config file to the lsp.

* Simplified lsp loading routine with anyhow

* Moved config to language.toml

* Fixed test case

* Cargo fmt

* Revert now-useless changes

* Renamed custom_config to config

Co-authored-by: Cor <prive@corpeters.nl>
2021-07-18 16:56:25 +09:00
Nathan Vegdahl
c2fd55e168 Update extend_line command to work with gap indexing. 2021-07-17 11:28:20 -07:00
Nathan Vegdahl
954314a7c9 Update change-case commands to work with gap indexing. 2021-07-17 11:03:39 -07:00
Nathan Vegdahl
a77274e8bb Merge branch 'master' into great_line_ending_and_cursor_range_cleanup 2021-07-17 10:49:03 -07:00
kabirz
6cba62b499 action: copy grammar libraries to runtime 2021-07-18 01:06:44 +09:00
Cor
9fcbbfa467 Changed startup behaviour to only open a single view when multiple files are specified on the commandline.
Changed the behaviour; the first argument on the commandline is the file on display
2021-07-18 00:29:05 +09:00
Blaž Hrastnik
000b7b7c97 Make instructions regarding runtime clearer 2021-07-18 00:22:58 +09:00
Cor Peters
722cfedb38 Added change_case command (#441)
* Added change_case command

* Added switch_to_uppercase and switch_to_lowercase

Renamed change_case to switch_case.

* Updated the Keymap section of the Book

* Use flat_map instead of map + flatten

* Fix switch_to_uppercase using to_lowercase

* Switched 'Alt-`' to uppercase and '`' to lowercase

Co-authored-by: Cor <prive@corpeters.nl>
2021-07-17 01:12:59 +09:00
Cor
e2bcef718a Removed double entry of extend_line 2021-07-15 23:25:00 +09:00
Kirawi
0b1ed8656d Fix #442 (#446)
* fix #442

fix #442

fmt

* create Rope from default line ending

* Fix use of encoding in Document::open()
2021-07-15 11:22:34 +09:00
Kirawi
d84b3a198a Update dark_plus.toml
Didn't realize what `ui.statusline.active` was for. It's needed for view splits.
2021-07-15 09:49:05 +09:00
Blaž Hrastnik
a4b077e9b9 Build ts/tsx again, refactor collect_tree_sitter_dirs 2021-07-14 10:00:05 +09:00
Blaž Hrastnik
3ca05fce31 Fix number highlighting 2021-07-14 10:00:05 +09:00
Blaž Hrastnik
e6bf6a8f28 Build each grammar in it's own src dir
Windows places temporary files in the current dir, so compiling in
parallel caused conflicts.
2021-07-14 10:00:05 +09:00
Blaž Hrastnik
a7fa5621ce Try to rearrange the file order? 2021-07-14 10:00:05 +09:00
Blaž Hrastnik
c8dc9b64dd windows: Try building inside OUT_DIR? 2021-07-14 10:00:05 +09:00
Blaž Hrastnik
dd2903ff10 Dynamically load grammar libraries at runtime 2021-07-14 10:00:05 +09:00
dependabot[bot]
dd5e8082e4 Bump anyhow from 1.0.41 to 1.0.42
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.41 to 1.0.42.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.41...1.0.42)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-13 09:59:13 +09:00
dependabot[bot]
7e5c20cc58 Bump cc from 1.0.68 to 1.0.69
Bumps [cc](https://github.com/alexcrichton/cc-rs) from 1.0.68 to 1.0.69.
- [Release notes](https://github.com/alexcrichton/cc-rs/releases)
- [Commits](https://github.com/alexcrichton/cc-rs/compare/1.0.68...1.0.69)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-13 09:59:05 +09:00
dependabot[bot]
c74198a3bf Bump tokio-stream from 0.1.6 to 0.1.7
Bumps [tokio-stream](https://github.com/tokio-rs/tokio) from 0.1.6 to 0.1.7.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-stream-0.1.6...tokio-stream-0.1.7)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-13 09:58:54 +09:00
dependabot[bot]
929f553f89 Bump tokio from 1.8.0 to 1.8.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.8.0 to 1.8.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.8.0...tokio-1.8.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-13 09:58:15 +09:00
Blaž Hrastnik
4a5cb0e04b Restore C-w shortcut 2021-07-11 16:42:23 +09:00
Blaž Hrastnik
d530d6e39d Further simplify error handling in :commands 2021-07-11 16:36:36 +09:00
Lionel Flandrin
9c02a1b070 Make command implementation return a Result<()>
The error message is displayed with cx.editor.set_error.
2021-07-11 16:36:20 +09:00
Ivan Tham
3e4cd8f8e6 Add infobox for view 2021-07-11 11:12:04 +09:00
Kirawi
bb121a3e4b Injection Query Support (#430)
* wip

* wip

* fixed unsafe

* fix clippy

* move out reference variable

* fmt

* remove arc

* change safety comment
2021-07-11 10:40:18 +09:00
Nathan Vegdahl
b4c59b444c Update surround commands to work with gap indexing. 2021-07-08 16:47:20 -07:00
Kirawi
084a8a9522 Rewritten Rust highlights.scm (#425)
* rewrote Rust highlights.scm

* wip

* wip

* wip

* wip

* fixed type highlighting

* wip

* rewrite again

* moved operators

* missing newline

* missing newline

* update book

* fix constructor highlighting

* fix constructor highlighting

* fix const highlighting

* better constructor highlighting

* remove dup, bug was my locals.scm file

* fixed docs

* merge

* fixed for highlighting

* add yield

* remove yield

* added yield back

* fixed yield highlighting

* unecessary
2021-07-09 01:11:20 +09:00
Kirawi
c7aa7bf4ba VSCode Dark+ Theme (#414)
* wip

* Add VSCode Dark+ Theme

wip

wip

wip

wip

wip

wip

properly detect constants

add bool

wip

* suggestion

* add variant for c/c++

* fix hexcode error

* removed regex highlight

* fixed constant higlighting

* wip

* add space

* add suggestions

* update theme

* update book

* suggestions

* fix c/c++ enum

* update book
2021-07-08 09:51:46 +09:00
Ivan Tham
1c71fced0e Add more modes to infobox 2021-07-08 09:37:18 +09:00
Nathan Vegdahl
753f7f381b Implement Range::put() which manages range movements and extensions.
In particular, this wraps the annoying logic involved in keeping the
cursor width to 1 grapheme.
2021-07-07 17:24:39 -07:00
Nathan Vegdahl
85d5b399de Merge branch 'master' into great_line_ending_and_cursor_range_cleanup 2021-07-05 20:27:49 -07:00
Nathan Vegdahl
6e15c9b874 Make vertical selection movement work properly. 2021-07-05 18:58:33 -07:00
Blaž Hrastnik
906cfd52e0 Clean up the default theme definition 2021-07-06 10:54:10 +09:00
Ivan Tham
a0a5bd555b More responsive key input
Use biased select!, don't eagerly process lsp message since we want to
prioritize user input rather than lsp messages, but still limit rendering
for lsp messages.
2021-07-06 10:07:01 +09:00
dependabot[bot]
47a6882738 Bump tokio from 1.7.1 to 1.8.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.7.1 to 1.8.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.7.1...tokio-1.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-06 09:52:47 +09:00
dependabot[bot]
c8681a820c Bump unicode-segmentation from 1.7.1 to 1.8.0
Bumps [unicode-segmentation](https://github.com/unicode-rs/unicode-segmentation) from 1.7.1 to 1.8.0.
- [Release notes](https://github.com/unicode-rs/unicode-segmentation/releases)
- [Commits](https://github.com/unicode-rs/unicode-segmentation/compare/1.7.1...v1.8.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-06 09:52:33 +09:00
dependabot[bot]
bb41a82a85 Bump slotmap from 1.0.3 to 1.0.5
Bumps [slotmap](https://github.com/orlp/slotmap) from 1.0.3 to 1.0.5.
- [Release notes](https://github.com/orlp/slotmap/releases)
- [Changelog](https://github.com/orlp/slotmap/blob/master/RELEASES.md)
- [Commits](https://github.com/orlp/slotmap/compare/v1.0.3...v1.0.5)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-06 09:52:26 +09:00
dependabot[bot]
9507c24f68 Bump thiserror from 1.0.25 to 1.0.26
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.25 to 1.0.26.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.25...1.0.26)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-07-06 09:52:17 +09:00
wesh
3c31f50116 julia language-server line was plain wrong 2021-07-05 20:28:49 +08:00
wesh
aa70362d20 Add julia support (LSP not working) 2021-07-05 20:28:49 +08:00
Nathan Vegdahl
4952d6f801 Fix phantom lines in some CRLF files.
Fixes #415.  The issue was that cursor highlighting wasn't extending
to encompass the entire CRLF grapheme, and therefore ended up splitting
it.  This presumably was messing up other grapheme rendering as
well, and this fixes that as well.
2021-07-05 20:07:06 +08:00
Blaž Hrastnik
fc34efea12 appease clippy 2021-07-05 10:34:48 +09:00
Blaž Hrastnik
48481db8ca fix: Make path absolute before normalizing
:open ../file.txt failed before because .. would be stripped
2021-07-05 10:26:51 +09:00
Blaž Hrastnik
b72c6204e5 fix: When calculating relative path, expand tilde last 2021-07-05 10:17:26 +09:00
Blaž Hrastnik
cb4bab8903 Remove outdated comment 2021-07-05 10:12:46 +09:00
Blaž Hrastnik
a4e28c6927 Implement X as extend selection to line bounds 2021-07-05 10:12:34 +09:00
Ivan Tham
d02bbb7bae Fix info panic on small terminal 2021-07-05 00:19:56 +09:00
Blaž Hrastnik
ebccc96cd4 Factor out goto t/m/b into a single function again 2021-07-04 18:07:58 +09:00
Blaž Hrastnik
6ce303977c Revert back to 'gm'
top / middle / bottom mnemonic.
2021-07-04 18:02:42 +09:00
Ivan Tham
916362d3a9 Info box add horizontal padding 2021-07-04 18:01:59 +09:00
Ivan Tham
bbbbfa9bcf Goto mode use infobox
In the meantime, change gm to gc.
Remove extra space in mode title.
2021-07-04 18:01:59 +09:00
Ivan Tham
5977b07e19 Reduce calculation and improve pattern in infobox
- switch to use static OnceCell to calculate Info once
- pass Vec<(&[KeyEvent], &str)> rather than Vec<(Vec<KeyEvent>, &str)>
- expr -> tt to allow using | as separator, make it more like match
2021-07-04 18:01:59 +09:00
Ivan Tham
64f83dfcbd Support infobox doc gen on stable release 2021-07-04 18:01:59 +09:00
Ivan Tham
61e925cbed Add infobox doc generation and improve ergonomics 2021-07-04 18:01:59 +09:00
Ivan Tham
6710855eac Fix rendering issues for infobox 2021-07-04 18:01:59 +09:00
Ivan Tham
9effe71b7d Apply suggestions from blaz for infobox 2021-07-04 18:01:59 +09:00
Ivan Tham
4c190ec9d9 Suggestions for infobox changes
Co-authored-by: Benoît Cortier <benoit.cortier@fried-world.eu>
2021-07-04 18:01:59 +09:00
Ivan Tham
8985c58fd3 Add infobox 2021-07-04 18:01:59 +09:00
Ivan Tham
6ccfa229ed Fix typo on comment in surround 2021-07-03 20:20:24 +08:00
Gokul Soumya
351c1e7e55 Fix surround bug when cursor on same pair
For example when the cursor is _on_ the `'` in `'word'`, the cursor
wouldn't move because the search for a matching pair started _from_ the
position of the cursor and simply found itself.
2021-07-03 20:20:24 +08:00
Gokul Soumya
37f0b9ee15 Add missing linenr.selected key to docs 2021-07-03 16:44:01 +09:00
Gokul Soumya
f909526ebd Update onedark theme
Add colors for matching brace, non primary selections, inactive
statusline
2021-07-03 16:43:41 +09:00
Blaž Hrastnik
83e7dd8602 fix: Temporary fix for #402 2021-07-03 12:30:13 +09:00
Gokul Soumya
c68fe1f2a3 Add object selection (textobjects) (#385)
* Add textobjects for word

* Add textobjects for surround characters

* Apply clippy lints

* Remove ThisWordPrevBound in favor of PrevWordEnd

It's the same as PrevWordEnd except for taking the current char
into account, so use a "flag" to capture that usecase

* Add tests for PrevWordEnd movement

* Remove ThisWord* movements

They did not preserve anchor positions and were only used
for textobject boundary search anyway so replace them with
simple position finding functions

* Rewrite tests of word textobject

* Add tests for surround textobject

* Add textobject docs

* Refactor textobject word position functions

* Apply clippy lints on textobject

* Fix overflow error with textobjects
2021-07-03 10:07:49 +09:00
Nathan Vegdahl
28d2d68804 Make horizontal selection movement work properly. 2021-07-02 09:51:29 -07:00
Kirawi
c5b2973739 :reload (#374)
* reloading functionality

* fn with_newline_eof()

* fmt

* wip

* wip

* wip

* wip

* moved to core, added simd feature for encoding_rs

* wip

* rm

* .gitignore

* wip

* local wip

* wip

* wip

* no features

* wip

* nit

* remove simd

* doc

* clippy

* clippy

* address comments

* add indentation & line ending change
2021-07-02 23:54:50 +09:00
Nathan Vegdahl
28627f97e9 Fix empty document test. 2021-07-02 00:06:53 -07:00
Nathan Vegdahl
7961a13007 Make new documents empty, rather than starting with a line ending. 2021-07-01 23:39:49 -07:00
Nathan Vegdahl
22dca3b111 Allow last line in file to lack a line break character. 2021-07-01 23:36:09 -07:00
Perry Thompson
e177b27baf Add missing import 2021-07-02 12:10:15 +09:00
Nathan Vegdahl
230248bbc3 Fix a couple additional unused warnings after merge. 2021-07-01 19:40:37 -07:00
Nathan Vegdahl
2224a1527e Merge branch 'master' into great_line_ending_and_cursor_range_cleanup 2021-07-01 19:37:28 -07:00
Nathan Vegdahl
9f62ad0715 Fixed last unused warning. 2021-07-01 19:06:52 -07:00
Nathan Vegdahl
c389f41f14 Fix one of the two remaining warnings.
One of them was a lot more obvious than I thought.
2021-07-01 19:06:52 -07:00
Nathan Vegdahl
220bc85821 Fix all remaining warnings in helix-core except for two.
I'm not sure how to address them, because they look like they
might be bugs, and code is involved.  Will poke the relevant people.
2021-07-01 19:06:52 -07:00
Nathan Vegdahl
b571f28641 Remove #[allow(unused)] from helix-core, and fix unused imports.
Still a bunch more warnings to fix in core, but it's a start.
2021-07-01 19:06:52 -07:00
Nathan Vegdahl
0b2d51cf5a Fix unused Result warnings in helix-term. 2021-07-01 19:06:52 -07:00
Nathan Vegdahl
efa3389b6a Fix unused variable, parameter, and mut warnings in helix-term. 2021-07-01 19:06:52 -07:00
Nathan Vegdahl
702a0491db Remove #[allow(unused)] from helix-term, and fix unused imports.
Lots of other warning still left.  Will address in subsequent commits.
2021-07-01 19:06:52 -07:00
Nathan Vegdahl
e725957704 Ensure a minimum selection width on commands that need it. 2021-07-01 14:22:28 -07:00
Nathan Vegdahl
7c7be6d583 Make Selection's normalize and transform methods self-consuming only. 2021-07-01 14:22:28 -07:00
Nathan Vegdahl
0ae522f3df Clean up Selection to not use so many allocations. 2021-07-01 14:22:28 -07:00
Nathan Vegdahl
77a266e818 Better validation method APIs for Range.
This way they do less work, are more specific to what we actually
need, and they compose.
2021-07-01 14:22:28 -07:00
Nathan Vegdahl
d07074740b Add Range methods for various kinds of validation. 2021-07-01 14:22:28 -07:00
Nathan Vegdahl
c1b0a71975 Change the Range type and associated functions to gap indexing. 2021-07-01 14:22:28 -07:00
Jakub Bartodziej
79f096963c Color palettes (#393)
* Enable using color palettes in theme files.

* Add an example theme defined using a gruvbox color palette.

* Fix clippy error.

* Small style improvement.

* Add documentation for the features to themes.md.

* Update runtime/themes/gruvbox.toml

Fix the value of purple0.

Co-authored-by: DrZingo <DrZingo@users.noreply.github.com>

Co-authored-by: DrZingo <DrZingo@users.noreply.github.com>
2021-06-30 23:24:30 +09:00
Blaž Hrastnik
2a92dd8d4d If completion arrives after we already stopped editing, ignore it 2021-06-30 17:49:42 +09:00
Kirawi
acaf22d005 Added native Windows clipboard support (#373)
* Added native Windows clipboard support

* make conditional

wip

better conditional

wip

wip

wip

wip

make conditional
2021-06-30 17:11:56 +09:00
Joe Neeman
b39e452d77 Make set_unmodified an enum. 2021-06-30 17:08:50 +09:00
Joe Neeman
2902a10a3e Make Document's format API a little nicer. 2021-06-30 17:08:50 +09:00
Joe Neeman
ffa2f2590b Satisfy clippy. 2021-06-30 17:08:50 +09:00
Joe Neeman
d64d75e724 Add some async job infrastructure. 2021-06-30 17:08:50 +09:00
Joe Neeman
c9be480bf8 Make formatting happen asynchronously. 2021-06-30 17:08:50 +09:00
Blaž Hrastnik
3007478567 fix: Correctly merge multiple selection ranges together
Fixes #391
2021-06-30 16:08:41 +09:00
Blaž Hrastnik
e9159887a9 ui: Use a box drawing character vertical line for splits 2021-06-30 01:01:28 +09:00
PabloMansanet
c2a292ecf3 Update keymap.md 2021-06-30 00:56:19 +09:00
PabloMansanet
de8745aea7 Incorporate long word commands into keymap 2021-06-30 00:56:19 +09:00
PabloMansanet
73572b7780 Add long word unit tests 2021-06-30 00:56:19 +09:00
PabloMansanet
073517a92f Add long word motion implementation 2021-06-30 00:56:19 +09:00
Kirawi
e81d665e18 Delete .gitattributes 2021-06-29 21:07:12 +09:00
Kirawi
5928d87837 Update .gitattributes to #372 2021-06-29 21:07:12 +09:00
Blaž Hrastnik
51162ae6b2 fix ca98210d20 2021-06-28 18:05:20 +09:00
Blaž Hrastnik
6214d707f3 fix: Don't panic on Enter on an empty document.
Refs #386
2021-06-28 17:52:57 +09:00
Blaž Hrastnik
ca98210d20 fix: insert() | delete() would calculate the new insert incorrectly
Refs #386
2021-06-28 17:49:34 +09:00
Blaž Hrastnik
d4e1ec339e Don't crash if diagnostics span past EOF 2021-06-28 14:50:35 +09:00
Blaž Hrastnik
d94410a678 Sort the files in descending order 2021-06-28 13:08:38 +09:00
Blaž Hrastnik
82fc28a0ce ui: Simplify conditional 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
bcca152ad5 Merge tab & char rendering code 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
01b1a62e2c This char_index is unused 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
4edfac21f6 Allocate the tab stop only once 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
1b102d5532 Extract the merge "operator" into helix-core 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
ae58d813b2 Appease clippy 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
c832aa5a49 There is no direct dirs-next dependency in term 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
f9cdb2afe2 Turn diagnostics rendering into span injection too 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
90d675fb15 Fix AnyComponent test 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
cac6e1b282 nix: Set up cargo-tarpaulin 2021-06-27 23:28:22 +09:00
Blaž Hrastnik
057bd630d8 Simplify selection rendering by injecting highlight scopes 2021-06-27 23:28:22 +09:00
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
157 changed files with 382667 additions and 5200 deletions

5
.envrc
View File

@@ -1,2 +1,5 @@
watch_file shell.nix 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

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

@@ -67,8 +67,9 @@ jobs:
- name: Run cargo test - name: Run cargo test
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
use-cross: ${{ matrix.cross }}
command: test command: test
args: --locked args: --release --locked --target ${{ matrix.target }}
- name: Build release binary - name: Build release binary
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -99,8 +100,9 @@ jobs:
else else
cp "target/${{ matrix.target }}/release/hx" "dist/" cp "target/${{ matrix.target }}/release/hx" "dist/"
fi fi
cp -r runtime dist
- uses: actions/upload-artifact@v2.2.3 - uses: actions/upload-artifact@v2.2.4
with: with:
name: bins-${{ matrix.build }} name: bins-${{ matrix.build }}
path: dist path: dist
@@ -148,7 +150,7 @@ jobs:
pkgname=helix-$TAG-$platform pkgname=helix-$TAG-$platform
mkdir tmp/$pkgname mkdir tmp/$pkgname
cp LICENSE README.md tmp/$pkgname cp LICENSE README.md tmp/$pkgname
cp -r runtime tmp/$pkgname/ mv bins-$platform/runtime tmp/$pkgname/
mv bins-$platform/hx$exe tmp/$pkgname mv bins-$platform/hx$exe tmp/$pkgname
chmod +x tmp/$pkgname/hx$exe chmod +x tmp/$pkgname/hx$exe

2
.gitignore vendored
View File

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

8
.gitmodules vendored
View File

@@ -86,3 +86,11 @@
path = helix-syntax/languages/tree-sitter-elixir path = helix-syntax/languages/tree-sitter-elixir
url = https://github.com/IceDragon200/tree-sitter-elixir url = https://github.com/IceDragon200/tree-sitter-elixir
shallow = true 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

131
CHANGELOG.md Normal file
View File

@@ -0,0 +1,131 @@
# 0.4.1 (2021-08-14)
A minor release that includes:
- A fix for rendering glitches that would occur after editing with multiple selections.
- CI fix for grammars not being cross-compiled for aarch64
# 0.4.1 (2021-08-13)
Two months have passed, so this is another big release. A big thank you to all
the contributors and package maintainers!
Helix has popped up in [Arch, Manjaro, Nix, MacPorts and Parabola and Termux repositories](https://repology.org/project/helix/versions)!
A [large scale refactor](https://github.com/helix-editor/helix/pull/376) landed that allows us to support zero width (empty)
selections in the future as well as resolves many bugs and edge cases.
- Multi-key remapping! Key binds now support much more complex usecases ([#454](https://github.com/helix-editor/helix/pull/454))
- Pending keys are shown in the statusline ([#515](https://github.com/helix-editor/helix/pull/515))
- Object selection / textobjects. `mi(` to select text inside parentheses ([#385](https://github.com/helix-editor/helix/pull/385))
- Autoinfo: `whichkey`-like popups which show available sub-mode shortcuts ([#316](https://github.com/helix-editor/helix/pull/316))
- Added WORD movements (W/B/E) ([#390](https://github.com/helix-editor/helix/pull/390))
- Vertical selections (repeat selection above/below) ([#462](https://github.com/helix-editor/helix/pull/462))
- Selection rotation via `(` and `)` ([66a90130](https://github.com/helix-editor/helix/commit/66a90130a5f99d769e9f6034025297f78ecaa3ec))
- Selection contents rotation via `Alt-(` and `Alt-)` ([02cba2a](https://github.com/helix-editor/helix/commit/02cba2a7f403f48eccb18100fb751f7b42373dba))
- Completion behavior improvements ([f917b5a4](https://github.com/helix-editor/helix/commit/f917b5a441ff3ae582358b6939ffbf889f4aa530), [627b899](https://github.com/helix-editor/helix/commit/627b89931576f7af86166ae8d5cbc55537877473))
- Fixed a language server crash ([385a6b5a](https://github.com/helix-editor/helix/commit/385a6b5a1adddfc26e917982641530e1a7c7aa81))
- Case change commands (`\``, `~`, `<a-~>`) ([#441](https://github.com/helix-editor/helix/pull/441))
- File pickers (including goto) now provide a preview! ([#534](https://github.com/helix-editor/helix/pull/534))
- Injection query support. Rust macro calls and embedded languages are now properly highlighted ([#430](https://github.com/helix-editor/helix/pull/430))
- Formatting is now asynchronous, and the async job infrastructure has been improved ([#285](https://github.com/helix-editor/helix/pull/285))
- Grammars are now compiled as separate shared libraries and loaded on-demand at runtime ([#432](https://github.com/helix-editor/helix/pull/432))
- Code action support ([#478](https://github.com/helix-editor/helix/pull/478))
- Mouse support ([#509](https://github.com/helix-editor/helix/pull/509), [#548](https://github.com/helix-editor/helix/pull/548))
- Native Windows clipboard support ([#373](https://github.com/helix-editor/helix/pull/373))
- Themes can now use color palettes ([#393](https://github.com/helix-editor/helix/pull/393))
- `:reload` command ([#374](https://github.com/helix-editor/helix/pull/374))
- Ctrl-z to suspend ([#464](https://github.com/helix-editor/helix/pull/464))
- Language servers can now be configured with a custom JSON config ([#460](https://github.com/helix-editor/helix/pull/460))
- Comment toggling now uses a language specific comment token ([#463](https://github.com/helix-editor/helix/pull/463))
- Julia support ([#413](https://github.com/helix-editor/helix/pull/413))
- Java support ([#448](https://github.com/helix-editor/helix/pull/448))
- Prompts have an (in-memory) history ([63e54e30](https://github.com/helix-editor/helix/commit/63e54e30a74bb0d1d782877ddbbcf95f2817d061))
# 0.3.0 (2021-06-27)
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

417
Cargo.lock generated
View File

@@ -13,9 +13,15 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.40" version = "1.0.42"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" checksum = "595d3cfa7a60d4555cb5067b99f07142a08ea778de5cf993f7b75c7d8fabc486"
[[package]]
name = "arc-swap"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e906254e445520903e7fc9da4f709886c84ae4bc4ddaf0e093188d66df4dc820"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@@ -25,9 +31,9 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "2da1976d75adbe5fbc88130ecd119529cf1cc6a93ae1546d8696ee66f0d21af1"
[[package]] [[package]]
name = "bstr" name = "bstr"
@@ -52,12 +58,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.68" version = "1.0.69"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" checksum = "e70cc2f62c6ce1868963827bd677764c62d07c3d9a3e1fb1177ee1a9ab199eb2"
dependencies = [
"jobserver",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@@ -65,6 +68,17 @@ version = "1.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd" checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]]
name = "chardetng"
version = "0.1.14"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36a5a2ca47925d19fb6835f53b3e70dec0d25659211c8ee5cc784f1fd6838f9c"
dependencies = [
"cfg-if",
"encoding_rs",
"memchr",
]
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.19" version = "0.4.19"
@@ -78,38 +92,48 @@ dependencies = [
] ]
[[package]] [[package]]
name = "crossbeam-utils" name = "clipboard-win"
version = "0.8.4" version = "4.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8"
dependencies = [
"error-code",
"str-buf",
"winapi",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [ dependencies = [
"autocfg",
"cfg-if", "cfg-if",
"lazy_static", "lazy_static",
] ]
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.19.0" version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossterm_winapi", "crossterm_winapi",
"futures-core", "futures-core",
"lazy_static",
"libc", "libc",
"mio", "mio",
"parking_lot", "parking_lot",
"signal-hook", "signal-hook",
"signal-hook-mio",
"winapi", "winapi",
] ]
[[package]] [[package]]
name = "crossterm_winapi" name = "crossterm_winapi"
version = "0.7.0" version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
@@ -135,6 +159,31 @@ dependencies = [
"winapi", "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",
]
[[package]]
name = "error-code"
version = "2.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b5115567ac25674e0043e472be13d14e537f37ea8aa4bdc4aef0c89add1db1ff"
dependencies = [
"libc",
"str-buf",
]
[[package]] [[package]]
name = "etcetera" name = "etcetera"
version = "0.3.2" version = "0.3.2"
@@ -183,15 +232,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.15" version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" checksum = "af51b1b4a7fdff033703db39de8802c673eb91855f2e0d47dcf3bf2c0ef01f99"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.15" version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" checksum = "4d0d535a57b87e1ae31437b892713aee90cd2d7b0ee48727cd11fc72ef54761c"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@@ -200,15 +249,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.15" version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" checksum = "bbe54a98670017f3be909561f6ad13e810d9a51f3f061b902062ca3da80799f2"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.15" version = "0.3.16"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" checksum = "67eb846bfd58e44a8481a00049e82c43e0ccb5d61f8dc071057cb19249dd4d78"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"futures-core", "futures-core",
@@ -238,17 +287,11 @@ dependencies = [
"wasi", "wasi",
] ]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]] [[package]]
name = "globset" name = "globset"
version = "0.4.6" version = "0.4.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a" checksum = "10463d9ff00a2a068db14231982f5132edebad0d7660cd956a1c30292dbcbfbd"
dependencies = [ dependencies = [
"aho-corasick", "aho-corasick",
"bstr", "bstr",
@@ -259,63 +302,62 @@ dependencies = [
[[package]] [[package]]
name = "helix-core" name = "helix-core"
version = "0.0.10" version = "0.4.1"
dependencies = [ dependencies = [
"arc-swap",
"etcetera", "etcetera",
"helix-syntax", "helix-syntax",
"once_cell", "once_cell",
"quickcheck",
"regex", "regex",
"ropey", "ropey",
"rust-embed",
"serde", "serde",
"similar",
"smallvec", "smallvec",
"tendril", "tendril",
"toml", "toml",
"tree-sitter", "tree-sitter",
"unicode-general-category",
"unicode-segmentation", "unicode-segmentation",
"unicode-width", "unicode-width",
] ]
[[package]] [[package]]
name = "helix-lsp" name = "helix-lsp"
version = "0.0.10" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-executor", "futures-executor",
"futures-util", "futures-util",
"glob",
"helix-core", "helix-core",
"jsonrpc-core", "jsonrpc-core",
"log", "log",
"lsp-types", "lsp-types",
"once_cell",
"pathdiff",
"serde", "serde",
"serde_json", "serde_json",
"thiserror", "thiserror",
"tokio", "tokio",
"tokio-stream", "tokio-stream",
"url",
] ]
[[package]] [[package]]
name = "helix-syntax" name = "helix-syntax"
version = "0.0.10" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow",
"cc", "cc",
"serde", "libloading",
"threadpool", "threadpool",
"tree-sitter", "tree-sitter",
] ]
[[package]] [[package]]
name = "helix-term" name = "helix-term"
version = "0.0.10" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"crossterm", "crossterm",
"dirs-next",
"fern", "fern",
"futures-util", "futures-util",
"fuzzy-matcher", "fuzzy-matcher",
@@ -330,28 +372,36 @@ dependencies = [
"pulldown-cmark", "pulldown-cmark",
"serde", "serde",
"serde_json", "serde_json",
"signal-hook",
"signal-hook-tokio",
"tokio", "tokio",
"toml", "toml",
] ]
[[package]] [[package]]
name = "helix-tui" name = "helix-tui"
version = "0.0.10" version = "0.4.1"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cassowary", "cassowary",
"crossterm", "crossterm",
"helix-core",
"helix-view",
"serde", "serde",
"unicode-segmentation", "unicode-segmentation",
"unicode-width",
] ]
[[package]] [[package]]
name = "helix-view" name = "helix-view"
version = "0.0.10" version = "0.4.1"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags",
"chardetng",
"clipboard-win",
"crossterm", "crossterm",
"encoding_rs",
"futures-util",
"helix-core", "helix-core",
"helix-lsp", "helix-lsp",
"helix-tui", "helix-tui",
@@ -362,13 +412,14 @@ dependencies = [
"tokio", "tokio",
"toml", "toml",
"url", "url",
"which",
] ]
[[package]] [[package]]
name = "hermit-abi" name = "hermit-abi"
version = "0.1.18" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "322f4de77956e22ed0e5032c359a0f1273f1f7f0d79bfa3b8ffbc730d7fbcc5c" checksum = "62b467343b94ba476dcb2500d242dadbb39557df889310ac77c5d99100aaac33"
dependencies = [ dependencies = [
"libc", "libc",
] ]
@@ -386,9 +437,9 @@ dependencies = [
[[package]] [[package]]
name = "ignore" name = "ignore"
version = "0.4.17" version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c" checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
dependencies = [ dependencies = [
"crossbeam-utils", "crossbeam-utils",
"globset", "globset",
@@ -404,9 +455,9 @@ dependencies = [
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.9" version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" checksum = "bee0328b1209d157ef001c94dd85b4f8f64139adb0eac2659f4b08382b2f474d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@@ -417,19 +468,11 @@ version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" checksum = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736"
[[package]]
name = "jobserver"
version = "0.1.22"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "972f5ae5d1cb9c6ae417789196c803205313edde988685da5e3aae0827b9e7fd"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "jsonrpc-core" name = "jsonrpc-core"
version = "17.1.0" version = "18.0.0"
source = "git+https://github.com/paritytech/jsonrpc#609d7a6cc160742d035510fa89fb424ccf077660" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "14f7f76aef2d054868398427f6c54943cf3d1caa9a7ec7d0c38d69df97a965eb"
dependencies = [ dependencies = [
"futures-util", "futures-util",
"log", "log",
@@ -446,9 +489,19 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.95" version = "0.2.99"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" checksum = "a7f823d141fe0a24df1e23b4af4e3c7ba9e5966ec514ea068c93024aa7deb765"
[[package]]
name = "libloading"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f84d96438c15fcd6c3f244c8fce01d1e2b9c6b5623e9c711dc9286d8fc92d6a"
dependencies = [
"cfg-if",
"winapi",
]
[[package]] [[package]]
name = "lock_api" name = "lock_api"
@@ -470,9 +523,9 @@ dependencies = [
[[package]] [[package]]
name = "lsp-types" name = "lsp-types"
version = "0.89.1" version = "0.89.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48b8a871b0a450bcec0e26d74a59583c8173cb9fb7d7f98889e18abb84838e0f" checksum = "852e0dedfd52cc32325598b2631e0eba31b7b708959676a9f837042f276b09a2"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"serde", "serde",
@@ -489,9 +542,9 @@ checksum = "c41e0c4fef86961ac6d6f8a82609f55f31b05e4fce149ac5710e439df7619ba4"
[[package]] [[package]]
name = "matches" name = "matches"
version = "0.1.8" version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7ffc5c5338469d4d3ea17d269fa8ea3512ad247247c30bd2df69e68309ed0a08" checksum = "a3e378b66a060d48947b590737b30a1be76706c8dd7b8ba0f2fe3989c68a853f"
[[package]] [[package]]
name = "memchr" name = "memchr"
@@ -501,9 +554,9 @@ checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc"
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.7.11" version = "0.7.13"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@@ -567,9 +620,9 @@ dependencies = [
[[package]] [[package]]
name = "once_cell" name = "once_cell"
version = "1.7.2" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3" checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]] [[package]]
name = "parking_lot" name = "parking_lot"
@@ -596,12 +649,6 @@ dependencies = [
"winapi", "winapi",
] ]
[[package]]
name = "pathdiff"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34"
[[package]] [[package]]
name = "percent-encoding" name = "percent-encoding"
version = "2.1.0" version = "2.1.0"
@@ -610,9 +657,9 @@ checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]] [[package]]
name = "pin-project-lite" name = "pin-project-lite"
version = "0.2.6" version = "0.2.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0e1f259c92177c30a4c9d177246edd0a3568b25756a977d0632cf8fa37e905" checksum = "8d31d11c69a6b52a174b42bdc0c30e5e11670f90788b2c471c31c1d17d449443"
[[package]] [[package]]
name = "pin-utils" name = "pin-utils"
@@ -622,9 +669,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.27" version = "1.0.28"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" checksum = "5c7ed8b8c7b886ea3ed7dde405212185f423ab44682667c8c6dd14aa1d9f6612"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@@ -640,6 +687,15 @@ dependencies = [
"unicase", "unicase",
] ]
[[package]]
name = "quickcheck"
version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"rand",
]
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.9"
@@ -650,10 +706,28 @@ dependencies = [
] ]
[[package]] [[package]]
name = "redox_syscall" name = "rand"
version = "0.2.8" version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "742739e41cd49414de871ea5e549afb7e2a3ac77b589bcbebe8c82fab37147fc" checksum = "2e7573632e6454cf6b99d7aac4ccca54be06da05aca2ef7423d22d27d4d4bcd8"
dependencies = [
"rand_core",
]
[[package]]
name = "rand_core"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d34f1408f55294453790c48b2f1ebbb1c5b4b7563eb1f418bcfcfdbb06ebb4e7"
dependencies = [
"getrandom",
]
[[package]]
name = "redox_syscall"
version = "0.2.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8383f39639269cde97d255a32bdb68c047337295414940c68bdd30c2e13203ff"
dependencies = [ dependencies = [
"bitflags", "bitflags",
] ]
@@ -687,46 +761,13 @@ checksum = "f497285884f3fcff424ffc933e56d7cbca511def0c9831a7f9b5f6153e3cc89b"
[[package]] [[package]]
name = "ropey" name = "ropey"
version = "1.2.0" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0f3ef16589fdbb3e8fbce3dca944c08e61f39c7f16064b21a257d68ea911a83" checksum = "9150aff6deb25b20ed110889f070a678bcd1033e46e5e9d6fb1abeab17947f28"
dependencies = [ dependencies = [
"smallvec", "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]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.5" version = "1.0.5"
@@ -750,18 +791,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.126" version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" checksum = "f03b9878abf6d14e6779d3f24f07b2cfa90352cfec4acc5aab8f1ac7f146fae8"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.126" version = "1.0.127"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" checksum = "a024926d3432516606328597e0f224a51355a493b49fdd67e9209187cbe55ecc"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -770,9 +811,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.64" version = "1.0.66"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" checksum = "336b10da19a12ad094b59d870ebde26a45402e5b470add4b5fd03c5048a32127"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@@ -792,35 +833,63 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.1.17" version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
dependencies = [ dependencies = [
"libc", "libc",
"mio",
"signal-hook-registry", "signal-hook-registry",
] ]
[[package]] [[package]]
name = "signal-hook-registry" name = "signal-hook-mio"
version = "1.3.0" version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" 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 = [ dependencies = [
"libc", "libc",
] ]
[[package]] [[package]]
name = "slab" name = "signal-hook-tokio"
version = "0.4.3" version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f173ac3d1a7e3b28003f40de0b5ce7fe2710f9b9dc3fc38664cebee46b3b6527" checksum = "f6c5d32165ff8b94e68e7b3bdecb1b082e958c22434b363482cfb89dcd6f3ff8"
dependencies = [
"futures-core",
"libc",
"signal-hook",
"tokio",
]
[[package]]
name = "similar"
version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1ad1d488a557b235fc46dae55512ffbfc429d2482b08b4d9435ab07384ca8aec"
[[package]]
name = "slab"
version = "0.4.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
[[package]] [[package]]
name = "slotmap" name = "slotmap"
version = "1.0.3" version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585cd5dffe4e9e06f6dfdf66708b70aca3f781bed561f4f667b2d9c0d4559e36" checksum = "a952280edbecfb1d4bd3cf2dbc309dc6ab523e53487c438ae21a6df09fe84bc4"
dependencies = [ dependencies = [
"version_check", "version_check",
] ]
@@ -832,10 +901,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e"
[[package]] [[package]]
name = "syn" name = "str-buf"
version = "1.0.72" version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
[[package]]
name = "syn"
version = "1.0.74"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1873d832550d4588c3dbc20f01361ab00bfe741048f71e3fecf145a7cc18b29c"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -855,18 +930,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.25" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" checksum = "93119e4feac1cbe6c798c34d3a53ea0026b0b1de6a120deef895137c0529bfe2"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.25" version = "1.0.26"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" checksum = "060d69a0afe7796bf42e9e2ff91f5ee691fb15c53d38b4b62a9a53eb23164745"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -893,9 +968,9 @@ dependencies = [
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.2.0" version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" checksum = "848a1e1181b9f6753b5e96a092749e29b11d19ede67dfbbd6c7dc7e0f49b5338"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]
@@ -908,9 +983,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.6.1" version = "1.10.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975" checksum = "01cf844b23c6131f624accf65ce0e4e9956a8bb329400ea5bcc26ae3a5c20b0b"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@@ -928,9 +1003,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.2.0" version = "1.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" checksum = "54473be61f4ebe4efd09cec9bd5d16fa51d70ea0192213d754d2d500457db110"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -939,9 +1014,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.6" version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066" checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
@@ -978,27 +1053,30 @@ dependencies = [
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.5" version = "0.3.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
dependencies = [
"matches", [[package]]
] name = "unicode-general-category"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
[[package]] [[package]]
name = "unicode-normalization" name = "unicode-normalization"
version = "0.1.17" version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef" checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [ dependencies = [
"tinyvec", "tinyvec",
] ]
[[package]] [[package]]
name = "unicode-segmentation" name = "unicode-segmentation"
version = "1.7.1" version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bb0d2e7be6ae3a5fa87eed5fb451aff96f2573d2694942e40543ae0bbe19c796" checksum = "8895849a949e7845e06bd6dc1aa51731a103c42707010a5b591c0038fb73385b"
[[package]] [[package]]
name = "unicode-width" name = "unicode-width"
@@ -1054,6 +1132,17 @@ version = "0.10.2+wasi-snapshot-preview1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6" checksum = "fd6fbd9a79829dd1ad0cc20627bf1ed606756a7f77edff7b66b7064f9cb327c6"
[[package]]
name = "which"
version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ea187a8ef279bc014ec368c27a920da2024d2a711109bfbe3440585d5cf27ad9"
dependencies = [
"either",
"lazy_static",
"libc",
]
[[package]] [[package]]
name = "winapi" name = "winapi"
version = "0.3.9" version = "0.3.9"

View File

@@ -13,7 +13,9 @@ myself agreeing with most of kakoune's design decisions.
For more information, see the [website](https://helix-editor.com) or For more information, see the [website](https://helix-editor.com) or
[documentation](https://docs.helix-editor.com/). [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) All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html).
[Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting)
# Features # Features
@@ -28,7 +30,7 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
# Installation # Installation
Note: Only certain languages have indentation definitions at the moment. Check Note: Only certain languages have indentation definitions at the moment. Check
`runtime/<lang>/` for `indents.toml`. `runtime/queries/<lang>/` for `indents.toml`.
We provide packaging for various distributions, but here's a quick method to We provide packaging for various distributions, but here's a quick method to
build from source. build from source.
@@ -41,26 +43,17 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`. This will install the `hx` binary to `$HOME/.cargo/bin`.
Now copy the `runtime/` directory somewhere. Helix will by default look for the Helix also needs it's runtime files so make sure to copy/symlink the `runtime/` directory into the
runtime inside the same folder as the executable, but that can be overriden via config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
the `HELIX_RUNTIME` environment variable. via the `HELIX_RUNTIME` environment variable.
> NOTE: You should set this to <path to repository>/runtime in development (if Packages already solve this for you by wrapping the `hx` binary with a wrapper
> running via cargo). that sets the variable to the install dir.
>
> `export HELIX_RUNTIME=$PWD/runtime`
If you want to embed the `runtime/` directory into the Helix binary you can build > NOTE: running via cargo also doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
it with: > detect the `runtime` directory in the project root.
``` [![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
cargo install --path helix-term --features "embed_runtime"
```
## Arch Linux
There are two packages available from AUR:
- `helix-bin`: contains prebuilt binary from GitHub releases
- `helix-git`: builds the master branch of this repository
## MacOS ## MacOS
Helix can be installed on MacOS through homebrew via: Helix can be installed on MacOS through homebrew via:
@@ -76,8 +69,11 @@ Contributors are very welcome! **No contribution is too small and all contributi
Some suggestions to get started: 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! - 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 - If your preferred language is missing, integrating a tree-sitter grammar for
it and defining syntax highlight queries for it is straight forward and it and defining syntax highlight queries for it is straight forward and
doesn't require much knowledge of the internals. doesn't require much knowledge of the internals.

26
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: - tree sitter:
- lua - lua
@@ -15,47 +10,30 @@ as you type completion!
- clojure - clojure
- erlang - erlang
as you type completion!
- [ ] use signature_help_provider and completion_provider trigger characters in - [ ] use signature_help_provider and completion_provider trigger characters in
a hook to trigger signature help text / autocompletion a hook to trigger signature help text / autocompletion
- [ ] document.on_type provider triggers - [ ] document.on_type provider triggers
- [ ] completion isIncomplete support - [ ] completion isIncomplete support
- [ ] scroll wheel support
- [ ] matching bracket highlight
1 1
- [ ] respect view fullscreen flag - [ ] respect view fullscreen flag
- [ ] Implement marks (superset of Selection/Range) - [ ] Implement marks (superset of Selection/Range)
- [ ] nixos packaging
- [ ] = for auto indent line/selection - [ ] = for auto indent line/selection
- [ ] :x for closing buffers - [ ] :x for closing buffers
- [ ] repeat selection - [ ] repeat selection
- [] jump to alt buffer
- [ ] lsp: signature help - [ ] 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 - [ ] search: smart case by default: insensitive unless upper detected
- [ ] move Compositor into tui
2 2
- [ ] surround bindings (select + surround ( wraps selection in parens )
- [ ] macro recording - [ ] macro recording
- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc ) - [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
- [x] bracket pairs
- [x] comment block (gcc)
- [ ] selection align - [ ] selection align
- [ ] store some state between restarts: file positions, prompt history - [ ] store some state between restarts: file positions, prompt history
- [ ] highlight matched characters in completion - [ ] highlight matched characters in picker
3 3
- [ ] diff mode with highlighting? - [ ] diff mode with highlighting?

View File

@@ -4,6 +4,7 @@ language = "en"
multilingual = false multilingual = false
src = "src" src = "src"
theme = "colibri" theme = "colibri"
edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
[output.html] [output.html]
cname = "docs.helix-editor.com" cname = "docs.helix-editor.com"

View File

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

View File

@@ -1,87 +1,11 @@
# Configuration # Configuration
## Theme To override global configuration parameters create a `config.toml` file located in your config directory (i.e `~/.config/helix/config.toml`).
Use a custom theme by placing a theme.toml in your config directory (i.e ~/.config/helix/theme.toml). 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/contrib/themes). ## LSP
Styles in theme.toml are specified of in the form:
To display all language server messages in the status line add the following to your `config.toml`:
```toml ```toml
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] } [lsp]
display-messages = true
``` ```
where `name` 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 | used for lifetimes |
| module | |
| ui.background | |
| ui.linenr | |
| ui.statusline | |
| ui.popup | |
| ui.window | |
| ui.help | |
| ui.text | |
| ui.text.focus | |
| ui.menu.selected | |
| 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

@@ -2,11 +2,16 @@
We provide pre-built binaries on the [GitHub Releases page](https://github.com/helix-editor/helix/releases). 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 ## OSX
TODO: brew tap A Homebrew tap is available:
Please use a pre-built binary release for the time being. ```
brew tap helix-editor/helix
brew install helix
```
## Linux ## Linux
@@ -32,13 +37,6 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`. This will install the `hx` binary to `$HOME/.cargo/bin`.
Now copy the `runtime/` directory somewhere. Helix will by default look for the Helix also needs it's runtime files so make sure to copy/symlink the `runtime/` directory into the
runtime inside the same folder as the executable, but that can be overriden via config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
the `HELIX_RUNTIME` environment variable. 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,75 +4,98 @@
### Movement ### Movement
> NOTE: `f`, `F`, `t` and `T` are not confined to the current line.
| Key | Description | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
| h, Left | move left | | `h`, `Left` | Move left |
| j, Down | move down | | `j`, `Down` | Move down |
| k, Up | move up | | `k`, `Up` | Move up |
| l, Right | move right | | `l`, `Right` | Move right |
| w | move next word start | | `w` | Move next word start |
| b | move previous word start | | `b` | Move previous word start |
| e | move next word end | | `e` | Move next word end |
| t | find 'till next char | | `W` | Move next WORD start |
| f | find next char | | `B` | Move previous WORD start |
| T | find 'till previous char | | `E` | Move next WORD end |
| F | find previous char | | `t` | Find 'till next char |
| Home | move to the start of the line | | `f` | Find next char |
| End | move to the end of the line | | `T` | Find 'till previous char |
| m | Jump to matching bracket | | `F` | Find previous char |
| PageUp | Move page up | | `Home` | Move to the start of the line |
| PageDown | Move page down | | `End` | Move to the end of the line |
| ctrl-u | Move half page up | | `PageUp` | Move page up |
| ctrl-d | Move half page down | | `PageDown` | Move page down |
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab | | `Ctrl-u` | Move half page up |
| ctrl-o | Jump backward on the jumplist | | `Ctrl-d` | Move half page down |
| v | Enter select (extend) mode | | `Ctrl-i` | Jump forward on the jumplist TODO: conflicts tab |
| g | Enter goto mode | | `Ctrl-o` | Jump backward on the jumplist |
| : | Enter command mode | | `v` | Enter [select (extend) mode](#select--extend-mode) |
| z | Enter view mode | | `g` | Enter [goto mode](#goto-mode) |
| ctrl-w | Enter window mode (maybe will be remove for spc w w later) | | `m` | Enter [match mode](#match-mode) |
| space | Enter space mode | | `:` | Enter command mode |
| K | Show documentation for the item under the cursor | | `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 ### Changes
| Key | Description | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
| r | replace (single character change) | | `r` | Replace with a character |
| i | Insert before selection | | `R` | Replace with yanked text |
| a | Insert after selection (append) | | `~` | Switch case of the selected text |
| I | Insert at the start of the line | | `` ` `` | Set the selected text to lower case |
| A | Insert at the end of the line | | `` Alt-` `` | Set the selected text to upper case |
| o | Open new line below selection | | `i` | Insert before selection |
| o | Open new line above selection | | `a` | Insert after selection (append) |
| u | Undo change | | `I` | Insert at the start of the line |
| U | Redo change | | `A` | Insert at the end of the line |
| y | Yank selection | | `o` | Open new line below selection |
| p | Paste after selection | | `O` | Open new line above selection |
| P | Paste before selection | | `u` | Undo change |
| > | Indent selection | | `U` | Redo change |
| < | Unindent selection | | `y` | Yank selection |
| = | Format selection | | `p` | Paste after selection |
| d | Delete selection | | `P` | Paste before selection |
| c | Change selection (delete and enter insert mode) | | `>` | Indent selection |
| `<` | Unindent selection |
| `=` | Format selection |
| `d` | Delete selection |
| `c` | Change selection (delete and enter insert mode) |
### Selection manipulation ### Selection manipulation
| Key | Description | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
| s | Select all regex matches inside selections | | `s` | Select all regex matches inside selections |
| S | Split selection into subselections on regex matches | | `S` | Split selection into subselections on regex matches |
| alt-s | Split selection on newlines | | `Alt-s` | Split selection on newlines |
| ; | Collapse selection onto a single cursor | | `;` | Collapse selection onto a single cursor |
| alt-; | Flip selection cursor and anchor | | `Alt-;` | Flip selection cursor and anchor |
| % | Select entire file | | `C` | Copy selection onto the next line |
| x | Select current line | | `Alt-C` | Copy selection onto the previous line |
| X | Extend to next line | | `(` | Rotate main selection forward |
| [ | Expand selection to parent syntax node TODO: pick a key | | `)` | Rotate main selection backward |
| J | join lines inside selection | | `Alt-(` | Rotate selection contents forward |
| K | keep selections matching the regex TODO: overlapped by hover help | | `Alt-)` | Rotate selection contents backward |
| space | keep only the primary selection TODO: overlapped by space mode | | `%` | Select entire file |
| ctrl-c | Comment/uncomment the selections | | `x` | Select current line, if already selected, extend to next line |
| `X` | Extend selection to line bounds (line-wise selection) |
| | 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 |
### Insert Mode
| Key | Description |
| ----- | ----------- |
| `Escape` | Switch to normal mode |
| `Ctrl-x` | Autocomplete |
| `Ctrl-w` | Delete previous word |
### Search ### Search
@@ -81,10 +104,10 @@ in reverse, or searching via smartcase.
| Key | Description | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
| / | Search for regex pattern | | `/` | Search for regex pattern |
| n | Select next search match | | `n` | Select next search match |
| N | Add next search match to selection | | `N` | Add next search match to selection |
| * | Use current selection as the search pattern | | `*` | Use current selection as the search pattern |
### Diagnostics ### Diagnostics
@@ -93,10 +116,10 @@ in reverse, or searching via smartcase.
| Key | Description | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
| [d | Go to previous diagnostic | | `[d` | Go to previous diagnostic |
| ]d | Go to next diagnostic | | `]d` | Go to next diagnostic |
| [D | Go to first diagnostic in document | | `[D` | Go to first diagnostic in document |
| ]D | Go to last diagnostic in document | | `]D` | Go to last diagnostic in document |
## Select / extend mode ## Select / extend mode
@@ -113,12 +136,12 @@ the selection.
| Key | Description | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
| z , c | Vertically center the line | | `z` , `c` | Vertically center the line |
| t | Align the line to the top of the screen | | `t` | Align the line to the top of the screen |
| b | Align the line to the bottom of the screen | | `b` | Align the line to the bottom of the screen |
| m | Align the line to the middle of the screen (horizontally) | | `m` | Align the line to the middle of the screen (horizontally) |
| j | Scroll the view downwards | | `j` | Scroll the view downwards |
| k | Scroll the view upwards | | `k` | Scroll the view upwards |
## Goto mode ## Goto mode
@@ -128,17 +151,34 @@ Jumps to various locations.
| Key | Description | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
| g | Go to the start of the file | | `g` | Go to the start of the file |
| e | Go to the end of the file | | `e` | Go to the end of the file |
| h | Go to the start of the line | | `h` | Go to the start of the line |
| l | Go to the end of the line | | `l` | Go to the end of the line |
| t | Go to the top of the screen | | `s` | Go to first non-whitespace character of the line |
| m | Go to the middle of the screen | | `t` | Go to the top of the screen |
| b | Go to the bottom of the screen | | `m` | Go to the middle of the screen |
| d | Go to definition | | `b` | Go to the bottom of the screen |
| y | Go to type definition | | `d` | Go to definition |
| r | Go to references | | `y` | Go to type definition |
| i | Go to implementation | | `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) for an explanation about [surround](./usage.md#surround)
and [textobject](./usage.md#textobject) 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>` |
| `a` `<object>` | Select around textobject |
| `i` `<object>` | Select inside textobject |
## Object mode ## Object mode
@@ -150,10 +190,10 @@ This layer is similar to vim keybindings as kakoune does not support window.
| Key | Description | | Key | Description |
| ----- | ------------- | | ----- | ------------- |
| w, ctrl-w | Switch to next window | | `w`, `Ctrl-w` | Switch to next window |
| v, ctrl-v | Vertical right split | | `v`, `Ctrl-v` | Vertical right split |
| h, ctrl-h | Horizontal bottom split | | `h`, `Ctrl-h` | Horizontal bottom split |
| q, ctrl-q | Close current window | | `q`, `Ctrl-q` | Close current window |
## Space mode ## Space mode
@@ -161,7 +201,29 @@ This layer is a kludge of mappings I had under leader key in neovim.
| Key | Description | | Key | Description |
| ----- | ----------- | | ----- | ----------- |
| f | Open file picker | | `f` | Open file picker |
| b | Open buffer picker | | `b` | Open buffer picker |
| w | Enter window mode | | `s` | Open symbol picker (current document) |
| space | Keep primary selection TODO: it's here because space mode replaced it | | `a` | Apply code action |
| `'` | Open last fuzzy picker |
| `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)

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

@@ -0,0 +1,122 @@
# 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) |
| `keyword.control` | Control flow |
| `namespace` | |
| `punctuation` | |
| `punctuation.delimiter` | |
| `operator` | |
| `special` | |
| `property` | |
| `variable` | |
| `variable.parameter` | |
| `type` | |
| `type.builtin` | |
| `type.enum.variant` | Enum variants |
| `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.linenr.selected` | |
| `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.
## Color palettes
You can define a palette of named colors, and refer to them from the
configuration values in your theme. To do this, add a table called
`palette` to your theme file:
```toml
ui.background = "white"
ui.text = "black"
[palette]
white = "#ffffff"
black = "#000000"
```
Remember that the `[palette]` table includes all keys after its header,
so you should define the palette after normal theme options.

View File

@@ -1 +1,42 @@
# Usage # 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.
## Textobjects
Currently supported: `word`, `surround`.
![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
| Key after `mi` or `ma` | Textobject selected |
| --- | --- |
| `w` | Word |
| `(`, `[`, `'`, etc | Specified surround pairs |
Textobjects based on treesitter, like `function`, `class`, etc are planned.

1
contrib/themes Symbolic link
View File

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

105
flake.lock generated
View File

@@ -1,73 +1,65 @@
{ {
"nodes": { "nodes": {
"flake-utils": { "devshell": {
"locked": { "locked": {
"lastModified": 1620759905, "lastModified": 1625086391,
"narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=", "narHash": "sha256-IpNPv1v8s4L3CoxhwcgZIitGpcrnNgnj09X7TA0QV3k=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "devshell",
"rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8", "rev": "4b5ac7cf7d9a1cc60b965bb51b59922f2210cbc7",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "devshell",
"type": "github" "type": "github"
} }
}, },
"flake-utils_2": { "flakeCompat": {
"flake": false,
"locked": { "locked": {
"lastModified": 1614513358, "lastModified": 1627913399,
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=", "narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
"owner": "numtide", "owner": "edolstra",
"repo": "flake-utils", "repo": "flake-compat",
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3", "rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "numtide", "owner": "edolstra",
"repo": "flake-utils", "repo": "flake-compat",
"type": "github" "type": "github"
} }
}, },
"naersk": { "nixCargoIntegration": {
"inputs": { "inputs": {
"nixpkgs": "nixpkgs" "devshell": "devshell",
"nixpkgs": [
"nixpkgs"
],
"rustOverlay": "rustOverlay"
}, },
"locked": { "locked": {
"lastModified": 1620316130, "lastModified": 1628489367,
"narHash": "sha256-sU0VS5oJS1FsHsZsLELAXc7G2eIelVuucRw+q5B1x9k=", "narHash": "sha256-ADYKHf8aPo1qTw1J+eqVprnEbH8lES0yZamD/yM7RAM=",
"owner": "nmattia", "owner": "yusdacra",
"repo": "naersk", "repo": "nix-cargo-integration",
"rev": "a3f40fe42cc6d267ff7518fa3199e99ff1444ac4", "rev": "0dc8383aae5f791a48e34120edb04670b947dc0b",
"type": "github" "type": "github"
}, },
"original": { "original": {
"owner": "nmattia", "owner": "yusdacra",
"repo": "naersk", "repo": "nix-cargo-integration",
"type": "github" "type": "github"
} }
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1622059058, "lastModified": 1628465643,
"narHash": "sha256-t1/ZMtyxClVSfcV4Pt5C1YpkeJ/UwFF3oitLD7Ch/UA=", "narHash": "sha256-QSNw9bDq9uGUniQQtakRuw4m21Jxugm23SXLVgEV4DM=",
"path": "/nix/store/2gam4i1fa1v19k3n5rc9vgvqac1c2xj5-source",
"rev": "84aa23742f6c72501f9cc209f29c438766f5352d",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1622194753,
"narHash": "sha256-76qtvFp/vFEz46lz5iZMJ0mnsWQYmuGYlb0fHgKqqMg=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "540dccb2aeaffa9dc69bfdc41c55abd7ccc6baa3", "rev": "6ef4f522d63f22b40004319778761040d3197390",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -77,40 +69,21 @@
"type": "github" "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": { "root": {
"inputs": { "inputs": {
"flake-utils": "flake-utils", "flakeCompat": "flakeCompat",
"naersk": "naersk", "nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs_2", "nixpkgs": "nixpkgs"
"rust-overlay": "rust-overlay"
} }
}, },
"rust-overlay": { "rustOverlay": {
"inputs": { "flake": false,
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3"
},
"locked": { "locked": {
"lastModified": 1622257069, "lastModified": 1628475192,
"narHash": "sha256-+QVnS/es9JCRZXphoHL0fOIUhpGqB4/wreBsXWArVck=", "narHash": "sha256-A32shcfPMCll7psCS0OBxVCkA+PKfeWvmU4y9lgNZzU=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "8aa5f93c0b665e5357af19c5631a3450bff4aba5", "rev": "56a8ddb827cbe7a914be88f4a52998a5f93ff468",
"type": "github" "type": "github"
}, },
"original": { "original": {

101
flake.nix
View File

@@ -3,31 +3,86 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils"; nixCargoIntegration = {
rust-overlay.url = "github:oxalica/rust-overlay"; url = "github:yusdacra/nix-cargo-integration";
naersk.url = "github:nmattia/naersk"; inputs.nixpkgs.follows = "nixpkgs";
};
flakeCompat = {
url = "github:edolstra/flake-compat";
flake = false;
};
}; };
outputs = inputs@{ self, nixpkgs, naersk, rust-overlay, flake-utils, ... }: outputs = inputs@{ self, nixCargoIntegration, ... }:
flake-utils.lib.eachDefaultSystem (system: nixCargoIntegration.lib.makeOutputs {
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;
};
in rec {
packages.helix = naerskLib.buildPackage {
pname = "helix";
root = ./.; 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 languages and theme toml files since helix-view expects them
helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
helix-syntax = prev: {
src =
let
pkgs = common.pkgs;
helix = pkgs.fetchgit {
url = "https://github.com/helix-editor/helix.git";
rev = "d4bd5b37669708361a0a6cd2917464b010e6b7f5";
fetchSubmodules = true;
sha256 = "sha256-KayR7K7UC0mT6EjHsZsCYY9IVDJzft63fGpPKGSY8nQ=";
};
in
pkgs.runCommand prev.src.name { } ''
mkdir -p $out
ln -s ${prev.src}/* $out
ln -sf ${helix}/helix-syntax/languages $out
'';
preConfigure = "mkdir -p ../runtime/grammars";
postInstall = "cp -r ../runtime $out/runtime";
};
};
mainBuild = common: prev:
let
inherit (common) pkgs lib;
helixSyntax = lib.buildCrate {
root = self;
memberName = "helix-syntax";
defaultCrateOverrides = {
helix-syntax = common.crateOverrides.helix-syntax;
};
release = false;
};
runtimeDir = pkgs.runCommand "helix-runtime" { } ''
mkdir -p $out
ln -s ${common.root}/runtime/* $out
ln -sf ${helixSyntax}/runtime/grammars $out
'';
in
lib.optionalAttrs (common.memberName == "helix-term") {
nativeBuildInputs = [ pkgs.makeWrapper ];
postFixup = ''
if [ -f "$out/bin/hx" ]; then
wrapProgram "$out/bin/hx" --set HELIX_RUNTIME "${runtimeDir}"
fi
'';
};
shell = common: prev: {
packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb cargo-tarpaulin ]);
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 {};
});
} }

View File

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

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: // insert hook:
// Fn(doc, selection, char) => Option<Transaction> // Fn(doc, selection, char) => Option<Transaction>
@@ -67,7 +67,7 @@ fn handle_open(
let mut offs = 0; 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 pos = range.head;
let next = next_char(doc, pos); 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 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 pos = range.head;
let next = next_char(doc, pos); let next = next_char(doc, pos);
@@ -139,7 +139,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
} }
// handle cases where open and close is the same, or in triples ("""docstring""") // handle cases where open and close is the same, or in triples ("""docstring""")
fn handle_same(doc: &Rope, selection: &Selection, token: char) -> Option<Transaction> { fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option<Transaction> {
// if not cursor but selection, wrap // if not cursor but selection, wrap
// let next = next char // let next = next char

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,20 +1,30 @@
use crate::{ 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; use std::borrow::Cow;
/// Given text, a comment token, and a set of line indices, returns the following:
/// - Whether the given lines should be considered commented
/// - If any of the lines are uncommented, all lines are considered as such.
/// - The lines to change for toggling comments
/// - This is all provided lines excluding blanks lines.
/// - The column of the comment tokens
/// - Column of existing tokens, if the lines are commented; column to place tokens at otherwise.
/// - The margin to the right of the comment tokens
/// - Defaults to `1`. If any existing comment token is not followed by a space, changes to `0`.
fn find_line_comment( fn find_line_comment(
token: &str, token: &str,
text: RopeSlice, text: RopeSlice,
lines: Range<usize>, lines: impl IntoIterator<Item = usize>,
) -> (bool, Vec<usize>, usize) { ) -> (bool, Vec<usize>, usize, usize) {
let mut commented = true; let mut commented = true;
let mut skipped = Vec::new(); let mut to_change = Vec::new();
let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
let mut margin = 1;
let token_len = token.chars().count();
for line in lines { for line in lines {
let line_slice = text.line(line); 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(); let len = line_slice.len_chars();
if pos < min { if pos < min {
@@ -29,47 +39,55 @@ fn find_line_comment(
// considered uncommented. // considered uncommented.
commented = false; commented = false;
} }
} else {
// blank line // determine margin of 0 or 1 for uncommenting; if any comment token is not followed by a space,
skipped.push(line); // a margin of 0 is used for all lines.
if matches!(line_slice.get_char(pos + token_len), Some(c) if c != ' ') {
margin = 0;
}
// blank lines don't get pushed.
to_change.push(line);
} }
} }
(commented, skipped, min) (commented, to_change, min, margin)
} }
#[must_use] #[must_use]
pub fn toggle_line_comments(doc: &Rope, selection: &Selection) -> Transaction { pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&str>) -> Transaction {
let text = doc.slice(..); let text = doc.slice(..);
let mut changes: Vec<Change> = Vec::new();
let token = "//"; let token = token.unwrap_or("//");
let comment = Tendril::from(format!("{} ", token)); let comment = Tendril::from(format!("{} ", token));
let mut lines: Vec<usize> = Vec::new();
let mut min_next_line = 0;
for selection in selection { for selection in selection {
let start = text.char_to_line(selection.from()); let (start, end) = selection.line_range(text);
let end = text.char_to_line(selection.to()); let start = start.max(min_next_line).min(text.len_lines());
let lines = start..end + 1; let end = (end + 1).min(text.len_lines());
let (commented, skipped, min) = find_line_comment(token, text, lines.clone());
changes.reserve(end - start - skipped.len()); lines.extend(start..end);
min_next_line = end + 1;
for line in lines {
if skipped.contains(&line) {
continue;
} }
let (commented, to_change, min, margin) = find_line_comment(token, text, lines);
let mut changes: Vec<Change> = Vec::with_capacity(to_change.len());
for line in to_change {
let pos = text.line_to_char(line) + min; let pos = text.line_to_char(line) + min;
if !commented { if !commented {
// comment line // comment line
changes.push((pos, pos, Some(comment.clone()))) changes.push((pos, pos, Some(comment.clone())));
} else { } else {
// uncomment line // uncomment line
let margin = 1; // TODO: margin is hardcoded 1 but could easily be 0 changes.push((pos, pos + token.len() + margin, None));
changes.push((pos, pos + token.len() + margin, None))
}
} }
} }
Transaction::change(doc, changes.into_iter()) Transaction::change(doc, changes.into_iter())
} }
@@ -91,23 +109,32 @@ mod test {
let text = state.doc.slice(..); let text = state.doc.slice(..);
let res = find_line_comment("//", text, 0..3); let res = find_line_comment("//", text, 0..3);
// (commented = true, skipped = [line 1], min = col 2) // (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1)
assert_eq!(res, (false, vec![1], 2)); assert_eq!(res, (false, vec![0, 2], 2, 1));
// comment // comment
let transaction = toggle_line_comments(&state.doc, &state.selection); let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc); transaction.apply(&mut state.doc);
state.selection = state.selection.clone().map(transaction.changes()); state.selection = state.selection.map(transaction.changes());
assert_eq!(state.doc, " // 1\n\n // 2\n // 3"); assert_eq!(state.doc, " // 1\n\n // 2\n // 3");
// uncomment // uncomment
let transaction = toggle_line_comments(&state.doc, &state.selection); let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc); transaction.apply(&mut state.doc);
state.selection = state.selection.clone().map(transaction.changes()); state.selection = state.selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3");
// 0 margin comments
state.doc = Rope::from(" //1\n\n //2\n //3");
// reset the selection.
state.selection = Selection::single(0, state.doc.len_chars() - 1);
let transaction = toggle_line_comments(&state.doc, &state.selection, None);
transaction.apply(&mut state.doc);
state.selection = state.selection.map(transaction.changes());
assert_eq!(state.doc, " 1\n\n 2\n 3"); assert_eq!(state.doc, " 1\n\n 2\n 3");
// TODO: account for no margin after comment
// TODO: account for uncommenting with uneven comment indentation // TODO: account for uncommenting with uneven comment indentation
} }
} }

View File

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

68
helix-core/src/diff.rs Normal file
View File

@@ -0,0 +1,68 @@
use crate::{Rope, Transaction};
/// Compares `old` and `new` to generate a [`Transaction`] describing
/// the steps required to get from `old` to `new`.
pub fn compare_ropes(old: &Rope, new: &Rope) -> Transaction {
// `similar` only works on contiguous data, so a `Rope` has
// to be temporarily converted into a `String`.
let old_converted = old.to_string();
let new_converted = new.to_string();
// A timeout is set so after 1 seconds, the algorithm will start
// approximating. This is especially important for big `Rope`s or
// `Rope`s that are extremely dissimilar to each other.
//
// Note: Ignore the clippy warning, as the trait bounds of
// `Transaction::change()` require an iterator implementing
// `ExactIterator`.
let mut config = similar::TextDiff::configure();
config.timeout(std::time::Duration::from_secs(1));
let diff = config.diff_chars(&old_converted, &new_converted);
// The current position of the change needs to be tracked to
// construct the `Change`s.
let mut pos = 0;
Transaction::change(
old,
diff.ops()
.iter()
.map(|op| op.as_tag_tuple())
.filter_map(|(tag, old_range, new_range)| {
// `old_pos..pos` is equivalent to `start..end` for where
// the change should be applied.
let old_pos = pos;
pos += old_range.end - old_range.start;
match tag {
// Semantically, inserts and replacements are the same thing.
similar::DiffTag::Insert | similar::DiffTag::Replace => {
// This is the text from the `new` rope that should be
// inserted into `old`.
let text: &str = {
let start = new.char_to_byte(new_range.start);
let end = new.char_to_byte(new_range.end);
&new_converted[start..end]
};
Some((old_pos, pos, Some(text.into())))
}
similar::DiffTag::Delete => Some((old_pos, pos, None)),
similar::DiffTag::Equal => None,
}
}),
)
}
#[cfg(test)]
mod tests {
use super::*;
quickcheck::quickcheck! {
fn test_compare_ropes(a: String, b: String) -> bool {
let mut old = Rope::from(a);
let new = Rope::from(b);
compare_ropes(&old, &new).apply(&mut old);
old.to_string() == new.to_string()
}
}
}

View File

@@ -3,6 +3,8 @@ use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete}; use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;
use std::fmt;
#[must_use] #[must_use]
pub fn grapheme_width(g: &str) -> usize { pub fn grapheme_width(g: &str) -> usize {
if g.as_bytes()[0] <= 127 { if g.as_bytes()[0] <= 127 {
@@ -69,6 +71,8 @@ pub fn nth_prev_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
} }
/// Finds the previous grapheme boundary before the given char position. /// Finds the previous grapheme boundary before the given char position.
#[must_use]
#[inline(always)]
pub fn prev_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize { pub fn prev_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
nth_prev_grapheme_boundary(slice, char_idx, 1) nth_prev_grapheme_boundary(slice, char_idx, 1)
} }
@@ -115,11 +119,38 @@ pub fn nth_next_grapheme_boundary(slice: RopeSlice, char_idx: usize, n: usize) -
} }
/// Finds the next grapheme boundary after the given char position. /// Finds the next grapheme boundary after the given char position.
#[must_use]
#[inline(always)]
pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize { pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
nth_next_grapheme_boundary(slice, char_idx, 1) 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.
#[must_use]
#[inline]
pub fn ensure_grapheme_boundary_next(slice: RopeSlice, char_idx: usize) -> usize {
if char_idx == 0 {
char_idx
} else {
next_grapheme_boundary(slice, char_idx - 1)
}
}
/// Returns the passed char index if it's already a grapheme boundary,
/// or the prev grapheme boundary char index if not.
#[must_use]
#[inline]
pub fn ensure_grapheme_boundary_prev(slice: RopeSlice, char_idx: usize) -> usize {
if char_idx == slice.len_chars() {
char_idx
} else {
prev_grapheme_boundary(slice, char_idx + 1)
}
}
/// Returns whether the given char position is a grapheme boundary. /// Returns whether the given char position is a grapheme boundary.
#[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool { pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
// Bounds check // Bounds check
debug_assert!(char_idx <= slice.len_chars()); debug_assert!(char_idx <= slice.len_chars());
@@ -156,6 +187,18 @@ pub struct RopeGraphemes<'a> {
cursor: GraphemeCursor, 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> { impl<'a> RopeGraphemes<'a> {
#[must_use] #[must_use]
pub fn new(slice: RopeSlice) -> RopeGraphemes { pub fn new(slice: RopeSlice) -> RopeGraphemes {
@@ -193,6 +236,10 @@ impl<'a> Iterator for RopeGraphemes<'a> {
self.cur_chunk_start += self.cur_chunk.len(); self.cur_chunk_start += self.cur_chunk.len();
self.cur_chunk = self.chunks.next().unwrap_or(""); 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!(), _ => unreachable!(),
} }
} }

View File

@@ -1,19 +1,60 @@
use crate::{ChangeSet, Rope, State, Transaction}; use crate::{ChangeSet, Rope, State, Transaction};
use smallvec::{smallvec, SmallVec}; use once_cell::sync::Lazy;
use regex::Regex;
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 { pub struct History {
revisions: Vec<Revision>, revisions: Vec<Revision>,
cursor: usize, current: usize,
} }
// A single point in history. See [History] for more information.
#[derive(Debug)] #[derive(Debug)]
struct Revision { struct Revision {
parent: usize, parent: usize,
children: SmallVec<[(usize, Transaction); 1]>, last_child: Option<NonZeroUsize>,
/// The transaction to revert to previous state. transaction: Transaction,
revert: Transaction, // We need an inversion for undos because delete transactions don't store
// selection before, selection after? // the deleted text.
inversion: Transaction,
timestamp: Instant,
} }
impl Default for History { impl Default for History {
@@ -22,72 +63,252 @@ impl Default for History {
Self { Self {
revisions: vec![Revision { revisions: vec![Revision {
parent: 0, parent: 0,
children: SmallVec::new(), last_child: None,
revert: Transaction::from(ChangeSet::new(&Rope::new())), transaction: Transaction::from(ChangeSet::new(&Rope::new())),
inversion: Transaction::from(ChangeSet::new(&Rope::new())),
timestamp: Instant::now(),
}], }],
cursor: 0, current: 0,
} }
} }
} }
impl History { impl History {
pub fn commit_revision(&mut self, transaction: &Transaction, original: &State) { pub fn commit_revision(&mut self, transaction: &Transaction, original: &State) {
// TODO: could store a single transaction, if deletes also stored the text they delete self.commit_revision_at_timestamp(transaction, original, Instant::now());
let revert = transaction }
pub fn commit_revision_at_timestamp(
&mut self,
transaction: &Transaction,
original: &State,
timestamp: Instant,
) {
let inversion = transaction
.invert(&original.doc) .invert(&original.doc)
// Store the current cursor position // Store the current cursor position
.with_selection(original.selection.clone()); .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 { self.revisions.push(Revision {
parent: self.cursor, parent: self.current,
children: SmallVec::new(), last_child: None,
revert, transaction: transaction.clone(),
inversion,
timestamp,
}); });
self.current = new_current;
// 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;
} }
#[inline] #[inline]
pub fn current_revision(&self) -> usize { pub fn current_revision(&self) -> usize {
self.cursor self.current
} }
#[inline] #[inline]
pub const fn at_root(&self) -> bool { pub const fn at_root(&self) -> bool {
self.cursor == 0 self.current == 0
} }
pub fn undo(&mut self) -> Option<&Transaction> { pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() { if self.at_root() {
// We're at the root of undo, nothing to do.
return None; return None;
} }
let current_revision = &self.revisions[self.cursor]; let current_revision = &self.revisions[self.current];
self.current = current_revision.parent;
self.cursor = current_revision.parent; Some(&current_revision.inversion)
Some(&current_revision.revert)
} }
pub fn redo(&mut self) -> Option<&Transaction> { pub fn redo(&mut self) -> Option<&Transaction> {
let current_revision = &self.revisions[self.cursor]; 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) Some(&self.revisions[last_child.get()].transaction)
if let Some((index, transaction)) = current_revision.children.last() { }
self.cursor = *index;
fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
return Some(&transaction); 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
} }
} }
@@ -143,4 +364,191 @@ mod test {
undo(&mut history, &mut state); undo(&mut history, &mut state);
assert_eq!("hello", state.doc); 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

@@ -1,13 +1,180 @@
use crate::{ use crate::{
chars::{char_is_line_ending, char_is_whitespace},
find_first_non_whitespace_char, find_first_non_whitespace_char,
syntax::{IndentQuery, LanguageConfiguration, Syntax}, syntax::{IndentQuery, LanguageConfiguration, Syntax},
tree_sitter::{Node, Tree}, tree_sitter::Node,
Rope, RopeSlice, Rope, RopeSlice,
}; };
/// Enum representing indentation style.
///
/// Only values 1-8 are valid for the `Spaces` variant.
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum IndentStyle {
Tabs,
Spaces(u8),
}
impl IndentStyle {
/// Creates an `IndentStyle` from an indentation string.
///
/// For example, passing `" "` (four spaces) will create `IndentStyle::Spaces(4)`.
#[allow(clippy::should_implement_trait)]
#[inline]
pub fn from_str(indent: &str) -> Self {
// XXX: do we care about validating the input more than this? Probably not...?
debug_assert!(!indent.is_empty() && indent.len() <= 8);
if indent.starts_with(' ') {
IndentStyle::Spaces(indent.len() as u8)
} else {
IndentStyle::Tabs
}
}
#[inline]
pub fn as_str(&self) -> &'static str {
match *self {
IndentStyle::Tabs => "\t",
IndentStyle::Spaces(1) => " ",
IndentStyle::Spaces(2) => " ",
IndentStyle::Spaces(3) => " ",
IndentStyle::Spaces(4) => " ",
IndentStyle::Spaces(5) => " ",
IndentStyle::Spaces(6) => " ",
IndentStyle::Spaces(7) => " ",
IndentStyle::Spaces(8) => " ",
// Unsupported indentation style. This should never happen,
// but just in case fall back to two spaces.
IndentStyle::Spaces(n) => {
debug_assert!(n > 0 && n <= 8); // Always triggers. `debug_panic!()` wanted.
" "
}
}
}
}
/// Attempts to detect the indentation style used in a document.
///
/// Returns the indentation style if the auto-detect confidence is
/// reasonably high, otherwise returns `None`.
pub fn auto_detect_indent_style(document_text: &Rope) -> Option<IndentStyle> {
// Build a histogram of the indentation *increases* between
// subsequent lines, ignoring lines that are all whitespace.
//
// Index 0 is for tabs, the rest are 1-8 spaces.
let histogram: [usize; 9] = {
let mut histogram = [0; 9];
let mut prev_line_is_tabs = false;
let mut prev_line_leading_count = 0usize;
// Loop through the lines, checking for and recording indentation
// increases as we go.
'outer: for line in document_text.lines().take(1000) {
let mut c_iter = line.chars();
// Is first character a tab or space?
let is_tabs = match c_iter.next() {
Some('\t') => true,
Some(' ') => false,
// Ignore blank lines.
Some(c) if char_is_line_ending(c) => continue,
_ => {
prev_line_is_tabs = false;
prev_line_leading_count = 0;
continue;
}
};
// Count the line's total leading tab/space characters.
let mut leading_count = 1;
let mut count_is_done = false;
for c in c_iter {
match c {
'\t' if is_tabs && !count_is_done => leading_count += 1,
' ' if !is_tabs && !count_is_done => leading_count += 1,
// We stop counting if we hit whitespace that doesn't
// qualify as indent or doesn't match the leading
// whitespace, but we don't exit the loop yet because
// we still want to determine if the line is blank.
c if char_is_whitespace(c) => count_is_done = true,
// Ignore blank lines.
c if char_is_line_ending(c) => continue 'outer,
_ => break,
}
// Bound the worst-case execution time for weird text files.
if leading_count > 256 {
continue 'outer;
}
}
// If there was an increase in indentation over the previous
// line, update the histogram with that increase.
if (prev_line_is_tabs == is_tabs || prev_line_leading_count == 0)
&& prev_line_leading_count < leading_count
{
if is_tabs {
histogram[0] += 1;
} else {
let amount = leading_count - prev_line_leading_count;
if amount <= 8 {
histogram[amount] += 1;
}
}
}
// Store this line's leading whitespace info for use with
// the next line.
prev_line_is_tabs = is_tabs;
prev_line_leading_count = leading_count;
}
// Give more weight to tabs, because their presence is a very
// strong indicator.
histogram[0] *= 2;
histogram
};
// Find the most frequent indent, its frequency, and the frequency of
// the next-most frequent indent.
let indent = histogram
.iter()
.enumerate()
.max_by_key(|kv| kv.1)
.unwrap()
.0;
let indent_freq = histogram[indent];
let indent_freq_2 = *histogram
.iter()
.enumerate()
.filter(|kv| kv.0 != indent)
.map(|kv| kv.1)
.max()
.unwrap();
// Return the the auto-detected result if we're confident enough in its
// accuracy, based on some heuristics.
if indent_freq >= 1 && (indent_freq_2 as f64 / indent_freq as f64) < 0.66 {
Some(match indent {
0 => IndentStyle::Tabs,
_ => IndentStyle::Spaces(indent as u8),
})
} else {
None
}
}
/// To determine indentation of a newly inserted line, figure out the indentation at the last col /// To determine indentation of a newly inserted line, figure out the indentation at the last col
/// of the previous line. /// of the previous line.
#[allow(dead_code)]
fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize { fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
let mut len = 0; let mut len = 0;
for ch in line.chars() { for ch in line.chars() {
@@ -47,7 +214,7 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
// NOTE: can't use contains() on query because of comparing Vec<String> and &str // NOTE: can't use contains() on query because of comparing Vec<String> and &str
// https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains // https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
let mut increment: i32 = 0; let mut increment: isize = 0;
let mut node = match node { let mut node = match node {
Some(node) => node, Some(node) => node,
@@ -93,23 +260,25 @@ fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool)
node = parent; node = parent;
} }
assert!(increment >= 0); increment.max(0) as usize
increment as usize
} }
#[allow(dead_code)]
fn suggested_indent_for_line( fn suggested_indent_for_line(
language_config: &LanguageConfiguration, language_config: &LanguageConfiguration,
syntax: Option<&Syntax>, syntax: Option<&Syntax>,
text: RopeSlice, text: RopeSlice,
line_num: usize, line_num: usize,
tab_width: usize, _tab_width: usize,
) -> usize { ) -> usize {
let line = text.line(line_num); if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
let current = indent_level_for_line(line, tab_width); return suggested_indent_for_pos(
Some(language_config),
if let Some(start) = find_first_non_whitespace_char(text, line_num) { syntax,
return suggested_indent_for_pos(Some(language_config), syntax, text, start, false); text,
start + text.line_to_char(line_num),
false,
);
}; };
// if the line is blank, indent should be zero // if the line is blank, indent should be zero
@@ -144,9 +313,30 @@ pub fn suggested_indent_for_pos(
} }
} }
pub fn get_scopes(syntax: Option<&Syntax>, text: RopeSlice, pos: usize) -> Vec<&'static str> {
let mut scopes = Vec::new();
if let Some(syntax) = syntax {
let byte_start = text.char_to_byte(pos);
let node = match get_highest_syntax_node_at_bytepos(syntax, byte_start) {
Some(node) => node,
None => return scopes,
};
scopes.push(node.kind());
while let Some(parent) = node.parent() {
scopes.push(parent.kind())
}
}
scopes.reverse();
scopes
}
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::Rope;
#[test] #[test]
fn test_indent_level() { fn test_indent_level() {
@@ -248,18 +438,20 @@ where
let doc = Rope::from(doc); let doc = Rope::from(doc);
use crate::syntax::{ use crate::syntax::{
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader, Configuration, IndentationConfiguration, LanguageConfiguration, Loader,
}; };
use once_cell::sync::OnceCell; use once_cell::sync::OnceCell;
let loader = Loader::new( let loader = Loader::new(Configuration {
Configuration {
language: vec![LanguageConfiguration { language: vec![LanguageConfiguration {
scope: "source.rust".to_string(), scope: "source.rust".to_string(),
file_types: vec!["rs".to_string()], file_types: vec!["rs".to_string()],
language_id: Lang::Rust, language_id: "Rust".to_string(),
highlight_config: OnceCell::new(), highlight_config: OnceCell::new(),
config: None,
// //
roots: vec![], roots: vec![],
comment_token: None,
auto_format: false,
language_server: None, language_server: None,
indent: Some(IndentationConfiguration { indent: Some(IndentationConfiguration {
tab_width: 4, tab_width: 4,
@@ -267,9 +459,7 @@ where
}), }),
indent_query: OnceCell::new(), indent_query: OnceCell::new(),
}], }],
}, });
Vec::new(),
);
// set runtime path so we can find the queries // set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR")); let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));

View File

@@ -1,10 +1,12 @@
#![allow(unused)]
pub mod auto_pairs; pub mod auto_pairs;
pub mod chars;
pub mod comment; pub mod comment;
pub mod diagnostic; pub mod diagnostic;
pub mod diff;
pub mod graphemes; pub mod graphemes;
mod history; pub mod history;
pub mod indent; pub mod indent;
pub mod line_ending;
pub mod macros; pub mod macros;
pub mod match_brackets; pub mod match_brackets;
pub mod movement; pub mod movement;
@@ -14,48 +16,69 @@ pub mod register;
pub mod search; pub mod search;
pub mod selection; pub mod selection;
mod state; mod state;
pub mod surround;
pub mod syntax; pub mod syntax;
pub mod textobject;
mod transaction; mod transaction;
pub mod words;
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> { pub mod unicode {
// find first non-whitespace char pub use unicode_general_category as category;
for (start, ch) in line.chars().enumerate() { pub use unicode_segmentation as segmentation;
// TODO: could use memchr with chunks? pub use unicode_width as width;
if ch != ' ' && ch != '\t' && ch != '\n' {
return Some(start);
}
} }
None static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
} once_cell::sync::Lazy::new(runtime_dir);
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 pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
for ch in line.chars() { line.chars().position(|ch| !ch.is_whitespace())
// TODO: could use memchr with chunks?
if ch != ' ' && ch != '\t' && ch != '\n' {
return Some(start);
}
start += 1;
} }
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)
}
}
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 None
} }
#[cfg(not(embed_runtime))]
pub fn runtime_dir() -> std::path::PathBuf { pub fn runtime_dir() -> std::path::PathBuf {
// runtime env var || dir where binary is located if let Ok(dir) = std::env::var("HELIX_RUNTIME") {
std::env::var("HELIX_RUNTIME") return dir.into();
.map(|path| path.into()) }
.unwrap_or_else(|_| {
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() std::env::current_exe()
.ok() .ok()
.and_then(|path| path.parent().map(|path| path.to_path_buf())) .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR)))
.unwrap() .unwrap()
})
} }
pub fn config_dir() -> std::path::PathBuf { pub fn config_dir() -> std::path::PathBuf {
@@ -74,22 +97,25 @@ pub fn cache_dir() -> std::path::PathBuf {
path path
} }
pub use etcetera::home_dir;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
pub use ropey::{Rope, RopeSlice}; pub use ropey::{Rope, RopeBuilder, RopeSlice};
pub use tendril::StrTendril as Tendril; pub use tendril::StrTendril as Tendril;
#[doc(inline)] #[doc(inline)]
pub use {regex, tree_sitter}; pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes;
pub use position::{coords_at_pos, pos_at_coords, Position}; pub use position::{coords_at_pos, pos_at_coords, Position};
pub use selection::{Range, Selection}; pub use selection::{Range, Selection};
pub use smallvec::SmallVec; pub use smallvec::SmallVec;
pub use syntax::Syntax; pub use syntax::Syntax;
pub use diagnostic::Diagnostic; pub use diagnostic::Diagnostic;
pub use history::History;
pub use state::State; pub use state::State;
pub use line_ending::{LineEnding, DEFAULT_LINE_ENDING};
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction}; pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};

View File

@@ -0,0 +1,265 @@
use crate::{Rope, 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)
}
/// Fetches line `line_idx` from the passed rope slice, sans any line ending.
pub fn line_without_line_ending<'a>(slice: &'a RopeSlice, line_idx: usize) -> RopeSlice<'a> {
let start = slice.line_to_char(line_idx);
let end = line_end_char_index(slice, line_idx);
slice.slice(start..end)
}
/// 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,4 +1,4 @@
use crate::{Range, Rope, Selection, Syntax}; use crate::{Rope, Syntax};
const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')]; const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')];
// limit matching pairs to only ( ) { } [ ] < > // limit matching pairs to only ( ) { } [ ] < >
@@ -12,7 +12,7 @@ pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
// most naive implementation: find the innermost syntax node, if we're at the edge of a node, // most naive implementation: find the innermost syntax node, if we're at the edge of a node,
// return the other edge. // return the other edge.
let mut node = match tree let node = match tree
.root_node() .root_node()
.named_descendant_for_byte_range(byte_pos, byte_pos) .named_descendant_for_byte_range(byte_pos, byte_pos)
{ {
@@ -24,12 +24,13 @@ pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
return None; return None;
} }
let start_byte = node.start_byte();
let len = doc.len_bytes(); let len = doc.len_bytes();
if start_byte >= len { let start_byte = node.start_byte();
let end_byte = node.end_byte() - 1; // it's end exclusive
if start_byte >= len || end_byte >= len {
return None; return None;
} }
let end_byte = node.end_byte() - 1; // it's end exclusive
let start_char = doc.byte_to_char(start_byte); let start_char = doc.byte_to_char(start_byte);
let end_char = doc.byte_to_char(end_byte); let end_char = doc.byte_to_char(end_byte);

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
use crate::{Range, RopeSlice, Selection, Syntax}; use crate::{Range, RopeSlice, Selection, Syntax};
use smallvec::smallvec;
// TODO: to contract_selection we'd need to store the previous ranges before expand. // TODO: to contract_selection we'd need to store the previous ranges before expand.
// Maybe just contract to the first child node? // Maybe just contract to the first child node?
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection { pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
let tree = syntax.tree(); let tree = syntax.tree();
selection.transform(|range| { selection.clone().transform(|range| {
let from = text.char_to_byte(range.from()); let from = text.char_to_byte(range.from());
let to = text.char_to_byte(range.to()); let to = text.char_to_byte(range.to());

View File

@@ -1,6 +1,8 @@
use crate::{ use crate::{
graphemes::{nth_next_grapheme_boundary, RopeGraphemes}, chars::char_is_line_ending,
Rope, RopeSlice, graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
line_ending::line_end_char_index,
RopeSlice,
}; };
/// Represents a single point in a text buffer. Zero indexed. /// Represents a single point in a text buffer. Zero indexed.
@@ -23,8 +25,9 @@ impl Position {
pub fn traverse(self, text: &crate::Tendril) -> Self { pub fn traverse(self, text: &crate::Tendril) -> Self {
let Self { mut row, mut col } = self; let Self { mut row, mut col } = self;
// TODO: there should be a better way here // TODO: there should be a better way here
for ch in text.chars() { let mut chars = text.chars().peekable();
if ch == '\n' { while let Some(ch) = chars.next() {
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
row += 1; row += 1;
col = 0; col = 0;
} else { } else {
@@ -50,24 +53,65 @@ impl From<Position> for tree_sitter::Point {
} }
} }
/// Convert a character index to (line, column) coordinates. /// Convert a character index to (line, column) coordinates.
///
/// TODO: this should be split into two methods: one for visual
/// row/column, and one for "objective" row/column (possibly with
/// the column specified in `char`s). The former would be used
/// for cursor movement, and the latter would be used for e.g. the
/// row:column display in the status line.
pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position { pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
let line = text.char_to_line(pos); let line = text.char_to_line(pos);
let line_start = text.line_to_char(line); let line_start = text.line_to_char(line);
let pos = ensure_grapheme_boundary_prev(text, pos);
let col = RopeGraphemes::new(text.slice(line_start..pos)).count(); let col = RopeGraphemes::new(text.slice(line_start..pos)).count();
Position::new(line, col) Position::new(line, col)
} }
/// Convert (line, column) coordinates to a character index. /// Convert (line, column) coordinates to a character index.
pub fn pos_at_coords(text: RopeSlice, coords: Position) -> usize { ///
/// If the `line` coordinate is beyond the end of the file, the EOF
/// position will be returned.
///
/// If the `column` coordinate is past the end of the given line, the
/// line-end position will be returned. What constitutes the "line-end
/// position" depends on the parameter `limit_before_line_ending`. If it's
/// `true`, the line-end position will be just *before* the line ending
/// character. If `false` it will be just *after* the line ending
/// character--on the border between the current line and the next.
///
/// Usually you only want `limit_before_line_ending` to be `true` if you're working
/// with left-side block-cursor positions, as this prevents the the block cursor
/// from jumping to the next line. Otherwise you typically want it to be `false`,
/// such as when dealing with raw anchor/head positions.
///
/// TODO: this should be changed to work in terms of visual row/column, not
/// graphemes.
pub fn pos_at_coords(text: RopeSlice, coords: Position, limit_before_line_ending: bool) -> usize {
let Position { row, col } = coords; let Position { row, col } = coords;
let line_start = text.line_to_char(row); let line_start = text.line_to_char(row);
// line_start + col let line_end = if limit_before_line_ending {
nth_next_grapheme_boundary(text, line_start, col) line_end_char_index(&text, row)
} else {
text.line_to_char((row + 1).min(text.len_lines()))
};
let mut col_char_offset = 0;
for (i, g) in RopeGraphemes::new(text.slice(line_start..line_end)).enumerate() {
if i == col {
break;
}
col_char_offset += g.chars().count();
}
line_start + col_char_offset
} }
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::Rope;
#[test] #[test]
fn test_ordering() { fn test_ordering() {
@@ -79,53 +123,107 @@ mod test {
fn test_coords_at_pos() { fn test_coords_at_pos() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
let slice = text.slice(..); let slice = text.slice(..);
// assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
// assert_eq!(coords_at_pos(slice, 5), (0, 5).into()); // position on \n assert_eq!(coords_at_pos(slice, 5), (0, 5).into()); // position on \n
// assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // position on w assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); // position on w
// assert_eq!(coords_at_pos(slice, 7), (1, 1).into()); // position on o assert_eq!(coords_at_pos(slice, 7), (1, 1).into()); // position on o
// assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
// test with grapheme clusters // Test with wide characters.
// TODO: account for character width.
let text = Rope::from("今日はいい\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
assert_eq!(coords_at_pos(slice, 1), (0, 1).into());
assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
assert_eq!(coords_at_pos(slice, 3), (0, 3).into());
assert_eq!(coords_at_pos(slice, 4), (0, 4).into());
assert_eq!(coords_at_pos(slice, 5), (0, 5).into());
assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
// Test with grapheme clusters.
let text = Rope::from("a̐éö̲\r\n"); let text = Rope::from("a̐éö̲\r\n");
let slice = text.slice(..); let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
assert_eq!(coords_at_pos(slice, 2), (0, 1).into()); assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
assert_eq!(coords_at_pos(slice, 4), (0, 2).into()); assert_eq!(coords_at_pos(slice, 4), (0, 2).into());
assert_eq!(coords_at_pos(slice, 7), (0, 3).into()); assert_eq!(coords_at_pos(slice, 7), (0, 3).into());
assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
let text = Rope::from("किमपि"); // Test with wide-character grapheme clusters.
// TODO: account for character width.
let text = Rope::from("किमपि\n");
let slice = text.slice(..); let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
assert_eq!(coords_at_pos(slice, 2), (0, 1).into()); assert_eq!(coords_at_pos(slice, 2), (0, 1).into());
assert_eq!(coords_at_pos(slice, 3), (0, 2).into()); assert_eq!(coords_at_pos(slice, 3), (0, 2).into());
assert_eq!(coords_at_pos(slice, 5), (0, 3).into()); assert_eq!(coords_at_pos(slice, 5), (0, 3).into());
assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
// Test with tabs.
// Todo: account for tab stops.
let text = Rope::from("\tHello\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
assert_eq!(coords_at_pos(slice, 1), (0, 1).into());
assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
} }
#[test] #[test]
fn test_pos_at_coords() { fn test_pos_at_coords() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
let slice = text.slice(..); let slice = text.slice(..);
assert_eq!(pos_at_coords(slice, (0, 0).into()), 0); assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 5).into()), 5); // position on \n assert_eq!(pos_at_coords(slice, (0, 5).into(), false), 5); // position on \n
assert_eq!(pos_at_coords(slice, (1, 0).into()), 6); // position on w assert_eq!(pos_at_coords(slice, (0, 6).into(), false), 6); // position after \n
assert_eq!(pos_at_coords(slice, (1, 1).into()), 7); // position on o assert_eq!(pos_at_coords(slice, (0, 6).into(), true), 5); // position after \n
assert_eq!(pos_at_coords(slice, (1, 4).into()), 10); // position on d assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 6); // position on w
assert_eq!(pos_at_coords(slice, (1, 1).into(), false), 7); // position on o
assert_eq!(pos_at_coords(slice, (1, 4).into(), false), 10); // position on d
// test with grapheme clusters // Test with wide characters.
// TODO: account for character width.
let text = Rope::from("今日はいい\n");
let slice = text.slice(..);
assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 3);
assert_eq!(pos_at_coords(slice, (0, 4).into(), false), 4);
assert_eq!(pos_at_coords(slice, (0, 5).into(), false), 5);
assert_eq!(pos_at_coords(slice, (0, 6).into(), false), 6);
assert_eq!(pos_at_coords(slice, (0, 6).into(), true), 5);
assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 6);
// Test with grapheme clusters.
let text = Rope::from("a̐éö̲\r\n"); let text = Rope::from("a̐éö̲\r\n");
let slice = text.slice(..); let slice = text.slice(..);
assert_eq!(pos_at_coords(slice, (0, 0).into()), 0); assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into()), 2); assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 2);
assert_eq!(pos_at_coords(slice, (0, 2).into()), 4); assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 4);
assert_eq!(pos_at_coords(slice, (0, 3).into()), 7); // \r\n is one char here assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 7); // \r\n is one char here
assert_eq!(pos_at_coords(slice, (0, 4).into()), 9); assert_eq!(pos_at_coords(slice, (0, 4).into(), false), 9);
assert_eq!(pos_at_coords(slice, (0, 4).into(), true), 7);
assert_eq!(pos_at_coords(slice, (1, 0).into(), false), 9);
// Test with wide-character grapheme clusters.
// TODO: account for character width.
let text = Rope::from("किमपि"); let text = Rope::from("किमपि");
// 2 - 1 - 2 codepoints // 2 - 1 - 2 codepoints
// TODO: delete handling as per https://news.ycombinator.com/item?id=20058454 // TODO: delete handling as per https://news.ycombinator.com/item?id=20058454
let slice = text.slice(..); let slice = text.slice(..);
assert_eq!(pos_at_coords(slice, (0, 0).into()), 0); assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into()), 2); assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 2);
assert_eq!(pos_at_coords(slice, (0, 2).into()), 3); assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 3);
assert_eq!(pos_at_coords(slice, (0, 3).into()), 5); // eol assert_eq!(pos_at_coords(slice, (0, 3).into(), false), 5);
assert_eq!(pos_at_coords(slice, (0, 3).into(), true), 5);
// Test with tabs.
// Todo: account for tab stops.
let text = Rope::from("\tHello\n");
let slice = text.slice(..);
assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 1);
assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 2);
} }
} }

View File

@@ -1,21 +1,63 @@
use crate::Tendril; use std::collections::HashMap;
use once_cell::sync::Lazy;
use std::{collections::HashMap, sync::RwLock};
// TODO: could be an instance on Editor #[derive(Debug)]
static REGISTRY: Lazy<RwLock<HashMap<char, Vec<String>>>> = pub struct Register {
Lazy::new(|| RwLock::new(HashMap::new())); name: char,
values: Vec<String>,
pub fn get(register: char) -> Option<Vec<String>> {
let registry = REGISTRY.read().unwrap();
// TODO: no cloning
registry.get(&register).cloned()
} }
// restoring: bool impl Register {
pub fn set(register: char, values: Vec<String>) { pub fn new(name: char) -> Self {
let mut registry = REGISTRY.write().unwrap(); Self {
name,
registry.insert(register, values); values: Vec::new(),
}
}
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) -> &[String] {
&self.values
}
pub fn write(&mut self, values: Vec<String>) {
self.values = values;
}
pub fn push(&mut self, value: String) {
self.values.push(value);
}
}
/// 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) -> &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<&[String]> {
self.get(name).map(|reg| reg.read())
}
} }

View File

@@ -1,18 +1,11 @@
use crate::RopeSlice; use crate::RopeSlice;
pub fn find_nth_next( pub fn find_nth_next(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
text: RopeSlice, if pos >= text.len_chars() || n == 0 {
ch: char,
mut pos: usize,
n: usize,
inclusive: bool,
) -> Option<usize> {
if pos >= text.len_chars() {
return None; return None;
} }
// start searching right after pos let mut chars = text.chars_at(pos);
let mut chars = text.chars_at(pos + 1);
for _ in 0..n { for _ in 0..n {
loop { loop {
@@ -26,28 +19,21 @@ pub fn find_nth_next(
} }
} }
if !inclusive { Some(pos - 1)
pos -= 1;
} }
Some(pos) pub fn find_nth_prev(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
if pos == 0 || n == 0 {
return None;
} }
pub fn find_nth_prev(
text: RopeSlice,
ch: char,
mut pos: usize,
n: usize,
inclusive: bool,
) -> Option<usize> {
// start searching right before pos
let mut chars = text.chars_at(pos); let mut chars = text.chars_at(pos);
for _ in 0..n { for _ in 0..n {
loop { loop {
let c = chars.prev()?; let c = chars.prev()?;
pos = pos.saturating_sub(1); pos -= 1;
if c == ch { if c == ch {
break; break;
@@ -55,9 +41,5 @@ pub fn find_nth_prev(
} }
} }
if !inclusive {
pos += 1;
}
Some(pos) Some(pos)
} }

View File

@@ -1,30 +1,59 @@
//! Selections are the primary editing construct. Even a single cursor is defined as an empty //! Selections are the primary editing construct. Even cursors are
//! single selection range. //! defined as a selection range.
//! //!
//! All positioning is done via `char` offsets into the buffer. //! All positioning is done via `char` offsets into the buffer.
use crate::{Assoc, ChangeSet, Rope, RopeSlice}; use crate::{
graphemes::{
ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary,
prev_grapheme_boundary,
},
Assoc, ChangeSet, RopeSlice,
};
use smallvec::{smallvec, SmallVec}; use smallvec::{smallvec, SmallVec};
use std::borrow::Cow; use std::borrow::Cow;
#[inline] /// A single selection range.
fn abs_difference(x: usize, y: usize) -> usize { ///
if x < y { /// A range consists of an "anchor" and "head" position in
y - x /// the text. The head is the part that the user moves when
} else { /// directly extending a selection. The head and anchor
x - y /// can be in any order, or even share the same position.
} ///
} /// The anchor and head positions use gap indexing, meaning
/// that their indices represent the the gaps *between* `char`s
/// A single selection range. Anchor-inclusive, head-exclusive. /// rather than the `char`s themselves. For example, 1
/// represents the position between the first and second `char`.
///
/// Below are some example `Range` configurations to better
/// illustrate. The anchor and head indices are show as
/// "(anchor, head)", followed by example text with "[" and "]"
/// inserted to represent the anchor and head positions:
///
/// - (0, 3): [Som]e text.
/// - (3, 0): ]Som[e text.
/// - (2, 7): So[me te]xt.
/// - (1, 1): S[]ome text.
///
/// Ranges are considered to be inclusive on the left and
/// exclusive on the right, regardless of anchor-head ordering.
/// This means, for example, that non-zero-width ranges that
/// are directly adjecent, sharing an edge, do not overlap.
/// However, a zero-width range will overlap with the shared
/// left-edge of another range.
///
/// By convention, user-facing ranges are considered to have
/// a block cursor on the head-side of the range that spans a
/// single grapheme inward from the range's edge. There are a
/// variety of helper methods on `Range` for working in terms of
/// that block cursor, all of which have `cursor` in their name.
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Range { pub struct Range {
// TODO: optimize into u32
/// The anchor of the range: the side that doesn't move when extending. /// The anchor of the range: the side that doesn't move when extending.
pub anchor: usize, pub anchor: usize,
/// The head of the range, moved when extending. /// The head of the range, moved when extending.
pub head: usize, pub head: usize,
pub horiz: Option<u32>, pub horiz: Option<u32>,
} // TODO: might be cheaper to store normalized as from/to and an inverted flag }
impl Range { impl Range {
pub fn new(anchor: usize, head: usize) -> Self { pub fn new(anchor: usize, head: usize) -> Self {
@@ -35,6 +64,10 @@ impl Range {
} }
} }
pub fn point(head: usize) -> Self {
Self::new(head, head)
}
/// Start of the range. /// Start of the range.
#[inline] #[inline]
#[must_use] #[must_use]
@@ -49,6 +82,20 @@ impl Range {
std::cmp::max(self.anchor, self.head) std::cmp::max(self.anchor, self.head)
} }
/// The (inclusive) range of lines that the range overlaps.
#[inline]
#[must_use]
pub fn line_range(&self, text: RopeSlice) -> (usize, usize) {
let from = self.from();
let to = if self.is_empty() {
self.to()
} else {
prev_grapheme_boundary(text, self.to()).max(from)
};
(text.char_to_line(from), text.char_to_line(to))
}
/// `true` when head and anchor are at the same position. /// `true` when head and anchor are at the same position.
#[inline] #[inline]
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
@@ -58,37 +105,39 @@ impl Range {
/// Check two ranges for overlap. /// Check two ranges for overlap.
#[must_use] #[must_use]
pub fn overlaps(&self, other: &Self) -> bool { pub fn overlaps(&self, other: &Self) -> bool {
// cursor overlap is checked differently // To my eye, it's non-obvious why this works, but I arrived
if self.is_empty() { // at it after transforming the slower version that explicitly
let pos = self.head; // enumerated more cases. The unit tests are thorough.
pos >= other.from() && other.to() >= pos self.from() == other.from() || (self.to() > other.from() && other.to() > self.from())
} else {
self.to() > other.from() && other.to() > self.from()
}
} }
pub fn contains(&self, pos: usize) -> bool { pub fn contains(&self, pos: usize) -> bool {
if self.is_empty() { self.from() <= pos && pos < self.to()
return false;
}
if self.anchor < self.head {
self.anchor <= pos && pos < self.head
} else {
self.head < pos && pos <= self.anchor
}
} }
/// Map a range through a set of changes. Returns a new range representing the same position /// Map a range through a set of changes. Returns a new range representing the same position
/// after the changes are applied. /// after the changes are applied.
pub fn map(self, changes: &ChangeSet) -> Self { pub fn map(self, changes: &ChangeSet) -> Self {
let anchor = changes.map_pos(self.anchor, Assoc::After); use std::cmp::Ordering;
let head = changes.map_pos(self.head, Assoc::After); let (anchor, head) = match self.anchor.cmp(&self.head) {
Ordering::Equal => (
changes.map_pos(self.anchor, Assoc::After),
changes.map_pos(self.head, Assoc::After),
),
Ordering::Less => (
changes.map_pos(self.anchor, Assoc::After),
changes.map_pos(self.head, Assoc::Before),
),
Ordering::Greater => (
changes.map_pos(self.anchor, Assoc::Before),
changes.map_pos(self.head, Assoc::After),
),
};
// TODO: possibly unnecessary // We want to return a new `Range` with `horiz == None` every time,
if self.anchor == anchor && self.head == head { // even if the anchor and head haven't changed, because we don't
return self; // know if the *visual* position hasn't changed due to
} // character-width or grapheme changes earlier in the text.
Self { Self {
anchor, anchor,
head, head,
@@ -99,30 +148,172 @@ impl Range {
/// Extend the range to cover at least `from` `to`. /// Extend the range to cover at least `from` `to`.
#[must_use] #[must_use]
pub fn extend(&self, from: usize, to: usize) -> Self { pub fn extend(&self, from: usize, to: usize) -> Self {
if from <= self.anchor && to >= self.anchor { debug_assert!(from <= to);
return Self {
anchor: from, if self.anchor <= self.head {
head: to, Self {
anchor: self.anchor.min(from),
head: self.head.max(to),
horiz: None, horiz: None,
}; }
} else {
Self {
anchor: self.anchor.max(to),
head: self.head.min(from),
horiz: None,
}
}
} }
Self { /// Returns a range that encompasses both input ranges.
anchor: self.anchor, ///
head: if abs_difference(from, self.anchor) > abs_difference(to, self.anchor) { /// This is like `extend()`, but tries to negotiate the
from /// anchor/head ordering between the two input ranges.
} else { #[must_use]
to pub fn merge(&self, other: Self) -> Self {
}, if self.anchor > self.head && other.anchor > other.head {
Range {
anchor: self.anchor.max(other.anchor),
head: self.head.min(other.head),
horiz: None, horiz: None,
} }
} else {
Range {
anchor: self.from().min(other.from()),
head: self.to().max(other.to()),
horiz: None,
}
}
} }
// groupAt // groupAt
#[inline] #[inline]
pub fn fragment<'a, 'b: 'a>(&'a self, text: RopeSlice<'b>) -> Cow<'b, str> { pub fn fragment<'a, 'b: 'a>(&'a self, text: RopeSlice<'b>) -> Cow<'b, str> {
Cow::from(text.slice(self.from()..self.to() + 1)) text.slice(self.from()..self.to()).into()
}
//--------------------------------
// Alignment methods.
/// Compute a possibly new range from this range, with its ends
/// shifted as needed to align with grapheme boundaries.
///
/// Zero-width ranges will always stay zero-width, and non-zero-width
/// ranges will never collapse to zero-width.
#[must_use]
pub fn grapheme_aligned(&self, slice: RopeSlice) -> Self {
use std::cmp::Ordering;
let (new_anchor, new_head) = match self.anchor.cmp(&self.head) {
Ordering::Equal => {
let pos = ensure_grapheme_boundary_prev(slice, self.anchor);
(pos, pos)
}
Ordering::Less => (
ensure_grapheme_boundary_prev(slice, self.anchor),
ensure_grapheme_boundary_next(slice, self.head),
),
Ordering::Greater => (
ensure_grapheme_boundary_next(slice, self.anchor),
ensure_grapheme_boundary_prev(slice, self.head),
),
};
Range {
anchor: new_anchor,
head: new_head,
horiz: if new_anchor == self.anchor {
self.horiz
} else {
None
},
}
}
/// Compute a possibly new range from this range, attempting to ensure
/// a minimum range width of 1 char by shifting the head in the forward
/// direction as needed.
///
/// This method will never shift the anchor, and will only shift the
/// head in the forward direction. Therefore, this method can fail
/// at ensuring the minimum width if and only if the passed range is
/// both zero-width and at the end of the `RopeSlice`.
///
/// If the input range is grapheme-boundary aligned, the returned range
/// will also be. Specifically, if the head needs to shift to achieve
/// the minimum width, it will shift to the next grapheme boundary.
#[must_use]
#[inline]
pub fn min_width_1(&self, slice: RopeSlice) -> Self {
if self.anchor == self.head {
Range {
anchor: self.anchor,
head: next_grapheme_boundary(slice, self.head),
horiz: self.horiz,
}
} else {
*self
}
}
//--------------------------------
// Block-cursor methods.
/// Gets the left-side position of the block cursor.
#[must_use]
#[inline]
pub fn cursor(self, text: RopeSlice) -> usize {
if self.head > self.anchor {
prev_grapheme_boundary(text, self.head)
} else {
self.head
}
}
/// Puts the left side of the block cursor at `char_idx`, optionally extending.
///
/// This follows "1-width" semantics, and therefore does a combination of anchor
/// and head moves to behave as if both the front and back of the range are 1-width
/// blocks
///
/// This method assumes that the range and `char_idx` are already properly
/// grapheme-aligned.
#[must_use]
#[inline]
pub fn put_cursor(self, text: RopeSlice, char_idx: usize, extend: bool) -> Range {
if extend {
let anchor = if self.head >= self.anchor && char_idx < self.anchor {
next_grapheme_boundary(text, self.anchor)
} else if self.head < self.anchor && char_idx >= self.anchor {
prev_grapheme_boundary(text, self.anchor)
} else {
self.anchor
};
if anchor <= char_idx {
Range::new(anchor, next_grapheme_boundary(text, char_idx))
} else {
Range::new(anchor, char_idx)
}
} else {
Range::point(char_idx)
}
}
/// The line number that the block-cursor is on.
#[inline]
#[must_use]
pub fn cursor_line(&self, text: RopeSlice) -> usize {
text.char_to_line(self.cursor(text))
}
}
impl From<(usize, usize)> for Range {
fn from(tuple: (usize, usize)) -> Self {
Self {
anchor: tuple.0,
head: tuple.1,
horiz: None,
}
} }
} }
@@ -138,14 +329,16 @@ pub struct Selection {
impl Selection { impl Selection {
// eq // eq
#[inline]
#[must_use] #[must_use]
pub fn primary(&self) -> Range { pub fn primary(&self) -> Range {
self.ranges[self.primary_index] self.ranges[self.primary_index]
} }
#[inline]
#[must_use] #[must_use]
pub fn cursor(&self) -> usize { pub fn primary_mut(&mut self) -> &mut Range {
self.primary().head &mut self.ranges[self.primary_index]
} }
/// Ensure selection containing only the primary selection. /// Ensure selection containing only the primary selection.
@@ -160,13 +353,12 @@ impl Selection {
} }
} }
/// Adds a new range to the selection and makes it the primary range.
pub fn push(mut self, range: Range) -> Self { pub fn push(mut self, range: Range) -> Self {
let index = self.ranges.len();
self.ranges.push(range); self.ranges.push(range);
self.set_primary_index(self.ranges().len() - 1);
Self::normalize(self.ranges, index) self.normalize()
} }
// replace_range
/// Map selections over a set of changes. Useful for adjusting the selection position after /// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document. /// applying changes to a document.
@@ -192,6 +384,11 @@ impl Selection {
self.primary_index self.primary_index
} }
pub fn set_primary_index(&mut self, idx: usize) {
assert!(idx < self.ranges.len());
self.primary_index = idx;
}
#[must_use] #[must_use]
/// Constructs a selection holding a single range. /// Constructs a selection holding a single range.
pub fn single(anchor: usize, head: usize) -> Self { pub fn single(anchor: usize, head: usize) -> Self {
@@ -210,77 +407,79 @@ impl Selection {
Self::single(pos, pos) Self::single(pos, pos)
} }
fn normalize(mut ranges: SmallVec<[Range; 1]>, mut primary_index: usize) -> Self { /// Normalizes a `Selection`.
let primary = ranges[primary_index]; fn normalize(mut self) -> Self {
ranges.sort_unstable_by_key(Range::from); let primary = self.ranges[self.primary_index];
primary_index = ranges.iter().position(|&range| range == primary).unwrap(); self.ranges.sort_unstable_by_key(Range::from);
self.primary_index = self
.ranges
.iter()
.position(|&range| range == primary)
.unwrap();
let mut result = SmallVec::with_capacity(ranges.len()); // approx let mut prev_i = 0;
for i in 1..self.ranges.len() {
// TODO: we could do with one vec by removing elements as we mutate if self.ranges[prev_i].overlaps(&self.ranges[i]) {
self.ranges[prev_i] = self.ranges[prev_i].merge(self.ranges[i]);
for (i, range) in ranges.into_iter().enumerate() {
// if previous value exists
if let Some(prev) = result.last_mut() {
// and we overlap it
// TODO: we used to simply check range.from() <(=) prev.to()
// avoiding two comparisons
if range.overlaps(prev) {
let from = prev.from();
let to = std::cmp::max(range.to(), prev.to());
if i <= primary_index {
primary_index -= 1
}
// merge into previous
if range.anchor > range.head {
prev.anchor = to;
prev.head = from;
} else { } else {
prev.anchor = from; prev_i += 1;
prev.head = to; self.ranges[prev_i] = self.ranges[i];
} }
continue; if i == self.primary_index {
self.primary_index = prev_i;
} }
} }
result.push(range) self.ranges.truncate(prev_i + 1);
}
Self { self
ranges: result,
primary_index,
}
} }
// TODO: consume an iterator or a vec to reduce allocations? // TODO: consume an iterator or a vec to reduce allocations?
#[must_use] #[must_use]
pub fn new(ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Self { pub fn new(ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Self {
assert!(!ranges.is_empty()); assert!(!ranges.is_empty());
debug_assert!(primary_index < ranges.len());
// fast path for a single selection (cursor) let mut selection = Self {
if ranges.len() == 1 {
return Self {
ranges, ranges,
primary_index: 0, primary_index,
}; };
}
if selection.ranges.len() > 1 {
// TODO: only normalize if needed (any ranges out of order) // TODO: only normalize if needed (any ranges out of order)
Self::normalize(ranges, primary_index) selection = selection.normalize();
} }
/// Takes a closure and maps each selection over the closure. selection
pub fn transform<F>(&self, f: F) -> Self }
/// Takes a closure and maps each `Range` over the closure.
pub fn transform<F>(mut self, f: F) -> Self
where where
F: Fn(Range) -> Range, F: Fn(Range) -> Range,
{ {
Self::new( for range in self.ranges.iter_mut() {
self.ranges.iter().copied().map(f).collect(), *range = f(*range)
self.primary_index, }
) self.normalize()
}
// Ensures the selection adheres to the following invariants:
// 1. All ranges are grapheme aligned.
// 2. All ranges are at least 1 character wide, unless at the
// very end of the document.
// 3. Ranges are non-overlapping.
// 4. Ranges are sorted by their position in the text.
pub fn ensure_invariants(self, text: RopeSlice) -> Self {
self.transform(|r| r.min_width_1(text).grapheme_aligned(text))
.normalize()
}
/// Transforms the selection into all of the left-side head positions,
/// using block-cursor semantics.
pub fn cursors(self, text: RopeSlice) -> Self {
self.transform(|range| Range::point(range.cursor(text)))
} }
pub fn fragments<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = Cow<str>> + 'a { pub fn fragments<'a>(&'a self, text: RopeSlice<'a>) -> impl Iterator<Item = Cow<str>> + 'a {
@@ -338,17 +537,15 @@ pub fn select_on_matches(
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet // TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
let fragment = sel.fragment(text); let fragment = sel.fragment(text);
let mut sel_start = sel.from(); let sel_start = sel.from();
let sel_end = sel.to(); let start_byte = text.char_to_byte(sel_start);
let mut start_byte = text.char_to_byte(sel_start);
for mat in regex.find_iter(&fragment) { for mat in regex.find_iter(&fragment) {
// TODO: retain range direction // TODO: retain range direction
let start = text.byte_to_char(start_byte + mat.start()); let start = text.byte_to_char(start_byte + mat.start());
let end = text.byte_to_char(start_byte + mat.end()); let end = text.byte_to_char(start_byte + mat.end());
result.push(Range::new(start, end - 1)); result.push(Range::new(start, end));
} }
} }
@@ -369,25 +566,30 @@ pub fn split_on_matches(
let mut result = SmallVec::with_capacity(selection.len()); let mut result = SmallVec::with_capacity(selection.len());
for sel in selection { for sel in selection {
// Special case: zero-width selection.
if sel.from() == sel.to() {
result.push(*sel);
continue;
}
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet // TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
let fragment = sel.fragment(text); let fragment = sel.fragment(text);
let mut sel_start = sel.from(); let sel_start = sel.from();
let sel_end = sel.to(); let sel_end = sel.to();
let mut start_byte = text.char_to_byte(sel_start); let start_byte = text.char_to_byte(sel_start);
let mut start = sel_start; let mut start = sel_start;
for mat in regex.find_iter(&fragment) { for mat in regex.find_iter(&fragment) {
// TODO: retain range direction // TODO: retain range direction
let end = text.byte_to_char(start_byte + mat.start()); let end = text.byte_to_char(start_byte + mat.start());
result.push(Range::new(start, end.saturating_sub(1))); result.push(Range::new(start, end));
start = text.byte_to_char(start_byte + mat.end()); start = text.byte_to_char(start_byte + mat.end());
} }
if start <= sel_end { if start < sel_end {
result.push(Range::new(start, sel_end)); result.push(Range::new(start, sel_end));
} }
} }
@@ -399,11 +601,12 @@ pub fn split_on_matches(
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::Rope;
#[test] #[test]
#[should_panic] #[should_panic]
fn test_new_empty() { fn test_new_empty() {
let sel = Selection::new(smallvec![], 0); let _ = Selection::new(smallvec![], 0);
} }
#[test] #[test]
@@ -430,6 +633,22 @@ mod test {
.join(","); .join(",");
assert_eq!(res, "0/6,6/7,7/8,9/13,13/14"); assert_eq!(res, "0/6,6/7,7/8,9/13,13/14");
// it correctly calculates a new primary index
let sel = Selection::new(
smallvec![Range::new(0, 2), Range::new(1, 5), Range::new(4, 7)],
2,
);
let res = sel
.ranges
.into_iter()
.map(|range| format!("{}/{}", range.anchor, range.head))
.collect::<Vec<String>>()
.join(",");
assert_eq!(res, "0/7");
assert_eq!(sel.primary_index, 0);
} }
#[test] #[test]
@@ -452,7 +671,7 @@ mod test {
.collect::<Vec<String>>() .collect::<Vec<String>>()
.join(","); .join(",");
assert_eq!(res, "8/10,10/12"); assert_eq!(res, "8/10,10/12,12/12");
} }
#[test] #[test]
@@ -466,9 +685,214 @@ mod test {
assert_eq!(range.contains(13), false); assert_eq!(range.contains(13), false);
let range = Range::new(9, 6); let range = Range::new(9, 6);
assert_eq!(range.contains(9), true); assert_eq!(range.contains(9), false);
assert_eq!(range.contains(7), true); assert_eq!(range.contains(7), true);
assert_eq!(range.contains(6), false); assert_eq!(range.contains(6), true);
}
#[test]
fn test_overlaps() {
fn overlaps(a: (usize, usize), b: (usize, usize)) -> bool {
Range::new(a.0, a.1).overlaps(&Range::new(b.0, b.1))
}
// Two non-zero-width ranges, no overlap.
assert!(!overlaps((0, 3), (3, 6)));
assert!(!overlaps((0, 3), (6, 3)));
assert!(!overlaps((3, 0), (3, 6)));
assert!(!overlaps((3, 0), (6, 3)));
assert!(!overlaps((3, 6), (0, 3)));
assert!(!overlaps((3, 6), (3, 0)));
assert!(!overlaps((6, 3), (0, 3)));
assert!(!overlaps((6, 3), (3, 0)));
// Two non-zero-width ranges, overlap.
assert!(overlaps((0, 4), (3, 6)));
assert!(overlaps((0, 4), (6, 3)));
assert!(overlaps((4, 0), (3, 6)));
assert!(overlaps((4, 0), (6, 3)));
assert!(overlaps((3, 6), (0, 4)));
assert!(overlaps((3, 6), (4, 0)));
assert!(overlaps((6, 3), (0, 4)));
assert!(overlaps((6, 3), (4, 0)));
// Zero-width and non-zero-width range, no overlap.
assert!(!overlaps((0, 3), (3, 3)));
assert!(!overlaps((3, 0), (3, 3)));
assert!(!overlaps((3, 3), (0, 3)));
assert!(!overlaps((3, 3), (3, 0)));
// Zero-width and non-zero-width range, overlap.
assert!(overlaps((1, 4), (1, 1)));
assert!(overlaps((4, 1), (1, 1)));
assert!(overlaps((1, 1), (1, 4)));
assert!(overlaps((1, 1), (4, 1)));
assert!(overlaps((1, 4), (3, 3)));
assert!(overlaps((4, 1), (3, 3)));
assert!(overlaps((3, 3), (1, 4)));
assert!(overlaps((3, 3), (4, 1)));
// Two zero-width ranges, no overlap.
assert!(!overlaps((0, 0), (1, 1)));
assert!(!overlaps((1, 1), (0, 0)));
// Two zero-width ranges, overlap.
assert!(overlaps((1, 1), (1, 1)));
}
#[test]
fn test_graphem_aligned() {
let r = Rope::from_str("\r\nHi\r\n");
let s = r.slice(..);
// Zero-width.
assert_eq!(Range::new(0, 0).grapheme_aligned(s), Range::new(0, 0));
assert_eq!(Range::new(1, 1).grapheme_aligned(s), Range::new(0, 0));
assert_eq!(Range::new(2, 2).grapheme_aligned(s), Range::new(2, 2));
assert_eq!(Range::new(3, 3).grapheme_aligned(s), Range::new(3, 3));
assert_eq!(Range::new(4, 4).grapheme_aligned(s), Range::new(4, 4));
assert_eq!(Range::new(5, 5).grapheme_aligned(s), Range::new(4, 4));
assert_eq!(Range::new(6, 6).grapheme_aligned(s), Range::new(6, 6));
// Forward.
assert_eq!(Range::new(0, 1).grapheme_aligned(s), Range::new(0, 2));
assert_eq!(Range::new(1, 2).grapheme_aligned(s), Range::new(0, 2));
assert_eq!(Range::new(2, 3).grapheme_aligned(s), Range::new(2, 3));
assert_eq!(Range::new(3, 4).grapheme_aligned(s), Range::new(3, 4));
assert_eq!(Range::new(4, 5).grapheme_aligned(s), Range::new(4, 6));
assert_eq!(Range::new(5, 6).grapheme_aligned(s), Range::new(4, 6));
assert_eq!(Range::new(0, 2).grapheme_aligned(s), Range::new(0, 2));
assert_eq!(Range::new(1, 3).grapheme_aligned(s), Range::new(0, 3));
assert_eq!(Range::new(2, 4).grapheme_aligned(s), Range::new(2, 4));
assert_eq!(Range::new(3, 5).grapheme_aligned(s), Range::new(3, 6));
assert_eq!(Range::new(4, 6).grapheme_aligned(s), Range::new(4, 6));
// Reverse.
assert_eq!(Range::new(1, 0).grapheme_aligned(s), Range::new(2, 0));
assert_eq!(Range::new(2, 1).grapheme_aligned(s), Range::new(2, 0));
assert_eq!(Range::new(3, 2).grapheme_aligned(s), Range::new(3, 2));
assert_eq!(Range::new(4, 3).grapheme_aligned(s), Range::new(4, 3));
assert_eq!(Range::new(5, 4).grapheme_aligned(s), Range::new(6, 4));
assert_eq!(Range::new(6, 5).grapheme_aligned(s), Range::new(6, 4));
assert_eq!(Range::new(2, 0).grapheme_aligned(s), Range::new(2, 0));
assert_eq!(Range::new(3, 1).grapheme_aligned(s), Range::new(3, 0));
assert_eq!(Range::new(4, 2).grapheme_aligned(s), Range::new(4, 2));
assert_eq!(Range::new(5, 3).grapheme_aligned(s), Range::new(6, 3));
assert_eq!(Range::new(6, 4).grapheme_aligned(s), Range::new(6, 4));
}
#[test]
fn test_min_width_1() {
let r = Rope::from_str("\r\nHi\r\n");
let s = r.slice(..);
// Zero-width.
assert_eq!(Range::new(0, 0).min_width_1(s), Range::new(0, 2));
assert_eq!(Range::new(1, 1).min_width_1(s), Range::new(1, 2));
assert_eq!(Range::new(2, 2).min_width_1(s), Range::new(2, 3));
assert_eq!(Range::new(3, 3).min_width_1(s), Range::new(3, 4));
assert_eq!(Range::new(4, 4).min_width_1(s), Range::new(4, 6));
assert_eq!(Range::new(5, 5).min_width_1(s), Range::new(5, 6));
assert_eq!(Range::new(6, 6).min_width_1(s), Range::new(6, 6));
// Forward.
assert_eq!(Range::new(0, 1).min_width_1(s), Range::new(0, 1));
assert_eq!(Range::new(1, 2).min_width_1(s), Range::new(1, 2));
assert_eq!(Range::new(2, 3).min_width_1(s), Range::new(2, 3));
assert_eq!(Range::new(3, 4).min_width_1(s), Range::new(3, 4));
assert_eq!(Range::new(4, 5).min_width_1(s), Range::new(4, 5));
assert_eq!(Range::new(5, 6).min_width_1(s), Range::new(5, 6));
// Reverse.
assert_eq!(Range::new(1, 0).min_width_1(s), Range::new(1, 0));
assert_eq!(Range::new(2, 1).min_width_1(s), Range::new(2, 1));
assert_eq!(Range::new(3, 2).min_width_1(s), Range::new(3, 2));
assert_eq!(Range::new(4, 3).min_width_1(s), Range::new(4, 3));
assert_eq!(Range::new(5, 4).min_width_1(s), Range::new(5, 4));
assert_eq!(Range::new(6, 5).min_width_1(s), Range::new(6, 5));
}
#[test]
fn test_line_range() {
let r = Rope::from_str("\r\nHi\r\nthere!");
let s = r.slice(..);
// Zero-width ranges.
assert_eq!(Range::new(0, 0).line_range(s), (0, 0));
assert_eq!(Range::new(1, 1).line_range(s), (0, 0));
assert_eq!(Range::new(2, 2).line_range(s), (1, 1));
assert_eq!(Range::new(3, 3).line_range(s), (1, 1));
// Forward ranges.
assert_eq!(Range::new(0, 1).line_range(s), (0, 0));
assert_eq!(Range::new(0, 2).line_range(s), (0, 0));
assert_eq!(Range::new(0, 3).line_range(s), (0, 1));
assert_eq!(Range::new(1, 2).line_range(s), (0, 0));
assert_eq!(Range::new(2, 3).line_range(s), (1, 1));
assert_eq!(Range::new(3, 8).line_range(s), (1, 2));
assert_eq!(Range::new(0, 12).line_range(s), (0, 2));
// Reverse ranges.
assert_eq!(Range::new(1, 0).line_range(s), (0, 0));
assert_eq!(Range::new(2, 0).line_range(s), (0, 0));
assert_eq!(Range::new(3, 0).line_range(s), (0, 1));
assert_eq!(Range::new(2, 1).line_range(s), (0, 0));
assert_eq!(Range::new(3, 2).line_range(s), (1, 1));
assert_eq!(Range::new(8, 3).line_range(s), (1, 2));
assert_eq!(Range::new(12, 0).line_range(s), (0, 2));
}
#[test]
fn test_cursor() {
let r = Rope::from_str("\r\nHi\r\nthere!");
let s = r.slice(..);
// Zero-width ranges.
assert_eq!(Range::new(0, 0).cursor(s), 0);
assert_eq!(Range::new(2, 2).cursor(s), 2);
assert_eq!(Range::new(3, 3).cursor(s), 3);
// Forward ranges.
assert_eq!(Range::new(0, 2).cursor(s), 0);
assert_eq!(Range::new(0, 3).cursor(s), 2);
assert_eq!(Range::new(3, 6).cursor(s), 4);
// Reverse ranges.
assert_eq!(Range::new(2, 0).cursor(s), 0);
assert_eq!(Range::new(6, 2).cursor(s), 2);
assert_eq!(Range::new(6, 3).cursor(s), 3);
}
#[test]
fn test_put_cursor() {
let r = Rope::from_str("\r\nHi\r\nthere!");
let s = r.slice(..);
// Zero-width ranges.
assert_eq!(Range::new(0, 0).put_cursor(s, 0, true), Range::new(0, 2));
assert_eq!(Range::new(0, 0).put_cursor(s, 2, true), Range::new(0, 3));
assert_eq!(Range::new(2, 3).put_cursor(s, 4, true), Range::new(2, 6));
assert_eq!(Range::new(2, 8).put_cursor(s, 4, true), Range::new(2, 6));
assert_eq!(Range::new(8, 8).put_cursor(s, 4, true), Range::new(9, 4));
// Forward ranges.
assert_eq!(Range::new(3, 6).put_cursor(s, 0, true), Range::new(4, 0));
assert_eq!(Range::new(3, 6).put_cursor(s, 2, true), Range::new(4, 2));
assert_eq!(Range::new(3, 6).put_cursor(s, 3, true), Range::new(3, 4));
assert_eq!(Range::new(3, 6).put_cursor(s, 4, true), Range::new(3, 6));
assert_eq!(Range::new(3, 6).put_cursor(s, 6, true), Range::new(3, 7));
assert_eq!(Range::new(3, 6).put_cursor(s, 8, true), Range::new(3, 9));
// Reverse ranges.
assert_eq!(Range::new(6, 3).put_cursor(s, 0, true), Range::new(6, 0));
assert_eq!(Range::new(6, 3).put_cursor(s, 2, true), Range::new(6, 2));
assert_eq!(Range::new(6, 3).put_cursor(s, 3, true), Range::new(6, 3));
assert_eq!(Range::new(6, 3).put_cursor(s, 4, true), Range::new(6, 4));
assert_eq!(Range::new(6, 3).put_cursor(s, 6, true), Range::new(4, 7));
assert_eq!(Range::new(6, 3).put_cursor(s, 8, true), Range::new(4, 9));
} }
#[test] #[test]
@@ -477,24 +901,35 @@ mod test {
let text = Rope::from(" abcd efg wrs xyz 123 456"); let text = Rope::from(" abcd efg wrs xyz 123 456");
let selection = Selection::new(smallvec![Range::new(0, 8), Range::new(10, 19),], 0); let selection = Selection::new(smallvec![Range::new(0, 9), Range::new(11, 20),], 0);
let result = split_on_matches(text.slice(..), &selection, &Regex::new(r"\s+").unwrap()); let result = split_on_matches(text.slice(..), &selection, &Regex::new(r"\s+").unwrap());
assert_eq!( assert_eq!(
result.ranges(), result.ranges(),
&[ &[
Range::new(0, 3), // TODO: rather than this behavior, maybe we want it
Range::new(5, 7), // to be based on which side is the anchor?
Range::new(10, 11), //
Range::new(15, 17), // We get a leading zero-width range when there's
Range::new(19, 19), // a leading match because ranges are inclusive on
// the left. Imagine, for example, if the entire
// selection range were matched: you'd still want
// at least one range to remain after the split.
Range::new(0, 0),
Range::new(1, 5),
Range::new(6, 9),
Range::new(11, 13),
Range::new(16, 19),
// In contrast to the comment above, there is no
// _trailing_ zero-width range despite the trailing
// match, because ranges are exclusive on the right.
] ]
); );
assert_eq!( assert_eq!(
result.fragments(text.slice(..)).collect::<Vec<_>>(), result.fragments(text.slice(..)).collect::<Vec<_>>(),
&["abcd", "efg", "rs", "xyz", "1"] &["", "abcd", "efg", "rs", "xyz"]
); );
} }
} }

View File

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

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

@@ -0,0 +1,286 @@
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);
if text.len_chars() < 2 || pos >= text.len_chars() {
return None;
}
if open == close {
if Some(open) == text.get_char(pos) {
// Special case: cursor is directly on a matching char.
match pos {
0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
_ if (pos + 1) == text.len_chars() => {
Some((search::find_nth_prev(text, open, pos, n)?, pos))
}
// We return no match because there's no way to know which
// side of the char we should be searching on.
_ => None,
}
} else {
Some((
search::find_nth_prev(text, open, pos, n)?,
search::find_nth_next(text, close, pos, n)?,
))
}
} else {
Some((
find_nth_open_pair(text, open, close, pos, n)?,
find_nth_close_pair(text, open, close, pos, n)?,
))
}
}
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)));
assert_eq!(find_nth_pairs_pos(slice, '(', 10, 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)));
// cursor on the quotes
assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
// this is the best we can do since opening and closing pairs are same
assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 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,34 +1,45 @@
use crate::{regex::Regex, Change, Rope, RopeSlice, Transaction}; use crate::{
pub use helix_syntax::{get_language, get_language_name, Lang}; chars::char_is_line_ending,
regex::Regex,
transaction::{ChangeSet, Operation},
Rope, RopeSlice, Tendril,
};
pub use helix_syntax::get_language;
use arc_swap::ArcSwap;
use std::{ use std::{
borrow::Cow, borrow::Cow,
cell::RefCell, cell::RefCell,
collections::{HashMap, HashSet}, collections::{HashMap, HashSet},
path::{Path, PathBuf}, fmt,
path::Path,
sync::Arc, sync::Arc,
}; };
use once_cell::sync::{Lazy, OnceCell}; use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Configuration { pub struct Configuration {
pub language: Vec<LanguageConfiguration>, pub language: Vec<LanguageConfiguration>,
} }
// largely based on tree-sitter/cli/src/loader.rs // largely based on tree-sitter/cli/src/loader.rs
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LanguageConfiguration { pub struct LanguageConfiguration {
#[serde(rename = "name")] #[serde(rename = "name")]
pub(crate) language_id: Lang, pub(crate) language_id: String,
pub scope: String, // source.rust pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc> pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml> pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>,
pub config: Option<String>,
// pub path: PathBuf, #[serde(default)]
// root_path for tree-sitter (^) pub auto_format: bool,
// content_regex // content_regex
// injection_regex // injection_regex
@@ -46,7 +57,7 @@ pub struct LanguageConfiguration {
pub(crate) indent_query: OnceCell<Option<IndentQuery>>, pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
} }
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration { pub struct LanguageServerConfiguration {
pub command: String, pub command: String,
@@ -55,14 +66,14 @@ pub struct LanguageServerConfiguration {
pub args: Vec<String>, pub args: Vec<String>,
} }
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct IndentationConfiguration { pub struct IndentationConfiguration {
pub tab_width: usize, pub tab_width: usize,
pub unit: String, pub unit: String,
} }
#[derive(Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct IndentQuery { pub struct IndentQuery {
#[serde(default)] #[serde(default)]
@@ -73,41 +84,14 @@ pub struct IndentQuery {
pub outdent: HashSet<String>, pub outdent: HashSet<String>,
} }
#[cfg(not(feature = "embed_runtime"))]
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> { fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let root = crate::runtime_dir(); let path = crate::RUNTIME_DIR
let path = root.join("queries").join(language).join(filename); .join("queries")
.join(language)
.join(filename);
std::fs::read_to_string(&path) 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 { fn read_query(language: &str, filename: &str) -> String {
static INHERITS_REGEX: Lazy<Regex> = static INHERITS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap()); Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
@@ -140,10 +124,8 @@ fn read_query(language: &str, filename: &str) -> String {
} }
impl LanguageConfiguration { impl LanguageConfiguration {
pub fn highlight_config(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> { fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfiguration>> {
self.highlight_config let language = self.language_id.to_ascii_lowercase();
.get_or_init(|| {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let highlights_query = read_query(&language, "highlights.scm"); let highlights_query = read_query(&language, "highlights.scm");
// always highlight syntax errors // always highlight syntax errors
@@ -151,33 +133,47 @@ impl LanguageConfiguration {
let injections_query = read_query(&language, "injections.scm"); let injections_query = read_query(&language, "injections.scm");
let locals_query = ""; let locals_query = read_query(&language, "locals.scm");
if highlights_query.is_empty() { if highlights_query.is_empty() {
None None
} else { } else {
let language = get_language(self.language_id); let language = get_language(&crate::RUNTIME_DIR, &self.language_id).ok()?;
let mut config = HighlightConfiguration::new( let config = HighlightConfiguration::new(
language, language,
&highlights_query, &highlights_query,
&injections_query, &injections_query,
locals_query, &locals_query,
) )
.unwrap(); // TODO: no unwrap .unwrap(); // TODO: no unwrap
config.configure(scopes); config.configure(scopes);
Some(Arc::new(config)) 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(|| self.initialize_highlight(scopes))
.clone() .clone()
} }
pub fn is_highlight_initialized(&self) -> bool {
self.highlight_config.get().is_some()
}
pub fn indent_query(&self) -> Option<&IndentQuery> { pub fn indent_query(&self) -> Option<&IndentQuery> {
self.indent_query self.indent_query
.get_or_init(|| { .get_or_init(|| {
let language = get_language_name(self.language_id).to_ascii_lowercase(); let language = self.language_id.to_ascii_lowercase();
let toml = load_runtime_file(&language, "indents.toml").ok()?; let toml = load_runtime_file(&language, "indents.toml").ok()?;
toml::from_slice(&toml.as_bytes()).ok() toml::from_slice(toml.as_bytes()).ok()
}) })
.as_ref() .as_ref()
} }
@@ -187,21 +183,18 @@ impl LanguageConfiguration {
} }
} }
pub static LOADER: OnceCell<Loader> = OnceCell::new(); #[derive(Debug)]
pub struct Loader { pub struct Loader {
// highlight_names ? // highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>, language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize> language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
scopes: Vec<String>,
} }
impl Loader { impl Loader {
pub fn new(config: Configuration, scopes: Vec<String>) -> Self { pub fn new(config: Configuration) -> Self {
let mut loader = Self { let mut loader = Self {
language_configs: Vec::new(), language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(), language_config_ids_by_file_type: HashMap::new(),
scopes,
}; };
for config in config.language { for config in config.language {
@@ -221,10 +214,6 @@ impl Loader {
loader loader
} }
pub fn scopes(&self) -> &[String] {
&self.scopes
}
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> { pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
// Find all the language configurations that match this file name // Find all the language configurations that match this file name
// or a suffix of the file name. // or a suffix of the file name.
@@ -249,6 +238,10 @@ impl Loader {
.find(|config| config.scope == scope) .find(|config| config.scope == scope)
.cloned() .cloned()
} }
pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter()
}
} }
pub struct TsParser { pub struct TsParser {
@@ -256,6 +249,12 @@ pub struct TsParser {
cursors: Vec<QueryCursor>, 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? // could also just use a pool, or a single instance?
thread_local! { thread_local! {
pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser { pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
@@ -264,6 +263,7 @@ thread_local! {
}) })
} }
#[derive(Debug)]
pub struct Syntax { pub struct Syntax {
config: Arc<HighlightConfiguration>, config: Arc<HighlightConfiguration>,
@@ -305,7 +305,8 @@ impl Syntax {
// update root layer // update root layer
PARSER.with(|ts_parser| { PARSER.with(|ts_parser| {
syntax.root_layer.parse( // TODO: handle the returned `Result` properly.
let _ = syntax.root_layer.parse(
&mut ts_parser.borrow_mut(), &mut ts_parser.borrow_mut(),
&syntax.config, &syntax.config,
source, source,
@@ -360,13 +361,17 @@ impl Syntax {
source: RopeSlice<'a>, source: RopeSlice<'a>,
range: Option<std::ops::Range<usize>>, range: Option<std::ops::Range<usize>>,
cancellation_flag: Option<&'a AtomicUsize>, cancellation_flag: Option<&'a AtomicUsize>,
mut injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, injection_callback: impl FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a { ) -> impl Iterator<Item = Result<HighlightEvent, Error>> + 'a {
// The `captures` iterator borrows the `Tree` and the `QueryCursor`, which // The `captures` iterator borrows the `Tree` and the `QueryCursor`, which
// prevents them from being moved. But both of these values are really just // prevents them from being moved. But both of these values are really just
// pointers, so it's actually ok to move them. // 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 tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) }; let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
@@ -440,6 +445,7 @@ impl Syntax {
// buffer_range_for_scope_at_pos // buffer_range_for_scope_at_pos
} }
#[derive(Debug)]
pub struct LanguageLayer { pub struct LanguageLayer {
// mode // mode
// grammar // grammar
@@ -447,12 +453,6 @@ pub struct LanguageLayer {
pub(crate) tree: Option<Tree>, pub(crate) tree: Option<Tree>,
} }
use crate::{
coords_at_pos,
transaction::{ChangeSet, Operation},
Tendril,
};
impl LanguageLayer { impl LanguageLayer {
// pub fn new() -> Self { // pub fn new() -> Self {
// Self { tree: None } // Self { tree: None }
@@ -468,8 +468,8 @@ impl LanguageLayer {
ts_parser: &mut TsParser, ts_parser: &mut TsParser,
config: &HighlightConfiguration, config: &HighlightConfiguration,
source: &Rope, source: &Rope,
mut depth: usize, _depth: usize,
mut ranges: Vec<Range>, ranges: Vec<Range>,
) -> Result<(), Error> { ) -> Result<(), Error> {
if ts_parser.parser.set_included_ranges(&ranges).is_ok() { if ts_parser.parser.set_included_ranges(&ranges).is_ok() {
ts_parser ts_parser
@@ -540,7 +540,6 @@ impl LanguageLayer {
) -> Vec<tree_sitter::InputEdit> { ) -> Vec<tree_sitter::InputEdit> {
use Operation::*; use Operation::*;
let mut old_pos = 0; let mut old_pos = 0;
let mut new_pos = 0;
let mut edits = Vec::new(); let mut edits = Vec::new();
@@ -563,9 +562,10 @@ impl LanguageLayer {
mut column, mut column,
} = point; } = point;
// TODO: there should be a better way here // TODO: there should be a better way here.
for ch in text.bytes() { let mut chars = text.chars().peekable();
if ch == b'\n' { while let Some(ch) = chars.next() {
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
row += 1; row += 1;
column = 0; column = 0;
} else { } else {
@@ -583,9 +583,7 @@ impl LanguageLayer {
let mut old_end = old_pos + len; let mut old_end = old_pos + len;
match change { match change {
Retain(_) => { Retain(_) => {}
new_pos += len;
}
Delete(_) => { Delete(_) => {
let (start_byte, start_position) = point_at_pos(old_text, old_pos); let (start_byte, start_position) = point_at_pos(old_text, old_pos);
let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end); let (old_end_byte, old_end_position) = point_at_pos(old_text, old_end);
@@ -609,8 +607,6 @@ impl LanguageLayer {
Insert(s) => { Insert(s) => {
let (start_byte, start_position) = point_at_pos(old_text, old_pos); let (start_byte, start_position) = point_at_pos(old_text, old_pos);
let ins = s.chars().count();
// a subsequent delete means a replace, consume it // a subsequent delete means a replace, consume it
if let Some(Delete(len)) = iter.peek() { if let Some(Delete(len)) = iter.peek() {
old_end = old_pos + len; old_end = old_pos + len;
@@ -638,8 +634,6 @@ impl LanguageLayer {
new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over) new_end_position: traverse(start_position, s), // old pos + chars, newlines matter too (iter over)
}); });
} }
new_pos += ins;
} }
} }
old_pos = old_end; old_pos = old_end;
@@ -662,8 +656,10 @@ impl LanguageLayer {
let edits = Self::generate_edits(old_source.slice(..), changeset); let edits = Self::generate_edits(old_source.slice(..), changeset);
// Notify the tree about all the changes // Notify the tree about all the changes
for edit in edits { for edit in edits.iter().rev() {
self.tree.as_mut().unwrap().edit(&edit); // apply the edits in reverse. If we applied them in order then edit 1 would disrupt
// the positioning of edit 2
self.tree.as_mut().unwrap().edit(edit);
} }
self.parse( self.parse(
@@ -748,13 +744,14 @@ pub enum HighlightEvent {
/// Contains the data neeeded to higlight code written in a particular language. /// Contains the data neeeded to higlight code written in a particular language.
/// ///
/// This struct is immutable and can be shared between threads. /// This struct is immutable and can be shared between threads.
#[derive(Debug)]
pub struct HighlightConfiguration { pub struct HighlightConfiguration {
pub language: Grammar, pub language: Grammar,
pub query: Query, pub query: Query,
combined_injections_query: Option<Query>, combined_injections_query: Option<Query>,
locals_pattern_index: usize, locals_pattern_index: usize,
highlights_pattern_index: usize, highlights_pattern_index: usize,
highlight_indices: Vec<Option<Highlight>>, highlight_indices: ArcSwap<Vec<Option<Highlight>>>,
non_local_variable_patterns: Vec<bool>, non_local_variable_patterns: Vec<bool>,
injection_content_capture_index: Option<u32>, injection_content_capture_index: Option<u32>,
injection_language_capture_index: Option<u32>, injection_language_capture_index: Option<u32>,
@@ -778,6 +775,7 @@ struct LocalScope<'a> {
local_defs: Vec<LocalDef<'a>>, local_defs: Vec<LocalDef<'a>>,
} }
#[derive(Debug)]
struct HighlightIter<'a, 'tree: 'a, F> struct HighlightIter<'a, 'tree: 'a, F>
where where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
@@ -803,6 +801,12 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
depth: usize, 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 { impl HighlightConfiguration {
/// Creates a `HighlightConfiguration` for a given `Grammar` and set of highlighting /// Creates a `HighlightConfiguration` for a given `Grammar` and set of highlighting
/// queries. /// queries.
@@ -899,7 +903,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 { Ok(Self {
language, language,
query, query,
@@ -932,17 +936,20 @@ impl HighlightConfiguration {
/// ///
/// When highlighting, results are returned as `Highlight` values, which contain the index /// When highlighting, results are returned as `Highlight` values, which contain the index
/// of the matched highlight this list of highlight names. /// 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(); let mut capture_parts = Vec::new();
self.highlight_indices.clear(); let indices: Vec<_> = self
self.highlight_indices .query
.extend(self.query.capture_names().iter().map(move |capture_name| { .capture_names()
.iter()
.map(move |capture_name| {
capture_parts.clear(); capture_parts.clear();
capture_parts.extend(capture_name.split('.')); capture_parts.extend(capture_name.split('.'));
let mut best_index = None; let mut best_index = None;
let mut best_match_len = 0; let mut best_match_len = 0;
for (i, recognized_name) in recognized_names.iter().enumerate() { for (i, recognized_name) in recognized_names.iter().enumerate() {
let recognized_name = recognized_name;
let mut len = 0; let mut len = 0;
let mut matches = true; let mut matches = true;
for part in recognized_name.split('.') { for part in recognized_name.split('.') {
@@ -958,7 +965,10 @@ impl HighlightConfiguration {
} }
} }
best_index.map(Highlight) best_index.map(Highlight)
})); })
.collect();
self.highlight_indices.store(Arc::new(indices));
} }
} }
@@ -1439,7 +1449,6 @@ where
// local scope at the top of the scope stack. // local scope at the top of the scope stack.
else if Some(capture.index) == layer.config.local_def_capture_index { else if Some(capture.index) == layer.config.local_def_capture_index {
reference_highlight = None; reference_highlight = None;
definition_highlight = None;
let scope = layer.scope_stack.last_mut().unwrap(); let scope = layer.scope_stack.last_mut().unwrap();
let mut value_range = 0..0; let mut value_range = 0..0;
@@ -1537,7 +1546,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 // If this node represents a local definition, then store the current
// highlight value on the local scope entry representing this node. // highlight value on the local scope entry representing this node.
@@ -1603,14 +1612,153 @@ fn injection_for_match<'a>(
(language_name, content_node, include_children) (language_name, content_node, include_children)
} }
fn shrink_and_clear<T>(vec: &mut Vec<T>, capacity: usize) { // fn shrink_and_clear<T>(vec: &mut Vec<T>, capacity: usize) {
if vec.len() > capacity { // if vec.len() > capacity {
vec.truncate(capacity); // vec.truncate(capacity);
vec.shrink_to_fit(); // vec.shrink_to_fit();
// }
// vec.clear();
// }
pub struct Merge<I> {
iter: I,
spans: Box<dyn Iterator<Item = (usize, std::ops::Range<usize>)>>,
next_event: Option<HighlightEvent>,
next_span: Option<(usize, std::ops::Range<usize>)>,
queue: Vec<HighlightEvent>,
} }
vec.clear();
/// Merge a list of spans into the highlight event stream.
pub fn merge<I: Iterator<Item = HighlightEvent>>(
iter: I,
spans: Vec<(usize, std::ops::Range<usize>)>,
) -> Merge<I> {
let spans = Box::new(spans.into_iter());
let mut merge = Merge {
iter,
spans,
next_event: None,
next_span: None,
queue: Vec::new(),
};
merge.next_event = merge.iter.next();
merge.next_span = merge.spans.next();
merge
} }
impl<I: Iterator<Item = HighlightEvent>> Iterator for Merge<I> {
type Item = HighlightEvent;
fn next(&mut self) -> Option<Self::Item> {
use HighlightEvent::*;
if let Some(event) = self.queue.pop() {
return Some(event);
}
loop {
match (self.next_event, &self.next_span) {
// this happens when range is partially or fully offscreen
(Some(Source { start, .. }), Some((span, range))) if start > range.start => {
if start > range.end {
self.next_span = self.spans.next();
} else {
self.next_span = Some((*span, start..range.end));
};
}
_ => break,
}
}
match (self.next_event, &self.next_span) {
(Some(HighlightStart(i)), _) => {
self.next_event = self.iter.next();
Some(HighlightStart(i))
}
(Some(HighlightEnd), _) => {
self.next_event = self.iter.next();
Some(HighlightEnd)
}
(Some(Source { start, end }), Some((_, range))) if start < range.start => {
let intersect = range.start.min(end);
let event = Source {
start,
end: intersect,
};
if end == intersect {
// the event is complete
self.next_event = self.iter.next();
} else {
// subslice the event
self.next_event = Some(Source {
start: intersect,
end,
});
};
Some(event)
}
(Some(Source { start, end }), Some((span, range))) if start == range.start => {
let intersect = range.end.min(end);
let event = HighlightStart(Highlight(*span));
// enqueue in reverse order
self.queue.push(HighlightEnd);
self.queue.push(Source {
start,
end: intersect,
});
if end == intersect {
// the event is complete
self.next_event = self.iter.next();
} else {
// subslice the event
self.next_event = Some(Source {
start: intersect,
end,
});
};
if intersect == range.end {
self.next_span = self.spans.next();
} else {
self.next_span = Some((*span, intersect..range.end));
}
Some(event)
}
(Some(event), None) => {
self.next_event = self.iter.next();
Some(event)
}
// Can happen if cursor at EOF and/or diagnostic reaches past the end.
// We need to actually emit events for the cursor-at-EOF situation,
// even though the range is past the end of the text. This needs to be
// handled appropriately by the drawing code by not assuming that
// all `Source` events point to valid indices in the rope.
(None, Some((span, range))) => {
let event = HighlightStart(Highlight(*span));
self.queue.push(HighlightEnd);
self.queue.push(Source {
start: range.start,
end: range.end,
});
self.next_span = self.spans.next();
Some(event)
}
(None, None) => None,
e => unreachable!("{:?}", e),
}
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::{Rope, Transaction};
#[test] #[test]
fn test_parser() { fn test_parser() {
let highlight_names: Vec<String> = [ let highlight_names: Vec<String> = [
@@ -1638,8 +1786,8 @@ fn test_parser() {
.map(String::from) .map(String::from)
.collect(); .collect();
let language = get_language(Lang::Rust); let language = get_language(&crate::RUNTIME_DIR, "Rust").unwrap();
let mut config = HighlightConfiguration::new( let config = HighlightConfiguration::new(
language, language,
&std::fs::read_to_string( &std::fs::read_to_string(
"../helix-syntax/languages/tree-sitter-rust/queries/highlights.scm", "../helix-syntax/languages/tree-sitter-rust/queries/highlights.scm",
@@ -1683,7 +1831,7 @@ fn test_input_edits() {
use crate::State; use crate::State;
use tree_sitter::InputEdit; use tree_sitter::InputEdit;
let mut state = State::new("hello world!\ntest 123".into()); let state = State::new("hello world!\ntest 123".into());
let transaction = Transaction::change( let transaction = Transaction::change(
&state.doc, &state.doc,
vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(), vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
@@ -1743,3 +1891,4 @@ fn test_load_runtime_file() {
let results = load_runtime_file("rust", "does-not-exist"); let results = load_runtime_file("rust", "does-not-exist");
assert!(results.is_err()); assert!(results.is_err());
} }
}

View File

@@ -0,0 +1,321 @@
use ropey::RopeSlice;
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::next_grapheme_boundary;
use crate::movement::Direction;
use crate::surround;
use crate::Range;
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
use CharCategory::{Eol, Whitespace};
let iter = match direction {
Direction::Forward => slice.chars_at(pos),
Direction::Backward => {
let mut iter = slice.chars_at(pos);
iter.reverse();
iter
}
};
let mut prev_category = match direction {
Direction::Forward if pos == 0 => Whitespace,
Direction::Forward => categorize_char(slice.char(pos - 1)),
Direction::Backward if pos == slice.len_chars() => Whitespace,
Direction::Backward => categorize_char(slice.char(pos)),
};
for ch in iter {
match categorize_char(ch) {
Eol | Whitespace => return pos,
category => {
if category != prev_category && pos != 0 && pos != slice.len_chars() {
return pos;
} else {
match direction {
Direction::Forward => pos += 1,
Direction::Backward => pos = pos.saturating_sub(1),
}
prev_category = category;
}
}
}
}
pos
}
#[derive(Copy, Clone, PartialEq, Eq, Debug)]
pub enum TextObject {
Around,
Inside,
}
// count doesn't do anything yet
pub fn textobject_word(
slice: RopeSlice,
range: Range,
textobject: TextObject,
_count: usize,
) -> Range {
let pos = range.cursor(slice);
let word_start = find_word_boundary(slice, pos, Direction::Backward);
let word_end = match slice.get_char(pos).map(categorize_char) {
None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
_ => find_word_boundary(slice, pos + 1, Direction::Forward),
};
// Special case.
if word_start == word_end {
return Range::new(word_start, word_end);
}
match textobject {
TextObject::Inside => Range::new(word_start, word_end),
TextObject::Around => {
let whitespace_count_right = slice
.chars_at(word_end)
.take_while(|c| char_is_whitespace(*c))
.count();
if whitespace_count_right > 0 {
Range::new(word_start, word_end + whitespace_count_right)
} else {
let whitespace_count_left = {
let mut iter = slice.chars_at(word_start);
iter.reverse();
iter.take_while(|c| char_is_whitespace(*c)).count()
};
Range::new(word_start - whitespace_count_left, word_end)
}
}
}
}
pub fn textobject_surround(
slice: RopeSlice,
range: Range,
textobject: TextObject,
ch: char,
count: usize,
) -> Range {
surround::find_nth_pairs_pos(slice, ch, range.head, count)
.map(|(anchor, head)| match textobject {
TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
})
.unwrap_or(range)
}
#[cfg(test)]
mod test {
use super::TextObject::*;
use super::*;
use crate::Range;
use ropey::Rope;
#[test]
fn test_textobject_word() {
// (text, [(cursor position, textobject, final range), ...])
let tests = &[
(
"cursor at beginning of doc",
vec![(0, Inside, (0, 6)), (0, Around, (0, 7))],
),
(
"cursor at middle of word",
vec![
(13, Inside, (10, 16)),
(10, Inside, (10, 16)),
(15, Inside, (10, 16)),
(13, Around, (10, 17)),
(10, Around, (10, 17)),
(15, Around, (10, 17)),
],
),
(
"cursor between word whitespace",
vec![(6, Inside, (6, 6)), (6, Around, (6, 6))],
),
(
"cursor on word before newline\n",
vec![
(22, Inside, (22, 29)),
(28, Inside, (22, 29)),
(25, Inside, (22, 29)),
(22, Around, (21, 29)),
(28, Around, (21, 29)),
(25, Around, (21, 29)),
],
),
(
"cursor on newline\nnext line",
vec![(17, Inside, (17, 17)), (17, Around, (17, 17))],
),
(
"cursor on word after newline\nnext line",
vec![
(29, Inside, (29, 33)),
(30, Inside, (29, 33)),
(32, Inside, (29, 33)),
(29, Around, (29, 34)),
(30, Around, (29, 34)),
(32, Around, (29, 34)),
],
),
(
"cursor on #$%:;* punctuation",
vec![
(13, Inside, (10, 16)),
(10, Inside, (10, 16)),
(15, Inside, (10, 16)),
(13, Around, (10, 17)),
(10, Around, (10, 17)),
(15, Around, (10, 17)),
],
),
(
"cursor on punc%^#$:;.tuation",
vec![
(14, Inside, (14, 21)),
(20, Inside, (14, 21)),
(17, Inside, (14, 21)),
(14, Around, (14, 21)),
(20, Around, (14, 21)),
(17, Around, (14, 21)),
],
),
(
"cursor in extra whitespace",
vec![
(9, Inside, (9, 9)),
(10, Inside, (10, 10)),
(11, Inside, (11, 11)),
(9, Around, (9, 9)),
(10, Around, (10, 10)),
(11, Around, (11, 11)),
],
),
(
"cursor on word with extra whitespace",
vec![(11, Inside, (10, 14)), (11, Around, (10, 17))],
),
(
"cursor at end with extra whitespace",
vec![(28, Inside, (27, 37)), (28, Around, (24, 37))],
),
(
"cursor at end of doc",
vec![(19, Inside, (17, 20)), (19, Around, (16, 20))],
),
];
for (sample, scenario) in tests {
let doc = Rope::from(*sample);
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range) = case;
let result = textobject_word(slice, Range::point(pos), objtype, 1);
assert_eq!(
result,
expected_range.into(),
"\nCase failed: {:?} - {:?}",
sample,
case
);
}
}
}
#[test]
fn test_textobject_surround() {
// (text, [(cursor position, textobject, final range, count), ...])
let tests = &[
(
"simple (single) surround pairs",
vec![
(3, Inside, (3, 3), '(', 1),
(7, Inside, (8, 14), ')', 1),
(10, Inside, (8, 14), '(', 1),
(14, Inside, (8, 14), ')', 1),
(3, Around, (3, 3), '(', 1),
(7, Around, (7, 15), ')', 1),
(10, Around, (7, 15), '(', 1),
(14, Around, (7, 15), ')', 1),
],
),
(
"samexx 'single' surround pairs",
vec![
(3, Inside, (3, 3), '\'', 1),
(7, Inside, (7, 7), '\'', 1),
(10, Inside, (8, 14), '\'', 1),
(14, Inside, (14, 14), '\'', 1),
(3, Around, (3, 3), '\'', 1),
(7, Around, (7, 7), '\'', 1),
(10, Around, (7, 15), '\'', 1),
(14, Around, (14, 14), '\'', 1),
],
),
(
"(nested (surround (pairs)) 3 levels)",
vec![
(0, Inside, (1, 35), '(', 1),
(6, Inside, (1, 35), ')', 1),
(8, Inside, (9, 25), '(', 1),
(8, Inside, (9, 35), ')', 2),
(20, Inside, (9, 25), '(', 2),
(20, Inside, (1, 35), ')', 3),
(0, Around, (0, 36), '(', 1),
(6, Around, (0, 36), ')', 1),
(8, Around, (8, 26), '(', 1),
(8, Around, (8, 36), ')', 2),
(20, Around, (8, 26), '(', 2),
(20, Around, (0, 36), ')', 3),
],
),
(
"(mixed {surround [pair] same} line)",
vec![
(2, Inside, (1, 34), '(', 1),
(9, Inside, (8, 28), '{', 1),
(18, Inside, (18, 22), '[', 1),
(2, Around, (0, 35), '(', 1),
(9, Around, (7, 29), '{', 1),
(18, Around, (17, 23), '[', 1),
],
),
(
"(stepped (surround) pairs (should) skip)",
vec![(22, Inside, (1, 39), '(', 1), (22, Around, (0, 40), '(', 1)],
),
(
"[surround pairs{\non different]\nlines}",
vec![
(7, Inside, (1, 29), '[', 1),
(15, Inside, (16, 36), '{', 1),
(7, Around, (0, 30), '[', 1),
(15, Around, (15, 37), '{', 1),
],
),
];
for (sample, scenario) in tests {
let doc = Rope::from(*sample);
let slice = doc.slice(..);
for &case in scenario {
let (pos, objtype, expected_range, ch, count) = case;
let result = textobject_surround(slice, Range::point(pos), objtype, ch, count);
assert_eq!(
result,
expected_range.into(),
"\nCase failed: {:?} - {:?}",
sample,
case
);
}
}
}
}

View File

@@ -1,5 +1,5 @@
use crate::{Range, Rope, Selection, State, Tendril}; use crate::{Range, Rope, Selection, Tendril};
use std::{borrow::Cow, convert::TryFrom}; use std::borrow::Cow;
/// (from, to, replacement) /// (from, to, replacement)
pub type Change = (usize, usize, Option<Tendril>); pub type Change = (usize, usize, Option<Tendril>);
@@ -15,7 +15,7 @@ pub enum Operation {
Insert(Tendril), Insert(Tendril),
} }
#[derive(Copy, Clone, PartialEq, Eq)] #[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Assoc { pub enum Assoc {
Before, Before,
After, After,
@@ -163,7 +163,7 @@ impl ChangeSet {
head_a = a; head_a = a;
head_b = changes_b.next(); head_b = changes_b.next();
} }
(None, _) | (_, None) => return unreachable!(), (None, val) | (val, None) => unreachable!("({:?})", val),
(Some(Retain(i)), Some(Retain(j))) => match i.cmp(&j) { (Some(Retain(i)), Some(Retain(j))) => match i.cmp(&j) {
Ordering::Less => { Ordering::Less => {
changes.retain(i); changes.retain(i);
@@ -193,15 +193,16 @@ impl ChangeSet {
head_b = changes_b.next(); head_b = changes_b.next();
} }
Ordering::Greater => { Ordering::Greater => {
// TODO: cover this with a test
// figure out the byte index of the truncated string end // figure out the byte index of the truncated string end
let (pos, _) = s.char_indices().nth(len - j).unwrap(); let (pos, _) = s.char_indices().nth(j).unwrap();
s.pop_front(s.len() as u32 - pos as u32); s.pop_front(pos as u32);
head_a = Some(Insert(s)); head_a = Some(Insert(s));
head_b = changes_b.next(); head_b = changes_b.next();
} }
} }
} }
(Some(Insert(mut s)), Some(Retain(j))) => { (Some(Insert(s)), Some(Retain(j))) => {
let len = s.chars().count(); let len = s.chars().count();
match len.cmp(&j) { match len.cmp(&j) {
Ordering::Less => { Ordering::Less => {
@@ -273,7 +274,6 @@ impl ChangeSet {
let mut changes = Self::with_capacity(self.changes.len()); let mut changes = Self::with_capacity(self.changes.len());
let mut pos = 0; let mut pos = 0;
let mut len = 0;
for change in &self.changes { for change in &self.changes {
use Operation::*; use Operation::*;
@@ -473,11 +473,13 @@ impl Transaction {
/// Generate a transaction from a set of changes. /// Generate a transaction from a set of changes.
pub fn change<I>(doc: &Rope, changes: I) -> Self pub fn change<I>(doc: &Rope, changes: I) -> Self
where where
I: IntoIterator<Item = Change> + ExactSizeIterator, I: IntoIterator<Item = Change> + Iterator,
{ {
let len = doc.len_chars(); let len = doc.len_chars();
let mut changeset = ChangeSet::with_capacity(2 * changes.len() + 1); // rough estimate let (lower, upper) = changes.size_hint();
let size = upper.unwrap_or(lower);
let mut changeset = ChangeSet::with_capacity(2 * size + 1); // rough estimate
// TODO: verify ranges are ordered and not overlapping or change will panic. // TODO: verify ranges are ordered and not overlapping or change will panic.
@@ -580,6 +582,7 @@ impl<'a> Iterator for ChangeIterator<'a> {
#[cfg(test)] #[cfg(test)]
mod test { mod test {
use super::*; use super::*;
use crate::State;
#[test] #[test]
fn composition() { fn composition() {
@@ -598,7 +601,7 @@ mod test {
}; };
let b = ChangeSet { let b = ChangeSet {
changes: vec![Delete(10), Insert("world".into()), Retain(5)], changes: vec![Delete(10), Insert("orld".into()), Retain(5)],
len: 15, len: 15,
len_after: 10, len_after: 10,
}; };
@@ -609,7 +612,7 @@ mod test {
let composed = a.compose(b); let composed = a.compose(b);
assert_eq!(composed.len, 8); assert_eq!(composed.len, 8);
assert!(composed.apply(&mut text)); assert!(composed.apply(&mut text));
assert_eq!(text, "world! abc"); assert_eq!(text, "orld! abc");
} }
#[test] #[test]
@@ -698,7 +701,7 @@ mod test {
#[test] #[test]
fn changes_iter() { fn changes_iter() {
let mut state = State::new("hello world!\ntest 123".into()); let state = State::new("hello world!\ntest 123".into());
let changes = vec![(6, 11, Some("void".into())), (12, 17, None)]; let changes = vec![(6, 11, Some("void".into())), (12, 17, None)];
let transaction = Transaction::change(&state.doc, changes.clone().into_iter()); let transaction = Transaction::change(&state.doc, changes.clone().into_iter());
assert_eq!(transaction.changes_iter().collect::<Vec<_>>(), changes); assert_eq!(transaction.changes_iter().collect::<Vec<_>>(), changes);
@@ -730,7 +733,7 @@ mod test {
// retain 1, e // retain 1, e
// retain 2, l // retain 2, l
let mut changes = t1 let changes = t1
.changes .changes
.compose(t2.changes) .compose(t2.changes)
.compose(t3.changes) .compose(t3.changes)
@@ -745,7 +748,7 @@ mod test {
#[test] #[test]
fn combine_with_empty() { fn combine_with_empty() {
let empty = Rope::from(""); let empty = Rope::from("");
let mut a = ChangeSet::new(&empty); let a = ChangeSet::new(&empty);
let mut b = ChangeSet::new(&empty); let mut b = ChangeSet::new(&empty);
b.insert("a".into()); b.insert("a".into());
@@ -758,10 +761,10 @@ mod test {
#[test] #[test]
fn combine_with_utf8() { fn combine_with_utf8() {
const TEST_CASE: &'static str = "Hello, これはヒレクスエディターです!"; const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
let empty = Rope::from(""); let empty = Rope::from("");
let mut a = ChangeSet::new(&empty); let a = ChangeSet::new(&empty);
let mut b = ChangeSet::new(&empty); let mut b = ChangeSet::new(&empty);
b.insert(TEST_CASE.into()); b.insert(TEST_CASE.into());

View File

@@ -1,65 +0,0 @@
use crate::movement::{categorize, is_horiz_blank, is_word, skip_over_prev};
use ropey::RopeSlice;
#[must_use]
pub fn nth_prev_word_boundary(slice: RopeSlice, mut char_idx: usize, count: usize) -> usize {
let mut with_end = false;
for _ in 0..count {
if char_idx == 0 {
break;
}
// return if not skip while?
skip_over_prev(slice, &mut char_idx, |ch| ch == '\n');
with_end = skip_over_prev(slice, &mut char_idx, is_horiz_blank);
// refetch
let ch = slice.char(char_idx);
if is_word(ch) {
with_end = skip_over_prev(slice, &mut char_idx, is_word);
} else if ch.is_ascii_punctuation() {
with_end = skip_over_prev(slice, &mut char_idx, |ch| ch.is_ascii_punctuation());
}
}
if with_end {
char_idx
} else {
char_idx + 1
}
}
#[test]
fn different_prev_word_boundary() {
use ropey::Rope;
let t = |x, y| {
let text = Rope::from(x);
let out = nth_prev_word_boundary(text.slice(..), text.len_chars() - 1, 1);
assert_eq!(text.slice(..out), y, r#"from "{}""#, x);
};
t("abcd\nefg\nwrs", "abcd\nefg\n");
t("abcd\nefg\n", "abcd\n");
t("abcd\n", "");
t("hello, world!", "hello, world");
t("hello, world", "hello, ");
t("hello, ", "hello");
t("hello", "");
t("こんにちは、世界!", "こんにちは、世界!"); // TODO: punctuation
t("こんにちは、世界", "こんにちは、");
t("こんにちは、", "こんにちは、"); // what?
t("こんにちは", "");
t("この世界。", "この世界。"); // what?
t("この世界", "");
t("お前はもう死んでいる", "");
t("その300円です", ""); // TODO: should stop at 300
t("唱k", ""); // TODO: should stop at 唱
t("1 + 1 = 2", "1 + 1 = ");
t("1 + 1 =", "1 + 1 ");
t("1 + 1", "1 + ");
t("1 + ", "1 ");
t("1 ", "");
t("1+1=2", "1+1=");
}

View File

@@ -1,29 +1,27 @@
[package] [package]
name = "helix-lsp" name = "helix-lsp"
version = "0.0.10" version = "0.4.1"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2018"
license = "MPL-2.0" 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
helix-core = { path = "../helix-core" } helix-core = { version = "0.4", path = "../helix-core" }
once_cell = "1.4" anyhow = "1.0"
futures-executor = "0.3"
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
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
jsonrpc-core = { version = "18.0", 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" thiserror = "1.0"
log = "~0.4" tokio = { version = "1.9", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
tokio-stream = "0.1.7"

View File

@@ -3,37 +3,37 @@ use crate::{
Call, Error, OffsetEncoding, Result, Call, Error, OffsetEncoding, Result,
}; };
use helix_core::{ChangeSet, Rope}; use helix_core::{chars::char_is_line_ending, find_root, ChangeSet, Rope};
// use std::collections::HashMap;
use std::future::Future;
use std::sync::atomic::{AtomicU64, Ordering};
use jsonrpc_core as jsonrpc; use jsonrpc_core as jsonrpc;
use lsp_types as lsp; use lsp_types as lsp;
use serde_json::Value; use serde_json::Value;
use std::future::Future;
use std::process::Stdio; use std::process::Stdio;
use std::sync::atomic::{AtomicU64, Ordering};
use tokio::{ use tokio::{
io::{BufReader, BufWriter}, io::{BufReader, BufWriter},
// prelude::*,
process::{Child, Command}, process::{Child, Command},
sync::mpsc::{channel, UnboundedReceiver, UnboundedSender}, sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
}; };
#[derive(Debug)]
pub struct Client { pub struct Client {
id: usize,
_process: Child, _process: Child,
server_tx: UnboundedSender<Payload>,
outgoing: UnboundedSender<Payload>, request_counter: AtomicU64,
// pub incoming: Receiver<Call>,
pub request_counter: AtomicU64,
capabilities: Option<lsp::ServerCapabilities>, capabilities: Option<lsp::ServerCapabilities>,
offset_encoding: OffsetEncoding, offset_encoding: OffsetEncoding,
config: Option<Value>,
} }
impl Client { impl Client {
pub fn start(cmd: &str, args: &[String]) -> Result<(Self, UnboundedReceiver<Call>)> { pub fn start(
cmd: &str,
args: &[String],
config: Option<Value>,
id: usize,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>)> {
let process = Command::new(cmd) let process = Command::new(cmd)
.args(args) .args(args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
@@ -43,40 +43,33 @@ impl Client {
.kill_on_drop(true) .kill_on_drop(true)
.spawn(); .spawn();
// use std::io::ErrorKind; let mut process = process?;
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())),
},
};
// TODO: do we need bufreader/writer here? or do we use async wrappers on unblock? // 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 writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout")); 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 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 { let client = Self {
id,
_process: process, _process: process,
server_tx,
outgoing,
// incoming,
request_counter: AtomicU64::new(0), request_counter: AtomicU64::new(0),
capabilities: None, capabilities: None,
// diagnostics: HashMap::new(),
offset_encoding: OffsetEncoding::Utf8, offset_encoding: OffsetEncoding::Utf8,
config,
}; };
// TODO: async client.initialize() // TODO: async client.initialize()
// maybe use an arc<atomic> flag // 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 { fn next_request_id(&self) -> jsonrpc::Id {
@@ -106,7 +99,7 @@ impl Client {
} }
/// Execute a RPC request on the language server. /// 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 where
R::Params: serde::Serialize, R::Params: serde::Serialize,
R::Result: core::fmt::Debug, // TODO: temporary R::Result: core::fmt::Debug, // TODO: temporary
@@ -118,17 +111,20 @@ impl Client {
} }
/// Execute a RPC request on the language server. /// Execute a RPC request on the language server.
pub fn call<R: lsp::request::Request>( fn call<R: lsp::request::Request>(
&self, &self,
params: R::Params, params: R::Params,
) -> impl Future<Output = Result<Value>> ) -> impl Future<Output = Result<Value>>
where where
R::Params: serde::Serialize, R::Params: serde::Serialize,
{ {
let outgoing = self.outgoing.clone(); let server_tx = self.server_tx.clone();
let id = self.next_request_id(); let id = self.next_request_id();
async move { async move {
use std::time::Duration;
use tokio::time::timeout;
let params = serde_json::to_value(params)?; let params = serde_json::to_value(params)?;
let request = jsonrpc::MethodCall { let request = jsonrpc::MethodCall {
@@ -140,32 +136,29 @@ impl Client {
let (tx, mut rx) = channel::<Result<Value>>(1); let (tx, mut rx) = channel::<Result<Value>>(1);
outgoing server_tx
.send(Payload::Request { .send(Payload::Request {
chan: tx, chan: tx,
value: request, value: request,
}) })
.map_err(|e| Error::Other(e.into()))?; .map_err(|e| Error::Other(e.into()))?;
use std::time::Duration;
use tokio::time::timeout;
timeout(Duration::from_secs(2), rx.recv()) timeout(Duration::from_secs(2), rx.recv())
.await .await
.map_err(|_| Error::Timeout)? // return Timeout .map_err(|_| Error::Timeout)? // return Timeout
.unwrap() // TODO: None if channel closed .ok_or(Error::StreamClosed)?
} }
} }
/// Send a RPC notification to the language server. /// Send a RPC notification to the language server.
pub fn notify<R: lsp::notification::Notification>( fn notify<R: lsp::notification::Notification>(
&self, &self,
params: R::Params, params: R::Params,
) -> impl Future<Output = Result<()>> ) -> impl Future<Output = Result<()>>
where where
R::Params: serde::Serialize, R::Params: serde::Serialize,
{ {
let outgoing = self.outgoing.clone(); let server_tx = self.server_tx.clone();
async move { async move {
let params = serde_json::to_value(params)?; let params = serde_json::to_value(params)?;
@@ -176,7 +169,7 @@ impl Client {
params: Self::value_into_params(params), params: Self::value_into_params(params),
}; };
outgoing server_tx
.send(Payload::Notification(notification)) .send(Payload::Notification(notification))
.map_err(|e| Error::Other(e.into()))?; .map_err(|e| Error::Other(e.into()))?;
@@ -185,13 +178,16 @@ impl Client {
} }
/// Reply to a language server RPC call. /// Reply to a language server RPC call.
pub async fn reply( pub fn reply(
&self, &self,
id: jsonrpc::Id, id: jsonrpc::Id,
result: core::result::Result<Value, jsonrpc::Error>, result: core::result::Result<Value, jsonrpc::Error>,
) -> Result<()> { ) -> impl Future<Output = Result<()>> {
use jsonrpc::{Failure, Output, Success, Version}; use jsonrpc::{Failure, Output, Success, Version};
let server_tx = self.server_tx.clone();
async move {
let output = match result { let output = match result {
Ok(result) => Output::Success(Success { Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2), jsonrpc: Some(Version::V2),
@@ -205,27 +201,33 @@ impl Client {
}), }),
}; };
self.outgoing server_tx
.send(Payload::Response(output)) .send(Payload::Response(output))
.map_err(|e| Error::Other(e.into()))?; .map_err(|e| Error::Other(e.into()))?;
Ok(()) Ok(())
} }
}
// ------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------
// General messages // 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 // 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());
if self.config.is_some() {
log::info!("Using custom LSP config: {}", self.config.as_ref().unwrap());
}
#[allow(deprecated)] #[allow(deprecated)]
let params = lsp::InitializeParams { let params = lsp::InitializeParams {
process_id: Some(std::process::id()), process_id: Some(std::process::id()),
// root_path is obsolete, use root_uri
root_path: None, root_path: None,
// root_uri: Some(lsp_types::Url::parse("file://localhost/")?), root_uri: root,
root_uri: None, // set to project root in the future initialization_options: self.config.clone(),
initialization_options: None,
capabilities: lsp::ClientCapabilities { capabilities: lsp::ClientCapabilities {
text_document: Some(lsp::TextDocumentClientCapabilities { text_document: Some(lsp::TextDocumentClientCapabilities {
completion: Some(lsp::CompletionClientCapabilities { completion: Some(lsp::CompletionClientCapabilities {
@@ -245,6 +247,30 @@ impl Client {
content_format: Some(vec![lsp::MarkupKind::Markdown]), content_format: Some(vec![lsp::MarkupKind::Markdown]),
..Default::default() ..Default::default()
}), }),
code_action: Some(lsp::CodeActionClientCapabilities {
code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
code_action_kind: lsp::CodeActionKindLiteralSupport {
value_set: [
lsp::CodeActionKind::EMPTY,
lsp::CodeActionKind::QUICKFIX,
lsp::CodeActionKind::REFACTOR,
lsp::CodeActionKind::REFACTOR_EXTRACT,
lsp::CodeActionKind::REFACTOR_INLINE,
lsp::CodeActionKind::REFACTOR_REWRITE,
lsp::CodeActionKind::SOURCE,
lsp::CodeActionKind::SOURCE_ORGANIZE_IMPORTS,
]
.iter()
.map(|kind| kind.as_str().to_string())
.collect(),
},
}),
..Default::default()
}),
..Default::default()
}),
window: Some(lsp::WindowClientCapabilities {
work_done_progress: Some(true),
..Default::default() ..Default::default()
}), }),
..Default::default() ..Default::default()
@@ -273,6 +299,21 @@ impl Client {
self.notify::<lsp::notification::Exit>(()) 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 // Text document
// ------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------
@@ -323,8 +364,9 @@ impl Client {
mut character, mut character,
} = pos; } = pos;
for ch in text.chars() { let mut chars = text.chars().peekable();
if ch == '\n' { while let Some(ch) = chars.next() {
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
line += 1; line += 1;
character = 0; character = 0;
} else { } else {
@@ -479,6 +521,7 @@ impl Client {
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> impl Future<Output = Result<Value>> {
// ) -> Result<Vec<lsp::CompletionItem>> { // ) -> Result<Vec<lsp::CompletionItem>> {
let params = lsp::CompletionParams { let params = lsp::CompletionParams {
@@ -487,9 +530,7 @@ impl Client {
position, position,
}, },
// TODO: support these tokens by async receiving and updating the choice list // TODO: support these tokens by async receiving and updating the choice list
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
work_done_token: None,
},
partial_result_params: lsp::PartialResultParams { partial_result_params: lsp::PartialResultParams {
partial_result_token: None, partial_result_token: None,
}, },
@@ -504,15 +545,14 @@ impl Client {
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> impl Future<Output = Result<Value>> {
let params = lsp::SignatureHelpParams { let params = lsp::SignatureHelpParams {
text_document_position_params: lsp::TextDocumentPositionParams { text_document_position_params: lsp::TextDocumentPositionParams {
text_document, text_document,
position, position,
}, },
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
work_done_token: None,
},
context: None, context: None,
// lsp::SignatureHelpContext // lsp::SignatureHelpContext
}; };
@@ -524,15 +564,14 @@ impl Client {
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> impl Future<Output = Result<Value>> {
let params = lsp::HoverParams { let params = lsp::HoverParams {
text_document_position_params: lsp::TextDocumentPositionParams { text_document_position_params: lsp::TextDocumentPositionParams {
text_document, text_document,
position, position,
}, },
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
work_done_token: None,
},
// lsp::SignatureHelpContext // lsp::SignatureHelpContext
}; };
@@ -545,6 +584,7 @@ impl Client {
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
options: lsp::FormattingOptions, options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> anyhow::Result<Vec<lsp::TextEdit>> { ) -> anyhow::Result<Vec<lsp::TextEdit>> {
let capabilities = self.capabilities.as_ref().unwrap(); let capabilities = self.capabilities.as_ref().unwrap();
@@ -559,9 +599,7 @@ impl Client {
let params = lsp::DocumentFormattingParams { let params = lsp::DocumentFormattingParams {
text_document, text_document,
options, options,
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
work_done_token: None,
},
}; };
let response = self.request::<lsp::request::Formatting>(params).await?; let response = self.request::<lsp::request::Formatting>(params).await?;
@@ -574,6 +612,7 @@ impl Client {
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
range: lsp::Range, range: lsp::Range,
options: lsp::FormattingOptions, options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> anyhow::Result<Vec<lsp::TextEdit>> { ) -> anyhow::Result<Vec<lsp::TextEdit>> {
let capabilities = self.capabilities.as_ref().unwrap(); let capabilities = self.capabilities.as_ref().unwrap();
@@ -589,9 +628,7 @@ impl Client {
text_document, text_document,
range, range,
options, options,
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
work_done_token: None,
},
}; };
let response = self let response = self
@@ -610,15 +647,14 @@ impl Client {
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> impl Future<Output = Result<Value>> {
let params = lsp::GotoDefinitionParams { let params = lsp::GotoDefinitionParams {
text_document_position_params: lsp::TextDocumentPositionParams { text_document_position_params: lsp::TextDocumentPositionParams {
text_document, text_document,
position, position,
}, },
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
work_done_token: None,
},
partial_result_params: lsp::PartialResultParams { partial_result_params: lsp::PartialResultParams {
partial_result_token: None, partial_result_token: None,
}, },
@@ -631,30 +667,42 @@ impl Client {
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> 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( pub fn goto_type_definition(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> 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( pub fn goto_implementation(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> 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( pub fn goto_reference(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
position: lsp::Position, position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> { ) -> impl Future<Output = Result<Value>> {
let params = lsp::ReferenceParams { let params = lsp::ReferenceParams {
text_document_position: lsp::TextDocumentPositionParams { text_document_position: lsp::TextDocumentPositionParams {
@@ -664,9 +712,7 @@ impl Client {
context: lsp::ReferenceContext { context: lsp::ReferenceContext {
include_declaration: true, include_declaration: true,
}, },
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
work_done_token: None,
},
partial_result_params: lsp::PartialResultParams { partial_result_params: lsp::PartialResultParams {
partial_result_token: None, partial_result_token: None,
}, },
@@ -674,4 +720,44 @@ impl Client {
self.call::<lsp::request::References>(params) 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)
}
// empty string to get all symbols
pub fn workspace_symbols(&self, query: String) -> impl Future<Output = Result<Value>> {
let params = lsp::WorkspaceSymbolParams {
query,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};
self.call::<lsp::request::WorkspaceSymbol>(params)
}
pub fn code_actions(
&self,
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
) -> impl Future<Output = Result<Value>> {
let params = lsp::CodeActionParams {
text_document,
range,
context: lsp::CodeActionContext::default(),
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};
self.call::<lsp::request::CodeActionRequest>(params)
}
} }

View File

@@ -1,25 +1,30 @@
mod client; mod client;
mod transport; mod transport;
pub use client::Client;
pub use futures_executor::block_on;
pub use jsonrpc::Call;
pub use jsonrpc_core as jsonrpc; pub use jsonrpc_core as jsonrpc;
pub use lsp::{Position, Url};
pub use lsp_types as lsp; pub use lsp_types as lsp;
pub use client::Client; use futures_util::stream::select_all::SelectAll;
pub use lsp::{Position, Url};
pub type Result<T> = core::result::Result<T, Error>;
use helix_core::syntax::LanguageConfiguration; use helix_core::syntax::LanguageConfiguration;
use thiserror::Error; use std::{
collections::{hash_map::Entry, HashMap},
use std::{collections::HashMap, sync::Arc}; sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream; 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)] #[derive(Error, Debug)]
pub enum Error { pub enum Error {
@@ -27,8 +32,14 @@ pub enum Error {
Rpc(#[from] jsonrpc::Error), Rpc(#[from] jsonrpc::Error),
#[error("failed to parse: {0}")] #[error("failed to parse: {0}")]
Parse(#[from] serde_json::Error), Parse(#[from] serde_json::Error),
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
#[error("request timed out")] #[error("request timed out")]
Timeout, Timeout,
#[error("server closed the stream")]
StreamClosed,
#[error("LSP not defined")]
LspNotDefined,
#[error(transparent)] #[error(transparent)]
Other(#[from] anyhow::Error), Other(#[from] anyhow::Error),
} }
@@ -47,23 +58,54 @@ pub mod util {
use super::*; use super::*;
use helix_core::{Range, Rope, Transaction}; 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( pub fn lsp_pos_to_pos(
doc: &Rope, doc: &Rope,
pos: lsp::Position, pos: lsp::Position,
offset_encoding: OffsetEncoding, 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 { match offset_encoding {
OffsetEncoding::Utf8 => { OffsetEncoding::Utf8 => {
let line = doc.line_to_char(pos.line as usize); let max_char = doc
line + pos.character as usize .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 => { 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); 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( pub fn pos_to_lsp_pos(
doc: &Rope, doc: &Rope,
pos: usize, pos: usize,
@@ -87,6 +129,7 @@ pub mod util {
} }
} }
/// Converts a range in the document to [`lsp::Range`].
pub fn range_to_lsp_range( pub fn range_to_lsp_range(
doc: &Rope, doc: &Rope,
range: Range, range: Range,
@@ -98,6 +141,17 @@ pub mod util {
lsp::Range::new(start, end) 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( pub fn generate_transaction_from_edits(
doc: &Rope, doc: &Rope,
edits: Vec<lsp::TextEdit>, edits: Vec<lsp::TextEdit>,
@@ -113,14 +167,61 @@ pub mod util {
None None
}; };
let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding); let start =
let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding); 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) (start, end, replacement)
}), }),
) )
} }
// apply_insert_replace_edit /// The result of asking the language server to format the document. This can be turned into a
/// `Transaction`, but the advantage of not doing that straight away is that this one is
/// `Send` and `Sync`.
#[derive(Clone, Debug)]
pub struct LspFormatting {
pub doc: Rope,
pub edits: Vec<lsp::TextEdit>,
pub offset_encoding: OffsetEncoding,
}
impl From<LspFormatting> for Transaction {
fn from(fmt: LspFormatting) -> Transaction {
generate_transaction_from_edits(&fmt.doc, fmt.edits, fmt.offset_encoding)
}
}
}
#[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)] #[derive(Debug, PartialEq, Clone)]
@@ -128,6 +229,7 @@ pub enum Notification {
PublishDiagnostics(lsp::PublishDiagnosticsParams), PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams), ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams), LogMessage(lsp::LogMessageParams),
ProgressMessage(lsp::ProgressParams),
} }
impl Notification { impl Notification {
@@ -145,17 +247,20 @@ impl Notification {
} }
lsp::notification::ShowMessage::METHOD => { lsp::notification::ShowMessage::METHOD => {
let params: lsp::ShowMessageParams = let params: lsp::ShowMessageParams = params.parse().ok()?;
params.parse().expect("Failed to parse ShowMessage params");
Self::ShowMessage(params) Self::ShowMessage(params)
} }
lsp::notification::LogMessage::METHOD => { lsp::notification::LogMessage::METHOD => {
let params: lsp::LogMessageParams = let params: lsp::LogMessageParams = params.parse().ok()?;
params.parse().expect("Failed to parse ShowMessage params");
Self::LogMessage(params) Self::LogMessage(params)
} }
lsp::notification::Progress::METHOD => {
let params: lsp::ProgressParams = params.parse().ok()?;
Self::ProgressMessage(params)
}
_ => { _ => {
log::error!("unhandled LSP notification: {}", method); log::error!("unhandled LSP notification: {}", method);
return None; return None;
@@ -166,16 +271,12 @@ impl Notification {
} }
} }
pub use jsonrpc::Call; #[derive(Debug)]
type LanguageId = String;
use futures_util::stream::select_all::SelectAll;
pub struct Registry { 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 { impl Default for Registry {
@@ -188,39 +289,129 @@ impl Registry {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
inner: HashMap::new(), inner: HashMap::new(),
counter: AtomicUsize::new(0),
incoming: SelectAll::new(), incoming: SelectAll::new(),
} }
} }
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Option<Arc<Client>> { pub fn get_by_id(&mut self, id: usize) -> Option<&Client> {
// TODO: propagate the error 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 { if let Some(config) = &language_config.language_server {
// avoid borrow issues // avoid borrow issues
let inner = &mut self.inner; let inner = &mut self.inner;
let s_incoming = &mut self.incoming; let s_incoming = &mut self.incoming;
let language_server = inner match inner.entry(language_config.scope.clone()) {
.entry(language_config.scope.clone()) // can't use entry with Borrow keys: https://github.com/rust-lang/rfcs/pull/1769 Entry::Occupied(entry) => Ok(entry.get().1.clone()),
.or_insert_with(|| { Entry::Vacant(entry) => {
// TODO: lookup defaults for id (name, args)
// initialize a new client // initialize a new client
let (mut client, incoming) = let id = self.counter.fetch_add(1, Ordering::Relaxed);
Client::start(&config.command, &config.args).ok()?; let (mut client, incoming) = Client::start(
&config.command,
&config.args,
serde_json::from_str(language_config.config.as_deref().unwrap_or("")).ok(),
id,
)?;
// TODO: run this async without blocking // 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)); s_incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client);
Some(Arc::new(client)) entry.insert((id, client.clone()));
}) Ok(client)
.clone(); }
}
return language_server; } 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))
} }
} }
@@ -249,3 +440,34 @@ impl Registry {
// there needs to be a way to process incoming lsp messages from all clients. // there needs to be a way to process incoming lsp messages from all clients.
// -> notifications need to be dispatched to wherever // -> notifications need to be dispatched to wherever
// -> requests need to generate a reply and travel back to the same lsp! // -> 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,19 +1,18 @@
use std::collections::HashMap; use crate::Result;
use std::io; use anyhow::Context;
use log::{error, info};
use crate::Error;
type Result<T> = core::result::Result<T, Error>;
use jsonrpc_core as jsonrpc; use jsonrpc_core as jsonrpc;
use log::{error, info};
use serde::{Deserialize, Serialize};
use serde_json::Value; use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::{ use tokio::{
io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter}, io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
process::{ChildStderr, ChildStdin, ChildStdout}, process::{ChildStderr, ChildStdin, ChildStdout},
sync::mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender}, sync::{
mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender},
Mutex,
},
}; };
#[derive(Debug)] #[derive(Debug)]
@@ -26,153 +25,165 @@ pub enum Payload {
Response(jsonrpc::Output), Response(jsonrpc::Output),
} }
use serde::{Deserialize, Serialize};
/// A type representing all possible values sent from the server to the client. /// A type representing all possible values sent from the server to the client.
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)] #[serde(deny_unknown_fields)]
#[serde(untagged)] #[serde(untagged)]
enum Message { enum ServerMessage {
/// A regular JSON-RPC request output (single response). /// A regular JSON-RPC request output (single response).
Output(jsonrpc::Output), Output(jsonrpc::Output),
/// A JSON-RPC request or notification. /// A JSON-RPC request or notification.
Call(jsonrpc::Call), Call(jsonrpc::Call),
} }
#[derive(Debug)]
pub struct Transport { pub struct Transport {
incoming: UnboundedSender<jsonrpc::Call>, id: usize,
outgoing: UnboundedReceiver<Payload>, pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
headers: HashMap<String, String>,
writer: BufWriter<ChildStdin>,
reader: BufReader<ChildStdout>,
stderr: BufReader<ChildStderr>,
} }
impl Transport { impl Transport {
pub fn start( pub fn start(
reader: BufReader<ChildStdout>, server_stdout: BufReader<ChildStdout>,
writer: BufWriter<ChildStdin>, server_stdin: BufWriter<ChildStdin>,
stderr: BufReader<ChildStderr>, server_stderr: BufReader<ChildStderr>,
) -> (UnboundedReceiver<jsonrpc::Call>, UnboundedSender<Payload>) { id: usize,
let (incoming, rx) = unbounded_channel(); ) -> (
let (tx, outgoing) = unbounded_channel(); UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedSender<Payload>,
) {
let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel();
let transport = Self { let transport = Self {
reader, id,
writer, pending_requests: Mutex::new(HashMap::default()),
stderr,
incoming,
outgoing,
pending_requests: HashMap::default(),
headers: HashMap::default(),
}; };
tokio::spawn(transport.duplex()); let transport = Arc::new(transport);
tokio::spawn(Self::recv(transport.clone(), server_stdout, client_tx));
tokio::spawn(Self::err(transport.clone(), server_stderr));
tokio::spawn(Self::send(transport, server_stdin, client_rx));
(rx, tx) (rx, tx)
} }
async fn recv( async fn recv_server_message(
reader: &mut (impl AsyncBufRead + Unpin + Send), reader: &mut (impl AsyncBufRead + Unpin + Send),
headers: &mut HashMap<String, String>, buffer: &mut String,
) -> core::result::Result<Message, std::io::Error> { ) -> Result<ServerMessage> {
// read headers let mut content_length = None;
loop { loop {
let mut header = String::new(); buffer.truncate(0);
// detect pipe closed if 0 reader.read_line(buffer).await?;
reader.read_line(&mut header).await?; let header = buffer.trim();
let header = header.trim();
if header.is_empty() { if header.is_empty() {
break; break;
} }
let parts: Vec<&str> = header.split(": ").collect(); let mut parts = header.split(": ");
if parts.len() != 2 {
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( return Err(std::io::Error::new(
std::io::ErrorKind::Other, std::io::ErrorKind::Other,
"Failed to parse header", "Failed to parse header",
)); )
.into());
}
} }
headers.insert(parts[0].to_string(), parts[1].to_string());
} }
// find content-length let content_length = content_length.context("missing content length")?;
let content_length = headers.get("Content-Length").unwrap().parse().unwrap();
//TODO: reuse vector
let mut content = vec![0; content_length]; let mut content = vec![0; content_length];
reader.read_exact(&mut content).await?; reader.read_exact(&mut content).await?;
let msg = String::from_utf8(content).unwrap(); let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
// read data
info!("<- {}", msg); info!("<- {}", msg);
// try parsing as output (server response) or call (server request) // 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?) Ok(output?)
} }
async fn err( async fn recv_server_error(
err: &mut (impl AsyncBufRead + Unpin + Send), err: &mut (impl AsyncBufRead + Unpin + Send),
) -> core::result::Result<(), std::io::Error> { buffer: &mut String,
let mut line = String::new(); ) -> Result<()> {
err.read_line(&mut line).await?; buffer.truncate(0);
error!("err <- {}", line); err.read_line(buffer).await?;
error!("err <- {}", buffer);
Ok(()) Ok(())
} }
pub async fn send_payload(&mut self, payload: Payload) -> io::Result<()> { async fn send_payload_to_server(
match payload { &self,
server_stdin: &mut BufWriter<ChildStdin>,
payload: Payload,
) -> Result<()> {
//TODO: reuse string
let json = match payload {
Payload::Request { chan, value } => { Payload::Request { chan, value } => {
self.pending_requests.insert(value.id.clone(), chan); self.pending_requests
.lock()
let json = serde_json::to_string(&value)?; .await
self.send(json).await .insert(value.id.clone(), chan);
} 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(server_stdin, json).await
} }
pub async fn send(&mut self, request: String) -> io::Result<()> { async fn send_string_to_server(
&self,
server_stdin: &mut BufWriter<ChildStdin>,
request: String,
) -> Result<()> {
info!("-> {}", request); info!("-> {}", request);
// send the headers // send the headers
self.writer server_stdin
.write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes()) .write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes())
.await?; .await?;
// send the body // send the body
self.writer.write_all(request.as_bytes()).await?; server_stdin.write_all(request.as_bytes()).await?;
self.writer.flush().await?; server_stdin.flush().await?;
Ok(()) Ok(())
} }
async fn recv_msg(&mut self, msg: Message) -> anyhow::Result<()> { async fn process_server_message(
&self,
client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
msg: ServerMessage,
) -> Result<()> {
match msg { match msg {
Message::Output(output) => self.recv_response(output).await?, ServerMessage::Output(output) => self.process_request_response(output).await?,
Message::Call(call) => { ServerMessage::Call(call) => {
self.incoming.send(call).unwrap(); client_tx
.send((self.id, call))
.context("failed to send a message to server")?;
// let notification = Notification::parse(&method, params); // let notification = Notification::parse(&method, params);
} }
}; };
Ok(()) Ok(())
} }
async fn recv_response(&mut self, output: jsonrpc::Output) -> io::Result<()> { async fn process_request_response(&self, output: jsonrpc::Output) -> Result<()> {
let (id, result) = match output { let (id, result) = match output {
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => { jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
info!("<- {}", result); info!("<- {}", result);
@@ -186,6 +197,8 @@ impl Transport {
let tx = self let tx = self
.pending_requests .pending_requests
.lock()
.await
.remove(&id) .remove(&id)
.expect("pending_request with id not found!"); .expect("pending_request with id not found!");
@@ -200,30 +213,51 @@ impl Transport {
Ok(()) Ok(())
} }
pub async fn duplex(mut self) { async fn recv(
transport: Arc<Self>,
mut server_stdout: BufReader<ChildStdout>,
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
) {
let mut recv_buffer = String::new();
loop { loop {
tokio::select! { match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
// client -> server Ok(msg) => {
msg = self.outgoing.recv() => { transport
if msg.is_none() { .process_server_message(&client_tx, msg)
.await
.unwrap();
}
Err(err) => {
error!("err: <- {:?}", err);
break; 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); async fn err(_transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
let mut recv_buffer = String::new();
loop {
match Self::recv_server_error(&mut server_stderr, &mut recv_buffer).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
break; break;
} }
let msg = msg.unwrap(); }
}
}
self.recv_msg(msg).await.unwrap(); async fn send(
} transport: Arc<Self>,
_msg = Self::err(&mut self.stderr) => {} mut server_stdin: BufWriter<ChildStdin>,
} mut client_rx: UnboundedReceiver<Payload>,
) {
while let Some(msg) = client_rx.recv().await {
transport
.send_payload_to_server(&mut server_stdin, msg)
.await
.unwrap()
} }
} }
} }

View File

@@ -1,16 +1,21 @@
[package] [package]
name = "helix-syntax" name = "helix-syntax"
version = "0.0.10" version = "0.4.1"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2018"
license = "MPL-2.0" license = "MPL-2.0"
description = "Tree-sitter grammars support"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"]
[dependencies] [dependencies]
tree-sitter = "0.19" tree-sitter = "0.19"
serde = { version = "1.0", features = ["derive"] } libloading = "0.7"
anyhow = "1"
[build-dependencies] [build-dependencies]
cc = { version = "1", features = ["parallel"] } cc = { version = "1" }
threadpool = { version = "1.0" } threadpool = { version = "1.0" }
anyhow = "1"

View File

@@ -1,79 +1,147 @@
use anyhow::{anyhow, Context, Result};
use std::fs; use std::fs;
use std::path::PathBuf; use std::time::SystemTime;
use std::{
path::{Path, PathBuf},
process::Command,
};
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
fn collect_tree_sitter_dirs(ignore: &[String]) -> Vec<String> { fn collect_tree_sitter_dirs(ignore: &[String]) -> Result<Vec<String>> {
let mut dirs = Vec::new(); let mut dirs = Vec::new();
for entry in fs::read_dir("languages").unwrap().flatten() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("languages");
let path = entry.path();
let dir = path.file_name().unwrap().to_str().unwrap().to_string();
if !ignore.contains(&dir) {
dirs.push(dir);
}
}
dirs
}
fn collect_src_files(dir: &str) -> (Vec<String>, Vec<String>) { for entry in fs::read_dir(path)? {
eprintln!("Collect files for {}", dir); let entry = entry?;
let mut c_files = Vec::new();
let mut cpp_files = Vec::new();
let path = PathBuf::from("languages").join(&dir).join("src");
for entry in fs::read_dir(path).unwrap().flatten() {
let path = entry.path(); let path = entry.path();
if path
.file_stem() if !entry.file_type()?.is_dir() {
.unwrap()
.to_str()
.unwrap()
.starts_with("binding")
{
continue; continue;
} }
if let Some(ext) = path.extension() {
if ext == "c" { let dir = path.file_name().unwrap().to_str().unwrap().to_string();
c_files.push(path.to_str().unwrap().to_string());
} else if ext == "cc" || ext == "cpp" || ext == "cxx" { // filter ignores
cpp_files.push(path.to_str().unwrap().to_string()); if ignore.contains(&dir) {
continue;
} }
} dirs.push(dir)
}
(c_files, cpp_files)
} }
fn build_c(files: Vec<String>, language: &str) { Ok(dirs)
let mut build = cc::Build::new();
for file in files {
build
.file(&file)
.include(PathBuf::from(file).parent().unwrap())
.pic(true)
.warnings(false);
}
build.compile(&format!("tree-sitter-{}-c", language));
} }
fn build_cpp(files: Vec<String>, language: &str) { #[cfg(unix)]
let mut build = cc::Build::new(); const DYLIB_EXTENSION: &str = "so";
let flag = if build.get_compiler().is_like_msvc() { #[cfg(windows)]
"/std:c++17" const DYLIB_EXTENSION: &str = "dll";
fn build_library(src_path: &Path, language: &str) -> Result<()> {
let header_path = src_path;
// let grammar_path = src_path.join("grammar.json");
let parser_path = src_path.join("parser.c");
let mut scanner_path = src_path.join("scanner.c");
let scanner_path = if scanner_path.exists() {
Some(scanner_path)
} else { } else {
"-std=c++14" scanner_path.set_extension("cc");
}; if scanner_path.exists() {
Some(scanner_path)
for file in files { } else {
build None
.file(&file)
.include(PathBuf::from(file).parent().unwrap())
.pic(true)
.warnings(false)
.cpp(true)
.flag_if_supported(flag);
} }
build.compile(&format!("tree-sitter-{}-cpp", language)); };
let parser_lib_path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../runtime/grammars");
let mut library_path = parser_lib_path.join(language);
library_path.set_extension(DYLIB_EXTENSION);
let recompile = needs_recompile(&library_path, &parser_path, &scanner_path)
.with_context(|| "Failed to compare source and binary timestamps")?;
if !recompile {
return Ok(());
}
let mut config = cc::Build::new();
config.cpp(true).opt_level(2).cargo_metadata(false);
let compiler = config.get_compiler();
let mut command = Command::new(compiler.path());
command.current_dir(src_path);
for (key, value) in compiler.env() {
command.env(key, value);
}
if cfg!(windows) {
command
.args(&["/nologo", "/LD", "/I"])
.arg(header_path)
.arg("/Od");
if let Some(scanner_path) = scanner_path.as_ref() {
command.arg(scanner_path);
}
command
.arg(parser_path)
.arg("/link")
.arg(format!("/out:{}", library_path.to_str().unwrap()));
} else {
command
.arg("-shared")
.arg("-fPIC")
.arg("-fno-exceptions")
.arg("-g")
.arg("-I")
.arg(header_path)
.arg("-o")
.arg(&library_path)
.arg("-O2");
if let Some(scanner_path) = scanner_path.as_ref() {
if scanner_path.extension() == Some("c".as_ref()) {
command.arg("-xc").arg("-std=c99").arg(scanner_path);
} else {
command.arg(scanner_path);
}
}
command.arg("-xc").arg(parser_path);
}
let output = command
.output()
.with_context(|| "Failed to execute C compiler")?;
if !output.status.success() {
return Err(anyhow!(
"Parser compilation failed.\nStdout: {}\nStderr: {}",
String::from_utf8_lossy(&output.stdout),
String::from_utf8_lossy(&output.stderr)
));
}
Ok(())
}
fn needs_recompile(
lib_path: &Path,
parser_c_path: &Path,
scanner_path: &Option<PathBuf>,
) -> Result<bool> {
if !lib_path.exists() {
return Ok(true);
}
let lib_mtime = mtime(lib_path)?;
if mtime(parser_c_path)? > lib_mtime {
return Ok(true);
}
if let Some(scanner_path) = scanner_path {
if mtime(scanner_path)? > lib_mtime {
return Ok(true);
}
}
Ok(false)
}
fn mtime(path: &Path) -> Result<SystemTime> {
Ok(fs::metadata(path)?.modified()?)
} }
fn build_dir(dir: &str, language: &str) { fn build_dir(dir: &str, language: &str) {
@@ -92,22 +160,21 @@ fn build_dir(dir: &str, language: &str) {
eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'."); eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'.");
std::process::exit(1); std::process::exit(1);
} }
let (c, cpp) = collect_src_files(dir);
if !c.is_empty() { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
build_c(c, language); .join("languages")
} .join(dir)
if !cpp.is_empty() { .join("src");
build_cpp(cpp, language);
} build_library(&path, language).unwrap();
} }
fn main() { fn main() {
let ignore = vec![ let ignore = vec![
"tree-sitter-typescript".to_string(), "tree-sitter-typescript".to_string(),
"tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34 "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 dirs = collect_tree_sitter_dirs(&ignore).unwrap();
let mut n_jobs = 0; let mut n_jobs = 0;
let pool = threadpool::Builder::new().build(); // by going through the builder, it'll use num_cpus let pool = threadpool::Builder::new().build(); // by going through the builder, it'll use num_cpus
@@ -118,7 +185,7 @@ fn main() {
n_jobs += 1; n_jobs += 1;
pool.execute(move || { pool.execute(move || {
let language = &dir[12..]; // skip tree-sitter- prefix let language = &dir.strip_prefix("tree-sitter-").unwrap();
build_dir(&dir, language); build_dir(&dir, language);
// report progress // report progress

View File

@@ -1,92 +1,31 @@
use serde::{Deserialize, Serialize}; use anyhow::{Context, Result};
use libloading::{Library, Symbol};
use tree_sitter::Language; use tree_sitter::Language;
#[macro_export] fn replace_dashes_with_underscores(name: &str) -> String {
macro_rules! mk_extern { name.replace('-', "_")
( $( $name:ident ),* ) => {
$(
extern "C" { pub fn $name() -> Language; }
)*
};
} }
#[cfg(unix)]
const DYLIB_EXTENSION: &str = "so";
#[macro_export] #[cfg(windows)]
macro_rules! mk_enum { const DYLIB_EXTENSION: &str = "dll";
( $( $camel:ident ),* ) => {
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Lang {
$(
$camel,
)*
}
};
}
#[macro_export] pub fn get_language(runtime_path: &std::path::Path, name: &str) -> Result<Language> {
macro_rules! mk_get_language { let name = name.to_ascii_lowercase();
( $( ($camel:ident, $name:ident) ),* ) => { let mut library_path = runtime_path.join("grammars").join(&name);
#[must_use] // TODO: duplicated under build
pub fn get_language(lang: Lang) -> Language { library_path.set_extension(DYLIB_EXTENSION);
unsafe {
match lang {
$(
Lang::$camel => $name(),
)*
}
}
}
};
}
#[macro_export] let library = unsafe { Library::new(&library_path) }
macro_rules! mk_get_language_name { .with_context(|| format!("Error opening dynamic library {:?}", &library_path))?;
( $( $camel:ident ),* ) => { let language_fn_name = format!("tree_sitter_{}", replace_dashes_with_underscores(&name));
#[must_use] let language = unsafe {
pub const fn get_language_name(lang: Lang) -> &'static str { let language_fn: Symbol<unsafe extern "C" fn() -> Language> = library
match lang { .get(language_fn_name.as_bytes())
$( .with_context(|| format!("Failed to load symbol {}", language_fn_name))?;
Lang::$camel => stringify!($camel), language_fn()
)*
}
}
}; };
std::mem::forget(library);
Ok(language)
} }
#[macro_export]
macro_rules! mk_langs {
( $( ($camel:ident, $name:ident) ),* ) => {
mk_extern!($( $name ),*);
mk_enum!($( $camel ),*);
mk_get_language!($( ($camel, $name) ),*);
mk_get_language_name!($( $camel ),*);
};
}
mk_langs!(
// 1) Name for enum
// 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),
(Css, tree_sitter_css),
(Elixir, tree_sitter_elixir),
(Go, tree_sitter_go),
// (Haskell, tree_sitter_haskell),
(Html, tree_sitter_html),
(Java, tree_sitter_java),
(Javascript, tree_sitter_javascript),
(Json, tree_sitter_json),
(Julia, tree_sitter_julia),
(Php, tree_sitter_php),
(Python, tree_sitter_python),
(Ruby, tree_sitter_ruby),
(Rust, tree_sitter_rust),
(Scala, tree_sitter_scala),
(Swift, tree_sitter_swift),
(Toml, tree_sitter_toml),
(Tsx, tree_sitter_tsx),
(Typescript, tree_sitter_typescript)
);

View File

@@ -1,32 +1,38 @@
[package] [package]
name = "helix-term" name = "helix-term"
version = "0.0.10" version = "0.4.1"
description = "A post-modern text editor." description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2018"
license = "MPL-2.0" 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] [features]
embed_runtime = ["helix-core/embed_runtime"]
[[bin]] [[bin]]
name = "hx" name = "hx"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
helix-core = { path = "../helix-core" } helix-core = { version = "0.4", path = "../helix-core" }
helix-view = { path = "../helix-view", features = ["term"]} helix-view = { version = "0.4", path = "../helix-view" }
helix-lsp = { path = "../helix-lsp"} helix-lsp = { version = "0.4", path = "../helix-lsp" }
anyhow = "1" anyhow = "1"
once_cell = "1.4" once_cell = "1.8"
tokio = { version = "1", features = ["full"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
num_cpus = "1" num_cpus = "1"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.19", features = ["event-stream"] } crossterm = { version = "0.20", features = ["event-stream"] }
signal-hook = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -39,7 +45,7 @@ log = "0.4"
fuzzy-matcher = "0.3" fuzzy-matcher = "0.3"
ignore = "0.4" ignore = "0.4"
# shellexpand = "2.1" # shellexpand = "2.1"
dirs-next = "2.0" # dirs-next = "2.0"
# markdown doc rendering # markdown doc rendering
pulldown-cmark = { version = "0.8", default-features = false } pulldown-cmark = { version = "0.8", default-features = false }
@@ -48,3 +54,6 @@ toml = "0.5"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
[target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100
signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] }

View File

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

View File

@@ -1,51 +1,94 @@
use helix_view::{document::Mode, Document, Editor, Theme, View}; use helix_core::syntax;
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{theme, Editor};
use crate::{compositor::Compositor, ui, Args}; use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui};
use log::{error, info}; use log::error;
use std::{ use std::{
future::Future, io::{stdout, Write},
io::{self, stdout, Stdout, Write},
path::PathBuf,
sync::Arc, sync::Arc,
time::Duration, time::{Duration, Instant},
}; };
use anyhow::Error; use anyhow::Error;
use crossterm::{ use crossterm::{
event::{Event, EventStream}, event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
execute, terminal, execute, terminal,
}; };
#[cfg(not(windows))]
use tui::layout::Rect; use {
signal_hook::{consts::signal, low_level},
use futures_util::stream::FuturesUnordered; signal_hook_tokio::Signals,
use std::pin::Pin; };
#[cfg(windows)]
type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>; type Signals = futures_util::stream::Empty<()>;
pub type LspCallback =
BoxFuture<Result<Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>, anyhow::Error>>;
pub type LspCallbacks = FuturesUnordered<LspCallback>;
pub type LspCallbackWrapper = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
pub struct Application { pub struct Application {
compositor: Compositor, compositor: Compositor,
editor: Editor, editor: Editor,
callbacks: LspCallbacks, // TODO should be separate to take only part of the config
config: Config,
// Currently never read from. Remove the `allow(dead_code)` when
// that changes.
#[allow(dead_code)]
theme_loader: Arc<theme::Loader>,
// Currently never read from. Remove the `allow(dead_code)` when
// that changes.
#[allow(dead_code)]
syn_loader: Arc<syntax::Loader>,
signals: Signals,
jobs: Jobs,
lsp_progress: LspProgressMap,
} }
impl Application { impl Application {
pub fn new(mut args: Args) -> Result<Self, Error> { pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
use helix_view::editor::Action; use helix_view::editor::Action;
let mut compositor = Compositor::new()?; let mut compositor = Compositor::new()?;
let size = compositor.size(); let size = compositor.size();
let mut editor = Editor::new(size);
compositor.push(Box::new(ui::EditorView::new())); 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(),
config.editor.clone(),
);
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
compositor.push(editor_view);
if !args.files.is_empty() { if !args.files.is_empty() {
let first = &args.files[0]; // we know it's not empty let first = &args.files[0]; // we know it's not empty
@@ -53,25 +96,42 @@ impl Application {
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(first.clone()))); compositor.push(Box::new(ui::file_picker(first.clone())));
} else { } else {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
for file in args.files { for file in args.files {
if file.is_dir() { if file.is_dir() {
return Err(anyhow::anyhow!( return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)" "expected a path to file, found a directory. (to open a directory pass it as first argument)"
)); ));
} else { } else {
editor.open(file, Action::VerticalSplit)?; editor.open(file.to_path_buf(), Action::Load)?;
} }
} }
editor.set_status(format!("Loaded {} files.", nr_of_files));
} }
} else { } else {
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
} }
let mut app = Self { editor.set_theme(theme);
#[cfg(windows)]
let signals = futures_util::stream::empty();
#[cfg(not(windows))]
let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT])?;
let app = Self {
compositor, compositor,
editor, editor,
callbacks: FuturesUnordered::new(), config,
theme_loader,
syn_loader,
signals,
jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(),
}; };
Ok(app) Ok(app)
@@ -80,11 +140,11 @@ impl Application {
fn render(&mut self) { fn render(&mut self) {
let editor = &mut self.editor; let editor = &mut self.editor;
let compositor = &mut self.compositor; let compositor = &mut self.compositor;
let callbacks = &mut self.callbacks; let jobs = &mut self.jobs;
let mut cx = crate::compositor::Context { let mut cx = crate::compositor::Context {
editor, editor,
callbacks, jobs,
scroll: None, scroll: None,
}; };
@@ -93,44 +153,78 @@ impl Application {
pub async fn event_loop(&mut self) { pub async fn event_loop(&mut self) {
let mut reader = EventStream::new(); let mut reader = EventStream::new();
let mut last_render = Instant::now();
let deadline = Duration::from_secs(1) / 60;
self.render(); self.render();
loop { loop {
if self.editor.should_close() { if self.editor.should_close() {
self.jobs.finish();
break; break;
} }
use futures_util::StreamExt; use futures_util::StreamExt;
tokio::select! { tokio::select! {
biased;
event = reader.next() => { event = reader.next() => {
self.handle_terminal_events(event) self.handle_terminal_events(event)
} }
Some(call) = self.editor.language_servers.incoming.next() => { Some(signal) = self.signals.next() => {
self.handle_language_server_message(call).await self.handle_signals(signal).await;
} }
Some(callback) = &mut self.callbacks.next() => { Some((id, call)) = self.editor.language_servers.incoming.next() => {
self.handle_language_server_callback(callback) self.handle_language_server_message(call, id).await;
} // limit render calls for fast language server messages
} let last = self.editor.language_servers.incoming.is_empty();
} if last || last_render.elapsed() > deadline {
}
pub fn handle_language_server_callback(
&mut self,
callback: Result<LspCallbackWrapper, anyhow::Error>,
) {
if let Ok(callback) = callback {
// TODO: handle Err()
callback(&mut self.editor, &mut self.compositor);
self.render(); self.render();
last_render = Instant::now();
}
}
Some(callback) = self.jobs.futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
}
Some(callback) = self.jobs.wait_futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
}
}
}
}
#[cfg(windows)]
// no signal handling available on windows
pub async fn handle_signals(&mut self, _signal: ()) {}
#[cfg(not(windows))]
pub async fn handle_signals(&mut self, signal: i32) {
use helix_view::graphics::Rect;
match signal {
signal::SIGTSTP => {
self.compositor.save_cursor();
self.restore_term().unwrap();
low_level::emulate_default_handler(signal::SIGTSTP).unwrap();
}
signal::SIGCONT => {
self.claim_term().await.unwrap();
// redraw the terminal
let Rect { width, height, .. } = self.compositor.size();
self.compositor.resize(width, height);
self.compositor.load_cursor();
self.render();
}
_ => unreachable!(),
} }
} }
pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) { pub fn handle_terminal_events(&mut self, event: Option<Result<Event, crossterm::ErrorKind>>) {
let mut cx = crate::compositor::Context { let mut cx = crate::compositor::Context {
editor: &mut self.editor, editor: &mut self.editor,
callbacks: &mut self.callbacks, jobs: &mut self.jobs,
scroll: None, scroll: None,
}; };
// Handle key events // Handle key events
@@ -151,8 +245,21 @@ impl Application {
} }
} }
pub async fn handle_language_server_message(&mut self, call: helix_lsp::Call) { pub async fn handle_language_server_message(
use helix_lsp::{Call, Notification}; &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 { match call {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => { Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) { let notification = match Notification::parse(&method, params) {
@@ -160,7 +267,6 @@ impl Application {
None => return, None => return,
}; };
// TODO: parse should return Result/Option
match notification { match notification {
Notification::PublishDiagnostics(params) => { Notification::PublishDiagnostics(params) => {
let path = Some(params.uri.to_file_path().unwrap()); let path = Some(params.uri.to_file_path().unwrap());
@@ -177,29 +283,39 @@ impl Application {
let diagnostics = params let diagnostics = params
.diagnostics .diagnostics
.into_iter() .into_iter()
.map(|diagnostic| { .filter_map(|diagnostic| {
use helix_core::{ use helix_core::{
diagnostic::{Range, Severity, Severity::*}, diagnostic::{Range, Severity::*},
Diagnostic, Diagnostic,
}; };
use helix_lsp::{lsp, util::lsp_pos_to_pos};
use lsp::DiagnosticSeverity; use lsp::DiagnosticSeverity;
let language_server = doc.language_server().unwrap(); let language_server = doc.language_server().unwrap();
// TODO: convert inside server // TODO: convert inside server
let start = lsp_pos_to_pos( let start = if let Some(start) = lsp_pos_to_pos(
text, text,
diagnostic.range.start, diagnostic.range.start,
language_server.offset_encoding(), 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, text,
diagnostic.range.end, diagnostic.range.end,
language_server.offset_encoding(), language_server.offset_encoding(),
); ) {
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
Diagnostic { Some(Diagnostic {
range: Range { start, end }, range: Range { start, end },
line: diagnostic.range.start.line as usize, line: diagnostic.range.start.line as usize,
message: diagnostic.message, message: diagnostic.message,
@@ -213,13 +329,11 @@ impl Application {
), ),
// code // code
// source // source
} })
}) })
.collect(); .collect();
doc.set_diagnostics(diagnostics); doc.set_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();
} }
} }
Notification::ShowMessage(params) => { Notification::ShowMessage(params) => {
@@ -228,12 +342,140 @@ impl Application {
Notification::LogMessage(params) => { Notification::LogMessage(params) => {
log::warn!("unhandled window/logMessage: {:?}", params); log::warn!("unhandled window/logMessage: {:?}", params);
} }
_ => unreachable!(), Notification::ProgressMessage(params) => {
} let lsp::ProgressParams { token, value } = params;
}
Call::MethodCall(call) => {
error!("Method not found {}", call.method);
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);
}
}
}
}
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method, params, 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( // self.language_server.reply(
// call.id, // call.id,
// // TODO: make a Into trait that can cast to Err(jsonrpc::Error) // // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
@@ -248,29 +490,48 @@ impl Application {
} }
} }
pub async fn run(&mut self) -> Result<(), Error> { async fn claim_term(&mut self) -> Result<(), Error> {
terminal::enable_raw_mode()?; terminal::enable_raw_mode()?;
let mut stdout = stdout(); let mut stdout = stdout();
execute!(stdout, terminal::EnterAlternateScreen)?; execute!(stdout, terminal::EnterAlternateScreen)?;
if self.config.editor.mouse {
execute!(stdout, EnableMouseCapture)?;
}
Ok(())
}
fn restore_term(&mut self) -> Result<(), Error> {
let mut stdout = stdout();
// reset cursor shape
write!(stdout, "\x1B[2 q")?;
execute!(stdout, DisableMouseCapture)?;
execute!(stdout, terminal::LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
Ok(())
}
pub async fn run(&mut self) -> Result<(), Error> {
self.claim_term().await?;
// Exit the alternate screen and disable raw mode before panicking // Exit the alternate screen and disable raw mode before panicking
let hook = std::panic::take_hook(); let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| { std::panic::set_hook(Box::new(move |info| {
execute!(std::io::stdout(), terminal::LeaveAlternateScreen); // We can't handle errors properly inside this closure. And it's
terminal::disable_raw_mode(); // probably not a good idea to `unwrap()` inside a panic handler.
// So we just ignore the `Result`s.
let _ = execute!(std::io::stdout(), DisableMouseCapture);
let _ = execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
let _ = terminal::disable_raw_mode();
hook(info); hook(info);
})); }));
self.event_loop().await; self.event_loop().await;
// reset cursor shape if self.editor.close_language_servers(None).await.is_err() {
write!(stdout, "\x1B[2 q"); log::error!("Timed out waiting for language servers to shutdown");
};
execute!(stdout, terminal::LeaveAlternateScreen)?; self.restore_term()?;
terminal::disable_raw_mode()?;
Ok(()) Ok(())
} }

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,11 @@
// Each component declares it's own size constraints and gets fitted based on it's parent. // Each component declares it's own size constraints and gets fitted based on it's parent.
// Q: how does this work with popups? // Q: how does this work with popups?
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>) // cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
use helix_core::Position;
use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event; use crossterm::event::Event;
use helix_core::Position; use tui::buffer::Buffer as Surface;
use tui::{buffer::Buffer as Surface, layout::Rect};
pub type Callback = Box<dyn FnOnce(&mut Compositor)>; pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
@@ -24,17 +25,17 @@ pub enum EventResult {
use helix_view::Editor; use helix_view::Editor;
use crate::application::LspCallbacks; use crate::job::Jobs;
pub struct Context<'a> { pub struct Context<'a> {
pub editor: &'a mut Editor, pub editor: &'a mut Editor,
pub scroll: Option<usize>, pub scroll: Option<usize>,
pub callbacks: &'a mut LspCallbacks, pub jobs: &'a mut Jobs,
} }
pub trait Component: Any + AnyComponent { pub trait Component: Any + AnyComponent {
/// Process input events, return true if handled. /// Process input events, return true if handled.
fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult { fn handle_event(&mut self, _event: Event, _ctx: &mut Context) -> EventResult {
EventResult::Ignored EventResult::Ignored
} }
// , args: () // , args: ()
@@ -45,15 +46,16 @@ pub trait Component: Any + AnyComponent {
} }
/// Render the component onto the provided surface. /// Render the component onto the provided surface.
fn render(&self, area: Rect, frame: &mut Surface, ctx: &mut Context); fn render(&mut self, area: Rect, frame: &mut Surface, ctx: &mut Context);
fn cursor_position(&self, area: Rect, ctx: &Editor) -> Option<Position> { /// Get cursor position and cursor kind.
None 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. /// May be used by the parent component to compute the child area.
/// viewport is the maximum allowed area, and the child should stay within those bounds. /// viewport is the maximum allowed area, and the child should stay within those bounds.
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
// TODO: for scrolling, the scroll wrapper should place a size + offset on the Context // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context
// that way render can use it // that way render can use it
None None
@@ -66,21 +68,24 @@ pub trait Component: Any + AnyComponent {
use anyhow::Error; use anyhow::Error;
use std::io::stdout; use std::io::stdout;
use tui::backend::CrosstermBackend; use tui::backend::{Backend, CrosstermBackend};
type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>; type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
pub struct Compositor { pub struct Compositor {
layers: Vec<Box<dyn Component>>, layers: Vec<Box<dyn Component>>,
terminal: Terminal, terminal: Terminal,
pub(crate) last_picker: Option<Box<dyn Component>>,
} }
impl Compositor { impl Compositor {
pub fn new() -> Result<Self, Error> { pub fn new() -> Result<Self, Error> {
let backend = CrosstermBackend::new(stdout()); let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?; let terminal = Terminal::new(backend)?;
Ok(Self { Ok(Self {
layers: Vec::new(), layers: Vec::new(),
terminal, terminal,
last_picker: None,
}) })
} }
@@ -94,6 +99,21 @@ impl Compositor {
.expect("Unable to resize terminal") .expect("Unable to resize terminal")
} }
pub fn save_cursor(&mut self) {
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal
.backend_mut()
.show_cursor(CursorKind::Block)
.ok();
}
}
pub fn load_cursor(&mut self) {
if self.terminal.cursor_kind() == CursorKind::Hidden {
self.terminal.backend_mut().hide_cursor().ok();
}
}
pub fn push(&mut self, mut layer: Box<dyn Component>) { pub fn push(&mut self, mut layer: Box<dyn Component>) {
let size = self.size(); let size = self.size();
// trigger required_size on init // trigger required_size on init
@@ -101,8 +121,8 @@ impl Compositor {
self.layers.push(layer); self.layers.push(layer);
} }
pub fn pop(&mut self) { pub fn pop(&mut self) -> Option<Box<dyn Component>> {
self.layers.pop(); self.layers.pop()
} }
pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool { pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool {
@@ -122,8 +142,7 @@ impl Compositor {
} }
pub fn render(&mut self, cx: &mut Context) { pub fn render(&mut self, cx: &mut Context) {
let area = self self.terminal
.terminal
.autoresize() .autoresize()
.expect("Unable to determine terminal size"); .expect("Unable to determine terminal size");
@@ -133,24 +152,23 @@ impl Compositor {
let area = *surface.area(); let area = *surface.area();
for layer in &self.layers { for layer in &mut self.layers {
layer.render(area, surface, cx) layer.render(area, surface, cx);
} }
let pos = self let (pos, kind) = self.cursor(area, cx.editor);
.cursor_position(area, cx.editor) let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos); self.terminal.draw(pos, kind).unwrap();
} }
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() { for layer in self.layers.iter().rev() {
if let Some(pos) = layer.cursor_position(area, editor) { if let (Some(pos), kind) = layer.cursor(area, editor) {
return Some(pos); return (Some(pos), kind);
} }
} }
None (None, CursorKind::Hidden)
} }
pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> { pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> {
@@ -182,10 +200,9 @@ pub trait AnyComponent {
/// # Examples /// # Examples
/// ///
/// ```rust /// ```rust
/// # use cursive_core::views::TextComponent; /// use helix_term::{ui::Text, compositor::Component};
/// # use cursive_core::view::Component; /// let boxed: Box<Component> = Box::new(Text::new("text".to_string()));
/// let boxed: Box<Component> = Box::new(TextComponent::new("text")); /// let text: Box<Text> = boxed.as_boxed_any().downcast().unwrap();
/// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
/// ``` /// ```
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>; fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
} }

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

@@ -0,0 +1,53 @@
use serde::Deserialize;
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,
#[serde(default)]
pub editor: helix_view::editor::Config,
}
#[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 crate::keymap;
use crate::keymap::Keymap;
use helix_core::hashmap;
use helix_view::document::Mode;
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 => Keymap::new(keymap!({ "Insert mode"
"y" => move_line_down,
"S-C-a" => delete_selection,
})),
Mode::Normal => Keymap::new(keymap!({ "Normal mode"
"A-F12" => move_next_word_end,
})),
}),
..Default::default()
}
);
}

100
helix-term/src/job.rs Normal file
View File

@@ -0,0 +1,100 @@
use helix_view::Editor;
use crate::compositor::Compositor;
use futures_util::future::{self, BoxFuture, Future, FutureExt};
use futures_util::stream::{FuturesUnordered, StreamExt};
pub type Callback = Box<dyn FnOnce(&mut Editor, &mut Compositor) + Send>;
pub type JobFuture = BoxFuture<'static, anyhow::Result<Option<Callback>>>;
pub struct Job {
pub future: BoxFuture<'static, anyhow::Result<Option<Callback>>>,
/// Do we need to wait for this job to finish before exiting?
pub wait: bool,
}
#[derive(Default)]
pub struct Jobs {
pub futures: FuturesUnordered<JobFuture>,
/// These are the ones that need to complete before we exit.
pub wait_futures: FuturesUnordered<JobFuture>,
}
impl Job {
pub fn new<F: Future<Output = anyhow::Result<()>> + Send + 'static>(f: F) -> Job {
Job {
future: f.map(|r| r.map(|()| None)).boxed(),
wait: false,
}
}
pub fn with_callback<F: Future<Output = anyhow::Result<Callback>> + Send + 'static>(
f: F,
) -> Job {
Job {
future: f.map(|r| r.map(Some)).boxed(),
wait: false,
}
}
pub fn wait_before_exiting(mut self) -> Job {
self.wait = true;
self
}
}
impl Jobs {
pub fn new() -> Jobs {
Jobs::default()
}
pub fn spawn<F: Future<Output = anyhow::Result<()>> + Send + 'static>(&mut self, f: F) {
self.add(Job::new(f));
}
pub fn callback<F: Future<Output = anyhow::Result<Callback>> + Send + 'static>(
&mut self,
f: F,
) {
self.add(Job::with_callback(f));
}
pub fn handle_callback(
&mut self,
editor: &mut Editor,
compositor: &mut Compositor,
call: anyhow::Result<Option<Callback>>,
) {
match call {
Ok(None) => {}
Ok(Some(call)) => {
call(editor, compositor);
}
Err(e) => {
editor.set_error(format!("Async job failed: {}", e));
}
}
}
pub async fn next_job(&mut self) -> Option<anyhow::Result<Option<Callback>>> {
tokio::select! {
event = self.futures.next() => { event }
event = self.wait_futures.next() => { event }
}
}
pub fn add(&mut self, j: Job) {
if j.wait {
self.wait_futures.push(j.future);
} else {
self.futures.push(j.future);
}
}
/// Blocks until all the jobs that need to be waited on are done.
pub fn finish(&mut self) {
let wait_futures = std::mem::take(&mut self.wait_futures);
helix_lsp::block_on(wait_futures.for_each(|_| future::ready(())));
}
}

View File

@@ -1,372 +1,629 @@
use crate::commands::{self, Command}; pub use crate::commands::Command;
use crate::config::Config;
use helix_core::hashmap; use helix_core::hashmap;
use helix_view::document::Mode; use helix_view::{document::Mode, info::Info, input::KeyEvent};
use std::collections::HashMap; use serde::Deserialize;
use std::{
// Kakoune-inspired: collections::HashMap,
// mode = { ops::{Deref, DerefMut},
// normal = { };
// q = record_macro
// w = (next) word
// W = next WORD
// e = end of word
// E = end of WORD
// r =
// t = 'till char
// y = yank
// u = undo
// U = redo
// i = insert
// I = INSERT (start of line)
// o = open below (insert on new line below)
// O = open above (insert on new line above)
// p = paste (before cursor)
// P = PASTE (after cursor)
// ` =
// [ = select to text object start (alt = select whole object)
// ] = select to text object end
// { = extend to inner object start
// } = extend to inner object end
// a = append
// A = APPEND (end of line)
// s = split
// S = select
// d = delete()
// f = find_char()
// g = goto (gg, G, gc, gd, etc)
//
// 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
// ` = convert case? (to lower) (alt = swap case)
// ~ = convert to upper case
// . = repeat last command
// \ = disable hook?
// / = search
// > = indent
// < = deindent
// % = select whole buffer (in vim = jump to matching bracket)
// * = search pattern in selection
// ( = rotate main selection backward
// ) = rotate main selection forward
// - = trim selections? (alt = merge contiguous sel together)
// @ = convert tabs to spaces
// & = align cursor
// ? = extend to next given regex match (alt = to prev)
//
// 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) || Home = start of line(first non blank char)
// $ = end of line || End = end of line
//
// z = save selections
// Z = restore selections
// x = select line
// X = extend line
// c = change selected text
// C = copy selection?
// v = view menu (viewport manipulation)
// b = select to previous word start
// B = select to previous WORD start
//
//
//
//
//
//
// = = align?
// + =
// }
//
// 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_export]
macro_rules! key { macro_rules! key {
($key:ident) => {
KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
}
};
($($ch:tt)*) => { ($($ch:tt)*) => {
KeyEvent { KeyEvent {
code: KeyCode::Char($($ch)*), code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: KeyModifiers::NONE, modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
} }
}; };
} }
macro_rules! ctrl { /// Macro for defining the root of a `Keymap` object. Example:
($($ch:tt)*) => { ///
KeyEvent { /// ```
code: KeyCode::Char($($ch)*), /// # use helix_core::hashmap;
modifiers: KeyModifiers::CONTROL, /// # use helix_term::keymap;
/// # use helix_term::keymap::Keymap;
/// let normal_mode = keymap!({ "Normal mode"
/// "i" => insert_mode,
/// "g" => { "Goto"
/// "g" => goto_file_start,
/// "e" => goto_file_end,
/// },
/// "j" | "down" => move_line_down,
/// });
/// let keymap = Keymap::new(normal_mode);
/// ```
#[macro_export]
macro_rules! keymap {
(@trie $cmd:ident) => {
$crate::keymap::KeyTrie::Leaf($crate::commands::Command::$cmd)
};
(@trie
{ $label:literal $($($key:literal)|+ => $value:tt,)+ }
) => {
keymap!({ $label $($($key)|+ => $value,)+ })
};
(
{ $label:literal $($($key:literal)|+ => $value:tt,)+ }
) => {
// modified from the hashmap! macro
{
let _cap = hashmap!(@count $($($key),+),*);
let mut _map = ::std::collections::HashMap::with_capacity(_cap);
let mut _order = ::std::vec::Vec::with_capacity(_cap);
$(
$(
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
_map.insert(
_key,
keymap!(@trie $value)
);
_order.push(_key);
)+
)*
$crate::keymap::KeyTrie::Node($crate::keymap::KeyTrieNode::new($label, _map, _order))
} }
}; };
} }
macro_rules! alt { #[derive(Debug, Clone, Deserialize)]
($($ch:tt)*) => { pub struct KeyTrieNode {
KeyEvent { /// A label for keys coming under this node, like "Goto mode"
code: KeyCode::Char($($ch)*), #[serde(skip)]
modifiers: KeyModifiers::ALT, name: String,
#[serde(flatten)]
map: HashMap<KeyEvent, KeyTrie>,
#[serde(skip)]
order: Vec<KeyEvent>,
} }
impl KeyTrieNode {
pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
Self {
name: name.to_string(),
map,
order,
}
}
pub fn name(&self) -> &str {
&self.name
}
/// Merge another Node in. Leaves and subnodes from the other node replace
/// corresponding keyevent in self, except when both other and self have
/// subnodes for same key. In that case the merge is recursive.
pub fn merge(&mut self, mut other: Self) {
for (key, trie) in std::mem::take(&mut other.map) {
if let Some(KeyTrie::Node(node)) = self.map.get_mut(&key) {
if let KeyTrie::Node(other_node) = trie {
node.merge(other_node);
continue;
}
}
self.map.insert(key, trie);
}
for &key in self.map.keys() {
if !self.order.contains(&key) {
self.order.push(key);
}
}
}
}
impl From<KeyTrieNode> for Info {
fn from(node: KeyTrieNode) -> Self {
let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(node.len());
for (&key, trie) in node.iter() {
let desc = match trie {
KeyTrie::Leaf(cmd) => cmd.doc(),
KeyTrie::Node(n) => n.name(),
}; };
match body.iter().position(|(d, _)| d == &desc) {
// FIXME: multiple keys are ordered randomly (use BTreeSet)
Some(pos) => body[pos].1.push(key),
None => body.push((desc, vec![key])),
}
}
body.sort_unstable_by_key(|(_, keys)| {
node.order.iter().position(|&k| k == keys[0]).unwrap()
});
let prefix = format!("{} ", node.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
body = body
.into_iter()
.map(|(desc, keys)| (desc.strip_prefix(&prefix).unwrap(), keys))
.collect();
}
Info::key(node.name(), body)
}
} }
pub fn default() -> Keymaps { impl Default for KeyTrieNode {
let normal = hashmap!( fn default() -> Self {
key!('h') => commands::move_char_left as Command, Self::new("", HashMap::new(), Vec::new())
key!('j') => commands::move_line_down, }
key!('k') => commands::move_line_up, }
key!('l') => commands::move_char_right,
KeyEvent { impl PartialEq for KeyTrieNode {
code: KeyCode::Left, fn eq(&self, other: &Self) -> bool {
modifiers: KeyModifiers::NONE self.map == other.map
} => commands::move_char_left, }
KeyEvent { }
code: KeyCode::Down,
modifiers: KeyModifiers::NONE
} => commands::move_line_down,
KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE
} => commands::move_line_up,
KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::NONE
} => commands::move_char_right,
key!('t') => commands::find_till_char, impl Deref for KeyTrieNode {
key!('f') => commands::find_next_char, type Target = HashMap<KeyEvent, KeyTrie>;
key!('T') => commands::till_prev_char,
key!('F') => commands::find_prev_char,
// and matching set for select mode (extend)
//
key!('r') => commands::replace,
KeyEvent { fn deref(&self) -> &Self::Target {
code: KeyCode::Home, &self.map
modifiers: KeyModifiers::NONE }
} => commands::move_line_start, }
KeyEvent { impl DerefMut for KeyTrieNode {
code: KeyCode::End, fn deref_mut(&mut self) -> &mut Self::Target {
modifiers: KeyModifiers::NONE &mut self.map
} => commands::move_line_end, }
}
key!('w') => commands::move_next_word_start, #[derive(Debug, Clone, PartialEq, Deserialize)]
key!('b') => commands::move_prev_word_start, #[serde(untagged)]
key!('e') => commands::move_next_word_end, pub enum KeyTrie {
Leaf(Command),
Node(KeyTrieNode),
}
key!('v') => commands::select_mode, impl KeyTrie {
key!('g') => commands::goto_mode, pub fn node(&self) -> Option<&KeyTrieNode> {
key!(':') => commands::command_mode, match *self {
KeyTrie::Node(ref node) => Some(node),
KeyTrie::Leaf(_) => None,
}
}
key!('i') => commands::insert_mode, pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
key!('I') => commands::prepend_to_line, match *self {
key!('a') => commands::append_mode, KeyTrie::Node(ref mut node) => Some(node),
key!('A') => commands::append_to_line, KeyTrie::Leaf(_) => None,
key!('o') => commands::open_below, }
key!('O') => commands::open_above, }
/// Merge another KeyTrie in, assuming that this KeyTrie and the other
/// are both Nodes. Panics otherwise.
pub fn merge_nodes(&mut self, mut other: Self) {
let node = std::mem::take(other.node_mut().unwrap());
self.node_mut().unwrap().merge(node);
}
pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> {
let mut trie = self;
for key in keys {
trie = match trie {
KeyTrie::Node(map) => map.get(key),
// leaf encountered while keys left to process
KeyTrie::Leaf(_) => None,
}?
}
Some(trie)
}
}
#[derive(Debug, Clone, PartialEq)]
pub enum KeymapResult {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
Matched(Command),
/// Key was not found in the root keymap
NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto
/// and including current (invalid) key.
Cancelled(Vec<KeyEvent>),
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
pub struct Keymap {
/// Always a Node
#[serde(flatten)]
root: KeyTrie,
/// Stores pending keys waiting for the next key
#[serde(skip)]
state: Vec<KeyEvent>,
}
impl Keymap {
pub fn new(root: KeyTrie) -> Self {
Keymap {
root,
state: Vec::new(),
}
}
pub fn root(&self) -> &KeyTrie {
&self.root
}
/// Returns list of keys waiting to be disambiguated.
pub fn pending(&self) -> &[KeyEvent] {
&self.state
}
/// Lookup `key` in the keymap to try and find a command to execute
pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
let &first = self.state.get(0).unwrap_or(&key);
let trie = match self.root.search(&[first]) {
Some(&KeyTrie::Leaf(cmd)) => return KeymapResult::Matched(cmd),
None => return KeymapResult::NotFound,
Some(t) => t,
};
self.state.push(key);
match trie.search(&self.state[1..]) {
Some(&KeyTrie::Node(ref map)) => KeymapResult::Pending(map.clone()),
Some(&KeyTrie::Leaf(command)) => {
self.state.clear();
KeymapResult::Matched(command)
}
None => KeymapResult::Cancelled(self.state.drain(..).collect()),
}
}
pub fn merge(&mut self, other: Self) {
self.root.merge_nodes(other.root);
}
}
impl Deref for Keymap {
type Target = KeyTrieNode;
fn deref(&self) -> &Self::Target {
self.root.node().unwrap()
}
}
impl Default for Keymap {
fn default() -> Self {
Self::new(KeyTrie::Node(KeyTrieNode::default()))
}
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(transparent)]
pub struct Keymaps(pub HashMap<Mode, Keymap>);
impl Keymaps {
/// Returns list of keys waiting to be disambiguated in current mode.
pub fn pending(&self) -> &[KeyEvent] {
self.0
.values()
.find_map(|keymap| match keymap.pending().is_empty() {
true => None,
false => Some(keymap.pending()),
})
.unwrap_or_default()
}
}
impl Deref for Keymaps {
type Target = HashMap<Mode, Keymap>;
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 = keymap!({ "Normal mode"
"h" | "left" => move_char_left,
"j" | "down" => move_line_down,
"k" | "up" => move_line_up,
"l" | "right" => move_char_right,
"t" => find_till_char,
"f" => find_next_char,
"T" => till_prev_char,
"F" => find_prev_char,
"r" => replace,
"R" => replace_with_yanked,
"~" => switch_case,
"`" => switch_to_lowercase,
"A-`" => switch_to_uppercase,
"home" => goto_line_start,
"end" => goto_line_end,
"w" => move_next_word_start,
"b" => move_prev_word_start,
"e" => move_next_word_end,
"W" => move_next_long_word_start,
"B" => move_prev_long_word_start,
"E" => move_next_long_word_end,
"v" => select_mode,
"G" => goto_line,
"g" => { "Goto"
"g" => goto_file_start,
"e" => goto_last_line,
"h" => goto_line_start,
"l" => goto_line_end,
"s" => goto_first_nonwhitespace,
"d" => goto_definition,
"y" => goto_type_definition,
"r" => goto_reference,
"i" => goto_implementation,
"t" => goto_window_top,
"m" => goto_window_middle,
"b" => goto_window_bottom,
"a" => goto_last_accessed_file,
},
":" => command_mode,
"i" => insert_mode,
"I" => prepend_to_line,
"a" => append_mode,
"A" => append_to_line,
"o" => open_below,
"O" => open_above,
// [<space> ]<space> equivalents too (add blank new line, no edit) // [<space> ]<space> equivalents too (add blank new line, no edit)
"d" => delete_selection,
key!('d') => commands::delete_selection,
// TODO: also delete without yanking // TODO: also delete without yanking
key!('c') => commands::change_selection, "c" => change_selection,
// TODO: also change delete without yanking // TODO: also change delete without yanking
// key!('r') => commands::replace_with_char, "C" => copy_selection_on_next_line,
"A-C" => copy_selection_on_prev_line,
key!('s') => commands::select_regex,
alt!('s') => commands::split_selection_on_newline,
key!('S') => commands::split_selection,
key!(';') => commands::collapse_selection,
alt!(';') => commands::flip_selections,
key!('%') => commands::select_all,
key!('x') => commands::select_line,
key!('X') => commands::extend_line,
// or select mode X?
// extend_to_whole_line, crop_to_whole_line
key!('m') => commands::match_brackets, "s" => select_regex,
// TODO: refactor into "A-s" => split_selection_on_newline,
// key!('m') => commands::select_to_matching, "S" => split_selection,
// key!('M') => commands::back_select_to_matching, ";" => collapse_selection,
// select mode extend equivalents "A-;" => flip_selections,
"%" => select_all,
"x" => extend_line,
"X" => extend_to_line_bounds,
// crop_to_whole_line
// key!('.') => commands::repeat_insert, "m" => { "Match"
// repeat_select "m" => match_brackets,
"s" => surround_add,
"r" => surround_replace,
"d" => surround_delete,
"a" => select_textobject_around,
"i" => select_textobject_inner,
},
"[" => { "Left bracket"
"d" => goto_prev_diag,
"D" => goto_first_diag,
},
"]" => { "Right bracket"
"d" => goto_next_diag,
"D" => goto_last_diag,
},
// TODO: figure out what key to use "/" => search,
// key!('[') => commands::expand_selection, ??
key!('[') => commands::left_bracket_mode,
key!(']') => commands::right_bracket_mode,
key!('/') => commands::search,
// ? for search_reverse // ? for search_reverse
key!('n') => commands::search_next, "n" => search_next,
key!('N') => commands::extend_search_next, "N" => extend_search_next,
// N for search_prev // N for search_prev
key!('*') => commands::search_selection, "*" => search_selection,
key!('u') => commands::undo, "u" => undo,
key!('U') => commands::redo, "U" => redo,
key!('y') => commands::yank, "y" => yank,
// yank_all // yank_all
key!('p') => commands::paste_after, "p" => paste_after,
// paste_all // paste_all
key!('P') => commands::paste_before, "P" => paste_before,
key!('>') => commands::indent, ">" => indent,
key!('<') => commands::unindent, "<" => unindent,
key!('=') => commands::format_selections, "=" => format_selections,
key!('J') => commands::join_selections, "J" => join_selections,
// TODO: conflicts hover/doc // TODO: conflicts hover/doc
key!('K') => commands::keep_selections, "K" => keep_selections,
// TODO: and another method for inverse // TODO: and another method for inverse
// TODO: clashes with space mode // TODO: clashes with space mode
key!(' ') => commands::keep_primary_selection, "space" => keep_primary_selection,
// key!('q') => commands::record_macro, // "q" => record_macro,
// key!('Q') => commands::replay_macro, // "Q" => replay_macro,
// ~ / apostrophe => change case
// & align selections // & align selections
// _ trim selections // _ trim selections
// C / altC = copy (repeat) selections on prev/next lines "(" => rotate_selections_backward,
")" => rotate_selections_forward,
"A-(" => rotate_selection_contents_backward,
"A-)" => rotate_selection_contents_forward,
KeyEvent { "esc" => normal_mode,
code: KeyCode::Esc, "C-b" | "pageup" => page_up,
modifiers: KeyModifiers::NONE "C-f" | "pagedown" => page_down,
} => commands::normal_mode, "C-u" => half_page_up,
KeyEvent { "C-d" => half_page_down,
code: KeyCode::PageUp,
modifiers: KeyModifiers::NONE
} => commands::page_up,
ctrl!('b') => commands::page_up,
KeyEvent {
code: KeyCode::PageDown,
modifiers: KeyModifiers::NONE
} => commands::page_down,
ctrl!('f') => commands::page_down,
ctrl!('u') => commands::half_page_up,
ctrl!('d') => commands::half_page_down,
ctrl!('w') => commands::window_mode, "C-w" => { "Window"
"C-w" | "w" => rotate_view,
"C-h" | "h" => hsplit,
"C-v" | "v" => vsplit,
"C-q" | "q" => wclose,
},
// move under <space>c // move under <space>c
ctrl!('c') => commands::toggle_comments, "C-c" => toggle_comments,
key!('K') => commands::hover, "K" => hover,
// z family for save/restore/combine from/to sels from register // z family for save/restore/combine from/to sels from register
ctrl!('i') => commands::jump_forward, // TODO: ctrl-i conflicts tab "tab" => jump_forward, // tab == <C-i>
ctrl!('o') => commands::jump_backward, "C-o" => jump_backward,
// ctrl!('s') => commands::save_selection, // "C-s" => save_selection,
key!(' ') => commands::space_mode, "space" => { "Space"
key!('z') => commands::view_mode, "f" => file_picker,
); "b" => buffer_picker,
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether "s" => symbol_picker,
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird "a" => code_action,
// because some selection operations can now be done from normal mode, some from select mode. "'" => last_picker,
"w" => { "Window"
"C-w" | "w" => rotate_view,
"C-h" | "h" => hsplit,
"C-v" | "v" => vsplit,
"C-q" | "q" => wclose,
},
"y" => yank_joined_to_clipboard,
"Y" => yank_main_selection_to_clipboard,
"p" => paste_clipboard_after,
"P" => paste_clipboard_before,
"R" => replace_selections_with_clipboard,
"space" => keep_primary_selection,
},
"z" => { "View"
"z" | "c" => align_view_center,
"t" => align_view_top,
"b" => align_view_bottom,
"m" => align_view_middle,
"k" => scroll_up,
"j" => scroll_down,
},
"\"" => select_register,
"C-z" => suspend,
});
let mut select = normal.clone(); let mut select = normal.clone();
select.extend( select.merge_nodes(keymap!({ "Select mode"
hashmap!( "h" | "left" => extend_char_left,
key!('h') => commands::extend_char_left as Command, "j" | "down" => extend_line_down,
key!('j') => commands::extend_line_down, "k" | "up" => extend_line_up,
key!('k') => commands::extend_line_up, "l" | "right" => extend_char_right,
key!('l') => commands::extend_char_right,
KeyEvent { "w" => extend_next_word_start,
code: KeyCode::Left, "b" => extend_prev_word_start,
modifiers: KeyModifiers::NONE "e" => extend_next_word_end,
} => commands::extend_char_left,
KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE
} => commands::extend_line_down,
KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE
} => commands::extend_line_up,
KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::NONE
} => commands::extend_char_right,
key!('w') => commands::extend_next_word_start, "t" => extend_till_char,
key!('b') => commands::extend_prev_word_start, "f" => extend_next_char,
key!('e') => commands::extend_next_word_end, "T" => extend_till_prev_char,
"F" => extend_prev_char,
key!('t') => commands::extend_till_char, "home" => goto_line_start,
key!('f') => commands::extend_next_char, "end" => goto_line_end,
"esc" => exit_select_mode,
}));
let insert = keymap!({ "Insert mode"
"esc" => normal_mode,
key!('T') => commands::extend_till_prev_char, "backspace" => delete_char_backward,
key!('F') => commands::extend_prev_char, "del" => delete_char_forward,
KeyEvent { "ret" => insert_newline,
code: KeyCode::Home, "tab" => insert_tab,
modifiers: KeyModifiers::NONE "C-w" => delete_word_backward,
} => commands::extend_line_start,
KeyEvent { "left" => move_char_left,
code: KeyCode::End, "down" => move_line_down,
modifiers: KeyModifiers::NONE "up" => move_line_up,
} => commands::extend_line_end, "right" => move_char_right,
KeyEvent { "pageup" => page_up,
code: KeyCode::Esc, "pagedown" => page_down,
modifiers: KeyModifiers::NONE "home" => goto_line_start,
} => commands::exit_select_mode, "end" => goto_line_end_newline,
"C-x" => completion,
});
Keymaps(hashmap!(
Mode::Normal => Keymap::new(normal),
Mode::Select => Keymap::new(select),
Mode::Insert => Keymap::new(insert),
))
}
}
/// 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.merge(delta.remove(mode).unwrap_or_default())
}
config
}
#[test]
fn merge_partial_keys() {
let config = Config {
keys: Keymaps(hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
"i" => normal_mode,
"" => insert_mode,
"z" => jump_backward,
"g" => { "Merge into goto mode"
"$" => goto_line_end,
"g" => delete_char_forward,
},
})
) )
.into_iter(), }),
..Default::default()
};
let mut merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config);
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
assert_eq!(
keymap.get(key!('i')),
KeymapResult::Matched(Command::normal_mode),
"Leaf should replace leaf"
);
assert_eq!(
keymap.get(key!('无')),
KeymapResult::Matched(Command::insert_mode),
"New leaf should be present in merged keymap"
);
// Assumes that z is a node in the default keymap
assert_eq!(
keymap.get(key!('z')),
KeymapResult::Matched(Command::jump_backward),
"Leaf should replace node"
);
// Assumes that `g` is a node in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('$')]).unwrap(),
&KeyTrie::Leaf(Command::goto_line_end),
"Leaf should be present in merged subnode"
);
// Assumes that `gg` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('g')]).unwrap(),
&KeyTrie::Leaf(Command::delete_char_forward),
"Leaf should replace old leaf in merged subnode"
);
// Assumes that `ge` is in default keymap
assert_eq!(
keymap.root().search(&[key!('g'), key!('e')]).unwrap(),
&KeyTrie::Leaf(Command::goto_last_line),
"Old leaves in subnode should be present in merged node"
); );
hashmap!( assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
// as long as you cast the first item, rust is able to infer the other cases assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
// 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,
ctrl!('w') => commands::insert::delete_word_backward,
),
)
} }

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

@@ -0,0 +1,11 @@
#[macro_use]
extern crate helix_view;
pub mod application;
pub mod args;
pub mod commands;
pub mod compositor;
pub mod config;
pub mod job;
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 std::path::PathBuf;
use anyhow::{Context, Error, Result}; 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;
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> { fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new(); 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 { base_config = match verbosity {
0 => base_config.level(log::LevelFilter::Warn), 0 => base_config.level(log::LevelFilter::Warn),
1 => base_config.level(log::LevelFilter::Info), 1 => base_config.level(log::LevelFilter::Info),
@@ -45,58 +33,11 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
Ok(()) Ok(())
} }
pub struct Args {
display_help: bool,
display_version: bool,
verbosity: u64,
files: Vec<PathBuf>,
}
fn parse_args(mut args: Args) -> Result<Args> {
let argv: Vec<String> = std::env::args().collect();
let mut iter = argv.iter();
iter.next(); // skip the program, we don't care about that
while let Some(arg) = iter.next() {
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)
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main() -> Result<()> {
let cache_dir = helix_core::cache_dir(); let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() { if !cache_dir.exists() {
std::fs::create_dir(&cache_dir); std::fs::create_dir_all(&cache_dir).ok();
} }
let logpath = cache_dir.join("helix.log"); let logpath = cache_dir.join("helix.log");
@@ -125,14 +66,7 @@ FLAGS:
logpath.display(), logpath.display(),
); );
let mut args: Args = Args { let args = Args::parse_args().context("could not parse arguments")?;
display_help: false,
display_version: false,
verbosity: 0,
files: [].to_vec(),
};
args = parse_args(args).context("could not parse arguments")?;
// Help has a higher priority and should be handled separately. // Help has a higher priority and should be handled separately.
if args.display_help { if args.display_help {
@@ -147,14 +81,20 @@ FLAGS:
let conf_dir = helix_core::config_dir(); let conf_dir = helix_core::config_dir();
if !conf_dir.exists() { if !conf_dir.exists() {
std::fs::create_dir(&conf_dir); std::fs::create_dir_all(&conf_dir).ok();
} }
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)),
};
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?; 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 // TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args).context("unable to create new appliction")?; let mut app = Application::new(args, config).context("unable to create new application")?;
app.run().await; app.run().await.unwrap();
Ok(()) Ok(())
} }

View File

@@ -1,23 +1,23 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{Component, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent};
use tui::{ use tui::buffer::Buffer as Surface;
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use std::borrow::Cow; use std::borrow::Cow;
use helix_core::{Position, Transaction}; use helix_core::Transaction;
use helix_view::Editor; use helix_view::{graphics::Rect, Document, Editor, View};
use crate::commands; use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::lsp; use helix_lsp::{lsp, util};
use lsp::CompletionItem; use lsp::CompletionItem;
impl menu::Item for CompletionItem { impl menu::Item for CompletionItem {
fn sort_text(&self) -> &str {
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
}
fn filter_text(&self) -> &str { fn filter_text(&self) -> &str {
self.filter_text.as_ref().unwrap_or(&self.label).as_str() self.filter_text.as_ref().unwrap_or(&self.label).as_str()
} }
@@ -68,7 +68,7 @@ impl menu::Item for CompletionItem {
/// Wraps a Menu. /// Wraps a Menu.
pub struct Completion { pub struct Completion {
popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents. popup: Popup<Menu<CompletionItem>>,
trigger_offset: usize, trigger_offset: usize,
// TODO: maintain a completioncontext with trigger kind & trigger char // TODO: maintain a completioncontext with trigger kind & trigger char
} }
@@ -80,51 +80,52 @@ impl Completion {
trigger_offset: usize, trigger_offset: usize,
) -> Self { ) -> Self {
// let items: Vec<CompletionItem> = Vec::new(); // let items: Vec<CompletionItem> = Vec::new();
let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| { let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
match event { fn item_to_transaction(
PromptEvent::Abort => { doc: &Document,
// revert state view: &View,
// let id = editor.view().doc; item: &CompletionItem,
// let doc = &mut editor.documents[id]; offset_encoding: helix_lsp::OffsetEncoding,
// doc.state = snapshot.clone(); ) -> Transaction {
} if let Some(edit) = &item.text_edit {
PromptEvent::Validate => { let edit = match edit {
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
// 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::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => { lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", 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.
}; };
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)
.primary()
.cursor(doc.text().slice(..));
Transaction::change(
doc.text(),
vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
)
}
}
match event {
PromptEvent::Abort => {}
PromptEvent::Update => {
let (view, doc) = current!(editor);
// always present here
let item = item.unwrap();
// if more text was entered, remove it // if more text was entered, remove it
let cursor = doc.selection(view.id).cursor(); // TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
let cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if trigger_offset < cursor { if trigger_offset < cursor {
let remove = Transaction::change( let remove = Transaction::change(
doc.text(), doc.text(),
@@ -133,15 +134,32 @@ impl Completion {
doc.apply(&remove, view.id); doc.apply(&remove, view.id);
} }
use helix_lsp::OffsetEncoding; let transaction = item_to_transaction(doc, view, item, offset_encoding);
let transaction = util::generate_transaction_from_edits( doc.apply(&transaction, view.id);
}
PromptEvent::Validate => {
let (view, doc) = current!(editor);
// always present here
let item = item.unwrap();
// if more text was entered, remove it
// TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
let cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(), doc.text(),
vec![edit], vec![(trigger_offset, cursor, None)].into_iter(),
offset_encoding, // TODO: should probably transcode in Client
); );
doc.apply(&remove, view.id);
}
let transaction = item_to_transaction(doc, view, item, offset_encoding);
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
// TODO: merge edit with additional_text_edits
if let Some(additional_edits) = &item.additional_text_edits { if let Some(additional_edits) = &item.additional_text_edits {
// gopls uses this to add extra imports // gopls uses this to add extra imports
if !additional_edits.is_empty() { if !additional_edits.is_empty() {
@@ -154,7 +172,6 @@ impl Completion {
} }
} }
} }
_ => (),
}; };
}); });
let popup = Popup::new(menu); let popup = Popup::new(menu);
@@ -167,7 +184,7 @@ impl Completion {
pub fn update(&mut self, cx: &mut commands::Context) { pub fn update(&mut self, cx: &mut commands::Context) {
// recompute menu based on matches // recompute menu based on matches
let menu = self.popup.contents_mut(); let menu = self.popup.contents_mut();
let (view, doc) = cx.editor.current(); let (view, doc) = current!(cx.editor);
// cx.hooks() // cx.hooks()
// cx.add_hook(enum type, ||) // cx.add_hook(enum type, ||)
@@ -179,7 +196,10 @@ impl Completion {
// TODO: hooks should get processed immediately so maybe do it after select!(), before // TODO: hooks should get processed immediately so maybe do it after select!(), before
// looping? // looping?
let cursor = doc.selection(view.id).cursor(); let cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if self.trigger_offset <= cursor { if self.trigger_offset <= cursor {
let fragment = doc.text().slice(self.trigger_offset..cursor); let fragment = doc.text().slice(self.trigger_offset..cursor);
let text = Cow::from(fragment); let text = Cow::from(fragment);
@@ -221,65 +241,89 @@ impl Component for Completion {
self.popup.required_size(viewport) self.popup.required_size(viewport)
} }
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.popup.render(area, surface, cx); 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() { if let Some(option) = self.popup.contents().selection() {
// need to render: // need to render:
// option.detail // option.detail
// --- // ---
// option.documentation // option.documentation
let (view, doc) = cx.editor.current(); let (view, doc) = current!(cx.editor);
let language = doc let language = doc
.language() .language()
.and_then(|scope| scope.strip_prefix("source.")) .and_then(|scope| scope.strip_prefix("source."))
.unwrap_or(""); .unwrap_or("");
let cursor_pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.first_line) as u16;
let doc = match &option.documentation { let mut doc = match &option.documentation {
Some(lsp::Documentation::String(contents)) Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText, kind: lsp::MarkupKind::PlainText,
value: contents, value: contents,
})) => { })) => {
// TODO: convert to wrapped text // TODO: convert to wrapped text
Markdown::new(format!( Markdown::new(
format!(
"```{}\n{}\n```\n{}", "```{}\n{}\n```\n{}",
language, language,
option.detail.as_deref().unwrap_or_default(), option.detail.as_deref().unwrap_or_default(),
contents.clone() contents.clone()
)) ),
cx.editor.syn_loader.clone(),
)
} }
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown, kind: lsp::MarkupKind::Markdown,
value: contents, value: contents,
})) => { })) => {
// TODO: set language based on doc scope // TODO: set language based on doc scope
Markdown::new(format!( Markdown::new(
format!(
"```{}\n{}\n```\n{}", "```{}\n{}\n```\n{}",
language, language,
option.detail.as_deref().unwrap_or_default(), option.detail.as_deref().unwrap_or_default(),
contents.clone() contents.clone()
)) ),
cx.editor.syn_loader.clone(),
)
} }
None if option.detail.is_some() => { None if option.detail.is_some() => {
// TODO: copied from above // TODO: copied from above
// TODO: set language based on doc scope // TODO: set language based on doc scope
Markdown::new(format!( Markdown::new(
format!(
"```{}\n{}\n```", "```{}\n{}\n```",
language, language,
option.detail.as_deref().unwrap_or_default(), option.detail.as_deref().unwrap_or_default(),
)) ),
cx.editor.syn_loader.clone(),
)
} }
None => return, None => return,
}; };
let half = area.height / 2; let half = area.height / 2;
let height = 15.min(half); let height = 15.min(half);
// 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. // -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); area.height.saturating_sub(height).saturating_sub(2)
};
let area = Rect::new(0, y, area.width, height);
// clear area // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");

File diff suppressed because it is too large Load Diff

38
helix-term/src/ui/info.rs Normal file
View File

@@ -0,0 +1,38 @@
use crate::compositor::{Component, Context};
use helix_view::graphics::Rect;
use helix_view::info::Info;
use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Widget};
impl Component for Info {
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
let style = cx.editor.theme.get("ui.popup");
// Calculate the area of the terminal to modify. Because we want to
// render at the bottom right, we use the viewport's width and height
// which evaluate to the most bottom right coordinate.
let (width, height) = (self.width + 2, self.height + 2);
let area = viewport.intersection(Rect::new(
viewport.width.saturating_sub(width),
viewport.height.saturating_sub(height + 2),
width,
height,
));
surface.clear_with(area, style);
let block = Block::default()
.title(self.title.as_str())
.borders(Borders::ALL)
.border_style(style);
let inner = block.inner(area);
block.render(area, surface);
// Only write as many lines as there are rows available.
for (y, line) in (inner.y..)
.zip(self.text.lines())
.take(inner.height as usize)
{
surface.set_string(inner.x, y, line, style);
}
}
}

View File

@@ -1,36 +1,47 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{Component, Context};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
layout::Rect, text::{Span, Spans, Text},
style::{Color, Style},
text::Text,
}; };
use std::borrow::Cow; use std::sync::Arc;
use helix_core::Position; use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use helix_view::{Editor, Theme};
use helix_core::{
syntax::{self, HighlightEvent, Syntax},
Rope,
};
use helix_view::{
graphics::{Color, Rect, Style},
Theme,
};
pub struct Markdown { pub struct Markdown {
contents: String, contents: String,
config_loader: Arc<syntax::Loader>,
} }
// TODO: pre-render and self reference via Pin // TODO: pre-render and self reference via Pin
// better yet, just use Tendril + subtendril for references // better yet, just use Tendril + subtendril for references
impl Markdown { impl Markdown {
pub fn new(contents: String) -> Self { pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
Self { contents } Self {
contents,
config_loader,
}
} }
} }
fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> { fn parse<'a>(
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag}; contents: &'a str,
use tui::text::{Span, Spans, Text}; theme: Option<&Theme>,
loader: &syntax::Loader,
// also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}} ) -> tui::text::Text<'a> {
let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```"; // // also 2021-03-04T16:33:58.553 helix_lsp::transport [INFO] <- {"contents":{"kind":"markdown","value":"\n```rust\ncore::num\n```\n\n```rust\npub const fn saturating_sub(self, rhs:Self) ->Self\n```\n\n---\n\n```rust\n```"},"range":{"end":{"character":61,"line":101},"start":{"character":47,"line":101}}}
// let text = "\n```rust\ncore::iter::traits::iterator::Iterator\n```\n\n```rust\nfn collect<B: FromIterator<Self::Item>>(self) -> B\nwhere\n Self: Sized,\n```\n\n---\n\nTransforms an iterator into a collection.\n\n`collect()` can take anything iterable, and turn it into a relevant\ncollection. This is one of the more powerful methods in the standard\nlibrary, used in a variety of contexts.\n\nThe most basic pattern in which `collect()` is used is to turn one\ncollection into another. You take a collection, call [`iter`](https://doc.rust-lang.org/nightly/core/iter/traits/iterator/trait.Iterator.html) on it,\ndo a bunch of transformations, and then `collect()` at the end.\n\n`collect()` can also create instances of types that are not typical\ncollections. For example, a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html) can be built from [`char`](type@char)s,\nand an iterator of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html) items can be collected\ninto `Result<Collection<T>, E>`. See the examples below for more.\n\nBecause `collect()` is so general, it can cause problems with type\ninference. As such, `collect()` is one of the few times you'll see\nthe syntax affectionately known as the 'turbofish': `::<>`. This\nhelps the inference algorithm understand specifically which collection\nyou're trying to collect into.\n\n# Examples\n\nBasic usage:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled: Vec<i32> = a.iter()\n .map(|&x| x * 2)\n .collect();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nNote that we needed the `: Vec<i32>` on the left-hand side. This is because\nwe could collect into, for example, a [`VecDeque<T>`](https://doc.rust-lang.org/nightly/core/iter/std/collections/struct.VecDeque.html) instead:\n\n```rust\nuse std::collections::VecDeque;\n\nlet a = [1, 2, 3];\n\nlet doubled: VecDeque<i32> = a.iter().map(|&x| x * 2).collect();\n\nassert_eq!(2, doubled[0]);\nassert_eq!(4, doubled[1]);\nassert_eq!(6, doubled[2]);\n```\n\nUsing the 'turbofish' instead of annotating `doubled`:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<i32>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nBecause `collect()` only cares about what you're collecting into, you can\nstill use a partial type hint, `_`, with the turbofish:\n\n```rust\nlet a = [1, 2, 3];\n\nlet doubled = a.iter().map(|x| x * 2).collect::<Vec<_>>();\n\nassert_eq!(vec![2, 4, 6], doubled);\n```\n\nUsing `collect()` to make a [`String`](https://doc.rust-lang.org/nightly/core/iter/std/string/struct.String.html):\n\n```rust\nlet chars = ['g', 'd', 'k', 'k', 'n'];\n\nlet hello: String = chars.iter()\n .map(|&x| x as u8)\n .map(|x| (x + 1) as char)\n .collect();\n\nassert_eq!(\"hello\", hello);\n```\n\nIf you have a list of [`Result<T, E>`](https://doc.rust-lang.org/nightly/core/result/enum.Result.html)s, you can use `collect()` to\nsee if any of them failed:\n\n```rust\nlet results = [Ok(1), Err(\"nope\"), Ok(3), Err(\"bad\")];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the first error\nassert_eq!(Err(\"nope\"), result);\n\nlet results = [Ok(1), Ok(3)];\n\nlet result: Result<Vec<_>, &str> = results.iter().cloned().collect();\n\n// gives us the list of answers\nassert_eq!(Ok(vec![1, 3]), result);\n```";
let mut options = Options::empty(); let mut options = Options::empty();
options.insert(Options::ENABLE_STRIKETHROUGH); options.insert(Options::ENABLE_STRIKETHROUGH);
@@ -75,18 +86,13 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
// TODO: temp workaround // TODO: temp workaround
if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() { if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
if let Some(theme) = theme { if let Some(theme) = theme {
use helix_core::syntax::{self, HighlightEvent, Syntax};
use helix_core::Rope;
let rope = Rope::from(text.as_ref()); let rope = Rope::from(text.as_ref());
let syntax = syntax::LOADER let syntax = loader
.get()
.unwrap()
.language_config_for_scope(&format!("source.{}", language)) .language_config_for_scope(&format!("source.{}", language))
.and_then(|config| config.highlight_config(theme.scopes())) .and_then(|config| config.highlight_config(theme.scopes()))
.map(|config| Syntax::new(&rope, config)); .map(|config| Syntax::new(&rope, config));
if let Some(mut syntax) = syntax { if let Some(syntax) = syntax {
// if we have a syntax available, highlight_iter and generate spans // if we have a syntax available, highlight_iter and generate spans
let mut highlights = Vec::new(); let mut highlights = Vec::new();
@@ -101,15 +107,15 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
} }
HighlightEvent::Source { start, end } => { HighlightEvent::Source { start, end } => {
let style = match highlights.first() { let style = match highlights.first() {
Some(span) => { Some(span) => theme.get(&theme.scopes()[span.0]),
theme.get(theme.scopes()[span.0].as_str())
}
None => text_style, None => text_style,
}; };
// TODO: replace tabs with indentation // TODO: replace tabs with indentation
let mut slice = &text[start..end]; 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') { while let Some(end) = slice.find('\n') {
// emit span up to newline // emit span up to newline
let text = &slice[..end]; let text = &slice[..end];
@@ -136,13 +142,13 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
} }
} else { } else {
for line in text.lines() { for line in text.lines() {
let mut span = Span::styled(line.to_string(), code_style); let span = Span::styled(line.to_string(), code_style);
lines.push(Spans::from(span)); lines.push(Spans::from(span));
} }
} }
} else { } else {
for line in text.lines() { for line in text.lines() {
let mut span = Span::styled(line.to_string(), code_style); let span = Span::styled(line.to_string(), code_style);
lines.push(Spans::from(span)); lines.push(Spans::from(span));
} }
} }
@@ -157,7 +163,6 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
} }
} }
Event::Code(text) | Event::Html(text) => { Event::Code(text) | Event::Html(text) => {
log::warn!("code {:?}", text);
let mut span = to_span(text); let mut span = to_span(text);
span.style = code_style; span.style = code_style;
spans.push(span); spans.push(span);
@@ -193,10 +198,10 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
Text::from(lines) Text::from(lines)
} }
impl Component for Markdown { impl Component for Markdown {
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
use tui::widgets::{Paragraph, Widget, Wrap}; 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) let par = Paragraph::new(text)
.wrap(Wrap { trim: false }) .wrap(Wrap { trim: false })
@@ -207,7 +212,7 @@ impl Component for Markdown {
} }
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { 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 padding = 2;
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0); let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1); let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);

View File

@@ -1,24 +1,17 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{ use tui::{buffer::Buffer as Surface, widgets::Table};
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
widgets::Table,
};
pub use tui::widgets::{Cell, Row}; pub use tui::widgets::{Cell, Row};
use std::borrow::Cow;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use helix_core::Position; use helix_view::{graphics::Rect, Editor};
use helix_view::Editor; use tui::layout::Constraint;
pub trait Item { pub trait Item {
// TODO: sort_text fn sort_text(&self) -> &str;
fn filter_text(&self) -> &str; fn filter_text(&self) -> &str;
fn label(&self) -> &str; fn label(&self) -> &str;
@@ -34,6 +27,8 @@ pub struct Menu<T: Item> {
/// (index, score) /// (index, score)
matches: Vec<(usize, i64)>, matches: Vec<(usize, i64)>,
widths: Vec<Constraint>,
callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>, callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
scroll: usize, scroll: usize,
@@ -52,6 +47,7 @@ impl<T: Item> Menu<T> {
matcher: Box::new(Matcher::default()), matcher: Box::new(Matcher::default()),
matches: Vec::new(), matches: Vec::new(),
cursor: None, cursor: None,
widths: Vec::new(),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
scroll: 0, scroll: 0,
size: (0, 0), size: (0, 0),
@@ -66,27 +62,23 @@ impl<T: Item> Menu<T> {
pub fn score(&mut self, pattern: &str) { pub fn score(&mut self, pattern: &str) {
// need to borrow via pattern match otherwise it complains about simultaneous borrow // need to borrow via pattern match otherwise it complains about simultaneous borrow
let Self { let Self {
ref mut options,
ref mut matcher, ref mut matcher,
ref mut matches, ref mut matches,
ref options,
.. ..
} = *self; } = *self;
// reuse the matches allocation // reuse the matches allocation
matches.clear(); matches.clear();
matches.extend( matches.extend(options.iter().enumerate().filter_map(|(index, option)| {
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
let text = option.filter_text(); let text = option.filter_text();
// TODO: using fuzzy_indices could give us the char idx for match highlighting // TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher matcher
.fuzzy_match(&text, pattern) .fuzzy_match(text, pattern)
.map(|score| (index, score)) .map(|score| (index, score))
}), }));
); // matches.sort_unstable_by_key(|(_, score)| -score);
matches.sort_unstable_by_key(|(_, score)| -score); matches.sort_unstable_by_key(|(index, _score)| options[*index].sort_text());
// reset cursor position // reset cursor position
self.cursor = None; self.cursor = None;
@@ -94,14 +86,15 @@ impl<T: Item> Menu<T> {
} }
pub fn move_up(&mut self) { pub fn move_up(&mut self) {
// TODO: wrap around to end let len = self.matches.len();
let pos = self.cursor.map_or(0, |i| i.saturating_sub(1)) % self.options.len(); let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len;
self.cursor = Some(pos); self.cursor = Some(pos);
self.adjust_scroll(); self.adjust_scroll();
} }
pub fn move_down(&mut self) { pub fn move_down(&mut self) {
let pos = self.cursor.map_or(0, |i| i + 1) % self.options.len(); let len = self.matches.len();
let pos = self.cursor.map_or(0, |i| i + 1) % len;
self.cursor = Some(pos); self.cursor = Some(pos);
self.adjust_scroll(); self.adjust_scroll();
} }
@@ -166,8 +159,8 @@ impl<T: Item + 'static> Component for Menu<T> {
} }
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
KeyEvent { KeyEvent {
code: KeyCode::Tab, code: KeyCode::BackTab,
modifiers: KeyModifiers::SHIFT, ..
} }
| KeyEvent { | KeyEvent {
code: KeyCode::Up, .. code: KeyCode::Up, ..
@@ -228,11 +221,32 @@ impl<T: Item + 'static> Component for Menu<T> {
} }
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let width = std::cmp::min(30, viewport.0); let n = self
.options
.first()
.map(|option| option.row().cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.row();
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
if width > *acc {
*acc = width;
}
}
const MAX: usize = 10; acc
let height = std::cmp::min(self.options.len(), MAX); });
let height = std::cmp::min(height, viewport.1 as usize); let len = max_lens.iter().sum::<usize>() + n + 1; // +1: reserve some space for scrollbar
let width = len.min(viewport.0 as usize);
self.widths = max_lens
.into_iter()
.map(|len| Constraint::Length(len as u16))
.collect();
let height = self.options.len().min(10).min(viewport.1 as usize);
self.size = (width as u16, height as u16); self.size = (width as u16, height as u16);
@@ -244,7 +258,7 @@ impl<T: Item + 'static> Component for Menu<T> {
// TODO: required size should re-trigger when we filter items so we can draw a smaller menu // TODO: required size should re-trigger when we filter items so we can draw a smaller menu
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let style = cx.editor.theme.get("ui.text"); let style = cx.editor.theme.get("ui.text");
let selected = cx.editor.theme.get("ui.menu.selected"); let selected = cx.editor.theme.get("ui.menu.selected");
@@ -272,13 +286,12 @@ impl<T: Item + 'static> Component for Menu<T> {
let scroll_line = (win_height - scroll_height) * scroll let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height)); / std::cmp::max(1, len.saturating_sub(win_height));
use tui::layout::Constraint;
let rows = options.iter().map(|option| option.row()); let rows = options.iter().map(|option| option.row());
let table = Table::new(rows) let table = Table::new(rows)
.style(style) .style(style)
.highlight_style(selected) .highlight_style(selected)
.column_spacing(1) .column_spacing(1)
.widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]); .widths(&self.widths);
use tui::widgets::TableState; use tui::widgets::TableState;
@@ -299,7 +312,7 @@ impl<T: Item + 'static> Component for Menu<T> {
// ) // )
// } // }
for (i, option) in (scroll..(scroll + win_height).min(len)).enumerate() { for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
let is_marked = i >= scroll_line && i < scroll_line + scroll_height; let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
if is_marked { if is_marked {

View File

@@ -1,45 +1,47 @@
mod completion; mod completion;
mod editor; pub(crate) mod editor;
mod info;
mod markdown; mod markdown;
mod menu; mod menu;
mod picker; mod picker;
mod popup; mod popup;
mod prompt; mod prompt;
mod spinner;
mod text; mod text;
pub use completion::Completion; pub use completion::Completion;
pub use editor::EditorView; pub use editor::EditorView;
pub use markdown::Markdown; pub use markdown::Markdown;
pub use menu::Menu; pub use menu::Menu;
pub use picker::Picker; pub use picker::{FilePicker, Picker};
pub use popup::Popup; pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent}; pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text; pub use text::Text;
pub use tui::layout::Rect;
pub use tui::style::{Color, Modifier, Style};
use helix_core::regex::Regex; use helix_core::regex::Regex;
use helix_core::register::Registers;
use helix_view::{Document, Editor, View}; use helix_view::{Document, Editor, View};
use std::path::{Path, PathBuf}; use std::path::PathBuf;
pub fn regex_prompt( pub fn regex_prompt(
cx: &mut crate::commands::Context, cx: &mut crate::commands::Context,
prompt: String, prompt: String,
fun: impl Fn(&mut View, &mut Document, Regex) + 'static, fun: impl Fn(&mut View, &mut Document, &mut Registers, Regex) + 'static,
) -> Prompt { ) -> Prompt {
let view_id = cx.view().id; let (view, doc) = current!(cx.editor);
let snapshot = cx.doc().selection(view_id).clone(); let view_id = view.id;
let snapshot = doc.selection(view_id).clone();
Prompt::new( Prompt::new(
prompt, prompt,
|input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate None,
move |editor: &mut Editor, input: &str, event: PromptEvent| { |_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event { match event {
PromptEvent::Abort => { PromptEvent::Abort => {
// TODO: also revert text let (view, doc) = current!(cx.editor);
let (view, doc) = editor.current();
doc.set_selection(view.id, snapshot.clone()); doc.set_selection(view.id, snapshot.clone());
} }
PromptEvent::Validate => { PromptEvent::Validate => {
@@ -53,15 +55,15 @@ pub fn regex_prompt(
match Regex::new(input) { match Regex::new(input) {
Ok(regex) => { 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 // revert state to what it was before the last update
// TODO: also revert text
doc.set_selection(view.id, snapshot.clone()); doc.set_selection(view.id, snapshot.clone());
fun(view, doc, regex); fun(view, doc, registers, regex);
view.ensure_cursor_in_view(doc); view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
} }
Err(_err) => (), // TODO: mark command line as error Err(_err) => (), // TODO: mark command line as error
} }
@@ -71,24 +73,41 @@ pub fn regex_prompt(
) )
} }
pub fn file_picker(root: PathBuf) -> Picker<PathBuf> { pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
use ignore::Walk; use ignore::Walk;
let files = Walk::new(root.clone()).filter_map(|entry| match entry { use std::time;
Ok(entry) => { let files = Walk::new(root.clone()).filter_map(|entry| {
// filter dirs, but we might need special handling for symlinks! let entry = entry.ok()?;
if !entry.file_type().map_or(false, |entry| entry.is_dir()) { // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
Some(entry.into_path()) if entry.path().is_dir() {
} else { // Will give a false positive if metadata cannot be read (eg. permission error)
None return None;
} }
}
Err(_err) => None, let time = entry.metadata().map_or(time::UNIX_EPOCH, |metadata| {
metadata
.accessed()
.or_else(|_| metadata.modified())
.or_else(|_| metadata.created())
.unwrap_or(time::UNIX_EPOCH)
}); });
const MAX: usize = 2048; Some((entry.into_path(), time))
});
Picker::new( let mut files: Vec<_> = if root.join(".git").is_dir() {
files.take(MAX).collect(), files.collect()
} else {
const MAX: usize = 8192;
files.take(MAX).collect()
};
files.sort_by_key(|file| std::cmp::Reverse(file.1));
let files = files.into_iter().map(|(path, _)| path).collect();
FilePicker::new(
files,
move |path: &PathBuf| { move |path: &PathBuf| {
// format_fn // format_fn
path.strip_prefix(&root) path.strip_prefix(&root)
@@ -98,30 +117,101 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
.into() .into()
}, },
move |editor: &mut Editor, path: &PathBuf, action| { move |editor: &mut Editor, path: &PathBuf, action| {
let document_id = editor editor
.open(path.into(), action) .open(path.into(), action)
.expect("editor.open failed"); .expect("editor.open failed");
}, },
|_editor, path| Some((path.clone(), None)),
) )
} }
pub mod completers { pub mod completers {
use crate::ui::prompt::Completion; use crate::ui::prompt::Completion;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_view::theme;
use std::borrow::Cow; use std::borrow::Cow;
use std::cmp::Reverse;
pub type Completer = fn(&str) -> Vec<Completion>; 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> { 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. // Rust's filename handling is really annoying.
use ignore::WalkBuilder; use ignore::WalkBuilder;
use std::path::{Path, PathBuf}; use std::path::Path;
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('/') { let (dir, file_name) = if input.ends_with('/') {
(path.into(), None) (path, None)
} else { } else {
let file_name = path let file_name = path
.file_name() .file_name()
@@ -136,23 +226,39 @@ pub mod completers {
(path, file_name) (path, file_name)
}; };
let end = (input.len()..); let end = input.len()..;
let mut files: Vec<_> = WalkBuilder::new(dir.clone()) let mut files: Vec<_> = WalkBuilder::new(dir.clone())
.max_depth(Some(1)) .max_depth(Some(1))
.build() .build()
.filter_map(|file| { .filter_map(|file| {
file.ok().map(|entry| { file.ok().and_then(|entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir()); 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 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(""); path.push("");
} }
let path = path.to_str().unwrap().to_owned(); let path = path.to_str().unwrap().to_owned();
(end.clone(), Cow::from(path)) Some((end.clone(), Cow::from(path)))
}) })
}) // TODO: unwrap or skip }) // TODO: unwrap or skip
.filter(|(_, path)| !path.is_empty()) // TODO .filter(|(_, path)| !path.is_empty()) // TODO
@@ -160,23 +266,19 @@ pub mod completers {
// if empty, return a list of dirs and files in current dir // if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name { 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(); let matcher = Matcher::default();
// inefficient, but we need to calculate the scores, filter out None, then sort. // inefficient, but we need to calculate the scores, filter out None, then sort.
let mut matches: Vec<_> = files let mut matches: Vec<_> = files
.into_iter() .into_iter()
.filter_map(|(range, file)| { .filter_map(|(_range, file)| {
matcher matcher
.fuzzy_match(&file, &file_name) .fuzzy_match(&file, &file_name)
.map(|score| (file, score)) .map(|score| (file, score))
}) })
.collect(); .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)); matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
files = matches files = matches

View File

@@ -1,21 +1,161 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::{
compositor::{Component, Compositor, Context, EventResult},
ui::EditorView,
};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
widgets::{Block, BorderType, Borders}, widgets::{Block, BorderType, Borders},
}; };
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget;
use std::borrow::Cow; use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use crate::ui::{Prompt, PromptEvent}; use crate::ui::{Prompt, PromptEvent};
use helix_core::Position; use helix_core::Position;
use helix_view::editor::Action; use helix_view::{
use helix_view::Editor; document::canonicalize_path,
editor::Action,
graphics::{Color, CursorKind, Rect, Style},
Document, Editor,
};
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
/// File path and line number (used to align and highlight a line)
type FileLocation = (PathBuf, Option<usize>);
pub struct FilePicker<T> {
picker: Picker<T>,
/// Caches paths to documents
preview_cache: HashMap<PathBuf, Document>,
/// Given an item in the picker, return the file path and line number to display.
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
}
impl<T> FilePicker<T> {
pub fn new(
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + 'static,
) -> Self {
Self {
picker: Picker::new(false, options, format_fn, callback_fn),
preview_cache: HashMap::new(),
file_fn: Box::new(preview_fn),
}
}
fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
self.picker
.selection()
.and_then(|current| (self.file_fn)(editor, current))
.and_then(|(path, line)| canonicalize_path(&path).ok().zip(Some(line)))
}
fn calculate_preview(&mut self, editor: &Editor) {
if let Some((path, _line)) = self.current_file(editor) {
if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
// TODO: enable syntax highlighting; blocked by async rendering
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
self.preview_cache.insert(path, doc);
}
}
}
}
impl<T: 'static> Component for FilePicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// +---------+ +---------+
// |prompt | |preview |
// +---------+ | |
// |picker | | |
// | | | |
// +---------+ +---------+
self.calculate_preview(cx.editor);
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
let area = inner_rect(area);
// -- Render the frame:
// clear area
let background = cx.editor.theme.get("ui.background");
surface.clear_with(area, background);
let picker_width = if render_preview {
area.width / 2
} else {
area.width
};
let picker_area = Rect::new(area.x, area.y, picker_width, area.height);
self.picker.render(picker_area, surface, cx);
if !render_preview {
return;
}
let preview_area = Rect::new(area.x + picker_width, area.y, area.width / 2, area.height);
// don't like this but the lifetime sucks
let block = Block::default().borders(Borders::ALL);
// calculate the inner area inside the box
let mut inner = block.inner(preview_area);
// 1 column gap on either side
inner.x += 1;
inner.width = inner.width.saturating_sub(2);
block.render(preview_area, surface);
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, line)| {
cx.editor
.document_by_path(&path)
.or_else(|| self.preview_cache.get(&path))
.zip(Some(line))
}) {
// align to middle
let first_line = line.unwrap_or(0).saturating_sub(inner.height as usize / 2);
let offset = Position::new(first_line, 0);
let highlights = EditorView::doc_syntax_highlights(
doc,
offset,
area.height,
&cx.editor.theme,
&cx.editor.syn_loader,
);
EditorView::render_text_highlights(
doc,
offset,
inner,
surface,
&cx.editor.theme,
highlights,
);
// highlight the line
if let Some(line) = line {
for x in inner.left()..inner.right() {
surface
.get_mut(x, inner.y + line.saturating_sub(first_line) as u16)
.set_style(cx.editor.theme.get("ui.selection"));
}
}
}
}
fn handle_event(&mut self, event: Event, ctx: &mut Context) -> EventResult {
// TODO: keybinds for scrolling preview
self.picker.handle_event(event, ctx)
}
fn cursor(&self, area: Rect, ctx: &Editor) -> (Option<Position>, CursorKind) {
self.picker.cursor(area, ctx)
}
}
pub struct Picker<T> { pub struct Picker<T> {
options: Vec<T>, options: Vec<T>,
@@ -23,10 +163,14 @@ pub struct Picker<T> {
matcher: Box<Matcher>, matcher: Box<Matcher>,
/// (index, score) /// (index, score)
matches: Vec<(usize, i64)>, matches: Vec<(usize, i64)>,
/// Filter over original options.
filters: Vec<usize>, // could be optimized into bit but not worth it now
cursor: usize, cursor: usize,
// pattern: String, // pattern: String,
prompt: Prompt, prompt: Prompt,
/// Whether to render in the middle of the area
render_centered: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>, format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>, callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>,
@@ -34,14 +178,16 @@ pub struct Picker<T> {
impl<T> Picker<T> { impl<T> Picker<T> {
pub fn new( pub fn new(
render_centered: bool,
options: Vec<T>, options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static, format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static, callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
) -> Self { ) -> Self {
let prompt = Prompt::new( let prompt = Prompt::new(
"".to_string(), "".to_string(),
|pattern: &str| Vec::new(), None,
|editor: &mut Editor, pattern: &str, event: PromptEvent| { |_pattern: &str| Vec::new(),
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {
// //
}, },
); );
@@ -50,8 +196,10 @@ impl<T> Picker<T> {
options, options,
matcher: Box::new(Matcher::default()), matcher: Box::new(Matcher::default()),
matches: Vec::new(), matches: Vec::new(),
filters: Vec::new(),
cursor: 0, cursor: 0,
prompt, prompt,
render_centered,
format_fn: Box::new(format_fn), format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
}; };
@@ -65,9 +213,9 @@ impl<T> Picker<T> {
pub fn score(&mut self) { pub fn score(&mut self) {
// need to borrow via pattern match otherwise it complains about simultaneous borrow // need to borrow via pattern match otherwise it complains about simultaneous borrow
let Self { let Self {
ref mut options,
ref mut matcher, ref mut matcher,
ref mut matches, ref mut matches,
ref filters,
ref format_fn, ref format_fn,
.. ..
} = *self; } = *self;
@@ -81,6 +229,10 @@ impl<T> Picker<T> {
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(index, option)| { .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 // TODO: maybe using format_fn isn't the best idea here
let text = (format_fn)(option); let text = (format_fn)(option);
// TODO: using fuzzy_indices could give us the char idx for match highlighting // TODO: using fuzzy_indices could give us the char idx for match highlighting
@@ -114,6 +266,14 @@ impl<T> Picker<T> {
.get(self.cursor) .get(self.cursor)
.map(|(index, _score)| &self.options[*index]) .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: // process:
@@ -122,8 +282,8 @@ impl<T> Picker<T> {
// - score all the names in relation to input // - score all the names in relation to input
fn inner_rect(area: Rect) -> Rect { fn inner_rect(area: Rect) -> Rect {
let padding_vertical = area.height * 20 / 100; let padding_vertical = area.height * 10 / 100;
let padding_horizontal = area.width * 20 / 100; let padding_horizontal = area.width * 10 / 100;
Rect::new( Rect::new(
area.x + padding_horizontal, area.x + padding_horizontal,
@@ -143,7 +303,7 @@ impl<T: 'static> Component for Picker<T> {
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
// remove the layer // remove the layer
compositor.pop(); compositor.last_picker = compositor.pop();
}))); })));
match key_event { match key_event {
@@ -151,19 +311,34 @@ impl<T: 'static> Component for Picker<T> {
code: KeyCode::Up, .. code: KeyCode::Up, ..
} }
| KeyEvent { | KeyEvent {
code: KeyCode::Char('k'), code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_up(), } => {
self.move_up();
}
KeyEvent { KeyEvent {
code: KeyCode::Down, code: KeyCode::Down,
.. ..
} }
| KeyEvent { | KeyEvent {
code: KeyCode::Char('j'), code: KeyCode::Tab, ..
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_down(), } => {
self.move_down();
}
KeyEvent { KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => { } => {
return close_fn; return close_fn;
} }
@@ -177,7 +352,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn; return close_fn;
} }
KeyEvent { KeyEvent {
code: KeyCode::Char('x'), code: KeyCode::Char('h'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => { } => {
if let Some(option) = self.selection() { if let Some(option) = self.selection() {
@@ -194,6 +369,12 @@ impl<T: 'static> Component for Picker<T> {
} }
return close_fn; return close_fn;
} }
KeyEvent {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::CONTROL,
} => {
self.save_filter();
}
_ => { _ => {
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
// TODO: recalculate only if pattern changed // TODO: recalculate only if pattern changed
@@ -205,16 +386,20 @@ impl<T: 'static> Component for Picker<T> {
EventResult::Consumed(None) EventResult::Consumed(None)
} }
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let area = inner_rect(area); let area = if self.render_centered {
inner_rect(area)
} else {
area
};
let text_style = cx.editor.theme.get("ui.text");
// -- Render the frame: // -- Render the frame:
// clear area // clear area
let background = cx.editor.theme.get("ui.background"); let background = cx.editor.theme.get("ui.background");
surface.clear_with(area, background); surface.clear_with(area, background);
use tui::widgets::Widget;
// don't like this but the lifetime sucks // don't like this but the lifetime sucks
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
@@ -222,30 +407,39 @@ impl<T: 'static> Component for Picker<T> {
let inner = block.inner(area); let inner = block.inner(area);
block.render(area, surface); 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: // -- Render the input bar:
let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1); let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1);
let count = format!("{}/{}", self.matches.len(), self.options.len());
surface.set_stringn(
(area.x + area.width).saturating_sub(count.len() as u16 + 1),
area.y,
&count,
(count.len()).min(area.width as usize),
text_style,
);
self.prompt.render(area, surface, cx); self.prompt.render(area, surface, cx);
// -- Separator // -- Separator
let style = Style::default().fg(Color::Rgb(90, 89, 119)); let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
let symbols = BorderType::line_symbols(BorderType::Plain); let borders = BorderType::line_symbols(BorderType::Plain);
for x in inner.left()..inner.right() { for x in inner.left()..inner.right() {
surface surface
.get_mut(x, inner.y + 1) .get_mut(x, inner.y + 1)
.set_symbol(symbols.horizontal) .set_symbol(borders.horizontal)
.set_style(style); .set_style(sep_style);
} }
// -- Render the contents: // -- Render the contents:
// subtract the area of the prompt (-2) and current item marker " > " (-3)
let inner = Rect::new(inner.x + 3, inner.y + 2, inner.width - 3, inner.height - 2);
let style = cx.editor.theme.get("ui.text");
let selected = Style::default().fg(Color::Rgb(255, 255, 255)); let selected = Style::default().fg(Color::Rgb(255, 255, 255));
let rows = inner.height - 2; // -1 for search bar let rows = inner.height;
let offset = self.cursor / (rows as usize) * (rows as usize); let offset = self.cursor / (rows as usize) * (rows as usize);
let files = self.matches.iter().skip(offset).map(|(index, _score)| { let files = self.matches.iter().skip(offset).map(|(index, _score)| {
@@ -254,24 +448,25 @@ impl<T: 'static> Component for Picker<T> {
for (i, (_index, option)) in files.take(rows as usize).enumerate() { for (i, (_index, option)) in files.take(rows as usize).enumerate() {
if i == (self.cursor - offset) { if i == (self.cursor - offset) {
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected); surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected);
} }
surface.set_stringn( surface.set_string_truncated(
inner.x + 3, inner.x,
inner.y + 2 + i as u16, inner.y + i as u16,
(self.format_fn)(option), (self.format_fn)(option),
inner.width as usize - 1, inner.width as usize,
if i == (self.cursor - offset) { if i == (self.cursor - offset) {
selected selected
} else { } else {
style text_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 // TODO: this is mostly duplicate code
let area = inner_rect(area); let area = inner_rect(area);
let block = Block::default().borders(Borders::ALL); let block = Block::default().borders(Borders::ALL);
@@ -281,6 +476,6 @@ impl<T: 'static> Component for Picker<T> {
// prompt area // prompt area
let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1); 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,9 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{ use tui::buffer::Buffer as Surface;
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use std::borrow::Cow;
use helix_core::Position; use helix_core::Position;
use helix_view::Editor; use helix_view::graphics::Rect;
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return // 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>) // a width/height hint. maybe Popup(Box<Component>)
@@ -58,7 +52,7 @@ impl<T: Component> Component for Popup<T> {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult { fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let key = match event { let key = match event {
Event::Key(event) => event, Event::Key(event) => event,
Event::Resize(width, height) => { Event::Resize(_, _) => {
// TODO: calculate inner area, call component's handle_event with that area // TODO: calculate inner area, call component's handle_event with that area
return EventResult::Ignored; return EventResult::Ignored;
} }
@@ -100,7 +94,7 @@ impl<T: Component> Component for Popup<T> {
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
} }
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
let (width, height) = self let (width, height) = self
.contents .contents
.required_size((120, 26)) // max width, max height .required_size((120, 26)) // max width, max height
@@ -111,12 +105,12 @@ impl<T: Component> Component for Popup<T> {
Some(self.size) Some(self.size)
} }
fn render(&self, viewport: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
cx.scroll = Some(self.scroll); cx.scroll = Some(self.scroll);
let position = self let position = self
.position .position
.or_else(|| cx.editor.cursor_position()) .or_else(|| cx.editor.cursor().0)
.unwrap_or_default(); .unwrap_or_default();
let (width, height) = self.size; let (width, height) = self.size;
@@ -125,18 +119,17 @@ impl<T: Component> Component for Popup<T> {
let mut rel_x = position.col as u16; let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16; let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width { 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 // TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete. // for menus/autocomplete.
if height <= rel_y { if height <= rel_y {
rel_y -= height // position above point rel_y = rel_y.saturating_sub(height) // position above point
} else { } else {
rel_y += 1 // position below point rel_y += 1 // position below point
} }
let area = Rect::new(rel_x, rel_y, width, height);
// clip to viewport // clip to viewport
let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height)); let area = viewport.intersection(Rect::new(rel_x, rel_y, width, height));

View File

@@ -1,9 +1,16 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{Component, Compositor, Context, EventResult};
use crate::ui; use crate::ui;
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use helix_core::Position;
use helix_view::{Editor, Theme};
use std::{borrow::Cow, ops::RangeFrom}; use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface;
use helix_core::{
unicode::segmentation::GraphemeCursor, unicode::width::UnicodeWidthStr, Position,
};
use helix_view::{
graphics::{CursorKind, Margin, Rect},
Editor,
};
pub type Completion = (RangeFrom<usize>, Cow<'static, str>); pub type Completion = (RangeFrom<usize>, Cow<'static, str>);
@@ -13,12 +20,14 @@ pub struct Prompt {
cursor: usize, cursor: usize,
completion: Vec<Completion>, completion: Vec<Completion>,
selection: Option<usize>, selection: Option<usize>,
history_register: Option<char>,
history_pos: Option<usize>,
completion_fn: Box<dyn FnMut(&str) -> Vec<Completion>>, 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>>, pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
} }
#[derive(PartialEq)] #[derive(Clone, Copy, PartialEq)]
pub enum PromptEvent { pub enum PromptEvent {
/// The prompt input has been updated. /// The prompt input has been updated.
Update, Update,
@@ -33,11 +42,23 @@ pub enum CompletionDirection {
Backward, Backward,
} }
#[derive(Debug, Clone, Copy)]
pub enum Movement {
BackwardChar(usize),
BackwardWord(usize),
ForwardChar(usize),
ForwardWord(usize),
StartOfLine,
EndOfLine,
None,
}
impl Prompt { impl Prompt {
pub fn new( pub fn new(
prompt: String, prompt: String,
history_register: Option<char>,
mut completion_fn: impl FnMut(&str) -> Vec<Completion> + 'static, 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 {
Self { Self {
prompt, prompt,
@@ -45,27 +66,128 @@ impl Prompt {
cursor: 0, cursor: 0,
completion: completion_fn(""), completion: completion_fn(""),
selection: None, selection: None,
history_register,
history_pos: None,
completion_fn: Box::new(completion_fn), completion_fn: Box::new(completion_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
doc_fn: Box::new(|_| None), doc_fn: Box::new(|_| None),
} }
} }
/// 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) { pub fn insert_char(&mut self, c: char) {
self.line.insert(self.cursor, c); 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.completion = (self.completion_fn)(&self.line);
self.exit_selection(); self.exit_selection();
} }
pub fn move_char_left(&mut self) { pub fn move_cursor(&mut self, movement: Movement) {
self.cursor = self.cursor.saturating_sub(1) let pos = self.eval_movement(movement);
} self.cursor = pos
pub fn move_char_right(&mut self) {
if self.cursor < self.line.len() {
self.cursor += 1;
}
} }
pub fn move_start(&mut self) { pub fn move_start(&mut self) {
@@ -77,12 +199,58 @@ impl Prompt {
} }
pub fn delete_char_backwards(&mut self) { pub fn delete_char_backwards(&mut self) {
if self.cursor > 0 { let pos = self.eval_movement(Movement::BackwardChar(1));
self.line.remove(self.cursor - 1); self.line.replace_range(pos..self.cursor, "");
self.cursor -= 1; self.cursor = pos;
self.exit_selection();
self.completion = (self.completion_fn)(&self.line); 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.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_history(&mut self, register: &[String], direction: CompletionDirection) {
if register.is_empty() {
return;
}
let end = register.len().saturating_sub(1);
let index = match direction {
CompletionDirection::Forward => self.history_pos.map_or(0, |i| i + 1),
CompletionDirection::Backward => {
self.history_pos.unwrap_or(register.len()).saturating_sub(1)
}
}
.min(end);
self.line = register[index].clone();
self.history_pos = Some(index);
self.move_end();
} }
pub fn change_completion_selection(&mut self, direction: CompletionDirection) { pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
@@ -111,12 +279,6 @@ impl Prompt {
} }
} }
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Modifier, Style},
};
const BASE_WIDTH: u16 = 30; const BASE_WIDTH: u16 = 30;
impl Prompt { impl Prompt {
@@ -126,8 +288,21 @@ impl Prompt {
let selected_color = theme.get("ui.menu.selected"); let selected_color = theme.get("ui.menu.selected");
// completion // completion
let max_col = std::cmp::max(1, area.width / BASE_WIDTH); let max_len = self
let height = ((self.completion.len() as u16 + max_col - 1) / max_col); .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( let completion_area = Rect::new(
area.x, area.x,
(area.height - height).saturating_sub(1), (area.height - height).saturating_sub(1),
@@ -139,12 +314,22 @@ impl Prompt {
let area = completion_area; let area = completion_area;
let background = theme.get("ui.statusline"); 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); surface.clear_with(area, background);
let mut row = 0; let mut row = 0;
let mut col = 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 { let color = if Some(i) == self.selection {
// Style::default().bg(Color::Rgb(104, 60, 232)) // Style::default().bg(Color::Rgb(104, 60, 232))
selected_color // TODO: just invert bg selected_color // TODO: just invert bg
@@ -152,10 +337,10 @@ impl Prompt {
text_color text_color
}; };
surface.set_stringn( surface.set_stringn(
area.x + 1 + col * BASE_WIDTH, area.x + col * (1 + col_width),
area.y + row, area.y + row,
&completion, &completion,
BASE_WIDTH as usize - 1, col_width.saturating_sub(1) as usize,
color, color,
); );
row += 1; row += 1;
@@ -163,19 +348,16 @@ impl Prompt {
row = 0; row = 0;
col += 1; col += 1;
} }
if col > max_col {
break;
}
} }
} }
if let Some(doc) = (self.doc_fn)(&self.line) { if let Some(doc) = (self.doc_fn)(&self.line) {
let text = ui::Text::new(doc.to_string()); let mut text = ui::Text::new(doc.to_string());
let viewport = area; let viewport = area;
let area = viewport.intersection(Rect::new( let area = viewport.intersection(Rect::new(
completion_area.x, completion_area.x,
completion_area.y - 3, completion_area.y.saturating_sub(3),
BASE_WIDTH * 3, BASE_WIDTH * 3,
3, 3,
)); ));
@@ -183,7 +365,6 @@ impl Prompt {
let background = theme.get("ui.help"); let background = theme.get("ui.help");
surface.clear_with(area, background); surface.clear_with(area, background);
use tui::layout::Margin;
text.render( text.render(
area.inner(&Margin { area.inner(&Margin {
vertical: 1, vertical: 1,
@@ -230,49 +411,124 @@ impl Component for Prompt {
modifiers: KeyModifiers::SHIFT, modifiers: KeyModifiers::SHIFT,
} => { } => {
self.insert_char(c); self.insert_char(c);
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
KeyEvent { KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Esc, .. code: KeyCode::Esc, ..
} => { } => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Abort); (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
return close_fn; return close_fn;
} }
KeyEvent { 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('f'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Right, code: KeyCode::Right,
.. ..
} => self.move_char_right(), } => self.move_cursor(Movement::ForwardChar(1)),
KeyEvent { KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Left, code: KeyCode::Left,
.. ..
} => self.move_char_left(), } => self.move_cursor(Movement::BackwardChar(1)),
KeyEvent { KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('e'), code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_end(), } => self.move_end(),
KeyEvent { KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('a'), code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_start(), } => self.move_start(),
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 { KeyEvent {
code: KeyCode::Backspace, code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::NONE,
} => { } => {
self.delete_char_backwards(); self.delete_char_backwards();
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
KeyEvent { KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
} => { } => {
if self.line.ends_with('/') { if self.selection.is_some() && self.line.ends_with('/') {
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(&self.line);
self.exit_selection(); self.exit_selection();
} else { } else {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate); (self.callback_fn)(cx, &self.line, PromptEvent::Validate);
if let Some(register) = self.history_register {
// store in history
let register = cx.editor.registers.get_mut(register);
register.push(self.line.clone());
}
return close_fn; return close_fn;
} }
} }
KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Up, ..
} => {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Backward);
}
}
KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Down,
..
} => {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Forward);
}
}
KeyEvent { KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
} => self.change_completion_selection(CompletionDirection::Forward), } => self.change_completion_selection(CompletionDirection::Forward),
@@ -290,15 +546,20 @@ impl Component for Prompt {
EventResult::Consumed(None) EventResult::Consumed(None)
} }
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.render_prompt(area, surface, cx) 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; let line = area.height as usize - 1;
(
Some(Position::new( Some(Position::new(
area.y as usize + line, area.y as usize + line,
area.x as usize + self.prompt.len() + self.cursor, 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,7 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{Component, Context};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use tui::buffer::Buffer as Surface;
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use std::borrow::Cow; use helix_view::graphics::Rect;
use helix_core::Position;
use helix_view::Editor;
pub struct Text { pub struct Text {
contents: String, contents: String,
@@ -21,12 +13,10 @@ impl Text {
} }
} }
impl Component for Text { impl Component for Text {
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { fn render(&mut self, area: Rect, surface: &mut Surface, _cx: &mut Context) {
use tui::widgets::{Paragraph, Widget, Wrap}; use tui::widgets::{Paragraph, Widget, Wrap};
let contents = tui::text::Text::from(self.contents.clone()); let contents = tui::text::Text::from(self.contents.clone());
let style = cx.editor.theme.get("ui.text");
let par = Paragraph::new(contents).wrap(Wrap { trim: false }); let par = Paragraph::new(contents).wrap(Wrap { trim: false });
// .scroll(x, y) offsets // .scroll(x, y) offsets

View File

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

View File

@@ -1,11 +1,6 @@
use crate::{ use crate::{backend::Backend, buffer::Cell};
backend::Backend,
buffer::Cell,
layout::Rect,
style::{Color, Modifier},
};
use crossterm::{ use crossterm::{
cursor::{Hide, MoveTo, Show}, cursor::{CursorShape, Hide, MoveTo, SetCursorShape, Show},
execute, queue, execute, queue,
style::{ style::{
Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor,
@@ -13,6 +8,7 @@ use crossterm::{
}, },
terminal::{self, Clear, ClearType}, terminal::{self, Clear, ClearType},
}; };
use helix_view::graphics::{Color, CursorKind, Modifier, Rect};
use std::io::{self, Write}; use std::io::{self, Write};
pub struct CrosstermBackend<W: Write> { pub struct CrosstermBackend<W: Write> {
@@ -93,8 +89,14 @@ where
map_error(execute!(self.buffer, Hide)) map_error(execute!(self.buffer, Hide))
} }
fn show_cursor(&mut self) -> io::Result<()> { fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
map_error(execute!(self.buffer, Show)) 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)> { 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())) 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)] #[derive(Debug)]
struct ModifierDiff { struct ModifierDiff {
pub from: Modifier, pub from: Modifier,

View File

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

View File

@@ -1,10 +1,10 @@
use crate::{ use crate::{
backend::Backend, backend::Backend,
buffer::{Buffer, Cell}, buffer::{Buffer, Cell},
layout::Rect,
}; };
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::{CursorKind, Rect};
use std::{fmt::Write, io}; use std::{fmt::Write, io};
use unicode_width::UnicodeWidthStr;
/// A backend used for the integration tests. /// A backend used for the integration tests.
#[derive(Debug)] #[derive(Debug)]
@@ -122,7 +122,7 @@ impl Backend for TestBackend {
Ok(()) Ok(())
} }
fn show_cursor(&mut self) -> Result<(), io::Error> { fn show_cursor(&mut self, _kind: CursorKind) -> Result<(), io::Error> {
self.cursor = true; self.cursor = true;
Ok(()) Ok(())
} }

View File

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

View File

@@ -1,11 +1,12 @@
use std::cell::RefCell; use std::cell::RefCell;
use std::cmp::{max, min};
use std::collections::HashMap; use std::collections::HashMap;
use cassowary::strength::{REQUIRED, WEAK}; use cassowary::strength::{REQUIRED, WEAK};
use cassowary::WeightedRelation::*; use cassowary::WeightedRelation::*;
use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable}; use cassowary::{Constraint as CassowaryConstraint, Expression, Solver, Variable};
use helix_view::graphics::{Margin, Rect};
#[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Hash, Clone, Copy, PartialEq, Eq)]
pub enum Corner { pub enum Corner {
TopLeft, 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)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum Alignment { pub enum Alignment {
Left, Left,
@@ -119,7 +114,8 @@ impl Layout {
/// ///
/// # Examples /// # 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() /// let chunks = Layout::default()
/// .direction(Direction::Vertical) /// .direction(Direction::Vertical)
/// .constraints([Constraint::Length(5), Constraint::Min(0)].as_ref()) /// .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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -487,48 +372,4 @@ mod tests {
assert_eq!(target.height, chunks.iter().map(|r| r.height).sum::<u16>()); 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)); 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. //! implement your own.
//! //!
//! Each widget follows a builder pattern API providing a default configuration along with methods //! 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. //! your widget instance an area to draw to.
//! //!
//! The following example renders a block of the size of the terminal: //! The following example renders a block of the size of the terminal:
@@ -125,7 +125,6 @@
pub mod backend; pub mod backend;
pub mod buffer; pub mod buffer;
pub mod layout; pub mod layout;
pub mod style;
pub mod symbols; pub mod symbols;
pub mod terminal; pub mod terminal;
pub mod text; pub mod text;

View File

@@ -1,281 +0,0 @@
//! `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)
);
}
}
}
}
}
}

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; use std::io;
#[derive(Debug, Clone, PartialEq)] #[derive(Debug, Clone, PartialEq)]
@@ -44,8 +45,8 @@ where
buffers: [Buffer; 2], buffers: [Buffer; 2],
/// Index of the current buffer in the previous array /// Index of the current buffer in the previous array
current: usize, current: usize,
/// Whether the cursor is currently hidden /// Kind of cursor (hidden or others)
hidden_cursor: bool, cursor_kind: CursorKind,
/// Viewport /// Viewport
viewport: Viewport, viewport: Viewport,
} }
@@ -56,8 +57,8 @@ where
{ {
fn drop(&mut self) { fn drop(&mut self) {
// Attempt to restore the cursor state // Attempt to restore the cursor state
if self.hidden_cursor { if self.cursor_kind == CursorKind::Hidden {
if let Err(err) = self.show_cursor() { if let Err(err) = self.show_cursor(CursorKind::Block) {
eprintln!("Failed to show the cursor: {}", err); eprintln!("Failed to show the cursor: {}", err);
} }
} }
@@ -92,7 +93,7 @@ where
Buffer::empty(options.viewport.area), Buffer::empty(options.viewport.area),
], ],
current: 0, current: 0,
hidden_cursor: false, cursor_kind: CursorKind::Block,
viewport: options.viewport, viewport: options.viewport,
}) })
} }
@@ -147,7 +148,11 @@ where
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state /// Synchronizes terminal size, calls the rendering closure, flushes the current internal state
/// and prepares for the next draw call. /// 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 // // Autoresize - otherwise we get glitches if shrinking or potential desync between widgets
// // and the terminal (if growing), which may OOB. // // and the terminal (if growing), which may OOB.
// self.autoresize()?; // self.autoresize()?;
@@ -162,12 +167,13 @@ where
// Draw to stdout // Draw to stdout
self.flush()?; self.flush()?;
match cursor_position { if let Some((x, y)) = cursor_position {
None => self.hide_cursor()?,
Some((x, y)) => {
self.show_cursor()?;
self.set_cursor(x, y)?; self.set_cursor(x, y)?;
} }
match cursor_kind {
CursorKind::Hidden => self.hide_cursor()?,
kind => self.show_cursor(kind)?,
} }
// Swap buffers // Swap buffers
@@ -179,15 +185,20 @@ where
Ok(()) Ok(())
} }
#[inline]
pub fn cursor_kind(&self) -> CursorKind {
self.cursor_kind
}
pub fn hide_cursor(&mut self) -> io::Result<()> { pub fn hide_cursor(&mut self) -> io::Result<()> {
self.backend.hide_cursor()?; self.backend.hide_cursor()?;
self.hidden_cursor = true; self.cursor_kind = CursorKind::Hidden;
Ok(()) Ok(())
} }
pub fn show_cursor(&mut self) -> io::Result<()> { pub fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> {
self.backend.show_cursor()?; self.backend.show_cursor(kind)?;
self.hidden_cursor = false; self.cursor_kind = kind;
Ok(()) Ok(())
} }

View File

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

View File

@@ -1,11 +1,10 @@
use crate::{ use crate::{
buffer::Buffer, buffer::Buffer,
layout::Rect,
style::Style,
symbols::line, symbols::line,
text::{Span, Spans}, text::{Span, Spans},
widgets::{Borders, Widget}, widgets::{Borders, Widget},
}; };
use helix_view::graphics::{Rect, Style};
#[derive(Debug, Clone, Copy, PartialEq)] #[derive(Debug, Clone, Copy, PartialEq)]
pub enum BorderType { pub enum BorderType {
@@ -33,7 +32,7 @@ impl BorderType {
/// ///
/// ``` /// ```
/// # use helix_tui::widgets::{Block, BorderType, Borders}; /// # use helix_tui::widgets::{Block, BorderType, Borders};
/// # use helix_tui::style::{Style, Color}; /// # use helix_view::graphics::{Style, Color};
/// Block::default() /// Block::default()
/// .title("Block") /// .title("Block")
/// .borders(Borders::LEFT | Borders::RIGHT) /// .borders(Borders::LEFT | Borders::RIGHT)
@@ -212,7 +211,6 @@ impl<'a> Widget for Block<'a> {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
use crate::layout::Rect;
#[test] #[test]
fn inner_takes_into_account_the_borders() { 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 //! 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. //! 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::paragraph::{Paragraph, Wrap};
pub use self::table::{Cell, Row, Table, TableState}; pub use self::table::{Cell, Row, Table, TableState};
use crate::{buffer::Buffer, layout::Rect}; use crate::buffer::Buffer;
use bitflags::bitflags; use bitflags::bitflags;
use helix_view::graphics::Rect;
bitflags! { bitflags! {
/// Bitflags that can be composed to set the visible borders essentially on the block widget. /// Bitflags that can be composed to set the visible borders essentially on the block widget.
pub struct Borders: u32 { pub struct Borders: u32 {

View File

@@ -1,15 +1,15 @@
use crate::{ use crate::{
buffer::Buffer, buffer::Buffer,
layout::{Alignment, Rect}, layout::Alignment,
style::Style,
text::{StyledGrapheme, Text}, text::{StyledGrapheme, Text},
widgets::{ widgets::{
reflow::{LineComposer, LineTruncator, WordWrapper}, reflow::{LineComposer, LineTruncator, WordWrapper},
Block, Widget, Block, Widget,
}, },
}; };
use helix_core::unicode::width::UnicodeWidthStr;
use helix_view::graphics::{Rect, Style};
use std::iter; use std::iter;
use unicode_width::UnicodeWidthStr;
fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 { fn get_line_offset(line_width: u16, text_area_width: u16, alignment: Alignment) -> u16 {
match alignment { match alignment {
@@ -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::text::{Text, Spans, Span};
/// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap}; /// # use helix_tui::widgets::{Block, Borders, Paragraph, Wrap};
/// # use helix_tui::style::{Style, Color, Modifier};
/// # use helix_tui::layout::{Alignment}; /// # use helix_tui::layout::{Alignment};
/// # use helix_view::graphics::{Style, Color, Modifier};
/// let text = vec![ /// let text = vec![
/// Spans::from(vec![ /// Spans::from(vec![
/// Span::raw("First"), /// Span::raw("First"),

View File

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

View File

@@ -1,7 +1,6 @@
use crate::{ use crate::{
buffer::Buffer, buffer::Buffer,
layout::{Constraint, Rect}, layout::Constraint,
style::Style,
text::Text, text::Text,
widgets::{Block, Widget}, widgets::{Block, Widget},
}; };
@@ -10,19 +9,17 @@ use cassowary::{
WeightedRelation::*, WeightedRelation::*,
{Expression, Solver}, {Expression, Solver},
}; };
use std::{ use helix_core::unicode::width::UnicodeWidthStr;
collections::HashMap, use helix_view::graphics::{Rect, Style};
iter::{self, Iterator}, use std::collections::HashMap;
};
use unicode_width::UnicodeWidthStr;
/// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`]. /// A [`Cell`] contains the [`Text`] to be displayed in a [`Row`] of a [`Table`].
/// ///
/// It can be created from anything that can be converted to a [`Text`]. /// It can be created from anything that can be converted to a [`Text`].
/// ```rust /// ```rust
/// # use helix_tui::widgets::Cell; /// # use helix_tui::widgets::Cell;
/// # use helix_tui::style::{Style, Modifier};
/// # use helix_tui::text::{Span, Spans, Text}; /// # use helix_tui::text::{Span, Spans, Text};
/// # use helix_view::graphics::{Style, Modifier};
/// Cell::from("simple string"); /// Cell::from("simple string");
/// ///
/// Cell::from(Span::from("span")); /// Cell::from(Span::from("span"));
@@ -39,7 +36,7 @@ use unicode_width::UnicodeWidthStr;
/// capabilities of [`Text`]. /// capabilities of [`Text`].
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct Cell<'a> { pub struct Cell<'a> {
content: Text<'a>, pub content: Text<'a>,
style: Style, style: Style,
} }
@@ -74,7 +71,7 @@ where
/// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s: /// But if you need a bit more control over individual cells, you can explicity create [`Cell`]s:
/// ```rust /// ```rust
/// # use helix_tui::widgets::{Row, Cell}; /// # use helix_tui::widgets::{Row, Cell};
/// # use helix_tui::style::{Style, Color}; /// # use helix_view::graphics::{Style, Color};
/// Row::new(vec![ /// Row::new(vec![
/// Cell::from("Cell1"), /// Cell::from("Cell1"),
/// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)), /// Cell::from("Cell2").style(Style::default().fg(Color::Yellow)),
@@ -84,7 +81,7 @@ where
/// By default, a row has a height of 1 but you can change this using [`Row::height`]. /// By default, a row has a height of 1 but you can change this using [`Row::height`].
#[derive(Debug, Clone, PartialEq, Default)] #[derive(Debug, Clone, PartialEq, Default)]
pub struct Row<'a> { pub struct Row<'a> {
cells: Vec<Cell<'a>>, pub cells: Vec<Cell<'a>>,
height: u16, height: u16,
style: Style, style: Style,
bottom_margin: u16, bottom_margin: u16,
@@ -137,7 +134,7 @@ impl<'a> Row<'a> {
/// ```rust /// ```rust
/// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell}; /// # use helix_tui::widgets::{Block, Borders, Table, Row, Cell};
/// # use helix_tui::layout::Constraint; /// # 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}; /// # use helix_tui::text::{Text, Spans, Span};
/// Table::new(vec![ /// Table::new(vec![
/// // Row can be created from simple strings. /// // Row can be created from simple strings.
@@ -415,9 +412,7 @@ impl<'a> Table<'a> {
let has_selection = state.selected.is_some(); let has_selection = state.selected.is_some();
let columns_widths = self.get_columns_widths(table_area.width, has_selection); let columns_widths = self.get_columns_widths(table_area.width, has_selection);
let highlight_symbol = self.highlight_symbol.unwrap_or(""); let highlight_symbol = self.highlight_symbol.unwrap_or("");
let blank_symbol = iter::repeat(" ") let blank_symbol = " ".repeat(highlight_symbol.width());
.take(highlight_symbol.width())
.collect::<String>();
let mut current_height = 0; let mut current_height = 0;
let mut rows_height = table_area.height; let mut rows_height = table_area.height;

View File

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

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

@@ -0,0 +1,318 @@
// Implementation reference: https://github.com/neovim/neovim/blob/f2906a4669a2eef6d7bf86a29648793d63c98949/runtime/autoload/provider/clipboard.vim#L68-L152
use anyhow::Result;
use std::borrow::Cow;
pub enum ClipboardType {
Clipboard,
Selection,
}
pub trait ClipboardProvider: std::fmt::Debug {
fn name(&self) -> Cow<str>;
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String>;
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> 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 ),* ],
},
get_primary_cmd: None,
set_primary_cmd: None,
})
}};
(paste => $get_prg:literal $( , $get_arg:literal )* ;
copy => $set_prg:literal $( , $set_arg:literal )* ;
primary_paste => $pr_get_prg:literal $( , $pr_get_arg:literal )* ;
primary_copy => $pr_set_prg:literal $( , $pr_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 ),* ],
},
get_primary_cmd: Some(provider::CommandConfig {
prg: $pr_get_prg,
args: &[ $( $pr_get_arg ),* ],
}),
set_primary_cmd: Some(provider::CommandConfig {
prg: $pr_set_prg,
args: &[ $( $pr_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";
primary_paste => "wl-paste", "-p", "--no-newline";
primary_copy => "wl-copy", "-p", "--type", "text/plain";
}
} else if env_var_is_set("DISPLAY") && exists("xclip") {
command_provider! {
paste => "xclip", "-o", "-selection", "clipboard";
copy => "xclip", "-i", "-selection", "clipboard";
primary_paste => "xclip", "-o";
primary_copy => "xclip", "-i";
}
} 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";
primary_paste => "xsel", "-o";
primary_copy => "xsel", "-i";
}
} 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 {
#[cfg(target_os = "windows")]
return Box::new(provider::WindowsProvider::new());
#[cfg(not(target_os = "windows"))]
return Box::new(provider::NopProvider::new());
}
}
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, ClipboardType};
use anyhow::{bail, Context as _, Result};
use std::borrow::Cow;
#[derive(Debug)]
pub struct NopProvider {
buf: String,
primary_buf: String,
}
impl NopProvider {
pub fn new() -> Self {
Self {
buf: String::new(),
primary_buf: String::new(),
}
}
}
impl ClipboardProvider for NopProvider {
fn name(&self) -> Cow<str> {
Cow::Borrowed("none")
}
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
let value = match clipboard_type {
ClipboardType::Clipboard => self.buf.clone(),
ClipboardType::Selection => self.primary_buf.clone(),
};
Ok(value)
}
fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> {
match clipboard_type {
ClipboardType::Clipboard => self.buf = content,
ClipboardType::Selection => self.primary_buf = content,
}
Ok(())
}
}
#[cfg(target_os = "windows")]
#[derive(Debug)]
pub struct WindowsProvider {
selection_buf: String,
}
#[cfg(target_os = "windows")]
impl WindowsProvider {
pub fn new() -> Self {
Self {
selection_buf: String::new(),
}
}
}
#[cfg(target_os = "windows")]
impl ClipboardProvider for WindowsProvider {
fn name(&self) -> Cow<str> {
Cow::Borrowed("clipboard-win")
}
fn get_contents(&self, clipboard_type: ClipboardType) -> Result<String> {
match clipboard_type {
ClipboardType::Clipboard => {
let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?;
Ok(contents)
}
ClipboardType::Selection => Ok(String::new()),
}
}
fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> {
match clipboard_type {
ClipboardType::Clipboard => {
clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents);
}
ClipboardType::Selection => {}
};
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,
pub get_primary_cmd: Option<CommandConfig>,
pub set_primary_cmd: Option<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, clipboard_type: ClipboardType) -> Result<String> {
match clipboard_type {
ClipboardType::Clipboard => Ok(self
.get_cmd
.execute(None, true)?
.context("output is missing")?),
ClipboardType::Selection => {
if let Some(cmd) = &self.get_primary_cmd {
return cmd.execute(None, true)?.context("output is missing");
}
Ok(String::new())
}
}
}
fn set_contents(&mut self, value: String, clipboard_type: ClipboardType) -> Result<()> {
let cmd = match clipboard_type {
ClipboardType::Clipboard => &self.set_cmd,
ClipboardType::Selection => {
if let Some(cmd) = &self.set_primary_cmd {
cmd
} else {
return Ok(());
}
}
};
cmd.execute(Some(&value), false).map(|_| ())
}
}
}

File diff suppressed because it is too large Load Diff

View File

@@ -1,53 +1,87 @@
use crate::{theme::Theme, tree::Tree, Document, DocumentId, View, ViewId}; use crate::{
use tui::layout::Rect; 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::{Path, PathBuf},
sync::Arc,
time::Duration,
};
use slotmap::SlotMap; use slotmap::SlotMap;
use anyhow::Error; use anyhow::Error;
pub use helix_core::diagnostic::Severity; pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
use helix_core::syntax;
use helix_core::Position;
use serde::Deserialize;
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
pub struct Config {
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
pub scrolloff: usize,
/// Number of lines to scroll at once. Defaults to 3
pub scroll_lines: isize,
/// Mouse support. Defaults to true.
pub mouse: bool,
/// Middle click paste support. Defaults to true
pub middle_click_paste: bool,
}
impl Default for Config {
fn default() -> Self {
Self {
scrolloff: 5,
scroll_lines: 3,
mouse: true,
middle_click_paste: true,
}
}
}
#[derive(Debug)]
pub struct Editor { pub struct Editor {
pub tree: Tree, pub tree: Tree,
pub documents: SlotMap<DocumentId, Document>, 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 theme: Theme,
pub language_servers: helix_lsp::Registry, 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)>, pub status_msg: Option<(String, Severity)>,
pub config: Config,
} }
#[derive(Copy, Clone)] #[derive(Debug, Copy, Clone)]
pub enum Action { pub enum Action {
Load,
Replace, Replace,
HorizontalSplit, HorizontalSplit,
VerticalSplit, VerticalSplit,
} }
impl Editor { impl Editor {
pub fn new(mut area: tui::layout::Rect) -> Self { pub fn new(
use helix_core::config_dir; mut area: Rect,
let config = std::fs::read(config_dir().join("theme.toml")); themes: Arc<theme::Loader>,
// load $HOME/.config/helix/theme.toml, fallback to default config config_loader: Arc<syntax::Loader>,
let toml = config config: Config,
.as_deref() ) -> Self {
.unwrap_or(include_bytes!("../../theme.toml"));
let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
// initialize language registry
use helix_core::syntax::{Loader, LOADER};
// load $HOME/.config/helix/languages.toml, fallback to default config
let config = std::fs::read(helix_core::config_dir().join("languages.toml"));
let toml = config
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec()));
let language_servers = helix_lsp::Registry::new(); let language_servers = helix_lsp::Registry::new();
// HAXX: offset the render area height by 1 to account for prompt/commandline // HAXX: offset the render area height by 1 to account for prompt/commandline
@@ -57,12 +91,22 @@ impl Editor {
tree: Tree::new(area), tree: Tree::new(area),
documents: SlotMap::with_key(), documents: SlotMap::with_key(),
count: None, count: None,
theme, selected_register: RegisterSelection::default(),
theme: themes.default(),
language_servers, language_servers,
syn_loader: config_loader,
theme_loader: themes,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None, status_msg: None,
config,
} }
} }
pub fn clear_status(&mut self) {
self.status_msg = None;
}
pub fn set_status(&mut self, status: String) { pub fn set_status(&mut self, status: String) {
self.status_msg = Some((status, Severity::Info)); self.status_msg = Some((status, Severity::Info));
} }
@@ -71,43 +115,79 @@ impl Editor {
self.status_msg = Some((error, Severity::Error)); 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) -> anyhow::Result<()> {
use anyhow::Context;
let theme = self
.theme_loader
.load(theme.as_ref())
.with_context(|| format!("failed setting theme `{}`", theme))?;
self.set_theme(theme);
Ok(())
}
fn _refresh(&mut self) { fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() { for (view, _) in self.tree.views_mut() {
let doc = &self.documents[view.doc]; let doc = &self.documents[view.doc];
view.ensure_cursor_in_view(doc) view.ensure_cursor_in_view(doc, self.config.scrolloff)
} }
} }
pub fn switch(&mut self, id: DocumentId, action: Action) { pub fn switch(&mut self, id: DocumentId, action: Action) {
use crate::tree::Layout; use crate::tree::Layout;
use helix_core::Selection; use helix_core::Selection;
if !self.documents.contains_key(id) {
log::error!("cannot switch to document that does not exist (anymore)");
return;
}
match action { match action {
Action::Replace => { Action::Replace => {
let view = self.view(); let view = view!(self);
let jump = ( let jump = (
view.doc, view.doc,
self.documents[view.doc].selection(view.id).clone(), self.documents[view.doc].selection(view.id).clone(),
); );
let view = self.view_mut(); let view = view_mut!(self);
view.jumps.push(jump); view.jumps.push(jump);
view.last_accessed_doc = Some(view.doc);
view.doc = id; view.doc = id;
view.first_line = 0; view.first_line = 0;
let (view, doc) = self.current(); let (view, doc) = current!(self);
// initialize selection for view // initialize selection for view
let selection = doc doc.selections
.selections
.entry(view.id) .entry(view.id)
.or_insert_with(|| Selection::point(0)); .or_insert_with(|| Selection::point(0));
// TODO: reuse align_view // TODO: reuse align_view
let pos = selection.cursor(); let pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos); let line = doc.text().char_to_line(pos);
view.first_line = line.saturating_sub(view.area.height as usize / 2); view.first_line = line.saturating_sub(view.area.height as usize / 2);
return; return;
} }
Action::Load => {
return;
}
Action::HorizontalSplit => { Action::HorizontalSplit => {
let view = View::new(id); let view = View::new(id);
let view_id = self.tree.split(view, Layout::Horizontal); let view_id = self.tree.split(view, Layout::Horizontal);
@@ -128,8 +208,7 @@ impl Editor {
} }
pub fn new_file(&mut self, action: Action) -> DocumentId { pub fn new_file(&mut self, action: Action) -> DocumentId {
use helix_core::Rope; let doc = Document::default();
let doc = Document::new(Rope::from("\n"));
let id = self.documents.insert(doc); let id = self.documents.insert(doc);
self.documents[id].id = id; self.documents[id].id = id;
self.switch(id, action); self.switch(id, action);
@@ -147,13 +226,13 @@ impl Editor {
let id = if let Some(id) = id { let id = if let Some(id) = id {
id id
} else { } else {
let mut doc = Document::load(path)?; 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 // try to find a language server based on the language name
let language_server = doc let language_server = doc
.language .language
.as_ref() .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 { if let Some(language_server) = language_server {
doc.set_language_server(Some(language_server.clone())); doc.set_language_server(Some(language_server.clone()));
@@ -194,7 +273,7 @@ impl Editor {
let language_server = doc let language_server = doc
.language .language
.as_ref() .as_ref()
.and_then(|language| language_servers.get(language)); .and_then(|language| language_servers.get(language).ok());
if let Some(language_server) = language_server { if let Some(language_server) = language_server {
tokio::spawn(language_server.text_document_did_close(doc.identifier())); tokio::spawn(language_server.text_document_did_close(doc.identifier()));
} }
@@ -219,49 +298,70 @@ impl Editor {
self.tree.is_empty() 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) { pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
let view = self.tree.get_mut(id); let view = self.tree.get_mut(id);
let doc = &self.documents[view.doc]; let doc = &self.documents[view.doc];
view.ensure_cursor_in_view(doc) view.ensure_cursor_in_view(doc, self.config.scrolloff)
} }
pub fn document(&self, id: DocumentId) -> Option<&Document> { pub fn document(&self, id: DocumentId) -> Option<&Document> {
self.documents.get(id) self.documents.get(id)
} }
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
self.documents.get_mut(id)
}
pub fn documents(&self) -> impl Iterator<Item = &Document> { pub fn documents(&self) -> impl Iterator<Item = &Document> {
self.documents.iter().map(|(_id, doc)| doc) 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 document_by_path<P: AsRef<Path>>(&self, path: P) -> Option<&Document> {
self.documents()
.find(|doc| doc.path().map(|p| p == path.as_ref()).unwrap_or(false))
}
// pub fn current_document(&self) -> Document { // pub fn current_document(&self) -> Document {
// let id = self.view().doc; // let id = self.view().doc;
// let doc = &mut editor.documents[id]; // 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 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 doc = &self.documents[view.doc];
let cursor = doc.selection(view.id).cursor(); let cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
if let Some(mut pos) = view.screen_coords_at_pos(doc, doc.text().slice(..), 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.col += view.area.x as usize + OFFSET as usize;
pos.row += view.area.y as usize; pos.row += view.area.y as usize;
return Some(pos); (Some(pos), CursorKind::Hidden)
} } else {
None (None, CursorKind::Hidden)
}
}
/// 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(|_| ())
} }
} }

530
helix-view/src/graphics.rs Normal file
View File

@@ -0,0 +1,530 @@
use bitflags::bitflags;
use std::{
cmp::{max, min},
str::FromStr,
};
#[derive(Debug, Clone, Copy, PartialEq)]
/// 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,
}
}
#[inline]
pub fn area(self) -> u16 {
self.width * self.height
}
#[inline]
pub fn left(self) -> u16 {
self.x
}
#[inline]
pub fn right(self) -> u16 {
self.x.saturating_add(self.width)
}
#[inline]
pub fn top(self) -> u16 {
self.y
}
#[inline]
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,
}
}
}
/// Calculate the union between two [`Rect`]s.
pub fn union(self, other: Rect) -> Rect {
// Example:
//
// If `Rect` A is positioned at `(0, 0)` with a width and height of `5`,
// and `Rect` B is positioned at `(5, 0)` with a width and height of `2`,
// then this is the resulting union:
//
// x1 = min(0, 5) => x1 = 0
// y1 = min(0, 0) => y1 = 0
// x2 = max(0 + 5, 5 + 2) => x2 = 7
// y2 = max(0 + 5, 0 + 2) => y2 = 5
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,
}
}
/// Calculate the intersection between two [`Rect`]s.
pub fn intersection(self, other: Rect) -> Rect {
// Example:
//
// If `Rect` A is positioned at `(0, 0)` with a width and height of `5`,
// and `Rect` B is positioned at `(5, 0)` with a width and height of `2`,
// then this is the resulting intersection:
//
// x1 = max(0, 5) => x1 = 5
// y1 = max(0, 0) => y1 = 0
// x2 = min(0 + 5, 5 + 2) => x2 = 5
// y2 = min(0 + 5, 0 + 2) => y2 = 2
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;
}
}
impl FromStr for Modifier {
type Err = &'static str;
fn from_str(modifier: &str) -> Result<Self, Self::Err> {
match modifier {
"bold" => Ok(Self::BOLD),
"dim" => Ok(Self::DIM),
"italic" => Ok(Self::ITALIC),
"underlined" => Ok(Self::UNDERLINED),
"slow_blink" => Ok(Self::SLOW_BLINK),
"rapid_blink" => Ok(Self::RAPID_BLINK),
"reversed" => Ok(Self::REVERSED),
"hidden" => Ok(Self::HIDDEN),
"crossed_out" => Ok(Self::CROSSED_OUT),
_ => Err("Invalid modifier"),
}
}
}
/// 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)
);
}
}
}
}
}
}

60
helix-view/src/info.rs Normal file
View File

@@ -0,0 +1,60 @@
use crate::input::KeyEvent;
use helix_core::unicode::width::UnicodeWidthStr;
use std::fmt::Write;
#[derive(Debug)]
/// Info box used in editor. Rendering logic will be in other crate.
pub struct Info {
/// Title shown at top.
pub title: String,
/// Text body, should contain newlines.
pub text: String,
/// Body width.
pub width: u16,
/// Body height.
pub height: u16,
}
impl Info {
// body is a BTreeMap instead of a HashMap because keymaps are represented
// with nested hashmaps with no ordering, and each invocation of infobox would
// show different orders of items
pub fn key(title: &str, body: Vec<(&str, Vec<KeyEvent>)>) -> Info {
let (lpad, mpad, rpad) = (1, 2, 1);
let keymaps_width: u16 = body
.iter()
.map(|r| r.1.iter().map(|e| e.width() as u16 + 2).sum::<u16>() - 2)
.max()
.unwrap();
let mut text = String::new();
let mut width = 0;
let height = body.len() as u16;
for (desc, keyevents) in body {
let keyevent = keyevents[0];
let mut left = keymaps_width - keyevent.width() as u16;
for _ in 0..lpad {
text.push(' ');
}
write!(text, "{}", keyevent).ok();
for keyevent in &keyevents[1..] {
write!(text, ", {}", keyevent).ok();
left -= 2 + keyevent.width() as u16;
}
for _ in 0..left + mpad {
text.push(' ');
}
let desc = desc.trim();
let w = lpad + keymaps_width + mpad + (desc.width() as u16) + rpad;
if w > width {
width = w;
}
writeln!(text, "{}", desc).ok();
}
Info {
title: title.to_string(),
text,
width,
height,
}
}
}

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

@@ -0,0 +1,318 @@
//! Input event handling, currently backed by crossterm.
use anyhow::{anyhow, Error};
use helix_core::unicode::width::UnicodeWidthStr;
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 KeyEvent {
/// If a character was pressed (without modifiers), return it.
pub fn char(&self) -> Option<char> {
match self.code {
KeyCode::Char(ch) if self.modifiers.is_empty() => Some(ch),
_ => None,
}
}
}
pub(crate) mod keys {
pub(crate) const BACKSPACE: &str = "backspace";
pub(crate) const ENTER: &str = "ret";
pub(crate) const LEFT: &str = "left";
pub(crate) const RIGHT: &str = "right";
pub(crate) const UP: &str = "up";
pub(crate) const DOWN: &str = "down";
pub(crate) const HOME: &str = "home";
pub(crate) const END: &str = "end";
pub(crate) const PAGEUP: &str = "pageup";
pub(crate) const PAGEDOWN: &str = "pagedown";
pub(crate) const TAB: &str = "tab";
pub(crate) const BACKTAB: &str = "backtab";
pub(crate) const DELETE: &str = "del";
pub(crate) const INSERT: &str = "ins";
pub(crate) const NULL: &str = "null";
pub(crate) const ESC: &str = "esc";
pub(crate) const SPACE: &str = "space";
pub(crate) const LESS_THAN: &str = "lt";
pub(crate) const GREATER_THAN: &str = "gt";
pub(crate) const PLUS: &str = "plus";
pub(crate) const MINUS: &str = "minus";
pub(crate) const SEMICOLON: &str = "semicolon";
pub(crate) const PERCENT: &str = "percent";
}
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(keys::BACKSPACE)?,
KeyCode::Enter => f.write_str(keys::ENTER)?,
KeyCode::Left => f.write_str(keys::LEFT)?,
KeyCode::Right => f.write_str(keys::RIGHT)?,
KeyCode::Up => f.write_str(keys::UP)?,
KeyCode::Down => f.write_str(keys::DOWN)?,
KeyCode::Home => f.write_str(keys::HOME)?,
KeyCode::End => f.write_str(keys::END)?,
KeyCode::PageUp => f.write_str(keys::PAGEUP)?,
KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?,
KeyCode::Tab => f.write_str(keys::TAB)?,
KeyCode::BackTab => f.write_str(keys::BACKTAB)?,
KeyCode::Delete => f.write_str(keys::DELETE)?,
KeyCode::Insert => f.write_str(keys::INSERT)?,
KeyCode::Null => f.write_str(keys::NULL)?,
KeyCode::Esc => f.write_str(keys::ESC)?,
KeyCode::Char(' ') => f.write_str(keys::SPACE)?,
KeyCode::Char('<') => f.write_str(keys::LESS_THAN)?,
KeyCode::Char('>') => f.write_str(keys::GREATER_THAN)?,
KeyCode::Char('+') => f.write_str(keys::PLUS)?,
KeyCode::Char('-') => f.write_str(keys::MINUS)?,
KeyCode::Char(';') => f.write_str(keys::SEMICOLON)?,
KeyCode::Char('%') => f.write_str(keys::PERCENT)?,
KeyCode::F(i) => f.write_fmt(format_args!("F{}", i))?,
KeyCode::Char(c) => f.write_fmt(format_args!("{}", c))?,
};
Ok(())
}
}
impl UnicodeWidthStr for KeyEvent {
fn width(&self) -> usize {
use helix_core::unicode::width::UnicodeWidthChar;
let mut width = match self.code {
KeyCode::Backspace => keys::BACKSPACE.len(),
KeyCode::Enter => keys::ENTER.len(),
KeyCode::Left => keys::LEFT.len(),
KeyCode::Right => keys::RIGHT.len(),
KeyCode::Up => keys::UP.len(),
KeyCode::Down => keys::DOWN.len(),
KeyCode::Home => keys::HOME.len(),
KeyCode::End => keys::END.len(),
KeyCode::PageUp => keys::PAGEUP.len(),
KeyCode::PageDown => keys::PAGEDOWN.len(),
KeyCode::Tab => keys::TAB.len(),
KeyCode::BackTab => keys::BACKTAB.len(),
KeyCode::Delete => keys::DELETE.len(),
KeyCode::Insert => keys::INSERT.len(),
KeyCode::Null => keys::NULL.len(),
KeyCode::Esc => keys::ESC.len(),
KeyCode::Char(' ') => keys::SPACE.len(),
KeyCode::Char('<') => keys::LESS_THAN.len(),
KeyCode::Char('>') => keys::GREATER_THAN.len(),
KeyCode::Char('+') => keys::PLUS.len(),
KeyCode::Char('-') => keys::MINUS.len(),
KeyCode::Char(';') => keys::SEMICOLON.len(),
KeyCode::Char('%') => keys::PERCENT.len(),
KeyCode::F(1..=9) => 2,
KeyCode::F(_) => 3,
KeyCode::Char(c) => c.width().unwrap_or(0),
};
if self.modifiers.contains(KeyModifiers::SHIFT) {
width += 2;
}
if self.modifiers.contains(KeyModifiers::ALT) {
width += 2;
}
if self.modifiers.contains(KeyModifiers::CONTROL) {
width += 2;
}
width
}
fn width_cjk(&self) -> usize {
self.width()
}
}
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"))? {
keys::BACKSPACE => KeyCode::Backspace,
keys::ENTER => KeyCode::Enter,
keys::LEFT => KeyCode::Left,
keys::RIGHT => KeyCode::Right,
keys::UP => KeyCode::Up,
keys::DOWN => KeyCode::Down,
keys::HOME => KeyCode::Home,
keys::END => KeyCode::End,
keys::PAGEUP => KeyCode::PageUp,
keys::PAGEDOWN => KeyCode::PageDown,
keys::TAB => KeyCode::Tab,
keys::BACKTAB => KeyCode::BackTab,
keys::DELETE => KeyCode::Delete,
keys::INSERT => KeyCode::Insert,
keys::NULL => KeyCode::Null,
keys::ESC => KeyCode::Esc,
keys::SPACE => KeyCode::Char(' '),
keys::LESS_THAN => KeyCode::Char('<'),
keys::GREATER_THAN => KeyCode::Char('>'),
keys::PLUS => KeyCode::Char('+'),
keys::MINUS => KeyCode::Char('-'),
keys::SEMICOLON => KeyCode::Char(';'),
keys::PERCENT => KeyCode::Char('%'),
single if single.chars().count() == 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 down 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,
}
}
}

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