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

imgbot
Blaž Hrastnik 3 years ago
commit d1854d8e6a

8
.gitmodules vendored

@ -134,3 +134,11 @@
path = helix-syntax/languages/tree-sitter-cmake
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

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

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

@ -8,5 +8,7 @@
- [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)

@ -0,0 +1,60 @@
# Adding languages
## Submodules
To add a new langauge, you should first add a tree-sitter submodule. To do this,
you can run the command
```sh
git submodule add -f <repository> helix-syntax/languages/tree-sitter-<name>
```
For example, to add tree-sitter-ocaml you would run
```sh
git submodule add -f https://github.com/tree-sitter/tree-sitter-ocaml helix-syntax/languages/tree-sitter-ocaml
```
Make sure the submodule is shallow by doing
```sh
git config -f .gitmodules submodule.helix-syntax/languages/tree-sitter-<name>.shallow true
```
or you can manually add `shallow = true` to `.gitmodules`.
## languages.toml
Next, you need to add the language to the [`languages.toml`][languages.toml] found in the root of
the repository; this `languages.toml` file is included at compilation time, and
is distinct from the `language.toml` file in the user's [configuration
directory](../configuration.md).
These are the available keys and descriptions for the file.
| Key | Description |
| ---- | ----------- |
| name | The name of the language |
| scope | A string like `source.js` that identifies the language. Currently, we strive to match the scope names used by popular TextMate grammars and by the Linguist library. Usually `source.<name>` or `text.<name>` in case of markup languages |
| injection-regex | regex pattern that will be tested against a language name in order to determine whether this language should be used for a potential [language injection][treesitter-language-injection] site. |
| file-types | The filetypes of the language, for example `["yml", "yaml"]` |
| shebangs | The interpreters from the shebang line, for example `["sh", "bash"]` |
| roots | A set of marker files to look for when trying to find the workspace root. For example `Cargo.lock`, `yarn.lock` |
| auto-format | Whether to autoformat this language when saving |
| comment-token | The token to use as a comment-token |
| indent | The indent to use. Has sub keys `tab-width` and `unit` |
| config | Language server configuration |
## Queries
For a language to have syntax-highlighting and indentation among other things, you have to add queries. Add a directory for your language with the path `runtime/queries/<name>/`. The tree-sitter [website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) gives more info on how to write queries.
## Common Issues
- If you get errors when building after switching branches, you may have to remove or update tree-sitter submodules. You can update submodules by running
```sh
git submodule sync; git submodule update --init
```
- Make sure to not use the `--remote` flag. To remove submodules look inside the `.gitmodules` and remove directories that are not present inside of it.
- If a parser is segfaulting or you want to remove the parser, make sure to remove the submodule *and* the compiled parser in `runtime/grammar/<name>.so`
- The indents query is `indents.toml`, *not* `indents.scm`. See [this](https://github.com/helix-editor/helix/issues/114) issue for more information.
[treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection
[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml

@ -5,7 +5,7 @@ require an accompanying tree-sitter grammar and a `textobjects.scm` query file
to work properly. Tree-sitter allows us to query the source code syntax tree
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

@ -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 @@
| `.` | 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 @@
| `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 @@
| `%` | 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 @@ key to return to normal mode after usage (useful when you're simply looking
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 @@ Jumps to various locations.
| `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 @@ and [textobject](./usage.md#textobject) usage.
| 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 @@ This layer is similar to vim keybindings as kakoune does not support window.
| `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 @@ Keys to use within picker. Remapping currently not supported.
| `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 @@ Keys to use within prompt, Remapping currently not supported.
| `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 |

@ -0,0 +1,14 @@
# Languages
Language-specific settings and settings for particular language servers can be configured in a `languages.toml` file placed in your [configuration directory](./configuration.md). Helix actually uses two `languages.toml` files, the [first one](https://github.com/helix-editor/helix/blob/master/languages.toml) is in the main helix repository; it contains the default settings for each language and is included in the helix binary at compile time. Users who want to see the available settings and options can either reference the helix repo's `languages.toml` file, or consult the table in the [adding languages](./guides/adding_languages.md) section.
Changes made to the `languages.toml` file in a user's [configuration directory](./configuration.md) are merged with helix's defaults on start-up, such that a user's settings will take precedence over defaults in the event of a collision. For example, the default `languages.toml` sets rust's `auto-format` to `true`. If a user wants to disable auto-format, they can change the `languages.toml` in their [configuration directory](./configuration.md) to make the rust entry read like the example below; the new key/value pair `auto-format = false` will override the default when the two sets of settings are merged on start-up:
```
# in <config_dir>/helix/languages.toml
[[language]]
name = "rust"
auto-format = false
```

@ -15,6 +15,7 @@ a = "move_char_left" # Maps the 'a' key to the move_char_left command
w = "move_line_up" # Maps the 'w' key move_line_up
"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 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
| Left | `"left"` |
| Right | `"right"` |
| Up | `"up"` |
| Down | `"down"` |
| Home | `"home"` |
| End | `"end"` |
| Page | `"pageup"` |

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

@ -1,4 +1,4 @@
use crate::{ChangeSet, Rope, State, Transaction};
use crate::{Assoc, ChangeSet, Range, Rope, State, Transaction};
use once_cell::sync::Lazy;
use regex::Regex;
use std::num::NonZeroUsize;
@ -133,6 +133,32 @@ impl History {
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 @@ impl History {
}
/// 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),

@ -450,6 +450,7 @@ where
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,

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

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

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

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

@ -40,7 +40,6 @@ pub fn expand_tilde(path: &Path) -> PathBuf {
/// needs to improve on.
/// 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()))

@ -360,7 +360,7 @@ impl Selection {
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 @@ impl<'a> IntoIterator for &'a Selection {
// 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();

@ -40,18 +40,21 @@ where
}
#[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 @@ impl Loader {
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 @@ impl Loader {
.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 @@ impl Loader {
// 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()

@ -10,7 +10,7 @@ use crate::surround;
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 @@ mod test {
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(),

@ -257,6 +257,12 @@ impl Client {
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 @@ impl Client {
self.call::<lsp::request::CodeActionRequest>(params)
}
pub async fn rename_symbol(
&self,
text_document: lsp::TextDocumentIdentifier,
position: lsp::Position,
new_name: String,
) -> anyhow::Result<lsp::WorkspaceEdit> {
let params = lsp::RenameParams {
text_document_position: lsp::TextDocumentPositionParams {
text_document,
position,
},
new_name,
work_done_progress_params: lsp::WorkDoneProgressParams {
work_done_token: None,
},
};
let response = self.request::<lsp::request::Rename>(params).await?;
Ok(response.unwrap_or_default())
}
}

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

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

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

@ -9,7 +9,7 @@ use crate::{
use log::{error, warn};
use std::{
io::{stdout, Write},
io::{stdin, stdout, Write},
sync::Arc,
time::{Duration, Instant},
};
@ -19,6 +19,7 @@ use anyhow::Error;
use crossterm::{
event::{DisableMouseCapture, EnableMouseCapture, Event, EventStream},
execute, terminal,
tty::IsTty,
};
#[cfg(not(windows))]
use {
@ -62,14 +63,19 @@ impl Application {
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 @@ impl Application {
};
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 @@ impl Application {
}
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 @@ impl Application {
}
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 @@ impl Application {
{
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 @@ impl Application {
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 @@ impl Application {
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 @@ impl Application {
self.restore_term()?;
Ok(())
Ok(self.editor.exit_code)
}
}

File diff suppressed because it is too large Load Diff

@ -177,11 +177,12 @@ impl Compositor {
.any(|component| component.type_name() == type_name)
}
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())
}
}

@ -3,6 +3,7 @@ use serde::Deserialize;
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,
}

@ -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 @@ impl KeyTrieNode {
cmd.doc()
}
KeyTrie::Node(n) => n.name(),
KeyTrie::Sequence(_) => "[Multiple commands]",
};
match body.iter().position(|(d, _)| d == &desc) {
Some(pos) => {
@ -207,6 +261,7 @@ impl DerefMut for KeyTrieNode {
#[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 @@ impl 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 @@ impl Keymap {
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 @@ impl Keymap {
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 @@ impl Default for Keymaps {
"a" => goto_last_accessed_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
"." => goto_last_modification,
},
":" => command_mode,
@ -511,6 +582,8 @@ impl Default for Keymaps {
"u" => undo,
"U" => redo,
"A-u" => earlier,
"A-U" => later,
"y" => yank,
// yank_all
@ -523,7 +596,7 @@ impl Default for 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 @@ impl Default for Keymaps {
// "Q" => replay_macro,
// & align selections
// _ trim selections
"_" => trim_selections,
"(" => rotate_selections_backward,
")" => rotate_selections_forward,
@ -550,6 +623,7 @@ impl Default for 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 @@ impl Default for 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 @@ impl Default for 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 @@ impl Default for 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 @@ impl Default for 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 @@ impl Default for 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 @@ mod tests {
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`"
);
}
}

@ -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 @@ FLAGS:
-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 @@ FLAGS:
}
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 @@ FLAGS:
}
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 @@ FLAGS:
// 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)
}

