Compare commits

...

219 Commits

Author SHA1 Message Date
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
80 changed files with 4229 additions and 1449 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

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

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

View File

@@ -37,6 +37,10 @@ jobs:
rust: stable
target: x86_64-pc-windows-msvc
cross: false
# - build: aarch64-macos
# os: macos-latest
# rust: stable
# target: aarch64-apple-darwin
# - build: x86_64-win-gnu
# os: windows-2019
# rust: stable-x86_64-gnu

1
.gitignore vendored
View File

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

7
.gitmodules vendored
View File

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

143
Cargo.lock generated
View File

@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
version = "1.0.40"
version = "1.0.41"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "28b2cd92db5cbd74e8e5028f7e27dd7aa3090e89e4f2a197cc7c8dfb69c7063b"
checksum = "15af2628f6890fe2609a3b91bef4c83450512802e59489f9c1cb1fa5df064a61"
[[package]]
name = "autocfg"
@@ -79,37 +79,36 @@ dependencies = [
[[package]]
name = "crossbeam-utils"
version = "0.8.4"
version = "0.8.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4feb231f0d4d6af81aed15928e58ecf5816aa62a2393e2c82f46973e92a9a278"
checksum = "d82cfc11ce7f2c3faef78d8a684447b40d503d9681acebed6cb728d45940c4db"
dependencies = [
"autocfg",
"cfg-if",
"lazy_static",
]
[[package]]
name = "crossterm"
version = "0.19.0"
version = "0.20.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7c36c10130df424b2f3552fcc2ddcd9b28a27b1e54b358b45874f88d1ca6888c"
checksum = "c0ebde6a9dd5e331cd6c6f48253254d117642c31653baa475e394657c59c1f7d"
dependencies = [
"bitflags",
"crossterm_winapi",
"futures-core",
"lazy_static",
"libc",
"mio",
"parking_lot",
"signal-hook",
"signal-hook-mio",
"winapi",
]
[[package]]
name = "crossterm_winapi"
version = "0.7.0"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0da8964ace4d3e4a044fd027919b2237000b24315a37c916f61809f1ff2140b9"
checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
dependencies = [
"winapi",
]
@@ -238,17 +237,11 @@ dependencies = [
"wasi",
]
[[package]]
name = "glob"
version = "0.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9b919933a397b79c37e33b77bb2aa3dc8eb6e165ad809e58ff75bc7db2e34574"
[[package]]
name = "globset"
version = "0.4.6"
version = "0.4.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c152169ef1e421390738366d2f796655fec62621dabbd0fd476f905934061e4a"
checksum = "f0fc1b9fa0e64ffb1aa5b95daa0f0f167734fd528b7c02eabc581d9d843649b1"
dependencies = [
"aho-corasick",
"bstr",
@@ -259,47 +252,45 @@ dependencies = [
[[package]]
name = "helix-core"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"etcetera",
"helix-syntax",
"once_cell",
"regex",
"ropey",
"rust-embed",
"serde",
"smallvec",
"tendril",
"toml",
"tree-sitter",
"unicode-general-category",
"unicode-segmentation",
"unicode-width",
]
[[package]]
name = "helix-lsp"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"anyhow",
"futures-executor",
"futures-util",
"glob",
"helix-core",
"jsonrpc-core",
"log",
"lsp-types",
"once_cell",
"pathdiff",
"serde",
"serde_json",
"thiserror",
"tokio",
"tokio-stream",
"url",
]
[[package]]
name = "helix-syntax"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"cc",
"serde",
@@ -309,7 +300,7 @@ dependencies = [
[[package]]
name = "helix-term"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"anyhow",
"chrono",
@@ -326,7 +317,6 @@ dependencies = [
"log",
"num_cpus",
"once_cell",
"pico-args",
"pulldown-cmark",
"serde",
"serde_json",
@@ -336,7 +326,7 @@ dependencies = [
[[package]]
name = "helix-tui"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"bitflags",
"cassowary",
@@ -348,13 +338,14 @@ dependencies = [
[[package]]
name = "helix-view"
version = "0.1.0"
version = "0.2.0"
dependencies = [
"anyhow",
"crossterm",
"helix-core",
"helix-lsp",
"helix-tui",
"log",
"once_cell",
"serde",
"slotmap",
@@ -385,9 +376,9 @@ dependencies = [
[[package]]
name = "ignore"
version = "0.4.17"
version = "0.4.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b287fb45c60bb826a0dc68ff08742b9d88a2fea13d6e0c286b3172065aaf878c"
checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d"
dependencies = [
"crossbeam-utils",
"globset",
@@ -428,7 +419,8 @@ dependencies = [
[[package]]
name = "jsonrpc-core"
version = "17.1.0"
source = "git+https://github.com/paritytech/jsonrpc#609d7a6cc160742d035510fa89fb424ccf077660"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4467ab6dfa369b69e52bd0692e480c4d117410538526a57a304a0f2250fd95e"
dependencies = [
"futures-util",
"log",
@@ -469,9 +461,9 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.89.1"
version = "0.89.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48b8a871b0a450bcec0e26d74a59583c8173cb9fb7d7f98889e18abb84838e0f"
checksum = "852e0dedfd52cc32325598b2631e0eba31b7b708959676a9f837042f276b09a2"
dependencies = [
"bitflags",
"serde",
@@ -566,9 +558,9 @@ dependencies = [
[[package]]
name = "once_cell"
version = "1.7.2"
version = "1.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56"
[[package]]
name = "parking_lot"
@@ -595,24 +587,12 @@ dependencies = [
"winapi",
]
[[package]]
name = "pathdiff"
version = "0.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "877630b3de15c0b64cc52f659345724fbf6bdad9bd9566699fc53688f3c34a34"
[[package]]
name = "percent-encoding"
version = "2.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d4fd5641d01c8f18a23da7b6fe29298ff4b55afcccdf78973b24cf3175fee32e"
[[package]]
name = "pico-args"
version = "0.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7d7afeb98c5a10e0bffcc7fc16e105b04d06729fac5fd6384aebf7ff5cb5a67d"
[[package]]
name = "pin-project-lite"
version = "0.2.6"
@@ -699,6 +679,39 @@ dependencies = [
"smallvec",
]
[[package]]
name = "rust-embed"
version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2fe1fe6aac5d6bb9e1ffd81002340363272a7648234ec7bdfac5ee202cb65523"
dependencies = [
"rust-embed-impl",
"rust-embed-utils",
"walkdir",
]
[[package]]
name = "rust-embed-impl"
version = "5.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ed91c41c42ef7bf687384439c312e75e0da9c149b0390889b94de3c7d9d9e66"
dependencies = [
"proc-macro2",
"quote",
"rust-embed-utils",
"syn",
"walkdir",
]
[[package]]
name = "rust-embed-utils"
version = "5.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2a512219132473ab0a77b52077059f1c47ce4af7fbdc94503e9862a34422876d"
dependencies = [
"walkdir",
]
[[package]]
name = "ryu"
version = "1.0.5"
@@ -764,20 +777,30 @@ dependencies = [
[[package]]
name = "signal-hook"
version = "0.1.17"
version = "0.3.9"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7e31d442c16f047a671b5a71e2161d6e68814012b7f5379d269ebd915fac2729"
checksum = "470c5a6397076fae0094aaf06a08e6ba6f37acb77d3b1b91ea92b4d6c8650c39"
dependencies = [
"libc",
"mio",
"signal-hook-registry",
]
[[package]]
name = "signal-hook-registry"
version = "1.3.0"
name = "signal-hook-mio"
version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "16f1d0fef1604ba8f7a073c7e701f213e056707210e9020af4528e0101ce11a6"
checksum = "29fd5867f1c4f2c5be079aee7a2adf1152ebb04a4bc4d341f504b7dece607ed4"
dependencies = [
"libc",
"mio",
"signal-hook",
]
[[package]]
name = "signal-hook-registry"
version = "1.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e51e73328dc4ac0c7ccbda3a494dfa03df1de2f46018127f60c693f2648455b0"
dependencies = [
"libc",
]
@@ -958,10 +981,16 @@ dependencies = [
]
[[package]]
name = "unicode-normalization"
version = "0.1.17"
name = "unicode-general-category"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07fbfce1c8a97d547e8b5334978438d9d6ec8c20e38f56d4a4374d181493eaef"
checksum = "07547e3ee45e28326cc23faac56d44f58f16ab23e413db526debce3b0bfd2742"
[[package]]
name = "unicode-normalization"
version = "0.1.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d54590932941a9e9266f0832deed84ebe1bf2e4c9e4a3554d393d18f5e854bf9"
dependencies = [
"tinyvec",
]

View File

@@ -13,6 +13,8 @@ myself agreeing with most of kakoune's design decisions.
For more information, see the [website](https://helix-editor.com) or
[documentation](https://docs.helix-editor.com/).
All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html)
# Features
- Vim-like modal editing
@@ -25,7 +27,8 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
# Installation
Note: Only Rust and Golang have indentation definitions at the moment.
Note: Only certain languages have indentation definitions at the moment. Check
`runtime/<lang>/` for `indents.toml`.
We provide packaging for various distributions, but here's a quick method to
build from source.
@@ -38,10 +41,33 @@ cargo install --path helix-term
This will install the `hx` binary to `$HOME/.cargo/bin`.
Now copy the `runtime/` directory somewhere. Helix will by default look for the
runtime inside the same folder as the executable, but that can be overriden via
the `HELIX_RUNTIME` environment variable.
Now copy the `runtime/` directory somewhere. Helix will by default look for the runtime
inside the config directory or the same directory as executable, but that can be overriden
via the `HELIX_RUNTIME` environment variable.
> NOTE: running via cargo doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically
> detect the `runtime` directory in the project root.
If you want to embed the `runtime/` directory into the Helix binary you can build
it with:
```
cargo install --path helix-term --features "embed_runtime"
```
## Arch Linux
There are two packages available from AUR:
- `helix-bin`: contains prebuilt binary from GitHub releases
- `helix-git`: builds the master branch of this repository
## MacOS
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.**
@@ -50,6 +76,9 @@ 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.
- Help with packaging on various distributions needed!
- To use print debugging to the `~/.cache/helix/helix.log` file, you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
- If your preferred language is missing, integrating a tree-sitter grammar for
it and defining syntax highlight queries for it is straight forward and
doesn't require much knowledge of the internals.
@@ -59,5 +88,4 @@ a good overview of the internals.
# Getting help
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-community:matrix.org).
Discuss the project on the community [Matrix Space](https://matrix.to/#/#helix-community:matrix.org) (make sure to join `#helix-editor:matrix.org` if you're on a client that doesn't support Matrix Spaces yet).

View File

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

View File

@@ -1 +1,88 @@
# Configuration
## Theme
Use a custom theme by placing a theme.toml in your config directory (i.e ~/.config/helix/theme.toml). The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/contrib/themes).
Styles in theme.toml are specified of in the form:
```toml
key = { fg = "#ffffff", bg = "#000000", modifiers = ["bold", "italic"] }
```
where `name` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, and `modifiers` is a list of style modifiers. `bg` and `modifiers` can be omitted to defer to the defaults.
To specify only the foreground color:
```toml
key = "#ffffff"
```
if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys).
```toml
"key.key" = "#ffffff"
```
Possible modifiers:
| modifier |
| --- |
| bold |
| dim |
| italic |
| underlined |
| slow\_blink |
| rapid\_blink |
| reversed |
| hidden |
| crossed\_out |
Possible keys:
| key | notes |
| --- | --- |
| attribute | |
| keyword | |
| keyword.directive | preprocessor directives (\#if in C) |
| namespace | |
| punctuation | |
| punctuation.delimiter | |
| operator | |
| special | |
| property | |
| variable | |
| variable.parameter | |
| type | |
| type.builtin | |
| constructor | |
| function | |
| function.macro | |
| function.builtin | |
| comment | |
| variable.builtin | |
| constant | |
| constant.builtin | |
| string | |
| number | |
| escape | escaped characters |
| label | used for lifetimes |
| module | |
| ui.background | |
| ui.linenr | |
| ui.statusline | |
| ui.popup | |
| ui.window | |
| ui.help | |
| ui.text | |
| ui.text.focus | |
| ui.menu.selected | |
| ui.selection | for selections in the editing area |
| warning | LSP warning |
| error | LSP error |
| info | LSP info |
| hint | LSP hint |
These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). We half-follow the common scopes from [macromates language grammars](https://macromates.com/manual/en/language_grammars) with some differences.
For a given highlight produced, styling will be determined based on the longest matching theme key. So it's enough to provide function to highlight `function.macro` and `function.builtin` as well, but you can use more specific scopes to highlight specific cases differently.

View File

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

View File

@@ -6,10 +6,10 @@
| Key | Description |
|-----|-----------|
| h | move left |
| j | move down |
| k | move up |
| l | move right |
| h, Left | move left |
| j, Down | move down |
| k, Up | move up |
| l, Right | move right |
| w | move next word start |
| b | move previous word start |
| e | move next word end |
@@ -17,20 +17,20 @@
| f | find next char |
| T | find 'till previous char |
| F | find previous char |
| ^ | move to the start of the line |
| $ | move to the end of the line |
| m | Jump to matching bracket |
| Home | move to the start of the line |
| End | 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 |
| ctrl-w | Enter window mode (maybe will be remove for spc w w later) |
| space | Enter space mode |
| K | Show documentation for the item under the cursor |
@@ -38,13 +38,14 @@
| Key | Description |
|-----|-----------|
| r | replace (single character change) |
| r | replace with a character |
| R | replace with yanked text |
| i | Insert before selection |
| a | Insert after selection (append) |
| I | Insert at the start of the line |
| A | Insert at the end of the line |
| o | Open new line below selection |
| o | Open new line above selection |
| 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 |
@@ -53,26 +54,26 @@
| > | Indent selection |
| < | Unindent selection |
| = | Format selection |
| d | Delete selection |
| c | Change selection (delete and enter insert mode) |
| d | Delete selection |
| c | Change selection (delete and enter insert mode) |
### Selection manipulation
| Key | Description |
|-----|-----------|
| s | Select all regex matches inside selections |
| S | Split selection into subselections on regex matches |
| alt-s | Split selection on newlines |
| ; | Collapse selection onto a single cursor |
| alt-; | Flip selection cursor and anchor |
| % | Select entire file |
| x | Select current line |
| X | Extend to next line |
| [ | Expand selection to parent syntax node TODO: pick a key |
| 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 |
| ctrl-c | Comment/uncomment the selections |
### Search
@@ -81,10 +82,22 @@ in reverse, or searching via smartcase.
| Key | Description |
|-----|-----------|
| / | Search for regex pattern |
| n | Select next search match |
| N | Add next search match to selection |
| * | Use current selection as the search pattern |
| / | Search for regex pattern |
| n | Select next search match |
| N | Add next search match to selection |
| * | Use current selection as the search pattern |
### Diagnostics
> NOTE: `[` and `]` will likely contain more pair mappings in the style of
> [vim-unimpaired](https://github.com/tpope/vim-unimpaired)
| Key | Description |
|-----|-----------|
| [d | Go to previous diagnostic |
| ]d | Go to next diagnostic |
| [D | Go to first diagnostic in document |
| ]D | Go to last diagnostic in document |
## Select / extend mode
@@ -118,15 +131,33 @@ Jumps to various locations.
|-----|-----------|
| 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 |
| h | Go to the start of the line |
| l | Go to the end of the line |
| s | Go to first non-whitespace character of the line |
| t | Go to the top of the screen |
| m | Go to the middle of the screen |
| b | Go to the bottom of the screen |
| d | Go to definition |
| y | Go to type definition |
| r | Go to references |
| i | Go to implementation |
| a | Go to the last accessed/alternate file |
## Object mode
TODO: Mappings for selecting syntax nodes (a superset of `[`).
## Window mode
This layer is similar to vim keybindings as kakoune does not support window.
| Key | Description |
|-----|-------------|
| w, ctrl-w | Switch to next window |
| v, ctrl-v | Vertical right split |
| h, ctrl-h | Horizontal bottom split |
| q, ctrl-q | Close current window |
## Space mode
This layer is a kludge of mappings I had under leader key in neovim.
@@ -135,7 +166,6 @@ This layer is a kludge of mappings I had under leader key in neovim.
|-----|-----------|
| 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 |
| s | Open symbol picker (current document)|
| w | Enter window mode |
| space | Keep primary selection TODO: it's here because space mode replaced it |

9
contrib/themes/README.md Normal file
View File

@@ -0,0 +1,9 @@
# User submitted themes
If you submit a theme, please include a comment at the top with your name and email address:
```toml
# Author : Name <email@my.domain>
```
We have a preview page for themes on our [wiki](https://github.com/helix-editor/helix/wiki/Themes)!

View File

@@ -0,0 +1,47 @@
# Author : Wojciech Kępka <wojciech@wkepka.dev>
"attribute" = "#dc7759"
"keyword" = { fg = "#dcb659", modifiers = ["bold"] }
"keyword.directive" = "#dcb659"
"namespace" = "#d32c5d"
"punctuation" = "#dc7759"
"punctuation.delimiter" = "#dc7759"
"operator" = { fg = "#dc7759", modifiers = ["bold"] }
"special" = "#7fdc59"
"property" = "#c6b8ad"
"variable" = "#c6b8ad"
"variable.parameter" = "#c6b8ad"
"type" = "#dc597f"
"type.builtin" = { fg = "#d32c5d", modifiers = ["bold"] }
"constructor" = "#dc597f"
"function" = "#59dcd8"
"function.macro" = { fg = "#dc7759", modifiers = ["bold"] }
"function.builtin" = { fg = "#59dcd8", modifiers = ["bold"] }
"comment" = "#627d9d"
"variable.builtin" = "#c6b8ad"
"constant" = "#59dcb7"
"constant.builtin" = "#59dcb7"
"string" = "#59dcb7"
"number" = "#59c0dc"
"escape" = { fg = "#7fdc59", modifiers = ["bold"] }
"label" = "#59c0dc"
"module" = "#d32c5d"
"ui.background" = { bg = "#161c23" }
"ui.linenr" = { fg = "#415367" }
"ui.statusline" = { bg = "#232d38" }
"ui.popup" = { bg = "#232d38" }
"ui.window" = { bg = "#232d38" }
"ui.help" = { bg = "#232d38", fg = "#e5ded6" }
"ui.text" = { fg = "#e5ded6" }
"ui.text.focus" = { fg = "#e5ded6", modifiers= ["bold"] }
"ui.selection" = { bg = "#540099" }
"ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" }
"warning" = "#dc7759"
"error" = "#dc597f"
"info" = "#59dcb7"
"hint" = "#59c0dc"

View File

@@ -0,0 +1,47 @@
# Author : Ingrid Rebecca Abraham <git@ingrids.email>
"attribute" = "#839A53"
"keyword" = { fg = "#D74E50", modifiers = ["bold"] }
"keyword.directive" = "#6F873E"
"namespace" = "#839A53"
"punctuation" = "#C97270"
"punctuation.delimiter" = "#C97270"
"operator" = { fg = "#D74E50", modifiers = ["bold"] }
"special" = "#D68482"
"property" = "#89BEB7"
"variable" = "#A6B6CE"
"variable.parameter" = "#89BEB7"
"type" = { fg = "#A6B6CE", modifiers = ["bold"] }
"type.builtin" = "#839A53"
"constructor" = { fg = "#839A53", modifiers = ["bold"] }
"function" = { fg = "#89BEB7", modifiers = ["bold"] }
"function.macro" = { fg = "#D4A520", modifiers = ["bold"] }
"function.builtin" = "#89BEB7"
"comment" = "#A6B6CE"
"variable.builtin" = "#D4A520"
"constant" = "#D4A520"
"constant.builtin" = "#D4A520"
"string" = "#D74E50"
"number" = "#D74E50"
"escape" = { fg = "#D74E50", modifiers = ["bold"] }
"label" = "#D68482"
"module" = "#839A53"
"ui.background" = { bg = "#FFFCFD" }
"ui.linenr" = { fg = "#bbbbbb" }
"ui.statusline" = { bg = "#F3EAE9" }
"ui.popup" = { bg = "#F3EAE9" }
"ui.window" = { bg = "#D8B8B3" }
"ui.help" = { bg = "#D8B8B3", fg = "#250E07" }
"ui.text" = { fg = "#7B91B3" }
"ui.text.focus" = { fg = "#250E07", modifiers= ["bold"] }
"ui.selection" = { bg = "#540099" }
"ui.menu.selected" = { fg = "#D74E50", bg = "#F3EAE9" }
"warning" = "#D4A520"
"error" = "#D74E50"
"info" = "#839A53"
"hint" = "#A6B6CE"

View File

@@ -0,0 +1,41 @@
# Author : Gokul Soumya <gokulps15@gmail.com>
"attribute" = { fg = "#E5C07B" }
"comment" = { fg = "#5C6370", modifiers = ['italic'] }
"constant" = { fg = "#56B6C2" }
"constant.builtin" = { fg = "#61AFEF" }
"constructor" = { fg = "#61AFEF" }
"escape" = { fg = "#D19A66" }
"function" = { fg = "#61AFEF" }
"function.builtin" = { fg = "#61AFEF" }
"function.macro" = { fg = "#C678DD" }
"keyword" = { fg = "#E06C75" }
"keyword.directive" = { fg = "#C678DD" }
"label" = { fg = "#C678DD" }
"namespace" = { fg = "#61AFEF" }
"number" = { fg = "#D19A66" }
"operator" = { fg = "#C678DD" }
"property" = { fg = "#E06C75" }
"special" = { fg = "#61AFEF" }
"string" = { fg = "#98C379" }
"type" = { fg = "#E5C07B" }
"type.builtin" = { fg = "#E5C07B" }
"variable" = { fg = "#61AFEF" }
"variable.builtin" = { fg = "#61AFEF" }
"variable.parameter" = { fg = "#E06C75" }
"info" = { fg = "#61afef", modifiers = ['bold'] }
"hint" = { fg = "#98c379", modifiers = ['bold'] }
"warning" = { fg = "#e5c07b", modifiers = ['bold'] }
"error" = { fg = "#e06c75", modifiers = ['bold'] }
"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" }
"ui.background" = { fg = "#ABB2BF", bg = "#282C34" }
"ui.help" = { bg = "#3E4452" }
"ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] }
"ui.popup" = { bg = "#3E4452" }
"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" }
"ui.selection" = { bg = "#3E4452" }
"ui.text" = { fg = "#ABB2BF", bg = "#282C34" }
"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] }
"ui.window" = { bg = "#3E4452" }

124
flake.lock generated
View File

@@ -1,73 +1,83 @@
{
"nodes": {
"flake-utils": {
"devshell": {
"locked": {
"lastModified": 1620759905,
"narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=",
"lastModified": 1622711433,
"narHash": "sha256-rGjXz7FA7HImAT3TtoqwecByLO5yhVPSwPdaYPBFRQw=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8",
"repo": "devshell",
"rev": "1f4fb67b662b65fa7cfe696fc003fcc1e8f7cc36",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"repo": "devshell",
"type": "github"
}
},
"flake-utils_2": {
"flakeCompat": {
"flake": false,
"locked": {
"lastModified": 1614513358,
"narHash": "sha256-LakhOx3S1dRjnh0b5Dg3mbZyH0ToC9I8Y2wKSkBaTzU=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "5466c5bbece17adaab2d82fae80b46e807611bf3",
"lastModified": 1606424373,
"narHash": "sha256-oq8d4//CJOrVj+EcOaSXvMebvuTkmBJuT5tzlfewUnQ=",
"owner": "edolstra",
"repo": "flake-compat",
"rev": "99f1c2157fba4bfe6211a321fd0ee43199025dbf",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"owner": "edolstra",
"repo": "flake-compat",
"type": "github"
}
},
"naersk": {
"helix": {
"flake": false,
"locked": {
"lastModified": 1623545930,
"narHash": "sha256-14ASoYbxXHU/qPGctiUymb4fMRCoih9c7YujjxqEkdU=",
"ref": "master",
"rev": "9640ed1425f2db904fb42cd0c54dc6fbc05ca292",
"revCount": 821,
"submodules": true,
"type": "git",
"url": "https://github.com/helix-editor/helix.git"
},
"original": {
"submodules": true,
"type": "git",
"url": "https://github.com/helix-editor/helix.git"
}
},
"nixCargoIntegration": {
"inputs": {
"nixpkgs": "nixpkgs"
"devshell": "devshell",
"nixpkgs": [
"nixpkgs"
],
"rustOverlay": "rustOverlay"
},
"locked": {
"lastModified": 1620316130,
"narHash": "sha256-sU0VS5oJS1FsHsZsLELAXc7G2eIelVuucRw+q5B1x9k=",
"owner": "nmattia",
"repo": "naersk",
"rev": "a3f40fe42cc6d267ff7518fa3199e99ff1444ac4",
"lastModified": 1623591988,
"narHash": "sha256-a8E5LYKxYjHmBWZsFxKnCBVGsWHFEWrKjeAJkplWrfI=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "8254b71eddd4e85173eddc189174b873fad85360",
"type": "github"
},
"original": {
"owner": "nmattia",
"repo": "naersk",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"type": "github"
}
},
"nixpkgs": {
"locked": {
"lastModified": 1622059058,
"narHash": "sha256-t1/ZMtyxClVSfcV4Pt5C1YpkeJ/UwFF3oitLD7Ch/UA=",
"path": "/nix/store/2gam4i1fa1v19k3n5rc9vgvqac1c2xj5-source",
"rev": "84aa23742f6c72501f9cc209f29c438766f5352d",
"type": "path"
},
"original": {
"id": "nixpkgs",
"type": "indirect"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1622194753,
"narHash": "sha256-76qtvFp/vFEz46lz5iZMJ0mnsWQYmuGYlb0fHgKqqMg=",
"lastModified": 1623324058,
"narHash": "sha256-Jm9GUTXdjXz56gWDKy++EpFfjrBaxqXlLvTLfgEi8lo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "540dccb2aeaffa9dc69bfdc41c55abd7ccc6baa3",
"rev": "432fc2d9a67f92e05438dff5fdc2b39d33f77997",
"type": "github"
},
"original": {
@@ -77,40 +87,22 @@
"type": "github"
}
},
"nixpkgs_3": {
"locked": {
"lastModified": 1617325113,
"narHash": "sha256-GksR0nvGxfZ79T91UUtWjjccxazv6Yh/MvEJ82v1Xmw=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "54c1e44240d8a527a8f4892608c4bce5440c3ecb",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flake-utils": "flake-utils",
"naersk": "naersk",
"nixpkgs": "nixpkgs_2",
"rust-overlay": "rust-overlay"
"flakeCompat": "flakeCompat",
"helix": "helix",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs"
}
},
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils_2",
"nixpkgs": "nixpkgs_3"
},
"rustOverlay": {
"flake": false,
"locked": {
"lastModified": 1622257069,
"narHash": "sha256-+QVnS/es9JCRZXphoHL0fOIUhpGqB4/wreBsXWArVck=",
"lastModified": 1623550815,
"narHash": "sha256-RumRrkE6OTJDndHV4qZNZv8kUGnzwRHZQSyzx29r6/g=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "8aa5f93c0b665e5357af19c5631a3450bff4aba5",
"rev": "9824f142cbd7bc3e2a92eefbb79addfff8704cd3",
"type": "github"
},
"original": {

View File

@@ -3,31 +3,54 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
flake-utils.url = "github:numtide/flake-utils";
rust-overlay.url = "github:oxalica/rust-overlay";
naersk.url = "github:nmattia/naersk";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
};
flakeCompat = {
url = "github:edolstra/flake-compat";
flake = false;
};
helix = {
url = "https://github.com/helix-editor/helix.git";
type = "git";
flake = false;
submodules = true;
};
};
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@{ nixCargoIntegration, helix, ... }:
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: { buildInputs = (prev.buildInputs or [ ]) ++ [ common.cCompiler.cc.lib ]; };
# link runtime since helix-core expects it because of embed_runtime feature
helix-core = _: { preConfigure = "ln -s ${common.root + "/runtime"} ../runtime"; };
# link languages and theme toml files since helix-view expects them
helix-view = _: { preConfigure = "ln -s ${common.root}/{languages.toml,theme.toml} .."; };
helix-syntax = prev: {
src = common.pkgs.runCommand prev.src.name { } ''
mkdir -p $out
ln -s ${prev.src}/* $out
ln -sf ${helix}/helix-syntax/languages $out
'';
};
};
in rec {
packages.helix = naerskLib.buildPackage {
pname = "helix";
root = ./.;
shell = common: prev: {
packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb ]);
env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; }
{ name = "RUSTFLAGS"; value = "-C link-arg=-fuse-ld=lld -C target-cpu=native"; }
];
};
defaultPackage = packages.helix;
devShell = pkgs.callPackage ./shell.nix {};
});
build = _: prev: { rootFeatures = prev.rootFeatures ++ [ "embed_runtime" ]; };
};
};
}

View File

@@ -1,26 +1,32 @@
[package]
name = "helix-core"
version = "0.1.0"
version = "0.2.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
embed_runtime = ["rust-embed"]
[dependencies]
helix-syntax = { path = "../helix-syntax" }
ropey = "1.2"
smallvec = "1.4"
tendril = "0.4.2"
unicode-segmentation = "1.6"
unicode-segmentation = "1.7.1"
unicode-width = "0.1"
unicode-general-category = "0.4.0"
# slab = "0.4.2"
tree-sitter = "0.19"
once_cell = "1.4"
once_cell = "1.8"
regex = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
etcetera = "0.3"
rust-embed = { version = "5.9.0", optional = true }

View File

@@ -67,7 +67,7 @@ fn handle_open(
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);
@@ -109,7 +109,7 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
let transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);

View File

@@ -1,5 +1,5 @@
use crate::{
find_first_non_whitespace_char2, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
};
use core::ops::Range;
use std::borrow::Cow;
@@ -14,7 +14,7 @@ fn find_line_comment(
let mut min = usize::MAX; // minimum col for find_first_non_whitespace_char
for line in lines {
let line_slice = text.line(line);
if let Some(pos) = find_first_non_whitespace_char2(line_slice) {
if let Some(pos) = find_first_non_whitespace_char(line_slice) {
let len = line_slice.len_chars();
if pos < min {

View File

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

View File

@@ -3,6 +3,8 @@ use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;
use std::fmt;
#[must_use]
pub fn grapheme_width(g: &str) -> usize {
if g.as_bytes()[0] <= 127 {
@@ -156,6 +158,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 {

View File

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

View File

@@ -105,11 +105,14 @@ fn suggested_indent_for_line(
line_num: usize,
tab_width: usize,
) -> usize {
let line = text.line(line_num);
let current = indent_level_for_line(line, tab_width);
if let Some(start) = find_first_non_whitespace_char(text, line_num) {
return suggested_indent_for_pos(Some(language_config), syntax, text, start, false);
if let Some(start) = find_first_non_whitespace_char(text.line(line_num)) {
return suggested_indent_for_pos(
Some(language_config),
syntax,
text,
start + text.line_to_char(line_num),
false,
);
};
// if the line is blank, indent should be zero
@@ -251,22 +254,26 @@ where
Configuration, IndentationConfiguration, Lang, 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,
highlight_config: OnceCell::new(),
//
roots: vec![],
language_server: None,
indent: Some(IndentationConfiguration {
tab_width: 4,
unit: String::from(" "),
}),
indent_query: OnceCell::new(),
}],
});
let loader = Loader::new(
Configuration {
language: vec![LanguageConfiguration {
scope: "source.rust".to_string(),
file_types: vec!["rs".to_string()],
language_id: Lang::Rust,
highlight_config: OnceCell::new(),
//
roots: vec![],
auto_format: false,
language_server: None,
indent: Some(IndentationConfiguration {
tab_width: 4,
unit: String::from(" "),
}),
indent_query: OnceCell::new(),
}],
},
Vec::new(),
);
// set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));

View File

@@ -3,7 +3,7 @@ pub mod auto_pairs;
pub mod comment;
pub mod diagnostic;
pub mod graphemes;
mod history;
pub mod history;
pub mod indent;
pub mod macros;
pub mod match_brackets;
@@ -17,58 +17,85 @@ mod state;
pub mod syntax;
mod transaction;
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
// find first non-whitespace char
for (start, ch) in line.chars().enumerate() {
// TODO: could use memchr with chunks?
if ch != ' ' && ch != '\t' && ch != '\n' {
return Some(start);
}
}
static RUNTIME_DIR: once_cell::sync::Lazy<std::path::PathBuf> =
once_cell::sync::Lazy::new(runtime_dir);
None
pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace())
}
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);
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()
})
#[cfg(not(embed_runtime))]
fn runtime_dir() -> std::path::PathBuf {
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 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
}
use etcetera::base_strategy::{choose_base_strategy, BaseStrategy};
pub use ropey::{Rope, RopeSlice};
pub use tendril::StrTendril as Tendril;
pub use unicode_general_category::get_general_category;
#[doc(inline)]
pub use {regex, tree_sitter};
@@ -78,7 +105,6 @@ pub use smallvec::SmallVec;
pub use syntax::Syntax;
pub use diagnostic::Diagnostic;
pub use history::History;
pub use state::State;
pub use transaction::{Assoc, Change, ChangeSet, Operation, Transaction};

View File

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

View File

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

View File

@@ -6,16 +6,15 @@ use std::{collections::HashMap, sync::RwLock};
static REGISTRY: Lazy<RwLock<HashMap<char, Vec<String>>>> =
Lazy::new(|| RwLock::new(HashMap::new()));
pub fn get(register: char) -> Option<Vec<String>> {
/// Read register values.
pub fn get(register_name: char) -> Option<Vec<String>> {
let registry = REGISTRY.read().unwrap();
// TODO: no cloning
registry.get(&register).cloned()
registry.get(&register_name).cloned() // TODO: no cloning
}
/// Read register values.
// restoring: bool
pub fn set(register: char, values: Vec<String>) {
pub fn set(register_name: char, values: Vec<String>) {
let mut registry = REGISTRY.write().unwrap();
registry.insert(register, values);
registry.insert(register_name, values);
}

View File

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

View File

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

View File

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

View File

@@ -5,6 +5,7 @@ use std::{
borrow::Cow,
cell::RefCell,
collections::{HashMap, HashSet},
fmt,
path::{Path, PathBuf},
sync::Arc,
};
@@ -12,13 +13,13 @@ use std::{
use once_cell::sync::{Lazy, OnceCell};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
}
// largely based on tree-sitter/cli/src/loader.rs
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
@@ -27,8 +28,8 @@ pub struct LanguageConfiguration {
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
// pub path: PathBuf,
// root_path for tree-sitter (^)
#[serde(default)]
pub auto_format: bool,
// content_regex
// injection_regex
@@ -46,7 +47,7 @@ pub struct LanguageConfiguration {
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct LanguageServerConfiguration {
pub command: String,
@@ -55,14 +56,14 @@ pub struct LanguageServerConfiguration {
pub args: Vec<String>,
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndentationConfiguration {
pub tab_width: usize,
pub unit: String,
}
#[derive(Serialize, Deserialize)]
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndentQuery {
#[serde(default)]
@@ -73,16 +74,48 @@ pub struct IndentQuery {
pub outdent: HashSet<String>,
}
#[cfg(not(feature = "embed_runtime"))]
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let path = crate::RUNTIME_DIR
.join("queries")
.join(language)
.join(filename);
std::fs::read_to_string(&path)
}
#[cfg(feature = "embed_runtime")]
fn load_runtime_file(language: &str, filename: &str) -> Result<String, Box<dyn std::error::Error>> {
use std::fmt;
#[derive(rust_embed::RustEmbed)]
#[folder = "../runtime/"]
struct Runtime;
#[derive(Debug)]
struct EmbeddedFileNotFoundError {
path: PathBuf,
}
impl std::error::Error for EmbeddedFileNotFoundError {}
impl fmt::Display for EmbeddedFileNotFoundError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "failed to load embedded file {}", self.path.display())
}
}
let path = PathBuf::from("queries").join(language).join(filename);
if let Some(query_bytes) = Runtime::get(&path.display().to_string()) {
String::from_utf8(query_bytes.to_vec()).map_err(|err| err.into())
} else {
Err(Box::new(EmbeddedFileNotFoundError { path }))
}
}
fn read_query(language: &str, filename: &str) -> String {
static INHERITS_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r";+\s*inherits\s*:?\s*([a-z_,()]+)\s*").unwrap());
let root = crate::runtime_dir();
// let root = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let path = root.join("queries").join(language).join(filename);
let query = std::fs::read_to_string(&path).unwrap_or_default();
let query = load_runtime_file(language, filename).unwrap_or_default();
// TODO: the collect() is not ideal
let inherits = INHERITS_REGEX
@@ -146,11 +179,8 @@ impl LanguageConfiguration {
.get_or_init(|| {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let root = crate::runtime_dir();
let path = root.join("queries").join(language).join("indents.toml");
let toml = std::fs::read(&path).ok()?;
toml::from_slice(&toml).ok()
let toml = load_runtime_file(&language, "indents.toml").ok()?;
toml::from_slice(&toml.as_bytes()).ok()
})
.as_ref()
}
@@ -162,17 +192,20 @@ impl LanguageConfiguration {
pub static LOADER: OnceCell<Loader> = OnceCell::new();
#[derive(Debug)]
pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
scopes: Vec<String>,
}
impl Loader {
pub fn new(config: Configuration) -> Self {
pub fn new(config: Configuration, scopes: Vec<String>) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(),
scopes,
};
for config in config.language {
@@ -192,6 +225,10 @@ impl Loader {
loader
}
pub fn scopes(&self) -> &[String] {
&self.scopes
}
pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageConfiguration>> {
// Find all the language configurations that match this file name
// or a suffix of the file name.
@@ -223,6 +260,12 @@ pub struct TsParser {
cursors: Vec<QueryCursor>,
}
impl fmt::Debug for TsParser {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TsParser").finish()
}
}
// could also just use a pool, or a single instance?
thread_local! {
pub static PARSER: RefCell<TsParser> = RefCell::new(TsParser {
@@ -231,6 +274,7 @@ thread_local! {
})
}
#[derive(Debug)]
pub struct Syntax {
config: Arc<HighlightConfiguration>,
@@ -333,7 +377,11 @@ impl Syntax {
// prevents them from being moved. But both of these values are really just
// pointers, so it's actually ok to move them.
let mut cursor = QueryCursor::new(); // reuse a pool
// reuse a cursor from the pool if possible
let mut cursor = PARSER.with(|ts_parser| {
let highlighter = &mut ts_parser.borrow_mut();
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
});
let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
@@ -407,6 +455,7 @@ impl Syntax {
// buffer_range_for_scope_at_pos
}
#[derive(Debug)]
pub struct LanguageLayer {
// mode
// grammar
@@ -715,6 +764,7 @@ pub enum HighlightEvent {
/// Contains the data neeeded to higlight code written in a particular language.
///
/// This struct is immutable and can be shared between threads.
#[derive(Debug)]
pub struct HighlightConfiguration {
pub language: Grammar,
pub query: Query,
@@ -745,6 +795,7 @@ struct LocalScope<'a> {
local_defs: Vec<LocalDef<'a>>,
}
#[derive(Debug)]
struct HighlightIter<'a, 'tree: 'a, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
@@ -770,6 +821,12 @@ struct HighlightIterLayer<'a, 'tree: 'a> {
depth: usize,
}
impl<'a, 'tree: 'a> fmt::Debug for HighlightIterLayer<'a, 'tree> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("HighlightIterLayer").finish()
}
}
impl HighlightConfiguration {
/// Creates a `HighlightConfiguration` for a given `Grammar` and set of highlighting
/// queries.
@@ -1700,3 +1757,13 @@ fn test_input_edits() {
}]
);
}
#[test]
fn test_load_runtime_file() {
// Test to make sure we can load some data from the runtime directory.
let contents = load_runtime_file("rust", "indents.toml").unwrap();
assert!(!contents.is_empty());
let results = load_runtime_file("rust", "does-not-exist");
assert!(results.is_err());
}

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-lsp"
version = "0.1.0"
version = "0.2.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
@@ -10,20 +10,14 @@ license = "MPL-2.0"
[dependencies]
helix-core = { path = "../helix-core" }
once_cell = "1.4"
lsp-types = { version = "0.89", features = ["proposed"] }
tokio = { version = "1", features = ["full"] }
tokio-stream = "0.1.5"
futures-executor = { version = "0.3" }
url = "2"
pathdiff = "0.2"
glob = "0.3"
anyhow = "1"
serde_json = "1.0"
serde = { version = "1.0", features = ["derive"] }
# jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
jsonrpc-core = { git = "https://github.com/paritytech/jsonrpc", default-features = false } # don't pull in all of futures
anyhow = "1.0"
futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
jsonrpc-core = { version = "17.1", default-features = false } # don't pull in all of futures
log = "0.4"
lsp-types = { version = "0.89", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
log = "~0.4"
tokio = { version = "1.6", features = ["full"] }
tokio-stream = "0.1.6"

View File

@@ -3,31 +3,24 @@ 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};
use tokio::{
io::{BufReader, BufWriter},
// prelude::*,
process::{Child, Command},
sync::mpsc::{channel, UnboundedReceiver, UnboundedSender},
};
#[derive(Debug)]
pub struct Client {
_process: Child,
outgoing: UnboundedSender<Payload>,
// pub incoming: Receiver<Call>,
pub request_counter: AtomicU64,
server_tx: UnboundedSender<Payload>,
request_counter: AtomicU64,
capabilities: Option<lsp::ServerCapabilities>,
offset_encoding: OffsetEncoding,
}
@@ -43,40 +36,27 @@ impl Client {
.kill_on_drop(true)
.spawn();
// use std::io::ErrorKind;
let mut process = match process {
Ok(process) => process,
Err(err) => match err.kind() {
// ErrorKind::NotFound | ErrorKind::PermissionDenied => {
// return Err(Error::Other(err.into()))
// }
_kind => return Err(Error::Other(err.into())),
},
};
let mut process = process?;
// TODO: do we need bufreader/writer here? or do we use async wrappers on unblock?
let writer = BufWriter::new(process.stdin.take().expect("Failed to open stdin"));
let reader = BufReader::new(process.stdout.take().expect("Failed to open stdout"));
let stderr = BufReader::new(process.stderr.take().expect("Failed to open stderr"));
let (incoming, outgoing) = Transport::start(reader, writer, stderr);
let (server_rx, server_tx) = Transport::start(reader, writer, stderr);
let client = Self {
_process: process,
outgoing,
// incoming,
server_tx,
request_counter: AtomicU64::new(0),
capabilities: None,
// diagnostics: HashMap::new(),
offset_encoding: OffsetEncoding::Utf8,
};
// TODO: async client.initialize()
// maybe use an arc<atomic> flag
Ok((client, incoming))
Ok((client, server_rx))
}
fn next_request_id(&self) -> jsonrpc::Id {
@@ -106,7 +86,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 +98,20 @@ impl Client {
}
/// Execute a RPC request on the language server.
pub fn call<R: lsp::request::Request>(
fn call<R: lsp::request::Request>(
&self,
params: R::Params,
) -> impl Future<Output = Result<Value>>
where
R::Params: serde::Serialize,
{
let outgoing = self.outgoing.clone();
let server_tx = self.server_tx.clone();
let id = self.next_request_id();
async move {
use std::time::Duration;
use tokio::time::timeout;
let params = serde_json::to_value(params)?;
let request = jsonrpc::MethodCall {
@@ -140,32 +123,29 @@ impl Client {
let (tx, mut rx) = channel::<Result<Value>>(1);
outgoing
server_tx
.send(Payload::Request {
chan: tx,
value: request,
})
.map_err(|e| Error::Other(e.into()))?;
use std::time::Duration;
use tokio::time::timeout;
timeout(Duration::from_secs(2), rx.recv())
.await
.map_err(|_| Error::Timeout)? // return Timeout
.unwrap() // TODO: None if channel closed
.ok_or(Error::StreamClosed)?
}
}
/// Send a RPC notification to the language server.
pub fn notify<R: lsp::notification::Notification>(
fn notify<R: lsp::notification::Notification>(
&self,
params: R::Params,
) -> impl Future<Output = Result<()>>
where
R::Params: serde::Serialize,
{
let outgoing = self.outgoing.clone();
let server_tx = self.server_tx.clone();
async move {
let params = serde_json::to_value(params)?;
@@ -176,7 +156,7 @@ impl Client {
params: Self::value_into_params(params),
};
outgoing
server_tx
.send(Payload::Notification(notification))
.map_err(|e| Error::Other(e.into()))?;
@@ -205,7 +185,7 @@ impl Client {
}),
};
self.outgoing
self.server_tx
.send(Payload::Response(output))
.map_err(|e| Error::Other(e.into()))?;
@@ -216,15 +196,16 @@ impl Client {
// General messages
// -------------------------------------------------------------------------------------------
pub async fn initialize(&mut self) -> Result<()> {
pub(crate) async fn initialize(&mut self) -> Result<()> {
// TODO: delay any requests that are triggered prior to initialize
let root = find_root(None).and_then(|root| lsp::Url::from_file_path(root).ok());
#[allow(deprecated)]
let params = lsp::InitializeParams {
process_id: Some(std::process::id()),
// root_path is obsolete, use root_uri
root_path: None,
// root_uri: Some(lsp_types::Url::parse("file://localhost/")?),
root_uri: None, // set to project root in the future
root_uri: root,
initialization_options: None,
capabilities: lsp::ClientCapabilities {
text_document: Some(lsp::TextDocumentClientCapabilities {
@@ -247,6 +228,11 @@ impl Client {
}),
..Default::default()
}),
window: Some(lsp::WindowClientCapabilities {
// TODO: temporarily disabled until we implement handling for window/workDoneProgress/create
// work_done_progress: Some(true),
..Default::default()
}),
..Default::default()
},
trace: None,
@@ -674,4 +660,17 @@ impl Client {
self.call::<lsp::request::References>(params)
}
pub fn document_symbols(
&self,
text_document: lsp::TextDocumentIdentifier,
) -> impl Future<Output = Result<Value>> {
let params = lsp::DocumentSymbolParams {
text_document,
work_done_progress_params: lsp::WorkDoneProgressParams::default(),
partial_result_params: lsp::PartialResultParams::default(),
};
self.call::<lsp::request::DocumentSymbolRequest>(params)
}
}

View File

@@ -1,26 +1,27 @@
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::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 +29,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 +55,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 +126,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 +138,17 @@ pub mod util {
lsp::Range::new(start, end)
}
pub fn lsp_range_to_range(
doc: &Rope,
range: lsp::Range,
offset_encoding: OffsetEncoding,
) -> Option<Range> {
let start = lsp_pos_to_pos(doc, range.start, offset_encoding)?;
let end = lsp_pos_to_pos(doc, range.end, offset_encoding)?;
Some(Range::new(start, end))
}
pub fn generate_transaction_from_edits(
doc: &Rope,
edits: Vec<lsp::TextEdit>,
@@ -114,14 +164,21 @@ pub mod util {
None
};
let start = lsp_pos_to_pos(doc, edit.range.start, offset_encoding);
let end = lsp_pos_to_pos(doc, edit.range.end, offset_encoding);
let start =
if let Some(start) = lsp_pos_to_pos(doc, edit.range.start, offset_encoding) {
start
} else {
return (0, 0, None);
};
let end = if let Some(end) = lsp_pos_to_pos(doc, edit.range.end, offset_encoding) {
end
} else {
return (0, 0, None);
};
(start, end, replacement)
}),
)
}
// apply_insert_replace_edit
}
#[derive(Debug, PartialEq, Clone)]
@@ -129,6 +186,7 @@ pub enum Notification {
PublishDiagnostics(lsp::PublishDiagnosticsParams),
ShowMessage(lsp::ShowMessageParams),
LogMessage(lsp::LogMessageParams),
ProgressMessage(lsp::ProgressParams),
}
impl Notification {
@@ -146,17 +204,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,14 +228,9 @@ 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, Arc<Client>>,
pub incoming: SelectAll<UnboundedReceiverStream<Call>>,
}
@@ -193,35 +249,29 @@ impl Registry {
}
}
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Option<Arc<Client>> {
// TODO: propagate the error
pub fn get(&mut self, language_config: &LanguageConfiguration) -> Result<Arc<Client>> {
if let Some(config) = &language_config.language_server {
// avoid borrow issues
let inner = &mut self.inner;
let s_incoming = &self.incoming;
let language_server = inner
.entry(language_config.scope.clone()) // can't use entry with Borrow keys: https://github.com/rust-lang/rfcs/pull/1769
.or_insert_with(|| {
// TODO: lookup defaults for id (name, args)
let s_incoming = &mut self.incoming;
match inner.entry(language_config.scope.clone()) {
Entry::Occupied(language_server) => Ok(language_server.get().clone()),
Entry::Vacant(entry) => {
// initialize a new client
let (mut client, incoming) =
Client::start(&config.command, &config.args).ok()?;
let (mut client, incoming) = Client::start(&config.command, &config.args)?;
// TODO: run this async without blocking
futures_executor::block_on(client.initialize()).unwrap();
futures_executor::block_on(client.initialize())?;
s_incoming.push(UnboundedReceiverStream::new(incoming));
let client = Arc::new(client);
Some(Arc::new(client))
})
.clone();
return language_server;
entry.insert(client.clone());
Ok(client)
}
}
} else {
Err(Error::LspNotDefined)
}
None
}
}
@@ -250,3 +300,34 @@ impl Registry {
// there needs to be a way to process incoming lsp messages from all clients.
// -> notifications need to be dispatched to wherever
// -> requests need to generate a reply and travel back to the same lsp!
#[cfg(test)]
mod tests {
use super::{lsp, util::*, OffsetEncoding};
use helix_core::Rope;
#[test]
fn converts_lsp_pos_to_pos() {
macro_rules! test_case {
($doc:expr, ($x:expr, $y:expr) => $want:expr) => {
let doc = Rope::from($doc);
let pos = lsp::Position::new($x, $y);
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf16));
assert_eq!($want, lsp_pos_to_pos(&doc, pos, OffsetEncoding::Utf8))
};
}
test_case!("", (0, 0) => Some(0));
test_case!("", (0, 1) => None);
test_case!("", (1, 0) => None);
test_case!("\n\n", (0, 0) => Some(0));
test_case!("\n\n", (1, 0) => Some(1));
test_case!("\n\n", (1, 1) => Some(2));
test_case!("\n\n", (2, 0) => Some(2));
test_case!("\n\n", (3, 0) => None);
test_case!("test\n\n\n\ncase", (4, 3) => Some(11));
test_case!("test\n\n\n\ncase", (4, 4) => Some(12));
test_case!("test\n\n\n\ncase", (4, 5) => None);
test_case!("", (u32::MAX, u32::MAX) => None);
}
}

View File

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

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-syntax"
version = "0.1.0"
version = "0.2.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"

View File

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

View File

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

View File

@@ -1,12 +1,17 @@
[package]
name = "helix-term"
version = "0.1.0"
version = "0.2.0"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[package.metadata.nix]
build = true
app = true
[features]
embed_runtime = ["helix-core/embed_runtime"]
[[bin]]
name = "hx"
@@ -18,13 +23,12 @@ helix-view = { path = "../helix-view", features = ["term"]}
helix-lsp = { path = "../helix-lsp"}
anyhow = "1"
once_cell = "1.4"
once_cell = "1.8"
tokio = { version = "1", features = ["full"] }
num_cpus = "1"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
crossterm = { version = "0.19", features = ["event-stream"] }
pico-args = "0.4"
crossterm = { version = "0.20", features = ["event-stream"] }
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }

View File

@@ -1,6 +1,7 @@
use helix_lsp::lsp;
use helix_view::{document::Mode, Document, Editor, Theme, View};
use crate::{compositor::Compositor, ui, Args};
use crate::{args::Args, compositor::Compositor, ui};
use log::{error, info};
@@ -45,16 +46,28 @@ impl Application {
let size = compositor.size();
let mut editor = Editor::new(size);
compositor.push(Box::new(ui::EditorView::new()));
if !args.files.is_empty() {
for file in args.files {
editor.open(file, Action::VerticalSplit)?;
let first = &args.files[0]; // we know it's not empty
if first.is_dir() {
editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(first.clone())));
} else {
for file in args.files {
if file.is_dir() {
return Err(anyhow::anyhow!(
"expected a path to file, found a directory. (to open a directory pass it as first argument)"
));
} else {
editor.open(file, Action::VerticalSplit)?;
}
}
}
} else {
editor.new_file(Action::VerticalSplit);
}
compositor.push(Box::new(ui::EditorView::new()));
let mut app = Self {
compositor,
editor,
@@ -165,7 +178,7 @@ impl Application {
let diagnostics = params
.diagnostics
.into_iter()
.map(|diagnostic| {
.filter_map(|diagnostic| {
use helix_core::{
diagnostic::{Range, Severity, Severity::*},
Diagnostic,
@@ -176,18 +189,29 @@ impl Application {
let language_server = doc.language_server().unwrap();
// TODO: convert inside server
let start = lsp_pos_to_pos(
let start = if let Some(start) = lsp_pos_to_pos(
text,
diagnostic.range.start,
language_server.offset_encoding(),
);
let end = lsp_pos_to_pos(
) {
start
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
let end = if let Some(end) = lsp_pos_to_pos(
text,
diagnostic.range.end,
language_server.offset_encoding(),
);
) {
end
} else {
log::warn!("lsp position out of bounds - {:?}", diagnostic);
return None;
};
Diagnostic {
Some(Diagnostic {
range: Range { start, end },
line: diagnostic.range.start.line as usize,
message: diagnostic.message,
@@ -201,11 +225,11 @@ impl Application {
),
// code
// source
}
})
})
.collect();
doc.diagnostics = diagnostics;
doc.set_diagnostics(diagnostics);
// TODO: we want to process all the events in queue, then render. publishDiagnostic tends to send a whole bunch of events
self.render();
}
@@ -216,6 +240,59 @@ impl Application {
Notification::LogMessage(params) => {
log::warn!("unhandled window/logMessage: {:?}", params);
}
Notification::ProgressMessage(params) => {
let token = match params.token {
lsp::NumberOrString::Number(n) => n.to_string(),
lsp::NumberOrString::String(s) => s,
};
let msg = {
let lsp::ProgressParamsValue::WorkDone(work) = params.value;
let parts = match work {
lsp::WorkDoneProgress::Begin(lsp::WorkDoneProgressBegin {
title,
message,
percentage,
..
}) => (Some(title), message, percentage.map(|n| n.to_string())),
lsp::WorkDoneProgress::Report(lsp::WorkDoneProgressReport {
message,
percentage,
..
}) => (None, message, percentage.map(|n| n.to_string())),
lsp::WorkDoneProgress::End(lsp::WorkDoneProgressEnd {
message,
}) => {
if let Some(message) = message {
(None, Some(message), None)
} else {
self.editor.clear_status();
return;
}
}
};
match parts {
(Some(title), Some(message), Some(percentage)) => {
format!("{}% {} - {}", percentage, title, message)
}
(Some(title), None, Some(percentage)) => {
format!("{}% {}", percentage, title)
}
(Some(title), Some(message), None) => {
format!("{} - {}", title, message)
}
(None, Some(message), Some(percentage)) => {
format!("{}% {}", percentage, message)
}
(Some(title), None, None) => title,
(None, Some(message), None) => message,
(None, None, Some(percentage)) => format!("{}%", percentage),
(None, None, None) => "".into(),
}
};
let status = format!("[{}] {}", token, msg);
self.editor.set_status(status);
self.render();
}
_ => unreachable!(),
}
}

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

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

File diff suppressed because it is too large Load Diff

View File

@@ -122,9 +122,17 @@ impl Compositor {
}
pub fn render(&mut self, cx: &mut Context) {
let area = self.size();
let area = self
.terminal
.autoresize()
.expect("Unable to determine terminal size");
// TODO: need to recalculate view tree if necessary
let surface = self.terminal.current_buffer_mut();
let area = *surface.area();
for layer in &self.layers {
layer.render(area, surface, cx)
}
@@ -174,10 +182,8 @@ 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();
/// // let boxed: Box<Component> = Box::new(TextComponent::new("text"));
/// // let text: Box<TextComponent> = boxed.as_boxed_any().downcast().unwrap();
/// ```
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
}

View File

@@ -11,7 +11,8 @@ use std::collections::HashMap;
// W = next WORD
// e = end of word
// E = end of WORD
// r =
// r = replace
// R = replace with yanked
// t = 'till char
// y = yank
// u = undo
@@ -35,10 +36,10 @@ use std::collections::HashMap;
// f = find_char()
// g = goto (gg, G, gc, gd, etc)
//
// h = move_char_left(n)
// j = move_line_down(n)
// k = move_line_up(n)
// l = move_char_right(n)
// h = move_char_left(n) || arrow-left = move_char_left(n)
// j = move_line_down(n) || arrow-down = move_line_down(n)
// k = move_line_up(n) || arrow_up = move_line_up(n)
// l = move_char_right(n) || arrow-right = move_char_right(n)
// : = command line
// ; = collapse selection to cursor
// " = use register
@@ -61,8 +62,8 @@ use std::collections::HashMap;
// in kakoune these are alt-h alt-l / gh gl
// select from curs to begin end / move curs to begin end
// 0 = start of line
// ^ = start of line (first non blank char)
// $ = end of line
// ^ = start of line(first non blank char) || Home = start of line(first non blank char)
// $ = end of line || End = end of line
//
// z = save selections
// Z = restore selections
@@ -85,6 +86,10 @@ use std::collections::HashMap;
//
// gd = goto definition
// gr = goto reference
// [d = previous diagnostic
// d] = next diagnostic
// [D = first diagnostic
// D] = last diagnostic
// }
// #[cfg(feature = "term")]
@@ -95,6 +100,12 @@ pub type Keymaps = HashMap<Mode, Keymap>;
#[macro_export]
macro_rules! key {
($key:ident) => {
KeyEvent {
code: KeyCode::$key,
modifiers: KeyModifiers::NONE,
}
};
($($ch:tt)*) => {
KeyEvent {
code: KeyCode::Char($($ch)*),
@@ -103,15 +114,6 @@ macro_rules! key {
};
}
macro_rules! shift {
($($ch:tt)*) => {
KeyEvent {
code: KeyCode::Char($($ch)*),
modifiers: KeyModifiers::SHIFT,
}
};
}
macro_rules! ctrl {
($($ch:tt)*) => {
KeyEvent {
@@ -137,16 +139,22 @@ pub fn default() -> Keymaps {
key!('k') => commands::move_line_up,
key!('l') => commands::move_char_right,
key!(Left) => commands::move_char_left,
key!(Down) => commands::move_line_down,
key!(Up) => commands::move_line_up,
key!(Right) => commands::move_char_right,
key!('t') => commands::find_till_char,
key!('f') => commands::find_next_char,
shift!('T') => commands::till_prev_char,
shift!('F') => commands::find_prev_char,
key!('T') => commands::till_prev_char,
key!('F') => commands::find_prev_char,
// and matching set for select mode (extend)
//
key!('r') => commands::replace,
key!('R') => commands::replace_with_yanked,
key!('^') => commands::move_line_start,
key!('$') => commands::move_line_end,
key!(Home) => commands::move_line_start,
key!(End) => commands::move_line_end,
key!('w') => commands::move_next_word_start,
key!('b') => commands::move_prev_word_start,
@@ -157,11 +165,11 @@ pub fn default() -> Keymaps {
key!(':') => commands::command_mode,
key!('i') => commands::insert_mode,
shift!('I') => commands::prepend_to_line,
key!('I') => commands::prepend_to_line,
key!('a') => commands::append_mode,
shift!('A') => commands::append_to_line,
key!('A') => commands::append_to_line,
key!('o') => commands::open_below,
shift!('O') => commands::open_above,
key!('O') => commands::open_above,
// [<space> ]<space> equivalents too (add blank new line, no edit)
@@ -174,12 +182,12 @@ pub fn default() -> Keymaps {
key!('s') => commands::select_regex,
alt!('s') => commands::split_selection_on_newline,
shift!('S') => commands::split_selection,
key!('S') => commands::split_selection,
key!(';') => commands::collapse_selection,
alt!(';') => commands::flip_selections,
key!('%') => commands::select_all,
key!('x') => commands::select_line,
shift!('X') => commands::extend_line,
key!('X') => commands::extend_line,
// or select mode X?
// extend_to_whole_line, crop_to_whole_line
@@ -194,30 +202,32 @@ pub fn default() -> Keymaps {
// repeat_select
// TODO: figure out what key to use
key!('[') => commands::expand_selection,
// key!('[') => commands::expand_selection, ??
key!('[') => commands::left_bracket_mode,
key!(']') => commands::right_bracket_mode,
key!('/') => commands::search,
// ? for search_reverse
key!('n') => commands::search_next,
shift!('N') => commands::extend_search_next,
key!('N') => commands::extend_search_next,
// N for search_prev
key!('*') => commands::search_selection,
key!('u') => commands::undo,
shift!('U') => commands::redo,
key!('U') => commands::redo,
key!('y') => commands::yank,
// yank_all
key!('p') => commands::paste_after,
// paste_all
shift!('P') => commands::paste_before,
key!('P') => commands::paste_before,
key!('>') => commands::indent,
key!('<') => commands::unindent,
key!('=') => commands::format_selections,
shift!('J') => commands::join_selections,
key!('J') => commands::join_selections,
// TODO: conflicts hover/doc
shift!('K') => commands::keep_selections,
key!('K') => commands::keep_selections,
// TODO: and another method for inverse
// TODO: clashes with space mode
@@ -232,38 +242,31 @@ pub fn default() -> Keymaps {
// C / altC = copy (repeat) selections on prev/next lines
KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE
} => commands::normal_mode,
KeyEvent {
code: KeyCode::PageUp,
modifiers: KeyModifiers::NONE
} => commands::page_up,
KeyEvent {
code: KeyCode::PageDown,
modifiers: KeyModifiers::NONE
} => commands::page_down,
key!(Esc) => commands::normal_mode,
key!(PageUp) => commands::page_up,
key!(PageDown) => commands::page_down,
ctrl!('b') => commands::page_up,
ctrl!('f') => commands::page_down,
ctrl!('u') => commands::half_page_up,
ctrl!('d') => commands::half_page_down,
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE
} => commands::next_view,
ctrl!('w') => commands::window_mode,
// move under <space>c
ctrl!('c') => commands::toggle_comments,
shift!('K') => commands::hover,
key!('K') => commands::hover,
// z family for save/restore/combine from/to sels from register
ctrl!('i') => commands::jump_forward, // TODO: ctrl-i conflicts tab
// supposedly ctrl!('i') but did not work
key!(Tab) => commands::jump_forward,
ctrl!('o') => commands::jump_backward,
// ctrl!('s') => commands::save_selection,
key!(' ') => commands::space_mode,
key!('z') => commands::view_mode,
key!('"') => commands::select_register,
);
// TODO: decide whether we want normal mode to also be select mode (kakoune-like), or whether
// we keep this separate select mode. More keys can fit into normal mode then, but it's weird
@@ -276,19 +279,23 @@ pub fn default() -> Keymaps {
key!('k') => commands::extend_line_up,
key!('l') => commands::extend_char_right,
key!(Left) => commands::extend_char_left,
key!(Down) => commands::extend_line_down,
key!(Up) => commands::extend_line_up,
key!(Right) => commands::extend_char_right,
key!('w') => commands::extend_next_word_start,
key!('b') => commands::extend_prev_word_start,
key!('e') => commands::extend_next_word_end,
key!('t') => commands::extend_till_char,
key!('f') => commands::extend_next_char,
shift!('T') => commands::extend_till_prev_char,
shift!('F') => commands::extend_prev_char,
KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE
} => commands::exit_select_mode as Command,
key!('T') => commands::extend_till_prev_char,
key!('F') => commands::extend_prev_char,
key!(Home) => commands::extend_line_start,
key!(End) => commands::extend_line_end,
key!(Esc) => commands::exit_select_mode,
)
.into_iter(),
);
@@ -299,28 +306,14 @@ pub fn default() -> Keymaps {
Mode::Normal => normal,
Mode::Select => select,
Mode::Insert => hashmap!(
KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE
} => commands::normal_mode as Command,
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE
} => commands::insert::delete_char_backward,
KeyEvent {
code: KeyCode::Delete,
modifiers: KeyModifiers::NONE
} => commands::insert::delete_char_forward,
KeyEvent {
code: KeyCode::Enter,
modifiers: KeyModifiers::NONE
} => commands::insert::insert_newline,
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE
} => commands::insert::insert_tab,
key!(Esc) => commands::normal_mode as Command,
key!(Backspace) => commands::insert::delete_char_backward,
key!(Delete) => commands::insert::delete_char_forward,
key!(Enter) => commands::insert::insert_newline,
key!(Tab) => commands::insert::insert_tab,
ctrl!('x') => commands::completion,
ctrl!('w') => commands::insert::delete_word_backward,
),
)
}

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

@@ -0,0 +1,8 @@
#![allow(unused)]
pub mod application;
pub mod args;
pub mod commands;
pub mod compositor;
pub mod keymap;
pub mod ui;

View File

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

View File

@@ -108,20 +108,6 @@ impl Completion {
let item = item.unwrap();
use helix_lsp::{lsp, util};
// determine what to insert: text_edit | insert_text | label
let edit = if let Some(edit) = &item.text_edit {
match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
}
} else {
item.insert_text.as_ref().unwrap_or(&item.label);
unimplemented!();
// lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
// and we insert at position.
};
// if more text was entered, remove it
let cursor = doc.selection(view.id).cursor();
@@ -134,11 +120,27 @@ impl Completion {
}
use helix_lsp::OffsetEncoding;
let transaction = util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
);
let transaction = if let Some(edit) = &item.text_edit {
let edit = match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
};
util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
)
} else {
let text = item.insert_text.as_ref().unwrap_or(&item.label);
let cursor = doc.selection(view.id).cursor();
Transaction::change(
doc.text(),
vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
)
};
doc.apply(&transaction, view.id);
// TODO: merge edit with additional_text_edits

View File

@@ -34,6 +34,12 @@ pub struct EditorView {
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
impl Default for EditorView {
fn default() -> Self {
Self::new()
}
}
impl EditorView {
pub fn new() -> Self {
Self {
@@ -195,7 +201,7 @@ impl EditorView {
}
// ugh,interleave highlight spans with diagnostic spans
let is_diagnostic = doc.diagnostics.iter().any(|diagnostic| {
let is_diagnostic = doc.diagnostics().iter().any(|diagnostic| {
diagnostic.range.start <= char_index
&& diagnostic.range.end > char_index
});
@@ -234,8 +240,7 @@ impl EditorView {
// .bg(Color::Rgb(255, 255, 255))
.add_modifier(Modifier::REVERSED);
// let selection_style = Style::default().bg(Color::Rgb(94, 0, 128));
let selection_style = Style::default().bg(Color::Rgb(84, 0, 153));
let selection_style = theme.get("ui.selection");
for selection in doc
.selection(view.id)
@@ -261,7 +266,16 @@ impl EditorView {
Rect::new(
viewport.x + start.col as u16,
viewport.y + start.row as u16,
(end.col - start.col) as u16 + 1,
// .min is important, because set_style does a
// for i in area.left()..area.right() and
// area.right = x + width !!! which shouldn't be > then surface.area.right()
// This is checked by a debug_assert! in Buffer::index_of
((end.col - start.col) as u16 + 1).min(
surface
.area
.width
.saturating_sub(viewport.x + start.col as u16),
),
1,
),
selection_style,
@@ -272,7 +286,7 @@ impl EditorView {
viewport.x + start.col as u16,
viewport.y + start.row as u16,
// text.line(view.first_line).len_chars() as u16 - start.col as u16,
viewport.width - start.col as u16,
viewport.width.saturating_sub(start.col as u16),
1,
),
selection_style,
@@ -290,7 +304,12 @@ impl EditorView {
);
}
surface.set_style(
Rect::new(viewport.x, viewport.y + end.row as u16, end.col as u16, 1),
Rect::new(
viewport.x,
viewport.y + end.row as u16,
(end.col as u16).min(viewport.width),
1,
),
selection_style,
);
}
@@ -306,6 +325,31 @@ impl EditorView {
),
cursor_style,
);
// TODO: set cursor position for IME
if let Some(syntax) = doc.syntax() {
use helix_core::match_brackets;
let pos = doc.selection(view.id).cursor();
let pos = match_brackets::find(syntax, doc.text(), pos);
if let Some(pos) = pos {
let pos = view.screen_coords_at_pos(doc, text, pos);
if let Some(pos) = pos {
if (pos.col as u16) < viewport.width + view.first_col as u16
&& pos.col >= view.first_col
{
let style = Style::default()
.add_modifier(Modifier::REVERSED)
.add_modifier(Modifier::DIM);
surface
.get_mut(
viewport.x + pos.col as u16,
viewport.y + pos.row as u16,
)
.set_style(style);
}
}
}
}
}
}
}
@@ -320,7 +364,7 @@ impl EditorView {
for (i, line) in (view.first_line..last_line).enumerate() {
use helix_core::diagnostic::Severity;
if let Some(diagnostic) = doc.diagnostics.iter().find(|d| d.line == line) {
if let Some(diagnostic) = doc.diagnostics().iter().find(|d| d.line == line) {
surface.set_stringn(
viewport.x - OFFSET,
viewport.y + i as u16,
@@ -364,7 +408,7 @@ impl EditorView {
let cursor = doc.selection(view.id).cursor();
let line = doc.text().char_to_line(cursor);
let diagnostics = doc.diagnostics.iter().filter(|diagnostic| {
let diagnostics = doc.diagnostics().iter().filter(|diagnostic| {
diagnostic.range.start <= cursor && diagnostic.range.end >= cursor
});
@@ -446,7 +490,7 @@ impl EditorView {
surface.set_stringn(
viewport.x + viewport.width.saturating_sub(15),
viewport.y,
format!("{}", doc.diagnostics.len()),
format!("{}", doc.diagnostics().len()),
4,
text_color,
);
@@ -482,7 +526,8 @@ impl EditorView {
// count handling
key!(i @ '0'..='9') => {
let i = i.to_digit(10).unwrap() as usize;
cxt.editor.count = Some(cxt.editor.count.map_or(i, |c| c * 10 + i));
cxt.editor.count =
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
}
// special handling for repeat operator
key!('.') => {
@@ -495,11 +540,14 @@ impl EditorView {
}
_ => {
// set the count
cxt.count = cxt.editor.count.take().unwrap_or(1);
cxt._count = cxt.editor.count.take();
// TODO: edge case: 0j -> reset to 1
// if this fails, count was Some(0)
// debug_assert!(cxt.count != 0);
// set the register
cxt.register = cxt.editor.register.take();
if let Some(command) = self.keymap[&mode].get(&event) {
command(cxt);
}
@@ -529,7 +577,8 @@ impl Component for EditorView {
cx.editor.resize(Rect::new(0, 0, width, height - 1));
EventResult::Consumed(None)
}
Event::Key(key) => {
Event::Key(mut key) => {
canonicalize_key(&mut key);
// clear status
cx.editor.status_msg = None;
@@ -537,11 +586,12 @@ impl Component for EditorView {
let mode = doc.mode();
let mut cxt = commands::Context {
register: helix_view::RegisterSelection::default(),
editor: &mut cx.editor,
count: 1,
_count: None,
callback: None,
callbacks: cx.callbacks,
on_next_key_callback: None,
callbacks: cx.callbacks,
};
if let Some(on_next_key) = self.on_next_key.take() {
@@ -633,6 +683,10 @@ impl Component for EditorView {
// clear with background color
surface.set_style(area, cx.editor.theme.get("ui.background"));
// if the terminal size suddenly changed, we need to trigger a resize
cx.editor
.resize(Rect::new(area.x, area.y, area.width, area.height - 1)); // - 1 to account for commandline
for (view, is_focused) in cx.editor.tree.views() {
let doc = cx.editor.document(view.doc).unwrap();
self.render_view(doc, view, area, surface, &cx.editor.theme, is_focused);
@@ -672,3 +726,13 @@ impl Component for EditorView {
None
}
}
fn canonicalize_key(key: &mut KeyEvent) {
if let KeyEvent {
code: KeyCode::Char(_),
modifiers: _,
} = key
{
key.modifiers.remove(KeyModifiers::SHIFT)
}
}

View File

@@ -166,8 +166,8 @@ impl<T: Item + 'static> Component for Menu<T> {
}
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
KeyEvent {
code: KeyCode::Tab,
modifiers: KeyModifiers::SHIFT,
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Up, ..

View File

@@ -85,10 +85,15 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
Err(_err) => None,
});
const MAX: usize = 2048;
let files = if root.join(".git").is_dir() {
files.collect()
} else {
const MAX: usize = 8192;
files.take(MAX).collect()
};
Picker::new(
files.take(MAX).collect(),
files,
move |path: &PathBuf| {
// format_fn
path.strip_prefix(&root)

View File

@@ -100,8 +100,11 @@ impl<T> Picker<T> {
}
pub fn move_down(&mut self) {
// TODO: len - 1
if self.cursor < self.options.len() {
if self.matches.is_empty() {
return;
}
if self.cursor < self.matches.len() - 1 {
self.cursor += 1;
}
}
@@ -148,7 +151,11 @@ impl<T: 'static> Component for Picker<T> {
code: KeyCode::Up, ..
}
| KeyEvent {
code: KeyCode::Char('k'),
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => self.move_up(),
KeyEvent {
@@ -156,11 +163,18 @@ impl<T: 'static> Component for Picker<T> {
..
}
| KeyEvent {
code: KeyCode::Char('j'),
code: KeyCode::Tab, ..
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => self.move_down(),
KeyEvent {
code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => {
return close_fn;
}
@@ -174,7 +188,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn;
}
KeyEvent {
code: KeyCode::Char('x'),
code: KeyCode::Char('h'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() {
@@ -243,13 +257,14 @@ impl<T: 'static> Component for Picker<T> {
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
let rows = inner.height - 2; // -1 for search bar
let offset = self.cursor / (rows as usize) * (rows as usize);
let files = self.matches.iter().map(|(index, _score)| {
let files = self.matches.iter().skip(offset).map(|(index, _score)| {
(index, self.options.get(*index).unwrap()) // get_unchecked
});
for (i, (_index, option)) in files.take(rows as usize).enumerate() {
if i == self.cursor {
if i == (self.cursor - offset) {
surface.set_string(inner.x + 1, inner.y + 2 + i as u16, ">", selected);
}
@@ -258,7 +273,11 @@ impl<T: 'static> Component for Picker<T> {
inner.y + 2 + i as u16,
(self.format_fn)(option),
inner.width as usize - 1,
if i == self.cursor { selected } else { style },
if i == (self.cursor - offset) {
selected
} else {
style
},
);
}
}

View File

@@ -125,13 +125,13 @@ impl<T: Component> Component for Popup<T> {
let mut rel_x = position.col as u16;
let mut rel_y = position.row as u16;
if viewport.width <= rel_x + width {
rel_x -= ((rel_x + width) - viewport.width)
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
};
// TODO: be able to specify orientation preference. We want above for most popups, below
// for menus/autocomplete.
if height <= rel_y {
rel_y -= height // position above point
rel_y = rel_y.saturating_sub(height) // position above point
} else {
rel_y += 1 // position below point
}

View File

@@ -18,7 +18,7 @@ pub struct Prompt {
pub doc_fn: Box<dyn Fn(&str) -> Option<&'static str>>,
}
#[derive(PartialEq)]
#[derive(Clone, Copy, PartialEq)]
pub enum PromptEvent {
/// The prompt input has been updated.
Update,
@@ -28,6 +28,11 @@ pub enum PromptEvent {
Abort,
}
pub enum CompletionDirection {
Forward,
Backward,
}
impl Prompt {
pub fn new(
prompt: String,
@@ -80,11 +85,39 @@ impl Prompt {
self.exit_selection();
}
pub fn change_completion_selection(&mut self) {
pub fn delete_word_backwards(&mut self) {
use helix_core::get_general_category;
let mut chars = self.line.char_indices().rev();
// TODO add skipping whitespace logic here
let (mut i, cat) = match chars.next() {
Some((i, c)) => (i, get_general_category(c)),
None => return,
};
self.cursor -= 1;
for (nn, nc) in chars {
if get_general_category(nc) != cat {
break;
}
i = nn;
self.cursor -= 1;
}
self.line.drain(i..);
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
}
pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
if self.completion.is_empty() {
return;
}
let index = self.selection.map_or(0, |i| i + 1) % self.completion.len();
let index = match direction {
CompletionDirection::Forward => self.selection.map_or(0, |i| i + 1),
CompletionDirection::Backward => {
self.selection.unwrap_or(0) + self.completion.len() - 1
}
} % self.completion.len();
self.selection = Some(index);
let (range, item) = &self.completion[index];
@@ -92,8 +125,8 @@ impl Prompt {
self.line.replace_range(range.clone(), item);
self.move_end();
// TODO: recalculate completion when completion item is accepted, (Enter)
}
pub fn exit_selection(&mut self) {
self.selection = None;
}
@@ -114,8 +147,21 @@ impl Prompt {
let selected_color = theme.get("ui.menu.selected");
// completion
let max_col = area.width / BASE_WIDTH;
let height = ((self.completion.len() as u16 + max_col - 1) / max_col);
let max_len = self
.completion
.iter()
.map(|(_, completion)| completion.len() as u16)
.max()
.unwrap_or(BASE_WIDTH)
.max(BASE_WIDTH);
let cols = std::cmp::max(1, area.width / max_len);
let col_width = (area.width - (cols)) / cols;
let height = ((self.completion.len() as u16 + cols - 1) / cols)
.min(10) // at most 10 rows (or less)
.min(area.height);
let completion_area = Rect::new(
area.x,
(area.height - height).saturating_sub(1),
@@ -132,7 +178,13 @@ impl Prompt {
let mut row = 0;
let mut col = 0;
for (i, (_range, completion)) in self.completion.iter().enumerate() {
// TODO: paginate
for (i, (_range, completion)) in self
.completion
.iter()
.enumerate()
.take(height as usize * cols as usize)
{
let color = if Some(i) == self.selection {
// Style::default().bg(Color::Rgb(104, 60, 232))
selected_color // TODO: just invert bg
@@ -140,10 +192,10 @@ impl Prompt {
text_color
};
surface.set_stringn(
area.x + 1 + col * BASE_WIDTH,
area.x + col * (1 + col_width),
area.y + row,
&completion,
BASE_WIDTH as usize - 1,
col_width.saturating_sub(1) as usize,
color,
);
row += 1;
@@ -151,16 +203,19 @@ impl Prompt {
row = 0;
col += 1;
}
if col > max_col {
break;
}
}
}
if let Some(doc) = (self.doc_fn)(&self.line) {
let text = ui::Text::new(doc.to_string());
let area = Rect::new(completion_area.x, completion_area.y - 3, BASE_WIDTH * 3, 3);
let viewport = area;
let area = viewport.intersection(Rect::new(
completion_area.x,
completion_area.y - 3,
BASE_WIDTH * 3,
3,
));
let background = theme.get("ui.help");
surface.clear_with(area, background);
@@ -236,6 +291,10 @@ impl Component for Prompt {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
} => self.move_start(),
KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::CONTROL,
} => self.delete_word_backwards(),
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE,
@@ -247,12 +306,21 @@ impl Component for Prompt {
code: KeyCode::Enter,
..
} => {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
return close_fn;
if self.line.ends_with('/') {
self.completion = (self.completion_fn)(&self.line);
self.exit_selection();
} else {
(self.callback_fn)(cx.editor, &self.line, PromptEvent::Validate);
return close_fn;
}
}
KeyEvent {
code: KeyCode::Tab, ..
} => self.change_completion_selection(),
} => self.change_completion_selection(CompletionDirection::Forward),
KeyEvent {
code: KeyCode::BackTab,
..
} => self.change_completion_selection(CompletionDirection::Backward),
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-tui"
version = "0.1.0"
version = "0.2.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
description = """
A library to build rich terminal user interfaces or dashboards
@@ -16,5 +16,5 @@ bitflags = "1.0"
cassowary = "0.3"
unicode-segmentation = "1.2"
unicode-width = "0.1"
crossterm = { version = "0.19", optional = true }
crossterm = { version = "0.20", optional = true }
serde = { version = "1", "optional" = true, features = ["derive"]}

View File

@@ -137,14 +137,12 @@ where
}
/// Queries the backend for size and resizes if it doesn't match the previous size.
pub fn autoresize(&mut self) -> io::Result<()> {
if self.viewport.resize_behavior == ResizeBehavior::Auto {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
}
pub fn autoresize(&mut self) -> io::Result<Rect> {
let size = self.size()?;
if size != self.viewport.area {
self.resize(size)?;
};
Ok(())
Ok(size)
}
/// Synchronizes terminal size, calls the rendering closure, flushes the current internal state

View File

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

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-view"
version = "0.1.0"
version = "0.2.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
@@ -18,8 +18,8 @@ helix-lsp = { path = "../helix-lsp"}
# Conversion traits
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"], optional = true }
crossterm = { version = "0.19", features = ["event-stream"], optional = true }
once_cell = "1.4"
crossterm = { version = "0.20", features = ["event-stream"], optional = true }
once_cell = "1.8"
url = "2"
tokio = { version = "1", features = ["full"] }
@@ -28,3 +28,4 @@ slotmap = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
log = "~0.4"

View File

@@ -1,18 +1,20 @@
use anyhow::{Context, Error};
use std::cell::Cell;
use std::future::Future;
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use helix_core::{
history::History,
syntax::{LanguageConfiguration, LOADER},
ChangeSet, Diagnostic, History, Rope, Selection, State, Syntax, Transaction,
ChangeSet, Diagnostic, Rope, Selection, State, Syntax, Transaction,
};
use crate::{DocumentId, ViewId};
use std::collections::HashMap;
#[derive(Copy, Clone, PartialEq, Eq, Hash)]
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode {
Normal,
Select,
@@ -40,14 +42,40 @@ pub struct Document {
/// State at last commit. Used for calculating reverts.
old_state: Option<State>,
/// Undo tree.
history: History,
// It can be used as a cell where we will take it out to get some parts of the history and put
// it back as it separated from the edits. We could split out the parts manually but that will
// be more troublesome.
history: Cell<History>,
last_saved_revision: usize,
version: i32, // should be usize?
pub diagnostics: Vec<Diagnostic>,
diagnostics: Vec<Diagnostic>,
language_server: Option<Arc<helix_lsp::Client>>,
}
use std::fmt;
impl fmt::Debug for Document {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("Document")
.field("id", &self.id)
.field("text", &self.text)
.field("selections", &self.selections)
.field("path", &self.path)
.field("mode", &self.mode)
.field("restore_cursor", &self.restore_cursor)
.field("syntax", &self.syntax)
.field("language", &self.language)
.field("changes", &self.changes)
.field("old_state", &self.old_state)
// .field("history", &self.history)
.field("last_saved_revision", &self.last_saved_revision)
.field("version", &self.version)
.field("diagnostics", &self.diagnostics)
// .field("language_server", &self.language_server)
.finish()
}
}
/// Like std::mem::replace() except it allows the replacement value to be mapped from the
/// original value.
fn take_with<T, F>(mut_ref: &mut T, closure: F)
@@ -100,6 +128,14 @@ pub fn normalize_path(path: &Path) -> PathBuf {
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 canonicalize_path(path: &Path) -> std::io::Result<PathBuf> {
std::env::current_dir().map(|current_dir| normalize_path(&current_dir.join(path)))
}
use helix_lsp::lsp;
use url::Url;
@@ -121,33 +157,31 @@ impl Document {
old_state,
diagnostics: Vec::new(),
version: 0,
history: History::default(),
history: Cell::new(History::default()),
last_saved_revision: 0,
language_server: None,
}
}
// TODO: passing scopes here is awkward
// TODO: async fn?
pub fn load(path: PathBuf, scopes: &[String]) -> Result<Self, Error> {
use std::{env, fs::File, io::BufReader};
let _current_dir = env::current_dir()?;
pub fn load(path: PathBuf) -> Result<Self, Error> {
use std::{fs::File, io::BufReader};
let file = File::open(path.clone()).context(format!("unable to open {:?}", path))?;
let doc = Rope::from_reader(BufReader::new(file))?;
// TODO: create if not found
let doc = if !path.exists() {
Rope::from("\n")
} else {
let file = File::open(&path).context(format!("unable to open {:?}", path))?;
let mut doc = Rope::from_reader(BufReader::new(file))?;
// add missing newline at the end of file
if doc.len_bytes() == 0 || doc.byte(doc.len_bytes() - 1) != b'\n' {
doc.insert_char(doc.len_chars(), '\n');
}
doc
};
let mut doc = Self::new(doc);
let language_config = LOADER
.get()
.unwrap()
.language_config_for_file_name(path.as_path());
doc.set_language(language_config, scopes);
// canonicalize path to absolute value
doc.path = Some(std::fs::canonicalize(path)?);
// set the path and try detecting the language
doc.set_path(&path)?;
Ok(doc)
}
@@ -190,10 +224,20 @@ impl Document {
let language_server = self.language_server.clone();
// reset the modified flag
self.last_saved_revision = self.history.current_revision();
let history = self.history.take();
self.last_saved_revision = history.current_revision();
self.history.set(history);
async move {
use tokio::{fs::File, io::AsyncWriteExt};
if let Some(parent) = path.parent() {
// TODO: display a prompt asking the user if the directories should be created
if !parent.exists() {
return Err(Error::msg(
"can't save file, parent directory does not exist",
));
}
}
let mut file = File::create(path).await?;
// write all the rope chunks to file
@@ -212,17 +256,25 @@ impl Document {
}
}
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
// canonicalize path to absolute value
let current_dir = std::env::current_dir()?;
let path = normalize_path(&current_dir.join(path));
if let Some(parent) = path.parent() {
// TODO: return error as necessary
if parent.exists() {
self.path = Some(path);
}
fn detect_language(&mut self) {
if let Some(path) = self.path() {
let loader = LOADER.get().unwrap();
let language_config = loader.language_config_for_file_name(path);
let scopes = loader.scopes();
self.set_language(language_config, scopes);
}
}
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
let path = canonicalize_path(path)?;
// if parent doesn't exist we still want to open the document
// and error out when document is saved
self.path = Some(path);
// try detecting the language based on filepath
self.detect_language();
Ok(())
}
@@ -245,8 +297,10 @@ impl Document {
};
}
pub fn set_language2(&mut self, scope: &str, scopes: &[String]) {
let language_config = LOADER.get().unwrap().language_config_for_scope(scope);
pub fn set_language2(&mut self, scope: &str) {
let loader = LOADER.get().unwrap();
let language_config = loader.language_config_for_scope(scope);
let scopes = loader.scopes();
self.set_language(language_config, scopes);
}
@@ -334,28 +388,48 @@ impl Document {
success
}
pub fn undo(&mut self, view_id: ViewId) -> bool {
if let Some(transaction) = self.history.undo() {
let success = self._apply(&transaction, view_id);
pub fn undo(&mut self, view_id: ViewId) {
let mut history = self.history.take();
let success = if let Some(transaction) = history.undo() {
self._apply(&transaction, view_id)
} else {
false
};
self.history.set(history);
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
return success;
}
false
}
pub fn redo(&mut self, view_id: ViewId) -> bool {
if let Some(transaction) = self.history.redo() {
let success = self._apply(&transaction, view_id);
pub fn redo(&mut self, view_id: ViewId) {
let mut history = self.history.take();
let success = if let Some(transaction) = history.redo() {
self._apply(&transaction, view_id)
} else {
false
};
self.history.set(history);
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
return success;
}
false
}
pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
let txns = self.history.get_mut().earlier(uk);
for txn in txns {
self._apply(&txn, view_id);
}
}
pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
let txns = self.history.get_mut().later(uk);
for txn in txns {
self._apply(&txn, view_id);
}
}
pub fn append_changes_to_history(&mut self, view_id: ViewId) {
@@ -373,7 +447,9 @@ impl Document {
// HAXX: we need to reconstruct the state as it was before the changes..
let old_state = self.old_state.take().expect("no old_state available");
self.history.commit_revision(&transaction, &old_state);
let mut history = self.history.take();
history.commit_revision(&transaction, &old_state);
self.history.set(history);
}
#[inline]
@@ -383,9 +459,10 @@ impl Document {
#[inline]
pub fn is_modified(&self) -> bool {
self.path.is_some()
&& (self.history.current_revision() != self.last_saved_revision
|| !self.changes.is_empty())
let history = self.history.take();
let current_revision = history.current_revision();
self.history.set(history);
current_revision != self.last_saved_revision || !self.changes.is_empty()
}
#[inline]
@@ -480,6 +557,14 @@ impl Document {
pub fn versioned_identifier(&self) -> lsp::VersionedTextDocumentIdentifier {
lsp::VersionedTextDocumentIdentifier::new(self.url().unwrap(), self.version)
}
pub fn diagnostics(&self) -> &[Diagnostic] {
&self.diagnostics
}
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
self.diagnostics = diagnostics;
}
}
#[cfg(test)]

View File

@@ -1,4 +1,4 @@
use crate::{theme::Theme, tree::Tree, Document, DocumentId, View, ViewId};
use crate::{theme::Theme, tree::Tree, Document, DocumentId, RegisterSelection, View, ViewId};
use tui::layout::Rect;
use std::path::PathBuf;
@@ -9,17 +9,19 @@ use anyhow::Error;
pub use helix_core::diagnostic::Severity;
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
pub documents: SlotMap<DocumentId, Document>,
pub count: Option<usize>,
pub count: Option<std::num::NonZeroUsize>,
pub register: RegisterSelection,
pub theme: Theme,
pub language_servers: helix_lsp::Registry,
pub status_msg: Option<(String, Severity)>,
}
#[derive(Copy, Clone)]
#[derive(Debug, Copy, Clone)]
pub enum Action {
Replace,
HorizontalSplit,
@@ -36,6 +38,18 @@ impl Editor {
.unwrap_or(include_bytes!("../../theme.toml"));
let theme: Theme = toml::from_slice(toml).expect("failed to parse theme.toml");
// initialize language registry
use helix_core::syntax::{Loader, LOADER};
// load $HOME/.config/helix/languages.toml, fallback to default config
let config = std::fs::read(helix_core::config_dir().join("languages.toml"));
let toml = config
.as_deref()
.unwrap_or(include_bytes!("../../languages.toml"));
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
LOADER.get_or_init(|| Loader::new(config, theme.scopes().to_vec()));
let language_servers = helix_lsp::Registry::new();
// HAXX: offset the render area height by 1 to account for prompt/commandline
@@ -45,12 +59,17 @@ impl Editor {
tree: Tree::new(area),
documents: SlotMap::with_key(),
count: None,
register: RegisterSelection::default(),
theme,
language_servers,
status_msg: None,
}
}
pub fn clear_status(&mut self) {
self.status_msg = None;
}
pub fn set_status(&mut self, status: String) {
self.status_msg = Some((status, Severity::Info));
}
@@ -69,6 +88,12 @@ impl Editor {
pub fn switch(&mut self, id: DocumentId, action: Action) {
use crate::tree::Layout;
use helix_core::Selection;
if !self.documents.contains_key(id) {
log::error!("cannot switch to document that does not exist (anymore)");
return;
}
match action {
Action::Replace => {
let view = self.view();
@@ -79,6 +104,7 @@ impl Editor {
let view = self.view_mut();
view.jumps.push(jump);
view.last_accessed_doc = Some(view.doc);
view.doc = id;
view.first_line = 0;
@@ -125,7 +151,7 @@ impl Editor {
}
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = std::fs::canonicalize(path)?;
let path = crate::document::canonicalize_path(&path)?;
let id = self
.documents()
@@ -135,13 +161,13 @@ impl Editor {
let id = if let Some(id) = id {
id
} else {
let mut doc = Document::load(path, self.theme.scopes())?;
let mut doc = Document::load(path)?;
// try to find a language server based on the language name
let language_server = doc
.language
.as_ref()
.and_then(|language| self.language_servers.get(language));
.and_then(|language| self.language_servers.get(language).ok());
if let Some(language_server) = language_server {
doc.set_language_server(Some(language_server.clone()));
@@ -182,7 +208,7 @@ impl Editor {
let language_server = doc
.language
.as_ref()
.and_then(|language| language_servers.get(language));
.and_then(|language| language_servers.get(language).ok());
if let Some(language_server) = language_server {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
@@ -194,8 +220,9 @@ impl Editor {
}
pub fn resize(&mut self, area: Rect) {
self.tree.resize(area);
self._refresh();
if self.tree.resize(area) {
self._refresh();
};
}
pub fn focus_next(&mut self) {

View File

@@ -1,5 +1,6 @@
pub mod document;
pub mod editor;
pub mod register_selection;
pub mod theme;
pub mod tree;
pub mod view;
@@ -10,5 +11,6 @@ new_key_type! { pub struct ViewId; }
pub use document::Document;
pub use editor::Editor;
pub use register_selection::RegisterSelection;
pub use theme::Theme;
pub use view::View;

View File

@@ -0,0 +1,48 @@
/// Register selection and configuration
///
/// This is a kind a of specialized `Option<char>` for register selection.
/// Point is to keep whether the register selection has been explicitely
/// set or not while being convenient by knowing the default register name.
#[derive(Debug)]
pub struct RegisterSelection {
selected: char,
default_name: char,
}
impl RegisterSelection {
pub fn new(default_name: char) -> Self {
Self {
selected: default_name,
default_name,
}
}
pub fn select(&mut self, name: char) {
self.selected = name;
}
pub fn take(&mut self) -> Self {
Self {
selected: std::mem::replace(&mut self.selected, self.default_name),
default_name: self.default_name,
}
}
pub fn is_default(&self) -> bool {
self.selected == self.default_name
}
pub fn name(&self) -> char {
self.selected
}
}
impl Default for RegisterSelection {
fn default() -> Self {
let default_name = '"';
Self {
selected: default_name,
default_name,
}
}
}

View File

@@ -1,10 +1,11 @@
use std::collections::HashMap;
use log::warn;
use serde::{Deserialize, Deserializer};
use toml::Value;
#[cfg(feature = "term")]
pub use tui::style::{Color, Style};
pub use tui::style::{Color, Modifier, Style};
// #[derive(Clone, Copy, PartialEq, Eq, Default, Hash)]
// pub struct Color {
@@ -115,6 +116,7 @@ impl<'de> Deserialize<'de> for Theme {
}
fn parse_style(style: &mut Style, value: Value) {
//TODO: alert user of parsing failures
if let Value::Table(entries) = value {
for (name, value) in entries {
match name.as_str() {
@@ -128,6 +130,13 @@ fn parse_style(style: &mut Style, value: Value) {
*style = style.bg(color);
}
}
"modifiers" => {
if let Value::Array(arr) = value {
for modifier in arr.iter().filter_map(parse_modifier) {
*style = style.add_modifier(modifier);
}
}
}
_ => (),
}
}
@@ -157,9 +166,34 @@ fn parse_color(value: Value) -> Option<Color> {
if let Some((red, green, blue)) = hex_string_to_rgb(&s) {
Some(Color::Rgb(red, green, blue))
} else {
warn!("malformed hexcode in theme: {}", s);
None
}
} else {
warn!("unrecognized value in theme: {}", value);
None
}
}
fn parse_modifier(value: &Value) -> Option<Modifier> {
if let Value::String(s) = value {
match s.as_str() {
"bold" => Some(Modifier::BOLD),
"dim" => Some(Modifier::DIM),
"italic" => Some(Modifier::ITALIC),
"underlined" => Some(Modifier::UNDERLINED),
"slow_blink" => Some(Modifier::SLOW_BLINK),
"rapid_blink" => Some(Modifier::RAPID_BLINK),
"reversed" => Some(Modifier::REVERSED),
"hidden" => Some(Modifier::HIDDEN),
"crossed_out" => Some(Modifier::CROSSED_OUT),
_ => {
warn!("unrecognized modifier in theme: {}", s);
None
}
}
} else {
warn!("unrecognized modifier in theme: {}", value);
None
}
}
@@ -177,3 +211,39 @@ impl Theme {
&self.scopes
}
}
#[test]
fn test_parse_style_string() {
let fg = Value::String("#ffffff".to_string());
let mut style = Style::default();
parse_style(&mut style, fg);
assert_eq!(style, Style::default().fg(Color::Rgb(255, 255, 255)));
}
#[test]
fn test_parse_style_table() {
let table = toml::toml! {
"keyword" = {
fg = "#ffffff",
bg = "#000000",
modifiers = ["bold"],
}
};
let mut style = Style::default();
if let Value::Table(entries) = table {
for (_name, value) in entries {
parse_style(&mut style, value);
}
}
assert_eq!(
style,
Style::default()
.fg(Color::Rgb(255, 255, 255))
.bg(Color::Rgb(0, 0, 0))
.add_modifier(Modifier::BOLD)
);
}

View File

@@ -4,6 +4,7 @@ use tui::layout::Rect;
// the dimensions are recomputed on windo resize/tree change.
//
#[derive(Debug)]
pub struct Tree {
root: ViewId,
// (container, index inside the container)
@@ -17,11 +18,13 @@ pub struct Tree {
stack: Vec<(ViewId, Rect)>,
}
#[derive(Debug)]
pub struct Node {
parent: ViewId,
content: Content,
}
#[derive(Debug)]
pub enum Content {
View(Box<View>),
Container(Box<Container>),
@@ -45,13 +48,14 @@ impl Node {
// TODO: screen coord to container + container coordinate helpers
#[derive(PartialEq, Eq)]
#[derive(Debug, PartialEq, Eq)]
pub enum Layout {
Horizontal,
Vertical,
// could explore stacked/tabbed
}
#[derive(Debug)]
pub struct Container {
layout: Layout,
children: Vec<ViewId>,
@@ -293,9 +297,13 @@ impl Tree {
}
}
pub fn resize(&mut self, area: Rect) {
self.area = area;
self.recalculate();
pub fn resize(&mut self, area: Rect) -> bool {
if self.area != area {
self.area = area;
self.recalculate();
return true;
}
false
}
pub fn recalculate(&mut self) {
@@ -428,6 +436,7 @@ impl Tree {
}
}
#[derive(Debug)]
pub struct Traverse<'a> {
tree: &'a Tree,
stack: Vec<ViewId>, // TODO: reuse the one we use on update

View File

@@ -12,6 +12,7 @@ pub const PADDING: usize = 5;
type Jump = (DocumentId, Selection);
#[derive(Debug)]
pub struct JumpList {
jumps: Vec<Jump>,
current: usize,
@@ -37,20 +38,28 @@ impl JumpList {
pub fn forward(&mut self, count: usize) -> Option<&Jump> {
if self.current + count < self.jumps.len() {
self.current += count;
return self.jumps.get(self.current);
self.jumps.get(self.current)
} else {
None
}
None
}
pub fn backward(&mut self, count: usize) -> Option<&Jump> {
if self.current.checked_sub(count).is_some() {
self.current -= count;
return self.jumps.get(self.current);
// Taking view and doc to prevent unnecessary cloning when jump is not required.
pub fn backward(&mut self, view_id: ViewId, doc: &mut Document, count: usize) -> Option<&Jump> {
if let Some(current) = self.current.checked_sub(count) {
if self.current == self.jumps.len() {
let jump = (doc.id(), doc.selection(view_id).clone());
self.push(jump);
}
self.current = current;
self.jumps.get(self.current)
} else {
None
}
None
}
}
#[derive(Debug)]
pub struct View {
pub id: ViewId,
pub doc: DocumentId,
@@ -58,6 +67,8 @@ pub struct View {
pub first_col: usize,
pub area: Rect,
pub jumps: JumpList,
/// the last accessed file before the current one
pub last_accessed_doc: Option<DocumentId>,
}
impl View {
@@ -69,6 +80,7 @@ impl View {
first_col: 0,
area: Rect::default(), // will get calculated upon inserting into tree
jumps: JumpList::new((doc, Selection::point(0))), // TODO: use actual sel
last_accessed_doc: None,
}
}
@@ -143,8 +155,9 @@ impl View {
}
}
let row = line - self.first_line as usize;
let col = col - self.first_col as usize;
// It is possible for underflow to occur if the buffer length is larger than the terminal width.
let row = line.saturating_sub(self.first_line);
let col = col.saturating_sub(self.first_col);
Some(Position::new(row, col))
}

View File

@@ -4,6 +4,7 @@ scope = "source.rust"
injection-regex = "rust"
file-types = ["rs"]
roots = []
auto-format = true
language-server = { command = "rust-analyzer" }
indent = { tab-width = 4, unit = " " }
@@ -17,6 +18,15 @@ roots = []
indent = { tab-width = 2, unit = " " }
[[language]]
name = "elixir"
scope = "source.elixir"
injection-regex = "elixir"
file-types = ["ex", "exs"]
roots = []
indent = { tab-width = 2, unit = " " }
[[language]]
name = "json"
scope = "source.json"
@@ -33,6 +43,7 @@ injection-regex = "c"
file-types = ["c"] # TODO: ["h"]
roots = []
language-server = { command = "clangd" }
indent = { tab-width = 2, unit = " " }
[[language]]
@@ -42,6 +53,7 @@ injection-regex = "cpp"
file-types = ["cc", "cpp", "hpp", "h"]
roots = []
language-server = { command = "clangd" }
indent = { tab-width = 2, unit = " " }
[[language]]
@@ -50,6 +62,7 @@ scope = "source.go"
injection-regex = "go"
file-types = ["go"]
roots = ["Gopkg.toml", "go.mod"]
auto-format = true
language-server = { command = "gopls" }
# TODO: gopls needs utf-8 offsets?
@@ -105,6 +118,15 @@ language-server = { command = "pyls" }
# TODO: pyls needs utf-8 offsets
indent = { tab-width = 2, unit = " " }
[[language]]
name = "nix"
scope = "source.nix"
injection-regex = "nix"
file-types = ["nix"]
roots = []
indent = { tab-width = 2, unit = " " }
[[language]]
name = "ruby"
scope = "source.ruby"
@@ -133,3 +155,12 @@ file-types = ["php"]
roots = []
indent = { tab-width = 2, unit = " " }
# [[language]]
# name = "haskell"
# scope = "source.haskell"
# injection-regex = "haskell"
# file-types = ["hs"]
# roots = []
#
# indent = { tab-width = 2, unit = " " }

View File

@@ -0,0 +1,138 @@
["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
[(true) (false) (nil)] @constant.builtin
(keyword
[(keyword_literal)
":"] @tag)
(keyword
(keyword_string
[(string_start)
(string_content)
(string_end)] @tag))
[(atom_literal)
(atom_start)
(atom_content)
(atom_end)] @tag
[(comment)
(unused_identifier)] @comment
(escape_sequence) @escape
(call function: (function_identifier) @keyword
(#match? @keyword "^(defmodule|defexception|defp|def|with|case|cond|raise|import|require|use|defmacrop|defmacro|defguardp|defguard|defdelegate|defstruct|alias|defimpl|defprotocol|defoverridable|receive|if|for|try|throw|unless|reraise|super|quote|unquote|unquote_splicing)$"))
(call function: (function_identifier) @keyword
[(call
function: (function_identifier) @function
(arguments
[(identifier) @variable.parameter
(_ (identifier) @variable.parameter)
(_ (_ (identifier) @variable.parameter))
(_ (_ (_ (identifier) @variable.parameter)))
(_ (_ (_ (_ (identifier) @variable.parameter))))
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
(binary_op
left:
(call
function: (function_identifier) @function
(arguments
[(identifier) @variable.parameter
(_ (identifier) @variable.parameter)
(_ (_ (identifier) @variable.parameter))
(_ (_ (_ (identifier) @variable.parameter)))
(_ (_ (_ (_ (identifier) @variable.parameter))))
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
operator: "when")
(binary_op
left: (identifier) @variable.parameter
operator: _ @function
right: (identifier) @variable.parameter)]
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
(call (function_identifier) @keyword
[(call
function: (function_identifier) @function)
(identifier) @function
(binary_op
left:
[(call
function: (function_identifier) @function)
(identifier) @function]
operator: "when")]
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
(anonymous_function
(stab_expression
left: (bare_arguments
[(identifier) @variable.parameter
(_ (identifier) @variable.parameter)
(_ (_ (identifier) @variable.parameter))
(_ (_ (_ (identifier) @variable.parameter)))
(_ (_ (_ (_ (identifier) @variable.parameter))))
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))])))
(unary_op
operator: "@"
(call (identifier) @attribute
(heredoc
[(heredoc_start)
(heredoc_content)
(heredoc_end)] @doc))
(#match? @attribute "^(doc|moduledoc)$"))
(module) @type
(unary_op
operator: "@" @attribute
[(call
function: (function_identifier) @attribute)
(identifier) @attribute])
(unary_op
operator: _ @operator)
(binary_op
operator: _ @operator)
(heredoc
[(heredoc_start)
(heredoc_content)
(heredoc_end)] @string)
(string
[(string_start)
(string_content)
(string_end)] @string)
(sigil_start) @string.special
(sigil_content) @string
(sigil_end) @string.special
(interpolation
"#{" @punctuation.special
"}" @punctuation.special)
[
","
"->"
"."
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
"<<"
">>"
] @punctuation.bracket
(special_identifier) @function.special
(ERROR) @warning

View File

@@ -0,0 +1,43 @@
(variable) @variable
(operator) @operator
(exp_name (constructor) @constructor)
(constructor_operator) @operator
(module) @module_name
(type) @type
(type) @class
(constructor) @constructor
(pragma) @pragma
(comment) @comment
(signature name: (variable) @fun_type_name)
(function name: (variable) @fun_name)
(constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class))
(instance (instance_head class: (class_name (type)) @class))
(integer) @literal
(exp_literal (float)) @literal
(char) @literal
(con_unit) @literal
(con_list) @literal
(tycon_arrow) @operator
(where) @keyword
"module" @keyword
"let" @keyword
"in" @keyword
"class" @keyword
"instance" @keyword
"data" @keyword
"newtype" @keyword
"family" @keyword
"type" @keyword
"import" @keyword
"qualified" @keyword
"as" @keyword
"deriving" @keyword
"via" @keyword
"stock" @keyword
"anyclass" @keyword
"do" @keyword
"mdo" @keyword
"rec" @keyword
"(" @paren
")" @paren

View File

@@ -0,0 +1,4 @@
(signature name: (variable)) @local.definition
(function name: (variable)) @local.definition
(pat_name (variable)) @local.definition
(exp_name (variable)) @local.reference

View File

@@ -0,0 +1,87 @@
(comment) @comment
[
"if"
"then"
"else"
"let"
"inherit"
"in"
"rec"
"with"
"assert"
] @keyword
((identifier) @variable.builtin
(#match? @variable.builtin "^(__currentSystem|__currentTime|__nixPath|__nixVersion|__storeDir|builtins|false|null|true)$")
(#is-not? local))
((identifier) @function.builtin
(#match? @function.builtin "^(__add|__addErrorContext|__all|__any|__appendContext|__attrNames|__attrValues|__bitAnd|__bitOr|__bitXor|__catAttrs|__compareVersions|__concatLists|__concatMap|__concatStringsSep|__deepSeq|__div|__elem|__elemAt|__fetchurl|__filter|__filterSource|__findFile|__foldl'|__fromJSON|__functionArgs|__genList|__genericClosure|__getAttr|__getContext|__getEnv|__hasAttr|__hasContext|__hashFile|__hashString|__head|__intersectAttrs|__isAttrs|__isBool|__isFloat|__isFunction|__isInt|__isList|__isPath|__isString|__langVersion|__length|__lessThan|__listToAttrs|__mapAttrs|__match|__mul|__parseDrvName|__partition|__path|__pathExists|__readDir|__readFile|__replaceStrings|__seq|__sort|__split|__splitVersion|__storePath|__stringLength|__sub|__substring|__tail|__toFile|__toJSON|__toPath|__toXML|__trace|__tryEval|__typeOf|__unsafeDiscardOutputDependency|__unsafeDiscardStringContext|__unsafeGetAttrPos|__valueSize|abort|baseNameOf|derivation|derivationStrict|dirOf|fetchGit|fetchMercurial|fetchTarball|fromTOML|import|isNull|map|placeholder|removeAttrs|scopedImport|throw|toString)$")
(#is-not? local))
[
(string)
(indented_string)
] @string
[
(path)
(hpath)
(spath)
] @string.special.path
(uri) @string.special.uri
[
(integer)
(float)
] @number
(interpolation
"${" @punctuation.special
"}" @punctuation.special) @embedded
(escape_sequence) @escape
(function
universal: (identifier) @variable.parameter
)
(formal
name: (identifier) @variable.parameter
"?"? @punctuation.delimiter)
(app
function: [
(identifier) @function
(select
attrpath: (attrpath
attr: (attr_identifier) @function .))])
(unary
operator: _ @operator)
(binary
operator: _ @operator)
(attr_identifier) @property
(inherit attrs: (attrs_inherited (identifier) @property) )
[
";"
"."
","
] @punctuation.delimiter
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
(identifier) @variable

View File

@@ -0,0 +1,9 @@
indent = [
"if",
"let",
"function",
"attrset",
"list",
"indented_string",
"parenthesized"
]

View File

@@ -34,6 +34,9 @@
; Namespaces
(crate) @namespace
(extern_crate_declaration
(crate)
name: (identifier) @namespace)
(scoped_use_list
path: (identifier) @namespace)
(scoped_use_list
@@ -62,11 +65,9 @@
function: (field_expression
field: (field_identifier) @function.method))
; (macro_invocation
; macro: (identifier) @function.macro
; "!" @function.macro)
(macro_invocation
macro: (identifier) @function.macro)
macro: (identifier) @function.macro
"!" @function.macro)
(macro_invocation
macro: (scoped_identifier
(identifier) @function.macro .))
@@ -111,6 +112,7 @@
(lifetime (identifier) @label)
"async" @keyword
"break" @keyword
"const" @keyword
"continue" @keyword
@@ -144,7 +146,7 @@
"use" @keyword
"where" @keyword
"while" @keyword
(mutable_specifier) @keyword
(mutable_specifier) @keyword.mut
(use_list (self) @keyword)
(scoped_use_list (self) @keyword)
(scoped_identifier (self) @keyword)

View File

@@ -1,18 +1,6 @@
{ stdenv, pkgs }:
pkgs.mkShell {
nativeBuildInputs = with pkgs; [
(rust-bin.stable.latest.default.override { extensions = ["rust-src"]; })
lld_10
stdenv.cc.cc.lib
# pkg-config
];
RUSTFLAGS = "-C link-arg=-fuse-ld=lld -C target-cpu=native";
RUST_BACKTRACE = "1";
# https://github.com/rust-lang/rust/issues/55979
LD_LIBRARY_PATH="${stdenv.cc.cc.lib}/lib64:$LD_LIBRARY_PATH";
# HELIX_RUNTIME=./runtime;
HELIX_RUNTIME="/home/speed/src/helix/runtime";
}
# Flake's devShell for non-flake-enabled nix instances
let
src = (builtins.fromJSON (builtins.readFile ./flake.lock)).nodes.flakeCompat.locked;
compat = fetchTarball { url = "https://github.com/edolstra/flake-compat/archive/${src.rev}.tar.gz"; sha256 = src.narHash; };
in
(import compat { src = ./.; }).shellNix.default

View File

@@ -50,6 +50,7 @@
"ui.text" = { fg = "#a4a0e8" } # lavender
"ui.text.focus" = { fg = "#dbbfef"} # lilac
"ui.selection" = { bg = "#540099" }
"ui.menu.selected" = { fg = "#281733", bg = "#ffffff" } # revolver
"warning" = "#ffcd1c"