on master help sign in
Merge remote-tracking branch 'origin/master' into debug
commitd1854d8e6af07cd78ab6c24c859a4471afb3514e authorBlaž Hrastnik <blaz@mxxn.io> dateSun 21 Nov 2021, 20:06 (5 years ago) parent8b85903b95c947
56 files changed+2625−599
.gitmodules+8 Cargo.lock+12−12 Cargo.toml+3 book/src/SUMMARY.md+2 book/src/guides/adding_languages.md+60 book/src/guides/textobject.md+1−1 book/src/keymap.md+101−61 book/src/languages.md+14 book/src/remapping.md+2 book/src/usage.md+1 helix-core/src/history.rs+28−2 helix-core/src/indent.rs+1 helix-core/src/lib.rs+1 helix-core/src/match_brackets.rs+8−1 helix-core/src/movement.rs+1−1 helix-core/src/numbers.rs+499 helix-core/src/path.rs+2−2 helix-core/src/selection.rs+4−3 helix-core/src/syntax.rs+22−1 helix-core/src/textobject.rs+6−5 helix-lsp/src/client.rs+27 helix-syntax/languages/tree-sitter-glsl+1 helix-syntax/languages/tree-sitter-perl+1 helix-term/build.rs+12 helix-term/src/application.rs+37−27 helix-term/src/commands.rs+697−132 helix-term/src/compositor.rs+3−2 helix-term/src/config.rs+2−1 helix-term/src/keymap.rs+152−19 helix-term/src/main.rs+22−12 helix-term/src/ui/editor.rs+25−16 helix-term/src/ui/markdown.rs+10−4 helix-term/src/ui/menu.rs+11−46 helix-term/src/ui/mod.rs+2 helix-term/src/ui/picker.rs+17−56 helix-term/src/ui/popup.rs+9−19 helix-term/src/ui/prompt.rs+54−104 helix-view/src/document.rs+26−6 helix-view/src/editor.rs+116−33 helix-view/src/macros.rs+20−20 helix-view/src/tree.rs+3 helix-view/src/view.rs+37−8 languages.toml+41 runtime/queries/glsl/folds.scm+1 runtime/queries/glsl/highlights.scm+37 runtime/queries/glsl/indents.toml+19 runtime/queries/glsl/injections.scm+3 runtime/queries/glsl/locals.scm+1 runtime/queries/json/indents.toml+9 runtime/queries/perl/highlights.scm+181 runtime/queries/perl/textobjects.scm+8 runtime/themes/nord.toml+3−3 runtime/themes/solarized_dark.toml+97 runtime/themes/solarized_light.toml+98 runtime/themes/spacebones_light.toml+65 theme.toml+2−2
.gitmodules+8
@@ -134,3 +134,11 @@
134134 path = helix-syntax/languages/tree-sitter-cmake
135135 url = https://github.com/uyha/tree-sitter-cmake
136136 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 = [
1313
1414[[package]]
1515name = "anyhow"
16−version = "1.0.44"
16+version = "1.0.46"
1717source = "registry+https://github.com/rust-lang/crates.io-index"
18−checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1"
18+checksum = "3aa828229c44c0293dd7d4d2300bdfc4d2883ffdba934c069a6b968957a81f70"
1919
2020[[package]]
2121name = "arc-swap"
22−version = "1.4.0"
22+version = "1.5.0"
2323source = "registry+https://github.com/rust-lang/crates.io-index"
24−checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63"
24+checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f"
2525
2626[[package]]
2727name = "autocfg"
@@ -66,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
6666
6767[[package]]
6868name = "cc"
69−version = "1.0.71"
69+version = "1.0.72"
7070source = "registry+https://github.com/rust-lang/crates.io-index"
71−checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd"
71+checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
7272
7373[[package]]
7474name = "cfg-if"
@@ -583,9 +583,9 @@ checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
583583
584584[[package]]
585585name = "libloading"
586−version = "0.7.1"
586+version = "0.7.2"
587587source = "registry+https://github.com/rust-lang/crates.io-index"
588−checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0"
588+checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52"
589589dependencies = [
590590 "cfg-if",
591591 "winapi",
@@ -914,9 +914,9 @@ dependencies = [
914914
915915[[package]]
916916name = "serde_json"
917−version = "1.0.68"
917+version = "1.0.70"
918918source = "registry+https://github.com/rust-lang/crates.io-index"
919−checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8"
919+checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
920920dependencies = [
921921 "itoa",
922922 "ryu",
@@ -1086,9 +1086,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
10861086
10871087[[package]]
10881088name = "tokio"
1089−version = "1.13.0"
1089+version = "1.13.1"
10901090source = "registry+https://github.com/rust-lang/crates.io-index"
1091−checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
1091+checksum = "52963f91310c08d91cb7bff5786dfc8b79642ab839e188187e92105dbfb9d2c8"
10921092dependencies = [
10931093 "autocfg",
10941094 "bytes",
Cargo.toml+3
@@ -15,3 +15,6 @@ members = [
1515
1616[profile.dev]
1717split-debuginfo = "unpacked"
18+
19+[profile.release]
20+lto = "thin"
book/src/SUMMARY.md+2
@@ -8,5 +8,7 @@
88 - [Keymap](./keymap.md)
99 - [Key Remapping](./remapping.md)
1010 - [Hooks](./hooks.md)
11+ - [Languages](./languages.md)
1112- [Guides](./guides/README.md)
13+ - [Adding Languages](./guides/adding_languages.md)
1214 - [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
55to work properly. Tree-sitter allows us to query the source code syntax tree
66and capture specific parts of it. The queries are written in a lisp dialect.
77More information on how to write queries can be found in the [official tree-sitter
8−documentation](tree-sitter-queries).
8+documentation][tree-sitter-queries].
99
1010Query files should be placed in `runtime/queries/{language}/textobjects.scm`
1111when contributing. Note that to test the query files locally you should put
book/src/keymap.md+101−61
@@ -1,5 +1,8 @@
11# Keymap
22
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+
36## Normal mode
47
58### Movement
@@ -58,15 +61,19 @@
5861| `.` | Repeat last change | N/A |
5962| `u` | Undo change | `undo` |
6063| `U` | Redo change | `redo` |
64+| `Alt-u` | Move backward in history | `earlier` |
65+| `Alt-U` | Move forward in history | `later` |
6166| `y` | Yank selection | `yank` |
6267| `p` | Paste after selection | `paste_after` |
6368| `P` | Paste before selection | `paste_before` |
6469| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
6570| `>` | Indent selection | `indent` |
6671| `<` | Unindent selection | `unindent` |
67−| `=` | Format selection | `format_selections` |
72+| `=` | Format selection (**LSP**) | `format_selections` |
6873| `d` | Delete selection | `delete_selection` |
6974| `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` |
7077
7178#### Shell
7279
@@ -85,6 +92,7 @@
8592| `s` | Select all regex matches inside selections | `select_regex` |
8693| `S` | Split selection into subselections on regex matches | `split_selection` |
8794| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
95+| `_` | Trim whitespace from the selection | `trim_selections` |
8896| `;` | Collapse selection onto a single cursor | `collapse_selection` |
8997| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
9098| `,` | Keep only the primary selection | `keep_primary_selection` |
@@ -98,9 +106,10 @@
98106| `%` | Select entire file | `select_all` |
99107| `x` | Select current line, if already selected, extend to next line | `extend_line` |
100108| `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` |
102110| `J` | Join lines inside selection | `join_selections` |
103111| `K` | Keep selections matching the regex | `keep_selections` |
112+| `Alt-K` | Remove selections matching the regex | `remove_selections` |
104113| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
105114| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
106115
@@ -127,25 +136,23 @@ key to return to normal mode after usage (useful when you're simply looking
127136over text and not actively editing it).
128137
129138
130−| Key | Description | Command |
131−| ----- | ----------- | ------- |
132−| `z` , `c` | Vertically center the line | `align_view_center` |
133−| `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` |
135−| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
136−| `j` | Scroll the view downwards | `scroll_down` |
137−| `k` | Scroll the view upwards | `scroll_up` |
138−| `f` | Move page down | `page_down` |
139−| `b` | Move page up | `page_up` |
140−| `d` | Move half page down | `half_page_down` |
141−| `u` | Move half page up | `half_page_up` |
139+| Key | Description | Command |
140+| ----- | ----------- | ------- |
141+| `z` , `c` | Vertically center the line | `align_view_center` |
142+| `t` | Align the line to the top of the screen | `align_view_top` |
143+| `b` | Align the line to the bottom of the screen | `align_view_bottom` |
144+| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
145+| `j` , `down` | Scroll the view downwards | `scroll_down` |
146+| `k` , `up` | Scroll the view upwards | `scroll_up` |
147+| `f` | Move page down | `page_down` |
148+| `b` | Move page up | `page_up` |
149+| `d` | Move half page down | `half_page_down` |
150+| `u` | Move half page up | `half_page_up` |
142151
143152#### Goto mode
144153
145154Jumps to various locations.
146155
147−> NOTE: Some of these features are only available with the LSP present.
148−
149156| Key | Description | Command |
150157| ----- | ----------- | ------- |
151158| `g` | Go to the start of the file | `goto_file_start` |
@@ -156,13 +163,14 @@ Jumps to various locations.
156163| `t` | Go to the top of the screen | `goto_window_top` |
157164| `m` | Go to the middle of the screen | `goto_window_middle` |
158165| `b` | Go to the bottom of the screen | `goto_window_bottom` |
159−| `d` | Go to definition | `goto_definition` |
160−| `y` | Go to type definition | `goto_type_definition` |
161−| `r` | Go to references | `goto_reference` |
162−| `i` | Go to implementation | `goto_implementation` |
166+| `d` | Go to definition (**LSP**) | `goto_definition` |
167+| `y` | Go to type definition (**LSP**) | `goto_type_definition` |
168+| `r` | Go to references (**LSP**) | `goto_reference` |
169+| `i` | Go to implementation (**LSP**) | `goto_implementation` |
163170| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
164171| `n` | Go to next buffer | `goto_next_buffer` |
165172| `p` | Go to previous buffer | `goto_previous_buffer` |
173+| `.` | Go to last modification in current file | `goto_last_modification` |
166174
167175#### Match mode
168176
@@ -172,7 +180,7 @@ and [textobject](./usage.md#textobject) usage.
172180
173181| Key | Description | Command |
174182| ----- | ----------- | ------- |
175−| `m` | Goto matching bracket | `match_brackets` |
183+| `m` | Goto matching bracket (**TS**) | `match_brackets` |
176184| `s` `<char>` | Surround current selection with `<char>` | `surround_add` |
177185| `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` |
178186| `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.
195203| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
196204| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
197205| `q`, `Ctrl-q` | Close current window | `wclose` |
206+| `o`, `Ctrl-o` | Only keep the current window, closing all the others | `wonly` |
198207
199208#### Space mode
200209
201210This layer is a kludge of mappings, mostly pickers.
202211
203−| Key | Description | Command |
204−| ----- | ----------- | ------- |
205−| `k` | Show documentation for the item under the cursor | `hover` |
206−| `f` | Open file picker | `file_picker` |
207−| `b` | Open buffer picker | `buffer_picker` |
208−| `s` | Open symbol picker (current document) | `symbol_picker` |
209−| `a` | Apply code action | `code_action` |
210−| `'` | Open last fuzzy picker | `last_picker` |
211−| `w` | Enter [window mode](#window-mode) | N/A |
212−| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
213−| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
214−| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
215−| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
216−| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
217−| `/` | Global search in workspace folder | `global_search` |
218−
219−> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
212+
213+| Key | Description | Command |
214+| ----- | ----------- | ------- |
215+| `f` | Open file picker | `file_picker` |
216+| `b` | Open buffer picker | `buffer_picker` |
217+| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
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` |
221+| `a` | Apply code action (**LSP**) | `code_action` |
222+| `'` | Open last fuzzy picker | `last_picker` |
223+| `w` | Enter [window mode](#window-mode) | N/A |
224+| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
225+| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
226+| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
227+| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
228+| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
229+| `/` | Global search in workspace folder | `global_search` |
230+
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 |
220241
221242#### Unimpaired
222243
223244Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
224245
225−| Key | Description | Command |
226−| ----- | ----------- | ------- |
227−| `[d` | Go to previous diagnostic | `goto_prev_diag` |
228−| `]d` | Go to next diagnostic | `goto_next_diag` |
229−| `[D` | Go to first diagnostic in document | `goto_first_diag` |
230−| `]D` | Go to last diagnostic in document | `goto_last_diag` |
231−| `[space` | Add newline above | `add_newline_above` |
232−| `]space` | Add newline below | `add_newline_below` |
246+| Key | Description | Command |
247+| ----- | ----------- | ------- |
248+| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
249+| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
250+| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
251+| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
252+| `[space` | Add newline above | `add_newline_above` |
253+| `]space` | Add newline below | `add_newline_below` |
233254
234255## Insert Mode
235256
236−| Key | Description | Command |
237−| ----- | ----------- | ------- |
238−| `Escape` | Switch to normal mode | `normal_mode` |
239−| `Ctrl-x` | Autocomplete | `completion` |
240−| `Ctrl-w` | Delete previous word | `delete_word_backward` |
257+| Key | Description | Command |
258+| ----- | ----------- | ------- |
259+| `Escape` | Switch to normal mode | `normal_mode` |
260+| `Ctrl-x` | Autocomplete | `completion` |
261+| `Ctrl-r` | Insert a register content | `insert_register` |
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` |
241276
242277## Select / extend mode
243278
@@ -262,7 +297,9 @@ Keys to use within picker. Remapping currently not supported.
262297| `Escape`, `Ctrl-c` | Close picker |
263298
264299# Prompt
300+
265301Keys to use within prompt, Remapping currently not supported.
302+
266303| Key | Description |
267304| ----- | ------------- |
268305| `Escape`, `Ctrl-c` | Close prompt |
@@ -270,15 +307,18 @@ Keys to use within prompt, Remapping currently not supported.
270307| `Ctrl-b`, `Left` | Backward a char |
271308| `Alt-f`, `Alt-Right` | Forward a word |
272309| `Ctrl-f`, `Right` | Forward a char |
273−| `Ctrl-e`, `End` | move prompt end |
274−| `Ctrl-a`, `Home` | move prompt start |
275−| `Ctrl-w` | delete previous word |
276−| `Ctrl-k` | delete to end of line |
277−| `backspace` | delete previous char |
278−| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
279−| `Ctrl-p`, `Up` | select previous history |
280−| `Ctrl-n`, `Down` | select next history |
281−| `Tab` | slect next completion item |
282−| `BackTab` | slect previous completion item |
310+| `Ctrl-e`, `End` | Move prompt end |
311+| `Ctrl-a`, `Home` | Move prompt start |
312+| `Ctrl-w` | Delete previous word |
313+| `Alt-d` | Delete next word |
314+| `Ctrl-u` | Delete to start of line |
315+| `Ctrl-k` | Delete to end of line |
316+| `backspace`, `Ctrl-h` | Delete previous char |
317+| `delete`, `Ctrl-d` | Delete previous char |
318+| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
319+| `Ctrl-p`, `Up` | Select previous history |
320+| `Ctrl-n`, `Down` | Select next history |
321+| `Tab` | Select next completion item |
322+| `BackTab` | Select previous completion item |
283323| `Enter` | Open selected |
284324
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
1515w = "move_line_up" # Maps the 'w' key move_line_up
1616"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
1717g = { 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
1819
1920[keys.insert]
2021"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
3839| Left | `"left"` |
3940| Right | `"right"` |
4041| Up | `"up"` |
42+| Down | `"down"` |
4143| Home | `"home"` |
4244| End | `"end"` |
4345| Page | `"pageup"` |
book/src/usage.md+1
@@ -62,6 +62,7 @@ Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
6262| Key after `mi` or `ma` | Textobject selected |
6363| --- | --- |
6464| `w` | Word |
65+| `W` | WORD |
6566| `(`, `[`, `'`, etc | Specified surround pairs |
6667| `f` | Function |
6768| `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};
22use once_cell::sync::Lazy;
33use regex::Regex;
44use std::num::NonZeroUsize;
@@ -133,6 +133,32 @@ impl History {
133133 Some(&self.revisions[last_child.get()].transaction)
134134 }
135135
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+
136162 fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
137163 use std::collections::HashSet;
138164 let mut a_path_set = HashSet::new();
@@ -256,7 +282,7 @@ impl History {
256282}
257283
258284/// Whether to undo by a number of edits or a duration of time.
259−#[derive(Debug, PartialEq)]
285+#[derive(Debug, PartialEq, Clone, Copy)]
260286pub enum UndoKind {
261287 Steps(usize),
262288 TimePeriod(std::time::Duration),
helix-core/src/indent.rs+1
@@ -450,6 +450,7 @@ where
450450 language: vec![LanguageConfiguration {
451451 scope: "source.rust".to_string(),
452452 file_types: vec!["rs".to_string()],
453+ shebangs: vec![],
453454 language_id: "Rust".to_string(),
454455 highlight_config: OnceCell::new(),
455456 config: None,
helix-core/src/lib.rs+1
@@ -10,6 +10,7 @@ pub mod line_ending;
1010pub mod macros;
1111pub mod match_brackets;
1212pub mod movement;
13+pub mod numbers;
1314pub mod object;
1415pub mod path;
1516mod position;
helix-core/src/match_brackets.rs+8−1
@@ -1,6 +1,13 @@
11use crate::{Rope, Syntax};
22
3−const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')];
3+const PAIRS: &[(char, char)] = &[
4+ ('(', ')'),
5+ ('{', '}'),
6+ ('[', ']'),
7+ ('<', '>'),
8+ ('\'', '\''),
9+ ('"', '"'),
10+];
411// limit matching pairs to only ( ) { } [ ] < >
512
613#[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
168168where
169169 F: Fn(char) -> bool,
170170{
171− let mut chars_starting_from_next = slice.chars_at(pos + 1);
171+ let mut chars_starting_from_next = slice.chars_at(pos);
172172 let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
173173 backwards.find_map(|(i, c)| {
174174 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 {
4040/// needs to improve on.
4141/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
4242pub fn get_normalized_path(path: &Path) -> PathBuf {
43− let path = expand_tilde(path);
4443 let mut components = path.components().peekable();
4544 let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
4645 components.next();
@@ -72,10 +71,11 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
7271/// This function is used instead of `std::fs::canonicalize` because we don't want to verify
7372/// here if the path exists, just normalize it's components.
7473pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
74+ let path = expand_tilde(path);
7575 let path = if path.is_relative() {
7676 std::env::current_dir().map(|current_dir| current_dir.join(path))?
7777 } else {
78− path.to_path_buf()
78+ path
7979 };
8080
8181 Ok(get_normalized_path(path.as_path()))
helix-core/src/selection.rs+4−3
@@ -360,7 +360,7 @@ impl Selection {
360360 self.normalize()
361361 }
362362
363− /// Adds a new range to the selection and makes it the primary range.
363+ /// Removes a range from the selection.
364364 pub fn remove(mut self, index: usize) -> Self {
365365 assert!(
366366 self.ranges.len() > 1,
@@ -528,14 +528,15 @@ impl<'a> IntoIterator for &'a Selection {
528528
529529// TODO: checkSelection -> check if valid for doc length && sorted
530530
531−pub fn keep_matches(
531+pub fn keep_or_remove_matches(
532532 text: RopeSlice,
533533 selection: &Selection,
534534 regex: &crate::regex::Regex,
535+ remove: bool,
535536) -> Option<Selection> {
536537 let result: SmallVec<_> = selection
537538 .iter()
538− .filter(|range| regex.is_match(&range.fragment(text)))
539+ .filter(|range| regex.is_match(&range.fragment(text)) ^ remove)
539540 .copied()
540541 .collect();
541542
helix-core/src/syntax.rs+22−1
@@ -40,18 +40,21 @@ where
4040}
4141
4242#[derive(Debug, Serialize, Deserialize)]
43+#[serde(deny_unknown_fields)]
4344pub struct Configuration {
4445 pub language: Vec<LanguageConfiguration>,
4546}
4647
4748// largely based on tree-sitter/cli/src/loader.rs
4849#[derive(Debug, Serialize, Deserialize)]
49−#[serde(rename_all = "kebab-case")]
50+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
5051pub struct LanguageConfiguration {
5152 #[serde(rename = "name")]
5253 pub language_id: String,
5354 pub scope: String, // source.rust
5455 pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
56+ #[serde(default)]
57+ pub shebangs: Vec<String>, // interpreter(s) associated with language
5558 pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
5659 pub comment_token: Option<String>,
5760
@@ -309,6 +312,7 @@ pub struct Loader {
309312 // highlight_names ?
310313 language_configs: Vec<Arc<LanguageConfiguration>>,
311314 language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
315+ language_config_ids_by_shebang: HashMap<String, usize>,
312316}
313317
314318impl Loader {
@@ -316,6 +320,7 @@ impl Loader {
316320 let mut loader = Self {
317321 language_configs: Vec::new(),
318322 language_config_ids_by_file_type: HashMap::new(),
323+ language_config_ids_by_shebang: HashMap::new(),
319324 };
320325
321326 for config in config.language {
@@ -328,6 +333,11 @@ impl Loader {
328333 .language_config_ids_by_file_type
329334 .insert(file_type.clone(), language_id);
330335 }
336+ for shebang in &config.shebangs {
337+ loader
338+ .language_config_ids_by_shebang
339+ .insert(shebang.clone(), language_id);
340+ }
331341
332342 loader.language_configs.push(Arc::new(config));
333343 }
@@ -353,6 +363,17 @@ impl Loader {
353363 // TODO: content_regex handling conflict resolution
354364 }
355365
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+
356377 pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> {
357378 self.language_configs
358379 .iter()
helix-core/src/textobject.rs+6−5
@@ -10,7 +10,7 @@ use crate::surround;
1010use crate::syntax::LanguageConfiguration;
1111use crate::Range;
1212
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 {
1414 use CharCategory::{Eol, Whitespace};
1515
1616 let iter = match direction {
@@ -33,7 +33,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) ->
3333 match categorize_char(ch) {
3434 Eol | Whitespace => return pos,
3535 category => {
36− if category != prev_category && pos != 0 && pos != slice.len_chars() {
36+ if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
3737 return pos;
3838 } else {
3939 match direction {
@@ -70,13 +70,14 @@ pub fn textobject_word(
7070 range: Range,
7171 textobject: TextObject,
7272 _count: usize,
73+ long: bool,
7374) -> Range {
7475 let pos = range.cursor(slice);
7576
76− let word_start = find_word_boundary(slice, pos, Direction::Backward);
77+ let word_start = find_word_boundary(slice, pos, Direction::Backward, long);
7778 let word_end = match slice.get_char(pos).map(categorize_char) {
7879 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),
8081 };
8182
8283 // Special case.
@@ -268,7 +269,7 @@ mod test {
268269 let slice = doc.slice(..);
269270 for &case in scenario {
270271 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);
272273 assert_eq!(
273274 result,
274275 expected_range.into(),
helix-lsp/src/client.rs+27
@@ -257,6 +257,12 @@ impl Client {
257257 content_format: Some(vec![lsp::MarkupKind::Markdown]),
258258 ..Default::default()
259259 }),
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+ }),
260266 code_action: Some(lsp::CodeActionClientCapabilities {
261267 code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
262268 code_action_kind: lsp::CodeActionKindLiteralSupport {
@@ -773,4 +779,25 @@ impl Client {
773779
774780 self.call::<lsp::request::CodeActionRequest>(params)
775781 }
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+ }
776803}
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::{
99
1010use log::{error, warn};
1111use std::{
12− io::{stdout, Write},
12+ io::{stdin, stdout, Write},
1313 sync::Arc,
1414 time::{Duration, Instant},
1515};
@@ -19,6 +19,7 @@ use anyhow::Error;
1919use crossterm::{
2020 event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
2121 execute, terminal,
22+ tty::IsTty,
2223};
2324#[cfg(not(windows))]
2425use {
@@ -62,14 +63,19 @@ impl Application {
6263 std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
6364
6465 // load default and user config, and merge both
65− let def_lang_conf: toml::Value = toml::from_slice(include_bytes!("../../languages.toml"))
66− .expect("Could not parse built-in languages.toml, something must be very wrong");
67− let user_lang_conf: Option<toml::Value> = std::fs::read(conf_dir.join("languages.toml"))
66+ let builtin_err_msg =
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);
72+ let user_lang_conf = std::fs::read(conf_dir.join("languages.toml"))
6873 .ok()
69− .map(|raw| toml::from_slice(&raw).expect("Could not parse user languages.toml"));
74+ .map(|raw| toml::from_slice(&raw));
7075 let lang_conf = match user_lang_conf {
71− Some(value) => merge_toml_values(def_lang_conf, value),
72− None => def_lang_conf,
76+ Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
77+ Some(err @ Err(_)) => err,
78+ None => Ok(def_lang_conf),
7379 };
7480
7581 let theme = if let Some(theme) = &config.theme {
@@ -85,8 +91,15 @@ impl Application {
8591 };
8692
8793 let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
88− .try_into()
89− .expect("Could not parse merged (built-in + user) languages.toml");
94+ .and_then(|conf| conf.try_into())
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+ });
90103 let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
91104
92105 let mut editor = Editor::new(
@@ -124,8 +137,17 @@ impl Application {
124137 }
125138 editor.set_status(format!("Loaded {} files.", nr_of_files));
126139 }
127− } else {
140+ } else if stdin().is_tty() {
128141 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));
129151 }
130152
131153 editor.set_theme(theme);
@@ -253,12 +275,8 @@ impl Application {
253275 }
254276 let editor_view = self
255277 .compositor
256− .find(std::any::type_name::<ui::EditorView>())
278+ .find::<ui::EditorView>()
257279 .expect("expected at least one EditorView");
258− let editor_view = editor_view
259− .as_any_mut()
260− .downcast_mut::<ui::EditorView>()
261− .unwrap();
262280
263281 if editor_view.completion.is_some() {
264282 return;
@@ -583,12 +601,8 @@ impl Application {
583601 {
584602 let editor_view = self
585603 .compositor
586− .find(std::any::type_name::<ui::EditorView>())
604+ .find::<ui::EditorView>()
587605 .expect("expected at least one EditorView");
588− let editor_view = editor_view
589− .as_any_mut()
590− .downcast_mut::<ui::EditorView>()
591− .unwrap();
592606 let lsp::ProgressParams { token, value } = params;
593607
594608 let lsp::ProgressParamsValue::WorkDone(work) = value;
@@ -702,12 +716,8 @@ impl Application {
702716
703717 let editor_view = self
704718 .compositor
705− .find(std::any::type_name::<ui::EditorView>())
719+ .find::<ui::EditorView>()
706720 .expect("expected at least one EditorView");
707− let editor_view = editor_view
708− .as_any_mut()
709− .downcast_mut::<ui::EditorView>()
710− .unwrap();
711721 let spinner = editor_view.spinners_mut().get_or_create(server_id);
712722 if spinner.is_stopped() {
713723 spinner.start();
@@ -742,7 +752,7 @@ impl Application {
742752 Ok(())
743753 }
744754
745− pub async fn run(&mut self) -> Result<(), Error> {
755+ pub async fn run(&mut self) -> Result<i32, Error> {
746756 self.claim_term().await?;
747757
748758 // Exit the alternate screen and disable raw mode before panicking
@@ -765,6 +775,6 @@ impl Application {
765775
766776 self.restore_term()?;
767777
768− Ok(())
778+ Ok(self.editor.exit_code)
769779 }
770780}
helix-term/src/commands.rs+697−132
@@ -3,21 +3,25 @@ pub(crate) mod dap;
33pub use dap::*;
44
55use 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,
79 indent::IndentStyle,
810 line_ending::{get_line_ending_of_str, line_end_char_index, str_is_line_ending},
911 match_brackets,
1012 movement::{self, Direction},
13+ numbers::NumberIncrementor,
1114 object, pos_at_coords,
1215 regex::{self, Regex, RegexBuilder},
1316 register::Register,
14− search, selection, surround, textobject, LineEnding, Position, Range, Rope, RopeGraphemes,
15− RopeSlice, Selection, SmallVec, Tendril, Transaction,
17+ search, selection, surround, textobject,
18+ unicode::width::UnicodeWidthChar,
19+ LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
20+ Transaction,
1621};
17−
1822use helix_view::{
1923 clipboard::ClipboardType,
20− document::Mode,
24+ document::{Mode, SCRATCH_BUFFER_NAME},
2125 editor::{Action, Motion},
2226 input::KeyEvent,
2327 keyboard::KeyCode,
@@ -27,7 +31,7 @@ use helix_view::{
2731
2832use anyhow::{anyhow, bail, Context as _};
2933use helix_lsp::{
30− lsp,
34+ block_on, lsp,
3135 util::{lsp_pos_to_pos, lsp_range_to_range, pos_to_lsp_pos, range_to_lsp_range},
3236 OffsetEncoding,
3337};
@@ -189,6 +193,7 @@ impl Command {
189193 copy_selection_on_prev_line, "Copy selection on previous line",
190194 move_next_word_start, "Move to beginning of next word",
191195 move_prev_word_start, "Move to beginning of previous word",
196+ move_prev_word_end, "Move to end of previous word",
192197 move_next_word_end, "Move to end of next word",
193198 move_next_long_word_start, "Move to beginning of next long word",
194199 move_prev_long_word_start, "Move to beginning of previous long word",
@@ -241,6 +246,7 @@ impl Command {
241246 code_action, "Perform code action",
242247 buffer_picker, "Open buffer picker",
243248 symbol_picker, "Open symbol picker",
249+ workspace_symbol_picker, "Open workspace symbol picker",
244250 last_picker, "Open last picker",
245251 prepend_to_line, "Insert at start of line",
246252 append_to_line, "Insert at end of line",
@@ -261,6 +267,7 @@ impl Command {
261267 goto_window_middle, "Goto window middle",
262268 goto_window_bottom, "Goto window bottom",
263269 goto_last_accessed_file, "Goto last accessed file",
270+ goto_last_modification, "Goto last modification",
264271 goto_line, "Goto line",
265272 goto_last_line, "Goto last line",
266273 goto_first_diag, "Goto first diagnostic",
@@ -274,6 +281,7 @@ impl Command {
274281 // TODO: different description ?
275282 goto_line_end_newline, "Goto line end",
276283 goto_first_nonwhitespace, "Goto first non-blank in line",
284+ trim_selections, "Trim whitespace from selections",
277285 extend_to_line_start, "Extend to line start",
278286 extend_to_line_end, "Extend to line end",
279287 extend_to_line_end_newline, "Extend to line end",
@@ -283,8 +291,13 @@ impl Command {
283291 delete_char_backward, "Delete previous char",
284292 delete_char_forward, "Delete next char",
285293 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",
286297 undo, "Undo change",
287298 redo, "Redo change",
299+ earlier, "Move backward in history",
300+ later, "Move forward in history",
288301 yank, "Yank selection",
289302 yank_joined_to_clipboard, "Join and yank selections to clipboard",
290303 yank_main_selection_to_clipboard, "Yank main selection to clipboard",
@@ -304,6 +317,7 @@ impl Command {
304317 format_selections, "Format selection",
305318 join_selections, "Join lines inside selection",
306319 keep_selections, "Keep selections matching regex",
320+ remove_selections, "Remove selections matching regex",
307321 keep_primary_selection, "Keep primary selection",
308322 remove_primary_selection, "Remove primary selection",
309323 completion, "Invoke completion popup",
@@ -324,7 +338,9 @@ impl Command {
324338 hsplit, "Horizontal bottom split",
325339 vsplit, "Vertical right split",
326340 wclose, "Close window",
341+ wonly, "Current window only",
327342 select_register, "Select register",
343+ insert_register, "Insert register",
328344 align_view_middle, "Align view middle",
329345 align_view_top, "Align view top",
330346 align_view_center, "Align view center",
@@ -358,6 +374,9 @@ impl Command {
358374 shell_append_output, "Append output of shell command after each selection",
359375 shell_keep_pipe, "Filter selections with shell predicate",
360376 suspend, "Suspend",
377+ rename_symbol, "Rename symbol",
378+ increment, "Increment",
379+ decrement, "Decrement",
361380 );
362381}
363382
@@ -581,6 +600,29 @@ fn extend_to_line_start(cx: &mut Context) {
581600 goto_line_start_impl(view, doc, Movement::Extend)
582601}
583602
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+
584626fn goto_first_nonwhitespace(cx: &mut Context) {
585627 let (view, doc) = current!(cx.editor);
586628 let text = doc.text().slice(..);
@@ -598,6 +640,42 @@ fn goto_first_nonwhitespace(cx: &mut Context) {
598640 doc.set_selection(view.id, selection);
599641}
600642
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+
601679fn goto_window(cx: &mut Context, align: Align) {
602680 let (view, doc) = current!(cx.editor);
603681
@@ -657,6 +735,10 @@ fn move_prev_word_start(cx: &mut Context) {
657735 move_word_impl(cx, movement::move_prev_word_start)
658736}
659737
738+fn move_prev_word_end(cx: &mut Context) {
739+ move_word_impl(cx, movement::move_prev_word_end)
740+}
741+
660742fn move_next_word_end(cx: &mut Context) {
661743 move_word_impl(cx, movement::move_next_word_end)
662744}
@@ -1193,6 +1275,7 @@ fn search_impl(
11931275 regex: &Regex,
11941276 movement: Movement,
11951277 direction: Direction,
1278+ scrolloff: usize,
11961279) {
11971280 let text = doc.text().slice(..);
11981281 let selection = doc.selection(view.id);
@@ -1251,7 +1334,11 @@ fn search_impl(
12511334 };
12521335
12531336 doc.set_selection(view.id, selection);
1254− align_view(doc, view, Align::Center);
1337+ if view.is_cursor_in_view(doc, 0) {
1338+ view.ensure_cursor_in_view(doc, scrolloff);
1339+ } else {
1340+ align_view(doc, view, Align::Center)
1341+ }
12551342 };
12561343}
12571344
@@ -1275,6 +1362,8 @@ fn rsearch(cx: &mut Context) {
12751362// TODO: use one function for search vs extend
12761363fn searcher(cx: &mut Context, direction: Direction) {
12771364 let reg = cx.register.unwrap_or('/');
1365+ let scrolloff = cx.editor.config.scrolloff;
1366+
12781367 let (_, doc) = current!(cx.editor);
12791368
12801369 // TODO: could probably share with select_on_matches?
@@ -1299,7 +1388,15 @@ fn searcher(cx: &mut Context, direction: Direction) {
12991388 if event != PromptEvent::Update {
13001389 return;
13011390 }
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+ );
13031400 },
13041401 );
13051402
@@ -1307,6 +1404,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
13071404}
13081405
13091406fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) {
1407+ let scrolloff = cx.editor.config.scrolloff;
13101408 let (view, doc) = current!(cx.editor);
13111409 let registers = &cx.editor.registers;
13121410 if let Some(query) = registers.read('/') {
@@ -1321,7 +1419,7 @@ fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Dir
13211419 .case_insensitive(case_insensitive)
13221420 .build()
13231421 {
1324− search_impl(doc, view, &contents, &regex, movement, direction);
1422+ search_impl(doc, view, &contents, &regex, movement, direction, scrolloff);
13251423 } else {
13261424 // get around warning `mutable_borrow_reservation_conflict`
13271425 // which will be a hard error in the future
@@ -1365,7 +1463,7 @@ fn global_search(cx: &mut Context) {
13651463 let completions = search_completions(cx, None);
13661464 let prompt = ui::regex_prompt(
13671465 cx,
1368− "global search:".into(),
1466+ "global-search:".into(),
13691467 None,
13701468 move |input: &str| {
13711469 completions
@@ -1543,6 +1641,17 @@ fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId
15431641 doc.apply(&transaction, view_id);
15441642}
15451643
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+
15461655fn delete_selection(cx: &mut Context) {
15471656 let reg_name = cx.register.unwrap_or('"');
15481657 let (view, doc) = current!(cx.editor);
@@ -1657,8 +1766,7 @@ mod cmd {
16571766 buffers_remaining_impl(cx.editor)?
16581767 }
16591768
1660− cx.editor
1661− .close(view!(cx.editor).id, /* close_buffer */ false);
1769+ cx.editor.close(view!(cx.editor).id);
16621770
16631771 Ok(())
16641772 }
@@ -1668,8 +1776,7 @@ mod cmd {
16681776 _args: &[&str],
16691777 _event: PromptEvent,
16701778 ) -> anyhow::Result<()> {
1671− cx.editor
1672− .close(view!(cx.editor).id, /* close_buffer */ false);
1779+ cx.editor.close(view!(cx.editor).id);
16731780
16741781 Ok(())
16751782 }
@@ -1679,11 +1786,30 @@ mod cmd {
16791786 args: &[&str],
16801787 _event: PromptEvent,
16811788 ) -> anyhow::Result<()> {
1682− use helix_core::path::expand_tilde;
16831789 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)?;
16871813 Ok(())
16881814 }
16891815
@@ -1838,13 +1964,13 @@ mod cmd {
18381964 args: &[&str],
18391965 _event: PromptEvent,
18401966 ) -> 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))?;
18451968
18461969 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+ }
18481974
18491975 Ok(())
18501976 }
@@ -1854,12 +1980,12 @@ mod cmd {
18541980 args: &[&str],
18551981 _event: PromptEvent,
18561982 ) -> 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))?;
18611984 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+ }
18631989
18641990 Ok(())
18651991 }
@@ -1891,7 +2017,7 @@ mod cmd {
18912017 .map(|doc| {
18922018 doc.relative_path()
18932019 .map(|path| path.to_string_lossy().to_string())
1894− .unwrap_or_else(|| "[scratch]".into())
2020+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
18952021 })
18962022 .collect();
18972023 if !modified.is_empty() {
@@ -1933,7 +2059,7 @@ mod cmd {
19332059 // close all views
19342060 let views: Vec<_> = cx.editor.tree.views().map(|(view, _)| view.id).collect();
19352061 for view_id in views {
1936− cx.editor.close(view_id, false);
2062+ cx.editor.close(view_id);
19372063 }
19382064 }
19392065
@@ -1977,7 +2103,7 @@ mod cmd {
19772103 // close all views
19782104 let views: Vec<_> = editor.tree.views().map(|(view, _)| view.id).collect();
19792105 for view_id in views {
1980− editor.close(view_id, false);
2106+ editor.close(view_id);
19812107 }
19822108
19832109 Ok(())
@@ -1999,6 +2125,25 @@ mod cmd {
19992125 quit_all_impl(&mut cx.editor, args, event, true)
20002126 }
20012127
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+
20022147 fn theme(
20032148 cx: &mut compositor::Context,
20042149 args: &[&str],
@@ -2241,7 +2386,6 @@ mod cmd {
22412386 args: &[&str],
22422387 _event: PromptEvent,
22432388 ) -> anyhow::Result<()> {
2244− use helix_lsp::block_on;
22452389 if let Some(debugger) = cx.editor.debugger.as_mut() {
22462390 let (frame, thread_id) = match (debugger.active_frame, debugger.thread_id) {
22472391 (Some(frame), Some(thread_id)) => (frame, thread_id),
@@ -2344,6 +2488,20 @@ mod cmd {
23442488 fun: open,
23452489 completer: Some(completers::filename),
23462490 },
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+ },
23472505 TypableCommand {
23482506 name: "write",
23492507 aliases: &["w"],
@@ -2442,6 +2600,13 @@ mod cmd {
24422600 fun: force_quit_all,
24432601 completer: None,
24442602 },
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+ },
24452610 TypableCommand {
24462611 name: "theme",
24472612 aliases: &[],
@@ -2698,36 +2863,66 @@ fn file_picker(cx: &mut Context) {
26982863fn buffer_picker(cx: &mut Context) {
26992864 let current = view!(cx.editor).doc;
27002865
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+
27012908 let picker = FilePicker::new(
27022909 cx.editor
27032910 .documents
27042911 .iter()
2705− .map(|(id, doc)| (*id, doc.path().cloned()))
2912+ .map(|(_, doc)| new_meta(doc))
27062913 .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()
2713− } else {
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);
2914+ BufferMeta::format,
2915+ |cx, meta, _action| {
2916+ cx.editor.switch(meta.id, Action::Replace);
27222917 },
2723− |editor, (id, path)| {
2724− let doc = &editor.documents.get(id)?;
2918+ |editor, meta| {
2919+ let doc = &editor.documents.get(&meta.id)?;
27252920 let &view_id = doc.selections().keys().next()?;
27262921 let line = doc
27272922 .selection(view_id)
27282923 .primary()
27292924 .cursor_line(doc.text().slice(..));
2730− Some((path.clone()?, Some((line, line))))
2925+ Some((meta.path.clone()?, Some((line, line))))
27312926 },
27322927 );
27332928 cx.push_layer(Box::new(picker));
@@ -2782,7 +2977,7 @@ fn symbol_picker(cx: &mut Context) {
27822977 }
27832978 };
27842979
2785− let picker = FilePicker::new(
2980+ let mut picker = FilePicker::new(
27862981 symbols,
27872982 |symbol| (&symbol.name).into(),
27882983 move |cx, symbol, _action| {
@@ -2807,6 +3002,69 @@ fn symbol_picker(cx: &mut Context) {
28073002 Some((path, line))
28083003 },
28093004 );
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;
28103068 compositor.push(Box::new(picker))
28113069 }
28123070 },
@@ -2864,14 +3122,104 @@ pub fn code_action(cx: &mut Context) {
28643122 )
28653123}
28663124
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+
28673178fn apply_workspace_edit(
28683179 editor: &mut Editor,
28693180 offset_encoding: OffsetEncoding,
28703181 workspace_edit: &lsp::WorkspaceEdit,
28713182) {
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+
28723217 if let Some(ref changes) = workspace_edit.changes {
28733218 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+ }
28753223 return;
28763224 // 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
28773225 // TODO: find some example that uses workspace changes, and test it
@@ -2889,30 +3237,6 @@ fn apply_workspace_edit(
28893237 match document_changes {
28903238 lsp::DocumentChanges::Edits(document_edits) => {
28913239 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−
29163240 let edits = document_edit
29173241 .edits
29183242 .iter()
@@ -2924,19 +3248,33 @@ fn apply_workspace_edit(
29243248 })
29253249 .cloned()
29263250 .collect();
2927−
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);
3251+ apply_edits(&document_edit.text_document.uri, edits);
29353252 }
29363253 }
29373254 lsp::DocumentChanges::Operations(operations) => {
29383255 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+ }
29403278 }
29413279 }
29423280 }
@@ -3175,6 +3513,19 @@ fn goto_last_accessed_file(cx: &mut Context) {
31753513 }
31763514}
31773515
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+
31783529fn select_mode(cx: &mut Context) {
31793530 let (view, doc) = current!(cx.editor);
31803531 let text = doc.text().slice(..);
@@ -3773,19 +4124,70 @@ pub mod insert {
37734124 doc.apply(&transaction, view.id);
37744125 }
37754126
3776− // TODO: handle indent-aware delete
37774127 pub fn delete_char_backward(cx: &mut Context) {
37784128 let count = cx.count();
37794129 let (view, doc) = current!(cx.editor);
37804130 let text = doc.text().slice(..);
4131+ let indent_unit = doc.indent_unit();
4132+ let tab_size = doc.tab_width();
4133+
37814134 let transaction =
37824135 Transaction::change_by_selection(doc.text(), doc.selection(view.id), |range| {
37834136 let pos = range.cursor(text);
3784− (
3785− graphemes::nth_prev_grapheme_boundary(text, pos, count),
3786− pos,
3787− None,
3788− )
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+ (
4144+ graphemes::nth_prev_grapheme_boundary(text, pos, 1),
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,
4188+ None,
4189+ )
4190+ }
37894191 });
37904192 doc.apply(&transaction, view.id);
37914193 }
@@ -3815,8 +4217,19 @@ pub mod insert {
38154217 .selection(view.id)
38164218 .clone()
38174219 .transform(|range| movement::move_prev_word_start(text, range, count));
3818− doc.set_selection(view.id, selection);
3819− delete_selection(cx)
4220+ delete_selection_insert_mode(doc, view, &selection);
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);
38204233 }
38214234}
38224235
@@ -3826,20 +4239,48 @@ pub mod insert {
38264239// storing it?
38274240
38284241fn undo(cx: &mut Context) {
4242+ let count = cx.count();
38294243 let (view, doc) = current!(cx.editor);
3830− let view_id = view.id;
3831− let success = doc.undo(view_id);
3832− if !success {
3833− cx.editor.set_status("Already at oldest change".to_owned());
4244+ for _ in 0..count {
4245+ if !doc.undo(view.id) {
4246+ cx.editor.set_status("Already at oldest change".to_owned());
4247+ break;
4248+ }
38344249 }
38354250}
38364251
38374252fn redo(cx: &mut Context) {
4253+ let count = cx.count();
38384254 let (view, doc) = current!(cx.editor);
3839− let view_id = view.id;
3840− let success = doc.redo(view_id);
3841− if !success {
3842− cx.editor.set_status("Already at newest change".to_owned());
4255+ for _ in 0..count {
4256+ if !doc.redo(view.id) {
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)) {
4281+ cx.editor.set_status("Already at newest change".to_owned());
4282+ break;
4283+ }
38434284 }
38444285}
38454286
@@ -4297,12 +4738,12 @@ fn join_selections(cx: &mut Context) {
42974738 doc.append_changes_to_history(view.id);
42984739}
42994740
4300−fn keep_selections(cx: &mut Context) {
4301− // keep selections matching regex
4741+fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) {
4742+ // keep or remove selections matching regex
43024743 let reg = cx.register.unwrap_or('/');
43034744 let prompt = ui::regex_prompt(
43044745 cx,
4305− "keep:".into(),
4746+ if !remove { "keep:" } else { "remove:" }.into(),
43064747 Some(reg),
43074748 |_input: &str| Vec::new(),
43084749 move |view, doc, regex, event| {
@@ -4311,7 +4752,9 @@ fn keep_selections(cx: &mut Context) {
43114752 }
43124753 let text = doc.text().slice(..);
43134754
4314− if let Some(selection) = selection::keep_matches(text, doc.selection(view.id), &regex) {
4755+ if let Some(selection) =
4756+ selection::keep_or_remove_matches(text, doc.selection(view.id), &regex, remove)
4757+ {
43154758 doc.set_selection(view.id, selection);
43164759 }
43174760 },
@@ -4320,6 +4763,14 @@ fn keep_selections(cx: &mut Context) {
43204763 cx.push_layer(Box::new(prompt));
43214764}
43224765
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+
43234774fn keep_primary_selection(cx: &mut Context) {
43244775 let (view, doc) = current!(cx.editor);
43254776 // TODO: handle count
@@ -4445,19 +4896,15 @@ pub fn completion(cx: &mut Context) {
44454896 return;
44464897 }
44474898 let size = compositor.size();
4448− let ui = compositor
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(
4453− editor,
4454− items,
4455− offset_encoding,
4456− start_offset,
4457− trigger_offset,
4458− size,
4459− );
4460− };
4899+ let ui = compositor.find::<ui::EditorView>().unwrap();
4900+ ui.set_completion(
4901+ editor,
4902+ items,
4903+ offset_encoding,
4904+ start_offset,
4905+ trigger_offset,
4906+ size,
4907+ );
44614908 },
44624909 );
44634910}
@@ -4487,18 +4934,27 @@ fn hover(cx: &mut Context) {
44874934 move |editor: &mut Editor, compositor: &mut Compositor, response: Option<lsp::Hover>| {
44884935 if let Some(hover) = response {
44894936 // hover.contents / .range <- used for visualizing
4490− let contents = match hover.contents {
4491− lsp::HoverContents::Scalar(contents) => {
4492− // markedstring(string/languagestring to be highlighted)
4493− // TODO
4494− log::error!("hover contents {:?}", contents);
4495− return;
4496− }
4497− lsp::HoverContents::Array(contents) => {
4498− log::error!("hover contents {:?}", contents);
4499− return;
4937+
4938+ fn marked_string_to_markdown(contents: lsp::MarkedString) -> String {
4939+ match contents {
4940+ lsp::MarkedString::String(contents) => contents,
4941+ lsp::MarkedString::LanguageString(string) => {
4942+ if string.language == "markdown" {
4943+ string.value
4944+ } else {
4945+ format!("```{}\n{}\n```", string.language, string.value)
4946+ }
4947+ }
45004948 }
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"),
45024958 lsp::HoverContents::Markup(contents) => contents.value,
45034959 };
45044960
@@ -4706,7 +5162,21 @@ fn wclose(cx: &mut Context) {
47065162 }
47075163 let view_id = view!(cx.editor).id;
47085164 // 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+ }
47105180}
47115181
47125182fn select_register(cx: &mut Context) {
@@ -4717,6 +5187,15 @@ fn select_register(cx: &mut Context) {
47175187 })
47185188}
47195189
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+
47205199fn align_view_top(cx: &mut Context) {
47215200 let (view, doc) = current!(cx.editor);
47225201 align_view(doc, view, Align::Top);
@@ -4785,10 +5264,19 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
47855264
47865265 let selection = doc.selection(view.id).clone().transform(|range| {
47875266 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),
47895269 'c' => textobject_treesitter("class", range),
47905270 'f' => textobject_treesitter("function", range),
47915271 '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+ }
47925280 // TODO: cancel new ranges if inconsistent surround matches across lines
47935281 ch if !ch.is_ascii_alphanumeric() => {
47945282 textobject::textobject_surround(text, range, objtype, ch, count)
@@ -5051,6 +5539,10 @@ fn shell(cx: &mut Context, prompt: Cow<'static, str>, behavior: ShellBehavior) {
50515539 doc.apply(&transaction, view.id);
50525540 doc.append_changes_to_history(view.id);
50535541 }
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);
50545546 },
50555547 None,
50565548 );
@@ -5096,3 +5588,76 @@ fn add_newline_impl(cx: &mut Context, open: Open) {
50965588 doc.apply(&transaction, view.id);
50975589 doc.append_changes_to_history(view.id);
50985590}
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 {
177177 .any(|component| component.type_name() == type_name)
178178 }
179179
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>();
181182 self.layers
182183 .iter_mut()
183184 .find(|component| component.type_name() == type_name)
184− .map(|component| component.as_mut())
185+ .and_then(|component| component.as_any_mut().downcast_mut())
185186 }
186187}
187188
helix-term/src/config.rs+2−1
@@ -3,6 +3,7 @@ use serde::Deserialize;
33use crate::keymap::Keymaps;
44
55#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
6+#[serde(deny_unknown_fields)]
67pub struct Config {
78 pub theme: Option<String>,
89 #[serde(default)]
@@ -14,7 +15,7 @@ pub struct Config {
1415}
1516
1617#[derive(Debug, Default, Clone, PartialEq, Deserialize)]
17−#[serde(rename_all = "kebab-case")]
18+#[serde(rename_all = "kebab-case", deny_unknown_fields)]
1819pub struct LspConfig {
1920 pub display_messages: bool,
2021}
helix-term/src/keymap.rs+152−19
@@ -25,6 +25,54 @@ macro_rules! key {
2525 };
2626}
2727
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+
2876/// Macro for defining the root of a `Keymap` object. Example:
2977///
3078/// ```
@@ -53,6 +101,10 @@ macro_rules! keymap {
53101 keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
54102 };
55103
104+ (@trie [$($cmd:ident),* $(,)?]) => {
105+ $crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
106+ };
107+
56108 (
57109 { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
58110 ) => {
@@ -64,10 +116,11 @@ macro_rules! keymap {
64116 $(
65117 $(
66118 let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
67− _map.insert(
119+ let _duplicate = _map.insert(
68120 _key,
69121 keymap!(@trie $value)
70122 );
123+ debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
71124 _order.push(_key);
72125 )+
73126 )*
@@ -147,6 +200,7 @@ impl KeyTrieNode {
147200 cmd.doc()
148201 }
149202 KeyTrie::Node(n) => n.name(),
203+ KeyTrie::Sequence(_) => "[Multiple commands]",
150204 };
151205 match body.iter().position(|(d, _)| d == &desc) {
152206 Some(pos) => {
@@ -207,6 +261,7 @@ impl DerefMut for KeyTrieNode {
207261#[serde(untagged)]
208262pub enum KeyTrie {
209263 Leaf(Command),
264+ Sequence(Vec<Command>),
210265 Node(KeyTrieNode),
211266}
212267
@@ -214,14 +269,14 @@ impl KeyTrie {
214269 pub fn node(&self) -> Option<&KeyTrieNode> {
215270 match *self {
216271 KeyTrie::Node(ref node) => Some(node),
217− KeyTrie::Leaf(_) => None,
272+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
218273 }
219274 }
220275
221276 pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
222277 match *self {
223278 KeyTrie::Node(ref mut node) => Some(node),
224− KeyTrie::Leaf(_) => None,
279+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
225280 }
226281 }
227282
@@ -238,7 +293,7 @@ impl KeyTrie {
238293 trie = match trie {
239294 KeyTrie::Node(map) => map.get(key),
240295 // leaf encountered while keys left to process
241− KeyTrie::Leaf(_) => None,
296+ KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
242297 }?
243298 }
244299 Some(trie)
@@ -250,6 +305,8 @@ pub enum KeymapResultKind {
250305 /// Needs more keys to execute a command. Contains valid keys for next keystroke.
251306 Pending(KeyTrieNode),
252307 Matched(Command),
308+ /// Matched a sequence of commands to execute.
309+ MatchedSequence(Vec<Command>),
253310 /// Key was not found in the root keymap
254311 NotFound,
255312 /// Key is invalid in combination with previous keys. Contains keys leading upto
@@ -332,6 +389,12 @@ impl Keymap {
332389 Some(&KeyTrie::Leaf(cmd)) => {
333390 return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
334391 }
392+ Some(&KeyTrie::Sequence(ref cmds)) => {
393+ return KeymapResult::new(
394+ KeymapResultKind::MatchedSequence(cmds.clone()),
395+ self.sticky(),
396+ )
397+ }
335398 None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
336399 Some(t) => t,
337400 };
@@ -349,6 +412,13 @@ impl Keymap {
349412 self.state.clear();
350413 return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
351414 }
415+ Some(&KeyTrie::Sequence(ref cmds)) => {
416+ self.state.clear();
417+ KeymapResult::new(
418+ KeymapResultKind::MatchedSequence(cmds.clone()),
419+ self.sticky(),
420+ )
421+ }
352422 None => KeymapResult::new(
353423 KeymapResultKind::Cancelled(self.state.drain(..).collect()),
354424 self.sticky(),
@@ -455,6 +525,7 @@ impl Default for Keymaps {
455525 "a" => goto_last_accessed_file,
456526 "n" => goto_next_buffer,
457527 "p" => goto_previous_buffer,
528+ "." => goto_last_modification,
458529 },
459530 ":" => command_mode,
460531
@@ -511,6 +582,8 @@ impl Default for Keymaps {
511582
512583 "u" => undo,
513584 "U" => redo,
585+ "A-u" => earlier,
586+ "A-U" => later,
514587
515588 "y" => yank,
516589 // yank_all
@@ -523,7 +596,7 @@ impl Default for Keymaps {
523596 "=" => format_selections,
524597 "J" => join_selections,
525598 "K" => keep_selections,
526− // TODO: and another method for inverse
599+ "A-K" => remove_selections,
527600
528601 "," => keep_primary_selection,
529602 "A-," => remove_primary_selection,
@@ -532,7 +605,7 @@ impl Default for Keymaps {
532605 // "Q" => replay_macro,
533606
534607 // & align selections
535− // _ trim selections
608+ "_" => trim_selections,
536609
537610 "(" => rotate_selections_backward,
538611 ")" => rotate_selections_forward,
@@ -550,6 +623,7 @@ impl Default for Keymaps {
550623 "C-s" | "s" => hsplit,
551624 "C-v" | "v" => vsplit,
552625 "C-q" | "q" => wclose,
626+ "C-o" | "o" => wonly,
553627 "C-h" | "h" | "left" => jump_view_left,
554628 "C-j" | "j" | "down" => jump_view_down,
555629 "C-k" | "k" | "up" => jump_view_up,
@@ -569,6 +643,7 @@ impl Default for Keymaps {
569643 "f" => file_picker,
570644 "b" => buffer_picker,
571645 "s" => symbol_picker,
646+ "S" => workspace_symbol_picker,
572647 "a" => code_action,
573648 "'" => last_picker,
574649 "d" => { "Debug" sticky=true
@@ -593,9 +668,14 @@ impl Default for Keymaps {
593668 },
594669 "w" => { "Window"
595670 "C-w" | "w" => rotate_view,
596− "C-h" | "h" => hsplit,
671+ "C-s" | "s" => hsplit,
597672 "C-v" | "v" => vsplit,
598673 "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,
599679 },
600680 "y" => yank_joined_to_clipboard,
601681 "Y" => yank_main_selection_to_clipboard,
@@ -604,30 +684,31 @@ impl Default for Keymaps {
604684 "R" => replace_selections_with_clipboard,
605685 "/" => global_search,
606686 "k" => hover,
687+ "r" => rename_symbol,
607688 },
608689 "z" => { "View"
609690 "z" | "c" => align_view_center,
610691 "t" => align_view_top,
611692 "b" => align_view_bottom,
612693 "m" => align_view_middle,
613− "k" => scroll_up,
614− "j" => scroll_down,
615− "b" => page_up,
616− "f" => page_down,
617− "u" => half_page_up,
618− "d" => half_page_down,
694+ "k" | "up" => scroll_up,
695+ "j" | "down" => scroll_down,
696+ "C-b" | "pageup" => page_up,
697+ "C-f" | "pagedown" => page_down,
698+ "C-u" => half_page_up,
699+ "C-d" => half_page_down,
619700 },
620701 "Z" => { "View" sticky=true
621702 "z" | "c" => align_view_center,
622703 "t" => align_view_top,
623704 "b" => align_view_bottom,
624705 "m" => align_view_middle,
625− "k" => scroll_up,
626− "j" => scroll_down,
627− "b" => page_up,
628− "f" => page_down,
629− "u" => half_page_up,
630− "d" => half_page_down,
706+ "k" | "up" => scroll_up,
707+ "j" | "down" => scroll_down,
708+ "C-b" | "pageup" => page_up,
709+ "C-f" | "pagedown" => page_down,
710+ "C-u" => half_page_up,
711+ "C-d" => half_page_down,
631712 },
632713
633714 "\"" => select_register,
@@ -637,6 +718,9 @@ impl Default for Keymaps {
637718 "A-!" => shell_append_output,
638719 "$" => shell_keep_pipe,
639720 "C-z" => suspend,
721+
722+ "C-a" => increment,
723+ "C-x" => decrement,
640724 });
641725 let mut select = normal.clone();
642726 select.merge_nodes(keymap!({ "Select mode"
@@ -670,21 +754,38 @@ impl Default for Keymaps {
670754 "esc" => normal_mode,
671755
672756 "backspace" => delete_char_backward,
757+ "C-h" => delete_char_backward,
673758 "del" => delete_char_forward,
759+ "C-d" => delete_char_forward,
674760 "ret" => insert_newline,
675761 "tab" => insert_tab,
676762 "C-w" => delete_word_backward,
763+ "A-d" => delete_word_forward,
677764
678765 "left" => move_char_left,
766+ "C-b" => move_char_left,
679767 "down" => move_line_down,
768+ "C-n" => move_line_down,
680769 "up" => move_line_up,
770+ "C-p" => move_line_up,
681771 "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,
682777 "pageup" => page_up,
683778 "pagedown" => page_down,
684779 "home" => goto_line_start,
780+ "C-a" => goto_line_start,
685781 "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,
686786
687787 "C-x" => completion,
788+ "C-r" => insert_register,
688789 });
689790 Keymaps(hashmap!(
690791 Mode::Normal => Keymap::new(normal),
@@ -706,6 +807,22 @@ pub fn merge_keys(mut config: Config) -> Config {
706807#[cfg(test)]
707808mod tests {
708809 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+
709826 #[test]
710827 fn merge_partial_keys() {
711828 let config = Config {
@@ -800,4 +917,20 @@ mod tests {
800917 let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
801918 assert!(!node.node().unwrap().order().is_empty())
802919 }
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+ }
803936}
helix-term/src/main.rs+22−12
@@ -16,11 +16,6 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
1616 };
1717
1818 // 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)?;
2419 let file_config = fern::Dispatch::new()
2520 .format(|out, message, record| {
2621 out.finish(format_args!(
@@ -31,15 +26,20 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
3126 message
3227 ))
3328 })
34− .chain(file);
29+ .chain(fern::log_file(logpath)?);
3530
3631 base_config.chain(file_config).apply()?;
3732
3833 Ok(())
3934}
4035
36+fn main() -> Result<()> {
37+ let exit_code = main_impl()?;
38+ std::process::exit(exit_code);
39+}
40+
4141#[tokio::main]
42−async fn main() -> Result<()> {
42+async fn main_impl() -> Result<i32> {
4343 let cache_dir = helix_core::cache_dir();
4444 if !cache_dir.exists() {
4545 std::fs::create_dir_all(&cache_dir).ok();
@@ -66,7 +66,7 @@ FLAGS:
6666 -V, --version Prints version information
6767",
6868 env!("CARGO_PKG_NAME"),
69− env!("CARGO_PKG_VERSION"),
69+ env!("VERSION_AND_GIT_HASH"),
7070 env!("CARGO_PKG_AUTHORS"),
7171 env!("CARGO_PKG_DESCRIPTION"),
7272 logpath.display(),
@@ -81,7 +81,7 @@ FLAGS:
8181 }
8282
8383 if args.display_version {
84− println!("helix {}", env!("CARGO_PKG_VERSION"));
84+ println!("helix {}", env!("VERSION_AND_GIT_HASH"));
8585 std::process::exit(0);
8686 }
8787
@@ -91,7 +91,16 @@ FLAGS:
9191 }
9292
9393 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+ }),
95104 Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
96105 Err(err) => return Err(Error::new(err)),
97106 };
@@ -100,7 +109,8 @@ FLAGS:
100109
101110 // TODO: use the thread local executor to spawn the application task separately from the work pool
102111 let mut app = Application::new(args, config).context("unable to create new application")?;
103− app.run().await.unwrap();
104112
105− Ok(())
113+ let exit_code = app.run().await?;
114+
115+ Ok(exit_code)
106116}
helix-term/src/ui/editor.rs+25−16
@@ -17,7 +17,7 @@ use helix_core::{
1717};
1818use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
1919use helix_view::{
20− document::Mode,
20+ document::{Mode, SCRATCH_BUFFER_NAME},
2121 editor::LineNumber,
2222 graphics::{Color, CursorKind, Modifier, Rect, Style},
2323 info::Info,
@@ -610,7 +610,7 @@ impl EditorView {
610610 use tui::{
611611 layout::Alignment,
612612 text::Text,
613− widgets::{Paragraph, Widget},
613+ widgets::{Paragraph, Widget, Wrap},
614614 };
615615
616616 let cursor = doc
@@ -665,8 +665,10 @@ impl EditorView {
665665 }
666666 }
667667
668− let paragraph = Paragraph::new(lines).alignment(Alignment::Right);
669− let width = 80.min(viewport.width);
668+ let paragraph = Paragraph::new(lines)
669+ .alignment(Alignment::Right)
670+ .wrap(Wrap { trim: true });
671+ let width = 100.min(viewport.width);
670672 let height = 15.min(viewport.height);
671673 paragraph.render(
672674 Rect::new(viewport.right() - width, viewport.y + 1, width, height),
@@ -716,18 +718,20 @@ impl EditorView {
716718 }
717719 surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
718720
719− if let Some(path) = doc.relative_path() {
720− let path = path.to_string_lossy();
721+ let rel_path = doc.relative_path();
722+ let path = rel_path
723+ .as_ref()
724+ .map(|p| p.to_string_lossy())
725+ .unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
721726
722− let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
723− surface.set_stringn(
724− viewport.x + 8,
725− viewport.y,
726− title,
727− viewport.width.saturating_sub(6) as usize,
728− base_style,
729− );
730− }
727+ let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
728+ surface.set_stringn(
729+ viewport.x + 8,
730+ viewport.y,
731+ title,
732+ viewport.width.saturating_sub(6) as usize,
733+ base_style,
734+ );
731735
732736 //-------------------------------
733737 // Right side of the status line.
@@ -830,6 +834,11 @@ impl EditorView {
830834 match &key_result.kind {
831835 KeymapResultKind::Matched(command) => command.execute(cxt),
832836 KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
837+ KeymapResultKind::MatchedSequence(commands) => {
838+ for command in commands {
839+ command.execute(cxt);
840+ }
841+ }
833842 KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
834843 }
835844 None
@@ -871,7 +880,7 @@ impl EditorView {
871880 std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
872881 }
873882 // special handling for repeat operator
874− key!('.') => {
883+ key!('.') if self.keymaps.pending().is_empty() => {
875884 // first execute whatever put us into insert mode
876885 self.last_insert.0.execute(cxt);
877886 // then replay the inputs
helix-term/src/ui/markdown.rs+10−4
@@ -13,7 +13,7 @@ use helix_core::{
1313 Rope,
1414};
1515use helix_view::{
16− graphics::{Color, Margin, Rect, Style},
16+ graphics::{Margin, Rect},
1717 Theme,
1818};
1919
@@ -61,9 +61,15 @@ fn parse<'a>(
6161 })
6262 }
6363
64− let text_style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
65− let code_style = Style::default().fg(Color::Rgb(255, 255, 255)); // white
66− let heading_style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
64+ let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
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
70+ let heading_style = theme
71+ .map(|theme| theme.get("ui.linenr.selected"))
72+ .unwrap_or_default(); // lilac
6773
6874 for event in parser {
6975 match event {
helix-term/src/ui/menu.rs+11−46
@@ -1,5 +1,8 @@
1−use crate::compositor::{Component, Compositor, Context, EventResult};
2−use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
1+use crate::{
2+ compositor::{Component, Compositor, Context, EventResult},
3+ ctrl, key, shift,
4+};
5+use crossterm::event::Event;
36use tui::{buffer::Buffer as Surface, widgets::Table};
47
58pub use tui::widgets::{Cell, Row};
@@ -192,63 +195,25 @@ impl<T: Item + 'static> Component for Menu<T> {
192195 compositor.pop();
193196 })));
194197
195− match event {
198+ match event.into() {
196199 // esc or ctrl-c aborts the completion and closes the menu
197− KeyEvent {
198− code: KeyCode::Esc, ..
199− }
200− | KeyEvent {
201− code: KeyCode::Char('c'),
202− modifiers: KeyModifiers::CONTROL,
203− } => {
200+ key!(Esc) | ctrl!('c') => {
204201 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
205202 return close_fn;
206203 }
207204 // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
208− KeyEvent {
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− } => {
205+ shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
223206 self.move_up();
224207 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
225208 return EventResult::Consumed(None);
226209 }
227− // 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− } => {
210+ key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
211+ // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
244212 self.move_down();
245213 (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
246214 return EventResult::Consumed(None);
247215 }
248− KeyEvent {
249− code: KeyCode::Enter,
250− ..
251− } => {
216+ key!(Enter) => {
252217 if let Some(selection) = self.selection() {
253218 (self.callback_fn)(cx.editor, Some(selection), MenuEvent::Validate);
254219 }
helix-term/src/ui/mod.rs+2
@@ -35,6 +35,7 @@ pub fn regex_prompt(
3535 let (view, doc) = current!(cx.editor);
3636 let view_id = view.id;
3737 let snapshot = doc.selection(view_id).clone();
38+ let offset_snapshot = view.offset;
3839
3940 Prompt::new(
4041 prompt,
@@ -45,6 +46,7 @@ pub fn regex_prompt(
4546 PromptEvent::Abort => {
4647 let (view, doc) = current!(cx.editor);
4748 doc.set_selection(view.id, snapshot.clone());
49+ view.offset = offset_snapshot;
4850 }
4951 PromptEvent::Validate => {
5052 // TODO: push_jump to store selection just before jump
helix-term/src/ui/picker.rs+17−56
@@ -1,8 +1,9 @@
11use crate::{
22 compositor::{Component, Compositor, Context, EventResult},
3+ ctrl, key, shift,
34 ui::EditorView,
45};
5−use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
6+use crossterm::event::Event;
67use tui::{
78 buffer::Buffer as Surface,
89 widgets::{Block, BorderType, Borders},
@@ -36,6 +37,7 @@ type FileLocation = (PathBuf, Option<(usize, usize)>);
3637
3738pub struct FilePicker<T> {
3839 picker: Picker<T>,
40+ pub truncate_start: bool,
3941 /// Caches paths to documents
4042 preview_cache: HashMap<PathBuf, CachedPreview>,
4143 read_buffer: Vec<u8>,
@@ -89,6 +91,7 @@ impl<T> FilePicker<T> {
8991 ) -> Self {
9092 Self {
9193 picker: Picker::new(false, options, format_fn, callback_fn),
94+ truncate_start: true,
9295 preview_cache: HashMap::new(),
9396 read_buffer: Vec::with_capacity(1024),
9497 file_fn: Box::new(preview_fn),
@@ -171,6 +174,7 @@ impl<T: 'static> Component for FilePicker<T> {
171174 };
172175
173176 let picker_area = area.with_width(picker_width);
177+ self.picker.truncate_start = self.truncate_start;
174178 self.picker.render(picker_area, surface, cx);
175179
176180 if !render_preview {
@@ -276,6 +280,8 @@ pub struct Picker<T> {
276280 prompt: Prompt,
277281 /// Whether to render in the middle of the area
278282 render_centered: bool,
283+ /// Wheather to truncate the start (default true)
284+ pub truncate_start: bool,
279285
280286 format_fn: Box<dyn Fn(&T) -> Cow<str>>,
281287 callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
@@ -306,6 +312,7 @@ impl<T> Picker<T> {
306312 cursor: 0,
307313 prompt,
308314 render_centered,
315+ truncate_start: true,
309316 format_fn: Box::new(format_fn),
310317 callback_fn: Box::new(callback_fn),
311318 };
@@ -403,81 +410,35 @@ impl<T: 'static> Component for Picker<T> {
403410 compositor.last_picker = compositor.pop();
404411 })));
405412
406− match key_event {
407− KeyEvent {
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− } => {
413+ match key_event.into() {
414+ shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
422415 self.move_up();
423416 }
424− KeyEvent {
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− } => {
417+ key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
439418 self.move_down();
440419 }
441− KeyEvent {
442− code: KeyCode::Esc, ..
443− }
444− | KeyEvent {
445− code: KeyCode::Char('c'),
446− modifiers: KeyModifiers::CONTROL,
447− } => {
420+ key!(Esc) | ctrl!('c') => {
448421 return close_fn;
449422 }
450− KeyEvent {
451− code: KeyCode::Enter,
452− ..
453− } => {
423+ key!(Enter) => {
454424 if let Some(option) = self.selection() {
455425 (self.callback_fn)(cx, option, Action::Replace);
456426 }
457427 return close_fn;
458428 }
459− KeyEvent {
460− code: KeyCode::Char('s'),
461− modifiers: KeyModifiers::CONTROL,
462− } => {
429+ ctrl!('s') => {
463430 if let Some(option) = self.selection() {
464431 (self.callback_fn)(cx, option, Action::HorizontalSplit);
465432 }
466433 return close_fn;
467434 }
468− KeyEvent {
469− code: KeyCode::Char('v'),
470− modifiers: KeyModifiers::CONTROL,
471− } => {
435+ ctrl!('v') => {
472436 if let Some(option) = self.selection() {
473437 (self.callback_fn)(cx, option, Action::VerticalSplit);
474438 }
475439 return close_fn;
476440 }
477− KeyEvent {
478− code: KeyCode::Char(' '),
479− modifiers: KeyModifiers::CONTROL,
480− } => {
441+ ctrl!(' ') => {
481442 self.save_filter();
482443 }
483444 _ => {
@@ -567,7 +528,7 @@ impl<T: 'static> Component for Picker<T> {
567528 text_style
568529 },
569530 true,
570− true,
531+ self.truncate_start,
571532 );
572533 }
573534 }
helix-term/src/ui/popup.rs+9−19
@@ -1,5 +1,8 @@
1−use crate::compositor::{Component, Compositor, Context, EventResult};
2−use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
1+use crate::{
2+ compositor::{Component, Compositor, Context, EventResult},
3+ ctrl, key,
4+};
5+use crossterm::event::Event;
36use tui::buffer::Buffer as Surface;
47
58use helix_core::Position;
@@ -95,27 +98,14 @@ impl<T: Component> Component for Popup<T> {
9598 compositor.pop();
9699 })));
97100
98− match key {
101+ match key.into() {
99102 // 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,
107−
108− KeyEvent {
109− code: KeyCode::Char('d'),
110− modifiers: KeyModifiers::CONTROL,
111− } => {
103+ key!(Esc) | ctrl!('c') => close_fn,
104+ ctrl!('d') => {
112105 self.scroll(self.size.1 as usize / 2, true);
113106 EventResult::Consumed(None)
114107 }
115− KeyEvent {
116− code: KeyCode::Char('u'),
117− modifiers: KeyModifiers::CONTROL,
118− } => {
108+ ctrl!('u') => {
119109 self.scroll(self.size.1 as usize / 2, false);
120110 EventResult::Consumed(None)
121111 }
helix-term/src/ui/prompt.rs+54−104
@@ -1,6 +1,8 @@
11use crate::compositor::{Component, Compositor, Context, EventResult};
2−use crate::ui;
3−use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers};
2+use crate::{alt, ctrl, key, shift, ui};
3+use crossterm::event::Event;
4+use helix_view::input::KeyEvent;
5+use helix_view::keyboard::{KeyCode, KeyModifiers};
46use std::{borrow::Cow, ops::RangeFrom};
57use tui::buffer::Buffer as Surface;
68
@@ -213,6 +215,14 @@ impl Prompt {
213215 self.completion = (self.completion_fn)(&self.line);
214216 }
215217
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+
216226 pub fn delete_word_backwards(&mut self) {
217227 let pos = self.eval_movement(Movement::BackwardWord(1));
218228 self.line.replace_range(pos..self.cursor, "");
@@ -222,6 +232,23 @@ impl Prompt {
222232 self.completion = (self.completion_fn)(&self.line);
223233 }
224234
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+
225252 pub fn kill_to_end_of_line(&mut self) {
226253 let pos = self.eval_movement(Movement::EndOfLine);
227254 self.line.replace_range(self.cursor..pos, "");
@@ -405,84 +432,30 @@ impl Component for Prompt {
405432 compositor.pop();
406433 })));
407434
408− match event {
409− KeyEvent {
410− code: KeyCode::Char('c'),
411− modifiers: KeyModifiers::CONTROL,
412− }
413− | KeyEvent {
414− code: KeyCode::Esc, ..
415− } => {
435+ match event.into() {
436+ ctrl!('c') | key!(Esc) => {
416437 (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
417438 return close_fn;
418439 }
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)),
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)),
435− KeyEvent {
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− 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(),
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(),
467− KeyEvent {
468− code: KeyCode::Char('w'),
469− modifiers: KeyModifiers::CONTROL,
470− } => self.delete_word_backwards(),
471− KeyEvent {
472− code: KeyCode::Char('k'),
473− modifiers: KeyModifiers::CONTROL,
474− } => self.kill_to_end_of_line(),
475− KeyEvent {
476− code: KeyCode::Backspace,
477− modifiers: KeyModifiers::NONE,
478− } => {
440+ alt!('b') | alt!(Left) => self.move_cursor(Movement::BackwardWord(1)),
441+ alt!('f') | alt!(Right) => self.move_cursor(Movement::ForwardWord(1)),
442+ ctrl!('b') | key!(Left) => self.move_cursor(Movement::BackwardChar(1)),
443+ ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)),
444+ ctrl!('e') | key!(End) => self.move_end(),
445+ ctrl!('a') | key!(Home) => self.move_start(),
446+ ctrl!('w') => self.delete_word_backwards(),
447+ alt!('d') => self.delete_word_forwards(),
448+ ctrl!('k') => self.kill_to_end_of_line(),
449+ ctrl!('u') => self.kill_to_start_of_line(),
450+ ctrl!('h') | key!(Backspace) => {
479451 self.delete_char_backwards();
480452 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
481453 }
482− KeyEvent {
483− code: KeyCode::Char('s'),
484− modifiers: KeyModifiers::CONTROL,
485− } => {
454+ ctrl!('d') | key!(Delete) => {
455+ self.delete_char_forwards();
456+ (self.callback_fn)(cx, &self.line, PromptEvent::Update);
457+ }
458+ ctrl!('s') => {
486459 let (view, doc) = current!(cx.editor);
487460 let text = doc.text().slice(..);
488461
@@ -492,6 +465,7 @@ impl Component for Prompt {
492465 doc.selection(view.id).primary(),
493466 textobject::TextObject::Inside,
494467 1,
468+ false,
495469 );
496470 let line = text.slice(range.from()..range.to()).to_string();
497471 if !line.is_empty() {
@@ -499,10 +473,7 @@ impl Component for Prompt {
499473 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
500474 }
501475 }
502− KeyEvent {
503− code: KeyCode::Enter,
504− ..
505− } => {
476+ key!(Enter) => {
506477 if self.selection.is_some() && self.line.ends_with('/') {
507478 self.completion = (self.completion_fn)(&self.line);
508479 self.exit_selection();
@@ -517,50 +488,29 @@ impl Component for Prompt {
517488 return close_fn;
518489 }
519490 }
520− KeyEvent {
521− code: KeyCode::Char('p'),
522− modifiers: KeyModifiers::CONTROL,
523− }
524− | KeyEvent {
525− code: KeyCode::Up, ..
526− } => {
491+ ctrl!('p') | key!(Up) => {
527492 if let Some(register) = self.history_register {
528493 let register = cx.editor.registers.get_mut(register);
529494 self.change_history(register.read(), CompletionDirection::Backward);
530495 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
531496 }
532497 }
533− KeyEvent {
534− code: KeyCode::Char('n'),
535− modifiers: KeyModifiers::CONTROL,
536− }
537− | KeyEvent {
538− code: KeyCode::Down,
539− ..
540− } => {
498+ ctrl!('n') | key!(Down) => {
541499 if let Some(register) = self.history_register {
542500 let register = cx.editor.registers.get_mut(register);
543501 self.change_history(register.read(), CompletionDirection::Forward);
544502 (self.callback_fn)(cx, &self.line, PromptEvent::Update);
545503 }
546504 }
547− KeyEvent {
548− code: KeyCode::Tab, ..
549− } => {
505+ key!(Tab) => {
550506 self.change_completion_selection(CompletionDirection::Forward);
551507 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
552508 }
553− KeyEvent {
554− code: KeyCode::BackTab,
555− ..
556− } => {
509+ shift!(BackTab) => {
557510 self.change_completion_selection(CompletionDirection::Backward);
558511 (self.callback_fn)(cx, &self.line, PromptEvent::Update)
559512 }
560− KeyEvent {
561− code: KeyCode::Char('q'),
562− modifiers: KeyModifiers::CONTROL,
563− } => self.exit_selection(),
513+ ctrl!('q') => self.exit_selection(),
564514 // any char event that's not combined with control or mapped to any other combo
565515 KeyEvent {
566516 code: KeyCode::Char(c),
helix-view/src/document.rs+26−6
@@ -25,6 +25,8 @@ const BUF_SIZE: usize = 8192;
2525
2626const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
2727
28+pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
29+
2830#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
2931pub enum Mode {
3032 Normal,
@@ -96,7 +98,7 @@ pub struct Document {
9698 // It can be used as a cell where we will take it out to get some parts of the history and put
9799 // it back as it separated from the edits. We could split out the parts manually but that will
98100 // be more troublesome.
99− history: Cell<History>,
101+ pub history: Cell<History>,
100102
101103 pub savepoint: Option<Transaction>,
102104
@@ -494,7 +496,9 @@ impl Document {
494496 /// Detect the programming language based on the file type.
495497 pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
496498 if let Some(path) = &self.path {
497− let language_config = config_loader.language_config_for_file_name(path);
499+ let language_config = config_loader
500+ .language_config_for_file_name(path)
501+ .or_else(|| config_loader.language_config_for_shebang(self.text()));
498502 self.set_language(theme, language_config);
499503 }
500504 }
@@ -749,19 +753,35 @@ impl Document {
749753 }
750754
751755 /// 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 {
753757 let txns = self.history.get_mut().earlier(uk);
758+ let mut success = false;
754759 for txn in txns {
755− self.apply_impl(&txn, view_id);
760+ if self.apply_impl(&txn, view_id) {
761+ success = true;
762+ }
756763 }
764+ if success {
765+ // reset changeset to fix len
766+ self.changes = ChangeSet::new(self.text());
767+ }
768+ success
757769 }
758770
759771 /// 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 {
761773 let txns = self.history.get_mut().later(uk);
774+ let mut success = false;
762775 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());
764783 }
784+ success
765785 }
766786
767787 /// Commit pending changes to history
helix-view/src/editor.rs+116−33
@@ -1,5 +1,6 @@
11use crate::{
22 clipboard::{get_clipboard_provider, ClipboardProvider},
3+ document::SCRATCH_BUFFER_NAME,
34 graphics::{CursorKind, Rect},
45 theme::{self, Theme},
56 tree::{self, Tree},
@@ -11,8 +12,8 @@ use futures_util::stream::select_all::SelectAll;
1112use tokio_stream::wrappers::UnboundedReceiverStream;
1213
1314use std::{
14− collections::BTreeMap,
15− collections::HashMap,
15+ collections::{BTreeMap, HashMap},
16+ io::stdin,
1617 path::{Path, PathBuf},
1718 pin::Pin,
1819 sync::Arc,
@@ -25,8 +26,8 @@ use anyhow::Error;
2526pub use helix_core::diagnostic::Severity;
2627pub use helix_core::register::Registers;
2728use helix_core::syntax;
28−use helix_core::Position;
2929use helix_dap as dap;
30+use helix_core::{Position, Selection};
3031
3132use serde::Deserialize;
3233
@@ -39,7 +40,7 @@ where
3940}
4041
4142#[derive(Debug, Clone, PartialEq, Deserialize)]
42−#[serde(rename_all = "kebab-case", default)]
43+#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
4344pub struct Config {
4445 /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
4546 pub scrolloff: usize,
@@ -138,6 +139,8 @@ pub struct Editor {
138139
139140 pub idle_timer: Pin<Box<Sleep>>,
140141 pub last_motion: Option<Motion>,
142+
143+ pub exit_code: i32,
141144}
142145
143146#[derive(Debug, Copy, Clone)]
@@ -179,6 +182,7 @@ impl Editor {
179182 idle_timer: Box::pin(sleep(config.idle_timeout)),
180183 last_motion: None,
181184 config,
185+ exit_code: 0,
182186 }
183187 }
184188
@@ -208,6 +212,12 @@ impl Editor {
208212 }
209213
210214 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+
211221 let scopes = theme.scopes();
212222 for config in self
213223 .syn_loader
@@ -238,9 +248,28 @@ impl Editor {
238248 }
239249 }
240250
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+
241271 pub fn switch(&mut self, id: DocumentId, action: Action) {
242272 use crate::tree::Layout;
243− use helix_core::Selection;
244273
245274 if !self.documents.contains_key(&id) {
246275 log::error!("cannot switch to document that does not exist (anymore)");
@@ -274,26 +303,19 @@ impl Editor {
274303 view.jumps.push(jump);
275304 view.last_accessed_doc = Some(view.doc);
276305 }
277− view.doc = id;
278− view.offset = Position::default();
279306
280− let (view, doc) = current!(self);
281−
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);
307+ let view_id = view.id;
308+ self.replace_document_in_view(view_id, id);
293309
294310 return;
295311 }
296312 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+ }
297319 return;
298320 }
299321 Action::HorizontalSplit => {
@@ -315,16 +337,29 @@ impl Editor {
315337 self._refresh();
316338 }
317339
318− pub fn new_file(&mut self, action: Action) -> DocumentId {
340+ fn new_document(&mut self, mut document: Document) -> DocumentId {
319341 let id = DocumentId(self.next_document_id);
320342 self.next_document_id += 1;
321− let mut doc = Document::default();
322− doc.id = id;
323− self.documents.insert(id, doc);
343+ document.id = id;
344+ self.documents.insert(id, document);
345+ id
346+ }
347+
348+ fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
349+ let id = self.new_document(document);
324350 self.switch(id, action);
325351 id
326352 }
327353
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+
328363 pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
329364 let path = helix_core::path::get_canonicalized_path(&path)?;
330365
@@ -381,7 +416,7 @@ impl Editor {
381416 Ok(id)
382417 }
383418
384− pub fn close(&mut self, id: ViewId, close_buffer: bool) {
419+ pub fn close(&mut self, id: ViewId) {
385420 let view = self.tree.get(self.tree.focus);
386421 // remove selection
387422 self.documents
@@ -390,18 +425,66 @@ impl Editor {
390425 .selections
391426 .remove(&id);
392427
393− if close_buffer {
394− // get around borrowck issues
395− let doc = &self.documents[&view.doc];
428+ self.tree.remove(id);
429+ self._refresh();
430+ }
396431
397− if let Some(language_server) = doc.language_server() {
398− tokio::spawn(language_server.text_document_did_close(doc.identifier()));
399− }
400− self.documents.remove(&view.doc);
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+
447+ if let Some(language_server) = doc.language_server() {
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+ }
460+ })
461+ .collect::<Vec<_>>();
462+
463+ for view_id in views_to_close {
464+ self.close(view_id);
465+ }
466+
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));
401483 }
402484
403− self.tree.remove(id);
404485 self._refresh();
486+
487+ Ok(())
405488 }
406489
407490 pub fn resize(&mut self, area: Rect) {
helix-view/src/macros.rs+20−20
@@ -11,10 +11,19 @@
1111/// Returns `(&mut View, &mut Document)`
1212#[macro_export]
1313macro_rules! current {
14− ( $( $editor:ident ).+ ) => {{
15− let view = $crate::view_mut!( $( $editor ).+ );
14+ ($editor:expr) => {{
15+ let view = $crate::view_mut!($editor);
1616 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];
1827 (view, doc)
1928 }};
2029}
@@ -23,8 +32,8 @@ macro_rules! current {
2332/// Returns `&mut Document`
2433#[macro_export]
2534macro_rules! doc_mut {
26− ( $( $editor:ident ).+ ) => {{
27− $crate::current!( $( $editor ).+ ).1
35+ ($editor:expr) => {{
36+ $crate::current!($editor).1
2837 }};
2938}
3039
@@ -32,8 +41,8 @@ macro_rules! doc_mut {
3241/// Returns `&mut View`
3342#[macro_export]
3443macro_rules! view_mut {
35− ( $( $editor:ident ).+ ) => {{
36− $( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus)
44+ ($editor:expr) => {{
45+ $editor.tree.get_mut($editor.tree.focus)
3746 }};
3847}
3948
@@ -41,23 +50,14 @@ macro_rules! view_mut {
4150/// Returns `&View`
4251#[macro_export]
4352macro_rules! view {
44− ( $( $editor:ident ).+ ) => {{
45− $( $editor ).+ .tree.get($( $editor ).+ .tree.focus)
53+ ($editor:expr) => {{
54+ $editor.tree.get($editor.tree.focus)
4655 }};
4756}
4857
4958#[macro_export]
5059macro_rules! doc {
51− ( $( $editor:ident ).+ ) => {{
52− $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)
60+ ($editor:expr) => {{
61+ $crate::current_ref!($editor).1
6262 }};
6363}
helix-view/src/tree.rs+3
@@ -314,6 +314,9 @@ impl Tree {
314314
315315 pub fn recalculate(&mut self) {
316316 if self.is_empty() {
317+ // There are no more views, so the tree should focus itself again.
318+ self.focus = self.root;
319+
317320 return;
318321 }
319322
helix-view/src/view.rs+37−8
@@ -54,6 +54,10 @@ impl JumpList {
5454 None
5555 }
5656 }
57+
58+ pub fn remove(&mut self, doc_id: &DocumentId) {
59+ self.jumps.retain(|(other_id, _)| other_id != doc_id);
60+ }
5761}
5862
5963#[derive(Debug)]
@@ -85,7 +89,12 @@ impl View {
8589 self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
8690 }
8791
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)> {
8998 let cursor = doc
9099 .selection(self.id)
91100 .primary()
@@ -104,23 +113,43 @@ impl View {
104113
105114 let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
106115
107− if line > last_line.saturating_sub(scrolloff) {
116+ let row = if line > last_line.saturating_sub(scrolloff) {
108117 // scroll down
109− self.offset.row += line - (last_line.saturating_sub(scrolloff));
118+ self.offset.row + line - (last_line.saturating_sub(scrolloff))
110119 } else if line < self.offset.row + scrolloff {
111120 // scroll up
112− self.offset.row = line.saturating_sub(scrolloff);
113− }
121+ line.saturating_sub(scrolloff)
122+ } else {
123+ self.offset.row
124+ };
114125
115− if col > last_col.saturating_sub(scrolloff) {
126+ let col = if col > last_col.saturating_sub(scrolloff) {
116127 // scroll right
117− self.offset.col += col - (last_col.saturating_sub(scrolloff));
128+ self.offset.col + col - (last_col.saturating_sub(scrolloff))
118129 } else if col < self.offset.col + scrolloff {
119130 // 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;
121146 }
122147 }
123148
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+
124153 /// Calculates the last visible line on screen
125154 #[inline]
126155 pub fn last_line(&self, doc: &Document) -> usize {
languages.toml+41
@@ -60,12 +60,25 @@ name = "elixir"
6060scope = "source.elixir"
6161injection-regex = "elixir"
6262file-types = ["ex", "exs"]
63+shebangs = ["elixir"]
6364roots = []
6465comment-token = "#"
6566
6667language-server = { command = "elixir-ls" }
6768indent = { tab-width = 2, unit = " " }
6869
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+
6982[[language]]
7083name = "json"
7184scope = "source.json"
@@ -202,6 +215,7 @@ name = "javascript"
202215scope = "source.js"
203216injection-regex = "^(js|javascript)$"
204217file-types = ["js", "mjs"]
218+shebangs = ["node"]
205219roots = []
206220comment-token = "//"
207221# TODO: highlights-jsx, highlights-params
@@ -225,6 +239,7 @@ name = "typescript"
225239scope = "source.ts"
226240injection-regex = "^(ts|typescript)$"
227241file-types = ["ts"]
242+shebangs = []
228243roots = []
229244# TODO: highlights-jsx, highlights-params
230245
@@ -265,6 +280,7 @@ name = "python"
265280scope = "source.python"
266281injection-regex = "python"
267282file-types = ["py"]
283+shebangs = ["python"]
268284roots = []
269285comment-token = "#"
270286
@@ -277,6 +293,7 @@ name = "nix"
277293scope = "source.nix"
278294injection-regex = "nix"
279295file-types = ["nix"]
296+shebangs = []
280297roots = []
281298comment-token = "#"
282299
@@ -288,6 +305,7 @@ name = "ruby"
288305scope = "source.ruby"
289306injection-regex = "ruby"
290307file-types = ["rb"]
308+shebangs = ["ruby"]
291309roots = []
292310comment-token = "#"
293311
@@ -299,6 +317,7 @@ name = "bash"
299317scope = "source.bash"
300318injection-regex = "bash"
301319file-types = ["sh", "bash"]
320+shebangs = ["sh", "bash", "dash"]
302321roots = []
303322comment-token = "#"
304323
@@ -310,6 +329,7 @@ name = "php"
310329scope = "source.php"
311330injection-regex = "php"
312331file-types = ["php"]
332+shebangs = ["php"]
313333roots = []
314334
315335indent = { tab-width = 4, unit = " " }
@@ -371,6 +391,7 @@ name = "ocaml"
371391scope = "source.ocaml"
372392injection-regex = "ocaml"
373393file-types = ["ml"]
394+shebangs = []
374395roots = []
375396comment-token = "(**)"
376397indent = { tab-width = 2, unit = " " }
@@ -379,6 +400,7 @@ indent = { tab-width = 2, unit = " " }
379400name = "ocaml-interface"
380401scope = "source.ocaml.interface"
381402file-types = ["mli"]
403+shebangs = []
382404roots = []
383405comment-token = "(**)"
384406indent = { tab-width = 2, unit = " "}
@@ -387,6 +409,7 @@ indent = { tab-width = 2, unit = " "}
387409name = "lua"
388410scope = "source.lua"
389411file-types = ["lua"]
412+shebangs = ["lua"]
390413roots = []
391414comment-token = "--"
392415indent = { tab-width = 2, unit = " " }
@@ -444,6 +467,7 @@ name = "prolog"
444467scope = "source.prolog"
445468roots = []
446469file-types = ["pl", "prolog"]
470+shebangs = ["swipl"]
447471comment-token = "%"
448472
449473language-server = { command = "swipl", args = [
@@ -467,3 +491,20 @@ roots = []
467491comment-token = "#"
468492indent = { tab-width = 2, unit = " " }
469493language-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 @@
1010# Polar Night
1111# nord0 - background color
1212"ui.background" = { bg = "nord0" }
13−"ui.statusline.inactive" = { fg = "nord4", bg = "nord0" }
13+"ui.statusline.inactive" = { fg = "nord8", bg = "nord1" }
1414
1515
1616# nord1 - status bars, panels, modals, autocompletion
17−"ui.statusline" = { fg = "nord8", bg = "nord1" }
17+"ui.statusline" = { fg = "nord4", bg = "#4c566a" }
1818"ui.popup" = { bg = "#232d38" }
1919"ui.window" = { bg = "#232d38" }
2020"ui.help" = { bg = "#232d38", fg = "nord4" }
@@ -25,7 +25,7 @@
2525
2626# nord3 - comments, nord3 based lighter color
2727# relative: https://github.com/arcticicestudio/nord/issues/94
28−"comment" = "gray"
28+"comment" = { fg = "gray", modifiers = ["italic"] }
2929"ui.linenr" = { fg = "gray" }
3030
3131# 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"
2323constant = "white"
2424"constant.builtin" = "white"
2525string = "silver"
26−number = "chamois"
27−escape = "honey"
26+"constant.numeric" = "chamois"
27+"constant.character.escape" = "honey"
2828# used for lifetimes
2929label = "honey"
3030