@ -17,7 +17,7 @@ use helix_core::{
};
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 @@ impl EditorView {
use tui::{
layout::Alignment,
text::Text,
widgets::{Paragraph, Widget},
widgets::{Paragraph, Widget, Wrap},
};
let cursor = doc
@ -665,8 +665,10 @@ impl EditorView {
}
}
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 @@ impl EditorView {
}
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 @@ impl EditorView {
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 @@ impl EditorView {
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

@ -13,7 +13,7 @@ use helix_core::{
Rope,
};
use helix_view::{
graphics::{Color, Margin, Rect, Style},
graphics::{Margin, Rect},
Theme,
};
@ -61,9 +61,15 @@ fn parse<'a>(
})
}
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 {

@ -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 @@ impl<T: Item + 'static> Component for Menu<T> {
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);
}

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

@ -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 @@ type FileLocation = (PathBuf, Option<(usize, usize)>);
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 @@ impl<T> FilePicker<T> {
) -> 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 @@ impl<T: 'static> Component for FilePicker<T> {
};
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 @@ impl<T> Picker<T> {
cursor: 0,
prompt,
render_centered,
truncate_start: true,
format_fn: Box::new(format_fn),
callback_fn: Box::new(callback_fn),
};
@ -403,81 +410,35 @@ impl<T: 'static> Component for Picker<T> {
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 @@ impl<T: 'static> Component for Picker<T> {
text_style
},
true,
true,
self.truncate_start,
);
}
}

