Compare commits

...

1029 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
Blaž Hrastnik
cbb3ebafdc Support ctrl-f and ctrl-b to page up/down, fixes #41 2021-06-02 13:20:36 +09:00
Blaž Hrastnik
0851110d10 f/t: Check if at bounds before searching, refs #43, closes #37 2021-06-02 13:20:27 +09:00
Blaž Hrastnik
3ace581191 Fix panics when triggering w or e on the last char of the line
Closes #32
2021-06-02 13:19:40 +09:00
Blaž Hrastnik
c0264b9f7f fix: Don't allow moving past last line, fixes #30, #24
Off by 1 error
2021-06-02 13:19:40 +09:00
Blaž Hrastnik
22dad592b8 Merge pull request #40 from data0x200/fix-empty-command
Fix empty command cause panic
2021-06-02 13:06:57 +09:00
Corey Powell
ca042a4bde Added elixir syntax
Using custom fork for now to get around generating the source files
2021-06-01 21:59:16 -05:00
Blaž Hrastnik
67b1cd32c7 Update install notes 2021-06-02 11:14:46 +09:00
Daichi Takamiya
4d12c7c3cf Fix empty command cause panic 2021-06-02 10:55:32 +09:00
Blaž Hrastnik
4f56a8e248 book: Always generate the CNAME file 2021-06-02 10:24:00 +09:00
Blaž Hrastnik
dbc392d92c Run fmt 2021-06-02 09:56:50 +09:00
Blaž Hrastnik
7967d312c0 Merge pull request #38 from nathom/master
Add .DS_Store to ignored directories
2021-06-02 09:37:09 +09:00
Blaž Hrastnik
db48d22384 Merge pull request #19 from wojciechkepka/archinstall
Add Arch Linux installation instructions to README
2021-06-02 09:30:41 +09:00
Blaž Hrastnik
533ff61d0e Merge pull request #34 from DanySpin97/improve-error
Improve errors handling in main by adding context
2021-06-02 09:30:16 +09:00
nathom
b1ce969d80 Add .DS_Store to ignored directories 2021-06-01 17:29:37 -07:00
Blaž Hrastnik
01bf363446 Merge pull request #31 from wullewutz/patch-1
Fixed c/p error in keymap doc
2021-06-02 09:26:29 +09:00
Blaž Hrastnik
cc323f7665 Merge pull request #36 from swdunlop/patch-1
Make HELIX_RUNTIME depend on pwd, not speed's HOME
2021-06-02 09:26:04 +09:00
Scott Dunlop
60caaf7fc4 Make HELIX_RUNTIME depend on pwd, not speed's HOME 2021-06-01 15:03:57 -07:00
Danilo Spinella
ea824ed05d Improve errors handling in main by adding context
Return a anyhow::Result in main function so that Context can be used
there too.
2021-06-01 23:27:16 +02:00
wullewutz
cfae07e7ba Fixed c/p error in keymap doc
Go to definition mapping is "gd" not "ge"
2021-06-01 22:36:42 +02:00
wojciechkepka
56dbc60840 Add Arch Linux installation instructions to README 2021-06-01 21:08:09 +02:00
Blaž Hrastnik
c2e6b9f506 Add typescript support & ts/js indentation queries 2021-06-01 17:55:11 +09:00
Blaž Hrastnik
8fd8006043 Golang indent queries 2021-06-01 17:26:10 +09:00
Blaž Hrastnik
ce25aa951e Allow setting a filepath on :write 2021-06-01 17:26:03 +09:00
Blaž Hrastnik
a2147fc7d5 Change help prompt styling 2021-06-01 12:00:25 +09:00
Blaž Hrastnik
d8e16554bf Don't crash if no filename specified on open 2021-06-01 11:59:59 +09:00
Blaž Hrastnik
2cc30cd07c Categorize _ as a word char, not punctuation 2021-05-31 21:09:17 +09:00
Blaž Hrastnik
0dde5f2cae Fix badge 2021-05-31 21:09:07 +09:00
Blaž Hrastnik
b8d6e6ad28 Allow setting verbosity to info again 2021-05-31 17:14:49 +09:00
Blaž Hrastnik
5825bce0e4 Link to install/keymap docs, fix build status badge 2021-05-31 17:12:09 +09:00
Blaž Hrastnik
aeabfc55a8 Adjust golang indents yet again 2021-05-31 17:09:19 +09:00
Blaž Hrastnik
17e9386388 Allow moving to EOL byte, also fixes #15 2021-05-31 17:08:19 +09:00
Blaž Hrastnik
138787f76e Drop clap for pico-args
We barely have any flags so it's not worth the compilation time or
binary size to use clap.
2021-05-31 17:07:43 +09:00
Blaž Hrastnik
1132c5122f Update mdbook styling, add link to AUR 2021-05-31 00:32:21 +09:00
Blaž Hrastnik
4a8053e832 Merge pull request #13 from helix-editor/dependabot/cargo/tokio-1.6.1
Bump tokio from 1.6.0 to 1.6.1
2021-05-30 18:02:52 +09:00
Blaž Hrastnik
e033a4b8ac Merge pull request #11 from helix-editor/dependabot/github_actions/actions/upload-artifact-2.2.3
Bump actions/upload-artifact from 1 to 2.2.3
2021-05-30 17:58:43 +09:00
Blaž Hrastnik
acbcd758bd Merge pull request #12 from helix-editor/dependabot/github_actions/actions/cache-2.1.6
Bump actions/cache from 1 to 2.1.6
2021-05-30 17:58:21 +09:00
dependabot[bot]
76eed4caad Bump tokio from 1.6.0 to 1.6.1
Bumps [tokio](https://github.com/tokio-rs/tokio) from 1.6.0 to 1.6.1.
- [Release notes](https://github.com/tokio-rs/tokio/releases)
- [Commits](https://github.com/tokio-rs/tokio/compare/tokio-1.6.0...tokio-1.6.1)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:43 +00:00
dependabot[bot]
3170c49be8 Bump actions/cache from 1 to 2.1.6
Bumps [actions/cache](https://github.com/actions/cache) from 1 to 2.1.6.
- [Release notes](https://github.com/actions/cache/releases)
- [Commits](https://github.com/actions/cache/compare/v1...v2.1.6)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:19 +00:00
dependabot[bot]
0327d66653 Bump actions/upload-artifact from 1 to 2.2.3
Bumps [actions/upload-artifact](https://github.com/actions/upload-artifact) from 1 to 2.2.3.
- [Release notes](https://github.com/actions/upload-artifact/releases)
- [Commits](https://github.com/actions/upload-artifact/compare/v1...v2.2.3)

Signed-off-by: dependabot[bot] <support@github.com>
2021-05-30 08:54:15 +00:00
Blaž Hrastnik
c67e31830d Add dependabot config 2021-05-30 17:52:56 +09:00
Blaž Hrastnik
6460501a44 Update architecture.md 2021-05-30 17:52:46 +09:00
Blaž Hrastnik
67b037050f Adjust rust indents 2021-05-30 17:13:32 +09:00
Blaž Hrastnik
87d0617f3b Completion: Format docs tabs & highlight in the doc's native language 2021-05-30 17:13:02 +09:00
Blaž Hrastnik
668f735232 Update the README's matrix link 2021-05-30 17:12:43 +09:00
Blaž Hrastnik
a3a9502596 Add a github pages auto-build action. 2021-05-30 17:12:29 +09:00
Blaž Hrastnik
3810650a6b Completion: Render non-markdown docs too 2021-05-30 10:36:58 +09:00
Blaž Hrastnik
2c48d65b15 Format document on save 2021-05-30 00:00:15 +09:00
Blaž Hrastnik
d5466eddf5 Update flake deps 2021-05-29 23:59:30 +09:00
Blaž Hrastnik
d54ae09d3b ESC should exit both completion and insert mode 2021-05-29 10:37:47 +09:00
Blaž Hrastnik
a28eaa81a0 Golang indent adjustment 2021-05-29 00:06:38 +09:00
Blaž Hrastnik
d708efe275 Fix cursor positioning for prompts 2021-05-29 00:06:23 +09:00
Blaž Hrastnik
3336023614 ui: Menu rendering adjustments 2021-05-28 00:01:17 +09:00
Blaž Hrastnik
094203c74e Update deps, introduce the new tree-sitter lifetimes 2021-05-28 00:00:51 +09:00
Blaž Hrastnik
b114cfa119 Display more data in completion popups. 2021-05-22 17:33:42 +09:00
Blaž Hrastnik
f1dc25a774 Support count for indent too 2021-05-19 00:37:01 +09:00
Blaž Hrastnik
4f335fabc8 Fix unindent to work with tabs, take a count 2021-05-19 00:35:33 +09:00
Blaž Hrastnik
f366b97bce Update dependencies 2021-05-18 18:30:48 +09:00
Blaž Hrastnik
9c24f1ec0e Drop selection_lines completely, change move_line_start binding 2021-05-18 18:28:32 +09:00
Blaž Hrastnik
f99a683991 Fix crash if appending at end of line on the last line of the file 2021-05-18 18:17:14 +09:00
Blaž Hrastnik
9edae7e1f8 syntax: golang: Indent type declarations 2021-05-18 17:54:18 +09:00
Blaž Hrastnik
51d1d43289 Double the UI picker file limit. 2021-05-18 17:53:58 +09:00
Blaž Hrastnik
5a245b83a0 Append :fmt as a separate history state 2021-05-18 17:53:00 +09:00
Blaž Hrastnik
2100f5a2c0 Address clippy lint. 2021-05-17 23:01:45 +09:00
Blaž Hrastnik
8f6f329057 If switching to a previously open buffer in the same view, keep it's old offset 2021-05-17 16:36:13 +09:00
Blaž Hrastnik
8949347e2c Completion: apply additionalTextEdits.
Used for adding imports to the file when completing.
2021-05-17 16:35:34 +09:00
Blaž Hrastnik
54de768915 Fix crash if typing | (regex or) into the prompt.
Zero-width matches at the start of the file make no sense to us.
2021-05-16 18:58:43 +09:00
Blaž Hrastnik
6e03019a2c Adjust highlighting for rust. 2021-05-16 18:58:27 +09:00
Blaž Hrastnik
31d41080ed Add indentation queries for golang. 2021-05-15 17:17:26 +09:00
Blaž Hrastnik
5e6b46e7c5 Use array::IntoIter. 2021-05-15 10:52:07 +09:00
Blaž Hrastnik
354b822d21 Fix crash on xa<Enter> if we were on the last line. 2021-05-15 10:50:36 +09:00
Blaž Hrastnik
fae2127a11 Drop cx.view_id, it was used before we had cx.current. 2021-05-15 10:50:36 +09:00
Blaž Hrastnik
0e5b421646 When calculating a new selection, we need to take newly inserted text into account. 2021-05-15 10:50:36 +09:00
Blaž Hrastnik
4a9d1163e0 Hacky way to specify indent scopes per language via toml configs.
Can't do it via a scm query nicely because it returns an iterator over
all the matches, whereas we want to traverse the tree ourselves.

Can't extract the pattern data from a parsed query either.

Oh well, toml files for now.
2021-05-14 19:21:46 +09:00
230 changed files with 389256 additions and 5626 deletions

5
.envrc
View File

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

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

14
.github/dependabot.yml vendored Normal file
View File

@@ -0,0 +1,14 @@
# Please see the documentation for all configuration options:
# https://help.github.com/github/administering-a-repository/configuration-options-for-dependency-updates
version: 2
updates:
- package-ecosystem: "cargo"
directory: "/"
schedule:
interval: "weekly"
- package-ecosystem: "github-actions"
directory: "/"
schedule:
interval: "weekly"

View File

@@ -1,11 +1,11 @@
name: Build
on:
pull_request:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
- cron: '00 01 * * *'
jobs:
check:
@@ -25,22 +25,22 @@ jobs:
override: true
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions-rs/cargo@v1
@@ -49,7 +49,7 @@ jobs:
test:
name: Test Suite
runs-on: ubuntu-latest
runs-on: ${{ matrix.os }}
steps:
- name: Checkout sources
uses: actions/checkout@v2
@@ -60,32 +60,37 @@ jobs:
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: ${{ matrix.rust }}
override: true
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions-rs/cargo@v1
with:
command: test
strategy:
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
rust: [stable]
lints:
name: Lints
runs-on: ubuntu-latest
@@ -104,22 +109,22 @@ jobs:
components: rustfmt, clippy
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
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
uses: actions-rs/cargo@v1

27
.github/workflows/gh-pages.yml vendored Normal file
View File

@@ -0,0 +1,27 @@
name: Github Pages
on:
push:
branches:
- master
jobs:
deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Setup mdBook
uses: peaceiris/actions-mdbook@v1
with:
mdbook-version: 'latest'
# mdbook-version: '0.4.8'
- run: mdbook build book
- name: Deploy
uses: peaceiris/actions-gh-pages@v3
if: github.ref == 'refs/heads/master'
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./book/book

View File

@@ -37,6 +37,10 @@ jobs:
rust: stable
target: x86_64-pc-windows-msvc
cross: false
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu
@@ -63,8 +67,9 @@ jobs:
- name: Run cargo test
uses: actions-rs/cargo@v1
with:
use-cross: ${{ matrix.cross }}
command: test
args: --locked
args: --release --locked --target ${{ matrix.target }}
- name: Build release binary
uses: actions-rs/cargo@v1
@@ -95,8 +100,9 @@ jobs:
else
cp "target/${{ matrix.target }}/release/hx" "dist/"
fi
cp -r runtime dist
- uses: actions/upload-artifact@v1
- uses: actions/upload-artifact@v2.2.4
with:
name: bins-${{ matrix.build }}
path: dist
@@ -144,7 +150,7 @@ jobs:
pkgname=helix-$TAG-$platform
mkdir 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
chmod +x tmp/$pkgname/hx$exe

2
.gitignore vendored
View File

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

52
.gitmodules vendored
View File

@@ -82,3 +82,55 @@
path = helix-syntax/languages/tree-sitter-toml
url = https://github.com/ikatyang/tree-sitter-toml
shallow = true
[submodule "helix-syntax/languages/tree-sitter-elixir"]
path = helix-syntax/languages/tree-sitter-elixir
url = https://github.com/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

547
Cargo.lock generated

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,7 @@
# Helix
[![Build status](https://github.com/helix-editor/helix/workflows/ci/badge.svg)](https://github.com/helix-editor/helix/actions)
[![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions)
![Screenshot](./screenshot.png)
@@ -10,7 +10,12 @@ A kakoune / neovim inspired editor, written in Rust.
The editing model is very heavily based on kakoune; during development I found
myself agreeing with most of kakoune's design decisions.
For more information, see the [website](https://helix-editor.com).
For more information, see the [website](https://helix-editor.com) or
[documentation](https://docs.helix-editor.com/).
All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html).
[Troubleshooting](https://github.com/helix-editor/helix/wiki/Troubleshooting)
# Features
@@ -24,7 +29,8 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
# Installation
Note: Only the Rust syntax has 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
build from source.
@@ -37,18 +43,37 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`.
Now copy the `runtime/` directory somewhere. Helix will by default look for the
runtime inside the same folder as the executable, but that can be overriden via
the `HELIX_RUNTIME` environment variable.
Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the
config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
via the `HELIX_RUNTIME` environment variable.
Packages already solve this for you by wrapping the `hx` binary with a wrapper
that sets the variable to the install dir.
> NOTE: running via cargo also doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
> detect the `runtime` directory in the project root.
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## MacOS
Helix can be installed on MacOS through homebrew via:
```
brew tap helix-editor/helix
brew install helix
```
# Contributing
Contributors are very welcome! **No contribution is too small and all contributions are valued.**
Some suggestions to get started:
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/good%20first%20issue) label on the issue tracker.
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
- Help with packaging on various distributions needed!
- To use print debugging to the `~/.cache/helix/helix.log` file, you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
- If your preferred language is missing, integrating a tree-sitter grammar for
it and defining syntax highlight queries for it is straight forward and
doesn't require much knowledge of the internals.
@@ -58,5 +83,6 @@ a good overview of the internals.
# Getting help
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-editor: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.
- syntax errors highlight query
------
- tree sitter:
- lua
- markdown
- zig
- regex
- vue
- kotlin
- julia
- clojure
- 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
- [ ] extract indentation calculation queries so we can support other languages.
1
- [ ] :format/:fmt that formats the buffer
- [ ] respect view fullscreen flag
- [ ] Implement marks (superset of Selection/Range)
- [ ] nixos packaging
- [ ] CI binary builds
- [ ] = for auto indent line/selection
- [ ] :x for closing buffers
- [ ] repeat selection
- [] jump to alt buffer
- [ ] lsp: signature help
- [x] lsp: hover
- [ ] lsp: document symbols (nested/vec)
- [ ] lsp: code actions
- [ ] lsp: formatting
- [x] lsp: goto
- [ ] search: smart case by default: insensitive unless upper detected
- [ ] move Compositor into tui
2
- [ ] surround bindings (select + surround ( wraps selection in parens )
- [ ] macro recording
- [ ] extend selection (treesitter select parent node) (replaces viw, vi(, va( etc )
- [x] bracket pairs
- [x] comment block (gcc)
- [ ] selection align
- [ ] store some state between restarts: file positions, prompt history
- [ ] highlight matched characters in completion
- [ ] highlight matched characters in picker
3
- [ ] diff mode with highlighting?

View File

@@ -3,4 +3,9 @@ authors = ["Blaž Hrastnik"]
language = "en"
multilingual = false
src = "src"
theme = "colibri"
edit-url-template = "https://github.com/helix-editor/helix/tree/master/book/{path}?mode=edit"
[output.html]
cname = "docs.helix-editor.com"
default-theme = "colibri"
preferred-dark-theme = "colibri"

View File

@@ -2,6 +2,11 @@
- [Installation](./install.md)
- [Usage](./usage.md)
- [Migrating from Vim](./from-vim.md)
- [Configuration](./configuration.md)
- [Themes](./themes.md)
- [Keymap](./keymap.md)
- [Key Remapping](./remapping.md)
- [Hooks](./hooks.md)
- [Guides](./guides/README.md)
- [Adding Textobject Queries](./guides/textobject.md)

View File

@@ -1 +1,32 @@
# 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,13 +2,15 @@
We provide pre-built binaries on the [GitHub Releases page](https://github.com/helix-editor/helix/releases).
[![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions)
## OSX
TODO: brew tap
A Homebrew tap is available:
```
$ brew tap helix-editor/helix
$ brew install helix
brew tap helix-editor/helix
brew install helix
```
## Linux
@@ -21,7 +23,11 @@ shell for working on Helix.
### Arch Linux
TODO: 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-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
## Build from source
@@ -33,6 +39,6 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`.
Now copy the `runtime/` directory somewhere. Helix will by default look for the
runtime inside the same folder as the executable, but that can be overriden via
the `HELIX_RUNTIME` environment variable.
Helix also needs it's runtime files so make sure to copy/symlink the `runtime/` directory into the
config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden
via the `HELIX_RUNTIME` environment variable.

View File

@@ -4,138 +4,257 @@
### Movement
| Key | Description |
|-----|-----------|
| h | move left |
| j | move down |
| k | move up |
| l | move right |
| w | move next word start |
| b | move previous word start |
| e | move next word end |
| t | find 'till next char |
| f | find next char |
| T | find 'till previous char |
| F | find previous char |
| 0 | move to the start of the line |
| $ | move to the end of the line |
| m | Jump to matching bracket |
| PageUp | Move page up |
| PageDown | Move page down |
| ctrl-u | Move half page up |
| ctrl-d | Move half page down |
| Tab | Switch to next view |
| ctrl-i | Jump forward on the jumplist TODO: conflicts tab |
| ctrl-o | Jump backward on the jumplist |
| v | Enter select (extend) mode |
| g | Enter goto mode |
| : | Enter command mode |
| z | Enter view mode |
| space | Enter space mode |
| K | Show documentation for the item under the cursor |
> NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `h`/`Left` | Move left | `move_char_left` |
| `j`/`Down` | Move down | `move_line_down` |
| `k`/`Up` | Move up | `move_line_up` |
| `l`/`Right` | Move right | `move_char_right` |
| `w` | Move next word start | `move_next_word_start` |
| `b` | Move previous word start | `move_prev_word_start` |
| `e` | Move next word end | `move_next_word_end` |
| `W` | Move next WORD start | `move_next_long_word_start` |
| `B` | Move previous WORD start | `move_prev_long_word_start` |
| `E` | Move next WORD end | `move_next_long_word_end` |
| `t` | Find 'till next char | `find_till_char` |
| `f` | Find next char | `find_next_char` |
| `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
| `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` |
| `PageUp` | Move page up | `page_up` |
| `PageDown` | Move page down | `page_down` |
| `Ctrl-u` | Move half page up | `half_page_up` |
| `Ctrl-d` | Move half page down | `half_page_down` |
| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` |
| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` |
| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` |
| `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
| Key | Description |
|-----|-----------|
| r | replace (single character change) |
| i | Insert before selection |
| a | Insert after selection (append) |
| I | Insert at the start of the line |
| A | Insert at the end of the line |
| o | Open new line below selection |
| o | Open new line above selection |
| u | Undo change |
| U | Redo change |
| y | Yank selection |
| p | Paste after selection |
| P | Paste before selection |
| > | Indent selection |
| < | Unindent selection |
| = | Format selection |
| d | Delete selection |
| c | Change selection (delete and enter insert mode) |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `r` | Replace with a character | `replace` |
| `R` | Replace with yanked text | `replace_with_yanked` |
| `~` | Switch case of the selected text | `switch_case` |
| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
| `i` | Insert before selection | `insert_mode` |
| `a` | Insert after selection (append) | `append_mode` |
| `I` | Insert at the start of the line | `prepend_to_line` |
| `A` | Insert at the end of the line | `append_to_line` |
| `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` |
| `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` |
| `U` | Redo change | `redo` |
| `y` | Yank selection | `yank` |
| `p` | Paste after selection | `paste_after` |
| `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
| Key | Description |
|-----|-----------|
| s | Select all regex matches inside selections |
| S | Split selection into subselections on regex matches |
| alt-s | Split selection on newlines |
| ; | Collapse selection onto a single cursor |
| alt-; | Flip selection cursor and anchor |
| % | Select entire file |
| x | Select current line |
| X | Extend to next line |
| [ | Expand selection to parent syntax node TODO: pick a key |
| J | join lines inside selection |
| K | keep selections matching the regex TODO: overlapped by hover help |
| space | keep only the primary selection TODO: overlapped by space mode |
| ctrl-c | Comment/uncomment the selections |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into subselections on regex matches | `split_selection` |
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| `,` | Keep only the primary selection | `keep_primary_selection` |
| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
| `(` | Rotate main selection backward | `rotate_selections_backward` |
| `)` | Rotate main selection forward | `rotate_selections_forward` |
| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
| `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
> TODO: The search implementation isn't ideal yet -- we don't support searching
in reverse, or searching via smartcase.
> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
| Key | Description |
|-----|-----------|
| / | Search for regex pattern |
| n | Select next search match |
| N | Add next search match to selection |
| * | Use current selection as the search pattern |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `/` | Search for regex pattern | `search` |
| `n` | Select next search match | `search_next` |
| `N` | Add next search match to selection | `extend_search_next` |
| `*` | 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
commands to extend the existing selection instead of replacing it.
These sub-modes are accessible from normal mode and typically switch back to normal mode after a command.
> NOTE: It's a bit confusing at the moment because extend hasn't been
> implemented for all movement commands yet.
## View mode
#### View mode
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.
> NOTE: Some of these features are only available with the LSP present.
| Key | Description |
|-----|-----------|
| g | Go to the start of the file |
| e | Go to the end of the file |
| e | Go to definition |
| t | Go to type definition |
| r | Go to references |
| i | Go to implementation |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `g` | Go to the start of the file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` |
| `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
| `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 `[`).
## 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 |
|-----|-----------|
| f | Open file picker |
| b | Open buffer picker |
| v | Open a new vertical split into the current file |
| w | Save changes to file |
| c | Close the current split |
| space | Keep primary selection TODO: it's here because space mode replaced it |
| Key | Description | 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 |
| ----- | ------------- |
| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
| `Ctrl-space` | Filter options |
| `Enter` | Open selected |
| `Ctrl-s` | Open horizontally |
| `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
(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;
}
:not(pre):not(a) > .hljs {
:not(pre):not(a):not(td):not(p) > .hljs {
color: var(--inline-code-color);
overflow-x: initial;
}

View File

@@ -109,11 +109,24 @@ h6:target::before {
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p, .content ol, .content ul, .content table, .content blockquote {
.content p, .content ol, .content ul, .content table {
margin-top: 0;
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 ol { line-height: 1.45em; }
.content ul { line-height: 1.45em; }
@@ -123,8 +136,7 @@ h6:target::before {
.content .header:link,
.content .header:visited {
color: var(--fg);
/* color: white; */
color: #281733;
color: var(--heading-fg);
}
.content .header:link,
.content .header:visited:hover {
@@ -150,7 +162,6 @@ table thead td {
table thead th {
padding: .75rem;
text-align: left;
color: var(--table-border-color);
font-weight: 500;
line-height: 1.5;
width: auto;
@@ -168,12 +179,15 @@ table tbody tr:nth-child(2n) {
blockquote {
margin: 20px 0;
padding: 0 20px;
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-top: .1em solid var(--quote-border);
border-bottom: .1em solid var(--quote-border);
border-left: 4px solid var(--quote-border);
}
blockquote *:last-child {
margin-bottom: 0;
}
@@ -213,3 +227,7 @@ blockquote {
margin: 5px 0px;
font-weight: bold;
}
.result-no-output {
font-style: italic;
}

View File

@@ -67,7 +67,7 @@
--links: #2b79a2;
--inline-code-color: #c5c8c6;;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
@@ -147,7 +147,7 @@
--links: #2b79a2;
--inline-code-color: #c5c8c6;;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
@@ -213,7 +213,7 @@
@media (prefers-color-scheme: dark) {
.light.no-js {
--bg: hsl(200, 7%, 8%);
--fg: #ebeafa;
--fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
@@ -228,7 +228,7 @@
--links: #2b79a2;
--inline-code-color: #c5c8c6;;
--inline-code-color: #c5c8c6;
--theme-popup-bg: #141617;
--theme-popup-border: #43484d;
@@ -255,6 +255,7 @@
.colibri {
--bg: #3b224c;
--fg: #bcbdd0;
--heading-fg: #fff;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
@@ -270,18 +271,19 @@
/* --links: #a4a0e8; */
--links: #ECCDBA;
--inline-code-color: #c5c8c6;;
--inline-code-color: hsl(48.7, 7.8%, 70%);
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: hsl(226, 15%, 17%);
--quote-bg: #281733;
--quote-border: hsl(226, 15%, 22%);
--table-border-color: hsl(226, 23%, 16%);
--table-header-bg: hsl(226, 23%, 31%);
--table-border-color: hsl(226, 23%, 76%);
--table-header-bg: hsla(226, 23%, 31%, 0);
--table-alternate-bg: hsl(226, 23%, 14%);
--table-border-line: hsla(201deg, 20%, 92%, 0.2);
--searchbar-border-color: #aaa;
--searchbar-bg: #aeaec6;
@@ -294,9 +296,11 @@
}
.colibri {
/*
--bg: #ffffff;
--fg: #452859;
--fg: #5a5977;
--heading-fg: #281733;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
@@ -311,14 +315,14 @@
--links: #6F44F0;
--inline-code-color: #697C81;
--inline-code-color: #a39e9b;
--theme-popup-bg: #161923;
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--quote-bg: rgba(0, 0, 0, 0);
--quote-border: hsl(226, 15%, 75%);
--table-border-color: #5a5977;
--table-border-color: hsl(201deg 10% 67%);
@@ -334,4 +338,5 @@
--searchresults-border-color: #5c5c68;
--searchresults-li-bg: #242430;
--search-mark-bg: #a2cff5;
*/
}

View File

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

1
contrib/themes Symbolic link
View File

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

View File

@@ -8,68 +8,78 @@
| helix-term | Terminal UI |
| helix-tui | TUI primitives, forked from tui-rs, inspired by Cursive |
# Notes
- server-client architecture via gRPC, UI separate from core
- multi cursor based editing and slicing
- WASM based plugins (builtin LSP & fuzzy file finder)
This document contains a high-level overview of Helix internals.
Structure similar to codemirror:
> NOTE: Use `cargo doc --open` for API documentation as well as dependency
> documentation.
- text (ropes)
- transactions
- changes
- invert changes (generates a revert)
- annotations (time changed etc)
- state effects
- additional editor state as facets
- snapshots as an async view into current state
- selections { anchor (nonmoving), head (moving) from/to } -> SelectionSet with a primary
- cursor is just a single range selection
- markers
track a position inside text that synchronizes with edits
- { doc, selection, update(), splice, changes(), facets, tabSize, identUnit, lineSeparator, changeFilter/transactionFilter to modify stuff before }
- view (actual UI)
- viewport(Lines) -> what's actually visible
- extend the view via Decorations (inline styling) or Components (UI)
- mark / wieget / line / replace decoration
- commands (transform state)
- movement
- selection extension
- deletion
- indentation
- keymap (maps keys to commands)
- history (undo tree via immutable ropes)
- undoes transactions via reverts
- (collab mode)
- gutter (line numbers, diagnostic marker, etc) -> ties into UI components
- rangeset/span -> mappable over changes (can be a marker primitive?)
- syntax (treesitter)
- fold
- selections (select mode/multiselect)
- matchbrackets
- closebrackets
- special-chars (shows dots etc for specials)
- panel (for UI: file pickers, search dialogs, etc)
- tooltip (for UI)
- search (regex)
- lint (async linters)
- lsp
- highlight
- stream-syntax
- autocomplete
- comment (gc, etc for auto commenting)
- snippets
- terminal mode?
## Core
- plugins can contain more commands/ui abstractions to use elsewhere
- languageData as presets for each language (syntax, indent, comment, etc)
The core contains basic building blocks used to construct the editor. It is
heavily based on [CodeMirror 6](https://codemirror.net/6/docs/). The primitives
are functional: most operations won't modify data in place but instead return
a new copy.
Vim stuff:
- motions/operators/text objects
- full visual mode
- macros
- jump lists
- marks
- yank/paste
- conceal for markdown markers, etc
The main data structure used for representing buffers is a `Rope`. We re-export
the excellent [ropey](https://github.com/cessen/ropey) library. Ropes are cheap
to clone, and allow us to easily make snapshots of a text state.
Multiple selections are a core editing primitive. Document selections are
represented by a `Selection`. Each `Range` in the selection consists of a moving
`head` and an immovable `anchor`. A single cursor in the editor is simply
a selection with a single range, with the head and the anchor in the same
position.
Ropes are modified by constructing an OT-like `Transaction`. It's represents
a single coherent change to the document and can be applied to the rope.
A transaction can be inverted to produce an undo. Selections and marks can be
mapped over a transaction to translate to a position in the new text state after
applying the transaction.
> NOTE: `Transaction::change`/`Transaction::change_by_selection` is the main
> interface used to generate text edits.
`Syntax` is the interface used to interact with tree-sitter ASTs for syntax
highling and other features.
## View
The `view` layer was supposed to be a frontend-agnostic imperative library that
would build on top of `core` to provide the common editor logic. Currently it's
tied to the terminal UI.
A `Document` ties together the `Rope`, `Selection`(s), `Syntax`, document
`History`, language server (etc.) into a comprehensive representation of an open
file.
A `View` represents an open split in the UI. It holds the currently open
document ID and other related state.
> NOTE: Multiple views are able to display the same document, so the document
> contains selections for each view. To retrieve, `document.selection()` takes
> a `ViewId`.
The `Editor` holds the global state: all the open documents, a tree
representation of all the view splits, and a registry of language servers. To
open or close files, interact with the editor.
## LSP
A language server protocol client.
## Term
The terminal frontend.
The `main` function sets up a new `Application` that runs the event loop.
`commands.rs` is probably the most interesting file. It contains all commands
(actions tied to keybindings).
`keymap.rs` links commands to key combinations.
## TUI / Term
TODO: document Component and rendering related stuff

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": {
"devshell": {
"locked": {
"lastModified": 1632436039,
"narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
"owner": "numtide",
"repo": "devshell",
"rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "devshell",
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1619345332,
"narHash": "sha256-qHnQkEp1uklKTpx3MvKtY6xzgcqXDsz5nLilbbuL+3A=",
"lastModified": 1623875721,
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "2ebf2558e5bf978c7fb8ea927dfaed8fefab2e28",
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
"type": "github"
},
"original": {
@@ -15,59 +30,53 @@
"type": "github"
}
},
"flake-utils_2": {
"flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1614513358,
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3",
"lastModified": 1627913399,
"narHash": "sha256-hY8g6H2KFL8ownSiFeMOjwPC8P0ueXpCVEbxgda3pko=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "12c64ca55c1014cdc1b16ed5a804aa8576601ff2",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"naersk": {
"nixCargoIntegration": {
"inputs": {
"nixpkgs": "nixpkgs"
"devshell": "devshell",
"nixpkgs": [
"nixpkgs"
],
"rustOverlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1620316130,
"narHash": "sha256-sU0VS5oJS1FsHsZsLELAXc7G2eIelVuucRw+q5B1x9k=",
"owner": "nmattia",
"repo": "naersk",
"rev": "a3f40fe42cc6d267ff7518fa3199e99ff1444ac4",
"lastModified": 1634796585,
"narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
"type": "github"
},
"original": {
"owner": "nmattia",
"repo": "naersk",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1619775165,
"narHash": "sha256-2qaBErjxuWpTIq6Yee5GJmhr84hmzBotLQ0ayg1VXg8=",
"path": "/nix/store/gs997rgx3pvdgcb54wd3fi9wbnznd9g4-source",
"rev": "849b29b4f76d66ec7aeeeed699b7e27ef3db7c02",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1620252427,
"narHash": "sha256-U1Q5QceuT4chJTJ1UOt7bZOn9Y2o5/7w27RISjqXoQw=",
"lastModified": 1634782485,
"narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d3ba49889a76539ea0f7d7285b203e7f81326ded",
"rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
"type": "github"
},
"original": {
@@ -77,13 +86,13 @@
"type": "github"
}
},
"nixpkgs_3": {
"nixpkgs_2": {
"locked": {
"lastModified": 1617325113,
"narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=",
"owner": "nixos",
"lastModified": 1628186154,
"narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "54c1e44240d8a527a8f4892608c4bce5440c3ecb",
"rev": "06552b72346632b6943c8032e57e702ea12413bf",
"type": "github"
},
"original": {
@@ -94,23 +103,23 @@
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3"
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1620355527,
"narHash": "sha256-mUTnUODiAtxH83gbv7uuvCbqZ/BNkYYk/wa3MkwrskE=",
"lastModified": 1634869268,
"narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d8efe70dc561c4bea0b7bf440d36ce98c497e054",
"rev": "c02c2d86354327317546501af001886fbb53d374",
"type": "github"
},
"original": {

View File

@@ -3,31 +3,73 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
naersk.url = "github:nmattia/naersk";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay";
};
flakeCompat = {
url = "github:edolstra/flake-compat";
flake = false;
};
};
outputs = inputs@{ self, nixpkgs, naersk, rust-overlay, flake-utils, ... }:
flake-utils.lib.eachDefaultSystem (system:
let
pkgs = import nixpkgs { inherit system; overlays = [ rust-overlay.overlay ]; };
rust = (pkgs.rustChannelOf {
date = "2021-05-01";
channel = "nightly";
}).minimal; # cargo, rustc and rust-std
naerskLib = naersk.lib."${system}".override {
# naersk can't build with stable?!
# inherit (pkgs.rust-bin.stable.latest) rustc cargo;
rustc = rust;
cargo = rust;
outputs = inputs@{ self, nixCargoIntegration, ... }:
nixCargoIntegration.lib.makeOutputs {
root = ./.;
buildPlatform = "crate2nix";
renameOutputs = { "helix-term" = "helix"; };
# Set default app to hx (binary is from helix-term release build)
# Set default package to helix-term release build
defaultOutputs = { app = "hx"; package = "helix"; };
overrides = {
crateOverrides = common: _: {
helix-term = prev: {
# link languages and theme toml files since helix-term expects them (for tests)
preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} ..";
buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ];
};
# link 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";
};
};
in rec {
packages.helix = naerskLib.buildPackage {
pname = "helix";
root = ./.;
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]
name = "helix-core"
version = "0.1.0"
version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
edition = "2021"
license = "MPL-2.0"
description = "Helix editor core editing primitives"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
[dependencies]
helix-syntax = { path = "../helix-syntax" }
helix-syntax = { version = "0.5", path = "../helix-syntax" }
ropey = "1.2"
anyhow = "1"
smallvec = "1.4"
ropey = "1.3"
smallvec = "1.7"
tendril = "0.4.2"
unicode-segmentation = "1.6"
unicode-segmentation = "1.8"
unicode-width = "0.1"
unicode-general-category = "0.4"
# slab = "0.4.2"
tree-sitter = "0.19"
once_cell = "1.4"
tree-sitter = "0.20"
once_cell = "1.8"
arc-swap = "1"
regex = "1"
log = "0.4"
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
toml = "0.5"
similar = "2.1"
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 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:
// Fn(doc, selection, char) => Option<Transaction>
@@ -65,19 +68,26 @@ fn handle_open(
) -> Transaction {
let mut ranges = SmallVec::with_capacity(selection.len());
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);
let head = pos + open.len_utf8();
let head = pos + offs + open.len_utf8();
// if selection, retain anchor, if cursor, move over
ranges.push(Range::new(
if range.is_empty() { head } else { range.anchor },
if range.is_empty() {
head
} else {
range.anchor + offs
},
head,
));
match next {
Some(ch) if !close_before.contains(ch) => {
offs += 1;
// TODO: else return (use default handler that inserts open)
(pos, pos, Some(Tendril::from_char(open)))
}
@@ -88,6 +98,8 @@ fn handle_open(
pair.push_char(open);
pair.push_char(close);
offs += 2;
(pos, pos, Some(pair))
}
}
@@ -99,14 +111,20 @@ fn handle_open(
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
let mut ranges = SmallVec::with_capacity(selection.len());
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
let mut offs = 0;
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);
let head = pos + close.len_utf8();
let head = pos + offs + close.len_utf8();
// if selection, retain anchor, if cursor, move over
ranges.push(Range::new(
if range.is_empty() { head } else { range.anchor },
if range.is_empty() {
head
} else {
range.anchor + offs
},
head,
));
@@ -114,6 +132,8 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
// return transaction that moves past close
(pos, pos, None) // no-op
} else {
offs += close.len_utf8();
// TODO: else return (use default handler that inserts close)
(pos, pos, Some(Tendril::from_char(close)))
}
@@ -123,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""")
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
// 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::{
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;
/// 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(
token: &str,
text: RopeSlice,
lines: Range<usize>,
) -> (bool, Vec<usize>, usize) {
lines: impl IntoIterator<Item = usize>,
) -> (bool, Vec<usize>, usize, usize) {
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 margin = 1;
let token_len = token.chars().count();
for line in lines {
let line_slice = text.line(line);
if let Some(pos) = find_first_non_whitespace_char2(line_slice) {
if let Some(pos) = find_first_non_whitespace_char(line_slice) {
let len = line_slice.len_chars();
if pos < min {
@@ -29,47 +42,55 @@ fn find_line_comment(
// considered uncommented.
commented = false;
}
} else {
// blank line
skipped.push(line);
// determine margin of 0 or 1 for uncommenting; if any comment token is not followed by a space,
// 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]
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 mut changes: Vec<Change> = Vec::new();
let token = "//";
let token = token.unwrap_or("//");
let comment = Tendril::from(format!("{} ", token));
let mut lines: Vec<usize> = Vec::new();
let mut min_next_line = 0;
for selection in selection {
let start = text.char_to_line(selection.from());
let end = text.char_to_line(selection.to());
let lines = start..end + 1;
let (commented, skipped, min) = find_line_comment(token, text, lines.clone());
let (start, end) = selection.line_range(text);
let start = start.max(min_next_line).min(text.len_lines());
let end = (end + 1).min(text.len_lines());
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 pos = text.line_to_char(line) + min;
let mut changes: Vec<Change> = Vec::with_capacity(to_change.len());
if !commented {
// comment line
changes.push((pos, pos, Some(comment.clone())))
} else {
// uncomment line
let margin = 1; // TODO: margin is hardcoded 1 but could easily be 0
changes.push((pos, pos + token.len() + margin, None))
}
for line in to_change {
let pos = text.line_to_char(line) + min;
if !commented {
// comment line
changes.push((pos, pos, Some(comment.clone())));
} else {
// uncomment line
changes.push((pos, pos + token.len() + margin, None));
}
}
Transaction::change(doc, changes.into_iter())
}
@@ -91,23 +112,32 @@ mod test {
let text = state.doc.slice(..);
let res = find_line_comment("//", text, 0..3);
// (commented = true, skipped = [line 1], min = col 2)
assert_eq!(res, (false, vec![1], 2));
// (commented = true, to_change = [line 0, line 2], min = col 2, margin = 1)
assert_eq!(res, (false, vec![0, 2], 2, 1));
// 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);
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");
// 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);
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");
// TODO: account for no margin after comment
// 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 {
Error,
Warning,
@@ -6,10 +9,15 @@ pub enum Severity {
Hint,
}
/// A range of `char`s within the text.
#[derive(Debug)]
pub struct Range {
pub start: 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 range: Range,
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 unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
use std::fmt;
#[must_use]
pub fn grapheme_width(g: &str) -> usize {
if g.as_bytes()[0] <= 127 {
@@ -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.
#[must_use]
#[inline(always)]
pub fn prev_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
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.
#[must_use]
#[inline(always)]
pub fn next_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> usize {
nth_next_grapheme_boundary(slice, char_idx, 1)
}
/// Returns the passed char index if it's already a grapheme boundary,
/// or the next grapheme boundary char index if not.
#[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.
#[must_use]
pub fn is_grapheme_boundary(slice: RopeSlice, char_idx: usize) -> bool {
// Bounds check
debug_assert!(char_idx <= slice.len_chars());
@@ -156,6 +189,18 @@ pub struct RopeGraphemes<'a> {
cursor: GraphemeCursor,
}
impl<'a> fmt::Debug for RopeGraphemes<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("RopeGraphemes")
.field("text", &self.text)
.field("chunks", &self.chunks)
.field("cur_chunk", &self.cur_chunk)
.field("cur_chunk_start", &self.cur_chunk_start)
// .field("cursor", &self.cursor)
.finish()
}
}
impl<'a> RopeGraphemes<'a> {
#[must_use]
pub fn new(slice: RopeSlice) -> RopeGraphemes {
@@ -193,6 +238,10 @@ impl<'a> Iterator for RopeGraphemes<'a> {
self.cur_chunk_start += self.cur_chunk.len();
self.cur_chunk = self.chunks.next().unwrap_or("");
}
Err(GraphemeIncomplete::PreContext(idx)) => {
let (chunk, byte_idx, _, _) = self.text.chunk_at_byte(idx.saturating_sub(1));
self.cursor.provide_context(chunk, byte_idx);
}
_ => unreachable!(),
}
}

View File

@@ -1,19 +1,62 @@
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 {
revisions: Vec<Revision>,
cursor: usize,
current: usize,
}
/// A single point in history. See [History] for more information.
#[derive(Debug)]
struct Revision {
parent: usize,
children: SmallVec<[(usize, Transaction); 1]>,
/// The transaction to revert to previous state.
revert: Transaction,
// selection before, selection after?
last_child: Option<NonZeroUsize>,
transaction: Transaction,
// We need an inversion for undos because delete transactions don't store
// the deleted text.
inversion: Transaction,
timestamp: Instant,
}
impl Default for History {
@@ -22,74 +65,275 @@ impl Default for History {
Self {
revisions: vec![Revision {
parent: 0,
children: SmallVec::new(),
revert: Transaction::from(ChangeSet::new(&Rope::new())),
last_child: None,
transaction: Transaction::from(ChangeSet::new(&Rope::new())),
inversion: Transaction::from(ChangeSet::new(&Rope::new())),
timestamp: Instant::now(),
}],
cursor: 0,
current: 0,
}
}
}
impl History {
pub fn commit_revision(&mut self, transaction: &Transaction, original: &State) {
// TODO: could store a single transaction, if deletes also stored the text they delete
let revert = transaction
self.commit_revision_at_timestamp(transaction, original, Instant::now());
}
pub fn commit_revision_at_timestamp(
&mut self,
transaction: &Transaction,
original: &State,
timestamp: Instant,
) {
let inversion = transaction
.invert(&original.doc)
// Store the current cursor position
.with_selection(original.selection.clone());
let new_cursor = self.revisions.len();
let new_current = self.revisions.len();
self.revisions[self.current].last_child = NonZeroUsize::new(new_current);
self.revisions.push(Revision {
parent: self.cursor,
children: SmallVec::new(),
revert,
parent: self.current,
last_child: None,
transaction: transaction.clone(),
inversion,
timestamp,
});
// add a reference to the parent
self.revisions
.get_mut(self.cursor)
.unwrap() // TODO: get_unchecked_mut
.children
.push((new_cursor, transaction.clone()));
self.cursor = new_cursor;
self.current = new_current;
}
#[inline]
pub fn current_revision(&self) -> usize {
self.cursor
self.current
}
#[inline]
pub const fn at_root(&self) -> bool {
self.cursor == 0
self.current == 0
}
// TODO: I'd like to pass Transaction by reference but it fights with the borrowck
pub fn undo(&mut self) -> Option<Transaction> {
/// Undo the last edit.
pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() {
// We're at the root of undo, nothing to do.
return None;
}
let current_revision = &self.revisions[self.cursor];
self.cursor = current_revision.parent;
Some(current_revision.revert.clone())
let current_revision = &self.revisions[self.current];
self.current = current_revision.parent;
Some(&current_revision.inversion)
}
pub fn redo(&mut self) -> Option<Transaction> {
let current_revision = &self.revisions[self.cursor];
/// Redo the last edit.
pub fn redo(&mut self) -> Option<&Transaction> {
let current_revision = &self.revisions[self.current];
let last_child = current_revision.last_child?;
self.current = last_child.get();
// for now, simply pick the latest child (linear undo / redo)
if let Some((index, transaction)) = current_revision.children.last() {
self.cursor = *index;
Some(&self.revisions[last_child.get()].transaction)
}
return Some(transaction.clone());
fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
use std::collections::HashSet;
let mut a_path_set = HashSet::new();
let mut b_path_set = HashSet::new();
loop {
a_path_set.insert(a);
b_path_set.insert(b);
if a_path_set.contains(&b) {
return b;
}
if b_path_set.contains(&a) {
return a;
}
a = self.revisions[a].parent; // Relies on the parent of 0 being 0.
b = self.revisions[b].parent; // Same as above.
}
}
/// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
/// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
let mut path = Vec::new();
while n != a {
path.push(n);
n = self.revisions[n].parent;
}
path
}
/// 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);
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::{
chars::{char_is_line_ending, char_is_whitespace},
find_first_non_whitespace_char,
syntax::Syntax,
tree_sitter::{Node, Tree},
syntax::{IndentQuery, LanguageConfiguration, Syntax},
tree_sitter::Node,
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
/// of the previous line.
#[allow(dead_code)]
fn indent_level_for_line(line: RopeSlice, tab_width: usize) -> usize {
let mut len = 0;
for ch in line.chars() {
@@ -43,40 +210,11 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
Some(node)
}
fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
let mut increment: i32 = 0;
fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
// 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
// Hardcoded for rust for now
let indent_scopes = &[
"while_expression",
"for_expression",
"loop_expression",
"if_expression",
"if_let_expression",
// "match_expression",
// "match_arm",
"tuple_expression",
"array_expression",
// indent_except_first_scopes
"use_list",
"block",
"match_block",
"arguments",
"parameters",
"declaration_list",
"field_declaration_list",
"field_initializer_list",
"struct_pattern",
"tuple_pattern",
"enum_variant_list",
// "function_item",
// "closure_expression",
"binary_expression",
"field_expression",
"where_clause",
];
let outdent = &["where", "}", "]", ")"];
let mut increment: isize = 0;
let mut node = match node {
Some(node) => node,
@@ -88,7 +226,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
// if we're calculating indentation for a brand new line then the current node will become the
// parent node. We need to take it's indentation level into account too.
let node_kind = node.kind();
if newline && indent_scopes.contains(&node_kind) {
if newline && query.indent.contains(node_kind) {
increment += 1;
}
@@ -102,14 +240,14 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
// }) <-- }) is two scopes
let starts_same_line = start == prev_start;
if outdent.contains(&node.kind()) && !starts_same_line {
if query.outdent.contains(node.kind()) && !starts_same_line {
// we outdent by skipping the rules for the current level and jumping up
// node = parent;
increment -= 1;
// continue;
}
if indent_scopes.contains(&parent_kind) // && not_first_or_last_sibling
if query.indent.contains(parent_kind) // && not_first_or_last_sibling
&& !starts_same_line
{
// println!("is_scope {}", parent_kind);
@@ -122,22 +260,25 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
node = parent;
}
assert!(increment >= 0);
increment as usize
increment.max(0) as usize
}
#[allow(dead_code)]
fn suggested_indent_for_line(
language_config: &LanguageConfiguration,
syntax: Option<&Syntax>,
text: RopeSlice,
line_num: usize,
tab_width: usize,
_tab_width: usize,
) -> usize {
let line = text.line(line_num);
let current = indent_level_for_line(line, tab_width);
if let Some(start) = find_first_non_whitespace_char(text, line_num) {
return suggested_indent_for_pos(syntax, text, start, false);
if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
return suggested_indent_for_pos(
Some(language_config),
syntax,
text,
start + text.line_to_char(line_num),
false,
);
};
// if the line is blank, indent should be zero
@@ -148,27 +289,59 @@ fn suggested_indent_for_line(
// - it should return 0 when mass indenting stuff
// - it should look up the wrapper node and count it too when we press o/O
pub fn suggested_indent_for_pos(
language_config: Option<&LanguageConfiguration>,
syntax: Option<&Syntax>,
text: RopeSlice,
pos: usize,
new_line: bool,
) -> usize {
if let Some(syntax) = syntax {
if let (Some(query), Some(syntax)) = (
language_config.and_then(|config| config.indent_query()),
syntax,
) {
let byte_start = text.char_to_byte(pos);
let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
// let config = load indentation query config from Syntax(should contain language_config)
// TODO: special case for comments
// TODO: if preserve_leading_whitespace
calculate_indentation(node, new_line)
calculate_indentation(query, node, new_line)
} else {
// TODO: heuristics for non-tree sitter grammars
0
}
}
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)]
mod test {
use super::*;
use crate::Rope;
#[test]
fn test_indent_level() {
@@ -270,22 +443,28 @@ where
let doc = Rope::from(doc);
use crate::syntax::{
Configuration, IndentationConfiguration, Lang, LanguageConfiguration, Loader,
Configuration, IndentationConfiguration, LanguageConfiguration, Loader,
};
use once_cell::sync::OnceCell;
let loader = Loader::new(Configuration {
language: vec![LanguageConfiguration {
scope: "source.rust".to_string(),
file_types: vec!["rs".to_string()],
language_id: Lang::Rust,
language_id: "Rust".to_string(),
highlight_config: OnceCell::new(),
config: None,
//
injection_regex: None,
roots: vec![],
comment_token: None,
auto_format: false,
language_server: None,
indent: Some(IndentationConfiguration {
tab_width: 4,
unit: String::from(" "),
}),
indent_query: OnceCell::new(),
textobject_query: OnceCell::new(),
}],
});
@@ -304,7 +483,7 @@ where
let line = text.line(i);
let indent = indent_level_for_line(line, tab_width);
assert_eq!(
suggested_indent_for_line(Some(&syntax), text, i, tab_width),
suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
indent,
"line {}: {}",
i,

View File

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

View File

@@ -0,0 +1,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 ( ) { } [ ] < >
#[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,
// return the other edge.
let mut node = match tree
let node = match tree
.root_node()
.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,
};
let start_byte = node.start_byte();
let end_byte = node.end_byte() - 1; // it's end exclusive
if start_byte == byte_pos {
return Some(doc.byte_to_char(end_byte));
if node.is_error() {
return None;
}
if end_byte == byte_pos {
return Some(doc.byte_to_char(start_byte));
let 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 {
return Some(end_char);
}
if end_byte == byte_pos {
return Some(start_char);
}
}
None

File diff suppressed because it is too large Load Diff

View File

@@ -1,12 +1,11 @@
use crate::{Range, RopeSlice, Selection, Syntax};
use smallvec::smallvec;
// TODO: to contract_selection we'd need to store the previous ranges before expand.
// Maybe just contract to the first child node?
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection) -> Selection {
let tree = syntax.tree();
selection.transform(|range| {
selection.clone().transform(|range| {
let from = text.char_to_byte(range.from());
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::{
graphemes::{nth_next_grapheme_boundary, RopeGraphemes},
Rope, RopeSlice,
chars::char_is_line_ending,
graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
line_ending::line_end_char_index,
RopeSlice,
};
/// Represents a single point in a text buffer. Zero indexed.
@@ -23,8 +25,9 @@ impl Position {
pub fn traverse(self, text: &crate::Tendril) -> Self {
let Self { mut row, mut col } = self;
// TODO: there should be a better way here
for ch in text.chars() {
if ch == '\n' {
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if char_is_line_ending(ch) && !(ch == '\r' && chars.peek() == Some(&'\n')) {
row += 1;
col = 0;
} else {
@@ -50,24 +53,65 @@ impl From<Position> for tree_sitter::Point {
}
}
/// 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 {
let line = text.char_to_line(pos);
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();
Position::new(line, col)
}
/// 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 line_start = text.line_to_char(row);
// line_start + col
nth_next_grapheme_boundary(text, line_start, col)
let line_end = if limit_before_line_ending {
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)]
mod test {
use super::*;
use crate::Rope;
#[test]
fn test_ordering() {
@@ -79,53 +123,107 @@ mod test {
fn test_coords_at_pos() {
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, 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, 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, 0), (0, 0).into());
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, 7), (1, 1).into()); // position on o
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 slice = text.slice(..);
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, 4), (0, 2).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(..);
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, 3), (0, 2).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]
fn test_pos_at_coords() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
let slice = text.slice(..);
assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
assert_eq!(pos_at_coords(slice, (0, 5).into()), 5); // position on \n
assert_eq!(pos_at_coords(slice, (1, 0).into()), 6); // position on w
assert_eq!(pos_at_coords(slice, (1, 1).into()), 7); // position on o
assert_eq!(pos_at_coords(slice, (1, 4).into()), 10); // position on d
assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 5).into(), false), 5); // position on \n
assert_eq!(pos_at_coords(slice, (0, 6).into(), false), 6); // position after \n
assert_eq!(pos_at_coords(slice, (0, 6).into(), true), 5); // position after \n
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 slice = text.slice(..);
assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
assert_eq!(pos_at_coords(slice, (0, 2).into()), 4);
assert_eq!(pos_at_coords(slice, (0, 3).into()), 7); // \r\n is one char here
assert_eq!(pos_at_coords(slice, (0, 4).into()), 9);
assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 2);
assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 4);
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(), 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("किमपि");
// 2 - 1 - 2 codepoints
// TODO: delete handling as per https://news.ycombinator.com/item?id=20058454
let slice = text.slice(..);
assert_eq!(pos_at_coords(slice, (0, 0).into()), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into()), 2);
assert_eq!(pos_at_coords(slice, (0, 2).into()), 3);
assert_eq!(pos_at_coords(slice, (0, 3).into()), 5); // eol
assert_eq!(pos_at_coords(slice, (0, 0).into(), false), 0);
assert_eq!(pos_at_coords(slice, (0, 1).into(), false), 2);
assert_eq!(pos_at_coords(slice, (0, 2).into(), false), 3);
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 once_cell::sync::Lazy;
use std::{collections::HashMap, sync::RwLock};
use std::collections::HashMap;
// TODO: could be an instance on Editor
static REGISTRY: Lazy<RwLock<HashMap<char, Vec<String>>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
pub fn get(register: char) -> Option<Vec<String>> {
let registry = REGISTRY.read().unwrap();
// TODO: no cloning
registry.get(&register).cloned()
#[derive(Debug)]
pub struct Register {
name: char,
values: Vec<String>,
}
// restoring: bool
pub fn set(register: char, values: Vec<String>) {
let mut registry = REGISTRY.write().unwrap();
impl Register {
pub const fn new(name: char) -> Self {
Self {
name,
values: Vec::new(),
}
}
registry.insert(register, values);
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
Self { name, values }
}
pub 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,14 +1,11 @@
use crate::RopeSlice;
pub fn find_nth_next(
text: RopeSlice,
ch: char,
mut pos: usize,
n: usize,
inclusive: bool,
) -> Option<usize> {
// start searching right after pos
let mut chars = text.chars_at(pos + 1);
pub fn find_nth_next(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
if pos >= text.len_chars() || n == 0 {
return None;
}
let mut chars = text.chars_at(pos);
for _ in 0..n {
loop {
@@ -22,28 +19,21 @@ pub fn find_nth_next(
}
}
if !inclusive {
pos -= 1;
}
Some(pos)
Some(pos - 1)
}
pub fn find_nth_prev(
text: RopeSlice,
ch: char,
mut pos: usize,
n: usize,
inclusive: bool,
) -> Option<usize> {
// start searching right before pos
let mut chars = text.chars_at(pos.saturating_sub(1));
pub fn find_nth_prev(text: RopeSlice, ch: char, mut pos: usize, n: usize) -> Option<usize> {
if pos == 0 || n == 0 {
return None;
}
let mut chars = text.chars_at(pos);
for _ in 0..n {
loop {
let c = chars.prev()?;
pos = pos.saturating_sub(1);
pos -= 1;
if c == ch {
break;
@@ -51,9 +41,5 @@ pub fn find_nth_prev(
}
}
if !inclusive {
pos -= 1;
}
Some(pos)
}

View File

@@ -1,30 +1,59 @@
//! Selections are the primary editing construct. Even a single cursor is defined as an empty
//! single selection range.
//! Selections are the primary editing construct. Even cursors are
//! defined as a selection range.
//!
//! 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 std::borrow::Cow;
#[inline]
fn abs_difference(x: usize, y: usize) -> usize {
if x < y {
y - x
} else {
x - y
}
}
/// A single selection range. Anchor-inclusive, head-exclusive.
/// A single selection range.
///
/// A range consists of an "anchor" and "head" position in
/// the text. The head is the part that the user moves when
/// directly extending a selection. The head and anchor
/// 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
/// 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)]
pub struct Range {
// TODO: optimize into u32
/// The anchor of the range: the side that doesn't move when extending.
pub anchor: usize,
/// The head of the range, moved when extending.
pub head: usize,
pub horiz: Option<u32>,
} // TODO: might be cheaper to store normalized as from/to and an inverted flag
}
impl Range {
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.
#[inline]
#[must_use]
@@ -49,6 +82,20 @@ impl Range {
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.
#[inline]
pub fn is_empty(&self) -> bool {
@@ -58,37 +105,39 @@ impl Range {
/// Check two ranges for overlap.
#[must_use]
pub fn overlaps(&self, other: &Self) -> bool {
// cursor overlap is checked differently
if self.is_empty() {
let pos = self.head;
pos >= other.from() && other.to() >= pos
} else {
self.to() > other.from() && other.to() > self.from()
}
// To my eye, it's non-obvious why this works, but I arrived
// at it after transforming the slower version that explicitly
// enumerated more cases. The unit tests are thorough.
self.from() == other.from() || (self.to() > other.from() && other.to() > self.from())
}
pub fn contains(&self, pos: usize) -> bool {
if self.is_empty() {
return false;
}
if self.anchor < self.head {
self.anchor <= pos && pos < self.head
} else {
self.head < pos && pos <= self.anchor
}
self.from() <= pos && pos < self.to()
}
/// Map a range through a set of changes. Returns a new range representing the same position
/// after the changes are applied.
pub fn map(self, changes: &ChangeSet) -> Self {
let anchor = changes.map_pos(self.anchor, Assoc::After);
let head = changes.map_pos(self.head, Assoc::After);
use std::cmp::Ordering;
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
if self.anchor == anchor && self.head == head {
return self;
}
// We want to return a new `Range` with `horiz == None` every time,
// even if the anchor and head haven't changed, because we don't
// know if the *visual* position hasn't changed due to
// character-width or grapheme changes earlier in the text.
Self {
anchor,
head,
@@ -99,22 +148,41 @@ impl Range {
/// Extend the range to cover at least `from` `to`.
#[must_use]
pub fn extend(&self, from: usize, to: usize) -> Self {
if from <= self.anchor && to >= self.anchor {
return Self {
anchor: from,
head: to,
horiz: None,
};
}
debug_assert!(from <= to);
Self {
anchor: self.anchor,
head: if abs_difference(from, self.anchor) > abs_difference(to, self.anchor) {
from
} else {
to
},
horiz: None,
if self.anchor <= self.head {
Self {
anchor: self.anchor.min(from),
head: self.head.max(to),
horiz: None,
}
} else {
Self {
anchor: self.anchor.max(to),
head: self.head.min(from),
horiz: None,
}
}
}
/// Returns a range that encompasses both input ranges.
///
/// This is like `extend()`, but tries to negotiate the
/// anchor/head ordering between the two input ranges.
#[must_use]
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,
}
} else {
Range {
anchor: self.from().min(other.from()),
head: self.to().max(other.to()),
horiz: None,
}
}
}
@@ -122,7 +190,130 @@ impl Range {
#[inline]
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 {
// eq
#[inline]
#[must_use]
pub fn primary(&self) -> Range {
self.ranges[self.primary_index]
}
#[inline]
#[must_use]
pub fn cursor(&self) -> usize {
self.primary().head
pub fn primary_mut(&mut self) -> &mut Range {
&mut self.ranges[self.primary_index]
}
/// 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 {
let index = self.ranges.len();
self.ranges.push(range);
Self::normalize(self.ranges, index)
self.set_primary_index(self.ranges().len() - 1);
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
/// applying changes to a document.
@@ -192,6 +393,11 @@ impl Selection {
self.primary_index
}
pub fn set_primary_index(&mut self, idx: usize) {
assert!(idx < self.ranges.len());
self.primary_index = idx;
}
#[must_use]
/// Constructs a selection holding a single range.
pub fn single(anchor: usize, head: usize) -> Self {
@@ -210,77 +416,79 @@ impl Selection {
Self::single(pos, pos)
}
fn normalize(mut ranges: SmallVec<[Range; 1]>, mut primary_index: usize) -> Self {
let primary = ranges[primary_index];
ranges.sort_unstable_by_key(Range::from);
primary_index = ranges.iter().position(|&range| range == primary).unwrap();
/// Normalizes a `Selection`.
fn normalize(mut self) -> Self {
let primary = self.ranges[self.primary_index];
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
// TODO: we could do with one vec by removing elements as we mutate
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 {
prev.anchor = from;
prev.head = to;
}
continue;
}
let mut prev_i = 0;
for i in 1..self.ranges.len() {
if self.ranges[prev_i].overlaps(&self.ranges[i]) {
self.ranges[prev_i] = self.ranges[prev_i].merge(self.ranges[i]);
} else {
prev_i += 1;
self.ranges[prev_i] = self.ranges[i];
}
if i == self.primary_index {
self.primary_index = prev_i;
}
result.push(range)
}
Self {
ranges: result,
primary_index,
}
self.ranges.truncate(prev_i + 1);
self
}
// TODO: consume an iterator or a vec to reduce allocations?
#[must_use]
pub fn new(ranges: SmallVec<[Range; 1]>, primary_index: usize) -> Self {
assert!(!ranges.is_empty());
debug_assert!(primary_index < ranges.len());
// fast path for a single selection (cursor)
if ranges.len() == 1 {
return Self {
ranges,
primary_index: 0,
};
let mut selection = Self {
ranges,
primary_index,
};
if selection.ranges.len() > 1 {
// TODO: only normalize if needed (any ranges out of order)
selection = selection.normalize();
}
// TODO: only normalize if needed (any ranges out of order)
Self::normalize(ranges, primary_index)
selection
}
/// Takes a closure and maps each selection over the closure.
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
F: Fn(Range) -> Range,
{
Self::new(
self.ranges.iter().copied().map(f).collect(),
self.primary_index,
)
for range in self.ranges.iter_mut() {
*range = f(*range)
}
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 {
@@ -338,17 +546,15 @@ pub fn select_on_matches(
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
let fragment = sel.fragment(text);
let mut sel_start = sel.from();
let sel_end = sel.to();
let mut start_byte = text.char_to_byte(sel_start);
let sel_start = sel.from();
let start_byte = text.char_to_byte(sel_start);
for mat in regex.find_iter(&fragment) {
// TODO: retain range direction
let start = text.byte_to_char(start_byte + mat.start());
let end = text.byte_to_char(start_byte + mat.end());
result.push(Range::new(start, end - 1));
result.push(Range::new(start, end));
}
}
@@ -369,25 +575,30 @@ pub fn split_on_matches(
let mut result = SmallVec::with_capacity(selection.len());
for sel in selection {
// Special case: zero-width selection.
if sel.from() == sel.to() {
result.push(*sel);
continue;
}
// TODO: can't avoid occasional allocations since Regex can't operate on chunks yet
let fragment = sel.fragment(text);
let mut sel_start = sel.from();
let sel_start = sel.from();
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;
for mat in regex.find_iter(&fragment) {
// TODO: retain range direction
let end = text.byte_to_char(start_byte + mat.start());
result.push(Range::new(start, end - 1));
result.push(Range::new(start, end));
start = text.byte_to_char(start_byte + mat.end());
}
if start <= sel_end {
if start < sel_end {
result.push(Range::new(start, sel_end));
}
}
@@ -399,11 +610,12 @@ pub fn split_on_matches(
#[cfg(test)]
mod test {
use super::*;
use crate::Rope;
#[test]
#[should_panic]
fn test_new_empty() {
let sel = Selection::new(smallvec![], 0);
let _ = Selection::new(smallvec![], 0);
}
#[test]
@@ -430,6 +642,22 @@ mod test {
.join(",");
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]
@@ -452,7 +680,7 @@ mod test {
.collect::<Vec<String>>()
.join(",");
assert_eq!(res, "8/10,10/12");
assert_eq!(res, "8/10,10/12,12/12");
}
#[test]
@@ -466,35 +694,251 @@ mod test {
assert_eq!(range.contains(13), false);
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(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]
fn test_split_on_matches() {
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());
assert_eq!(
result.ranges(),
&[
Range::new(0, 3),
Range::new(5, 7),
Range::new(10, 11),
Range::new(15, 17),
Range::new(19, 19),
// TODO: rather than this behavior, maybe we want it
// to be based on which side is the anchor?
//
// We get a leading zero-width range when there's
// 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!(
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};
/// A state represents the current editor state of a single buffer.
#[derive(Clone)]
#[derive(Debug, Clone)]
pub struct State {
pub doc: Rope,
pub selection: Selection,
@@ -15,27 +14,4 @@ impl State {
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
);
}
}

File diff suppressed because it is too large Load Diff

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 std::{borrow::Cow, convert::TryFrom};
use crate::{Range, Rope, Selection, Tendril};
use std::borrow::Cow;
/// (from, to, replacement)
pub type Change = (usize, usize, Option<Tendril>);
@@ -15,7 +15,7 @@ pub enum Operation {
Insert(Tendril),
}
#[derive(Copy, Clone, PartialEq, Eq)]
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
pub enum Assoc {
Before,
After,
@@ -90,7 +90,8 @@ impl ChangeSet {
return;
}
self.len_after += fragment.len();
// Avoiding std::str::len() to account for UTF-8 characters.
self.len_after += fragment.chars().count();
let new_last = match self.changes.as_mut_slice() {
[.., Insert(prev)] | [.., Insert(prev), Delete(_)] => {
@@ -124,13 +125,16 @@ impl ChangeSet {
/// In other words, If `this` goes `docA` → `docB` and `other` represents `docB` → `docC`, the
/// returned value will represent the change `docA` → `docC`.
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
// a: [] len: 0 len_after: 1 | b: [Insert(Tendril<UTF8>(inline: "\n")), Retain(1)] len 1
if self.changes.is_empty() {
return other;
}
if other.changes.is_empty() {
return self;
}
let len = self.changes.len();
@@ -162,7 +166,7 @@ impl ChangeSet {
head_a = a;
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) {
Ordering::Less => {
changes.retain(i);
@@ -192,15 +196,16 @@ impl ChangeSet {
head_b = changes_b.next();
}
Ordering::Greater => {
// TODO: cover this with a test
// figure out the byte index of the truncated string end
let (pos, _) = s.char_indices().nth(len - j).unwrap();
s.pop_front(s.len() as u32 - pos as u32);
let (pos, _) = s.char_indices().nth(j).unwrap();
s.pop_front(pos as u32);
head_a = Some(Insert(s));
head_b = changes_b.next();
}
}
}
(Some(Insert(mut s)), Some(Retain(j))) => {
(Some(Insert(s)), Some(Retain(j))) => {
let len = s.chars().count();
match len.cmp(&j) {
Ordering::Less => {
@@ -272,7 +277,6 @@ impl ChangeSet {
let mut changes = Self::with_capacity(self.changes.len());
let mut pos = 0;
let mut len = 0;
for change in &self.changes {
use Operation::*;
@@ -415,7 +419,7 @@ impl ChangeSet {
/// Transaction represents a single undoable unit of changes. Several changes can be grouped into
/// a single transaction.
#[derive(Debug, Clone)]
#[derive(Debug, Default, Clone)]
pub struct Transaction {
changes: ChangeSet,
selection: Option<Selection>,
@@ -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 {
self.selection = Some(selection);
self
@@ -472,11 +483,13 @@ impl Transaction {
/// Generate a transaction from a set of changes.
pub fn change<I>(doc: &Rope, changes: I) -> Self
where
I: IntoIterator<Item = Change> + ExactSizeIterator,
I: IntoIterator<Item = Change> + Iterator,
{
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.
@@ -579,6 +592,7 @@ impl<'a> Iterator for ChangeIterator<'a> {
#[cfg(test)]
mod test {
use super::*;
use crate::State;
#[test]
fn composition() {
@@ -597,7 +611,7 @@ mod test {
};
let b = ChangeSet {
changes: vec![Delete(10), Insert("world".into()), Retain(5)],
changes: vec![Delete(10), Insert("orld".into()), Retain(5)],
len: 15,
len_after: 10,
};
@@ -608,7 +622,7 @@ mod test {
let composed = a.compose(b);
assert_eq!(composed.len, 8);
assert!(composed.apply(&mut text));
assert_eq!(text, "world! abc");
assert_eq!(text, "orld! abc");
}
#[test]
@@ -685,21 +699,21 @@ mod test {
#[test]
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(
&state.doc,
&doc,
// (1, 1, None) is a useless 0-width delete
vec![(1, 1, None), (6, 11, Some("void".into())), (12, 17, None)].into_iter(),
);
transaction.apply(&mut state.doc);
assert_eq!(state.doc, Rope::from_str("hello void! 123"));
transaction.apply(&mut doc);
assert_eq!(doc, Rope::from_str("hello void! 123"));
}
#[test]
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 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);
}
@@ -729,7 +743,7 @@ mod test {
// retain 1, e
// retain 2, l
let mut changes = t1
let changes = t1
.changes
.compose(t2.changes)
.compose(t3.changes)
@@ -744,7 +758,7 @@ mod test {
#[test]
fn combine_with_empty() {
let empty = Rope::from("");
let mut a = ChangeSet::new(&empty);
let a = ChangeSet::new(&empty);
let mut b = ChangeSet::new(&empty);
b.insert("a".into());
@@ -754,4 +768,21 @@ mod test {
use Operation::*;
assert_eq!(changes.changes, &[Insert("a".into())]);
}
#[test]
fn combine_with_utf8() {
const TEST_CASE: &'static str = "Hello, これはヘリックスエディターです!";
let empty = Rope::from("");
let a = ChangeSet::new(&empty);
let mut b = ChangeSet::new(&empty);
b.insert(TEST_CASE.into());
let changes = a.compose(b);
use Operation::*;
assert_eq!(changes.changes, &[Insert(TEST_CASE.into())]);
assert_eq!(changes.len_after, TEST_CASE.chars().count());
}
}

View File

@@ -1,29 +1,27 @@
[package]
name = "helix-lsp"
version = "0.1.0"
version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
edition = "2021"
license = "MPL-2.0"
description = "LSP client implementation for Helix project"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
helix-core = { path = "../helix-core" }
helix-core = { version = "0.5", path = "../helix-core" }
once_cell = "1.4"
lsp-types = { version = "0.89", features = ["proposed"] }
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1.5"
futures-executor = { version = "0.3" }
url = "2"
pathdiff = "0.2"
glob = "0.3"
anyhow = "1"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
# jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc", default-features = false } # don't pull in all of futures
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
jsonrpc-core = { version = "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"
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,
};
use helix_core::{ChangeSet, Rope};
// use std::collections::HashMap;
use std::future::Future;
use std::sync::atomic::{AtomicU64, Ordering};
use helix_core::{find_root, ChangeSet, Rope};
use jsonrpc_core as jsonrpc;
use lsp_types as lsp;
use serde_json::Value;
use std::future::Future;
use std::process::Stdio;
use std::sync::{
atomic::{AtomicU64, Ordering},
Arc,
};
use tokio::{
io::{BufReader, BufWriter},
// prelude::*,
process::{Child, Command},
sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
sync::{
mpsc::{channel, UnboundedReceiver, UnboundedSender},
Notify, OnceCell,
},
};
#[derive(Debug)]
pub struct Client {
id: usize,
_process: Child,
outgoing: UnboundedSender<Payload>,
// pub incoming: Receiver<Call>,
pub request_counter: AtomicU64,
capabilities: Option<lsp::ServerCapabilities>,
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
pub(crate) capabilities: OnceCell<lsp::ServerCapabilities>,
offset_encoding: OffsetEncoding,
config: Option<Value>,
}
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)
.args(args)
.stdin(Stdio::piped())
@@ -43,40 +50,31 @@ impl Client {
.kill_on_drop(true)
.spawn();
// use std::io::ErrorKind;
let mut process = match process {
Ok(process) => process,
Err(err) => match err.kind() {
// ErrorKind::NotFound | ErrorKind::PermissionDenied => {
// return Err(Error::Other(err.into()))
// }
_kind => return Err(Error::Other(err.into())),
},
};
let mut process = process?;
// TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
let (incoming, outgoing) = Transport::start(reader, writer, stderr);
let (server_rx, server_tx, initialize_notify) =
Transport::start(reader, writer, stderr, id);
let client = Self {
id,
_process: process,
outgoing,
// incoming,
server_tx,
request_counter: AtomicU64::new(0),
capabilities: None,
// diagnostics: HashMap::new(),
capabilities: OnceCell::new(),
offset_encoding: OffsetEncoding::Utf8,
config,
};
// TODO: async client.initialize()
// maybe use an arc<atomic> flag
Ok((client, server_rx, initialize_notify))
}
Ok((client, incoming))
pub fn id(&self) -> usize {
self.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 {
self.capabilities
.as_ref()
.get()
.expect("language server not yet initialized!")
}
@@ -106,7 +108,7 @@ impl Client {
}
/// Execute a RPC request on the language server.
pub async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
async fn request<R: lsp::request::Request>(&self, params: R::Params) -> Result<R::Result>
where
R::Params: serde::Serialize,
R::Result: core::fmt::Debug, // TODO: temporary
@@ -118,17 +120,20 @@ impl Client {
}
/// Execute a RPC request on the language server.
pub fn call<R: lsp::request::Request>(
fn call<R: lsp::request::Request>(
&self,
params: R::Params,
) -> impl Future<Output = Result<Value>>
where
R::Params: serde::Serialize,
{
let outgoing = self.outgoing.clone();
let server_tx = self.server_tx.clone();
let id = self.next_request_id();
async move {
use std::time::Duration;
use tokio::time::timeout;
let params = serde_json::to_value(params)?;
let request = jsonrpc::MethodCall {
@@ -140,20 +145,18 @@ impl Client {
let (tx, mut rx) = channel::<Result<Value>>(1);
outgoing
server_tx
.send(Payload::Request {
chan: tx,
value: request,
})
.map_err(|e| Error::Other(e.into()))?;
use std::time::Duration;
use tokio::time::timeout;
timeout(Duration::from_secs(2), rx.recv())
// TODO: specifiable timeout, delay other calls until initialize success
timeout(Duration::from_secs(20), rx.recv())
.await
.map_err(|_| Error::Timeout)? // return Timeout
.unwrap() // TODO: None if channel closed
.ok_or(Error::StreamClosed)?
}
}
@@ -165,7 +168,7 @@ impl Client {
where
R::Params: serde::Serialize,
{
let outgoing = self.outgoing.clone();
let server_tx = self.server_tx.clone();
async move {
let params = serde_json::to_value(params)?;
@@ -176,7 +179,7 @@ impl Client {
params: Self::value_into_params(params),
};
outgoing
server_tx
.send(Payload::Notification(notification))
.map_err(|e| Error::Other(e.into()))?;
@@ -185,47 +188,56 @@ impl Client {
}
/// Reply to a language server RPC call.
pub async fn reply(
pub fn reply(
&self,
id: jsonrpc::Id,
result: core::result::Result<Value, jsonrpc::Error>,
) -> Result<()> {
) -> impl Future<Output = Result<()>> {
use jsonrpc::{Failure, Output, Success, Version};
let output = match result {
Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2),
id,
result,
}),
Err(error) => Output::Failure(Failure {
jsonrpc: Some(Version::V2),
id,
error,
}),
};
let server_tx = self.server_tx.clone();
self.outgoing
.send(Payload::Response(output))
.map_err(|e| Error::Other(e.into()))?;
async move {
let output = match result {
Ok(result) => Output::Success(Success {
jsonrpc: Some(Version::V2),
id,
result,
}),
Err(error) => Output::Failure(Failure {
jsonrpc: Some(Version::V2),
id,
error,
}),
};
Ok(())
server_tx
.send(Payload::Response(output))
.map_err(|e| Error::Other(e.into()))?;
Ok(())
}
}
// -------------------------------------------------------------------------------------------
// General messages
// -------------------------------------------------------------------------------------------
pub async fn initialize(&mut self) -> Result<()> {
pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
// 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)]
let params = lsp::InitializeParams {
process_id: Some(std::process::id()),
// root_path is obsolete, use root_uri
root_path: None,
// root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
root_uri: None, // set to project root in the future
initialization_options: None,
root_uri: root,
initialization_options: self.config.clone(),
capabilities: lsp::ClientCapabilities {
text_document: Some(lsp::TextDocumentClientCapabilities {
completion: Some(lsp::CompletionClientCapabilities {
@@ -245,6 +257,30 @@ impl Client {
content_format: Some(vec![lsp::MarkupKind::Markdown]),
..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()
@@ -255,14 +291,7 @@ impl Client {
locale: None, // TODO
};
let response = 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(())
self.request::<lsp::request::Initialize>(params).await
}
pub async fn shutdown(&self) -> Result<()> {
@@ -273,6 +302,21 @@ impl Client {
self.notify::<lsp::notification::Exit>(())
}
/// Tries to shut down the language server but returns
/// early if server responds with an error.
pub async fn shutdown_and_exit(&self) -> Result<()> {
self.shutdown().await?;
self.exit().await
}
/// Forcefully shuts down the language server ignoring any errors.
pub async fn force_shutdown(&self) -> Result<()> {
if let Err(e) = self.shutdown().await {
log::warn!("language server failed to terminate gracefully - {}", e);
}
self.exit().await
}
// -------------------------------------------------------------------------------------------
// Text document
// -------------------------------------------------------------------------------------------
@@ -315,7 +359,6 @@ impl Client {
//
// Calculation is therefore a bunch trickier.
// TODO: stolen from syntax.rs, share
use helix_core::RopeSlice;
fn traverse(pos: lsp::Position, text: RopeSlice) -> lsp::Position {
let lsp::Position {
@@ -323,8 +366,14 @@ impl Client {
mut character,
} = pos;
for ch in text.chars() {
if ch == '\n' {
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
// 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;
character = 0;
} else {
@@ -399,7 +448,7 @@ impl Client {
) -> Option<impl Future<Output = Result<()>>> {
// 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 {
Some(lsp::TextDocumentSyncCapability::Kind(kind))
@@ -417,7 +466,7 @@ impl Client {
// range = None -> whole document
range: None, //Some(Range)
range_length: None, // u64 apparently deprecated
text: "".to_string(),
text: new_text.to_string(),
}]
}
lsp::TextDocumentSyncKind::Incremental => {
@@ -445,12 +494,12 @@ impl Client {
// will_save / will_save_wait_until
pub async fn text_document_did_save(
pub fn text_document_did_save(
&self,
text_document: lsp::TextDocumentIdentifier,
text: &Rope,
) -> Result<()> {
let capabilities = self.capabilities.as_ref().unwrap();
) -> Option<impl Future<Output = Result<()>>> {
let capabilities = self.capabilities.get().unwrap();
let include_text = match &capabilities.text_document_sync {
Some(lsp::TextDocumentSyncCapability::Options(lsp::TextDocumentSyncOptions {
@@ -462,23 +511,25 @@ impl Client {
include_text,
}) => include_text.unwrap_or(false),
// Supported(false)
_ => return Ok(()),
_ => return None,
},
// unsupported
_ => return Ok(()),
_ => return None,
};
self.notify::<lsp::notification::DidSaveTextDocument>(lsp::DidSaveTextDocumentParams {
text_document,
text: include_text.then(|| text.into()),
})
.await
Some(self.notify::<lsp::notification::DidSaveTextDocument>(
lsp::DidSaveTextDocumentParams {
text_document,
text: include_text.then(|| text.into()),
},
))
}
pub fn completion(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
// ) -> Result<Vec<lsp::CompletionItem>> {
let params = lsp::CompletionParams {
@@ -487,9 +538,7 @@ impl Client {
position,
},
// TODO: support these tokens by async receiving and updating the choice list
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
@@ -504,15 +553,14 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::SignatureHelpParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
position,
},
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
context: None,
// lsp::SignatureHelpContext
};
@@ -524,15 +572,14 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::HoverParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
position,
},
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
// lsp::SignatureHelpContext
};
@@ -541,32 +588,35 @@ impl Client {
// formatting
pub async fn text_document_formatting(
pub fn text_document_formatting(
&self,
text_document: lsp::TextDocumentIdentifier,
options: lsp::FormattingOptions,
) -> anyhow::Result<Vec<lsp::TextEdit>> {
let capabilities = self.capabilities.as_ref().unwrap();
work_done_token: Option<lsp::ProgressToken>,
) -> Option<impl Future<Output = Result<Vec<lsp::TextEdit>>>> {
let capabilities = self.capabilities.get().unwrap();
// check if we're able to format
match capabilities.document_formatting_provider {
Some(lsp::OneOf::Left(true)) | Some(lsp::OneOf::Right(_)) => (),
// None | Some(false)
_ => return Ok(Vec::new()),
_ => return None,
};
// TODO: return err::unavailable so we can fall back to tree sitter formatting
let params = lsp::DocumentFormattingParams {
text_document,
options,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let response = self.request::<lsp::request::Formatting>(params).await?;
let request = self.call::<lsp::request::Formatting>(params);
Ok(response.unwrap_or_default())
Some(async move {
let json = request.await?;
let response: Option<Vec<lsp::TextEdit>> = serde_json::from_value(json)?;
Ok(response.unwrap_or_default())
})
}
pub async fn text_document_range_formatting(
@@ -574,8 +624,9 @@ impl Client {
text_document: lsp::TextDocumentIdentifier,
range: lsp::Range,
options: lsp::FormattingOptions,
work_done_token: Option<lsp::ProgressToken>,
) -> anyhow::Result<Vec<lsp::TextEdit>> {
let capabilities = self.capabilities.as_ref().unwrap();
let capabilities = self.capabilities.get().unwrap();
// check if we're able to format
match capabilities.document_range_formatting_provider {
@@ -589,9 +640,7 @@ impl Client {
text_document,
range,
options,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
};
let response = self
@@ -610,15 +659,14 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::GotoDefinitionParams {
text_document_position_params: lsp::TextDocumentPositionParams {
text_document,
position,
},
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
@@ -631,30 +679,42 @@ impl Client {
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoDefinition>(text_document, position)
self.goto_request::<lsp::request::GotoDefinition>(text_document, position, work_done_token)
}
pub fn goto_type_definition(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoTypeDefinition>(text_document, position)
self.goto_request::<lsp::request::GotoTypeDefinition>(
text_document,
position,
work_done_token,
)
}
pub fn goto_implementation(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
self.goto_request::<lsp::request::GotoImplementation>(text_document, position)
self.goto_request::<lsp::request::GotoImplementation>(
text_document,
position,
work_done_token,
)
}
pub fn goto_reference(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
work_done_token: Option<lsp::ProgressToken>,
) -> impl Future<Output = Result<Value>> {
let params = lsp::ReferenceParams {
text_document_position: lsp::TextDocumentPositionParams {
@@ -664,9 +724,7 @@ impl Client {
context: lsp::ReferenceContext {
include_declaration: true,
},
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
work_done_progress_params: lsp::WorkDoneProgressParams { work_done_token },
partial_result_params: lsp::PartialResultParams {
partial_result_token: None,
},
@@ -674,4 +732,44 @@ impl Client {
self.call::<lsp::request::References>(params)
}
pub fn document_symbols(
&self,
text_document: lsp::TextDocumentIdentifier,
) -> impl Future<Output = Result<Value>> {
let params = lsp::DocumentSymbolParams {
text_document,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};
self.call::<lsp::request::DocumentSymbolRequest>(params)
}
// 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 select_all;
mod transport;
pub use client::Client;
pub use futures_executor::block_on;
pub use jsonrpc::Call;
pub use jsonrpc_core as jsonrpc;
pub use lsp::{Position, Url};
pub use lsp_types as lsp;
pub use client::Client;
pub use lsp::{Position, Url};
pub type Result<T> = core::result::Result<T, Error>;
use futures_util::stream::select_all::SelectAll;
use helix_core::syntax::LanguageConfiguration;
use thiserror::Error;
use std::{collections::HashMap, sync::Arc};
use std::{
collections::{hash_map::Entry, HashMap},
sync::{
atomic::{AtomicUsize, Ordering},
Arc,
},
};
use serde::{Deserialize, Serialize};
use thiserror::Error;
use tokio_stream::wrappers::UnboundedReceiverStream;
pub use futures_executor::block_on;
pub type Result<T> = core::result::Result<T, Error>;
type LanguageId = String;
#[derive(Error, Debug)]
pub enum Error {
@@ -28,8 +32,14 @@ pub enum Error {
Rpc(#[from] jsonrpc::Error),
#[error("failed to parse: {0}")]
Parse(#[from] serde_json::Error),
#[error("IO Error: {0}")]
IO(#[from] std::io::Error),
#[error("request timed out")]
Timeout,
#[error("server closed the stream")]
StreamClosed,
#[error("LSP not defined")]
LspNotDefined,
#[error(transparent)]
Other(#[from] anyhow::Error),
}
@@ -48,23 +58,54 @@ pub mod util {
use super::*;
use helix_core::{Range, Rope, Transaction};
/// Converts [`lsp::Position`] to a position in the document.
///
/// Returns `None` if position exceeds document length or an operation overflows.
pub fn lsp_pos_to_pos(
doc: &Rope,
pos: lsp::Position,
offset_encoding: OffsetEncoding,
) -> usize {
) -> Option<usize> {
let max_line = doc.lines().count().saturating_sub(1);
let pos_line = pos.line as usize;
let pos_line = if pos_line > max_line {
return None;
} else {
pos_line
};
match offset_encoding {
OffsetEncoding::Utf8 => {
let line = doc.line_to_char(pos.line as usize);
line + pos.character as usize
let max_char = doc
.line_to_char(max_line)
.checked_add(doc.line(max_line).len_chars())?;
let line = doc.line_to_char(pos_line);
let pos = line.checked_add(pos.character as usize)?;
if pos <= max_char {
Some(pos)
} else {
None
}
}
OffsetEncoding::Utf16 => {
let line = doc.line_to_char(pos.line as usize);
let max_char = doc
.line_to_char(max_line)
.checked_add(doc.line(max_line).len_chars())?;
let max_cu = doc.char_to_utf16_cu(max_char);
let line = doc.line_to_char(pos_line);
let line_start = doc.char_to_utf16_cu(line);
doc.utf16_cu_to_char(line_start + pos.character as usize)
let pos = line_start.checked_add(pos.character as usize)?;
if pos <= max_cu {
Some(doc.utf16_cu_to_char(pos))
} else {
None
}
}
}
}
/// Converts position in the document to [`lsp::Position`].
///
/// Panics when `pos` is out of `doc` bounds or operation overflows.
pub fn pos_to_lsp_pos(
doc: &Rope,
pos: usize,
@@ -88,6 +129,7 @@ pub mod util {
}
}
/// Converts a range in the document to [`lsp::Range`].
pub fn range_to_lsp_range(
doc: &Rope,
range: Range,
@@ -99,6 +141,17 @@ pub mod util {
lsp::Range::new(start, end)
}
pub fn lsp_range_to_range(
doc: &Rope,
range: lsp::Range,
offset_encoding: OffsetEncoding,
) -> Option<Range> {
let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?;
let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?;
Some(Range::new(start, end))
}
pub fn generate_transaction_from_edits(
doc: &Rope,
edits: Vec<lsp::TextEdit>,
@@ -114,21 +167,71 @@ pub mod util {
None
};
let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding);
let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding);
let start =
if let Some(start) = lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
start
} else {
return (0, 0, None);
};
let end = if let Some(end) = lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
end
} else {
return (0, 0, None);
};
(start, end, replacement)
}),
)
}
// apply_insert_replace_edit
/// 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)]
pub enum Notification {
// we inject this notification to signal the LSP is ready
Initialized,
PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams),
ProgressMessage(lsp::ProgressParams),
}
impl Notification {
@@ -136,6 +239,7 @@ impl Notification {
use lsp::notification::Notification as _;
let notification = match method {
lsp::notification::Initialized::METHOD => Self::Initialized,
lsp::notification::PublishDiagnostics::METHOD => {
let params: lsp::PublishDiagnosticsParams = params
.parse()
@@ -146,17 +250,20 @@ impl Notification {
}
lsp::notification::ShowMessage::METHOD => {
let params: lsp::ShowMessageParams =
params.parse().expect("Failed to parse ShowMessage params");
let params: lsp::ShowMessageParams = params.parse().ok()?;
Self::ShowMessage(params)
}
lsp::notification::LogMessage::METHOD => {
let params: lsp::LogMessageParams =
params.parse().expect("Failed to parse ShowMessage params");
let params: lsp::LogMessageParams = params.parse().ok()?;
Self::LogMessage(params)
}
lsp::notification::Progress::METHOD => {
let params: lsp::ProgressParams = params.parse().ok()?;
Self::ProgressMessage(params)
}
_ => {
log::error!("unhandled LSP notification: {}", method);
return None;
@@ -167,16 +274,12 @@ impl Notification {
}
}
pub use jsonrpc::Call;
type LanguageId = String;
use crate::select_all::SelectAll;
#[derive(Debug)]
pub struct Registry {
inner: HashMap<LanguageId, Option<Arc<Client>>>,
inner: HashMap<LanguageId, (usize, Arc<Client>)>,
pub incoming: SelectAll<UnboundedReceiverStream<Call>>,
counter: AtomicUsize,
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
}
impl Default for Registry {
@@ -189,64 +292,178 @@ impl Registry {
pub fn new() -> Self {
Self {
inner: HashMap::new(),
counter: AtomicUsize::new(0),
incoming: SelectAll::new(),
}
}
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Option<Arc<Client>> {
// TODO: propagate the error
if let Some(config) = &language_config.language_server {
// avoid borrow issues
let inner = &mut self.inner;
let s_incoming = &self.incoming;
pub fn get_by_id(&self, id: usize) -> Option<&Client> {
self.inner
.values()
.find(|(client_id, _)| client_id == &id)
.map(|(_, client)| client.as_ref())
}
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)
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),
};
// initialize a new client
let (mut client, incoming) =
Client::start(&config.command, &config.args).ok()?;
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);
// TODO: run this async without blocking
futures_executor::block_on(client.initialize()).unwrap();
// 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;
s_incoming.push(UnboundedReceiverStream::new(incoming));
value.expect("failed to initialize capabilities");
Some(Arc::new(client))
})
.clone();
// next up, notify<initialized>
_client
.notify::<lsp::notification::Initialized>(lsp::InitializedParams {})
.await
.unwrap();
return language_server;
initialize_notify.notify_one();
});
entry.insert((id, client.clone()));
Ok(client)
}
}
}
None
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
self.inner.values().map(|(_, client)| client)
}
}
// REGISTRY = HashMap<LanguageId, Lazy/OnceCell<Arc<RwLock<Client>>>
// spawn one server per language type, need to spawn one per workspace if server doesn't support
// workspaces
//
// could also be a client per root dir
//
// storing a copy of Option<Arc<RwLock<Client>>> on Document would make the LSP client easily
// accessible during edit/save callbacks
//
// the event loop needs to process all incoming streams, maybe we can just have that be a separate
// task that's continually running and store the state on the client, then use read lock to
// 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
//
//
// v2:
//
// there should be a registry of lsp clients, one per language type (or workspace).
// the clients should lazy init on first access
// the client.initialize() should be called async and we buffer any requests until that completes
// there needs to be a way to process incoming lsp messages from all clients.
// -> notifications need to be dispatched to wherever
// -> requests need to generate a reply and travel back to the same lsp!
#[derive(Debug)]
pub enum ProgressStatus {
Created,
Started(lsp::WorkDoneProgress),
}
impl ProgressStatus {
pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
match &self {
ProgressStatus::Created => None,
ProgressStatus::Started(progress) => Some(progress),
}
}
}
#[derive(Default, Debug)]
/// Acts as a container for progress reported by language servers. Each server
/// has a unique id assigned at creation through [`Registry`]. This id is then used
/// to store the progress in this map.
pub struct LspProgressMap(HashMap<usize, HashMap<lsp::ProgressToken, ProgressStatus>>);
impl LspProgressMap {
pub fn new() -> Self {
Self::default()
}
/// Returns a map of all tokens coresponding to the lanaguage server with `id`.
pub fn progress_map(&self, id: usize) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
self.0.get(&id)
}
pub fn is_progressing(&self, id: usize) -> bool {
self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default()
}
/// Returns last progress status for a given server with `id` and `token`.
pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> {
self.0.get(&id).and_then(|values| values.get(token))
}
/// Checks if progress `token` for server with `id` is created.
pub fn is_created(&mut self, id: usize, token: &lsp::ProgressToken) -> bool {
self.0
.get(&id)
.map(|values| values.get(token).is_some())
.unwrap_or_default()
}
pub fn create(&mut self, id: usize, token: lsp::ProgressToken) {
self.0
.entry(id)
.or_default()
.insert(token, ProgressStatus::Created);
}
/// Ends the progress by removing the `token` from server with `id`, if removed returns the value.
pub fn end_progress(
&mut self,
id: usize,
token: &lsp::ProgressToken,
) -> Option<ProgressStatus> {
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
}
/// Updates the progess of `token` for server with `id` to `status`, returns the value replaced or `None`.
pub fn update(
&mut self,
id: usize,
token: lsp::ProgressToken,
status: lsp::WorkDoneProgress,
) -> Option<ProgressStatus> {
self.0
.entry(id)
.or_default()
.insert(token, ProgressStatus::Started(status))
}
}
#[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 std::io;
use log::{error, info};
use crate::Error;
type Result<T> = core::result::Result<T, Error>;
use crate::{Error, Result};
use anyhow::Context;
use jsonrpc_core as jsonrpc;
use log::{error, info};
use serde::{Deserialize, Serialize};
use serde_json::Value;
use std::collections::HashMap;
use std::sync::Arc;
use tokio::{
io::{AsyncBufRead, AsyncBufReadExt, AsyncReadExt, AsyncWriteExt, BufReader, BufWriter},
process::{ChildStderr, ChildStdin, ChildStdout},
sync::mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender},
sync::{
mpsc::{unbounded_channel, Sender, UnboundedReceiver, UnboundedSender},
Mutex, Notify,
},
};
#[derive(Debug)]
@@ -26,153 +25,185 @@ pub enum Payload {
Response(jsonrpc::Output),
}
use serde::{Deserialize, Serialize};
/// A type representing all possible values sent from the server to the client.
#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)]
#[serde(deny_unknown_fields)]
#[serde(untagged)]
enum Message {
enum ServerMessage {
/// A regular JSON-RPC request output (single response).
Output(jsonrpc::Output),
/// A JSON-RPC request or notification.
Call(jsonrpc::Call),
}
#[derive(Debug)]
pub struct Transport {
incoming: UnboundedSender<jsonrpc::Call>,
outgoing: UnboundedReceiver<Payload>,
pending_requests: HashMap<jsonrpc::Id, Sender<Result<Value>>>,
headers: HashMap<String, String>,
writer: BufWriter<ChildStdin>,
reader: BufReader<ChildStdout>,
stderr: BufReader<ChildStderr>,
id: usize,
pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
}
impl Transport {
pub fn start(
reader: BufReader<ChildStdout>,
writer: BufWriter<ChildStdin>,
stderr: BufReader<ChildStderr>,
) -> (UnboundedReceiver<jsonrpc::Call>, UnboundedSender<Payload>) {
let (incoming, rx) = unbounded_channel();
let (tx, outgoing) = unbounded_channel();
server_stdout: BufReader<ChildStdout>,
server_stdin: BufWriter<ChildStdin>,
server_stderr: BufReader<ChildStderr>,
id: usize,
) -> (
UnboundedReceiver<(usize, jsonrpc::Call)>,
UnboundedSender<Payload>,
Arc<Notify>,
) {
let (client_tx, rx) = unbounded_channel();
let (tx, client_rx) = unbounded_channel();
let notify = Arc::new(Notify::new());
let transport = Self {
reader,
writer,
stderr,
incoming,
outgoing,
pending_requests: HashMap::default(),
headers: HashMap::default(),
id,
pending_requests: Mutex::new(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),
headers: &mut HashMap<String, String>,
) -> core::result::Result<Message, std::io::Error> {
// read headers
buffer: &mut String,
) -> Result<ServerMessage> {
let mut content_length = None;
loop {
let mut header = String::new();
// detect pipe closed if 0
reader.read_line(&mut header).await?;
let header = header.trim();
buffer.truncate(0);
if reader.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed);
};
if header.is_empty() {
// debug!("<- header {:?}", buffer);
if buffer == "\r\n" {
// look for an empty CRLF line
break;
}
let parts: Vec<&str> = header.split(": ").collect();
if parts.len() != 2 {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"Failed to parse header",
));
let header = buffer.trim();
let parts = header.split_once(": ");
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 = headers.get("Content-Length").unwrap().parse().unwrap();
let content_length = content_length.context("missing content length")?;
//TODO: reuse vector
let mut content = vec![0; content_length];
reader.read_exact(&mut content).await?;
let msg = String::from_utf8(content).unwrap();
// read data
let msg = std::str::from_utf8(&content).context("invalid utf8 from server")?;
info!("<- {}", msg);
// try parsing as output (server response) or call (server request)
let output: serde_json::Result<Message> = serde_json::from_str(&msg);
let output: serde_json::Result<ServerMessage> = serde_json::from_str(msg);
Ok(output?)
}
async fn err(
async fn recv_server_error(
err: &mut (impl AsyncBufRead + Unpin + Send),
) -> core::result::Result<(), std::io::Error> {
let mut line = String::new();
err.read_line(&mut line).await?;
error!("err <- {}", line);
buffer: &mut String,
) -> Result<()> {
buffer.truncate(0);
if err.read_line(buffer).await? == 0 {
return Err(Error::StreamClosed);
};
error!("err <- {:?}", buffer);
Ok(())
}
pub async fn send_payload(&mut self, payload: Payload) -> io::Result<()> {
match payload {
async fn send_payload_to_server(
&self,
server_stdin: &mut BufWriter<ChildStdin>,
payload: Payload,
) -> Result<()> {
//TODO: reuse string
let json = match payload {
Payload::Request { chan, value } => {
self.pending_requests.insert(value.id.clone(), chan);
let json = serde_json::to_string(&value)?;
self.send(json).await
self.pending_requests
.lock()
.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);
// send the headers
self.writer
server_stdin
.write_all(format!("Content-Length: {}\r\n\r\n", request.len()).as_bytes())
.await?;
// send the body
self.writer.write_all(request.as_bytes()).await?;
server_stdin.write_all(request.as_bytes()).await?;
self.writer.flush().await?;
server_stdin.flush().await?;
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 {
Message::Output(output) => self.recv_response(output).await?,
Message::Call(call) => {
self.incoming.send(call).unwrap();
ServerMessage::Output(output) => self.process_request_response(output).await?,
ServerMessage::Call(call) => {
client_tx
.send((self.id, call))
.context("failed to send a message to server")?;
// let notification = Notification::parse(&method, params);
}
};
Ok(())
}
async fn recv_response(&mut self, output: jsonrpc::Output) -> io::Result<()> {
async fn process_request_response(&self, output: jsonrpc::Output) -> Result<()> {
let (id, result) = match output {
jsonrpc::Output::Success(jsonrpc::Success { id, result, .. }) => {
info!("<- {}", result);
@@ -186,6 +217,8 @@ impl Transport {
let tx = self
.pending_requests
.lock()
.await
.remove(&id)
.expect("pending_request with id not found!");
@@ -200,29 +233,132 @@ impl Transport {
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 {
tokio::select! {
// client -> server
msg = self.outgoing.recv() => {
if msg.is_none() {
biased;
_ = initialize_notify.notified() => { // TODO: notified is technically not cancellation safe
// 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;
}
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]
name = "helix-syntax"
version = "0.1.0"
version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
edition = "2021"
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
description = "Tree-sitter grammars support"
categories = ["editor"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "languages/**/*", "build.rs", "!**/docs/**/*", "!**/test/**/*", "!**/examples/**/*", "!**/build/**/*"]
[dependencies]
tree-sitter = "0.19"
serde = { version = "1.0", features = ["derive"] }
tree-sitter = "0.20"
libloading = "0.7"
anyhow = "1"
[build-dependencies]
cc = { version = "1", features = ["parallel"] }
cc = { version = "1" }
threadpool = { version = "1.0" }
anyhow = "1"

View File

@@ -1,84 +1,151 @@
use std::path::PathBuf;
use std::{env, fs};
use anyhow::{anyhow, Context, Result};
use std::fs;
use std::time::SystemTime;
use std::{
path::{Path, PathBuf},
process::Command,
};
use std::sync::mpsc::channel;
fn get_opt_level() -> u32 {
env::var("OPT_LEVEL").unwrap().parse::<u32>().unwrap()
}
fn get_debug() -> bool {
env::var("DEBUG").unwrap() == "true"
}
fn collect_tree_sitter_dirs(ignore: &[String]) -> Vec<String> {
fn collect_tree_sitter_dirs(ignore: &[String]) -> Result<Vec<String>> {
let mut dirs = Vec::new();
for entry in fs::read_dir("languages").unwrap().flatten() {
let path = entry.path();
let dir = path.file_name().unwrap().to_str().unwrap().to_string();
if !ignore.contains(&dir) {
dirs.push(dir);
}
}
dirs
}
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("languages");
fn collect_src_files(dir: &str) -> (Vec<String>, Vec<String>) {
eprintln!("Collect files for {}", dir);
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() {
for entry in fs::read_dir(path)? {
let entry = entry?;
let path = entry.path();
if path
.file_stem()
.unwrap()
.to_str()
.unwrap()
.starts_with("binding")
{
if !entry.file_type()?.is_dir() {
continue;
}
if let Some(ext) = path.extension() {
if ext == "c" {
c_files.push(path.to_str().unwrap().to_string());
} else if ext == "cc" || ext == "cpp" || ext == "cxx" {
cpp_files.push(path.to_str().unwrap().to_string());
let dir = path.file_name().unwrap().to_str().unwrap().to_string();
// filter ignores
if ignore.contains(&dir) {
continue;
}
dirs.push(dir)
}
Ok(dirs)
}
#[cfg(unix)]
const DYLIB_EXTENSION: &str = "so";
#[cfg(windows)]
const DYLIB_EXTENSION: &str = "dll";
fn build_library(src_path: &Path, language: &str) -> Result<()> {
let header_path = src_path;
// let grammar_path = src_path.join("grammar.json");
let parser_path = src_path.join("parser.c");
let mut scanner_path = src_path.join("scanner.c");
let scanner_path = if scanner_path.exists() {
Some(scanner_path)
} else {
scanner_path.set_extension("cc");
if scanner_path.exists() {
Some(scanner_path)
} else {
None
}
};
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");
}
}
(c_files, cpp_files)
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_c(files: Vec<String>, language: &str) {
let mut build = cc::Build::new();
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)
.flag_if_supported("-std=c99");
}
build.compile(&format!("tree-sitter-{}-c", language));
}
fn build_cpp(files: Vec<String>, language: &str) {
let mut build = cc::Build::new();
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 mtime(path: &Path) -> Result<SystemTime> {
Ok(fs::metadata(path)?.modified()?)
}
fn build_dir(dir: &str, language: &str) {
@@ -91,24 +158,27 @@ fn build_dir(dir: &str, language: &str) {
.is_none()
{
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
);
eprintln!("You can fix in using 'git submodule init && git submodule update --recursive'.");
std::process::exit(1);
}
let (c, cpp) = collect_src_files(dir);
if !c.is_empty() {
build_c(c, language);
}
if !cpp.is_empty() {
build_cpp(cpp, language);
}
let path = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("languages")
.join(dir)
.join("src");
build_library(&path, language).unwrap();
}
fn main() {
let ignore = vec!["tree-sitter-typescript".to_string()];
let dirs = collect_tree_sitter_dirs(&ignore);
let ignore = vec![
"tree-sitter-typescript".to_string(),
"tree-sitter-haskell".to_string(), // aarch64 failures: https://github.com/tree-sitter/tree-sitter-haskell/issues/34
"tree-sitter-ocaml".to_string(),
];
let dirs = collect_tree_sitter_dirs(&ignore).unwrap();
let mut n_jobs = 0;
let pool = threadpool::Builder::new().build(); // by going through the builder, it'll use num_cpus
@@ -119,7 +189,7 @@ fn main() {
n_jobs += 1;
pool.execute(move || {
let language = &dir[12..]; // skip tree-sitter- prefix
let language = &dir.strip_prefix("tree-sitter-").unwrap();
build_dir(&dir, language);
// report progress
@@ -132,4 +202,6 @@ fn main() {
build_dir("tree-sitter-typescript/tsx", "tsx");
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;
#[macro_export]
macro_rules! mk_extern {
( $( $name:ident ),* ) => {
$(
extern "C" { pub fn $name() -> Language; }
)*
};
fn replace_dashes_with_underscores(name: &str) -> String {
name.replace('-', "_")
}
#[cfg(unix)]
const DYLIB_EXTENSION: &str = "so";
#[macro_export]
macro_rules! mk_enum {
( $( $camel:ident ),* ) => {
#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum Lang {
$(
$camel,
)*
}
#[cfg(windows)]
const DYLIB_EXTENSION: &str = "dll";
pub fn get_language(runtime_path: &std::path::Path, name: &str) -> Result<Language> {
let name = name.to_ascii_lowercase();
let mut library_path = runtime_path.join("grammars").join(&name);
// TODO: duplicated under build
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]
name = "helix-term"
version = "0.1.0"
version = "0.5.0"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
edition = "2021"
license = "MPL-2.0"
categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix"
homepage = "https://helix-editor.com"
include = ["src/**/*", "README.md"]
# 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]]
name = "hx"
path = "src/main.rs"
[dependencies]
helix-core = { path = "../helix-core" }
helix-view = { path = "../helix-view", features = ["term"]}
helix-lsp = { path = "../helix-lsp"}
helix-core = { version = "0.5", path = "../helix-core" }
helix-view = { version = "0.5", path = "../helix-view" }
helix-lsp = { version = "0.5", path = "../helix-lsp" }
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"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.19", features = ["event-stream"] }
clap = { version = "3.0.0-beta.2 ", default-features = false, features = ["std", "cargo"] }
crossterm = { version = "0.22", features = ["event-stream"] }
signal-hook = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@@ -36,8 +44,6 @@ log = "0.4"
# File picker
fuzzy-matcher = "0.3"
ignore = "0.4"
# shellexpand = "2.1"
dirs-next = "2.0"
# markdown doc rendering
pulldown-cmark = { version = "0.8", default-features = false }
@@ -46,3 +52,11 @@ toml = "0.5"
serde_json = "1.0"
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,67 +1,150 @@
use clap::ArgMatches as Args;
use helix_core::{merge_toml_values, syntax};
use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap};
use helix_view::{theme, Editor};
use helix_view::{document::Mode, Document, Editor, Theme, View};
use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui};
use crate::{compositor::Compositor, ui};
use log::{error, info};
use log::{error, warn};
use std::{
future::Future,
io::{self, stdout, Stdout, Write},
path::PathBuf,
io::{stdout, Write},
sync::Arc,
time::Duration,
time::{Duration, Instant},
};
use anyhow::Error;
use crossterm::{
event::{Event, EventStream},
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
execute, terminal,
};
use tui::layout::Rect;
use futures_util::stream::FuturesUnordered;
use std::pin::Pin;
type BoxFuture<T> = Pin<Box<dyn Future<Output = T> + Send>>;
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>;
#[cfg(not(windows))]
use {
signal_hook::{consts::signal, low_level},
signal_hook_tokio::Signals,
};
#[cfg(windows)]
type Signals = futures_util::stream::Empty<()>;
pub struct Application {
compositor: Compositor,
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 {
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;
let mut compositor = Compositor::new()?;
let size = compositor.size();
let mut editor = Editor::new(size);
if let Ok(files) = args.values_of_t::<PathBuf>("files") {
for file in files {
editor.open(file, Action::VerticalSplit)?;
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 {
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 {
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,
editor,
callbacks: FuturesUnordered::new(),
config,
theme_loader,
syn_loader,
signals,
jobs: Jobs::new(),
lsp_progress: LspProgressMap::new(),
};
Ok(app)
@@ -70,11 +153,11 @@ impl Application {
fn render(&mut self) {
let editor = &mut self.editor;
let compositor = &mut self.compositor;
let callbacks = &mut self.callbacks;
let jobs = &mut self.jobs;
let mut cx = crate::compositor::Context {
editor,
callbacks,
jobs,
scroll: None,
};
@@ -83,44 +166,115 @@ impl Application {
pub async fn event_loop(&mut self) {
let mut reader = EventStream::new();
let mut last_render = Instant::now();
let deadline = Duration::from_secs(1) / 60;
self.render();
loop {
if self.editor.should_close() {
self.jobs.finish();
break;
}
use futures_util::StreamExt;
tokio::select! {
biased;
event = reader.next() => {
self.handle_terminal_events(event)
}
Some(call) = self.editor.language_servers.incoming.next() => {
self.handle_language_server_message(call).await
Some(signal) = self.signals.next() => {
self.handle_signals(signal).await;
}
Some(callback) = &mut self.callbacks.next() => {
self.handle_language_server_callback(callback)
Some((id, call)) = self.editor.language_servers.incoming.next() => {
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);
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();
}
}
}
}
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();
#[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>>) {
let mut cx = crate::compositor::Context {
editor: &mut self.editor,
callbacks: &mut self.callbacks,
jobs: &mut self.jobs,
scroll: None,
};
// Handle key events
@@ -141,8 +295,13 @@ impl Application {
}
}
pub async fn handle_language_server_message(&mut self, call: helix_lsp::Call) {
use helix_lsp::{Call, Notification};
pub async fn handle_language_server_message(
&mut self,
call: helix_lsp::Call,
server_id: usize,
) {
use helix_lsp::{Call, MethodCall, Notification};
match call {
Call::Notification(helix_lsp::jsonrpc::Notification { method, params, .. }) => {
let notification = match Notification::parse(&method, params) {
@@ -150,46 +309,81 @@ impl Application {
None => return,
};
// TODO: parse should return Result/Option
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) => {
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
.editor
.documents
.iter_mut()
.find(|(_, doc)| doc.path() == path.as_ref());
if let Some((_, doc)) = doc {
if let Some(doc) = doc {
let text = doc.text();
let diagnostics = params
.diagnostics
.into_iter()
.map(|diagnostic| {
.filter_map(|diagnostic| {
use helix_core::{
diagnostic::{Range, Severity, Severity::*},
diagnostic::{Range, Severity::*},
Diagnostic,
};
use helix_lsp::{lsp, util::lsp_pos_to_pos};
use lsp::DiagnosticSeverity;
let language_server = doc.language_server().unwrap();
// TODO: convert inside server
let start = lsp_pos_to_pos(
let start = if let Some(start) = lsp_pos_to_pos(
text,
diagnostic.range.start,
language_server.offset_encoding(),
);
let end = lsp_pos_to_pos(
) {
start
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
let end = if let Some(end) = lsp_pos_to_pos(
text,
diagnostic.range.end,
language_server.offset_encoding(),
);
) {
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
Diagnostic {
Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
message: diagnostic.message,
@@ -203,64 +397,207 @@ impl Application {
),
// code
// source
}
})
})
.collect();
doc.diagnostics = diagnostics;
// TODO: we want to process all the events in queue, then render. publishDiagnostic tends to send a whole bunch of events
self.render();
doc.set_diagnostics(diagnostics);
}
}
Notification::ShowMessage(params) => {
log::warn!("unhandled window/showMessage: {:?}", params);
}
Notification::LogMessage(params) => {
log::warn!("unhandled window/logMessage: {:?}", params);
log::info!("window/logMessage: {:?}", params);
}
Notification::ProgressMessage(params)
if !self
.compositor
.has_component(std::any::type_name::<ui::Prompt>()) =>
{
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;
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
}
_ => unreachable!(),
}
}
Call::MethodCall(call) => {
error!("Method not found {}", call.method);
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;
}
};
// self.language_server.reply(
// call.id,
// // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
// Err(helix_lsp::jsonrpc::Error {
// code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
// message: "Method not found".to_string(),
// data: None,
// }),
// );
let call = match MethodCall::parse(&method, params) {
Some(call) => call,
None => {
error!("Method not found {}", method);
// language_server.reply(
// call.id,
// // TODO: make a Into trait that can cast to Err(jsonrpc::Error)
// Err(helix_lsp::jsonrpc::Error {
// code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound,
// message: "Method not found".to_string(),
// 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),
}
}
pub async fn run(&mut self) -> Result<(), Error> {
async fn claim_term(&mut self) -> Result<(), Error> {
terminal::enable_raw_mode()?;
let mut stdout = stdout();
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
let hook = std::panic::take_hook();
std::panic::set_hook(Box::new(move |info| {
execute!(std::io::stdout(), terminal::LeaveAlternateScreen);
terminal::disable_raw_mode();
// We can't handle errors properly inside this closure. And it's
// 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);
}));
self.event_loop().await;
// reset cursor shape
write!(stdout, "\x1B[2 q");
if self.editor.close_language_servers(None).await.is_err() {
log::error!("Timed out waiting for language servers to shutdown");
};
execute!(stdout, terminal::LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
self.restore_term()?;
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.
// Q: how does this work with popups?
// cursive does compositor.screen_mut().add_layer_at(pos::absolute(x, y), <component>)
use helix_core::Position;
use helix_view::graphics::{CursorKind, Rect};
use crossterm::event::Event;
use helix_core::Position;
use tui::{buffer::Buffer as Surface, layout::Rect};
use tui::buffer::Buffer as Surface;
pub type Callback = Box<dyn FnOnce(&mut Compositor)>;
@@ -24,17 +25,17 @@ pub enum EventResult {
use helix_view::Editor;
use crate::application::LspCallbacks;
use crate::job::Jobs;
pub struct Context<'a> {
pub editor: &'a mut Editor,
pub scroll: Option<usize>,
pub callbacks: &'a mut LspCallbacks,
pub jobs: &'a mut Jobs,
}
pub trait Component: Any + AnyComponent {
/// 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
}
// , args: ()
@@ -45,15 +46,16 @@ pub trait Component: Any + AnyComponent {
}
/// 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> {
None
/// Get cursor position and cursor kind.
fn cursor(&self, _area: Rect, _ctx: &Editor) -> (Option<Position>, CursorKind) {
(None, CursorKind::Hidden)
}
/// May be used by the parent component to compute the child area.
/// 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
// that way render can use it
None
@@ -66,21 +68,24 @@ pub trait Component: Any + AnyComponent {
use anyhow::Error;
use std::io::stdout;
use tui::backend::CrosstermBackend;
use tui::backend::{Backend, CrosstermBackend};
type Terminal = tui::terminal::Terminal<CrosstermBackend<std::io::Stdout>>;
pub struct Compositor {
layers: Vec<Box<dyn Component>>,
terminal: Terminal,
pub(crate) last_picker: Option<Box<dyn Component>>,
}
impl Compositor {
pub fn new() -> Result<Self, Error> {
let backend = CrosstermBackend::new(stdout());
let mut terminal = Terminal::new(backend)?;
let terminal = Terminal::new(backend)?;
Ok(Self {
layers: Vec::new(),
terminal,
last_picker: None,
})
}
@@ -94,6 +99,21 @@ impl Compositor {
.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>) {
let size = self.size();
// trigger required_size on init
@@ -101,8 +121,8 @@ impl Compositor {
self.layers.push(layer);
}
pub fn pop(&mut self) {
self.layers.pop();
pub fn pop(&mut self) -> Option<Box<dyn Component>> {
self.layers.pop()
}
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) {
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();
for layer in &self.layers {
layer.render(area, surface, cx)
let area = *surface.area();
for layer in &mut self.layers {
layer.render(area, surface, cx);
}
let pos = self
.cursor_position(area, cx.editor)
.map(|pos| (pos.col as u16, pos.row as u16));
let (pos, kind) = self.cursor(area, cx.editor);
let pos = pos.map(|pos| (pos.col as u16, pos.row as u16));
self.terminal.draw(pos);
self.terminal.draw(pos, kind).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() {
if let Some(pos) = layer.cursor_position(area, editor) {
return Some(pos);
if let (Some(pos), kind) = layer.cursor(area, editor) {
return (Some(pos), kind);
}
}
None
(None, CursorKind::Hidden)
}
pub fn 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> {
@@ -174,10 +206,9 @@ pub trait AnyComponent {
/// # Examples
///
/// ```rust
/// # use cursive_core::views::TextComponent;
/// # use cursive_core::view::Component;
/// let boxed: Box<Component> = Box::new(TextComponent::new("text"));
/// let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
/// use helix_term::{ui::Text, compositor::Component};
/// let boxed: Box<dyn Component> = Box::new(Text::new("text".to_string()));
/// let text: Box<Text> = boxed.as_boxed_any().downcast().unwrap();
/// ```
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,26 +1,13 @@
#![allow(unused)]
mod application;
mod commands;
mod compositor;
mod keymap;
mod ui;
use application::Application;
use clap::{App, Arg};
use anyhow::{Context, Error, Result};
use helix_term::application::Application;
use helix_term::args::Args;
use helix_term::config::Config;
use helix_term::keymap::merge_keys;
use std::path::PathBuf;
use anyhow::Error;
fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
let mut base_config = fern::Dispatch::new();
// Let's say we depend on something which whose "info" level messages are too
// verbose to include in end-user output. If we don't need them,
// let's not include them.
// .level_for("overly-verbose-target", log::LevelFilter::Warn)
base_config = match verbosity {
0 => base_config.level(log::LevelFilter::Warn),
1 => base_config.level(log::LevelFilter::Info),
@@ -28,9 +15,12 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
_3_or_more => base_config.level(log::LevelFilter::Trace),
};
let home = dirs_next::home_dir().expect("can't find the home directory");
// Separate file config so we can include year, month and day in file logs
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(logpath)?;
let file_config = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
@@ -41,56 +31,76 @@ fn setup_logging(verbosity: u64) -> Result<(), fern::InitError> {
message
))
})
.chain(fern::log_file(home.join("helix.log"))?);
.chain(file);
base_config.chain(file_config).apply()?;
Ok(())
}
fn main() {
let args = clap::app_from_crate!()
.arg(
Arg::new("files")
.about("Sets the input file to use")
.required(false)
.multiple(true)
.index(1),
)
.arg(
Arg::new("verbose")
.about("Increases logging verbosity each use for up to 3 times")
.short('v')
.takes_value(false)
.multiple_occurrences(true),
)
.get_matches();
#[tokio::main]
async fn main() -> Result<()> {
let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() {
std::fs::create_dir_all(&cache_dir).ok();
}
let verbosity: u64 = args.occurrences_of("verbose");
let logpath = cache_dir.join("helix.log");
let help = format!(
"\
{} {}
{}
{}
setup_logging(verbosity).expect("failed to initialize logging.");
USAGE:
hx [FLAGS] [files]...
// initialize language registry
use helix_core::config_dir;
use helix_core::syntax::{Loader, LOADER};
ARGS:
<files>... Sets the input file to use
// 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"));
FLAGS:
-h, --help Prints help information
--tutor Loads the tutorial
-v Increases logging verbosity each use for up to 3 times
(default file: {})
-V, --version Prints version information
",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
logpath.display(),
);
LOADER.get_or_init(|| {
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
Loader::new(config)
});
let args = Args::parse_args().context("could not parse arguments")?;
let runtime = tokio::runtime::Runtime::new().unwrap();
// Help has a higher priority and should be handled separately.
if args.display_help {
print!("{}", help);
std::process::exit(0);
}
if args.display_version {
println!("helix {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
let conf_dir = helix_core::config_dir();
if !conf_dir.exists() {
std::fs::create_dir_all(&conf_dir).ok();
}
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
Ok(config) => merge_keys(toml::from_str(&config)?),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)),
};
setup_logging(logpath, args.verbosity).context("failed to initialize logging")?;
// TODO: use the thread local executor to spawn the application task separately from the work pool
runtime.block_on(async move {
let mut app = Application::new(args).unwrap();
let mut app = Application::new(args, config).context("unable to create new application")?;
app.run().await.unwrap();
app.run().await;
});
Ok(())
}

View File

@@ -1,130 +1,189 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use crate::compositor::{Component, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent};
use tui::buffer::Buffer as Surface;
use std::borrow::Cow;
use helix_core::{Position, Transaction};
use helix_view::Editor;
use helix_core::Transaction;
use helix_view::{graphics::Rect, Document, Editor};
use crate::commands;
use crate::ui::{Markdown, Menu, Popup, PromptEvent};
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::lsp;
use helix_lsp::{lsp, util};
use lsp::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 {
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
}
fn label(&self) -> &str {
self.label.as_str()
}
fn row(&self) -> menu::Row {
menu::Row::new(vec![
menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind {
Some(lsp::CompletionItemKind::Text) => "text",
Some(lsp::CompletionItemKind::Method) => "method",
Some(lsp::CompletionItemKind::Function) => "function",
Some(lsp::CompletionItemKind::Constructor) => "constructor",
Some(lsp::CompletionItemKind::Field) => "field",
Some(lsp::CompletionItemKind::Variable) => "variable",
Some(lsp::CompletionItemKind::Class) => "class",
Some(lsp::CompletionItemKind::Interface) => "interface",
Some(lsp::CompletionItemKind::Module) => "module",
Some(lsp::CompletionItemKind::Property) => "property",
Some(lsp::CompletionItemKind::Unit) => "unit",
Some(lsp::CompletionItemKind::Value) => "value",
Some(lsp::CompletionItemKind::Enum) => "enum",
Some(lsp::CompletionItemKind::Keyword) => "keyword",
Some(lsp::CompletionItemKind::Snippet) => "snippet",
Some(lsp::CompletionItemKind::Color) => "color",
Some(lsp::CompletionItemKind::File) => "file",
Some(lsp::CompletionItemKind::Reference) => "reference",
Some(lsp::CompletionItemKind::Folder) => "folder",
Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
Some(lsp::CompletionItemKind::Constant) => "constant",
Some(lsp::CompletionItemKind::Struct) => "struct",
Some(lsp::CompletionItemKind::Event) => "event",
Some(lsp::CompletionItemKind::Operator) => "operator",
Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
None => "",
}),
// self.detail.as_deref().unwrap_or("")
// self.label_details
// .as_ref()
// .or(self.detail())
// .as_str(),
])
}
}
/// Wraps a Menu.
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,
// TODO: maintain a completioncontext with trigger kind & trigger char
}
impl Completion {
pub fn new(
editor: &Editor,
items: Vec<CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize,
) -> Self {
// let items: Vec<CompletionItem> = Vec::new();
let mut menu = Menu::new(
items,
|item| {
// format_fn
item.label.as_str().into()
// TODO: use item.filter_text for filtering
},
move |editor: &mut Editor, item, event| {
match event {
PromptEvent::Abort => {
// revert state
// let id = editor.view().doc;
// let doc = &mut editor.documents[id];
// doc.state = snapshot.clone();
}
PromptEvent::Validate => {
let (view, doc) = editor.current();
// revert state to what it was before the last update
// doc.state = snapshot.clone();
// extract as fn(doc, item):
// TODO: need to apply without composing state...
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
// the final state?
// -> on update simply update the snapshot, then on accept redo the call,
// finally updating doc.changes + notifying lsp.
//
// or we could simply use doc.undo + apply when changing between options
// always present here
let item = item.unwrap();
use helix_lsp::{lsp, util};
// determine what to insert: text_edit | insert_text | label
let edit = if let Some(edit) = &item.text_edit {
match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
}
} else {
item.insert_text.as_ref().unwrap_or(&item.label);
unimplemented!();
// lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
// and we insert at position.
};
// TODO: merge edit with additional_text_edits
if let Some(additional_edits) = &item.additional_text_edits {
if !additional_edits.is_empty() {
unimplemented!(
"completion: additional_text_edits: {:?}",
additional_edits
);
}
let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
fn item_to_transaction(
doc: &Document,
item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize,
) -> Transaction {
if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
};
util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
)
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
// 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(),
)
}
}
// if more text was entered, remove it
let cursor = doc.selection(view.id).cursor();
if trigger_offset < cursor {
let remove = Transaction::change(
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);
if let Some(additional_edits) = &item.additional_text_edits {
// gopls uses this to add extra imports
if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits(
doc.text(),
vec![(trigger_offset, cursor, None)].into_iter(),
additional_edits.clone(),
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&remove, view.id);
doc.apply(&transaction, view.id);
}
use helix_lsp::OffsetEncoding;
let transaction = util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
}
_ => (),
};
},
);
}
};
});
let popup = Popup::new(menu);
Self {
let mut completion = Self {
popup,
start_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
let menu = self.popup.contents_mut();
let (view, doc) = cx.editor.current();
let (view, doc) = current_ref!(editor);
// cx.hooks()
// cx.add_hook(enum type, ||)
@@ -136,15 +195,26 @@ impl Completion {
// TODO: hooks should get processed immediately so maybe do it after select!(), before
// looping?
let cursor = doc.selection(view.id).cursor();
let cursor = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
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);
// TODO: logic is same as ui/picker
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 {
self.popup.contents().is_empty()
}
@@ -164,6 +234,13 @@ impl Completion {
impl Component for Completion {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
// let the Editor handle Esc instead
if let Event::Key(KeyEvent {
code: KeyCode::Esc, ..
}) = event
{
return EventResult::Ignored;
}
self.popup.handle_event(event, cx)
}
@@ -171,41 +248,111 @@ impl Component for Completion {
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);
// TODO: if we have a selection, render a markdown popup on top/below with info
// if we have a selection, render a markdown popup on top/below with info
if let Some(option) = self.popup.contents().selection() {
// need to render:
// option.detail
// ---
// option.documentation
match &option.documentation {
Some(lsp::Documentation::String(s))
let (view, doc) = current!(cx.editor);
let language = doc
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
let cursor_pos = doc
.selection(view.id)
.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::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: s,
value: contents,
})) => {
// TODO: convert to wrapped text
let doc = s;
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
}
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: contents,
})) => {
let doc = Markdown::new(contents.clone());
let half = area.height / 2;
let height = 15.min(half);
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
let area = Rect::new(0, area.height - height - 2, area.width, height);
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
doc.render(area, surface, cx);
// TODO: set language based on doc scope
Markdown::new(
format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
),
cx.editor.syn_loader.clone(),
)
}
None => (),
}
None if option.detail.is_some() => {
// TODO: copied from above
// TODO: set language based on doc scope
Markdown::new(
format!(
"```{}\n{}\n```",
language,
option.detail.as_deref().unwrap_or_default(),
),
cx.editor.syn_loader.clone(),
)
}
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 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.
area.height.saturating_sub(height).saturating_sub(2)
};
Rect::new(0, y, area.width, height)
};
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
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 crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use crate::compositor::{Component, Context};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
text::Text,
text::{Span, Spans, Text},
};
use std::borrow::Cow;
use std::sync::Arc;
use helix_core::Position;
use helix_view::{Editor, Theme};
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use helix_core::{
syntax::{self, HighlightEvent, Syntax},
Rope,
};
use helix_view::{
graphics::{Color, Margin, Rect, Style},
Theme,
};
pub struct Markdown {
contents: String,
config_loader: Arc<syntax::Loader>,
}
// TODO: pre-render and self reference via Pin
// better yet, just use Tendril + subtendril for references
impl Markdown {
pub fn new(contents: String) -> Self {
Self { contents }
pub fn new(contents: String, config_loader: Arc<syntax::Loader>) -> Self {
Self {
contents,
config_loader,
}
}
}
fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
use pulldown_cmark::{CodeBlockKind, CowStr, Event, Options, Parser, Tag};
use tui::text::{Span, Spans, Text};
// 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```";
fn parse<'a>(
contents: &'a str,
theme: Option<&Theme>,
loader: &syntax::Loader,
) -> tui::text::Text<'a> {
// // 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();
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
if let Some(Tag::CodeBlock(CodeBlockKind::Fenced(language))) = tags.last() {
if let Some(theme) = theme {
use helix_core::syntax::{self, HighlightEvent, Syntax};
use helix_core::Rope;
let rope = Rope::from(text.as_ref());
let syntax = syntax::LOADER
.get()
.unwrap()
.language_config_for_scope(&format!("source.{}", language))
let syntax = loader
.language_configuration_for_injection_string(language)
.and_then(|config| config.highlight_config(theme.scopes()))
.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
let mut highlights = Vec::new();
@@ -101,17 +107,20 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
}
HighlightEvent::Source { start, end } => {
let style = match highlights.first() {
Some(span) => {
theme.get(theme.scopes()[span.0].as_str())
}
Some(span) => theme.get(&theme.scopes()[span.0]),
None => text_style,
};
// TODO: replace tabs with indentation
let mut slice = &text[start..end];
// TODO: do we need to handle all unicode line endings
// here, or is just '\n' okay?
while let Some(end) = slice.find('\n') {
// emit span up to newline
let text = &slice[..end];
let span = Span::styled(text.to_owned(), style);
let text = text.replace('\t', " "); // replace tabs
let span = Span::styled(text, style);
spans.push(span);
// truncate slice to after newline
@@ -124,7 +133,8 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
// if there's anything left, emit it too
if !slice.is_empty() {
let span = Span::styled(slice.to_owned(), style);
let span =
Span::styled(slice.replace('\t', " "), style);
spans.push(span);
}
}
@@ -132,13 +142,13 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
}
} else {
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));
}
}
} else {
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));
}
}
@@ -167,7 +177,9 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
lines.push(Spans::default());
}
// TaskListMarker(bool) true if checked
_ => (),
_ => {
log::warn!("unhandled markdown event {:?}", event);
}
}
// build up a vec of Paragraph tui widgets
}
@@ -186,24 +198,47 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
Text::from(lines)
}
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};
let text = parse(&self.contents, Some(&cx.editor.theme));
let text = parse(&self.contents, Some(&cx.editor.theme), &self.config_loader);
let par = Paragraph::new(text)
.wrap(Wrap { trim: false })
.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);
par.render(area, surface);
let margin = Margin {
vertical: 1,
horizontal: 1,
};
par.render(area.inner(&margin), surface);
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let contents = parse(&self.contents, None);
let padding = 2;
let width = std::cmp::min(contents.width() as u16 + padding, viewport.0);
let height = std::cmp::min(contents.height() as u16 + padding, viewport.1);
Some((width, height))
if padding >= viewport.1 || padding >= viewport.0 {
return None;
}
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,20 +1,24 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use tui::{buffer::Buffer as Surface, widgets::Table};
use std::borrow::Cow;
pub use tui::widgets::{Cell, Row};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use helix_core::Position;
use helix_view::Editor;
use helix_view::{graphics::Rect, Editor};
use tui::layout::Constraint;
pub struct Menu<T> {
pub trait Item {
fn sort_text(&self) -> &str;
fn filter_text(&self) -> &str;
fn label(&self) -> &str;
fn row(&self) -> Row;
}
pub struct Menu<T: Item> {
options: Vec<T>,
cursor: Option<usize>,
@@ -23,19 +27,21 @@ pub struct Menu<T> {
/// (index, score)
matches: Vec<(usize, i64)>,
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
widths: Vec<Constraint>,
callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
scroll: usize,
size: (u16, u16),
viewport: (u16, u16),
recalculate: bool,
}
impl<T> Menu<T> {
impl<T: Item> Menu<T> {
// TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
// rendering)
pub fn new(
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let mut menu = Self {
@@ -43,10 +49,12 @@ impl<T> Menu<T> {
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
cursor: None,
format_fn: Box::new(format_fn),
widths: Vec::new(),
callback_fn: Box::new(callback_fn),
scroll: 0,
size: (0, 0),
viewport: (0, 0),
recalculate: true,
};
// TODO: scoring on empty input should just use a fastpath
@@ -56,31 +64,32 @@ impl<T> Menu<T> {
}
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,
ref format_fn,
..
} = *self;
// reuse the matches allocation
matches.clear();
matches.extend(
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
// TODO: maybe using format_fn isn't the best idea here
let text = (format_fn)(option);
let text = option.filter_text();
// TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher
.fuzzy_match(&text, pattern)
self.matcher
.fuzzy_match(text, pattern)
.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
self.cursor = None;
@@ -88,18 +97,55 @@ impl<T> Menu<T> {
}
pub fn move_up(&mut self) {
// TODO: wrap around to end
let pos = self.cursor.map_or(0, |i| i.saturating_sub(1)) % self.options.len();
let len = self.matches.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.adjust_scroll();
}
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.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) {
let win_height = self.size.1 as usize;
if let Some(cursor) = self.cursor {
@@ -134,7 +180,7 @@ impl<T> Menu<T> {
use super::PromptEvent as MenuEvent;
impl<T: 'static> Component for Menu<T> {
impl<T: Item + 'static> Component for Menu<T> {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let event = match event {
Event::Key(event) => event,
@@ -160,8 +206,8 @@ impl<T: 'static> Component for Menu<T> {
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::SHIFT,
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Up, ..
@@ -169,6 +215,10 @@ impl<T: 'static> Component for Menu<T> {
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@@ -186,6 +236,10 @@ impl<T: 'static> Component for Menu<T> {
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_down();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@@ -222,23 +276,19 @@ impl<T: 'static> Component for Menu<T> {
}
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let width = std::cmp::min(30, viewport.0);
const MAX: usize = 5;
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();
if viewport != self.viewport || self.recalculate {
self.recalculate_size(viewport);
}
Some(self.size)
}
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let style = cx.editor.theme.get("ui.text");
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme;
let style = theme
.try_get("ui.menu")
.unwrap_or_else(|| theme.get("ui.text"));
let selected = theme.get("ui.menu.selected");
let scroll = self.scroll;
@@ -264,26 +314,31 @@ impl<T: 'static> Component for Menu<T> {
let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height));
for (i, option) in options[scroll..(scroll + win_height).min(len)]
.iter()
.enumerate()
{
let line = Some(i + scroll);
// TODO: set bg for the whole row if selected
surface.set_stringn(
area.x,
area.y + i as u16,
(self.format_fn)(option),
area.width as usize - 1,
if line == self.cursor { selected } else { style },
);
let rows = options.iter().map(|option| option.row());
let table = Table::new(rows)
.style(style)
.highlight_style(selected)
.column_spacing(1)
.widths(&self.widths);
use tui::widgets::TableState;
table.render_table(
area,
surface,
&mut TableState {
offset: scroll,
selected: self.cursor,
},
);
for (i, _) in (scroll..(scroll + win_height).min(len)).enumerate() {
let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
if is_marked {
let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16);
cell.set_symbol("");
cell.set_style(selected);
// cell.set_style(selected);
// cell.set_style(if is_marked { selected } else { style });
}
}

View File

@@ -1,49 +1,60 @@
mod completion;
mod editor;
pub(crate) mod editor;
mod info;
mod markdown;
mod menu;
mod picker;
mod popup;
mod prompt;
mod spinner;
mod text;
pub use completion::Completion;
pub use editor::EditorView;
pub use markdown::Markdown;
pub use menu::Menu;
pub use picker::Picker;
pub use picker::{FilePicker, Picker};
pub use popup::Popup;
pub use prompt::{Prompt, PromptEvent};
pub use spinner::{ProgressSpinners, Spinner};
pub use text::Text;
pub use tui::layout::Rect;
pub use tui::style::{Color, Modifier, Style};
use helix_core::regex::Regex;
use helix_core::regex::RegexBuilder;
use helix_view::{Document, Editor, View};
use std::path::{Path, PathBuf};
use std::path::PathBuf;
pub fn regex_prompt(
cx: &mut crate::commands::Context,
prompt: String,
fun: impl Fn(&mut View, &mut Document, Regex) + 'static,
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
) -> Prompt {
let view_id = cx.view().id;
let snapshot = cx.doc().selection(view_id).clone();
let (view, doc) = current!(cx.editor);
let view_id = view.id;
let snapshot = doc.selection(view_id).clone();
Prompt::new(
prompt,
|input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
move |editor: &mut Editor, input: &str, event: PromptEvent| {
history_register,
|_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 {
PromptEvent::Abort => {
// TODO: also revert text
let (view, doc) = editor.current();
let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, snapshot.clone());
}
PromptEvent::Validate => {
// 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 => {
// skip empty input, TODO: trigger default
@@ -51,17 +62,25 @@ pub fn regex_prompt(
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) => {
let (view, doc) = editor.current();
let (view, doc) = current!(cx.editor);
// revert state to what it was before the last update
// TODO: also revert text
doc.set_selection(view.id, snapshot.clone());
fun(view, doc, regex);
fun(view, doc, 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
}
@@ -71,24 +90,57 @@ pub fn regex_prompt(
)
}
pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
use ignore::Walk;
let files = Walk::new(root.clone()).filter_map(|entry| match entry {
Ok(entry) => {
// filter dirs, but we might need special handling for symlinks!
if !entry.file_type().map_or(false, |entry| entry.is_dir()) {
Some(entry.into_path())
} else {
None
}
pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time;
// We want to exclude files that the editor can't handle yet
let mut type_builder = TypesBuilder::new();
let mut walk_builder = WalkBuilder::new(&root);
let walk_builder = match type_builder.add(
"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)
}
Err(_err) => None,
};
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;
}
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)
});
Some((entry.into_path(), time))
});
const MAX: usize = 1024;
let mut files: Vec<_> = if root.join(".git").is_dir() {
files.collect()
} else {
const MAX: usize = 8192;
files.take(MAX).collect()
};
Picker::new(
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| {
// format_fn
path.strip_prefix(&root)
@@ -98,30 +150,101 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
.into()
},
move |editor: &mut Editor, path: &PathBuf, action| {
let document_id = editor
editor
.open(path.into(), action)
.expect("editor.open failed");
},
|_editor, path| Some((path.clone(), None)),
)
}
pub mod completers {
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::cmp::Reverse;
pub type Completer = fn(&str) -> Vec<Completion>;
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
pub fn theme(input: &str) -> Vec<Completion> {
let mut names = theme::Loader::read_names(&helix_core::runtime_dir().join("themes"));
names.extend(theme::Loader::read_names(
&helix_core::config_dir().join("themes"),
));
names.push("default".into());
let mut names: Vec<_> = names
.into_iter()
.map(|name| ((0..), Cow::from(name)))
.collect();
let matcher = Matcher::default();
let mut matches: Vec<_> = names
.into_iter()
.filter_map(|(_range, name)| {
matcher.fuzzy_match(&name, input).map(|score| (name, score))
})
.collect();
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
names = matches.into_iter().map(|(name, _)| ((0..), name)).collect();
names
}
pub fn filename(input: &str) -> Vec<Completion> {
filename_impl(input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
if is_dir {
FileMatch::AcceptIncomplete
} else {
FileMatch::Accept
}
})
}
pub fn directory(input: &str) -> Vec<Completion> {
filename_impl(input, |entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
if is_dir {
FileMatch::Accept
} else {
FileMatch::Reject
}
})
}
#[derive(Copy, Clone, PartialEq, Eq)]
enum FileMatch {
/// Entry should be ignored
Reject,
/// Entry is usable but can't be the end (for instance if the entry is a directory and we
/// try to match a file)
AcceptIncomplete,
/// Entry is usable and can be the end of the match
Accept,
}
// TODO: we could return an iter/lazy thing so it can fetch as many as it needs.
fn filename_impl<F>(input: &str, filter_fn: F) -> Vec<Completion>
where
F: Fn(&ignore::DirEntry) -> FileMatch,
{
// Rust's filename handling is really annoying.
use ignore::WalkBuilder;
use std::path::{Path, PathBuf};
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('/') {
(path.into(), None)
(path, None)
} else {
let file_name = path
.file_name()
@@ -136,23 +259,40 @@ pub mod completers {
(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))
.build()
.filter_map(|file| {
file.ok().map(|entry| {
let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
file.ok().and_then(|entry| {
let fmatch = filter_fn(&entry);
if fmatch == FileMatch::Reject {
return None;
}
//let is_dir = entry.file_type().map_or(false, |entry| entry.is_dir());
let path = entry.path();
let mut path = path.strip_prefix(&dir).unwrap_or(path).to_path_buf();
let mut path = if is_tilde {
// if it's a single tilde an absolute path is displayed so that when `TAB` is pressed on
// one of the directories the tilde will be replaced with a valid path not with a relative
// home directory name.
// ~ -> <TAB> -> /home/user
// ~/ -> <TAB> -> ~/first_entry
path.to_path_buf()
} else {
path.strip_prefix(&dir).unwrap_or(path).to_path_buf()
};
if is_dir {
if fmatch == FileMatch::AcceptIncomplete {
path.push("");
}
let path = path.to_str().unwrap().to_owned();
(end.clone(), Cow::from(path))
Some((end.clone(), Cow::from(path)))
})
}) // TODO: unwrap or skip
.filter(|(_, path)| !path.is_empty()) // TODO
@@ -160,23 +300,19 @@ pub mod completers {
// if empty, return a list of dirs and files in current dir
if let Some(file_name) = file_name {
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use std::cmp::Reverse;
let matcher = Matcher::default();
// inefficient, but we need to calculate the scores, filter out None, then sort.
let mut matches: Vec<_> = files
.into_iter()
.filter_map(|(range, file)| {
.filter_map(|(_range, file)| {
matcher
.fuzzy_match(&file, &file_name)
.map(|score| (file, score))
})
.collect();
let range = ((input.len() - file_name.len())..);
let range = (input.len().saturating_sub(file_name.len()))..;
matches.sort_unstable_by_key(|(_file, score)| Reverse(*score));
files = matches

View File

@@ -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 tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
widgets::{Block, BorderType, Borders},
};
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
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 helix_core::Position;
use helix_view::editor::Action;
use helix_view::Editor;
use helix_view::{
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> {
options: Vec<T>,
@@ -23,10 +185,14 @@ pub struct Picker<T> {
matcher: Box<Matcher>,
/// (index, score)
matches: Vec<(usize, i64)>,
/// Filter over original options.
filters: Vec<usize>, // could be optimized into bit but not worth it now
cursor: usize,
// pattern: String,
prompt: Prompt,
/// Whether to render in the middle of the area
render_centered: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Editor, &T, Action)>,
@@ -34,14 +200,16 @@ pub struct Picker<T> {
impl<T> Picker<T> {
pub fn new(
render_centered: bool,
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, &T, Action) + 'static,
) -> Self {
let prompt = Prompt::new(
"".to_string(),
|pattern: &str| Vec::new(),
|editor: &mut Editor, pattern: &str, event: PromptEvent| {
"".into(),
None,
|_pattern: &str| Vec::new(),
|_editor: &mut Context, _pattern: &str, _event: PromptEvent| {
//
},
);
@@ -50,8 +218,10 @@ impl<T> Picker<T> {
options,
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
filters: Vec::new(),
cursor: 0,
prompt,
render_centered,
format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn),
};
@@ -63,47 +233,49 @@ impl<T> Picker<T> {
}
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;
// reuse the matches allocation
matches.clear();
matches.extend(
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.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
let text = (format_fn)(option);
let text = (self.format_fn)(option);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher
self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
matches.sort_unstable_by_key(|(_, score)| -score);
self.matches.sort_unstable_by_key(|(_, score)| -score);
// reset cursor position
self.cursor = 0;
}
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) {
// TODO: len - 1
if self.cursor < self.options.len() {
self.cursor += 1;
if self.matches.is_empty() {
return;
}
let len = self.matches.len();
let pos = (self.cursor + 1) % len;
self.cursor = pos;
}
pub fn selection(&self) -> Option<&T> {
@@ -111,6 +283,14 @@ impl<T> Picker<T> {
.get(self.cursor)
.map(|(index, _score)| &self.options[*index])
}
pub fn save_filter(&mut self) {
self.filters.clear();
self.filters
.extend(self.matches.iter().map(|(index, _)| *index));
self.filters.sort_unstable(); // used for binary search later
self.prompt.clear();
}
}
// process:
@@ -118,6 +298,14 @@ impl<T> Picker<T> {
// - on input change:
// - score all the names in relation to input
fn inner_rect(area: Rect) -> Rect {
let margin = Margin {
vertical: area.height * 10 / 100,
horizontal: area.width * 10 / 100,
};
area.inner(&margin)
}
impl<T: 'static> Component for Picker<T> {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let key_event = match event {
@@ -128,27 +316,50 @@ impl<T: 'static> Component for Picker<T> {
let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| {
// remove the layer
compositor.pop();
compositor.last_picker = compositor.pop();
})));
match key_event {
KeyEvent {
code: KeyCode::Up, ..
}
| KeyEvent {
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => self.move_up(),
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_up();
}
KeyEvent {
code: KeyCode::Down,
..
}
| KeyEvent {
code: KeyCode::Tab, ..
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
} => self.move_down(),
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_down();
}
KeyEvent {
code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => {
return close_fn;
}
@@ -162,7 +373,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn;
}
KeyEvent {
code: KeyCode::Char('x'),
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() {
@@ -179,6 +390,12 @@ impl<T: 'static> Component for Picker<T> {
}
return close_fn;
}
KeyEvent {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::CONTROL,
} => {
self.save_filter();
}
_ => {
if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) {
// TODO: recalculate only if pattern changed
@@ -190,24 +407,20 @@ impl<T: 'static> Component for Picker<T> {
EventResult::Consumed(None)
}
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let padding_vertical = area.height * 20 / 100;
let padding_horizontal = area.width * 20 / 100;
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let area = if self.render_centered {
inner_rect(area)
} else {
area
};
let area = Rect::new(
area.x + padding_horizontal,
area.y + padding_vertical,
area.width - padding_horizontal * 2,
area.height - padding_vertical * 2,
);
let text_style = cx.editor.theme.get("ui.text");
// -- Render the frame:
// clear area
let background = cx.editor.theme.get("ui.background");
surface.clear_with(area, background);
use tui::widgets::Widget;
// don't like this but the lifetime sucks
let block = Block::default().borders(Borders::ALL);
@@ -215,51 +428,75 @@ impl<T: 'static> Component for Picker<T> {
let inner = block.inner(area);
block.render(area, surface);
// TODO: abstract into a clear(area) fn
// surface.set_style(inner, Style::default().bg(Color::Rgb(150, 50, 0)));
// -- Render the input bar:
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);
// -- Separator
let style = Style::default().fg(Color::Rgb(90, 89, 119));
let symbols = BorderType::line_symbols(BorderType::Plain);
let sep_style = Style::default().fg(Color::Rgb(90, 89, 119));
let borders = BorderType::line_symbols(BorderType::Plain);
for x in inner.left()..inner.right() {
surface
.get_mut(x, inner.y + 1)
.set_symbol(symbols.horizontal)
.set_style(style);
.set_symbol(borders.horizontal)
.set_style(sep_style);
}
// -- 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 = Style::default().fg(Color::Rgb(255, 255, 255));
let selected = cx.editor.theme.get("ui.text.focus");
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
});
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
if i == self.cursor {
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
if i == (self.cursor - offset) {
surface.set_string(inner.x - 2, inner.y + i as u16, ">", selected);
}
surface.set_stringn(
inner.x + 3,
inner.y + 2 + i as u16,
surface.set_string_truncated(
inner.x,
inner.y + i as u16,
(self.format_fn)(option),
inner.width as usize - 1,
if i == self.cursor { selected } else { style },
inner.width as usize,
if i == (self.cursor - offset) {
selected
} else {
text_style
},
true,
);
}
}
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
self.prompt.cursor_position(area, editor)
fn cursor(&self, area: Rect, editor: &Editor) -> (Option<Position>, CursorKind) {
// TODO: this is mostly duplicate code
let area = inner_rect(area);
let block = Block::default().borders(Borders::ALL);
// calculate the inner area inside the box
let inner = block.inner(area);
// prompt area
let area = inner.clip_left(1).with_height(1);
self.prompt.cursor(area, editor)
}
}

View File

@@ -1,15 +1,9 @@
use crate::compositor::{Component, Compositor, Context, EventResult};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
};
use std::borrow::Cow;
use tui::buffer::Buffer as Surface;
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
// a width/height hint. maybe Popup(Box<Component>)
@@ -22,8 +16,6 @@ pub struct Popup<T: Component> {
}
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 {
Self {
contents,
@@ -37,6 +29,39 @@ impl<T: Component> Popup<T> {
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) {
if direction {
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 {
let key = match event {
Event::Key(event) => event,
Event::Resize(width, height) => {
Event::Resize(_, _) => {
// TODO: calculate inner area, call component's handle_event with that area
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.
}
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
.contents
.required_size((120, 26)) // max width, max height
@@ -111,34 +136,16 @@ impl<T: Component> Component for Popup<T> {
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);
let position = self
.position
.or_else(|| cx.editor.cursor_position())
.unwrap_or_default();
let (rel_x, rel_y) = self.get_rel_position(viewport, cx);
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
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
let background = cx.editor.theme.get("ui.popup");

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