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

This commit is contained in:
Blaž Hrastnik 2021-11-21 20:06:45 +09:00
commit d1854d8e6a
56 changed files with 2626 additions and 600 deletions

8
.gitmodules vendored
View File

@ -134,3 +134,11 @@
path = helix-syntax/languages/tree-sitter-cmake
url = https://github.com/uyha/tree-sitter-cmake
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
View File

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

View File

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

View File

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

View File

@ -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

View File

@ -5,7 +5,7 @@ # Adding Textobject Queries
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.
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`
when contributing. Note that to test the query files locally you should put

View File

@ -1,5 +1,8 @@
# 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
### Movement
@ -58,15 +61,19 @@ ### Changes
| `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` |
| `U` | Redo change | `redo` |
| `Alt-u` | Move backward in history | `earlier` |
| `Alt-U` | Move forward in history | `later` |
| `y` | Yank selection | `yank` |
| `p` | Paste after selection | `paste_after` |
| `P` | Paste before selection | `paste_before` |
| `"` `<reg>` | Select a register to yank to or paste from | `select_register` |
| `>` | Indent selection | `indent` |
| `<` | Unindent selection | `unindent` |
| `=` | Format selection | `format_selections` |
| `=` | Format selection (**LSP**) | `format_selections` |
| `d` | Delete selection | `delete_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
@ -85,6 +92,7 @@ ### Selection manipulation
| `s` | Select all regex matches inside selections | `select_regex` |
| `S` | Split selection into subselections on regex matches | `split_selection` |
| `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` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| `,` | Keep only the primary selection | `keep_primary_selection` |
@ -98,9 +106,10 @@ ### Selection manipulation
| `%` | Select entire file | `select_all` |
| `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` |
| | 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` |
| `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` |
| `Ctrl-c` | Comment/uncomment the selections | `toggle_comments` |
@ -127,25 +136,23 @@ #### View mode
over text and not actively editing it).
| Key | Description | Command |
| ----- | ----------- | ------- |
| `z` , `c` | Vertically center the line | `align_view_center` |
| `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` |
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j` | Scroll the view downwards | `scroll_down` |
| `k` | Scroll the view upwards | `scroll_up` |
| `f` | Move page down | `page_down` |
| `b` | Move page up | `page_up` |
| `d` | Move half page down | `half_page_down` |
| `u` | Move half page up | `half_page_up` |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `z` , `c` | Vertically center the line | `align_view_center` |
| `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` |
| `m` | Align the line to the middle of the screen (horizontally) | `align_view_middle` |
| `j` , `down` | Scroll the view downwards | `scroll_down` |
| `k` , `up` | Scroll the view upwards | `scroll_up` |
| `f` | Move page down | `page_down` |
| `b` | Move page up | `page_up` |
| `d` | Move half page down | `half_page_down` |
| `u` | Move half page up | `half_page_up` |
#### Goto mode
Jumps to various locations.
> NOTE: Some of these features are only available with the LSP present.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `g` | Go to the start of the file | `goto_file_start` |
@ -156,13 +163,14 @@ #### Goto mode
| `t` | Go to the top of the screen | `goto_window_top` |
| `m` | Go to the middle of the screen | `goto_window_middle` |
| `b` | Go to the bottom of the screen | `goto_window_bottom` |
| `d` | Go to definition | `goto_definition` |
| `y` | Go to type definition | `goto_type_definition` |
| `r` | Go to references | `goto_reference` |
| `i` | Go to implementation | `goto_implementation` |
| `d` | Go to definition (**LSP**) | `goto_definition` |
| `y` | Go to type definition (**LSP**) | `goto_type_definition` |
| `r` | Go to references (**LSP**) | `goto_reference` |
| `i` | Go to implementation (**LSP**) | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
| `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` |
| `.` | Go to last modification in current file | `goto_last_modification` |
#### Match mode
@ -172,7 +180,7 @@ #### Match mode
| 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` |
| `r` `<from><to>` | Replace surround character `<from>` with `<to>` | `surround_replace` |
| `d` `<char>` | Delete surround character `<char>` | `surround_delete` |
@ -195,49 +203,76 @@ #### Window mode
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
| `q`, `Ctrl-q` | Close current window | `wclose` |
| `o`, `Ctrl-o` | Only keep the current window, closing all the others | `wonly` |
#### Space mode
This layer is a kludge of mappings, mostly pickers.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `k` | Show documentation for the item under the cursor | `hover` |
| `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` |
| `s` | Open symbol picker (current document) | `symbol_picker` |
| `a` | Apply code action | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` |
| `/` | Global search in workspace folder | `global_search` |
> NOTE: Global search display results in a fuzzy picker, use `space + '` to bring it back up after opening a file.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `f` | Open file picker | `file_picker` |
| `b` | Open buffer picker | `buffer_picker` |
| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` |
| `s` | Open document symbol picker (**LSP**) | `symbol_picker` |
| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` |
| `r` | Rename symbol (**LSP**) | `rename_symbol` |
| `a` | Apply code action (**LSP**) | `code_action` |
| `'` | Open last fuzzy picker | `last_picker` |
| `w` | Enter [window mode](#window-mode) | N/A |
| `p` | Paste system clipboard after selections | `paste_clipboard_after` |
| `P` | Paste system clipboard before selections | `paste_clipboard_before` |
| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` |
| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` |
| `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
Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired).
| Key | Description | Command |
| ----- | ----------- | ------- |
| `[d` | Go to previous diagnostic | `goto_prev_diag` |
| `]d` | Go to next diagnostic | `goto_next_diag` |
| `[D` | Go to first diagnostic in document | `goto_first_diag` |
| `]D` | Go to last diagnostic in document | `goto_last_diag` |
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `[d` | Go to previous diagnostic (**LSP**) | `goto_prev_diag` |
| `]d` | Go to next diagnostic (**LSP**) | `goto_next_diag` |
| `[D` | Go to first diagnostic in document (**LSP**) | `goto_first_diag` |
| `]D` | Go to last diagnostic in document (**LSP**) | `goto_last_diag` |
| `[space` | Add newline above | `add_newline_above` |
| `]space` | Add newline below | `add_newline_below` |
## Insert Mode
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` |
| `Ctrl-w` | Delete previous word | `delete_word_backward` |
| Key | Description | Command |
| ----- | ----------- | ------- |
| `Escape` | Switch to normal mode | `normal_mode` |
| `Ctrl-x` | Autocomplete | `completion` |
| `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
@ -262,7 +297,9 @@ # Picker
| `Escape`, `Ctrl-c` | Close picker |
# Prompt
Keys to use within prompt, Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Escape`, `Ctrl-c` | Close prompt |
@ -270,15 +307,18 @@ # Prompt
| `Ctrl-b`, `Left` | Backward a char |
| `Alt-f`, `Alt-Right` | Forward a word |
| `Ctrl-f`, `Right` | Forward a char |
| `Ctrl-e`, `End` | move prompt end |
| `Ctrl-a`, `Home` | move prompt start |
| `Ctrl-w` | delete previous word |
| `Ctrl-k` | delete to end of line |
| `backspace` | delete previous char |
| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | select previous history |
| `Ctrl-n`, `Down` | select next history |
| `Tab` | slect next completion item |
| `BackTab` | slect previous completion item |
| `Ctrl-e`, `End` | Move prompt end |
| `Ctrl-a`, `Home` | Move prompt start |
| `Ctrl-w` | Delete previous word |
| `Alt-d` | Delete next word |
| `Ctrl-u` | Delete to start of line |
| `Ctrl-k` | Delete to end of line |
| `backspace`, `Ctrl-h` | Delete previous char |
| `delete`, `Ctrl-d` | Delete previous char |
| `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `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 |

14
book/src/languages.md Normal file
View File

@ -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
```

View File

@ -15,6 +15,7 @@ # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
w = "move_line_up" # Maps the 'w' key move_line_up
"C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line
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]
"A-x" = "normal_mode" # Maps Alt-X to enter normal mode
@ -38,6 +39,7 @@ # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select'
| Left | `"left"` |
| Right | `"right"` |
| Up | `"up"` |
| Down | `"down"` |
| Home | `"home"` |
| End | `"end"` |
| Page | `"pageup"` |

View File

@ -62,6 +62,7 @@ ## Textobjects
| Key after `mi` or `ma` | Textobject selected |
| --- | --- |
| `w` | Word |
| `W` | WORD |
| `(`, `[`, `'`, etc | Specified surround pairs |
| `f` | Function |
| `c` | Class |

View File

@ -1,4 +1,4 @@
use crate::{ChangeSet, Rope, State, Transaction};
use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction};
use once_cell::sync::Lazy;
use regex::Regex;
use std::num::NonZeroUsize;
@ -133,6 +133,32 @@ pub fn redo(&mut self) -> Option<&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 {
use std::collections::HashSet;
let mut a_path_set = HashSet::new();
@ -256,7 +282,7 @@ pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
}
/// Whether to undo by a number of edits or a duration of time.
#[derive(Debug, PartialEq)]
#[derive(Debug, PartialEq, Clone, Copy)]
pub enum UndoKind {
Steps(usize),
TimePeriod(std::time::Duration),

View File

@ -450,6 +450,7 @@ pub fn change<I>(document: &Document, changes: I) -> Self
language: vec![LanguageConfiguration {
scope: "source.rust".to_string(),
file_types: vec!["rs".to_string()],
shebangs: vec![],
language_id: "Rust".to_string(),
highlight_config: OnceCell::new(),
config: None,

View File

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

View File

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

View File

@ -168,7 +168,7 @@ pub fn backwards_skip_while<F>(slice: RopeSlice, pos: usize, fun: F) -> Option<u
where
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();
backwards.find_map(|(i, c)| {
if !fun(c) {

499
helix-core/src/numbers.rs Normal file
View File

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

View File

@ -40,7 +40,6 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
/// needs to improve on.
/// 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 {
let path = expand_tilde(path);
let mut components = path.components().peekable();
let mut ret = if let Some(c @ Component::Prefix(..)) = components.peek().cloned() {
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
/// here if the path exists, just normalize it's components.
pub fn get_canonicalized_path(path: &Path) -> std::io::Result<PathBuf> {
let path = expand_tilde(path);
let path = if path.is_relative() {
std::env::current_dir().map(|current_dir| current_dir.join(path))?
} else {
path.to_path_buf()
path
};
Ok(get_normalized_path(path.as_path()))

View File

@ -360,7 +360,7 @@ pub fn push(mut self, range: Range) -> Self {
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 {
assert!(
self.ranges.len() > 1,
@ -528,14 +528,15 @@ fn into_iter(self) -> std::slice::Iter<'a, Range> {
// TODO: checkSelection -> check if valid for doc length && sorted
pub fn keep_matches(
pub fn keep_or_remove_matches(
text: RopeSlice,
selection: &Selection,
regex: &crate::regex::Regex,
remove: bool,
) -> Option<Selection> {
let result: SmallVec<_> = selection
.iter()
.filter(|range| regex.is_match(&range.fragment(text)))
.filter(|range| regex.is_match(&range.fragment(text)) ^ remove)
.copied()
.collect();

View File

@ -40,18 +40,21 @@ fn deserialize_lsp_config<'de, D>(deserializer: D) -> Result<Option<serde_json::
}
#[derive(Debug, Serialize, Deserialize)]
#[serde(deny_unknown_fields)]
pub struct Configuration {
pub language: Vec<LanguageConfiguration>,
}
// largely based on tree-sitter/cli/src/loader.rs
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[serde(rename_all = "kebab-case", deny_unknown_fields)]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
pub language_id: String,
pub scope: String, // source.rust
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 comment_token: Option<String>,
@ -309,6 +312,7 @@ pub struct Loader {
// highlight_names ?
language_configs: Vec<Arc<LanguageConfiguration>>,
language_config_ids_by_file_type: HashMap<String, usize>, // Vec<usize>
language_config_ids_by_shebang: HashMap<String, usize>,
}
impl Loader {
@ -316,6 +320,7 @@ pub fn new(config: Configuration) -> Self {
let mut loader = Self {
language_configs: Vec::new(),
language_config_ids_by_file_type: HashMap::new(),
language_config_ids_by_shebang: HashMap::new(),
};
for config in config.language {
@ -328,6 +333,11 @@ pub fn new(config: Configuration) -> Self {
.language_config_ids_by_file_type
.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));
}
@ -353,6 +363,17 @@ pub fn language_config_for_file_name(&self, path: &Path) -> Option<Arc<LanguageC
// 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>> {
self.language_configs
.iter()

View File

@ -10,7 +10,7 @@
use crate::syntax::LanguageConfiguration;
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};
let iter = match direction {
@ -33,7 +33,7 @@ fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) ->
match categorize_char(ch) {
Eol | Whitespace => return pos,
category => {
if category != prev_category && pos != 0 && pos != slice.len_chars() {
if !long && category != prev_category && pos != 0 && pos != slice.len_chars() {
return pos;
} else {
match direction {
@ -70,13 +70,14 @@ pub fn textobject_word(
range: Range,
textobject: TextObject,
_count: usize,
long: bool,
) -> Range {
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) {
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.
@ -268,7 +269,7 @@ fn test_textobject_word() {
let slice = doc.slice(..);
for &case in scenario {
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!(
result,
expected_range.into(),

View File

@ -257,6 +257,12 @@ pub(crate) async fn initialize(&self) -> Result<lsp::InitializeResult> {
content_format: Some(vec![lsp::MarkupKind::Markdown]),
..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_literal_support: Some(lsp::CodeActionLiteralSupport {
code_action_kind: lsp::CodeActionKindLiteralSupport {
@ -773,4 +779,25 @@ pub fn code_actions(
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

12
helix-term/build.rs Normal file
View File

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

View File

@ -9,7 +9,7 @@
use log::{error, warn};
use std::{
io::{stdout, Write},
io::{stdin, stdout, Write},
sync::Arc,
time::{Duration, Instant},
};
@ -19,6 +19,7 @@
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
execute, terminal,
tty::IsTty,
};
#[cfg(not(windows))]
use {
@ -62,14 +63,19 @@ pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
std::sync::Arc::new(theme::Loader::new(&conf_dir, &helix_core::runtime_dir()));
// load default and user config, and merge both
let def_lang_conf: toml::Value = toml::from_slice(include_bytes!("../../languages.toml"))
.expect("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 builtin_err_msg =
"Could not parse built-in languages.toml, something must be very wrong";
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()
.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 {
Some(value) => merge_toml_values(def_lang_conf, value),
None => def_lang_conf,
Some(Ok(value)) => Ok(merge_toml_values(def_lang_conf, value)),
Some(err @ Err(_)) => err,
None => Ok(def_lang_conf),
};
let theme = if let Some(theme) = &config.theme {
@ -85,8 +91,15 @@ pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
};
let syn_loader_conf: helix_core::syntax::Configuration = lang_conf
.try_into()
.expect("Could not parse merged (built-in + user) languages.toml");
.and_then(|conf| conf.try_into())
.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 mut editor = Editor::new(
@ -124,8 +137,17 @@ pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
}
editor.set_status(format!("Loaded {} files.", nr_of_files));
}
} else {
} else if stdin().is_tty() {
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);
@ -253,12 +275,8 @@ pub fn handle_idle_timeout(&mut self) {
}
let editor_view = self
.compositor
.find(std::any::type_name::<ui::EditorView>())
.find::<ui::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() {
return;
@ -583,12 +601,8 @@ pub async fn handle_language_server_message(
{
let editor_view = self
.compositor
.find(std::any::type_name::<ui::EditorView>())
.find::<ui::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::ProgressParamsValue::WorkDone(work) = value;
@ -702,12 +716,8 @@ pub async fn handle_language_server_message(
let editor_view = self
.compositor
.find(std::any::type_name::<ui::EditorView>())
.find::<ui::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);
if spinner.is_stopped() {
spinner.start();
@ -742,7 +752,7 @@ fn restore_term(&mut self) -> Result<(), Error> {
Ok(())
}
pub async fn run(&mut self) -> Result<(), Error> {
pub async fn run(&mut self) -> Result<i32, Error> {
self.claim_term().await?;
// Exit the alternate screen and disable raw mode before panicking
@ -765,6 +775,6 @@ pub async fn run(&mut self) -> Result<(), Error> {
self.restore_term()?;
Ok(())
Ok(self.editor.exit_code)
}
}

File diff suppressed because it is too large Load Diff

View File

@ -177,11 +177,12 @@ pub fn has_component(&self, type_name: &str) -> bool {
.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
.iter_mut()
.find(|component| component.type_name() == type_name)
.map(|component| component.as_mut())
.and_then(|component| component.as_any_mut().downcast_mut())
}
}

View File

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

View File

@ -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:
///
/// ```
@ -53,6 +101,10 @@ macro_rules! keymap {
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,)+ }
) => {
@ -64,10 +116,11 @@ macro_rules! keymap {
$(
$(
let _key = $key.parse::<::helix_view::input::KeyEvent>().unwrap();
_map.insert(
let _duplicate = _map.insert(
_key,
keymap!(@trie $value)
);
debug_assert!(_duplicate.is_none(), "Duplicate key found: {:?}", _duplicate.unwrap());
_order.push(_key);
)+
)*
@ -147,6 +200,7 @@ pub fn infobox(&self) -> Info {
cmd.doc()
}
KeyTrie::Node(n) => n.name(),
KeyTrie::Sequence(_) => "[Multiple commands]",
};
match body.iter().position(|(d, _)| d == &desc) {
Some(pos) => {
@ -207,6 +261,7 @@ fn deref_mut(&mut self) -> &mut Self::Target {
#[serde(untagged)]
pub enum KeyTrie {
Leaf(Command),
Sequence(Vec<Command>),
Node(KeyTrieNode),
}
@ -214,14 +269,14 @@ impl KeyTrie {
pub fn node(&self) -> Option<&KeyTrieNode> {
match *self {
KeyTrie::Node(ref node) => Some(node),
KeyTrie::Leaf(_) => None,
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
}
}
pub fn node_mut(&mut self) -> Option<&mut KeyTrieNode> {
match *self {
KeyTrie::Node(ref mut node) => Some(node),
KeyTrie::Leaf(_) => None,
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
}
}
@ -238,7 +293,7 @@ pub fn search(&self, keys: &[KeyEvent]) -> Option<&KeyTrie> {
trie = match trie {
KeyTrie::Node(map) => map.get(key),
// leaf encountered while keys left to process
KeyTrie::Leaf(_) => None,
KeyTrie::Leaf(_) | KeyTrie::Sequence(_) => None,
}?
}
Some(trie)
@ -250,6 +305,8 @@ pub enum KeymapResultKind {
/// Needs more keys to execute a command. Contains valid keys for next keystroke.
Pending(KeyTrieNode),
Matched(Command),
/// Matched a sequence of commands to execute.
MatchedSequence(Vec<Command>),
/// Key was not found in the root keymap
NotFound,
/// Key is invalid in combination with previous keys. Contains keys leading upto
@ -332,6 +389,12 @@ pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
Some(&KeyTrie::Leaf(cmd)) => {
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()),
Some(t) => t,
};
@ -349,6 +412,13 @@ pub fn get(&mut self, key: KeyEvent) -> KeymapResult {
self.state.clear();
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(
KeymapResultKind::Cancelled(self.state.drain(..).collect()),
self.sticky(),
@ -455,6 +525,7 @@ fn default() -> Keymaps {
"a" => goto_last_accessed_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
"." => goto_last_modification,
},
":" => command_mode,
@ -511,6 +582,8 @@ fn default() -> Keymaps {
"u" => undo,
"U" => redo,
"A-u" => earlier,
"A-U" => later,
"y" => yank,
// yank_all
@ -523,7 +596,7 @@ fn default() -> Keymaps {
"=" => format_selections,
"J" => join_selections,
"K" => keep_selections,
// TODO: and another method for inverse
"A-K" => remove_selections,
"," => keep_primary_selection,
"A-," => remove_primary_selection,
@ -532,7 +605,7 @@ fn default() -> Keymaps {
// "Q" => replay_macro,
// & align selections
// _ trim selections
"_" => trim_selections,
"(" => rotate_selections_backward,
")" => rotate_selections_forward,
@ -550,6 +623,7 @@ fn default() -> Keymaps {
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
"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,
@ -569,6 +643,7 @@ fn default() -> Keymaps {
"f" => file_picker,
"b" => buffer_picker,
"s" => symbol_picker,
"S" => workspace_symbol_picker,
"a" => code_action,
"'" => last_picker,
"d" => { "Debug" sticky=true
@ -593,9 +668,14 @@ fn default() -> Keymaps {
},
"w" => { "Window"
"C-w" | "w" => rotate_view,
"C-h" | "h" => hsplit,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
"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_main_selection_to_clipboard,
@ -604,30 +684,31 @@ fn default() -> Keymaps {
"R" => replace_selections_with_clipboard,
"/" => global_search,
"k" => hover,
"r" => rename_symbol,
},
"z" => { "View"
"z" | "c" => align_view_center,
"t" => align_view_top,
"b" => align_view_bottom,
"m" => align_view_middle,
"k" => scroll_up,
"j" => scroll_down,
"b" => page_up,
"f" => page_down,
"u" => half_page_up,
"d" => half_page_down,
"k" | "up" => scroll_up,
"j" | "down" => scroll_down,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
"C-u" => half_page_up,
"C-d" => half_page_down,
},
"Z" => { "View" sticky=true
"z" | "c" => align_view_center,
"t" => align_view_top,
"b" => align_view_bottom,
"m" => align_view_middle,
"k" => scroll_up,
"j" => scroll_down,
"b" => page_up,
"f" => page_down,
"u" => half_page_up,
"d" => half_page_down,
"k" | "up" => scroll_up,
"j" | "down" => scroll_down,
"C-b" | "pageup" => page_up,
"C-f" | "pagedown" => page_down,
"C-u" => half_page_up,
"C-d" => half_page_down,
},
"\"" => select_register,
@ -637,6 +718,9 @@ fn default() -> Keymaps {
"A-!" => shell_append_output,
"$" => shell_keep_pipe,
"C-z" => suspend,
"C-a" => increment,
"C-x" => decrement,
});
let mut select = normal.clone();
select.merge_nodes(keymap!({ "Select mode"
@ -670,21 +754,38 @@ fn default() -> Keymaps {
"esc" => normal_mode,
"backspace" => delete_char_backward,
"C-h" => delete_char_backward,
"del" => delete_char_forward,
"C-d" => delete_char_forward,
"ret" => insert_newline,
"tab" => insert_tab,
"C-w" => delete_word_backward,
"A-d" => delete_word_forward,
"left" => move_char_left,
"C-b" => move_char_left,
"down" => move_line_down,
"C-n" => move_line_down,
"up" => move_line_up,
"C-p" => move_line_up,
"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,
"pagedown" => page_down,
"home" => goto_line_start,
"C-a" => goto_line_start,
"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-r" => insert_register,
});
Keymaps(hashmap!(
Mode::Normal => Keymap::new(normal),
@ -706,6 +807,22 @@ pub fn merge_keys(mut config: Config) -> Config {
#[cfg(test)]
mod tests {
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]
fn merge_partial_keys() {
let config = Config {
@ -800,4 +917,20 @@ fn order_should_be_set() {
let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
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`"
);
}
}

View File

@ -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
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(logpath)?;
let file_config = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
@ -31,15 +26,20 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message
))
})
.chain(file);
.chain(fern::log_file(logpath)?);
base_config.chain(file_config).apply()?;
Ok(())
}
fn main() -> Result<()> {
let exit_code = main_impl()?;
std::process::exit(exit_code);
}
#[tokio::main]
async fn main() -> Result<()> {
async fn main_impl() -> Result<i32> {
let cache_dir = helix_core::cache_dir();
if !cache_dir.exists() {
std::fs::create_dir_all(&cache_dir).ok();
@ -66,7 +66,7 @@ async fn main() -> Result<()> {
-V, --version Prints version information
",
env!("CARGO_PKG_NAME"),
env!("CARGO_PKG_VERSION"),
env!("VERSION_AND_GIT_HASH"),
env!("CARGO_PKG_AUTHORS"),
env!("CARGO_PKG_DESCRIPTION"),
logpath.display(),
@ -81,7 +81,7 @@ async fn main() -> Result<()> {
}
if args.display_version {
println!("helix {}", env!("CARGO_PKG_VERSION"));
println!("helix {}", env!("VERSION_AND_GIT_HASH"));
std::process::exit(0);
}
@ -91,7 +91,16 @@ async fn main() -> Result<()> {
}
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) => return Err(Error::new(err)),
};
@ -100,7 +109,8 @@ async fn main() -> Result<()> {
// 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")?;
app.run().await.unwrap();
Ok(())
let exit_code = app.run().await?;
Ok(exit_code)
}

View File

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

View File

@ -13,7 +13,7 @@
Rope,
};
use helix_view::{
graphics::{Color, Margin, Rect, Style},
graphics::{Margin, Rect},
Theme,
};
@ -61,9 +61,15 @@ fn to_span(text: pulldown_cmark::CowStr) -> Span {
})
}
let text_style = Style::default().fg(Color::Rgb(164, 160, 232)); // lavender
let code_style = Style::default().fg(Color::Rgb(255, 255, 255)); // white
let heading_style = Style::default().fg(Color::Rgb(219, 191, 239)); // lilac
let text_style = theme.map(|theme| theme.get("ui.text")).unwrap_or_default();
// 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 {
match event {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -25,6 +25,8 @@
const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
pub const SCRATCH_BUFFER_NAME: &str = "[scratch]";
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode {
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 back as it separated from the edits. We could split out the parts manually but that will
// be more troublesome.
history: Cell<History>,
pub history: Cell<History>,
pub savepoint: Option<Transaction>,
@ -494,7 +496,9 @@ fn save_impl<F: Future<Output = LspFormatting>>(
/// Detect the programming language based on the file type.
pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax::Loader) {
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);
}
}
@ -749,19 +753,35 @@ pub fn restore(&mut self, view_id: ViewId) {
}
/// 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 mut success = false;
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`.
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 mut success = false;
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

View File

@ -1,5 +1,6 @@
use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
document::SCRATCH_BUFFER_NAME,
graphics::{CursorKind, Rect},
theme::{self, Theme},
tree::{self, Tree},
@ -11,8 +12,8 @@
use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
collections::BTreeMap,
collections::HashMap,
collections::{BTreeMap, HashMap},
io::stdin,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@ -25,8 +26,8 @@
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
use helix_core::syntax;
use helix_core::Position;
use helix_dap as dap;
use helix_core::{Position, Selection};
use serde::Deserialize;
@ -39,7 +40,7 @@ fn deserialize_duration_millis<'de, D>(deserializer: D) -> Result<Duration, D::E
}
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", default)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
pub struct Config {
/// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5.
pub scrolloff: usize,
@ -138,6 +139,8 @@ pub struct Editor {
pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>,
pub exit_code: i32,
}
#[derive(Debug, Copy, Clone)]
@ -179,6 +182,7 @@ pub fn new(
idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
config,
exit_code: 0,
}
}
@ -208,6 +212,12 @@ pub fn set_error(&mut self, error: String) {
}
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();
for config in self
.syn_loader
@ -238,9 +248,28 @@ fn _refresh(&mut self) {
}
}
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) {
use crate::tree::Layout;
use helix_core::Selection;
if !self.documents.contains_key(&id) {
log::error!("cannot switch to document that does not exist (anymore)");
@ -274,26 +303,19 @@ pub fn switch(&mut self, id: DocumentId, action: Action) {
view.jumps.push(jump);
view.last_accessed_doc = Some(view.doc);
}
view.doc = id;
view.offset = Position::default();
let (view, doc) = current!(self);
// 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);
let view_id = view.id;
self.replace_document_in_view(view_id, id);
return;
}
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;
}
Action::HorizontalSplit => {
@ -315,16 +337,29 @@ pub fn switch(&mut self, id: DocumentId, action: Action) {
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);
self.next_document_id += 1;
let mut doc = Document::default();
doc.id = id;
self.documents.insert(id, doc);
document.id = id;
self.documents.insert(id, document);
id
}
fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
let id = self.new_document(document);
self.switch(id, action);
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> {
let path = helix_core::path::get_canonicalized_path(&path)?;
@ -381,7 +416,7 @@ pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Erro
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);
// remove selection
self.documents
@ -390,20 +425,68 @@ pub fn close(&mut self, id: ViewId, close_buffer: bool) {
.selections
.remove(&id);
if close_buffer {
// get around borrowck issues
let doc = &self.documents[&view.doc];
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
self.documents.remove(&view.doc);
}
self.tree.remove(id);
self._refresh();
}
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
let doc = match self.documents.get(&doc_id) {
Some(doc) => 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._refresh();
Ok(())
}
pub fn resize(&mut self, area: Rect) {
if self.tree.resize(area) {
self._refresh();

View File

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

View File

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

View File

@ -54,6 +54,10 @@ pub fn backward(&mut self, view_id: ViewId, doc: &mut Document, count: usize) ->
None
}
}
pub fn remove(&mut self, doc_id: &DocumentId) {
self.jumps.retain(|(other_id, _)| other_id != doc_id);
}
}
#[derive(Debug)]
@ -85,7 +89,12 @@ pub fn inner_area(&self) -> Rect {
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
.selection(self.id)
.primary()
@ -104,23 +113,43 @@ pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: 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
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 {
// 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
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 {
// 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
#[inline]
pub fn last_line(&self, doc: &Document) -> usize {

View File

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

View File

@ -0,0 +1 @@
; inherits: c

View File

@ -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_")
)

View File

@ -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",
"{",
"}"
]

View File

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

View File

@ -0,0 +1 @@
; inherits: c

View File

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

View File

@ -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

View File

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

View File

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

View File

@ -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"

View File

@ -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'

View File

@ -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"

View File

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