Compare commits

..

179 Commits

Author SHA1 Message Date
Blaž Hrastnik
59f94d13b8 Disable haskell grammar until build issues are resolved 2021-06-07 10:17:25 +09:00
Blaž Hrastnik
b3eeac7bbf Disable aarch64-macos, it fails to build on macos-latest 2021-06-07 09:50:15 +09:00
Blaž Hrastnik
f48a60b8e2 Release 0.0.10 2021-06-07 09:42:15 +09:00
Blaž Hrastnik
4f561e93b8 View mode: Use saturating_sub when calculating first_col 2021-06-07 09:29:21 +09:00
Blaž Hrastnik
01b1bd15a1 commands: use chars().count() over .len() on strings 2021-06-07 09:26:49 +09:00
Blaž Hrastnik
ff8a031cb2 Add diagnostics keys to keymap.md 2021-06-07 09:24:23 +09:00
Blaž Hrastnik
d9b2f6feac Only test on stable rust
Shorter CI times, and it should be good enough.
2021-06-07 09:20:36 +09:00
Blaž Hrastnik
582f1ee9d8 Add aarch64-macos (M1) to the release build matrix 2021-06-07 09:19:51 +09:00
ahkrr
e2d780f993 fix: 2 panics while setting style + off by 1
The panics would occur because set_style 
would draw outside of the the surface. 
Both occured using `find_prev` or `till_prev`
In my case the first panic! would appear
in a terminal with around 80 columns 
in helix/README.md going to the end of the file
with `geglf(`
the second with `geglfX`
The off by one fix ensures that `find_nth_prev` 
starts at the first character to the left
2021-06-07 09:15:08 +09:00
Ethan Bodzioney
843c2cdebd Install instructions and version number corrections (#148)
* Add MacOS install instructions

* Change version name argument

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

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number

* Fixed version number

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

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

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

* docs: theme documentation

* fixup: parse modifiers with filter_map

* theme: tests for parse_style

* theme: Log invalid cases in theme.toml parse

* docs: theme documentation fixup

* docs: Blaz's theming comments

* docs: Theme doc fixes from pickfire

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

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

* contrib: Ingrid's theme

* docs: Theme subsection fixes

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

* wip

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

* implement extend methods for extend_line_start, extend_line_end

* add home-end mappings to keymaps.md

* add ^-$ extend mappings for extend mode

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

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

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

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

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

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

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

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

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

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

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

View File

@@ -1,11 +1,11 @@
name: Build
on:
pull_request:
push:
branches:
- master
pull_request:
branches:
- master
schedule:
- cron: '00 01 * * *'
jobs:
check:
@@ -25,19 +25,19 @@ jobs:
override: true
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -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,23 +60,23 @@ jobs:
uses: actions-rs/toolchain@v1
with:
profile: minimal
toolchain: stable
toolchain: ${{ matrix.rust }}
override: true
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -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
@@ -104,19 +109,19 @@ jobs:
components: rustfmt, clippy
- name: Cache cargo registry
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v1
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}

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

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

View File

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

4
.gitmodules vendored
View File

@@ -82,3 +82,7 @@
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

154
Cargo.lock generated
View File

