aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.github/ISSUE_TEMPLATE/bug_report.md2
-rw-r--r--.github/workflows/build.yml18
-rw-r--r--.gitmodules8
-rw-r--r--Cargo.lock37
-rw-r--r--README.md2
-rw-r--r--book/src/configuration.md12
-rw-r--r--book/src/install.md4
-rw-r--r--book/src/keymap.md78
-rw-r--r--book/src/remapping.md3
-rw-r--r--book/src/themes.md5
-rw-r--r--book/src/usage.md2
-rw-r--r--helix-core/src/diagnostic.rs4
-rw-r--r--helix-core/src/match_brackets.rs95
-rw-r--r--helix-core/src/register.rs14
-rw-r--r--helix-core/src/selection.rs6
-rw-r--r--helix-core/src/surround.rs148
-rw-r--r--helix-core/src/textobject.rs10
-rw-r--r--helix-lsp/Cargo.toml2
m---------helix-syntax/languages/tree-sitter-wgsl0
-rw-r--r--helix-term/src/application.rs4
-rw-r--r--helix-term/src/commands.rs328
-rw-r--r--helix-term/src/keymap.rs11
-rw-r--r--helix-term/src/ui/editor.rs289
-rw-r--r--helix-term/src/ui/markdown.rs7
-rw-r--r--helix-term/src/ui/mod.rs11
-rw-r--r--helix-view/src/editor.rs204
-rw-r--r--helix-view/src/gutter.rs95
-rw-r--r--helix-view/src/lib.rs15
-rw-r--r--helix-view/src/view.rs24
-rw-r--r--languages.toml25
-rw-r--r--runtime/queries/llvm/highlights.scm14
-rw-r--r--runtime/queries/wgsl/highlights.scm102
-rw-r--r--runtime/themes/solarized_dark.toml12
-rw-r--r--runtime/themes/solarized_light.toml12
34 files changed, 1097 insertions, 506 deletions
diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md
index 9b7c22e7..958407bb 100644
--- a/.github/ISSUE_TEMPLATE/bug_report.md
+++ b/.github/ISSUE_TEMPLATE/bug_report.md
@@ -17,7 +17,7 @@ Please search on the issue tracker before creating one. -->
### Environment
- Platform: <!-- macOS / Windows / Linux -->
-- Helix version: <!-- 'hx -v' if using a release, 'git describe' if building from master -->
+- Helix version: <!-- 'hx -V' if using a release, 'git describe' if building from master -->
<details><summary>~/.cache/helix/helix.log</summary>
diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index d4822f70..21629180 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -25,19 +25,19 @@ jobs:
override: true
- name: Cache cargo registry
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -64,19 +64,19 @@ jobs:
override: true
- name: Cache cargo registry
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
@@ -109,19 +109,19 @@ jobs:
components: rustfmt, clippy
- name: Cache cargo registry
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
- uses: actions/cache@v2.1.6
+ uses: actions/cache@v2.1.7
with:
path: target
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
diff --git a/.gitmodules b/.gitmodules
index bf596bdc..6295b9e9 100644
--- a/.gitmodules
+++ b/.gitmodules
@@ -142,3 +142,11 @@
path = helix-syntax/languages/tree-sitter-perl
url = https://github.com/ganezdragon/tree-sitter-perl
shallow = true
+[submodule "helix-syntax/languages/tree-sitter-wgsl"]
+ path = helix-syntax/languages/tree-sitter-wgsl
+ url = https://github.com/szebniok/tree-sitter-wgsl
+ shallow = true
+[submodule "helix-syntax/tree-sitter-llvm"]
+ path = helix-syntax/languages/tree-sitter-llvm
+ url = https://github.com/benwilliamgraham/tree-sitter-llvm
+ shallow = true
diff --git a/Cargo.lock b/Cargo.lock
index 2d64fb33..89c6388e 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -13,9 +13,9 @@ dependencies = [
[[package]]
name = "anyhow"
-version = "1.0.46"
+version = "1.0.51"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "3aa828229c44c0293dd7d4d2300bdfc4d2883ffdba934c069a6b968957a81f70"
+checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
[[package]]
name = "arc-swap"
@@ -258,15 +258,15 @@ dependencies = [
[[package]]
name = "futures-core"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
+checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
[[package]]
name = "futures-executor"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
+checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
dependencies = [
"futures-core",
"futures-task",
@@ -275,17 +275,16 @@ dependencies = [
[[package]]
name = "futures-task"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
+checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
[[package]]
name = "futures-util"
-version = "0.3.17"
+version = "0.3.18"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
+checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
dependencies = [
- "autocfg",
"futures-core",
"futures-task",
"pin-project-lite",
@@ -914,9 +913,9 @@ dependencies = [
[[package]]
name = "serde_json"
-version = "1.0.70"
+version = "1.0.72"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
+checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
dependencies = [
"itoa",
"ryu",
@@ -1086,9 +1085,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]]
name = "tokio"
-version = "1.13.1"
+version = "1.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "52963f91310c08d91cb7bff5786dfc8b79642ab839e188187e92105dbfb9d2c8"
+checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
dependencies = [
"autocfg",
"bytes",
@@ -1106,9 +1105,9 @@ dependencies = [
[[package]]
name = "tokio-macros"
-version = "1.5.0"
+version = "1.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
+checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
dependencies = [
"proc-macro2",
"quote",
@@ -1137,9 +1136,9 @@ dependencies = [
[[package]]
name = "tree-sitter"
-version = "0.20.0"
+version = "0.20.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9"
+checksum = "9394e9dbfe967b5f3d6ab79e302e78b5fb7b530c368d634ff3b8d67ede138bf1"
dependencies = [
"cc",
"regex",
diff --git a/README.md b/README.md
index faf5851e..3f4087b9 100644
--- a/README.md
+++ b/README.md
@@ -69,7 +69,7 @@ Contributors are very welcome! **No contribution is too small and all contributi
Some suggestions to get started:
-- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
+- You can look at the [good first issue](https://github.com/helix-editor/helix/issues?q=is%3Aopen+label%3AE-easy+label%3AE-good-first-issue) label on the issue tracker.
- Help with packaging on various distributions needed!
- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
diff --git a/book/src/configuration.md b/book/src/configuration.md
index be25441f..2ed48d51 100644
--- a/book/src/configuration.md
+++ b/book/src/configuration.md
@@ -24,6 +24,18 @@ To override global configuration parameters, create a `config.toml` file located
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `auto-info` | Whether to display infoboxes | `true` |
+`[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default.
+
+| Key | Description | Default |
+|--|--|---------|
+|`hidden` | Enables ignoring hidden files. | true
+|`parents` | Enables reading ignore files from parent directories. | true
+|`ignore` | Enables reading `.ignore` files. | true
+|`git-ignore` | Enables reading `.gitignore` files. | true
+|`git-global` | Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option. | true
+|`git-exclude` | Enables reading `.git/info/exclude` files. | true
+|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`.
+
## LSP
To display all language server messages in the status line add the following to your `config.toml`:
diff --git a/book/src/install.md b/book/src/install.md
index b9febbcc..d831934c 100644
--- a/book/src/install.md
+++ b/book/src/install.md
@@ -25,9 +25,7 @@ shell for working on Helix.
Releases are available in the `community` repository.
-Packages are also available on AUR:
-- [helix-bin](https://aur.archlinux.org/packages/helix-bin/) contains the pre-built release
-- [helix-git](https://aur.archlinux.org/packages/helix-git/) builds the master branch
+A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
## Build from source
diff --git a/book/src/keymap.md b/book/src/keymap.md
index c88ed767..865a700b 100644
--- a/book/src/keymap.md
+++ b/book/src/keymap.md
@@ -45,44 +45,46 @@
### Changes
-| Key | Description | Command |
-| ----- | ----------- | ------- |
-| `r` | Replace with a character | `replace` |
-| `R` | Replace with yanked text | `replace_with_yanked` |
-| `~` | Switch case of the selected text | `switch_case` |
-| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
-| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
-| `i` | Insert before selection | `insert_mode` |
-| `a` | Insert after selection (append) | `append_mode` |
-| `I` | Insert at the start of the line | `prepend_to_line` |
-| `A` | Insert at the end of the line | `append_to_line` |
-| `o` | Open new line below selection | `open_below` |
-| `O` | Open new line above selection | `open_above` |
-| `.` | 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 (**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` |
+| Key | Description | Command |
+| ----- | ----------- | ------- |
+| `r` | Replace with a character | `replace` |
+| `R` | Replace with yanked text | `replace_with_yanked` |
+| `~` | Switch case of the selected text | `switch_case` |
+| `` ` `` | Set the selected text to lower case | `switch_to_lowercase` |
+| `` Alt-` `` | Set the selected text to upper case | `switch_to_uppercase` |
+| `i` | Insert before selection | `insert_mode` |
+| `a` | Insert after selection (append) | `append_mode` |
+| `I` | Insert at the start of the line | `prepend_to_line` |
+| `A` | Insert at the end of the line | `append_to_line` |
+| `o` | Open new line below selection | `open_below` |
+| `O` | Open new line above selection | `open_above` |
+| `.` | 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 (**LSP**) | `format_selections` |
+| `d` | Delete selection | `delete_selection` |
+| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
+| `c` | Change selection (delete and enter insert mode) | `change_selection` |
+| `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` |
+| `Ctrl-a` | Increment object (number) under cursor | `increment` |
+| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
#### Shell
-| Key | Description | Command |
-| ------ | ----------- | ------- |
-| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
-| <code>A-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
-| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
-| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
+| Key | Description | Command |
+| ------ | ----------- | ------- |
+| <code>&#124;</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
+| <code>Alt-&#124;</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
+| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
+| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
### Selection manipulation
@@ -92,6 +94,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` |
+| `&` | Align selection in columns | `align_selections` |
| `_` | Trim whitespace from the selection | `trim_selections` |
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
@@ -157,6 +160,7 @@ Jumps to various locations.
| ----- | ----------- | ------- |
| `g` | Go to the start of the file | `goto_file_start` |
| `e` | Go to the end of the file | `goto_last_line` |
+| `f` | Go to files in the selection | `goto_file` |
| `h` | Go to the start of the line | `goto_line_start` |
| `l` | Go to the end of the line | `goto_line_end` |
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
@@ -199,6 +203,8 @@ This layer is similar to vim keybindings as kakoune does not support window.
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
+| `f` | Go to files in the selection in horizontal splits | `goto_file` |
+| `F` | Go to files in the selection in vertical splits | `goto_file` |
| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
@@ -314,7 +320,7 @@ Keys to use within prompt, Remapping currently not supported.
| `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 |
+| `delete`, `Ctrl-d` | Delete next 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 |
diff --git a/book/src/remapping.md b/book/src/remapping.md
index 532f502a..fffd189b 100644
--- a/book/src/remapping.md
+++ b/book/src/remapping.md
@@ -53,4 +53,5 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes
Keys can be disabled by binding them to the `no_op` command.
-Commands can be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs)
+Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands.
+> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `commands!` macro.
diff --git a/book/src/themes.md b/book/src/themes.md
index ecbbb6e9..fd3f5b1e 100644
--- a/book/src/themes.md
+++ b/book/src/themes.md
@@ -145,11 +145,12 @@ We use a similar set of scopes as
- `conditional` - `if`, `else`
- `repeat` - `for`, `while`, `loop`
- `import` - `import`, `export`
- - (TODO: return?)
+ - `return`
+ - `operator` - `or`, `in`
- `directive` - Preprocessor directives (`#if` in C)
- `function` - `fn`, `func`
-- `operator` - `||`, `+=`, `>`, `or`
+- `operator` - `||`, `+=`, `>`
- `function`
- `builtin`
diff --git a/book/src/usage.md b/book/src/usage.md
index 6b7cbc41..cf7d9d48 100644
--- a/book/src/usage.md
+++ b/book/src/usage.md
@@ -23,8 +23,10 @@ If there is a selected register before invoking a change or delete command, the
| `/` | Last search |
| `:` | Last executed command |
| `"` | Last yanked text |
+| `_` | Black hole |
> There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics.
+> The black hole register works as a no-op register, meaning no data will be written to / read from it.
## Surround
diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs
index ad1ba16a..4fcf51c9 100644
--- a/helix-core/src/diagnostic.rs
+++ b/helix-core/src/diagnostic.rs
@@ -1,7 +1,7 @@
//! LSP diagnostic utility types.
/// Describes the severity level of a [`Diagnostic`].
-#[derive(Debug, Eq, PartialEq)]
+#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum Severity {
Error,
Warning,
@@ -17,7 +17,7 @@ pub struct Range {
}
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
-#[derive(Debug)]
+#[derive(Debug, Clone)]
pub struct Diagnostic {
pub range: Range,
pub line: usize,
diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs
index 136ce320..cd554005 100644
--- a/helix-core/src/match_brackets.rs
+++ b/helix-core/src/match_brackets.rs
@@ -1,3 +1,5 @@
+use tree_sitter::Node;
+
use crate::{Rope, Syntax};
const PAIRS: &[(char, char)] = &[
@@ -6,50 +8,85 @@ const PAIRS: &[(char, char)] = &[
('[', ']'),
('<', '>'),
('\'', '\''),
- ('"', '"'),
+ ('\"', '\"'),
];
+
// limit matching pairs to only ( ) { } [ ] < >
+// Returns the position of the matching bracket under cursor.
+//
+// If the cursor is one the opening bracket, the position of
+// the closing bracket is returned. If the cursor in the closing
+// bracket, the position of the opening bracket is returned.
+//
+// If the cursor is not on a bracket, `None` is returned.
+#[must_use]
+pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
+ if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
+ return None;
+ }
+ find_pair(syntax, doc, pos, false)
+}
+
+// Returns the position of the bracket that is closing the current scope.
+//
+// If the cursor is on an opening or closing bracket, the function
+// behaves equivalent to [`find_matching_bracket`].
+//
+// If the cursor position is within a scope, the function searches
+// for the surrounding scope that is surrounded by brackets and
+// returns the position of the closing bracket for that scope.
+//
+// If no surrounding scope is found, the function returns `None`.
#[must_use]
-pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
+pub fn find_matching_bracket_fuzzy(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
+ find_pair(syntax, doc, pos, true)
+}
+
+fn find_pair(syntax: &Syntax, doc: &Rope, pos: usize, traverse_parents: bool) -> Option<usize> {
let tree = syntax.tree();
+ let pos = doc.char_to_byte(pos);
- let byte_pos = doc.char_to_byte(pos);
+ let mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
- // most naive implementation: find the innermost syntax node, if we're at the edge of a node,
- // return the other edge.
+ loop {
+ let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
+ let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
- let node = match tree
- .root_node()
- .named_descendant_for_byte_range(byte_pos, byte_pos)
- {
- Some(node) => node,
- None => return None,
- };
+ if is_valid_pair(doc, start_char, end_char) {
+ if end_byte == pos {
+ return Some(start_char);
+ }
+ // We return the end char if the cursor is either on the start char
+ // or at some arbitrary position between start and end char.
+ return Some(end_char);
+ }
- if node.is_error() {
- return None;
+ if traverse_parents {
+ node = node.parent()?;
+ } else {
+ return None;
+ }
}
+}
+fn is_valid_bracket(c: char) -> bool {
+ PAIRS.iter().any(|(l, r)| *l == c || *r == c)
+}
+
+fn is_valid_pair(doc: &Rope, start_char: usize, end_char: usize) -> bool {
+ PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
+}
+
+fn surrounding_bytes(doc: &Rope, node: &Node) -> Option<(usize, usize)> {
let len = doc.len_bytes();
+
let start_byte = node.start_byte();
- let end_byte = node.end_byte().saturating_sub(1); // it's end exclusive
+ let end_byte = node.end_byte().saturating_sub(1);
+
if start_byte >= len || end_byte >= len {
return None;
}
- let start_char = doc.byte_to_char(start_byte);
- let end_char = doc.byte_to_char(end_byte);
-
- if PAIRS.contains(&(doc.char(start_char), doc.char(end_char))) {
- if start_byte == byte_pos {
- return Some(end_char);
- }
-
- if end_byte == byte_pos {
- return Some(start_char);
- }
- }
-
- None
+ Some((start_byte, end_byte))
}
diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs
index c5444eb7..b9eb497d 100644
--- a/helix-core/src/register.rs
+++ b/helix-core/src/register.rs
@@ -15,7 +15,11 @@ impl Register {
}
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
- Self { name, values }
+ if name == '_' {
+ Self::new(name)
+ } else {
+ Self { name, values }
+ }
}
pub const fn name(&self) -> char {
@@ -27,11 +31,15 @@ impl Register {
}
pub fn write(&mut self, values: Vec<String>) {
- self.values = values;
+ if self.name != '_' {
+ self.values = values;
+ }
}
pub fn push(&mut self, value: String) {
- self.values.push(value);
+ if self.name != '_' {
+ self.values.push(value);
+ }
}
}
diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs
index b4d1dffa..116a1c7c 100644
--- a/helix-core/src/selection.rs
+++ b/helix-core/src/selection.rs
@@ -308,10 +308,10 @@ impl Range {
}
impl From<(usize, usize)> for Range {
- fn from(tuple: (usize, usize)) -> Self {
+ fn from((anchor, head): (usize, usize)) -> Self {
Self {
- anchor: tuple.0,
- head: tuple.1,
+ anchor,
+ head,
horiz: None,
}
}
diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs
index 32161b70..b53b0a78 100644
--- a/helix-core/src/surround.rs
+++ b/helix-core/src/surround.rs
@@ -1,4 +1,4 @@
-use crate::{search, Selection};
+use crate::{search, Range, Selection};
use ropey::RopeSlice;
pub const PAIRS: &[(char, char)] = &[
@@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) {
pub fn find_nth_pairs_pos(
text: RopeSlice,
ch: char,
- pos: usize,
+ range: Range,
n: usize,
) -> Option<(usize, usize)> {
- let (open, close) = get_pair(ch);
-
- if text.len_chars() < 2 || pos >= text.len_chars() {
+ if text.len_chars() < 2 || range.to() >= text.len_chars() {
return None;
}
+ let (open, close) = get_pair(ch);
+ let pos = range.cursor(text);
+
if open == close {
if Some(open) == text.get_char(pos) {
- // Special case: cursor is directly on a matching char.
- match pos {
- 0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
- _ if (pos + 1) == text.len_chars() => {
- Some((search::find_nth_prev(text, open, pos, n)?, pos))
- }
- // We return no match because there's no way to know which
- // side of the char we should be searching on.
- _ => None,
- }
- } else {
- Some((
- search::find_nth_prev(text, open, pos, n)?,
- search::find_nth_next(text, close, pos, n)?,
- ))
+ // Cursor is directly on match char. We return no match
+ // because there's no way to know which side of the char
+ // we should be searching on.
+ return None;
}
+ Some((
+ search::find_nth_prev(text, open, pos, n)?,
+ search::find_nth_next(text, close, pos, n)?,
+ ))
} else {
Some((
find_nth_open_pair(text, open, close, pos, n)?,
@@ -160,8 +154,8 @@ pub fn get_surround_pos(
) -> Option<Vec<usize>> {
let mut change_pos = Vec::new();
- for range in selection {
- let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
+ for &range in selection {
+ let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?;
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
return None;
}
@@ -178,67 +172,91 @@ mod test {
use ropey::Rope;
use smallvec::SmallVec;
- #[test]
- fn test_find_nth_pairs_pos() {
- let doc = Rope::from("some (text) here");
+ fn check_find_nth_pair_pos(
+ text: &str,
+ cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
+ ) {
+ let doc = Rope::from(text);
let slice = doc.slice(..);
- // cursor on [t]ext
- assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
- assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
- // cursor on so[m]e
- assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
- // cursor on bracket itself
- assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10)));
+ for (cursor_pos, ch, n, expected_range) in cases {
+ let range = find_nth_pairs_pos(slice, ch, (cursor_pos, cursor_pos + 1).into(), n);
+ assert_eq!(
+ range, expected_range,
+ "Expected {:?}, got {:?}",
+ expected_range, range
+ );
+ }
}
#[test]
- fn test_find_nth_pairs_pos_skip() {
- let doc = Rope::from("(so (many (good) text) here)");
- let slice = doc.slice(..);
+ fn test_find_nth_pairs_pos() {
+ check_find_nth_pair_pos(
+ "some (text) here",
+ vec![
+ // cursor on [t]ext
+ (6, '(', 1, Some((5, 10))),
+ (6, ')', 1, Some((5, 10))),
+ // cursor on so[m]e
+ (2, '(', 1, None),
+ // cursor on bracket itself
+ (5, '(', 1, Some((5, 10))),
+ (10, '(', 1, Some((5, 10))),
+ ],
+ );
+ }
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
+ #[test]
+ fn test_find_nth_pairs_pos_skip() {
+ check_find_nth_pair_pos(
+ "(so (many (good) text) here)",
+ vec![
+ // cursor on go[o]d
+ (13, '(', 1, Some((10, 15))),
+ (13, '(', 2, Some((4, 21))),
+ (13, '(', 3, Some((0, 27))),
+ ],
+ );
}
#[test]
fn test_find_nth_pairs_pos_same() {
- let doc = Rope::from("'so 'many 'good' text' here'");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
- // cursor on the quotes
- assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
- // this is the best we can do since opening and closing pairs are same
- assert_eq!(find_nth_pairs_pos(slice, '\'', 0, 1), Some((0, 4)));
- assert_eq!(find_nth_pairs_pos(slice, '\'', 27, 1), Some((21, 27)));
+ check_find_nth_pair_pos(
+ "'so 'many 'good' text' here'",
+ vec![
+ // cursor on go[o]d
+ (13, '\'', 1, Some((10, 15))),
+ (13, '\'', 2, Some((4, 21))),
+ (13, '\'', 3, Some((0, 27))),
+ // cursor on the quotes
+ (10, '\'', 1, None),
+ ],
+ )
}
#[test]
fn test_find_nth_pairs_pos_step() {
- let doc = Rope::from("((so)((many) good (text))(here))");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
+ check_find_nth_pair_pos(
+ "((so)((many) good (text))(here))",
+ vec![
+ // cursor on go[o]d
+ (15, '(', 1, Some((5, 24))),
+ (15, '(', 2, Some((0, 31))),
+ ],
+ )
}
#[test]
fn test_find_nth_pairs_pos_mixed() {
- let doc = Rope::from("(so [many {good} text] here)");
- let slice = doc.slice(..);
-
- // cursor on go[o]d
- assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
- assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
- assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
+ check_find_nth_pair_pos(
+ "(so [many {good} text] here)",
+ vec![
+ // cursor on go[o]d
+ (13, '{', 1, Some((10, 15))),
+ (13, '[', 1, Some((4, 21))),
+ (13, '(', 1, Some((0, 27))),
+ ],
+ )
}
#[test]
diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs
index 24f063d4..21ceec04 100644
--- a/helix-core/src/textobject.rs
+++ b/helix-core/src/textobject.rs
@@ -114,7 +114,7 @@ pub fn textobject_surround(
ch: char,
count: usize,
) -> Range {
- surround::find_nth_pairs_pos(slice, ch, range.head, count)
+ surround::find_nth_pairs_pos(slice, ch, range, count)
.map(|(anchor, head)| match textobject {
TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
@@ -170,7 +170,7 @@ mod test {
#[test]
fn test_textobject_word() {
- // (text, [(cursor position, textobject, final range), ...])
+ // (text, [(char position, textobject, final range), ...])
let tests = &[
(
"cursor at beginning of doc",
@@ -269,7 +269,9 @@ 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, false);
+ // cursor is a single width selection
+ let range = Range::new(pos, pos + 1);
+ let result = textobject_word(slice, range, objtype, 1, false);
assert_eq!(
result,
expected_range.into(),
@@ -283,7 +285,7 @@ mod test {
#[test]
fn test_textobject_surround() {
- // (text, [(cursor position, textobject, final range, count), ...])
+ // (text, [(cursor position, textobject, final range, surround char, count), ...])
let tests = &[
(
"simple (single) surround pairs",
diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml
index 0192ba1e..83b2978d 100644
--- a/helix-lsp/Cargo.toml
+++ b/helix-lsp/Cargo.toml
@@ -23,5 +23,5 @@ lsp-types = { version = "0.91", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
thiserror = "1.0"
-tokio = { version = "1.13", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
+tokio = { version = "1.14", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.8"
diff --git a/helix-syntax/languages/tree-sitter-wgsl b/helix-syntax/languages/tree-sitter-wgsl
new file mode 160000
+Subproject f00ff52251edbd58f4d39c9c3204383253032c1
diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs
index c76a2e28..69a51a21 100644
--- a/helix-term/src/application.rs
+++ b/helix-term/src/application.rs
@@ -122,7 +122,7 @@ impl Application {
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
- compositor.push(Box::new(ui::file_picker(".".into())));
+ compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
} else {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@@ -270,7 +270,7 @@ impl Application {
use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode;
- if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
+ if doc!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
return;
}
let editor_view = self
diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs
index 54466c56..084479cc 100644
--- a/helix-term/src/commands.rs
+++ b/helix-term/src/commands.rs
@@ -13,7 +13,6 @@ use helix_core::{
numbers::NumberIncrementor,
object, pos_at_coords,
regex::{self, Regex, RegexBuilder},
- register::Register,
search, selection, surround, textobject,
unicode::width::UnicodeWidthChar,
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
@@ -236,7 +235,9 @@ impl Command {
extend_line, "Select current line, if already selected, extend to next line",
extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
delete_selection, "Delete selection",
+ delete_selection_noyank, "Delete selection, without yanking",
change_selection, "Change selection (delete and enter insert mode)",
+ change_selection_noyank, "Change selection (delete and enter insert mode, without yanking)",
collapse_selection, "Collapse selection onto a single cursor",
flip_selections, "Flip selection cursor and anchor",
insert_mode, "Insert before selection",
@@ -262,6 +263,9 @@ impl Command {
goto_implementation, "Goto implementation",
goto_file_start, "Goto file start/line",
goto_file_end, "Goto file end",
+ goto_file, "Goto files in the selection",
+ goto_file_hsplit, "Goto files in the selection in horizontal splits",
+ goto_file_vsplit, "Goto files in the selection in vertical splits",
goto_reference, "Goto references",
goto_window_top, "Goto window top",
goto_window_middle, "Goto window middle",
@@ -318,6 +322,7 @@ impl Command {
join_selections, "Join lines inside selection",
keep_selections, "Keep selections matching regex",
remove_selections, "Remove selections matching regex",
+ align_selections, "Align selections in column",
keep_primary_selection, "Keep primary selection",
remove_primary_selection, "Remove primary selection",
completion, "Invoke completion popup",
@@ -676,11 +681,83 @@ fn trim_selections(cx: &mut Context) {
};
}
+// align text in selection
+fn align_selections(cx: &mut Context) {
+ let align_style = cx.count();
+ if align_style > 3 {
+ cx.editor.set_error(
+ "align only accept 1,2,3 as count to set left/center/right align".to_string(),
+ );
+ return;
+ }
+
+ let (view, doc) = current!(cx.editor);
+ let text = doc.text().slice(..);
+ let selection = doc.selection(view.id);
+ let mut column_widths = vec![];
+ let mut last_line = text.len_lines();
+ let mut column = 0;
+ // first of all, we need compute all column's width, let use max width of the selections in a column
+ for sel in selection {
+ let (l1, l2) = sel.line_range(text);
+ if l1 != l2 {
+ cx.editor
+ .set_error("align cannot work with multi line selections".to_string());
+ return;
+ }
+ // if the selection is not in the same line with last selection, we set the column to 0
+ column = if l1 != last_line { 0 } else { column + 1 };
+ last_line = l1;
+
+ if column < column_widths.len() {
+ if sel.to() - sel.from() > column_widths[column] {
+ column_widths[column] = sel.to() - sel.from();
+ }
+ } else {
+ // a new column, current selection width is the temp width of the column
+ column_widths.push(sel.to() - sel.from());
+ }
+ }
+ last_line = text.len_lines();
+ // once we get the with of each column, we transform each selection with to it's column width based on the align style
+ let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
+ let l = range.cursor_line(text);
+ column = if l != last_line { 0 } else { column + 1 };
+ last_line = l;
+
+ (
+ range.from(),
+ range.to(),
+ Some(
+ align_fragment_to_width(&range.fragment(text), column_widths[column], align_style)
+ .into(),
+ ),
+ )
+ });
+
+ doc.apply(&transaction, view.id);
+ doc.append_changes_to_history(view.id);
+}
+
+fn align_fragment_to_width(fragment: &str, width: usize, align_style: usize) -> String {
+ let trimed = fragment.trim_matches(|c| c == ' ');
+ let mut s = " ".repeat(width - trimed.chars().count());
+ match align_style {
+ 1 => s.insert_str(0, trimed), // left align
+ 2 => s.insert_str(s.len() / 2, trimed), // center align
+ 3 => s.push_str(trimed), // right align
+ n => unimplemented!("{}", n),
+ }
+ s
+}
+
fn goto_window(cx: &mut Context, align: Align) {
+ let count = cx.count() - 1;
let (view, doc) = current!(cx.editor);
let height = view.inner_area().height as usize;
+ // respect user given count if any
// - 1 so we have at least one gap in the middle.
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth
// as we type
@@ -689,11 +766,12 @@ fn goto_window(cx: &mut Context, align: Align) {
let last_line = view.last_line(doc);
let line = match align {
- Align::Top => (view.offset.row + scrolloff),
- Align::Center => (view.offset.row + (height / 2)),
- Align::Bottom => last_line.saturating_sub(scrolloff),
+ Align::Top => (view.offset.row + scrolloff + count),
+ Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
+ Align::Bottom => last_line.saturating_sub(scrolloff + count),
}
- .min(last_line.saturating_sub(scrolloff));
+ .min(last_line.saturating_sub(scrolloff))
+ .max(view.offset.row + scrolloff);
let pos = doc.text().line_to_char(line);
@@ -782,6 +860,49 @@ fn goto_file_end(cx: &mut Context) {
doc.set_selection(view.id, selection);
}
+fn goto_file(cx: &mut Context) {
+ goto_file_impl(cx, Action::Replace);
+}
+
+fn goto_file_hsplit(cx: &mut Context) {
+ goto_file_impl(cx, Action::HorizontalSplit);
+}
+
+fn goto_file_vsplit(cx: &mut Context) {
+ goto_file_impl(cx, Action::VerticalSplit);
+}
+
+fn goto_file_impl(cx: &mut Context, action: Action) {
+ let (view, doc) = current_ref!(cx.editor);
+ let text = doc.text();
+ let selections = doc.selection(view.id);
+ let mut paths: Vec<_> = selections
+ .iter()
+ .map(|r| text.slice(r.from()..r.to()).to_string())
+ .collect();
+ let primary = selections.primary();
+ if selections.len() == 1 && primary.to() - primary.from() == 1 {
+ let current_word = movement::move_next_long_word_start(
+ text.slice(..),
+ movement::move_prev_long_word_start(text.slice(..), primary, 1),
+ 1,
+ );
+ paths.clear();
+ paths.push(
+ text.slice(current_word.from()..current_word.to())
+ .to_string(),
+ );
+ }
+ for sel in paths {
+ let p = sel.trim();
+ if !p.is_empty() {
+ if let Err(e) = cx.editor.open(PathBuf::from(p), action) {
+ cx.editor.set_error(format!("Open file failed: {:?}", e));
+ }
+ }
+ }
+}
+
fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
where
F: Fn(RopeSlice, Range, usize) -> Range,
@@ -1459,6 +1580,7 @@ fn global_search(cx: &mut Context) {
let (all_matches_sx, all_matches_rx) =
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
let smart_case = cx.editor.config.smart_case;
+ let file_picker_config = cx.editor.config.file_picker.clone();
let completions = search_completions(cx, None);
let prompt = ui::regex_prompt(
@@ -1487,41 +1609,55 @@ fn global_search(cx: &mut Context) {
let search_root = std::env::current_dir()
.expect("Global search error: Failed to get current dir");
- WalkBuilder::new(search_root).build_parallel().run(|| {
- let mut searcher_cl = searcher.clone();
- let matcher_cl = matcher.clone();
- let all_matches_sx_cl = all_matches_sx.clone();
- Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
- let dent = match dent {
- Ok(dent) => dent,
- Err(_) => return WalkState::Continue,
- };
-
- match dent.file_type() {
- Some(fi) => {
- if !fi.is_file() {
- return WalkState::Continue;
+ WalkBuilder::new(search_root)
+ .hidden(file_picker_config.hidden)
+ .parents(file_picker_config.parents)
+ .ignore(file_picker_config.ignore)
+ .git_ignore(file_picker_config.git_ignore)
+ .git_global(file_picker_config.git_global)
+ .git_exclude(file_picker_config.git_exclude)
+ .max_depth(file_picker_config.max_depth)
+ .build_parallel()
+ .run(|| {
+ let mut searcher_cl = searcher.clone();
+ let matcher_cl = matcher.clone();
+ let all_matches_sx_cl = all_matches_sx.clone();
+ Box::new(move |dent: Result<DirEntry, ignore::Error>| -> WalkState {
+ let dent = match dent {
+ Ok(dent) => dent,
+ Err(_) => return WalkState::Continue,
+ };
+
+ match dent.file_type() {
+ Some(fi) => {
+ if !fi.is_file() {
+ return WalkState::Continue;
+ }
}
+ None => return WalkState::Continue,
}
- None => return WalkState::Continue,
- }
- let result_sink = sinks::UTF8(|line_num, _| {
- match all_matches_sx_cl
- .send((line_num as usize - 1, dent.path().to_path_buf()))
- {
- Ok(_) => Ok(true),
- Err(_) => Ok(false),
+ let result_sink = sinks::UTF8(|line_num, _| {
+ match all_matches_sx_cl
+ .send((line_num as usize - 1, dent.path().to_path_buf()))
+ {
+ Ok(_) => Ok(true),
+ Err(_) => Ok(false),
+ }
+ });
+ let result =
+ searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
+
+ if let Err(err) = result {
+ log::error!(
+ "Global search error: {}, {}",
+ dent.path().display(),
+ err
+ );
}
- });
- let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
-
- if let Err(err) = result {
- log::error!("Global search error: {}, {}", dent.path().display(), err);
- }
- WalkState::Continue
- })
- });
+ WalkState::Continue
+ })
+ });
} else {
// Otherwise do nothing
// log::warn!("Global Search Invalid Pattern")
@@ -1626,19 +1762,42 @@ fn extend_to_line_bounds(cx: &mut Context) {
);
}
-fn delete_selection_impl(reg: &mut Register, doc: &mut Document, view_id: ViewId) {
+enum Operation {
+ Delete,
+ Change,
+}
+
+fn delete_selection_impl(cx: &mut Context, op: Operation) {
+ let (view, doc) = current!(cx.editor);
+
let text = doc.text().slice(..);
- let selection = doc.selection(view_id);
+ let selection = doc.selection(view.id);
- // first yank the selection
- let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
- reg.write(values);
+ if cx.register != Some('_') {
+ // first yank the selection
+ let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
+ let reg_name = cx.register.unwrap_or('"');
+ let registers = &mut cx.editor.registers;
+ let reg = registers.get_mut(reg_name);
+ reg.write(values);
+ };
// then delete
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
(range.from(), range.to(), None)
});
- doc.apply(&transaction, view_id);
+ doc.apply(&transaction, view.id);
+
+ match op {
+ Operation::Delete => {
+ doc.append_changes_to_history(view.id);
+ // exit select mode, if currently in select mode
+ exit_select_mode(cx);
+ }
+ Operation::Change => {
+ enter_insert_mode(doc);
+ }
+ }
}
#[inline]
@@ -1653,25 +1812,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
}
fn delete_selection(cx: &mut Context) {
- let reg_name = cx.register.unwrap_or('"');
- let (view, doc) = current!(cx.editor);
- let registers = &mut cx.editor.registers;
- let reg = registers.get_mut(reg_name);
- delete_selection_impl(reg, doc, view.id);
-
- doc.append_changes_to_history(view.id);
+ delete_selection_impl(cx, Operation::Delete);
+}
- // exit select mode, if currently in select mode
- exit_select_mode(cx);
+fn delete_selection_noyank(cx: &mut Context) {
+ cx.register = Some('_');
+ delete_selection_impl(cx, Operation::Delete);
}
fn change_selection(cx: &mut Context) {
- let reg_name = cx.register.unwrap_or('"');
- let (view, doc) = current!(cx.editor);
- let registers = &mut cx.editor.registers;
- let reg = registers.get_mut(reg_name);
- delete_selection_impl(reg, doc, view.id);
- enter_insert_mode(doc);
+ delete_selection_impl(cx, Operation::Change);
+}
+
+fn change_selection_noyank(cx: &mut Context) {
+ cx.register = Some('_');
+ delete_selection_impl(cx, Operation::Change);
}
fn collapse_selection(cx: &mut Context) {
@@ -1820,7 +1975,7 @@ mod cmd {
let jobs = &mut cx.jobs;
let (_, doc) = current!(cx.editor);
- if let Some(path) = path {
+ if let Some(ref path) = path {
doc.set_path(Some(path.as_ref()))
.context("invalid filepath")?;
}
@@ -1840,6 +1995,11 @@ mod cmd {
});
let future = doc.format_and_save(fmt);
cx.jobs.add(Job::new(future).wait_before_exiting());
+
+ if path.is_some() {
+ let id = doc.id();
+ let _ = cx.editor.refresh_language_server(id);
+ }
Ok(())
}
@@ -2466,6 +2626,26 @@ mod cmd {
Ok(())
}
+ pub(super) fn goto_line_number(
+ cx: &mut compositor::Context,
+ args: &[&str],
+ _event: PromptEvent,
+ ) -> anyhow::Result<()> {
+ if args.is_empty() {
+ bail!("Line number required");
+ }
+
+ let line = args[0].parse::<usize>()?;
+
+ goto_line_impl(&mut cx.editor, NonZeroUsize::new(line));
+
+ let (view, doc) = current!(cx.editor);
+
+ view.ensure_cursor_in_view(doc, line);
+
+ Ok(())
+ }
+
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
TypableCommand {
name: "quit",
@@ -2768,6 +2948,13 @@ mod cmd {
fun: tutor,
completer: None,
},
+ TypableCommand {
+ name: "goto",
+ aliases: &["g"],
+ doc: "Go to line number.",
+ fun: goto_line_number,
+ completer: None,
+ }
];
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
@@ -2830,6 +3017,15 @@ fn command_mode(cx: &mut Context) {
return;
}
+ // If command is numeric, interpret as line number and go there.
+ if parts.len() == 1 && parts[0].parse::<usize>().ok().is_some() {
+ if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) {
+ cx.editor.set_error(format!("{}", e));
+ }
+ return;
+ }
+
+ // Handle typable commands
if let Some(cmd) = cmd::COMMANDS.get(parts[0]) {
if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
cx.editor.set_error(format!("{}", e));
@@ -2855,7 +3051,7 @@ fn command_mode(cx: &mut Context) {
fn file_picker(cx: &mut Context) {
let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
- let picker = ui::file_picker(root);
+ let picker = ui::file_picker(root, &cx.editor.config);
cx.push_layer(Box::new(picker));
}
@@ -3463,10 +3659,14 @@ fn push_jump(editor: &mut Editor) {
}
fn goto_line(cx: &mut Context) {
- if let Some(count) = cx.count {
- push_jump(cx.editor);
+ goto_line_impl(&mut cx.editor, cx.count)
+}
- let (view, doc) = current!(cx.editor);
+fn goto_line_impl(editor: &mut Editor, count: Option<NonZeroUsize>) {
+ if let Some(count) = count {
+ push_jump(editor);
+
+ let (view, doc) = current!(editor);
let max_line = if doc.text().line(doc.text().len_lines() - 1).len_chars() == 0 {
// If the last line is blank, don't jump to it.
doc.text().len_lines().saturating_sub(2)
@@ -5065,7 +5265,9 @@ fn match_brackets(cx: &mut Context) {
if let Some(syntax) = doc.syntax() {
let text = doc.text().slice(..);
let selection = doc.selection(view.id).clone().transform(|range| {
- if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) {
+ if let Some(pos) =
+ match_brackets::find_matching_bracket_fuzzy(syntax, doc.text(), range.anchor)
+ {
range.put_cursor(text, pos, doc.mode == Mode::Select)
} else {
range
diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs
index 42a62fc2..b317242d 100644
--- a/helix-term/src/keymap.rs
+++ b/helix-term/src/keymap.rs
@@ -512,6 +512,7 @@ impl Default for Keymaps {
"g" => { "Goto"
"g" => goto_file_start,
"e" => goto_last_line,
+ "f" => goto_file,
"h" => goto_line_start,
"l" => goto_line_end,
"s" => goto_first_nonwhitespace,
@@ -537,9 +538,9 @@ impl Default for Keymaps {
"O" => open_above,
"d" => delete_selection,
- // TODO: also delete without yanking
+ "A-d" => delete_selection_noyank,
"c" => change_selection,
- // TODO: also change delete without yanking
+ "A-c" => change_selection_noyank,
"C" => copy_selection_on_next_line,
"A-C" => copy_selection_on_prev_line,
@@ -604,7 +605,7 @@ impl Default for Keymaps {
// "q" => record_macro,
// "Q" => replay_macro,
- // & align selections
+ "&" => align_selections,
"_" => trim_selections,
"(" => rotate_selections_backward,
@@ -622,6 +623,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
+ "f" => goto_file_hsplit,
+ "F" => goto_file_vsplit,
"C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
@@ -670,6 +673,8 @@ impl Default for Keymaps {
"C-w" | "w" => rotate_view,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
+ "f" => goto_file_hsplit,
+ "F" => goto_file_vsplit,
"C-q" | "q" => wclose,
"C-o" | "o" => wonly,
"C-h" | "h" | "left" => jump_view_left,
diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs
index 0e243271..96c5f083 100644
--- a/helix-term/src/ui/editor.rs
+++ b/helix-term/src/ui/editor.rs
@@ -18,7 +18,6 @@ use helix_core::{
use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
use helix_view::{
document::{Mode, SCRATCH_BUFFER_NAME},
- editor::LineNumber,
graphics::{Color, CursorKind, Modifier, Rect, Style},
info::Info,
input::KeyEvent,
@@ -392,7 +391,7 @@ impl EditorView {
use helix_core::match_brackets;
let pos = doc.selection(view.id).primary().cursor(text);
- let pos = match_brackets::find(syntax, doc.text(), pos)
+ let pos = match_brackets::find_matching_bracket(syntax, doc.text(), pos)
.and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
if let Some(pos) = pos {
@@ -430,22 +429,6 @@ impl EditorView {
let text = doc.text().slice(..);
let last_line = view.last_line(doc);
- let linenr = theme.get("ui.linenr");
- let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
-
- let warning = theme.get("warning");
- let error = theme.get("error");
- let info = theme.get("info");
- let hint = theme.get("hint");
-
- // Whether to draw the line number for the last line of the
- // document or not. We only draw it if it's not an empty line.
- let draw_last = text.line_to_byte(last_line) < text.len_bytes();
-
- let current_line = doc
- .text()
- .char_to_line(doc.selection(view.id).primary().cursor(text));
-
// it's used inside an iterator so the collect isn't needless:
// https://github.com/rust-lang/rust-clippy/issues/6164
#[allow(clippy::needless_collect)]
@@ -455,146 +438,137 @@ impl EditorView {
.map(|range| range.cursor_line(text))
.collect();
- let mut breakpoints: Option<&Vec<SourceBreakpoint>> = None;
- let mut stack_frame: Option<&StackFrame> = None;
- if let Some(path) = doc.path() {
- breakpoints = all_breakpoints.get(path);
- if let Some(debugger) = debugger {
- // if we have a frame, and the frame path matches document
- if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id)
- {
- let frame = debugger
- .stack_frames
- .get(&thread_id)
- .and_then(|bt| bt.get(frame)); // TODO: drop the clone..
- if let Some(StackFrame {
- source: Some(source),
- ..
- }) = &frame
- {
- if source.path.as_ref() == Some(path) {
- stack_frame = frame;
- }
- };
- };
- }
+ use helix_view::gutter::GutterFn;
+ fn breakpoints<'doc>(
+ doc: &'doc Document,
+ _view: &View,
+ theme: &Theme,
+ _config: &Config,
+ _is_focused: bool,
+ _width: usize,
+ ) -> GutterFn<'doc> {
+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
+ //
+ })
}
-
- for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
- use helix_core::diagnostic::Severity;
- if let Ok(diagnostic) = doc.diagnostics().binary_search_by_key(&line, |d| d.line) {
- let diagnostic = &doc.diagnostics()[diagnostic];
- surface.set_stringn(
- viewport.x,
- viewport.y + i as u16,
- "●",
- 1,
- match diagnostic.severity {
- Some(Severity::Error) => error,
- Some(Severity::Warning) | None => warning,
- Some(Severity::Info) => info,
- Some(Severity::Hint) => hint,
- },
- );
- }
-
- let selected = cursors.contains(&line);
-
- // TODO: debugger should translate received breakpoints to 0-indexing
-
- if let Some(user) = breakpoints.as_ref() {
- let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() {
- debugger.iter().find(|breakpoint| {
- if breakpoint.source.is_some()
- && doc.path().is_some()
- && breakpoint.source.as_ref().unwrap().path == doc.path().cloned()
- {
- match (breakpoint.line, breakpoint.end_line) {
- #[allow(clippy::int_plus_one)]
- (Some(l), Some(el)) => l - 1 <= line && line <= el - 1,
- (Some(l), None) => l - 1 == line,
- _ => false,
- }
- } else {
- false
- }
- })
- } else {
- None
- };
-
- if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line)
- {
- let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false);
- let mut style =
- if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
- error.add_modifier(Modifier::UNDERLINED)
- } else if breakpoint.condition.is_some() {
- error
- } else if breakpoint.log_message.is_some() {
- info
- } else {
- warning
- };
- if !verified {
- // Faded colors
- style = if let Some(Color::Rgb(r, g, b)) = style.fg {
- style.fg(Color::Rgb(
- ((r as f32) * 0.4).floor() as u8,
- ((g as f32) * 0.4).floor() as u8,
- ((b as f32) * 0.4).floor() as u8,
- ))
- } else {
- style.fg(Color::Gray)
- }
- };
- surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style);
- } else if let Some(breakpoint) = debugger_breakpoint {
- let style = if breakpoint.verified {
- info
- } else {
- info.fg(Color::Gray)
- };
- surface.set_stringn(viewport.x, viewport.y + i as u16, "⊚", 1, style);
- }
- }
-
- if let Some(frame) = stack_frame {
- if frame.line - 1 == line {
- surface.set_style(
- Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
- helix_view::graphics::Style::default()
- .bg(helix_view::graphics::Color::LightYellow),
+ // let mut breakpoints: Option<&Vec<SourceBreakpoint>> = None;
+ // let mut stack_frame: Option<&StackFrame> = None;
+ // if let Some(path) = doc.path() {
+ // breakpoints = all_breakpoints.get(path);
+ // if let Some(debugger) = debugger {
+ // // if we have a frame, and the frame path matches document
+ // if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id)
+ // {
+ // let frame = debugger
+ // .stack_frames
+ // .get(&thread_id)
+ // .and_then(|bt| bt.get(frame)); // TODO: drop the clone..
+ // if let Some(StackFrame {
+ // source: Some(source),
+ // ..
+ // }) = &frame
+ // {
+ // if source.path.as_ref() == Some(path) {
+ // stack_frame = frame;
+ // }
+ // };
+ // };
+ // }
+ // }
+
+ // TODO: debugger should translate received breakpoints to 0-indexing
+
+ // if let Some(user) = breakpoints.as_ref() {
+ // let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() {
+ // debugger.iter().find(|breakpoint| {
+ // if breakpoint.source.is_some()
+ // && doc.path().is_some()
+ // && breakpoint.source.as_ref().unwrap().path == doc.path().cloned()
+ // {
+ // match (breakpoint.line, breakpoint.end_line) {
+ // #[allow(clippy::int_plus_one)]
+ // (Some(l), Some(el)) => l - 1 <= line && line <= el - 1,
+ // (Some(l), None) => l - 1 == line,
+ // _ => false,
+ // }
+ // } else {
+ // false
+ // }
+ // })
+ // } else {
+ // None
+ // };
+
+ // if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line)
+ // {
+ // let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false);
+ // let mut style =
+ // if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
+ // error.add_modifier(Modifier::UNDERLINED)
+ // } else if breakpoint.condition.is_some() {
+ // error
+ // } else if breakpoint.log_message.is_some() {
+ // info
+ // } else {
+ // warning
+ // };
+ // if !verified {
+ // // Faded colors
+ // style = if let Some(Color::Rgb(r, g, b)) = style.fg {
+ // style.fg(Color::Rgb(
+ // ((r as f32) * 0.4).floor() as u8,
+ // ((g as f32) * 0.4).floor() as u8,
+ // ((b as f32) * 0.4).floor() as u8,
+ // ))
+ // } else {
+ // style.fg(Color::Gray)
+ // }
+ // };
+ // surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style);
+ // } else if let Some(breakpoint) = debugger_breakpoint {
+ // let style = if breakpoint.verified {
+ // info
+ // } else {
+ // info.fg(Color::Gray)
+ // };
+ // surface.set_stringn(viewport.x, viewport.y + i as u16, "⊚", 1, style);
+ // }
+ // }
+
+ // if let Some(frame) = stack_frame {
+ // if frame.line - 1 == line {
+ // surface.set_style(
+ // Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
+ // helix_view::graphics::Style::default()
+ // .bg(helix_view::graphics::Color::LightYellow),
+ // );
+ // }
+ // }
+
+ let mut offset = 0;
+
+ // avoid lots of small allocations by reusing a text buffer for each line
+ let mut text = String::with_capacity(8);
+
+ for (constructor, width) in view.gutters() {
+ let gutter = constructor(doc, view, theme, config, is_focused, *width);
+ text.reserve(*width); // ensure there's enough space for the gutter
+ for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
+ let selected = cursors.contains(&line);
+
+ if let Some(style) = gutter(line, selected, &mut text) {
+ surface.set_stringn(
+ viewport.x + offset,
+ viewport.y + i as u16,
+ &text,
+ *width,
+ style,
);
}
+ text.clear();
}
- let text = if line == last_line && !draw_last {
- " ~".into()
- } else {
- let line = match config.line_number {
- LineNumber::Absolute => line + 1,
- LineNumber::Relative => {
- if current_line == line {
- line + 1
- } else {
- abs_diff(current_line, line)
- }
- }
- };
- format!("{:>5}", line)
- };
- surface.set_stringn(
- viewport.x + 1,
- viewport.y + i as u16,
- text,
- 5,
- if selected && is_focused {
- linenr_select
- } else {
- linenr
- },
- );
+ offset += *width as u16;
}
}
@@ -1364,12 +1338,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
key.modifiers.remove(KeyModifiers::SHIFT)
}
}
-
-#[inline]
-const fn abs_diff(a: usize, b: usize) -> usize {
- if a > b {
- a - b
- } else {
- b - a
- }
-}
diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs
index 61630d55..ca8303dd 100644
--- a/helix-term/src/ui/markdown.rs
+++ b/helix-term/src/ui/markdown.rs
@@ -55,7 +55,7 @@ fn parse<'a>(
fn to_span(text: pulldown_cmark::CowStr) -> Span {
use std::ops::Deref;
Span::raw::<std::borrow::Cow<_>>(match text {
- CowStr::Borrowed(s) => s.to_string().into(), // could retain borrow
+ CowStr::Borrowed(s) => s.into(),
CowStr::Boxed(s) => s.to_string().into(),
CowStr::Inlined(s) => s.deref().to_owned().into(),
})
@@ -179,7 +179,9 @@ fn parse<'a>(
spans.push(Span::raw(" "));
}
Event::Rule => {
- lines.push(Spans::from("---"));
+ let mut span = Span::raw("---");
+ span.style = code_style;
+ lines.push(Spans::from(span));
lines.push(Spans::default());
}
// TaskListMarker(bool) true if checked
@@ -226,6 +228,7 @@ impl Component for Markdown {
return None;
}
let contents = parse(&self.contents, None, &self.config_loader);
+ // TODO: account for tab width
let max_text_width = (viewport.0 - padding).min(120);
let mut text_width = 0;
let mut height = padding;
diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs
index 9d3b0bc5..3c203326 100644
--- a/helix-term/src/ui/mod.rs
+++ b/helix-term/src/ui/mod.rs
@@ -93,13 +93,22 @@ pub fn regex_prompt(
)
}
-pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
+pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> FilePicker<PathBuf> {
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time;
// We want to exclude files that the editor can't handle yet
let mut type_builder = TypesBuilder::new();
let mut walk_builder = WalkBuilder::new(&root);
+ walk_builder
+ .hidden(config.file_picker.hidden)
+ .parents(config.file_picker.parents)
+ .ignore(config.file_picker.ignore)
+ .git_ignore(config.file_picker.git_ignore)
+ .git_global(config.file_picker.git_global)
+ .git_exclude(config.file_picker.git_exclude)
+ .max_depth(config.file_picker.max_depth);
+
let walk_builder = match type_builder.add(
"compressed",
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs
index 31f8dc84..73da67c9 100644
--- a/helix-view/src/editor.rs
+++ b/helix-view/src/editor.rs
@@ -14,6 +14,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
collections::{BTreeMap, HashMap},
io::stdin,
+ num::NonZeroUsize,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@@ -21,7 +22,7 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep};
-use anyhow::Error;
+use anyhow::{bail, Context, Error};
pub use helix_core::diagnostic::Severity;
pub use helix_core::register::Registers;
@@ -41,6 +42,46 @@ where
#[derive(Debug, Clone, PartialEq, Deserialize)]
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
+pub struct FilePickerConfig {
+ /// IgnoreOptions
+ /// Enables ignoring hidden files.
+ /// Whether to hide hidden files in file picker and global search results. Defaults to true.
+ pub hidden: bool,
+ /// Enables reading ignore files from parent directories. Defaults to true.
+ pub parents: bool,
+ /// Enables reading `.ignore` files.
+ /// Whether to hide files listed in .ignore in file picker and global search results. Defaults to true.
+ pub ignore: bool,
+ /// Enables reading `.gitignore` files.
+ /// Whether to hide files listed in .gitignore in file picker and global search results. Defaults to true.
+ pub git_ignore: bool,
+ /// Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option.
+ /// Whether to hide files listed in global .gitignore in file picker and global search results. Defaults to true.
+ pub git_global: bool,
+ /// Enables reading `.git/info/exclude` files.
+ /// Whether to hide files listed in .git/info/exclude in file picker and global search results. Defaults to true.
+ pub git_exclude: bool,
+ /// WalkBuilder options
+ /// Maximum Depth to recurse directories in file picker and global search. Defaults to `None`.
+ pub max_depth: Option<usize>,
+}
+
+impl Default for FilePickerConfig {
+ fn default() -> Self {
+ Self {
+ hidden: true,
+ parents: true,
+ ignore: true,
+ git_ignore: true,
+ git_global: true,
+ git_exclude: true,
+ max_depth: None,
+ }
+ }
+}
+
+#[derive(Debug, Clone, PartialEq, Deserialize)]
+#[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,
@@ -66,9 +107,10 @@ pub struct Config {
pub completion_trigger_len: u8,
/// Whether to display infoboxes. Defaults to true.
pub auto_info: bool,
+ pub file_picker: FilePickerConfig,
}
-#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
+#[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum LineNumber {
/// Show absolute line number
@@ -97,6 +139,7 @@ impl Default for Config {
idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2,
auto_info: true,
+ file_picker: FilePickerConfig::default(),
}
}
}
@@ -116,7 +159,7 @@ impl std::fmt::Debug for Motion {
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
- pub next_document_id: usize,
+ pub next_document_id: DocumentId,
pub documents: BTreeMap<DocumentId, Document>,
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
@@ -154,8 +197,8 @@ pub enum Action {
impl Editor {
pub fn new(
mut area: Rect,
- themes: Arc<theme::Loader>,
- config_loader: Arc<syntax::Loader>,
+ theme_loader: Arc<theme::Loader>,
+ syn_loader: Arc<syntax::Loader>,
config: Config,
) -> Self {
let language_servers = helix_lsp::Registry::new();
@@ -165,17 +208,17 @@ impl Editor {
Self {
tree: Tree::new(area),
- next_document_id: 0,
+ next_document_id: DocumentId::default(),
documents: BTreeMap::new(),
count: None,
selected_register: None,
- theme: themes.default(),
+ theme: theme_loader.default(),
language_servers,
debugger: None,
debugger_events: SelectAll::new(),
breakpoints: HashMap::new(),
- syn_loader: config_loader,
- theme_loader: themes,
+ syn_loader,
+ theme_loader,
registers: Registers::default(),
clipboard_provider: get_clipboard_provider(),
status_msg: None,
@@ -232,7 +275,6 @@ impl Editor {
}
pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
- use anyhow::Context;
let theme = self
.theme_loader
.load(theme.as_ref())
@@ -241,6 +283,53 @@ impl Editor {
Ok(())
}
+ /// Refreshes the language server for a given document
+ pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> {
+ let doc = self.documents.get_mut(&doc_id)?;
+ doc.detect_language(Some(&self.theme), &self.syn_loader);
+ Self::launch_language_server(&mut self.language_servers, doc)
+ }
+
+ /// Launch a language server for a given document
+ fn launch_language_server(ls: &mut helix_lsp::Registry, doc: &mut Document) -> Option<()> {
+ // try to find a language server based on the language name
+ let language_server = doc.language.as_ref().and_then(|language| {
+ ls.get(language)
+ .map_err(|e| {
+ log::error!(
+ "Failed to initialize the LSP for `{}` {{ {} }}",
+ language.scope(),
+ e
+ )
+ })
+ .ok()
+ });
+ if let Some(language_server) = language_server {
+ // only spawn a new lang server if the servers aren't the same
+ if Some(language_server.id()) != doc.language_server().map(|server| server.id()) {
+ if let Some(language_server) = doc.language_server() {
+ tokio::spawn(language_server.text_document_did_close(doc.identifier()));
+ }
+ let language_id = doc
+ .language()
+ .and_then(|s| s.split('.').last()) // source.rust
+ .map(ToOwned::to_owned)
+ .unwrap_or_default();
+
+ // TODO: this now races with on_init code if the init happens too quickly
+ tokio::spawn(language_server.text_document_did_open(
+ doc.url().unwrap(),
+ doc.version(),
+ doc.text(),
+ language_id,
+ ));
+
+ doc.set_language_server(Some(language_server));
+ }
+ }
+ Some(())
+ }
+
fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() {
let doc = &self.documents[&view.doc];
@@ -311,23 +400,22 @@ impl Editor {
}
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));
- }
+ let doc = self.documents.get_mut(&id).unwrap();
+ if doc.selections().is_empty() {
+ doc.selections.insert(view_id, Selection::point(0));
}
return;
}
- Action::HorizontalSplit => {
- let view = View::new(id);
- let view_id = self.tree.split(view, Layout::Horizontal);
- // initialize selection for view
- let doc = self.documents.get_mut(&id).unwrap();
- doc.selections.insert(view_id, Selection::point(0));
- }
- Action::VerticalSplit => {
+ Action::HorizontalSplit | Action::VerticalSplit => {
let view = View::new(id);
- let view_id = self.tree.split(view, Layout::Vertical);
+ let view_id = self.tree.split(
+ view,
+ match action {
+ Action::HorizontalSplit => Layout::Horizontal,
+ Action::VerticalSplit => Layout::Vertical,
+ _ => unreachable!(),
+ },
+ );
// initialize selection for view
let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
@@ -337,16 +425,19 @@ impl Editor {
self._refresh();
}
- fn new_document(&mut self, mut document: Document) -> DocumentId {
- let id = DocumentId(self.next_document_id);
- self.next_document_id += 1;
- document.id = id;
- self.documents.insert(id, document);
+ /// Generate an id for a new document and register it.
+ fn new_document(&mut self, mut doc: Document) -> DocumentId {
+ let id = self.next_document_id;
+ // Safety: adding 1 from 1 is fine, probably impossible to reach usize max
+ self.next_document_id =
+ DocumentId(unsafe { NonZeroUsize::new_unchecked(self.next_document_id.0.get() + 1) });
+ doc.id = id;
+ self.documents.insert(id, doc);
id
}
- fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
- let id = self.new_document(document);
+ fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
+ let id = self.new_document(doc);
self.switch(id, action);
id
}
@@ -362,54 +453,16 @@ impl Editor {
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
let path = helix_core::path::get_canonicalized_path(&path)?;
-
- let id = self
- .documents()
- .find(|doc| doc.path() == Some(&path))
- .map(|doc| doc.id);
+ let id = self.document_by_path(&path).map(|doc| doc.id);
let id = if let Some(id) = id {
id
} else {
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
- // try to find a language server based on the language name
- let language_server = doc.language.as_ref().and_then(|language| {
- self.language_servers
- .get(language)
- .map_err(|e| {
- log::error!(
- "Failed to initialize the LSP for `{}` {{ {} }}",
- language.scope(),
- e
- )
- })
- .ok()
- });
-
- if let Some(language_server) = language_server {
- let language_id = doc
- .language()
- .and_then(|s| s.split('.').last()) // source.rust
- .map(ToOwned::to_owned)
- .unwrap_or_default();
-
- // TODO: this now races with on_init code if the init happens too quickly
- tokio::spawn(language_server.text_document_did_open(
- doc.url().unwrap(),
- doc.version(),
- doc.text(),
- language_id,
- ));
-
- doc.set_language_server(Some(language_server));
- }
+ let _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
- let id = DocumentId(self.next_document_id);
- self.next_document_id += 1;
- doc.id = id;
- self.documents.insert(id, doc);
- id
+ self.new_document(doc)
};
self.switch(id, action);
@@ -432,11 +485,11 @@ impl Editor {
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"),
+ None => bail!("document does not exist"),
};
if !force && doc.is_modified() {
- anyhow::bail!(
+ bail!(
"buffer {:?} is modified",
doc.relative_path()
.map(|path| path.to_string_lossy().to_string())
@@ -469,7 +522,7 @@ impl Editor {
// 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() {
+ if self.tree.views().next().is_none() {
let doc_id = self
.documents
.iter()
@@ -554,8 +607,7 @@ impl Editor {
}
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
- let view = view!(self);
- let doc = &self.documents[&view.doc];
+ let (view, doc) = current_ref!(self);
let cursor = doc
.selection(view.id)
.primary()
diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs
new file mode 100644
index 00000000..86773c1d
--- /dev/null
+++ b/helix-view/src/gutter.rs
@@ -0,0 +1,95 @@
+use std::fmt::Write;
+
+use crate::{editor::Config, graphics::Style, Document, Theme, View};
+
+pub type GutterFn<'doc> = Box<dyn Fn(usize, bool, &mut String) -> Option<Style> + 'doc>;
+pub type Gutter =
+ for<'doc> fn(&'doc Document, &View, &Theme, &Config, bool, usize) -> GutterFn<'doc>;
+
+pub fn diagnostic<'doc>(
+ doc: &'doc Document,
+ _view: &View,
+ theme: &Theme,
+ _config: &Config,
+ _is_focused: bool,
+ _width: usize,
+) -> GutterFn<'doc> {
+ let warning = theme.get("warning");
+ let error = theme.get("error");
+ let info = theme.get("info");
+ let hint = theme.get("hint");
+ let diagnostics = doc.diagnostics();
+
+ Box::new(move |line: usize, _selected: bool, out: &mut String| {
+ use helix_core::diagnostic::Severity;
+ if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) {
+ write!(out, "●").unwrap();
+ return Some(match diagnostic.severity {
+ Some(Severity::Error) => error,
+ Some(Severity::Warning) | None => warning,
+ Some(Severity::Info) => info,
+ Some(Severity::Hint) => hint,
+ });
+ }
+ None
+ })
+}
+
+pub fn line_number<'doc>(
+ doc: &'doc Document,
+ view: &View,
+ theme: &Theme,
+ config: &Config,
+ is_focused: bool,
+ width: usize,
+) -> GutterFn<'doc> {
+ let text = doc.text().slice(..);
+ let last_line = view.last_line(doc);
+ // Whether to draw the line number for the last line of the
+ // document or not. We only draw it if it's not an empty line.
+ let draw_last = text.line_to_byte(last_line) < text.len_bytes();
+
+ let linenr = theme.get("ui.linenr");
+ let linenr_select: Style = theme.try_get("ui.linenr.selected").unwrap_or(linenr);
+
+ let current_line = doc
+ .text()
+ .char_to_line(doc.selection(view.id).primary().cursor(text));
+
+ let config = config.line_number;
+
+ Box::new(move |line: usize, selected: bool, out: &mut String| {
+ if line == last_line && !draw_last {
+ write!(out, "{:>1$}", '~', width).unwrap();
+ Some(linenr)
+ } else {
+ use crate::editor::LineNumber;
+ let line = match config {
+ LineNumber::Absolute => line + 1,
+ LineNumber::Relative => {
+ if current_line == line {
+ line + 1
+ } else {
+ abs_diff(current_line, line)
+ }
+ }
+ };
+ let style = if selected && is_focused {
+ linenr_select
+ } else {
+ linenr
+ };
+ write!(out, "{:>1$}", line, width).unwrap();
+ Some(style)
+ }
+ })
+}
+
+#[inline(always)]
+const fn abs_diff(a: usize, b: usize) -> usize {
+ if a > b {
+ a - b
+ } else {
+ b - a
+ }
+}
diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs
index 3e779356..a56c914d 100644
--- a/helix-view/src/lib.rs
+++ b/helix-view/src/lib.rs
@@ -5,6 +5,7 @@ pub mod clipboard;
pub mod document;
pub mod editor;
pub mod graphics;
+pub mod gutter;
pub mod info;
pub mod input;
pub mod keyboard;
@@ -12,8 +13,18 @@ pub mod theme;
pub mod tree;
pub mod view;
-#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
-pub struct DocumentId(usize);
+use std::num::NonZeroUsize;
+
+// uses NonZeroUsize so Option<DocumentId> use a byte rather than two
+#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Debug)]
+pub struct DocumentId(NonZeroUsize);
+
+impl Default for DocumentId {
+ fn default() -> DocumentId {
+ // Safety: 1 is non-zero
+ DocumentId(unsafe { NonZeroUsize::new_unchecked(1) })
+ }
+}
slotmap::new_key_type! {
pub struct ViewId;
diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs
index 3066801b..78b3eb24 100644
--- a/helix-view/src/view.rs
+++ b/helix-view/src/view.rs
@@ -1,6 +1,10 @@
use std::borrow::Cow;
-use crate::{graphics::Rect, Document, DocumentId, ViewId};
+use crate::{
+ graphics::Rect,
+ gutter::{self, Gutter},
+ Document, DocumentId, ViewId,
+};
use helix_core::{
graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index,
@@ -60,6 +64,8 @@ impl JumpList {
}
}
+const GUTTERS: &[(Gutter, usize)] = &[(gutter::diagnostic, 1), (gutter::line_number, 5)];
+
#[derive(Debug)]
pub struct View {
pub id: ViewId,
@@ -83,10 +89,19 @@ impl View {
}
}
+ pub fn gutters(&self) -> &[(Gutter, usize)] {
+ GUTTERS
+ }
+
pub fn inner_area(&self) -> Rect {
- // TODO: not ideal
- const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
- self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
+ // TODO: cache this
+ let offset = self
+ .gutters()
+ .iter()
+ .map(|(_, width)| *width as u16)
+ .sum::<u16>()
+ + 1; // +1 for some space between gutters and line
+ self.area.clip_left(offset).clip_bottom(1) // -1 for statusline
}
//
@@ -296,6 +311,7 @@ mod tests {
use super::*;
use helix_core::Rope;
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
+ // const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
#[test]
fn test_text_pos_at_screen_coords() {
diff --git a/languages.toml b/languages.toml
index 2b39c4c8..f4b3874f 100644
--- a/languages.toml
+++ b/languages.toml
@@ -508,3 +508,28 @@ shebangs = ["perl"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
+
+[[language]]
+name = "racket"
+scope = "source.rkt"
+roots = []
+file-types = ["rkt"]
+shebangs = ["racket"]
+comment-token = ";"
+language-server = { command = "racket", args = ["-l", "racket-langserver"] }
+
+[[language]]
+name = "wgsl"
+scope = "source.wgsl"
+file-types = ["wgsl"]
+roots = []
+comment-token = "//"
+indent = { tab-width = 4, unit = " " }
+
+[[language]]
+name = "llvm"
+scope = "source.llvm"
+roots = []
+file-types = ["ll"]
+comment-token = ";"
+indent = { tab-width = 2, unit = " " }
diff --git a/runtime/queries/llvm/highlights.scm b/runtime/queries/llvm/highlights.scm
new file mode 100644
index 00000000..73afe85e
--- /dev/null
+++ b/runtime/queries/llvm/highlights.scm
@@ -0,0 +1,14 @@
+(type) @type
+(statement) @keyword.operator
+(number) @constant.numeric.integer
+(comment) @comment
+(string) @string
+(label) @label
+(keyword) @keyword
+"ret" @keyword.control.return
+(boolean) @constant.builtin.boolean
+(float) @constant.numeric.float
+(constant) @constant
+(identifier) @variable
+(symbol) @punctuation.delimiter
+(bracket) @punctuation.bracket
diff --git a/runtime/queries/wgsl/highlights.scm b/runtime/queries/wgsl/highlights.scm
new file mode 100644
index 00000000..7fbc87d8
--- /dev/null
+++ b/runtime/queries/wgsl/highlights.scm
@@ -0,0 +1,102 @@
+(const_literal) @constant.numeric
+
+(type_declaration) @type
+
+(function_declaration
+ (identifier) @function)
+
+(struct_declaration
+ (identifier) @type)
+
+(type_constructor_or_function_call_expression
+ (type_declaration) @function)
+
+(parameter
+ (variable_identifier_declaration (identifier) @variable.parameter))
+
+[
+ "struct"
+ "bitcast"
+ ; "block"
+ "discard"
+ "enable"
+ "fallthrough"
+ "fn"
+ "let"
+ "private"
+ "read"
+ "read_write"
+ "return"
+ "storage"
+ "type"
+ "uniform"
+ "var"
+ "workgroup"
+ "write"
+ (texel_format)
+] @keyword ; TODO reserved keywords
+
+[
+ (true)
+ (false)
+] @constant.builtin.boolean
+
+[ "," "." ":" ";" ] @punctuation.delimiter
+
+;; brackets
+[
+ "("
+ ")"
+ "["
+ "]"
+ "{"
+ "}"
+] @punctuation.bracket
+
+[
+ "loop"
+ "for"
+ "break"
+ "continue"
+ "continuing"
+] @keyword.control.repeat
+
+[
+ "if"
+ "else"
+ "elseif"
+ "switch"
+ "case"
+ "default"
+] @keyword.control.conditional
+
+[
+ "&"
+ "&&"
+ "/"
+ "!"
+ "="
+ "=="
+ "!="
+ ">"
+ ">="
+ ">>"
+ "<"
+ "<="
+ "<<"
+ "%"
+ "-"
+ "+"
+ "|"
+ "||"
+ "*"
+ "~"
+ "^"
+] @operator
+
+(attribute
+ (identifier) @variable.other.member)
+
+(comment) @comment
+
+(ERROR) @error
diff --git a/runtime/themes/solarized_dark.toml b/runtime/themes/solarized_dark.toml
index afcafd54..984c86ee 100644
--- a/runtime/themes/solarized_dark.toml
+++ b/runtime/themes/solarized_dark.toml
@@ -28,18 +28,18 @@
# 行号栏
"ui.linenr" = { fg = "base0", bg = "base02" }
# 当前行号栏
-"ui.linenr.selected" = { fg = "red", modifiers = ["bold"] }
+"ui.linenr.selected" = { fg = "blue", modifiers = ["bold"] }
# 状态栏
-"ui.statusline" = { fg = "base02", bg = "base1" }
+"ui.statusline" = { fg = "base03", bg = "base0" }
# 非活动状态栏
-"ui.statusline.inactive" = { fg = "base02", bg = "base00" }
+"ui.statusline.inactive" = { fg = "base1", bg = "base01" }
# 补全窗口, preview窗口
-"ui.popup" = { bg = "base1" }
+"ui.popup" = { bg = "base02" }
# 影响 补全选中 cmd弹出信息选中
-"ui.menu.selected" = { fg = "base02", bg = "violet"}
-"ui.menu" = { fg = "base02" }
+"ui.menu.selected" = { fg = "base02", bg = "base2"}
+"ui.menu" = { fg = "base1" }
# ??
"ui.window" = { fg = "base3" }
# 命令行 补全的帮助信息
diff --git a/runtime/themes/solarized_light.toml b/runtime/themes/solarized_light.toml
index aec5bf48..0ab1b962 100644
--- a/runtime/themes/solarized_light.toml
+++ b/runtime/themes/solarized_light.toml
@@ -28,18 +28,18 @@
# 行号栏
"ui.linenr" = { fg = "base0", bg = "base02" }
# 当前行号栏
-"ui.linenr.selected" = { fg = "red", modifiers = ["bold"] }
+"ui.linenr.selected" = { fg = "blue", modifiers = ["bold"] }
# 状态栏
-"ui.statusline" = { fg = "base02", bg = "base1" }
+"ui.statusline" = { fg = "base03", bg = "base0" }
# 非活动状态栏
-"ui.statusline.inactive" = { fg = "base02", bg = "base00" }
+"ui.statusline.inactive" = { fg = "base1", bg = "base01" }
# 补全窗口, preview窗口
-"ui.popup" = { bg = "base1" }
+"ui.popup" = { bg = "base02" }
# 影响 补全选中 cmd弹出信息选中
-"ui.menu.selected" = { fg = "base02", bg = "violet"}
-"ui.menu" = { fg = "base02" }
+"ui.menu.selected" = { fg = "base02", bg = "base2"}
+"ui.menu" = { fg = "base1" }
# ??
"ui.window" = { fg = "base3" }
# 命令行 补全的帮助信息