Merge remote-tracking branch 'origin/master' into debug

imgbot
Blaž Hrastnik 3 years ago
commit d1854d8e6a

8
.gitmodules vendored

@ -134,3 +134,11 @@
path = helix-syntax/languages/tree-sitter-cmake path = helix-syntax/languages/tree-sitter-cmake
url = https://github.com/uyha/tree-sitter-cmake url = https://github.com/uyha/tree-sitter-cmake
shallow = true shallow = true
[submodule "helix-syntax/languages/tree-sitter-glsl"]
path = helix-syntax/languages/tree-sitter-glsl
url = https://github.com/theHamsta/tree-sitter-glsl.git
shallow = true
[submodule "helix-syntax/languages/tree-sitter-perl"]
path = helix-syntax/languages/tree-sitter-perl
url = https://github.com/ganezdragon/tree-sitter-perl
shallow = true

24
Cargo.lock generated

@ -13,15 +13,15 @@ dependencies = [
[[package]] [[package]]
name = "anyhow" name = "anyhow"
version = "1.0.44" version = "1.0.46"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "61604a8f862e1d5c3229fdd78f8b02c68dcf73a4c4b05fd636d12240aaa242c1" checksum = "3aa828229c44c0293dd7d4d2300bdfc4d2883ffdba934c069a6b968957a81f70"
[[package]] [[package]]
name = "arc-swap" name = "arc-swap"
version = "1.4.0" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e6df5aef5c5830360ce5218cecb8f018af3438af5686ae945094affc86fdec63" checksum = "c5d78ce20460b82d3fa150275ed9d55e21064fc7951177baacf86a145c4a4b1f"
[[package]] [[package]]
name = "autocfg" name = "autocfg"
@ -66,9 +66,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
[[package]] [[package]]
name = "cc" name = "cc"
version = "1.0.71" version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "79c2681d6594606957bbb8631c4b90a7fcaaa72cdb714743a437b156d6a7eedd" checksum = "22a9137b95ea06864e018375b72adfb7db6e6f68cfc8df5a04d00288050485ee"
[[package]] [[package]]
name = "cfg-if" name = "cfg-if"
@ -583,9 +583,9 @@ checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
[[package]] [[package]]
name = "libloading" name = "libloading"
version = "0.7.1" version = "0.7.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0cf036d15402bea3c5d4de17b3fce76b3e4a56ebc1f577be0e7a72f7c607cf0" checksum = "afe203d669ec979b7128619bae5a63b7b42e9203c1b29146079ee05e2f604b52"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"winapi", "winapi",
@ -914,9 +914,9 @@ dependencies = [
[[package]] [[package]]
name = "serde_json" name = "serde_json"
version = "1.0.68" version = "1.0.70"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0f690853975602e1bfe1ccbf50504d67174e3bcf340f23b5ea9992e0587a52d8" checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
dependencies = [ dependencies = [
"itoa", "itoa",
"ryu", "ryu",
@ -1086,9 +1086,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.13.0" version = "1.13.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee" checksum = "52963f91310c08d91cb7bff5786dfc8b79642ab839e188187e92105dbfb9d2c8"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",

@ -15,3 +15,6 @@ members = [
[profile.dev] [profile.dev]
split-debuginfo = "unpacked" split-debuginfo = "unpacked"
[profile.release]
lto = "thin"

@ -8,5 +8,7 @@
- [Keymap](./keymap.md) - [Keymap](./keymap.md)
- [Key Remapping](./remapping.md) - [Key Remapping](./remapping.md)
- [Hooks](./hooks.md) - [Hooks](./hooks.md)
- [Languages](./languages.md)
- [Guides](./guides/README.md) - [Guides](./guides/README.md)
- [Adding Languages](./guides/adding_languages.md)
- [Adding Textobject Queries](./guides/textobject.md) - [Adding Textobject Queries](./guides/textobject.md)

@ -0,0 +1,60 @@
# Adding languages
## Submodules
To add a new langauge, you should first add a tree-sitter submodule. To do this,
you can run the command
```sh
git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
```
For example, to add tree-sitter-ocaml you would run
```sh
git submodule add -f https://github.com/tree-sitter/tree-sitter-ocaml helix-syntax/languages/tree-sitter-ocaml
```
Make sure the submodule is shallow by doing
```sh
git config -f .gitmodules submodule.helix-syntax/languages/tree-sitter-<name>.shallow true
```
or you can manually add `shallow = true` to `.gitmodules`.
## languages.toml
Next, you need to add the language to the [`languages.toml`][languages.toml] found in the root of
the repository; this `languages.toml` file is included at compilation time, and
is distinct from the `language.toml` file in the user's [configuration
directory](../configuration.md).
These are the available keys and descriptions for the file.
| Key | Description |
| ---- | ----------- |
| name | The name of the language |
| 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 |
| 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. |
| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` |
| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
| auto-format | Whether to autoformat this language when saving |
| comment-token | The token to use as a comment-token |
| indent | The indent to use. Has sub keys `tab-width` and `unit` |
| config | Language server configuration |
## Queries
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.
## Common Issues
- 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
```sh
git submodule sync; git submodule update --init
```
- 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.
- 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`
- The indents query is `indents.toml`, *not* `indents.scm`. See [this](https://github.com/helix-editor/helix/issues/114) issue for more information.
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml

@ -5,7 +5,7 @@ require an accompanying tree-sitter grammar and a `textobjects.scm` query file
to work properly. Tree-sitter allows us to query the source code syntax tree to work properly. Tree-sitter allows us to query the source code syntax tree
and capture specific parts of it. The queries are written in a lisp dialect. and capture specific parts of it. The queries are written in a lisp dialect.
More information on how to write queries can be found in the [official tree-sitter More information on how to write queries can be found in the [official tree-sitter
documentation](tree-sitter-queries). documentation][tree-sitter-queries].
Query files should be placed in `runtime/queries/{language}/textobjects.scm` Query files should be placed in `runtime/queries/{language}/textobjects.scm`
when contributing. Note that to test the query files locally you should put when contributing. Note that to test the query files locally you should put

@ -1,5 +1,8 @@
# Keymap # Keymap
- Mappings marked (**LSP**) require an active language server for the file.
- Mappings marked (**TS**) require a tree-sitter grammar for the filetype.
## Normal mode ## Normal mode
### Movement ### Movement
@ -58,15 +61,19 @@
| `.` | Repeat last change | N/A | | `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` | | `u` | Undo change | `undo` |
| `U` | Redo change | `redo` | | `U` | Redo change | `redo` |
| `Alt-u` | Move backward in history | `earlier` |
| `Alt-U` | Move forward in history | `later` |
| `y` | Yank selection | `yank` | | `y` | Yank selection | `yank` |
| `p` | Paste after selection | `paste_after` | | `p` | Paste after selection | `paste_after` |
| `P` | Paste before selection | `paste_before` | | `P` | Paste before selection | `paste_before` |
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` | | `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` | | `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` | | `<` | Unindent selection | `unindent` |
| `=` | Format selection | `format_selections` | | `=` | Format selection (**LSP**) | `format_selections` |
| `d` | Delete selection | `delete_selection` | | `d` | Delete selection | `delete_selection` |
| `c` | Change selection (delete and enter insert mode) | `change_selection` | | `c` | Change selection (delete and enter insert mode) | `change_selection` |
| `Ctrl-a` | Increment object (number) under cursor | `increment` |
| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
#### Shell #### Shell
@ -85,6 +92,7 @@
| `s` | Select all regex matches inside selections | `select_regex` | | `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into subselections on regex matches | `split_selection` | | `S` | Split selection into subselections on regex matches | `split_selection` |
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
| `_` | Trim whitespace from the selection | `trim_selections` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` | | `;` | Collapse selection onto a single cursor | `collapse_selection` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| `,` | Keep only the primary selection | `keep_primary_selection` | | `,` | Keep only the primary selection | `keep_primary_selection` |
@ -98,9 +106,10 @@
| `%` | Select entire file | `select_all` | | `%` | Select entire file | `select_all` |
| `x` | Select current line, if already selected, extend to next line | `extend_line` | | `x` | Select current line, if already selected, extend to next line | `extend_line` |
| `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` | | `X` | Extend selection to line bounds (line-wise selection) | `extend_to_line_bounds` |
| | Expand selection to parent syntax node TODO: pick a key | `expand_selection` | | | Expand selection to parent syntax node TODO: pick a key (**TS**) | `expand_selection` |
| `J` | Join lines inside selection | `join_selections` | | `J` | Join lines inside selection | `join_selections` |
| `K` | Keep selections matching the regex | `keep_selections` | | `K` | Keep selections matching the regex | `keep_selections` |
| `Alt-K` | Remove selections matching the regex | `remove_selections` |
| `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` | | `$` | Pipe each selection into shell command, keep selections where command returned 0 | `shell_keep_pipe` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` | | `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
@ -127,25 +136,23 @@ key to return to normal mode after usage (useful when you're simply looking
over text and not actively editing it). over text and not actively editing it).
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `z` , `c` | Vertically center the line | `align_view_center` | | `z` , `c` | Vertically center the line | `align_view_center` |
| `t` | Align the line to the top of the screen | `align_view_top` | | `t` | Align the line to the top of the screen | `align_view_top` |
| `b` | Align the line to the bottom of the screen | `align_view_bottom` | | `b` | Align the line to the bottom of the screen | `align_view_bottom` |
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` | | `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j` | Scroll the view downwards | `scroll_down` | | `j` , `down` | Scroll the view downwards | `scroll_down` |
| `k` | Scroll the view upwards | `scroll_up` | | `k` , `up` | Scroll the view upwards | `scroll_up` |
| `f` | Move page down | `page_down` | | `f` | Move page down | `page_down` |
| `b` | Move page up | `page_up` | | `b` | Move page up | `page_up` |
| `d` | Move half page down | `half_page_down` | | `d` | Move half page down | `half_page_down` |
| `u` | Move half page up | `half_page_up` | | `u` | Move half page up | `half_page_up` |
#### Goto mode #### Goto mode
Jumps to various locations. Jumps to various locations.
> NOTE: Some of these features are only available with the LSP present.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `g` | Go to the start of the file | `goto_file_start` | | `g` | Go to the start of the file | `goto_file_start` |
@ -156,13 +163,14 @@ Jumps to various locations.
| `t` | Go to the top of the screen | `goto_window_top` | | `t` | Go to the top of the screen | `goto_window_top` |
| `m` | Go to the middle of the screen | `goto_window_middle` | | `m` | Go to the middle of the screen | `goto_window_middle` |
| `b` | Go to the bottom of the screen | `goto_window_bottom` | | `b` | Go to the bottom of the screen | `goto_window_bottom` |
| `d` | Go to definition | `goto_definition` | | `d` | Go to definition (**LSP**) | `goto_definition` |
| `y` | Go to type definition | `goto_type_definition` | | `y` | Go to type definition (**LSP**) | `goto_type_definition` |
| `r` | Go to references | `goto_reference` | | `r` | Go to references (**LSP**) | `goto_reference` |
| `i` | Go to implementation | `goto_implementation` | | `i` | Go to implementation (**LSP**) | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | | `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
| `n` | Go to next buffer | `goto_next_buffer` | | `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` | | `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | Go to last modification in current file | `goto_last_modification` |
#### Match mode #### Match mode
@ -172,7 +180,7 @@ and [textobject](./usage.md#textobject) usage.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `m` | Goto matching bracket | `match_brackets` | | `m` | Goto matching bracket (**TS**) | `match_brackets` |
| `s` `<char>` | Surround current selection with `<char>` | `surround_add` | | `s` `<char>` | Surround current selection with `<char>` | `surround_add` |
| `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` | | `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` |
| `d` `<char>` | Delete surround character `<char>` | `surround_delete` | | `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.
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` | | `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` | | `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
| `q`, `Ctrl-q` | Close current window | `wclose` | | `q`, `Ctrl-q` | Close current window | `wclose` |
| `o`, `Ctrl-o` | Only keep the current window, closing all the others | `wonly` |
#### Space mode #### Space mode
This layer is a kludge of mappings, mostly pickers. This layer is a kludge of mappings, mostly pickers.
| Key | Description | Command |
| ----- | ----------- | ------- | | Key | Description | Command |
| `k` | Show documentation for the item under the cursor | `hover` | | ----- | ----------- | ------- |
| `f` | Open file picker | `file_picker` | | `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` | | `b` | Open buffer picker | `buffer_picker` |
| `s` | Open symbol picker (current document) | `symbol_picker` | | `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `a` | Apply code action | `code_action` | | `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `'` | Open last fuzzy picker | `last_picker` | | `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
| `w` | Enter [window mode](#window-mode) | N/A | | `r` | Rename symbol (**LSP**) | `rename_symbol` |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` | | `a` | Apply code action (**LSP**) | `code_action` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` | | `'` | Open last fuzzy picker | `last_picker` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | | `w` | Enter [window mode](#window-mode) | N/A |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | | `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | | `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `/` | Global search in workspace folder | `global_search` | | `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file. | `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
> TIP: Global search displays results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
##### Popup
Displays documentation for item under cursor.
| Key | Description |
| ---- | ----------- |
| `Ctrl-u` | Scroll up |
| `Ctrl-d` | Scroll down |
#### Unimpaired #### Unimpaired
Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `[d` | Go to previous diagnostic | `goto_prev_diag` | | `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
| `]d` | Go to next diagnostic | `goto_next_diag` | | `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[D` | Go to first diagnostic in document | `goto_first_diag` | | `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
| `]D` | Go to last diagnostic in document | `goto_last_diag` | | `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `[space` | Add newline above | `add_newline_above` | | `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` | | `]space` | Add newline below | `add_newline_below` |
## Insert Mode ## Insert Mode
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` | | `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` | | `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-w` | Delete previous word | `delete_word_backward` | | `Ctrl-r` | Insert a register content | `insert_register` |
| `Ctrl-w` | Delete previous word | `delete_word_backward` |
| `Alt-d` | Delete next word | `delete_word_forward` |
| `Alt-b`, `Alt-Left` | Backward a word | `move_prev_word_end` |
| `Ctrl-b`, `Left` | Backward a char | `move_char_left` |
| `Alt-f`, `Alt-Right` | Forward a word | `move_next_word_start` |
| `Ctrl-f`, `Right` | Forward a char | `move_char_right` |
| `Ctrl-e`, `End` | move to line end | `goto_line_end_newline` |
| `Ctrl-a`, `Home` | move to line start | `goto_line_start` |
| `Ctrl-u` | delete to start of line | `kill_to_line_start` |
| `Ctrl-k` | delete to end of line | `kill_to_line_end` |
| `backspace`, `Ctrl-h` | delete previous char | `delete_char_backward` |
| `delete`, `Ctrl-d` | delete previous char | `delete_char_forward` |
| `Ctrl-p`, `Up` | move to previous line | `move_line_up` |
| `Ctrl-n`, `Down` | move to next line | `move_line_down` |
## Select / extend mode ## Select / extend mode
@ -262,7 +297,9 @@ Keys to use within picker. Remapping currently not supported.
| `Escape`, `Ctrl-c` | Close picker | | `Escape`, `Ctrl-c` | Close picker |
# Prompt # Prompt
Keys to use within prompt, Remapping currently not supported. Keys to use within prompt, Remapping currently not supported.
| Key | Description | | Key | Description |
| ----- | ------------- | | ----- | ------------- |
| `Escape`, `Ctrl-c` | Close prompt | | `Escape`, `Ctrl-c` | Close prompt |
@ -270,15 +307,18 @@ Keys to use within prompt, Remapping currently not supported.
| `Ctrl-b`, `Left` | Backward a char | | `Ctrl-b`, `Left` | Backward a char |
| `Alt-f`, `Alt-Right` | Forward a word | | `Alt-f`, `Alt-Right` | Forward a word |
| `Ctrl-f`, `Right` | Forward a char | | `Ctrl-f`, `Right` | Forward a char |
| `Ctrl-e`, `End` | move prompt end | | `Ctrl-e`, `End` | Move prompt end |
| `Ctrl-a`, `Home` | move prompt start | | `Ctrl-a`, `Home` | Move prompt start |
| `Ctrl-w` | delete previous word | | `Ctrl-w` | Delete previous word |
| `Ctrl-k` | delete to end of line | | `Alt-d` | Delete next word |
| `backspace` | delete previous char | | `Ctrl-u` | Delete to start of line |
| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later | | `Ctrl-k` | Delete to end of line |
| `Ctrl-p`, `Up` | select previous history | | `backspace`, `Ctrl-h` | Delete previous char |
| `Ctrl-n`, `Down` | select next history | | `delete`, `Ctrl-d` | Delete previous char |
| `Tab` | slect next completion item | | `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `BackTab` | slect previous completion item | | `Ctrl-p`, `Up` | Select previous history |
| `Ctrl-n`, `Down` | Select next history |
| `Tab` | Select next completion item |
| `BackTab` | Select previous completion item |
| `Enter` | Open selected | | `Enter` | Open selected |

@ -0,0 +1,14 @@
# Languages
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.
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:
```
# in <config_dir>/helix/languages.toml
[[language]]
name = "rust"
auto-format = false
```

@ -15,6 +15,7 @@ a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line "C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
g = { a = "code_action" } # Maps `ga` to show possible code actions g = { a = "code_action" } # Maps `ga` to show possible code actions
"ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode
[keys.insert] [keys.insert]
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode "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
| Left | `"left"` | | Left | `"left"` |
| Right | `"right"` | | Right | `"right"` |
| Up | `"up"` | | Up | `"up"` |
| Down | `"down"` |
| Home | `"home"` | | Home | `"home"` |
| End | `"end"` | | End | `"end"` |
| Page | `"pageup"` | | Page | `"pageup"` |

@ -62,6 +62,7 @@ Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
| Key after `mi` or `ma` | Textobject selected | | Key after `mi` or `ma` | Textobject selected |
| --- | --- | | --- | --- |
| `w` | Word | | `w` | Word |
| `W` | WORD |
| `(`, `[`, `'`, etc | Specified surround pairs | | `(`, `[`, `'`, etc | Specified surround pairs |
| `f` | Function | | `f` | Function |
| `c` | Class | | `c` | Class |

@ -1,4 +1,4 @@
use crate::{ChangeSet, Rope, State, Transaction}; use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction};
use once_cell::sync::Lazy; use once_cell::sync::Lazy;
use regex::Regex; use regex::Regex;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
@ -133,6 +133,32 @@ impl History {
Some(&self.revisions[last_child.get()].transaction) Some(&self.revisions[last_child.get()].transaction)
} }
// Get the position of last change
pub fn last_edit_pos(&self) -> Option<usize> {
if self.current == 0 {
return None;
}
let current_revision = &self.revisions[self.current];
let primary_selection = current_revision
.inversion
.selection()
.expect("inversion always contains a selection")
.primary();
let (_from, to, _fragment) = current_revision
.transaction
.changes_iter()
// find a change that matches the primary selection
.find(|(from, to, _fragment)| Range::new(*from, *to).overlaps(&primary_selection))
// or use the first change
.or_else(|| current_revision.transaction.changes_iter().next())
.unwrap();
let pos = current_revision
.transaction
.changes()
.map_pos(to, Assoc::After);
Some(pos)
}
fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize { fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
use std::collections::HashSet; use std::collections::HashSet;
let mut a_path_set = HashSet::new(); let mut a_path_set = HashSet::new();
@ -256,7 +282,7 @@ impl History {
} }
/// Whether to undo by a number of edits or a duration of time. /// Whether to undo by a number of edits or a duration of time.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq, Clone, Copy)]
pub enum UndoKind { pub enum UndoKind {
Steps(usize), Steps(usize),
TimePeriod(std::time::Duration), TimePeriod(std::time::Duration),

@ -450,6 +450,7 @@ where
language: vec![LanguageConfiguration { language: vec![LanguageConfiguration {
scope: "source.rust".to_string(), scope: "source.rust".to_string(),
file_types: vec!["rs".to_string()], file_types: vec!["rs".to_string()],
shebangs: vec![],
language_id: "Rust".to_string(), language_id: "Rust".to_string(),
highlight_config: OnceCell::new(), highlight_config: OnceCell::new(),
config: None, config: None,

@ -10,6 +10,7 @@ pub mod line_ending;
pub mod macros; pub mod macros;
pub mod match_brackets; pub mod match_brackets;
pub mod movement; pub mod movement;
pub mod numbers;
pub mod object; pub mod object;
pub mod path; pub mod path;
mod position; mod position;

@ -1,6 +1,13 @@
use crate::{Rope, Syntax}; use crate::{Rope, Syntax};
const PAIRS: &[(char, char)] = &[('(', ')'), ('{', '}'), ('[', ']'), ('<', '>')]; const PAIRS: &[(char, char)] = &[
('(', ')'),
('{', '}'),
('[', ']'),
('<', '>'),
('\'', '\''),
('"', '"'),
];
// limit matching pairs to only ( ) { } [ ] < > // limit matching pairs to only ( ) { } [ ] < >
#[must_use] #[must_use]

@ -168,7 +168,7 @@ pub fn backwards_skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<u
where where
F: Fn(char) -> bool, F: Fn(char) -> bool,
{ {
let mut chars_starting_from_next = slice.chars_at(pos + 1); let mut chars_starting_from_next = slice.chars_at(pos);
let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate(); let mut backwards = iter::from_fn(|| chars_starting_from_next.prev()).enumerate();
backwards.find_map(|(i, c)| { backwards.find_map(|(i, c)| {
if !fun(c) { if !fun(c) {

@ -0,0 +1,499 @@
use std::borrow::Cow;
use ropey::RopeSlice;
use crate::{
textobject::{textobject_word, TextObject},
Range, Tendril,
};
#[derive(Debug, PartialEq, Eq)]
pub struct NumberIncrementor<'a> {
pub range: Range,
pub value: i64,
pub radix: u32,
text: RopeSlice<'a>,
}
impl<'a> NumberIncrementor<'a> {
/// Return information about number under rang if there is one.
pub fn from_range(text: RopeSlice, range: Range) -> Option<NumberIncrementor> {
// If the cursor is on the minus sign of a number we want to get the word textobject to the
// right of it.
let range = if range.to() < text.len_chars()
&& range.to() - range.from() <= 1
&& text.char(range.from()) == '-'
{
Range::new(range.from() + 1, range.to() + 1)
} else {
range
};
let range = textobject_word(text, range, TextObject::Inside, 1, false);
// If there is a minus sign to the left of the word object, we want to include it in the range.
let range = if range.from() > 0 && text.char(range.from() - 1) == '-' {
range.extend(range.from() - 1, range.from())
} else {
range
};
let word: String = text
.slice(range.from()..range.to())
.chars()
.filter(|&c| c != '_')
.collect();
let (radix, prefixed) = if word.starts_with("0x") {
(16, true)
} else if word.starts_with("0o") {
(8, true)
} else if word.starts_with("0b") {
(2, true)
} else {
(10, false)
};
let number = if prefixed { &word[2..] } else { &word };
let value = i128::from_str_radix(number, radix).ok()?;
if (value.is_positive() && value.leading_zeros() < 64)
|| (value.is_negative() && value.leading_ones() < 64)
{
return None;
}
let value = value as i64;
Some(NumberIncrementor {
range,
value,
radix,
text,
})
}
/// Add `amount` to the number and return the formatted text.
pub fn incremented_text(&self, amount: i64) -> Tendril {
let old_text: Cow<str> = self.text.slice(self.range.from()..self.range.to()).into();
let old_length = old_text.len();
let new_value = self.value.wrapping_add(amount);
// Get separator indexes from right to left.
let separator_rtl_indexes: Vec<usize> = old_text
.chars()
.rev()
.enumerate()
.filter_map(|(i, c)| if c == '_' { Some(i) } else { None })
.collect();
let format_length = if self.radix == 10 {
match (self.value.is_negative(), new_value.is_negative()) {
(true, false) => old_length - 1,
(false, true) => old_length + 1,
_ => old_text.len(),
}
} else {
old_text.len() - 2
} - separator_rtl_indexes.len();
let mut new_text = match self.radix {
2 => format!("0b{:01$b}", new_value, format_length),
8 => format!("0o{:01$o}", new_value, format_length),
10 if old_text.starts_with('0') || old_text.starts_with("-0") => {
format!("{:01$}", new_value, format_length)
}
10 => format!("{}", new_value),
16 => {
let (lower_count, upper_count): (usize, usize) =
old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| {
(
lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0),
upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0),
)
});
if upper_count > lower_count {
format!("0x{:01$X}", new_value, format_length)
} else {
format!("0x{:01$x}", new_value, format_length)
}
}
_ => unimplemented!("radix not supported: {}", self.radix),
};
// Add separators from original number.
for &rtl_index in &separator_rtl_indexes {
if rtl_index < new_text.len() {
let new_index = new_text.len() - rtl_index;
new_text.insert(new_index, '_');
}
}
// Add in additional separators if necessary.
if new_text.len() > old_length && !separator_rtl_indexes.is_empty() {
let spacing = match separator_rtl_indexes.as_slice() {
[.., b, a] => a - b - 1,
_ => separator_rtl_indexes[0],
};
let prefix_length = if self.radix == 10 { 0 } else { 2 };
if let Some(mut index) = new_text.find('_') {
while index - prefix_length > spacing {
index -= spacing;
new_text.insert(index, '_');
}
}
}
new_text.into()
}
}
#[cfg(test)]
mod test {
use super::*;
use crate::Rope;
#[test]
fn test_decimal_at_point() {
let rope = Rope::from_str("Test text 12345 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 15),
value: 12345,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_uppercase_hexadecimal_at_point() {
let rope = Rope::from_str("Test text 0x123ABCDEF more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 21),
value: 0x123ABCDEF,
radix: 16,
text: rope.slice(..),
})
);
}
#[test]
fn test_lowercase_hexadecimal_at_point() {
let rope = Rope::from_str("Test text 0xfa3b4e more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 18),
value: 0xfa3b4e,
radix: 16,
text: rope.slice(..),
})
);
}
#[test]
fn test_octal_at_point() {
let rope = Rope::from_str("Test text 0o1074312 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 19),
value: 0o1074312,
radix: 8,
text: rope.slice(..),
})
);
}
#[test]
fn test_binary_at_point() {
let rope = Rope::from_str("Test text 0b10111010010101 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 26),
value: 0b10111010010101,
radix: 2,
text: rope.slice(..),
})
);
}
#[test]
fn test_negative_decimal_at_point() {
let rope = Rope::from_str("Test text -54321 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 16),
value: -54321,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_decimal_with_leading_zeroes_at_point() {
let rope = Rope::from_str("Test text 000045326 more text.");
let range = Range::point(12);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 19),
value: 45326,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_negative_decimal_cursor_on_minus_sign() {
let rope = Rope::from_str("Test text -54321 more text.");
let range = Range::point(10);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(10, 16),
value: -54321,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_under_range_start_of_rope() {
let rope = Rope::from_str("100");
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(0, 3),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_under_range_end_of_rope() {
let rope = Rope::from_str("100");
let range = Range::point(2);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(0, 3),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_number_surrounded_by_punctuation() {
let rope = Rope::from_str(",100;");
let range = Range::point(1);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range),
Some(NumberIncrementor {
range: Range::new(1, 4),
value: 100,
radix: 10,
text: rope.slice(..),
})
);
}
#[test]
fn test_not_a_number_point() {
let rope = Rope::from_str("Test text 45326 more text.");
let range = Range::point(6);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_too_large_at_point() {
let rope = Rope::from_str("Test text 0xFFFFFFFFFFFFFFFFF more text.");
let range = Range::point(12);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_cursor_one_right_of_number() {
let rope = Rope::from_str("100 ");
let range = Range::point(3);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_number_cursor_one_left_of_number() {
let rope = Rope::from_str(" 100");
let range = Range::point(0);
assert_eq!(NumberIncrementor::from_range(rope.slice(..), range), None);
}
#[test]
fn test_increment_basic_decimal_numbers() {
let tests = [
("100", 1, "101"),
("100", -1, "99"),
("99", 1, "100"),
("100", 1000, "1100"),
("100", -1000, "-900"),
("-1", 1, "0"),
("-1", 2, "1"),
("1", -1, "0"),
("1", -2, "-1"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
expected.into()
);
}
}
#[test]
fn test_increment_basic_hexadedimal_numbers() {
let tests = [
("0x0100", 1, "0x0101"),
("0x0100", -1, "0x00ff"),
("0x0001", -1, "0x0000"),
("0x0000", -1, "0xffffffffffffffff"),
("0xffffffffffffffff", 1, "0x0000000000000000"),
("0xffffffffffffffff", 2, "0x0000000000000001"),
("0xffffffffffffffff", -1, "0xfffffffffffffffe"),
("0xABCDEF1234567890", 1, "0xABCDEF1234567891"),
("0xabcdef1234567890", 1, "0xabcdef1234567891"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
expected.into()
);
}
}
#[test]
fn test_increment_basic_octal_numbers() {
let tests = [
("0o0107", 1, "0o0110"),
("0o0110", -1, "0o0107"),
("0o0001", -1, "0o0000"),
("0o7777", 1, "0o10000"),
("0o1000", -1, "0o0777"),
("0o0107", 10, "0o0121"),
("0o0000", -1, "0o1777777777777777777777"),
("0o1777777777777777777777", 1, "0o0000000000000000000000"),
("0o1777777777777777777777", 2, "0o0000000000000000000001"),
("0o1777777777777777777777", -1, "0o1777777777777777777776"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
expected.into()
);
}
}
#[test]
fn test_increment_basic_binary_numbers() {
let tests = [
("0b00000100", 1, "0b00000101"),
("0b00000100", -1, "0b00000011"),
("0b00000100", 2, "0b00000110"),
("0b00000100", -2, "0b00000010"),
("0b00000001", -1, "0b00000000"),
("0b00111111", 10, "0b01001001"),
("0b11111111", 1, "0b100000000"),
("0b10000000", -1, "0b01111111"),
(
"0b0000",
-1,
"0b1111111111111111111111111111111111111111111111111111111111111111",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
1,
"0b0000000000000000000000000000000000000000000000000000000000000000",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
2,
"0b0000000000000000000000000000000000000000000000000000000000000001",
),
(
"0b1111111111111111111111111111111111111111111111111111111111111111",
-1,
"0b1111111111111111111111111111111111111111111111111111111111111110",
),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
expected.into()
);
}
}
#[test]
fn test_increment_with_separators() {
let tests = [
("999_999", 1, "1_000_000"),
("1_000_000", -1, "999_999"),
("-999_999", -1, "-1_000_000"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000_0001", 0x1_ffff_0000, "0x0001_ffff_0001"),
("0x0000_0000", -1, "0xffff_ffff_ffff_ffff"),
("0x0000_0000_0000", -1, "0xffff_ffff_ffff_ffff"),
("0b01111111_11111111", 1, "0b10000000_00000000"),
("0b11111111_11111111", 1, "0b1_00000000_00000000"),
];
for (original, amount, expected) in tests {
let rope = Rope::from_str(original);
let range = Range::point(0);
assert_eq!(
NumberIncrementor::from_range(rope.slice(..), range)
.unwrap()
.incremented_text(amount),
expected.into()
);
}
}
}

@ -40,7 +40,6 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
/// needs to improve on. /// needs to improve on.
/// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81> /// Copied from cargo: <https://github.com/rust-lang/cargo/blob/070e459c2d8b79c5b2ac5218064e7603329c92ae/crates/cargo-util/src/paths.rs#L81>
pub fn get_normalized_path(path: &Path) -> PathBuf { pub fn get_normalized_path(path: &Path) -> PathBuf {
let path = expand_tilde(path);
let mut components = path.components().peekable(); let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() { let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
components.next(); components.next();
@ -72,10 +71,11 @@ pub fn get_normalized_path(path: &Path) -> PathBuf {
/// This function is used instead of `std::fs::canonicalize` because we don't want to verify /// This function is used instead of `std::fs::canonicalize` because we don't want to verify
/// here if the path exists, just normalize it's components. /// here if the path exists, just normalize it's components.
pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> { pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
let path = expand_tilde(path);
let path = if path.is_relative() { let path = if path.is_relative() {
std::env::current_dir().map(|current_dir| current_dir.join(path))? std::env::current_dir().map(|current_dir| current_dir.join(path))?
} else { } else {
path.to_path_buf() path
}; };
Ok(get_normalized_path(path.as_path())) Ok(get_normalized_path(path.as_path()))

@ -360,7 +360,7 @@ impl Selection {
self.normalize() self.normalize()
} }
/// Adds a new range to the selection and makes it the primary range. /// Removes a range from the selection.
pub fn remove(mut self, index: usize) -> Self { pub fn remove(mut self, index: usize) -> Self {
assert!( assert!(
self.ranges.len() > 1, self.ranges.len() > 1,
@ -528,14 +528,15 @@ impl<'a> IntoIterator for &'a Selection {
// TODO: checkSelection -> check if valid for doc length && sorted // TODO: checkSelection -> check if valid for doc length && sorted
pub fn keep_matches( pub fn keep_or_remove_matches(
text: RopeSlice, text: RopeSlice,
selection: &Selection, selection: &Selection,
regex: &crate::regex::Regex, regex: &crate::regex::Regex,
remove: bool,
) -> Option<Selection> { ) -> Option<Selection> {
let result: SmallVec<_> = selection let result: SmallVec<_> = selection
.iter() .iter()
.filter(|range| regex.is_match(&range.fragment(text))) .filter(|range| regex.is_match(&range.fragment(text)) ^ remove)
.copied() .copied()
.collect(); .collect();

@ -40,18 +40,21 @@ where
} }
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Configuration { pub struct Configuration {
pub language: Vec<LanguageConfiguration>, pub language: Vec<LanguageConfiguration>,
} }
// largely based on tree-sitter/cli/src/loader.rs // largely based on tree-sitter/cli/src/loader.rs
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration { pub struct LanguageConfiguration {
#[serde(rename = "name")] #[serde(rename = "name")]
pub language_id: String, pub language_id: String,
pub scope: String, // source.rust pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc> pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
#[serde(default)]
pub shebangs: Vec<String>, // interpreter(s) associated with language
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml> pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
pub comment_token: Option<String>, pub comment_token: Option<String>,
@ -309,6 +312,7 @@ pub struct Loader {
// highlight_names ? // highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>, language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize> language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_shebang: HashMap<String, usize>,
} }
impl Loader { impl Loader {
@ -316,6 +320,7 @@ impl Loader {
let mut loader = Self { let mut loader = Self {
language_configs: Vec::new(), language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(), language_config_ids_by_file_type: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
}; };
for config in config.language { for config in config.language {
@ -328,6 +333,11 @@ impl Loader {
.language_config_ids_by_file_type .language_config_ids_by_file_type
.insert(file_type.clone(), language_id); .insert(file_type.clone(), language_id);
} }
for shebang in &config.shebangs {
loader
.language_config_ids_by_shebang
.insert(shebang.clone(), language_id);
}
loader.language_configs.push(Arc::new(config)); loader.language_configs.push(Arc::new(config));
} }
@ -353,6 +363,17 @@ impl Loader {
// TODO: content_regex handling conflict resolution // TODO: content_regex handling conflict resolution
} }
pub fn language_config_for_shebang(&self, source: &Rope) -> Option<Arc<LanguageConfiguration>> {
let line = Cow::from(source.line(0));
static SHEBANG_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^#!\s*(?:\S*[/\\](?:env\s+)?)?([^\s\.\d]+)").unwrap());
let configuration_id = SHEBANG_REGEX
.captures(&line)
.and_then(|cap| self.language_config_ids_by_shebang.get(&cap[1]));
configuration_id.and_then(|&id| self.language_configs.get(id).cloned())
}
pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> { pub fn language_config_for_scope(&self, scope: &str) -> Option<Arc<LanguageConfiguration>> {
self.language_configs self.language_configs
.iter() .iter()

@ -10,7 +10,7 @@ use crate::surround;
use crate::syntax::LanguageConfiguration; use crate::syntax::LanguageConfiguration;
use crate::Range; use crate::Range;
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
use CharCategory::{Eol, Whitespace}; use CharCategory::{Eol, Whitespace};
let iter = match direction { let iter = match direction {
@ -33,7 +33,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) ->
match categorize_char(ch) { match categorize_char(ch) {
Eol | Whitespace => return pos, Eol | Whitespace => return pos,
category => { category => {
if category != prev_category && pos != 0 && pos != slice.len_chars() { if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
return pos; return pos;
} else { } else {
match direction { match direction {
@ -70,13 +70,14 @@ pub fn textobject_word(
range: Range, range: Range,
textobject: TextObject, textobject: TextObject,
_count: usize, _count: usize,
long: bool,
) -> Range { ) -> Range {
let pos = range.cursor(slice); let pos = range.cursor(slice);
let word_start = find_word_boundary(slice, pos, Direction::Backward); let word_start = find_word_boundary(slice, pos, Direction::Backward, long);
let word_end = match slice.get_char(pos).map(categorize_char) { let word_end = match slice.get_char(pos).map(categorize_char) {
None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos, None | Some(CharCategory::Whitespace | CharCategory::Eol) => pos,
_ => find_word_boundary(slice, pos + 1, Direction::Forward), _ => find_word_boundary(slice, pos + 1, Direction::Forward, long),
}; };
// Special case. // Special case.
@ -268,7 +269,7 @@ mod test {
let slice = doc.slice(..); let slice = doc.slice(..);
for &case in scenario { for &case in scenario {
let (pos, objtype, expected_range) = case; let (pos, objtype, expected_range) = case;
let result = textobject_word(slice, Range::point(pos), objtype, 1); let result = textobject_word(slice, Range::point(pos), objtype, 1, false);
assert_eq!( assert_eq!(
result, result,
expected_range.into(), expected_range.into(),

@ -257,6 +257,12 @@ impl Client {
content_format: Some(vec![lsp::MarkupKind::Markdown]), content_format: Some(vec![lsp::MarkupKind::Markdown]),
..Default::default() ..Default::default()
}), }),
rename: Some(lsp::RenameClientCapabilities {
dynamic_registration: Some(false),
prepare_support: Some(false),
prepare_support_default_behavior: None,
honors_change_annotations: Some(false),
}),
code_action: Some(lsp::CodeActionClientCapabilities { code_action: Some(lsp::CodeActionClientCapabilities {
code_action_literal_support: Some(lsp::CodeActionLiteralSupport { code_action_literal_support: Some(lsp::CodeActionLiteralSupport {
code_action_kind: lsp::CodeActionKindLiteralSupport { code_action_kind: lsp::CodeActionKindLiteralSupport {
@ -773,4 +779,25 @@ impl Client {
self.call::<lsp::request::CodeActionRequest>(params) self.call::<lsp::request::CodeActionRequest>(params)
} }
pub async fn rename_symbol(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
new_name: String,
) -> anyhow::Result<lsp::WorkspaceEdit> {
let params = lsp::RenameParams {
text_document_position: lsp::TextDocumentPositionParams {
text_document,
position,
},
new_name,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
};
let response = self.request::<lsp::request::Rename>(params).await?;
Ok(response.unwrap_or_default())
}
} }

@ -0,0 +1 @@
Subproject commit 88408ffc5e27abcffced7010fc77396ae3636d7e

@ -0,0 +1 @@
Subproject commit 0ac2c6da562c7a2c26ed7e8691d4a590f7e8b90a

@ -0,0 +1,12 @@
use std::process::Command;
fn main() {
let git_hash = Command::new("git")
.args(&["describe", "--dirty"])
.output()
.map(|x| String::from_utf8(x.stdout).ok())
.ok()
.flatten()
.unwrap_or_else(|| String::from(env!("CARGO_PKG_VERSION")));
println!("cargo:rustc-env=VERSION_AND_GIT_HASH={}", git_hash);
}

@ -9,7 +9,7 @@ use crate::{
use log::{error, warn}; use log::{error, warn};
use std::{ use std::{
io::{stdout, Write}, io::{stdin, stdout, Write},
sync::Arc, sync::Arc,
time::{Duration, Instant}, time::{Duration, Instant},
}; };
@ -19,6 +19,7 @@ use anyhow::Error;
use crossterm::{ use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream}, event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
execute, terminal, execute, terminal,
tty::IsTty,
}; };
#[cfg(not(windows))] #[cfg(not(windows))]
use { use {
@ -62,14 +63,19 @@ impl Application {
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir())); std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
// load default and user config, and merge both // load default and user config, and merge both
let def_lang_conf: toml::Value = toml::from_slice(include_bytes!("../../languages.toml")) let builtin_err_msg =
.expect("Could not parse built-in languages.toml, something must be very wrong"); "Could not parse built-in languages.toml, something must be very wrong";
let user_lang_conf: Option<toml::Value> = std::fs::read(conf_dir.join("languages.toml")) let def_lang_conf: toml::Value =
toml::from_slice(include_bytes!("../../languages.toml")).expect(builtin_err_msg);
let def_syn_loader_conf: helix_core::syntax::Configuration =
def_lang_conf.clone().try_into().expect(builtin_err_msg);
let user_lang_conf = std::fs::read(conf_dir.join("languages.toml"))
.ok() .ok()
.map(|raw| toml::from_slice(&raw).expect("Could not parse user languages.toml")); .map(|raw| toml::from_slice(&raw));
let lang_conf = match user_lang_conf { let lang_conf = match user_lang_conf {
Some(value) => merge_toml_values(def_lang_conf, value), Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
None => def_lang_conf, Some(err @ Err(_)) => err,
None => Ok(def_lang_conf),
}; };
let theme = if let Some(theme) = &config.theme { let theme = if let Some(theme) = &config.theme {
@ -85,8 +91,15 @@ impl Application {
}; };
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.try_into() .and_then(|conf| conf.try_into())
.expect("Could not parse merged (built-in + user) languages.toml"); .unwrap_or_else(|err| {
eprintln!("Bad language config: {}", err);
eprintln!("Press <ENTER> to continue with default language config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
def_syn_loader_conf
});
let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf)); let syn_loader = std::sync::Arc::new(syntax::Loader::new(syn_loader_conf));
let mut editor = Editor::new( let mut editor = Editor::new(
@ -124,8 +137,17 @@ impl Application {
} }
editor.set_status(format!("Loaded {} files.", nr_of_files)); editor.set_status(format!("Loaded {} files.", nr_of_files));
} }
} else { } else if stdin().is_tty() {
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
} else if cfg!(target_os = "macos") {
// On Linux and Windows, we allow the output of a command to be piped into the new buffer.
// This doesn't currently work on macOS because of the following issue:
// https://github.com/crossterm-rs/crossterm/issues/500
anyhow::bail!("Piping into helix-term is currently not supported on macOS");
} else {
editor
.new_file_from_stdin(Action::VerticalSplit)
.unwrap_or_else(|_| editor.new_file(Action::VerticalSplit));
} }
editor.set_theme(theme); editor.set_theme(theme);
@ -253,12 +275,8 @@ impl Application {
} }
let editor_view = self let editor_view = self
.compositor .compositor
.find(std::any::type_name::<ui::EditorView>()) .find::<ui::EditorView>()
.expect("expected at least one EditorView"); .expect("expected at least one EditorView");
let editor_view = editor_view
.as_any_mut()
.downcast_mut::<ui::EditorView>()
.unwrap();
if editor_view.completion.is_some() { if editor_view.completion.is_some() {
return; return;
@ -583,12 +601,8 @@ impl Application {
{ {
let editor_view = self let editor_view = self
.compositor .compositor
.find(std::any::type_name::<ui::EditorView>()) .find::<ui::EditorView>()
.expect("expected at least one EditorView"); .expect("expected at least one EditorView");
let editor_view = editor_view
.as_any_mut()
.downcast_mut::<ui::EditorView>()
.unwrap();
let lsp::ProgressParams { token, value } = params; let lsp::ProgressParams { token, value } = params;
let lsp::ProgressParamsValue::WorkDone(work) = value; let lsp::ProgressParamsValue::WorkDone(work) = value;
@ -702,12 +716,8 @@ impl Application {
let editor_view = self let editor_view = self
.compositor .compositor
.find(std::any::type_name::<ui::EditorView>()) .find::<ui::EditorView>()
.expect("expected at least one EditorView"); .expect("expected at least one EditorView");
let editor_view = editor_view
.as_any_mut()
.downcast_mut::<ui::EditorView>()
.unwrap();
let spinner = editor_view.spinners_mut().get_or_create(server_id); let spinner = editor_view.spinners_mut().get_or_create(server_id);
if spinner.is_stopped() { if spinner.is_stopped() {
spinner.start(); spinner.start();
@ -742,7 +752,7 @@ impl Application {
Ok(()) Ok(())
} }
pub async fn run(&mut self) -> Result<(), Error> { pub async fn run(&mut self) -> Result<i32, Error> {
self.claim_term().await?; self.claim_term().await?;
// Exit the alternate screen and disable raw mode before panicking // Exit the alternate screen and disable raw mode before panicking
@ -765,6 +775,6 @@ impl Application {
self.restore_term()?; self.restore_term()?;
Ok(()) Ok(self.editor.exit_code)
} }
} }

File diff suppressed because it is too large Load Diff

@ -177,11 +177,12 @@ impl Compositor {
.any(|component| component.type_name() == type_name) .any(|component| component.type_name() == type_name)
} }
pub fn find(&mut self, type_name: &str) -> Option<&mut dyn Component> { pub fn find<T: 'static>(&mut self) -> Option<&mut T> {
let type_name = std::any::type_name::<T>();
self.layers self.layers
.iter_mut() .iter_mut()
.find(|component| component.type_name() == type_name) .find(|component| component.type_name() == type_name)
.map(|component| component.as_mut()) .and_then(|component| component.as_any_mut().downcast_mut())
} }
} }

@ -3,6 +3,7 @@ use serde::Deserialize;
use crate::keymap::Keymaps; use crate::keymap::Keymaps;
#[derive(Debug, Default, Clone, PartialEq, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Config { pub struct Config {
pub theme: Option<String>, pub theme: Option<String>,
#[serde(default)] #[serde(default)]
@ -14,7 +15,7 @@ pub struct Config {
} }
#[derive(Debug, Default, Clone, PartialEq, Deserialize)] #[derive(Debug, Default, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LspConfig { pub struct LspConfig {
pub display_messages: bool, pub display_messages: bool,
} }

@ -25,6 +25,54 @@ macro_rules! key {
}; };
} }
#[macro_export]
macro_rules! shift {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::SHIFT,
}
};
}
#[macro_export]
macro_rules! ctrl {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::CONTROL,
}
};
}
#[macro_export]
macro_rules! alt {
($key:ident) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
}
};
($($ch:tt)*) => {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::ALT,
}
};
}
/// Macro for defining the root of a `Keymap` object. Example: /// Macro for defining the root of a `Keymap` object. Example:
/// ///
/// ``` /// ```
@ -53,6 +101,10 @@ macro_rules! keymap {
keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ }) keymap!({ $label $(sticky=$sticky)? $($($key)|+ => $value,)+ })
}; };
(@trie [$($cmd:ident),* $(,)?]) => {
$crate::keymap::KeyTrie::Sequence(vec![$($crate::commands::Command::$cmd),*])
};
( (
{ $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ } { $label:literal $(sticky=$sticky:literal)? $($($key:literal)|+ => $value:tt,)+ }
) => { ) => {
@ -64,10 +116,11 @@ macro_rules! keymap {
$( $(
$( $(
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap(); let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
_map.insert( let _duplicate = _map.insert(
_key, _key,
keymap!(@trie $value) keymap!(@trie $value)
); );
debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key); _order.push(_key);
)+ )+
)* )*
@ -147,6 +200,7 @@ impl KeyTrieNode {
cmd.doc() cmd.doc()
} }
KeyTrie::Node(n) => n.name(), KeyTrie::Node(n) => n.name(),
KeyTrie::Sequence(_) => "[Multiple commands]",
}; };
match body.iter().position(|(d, _)| d == &desc) { match body.iter().position(|(d, _)| d == &desc) {
Some(pos) => { Some(pos) => {
@ -207,6 +261,7 @@ impl DerefMut for KeyTrieNode {
#[serde(untagged)] #[serde(untagged)]
pub enum KeyTrie { pub enum KeyTrie {
Leaf(Command), Leaf(Command),
Sequence(Vec<Command>),
Node(KeyTrieNode), Node(KeyTrieNode),
} }
@ -214,14 +269,14 @@ impl KeyTrie {
pub fn node(&self) -> Option<&KeyTrieNode> { pub fn node(&self) -> Option<&KeyTrieNode> {
match *self { match *self {
KeyTrie::Node(ref node) => Some(node), KeyTrie::Node(ref node) => Some(node),
KeyTrie::Leaf(_) => None, KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
} }
} }
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> { pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
match *self { match *self {
KeyTrie::Node(ref mut node) => Some(node), KeyTrie::Node(ref mut node) => Some(node),
KeyTrie::Leaf(_) => None, KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
} }
} }
@ -238,7 +293,7 @@ impl KeyTrie {
trie = match trie { trie = match trie {
KeyTrie::Node(map) => map.get(key), KeyTrie::Node(map) => map.get(key),
// leaf encountered while keys left to process // leaf encountered while keys left to process
KeyTrie::Leaf(_) => None, KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
}? }?
} }
Some(trie) Some(trie)
@ -250,6 +305,8 @@ pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke. /// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode), Pending(KeyTrieNode),
Matched(Command), Matched(Command),
/// Matched a sequence of commands to execute.
MatchedSequence(Vec<Command>),
/// Key was not found in the root keymap /// Key was not found in the root keymap
NotFound, NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto /// Key is invalid in combination with previous keys. Contains keys leading upto
@ -332,6 +389,12 @@ impl Keymap {
Some(&KeyTrie::Leaf(cmd)) => { Some(&KeyTrie::Leaf(cmd)) => {
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()) return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky())
} }
Some(&KeyTrie::Sequence(ref cmds)) => {
return KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
)
}
None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()), None => return KeymapResult::new(KeymapResultKind::NotFound, self.sticky()),
Some(t) => t, Some(t) => t,
}; };
@ -349,6 +412,13 @@ impl Keymap {
self.state.clear(); self.state.clear();
return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky()); return KeymapResult::new(KeymapResultKind::Matched(cmd), self.sticky());
} }
Some(&KeyTrie::Sequence(ref cmds)) => {
self.state.clear();
KeymapResult::new(
KeymapResultKind::MatchedSequence(cmds.clone()),
self.sticky(),
)
}
None => KeymapResult::new( None => KeymapResult::new(
KeymapResultKind::Cancelled(self.state.drain(..).collect()), KeymapResultKind::Cancelled(self.state.drain(..).collect()),
self.sticky(), self.sticky(),
@ -455,6 +525,7 @@ impl Default for Keymaps {
"a" => goto_last_accessed_file, "a" => goto_last_accessed_file,
"n" => goto_next_buffer, "n" => goto_next_buffer,
"p" => goto_previous_buffer, "p" => goto_previous_buffer,
"." => goto_last_modification,
}, },
":" => command_mode, ":" => command_mode,
@ -511,6 +582,8 @@ impl Default for Keymaps {
"u" => undo, "u" => undo,
"U" => redo, "U" => redo,
"A-u" => earlier,
"A-U" => later,
"y" => yank, "y" => yank,
// yank_all // yank_all
@ -523,7 +596,7 @@ impl Default for Keymaps {
"=" => format_selections, "=" => format_selections,
"J" => join_selections, "J" => join_selections,
"K" => keep_selections, "K" => keep_selections,
// TODO: and another method for inverse "A-K" => remove_selections,
"," => keep_primary_selection, "," => keep_primary_selection,
"A-," => remove_primary_selection, "A-," => remove_primary_selection,
@ -532,7 +605,7 @@ impl Default for Keymaps {
// "Q" => replay_macro, // "Q" => replay_macro,
// & align selections // & align selections
// _ trim selections "_" => trim_selections,
"(" => rotate_selections_backward, "(" => rotate_selections_backward,
")" => rotate_selections_forward, ")" => rotate_selections_forward,
@ -550,6 +623,7 @@ impl Default for Keymaps {
"C-s" | "s" => hsplit, "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit, "C-v" | "v" => vsplit,
"C-q" | "q" => wclose, "C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left, "C-h" | "h" | "left" => jump_view_left,
"C-j" | "j" | "down" => jump_view_down, "C-j" | "j" | "down" => jump_view_down,
"C-k" | "k" | "up" => jump_view_up, "C-k" | "k" | "up" => jump_view_up,
@ -569,6 +643,7 @@ impl Default for Keymaps {
"f" => file_picker, "f" => file_picker,
"b" => buffer_picker, "b" => buffer_picker,
"s" => symbol_picker, "s" => symbol_picker,
"S" => workspace_symbol_picker,
"a" => code_action, "a" => code_action,
"'" => last_picker, "'" => last_picker,
"d" => { "Debug" sticky=true "d" => { "Debug" sticky=true
@ -593,9 +668,14 @@ impl Default for Keymaps {
}, },
"w" => { "Window" "w" => { "Window"
"C-w" | "w" => rotate_view, "C-w" | "w" => rotate_view,
"C-h" | "h" => hsplit, "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit, "C-v" | "v" => vsplit,
"C-q" | "q" => wclose, "C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
"C-j" | "j" | "down" => jump_view_down,
"C-k" | "k" | "up" => jump_view_up,
"C-l" | "l" | "right" => jump_view_right,
}, },
"y" => yank_joined_to_clipboard, "y" => yank_joined_to_clipboard,
"Y" => yank_main_selection_to_clipboard, "Y" => yank_main_selection_to_clipboard,
@ -604,30 +684,31 @@ impl Default for Keymaps {
"R" => replace_selections_with_clipboard, "R" => replace_selections_with_clipboard,
"/" => global_search, "/" => global_search,
"k" => hover, "k" => hover,
"r" => rename_symbol,
}, },
"z" => { "View" "z" => { "View"
"z" | "c" => align_view_center, "z" | "c" => align_view_center,
"t" => align_view_top, "t" => align_view_top,
"b" => align_view_bottom, "b" => align_view_bottom,
"m" => align_view_middle, "m" => align_view_middle,
"k" => scroll_up, "k" | "up" => scroll_up,
"j" => scroll_down, "j" | "down" => scroll_down,
"b" => page_up, "C-b" | "pageup" => page_up,
"f" => page_down, "C-f" | "pagedown" => page_down,
"u" => half_page_up, "C-u" => half_page_up,
"d" => half_page_down, "C-d" => half_page_down,
}, },
"Z" => { "View" sticky=true "Z" => { "View" sticky=true
"z" | "c" => align_view_center, "z" | "c" => align_view_center,
"t" => align_view_top, "t" => align_view_top,
"b" => align_view_bottom, "b" => align_view_bottom,
"m" => align_view_middle, "m" => align_view_middle,
"k" => scroll_up, "k" | "up" => scroll_up,
"j" => scroll_down, "j" | "down" => scroll_down,
"b" => page_up, "C-b" | "pageup" => page_up,
"f" => page_down, "C-f" | "pagedown" => page_down,
"u" => half_page_up, "C-u" => half_page_up,
"d" => half_page_down, "C-d" => half_page_down,
}, },
"\"" => select_register, "\"" => select_register,
@ -637,6 +718,9 @@ impl Default for Keymaps {
"A-!" => shell_append_output, "A-!" => shell_append_output,
"$" => shell_keep_pipe, "$" => shell_keep_pipe,
"C-z" => suspend, "C-z" => suspend,
"C-a" => increment,
"C-x" => decrement,
}); });
let mut select = normal.clone(); let mut select = normal.clone();
select.merge_nodes(keymap!({ "Select mode" select.merge_nodes(keymap!({ "Select mode"
@ -670,21 +754,38 @@ impl Default for Keymaps {
"esc" => normal_mode, "esc" => normal_mode,
"backspace" => delete_char_backward, "backspace" => delete_char_backward,
"C-h" => delete_char_backward,
"del" => delete_char_forward, "del" => delete_char_forward,
"C-d" => delete_char_forward,
"ret" => insert_newline, "ret" => insert_newline,
"tab" => insert_tab, "tab" => insert_tab,
"C-w" => delete_word_backward, "C-w" => delete_word_backward,
"A-d" => delete_word_forward,
"left" => move_char_left, "left" => move_char_left,
"C-b" => move_char_left,
"down" => move_line_down, "down" => move_line_down,
"C-n" => move_line_down,
"up" => move_line_up, "up" => move_line_up,
"C-p" => move_line_up,
"right" => move_char_right, "right" => move_char_right,
"C-f" => move_char_right,
"A-b" => move_prev_word_end,
"A-left" => move_prev_word_end,
"A-f" => move_next_word_start,
"A-right" => move_next_word_start,
"pageup" => page_up, "pageup" => page_up,
"pagedown" => page_down, "pagedown" => page_down,
"home" => goto_line_start, "home" => goto_line_start,
"C-a" => goto_line_start,
"end" => goto_line_end_newline, "end" => goto_line_end_newline,
"C-e" => goto_line_end_newline,
"C-k" => kill_to_line_end,
"C-u" => kill_to_line_start,
"C-x" => completion, "C-x" => completion,
"C-r" => insert_register,
}); });
Keymaps(hashmap!( Keymaps(hashmap!(
Mode::Normal => Keymap::new(normal), Mode::Normal => Keymap::new(normal),
@ -706,6 +807,22 @@ pub fn merge_keys(mut config: Config) -> Config {
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
#[test]
#[should_panic]
fn duplicate_keys_should_panic() {
keymap!({ "Normal mode"
"i" => normal_mode,
"i" => goto_definition,
});
}
#[test]
fn check_duplicate_keys_in_default_keymap() {
// will panic on duplicate keys, assumes that `Keymaps` uses keymap! macro
Keymaps::default();
}
#[test] #[test]
fn merge_partial_keys() { fn merge_partial_keys() {
let config = Config { let config = Config {
@ -800,4 +917,20 @@ mod tests {
let node = keymap.root().search(&[crate::key!(' ')]).unwrap(); let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty()) assert!(!node.node().unwrap().order().is_empty())
} }
#[test]
fn aliased_modes_are_same_in_default_keymap() {
let keymaps = Keymaps::default();
let root = keymaps.get(&Mode::Normal).unwrap().root();
assert_eq!(
root.search(&[key!(' '), key!('w')]).unwrap(),
root.search(&["C-w".parse::<KeyEvent>().unwrap()]).unwrap(),
"Mismatch for window mode on `Space-w` and `Ctrl-w`"
);
assert_eq!(
root.search(&[key!('z')]).unwrap(),
root.search(&[key!('Z')]).unwrap(),
"Mismatch for view mode on `z` and `Z`"
);
}
} }

@ -16,11 +16,6 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
}; };
// Separate file config so we can include year, month and day in file logs // Separate file config so we can include year, month and day in file logs
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(logpath)?;
let file_config = fern::Dispatch::new() let file_config = fern::Dispatch::new()
.format(|out, message, record| { .format(|out, message, record| {
out.finish(format_args!( out.finish(format_args!(
@ -31,15 +26,20 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message message
)) ))
}) })
.chain(file); .chain(fern::log_file(logpath)?);
base_config.chain(file_config).apply()?; base_config.chain(file_config).apply()?;
Ok(()) Ok(())
} }
fn main() -> Result<()> {
let exit_code = main_impl()?;
std::process::exit(exit_code);
}
#[tokio::main] #[tokio::main]
async fn main() -> Result<()> { async fn main_impl() -> Result<i32> {
let cache_dir = helix_core::cache_dir(); let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() { if !cache_dir.exists() {
std::fs::create_dir_all(&cache_dir).ok(); std::fs::create_dir_all(&cache_dir).ok();
@ -66,7 +66,7 @@ FLAGS:
-V, --version Prints version information -V, --version Prints version information
", ",
env!("CARGO_PKG_NAME"), env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"), env!("VERSION_AND_GIT_HASH"),
env!("CARGO_PKG_AUTHORS"), env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"), env!("CARGO_PKG_DESCRIPTION"),
logpath.display(), logpath.display(),
@ -81,7 +81,7 @@ FLAGS:
} }
if args.display_version { if args.display_version {
println!("helix {}", env!("CARGO_PKG_VERSION")); println!("helix {}", env!("VERSION_AND_GIT_HASH"));
std::process::exit(0); std::process::exit(0);
} }
@ -91,7 +91,16 @@ FLAGS:
} }
let config = match std::fs::read_to_string(conf_dir.join("config.toml")) { let config = match std::fs::read_to_string(conf_dir.join("config.toml")) {
Ok(config) => merge_keys(toml::from_str(&config)?), Ok(config) => toml::from_str(&config)
.map(merge_keys)
.unwrap_or_else(|err| {
eprintln!("Bad config: {}", err);
eprintln!("Press <ENTER> to continue with default config");
use std::io::Read;
// This waits for an enter press.
let _ = std::io::stdin().read(&mut []);
Config::default()
}),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(), Err(err) if err.kind() == std::io::ErrorKind::NotFound => Config::default(),
Err(err) => return Err(Error::new(err)), Err(err) => return Err(Error::new(err)),
}; };
@ -100,7 +109,8 @@ FLAGS:
// TODO: use the thread local executor to spawn the application task separately from the work pool // TODO: use the thread local executor to spawn the application task separately from the work pool
let mut app = Application::new(args, config).context("unable to create new application")?; let mut app = Application::new(args, config).context("unable to create new application")?;
app.run().await.unwrap();
Ok(()) let exit_code = app.run().await?;
Ok(exit_code)
} }

@ -17,7 +17,7 @@ use helix_core::{
}; };
use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame}; use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
use helix_view::{ use helix_view::{
document::Mode, document::{Mode, SCRATCH_BUFFER_NAME},
editor::LineNumber, editor::LineNumber,
graphics::{Color, CursorKind, Modifier, Rect, Style}, graphics::{Color, CursorKind, Modifier, Rect, Style},
info::Info, info::Info,
@ -610,7 +610,7 @@ impl EditorView {
use tui::{ use tui::{
layout::Alignment, layout::Alignment,
text::Text, text::Text,
widgets::{Paragraph, Widget}, widgets::{Paragraph, Widget, Wrap},
}; };
let cursor = doc let cursor = doc
@ -665,8 +665,10 @@ impl EditorView {
} }
} }
let paragraph = Paragraph::new(lines).alignment(Alignment::Right); let paragraph = Paragraph::new(lines)
let width = 80.min(viewport.width); .alignment(Alignment::Right)
.wrap(Wrap { trim: true });
let width = 100.min(viewport.width);
let height = 15.min(viewport.height); let height = 15.min(viewport.height);
paragraph.render( paragraph.render(
Rect::new(viewport.right() - width, viewport.y + 1, width, height), Rect::new(viewport.right() - width, viewport.y + 1, width, height),
@ -716,18 +718,20 @@ impl EditorView {
} }
surface.set_string(viewport.x + 5, viewport.y, progress, base_style); surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
if let Some(path) = doc.relative_path() { let rel_path = doc.relative_path();
let path = path.to_string_lossy(); let path = rel_path
.as_ref()
.map(|p| p.to_string_lossy())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into());
let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" }); let title = format!("{}{}", path, if doc.is_modified() { "[+]" } else { "" });
surface.set_stringn( surface.set_stringn(
viewport.x + 8, viewport.x + 8,
viewport.y, viewport.y,
title, title,
viewport.width.saturating_sub(6) as usize, viewport.width.saturating_sub(6) as usize,
base_style, base_style,
); );
}
//------------------------------- //-------------------------------
// Right side of the status line. // Right side of the status line.
@ -830,6 +834,11 @@ impl EditorView {
match &key_result.kind { match &key_result.kind {
KeymapResultKind::Matched(command) => command.execute(cxt), KeymapResultKind::Matched(command) => command.execute(cxt),
KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()), KeymapResultKind::Pending(node) => self.autoinfo = Some(node.infobox()),
KeymapResultKind::MatchedSequence(commands) => {
for command in commands {
command.execute(cxt);
}
}
KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result), KeymapResultKind::NotFound | KeymapResultKind::Cancelled(_) => return Some(key_result),
} }
None None
@ -871,7 +880,7 @@ impl EditorView {
std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i)); std::num::NonZeroUsize::new(cxt.editor.count.map_or(i, |c| c.get() * 10 + i));
} }
// special handling for repeat operator // special handling for repeat operator
key!('.') => { key!('.') if self.keymaps.pending().is_empty() => {
// first execute whatever put us into insert mode // first execute whatever put us into insert mode
self.last_insert.0.execute(cxt); self.last_insert.0.execute(cxt);
// then replay the inputs // then replay the inputs

@ -13,7 +13,7 @@ use helix_core::{
Rope, Rope,
}; };
use helix_view::{ use helix_view::{
graphics::{Color, Margin, Rect, Style}, graphics::{Margin, Rect},
Theme, Theme,
}; };
@ -61,9 +61,15 @@ fn parse<'a>(
}) })
} }
let text_style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
let code_style = Style::default().fg(Color::Rgb(255, 255, 255)); // white
let heading_style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac // TODO: use better scopes for these, `markup.raw.block`, `markup.heading`
let code_style = theme
.map(|theme| theme.get("ui.text.focus"))
.unwrap_or_default(); // white
let heading_style = theme
.map(|theme| theme.get("ui.linenr.selected"))
.unwrap_or_default(); // lilac
for event in parser { for event in parser {
match event { match event {

@ -1,5 +1,8 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::{
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; compositor::{Component, Compositor, Context, EventResult},
ctrl, key, shift,
};
use crossterm::event::Event;
use tui::{buffer::Buffer as Surface, widgets::Table}; use tui::{buffer::Buffer as Surface, widgets::Table};
pub use tui::widgets::{Cell, Row}; pub use tui::widgets::{Cell, Row};
@ -192,63 +195,25 @@ impl<T: Item + 'static> Component for Menu<T> {
compositor.pop(); compositor.pop();
}))); })));
match event { match event.into() {
// esc or ctrl-c aborts the completion and closes the menu // esc or ctrl-c aborts the completion and closes the menu
KeyEvent { key!(Esc) | ctrl!('c') => {
code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => {
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Abort);
return close_fn; return close_fn;
} }
// arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc)
KeyEvent { shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Up, ..
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_up(); self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None); return EventResult::Consumed(None);
} }
// arrow down/ctrl-n/tab advances completion choice (including updating the doc) key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
KeyEvent { // arrow down/ctrl-n/tab advances completion choice (including updating the doc)
code: KeyCode::Tab,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Down,
..
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_down(); self.move_down();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
return EventResult::Consumed(None); return EventResult::Consumed(None);
} }
KeyEvent { key!(Enter) => {
code: KeyCode::Enter,
..
} => {
if let Some(selection) = self.selection() { if let Some(selection) = self.selection() {
(self.callback_fn)(cx.editor, Some(selection), MenuEvent::Validate); (self.callback_fn)(cx.editor, Some(selection), MenuEvent::Validate);
} }

@ -35,6 +35,7 @@ pub fn regex_prompt(
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let view_id = view.id; let view_id = view.id;
let snapshot = doc.selection(view_id).clone(); let snapshot = doc.selection(view_id).clone();
let offset_snapshot = view.offset;
Prompt::new( Prompt::new(
prompt, prompt,
@ -45,6 +46,7 @@ pub fn regex_prompt(
PromptEvent::Abort => { PromptEvent::Abort => {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
doc.set_selection(view.id, snapshot.clone()); doc.set_selection(view.id, snapshot.clone());
view.offset = offset_snapshot;
} }
PromptEvent::Validate => { PromptEvent::Validate => {
// TODO: push_jump to store selection just before jump // TODO: push_jump to store selection just before jump

@ -1,8 +1,9 @@
use crate::{ use crate::{
compositor::{Component, Compositor, Context, EventResult}, compositor::{Component, Compositor, Context, EventResult},
ctrl, key, shift,
ui::EditorView, ui::EditorView,
}; };
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::Event;
use tui::{ use tui::{
buffer::Buffer as Surface, buffer::Buffer as Surface,
widgets::{Block, BorderType, Borders}, widgets::{Block, BorderType, Borders},
@ -36,6 +37,7 @@ type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> { pub struct FilePicker<T> {
picker: Picker<T>, picker: Picker<T>,
pub truncate_start: bool,
/// Caches paths to documents /// Caches paths to documents
preview_cache: HashMap<PathBuf, CachedPreview>, preview_cache: HashMap<PathBuf, CachedPreview>,
read_buffer: Vec<u8>, read_buffer: Vec<u8>,
@ -89,6 +91,7 @@ impl<T> FilePicker<T> {
) -> Self { ) -> Self {
Self { Self {
picker: Picker::new(false, options, format_fn, callback_fn), picker: Picker::new(false, options, format_fn, callback_fn),
truncate_start: true,
preview_cache: HashMap::new(), preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024), read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn), file_fn: Box::new(preview_fn),
@ -171,6 +174,7 @@ impl<T: 'static> Component for FilePicker<T> {
}; };
let picker_area = area.with_width(picker_width); let picker_area = area.with_width(picker_width);
self.picker.truncate_start = self.truncate_start;
self.picker.render(picker_area, surface, cx); self.picker.render(picker_area, surface, cx);
if !render_preview { if !render_preview {
@ -276,6 +280,8 @@ pub struct Picker<T> {
prompt: Prompt, prompt: Prompt,
/// Whether to render in the middle of the area /// Whether to render in the middle of the area
render_centered: bool, render_centered: bool,
/// Wheather to truncate the start (default true)
pub truncate_start: bool,
format_fn: Box<dyn Fn(&T) -> Cow<str>>, format_fn: Box<dyn Fn(&T) -> Cow<str>>,
callback_fn: Box<dyn Fn(&mut Context, &T, Action)>, callback_fn: Box<dyn Fn(&mut Context, &T, Action)>,
@ -306,6 +312,7 @@ impl<T> Picker<T> {
cursor: 0, cursor: 0,
prompt, prompt,
render_centered, render_centered,
truncate_start: true,
format_fn: Box::new(format_fn), format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn), callback_fn: Box::new(callback_fn),
}; };
@ -403,81 +410,35 @@ impl<T: 'static> Component for Picker<T> {
compositor.last_picker = compositor.pop(); compositor.last_picker = compositor.pop();
}))); })));
match key_event { match key_event.into() {
KeyEvent { shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => {
code: KeyCode::Up, ..
}
| KeyEvent {
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_up(); self.move_up();
} }
KeyEvent { key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => {
code: KeyCode::Down,
..
}
| KeyEvent {
code: KeyCode::Tab, ..
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_down(); self.move_down();
} }
KeyEvent { key!(Esc) | ctrl!('c') => {
code: KeyCode::Esc, ..
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => {
return close_fn; return close_fn;
} }
KeyEvent { key!(Enter) => {
code: KeyCode::Enter,
..
} => {
if let Some(option) = self.selection() { if let Some(option) = self.selection() {
(self.callback_fn)(cx, option, Action::Replace); (self.callback_fn)(cx, option, Action::Replace);
} }
return close_fn; return close_fn;
} }
KeyEvent { ctrl!('s') => {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() { if let Some(option) = self.selection() {
(self.callback_fn)(cx, option, Action::HorizontalSplit); (self.callback_fn)(cx, option, Action::HorizontalSplit);
} }
return close_fn; return close_fn;
} }
KeyEvent { ctrl!('v') => {
code: KeyCode::Char('v'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() { if let Some(option) = self.selection() {
(self.callback_fn)(cx, option, Action::VerticalSplit); (self.callback_fn)(cx, option, Action::VerticalSplit);
} }
return close_fn; return close_fn;
} }
KeyEvent { ctrl!(' ') => {
code: KeyCode::Char(' '),
modifiers: KeyModifiers::CONTROL,
} => {
self.save_filter(); self.save_filter();
} }
_ => { _ => {
@ -567,7 +528,7 @@ impl<T: 'static> Component for Picker<T> {
text_style text_style
}, },
true, true,
true, self.truncate_start,
); );
} }
} }

@ -1,5 +1,8 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::{
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; compositor::{Component, Compositor, Context, EventResult},
ctrl, key,
};
use crossterm::event::Event;
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
use helix_core::Position; use helix_core::Position;
@ -95,27 +98,14 @@ impl<T: Component> Component for Popup<T> {
compositor.pop(); compositor.pop();
}))); })));
match key { match key.into() {
// esc or ctrl-c aborts the completion and closes the menu // esc or ctrl-c aborts the completion and closes the menu
KeyEvent { key!(Esc) | ctrl!('c') => close_fn,
code: KeyCode::Esc, .. ctrl!('d') => {
}
| KeyEvent {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
} => close_fn,
KeyEvent {
code: KeyCode::Char('d'),
modifiers: KeyModifiers::CONTROL,
} => {
self.scroll(self.size.1 as usize / 2, true); self.scroll(self.size.1 as usize / 2, true);
EventResult::Consumed(None) EventResult::Consumed(None)
} }
KeyEvent { ctrl!('u') => {
code: KeyCode::Char('u'),
modifiers: KeyModifiers::CONTROL,
} => {
self.scroll(self.size.1 as usize / 2, false); self.scroll(self.size.1 as usize / 2, false);
EventResult::Consumed(None) EventResult::Consumed(None)
} }

@ -1,6 +1,8 @@
use crate::compositor::{Component, Compositor, Context, EventResult}; use crate::compositor::{Component, Compositor, Context, EventResult};
use crate::ui; use crate::{alt, ctrl, key, shift, ui};
use crossterm::event::{Event, KeyCode, KeyEvent, KeyModifiers}; use crossterm::event::Event;
use helix_view::input::KeyEvent;
use helix_view::keyboard::{KeyCode, KeyModifiers};
use std::{borrow::Cow, ops::RangeFrom}; use std::{borrow::Cow, ops::RangeFrom};
use tui::buffer::Buffer as Surface; use tui::buffer::Buffer as Surface;
@ -213,6 +215,14 @@ impl Prompt {
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(&self.line);
} }
pub fn delete_char_forwards(&mut self) {
let pos = self.eval_movement(Movement::ForwardChar(1));
self.line.replace_range(self.cursor..pos, "");
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn delete_word_backwards(&mut self) { pub fn delete_word_backwards(&mut self) {
let pos = self.eval_movement(Movement::BackwardWord(1)); let pos = self.eval_movement(Movement::BackwardWord(1));
self.line.replace_range(pos..self.cursor, ""); self.line.replace_range(pos..self.cursor, "");
@ -222,6 +232,23 @@ impl Prompt {
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(&self.line);
} }
pub fn delete_word_forwards(&mut self) {
let pos = self.eval_movement(Movement::ForwardWord(1));
self.line.replace_range(self.cursor..pos, "");
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn kill_to_start_of_line(&mut self) {
let pos = self.eval_movement(Movement::StartOfLine);
self.line.replace_range(pos..self.cursor, "");
self.cursor = pos;
self.exit_selection();
self.completion = (self.completion_fn)(&self.line);
}
pub fn kill_to_end_of_line(&mut self) { pub fn kill_to_end_of_line(&mut self) {
let pos = self.eval_movement(Movement::EndOfLine); let pos = self.eval_movement(Movement::EndOfLine);
self.line.replace_range(self.cursor..pos, ""); self.line.replace_range(self.cursor..pos, "");
@ -405,84 +432,30 @@ impl Component for Prompt {
compositor.pop(); compositor.pop();
}))); })));
match event { match event.into() {
KeyEvent { ctrl!('c') | key!(Esc) => {
code: KeyCode::Char('c'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Esc, ..
} => {
(self.callback_fn)(cx, &self.line, PromptEvent::Abort); (self.callback_fn)(cx, &self.line, PromptEvent::Abort);
return close_fn; return close_fn;
} }
KeyEvent { alt!('b') | alt!(Left) => self.move_cursor(Movement::BackwardWord(1)),
code: KeyCode::Left, alt!('f') | alt!(Right) => self.move_cursor(Movement::ForwardWord(1)),
modifiers: KeyModifiers::ALT, ctrl!('b') | key!(Left) => self.move_cursor(Movement::BackwardChar(1)),
} ctrl!('f') | key!(Right) => self.move_cursor(Movement::ForwardChar(1)),
| KeyEvent { ctrl!('e') | key!(End) => self.move_end(),
code: KeyCode::Char('b'), ctrl!('a') | key!(Home) => self.move_start(),
modifiers: KeyModifiers::ALT, ctrl!('w') => self.delete_word_backwards(),
} => self.move_cursor(Movement::BackwardWord(1)), alt!('d') => self.delete_word_forwards(),
KeyEvent { ctrl!('k') => self.kill_to_end_of_line(),
code: KeyCode::Right, ctrl!('u') => self.kill_to_start_of_line(),
modifiers: KeyModifiers::ALT, ctrl!('h') | key!(Backspace) => {
}
| KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::ALT,
} => self.move_cursor(Movement::ForwardWord(1)),
KeyEvent {
code: KeyCode::Char('f'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Right,
..
} => self.move_cursor(Movement::ForwardChar(1)),
KeyEvent {
code: KeyCode::Char('b'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Left,
..
} => self.move_cursor(Movement::BackwardChar(1)),
KeyEvent {
code: KeyCode::End,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('e'),
modifiers: KeyModifiers::CONTROL,
} => self.move_end(),
KeyEvent {
code: KeyCode::Home,
modifiers: KeyModifiers::NONE,
}
| KeyEvent {
code: KeyCode::Char('a'),
modifiers: KeyModifiers::CONTROL,
} => self.move_start(),
KeyEvent {
code: KeyCode::Char('w'),
modifiers: KeyModifiers::CONTROL,
} => self.delete_word_backwards(),
KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => self.kill_to_end_of_line(),
KeyEvent {
code: KeyCode::Backspace,
modifiers: KeyModifiers::NONE,
} => {
self.delete_char_backwards(); self.delete_char_backwards();
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
KeyEvent { ctrl!('d') | key!(Delete) => {
code: KeyCode::Char('s'), self.delete_char_forwards();
modifiers: KeyModifiers::CONTROL, (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} => { }
ctrl!('s') => {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..); let text = doc.text().slice(..);
@ -492,6 +465,7 @@ impl Component for Prompt {
doc.selection(view.id).primary(), doc.selection(view.id).primary(),
textobject::TextObject::Inside, textobject::TextObject::Inside,
1, 1,
false,
); );
let line = text.slice(range.from()..range.to()).to_string(); let line = text.slice(range.from()..range.to()).to_string();
if !line.is_empty() { if !line.is_empty() {
@ -499,10 +473,7 @@ impl Component for Prompt {
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
} }
KeyEvent { key!(Enter) => {
code: KeyCode::Enter,
..
} => {
if self.selection.is_some() && self.line.ends_with('/') { if self.selection.is_some() && self.line.ends_with('/') {
self.completion = (self.completion_fn)(&self.line); self.completion = (self.completion_fn)(&self.line);
self.exit_selection(); self.exit_selection();
@ -517,50 +488,29 @@ impl Component for Prompt {
return close_fn; return close_fn;
} }
} }
KeyEvent { ctrl!('p') | key!(Up) => {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Up, ..
} => {
if let Some(register) = self.history_register { if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register); let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Backward); self.change_history(register.read(), CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
} }
KeyEvent { ctrl!('n') | key!(Down) => {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Down,
..
} => {
if let Some(register) = self.history_register { if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register); let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Forward); self.change_history(register.read(), CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
} }
KeyEvent { key!(Tab) => {
code: KeyCode::Tab, ..
} => {
self.change_completion_selection(CompletionDirection::Forward); self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update) (self.callback_fn)(cx, &self.line, PromptEvent::Update)
} }
KeyEvent { shift!(BackTab) => {
code: KeyCode::BackTab,
..
} => {
self.change_completion_selection(CompletionDirection::Backward); self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update) (self.callback_fn)(cx, &self.line, PromptEvent::Update)
} }
KeyEvent { ctrl!('q') => self.exit_selection(),
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,
} => self.exit_selection(),
// any char event that's not combined with control or mapped to any other combo // any char event that's not combined with control or mapped to any other combo
KeyEvent { KeyEvent {
code: KeyCode::Char(c), code: KeyCode::Char(c),

@ -25,6 +25,8 @@ const BUF_SIZE: usize = 8192;
const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4); const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode { pub enum Mode {
Normal, Normal,
@ -96,7 +98,7 @@ pub struct Document {
// It can be used as a cell where we will take it out to get some parts of the history and put // It can be used as a cell where we will take it out to get some parts of the history and put
// it back as it separated from the edits. We could split out the parts manually but that will // it back as it separated from the edits. We could split out the parts manually but that will
// be more troublesome. // be more troublesome.
history: Cell<History>, pub history: Cell<History>,
pub savepoint: Option<Transaction>, pub savepoint: Option<Transaction>,
@ -494,7 +496,9 @@ impl Document {
/// Detect the programming language based on the file type. /// Detect the programming language based on the file type.
pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) { pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
if let Some(path) = &self.path { if let Some(path) = &self.path {
let language_config = config_loader.language_config_for_file_name(path); let language_config = config_loader
.language_config_for_file_name(path)
.or_else(|| config_loader.language_config_for_shebang(self.text()));
self.set_language(theme, language_config); self.set_language(theme, language_config);
} }
} }
@ -749,19 +753,35 @@ impl Document {
} }
/// Undo modifications to the [`Document`] according to `uk`. /// Undo modifications to the [`Document`] according to `uk`.
pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { pub fn earlier(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
let txns = self.history.get_mut().earlier(uk); let txns = self.history.get_mut().earlier(uk);
let mut success = false;
for txn in txns { for txn in txns {
self.apply_impl(&txn, view_id); if self.apply_impl(&txn, view_id) {
success = true;
}
} }
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
}
success
} }
/// Redo modifications to the [`Document`] according to `uk`. /// Redo modifications to the [`Document`] according to `uk`.
pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) { pub fn later(&mut self, view_id: ViewId, uk: helix_core::history::UndoKind) -> bool {
let txns = self.history.get_mut().later(uk); let txns = self.history.get_mut().later(uk);
let mut success = false;
for txn in txns { for txn in txns {
self.apply_impl(&txn, view_id); if self.apply_impl(&txn, view_id) {
success = true;
}
}
if success {
// reset changeset to fix len
self.changes = ChangeSet::new(self.text());
} }
success
} }
/// Commit pending changes to history /// Commit pending changes to history

@ -1,5 +1,6 @@
use crate::{ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
document::SCRATCH_BUFFER_NAME,
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
theme::{self, Theme}, theme::{self, Theme},
tree::{self, Tree}, tree::{self, Tree},
@ -11,8 +12,8 @@ use futures_util::stream::select_all::SelectAll;
use tokio_stream::wrappers::UnboundedReceiverStream; use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{ use std::{
collections::BTreeMap, collections::{BTreeMap, HashMap},
collections::HashMap, io::stdin,
path::{Path, PathBuf}, path::{Path, PathBuf},
pin::Pin, pin::Pin,
sync::Arc, sync::Arc,
@ -25,8 +26,8 @@ use anyhow::Error;
pub use helix_core::diagnostic::Severity; pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers; pub use helix_core::register::Registers;
use helix_core::syntax; use helix_core::syntax;
use helix_core::Position;
use helix_dap as dap; use helix_dap as dap;
use helix_core::{Position, Selection};
use serde::Deserialize; use serde::Deserialize;
@ -39,7 +40,7 @@ where
} }
#[derive(Debug, Clone, PartialEq, Deserialize)] #[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", default)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config { pub struct Config {
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
pub scrolloff: usize, pub scrolloff: usize,
@ -138,6 +139,8 @@ pub struct Editor {
pub idle_timer: Pin<Box<Sleep>>, pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>, pub last_motion: Option<Motion>,
pub exit_code: i32,
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
@ -179,6 +182,7 @@ impl Editor {
idle_timer: Box::pin(sleep(config.idle_timeout)), idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None, last_motion: None,
config, config,
exit_code: 0,
} }
} }
@ -208,6 +212,12 @@ impl Editor {
} }
pub fn set_theme(&mut self, theme: Theme) { pub fn set_theme(&mut self, theme: Theme) {
// `ui.selection` is the only scope required to be able to render a theme.
if theme.find_scope_index("ui.selection").is_none() {
self.set_error("Invalid theme: `ui.selection` required".to_owned());
return;
}
let scopes = theme.scopes(); let scopes = theme.scopes();
for config in self for config in self
.syn_loader .syn_loader
@ -238,9 +248,28 @@ impl Editor {
} }
} }
fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
let view = self.tree.get_mut(current_view);
view.doc = doc_id;
view.offset = Position::default();
let doc = self.documents.get_mut(&doc_id).unwrap();
// initialize selection for view
doc.selections
.entry(view.id)
.or_insert_with(|| Selection::point(0));
// TODO: reuse align_view
let pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
}
pub fn switch(&mut self, id: DocumentId, action: Action) { pub fn switch(&mut self, id: DocumentId, action: Action) {
use crate::tree::Layout; use crate::tree::Layout;
use helix_core::Selection;
if !self.documents.contains_key(&id) { if !self.documents.contains_key(&id) {
log::error!("cannot switch to document that does not exist (anymore)"); log::error!("cannot switch to document that does not exist (anymore)");
@ -274,26 +303,19 @@ impl Editor {
view.jumps.push(jump); view.jumps.push(jump);
view.last_accessed_doc = Some(view.doc); view.last_accessed_doc = Some(view.doc);
} }
view.doc = id;
view.offset = Position::default();
let (view, doc) = current!(self); let view_id = view.id;
self.replace_document_in_view(view_id, id);
// initialize selection for view
doc.selections
.entry(view.id)
.or_insert_with(|| Selection::point(0));
// TODO: reuse align_view
let pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
return; return;
} }
Action::Load => { Action::Load => {
let view_id = view!(self).id;
if let Some(doc) = self.document_mut(id) {
if doc.selections().is_empty() {
doc.selections.insert(view_id, Selection::point(0));
}
}
return; return;
} }
Action::HorizontalSplit => { Action::HorizontalSplit => {
@ -315,16 +337,29 @@ impl Editor {
self._refresh(); self._refresh();
} }
pub fn new_file(&mut self, action: Action) -> DocumentId { fn new_document(&mut self, mut document: Document) -> DocumentId {
let id = DocumentId(self.next_document_id); let id = DocumentId(self.next_document_id);
self.next_document_id += 1; self.next_document_id += 1;
let mut doc = Document::default(); document.id = id;
doc.id = id; self.documents.insert(id, document);
self.documents.insert(id, doc); id
}
fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
let id = self.new_document(document);
self.switch(id, action); self.switch(id, action);
id id
} }
pub fn new_file(&mut self, action: Action) -> DocumentId {
self.new_file_from_document(action, Document::default())
}
pub fn new_file_from_stdin(&mut self, action: Action) -> Result<DocumentId, Error> {
let (rope, encoding) = crate::document::from_reader(&mut stdin(), None)?;
Ok(self.new_file_from_document(action, Document::from(rope, Some(encoding))))
}
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> { pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(&path)?; let path = helix_core::path::get_canonicalized_path(&path)?;
@ -381,7 +416,7 @@ impl Editor {
Ok(id) Ok(id)
} }
pub fn close(&mut self, id: ViewId, close_buffer: bool) { pub fn close(&mut self, id: ViewId) {
let view = self.tree.get(self.tree.focus); let view = self.tree.get(self.tree.focus);
// remove selection // remove selection
self.documents self.documents
@ -390,18 +425,66 @@ impl Editor {
.selections .selections
.remove(&id); .remove(&id);
if close_buffer { self.tree.remove(id);
// get around borrowck issues self._refresh();
let doc = &self.documents[&view.doc]; }
if let Some(language_server) = doc.language_server() { pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
tokio::spawn(language_server.text_document_did_close(doc.identifier())); let doc = match self.documents.get(&doc_id) {
} Some(doc) => doc,
self.documents.remove(&view.doc); None => anyhow::bail!("document does not exist"),
};
if !force && doc.is_modified() {
anyhow::bail!(
"buffer {:?} is modified",
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
.unwrap_or_else(|| SCRATCH_BUFFER_NAME.into())
);
}
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
let views_to_close = self
.tree
.views()
.filter_map(|(view, _focus)| {
if view.doc == doc_id {
Some(view.id)
} else {
None
}
})
.collect::<Vec<_>>();
for view_id in views_to_close {
self.close(view_id);
}
self.documents.remove(&doc_id);
// If the document we removed was visible in all views, we will have no more views. We don't
// want to close the editor just for a simple buffer close, so we need to create a new view
// containing either an existing document, or a brand new document.
if self.tree.views().peekable().peek().is_none() {
let doc_id = self
.documents
.iter()
.map(|(&doc_id, _)| doc_id)
.next()
.unwrap_or_else(|| self.new_document(Document::default()));
let view = View::new(doc_id);
let view_id = self.tree.insert(view);
let doc = self.documents.get_mut(&doc_id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
} }
self.tree.remove(id);
self._refresh(); self._refresh();
Ok(())
} }
pub fn resize(&mut self, area: Rect) { pub fn resize(&mut self, area: Rect) {

@ -11,10 +11,19 @@
/// Returns `(&mut View, &mut Document)` /// Returns `(&mut View, &mut Document)`
#[macro_export] #[macro_export]
macro_rules! current { macro_rules! current {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
let view = $crate::view_mut!( $( $editor ).+ ); let view = $crate::view_mut!($editor);
let id = view.doc; let id = view.doc;
let doc = $( $editor ).+ .documents.get_mut(&id).unwrap(); let doc = $editor.documents.get_mut(&id).unwrap();
(view, doc)
}};
}
#[macro_export]
macro_rules! current_ref {
($editor:expr) => {{
let view = $editor.tree.get($editor.tree.focus);
let doc = &$editor.documents[&view.doc];
(view, doc) (view, doc)
}}; }};
} }
@ -23,8 +32,8 @@ macro_rules! current {
/// Returns `&mut Document` /// Returns `&mut Document`
#[macro_export] #[macro_export]
macro_rules! doc_mut { macro_rules! doc_mut {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
$crate::current!( $( $editor ).+ ).1 $crate::current!($editor).1
}}; }};
} }
@ -32,8 +41,8 @@ macro_rules! doc_mut {
/// Returns `&mut View` /// Returns `&mut View`
#[macro_export] #[macro_export]
macro_rules! view_mut { macro_rules! view_mut {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
$( $editor ).+ .tree.get_mut($( $editor ).+ .tree.focus) $editor.tree.get_mut($editor.tree.focus)
}}; }};
} }
@ -41,23 +50,14 @@ macro_rules! view_mut {
/// Returns `&View` /// Returns `&View`
#[macro_export] #[macro_export]
macro_rules! view { macro_rules! view {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
$( $editor ).+ .tree.get($( $editor ).+ .tree.focus) $editor.tree.get($editor.tree.focus)
}}; }};
} }
#[macro_export] #[macro_export]
macro_rules! doc { macro_rules! doc {
( $( $editor:ident ).+ ) => {{ ($editor:expr) => {{
$crate::current_ref!( $( $editor ).+ ).1 $crate::current_ref!($editor).1
}};
}
#[macro_export]
macro_rules! current_ref {
( $( $editor:ident ).+ ) => {{
let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
let doc = &$( $editor ).+ .documents[&view.doc];
(view, doc)
}}; }};
} }

