Merge remote-tracking branch 'origin/master' into debug
This commit is contained in:
commit
9ed930b233
34 changed files with 1096 additions and 504 deletions
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
2
.github/ISSUE_TEMPLATE/bug_report.md
vendored
|
@ -17,7 +17,7 @@ Please search on the issue tracker before creating one. -->
|
||||||
### Environment
|
### Environment
|
||||||
|
|
||||||
- Platform: <!-- macOS / Windows / Linux -->
|
- 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>
|
<details><summary>~/.cache/helix/helix.log</summary>
|
||||||
|
|
||||||
|
|
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
|
@ -25,19 +25,19 @@ jobs:
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- name: Cache cargo registry
|
- name: Cache cargo registry
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/git
|
path: ~/.cargo/git
|
||||||
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Cache cargo target dir
|
- name: Cache cargo target dir
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: target
|
path: target
|
||||||
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
@ -64,19 +64,19 @@ jobs:
|
||||||
override: true
|
override: true
|
||||||
|
|
||||||
- name: Cache cargo registry
|
- name: Cache cargo registry
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/git
|
path: ~/.cargo/git
|
||||||
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Cache cargo target dir
|
- name: Cache cargo target dir
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: target
|
path: target
|
||||||
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
@ -109,19 +109,19 @@ jobs:
|
||||||
components: rustfmt, clippy
|
components: rustfmt, clippy
|
||||||
|
|
||||||
- name: Cache cargo registry
|
- name: Cache cargo registry
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/registry
|
path: ~/.cargo/registry
|
||||||
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Cache cargo index
|
- name: Cache cargo index
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: ~/.cargo/git
|
path: ~/.cargo/git
|
||||||
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
||||||
- name: Cache cargo target dir
|
- name: Cache cargo target dir
|
||||||
uses: actions/cache@v2.1.6
|
uses: actions/cache@v2.1.7
|
||||||
with:
|
with:
|
||||||
path: target
|
path: target
|
||||||
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||||
|
|
8
.gitmodules
vendored
8
.gitmodules
vendored
|
@ -142,3 +142,11 @@
|
||||||
path = helix-syntax/languages/tree-sitter-perl
|
path = helix-syntax/languages/tree-sitter-perl
|
||||||
url = https://github.com/ganezdragon/tree-sitter-perl
|
url = https://github.com/ganezdragon/tree-sitter-perl
|
||||||
shallow = true
|
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
|
||||||
|
|
37
Cargo.lock
generated
37
Cargo.lock
generated
|
@ -13,9 +13,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "anyhow"
|
name = "anyhow"
|
||||||
version = "1.0.46"
|
version = "1.0.51"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3aa828229c44c0293dd7d4d2300bdfc4d2883ffdba934c069a6b968957a81f70"
|
checksum = "8b26702f315f53b6071259e15dd9d64528213b44d61de1ec926eca7715d62203"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "arc-swap"
|
name = "arc-swap"
|
||||||
|
@ -258,15 +258,15 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-core"
|
name = "futures-core"
|
||||||
version = "0.3.17"
|
version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "88d1c26957f23603395cd326b0ffe64124b818f4449552f960d815cfba83a53d"
|
checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-executor"
|
name = "futures-executor"
|
||||||
version = "0.3.17"
|
version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "45025be030969d763025784f7f355043dc6bc74093e4ecc5000ca4dc50d8745c"
|
checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
|
@ -275,17 +275,16 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-task"
|
name = "futures-task"
|
||||||
version = "0.3.17"
|
version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "1d3d00f4eddb73e498a54394f228cd55853bdf059259e8e7bc6e69d408892e99"
|
checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.17"
|
version = "0.3.18"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "36568465210a3a6ee45e1f165136d68671471a501e632e9a98d96872222b5481"
|
checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
|
||||||
"futures-core",
|
"futures-core",
|
||||||
"futures-task",
|
"futures-task",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
|
@ -914,9 +913,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "serde_json"
|
name = "serde_json"
|
||||||
version = "1.0.70"
|
version = "1.0.72"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "e277c495ac6cd1a01a58d0a0c574568b4d1ddf14f59965c6a58b8d96400b54f3"
|
checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"itoa",
|
"itoa",
|
||||||
"ryu",
|
"ryu",
|
||||||
|
@ -1086,9 +1085,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio"
|
name = "tokio"
|
||||||
version = "1.13.1"
|
version = "1.14.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "52963f91310c08d91cb7bff5786dfc8b79642ab839e188187e92105dbfb9d2c8"
|
checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"autocfg",
|
"autocfg",
|
||||||
"bytes",
|
"bytes",
|
||||||
|
@ -1106,9 +1105,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tokio-macros"
|
name = "tokio-macros"
|
||||||
version = "1.5.0"
|
version = "1.6.0"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
|
checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
|
@ -1137,9 +1136,9 @@ dependencies = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "tree-sitter"
|
name = "tree-sitter"
|
||||||
version = "0.20.0"
|
version = "0.20.1"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "63ec02a07a782abef91279b72fe8fd2bee4c168a22112cedec5d3b0d49b9e4f9"
|
checksum = "9394e9dbfe967b5f3d6ab79e302e78b5fb7b530c368d634ff3b8d67ede138bf1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
|
@ -69,7 +69,7 @@ Contributors are very welcome! **No contribution is too small and all contributi
|
||||||
|
|
||||||
Some suggestions to get started:
|
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!
|
- 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:
|
- 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!")`)
|
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
|
||||||
|
|
|
@ -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` |
|
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
|
||||||
| `auto-info` | Whether to display infoboxes | `true` |
|
| `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
|
## LSP
|
||||||
|
|
||||||
To display all language server messages in the status line add the following to your `config.toml`:
|
To display all language server messages in the status line add the following to your `config.toml`:
|
||||||
|
|
|
@ -25,9 +25,7 @@ shell for working on Helix.
|
||||||
|
|
||||||
Releases are available in the `community` repository.
|
Releases are available in the `community` repository.
|
||||||
|
|
||||||
Packages are also available on AUR:
|
A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch.
|
||||||
- [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
|
|
||||||
|
|
||||||
## Build from source
|
## Build from source
|
||||||
|
|
||||||
|
|
|
@ -71,7 +71,9 @@
|
||||||
| `<` | Unindent selection | `unindent` |
|
| `<` | Unindent selection | `unindent` |
|
||||||
| `=` | Format selection (**LSP**) | `format_selections` |
|
| `=` | Format selection (**LSP**) | `format_selections` |
|
||||||
| `d` | Delete selection | `delete_selection` |
|
| `d` | Delete selection | `delete_selection` |
|
||||||
|
| `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` |
|
||||||
| `c` | Change selection (delete and enter insert mode) | `change_selection` |
|
| `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-a` | Increment object (number) under cursor | `increment` |
|
||||||
| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
|
| `Ctrl-x` | Decrement object (number) under cursor | `decrement` |
|
||||||
|
|
||||||
|
@ -80,9 +82,9 @@
|
||||||
| Key | Description | Command |
|
| Key | Description | Command |
|
||||||
| ------ | ----------- | ------- |
|
| ------ | ----------- | ------- |
|
||||||
| <code>|</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
|
| <code>|</code> | Pipe each selection through shell command, replacing with output | `shell_pipe` |
|
||||||
| <code>A-|</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
|
| <code>Alt-|</code> | Pipe each selection into shell command, ignoring output | `shell_pipe_to` |
|
||||||
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
|
| `!` | Run shell command, inserting output before each selection | `shell_insert_output` |
|
||||||
| `A-!` | Run shell command, appending output after each selection | `shell_append_output` |
|
| `Alt-!` | Run shell command, appending output after each selection | `shell_append_output` |
|
||||||
|
|
||||||
|
|
||||||
### Selection manipulation
|
### Selection manipulation
|
||||||
|
@ -92,6 +94,7 @@
|
||||||
| `s` | Select all regex matches inside selections | `select_regex` |
|
| `s` | Select all regex matches inside selections | `select_regex` |
|
||||||
| `S` | Split selection into subselections on regex matches | `split_selection` |
|
| `S` | Split selection into subselections on regex matches | `split_selection` |
|
||||||
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
|
| `Alt-s` | Split selection on newlines | `split_selection_on_newline` |
|
||||||
|
| `&` | Align selection in columns | `align_selections` |
|
||||||
| `_` | Trim whitespace from the selection | `trim_selections` |
|
| `_` | Trim whitespace from the selection | `trim_selections` |
|
||||||
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
|
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
|
||||||
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
|
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
|
||||||
|
@ -157,6 +160,7 @@ Jumps to various locations.
|
||||||
| ----- | ----------- | ------- |
|
| ----- | ----------- | ------- |
|
||||||
| `g` | Go to the start of the file | `goto_file_start` |
|
| `g` | Go to the start of the file | `goto_file_start` |
|
||||||
| `e` | Go to the end of the file | `goto_last_line` |
|
| `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` |
|
| `h` | Go to the start of the line | `goto_line_start` |
|
||||||
| `l` | Go to the end of the line | `goto_line_end` |
|
| `l` | Go to the end of the line | `goto_line_end` |
|
||||||
| `s` | Go to first non-whitespace character of the line | `goto_first_nonwhitespace` |
|
| `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` |
|
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
|
||||||
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
|
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
|
||||||
| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
|
| `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` |
|
| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
|
||||||
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
|
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
|
||||||
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
|
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
|
||||||
|
@ -314,7 +320,7 @@ Keys to use within prompt, Remapping currently not supported.
|
||||||
| `Ctrl-u` | Delete to start of line |
|
| `Ctrl-u` | Delete to start of line |
|
||||||
| `Ctrl-k` | Delete to end of line |
|
| `Ctrl-k` | Delete to end of line |
|
||||||
| `backspace`, `Ctrl-h` | Delete previous char |
|
| `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-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
|
||||||
| `Ctrl-p`, `Up` | Select previous history |
|
| `Ctrl-p`, `Up` | Select previous history |
|
||||||
| `Ctrl-n`, `Down` | Select next history |
|
| `Ctrl-n`, `Down` | Select next history |
|
||||||
|
|
|
@ -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.
|
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.
|
||||||
|
|
|
@ -145,11 +145,12 @@ We use a similar set of scopes as
|
||||||
- `conditional` - `if`, `else`
|
- `conditional` - `if`, `else`
|
||||||
- `repeat` - `for`, `while`, `loop`
|
- `repeat` - `for`, `while`, `loop`
|
||||||
- `import` - `import`, `export`
|
- `import` - `import`, `export`
|
||||||
- (TODO: return?)
|
- `return`
|
||||||
|
- `operator` - `or`, `in`
|
||||||
- `directive` - Preprocessor directives (`#if` in C)
|
- `directive` - Preprocessor directives (`#if` in C)
|
||||||
- `function` - `fn`, `func`
|
- `function` - `fn`, `func`
|
||||||
|
|
||||||
- `operator` - `||`, `+=`, `>`, `or`
|
- `operator` - `||`, `+=`, `>`
|
||||||
|
|
||||||
- `function`
|
- `function`
|
||||||
- `builtin`
|
- `builtin`
|
||||||
|
|
|
@ -23,8 +23,10 @@ If there is a selected register before invoking a change or delete command, the
|
||||||
| `/` | Last search |
|
| `/` | Last search |
|
||||||
| `:` | Last executed command |
|
| `:` | Last executed command |
|
||||||
| `"` | Last yanked text |
|
| `"` | 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.
|
> 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
|
## Surround
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
//! LSP diagnostic utility types.
|
//! LSP diagnostic utility types.
|
||||||
|
|
||||||
/// Describes the severity level of a [`Diagnostic`].
|
/// Describes the severity level of a [`Diagnostic`].
|
||||||
#[derive(Debug, Eq, PartialEq)]
|
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||||
pub enum Severity {
|
pub enum Severity {
|
||||||
Error,
|
Error,
|
||||||
Warning,
|
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)
|
/// 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 struct Diagnostic {
|
||||||
pub range: Range,
|
pub range: Range,
|
||||||
pub line: usize,
|
pub line: usize,
|
||||||
|
|
|
@ -1,3 +1,5 @@
|
||||||
|
use tree_sitter::Node;
|
||||||
|
|
||||||
use crate::{Rope, Syntax};
|
use crate::{Rope, Syntax};
|
||||||
|
|
||||||
const PAIRS: &[(char, char)] = &[
|
const PAIRS: &[(char, char)] = &[
|
||||||
|
@ -6,50 +8,85 @@ const PAIRS: &[(char, char)] = &[
|
||||||
('[', ']'),
|
('[', ']'),
|
||||||
('<', '>'),
|
('<', '>'),
|
||||||
('\'', '\''),
|
('\'', '\''),
|
||||||
('"', '"'),
|
('\"', '\"'),
|
||||||
];
|
];
|
||||||
|
|
||||||
// limit matching pairs to only ( ) { } [ ] < >
|
// 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]
|
#[must_use]
|
||||||
pub fn find(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
|
pub fn find_matching_bracket(syntax: &Syntax, doc: &Rope, pos: usize) -> Option<usize> {
|
||||||
let tree = syntax.tree();
|
if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
|
||||||
|
|
||||||
let byte_pos = doc.char_to_byte(pos);
|
|
||||||
|
|
||||||
// most naive implementation: find the innermost syntax node, if we're at the edge of a node,
|
|
||||||
// return the other edge.
|
|
||||||
|
|
||||||
let node = match tree
|
|
||||||
.root_node()
|
|
||||||
.named_descendant_for_byte_range(byte_pos, byte_pos)
|
|
||||||
{
|
|
||||||
Some(node) => node,
|
|
||||||
None => return None,
|
|
||||||
};
|
|
||||||
|
|
||||||
if node.is_error() {
|
|
||||||
return None;
|
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_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 mut node = tree.root_node().named_descendant_for_byte_range(pos, pos)?;
|
||||||
|
|
||||||
|
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));
|
||||||
|
|
||||||
|
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 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 len = doc.len_bytes();
|
||||||
|
|
||||||
let start_byte = node.start_byte();
|
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 {
|
if start_byte >= len || end_byte >= len {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
let start_char = doc.byte_to_char(start_byte);
|
Some((start_byte, end_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
|
|
||||||
}
|
}
|
||||||
|
|
|
@ -15,8 +15,12 @@ impl Register {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
|
pub fn new_with_values(name: char, values: Vec<String>) -> Self {
|
||||||
|
if name == '_' {
|
||||||
|
Self::new(name)
|
||||||
|
} else {
|
||||||
Self { name, values }
|
Self { name, values }
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub const fn name(&self) -> char {
|
pub const fn name(&self) -> char {
|
||||||
self.name
|
self.name
|
||||||
|
@ -27,13 +31,17 @@ impl Register {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn write(&mut self, values: Vec<String>) {
|
pub fn write(&mut self, values: Vec<String>) {
|
||||||
|
if self.name != '_' {
|
||||||
self.values = values;
|
self.values = values;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub fn push(&mut self, value: String) {
|
pub fn push(&mut self, value: String) {
|
||||||
|
if self.name != '_' {
|
||||||
self.values.push(value);
|
self.values.push(value);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Currently just wraps a `HashMap` of `Register`s
|
/// Currently just wraps a `HashMap` of `Register`s
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
|
|
|
@ -308,10 +308,10 @@ impl Range {
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<(usize, usize)> for Range {
|
impl From<(usize, usize)> for Range {
|
||||||
fn from(tuple: (usize, usize)) -> Self {
|
fn from((anchor, head): (usize, usize)) -> Self {
|
||||||
Self {
|
Self {
|
||||||
anchor: tuple.0,
|
anchor,
|
||||||
head: tuple.1,
|
head,
|
||||||
horiz: None,
|
horiz: None,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
use crate::{search, Selection};
|
use crate::{search, Range, Selection};
|
||||||
use ropey::RopeSlice;
|
use ropey::RopeSlice;
|
||||||
|
|
||||||
pub const PAIRS: &[(char, char)] = &[
|
pub const PAIRS: &[(char, char)] = &[
|
||||||
|
@ -35,33 +35,27 @@ pub fn get_pair(ch: char) -> (char, char) {
|
||||||
pub fn find_nth_pairs_pos(
|
pub fn find_nth_pairs_pos(
|
||||||
text: RopeSlice,
|
text: RopeSlice,
|
||||||
ch: char,
|
ch: char,
|
||||||
pos: usize,
|
range: Range,
|
||||||
n: usize,
|
n: usize,
|
||||||
) -> Option<(usize, usize)> {
|
) -> Option<(usize, usize)> {
|
||||||
let (open, close) = get_pair(ch);
|
if text.len_chars() < 2 || range.to() >= text.len_chars() {
|
||||||
|
|
||||||
if text.len_chars() < 2 || pos >= text.len_chars() {
|
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let (open, close) = get_pair(ch);
|
||||||
|
let pos = range.cursor(text);
|
||||||
|
|
||||||
if open == close {
|
if open == close {
|
||||||
if Some(open) == text.get_char(pos) {
|
if Some(open) == text.get_char(pos) {
|
||||||
// Special case: cursor is directly on a matching char.
|
// Cursor is directly on match char. We return no match
|
||||||
match pos {
|
// because there's no way to know which side of the char
|
||||||
0 => Some((pos, search::find_nth_next(text, close, pos + 1, n)?)),
|
// we should be searching on.
|
||||||
_ if (pos + 1) == text.len_chars() => {
|
return None;
|
||||||
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((
|
Some((
|
||||||
search::find_nth_prev(text, open, pos, n)?,
|
search::find_nth_prev(text, open, pos, n)?,
|
||||||
search::find_nth_next(text, close, pos, n)?,
|
search::find_nth_next(text, close, pos, n)?,
|
||||||
))
|
))
|
||||||
}
|
|
||||||
} else {
|
} else {
|
||||||
Some((
|
Some((
|
||||||
find_nth_open_pair(text, open, close, pos, n)?,
|
find_nth_open_pair(text, open, close, pos, n)?,
|
||||||
|
@ -160,8 +154,8 @@ pub fn get_surround_pos(
|
||||||
) -> Option<Vec<usize>> {
|
) -> Option<Vec<usize>> {
|
||||||
let mut change_pos = Vec::new();
|
let mut change_pos = Vec::new();
|
||||||
|
|
||||||
for range in selection {
|
for &range in selection {
|
||||||
let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range.head, skip)?;
|
let (open_pos, close_pos) = find_nth_pairs_pos(text, ch, range, skip)?;
|
||||||
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
|
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
|
@ -178,67 +172,91 @@ mod test {
|
||||||
use ropey::Rope;
|
use ropey::Rope;
|
||||||
use smallvec::SmallVec;
|
use smallvec::SmallVec;
|
||||||
|
|
||||||
#[test]
|
fn check_find_nth_pair_pos(
|
||||||
fn test_find_nth_pairs_pos() {
|
text: &str,
|
||||||
let doc = Rope::from("some (text) here");
|
cases: Vec<(usize, char, usize, Option<(usize, usize)>)>,
|
||||||
|
) {
|
||||||
|
let doc = Rope::from(text);
|
||||||
let slice = doc.slice(..);
|
let slice = doc.slice(..);
|
||||||
|
|
||||||
|
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() {
|
||||||
|
check_find_nth_pair_pos(
|
||||||
|
"some (text) here",
|
||||||
|
vec![
|
||||||
// cursor on [t]ext
|
// cursor on [t]ext
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 6, 1), Some((5, 10)));
|
(6, '(', 1, Some((5, 10))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, ')', 6, 1), Some((5, 10)));
|
(6, ')', 1, Some((5, 10))),
|
||||||
// cursor on so[m]e
|
// cursor on so[m]e
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 2, 1), None);
|
(2, '(', 1, None),
|
||||||
// cursor on bracket itself
|
// cursor on bracket itself
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 5, 1), Some((5, 10)));
|
(5, '(', 1, Some((5, 10))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 10, 1), Some((5, 10)));
|
(10, '(', 1, Some((5, 10))),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_nth_pairs_pos_skip() {
|
fn test_find_nth_pairs_pos_skip() {
|
||||||
let doc = Rope::from("(so (many (good) text) here)");
|
check_find_nth_pair_pos(
|
||||||
let slice = doc.slice(..);
|
"(so (many (good) text) here)",
|
||||||
|
vec![
|
||||||
// cursor on go[o]d
|
// cursor on go[o]d
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((10, 15)));
|
(13, '(', 1, Some((10, 15))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 2), Some((4, 21)));
|
(13, '(', 2, Some((4, 21))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 3), Some((0, 27)));
|
(13, '(', 3, Some((0, 27))),
|
||||||
|
],
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_nth_pairs_pos_same() {
|
fn test_find_nth_pairs_pos_same() {
|
||||||
let doc = Rope::from("'so 'many 'good' text' here'");
|
check_find_nth_pair_pos(
|
||||||
let slice = doc.slice(..);
|
"'so 'many 'good' text' here'",
|
||||||
|
vec![
|
||||||
// cursor on go[o]d
|
// cursor on go[o]d
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 1), Some((10, 15)));
|
(13, '\'', 1, Some((10, 15))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 2), Some((4, 21)));
|
(13, '\'', 2, Some((4, 21))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 13, 3), Some((0, 27)));
|
(13, '\'', 3, Some((0, 27))),
|
||||||
// cursor on the quotes
|
// cursor on the quotes
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '\'', 10, 1), None);
|
(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)));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_nth_pairs_pos_step() {
|
fn test_find_nth_pairs_pos_step() {
|
||||||
let doc = Rope::from("((so)((many) good (text))(here))");
|
check_find_nth_pair_pos(
|
||||||
let slice = doc.slice(..);
|
"((so)((many) good (text))(here))",
|
||||||
|
vec![
|
||||||
// cursor on go[o]d
|
// cursor on go[o]d
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 1), Some((5, 24)));
|
(15, '(', 1, Some((5, 24))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 15, 2), Some((0, 31)));
|
(15, '(', 2, Some((0, 31))),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_find_nth_pairs_pos_mixed() {
|
fn test_find_nth_pairs_pos_mixed() {
|
||||||
let doc = Rope::from("(so [many {good} text] here)");
|
check_find_nth_pair_pos(
|
||||||
let slice = doc.slice(..);
|
"(so [many {good} text] here)",
|
||||||
|
vec![
|
||||||
// cursor on go[o]d
|
// cursor on go[o]d
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '{', 13, 1), Some((10, 15)));
|
(13, '{', 1, Some((10, 15))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '[', 13, 1), Some((4, 21)));
|
(13, '[', 1, Some((4, 21))),
|
||||||
assert_eq!(find_nth_pairs_pos(slice, '(', 13, 1), Some((0, 27)));
|
(13, '(', 1, Some((0, 27))),
|
||||||
|
],
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
|
|
|
@ -114,7 +114,7 @@ pub fn textobject_surround(
|
||||||
ch: char,
|
ch: char,
|
||||||
count: usize,
|
count: usize,
|
||||||
) -> Range {
|
) -> 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 {
|
.map(|(anchor, head)| match textobject {
|
||||||
TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
|
TextObject::Inside => Range::new(next_grapheme_boundary(slice, anchor), head),
|
||||||
TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
|
TextObject::Around => Range::new(anchor, next_grapheme_boundary(slice, head)),
|
||||||
|
@ -170,7 +170,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_textobject_word() {
|
fn test_textobject_word() {
|
||||||
// (text, [(cursor position, textobject, final range), ...])
|
// (text, [(char position, textobject, final range), ...])
|
||||||
let tests = &[
|
let tests = &[
|
||||||
(
|
(
|
||||||
"cursor at beginning of doc",
|
"cursor at beginning of doc",
|
||||||
|
@ -269,7 +269,9 @@ mod test {
|
||||||
let slice = doc.slice(..);
|
let slice = doc.slice(..);
|
||||||
for &case in scenario {
|
for &case in scenario {
|
||||||
let (pos, objtype, expected_range) = case;
|
let (pos, objtype, expected_range) = case;
|
||||||
let result = textobject_word(slice, Range::point(pos), objtype, 1, 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!(
|
assert_eq!(
|
||||||
result,
|
result,
|
||||||
expected_range.into(),
|
expected_range.into(),
|
||||||
|
@ -283,7 +285,7 @@ mod test {
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_textobject_surround() {
|
fn test_textobject_surround() {
|
||||||
// (text, [(cursor position, textobject, final range, count), ...])
|
// (text, [(cursor position, textobject, final range, surround char, count), ...])
|
||||||
let tests = &[
|
let tests = &[
|
||||||
(
|
(
|
||||||
"simple (single) surround pairs",
|
"simple (single) surround pairs",
|
||||||
|
|
|
@ -23,5 +23,5 @@ lsp-types = { version = "0.91", features = ["proposed"] }
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
serde_json = "1.0"
|
serde_json = "1.0"
|
||||||
thiserror = "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"
|
tokio-stream = "0.1.8"
|
||||||
|
|
1
helix-syntax/languages/tree-sitter-wgsl
Submodule
1
helix-syntax/languages/tree-sitter-wgsl
Submodule
|
@ -0,0 +1 @@
|
||||||
|
Subproject commit f00ff52251edbd58f4d39c9c3204383253032c11
|
|
@ -122,7 +122,7 @@ impl Application {
|
||||||
if first.is_dir() {
|
if first.is_dir() {
|
||||||
std::env::set_current_dir(&first)?;
|
std::env::set_current_dir(&first)?;
|
||||||
editor.new_file(Action::VerticalSplit);
|
editor.new_file(Action::VerticalSplit);
|
||||||
compositor.push(Box::new(ui::file_picker(".".into())));
|
compositor.push(Box::new(ui::file_picker(".".into(), &config.editor)));
|
||||||
} else {
|
} else {
|
||||||
let nr_of_files = args.files.len();
|
let nr_of_files = args.files.len();
|
||||||
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
|
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
|
||||||
|
@ -270,7 +270,7 @@ impl Application {
|
||||||
use crate::commands::{insert::idle_completion, Context};
|
use crate::commands::{insert::idle_completion, Context};
|
||||||
use helix_view::document::Mode;
|
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;
|
return;
|
||||||
}
|
}
|
||||||
let editor_view = self
|
let editor_view = self
|
||||||
|
|
|
@ -13,7 +13,6 @@ use helix_core::{
|
||||||
numbers::NumberIncrementor,
|
numbers::NumberIncrementor,
|
||||||
object, pos_at_coords,
|
object, pos_at_coords,
|
||||||
regex::{self, Regex, RegexBuilder},
|
regex::{self, Regex, RegexBuilder},
|
||||||
register::Register,
|
|
||||||
search, selection, surround, textobject,
|
search, selection, surround, textobject,
|
||||||
unicode::width::UnicodeWidthChar,
|
unicode::width::UnicodeWidthChar,
|
||||||
LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril,
|
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_line, "Select current line, if already selected, extend to next line",
|
||||||
extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
|
extend_to_line_bounds, "Extend selection to line bounds (line-wise selection)",
|
||||||
delete_selection, "Delete selection",
|
delete_selection, "Delete selection",
|
||||||
|
delete_selection_noyank, "Delete selection, without yanking",
|
||||||
change_selection, "Change selection (delete and enter insert mode)",
|
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",
|
collapse_selection, "Collapse selection onto a single cursor",
|
||||||
flip_selections, "Flip selection cursor and anchor",
|
flip_selections, "Flip selection cursor and anchor",
|
||||||
insert_mode, "Insert before selection",
|
insert_mode, "Insert before selection",
|
||||||
|
@ -262,6 +263,9 @@ impl Command {
|
||||||
goto_implementation, "Goto implementation",
|
goto_implementation, "Goto implementation",
|
||||||
goto_file_start, "Goto file start/line",
|
goto_file_start, "Goto file start/line",
|
||||||
goto_file_end, "Goto file end",
|
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_reference, "Goto references",
|
||||||
goto_window_top, "Goto window top",
|
goto_window_top, "Goto window top",
|
||||||
goto_window_middle, "Goto window middle",
|
goto_window_middle, "Goto window middle",
|
||||||
|
@ -318,6 +322,7 @@ impl Command {
|
||||||
join_selections, "Join lines inside selection",
|
join_selections, "Join lines inside selection",
|
||||||
keep_selections, "Keep selections matching regex",
|
keep_selections, "Keep selections matching regex",
|
||||||
remove_selections, "Remove selections matching regex",
|
remove_selections, "Remove selections matching regex",
|
||||||
|
align_selections, "Align selections in column",
|
||||||
keep_primary_selection, "Keep primary selection",
|
keep_primary_selection, "Keep primary selection",
|
||||||
remove_primary_selection, "Remove primary selection",
|
remove_primary_selection, "Remove primary selection",
|
||||||
completion, "Invoke completion popup",
|
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) {
|
fn goto_window(cx: &mut Context, align: Align) {
|
||||||
|
let count = cx.count() - 1;
|
||||||
let (view, doc) = current!(cx.editor);
|
let (view, doc) = current!(cx.editor);
|
||||||
|
|
||||||
let height = view.inner_area().height as usize;
|
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.
|
// - 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
|
// a height of 6 with padding of 3 on each side will keep shifting the view back and forth
|
||||||
// as we type
|
// as we type
|
||||||
|
@ -689,11 +766,12 @@ fn goto_window(cx: &mut Context, align: Align) {
|
||||||
let last_line = view.last_line(doc);
|
let last_line = view.last_line(doc);
|
||||||
|
|
||||||
let line = match align {
|
let line = match align {
|
||||||
Align::Top => (view.offset.row + scrolloff),
|
Align::Top => (view.offset.row + scrolloff + count),
|
||||||
Align::Center => (view.offset.row + (height / 2)),
|
Align::Center => (view.offset.row + ((last_line - view.offset.row) / 2)),
|
||||||
Align::Bottom => last_line.saturating_sub(scrolloff),
|
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);
|
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);
|
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)
|
fn extend_word_impl<F>(cx: &mut Context, extend_fn: F)
|
||||||
where
|
where
|
||||||
F: Fn(RopeSlice, Range, usize) -> Range,
|
F: Fn(RopeSlice, Range, usize) -> Range,
|
||||||
|
@ -1459,6 +1580,7 @@ fn global_search(cx: &mut Context) {
|
||||||
let (all_matches_sx, all_matches_rx) =
|
let (all_matches_sx, all_matches_rx) =
|
||||||
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
|
tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>();
|
||||||
let smart_case = cx.editor.config.smart_case;
|
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 completions = search_completions(cx, None);
|
||||||
let prompt = ui::regex_prompt(
|
let prompt = ui::regex_prompt(
|
||||||
|
@ -1487,7 +1609,16 @@ fn global_search(cx: &mut Context) {
|
||||||
|
|
||||||
let search_root = std::env::current_dir()
|
let search_root = std::env::current_dir()
|
||||||
.expect("Global search error: Failed to get current dir");
|
.expect("Global search error: Failed to get current dir");
|
||||||
WalkBuilder::new(search_root).build_parallel().run(|| {
|
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 mut searcher_cl = searcher.clone();
|
||||||
let matcher_cl = matcher.clone();
|
let matcher_cl = matcher.clone();
|
||||||
let all_matches_sx_cl = all_matches_sx.clone();
|
let all_matches_sx_cl = all_matches_sx.clone();
|
||||||
|
@ -1514,10 +1645,15 @@ fn global_search(cx: &mut Context) {
|
||||||
Err(_) => Ok(false),
|
Err(_) => Ok(false),
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
let result = searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
|
let result =
|
||||||
|
searcher_cl.search_path(&matcher_cl, dent.path(), result_sink);
|
||||||
|
|
||||||
if let Err(err) = result {
|
if let Err(err) = result {
|
||||||
log::error!("Global search error: {}, {}", dent.path().display(), err);
|
log::error!(
|
||||||
|
"Global search error: {}, {}",
|
||||||
|
dent.path().display(),
|
||||||
|
err
|
||||||
|
);
|
||||||
}
|
}
|
||||||
WalkState::Continue
|
WalkState::Continue
|
||||||
})
|
})
|
||||||
|
@ -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 {
|
||||||
let text = doc.text().slice(..);
|
Delete,
|
||||||
let selection = doc.selection(view_id);
|
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);
|
||||||
|
|
||||||
|
if cx.register != Some('_') {
|
||||||
// first yank the selection
|
// first yank the selection
|
||||||
let values: Vec<String> = selection.fragments(text).map(Cow::into_owned).collect();
|
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);
|
reg.write(values);
|
||||||
|
};
|
||||||
|
|
||||||
// then delete
|
// then delete
|
||||||
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
|
let transaction = Transaction::change_by_selection(doc.text(), selection, |range| {
|
||||||
(range.from(), range.to(), None)
|
(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]
|
#[inline]
|
||||||
|
@ -1653,25 +1812,21 @@ fn delete_selection_insert_mode(doc: &mut Document, view: &View, selection: &Sel
|
||||||
}
|
}
|
||||||
|
|
||||||
fn delete_selection(cx: &mut Context) {
|
fn delete_selection(cx: &mut Context) {
|
||||||
let reg_name = cx.register.unwrap_or('"');
|
delete_selection_impl(cx, Operation::Delete);
|
||||||
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);
|
fn delete_selection_noyank(cx: &mut Context) {
|
||||||
|
cx.register = Some('_');
|
||||||
// exit select mode, if currently in select mode
|
delete_selection_impl(cx, Operation::Delete);
|
||||||
exit_select_mode(cx);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn change_selection(cx: &mut Context) {
|
fn change_selection(cx: &mut Context) {
|
||||||
let reg_name = cx.register.unwrap_or('"');
|
delete_selection_impl(cx, Operation::Change);
|
||||||
let (view, doc) = current!(cx.editor);
|
}
|
||||||
let registers = &mut cx.editor.registers;
|
|
||||||
let reg = registers.get_mut(reg_name);
|
fn change_selection_noyank(cx: &mut Context) {
|
||||||
delete_selection_impl(reg, doc, view.id);
|
cx.register = Some('_');
|
||||||
enter_insert_mode(doc);
|
delete_selection_impl(cx, Operation::Change);
|
||||||
}
|
}
|
||||||
|
|
||||||
fn collapse_selection(cx: &mut Context) {
|
fn collapse_selection(cx: &mut Context) {
|
||||||
|
@ -1820,7 +1975,7 @@ mod cmd {
|
||||||
let jobs = &mut cx.jobs;
|
let jobs = &mut cx.jobs;
|
||||||
let (_, doc) = current!(cx.editor);
|
let (_, doc) = current!(cx.editor);
|
||||||
|
|
||||||
if let Some(path) = path {
|
if let Some(ref path) = path {
|
||||||
doc.set_path(Some(path.as_ref()))
|
doc.set_path(Some(path.as_ref()))
|
||||||
.context("invalid filepath")?;
|
.context("invalid filepath")?;
|
||||||
}
|
}
|
||||||
|
@ -1840,6 +1995,11 @@ mod cmd {
|
||||||
});
|
});
|
||||||
let future = doc.format_and_save(fmt);
|
let future = doc.format_and_save(fmt);
|
||||||
cx.jobs.add(Job::new(future).wait_before_exiting());
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2466,6 +2626,26 @@ mod cmd {
|
||||||
Ok(())
|
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] = &[
|
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||||
TypableCommand {
|
TypableCommand {
|
||||||
name: "quit",
|
name: "quit",
|
||||||
|
@ -2768,6 +2948,13 @@ mod cmd {
|
||||||
fun: tutor,
|
fun: tutor,
|
||||||
completer: None,
|
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(|| {
|
pub static COMMANDS: Lazy<HashMap<&'static str, &'static TypableCommand>> = Lazy::new(|| {
|
||||||
|
@ -2830,6 +3017,15 @@ fn command_mode(cx: &mut Context) {
|
||||||
return;
|
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 Some(cmd) = cmd::COMMANDS.get(parts[0]) {
|
||||||
if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
|
if let Err(e) = (cmd.fun)(cx, &parts[1..], event) {
|
||||||
cx.editor.set_error(format!("{}", e));
|
cx.editor.set_error(format!("{}", e));
|
||||||
|
@ -2855,7 +3051,7 @@ fn command_mode(cx: &mut Context) {
|
||||||
|
|
||||||
fn file_picker(cx: &mut Context) {
|
fn file_picker(cx: &mut Context) {
|
||||||
let root = find_root(None).unwrap_or_else(|| PathBuf::from("./"));
|
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));
|
cx.push_layer(Box::new(picker));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -3463,10 +3659,14 @@ fn push_jump(editor: &mut Editor) {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn goto_line(cx: &mut Context) {
|
fn goto_line(cx: &mut Context) {
|
||||||
if let Some(count) = cx.count {
|
goto_line_impl(&mut cx.editor, cx.count)
|
||||||
push_jump(cx.editor);
|
}
|
||||||
|
|
||||||
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 {
|
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.
|
// If the last line is blank, don't jump to it.
|
||||||
doc.text().len_lines().saturating_sub(2)
|
doc.text().len_lines().saturating_sub(2)
|
||||||
|
@ -5065,7 +5265,9 @@ fn match_brackets(cx: &mut Context) {
|
||||||
if let Some(syntax) = doc.syntax() {
|
if let Some(syntax) = doc.syntax() {
|
||||||
let text = doc.text().slice(..);
|
let text = doc.text().slice(..);
|
||||||
let selection = doc.selection(view.id).clone().transform(|range| {
|
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)
|
range.put_cursor(text, pos, doc.mode == Mode::Select)
|
||||||
} else {
|
} else {
|
||||||
range
|
range
|
||||||
|
|
|
@ -512,6 +512,7 @@ impl Default for Keymaps {
|
||||||
"g" => { "Goto"
|
"g" => { "Goto"
|
||||||
"g" => goto_file_start,
|
"g" => goto_file_start,
|
||||||
"e" => goto_last_line,
|
"e" => goto_last_line,
|
||||||
|
"f" => goto_file,
|
||||||
"h" => goto_line_start,
|
"h" => goto_line_start,
|
||||||
"l" => goto_line_end,
|
"l" => goto_line_end,
|
||||||
"s" => goto_first_nonwhitespace,
|
"s" => goto_first_nonwhitespace,
|
||||||
|
@ -537,9 +538,9 @@ impl Default for Keymaps {
|
||||||
"O" => open_above,
|
"O" => open_above,
|
||||||
|
|
||||||
"d" => delete_selection,
|
"d" => delete_selection,
|
||||||
// TODO: also delete without yanking
|
"A-d" => delete_selection_noyank,
|
||||||
"c" => change_selection,
|
"c" => change_selection,
|
||||||
// TODO: also change delete without yanking
|
"A-c" => change_selection_noyank,
|
||||||
|
|
||||||
"C" => copy_selection_on_next_line,
|
"C" => copy_selection_on_next_line,
|
||||||
"A-C" => copy_selection_on_prev_line,
|
"A-C" => copy_selection_on_prev_line,
|
||||||
|
@ -604,7 +605,7 @@ impl Default for Keymaps {
|
||||||
// "q" => record_macro,
|
// "q" => record_macro,
|
||||||
// "Q" => replay_macro,
|
// "Q" => replay_macro,
|
||||||
|
|
||||||
// & align selections
|
"&" => align_selections,
|
||||||
"_" => trim_selections,
|
"_" => trim_selections,
|
||||||
|
|
||||||
"(" => rotate_selections_backward,
|
"(" => rotate_selections_backward,
|
||||||
|
@ -622,6 +623,8 @@ impl Default for Keymaps {
|
||||||
"C-w" | "w" => rotate_view,
|
"C-w" | "w" => rotate_view,
|
||||||
"C-s" | "s" => hsplit,
|
"C-s" | "s" => hsplit,
|
||||||
"C-v" | "v" => vsplit,
|
"C-v" | "v" => vsplit,
|
||||||
|
"f" => goto_file_hsplit,
|
||||||
|
"F" => goto_file_vsplit,
|
||||||
"C-q" | "q" => wclose,
|
"C-q" | "q" => wclose,
|
||||||
"C-o" | "o" => wonly,
|
"C-o" | "o" => wonly,
|
||||||
"C-h" | "h" | "left" => jump_view_left,
|
"C-h" | "h" | "left" => jump_view_left,
|
||||||
|
@ -670,6 +673,8 @@ impl Default for Keymaps {
|
||||||
"C-w" | "w" => rotate_view,
|
"C-w" | "w" => rotate_view,
|
||||||
"C-s" | "s" => hsplit,
|
"C-s" | "s" => hsplit,
|
||||||
"C-v" | "v" => vsplit,
|
"C-v" | "v" => vsplit,
|
||||||
|
"f" => goto_file_hsplit,
|
||||||
|
"F" => goto_file_vsplit,
|
||||||
"C-q" | "q" => wclose,
|
"C-q" | "q" => wclose,
|
||||||
"C-o" | "o" => wonly,
|
"C-o" | "o" => wonly,
|
||||||
"C-h" | "h" | "left" => jump_view_left,
|
"C-h" | "h" | "left" => jump_view_left,
|
||||||
|
|
|
@ -18,7 +18,6 @@ use helix_core::{
|
||||||
use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
|
use helix_dap::{Breakpoint, SourceBreakpoint, StackFrame};
|
||||||
use helix_view::{
|
use helix_view::{
|
||||||
document::{Mode, SCRATCH_BUFFER_NAME},
|
document::{Mode, SCRATCH_BUFFER_NAME},
|
||||||
editor::LineNumber,
|
|
||||||
graphics::{Color, CursorKind, Modifier, Rect, Style},
|
graphics::{Color, CursorKind, Modifier, Rect, Style},
|
||||||
info::Info,
|
info::Info,
|
||||||
input::KeyEvent,
|
input::KeyEvent,
|
||||||
|
@ -392,7 +391,7 @@ impl EditorView {
|
||||||
use helix_core::match_brackets;
|
use helix_core::match_brackets;
|
||||||
let pos = doc.selection(view.id).primary().cursor(text);
|
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));
|
.and_then(|pos| view.screen_coords_at_pos(doc, text, pos));
|
||||||
|
|
||||||
if let Some(pos) = pos {
|
if let Some(pos) = pos {
|
||||||
|
@ -430,22 +429,6 @@ impl EditorView {
|
||||||
let text = doc.text().slice(..);
|
let text = doc.text().slice(..);
|
||||||
let last_line = view.last_line(doc);
|
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:
|
// it's used inside an iterator so the collect isn't needless:
|
||||||
// https://github.com/rust-lang/rust-clippy/issues/6164
|
// https://github.com/rust-lang/rust-clippy/issues/6164
|
||||||
#[allow(clippy::needless_collect)]
|
#[allow(clippy::needless_collect)]
|
||||||
|
@ -455,147 +438,138 @@ impl EditorView {
|
||||||
.map(|range| range.cursor_line(text))
|
.map(|range| range.cursor_line(text))
|
||||||
.collect();
|
.collect();
|
||||||
|
|
||||||
let mut breakpoints: Option<&Vec<SourceBreakpoint>> = None;
|
use helix_view::gutter::GutterFn;
|
||||||
let mut stack_frame: Option<&StackFrame> = None;
|
fn breakpoints<'doc>(
|
||||||
if let Some(path) = doc.path() {
|
doc: &'doc Document,
|
||||||
breakpoints = all_breakpoints.get(path);
|
_view: &View,
|
||||||
if let Some(debugger) = debugger {
|
theme: &Theme,
|
||||||
// if we have a frame, and the frame path matches document
|
_config: &Config,
|
||||||
if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id)
|
_is_focused: bool,
|
||||||
{
|
_width: usize,
|
||||||
let frame = debugger
|
) -> GutterFn<'doc> {
|
||||||
.stack_frames
|
Box::new(move |line: usize, _selected: bool, out: &mut String| {
|
||||||
.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;
|
|
||||||
}
|
}
|
||||||
};
|
// 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 {
|
||||||
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
|
// // if we have a frame, and the frame path matches document
|
||||||
use helix_core::diagnostic::Severity;
|
// if let (Some(frame), Some(thread_id)) = (debugger.active_frame, debugger.thread_id)
|
||||||
if let Ok(diagnostic) = doc.diagnostics().binary_search_by_key(&line, |d| d.line) {
|
// {
|
||||||
let diagnostic = &doc.diagnostics()[diagnostic];
|
// let frame = debugger
|
||||||
surface.set_stringn(
|
// .stack_frames
|
||||||
viewport.x,
|
// .get(&thread_id)
|
||||||
viewport.y + i as u16,
|
// .and_then(|bt| bt.get(frame)); // TODO: drop the clone..
|
||||||
"●",
|
// if let Some(StackFrame {
|
||||||
1,
|
// source: Some(source),
|
||||||
match diagnostic.severity {
|
// ..
|
||||||
Some(Severity::Error) => error,
|
// }) = &frame
|
||||||
Some(Severity::Warning) | None => warning,
|
// {
|
||||||
Some(Severity::Info) => info,
|
// if source.path.as_ref() == Some(path) {
|
||||||
Some(Severity::Hint) => hint,
|
// stack_frame = frame;
|
||||||
},
|
// }
|
||||||
);
|
// };
|
||||||
}
|
// };
|
||||||
|
// }
|
||||||
let selected = cursors.contains(&line);
|
// }
|
||||||
|
|
||||||
// TODO: debugger should translate received breakpoints to 0-indexing
|
// TODO: debugger should translate received breakpoints to 0-indexing
|
||||||
|
|
||||||
if let Some(user) = breakpoints.as_ref() {
|
// if let Some(user) = breakpoints.as_ref() {
|
||||||
let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() {
|
// let debugger_breakpoint = if let Some(debugger) = dbg_breakpoints.as_ref() {
|
||||||
debugger.iter().find(|breakpoint| {
|
// debugger.iter().find(|breakpoint| {
|
||||||
if breakpoint.source.is_some()
|
// if breakpoint.source.is_some()
|
||||||
&& doc.path().is_some()
|
// && doc.path().is_some()
|
||||||
&& breakpoint.source.as_ref().unwrap().path == doc.path().cloned()
|
// && breakpoint.source.as_ref().unwrap().path == doc.path().cloned()
|
||||||
{
|
// {
|
||||||
match (breakpoint.line, breakpoint.end_line) {
|
// match (breakpoint.line, breakpoint.end_line) {
|
||||||
#[allow(clippy::int_plus_one)]
|
// #[allow(clippy::int_plus_one)]
|
||||||
(Some(l), Some(el)) => l - 1 <= line && line <= el - 1,
|
// (Some(l), Some(el)) => l - 1 <= line && line <= el - 1,
|
||||||
(Some(l), None) => l - 1 == line,
|
// (Some(l), None) => l - 1 == line,
|
||||||
_ => false,
|
// _ => false,
|
||||||
}
|
// }
|
||||||
} else {
|
// } else {
|
||||||
false
|
// false
|
||||||
}
|
// }
|
||||||
})
|
// })
|
||||||
} else {
|
// } else {
|
||||||
None
|
// None
|
||||||
};
|
// };
|
||||||
|
|
||||||
if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line)
|
// if let Some(breakpoint) = user.iter().find(|breakpoint| breakpoint.line - 1 == line)
|
||||||
{
|
// {
|
||||||
let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false);
|
// let verified = debugger_breakpoint.map(|b| b.verified).unwrap_or(false);
|
||||||
let mut style =
|
// let mut style =
|
||||||
if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
|
// if breakpoint.condition.is_some() && breakpoint.log_message.is_some() {
|
||||||
error.add_modifier(Modifier::UNDERLINED)
|
// error.add_modifier(Modifier::UNDERLINED)
|
||||||
} else if breakpoint.condition.is_some() {
|
// } else if breakpoint.condition.is_some() {
|
||||||
error
|
// error
|
||||||
} else if breakpoint.log_message.is_some() {
|
// } else if breakpoint.log_message.is_some() {
|
||||||
info
|
// info
|
||||||
} else {
|
// } else {
|
||||||
warning
|
// warning
|
||||||
};
|
// };
|
||||||
if !verified {
|
// if !verified {
|
||||||
// Faded colors
|
// // Faded colors
|
||||||
style = if let Some(Color::Rgb(r, g, b)) = style.fg {
|
// style = if let Some(Color::Rgb(r, g, b)) = style.fg {
|
||||||
style.fg(Color::Rgb(
|
// style.fg(Color::Rgb(
|
||||||
((r as f32) * 0.4).floor() as u8,
|
// ((r as f32) * 0.4).floor() as u8,
|
||||||
((g as f32) * 0.4).floor() as u8,
|
// ((g as f32) * 0.4).floor() as u8,
|
||||||
((b as f32) * 0.4).floor() as u8,
|
// ((b as f32) * 0.4).floor() as u8,
|
||||||
))
|
// ))
|
||||||
} else {
|
// } else {
|
||||||
style.fg(Color::Gray)
|
// style.fg(Color::Gray)
|
||||||
}
|
// }
|
||||||
};
|
// };
|
||||||
surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style);
|
// surface.set_stringn(viewport.x, viewport.y + i as u16, "▲", 1, style);
|
||||||
} else if let Some(breakpoint) = debugger_breakpoint {
|
// } else if let Some(breakpoint) = debugger_breakpoint {
|
||||||
let style = if breakpoint.verified {
|
// let style = if breakpoint.verified {
|
||||||
info
|
// info
|
||||||
} else {
|
// } else {
|
||||||
info.fg(Color::Gray)
|
// info.fg(Color::Gray)
|
||||||
};
|
// };
|
||||||
surface.set_stringn(viewport.x, viewport.y + i as u16, "⊚", 1, style);
|
// surface.set_stringn(viewport.x, viewport.y + i as u16, "⊚", 1, style);
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
if let Some(frame) = stack_frame {
|
// if let Some(frame) = stack_frame {
|
||||||
if frame.line - 1 == line {
|
// if frame.line - 1 == line {
|
||||||
surface.set_style(
|
// surface.set_style(
|
||||||
Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
|
// Rect::new(viewport.x, viewport.y + i as u16, 6, 1),
|
||||||
helix_view::graphics::Style::default()
|
// helix_view::graphics::Style::default()
|
||||||
.bg(helix_view::graphics::Color::LightYellow),
|
// .bg(helix_view::graphics::Color::LightYellow),
|
||||||
);
|
// );
|
||||||
}
|
// }
|
||||||
}
|
// }
|
||||||
|
|
||||||
let text = if line == last_line && !draw_last {
|
let mut offset = 0;
|
||||||
" ~".into()
|
|
||||||
} else {
|
// avoid lots of small allocations by reusing a text buffer for each line
|
||||||
let line = match config.line_number {
|
let mut text = String::with_capacity(8);
|
||||||
LineNumber::Absolute => line + 1,
|
|
||||||
LineNumber::Relative => {
|
for (constructor, width) in view.gutters() {
|
||||||
if current_line == line {
|
let gutter = constructor(doc, view, theme, config, is_focused, *width);
|
||||||
line + 1
|
text.reserve(*width); // ensure there's enough space for the gutter
|
||||||
} else {
|
for (i, line) in (view.offset.row..(last_line + 1)).enumerate() {
|
||||||
abs_diff(current_line, line)
|
let selected = cursors.contains(&line);
|
||||||
}
|
|
||||||
}
|
if let Some(style) = gutter(line, selected, &mut text) {
|
||||||
};
|
|
||||||
format!("{:>5}", line)
|
|
||||||
};
|
|
||||||
surface.set_stringn(
|
surface.set_stringn(
|
||||||
viewport.x + 1,
|
viewport.x + offset,
|
||||||
viewport.y + i as u16,
|
viewport.y + i as u16,
|
||||||
text,
|
&text,
|
||||||
5,
|
*width,
|
||||||
if selected && is_focused {
|
style,
|
||||||
linenr_select
|
|
||||||
} else {
|
|
||||||
linenr
|
|
||||||
},
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
text.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += *width as u16;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn render_diagnostics(
|
pub fn render_diagnostics(
|
||||||
|
@ -1364,12 +1338,3 @@ fn canonicalize_key(key: &mut KeyEvent) {
|
||||||
key.modifiers.remove(KeyModifiers::SHIFT)
|
key.modifiers.remove(KeyModifiers::SHIFT)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[inline]
|
|
||||||
const fn abs_diff(a: usize, b: usize) -> usize {
|
|
||||||
if a > b {
|
|
||||||
a - b
|
|
||||||
} else {
|
|
||||||
b - a
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -55,7 +55,7 @@ fn parse<'a>(
|
||||||
fn to_span(text: pulldown_cmark::CowStr) -> Span {
|
fn to_span(text: pulldown_cmark::CowStr) -> Span {
|
||||||
use std::ops::Deref;
|
use std::ops::Deref;
|
||||||
Span::raw::<std::borrow::Cow<_>>(match text {
|
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::Boxed(s) => s.to_string().into(),
|
||||||
CowStr::Inlined(s) => s.deref().to_owned().into(),
|
CowStr::Inlined(s) => s.deref().to_owned().into(),
|
||||||
})
|
})
|
||||||
|
@ -179,7 +179,9 @@ fn parse<'a>(
|
||||||
spans.push(Span::raw(" "));
|
spans.push(Span::raw(" "));
|
||||||
}
|
}
|
||||||
Event::Rule => {
|
Event::Rule => {
|
||||||
lines.push(Spans::from("---"));
|
let mut span = Span::raw("---");
|
||||||
|
span.style = code_style;
|
||||||
|
lines.push(Spans::from(span));
|
||||||
lines.push(Spans::default());
|
lines.push(Spans::default());
|
||||||
}
|
}
|
||||||
// TaskListMarker(bool) true if checked
|
// TaskListMarker(bool) true if checked
|
||||||
|
@ -226,6 +228,7 @@ impl Component for Markdown {
|
||||||
return None;
|
return None;
|
||||||
}
|
}
|
||||||
let contents = parse(&self.contents, None, &self.config_loader);
|
let contents = parse(&self.contents, None, &self.config_loader);
|
||||||
|
// TODO: account for tab width
|
||||||
let max_text_width = (viewport.0 - padding).min(120);
|
let max_text_width = (viewport.0 - padding).min(120);
|
||||||
let mut text_width = 0;
|
let mut text_width = 0;
|
||||||
let mut height = padding;
|
let mut height = padding;
|
||||||
|
|
|
@ -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 ignore::{types::TypesBuilder, WalkBuilder};
|
||||||
use std::time;
|
use std::time;
|
||||||
|
|
||||||
// We want to exclude files that the editor can't handle yet
|
// We want to exclude files that the editor can't handle yet
|
||||||
let mut type_builder = TypesBuilder::new();
|
let mut type_builder = TypesBuilder::new();
|
||||||
let mut walk_builder = WalkBuilder::new(&root);
|
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(
|
let walk_builder = match type_builder.add(
|
||||||
"compressed",
|
"compressed",
|
||||||
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
|
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
|
||||||
|
|
|
@ -14,6 +14,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||||
use std::{
|
use std::{
|
||||||
collections::{BTreeMap, HashMap},
|
collections::{BTreeMap, HashMap},
|
||||||
io::stdin,
|
io::stdin,
|
||||||
|
num::NonZeroUsize,
|
||||||
path::{Path, PathBuf},
|
path::{Path, PathBuf},
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
|
@ -21,7 +22,7 @@ use std::{
|
||||||
|
|
||||||
use tokio::time::{sleep, Duration, Instant, Sleep};
|
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::diagnostic::Severity;
|
||||||
pub use helix_core::register::Registers;
|
pub use helix_core::register::Registers;
|
||||||
|
@ -39,6 +40,46 @@ where
|
||||||
Ok(Duration::from_millis(millis))
|
Ok(Duration::from_millis(millis))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[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)]
|
#[derive(Debug, Clone, PartialEq, Deserialize)]
|
||||||
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
#[serde(rename_all = "kebab-case", default, deny_unknown_fields)]
|
||||||
pub struct Config {
|
pub struct Config {
|
||||||
|
@ -66,9 +107,10 @@ pub struct Config {
|
||||||
pub completion_trigger_len: u8,
|
pub completion_trigger_len: u8,
|
||||||
/// Whether to display infoboxes. Defaults to true.
|
/// Whether to display infoboxes. Defaults to true.
|
||||||
pub auto_info: bool,
|
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")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum LineNumber {
|
pub enum LineNumber {
|
||||||
/// Show absolute line number
|
/// Show absolute line number
|
||||||
|
@ -97,6 +139,7 @@ impl Default for Config {
|
||||||
idle_timeout: Duration::from_millis(400),
|
idle_timeout: Duration::from_millis(400),
|
||||||
completion_trigger_len: 2,
|
completion_trigger_len: 2,
|
||||||
auto_info: true,
|
auto_info: true,
|
||||||
|
file_picker: FilePickerConfig::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -116,7 +159,7 @@ impl std::fmt::Debug for Motion {
|
||||||
#[derive(Debug)]
|
#[derive(Debug)]
|
||||||
pub struct Editor {
|
pub struct Editor {
|
||||||
pub tree: Tree,
|
pub tree: Tree,
|
||||||
pub next_document_id: usize,
|
pub next_document_id: DocumentId,
|
||||||
pub documents: BTreeMap<DocumentId, Document>,
|
pub documents: BTreeMap<DocumentId, Document>,
|
||||||
pub count: Option<std::num::NonZeroUsize>,
|
pub count: Option<std::num::NonZeroUsize>,
|
||||||
pub selected_register: Option<char>,
|
pub selected_register: Option<char>,
|
||||||
|
@ -154,8 +197,8 @@ pub enum Action {
|
||||||
impl Editor {
|
impl Editor {
|
||||||
pub fn new(
|
pub fn new(
|
||||||
mut area: Rect,
|
mut area: Rect,
|
||||||
themes: Arc<theme::Loader>,
|
theme_loader: Arc<theme::Loader>,
|
||||||
config_loader: Arc<syntax::Loader>,
|
syn_loader: Arc<syntax::Loader>,
|
||||||
config: Config,
|
config: Config,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
let language_servers = helix_lsp::Registry::new();
|
let language_servers = helix_lsp::Registry::new();
|
||||||
|
@ -165,17 +208,17 @@ impl Editor {
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
tree: Tree::new(area),
|
tree: Tree::new(area),
|
||||||
next_document_id: 0,
|
next_document_id: DocumentId::default(),
|
||||||
documents: BTreeMap::new(),
|
documents: BTreeMap::new(),
|
||||||
count: None,
|
count: None,
|
||||||
selected_register: None,
|
selected_register: None,
|
||||||
theme: themes.default(),
|
theme: theme_loader.default(),
|
||||||
language_servers,
|
language_servers,
|
||||||
debugger: None,
|
debugger: None,
|
||||||
debugger_events: SelectAll::new(),
|
debugger_events: SelectAll::new(),
|
||||||
breakpoints: HashMap::new(),
|
breakpoints: HashMap::new(),
|
||||||
syn_loader: config_loader,
|
syn_loader,
|
||||||
theme_loader: themes,
|
theme_loader,
|
||||||
registers: Registers::default(),
|
registers: Registers::default(),
|
||||||
clipboard_provider: get_clipboard_provider(),
|
clipboard_provider: get_clipboard_provider(),
|
||||||
status_msg: None,
|
status_msg: None,
|
||||||
|
@ -232,7 +275,6 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
|
pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
|
||||||
use anyhow::Context;
|
|
||||||
let theme = self
|
let theme = self
|
||||||
.theme_loader
|
.theme_loader
|
||||||
.load(theme.as_ref())
|
.load(theme.as_ref())
|
||||||
|
@ -241,6 +283,53 @@ impl Editor {
|
||||||
Ok(())
|
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) {
|
fn _refresh(&mut self) {
|
||||||
for (view, _) in self.tree.views_mut() {
|
for (view, _) in self.tree.views_mut() {
|
||||||
let doc = &self.documents[&view.doc];
|
let doc = &self.documents[&view.doc];
|
||||||
|
@ -311,23 +400,22 @@ impl Editor {
|
||||||
}
|
}
|
||||||
Action::Load => {
|
Action::Load => {
|
||||||
let view_id = view!(self).id;
|
let view_id = view!(self).id;
|
||||||
if let Some(doc) = self.document_mut(id) {
|
let doc = self.documents.get_mut(&id).unwrap();
|
||||||
if doc.selections().is_empty() {
|
if doc.selections().is_empty() {
|
||||||
doc.selections.insert(view_id, Selection::point(0));
|
doc.selections.insert(view_id, Selection::point(0));
|
||||||
}
|
}
|
||||||
}
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Action::HorizontalSplit => {
|
Action::HorizontalSplit | Action::VerticalSplit => {
|
||||||
let view = View::new(id);
|
let view = View::new(id);
|
||||||
let view_id = self.tree.split(view, Layout::Horizontal);
|
let view_id = self.tree.split(
|
||||||
// initialize selection for view
|
view,
|
||||||
let doc = self.documents.get_mut(&id).unwrap();
|
match action {
|
||||||
doc.selections.insert(view_id, Selection::point(0));
|
Action::HorizontalSplit => Layout::Horizontal,
|
||||||
}
|
Action::VerticalSplit => Layout::Vertical,
|
||||||
Action::VerticalSplit => {
|
_ => unreachable!(),
|
||||||
let view = View::new(id);
|
},
|
||||||
let view_id = self.tree.split(view, Layout::Vertical);
|
);
|
||||||
// initialize selection for view
|
// initialize selection for view
|
||||||
let doc = self.documents.get_mut(&id).unwrap();
|
let doc = self.documents.get_mut(&id).unwrap();
|
||||||
doc.selections.insert(view_id, Selection::point(0));
|
doc.selections.insert(view_id, Selection::point(0));
|
||||||
|
@ -337,16 +425,19 @@ impl Editor {
|
||||||
self._refresh();
|
self._refresh();
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_document(&mut self, mut document: Document) -> DocumentId {
|
/// Generate an id for a new document and register it.
|
||||||
let id = DocumentId(self.next_document_id);
|
fn new_document(&mut self, mut doc: Document) -> DocumentId {
|
||||||
self.next_document_id += 1;
|
let id = self.next_document_id;
|
||||||
document.id = id;
|
// Safety: adding 1 from 1 is fine, probably impossible to reach usize max
|
||||||
self.documents.insert(id, document);
|
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
|
id
|
||||||
}
|
}
|
||||||
|
|
||||||
fn new_file_from_document(&mut self, action: Action, document: Document) -> DocumentId {
|
fn new_file_from_document(&mut self, action: Action, doc: Document) -> DocumentId {
|
||||||
let id = self.new_document(document);
|
let id = self.new_document(doc);
|
||||||
self.switch(id, action);
|
self.switch(id, action);
|
||||||
id
|
id
|
||||||
}
|
}
|
||||||
|
@ -362,54 +453,16 @@ impl Editor {
|
||||||
|
|
||||||
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
|
pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Error> {
|
||||||
let path = helix_core::path::get_canonicalized_path(&path)?;
|
let path = helix_core::path::get_canonicalized_path(&path)?;
|
||||||
|
let id = self.document_by_path(&path).map(|doc| doc.id);
|
||||||
let id = self
|
|
||||||
.documents()
|
|
||||||
.find(|doc| doc.path() == Some(&path))
|
|
||||||
.map(|doc| doc.id);
|
|
||||||
|
|
||||||
let id = if let Some(id) = id {
|
let id = if let Some(id) = id {
|
||||||
id
|
id
|
||||||
} else {
|
} else {
|
||||||
let mut doc = Document::open(&path, None, Some(&self.theme), Some(&self.syn_loader))?;
|
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 _ = Self::launch_language_server(&mut self.language_servers, &mut doc);
|
||||||
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 {
|
self.new_document(doc)
|
||||||
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 id = DocumentId(self.next_document_id);
|
|
||||||
self.next_document_id += 1;
|
|
||||||
doc.id = id;
|
|
||||||
self.documents.insert(id, doc);
|
|
||||||
id
|
|
||||||
};
|
};
|
||||||
|
|
||||||
self.switch(id, action);
|
self.switch(id, action);
|
||||||
|
@ -432,11 +485,11 @@ impl Editor {
|
||||||
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
|
pub fn close_document(&mut self, doc_id: DocumentId, force: bool) -> anyhow::Result<()> {
|
||||||
let doc = match self.documents.get(&doc_id) {
|
let doc = match self.documents.get(&doc_id) {
|
||||||
Some(doc) => doc,
|
Some(doc) => doc,
|
||||||
None => anyhow::bail!("document does not exist"),
|
None => bail!("document does not exist"),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !force && doc.is_modified() {
|
if !force && doc.is_modified() {
|
||||||
anyhow::bail!(
|
bail!(
|
||||||
"buffer {:?} is modified",
|
"buffer {:?} is modified",
|
||||||
doc.relative_path()
|
doc.relative_path()
|
||||||
.map(|path| path.to_string_lossy().to_string())
|
.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
|
// 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
|
// 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.
|
// 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
|
let doc_id = self
|
||||||
.documents
|
.documents
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -554,8 +607,7 @@ impl Editor {
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
|
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
|
||||||
let view = view!(self);
|
let (view, doc) = current_ref!(self);
|
||||||
let doc = &self.documents[&view.doc];
|
|
||||||
let cursor = doc
|
let cursor = doc
|
||||||
.selection(view.id)
|
.selection(view.id)
|
||||||
.primary()
|
.primary()
|
||||||
|
|
95
helix-view/src/gutter.rs
Normal file
95
helix-view/src/gutter.rs
Normal file
|
@ -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
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,6 +5,7 @@ pub mod clipboard;
|
||||||
pub mod document;
|
pub mod document;
|
||||||
pub mod editor;
|
pub mod editor;
|
||||||
pub mod graphics;
|
pub mod graphics;
|
||||||
|
pub mod gutter;
|
||||||
pub mod info;
|
pub mod info;
|
||||||
pub mod input;
|
pub mod input;
|
||||||
pub mod keyboard;
|
pub mod keyboard;
|
||||||
|
@ -12,8 +13,18 @@ pub mod theme;
|
||||||
pub mod tree;
|
pub mod tree;
|
||||||
pub mod view;
|
pub mod view;
|
||||||
|
|
||||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
|
use std::num::NonZeroUsize;
|
||||||
pub struct DocumentId(usize);
|
|
||||||
|
// 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! {
|
slotmap::new_key_type! {
|
||||||
pub struct ViewId;
|
pub struct ViewId;
|
||||||
|
|
|
@ -1,6 +1,10 @@
|
||||||
use std::borrow::Cow;
|
use std::borrow::Cow;
|
||||||
|
|
||||||
use crate::{graphics::Rect, Document, DocumentId, ViewId};
|
use crate::{
|
||||||
|
graphics::Rect,
|
||||||
|
gutter::{self, Gutter},
|
||||||
|
Document, DocumentId, ViewId,
|
||||||
|
};
|
||||||
use helix_core::{
|
use helix_core::{
|
||||||
graphemes::{grapheme_width, RopeGraphemes},
|
graphemes::{grapheme_width, RopeGraphemes},
|
||||||
line_ending::line_end_char_index,
|
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)]
|
#[derive(Debug)]
|
||||||
pub struct View {
|
pub struct View {
|
||||||
pub id: ViewId,
|
pub id: ViewId,
|
||||||
|
@ -83,10 +89,19 @@ impl View {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn gutters(&self) -> &[(Gutter, usize)] {
|
||||||
|
GUTTERS
|
||||||
|
}
|
||||||
|
|
||||||
pub fn inner_area(&self) -> Rect {
|
pub fn inner_area(&self) -> Rect {
|
||||||
// TODO: not ideal
|
// TODO: cache this
|
||||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
let offset = self
|
||||||
self.area.clip_left(OFFSET).clip_bottom(1) // -1 for statusline
|
.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 super::*;
|
||||||
use helix_core::Rope;
|
use helix_core::Rope;
|
||||||
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
const OFFSET: u16 = 7; // 1 diagnostic + 5 linenr + 1 gutter
|
||||||
|
// const OFFSET: u16 = GUTTERS.iter().map(|(_, width)| *width as u16).sum();
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_text_pos_at_screen_coords() {
|
fn test_text_pos_at_screen_coords() {
|
||||||
|
|
|
@ -508,3 +508,28 @@ shebangs = ["perl"]
|
||||||
roots = []
|
roots = []
|
||||||
comment-token = "#"
|
comment-token = "#"
|
||||||
indent = { tab-width = 2, unit = " " }
|
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 = " " }
|
||||||
|
|
14
runtime/queries/llvm/highlights.scm
Normal file
14
runtime/queries/llvm/highlights.scm
Normal file
|
@ -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
|
102
runtime/queries/wgsl/highlights.scm
Normal file
102
runtime/queries/wgsl/highlights.scm
Normal file
|
@ -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
|
|
@ -28,18 +28,18 @@
|
||||||
# 行号栏
|
# 行号栏
|
||||||
"ui.linenr" = { fg = "base0", bg = "base02" }
|
"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窗口
|
# 补全窗口, preview窗口
|
||||||
"ui.popup" = { bg = "base1" }
|
"ui.popup" = { bg = "base02" }
|
||||||
# 影响 补全选中 cmd弹出信息选中
|
# 影响 补全选中 cmd弹出信息选中
|
||||||
"ui.menu.selected" = { fg = "base02", bg = "violet"}
|
"ui.menu.selected" = { fg = "base02", bg = "base2"}
|
||||||
"ui.menu" = { fg = "base02" }
|
"ui.menu" = { fg = "base1" }
|
||||||
# ??
|
# ??
|
||||||
"ui.window" = { fg = "base3" }
|
"ui.window" = { fg = "base3" }
|
||||||
# 命令行 补全的帮助信息
|
# 命令行 补全的帮助信息
|
||||||
|
|
|
@ -28,18 +28,18 @@
|
||||||
# 行号栏
|
# 行号栏
|
||||||
"ui.linenr" = { fg = "base0", bg = "base02" }
|
"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窗口
|
# 补全窗口, preview窗口
|
||||||
"ui.popup" = { bg = "base1" }
|
"ui.popup" = { bg = "base02" }
|
||||||
# 影响 补全选中 cmd弹出信息选中
|
# 影响 补全选中 cmd弹出信息选中
|
||||||
"ui.menu.selected" = { fg = "base02", bg = "violet"}
|
"ui.menu.selected" = { fg = "base02", bg = "base2"}
|
||||||
"ui.menu" = { fg = "base02" }
|
"ui.menu" = { fg = "base1" }
|
||||||
# ??
|
# ??
|
||||||
"ui.window" = { fg = "base3" }
|
"ui.window" = { fg = "base3" }
|
||||||
# 命令行 补全的帮助信息
|
# 命令行 补全的帮助信息
|
||||||
|
|
Loading…
Add table
Reference in a new issue