on master help sign in
Merge remote-tracking branch 'origin/master' into debug
d1854d8Blaž Hrastnik
.gitmodules+8
@@ -134,3 +134,11 @@
134 path = helix-syntax/languages/tree-sitter-cmake
134 path = helix-syntax/languages/tree-sitter-cmake
135 url = https://github.com/uyha/tree-sitter-cmake
135 url = https://github.com/uyha/tree-sitter-cmake
136 shallow = true
136 shallow = true
137+[submodule "helix-syntax/languages/tree-sitter-glsl"]
138+ path = helix-syntax/languages/tree-sitter-glsl
139+ url = https://github.com/theHamsta/tree-sitter-glsl.git
140+ shallow = true
141+[submodule "helix-syntax/languages/tree-sitter-perl"]
142+ path = helix-syntax/languages/tree-sitter-perl
143+ url = https://github.com/ganezdragon/tree-sitter-perl
144+ shallow = true
Cargo.lock+12−12
@@ -13,15 +13,15 @@ dependencies = [
13
13
14[[package]]
14[[package]]
15name = "anyhow"
15name = "anyhow"
16−version = "1.0.44"
16+version = "1.0.46"
17source = "registry+https://github.com/rust-lang/crates.io-index"
17source = "registry+https://github.com/rust-lang/crates.io-index"
18−checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
18+checksum = "3aa828229c44c0293dd7d4d2300bdfc4d2883ffdba934c069a6b968957a81f70"
19
19
20[[package]]
20[[package]]
21name = "arc-swap"
21name = "arc-swap"
22−version = "1.4.0"
22+version = "1.5.0"
23source = "registry+https://github.com/rust-lang/crates.io-index"
23source = "registry+https://github.com/rust-lang/crates.io-index"
24−checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
24+checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f"
25
25
26[[package]]
26[[package]]
27name = "autocfg"
27name = "autocfg"
@@ -66,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
66
66
67[[package]]
67[[package]]
68name = "cc"
68name = "cc"
69−version = "1.0.71"
69+version = "1.0.72"
70source = "registry+https://github.com/rust-lang/crates.io-index"
70source = "registry+https://github.com/rust-lang/crates.io-index"
71−checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
71+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
72
72
73[[package]]
73[[package]]
74name = "cfg-if"
74name = "cfg-if"
@@ -583,9 +583,9 @@ checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
583
583
584[[package]]
584[[package]]
585name = "libloading"
585name = "libloading"
586−version = "0.7.1"
586+version = "0.7.2"
587source = "registry+https://github.com/rust-lang/crates.io-index"
587source = "registry+https://github.com/rust-lang/crates.io-index"
588−checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0"
588+checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52"
589dependencies = [
589dependencies = [
590 "cfg-if",
590 "cfg-if",
591 "winapi",
591 "winapi",
@@ -914,9 +914,9 @@ dependencies = [
914
914
915[[package]]
915[[package]]
916name = "serde_json"
916name = "serde_json"
917−version = "1.0.68"
917+version = "1.0.70"
918source = "registry+https://github.com/rust-lang/crates.io-index"
918source = "registry+https://github.com/rust-lang/crates.io-index"
919−checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
919+checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
920dependencies = [
920dependencies = [
921 "itoa",
921 "itoa",
922 "ryu",
922 "ryu",
@@ -1086,9 +1086,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
1086
1086
1087[[package]]
1087[[package]]
1088name = "tokio"
1088name = "tokio"
1089−version = "1.13.0"
1089+version = "1.13.1"
1090source = "registry+https://github.com/rust-lang/crates.io-index"
1090source = "registry+https://github.com/rust-lang/crates.io-index"
1091−checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
1091+checksum = "52963f91310c08d91cb7bff5786dfc8b79642ab839e188187e92105dbfb9d2c8"
1092dependencies = [
1092dependencies = [
1093 "autocfg",
1093 "autocfg",
1094 "bytes",
1094 "bytes",
Cargo.toml+3
@@ -15,3 +15,6 @@ members = [
15
15
16[profile.dev]
16[profile.dev]
17split-debuginfo = "unpacked"
17split-debuginfo = "unpacked"
18+
19+[profile.release]
20+lto = "thin"
book/src/SUMMARY.md+2
@@ -8,5 +8,7 @@
8 - [Keymap](./keymap.md)
8 - [Keymap](./keymap.md)
9 - [Key Remapping](./remapping.md)
9 - [Key Remapping](./remapping.md)
10 - [Hooks](./hooks.md)
10 - [Hooks](./hooks.md)
11+ - [Languages](./languages.md)
11- [Guides](./guides/README.md)
12- [Guides](./guides/README.md)
13+ - [Adding Languages](./guides/adding_languages.md)
12 - [Adding Textobject Queries](./guides/textobject.md)
14 - [Adding Textobject Queries](./guides/textobject.md)
book/src/guides/adding_languages.mdnew file+60
@@ -0,0 +1,60 @@
1+# Adding languages
2+
3+## Submodules
4+
5+To add a new langauge, you should first add a tree-sitter submodule. To do this,
6+you can run the command
7+```sh
8+git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
9+```
10+For example, to add tree-sitter-ocaml you would run
11+```sh
12+git submodule add -f https://github.com/tree-sitter/tree-sitter-ocaml helix-syntax/languages/tree-sitter-ocaml
13+```
14+Make sure the submodule is shallow by doing
15+```sh
16+git config -f .gitmodules submodule.helix-syntax/languages/tree-sitter-<name>.shallow true
17+```
18+
19+or you can manually add `shallow = true` to `.gitmodules`.
20+
21+## languages.toml
22+
23+Next, you need to add the language to the [`languages.toml`][languages.toml] found in the root of
24+the repository; this `languages.toml` file is included at compilation time, and
25+is distinct from the `language.toml` file in the user's [configuration
26+directory](../configuration.md).
27+
28+These are the available keys and descriptions for the file.
29+
30+| Key | Description |
31+| ---- | ----------- |
32+| name | The name of the language |
33+| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
34+| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
35+| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
36+| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` |
37+| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
38+| auto-format | Whether to autoformat this language when saving |
39+| comment-token | The token to use as a comment-token |
40+| indent | The indent to use. Has sub keys `tab-width` and `unit` |
41+| config | Language server configuration |
42+
43+## Queries
44+
45+For a language to have syntax-highlighting and indentation among other things, you have to add queries. Add a directory for your language with the path `runtime/queries/<name>/`. The tree-sitter [website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) gives more info on how to write queries.
46+
47+## Common Issues
48+
49+- If you get errors when building after switching branches, you may have to remove or update tree-sitter submodules. You can update submodules by running
50+ ```sh
51+ git submodule sync; git submodule update --init
52+ ```
53+- Make sure to not use the `--remote` flag. To remove submodules look inside the `.gitmodules` and remove directories that are not present inside of it.
54+
55+- If a parser is segfaulting or you want to remove the parser, make sure to remove the submodule *and* the compiled parser in `runtime/grammar/<name>.so`
56+
57+- The indents query is `indents.toml`, *not* `indents.scm`. See [this](https://github.com/helix-editor/helix/issues/114) issue for more information.
58+
59+[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
60+[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml
book/src/guides/textobject.md+1−1
@@ -5,7 +5,7 @@ require an accompanying tree-sitter grammar and a `textobjects.scm` query file
5to work properly. Tree-sitter allows us to query the source code syntax tree
5to work properly. Tree-sitter allows us to query the source code syntax tree
6and capture specific parts of it. The queries are written in a lisp dialect.
6and capture specific parts of it. The queries are written in a lisp dialect.
7More information on how to write queries can be found in the [official tree-sitter
7More information on how to write queries can be found in the [official tree-sitter
8−documentation](tree-sitter-queries).
8+documentation][tree-sitter-queries].
9
9
10Query files should be placed in `runtime/queries/{language}/textobjects.scm`
10Query files should be placed in `runtime/queries/{language}/textobjects.scm`
11when contributing. Note that to test the query files locally you should put
11when contributing. Note that to test the query files locally you should put
book/src/keymap.md+101−61
@@ -1,5 +1,8 @@
1# Keymap
1# Keymap
2
2
3+- Mappings marked (**LSP**) require an active language server for the file.
4+- Mappings marked (**TS**) require a tree-sitter grammar for the filetype.
5+
3## Normal mode
6## Normal mode
4
7
5### Movement
8### Movement
@@ -58,15 +61,19 @@
58| `.` | Repeat last change | N/A |
61| `.` | Repeat last change | N/A |
59| `u` | Undo change | `undo` |
62| `u` | Undo change | `undo` |
60| `U` | Redo change | `redo` |
63| `U` | Redo change | `redo` |
64+| `Alt-u` | Move backward in history | `earlier` |
65+| `Alt-U` | Move forward in history | `later` |
61| `y` | Yank selection | `yank` |
66| `y` | Yank selection | `yank` |
62| `p` | Paste after selection | `paste_after` |
67| `p` | Paste after selection | `paste_after` |
63| `P` | Paste before selection | `paste_before` |
68| `P` | Paste before selection | `paste_before` |
64| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
69| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
65| `>` | Indent selection | `indent` |
70| `>` | Indent selection | `indent` |
66| `<` | Unindent selection | `unindent` |
71| `<` | Unindent selection | `unindent` |
67−| `=` | Format selection | `format_selections` |
72+| `=` | Format selection (**LSP**) | `format_selections` |
68| `d` | Delete selection | `delete_selection` |
73| `d` | Delete selection | `delete_selection` |
69| `c` | Change selection (delete and enter insert mode) | `change_selection` |
74| `c` | Change selection (delete and enter insert mode) | `change_selection` |
75+| `Ctrl-a` | Increment object (number) under cursor | `increment` |
76+| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
70
77
71#### Shell
78#### Shell
72
79
@@ -85,6 +92,7 @@
85| `s` | Select all regex matches inside selections | `select_regex` |
92| `s` | Select all regex matches inside selections | `select_regex` |
86| `S` | Split selection into subselections on regex matches | `split_selection` |
93| `S` | Split selection into subselections on regex matches | `split_selection` |
87| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
94| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
95+| `_` | Trim whitespace from the selection | `trim_selections` |
88| `;` | Collapse selection onto a single cursor | `collapse_selection` |
96| `;` | Collapse selection onto a single cursor | `collapse_selection` |
89| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
97| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
90| `,` | Keep only the primary selection | `keep_primary_selection` |
98| `,` | Keep only the primary selection | `keep_primary_selection` |
@@ -98,9 +106,10 @@
98| `%` | Select entire file | `select_all` |
106| `%` | Select entire file | `select_all` |
99| `x` | Select current line, if already selected, extend to next line | `extend_line` |
107| `x` | Select current line, if already selected, extend to next line | `extend_line` |
100| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
108| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
101−| | Expand selection to parent syntax node TODO: pick a key | `expand_selection` |
109+| | Expand selection to parent syntax node TODO: pick a key (**TS**) | `expand_selection` |
102| `J` | Join lines inside selection | `join_selections` |
110| `J` | Join lines inside selection | `join_selections` |
103| `K` | Keep selections matching the regex | `keep_selections` |
111| `K` | Keep selections matching the regex | `keep_selections` |
112+| `Alt-K` | Remove selections matching the regex | `remove_selections` |
104| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
113| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
105| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
114| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
106
115
@@ -127,25 +136,23 @@ key to return to normal mode after usage (useful when you're simply looking
127over text and not actively editing it).
136over text and not actively editing it).
128
137
129
138
130−| Key | Description | Command |
139+| Key | Description | Command |
131−| ----- | ----------- | ------- |
140+| ----- | ----------- | ------- |
132−| `z` , `c` | Vertically center the line | `align_view_center` |
141+| `z` , `c` | Vertically center the line | `align_view_center` |
133−| `t` | Align the line to the top of the screen | `align_view_top` |
142+| `t` | Align the line to the top of the screen | `align_view_top` |
134−| `b` | Align the line to the bottom of the screen | `align_view_bottom` |
143+| `b` | Align the line to the bottom of the screen | `align_view_bottom` |
135−| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
144+| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
136−| `j` | Scroll the view downwards | `scroll_down` |
145+| `j` , `down` | Scroll the view downwards | `scroll_down` |
137−| `k` | Scroll the view upwards | `scroll_up` |
146+| `k` , `up` | Scroll the view upwards | `scroll_up` |
138−| `f` | Move page down | `page_down` |
147+| `f` | Move page down | `page_down` |
139−| `b` | Move page up | `page_up` |
148+| `b` | Move page up | `page_up` |
140−| `d` | Move half page down | `half_page_down` |
149+| `d` | Move half page down | `half_page_down` |
141−| `u` | Move half page up | `half_page_up` |
150+| `u` | Move half page up | `half_page_up` |
142
151
143#### Goto mode
152#### Goto mode
144
153
145Jumps to various locations.
154Jumps to various locations.
146
155
147−> NOTE: Some of these features are only available with the LSP present.
148−
149| Key | Description | Command |
156| Key | Description | Command |
150| ----- | ----------- | ------- |
157| ----- | ----------- | ------- |
151| `g` | Go to the start of the file | `goto_file_start` |
158| `g` | Go to the start of the file | `goto_file_start` |
@@ -156,13 +163,14 @@ Jumps to various locations.
156| `t` | Go to the top of the screen | `goto_window_top` |
163| `t` | Go to the top of the screen | `goto_window_top` |
157| `m` | Go to the middle of the screen | `goto_window_middle` |
164| `m` | Go to the middle of the screen | `goto_window_middle` |
158| `b` | Go to the bottom of the screen | `goto_window_bottom` |
165| `b` | Go to the bottom of the screen | `goto_window_bottom` |
159−| `d` | Go to definition | `goto_definition` |
166+| `d` | Go to definition (**LSP**) | `goto_definition` |
160−| `y` | Go to type definition | `goto_type_definition` |
167+| `y` | Go to type definition (**LSP**) | `goto_type_definition` |
161−| `r` | Go to references | `goto_reference` |
168+| `r` | Go to references (**LSP**) | `goto_reference` |
162−| `i` | Go to implementation | `goto_implementation` |
169+| `i` | Go to implementation (**LSP**) | `goto_implementation` |
163| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
170| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
164| `n` | Go to next buffer | `goto_next_buffer` |
171| `n` | Go to next buffer | `goto_next_buffer` |
165| `p` | Go to previous buffer | `goto_previous_buffer` |
172| `p` | Go to previous buffer | `goto_previous_buffer` |
173+| `.` | Go to last modification in current file | `goto_last_modification` |
166
174
167#### Match mode
175#### Match mode
168
176
@@ -172,7 +180,7 @@ and [textobject](./usage.md#textobject) usage.
172
180
173| Key | Description | Command |
181| Key | Description | Command |
174| ----- | ----------- | ------- |
182| ----- | ----------- | ------- |
175−| `m` | Goto matching bracket | `match_brackets` |
183+| `m` | Goto matching bracket (**TS**) | `match_brackets` |
176| `s` `<char>` | Surround current selection with `<char>` | `surround_add` |
184| `s` `<char>` | Surround current selection with `<char>` | `surround_add` |
177| `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` |
185| `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` |
178| `d` `<char>` | Delete surround character `<char>` | `surround_delete` |
186| `d` `<char>` | Delete surround character `<char>` | `surround_delete` |
@@ -195,49 +203,76 @@ This layer is similar to vim keybindings as kakoune does not support window.
195| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
203| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
196| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
204| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
197| `q`, `Ctrl-q` | Close current window | `wclose` |
205| `q`, `Ctrl-q` | Close current window | `wclose` |
206+| `o`, `Ctrl-o` | Only keep the current window, closing all the others | `wonly` |
198
207
199#### Space mode
208#### Space mode
200
209
201This layer is a kludge of mappings, mostly pickers.
210This layer is a kludge of mappings, mostly pickers.
202
211
212+
203−| Key | Description | Command |
213+| Key | Description | Command |
204−| ----- | ----------- | ------- |
214+| ----- | ----------- | ------- |
205−| `k` | Show documentation for the item under the cursor | `hover` |
206−| `f` | Open file picker | `file_picker` |
215+| `f` | Open file picker | `file_picker` |
207−| `b` | Open buffer picker | `buffer_picker` |
216+| `b` | Open buffer picker | `buffer_picker` |
217+| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
208−| `s` | Open symbol picker (current document) | `symbol_picker` |
218+| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
219+| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
220+| `r` | Rename symbol (**LSP**) | `rename_symbol` |
209−| `a` | Apply code action | `code_action` |
221+| `a` | Apply code action (**LSP**) | `code_action` |
210−| `'` | Open last fuzzy picker | `last_picker` |
222+| `'` | Open last fuzzy picker | `last_picker` |
211−| `w` | Enter [window mode](#window-mode) | N/A |
223+| `w` | Enter [window mode](#window-mode) | N/A |
212−| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
224+| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
213−| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
225+| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
214−| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
226+| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
215−| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
227+| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
216−| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
228+| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
217−| `/` | Global search in workspace folder | `global_search` |
229+| `/` | Global search in workspace folder | `global_search` |
218−
230+
219−> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
231+> TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
232+
233+##### Popup
234+
235+Displays documentation for item under cursor.
236+
237+| Key | Description |
238+| ---- | ----------- |
239+| `Ctrl-u` | Scroll up |
240+| `Ctrl-d` | Scroll down |
220
241
221#### Unimpaired
242#### Unimpaired
222
243
223Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
244Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
224
245
225−| Key | Description | Command |
246+| Key | Description | Command |
226−| ----- | ----------- | ------- |
247+| ----- | ----------- | ------- |
227−| `[d` | Go to previous diagnostic | `goto_prev_diag` |
248+| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
228−| `]d` | Go to next diagnostic | `goto_next_diag` |
249+| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
229−| `[D` | Go to first diagnostic in document | `goto_first_diag` |
250+| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
230−| `]D` | Go to last diagnostic in document | `goto_last_diag` |
251+| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
231−| `[space` | Add newline above | `add_newline_above` |
252+| `[space` | Add newline above | `add_newline_above` |
232−| `]space` | Add newline below | `add_newline_below` |
253+| `]space` | Add newline below | `add_newline_below` |
233
254
234## Insert Mode
255## Insert Mode
235
256
236−| Key | Description | Command |
257+| Key | Description | Command |
237−| ----- | ----------- | ------- |
258+| ----- | ----------- | ------- |
238−| `Escape` | Switch to normal mode | `normal_mode` |
259+| `Escape` | Switch to normal mode | `normal_mode` |
239−| `Ctrl-x` | Autocomplete | `completion` |
260+| `Ctrl-x` | Autocomplete | `completion` |
261+| `Ctrl-r` | Insert a register content | `insert_register` |
240−| `Ctrl-w` | Delete previous word | `delete_word_backward` |
262+| `Ctrl-w` | Delete previous word | `delete_word_backward` |
263+| `Alt-d` | Delete next word | `delete_word_forward` |
264+| `Alt-b`, `Alt-Left` | Backward a word | `move_prev_word_end` |
265+| `Ctrl-b`, `Left` | Backward a char | `move_char_left` |
266+| `Alt-f`, `Alt-Right` | Forward a word | `move_next_word_start` |
267+| `Ctrl-f`, `Right` | Forward a char | `move_char_right` |
268+| `Ctrl-e`, `End` | move to line end | `goto_line_end_newline` |
269+| `Ctrl-a`, `Home` | move to line start | `goto_line_start` |
270+| `Ctrl-u` | delete to start of line | `kill_to_line_start` |
271+| `Ctrl-k` | delete to end of line | `kill_to_line_end` |
272+| `backspace`, `Ctrl-h` | delete previous char | `delete_char_backward` |
273+| `delete`, `Ctrl-d` | delete previous char | `delete_char_forward` |
274+| `Ctrl-p`, `Up` | move to previous line | `move_line_up` |
275+| `Ctrl-n`, `Down` | move to next line | `move_line_down` |
241
276
242## Select / extend mode
277## Select / extend mode
243
278
@@ -262,7 +297,9 @@ Keys to use within picker. Remapping currently not supported.
262| `Escape`, `Ctrl-c` | Close picker |
297| `Escape`, `Ctrl-c` | Close picker |
263
298
264# Prompt
299# Prompt
300+
265Keys to use within prompt, Remapping currently not supported.
301Keys to use within prompt, Remapping currently not supported.
302+
266| Key | Description |
303| Key | Description |
267| ----- | ------------- |
304| ----- | ------------- |
268| `Escape`, `Ctrl-c` | Close prompt |
305| `Escape`, `Ctrl-c` | Close prompt |
@@ -270,15 +307,18 @@ Keys to use within prompt, Remapping currently not supported.
270| `Ctrl-b`, `Left` | Backward a char |
307| `Ctrl-b`, `Left` | Backward a char |
271| `Alt-f`, `Alt-Right` | Forward a word |
308| `Alt-f`, `Alt-Right` | Forward a word |
272| `Ctrl-f`, `Right` | Forward a char |
309| `Ctrl-f`, `Right` | Forward a char |
273−| `Ctrl-e`, `End` | move prompt end |
310+| `Ctrl-e`, `End` | Move prompt end |
274−| `Ctrl-a`, `Home` | move prompt start |
311+| `Ctrl-a`, `Home` | Move prompt start |
275−| `Ctrl-w` | delete previous word |
312+| `Ctrl-w` | Delete previous word |
313+| `Alt-d` | Delete next word |
314+| `Ctrl-u` | Delete to start of line |
276−| `Ctrl-k` | delete to end of line |
315+| `Ctrl-k` | Delete to end of line |
316+| `backspace`, `Ctrl-h` | Delete previous char |
277−| `backspace` | delete previous char |
317+| `delete`, `Ctrl-d` | Delete previous char |
278−| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
318+| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
279−| `Ctrl-p`, `Up` | select previous history |
319+| `Ctrl-p`, `Up` | Select previous history |
280−| `Ctrl-n`, `Down` | select next history |
320+| `Ctrl-n`, `Down` | Select next history |
281−| `Tab` | slect next completion item |
321+| `Tab` | Select next completion item |
282−| `BackTab` | slect previous completion item |
322+| `BackTab` | Select previous completion item |
283| `Enter` | Open selected |
323| `Enter` | Open selected |
284
324
book/src/languages.mdnew file+14
@@ -0,0 +1,14 @@
1+# Languages
2+
3+Language-specific settings and settings for particular language servers can be configured in a `languages.toml` file placed in your [configuration directory](./configuration.md). Helix actually uses two `languages.toml` files, the [first one](https://github.com/helix-editor/helix/blob/master/languages.toml) is in the main helix repository; it contains the default settings for each language and is included in the helix binary at compile time. Users who want to see the available settings and options can either reference the helix repo's `languages.toml` file, or consult the table in the [adding languages](./guides/adding_languages.md) section.
4+
5+Changes made to the `languages.toml` file in a user's [configuration directory](./configuration.md) are merged with helix's defaults on start-up, such that a user's settings will take precedence over defaults in the event of a collision. For example, the default `languages.toml` sets rust's `auto-format` to `true`. If a user wants to disable auto-format, they can change the `languages.toml` in their [configuration directory](./configuration.md) to make the rust entry read like the example below; the new key/value pair `auto-format = false` will override the default when the two sets of settings are merged on start-up:
6+
7+```
8+# in <config_dir>/helix/languages.toml
9+
10+[[language]]
11+name = "rust"
12+auto-format = false
13+```
14+
book/src/remapping.md+2
@@ -15,6 +15,7 @@ a = "move_char_left" # Maps the 'a' key to the move_char_left command
15w = "move_line_up" # Maps the 'w' key move_line_up
15w = "move_line_up" # Maps the 'w' key move_line_up
16"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
16"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
17g = { a = "code_action" } # Maps `ga` to show possible code actions
17g = { a = "code_action" } # Maps `ga` to show possible code actions
18+"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode
18
19
19[keys.insert]
20[keys.insert]
20"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
21"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
@@ -38,6 +39,7 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
38| Left | `"left"` |
39| Left | `"left"` |
39| Right | `"right"` |
40| Right | `"right"` |
40| Up | `"up"` |
41| Up | `"up"` |
42+| Down | `"down"` |
41| Home | `"home"` |
43| Home | `"home"` |
42| End | `"end"` |
44| End | `"end"` |
43| Page | `"pageup"` |
45| Page | `"pageup"` |
book/src/usage.md+1
@@ -62,6 +62,7 @@ Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
62| Key after `mi` or `ma` | Textobject selected |
62| Key after `mi` or `ma` | Textobject selected |
63| --- | --- |
63| --- | --- |
64| `w` | Word |
64| `w` | Word |
65+| `W` | WORD |
65| `(`, `[`, `'`, etc | Specified surround pairs |
66| `(`, `[`, `'`, etc | Specified surround pairs |
66| `f` | Function |
67| `f` | Function |
67| `c` | Class |
68| `c` | Class |
helix-core/src/history.rs+28−2
@@ -1,4 +1,4 @@
1−use crate::{ChangeSet, Rope, State, Transaction};
1+use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction};
2use once_cell::sync::Lazy;
2use once_cell::sync::Lazy;
3use regex::Regex;
3use regex::Regex;
4use std::num::NonZeroUsize;
4use std::num::NonZeroUsize;
@@ -133,6 +133,32 @@ impl History {
133 Some(&self.revisions[last_child.get()].transaction)
133 Some(&self.revisions[last_child.get()].transaction)
134 }
134 }
135
135
136+ // Get the position of last change
137+ pub fn last_edit_pos(&self) -> Option<usize> {
138+ if self.current == 0 {
139+ return None;
140+ }
141+ let current_revision = &self.revisions[self.current];
142+ let primary_selection = current_revision
143+ .inversion
144+ .selection()
145+ .expect("inversion always contains a selection")
146+ .primary();
147+ let (_from, to, _fragment) = current_revision
148+ .transaction
149+ .changes_iter()
150+ // find a change that matches the primary selection
151+ .find(|(from, to, _fragment)| Range::new(*from, *to).overlaps(&primary_selection))
152+ // or use the first change
153+ .or_else(|| current_revision.transaction.changes_iter().next())
154+ .unwrap();
155+ let pos = current_revision
156+ .transaction
157+ .changes()
158+ .map_pos(to, Assoc::After);
159+ Some(pos)
160+ }
161+
136 fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
162 fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
137 use std::collections::HashSet;
163 use std::collections::HashSet;
138 let mut a_path_set = HashSet::new();
164 let mut a_path_set = HashSet::new();
@@ -256,7 +282,7 @@ impl History {
256}
282}
257
283
258/// Whether to undo by a number of edits or a duration of time.
284/// Whether to undo by a number of edits or a duration of time.
259−#[derive(Debug, PartialEq)]
285+#[derive(Debug, PartialEq, Clone, Copy)]
260pub enum UndoKind {
286pub enum UndoKind {
261 Steps(usize),
287 Steps(usize),
262 TimePeriod(std::time::Duration),
288 TimePeriod(std::time::Duration),
helix-core/src/indent.rs+1
@@ -450,6 +450,7 @@ where
450 language: vec![LanguageConfiguration {
450 language: vec![LanguageConfiguration {
451 scope: "source.rust".to_string(),
451 scope: "source.rust".to_string(),
452 file_types: vec!["rs".to_string()],
452 file_types: vec!["rs".to_string()],
453+ shebangs: vec![],
453 language_id: "Rust".to_string(),
454 language_id: "Rust".to_string(),
454 highlight_config: OnceCell::new(),
455 highlight_config: OnceCell::new(),
455 config: None,
456 config: None,
helix-core/src/lib.rs+1
@@ -10,6 +10,7 @@ pub mod line_ending;
10pub mod macros;
10pub mod macros;
11pub mod match_brackets;
11pub mod match_brackets;
12pub mod movement;
12pub mod movement;
13+pub mod numbers;
13pub mod object;
14pub mod object;
14pub mod path;
15pub mod path;
15mod position;
16mod position;
helix-core/src/match_brackets.rs+8−1
@@ -1,6 +1,13 @@
1use crate::{Rope, Syntax};
1use crate::{Rope, Syntax};
2
2
3−const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')];
3+const PAIRS: &[(char, char)] = &[
4+ ('(', ')'),
5+ ('{', '}'),
6+ ('[', ']'),
7+ ('<', '>'),
8+ ('\'', '\''),
9+ ('"', '"'),
10+];
4// limit matching pairs to only ( ) { } [ ] < >
11// limit matching pairs to only ( ) { } [ ] < >
5
12
6#[must_use]
13#[must_use]
helix-core/src/movement.rs+1−1
@@ -168,7 +168,7 @@ pub fn backwards_skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<u
168where
168where
169 F: Fn(char) -> bool,
169 F: Fn(char) -> bool,
170{
170{
171− let mut chars_starting_from_next = slice.chars_at(pos + 1);
171+ let mut chars_starting_from_next = slice.chars_at(pos);
172 let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
172 let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
173 backwards.find_map(|(i, c)| {
173 backwards.find_map(|(i, c)| {
174 if !fun(c) {
174 if !fun(c) {
helix-core/src/numbers.rsnew file+499
@@ -0,0 +1,499 @@
1+use std::borrow::Cow;
2+
3+use ropey::RopeSlice;
4+
5+use crate::{
6+ textobject::{textobject_word, TextObject},
7+ Range, Tendril,
8+};
9+
10+#[derive(Debug, PartialEq, Eq)]
11+pub struct NumberIncrementor<'a> {
12+ pub range: Range,
13+ pub value: i64,
14+ pub radix: u32,
15+
16+ text: RopeSlice<'a>,
17+}
18+
19+impl<'a> NumberIncrementor<'a> {
20+ /// Return information about number under rang if there is one.
21+ pub fn from_range(text: RopeSlice, range: Range) -> Option<NumberIncrementor> {
22+ // If the cursor is on the minus sign of a number we want to get the word textobject to the
23+ // right of it.
24+ let range = if range.to() < text.len_chars()
25+ && range.to() - range.from() <= 1
26+ && text.char(range.from()) == '-'
27+ {
28+ Range::new(range.from() + 1, range.to() + 1)
29+ } else {
30+ range
31+ };
32+
33+ let range = textobject_word(text, range, TextObject::Inside, 1, false);
34+
35+ // If there is a minus sign to the left of the word object, we want to include it in the range.
36+ let range = if range.from() > 0 && text.char(range.from() - 1) == '-' {
37+ range.extend(range.from() - 1, range.from())
38+ } else {
39+ range
40+ };
41+
42+ let word: String = text
43+ .slice(range.from()..range.to())
44+ .chars()
45+ .filter(|&c| c != '_')
46+ .collect();
47+ let (radix, prefixed) = if word.starts_with("0x") {
48+ (16, true)
49+ } else if word.starts_with("0o") {
50+ (8, true)
51+ } else if word.starts_with("0b") {
52+ (2, true)
53+ } else {
54+ (10, false)
55+ };
56+
57+ let number = if prefixed { &word[2..] } else { &word };
58+
59+ let value = i128::from_str_radix(number, radix).ok()?;
60+ if (value.is_positive() && value.leading_zeros() < 64)
61+ || (value.is_negative() && value.leading_ones() < 64)
62+ {
63+ return None;
64+ }
65+
66+ let value = value as i64;
67+ Some(NumberIncrementor {
68+ range,
69+ value,
70+ radix,
71+ text,
72+ })
73+ }
74+
75+ /// Add `amount` to the number and return the formatted text.
76+ pub fn incremented_text(&self, amount: i64) -> Tendril {
77+ let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
78+ let old_length = old_text.len();
79+ let new_value = self.value.wrapping_add(amount);
80+
81+ // Get separator indexes from right to left.
82+ let separator_rtl_indexes: Vec<usize> = old_text
83+ .chars()
84+ .rev()
85+ .enumerate()
86+ .filter_map(|(i, c)| if c == '_' { Some(i) } else { None })
87+ .collect();
88+
89+ let format_length = if self.radix == 10 {
90+ match (self.value.is_negative(), new_value.is_negative()) {
91+ (true, false) => old_length - 1,
92+ (false, true) => old_length + 1,
93+ _ => old_text.len(),
94+ }
95+ } else {
96+ old_text.len() - 2
97+ } - separator_rtl_indexes.len();
98+
99+ let mut new_text = match self.radix {
100+ 2 => format!("0b{:01$b}", new_value, format_length),
101+ 8 => format!("0o{:01$o}", new_value, format_length),
102+ 10 if old_text.starts_with('0') || old_text.starts_with("-0") => {
103+ format!("{:01$}", new_value, format_length)
104+ }
105+ 10 => format!("{}", new_value),
106+ 16 => {
107+ let (lower_count, upper_count): (usize, usize) =
108+ old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| {
109+ (
110+ lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0),
111+ upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0),
112+ )
113+ });
114+ if upper_count > lower_count {
115+ format!("0x{:01$X}", new_value, format_length)
116+ } else {
117+ format!("0x{:01$x}", new_value, format_length)
118+ }
119+ }
120+ _ => unimplemented!("radix not supported: {}", self.radix),
121+ };
122+
123+ // Add separators from original number.
124+ for &rtl_index in &separator_rtl_indexes {
125+ if rtl_index < new_text.len() {
126+ let new_index = new_text.len() - rtl_index;
127+ new_text.insert(new_index, '_');
128+ }
129+ }
130+
131+ // Add in additional separators if necessary.
132+ if new_text.len() > old_length && !separator_rtl_indexes.is_empty() {
133+ let spacing = match separator_rtl_indexes.as_slice() {
134+ [.., b, a] => a - b - 1,
135+ _ => separator_rtl_indexes[0],
136+ };
137+
138+ let prefix_length = if self.radix == 10 { 0 } else { 2 };
139+ if let Some(mut index) = new_text.find('_') {
140+ while index - prefix_length > spacing {
141+ index -= spacing;
142+ new_text.insert(index, '_');
143+ }
144+ }
145+ }
146+
147+ new_text.into()
148+ }
149+}
150+
151+#[cfg(test)]
152+mod test {
153+ use super::*;
154+ use crate::Rope;
155+
156+ #[test]
157+ fn test_decimal_at_point() {
158+ let rope = Rope::from_str("Test text 12345 more text.");
159+ let range = Range::point(12);
160+ assert_eq!(
161+ NumberIncrementor::from_range(rope.slice(..), range),
162+ Some(NumberIncrementor {
163+ range: Range::new(10, 15),
164+ value: 12345,
165+ radix: 10,
166+ text: rope.slice(..),
167+ })
168+ );
169+ }
170+
171+ #[test]
172+ fn test_uppercase_hexadecimal_at_point() {
173+ let rope = Rope::from_str("Test text 0x123ABCDEF more text.");
174+ let range = Range::point(12);
175+ assert_eq!(
176+ NumberIncrementor::from_range(rope.slice(..), range),
177+ Some(NumberIncrementor {
178+ range: Range::new(10, 21),
179+ value: 0x123ABCDEF,
180+ radix: 16,
181+ text: rope.slice(..),
182+ })
183+ );
184+ }
185+
186+ #[test]
187+ fn test_lowercase_hexadecimal_at_point() {
188+ let rope = Rope::from_str("Test text 0xfa3b4e more text.");
189+ let range = Range::point(12);
190+ assert_eq!(
191+ NumberIncrementor::from_range(rope.slice(..), range),
192+ Some(NumberIncrementor {
193+ range: Range::new(10, 18),
194+ value: 0xfa3b4e,
195+ radix: 16,
196+ text: rope.slice(..),
197+ })
198+ );
199+ }
200+
201+ #[test]
202+ fn test_octal_at_point() {
203+ let rope = Rope::from_str("Test text 0o1074312 more text.");
204+ let range = Range::point(12);
205+ assert_eq!(
206+ NumberIncrementor::from_range(rope.slice(..), range),
207+ Some(NumberIncrementor {
208+ range: Range::new(10, 19),
209+ value: 0o1074312,
210+ radix: 8,
211+ text: rope.slice(..),
212+ })
213+ );
214+ }
215+
216+ #[test]
217+ fn test_binary_at_point() {
218+ let rope = Rope::from_str("Test text 0b10111010010101 more text.");
219+ let range = Range::point(12);
220+ assert_eq!(
221+ NumberIncrementor::from_range(rope.slice(..), range),
222+ Some(NumberIncrementor {
223+ range: Range::new(10, 26),
224+ value: 0b10111010010101,
225+ radix: 2,
226+ text: rope.slice(..),
227+ })
228+ );
229+ }
230+
231+ #[test]
232+ fn test_negative_decimal_at_point() {
233+ let rope = Rope::from_str("Test text -54321 more text.");
234+ let range = Range::point(12);
235+ assert_eq!(
236+ NumberIncrementor::from_range(rope.slice(..), range),
237+ Some(NumberIncrementor {
238+ range: Range::new(10, 16),
239+ value: -54321,
240+ radix: 10,
241+ text: rope.slice(..),
242+ })
243+ );
244+ }
245+
246+ #[test]
247+ fn test_decimal_with_leading_zeroes_at_point() {
248+ let rope = Rope::from_str("Test text 000045326 more text.");
249+ let range = Range::point(12);
250+ assert_eq!(
251+ NumberIncrementor::from_range(rope.slice(..), range),
252+ Some(NumberIncrementor {
253+ range: Range::new(10, 19),
254+ value: 45326,
255+ radix: 10,
256+ text: rope.slice(..),
257+ })
258+ );
259+ }
260+
261+ #[test]
262+ fn test_negative_decimal_cursor_on_minus_sign() {
263+ let rope = Rope::from_str("Test text -54321 more text.");
264+ let range = Range::point(10);
265+ assert_eq!(
266+ NumberIncrementor::from_range(rope.slice(..), range),
267+ Some(NumberIncrementor {
268+ range: Range::new(10, 16),
269+ value: -54321,
270+ radix: 10,
271+ text: rope.slice(..),
272+ })
273+ );
274+ }
275+
276+ #[test]
277+ fn test_number_under_range_start_of_rope() {
278+ let rope = Rope::from_str("100");
279+ let range = Range::point(0);
280+ assert_eq!(
281+ NumberIncrementor::from_range(rope.slice(..), range),
282+ Some(NumberIncrementor {
283+ range: Range::new(0, 3),
284+ value: 100,
285+ radix: 10,
286+ text: rope.slice(..),
287+ })
288+ );
289+ }
290+
291+ #[test]
292+ fn test_number_under_range_end_of_rope() {
293+ let rope = Rope::from_str("100");
294+ let range = Range::point(2);
295+ assert_eq!(
296+ NumberIncrementor::from_range(rope.slice(..), range),
297+ Some(NumberIncrementor {
298+ range: Range::new(0, 3),
299+ value: 100,
300+ radix: 10,
301+ text: rope.slice(..),
302+ })
303+ );
304+ }
305+
306+ #[test]
307+ fn test_number_surrounded_by_punctuation() {
308+ let rope = Rope::from_str(",100;");
309+ let range = Range::point(1);
310+ assert_eq!(
311+ NumberIncrementor::from_range(rope.slice(..), range),
312+ Some(NumberIncrementor {
313+ range: Range::new(1, 4),
314+ value: 100,
315+ radix: 10,
316+ text: rope.slice(..),
317+ })
318+ );
319+ }
320+
321+ #[test]
322+ fn test_not_a_number_point() {
323+ let rope = Rope::from_str("Test text 45326 more text.");
324+ let range = Range::point(6);
325+ assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
326+ }
327+
328+ #[test]
329+ fn test_number_too_large_at_point() {
330+ let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text.");
331+ let range = Range::point(12);
332+ assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
333+ }
334+
335+ #[test]
336+ fn test_number_cursor_one_right_of_number() {
337+ let rope = Rope::from_str("100 ");
338+ let range = Range::point(3);
339+ assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
340+ }
341+
342+ #[test]
343+ fn test_number_cursor_one_left_of_number() {
344+ let rope = Rope::from_str(" 100");
345+ let range = Range::point(0);
346+ assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
347+ }
348+
349+ #[test]
350+ fn test_increment_basic_decimal_numbers() {
351+ let tests = [
352+ ("100", 1, "101"),
353+ ("100", -1, "99"),
354+ ("99", 1, "100"),
355+ ("100", 1000, "1100"),
356+ ("100", -1000, "-900"),
357+ ("-1", 1, "0"),
358+ ("-1", 2, "1"),
359+ ("1", -1, "0"),
360+ ("1", -2, "-1"),
361+ ];
362+
363+ for (original, amount, expected) in tests {
364+ let rope = Rope::from_str(original);
365+ let range = Range::point(0);
366+ assert_eq!(
367+ NumberIncrementor::from_range(rope.slice(..), range)
368+ .unwrap()
369+ .incremented_text(amount),
370+ expected.into()
371+ );
372+ }
373+ }
374+
375+ #[test]
376+ fn test_increment_basic_hexadedimal_numbers() {
377+ let tests = [
378+ ("0x0100", 1, "0x0101"),
379+ ("0x0100", -1, "0x00ff"),
380+ ("0x0001", -1, "0x0000"),
381+ ("0x0000", -1, "0xffffffffffffffff"),
382+ ("0xffffffffffffffff", 1, "0x0000000000000000"),
383+ ("0xffffffffffffffff", 2, "0x0000000000000001"),
384+ ("0xffffffffffffffff", -1, "0xfffffffffffffffe"),
385+ ("0xABCDEF1234567890", 1, "0xABCDEF1234567891"),
386+ ("0xabcdef1234567890", 1, "0xabcdef1234567891"),
387+ ];
388+
389+ for (original, amount, expected) in tests {
390+ let rope = Rope::from_str(original);
391+ let range = Range::point(0);
392+ assert_eq!(
393+ NumberIncrementor::from_range(rope.slice(..), range)
394+ .unwrap()
395+ .incremented_text(amount),
396+ expected.into()
397+ );
398+ }
399+ }
400+
401+ #[test]
402+ fn test_increment_basic_octal_numbers() {
403+ let tests = [
404+ ("0o0107", 1, "0o0110"),
405+ ("0o0110", -1, "0o0107"),
406+ ("0o0001", -1, "0o0000"),
407+ ("0o7777", 1, "0o10000"),
408+ ("0o1000", -1, "0o0777"),
409+ ("0o0107", 10, "0o0121"),
410+ ("0o0000", -1, "0o1777777777777777777777"),
411+ ("0o1777777777777777777777", 1, "0o0000000000000000000000"),
412+ ("0o1777777777777777777777", 2, "0o0000000000000000000001"),
413+ ("0o1777777777777777777777", -1, "0o1777777777777777777776"),
414+ ];
415+
416+ for (original, amount, expected) in tests {
417+ let rope = Rope::from_str(original);
418+ let range = Range::point(0);
419+ assert_eq!(
420+ NumberIncrementor::from_range(rope.slice(..), range)
421+ .unwrap()
422+ .incremented_text(amount),
423+ expected.into()
424+ );
425+ }
426+ }
427+
428+ #[test]
429+ fn test_increment_basic_binary_numbers() {
430+ let tests = [
431+ ("0b00000100", 1, "0b00000101"),
432+ ("0b00000100", -1, "0b00000011"),
433+ ("0b00000100", 2, "0b00000110"),
434+ ("0b00000100", -2, "0b00000010"),
435+ ("0b00000001", -1, "0b00000000"),
436+ ("0b00111111", 10, "0b01001001"),
437+ ("0b11111111", 1, "0b100000000"),
438+ ("0b10000000", -1, "0b01111111"),
439+ (
440+ "0b0000",
441+ -1,
442+ "0b1111111111111111111111111111111111111111111111111111111111111111",
443+ ),
444+ (
445+ "0b1111111111111111111111111111111111111111111111111111111111111111",
446+ 1,
447+ "0b0000000000000000000000000000000000000000000000000000000000000000",
448+ ),
449+ (
450+ "0b1111111111111111111111111111111111111111111111111111111111111111",
451+ 2,
452+ "0b0000000000000000000000000000000000000000000000000000000000000001",
453+ ),
454+ (
455+ "0b1111111111111111111111111111111111111111111111111111111111111111",
456+ -1,
457+ "0b1111111111111111111111111111111111111111111111111111111111111110",
458+ ),
459+ ];
460+
461+ for (original, amount, expected) in tests {
462+ let rope = Rope::from_str(original);
463+ let range = Range::point(0);
464+ assert_eq!(
465+ NumberIncrementor::from_range(rope.slice(..), range)
466+ .unwrap()
467+ .incremented_text(amount),
468+ expected.into()
469+ );
470+ }
471+ }
472+
473+ #[test]
474+ fn test_increment_with_separators() {
475+ let tests = [
476+ ("999_999", 1, "1_000_000"),
477+ ("1_000_000", -1, "999_999"),
478+ ("-999_999", -1, "-1_000_000"),
479+ ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
480+ ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
481+ ("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
482+ ("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"),
483+ ("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"),
484+ ("0b01111111_11111111", 1, "0b10000000_00000000"),
485+ ("0b11111111_11111111", 1, "0b1_00000000_00000000"),
486+ ];
487+
488+ for (original, amount, expected) in tests {
489+ let rope = Rope::from_str(original);
490+ let range = Range::point(0);
491+ assert_eq!(
492+ NumberIncrementor::from_range(rope.slice(..), range)
493+ .unwrap()
494+ .incremented_text(amount),
495+ expected.into()
496+ );
497+ }
498+ }
499+}
helix-core/src/path.rs+2−2
@@ -40,7 +40,6 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
40/// needs to improve on.
40/// needs to improve on.
41/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
41/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
42pub fn get_normalized_path(path: &Path) -> PathBuf {
42pub fn get_normalized_path(path: &Path) -> PathBuf {
43− let path = expand_tilde(path);
44 let mut components = path.components().peekable();
43 let mut components = path.components().peekable();
45 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
44 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
46 components.next();
45 components.next();
@@ -72,10 +71,11 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
72/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
71/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
73/// here if the path exists, just normalize it's components.
72/// here if the path exists, just normalize it's components.
74pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
73pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
74+ let path = expand_tilde(path);
75 let path = if path.is_relative() {
75 let path = if path.is_relative() {
76 std::env::current_dir().map(|current_dir| current_dir.join(path))?
76 std::env::current_dir().map(|current_dir| current_dir.join(path))?
77 } else {
77 } else {
78− path.to_path_buf()
78+ path
79 };
79 };
80
80
81 Ok(get_normalized_path(path.as_path()))
81 Ok(get_normalized_path(path.as_path()))
helix-core/src/selection.rs+4−3
@@ -360,7 +360,7 @@ impl Selection {
360 self.normalize()
360 self.normalize()
361 }
361 }
362
362
363− /// Adds a new range to the selection and makes it the primary range.
363+ /// Removes a range from the selection.
364 pub fn remove(mut self, index: usize) -> Self {
364 pub fn remove(mut self, index: usize) -> Self {
365 assert!(
365 assert!(
366 self.ranges.len() > 1,
366 self.ranges.len() > 1,
@@ -528,14 +528,15 @@ impl<'a> IntoIterator for &'a Selection {
528
528
529// TODO: checkSelection -> check if valid for doc length && sorted
529// TODO: checkSelection -> check if valid for doc length && sorted
530
530
531−pub fn keep_matches(
531+pub fn keep_or_remove_matches(
532 text: RopeSlice,
532 text: RopeSlice,
533 selection: &Selection,
533 selection: &Selection,
534 regex: &crate::regex::Regex,
534 regex: &crate::regex::Regex,
535+ remove: bool,
535) -> Option<Selection> {
536) -> Option<Selection> {
536 let result: SmallVec<_> = selection
537 let result: SmallVec<_> = selection
537 .iter()
538 .iter()
538− .filter(|range| regex.is_match(&range.fragment(text)))
539+ .filter(|range| regex.is_match(&range.fragment(text)) ^ remove)
539 .copied()
540 .copied()
540 .collect();
541 .collect();
541
542
helix-core/src/syntax.rs+22−1
@@ -40,18 +40,21 @@ where
40}
40}
41
41
42#[derive(Debug, Serialize, Deserialize)]
42#[derive(Debug, Serialize, Deserialize)]
43+#[serde(deny_unknown_fields)]
43pub struct Configuration {
44pub struct Configuration {
44 pub language: Vec<LanguageConfiguration>,
45 pub language: Vec<LanguageConfiguration>,
45}
46}
46
47
47// largely based on tree-sitter/cli/src/loader.rs
48// largely based on tree-sitter/cli/src/loader.rs
48#[derive(Debug, Serialize, Deserialize)]
49#[derive(Debug, Serialize, Deserialize)]
49−#[serde(rename_all = "kebab-case")]
50+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
50pub struct LanguageConfiguration {
51pub struct LanguageConfiguration {
51 #[serde(rename = "name")]
52 #[serde(rename = "name")]
52 pub language_id: String,
53 pub language_id: String,
53 pub scope: String, // source.rust
54 pub scope: String, // source.rust
54 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
55 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
56+ #[serde(default)]
57+ pub shebangs: Vec<String>, // interpreter(s) associated with language
55 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
58 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
56 pub comment_token: Option<String>,
59 pub comment_token: Option<String>,
57
60
@@ -309,6 +312,7 @@ pub struct Loader {
309 // highlight_names ?
312 // highlight_names ?
310 language_configs: Vec<Arc<LanguageConfiguration>>,
313 language_configs: Vec<Arc<LanguageConfiguration>>,
311 language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
314 language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
315+ language_config_ids_by_shebang: HashMap<String, usize>,
312}
316}
313
317
314impl Loader {
318impl Loader {
@@ -316,6 +320,7 @@ impl Loader {
316 let mut loader = Self {
320 let mut loader = Self {
317 language_configs: Vec::new(),
321 language_configs: Vec::new(),
318 language_config_ids_by_file_type: HashMap::new(),
322 language_config_ids_by_file_type: HashMap::new(),
323+ language_config_ids_by_shebang: HashMap::new(),
319 };
324 };
320
325
321 for config in config.language {
326 for config in config.language {
@@ -328,6 +333,11 @@ impl Loader {
328 .language_config_ids_by_file_type
333 .language_config_ids_by_file_type
329 .insert(file_type.clone(), language_id);
334 .insert(file_type.clone(), language_id);
330 }
335 }
336+ for shebang in &config.shebangs {
337+ loader
338+ .language_config_ids_by_shebang
339+ .insert(shebang.clone(), language_id);
340+ }
331
341
332 loader.language_configs.push(Arc::new(config));
342 loader.language_configs.push(Arc::new(config));
333 }
343 }
@@ -353,6 +363,17 @@ impl Loader {
353 // TODO: content_regex handling conflict resolution
363 // TODO: content_regex handling conflict resolution
354 }
364 }
355
365
366+ pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
367+ let line = Cow::from(source.line(0));
368+ static SHEBANG_REGEX: Lazy<Regex> =
369+ Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
370+ let configuration_id = SHEBANG_REGEX
371+ .captures(&line)
372+ .and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
373+
374+ configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
375+ }
376+
356 pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> {
377 pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> {
357 self.language_configs
378 self.language_configs
358 .iter()
379 .iter()
helix-core/src/textobject.rs+6−5
@@ -10,7 +10,7 @@ use crate::surround;
10use crate::syntax::LanguageConfiguration;
10use crate::syntax::LanguageConfiguration;
11use crate::Range;
11use crate::Range;
12
12
13−fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
13+fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
14 use CharCategory::{Eol, Whitespace};
14 use CharCategory::{Eol, Whitespace};
15
15
16 let iter = match direction {
16 let iter = match direction {
@@ -33,7 +33,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) ->
33 match categorize_char(ch) {
33 match categorize_char(ch) {
34 Eol | Whitespace => return pos,
34 Eol | Whitespace => return pos,
35 category => {
35 category => {
36− if category != prev_category && pos != 0 && pos != slice.len_chars() {
36+ if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
37 return pos;
37 return pos;
38 } else {
38 } else {
39 match direction {
39 match direction {
@@ -70,13 +70,14 @@ pub fn textobject_word(
70 range: Range,
70 range: Range,
71 textobject: TextObject,
71 textobject: TextObject,
72 _count: usize,
72 _count: usize,
73+ long: bool,
73) -> Range {
74) -> Range {
74 let pos = range.cursor(slice);
75 let pos = range.cursor(slice);
75
76
76− let word_start = find_word_boundary(slice, pos, Direction::Backward);
77+ let word_start = find_word_boundary(slice, pos, Direction::Backward, long);
77 let word_end = match slice.get_char(pos).map(categorize_char) {
78 let word_end = match slice.get_char(pos).map(categorize_char) {
78 None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
79 None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
79− _ => find_word_boundary(slice, pos + 1, Direction::Forward),
80+ _ => find_word_boundary(slice, pos + 1, Direction::Forward, long),
80 };
81 };
81
82
82 // Special case.
83 // Special case.
@@ -268,7 +269,7 @@ mod test {
268 let slice = doc.slice(..);
269 let slice = doc.slice(..);
269 for &case in scenario {
270 for &case in scenario {
270 let (pos, objtype, expected_range) = case;
271 let (pos, objtype, expected_range) = case;
271− let result = textobject_word(slice, Range::point(pos), objtype, 1);
272+ let result = textobject_word(slice, Range::point(pos), objtype, 1, false);
272 assert_eq!(
273 assert_eq!(
273 result,
274 result,
274 expected_range.into(),
275 expected_range.into(),
helix-lsp/src/client.rs+27
@@ -257,6 +257,12 @@ impl Client {
257 content_format: Some(vec![lsp::MarkupKind::Markdown]),
257 content_format: Some(vec![lsp::MarkupKind::Markdown]),
258 ..Default::default()
258 ..Default::default()
259 }),
259 }),
260+ rename: Some(lsp::RenameClientCapabilities {
261+ dynamic_registration: Some(false),
262+ prepare_support: Some(false),
263+ prepare_support_default_behavior: None,
264+ honors_change_annotations: Some(false),
265+ }),
260 code_action: Some(lsp::CodeActionClientCapabilities {
266 code_action: Some(lsp::CodeActionClientCapabilities {
261 code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
267 code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
262 code_action_kind: lsp::CodeActionKindLiteralSupport {
268 code_action_kind: lsp::CodeActionKindLiteralSupport {
@@ -773,4 +779,25 @@ impl Client {
773
779
774 self.call::<lsp::request::CodeActionRequest>(params)
780 self.call::<lsp::request::CodeActionRequest>(params)
775 }
781 }
782+
783+ pub async fn rename_symbol(
784+ &self,
785+ text_document: lsp::TextDocumentIdentifier,
786+ position: lsp::Position,
787+ new_name: String,
788+ ) -> anyhow::Result<lsp::WorkspaceEdit> {
789+ let params = lsp::RenameParams {
790+ text_document_position: lsp::TextDocumentPositionParams {
791+ text_document,
792+ position,
793+ },
794+ new_name,
795+ work_done_progress_params: lsp::WorkDoneProgressParams {
796+ work_done_token: None,
797+ },
798+ };
799+
800+ let response = self.request::<lsp::request::Rename>(params).await?;
801+ Ok(response.unwrap_or_default())
802+ }
776}
803}
helix-syntax/languages/tree-sitter-glslnew file+1

Submodule changed.

helix-syntax/languages/tree-sitter-perlnew file+1

Submodule changed.

helix-term/build.rsnew file+12
@@ -0,0 +1,12 @@
1+use std::process::Command;
2+
3+fn main() {
4+ let git_hash = Command::new("git")
5+ .args(&["describe", "--dirty"])
6+ .output()
7+ .map(|x| String::from_utf8(x.stdout).ok())
8+ .ok()
9+ .flatten()
10+ .unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION")));
11+ println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash);
12+}
helix-term/src/application.rs+37−27
@@ -9,7 +9,7 @@ use crate::{
9
9
10use log::{error, warn};
10use log::{error, warn};
11use std::{
11use std::{
12− io::{stdout, Write},
12+ io::{stdin, stdout, Write},
13 sync::Arc,
13 sync::Arc,
14 time::{Duration, Instant},
14 time::{Duration, Instant},
15};
15};
@@ -19,6 +19,7 @@ use anyhow::Error;
19use crossterm::{
19use crossterm::{
20 event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
20 event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
21 execute, terminal,
21 execute, terminal,
22+ tty::IsTty,
22};
23};
23#[cfg(not(windows))]
24#[cfg(not(windows))]
24use {
25use {
@@ -62,14 +63,19 @@ impl Application {
62 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
63 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
63
64
64 // load default and user config, and merge both
65 // load default and user config, and merge both
65− let def_lang_conf: toml::Value = toml::from_slice(include_bytes!("../../languages.toml"))
66+ let builtin_err_msg =
66− .expect("Could not parse built-in languages.toml, something must be very wrong");
67+ "Could not parse built-in languages.toml, something must be very wrong";
68+ let def_lang_conf: toml::Value =
69+ toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg);
70+ let def_syn_loader_conf: helix_core::syntax::Configuration =
71+ def_lang_conf.clone().try_into().expect(builtin_err_msg);
67− let user_lang_conf: Option<toml::Value> = std::fs::read(conf_dir.join("languages.toml"))
72+ let user_lang_conf = std::fs::read(conf_dir.join("languages.toml"))
68 .ok()
73 .ok()
69− .map(|raw| toml::from_slice(&raw).expect("Could not parse user languages.toml"));
74+ .map(|raw| toml::from_slice(&raw));
70 let lang_conf = match user_lang_conf {
75 let lang_conf = match user_lang_conf {
71− Some(value) => merge_toml_values(def_lang_conf, value),
76+ Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
77+ Some(err @ Err(_)) => err,
72− None => def_lang_conf,
78+ None => Ok(def_lang_conf),
73 };
79 };
74
80
75 let theme = if let Some(theme) = &config.theme {
81 let theme = if let Some(theme) = &config.theme {
@@ -85,8 +91,15 @@ impl Application {
85 };
91 };
86
92
87 let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
93 let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
88− .try_into()
94+ .and_then(|conf| conf.try_into())
89− .expect("Could not parse merged (built-in + user) languages.toml");
95+ .unwrap_or_else(|err| {
96+ eprintln!("Bad language config: {}", err);
97+ eprintln!("Press <ENTER> to continue with default language config");
98+ use std::io::Read;
99+ // This waits for an enter press.
100+ let _ = std::io::stdin().read(&mut []);
101+ def_syn_loader_conf
102+ });
90 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
103 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
91
104
92 let mut editor = Editor::new(
105 let mut editor = Editor::new(
@@ -124,8 +137,17 @@ impl Application {
124 }
137 }
125 editor.set_status(format!("Loaded {} files.", nr_of_files));
138 editor.set_status(format!("Loaded {} files.", nr_of_files));
126 }
139 }
127− } else {
140+ } else if stdin().is_tty() {
128 editor.new_file(Action::VerticalSplit);
141 editor.new_file(Action::VerticalSplit);
142+ } else if cfg!(target_os = "macos") {
143+ // On Linux and Windows, we allow the output of a command to be piped into the new buffer.
144+ // This doesn't currently work on macOS because of the following issue:
145+ // https://github.com/crossterm-rs/crossterm/issues/500
146+ anyhow::bail!("Piping into helix-term is currently not supported on macOS");
147+ } else {
148+ editor
149+ .new_file_from_stdin(Action::VerticalSplit)
150+ .unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
129 }
151 }
130
152
131 editor.set_theme(theme);
153 editor.set_theme(theme);
@@ -253,12 +275,8 @@ impl Application {
253 }
275 }
254 let editor_view = self
276 let editor_view = self
255 .compositor
277 .compositor
256− .find(std::any::type_name::<ui::EditorView>())
278+ .find::<ui::EditorView>()
257 .expect("expected at least one EditorView");
279 .expect("expected at least one EditorView");
258− let editor_view = editor_view
259− .as_any_mut()
260− .downcast_mut::<ui::EditorView>()
261− .unwrap();
262
280
263 if editor_view.completion.is_some() {
281 if editor_view.completion.is_some() {
264 return;
282 return;
@@ -583,12 +601,8 @@ impl Application {
583 {
601 {
584 let editor_view = self
602 let editor_view = self
585 .compositor
603 .compositor
586− .find(std::any::type_name::<ui::EditorView>())
604+ .find::<ui::EditorView>()
587 .expect("expected at least one EditorView");
605 .expect("expected at least one EditorView");
588− let editor_view = editor_view
589− .as_any_mut()
590− .downcast_mut::<ui::EditorView>()
591− .unwrap();
592 let lsp::ProgressParams { token, value } = params;
606 let lsp::ProgressParams { token, value } = params;
593
607
594 let lsp::ProgressParamsValue::WorkDone(work) = value;
608 let lsp::ProgressParamsValue::WorkDone(work) = value;
@@ -702,12 +716,8 @@ impl Application {
702
716
703 let editor_view = self
717 let editor_view = self
704 .compositor
718 .compositor
705− .find(std::any::type_name::<ui::EditorView>())
719+ .find::<ui::EditorView>()
706 .expect("expected at least one EditorView");
720 .expect("expected at least one EditorView");
707− let editor_view = editor_view
708− .as_any_mut()
709− .downcast_mut::<ui::EditorView>()
710− .unwrap();
711 let spinner = editor_view.spinners_mut().get_or_create(server_id);
721 let spinner = editor_view.spinners_mut().get_or_create(server_id);
712 if spinner.is_stopped() {
722 if spinner.is_stopped() {
713 spinner.start();
723 spinner.start();
@@ -742,7 +752,7 @@ impl Application {
742 Ok(())
752 Ok(())
743 }
753 }
744
754
745− pub async fn run(&mut self) -> Result<(), Error> {
755+ pub async fn run(&mut self) -> Result<i32, Error> {
746 self.claim_term().await?;
756 self.claim_term().await?;
747
757
748 // Exit the alternate screen and disable raw mode before panicking
758 // Exit the alternate screen and disable raw mode before panicking
@@ -765,6 +775,6 @@ impl Application {
765
775
766 self.restore_term()?;
776 self.restore_term()?;
767
777
768− Ok(())
778+ Ok(self.editor.exit_code)
769 }
779 }
770}
780}
helix-term/src/commands.rs+697−132
@@ -3,21 +3,25 @@ pub(crate) mod dap;
3pub use dap::*;
3pub use dap::*;
4
4
5use helix_core::{
5use helix_core::{
6− comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, indent,
6+ comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes,
7+ history::UndoKind,
8+ indent,
7 indent::IndentStyle,
9 indent::IndentStyle,
8 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
10 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
9 match_brackets,
11 match_brackets,
10 movement::{self, Direction},
12 movement::{self, Direction},
13+ numbers::NumberIncrementor,
11 object, pos_at_coords,
14 object, pos_at_coords,
12 regex::{self, Regex, RegexBuilder},
15 regex::{self, Regex, RegexBuilder},
13 register::Register,
16 register::Register,
14− search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
17+ search, selection, surround, textobject,
18+ unicode::width::UnicodeWidthChar,
15− RopeSlice, Selection, SmallVec, Tendril, Transaction,
19+ LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
20+ Transaction,
16};
21};
17−
18use helix_view::{
22use helix_view::{
19 clipboard::ClipboardType,
23 clipboard::ClipboardType,
20− document::Mode,
24+ document::{Mode, SCRATCH_BUFFER_NAME},
21 editor::{Action, Motion},
25 editor::{Action, Motion},
22 input::KeyEvent,
26 input::KeyEvent,
23 keyboard::KeyCode,
27 keyboard::KeyCode,
@@ -27,7 +31,7 @@ use helix_view::{
27
31
28use anyhow::{anyhow, bail, Context as _};
32use anyhow::{anyhow, bail, Context as _};
29use helix_lsp::{
33use helix_lsp::{
30− lsp,
34+ block_on, lsp,
31 util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
35 util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
32 OffsetEncoding,
36 OffsetEncoding,
33};
37};
@@ -189,6 +193,7 @@ impl Command {
189 copy_selection_on_prev_line, "Copy selection on previous line",
193 copy_selection_on_prev_line, "Copy selection on previous line",
190 move_next_word_start, "Move to beginning of next word",
194 move_next_word_start, "Move to beginning of next word",
191 move_prev_word_start, "Move to beginning of previous word",
195 move_prev_word_start, "Move to beginning of previous word",
196+ move_prev_word_end, "Move to end of previous word",
192 move_next_word_end, "Move to end of next word",
197 move_next_word_end, "Move to end of next word",
193 move_next_long_word_start, "Move to beginning of next long word",
198 move_next_long_word_start, "Move to beginning of next long word",
194 move_prev_long_word_start, "Move to beginning of previous long word",
199 move_prev_long_word_start, "Move to beginning of previous long word",
@@ -241,6 +246,7 @@ impl Command {
241 code_action, "Perform code action",
246 code_action, "Perform code action",
242 buffer_picker, "Open buffer picker",
247 buffer_picker, "Open buffer picker",
243 symbol_picker, "Open symbol picker",
248 symbol_picker, "Open symbol picker",
249+ workspace_symbol_picker, "Open workspace symbol picker",
244 last_picker, "Open last picker",
250 last_picker, "Open last picker",
245 prepend_to_line, "Insert at start of line",
251 prepend_to_line, "Insert at start of line",
246 append_to_line, "Insert at end of line",
252 append_to_line, "Insert at end of line",
@@ -261,6 +267,7 @@ impl Command {
261 goto_window_middle, "Goto window middle",
267 goto_window_middle, "Goto window middle",
262 goto_window_bottom, "Goto window bottom",
268 goto_window_bottom, "Goto window bottom",
263 goto_last_accessed_file, "Goto last accessed file",
269 goto_last_accessed_file, "Goto last accessed file",
270+ goto_last_modification, "Goto last modification",
264 goto_line, "Goto line",
271 goto_line, "Goto line",
265 goto_last_line, "Goto last line",
272 goto_last_line, "Goto last line",
266 goto_first_diag, "Goto first diagnostic",
273 goto_first_diag, "Goto first diagnostic",
@@ -274,6 +281,7 @@ impl Command {
274 // TODO: different description ?
281 // TODO: different description ?
275 goto_line_end_newline, "Goto line end",
282 goto_line_end_newline, "Goto line end",
276 goto_first_nonwhitespace, "Goto first non-blank in line",
283 goto_first_nonwhitespace, "Goto first non-blank in line",
284+ trim_selections, "Trim whitespace from selections",
277 extend_to_line_start, "Extend to line start",
285 extend_to_line_start, "Extend to line start",
278 extend_to_line_end, "Extend to line end",
286 extend_to_line_end, "Extend to line end",
279 extend_to_line_end_newline, "Extend to line end",
287 extend_to_line_end_newline, "Extend to line end",
@@ -283,8 +291,13 @@ impl Command {
283 delete_char_backward, "Delete previous char",
291 delete_char_backward, "Delete previous char",
284 delete_char_forward, "Delete next char",
292 delete_char_forward, "Delete next char",
285 delete_word_backward, "Delete previous word",
293 delete_word_backward, "Delete previous word",
294+ delete_word_forward, "Delete next word",
295+ kill_to_line_start, "Delete content till the start of the line",
296+ kill_to_line_end, "Delete content till the end of the line",
286 undo, "Undo change",
297 undo, "Undo change",
287 redo, "Redo change",
298 redo, "Redo change",
299+ earlier, "Move backward in history",
300+ later, "Move forward in history",
288 yank, "Yank selection",
301 yank, "Yank selection",
289 yank_joined_to_clipboard, "Join and yank selections to clipboard",
302 yank_joined_to_clipboard, "Join and yank selections to clipboard",
290 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
303 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -304,6 +317,7 @@ impl Command {
304 format_selections, "Format selection",
317 format_selections, "Format selection",
305 join_selections, "Join lines inside selection",
318 join_selections, "Join lines inside selection",
306 keep_selections, "Keep selections matching regex",
319 keep_selections, "Keep selections matching regex",
320+ remove_selections, "Remove selections matching regex",
307 keep_primary_selection, "Keep primary selection",
321 keep_primary_selection, "Keep primary selection",
308 remove_primary_selection, "Remove primary selection",
322 remove_primary_selection, "Remove primary selection",
309 completion, "Invoke completion popup",
323 completion, "Invoke completion popup",
@@ -324,7 +338,9 @@ impl Command {
324 hsplit, "Horizontal bottom split",
338 hsplit, "Horizontal bottom split",
325 vsplit, "Vertical right split",
339 vsplit, "Vertical right split",
326 wclose, "Close window",
340 wclose, "Close window",
341+ wonly, "Current window only",
327 select_register, "Select register",
342 select_register, "Select register",
343+ insert_register, "Insert register",
328 align_view_middle, "Align view middle",
344 align_view_middle, "Align view middle",
329 align_view_top, "Align view top",
345 align_view_top, "Align view top",
330 align_view_center, "Align view center",
346 align_view_center, "Align view center",
@@ -358,6 +374,9 @@ impl Command {
358 shell_append_output, "Append output of shell command after each selection",
374 shell_append_output, "Append output of shell command after each selection",
359 shell_keep_pipe, "Filter selections with shell predicate",
375 shell_keep_pipe, "Filter selections with shell predicate",
360 suspend, "Suspend",
376 suspend, "Suspend",
377+ rename_symbol, "Rename symbol",
378+ increment, "Increment",
379+ decrement, "Decrement",
361 );
380 );
362}
381}
363
382
@@ -581,6 +600,29 @@ fn extend_to_line_start(cx: &mut Context) {
581 goto_line_start_impl(view, doc, Movement::Extend)
600 goto_line_start_impl(view, doc, Movement::Extend)
582}
601}
583
602
603+fn kill_to_line_start(cx: &mut Context) {
604+ let (view, doc) = current!(cx.editor);
605+ let text = doc.text().slice(..);
606+
607+ let selection = doc.selection(view.id).clone().transform(|range| {
608+ let line = range.cursor_line(text);
609+ range.put_cursor(text, text.line_to_char(line), true)
610+ });
611+ delete_selection_insert_mode(doc, view, &selection);
612+}
613+
614+fn kill_to_line_end(cx: &mut Context) {
615+ let (view, doc) = current!(cx.editor);
616+ let text = doc.text().slice(..);
617+
618+ let selection = doc.selection(view.id).clone().transform(|range| {
619+ let line = range.cursor_line(text);
620+ let pos = line_end_char_index(&text, line);
621+ range.put_cursor(text, pos, true)
622+ });
623+ delete_selection_insert_mode(doc, view, &selection);
624+}
625+
584fn goto_first_nonwhitespace(cx: &mut Context) {
626fn goto_first_nonwhitespace(cx: &mut Context) {
585 let (view, doc) = current!(cx.editor);
627 let (view, doc) = current!(cx.editor);
586 let text = doc.text().slice(..);
628 let text = doc.text().slice(..);
@@ -598,6 +640,42 @@ fn goto_first_nonwhitespace(cx: &mut Context) {
598 doc.set_selection(view.id, selection);
640 doc.set_selection(view.id, selection);
599}
641}
600
642
643+fn trim_selections(cx: &mut Context) {
644+ let (view, doc) = current!(cx.editor);
645+ let text = doc.text().slice(..);
646+
647+ let ranges: SmallVec<[Range; 1]> = doc
648+ .selection(view.id)
649+ .iter()
650+ .filter_map(|range| {
651+ if range.is_empty() || range.fragment(text).chars().all(|ch| ch.is_whitespace()) {
652+ return None;
653+ }
654+ let mut start = range.from();
655+ let mut end = range.to();
656+ start = movement::skip_while(text, start, |x| x.is_whitespace()).unwrap_or(start);
657+ end = movement::backwards_skip_while(text, end, |x| x.is_whitespace()).unwrap_or(end);
658+ if range.anchor < range.head {
659+ Some(Range::new(start, end))
660+ } else {
661+ Some(Range::new(end, start))
662+ }
663+ })
664+ .collect();
665+
666+ if !ranges.is_empty() {
667+ let primary = doc.selection(view.id).primary();
668+ let idx = ranges
669+ .iter()
670+ .position(|range| range.overlaps(&primary))
671+ .unwrap_or(ranges.len() - 1);
672+ doc.set_selection(view.id, Selection::new(ranges, idx));
673+ } else {
674+ collapse_selection(cx);
675+ keep_primary_selection(cx);
676+ };
677+}
678+
601fn goto_window(cx: &mut Context, align: Align) {
679fn goto_window(cx: &mut Context, align: Align) {
602 let (view, doc) = current!(cx.editor);
680 let (view, doc) = current!(cx.editor);
603
681
@@ -657,6 +735,10 @@ fn move_prev_word_start(cx: &mut Context) {
657 move_word_impl(cx, movement::move_prev_word_start)
735 move_word_impl(cx, movement::move_prev_word_start)
658}
736}
659
737
738+fn move_prev_word_end(cx: &mut Context) {
739+ move_word_impl(cx, movement::move_prev_word_end)
740+}
741+
660fn move_next_word_end(cx: &mut Context) {
742fn move_next_word_end(cx: &mut Context) {
661 move_word_impl(cx, movement::move_next_word_end)
743 move_word_impl(cx, movement::move_next_word_end)
662}
744}
@@ -1193,6 +1275,7 @@ fn search_impl(
1193 regex: &Regex,
1275 regex: &Regex,
1194 movement: Movement,
1276 movement: Movement,
1195 direction: Direction,
1277 direction: Direction,
1278+ scrolloff: usize,
1196) {
1279) {
1197 let text = doc.text().slice(..);
1280 let text = doc.text().slice(..);
1198 let selection = doc.selection(view.id);
1281 let selection = doc.selection(view.id);
@@ -1251,7 +1334,11 @@ fn search_impl(
1251 };
1334 };
1252
1335
1253 doc.set_selection(view.id, selection);
1336 doc.set_selection(view.id, selection);
1337+ if view.is_cursor_in_view(doc, 0) {
1338+ view.ensure_cursor_in_view(doc, scrolloff);
1339+ } else {
1254− align_view(doc, view, Align::Center);
1340+ align_view(doc, view, Align::Center)
1341+ }
1255 };
1342 };
1256}
1343}
1257
1344
@@ -1275,6 +1362,8 @@ fn rsearch(cx: &mut Context) {
1275// TODO: use one function for search vs extend
1362// TODO: use one function for search vs extend
1276fn searcher(cx: &mut Context, direction: Direction) {
1363fn searcher(cx: &mut Context, direction: Direction) {
1277 let reg = cx.register.unwrap_or('/');
1364 let reg = cx.register.unwrap_or('/');
1365+ let scrolloff = cx.editor.config.scrolloff;
1366+
1278 let (_, doc) = current!(cx.editor);
1367 let (_, doc) = current!(cx.editor);
1279
1368
1280 // TODO: could probably share with select_on_matches?
1369 // TODO: could probably share with select_on_matches?
@@ -1299,7 +1388,15 @@ fn searcher(cx: &mut Context, direction: Direction) {
1299 if event != PromptEvent::Update {
1388 if event != PromptEvent::Update {
1300 return;
1389 return;
1301 }
1390 }
1302− search_impl(doc, view, &contents, &regex, Movement::Move, direction);
1391+ search_impl(
1392+ doc,
1393+ view,
1394+ &contents,
1395+ &regex,
1396+ Movement::Move,
1397+ direction,
1398+ scrolloff,
1399+ );
1303 },
1400 },
1304 );
1401 );
1305
1402
@@ -1307,6 +1404,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
1307}
1404}
1308
1405
1309fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1406fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1407+ let scrolloff = cx.editor.config.scrolloff;
1310 let (view, doc) = current!(cx.editor);
1408 let (view, doc) = current!(cx.editor);
1311 let registers = &cx.editor.registers;
1409 let registers = &cx.editor.registers;
1312 if let Some(query) = registers.read('/') {
1410 if let Some(query) = registers.read('/') {
@@ -1321,7 +1419,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
1321 .case_insensitive(case_insensitive)
1419 .case_insensitive(case_insensitive)
1322 .build()
1420 .build()
1323 {
1421 {
1324− search_impl(doc, view, &contents, &regex, movement, direction);
1422+ search_impl(doc, view, &contents, &regex, movement, direction, scrolloff);
1325 } else {
1423 } else {
1326 // get around warning `mutable_borrow_reservation_conflict`
1424 // get around warning `mutable_borrow_reservation_conflict`
1327 // which will be a hard error in the future
1425 // which will be a hard error in the future
@@ -1365,7 +1463,7 @@ fn global_search(cx: &mut Context) {
1365 let completions = search_completions(cx, None);
1463 let completions = search_completions(cx, None);
1366 let prompt = ui::regex_prompt(
1464 let prompt = ui::regex_prompt(
1367 cx,
1465 cx,
1368− "global search:".into(),
1466+ "global-search:".into(),
1369 None,
1467 None,
1370 move |input: &str| {
1468 move |input: &str| {
1371 completions
1469 completions
@@ -1543,6 +1641,17 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId
1543 doc.apply(&transaction, view_id);
1641 doc.apply(&transaction, view_id);
1544}
1642}
1545
1643
1644+#[inline]
1645+fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Selection) {
1646+ let view_id = view.id;
1647+
1648+ // then delete
1649+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
1650+ (range.from(), range.to(), None)
1651+ });
1652+ doc.apply(&transaction, view_id);
1653+}
1654+
1546fn delete_selection(cx: &mut Context) {
1655fn delete_selection(cx: &mut Context) {
1547 let reg_name = cx.register.unwrap_or('"');
1656 let reg_name = cx.register.unwrap_or('"');
1548 let (view, doc) = current!(cx.editor);
1657 let (view, doc) = current!(cx.editor);
@@ -1657,8 +1766,7 @@ mod cmd {
1657 buffers_remaining_impl(cx.editor)?
1766 buffers_remaining_impl(cx.editor)?
1658 }
1767 }
1659
1768
1660− cx.editor
1661− .close(view!(cx.editor).id, /* close_buffer */ false);
1769+ cx.editor.close(view!(cx.editor).id);
1662
1770
1663 Ok(())
1771 Ok(())
1664 }
1772 }
@@ -1668,8 +1776,7 @@ mod cmd {
1668 _args: &[&str],
1776 _args: &[&str],
1669 _event: PromptEvent,
1777 _event: PromptEvent,
1670 ) -> anyhow::Result<()> {
1778 ) -> anyhow::Result<()> {
1671− cx.editor
1672− .close(view!(cx.editor).id, /* close_buffer */ false);
1779+ cx.editor.close(view!(cx.editor).id);
1673
1780
1674 Ok(())
1781 Ok(())
1675 }
1782 }
@@ -1679,11 +1786,30 @@ mod cmd {
1679 args: &[&str],
1786 args: &[&str],
1680 _event: PromptEvent,
1787 _event: PromptEvent,
1681 ) -> anyhow::Result<()> {
1788 ) -> anyhow::Result<()> {
1682− use helix_core::path::expand_tilde;
1683 let path = args.get(0).context("wrong argument count")?;
1789 let path = args.get(0).context("wrong argument count")?;
1684− let _ = cx
1685− .editor
1686− .open(expand_tilde(Path::new(path)), Action::Replace)?;
1790+ let _ = cx.editor.open(path.into(), Action::Replace)?;
1791+ Ok(())
1792+ }
1793+
1794+ fn buffer_close(
1795+ cx: &mut compositor::Context,
1796+ _args: &[&str],
1797+ _event: PromptEvent,
1798+ ) -> anyhow::Result<()> {
1799+ let view = view!(cx.editor);
1800+ let doc_id = view.doc;
1801+ cx.editor.close_document(doc_id, false)?;
1802+ Ok(())
1803+ }
1804+
1805+ fn force_buffer_close(
1806+ cx: &mut compositor::Context,
1807+ _args: &[&str],
1808+ _event: PromptEvent,
1809+ ) -> anyhow::Result<()> {
1810+ let view = view!(cx.editor);
1811+ let doc_id = view.doc;
1812+ cx.editor.close_document(doc_id, true)?;
1687 Ok(())
1813 Ok(())
1688 }
1814 }
1689
1815
@@ -1838,13 +1964,13 @@ mod cmd {
1838 args: &[&str],
1964 args: &[&str],
1839 _event: PromptEvent,
1965 _event: PromptEvent,
1840 ) -> anyhow::Result<()> {
1966 ) -> anyhow::Result<()> {
1841− let uk = args
1842− .join(" ")
1843− .parse::<helix_core::history::UndoKind>()
1844− .map_err(|s| anyhow!(s))?;
1967+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
1845
1968
1846 let (view, doc) = current!(cx.editor);
1969 let (view, doc) = current!(cx.editor);
1847− doc.earlier(view.id, uk);
1970+ let success = doc.earlier(view.id, uk);
1971+ if !success {
1972+ cx.editor.set_status("Already at oldest change".to_owned());
1973+ }
1848
1974
1849 Ok(())
1975 Ok(())
1850 }
1976 }
@@ -1854,12 +1980,12 @@ mod cmd {
1854 args: &[&str],
1980 args: &[&str],
1855 _event: PromptEvent,
1981 _event: PromptEvent,
1856 ) -> anyhow::Result<()> {
1982 ) -> anyhow::Result<()> {
1857− let uk = args
1858− .join(" ")
1859− .parse::<helix_core::history::UndoKind>()
1860− .map_err(|s| anyhow!(s))?;
1983+ let uk = args.join(" ").parse::<UndoKind>().map_err(|s| anyhow!(s))?;
1861 let (view, doc) = current!(cx.editor);
1984 let (view, doc) = current!(cx.editor);
1862− doc.later(view.id, uk);
1985+ let success = doc.later(view.id, uk);
1986+ if !success {
1987+ cx.editor.set_status("Already at newest change".to_owned());
1988+ }
1863
1989
1864 Ok(())
1990 Ok(())
1865 }
1991 }
@@ -1891,7 +2017,7 @@ mod cmd {
1891 .map(|doc| {
2017 .map(|doc| {
1892 doc.relative_path()
2018 doc.relative_path()
1893 .map(|path| path.to_string_lossy().to_string())
2019 .map(|path| path.to_string_lossy().to_string())
1894− .unwrap_or_else(|| "[scratch]".into())
2020+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
1895 })
2021 })
1896 .collect();
2022 .collect();
1897 if !modified.is_empty() {
2023 if !modified.is_empty() {
@@ -1933,7 +2059,7 @@ mod cmd {
1933 // close all views
2059 // close all views
1934 let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
2060 let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
1935 for view_id in views {
2061 for view_id in views {
1936− cx.editor.close(view_id, false);
2062+ cx.editor.close(view_id);
1937 }
2063 }
1938 }
2064 }
1939
2065
@@ -1977,7 +2103,7 @@ mod cmd {
1977 // close all views
2103 // close all views
1978 let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
2104 let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
1979 for view_id in views {
2105 for view_id in views {
1980− editor.close(view_id, false);
2106+ editor.close(view_id);
1981 }
2107 }
1982
2108
1983 Ok(())
2109 Ok(())
@@ -1999,6 +2125,25 @@ mod cmd {
1999 quit_all_impl(&mut cx.editor, args, event, true)
2125 quit_all_impl(&mut cx.editor, args, event, true)
2000 }
2126 }
2001
2127
2128+ fn cquit(
2129+ cx: &mut compositor::Context,
2130+ args: &[&str],
2131+ _event: PromptEvent,
2132+ ) -> anyhow::Result<()> {
2133+ let exit_code = args
2134+ .first()
2135+ .and_then(|code| code.parse::<i32>().ok())
2136+ .unwrap_or(1);
2137+ cx.editor.exit_code = exit_code;
2138+
2139+ let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
2140+ for view_id in views {
2141+ cx.editor.close(view_id);
2142+ }
2143+
2144+ Ok(())
2145+ }
2146+
2002 fn theme(
2147 fn theme(
2003 cx: &mut compositor::Context,
2148 cx: &mut compositor::Context,
2004 args: &[&str],
2149 args: &[&str],
@@ -2241,7 +2386,6 @@ mod cmd {
2241 args: &[&str],
2386 args: &[&str],
2242 _event: PromptEvent,
2387 _event: PromptEvent,
2243 ) -> anyhow::Result<()> {
2388 ) -> anyhow::Result<()> {
2244− use helix_lsp::block_on;
2245 if let Some(debugger) = cx.editor.debugger.as_mut() {
2389 if let Some(debugger) = cx.editor.debugger.as_mut() {
2246 let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
2390 let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
2247 (Some(frame), Some(thread_id)) => (frame, thread_id),
2391 (Some(frame), Some(thread_id)) => (frame, thread_id),
@@ -2344,6 +2488,20 @@ mod cmd {
2344 fun: open,
2488 fun: open,
2345 completer: Some(completers::filename),
2489 completer: Some(completers::filename),
2346 },
2490 },
2491+ TypableCommand {
2492+ name: "buffer-close",
2493+ aliases: &["bc", "bclose"],
2494+ doc: "Close the current buffer.",
2495+ fun: buffer_close,
2496+ completer: None, // FIXME: buffer completer
2497+ },
2498+ TypableCommand {
2499+ name: "buffer-close!",
2500+ aliases: &["bc!", "bclose!"],
2501+ doc: "Close the current buffer forcefully (ignoring unsaved changes).",
2502+ fun: force_buffer_close,
2503+ completer: None, // FIXME: buffer completer
2504+ },
2347 TypableCommand {
2505 TypableCommand {
2348 name: "write",
2506 name: "write",
2349 aliases: &["w"],
2507 aliases: &["w"],
@@ -2442,6 +2600,13 @@ mod cmd {
2442 fun: force_quit_all,
2600 fun: force_quit_all,
2443 completer: None,
2601 completer: None,
2444 },
2602 },
2603+ TypableCommand {
2604+ name: "cquit",
2605+ aliases: &["cq"],
2606+ doc: "Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2).",
2607+ fun: cquit,
2608+ completer: None,
2609+ },
2445 TypableCommand {
2610 TypableCommand {
2446 name: "theme",
2611 name: "theme",
2447 aliases: &[],
2612 aliases: &[],
@@ -2698,36 +2863,66 @@ fn file_picker(cx: &mut Context) {
2698fn buffer_picker(cx: &mut Context) {
2863fn buffer_picker(cx: &mut Context) {
2699 let current = view!(cx.editor).doc;
2864 let current = view!(cx.editor).doc;
2700
2865
2866+ struct BufferMeta {
2867+ id: DocumentId,
2868+ path: Option<PathBuf>,
2869+ is_modified: bool,
2870+ is_current: bool,
2871+ }
2872+
2873+ impl BufferMeta {
2874+ fn format(&self) -> Cow<str> {
2875+ let path = self
2876+ .path
2877+ .as_deref()
2878+ .map(helix_core::path::get_relative_path);
2879+ let path = match path.as_deref().and_then(Path::to_str) {
2880+ Some(path) => path,
2881+ None => return Cow::Borrowed(SCRATCH_BUFFER_NAME),
2882+ };
2883+
2884+ let mut flags = Vec::new();
2885+ if self.is_modified {
2886+ flags.push("+");
2887+ }
2888+ if self.is_current {
2889+ flags.push("*");
2890+ }
2891+
2892+ let flag = if flags.is_empty() {
2893+ "".into()
2894+ } else {
2895+ format!(" ({})", flags.join(""))
2896+ };
2897+ Cow::Owned(format!("{}{}", path, flag))
2898+ }
2899+ }
2900+
2901+ let new_meta = |doc: &Document| BufferMeta {
2902+ id: doc.id(),
2903+ path: doc.path().cloned(),
2904+ is_modified: doc.is_modified(),
2905+ is_current: doc.id() == current,
2906+ };
2907+
2701 let picker = FilePicker::new(
2908 let picker = FilePicker::new(
2702 cx.editor
2909 cx.editor
2703 .documents
2910 .documents
2704 .iter()
2911 .iter()
2705− .map(|(id, doc)| (*id, doc.path().cloned()))
2912+ .map(|(_, doc)| new_meta(doc))
2706 .collect(),
2913 .collect(),
2707− move |(id, path): &(DocumentId, Option<PathBuf>)| {
2708− let path = path.as_deref().map(helix_core::path::get_relative_path);
2709− match path.as_ref().and_then(|path| path.to_str()) {
2710− Some(path) => {
2711− if *id == current {
2712− format!("{} (*)", &path).into()
2914+ BufferMeta::format,
2713− } else {
2915+ |cx, meta, _action| {
2714− path.to_owned().into()
2715− }
2716− }
2717− None => "[scratch buffer]".into(),
2718− }
2719− },
2720− |cx, (id, _path): &(DocumentId, Option<PathBuf>), _action| {
2721− cx.editor.switch(*id, Action::Replace);
2916+ cx.editor.switch(meta.id, Action::Replace);
2722 },
2917 },
2723− |editor, (id, path)| {
2918+ |editor, meta| {
2724− let doc = &editor.documents.get(id)?;
2919+ let doc = &editor.documents.get(&meta.id)?;
2725 let &view_id = doc.selections().keys().next()?;
2920 let &view_id = doc.selections().keys().next()?;
2726 let line = doc
2921 let line = doc
2727 .selection(view_id)
2922 .selection(view_id)
2728 .primary()
2923 .primary()
2729 .cursor_line(doc.text().slice(..));
2924 .cursor_line(doc.text().slice(..));
2730− Some((path.clone()?, Some((line, line))))
2925+ Some((meta.path.clone()?, Some((line, line))))
2731 },
2926 },
2732 );
2927 );
2733 cx.push_layer(Box::new(picker));
2928 cx.push_layer(Box::new(picker));
@@ -2782,7 +2977,7 @@ fn symbol_picker(cx: &mut Context) {
2782 }
2977 }
2783 };
2978 };
2784
2979
2785− let picker = FilePicker::new(
2980+ let mut picker = FilePicker::new(
2786 symbols,
2981 symbols,
2787 |symbol| (&symbol.name).into(),
2982 |symbol| (&symbol.name).into(),
2788 move |cx, symbol, _action| {
2983 move |cx, symbol, _action| {
@@ -2807,6 +3002,69 @@ fn symbol_picker(cx: &mut Context) {
2807 Some((path, line))
3002 Some((path, line))
2808 },
3003 },
2809 );
3004 );
3005+ picker.truncate_start = false;
3006+ compositor.push(Box::new(picker))
3007+ }
3008+ },
3009+ )
3010+}
3011+
3012+fn workspace_symbol_picker(cx: &mut Context) {
3013+ let (_, doc) = current!(cx.editor);
3014+
3015+ let language_server = match doc.language_server() {
3016+ Some(language_server) => language_server,
3017+ None => return,
3018+ };
3019+ let offset_encoding = language_server.offset_encoding();
3020+
3021+ let future = language_server.workspace_symbols("".to_string());
3022+
3023+ let current_path = doc_mut!(cx.editor).path().cloned();
3024+ cx.callback(
3025+ future,
3026+ move |_editor: &mut Editor,
3027+ compositor: &mut Compositor,
3028+ response: Option<Vec<lsp::SymbolInformation>>| {
3029+ if let Some(symbols) = response {
3030+ let mut picker = FilePicker::new(
3031+ symbols,
3032+ move |symbol| {
3033+ let path = symbol.location.uri.to_file_path().unwrap();
3034+ if current_path.as_ref().map(|p| p == &path).unwrap_or(false) {
3035+ (&symbol.name).into()
3036+ } else {
3037+ let relative_path = helix_core::path::get_relative_path(path.as_path())
3038+ .to_str()
3039+ .unwrap()
3040+ .to_owned();
3041+ format!("{} ({})", &symbol.name, relative_path).into()
3042+ }
3043+ },
3044+ move |cx, symbol, action| {
3045+ let path = symbol.location.uri.to_file_path().unwrap();
3046+ cx.editor.open(path, action).expect("editor.open failed");
3047+ let (view, doc) = current!(cx.editor);
3048+
3049+ if let Some(range) =
3050+ lsp_range_to_range(doc.text(), symbol.location.range, offset_encoding)
3051+ {
3052+ // we flip the range so that the cursor sits on the start of the symbol
3053+ // (for example start of the function).
3054+ doc.set_selection(view.id, Selection::single(range.head, range.anchor));
3055+ align_view(doc, view, Align::Center);
3056+ }
3057+ },
3058+ move |_editor, symbol| {
3059+ let path = symbol.location.uri.to_file_path().unwrap();
3060+ let line = Some((
3061+ symbol.location.range.start.line as usize,
3062+ symbol.location.range.end.line as usize,
3063+ ));
3064+ Some((path, line))
3065+ },
3066+ );
3067+ picker.truncate_start = false;
2810 compositor.push(Box::new(picker))
3068 compositor.push(Box::new(picker))
2811 }
3069 }
2812 },
3070 },
@@ -2864,14 +3122,104 @@ pub fn code_action(cx: &mut Context) {
2864 )
3122 )
2865}
3123}
2866
3124
3125+pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> {
3126+ use lsp::ResourceOp;
3127+ use std::fs;
3128+ match op {
3129+ ResourceOp::Create(op) => {
3130+ let path = op.uri.to_file_path().unwrap();
3131+ let ignore_if_exists = if let Some(options) = &op.options {
3132+ !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
3133+ } else {
3134+ false
3135+ };
3136+ if ignore_if_exists && path.exists() {
3137+ Ok(())
3138+ } else {
3139+ fs::write(&path, [])
3140+ }
3141+ }
3142+ ResourceOp::Delete(op) => {
3143+ let path = op.uri.to_file_path().unwrap();
3144+ if path.is_dir() {
3145+ let recursive = if let Some(options) = &op.options {
3146+ options.recursive.unwrap_or(false)
3147+ } else {
3148+ false
3149+ };
3150+ if recursive {
3151+ fs::remove_dir_all(&path)
3152+ } else {
3153+ fs::remove_dir(&path)
3154+ }
3155+ } else if path.is_file() {
3156+ fs::remove_file(&path)
3157+ } else {
3158+ Ok(())
3159+ }
3160+ }
3161+ ResourceOp::Rename(op) => {
3162+ let from = op.old_uri.to_file_path().unwrap();
3163+ let to = op.new_uri.to_file_path().unwrap();
3164+ let ignore_if_exists = if let Some(options) = &op.options {
3165+ !options.overwrite.unwrap_or(false) && options.ignore_if_exists.unwrap_or(false)
3166+ } else {
3167+ false
3168+ };
3169+ if ignore_if_exists && to.exists() {
3170+ Ok(())
3171+ } else {
3172+ fs::rename(&from, &to)
3173+ }
3174+ }
3175+ }
3176+}
3177+
2867fn apply_workspace_edit(
3178fn apply_workspace_edit(
2868 editor: &mut Editor,
3179 editor: &mut Editor,
2869 offset_encoding: OffsetEncoding,
3180 offset_encoding: OffsetEncoding,
2870 workspace_edit: &lsp::WorkspaceEdit,
3181 workspace_edit: &lsp::WorkspaceEdit,
2871) {
3182) {
3183+ let mut apply_edits = |uri: &helix_lsp::Url, text_edits: Vec<lsp::TextEdit>| {
3184+ let path = uri
3185+ .to_file_path()
3186+ .expect("unable to convert URI to filepath");
3187+
3188+ let current_view_id = view!(editor).id;
3189+ let doc_id = editor.open(path, Action::Load).unwrap();
3190+ let doc = editor
3191+ .document_mut(doc_id)
3192+ .expect("Document for document_changes not found");
3193+
3194+ // Need to determine a view for apply/append_changes_to_history
3195+ let selections = doc.selections();
3196+ let view_id = if selections.contains_key(&current_view_id) {
3197+ // use current if possible
3198+ current_view_id
3199+ } else {
3200+ // Hack: we take the first available view_id
3201+ selections
3202+ .keys()
3203+ .next()
3204+ .copied()
3205+ .expect("No view_id available")
3206+ };
3207+
3208+ let transaction = helix_lsp::util::generate_transaction_from_edits(
3209+ doc.text(),
3210+ text_edits,
3211+ offset_encoding,
3212+ );
3213+ doc.apply(&transaction, view_id);
3214+ doc.append_changes_to_history(view_id);
3215+ };
3216+
2872 if let Some(ref changes) = workspace_edit.changes {
3217 if let Some(ref changes) = workspace_edit.changes {
2873 log::debug!("workspace changes: {:?}", changes);
3218 log::debug!("workspace changes: {:?}", changes);
2874− editor.set_error(String::from("Handling workspace_edit.changes is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
3219+ for (uri, text_edits) in changes {
3220+ let text_edits = text_edits.to_vec();
3221+ apply_edits(uri, text_edits);
3222+ }
2875 return;
3223 return;
2876 // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
3224 // Not sure if it works properly, it'll be safer to just panic here to avoid breaking some parts of code on which code actions will be used
2877 // TODO: find some example that uses workspace changes, and test it
3225 // TODO: find some example that uses workspace changes, and test it
@@ -2889,30 +3237,6 @@ fn apply_workspace_edit(
2889 match document_changes {
3237 match document_changes {
2890 lsp::DocumentChanges::Edits(document_edits) => {
3238 lsp::DocumentChanges::Edits(document_edits) => {
2891 for document_edit in document_edits {
3239 for document_edit in document_edits {
2892− let path = document_edit
2893− .text_document
2894− .uri
2895− .to_file_path()
2896− .expect("unable to convert URI to filepath");
2897− let current_view_id = view!(editor).id;
2898− let doc = editor
2899− .document_by_path_mut(path)
2900− .expect("Document for document_changes not found");
2901−
2902− // Need to determine a view for apply/append_changes_to_history
2903− let selections = doc.selections();
2904− let view_id = if selections.contains_key(&current_view_id) {
2905− // use current if possible
2906− current_view_id
2907− } else {
2908− // Hack: we take the first available view_id
2909− selections
2910− .keys()
2911− .next()
2912− .copied()
2913− .expect("No view_id available")
2914− };
2915−
2916 let edits = document_edit
3240 let edits = document_edit
2917 .edits
3241 .edits
2918 .iter()
3242 .iter()
@@ -2924,19 +3248,33 @@ fn apply_workspace_edit(
2924 })
3248 })
2925 .cloned()
3249 .cloned()
2926 .collect();
3250 .collect();
2927−
3251+ apply_edits(&document_edit.text_document.uri, edits);
2928− let transaction = helix_lsp::util::generate_transaction_from_edits(
2929− doc.text(),
2930− edits,
2931− offset_encoding,
2932− );
2933− doc.apply(&transaction, view_id);
2934− doc.append_changes_to_history(view_id);
2935 }
3252 }
2936 }
3253 }
2937 lsp::DocumentChanges::Operations(operations) => {
3254 lsp::DocumentChanges::Operations(operations) => {
2938 log::debug!("document changes - operations: {:?}", operations);
3255 log::debug!("document changes - operations: {:?}", operations);
2939− editor.set_error(String::from("Handling document operations is not implemented yet, see https://github.com/helix-editor/helix/issues/183"));
3256+ for operateion in operations {
3257+ match operateion {
3258+ lsp::DocumentChangeOperation::Op(op) => {
3259+ apply_document_resource_op(op).unwrap();
3260+ }
3261+
3262+ lsp::DocumentChangeOperation::Edit(document_edit) => {
3263+ let edits = document_edit
3264+ .edits
3265+ .iter()
3266+ .map(|edit| match edit {
3267+ lsp::OneOf::Left(text_edit) => text_edit,
3268+ lsp::OneOf::Right(annotated_text_edit) => {
3269+ &annotated_text_edit.text_edit
3270+ }
3271+ })
3272+ .cloned()
3273+ .collect();
3274+ apply_edits(&document_edit.text_document.uri, edits);
3275+ }
3276+ }
3277+ }
2940 }
3278 }
2941 }
3279 }
2942 }
3280 }
@@ -3175,6 +3513,19 @@ fn goto_last_accessed_file(cx: &mut Context) {
3175 }
3513 }
3176}
3514}
3177
3515
3516+fn goto_last_modification(cx: &mut Context) {
3517+ let (view, doc) = current!(cx.editor);
3518+ let pos = doc.history.get_mut().last_edit_pos();
3519+ let text = doc.text().slice(..);
3520+ if let Some(pos) = pos {
3521+ let selection = doc
3522+ .selection(view.id)
3523+ .clone()
3524+ .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select));
3525+ doc.set_selection(view.id, selection);
3526+ }
3527+}
3528+
3178fn select_mode(cx: &mut Context) {
3529fn select_mode(cx: &mut Context) {
3179 let (view, doc) = current!(cx.editor);
3530 let (view, doc) = current!(cx.editor);
3180 let text = doc.text().slice(..);
3531 let text = doc.text().slice(..);
@@ -3773,19 +4124,70 @@ pub mod insert {
3773 doc.apply(&transaction, view.id);
4124 doc.apply(&transaction, view.id);
3774 }
4125 }
3775
4126
3776− // TODO: handle indent-aware delete
3777 pub fn delete_char_backward(cx: &mut Context) {
4127 pub fn delete_char_backward(cx: &mut Context) {
3778 let count = cx.count();
4128 let count = cx.count();
3779 let (view, doc) = current!(cx.editor);
4129 let (view, doc) = current!(cx.editor);
3780 let text = doc.text().slice(..);
4130 let text = doc.text().slice(..);
4131+ let indent_unit = doc.indent_unit();
4132+ let tab_size = doc.tab_width();
4133+
3781 let transaction =
4134 let transaction =
3782 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
4135 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
3783 let pos = range.cursor(text);
4136 let pos = range.cursor(text);
3784− (
4137+ let line_start_pos = text.line_to_char(range.cursor_line(text));
4138+ // considier to delete by indent level if all characters before `pos` are indent units.
4139+ let fragment = Cow::from(text.slice(line_start_pos..pos));
4140+ if !fragment.is_empty() && fragment.chars().all(|ch| ch.is_whitespace()) {
4141+ if text.get_char(pos.saturating_sub(1)) == Some('\t') {
4142+ // fast path, delete one char
4143+ (
3785− graphemes::nth_prev_grapheme_boundary(text, pos, count),
4144+ graphemes::nth_prev_grapheme_boundary(text, pos, 1),
3786− pos,
4145+ pos,
4146+ None,
4147+ )
4148+ } else {
4149+ let unit_len = indent_unit.chars().count();
4150+ // NOTE: indent_unit always contains 'only spaces' or 'only tab' according to `IndentStyle` definition.
4151+ let unit_size = if indent_unit.starts_with('\t') {
4152+ tab_size * unit_len
4153+ } else {
4154+ unit_len
4155+ };
4156+ let width: usize = fragment
4157+ .chars()
4158+ .map(|ch| {
4159+ if ch == '\t' {
4160+ tab_size
4161+ } else {
4162+ // it can be none if it still meet control characters other than '\t'
4163+ // here just set the width to 1 (or some value better?).
4164+ ch.width().unwrap_or(1)
4165+ }
4166+ })
4167+ .sum();
4168+ let mut drop = width % unit_size; // round down to nearest unit
4169+ if drop == 0 {
4170+ drop = unit_size
4171+ }; // if it's already at a unit, consume a whole unit
4172+ let mut chars = fragment.chars().rev();
4173+ let mut start = pos;
4174+ for _ in 0..drop {
4175+ // delete up to `drop` spaces
4176+ match chars.next() {
4177+ Some(' ') => start -= 1,
4178+ _ => break,
4179+ }
4180+ }
4181+ (start, pos, None) // delete!
4182+ }
4183+ } else {
4184+ // delete char
4185+ (
4186+ graphemes::nth_prev_grapheme_boundary(text, pos, count),
4187+ pos,
3787− None,
4188+ None,
3788− )
4189+ )
4190+ }
3789 });
4191 });
3790 doc.apply(&transaction, view.id);
4192 doc.apply(&transaction, view.id);
3791 }
4193 }
@@ -3815,8 +4217,19 @@ pub mod insert {
3815 .selection(view.id)
4217 .selection(view.id)
3816 .clone()
4218 .clone()
3817 .transform(|range| movement::move_prev_word_start(text, range, count));
4219 .transform(|range| movement::move_prev_word_start(text, range, count));
3818− doc.set_selection(view.id, selection);
4220+ delete_selection_insert_mode(doc, view, &selection);
3819− delete_selection(cx)
4221+ }
4222+
4223+ pub fn delete_word_forward(cx: &mut Context) {
4224+ let count = cx.count();
4225+ let (view, doc) = current!(cx.editor);
4226+ let text = doc.text().slice(..);
4227+
4228+ let selection = doc
4229+ .selection(view.id)
4230+ .clone()
4231+ .transform(|range| movement::move_next_word_start(text, range, count));
4232+ delete_selection_insert_mode(doc, view, &selection);
3820 }
4233 }
3821}
4234}
3822
4235
@@ -3826,20 +4239,48 @@ pub mod insert {
3826// storing it?
4239// storing it?
3827
4240
3828fn undo(cx: &mut Context) {
4241fn undo(cx: &mut Context) {
4242+ let count = cx.count();
3829 let (view, doc) = current!(cx.editor);
4243 let (view, doc) = current!(cx.editor);
3830− let view_id = view.id;
4244+ for _ in 0..count {
3831− let success = doc.undo(view_id);
4245+ if !doc.undo(view.id) {
3832− if !success {
3833− cx.editor.set_status("Already at oldest change".to_owned());
4246+ cx.editor.set_status("Already at oldest change".to_owned());
4247+ break;
4248+ }
3834 }
4249 }
3835}
4250}
3836
4251
3837fn redo(cx: &mut Context) {
4252fn redo(cx: &mut Context) {
4253+ let count = cx.count();
3838 let (view, doc) = current!(cx.editor);
4254 let (view, doc) = current!(cx.editor);
3839− let view_id = view.id;
4255+ for _ in 0..count {
3840− let success = doc.redo(view_id);
4256+ if !doc.redo(view.id) {
3841− if !success {
4257+ cx.editor.set_status("Already at newest change".to_owned());
4258+ break;
4259+ }
4260+ }
4261+}
4262+
4263+fn earlier(cx: &mut Context) {
4264+ let count = cx.count();
4265+ let (view, doc) = current!(cx.editor);
4266+ for _ in 0..count {
4267+ // rather than doing in batch we do this so get error halfway
4268+ if !doc.earlier(view.id, UndoKind::Steps(1)) {
4269+ cx.editor.set_status("Already at oldest change".to_owned());
4270+ break;
4271+ }
4272+ }
4273+}
4274+
4275+fn later(cx: &mut Context) {
4276+ let count = cx.count();
4277+ let (view, doc) = current!(cx.editor);
4278+ for _ in 0..count {
4279+ // rather than doing in batch we do this so get error halfway
4280+ if !doc.later(view.id, UndoKind::Steps(1)) {
3842− cx.editor.set_status("Already at newest change".to_owned());
4281+ cx.editor.set_status("Already at newest change".to_owned());
4282+ break;
4283+ }
3843 }
4284 }
3844}
4285}
3845
4286
@@ -4297,12 +4738,12 @@ fn join_selections(cx: &mut Context) {
4297 doc.append_changes_to_history(view.id);
4738 doc.append_changes_to_history(view.id);
4298}
4739}
4299
4740
4300−fn keep_selections(cx: &mut Context) {
4741+fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
4301− // keep selections matching regex
4742+ // keep or remove selections matching regex
4302 let reg = cx.register.unwrap_or('/');
4743 let reg = cx.register.unwrap_or('/');
4303 let prompt = ui::regex_prompt(
4744 let prompt = ui::regex_prompt(
4304 cx,
4745 cx,
4305− "keep:".into(),
4746+ if !remove { "keep:" } else { "remove:" }.into(),
4306 Some(reg),
4747 Some(reg),
4307 |_input: &str| Vec::new(),
4748 |_input: &str| Vec::new(),
4308 move |view, doc, regex, event| {
4749 move |view, doc, regex, event| {
@@ -4311,7 +4752,9 @@ fn keep_selections(cx: &mut Context) {
4311 }
4752 }
4312 let text = doc.text().slice(..);
4753 let text = doc.text().slice(..);
4313
4754
4755+ if let Some(selection) =
4314− if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
4756+ selection::keep_or_remove_matches(text, doc.selection(view.id), &regex, remove)
4757+ {
4315 doc.set_selection(view.id, selection);
4758 doc.set_selection(view.id, selection);
4316 }
4759 }
4317 },
4760 },
@@ -4320,6 +4763,14 @@ fn keep_selections(cx: &mut Context) {
4320 cx.push_layer(Box::new(prompt));
4763 cx.push_layer(Box::new(prompt));
4321}
4764}
4322
4765
4766+fn keep_selections(cx: &mut Context) {
4767+ keep_or_remove_selections_impl(cx, false)
4768+}
4769+
4770+fn remove_selections(cx: &mut Context) {
4771+ keep_or_remove_selections_impl(cx, true)
4772+}
4773+
4323fn keep_primary_selection(cx: &mut Context) {
4774fn keep_primary_selection(cx: &mut Context) {
4324 let (view, doc) = current!(cx.editor);
4775 let (view, doc) = current!(cx.editor);
4325 // TODO: handle count
4776 // TODO: handle count
@@ -4445,19 +4896,15 @@ pub fn completion(cx: &mut Context) {
4445 return;
4896 return;
4446 }
4897 }
4447 let size = compositor.size();
4898 let size = compositor.size();
4448− let ui = compositor
4899+ let ui = compositor.find::<ui::EditorView>().unwrap();
4449− .find(std::any::type_name::<ui::EditorView>())
4450− .unwrap();
4451− if let Some(ui) = ui.as_any_mut().downcast_mut::<ui::EditorView>() {
4452− ui.set_completion(
4900+ ui.set_completion(
4453− editor,
4901+ editor,
4454− items,
4902+ items,
4455− offset_encoding,
4903+ offset_encoding,
4456− start_offset,
4904+ start_offset,
4457− trigger_offset,
4905+ trigger_offset,
4458− size,
4906+ size,
4459− );
4907+ );
4460− };
4461 },
4908 },
4462 );
4909 );
4463}
4910}
@@ -4487,18 +4934,27 @@ fn hover(cx: &mut Context) {
4487 move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
4934 move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
4488 if let Some(hover) = response {
4935 if let Some(hover) = response {
4489 // hover.contents / .range <- used for visualizing
4936 // hover.contents / .range <- used for visualizing
4937+
4938+ fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
4490− let contents = match hover.contents {
4939+ match contents {
4491− lsp::HoverContents::Scalar(contents) => {
4940+ lsp::MarkedString::String(contents) => contents,
4492− // markedstring(string/languagestring to be highlighted)
4941+ lsp::MarkedString::LanguageString(string) => {
4493− // TODO
4942+ if string.language == "markdown" {
4494− log::error!("hover contents {:?}", contents);
4943+ string.value
4495− return;
4944+ } else {
4496− }
4945+ format!("```{}\n{}\n```", string.language, string.value)
4497− lsp::HoverContents::Array(contents) => {
4946+ }
4498− log::error!("hover contents {:?}", contents);
4947+ }
4499− return;
4500 }
4948 }
4501− // TODO: render markdown
4949+ }
4950+
4951+ let contents = match hover.contents {
4952+ lsp::HoverContents::Scalar(contents) => marked_string_to_markdown(contents),
4953+ lsp::HoverContents::Array(contents) => contents
4954+ .into_iter()
4955+ .map(marked_string_to_markdown)
4956+ .collect::<Vec<_>>()
4957+ .join("\n\n"),
4502 lsp::HoverContents::Markup(contents) => contents.value,
4958 lsp::HoverContents::Markup(contents) => contents.value,
4503 };
4959 };
4504
4960
@@ -4706,7 +5162,21 @@ fn wclose(cx: &mut Context) {
4706 }
5162 }
4707 let view_id = view!(cx.editor).id;
5163 let view_id = view!(cx.editor).id;
4708 // close current split
5164 // close current split
4709− cx.editor.close(view_id, /* close_buffer */ false);
5165+ cx.editor.close(view_id);
5166+}
5167+
5168+fn wonly(cx: &mut Context) {
5169+ let views = cx
5170+ .editor
5171+ .tree
5172+ .views()
5173+ .map(|(v, focus)| (v.id, focus))
5174+ .collect::<Vec<_>>();
5175+ for (view_id, focus) in views {
5176+ if !focus {
5177+ cx.editor.close(view_id);
5178+ }
5179+ }
4710}
5180}
4711
5181
4712fn select_register(cx: &mut Context) {
5182fn select_register(cx: &mut Context) {
@@ -4717,6 +5187,15 @@ fn select_register(cx: &mut Context) {
4717 })
5187 })
4718}
5188}
4719
5189
5190+fn insert_register(cx: &mut Context) {
5191+ cx.on_next_key(move |cx, event| {
5192+ if let Some(ch) = event.char() {
5193+ cx.editor.selected_register = Some(ch);
5194+ paste_before(cx);
5195+ }
5196+ })
5197+}
5198+
4720fn align_view_top(cx: &mut Context) {
5199fn align_view_top(cx: &mut Context) {
4721 let (view, doc) = current!(cx.editor);
5200 let (view, doc) = current!(cx.editor);
4722 align_view(doc, view, Align::Top);
5201 align_view(doc, view, Align::Top);
@@ -4785,10 +5264,19 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
4785
5264
4786 let selection = doc.selection(view.id).clone().transform(|range| {
5265 let selection = doc.selection(view.id).clone().transform(|range| {
4787 match ch {
5266 match ch {
4788− 'w' => textobject::textobject_word(text, range, objtype, count),
5267+ 'w' => textobject::textobject_word(text, range, objtype, count, false),
5268+ 'W' => textobject::textobject_word(text, range, objtype, count, true),
4789 'c' => textobject_treesitter("class", range),
5269 'c' => textobject_treesitter("class", range),
4790 'f' => textobject_treesitter("function", range),
5270 'f' => textobject_treesitter("function", range),
4791 'p' => textobject_treesitter("parameter", range),
5271 'p' => textobject_treesitter("parameter", range),
5272+ 'm' => {
5273+ let ch = text.char(range.cursor(text));
5274+ if !ch.is_ascii_alphanumeric() {
5275+ textobject::textobject_surround(text, range, objtype, ch, count)
5276+ } else {
5277+ range
5278+ }
5279+ }
4792 // TODO: cancel new ranges if inconsistent surround matches across lines
5280 // TODO: cancel new ranges if inconsistent surround matches across lines
4793 ch if !ch.is_ascii_alphanumeric() => {
5281 ch if !ch.is_ascii_alphanumeric() => {
4794 textobject::textobject_surround(text, range, objtype, ch, count)
5282 textobject::textobject_surround(text, range, objtype, ch, count)
@@ -5051,6 +5539,10 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
5051 doc.apply(&transaction, view.id);
5539 doc.apply(&transaction, view.id);
5052 doc.append_changes_to_history(view.id);
5540 doc.append_changes_to_history(view.id);
5053 }
5541 }
5542+
5543+ // after replace cursor may be out of bounds, do this to
5544+ // make sure cursor is in view and update scroll as well
5545+ view.ensure_cursor_in_view(doc, cx.editor.config.scrolloff);
5054 },
5546 },
5055 None,
5547 None,
5056 );
5548 );
@@ -5096,3 +5588,76 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
5096 doc.apply(&transaction, view.id);
5588 doc.apply(&transaction, view.id);
5097 doc.append_changes_to_history(view.id);
5589 doc.append_changes_to_history(view.id);
5098}
5590}
5591+
5592+fn rename_symbol(cx: &mut Context) {
5593+ let prompt = Prompt::new(
5594+ "rename-to:".into(),
5595+ None,
5596+ |_input: &str| Vec::new(),
5597+ move |cx: &mut compositor::Context, input: &str, event: PromptEvent| {
5598+ if event != PromptEvent::Validate {
5599+ return;
5600+ }
5601+
5602+ log::debug!("renaming to: {:?}", input);
5603+
5604+ let (view, doc) = current!(cx.editor);
5605+ let language_server = match doc.language_server() {
5606+ Some(language_server) => language_server,
5607+ None => return,
5608+ };
5609+
5610+ let offset_encoding = language_server.offset_encoding();
5611+
5612+ let pos = pos_to_lsp_pos(
5613+ doc.text(),
5614+ doc.selection(view.id)
5615+ .primary()
5616+ .cursor(doc.text().slice(..)),
5617+ offset_encoding,
5618+ );
5619+
5620+ let task = language_server.rename_symbol(doc.identifier(), pos, input.to_string());
5621+ let edits = block_on(task).unwrap_or_default();
5622+ log::debug!("Edits from LSP: {:?}", edits);
5623+ apply_workspace_edit(&mut cx.editor, offset_encoding, &edits);
5624+ },
5625+ None,
5626+ );
5627+ cx.push_layer(Box::new(prompt));
5628+}
5629+
5630+/// Increment object under cursor by count.
5631+fn increment(cx: &mut Context) {
5632+ increment_impl(cx, cx.count() as i64);
5633+}
5634+
5635+/// Decrement object under cursor by count.
5636+fn decrement(cx: &mut Context) {
5637+ increment_impl(cx, -(cx.count() as i64));
5638+}
5639+
5640+/// Decrement object under cursor by `amount`.
5641+fn increment_impl(cx: &mut Context, amount: i64) {
5642+ let (view, doc) = current!(cx.editor);
5643+ let selection = doc.selection(view.id);
5644+ let text = doc.text();
5645+
5646+ let changes = selection.ranges().iter().filter_map(|range| {
5647+ let incrementor = NumberIncrementor::from_range(text.slice(..), *range)?;
5648+ let new_text = incrementor.incremented_text(amount);
5649+ Some((
5650+ incrementor.range.from(),
5651+ incrementor.range.to(),
5652+ Some(new_text),
5653+ ))
5654+ });
5655+
5656+ if changes.clone().count() > 0 {
5657+ let transaction = Transaction::change(doc.text(), changes);
5658+ let transaction = transaction.with_selection(selection.clone());
5659+
5660+ doc.apply(&transaction, view.id);
5661+ doc.append_changes_to_history(view.id);
5662+ }
5663+}
helix-term/src/compositor.rs+3−2
@@ -177,11 +177,12 @@ impl Compositor {
177 .any(|component| component.type_name() == type_name)
177 .any(|component| component.type_name() == type_name)
178 }
178 }
179
179
180− pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> {
180+ pub fn find<T: 'static>(&mut self) -> Option<&mut T> {
181+ let type_name = std::any::type_name::<T>();
181 self.layers
182 self.layers
182 .iter_mut()
183 .iter_mut()
183 .find(|component| component.type_name() == type_name)
184 .find(|component| component.type_name() == type_name)
184− .map(|component| component.as_mut())
185+ .and_then(|component| component.as_any_mut().downcast_mut())
185 }
186 }
186}
187}
187
188
helix-term/src/config.rs+2−1
@@ -3,6 +3,7 @@ use serde::Deserialize;
3use crate::keymap::Keymaps;
3use crate::keymap::Keymaps;
4
4
5#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
5#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
6+#[serde(deny_unknown_fields)]
6pub struct Config {
7pub struct Config {
7 pub theme: Option<String>,
8 pub theme: Option<String>,
8 #[serde(default)]
9 #[serde(default)]
@@ -14,7 +15,7 @@ pub struct Config {
14}
15}
15
16
16#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
17#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
17−#[serde(rename_all = "kebab-case")]
18+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
18pub struct LspConfig {
19pub struct LspConfig {
19 pub display_messages: bool,
20 pub display_messages: bool,
20}
21}
helix-term/src/keymap.rs+152−19
@@ -25,6 +25,54 @@ macro_rules! key {
25 };
25 };
26}
26}
27
27
28+#[macro_export]
29+macro_rules! shift {
30+ ($key:ident) => {
31+ ::helix_view::input::KeyEvent {
32+ code: ::helix_view::keyboard::KeyCode::$key,
33+ modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
34+ }
35+ };
36+ ($($ch:tt)*) => {
37+ ::helix_view::input::KeyEvent {
38+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
39+ modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
40+ }
41+ };
42+}
43+
44+#[macro_export]
45+macro_rules! ctrl {
46+ ($key:ident) => {
47+ ::helix_view::input::KeyEvent {
48+ code: ::helix_view::keyboard::KeyCode::$key,
49+ modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
50+ }
51+ };
52+ ($($ch:tt)*) => {
53+ ::helix_view::input::KeyEvent {
54+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
55+ modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
56+ }
57+ };
58+}
59+
60+#[macro_export]
61+macro_rules! alt {
62+ ($key:ident) => {
63+ ::helix_view::input::KeyEvent {
64+ code: ::helix_view::keyboard::KeyCode::$key,
65+ modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
66+ }
67+ };
68+ ($($ch:tt)*) => {
69+ ::helix_view::input::KeyEvent {
70+ code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
71+ modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
72+ }
73+ };
74+}
75+
28/// Macro for defining the root of a `Keymap` object. Example:
76/// Macro for defining the root of a `Keymap` object. Example:
29///
77///
30/// ```
78/// ```
@@ -53,6 +101,10 @@ macro_rules! keymap {
53 keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
101 keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
54 };
102 };
55
103
104+ (@trie [$($cmd:ident),* $(,)?]) => {
105+ $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
106+ };
107+
56 (
108 (
57 { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
109 { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
58 ) => {
110 ) => {
@@ -64,10 +116,11 @@ macro_rules! keymap {
64 $(
116 $(
65 $(
117 $(
66 let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
118 let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
67− _map.insert(
119+ let _duplicate = _map.insert(
68 _key,
120 _key,
69 keymap!(@trie $value)
121 keymap!(@trie $value)
70 );
122 );
123+ debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
71 _order.push(_key);
124 _order.push(_key);
72 )+
125 )+
73 )*
126 )*
@@ -147,6 +200,7 @@ impl KeyTrieNode {
147 cmd.doc()
200 cmd.doc()
148 }
201 }
149 KeyTrie::Node(n) => n.name(),
202 KeyTrie::Node(n) => n.name(),
203+ KeyTrie::Sequence(_) => "[Multiple commands]",
150 };
204 };
151 match body.iter().position(|(d, _)| d == &desc) {
205 match body.iter().position(|(d, _)| d == &desc) {
152 Some(pos) => {
206 Some(pos) => {
@@ -207,6 +261,7 @@ impl DerefMut for KeyTrieNode {
207#[serde(untagged)]
261#[serde(untagged)]
208pub enum KeyTrie {
262pub enum KeyTrie {
209 Leaf(Command),
263 Leaf(Command),
264+ Sequence(Vec<Command>),
210 Node(KeyTrieNode),
265 Node(KeyTrieNode),
211}
266}
212
267
@@ -214,14 +269,14 @@ impl KeyTrie {
214 pub fn node(&self) -> Option<&KeyTrieNode> {
269 pub fn node(&self) -> Option<&KeyTrieNode> {
215 match *self {
270 match *self {
216 KeyTrie::Node(ref node) => Some(node),
271 KeyTrie::Node(ref node) => Some(node),
217− KeyTrie::Leaf(_) => None,
272+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
218 }
273 }
219 }
274 }
220
275
221 pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
276 pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
222 match *self {
277 match *self {
223 KeyTrie::Node(ref mut node) => Some(node),
278 KeyTrie::Node(ref mut node) => Some(node),
224− KeyTrie::Leaf(_) => None,
279+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
225 }
280 }
226 }
281 }
227
282
@@ -238,7 +293,7 @@ impl KeyTrie {
238 trie = match trie {
293 trie = match trie {
239 KeyTrie::Node(map) => map.get(key),
294 KeyTrie::Node(map) => map.get(key),
240 // leaf encountered while keys left to process
295 // leaf encountered while keys left to process
241− KeyTrie::Leaf(_) => None,
296+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
242 }?
297 }?
243 }
298 }
244 Some(trie)
299 Some(trie)
@@ -250,6 +305,8 @@ pub enum KeymapResultKind {
250 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
305 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
251 Pending(KeyTrieNode),
306 Pending(KeyTrieNode),
252 Matched(Command),
307 Matched(Command),
308+ /// Matched a sequence of commands to execute.
309+ MatchedSequence(Vec<Command>),
253 /// Key was not found in the root keymap
310 /// Key was not found in the root keymap
254 NotFound,
311 NotFound,
255 /// Key is invalid in combination with previous keys. Contains keys leading upto
312 /// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -332,6 +389,12 @@ impl Keymap {
332 Some(&KeyTrie::Leaf(cmd)) => {
389 Some(&KeyTrie::Leaf(cmd)) => {
333 return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
390 return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
334 }
391 }
392+ Some(&KeyTrie::Sequence(ref cmds)) => {
393+ return KeymapResult::new(
394+ KeymapResultKind::MatchedSequence(cmds.clone()),
395+ self.sticky(),
396+ )
397+ }
335 None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
398 None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
336 Some(t) => t,
399 Some(t) => t,
337 };
400 };
@@ -349,6 +412,13 @@ impl Keymap {
349 self.state.clear();
412 self.state.clear();
350 return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
413 return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
351 }
414 }
415+ Some(&KeyTrie::Sequence(ref cmds)) => {
416+ self.state.clear();
417+ KeymapResult::new(
418+ KeymapResultKind::MatchedSequence(cmds.clone()),
419+ self.sticky(),
420+ )
421+ }
352 None => KeymapResult::new(
422 None => KeymapResult::new(
353 KeymapResultKind::Cancelled(self.state.drain(..).collect()),
423 KeymapResultKind::Cancelled(self.state.drain(..).collect()),
354 self.sticky(),
424 self.sticky(),
@@ -455,6 +525,7 @@ impl Default for Keymaps {
455 "a" => goto_last_accessed_file,
525 "a" => goto_last_accessed_file,
456 "n" => goto_next_buffer,
526 "n" => goto_next_buffer,
457 "p" => goto_previous_buffer,
527 "p" => goto_previous_buffer,
528+ "." => goto_last_modification,
458 },
529 },
459 ":" => command_mode,
530 ":" => command_mode,
460
531
@@ -511,6 +582,8 @@ impl Default for Keymaps {
511
582
512 "u" => undo,
583 "u" => undo,
513 "U" => redo,
584 "U" => redo,
585+ "A-u" => earlier,
586+ "A-U" => later,
514
587
515 "y" => yank,
588 "y" => yank,
516 // yank_all
589 // yank_all
@@ -523,7 +596,7 @@ impl Default for Keymaps {
523 "=" => format_selections,
596 "=" => format_selections,
524 "J" => join_selections,
597 "J" => join_selections,
525 "K" => keep_selections,
598 "K" => keep_selections,
526− // TODO: and another method for inverse
599+ "A-K" => remove_selections,
527
600
528 "," => keep_primary_selection,
601 "," => keep_primary_selection,
529 "A-," => remove_primary_selection,
602 "A-," => remove_primary_selection,
@@ -532,7 +605,7 @@ impl Default for Keymaps {
532 // "Q" => replay_macro,
605 // "Q" => replay_macro,
533
606
534 // & align selections
607 // & align selections
535− // _ trim selections
608+ "_" => trim_selections,
536
609
537 "(" => rotate_selections_backward,
610 "(" => rotate_selections_backward,
538 ")" => rotate_selections_forward,
611 ")" => rotate_selections_forward,
@@ -550,6 +623,7 @@ impl Default for Keymaps {
550 "C-s" | "s" => hsplit,
623 "C-s" | "s" => hsplit,
551 "C-v" | "v" => vsplit,
624 "C-v" | "v" => vsplit,
552 "C-q" | "q" => wclose,
625 "C-q" | "q" => wclose,
626+ "C-o" | "o" => wonly,
553 "C-h" | "h" | "left" => jump_view_left,
627 "C-h" | "h" | "left" => jump_view_left,
554 "C-j" | "j" | "down" => jump_view_down,
628 "C-j" | "j" | "down" => jump_view_down,
555 "C-k" | "k" | "up" => jump_view_up,
629 "C-k" | "k" | "up" => jump_view_up,
@@ -569,6 +643,7 @@ impl Default for Keymaps {
569 "f" => file_picker,
643 "f" => file_picker,
570 "b" => buffer_picker,
644 "b" => buffer_picker,
571 "s" => symbol_picker,
645 "s" => symbol_picker,
646+ "S" => workspace_symbol_picker,
572 "a" => code_action,
647 "a" => code_action,
573 "'" => last_picker,
648 "'" => last_picker,
574 "d" => { "Debug" sticky=true
649 "d" => { "Debug" sticky=true
@@ -593,9 +668,14 @@ impl Default for Keymaps {
593 },
668 },
594 "w" => { "Window"
669 "w" => { "Window"
595 "C-w" | "w" => rotate_view,
670 "C-w" | "w" => rotate_view,
596− "C-h" | "h" => hsplit,
671+ "C-s" | "s" => hsplit,
597 "C-v" | "v" => vsplit,
672 "C-v" | "v" => vsplit,
598 "C-q" | "q" => wclose,
673 "C-q" | "q" => wclose,
674+ "C-o" | "o" => wonly,
675+ "C-h" | "h" | "left" => jump_view_left,
676+ "C-j" | "j" | "down" => jump_view_down,
677+ "C-k" | "k" | "up" => jump_view_up,
678+ "C-l" | "l" | "right" => jump_view_right,
599 },
679 },
600 "y" => yank_joined_to_clipboard,
680 "y" => yank_joined_to_clipboard,
601 "Y" => yank_main_selection_to_clipboard,
681 "Y" => yank_main_selection_to_clipboard,
@@ -604,30 +684,31 @@ impl Default for Keymaps {
604 "R" => replace_selections_with_clipboard,
684 "R" => replace_selections_with_clipboard,
605 "/" => global_search,
685 "/" => global_search,
606 "k" => hover,
686 "k" => hover,
687+ "r" => rename_symbol,
607 },
688 },
608 "z" => { "View"
689 "z" => { "View"
609 "z" | "c" => align_view_center,
690 "z" | "c" => align_view_center,
610 "t" => align_view_top,
691 "t" => align_view_top,
611 "b" => align_view_bottom,
692 "b" => align_view_bottom,
612 "m" => align_view_middle,
693 "m" => align_view_middle,
613− "k" => scroll_up,
694+ "k" | "up" => scroll_up,
614− "j" => scroll_down,
695+ "j" | "down" => scroll_down,
615− "b" => page_up,
696+ "C-b" | "pageup" => page_up,
616− "f" => page_down,
697+ "C-f" | "pagedown" => page_down,
617− "u" => half_page_up,
698+ "C-u" => half_page_up,
618− "d" => half_page_down,
699+ "C-d" => half_page_down,
619 },
700 },
620 "Z" => { "View" sticky=true
701 "Z" => { "View" sticky=true
621 "z" | "c" => align_view_center,
702 "z" | "c" => align_view_center,
622 "t" => align_view_top,
703 "t" => align_view_top,
623 "b" => align_view_bottom,
704 "b" => align_view_bottom,
624 "m" => align_view_middle,
705 "m" => align_view_middle,
625− "k" => scroll_up,
706+ "k" | "up" => scroll_up,
626− "j" => scroll_down,
707+ "j" | "down" => scroll_down,
627− "b" => page_up,
708+ "C-b" | "pageup" => page_up,
628− "f" => page_down,
709+ "C-f" | "pagedown" => page_down,
629− "u" => half_page_up,
710+ "C-u" => half_page_up,
630− "d" => half_page_down,
711+ "C-d" => half_page_down,
631 },
712 },
632
713
633 "\"" => select_register,
714 "\"" => select_register,
@@ -637,6 +718,9 @@ impl Default for Keymaps {
637 "A-!" => shell_append_output,
718 "A-!" => shell_append_output,
638 "$" => shell_keep_pipe,
719 "$" => shell_keep_pipe,
639 "C-z" => suspend,
720 "C-z" => suspend,
721+
722+ "C-a" => increment,
723+ "C-x" => decrement,
640 });
724 });
641 let mut select = normal.clone();
725 let mut select = normal.clone();
642 select.merge_nodes(keymap!({ "Select mode"
726 select.merge_nodes(keymap!({ "Select mode"
@@ -670,21 +754,38 @@ impl Default for Keymaps {
670 "esc" => normal_mode,
754 "esc" => normal_mode,
671
755
672 "backspace" => delete_char_backward,
756 "backspace" => delete_char_backward,
757+ "C-h" => delete_char_backward,
673 "del" => delete_char_forward,
758 "del" => delete_char_forward,
759+ "C-d" => delete_char_forward,
674 "ret" => insert_newline,
760 "ret" => insert_newline,
675 "tab" => insert_tab,
761 "tab" => insert_tab,
676 "C-w" => delete_word_backward,
762 "C-w" => delete_word_backward,
763+ "A-d" => delete_word_forward,
677
764
678 "left" => move_char_left,
765 "left" => move_char_left,
766+ "C-b" => move_char_left,
679 "down" => move_line_down,
767 "down" => move_line_down,
768+ "C-n" => move_line_down,
680 "up" => move_line_up,
769 "up" => move_line_up,
770+ "C-p" => move_line_up,
681 "right" => move_char_right,
771 "right" => move_char_right,
772+ "C-f" => move_char_right,
773+ "A-b" => move_prev_word_end,
774+ "A-left" => move_prev_word_end,
775+ "A-f" => move_next_word_start,
776+ "A-right" => move_next_word_start,
682 "pageup" => page_up,
777 "pageup" => page_up,
683 "pagedown" => page_down,
778 "pagedown" => page_down,
684 "home" => goto_line_start,
779 "home" => goto_line_start,
780+ "C-a" => goto_line_start,
685 "end" => goto_line_end_newline,
781 "end" => goto_line_end_newline,
782+ "C-e" => goto_line_end_newline,
783+
784+ "C-k" => kill_to_line_end,
785+ "C-u" => kill_to_line_start,
686
786
687 "C-x" => completion,
787 "C-x" => completion,
788+ "C-r" => insert_register,
688 });
789 });
689 Keymaps(hashmap!(
790 Keymaps(hashmap!(
690 Mode::Normal => Keymap::new(normal),
791 Mode::Normal => Keymap::new(normal),
@@ -706,6 +807,22 @@ pub fn merge_keys(mut config: Config) -> Config {
706#[cfg(test)]
807#[cfg(test)]
707mod tests {
808mod tests {
708 use super::*;
809 use super::*;
810+
811+ #[test]
812+ #[should_panic]
813+ fn duplicate_keys_should_panic() {
814+ keymap!({ "Normal mode"
815+ "i" => normal_mode,
816+ "i" => goto_definition,
817+ });
818+ }
819+
820+ #[test]
821+ fn check_duplicate_keys_in_default_keymap() {
822+ // will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro
823+ Keymaps::default();
824+ }
825+
709 #[test]
826 #[test]
710 fn merge_partial_keys() {
827 fn merge_partial_keys() {
711 let config = Config {
828 let config = Config {
@@ -800,4 +917,20 @@ mod tests {
800 let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
917 let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
801 assert!(!node.node().unwrap().order().is_empty())
918 assert!(!node.node().unwrap().order().is_empty())
802 }
919 }
920+
921+ #[test]
922+ fn aliased_modes_are_same_in_default_keymap() {
923+ let keymaps = Keymaps::default();
924+ let root = keymaps.get(&Mode::Normal).unwrap().root();
925+ assert_eq!(
926+ root.search(&[key!(' '), key!('w')]).unwrap(),
927+ root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
928+ "Mismatch for window mode on `Space-w` and `Ctrl-w`"
929+ );
930+ assert_eq!(
931+ root.search(&[key!('z')]).unwrap(),
932+ root.search(&[key!('Z')]).unwrap(),
933+ "Mismatch for view mode on `z` and `Z`"
934+ );
935+ }
803}
936}
helix-term/src/main.rs+22−12
@@ -16,11 +16,6 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
16 };
16 };
17
17
18 // Separate file config so we can include year, month and day in file logs
18 // Separate file config so we can include year, month and day in file logs
19− let file = std::fs::OpenOptions::new()
20− .write(true)
21− .create(true)
22− .truncate(true)
23− .open(logpath)?;
24 let file_config = fern::Dispatch::new()
19 let file_config = fern::Dispatch::new()
25 .format(|out, message, record| {
20 .format(|out, message, record| {
26 out.finish(format_args!(
21 out.finish(format_args!(
@@ -31,15 +26,20 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
31 message
26 message
32 ))
27 ))
33 })
28 })
34− .chain(file);
29+ .chain(fern::log_file(logpath)?);
35
30
36 base_config.chain(file_config).apply()?;
31 base_config.chain(file_config).apply()?;
37
32
38 Ok(())
33 Ok(())
39}
34}
40
35
36+fn main() -> Result<()> {
37+ let exit_code = main_impl()?;
38+ std::process::exit(exit_code);
39+}
40+
41#[tokio::main]
41#[tokio::main]
42−async fn main() -> Result<()> {
42+async fn main_impl() -> Result<i32> {
43 let cache_dir = helix_core::cache_dir();
43 let cache_dir = helix_core::cache_dir();
44 if !cache_dir.exists() {
44 if !cache_dir.exists() {
45 std::fs::create_dir_all(&cache_dir).ok();
45 std::fs::create_dir_all(&cache_dir).ok();
@@ -66,7 +66,7 @@ FLAGS:
66 -V, --version Prints version information
66 -V, --version Prints version information
67",
67",
68 env!("CARGO_PKG_NAME"),
68 env!("CARGO_PKG_NAME"),
69− env!("CARGO_PKG_VERSION"),
69+ env!("VERSION_AND_GIT_HASH"),
70 env!("CARGO_PKG_AUTHORS"),
70 env!("CARGO_PKG_AUTHORS"),
71 env!("CARGO_PKG_DESCRIPTION"),
71 env!("CARGO_PKG_DESCRIPTION"),
72 logpath.display(),
72 logpath.display(),
@@ -81,7 +81,7 @@ FLAGS:
81 }
81 }
82
82
83 if args.display_version {
83 if args.display_version {
84− println!("helix {}", env!("CARGO_PKG_VERSION"));
84+ println!("helix {}", env!("VERSION_AND_GIT_HASH"));
85 std::process::exit(0);
85 std::process::exit(0);
86 }
86 }
87
87
@@ -91,7 +91,16 @@ FLAGS:
91 }
91 }
92
92
93 let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
93 let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
94− Ok(config) => merge_keys(toml::from_str(&config)?),
94+ Ok(config) => toml::from_str(&config)
95+ .map(merge_keys)
96+ .unwrap_or_else(|err| {
97+ eprintln!("Bad config: {}", err);
98+ eprintln!("Press <ENTER> to continue with default config");
99+ use std::io::Read;
100+ // This waits for an enter press.
101+ let _ = std::io::stdin().read(&mut []);
102+ Config::default()
103+ }),
95 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
104 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
96 Err(err) => return Err(Error::new(err)),
105 Err(err) => return Err(Error::new(err)),
97 };
106 };
@@ -100,7 +109,8 @@ FLAGS:
100
109
101 // TODO: use the thread local executor to spawn the application task separately from the work pool
110 // TODO: use the thread local executor to spawn the application task separately from the work pool
102 let mut app = Application::new(args, config).context("unable to create new application")?;
111 let mut app = Application::new(args, config).context("unable to create new application")?;
103− app.run().await.unwrap();
104
112
105− Ok(())
113+ let exit_code = app.run().await?;
114+
115+ Ok(exit_code)
106}
116}
helix-term/src/ui/editor.rs+25−16
@@ -17,7 +17,7 @@ use helix_core::{
17};
17};
18use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
18use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
19use helix_view::{
19use helix_view::{
20− document::Mode,
20+ document::{Mode, SCRATCH_BUFFER_NAME},
21 editor::LineNumber,
21 editor::LineNumber,
22 graphics::{Color, CursorKind, Modifier, Rect, Style},
22 graphics::{Color, CursorKind, Modifier, Rect, Style},
23 info::Info,
23 info::Info,
@@ -610,7 +610,7 @@ impl EditorView {
610 use tui::{
610 use tui::{
611 layout::Alignment,
611 layout::Alignment,
612 text::Text,
612 text::Text,
613− widgets::{Paragraph, Widget},
613+ widgets::{Paragraph, Widget, Wrap},
614 };
614 };
615
615
616 let cursor = doc
616 let cursor = doc
@@ -665,8 +665,10 @@ impl EditorView {
665 }
665 }
666 }
666 }
667
667
668− let paragraph = Paragraph::new(lines).alignment(Alignment::Right);
668+ let paragraph = Paragraph::new(lines)
669+ .alignment(Alignment::Right)
670+ .wrap(Wrap { trim: true });
669− let width = 80.min(viewport.width);
671+ let width = 100.min(viewport.width);
670 let height = 15.min(viewport.height);
672 let height = 15.min(viewport.height);
671 paragraph.render(
673 paragraph.render(
672 Rect::new(viewport.right() - width, viewport.y + 1, width, height),
674 Rect::new(viewport.right() - width, viewport.y + 1, width, height),
@@ -716,18 +718,20 @@ impl EditorView {
716 }
718 }
717 surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
719 surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
718
720
719− if let Some(path) = doc.relative_path() {
721+ let rel_path = doc.relative_path();
722+ let path = rel_path
723+ .as_ref()
720− let path = path.to_string_lossy();
724+ .map(|p| p.to_string_lossy())
725+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
721
726
722− let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
727+ let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
723− surface.set_stringn(
728+ surface.set_stringn(
724− viewport.x + 8,
729+ viewport.x + 8,
725− viewport.y,
730+ viewport.y,
726− title,
731+ title,
727− viewport.width.saturating_sub(6) as usize,
732+ viewport.width.saturating_sub(6) as usize,
728− base_style,
733+ base_style,
729− );
734+ );
730− }
731
735
732 //-------------------------------
736 //-------------------------------
733 // Right side of the status line.
737 // Right side of the status line.
@@ -830,6 +834,11 @@ impl EditorView {
830 match &key_result.kind {
834 match &key_result.kind {
831 KeymapResultKind::Matched(command) => command.execute(cxt),
835 KeymapResultKind::Matched(command) => command.execute(cxt),
832 KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
836 KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
837+ KeymapResultKind::MatchedSequence(commands) => {
838+ for command in commands {
839+ command.execute(cxt);
840+ }
841+ }
833 KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
842 KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
834 }
843 }
835 None
844 None
@@ -871,7 +880,7 @@ impl EditorView {
871 std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
880 std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
872 }
881 }
873 // special handling for repeat operator
882 // special handling for repeat operator
874− key!('.') => {
883+ key!('.') if self.keymaps.pending().is_empty() => {
875 // first execute whatever put us into insert mode
884 // first execute whatever put us into insert mode
876 self.last_insert.0.execute(cxt);
885 self.last_insert.0.execute(cxt);
877 // then replay the inputs
886 // then replay the inputs
helix-term/src/ui/markdown.rs+10−4
@@ -13,7 +13,7 @@ use helix_core::{
13 Rope,
13 Rope,
14};
14};
15use helix_view::{
15use helix_view::{
16− graphics::{Color, Margin, Rect, Style},
16+ graphics::{Margin, Rect},
17 Theme,
17 Theme,
18};
18};
19
19
@@ -61,9 +61,15 @@ fn parse<'a>(
61 })
61 })
62 }
62 }
63
63
64− let text_style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
64+ let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
65− let code_style = Style::default().fg(Color::Rgb(255, 255, 255)); // white
65+
66+ // TODO: use better scopes for these, `markup.raw.block`, `markup.heading`
67+ let code_style = theme
68+ .map(|theme| theme.get("ui.text.focus"))
69+ .unwrap_or_default(); // white
66− let heading_style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
70+ let heading_style = theme
71+ .map(|theme| theme.get("ui.linenr.selected"))
72+ .unwrap_or_default(); // lilac
67
73
68 for event in parser {
74 for event in parser {
69 match event {
75 match event {
helix-term/src/ui/menu.rs+11−46
@@ -1,5 +1,8 @@
1+use crate::{
1−use crate::compositor::{Component, Compositor, Context, EventResult};
2+ compositor::{Component, Compositor, Context, EventResult},
3+ ctrl, key, shift,
4+};
2−use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
5+use crossterm::event::Event;
3use tui::{buffer::Buffer as Surface, widgets::Table};
6use tui::{buffer::Buffer as Surface, widgets::Table};
4
7
5pub use tui::widgets::{Cell, Row};
8pub use tui::widgets::{Cell, Row};
@@ -192,63 +195,25 @@ impl<T: Item + 'static> Component for Menu<T> {
192 compositor.pop();
195 compositor.pop();
193 })));
196 })));
194
197
195− match event {
198+ match event.into() {
196 // esc or ctrl-c aborts the completion and closes the menu
199 // esc or ctrl-c aborts the completion and closes the menu
197− KeyEvent {
200+ key!(Esc) | ctrl!('c') => {
198− code: KeyCode::Esc, ..
199− }
200− | KeyEvent {
201− code: KeyCode::Char('c'),
202− modifiers: KeyModifiers::CONTROL,
203− } => {
204 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
201 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
205 return close_fn;
202 return close_fn;
206 }
203 }
207 // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
204 // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
208− KeyEvent {
205+ shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
209− code: KeyCode::BackTab,
210− ..
211− }
212− | KeyEvent {
213− code: KeyCode::Up, ..
214− }
215− | KeyEvent {
216− code: KeyCode::Char('p'),
217− modifiers: KeyModifiers::CONTROL,
218− }
219− | KeyEvent {
220− code: KeyCode::Char('k'),
221− modifiers: KeyModifiers::CONTROL,
222− } => {
223 self.move_up();
206 self.move_up();
224 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
207 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
225 return EventResult::Consumed(None);
208 return EventResult::Consumed(None);
226 }
209 }
210+ key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
227− // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
211+ // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
228− KeyEvent {
229− code: KeyCode::Tab,
230− modifiers: KeyModifiers::NONE,
231− }
232− | KeyEvent {
233− code: KeyCode::Down,
234− ..
235− }
236− | KeyEvent {
237− code: KeyCode::Char('n'),
238− modifiers: KeyModifiers::CONTROL,
239− }
240− | KeyEvent {
241− code: KeyCode::Char('j'),
242− modifiers: KeyModifiers::CONTROL,
243− } => {
244 self.move_down();
212 self.move_down();
245 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
213 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
246 return EventResult::Consumed(None);
214 return EventResult::Consumed(None);
247 }
215 }
248− KeyEvent {
249− code: KeyCode::Enter,
216+ key!(Enter) => {
250− ..
251− } => {
252 if let Some(selection) = self.selection() {
217 if let Some(selection) = self.selection() {
253 (self.callback_fn)(cx.editor, Some(selection), MenuEvent::Validate);
218 (self.callback_fn)(cx.editor, Some(selection), MenuEvent::Validate);
254 }
219 }
helix-term/src/ui/mod.rs+2
@@ -35,6 +35,7 @@ pub fn regex_prompt(
35 let (view, doc) = current!(cx.editor);
35 let (view, doc) = current!(cx.editor);
36 let view_id = view.id;
36 let view_id = view.id;
37 let snapshot = doc.selection(view_id).clone();
37 let snapshot = doc.selection(view_id).clone();
38+ let offset_snapshot = view.offset;
38
39
39 Prompt::new(
40 Prompt::new(
40 prompt,
41 prompt,
@@ -45,6 +46,7 @@ pub fn regex_prompt(
45 PromptEvent::Abort => {
46 PromptEvent::Abort => {
46 let (view, doc) = current!(cx.editor);
47 let (view, doc) = current!(cx.editor);
47 doc.set_selection(view.id, snapshot.clone());
48 doc.set_selection(view.id, snapshot.clone());
49+ view.offset = offset_snapshot;
48 }
50 }
49 PromptEvent::Validate => {
51 PromptEvent::Validate => {
50 // TODO: push_jump to store selection just before jump
52 // TODO: push_jump to store selection just before jump
helix-term/src/ui/picker.rs+17−56
@@ -1,8 +1,9 @@
1use crate::{
1use crate::{
2 compositor::{Component, Compositor, Context, EventResult},
2 compositor::{Component, Compositor, Context, EventResult},
3+ ctrl, key, shift,
3 ui::EditorView,
4 ui::EditorView,
4};
5};
5−use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
6+use crossterm::event::Event;
6use tui::{
7use tui::{
7 buffer::Buffer as Surface,
8 buffer::Buffer as Surface,
8 widgets::{Block, BorderType, Borders},
9 widgets::{Block, BorderType, Borders},
@@ -36,6 +37,7 @@ type FileLocation = (PathBuf, Option<(usize, usize)>);
36
37
37pub struct FilePicker<T> {
38pub struct FilePicker<T> {
38 picker: Picker<T>,
39 picker: Picker<T>,
40+ pub truncate_start: bool,
39 /// Caches paths to documents
41 /// Caches paths to documents
40 preview_cache: HashMap<PathBuf, CachedPreview>,
42 preview_cache: HashMap<PathBuf, CachedPreview>,
41 read_buffer: Vec<u8>,
43 read_buffer: Vec<u8>,
@@ -89,6 +91,7 @@ impl<T> FilePicker<T> {
89 ) -> Self {
91 ) -> Self {
90 Self {
92 Self {
91 picker: Picker::new(false, options, format_fn, callback_fn),
93 picker: Picker::new(false, options, format_fn, callback_fn),
94+ truncate_start: true,
92 preview_cache: HashMap::new(),
95 preview_cache: HashMap::new(),
93 read_buffer: Vec::with_capacity(1024),
96 read_buffer: Vec::with_capacity(1024),
94 file_fn: Box::new(preview_fn),
97 file_fn: Box::new(preview_fn),
@@ -171,6 +174,7 @@ impl<T: 'static> Component for FilePicker<T> {
171 };
174 };
172
175
173 let picker_area = area.with_width(picker_width);
176 let picker_area = area.with_width(picker_width);
177+ self.picker.truncate_start = self.truncate_start;
174 self.picker.render(picker_area, surface, cx);
178 self.picker.render(picker_area, surface, cx);
175
179
176 if !render_preview {
180 if !render_preview {
@@ -276,6 +280,8 @@ pub struct Picker<T> {
276 prompt: Prompt,
280 prompt: Prompt,
277 /// Whether to render in the middle of the area
281 /// Whether to render in the middle of the area
278 render_centered: bool,
282 render_centered: bool,
283+ /// Wheather to truncate the start (default true)
284+ pub truncate_start: bool,
279
285
280 format_fn: Box<dyn Fn(&T) -> Cow<str>>,
286 format_fn: Box<dyn Fn(&T) -> Cow<str>>,
281 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
287 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
@@ -306,6 +312,7 @@ impl<T> Picker<T> {
306 cursor: 0,
312 cursor: 0,
307 prompt,
313 prompt,
308 render_centered,
314 render_centered,
315+ truncate_start: true,
309 format_fn: Box::new(format_fn),
316 format_fn: Box::new(format_fn),
310 callback_fn: Box::new(callback_fn),
317 callback_fn: Box::new(callback_fn),
311 };
318 };
@@ -403,81 +410,35 @@ impl<T: 'static> Component for Picker<T> {
403 compositor.last_picker = compositor.pop();
410 compositor.last_picker = compositor.pop();
404 })));
411 })));
405
412
406− match key_event {
413+ match key_event.into() {
407− KeyEvent {
414+ shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
408− code: KeyCode::Up, ..
409− }
410− | KeyEvent {
411− code: KeyCode::BackTab,
412− ..
413− }
414− | KeyEvent {
415− code: KeyCode::Char('k'),
416− modifiers: KeyModifiers::CONTROL,
417− }
418− | KeyEvent {
419− code: KeyCode::Char('p'),
420− modifiers: KeyModifiers::CONTROL,
421− } => {
422 self.move_up();
415 self.move_up();
423 }
416 }
424− KeyEvent {
417+ key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
425− code: KeyCode::Down,
426− ..
427− }
428− | KeyEvent {
429− code: KeyCode::Tab, ..
430− }
431− | KeyEvent {
432− code: KeyCode::Char('j'),
433− modifiers: KeyModifiers::CONTROL,
434− }
435− | KeyEvent {
436− code: KeyCode::Char('n'),
437− modifiers: KeyModifiers::CONTROL,
438− } => {
439 self.move_down();
418 self.move_down();
440 }
419 }
441− KeyEvent {
420+ key!(Esc) | ctrl!('c') => {
442− code: KeyCode::Esc, ..
443− }
444− | KeyEvent {
445− code: KeyCode::Char('c'),
446− modifiers: KeyModifiers::CONTROL,
447− } => {
448 return close_fn;
421 return close_fn;
449 }
422 }
450− KeyEvent {
451− code: KeyCode::Enter,
423+ key!(Enter) => {
452− ..
453− } => {
454 if let Some(option) = self.selection() {
424 if let Some(option) = self.selection() {
455 (self.callback_fn)(cx, option, Action::Replace);
425 (self.callback_fn)(cx, option, Action::Replace);
456 }
426 }
457 return close_fn;
427 return close_fn;
458 }
428 }
459− KeyEvent {
429+ ctrl!('s') => {
460− code: KeyCode::Char('s'),
461− modifiers: KeyModifiers::CONTROL,
462− } => {
463 if let Some(option) = self.selection() {
430 if let Some(option) = self.selection() {
464 (self.callback_fn)(cx, option, Action::HorizontalSplit);
431 (self.callback_fn)(cx, option, Action::HorizontalSplit);
465 }
432 }
466 return close_fn;
433 return close_fn;
467 }
434 }
468− KeyEvent {
435+ ctrl!('v') => {
469− code: KeyCode::Char('v'),
470− modifiers: KeyModifiers::CONTROL,
471− } => {
472 if let Some(option) = self.selection() {
436 if let Some(option) = self.selection() {
473 (self.callback_fn)(cx, option, Action::VerticalSplit);
437 (self.callback_fn)(cx, option, Action::VerticalSplit);
474 }
438 }
475 return close_fn;
439 return close_fn;
476 }
440 }
477− KeyEvent {
441+ ctrl!(' ') => {
478− code: KeyCode::Char(' '),
479− modifiers: KeyModifiers::CONTROL,
480− } => {
481 self.save_filter();
442 self.save_filter();
482 }
443 }
483 _ => {
444 _ => {
@@ -567,7 +528,7 @@ impl<T: 'static> Component for Picker<T> {
567 text_style
528 text_style
568 },
529 },
569 true,
530 true,
570− true,
531+ self.truncate_start,
571 );
532 );
572 }
533 }
573 }
534 }
helix-term/src/ui/popup.rs+9−19
@@ -1,5 +1,8 @@
1+use crate::{
1−use crate::compositor::{Component, Compositor, Context, EventResult};
2+ compositor::{Component, Compositor, Context, EventResult},
3+ ctrl, key,
4+};
2−use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
5+use crossterm::event::Event;
3use tui::buffer::Buffer as Surface;
6use tui::buffer::Buffer as Surface;
4
7
5use helix_core::Position;
8use helix_core::Position;
@@ -95,27 +98,14 @@ impl<T: Component> Component for Popup<T> {
95 compositor.pop();
98 compositor.pop();
96 })));
99 })));
97
100
98− match key {
101+ match key.into() {
99 // esc or ctrl-c aborts the completion and closes the menu
102 // esc or ctrl-c aborts the completion and closes the menu
100− KeyEvent {
101− code: KeyCode::Esc, ..
102− }
103− | KeyEvent {
104− code: KeyCode::Char('c'),
105− modifiers: KeyModifiers::CONTROL,
106− } => close_fn,
103+ key!(Esc) | ctrl!('c') => close_fn,
107−
104+ ctrl!('d') => {
108− KeyEvent {
109− code: KeyCode::Char('d'),
110− modifiers: KeyModifiers::CONTROL,
111− } => {
112 self.scroll(self.size.1 as usize / 2, true);
105 self.scroll(self.size.1 as usize / 2, true);
113 EventResult::Consumed(None)
106 EventResult::Consumed(None)
114 }
107 }
115− KeyEvent {
108+ ctrl!('u') => {
116− code: KeyCode::Char('u'),
117− modifiers: KeyModifiers::CONTROL,
118− } => {
119 self.scroll(self.size.1 as usize / 2, false);
109 self.scroll(self.size.1 as usize / 2, false);
120 EventResult::Consumed(None)
110 EventResult::Consumed(None)
121 }
111 }
helix-term/src/ui/prompt.rs+54−104
@@ -1,6 +1,8 @@
1use crate::compositor::{Component, Compositor, Context, EventResult};
1use crate::compositor::{Component, Compositor, Context, EventResult};
2−use crate::ui;
2+use crate::{alt, ctrl, key, shift, ui};
3−use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
3+use crossterm::event::Event;
4+use helix_view::input::KeyEvent;
5+use helix_view::keyboard::{KeyCode, KeyModifiers};
4use std::{borrow::Cow, ops::RangeFrom};
6use std::{borrow::Cow, ops::RangeFrom};
5use tui::buffer::Buffer as Surface;
7use tui::buffer::Buffer as Surface;
6
8
@@ -213,6 +215,14 @@ impl Prompt {
213 self.completion = (self.completion_fn)(&self.line);
215 self.completion = (self.completion_fn)(&self.line);
214 }
216 }
215
217
218+ pub fn delete_char_forwards(&mut self) {
219+ let pos = self.eval_movement(Movement::ForwardChar(1));
220+ self.line.replace_range(self.cursor..pos, "");
221+
222+ self.exit_selection();
223+ self.completion = (self.completion_fn)(&self.line);
224+ }
225+
216 pub fn delete_word_backwards(&mut self) {
226 pub fn delete_word_backwards(&mut self) {
217 let pos = self.eval_movement(Movement::BackwardWord(1));
227 let pos = self.eval_movement(Movement::BackwardWord(1));
218 self.line.replace_range(pos..self.cursor, "");
228 self.line.replace_range(pos..self.cursor, "");
@@ -222,6 +232,23 @@ impl Prompt {
222 self.completion = (self.completion_fn)(&self.line);
232 self.completion = (self.completion_fn)(&self.line);
223 }
233 }
224
234
235+ pub fn delete_word_forwards(&mut self) {
236+ let pos = self.eval_movement(Movement::ForwardWord(1));
237+ self.line.replace_range(self.cursor..pos, "");
238+
239+ self.exit_selection();
240+ self.completion = (self.completion_fn)(&self.line);
241+ }
242+
243+ pub fn kill_to_start_of_line(&mut self) {
244+ let pos = self.eval_movement(Movement::StartOfLine);
245+ self.line.replace_range(pos..self.cursor, "");
246+ self.cursor = pos;
247+
248+ self.exit_selection();
249+ self.completion = (self.completion_fn)(&self.line);
250+ }
251+
225 pub fn kill_to_end_of_line(&mut self) {
252 pub fn kill_to_end_of_line(&mut self) {
226 let pos = self.eval_movement(Movement::EndOfLine);
253 let pos = self.eval_movement(Movement::EndOfLine);
227 self.line.replace_range(self.cursor..pos, "");
254 self.line.replace_range(self.cursor..pos, "");
@@ -405,84 +432,30 @@ impl Component for Prompt {
405 compositor.pop();
432 compositor.pop();
406 })));
433 })));
407
434
408− match event {
435+ match event.into() {
409− KeyEvent {
436+ ctrl!('c') | key!(Esc) => {
410− code: KeyCode::Char('c'),
411− modifiers: KeyModifiers::CONTROL,
412− }
413− | KeyEvent {
414− code: KeyCode::Esc, ..
415− } => {
416 (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
437 (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
417 return close_fn;
438 return close_fn;
418 }
439 }
419− KeyEvent {
420− code: KeyCode::Left,
421− modifiers: KeyModifiers::ALT,
422− }
423− | KeyEvent {
424− code: KeyCode::Char('b'),
425− modifiers: KeyModifiers::ALT,
426− } => self.move_cursor(Movement::BackwardWord(1)),
440+ alt!('b') | alt!(Left) => self.move_cursor(Movement::BackwardWord(1)),
427− KeyEvent {
428− code: KeyCode::Right,
429− modifiers: KeyModifiers::ALT,
430− }
431− | KeyEvent {
432− code: KeyCode::Char('f'),
433− modifiers: KeyModifiers::ALT,
434− } => self.move_cursor(Movement::ForwardWord(1)),
441+ alt!('f') | alt!(Right) => self.move_cursor(Movement::ForwardWord(1)),
435− KeyEvent {
442+ ctrl!('b') | key!(Left) => self.move_cursor(Movement::BackwardChar(1)),
436− code: KeyCode::Char('f'),
437− modifiers: KeyModifiers::CONTROL,
438− }
439− | KeyEvent {
440− code: KeyCode::Right,
441− ..
442− } => self.move_cursor(Movement::ForwardChar(1)),
443+ ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)),
443− KeyEvent {
444− code: KeyCode::Char('b'),
445− modifiers: KeyModifiers::CONTROL,
446− }
447− | KeyEvent {
448− code: KeyCode::Left,
449− ..
450− } => self.move_cursor(Movement::BackwardChar(1)),
451− KeyEvent {
452− code: KeyCode::End,
453− modifiers: KeyModifiers::NONE,
454− }
455− | KeyEvent {
456− code: KeyCode::Char('e'),
457− modifiers: KeyModifiers::CONTROL,
458− } => self.move_end(),
444+ ctrl!('e') | key!(End) => self.move_end(),
459− KeyEvent {
460− code: KeyCode::Home,
461− modifiers: KeyModifiers::NONE,
462− }
463− | KeyEvent {
464− code: KeyCode::Char('a'),
465− modifiers: KeyModifiers::CONTROL,
466− } => self.move_start(),
445+ ctrl!('a') | key!(Home) => self.move_start(),
467− KeyEvent {
468− code: KeyCode::Char('w'),
469− modifiers: KeyModifiers::CONTROL,
470− } => self.delete_word_backwards(),
446+ ctrl!('w') => self.delete_word_backwards(),
471− KeyEvent {
447+ alt!('d') => self.delete_word_forwards(),
472− code: KeyCode::Char('k'),
473− modifiers: KeyModifiers::CONTROL,
474− } => self.kill_to_end_of_line(),
448+ ctrl!('k') => self.kill_to_end_of_line(),
475− KeyEvent {
449+ ctrl!('u') => self.kill_to_start_of_line(),
476− code: KeyCode::Backspace,
450+ ctrl!('h') | key!(Backspace) => {
477− modifiers: KeyModifiers::NONE,
478− } => {
479 self.delete_char_backwards();
451 self.delete_char_backwards();
480 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
452 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
481 }
453 }
482− KeyEvent {
454+ ctrl!('d') | key!(Delete) => {
483− code: KeyCode::Char('s'),
455+ self.delete_char_forwards();
484− modifiers: KeyModifiers::CONTROL,
456+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
485− } => {
457+ }
458+ ctrl!('s') => {
486 let (view, doc) = current!(cx.editor);
459 let (view, doc) = current!(cx.editor);
487 let text = doc.text().slice(..);
460 let text = doc.text().slice(..);
488
461
@@ -492,6 +465,7 @@ impl Component for Prompt {
492 doc.selection(view.id).primary(),
465 doc.selection(view.id).primary(),
493 textobject::TextObject::Inside,
466 textobject::TextObject::Inside,
494 1,
467 1,
468+ false,
495 );
469 );
496 let line = text.slice(range.from()..range.to()).to_string();
470 let line = text.slice(range.from()..range.to()).to_string();
497 if !line.is_empty() {
471 if !line.is_empty() {
@@ -499,10 +473,7 @@ impl Component for Prompt {
499 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
473 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
500 }
474 }
501 }
475 }
502− KeyEvent {
503− code: KeyCode::Enter,
476+ key!(Enter) => {
504− ..
505− } => {
506 if self.selection.is_some() && self.line.ends_with('/') {
477 if self.selection.is_some() && self.line.ends_with('/') {
507 self.completion = (self.completion_fn)(&self.line);
478 self.completion = (self.completion_fn)(&self.line);
508 self.exit_selection();
479 self.exit_selection();
@@ -517,50 +488,29 @@ impl Component for Prompt {
517 return close_fn;
488 return close_fn;
518 }
489 }
519 }
490 }
520− KeyEvent {
491+ ctrl!('p') | key!(Up) => {
521− code: KeyCode::Char('p'),
522− modifiers: KeyModifiers::CONTROL,
523− }
524− | KeyEvent {
525− code: KeyCode::Up, ..
526− } => {
527 if let Some(register) = self.history_register {
492 if let Some(register) = self.history_register {
528 let register = cx.editor.registers.get_mut(register);
493 let register = cx.editor.registers.get_mut(register);
529 self.change_history(register.read(), CompletionDirection::Backward);
494 self.change_history(register.read(), CompletionDirection::Backward);
530 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
495 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
531 }
496 }
532 }
497 }
533− KeyEvent {
498+ ctrl!('n') | key!(Down) => {
534− code: KeyCode::Char('n'),
535− modifiers: KeyModifiers::CONTROL,
536− }
537− | KeyEvent {
538− code: KeyCode::Down,
539− ..
540− } => {
541 if let Some(register) = self.history_register {
499 if let Some(register) = self.history_register {
542 let register = cx.editor.registers.get_mut(register);
500 let register = cx.editor.registers.get_mut(register);
543 self.change_history(register.read(), CompletionDirection::Forward);
501 self.change_history(register.read(), CompletionDirection::Forward);
544 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
502 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
545 }
503 }
546 }
504 }
547− KeyEvent {
505+ key!(Tab) => {
548− code: KeyCode::Tab, ..
549− } => {
550 self.change_completion_selection(CompletionDirection::Forward);
506 self.change_completion_selection(CompletionDirection::Forward);
551 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
507 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
552 }
508 }
553− KeyEvent {
554− code: KeyCode::BackTab,
509+ shift!(BackTab) => {
555− ..
556− } => {
557 self.change_completion_selection(CompletionDirection::Backward);
510 self.change_completion_selection(CompletionDirection::Backward);
558 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
511 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
559 }
512 }
560− KeyEvent {
561− code: KeyCode::Char('q'),
562− modifiers: KeyModifiers::CONTROL,
563− } => self.exit_selection(),
513+ ctrl!('q') => self.exit_selection(),
564 // any char event that's not combined with control or mapped to any other combo
514 // any char event that's not combined with control or mapped to any other combo
565 KeyEvent {
515 KeyEvent {
566 code: KeyCode::Char(c),
516 code: KeyCode::Char(c),
helix-view/src/document.rs+26−6
@@ -25,6 +25,8 @@ const BUF_SIZE: usize = 8192;
25
25
26const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
26const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
27
27
28+pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
29+
28#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
30#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
29pub enum Mode {
31pub enum Mode {
30 Normal,
32 Normal,
@@ -96,7 +98,7 @@ pub struct Document {
96 // It can be used as a cell where we will take it out to get some parts of the history and put
98 // It can be used as a cell where we will take it out to get some parts of the history and put
97 // it back as it separated from the edits. We could split out the parts manually but that will
99 // it back as it separated from the edits. We could split out the parts manually but that will
98 // be more troublesome.
100 // be more troublesome.
99− history: Cell<History>,
101+ pub history: Cell<History>,
100
102
101 pub savepoint: Option<Transaction>,
103 pub savepoint: Option<Transaction>,
102
104
@@ -494,7 +496,9 @@ impl Document {
494 /// Detect the programming language based on the file type.
496 /// Detect the programming language based on the file type.
495 pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
497 pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
496 if let Some(path) = &self.path {
498 if let Some(path) = &self.path {
499+ let language_config = config_loader
497− let language_config = config_loader.language_config_for_file_name(path);
500+ .language_config_for_file_name(path)
501+ .or_else(|| config_loader.language_config_for_shebang(self.text()));
498 self.set_language(theme, language_config);
502 self.set_language(theme, language_config);
499 }
503 }
500 }
504 }
@@ -749,19 +753,35 @@ impl Document {
749 }
753 }
750
754
751 /// Undo modifications to the [`Document`] according to `uk`.
755 /// Undo modifications to the [`Document`] according to `uk`.
752− pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
756+ pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
753 let txns = self.history.get_mut().earlier(uk);
757 let txns = self.history.get_mut().earlier(uk);
758+ let mut success = false;
754 for txn in txns {
759 for txn in txns {
755− self.apply_impl(&txn, view_id);
760+ if self.apply_impl(&txn, view_id) {
761+ success = true;
762+ }
756 }
763 }
764+ if success {
765+ // reset changeset to fix len
766+ self.changes = ChangeSet::new(self.text());
767+ }
768+ success
757 }
769 }
758
770
759 /// Redo modifications to the [`Document`] according to `uk`.
771 /// Redo modifications to the [`Document`] according to `uk`.
760− pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) {
772+ pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
761 let txns = self.history.get_mut().later(uk);
773 let txns = self.history.get_mut().later(uk);
774+ let mut success = false;
762 for txn in txns {
775 for txn in txns {
763− self.apply_impl(&txn, view_id);
776+ if self.apply_impl(&txn, view_id) {
777+ success = true;
778+ }
779+ }
780+ if success {
781+ // reset changeset to fix len
782+ self.changes = ChangeSet::new(self.text());
764 }
783 }
784+ success
765 }
785 }
766
786
767 /// Commit pending changes to history
787 /// Commit pending changes to history
helix-view/src/editor.rs+116−33
@@ -1,5 +1,6 @@
1use crate::{
1use crate::{
2 clipboard::{get_clipboard_provider, ClipboardProvider},
2 clipboard::{get_clipboard_provider, ClipboardProvider},
3+ document::SCRATCH_BUFFER_NAME,
3 graphics::{CursorKind, Rect},
4 graphics::{CursorKind, Rect},
4 theme::{self, Theme},
5 theme::{self, Theme},
5 tree::{self, Tree},
6 tree::{self, Tree},
@@ -11,8 +12,8 @@ use futures_util::stream::select_all::SelectAll;
11use tokio_stream::wrappers::UnboundedReceiverStream;
12use tokio_stream::wrappers::UnboundedReceiverStream;
12
13
13use std::{
14use std::{
14− collections::BTreeMap,
15+ collections::{BTreeMap, HashMap},
15− collections::HashMap,
16+ io::stdin,
16 path::{Path, PathBuf},
17 path::{Path, PathBuf},
17 pin::Pin,
18 pin::Pin,
18 sync::Arc,
19 sync::Arc,
@@ -25,8 +26,8 @@ use anyhow::Error;
25pub use helix_core::diagnostic::Severity;
26pub use helix_core::diagnostic::Severity;
26pub use helix_core::register::Registers;
27pub use helix_core::register::Registers;
27use helix_core::syntax;
28use helix_core::syntax;
28−use helix_core::Position;
29use helix_dap as dap;
29use helix_dap as dap;
30+use helix_core::{Position, Selection};
30
31
31use serde::Deserialize;
32use serde::Deserialize;
32
33
@@ -39,7 +40,7 @@ where
39}
40}
40
41
41#[derive(Debug, Clone, PartialEq, Deserialize)]
42#[derive(Debug, Clone, PartialEq, Deserialize)]
42−#[serde(rename_all = "kebab-case", default)]
43+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
43pub struct Config {
44pub struct Config {
44 /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
45 /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
45 pub scrolloff: usize,
46 pub scrolloff: usize,
@@ -138,6 +139,8 @@ pub struct Editor {
138
139
139 pub idle_timer: Pin<Box<Sleep>>,
140 pub idle_timer: Pin<Box<Sleep>>,
140 pub last_motion: Option<Motion>,
141 pub last_motion: Option<Motion>,
142+
143+ pub exit_code: i32,
141}
144}
142
145
143#[derive(Debug, Copy, Clone)]
146#[derive(Debug, Copy, Clone)]
@@ -179,6 +182,7 @@ impl Editor {
179 idle_timer: Box::pin(sleep(config.idle_timeout)),
182 idle_timer: Box::pin(sleep(config.idle_timeout)),
180 last_motion: None,
183 last_motion: None,
181 config,
184 config,
185+ exit_code: 0,
182 }
186 }
183 }
187 }
184
188
@@ -208,6 +212,12 @@ impl Editor {
208 }
212 }
209
213
210 pub fn set_theme(&mut self, theme: Theme) {
214 pub fn set_theme(&mut self, theme: Theme) {
215+ // `ui.selection` is the only scope required to be able to render a theme.
216+ if theme.find_scope_index("ui.selection").is_none() {
217+ self.set_error("Invalid theme: `ui.selection` required".to_owned());
218+ return;
219+ }
220+
211 let scopes = theme.scopes();
221 let scopes = theme.scopes();
212 for config in self
222 for config in self
213 .syn_loader
223 .syn_loader
@@ -238,9 +248,28 @@ impl Editor {
238 }
248 }
239 }
249 }
240
250
251+ fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
252+ let view = self.tree.get_mut(current_view);
253+ view.doc = doc_id;
254+ view.offset = Position::default();
255+
256+ let doc = self.documents.get_mut(&doc_id).unwrap();
257+
258+ // initialize selection for view
259+ doc.selections
260+ .entry(view.id)
261+ .or_insert_with(|| Selection::point(0));
262+ // TODO: reuse align_view
263+ let pos = doc
264+ .selection(view.id)
265+ .primary()
266+ .cursor(doc.text().slice(..));
267+ let line = doc.text().char_to_line(pos);
268+ view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
269+ }
270+
241 pub fn switch(&mut self, id: DocumentId, action: Action) {
271 pub fn switch(&mut self, id: DocumentId, action: Action) {
242 use crate::tree::Layout;
272 use crate::tree::Layout;
243− use helix_core::Selection;
244
273
245 if !self.documents.contains_key(&id) {
274 if !self.documents.contains_key(&id) {
246 log::error!("cannot switch to document that does not exist (anymore)");
275 log::error!("cannot switch to document that does not exist (anymore)");
@@ -274,26 +303,19 @@ impl Editor {
274 view.jumps.push(jump);
303 view.jumps.push(jump);
275 view.last_accessed_doc = Some(view.doc);
304 view.last_accessed_doc = Some(view.doc);
276 }
305 }
277− view.doc = id;
278− view.offset = Position::default();
279
306
280− let (view, doc) = current!(self);
307+ let view_id = view.id;
281−
308+ self.replace_document_in_view(view_id, id);
282− // initialize selection for view
283− doc.selections
284− .entry(view.id)
285− .or_insert_with(|| Selection::point(0));
286− // TODO: reuse align_view
287− let pos = doc
288− .selection(view.id)
289− .primary()
290− .cursor(doc.text().slice(..));
291− let line = doc.text().char_to_line(pos);
292− view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
293
309
294 return;
310 return;
295 }
311 }
296 Action::Load => {
312 Action::Load => {
313+ let view_id = view!(self).id;
314+ if let Some(doc) = self.document_mut(id) {
315+ if doc.selections().is_empty() {
316+ doc.selections.insert(view_id, Selection::point(0));
317+ }
318+ }
297 return;
319 return;
298 }
320 }
299 Action::HorizontalSplit => {
321 Action::HorizontalSplit => {
@@ -315,16 +337,29 @@ impl Editor {
315 self._refresh();
337 self._refresh();
316 }
338 }
317
339
318− pub fn new_file(&mut self, action: Action) -> DocumentId {
340+ fn new_document(&mut self, mut document: Document) -> DocumentId {
319 let id = DocumentId(self.next_document_id);
341 let id = DocumentId(self.next_document_id);
320 self.next_document_id += 1;
342 self.next_document_id += 1;
321− let mut doc = Document::default();
343+ document.id = id;
322− doc.id = id;
344+ self.documents.insert(id, document);
323− self.documents.insert(id, doc);
345+ id
346+ }
347+
348+ fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
349+ let id = self.new_document(document);
324 self.switch(id, action);
350 self.switch(id, action);
325 id
351 id
326 }
352 }
327
353
354+ pub fn new_file(&mut self, action: Action) -> DocumentId {
355+ self.new_file_from_document(action, Document::default())
356+ }
357+
358+ pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
359+ let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?;
360+ Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
361+ }
362+
328 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
363 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
329 let path = helix_core::path::get_canonicalized_path(&path)?;
364 let path = helix_core::path::get_canonicalized_path(&path)?;
330
365
@@ -381,7 +416,7 @@ impl Editor {
381 Ok(id)
416 Ok(id)
382 }
417 }
383
418
384− pub fn close(&mut self, id: ViewId, close_buffer: bool) {
419+ pub fn close(&mut self, id: ViewId) {
385 let view = self.tree.get(self.tree.focus);
420 let view = self.tree.get(self.tree.focus);
386 // remove selection
421 // remove selection
387 self.documents
422 self.documents
@@ -390,18 +425,66 @@ impl Editor {
390 .selections
425 .selections
391 .remove(&id);
426 .remove(&id);
392
427
393− if close_buffer {
428+ self.tree.remove(id);
394− // get around borrowck issues
429+ self._refresh();
395− let doc = &self.documents[&view.doc];
430+ }
396
431
432+ pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
433+ let doc = match self.documents.get(&doc_id) {
434+ Some(doc) => doc,
435+ None => anyhow::bail!("document does not exist"),
436+ };
437+
438+ if !force && doc.is_modified() {
439+ anyhow::bail!(
440+ "buffer {:?} is modified",
441+ doc.relative_path()
442+ .map(|path| path.to_string_lossy().to_string())
443+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
444+ );
445+ }
446+
397− if let Some(language_server) = doc.language_server() {
447+ if let Some(language_server) = doc.language_server() {
398− tokio::spawn(language_server.text_document_did_close(doc.identifier()));
448+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
449+ }
450+
451+ let views_to_close = self
452+ .tree
453+ .views()
454+ .filter_map(|(view, _focus)| {
455+ if view.doc == doc_id {
456+ Some(view.id)
457+ } else {
458+ None
459+ }
399− }
460+ })
461+ .collect::<Vec<_>>();
462+
463+ for view_id in views_to_close {
464+ self.close(view_id);
465+ }
466+
400− self.documents.remove(&view.doc);
467+ self.documents.remove(&doc_id);
468+
469+ // If the document we removed was visible in all views, we will have no more views. We don't
470+ // want to close the editor just for a simple buffer close, so we need to create a new view
471+ // containing either an existing document, or a brand new document.
472+ if self.tree.views().peekable().peek().is_none() {
473+ let doc_id = self
474+ .documents
475+ .iter()
476+ .map(|(&doc_id, _)| doc_id)
477+ .next()
478+ .unwrap_or_else(|| self.new_document(Document::default()));
479+ let view = View::new(doc_id);
480+ let view_id = self.tree.insert(view);
481+ let doc = self.documents.get_mut(&doc_id).unwrap();
482+ doc.selections.insert(view_id, Selection::point(0));
401 }
483 }
402
484
403− self.tree.remove(id);
404 self._refresh();
485 self._refresh();
486+
487+ Ok(())
405 }
488 }
406
489
407 pub fn resize(&mut self, area: Rect) {
490 pub fn resize(&mut self, area: Rect) {
helix-view/src/macros.rs+20−20
@@ -11,10 +11,19 @@
11/// Returns `(&mut View, &mut Document)`
11/// Returns `(&mut View, &mut Document)`
12#[macro_export]
12#[macro_export]
13macro_rules! current {
13macro_rules! current {
14− ( $( $editor:ident ).+ ) => {{
14+ ($editor:expr) => {{
15− let view = $crate::view_mut!( $( $editor ).+ );
15+ let view = $crate::view_mut!($editor);
16 let id = view.doc;
16 let id = view.doc;
17− let doc = $( $editor ).+ .documents.get_mut(&id).unwrap();
17+ let doc = $editor.documents.get_mut(&id).unwrap();
18+ (view, doc)
19+ }};
20+}
21+
22+#[macro_export]
23+macro_rules! current_ref {
24+ ($editor:expr) => {{
25+ let view = $editor.tree.get($editor.tree.focus);
26+ let doc = &$editor.documents[&view.doc];
18 (view, doc)
27 (view, doc)
19 }};
28 }};
20}
29}
@@ -23,8 +32,8 @@ macro_rules! current {
23/// Returns `&mut Document`
32/// Returns `&mut Document`
24#[macro_export]
33#[macro_export]
25macro_rules! doc_mut {
34macro_rules! doc_mut {
26− ( $( $editor:ident ).+ ) => {{
35+ ($editor:expr) => {{
27− $crate::current!( $( $editor ).+ ).1
36+ $crate::current!($editor).1
28 }};
37 }};
29}
38}
30
39
@@ -32,8 +41,8 @@ macro_rules! doc_mut {
32/// Returns `&mut View`
41/// Returns `&mut View`
33#[macro_export]
42#[macro_export]
34macro_rules! view_mut {
43macro_rules! view_mut {
35− ( $( $editor:ident ).+ ) => {{
44+ ($editor:expr) => {{
36− $( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus)
45+ $editor.tree.get_mut($editor.tree.focus)
37 }};
46 }};
38}
47}
39
48
@@ -41,23 +50,14 @@ macro_rules! view_mut {
41/// Returns `&View`
50/// Returns `&View`
42#[macro_export]
51#[macro_export]
43macro_rules! view {
52macro_rules! view {
44− ( $( $editor:ident ).+ ) => {{
53+ ($editor:expr) => {{
45− $( $editor ).+ .tree.get($( $editor ).+ .tree.focus)
54+ $editor.tree.get($editor.tree.focus)
46 }};
55 }};
47}
56}
48
57
49#[macro_export]
58#[macro_export]
50macro_rules! doc {
59macro_rules! doc {
51− ( $( $editor:ident ).+ ) => {{
60+ ($editor:expr) => {{
52− $crate::current_ref!( $( $editor ).+ ).1
61+ $crate::current_ref!($editor).1
53− }};
54−}
55−
56−#[macro_export]
57−macro_rules! current_ref {
58− ( $( $editor:ident ).+ ) => {{
59− let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
60− let doc = &$( $editor ).+ .documents[&view.doc];
61− (view, doc)
62 }};
62 }};
63}
63}
helix-view/src/tree.rs+3
@@ -314,6 +314,9 @@ impl Tree {
314
314
315 pub fn recalculate(&mut self) {
315 pub fn recalculate(&mut self) {
316 if self.is_empty() {
316 if self.is_empty() {
317+ // There are no more views, so the tree should focus itself again.
318+ self.focus = self.root;
319+
317 return;
320 return;
318 }
321 }
319
322
helix-view/src/view.rs+37−8
@@ -54,6 +54,10 @@ impl JumpList {
54 None
54 None
55 }
55 }
56 }
56 }
57+
58+ pub fn remove(&mut self, doc_id: &DocumentId) {
59+ self.jumps.retain(|(other_id, _)| other_id != doc_id);
60+ }
57}
61}
58
62
59#[derive(Debug)]
63#[derive(Debug)]
@@ -85,7 +89,12 @@ impl View {
85 self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
89 self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
86 }
90 }
87
91
88− pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
92+ //
93+ pub fn offset_coords_to_in_view(
94+ &self,
95+ doc: &Document,
96+ scrolloff: usize,
97+ ) -> Option<(usize, usize)> {
89 let cursor = doc
98 let cursor = doc
90 .selection(self.id)
99 .selection(self.id)
91 .primary()
100 .primary()
@@ -104,23 +113,43 @@ impl View {
104
113
105 let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
114 let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
106
115
107− if line > last_line.saturating_sub(scrolloff) {
116+ let row = if line > last_line.saturating_sub(scrolloff) {
108 // scroll down
117 // scroll down
109− self.offset.row += line - (last_line.saturating_sub(scrolloff));
118+ self.offset.row + line - (last_line.saturating_sub(scrolloff))
110 } else if line < self.offset.row + scrolloff {
119 } else if line < self.offset.row + scrolloff {
111 // scroll up
120 // scroll up
112− self.offset.row = line.saturating_sub(scrolloff);
121+ line.saturating_sub(scrolloff)
113− }
122+ } else {
123+ self.offset.row
124+ };
114
125
115− if col > last_col.saturating_sub(scrolloff) {
126+ let col = if col > last_col.saturating_sub(scrolloff) {
116 // scroll right
127 // scroll right
117− self.offset.col += col - (last_col.saturating_sub(scrolloff));
128+ self.offset.col + col - (last_col.saturating_sub(scrolloff))
118 } else if col < self.offset.col + scrolloff {
129 } else if col < self.offset.col + scrolloff {
119 // scroll left
130 // scroll left
120− self.offset.col = col.saturating_sub(scrolloff);
131+ col.saturating_sub(scrolloff)
132+ } else {
133+ self.offset.col
134+ };
135+ if row == self.offset.row && col == self.offset.col {
136+ None
137+ } else {
138+ Some((row, col))
139+ }
140+ }
141+
142+ pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
143+ if let Some((row, col)) = self.offset_coords_to_in_view(doc, scrolloff) {
144+ self.offset.row = row;
145+ self.offset.col = col;
121 }
146 }
122 }
147 }
123
148
149+ pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool {
150+ self.offset_coords_to_in_view(doc, scrolloff).is_none()
151+ }
152+
124 /// Calculates the last visible line on screen
153 /// Calculates the last visible line on screen
125 #[inline]
154 #[inline]
126 pub fn last_line(&self, doc: &Document) -> usize {
155 pub fn last_line(&self, doc: &Document) -> usize {
languages.toml+41
@@ -60,12 +60,25 @@ name = "elixir"
60scope = "source.elixir"
60scope = "source.elixir"
61injection-regex = "elixir"
61injection-regex = "elixir"
62file-types = ["ex", "exs"]
62file-types = ["ex", "exs"]
63+shebangs = ["elixir"]
63roots = []
64roots = []
64comment-token = "#"
65comment-token = "#"
65
66
66language-server = { command = "elixir-ls" }
67language-server = { command = "elixir-ls" }
67indent = { tab-width = 2, unit = " " }
68indent = { tab-width = 2, unit = " " }
68
69
70+[[language]]
71+name = "mint"
72+scope = "source.mint"
73+injection-regex = "mint"
74+file-types = ["mint"]
75+shebangs = []
76+roots = []
77+comment-token = "//"
78+
79+language-server = { command = "mint", args = ["ls"] }
80+indent = { tab-width = 2, unit = " " }
81+
69[[language]]
82[[language]]
70name = "json"
83name = "json"
71scope = "source.json"
84scope = "source.json"
@@ -202,6 +215,7 @@ name = "javascript"
202scope = "source.js"
215scope = "source.js"
203injection-regex = "^(js|javascript)$"
216injection-regex = "^(js|javascript)$"
204file-types = ["js", "mjs"]
217file-types = ["js", "mjs"]
218+shebangs = ["node"]
205roots = []
219roots = []
206comment-token = "//"
220comment-token = "//"
207# TODO: highlights-jsx, highlights-params
221# TODO: highlights-jsx, highlights-params
@@ -225,6 +239,7 @@ name = "typescript"
225scope = "source.ts"
239scope = "source.ts"
226injection-regex = "^(ts|typescript)$"
240injection-regex = "^(ts|typescript)$"
227file-types = ["ts"]
241file-types = ["ts"]
242+shebangs = []
228roots = []
243roots = []
229# TODO: highlights-jsx, highlights-params
244# TODO: highlights-jsx, highlights-params
230
245
@@ -265,6 +280,7 @@ name = "python"
265scope = "source.python"
280scope = "source.python"
266injection-regex = "python"
281injection-regex = "python"
267file-types = ["py"]
282file-types = ["py"]
283+shebangs = ["python"]
268roots = []
284roots = []
269comment-token = "#"
285comment-token = "#"
270
286
@@ -277,6 +293,7 @@ name = "nix"
277scope = "source.nix"
293scope = "source.nix"
278injection-regex = "nix"
294injection-regex = "nix"
279file-types = ["nix"]
295file-types = ["nix"]
296+shebangs = []
280roots = []
297roots = []
281comment-token = "#"
298comment-token = "#"
282
299
@@ -288,6 +305,7 @@ name = "ruby"
288scope = "source.ruby"
305scope = "source.ruby"
289injection-regex = "ruby"
306injection-regex = "ruby"
290file-types = ["rb"]
307file-types = ["rb"]
308+shebangs = ["ruby"]
291roots = []
309roots = []
292comment-token = "#"
310comment-token = "#"
293
311
@@ -299,6 +317,7 @@ name = "bash"
299scope = "source.bash"
317scope = "source.bash"
300injection-regex = "bash"
318injection-regex = "bash"
301file-types = ["sh", "bash"]
319file-types = ["sh", "bash"]
320+shebangs = ["sh", "bash", "dash"]
302roots = []
321roots = []
303comment-token = "#"
322comment-token = "#"
304
323
@@ -310,6 +329,7 @@ name = "php"
310scope = "source.php"
329scope = "source.php"
311injection-regex = "php"
330injection-regex = "php"
312file-types = ["php"]
331file-types = ["php"]
332+shebangs = ["php"]
313roots = []
333roots = []
314
334
315indent = { tab-width = 4, unit = " " }
335indent = { tab-width = 4, unit = " " }
@@ -371,6 +391,7 @@ name = "ocaml"
371scope = "source.ocaml"
391scope = "source.ocaml"
372injection-regex = "ocaml"
392injection-regex = "ocaml"
373file-types = ["ml"]
393file-types = ["ml"]
394+shebangs = []
374roots = []
395roots = []
375comment-token = "(**)"
396comment-token = "(**)"
376indent = { tab-width = 2, unit = " " }
397indent = { tab-width = 2, unit = " " }
@@ -379,6 +400,7 @@ indent = { tab-width = 2, unit = " " }
379name = "ocaml-interface"
400name = "ocaml-interface"
380scope = "source.ocaml.interface"
401scope = "source.ocaml.interface"
381file-types = ["mli"]
402file-types = ["mli"]
403+shebangs = []
382roots = []
404roots = []
383comment-token = "(**)"
405comment-token = "(**)"
384indent = { tab-width = 2, unit = " "}
406indent = { tab-width = 2, unit = " "}
@@ -387,6 +409,7 @@ indent = { tab-width = 2, unit = " "}
387name = "lua"
409name = "lua"
388scope = "source.lua"
410scope = "source.lua"
389file-types = ["lua"]
411file-types = ["lua"]
412+shebangs = ["lua"]
390roots = []
413roots = []
391comment-token = "--"
414comment-token = "--"
392indent = { tab-width = 2, unit = " " }
415indent = { tab-width = 2, unit = " " }
@@ -444,6 +467,7 @@ name = "prolog"
444scope = "source.prolog"
467scope = "source.prolog"
445roots = []
468roots = []
446file-types = ["pl", "prolog"]
469file-types = ["pl", "prolog"]
470+shebangs = ["swipl"]
447comment-token = "%"
471comment-token = "%"
448
472
449language-server = { command = "swipl", args = [
473language-server = { command = "swipl", args = [
@@ -467,3 +491,20 @@ roots = []
467comment-token = "#"
491comment-token = "#"
468indent = { tab-width = 2, unit = " " }
492indent = { tab-width = 2, unit = " " }
469language-server = { command = "cmake-language-server" }
493language-server = { command = "cmake-language-server" }
494+
495+[[language]]
496+name = "glsl"
497+scope = "source.glsl"
498+file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
499+roots = []
500+comment-token = "//"
501+indent = { tab-width = 4, unit = " " }
502+
503+[[language]]
504+name = "perl"
505+scope = "source.perl"
506+file-types = ["pl", "pm"]
507+shebangs = ["perl"]
508+roots = []
509+comment-token = "#"
510+indent = { tab-width = 2, unit = " " }
runtime/queries/glsl/folds.scmnew file+1
@@ -0,0 +1 @@
1+; inherits: c
runtime/queries/glsl/highlights.scmnew file+37
@@ -0,0 +1,37 @@
1+; inherits: c
2+
3+[
4+ "in"
5+ "out"
6+ "inout"
7+ "uniform"
8+ "shared"
9+ "layout"
10+ "attribute"
11+ "varying"
12+ "buffer"
13+ "coherent"
14+ "readonly"
15+ "writeonly"
16+ "precision"
17+ "highp"
18+ "mediump"
19+ "lowp"
20+ "centroid"
21+ "sample"
22+ "patch"
23+ "smooth"
24+ "flat"
25+ "noperspective"
26+ "invariant"
27+ "precise"
28+] @keyword
29+
30+"subroutine" @keyword.function
31+
32+(extension_storage_class) @attribute
33+
34+(
35+ (identifier) @variable.builtin
36+ (#match? @variable.builtin "^gl_")
37+)
runtime/queries/glsl/indents.tomlnew file+19
@@ -0,0 +1,19 @@
1+indent = [
2+ "init_declarator",
3+ "compound_statement",
4+ "preproc_arg",
5+ "field_declaration_list",
6+ "case_statement",
7+ "conditional_expression",
8+ "enumerator_list",
9+ "struct_specifier",
10+ "compound_literal_expression"
11+]
12+
13+outdent = [
14+ "#define",
15+ "#ifdef",
16+ "#endif",
17+ "{",
18+ "}"
19+]
runtime/queries/glsl/injections.scmnew file+3
@@ -0,0 +1,3 @@
1+(preproc_arg) @glsl
2+
3+(comment) @comment
runtime/queries/glsl/locals.scmnew file+1
@@ -0,0 +1 @@
1+; inherits: c
runtime/queries/json/indents.tomlnew file+9
@@ -0,0 +1,9 @@
1+indent = [
2+ "object",
3+ "array"
4+]
5+
6+outdent = [
7+ "]",
8+ "}"
9+]
runtime/queries/perl/highlights.scmnew file+181
@@ -0,0 +1,181 @@
1+; Variables
2+(variable_declaration
3+ .
4+ (scope) @keyword)
5+[
6+(single_var_declaration)
7+(scalar_variable)
8+(array_variable)
9+(hash_variable)
10+(hash_variable)
11+] @variable
12+
13+
14+[
15+(package_name)
16+(special_scalar_variable)
17+(special_array_variable)
18+(special_hash_variable)
19+(special_literal)
20+(super)
21+] @constant
22+
23+(
24+ [
25+ (package_name)
26+ (super)
27+ ]
28+ .
29+ ("::" @operator)
30+)
31+
32+(comments) @comment
33+(pod_statement) @comment.block.documentation
34+
35+[
36+(use_no_statement)
37+(use_no_feature_statement)
38+(use_no_if_statement)
39+(use_no_version)
40+(use_constant_statement)
41+(use_parent_statement)
42+] @keyword
43+
44+(use_constant_statement
45+ constant: (identifier) @constant)
46+
47+[
48+"require"
49+] @keyword
50+
51+(method_invocation
52+ .
53+ (identifier) @variable)
54+
55+(method_invocation
56+ (arrow_operator)
57+ .
58+ (identifier) @function)
59+(method_invocation
60+ function_name: (identifier) @function)
61+(named_block_statement
62+ function_name: (identifier) @function)
63+
64+(call_expression
65+ function_name: (identifier) @function)
66+(function_definition
67+ name: (identifier) @function)
68+[
69+(function)
70+(map)
71+(grep)
72+(bless)
73+] @function
74+
75+[
76+"return"
77+"sub"
78+"package"
79+"BEGIN"
80+"END"
81+] @keyword.function
82+
83+[
84+"("
85+")"
86+"["
87+"]"
88+"{"
89+"}"
90+] @punctuation.bracket
91+(standard_input_to_variable) @punctuation.bracket
92+
93+[
94+"=~"
95+"or"
96+"="
97+"=="
98+"+"
99+"-"
100+"."
101+"//"
102+"||"
103+(arrow_operator)
104+(hash_arrow_operator)
105+(array_dereference)
106+(hash_dereference)
107+(to_reference)
108+(type_glob)
109+(hash_access_variable)
110+(ternary_expression)
111+(ternary_expression_in_hash)
112+] @operator
113+
114+[
115+(regex_option)
116+(regex_option_for_substitution)
117+(regex_option_for_transliteration)
118+] @variable.parameter
119+
120+(type_glob
121+ (identifier) @variable)
122+(
123+ (scalar_variable)
124+ .
125+ ("->" @operator))
126+
127+[
128+(word_list_qw)
129+(command_qx_quoted)
130+(string_single_quoted)
131+(string_double_quoted)
132+(string_qq_quoted)
133+(bareword)
134+(transliteration_tr_or_y)
135+] @string
136+
137+[
138+(regex_pattern_qr)
139+(patter_matcher_m)
140+(substitution_pattern_s)
141+] @string.regexp
142+
143+(escape_sequence) @string.special
144+
145+[
146+","
147+(semi_colon)
148+(start_delimiter)
149+(end_delimiter)
150+(ellipsis_statement)
151+] @punctuation.delimiter
152+
153+[
154+(integer)
155+(floating_point)
156+(scientific_notation)
157+(hexadecimal)
158+] @constant.numeric
159+
160+[
161+; (if_statement)
162+(unless_statement)
163+(if_simple_statement)
164+(unless_simple_statement)
165+] @keyword.control.conditional
166+
167+[
168+"if"
169+"elsif"
170+"else"
171+] @keyword.control.conditional
172+
173+(foreach_statement) @keyword.control.repeat
174+(foreach_statement
175+ .
176+ (scope) @keyword)
177+
178+(function_attribute) @label
179+
180+(function_signature) @type
181+
runtime/queries/perl/textobjects.scmnew file+8
@@ -0,0 +1,8 @@
1+(function_definition
2+ (identifier) (_) @function.inside) @function.around
3+
4+(anonymous_function
5+ (_) @function.inside) @function.around
6+
7+(argument
8+ (_) @parameter.inside)
runtime/themes/nord.toml+3−3
@@ -10,11 +10,11 @@
10# Polar Night
10# Polar Night
11# nord0 - background color
11# nord0 - background color
12"ui.background" = { bg = "nord0" }
12"ui.background" = { bg = "nord0" }
13−"ui.statusline.inactive" = { fg = "nord4", bg = "nord0" }
13+"ui.statusline.inactive" = { fg = "nord8", bg = "nord1" }
14
14
15
15
16# nord1 - status bars, panels, modals, autocompletion
16# nord1 - status bars, panels, modals, autocompletion
17−"ui.statusline" = { fg = "nord8", bg = "nord1" }
17+"ui.statusline" = { fg = "nord4", bg = "#4c566a" }
18"ui.popup" = { bg = "#232d38" }
18"ui.popup" = { bg = "#232d38" }
19"ui.window" = { bg = "#232d38" }
19"ui.window" = { bg = "#232d38" }
20"ui.help" = { bg = "#232d38", fg = "nord4" }
20"ui.help" = { bg = "#232d38", fg = "nord4" }
@@ -25,7 +25,7 @@
25
25
26# nord3 - comments, nord3 based lighter color
26# nord3 - comments, nord3 based lighter color
27# relative: https://github.com/arcticicestudio/nord/issues/94
27# relative: https://github.com/arcticicestudio/nord/issues/94
28−"comment" = "gray"
28+"comment" = { fg = "gray", modifiers = ["italic"] }
29"ui.linenr" = { fg = "gray" }
29"ui.linenr" = { fg = "gray" }
30
30
31# Snow Storm
31# Snow Storm
runtime/themes/solarized_dark.tomlnew file+97
@@ -0,0 +1,97 @@
1+"attribute" = { fg = "violet" }
2+"keyword" = { fg = "green" }
3+"keyword.directive" = { fg = "orange" }
4+"namespace" = { fg = "violet" }
5+"operator" = { fg = "green" }
6+"special" = { fg = "orange" }
7+"variable.builtin" = { fg = "cyan", modifiers = ["bold"] }
8+"variable.function" = { fg = "blue" }
9+"type" = { fg = "yellow" }
10+"type.builtin" = { fg = "yellow", modifiers = ["bold"] }
11+"constructor" = { fg = "blue" }
12+"function" = { fg = "blue" }
13+"function.macro" = { fg = "magenta" }
14+"function.builtin" = { fg = "blue", modifiers = ["bold"] }
15+"function.special" = { fg = "magenta" }
16+"comment" = { fg = "base01" }
17+"string" = { fg = "cyan" }
18+"constant" = { fg = "cyan" }
19+"constant.builtin" = { fg = "cyan", modifiers = ["bold"] }
20+"constant.character.escape" = { fg = "red", modifiers = ["bold"] }
21+"label" = { fg = "green" }
22+"module" = { fg = "violet" }
23+"tag" = { fg = "magenta" }
24+
25+# 背景
26+"ui.background" = { bg = "base03" }
27+
28+# 行号栏
29+"ui.linenr" = { fg = "base0", bg = "base02" }
30+# 当前行号栏
31+"ui.linenr.selected" = { fg = "red", modifiers = ["bold"] }
32+
33+# 状态栏
34+"ui.statusline" = { fg = "base02", bg = "base1" }
35+# 非活动状态栏
36+"ui.statusline.inactive" = { fg = "base02", bg = "base00" }
37+
38+# 补全窗口, preview窗口
39+"ui.popup" = { bg = "base1" }
40+# 影响 补全选中 cmd弹出信息选中
41+"ui.menu.selected" = { fg = "base02", bg = "violet"}
42+"ui.menu" = { fg = "base02" }
43+# ??
44+"ui.window" = { fg = "base3" }
45+# 命令行 补全的帮助信息
46+"ui.help" = { modifiers = ["reversed"] }
47+
48+# 快捷键窗口
49+"ui.info" = { bg = "base1" }
50+# 快捷键字体
51+"ui.info.text" = {fg = "base02", modifiers = ["bold"]}
52+
53+# 普通ui的字体样式
54+"ui.text" = { fg = "base1" }
55+# 影响 picker列表选中, 快捷键帮助窗口文本
56+"ui.text.focus" = { fg = "blue", modifiers = ["bold"]}
57+# file picker中, 预览的当前选中项
58+"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
59+
60+# 主光标/selectio
61+"ui.cursor.primary" = {fg = "base03", bg = "base1"}
62+"ui.selection.primary" = { fg = "base03", bg = "base01" }
63+"ui.cursor.select" = {fg = "base02", bg = "green"}
64+"ui.selection" = { fg = "base02", bg = "yellow" }
65+
66+# normal模式的光标
67+"ui.cursor" = {fg = "base03", bg = "green"}
68+"ui.cursor.insert" = {fg = "base03", bg = "base3"}
69+# 当前光标匹配的标点符号
70+"ui.cursor.match" = {modifiers = ["reversed"]}
71+
72+"warning" = { fg = "orange", modifiers= ["bold", "underlined"] }
73+"error" = { fg = "red", modifiers= ["bold", "underlined"] }
74+"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
75+"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
76+"diagnostic" = { mdifiers = ["underlined"] }
77+
78+[palette]
79+# 深色 越来越深
80+base03 = "#002b36"
81+base02 = "#073642"
82+base01 = "#586e75"
83+base00 = "#657b83"
84+base0 = "#839496"
85+base1 = "#93a1a1"
86+base2 = "#eee8d5"
87+base3 = "#fdf6e3"
88+
89+# 浅色 越來越浅
90+yellow = "#b58900"
91+orange = "#cb4b16"
92+red = "#dc322f"
93+magenta = "#d33682"
94+violet = "#6c71c4"
95+blue = "#268bd2"
96+cyan = "#2aa198"
97+green = "#859900"
runtime/themes/solarized_light.tomlnew file+98
@@ -0,0 +1,98 @@
1+"attribute" = { fg = "violet" }
2+"keyword" = { fg = "green" }
3+"keyword.directive" = { fg = "orange" }
4+"namespace" = { fg = "violet" }
5+"operator" = { fg = "green" }
6+"special" = { fg = "orange" }
7+"variable.builtin" = { fg = "cyan", modifiers = ["bold"] }
8+"variable.function" = { fg = "blue" }
9+"type" = { fg = "yellow" }
10+"type.builtin" = { fg = "yellow", modifiers = ["bold"] }
11+"constructor" = { fg = "blue" }
12+"function" = { fg = "blue" }
13+"function.macro" = { fg = "magenta" }
14+"function.builtin" = { fg = "blue", modifiers = ["bold"] }
15+"function.special" = { fg = "magenta" }
16+"comment" = { fg = "base01" }
17+"string" = { fg = "cyan" }
18+"constant" = { fg = "cyan" }
19+"constant.builtin" = { fg = "cyan", modifiers = ["bold"] }
20+"constant.character.escape" = { fg = "red", modifiers = ["bold"] }
21+"label" = { fg = "green" }
22+"module" = { fg = "violet" }
23+"tag" = { fg = "magenta" }
24+
25+# 背景
26+"ui.background" = { bg = "base03" }
27+
28+# 行号栏
29+"ui.linenr" = { fg = "base0", bg = "base02" }
30+# 当前行号栏
31+"ui.linenr.selected" = { fg = "red", modifiers = ["bold"] }
32+
33+# 状态栏
34+"ui.statusline" = { fg = "base02", bg = "base1" }
35+# 非活动状态栏
36+"ui.statusline.inactive" = { fg = "base02", bg = "base00" }
37+
38+# 补全窗口, preview窗口
39+"ui.popup" = { bg = "base1" }
40+# 影响 补全选中 cmd弹出信息选中
41+"ui.menu.selected" = { fg = "base02", bg = "violet"}
42+"ui.menu" = { fg = "base02" }
43+# ??
44+"ui.window" = { fg = "base3" }
45+# 命令行 补全的帮助信息
46+"ui.help" = { modifiers = ["reversed"] }
47+
48+# 快捷键窗口
49+"ui.info" = { bg = "base1" }
50+# 快捷键字体
51+"ui.info.text" = {fg = "base02", modifiers = ["bold"]}
52+
53+# 普通ui的字体样式
54+"ui.text" = { fg = "base1" }
55+# 影响 picker列表选中, 快捷键帮助窗口文本
56+"ui.text.focus" = { fg = "blue", modifiers = ["bold"]}
57+# file picker中, 预览的当前选中项
58+"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
59+
60+# 主光标/selectio
61+"ui.cursor.primary" = {fg = "base03", bg = "base1"}
62+"ui.selection.primary" = { fg = "base03", bg = "base01" }
63+"ui.cursor.select" = {fg = "base02", bg = "green"}
64+"ui.selection" = { fg = "base02", bg = "yellow" }
65+
66+# normal模式的光标
67+"ui.cursor" = {fg = "base03", bg = "green"}
68+"ui.cursor.insert" = {fg = "base03", bg = "base3"}
69+# 当前光标匹配的标点符号
70+"ui.cursor.match" = {modifiers = ["reversed"]}
71+
72+"warning" = { fg = "orange", modifiers= ["bold", "underlined"] }
73+"error" = { fg = "red", modifiers= ["bold", "underlined"] }
74+"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
75+"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
76+"diagnostic" = { mdifiers = ["underlined"] }
77+
78+[palette]
79+red = '#dc322f'
80+green = '#859900'
81+yellow = '#b58900'
82+blue = '#268bd2'
83+magenta = '#d33682'
84+cyan = '#2aa198'
85+orange = '#cb4b16'
86+violet = '#6c71c4'
87+
88+# 深色 越来越深
89+base0 = '#657b83'
90+base1 = '#586e75'
91+base2 = '#073642'
92+base3 = '#002b36'
93+
94+## 浅色 越來越浅
95+base00 = '#839496'
96+base01 = '#93a1a1'
97+base02 = '#eee8d5'
98+base03 = '#fdf6e3'
runtime/themes/spacebones_light.tomlnew file+65
@@ -0,0 +1,65 @@
1+# Author : Koen Van der Auwera <atog@hey.com>
2+# Based on SpaceBones Light https://github.com/chipotle/spacebones
3+# https://github.com/chipotle/spacebones/blob/main/SpaceBones%20Light.bbColorScheme
4+
5+"attribute" = "#b1951d"
6+"keyword" = { fg = "#3a81c3" }
7+"keyword.directive" = "#3a81c3"
8+"namespace" = "#b1951d"
9+"punctuation" = "#6c3163"
10+"punctuation.delimiter" = "#6c3163"
11+"operator" = "#ba2f59"
12+"special" = "#ba2f59"
13+"property" = "#7590db"
14+"variable.property" = "#7590db"
15+"variable" = "#715ab1"
16+"variable.builtin" = "#715ab1"
17+"variable.parameter" = "#7590db"
18+"type" = "#6c3163"
19+"type.builtin" = "#6c3163"
20+"constructor" = { fg = "#4e3163", modifiers = ["bold"] }
21+"function" = { fg = "#715ab1", modifiers = ["bold"] }
22+"function.macro" = "#b1951d"
23+"function.builtin" = "#b1951d"
24+"comment" = { fg = "#a49da5", modifiers = ["italic"] }
25+"constant" = { fg = "#6c3163" }
26+"constant.builtin" = { fg = "#6c3163", modifiers = ["bold"] }
27+"string" = "#2d9574"
28+"number" = "#6c3163"
29+"escape" = { fg = "fg2", modifiers = ["bold"] }
30+"label" = "#b1951d"
31+"module" = "#b1951d"
32+
33+"warning" = { fg = "#da8b55" }
34+"error" = { fg = "#e0211d" }
35+"info" = { fg = "#b1951d" }
36+"hint" = { fg = "#d1dcdf" }
37+
38+"ui.background" = { bg = "bg0" }
39+"ui.linenr" = { fg = "bg3" }
40+"ui.linenr.selected" = { fg = "#b1951d" }
41+"ui.statusline" = { fg = "fg1", bg = "bg2" }
42+"ui.statusline.inactive" = { fg = "fg4", bg = "bg1" }
43+"ui.popup" = { bg = "bg1" }
44+"ui.window" = { bg = "bg1" }
45+"ui.help" = { bg = "bg1", fg = "fg1" }
46+"ui.text" = { fg = "fg1" }
47+"ui.text.focus" = { fg = "fg1" }
48+"ui.selection" = { bg = "bg3", modifiers = ["reversed"] }
49+"ui.cursor.primary" = { modifiers = ["reversed"] }
50+"ui.cursor.match" = { modifiers = ["reversed"] }
51+"ui.menu" = { fg = "fg1", bg = "bg2" }
52+"ui.menu.selected" = { fg = "#655370", bg = "#d1dcdf", modifiers = ["bold"] }
53+
54+"diagnostic" = { modifiers = ["underlined"] }
55+
56+[palette]
57+bg0 = "#fbf8ef"
58+bg1 = "#efeae9"
59+bg2 = "#d1dcdf"
60+bg3 = "#b4c6cb"
61+
62+fg1 = "#655370"
63+fg2 = "#5f3bc4"
64+fg3 = "#bdae93"
65+fg4 = "#a89984"
theme.toml+2−2
@@ -23,8 +23,8 @@ comment = "sirocco"
23constant = "white"
23constant = "white"
24"constant.builtin" = "white"
24"constant.builtin" = "white"
25string = "silver"
25string = "silver"
26−number = "chamois"
26+"constant.numeric" = "chamois"
27−escape = "honey"
27+"constant.character.escape" = "honey"
28# used for lifetimes
28# used for lifetimes
29label = "honey"
29label = "honey"
30
30