@ -314,6 +314,9 @@ impl Tree {
pub fn recalculate(&mut self) { pub fn recalculate(&mut self) {
if self.is_empty() { if self.is_empty() {
// There are no more views, so the tree should focus itself again.
self.focus = self.root;
return; return;
} }

@ -54,6 +54,10 @@ impl JumpList {
None None
} }
} }
pub fn remove(&mut self, doc_id: &DocumentId) {
self.jumps.retain(|(other_id, _)| other_id != doc_id);
}
} }
#[derive(Debug)] #[derive(Debug)]
@ -85,7 +89,12 @@ impl View {
self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
} }
pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) { //
pub fn offset_coords_to_in_view(
&self,
doc: &Document,
scrolloff: usize,
) -> Option<(usize, usize)> {
let cursor = doc let cursor = doc
.selection(self.id) .selection(self.id)
.primary() .primary()
@ -104,23 +113,43 @@ impl View {
let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize; let last_col = self.offset.col + inner_area.width.saturating_sub(1) as usize;
if line > last_line.saturating_sub(scrolloff) { let row = if line > last_line.saturating_sub(scrolloff) {
// scroll down // scroll down
self.offset.row += line - (last_line.saturating_sub(scrolloff)); self.offset.row + line - (last_line.saturating_sub(scrolloff))
} else if line < self.offset.row + scrolloff { } else if line < self.offset.row + scrolloff {
// scroll up // scroll up
self.offset.row = line.saturating_sub(scrolloff); line.saturating_sub(scrolloff)
} } else {
self.offset.row
};
if col > last_col.saturating_sub(scrolloff) { let col = if col > last_col.saturating_sub(scrolloff) {
// scroll right // scroll right
self.offset.col += col - (last_col.saturating_sub(scrolloff)); self.offset.col + col - (last_col.saturating_sub(scrolloff))
} else if col < self.offset.col + scrolloff { } else if col < self.offset.col + scrolloff {
// scroll left // scroll left
self.offset.col = col.saturating_sub(scrolloff); col.saturating_sub(scrolloff)
} else {
self.offset.col
};
if row == self.offset.row && col == self.offset.col {
None
} else {
Some((row, col))
}
}
pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
if let Some((row, col)) = self.offset_coords_to_in_view(doc, scrolloff) {
self.offset.row = row;
self.offset.col = col;
} }
} }
pub fn is_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) -> bool {
self.offset_coords_to_in_view(doc, scrolloff).is_none()
}
/// Calculates the last visible line on screen /// Calculates the last visible line on screen
#[inline] #[inline]
pub fn last_line(&self, doc: &Document) -> usize { pub fn last_line(&self, doc: &Document) -> usize {

@ -60,12 +60,25 @@ name = "elixir"
scope = "source.elixir" scope = "source.elixir"
injection-regex = "elixir" injection-regex = "elixir"
file-types = ["ex", "exs"] file-types = ["ex", "exs"]
shebangs = ["elixir"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
language-server = { command = "elixir-ls" } language-server = { command = "elixir-ls" }
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
[[language]]
name = "mint"
scope = "source.mint"
injection-regex = "mint"
file-types = ["mint"]
shebangs = []
roots = []
comment-token = "//"
language-server = { command = "mint", args = ["ls"] }
indent = { tab-width = 2, unit = " " }
[[language]] [[language]]
name = "json" name = "json"
scope = "source.json" scope = "source.json"
@ -202,6 +215,7 @@ name = "javascript"
scope = "source.js" scope = "source.js"
injection-regex = "^(js|javascript)$" injection-regex = "^(js|javascript)$"
file-types = ["js", "mjs"] file-types = ["js", "mjs"]
shebangs = ["node"]
roots = [] roots = []
comment-token = "//" comment-token = "//"
# TODO: highlights-jsx, highlights-params # TODO: highlights-jsx, highlights-params
@ -225,6 +239,7 @@ name = "typescript"
scope = "source.ts" scope = "source.ts"
injection-regex = "^(ts|typescript)$" injection-regex = "^(ts|typescript)$"
file-types = ["ts"] file-types = ["ts"]
shebangs = []
roots = [] roots = []
# TODO: highlights-jsx, highlights-params # TODO: highlights-jsx, highlights-params
@ -265,6 +280,7 @@ name = "python"
scope = "source.python" scope = "source.python"
injection-regex = "python" injection-regex = "python"
file-types = ["py"] file-types = ["py"]
shebangs = ["python"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
@ -277,6 +293,7 @@ name = "nix"
scope = "source.nix" scope = "source.nix"
injection-regex = "nix" injection-regex = "nix"
file-types = ["nix"] file-types = ["nix"]
shebangs = []
roots = [] roots = []
comment-token = "#" comment-token = "#"
@ -288,6 +305,7 @@ name = "ruby"
scope = "source.ruby" scope = "source.ruby"
injection-regex = "ruby" injection-regex = "ruby"
file-types = ["rb"] file-types = ["rb"]
shebangs = ["ruby"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
@ -299,6 +317,7 @@ name = "bash"
scope = "source.bash" scope = "source.bash"
injection-regex = "bash" injection-regex = "bash"
file-types = ["sh", "bash"] file-types = ["sh", "bash"]
shebangs = ["sh", "bash", "dash"]
roots = [] roots = []
comment-token = "#" comment-token = "#"
@ -310,6 +329,7 @@ name = "php"
scope = "source.php" scope = "source.php"
injection-regex = "php" injection-regex = "php"
file-types = ["php"] file-types = ["php"]
shebangs = ["php"]
roots = [] roots = []
indent = { tab-width = 4, unit = " " } indent = { tab-width = 4, unit = " " }
@ -371,6 +391,7 @@ name = "ocaml"
scope = "source.ocaml" scope = "source.ocaml"
injection-regex = "ocaml" injection-regex = "ocaml"
file-types = ["ml"] file-types = ["ml"]
shebangs = []
roots = [] roots = []
comment-token = "(**)" comment-token = "(**)"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
@ -379,6 +400,7 @@ indent = { tab-width = 2, unit = " " }
name = "ocaml-interface" name = "ocaml-interface"
scope = "source.ocaml.interface" scope = "source.ocaml.interface"
file-types = ["mli"] file-types = ["mli"]
shebangs = []
roots = [] roots = []
comment-token = "(**)" comment-token = "(**)"
indent = { tab-width = 2, unit = " "} indent = { tab-width = 2, unit = " "}
@ -387,6 +409,7 @@ indent = { tab-width = 2, unit = " "}
name = "lua" name = "lua"
scope = "source.lua" scope = "source.lua"
file-types = ["lua"] file-types = ["lua"]
shebangs = ["lua"]
roots = [] roots = []
comment-token = "--" comment-token = "--"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
@ -444,6 +467,7 @@ name = "prolog"
scope = "source.prolog" scope = "source.prolog"
roots = [] roots = []
file-types = ["pl", "prolog"] file-types = ["pl", "prolog"]
shebangs = ["swipl"]
comment-token = "%" comment-token = "%"
language-server = { command = "swipl", args = [ language-server = { command = "swipl", args = [
@ -467,3 +491,20 @@ roots = []
comment-token = "#" comment-token = "#"
indent = { tab-width = 2, unit = " " } indent = { tab-width = 2, unit = " " }
language-server = { command = "cmake-language-server" } language-server = { command = "cmake-language-server" }
[[language]]
name = "glsl"
scope = "source.glsl"
file-types = ["glsl", "vert", "tesc", "tese", "geom", "frag", "comp" ]
roots = []
comment-token = "//"
indent = { tab-width = 4, unit = " " }
[[language]]
name = "perl"
scope = "source.perl"
file-types = ["pl", "pm"]
shebangs = ["perl"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }

@ -0,0 +1,37 @@
; inherits: c
[
"in"
"out"
"inout"
"uniform"
"shared"
"layout"
"attribute"
"varying"
"buffer"
"coherent"
"readonly"
"writeonly"
"precision"
"highp"
"mediump"
"lowp"
"centroid"
"sample"
"patch"
"smooth"
"flat"
"noperspective"
"invariant"
"precise"
] @keyword
"subroutine" @keyword.function
(extension_storage_class) @attribute
(
(identifier) @variable.builtin
(#match? @variable.builtin "^gl_")
)

@ -0,0 +1,19 @@
indent = [
"init_declarator",
"compound_statement",
"preproc_arg",
"field_declaration_list",
"case_statement",
"conditional_expression",
"enumerator_list",
"struct_specifier",
"compound_literal_expression"
]
outdent = [
"#define",
"#ifdef",
"#endif",
"{",
"}"
]

@ -0,0 +1,3 @@
(preproc_arg) @glsl
(comment) @comment

@ -0,0 +1,9 @@
indent = [
"object",
"array"
]
outdent = [
"]",
"}"
]

@ -0,0 +1,181 @@
; Variables
(variable_declaration
.
(scope) @keyword)
[
(single_var_declaration)
(scalar_variable)
(array_variable)
(hash_variable)
(hash_variable)
] @variable
[
(package_name)
(special_scalar_variable)
(special_array_variable)
(special_hash_variable)
(special_literal)
(super)
] @constant
(
[
(package_name)
(super)
]
.
("::" @operator)
)
(comments) @comment
(pod_statement) @comment.block.documentation
[
(use_no_statement)
(use_no_feature_statement)
(use_no_if_statement)
(use_no_version)
(use_constant_statement)
(use_parent_statement)
] @keyword
(use_constant_statement
constant: (identifier) @constant)
[
"require"
] @keyword
(method_invocation
.
(identifier) @variable)
(method_invocation
(arrow_operator)
.
(identifier) @function)
(method_invocation
function_name: (identifier) @function)
(named_block_statement
function_name: (identifier) @function)
(call_expression
function_name: (identifier) @function)
(function_definition
name: (identifier) @function)
[
(function)
(map)
(grep)
(bless)
] @function
[
"return"
"sub"
"package"
"BEGIN"
"END"
] @keyword.function
[
"("
")"
"["
"]"
"{"
"}"
] @punctuation.bracket
(standard_input_to_variable) @punctuation.bracket
[
"=~"
"or"
"="
"=="
"+"
"-"
"."
"//"
"||"
(arrow_operator)
(hash_arrow_operator)
(array_dereference)
(hash_dereference)
(to_reference)
(type_glob)
(hash_access_variable)
(ternary_expression)
(ternary_expression_in_hash)
] @operator
[
(regex_option)
(regex_option_for_substitution)
(regex_option_for_transliteration)
] @variable.parameter
(type_glob
(identifier) @variable)
(
(scalar_variable)
.
("->" @operator))
[
(word_list_qw)
(command_qx_quoted)
(string_single_quoted)
(string_double_quoted)
(string_qq_quoted)
(bareword)
(transliteration_tr_or_y)
] @string
[
(regex_pattern_qr)
(patter_matcher_m)
(substitution_pattern_s)
] @string.regexp
(escape_sequence) @string.special
[
","
(semi_colon)
(start_delimiter)
(end_delimiter)
(ellipsis_statement)
] @punctuation.delimiter
[
(integer)
(floating_point)
(scientific_notation)
(hexadecimal)
] @constant.numeric
[
; (if_statement)
(unless_statement)
(if_simple_statement)
(unless_simple_statement)
] @keyword.control.conditional
[
"if"
"elsif"
"else"
] @keyword.control.conditional
(foreach_statement) @keyword.control.repeat
(foreach_statement
.
(scope) @keyword)
(function_attribute) @label
(function_signature) @type

@ -0,0 +1,8 @@
(function_definition
(identifier) (_) @function.inside) @function.around
(anonymous_function
(_) @function.inside) @function.around
(argument
(_) @parameter.inside)

@ -10,11 +10,11 @@
# Polar Night # Polar Night
# nord0 - background color # nord0 - background color
"ui.background" = { bg = "nord0" } "ui.background" = { bg = "nord0" }
"ui.statusline.inactive" = { fg = "nord4", bg = "nord0" } "ui.statusline.inactive" = { fg = "nord8", bg = "nord1" }
# nord1 - status bars, panels, modals, autocompletion # nord1 - status bars, panels, modals, autocompletion
"ui.statusline" = { fg = "nord8", bg = "nord1" } "ui.statusline" = { fg = "nord4", bg = "#4c566a" }
"ui.popup" = { bg = "#232d38" } "ui.popup" = { bg = "#232d38" }
"ui.window" = { bg = "#232d38" } "ui.window" = { bg = "#232d38" }
"ui.help" = { bg = "#232d38", fg = "nord4" } "ui.help" = { bg = "#232d38", fg = "nord4" }
@ -25,7 +25,7 @@
# nord3 - comments, nord3 based lighter color # nord3 - comments, nord3 based lighter color
# relative: https://github.com/arcticicestudio/nord/issues/94 # relative: https://github.com/arcticicestudio/nord/issues/94
"comment" = "gray" "comment" = { fg = "gray", modifiers = ["italic"] }
"ui.linenr" = { fg = "gray" } "ui.linenr" = { fg = "gray" }
# Snow Storm # Snow Storm

@ -0,0 +1,97 @@
"attribute" = { fg = "violet" }
"keyword" = { fg = "green" }
"keyword.directive" = { fg = "orange" }
"namespace" = { fg = "violet" }
"operator" = { fg = "green" }
"special" = { fg = "orange" }
"variable.builtin" = { fg = "cyan", modifiers = ["bold"] }
"variable.function" = { fg = "blue" }
"type" = { fg = "yellow" }
"type.builtin" = { fg = "yellow", modifiers = ["bold"] }
"constructor" = { fg = "blue" }
"function" = { fg = "blue" }
"function.macro" = { fg = "magenta" }
"function.builtin" = { fg = "blue", modifiers = ["bold"] }
"function.special" = { fg = "magenta" }
"comment" = { fg = "base01" }
"string" = { fg = "cyan" }
"constant" = { fg = "cyan" }
"constant.builtin" = { fg = "cyan", modifiers = ["bold"] }
"constant.character.escape" = { fg = "red", modifiers = ["bold"] }
"label" = { fg = "green" }
"module" = { fg = "violet" }
"tag" = { fg = "magenta" }
# 背景
"ui.background" = { bg = "base03" }
# 行号栏
"ui.linenr" = { fg = "base0", bg = "base02" }
# 当前行号栏
"ui.linenr.selected" = { fg = "red", modifiers = ["bold"] }
# 状态栏
"ui.statusline" = { fg = "base02", bg = "base1" }
# 非活动状态栏
"ui.statusline.inactive" = { fg = "base02", bg = "base00" }
# 补全窗口, preview窗口
"ui.popup" = { bg = "base1" }
# 影响 补全选中 cmd弹出信息选中
"ui.menu.selected" = { fg = "base02", bg = "violet"}
"ui.menu" = { fg = "base02" }
# ??
"ui.window" = { fg = "base3" }
# 命令行 补全的帮助信息
"ui.help" = { modifiers = ["reversed"] }
# 快捷键窗口
"ui.info" = { bg = "base1" }
# 快捷键字体
"ui.info.text" = {fg = "base02", modifiers = ["bold"]}
# 普通ui的字体样式
"ui.text" = { fg = "base1" }
# 影响 picker列表选中, 快捷键帮助窗口文本
"ui.text.focus" = { fg = "blue", modifiers = ["bold"]}
# file picker中 预览的当前选中项
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
"ui.cursor.primary" = {fg = "base03", bg = "base1"}
"ui.selection.primary" = { fg = "base03", bg = "base01" }
"ui.cursor.select" = {fg = "base02", bg = "green"}
"ui.selection" = { fg = "base02", bg = "yellow" }
# normal模式的光标
"ui.cursor" = {fg = "base03", bg = "green"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
"warning" = { fg = "orange", modifiers= ["bold", "underlined"] }
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { mdifiers = ["underlined"] }
[palette]
# 深色 越来越深
base03 = "#002b36"
base02 = "#073642"
base01 = "#586e75"
base00 = "#657b83"
base0 = "#839496"
base1 = "#93a1a1"
base2 = "#eee8d5"
base3 = "#fdf6e3"
# 浅色 越來越浅
yellow = "#b58900"
orange = "#cb4b16"
red = "#dc322f"
magenta = "#d33682"
violet = "#6c71c4"
blue = "#268bd2"
cyan = "#2aa198"
green = "#859900"

@ -0,0 +1,98 @@
"attribute" = { fg = "violet" }
"keyword" = { fg = "green" }
"keyword.directive" = { fg = "orange" }
"namespace" = { fg = "violet" }
"operator" = { fg = "green" }
"special" = { fg = "orange" }
"variable.builtin" = { fg = "cyan", modifiers = ["bold"] }
"variable.function" = { fg = "blue" }
"type" = { fg = "yellow" }
"type.builtin" = { fg = "yellow", modifiers = ["bold"] }
"constructor" = { fg = "blue" }
"function" = { fg = "blue" }
"function.macro" = { fg = "magenta" }
"function.builtin" = { fg = "blue", modifiers = ["bold"] }
"function.special" = { fg = "magenta" }
"comment" = { fg = "base01" }
"string" = { fg = "cyan" }
"constant" = { fg = "cyan" }
"constant.builtin" = { fg = "cyan", modifiers = ["bold"] }
"constant.character.escape" = { fg = "red", modifiers = ["bold"] }
"label" = { fg = "green" }
"module" = { fg = "violet" }
"tag" = { fg = "magenta" }
# 背景
"ui.background" = { bg = "base03" }
# 行号栏
"ui.linenr" = { fg = "base0", bg = "base02" }
# 当前行号栏
"ui.linenr.selected" = { fg = "red", modifiers = ["bold"] }
# 状态栏
"ui.statusline" = { fg = "base02", bg = "base1" }
# 非活动状态栏
"ui.statusline.inactive" = { fg = "base02", bg = "base00" }
# 补全窗口, preview窗口
"ui.popup" = { bg = "base1" }
# 影响 补全选中 cmd弹出信息选中
"ui.menu.selected" = { fg = "base02", bg = "violet"}
"ui.menu" = { fg = "base02" }
# ??
"ui.window" = { fg = "base3" }
# 命令行 补全的帮助信息
"ui.help" = { modifiers = ["reversed"] }
# 快捷键窗口
"ui.info" = { bg = "base1" }
# 快捷键字体
"ui.info.text" = {fg = "base02", modifiers = ["bold"]}
# 普通ui的字体样式
"ui.text" = { fg = "base1" }
# 影响 picker列表选中, 快捷键帮助窗口文本
"ui.text.focus" = { fg = "blue", modifiers = ["bold"]}
# file picker中 预览的当前选中项
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
# 主光标/selectio
"ui.cursor.primary" = {fg = "base03", bg = "base1"}
"ui.selection.primary" = { fg = "base03", bg = "base01" }
"ui.cursor.select" = {fg = "base02", bg = "green"}
"ui.selection" = { fg = "base02", bg = "yellow" }
# normal模式的光标
"ui.cursor" = {fg = "base03", bg = "green"}
"ui.cursor.insert" = {fg = "base03", bg = "base3"}
# 当前光标匹配的标点符号
"ui.cursor.match" = {modifiers = ["reversed"]}
"warning" = { fg = "orange", modifiers= ["bold", "underlined"] }
"error" = { fg = "red", modifiers= ["bold", "underlined"] }
"info" = { fg = "blue", modifiers= ["bold", "underlined"] }
"hint" = { fg = "base01", modifiers= ["bold", "underlined"] }
"diagnostic" = { mdifiers = ["underlined"] }
[palette]
red = '#dc322f'
green = '#859900'
yellow = '#b58900'
blue = '#268bd2'
magenta = '#d33682'
cyan = '#2aa198'
orange = '#cb4b16'
violet = '#6c71c4'
# 深色 越来越深
base0 = '#657b83'
base1 = '#586e75'
base2 = '#073642'
base3 = '#002b36'
## 浅色 越來越浅
base00 = '#839496'
base01 = '#93a1a1'
base02 = '#eee8d5'
base03 = '#fdf6e3'

@ -0,0 +1,65 @@
# Author : Koen Van der Auwera <atog@hey.com>
# Based on SpaceBones Light https://github.com/chipotle/spacebones
# https://github.com/chipotle/spacebones/blob/main/SpaceBones%20Light.bbColorScheme
"attribute" = "#b1951d"
"keyword" = { fg = "#3a81c3" }
"keyword.directive" = "#3a81c3"
"namespace" = "#b1951d"
"punctuation" = "#6c3163"
"punctuation.delimiter" = "#6c3163"
"operator" = "#ba2f59"
"special" = "#ba2f59"
"property" = "#7590db"
"variable.property" = "#7590db"
"variable" = "#715ab1"
"variable.builtin" = "#715ab1"
"variable.parameter" = "#7590db"
"type" = "#6c3163"
"type.builtin" = "#6c3163"
"constructor" = { fg = "#4e3163", modifiers = ["bold"] }
"function" = { fg = "#715ab1", modifiers = ["bold"] }
"function.macro" = "#b1951d"
"function.builtin" = "#b1951d"
"comment" = { fg = "#a49da5", modifiers = ["italic"] }
"constant" = { fg = "#6c3163" }
"constant.builtin" = { fg = "#6c3163", modifiers = ["bold"] }
"string" = "#2d9574"
"number" = "#6c3163"
"escape" = { fg = "fg2", modifiers = ["bold"] }
"label" = "#b1951d"
"module" = "#b1951d"
"warning" = { fg = "#da8b55" }
"error" = { fg = "#e0211d" }
"info" = { fg = "#b1951d" }
"hint" = { fg = "#d1dcdf" }
"ui.background" = { bg = "bg0" }
"ui.linenr" = { fg = "bg3" }
"ui.linenr.selected" = { fg = "#b1951d" }
"ui.statusline" = { fg = "fg1", bg = "bg2" }
"ui.statusline.inactive" = { fg = "fg4", bg = "bg1" }
"ui.popup" = { bg = "bg1" }
"ui.window" = { bg = "bg1" }
"ui.help" = { bg = "bg1", fg = "fg1" }
"ui.text" = { fg = "fg1" }
"ui.text.focus" = { fg = "fg1" }
"ui.selection" = { bg = "bg3", modifiers = ["reversed"] }
"ui.cursor.primary" = { modifiers = ["reversed"] }
"ui.cursor.match" = { modifiers = ["reversed"] }
"ui.menu" = { fg = "fg1", bg = "bg2" }
"ui.menu.selected" = { fg = "#655370", bg = "#d1dcdf", modifiers = ["bold"] }
"diagnostic" = { modifiers = ["underlined"] }
[palette]
bg0 = "#fbf8ef"
bg1 = "#efeae9"
bg2 = "#d1dcdf"
bg3 = "#b4c6cb"
fg1 = "#655370"
fg2 = "#5f3bc4"
fg3 = "#bdae93"
fg4 = "#a89984"

@ -23,8 +23,8 @@ comment = "sirocco"
constant = "white" constant = "white"
"constant.builtin" = "white" "constant.builtin" = "white"
string = "silver" string = "silver"
number = "chamois" "constant.numeric" = "chamois"
escape = "honey" "constant.character.escape" = "honey"
# used for lifetimes # used for lifetimes
label = "honey" label = "honey"

Loading…
Cancel
Save