mirror of
https://github.com/helix-editor/helix.git
synced 2024-11-22 09:26:19 +04:00
Merge branch 'master' into debug
This commit is contained in:
commit
f2b709a3c3
18
.github/workflows/build.yml
vendored
18
.github/workflows/build.yml
vendored
@ -28,19 +28,19 @@ jobs:
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo target dir
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run cargo check
|
||||
uses: actions-rs/cargo@v1
|
||||
@ -67,19 +67,19 @@ jobs:
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo target dir
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run cargo test
|
||||
uses: actions-rs/cargo@v1
|
||||
@ -112,19 +112,19 @@ jobs:
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/registry
|
||||
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo index
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: ~/.cargo/git
|
||||
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Cache cargo target dir
|
||||
uses: actions/cache@v2.1.6
|
||||
with:
|
||||
path: target
|
||||
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
|
||||
|
||||
- name: Run cargo fmt
|
||||
uses: actions-rs/cargo@v1
|
||||
|
6
.gitmodules
vendored
6
.gitmodules
vendored
@ -84,7 +84,7 @@
|
||||
shallow = true
|
||||
[submodule "helix-syntax/languages/tree-sitter-elixir"]
|
||||
path = helix-syntax/languages/tree-sitter-elixir
|
||||
url = https://github.com/IceDragon200/tree-sitter-elixir
|
||||
url = https://github.com/elixir-lang/tree-sitter-elixir
|
||||
shallow = true
|
||||
[submodule "helix-syntax/languages/tree-sitter-nix"]
|
||||
path = helix-syntax/languages/tree-sitter-nix
|
||||
@ -130,3 +130,7 @@
|
||||
path = helix-syntax/languages/tree-sitter-tsq
|
||||
url = https://github.com/tree-sitter/tree-sitter-tsq
|
||||
shallow = true
|
||||
[submodule "helix-syntax/languages/tree-sitter-cmake"]
|
||||
path = helix-syntax/languages/tree-sitter-cmake
|
||||
url = https://github.com/uyha/tree-sitter-cmake
|
||||
shallow = true
|
||||
|
87
CHANGELOG.md
87
CHANGELOG.md
@ -1,4 +1,85 @@
|
||||
|
||||
# 0.5.0 (2021-10-28)
|
||||
|
||||
A big shout out to all the contributors! We had 46 contributors in this release.
|
||||
|
||||
Helix has popped up in [Scoop, FreeBSD Ports and Gentu GURU](https://repology.org/project/helix/versions)!
|
||||
|
||||
The following is a quick rundown of the larger changes, there were many more
|
||||
(check the git history for more details).
|
||||
|
||||
Breaking changes:
|
||||
|
||||
- A couple of keymaps moved to resolve a few conflicting keybinds.
|
||||
- Documentation popups were moved from `K` to `space+k`
|
||||
- `K` is now `keep_selections` which filters selections to only keeps ones matching the regex
|
||||
- `keep_primary_selection` moved from `space+space` to `,`
|
||||
- `Alt-,` is now `remove_primary_selection` which keeps all selections except the primary one
|
||||
- Opening files in a split moved from `C-h` to `C-s`
|
||||
- Some configuration options moved from a `[terminal]` section to `[editor]`. [Consult the documentation for more information.](https://docs.helix-editor.com/configuration.html)
|
||||
|
||||
Features:
|
||||
|
||||
- LSP compatibility greatly improved for some implementations (Julia, Python, Typescript)
|
||||
- Autocompletion! Completion now triggers automatically after a set idle timeout
|
||||
- Completion documentation is now displayed next to the popup ([#691](https://github.com/helix-editor/helix/pull/691))
|
||||
- Treesitter textobjects (select a function via `mf`, class via `mc`) ([#728](https://github.com/helix-editor/helix/pull/728))
|
||||
- Global search across entire workspace `space+/` ([#651](https://github.com/helix-editor/helix/pull/651))
|
||||
- Relative line number support ([#485](https://github.com/helix-editor/helix/pull/485))
|
||||
- Prompts now store a history (72cf86e)
|
||||
- `:vsplit` and `:hsplit` commands ([#639](https://github.com/helix-editor/helix/pull/639))
|
||||
- `C-w h/j/k/l` can now be used to navigate between splits ([#860](https://github.com/helix-editor/helix/pull/860))
|
||||
- `C-j` and `C-k` are now alternative keybindings to `C-n` and `C-p` in the UI ([#876](https://github.com/helix-editor/helix/pull/876))
|
||||
- Shell commands (shell-pipe, pipe-to, shell-insert-output, shell-append-output, keep-pipe) ([#547](https://github.com/helix-editor/helix/pull/547))
|
||||
- Searching now defaults to smart case search (case insensitive unless uppercase is used) ([#761](https://github.com/helix-editor/helix/pull/761))
|
||||
- The preview pane was improved to highlight and center line ranges
|
||||
- The user `languages.toml` is now merged into defaults, no longer need to copy the entire file (dc57f8dc)
|
||||
- Show hidden files in completions ([#648](https://github.com/helix-editor/helix/pull/648))
|
||||
- Grammar injections are now properly handled (dd0b15e)
|
||||
- `v` in select mode now switches back to normal mode ([#660](https://github.com/helix-editor/helix/pull/660))
|
||||
- View mode can now be triggered as a "sticky" mode ([#719](https://github.com/helix-editor/helix/pull/719))
|
||||
- `f`/`t` and object selection motions can now be repeated via `Alt-.` ([#891](https://github.com/helix-editor/helix/pull/891))
|
||||
- Statusline now displays total selection count and diagnostics counts for both errors and warnings ([#916](https://github.com/helix-editor/helix/pull/916))
|
||||
|
||||
New grammars:
|
||||
|
||||
- Ledger ([#572](https://github.com/helix-editor/helix/pull/572))
|
||||
- Protobuf ([#614](https://github.com/helix-editor/helix/pull/614))
|
||||
- Zig ([#631](https://github.com/helix-editor/helix/pull/631))
|
||||
- YAML ([#667](https://github.com/helix-editor/helix/pull/667))
|
||||
- Lua ([#665](https://github.com/helix-editor/helix/pull/665))
|
||||
- OCaml ([#666](https://github.com/helix-editor/helix/pull/666))
|
||||
- Svelte ([#733](https://github.com/helix-editor/helix/pull/733))
|
||||
- Vue ([#787](https://github.com/helix-editor/helix/pull/787))
|
||||
- Tree-sitter queries ([#845](https://github.com/helix-editor/helix/pull/845))
|
||||
- CMake ([#888](https://github.com/helix-editor/helix/pull/888))
|
||||
- Elixir (we switched over to the official grammar) (6c0786e)
|
||||
- Language server definitions for Nix and Elixir ([#725](https://github.com/helix-editor/helix/pull/725))
|
||||
- Python now uses `pylsp` instead of `pyls`
|
||||
- Python now supports indentation
|
||||
|
||||
New themes:
|
||||
|
||||
- Monokai ([#628](https://github.com/helix-editor/helix/pull/628))
|
||||
- Everforest Dark ([#760](https://github.com/helix-editor/helix/pull/760))
|
||||
- Nord ([#799](https://github.com/helix-editor/helix/pull/799))
|
||||
- Base16 Default Dark ([#833](https://github.com/helix-editor/helix/pull/833))
|
||||
- Rose Pine ([#897](https://github.com/helix-editor/helix/pull/897))
|
||||
|
||||
Fixes:
|
||||
|
||||
- Fix crash on empty rust file ([#592](https://github.com/helix-editor/helix/pull/592))
|
||||
- Exit select mode after toggle comment ([#598](https://github.com/helix-editor/helix/pull/598))
|
||||
- Pin popups with no positioning to the initial position (12ea3888)
|
||||
- xsel copy should not freeze the editor (6dd7dc4)
|
||||
- `*` now only sets the search register and doesn't jump to the next occurrence (3426285)
|
||||
- Goto line start/end commands extend when in select mode ([#739](https://github.com/helix-editor/helix/pull/739))
|
||||
- Fix documentation popups sometimes not getting fully highlighted (066367c)
|
||||
- Refactor apply_workspace_edit to remove assert (b02d872)
|
||||
- Wrap around the top of the picker menu when scrolling (c7d6e44)
|
||||
- Don't allow closing the last split if there's unsaved changes (3ff5b00)
|
||||
- Indentation used different default on hx vs hx new_file.txt (c913bad)
|
||||
|
||||
# 0.4.1 (2021-08-14)
|
||||
|
||||
A minor release that includes:
|
||||
@ -7,6 +88,8 @@ # 0.4.1 (2021-08-14)
|
||||
|
||||
# 0.4.0 (2021-08-13)
|
||||
|
||||
A big shout out to all the contributors! We had 28 contributors in this release.
|
||||
|
||||
Two months have passed, so this is another big release. A big thank you to all
|
||||
the contributors and package maintainers!
|
||||
|
||||
@ -44,6 +127,8 @@ # 0.4.0 (2021-08-13)
|
||||
|
||||
# 0.3.0 (2021-06-27)
|
||||
|
||||
A big shout out to all the contributors! We had 24 contributors in this release.
|
||||
|
||||
Another big release.
|
||||
|
||||
Highlights:
|
||||
@ -90,6 +175,8 @@ # 0.2.1
|
||||
|
||||
# 0.2.0
|
||||
|
||||
A big shout out to all the contributors! We had 18 contributors in this release.
|
||||
|
||||
Enough has changed to bump the version. We're skipping 0.1.x because
|
||||
previously the CLI would always report version as 0.1.0, and we'd like
|
||||
to distinguish it in bug reports..
|
||||
|
91
Cargo.lock
generated
91
Cargo.lock
generated
@ -78,9 +78,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
|
||||
|
||||
[[package]]
|
||||
name = "chardetng"
|
||||
version = "0.1.14"
|
||||
version = "0.1.15"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "36a5a2ca47925d19fb6835f53b3e70dec0d25659211c8ee5cc784f1fd6838f9c"
|
||||
checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"encoding_rs",
|
||||
@ -101,15 +101,24 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "4.2.1"
|
||||
version = "4.2.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8"
|
||||
checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
"str-buf",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "content_inspector"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
|
||||
dependencies = [
|
||||
"memchr",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "crossbeam-utils"
|
||||
version = "0.8.5"
|
||||
@ -122,9 +131,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossterm"
|
||||
version = "0.21.0"
|
||||
version = "0.22.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788"
|
||||
checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"crossterm_winapi",
|
||||
@ -139,9 +148,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "crossterm_winapi"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507"
|
||||
checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
|
||||
dependencies = [
|
||||
"winapi",
|
||||
]
|
||||
@ -175,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
|
||||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.28"
|
||||
version = "0.8.29"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065"
|
||||
checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
@ -358,11 +367,12 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-core"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"etcetera",
|
||||
"helix-syntax",
|
||||
"log",
|
||||
"once_cell",
|
||||
"quickcheck",
|
||||
"regex",
|
||||
@ -395,7 +405,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-lsp"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"futures-executor",
|
||||
@ -413,7 +423,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-syntax"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"cc",
|
||||
@ -424,10 +434,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-term"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"content_inspector",
|
||||
"crossterm",
|
||||
"fern",
|
||||
"futures-util",
|
||||
@ -455,7 +466,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-tui"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"cassowary",
|
||||
@ -468,7 +479,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "helix-view"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags",
|
||||
@ -532,9 +543,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "instant"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd"
|
||||
checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
@ -566,9 +577,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
|
||||
|
||||
[[package]]
|
||||
name = "libc"
|
||||
version = "0.2.103"
|
||||
version = "0.2.104"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6"
|
||||
checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
|
||||
|
||||
[[package]]
|
||||
name = "libloading"
|
||||
@ -600,9 +611,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "lsp-types"
|
||||
version = "0.90.1"
|
||||
version = "0.91.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f3734ab1d7d157fc0c45110e06b587c31cd82bea2ccfd6b563cbff0aaeeb1d3"
|
||||
checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
|
||||
dependencies = [
|
||||
"bitflags",
|
||||
"serde",
|
||||
@ -640,9 +651,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.7.13"
|
||||
version = "0.7.14"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16"
|
||||
checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
@ -755,9 +766,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
|
||||
|
||||
[[package]]
|
||||
name = "proc-macro2"
|
||||
version = "1.0.29"
|
||||
version = "1.0.30"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d"
|
||||
checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
|
||||
dependencies = [
|
||||
"unicode-xid",
|
||||
]
|
||||
@ -784,9 +795,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "quote"
|
||||
version = "1.0.9"
|
||||
version = "1.0.10"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7"
|
||||
checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
]
|
||||
@ -973,9 +984,9 @@ checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
|
||||
|
||||
[[package]]
|
||||
name = "slab"
|
||||
version = "0.4.4"
|
||||
version = "0.4.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590"
|
||||
checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
|
||||
|
||||
[[package]]
|
||||
name = "slotmap"
|
||||
@ -1000,9 +1011,9 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
|
||||
|
||||
[[package]]
|
||||
name = "syn"
|
||||
version = "1.0.78"
|
||||
version = "1.0.80"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0"
|
||||
checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1075,9 +1086,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
|
||||
|
||||
[[package]]
|
||||
name = "tokio"
|
||||
version = "1.12.0"
|
||||
version = "1.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc"
|
||||
checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
|
||||
dependencies = [
|
||||
"autocfg",
|
||||
"bytes",
|
||||
@ -1095,9 +1106,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-macros"
|
||||
version = "1.4.1"
|
||||
version = "1.5.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb"
|
||||
checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@ -1106,9 +1117,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tokio-stream"
|
||||
version = "0.1.7"
|
||||
version = "0.1.8"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f"
|
||||
checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
|
||||
dependencies = [
|
||||
"futures-core",
|
||||
"pin-project-lite",
|
||||
@ -1145,9 +1156,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "unicode-bidi"
|
||||
version = "0.3.6"
|
||||
version = "0.3.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085"
|
||||
checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
|
||||
|
||||
[[package]]
|
||||
name = "unicode-general-category"
|
||||
|
@ -27,11 +27,11 @@ # Features
|
||||
It's a terminal-based editor first, but I'd like to explore a custom renderer
|
||||
(similar to emacs) in wgpu or skulpin.
|
||||
|
||||
# Installation
|
||||
|
||||
Note: Only certain languages have indentation definitions at the moment. Check
|
||||
`runtime/queries/<lang>/` for `indents.toml`.
|
||||
|
||||
# Installation
|
||||
|
||||
We provide packaging for various distributions, but here's a quick method to
|
||||
build from source.
|
||||
|
||||
@ -71,7 +71,7 @@ # Contributing
|
||||
|
||||
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
|
||||
- Help with packaging on various distributions needed!
|
||||
- To use print debugging to the `~/.cache/helix/helix.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!")`)
|
||||
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
|
||||
- If your preferred language is missing, integrating a tree-sitter grammar for
|
||||
|
6
TODO.md
6
TODO.md
@ -6,10 +6,6 @@
|
||||
- clojure
|
||||
- erlang
|
||||
|
||||
as you type completion!
|
||||
- [ ] use signature_help_provider and completion_provider trigger characters in
|
||||
a hook to trigger signature help text / autocompletion
|
||||
- [ ] document.on_type provider triggers
|
||||
- [ ] completion isIncomplete support
|
||||
|
||||
1
|
||||
@ -18,8 +14,6 @@
|
||||
|
||||
- [ ] = for auto indent line/selection
|
||||
- [ ] :x for closing buffers
|
||||
- [ ] repeat selection
|
||||
|
||||
- [ ] lsp: signature help
|
||||
|
||||
2
|
||||
|
@ -8,3 +8,5 @@ # Summary
|
||||
- [Keymap](./keymap.md)
|
||||
- [Key Remapping](./remapping.md)
|
||||
- [Hooks](./hooks.md)
|
||||
- [Guides](./guides/README.md)
|
||||
- [Adding Textobject Queries](./guides/textobject.md)
|
||||
|
@ -21,6 +21,8 @@ ## Editor
|
||||
| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
|
||||
| `auto-completion` | Enable automatic pop up of auto-completion. | `true` |
|
||||
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
|
||||
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
|
||||
| `auto-info` | Whether to display infoboxes | `true` |
|
||||
|
||||
## LSP
|
||||
|
||||
|
4
book/src/guides/README.md
Normal file
4
book/src/guides/README.md
Normal file
@ -0,0 +1,4 @@
|
||||
# Guides
|
||||
|
||||
This section contains guides for adding new language server configurations,
|
||||
tree-sitter grammers, textobject queries, etc.
|
30
book/src/guides/textobject.md
Normal file
30
book/src/guides/textobject.md
Normal file
@ -0,0 +1,30 @@
|
||||
# Adding Textobject Queries
|
||||
|
||||
Textobjects that are language specific ([like functions, classes, etc][textobjects])
|
||||
require an accompanying tree-sitter grammar and a `textobjects.scm` query file
|
||||
to work properly. Tree-sitter allows us to query the source code syntax tree
|
||||
and capture specific parts of it. The queries are written in a lisp dialect.
|
||||
More information on how to write queries can be found in the [official tree-sitter
|
||||
documentation](tree-sitter-queries).
|
||||
|
||||
Query files should be placed in `runtime/queries/{language}/textobjects.scm`
|
||||
when contributing. Note that to test the query files locally you should put
|
||||
them under your local runtime directory (`~/.config/helix/runtime` on Linux
|
||||
for example).
|
||||
|
||||
The following [captures][tree-sitter-captures] are recognized:
|
||||
|
||||
| Capture Name |
|
||||
| --- |
|
||||
| `function.inside` |
|
||||
| `function.around` |
|
||||
| `class.inside` |
|
||||
| `class.around` |
|
||||
| `parameter.inside` |
|
||||
|
||||
[Example query files][textobject-examples] can be found in the helix GitHub repository.
|
||||
|
||||
[textobjects]: ../usage.md#textobjects
|
||||
[tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax
|
||||
[tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes
|
||||
[textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=
|
@ -8,10 +8,10 @@ ### Movement
|
||||
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `h`, `Left` | Move left | `move_char_left` |
|
||||
| `j`, `Down` | Move down | `move_char_right` |
|
||||
| `k`, `Up` | Move up | `move_line_up` |
|
||||
| `l`, `Right` | Move right | `move_line_down` |
|
||||
| `h`/`Left` | Move left | `move_char_left` |
|
||||
| `j`/`Down` | Move down | `move_line_down` |
|
||||
| `k`/`Up` | Move up | `move_line_up` |
|
||||
| `l`/`Right` | Move right | `move_char_right` |
|
||||
| `w` | Move next word start | `move_next_word_start` |
|
||||
| `b` | Move previous word start | `move_prev_word_start` |
|
||||
| `e` | Move next word end | `move_next_word_end` |
|
||||
@ -22,6 +22,7 @@ ### Movement
|
||||
| `f` | Find next char | `find_next_char` |
|
||||
| `T` | Find 'till previous char | `till_prev_char` |
|
||||
| `F` | Find previous char | `find_prev_char` |
|
||||
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
|
||||
| `Home` | Move to the start of the line | `goto_line_start` |
|
||||
| `End` | Move to the end of the line | `goto_line_end` |
|
||||
| `PageUp` | Move page up | `page_up` |
|
||||
@ -54,6 +55,7 @@ ### Changes
|
||||
| `A` | Insert at the end of the line | `append_to_line` |
|
||||
| `o` | Open new line below selection | `open_below` |
|
||||
| `O` | Open new line above selection | `open_above` |
|
||||
| `.` | Repeat last change | N/A |
|
||||
| `u` | Undo change | `undo` |
|
||||
| `U` | Redo change | `redo` |
|
||||
| `y` | Yank selection | `yank` |
|
||||
@ -86,8 +88,9 @@ ### Selection manipulation
|
||||
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
|
||||
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
|
||||
| `,` | Keep only the primary selection | `keep_primary_selection` |
|
||||
| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
|
||||
| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
|
||||
| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
|
||||
| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` |
|
||||
| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` |
|
||||
| `(` | Rotate main selection backward | `rotate_selections_backward` |
|
||||
| `)` | Rotate main selection forward | `rotate_selections_forward` |
|
||||
| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
|
||||
@ -103,13 +106,13 @@ ### Selection manipulation
|
||||
|
||||
### Search
|
||||
|
||||
> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
|
||||
|
||||
| Key | Description | Command |
|
||||
| ----- | ----------- | ------- |
|
||||
| `/` | Search for regex pattern | `search` |
|
||||
| `?` | Search for previous pattern | `rsearch` |
|
||||
| `n` | Select next search match | `search_next` |
|
||||
| `N` | Add next search match to selection | `extend_search_next` |
|
||||
| `N` | Select previous search match | `search_prev` |
|
||||
| `*` | Use current selection as the search pattern | `search_selection` |
|
||||
|
||||
### Minor modes
|
||||
@ -158,6 +161,8 @@ #### Goto mode
|
||||
| `r` | Go to references | `goto_reference` |
|
||||
| `i` | Go to implementation | `goto_implementation` |
|
||||
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
|
||||
| `n` | Go to next buffer | `goto_next_buffer` |
|
||||
| `p` | Go to previous buffer | `goto_previous_buffer` |
|
||||
|
||||
#### Match mode
|
||||
|
||||
@ -184,7 +189,11 @@ #### Window mode
|
||||
| ----- | ------------- | ------- |
|
||||
| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
|
||||
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
|
||||
| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
|
||||
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
|
||||
| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
|
||||
| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
|
||||
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
|
||||
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
|
||||
| `q`, `Ctrl-q` | Close current window | `wclose` |
|
||||
|
||||
#### Space mode
|
||||
@ -244,10 +253,32 @@ # Picker
|
||||
|
||||
| Key | Description |
|
||||
| ----- | ------------- |
|
||||
| `Up`, `Ctrl-p` | Previous entry |
|
||||
| `Down`, `Ctrl-n` | Next entry |
|
||||
| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
|
||||
| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
|
||||
| `Ctrl-space` | Filter options |
|
||||
| `Enter` | Open selected |
|
||||
| `Ctrl-h` | Open horizontally |
|
||||
| `Ctrl-s` | Open horizontally |
|
||||
| `Ctrl-v` | Open vertically |
|
||||
| `Escape`, `Ctrl-c` | Close picker |
|
||||
|
||||
# Prompt
|
||||
Keys to use within prompt, Remapping currently not supported.
|
||||
| Key | Description |
|
||||
| ----- | ------------- |
|
||||
| `Escape`, `Ctrl-c` | Close prompt |
|
||||
| `Alt-b`, `Alt-Left` | Backward a word |
|
||||
| `Ctrl-b`, `Left` | Backward a char |
|
||||
| `Alt-f`, `Alt-Right` | Forward a word |
|
||||
| `Ctrl-f`, `Right` | Forward a char |
|
||||
| `Ctrl-e`, `End` | move prompt end |
|
||||
| `Ctrl-a`, `Home` | move prompt start |
|
||||
| `Ctrl-w` | delete previous word |
|
||||
| `Ctrl-k` | delete to end of line |
|
||||
| `backspace` | delete previous char |
|
||||
| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
|
||||
| `Ctrl-p`, `Up` | select previous history |
|
||||
| `Ctrl-n`, `Down` | select next history |
|
||||
| `Tab` | slect next completion item |
|
||||
| `BackTab` | slect previous completion item |
|
||||
| `Enter` | Open selected |
|
||||
|
||||
|
@ -2,7 +2,7 @@ # Key Remapping
|
||||
|
||||
One-way key remapping is temporarily supported via a simple TOML configuration
|
||||
file. (More powerful solutions such as rebinding via commands will be
|
||||
available in the feature).
|
||||
available in the future).
|
||||
|
||||
To remap keys, write a `config.toml` file in your `helix` configuration
|
||||
directory (default `~/.config/helix` in Linux systems) with a structure like
|
||||
|
@ -103,8 +103,6 @@ #### Syntax highlighting
|
||||
[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
|
||||
[TextMate](https://macromates.com/manual/en/language_grammars) scopes.
|
||||
|
||||
- `escape` (TODO: rename to (constant).character.escape)
|
||||
|
||||
- `type` - Types
|
||||
- `builtin` - Primitive types provided by the language (`int`, `usize`)
|
||||
|
||||
@ -112,13 +110,17 @@ #### Syntax highlighting
|
||||
- `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
|
||||
- `boolean`
|
||||
- `character`
|
||||
- `escape`
|
||||
- `numeric` (numbers)
|
||||
- `integer`
|
||||
- `float`
|
||||
|
||||
- `number` (TODO: rename to constant.number/.numeric.{integer, float, complex})
|
||||
- `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)?
|
||||
- `regexp` - Regular expressions
|
||||
- `special`
|
||||
- `path`
|
||||
- `url`
|
||||
- `symbol` - Erlang/Elixir atoms, Ruby symbols, Clojure keywords
|
||||
|
||||
- `comment` - Code comments
|
||||
- `line` - Single line comments (`//`)
|
||||
@ -128,7 +130,8 @@ #### Syntax highlighting
|
||||
- `variable` - Variables
|
||||
- `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
|
||||
- `parameter` - Function parameters
|
||||
- `property`
|
||||
- `other`
|
||||
- `member` - Fields of composite data types (e.g. structs, unions)
|
||||
- `function` (TODO: ?)
|
||||
|
||||
- `label`
|
||||
|
@ -2,6 +2,8 @@ # Usage
|
||||
|
||||
(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
|
||||
|
||||
See [tutor.txt](https://github.com/helix-editor/helix/blob/master/runtime/tutor.txt) (accessible via `hx --tutor` or `:tutor`) for a vimtutor-like introduction.
|
||||
|
||||
## Registers
|
||||
|
||||
Vim-like registers can be used to yank and store text to be pasted later. Usage is similar, with `"` being used to select a register:
|
||||
@ -49,9 +51,10 @@ ## Surround
|
||||
|
||||
## Textobjects
|
||||
|
||||
Currently supported: `word`, `surround`.
|
||||
Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
|
||||
|
||||
![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
|
||||
![textobject-treesitter-demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
|
||||
|
||||
- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
|
||||
- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
|
||||
@ -60,5 +63,11 @@ ## Textobjects
|
||||
| --- | --- |
|
||||
| `w` | Word |
|
||||
| `(`, `[`, `'`, etc | Specified surround pairs |
|
||||
| `f` | Function |
|
||||
| `c` | Class |
|
||||
| `p` | Parameter |
|
||||
|
||||
Textobjects based on treesitter, like `function`, `class`, etc are planned.
|
||||
Note: `f`, `c`, etc need a tree-sitter grammar active for the current
|
||||
document and a special tree-sitter query file to work properly. [Only
|
||||
some grammars](https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l=)
|
||||
currently have the query file implemented. Contributions are welcome !
|
||||
|
68
flake.lock
68
flake.lock
@ -2,11 +2,11 @@
|
||||
"nodes": {
|
||||
"devshell": {
|
||||
"locked": {
|
||||
"lastModified": 1630239564,
|
||||
"narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=",
|
||||
"lastModified": 1632436039,
|
||||
"narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
|
||||
"owner": "numtide",
|
||||
"repo": "devshell",
|
||||
"rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73",
|
||||
"rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -15,6 +15,21 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flake-utils": {
|
||||
"locked": {
|
||||
"lastModified": 1623875721,
|
||||
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "numtide",
|
||||
"repo": "flake-utils",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"flakeCompat": {
|
||||
"flake": false,
|
||||
"locked": {
|
||||
@ -37,14 +52,16 @@
|
||||
"nixpkgs": [
|
||||
"nixpkgs"
|
||||
],
|
||||
"rustOverlay": "rustOverlay"
|
||||
"rustOverlay": [
|
||||
"rust-overlay"
|
||||
]
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1631254163,
|
||||
"narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=",
|
||||
"lastModified": 1634796585,
|
||||
"narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
|
||||
"owner": "yusdacra",
|
||||
"repo": "nix-cargo-integration",
|
||||
"rev": "432d8504a32232e8d74710024d5bf5cc31767651",
|
||||
"rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -55,11 +72,11 @@
|
||||
},
|
||||
"nixpkgs": {
|
||||
"locked": {
|
||||
"lastModified": 1631206977,
|
||||
"narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
|
||||
"lastModified": 1634782485,
|
||||
"narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
|
||||
"owner": "nixos",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
|
||||
"rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
@ -69,21 +86,40 @@
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"nixpkgs_2": {
|
||||
"locked": {
|
||||
"lastModified": 1628186154,
|
||||
"narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"rev": "06552b72346632b6943c8032e57e702ea12413bf",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
"owner": "NixOS",
|
||||
"repo": "nixpkgs",
|
||||
"type": "github"
|
||||
}
|
||||
},
|
||||
"root": {
|
||||
"inputs": {
|
||||
"flakeCompat": "flakeCompat",
|
||||
"nixCargoIntegration": "nixCargoIntegration",
|
||||
"nixpkgs": "nixpkgs"
|
||||
"nixpkgs": "nixpkgs",
|
||||
"rust-overlay": "rust-overlay"
|
||||
}
|
||||
},
|
||||
"rustOverlay": {
|
||||
"flake": false,
|
||||
"rust-overlay": {
|
||||
"inputs": {
|
||||
"flake-utils": "flake-utils",
|
||||
"nixpkgs": "nixpkgs_2"
|
||||
},
|
||||
"locked": {
|
||||
"lastModified": 1631240108,
|
||||
"narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=",
|
||||
"lastModified": 1634869268,
|
||||
"narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
|
||||
"owner": "oxalica",
|
||||
"repo": "rust-overlay",
|
||||
"rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289",
|
||||
"rev": "c02c2d86354327317546501af001886fbb53d374",
|
||||
"type": "github"
|
||||
},
|
||||
"original": {
|
||||
|
@ -3,9 +3,11 @@
|
||||
|
||||
inputs = {
|
||||
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
|
||||
rust-overlay.url = "github:oxalica/rust-overlay";
|
||||
nixCargoIntegration = {
|
||||
url = "github:yusdacra/nix-cargo-integration";
|
||||
inputs.nixpkgs.follows = "nixpkgs";
|
||||
inputs.rustOverlay.follows = "rust-overlay";
|
||||
};
|
||||
flakeCompat = {
|
||||
url = "github:edolstra/flake-compat";
|
||||
@ -61,7 +63,7 @@
|
||||
'';
|
||||
};
|
||||
shell = common: prev: {
|
||||
packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb cargo-tarpaulin ]);
|
||||
packages = prev.packages ++ (with common.pkgs; [ lld_12 lldb cargo-tarpaulin ]);
|
||||
env = prev.env ++ [
|
||||
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
|
||||
{ name = "RUST_BACKTRACE"; value = "1"; }
|
||||
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "helix-core"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "Helix editor core editing primitives"
|
||||
categories = ["editor"]
|
||||
@ -13,7 +13,7 @@ include = ["src/**/*", "README.md"]
|
||||
[features]
|
||||
|
||||
[dependencies]
|
||||
helix-syntax = { version = "0.4", path = "../helix-syntax" }
|
||||
helix-syntax = { version = "0.5", path = "../helix-syntax" }
|
||||
|
||||
ropey = "1.3"
|
||||
smallvec = "1.7"
|
||||
@ -27,6 +27,7 @@ once_cell = "1.8"
|
||||
arc-swap = "1"
|
||||
regex = "1"
|
||||
|
||||
log = "0.4"
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
toml = "0.5"
|
||||
|
@ -1,3 +1,6 @@
|
||||
//! When typing the opening character of one of the possible pairs defined below,
|
||||
//! this module provides the functionality to insert the paired closing character.
|
||||
|
||||
use crate::{Range, Rope, Selection, Tendril, Transaction};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
|
@ -1,3 +1,5 @@
|
||||
//! Utility functions to categorize a `char`.
|
||||
|
||||
use crate::LineEnding;
|
||||
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
|
@ -1,3 +1,6 @@
|
||||
//! This module contains the functionality toggle comments on lines over the selection
|
||||
//! using the comment character defined in the user's `languages.toml`
|
||||
|
||||
use crate::{
|
||||
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
|
||||
};
|
||||
@ -60,7 +63,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
|
||||
let token = token.unwrap_or("//");
|
||||
let comment = Tendril::from(format!("{} ", token));
|
||||
|
||||
let mut lines: Vec<usize> = Vec::new();
|
||||
let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
|
||||
|
||||
let mut min_next_line = 0;
|
||||
for selection in selection {
|
||||
|
@ -1,3 +1,6 @@
|
||||
//! LSP diagnostic utility types.
|
||||
|
||||
/// Describes the severity level of a [`Diagnostic`].
|
||||
#[derive(Debug, Eq, PartialEq)]
|
||||
pub enum Severity {
|
||||
Error,
|
||||
@ -6,12 +9,14 @@ pub enum Severity {
|
||||
Hint,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
/// A range of `char`s within the text.
|
||||
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
|
||||
pub struct Range {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
}
|
||||
|
||||
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
|
||||
#[derive(Debug)]
|
||||
pub struct Diagnostic {
|
||||
pub range: Range,
|
||||
|
@ -1,4 +1,6 @@
|
||||
// Based on https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs
|
||||
//! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents.
|
||||
//!
|
||||
//! Based on <https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs>
|
||||
use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
|
||||
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
|
||||
use unicode_width::UnicodeWidthStr;
|
||||
|
@ -4,48 +4,50 @@
|
||||
use std::num::NonZeroUsize;
|
||||
use std::time::{Duration, Instant};
|
||||
|
||||
// Stores the history of changes to a buffer.
|
||||
//
|
||||
// Currently the history is represented as a vector of revisions. The vector
|
||||
// always has at least one element: the empty root revision. Each revision
|
||||
// with the exception of the root has a parent revision, a [Transaction]
|
||||
// that can be applied to its parent to transition from the parent to itself,
|
||||
// and an inversion of that transaction to transition from the parent to its
|
||||
// latest child.
|
||||
//
|
||||
// When using `u` to undo a change, an inverse of the stored transaction will
|
||||
// be applied which will transition the buffer to the parent state.
|
||||
//
|
||||
// Each revision with the exception of the last in the vector also has a
|
||||
// last child revision. When using `U` to redo a change, the last child transaction
|
||||
// will be applied to the current state of the buffer.
|
||||
//
|
||||
// The current revision is the one currently displayed in the buffer.
|
||||
//
|
||||
// Commiting a new revision to the history will update the last child of the
|
||||
// current revision, and push a new revision to the end of the vector.
|
||||
//
|
||||
// Revisions are commited with a timestamp. :earlier and :later can be used
|
||||
// to jump to the closest revision to a moment in time relative to the timestamp
|
||||
// of the current revision plus (:later) or minus (:earlier) the duration
|
||||
// given to the command. If a single integer is given, the editor will instead
|
||||
// jump the given number of revisions in the vector.
|
||||
//
|
||||
// Limitations:
|
||||
// * Changes in selections currently don't commit history changes. The selection
|
||||
// will only be updated to the state after a commited buffer change.
|
||||
// * The vector of history revisions is currently unbounded. This might
|
||||
// cause the memory consumption to grow significantly large during long
|
||||
// editing sessions.
|
||||
// * Because delete transactions currently don't store the text that they
|
||||
// delete, we also store an inversion of the transaction.
|
||||
/// Stores the history of changes to a buffer.
|
||||
///
|
||||
/// Currently the history is represented as a vector of revisions. The vector
|
||||
/// always has at least one element: the empty root revision. Each revision
|
||||
/// with the exception of the root has a parent revision, a [Transaction]
|
||||
/// that can be applied to its parent to transition from the parent to itself,
|
||||
/// and an inversion of that transaction to transition from the parent to its
|
||||
/// latest child.
|
||||
///
|
||||
/// When using `u` to undo a change, an inverse of the stored transaction will
|
||||
/// be applied which will transition the buffer to the parent state.
|
||||
///
|
||||
/// Each revision with the exception of the last in the vector also has a
|
||||
/// last child revision. When using `U` to redo a change, the last child transaction
|
||||
/// will be applied to the current state of the buffer.
|
||||
///
|
||||
/// The current revision is the one currently displayed in the buffer.
|
||||
///
|
||||
/// Commiting a new revision to the history will update the last child of the
|
||||
/// current revision, and push a new revision to the end of the vector.
|
||||
///
|
||||
/// Revisions are commited with a timestamp. :earlier and :later can be used
|
||||
/// to jump to the closest revision to a moment in time relative to the timestamp
|
||||
/// of the current revision plus (:later) or minus (:earlier) the duration
|
||||
/// given to the command. If a single integer is given, the editor will instead
|
||||
/// jump the given number of revisions in the vector.
|
||||
///
|
||||
/// Limitations:
|
||||
/// * Changes in selections currently don't commit history changes. The selection
|
||||
/// will only be updated to the state after a commited buffer change.
|
||||
/// * The vector of history revisions is currently unbounded. This might
|
||||
/// cause the memory consumption to grow significantly large during long
|
||||
/// editing sessions.
|
||||
/// * Because delete transactions currently don't store the text that they
|
||||
/// delete, we also store an inversion of the transaction.
|
||||
///
|
||||
/// Using time to navigate the history: <https://github.com/helix-editor/helix/pull/194>
|
||||
#[derive(Debug)]
|
||||
pub struct History {
|
||||
revisions: Vec<Revision>,
|
||||
current: usize,
|
||||
}
|
||||
|
||||
// A single point in history. See [History] for more information.
|
||||
/// A single point in history. See [History] for more information.
|
||||
#[derive(Debug)]
|
||||
struct Revision {
|
||||
parent: usize,
|
||||
@ -111,6 +113,7 @@ pub const fn at_root(&self) -> bool {
|
||||
self.current == 0
|
||||
}
|
||||
|
||||
/// Undo the last edit.
|
||||
pub fn undo(&mut self) -> Option<&Transaction> {
|
||||
if self.at_root() {
|
||||
return None;
|
||||
@ -121,6 +124,7 @@ pub fn undo(&mut self) -> Option<&Transaction> {
|
||||
Some(¤t_revision.inversion)
|
||||
}
|
||||
|
||||
/// Redo the last edit.
|
||||
pub fn redo(&mut self) -> Option<&Transaction> {
|
||||
let current_revision = &self.revisions[self.current];
|
||||
let last_child = current_revision.last_child?;
|
||||
@ -147,8 +151,8 @@ fn lowest_common_ancestor(&self, mut a: usize, mut b: usize) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
|
||||
// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
|
||||
/// List of nodes on the way from `n` to 'a`. Doesn`t include `a`.
|
||||
/// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
|
||||
fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
|
||||
let mut path = Vec::new();
|
||||
while n != a {
|
||||
@ -158,6 +162,7 @@ fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
|
||||
path
|
||||
}
|
||||
|
||||
/// Create a [`Transaction`] that will jump to a specific revision in the history.
|
||||
fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
|
||||
let lca = self.lowest_common_ancestor(self.current, to);
|
||||
let up = self.path_up(self.current, lca);
|
||||
@ -171,10 +176,12 @@ fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
|
||||
up_txns.chain(down_txns).collect()
|
||||
}
|
||||
|
||||
/// Creates a [`Transaction`] that will undo `delta` revisions.
|
||||
fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
|
||||
self.jump_to(self.current.saturating_sub(delta))
|
||||
}
|
||||
|
||||
/// Creates a [`Transaction`] that will redo `delta` revisions.
|
||||
fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
|
||||
self.jump_to(
|
||||
self.current
|
||||
@ -183,7 +190,7 @@ fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
|
||||
)
|
||||
}
|
||||
|
||||
// Helper for a binary search case below.
|
||||
/// Helper for a binary search case below.
|
||||
fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
|
||||
let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
|
||||
let dur_i = self.revisions[i].timestamp.duration_since(instant);
|
||||
@ -194,6 +201,8 @@ fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Transaction`] that will match a revision created at around
|
||||
/// `instant`.
|
||||
fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
|
||||
let search_result = self
|
||||
.revisions
|
||||
@ -209,6 +218,8 @@ fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
|
||||
self.jump_to(revision)
|
||||
}
|
||||
|
||||
/// Creates a [`Transaction`] that will match a revision created `duration` ago
|
||||
/// from the timestamp of current revision.
|
||||
fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
|
||||
match self.revisions[self.current].timestamp.checked_sub(duration) {
|
||||
Some(instant) => self.jump_instant(instant),
|
||||
@ -216,6 +227,8 @@ fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a [`Transaction`] that will match a revision created `duration` in
|
||||
/// the future from the timestamp of the current revision.
|
||||
fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
|
||||
match self.revisions[self.current].timestamp.checked_add(duration) {
|
||||
Some(instant) => self.jump_instant(instant),
|
||||
@ -223,6 +236,7 @@ fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates an undo [`Transaction`].
|
||||
pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
|
||||
use UndoKind::*;
|
||||
match uk {
|
||||
@ -231,6 +245,7 @@ pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Creates a redo [`Transaction`].
|
||||
pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
|
||||
use UndoKind::*;
|
||||
match uk {
|
||||
@ -240,13 +255,14 @@ pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
|
||||
}
|
||||
}
|
||||
|
||||
/// Whether to undo by a number of edits or a duration of time.
|
||||
#[derive(Debug, PartialEq)]
|
||||
pub enum UndoKind {
|
||||
Steps(usize),
|
||||
TimePeriod(std::time::Duration),
|
||||
}
|
||||
|
||||
// A subset of sytemd.time time span syntax units.
|
||||
/// A subset of sytemd.time time span syntax units.
|
||||
const TIME_UNITS: &[(&[&str], &str, u64)] = &[
|
||||
(&["seconds", "second", "sec", "s"], "seconds", 1),
|
||||
(&["minutes", "minute", "min", "m"], "minutes", 60),
|
||||
@ -254,11 +270,20 @@ pub enum UndoKind {
|
||||
(&["days", "day", "d"], "days", 24 * 60 * 60),
|
||||
];
|
||||
|
||||
/// Checks if the duration input can be turned into a valid duration. It must be a
|
||||
/// positive integer and denote the [unit of time.](`TIME_UNITS`)
|
||||
/// Examples of valid durations:
|
||||
/// * `5 sec`
|
||||
/// * `5 min`
|
||||
/// * `5 hr`
|
||||
/// * `5 days`
|
||||
static DURATION_VALIDATION_REGEX: Lazy<Regex> =
|
||||
Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap());
|
||||
|
||||
/// Captures both the number and unit as separate capture groups.
|
||||
static NUMBER_UNIT_REGEX: Lazy<Regex> = Lazy::new(|| Regex::new(r"(\d+)\s*([a-z]+)").unwrap());
|
||||
|
||||
/// Parse a string (e.g. "5 sec") and try to convert it into a [`Duration`].
|
||||
fn parse_human_duration(s: &str) -> Result<Duration, String> {
|
||||
if !DURATION_VALIDATION_REGEX.is_match(s) {
|
||||
return Err("duration should be composed \
|
||||
|
@ -464,6 +464,7 @@ pub fn change<I>(document: &Document, changes: I) -> Self
|
||||
unit: String::from(" "),
|
||||
}),
|
||||
indent_query: OnceCell::new(),
|
||||
textobject_query: OnceCell::new(),
|
||||
debugger: None,
|
||||
}],
|
||||
});
|
||||
|
@ -35,6 +35,7 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
|
||||
line.chars().position(|ch| !ch.is_whitespace())
|
||||
}
|
||||
|
||||
/// Find `.git` root.
|
||||
pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
|
||||
let current_dir = std::env::current_dir().expect("unable to determine current directory");
|
||||
|
||||
@ -193,7 +194,7 @@ fn language_tomls() {
|
||||
pub use {regex, tree_sitter};
|
||||
|
||||
pub use graphemes::RopeGraphemes;
|
||||
pub use position::{coords_at_pos, pos_at_coords, Position};
|
||||
pub use position::{coords_at_pos, pos_at_coords, visual_coords_at_pos, Position};
|
||||
pub use selection::{Range, Selection};
|
||||
pub use smallvec::{smallvec, SmallVec};
|
||||
pub use syntax::Syntax;
|
||||
|
@ -20,7 +20,7 @@ pub enum LineEnding {
|
||||
|
||||
impl LineEnding {
|
||||
#[inline]
|
||||
pub fn len_chars(&self) -> usize {
|
||||
pub const fn len_chars(&self) -> usize {
|
||||
match self {
|
||||
Self::Crlf => 2,
|
||||
_ => 1,
|
||||
@ -28,7 +28,7 @@ pub fn len_chars(&self) -> usize {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn as_str(&self) -> &'static str {
|
||||
pub const fn as_str(&self) -> &'static str {
|
||||
match self {
|
||||
Self::Crlf => "\u{000D}\u{000A}",
|
||||
Self::LF => "\u{000A}",
|
||||
@ -42,7 +42,7 @@ pub fn as_str(&self) -> &'static str {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn from_char(ch: char) -> Option<LineEnding> {
|
||||
pub const fn from_char(ch: char) -> Option<LineEnding> {
|
||||
match ch {
|
||||
'\u{000A}' => Some(LineEnding::LF),
|
||||
'\u{000B}' => Some(LineEnding::VT),
|
||||
|
@ -53,6 +53,10 @@ pub fn move_vertically(
|
||||
let pos = range.cursor(slice);
|
||||
|
||||
// Compute the current position's 2d coordinates.
|
||||
// TODO: switch this to use `visual_coords_at_pos` rather than
|
||||
// `coords_at_pos` as this will cause a jerky movement when the visual
|
||||
// position does not match, like moving from a line with tabs/CJK to
|
||||
// a line without
|
||||
let Position { row, col } = coords_at_pos(slice, pos);
|
||||
let horiz = range.horiz.unwrap_or(col as u32);
|
||||
|
||||
|
@ -13,8 +13,13 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection)
|
||||
let parent = match tree
|
||||
.root_node()
|
||||
.descendant_for_byte_range(from, to)
|
||||
.and_then(|node| node.parent())
|
||||
{
|
||||
.and_then(|node| {
|
||||
if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) {
|
||||
node.parent()
|
||||
} else {
|
||||
Some(node)
|
||||
}
|
||||
}) {
|
||||
Some(parent) => parent,
|
||||
None => return range,
|
||||
};
|
||||
|
@ -2,6 +2,7 @@
|
||||
chars::char_is_line_ending,
|
||||
graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
|
||||
line_ending::line_end_char_index,
|
||||
unicode::width::UnicodeWidthChar,
|
||||
RopeSlice,
|
||||
};
|
||||
|
||||
@ -54,11 +55,8 @@ fn from(pos: Position) -> Self {
|
||||
}
|
||||
/// Convert a character index to (line, column) coordinates.
|
||||
///
|
||||
/// TODO: this should be split into two methods: one for visual
|
||||
/// row/column, and one for "objective" row/column (possibly with
|
||||
/// the column specified in `char`s). The former would be used
|
||||
/// for cursor movement, and the latter would be used for e.g. the
|
||||
/// row:column display in the status line.
|
||||
/// column in `char` count which can be used for row:column display in
|
||||
/// status line. See [`visual_coords_at_pos`] for a visual one.
|
||||
pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
|
||||
let line = text.char_to_line(pos);
|
||||
|
||||
@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
|
||||
Position::new(line, col)
|
||||
}
|
||||
|
||||
/// Convert a character index to (line, column) coordinates visually.
|
||||
///
|
||||
/// Takes \t, double-width characters (CJK) into account as well as text
|
||||
/// not in the document in the future.
|
||||
/// See [`coords_at_pos`] for an "objective" one.
|
||||
pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position {
|
||||
let line = text.char_to_line(pos);
|
||||
|
||||
let line_start = text.line_to_char(line);
|
||||
let pos = ensure_grapheme_boundary_prev(text, pos);
|
||||
let col = text
|
||||
.slice(line_start..pos)
|
||||
.chars()
|
||||
.flat_map(|c| match c {
|
||||
'\t' => Some(tab_width),
|
||||
c => UnicodeWidthChar::width(c),
|
||||
})
|
||||
.sum();
|
||||
|
||||
Position::new(line, col)
|
||||
}
|
||||
|
||||
/// Convert (line, column) coordinates to a character index.
|
||||
///
|
||||
/// If the `line` coordinate is beyond the end of the file, the EOF
|
||||
@ -130,7 +150,6 @@ fn test_coords_at_pos() {
|
||||
assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
|
||||
|
||||
// Test with wide characters.
|
||||
// TODO: account for character width.
|
||||
let text = Rope::from("今日はいい\n");
|
||||
let slice = text.slice(..);
|
||||
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
|
||||
@ -151,7 +170,6 @@ fn test_coords_at_pos() {
|
||||
assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
|
||||
|
||||
// Test with wide-character grapheme clusters.
|
||||
// TODO: account for character width.
|
||||
let text = Rope::from("किमपि\n");
|
||||
let slice = text.slice(..);
|
||||
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
|
||||
@ -161,7 +179,6 @@ fn test_coords_at_pos() {
|
||||
assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
|
||||
|
||||
// Test with tabs.
|
||||
// Todo: account for tab stops.
|
||||
let text = Rope::from("\tHello\n");
|
||||
let slice = text.slice(..);
|
||||
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
|
||||
@ -169,6 +186,54 @@ fn test_coords_at_pos() {
|
||||
assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_visual_coords_at_pos() {
|
||||
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
|
||||
let slice = text.slice(..);
|
||||
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); // position on \n
|
||||
assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); // position on w
|
||||
assert_eq!(visual_coords_at_pos(slice, 7, 8), (1, 1).into()); // position on o
|
||||
assert_eq!(visual_coords_at_pos(slice, 10, 8), (1, 4).into()); // position on d
|
||||
|
||||
// Test with wide characters.
|
||||
let text = Rope::from("今日はいい\n");
|
||||
let slice = text.slice(..);
|
||||
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 2).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 4).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 6).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 8).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 10).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
|
||||
|
||||
// Test with grapheme clusters.
|
||||
let text = Rope::from("a̐éö̲\r\n");
|
||||
let slice = text.slice(..);
|
||||
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 1).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 2).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 7, 8), (0, 3).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 9, 8), (1, 0).into());
|
||||
|
||||
// Test with wide-character grapheme clusters.
|
||||
// TODO: account for cluster.
|
||||
let text = Rope::from("किमपि\n");
|
||||
let slice = text.slice(..);
|
||||
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 2).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 3).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
|
||||
|
||||
// Test with tabs.
|
||||
let text = Rope::from("\tHello\n");
|
||||
let slice = text.slice(..);
|
||||
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 8).into());
|
||||
assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_pos_at_coords() {
|
||||
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
|
||||
|
@ -7,7 +7,7 @@ pub struct Register {
|
||||
}
|
||||
|
||||
impl Register {
|
||||
pub fn new(name: char) -> Self {
|
||||
pub const fn new(name: char) -> Self {
|
||||
Self {
|
||||
name,
|
||||
values: Vec::new(),
|
||||
@ -18,7 +18,7 @@ pub fn new_with_values(name: char, values: Vec<String>) -> Self {
|
||||
Self { name, values }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> char {
|
||||
pub const fn name(&self) -> char {
|
||||
self.name
|
||||
}
|
||||
|
||||
|
@ -362,6 +362,11 @@ pub fn push(mut self, range: Range) -> Self {
|
||||
|
||||
/// Adds a new range to the selection and makes it the primary range.
|
||||
pub fn remove(mut self, index: usize) -> Self {
|
||||
assert!(
|
||||
self.ranges.len() > 1,
|
||||
"can't remove the last range from a selection!"
|
||||
);
|
||||
|
||||
self.ranges.remove(index);
|
||||
if index < self.primary_index || self.primary_index == self.ranges.len() {
|
||||
self.primary_index -= 1;
|
||||
@ -369,6 +374,12 @@ pub fn remove(mut self, index: usize) -> Self {
|
||||
self
|
||||
}
|
||||
|
||||
/// Replace a range in the selection with a new range.
|
||||
pub fn replace(mut self, index: usize, range: Range) -> Self {
|
||||
self.ranges[index] = range;
|
||||
self.normalize()
|
||||
}
|
||||
|
||||
/// Map selections over a set of changes. Useful for adjusting the selection position after
|
||||
/// applying changes to a document.
|
||||
pub fn map(self, changes: &ChangeSet) -> Self {
|
||||
|
@ -49,7 +49,7 @@ pub struct Configuration {
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub struct LanguageConfiguration {
|
||||
#[serde(rename = "name")]
|
||||
pub(crate) language_id: String,
|
||||
pub language_id: String,
|
||||
pub scope: String, // source.rust
|
||||
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
|
||||
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
|
||||
@ -76,6 +76,8 @@ pub struct LanguageConfiguration {
|
||||
|
||||
#[serde(skip)]
|
||||
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
|
||||
#[serde(skip)]
|
||||
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub debugger: Option<DebugAdapterConfig>,
|
||||
}
|
||||
@ -160,6 +162,32 @@ pub struct IndentQuery {
|
||||
pub outdent: HashSet<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct TextObjectQuery {
|
||||
pub query: Query,
|
||||
}
|
||||
|
||||
impl TextObjectQuery {
|
||||
/// Run the query on the given node and return sub nodes which match given
|
||||
/// capture ("function.inside", "class.around", etc).
|
||||
pub fn capture_nodes<'a>(
|
||||
&'a self,
|
||||
capture_name: &str,
|
||||
node: Node<'a>,
|
||||
slice: RopeSlice<'a>,
|
||||
cursor: &'a mut QueryCursor,
|
||||
) -> Option<impl Iterator<Item = Node<'a>>> {
|
||||
let capture_idx = self.query.capture_index_for_name(capture_name)?;
|
||||
let captures = cursor.captures(&self.query, node, RopeProvider(slice));
|
||||
|
||||
captures
|
||||
.filter_map(move |(mat, idx)| {
|
||||
(mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node)
|
||||
})
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
|
||||
let path = crate::RUNTIME_DIR
|
||||
.join("queries")
|
||||
@ -208,13 +236,14 @@ fn initialize_highlight(&self, scopes: &[String]) -> Option<Arc<HighlightConfigu
|
||||
// highlights_query += "\n(ERROR) @error";
|
||||
|
||||
let injections_query = read_query(&language, "injections.scm");
|
||||
|
||||
let locals_query = read_query(&language, "locals.scm");
|
||||
|
||||
if highlights_query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let language = get_language(&crate::RUNTIME_DIR, &self.language_id).ok()?;
|
||||
let language = get_language(&crate::RUNTIME_DIR, &self.language_id)
|
||||
.map_err(|e| log::info!("{}", e))
|
||||
.ok()?;
|
||||
let config = HighlightConfiguration::new(
|
||||
language,
|
||||
&highlights_query,
|
||||
@ -258,6 +287,18 @@ pub fn indent_query(&self) -> Option<&IndentQuery> {
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn textobject_query(&self) -> Option<&TextObjectQuery> {
|
||||
self.textobject_query
|
||||
.get_or_init(|| -> Option<TextObjectQuery> {
|
||||
let lang_name = self.language_id.to_ascii_lowercase();
|
||||
let query_text = read_query(&lang_name, "textobjects.scm");
|
||||
let lang = self.highlight_config.get()?.as_ref()?.language;
|
||||
let query = Query::new(lang, &query_text).ok()?;
|
||||
Some(TextObjectQuery { query })
|
||||
})
|
||||
.as_ref()
|
||||
}
|
||||
|
||||
pub fn scope(&self) -> &str {
|
||||
&self.scope
|
||||
}
|
||||
@ -451,7 +492,7 @@ pub fn tree(&self) -> &Tree {
|
||||
|
||||
/// Iterate over the highlighted regions for a given slice of source code.
|
||||
pub fn highlight_iter<'a>(
|
||||
&self,
|
||||
&'a self,
|
||||
source: RopeSlice<'a>,
|
||||
range: Option<std::ops::Range<usize>>,
|
||||
cancellation_flag: Option<&'a AtomicUsize>,
|
||||
@ -466,11 +507,10 @@ pub fn highlight_iter<'a>(
|
||||
let highlighter = &mut ts_parser.borrow_mut();
|
||||
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
|
||||
});
|
||||
let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
|
||||
let tree_ref = self.tree();
|
||||
let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
|
||||
let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
|
||||
let config_ref =
|
||||
unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
|
||||
let query_ref = &self.config.query;
|
||||
let config_ref = self.config.as_ref();
|
||||
|
||||
// if reusing cursors & no range this resets to whole range
|
||||
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
|
||||
@ -582,39 +622,7 @@ fn parse(
|
||||
self.tree.as_ref(),
|
||||
)
|
||||
.ok_or(Error::Cancelled)?;
|
||||
// unsafe { syntax.parser.set_cancellation_flag(None) };
|
||||
// let mut cursor = syntax.cursors.pop().unwrap_or_else(QueryCursor::new);
|
||||
|
||||
// Process combined injections. (ERB, EJS, etc https://github.com/tree-sitter/tree-sitter/pull/526)
|
||||
// if let Some(combined_injections_query) = &config.combined_injections_query {
|
||||
// let mut injections_by_pattern_index =
|
||||
// vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
|
||||
// let matches =
|
||||
// cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source));
|
||||
// for mat in matches {
|
||||
// let entry = &mut injections_by_pattern_index[mat.pattern_index];
|
||||
// let (language_name, content_node, include_children) =
|
||||
// injection_for_match(config, combined_injections_query, &mat, source);
|
||||
// if language_name.is_some() {
|
||||
// entry.0 = language_name;
|
||||
// }
|
||||
// if let Some(content_node) = content_node {
|
||||
// entry.1.push(content_node);
|
||||
// }
|
||||
// entry.2 = include_children;
|
||||
// }
|
||||
// for (lang_name, content_nodes, includes_children) in injections_by_pattern_index {
|
||||
// if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
|
||||
// if let Some(next_config) = (injection_callback)(lang_name) {
|
||||
// let ranges =
|
||||
// Self::intersect_ranges(&ranges, &content_nodes, includes_children);
|
||||
// if !ranges.is_empty() {
|
||||
// queue.push((next_config, depth + 1, ranges));
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
self.tree = Some(tree)
|
||||
}
|
||||
Ok(())
|
||||
|
@ -1,9 +1,13 @@
|
||||
use std::fmt::Display;
|
||||
|
||||
use ropey::RopeSlice;
|
||||
use tree_sitter::{Node, QueryCursor};
|
||||
|
||||
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
|
||||
use crate::graphemes::next_grapheme_boundary;
|
||||
use crate::movement::Direction;
|
||||
use crate::surround;
|
||||
use crate::syntax::LanguageConfiguration;
|
||||
use crate::Range;
|
||||
|
||||
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
|
||||
@ -51,6 +55,15 @@ pub enum TextObject {
|
||||
Inside,
|
||||
}
|
||||
|
||||
impl Display for TextObject {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str(match self {
|
||||
Self::Around => "around",
|
||||
Self::Inside => "inside",
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// count doesn't do anything yet
|
||||
pub fn textobject_word(
|
||||
slice: RopeSlice,
|
||||
@ -108,6 +121,44 @@ pub fn textobject_surround(
|
||||
.unwrap_or(range)
|
||||
}
|
||||
|
||||
/// Transform the given range to select text objects based on tree-sitter.
|
||||
/// `object_name` is a query capture base name like "function", "class", etc.
|
||||
/// `slice_tree` is the tree-sitter node corresponding to given text slice.
|
||||
pub fn textobject_treesitter(
|
||||
slice: RopeSlice,
|
||||
range: Range,
|
||||
textobject: TextObject,
|
||||
object_name: &str,
|
||||
slice_tree: Node,
|
||||
lang_config: &LanguageConfiguration,
|
||||
_count: usize,
|
||||
) -> Range {
|
||||
let get_range = move || -> Option<Range> {
|
||||
let byte_pos = slice.char_to_byte(range.cursor(slice));
|
||||
|
||||
let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
|
||||
let mut cursor = QueryCursor::new();
|
||||
let node = lang_config
|
||||
.textobject_query()?
|
||||
.capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
|
||||
.filter(|node| node.byte_range().contains(&byte_pos))
|
||||
.min_by_key(|node| node.byte_range().len())?;
|
||||
|
||||
let len = slice.len_bytes();
|
||||
let start_byte = node.start_byte();
|
||||
let end_byte = node.end_byte();
|
||||
if start_byte >= len || end_byte >= len {
|
||||
return None;
|
||||
}
|
||||
|
||||
let start_char = slice.byte_to_char(start_byte);
|
||||
let end_char = slice.byte_to_char(end_byte);
|
||||
|
||||
Some(Range::new(start_char, end_char))
|
||||
};
|
||||
get_range().unwrap_or(range)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::TextObject::*;
|
||||
|
@ -132,6 +132,9 @@ pub fn compose(self, other: Self) -> Self {
|
||||
if self.changes.is_empty() {
|
||||
return other;
|
||||
}
|
||||
if other.changes.is_empty() {
|
||||
return self;
|
||||
}
|
||||
|
||||
let len = self.changes.len();
|
||||
|
||||
@ -465,6 +468,13 @@ pub fn invert(&self, original: &Rope) -> Self {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn compose(mut self, other: Self) -> Self {
|
||||
self.changes = self.changes.compose(other.changes);
|
||||
// Other selection takes precedence
|
||||
self.selection = other.selection;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_selection(mut self, selection: Selection) -> Self {
|
||||
self.selection = Some(selection);
|
||||
self
|
||||
|
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "helix-dap"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
license = "MPL-2.0"
|
||||
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "helix-lsp"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "LSP client implementation for Helix project"
|
||||
categories = ["editor"]
|
||||
@ -12,16 +12,16 @@ homepage = "https://helix-editor.com"
|
||||
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||
|
||||
[dependencies]
|
||||
helix-core = { version = "0.4", path = "../helix-core" }
|
||||
helix-core = { version = "0.5", path = "../helix-core" }
|
||||
|
||||
anyhow = "1.0"
|
||||
futures-executor = "0.3"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures
|
||||
log = "0.4"
|
||||
lsp-types = { version = "0.90", features = ["proposed"] }
|
||||
lsp-types = { version = "0.91", features = ["proposed"] }
|
||||
serde = { version = "1.0", features = ["derive"] }
|
||||
serde_json = "1.0"
|
||||
thiserror = "1.0"
|
||||
tokio = { version = "1.12", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio-stream = "0.1.7"
|
||||
tokio = { version = "1.13", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
|
||||
tokio-stream = "0.1.8"
|
||||
|
@ -461,7 +461,7 @@ pub fn text_document_did_change(
|
||||
};
|
||||
|
||||
let changes = match sync_capabilities {
|
||||
lsp::TextDocumentSyncKind::Full => {
|
||||
lsp::TextDocumentSyncKind::FULL => {
|
||||
vec![lsp::TextDocumentContentChangeEvent {
|
||||
// range = None -> whole document
|
||||
range: None, //Some(Range)
|
||||
@ -469,10 +469,11 @@ pub fn text_document_did_change(
|
||||
text: new_text.to_string(),
|
||||
}]
|
||||
}
|
||||
lsp::TextDocumentSyncKind::Incremental => {
|
||||
lsp::TextDocumentSyncKind::INCREMENTAL => {
|
||||
Self::changeset_to_changes(old_text, new_text, changes, self.offset_encoding)
|
||||
}
|
||||
lsp::TextDocumentSyncKind::None => return None,
|
||||
lsp::TextDocumentSyncKind::NONE => return None,
|
||||
kind => unimplemented!("{:?}", kind),
|
||||
};
|
||||
|
||||
Some(self.notify::<lsp::notification::DidChangeTextDocument>(
|
||||
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "helix-syntax"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "Tree-sitter grammars support"
|
||||
categories = ["editor"]
|
||||
|
1
helix-syntax/languages/tree-sitter-cmake
Submodule
1
helix-syntax/languages/tree-sitter-cmake
Submodule
@ -0,0 +1 @@
|
||||
Subproject commit f6616f1e417ee8b62daf251aa1daa5d73781c596
|
@ -1 +1 @@
|
||||
Subproject commit c61212414a3e95b5f7507f98e83de1d638044adc
|
||||
Subproject commit e8dcc9d2b404c542fd236ea5f7208f90be8a6e89
|
@ -1 +1 @@
|
||||
Subproject commit 295e62a43b92cea909cfabe57e8818d177f4857b
|
||||
Subproject commit f5d7bda543da788bd507b05bd722627dde66c9ec
|
@ -1,9 +1,9 @@
|
||||
[package]
|
||||
name = "helix-term"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
description = "A post-modern text editor."
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
categories = ["editor", "command-line-utilities"]
|
||||
repository = "https://github.com/helix-editor/helix"
|
||||
@ -21,10 +21,10 @@ name = "hx"
|
||||
path = "src/main.rs"
|
||||
|
||||
[dependencies]
|
||||
helix-core = { version = "0.4", path = "../helix-core" }
|
||||
helix-view = { version = "0.4", path = "../helix-view" }
|
||||
helix-lsp = { version = "0.4", path = "../helix-lsp" }
|
||||
helix-dap = { version = "0.4", path = "../helix-dap" }
|
||||
helix-core = { version = "0.5", path = "../helix-core" }
|
||||
helix-view = { version = "0.5", path = "../helix-view" }
|
||||
helix-lsp = { version = "0.5", path = "../helix-lsp" }
|
||||
helix-dap = { version = "0.5", path = "../helix-dap" }
|
||||
|
||||
anyhow = "1"
|
||||
once_cell = "1.8"
|
||||
@ -32,7 +32,7 @@ once_cell = "1.8"
|
||||
tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
|
||||
num_cpus = "1"
|
||||
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] }
|
||||
crossterm = { version = "0.21", features = ["event-stream"] }
|
||||
crossterm = { version = "0.22", features = ["event-stream"] }
|
||||
signal-hook = "0.3"
|
||||
tokio-stream = "0.1"
|
||||
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
|
||||
@ -45,10 +45,10 @@ log = "0.4"
|
||||
# File picker
|
||||
fuzzy-matcher = "0.3"
|
||||
ignore = "0.4"
|
||||
# shellexpand = "2.1"
|
||||
# dirs-next = "2.0"
|
||||
# markdown doc rendering
|
||||
pulldown-cmark = { version = "0.8", default-features = false }
|
||||
# file type detection
|
||||
content_inspector = "0.2.4"
|
||||
|
||||
# config
|
||||
toml = "0.5"
|
||||
|
@ -99,12 +99,17 @@ pub fn new(args: Args, mut config: Config) -> Result<Self, Error> {
|
||||
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
|
||||
compositor.push(editor_view);
|
||||
|
||||
if !args.files.is_empty() {
|
||||
if args.load_tutor {
|
||||
let path = helix_core::runtime_dir().join("tutor.txt");
|
||||
editor.open(path, Action::VerticalSplit)?;
|
||||
// Unset path to prevent accidentally saving to the original tutor file.
|
||||
doc_mut!(editor).set_path(None)?;
|
||||
} else if !args.files.is_empty() {
|
||||
let first = &args.files[0]; // we know it's not empty
|
||||
if first.is_dir() {
|
||||
std::env::set_current_dir(&first)?;
|
||||
editor.new_file(Action::VerticalSplit);
|
||||
compositor.push(Box::new(ui::file_picker(first.clone())));
|
||||
compositor.push(Box::new(ui::file_picker(".".into())));
|
||||
} else {
|
||||
let nr_of_files = args.files.len();
|
||||
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
|
||||
@ -240,7 +245,7 @@ pub async fn handle_signals(&mut self, signal: i32) {
|
||||
}
|
||||
|
||||
pub fn handle_idle_timeout(&mut self) {
|
||||
use crate::commands::{completion, Context};
|
||||
use crate::commands::{insert::idle_completion, Context};
|
||||
use helix_view::document::Mode;
|
||||
|
||||
if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
|
||||
@ -267,7 +272,7 @@ pub fn handle_idle_timeout(&mut self) {
|
||||
callback: None,
|
||||
on_next_key_callback: None,
|
||||
};
|
||||
completion(&mut cx);
|
||||
idle_completion(&mut cx);
|
||||
self.render();
|
||||
}
|
||||
|
||||
@ -548,10 +553,11 @@ pub async fn handle_language_server_message(
|
||||
message: diagnostic.message,
|
||||
severity: diagnostic.severity.map(
|
||||
|severity| match severity {
|
||||
DiagnosticSeverity::Error => Error,
|
||||
DiagnosticSeverity::Warning => Warning,
|
||||
DiagnosticSeverity::Information => Info,
|
||||
DiagnosticSeverity::Hint => Hint,
|
||||
DiagnosticSeverity::ERROR => Error,
|
||||
DiagnosticSeverity::WARNING => Warning,
|
||||
DiagnosticSeverity::INFORMATION => Info,
|
||||
DiagnosticSeverity::HINT => Hint,
|
||||
severity => unimplemented!("{:?}", severity),
|
||||
},
|
||||
),
|
||||
// code
|
||||
@ -727,7 +733,9 @@ fn restore_term(&mut self) -> Result<(), Error> {
|
||||
let mut stdout = stdout();
|
||||
// reset cursor shape
|
||||
write!(stdout, "\x1B[2 q")?;
|
||||
execute!(stdout, DisableMouseCapture)?;
|
||||
// Ignore errors on disabling, this might trigger on windows if we call
|
||||
// disable without calling enable previously
|
||||
let _ = execute!(stdout, DisableMouseCapture);
|
||||
execute!(stdout, terminal::LeaveAlternateScreen)?;
|
||||
terminal::disable_raw_mode()?;
|
||||
Ok(())
|
||||
|
@ -5,6 +5,7 @@
|
||||
pub struct Args {
|
||||
pub display_help: bool,
|
||||
pub display_version: bool,
|
||||
pub load_tutor: bool,
|
||||
pub verbosity: u64,
|
||||
pub files: Vec<PathBuf>,
|
||||
}
|
||||
@ -22,6 +23,7 @@ pub fn parse_args() -> Result<Args> {
|
||||
"--" => break, // stop parsing at this point treat the remaining as files
|
||||
"--version" => args.display_version = true,
|
||||
"--help" => args.display_help = true,
|
||||
"--tutor" => args.load_tutor = true,
|
||||
arg if arg.starts_with("--") => {
|
||||
return Err(Error::msg(format!(
|
||||
"unexpected double dash argument: {}",
|
||||
|
File diff suppressed because it is too large
Load Diff
@ -207,7 +207,7 @@ pub trait AnyComponent {
|
||||
///
|
||||
/// ```rust
|
||||
/// use helix_term::{ui::Text, compositor::Component};
|
||||
/// let boxed: Box<Component> = Box::new(Text::new("text".to_string()));
|
||||
/// let boxed: Box<dyn Component> = Box::new(Text::new("text".to_string()));
|
||||
/// let text: Box<Text> = boxed.as_boxed_any().downcast().unwrap();
|
||||
/// ```
|
||||
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;
|
||||
|
@ -5,20 +5,20 @@
|
||||
use serde::Deserialize;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
collections::{BTreeSet, HashMap},
|
||||
ops::{Deref, DerefMut},
|
||||
};
|
||||
|
||||
#[macro_export]
|
||||
macro_rules! key {
|
||||
($key:ident) => {
|
||||
KeyEvent {
|
||||
::helix_view::input::KeyEvent {
|
||||
code: ::helix_view::keyboard::KeyCode::$key,
|
||||
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
|
||||
}
|
||||
};
|
||||
($($ch:tt)*) => {
|
||||
KeyEvent {
|
||||
::helix_view::input::KeyEvent {
|
||||
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
|
||||
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
|
||||
}
|
||||
@ -78,19 +78,30 @@ macro_rules! keymap {
|
||||
};
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct KeyTrieNode {
|
||||
/// A label for keys coming under this node, like "Goto mode"
|
||||
#[serde(skip)]
|
||||
name: String,
|
||||
#[serde(flatten)]
|
||||
map: HashMap<KeyEvent, KeyTrie>,
|
||||
#[serde(skip)]
|
||||
order: Vec<KeyEvent>,
|
||||
#[serde(skip)]
|
||||
pub is_sticky: bool,
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for KeyTrieNode {
|
||||
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let map = HashMap::<KeyEvent, KeyTrie>::deserialize(deserializer)?;
|
||||
let order = map.keys().copied().collect::<Vec<_>>(); // NOTE: map.keys() has arbitrary order
|
||||
Ok(Self {
|
||||
map,
|
||||
order,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl KeyTrieNode {
|
||||
pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
|
||||
Self {
|
||||
@ -118,7 +129,6 @@ pub fn merge(&mut self, mut other: Self) {
|
||||
}
|
||||
self.map.insert(key, trie);
|
||||
}
|
||||
|
||||
for &key in self.map.keys() {
|
||||
if !self.order.contains(&key) {
|
||||
self.order.push(key);
|
||||
@ -127,20 +137,29 @@ pub fn merge(&mut self, mut other: Self) {
|
||||
}
|
||||
|
||||
pub fn infobox(&self) -> Info {
|
||||
let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
|
||||
let mut body: Vec<(&str, BTreeSet<KeyEvent>)> = Vec::with_capacity(self.len());
|
||||
for (&key, trie) in self.iter() {
|
||||
let desc = match trie {
|
||||
KeyTrie::Leaf(cmd) => cmd.doc(),
|
||||
KeyTrie::Leaf(cmd) => {
|
||||
if cmd.name() == "no_op" {
|
||||
continue;
|
||||
}
|
||||
cmd.doc()
|
||||
}
|
||||
KeyTrie::Node(n) => n.name(),
|
||||
};
|
||||
match body.iter().position(|(d, _)| d == &desc) {
|
||||
// FIXME: multiple keys are ordered randomly (use BTreeSet)
|
||||
Some(pos) => body[pos].1.push(key),
|
||||
None => body.push((desc, vec![key])),
|
||||
Some(pos) => {
|
||||
body[pos].1.insert(key);
|
||||
}
|
||||
None => body.push((desc, BTreeSet::from([key]))),
|
||||
}
|
||||
}
|
||||
body.sort_unstable_by_key(|(_, keys)| {
|
||||
self.order.iter().position(|&k| k == keys[0]).unwrap()
|
||||
self.order
|
||||
.iter()
|
||||
.position(|&k| k == *keys.iter().next().unwrap())
|
||||
.unwrap()
|
||||
});
|
||||
let prefix = format!("{} ", self.name());
|
||||
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
|
||||
@ -151,6 +170,11 @@ pub fn infobox(&self) -> Info {
|
||||
}
|
||||
Info::new(self.name(), body)
|
||||
}
|
||||
|
||||
/// Get a reference to the key trie node's order.
|
||||
pub fn order(&self) -> &[KeyEvent] {
|
||||
self.order.as_slice()
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for KeyTrieNode {
|
||||
@ -235,6 +259,7 @@ pub enum KeymapResultKind {
|
||||
|
||||
/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
|
||||
/// reference to the sticky node if one is currently active.
|
||||
#[derive(Debug)]
|
||||
pub struct KeymapResult<'a> {
|
||||
pub kind: KeymapResultKind,
|
||||
pub sticky: Option<&'a KeyTrieNode>,
|
||||
@ -395,6 +420,7 @@ fn default() -> Keymaps {
|
||||
"F" => find_prev_char,
|
||||
"r" => replace,
|
||||
"R" => replace_with_yanked,
|
||||
"A-." => repeat_last_motion,
|
||||
|
||||
"~" => switch_case,
|
||||
"`" => switch_to_lowercase,
|
||||
@ -427,6 +453,8 @@ fn default() -> Keymaps {
|
||||
"m" => goto_window_middle,
|
||||
"b" => goto_window_bottom,
|
||||
"a" => goto_last_accessed_file,
|
||||
"n" => goto_next_buffer,
|
||||
"p" => goto_previous_buffer,
|
||||
},
|
||||
":" => command_mode,
|
||||
|
||||
@ -476,10 +504,9 @@ fn default() -> Keymaps {
|
||||
},
|
||||
|
||||
"/" => search,
|
||||
// ? for search_reverse
|
||||
"?" => rsearch,
|
||||
"n" => search_next,
|
||||
"N" => extend_search_next,
|
||||
// N for search_prev
|
||||
"N" => search_prev,
|
||||
"*" => search_selection,
|
||||
|
||||
"u" => undo,
|
||||
@ -520,9 +547,13 @@ fn default() -> Keymaps {
|
||||
|
||||
"C-w" => { "Window"
|
||||
"C-w" | "w" => rotate_view,
|
||||
"C-h" | "h" => hsplit,
|
||||
"C-s" | "s" => hsplit,
|
||||
"C-v" | "v" => vsplit,
|
||||
"C-q" | "q" => wclose,
|
||||
"C-h" | "h" | "left" => jump_view_left,
|
||||
"C-j" | "j" | "down" => jump_view_down,
|
||||
"C-k" | "k" | "up" => jump_view_up,
|
||||
"C-l" | "l" | "right" => jump_view_right,
|
||||
},
|
||||
|
||||
// move under <space>c
|
||||
@ -621,6 +652,9 @@ fn default() -> Keymaps {
|
||||
"B" => extend_prev_long_word_start,
|
||||
"E" => extend_next_long_word_end,
|
||||
|
||||
"n" => extend_search_next,
|
||||
"N" => extend_search_prev,
|
||||
|
||||
"t" => extend_till_char,
|
||||
"f" => extend_next_char,
|
||||
"T" => extend_till_prev_char,
|
||||
@ -669,6 +703,9 @@ pub fn merge_keys(mut config: Config) -> Config {
|
||||
config
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
#[test]
|
||||
fn merge_partial_keys() {
|
||||
let config = Config {
|
||||
@ -729,3 +766,38 @@ fn merge_partial_keys() {
|
||||
assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
|
||||
assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn order_should_be_set() {
|
||||
let config = Config {
|
||||
keys: Keymaps(hashmap! {
|
||||
Mode::Normal => Keymap::new(
|
||||
keymap!({ "Normal mode"
|
||||
"space" => { ""
|
||||
"s" => { ""
|
||||
"v" => vsplit,
|
||||
"c" => hsplit,
|
||||
},
|
||||
},
|
||||
})
|
||||
)
|
||||
}),
|
||||
..Default::default()
|
||||
};
|
||||
let mut merged_config = merge_keys(config.clone());
|
||||
assert_ne!(config, merged_config);
|
||||
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
|
||||
// Make sure mapping works
|
||||
assert_eq!(
|
||||
keymap
|
||||
.root()
|
||||
.search(&[key!(' '), key!('s'), key!('v')])
|
||||
.unwrap(),
|
||||
&KeyTrie::Leaf(Command::vsplit),
|
||||
"Leaf should be present in merged subnode"
|
||||
);
|
||||
// Make sure an order was set during merge
|
||||
let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
|
||||
assert!(!node.node().unwrap().order().is_empty())
|
||||
}
|
||||
}
|
||||
|
@ -16,6 +16,11 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||
};
|
||||
|
||||
// Separate file config so we can include year, month and day in file logs
|
||||
let file = std::fs::OpenOptions::new()
|
||||
.write(true)
|
||||
.create(true)
|
||||
.truncate(true)
|
||||
.open(logpath)?;
|
||||
let file_config = fern::Dispatch::new()
|
||||
.format(|out, message, record| {
|
||||
out.finish(format_args!(
|
||||
@ -26,7 +31,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
|
||||
message
|
||||
))
|
||||
})
|
||||
.chain(fern::log_file(logpath)?);
|
||||
.chain(file);
|
||||
|
||||
base_config.chain(file_config).apply()?;
|
||||
|
||||
@ -55,6 +60,7 @@ async fn main() -> Result<()> {
|
||||
|
||||
FLAGS:
|
||||
-h, --help Prints help information
|
||||
--tutor Loads the tutorial
|
||||
-v Increases logging verbosity each use for up to 3 times
|
||||
(default file: {})
|
||||
-V, --version Prints version information
|
||||
|
@ -5,7 +5,7 @@
|
||||
use std::borrow::Cow;
|
||||
|
||||
use helix_core::Transaction;
|
||||
use helix_view::{graphics::Rect, Document, Editor, View};
|
||||
use helix_view::{graphics::Rect, Document, Editor};
|
||||
|
||||
use crate::commands;
|
||||
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||
@ -30,31 +30,32 @@ fn row(&self) -> menu::Row {
|
||||
menu::Row::new(vec![
|
||||
menu::Cell::from(self.label.as_str()),
|
||||
menu::Cell::from(match self.kind {
|
||||
Some(lsp::CompletionItemKind::Text) => "text",
|
||||
Some(lsp::CompletionItemKind::Method) => "method",
|
||||
Some(lsp::CompletionItemKind::Function) => "function",
|
||||
Some(lsp::CompletionItemKind::Constructor) => "constructor",
|
||||
Some(lsp::CompletionItemKind::Field) => "field",
|
||||
Some(lsp::CompletionItemKind::Variable) => "variable",
|
||||
Some(lsp::CompletionItemKind::Class) => "class",
|
||||
Some(lsp::CompletionItemKind::Interface) => "interface",
|
||||
Some(lsp::CompletionItemKind::Module) => "module",
|
||||
Some(lsp::CompletionItemKind::Property) => "property",
|
||||
Some(lsp::CompletionItemKind::Unit) => "unit",
|
||||
Some(lsp::CompletionItemKind::Value) => "value",
|
||||
Some(lsp::CompletionItemKind::Enum) => "enum",
|
||||
Some(lsp::CompletionItemKind::Keyword) => "keyword",
|
||||
Some(lsp::CompletionItemKind::Snippet) => "snippet",
|
||||
Some(lsp::CompletionItemKind::Color) => "color",
|
||||
Some(lsp::CompletionItemKind::File) => "file",
|
||||
Some(lsp::CompletionItemKind::Reference) => "reference",
|
||||
Some(lsp::CompletionItemKind::Folder) => "folder",
|
||||
Some(lsp::CompletionItemKind::EnumMember) => "enum_member",
|
||||
Some(lsp::CompletionItemKind::Constant) => "constant",
|
||||
Some(lsp::CompletionItemKind::Struct) => "struct",
|
||||
Some(lsp::CompletionItemKind::Event) => "event",
|
||||
Some(lsp::CompletionItemKind::Operator) => "operator",
|
||||
Some(lsp::CompletionItemKind::TypeParameter) => "type_param",
|
||||
Some(lsp::CompletionItemKind::TEXT) => "text",
|
||||
Some(lsp::CompletionItemKind::METHOD) => "method",
|
||||
Some(lsp::CompletionItemKind::FUNCTION) => "function",
|
||||
Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
|
||||
Some(lsp::CompletionItemKind::FIELD) => "field",
|
||||
Some(lsp::CompletionItemKind::VARIABLE) => "variable",
|
||||
Some(lsp::CompletionItemKind::CLASS) => "class",
|
||||
Some(lsp::CompletionItemKind::INTERFACE) => "interface",
|
||||
Some(lsp::CompletionItemKind::MODULE) => "module",
|
||||
Some(lsp::CompletionItemKind::PROPERTY) => "property",
|
||||
Some(lsp::CompletionItemKind::UNIT) => "unit",
|
||||
Some(lsp::CompletionItemKind::VALUE) => "value",
|
||||
Some(lsp::CompletionItemKind::ENUM) => "enum",
|
||||
Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
|
||||
Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
|
||||
Some(lsp::CompletionItemKind::COLOR) => "color",
|
||||
Some(lsp::CompletionItemKind::FILE) => "file",
|
||||
Some(lsp::CompletionItemKind::REFERENCE) => "reference",
|
||||
Some(lsp::CompletionItemKind::FOLDER) => "folder",
|
||||
Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
|
||||
Some(lsp::CompletionItemKind::CONSTANT) => "constant",
|
||||
Some(lsp::CompletionItemKind::STRUCT) => "struct",
|
||||
Some(lsp::CompletionItemKind::EVENT) => "event",
|
||||
Some(lsp::CompletionItemKind::OPERATOR) => "operator",
|
||||
Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
|
||||
Some(kind) => unimplemented!("{:?}", kind),
|
||||
None => "",
|
||||
}),
|
||||
// self.detail.as_deref().unwrap_or("")
|
||||
@ -83,13 +84,13 @@ pub fn new(
|
||||
start_offset: usize,
|
||||
trigger_offset: usize,
|
||||
) -> Self {
|
||||
// let items: Vec<CompletionItem> = Vec::new();
|
||||
let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
|
||||
fn item_to_transaction(
|
||||
doc: &Document,
|
||||
view: &View,
|
||||
item: &CompletionItem,
|
||||
offset_encoding: helix_lsp::OffsetEncoding,
|
||||
start_offset: usize,
|
||||
trigger_offset: usize,
|
||||
) -> Transaction {
|
||||
if let Some(edit) = &item.text_edit {
|
||||
let edit = match edit {
|
||||
@ -105,63 +106,52 @@ fn item_to_transaction(
|
||||
)
|
||||
} else {
|
||||
let text = item.insert_text.as_ref().unwrap_or(&item.label);
|
||||
let cursor = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
// Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
|
||||
// in these cases we need to check for a common prefix and remove it
|
||||
let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
|
||||
let text = text.trim_start_matches::<&str>(&prefix);
|
||||
Transaction::change(
|
||||
doc.text(),
|
||||
vec![(cursor, cursor, Some(text.as_str().into()))].into_iter(),
|
||||
vec![(trigger_offset, trigger_offset, Some(text.into()))].into_iter(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
// if more text was entered, remove it
|
||||
doc.restore(view.id);
|
||||
|
||||
match event {
|
||||
PromptEvent::Abort => {}
|
||||
PromptEvent::Update => {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
// always present here
|
||||
let item = item.unwrap();
|
||||
|
||||
// if more text was entered, remove it
|
||||
// TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
|
||||
let cursor = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
if trigger_offset < cursor {
|
||||
let remove = Transaction::change(
|
||||
doc.text(),
|
||||
vec![(trigger_offset, cursor, None)].into_iter(),
|
||||
let transaction = item_to_transaction(
|
||||
doc,
|
||||
item,
|
||||
offset_encoding,
|
||||
start_offset,
|
||||
trigger_offset,
|
||||
);
|
||||
doc.apply(&remove, view.id);
|
||||
}
|
||||
|
||||
let transaction = item_to_transaction(doc, view, item, offset_encoding);
|
||||
// initialize a savepoint
|
||||
doc.savepoint();
|
||||
|
||||
doc.apply(&transaction, view.id);
|
||||
}
|
||||
PromptEvent::Validate => {
|
||||
let (view, doc) = current!(editor);
|
||||
|
||||
// always present here
|
||||
let item = item.unwrap();
|
||||
|
||||
// if more text was entered, remove it
|
||||
// TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes
|
||||
let cursor = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
if trigger_offset < cursor {
|
||||
let remove = Transaction::change(
|
||||
doc.text(),
|
||||
vec![(trigger_offset, cursor, None)].into_iter(),
|
||||
let transaction = item_to_transaction(
|
||||
doc,
|
||||
item,
|
||||
offset_encoding,
|
||||
start_offset,
|
||||
trigger_offset,
|
||||
);
|
||||
doc.apply(&remove, view.id);
|
||||
}
|
||||
|
||||
let transaction = item_to_transaction(doc, view, item, offset_encoding);
|
||||
doc.apply(&transaction, view.id);
|
||||
|
||||
if let Some(additional_edits) = &item.additional_text_edits {
|
||||
@ -210,7 +200,7 @@ pub fn recompute_filter(&mut self, editor: &Editor) {
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
if self.start_offset <= cursor {
|
||||
if self.trigger_offset <= cursor {
|
||||
let fragment = doc.text().slice(self.start_offset..cursor);
|
||||
let text = Cow::from(fragment);
|
||||
// TODO: logic is same as ui/picker
|
||||
@ -274,12 +264,10 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
.language()
|
||||
.and_then(|scope| scope.strip_prefix("source."))
|
||||
.unwrap_or("");
|
||||
let cursor_pos = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
|
||||
- view.offset.row) as u16;
|
||||
let text = doc.text().slice(..);
|
||||
let cursor_pos = doc.selection(view.id).primary().cursor(text);
|
||||
let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
|
||||
let cursor_pos = (coords.row - view.offset.row) as u16;
|
||||
let mut markdown_doc = match &option.documentation {
|
||||
Some(lsp::Documentation::String(contents))
|
||||
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {
|
||||
|
@ -689,6 +689,8 @@ pub fn render_statusline(
|
||||
theme: &Theme,
|
||||
is_focused: bool,
|
||||
) {
|
||||
use tui::text::{Span, Spans};
|
||||
|
||||
//-------------------------------
|
||||
// Left side of the status line.
|
||||
//-------------------------------
|
||||
@ -707,17 +709,17 @@ pub fn render_statusline(
|
||||
})
|
||||
.unwrap_or("");
|
||||
|
||||
let style = if is_focused {
|
||||
let base_style = if is_focused {
|
||||
theme.get("ui.statusline")
|
||||
} else {
|
||||
theme.get("ui.statusline.inactive")
|
||||
};
|
||||
// statusline
|
||||
surface.set_style(viewport.with_height(1), style);
|
||||
surface.set_style(viewport.with_height(1), base_style);
|
||||
if is_focused {
|
||||
surface.set_string(viewport.x + 1, viewport.y, mode, style);
|
||||
surface.set_string(viewport.x + 1, viewport.y, mode, base_style);
|
||||
}
|
||||
surface.set_string(viewport.x + 5, viewport.y, progress, style);
|
||||
surface.set_string(viewport.x + 5, viewport.y, progress, base_style);
|
||||
|
||||
if let Some(path) = doc.relative_path() {
|
||||
let path = path.to_string_lossy();
|
||||
@ -728,7 +730,7 @@ pub fn render_statusline(
|
||||
viewport.y,
|
||||
title,
|
||||
viewport.width.saturating_sub(6) as usize,
|
||||
style,
|
||||
base_style,
|
||||
);
|
||||
}
|
||||
|
||||
@ -736,8 +738,50 @@ pub fn render_statusline(
|
||||
// Right side of the status line.
|
||||
//-------------------------------
|
||||
|
||||
// Compute the individual info strings.
|
||||
let diag_count = format!("{}", doc.diagnostics().len());
|
||||
let mut right_side_text = Spans::default();
|
||||
|
||||
// Compute the individual info strings and add them to `right_side_text`.
|
||||
|
||||
// Diagnostics
|
||||
let diags = doc.diagnostics().iter().fold((0, 0), |mut counts, diag| {
|
||||
use helix_core::diagnostic::Severity;
|
||||
match diag.severity {
|
||||
Some(Severity::Warning) => counts.0 += 1,
|
||||
Some(Severity::Error) | None => counts.1 += 1,
|
||||
_ => {}
|
||||
}
|
||||
counts
|
||||
});
|
||||
let (warnings, errors) = diags;
|
||||
let warning_style = theme.get("warning");
|
||||
let error_style = theme.get("error");
|
||||
for i in 0..2 {
|
||||
let (count, style) = match i {
|
||||
0 => (warnings, warning_style),
|
||||
1 => (errors, error_style),
|
||||
_ => unreachable!(),
|
||||
};
|
||||
if count == 0 {
|
||||
continue;
|
||||
}
|
||||
let style = base_style.patch(style);
|
||||
right_side_text.0.push(Span::styled("●", style));
|
||||
right_side_text
|
||||
.0
|
||||
.push(Span::styled(format!(" {} ", count), base_style));
|
||||
}
|
||||
|
||||
// Selections
|
||||
let sels_count = doc.selection(view.id).len();
|
||||
right_side_text.0.push(Span::styled(
|
||||
format!(
|
||||
" {} sel{} ",
|
||||
sels_count,
|
||||
if sels_count == 1 { "" } else { "s" }
|
||||
),
|
||||
base_style,
|
||||
));
|
||||
|
||||
// let indent_info = match doc.indent_style {
|
||||
// IndentStyle::Tabs => "tabs",
|
||||
// IndentStyle::Spaces(1) => "spaces:1",
|
||||
@ -750,29 +794,28 @@ pub fn render_statusline(
|
||||
// IndentStyle::Spaces(8) => "spaces:8",
|
||||
// _ => "indent:ERROR",
|
||||
// };
|
||||
let position_info = {
|
||||
|
||||
// Position
|
||||
let pos = coords_at_pos(
|
||||
doc.text().slice(..),
|
||||
doc.selection(view.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..)),
|
||||
);
|
||||
format!("{}:{}", pos.row + 1, pos.col + 1) // convert to 1-indexing
|
||||
};
|
||||
right_side_text.0.push(Span::styled(
|
||||
format!(" {}:{} ", pos.row + 1, pos.col + 1), // Convert to 1-indexing.
|
||||
base_style,
|
||||
));
|
||||
|
||||
// Render them to the status line together.
|
||||
let right_side_text = format!(
|
||||
"{} {} ",
|
||||
&diag_count[..diag_count.len().min(4)],
|
||||
// indent_info,
|
||||
position_info
|
||||
);
|
||||
let text_len = right_side_text.len() as u16;
|
||||
surface.set_string(
|
||||
viewport.x + viewport.width.saturating_sub(text_len),
|
||||
// Render to the statusline.
|
||||
surface.set_spans(
|
||||
viewport.x
|
||||
+ viewport
|
||||
.width
|
||||
.saturating_sub(right_side_text.width() as u16),
|
||||
viewport.y,
|
||||
right_side_text,
|
||||
style,
|
||||
&right_side_text,
|
||||
right_side_text.width() as u16,
|
||||
);
|
||||
}
|
||||
|
||||
@ -984,7 +1027,7 @@ fn command_mode(&mut self, mode: Mode, cxt: &mut commands::Context, event: KeyEv
|
||||
|
||||
pub fn set_completion(
|
||||
&mut self,
|
||||
editor: &Editor,
|
||||
editor: &mut Editor,
|
||||
items: Vec<helix_lsp::lsp::CompletionItem>,
|
||||
offset_encoding: helix_lsp::OffsetEncoding,
|
||||
start_offset: usize,
|
||||
@ -999,10 +1042,21 @@ pub fn set_completion(
|
||||
return;
|
||||
}
|
||||
|
||||
// Immediately initialize a savepoint
|
||||
doc_mut!(editor).savepoint();
|
||||
|
||||
// TODO : propagate required size on resize to completion too
|
||||
completion.required_size((size.width, size.height));
|
||||
self.completion = Some(completion);
|
||||
}
|
||||
|
||||
pub fn clear_completion(&mut self, editor: &mut Editor) {
|
||||
self.completion = None;
|
||||
// Clear any savepoints
|
||||
let (_, doc) = current!(editor);
|
||||
doc.savepoint = None;
|
||||
editor.clear_idle_timer(); // don't retrigger
|
||||
}
|
||||
}
|
||||
|
||||
impl EditorView {
|
||||
@ -1022,12 +1076,12 @@ fn handle_mouse_event(
|
||||
let editor = &mut cxt.editor;
|
||||
|
||||
let result = editor.tree.views().find_map(|(view, _focus)| {
|
||||
view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
|
||||
view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
|
||||
.map(|pos| (pos, view.id))
|
||||
});
|
||||
|
||||
if let Some((pos, view_id)) = result {
|
||||
let doc = &mut editor.documents[editor.tree.get(view_id).doc];
|
||||
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
|
||||
|
||||
if modifiers == crossterm::event::KeyModifiers::ALT {
|
||||
let selection = doc.selection(view_id).clone();
|
||||
@ -1096,7 +1150,7 @@ fn handle_mouse_event(
|
||||
};
|
||||
|
||||
let result = cxt.editor.tree.views().find_map(|(view, _focus)| {
|
||||
view.pos_at_screen_coords(&cxt.editor.documents[view.doc], row, column)
|
||||
view.pos_at_screen_coords(&cxt.editor.documents[&view.doc], row, column)
|
||||
.map(|_| view.id)
|
||||
});
|
||||
|
||||
@ -1182,12 +1236,12 @@ fn handle_mouse_event(
|
||||
}
|
||||
|
||||
let result = editor.tree.views().find_map(|(view, _focus)| {
|
||||
view.pos_at_screen_coords(&editor.documents[view.doc], row, column)
|
||||
view.pos_at_screen_coords(&editor.documents[&view.doc], row, column)
|
||||
.map(|pos| (pos, view.id))
|
||||
});
|
||||
|
||||
if let Some((pos, view_id)) = result {
|
||||
let doc = &mut editor.documents[editor.tree.get(view_id).doc];
|
||||
let doc = editor.document_mut(editor.tree.get(view_id).doc).unwrap();
|
||||
doc.set_selection(view_id, Selection::point(pos));
|
||||
editor.tree.focus = view_id;
|
||||
commands::Command::paste_primary_clipboard_before.execute(cxt);
|
||||
@ -1254,8 +1308,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
|
||||
if callback.is_some() {
|
||||
// assume close_fn
|
||||
self.completion = None;
|
||||
cxt.editor.clear_idle_timer(); // don't retrigger
|
||||
self.clear_completion(cxt.editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1268,8 +1321,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
if let Some(completion) = &mut self.completion {
|
||||
completion.update(&mut cxt);
|
||||
if completion.is_empty() {
|
||||
self.completion = None;
|
||||
cxt.editor.clear_idle_timer(); // don't retrigger
|
||||
self.clear_completion(cxt.editor);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1397,9 +1449,11 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
info.render(area, surface, cx);
|
||||
}
|
||||
|
||||
if cx.editor.config.auto_info {
|
||||
if let Some(ref mut info) = self.autoinfo {
|
||||
info.render(area, surface, cx);
|
||||
}
|
||||
}
|
||||
|
||||
let key_width = 15u16; // for showing pending keys
|
||||
let mut status_msg_width = 0;
|
||||
@ -1469,7 +1523,7 @@ fn canonicalize_key(key: &mut KeyEvent) {
|
||||
}
|
||||
|
||||
#[inline]
|
||||
fn abs_diff(a: usize, b: usize) -> usize {
|
||||
const fn abs_diff(a: usize, b: usize) -> usize {
|
||||
if a > b {
|
||||
a - b
|
||||
} else {
|
||||
|
@ -64,25 +64,23 @@ pub fn new(
|
||||
}
|
||||
|
||||
pub fn score(&mut self, pattern: &str) {
|
||||
// need to borrow via pattern match otherwise it complains about simultaneous borrow
|
||||
let Self {
|
||||
ref mut matcher,
|
||||
ref mut matches,
|
||||
ref options,
|
||||
..
|
||||
} = *self;
|
||||
|
||||
// reuse the matches allocation
|
||||
matches.clear();
|
||||
matches.extend(options.iter().enumerate().filter_map(|(index, option)| {
|
||||
self.matches.clear();
|
||||
self.matches.extend(
|
||||
self.options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, option)| {
|
||||
let text = option.filter_text();
|
||||
// TODO: using fuzzy_indices could give us the char idx for match highlighting
|
||||
matcher
|
||||
self.matcher
|
||||
.fuzzy_match(text, pattern)
|
||||
.map(|score| (index, score))
|
||||
}));
|
||||
}),
|
||||
);
|
||||
// matches.sort_unstable_by_key(|(_, score)| -score);
|
||||
matches.sort_unstable_by_key(|(index, _score)| options[*index].sort_text());
|
||||
self.matches
|
||||
.sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
|
||||
|
||||
// reset cursor position
|
||||
self.cursor = None;
|
||||
@ -100,7 +98,8 @@ pub fn clear(&mut self) {
|
||||
|
||||
pub fn move_up(&mut self) {
|
||||
let len = self.matches.len();
|
||||
let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len;
|
||||
let max_index = len.saturating_sub(1);
|
||||
let pos = self.cursor.map_or(max_index, |i| (i + max_index) % len) % len;
|
||||
self.cursor = Some(pos);
|
||||
self.adjust_scroll();
|
||||
}
|
||||
@ -216,6 +215,10 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('k'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
self.move_up();
|
||||
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
|
||||
@ -233,6 +236,10 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('j'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
self.move_down();
|
||||
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
|
||||
|
@ -29,6 +29,7 @@ pub fn regex_prompt(
|
||||
cx: &mut crate::commands::Context,
|
||||
prompt: std::borrow::Cow<'static, str>,
|
||||
history_register: Option<char>,
|
||||
completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static,
|
||||
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
|
||||
) -> Prompt {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
@ -38,7 +39,7 @@ pub fn regex_prompt(
|
||||
Prompt::new(
|
||||
prompt,
|
||||
history_register,
|
||||
|_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
|
||||
completion_fn,
|
||||
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
|
||||
match event {
|
||||
PromptEvent::Abort => {
|
||||
@ -92,9 +93,25 @@ pub fn regex_prompt(
|
||||
}
|
||||
|
||||
pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
|
||||
use ignore::Walk;
|
||||
use ignore::{types::TypesBuilder, WalkBuilder};
|
||||
use std::time;
|
||||
let files = Walk::new(&root).filter_map(|entry| {
|
||||
|
||||
// We want to exclude files that the editor can't handle yet
|
||||
let mut type_builder = TypesBuilder::new();
|
||||
let mut walk_builder = WalkBuilder::new(&root);
|
||||
let walk_builder = match type_builder.add(
|
||||
"compressed",
|
||||
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
|
||||
) {
|
||||
Err(_) => &walk_builder,
|
||||
_ => {
|
||||
type_builder.negate("all");
|
||||
let excluded_types = type_builder.build().unwrap();
|
||||
walk_builder.types(excluded_types)
|
||||
}
|
||||
};
|
||||
|
||||
let files = walk_builder.build().filter_map(|entry| {
|
||||
let entry = entry.ok()?;
|
||||
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
|
||||
if entry.path().is_dir() {
|
||||
|
@ -12,7 +12,12 @@
|
||||
use fuzzy_matcher::FuzzyMatcher;
|
||||
use tui::widgets::Widget;
|
||||
|
||||
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::HashMap,
|
||||
io::Read,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
use crate::ui::{Prompt, PromptEvent};
|
||||
use helix_core::Position;
|
||||
@ -23,18 +28,58 @@
|
||||
};
|
||||
|
||||
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
|
||||
/// Biggest file size to preview in bytes
|
||||
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
|
||||
|
||||
/// File path and line number (used to align and highlight a line)
|
||||
/// File path and range of lines (used to align and highlight lines)
|
||||
type FileLocation = (PathBuf, Option<(usize, usize)>);
|
||||
|
||||
pub struct FilePicker<T> {
|
||||
picker: Picker<T>,
|
||||
/// Caches paths to documents
|
||||
preview_cache: HashMap<PathBuf, Document>,
|
||||
preview_cache: HashMap<PathBuf, CachedPreview>,
|
||||
read_buffer: Vec<u8>,
|
||||
/// Given an item in the picker, return the file path and line number to display.
|
||||
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
|
||||
}
|
||||
|
||||
pub enum CachedPreview {
|
||||
Document(Document),
|
||||
Binary,
|
||||
LargeFile,
|
||||
NotFound,
|
||||
}
|
||||
|
||||
// We don't store this enum in the cache so as to avoid lifetime constraints
|
||||
// from borrowing a document already opened in the editor.
|
||||
pub enum Preview<'picker, 'editor> {
|
||||
Cached(&'picker CachedPreview),
|
||||
EditorDocument(&'editor Document),
|
||||
}
|
||||
|
||||
impl Preview<'_, '_> {
|
||||
fn document(&self) -> Option<&Document> {
|
||||
match self {
|
||||
Preview::EditorDocument(doc) => Some(doc),
|
||||
Preview::Cached(CachedPreview::Document(doc)) => Some(doc),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
/// Alternate text to show for the preview.
|
||||
fn placeholder(&self) -> &str {
|
||||
match *self {
|
||||
Self::EditorDocument(_) => "<File preview>",
|
||||
Self::Cached(preview) => match preview {
|
||||
CachedPreview::Document(_) => "<File preview>",
|
||||
CachedPreview::Binary => "<Binary file>",
|
||||
CachedPreview::LargeFile => "<File too large to preview>",
|
||||
CachedPreview::NotFound => "<File not found>",
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> FilePicker<T> {
|
||||
pub fn new(
|
||||
options: Vec<T>,
|
||||
@ -45,6 +90,7 @@ pub fn new(
|
||||
Self {
|
||||
picker: Picker::new(false, options, format_fn, callback_fn),
|
||||
preview_cache: HashMap::new(),
|
||||
read_buffer: Vec::with_capacity(1024),
|
||||
file_fn: Box::new(preview_fn),
|
||||
}
|
||||
}
|
||||
@ -60,14 +106,45 @@ fn current_file(&self, editor: &Editor) -> Option<FileLocation> {
|
||||
})
|
||||
}
|
||||
|
||||
fn calculate_preview(&mut self, editor: &Editor) {
|
||||
if let Some((path, _line)) = self.current_file(editor) {
|
||||
if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
|
||||
/// Get (cached) preview for a given path. If a document corresponding
|
||||
/// to the path is already open in the editor, it is used instead.
|
||||
fn get_preview<'picker, 'editor>(
|
||||
&'picker mut self,
|
||||
path: &Path,
|
||||
editor: &'editor Editor,
|
||||
) -> Preview<'picker, 'editor> {
|
||||
if let Some(doc) = editor.document_by_path(path) {
|
||||
return Preview::EditorDocument(doc);
|
||||
}
|
||||
|
||||
if self.preview_cache.contains_key(path) {
|
||||
return Preview::Cached(&self.preview_cache[path]);
|
||||
}
|
||||
|
||||
let data = std::fs::File::open(path).and_then(|file| {
|
||||
let metadata = file.metadata()?;
|
||||
// Read up to 1kb to detect the content type
|
||||
let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
|
||||
let content_type = content_inspector::inspect(&self.read_buffer[..n]);
|
||||
self.read_buffer.clear();
|
||||
Ok((metadata, content_type))
|
||||
});
|
||||
let preview = data
|
||||
.map(
|
||||
|(metadata, content_type)| match (metadata.len(), content_type) {
|
||||
(_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
|
||||
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
|
||||
_ => {
|
||||
// TODO: enable syntax highlighting; blocked by async rendering
|
||||
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
|
||||
self.preview_cache.insert(path, doc);
|
||||
}
|
||||
Document::open(path, None, Some(&editor.theme), None)
|
||||
.map(CachedPreview::Document)
|
||||
.unwrap_or(CachedPreview::NotFound)
|
||||
}
|
||||
},
|
||||
)
|
||||
.unwrap_or(CachedPreview::NotFound);
|
||||
self.preview_cache.insert(path.to_owned(), preview);
|
||||
Preview::Cached(&self.preview_cache[path])
|
||||
}
|
||||
}
|
||||
|
||||
@ -79,12 +156,12 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
// |picker | | |
|
||||
// | | | |
|
||||
// +---------+ +---------+
|
||||
self.calculate_preview(cx.editor);
|
||||
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
|
||||
let area = inner_rect(area);
|
||||
// -- Render the frame:
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.background");
|
||||
let text = cx.editor.theme.get("ui.text");
|
||||
surface.clear_with(area, background);
|
||||
|
||||
let picker_width = if render_preview {
|
||||
@ -113,17 +190,23 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
horizontal: 1,
|
||||
};
|
||||
let inner = inner.inner(&margin);
|
||||
|
||||
block.render(preview_area, surface);
|
||||
|
||||
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
|
||||
cx.editor
|
||||
.document_by_path(&path)
|
||||
.or_else(|| self.preview_cache.get(&path))
|
||||
.zip(Some(range))
|
||||
}) {
|
||||
if let Some((path, range)) = self.current_file(cx.editor) {
|
||||
let preview = self.get_preview(&path, cx.editor);
|
||||
let doc = match preview.document() {
|
||||
Some(doc) => doc,
|
||||
None => {
|
||||
let alt_text = preview.placeholder();
|
||||
let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
|
||||
let y = inner.y + inner.height / 2;
|
||||
surface.set_stringn(x, y, alt_text, inner.width as usize, text);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
// align to middle
|
||||
let first_line = line
|
||||
let first_line = range
|
||||
.map(|(start, end)| {
|
||||
let height = end.saturating_sub(start) + 1;
|
||||
let middle = start + (height.saturating_sub(1) / 2);
|
||||
@ -150,7 +233,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
);
|
||||
|
||||
// highlight the line
|
||||
if let Some((start, end)) = line {
|
||||
if let Some((start, end)) = range {
|
||||
let offset = start.saturating_sub(first_line) as u16;
|
||||
surface.set_style(
|
||||
Rect::new(
|
||||
@ -234,37 +317,28 @@ pub fn new(
|
||||
}
|
||||
|
||||
pub fn score(&mut self) {
|
||||
// need to borrow via pattern match otherwise it complains about simultaneous borrow
|
||||
let Self {
|
||||
ref mut matcher,
|
||||
ref mut matches,
|
||||
ref filters,
|
||||
ref format_fn,
|
||||
..
|
||||
} = *self;
|
||||
|
||||
let pattern = &self.prompt.line;
|
||||
|
||||
// reuse the matches allocation
|
||||
matches.clear();
|
||||
matches.extend(
|
||||
self.matches.clear();
|
||||
self.matches.extend(
|
||||
self.options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(index, option)| {
|
||||
// filter options first before matching
|
||||
if !filters.is_empty() {
|
||||
filters.binary_search(&index).ok()?;
|
||||
if !self.filters.is_empty() {
|
||||
self.filters.binary_search(&index).ok()?;
|
||||
}
|
||||
// TODO: maybe using format_fn isn't the best idea here
|
||||
let text = (format_fn)(option);
|
||||
let text = (self.format_fn)(option);
|
||||
// TODO: using fuzzy_indices could give us the char idx for match highlighting
|
||||
matcher
|
||||
self.matcher
|
||||
.fuzzy_match(&text, pattern)
|
||||
.map(|score| (index, score))
|
||||
}),
|
||||
);
|
||||
matches.sort_unstable_by_key(|(_, score)| -score);
|
||||
self.matches.sort_unstable_by_key(|(_, score)| -score);
|
||||
|
||||
// reset cursor position
|
||||
self.cursor = 0;
|
||||
@ -337,6 +411,10 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
code: KeyCode::BackTab,
|
||||
..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('k'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('p'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
@ -350,6 +428,10 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
| KeyEvent {
|
||||
code: KeyCode::Tab, ..
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('j'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
}
|
||||
| KeyEvent {
|
||||
code: KeyCode::Char('n'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
@ -375,7 +457,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
return close_fn;
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('h'),
|
||||
code: KeyCode::Char('s'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
if let Some(option) = self.selection() {
|
||||
@ -485,6 +567,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
text_style
|
||||
},
|
||||
true,
|
||||
true,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -186,6 +186,11 @@ pub fn insert_char(&mut self, c: char) {
|
||||
self.exit_selection();
|
||||
}
|
||||
|
||||
pub fn insert_str(&mut self, s: &str) {
|
||||
self.line.insert_str(self.cursor, s);
|
||||
self.cursor += s.len();
|
||||
}
|
||||
|
||||
pub fn move_cursor(&mut self, movement: Movement) {
|
||||
let pos = self.eval_movement(movement);
|
||||
self.cursor = pos
|
||||
@ -474,6 +479,26 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
self.delete_char_backwards();
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('s'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
} => {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
let text = doc.text().slice(..);
|
||||
|
||||
use helix_core::textobject;
|
||||
let range = textobject::textobject_word(
|
||||
text,
|
||||
doc.selection(view.id).primary(),
|
||||
textobject::TextObject::Inside,
|
||||
1,
|
||||
);
|
||||
let line = text.slice(range.from()..range.to()).to_string();
|
||||
if !line.is_empty() {
|
||||
self.insert_str(line.as_str());
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Enter,
|
||||
..
|
||||
@ -502,6 +527,7 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
if let Some(register) = self.history_register {
|
||||
let register = cx.editor.registers.get_mut(register);
|
||||
self.change_history(register.read(), CompletionDirection::Backward);
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
@ -515,15 +541,22 @@ fn handle_event(&mut self, event: Event, cx: &mut Context) -> EventResult {
|
||||
if let Some(register) = self.history_register {
|
||||
let register = cx.editor.registers.get_mut(register);
|
||||
self.change_history(register.read(), CompletionDirection::Forward);
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
|
||||
}
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Tab, ..
|
||||
} => self.change_completion_selection(CompletionDirection::Forward),
|
||||
} => {
|
||||
self.change_completion_selection(CompletionDirection::Forward);
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::BackTab,
|
||||
..
|
||||
} => self.change_completion_selection(CompletionDirection::Backward),
|
||||
} => {
|
||||
self.change_completion_selection(CompletionDirection::Backward);
|
||||
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
|
||||
}
|
||||
KeyEvent {
|
||||
code: KeyCode::Char('q'),
|
||||
modifiers: KeyModifiers::CONTROL,
|
||||
|
@ -1,11 +1,11 @@
|
||||
[package]
|
||||
name = "helix-tui"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
description = """
|
||||
A library to build rich terminal user interfaces or dashboards
|
||||
"""
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
categories = ["editor"]
|
||||
repository = "https://github.com/helix-editor/helix"
|
||||
@ -19,7 +19,7 @@ default = ["crossterm"]
|
||||
bitflags = "1.3"
|
||||
cassowary = "0.3"
|
||||
unicode-segmentation = "1.8"
|
||||
crossterm = { version = "0.21", optional = true }
|
||||
crossterm = { version = "0.22", optional = true }
|
||||
serde = { version = "1", "optional" = true, features = ["derive"]}
|
||||
helix-view = { version = "0.4", path = "../helix-view", features = ["term"] }
|
||||
helix-core = { version = "0.4", path = "../helix-core" }
|
||||
helix-view = { version = "0.5", path = "../helix-view", features = ["term"] }
|
||||
helix-core = { version = "0.5", path = "../helix-core" }
|
||||
|
@ -266,12 +266,14 @@ pub fn set_stringn<S>(
|
||||
where
|
||||
S: AsRef<str>,
|
||||
{
|
||||
self.set_string_truncated(x, y, string, width, style, false)
|
||||
self.set_string_truncated(x, y, string, width, style, false, false)
|
||||
}
|
||||
|
||||
/// Print at most the first `width` characters of a string if enough space is available
|
||||
/// until the end of the line. If `markend` is true appends a `…` at the end of
|
||||
/// truncated lines.
|
||||
/// until the end of the line. If `ellipsis` is true appends a `…` at the end of
|
||||
/// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
|
||||
/// instead of the end.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub fn set_string_truncated<S>(
|
||||
&mut self,
|
||||
x: u16,
|
||||
@ -280,6 +282,7 @@ pub fn set_string_truncated<S>(
|
||||
width: usize,
|
||||
style: Style,
|
||||
ellipsis: bool,
|
||||
truncate_start: bool,
|
||||
) -> (u16, u16)
|
||||
where
|
||||
S: AsRef<str>,
|
||||
@ -289,6 +292,7 @@ pub fn set_string_truncated<S>(
|
||||
let width = if ellipsis { width - 1 } else { width };
|
||||
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
|
||||
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
|
||||
if !truncate_start {
|
||||
for s in graphemes {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
@ -312,6 +316,36 @@ pub fn set_string_truncated<S>(
|
||||
if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
|
||||
self.content[index].set_symbol("…");
|
||||
}
|
||||
} else {
|
||||
let mut start_index = self.index_of(x, y);
|
||||
let mut index = self.index_of(max_offset as u16, y);
|
||||
|
||||
let total_width = string.as_ref().width();
|
||||
let truncated = total_width > width;
|
||||
if ellipsis && truncated {
|
||||
self.content[start_index].set_symbol("…");
|
||||
start_index += 1;
|
||||
}
|
||||
if !truncated {
|
||||
index -= width - total_width;
|
||||
}
|
||||
for s in graphemes.rev() {
|
||||
let width = s.width();
|
||||
if width == 0 {
|
||||
continue;
|
||||
}
|
||||
let start = index - width;
|
||||
if start < start_index {
|
||||
break;
|
||||
}
|
||||
self.content[start].set_symbol(s);
|
||||
self.content[start].set_style(style);
|
||||
for i in start + 1..index {
|
||||
self.content[i].reset();
|
||||
}
|
||||
index -= width;
|
||||
}
|
||||
}
|
||||
(x_offset as u16, y)
|
||||
}
|
||||
|
||||
|
@ -1,8 +1,8 @@
|
||||
[package]
|
||||
name = "helix-view"
|
||||
version = "0.4.1"
|
||||
version = "0.5.0"
|
||||
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
|
||||
edition = "2018"
|
||||
edition = "2021"
|
||||
license = "MPL-2.0"
|
||||
description = "UI abstractions for use in backends"
|
||||
categories = ["editor"]
|
||||
@ -16,10 +16,10 @@ term = ["crossterm"]
|
||||
[dependencies]
|
||||
bitflags = "1.3"
|
||||
anyhow = "1"
|
||||
helix-core = { version = "0.4", path = "../helix-core" }
|
||||
helix-lsp = { version = "0.4", path = "../helix-lsp"}
|
||||
helix-dap = { version = "0.4", path = "../helix-dap"}
|
||||
crossterm = { version = "0.21", optional = true }
|
||||
helix-core = { version = "0.5", path = "../helix-core" }
|
||||
helix-lsp = { version = "0.5", path = "../helix-lsp"}
|
||||
helix-dap = { version = "0.5", path = "../helix-dap"}
|
||||
crossterm = { version = "0.22", optional = true }
|
||||
|
||||
# Conversion traits
|
||||
once_cell = "1.8"
|
||||
|
@ -116,7 +116,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
|
||||
}
|
||||
} else {
|
||||
#[cfg(target_os = "windows")]
|
||||
return Box::new(provider::WindowsProvider::new());
|
||||
return Box::new(provider::WindowsProvider::default());
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
return Box::new(provider::NopProvider::new());
|
||||
@ -145,15 +145,15 @@ mod provider {
|
||||
use anyhow::{bail, Context as _, Result};
|
||||
use std::borrow::Cow;
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
#[derive(Debug)]
|
||||
pub struct NopProvider {
|
||||
buf: String,
|
||||
primary_buf: String,
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
impl NopProvider {
|
||||
#[allow(dead_code)]
|
||||
// Only dead_code on Windows.
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
buf: String::new(),
|
||||
@ -162,6 +162,7 @@ pub fn new() -> Self {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(not(target_os = "windows"))]
|
||||
impl ClipboardProvider for NopProvider {
|
||||
fn name(&self) -> Cow<str> {
|
||||
Cow::Borrowed("none")
|
||||
@ -186,19 +187,8 @@ fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Re
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
#[derive(Debug)]
|
||||
pub struct WindowsProvider {
|
||||
selection_buf: String,
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl WindowsProvider {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
selection_buf: String::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
#[derive(Default, Debug)]
|
||||
pub struct WindowsProvider;
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
impl ClipboardProvider for WindowsProvider {
|
||||
|
@ -23,6 +23,8 @@
|
||||
/// 8kB of buffer space for encoding and decoding `Rope`s.
|
||||
const BUF_SIZE: usize = 8192;
|
||||
|
||||
const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
||||
pub enum Mode {
|
||||
Normal,
|
||||
@ -95,6 +97,9 @@ pub struct Document {
|
||||
// it back as it separated from the edits. We could split out the parts manually but that will
|
||||
// be more troublesome.
|
||||
history: Cell<History>,
|
||||
|
||||
pub savepoint: Option<Transaction>,
|
||||
|
||||
last_saved_revision: usize,
|
||||
version: i32, // should be usize?
|
||||
|
||||
@ -306,8 +311,7 @@ fn take_with<T, F>(mut_ref: &mut T, f: F)
|
||||
T: Default,
|
||||
F: FnOnce(T) -> T,
|
||||
{
|
||||
let t = mem::take(mut_ref);
|
||||
let _ = mem::replace(mut_ref, f(t));
|
||||
*mut_ref = f(mem::take(mut_ref));
|
||||
}
|
||||
|
||||
use helix_lsp::lsp;
|
||||
@ -325,7 +329,8 @@ pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Sel
|
||||
encoding,
|
||||
text,
|
||||
selections: HashMap::default(),
|
||||
indent_style: IndentStyle::Spaces(4),
|
||||
indent_style: DEFAULT_INDENT,
|
||||
line_ending: DEFAULT_LINE_ENDING,
|
||||
mode: Mode::Normal,
|
||||
restore_cursor: false,
|
||||
syntax: None,
|
||||
@ -335,9 +340,9 @@ pub fn from(text: Rope, encoding: Option<&'static encoding_rs::Encoding>) -> Sel
|
||||
diagnostics: Vec::new(),
|
||||
version: 0,
|
||||
history: Cell::new(History::default()),
|
||||
savepoint: None,
|
||||
last_saved_revision: 0,
|
||||
language_server: None,
|
||||
line_ending: DEFAULT_LINE_ENDING,
|
||||
}
|
||||
}
|
||||
|
||||
@ -363,7 +368,7 @@ pub fn open(
|
||||
let mut doc = Self::from(rope, Some(encoding));
|
||||
|
||||
// set the path and try detecting the language
|
||||
doc.set_path(path)?;
|
||||
doc.set_path(Some(path))?;
|
||||
if let Some(loader) = config_loader {
|
||||
doc.detect_language(theme, loader);
|
||||
}
|
||||
@ -495,17 +500,15 @@ pub fn detect_language(&mut self, theme: Option<&Theme>, config_loader: &syntax:
|
||||
}
|
||||
|
||||
/// Detect the indentation used in the file, or otherwise defaults to the language indentation
|
||||
/// configured in `languages.toml`, with a fallback back to 2 space indentation if it isn't
|
||||
/// configured in `languages.toml`, with a fallback to 4 space indentation if it isn't
|
||||
/// specified. Line ending is likewise auto-detected, and will fallback to the default OS
|
||||
/// line ending.
|
||||
pub fn detect_indent_and_line_ending(&mut self) {
|
||||
self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
|
||||
IndentStyle::from_str(
|
||||
self.language
|
||||
.as_ref()
|
||||
.and_then(|config| config.indent.as_ref())
|
||||
.map_or(" ", |config| config.unit.as_str()), // Fallback to 2 spaces.
|
||||
)
|
||||
.map_or(DEFAULT_INDENT, |config| IndentStyle::from_str(&config.unit))
|
||||
});
|
||||
self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
|
||||
}
|
||||
@ -550,12 +553,14 @@ pub fn encoding(&self) -> &'static encoding_rs::Encoding {
|
||||
self.encoding
|
||||
}
|
||||
|
||||
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> {
|
||||
let path = helix_core::path::get_canonicalized_path(path)?;
|
||||
pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> {
|
||||
let path = path
|
||||
.map(helix_core::path::get_canonicalized_path)
|
||||
.transpose()?;
|
||||
|
||||
// if parent doesn't exist we still want to open the document
|
||||
// and error out when document is saved
|
||||
self.path = Some(path);
|
||||
self.path = path;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@ -635,6 +640,14 @@ fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
||||
if !transaction.changes().is_empty() {
|
||||
self.version += 1;
|
||||
|
||||
// generate revert to savepoint
|
||||
if self.savepoint.is_some() {
|
||||
take_with(&mut self.savepoint, |prev_revert| {
|
||||
let revert = transaction.invert(&old_doc);
|
||||
Some(revert.compose(prev_revert.unwrap()))
|
||||
});
|
||||
}
|
||||
|
||||
// update tree-sitter syntax tree
|
||||
if let Some(syntax) = &mut self.syntax {
|
||||
// TODO: no unwrap
|
||||
@ -644,14 +657,13 @@ fn apply_impl(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
||||
}
|
||||
|
||||
// map state.diagnostics over changes::map_pos too
|
||||
// NOTE: seems to do nothing since the language server resends diagnostics on each edit
|
||||
// for diagnostic in &mut self.diagnostics {
|
||||
// use helix_core::Assoc;
|
||||
// let changes = transaction.changes();
|
||||
// diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
|
||||
// diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
|
||||
// diagnostic.line = self.text.char_to_line(diagnostic.range.start);
|
||||
// }
|
||||
for diagnostic in &mut self.diagnostics {
|
||||
use helix_core::Assoc;
|
||||
let changes = transaction.changes();
|
||||
diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
|
||||
diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
|
||||
diagnostic.line = self.text.char_to_line(diagnostic.range.start);
|
||||
}
|
||||
|
||||
// emit lsp notification
|
||||
if let Some(language_server) = self.language_server() {
|
||||
@ -692,8 +704,8 @@ pub fn apply(&mut self, transaction: &Transaction, view_id: ViewId) -> bool {
|
||||
success
|
||||
}
|
||||
|
||||
/// Undo the last modification to the [`Document`].
|
||||
pub fn undo(&mut self, view_id: ViewId) {
|
||||
/// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
|
||||
pub fn undo(&mut self, view_id: ViewId) -> bool {
|
||||
let mut history = self.history.take();
|
||||
let success = if let Some(transaction) = history.undo() {
|
||||
self.apply_impl(transaction, view_id)
|
||||
@ -706,10 +718,11 @@ pub fn undo(&mut self, view_id: ViewId) {
|
||||
// reset changeset to fix len
|
||||
self.changes = ChangeSet::new(self.text());
|
||||
}
|
||||
success
|
||||
}
|
||||
|
||||
/// Redo the last modification to the [`Document`].
|
||||
pub fn redo(&mut self, view_id: ViewId) {
|
||||
/// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
|
||||
pub fn redo(&mut self, view_id: ViewId) -> bool {
|
||||
let mut history = self.history.take();
|
||||
let success = if let Some(transaction) = history.redo() {
|
||||
self.apply_impl(transaction, view_id)
|
||||
@ -722,6 +735,17 @@ pub fn redo(&mut self, view_id: ViewId) {
|
||||
// reset changeset to fix len
|
||||
self.changes = ChangeSet::new(self.text());
|
||||
}
|
||||
success
|
||||
}
|
||||
|
||||
pub fn savepoint(&mut self) {
|
||||
self.savepoint = Some(Transaction::new(self.text()));
|
||||
}
|
||||
|
||||
pub fn restore(&mut self, view_id: ViewId) {
|
||||
if let Some(revert) = self.savepoint.take() {
|
||||
self.apply(&revert, view_id);
|
||||
}
|
||||
}
|
||||
|
||||
/// Undo modifications to the [`Document`] according to `uk`.
|
||||
@ -894,6 +918,9 @@ pub fn diagnostics(&self) -> &[Diagnostic] {
|
||||
|
||||
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
|
||||
self.diagnostics = diagnostics;
|
||||
// sort by range
|
||||
self.diagnostics
|
||||
.sort_unstable_by_key(|diagnostic| diagnostic.range);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -2,7 +2,7 @@
|
||||
clipboard::{get_clipboard_provider, ClipboardProvider},
|
||||
graphics::{CursorKind, Rect},
|
||||
theme::{self, Theme},
|
||||
tree::Tree,
|
||||
tree::{self, Tree},
|
||||
Document, DocumentId, View, ViewId,
|
||||
};
|
||||
|
||||
@ -12,6 +12,7 @@
|
||||
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
collections::BTreeMap,
|
||||
path::{Path, PathBuf},
|
||||
pin::Pin,
|
||||
sync::Arc,
|
||||
@ -19,8 +20,6 @@
|
||||
|
||||
use tokio::time::{sleep, Duration, Instant, Sleep};
|
||||
|
||||
use slotmap::SlotMap;
|
||||
|
||||
use anyhow::Error;
|
||||
|
||||
pub use helix_core::diagnostic::Severity;
|
||||
@ -63,6 +62,9 @@ pub struct Config {
|
||||
/// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
|
||||
#[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
|
||||
pub idle_timeout: Duration,
|
||||
pub completion_trigger_len: u8,
|
||||
/// Whether to display infoboxes. Defaults to true.
|
||||
pub auto_info: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
|
||||
@ -92,14 +94,29 @@ fn default() -> Self {
|
||||
auto_pairs: true,
|
||||
auto_completion: true,
|
||||
idle_timeout: Duration::from_millis(400),
|
||||
completion_trigger_len: 2,
|
||||
auto_info: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct Motion(pub Box<dyn Fn(&mut Editor)>);
|
||||
impl Motion {
|
||||
pub fn run(&self, e: &mut Editor) {
|
||||
(self.0)(e)
|
||||
}
|
||||
}
|
||||
impl std::fmt::Debug for Motion {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("motion")
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Editor {
|
||||
pub tree: Tree,
|
||||
pub documents: SlotMap<DocumentId, Document>,
|
||||
pub next_document_id: usize,
|
||||
pub documents: BTreeMap<DocumentId, Document>,
|
||||
pub count: Option<std::num::NonZeroUsize>,
|
||||
pub selected_register: Option<char>,
|
||||
pub registers: Registers,
|
||||
@ -124,6 +141,7 @@ pub struct Editor {
|
||||
pub config: Config,
|
||||
|
||||
pub idle_timer: Pin<Box<Sleep>>,
|
||||
pub last_motion: Option<Motion>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
@ -148,7 +166,8 @@ pub fn new(
|
||||
|
||||
Self {
|
||||
tree: Tree::new(area),
|
||||
documents: SlotMap::with_key(),
|
||||
next_document_id: 0,
|
||||
documents: BTreeMap::new(),
|
||||
count: None,
|
||||
selected_register: None,
|
||||
theme: themes.default(),
|
||||
@ -166,6 +185,7 @@ pub fn new(
|
||||
clipboard_provider: get_clipboard_provider(),
|
||||
status_msg: None,
|
||||
idle_timer: Box::pin(sleep(config.idle_timeout)),
|
||||
last_motion: None,
|
||||
config,
|
||||
}
|
||||
}
|
||||
@ -221,7 +241,7 @@ pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> {
|
||||
|
||||
fn _refresh(&mut self) {
|
||||
for (view, _) in self.tree.views_mut() {
|
||||
let doc = &self.documents[view.doc];
|
||||
let doc = &self.documents[&view.doc];
|
||||
view.ensure_cursor_in_view(doc, self.config.scrolloff)
|
||||
}
|
||||
}
|
||||
@ -230,22 +250,38 @@ pub fn switch(&mut self, id: DocumentId, action: Action) {
|
||||
use crate::tree::Layout;
|
||||
use helix_core::Selection;
|
||||
|
||||
if !self.documents.contains_key(id) {
|
||||
if !self.documents.contains_key(&id) {
|
||||
log::error!("cannot switch to document that does not exist (anymore)");
|
||||
return;
|
||||
}
|
||||
|
||||
match action {
|
||||
Action::Replace => {
|
||||
let view = view!(self);
|
||||
let jump = (
|
||||
view.doc,
|
||||
self.documents[view.doc].selection(view.id).clone(),
|
||||
);
|
||||
|
||||
let (view, doc) = current_ref!(self);
|
||||
// If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
|
||||
// Boolean value is determined before the call to `view_mut` because the operation requires a borrow
|
||||
// of `self.tree`, which is mutably borrowed when `view_mut` is called.
|
||||
let remove_empty_scratch = !doc.is_modified()
|
||||
// If the buffer has no path and is not modified, it is an empty scratch buffer.
|
||||
&& doc.path().is_none()
|
||||
// If the buffer we are changing to is not this buffer
|
||||
&& id != doc.id
|
||||
// Ensure the buffer is not displayed in any other splits.
|
||||
&& !self
|
||||
.tree
|
||||
.traverse()
|
||||
.any(|(_, v)| v.doc == doc.id && v.id != view.id);
|
||||
let view = view_mut!(self);
|
||||
if remove_empty_scratch {
|
||||
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
|
||||
// borrow, invalidating direct access to `doc.id`.
|
||||
let id = doc.id;
|
||||
self.documents.remove(&id);
|
||||
} else {
|
||||
let jump = (view.doc, doc.selection(view.id).clone());
|
||||
view.jumps.push(jump);
|
||||
view.last_accessed_doc = Some(view.doc);
|
||||
}
|
||||
view.doc = id;
|
||||
view.offset = Position::default();
|
||||
|
||||
@ -272,14 +308,14 @@ pub fn switch(&mut self, id: DocumentId, action: Action) {
|
||||
let view = View::new(id);
|
||||
let view_id = self.tree.split(view, Layout::Horizontal);
|
||||
// initialize selection for view
|
||||
let doc = &mut self.documents[id];
|
||||
let doc = self.documents.get_mut(&id).unwrap();
|
||||
doc.selections.insert(view_id, Selection::point(0));
|
||||
}
|
||||
Action::VerticalSplit => {
|
||||
let view = View::new(id);
|
||||
let view_id = self.tree.split(view, Layout::Vertical);
|
||||
// initialize selection for view
|
||||
let doc = &mut self.documents[id];
|
||||
let doc = self.documents.get_mut(&id).unwrap();
|
||||
doc.selections.insert(view_id, Selection::point(0));
|
||||
}
|
||||
}
|
||||
@ -288,9 +324,11 @@ pub fn switch(&mut self, id: DocumentId, action: Action) {
|
||||
}
|
||||
|
||||
pub fn new_file(&mut self, action: Action) -> DocumentId {
|
||||
let doc = Document::default();
|
||||
let id = self.documents.insert(doc);
|
||||
self.documents[id].id = id;
|
||||
let id = DocumentId(self.next_document_id);
|
||||
self.next_document_id += 1;
|
||||
let mut doc = Document::default();
|
||||
doc.id = id;
|
||||
self.documents.insert(id, doc);
|
||||
self.switch(id, action);
|
||||
id
|
||||
}
|
||||
@ -313,7 +351,11 @@ pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Erro
|
||||
self.language_servers
|
||||
.get(language)
|
||||
.map_err(|e| {
|
||||
log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
|
||||
log::error!(
|
||||
"Failed to initialize the LSP for `{}` {{ {} }}",
|
||||
language.scope(),
|
||||
e
|
||||
)
|
||||
})
|
||||
.ok()
|
||||
});
|
||||
@ -336,8 +378,10 @@ pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Erro
|
||||
doc.set_language_server(Some(language_server));
|
||||
}
|
||||
|
||||
let id = self.documents.insert(doc);
|
||||
self.documents[id].id = id;
|
||||
let id = DocumentId(self.next_document_id);
|
||||
self.next_document_id += 1;
|
||||
doc.id = id;
|
||||
self.documents.insert(id, doc);
|
||||
id
|
||||
};
|
||||
|
||||
@ -348,16 +392,20 @@ pub fn open(&mut self, path: PathBuf, action: Action) -> Result<DocumentId, Erro
|
||||
pub fn close(&mut self, id: ViewId, close_buffer: bool) {
|
||||
let view = self.tree.get(self.tree.focus);
|
||||
// remove selection
|
||||
self.documents[view.doc].selections.remove(&id);
|
||||
self.documents
|
||||
.get_mut(&view.doc)
|
||||
.unwrap()
|
||||
.selections
|
||||
.remove(&id);
|
||||
|
||||
if close_buffer {
|
||||
// get around borrowck issues
|
||||
let doc = &self.documents[view.doc];
|
||||
let doc = &self.documents[&view.doc];
|
||||
|
||||
if let Some(language_server) = doc.language_server() {
|
||||
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
|
||||
}
|
||||
self.documents.remove(view.doc);
|
||||
self.documents.remove(&view.doc);
|
||||
}
|
||||
|
||||
self.tree.remove(id);
|
||||
@ -374,24 +422,40 @@ pub fn focus_next(&mut self) {
|
||||
self.tree.focus_next();
|
||||
}
|
||||
|
||||
pub fn focus_right(&mut self) {
|
||||
self.tree.focus_direction(tree::Direction::Right);
|
||||
}
|
||||
|
||||
pub fn focus_left(&mut self) {
|
||||
self.tree.focus_direction(tree::Direction::Left);
|
||||
}
|
||||
|
||||
pub fn focus_up(&mut self) {
|
||||
self.tree.focus_direction(tree::Direction::Up);
|
||||
}
|
||||
|
||||
pub fn focus_down(&mut self) {
|
||||
self.tree.focus_direction(tree::Direction::Down);
|
||||
}
|
||||
|
||||
pub fn should_close(&self) -> bool {
|
||||
self.tree.is_empty()
|
||||
}
|
||||
|
||||
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
|
||||
let view = self.tree.get_mut(id);
|
||||
let doc = &self.documents[view.doc];
|
||||
let doc = &self.documents[&view.doc];
|
||||
view.ensure_cursor_in_view(doc, self.config.scrolloff)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn document(&self, id: DocumentId) -> Option<&Document> {
|
||||
self.documents.get(id)
|
||||
self.documents.get(&id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
|
||||
self.documents.get_mut(id)
|
||||
self.documents.get_mut(&id)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@ -416,7 +480,7 @@ pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut D
|
||||
|
||||
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
|
||||
let view = view!(self);
|
||||
let doc = &self.documents[view.doc];
|
||||
let doc = &self.documents[&view.doc];
|
||||
let cursor = doc
|
||||
.selection(view.id)
|
||||
.primary()
|
||||
|
@ -1,6 +1,6 @@
|
||||
use crate::input::KeyEvent;
|
||||
use helix_core::unicode::width::UnicodeWidthStr;
|
||||
use std::fmt::Write;
|
||||
use std::{collections::BTreeSet, fmt::Write};
|
||||
|
||||
#[derive(Debug)]
|
||||
/// Info box used in editor. Rendering logic will be in other crate.
|
||||
@ -16,7 +16,7 @@ pub struct Info {
|
||||
}
|
||||
|
||||
impl Info {
|
||||
pub fn new(title: &str, body: Vec<(&str, Vec<KeyEvent>)>) -> Info {
|
||||
pub fn new(title: &str, body: Vec<(&str, BTreeSet<KeyEvent>)>) -> Info {
|
||||
let body = body
|
||||
.into_iter()
|
||||
.map(|(desc, events)| {
|
||||
|
@ -8,7 +8,7 @@
|
||||
|
||||
/// Represents a key event.
|
||||
// We use a newtype here because we want to customize Deserialize and Display.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
|
||||
pub struct KeyEvent {
|
||||
pub code: KeyCode,
|
||||
pub modifiers: KeyModifiers,
|
||||
|
@ -54,7 +54,7 @@ fn from(val: crossterm::event::KeyModifiers) -> Self {
|
||||
}
|
||||
|
||||
/// Represents a key.
|
||||
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
|
||||
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
|
||||
pub enum KeyCode {
|
||||
/// Backspace key.
|
||||
|
@ -12,8 +12,10 @@
|
||||
pub mod tree;
|
||||
pub mod view;
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
|
||||
pub struct DocumentId(usize);
|
||||
|
||||
slotmap::new_key_type! {
|
||||
pub struct DocumentId;
|
||||
pub struct ViewId;
|
||||
}
|
||||
|
||||
|
@ -13,7 +13,8 @@
|
||||
macro_rules! current {
|
||||
( $( $editor:ident ).+ ) => {{
|
||||
let view = $crate::view_mut!( $( $editor ).+ );
|
||||
let doc = &mut $( $editor ).+ .documents[view.doc];
|
||||
let id = view.doc;
|
||||
let doc = $( $editor ).+ .documents.get_mut(&id).unwrap();
|
||||
(view, doc)
|
||||
}};
|
||||
}
|
||||
@ -56,7 +57,7 @@ macro_rules! doc {
|
||||
macro_rules! current_ref {
|
||||
( $( $editor:ident ).+ ) => {{
|
||||
let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
|
||||
let doc = &$( $editor ).+ .documents[view.doc];
|
||||
let doc = &$( $editor ).+ .documents[&view.doc];
|
||||
(view, doc)
|
||||
}};
|
||||
}
|
||||
|
@ -1,6 +1,5 @@
|
||||
use std::{
|
||||
collections::HashMap,
|
||||
convert::TryFrom,
|
||||
path::{Path, PathBuf},
|
||||
};
|
||||
|
||||
|
@ -47,13 +47,21 @@ pub fn view(view: View) -> Self {
|
||||
|
||||
// TODO: screen coord to container + container coordinate helpers
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum Layout {
|
||||
Horizontal,
|
||||
Vertical,
|
||||
// could explore stacked/tabbed
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum Direction {
|
||||
Up,
|
||||
Down,
|
||||
Left,
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Container {
|
||||
layout: Layout,
|
||||
@ -150,7 +158,6 @@ pub fn split(&mut self, view: View, layout: Layout) -> ViewId {
|
||||
} => container,
|
||||
_ => unreachable!(),
|
||||
};
|
||||
|
||||
if container.layout == layout {
|
||||
// insert node after the current item if there is children already
|
||||
let pos = if container.children.is_empty() {
|
||||
@ -393,6 +400,112 @@ pub fn traverse(&self) -> Traverse {
|
||||
Traverse::new(self)
|
||||
}
|
||||
|
||||
// Finds the split in the given direction if it exists
|
||||
pub fn find_split_in_direction(&self, id: ViewId, direction: Direction) -> Option<ViewId> {
|
||||
let parent = self.nodes[id].parent;
|
||||
// Base case, we found the root of the tree
|
||||
if parent == id {
|
||||
return None;
|
||||
}
|
||||
// Parent must always be a container
|
||||
let parent_container = match &self.nodes[parent].content {
|
||||
Content::Container(container) => container,
|
||||
Content::View(_) => unreachable!(),
|
||||
};
|
||||
|
||||
match (direction, parent_container.layout) {
|
||||
(Direction::Up, Layout::Vertical)
|
||||
| (Direction::Left, Layout::Horizontal)
|
||||
| (Direction::Right, Layout::Horizontal)
|
||||
| (Direction::Down, Layout::Vertical) => {
|
||||
// The desired direction of movement is not possible within
|
||||
// the parent container so the search must continue closer to
|
||||
// the root of the split tree.
|
||||
self.find_split_in_direction(parent, direction)
|
||||
}
|
||||
(Direction::Up, Layout::Horizontal)
|
||||
| (Direction::Down, Layout::Horizontal)
|
||||
| (Direction::Left, Layout::Vertical)
|
||||
| (Direction::Right, Layout::Vertical) => {
|
||||
// It's possible to move in the desired direction within
|
||||
// the parent container so an attempt is made to find the
|
||||
// correct child.
|
||||
match self.find_child(id, &parent_container.children, direction) {
|
||||
// Child is found, search is ended
|
||||
Some(id) => Some(id),
|
||||
// A child is not found. This could be because of either two scenarios
|
||||
// 1. Its not possible to move in the desired direction, and search should end
|
||||
// 2. A layout like the following with focus at X and desired direction Right
|
||||
// | _ | x | |
|
||||
// | _ _ _ | |
|
||||
// | _ _ _ | |
|
||||
// The container containing X ends at X so no rightward movement is possible
|
||||
// however there still exists another view/container to the right that hasn't
|
||||
// been explored. Thus another search is done here in the parent container
|
||||
// before concluding it's not possible to move in the desired direction.
|
||||
None => self.find_split_in_direction(parent, direction),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn find_child(&self, id: ViewId, children: &[ViewId], direction: Direction) -> Option<ViewId> {
|
||||
let mut child_id = match direction {
|
||||
// index wise in the child list the Up and Left represents a -1
|
||||
// thus reversed iterator.
|
||||
Direction::Up | Direction::Left => children
|
||||
.iter()
|
||||
.rev()
|
||||
.skip_while(|i| **i != id)
|
||||
.copied()
|
||||
.nth(1)?,
|
||||
// Down and Right => +1 index wise in the child list
|
||||
Direction::Down | Direction::Right => {
|
||||
children.iter().skip_while(|i| **i != id).copied().nth(1)?
|
||||
}
|
||||
};
|
||||
let (current_x, current_y) = match &self.nodes[self.focus].content {
|
||||
Content::View(current_view) => (current_view.area.left(), current_view.area.top()),
|
||||
Content::Container(_) => unreachable!(),
|
||||
};
|
||||
|
||||
// If the child is a container the search finds the closest container child
|
||||
// visually based on screen location.
|
||||
while let Content::Container(container) = &self.nodes[child_id].content {
|
||||
match (direction, container.layout) {
|
||||
(_, Layout::Vertical) => {
|
||||
// find closest split based on x because y is irrelevant
|
||||
// in a vertical container (and already correct based on previous search)
|
||||
child_id = *container.children.iter().min_by_key(|id| {
|
||||
let x = match &self.nodes[**id].content {
|
||||
Content::View(view) => view.inner_area().left(),
|
||||
Content::Container(container) => container.area.left(),
|
||||
};
|
||||
(current_x as i16 - x as i16).abs()
|
||||
})?;
|
||||
}
|
||||
(_, Layout::Horizontal) => {
|
||||
// find closest split based on y because x is irrelevant
|
||||
// in a horizontal container (and already correct based on previous search)
|
||||
child_id = *container.children.iter().min_by_key(|id| {
|
||||
let y = match &self.nodes[**id].content {
|
||||
Content::View(view) => view.inner_area().top(),
|
||||
Content::Container(container) => container.area.top(),
|
||||
};
|
||||
(current_y as i16 - y as i16).abs()
|
||||
})?;
|
||||
}
|
||||
}
|
||||
}
|
||||
Some(child_id)
|
||||
}
|
||||
|
||||
pub fn focus_direction(&mut self, direction: Direction) {
|
||||
if let Some(id) = self.find_split_in_direction(self.focus, direction) {
|
||||
self.focus = id;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focus_next(&mut self) {
|
||||
// This function is very dumb, but that's because we don't store any parent links.
|
||||
// (we'd be able to go parent.next_sibling() recursively until we find something)
|
||||
@ -420,13 +533,12 @@ pub fn focus_next(&mut self) {
|
||||
// if found = container -> found = first child
|
||||
// }
|
||||
|
||||
let iter = self.traverse();
|
||||
|
||||
let mut iter = iter.skip_while(|&(key, _view)| key != self.focus);
|
||||
iter.next(); // take the focused value
|
||||
|
||||
if let Some((key, _)) = iter.next() {
|
||||
self.focus = key;
|
||||
let mut views = self
|
||||
.traverse()
|
||||
.skip_while(|&(id, _view)| id != self.focus)
|
||||
.skip(1); // Skip focused value
|
||||
if let Some((id, _)) = views.next() {
|
||||
self.focus = id;
|
||||
} else {
|
||||
// extremely crude, take the first item again
|
||||
let (key, _) = self.traverse().next().unwrap();
|
||||
@ -472,3 +584,64 @@ fn next(&mut self) -> Option<Self::Item> {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use super::*;
|
||||
use crate::DocumentId;
|
||||
|
||||
#[test]
|
||||
fn find_split_in_direction() {
|
||||
let mut tree = Tree::new(Rect {
|
||||
x: 0,
|
||||
y: 0,
|
||||
width: 180,
|
||||
height: 80,
|
||||
});
|
||||
let mut view = View::new(DocumentId::default());
|
||||
view.area = Rect::new(0, 0, 180, 80);
|
||||
tree.insert(view);
|
||||
|
||||
let l0 = tree.focus;
|
||||
let view = View::new(DocumentId::default());
|
||||
tree.split(view, Layout::Vertical);
|
||||
let r0 = tree.focus;
|
||||
|
||||
tree.focus = l0;
|
||||
let view = View::new(DocumentId::default());
|
||||
tree.split(view, Layout::Horizontal);
|
||||
let l1 = tree.focus;
|
||||
|
||||
tree.focus = l0;
|
||||
let view = View::new(DocumentId::default());
|
||||
tree.split(view, Layout::Vertical);
|
||||
let l2 = tree.focus;
|
||||
|
||||
// Tree in test
|
||||
// | L0 | L2 | |
|
||||
// | L1 | R0 |
|
||||
tree.focus = l2;
|
||||
assert_eq!(Some(l0), tree.find_split_in_direction(l2, Direction::Left));
|
||||
assert_eq!(Some(l1), tree.find_split_in_direction(l2, Direction::Down));
|
||||
assert_eq!(Some(r0), tree.find_split_in_direction(l2, Direction::Right));
|
||||
assert_eq!(None, tree.find_split_in_direction(l2, Direction::Up));
|
||||
|
||||
tree.focus = l1;
|
||||
assert_eq!(None, tree.find_split_in_direction(l1, Direction::Left));
|
||||
assert_eq!(None, tree.find_split_in_direction(l1, Direction::Down));
|
||||
assert_eq!(Some(r0), tree.find_split_in_direction(l1, Direction::Right));
|
||||
assert_eq!(Some(l0), tree.find_split_in_direction(l1, Direction::Up));
|
||||
|
||||
tree.focus = l0;
|
||||
assert_eq!(None, tree.find_split_in_direction(l0, Direction::Left));
|
||||
assert_eq!(Some(l1), tree.find_split_in_direction(l0, Direction::Down));
|
||||
assert_eq!(Some(l2), tree.find_split_in_direction(l0, Direction::Right));
|
||||
assert_eq!(None, tree.find_split_in_direction(l0, Direction::Up));
|
||||
|
||||
tree.focus = r0;
|
||||
assert_eq!(Some(l2), tree.find_split_in_direction(r0, Direction::Left));
|
||||
assert_eq!(None, tree.find_split_in_direction(r0, Direction::Down));
|
||||
assert_eq!(None, tree.find_split_in_direction(r0, Direction::Right));
|
||||
assert_eq!(None, tree.find_split_in_direction(r0, Direction::Up));
|
||||
}
|
||||
}
|
||||
|
@ -2,10 +2,9 @@
|
||||
|
||||
use crate::{graphics::Rect, Document, DocumentId, ViewId};
|
||||
use helix_core::{
|
||||
coords_at_pos,
|
||||
graphemes::{grapheme_width, RopeGraphemes},
|
||||
line_ending::line_end_char_index,
|
||||
Position, RopeSlice, Selection,
|
||||
visual_coords_at_pos, Position, RopeSlice, Selection,
|
||||
};
|
||||
|
||||
type Jump = (DocumentId, Selection);
|
||||
@ -91,7 +90,10 @@ pub fn ensure_cursor_in_view(&mut self, doc: &Document, scrolloff: usize) {
|
||||
.selection(self.id)
|
||||
.primary()
|
||||
.cursor(doc.text().slice(..));
|
||||
let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor);
|
||||
|
||||
let Position { col, row: line } =
|
||||
visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
|
||||
|
||||
let inner_area = self.inner_area();
|
||||
let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);
|
||||
|
||||
|
@ -312,7 +312,7 @@ injection-regex = "php"
|
||||
file-types = ["php"]
|
||||
roots = []
|
||||
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "latex"
|
||||
@ -458,3 +458,12 @@ file-types = ["scm"]
|
||||
roots = []
|
||||
comment-token = ";"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[[language]]
|
||||
name = "cmake"
|
||||
scope = "source.cmake"
|
||||
file-types = ["cmake", "CMakeLists.txt"]
|
||||
roots = []
|
||||
comment-token = "#"
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
language-server = { command = "cmake-language-server" }
|
||||
|
@ -7,7 +7,7 @@
|
||||
|
||||
(command_name) @function
|
||||
|
||||
(variable_name) @property
|
||||
(variable_name) @variable.other.member
|
||||
|
||||
[
|
||||
"case"
|
||||
@ -31,7 +31,7 @@
|
||||
|
||||
(function_definition name: (word) @function)
|
||||
|
||||
(file_descriptor) @number
|
||||
(file_descriptor) @constant.numeric.integer
|
||||
|
||||
[
|
||||
(command_substitution)
|
||||
|
@ -20,16 +20,16 @@
|
||||
] @type.builtin
|
||||
|
||||
;; Enum
|
||||
(enum_member_declaration (identifier) @variable.property)
|
||||
(enum_member_declaration (identifier) @variable.other.member)
|
||||
|
||||
;; Literals
|
||||
[
|
||||
(real_literal)
|
||||
(integer_literal)
|
||||
] @number
|
||||
] @constant.numeric.integer
|
||||
|
||||
(character_literal) @constant.character
|
||||
[
|
||||
(character_literal)
|
||||
(string_literal)
|
||||
(verbatim_string_literal)
|
||||
(interpolated_string_text)
|
||||
@ -40,8 +40,8 @@
|
||||
"$@\""
|
||||
] @string
|
||||
|
||||
(boolean_literal) @constant.builtin.boolean
|
||||
[
|
||||
(boolean_literal)
|
||||
(null_literal)
|
||||
(void_keyword)
|
||||
] @constant.builtin
|
||||
@ -98,7 +98,7 @@
|
||||
;; Keywords
|
||||
(modifier) @keyword
|
||||
(this_expression) @keyword
|
||||
(escape_sequence) @keyword
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
[
|
||||
"as"
|
||||
|
@ -60,8 +60,8 @@
|
||||
(system_lib_string) @string
|
||||
|
||||
(null) @constant
|
||||
(number_literal) @number
|
||||
(char_literal) @string
|
||||
(number_literal) @constant.numeric.integer
|
||||
(char_literal) @constant.character
|
||||
|
||||
(call_expression
|
||||
function: (identifier) @function)
|
||||
@ -73,7 +73,7 @@
|
||||
(preproc_function_def
|
||||
name: (identifier) @function.special)
|
||||
|
||||
(field_identifier) @property
|
||||
(field_identifier) @variable.other.member
|
||||
(statement_identifier) @label
|
||||
(type_identifier) @type
|
||||
(primitive_type) @type
|
||||
|
97
runtime/queries/cmake/highlights.scm
Normal file
97
runtime/queries/cmake/highlights.scm
Normal file
@ -0,0 +1,97 @@
|
||||
[
|
||||
(quoted_argument)
|
||||
(bracket_argument)
|
||||
] @string
|
||||
|
||||
(variable) @variable
|
||||
|
||||
[
|
||||
(bracket_comment)
|
||||
(line_comment)
|
||||
] @comment
|
||||
|
||||
(normal_command (identifier) @function)
|
||||
|
||||
["ENV" "CACHE"] @string.special.symbol
|
||||
["$" "{" "}" "<" ">"] @punctuation
|
||||
["(" ")"] @punctuation.bracket
|
||||
|
||||
[
|
||||
(function)
|
||||
(endfunction)
|
||||
(macro)
|
||||
(endmacro)
|
||||
] @keyword.function
|
||||
|
||||
[
|
||||
(if)
|
||||
(elseif)
|
||||
(else)
|
||||
(endif)
|
||||
] @keyword.control.conditional
|
||||
|
||||
[
|
||||
(foreach)
|
||||
(endforeach)
|
||||
(while)
|
||||
(endwhile)
|
||||
] @keyword.control.repeat
|
||||
|
||||
(function_command
|
||||
(function)
|
||||
. (argument) @function
|
||||
(argument)* @variable.parameter
|
||||
)
|
||||
|
||||
(macro_command
|
||||
(macro)
|
||||
. (argument) @function.macro
|
||||
(argument)* @variable.parameter
|
||||
)
|
||||
|
||||
(normal_command
|
||||
(identifier) @function.builtin
|
||||
. (argument) @variable
|
||||
(#match? @function.builtin "^(?i)(set)$"))
|
||||
|
||||
(normal_command
|
||||
(identifier) @function.builtin
|
||||
. (argument)
|
||||
(argument) @constant
|
||||
(#match? @constant "^(?:PARENT_SCOPE|CACHE)$")
|
||||
(#match? @function.builtin "^(?i)(unset)$"))
|
||||
|
||||
(normal_command
|
||||
(identifier) @function.builtin
|
||||
. (argument)
|
||||
. (argument)
|
||||
(argument) @constant
|
||||
(#match? @constant "^(?:PARENT_SCOPE|CACHE|FORCE)$")
|
||||
(#match? @function.builtin "^(?i)(set)$")
|
||||
)
|
||||
|
||||
((argument) @constant.builtin.boolean
|
||||
(#match? @constant.builtin.boolean "^(?i)(?:1|on|yes|true|y|0|off|no|false|n|ignore|notfound|.*-notfound)$")
|
||||
)
|
||||
|
||||
(if_command
|
||||
(if)
|
||||
(argument) @operator
|
||||
(#match? @operator "^(?:NOT|AND|OR|COMMAND|POLICY|TARGET|TEST|DEFINED|IN_LIST|EXISTS|IS_NEWER_THAN|IS_DIRECTORY|IS_SYMLINK|IS_ABSOLUTE|MATCHES|LESS|GREATER|EQUAL|LESS_EQUAL|GREATER_EQUAL|STRLESS|STRGREATER|STREQUAL|STRLESS_EQUAL|STRGREATER_EQUAL|VERSION_LESS|VERSION_GREATER|VERSION_EQUAL|VERSION_LESS_EQUAL|VERSION_GREATER_EQUAL)$")
|
||||
)
|
||||
|
||||
(normal_command
|
||||
(identifier) @function.builtin
|
||||
. (argument)
|
||||
(argument) @constant
|
||||
(#match? @constant "^(?:ALL|COMMAND|DEPENDS|BYPRODUCTS|WORKING_DIRECTORY|COMMENT|JOB_POOL|VERBATIM|USES_TERMINAL|COMMAND_EXPAND_LISTS|SOURCES)$")
|
||||
(#match? @function.builtin "^(?i)(add_custom_target)$")
|
||||
)
|
||||
|
||||
(normal_command
|
||||
(identifier) @function.builtin
|
||||
(argument) @constant
|
||||
(#match? @constant "^(?:OUTPUT|COMMAND|MAIN_DEPENDENCY|DEPENDS|BYPRODUCTS|IMPLICIT_DEPENDS|WORKING_DIRECTORY|COMMENT|DEPFILE|JOB_POOL|VERBATIM|APPEND|USES_TERMINAL|COMMAND_EXPAND_LISTS)$")
|
||||
(#match? @function.builtin "^(?i)(add_custom_command)$")
|
||||
)
|
||||
|
@ -3,7 +3,7 @@
|
||||
; Functions
|
||||
|
||||
(call_expression
|
||||
function: (scoped_identifier
|
||||
function: (qualified_identifier
|
||||
name: (identifier) @function))
|
||||
|
||||
(template_function
|
||||
@ -13,15 +13,14 @@
|
||||
name: (field_identifier) @function)
|
||||
|
||||
(template_function
|
||||
name: (scoped_identifier
|
||||
name: (identifier) @function)
|
||||
|
||||
(function_declarator
|
||||
declarator: (qualified_identifier
|
||||
name: (identifier) @function))
|
||||
|
||||
(function_declarator
|
||||
declarator: (scoped_identifier
|
||||
name: (identifier) @function))
|
||||
|
||||
(function_declarator
|
||||
declarator: (scoped_identifier
|
||||
declarator: (qualified_identifier
|
||||
name: (identifier) @function))
|
||||
|
||||
(function_declarator
|
||||
|
@ -26,11 +26,11 @@
|
||||
(pseudo_element_selector (tag_name) @attribute)
|
||||
(pseudo_class_selector (class_name) @attribute)
|
||||
|
||||
(class_name) @property
|
||||
(id_name) @property
|
||||
(namespace_name) @property
|
||||
(property_name) @property
|
||||
(feature_name) @property
|
||||
(class_name) @variable.other.member
|
||||
(id_name) @variable.other.member
|
||||
(namespace_name) @variable.other.member
|
||||
(property_name) @variable.other.member
|
||||
(feature_name) @variable.other.member
|
||||
|
||||
(attribute_name) @attribute
|
||||
|
||||
@ -55,8 +55,8 @@
|
||||
(string_value) @string
|
||||
(color_value) @string.special
|
||||
|
||||
(integer_value) @number
|
||||
(float_value) @number
|
||||
(integer_value) @constant.numeric.integer
|
||||
(float_value) @constant.numeric.float
|
||||
(unit) @type
|
||||
|
||||
"#" @punctuation.delimiter
|
||||
|
@ -1,125 +1,210 @@
|
||||
["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
|
||||
; The following code originates mostly from
|
||||
; https://github.com/elixir-lang/tree-sitter-elixir, with minor edits to
|
||||
; align the captures with helix. The following should be considered
|
||||
; Copyright 2021 The Elixir Team
|
||||
;
|
||||
; Licensed under the Apache License, Version 2.0 (the "License");
|
||||
; you may not use this file except in compliance with the License.
|
||||
; You may obtain a copy of the License at
|
||||
;
|
||||
; https://www.apache.org/licenses/LICENSE-2.0
|
||||
;
|
||||
; Unless required by applicable law or agreed to in writing, software
|
||||
; distributed under the License is distributed on an "AS IS" BASIS,
|
||||
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
; See the License for the specific language governing permissions and
|
||||
; limitations under the License.
|
||||
|
||||
[(true) (false) (nil)] @constant.builtin
|
||||
; Reserved keywords
|
||||
|
||||
(keyword
|
||||
[(keyword_literal)
|
||||
":"] @tag)
|
||||
["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
|
||||
|
||||
(keyword
|
||||
(keyword_string
|
||||
[(string_start)
|
||||
(string_content)
|
||||
(string_end)] @tag))
|
||||
; Operators
|
||||
|
||||
[(atom_literal)
|
||||
(atom_start)
|
||||
(atom_content)
|
||||
(atom_end)] @tag
|
||||
|
||||
[(comment)
|
||||
(unused_identifier)] @comment
|
||||
|
||||
(escape_sequence) @escape
|
||||
|
||||
(call function: (function_identifier) @keyword
|
||||
(#match? @keyword "^(defmodule|defexception|defp|def|with|case|cond|raise|import|require|use|defmacrop|defmacro|defguardp|defguard|defdelegate|defstruct|alias|defimpl|defprotocol|defoverridable|receive|if|for|try|throw|unless|reraise|super|quote|unquote|unquote_splicing)$"))
|
||||
|
||||
(call function: (function_identifier) @keyword
|
||||
[(call
|
||||
function: (function_identifier) @function
|
||||
; * doc string
|
||||
(unary_operator
|
||||
operator: "@" @comment.block.documentation
|
||||
operand: (call
|
||||
target: (identifier) @comment.block.documentation.__attribute__
|
||||
(arguments
|
||||
[(identifier) @variable.parameter
|
||||
(_ (identifier) @variable.parameter)
|
||||
(_ (_ (identifier) @variable.parameter))
|
||||
(_ (_ (_ (identifier) @variable.parameter)))
|
||||
(_ (_ (_ (_ (identifier) @variable.parameter))))
|
||||
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
|
||||
(binary_op
|
||||
left:
|
||||
[
|
||||
(string) @comment.block.documentation
|
||||
(charlist) @comment.block.documentation
|
||||
(sigil
|
||||
quoted_start: _ @comment.block.documentation
|
||||
quoted_end: _ @comment.block.documentation) @comment.block.documentation
|
||||
(boolean) @comment.block.documentation
|
||||
]))
|
||||
(#match? @comment.block.documentation.__attribute__ "^(moduledoc|typedoc|doc)$"))
|
||||
|
||||
; * module attribute
|
||||
(unary_operator
|
||||
operator: "@" @variable.other.member
|
||||
operand: [
|
||||
(identifier) @variable.other.member
|
||||
(call
|
||||
function: (function_identifier) @function
|
||||
(arguments
|
||||
[(identifier) @variable.parameter
|
||||
(_ (identifier) @variable.parameter)
|
||||
(_ (_ (identifier) @variable.parameter))
|
||||
(_ (_ (_ (identifier) @variable.parameter)))
|
||||
(_ (_ (_ (_ (identifier) @variable.parameter))))
|
||||
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
|
||||
operator: "when")
|
||||
(binary_op
|
||||
left: (identifier) @variable.parameter
|
||||
operator: _ @function
|
||||
right: (identifier) @variable.parameter)]
|
||||
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
|
||||
target: (identifier) @variable.other.member)
|
||||
(boolean) @variable.other.member
|
||||
(nil) @variable.other.member
|
||||
])
|
||||
|
||||
(call (function_identifier) @keyword
|
||||
[(call
|
||||
function: (function_identifier) @function)
|
||||
; * capture operator
|
||||
(unary_operator
|
||||
operator: "&"
|
||||
operand: [
|
||||
(integer) @operator
|
||||
(binary_operator
|
||||
left: [
|
||||
(call target: (dot left: (_) right: (identifier) @function))
|
||||
(identifier) @function
|
||||
(binary_op
|
||||
left:
|
||||
[(call
|
||||
function: (function_identifier) @function)
|
||||
(identifier) @function]
|
||||
operator: "when")]
|
||||
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
|
||||
] operator: "/" right: (integer) @operator)
|
||||
])
|
||||
|
||||
(anonymous_function
|
||||
(stab_expression
|
||||
left: (bare_arguments
|
||||
[(identifier) @variable.parameter
|
||||
(_ (identifier) @variable.parameter)
|
||||
(_ (_ (identifier) @variable.parameter))
|
||||
(_ (_ (_ (identifier) @variable.parameter)))
|
||||
(_ (_ (_ (_ (identifier) @variable.parameter))))
|
||||
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))])))
|
||||
(operator_identifier) @operator
|
||||
|
||||
(unary_op
|
||||
operator: "@"
|
||||
(call (identifier) @attribute
|
||||
(heredoc
|
||||
[(heredoc_start)
|
||||
(heredoc_content)
|
||||
(heredoc_end)] @doc))
|
||||
(#match? @attribute "^(doc|moduledoc)$"))
|
||||
|
||||
(module) @type
|
||||
|
||||
(unary_op
|
||||
operator: "@" @attribute
|
||||
[(call
|
||||
function: (function_identifier) @attribute)
|
||||
(identifier) @attribute])
|
||||
|
||||
(unary_op
|
||||
(unary_operator
|
||||
operator: _ @operator)
|
||||
|
||||
(binary_op
|
||||
(binary_operator
|
||||
operator: _ @operator)
|
||||
|
||||
(heredoc
|
||||
[(heredoc_start)
|
||||
(heredoc_content)
|
||||
(heredoc_end)] @string)
|
||||
(dot
|
||||
operator: _ @operator)
|
||||
|
||||
(string
|
||||
[(string_start)
|
||||
(string_content)
|
||||
(string_end)] @string)
|
||||
(stab_clause
|
||||
operator: _ @operator)
|
||||
|
||||
(sigil_start) @string.special
|
||||
(sigil_content) @string
|
||||
(sigil_end) @string.special
|
||||
; Literals
|
||||
|
||||
(interpolation
|
||||
"#{" @punctuation.special
|
||||
"}" @punctuation.special)
|
||||
(nil) @constant.builtin
|
||||
|
||||
(boolean) @constant.builtin.boolean
|
||||
(integer) @constant.numeric.integer
|
||||
(float) @constant.numeric.float
|
||||
|
||||
(alias) @type
|
||||
|
||||
(call
|
||||
target: (dot
|
||||
left: (atom) @type))
|
||||
|
||||
(char) @constant.character
|
||||
|
||||
; Quoted content
|
||||
|
||||
(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
|
||||
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
[
|
||||
(atom)
|
||||
(quoted_atom)
|
||||
(keyword)
|
||||
(quoted_keyword)
|
||||
] @string.special.symbol
|
||||
|
||||
[
|
||||
(string)
|
||||
(charlist)
|
||||
] @string
|
||||
|
||||
; Note that we explicitly target sigil quoted start/end, so they are not overridden by delimiters
|
||||
|
||||
(sigil
|
||||
(sigil_name) @__name__
|
||||
quoted_start: _ @string
|
||||
quoted_end: _ @string
|
||||
(#match? @__name__ "^[sS]$")) @string
|
||||
|
||||
(sigil
|
||||
(sigil_name) @__name__
|
||||
quoted_start: _ @string.regexp
|
||||
quoted_end: _ @string.regexp
|
||||
(#match? @__name__ "^[rR]$")) @string.regexp
|
||||
|
||||
(sigil
|
||||
(sigil_name) @__name__
|
||||
quoted_start: _ @string.special
|
||||
quoted_end: _ @string.special) @string.special
|
||||
|
||||
; Calls
|
||||
|
||||
; * definition keyword
|
||||
(call
|
||||
target: (identifier) @keyword
|
||||
(#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
|
||||
|
||||
; * kernel or special forms keyword
|
||||
(call
|
||||
target: (identifier) @keyword
|
||||
(#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
|
||||
|
||||
; * function call
|
||||
(call
|
||||
target: [
|
||||
; local
|
||||
(identifier) @function
|
||||
; remote
|
||||
(dot
|
||||
right: (identifier) @function)
|
||||
])
|
||||
|
||||
; * just identifier in function definition
|
||||
(call
|
||||
target: (identifier) @keyword
|
||||
(arguments
|
||||
[
|
||||
(identifier) @function
|
||||
(binary_operator
|
||||
left: (identifier) @function
|
||||
operator: "when")
|
||||
])
|
||||
(#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
|
||||
|
||||
; * pipe into identifier (definition)
|
||||
(call
|
||||
target: (identifier) @keyword
|
||||
(arguments
|
||||
(binary_operator
|
||||
operator: "|>"
|
||||
right: (identifier) @variable))
|
||||
(#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
|
||||
|
||||
; * pipe into identifier (function call)
|
||||
(binary_operator
|
||||
operator: "|>"
|
||||
right: (identifier) @function)
|
||||
|
||||
; Identifiers
|
||||
|
||||
; * special
|
||||
(
|
||||
(identifier) @constant.builtin
|
||||
(#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
|
||||
)
|
||||
|
||||
; * unused
|
||||
(
|
||||
(identifier) @comment
|
||||
(#match? @comment "^_")
|
||||
)
|
||||
|
||||
; * regular
|
||||
(identifier) @variable
|
||||
|
||||
; Comment
|
||||
|
||||
(comment) @comment
|
||||
|
||||
; Punctuation
|
||||
|
||||
[
|
||||
"%"
|
||||
] @punctuation
|
||||
|
||||
[
|
||||
","
|
||||
"->"
|
||||
"."
|
||||
";"
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
@ -133,6 +218,4 @@
|
||||
">>"
|
||||
] @punctuation.bracket
|
||||
|
||||
(special_identifier) @function.special
|
||||
|
||||
(ERROR) @warning
|
||||
|
@ -25,7 +25,7 @@
|
||||
(variadic_parameter_declaration (identifier) @variable.parameter)
|
||||
|
||||
(type_identifier) @type
|
||||
(field_identifier) @property
|
||||
(field_identifier) @variable.other.member
|
||||
(identifier) @variable
|
||||
(package_identifier) @variable
|
||||
|
||||
@ -130,13 +130,13 @@
|
||||
(rune_literal)
|
||||
] @string
|
||||
|
||||
(escape_sequence) @escape
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
[
|
||||
(int_literal)
|
||||
(float_literal)
|
||||
(imaginary_literal)
|
||||
] @number
|
||||
] @constant.numeric.integer
|
||||
|
||||
[
|
||||
(true)
|
||||
|
21
runtime/queries/go/textobjects.scm
Normal file
21
runtime/queries/go/textobjects.scm
Normal file
@ -0,0 +1,21 @@
|
||||
(function_declaration
|
||||
body: (block)? @function.inside) @function.around
|
||||
|
||||
(func_literal
|
||||
(_)? @function.inside) @function.around
|
||||
|
||||
(method_declaration
|
||||
body: (block)? @function.inside) @function.around
|
||||
|
||||
;; struct and interface declaration as class textobject?
|
||||
(type_declaration
|
||||
(type_spec (type_identifier) (struct_type (field_declaration_list (_)?) @class.inside))) @class.around
|
||||
|
||||
(type_declaration
|
||||
(type_spec (type_identifier) (interface_type (method_spec_list (_)?) @class.inside))) @class.around
|
||||
|
||||
(parameter_list
|
||||
(_) @parameter.inside)
|
||||
|
||||
(argument_list
|
||||
(_) @parameter.inside)
|
@ -13,9 +13,9 @@
|
||||
(constraint class: (class_name (type)) @class)
|
||||
(class (class_head class: (class_name (type)) @class))
|
||||
(instance (instance_head class: (class_name (type)) @class))
|
||||
(integer) @number
|
||||
(exp_literal (float)) @number
|
||||
(char) @literal
|
||||
(integer) @constant.numeric.integer
|
||||
(exp_literal (float)) @constant.numeric.float
|
||||
(char) @constant.character
|
||||
(con_unit) @literal
|
||||
(con_list) @literal
|
||||
(tycon_arrow) @operator
|
||||
|
@ -59,14 +59,15 @@
|
||||
(hex_integer_literal)
|
||||
(decimal_integer_literal)
|
||||
(octal_integer_literal)
|
||||
(decimal_floating_point_literal)
|
||||
(hex_floating_point_literal)
|
||||
] @number
|
||||
] @constant.numeric.integer
|
||||
|
||||
[
|
||||
(character_literal)
|
||||
(string_literal)
|
||||
] @string
|
||||
(decimal_floating_point_literal)
|
||||
(hex_floating_point_literal)
|
||||
] @constant.numeric.float
|
||||
|
||||
(character_literal) @constant.character
|
||||
(string_literal) @string
|
||||
|
||||
[
|
||||
(true)
|
||||
|
@ -65,7 +65,7 @@
|
||||
; Properties
|
||||
;-----------
|
||||
|
||||
(property_identifier) @property
|
||||
(property_identifier) @variable.other.member
|
||||
|
||||
; Literals
|
||||
;---------
|
||||
@ -88,7 +88,7 @@
|
||||
] @string
|
||||
|
||||
(regex) @string.regexp
|
||||
(number) @number
|
||||
(number) @constant.numeric.integer
|
||||
|
||||
; Tokens
|
||||
;-------
|
||||
|
@ -1,9 +1,20 @@
|
||||
[
|
||||
(true)
|
||||
(false)
|
||||
] @constant.builtin.boolean
|
||||
(null) @constant.builtin
|
||||
(number) @constant.numeric
|
||||
(pair
|
||||
key: (_) @keyword)
|
||||
|
||||
(string) @string
|
||||
(escape_sequence) @constant.character.escape
|
||||
(ERROR) @error
|
||||
|
||||
(object
|
||||
"{" @escape
|
||||
(_)
|
||||
"}" @escape)
|
||||
"," @punctuation.delimiter
|
||||
[
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
@ -15,7 +15,7 @@
|
||||
|
||||
(field_expression
|
||||
(identifier)
|
||||
(identifier) @field .)
|
||||
(identifier) @variable.other.member .)
|
||||
|
||||
(function_definition
|
||||
name: (identifier) @function)
|
||||
@ -80,14 +80,14 @@
|
||||
(struct_definition
|
||||
name: (identifier) @type)
|
||||
|
||||
(number) @number
|
||||
(number) @constant.numeric.integer
|
||||
(range_expression
|
||||
(identifier) @number
|
||||
(eq? @number "end"))
|
||||
(identifier) @constant.numeric.integer
|
||||
(eq? @constant.numeric.integer "end"))
|
||||
(range_expression
|
||||
(_
|
||||
(identifier) @number
|
||||
(eq? @number "end")))
|
||||
(identifier) @constant.numeric.integer
|
||||
(eq? @constant.numeric.integer "end")))
|
||||
(coefficient_expression
|
||||
(number)
|
||||
(identifier) @constant.builtin)
|
||||
|
@ -7,9 +7,9 @@
|
||||
(date)
|
||||
(interval)
|
||||
(quantity)
|
||||
] @number
|
||||
] @constant.numeric.integer
|
||||
|
||||
((account) @field)
|
||||
((account) @variable.other.member)
|
||||
((commodity) @text.literal)
|
||||
|
||||
"include" @include
|
||||
|
@ -150,14 +150,14 @@
|
||||
(table ["{" "}"] @constructor)
|
||||
(comment) @comment
|
||||
(string) @string
|
||||
(number) @number
|
||||
(number) @constant.numeric.integer
|
||||
(label_statement) @label
|
||||
; A bit of a tricky one, this will only match field names
|
||||
(field . (identifier) @property (_))
|
||||
(field . (identifier) @variable.other.member (_))
|
||||
(shebang) @comment
|
||||
|
||||
;; Property
|
||||
(property_identifier) @property
|
||||
(property_identifier) @variable.other.member
|
||||
|
||||
;; Variable
|
||||
(identifier) @variable
|
||||
|
@ -33,16 +33,14 @@
|
||||
|
||||
(uri) @string.special.uri
|
||||
|
||||
[
|
||||
(integer)
|
||||
(float)
|
||||
] @number
|
||||
(integer) @constant.numeric.integer
|
||||
(float) @constant.numeric.float
|
||||
|
||||
(interpolation
|
||||
"${" @punctuation.special
|
||||
"}" @punctuation.special) @embedded
|
||||
|
||||
(escape_sequence) @escape
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
(function
|
||||
universal: (identifier) @variable.parameter
|
||||
@ -66,8 +64,8 @@
|
||||
(binary
|
||||
operator: _ @operator)
|
||||
|
||||
(attr_identifier) @property
|
||||
(inherit attrs: (attrs_inherited (identifier) @property) )
|
||||
(attr_identifier) @variable.other.member
|
||||
(inherit attrs: (attrs_inherited (identifier) @variable.other.member) )
|
||||
|
||||
[
|
||||
";"
|
||||
|
@ -51,14 +51,14 @@
|
||||
; Properties
|
||||
;-----------
|
||||
|
||||
[(label_name) (field_name) (instance_variable_name)] @property
|
||||
[(label_name) (field_name) (instance_variable_name)] @variable.other.member
|
||||
|
||||
; Constants
|
||||
;----------
|
||||
|
||||
[(boolean) (unit)] @constant
|
||||
|
||||
[(number) (signed_number)] @number
|
||||
[(number) (signed_number)] @constant.numeric.integer
|
||||
|
||||
(character) @constant.character
|
||||
|
||||
@ -66,7 +66,7 @@
|
||||
|
||||
(quoted_string "{" @string "}" @string) @string
|
||||
|
||||
(escape_sequence) @string.escape
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
[
|
||||
(conversion_specification)
|
||||
@ -145,7 +145,7 @@
|
||||
; Attributes
|
||||
;-----------
|
||||
|
||||
(attribute_id) @property
|
||||
(attribute_id) @variable.other.member
|
||||
|
||||
; Comments
|
||||
;---------
|
||||
|
@ -30,12 +30,12 @@
|
||||
; Member
|
||||
|
||||
(property_element
|
||||
(variable_name) @property)
|
||||
(variable_name) @variable.other.member)
|
||||
|
||||
(member_access_expression
|
||||
name: (variable_name (name)) @property)
|
||||
name: (variable_name (name)) @variable.other.member)
|
||||
(member_access_expression
|
||||
name: (name) @property)
|
||||
name: (name) @variable.other.member)
|
||||
|
||||
; Variables
|
||||
|
||||
@ -56,10 +56,10 @@
|
||||
|
||||
(string) @string
|
||||
(heredoc) @string
|
||||
(boolean) @constant.builtin
|
||||
(boolean) @constant.builtin.boolean
|
||||
(null) @constant.builtin
|
||||
(integer) @number
|
||||
(float) @number
|
||||
(integer) @constant.numeric.integer
|
||||
(float) @constant.numeric.float
|
||||
(comment) @comment
|
||||
|
||||
"$" @operator
|
||||
|
17
runtime/queries/php/indents.toml
Normal file
17
runtime/queries/php/indents.toml
Normal file
@ -0,0 +1,17 @@
|
||||
indent = [
|
||||
"array_creation_expression",
|
||||
"arguments",
|
||||
"formal_parameters",
|
||||
"compound_statement",
|
||||
"declaration_list",
|
||||
"binary_expression",
|
||||
"return_statement",
|
||||
"expression_statement",
|
||||
"switch_block",
|
||||
"anonymous_function_use_clause",
|
||||
]
|
||||
|
||||
oudent = [
|
||||
"}",
|
||||
")",
|
||||
]
|
@ -34,16 +34,14 @@
|
||||
[
|
||||
(fieldName)
|
||||
(optionName)
|
||||
] @property
|
||||
] @variable.other.member
|
||||
(enumVariantName) @type.enum.variant
|
||||
|
||||
(fullIdent) @namespace
|
||||
|
||||
[
|
||||
(intLit)
|
||||
(floatLit)
|
||||
] @number
|
||||
(boolLit) @constant.builtin
|
||||
(intLit) @constant.numeric.integer
|
||||
(floatLit) @constant.numeric.float
|
||||
(boolLit) @constant.builtin.boolean
|
||||
(strLit) @string
|
||||
|
||||
(constant) @constant
|
||||
|
@ -29,7 +29,7 @@
|
||||
name: (identifier) @function)
|
||||
|
||||
(identifier) @variable
|
||||
(attribute attribute: (identifier) @property)
|
||||
(attribute attribute: (identifier) @variable.other.member)
|
||||
(type (identifier) @type)
|
||||
|
||||
; Literals
|
||||
@ -40,14 +40,11 @@
|
||||
(false)
|
||||
] @constant.builtin
|
||||
|
||||
[
|
||||
(integer)
|
||||
(float)
|
||||
] @number
|
||||
|
||||
(integer) @constant.numeric.integer
|
||||
(float) @constant.numeric.float
|
||||
(comment) @comment
|
||||
(string) @string
|
||||
(escape_sequence) @escape
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
(interpolation
|
||||
"{" @punctuation.special
|
||||
|
39
runtime/queries/python/indents.toml
Normal file
39
runtime/queries/python/indents.toml
Normal file
@ -0,0 +1,39 @@
|
||||
indent = [
|
||||
"list",
|
||||
"tuple",
|
||||
"dictionary",
|
||||
"set",
|
||||
|
||||
"if_statement",
|
||||
"for_statement",
|
||||
"while_statement",
|
||||
"with_statement",
|
||||
"try_statement",
|
||||
"import_from_statement",
|
||||
|
||||
"parenthesized_expression",
|
||||
"generator_expression",
|
||||
"list_comprehension",
|
||||
"set_comprehension",
|
||||
"dictionary_comprehension",
|
||||
|
||||
"tuple_pattern",
|
||||
"list_pattern",
|
||||
"argument_list",
|
||||
"parameters",
|
||||
"binary_operator",
|
||||
|
||||
"function_definition",
|
||||
"class_definition",
|
||||
]
|
||||
|
||||
outdent = [
|
||||
")",
|
||||
"]",
|
||||
"}",
|
||||
"return_statement",
|
||||
"pass_statement",
|
||||
"raise_statement",
|
||||
]
|
||||
|
||||
ignore = ["string"]
|
14
runtime/queries/python/textobjects.scm
Normal file
14
runtime/queries/python/textobjects.scm
Normal file
@ -0,0 +1,14 @@
|
||||
(function_definition
|
||||
body: (block)? @function.inside) @function.around
|
||||
|
||||
(class_definition
|
||||
body: (block)? @class.inside) @class.around
|
||||
|
||||
(parameters
|
||||
(_) @parameter.inside)
|
||||
|
||||
(lambda_parameters
|
||||
(_) @parameter.inside)
|
||||
|
||||
(argument_list
|
||||
(_) @parameter.inside)
|
@ -55,7 +55,7 @@
|
||||
[
|
||||
(class_variable)
|
||||
(instance_variable)
|
||||
] @property
|
||||
] @variable.other.member
|
||||
|
||||
((identifier) @constant.builtin
|
||||
(#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
|
||||
@ -101,12 +101,12 @@
|
||||
] @string.special.symbol
|
||||
|
||||
(regex) @string.regexp
|
||||
(escape_sequence) @escape
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
[
|
||||
(integer)
|
||||
(float)
|
||||
] @number
|
||||
] @constant.numeric.integer
|
||||
|
||||
[
|
||||
(nil)
|
||||
|
@ -15,15 +15,13 @@
|
||||
; Primitives
|
||||
; ---
|
||||
|
||||
(escape_sequence) @escape
|
||||
(escape_sequence) @constant.character.escape
|
||||
(primitive_type) @type.builtin
|
||||
(boolean_literal) @constant.builtin.boolean
|
||||
(integer_literal) @constant.numeric.integer
|
||||
(float_literal) @constant.numeric.float
|
||||
(char_literal) @constant.character
|
||||
[
|
||||
(integer_literal)
|
||||
(float_literal)
|
||||
] @number
|
||||
[
|
||||
(char_literal)
|
||||
(string_literal)
|
||||
(raw_string_literal)
|
||||
] @string
|
||||
@ -40,10 +38,10 @@
|
||||
(enum_variant (identifier) @type.enum.variant)
|
||||
|
||||
(field_initializer
|
||||
(field_identifier) @property)
|
||||
(field_identifier) @variable.other.member)
|
||||
(shorthand_field_initializer
|
||||
(identifier) @variable.property)
|
||||
(shorthand_field_identifier) @variable.property
|
||||
(identifier) @variable.other.member)
|
||||
(shorthand_field_identifier) @variable.other.member
|
||||
|
||||
(lifetime
|
||||
"'" @label
|
||||
@ -81,9 +79,24 @@
|
||||
] @punctuation.bracket)
|
||||
|
||||
; ---
|
||||
; Parameters
|
||||
; Variables
|
||||
; ---
|
||||
|
||||
(let_declaration
|
||||
pattern: [
|
||||
((identifier) @variable)
|
||||
((tuple_pattern
|
||||
(identifier) @variable))
|
||||
])
|
||||
|
||||
; It needs to be anonymous to not conflict with `call_expression` further below.
|
||||
(_
|
||||
value: (field_expression
|
||||
value: (identifier)? @variable
|
||||
field: (field_identifier) @variable.other.member))
|
||||
|
||||
(arguments
|
||||
(identifier) @variable.parameter)
|
||||
(parameter
|
||||
pattern: (identifier) @variable.parameter)
|
||||
(closure_parameters
|
||||
@ -336,4 +349,4 @@
|
||||
|
||||
(type_identifier) @type
|
||||
(identifier) @variable
|
||||
(field_identifier) @property
|
||||
(field_identifier) @variable.other.member
|
||||
|
26
runtime/queries/rust/textobjects.scm
Normal file
26
runtime/queries/rust/textobjects.scm
Normal file
@ -0,0 +1,26 @@
|
||||
(function_item
|
||||
body: (_) @function.inside) @function.around
|
||||
|
||||
(struct_item
|
||||
body: (_) @class.inside) @class.around
|
||||
|
||||
(enum_item
|
||||
body: (_) @class.inside) @class.around
|
||||
|
||||
(union_item
|
||||
body: (_) @class.inside) @class.around
|
||||
|
||||
(trait_item
|
||||
body: (_) @class.inside) @class.around
|
||||
|
||||
(impl_item
|
||||
body: (_) @class.inside) @class.around
|
||||
|
||||
(parameters
|
||||
(_) @parameter.inside)
|
||||
|
||||
(closure_parameters
|
||||
(_) @parameter.inside)
|
||||
|
||||
(arguments
|
||||
(_) @parameter.inside)
|
@ -29,7 +29,7 @@
|
||||
(#match? @_attr "^(href|src)$"))
|
||||
|
||||
(tag_name) @tag
|
||||
(attribute_name) @property
|
||||
(attribute_name) @variable.other.member
|
||||
(erroneous_end_tag_name) @error
|
||||
(comment) @comment
|
||||
|
||||
|
@ -1,17 +1,17 @@
|
||||
; Properties
|
||||
;-----------
|
||||
|
||||
(bare_key) @property
|
||||
(bare_key) @variable.other.member
|
||||
(quoted_key) @string
|
||||
|
||||
; Literals
|
||||
;---------
|
||||
|
||||
(boolean) @constant.builtin
|
||||
(boolean) @constant.builtin.boolean
|
||||
(comment) @comment
|
||||
(string) @string
|
||||
(integer) @number
|
||||
(float) @number
|
||||
(integer) @constant.numeric.integer
|
||||
(float) @constant.numeric.float
|
||||
(offset_date_time) @string.special
|
||||
(local_date_time) @string.special
|
||||
(local_date) @string.special
|
||||
|
@ -35,12 +35,12 @@
|
||||
|
||||
(comment) @comment
|
||||
|
||||
(field_name) @property
|
||||
(field_name) @variable.other.member
|
||||
|
||||
(capture) @label
|
||||
|
||||
(predicate_name) @function
|
||||
|
||||
(escape_sequence) @escape
|
||||
(escape_sequence) @constant.character.escape
|
||||
|
||||
(node_name) @variable
|
||||
|
@ -1,12 +1,12 @@
|
||||
(block_mapping_pair key: (_) @property)
|
||||
(flow_mapping (_ key: (_) @property))
|
||||
(block_mapping_pair key: (_) @variable.other.member)
|
||||
(flow_mapping (_ key: (_) @variable.other.member))
|
||||
(boolean_scalar) @constant.builtin.boolean
|
||||
(null_scalar) @constant.builtin
|
||||
(double_quote_scalar) @string
|
||||
(single_quote_scalar) @string
|
||||
(escape_sequence) @string.escape
|
||||
(integer_scalar) @number
|
||||
(float_scalar) @number
|
||||
(escape_sequence) @constant.character.escape
|
||||
(integer_scalar) @constant.numeric.integer
|
||||
(float_scalar) @constant.numeric.float
|
||||
(comment) @comment
|
||||
(anchor_name) @type
|
||||
(alias_name) @type
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in New Issue
Block a user