Compare commits

...

957 Commits

Author SHA1 Message Date
Blaž Hrastnik
a1b7f003a6 Include the missing dependency bump 2021-10-28 16:44:36 +09:00
Blaž Hrastnik
f3c7f20dbc Release v0.5.0 2021-10-28 16:41:34 +09:00
Omnikar
db56de589a Add --tutor option to hx --help output (#924)
* Add `--tutor` option to `hx --help` output

* Adjust `--tutor` location in help output
2021-10-28 16:27:28 +09:00
Blaž Hrastnik
c1e5831b21 set_path: Pass in the function directly 2021-10-28 10:51:19 +09:00
Blaž Hrastnik
3e69a4852e Simplify set_path 2021-10-28 10:50:17 +09:00
Gygaxis Vainhardt
0a38983ee3 Remove three transmutes from helix-core syntax.rs (#923) 2021-10-28 10:24:11 +09:00
Omnikar
e2ed691537 Implement hx --tutor and :tutor to load tutor.txt (#898)
* Implement `hx --tutor` and `:tutor` to load `tutor.txt`

* Document `hx --tutor` and `:tutor`

* Change `Document::set_path` to take an `Option`

* `Document::set_path` accepts an `Option<&Path>` instead of `&Path`.
* Remove `Editor::open_tutor` and make tutor-open functionality use
  `Editor::open` and `Document::set_path`.

* Use `PathBuf::join`

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

* Add comments explaining unsetting tutor path

Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-10-28 10:23:46 +09:00
Nehliin
3b0c5e993a Use deserialization fix instead 2021-10-28 10:22:52 +09:00
Oskar Nehlin
6e455fd3fb Apply suggestions from code review
Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2021-10-28 10:22:52 +09:00
Nehliin
da4d9340ba Make key macro more portable 2021-10-28 10:22:52 +09:00
Nehliin
a4c5f46739 Fix order being empty and add test 2021-10-28 10:22:52 +09:00
Nehliin
f133d80e70 Move test to test module 2021-10-28 10:22:52 +09:00
Nehliin
fbba47fbc0 Fix panic when using multi-level key mapping 2021-10-28 10:22:52 +09:00
Blaž Hrastnik
5501669f8c Revert "minor: Rearrange helix-term Cargo.toml"
This reverts commit 2cee0c58ba.
2021-10-28 00:21:30 +09:00
Blaž Hrastnik
4a32851103 Break CI cache 2021-10-28 00:13:21 +09:00
Blaž Hrastnik
1066b081dd fix: When cycling through prompt history, update event needs to trigger 2021-10-27 18:23:17 +09:00
Blaž Hrastnik
2cee0c58ba minor: Rearrange helix-term Cargo.toml 2021-10-27 12:25:00 +09:00
Blaž Hrastnik
3fe353c47c Remove some old TODOs 2021-10-27 12:25:00 +09:00
Blaž Hrastnik
e36ad8b4ed minor: Further simplify take_with 2021-10-27 12:25:00 +09:00
Omnikar
2505802d39 Improve statusline (#916)
* Improve statusline

* Change diagnostic count display to show counts of individual
  diagnostic types next to their corresponding gutter dots.
* Add selection count to the statusline.

* Do not display info or hint count in statusline

* Reduce padding

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

* Reduce padding

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

* Use `Span::styled`

* Reduce padding

* Use `Style::patch`

* Remove unnecessary `Cow` creation

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2021-10-27 12:24:24 +09:00
Michael Davis
7e6ade9290 fix: string.regex{=>p} 2021-10-27 10:03:33 +09:00
Michael Davis
bf20e51044 use punctuation.special for interpolation #{ } 2021-10-27 10:03:33 +09:00
radical3dd
d61e5e686b Use current dir for file picker, after change dir. (#910) 2021-10-26 09:43:14 +09:00
CossonLeo
f331ba9df4 Clear competion items when start_offset > cursor (#906) 2021-10-26 09:42:37 +09:00
CossonLeo
b142fd4080 move_up will select last item, when no item selected (#907) 2021-10-26 09:42:23 +09:00
CossonLeo
bca98b5bed Add c-j c-k to menu keymap for move_up move_down (#908) 2021-10-26 09:42:08 +09:00
dependabot[bot]
a0cb9d82d1 build(deps): bump clipboard-win from 4.2.1 to 4.2.2 (#911)
Bumps [clipboard-win](https://github.com/DoumanAsh/clipboard-win) from 4.2.1 to 4.2.2.
- [Release notes](https://github.com/DoumanAsh/clipboard-win/releases)
- [Commits](https://github.com/DoumanAsh/clipboard-win/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-26 09:41:45 +09:00
Kirawi
92c2d5d3bf Document more of helix-core (#904) 2021-10-26 01:02:16 +09:00
Blaž Hrastnik
acc5ac5e73 fix warning 2021-10-25 11:11:11 +09:00
Blaž Hrastnik
3edca7854e completion: fully revert state before apply & insertText common prefix 2021-10-25 11:09:09 +09:00
Blaž Hrastnik
bfb6cff5a9 fix: Compose where changes.compose(empty_other) 2021-10-25 11:09:09 +09:00
Ray Gervais
d4d16ca1b0 runtime: Rose Pine colorscheme (#897) 2021-10-25 10:18:04 +09:00
Omnikar
a7d87c79ce Fix :quit! description and tense of other commands (#902) 2021-10-25 09:25:47 +09:00
Blaž Hrastnik
42eee9d5bf book: Document Alt-. and . 2021-10-24 23:09:24 +09:00
CossonLeo
2ed01f2d9c find motion and textobj motion repeat (#891) 2021-10-24 22:47:10 +09:00
Blaž Hrastnik
cee7ad781e Mark a few functions as const 2021-10-24 17:28:29 +09:00
Blaž Hrastnik
c913bade0a fix: Indentation used different default on hx vs hx new_file.txt 2021-10-24 17:20:30 +09:00
Blaž Hrastnik
4b4e972af0 nix: Update lld to 12 2021-10-24 16:55:43 +09:00
CossonLeo
971ba8929f Filter completion items from language server by starts_with word under cursor (#883)
* filter items by starts_with pre nth char of cursor

* add config for filter completion items by starts_with

* filter items by starts_with pre nth char of cursor

* add config for filter completion items by starts_with

* remove completion items pre filter configuratio
2021-10-24 16:55:29 +09:00
Kirawi
0cb5e0b2ca log syntax highlighting init errors (#895) 2021-10-23 21:52:18 +09:00
Oskar Nehlin
0f886af4b9 Add commands for moving between splits with a direction (#860)
* Add commands for moving between splits with a direction

* Update keymaps

* Change picker mapping

* Add test and clean up some comments
2021-10-23 20:06:40 +09:00
Gokul Soumya
4ee92cad19 Add treesitter textobjects (#728)
* Add treesitter textobject queries

Only for Go, Python and Rust for now.

* Add tree-sitter textobjects

Only has functions and class objects as of now.

* Fix tests

* Add docs for tree-sitter textobjects

* Add guide for creating new textobject queries

* Add parameter textobject

Only parameter.inside is implemented now, parameter.around
will probably require custom predicates akin to nvim' `make-range`
since we want to select a trailing comma too (a comma will be
an anonymous node and matching against them doesn't work similar
to named nodes)

* Simplify TextObject cell init
2021-10-23 11:41:19 +09:00
Blaž Hrastnik
c5298caa75 book: Add a link to tutor.txt 2021-10-23 11:33:17 +09:00
ath3
787ba4f233 CMake support (#888) 2021-10-23 08:57:21 +09:00
Rowan H
6c995fa690 Fixed incorrect move commands (#894) 2021-10-23 08:54:23 +09:00
Rowan H
75a8e8afbd Typo fix (#893) 2021-10-23 08:54:02 +09:00
Blaž Hrastnik
96945be1a8 Fix doctest broken on 2021 edition 2021-10-22 12:47:02 +09:00
Blaž Hrastnik
182a59b552 Update to rust 1.56 + 2021 edition 2021-10-22 12:15:18 +09:00
Daniel S Poulin
3b032e8e1f First stab at ignoring compressed files from picker (#767) 2021-10-22 10:02:05 +09:00
Ray Gervais
2edc85e953 fixes: missing info, warning diagnostic (#890) 2021-10-22 09:58:49 +09:00
Omnikar
f467154e18 Add Alt-, to keymap.md, and replace hard-to-see commas with slashes (#884)
* Add `A-,` to `keymap.md`, and remove out-of-place commas

* Update book/src/keymap.md

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

* Add slashes in place of previous commas in `keymap.md`

Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-10-22 09:58:26 +09:00
radical3dd
b1ebd7a07e Replace current selection with all yanked values. (#882) 2021-10-21 09:44:53 +09:00
Blaž Hrastnik
e9b23c29d8 Ignore errors when disabling mouse capture 2021-10-20 00:01:11 +09:00
Blaž Hrastnik
9688cb74a1 Update dependencies to bump crossterm to 0.22.1
Fixes #825
Fixes #690
2021-10-19 23:58:51 +09:00
VuiMuich
67829976fa Add C-j and C-k to keybinds for picker (#876)
* Add `C-j` and `C-k` for moving down/up in pickers

* Add new binds to keymap doc
2021-10-19 18:37:38 +09:00
Michael Davis
1766bdb9d4 clean up combined-injections comment (#880) 2021-10-19 13:08:06 +09:00
WindSoilder
7146ae9388 Refactor nord theme (#874)
* refactor again

* remove useless color
2021-10-19 12:17:05 +09:00
dependabot[bot]
cdfa0dfa36 build(deps): bump chardetng from 0.1.14 to 0.1.15 (#879)
Bumps [chardetng](https://github.com/hsivonen/chardetng) from 0.1.14 to 0.1.15.
- [Release notes](https://github.com/hsivonen/chardetng/releases)
- [Commits](https://github.com/hsivonen/chardetng/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-19 09:36:25 +09:00
dependabot[bot]
c212325e6a build(deps): bump encoding_rs from 0.8.28 to 0.8.29 (#877)
Bumps [encoding_rs](https://github.com/hsivonen/encoding_rs) from 0.8.28 to 0.8.29.
- [Release notes](https://github.com/hsivonen/encoding_rs/releases)
- [Commits](https://github.com/hsivonen/encoding_rs/compare/v0.8.28...v0.8.29)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-19 09:35:52 +09:00
WindSoilder
2ac9d30bf3 improve menu selected color for nord (#873) 2021-10-18 16:39:54 +09:00
CossonLeo
9ac0c95161 Improve completion trigger (#838)
* improve idle completion trigger

* add completion-trigger-len to book

* rename semantics_completion to language_server_completion and optimize idle completion trigger
2021-10-18 15:14:50 +09:00
Blaž Hrastnik
a03b12530c Merge pull request #830 from the-mikedavis/official-elixir-tree-sitter
prefer elixir-lang/tree-sitter-elixir
2021-10-18 15:13:39 +09:00
Ray Gervais
c278b43319 adds: base16 theme for Helix editor (#833) 2021-10-18 10:31:57 +09:00
WindSoilder
bb011f9fb2 Add indents for python, but it's not perfect. (#837)
* add indents for python, but it's not Perfect

* add last line
2021-10-18 10:01:53 +09:00
Michael Davis
4d8eb09b7c scope arities in captures as operators 2021-10-17 10:50:20 -05:00
Michael Davis
80b54f2f69 use special.string.symbol instead of symbol
this aligns better with how ruby highlights symbols
2021-10-17 10:50:20 -05:00
Michael Davis
8f658f0dce use latest tree-sitter-elixir with 'not in' query support
connects https://github.com/elixir-lang/tree-sitter-elixir/issues/9
2021-10-17 10:50:20 -05:00
Michael Davis
4771cc7ee4 align highlight scopes with documented scopes 2021-10-17 10:50:20 -05:00
Michael Davis
c502cafecc highlight calls to erlang modules as types
connects https://github.com/elixir-lang/tree-sitter-elixir/pull/5
2021-10-17 10:50:20 -05:00
Michael Davis
b2655a7f5c add LICENSE snippet at elixir hightlights top 2021-10-17 10:50:19 -05:00
Michael Davis
95ab40d171 use the warning type for tree-sitter ERRORs 2021-10-17 10:50:19 -05:00
Michael Davis
5db248cc1c describe atoms as tags 2021-10-17 10:50:19 -05:00
Michael Davis
d1b434d230 add highlights query from elixir-lang/tree-sitter-elixir 2021-10-17 10:50:19 -05:00
Michael Davis
6c0786edc5 prefer elixir-lang/tree-sitter-elixir 2021-10-17 10:50:19 -05:00
Michael Davis
e216e9621e Enable c-sharp language and highlights (#861) 2021-10-17 13:45:09 +09:00
Ivan Tham
89707a858f Make auto-completion a config (#853) 2021-10-16 22:57:41 +09:00
Blaž Hrastnik
2c0468ffd1 fix: If backspacing past the start offset, cancel completion
Refs #822
2021-10-16 18:43:07 +09:00
Michael Davis
be428a295a fix digit escapes in java & php highlights (#846) 2021-10-16 18:02:06 +09:00
Michael Davis
e069fb9dea Add highlight support for tree-sitter-query language (tsq) (#845)
* add submodule on tree-sitter/tree-sitter-tsq

mark tsq submodule as shallow

* add tree-sitter-tsq to languages

* add highlight queries for tsq

* Update .gitmodules

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2021-10-16 17:58:04 +09:00
Omnikar
43465926be Continue tutor (#737)
* Add sections

* `COUNTS WITH MOTIONS`
* `SELECTING LINES`
* `UNDOING`

* Adjust lesson spacing to conform to page-wise scrolling

Vertical length of lessons reduced by 1 line so that page-up and
page-down move cleanly between lessons.

* Add sections

* `THE CHANGE COMMAND`
* `RECAP`
* `MULTIPLE CURSORS`

* Fix height of `RECAP` section

* Fix typo in `MULTIPLE CURSORS`

* Add additional information about space mode to `MULTIPLE CURSORS`

* Change `<SPACE><SPACE>` to `,`

* Add sections

* `THE SELECT COMMAND`
* `SELECTING VIA REGEX`
* `COLLAPSING SELECTIONS`

* Fix quote inconsistency
2021-10-16 12:47:45 +09:00
Omnikar
6063ecf3b4 Add note about FAQ to README.md (#848) 2021-10-16 10:05:29 +09:00
Omnikar
c71b49497d Set CWD when editor is started with a directory (#849) 2021-10-16 10:04:26 +09:00
Leoi Hung Kin
4d07eaa48b Prevent LSP Messages from displaying when a prompt is presented (#824)
* Prevent LSP Messages from displaying when a prompt is presented

* use match guard
2021-10-15 17:36:39 +09:00
WindSoilder
ef3f78b6ce fix nord ui focus color (#844) 2021-10-15 17:36:01 +09:00
WindSoilder
47208b990b improve contract on nord comment color (#842) 2021-10-14 18:03:35 +09:00
WindSoilder
b42ef0e028 Using pylsp instead of pyls (#834) 2021-10-13 11:24:37 +09:00
dependabot[bot]
933db94f2f build(deps): bump lsp-types from 0.90.0 to 0.90.1 (#829)
Bumps [lsp-types](https://github.com/gluon-lang/lsp-types) from 0.90.0 to 0.90.1.
- [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.90.0...v0.90.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-12 09:40:19 +09:00
dependabot[bot]
a6b393f598 build(deps): bump thiserror from 1.0.29 to 1.0.30 (#828)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.29 to 1.0.30.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.29...1.0.30)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-12 09:40:06 +09:00
dependabot[bot]
6cb0d1c4e4 build(deps): bump libloading from 0.7.0 to 0.7.1 (#827)
Bumps [libloading](https://github.com/nagisa/rust_libloading) from 0.7.0 to 0.7.1.
- [Release notes](https://github.com/nagisa/rust_libloading/releases)
- [Commits](https://github.com/nagisa/rust_libloading/commits)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-12 09:39:56 +09:00
dependabot[bot]
c15e3b32d6 build(deps): bump cc from 1.0.70 to 1.0.71 (#826)
Bumps [cc](https://github.com/alexcrichton/cc-rs) from 1.0.70 to 1.0.71.
- [Release notes](https://github.com/alexcrichton/cc-rs/releases)
- [Commits](https://github.com/alexcrichton/cc-rs/compare/1.0.70...1.0.71)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-10-12 09:39:48 +09:00
Blaž Hrastnik
a930f99179 fix: Make sure to actually use idle_timeout config value for the timers 2021-10-10 22:39:47 +09:00
Blaž Hrastnik
f8f63c5508 Merge pull request #821 from helix-editor/idle-timer
Idle timer / Autocompletion
2021-10-10 22:11:01 +09:00
Thomas Wehmöller
a7f49fa56f Add Vue tree sitter grammar (#787)
*  Add vue tree sitter support

* Update .gitmodules

Co-authored-by: Blaž Hrastnik <blaz@mxxn.io>
2021-10-10 22:09:17 +09:00
Blaž Hrastnik
76b1bbc23a Allow trigger_offset to be unused for now 2021-10-10 12:33:22 +09:00
Blaž Hrastnik
633b981db2 Make idle-timeout configurable 2021-10-10 12:32:06 +09:00
Blaž Hrastnik
c7f3a971c0 Remove resolved TODOs 2021-10-10 12:22:11 +09:00
Ivan Tham
4260b31ec0 Update mdbook style and fix unreadable table head (#806)
The styles are now pulled from upstream styles, some of the changes I
submitted it back to upstream.

Fix #796
2021-10-09 20:35:27 +09:00
Leoi Hung Kin
a6852fb88f Picker: Don't panick at move_up/move_down when matches is empty (#818) 2021-10-09 20:34:10 +09:00
Midnight Exigent
eedcea7e6b Allow language.config (in languages.toml) to be passed in as a toml object (#807)
* allow language.config (in languages.toml) to be passed in as a toml object

* Change config field for languages from json string to toml object

* remove indents on languages.toml config

* fix: remove patch version from serde_json import in helix-core

* Use same tree-sitter-zig as upstream/master
2021-10-08 11:14:12 +09:00
Ethan Frei
9f27be429d relative paths showing active file in global search (#803) 2021-10-08 11:08:10 +09:00
James Cash
2e692dc184 Add (SWI-)Prolog LSP support (#816)
As discussed in #809 ; I also have a [tree-sitter implementation](https://github.com/jamesnvc/tree-sitter-prolog), but for reasons discussed in the linked post, I kind of gave up on that sort of static approach for making a general-purpose Prolog grammar (since it has a very flexible syntax and allows defining new operators with new precedences dynamically).

That being said, the LSP implementation here at least shows documentation and does support the semantic token API, so when Helix supports that, this should also provide highlighting.
2021-10-08 11:05:30 +09:00
Blaž Hrastnik
f692ede2b7 fix: Don't crash on empty completion, don't retrigger on close 2021-10-07 10:37:35 +09:00
Blaž Hrastnik
8ca91891d1 fix compilation 2021-10-05 22:35:38 +09:00
Blaž Hrastnik
66f26e82ce Filter the initial completion 2021-10-05 22:27:35 +09:00
Blaž Hrastnik
40abec80e1 Experiment with autocompletion on idle 2021-10-05 22:27:33 +09:00
Blaž Hrastnik
f99bea404f idle timer wip 2021-10-05 22:27:10 +09:00
dependabot[bot]
8925fdd6f3 build(deps): bump smallvec from 1.6.1 to 1.7.0 (#813)
Bumps [smallvec](https://github.com/servo/rust-smallvec) from 1.6.1 to 1.7.0.
- [Release notes](https://github.com/servo/rust-smallvec/releases)
- [Commits](https://github.com/servo/rust-smallvec/compare/v1.6.1...v1.7.0)

---
updated-dependencies:
- dependency-name: smallvec
  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>
2021-10-05 12:18:37 +09:00
dependabot[bot]
97b24fd91e build(deps): bump similar from 2.0.0 to 2.1.0 (#812)
Bumps [similar](https://github.com/mitsuhiko/similar) from 2.0.0 to 2.1.0.
- [Release notes](https://github.com/mitsuhiko/similar/releases)
- [Changelog](https://github.com/mitsuhiko/similar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/similar/compare/2.0.0...2.1.0)

---
updated-dependencies:
- dependency-name: similar
  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>
2021-10-05 12:18:25 +09:00
voroskoi
0e06c10d8c Zig tree-sitter rework (#811)
- update tree-sitter-zig subproject
- use highlights.scm from upstream, just use helix scopes
- update indents.toml, this one actually works
2021-10-05 12:18:15 +09:00
Irevoire
c4ae17dfd4 fix clippy warnings (#804) 2021-10-03 12:40:33 +09:00
Irevoire
7e958e1834 Add a bunch of aliases (#797)
* add a bunch of aliases

* apply code review from archseer
2021-10-03 11:41:41 +09:00
Ray Gervais
0af8928d63 adds: nord colortheme (#799) 2021-10-03 10:13:53 +09:00
Dylan Richardson
4a92a79da4 global search: show file names as relative paths (#802)
This commit fixes #786
2021-10-03 08:41:52 +09:00
Omnikar
e47632114a Fix swapped selection rotation docs in keymap.md (#792) 2021-09-29 21:07:16 +09:00
dependabot[bot]
d68cff837f build(deps): bump lsp-types from 0.89.2 to 0.90.0
Bumps [lsp-types](https://github.com/gluon-lang/lsp-types) from 0.89.2 to 0.90.0.
- [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.2...v0.90.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-09-28 08:29:14 +08:00
dependabot[bot]
466e69bbb9 build(deps): bump tokio from 1.11.0 to 1.12.0
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.11.0 to 1.12.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.11.0...tokio-1.12.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-09-28 08:28:52 +08:00
Matt W
df55eaae69 Add tilde expansion for file opening (#782)
* change to helix_core's tilde expansion,
    from helix-core::path::expand_tilde
2021-09-24 11:21:04 +09:00
Blaž Hrastnik
2e0803c8d9 Implement 'remove_primary_selection' as Alt-,
This allows removing search matches from the selection

Fixes #713
2021-09-24 10:30:28 +09:00
Blaž Hrastnik
75dba1f956 experiment: space+k for LSP doc, K for keep_selections 2021-09-24 10:30:23 +09:00
Blaž Hrastnik
9ea9e779b2 experiment: Move keep_primary_selection to , 2021-09-24 10:30:17 +09:00
lurpahi
a958d34bfb Add option for automatic insertion of closing-parens/brackets/etc (#779)
* Add auto-pair editor option

* Document auto-pair editor option

* Make cargo fmt happy

* Actually make cargo fmt happy

* Rename auto-pair option to auto-pairs

* Inline a few constants

Co-authored-by: miaomai <cunso@tutanota.com>
2021-09-24 10:28:44 +09:00
Leoi Hung Kin
432bec10ed allow smart case in global search (#781) 2021-09-24 10:27:16 +09:00
Leoi Hung Kin
9456d5c1a2 Initial implementation of global search (#651)
* initial implementation of global search

* use tokio::sync::mpsc::unbounded_channel instead of Arc, Mutex, Waker poll_fn

* use tokio_stream::wrappers::UnboundedReceiverStream to collect all search matches

* regex_prompt: unified callback; refactor

* global search doc
2021-09-22 01:03:12 +09:00
dependabot[bot]
a512f48e45 build(deps): bump serde_json from 1.0.67 to 1.0.68 (#770)
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.67 to 1.0.68.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.67...v1.0.68)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-21 10:01:53 +09:00
dependabot[bot]
5b4ae7c7b6 build(deps): bump unicode-width from 0.1.8 to 0.1.9 (#771)
Bumps [unicode-width](https://github.com/unicode-rs/unicode-width) from 0.1.8 to 0.1.9.
- [Release notes](https://github.com/unicode-rs/unicode-width/releases)
- [Commits](https://github.com/unicode-rs/unicode-width/compare/v0.1.8...v0.1.9)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-21 10:01:45 +09:00
dependabot[bot]
589f0acec5 build(deps): bump arc-swap from 1.3.2 to 1.4.0 (#772)
Bumps [arc-swap](https://github.com/vorner/arc-swap) from 1.3.2 to 1.4.0.
- [Release notes](https://github.com/vorner/arc-swap/releases)
- [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/arc-swap/compare/v1.3.2...v1.4.0)

---
updated-dependencies:
- dependency-name: arc-swap
  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>
2021-09-21 10:01:37 +09:00
kraem
4a003782a5 enable smart case regex search by default (#761) 2021-09-20 13:45:07 +09:00
Raphael Megzari
e0e41f4f77 languages: add svelte submodule reference (#766) 2021-09-19 11:55:15 +09:00
Raphael Megzari
ae4d37de28 flake: remove hack to fix helix version (#762) 2021-09-18 09:59:26 +09:00
Alex
70a20b7cf8 add everforest dark theme (#760) 2021-09-17 22:40:24 +09:00
Leoi Hung Kin
1d04e5938d search_next_impl: don't panic on invalid regex (#740) 2021-09-17 17:22:17 +09:00
Blaž Hrastnik
3ff5b001ac fix: Don't allow closing the last split if there's unsaved changes
Fixes #674
2021-09-17 14:43:06 +09:00
Blaž Hrastnik
c7d6e4461f fix: Wrap around the top of the picker menu when scrolling
Forgot to port the improvements in menu.rs

Fixes #734
2021-09-17 14:43:06 +09:00
Blaž Hrastnik
b02d872938 fix: Refactor apply_workspace_edit to remove assert
Fixes #698
2021-09-17 14:43:06 +09:00
Blaž Hrastnik
07be66c677 Revert parameter underlining on default theme
I like it, but it clashes with diagnostics underlines since we can't
color them differently in the terminal. If undercurl support is
sufficient enough I'd consider changing diagnostics to use that instead.
2021-09-17 14:43:06 +09:00
Raphael Megzari
b2195e08b5 languages: add svelte support (#733)
* languages: add svelte support

* languages: add svelte injections
2021-09-17 11:04:55 +09:00
Blaž Hrastnik
64e8f0017c ... 2021-09-16 16:04:32 +09:00
Blaž Hrastnik
d8b94ba85f Fix broken test 2021-09-16 15:54:43 +09:00
Blaž Hrastnik
dd0b15e1f1 syntax: Properly handle injection-regex for language injections 2021-09-16 15:50:14 +09:00
Kirawi
ef532e0c0d log errors produced when trying to initialize the LSP (#746) 2021-09-15 14:58:06 +09:00
dependabot[bot]
51b7f40da1 build(deps): bump similar from 1.3.0 to 2.0.0 (#754)
Bumps [similar](https://github.com/mitsuhiko/similar) from 1.3.0 to 2.0.0.
- [Release notes](https://github.com/mitsuhiko/similar/releases)
- [Changelog](https://github.com/mitsuhiko/similar/blob/main/CHANGELOG.md)
- [Commits](https://github.com/mitsuhiko/similar/compare/1.3.0...2.0.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-14 09:21:35 +09:00
dependabot[bot]
2f32d1859d build(deps): bump anyhow from 1.0.43 to 1.0.44 (#755)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.43 to 1.0.44.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.43...1.0.44)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-14 09:21:24 +09:00
Gokul Soumya
116e562ff6 Document diagnostic theme scope (#751) 2021-09-13 17:48:58 +09:00
Omnikar
3e12b00993 Add no_op command (#743)
* Add `no_op` command

* Document `no_op` in `remapping.md`
2021-09-13 17:48:12 +09:00
Blaž Hrastnik
1540b37f34 lsp: Silence window/logMessage if -v isn't used 2021-09-13 17:45:02 +09:00
Blaž Hrastnik
066367c0a4 fix: Need to reset set_byte_range in case cursor_ref is reused. 2021-09-13 17:44:57 +09:00
Blaž Hrastnik
32977ed341 ui: Trigger recalculate_size per popup render so contents can readjust 2021-09-13 17:44:50 +09:00
Kirawi
f2c73d1567 Update dark_plus error colour
This was recently changed in VSCode.
2021-09-12 21:53:10 +05:30
Yusuf Bera Ertan
004c8fd462 chore(nix): update flake inputs and submodule 2021-09-12 20:11:04 +08:00
Kangwook Lee (이강욱)
05c2a72ccb goto line start/end commands extend when in select mode (#739) 2021-09-11 18:31:40 +09:00
Kirawi
987d8e6dd6 Convert clipboard line ending to document line ending when pasting (#716)
* convert a paste's line-ending to the current doc's line-ending

* move paste regex into paste_impl
2021-09-11 00:12:26 +09:00
Gokul Soumya
94abc52b3b feat: Sticky view mode with Z (#719) 2021-09-10 23:14:23 +09:00
Blaž Hrastnik
0b1bc566e4 fix: lsp: Regression with textDocument/didSave not getting sent 2021-09-09 11:54:43 +09:00
Blaž Hrastnik
bb47a9a0b8 fix: Fix regression where formatting would fail on null response 2021-09-09 11:49:45 +09:00
cbarrete
394cc4f30f Update ledger treesitter injections (#732)
Co-authored-by: Cédric Barreteau <cbarrete@users.noreply.github.com>
2021-09-09 10:13:11 +09:00
Blaž Hrastnik
3426285a63 fix: Don't automatically search_next on *
Refs #713
2021-09-08 16:34:04 +09:00
Blaž Hrastnik
d991715ff1 Switch rust-toolchain.toml over to stable 2021-09-08 16:34:04 +09:00
Blaž Hrastnik
72cf86e462 Regex prompts should have a history with a specifiable register 2021-09-08 16:34:04 +09:00
CossonLeo
011f9aa47f Optimize completion doc position. (#691)
* optimize completion doc's render

* optimize completion doc's render

* optimize completion doc position

* cargo fmt

* fix panic

* use saturating_sub

* fixs

* fix clippy

* limit completion doc max width 120
2021-09-08 16:33:59 +09:00
Blaž Hrastnik
2ce87968cd ui: Be smarter about centering previews
Try centering the whole block. If the block is too big for the viewport,
then make sure that the first line is within the preview.
2021-09-08 14:19:25 +09:00
Raphael Megzari
f871d318c0 add language server for elixir and nix (#725) 2021-09-07 23:23:05 +09:00
Ivan Tham
89f0dbe8e8 Update tree-sitter-ledger (#724) 2021-09-07 23:22:53 +09:00
Kangwook Lee (이강욱)
7a9db95182 Add command to extend to line start or end (#717) 2021-09-07 23:22:39 +09:00
Blaž Hrastnik
fd36fbdebf Merge branch 'lsp-async-init' 2021-09-07 13:05:53 +09:00
Blaž Hrastnik
3cbdc057de lsp: Don't import SymbolServer for Julia anymore, it's not necessary 2021-09-07 13:05:20 +09:00
Blaž Hrastnik
4cc562318a Improve docs, fix up a few highlight scopes 2021-09-07 13:03:48 +09:00
dependabot[bot]
fde0a84bba build(deps): bump tokio from 1.10.1 to 1.11.0 (#723)
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.10.1 to 1.11.0.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.10.1...tokio-1.11.0)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-07 09:15:31 +09:00
dependabot[bot]
a5c9ebdf36 build(deps): bump signal-hook from 0.3.9 to 0.3.10 (#722)
Bumps [signal-hook](https://github.com/vorner/signal-hook) from 0.3.9 to 0.3.10.
- [Release notes](https://github.com/vorner/signal-hook/releases)
- [Changelog](https://github.com/vorner/signal-hook/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/signal-hook/compare/v0.3.9...v0.3.10)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-07 09:15:26 +09:00
dependabot[bot]
3fc4e9ff58 build(deps): bump cc from 1.0.69 to 1.0.70 (#721)
Bumps [cc](https://github.com/alexcrichton/cc-rs) from 1.0.69 to 1.0.70.
- [Release notes](https://github.com/alexcrichton/cc-rs/releases)
- [Commits](https://github.com/alexcrichton/cc-rs/compare/1.0.69...1.0.70)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-07 09:15:21 +09:00
dependabot[bot]
4320821fa4 build(deps): bump thiserror from 1.0.28 to 1.0.29 (#720)
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.28 to 1.0.29.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.28...1.0.29)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-09-07 09:15:11 +09:00
Blaž Hrastnik
4ac29434cb syntax: Add go & rust locals, improve tree-sitter error message 2021-09-06 18:13:52 +09:00
Blaž Hrastnik
2bef245b7a At least partly highlight tsx 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
d85a8adb27 Improve highlighting scopes 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
be81f40df8 lsp: This doesn't need to be a mutable reference 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
9b9c3c77f8 runtime: Query improvements 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
64099af3f1 Don't panic on save if language_server isn't initialized 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
ade1a453ef syntax: Improve go highlights 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
585e3ce830 fix: tree-sitter-scopes would infinitely loop 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
a6108baec9 Improve grammar definitions 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
37606bad47 lsp: doc.language_server() is None until initialize completes 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
46f3c69f06 lsp: Don't send notifications until initialize completes
Then send open events for all documents with the LSP attached.
2021-09-06 15:25:46 +09:00
Blaž Hrastnik
2793ff3832 lsp: SyncKind::Full: we need to send the whole document on each change 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
59ed1c8c78 Simplify documents & documents_mut() 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
dc7799b980 lsp: Refactor code that could use document_by_path_mut 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
63e191ea3b lsp: Simplify lookup under method call 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
48fd4843fc lsp: Outdated comment 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
10b690b5bd Drop some &mut bounds where & would have sufficed 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
800d79b584 ls: Refactor textDocument/didSave in a similar vein 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
184637c55a lsp: refactor format so we stop cloning the language_server 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
c00cf238af Simplify textDocument/didClose, we don't need to look up LSP again 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
8744f367bd wip 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
d2b9a5d654 lsp: Update the julia definition 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
905efe3a48 Improve build error when a new grammar was added 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
5a558e0d8e lsp: Delay requests & notifications until initialization is complete 2021-09-06 15:25:46 +09:00
Blaž Hrastnik
c3a58cdadd lsp: Refactor capabilities as an async OnceCell
First step in making LSP init asynchronous
2021-09-06 15:25:46 +09:00
Blaž Hrastnik
41f1e8e4fb fix: lsp: Terminate transport on EOF
If stdout/stderr is closed, read_line will return 0 indicating EOF.
2021-09-06 15:25:46 +09:00
Blaž Hrastnik
fe17b99ab3 fix: lsp: Don't consume \n\n as a single newline 2021-09-06 15:25:08 +09:00
Blaž Hrastnik
3cb95be452 Update tree-sitter to 0.20
0.20 includes querying improvements, we no longer have to convert
fragments to strings but can return an iterator of chunks instead.
2021-09-06 13:21:53 +09:00
Blaž Hrastnik
57ed5180e0 lsp: Improve line ending handling when generating TextEdit 2021-09-06 11:00:33 +09:00
Blaž Hrastnik
08967baef6 flake: Update dependencies 2021-09-06 10:59:29 +09:00
Gokul Soumya
6e21a748b8 Fix escape not exiting insert mode (#712)
Regression due to #635 where escape key in insert mode
would not exit normal mode. This happened due to hard
coding the escape key to cancel a sticky keymap node.
2021-09-05 21:20:11 +09:00
Gokul Soumya
183dcce992 Add a sticky mode for keymaps (#635) 2021-09-05 12:55:13 +09:00
oberblastmeister
99a753a579 Document macros (#693)
* add docs

* clean up

* remove

* more

* Update helix-view/src/macros.rs

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

Co-authored-by: Ivan Tham <pickfire@riseup.net>
2021-09-05 12:42:33 +09:00
Gokul Soumya
e4e93e176c fix: Merge default palette with user palette 2021-09-05 12:42:03 +09:00
Gokul Soumya
e40e6db227 feat: Default theme palette using 16 terminal colors 2021-09-05 12:42:03 +09:00
Gokul Soumya
95cd2c645b Refactor switch_case commands 2021-09-05 12:41:19 +09:00
Gokul Soumya
33ce8779fd Refactor {move,extend}_word_* commands 2021-09-05 12:41:19 +09:00
Gokul Soumya
ea2b4c687d Refactor {move,extend}_char_* commands 2021-09-05 12:41:19 +09:00
Kangwook Lee (이강욱)
07fe4a6a40 Add commands that extends to long words (#706) 2021-09-04 19:00:32 +05:30
Wojciech Kępka
7e1123680f Expand ~ in change-current-directory command (#692) 2021-09-02 11:03:42 +09:00
oberblastmeister
5766f5da8f OCaml support (#666)
* added some stuff

* add interface

* indent

* highlights and locals

* scope

* change some stuff

* add indents

* fix blanket highlight

* macro

* use inherits
2021-09-02 01:08:08 +09:00
oberblastmeister
825bceeab6 add_newline unimpaired mapping (#653)
* added some keymaps

* remove

* remove wrong mappings

* remove

* wrong import

* use enum

* correct line ending

* added to book

* column
2021-09-02 00:55:16 +09:00
oberblastmeister
ae3f936611 Lua support (#665)
* added submodule

* small changes

* updated some stuff

* remove

* shallow clone

* correct indent

* shallow

* ok

* highlights

* proper captures
2021-09-02 00:54:21 +09:00
Nathan Vegdahl
31f1455c71 Add a "vision" document, to help give people a sense of Helix's direction. (#657)
* Add a "vision" document, to help give people a sense of Helix's direction.

* Fix typo in vision document.

* Fix spelling errors in vision document.

Caught in PR review.  Thanks!
2021-09-02 00:18:56 +09:00
oberblastmeister
1586b0eec7 YAML support (#667)
* added submodule

* remove wrong one

* added highlights

* use property

* add indents

* shallow
2021-09-02 00:16:16 +09:00
Blaž Hrastnik
ce7ad2beb5 Reimplement keep-pipe, it needs to manipulate selections, not text 2021-09-01 11:09:50 +09:00
Blaž Hrastnik
dc609cafb5 Extract the shell command into a separate function 2021-09-01 10:46:35 +09:00
Blaž Hrastnik
a3bd80a6fa ui: prompt: Avoid allocating a prompt name if it's a static string 2021-08-31 18:29:24 +09:00
Blaž Hrastnik
9b96bb5ac8 Refactor a bit further 2021-08-31 18:24:24 +09:00
Blaž Hrastnik
4a76ea8f88 shell: Move changes outside so we can properly handle errors 2021-08-31 18:19:18 +09:00
Omnikar
e772808a5b Shell commands (#547)
* Implement shell interaction commands

* Use slice instead of iterator for shell invocation

* Default to `sh` instead of `$SHELL` for shell commands

* Enforce trailing comma in `commands` macro

* Use `|` register for shell commands

* Move shell config to `editor` and use in command

* Update shell command prompts

* Remove clone of shell config

* Change shell function names to match prompts

* Log stderr contents upon external command error

* Remove `unwrap` calls on potential common errors

`shell` will no longer panic if:
  * The user-configured shell cannot be found
  * The shell command does not output UTF-8

* Remove redundant `pipe` parameter

* Rename `ShellBehavior::None` to `Ignore`

* Display error when shell command is used and `shell = []`

* Document shell commands in `keymap.md`
2021-08-31 18:13:16 +09:00
dependabot[bot]
dbfd054562 build(deps): bump serde from 1.0.129 to 1.0.130
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.129 to 1.0.130.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.129...v1.0.130)

---
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-31 10:36:11 +09:00
dependabot[bot]
daff9f5fd2 build(deps): bump serde_json from 1.0.66 to 1.0.67
Bumps [serde_json](https://github.com/serde-rs/json) from 1.0.66 to 1.0.67.
- [Release notes](https://github.com/serde-rs/json/releases)
- [Commits](https://github.com/serde-rs/json/compare/v1.0.66...v1.0.67)

---
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-31 10:35:55 +09:00
dependabot[bot]
40223bbb45 build(deps): bump arc-swap from 1.3.1 to 1.3.2
Bumps [arc-swap](https://github.com/vorner/arc-swap) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/vorner/arc-swap/releases)
- [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/arc-swap/compare/v1.3.1...v1.3.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-31 10:13:50 +09:00
dependabot[bot]
3ce578c1a1 build(deps): bump slotmap from 1.0.5 to 1.0.6
Bumps [slotmap](https://github.com/orlp/slotmap) from 1.0.5 to 1.0.6.
- [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.5...v1.0.6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-31 10:02:17 +09:00
dependabot[bot]
9d83a4483d build(deps): bump tokio from 1.10.0 to 1.10.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.10.0 to 1.10.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.10.0...tokio-1.10.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-08-31 10:02:06 +09:00
dependabot[bot]
d1d6810560 build(deps): bump futures-executor from 0.3.16 to 0.3.17
Bumps [futures-executor](https://github.com/rust-lang/futures-rs) from 0.3.16 to 0.3.17.
- [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.16...0.3.17)

---
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-08-31 10:01:59 +09:00
dependabot[bot]
d7b2ac0381 build(deps): bump futures-util from 0.3.16 to 0.3.17
Bumps [futures-util](https://github.com/rust-lang/futures-rs) from 0.3.16 to 0.3.17.
- [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.16...0.3.17)

---
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-08-31 10:01:24 +09:00
dependabot[bot]
e57a00c19c build(deps): bump thiserror from 1.0.26 to 1.0.28
Bumps [thiserror](https://github.com/dtolnay/thiserror) from 1.0.26 to 1.0.28.
- [Release notes](https://github.com/dtolnay/thiserror/releases)
- [Commits](https://github.com/dtolnay/thiserror/compare/1.0.26...1.0.28)

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

Signed-off-by: dependabot[bot] <support@github.com>
2021-08-31 10:00:47 +09:00
gbaranski
9c5752cbac fix: use .cursor() instead of .head 2021-08-30 12:03:10 -07:00
gbaranski
b590504143 fix: use head instead of anchor for relative line 2021-08-30 12:03:10 -07:00
Sven-Hendrik Haase
4f8dc4cad8 Fix it's -> its (#676) 2021-08-30 17:58:22 +09:00
Omnikar
03ad9e0bfa Fix code indentation (#671) 2021-08-30 09:15:49 +09:00
Blaž Hrastnik
847d1fa496 fix: Work around crashes on LSPs that don't just emit JSON-RPC 2021-08-29 18:38:28 +09:00
Blaž Hrastnik
7eff905680 lsp: slightly refactor header parsing, add more logging 2021-08-29 12:40:21 +09:00
CossonLeo
d6a9c2c0f6 Add ui.menu text style (#664)
* add menu text style

* add ui.menu.text ui.info ui.info.text to book

* change ui.menu.text to ui.menu

* fix book's ui.menu
2021-08-28 13:54:24 +09:00
voroskoi
f22e0aa2ae Add zig tree-sitter support (#631)
* Add initial zig tree-sitter support

* zig/highlights.scm: remove unnecessary queries

* Add zig/indents.toml
2021-08-28 13:32:01 +09:00
Blaž Hrastnik
5cee3b634d ui: prompt: Fix typing with alt 2021-08-27 16:39:52 +09:00
Omnikar
46f537d4ce Fix missing backtick in keymap.md 2021-08-27 11:38:17 +05:30
Omnikar
048a390568 Add Command column to keymap documentation (#662) 2021-08-27 11:45:18 +09:00
Omnikar
bfce4d4f29 Make v in select mode switch back to normal mode (#660)
* Make `v` in select mode switch back to normal mode

* Move select mode toggle to keymap instead of command
2021-08-27 10:03:49 +09:00
Brian Shu
fa4caf7e3d remove unsafe 2021-08-27 09:50:57 +09:00
Grzegorz Baranski
cec5d437d8 fix: show current line number even if relative line is on (#656) 2021-08-26 23:18:33 +05:30
Stuart Hinson
6192f2fa25 Show hidden files in filename completer (#648)
also removes unnecessary clone
2021-08-27 00:30:47 +09:00
Yusuf Bera Ertan
dc57f8dc89 feat: merge default languages.toml with user provided languages.toml, add a generic TOML value merge function (#654)
* feat: merge default languages.toml with user provided languages.toml

* refactor: use catch-all to override all other values for merge toml

* tests: add a test case for merging languages configs

* refactor: change test module name
2021-08-27 00:29:14 +09:00
Ivan Tham
4bafda3995 Change vsp to vs (#647)
Follow up on #639 to match vim behavior
2021-08-27 00:20:37 +09:00
Blaž Hrastnik
68bf9fdf02 Fix tests broken by the State change 2021-08-26 09:26:38 +09:00
Blaž Hrastnik
28919898e9 fix: KeyEvent::char needs to ignore modifiers
Fixes #595
2021-08-26 09:21:41 +09:00
Blaž Hrastnik
9d4c301563 Reduce State use a bit further
This is a legacy type that should be fully removed.
2021-08-26 09:21:07 +09:00
Kirawi
44a0512d95 Add Monokai theme (#628)
* init

* update

* cleanup
2021-08-25 10:09:18 +09:00
Kirawi
b99db7c687 Move path util functions from helix-term to helix-core (#650) 2021-08-25 10:04:05 +09:00
Blaž Hrastnik
bf5b9a9f35 ui: Tone down the preview highlight by adding a new scope 2021-08-24 13:25:39 +09:00
Blaž Hrastnik
e6cb183134 ui: Fix preview window padding: we want horizontal, not vertical 2021-08-24 13:25:39 +09:00
Blaž Hrastnik
a5c3c6c6a9 ui: Highlight line ranges in the preview 2021-08-24 13:25:39 +09:00
CossonLeo
490125f008 info component style config use ui.info ui.info.text (#643) 2021-08-24 09:58:19 +09:00
Blaž Hrastnik
1d45f50781 fix: Don't internally use relative paths in the buffer picker
Fixes #619
2021-08-24 09:56:09 +09:00
devins2518
e1c9f13263 Add :vsplit and :hsplit commands (#639)
* add vsplit and hsplit commands

* handle splits more elegantly
2021-08-24 09:37:44 +09:00
dependabot[bot]
81984be9f4 Bump arc-swap from 1.3.0 to 1.3.1 (#646)
Bumps [arc-swap](https://github.com/vorner/arc-swap) from 1.3.0 to 1.3.1.
- [Release notes](https://github.com/vorner/arc-swap/releases)
- [Changelog](https://github.com/vorner/arc-swap/blob/master/CHANGELOG.md)
- [Commits](https://github.com/vorner/arc-swap/compare/v1.3.0...v1.3.1)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-24 09:18:26 +09:00
dependabot[bot]
39cc1d62cd Bump serde from 1.0.127 to 1.0.129 (#645)
Bumps [serde](https://github.com/serde-rs/serde) from 1.0.127 to 1.0.129.
- [Release notes](https://github.com/serde-rs/serde/releases)
- [Commits](https://github.com/serde-rs/serde/compare/v1.0.127...v1.0.129)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-24 09:18:10 +09:00
dependabot[bot]
864618242b Bump crossterm from 0.20.0 to 0.21.0 (#644)
Bumps [crossterm](https://github.com/crossterm-rs/crossterm) from 0.20.0 to 0.21.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/commits)

---
updated-dependencies:
- dependency-name: crossterm
  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>
2021-08-24 09:17:57 +09:00
Timothy DeHerrera
ed8c3e6574 don't panic on defunct lsp process (#583) 2021-08-23 18:04:22 +09:00
Blaž Hrastnik
6dd7dc4eb2 fix: xsel copy should not freeze the editor
If using --nodetach, xsel would end up continually running in the
foreground, so the command execution would never finish.

Fixes #630
2021-08-22 15:01:12 +09:00
Blaž Hrastnik
607b92b2e3 fix: Place the cursor on the start of the selected symbol
Fixes #626
2021-08-22 15:00:07 +09:00
Kirawi
59e0ceef8c better panic messages for when you're missing selection scopes (#608) 2021-08-22 11:15:33 +09:00
Gokul Soumya
f9375f449c Refactor new Rect construction (#575)
* Refactor new Rect construction

Introduces methods that can be chained to construct new Rects
out of pre-existing ones

* Clamp x and y to edges in Rect chop methods

* Rename Rect clipping functions
2021-08-21 14:21:20 +09:00
Yusuf Bera Ertan
ac8bc54108 fix: add missing optional keyword to protobuf syntax 2021-08-20 19:52:21 +09:00
Blaž Hrastnik
d4c17b633c minor: Extract doc.text().slice(..) into a var 2021-08-20 13:42:47 +09:00
Blaž Hrastnik
38e932bd4c minor: Nicer errors, std::io::Error provides a Display impl 2021-08-20 13:42:17 +09:00
Blaž Hrastnik
a76ec9a64e Make scrolling extend in extend mode 2021-08-20 13:42:01 +09:00
Blaž Hrastnik
07fea61d03 Use the correct search register 2021-08-20 11:14:57 +09:00
Blaž Hrastnik
f60b549fb7 cargo fmt 2021-08-20 11:02:28 +09:00
Blaž Hrastnik
68626b8f78 ui: Refactor styling a bit, ensure infobox is stylable 2021-08-20 10:58:44 +09:00
Blaž Hrastnik
cbd39d67a4 minor: Refactor commands.rs a bit more 2021-08-20 10:43:22 +09:00
Kirawi
da8810809a use ui.text.focus for the picker (fix #622) 2021-08-20 10:43:08 +09:00
Blaž Hrastnik
0595b0626a Fix clippy attr 2021-08-19 16:05:05 +09:00
Blaž Hrastnik
ab4e765ff3 Bump memchr 2.4.0 -> 2.4.1 2021-08-19 16:00:09 +09:00
Blaž Hrastnik
5f8b1c7320 Avoid looking up ui.text per highlight range 2021-08-19 15:59:08 +09:00
Blaž Hrastnik
557fd86e71 Extract view.inner_area(), simplify render_focused_view_elements 2021-08-19 15:59:03 +09:00
Blaž Hrastnik
9776553ad0 Refactor view.first_line/first_col into view.offset (Position) 2021-08-19 12:52:07 +09:00
Blaž Hrastnik
115754c5ee Simplify write/write_all commands, we no longer need to excessively block 2021-08-19 11:37:42 +09:00
Blaž Hrastnik
12ea3888c5 fix: ui: Pin popups with no positioning to the initial cursor position
This avoids the floating popup following the cursor as we type.
2021-08-19 11:25:19 +09:00
Blaž Hrastnik
466528c493 Golang indent improvements 2021-08-19 11:25:14 +09:00
Yusuf Bera Ertan
2f42b2338e feat: add indenting for protobuf 2021-08-19 09:54:14 +09:00
Yusuf Bera Ertan
4b45f27a13 feat: add protobuf tree-sitter parser with highlighting queries 2021-08-19 09:54:14 +09:00
Conscat
1158fc4487 Added more cpp filename extensions 2021-08-18 11:56:19 -07:00
Shafkath Shuhan
b63afbe74c fix warnings 2021-08-18 11:45:01 -07:00
oberblastmeister
098b6b6eed gruvbox theme changes (#594)
* changed some gruvbox highlights

* more stuff including cursors

* use property instead

* use variable.property
2021-08-19 01:02:15 +09:00
langbamit
36095326d0 Fix auto pairs return wrong selection (#613) 2021-08-19 00:59:53 +09:00
Kirawi
7560af1211 Update dark_plus.toml 2021-08-18 10:23:11 +08:00
Kirawi
16bf8e1e6b Document more of document.rs (#562) 2021-08-18 09:59:10 +09:00
Gokul Soumya
b59b248561 Add docs for registers, multi key remaps (#557) 2021-08-18 09:53:50 +09:00
Yerlan
fdd6530df7 Adding mjs to JavaScript file type (#607)
MJS is a file extension for JavaScript modules using standard ES2015+
2021-08-18 09:40:00 +09:00
Kirawi
ad462b4322 Update CHANGELOG.md (#606) 2021-08-18 09:39:52 +09:00
Leoi Hung Kin
89089a7355 Added "/utf-8" to Windows compilation options. (#603) 2021-08-17 20:58:29 +09:00
Yerlan
a2cd9cce9d Adding INO to C++ file type (#596)
INO is file extension for C++ files used in Arduino sketches.
Reference: https://www.arduino.cc/en/Guide/Environment
2021-08-17 10:45:29 +09:00
Yerlan
18c0509593 Exit select mode after toggle_comment. Fixes #597 (#598)
Consistent with yanking, exit select mode after toggling comment. Fixes #597
2021-08-17 09:52:52 +09:00
Orhun Parmaksız
9912bd7821 Compile the grammar libraries with full RELRO on Linux (#599)
* Compile the grammar libraries with full RELRO

* Set RELRO compiler options for only Linux
2021-08-17 09:52:25 +09:00
Gokul Soumya
14c08e855f Refactor infobox rendering and parsing (#579) 2021-08-17 09:25:48 +09:00
dependabot[bot]
27616153bc Bump bitflags from 1.3.1 to 1.3.2 (#600)
Bumps [bitflags](https://github.com/bitflags/bitflags) from 1.3.1 to 1.3.2.
- [Release notes](https://github.com/bitflags/bitflags/releases)
- [Changelog](https://github.com/bitflags/bitflags/blob/main/CHANGELOG.md)
- [Commits](https://github.com/bitflags/bitflags/compare/1.3.1...1.3.2)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-17 09:23:59 +09:00
dependabot[bot]
ecf58af497 Bump anyhow from 1.0.42 to 1.0.43 (#601)
Bumps [anyhow](https://github.com/dtolnay/anyhow) from 1.0.42 to 1.0.43.
- [Release notes](https://github.com/dtolnay/anyhow/releases)
- [Commits](https://github.com/dtolnay/anyhow/compare/1.0.42...1.0.43)

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

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

Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2021-08-17 09:23:47 +09:00
superlou
4b5090a5f6 Update configuration.md for Windows
Added explicit paths for WIndows, Mac, and Linux based on [`choose_base_strategy`](https://docs.rs/etcetera/0.3.2/etcetera/base_strategy/fn.choose_base_strategy.html)
2021-08-16 19:11:56 +05:30
Cor Peters
ac3c1719c9 Fixes crash on empty rust file. (#592)
Fixes #591

Co-authored-by: Cor Peters <luctius@gmail.com>
2021-08-16 19:18:23 +09:00
Grzegorz Baranski
78923496a6 feat: relative numbers (#485)
* feat(helix-view): configuring line-number

* feat(helix-term): relative line numbers

* feat(helix-term): passing editor::Config to render

* fix(helix-view): remove LineNumber::None

* feat(helix-term): rendering line-number according to configuration

* fix(term): put calculating current line above line iteration

* fix: add abs_diff function

* deps: cargo update

* fix: pass config argument
2021-08-16 11:11:53 +09:00
Ivan Tham
aaccc9419a Add ledger tree-sitter (#572)
Might need to update later since the current one highlight does not
work very well yet.
2021-08-16 11:03:57 +09:00
Gokul Soumya
3bde65c599 Fix change case commands in changelog (#586) 2021-08-16 11:01:51 +09:00
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
Blaž Hrastnik
59f94d13b8 Disable haskell grammar until build issues are resolved 2021-06-07 10:17:25 +09:00
Blaž Hrastnik
b3eeac7bbf Disable aarch64-macos, it fails to build on macos-latest 2021-06-07 09:50:15 +09:00
Blaž Hrastnik
f48a60b8e2 Release 0.0.10 2021-06-07 09:42:15 +09:00
Blaž Hrastnik
4f561e93b8 View mode: Use saturating_sub when calculating first_col 2021-06-07 09:29:21 +09:00
Blaž Hrastnik
01b1bd15a1 commands: use chars().count() over .len() on strings 2021-06-07 09:26:49 +09:00
Blaž Hrastnik
ff8a031cb2 Add diagnostics keys to keymap.md 2021-06-07 09:24:23 +09:00
Blaž Hrastnik
d9b2f6feac Only test on stable rust
Shorter CI times, and it should be good enough.
2021-06-07 09:20:36 +09:00
Blaž Hrastnik
582f1ee9d8 Add aarch64-macos (M1) to the release build matrix 2021-06-07 09:19:51 +09:00
ahkrr
e2d780f993 fix: 2 panics while setting style + off by 1
The panics would occur because set_style 
would draw outside of the the surface. 
Both occured using `find_prev` or `till_prev`
In my case the first panic! would appear
in a terminal with around 80 columns 
in helix/README.md going to the end of the file
with `geglf(`
the second with `geglfX`
The off by one fix ensures that `find_nth_prev` 
starts at the first character to the left
2021-06-07 09:15:08 +09:00
Ethan Bodzioney
843c2cdebd Install instructions and version number corrections (#148)
* Add MacOS install instructions

* Change version name argument

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

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number

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

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

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

* docs: theme documentation

* fixup: parse modifiers with filter_map

* theme: tests for parse_style

* theme: Log invalid cases in theme.toml parse

* docs: theme documentation fixup

* docs: Blaz's theming comments

* docs: Theme doc fixes from pickfire

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

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

* contrib: Ingrid's theme

* docs: Theme subsection fixes

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

* wip

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

* implement extend methods for extend_line_start, extend_line_end

* add home-end mappings to keymaps.md

* add ^-$ extend mappings for extend mode

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

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

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

Fix #89
2021-06-04 01:27:09 +09:00
Antoni Stevent
3071339cbc update keymap.md to include arrow keys for movement 2021-06-03 23:24:24 +09:00
Antoni Stevent
27aee705e0 use correct _extend methods, also remove unnecessary casts 2021-06-03 23:24:24 +09:00
Antoni Stevent
f0fe558f38 Add up/right/left/down arrow keymaps, similar to kakoune 2021-06-03 23:24:24 +09:00
Jakub Bartodziej
09a7db637e Avoid theoretical underflow. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
31ed4db153 Clean up leftover log. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
3c5dfb0633 Improve on the fix for deleting from the end of the buffer. 2021-06-03 23:23:23 +09:00
Jakub Bartodziej
6cbc0aea92 Disable deleting from an empty buffer which can cause a crash. 2021-06-03 23:23:23 +09:00
Jan Hrastnik
c1c3750d38 key is now modified in place at start of handle_event 2021-06-03 23:16:04 +09:00
Jan Hrastnik
daad8ebe12 key_canonicalization now only matches chars 2021-06-03 23:16:04 +09:00
Jan Hrastnik
68abc67ec6 put the key canonicalization in a seperate function. only chars now get stripped of Shift modifier 2021-06-03 23:16:04 +09:00
Jan Hrastnik
712f25c2b9 removed shift matching 2021-06-03 23:16:04 +09:00
Blaž Hrastnik
abe8a83d8e Merge pull request #92 from bfredl/clangd
LSP: add clangd as server for c/c++
2021-06-03 22:23:20 +09:00
Blaž Hrastnik
a05fb95769 Merge pull request #80 from notoria/highlight
Highlight matching brackets
2021-06-03 22:14:37 +09:00
Blaž Hrastnik
74e4ac8d49 Merge pull request #77 from notoria/match_brackets
Fix match_brackets::find
2021-06-03 22:13:48 +09:00
Björn Linse
0e6f007028 LSP: add clangd as server for c/c++ 2021-06-03 15:07:50 +02:00
notoria
c3a98b6a3e Highlight matching brackets 2021-06-03 11:40:46 +02:00
notoria
4fe654cf9a Fix match_brackets::find 2021-06-03 10:35:17 +02:00
Blaž Hrastnik
661dbdca57 Fix cursor not showing on (0, 0) 2021-06-03 13:34:00 +09:00
Blaž Hrastnik
5773bd6a40 Merge pull request #64 from pickfire/log
Default log file to cache
2021-06-03 12:58:31 +09:00
Ivan Tham
d664d1dec0 Default log file to cache 2021-06-03 10:15:17 +08:00
Blaž Hrastnik
7e8603247d Merge pull request #66 from IceDragon200/replaced-args-parser
Drop pico-args in favour of a hand rolled parser
2021-06-03 10:32:42 +09:00
Blaž Hrastnik
7140908f6e Nix: add lldb to shell 2021-06-03 10:31:33 +09:00
Blaž Hrastnik
6dba1e7ec7 Clippy lint 2021-06-03 10:31:14 +09:00
Blaž Hrastnik
c0332bd935 Fix split sizes getting out of sync with the terminal size, refs #69 2021-06-03 10:28:49 +09:00
Blaž Hrastnik
3c7729906c Merge pull request #70 from RLHerbert/master
Fix panic when buffer larger than terminal width
2021-06-03 10:28:14 +09:00
Rowan Herbert
1b67fae9f4 Fix panic when buffer larger than terminal width 2021-06-02 16:30:40 -07:00
Corey Powell
f0018280cb Refactored parse_args loop
Thanks @PabloMansanet
2021-06-02 14:26:20 -05:00
Corey Powell
7202953e69 Dropped pico-args in favour of a simpler hand roller parser
Not the greatest looking, but it gets the job done
2021-06-02 14:26:13 -05:00
Corey Powell
7761c88d61 Merge pull request #62 from pickfire/cell
Separate document history into Cell
2021-06-02 13:27:35 -05:00
Corey Powell
68f5031dcc Merge pull request #49 from eleijonmarck/patch-1
Update README.md to include shortcuts
2021-06-02 13:15:32 -05:00
Corey Powell
83031564db Merge pull request #57 from pickfire/fix-panic
Fix panic opening rust file
2021-06-02 13:14:19 -05:00
Ivan Tham
eab6e53511 Fix panic opening rust file
Application::new will use stuff that requires tokio runtime.
2021-06-02 23:49:26 +08:00
Ivan Tham
f5f46b1fed Separate document history into Cell
As history is used separately from the rest of the edits, separating it
can avoid needless borrowing and cloning. But one need to be aware later.
2021-06-02 23:47:50 +08:00
Eric Leijonmarck
5f49bafbe8 Update README.md 2021-06-02 17:05:15 +02:00
Blaž Hrastnik
2719a35123 Merge pull request #55 from helix-editor/autoresize
autoresize terminal in compositor render
2021-06-02 22:45:43 +09:00
Blaž Hrastnik
0a6672c626 Merge pull request #50 from wojciechkepka/config
Use config_dir for logging, create config_dir
2021-06-02 22:43:28 +09:00
Blaž Hrastnik
b51111a364 Merge pull request #21 from IceDragon200/elixir-syntax
Added elixir syntax
2021-06-02 22:41:51 +09:00
Jan Hrastnik
78980f575b autoresize terminal in compositor render 2021-06-02 15:40:08 +02:00
Corey Powell
0bb375bafa Added missing tree-sitter-elixir submodule 2021-06-02 06:43:22 -05:00
Eric Leijonmarck
c960bcfc24 Update README.md 2021-06-02 13:15:31 +02:00
Wojciech Kępka
e88383d990 Use config_dir for logging, create config_dir 2021-06-02 12:25:25 +02:00
Eric Leijonmarck
312b29f712 Update README.md 2021-06-02 12:05:39 +02:00
Blaž Hrastnik
f4560cb68a Better fix for w/e that also covers ia<esc>we/ia<esc>wb 2021-06-02 14:57:43 +09:00
Corey Powell
ca042a4bde Added elixir syntax
Using custom fork for now to get around generating the source files
2021-06-01 21:59:16 -05:00
222 changed files with 388649 additions and 5579 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

@@ -1,11 +1,11 @@
name: Build name: Build
on: on:
pull_request:
push: push:
branches: branches:
- master - master
pull_request: schedule:
branches: - cron: '00 01 * * *'
- master
jobs: jobs:
check: check:
@@ -28,19 +28,19 @@ jobs:
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/git path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: target path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo check - name: Run cargo check
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@@ -49,7 +49,7 @@ jobs:
test: test:
name: Test Suite name: Test Suite
runs-on: ubuntu-latest runs-on: ${{ matrix.os }}
steps: steps:
- name: Checkout sources - name: Checkout sources
uses: actions/checkout@v2 uses: actions/checkout@v2
@@ -60,32 +60,37 @@ jobs:
uses: actions-rs/toolchain@v1 uses: actions-rs/toolchain@v1
with: with:
profile: minimal profile: minimal
toolchain: stable toolchain: ${{ matrix.rust }}
override: true override: true
- name: Cache cargo registry - name: Cache cargo registry
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/git path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: target path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo test - name: Run cargo test
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
with: with:
command: test command: test
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
lints: lints:
name: Lints name: Lints
runs-on: ubuntu-latest runs-on: ubuntu-latest
@@ -107,19 +112,19 @@ jobs:
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/registry path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/git path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: target path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo fmt - name: Run cargo fmt
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1

View File

@@ -37,6 +37,10 @@ jobs:
rust: stable rust: stable
target: x86_64-pc-windows-msvc target: x86_64-pc-windows-msvc
cross: false cross: false
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
# - build: x86_64-win-gnu # - build: x86_64-win-gnu
# os: windows-2019 # os: windows-2019
# rust: stable-x86_64-gnu # rust: stable-x86_64-gnu
@@ -63,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
@@ -95,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
@@ -144,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

52
.gitmodules vendored
View File

@@ -82,3 +82,55 @@
path = helix-syntax/languages/tree-sitter-toml path = helix-syntax/languages/tree-sitter-toml
url = https://github.com/ikatyang/tree-sitter-toml url = https://github.com/ikatyang/tree-sitter-toml
shallow = true shallow = true
[submodule "helix-syntax/languages/tree-sitter-elixir"]
path = helix-syntax/languages/tree-sitter-elixir
url = https://github.com/elixir-lang/tree-sitter-elixir
shallow = true
[submodule "helix-syntax/languages/tree-sitter-nix"]
path = helix-syntax/languages/tree-sitter-nix
url = https://github.com/cstrahan/tree-sitter-nix
shallow = true
[submodule "helix-syntax/languages/tree-sitter-latex"]
path = helix-syntax/languages/tree-sitter-latex
url = https://github.com/latex-lsp/tree-sitter-latex
shallow = true
[submodule "helix-syntax/languages/tree-sitter-ledger"]
path = helix-syntax/languages/tree-sitter-ledger
url = https://github.com/cbarrete/tree-sitter-ledger
shallow = true
[submodule "helix-syntax/languages/tree-sitter-protobuf"]
path = helix-syntax/languages/tree-sitter-protobuf
url = https://github.com/yusdacra/tree-sitter-protobuf.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-ocaml"]
path = helix-syntax/languages/tree-sitter-ocaml
url = https://github.com/tree-sitter/tree-sitter-ocaml
shallow = true
[submodule "helix-syntax/languages/tree-sitter-lua"]
path = helix-syntax/languages/tree-sitter-lua
url = https://github.com/nvim-treesitter/tree-sitter-lua
shallow = true
[submodule "helix-syntax/languages/tree-sitter-yaml"]
path = helix-syntax/languages/tree-sitter-yaml
url = https://github.com/ikatyang/tree-sitter-yaml
shallow = true
[submodule "helix-syntax/languages/tree-sitter-zig"]
path = helix-syntax/languages/tree-sitter-zig
url = https://github.com/maxxnino/tree-sitter-zig
shallow = true
[submodule "helix-syntax/languages/tree-sitter-svelte"]
path = helix-syntax/languages/tree-sitter-svelte
url = https://github.com/Himujjal/tree-sitter-svelte
shallow = true
[submodule "helix-syntax/languages/tree-sitter-vue"]
path = helix-syntax/languages/tree-sitter-vue
url = https://github.com/ikatyang/tree-sitter-vue
shallow = true
[submodule "helix-syntax/languages/tree-sitter-tsq"]
path = helix-syntax/languages/tree-sitter-tsq
url = https://github.com/tree-sitter/tree-sitter-tsq
shallow = true
[submodule "helix-syntax/languages/tree-sitter-cmake"]
path = helix-syntax/languages/tree-sitter-cmake
url = https://github.com/uyha/tree-sitter-cmake
shallow = true

217
CHANGELOG.md Normal file
View File

@@ -0,0 +1,217 @@
# 0.5.0 (2021-11-28)
A big shout out to all the contributors! We had 46 contributors in this release.
Helix has popped up in [Scoop, FreeBSD Ports and Gentu GURU](https://repology.org/project/helix/versions)!
The following is a quick rundown of the larger changes, there were many more
(check the git history for more details).
Breaking changes:
- A couple of keymaps moved to resolve a few conflicting keybinds.
- Documentation popups were moved from `K` to `space+k`
- `K` is now `keep_selections` which filters selections to only keeps ones matching the regex
- `keep_primary_selection` moved from `space+space` to `,`
- `Alt-,` is now `remove_primary_selection` which keeps all selections except the primary one
- Opening files in a split moved from `C-h` to `C-s`
- Some configuration options moved from a `[terminal]` section to `[editor]`. [Consult the documentation for more information.](https://docs.helix-editor.com/configuration.html).
Features:
- LSP compatibility greatly improved for some implementations (Julia, Python, Typescript)
- Autocompletion! Completion now triggers automatically after a set idle timeout
- Completion documentation is now displayed next to the popup (#691)
- Treesitter textobjects (select a function via `mf`, class via `mc`) (#728)
- Global search across entire workspace `space+/` (#651)
- Relative line number support (#485)
- Prompts now store a history (72cf86e)
- `:vsplit` and `:hsplit` commands (#639)
- `C-w h/j/k/l` can now be used to navigate between splits (#860)
- `C-j` and `C-k` are now alternative keybindings to `C-n` and `C-p` in the UI (#876)
- Shell commands (shell-pipe, pipe-to, shell-insert-output, shell-append-output, keep-pipe) (#547)
- Searching now defaults to smart case search (case insensitive unless uppercase is used) (#761)
- The preview pane was improved to highlight and center line ranges
- The user `languages.toml` is now merged into defaults, no longer need to copy the entire file (dc57f8dc)
- Show hidden files in completions (#648)
- Grammar injections are now properly handled (dd0b15e)
- `v` in select mode now switches back to normal mode (#660)
- View mode can now be triggered as a "sticky" mode (#719)
- `f`/`t` and object selection motions can now be repeated via `Alt-.` (#891)
- Statusline now displays total selection count and diagnostics counts for both errors and warnings (#916)
New grammars:
- Ledger (#572)
- Protobuf (#614)
- Zig (#631)
- YAML (#667)
- Lua (#665)
- OCaml (#666)
- Svelte (#733)
- Vue (#787)
- Tree-sitter queries (#845)
- Elixir (we switched over to the official grammar) (6c0786e)
- Language server definitions for Nix and Elixir (#725)
- Python now uses `pylsp` instead of `pyls`
- Python now supports indentation
New themes:
- Monokai (#628)
- Everforest Dark (#760)
- Nord (#799)
- Base16 Default Dark (#833)
- Rose Pine (#897)
Fixes:
- Fix crash on empty rust file (#592)
- Exit select mode after toggle comment (#598)
- Pin popups with no positioning to the initial position (12ea3888)
- xsel copy should not freeze the editor (6dd7dc4)
- `*` now only sets the search register and doesn't jump to the next occurrence (3426285)
- Goto line start/end commands extend when in select mode (#739)
- Fix documentation popups sometimes not getting fully highlighted (066367c)
- Refactor apply_workspace_edit to remove assert (b02d872)
- Wrap around the top of the picker menu when scrolling (c7d6e44)
- Don't allow closing the last split if there's unsaved changes (3ff5b00)
- Indentation used different default on hx vs hx new_file.txt (c913bad)
# 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.0 (2021-08-13)
A big shout out to all the contributors! We had 28 contributors in this release.
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)
A big shout out to all the contributors! We had 24 contributors in this release.
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
A big shout out to all the contributors! We had 18 contributors in this release.
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

515
Cargo.lock generated
View File

@@ -13,9 +13,15 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.40" version = "1.0.44"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b" checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
[[package]]
name = "arc-swap"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@@ -25,24 +31,32 @@ checksum = "cdb031dd78e28731d87d56cc8ffef4a8f36ca26c38fe2de700543e627f8a464a"
[[package]] [[package]]
name = "bitflags" name = "bitflags"
version = "1.2.1" version = "1.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf1de2fe8c75bc145a2f577add951f8134889b4795d47466a54a5c846d691693" checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "0.2.16" version = "0.2.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "90682c8d613ad3373e66de8c6411e0ae2ab2571e879d2efbf73558cc66f21279" checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223"
dependencies = [ dependencies = [
"lazy_static",
"memchr", "memchr",
"regex-automata",
] ]
[[package]] [[package]]
name = "bytes" name = "bytecount"
version = "1.0.1" version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b700ce4376041dcd0a327fd0097c41095743c4c8af8887265942faf1100bd040" checksum = "72feb31ffc86498dacdbd0fcebb56138e7177a8cc5cea4516031d15ae85a742e"
[[package]]
name = "bytes"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c4872d67bab6358e59559027aa3b9157c53d9358c51423c17554809a8858e0f8"
[[package]] [[package]]
name = "cassowary" name = "cassowary"
@@ -52,12 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.68" version = "1.0.71"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787" checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
dependencies = [
"jobserver",
]
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@@ -65,6 +76,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.15"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e"
dependencies = [
"cfg-if",
"encoding_rs",
"memchr",
]
[[package]] [[package]]
name = "chrono" name = "chrono"
version = "0.4.19" version = "0.4.19"
@@ -78,38 +100,48 @@ dependencies = [
] ]
[[package]] [[package]]
name = "crossbeam-utils" name = "clipboard-win"
version = "0.8.4" version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278" checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed"
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.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c" checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
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.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9" checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
@@ -135,6 +167,40 @@ 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.29"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
dependencies = [
"cfg-if",
]
[[package]]
name = "encoding_rs_io"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1cc3c5651fb62ab8aa3103998dade57efdd028544bd300516baa31840c252a83"
dependencies = [
"encoding_rs",
]
[[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 +249,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-core" name = "futures-core"
version = "0.3.15" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0402f765d8a89a26043b889b26ce3c4679d268fa6bb22cd7c6aad98340e179d1" checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
[[package]] [[package]]
name = "futures-executor" name = "futures-executor"
version = "0.3.15" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "badaa6a909fac9e7236d0620a2f57f7664640c56575b71a7552fbd68deafab79" checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"futures-task", "futures-task",
@@ -200,15 +266,15 @@ dependencies = [
[[package]] [[package]]
name = "futures-task" name = "futures-task"
version = "0.3.15" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a16bef9fc1a4dddb5bee51c989e3fbba26569cbb0e31f5b303c184e3dd33dae" checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
[[package]] [[package]]
name = "futures-util" name = "futures-util"
version = "0.3.15" version = "0.3.17"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "feb5c238d27e2bf94ffdfd27b2c29e3df4a68c4193bb6427384259e2bf191967" checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"futures-core", "futures-core",
@@ -238,17 +304,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",
@@ -258,66 +318,109 @@ dependencies = [
] ]
[[package]] [[package]]
name = "helix-core" name = "grep-matcher"
version = "0.1.0" version = "0.1.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc"
dependencies = [ dependencies = [
"memchr",
]
[[package]]
name = "grep-regex"
version = "0.1.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121553c9768c363839b92fc2d7cdbbad44a3b70e8d6e7b1b72b05c977527bd06"
dependencies = [
"aho-corasick",
"bstr",
"grep-matcher",
"log",
"regex",
"regex-syntax",
"thread_local",
]
[[package]]
name = "grep-searcher"
version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7fbdbde90ba52adc240d2deef7b6ad1f99f53142d074b771fe9b7bede6c4c23d"
dependencies = [
"bstr",
"bytecount",
"encoding_rs",
"encoding_rs_io",
"grep-matcher",
"log",
"memmap2",
]
[[package]]
name = "helix-core"
version = "0.5.0"
dependencies = [
"arc-swap",
"etcetera", "etcetera",
"helix-syntax", "helix-syntax",
"log",
"once_cell", "once_cell",
"quickcheck",
"regex", "regex",
"ropey", "ropey",
"serde", "serde",
"serde_json",
"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.1.0" version = "0.5.0"
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.1.0" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow",
"cc", "cc",
"serde", "libloading",
"threadpool", "threadpool",
"tree-sitter", "tree-sitter",
] ]
[[package]] [[package]]
name = "helix-term" name = "helix-term"
version = "0.1.0" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"crossterm", "crossterm",
"dirs-next",
"fern", "fern",
"futures-util", "futures-util",
"fuzzy-matcher", "fuzzy-matcher",
"grep-regex",
"grep-searcher",
"helix-core", "helix-core",
"helix-lsp", "helix-lsp",
"helix-tui", "helix-tui",
@@ -326,48 +429,58 @@ dependencies = [
"log", "log",
"num_cpus", "num_cpus",
"once_cell", "once_cell",
"pico-args",
"pulldown-cmark", "pulldown-cmark",
"serde", "serde",
"serde_json", "serde_json",
"signal-hook",
"signal-hook-tokio",
"tokio", "tokio",
"tokio-stream",
"toml", "toml",
] ]
[[package]] [[package]]
name = "helix-tui" name = "helix-tui"
version = "0.1.0" version = "0.5.0"
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.1.0" version = "0.5.0"
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",
"log",
"once_cell", "once_cell",
"serde", "serde",
"slotmap", "slotmap",
"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",
] ]
@@ -385,9 +498,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",
@@ -403,32 +516,24 @@ dependencies = [
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.9" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61124eeebbd69b8190558df225adf7e4caafce0d743919e5d6b19652314ec5ec" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
[[package]] [[package]]
name = "itoa" name = "itoa"
version = "0.4.7" 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 = "dd25036021b0de88a0aff6b850051563c6516d0bf53f8638938edbb9de732736" checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4"
[[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",
@@ -445,15 +550,25 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.95" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36" checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
[[package]]
name = "libloading"
version = "0.7.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0"
dependencies = [
"cfg-if",
"winapi",
]
[[package]] [[package]]
name = "lock_api" name = "lock_api"
version = "0.4.4" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0382880606dff6d15c9476c416d18690b72742aa7b605bb6dd6ec9030fbf07eb" checksum = "712a4d093c9976e24e7dbca41db895dabcbac38eb5f4045393d17a95bdfb1109"
dependencies = [ dependencies = [
"scopeguard", "scopeguard",
] ]
@@ -469,9 +584,9 @@ dependencies = [
[[package]] [[package]]
name = "lsp-types" name = "lsp-types"
version = "0.89.1" version = "0.90.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48b8a871b0a450bcec0e26d74a59583c8173cb9fb7d7f98889e18abb84838e0f" checksum = "6f3734ab1d7d157fc0c45110e06b587c31cd82bea2ccfd6b563cbff0aaeeb1d3"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"serde", "serde",
@@ -488,21 +603,30 @@ 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"
version = "2.4.0" version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b16bd47d9e329435e309c58469fe0791c2d0d1ba96ec0954152a5ae2b04387dc" checksum = "308cc39be01b73d0d18f82a0e7b2a3df85245f84af96fdddc5d202d27e47b86a"
[[package]]
name = "memmap2"
version = "0.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "00b6c2ebff6180198788f5db08d7ce3bc1d0b617176678831a7510825973e357"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.7.11" version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cf80d3e903b34e0bd7282b218398aec54e082c840d9baf8339e0080a0c542956" checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@@ -566,15 +690,15 @@ 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"
version = "0.11.1" version = "0.11.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d7744ac029df22dca6284efe4e898991d28e3085c706c972bcd7da4a27a15eb" checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99"
dependencies = [ dependencies = [
"instant", "instant",
"lock_api", "lock_api",
@@ -583,9 +707,9 @@ dependencies = [
[[package]] [[package]]
name = "parking_lot_core" name = "parking_lot_core"
version = "0.8.3" version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa7a782938e745763fe6907fc6ba86946d72f49fe7e21de074e08128a99fb018" checksum = "d76e8e1493bcac0d2766c42737f34458f1c8c50c0d23bcb24ea953affb273216"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"instant", "instant",
@@ -595,29 +719,17 @@ 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"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e" checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pico-args"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7afeb98c5a10e0bffcc7fc16e105b04d06729fac5fd6384aebf7ff5cb5a67d"
[[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"
@@ -627,9 +739,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.27" version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038" checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@@ -646,19 +758,46 @@ dependencies = [
] ]
[[package]] [[package]]
name = "quote" name = "quickcheck"
version = "1.0.9" version = "1.0.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" checksum = "588f6378e4dd99458b60ec275b4477add41ce4fa9f64dcba6f15adccb19b50d6"
dependencies = [
"rand",
]
[[package]]
name = "quote"
version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
[[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",
] ]
@@ -684,6 +823,12 @@ dependencies = [
"regex-syntax", "regex-syntax",
] ]
[[package]]
name = "regex-automata"
version = "0.1.10"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6c230d73fb8d8c1b9c0b3135c5142a8acee3a0558fb8db5cf1cb65f8d7862132"
[[package]] [[package]]
name = "regex-syntax" name = "regex-syntax"
version = "0.6.25" version = "0.6.25"
@@ -692,9 +837,9 @@ 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",
] ]
@@ -722,18 +867,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]] [[package]]
name = "serde" name = "serde"
version = "1.0.126" version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03" checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913"
dependencies = [ dependencies = [
"serde_derive", "serde_derive",
] ]
[[package]] [[package]]
name = "serde_derive" name = "serde_derive"
version = "1.0.126" version = "1.0.130"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43" checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -742,9 +887,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.64" version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "799e97dc9fdae36a5c8b8f2cae9ce2ee9fdce2058c57a93e6099d919fd982f79" checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@@ -764,50 +909,84 @@ dependencies = [
[[package]] [[package]]
name = "signal-hook" name = "signal-hook"
version = "0.1.17" version = "0.3.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729" checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1"
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 = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
[[package]]
name = "slab"
version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]] [[package]]
name = "slotmap" name = "slotmap"
version = "1.0.3" version = "1.0.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "585cd5dffe4e9e06f6dfdf66708b70aca3f781bed561f4f667b2d9c0d4559e36" checksum = "e1e08e261d0e8f5c43123b7adf3e4ca1690d655377ac93a03b2c9d3e98de1342"
dependencies = [ dependencies = [
"version_check", "version_check",
] ]
[[package]] [[package]]
name = "smallvec" name = "smallvec"
version = "1.6.1" version = "1.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe0f37c9e8f3c5a4a66ad655a93c74daac4ad00c441533bf5c6e7990bb42604e" checksum = "1ecab6c735a6bb4139c0caafd0cc3635748bbb3acf4550e8138122099251f309"
[[package]]
name = "str-buf"
version = "1.0.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.72" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1e8cdbefb79a9a5a65e0db8b47b723ee907b7c7f8496c76a1770b5c310bab82" checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -827,18 +1006,18 @@ dependencies = [
[[package]] [[package]]
name = "thiserror" name = "thiserror"
version = "1.0.25" version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6" checksum = "854babe52e4df1653706b98fcfc05843010039b406875930a70e4d9644e5c417"
dependencies = [ dependencies = [
"thiserror-impl", "thiserror-impl",
] ]
[[package]] [[package]]
name = "thiserror-impl" name = "thiserror-impl"
version = "1.0.25" version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d" checksum = "aa32fd3f627f367fe16f893e2597ae3c05020f8bba2666a4e6ea73d377e5714b"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -865,9 +1044,9 @@ dependencies = [
[[package]] [[package]]
name = "tinyvec" name = "tinyvec"
version = "1.2.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5b5220f05bb7de7f3f53c7c065e1199b3172696fe2db9f9c4d8ad9b4ee74c342" checksum = "f83b2a3d4d9091d0abd7eba4dc2710b1718583bd4d8992e2190720ea38f391f7"
dependencies = [ dependencies = [
"tinyvec_macros", "tinyvec_macros",
] ]
@@ -880,9 +1059,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.6.1" version = "1.12.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975" checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@@ -900,9 +1079,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.2.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37" checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@@ -911,9 +1090,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",
@@ -931,9 +1110,9 @@ dependencies = [
[[package]] [[package]]
name = "tree-sitter" name = "tree-sitter"
version = "0.19.5" 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 = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0" checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9"
dependencies = [ dependencies = [
"cc", "cc",
"regex", "regex",
@@ -950,33 +1129,36 @@ dependencies = [
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.5" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "eeb8be209bb1c96b7c177c7420d26e04eccacb0eeae6b980e35fcb74678107e0" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
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"
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 = "9337591893a19b88d8d87f2cec1e73fad5cdfd10e5a6f349f498ad6ea2ffb1e3" checksum = "3ed742d4ea2bd1176e236172c8429aaf54486e7ac098db29ffe6529e0ce50973"
[[package]] [[package]]
name = "unicode-xid" name = "unicode-xid"
@@ -1026,6 +1208,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,6 +13,10 @@ 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).
[Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting)
# Features # Features
- Vim-like modal editing - Vim-like modal editing
@@ -25,7 +29,8 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
# Installation # Installation
Note: Only Rust and Golang have indentation definitions at the moment. Note: Only certain languages have indentation definitions at the moment. Check
`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.
@@ -38,17 +43,25 @@ 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 its 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.
## Arch Linux > NOTE: running via cargo also doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
There are two packages available from AUR: > detect the `runtime` directory in the project root.
- `helix-bin`: contains prebuilt binary from GitHub releases
- `helix-git`: builds the master branch of this repository [![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## MacOS
Helix can be installed on MacOS through homebrew via:
```
brew tap helix-editor/helix
brew install helix
```
# Contributing # Contributing
@@ -56,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.
@@ -67,5 +83,6 @@ a good overview of the internals.
# Getting help # Getting help
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-community:matrix.org). Your question might already be answered on the [FAQ](https://github.com/helix-editor/helix/wiki/FAQ).
Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet).

36
TODO.md
View File

@@ -1,61 +1,27 @@
- Refactor tree-sitter-highlight to work like the atom one, recomputing partial tree updates.
------
as you type completion!
- tree sitter: - tree sitter:
- lua
- markdown - markdown
- zig
- regex - regex
- vue
- kotlin - kotlin
- julia
- clojure - clojure
- erlang - erlang
- [ ] use signature_help_provider and completion_provider trigger characters in
a hook to trigger signature help text / autocompletion
- [ ] document.on_type provider triggers
- [ ] completion isIncomplete support - [ ] 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
- [] 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
- [ ] 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

@@ -3,7 +3,9 @@ authors = ["Blaž Hrastnik"]
language = "en" language = "en"
multilingual = false multilingual = false
src = "src" src = "src"
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"
default-theme = "colibri"
preferred-dark-theme = "colibri"

View File

@@ -2,6 +2,11 @@
- [Installation](./install.md) - [Installation](./install.md)
- [Usage](./usage.md) - [Usage](./usage.md)
- [Migrating from Vim](./from-vim.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)
- [Guides](./guides/README.md)
- [Adding Textobject Queries](./guides/textobject.md)

View File

@@ -1 +1,32 @@
# Configuration # Configuration
To override global configuration parameters, create a `config.toml` file located in your config directory:
* Linux and Mac: `~/.config/helix/config.toml`
* Windows: `%AppData%\helix\config.toml`
## Editor
`[editor]` section of the config.
| Key | Description | Default |
|--|--|---------|
| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `3` |
| `mouse` | Enable mouse mode. | `true` |
| `middle-click-paste` | Middle click paste support. | `true` |
| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` |
| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`<br/>Windows: `["cmd", "/C"]` |
| `line-number` | Line number display (`absolute`, `relative`) | `absolute` |
| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` |
| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
## LSP
To display all language server messages in the status line add the following to your `config.toml`:
```toml
[lsp]
display-messages = true
```

12
book/src/from-vim.md Normal file
View File

@@ -0,0 +1,12 @@
# Migrating from Vim
Helix's editing model is strongly inspired from vim and kakoune, and a notable
difference from vim (and the most striking similarity to kakoune) is that Helix
follows the `selection → action` model. This means that the whatever you are
going to act on (a word, a paragraph, a line, etc) is selected first and the
action itself (delete, change, yank, etc) comes second. A cursor is simply a
single width selection.
See also Kakoune's [Migrating from Vim](https://github.com/mawww/kakoune/wiki/Migrating-from-Vim).
> TODO: Mention texobjects, surround, registers

View File

@@ -0,0 +1,4 @@
# Guides
This section contains guides for adding new language server configurations,
tree-sitter grammers, textobject queries, etc.

View File

@@ -0,0 +1,30 @@
# Adding Textobject Queries
Textobjects that are language specific ([like functions, classes, etc][textobjects])
require an accompanying tree-sitter grammar and a `textobjects.scm` query file
to work properly. Tree-sitter allows us to query the source code syntax tree
and capture specific parts of it. The queries are written in a lisp dialect.
More information on how to write queries can be found in the [official tree-sitter
documentation](tree-sitter-queries).
Query files should be placed in `runtime/queries/{language}/textobjects.scm`
when contributing. Note that to test the query files locally you should put
them under your local runtime directory (`~/.config/helix/runtime` on Linux
for example).
The following [captures][tree-sitter-captures] are recognized:
| Capture Name |
| --- |
| `function.inside` |
| `function.around` |
| `class.inside` |
| `class.around` |
| `parameter.inside` |
[Example query files][textobject-examples] can be found in the helix GitHub repository.
[textobjects]: ../usage.md#textobjects
[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=

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
@@ -18,7 +23,9 @@ shell for working on Helix.
### Arch Linux ### Arch Linux
Binary packages are available on AUR: Releases are available in the `community` repository.
Packages are also available on AUR:
- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release - [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch - [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
@@ -32,6 +39,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.

View File

@@ -4,138 +4,257 @@
### Movement ### Movement
| Key | Description | > NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
|-----|-----------|
| h | move left | | Key | Description | Command |
| j | move down | | ----- | ----------- | ------- |
| k | move up | | `h`/`Left` | Move left | `move_char_left` |
| l | move right | | `j`/`Down` | Move down | `move_line_down` |
| w | move next word start | | `k`/`Up` | Move up | `move_line_up` |
| b | move previous word start | | `l`/`Right` | Move right | `move_char_right` |
| e | move next word end | | `w` | Move next word start | `move_next_word_start` |
| t | find 'till next char | | `b` | Move previous word start | `move_prev_word_start` |
| f | find next char | | `e` | Move next word end | `move_next_word_end` |
| T | find 'till previous char | | `W` | Move next WORD start | `move_next_long_word_start` |
| F | find previous char | | `B` | Move previous WORD start | `move_prev_long_word_start` |
| ^ | move to the start of the line | | `E` | Move next WORD end | `move_next_long_word_end` |
| $ | move to the end of the line | | `t` | Find 'till next char | `find_till_char` |
| m | Jump to matching bracket | | `f` | Find next char | `find_next_char` |
| PageUp | Move page up | | `T` | Find 'till previous char | `till_prev_char` |
| PageDown | Move page down | | `F` | Find previous char | `find_prev_char` |
| ctrl-u | Move half page up | | `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
| ctrl-d | Move half page down | | `Home` | Move to the start of the line | `goto_line_start` |
| Tab | Switch to next view | | `End` | Move to the end of the line | `goto_line_end` |
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab | | `PageUp` | Move page up | `page_up` |
| ctrl-o | Jump backward on the jumplist | | `PageDown` | Move page down | `page_down` |
| v | Enter select (extend) mode | | `Ctrl-u` | Move half page up | `half_page_up` |
| g | Enter goto mode | | `Ctrl-d` | Move half page down | `half_page_down` |
| : | Enter command mode | | `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| z | Enter view mode | | `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| space | Enter space mode | | `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| K | Show documentation for the item under the cursor | | `g` | Enter [goto mode](#goto-mode) | N/A |
| `m` | Enter [match mode](#match-mode) | N/A |
| `:` | Enter command mode | `command_mode` |
| `z` | Enter [view mode](#view-mode) | N/A |
| `Z` | Enter sticky [view mode](#view-mode) | N/A |
| `Ctrl-w` | Enter [window mode](#window-mode) | N/A |
| `Space` | Enter [space mode](#space-mode) | N/A |
### Changes ### Changes
| Key | Description | | Key | Description | Command |
|-----|-----------| | ----- | ----------- | ------- |
| r | replace (single character change) | | `r` | Replace with a character | `replace` |
| i | Insert before selection | | `R` | Replace with yanked text | `replace_with_yanked` |
| a | Insert after selection (append) | | `~` | Switch case of the selected text | `switch_case` |
| I | Insert at the start of the line | | `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
| A | Insert at the end of the line | | `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
| o | Open new line below selection | | `i` | Insert before selection | `insert_mode` |
| o | Open new line above selection | | `a` | Insert after selection (append) | `append_mode` |
| u | Undo change | | `I` | Insert at the start of the line | `prepend_to_line` |
| U | Redo change | | `A` | Insert at the end of the line | `append_to_line` |
| y | Yank selection | | `o` | Open new line below selection | `open_below` |
| p | Paste after selection | | `O` | Open new line above selection | `open_above` |
| P | Paste before selection | | `.` | Repeat last change | N/A |
| > | Indent selection | | `u` | Undo change | `undo` |
| < | Unindent selection | | `U` | Redo change | `redo` |
| = | Format selection | | `y` | Yank selection | `yank` |
| d | Delete selection | | `p` | Paste after selection | `paste_after` |
| c | Change selection (delete and enter insert mode) | | `P` | Paste before selection | `paste_before` |
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` |
| `=` | Format selection | `format_selections` |
| `d` | Delete selection | `delete_selection` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` |
#### Shell
| Key | Description | Command |
| ------ | ----------- | ------- |
| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
| <code>A-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
### Selection manipulation ### Selection manipulation
| Key | Description | | Key | Description | Command |
|-----|-----------| | ----- | ----------- | ------- |
| s | Select all regex matches inside selections | | `s` | Select all regex matches inside selections | `select_regex` |
| S | Split selection into subselections on regex matches | | `S` | Split selection into subselections on regex matches | `split_selection` |
| alt-s | Split selection on newlines | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| ; | Collapse selection onto a single cursor | | `;` | Collapse selection onto a single cursor | `collapse_selection` |
| alt-; | Flip selection cursor and anchor | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| % | Select entire file | | `,` | Keep only the primary selection | `keep_primary_selection` |
| x | Select current line | | `Alt-,` | Remove the primary selection | `remove_primary_selection` |
| X | Extend to next line | | `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
| [ | Expand selection to parent syntax node TODO: pick a key | | `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
| J | join lines inside selection | | `(` | Rotate main selection backward | `rotate_selections_backward` |
| K | keep selections matching the regex TODO: overlapped by hover help | | `)` | Rotate main selection forward | `rotate_selections_forward` |
| space | keep only the primary selection TODO: overlapped by space mode | | `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
| ctrl-c | Comment/uncomment the selections | | `Alt-)` | Rotate selection contents forward | `rotate_selection_contents_forward` |
| `%` | Select entire file | `select_all` |
| `x` | Select current line, if already selected, extend to next line | `extend_line` |
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
| | Expand selection to parent syntax node TODO: pick a key | `expand_selection` |
| `J` | Join lines inside selection | `join_selections` |
| `K` | Keep selections matching the regex | `keep_selections` |
| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
### Search ### Search
> TODO: The search implementation isn't ideal yet -- we don't support searching > TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
in reverse, or searching via smartcase.
| Key | Description | | Key | Description | Command |
|-----|-----------| | ----- | ----------- | ------- |
| / | Search for regex pattern | | `/` | Search for regex pattern | `search` |
| n | Select next search match | | `n` | Select next search match | `search_next` |
| N | Add next search match to selection | | `N` | Add next search match to selection | `extend_search_next` |
| * | Use current selection as the search pattern | | `*` | Use current selection as the search pattern | `search_selection` |
## Select / extend mode ### Minor modes
I'm still pondering whether to keep this mode or not. It changes movement These sub-modes are accessible from normal mode and typically switch back to normal mode after a command.
commands to extend the existing selection instead of replacing it.
> NOTE: It's a bit confusing at the moment because extend hasn't been #### View mode
> implemented for all movement commands yet.
## View mode
View mode is intended for scrolling and manipulating the view without changing View mode is intended for scrolling and manipulating the view without changing
the selection. the selection. The "sticky" variant of this mode is persistent; use the Escape
key to return to normal mode after usage (useful when you're simply looking
over text and not actively editing it).
| Key | Description |
|-----|-----------|
| z , c | Vertically center the line |
| t | Align the line to the top of the screen |
| b | Align the line to the bottom of the screen |
| m | Align the line to the middle of the screen (horizontally) |
| j | Scroll the view downwards |
| k | Scroll the view upwards |
## Goto mode | Key | Description | Command |
| ----- | ----------- | ------- |
| `z` , `c` | Vertically center the line | `align_view_center` |
| `t` | Align the line to the top of the screen | `align_view_top` |
| `b` | Align the line to the bottom of the screen | `align_view_bottom` |
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j` | Scroll the view downwards | `scroll_down` |
| `k` | Scroll the view upwards | `scroll_up` |
| `f` | Move page down | `page_down` |
| `b` | Move page up | `page_up` |
| `d` | Move half page down | `half_page_down` |
| `u` | Move half page up | `half_page_up` |
#### Goto mode
Jumps to various locations. Jumps to various locations.
> NOTE: Some of these features are only available with the LSP present. > NOTE: Some of these features are only available with the LSP present.
| Key | Description | | Key | Description | Command |
|-----|-----------| | ----- | ----------- | ------- |
| g | Go to the start of the file | | `g` | Go to the start of the file | `goto_file_start` |
| e | Go to the end of the file | | `e` | Go to the end of the file | `goto_last_line` |
| d | Go to definition | | `h` | Go to the start of the line | `goto_line_start` |
| t | Go to type definition | | `l` | Go to the end of the line | `goto_line_end` |
| r | Go to references | | `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
| i | Go to implementation | | `t` | Go to the top of the screen | `goto_window_top` |
| `m` | Go to the middle of the screen | `goto_window_middle` |
| `b` | Go to the bottom of the screen | `goto_window_bottom` |
| `d` | Go to definition | `goto_definition` |
| `y` | Go to type definition | `goto_type_definition` |
| `r` | Go to references | `goto_reference` |
| `i` | Go to implementation | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
## Object mode #### 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 | Command |
| ----- | ----------- | ------- |
| `m` | Goto matching bracket | `match_brackets` |
| `s` `<char>` | Surround current selection with `<char>` | `surround_add` |
| `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` |
| `d` `<char>` | Delete surround character `<char>` | `surround_delete` |
| `a` `<object>` | Select around textobject | `select_textobject_around` |
| `i` `<object>` | Select inside textobject | `select_textobject_inner` |
TODO: Mappings for selecting syntax nodes (a superset of `[`). TODO: Mappings for selecting syntax nodes (a superset of `[`).
## Space mode #### Window mode
This layer is a kludge of mappings I had under leader key in neovim. This layer is similar to vim keybindings as kakoune does not support window.
| Key | Description | Command |
| ----- | ------------- | ------- |
| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `h`, `Ctrl-h` | Move to left split | `jump_view_left` |
| `j`, `Ctrl-j` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l` | Move to right split | `jump_view_right` |
| `q`, `Ctrl-q` | Close current window | `wclose` |
#### Space mode
This layer is a kludge of mappings, mostly pickers.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `k` | Show documentation for the item under the cursor | `hover` |
| `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` |
| `s` | Open symbol picker (current document) | `symbol_picker` |
| `a` | Apply code action | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
#### Unimpaired
Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
| Key | Description | Command |
| ----- | ----------- | ------- |
| `[d` | Go to previous diagnostic | `goto_prev_diag` |
| `]d` | Go to next diagnostic | `goto_next_diag` |
| `[D` | Go to first diagnostic in document | `goto_first_diag` |
| `]D` | Go to last diagnostic in document | `goto_last_diag` |
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |
## Insert Mode
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-w` | Delete previous word | `delete_word_backward` |
## Select / extend mode
I'm still pondering whether to keep this mode or not. It changes movement
commands (including goto) to extend the existing selection instead of replacing it.
> NOTE: It's a bit confusing at the moment because extend hasn't been
> implemented for all movement commands yet.
# Picker
Keys to use within picker. Remapping currently not supported.
| Key | Description | | Key | Description |
|-----|-----------| | ----- | ------------- |
| f | Open file picker | | `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
| b | Open buffer picker | | `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
| v | Open a new vertical split into the current file | | `Ctrl-space` | Filter options |
| w | Save changes to file | | `Enter` | Open selected |
| c | Close the current split | | `Ctrl-s` | Open horizontally |
| space | Keep primary selection TODO: it's here because space mode replaced it | | `Ctrl-v` | Open vertically |
| `Escape`, `Ctrl-c` | Close picker |

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

@@ -0,0 +1,54 @@
# 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 future).
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
g = { a = "code_action" } # Maps `ga` to show possible code actions
[keys.insert]
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
j = { k = "normal_mode" } # Maps `jk` to exit insert 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"` |
Keys can be disabled by binding them to the `no_op` command.
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)

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

@@ -0,0 +1,195 @@
# 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"
```
### Color palettes
It's recommended 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.
The default palette uses the terminal's default 16 colors, and the colors names
are listed below. The `[palette]` section in the config file takes precedence
over it and is merged into the default palette.
| Color Name |
| --- |
| `black` |
| `red` |
| `green` |
| `yellow` |
| `blue` |
| `magenta` |
| `cyan` |
| `gray` |
| `light-red` |
| `light-green` |
| `light-yellow` |
| `light-blue` |
| `light-magenta` |
| `light-cyan` |
| `light-gray` |
| `white` |
### Modifiers
The following values may be used as modifiers.
Less common modifiers might not be supported by your terminal emulator.
| Modifier |
| --- |
| `bold` |
| `dim` |
| `italic` |
| `underlined` |
| `slow_blink` |
| `rapid_blink` |
| `reversed` |
| `hidden` |
| `crossed_out` |
### Scopes
The following is a list of scopes available to use for styling.
#### Syntax highlighting
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme).
For a given highlight produced, styling will be determined based on the longest matching theme key. For example, the highlight `function.builtin.static` would match the key `function.builtin` rather than `function`.
We use a similar set of scopes as
[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
- `escape` (TODO: rename to (constant).character.escape)
- `type` - Types
- `builtin` - Primitive types provided by the language (`int`, `usize`)
- `constant` (TODO: constant.other.placeholder for %v)
- `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
- `boolean`
- `character`
- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex})
- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)?
- `regexp` - Regular expressions
- `special`
- `path`
- `url`
- `symbol` - Erlang/Elixir atoms, Ruby symbols, Clojure keywords
- `comment` - Code comments
- `line` - Single line comments (`//`)
- `block` - Block comments (e.g. (`/* */`)
- `documentation` - Documentation comments (e.g. `///` in Rust)
- `variable` - Variables
- `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
- `parameter` - Function parameters
- `property`
- `function` (TODO: ?)
- `label`
- `punctuation`
- `delimiter` - Commas, colons
- `bracket` - Parentheses, angle brackets, etc.
- `keyword`
- `control`
- `conditional` - `if`, `else`
- `repeat` - `for`, `while`, `loop`
- `import` - `import`, `export`
- (TODO: return?)
- `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func`
- `operator` - `||`, `+=`, `>`, `or`
- `function`
- `builtin`
- `method`
- `macro`
- `special` (preprocesor in C)
- `tag` - Tags (e.g. `<body>` in HTML)
- `namespace`
#### Interface
These scopes are used for theming the editor interface.
| Key | Notes |
| --- | --- |
| `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` | Statusline |
| `ui.statusline.inactive` | Statusline (unfocused document) |
| `ui.popup` | |
| `ui.window` | |
| `ui.help` | |
| `ui.text` | |
| `ui.text.focus` | |
| `ui.info` | |
| `ui.info.text` | |
| `ui.menu` | |
| `ui.menu.selected` | |
| `ui.selection` | For selections in the editing area |
| `ui.selection.primary` | |
| `warning` | Diagnostics warning (gutter) |
| `error` | Diagnostics error (gutter) |
| `info` | Diagnostics info (gutter) |
| `hint` | Diagnostics hint (gutter) |
| `diagnostic` | For text in editing area |

View File

@@ -1 +1,73 @@
# Usage # Usage
(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
See [tutor.txt](https://github.com/helix-editor/helix/blob/master/runtime/tutor.txt) (accessible via `hx --tutor` or `:tutor`) for a vimtutor-like introduction.
## Registers
Vim-like registers can be used to yank and store text to be pasted later. Usage is similar, with `"` being used to select a register:
- `"ay` - Yank the current selection to register `a`.
- `"op` - Paste the text in register `o` after the selection.
If there is a selected register before invoking a change or delete command, the selection will be stored in the register and the action will be carried out:
- `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode).
- `"md` - Store the selection in register `m` and delete it.
### Special Registers
| Register character | Contains |
| --- | --- |
| `/` | Last search |
| `:` | Last executed command |
| `"` | Last yanked text |
> There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics.
## 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`, `function`, `class`, `parameter`.
![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
![textobject-treesitter-demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
| Key after `mi` or `ma` | Textobject selected |
| --- | --- |
| `w` | Word |
| `(`, `[`, `'`, etc | Specified surround pairs |
| `f` | Function |
| `c` | Class |
| `p` | Parameter |
Note: `f`, `c`, etc need a tree-sitter grammar active for the current
document and a special tree-sitter query file to work properly. [Only
some grammars](https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=)
currently have the query file implemented. Contributions are welcome !

View File

@@ -196,7 +196,7 @@ a > .hljs {
border-radius: 3px; border-radius: 3px;
} }
:not(pre):not(a) > .hljs { :not(pre):not(a):not(td):not(p) > .hljs {
color: var(--inline-code-color); color: var(--inline-code-color);
overflow-x: initial; overflow-x: initial;
} }

View File

@@ -114,6 +114,19 @@ h6:target::before {
margin-bottom: .875em; margin-bottom: .875em;
} }
.content ul li {
margin-bottom: .25rem;
}
.content ul {
list-style-type: square;
}
.content ul ul, .content ol ul {
margin-bottom: .5rem;
}
.content li p {
margin-bottom: .5em;
}
.content p { line-height: 1.45em; } .content p { line-height: 1.45em; }
.content ol { line-height: 1.45em; } .content ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; } .content ul { line-height: 1.45em; }
@@ -149,7 +162,6 @@ table thead td {
table thead th { table thead th {
padding: .75rem; padding: .75rem;
text-align: left; text-align: left;
color: var(--table-border-color);
font-weight: 500; font-weight: 500;
line-height: 1.5; line-height: 1.5;
width: auto; width: auto;
@@ -215,3 +227,7 @@ blockquote *:last-child {
margin: 5px 0px; margin: 5px 0px;
font-weight: bold; font-weight: bold;
} }
.result-no-output {
font-style: italic;
}

View File

@@ -13,7 +13,6 @@
.ayu { .ayu {
--bg: hsl(210, 25%, 8%); --bg: hsl(210, 25%, 8%);
--fg: #c5c5c5; --fg: #c5c5c5;
--heading-fg: #c5c5c5;
--sidebar-bg: #14191f; --sidebar-bg: #14191f;
--sidebar-fg: #c8c9db; --sidebar-fg: #c8c9db;
@@ -54,7 +53,6 @@
.coal { .coal {
--bg: hsl(200, 7%, 8%); --bg: hsl(200, 7%, 8%);
--fg: #98a3ad; --fg: #98a3ad;
--heading-fg: #98a3ad;
--sidebar-bg: #292c2f; --sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8; --sidebar-fg: #a1adb8;
@@ -69,7 +67,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #c5c8c6;
--theme-popup-bg: #141617; --theme-popup-bg: #141617;
--theme-popup-border: #43484d; --theme-popup-border: #43484d;
@@ -95,7 +93,6 @@
.light { .light {
--bg: hsl(0, 0%, 100%); --bg: hsl(0, 0%, 100%);
--fg: hsl(0, 0%, 0%); --fg: hsl(0, 0%, 0%);
--heading-fg: hsl(0, 0%, 0%);
--sidebar-bg: #fafafa; --sidebar-bg: #fafafa;
--sidebar-fg: hsl(0, 0%, 0%); --sidebar-fg: hsl(0, 0%, 0%);
@@ -136,7 +133,6 @@
.navy { .navy {
--bg: hsl(226, 23%, 11%); --bg: hsl(226, 23%, 11%);
--fg: #bcbdd0; --fg: #bcbdd0;
--heading-fg: #bcbdd0;
--sidebar-bg: #282d3f; --sidebar-bg: #282d3f;
--sidebar-fg: #c8c9db; --sidebar-fg: #c8c9db;
@@ -151,7 +147,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #c5c8c6;
--theme-popup-bg: #161923; --theme-popup-bg: #161923;
--theme-popup-border: #737480; --theme-popup-border: #737480;
@@ -177,7 +173,6 @@
.rust { .rust {
--bg: hsl(60, 9%, 87%); --bg: hsl(60, 9%, 87%);
--fg: #262625; --fg: #262625;
--heading-fg: #262625;
--sidebar-bg: #3b2e2a; --sidebar-bg: #3b2e2a;
--sidebar-fg: #c8c9db; --sidebar-fg: #c8c9db;
@@ -218,8 +213,7 @@
@media (prefers-color-scheme: dark) { @media (prefers-color-scheme: dark) {
.light.no-js { .light.no-js {
--bg: hsl(200, 7%, 8%); --bg: hsl(200, 7%, 8%);
--fg: #ebeafa; --fg: #98a3ad;
--heading-fg: #ebeafa;
--sidebar-bg: #292c2f; --sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8; --sidebar-fg: #a1adb8;
@@ -234,7 +228,7 @@
--links: #2b79a2; --links: #2b79a2;
--inline-code-color: #c5c8c6;; --inline-code-color: #c5c8c6;
--theme-popup-bg: #141617; --theme-popup-bg: #141617;
--theme-popup-border: #43484d; --theme-popup-border: #43484d;
@@ -261,6 +255,7 @@
.colibri { .colibri {
--bg: #3b224c; --bg: #3b224c;
--fg: #bcbdd0; --fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733; --sidebar-bg: #281733;
--sidebar-fg: #c8c9db; --sidebar-fg: #c8c9db;
@@ -276,18 +271,19 @@
/* --links: #a4a0e8; */ /* --links: #a4a0e8; */
--links: #ECCDBA; --links: #ECCDBA;
--inline-code-color: #c5c8c6;; --inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923; --theme-popup-bg: #161923;
--theme-popup-border: #737480; --theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2); --theme-hover: rgba(0,0,0, .2);
--quote-bg: hsl(226, 15%, 17%); --quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%); --quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 16%); --table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsl(226, 23%, 31%); --table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%); --table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa; --searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6; --searchbar-bg: #aeaec6;
@@ -300,6 +296,7 @@
} }
.colibri { .colibri {
/*
--bg: #ffffff; --bg: #ffffff;
--fg: #452859; --fg: #452859;
--fg: #5a5977; --fg: #5a5977;
@@ -318,7 +315,7 @@
--links: #6F44F0; --links: #6F44F0;
--inline-code-color: #697C81; --inline-code-color: #a39e9b;
--theme-popup-bg: #161923; --theme-popup-bg: #161923;
--theme-popup-border: #737480; --theme-popup-border: #737480;
@@ -341,4 +338,5 @@
--searchresults-border-color: #5c5c68; --searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430; --searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5; --search-mark-bg: #a2cff5;
*/
} }

View File

@@ -1,83 +1,56 @@
/* pre code.hljs {
* An increased contrast highlighting scheme loosely based on the display:block;
* "Base16 Atelier Dune Light" theme by Bram de Haan overflow-x:auto;
* (http://atelierbram.github.io/syntax-highlighting/atelier-schemes/dune) padding:1em
* Original Base16 color scheme by Chris Kempson }
* (https://github.com/chriskempson/base16) code.hljs {
*/ padding:3px 5px
}
/* Comment */ .hljs {
background:#2f1e2e;
color:#a39e9b
}
.hljs-comment, .hljs-comment,
.hljs-quote { .hljs-quote {
color: #575757; color:#8d8687
} }
.hljs-link,
/* Red */ .hljs-meta,
.hljs-variable,
.hljs-template-variable,
.hljs-attribute,
.hljs-tag,
.hljs-name, .hljs-name,
.hljs-regexp, .hljs-regexp,
.hljs-link, .hljs-selector-class,
.hljs-name,
.hljs-selector-id, .hljs-selector-id,
.hljs-selector-class { .hljs-tag,
color: #d70025; .hljs-template-variable,
.hljs-variable {
color:#ef6155
} }
/* Orange */
.hljs-number,
.hljs-meta,
.hljs-built_in, .hljs-built_in,
.hljs-builtin-name, .hljs-deletion,
.hljs-literal, .hljs-literal,
.hljs-type, .hljs-number,
.hljs-params { .hljs-params,
color: #b21e00; .hljs-type {
color:#f99b15
} }
.hljs-attribute,
/* Green */ .hljs-section,
.hljs-title {
color:#fec418
}
.hljs-addition,
.hljs-bullet,
.hljs-string, .hljs-string,
.hljs-symbol, .hljs-symbol {
.hljs-bullet { color:#48b685
color: #008200;
} }
/* Blue */
.hljs-title,
.hljs-section {
color: #0030f2;
}
/* Purple */
.hljs-keyword, .hljs-keyword,
.hljs-selector-tag { .hljs-selector-tag {
color: #9d00ec; color:#815ba4
} }
.hljs {
display: block;
overflow-x: auto;
background: #f6f7f6;
color: #000;
padding: 0.5em;
}
.hljs-emphasis { .hljs-emphasis {
font-style: italic; font-style:italic
} }
.hljs-strong { .hljs-strong {
font-weight: bold; font-weight:700
}
.hljs-addition {
color: #22863a;
background-color: #f0fff4;
}
.hljs-deletion {
color: #b31d28;
background-color: #ffeef0;
} }

1
contrib/themes Symbolic link
View File

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

24
docs/vision.md Normal file
View File

@@ -0,0 +1,24 @@
The Helix project still has a ways to go before reaching its goals. This document outlines some of those goals and the overall vision for the project.
# Vision
An efficient, batteries-included editor you can take anywhere and be productive... if it's your kind of thing.
* **Cross-platform.** Whether on Linux, Windows, or OSX, you should be able to take your editor with you.
* **Terminal first.** Not all environments have a windowing system, and you shouldn't have to abandon your preferred editor in those cases.
* **Native.** No Electron or HTML DOM here. We want an efficient, native-compiled editor that can run with minimal resources when needed. If you're working on a Raspberry Pi, your editor shouldn't consume half of your RAM.
* **Batteries included.** Both the default configuration and bundled features should be enough to have a good editing experience and be productive. You shouldn't need a massive custom config or external executables and plugins for basic features and functionality.
* **Don't try to be everything for everyone.** There are many great editors out there to choose from. Let's make Helix *one of* those great options, with its own take on things.
# Goals
Vision statements are all well and good, but are also vague and subjective. Here is a (non-exhaustive) list of some of Helix's more concrete goals, to help give a clearer idea of the project's direction:
* **Modal.** Vim is a great idea.
* **Selection -> Action**, not Verb -> Object. Interaction models aren't linguistics, and "selection first" lets you see what you're doing (among other benefits).
* **We aren't playing code golf.** It's more important for the keymap to be consistent and easy to memorize than it is to save a key stroke or two when editing.
* **Built-in tools** for working with code bases efficiently. Most projects aren't a single file, and an editor should handle that as a first-class use case. In Helix's case, this means (among other things) a fuzzy-search file navigator and LSP support.
* **Edit anything** that comes up when coding, within reason. Whether it's a 200 MB XML file, a megabyte of minified javascript on a single line, or Japanese text encoded in ShiftJIS, you should be able to open it and edit it without problems. (Note: this doesn't mean handle every esoteric use case. Sometimes you do just need a specialized tool, and Helix isn't that.)
* **Configurable**, within reason. Although the defaults should be good, not everyone will agree on what "good" is. Within the bounds of Helix's core interaction models, it should be reasonably configurable so that it can be "good" for more people. This means, for example, custom key maps among other things.
* **Extensible**, within reason. Although we want Helix to be productive out-of-the-box, it's not practical or desirable to cram every useful feature and use case into the core editor. The basics should be built-in, but you should be able to extend it with additional functionality as needed. Right now we're thinking Wasm-based plugins.
* **Clean code base.** Sometimes other factors (e.g. significant performance gains, important features, correctness, etc.) will trump strict readability, but we nevertheless want to keep the code base straightforward and easy to understand to the extent we can.

107
flake.lock generated
View File

@@ -1,12 +1,27 @@
{ {
"nodes": { "nodes": {
"devshell": {
"locked": {
"lastModified": 1632436039,
"narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
"owner": "numtide",
"repo": "devshell",
"rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": { "flake-utils": {
"locked": { "locked": {
"lastModified": 1620759905, "lastModified": 1623875721,
"narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=", "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
"owner": "numtide", "owner": "numtide",
"repo": "flake-utils", "repo": "flake-utils",
"rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8", "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -15,59 +30,53 @@
"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": [
"rust-overlay"
]
}, },
"locked": { "locked": {
"lastModified": 1620316130, "lastModified": 1634796585,
"narHash": "sha256-sU0VS5oJS1FsHsZsLELAXc7G2eIelVuucRw+q5B1x9k=", "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
"owner": "nmattia", "owner": "yusdacra",
"repo": "naersk", "repo": "nix-cargo-integration",
"rev": "a3f40fe42cc6d267ff7518fa3199e99ff1444ac4", "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
"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": 1634782485,
"narHash": "sha256-t1/ZMtyxClVSfcV4Pt5C1YpkeJ/UwFF3oitLD7Ch/UA=", "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
"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": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -77,13 +86,13 @@
"type": "github" "type": "github"
} }
}, },
"nixpkgs_3": { "nixpkgs_2": {
"locked": { "locked": {
"lastModified": 1617325113, "lastModified": 1628186154,
"narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=", "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
"owner": "nixos", "owner": "NixOS",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "54c1e44240d8a527a8f4892608c4bce5440c3ecb", "rev": "06552b72346632b6943c8032e57e702ea12413bf",
"type": "github" "type": "github"
}, },
"original": { "original": {
@@ -94,23 +103,23 @@
}, },
"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": "rust-overlay"
} }
}, },
"rust-overlay": { "rust-overlay": {
"inputs": { "inputs": {
"flake-utils": "flake-utils_2", "flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_3" "nixpkgs": "nixpkgs_2"
}, },
"locked": { "locked": {
"lastModified": 1622257069, "lastModified": 1634869268,
"narHash": "sha256-+QVnS/es9JCRZXphoHL0fOIUhpGqB4/wreBsXWArVck=", "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "8aa5f93c0b665e5357af19c5631a3450bff4aba5", "rev": "c02c2d86354327317546501af001886fbb53d374",
"type": "github" "type": "github"
}, },
"original": { "original": {

View File

@@ -3,31 +3,73 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay"; rust-overlay.url = "github:oxalica/rust-overlay";
naersk.url = "github:nmattia/naersk"; nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay";
};
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: {
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_12 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,26 +1,40 @@
[package] [package]
name = "helix-core" name = "helix-core"
version = "0.1.0" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
description = "Helix editor core editing primitives"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features]
[dependencies] [dependencies]
helix-syntax = { path = "../helix-syntax" } helix-syntax = { version = "0.5", path = "../helix-syntax" }
ropey = "1.2" ropey = "1.3"
smallvec = "1.4" smallvec = "1.7"
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.20"
once_cell = "1.4" once_cell = "1.8"
arc-swap = "1"
regex = "1" regex = "1"
log = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5" toml = "0.5"
similar = "2.1"
etcetera = "0.3" etcetera = "0.3"
[dev-dependencies]
quickcheck = { version = "1", default-features = false }

View File

@@ -1,3 +1,6 @@
//! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction}; use crate::{Range, Rope, Selection, Tendril, Transaction};
use smallvec::SmallVec; use smallvec::SmallVec;
@@ -12,7 +15,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 +70,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);
@@ -84,6 +87,7 @@ fn handle_open(
match next { match next {
Some(ch) if !close_before.contains(ch) => { Some(ch) if !close_before.contains(ch) => {
offs += 1;
// TODO: else return (use default handler that inserts open) // TODO: else return (use default handler that inserts open)
(pos, pos, Some(Tendril::from_char(open))) (pos, pos, Some(Tendril::from_char(open)))
} }
@@ -109,7 +113,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 +143,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

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

@@ -0,0 +1,135 @@
//! Utility functions to categorize a `char`.
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,33 @@
//! This module contains the functionality toggle comments on lines over the selection
//! using the comment character defined in the user's `languages.toml`
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 +42,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 +112,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,7 @@
#[derive(Eq, PartialEq)] //! LSP diagnostic utility types.
/// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Eq, PartialEq)]
pub enum Severity { pub enum Severity {
Error, Error,
Warning, Warning,
@@ -6,10 +9,15 @@ pub enum Severity {
Hint, Hint,
} }
/// A range of `char`s within the text.
#[derive(Debug)]
pub struct Range { pub struct Range {
pub start: usize, pub start: usize,
pub end: usize, pub end: usize,
} }
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
#[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

@@ -1,8 +1,12 @@
// Based on https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs //! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents.
//!
//! Based on https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs
use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice}; 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 +73,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 +121,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 +189,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 +238,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,62 @@
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.
///
/// Using time to navigate the history: https://github.com/helix-editor/helix/pull/194
#[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,74 +65,275 @@ 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
} }
// TODO: I'd like to pass Transaction by reference but it fights with the borrowck /// Undo the last edit.
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.clone())
} }
pub fn redo(&mut self) -> Option<Transaction> { /// Redo the last edit.
let current_revision = &self.revisions[self.cursor]; pub fn redo(&mut self) -> Option<&Transaction> {
let current_revision = &self.revisions[self.current];
let last_child = current_revision.last_child?;
self.current = last_child.get();
// for now, simply pick the latest child (linear undo / redo) 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.clone()); 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
}
/// Create a [`Transaction`] that will jump to a specific revision in the history.
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()
}
/// Creates a [`Transaction`] that will undo `delta` revisions.
fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
self.jump_to(self.current.saturating_sub(delta))
}
/// Creates a [`Transaction`] that will redo `delta` revisions.
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,
}
}
/// Creates a [`Transaction`] that will match a revision created at around
/// `instant`.
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)
}
/// Creates a [`Transaction`] that will match a revision created `duration` ago
/// from the timestamp of current 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),
}
}
/// Creates a [`Transaction`] that will match a revision created `duration` in
/// the future from the timestamp of the current revision.
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),
}
}
/// Creates an undo [`Transaction`].
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),
}
}
/// Creates a redo [`Transaction`].
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),
}
}
}
/// Whether to undo by a number of edits or a duration of time.
#[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),
];
/// Checks if the duration input can be turned into a valid duration. It must be a
/// positive integer and denote the [unit of time.](`TIME_UNITS`)
/// Examples of valid durations:
/// * `5 sec`
/// * `5 min`
/// * `5 hr`
/// * `5 days`
static DURATION_VALIDATION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
/// Captures both the number and unit as separate capture groups.
static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
/// Parse a string (e.g. "5 sec") and try to convert it into a [`Duration`].
fn parse_human_duration(s: &str) -> Result<Duration, String> {
if !DURATION_VALIDATION_REGEX.is_match(s) {
return Err("duration should be composed \
of positive integers followed by time units"
.to_string());
}
let mut specified = [false; TIME_UNITS.len()];
let mut seconds = 0u64;
for cap in NUMBER_UNIT_REGEX.captures_iter(s) {
let (n, unit_str) = (&cap[1], &cap[2]);
let n: u64 = n.parse().map_err(|_| format!("integer too large: {}", n))?;
let time_unit = TIME_UNITS
.iter()
.enumerate()
.find(|(_, (forms, _, _))| forms.iter().any(|f| f == &unit_str));
if let Some((i, (_, unit, mul))) = time_unit {
if specified[i] {
return Err(format!("{} specified more than once", unit));
}
specified[i] = true;
let new_seconds = n.checked_mul(*mul).and_then(|s| seconds.checked_add(s));
match new_seconds {
Some(ns) => seconds = ns,
None => return Err("duration too large".to_string()),
}
} else {
return Err(format!("incorrect time unit: {}", unit_str));
}
}
Ok(Duration::from_secs(seconds))
}
impl std::str::FromStr for UndoKind {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let s = s.trim();
if s.is_empty() {
Ok(Self::Steps(1usize))
} else if let Ok(n) = s.parse::<usize>() {
Ok(UndoKind::Steps(n))
} else {
Ok(Self::TimePeriod(parse_human_duration(s)?))
} }
None
} }
} }
@@ -145,4 +389,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,35 @@ 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 pos = text.char_to_byte(pos);
let mut node = match syntax
.tree()
.root_node()
.descendant_for_byte_range(pos, pos)
{
Some(node) => node,
None => return scopes,
};
scopes.push(node.kind());
while let Some(parent) = node.parent() {
scopes.push(parent.kind());
node = parent;
}
}
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,23 +443,28 @@ 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(Configuration { let loader = Loader::new(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,
// //
injection_regex: 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,
unit: String::from(" "), unit: String::from(" "),
}), }),
indent_query: OnceCell::new(), indent_query: OnceCell::new(),
textobject_query: OnceCell::new(),
}], }],
}); });

View File

@@ -1,84 +1,205 @@
#![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;
pub mod object; pub mod object;
pub mod path;
mod position; mod position;
pub mod register; 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(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
} }
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 static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
for ch in line.chars() { once_cell::sync::Lazy::new(runtime_dir);
// TODO: could use memchr with chunks?
if ch != ' ' && ch != '\t' && ch != '\n' {
return Some(start);
}
start += 1;
}
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}
pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory");
let root = match root {
Some(root) => {
let root = std::path::Path::new(root);
if root.is_absolute() {
root.to_path_buf()
} else {
current_dir.join(root)
}
}
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
} }
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 {
// TODO: allow env var override // TODO: allow env var override
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
let strategy = choose_base_strategy().expect("Unable to find the config directory!"); let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let mut path = strategy.config_dir(); let mut path = strategy.config_dir();
path.push("helix"); path.push("helix");
path path
} }
pub use ropey::{Rope, RopeSlice}; pub fn cache_dir() -> std::path::PathBuf {
// TODO: allow env var override
let strategy = choose_base_strategy().expect("Unable to find the config directory!");
let mut path = strategy.cache_dir();
path.push("helix");
path
}
// right overrides left
pub fn merge_toml_values(left: toml::Value, right: toml::Value) -> toml::Value {
use toml::Value;
fn get_name(v: &Value) -> Option<&str> {
v.get("name").and_then(Value::as_str)
}
match (left, right) {
(Value::Array(mut left_items), Value::Array(right_items)) => {
left_items.reserve(right_items.len());
for rvalue in right_items {
let lvalue = get_name(&rvalue)
.and_then(|rname| left_items.iter().position(|v| get_name(v) == Some(rname)))
.map(|lpos| left_items.remove(lpos));
let mvalue = match lvalue {
Some(lvalue) => merge_toml_values(lvalue, rvalue),
None => rvalue,
};
left_items.push(mvalue);
}
Value::Array(left_items)
}
(Value::Table(mut left_map), Value::Table(right_map)) => {
for (rname, rvalue) in right_map {
match left_map.remove(&rname) {
Some(lvalue) => {
let merged_value = merge_toml_values(lvalue, rvalue);
left_map.insert(rname, merged_value);
}
None => {
left_map.insert(rname, rvalue);
}
}
}
Value::Table(left_map)
}
// Catch everything else we didn't handle, and use the right value
(_, value) => value,
}
}
#[cfg(test)]
mod merge_toml_tests {
use super::merge_toml_values;
#[test]
fn language_tomls() {
use toml::Value;
const USER: &str = "
[[language]]
name = \"nix\"
test = \"bbb\"
indent = { tab-width = 4, unit = \" \", test = \"aaa\" }
";
let base: Value = toml::from_slice(include_bytes!("../../languages.toml"))
.expect("Couldn't parse built-in langauges config");
let user: Value = toml::from_str(USER).unwrap();
let merged = merge_toml_values(base, user);
let languages = merged.get("language").unwrap().as_array().unwrap();
let nix = languages
.iter()
.find(|v| v.get("name").unwrap().as_str().unwrap() == "nix")
.unwrap();
let nix_indent = nix.get("indent").unwrap();
// We changed tab-width and unit in indent so check them if they are the new values
assert_eq!(
nix_indent.get("tab-width").unwrap().as_integer().unwrap(),
4
);
assert_eq!(nix_indent.get("unit").unwrap().as_str().unwrap(), " ");
// We added a new keys, so check them
assert_eq!(nix.get("test").unwrap().as_str().unwrap(), "bbb");
assert_eq!(nix_indent.get("test").unwrap().as_str().unwrap(), "aaa");
// We didn't change comment-token so it should be same
assert_eq!(nix.get("comment-token").unwrap().as_str().unwrap(), "#");
}
}
pub use etcetera::home_dir;
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
pub use ropey::{Rope, RopeBuilder, RopeSlice};
pub use tendril::StrTendril as Tendril; 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 const fn len_chars(&self) -> usize {
match self {
Self::Crlf => 2,
_ => 1,
}
}
#[inline]
pub const 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 const 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,6 +1,6 @@
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 ( ) { } [ ] < >
#[must_use] #[must_use]
@@ -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)
{ {
@@ -20,15 +20,28 @@ pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
None => return None, None => return None,
}; };
let start_byte = node.start_byte(); if node.is_error() {
let end_byte = node.end_byte() - 1; // it's end exclusive return None;
}
let len = doc.len_bytes();
let start_byte = node.start_byte();
let end_byte = node.end_byte().saturating_sub(1); // it's end exclusive
if start_byte >= len || end_byte >= len {
return None;
}
let start_char = doc.byte_to_char(start_byte);
let end_char = doc.byte_to_char(end_byte);
if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
if start_byte == byte_pos { if start_byte == byte_pos {
return Some(doc.byte_to_char(end_byte)); return Some(end_char);
} }
if end_byte == byte_pos { if end_byte == byte_pos {
return Some(doc.byte_to_char(start_byte)); return Some(start_char);
}
} }
None None

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());

92
helix-core/src/path.rs Normal file
View File

@@ -0,0 +1,92 @@
use std::path::{Component, Path, PathBuf};
/// Replaces users home directory from `path` with tilde `~` if the directory
/// is available, otherwise returns the path unchanged.
pub fn fold_home_dir(path: &Path) -> PathBuf {
if let Ok(home) = super::home_dir() {
if path.starts_with(&home) {
// it's ok to unwrap, the path starts with home dir
return PathBuf::from("~").join(path.strip_prefix(&home).unwrap());
}
}
path.to_path_buf()
}
/// Expands tilde `~` into users home directory if avilable, otherwise returns the path
/// unchanged. The tilde will only be expanded when present as the first component of the path
/// and only slash follows it.
pub fn expand_tilde(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
if let Some(Component::Normal(c)) = components.peek() {
if c == &"~" {
if let Ok(home) = super::home_dir() {
// it's ok to unwrap, the path starts with `~`
return home.join(path.strip_prefix("~").unwrap());
}
}
}
path.to_path_buf()
}
/// Normalize a path, removing things like `.` and `..`.
///
/// CAUTION: This does not resolve symlinks (unlike
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
/// behavior at times. This should be used carefully. Unfortunately,
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
/// needs to improve on.
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
pub fn get_normalized_path(path: &Path) -> PathBuf {
let path = expand_tilde(path);
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
/// Returns the canonical, absolute form of a path with all intermediate components normalized.
///
/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
/// here if the path exists, just normalize it's components.
pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
let path = if path.is_relative() {
std::env::current_dir().map(|current_dir| current_dir.join(path))?
} else {
path.to_path_buf()
};
Ok(get_normalized_path(path.as_path()))
}
pub fn get_relative_path(path: &Path) -> PathBuf {
let path = if path.is_absolute() {
let cwdir = std::env::current_dir().expect("couldn't determine current directory");
path.strip_prefix(cwdir).unwrap_or(path)
} else {
path
};
fold_home_dir(path)
}

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 const fn new(name: char) -> Self {
let mut registry = REGISTRY.write().unwrap(); Self {
name,
values: Vec::new(),
}
}
registry.insert(register, values); pub fn new_with_values(name: char, values: Vec<String>) -> Self {
Self { name, values }
}
pub const 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( pub fn find_nth_prev(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
text: RopeSlice, if pos == 0 || n == 0 {
ch: char, return None;
mut pos: usize, }
n: usize,
inclusive: bool, let mut chars = text.chars_at(pos);
) -> Option<usize> {
// start searching right before pos
let mut chars = text.chars_at(pos.saturating_sub(1));
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,21 @@ 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()
}
/// Adds a new range to the selection and makes it the primary range.
pub fn remove(mut self, index: usize) -> Self {
self.ranges.remove(index);
if index < self.primary_index || self.primary_index == self.ranges.len() {
self.primary_index -= 1;
}
self
} }
// 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 +393,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 +416,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 +546,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 +575,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 - 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 +610,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 +642,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 +680,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,35 +694,251 @@ 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]
fn test_split_on_matches() { fn test_split_on_matches() {
use crate::regex::Regex; use crate::regex::Regex;
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,6 @@
use crate::{Rope, Selection}; use crate::{Rope, Selection};
/// A state represents the current editor state of a single buffer. #[derive(Debug, Clone)]
#[derive(Clone)]
pub struct State { pub struct State {
pub doc: Rope, pub doc: Rope,
pub selection: Selection, pub selection: Selection,
@@ -15,27 +14,4 @@ impl State {
selection: Selection::point(0), selection: Selection::point(0),
} }
} }
// update/transact:
// update(desc) => transaction ? transaction.doc() for applied doc
// transaction.apply(doc)
// doc.transact(fn -> ... end)
// replaceSelection (transaction that replaces selection)
// changeByRange
// changes
// slice
//
// getters:
// tabSize
// indentUnit
// languageDataAt()
//
// config:
// indentation
// tabSize
// lineUnit
// syntax
// foldable
// changeFilter/transactionFilter
} }

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,37 +1,69 @@
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)] fn deserialize_regex<'de, D>(deserializer: D) -> Result<Option<Regex>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<String>::deserialize(deserializer)?
.map(|buf| Regex::new(&buf).map_err(serde::de::Error::custom))
.transpose()
}
fn deserialize_lsp_config<'de, D>(deserializer: D) -> Result<Option<serde_json::Value>, D::Error>
where
D: serde::Deserializer<'de>,
{
Option::<toml::Value>::deserialize(deserializer)?
.map(|toml| toml.try_into().map_err(serde::de::Error::custom))
.transpose()
}
#[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 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 path: PathBuf, #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")]
// root_path for tree-sitter (^) pub config: Option<serde_json::Value>,
#[serde(default)]
pub auto_format: bool,
// content_regex // content_regex
// injection_regex #[serde(default, skip_serializing, deserialize_with = "deserialize_regex")]
pub injection_regex: Option<Regex>,
// first_line_regex // first_line_regex
// //
#[serde(skip)] #[serde(skip)]
@@ -44,9 +76,11 @@ pub struct LanguageConfiguration {
#[serde(skip)] #[serde(skip)]
pub(crate) indent_query: OnceCell<Option<IndentQuery>>, pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
#[serde(skip)]
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
} }
#[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 +89,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,16 +107,45 @@ pub struct IndentQuery {
pub outdent: HashSet<String>, pub outdent: HashSet<String>,
} }
#[derive(Debug)]
pub struct TextObjectQuery {
pub query: Query,
}
impl TextObjectQuery {
/// Run the query on the given node and return sub nodes which match given
/// capture ("function.inside", "class.around", etc).
pub fn capture_nodes<'a>(
&'a self,
capture_name: &str,
node: Node<'a>,
slice: RopeSlice<'a>,
cursor: &'a mut QueryCursor,
) -> Option<impl Iterator<Item = Node<'a>>> {
let capture_idx = self.query.capture_index_for_name(capture_name)?;
let captures = cursor.captures(&self.query, node, RopeProvider(slice));
captures
.filter_map(move |(mat, idx)| {
(mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node)
})
.into()
}
}
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let path = crate::RUNTIME_DIR
.join("queries")
.join(language)
.join(filename);
std::fs::read_to_string(&path)
}
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());
let root = crate::runtime_dir(); let query = load_runtime_file(language, filename).unwrap_or_default();
// let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let path = root.join("queries").join(language).join(filename);
let query = std::fs::read_to_string(&path).unwrap_or_default();
// TODO: the collect() is not ideal // TODO: the collect() is not ideal
let inherits = INHERITS_REGEX let inherits = INHERITS_REGEX
@@ -110,47 +173,73 @@ 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
// highlights_query += "\n(ERROR) @error"; // highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm"); let injections_query = read_query(&language, "injections.scm");
let locals_query = read_query(&language, "locals.scm");
let locals_query = "";
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)
let mut config = HighlightConfiguration::new( .map_err(|e| log::info!("{}", e))
.ok()?;
let config = HighlightConfiguration::new(
language, language,
&highlights_query, &highlights_query,
&injections_query, &injections_query,
locals_query, &locals_query,
) );
.unwrap(); // TODO: no unwrap
let config = match config {
Ok(config) => config,
Err(err) => panic!("{}", err),
}; // TODO: avoid panic
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 root = crate::runtime_dir(); let toml = load_runtime_file(&language, "indents.toml").ok()?;
let path = root.join("queries").join(language).join("indents.toml"); toml::from_slice(toml.as_bytes()).ok()
})
.as_ref()
}
let toml = std::fs::read(&path).ok()?; pub fn textobject_query(&self) -> Option<&TextObjectQuery> {
toml::from_slice(&toml).ok() self.textobject_query
.get_or_init(|| -> Option<TextObjectQuery> {
let lang_name = self.language_id.to_ascii_lowercase();
let query_text = read_query(&lang_name, "textobjects.scm");
let lang = self.highlight_config.get()?.as_ref()?.language;
let query = Query::new(lang, &query_text).ok()?;
Some(TextObjectQuery { query })
}) })
.as_ref() .as_ref()
} }
@@ -160,8 +249,7 @@ 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>>,
@@ -216,6 +304,34 @@ impl Loader {
.find(|config| config.scope == scope) .find(|config| config.scope == scope)
.cloned() .cloned()
} }
pub fn language_configuration_for_injection_string(
&self,
string: &str,
) -> Option<Arc<LanguageConfiguration>> {
let mut best_match_length = 0;
let mut best_match_position = None;
for (i, configuration) in self.language_configs.iter().enumerate() {
if let Some(injection_regex) = &configuration.injection_regex {
if let Some(mat) = injection_regex.find(string) {
let length = mat.end() - mat.start();
if length > best_match_length {
best_match_position = Some(i);
best_match_length = length;
}
}
}
}
if let Some(i) = best_match_position {
let configuration = &self.language_configs[i];
return Some(configuration.clone());
}
None
}
pub fn language_configs_iter(&self) -> impl Iterator<Item = &Arc<LanguageConfiguration>> {
self.language_configs.iter()
}
} }
pub struct TsParser { pub struct TsParser {
@@ -223,6 +339,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 {
@@ -231,6 +353,7 @@ thread_local! {
}) })
} }
#[derive(Debug)]
pub struct Syntax { pub struct Syntax {
config: Arc<HighlightConfiguration>, config: Arc<HighlightConfiguration>,
@@ -243,16 +366,6 @@ fn byte_range_to_str(range: std::ops::Range<usize>, source: RopeSlice) -> Cow<st
Cow::from(source.slice(start_char..end_char)) Cow::from(source.slice(start_char..end_char))
} }
fn node_to_bytes<'a>(node: Node, source: RopeSlice<'a>) -> Cow<'a, [u8]> {
let start_char = source.byte_to_char(node.start_byte());
let end_char = source.byte_to_char(node.end_byte());
let fragment = source.slice(start_char..end_char);
match fragment.as_str() {
Some(fragment) => Cow::Borrowed(fragment.as_bytes()),
None => Cow::Owned(String::from(fragment).into_bytes()),
}
}
impl Syntax { impl Syntax {
// buffer, grammar, config, grammars, sync_timeout? // buffer, grammar, config, grammars, sync_timeout?
pub fn new( pub fn new(
@@ -272,7 +385,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,
@@ -323,33 +437,31 @@ impl Syntax {
/// Iterate over the highlighted regions for a given slice of source code. /// Iterate over the highlighted regions for a given slice of source code.
pub fn highlight_iter<'a>( pub fn highlight_iter<'a>(
&self, &'a self,
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 tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) }; 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 = 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 = &self.config.query;
let config_ref = let config_ref = self.config.as_ref();
unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
// TODO: if reusing cursors this might need resetting // if reusing cursors & no range this resets to whole range
if let Some(range) = &range { cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
cursor_ref.set_byte_range(range.start, range.end);
}
let captures = cursor_ref let captures = cursor_ref
.captures(query_ref, tree_ref.root_node(), move |n: Node| { .captures(query_ref, tree_ref.root_node(), RopeProvider(source))
// &source[n.byte_range()]
node_to_bytes(n, source)
})
.peekable(); .peekable();
// manually craft the root layer based on the existing tree // manually craft the root layer based on the existing tree
@@ -407,6 +519,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
@@ -414,12 +527,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 }
@@ -435,8 +542,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
@@ -460,42 +567,7 @@ impl LanguageLayer {
self.tree.as_ref(), self.tree.as_ref(),
) )
.ok_or(Error::Cancelled)?; .ok_or(Error::Cancelled)?;
// unsafe { syntax.parser.set_cancellation_flag(None) };
// let mut cursor = syntax.cursors.pop().unwrap_or_else(QueryCursor::new);
// Process combined injections. (ERB, EJS, etc https://github.com/tree-sitter/tree-sitter/pull/526)
// if let Some(combined_injections_query) = &config.combined_injections_query {
// let mut injections_by_pattern_index =
// vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
// let matches =
// cursor.matches(combined_injections_query, tree.root_node(), |n: Node| {
// // &source[n.byte_range()]
// node_to_bytes(n, source)
// });
// for mat in matches {
// let entry = &mut injections_by_pattern_index[mat.pattern_index];
// let (language_name, content_node, include_children) =
// injection_for_match(config, combined_injections_query, &mat, source);
// if language_name.is_some() {
// entry.0 = language_name;
// }
// if let Some(content_node) = content_node {
// entry.1.push(content_node);
// }
// entry.2 = include_children;
// }
// for (lang_name, content_nodes, includes_children) in injections_by_pattern_index {
// if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
// if let Some(next_config) = (injection_callback)(lang_name) {
// let ranges =
// Self::intersect_ranges(&ranges, &content_nodes, includes_children);
// if !ranges.is_empty() {
// queue.push((next_config, depth + 1, ranges));
// }
// }
// }
// }
// }
self.tree = Some(tree) self.tree = Some(tree)
} }
Ok(()) Ok(())
@@ -507,7 +579,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();
@@ -530,9 +601,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 {
@@ -550,9 +622,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);
@@ -576,8 +646,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;
@@ -605,8 +673,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;
@@ -629,8 +695,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(
@@ -687,7 +755,7 @@ use std::sync::atomic::{AtomicUsize, Ordering};
use std::{iter, mem, ops, str, usize}; use std::{iter, mem, ops, str, usize};
use tree_sitter::{ use tree_sitter::{
Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError, Language as Grammar, Node, Parser, Point, Query, QueryCaptures, QueryCursor, QueryError,
QueryMatch, Range, Tree, QueryMatch, Range, TextProvider, Tree,
}; };
const CANCELLATION_CHECK_INTERVAL: usize = 100; const CANCELLATION_CHECK_INTERVAL: usize = 100;
@@ -715,13 +783,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>,
@@ -745,7 +814,8 @@ struct LocalScope<'a> {
local_defs: Vec<LocalDef<'a>>, local_defs: Vec<LocalDef<'a>>,
} }
struct HighlightIter<'a, 'tree: 'a, F> #[derive(Debug)]
struct HighlightIter<'a, F>
where where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{ {
@@ -753,16 +823,41 @@ where
byte_offset: usize, byte_offset: usize,
injection_callback: F, injection_callback: F,
cancellation_flag: Option<&'a AtomicUsize>, cancellation_flag: Option<&'a AtomicUsize>,
layers: Vec<HighlightIterLayer<'a, 'tree>>, layers: Vec<HighlightIterLayer<'a>>,
iter_count: usize, iter_count: usize,
next_event: Option<HighlightEvent>, next_event: Option<HighlightEvent>,
last_highlight_range: Option<(usize, usize, usize)>, last_highlight_range: Option<(usize, usize, usize)>,
} }
struct HighlightIterLayer<'a, 'tree: 'a> { // Adapter to convert rope chunks to bytes
struct ChunksBytes<'a> {
chunks: ropey::iter::Chunks<'a>,
}
impl<'a> Iterator for ChunksBytes<'a> {
type Item = &'a [u8];
fn next(&mut self) -> Option<Self::Item> {
self.chunks.next().map(str::as_bytes)
}
}
struct RopeProvider<'a>(RopeSlice<'a>);
impl<'a> TextProvider<'a> for RopeProvider<'a> {
type I = ChunksBytes<'a>;
fn text(&mut self, node: Node) -> Self::I {
let start_char = self.0.byte_to_char(node.start_byte());
let end_char = self.0.byte_to_char(node.end_byte());
let fragment = self.0.slice(start_char..end_char);
ChunksBytes {
chunks: fragment.chunks(),
}
}
}
struct HighlightIterLayer<'a> {
_tree: Option<Tree>, _tree: Option<Tree>,
cursor: QueryCursor, cursor: QueryCursor,
captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>, captures: iter::Peekable<QueryCaptures<'a, 'a, RopeProvider<'a>>>,
config: &'a HighlightConfiguration, config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>, highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>, scope_stack: Vec<LocalScope<'a>>,
@@ -770,6 +865,12 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
depth: usize, depth: usize,
} }
impl<'a> fmt::Debug for HighlightIterLayer<'a> {
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.
@@ -866,7 +967,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,
@@ -899,17 +1000,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('.') {
@@ -925,11 +1029,14 @@ impl HighlightConfiguration {
} }
} }
best_index.map(Highlight) best_index.map(Highlight)
})); })
.collect();
self.highlight_indices.store(Arc::new(indices));
} }
} }
impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> { impl<'a> HighlightIterLayer<'a> {
/// Create a new 'layer' of highlighting for this document. /// Create a new 'layer' of highlighting for this document.
/// ///
/// In the even that the new layer contains "combined injections" (injections where multiple /// In the even that the new layer contains "combined injections" (injections where multiple
@@ -986,10 +1093,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
let matches = cursor.matches( let matches = cursor.matches(
combined_injections_query, combined_injections_query,
tree.root_node(), tree.root_node(),
|n: Node| { RopeProvider(source),
// &source[n.byte_range()]
node_to_bytes(n, source)
},
); );
for mat in matches { for mat in matches {
let entry = &mut injections_by_pattern_index[mat.pattern_index]; let entry = &mut injections_by_pattern_index[mat.pattern_index];
@@ -1036,10 +1140,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
let cursor_ref = let cursor_ref =
unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) }; unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
let captures = cursor_ref let captures = cursor_ref
.captures(&config.query, tree_ref.root_node(), move |n: Node| { .captures(&config.query, tree_ref.root_node(), RopeProvider(source))
// &source[n.byte_range()]
node_to_bytes(n, source)
})
.peekable(); .peekable();
result.push(HighlightIterLayer { result.push(HighlightIterLayer {
@@ -1193,7 +1294,7 @@ impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
} }
} }
impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F> impl<'a, F> HighlightIter<'a, F>
where where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{ {
@@ -1244,7 +1345,7 @@ where
} }
} }
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) { fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
if let Some(sort_key) = layer.sort_key() { if let Some(sort_key) = layer.sort_key() {
let mut i = 1; let mut i = 1;
while i < self.layers.len() { while i < self.layers.len() {
@@ -1263,7 +1364,7 @@ where
} }
} }
impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F> impl<'a, F> Iterator for HighlightIter<'a, F>
where where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a, F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{ {
@@ -1406,7 +1507,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;
@@ -1504,7 +1604,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.
@@ -1528,7 +1628,7 @@ where
fn injection_for_match<'a>( fn injection_for_match<'a>(
config: &HighlightConfiguration, config: &HighlightConfiguration,
query: &'a Query, query: &'a Query,
query_match: &QueryMatch<'a>, query_match: &QueryMatch<'a, 'a>,
source: RopeSlice<'a>, source: RopeSlice<'a>,
) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) { ) -> (Option<Cow<'a, str>>, Option<Node<'a>>, bool) {
let content_capture_index = config.injection_content_capture_index; let content_capture_index = config.injection_content_capture_index;
@@ -1570,16 +1670,155 @@ 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(); // 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>,
} }
#[test] /// Merge a list of spans into the highlight event stream.
fn test_parser() { 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]
fn test_parser() {
let highlight_names: Vec<String> = [ let highlight_names: Vec<String> = [
"attribute", "attribute",
"constant", "constant",
@@ -1605,8 +1844,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",
@@ -1643,19 +1882,18 @@ fn test_parser() {
let struct_node = root.child(0).unwrap(); let struct_node = root.child(0).unwrap();
assert_eq!(struct_node.kind(), "struct_item"); assert_eq!(struct_node.kind(), "struct_item");
} }
#[test] #[test]
fn test_input_edits() { fn test_input_edits() {
use crate::State;
use tree_sitter::InputEdit; use tree_sitter::InputEdit;
let mut state = State::new("hello world!\ntest 123".into()); let doc = Rope::from("hello world!\ntest 123");
let transaction = Transaction::change( let transaction = Transaction::change(
&state.doc, &doc,
vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(), vec![(6, 11, Some("test".into())), (12, 17, None)].into_iter(),
); );
let edits = LanguageLayer::generate_edits(state.doc.slice(..), transaction.changes()); let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes());
// transaction.apply(&mut state); // transaction.apply(&mut state);
assert_eq!( assert_eq!(
@@ -1681,13 +1919,13 @@ fn test_input_edits() {
); );
// Testing with the official example from tree-sitter // Testing with the official example from tree-sitter
let mut state = State::new("fn test() {}".into()); let mut doc = Rope::from("fn test() {}");
let transaction = let transaction =
Transaction::change(&state.doc, vec![(8, 8, Some("a: u32".into()))].into_iter()); Transaction::change(&doc, vec![(8, 8, Some("a: u32".into()))].into_iter());
let edits = LanguageLayer::generate_edits(state.doc.slice(..), transaction.changes()); let edits = LanguageLayer::generate_edits(doc.slice(..), transaction.changes());
transaction.apply(&mut state.doc); transaction.apply(&mut doc);
assert_eq!(state.doc, "fn test(a: u32) {}"); assert_eq!(doc, "fn test(a: u32) {}");
assert_eq!( assert_eq!(
edits, edits,
&[InputEdit { &[InputEdit {
@@ -1699,4 +1937,15 @@ fn test_input_edits() {
new_end_position: Point { row: 0, column: 14 } new_end_position: Point { row: 0, column: 14 }
}] }]
); );
}
#[test]
fn test_load_runtime_file() {
// Test to make sure we can load some data from the runtime directory.
let contents = load_runtime_file("rust", "indents.toml").unwrap();
assert!(!contents.is_empty());
let results = load_runtime_file("rust", "does-not-exist");
assert!(results.is_err());
}
} }

View File

@@ -0,0 +1,372 @@
use std::fmt::Display;
use ropey::RopeSlice;
use tree_sitter::{Node, QueryCursor};
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::next_grapheme_boundary;
use crate::movement::Direction;
use crate::surround;
use crate::syntax::LanguageConfiguration;
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,
}
impl Display for TextObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Around => "around",
Self::Inside => "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)
}
/// Transform the given range to select text objects based on tree-sitter.
/// `object_name` is a query capture base name like "function", "class", etc.
/// `slice_tree` is the tree-sitter node corresponding to given text slice.
pub fn textobject_treesitter(
slice: RopeSlice,
range: Range,
textobject: TextObject,
object_name: &str,
slice_tree: Node,
lang_config: &LanguageConfiguration,
_count: usize,
) -> Range {
let get_range = move || -> Option<Range> {
let byte_pos = slice.char_to_byte(range.cursor(slice));
let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
let mut cursor = QueryCursor::new();
let node = lang_config
.textobject_query()?
.capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
.filter(|node| node.byte_range().contains(&byte_pos))
.min_by_key(|node| node.byte_range().len())?;
let len = slice.len_bytes();
let start_byte = node.start_byte();
let end_byte = node.end_byte();
if start_byte >= len || end_byte >= len {
return None;
}
let start_char = slice.byte_to_char(start_byte);
let end_char = slice.byte_to_char(end_byte);
Some(Range::new(start_char, end_char))
};
get_range().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,
@@ -90,7 +90,8 @@ impl ChangeSet {
return; return;
} }
self.len_after += fragment.len(); // Avoiding std::str::len() to account for UTF-8 characters.
self.len_after += fragment.chars().count();
let new_last = match self.changes.as_mut_slice() { let new_last = match self.changes.as_mut_slice() {
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => { [.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
@@ -124,13 +125,16 @@ impl ChangeSet {
/// In other words, If `this` goes `docA` → `docB` and `other` represents `docB` → `docC`, the /// In other words, If `this` goes `docA` → `docB` and `other` represents `docB` → `docC`, the
/// returned value will represent the change `docA` → `docC`. /// returned value will represent the change `docA` → `docC`.
pub fn compose(self, other: Self) -> Self { pub fn compose(self, other: Self) -> Self {
debug_assert!(self.len_after == other.len); assert!(self.len_after == other.len);
// composing fails in weird ways if one of the sets is empty // composing fails in weird ways if one of the sets is empty
// a: [] len: 0 len_after: 1 | b: [Insert(Tendril<UTF8>(inline: "\n")), Retain(1)] len 1 // a: [] len: 0 len_after: 1 | b: [Insert(Tendril<UTF8>(inline: "\n")), Retain(1)] len 1
if self.changes.is_empty() { if self.changes.is_empty() {
return other; return other;
} }
if other.changes.is_empty() {
return self;
}
let len = self.changes.len(); let len = self.changes.len();
@@ -162,7 +166,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);
@@ -192,15 +196,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 => {
@@ -272,7 +277,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::*;
@@ -415,7 +419,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into /// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction. /// a single transaction.
#[derive(Debug, Clone)] #[derive(Debug, Default, Clone)]
pub struct Transaction { pub struct Transaction {
changes: ChangeSet, changes: ChangeSet,
selection: Option<Selection>, selection: Option<Selection>,
@@ -464,6 +468,13 @@ impl Transaction {
} }
} }
pub fn compose(mut self, other: Self) -> Self {
self.changes = self.changes.compose(other.changes);
// Other selection takes precedence
self.selection = other.selection;
self
}
pub fn with_selection(mut self, selection: Selection) -> Self { pub fn with_selection(mut self, selection: Selection) -> Self {
self.selection = Some(selection); self.selection = Some(selection);
self self
@@ -472,11 +483,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.
@@ -579,6 +592,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() {
@@ -597,7 +611,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,
}; };
@@ -608,7 +622,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]
@@ -685,21 +699,21 @@ mod test {
#[test] #[test]
fn transaction_change() { fn transaction_change() {
let mut state = State::new("hello world!\ntest 123".into()); let mut doc = Rope::from("hello world!\ntest 123");
let transaction = Transaction::change( let transaction = Transaction::change(
&state.doc, &doc,
// (1, 1, None) is a useless 0-width delete // (1, 1, None) is a useless 0-width delete
vec![(1, 1, None), (6, 11, Some("void".into())), (12, 17, None)].into_iter(), vec![(1, 1, None), (6, 11, Some("void".into())), (12, 17, None)].into_iter(),
); );
transaction.apply(&mut state.doc); transaction.apply(&mut doc);
assert_eq!(state.doc, Rope::from_str("hello void! 123")); assert_eq!(doc, Rope::from_str("hello void! 123"));
} }
#[test] #[test]
fn changes_iter() { fn changes_iter() {
let mut state = State::new("hello world!\ntest 123".into()); let doc = Rope::from("hello world!\ntest 123");
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(&doc, changes.clone().into_iter());
assert_eq!(transaction.changes_iter().collect::<Vec<_>>(), changes); assert_eq!(transaction.changes_iter().collect::<Vec<_>>(), changes);
} }
@@ -729,7 +743,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)
@@ -744,7 +758,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());
@@ -754,4 +768,21 @@ mod test {
use Operation::*; use Operation::*;
assert_eq!(changes.changes, &[Insert("a".into())]); assert_eq!(changes.changes, &[Insert("a".into())]);
} }
#[test]
fn combine_with_utf8() {
const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
let empty = Rope::from("");
let a = ChangeSet::new(&empty);
let mut b = ChangeSet::new(&empty);
b.insert(TEST_CASE.into());
let changes = a.compose(b);
use Operation::*;
assert_eq!(changes.changes, &[Insert(TEST_CASE.into())]);
assert_eq!(changes.len_after, TEST_CASE.chars().count());
}
} }

View File

@@ -1,29 +1,27 @@
[package] [package]
name = "helix-lsp" name = "helix-lsp"
version = "0.1.0" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2021"
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.5", 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.90", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
log = "~0.4" tokio = { version = "1.12", 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,44 @@ use crate::{
Call, Error, OffsetEncoding, Result, Call, Error, OffsetEncoding, Result,
}; };
use helix_core::{ChangeSet, Rope}; use helix_core::{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},
Arc,
};
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},
Notify, OnceCell,
},
}; };
#[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(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
pub request_counter: AtomicU64,
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>)> { #[allow(clippy::type_complexity)]
pub fn start(
cmd: &str,
args: &[String],
config: Option<Value>,
id: usize,
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
let process = Command::new(cmd) let process = Command::new(cmd)
.args(args) .args(args)
.stdin(Stdio::piped()) .stdin(Stdio::piped())
@@ -43,40 +50,31 @@ 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, initialize_notify) =
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: OnceCell::new(),
capabilities: None,
// diagnostics: HashMap::new(),
offset_encoding: OffsetEncoding::Utf8, offset_encoding: OffsetEncoding::Utf8,
config,
}; };
// TODO: async client.initialize() Ok((client, server_rx, initialize_notify))
// maybe use an arc<atomic> flag }
Ok((client, incoming)) pub fn id(&self) -> usize {
self.id
} }
fn next_request_id(&self) -> jsonrpc::Id { fn next_request_id(&self) -> jsonrpc::Id {
@@ -95,9 +93,13 @@ impl Client {
} }
} }
pub fn is_initialized(&self) -> bool {
self.capabilities.get().is_some()
}
pub fn capabilities(&self) -> &lsp::ServerCapabilities { pub fn capabilities(&self) -> &lsp::ServerCapabilities {
self.capabilities self.capabilities
.as_ref() .get()
.expect("language server not yet initialized!") .expect("language server not yet initialized!")
} }
@@ -106,7 +108,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 +120,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,20 +145,18 @@ 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; // TODO: specifiable timeout, delay other calls until initialize success
use tokio::time::timeout; timeout(Duration::from_secs(20), 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)?
} }
} }
@@ -165,7 +168,7 @@ impl Client {
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 +179,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 +188,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 +211,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(&self) -> Result<lsp::InitializeResult> {
// 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 +257,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()
@@ -255,14 +291,7 @@ impl Client {
locale: None, // TODO locale: None, // TODO
}; };
let response = self.request::<lsp::request::Initialize>(params).await?; self.request::<lsp::request::Initialize>(params).await
self.capabilities = Some(response.capabilities);
// next up, notify<initialized>
self.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await?;
Ok(())
} }
pub async fn shutdown(&self) -> Result<()> { pub async fn shutdown(&self) -> Result<()> {
@@ -273,6 +302,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
// ------------------------------------------------------------------------------------------- // -------------------------------------------------------------------------------------------
@@ -315,7 +359,6 @@ impl Client {
// //
// Calculation is therefore a bunch trickier. // Calculation is therefore a bunch trickier.
// TODO: stolen from syntax.rs, share
use helix_core::RopeSlice; use helix_core::RopeSlice;
fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position { fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position {
let lsp::Position { let lsp::Position {
@@ -323,8 +366,14 @@ 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() {
// LSP only considers \n, \r or \r\n as line endings
if ch == '\n' || ch == '\r' {
// consume a \r\n
if ch == '\r' && chars.peek() == Some(&'\n') {
chars.next();
}
line += 1; line += 1;
character = 0; character = 0;
} else { } else {
@@ -399,7 +448,7 @@ impl Client {
) -> Option<impl Future<Output = Result<()>>> { ) -> Option<impl Future<Output = Result<()>>> {
// figure out what kind of sync the server supports // figure out what kind of sync the server supports
let capabilities = self.capabilities.as_ref().unwrap(); let capabilities = self.capabilities.get().unwrap();
let sync_capabilities = match capabilities.text_document_sync { let sync_capabilities = match capabilities.text_document_sync {
Some(lsp::TextDocumentSyncCapability::Kind(kind)) Some(lsp::TextDocumentSyncCapability::Kind(kind))
@@ -417,7 +466,7 @@ impl Client {
// range = None -> whole document // range = None -> whole document
range: None, //Some(Range) range: None, //Some(Range)
range_length: None, // u64 apparently deprecated range_length: None, // u64 apparently deprecated
text: "".to_string(), text: new_text.to_string(),
}] }]
} }
lsp::TextDocumentSyncKind::Incremental => { lsp::TextDocumentSyncKind::Incremental => {
@@ -445,12 +494,12 @@ impl Client {
// will_save / will_save_wait_until // will_save / will_save_wait_until
pub async fn text_document_did_save( pub fn text_document_did_save(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
text: &Rope, text: &Rope,
) -> Result<()> { ) -> Option<impl Future<Output = Result<()>>> {
let capabilities = self.capabilities.as_ref().unwrap(); let capabilities = self.capabilities.get().unwrap();
let include_text = match &capabilities.text_document_sync { let include_text = match &capabilities.text_document_sync {
Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions { Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
@@ -462,23 +511,25 @@ impl Client {
include_text, include_text,
}) => include_text.unwrap_or(false), }) => include_text.unwrap_or(false),
// Supported(false) // Supported(false)
_ => return Ok(()), _ => return None,
}, },
// unsupported // unsupported
_ => return Ok(()), _ => return None,
}; };
self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams { Some(self.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document, text_document,
text: include_text.then(|| text.into()), text: include_text.then(|| text.into()),
}) },
.await ))
} }
pub fn completion( pub fn completion(
&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 +538,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 +553,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 +572,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
}; };
@@ -541,32 +588,35 @@ impl Client {
// formatting // formatting
pub async fn text_document_formatting( pub fn text_document_formatting(
&self, &self,
text_document: lsp::TextDocumentIdentifier, text_document: lsp::TextDocumentIdentifier,
options: lsp::FormattingOptions, options: lsp::FormattingOptions,
) -> anyhow::Result<Vec<lsp::TextEdit>> { work_done_token: Option<lsp::ProgressToken>,
let capabilities = self.capabilities.as_ref().unwrap(); ) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
let capabilities = self.capabilities.get().unwrap();
// check if we're able to format // check if we're able to format
match capabilities.document_formatting_provider { match capabilities.document_formatting_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (), Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
// None | Some(false) // None | Some(false)
_ => return Ok(Vec::new()), _ => return None,
}; };
// TODO: return err::unavailable so we can fall back to tree sitter formatting // TODO: return err::unavailable so we can fall back to tree sitter formatting
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 request = self.call::<lsp::request::Formatting>(params);
Some(async move {
let json = request.await?;
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default()) Ok(response.unwrap_or_default())
})
} }
pub async fn text_document_range_formatting( pub async fn text_document_range_formatting(
@@ -574,8 +624,9 @@ 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.get().unwrap();
// check if we're able to format // check if we're able to format
match capabilities.document_range_formatting_provider { match capabilities.document_range_formatting_provider {
@@ -589,9 +640,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 +659,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 +679,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 +724,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 +732,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,26 +1,30 @@
mod client; mod client;
mod select_all;
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 {
@@ -28,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),
} }
@@ -48,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,
@@ -88,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,
@@ -99,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>,
@@ -114,21 +167,71 @@ 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)]
pub enum Notification { pub enum Notification {
// we inject this notification to signal the LSP is ready
Initialized,
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 {
@@ -136,6 +239,7 @@ impl Notification {
use lsp::notification::Notification as _; use lsp::notification::Notification as _;
let notification = match method { let notification = match method {
lsp::notification::Initialized::METHOD => Self::Initialized,
lsp::notification::PublishDiagnostics::METHOD => { lsp::notification::PublishDiagnostics::METHOD => {
let params: lsp::PublishDiagnosticsParams = params let params: lsp::PublishDiagnosticsParams = params
.parse() .parse()
@@ -146,17 +250,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;
@@ -167,16 +274,12 @@ impl Notification {
} }
} }
pub use jsonrpc::Call; #[derive(Debug)]
type LanguageId = String;
use crate::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 {
@@ -189,64 +292,178 @@ 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(&self, id: usize) -> Option<&Client> {
// TODO: propagate the error self.inner
if let Some(config) = &language_config.language_server { .values()
// avoid borrow issues .find(|(client_id, _)| client_id == &id)
let inner = &mut self.inner; .map(|(_, client)| client.as_ref())
let s_incoming = &self.incoming;
let language_server = inner
.entry(language_config.scope.clone()) // can't use entry with Borrow keys: https://github.com/rust-lang/rfcs/pull/1769
.or_insert_with(|| {
// TODO: lookup defaults for id (name, args)
// initialize a new client
let (mut client, incoming) =
Client::start(&config.command, &config.args).ok()?;
// TODO: run this async without blocking
futures_executor::block_on(client.initialize()).unwrap();
s_incoming.push(UnboundedReceiverStream::new(incoming));
Some(Arc::new(client))
})
.clone();
return language_server;
} }
None pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
let config = match &language_config.language_server {
Some(config) => config,
None => return Err(Error::LspNotDefined),
};
match self.inner.entry(language_config.scope.clone()) {
Entry::Occupied(entry) => Ok(entry.get().1.clone()),
Entry::Vacant(entry) => {
// initialize a new client
let id = self.counter.fetch_add(1, Ordering::Relaxed);
let (client, incoming, initialize_notify) = Client::start(
&config.command,
&config.args,
language_config.config.clone(),
id,
)?;
self.incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client);
// Initialize the client asynchronously
let _client = client.clone();
tokio::spawn(async move {
use futures_util::TryFutureExt;
let value = _client
.capabilities
.get_or_try_init(|| {
_client
.initialize()
.map_ok(|response| response.capabilities)
})
.await;
value.expect("failed to initialize capabilities");
// next up, notify<initialized>
_client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await
.unwrap();
initialize_notify.notify_one();
});
entry.insert((id, client.clone()));
Ok(client)
}
}
}
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().map(|(_, client)| client)
} }
} }
// REGISTRY = HashMap<LanguageId, Lazy/OnceCell<Arc<RwLock<Client>>> #[derive(Debug)]
// spawn one server per language type, need to spawn one per workspace if server doesn't support pub enum ProgressStatus {
// workspaces Created,
// Started(lsp::WorkDoneProgress),
// could also be a client per root dir }
//
// storing a copy of Option<Arc<RwLock<Client>>> on Document would make the LSP client easily impl ProgressStatus {
// accessible during edit/save callbacks pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
// match &self {
// the event loop needs to process all incoming streams, maybe we can just have that be a separate ProgressStatus::Created => None,
// task that's continually running and store the state on the client, then use read lock to ProgressStatus::Started(progress) => Some(progress),
// retrieve data during render }
// -> PROBLEM: how do you trigger an update on the editor side when data updates? }
// }
// -> The data updates should pull all events until we run out so we don't frequently re-render
// #[derive(Default, Debug)]
// /// Acts as a container for progress reported by language servers. Each server
// v2: /// has a unique id assigned at creation through [`Registry`]. This id is then used
// /// to store the progress in this map.
// there should be a registry of lsp clients, one per language type (or workspace). pub struct LspProgressMap(HashMap<usize, HashMap<lsp::ProgressToken, ProgressStatus>>);
// the clients should lazy init on first access
// the client.initialize() should be called async and we buffer any requests until that completes impl LspProgressMap {
// there needs to be a way to process incoming lsp messages from all clients. pub fn new() -> Self {
// -> notifications need to be dispatched to wherever Self::default()
// -> requests need to generate a reply and travel back to the same lsp! }
/// 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))
}
}
#[cfg(test)]
mod tests {
use super::{lsp, util::*, OffsetEncoding};
use helix_core::Rope;
#[test]
fn converts_lsp_pos_to_pos() {
macro_rules! test_case {
($doc:expr, ($x:expr, $y:expr) => $want:expr) => {
let doc = Rope::from($doc);
let pos = lsp::Position::new($x, $y);
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf16));
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf8))
};
}
test_case!("", (0, 0) => Some(0));
test_case!("", (0, 1) => None);
test_case!("", (1, 0) => None);
test_case!("\n\n", (0, 0) => Some(0));
test_case!("\n\n", (1, 0) => Some(1));
test_case!("\n\n", (1, 1) => Some(2));
test_case!("\n\n", (2, 0) => Some(2));
test_case!("\n\n", (3, 0) => None);
test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
test_case!("test\n\n\n\ncase", (4, 5) => None);
test_case!("", (u32::MAX, u32::MAX) => None);
}
}

View File

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

View File

@@ -1,19 +1,18 @@
use std::collections::HashMap; use crate::{Error, 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, Notify,
},
}; };
#[derive(Debug)] #[derive(Debug)]
@@ -26,153 +25,185 @@ 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>,
Arc<Notify>,
) {
let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel();
let notify = Arc::new(Notify::new());
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);
(rx, tx) tokio::spawn(Self::recv(
transport.clone(),
server_stdout,
client_tx.clone(),
));
tokio::spawn(Self::err(transport.clone(), server_stderr));
tokio::spawn(Self::send(
transport,
server_stdin,
client_tx,
client_rx,
notify.clone(),
));
(rx, tx, notify)
} }
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 if reader.read_line(buffer).await? == 0 {
reader.read_line(&mut header).await?; return Err(Error::StreamClosed);
let header = header.trim(); };
if header.is_empty() { // debug!("<- header {:?}", buffer);
if buffer == "\r\n" {
// look for an empty CRLF line
break; break;
} }
let parts: Vec<&str> = header.split(": ").collect(); let header = buffer.trim();
if parts.len() != 2 {
return Err(std::io::Error::new( let parts = header.split_once(": ");
std::io::ErrorKind::Other,
"Failed to parse header", match parts {
)); Some(("Content-Length", value)) => {
content_length = Some(value.parse().context("invalid content length")?);
}
Some((_, _)) => {}
None => {
// Workaround: Some non-conformant language servers will output logging and other garbage
// into the same stream as JSON-RPC messages. This can also happen from shell scripts that spawn
// the server. Skip such lines and log a warning.
// warn!("Failed to parse header: {:?}", header);
}
} }
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); if err.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed);
};
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 +217,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,29 +233,132 @@ 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 {
match Self::recv_server_message(&mut server_stdout, &mut recv_buffer).await {
Ok(msg) => {
match transport.process_server_message(&client_tx, msg).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
break;
}
};
}
Err(err) => {
error!("err: <- {:?}", err);
break;
}
}
}
}
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;
}
}
}
}
async fn send(
transport: Arc<Self>,
mut server_stdin: BufWriter<ChildStdin>,
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
mut client_rx: UnboundedReceiver<Payload>,
initialize_notify: Arc<Notify>,
) {
let mut pending_messages: Vec<Payload> = Vec::new();
let mut is_pending = true;
// Determine if a message is allowed to be sent early
fn is_initialize(payload: &Payload) -> bool {
use lsp_types::{
notification::{Initialized, Notification},
request::{Initialize, Request},
};
match payload {
Payload::Request {
value: jsonrpc::MethodCall { method, .. },
..
} if method == Initialize::METHOD => true,
Payload::Notification(jsonrpc::Notification { method, .. })
if method == Initialized::METHOD =>
{
true
}
_ => false,
}
}
// TODO: events that use capabilities need to do the right thing
loop { loop {
tokio::select! { tokio::select! {
// client -> server biased;
msg = self.outgoing.recv() => { _ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe
if msg.is_none() { // server successfully initialized
is_pending = false;
use lsp_types::notification::Notification;
// Hack: inject an initialized notification so we trigger code that needs to happen after init
let notification = ServerMessage::Call(jsonrpc::Call::Notification(jsonrpc::Notification {
jsonrpc: None,
method: lsp_types::notification::Initialized::METHOD.to_string(),
params: jsonrpc::Params::None,
}));
match transport.process_server_message(&client_tx, notification).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
}
}
// drain the pending queue and send payloads to server
for msg in pending_messages.drain(..) {
log::info!("Draining pending message {:?}", msg);
match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
}
}
}
}
msg = client_rx.recv() => {
if let Some(msg) = msg {
if is_pending && !is_initialize(&msg) {
// ignore notifications
if let Payload::Notification(_) = msg {
continue;
}
log::info!("Language server not initialized, delaying request");
pending_messages.push(msg);
} else {
match transport.send_payload_to_server(&mut server_stdin, msg).await {
Ok(_) => {}
Err(err) => {
error!("err: <- {:?}", err);
}
}
}
} else {
// channel closed
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);
break;
}
let msg = msg.unwrap();
self.recv_msg(msg).await.unwrap();
}
_msg = Self::err(&mut self.stderr) => {}
} }
} }
} }

View File

@@ -1,16 +1,21 @@
[package] [package]
name = "helix-syntax" name = "helix-syntax"
version = "0.1.0" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2021"
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.20"
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,84 +1,151 @@
use std::path::PathBuf; use anyhow::{anyhow, Context, Result};
use std::{env, fs}; use std::fs;
use std::time::SystemTime;
use std::{
path::{Path, PathBuf},
process::Command,
};
use std::sync::mpsc::channel; use std::sync::mpsc::channel;
fn get_opt_level() -> u32 { fn collect_tree_sitter_dirs(ignore: &[String]) -> Result<Vec<String>> {
env::var("OPT_LEVEL").unwrap().parse::<u32>().unwrap()
}
fn get_debug() -> bool {
env::var("DEBUG").unwrap() == "true"
}
fn collect_tree_sitter_dirs(ignore: &[String]) -> Vec<String> {
let mut dirs = Vec::new(); 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) Ok(dirs)
} }
fn build_c(files: Vec<String>, language: &str) { #[cfg(unix)]
let mut build = cc::Build::new(); const DYLIB_EXTENSION: &str = "so";
for file in files {
build #[cfg(windows)]
.file(&file) const DYLIB_EXTENSION: &str = "dll";
.include(PathBuf::from(file).parent().unwrap())
.pic(true) fn build_library(src_path: &Path, language: &str) -> Result<()> {
.opt_level(get_opt_level()) let header_path = src_path;
.debug(get_debug()) // let grammar_path = src_path.join("grammar.json");
.warnings(false) let parser_path = src_path.join("parser.c");
.flag_if_supported("-std=c99"); let mut scanner_path = src_path.join("scanner.c");
let scanner_path = if scanner_path.exists() {
Some(scanner_path)
} else {
scanner_path.set_extension("cc");
if scanner_path.exists() {
Some(scanner_path)
} else {
None
} }
build.compile(&format!("tree-sitter-{}-c", 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")
.arg("/utf-8");
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);
if cfg!(all(unix, not(target_os = "macos"))) {
command.arg("-Wl,-z,relro,-z,now");
}
}
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 build_cpp(files: Vec<String>, language: &str) { fn mtime(path: &Path) -> Result<SystemTime> {
let mut build = cc::Build::new(); Ok(fs::metadata(path)?.modified()?)
for file in files {
build
.file(&file)
.include(PathBuf::from(file).parent().unwrap())
.pic(true)
.opt_level(get_opt_level())
.debug(get_debug())
.warnings(false)
.cpp(true);
}
build.compile(&format!("tree-sitter-{}-cpp", language));
} }
fn build_dir(dir: &str, language: &str) { fn build_dir(dir: &str, language: &str) {
@@ -91,27 +158,27 @@ fn build_dir(dir: &str, language: &str) {
.is_none() .is_none()
{ {
eprintln!( eprintln!(
"The directory {} is empty, did you use 'git clone --recursive'?", "The directory {} is empty, you probably need to use 'git submodule update --init --recursive'?",
dir dir
); );
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(),
".DS_Store".to_string(), "tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34
"tree-sitter-ocaml".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
@@ -122,7 +189,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
@@ -135,4 +202,6 @@ fn main() {
build_dir("tree-sitter-typescript/tsx", "tsx"); build_dir("tree-sitter-typescript/tsx", "tsx");
build_dir("tree-sitter-typescript/typescript", "typescript"); build_dir("tree-sitter-typescript/typescript", "typescript");
build_dir("tree-sitter-ocaml/ocaml", "ocaml");
build_dir("tree-sitter-ocaml/interface", "ocaml-interface")
} }

View File

@@ -1,91 +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)] pub fn get_language(runtime_path: &std::path::Path, name: &str) -> Result<Language> {
#[serde(rename_all = "lowercase")] let name = name.to_ascii_lowercase();
pub enum Lang { let mut library_path = runtime_path.join("grammars").join(&name);
$( // TODO: duplicated under build
$camel, library_path.set_extension(DYLIB_EXTENSION);
)*
} let library = unsafe { Library::new(&library_path) }
.with_context(|| format!("Error opening dynamic library {:?}", &library_path))?;
let language_fn_name = format!("tree_sitter_{}", replace_dashes_with_underscores(&name));
let language = unsafe {
let language_fn: Symbol<unsafe extern "C" fn() -> Language> = library
.get(language_fn_name.as_bytes())
.with_context(|| format!("Failed to load symbol {}", language_fn_name))?;
language_fn()
}; };
std::mem::forget(library);
Ok(language)
} }
#[macro_export]
macro_rules! mk_get_language {
( $( ($camel:ident, $name:ident) ),* ) => {
#[must_use]
pub fn get_language(lang: Lang) -> Language {
unsafe {
match lang {
$(
Lang::$camel => $name(),
)*
}
}
}
};
}
#[macro_export]
macro_rules! mk_get_language_name {
( $( $camel:ident ),* ) => {
#[must_use]
pub const fn get_language_name(lang: Lang) -> &'static str {
match lang {
$(
Lang::$camel => stringify!($camel),
)*
}
}
};
}
#[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),
(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,30 +1,38 @@
[package] [package]
name = "helix-term" name = "helix-term"
version = "0.1.0" version = "0.5.0"
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 = "2021"
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]
[[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.5", path = "../helix-core" }
helix-view = { path = "../helix-view", features = ["term"]} helix-view = { version = "0.5", path = "../helix-view" }
helix-lsp = { path = "../helix-lsp"} helix-lsp = { version = "0.5", 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.22", features = ["event-stream"] }
pico-args = "0.4" 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 }
@@ -36,8 +44,6 @@ log = "0.4"
# File picker # File picker
fuzzy-matcher = "0.3" fuzzy-matcher = "0.3"
ignore = "0.4" ignore = "0.4"
# shellexpand = "2.1"
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 }
@@ -46,3 +52,11 @@ toml = "0.5"
serde_json = "1.0" serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
# ripgrep for global search
grep-regex = "0.1.9"
grep-searcher = "0.1.8"
tokio-stream = "0.1.7"
[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,65 +1,150 @@
use helix_view::{document::Mode, Document, Editor, Theme, View}; use helix_core::{merge_toml_values, 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, warn};
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);
if !args.files.is_empty() { let conf_dir = helix_core::config_dir();
let theme_loader =
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
// load default and user config, and merge both
let def_lang_conf: toml::Value = toml::from_slice(include_bytes!("../../languages.toml"))
.expect("Could not parse built-in languages.toml, something must be very wrong");
let user_lang_conf: Option<toml::Value> = std::fs::read(conf_dir.join("languages.toml"))
.ok()
.map(|raw| toml::from_slice(&raw).expect("Could not parse user languages.toml"));
let lang_conf = match user_lang_conf {
Some(value) => merge_toml_values(def_lang_conf, value),
None => def_lang_conf,
};
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: helix_core::syntax::Configuration = lang_conf
.try_into()
.expect("Could not parse merged (built-in + user) 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.load_tutor {
let path = helix_core::runtime_dir().join("tutor.txt");
editor.open(path, Action::VerticalSplit)?;
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
} else if !args.files.is_empty() {
let first = &args.files[0]; // we know it's not empty
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(".".into())));
} 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 {
editor.open(file, Action::VerticalSplit)?; if file.is_dir() {
return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
));
} else {
editor.open(file.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);
} }
compositor.push(Box::new(ui::EditorView::new())); editor.set_theme(theme);
let mut app = Self { #[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)
@@ -68,11 +153,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,
}; };
@@ -81,44 +166,115 @@ 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 {
self.render();
last_render = Instant::now();
} }
} }
} Some(callback) = self.jobs.futures.next() => {
} self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
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();
} }
Some(callback) = self.jobs.wait_futures.next() => {
self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback);
self.render();
}
_ = &mut self.editor.idle_timer => {
// idle timeout
self.editor.clear_idle_timer();
self.handle_idle_timeout();
}
}
}
}
#[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_idle_timeout(&mut self) {
use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode;
if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
return;
}
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();
if editor_view.completion.is_some() {
return;
}
let mut cx = Context {
register: None,
editor: &mut self.editor,
jobs: &mut self.jobs,
count: None,
callback: None,
on_next_key_callback: None,
};
idle_completion(&mut cx);
self.render();
} }
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
@@ -139,8 +295,13 @@ 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};
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) {
@@ -148,46 +309,81 @@ impl Application {
None => return, None => return,
}; };
// TODO: parse should return Result/Option
match notification { match notification {
Notification::Initialized => {
let language_server =
match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
let docs = self.editor.documents().filter(|doc| {
doc.language_server().map(|server| server.id()) == Some(server_id)
});
// trigger textDocument/didOpen for docs that are already open
for doc in docs {
// TODO: extract and share with editor.open
let language_id = doc
.language()
.and_then(|s| s.split('.').last()) // source.rust
.map(ToOwned::to_owned)
.unwrap_or_default();
tokio::spawn(language_server.text_document_did_open(
doc.url().unwrap(),
doc.version(),
doc.text(),
language_id,
));
}
}
Notification::PublishDiagnostics(params) => { Notification::PublishDiagnostics(params) => {
let path = Some(params.uri.to_file_path().unwrap()); let path = params.uri.to_file_path().unwrap();
let doc = self.editor.document_by_path_mut(&path);
let doc = self if let Some(doc) = doc {
.editor
.documents
.iter_mut()
.find(|(_, doc)| doc.path() == path.as_ref());
if let Some((_, doc)) = doc {
let text = doc.text(); let text = doc.text();
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,
@@ -201,28 +397,127 @@ impl Application {
), ),
// code // code
// source // source
} })
}) })
.collect(); .collect();
doc.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) => {
log::warn!("unhandled window/showMessage: {:?}", params); log::warn!("unhandled window/showMessage: {:?}", params);
} }
Notification::LogMessage(params) => { Notification::LogMessage(params) => {
log::warn!("unhandled window/logMessage: {:?}", params); log::info!("window/logMessage: {:?}", params);
} }
_ => unreachable!(), Notification::ProgressMessage(params)
} if !self
} .compositor
Call::MethodCall(call) => { .has_component(std::any::type_name::<ui::Prompt>()) =>
error!("Method not found {}", call.method); {
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();
let lsp::ProgressParams { token, value } = params;
// self.language_server.reply( 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);
}
}
Notification::ProgressMessage(_params) => {
// do nothing
}
}
}
Call::MethodCall(helix_lsp::jsonrpc::MethodCall {
method, params, id, ..
}) => {
let language_server = match self.editor.language_servers.get_by_id(server_id) {
Some(language_server) => language_server,
None => {
warn!("can't find language server with id `{}`", server_id);
return;
}
};
let call = match MethodCall::parse(&method, params) {
Some(call) => call,
None => {
error!("Method not found {}", method);
// 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)
// Err(helix_lsp::jsonrpc::Error { // Err(helix_lsp::jsonrpc::Error {
@@ -231,34 +526,78 @@ impl Application {
// data: None, // data: None,
// }), // }),
// ); // );
return;
}
};
match call {
MethodCall::WorkDoneProgressCreate(params) => {
self.lsp_progress.create(server_id, params.token);
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();
let spinner = editor_view.spinners_mut().get_or_create(server_id);
if spinner.is_stopped() {
spinner.start();
}
tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null)));
}
}
} }
e => unreachable!("{:?}", e), e => unreachable!("{:?}", e),
} }
} }
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")?;
// Ignore errors on disabling, this might trigger on windows if we call
// disable without calling enable previously
let _ = 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(())
} }

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

@@ -0,0 +1,55 @@
use anyhow::{Error, Result};
use std::path::PathBuf;
#[derive(Default)]
pub struct Args {
pub display_help: bool,
pub display_version: bool,
pub load_tutor: 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,
"--tutor" => args.load_tutor = 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,27 +142,39 @@ impl Compositor {
} }
pub fn render(&mut self, cx: &mut Context) { pub fn render(&mut self, cx: &mut Context) {
let area = self.size(); self.terminal
.autoresize()
.expect("Unable to determine terminal size");
// TODO: need to recalculate view tree if necessary
let surface = self.terminal.current_buffer_mut(); let surface = self.terminal.current_buffer_mut();
for layer in &self.layers { let area = *surface.area();
layer.render(area, surface, cx)
for layer in &mut self.layers {
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 has_component(&self, type_name: &str) -> bool {
self.layers
.iter()
.any(|component| component.type_name() == type_name)
} }
pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> { pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> {
@@ -174,10 +206,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<dyn 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(
&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(&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(())));
}
}

File diff suppressed because it is too large Load Diff

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)] use anyhow::{Context, Error, Result};
use helix_term::application::Application;
mod application; use helix_term::args::Args;
mod commands; use helix_term::config::Config;
mod compositor; use helix_term::keymap::merge_keys;
mod keymap;
mod ui;
use application::Application;
use std::path::PathBuf; use std::path::PathBuf;
use anyhow::{Context, Result}; fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
fn setup_logging(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),
@@ -27,9 +15,12 @@ fn setup_logging(verbosity: u64) -> Result<()> {
_3_or_more => base_config.level(log::LevelFilter::Trace), _3_or_more => base_config.level(log::LevelFilter::Trace),
}; };
let home = dirs_next::home_dir().context("can't find the home directory")?;
// Separate file config so we can include year, month and day in file logs // Separate file config so we can include year, month and day in file logs
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(logpath)?;
let file_config = fern::Dispatch::new() let file_config = fern::Dispatch::new()
.format(|out, message, record| { .format(|out, message, record| {
out.finish(format_args!( out.finish(format_args!(
@@ -40,18 +31,21 @@ fn setup_logging(verbosity: u64) -> Result<()> {
message message
)) ))
}) })
.chain(fern::log_file(home.join("helix.log"))?); .chain(file);
base_config.chain(file_config).apply()?; base_config.chain(file_config).apply()?;
Ok(()) Ok(())
} }
pub struct Args { #[tokio::main]
files: Vec<PathBuf>, async fn main() -> Result<()> {
} let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() {
std::fs::create_dir_all(&cache_dir).ok();
}
fn main() -> Result<()> { let logpath = cache_dir.join("helix.log");
let help = format!( let help = format!(
"\ "\
{} {} {} {}
@@ -66,55 +60,47 @@ ARGS:
FLAGS: FLAGS:
-h, --help Prints help information -h, --help Prints help information
--tutor Loads the tutorial
-v Increases logging verbosity each use for up to 3 times -v Increases logging verbosity each use for up to 3 times
(default file: {})
-V, --version Prints version information -V, --version Prints version information
", ",
env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"), env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS"), env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"), env!("CARGO_PKG_DESCRIPTION"),
logpath.display(),
); );
let mut pargs = pico_args::Arguments::from_env(); let args = Args::parse_args().context("could not parse arguments")?;
// Help has a higher priority and should be handled separately. // Help has a higher priority and should be handled separately.
if pargs.contains(["-h", "--help"]) { if args.display_help {
print!("{}", help); print!("{}", help);
std::process::exit(0); std::process::exit(0);
} }
let mut verbosity: u64 = 0; if args.display_version {
println!("helix {}", env!("CARGO_PKG_VERSION"));
if pargs.contains("-v") { std::process::exit(0);
verbosity = 1;
} }
setup_logging(verbosity).context("failed to initialize logging")?; let conf_dir = helix_core::config_dir();
if !conf_dir.exists() {
std::fs::create_dir_all(&conf_dir).ok();
}
let args = Args { let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
files: pargs.finish().into_iter().map(|arg| arg.into()).collect(), Ok(config) => merge_keys(toml::from_str(&config)?),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
}; };
// initialize language registry setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
use helix_core::config_dir;
use helix_core::syntax::{Loader, LOADER};
// load $HOME/.config/helix/languages.toml, fallback to default config
let config = std::fs::read(config_dir().join("languages.toml"));
let toml = config
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
let config = toml::from_slice(toml).context("Could not parse languages.toml")?;
LOADER.get_or_init(|| Loader::new(config));
let runtime = tokio::runtime::Runtime::new().context("unable to start tokio runtime")?;
// 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")?;
runtime.block_on(async move { app.run().await.unwrap();
app.run().await;
});
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};
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,80 +68,91 @@ 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>>,
start_offset: usize,
#[allow(dead_code)]
trigger_offset: usize, trigger_offset: usize,
// TODO: maintain a completioncontext with trigger kind & trigger char // TODO: maintain a completioncontext with trigger kind & trigger char
} }
impl Completion { impl Completion {
pub fn new( pub fn new(
editor: &Editor,
items: Vec<CompletionItem>, items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding, offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
) -> Self { ) -> Self {
// let items: Vec<CompletionItem> = Vec::new(); let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| { fn item_to_transaction(
match event { doc: &Document,
PromptEvent::Abort => { item: &CompletionItem,
// revert state offset_encoding: helix_lsp::OffsetEncoding,
// let id = editor.view().doc; start_offset: usize,
// let doc = &mut editor.documents[id]; trigger_offset: usize,
// 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(
// if more text was entered, remove it
let cursor = doc.selection(view.id).cursor();
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(),
vec![(trigger_offset, cursor, None)].into_iter(),
);
doc.apply(&remove, view.id);
}
use helix_lsp::OffsetEncoding;
let transaction = util::generate_transaction_from_edits(
doc.text(), doc.text(),
vec![edit], vec![edit],
offset_encoding, // TODO: should probably transcode in Client offset_encoding, // TODO: should probably transcode in Client
)
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
// Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
// in these cases we need to check for a common prefix and remove it
let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
let text = text.trim_start_matches::<&str>(&prefix);
Transaction::change(
doc.text(),
vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
)
}
}
let (view, doc) = current!(editor);
// if more text was entered, remove it
doc.restore(view.id);
match event {
PromptEvent::Abort => {}
PromptEvent::Update => {
// always present here
let item = item.unwrap();
let transaction = item_to_transaction(
doc,
item,
offset_encoding,
start_offset,
trigger_offset,
);
// initialize a savepoint
doc.savepoint();
doc.apply(&transaction, view.id);
}
PromptEvent::Validate => {
// always present here
let item = item.unwrap();
let transaction = item_to_transaction(
doc,
item,
offset_encoding,
start_offset,
trigger_offset,
); );
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,20 +165,25 @@ impl Completion {
} }
} }
} }
_ => (),
}; };
}); });
let popup = Popup::new(menu); let popup = Popup::new(menu);
Self { let mut completion = Self {
popup, popup,
start_offset,
trigger_offset, trigger_offset,
} };
// need to recompute immediately in case start_offset != trigger_offset
completion.recompute_filter(editor);
completion
} }
pub fn update(&mut self, cx: &mut commands::Context) { pub fn recompute_filter(&mut self, editor: &Editor) {
// 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_ref!(editor);
// cx.hooks() // cx.hooks()
// cx.add_hook(enum type, ||) // cx.add_hook(enum type, ||)
@@ -179,15 +195,26 @@ 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.start_offset..cursor);
let text = Cow::from(fragment); let text = Cow::from(fragment);
// TODO: logic is same as ui/picker // TODO: logic is same as ui/picker
menu.score(&text); menu.score(&text);
} else {
// we backspaced before the start offset, clear the menu
// this will cause the editor to remove the completion popup
menu.clear();
} }
} }
pub fn update(&mut self, cx: &mut commands::Context) {
self.recompute_filter(cx.editor)
}
pub fn is_empty(&self) -> bool { pub fn is_empty(&self) -> bool {
self.popup.contents().is_empty() self.popup.contents().is_empty()
} }
@@ -221,70 +248,111 @@ 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
let doc = match &option.documentation { .selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.offset.row) as u16;
let mut markdown_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 (popup_x, popup_y) = self.popup.get_rel_position(area, cx);
let (popup_width, _popup_height) = self.popup.get_size();
let mut width = area
.width
.saturating_sub(popup_x)
.saturating_sub(popup_width);
let area = if width > 30 {
let mut height = area.height.saturating_sub(popup_y);
let x = popup_x + popup_width;
let y = popup_y;
if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) {
width = rel_width;
height = rel_height;
}
Rect::new(x, y, width, height)
} else {
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 + 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)
};
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");
surface.clear_with(area, background); surface.clear_with(area, background);
doc.render(area, surface, cx); markdown_doc.render(area, surface, cx);
} }
} }
} }

File diff suppressed because it is too large Load Diff

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

@@ -0,0 +1,45 @@
use crate::compositor::{Component, Context};
use helix_view::graphics::{Margin, Rect};
use helix_view::info::Info;
use tui::buffer::Buffer as Surface;
use tui::widgets::{Block, Borders, Paragraph, Widget};
impl Component for Info {
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
let get_theme = |style, fallback| {
let theme = &cx.editor.theme;
theme.try_get(style).unwrap_or_else(|| theme.get(fallback))
};
let text_style = get_theme("ui.info.text", "ui.text");
let popup_style = text_style.patch(get_theme("ui.info", "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 = self.width + 2 + 2; // +2 for border, +2 for margin
let height = self.height + 2; // +2 for border
let area = viewport.intersection(Rect::new(
viewport.width.saturating_sub(width),
viewport.height.saturating_sub(height + 2), // +2 for statusline
width,
height,
));
surface.clear_with(area, popup_style);
let block = Block::default()
.title(self.title.as_str())
.borders(Borders::ALL)
.border_style(popup_style);
let margin = Margin {
vertical: 0,
horizontal: 1,
};
let inner = block.inner(area).inner(&margin);
block.render(area, surface);
Paragraph::new(self.text.as_str())
.style(text_style)
.render(inner, surface);
}
}

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, Margin, 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() .language_configuration_for_injection_string(language)
.unwrap()
.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,24 +198,47 @@ 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 })
.scroll((cx.scroll.unwrap_or_default() as u16, 0)); .scroll((cx.scroll.unwrap_or_default() as u16, 0));
let area = Rect::new(area.x + 1, area.y + 1, area.width - 2, area.height - 2); let margin = Margin {
par.render(area, surface); vertical: 1,
horizontal: 1,
};
par.render(area.inner(&margin), surface);
} }
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 padding = 2; let padding = 2;
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0); if padding >= viewport.1 || padding >= viewport.0 {
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1); return None;
Some((width, height)) }
let contents = parse(&self.contents, None, &self.config_loader);
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
let mut height = padding;
for content in contents {
height += 1;
let content_width = content.width() as u16;
if content_width > max_text_width {
text_width = max_text_width;
height += content_width / max_text_width;
} else if content_width > text_width {
text_width = content_width;
}
if height >= viewport.1 {
height = viewport.1;
break;
}
}
Some((text_width + padding, height))
} }
} }

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,10 +27,14 @@ 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,
size: (u16, u16), size: (u16, u16),
viewport: (u16, u16),
recalculate: bool,
} }
impl<T: Item> Menu<T> { impl<T: Item> Menu<T> {
@@ -52,9 +49,12 @@ 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),
viewport: (0, 0),
recalculate: true,
}; };
// TODO: scoring on empty input should just use a fastpath // TODO: scoring on empty input should just use a fastpath
@@ -64,29 +64,32 @@ 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
let Self {
ref mut options,
ref mut matcher,
ref mut matches,
..
} = *self;
// reuse the matches allocation // reuse the matches allocation
matches.clear(); self.matches.clear();
matches.extend( self.matches.extend(
self.options self.options
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(index, option)| { .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 self.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);
self.matches
.sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
// reset cursor position
self.cursor = None;
self.scroll = 0;
self.recalculate = true;
}
pub fn clear(&mut self) {
self.matches.clear();
// reset cursor position // reset cursor position
self.cursor = None; self.cursor = None;
@@ -94,18 +97,55 @@ 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 max_index = len.saturating_sub(1);
let pos = self.cursor.map_or(max_index, |i| (i + max_index) % 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();
} }
fn recalculate_size(&mut self, viewport: (u16, u16)) {
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;
}
}
acc
});
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.matches.len().min(10).min(viewport.1 as usize);
self.size = (width as u16, height as u16);
// adjust scroll offsets if size changed
self.adjust_scroll();
self.recalculate = false;
}
fn adjust_scroll(&mut self) { fn adjust_scroll(&mut self) {
let win_height = self.size.1 as usize; let win_height = self.size.1 as usize;
if let Some(cursor) = self.cursor { if let Some(cursor) = self.cursor {
@@ -166,8 +206,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, ..
@@ -175,6 +215,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent { | KeyEvent {
code: KeyCode::Char('p'), code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => { } => {
self.move_up(); self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@@ -192,6 +236,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent { | KeyEvent {
code: KeyCode::Char('n'), code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
} => { } => {
self.move_down(); self.move_down();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@@ -228,25 +276,19 @@ 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); if viewport != self.viewport || self.recalculate {
self.recalculate_size(viewport);
const MAX: usize = 10; }
let height = std::cmp::min(self.options.len(), MAX);
let height = std::cmp::min(height, viewport.1 as usize);
self.size = (width as u16, height as u16);
// adjust scroll offsets if size changed
self.adjust_scroll();
Some(self.size) Some(self.size)
} }
// TODO: required size should re-trigger when we filter items so we can draw a smaller menu fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme;
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { let style = theme
let style = cx.editor.theme.get("ui.text"); .try_get("ui.menu")
let selected = cx.editor.theme.get("ui.menu.selected"); .unwrap_or_else(|| theme.get("ui.text"));
let selected = theme.get("ui.menu.selected");
let scroll = self.scroll; let scroll = self.scroll;
@@ -272,13 +314,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;
@@ -291,15 +332,7 @@ impl<T: Item + 'static> Component for Menu<T> {
}, },
); );
// // TODO: set bg for the whole row if selected for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
// if line == self.cursor {
// surface.set_style(
// Rect::new(area.x, area.y + i as u16, area.width - 1, 1),
// selected,
// )
// }
for (i, option) 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,49 +1,60 @@
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::regex::RegexBuilder;
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: std::borrow::Cow<'static, str>,
fun: impl Fn(&mut View, &mut Document, Regex) + 'static, history_register: Option<char>,
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + '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 history_register,
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 => {
// TODO: push_jump to store selection just before jump // TODO: push_jump to store selection just before jump
match Regex::new(input) {
Ok(regex) => {
let (view, doc) = current!(cx.editor);
fun(view, doc, regex, event);
}
Err(_err) => (), // TODO: mark command line as error
}
} }
PromptEvent::Update => { PromptEvent::Update => {
// skip empty input, TODO: trigger default // skip empty input, TODO: trigger default
@@ -51,17 +62,25 @@ pub fn regex_prompt(
return; return;
} }
match Regex::new(input) { let case_insensitive = if cx.editor.config.smart_case {
!input.chars().any(char::is_uppercase)
} else {
false
};
match RegexBuilder::new(input)
.case_insensitive(case_insensitive)
.build()
{
Ok(regex) => { Ok(regex) => {
let (view, doc) = editor.current(); let (view, doc) = current!(cx.editor);
// 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, regex, event);
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 +90,57 @@ 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::{types::TypesBuilder, WalkBuilder};
let files = Walk::new(root.clone()).filter_map(|entry| match entry { use std::time;
Ok(entry) => {
// filter dirs, but we might need special handling for symlinks! // We want to exclude files that the editor can't handle yet
if !entry.file_type().map_or(false, |entry| entry.is_dir()) { let mut type_builder = TypesBuilder::new();
Some(entry.into_path()) let mut walk_builder = WalkBuilder::new(&root);
} else { let walk_builder = match type_builder.add(
None "compressed",
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
) {
Err(_) => &walk_builder,
_ => {
type_builder.negate("all");
let excluded_types = type_builder.build().unwrap();
walk_builder.types(excluded_types)
} }
};
let files = walk_builder.build().filter_map(|entry| {
let entry = entry.ok()?;
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
if entry.path().is_dir() {
// Will give a false positive if metadata cannot be read (eg. permission error)
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 +150,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_core::path::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 +259,40 @@ 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)
.hidden(false)
.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 +300,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,183 @@
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; editor::Action,
graphics::{Color, CursorKind, Margin, 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, 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)| {
helix_core::path::get_canonicalized_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 = area.with_width(picker_width);
self.picker.render(picker_area, surface, cx);
if !render_preview {
return;
}
let preview_area = area.clip_left(picker_width);
// don't like this but the lifetime sucks
let block = Block::default().borders(Borders::ALL);
// calculate the inner area inside the box
let inner = block.inner(preview_area);
// 1 column gap on either side
let margin = Margin {
vertical: 0,
horizontal: 1,
};
let inner = inner.inner(&margin);
block.render(preview_area, surface);
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
cx.editor
.document_by_path(&path)
.or_else(|| self.preview_cache.get(&path))
.zip(Some(range))
}) {
// align to middle
let first_line = line
.map(|(start, end)| {
let height = end.saturating_sub(start) + 1;
let middle = start + (height.saturating_sub(1) / 2);
middle.saturating_sub(inner.height as usize / 2).min(start)
})
.unwrap_or(0);
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((start, end)) = line {
let offset = start.saturating_sub(first_line) as u16;
surface.set_style(
Rect::new(
inner.x,
inner.y + offset,
inner.width,
(end.saturating_sub(start) as u16 + 1)
.min(inner.height.saturating_sub(offset)),
),
cx.editor
.theme
.try_get("ui.highlight")
.unwrap_or_else(|| 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 +185,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 +200,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(), "".into(),
|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 +218,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),
}; };
@@ -63,47 +233,49 @@ 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
let Self {
ref mut options,
ref mut matcher,
ref mut matches,
ref format_fn,
..
} = *self;
let pattern = &self.prompt.line; let pattern = &self.prompt.line;
// reuse the matches allocation // reuse the matches allocation
matches.clear(); self.matches.clear();
matches.extend( self.matches.extend(
self.options self.options
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(index, option)| { .filter_map(|(index, option)| {
// filter options first before matching
if !self.filters.is_empty() {
self.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 = (self.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
matcher self.matcher
.fuzzy_match(&text, pattern) .fuzzy_match(&text, pattern)
.map(|score| (index, score)) .map(|score| (index, score))
}), }),
); );
matches.sort_unstable_by_key(|(_, score)| -score); self.matches.sort_unstable_by_key(|(_, score)| -score);
// reset cursor position // reset cursor position
self.cursor = 0; self.cursor = 0;
} }
pub fn move_up(&mut self) { pub fn move_up(&mut self) {
self.cursor = self.cursor.saturating_sub(1); if self.matches.is_empty() {
return;
}
let len = self.matches.len();
let pos = ((self.cursor + len.saturating_sub(1)) % len) % len;
self.cursor = pos;
} }
pub fn move_down(&mut self) { pub fn move_down(&mut self) {
// TODO: len - 1 if self.matches.is_empty() {
if self.cursor < self.options.len() { return;
self.cursor += 1;
} }
let len = self.matches.len();
let pos = (self.cursor + 1) % len;
self.cursor = pos;
} }
pub fn selection(&self) -> Option<&T> { pub fn selection(&self) -> Option<&T> {
@@ -111,6 +283,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:
@@ -119,15 +299,11 @@ 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 margin = Margin {
let padding_horizontal = area.width * 20 / 100; vertical: area.height * 10 / 100,
horizontal: area.width * 10 / 100,
Rect::new( };
area.x + padding_horizontal, area.inner(&margin)
area.y + padding_vertical,
area.width - padding_horizontal * 2,
area.height - padding_vertical * 2,
)
} }
impl<T: 'static> Component for Picker<T> { impl<T: 'static> Component for Picker<T> {
@@ -140,27 +316,50 @@ 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 {
KeyEvent { KeyEvent {
code: KeyCode::Up, .. code: KeyCode::Up, ..
} }
| KeyEvent {
code: KeyCode::BackTab,
..
}
| KeyEvent { | KeyEvent {
code: KeyCode::Char('k'), code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_up(), }
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_up();
}
KeyEvent { KeyEvent {
code: KeyCode::Down, code: KeyCode::Down,
.. ..
} }
| KeyEvent {
code: KeyCode::Tab, ..
}
| KeyEvent { | KeyEvent {
code: KeyCode::Char('j'), code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.move_down(), }
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => {
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;
} }
@@ -174,7 +373,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn; return close_fn;
} }
KeyEvent { KeyEvent {
code: KeyCode::Char('x'), code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => { } => {
if let Some(option) = self.selection() { if let Some(option) = self.selection() {
@@ -191,6 +390,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
@@ -202,16 +407,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);
@@ -219,51 +428,66 @@ 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 = inner.clip_left(1).with_height(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 area of prompt from top and current item marker " > " from left
let inner = inner.clip_top(2).clip_left(3);
let style = cx.editor.theme.get("ui.text"); let selected = cx.editor.theme.get("ui.text.focus");
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 files = self.matches.iter().map(|(index, _score)| { let files = self.matches.iter().skip(offset).map(|(index, _score)| {
(index, self.options.get(*index).unwrap()) // get_unchecked (index, self.options.get(*index).unwrap()) // get_unchecked
}); });
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 { 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 { selected } else { style }, if i == (self.cursor - offset) {
selected
} else {
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);
@@ -271,8 +495,8 @@ impl<T: 'static> Component for Picker<T> {
let inner = block.inner(area); let inner = block.inner(area);
// prompt area // prompt area
let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1); let area = inner.clip_left(1).with_height(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>)
@@ -22,8 +16,6 @@ pub struct Popup<T: Component> {
} }
impl<T: Component> Popup<T> { impl<T: Component> Popup<T> {
// TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
// rendering)
pub fn new(contents: T) -> Self { pub fn new(contents: T) -> Self {
Self { Self {
contents, contents,
@@ -37,6 +29,39 @@ impl<T: Component> Popup<T> {
self.position = pos; self.position = pos;
} }
pub fn get_rel_position(&mut self, viewport: Rect, cx: &Context) -> (u16, u16) {
let position = self
.position
.get_or_insert_with(|| cx.editor.cursor().0.unwrap_or_default());
let (width, height) = self.size;
// if there's a orientation preference, use that
// if we're on the top part of the screen, do below
// if we're on the bottom part, do above
// -- make sure frame doesn't stick out of bounds
let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width {
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
}
// TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete.
if viewport.height > rel_y + height {
rel_y += 1 // position below point
} else {
rel_y = rel_y.saturating_sub(height) // position above point
}
(rel_x, rel_y)
}
pub fn get_size(&self) -> (u16, u16) {
(self.size.0, self.size.1)
}
pub fn scroll(&mut self, offset: usize, direction: bool) { pub fn scroll(&mut self, offset: usize, direction: bool) {
if direction { if direction {
self.scroll += offset; self.scroll += offset;
@@ -58,7 +83,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 +125,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,34 +136,16 @@ 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) {
// trigger required_size so we recalculate if the child changed
self.required_size((viewport.width, viewport.height));
cx.scroll = Some(self.scroll); cx.scroll = Some(self.scroll);
let position = self let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
.position
.or_else(|| cx.editor.cursor_position())
.unwrap_or_default();
let (width, height) = self.size;
// -- make sure frame doesn't stick out of bounds
let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width {
rel_x -= ((rel_x + width) - viewport.width)
};
// TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete.
if height <= rel_y {
rel_y -= height // position above point
} else {
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, self.size.0, self.size.1));
// clear area // clear area
let background = cx.editor.theme.get("ui.popup"); let background = cx.editor.theme.get("ui.popup");

View File

@@ -1,24 +1,33 @@
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>);
pub struct Prompt { pub struct Prompt {
prompt: String, prompt: Cow<'static, str>,
pub line: String, pub line: String,
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,
@@ -28,11 +37,28 @@ pub enum PromptEvent {
Abort, Abort,
} }
pub enum CompletionDirection {
Forward,
Backward,
}
#[derive(Debug, Clone, Copy)]
pub enum Movement {
BackwardChar(usize),
BackwardWord(usize),
ForwardChar(usize),
ForwardWord(usize),
StartOfLine,
EndOfLine,
None,
}
impl Prompt { impl Prompt {
pub fn new( pub fn new(
prompt: String, prompt: Cow<'static, str>,
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,
@@ -40,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) {
@@ -72,19 +199,72 @@ 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.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(); self.exit_selection();
} }
pub fn change_completion_selection(&mut self) { 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) {
if self.completion.is_empty() { if self.completion.is_empty() {
return; return;
} }
let index = self.selection.map_or(0, |i| i + 1) % self.completion.len();
let index = match direction {
CompletionDirection::Forward => self.selection.map_or(0, |i| i + 1),
CompletionDirection::Backward => {
self.selection.unwrap_or(0) + self.completion.len() - 1
}
} % self.completion.len();
self.selection = Some(index); self.selection = Some(index);
let (range, item) = &self.completion[index]; let (range, item) = &self.completion[index];
@@ -92,30 +272,38 @@ impl Prompt {
self.line.replace_range(range.clone(), item); self.line.replace_range(range.clone(), item);
self.move_end(); self.move_end();
// TODO: recalculate completion when completion item is accepted, (Enter)
} }
pub fn exit_selection(&mut self) { pub fn exit_selection(&mut self) {
self.selection = None; self.selection = None;
} }
} }
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 {
pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) { pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme; let theme = &cx.editor.theme;
let text_color = theme.get("ui.text.focus"); let prompt_color = theme.get("ui.text");
let completion_color = theme.get("ui.statusline");
let selected_color = theme.get("ui.menu.selected"); let selected_color = theme.get("ui.menu.selected");
// completion // completion
let max_col = 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),
@@ -127,23 +315,31 @@ 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() { 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))
selected_color // TODO: just invert bg selected_color // TODO: just invert bg
} else { } else {
text_color completion_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;
@@ -151,21 +347,23 @@ 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 area = Rect::new(completion_area.x, completion_area.y - 3, BASE_WIDTH * 3, 3); let viewport = area;
let area = viewport.intersection(Rect::new(
completion_area.x,
completion_area.y.saturating_sub(3),
BASE_WIDTH * 3,
3,
));
let background = theme.get("ui.help"); 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,
@@ -178,12 +376,12 @@ impl Prompt {
let line = area.height - 1; let line = area.height - 1;
// render buffer text // render buffer text
surface.set_string(area.x, area.y + line, &self.prompt, text_color); surface.set_string(area.x, area.y + line, &self.prompt, prompt_color);
surface.set_string( surface.set_string(
area.x + self.prompt.len() as u16, area.x + self.prompt.len() as u16,
area.y + line, area.y + line,
&self.line, &self.line,
text_color, prompt_color,
); );
} }
} }
@@ -202,76 +400,163 @@ impl Component for Prompt {
}))); })));
match event { match event {
// char or shift char
KeyEvent { KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char('c'),
modifiers: KeyModifiers::NONE, modifiers: KeyModifiers::CONTROL,
} }
| KeyEvent { | KeyEvent {
code: KeyCode::Char(c),
modifiers: KeyModifiers::SHIFT,
} => {
self.insert_char(c);
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Update);
}
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,
.. ..
} => { } => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate); if self.selection.is_some() && self.line.ends_with('/') {
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
} else {
(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);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
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);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
KeyEvent { KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
} => self.change_completion_selection(), } => self.change_completion_selection(CompletionDirection::Forward),
KeyEvent {
code: KeyCode::BackTab,
..
} => self.change_completion_selection(CompletionDirection::Backward),
KeyEvent { KeyEvent {
code: KeyCode::Char('q'), code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => self.exit_selection(), } => self.exit_selection(),
// any char event that's not combined with control or mapped to any other combo
KeyEvent {
code: KeyCode::Char(c),
modifiers,
} if !modifiers.contains(KeyModifiers::CONTROL) => {
self.insert_char(c);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
_ => (), _ => (),
}; };
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,32 +1,28 @@
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,
size: (u16, u16),
viewport: (u16, u16),
} }
impl Text { impl Text {
pub fn new(contents: String) -> Self { pub fn new(contents: String) -> Self {
Self { contents } Self {
contents,
size: (0, 0),
viewport: (0, 0),
}
} }
} }
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
@@ -34,9 +30,13 @@ impl Component for Text {
} }
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
if viewport != self.viewport {
let contents = tui::text::Text::from(self.contents.clone()); let contents = tui::text::Text::from(self.contents.clone());
let width = std::cmp::min(contents.width() as u16, viewport.0); let width = std::cmp::min(contents.width() as u16, viewport.0);
let height = std::cmp::min(contents.height() as u16, viewport.1); let height = std::cmp::min(contents.height() as u16, viewport.1);
Some((width, height)) self.size = (width, height);
self.viewport = viewport;
}
Some(self.size)
} }
} }

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