@ -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 @@ impl<T: Component> Component for Popup<T> {
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)
}

@ -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 @@ impl Prompt {
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 @@ impl Prompt {
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 @@ impl Component for Prompt {
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 @@ impl Component for Prompt {
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 @@ impl Component for Prompt {
(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 @@ impl Component for Prompt {
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),

@ -25,6 +25,8 @@ const BUF_SIZE: usize = 8192;
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 @@ impl Document {
/// 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 @@ impl Document {
}
/// 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

@ -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 futures_util::stream::select_all::SelectAll;
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 @@ use anyhow::Error;
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 @@ where
}
#[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 @@ impl Editor {
idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
config,
exit_code: 0,
}
}
@ -208,6 +212,12 @@ impl Editor {
}
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 @@ impl Editor {
}
}
fn replace_document_in_view(&mut self, current_view: ViewId, doc_id: DocumentId) {
let view = self.tree.get_mut(current_view);
view.doc = doc_id;
view.offset = Position::default();
let doc = self.documents.get_mut(&doc_id).unwrap();
// initialize selection for view
doc.selections
.entry(view.id)
.or_insert_with(|| Selection::point(0));
// TODO: reuse align_view
let pos = doc
.selection(view.id)
.primary()
.cursor(doc.text().slice(..));
let line = doc.text().char_to_line(pos);
view.offset.row = line.saturating_sub(view.inner_area().height as usize / 2);
}
pub fn switch(&mut self, id: DocumentId, action: Action) {
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 @@ impl Editor {
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 @@ impl Editor {
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 @@ impl Editor {
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,18 +425,66 @@ impl Editor {
.selections
.remove(&id);
if close_buffer {
// get around borrowck issues
let doc = &self.documents[&view.doc];
self.tree.remove(id);
self._refresh();
}
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
self.documents.remove(&view.doc);
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.tree.remove(id);
self._refresh();
Ok(())
}
pub fn resize(&mut self, area: Rect) {

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

@ -314,6 +314,9 @@ impl Tree {
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;
}

@ -54,6 +54,10 @@ impl JumpList {
None
}
}
pub fn remove(&mut self, doc_id: &DocumentId) {
self.jumps.retain(|(other_id, _)| other_id != doc_id);
}
}
#[derive(Debug)]
@ -85,7 +89,12 @@ impl View {
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 @@ impl View {
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 {

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

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

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

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

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

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

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

@ -10,11 +10,11 @@
# Polar Night
# 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

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

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

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

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

Loading…
Cancel
Save