@@ -52,9 +52,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]]
name = "cc"
version = "1.0.67"
version = "1.0.68"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e3c69b077ad434294d3ce9f1f6143a2a4b89a8a2d54ef813d85003a4fd1137fd"
checksum = "4a72c244c1ff497a746a7e1fb3d14bd08420ecda70c8f25c7112f2781652d787"
dependencies = [
"jobserver",
]
@@ -77,21 +77,6 @@ dependencies = [
"winapi",
]
[[package]]
name = "clap"
version = "3.0.0-beta.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4bd1061998a501ee7d4b6d449020df3266ca3124b941ec56cf2005c3779ca142"
dependencies = [
"bitflags",
"indexmap",
"lazy_static",
"os_str_bytes",
"textwrap",
"unicode-width",
"vec_map",
]
[[package]]
name = "crossbeam-utils"
version = "0.8.4"
@@ -244,9 +229,9 @@ dependencies = [
[[package]]
name = "getrandom"
version = "0.2.2"
version = "0.2.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c9495705279e7140bf035dde1f6e750c162df8b625267cd52cc44e0b156732c8"
checksum = "7fcd999463524c52659517fe2cea98493cfe485d10565e7b0fb07dbba7ad2753"
dependencies = [
"cfg-if",
"libc",
@@ -272,25 +257,20 @@ dependencies = [
"regex",
]
[[package]]
name = "hashbrown"
version = "0.9.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d7afe4a420e3fe79967a00898cc1f4db7c8a49a9333a29f8a4bd76a253d5cd04"
[[package]]
name = "helix-core"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"anyhow",
"etcetera",
"helix-syntax",
"once_cell",
"regex",
"ropey",
"rust-embed",
"serde",
"smallvec",
"tendril",
"toml",
"tree-sitter",
"unicode-segmentation",
"unicode-width",
@@ -298,7 +278,7 @@ dependencies = [
[[package]]
name = "helix-lsp"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"anyhow",
"futures-executor",
@@ -320,7 +300,7 @@ dependencies = [
[[package]]
name = "helix-syntax"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"cc",
"serde",
@@ -330,11 +310,10 @@ dependencies = [
[[package]]
name = "helix-term"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"anyhow",
"chrono",
"clap",
"crossterm",
"dirs-next",
"fern",
@@ -357,7 +336,7 @@ dependencies = [
[[package]]
name = "helix-tui"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"bitflags",
"cassowary",
@@ -369,13 +348,14 @@ dependencies = [
[[package]]
name = "helix-view"
version = "0.1.0"
version = "0.0.10"
dependencies = [
"anyhow",
"crossterm",
"helix-core",
"helix-lsp",
"helix-tui",
"log",
"once_cell",
"serde",
"slotmap",
@@ -422,16 +402,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "indexmap"
version = "1.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "824845a0bf897a9042383849b02c1bc219c2383772efcd5c6f9766fa4b81aef3"
dependencies = [
"autocfg",
"hashbrown",
]
[[package]]
name = "instant"
version = "0.1.9"
@@ -476,9 +446,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]]
name = "libc"
version = "0.2.94"
version = "0.2.95"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "18794a8ad5b29321f790b55d93dfba91e125cb1a9edbd4f8e3150acc771c1a5e"
checksum = "789da6d93f1b866ffe175afc5322a4d76c038605a1c3319bb57b06967ca98a36"
[[package]]
name = "lock_api"
@@ -500,9 +470,9 @@ dependencies = [
[[package]]
name = "lsp-types"
version = "0.89.0"
version = "0.89.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "07731ecd4ee0654728359a5b95e2a254c857876c04b85225496a35d60345daa7"
checksum = "48b8a871b0a450bcec0e26d74a59583c8173cb9fb7d7f98889e18abb84838e0f"
dependencies = [
"bitflags",
"serde",
@@ -601,12 +571,6 @@ version = "1.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af8b08b04175473088b46763e51ee54da5f9a164bc162f615b91bc179dbf15a3"
[[package]]
name = "os_str_bytes"
version = "2.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "afb2e1c3ee07430c2cf76151675e583e0f19985fa6efae47d6848a3e2c824f85"
[[package]]
name = "parking_lot"
version = "0.11.1"
@@ -658,9 +622,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]]
name = "proc-macro2"
version = "1.0.26"
version = "1.0.27"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a152013215dca273577e18d2bf00fa862b89b24169fb78c4c95aeb07992c9cec"
checksum = "f0d8caf72986c1a598726adc988bb5984792ef84f5ee5aa50209145ee8077038"
dependencies = [
"unicode-xid",
]
@@ -730,6 +694,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"
@@ -753,18 +750,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd"
[[package]]
name = "serde"
version = "1.0.125"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "558dc50e1a5a5fa7112ca2ce4effcb321b0300c0d4ccf0776a9f60cd89031171"
checksum = "ec7505abeacaec74ae4778d9d9328fe5a5d04253220a85c4ee022239fc996d03"
dependencies = [
"serde_derive",
]
[[package]]
name = "serde_derive"
version = "1.0.125"
version = "1.0.126"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b093b7a2bb58203b5da3056c05b4ec1fed827dcfdb37347a8841695263b3d06d"
checksum = "963a7dbc9895aeac7ac90e74f34a5d5261828f79df35cbed41e10189d3804d43"
dependencies = [
"proc-macro2",
"quote",
@@ -856,29 +853,20 @@ dependencies = [
"utf-8",
]
[[package]]
name = "textwrap"
version = "0.12.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "203008d98caf094106cfaba70acfed15e18ed3ddb7d94e49baec153a2b462789"
dependencies = [
"unicode-width",
]
[[package]]
name = "thiserror"
version = "1.0.24"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e0f4a65597094d4483ddaed134f409b2cb7c1beccf25201a9f73c719254fa98e"
checksum = "fa6f76457f59514c7eeb4e59d891395fab0b2fd1d40723ae737d64153392e9c6"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.24"
version = "1.0.25"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7765189610d8241a44529806d6fd1f2e0a08734313a35d5b3a556f92b381f3c0"
checksum = "8a36768c0fbf1bb15eca10defa29526bda730a2376c2ab4393ccfa16fb1a318d"
dependencies = [
"proc-macro2",
"quote",
@@ -920,9 +908,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
version = "1.5.0"
version = "1.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "83f0c8e7c0addab50b663055baf787d0af7f413a46e6e7fb9559a4e4db7137a5"
checksum = "0a38d31d7831c6ed7aad00aa4c12d9375fd225a6dd77da1d25b707346319a975"
dependencies = [
"autocfg",
"bytes",
@@ -940,9 +928,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
version = "1.1.0"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "caf7b11a536f46a809a8a9f0bb4237020f70ecbf115b842360afb127ea2fda57"
checksum = "c49e3df43841dafb86046472506755d8501c5615673955f6aa17181125d13c37"
dependencies = [
"proc-macro2",
"quote",
@@ -951,9 +939,9 @@ dependencies = [
[[package]]
name = "tokio-stream"
version = "0.1.5"
version = "0.1.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e177a5d8c3bf36de9ebe6d58537d8879e964332f93fb3339e43f618c81361af0"
checksum = "f8864d706fdb3cc0843a49647ac892720dac98a6eeb818b77190592cf4994066"
dependencies = [
"futures-core",
"pin-project-lite",
@@ -971,9 +959,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
version = "0.19.3"
version = "0.19.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1f41201fed3db3b520405a9c01c61773a250d4c3f43e9861c14b2bb232c981ab"
checksum = "ad726ec26496bf4c083fff0f43d4eb3a2ad1bba305323af5ff91383c0b6ecac0"
dependencies = [
"cc",
"regex",
@@ -1043,12 +1031,6 @@ version = "0.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "09cc8ee72d2a9becf2f2febe0205bbed8fc6615b7cb429ad062dc7b7ddd036a9"
[[package]]
name = "vec_map"
version = "0.8.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f1bddf1187be692e79c5ffeab891132dfb0f236ed36a43c7ed39f1165ee20191"
[[package]]
name = "version_check"
version = "0.9.3"

View File

@@ -1,7 +1,7 @@
# Helix
[![Build status](https://github.com/helix-editor/helix/workflows/ci/badge.svg)](https://github.com/helix-editor/helix/actions)
[![Build status](https://github.com/helix-editor/helix/actions/workflows/build.yml/badge.svg)](https://github.com/helix-editor/helix/actions)
![Screenshot](./screenshot.png)
@@ -10,7 +10,10 @@ A kakoune / neovim inspired editor, written in Rust.
The editing model is very heavily based on kakoune; during development I found
myself agreeing with most of kakoune's design decisions.
For more information, see the [website](https://helix-editor.com).
For more information, see the [website](https://helix-editor.com) or
[documentation](https://docs.helix-editor.com/).
All shortcuts/keymaps can be found [in the documentation on the website](https://docs.helix-editor.com/keymap.html)
# Features
@@ -24,7 +27,8 @@ It's a terminal-based editor first, but I'd like to explore a custom renderer
# Installation
Note: Only the Rust syntax has indentation definitions at the moment.
Note: Only certain languages have indentation definitions at the moment. Check
`runtime/<lang>/` for `indents.toml`.
We provide packaging for various distributions, but here's a quick method to
build from source.
@@ -41,6 +45,31 @@ 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.
> NOTE: You should set this to <path to repository>/runtime in development (if
> running via cargo).
>
> `export HELIX_RUNTIME=$PWD/runtime`
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.**
@@ -58,5 +87,4 @@ a good overview of the internals.
# Getting help
Discuss the project on the community [Matrix channel](https://matrix.to/#/#helix-editor: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

@@ -1,8 +1,9 @@
- Refactor tree-sitter-highlight to work like the atom one, recomputing partial tree updates.
- syntax errors highlight query
------
as you type completion!
- tree sitter:
- lua
- markdown
@@ -19,15 +20,14 @@
- [ ] document.on_type provider triggers
- [ ] completion isIncomplete support
- [ ] extract indentation calculation queries so we can support other languages.
- [ ] scroll wheel support
- [ ] matching bracket highlight
1
- [ ] :format/:fmt that formats the buffer
- [ ] respect view fullscreen flag
- [ ] Implement marks (superset of Selection/Range)
- [ ] nixos packaging
- [ ] CI binary builds
- [ ] = for auto indent line/selection
- [ ] :x for closing buffers

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,87 @@
# 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 | |
| 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
TODO: AUR
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 |
| 0 | move to the start of the line |
| $ | move to the end of the line |
| 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 |
@@ -86,6 +86,18 @@ in reverse, or searching via smartcase.
| 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
I'm still pondering whether to keep this mode or not. It changes movement
@@ -118,8 +130,13 @@ 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 |
| 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 |
@@ -127,6 +144,17 @@ Jumps to various locations.
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 +163,5 @@ 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 |
| w | Enter window mode |
| space | Keep primary selection TODO: it's here because space mode replaced it |

View File

@@ -109,7 +109,7 @@ h6:target::before {
margin-top: 1.275em;
margin-bottom: .875em;
}
.content p, .content ol, .content ul, .content table, .content blockquote {
.content p, .content ol, .content ul, .content table {
margin-top: 0;
margin-bottom: .875em;
}
@@ -123,8 +123,7 @@ h6:target::before {
.content .header:link,
.content .header:visited {
color: var(--fg);
/* color: white; */
color: #281733;
color: var(--heading-fg);
}
.content .header:link,
.content .header:visited:hover {
@@ -168,12 +167,15 @@ table tbody tr:nth-child(2n) {
blockquote {
margin: 20px 0;
padding: 0 20px;
margin: 1.5rem 0;
padding: 1rem 1.5rem;
color: var(--fg);
opacity: .9;
background-color: var(--quote-bg);
border-top: .1em solid var(--quote-border);
border-bottom: .1em solid var(--quote-border);
border-left: 4px solid var(--quote-border);
}
blockquote *:last-child {
margin-bottom: 0;
}

View File

@@ -13,6 +13,7 @@
.ayu {
--bg: hsl(210, 25%, 8%);
--fg: #c5c5c5;
--heading-fg: #c5c5c5;
--sidebar-bg: #14191f;
--sidebar-fg: #c8c9db;
@@ -53,6 +54,7 @@
.coal {
--bg: hsl(200, 7%, 8%);
--fg: #98a3ad;
--heading-fg: #98a3ad;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
@@ -93,6 +95,7 @@
.light {
--bg: hsl(0, 0%, 100%);
--fg: hsl(0, 0%, 0%);
--heading-fg: hsl(0, 0%, 0%);
--sidebar-bg: #fafafa;
--sidebar-fg: hsl(0, 0%, 0%);
@@ -133,6 +136,7 @@
.navy {
--bg: hsl(226, 23%, 11%);
--fg: #bcbdd0;
--heading-fg: #bcbdd0;
--sidebar-bg: #282d3f;
--sidebar-fg: #c8c9db;
@@ -173,6 +177,7 @@
.rust {
--bg: hsl(60, 9%, 87%);
--fg: #262625;
--heading-fg: #262625;
--sidebar-bg: #3b2e2a;
--sidebar-fg: #c8c9db;
@@ -214,6 +219,7 @@
.light.no-js {
--bg: hsl(200, 7%, 8%);
--fg: #ebeafa;
--heading-fg: #ebeafa;
--sidebar-bg: #292c2f;
--sidebar-fg: #a1adb8;
@@ -297,6 +303,7 @@
--bg: #ffffff;
--fg: #452859;
--fg: #5a5977;
--heading-fg: #281733;
--sidebar-bg: #281733;
--sidebar-fg: #c8c9db;
@@ -317,8 +324,8 @@
--theme-popup-border: #737480;
--theme-hover: rgba(0,0,0, .2);
--quote-bg: hsl(226, 15%, 17%);
--quote-border: hsl(226, 15%, 22%);
--quote-bg: rgba(0, 0, 0, 0);
--quote-border: hsl(226, 15%, 75%);
--table-border-color: #5a5977;
--table-border-color: hsl(201deg 10% 67%);

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,46 @@
# 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.menu.selected" = { fg = "#D74E50", bg = "#F3EAE9" }
"warning" = "#D4A520"
"error" = "#D74E50"
"info" = "#839A53"
"hint" = "#A6B6CE"

View File

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

26
flake.lock generated
View File

@@ -2,11 +2,11 @@
"nodes": {
"flake-utils": {
"locked": {
"lastModified": 1619345332,
"narHash": "sha256-qHnQkEp1uklKTpx3MvKtY6xzgcqXDsz5nLilbbuL+3A=",
"lastModified": 1620759905,
"narHash": "sha256-WiyWawrgmyN0EdmiHyG2V+fqReiVi8bM9cRdMaKQOFg=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "2ebf2558e5bf978c7fb8ea927dfaed8fefab2e28",
"rev": "b543720b25df6ffdfcf9227afafc5b8c1fabfae8",
"type": "github"
},
"original": {
@@ -50,10 +50,10 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1619775165,
"narHash": "sha256-2qaBErjxuWpTIq6Yee5GJmhr84hmzBotLQ0ayg1VXg8=",
"path": "/nix/store/gs997rgx3pvdgcb54wd3fi9wbnznd9g4-source",
"rev": "849b29b4f76d66ec7aeeeed699b7e27ef3db7c02",
"lastModified": 1622059058,
"narHash": "sha256-t1/ZMtyxClVSfcV4Pt5C1YpkeJ/UwFF3oitLD7Ch/UA=",
"path": "/nix/store/2gam4i1fa1v19k3n5rc9vgvqac1c2xj5-source",
"rev": "84aa23742f6c72501f9cc209f29c438766f5352d",
"type": "path"
},
"original": {
@@ -63,11 +63,11 @@
},
"nixpkgs_2": {
"locked": {
"lastModified": 1620252427,
"narHash": "sha256-U1Q5QceuT4chJTJ1UOt7bZOn9Y2o5/7w27RISjqXoQw=",
"lastModified": 1622194753,
"narHash": "sha256-76qtvFp/vFEz46lz5iZMJ0mnsWQYmuGYlb0fHgKqqMg=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "d3ba49889a76539ea0f7d7285b203e7f81326ded",
"rev": "540dccb2aeaffa9dc69bfdc41c55abd7ccc6baa3",
"type": "github"
},
"original": {
@@ -106,11 +106,11 @@
"nixpkgs": "nixpkgs_3"
},
"locked": {
"lastModified": 1620355527,
"narHash": "sha256-mUTnUODiAtxH83gbv7uuvCbqZ/BNkYYk/wa3MkwrskE=",
"lastModified": 1622257069,
"narHash": "sha256-+QVnS/es9JCRZXphoHL0fOIUhpGqB4/wreBsXWArVck=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "d8efe70dc561c4bea0b7bf440d36ce98c497e054",
"rev": "8aa5f93c0b665e5357af19c5631a3450bff4aba5",
"type": "github"
},
"original": {

View File

@@ -1,17 +1,20 @@
[package]
name = "helix-core"
version = "0.1.0"
version = "0.0.10"
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"
anyhow = "1"
smallvec = "1.4"
tendril = "0.4.2"
unicode-segmentation = "1.6"
@@ -22,5 +25,7 @@ once_cell = "1.4"
regex = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
etcetera = "0.3"
rust-embed = { version = "5.9.0", optional = true }

View File

@@ -65,14 +65,20 @@ fn handle_open(
) -> Transaction {
let mut ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);
let head = pos + open.len_utf8();
let head = pos + offs + open.len_utf8();
// if selection, retain anchor, if cursor, move over
ranges.push(Range::new(
if range.is_empty() { head } else { range.anchor },
if range.is_empty() {
head
} else {
range.anchor + offs
},
head,
));
@@ -88,6 +94,8 @@ fn handle_open(
pair.push_char(open);
pair.push_char(close);
offs += 2;
(pos, pos, Some(pair))
}
}
@@ -99,14 +107,20 @@ fn handle_open(
fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction {
let mut ranges = SmallVec::with_capacity(selection.len());
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(doc, selection, |range| {
let pos = range.head;
let next = next_char(doc, pos);
let head = pos + close.len_utf8();
let head = pos + offs + close.len_utf8();
// if selection, retain anchor, if cursor, move over
ranges.push(Range::new(
if range.is_empty() { head } else { range.anchor },
if range.is_empty() {
head
} else {
range.anchor + offs
},
head,
));
@@ -114,6 +128,8 @@ fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) ->
// return transaction that moves past close
(pos, pos, None) // no-op
} else {
offs += close.len_utf8();
// TODO: else return (use default handler that inserts close)
(pos, pos, Some(Tendril::from_char(close)))
}

View File

@@ -65,9 +65,7 @@ impl History {
self.cursor == 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;
@@ -77,17 +75,17 @@ impl History {
self.cursor = current_revision.parent;
Some(current_revision.revert.clone())
Some(&current_revision.revert)
}
pub fn redo(&mut self) -> Option<Transaction> {
pub fn redo(&mut self) -> Option<&Transaction> {
let current_revision = &self.revisions[self.cursor];
// for now, simply pick the latest child (linear undo / redo)
if let Some((index, transaction)) = current_revision.children.last() {
self.cursor = *index;
return Some(transaction.clone());
return Some(&transaction);
}
None
}

View File

@@ -1,6 +1,6 @@
use crate::{
find_first_non_whitespace_char,
syntax::Syntax,
syntax::{IndentQuery, LanguageConfiguration, Syntax},
tree_sitter::{Node, Tree},
Rope, RopeSlice,
};
@@ -43,41 +43,12 @@ fn get_highest_syntax_node_at_bytepos(syntax: &Syntax, pos: usize) -> Option<Nod
Some(node)
}
fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
fn calculate_indentation(query: &IndentQuery, node: Option<Node>, newline: bool) -> usize {
// NOTE: can't use contains() on query because of comparing Vec<String> and &str
// https://doc.rust-lang.org/std/vec/struct.Vec.html#method.contains
let mut increment: i32 = 0;
// Hardcoded for rust for now
let indent_scopes = &[
"while_expression",
"for_expression",
"loop_expression",
"if_expression",
"if_let_expression",
// "match_expression",
// "match_arm",
"tuple_expression",
"array_expression",
// indent_except_first_scopes
"use_list",
"block",
"match_block",
"arguments",
"parameters",
"declaration_list",
"field_declaration_list",
"field_initializer_list",
"struct_pattern",
"tuple_pattern",
"enum_variant_list",
// "function_item",
// "closure_expression",
"binary_expression",
"field_expression",
"where_clause",
];
let outdent = &["where", "}", "]", ")"];
let mut node = match node {
Some(node) => node,
None => return 0,
@@ -88,7 +59,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
// if we're calculating indentation for a brand new line then the current node will become the
// parent node. We need to take it's indentation level into account too.
let node_kind = node.kind();
if newline && indent_scopes.contains(&node_kind) {
if newline && query.indent.contains(node_kind) {
increment += 1;
}
@@ -102,14 +73,14 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
// }) <-- }) is two scopes
let starts_same_line = start == prev_start;
if outdent.contains(&node.kind()) && !starts_same_line {
if query.outdent.contains(node.kind()) && !starts_same_line {
// we outdent by skipping the rules for the current level and jumping up
// node = parent;
increment -= 1;
// continue;
}
if indent_scopes.contains(&parent_kind) // && not_first_or_last_sibling
if query.indent.contains(parent_kind) // && not_first_or_last_sibling
&& !starts_same_line
{
// println!("is_scope {}", parent_kind);
@@ -128,6 +99,7 @@ fn calculate_indentation(node: Option<Node>, newline: bool) -> usize {
}
fn suggested_indent_for_line(
language_config: &LanguageConfiguration,
syntax: Option<&Syntax>,
text: RopeSlice,
line_num: usize,
@@ -137,7 +109,7 @@ fn suggested_indent_for_line(
let current = indent_level_for_line(line, tab_width);
if let Some(start) = find_first_non_whitespace_char(text, line_num) {
return suggested_indent_for_pos(syntax, text, start, false);
return suggested_indent_for_pos(Some(language_config), syntax, text, start, false);
};
// if the line is blank, indent should be zero
@@ -148,18 +120,24 @@ fn suggested_indent_for_line(
// - it should return 0 when mass indenting stuff
// - it should look up the wrapper node and count it too when we press o/O
pub fn suggested_indent_for_pos(
language_config: Option<&LanguageConfiguration>,
syntax: Option<&Syntax>,
text: RopeSlice,
pos: usize,
new_line: bool,
) -> usize {
if let Some(syntax) = syntax {
if let (Some(query), Some(syntax)) = (
language_config.and_then(|config| config.indent_query()),
syntax,
) {
let byte_start = text.char_to_byte(pos);
let node = get_highest_syntax_node_at_bytepos(syntax, byte_start);
// let config = load indentation query config from Syntax(should contain language_config)
// TODO: special case for comments
// TODO: if preserve_leading_whitespace
calculate_indentation(node, new_line)
calculate_indentation(query, node, new_line)
} else {
// TODO: heuristics for non-tree sitter grammars
0
@@ -273,21 +251,25 @@ 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(" "),
}),
}],
});
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(),
}],
},
Vec::new(),
);
// set runtime path so we can find the queries
let mut runtime = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
@@ -304,7 +286,7 @@ where
let line = text.line(i);
let indent = indent_level_for_line(line, tab_width);
assert_eq!(
suggested_indent_for_line(Some(&syntax), text, i, tab_width),
suggested_indent_for_line(&language_config, Some(&syntax), text, i, tab_width),
indent,
"line {}: {}",
i,

View File

@@ -13,9 +13,10 @@ mod position;
pub mod register;
pub mod search;
pub mod selection;
pub mod state;
mod state;
pub mod syntax;
mod transaction;
pub mod words;
pub(crate) fn find_first_non_whitespace_char2(line: RopeSlice) -> Option<usize> {
// find first non-whitespace char
@@ -44,6 +45,7 @@ pub(crate) fn find_first_non_whitespace_char(text: RopeSlice, line_num: usize) -
None
}
#[cfg(not(embed_runtime))]
pub fn runtime_dir() -> std::path::PathBuf {
// runtime env var || dir where binary is located
std::env::var("HELIX_RUNTIME")
@@ -58,13 +60,22 @@ pub fn runtime_dir() -> std::path::PathBuf {
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;

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

@@ -25,8 +25,7 @@ pub fn move_horizontally(
}
Direction::Forward => {
// Line end is pos at the start of next line - 1
// subtract another 1 because the line ends with \n
let end = text.line_to_char(line + 1).saturating_sub(2);
let end = text.line_to_char(line + 1).saturating_sub(1);
nth_next_grapheme_boundary(text, pos, count).min(end)
}
};
@@ -46,7 +45,10 @@ pub fn move_vertically(
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),
text.len_lines().saturating_sub(2),
),
};
// convert to 0-indexed, subtract another 1 because len_chars() counts \n
@@ -77,8 +79,9 @@ pub fn move_next_word_start(slice: RopeSlice, mut begin: usize, count: usize) ->
begin += 1;
}
// return if not skip while?
skip_over_next(slice, &mut begin, |ch| ch == '\n');
if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
return None;
};
ch = slice.char(begin);
end = begin + 1;
@@ -135,7 +138,7 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
let mut end = begin;
for _ in 0..count {
if begin + 1 == slice.len_chars() {
if begin + 2 >= slice.len_chars() {
return None;
}
@@ -146,8 +149,9 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
begin += 1;
}
// return if not skip while?
skip_over_next(slice, &mut begin, |ch| ch == '\n');
if !skip_over_next(slice, &mut begin, |ch| ch == '\n') {
return None;
};
end = begin;
@@ -170,48 +174,52 @@ pub fn move_next_word_end(slice: RopeSlice, mut begin: usize, count: usize) -> O
// used for by-word movement
fn is_word(ch: char) -> bool {
pub(crate) fn is_word(ch: char) -> bool {
ch.is_alphanumeric() || ch == '_'
}
fn is_horiz_blank(ch: char) -> bool {
pub(crate) fn is_horiz_blank(ch: char) -> bool {
matches!(ch, ' ' | '\t')
}
#[derive(Debug, Eq, PartialEq)]
enum Category {
pub(crate) enum Category {
Whitespace,
Eol,
Word,
Punctuation,
Unknown,
}
fn categorize(ch: char) -> Category {
pub(crate) fn categorize(ch: char) -> Category {
if ch == '\n' {
Category::Eol
} else if ch.is_ascii_whitespace() {
Category::Whitespace
} else if is_word(ch) {
Category::Word
} else if ch.is_ascii_punctuation() {
Category::Punctuation
} else if ch.is_ascii_alphanumeric() {
Category::Word
} else {
unreachable!()
Category::Unknown
}
}
#[inline]
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F)
/// Returns true if there are more characters left after the new position.
pub fn skip_over_next<F>(slice: RopeSlice, pos: &mut usize, fun: F) -> bool
where
F: Fn(char) -> bool,
{
let mut chars = slice.chars_at(*pos);
for ch in chars {
while let Some(ch) = chars.next() {
if !fun(ch) {
break;
}
*pos += 1;
}
chars.next().is_some()
}
#[inline]

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

@@ -383,7 +383,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

@@ -4,7 +4,7 @@ pub use helix_syntax::{get_language, get_language_name, Lang};
use std::{
borrow::Cow,
cell::RefCell,
collections::HashMap,
collections::{HashMap, HashSet},
path::{Path, PathBuf},
sync::Arc,
};
@@ -41,6 +41,9 @@ pub struct LanguageConfiguration {
pub language_server: Option<LanguageServerConfiguration>,
#[serde(skip_serializing_if = "Option::is_none")]
pub indent: Option<IndentationConfiguration>,
#[serde(skip)]
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
}
#[derive(Serialize, Deserialize)]
@@ -59,16 +62,57 @@ pub struct IndentationConfiguration {
pub unit: String,
}
#[derive(Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub struct IndentQuery {
#[serde(default)]
#[serde(skip_serializing_if = "HashSet::is_empty")]
pub indent: HashSet<String>,
#[serde(default)]
#[serde(skip_serializing_if = "HashSet::is_empty")]
pub outdent: HashSet<String>,
}
#[cfg(not(feature = "embed_runtime"))]
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let root = crate::runtime_dir();
let path = root.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
@@ -127,6 +171,17 @@ impl LanguageConfiguration {
.clone()
}
pub fn indent_query(&self) -> Option<&IndentQuery> {
self.indent_query
.get_or_init(|| {
let language = get_language_name(self.language_id).to_ascii_lowercase();
let toml = load_runtime_file(&language, "indents.toml").ok()?;
toml::from_slice(&toml.as_bytes()).ok()
})
.as_ref()
}
pub fn scope(&self) -> &str {
&self.scope
}
@@ -138,13 +193,15 @@ 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 {
@@ -164,6 +221,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.
@@ -717,7 +778,7 @@ struct LocalScope<'a> {
local_defs: Vec<LocalDef<'a>>,
}
struct HighlightIter<'a, F>
struct HighlightIter<'a, 'tree: 'a, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{
@@ -725,16 +786,16 @@ where
byte_offset: usize,
injection_callback: F,
cancellation_flag: Option<&'a AtomicUsize>,
layers: Vec<HighlightIterLayer<'a>>,
layers: Vec<HighlightIterLayer<'a, 'tree>>,
iter_count: usize,
next_event: Option<HighlightEvent>,
last_highlight_range: Option<(usize, usize, usize)>,
}
struct HighlightIterLayer<'a> {
struct HighlightIterLayer<'a, 'tree: 'a> {
_tree: Option<Tree>,
cursor: QueryCursor,
captures: iter::Peekable<QueryCaptures<'a, Cow<'a, [u8]>>>,
captures: iter::Peekable<QueryCaptures<'a, 'tree, Cow<'a, [u8]>>>,
config: &'a HighlightConfiguration,
highlight_end_stack: Vec<usize>,
scope_stack: Vec<LocalScope<'a>>,
@@ -901,7 +962,7 @@ impl HighlightConfiguration {
}
}
impl<'a> HighlightIterLayer<'a> {
impl<'a, 'tree: 'a> HighlightIterLayer<'a, 'tree> {
/// Create a new 'layer' of highlighting for this document.
///
/// In the even that the new layer contains "combined injections" (injections where multiple
@@ -1165,7 +1226,7 @@ impl<'a> HighlightIterLayer<'a> {
}
}
impl<'a, F> HighlightIter<'a, F>
impl<'a, 'tree: 'a, F> HighlightIter<'a, 'tree, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{
@@ -1216,7 +1277,7 @@ where
}
}
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a>) {
fn insert_layer(&mut self, mut layer: HighlightIterLayer<'a, 'tree>) {
if let Some(sort_key) = layer.sort_key() {
let mut i = 1;
while i < self.layers.len() {
@@ -1235,7 +1296,7 @@ where
}
}
impl<'a, F> Iterator for HighlightIter<'a, F>
impl<'a, 'tree: 'a, F> Iterator for HighlightIter<'a, 'tree, F>
where
F: FnMut(&str) -> Option<&'a HighlightConfiguration> + 'a,
{
@@ -1672,3 +1733,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

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

65
helix-core/src/words.rs Normal file
View File

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

View File

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

View File

@@ -1,5 +1,4 @@
mod client;
mod select_all;
mod transport;
pub use jsonrpc_core as jsonrpc;
@@ -171,7 +170,7 @@ pub use jsonrpc::Call;
type LanguageId = String;
use crate::select_all::SelectAll;
use futures_util::stream::select_all::SelectAll;
pub struct Registry {
inner: HashMap<LanguageId, Option<Arc<Client>>>,
@@ -198,7 +197,7 @@ impl Registry {
if let Some(config) = &language_config.language_server {
// avoid borrow issues
let inner = &mut self.inner;
let s_incoming = &self.incoming;
let s_incoming = &mut 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

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,6 +1,6 @@
[package]
name = "helix-syntax"
version = "0.1.0"
version = "0.0.10"
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

@@ -72,6 +72,7 @@ mk_langs!(
(CSharp, tree_sitter_c_sharp),
(Cpp, tree_sitter_cpp),
(Css, tree_sitter_css),
(Elixir, tree_sitter_elixir),
(Go, tree_sitter_go),
// (Haskell, tree_sitter_haskell),
(Html, tree_sitter_html),

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-term"
version = "0.1.0"
version = "0.0.10"
description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
@@ -8,6 +8,9 @@ license = "MPL-2.0"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[features]
embed_runtime = ["helix-core/embed_runtime"]
[[bin]]
name = "hx"
path = "src/main.rs"
@@ -24,7 +27,6 @@ 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"] }
clap = { version = "3.0.0-beta.2 ", default-features = false, features = ["std", "cargo"] }
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }

View File

@@ -1,8 +1,6 @@
use clap::ArgMatches as Args;
use helix_view::{document::Mode, Document, Editor, Theme, View};
use crate::{compositor::Compositor, ui};
use crate::{compositor::Compositor, ui, Args};
use log::{error, info};
@@ -47,16 +45,28 @@ impl Application {
let size = compositor.size();
let mut editor = Editor::new(size);
if let Ok(files) = args.values_of_t::<PathBuf>("files") {
for file in files {
editor.open(file, Action::VerticalSplit)?;
compositor.push(Box::new(ui::EditorView::new()));
if !args.files.is_empty() {
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,
@@ -207,7 +217,7 @@ impl Application {
})
.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();
}

View File

@@ -3,8 +3,8 @@ use helix_core::{
movement::{self, Direction},
object, pos_at_coords,
regex::{self, Regex},
register, search, selection, Change, ChangeSet, Position, Range, Rope, RopeSlice, Selection,
SmallVec, Tendril, Transaction,
register, search, selection, words, Change, ChangeSet, Position, Range, Rope, RopeSlice,
Selection, SmallVec, Tendril, Transaction,
};
use helix_view::{
@@ -37,7 +37,6 @@ use once_cell::sync::Lazy;
pub struct Context<'a> {
pub count: usize,
pub editor: &'a mut Editor,
pub view_id: ViewId,
pub callback: Option<crate::compositor::Callback>,
pub on_next_key_callback: Option<Box<dyn FnOnce(&mut Context, KeyEvent)>>,
@@ -187,37 +186,31 @@ pub fn move_line_down(cx: &mut Context) {
pub fn move_line_end(cx: &mut Context) {
let (view, doc) = cx.current();
let lines = selection_lines(doc.text(), doc.selection(view.id));
let positions = lines
.into_iter()
.map(|index| {
// adjust all positions to the end of the line.
let selection = doc.selection(view.id).transform(|range| {
let text = doc.text();
let line = text.char_to_line(range.head);
// Line end is pos at the start of next line - 1
// subtract another 1 because the line ends with \n
doc.text().line_to_char(index + 1).saturating_sub(2)
})
.map(|pos| Range::new(pos, pos));
let selection = Selection::new(positions.collect(), 0);
// Line end is pos at the start of next line - 1
// subtract another 1 because the line ends with \n
let pos = text.line_to_char(line + 1).saturating_sub(2);
Range::new(pos, pos)
});
doc.set_selection(view.id, selection);
}
pub fn move_line_start(cx: &mut Context) {
let (view, doc) = cx.current();
let lines = selection_lines(doc.text(), doc.selection(view.id));
let positions = lines
.into_iter()
.map(|index| {
// adjust all positions to the start of the line.
doc.text().line_to_char(index)
})
.map(|pos| Range::new(pos, pos));
let selection = doc.selection(view.id).transform(|range| {
let text = doc.text();
let line = text.char_to_line(range.head);
let selection = Selection::new(positions.collect(), 0);
// adjust to start of the line
let pos = text.line_to_char(line);
Range::new(pos, pos)
});
doc.set_selection(view.id, selection);
}
@@ -322,7 +315,7 @@ fn _find_char<F>(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool)
where
// TODO: make an options struct for and abstract this Fn into a searcher type
// use the definition for w/b/e too
F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize>,
F: Fn(RopeSlice, char, usize, usize, bool) -> Option<usize> + 'static,
{
// TODO: count is reset to 1 before next key so we move it into the closure here.
// Would be nice to carry over.
@@ -339,7 +332,7 @@ where
let text = doc.text().slice(..);
let selection = doc.selection(view.id).transform(|mut range| {
search::find_nth_next(text, ch, range.head, count, inclusive).map_or(range, |pos| {
search_fn(text, ch, range.head, count, inclusive).map_or(range, |pos| {
if extend {
Range::new(range.anchor, pos)
} else {
@@ -480,10 +473,10 @@ fn scroll(cx: &mut Context, offset: usize, direction: Direction) {
let last_line = view.last_line(doc);
// clamp into viewport
let line = cursor.row.clamp(
view.first_line + scrolloff,
last_line.saturating_sub(scrolloff),
);
let line = cursor
.row
.max(view.first_line + scrolloff)
.min(last_line.saturating_sub(scrolloff));
let text = doc.text().slice(..);
let pos = pos_at_coords(text, Position::new(line, cursor.col)); // this func will properly truncate to line end
@@ -580,6 +573,37 @@ pub fn extend_line_down(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
pub fn extend_line_end(cx: &mut Context) {
let (view, doc) = cx.current();
let selection = doc.selection(view.id).transform(|range| {
let text = doc.text();
let line = text.char_to_line(range.head);
// Line end is pos at the start of next line - 1
// subtract another 1 because the line ends with \n
let pos = text.line_to_char(line + 1).saturating_sub(2);
Range::new(range.anchor, pos)
});
doc.set_selection(view.id, selection);
}
pub fn extend_line_start(cx: &mut Context) {
let (view, doc) = cx.current();
let selection = doc.selection(view.id).transform(|range| {
let text = doc.text();
let line = text.char_to_line(range.head);
// adjust to start of the line
let pos = text.line_to_char(line);
Range::new(range.anchor, pos)
});
doc.set_selection(view.id, selection);
}
pub fn select_all(cx: &mut Context) {
let (view, doc) = cx.current();
@@ -640,7 +664,12 @@ fn _search(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, e
let start = text.byte_to_char(mat.start());
let end = text.byte_to_char(mat.end());
let head = end - 1;
if end == 0 {
// skip empty matches that don't make sense
return;
}
let head = end;
let selection = if extend {
selection.clone().push(Range::new(start, head))
@@ -656,7 +685,7 @@ fn _search(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, e
// TODO: use one function for search vs extend
pub fn search(cx: &mut Context) {
let doc = cx.doc();
let (view, doc) = cx.current();
// TODO: could probably share with select_on_matches?
@@ -664,7 +693,7 @@ pub fn search(cx: &mut Context) {
// feed chunks into the regex yet
let contents = doc.text().slice(..).to_string();
let view_id = cx.view_id;
let view_id = view.id;
let prompt = ui::regex_prompt(cx, "search:".to_string(), move |view, doc, regex| {
let text = doc.text();
let start = doc.selection(view.id).cursor();
@@ -676,6 +705,7 @@ pub fn search(cx: &mut Context) {
cx.push_layer(Box::new(prompt));
}
// can't search next for ""compose"" for some reason
pub fn _search_next(cx: &mut Context, extend: bool) {
if let Some(query) = register::get('\\') {
@@ -719,7 +749,9 @@ pub fn select_line(cx: &mut Context) {
let line = text.char_to_line(pos.head);
let start = text.line_to_char(line);
let end = text.line_to_char(line + count).saturating_sub(1);
let end = text
.line_to_char(std::cmp::min(doc.text().len_lines(), line + count))
.saturating_sub(1);
doc.set_selection(view.id, Selection::single(start, end));
}
@@ -760,7 +792,9 @@ fn _delete_selection(doc: &mut Document, view_id: ViewId) {
// then delete
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| {
(range.from(), range.to() + 1, None)
let max_to = doc.text().len_chars().saturating_sub(1);
let to = std::cmp::min(max_to, range.to() + 1);
(range.from(), to, None)
});
doc.apply(&transaction, view_id);
}
@@ -770,6 +804,9 @@ pub fn delete_selection(cx: &mut Context) {
_delete_selection(doc, view.id);
doc.append_changes_to_history(view.id);
// exit select mode, if currently in select mode
exit_select_mode(cx);
}
pub fn change_selection(cx: &mut Context) {
@@ -824,6 +861,17 @@ pub fn append_mode(cx: &mut Context) {
graphemes::next_grapheme_boundary(text, range.to()), // to() + next char
)
});
let end = text.len_chars();
if selection.iter().any(|range| range.head == end) {
let transaction = Transaction::change(
doc.text(),
std::array::IntoIter::new([(end, end, Some(Tendril::from_char('\n')))]),
);
doc.apply(&transaction, view.id);
}
doc.set_selection(view.id, selection);
}
@@ -875,17 +923,30 @@ mod cmd {
}
fn open(editor: &mut Editor, args: &[&str], event: PromptEvent) {
let path = args[0];
editor.open(path.into(), Action::Replace);
match args.get(0) {
Some(path) => {
// TODO: handle error
editor.open(path.into(), Action::Replace);
}
None => {
editor.set_error("wrong argument count".to_string());
}
};
}
fn write(editor: &mut Editor, args: &[&str], event: PromptEvent) {
let id = editor.view().doc;
let doc = &mut editor.documents[id];
let (view, doc) = editor.current();
if let Some(path) = args.get(0) {
if let Err(err) = doc.set_path(Path::new(path)) {
editor.set_error(format!("invalid filepath: {}", err));
return;
};
}
if doc.path().is_none() {
editor.set_error("cannot write a buffer without a filename".to_string());
return;
}
doc.format(view.id); // TODO: merge into save
tokio::spawn(doc.save());
}
@@ -896,24 +957,7 @@ mod cmd {
fn format(editor: &mut Editor, args: &[&str], event: PromptEvent) {
let (view, doc) = editor.current();
if let Some(language_server) = doc.language_server() {
// TODO: await, no blocking
let transaction = helix_lsp::block_on(
language_server
.text_document_formatting(doc.identifier(), lsp::FormattingOptions::default()),
)
.map(|edits| {
helix_lsp::util::generate_transaction_from_edits(
doc.text(),
edits,
language_server.offset_encoding(),
)
});
if let Ok(transaction) = transaction {
doc.apply(&transaction, view.id);
}
}
doc.format(view.id)
}
pub const COMMAND_LIST: &[Command] = &[
@@ -941,7 +985,7 @@ mod cmd {
Command {
name: "write",
alias: Some("w"),
doc: "Write changes to disk.",
doc: "Write changes to disk. Accepts an optional path (:write some/path.txt)",
fun: write,
completer: Some(completers::filename),
},
@@ -1025,6 +1069,9 @@ pub fn command_mode(cx: &mut Context) {
}
let parts = input.split_ascii_whitespace().collect::<Vec<&str>>();
if parts.is_empty() {
return;
}
if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
(cmd.fun)(editor, &parts[1..], event);
@@ -1107,19 +1154,6 @@ pub fn buffer_picker(cx: &mut Context) {
cx.push_layer(Box::new(picker));
}
// calculate line numbers for each selection range
fn selection_lines(doc: &Rope, selection: &Selection) -> Vec<usize> {
let mut lines = selection
.iter()
.map(|range| doc.char_to_line(range.head))
.collect::<Vec<_>>();
lines.sort_unstable(); // sorting by usize so _unstable is preferred
lines.dedup();
lines
}
// I inserts at the start of each line with a selection
pub fn prepend_to_line(cx: &mut Context) {
move_line_start(cx);
@@ -1129,15 +1163,14 @@ pub fn prepend_to_line(cx: &mut Context) {
// A inserts at the end of each line with a selection
pub fn append_to_line(cx: &mut Context) {
move_line_end(cx);
let (view, doc) = cx.current();
enter_insert_mode(doc);
// offset by another 1 char since move_line_end will position on the last char, we want to
// append past that
let selection = doc.selection(view.id).transform(|range| {
let pos = range.head + 1;
let text = doc.text();
let line = text.char_to_line(range.head);
// we can't use line_to_char(line + 1) - 2 because the last line might not contain \n
let pos = (text.line_to_char(line) + text.line(line).len_chars()).saturating_sub(1);
Range::new(pos, pos)
});
doc.set_selection(view.id, selection);
@@ -1154,13 +1187,15 @@ fn open(cx: &mut Context, open: Open) {
enter_insert_mode(doc);
let text = doc.text().slice(..);
let lines = selection_lines(doc.text(), doc.selection(view.id));
let selection = doc.selection(view.id);
let mut ranges = SmallVec::with_capacity(lines.len());
let mut ranges = SmallVec::with_capacity(selection.len());
let changes: Vec<Change> = selection
.iter()
.map(|range| {
let line = text.char_to_line(range.head);
let changes: Vec<Change> = lines
.into_iter()
.map(|line| {
let line = match open {
// adjust position to the end of the line (next line - 1)
Open::Below => line + 1,
@@ -1171,7 +1206,13 @@ fn open(cx: &mut Context, open: Open) {
let index = doc.text().line_to_char(line).saturating_sub(1);
// TODO: share logic with insert_newline for indentation
let indent_level = indent::suggested_indent_for_pos(doc.syntax(), text, index, true);
let indent_level = indent::suggested_indent_for_pos(
doc.language_config(),
doc.syntax(),
text,
index,
true,
);
let indent = doc.indent_unit().repeat(indent_level);
let mut text = String::with_capacity(1 + indent.len());
text.push('\n');
@@ -1179,7 +1220,7 @@ fn open(cx: &mut Context, open: Open) {
let text = text.repeat(count);
// calculate new selection range
let pos = index + text.len();
let pos = index + text.chars().count();
ranges.push(Range::new(pos, pos));
(index, index, Some(text.into()))
@@ -1245,7 +1286,8 @@ pub fn goto_mode(cx: &mut Context) {
// TODO: can't go to line 1 since we can't distinguish between g and 1g, g gets converted
// to 1g
let (view, doc) = cx.current();
let pos = doc.text().line_to_char(count - 1);
let line_idx = std::cmp::min(count - 1, doc.text().len_lines().saturating_sub(2));
let pos = doc.text().line_to_char(line_idx);
doc.set_selection(view.id, Selection::point(pos));
return;
}
@@ -1260,10 +1302,35 @@ pub fn goto_mode(cx: &mut Context) {
match ch {
'g' => move_file_start(cx),
'e' => move_file_end(cx),
'h' => move_line_start(cx),
'l' => move_line_end(cx),
'd' => goto_definition(cx),
't' => goto_type_definition(cx),
'y' => goto_type_definition(cx),
'r' => goto_reference(cx),
'i' => goto_implementation(cx),
't' | 'm' | 'b' => {
let (view, doc) = cx.current();
let pos = doc.selection(view.id).cursor();
let line = doc.text().char_to_line(pos);
let scrolloff = PADDING.min(view.area.height as usize / 2); // TODO: user pref
let last_line = view.last_line(doc);
let line = match ch {
't' => (view.first_line + scrolloff),
'm' => (view.first_line + (view.area.height as usize / 2)),
'b' => last_line.saturating_sub(scrolloff),
_ => unreachable!(),
}
.min(last_line.saturating_sub(scrolloff));
let pos = doc.text().line_to_char(line);
doc.set_selection(view.id, Selection::point(pos));
}
_ => (),
}
}
@@ -1469,6 +1536,86 @@ pub fn goto_reference(cx: &mut Context) {
);
}
fn goto_pos(editor: &mut Editor, pos: usize) {
push_jump(editor);
let (view, doc) = editor.current();
doc.set_selection(view.id, Selection::point(pos));
align_view(doc, view, Align::Center);
}
pub fn goto_first_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (view, doc) = editor.current();
let cursor_pos = doc.selection(view.id).cursor();
let diag = if let Some(diag) = doc.diagnostics().first() {
diag.range.start
} else {
return;
};
goto_pos(editor, diag);
}
pub fn goto_last_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (view, doc) = editor.current();
let cursor_pos = doc.selection(view.id).cursor();
let diag = if let Some(diag) = doc.diagnostics().last() {
diag.range.start
} else {
return;
};
goto_pos(editor, diag);
}
pub fn goto_next_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (view, doc) = editor.current();
let cursor_pos = doc.selection(view.id).cursor();
let diag = if let Some(diag) = doc
.diagnostics()
.iter()
.map(|diag| diag.range.start)
.find(|&pos| pos > cursor_pos)
{
diag
} else if let Some(diag) = doc.diagnostics().first() {
diag.range.start
} else {
return;
};
goto_pos(editor, diag);
}
pub fn goto_prev_diag(cx: &mut Context) {
let editor = &mut cx.editor;
let (view, doc) = editor.current();
let cursor_pos = doc.selection(view.id).cursor();
let diag = if let Some(diag) = doc
.diagnostics()
.iter()
.rev()
.map(|diag| diag.range.start)
.find(|&pos| pos < cursor_pos)
{
diag
} else if let Some(diag) = doc.diagnostics().last() {
diag.range.start
} else {
return;
};
goto_pos(editor, diag);
}
pub fn signature_help(cx: &mut Context) {
let (view, doc) = cx.current();
@@ -1638,6 +1785,9 @@ pub mod insert {
let selection = doc.selection(view.id);
let mut ranges = SmallVec::with_capacity(selection.len());
// TODO: this is annoying, but we need to do it to properly calculate pos after edits
let mut offs = 0;
let mut transaction = Transaction::change_by_selection(contents, selection, |range| {
let pos = range.head;
@@ -1649,17 +1799,29 @@ pub mod insert {
let curr = contents.char(pos);
// TODO: offset range.head by 1? when calculating?
let indent_level =
indent::suggested_indent_for_pos(doc.syntax(), text, pos.saturating_sub(1), true);
let indent_level = indent::suggested_indent_for_pos(
doc.language_config(),
doc.syntax(),
text,
pos.saturating_sub(1),
true,
);
let indent = doc.indent_unit().repeat(indent_level);
let mut text = String::with_capacity(1 + indent.len());
text.push('\n');
text.push_str(&indent);
let head = pos + text.len();
let head = pos + offs + text.chars().count();
// TODO: range replace or extend
// range.replace(|range| range.is_empty(), head); -> fn extend if cond true, new head pos
// can be used with cx.mode to do replace or extend on most changes
ranges.push(Range::new(
if range.is_empty() { head } else { range.anchor },
if range.is_empty() {
head
} else {
range.anchor + offs
},
head,
));
@@ -1669,11 +1831,11 @@ pub mod insert {
let indent = doc.indent_unit().repeat(indent_level.saturating_sub(1));
text.push('\n');
text.push_str(&indent);
(pos, pos, Some(text.into()))
} else {
(pos, pos, Some(text.into()))
}
offs += text.chars().count();
(pos, pos, Some(text.into()))
});
transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index()));
@@ -1700,7 +1862,6 @@ pub mod insert {
pub fn delete_char_forward(cx: &mut Context) {
let count = cx.count;
let doc = cx.doc();
let (view, doc) = cx.current();
let text = doc.text().slice(..);
let transaction =
@@ -1713,6 +1874,21 @@ pub mod insert {
});
doc.apply(&transaction, view.id);
}
pub fn delete_word_backward(cx: &mut Context) {
let count = cx.count;
let (view, doc) = cx.current();
let text = doc.text().slice(..);
let transaction =
Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
(
words::nth_prev_word_boundary(text, range.head, count),
range.head,
None,
)
});
doc.apply(&transaction, view.id);
}
}
// Undo / Redo
@@ -1721,12 +1897,12 @@ pub mod insert {
// storing it?
pub fn undo(cx: &mut Context) {
let view_id = cx.view_id;
let view_id = cx.view().id;
cx.doc().undo(view_id);
}
pub fn redo(cx: &mut Context) {
let view_id = cx.view_id;
let view_id = cx.view().id;
cx.doc().redo(view_id);
}
@@ -1839,11 +2015,12 @@ fn get_lines(doc: &Document, view_id: ViewId) -> Vec<usize> {
}
pub fn indent(cx: &mut Context) {
let count = cx.count;
let (view, doc) = cx.current();
let lines = get_lines(doc, view.id);
// Indent by one level
let indent = Tendril::from(doc.indent_unit());
let indent = Tendril::from(doc.indent_unit().repeat(count));
let transaction = Transaction::change(
doc.text(),
@@ -1857,14 +2034,17 @@ pub fn indent(cx: &mut Context) {
}
pub fn unindent(cx: &mut Context) {
let count = cx.count;
let (view, doc) = cx.current();
let lines = get_lines(doc, view.id);
let mut changes = Vec::with_capacity(lines.len());
let tab_width = doc.tab_width();
let indent_width = count * tab_width;
for line_idx in lines {
let line = doc.text().line(line_idx);
let mut width = 0;
let mut pos = 0;
for ch in line.chars() {
match ch {
@@ -1873,14 +2053,17 @@ pub fn unindent(cx: &mut Context) {
_ => break,
}
if width >= tab_width {
pos += 1;
if width >= indent_width {
break;
}
}
if width > 0 {
// now delete from start to first non-blank
if pos > 0 {
let start = doc.text().line_to_char(line_idx);
changes.push((start, start + width, None))
changes.push((start, start + pos, None))
}
}
@@ -2151,11 +2334,6 @@ pub fn hover(cx: &mut Context) {
);
}
// view movements
pub fn next_view(cx: &mut Context) {
cx.editor.focus_next()
}
// comments
pub fn toggle_comments(cx: &mut Context) {
let (view, doc) = cx.current();
@@ -2219,16 +2397,38 @@ pub fn jump_backward(cx: &mut Context) {
};
}
//
pub fn window_mode(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
if let KeyEvent {
code: KeyCode::Char(ch),
..
} = event
{
match ch {
'w' => rotate_view(cx),
'h' => hsplit(cx),
'v' => vsplit(cx),
'q' => wclose(cx),
_ => {}
}
}
})
}
pub fn vsplit(cx: &mut Context) {
pub fn rotate_view(cx: &mut Context) {
cx.editor.focus_next()
}
// split helper, clear it later
use helix_view::editor::Action;
fn split(cx: &mut Context, action: Action) {
use helix_view::editor::Action;
let (view, doc) = cx.current();
let id = doc.id();
let selection = doc.selection(view.id).clone();
let first_line = view.first_line;
cx.editor.switch(id, Action::VerticalSplit);
cx.editor.switch(id, action);
// match the selection in the previous view
let (view, doc) = cx.current();
@@ -2236,6 +2436,20 @@ pub fn vsplit(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
pub fn hsplit(cx: &mut Context) {
split(cx, Action::HorizontalSplit);
}
pub fn vsplit(cx: &mut Context) {
split(cx, Action::VerticalSplit);
}
pub fn wclose(cx: &mut Context) {
let view_id = cx.view().id;
// close current split
cx.editor.close(view_id, /* close_buffer */ false);
}
pub fn space_mode(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
if let KeyEvent {
@@ -2247,16 +2461,7 @@ pub fn space_mode(cx: &mut Context) {
match ch {
'f' => file_picker(cx),
'b' => buffer_picker(cx),
'v' => vsplit(cx),
'w' => {
// save current buffer
let doc = cx.doc();
tokio::spawn(doc.save());
}
'c' => {
// close current split
cx.editor.close(cx.view_id, /* close_buffer */ false);
}
'w' => window_mode(cx),
// ' ' => toggle_alternate_buffer(cx),
// TODO: temporary since space mode took it's old key
' ' => keep_primary_selection(cx),
@@ -2297,7 +2502,7 @@ pub fn view_mode(cx: &mut Context) {
let pos = coords_at_pos(doc.text().slice(..), pos);
const OFFSET: usize = 7; // gutters
view.first_col = pos.col.saturating_sub((view.area.width as usize - OFFSET) / 2);
view.first_col = pos.col.saturating_sub(((view.area.width as usize).saturating_sub(OFFSET)) / 2);
},
'h' => (),
'j' => scroll(cx, 1, Direction::Forward),
@@ -2308,3 +2513,35 @@ pub fn view_mode(cx: &mut Context) {
}
})
}
pub fn left_bracket_mode(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
if let KeyEvent {
code: KeyCode::Char(ch),
..
} = event
{
match ch {
'd' => goto_prev_diag(cx),
'D' => goto_first_diag(cx),
_ => (),
}
}
})
}
pub fn right_bracket_mode(cx: &mut Context) {
cx.on_next_key(move |cx, event| {
if let KeyEvent {
code: KeyCode::Char(ch),
..
} = event
{
match ch {
'd' => goto_next_diag(cx),
'D' => goto_last_diag(cx),
_ => (),
}
}
})
}

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)
}

View File

@@ -35,10 +35,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 +61,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 +85,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")]
@@ -103,15 +107,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 +132,40 @@ pub fn default() -> Keymaps {
key!('k') => commands::move_line_up,
key!('l') => commands::move_char_right,
KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE
} => commands::move_char_left,
KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE
} => commands::move_line_down,
KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE
} => commands::move_line_up,
KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::NONE
} => commands::move_char_right,
key!('t') => commands::find_till_char,
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!('0') => commands::move_line_start,
key!('$') => commands::move_line_end,
KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE
} => commands::move_line_start,
KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE
} => commands::move_line_end,
key!('w') => commands::move_next_word_start,
key!('b') => commands::move_prev_word_start,
@@ -157,11 +176,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 +193,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 +213,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
@@ -240,21 +261,20 @@ pub fn default() -> Keymaps {
code: KeyCode::PageUp,
modifiers: KeyModifiers::NONE
} => commands::page_up,
ctrl!('b') => commands::page_up,
KeyEvent {
code: KeyCode::PageDown,
modifiers: KeyModifiers::NONE
} => commands::page_down,
ctrl!('f') => commands::page_down,
ctrl!('u') => commands::half_page_up,
ctrl!('d') => commands::half_page_down,
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
@@ -276,19 +296,44 @@ pub fn default() -> Keymaps {
key!('k') => commands::extend_line_up,
key!('l') => commands::extend_char_right,
KeyEvent {
code: KeyCode::Left,
modifiers: KeyModifiers::NONE
} => commands::extend_char_left,
KeyEvent {
code: KeyCode::Down,
modifiers: KeyModifiers::NONE
} => commands::extend_line_down,
KeyEvent {
code: KeyCode::Up,
modifiers: KeyModifiers::NONE
} => commands::extend_line_up,
KeyEvent {
code: KeyCode::Right,
modifiers: KeyModifiers::NONE
} => commands::extend_char_right,
key!('w') => commands::extend_next_word_start,
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,
key!('T') => commands::extend_till_prev_char,
key!('F') => commands::extend_prev_char,
KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE
} => commands::extend_line_start,
KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE
} => commands::extend_line_end,
KeyEvent {
code: KeyCode::Esc,
modifiers: KeyModifiers::NONE
} => commands::exit_select_mode as Command,
} => commands::exit_select_mode,
)
.into_iter(),
);
@@ -321,6 +366,7 @@ pub fn default() -> Keymaps {
} => commands::insert::insert_tab,
ctrl!('x') => commands::completion,
ctrl!('w') => commands::insert::delete_word_backward,
),
)
}

View File

@@ -8,12 +8,11 @@ mod ui;
use application::Application;
use clap::{App, Arg};
use std::path::PathBuf;
use anyhow::Error;
use anyhow::{Context, Error, 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
@@ -28,8 +27,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| {
@@ -41,56 +38,123 @@ 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(())
}
fn main() {
let args = clap::app_from_crate!()
.arg(
Arg::new("files")
.about("Sets the input file to use")
.required(false)
.multiple(true)
.index(1),
)
.arg(
Arg::new("verbose")
.about("Increases logging verbosity each use for up to 3 times")
.short('v')
.takes_value(false)
.multiple_occurrences(true),
)
.get_matches();
pub struct Args {
display_help: bool,
display_version: bool,
verbosity: u64,
files: Vec<PathBuf>,
}
let verbosity: u64 = args.occurrences_of("verbose");
fn parse_args(mut args: Args) -> Result<Args> {
let argv: Vec<String> = std::env::args().collect();
let mut iter = argv.iter();
setup_logging(verbosity).expect("failed to initialize logging.");
iter.next(); // skip the program, we don't care about that
// initialize language registry
use helix_core::config_dir;
use helix_core::syntax::{Loader, LOADER};
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)),
}
}
// 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"));
// push the remaining args, if any to the files
for filename in iter {
args.files.push(PathBuf::from(filename));
}
LOADER.get_or_init(|| {
let config = toml::from_slice(toml).expect("Could not parse languages.toml");
Loader::new(config)
});
Ok(args)
}
let runtime = tokio::runtime::Runtime::new().unwrap();
#[tokio::main]
async fn main() -> Result<()> {
let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() {
std::fs::create_dir(&cache_dir);
}
let logpath = cache_dir.join("helix.log");
let help = format!(
"\
{} {}
{}
{}
USAGE:
hx [FLAGS] [files]...
ARGS:
<files>... Sets the input file to use
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 args: Args = Args {
display_help: false,
display_version: false,
verbosity: 0,
files: [].to_vec(),
};
args = parse_args(args).context("could not parse arguments")?;
// Help has a higher priority and should be handled separately.
if args.display_help {
print!("{}", help);
std::process::exit(0);
}
if args.display_version {
println!("helix {}", env!("CARGO_PKG_VERSION"));
std::process::exit(0);
}
let conf_dir = helix_core::config_dir();
if !conf_dir.exists() {
std::fs::create_dir(&conf_dir);
}
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;
app.run().await;
});
Ok(())
}

View File

@@ -12,11 +12,60 @@ use helix_core::{Position, Transaction};
use helix_view::Editor;
use crate::commands;
use crate::ui::{Markdown, Menu, Popup, PromptEvent};
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
use helix_lsp::lsp;
use lsp::CompletionItem;
impl menu::Item for CompletionItem {
fn filter_text(&self) -> &str {
self.filter_text.as_ref().unwrap_or(&self.label).as_str()
}
fn label(&self) -> &str {
self.label.as_str()
}
fn row(&self) -> menu::Row {
menu::Row::new(vec![
menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind {
Some(lsp::CompletionItemKind::Text) => "text",
Some(lsp::CompletionItemKind::Method) => "method",
Some(lsp::CompletionItemKind::Function) => "function",
Some(lsp::CompletionItemKind::Constructor) => "constructor",
Some(lsp::CompletionItemKind::Field) => "field",
Some(lsp::CompletionItemKind::Variable) => "variable",
Some(lsp::CompletionItemKind::Class) => "class",
Some(lsp::CompletionItemKind::Interface) => "interface",
Some(lsp::CompletionItemKind::Module) => "module",
Some(lsp::CompletionItemKind::Property) => "property",
Some(lsp::CompletionItemKind::Unit) => "unit",
Some(lsp::CompletionItemKind::Value) => "value",
Some(lsp::CompletionItemKind::Enum) => "enum",
Some(lsp::CompletionItemKind::Keyword) => "keyword",
Some(lsp::CompletionItemKind::Snippet) => "snippet",
Some(lsp::CompletionItemKind::Color) => "color",
Some(lsp::CompletionItemKind::File) => "file",
Some(lsp::CompletionItemKind::Reference) => "reference",
Some(lsp::CompletionItemKind::Folder) => "folder",
Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
Some(lsp::CompletionItemKind::Constant) => "constant",
Some(lsp::CompletionItemKind::Struct) => "struct",
Some(lsp::CompletionItemKind::Event) => "event",
Some(lsp::CompletionItemKind::Operator) => "operator",
Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
None => "",
}),
// self.detail.as_deref().unwrap_or("")
// self.label_details
// .as_ref()
// .or(self.detail())
// .as_str(),
])
}
}
/// Wraps a Menu.
pub struct Completion {
popup: Popup<Menu<CompletionItem>>, // TODO: Popup<Menu> need to be able to access contents.
@@ -31,89 +80,83 @@ impl Completion {
trigger_offset: usize,
) -> Self {
// let items: Vec<CompletionItem> = Vec::new();
let mut menu = Menu::new(
items,
|item| {
// format_fn
item.label.as_str().into()
let mut menu = Menu::new(items, move |editor: &mut Editor, item, event| {
match event {
PromptEvent::Abort => {
// revert state
// let id = editor.view().doc;
// let doc = &mut editor.documents[id];
// doc.state = snapshot.clone();
}
PromptEvent::Validate => {
let (view, doc) = editor.current();
// TODO: use item.filter_text for filtering
},
move |editor: &mut Editor, item, event| {
match event {
PromptEvent::Abort => {
// revert state
// let id = editor.view().doc;
// let doc = &mut editor.documents[id];
// doc.state = snapshot.clone();
}
PromptEvent::Validate => {
let (view, doc) = editor.current();
// revert state to what it was before the last update
// doc.state = snapshot.clone();
// revert state to what it was before the last update
// doc.state = snapshot.clone();
// extract as fn(doc, item):
// extract as fn(doc, item):
// TODO: need to apply without composing state...
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
// the final state?
// -> on update simply update the snapshot, then on accept redo the call,
// finally updating doc.changes + notifying lsp.
//
// or we could simply use doc.undo + apply when changing between options
// TODO: need to apply without composing state...
// TODO: need to update lsp on accept/cancel by diffing the snapshot with
// the final state?
// -> on update simply update the snapshot, then on accept redo the call,
// finally updating doc.changes + notifying lsp.
//
// or we could simply use doc.undo + apply when changing between options
// always present here
let item = item.unwrap();
// always present here
let item = item.unwrap();
use helix_lsp::{lsp, util};
// determine what to insert: text_edit | insert_text | label
let edit = if let Some(edit) = &item.text_edit {
match edit {
lsp::CompletionTextEdit::Edit(edit) => edit.clone(),
lsp::CompletionTextEdit::InsertAndReplace(item) => {
unimplemented!("completion: insert_and_replace {:?}", item)
}
}
} else {
item.insert_text.as_ref().unwrap_or(&item.label);
unimplemented!();
// lsp::TextEdit::new(); TODO: calculate a TextEdit from insert_text
// and we insert at position.
};
// TODO: merge edit with additional_text_edits
if let Some(additional_edits) = &item.additional_text_edits {
if !additional_edits.is_empty() {
unimplemented!(
"completion: additional_text_edits: {:?}",
additional_edits
);
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();
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(),
vec![(trigger_offset, cursor, None)].into_iter(),
);
doc.apply(&remove, view.id);
}
use helix_lsp::OffsetEncoding;
let transaction = util::generate_transaction_from_edits(
// if more text was entered, remove it
let cursor = doc.selection(view.id).cursor();
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
vec![(trigger_offset, cursor, None)].into_iter(),
);
doc.apply(&transaction, view.id);
doc.apply(&remove, view.id);
}
_ => (),
};
},
);
use helix_lsp::OffsetEncoding;
let transaction = util::generate_transaction_from_edits(
doc.text(),
vec![edit],
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
// TODO: merge edit with additional_text_edits
if let Some(additional_edits) = &item.additional_text_edits {
// gopls uses this to add extra imports
if !additional_edits.is_empty() {
let transaction = util::generate_transaction_from_edits(
doc.text(),
additional_edits.clone(),
offset_encoding, // TODO: should probably transcode in Client
);
doc.apply(&transaction, view.id);
}
}
}
_ => (),
};
});
let popup = Popup::new(menu);
Self {
popup,
@@ -164,6 +207,13 @@ impl Completion {
impl Component for Completion {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
// let the Editor handle Esc instead
if let Event::Key(KeyEvent {
code: KeyCode::Esc, ..
}) = event
{
return EventResult::Ignored;
}
self.popup.handle_event(event, cx)
}
@@ -180,32 +230,61 @@ impl Component for Completion {
// option.detail
// ---
// option.documentation
match &option.documentation {
Some(lsp::Documentation::String(s))
let (view, doc) = cx.editor.current();
let language = doc
.language()
.and_then(|scope| scope.strip_prefix("source."))
.unwrap_or("");
let doc = match &option.documentation {
Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::PlainText,
value: s,
value: contents,
})) => {
// TODO: convert to wrapped text
let doc = s;
Markdown::new(format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
))
}
Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
kind: lsp::MarkupKind::Markdown,
value: contents,
})) => {
let doc = Markdown::new(contents.clone());
let half = area.height / 2;
let height = 15.min(half);
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
let area = Rect::new(0, area.height - height - 2, area.width, height);
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
doc.render(area, surface, cx);
// TODO: set language based on doc scope
Markdown::new(format!(
"```{}\n{}\n```\n{}",
language,
option.detail.as_deref().unwrap_or_default(),
contents.clone()
))
}
None => (),
}
None if option.detail.is_some() => {
// TODO: copied from above
// TODO: set language based on doc scope
Markdown::new(format!(
"```{}\n{}\n```",
language,
option.detail.as_deref().unwrap_or_default(),
))
}
None => return,
};
let half = area.height / 2;
let height = 15.min(half);
// -2 to subtract command line + statusline. a bit of a hack, because of splits.
let area = Rect::new(0, area.height - height - 2, area.width, height);
// clear area
let background = cx.editor.theme.get("ui.popup");
surface.clear_with(area, background);
doc.render(area, surface, cx);
}
}
}

View File

@@ -148,6 +148,13 @@ impl EditorView {
// TODO: scope matching: biggest union match? [string] & [html, string], [string, html] & [ string, html]
// can do this by sorting our theme matches based on array len (longest first) then stopping at the
// first rule that matches (rule.all(|scope| scopes.contains(scope)))
// log::info!(
// "scopes: {:?}",
// spans
// .iter()
// .map(|span| theme.scopes()[span.0].as_str())
// .collect::<Vec<_>>()
// );
let style = match spans.first() {
Some(span) => theme.get(theme.scopes()[span.0].as_str()),
None => theme.get("ui.text"),
@@ -188,7 +195,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
});
@@ -254,7 +261,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,
@@ -265,7 +281,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,
@@ -283,7 +299,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,
);
}
@@ -299,6 +320,29 @@ 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 {
// this only prevents panic due to painting selection too far
// TODO: prevent painting when scroll past x or in gutter
// TODO: use a more correct width check
if (pos.col as u16) < viewport.width {
let style = Style::default().add_modifier(Modifier::REVERSED);
surface
.get_mut(
viewport.x + pos.col as u16,
viewport.y + pos.row as u16,
)
.set_style(style);
}
}
}
}
}
}
}
@@ -311,9 +355,9 @@ impl EditorView {
let info: Style = theme.get("info");
let hint: Style = theme.get("hint");
for (i, line) in (view.first_line..last_line).enumerate() {
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,
@@ -357,7 +401,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
});
@@ -439,7 +483,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,
);
@@ -522,7 +566,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;
@@ -530,7 +575,6 @@ impl Component for EditorView {
let mode = doc.mode();
let mut cxt = commands::Context {
view_id: view.id,
editor: &mut cx.editor,
count: 1,
callback: None,
@@ -578,7 +622,6 @@ impl Component for EditorView {
if completion.is_empty() {
self.completion = None;
}
// TODO: if exiting InsertMode, remove completion
}
}
}
@@ -599,16 +642,24 @@ impl Component for EditorView {
let (view, doc) = cx.editor.current();
view.ensure_cursor_in_view(doc);
if mode == Mode::Normal && doc.mode() == Mode::Insert {
// HAXX: if we just entered insert mode from normal, clear key buf
// and record the command that got us into this mode.
// mode transitions
match (mode, doc.mode()) {
(Mode::Normal, Mode::Insert) => {
// HAXX: if we just entered insert mode from normal, clear key buf
// and record the command that got us into this mode.
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
// how we entered insert mode is important, and we should track that so
// we can repeat the side effect.
self.last_insert.0 = self.keymap[&mode][&key];
self.last_insert.1.clear();
};
self.last_insert.0 = self.keymap[&mode][&key];
self.last_insert.1.clear();
}
(Mode::Insert, Mode::Normal) => {
// if exiting insert mode, remove completion
self.completion = None;
}
_ => (),
}
EventResult::Consumed(callback)
}
@@ -620,6 +671,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);
@@ -659,3 +714,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

@@ -107,11 +107,14 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
None => text_style,
};
// TODO: replace tabs with indentation
let mut slice = &text[start..end];
while let Some(end) = slice.find('\n') {
// emit span up to newline
let text = &slice[..end];
let span = Span::styled(text.to_owned(), style);
let text = text.replace('\t', " "); // replace tabs
let span = Span::styled(text, style);
spans.push(span);
// truncate slice to after newline
@@ -124,7 +127,8 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
// if there's anything left, emit it too
if !slice.is_empty() {
let span = Span::styled(slice.to_owned(), style);
let span =
Span::styled(slice.replace('\t', " "), style);
spans.push(span);
}
}
@@ -153,6 +157,7 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
}
}
Event::Code(text) | Event::Html(text) => {
log::warn!("code {:?}", text);
let mut span = to_span(text);
span.style = code_style;
spans.push(span);
@@ -167,7 +172,9 @@ fn parse<'a>(contents: &'a str, theme: Option<&Theme>) -> tui::text::Text<'a> {
lines.push(Spans::default());
}
// TaskListMarker(bool) true if checked
_ => (),
_ => {
log::warn!("unhandled markdown event {:?}", event);
}
}
// build up a vec of Paragraph tui widgets
}

View File

@@ -4,8 +4,11 @@ use tui::{
buffer::Buffer as Surface,
layout::Rect,
style::{Color, Style},
widgets::Table,
};
pub use tui::widgets::{Cell, Row};
use std::borrow::Cow;
use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
@@ -14,7 +17,15 @@ use fuzzy_matcher::FuzzyMatcher;
use helix_core::Position;
use helix_view::Editor;
pub struct Menu<T> {
pub trait Item {
// TODO: sort_text
fn filter_text(&self) -> &str;
fn label(&self) -> &str;
fn row(&self) -> Row;
}
pub struct Menu<T: Item> {
options: Vec<T>,
cursor: Option<usize>,
@@ -23,19 +34,17 @@ pub struct Menu<T> {
/// (index, score)
matches: Vec<(usize, i64)>,
format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Editor, Option<&T>, MenuEvent)>,
scroll: usize,
size: (u16, u16),
}
impl<T> Menu<T> {
impl<T: Item> Menu<T> {
// TODO: it's like a slimmed down picker, share code? (picker = menu + prompt with different
// rendering)
pub fn new(
options: Vec<T>,
format_fn: impl Fn(&T) -> Cow<str> + 'static,
callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static,
) -> Self {
let mut menu = Self {
@@ -43,7 +52,6 @@ impl<T> Menu<T> {
matcher: Box::new(Matcher::default()),
matches: Vec::new(),
cursor: None,
format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn),
scroll: 0,
size: (0, 0),
@@ -61,7 +69,6 @@ impl<T> Menu<T> {
ref mut options,
ref mut matcher,
ref mut matches,
ref format_fn,
..
} = *self;
@@ -72,8 +79,7 @@ impl<T> Menu<T> {
.iter()
.enumerate()
.filter_map(|(index, option)| {
// TODO: maybe using format_fn isn't the best idea here
let text = (format_fn)(option);
let text = option.filter_text();
// TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher
.fuzzy_match(&text, pattern)
@@ -134,7 +140,7 @@ impl<T> Menu<T> {
use super::PromptEvent as MenuEvent;
impl<T: 'static> Component for Menu<T> {
impl<T: Item + 'static> Component for Menu<T> {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let event = match event {
Event::Key(event) => event,
@@ -224,7 +230,7 @@ impl<T: 'static> Component for Menu<T> {
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
let width = std::cmp::min(30, viewport.0);
const MAX: usize = 5;
const MAX: usize = 10;
let height = std::cmp::min(self.options.len(), MAX);
let height = std::cmp::min(height, viewport.1 as usize);
@@ -236,9 +242,11 @@ impl<T: 'static> Component for Menu<T> {
Some(self.size)
}
// TODO: required size should re-trigger when we filter items so we can draw a smaller menu
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let style = cx.editor.theme.get("ui.text");
let selected = Style::default().fg(Color::Rgb(255, 255, 255));
let selected = cx.editor.theme.get("ui.menu.selected");
let scroll = self.scroll;
@@ -264,26 +272,40 @@ impl<T: 'static> Component for Menu<T> {
let scroll_line = (win_height - scroll_height) * scroll
/ std::cmp::max(1, len.saturating_sub(win_height));
for (i, option) in options[scroll..(scroll + win_height).min(len)]
.iter()
.enumerate()
{
let line = Some(i + scroll);
// TODO: set bg for the whole row if selected
surface.set_stringn(
area.x,
area.y + i as u16,
(self.format_fn)(option),
area.width as usize - 1,
if line == self.cursor { selected } else { style },
);
use tui::layout::Constraint;
let rows = options.iter().map(|option| option.row());
let table = Table::new(rows)
.style(style)
.highlight_style(selected)
.column_spacing(1)
.widths(&[Constraint::Percentage(50), Constraint::Percentage(50)]);
use tui::widgets::TableState;
table.render_table(
area,
surface,
&mut TableState {
offset: scroll,
selected: self.cursor,
},
);
// // TODO: set bg for the whole row if selected
// if line == self.cursor {
// surface.set_style(
// Rect::new(area.x, area.y + i as u16, area.width - 1, 1),
// selected,
// )
// }
for (i, option) in (scroll..(scroll + win_height).min(len)).enumerate() {
let is_marked = i >= scroll_line && i < scroll_line + scroll_height;
if is_marked {
let cell = surface.get_mut(area.x + area.width - 2, area.y + i as u16);
cell.set_symbol("");
cell.set_style(selected);
// cell.set_style(selected);
// cell.set_style(if is_marked { selected } else { style });
}
}

View File

@@ -85,7 +85,7 @@ pub fn file_picker(root: PathBuf) -> Picker<PathBuf> {
Err(_err) => None,
});
const MAX: usize = 1024;
const MAX: usize = 2048;
Picker::new(
files.take(MAX).collect(),

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;
}
}
@@ -118,6 +121,18 @@ impl<T> Picker<T> {
// - on input change:
// - score all the names in relation to input
fn inner_rect(area: Rect) -> Rect {
let padding_vertical = area.height * 20 / 100;
let padding_horizontal = area.width * 20 / 100;
Rect::new(
area.x + padding_horizontal,
area.y + padding_vertical,
area.width - padding_horizontal * 2,
area.height - padding_vertical * 2,
)
}
impl<T: 'static> Component for Picker<T> {
fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
let key_event = match event {
@@ -191,15 +206,7 @@ impl<T: 'static> Component for Picker<T> {
}
fn render(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let padding_vertical = area.height * 20 / 100;
let padding_horizontal = area.width * 20 / 100;
let area = Rect::new(
area.x + padding_horizontal,
area.y + padding_vertical,
area.width - padding_horizontal * 2,
area.height - padding_vertical * 2,
);
let area = inner_rect(area);
// -- Render the frame:
@@ -239,13 +246,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);
}
@@ -254,12 +262,25 @@ 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
},
);
}
}
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
// TODO: this is mostly duplicate code
let area = inner_rect(area);
let block = Block::default().borders(Borders::ALL);
// calculate the inner area inside the box
let inner = block.inner(area);
// prompt area
let area = Rect::new(inner.x + 1, inner.y, inner.width - 1, 1);
self.prompt.cursor_position(area, editor)
}
}

View File

@@ -28,6 +28,11 @@ pub enum PromptEvent {
Abort,
}
pub enum CompletionDirection {
Forward,
Backward,
}
impl Prompt {
pub fn new(
prompt: String,
@@ -80,11 +85,18 @@ impl Prompt {
self.exit_selection();
}
pub fn change_completion_selection(&mut self) {
pub fn change_completion_selection(&mut self, direction: CompletionDirection) {
if self.completion.is_empty() {
return;
}
let index = self.selection.map_or(0, |i| i + 1) % self.completion.len();
let index = match direction {
CompletionDirection::Forward => self.selection.map_or(0, |i| i + 1),
CompletionDirection::Backward => {
self.selection.unwrap_or(0) + self.completion.len() - 1
}
} % self.completion.len();
self.selection = Some(index);
let (range, item) = &self.completion[index];
@@ -92,8 +104,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;
}
@@ -111,9 +123,10 @@ impl Prompt {
pub fn render_prompt(&self, area: Rect, surface: &mut Surface, cx: &mut Context) {
let theme = &cx.editor.theme;
let text_color = theme.get("ui.text.focus");
let selected_color = theme.get("ui.menu.selected");
// completion
let max_col = area.width / BASE_WIDTH;
let max_col = std::cmp::max(1, area.width / BASE_WIDTH);
let height = ((self.completion.len() as u16 + max_col - 1) / max_col);
let completion_area = Rect::new(
area.x,
@@ -133,7 +146,8 @@ impl Prompt {
for (i, (_range, completion)) in self.completion.iter().enumerate() {
let color = if Some(i) == self.selection {
Style::default().bg(Color::Rgb(104, 60, 232))
// Style::default().bg(Color::Rgb(104, 60, 232))
selected_color // TODO: just invert bg
} else {
text_color
};
@@ -158,14 +172,15 @@ impl Prompt {
if let Some(doc) = (self.doc_fn)(&self.line) {
let text = ui::Text::new(doc.to_string());
let area = Rect::new(
let viewport = area;
let area = viewport.intersection(Rect::new(
completion_area.x,
completion_area.y - 3,
completion_area.width,
BASE_WIDTH * 3,
3,
);
));
let background = theme.get("ui.window");
let background = theme.get("ui.help");
surface.clear_with(area, background);
use tui::layout::Margin;
@@ -250,12 +265,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,
@@ -271,8 +295,9 @@ impl Component for Prompt {
}
fn cursor_position(&self, area: Rect, editor: &Editor) -> Option<Position> {
let line = area.height as usize - 1;
Some(Position::new(
area.height as usize,
area.y as usize + line,
area.x as usize + self.prompt.len() + self.cursor,
))
}

View File

@@ -1,6 +1,6 @@
[package]
name = "helix-tui"
version = "0.1.0"
version = "0.0.10"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
description = """
A library to build rich terminal user interfaces or dashboards

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

@@ -13,12 +13,12 @@ mod block;
// mod list;
mod paragraph;
mod reflow;
// mod table;
mod table;
pub use self::block::{Block, BorderType};
// pub use self::list::{List, ListItem, ListState};
pub use self::paragraph::{Paragraph, Wrap};
// pub use self::table::{Cell, Row, Table, TableState};
pub use self::table::{Cell, Row, Table, TableState};
use crate::{buffer::Buffer, layout::Rect};
use bitflags::bitflags;

View File

@@ -3,7 +3,7 @@ use crate::{
layout::{Constraint, Rect},
style::Style,
text::Text,
widgets::{Block, StatefulWidget, Widget},
widgets::{Block, Widget},
};
use cassowary::{
strength::{MEDIUM, REQUIRED, WEAK},
@@ -368,8 +368,8 @@ impl<'a> Table<'a> {
#[derive(Debug, Clone)]
pub struct TableState {
offset: usize,
selected: Option<usize>,
pub offset: usize,
pub selected: Option<usize>,
}
impl Default for TableState {
@@ -394,10 +394,11 @@ impl TableState {
}
}
impl<'a> StatefulWidget for Table<'a> {
type State = TableState;
// impl<'a> StatefulWidget for Table<'a> {
impl<'a> Table<'a> {
// type State = TableState;
fn render(mut self, area: Rect, buf: &mut Buffer, state: &mut Self::State) {
pub fn render_table(mut self, area: Rect, buf: &mut Buffer, state: &mut TableState) {
if area.area() == 0 {
return;
}
@@ -522,7 +523,7 @@ fn render_cell(buf: &mut Buffer, cell: &Cell, area: Rect) {
impl<'a> Widget for Table<'a> {
fn render(self, area: Rect, buf: &mut Buffer) {
let mut state = TableState::default();
StatefulWidget::render(self, area, buf, &mut state);
Table::render_table(self, area, buf, &mut 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.0.10"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018"
license = "MPL-2.0"
@@ -28,3 +28,4 @@ slotmap = "1"
serde = { version = "1.0", features = ["derive"] }
toml = "0.5"
log = "~0.4"

View File

@@ -1,6 +1,7 @@
use anyhow::{Context, Error};
use std::cell::Cell;
use std::future::Future;
use std::path::{Path, PathBuf};
use std::path::{Component, Path, PathBuf};
use std::sync::Arc;
use helix_core::{
@@ -40,11 +41,14 @@ 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>>,
}
@@ -64,6 +68,50 @@ where
}
}
/// Normalize a path, removing things like `.` and `..`.
///
/// CAUTION: This does not resolve symlinks (unlike
/// [`std::fs::canonicalize`]). This may cause incorrect or surprising
/// behavior at times. This should be used carefully. Unfortunately,
/// [`std::fs::canonicalize`] can be hard to use correctly, since it can often
/// fail, or on Windows returns annoying device paths. This is a problem Cargo
/// needs to improve on.
/// Copied from cargo: https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81
pub fn normalize_path(path: &Path) -> PathBuf {
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next();
PathBuf::from(c.as_os_str())
} else {
PathBuf::new()
};
for component in components {
match component {
Component::Prefix(..) => unreachable!(),
Component::RootDir => {
ret.push(component.as_os_str());
}
Component::CurDir => {}
Component::ParentDir => {
ret.pop();
}
Component::Normal(c) => {
ret.push(c);
}
}
}
ret
}
// Returns the canonical, absolute form of a path with all intermediate components normalized.
//
// This function is used instead of `std::fs::canonicalize` because we don't want to verify
// here if the path exists, just normalize it's components.
pub fn 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;
@@ -85,37 +133,53 @@ 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))?;
Rope::from_reader(BufReader::new(file))?
};
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)
}
// TODO: remove view_id dependency here
pub fn format(&mut self, view_id: ViewId) {
if let Some(language_server) = self.language_server() {
// TODO: await, no blocking
let transaction = helix_lsp::block_on(
language_server
.text_document_formatting(self.identifier(), lsp::FormattingOptions::default()),
)
.map(|edits| {
helix_lsp::util::generate_transaction_from_edits(
self.text(),
edits,
language_server.offset_encoding(),
)
});
if let Ok(transaction) = transaction {
self.apply(&transaction, view_id);
self.append_changes_to_history(view_id);
}
}
}
// TODO: do we need some way of ensuring two save operations on the same doc can't run at once?
// or is that handled by the OS/async layer
pub fn save(&mut self) -> impl Future<Output = Result<(), anyhow::Error>> {
@@ -131,10 +195,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
@@ -153,6 +227,28 @@ impl Document {
}
}
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(())
}
pub fn set_language(
&mut self,
language_config: Option<Arc<helix_core::syntax::LanguageConfiguration>>,
@@ -172,8 +268,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);
}
@@ -262,26 +360,36 @@ impl Document {
}
pub fn undo(&mut self, view_id: ViewId) -> bool {
if let Some(transaction) = self.history.undo() {
let success = self._apply(&transaction, view_id);
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
success
}
pub fn redo(&mut self, view_id: ViewId) -> bool {
if let Some(transaction) = self.history.redo() {
let success = self._apply(&transaction, view_id);
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
}
@@ -300,7 +408,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]
@@ -310,9 +420,11 @@ impl Document {
#[inline]
pub fn is_modified(&self) -> bool {
let history = self.history.take();
let current_revision = history.current_revision();
self.history.set(history);
self.path.is_some()
&& (self.history.current_revision() != self.last_saved_revision
|| !self.changes.is_empty())
&& (current_revision != self.last_saved_revision || !self.changes.is_empty())
}
#[inline]
@@ -328,6 +440,11 @@ impl Document {
.map(|language| language.scope.as_str())
}
#[inline]
pub fn language_config(&self) -> Option<&LanguageConfiguration> {
self.language.as_deref()
}
#[inline]
/// Current document version, incremented at each change.
pub fn version(&self) -> i32 {
@@ -402,6 +519,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

@@ -36,6 +36,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
@@ -81,11 +93,18 @@ impl Editor {
view.jumps.push(jump);
view.doc = id;
view.first_line = 0;
let view_id = view.id;
let (view, doc) = self.current();
// initialize selection for view
let doc = &mut self.documents[id];
doc.selections.insert(view_id, Selection::point(0));
let selection = doc
.selections
.entry(view.id)
.or_insert_with(|| Selection::point(0));
// TODO: reuse align_view
let pos = selection.cursor();
let line = doc.text().char_to_line(pos);
view.first_line = line.saturating_sub(view.area.height as usize / 2);
return;
}
@@ -118,7 +137,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()
@@ -128,7 +147,7 @@ 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
@@ -187,8 +206,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,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

@@ -293,9 +293,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) {

View File

@@ -106,7 +106,7 @@ impl View {
/// Calculates the last visible line on screen
#[inline]
pub fn last_line(&self, doc: &Document) -> usize {
let height = self.area.height.saturating_sub(1); // - 1 for statusline
let height = self.area.height.saturating_sub(2); // - 2 for statusline
std::cmp::min(
self.first_line + height as usize,
doc.text().len_lines() - 1,
@@ -143,8 +143,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

@@ -17,6 +17,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 +42,7 @@ injection-regex = "c"
file-types = ["c"] # TODO: ["h"]
roots = []
language-server = { command = "clangd" }
indent = { tab-width = 2, unit = " " }
[[language]]
@@ -42,6 +52,7 @@ injection-regex = "cpp"
file-types = ["cc", "cpp", "hpp", "h"]
roots = []
language-server = { command = "clangd" }
indent = { tab-width = 2, unit = " " }
[[language]]
@@ -65,6 +76,17 @@ roots = []
indent = { tab-width = 2, unit = " " }
[[language]]
name = "typescript"
scope = "source.ts"
injection-regex = "^(ts|typescript)$"
file-types = ["ts"]
roots = []
# TODO: highlights-jsx, highlights-params
language-server = { command = "typescript-language-server", args = ["--stdio"] }
indent = { tab-width = 2, unit = " " }
[[language]]
name = "css"
scope = "source.css"
@@ -122,3 +144,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,146 @@
["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) @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)$")
(#match? @variable.parameter "^[^_]"))
(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)))))]))
(#match? @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
[(identifier) @function.special
(#match? @function.special "^__.+__$")]
[(remote_identifier) @function.special
(#match? @function.special "^__.+__$")]
[(identifier) @comment
(#match? @comment "^_")]
(ERROR) @warning

View File

@@ -0,0 +1,26 @@
indent = [
"import_declaration",
"const_declaration",
"var_declaration",
"type_declaration",
"type_spec",
# simply block should be enough
# "function_declaration",
# "method_declaration",
"composite_literal",
"func_literal",
"literal_value",
"expression_case",
"default_case",
"type_case",
"communication_case",
"argument_list",
"block",
]
outdent = [
"case",
"}",
"]",
")"
]

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,28 @@
indent = [
"array",
"object",
"arguments",
"formal_parameters",
"statement_block",
"object_pattern",
"class_body",
"named_imports",
"binary_expression",
"return_statement",
"template_substitution",
# (expression_statement (call_expression))
"export_clause",
# typescript
"enum_declaration",
"interface_declaration",
"object_type",
]
outdent = [
"}",
"]",
")"
]

View File

@@ -1,10 +1,28 @@
; Identifier conventions
; Assume all-caps names are constants
((identifier) @constant
(#match? @constant "^[A-Z][A-Z\\d_]+$'"))
; Assume other uppercase names are enum constructors
((identifier) @constructor
(#match? @constructor "^[A-Z]"))
; Assume that uppercase names in paths are types
(mod_item
name: (identifier) @namespace)
(scoped_identifier
path: (identifier) @namespace)
(scoped_identifier
(scoped_identifier
name: (identifier) @namespace))
(scoped_type_identifier
path: (identifier) @namespace)
(scoped_type_identifier
(scoped_identifier
name: (identifier) @namespace))
((scoped_identifier
path: (identifier) @type)
(#match? @type "^[A-Z]"))
@@ -13,9 +31,18 @@
name: (identifier) @type))
(#match? @type "^[A-Z]"))
; Assume other uppercase names are enum constructors
((identifier) @constructor
(#match? @constructor "^[A-Z]"))
; Namespaces
(crate) @namespace
(extern_crate_declaration
(crate)
name: (identifier) @namespace)
(scoped_use_list
path: (identifier) @namespace)
(scoped_use_list
path: (scoped_identifier
(identifier) @namespace))
(use_list (scoped_identifier (identifier) @namespace . (_)))
; Function calls
@@ -41,6 +68,14 @@
(macro_invocation
macro: (identifier) @function.macro
"!" @function.macro)
(macro_invocation
macro: (scoped_identifier
(identifier) @function.macro .))
; (metavariable) @variable
(metavariable) @function.macro
"$" @function.macro
; Function definitions
@@ -73,9 +108,11 @@
";" @punctuation.delimiter
(parameter (identifier) @variable.parameter)
(closure_parameters (_) @variable.parameter)
(lifetime (identifier) @label)
"async" @keyword
"break" @keyword
"const" @keyword
"continue" @keyword
@@ -109,14 +146,14 @@
"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)
(super) @keyword
"as" @keyword
(self) @variable.builtin
(metavariable) @variable
[
(char_literal)
@@ -133,7 +170,44 @@
(attribute_item) @attribute
(inner_attribute_item) @attribute
"as" @operator
"*" @operator
"&" @operator
"'" @operator
[
"*"
"'"
"->"
"=>"
"<="
"="
"=="
"!"
"!="
"%"
"%="
"&"
"&="
"&&"
"|"
"|="
"||"
"^"
"^="
"*"
"*="
"-"
"-="
"+"
"+="
"/"
"/="
">"
"<"
">="
">>"
"<<"
">>="
"@"
".."
"..="
"'"
] @operator
"?" @special

View File

@@ -0,0 +1,31 @@
indent = [
"while_expression",
"for_expression",
"loop_expression",
"if_expression",
"if_let_expression",
"tuple_expression",
"array_expression",
"use_list",
"block",
"match_block",
"arguments",
"parameters",
"declaration_list",
"field_declaration_list",
"field_initializer_list",
"struct_pattern",
"tuple_pattern",
"enum_variant_list",
"binary_expression",
"field_expression",
"where_clause",
"macro_invocation"
]
outdent = [
"where",
"}",
"]",
")"
]

View File

@@ -0,0 +1,36 @@
; inherits: javascript
; Types
(type_identifier) @type
(predefined_type) @type.builtin
((identifier) @type
(#match? @type "^[A-Z]"))
(type_arguments
"<" @punctuation.bracket
">" @punctuation.bracket)
; Variables
(required_parameter (identifier) @variable.parameter)
(optional_parameter (identifier) @variable.parameter)
; Keywords
[
"abstract"
"declare"
"enum"
"export"
"implements"
"interface"
"keyof"
"namespace"
"private"
"protected"
"public"
"type"
"readonly"
] @keyword

View File

@@ -0,0 +1 @@
../javascript/indents.toml

View File

@@ -0,0 +1,2 @@
(required_parameter (identifier) @local.definition)
(optional_parameter (identifier) @local.definition)

View File

@@ -0,0 +1,23 @@
(function_signature
name: (identifier) @name) @definition.function
(method_signature
name: (property_identifier) @name) @definition.method
(abstract_method_signature
name: (property_identifier) @name) @definition.method
(abstract_class_declaration
name: (type_identifier) @name) @definition.class
(module
name: (identifier) @name) @definition.module
(interface_declaration
name: (type_identifier) @name) @definition.interface
(type_annotation
(type_identifier) @name) @reference.type
(new_expression
constructor: (identifier) @name) @reference.class

View File

@@ -4,6 +4,8 @@ pkgs.mkShell {
nativeBuildInputs = with pkgs; [
(rust-bin.stable.latest.default.override { extensions = ["rust-src"]; })
lld_10
lldb
# pythonPackages.six
stdenv.cc.cc.lib
# pkg-config
];
@@ -12,6 +14,7 @@ pkgs.mkShell {
# https://github.com/rust-lang/rust/issues/55979
LD_LIBRARY_PATH="${stdenv.cc.cc.lib}/lib64:$LD_LIBRARY_PATH";
HELIX_RUNTIME=./runtime;
shellHook = ''
export HELIX_RUNTIME=$PWD/runtime
'';
}

View File

@@ -1,9 +1,11 @@
"attribute" = "#dbbfef" # lilac
"keyword" = "#eccdba" # almond
"keyword.directive" = "#dbbfef" # lilac -- preprocessor comments (#if in C)
"namespace" = "#dbbfef" # lilac
"punctuation" = "#a4a0e8" # lavender
"punctuation.delimiter" = "#a4a0e8" # lavender
"operator" = "#dbbfef" # lilac
"special" = "#efba5d" # honey
# "property" = "#a4a0e8" # lavender
"property" = "#ffffff" # white
"variable" = "#a4a0e8" # lavender
@@ -31,7 +33,6 @@
# TODO: variable as lilac
# TODO: mod/use statements as white
# TODO: mod stuff as chamois
# TODO: add "(scoped_identifier) @path" for std::mem::
#
# concat (ERROR) @syntax-error and "MISSING ;" selectors for errors
@@ -42,10 +43,15 @@
"ui.statusline" = { bg = "#281733" } # revolver
"ui.popup" = { bg = "#281733" } # revolver
"ui.window" = { bg = "#452859" } # bossa nova
"ui.window" = { bg = "#452859" } # bossa nova
"ui.help" = { bg = "#6F44F0", fg = "#a4a0e8" }
"ui.help" = { bg = "#7958DC", fg = "#171452" }
"ui.text" = { fg = "#a4a0e8"} # lavender
"ui.text" = { fg = "#a4a0e8" } # lavender
"ui.text.focus" = { fg = "#dbbfef"} # lilac
"ui.menu.selected" = { fg = "#281733", bg = "#ffffff" } # revolver
"warning" = "#ffcd1c"
"error" = "#f47868"
"info" = "#6F44F0"