Merge branch 'master' into debug

pull/574/head
Blaž Hrastnik 3 years ago
commit f2b709a3c3

@ -28,19 +28,19 @@ jobs:
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/registry 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 - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/git 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 - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: target 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 - name: Run cargo check
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@ -67,19 +67,19 @@ jobs:
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/registry 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 - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/git 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 - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: target 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 - name: Run cargo test
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1
@ -112,19 +112,19 @@ jobs:
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/registry 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 - name: Cache cargo index
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: ~/.cargo/git 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 - name: Cache cargo target dir
uses: actions/cache@v2.1.6 uses: actions/cache@v2.1.6
with: with:
path: target 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 - name: Run cargo fmt
uses: actions-rs/cargo@v1 uses: actions-rs/cargo@v1

6
.gitmodules vendored

@ -84,7 +84,7 @@
shallow = true shallow = true
[submodule "helix-syntax/languages/tree-sitter-elixir"] [submodule "helix-syntax/languages/tree-sitter-elixir"]
path = 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 shallow = true
[submodule "helix-syntax/languages/tree-sitter-nix"] [submodule "helix-syntax/languages/tree-sitter-nix"]
path = helix-syntax/languages/tree-sitter-nix path = helix-syntax/languages/tree-sitter-nix
@ -130,3 +130,7 @@
path = helix-syntax/languages/tree-sitter-tsq path = helix-syntax/languages/tree-sitter-tsq
url = https://github.com/tree-sitter/tree-sitter-tsq url = https://github.com/tree-sitter/tree-sitter-tsq
shallow = true 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

@ -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) # 0.4.1 (2021-08-14)
A minor release that includes: A minor release that includes:
@ -7,6 +88,8 @@ A minor release that includes:
# 0.4.0 (2021-08-13) # 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 Two months have passed, so this is another big release. A big thank you to all
the contributors and package maintainers! the contributors and package maintainers!
@ -44,6 +127,8 @@ selections in the future as well as resolves many bugs and edge cases.
# 0.3.0 (2021-06-27) # 0.3.0 (2021-06-27)
A big shout out to all the contributors! We had 24 contributors in this release.
Another big release. Another big release.
Highlights: Highlights:
@ -90,6 +175,8 @@ Includes a fix where wq/wqa could exit before file saving completed.
# 0.2.0 # 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 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 previously the CLI would always report version as 0.1.0, and we'd like
to distinguish it in bug reports.. to distinguish it in bug reports..

91
Cargo.lock generated

@ -78,9 +78,9 @@ checksum = "baf1de4339761588bc0619e3cbc0120ee582ebb74b53b4efbf79117bd2da40fd"
[[package]] [[package]]
name = "chardetng" name = "chardetng"
version = "0.1.14" version = "0.1.15"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "36a5a2ca47925d19fb6835f53b3e70dec0d25659211c8ee5cc784f1fd6838f9c" checksum = "83ee29c16b81c32fbc882ecc568305793338a8353952573db837f4f4a6cd5c2e"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"encoding_rs", "encoding_rs",
@ -101,15 +101,24 @@ dependencies = [
[[package]] [[package]]
name = "clipboard-win" name = "clipboard-win"
version = "4.2.1" version = "4.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4e4ea1881992efc993e4dc50a324cdbd03216e41bdc8385720ff47efc9bd2ca8" checksum = "3db8340083d28acb43451166543b98c838299b7e0863621be53a338adceea0ed"
dependencies = [ dependencies = [
"error-code", "error-code",
"str-buf", "str-buf",
"winapi", "winapi",
] ]
[[package]]
name = "content_inspector"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7bda66e858c683005a53a9a60c69a4aca7eeaa45d124526e389f7aec8e62f38"
dependencies = [
"memchr",
]
[[package]] [[package]]
name = "crossbeam-utils" name = "crossbeam-utils"
version = "0.8.5" version = "0.8.5"
@ -122,9 +131,9 @@ dependencies = [
[[package]] [[package]]
name = "crossterm" name = "crossterm"
version = "0.21.0" version = "0.22.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "486d44227f71a1ef39554c0dc47e44b9f4139927c75043312690c3f476d1d788" checksum = "c85525306c4291d1b73ce93c8acf9c339f9b213aef6c1d85c3830cbf1c16325c"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"crossterm_winapi", "crossterm_winapi",
@ -139,9 +148,9 @@ dependencies = [
[[package]] [[package]]
name = "crossterm_winapi" name = "crossterm_winapi"
version = "0.8.0" version = "0.9.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3a6966607622438301997d3dac0d2f6e9a90c68bb6bc1785ea98456ab93c0507" checksum = "2ae1b35a484aa10e07fe0638d02301c5ad24de82d310ccbd2f3693da5f09bf1c"
dependencies = [ dependencies = [
"winapi", "winapi",
] ]
@ -175,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457"
[[package]] [[package]]
name = "encoding_rs" name = "encoding_rs"
version = "0.8.28" version = "0.8.29"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "80df024fbc5ac80f87dfef0d9f5209a252f2a497f7f42944cff24d8253cac065" checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -358,11 +367,12 @@ dependencies = [
[[package]] [[package]]
name = "helix-core" name = "helix-core"
version = "0.4.1" version = "0.5.0"
dependencies = [ dependencies = [
"arc-swap", "arc-swap",
"etcetera", "etcetera",
"helix-syntax", "helix-syntax",
"log",
"once_cell", "once_cell",
"quickcheck", "quickcheck",
"regex", "regex",
@ -395,7 +405,7 @@ dependencies = [
[[package]] [[package]]
name = "helix-lsp" name = "helix-lsp"
version = "0.4.1" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"futures-executor", "futures-executor",
@ -413,7 +423,7 @@ dependencies = [
[[package]] [[package]]
name = "helix-syntax" name = "helix-syntax"
version = "0.4.1" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"cc", "cc",
@ -424,10 +434,11 @@ dependencies = [
[[package]] [[package]]
name = "helix-term" name = "helix-term"
version = "0.4.1" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"chrono", "chrono",
"content_inspector",
"crossterm", "crossterm",
"fern", "fern",
"futures-util", "futures-util",
@ -455,7 +466,7 @@ dependencies = [
[[package]] [[package]]
name = "helix-tui" name = "helix-tui"
version = "0.4.1" version = "0.5.0"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"cassowary", "cassowary",
@ -468,7 +479,7 @@ dependencies = [
[[package]] [[package]]
name = "helix-view" name = "helix-view"
version = "0.4.1" version = "0.5.0"
dependencies = [ dependencies = [
"anyhow", "anyhow",
"bitflags", "bitflags",
@ -532,9 +543,9 @@ dependencies = [
[[package]] [[package]]
name = "instant" name = "instant"
version = "0.1.11" version = "0.1.12"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "716d3d89f35ac6a34fd0eed635395f4c3b76fa889338a4632e5231a8684216bd" checksum = "7a5bbe824c507c5da5956355e86a746d82e0e1464f65d862cc5e71da70e94b2c"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
] ]
@ -566,9 +577,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
[[package]] [[package]]
name = "libc" name = "libc"
version = "0.2.103" version = "0.2.104"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd8f7255a17a627354f321ef0055d63b898c6fb27eff628af4d1b66b7331edf6" checksum = "7b2f96d100e1cf1929e7719b7edb3b90ab5298072638fccd77be9ce942ecdfce"
[[package]] [[package]]
name = "libloading" name = "libloading"
@ -600,9 +611,9 @@ dependencies = [
[[package]] [[package]]
name = "lsp-types" name = "lsp-types"
version = "0.90.1" version = "0.91.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6f3734ab1d7d157fc0c45110e06b587c31cd82bea2ccfd6b563cbff0aaeeb1d3" checksum = "2368312c59425dd133cb9a327afee65be0a633a8ce471d248e2202a48f8f68ae"
dependencies = [ dependencies = [
"bitflags", "bitflags",
"serde", "serde",
@ -640,9 +651,9 @@ dependencies = [
[[package]] [[package]]
name = "mio" name = "mio"
version = "0.7.13" version = "0.7.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8c2bdb6314ec10835cd3293dd268473a835c02b7b352e788be788b3c6ca6bb16" checksum = "8067b404fe97c70829f082dec8bcf4f71225d7eaea1d8645349cb76fa06205cc"
dependencies = [ dependencies = [
"libc", "libc",
"log", "log",
@ -755,9 +766,9 @@ checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184"
[[package]] [[package]]
name = "proc-macro2" name = "proc-macro2"
version = "1.0.29" version = "1.0.30"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b9f5105d4fdaab20335ca9565e106a5d9b82b6219b5ba735731124ac6711d23d" checksum = "edc3358ebc67bc8b7fa0c007f945b0b18226f78437d61bec735a9eb96b61ee70"
dependencies = [ dependencies = [
"unicode-xid", "unicode-xid",
] ]
@ -784,9 +795,9 @@ dependencies = [
[[package]] [[package]]
name = "quote" name = "quote"
version = "1.0.9" version = "1.0.10"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c3d0b9745dc2debf507c8422de05d7226cc1f0644216dfdfead988f9b1ab32a7" checksum = "38bc8cc6a5f2e3655e0899c1b848643b2562f853f114bfec7be120678e3ace05"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
] ]
@ -973,9 +984,9 @@ checksum = "2e24979f63a11545f5f2c60141afe249d4f19f84581ea2138065e400941d83d3"
[[package]] [[package]]
name = "slab" name = "slab"
version = "0.4.4" version = "0.4.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c307a32c1c5c437f38c7fd45d753050587732ba8628319fbdf12a7e289ccc590" checksum = "9def91fd1e018fe007022791f865d0ccc9b3a0d5001e01aabb8b40e46000afb5"
[[package]] [[package]]
name = "slotmap" name = "slotmap"
@ -1000,9 +1011,9 @@ checksum = "d44a3643b4ff9caf57abcee9c2c621d6c03d9135e0d8b589bd9afb5992cb176a"
[[package]] [[package]]
name = "syn" name = "syn"
version = "1.0.78" version = "1.0.80"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a4eac2e6c19f5c3abc0c229bea31ff0b9b091c7b14990e8924b92902a303a0c0" checksum = "d010a1623fbd906d51d650a9916aaefc05ffa0e4053ff7fe601167f3e715d194"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1075,9 +1086,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c"
[[package]] [[package]]
name = "tokio" name = "tokio"
version = "1.12.0" version = "1.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c2c2416fdedca8443ae44b4527de1ea633af61d8f7169ffa6e72c5b53d24efcc" checksum = "588b2d10a336da58d877567cd8fb8a14b463e2104910f8132cd054b4b96e29ee"
dependencies = [ dependencies = [
"autocfg", "autocfg",
"bytes", "bytes",
@ -1095,9 +1106,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-macros" name = "tokio-macros"
version = "1.4.1" version = "1.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "154794c8f499c2619acd19e839294703e9e32e7630ef5f46ea80d4ef0fbee5eb" checksum = "b2dd85aeaba7b68df939bd357c6afb36c87951be9e80bf9c859f2fc3e9fca0fd"
dependencies = [ dependencies = [
"proc-macro2", "proc-macro2",
"quote", "quote",
@ -1106,9 +1117,9 @@ dependencies = [
[[package]] [[package]]
name = "tokio-stream" name = "tokio-stream"
version = "0.1.7" version = "0.1.8"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7b2f3f698253f03119ac0102beaa64f67a67e08074d03a22d18784104543727f" checksum = "50145484efff8818b5ccd256697f36863f587da82cf8b409c53adf1e840798e3"
dependencies = [ dependencies = [
"futures-core", "futures-core",
"pin-project-lite", "pin-project-lite",
@ -1145,9 +1156,9 @@ dependencies = [
[[package]] [[package]]
name = "unicode-bidi" name = "unicode-bidi"
version = "0.3.6" version = "0.3.7"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "246f4c42e67e7a4e3c6106ff716a5d067d4132a642840b242e357e468a2a0085" checksum = "1a01404663e3db436ed2746d9fefef640d868edae3cceb81c3b8d5732fda678f"
[[package]] [[package]]
name = "unicode-general-category" name = "unicode-general-category"

@ -27,11 +27,11 @@ All shortcuts/keymaps can be found [in the documentation on the website](https:/
It's a terminal-based editor first, but I'd like to explore a custom renderer It's a terminal-based editor first, but I'd like to explore a custom renderer
(similar to emacs) in wgpu or skulpin. (similar to emacs) in wgpu or skulpin.
# Installation
Note: Only certain languages have indentation definitions at the moment. Check Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.toml`. `runtime/queries/<lang>/` for `indents.toml`.
# Installation
We provide packaging for various distributions, but here's a quick method to We provide packaging for various distributions, but here's a quick method to
build from source. build from source.
@ -71,7 +71,7 @@ Some suggestions to get started:
- You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker. - You can look at the [good first issue](https://github.com/helix-editor/helix/labels/E-easy) label on the issue tracker.
- Help with packaging on various distributions needed! - Help with packaging on various distributions needed!
- To use print debugging to the `~/.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!")`) * 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) * 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 - If your preferred language is missing, integrating a tree-sitter grammar for

@ -6,10 +6,6 @@
- clojure - clojure
- erlang - 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 - [ ] completion isIncomplete support
1 1
@ -18,8 +14,6 @@ as you type completion!
- [ ] = for auto indent line/selection - [ ] = for auto indent line/selection
- [ ] :x for closing buffers - [ ] :x for closing buffers
- [ ] repeat selection
- [ ] lsp: signature help - [ ] lsp: signature help
2 2

@ -8,3 +8,5 @@
- [Keymap](./keymap.md) - [Keymap](./keymap.md)
- [Key Remapping](./remapping.md) - [Key Remapping](./remapping.md)
- [Hooks](./hooks.md) - [Hooks](./hooks.md)
- [Guides](./guides/README.md)
- [Adding Textobject Queries](./guides/textobject.md)

@ -21,6 +21,8 @@ To override global configuration parameters, create a `config.toml` file located
| `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` | | `auto-pairs` | Enable automatic insertion of pairs to parenthese, brackets, etc. | `true` |
| `auto-completion` | Enable automatic pop up of auto-completion. | `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` | | `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 ## LSP

@ -0,0 +1,4 @@
# Guides
This section contains guides for adding new language server configurations,
tree-sitter grammers, textobject queries, etc.

@ -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 @@
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `h`, `Left` | Move left | `move_char_left` | | `h`/`Left` | Move left | `move_char_left` |
| `j`, `Down` | Move down | `move_char_right` | | `j`/`Down` | Move down | `move_line_down` |
| `k`, `Up` | Move up | `move_line_up` | | `k`/`Up` | Move up | `move_line_up` |
| `l`, `Right` | Move right | `move_line_down` | | `l`/`Right` | Move right | `move_char_right` |
| `w` | Move next word start | `move_next_word_start` | | `w` | Move next word start | `move_next_word_start` |
| `b` | Move previous word start | `move_prev_word_start` | | `b` | Move previous word start | `move_prev_word_start` |
| `e` | Move next word end | `move_next_word_end` | | `e` | Move next word end | `move_next_word_end` |
@ -22,6 +22,7 @@
| `f` | Find next char | `find_next_char` | | `f` | Find next char | `find_next_char` |
| `T` | Find 'till previous char | `till_prev_char` | | `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_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` | | `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` | | `End` | Move to the end of the line | `goto_line_end` |
| `PageUp` | Move page up | `page_up` | | `PageUp` | Move page up | `page_up` |
@ -54,6 +55,7 @@
| `A` | Insert at the end of the line | `append_to_line` | | `A` | Insert at the end of the line | `append_to_line` |
| `o` | Open new line below selection | `open_below` | | `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` | | `O` | Open new line above selection | `open_above` |
| `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` | | `u` | Undo change | `undo` |
| `U` | Redo change | `redo` | | `U` | Redo change | `redo` |
| `y` | Yank selection | `yank` | | `y` | Yank selection | `yank` |
@ -86,8 +88,9 @@
| `;` | Collapse selection onto a single cursor | `collapse_selection` | | `;` | Collapse selection onto a single cursor | `collapse_selection` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` | | `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| `,` | Keep only the primary selection | `keep_primary_selection` | | `,` | Keep only the primary selection | `keep_primary_selection` |
| `C` | Copy selection onto the next line | `copy_selection_on_next_line` | | `Alt-,` | Remove the primary selection | `remove_primary_selection` |
| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` | | `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 backward | `rotate_selections_backward` |
| `)` | Rotate main selection forward | `rotate_selections_forward` | | `)` | Rotate main selection forward | `rotate_selections_forward` |
| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` | | `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
@ -103,13 +106,13 @@
### Search ### Search
> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
| Key | Description | Command | | Key | Description | Command |
| ----- | ----------- | ------- | | ----- | ----------- | ------- |
| `/` | Search for regex pattern | `search` | | `/` | Search for regex pattern | `search` |
| `?` | Search for previous pattern | `rsearch` |
| `n` | Select next search match | `search_next` | | `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` | | `*` | Use current selection as the search pattern | `search_selection` |
### Minor modes ### Minor modes
@ -158,6 +161,8 @@ Jumps to various locations.
| `r` | Go to references | `goto_reference` | | `r` | Go to references | `goto_reference` |
| `i` | Go to implementation | `goto_implementation` | | `i` | Go to implementation | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` | | `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 #### Match mode
@ -184,7 +189,11 @@ This layer is similar to vim keybindings as kakoune does not support window.
| ----- | ------------- | ------- | | ----- | ------------- | ------- |
| `w`, `Ctrl-w` | Switch to next window | `rotate_view` | | `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
| `v`, `Ctrl-v` | Vertical right split | `vsplit` | | `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` | | `q`, `Ctrl-q` | Close current window | `wclose` |
#### Space mode #### Space mode
@ -244,10 +253,32 @@ Keys to use within picker. Remapping currently not supported.
| Key | Description | | Key | Description |
| ----- | ------------- | | ----- | ------------- |
| `Up`, `Ctrl-p` | Previous entry | | `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
| `Down`, `Ctrl-n` | Next entry | | `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
| `Ctrl-space` | Filter options | | `Ctrl-space` | Filter options |
| `Enter` | Open selected | | `Enter` | Open selected |
| `Ctrl-h` | Open horizontally | | `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically | | `Ctrl-v` | Open vertically |
| `Escape`, `Ctrl-c` | Close picker | | `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 @@
One-way key remapping is temporarily supported via a simple TOML configuration One-way key remapping is temporarily supported via a simple TOML configuration
file. (More powerful solutions such as rebinding via commands will be 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 To remap keys, write a `config.toml` file in your `helix` configuration
directory (default `~/.config/helix` in Linux systems) with a structure like directory (default `~/.config/helix` in Linux systems) with a structure like

@ -103,8 +103,6 @@ We use a similar set of scopes as
[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also [SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also
[TextMate](https://macromates.com/manual/en/language_grammars) scopes. [TextMate](https://macromates.com/manual/en/language_grammars) scopes.
- `escape` (TODO: rename to (constant).character.escape)
- `type` - Types - `type` - Types
- `builtin` - Primitive types provided by the language (`int`, `usize`) - `builtin` - Primitive types provided by the language (`int`, `usize`)
@ -112,13 +110,17 @@ We use a similar set of scopes as
- `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc)
- `boolean` - `boolean`
- `character` - `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)? - `string` (TODO: string.quoted.{single, double}, string.raw/.unquoted)?
- `regexp` - Regular expressions - `regexp` - Regular expressions
- `special` - `special`
- `path` - `path`
- `url` - `url`
- `symbol` - Erlang/Elixir atoms, Ruby symbols, Clojure keywords
- `comment` - Code comments - `comment` - Code comments
- `line` - Single line comments (`//`) - `line` - Single line comments (`//`)
@ -128,7 +130,8 @@ We use a similar set of scopes as
- `variable` - Variables - `variable` - Variables
- `builtin` - Reserved language variables (`self`, `this`, `super`, etc) - `builtin` - Reserved language variables (`self`, `this`, `super`, etc)
- `parameter` - Function parameters - `parameter` - Function parameters
- `property` - `other`
- `member` - Fields of composite data types (e.g. structs, unions)
- `function` (TODO: ?) - `function` (TODO: ?)
- `label` - `label`

@ -2,6 +2,8 @@
(Currently not fully documented, see the [keymappings](./keymap.md) list for more.) (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 ## 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: 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 @@ Multiple characters are currently not supported, but planned.
## Textobjects ## 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-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) - `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune) - `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
@ -60,5 +63,11 @@ Currently supported: `word`, `surround`.
| --- | --- | | --- | --- |
| `w` | Word | | `w` | Word |
| `(`, `[`, `'`, etc | Specified surround pairs | | `(`, `[`, `'`, etc | Specified surround pairs |
| `f` | Function |
Textobjects based on treesitter, like `function`, `class`, etc are planned. | `c` | Class |
| `p` | Parameter |
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 !

@ -2,11 +2,11 @@
"nodes": { "nodes": {
"devshell": { "devshell": {
"locked": { "locked": {
"lastModified": 1630239564, "lastModified": 1632436039,
"narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=", "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
"owner": "numtide", "owner": "numtide",
"repo": "devshell", "repo": "devshell",
"rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73", "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -15,6 +15,21 @@
"type": "github" "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": { "flakeCompat": {
"flake": false, "flake": false,
"locked": { "locked": {
@ -37,14 +52,16 @@
"nixpkgs": [ "nixpkgs": [
"nixpkgs" "nixpkgs"
], ],
"rustOverlay": "rustOverlay" "rustOverlay": [
"rust-overlay"
]
}, },
"locked": { "locked": {
"lastModified": 1631254163, "lastModified": 1634796585,
"narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=", "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
"owner": "yusdacra", "owner": "yusdacra",
"repo": "nix-cargo-integration", "repo": "nix-cargo-integration",
"rev": "432d8504a32232e8d74710024d5bf5cc31767651", "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -55,11 +72,11 @@
}, },
"nixpkgs": { "nixpkgs": {
"locked": { "locked": {
"lastModified": 1631206977, "lastModified": 1634782485,
"narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=", "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
"owner": "nixos", "owner": "nixos",
"repo": "nixpkgs", "repo": "nixpkgs",
"rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b", "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
"type": "github" "type": "github"
}, },
"original": { "original": {
@ -69,21 +86,40 @@
"type": "github" "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": { "root": {
"inputs": { "inputs": {
"flakeCompat": "flakeCompat", "flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration", "nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs" "nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
} }
}, },
"rustOverlay": { "rust-overlay": {
"flake": false, "inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": { "locked": {
"lastModified": 1631240108, "lastModified": 1634869268,
"narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=", "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
"owner": "oxalica", "owner": "oxalica",
"repo": "rust-overlay", "repo": "rust-overlay",
"rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289", "rev": "c02c2d86354327317546501af001886fbb53d374",
"type": "github" "type": "github"
}, },
"original": { "original": {

@ -3,9 +3,11 @@
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
nixCargoIntegration = { nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration"; url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs"; inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay";
}; };
flakeCompat = { flakeCompat = {
url = "github:edolstra/flake-compat"; url = "github:edolstra/flake-compat";
@ -61,7 +63,7 @@
''; '';
}; };
shell = common: prev: { 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 ++ [ env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; } { name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; } { name = "RUST_BACKTRACE"; value = "1"; }

@ -1,8 +1,8 @@
[package] [package]
name = "helix-core" name = "helix-core"
version = "0.4.1" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
description = "Helix editor core editing primitives" description = "Helix editor core editing primitives"
categories = ["editor"] categories = ["editor"]
@ -13,7 +13,7 @@ include = ["src/**/*", "README.md"]
[features] [features]
[dependencies] [dependencies]
helix-syntax = { version = "0.4", path = "../helix-syntax" } helix-syntax = { version = "0.5", path = "../helix-syntax" }
ropey = "1.3" ropey = "1.3"
smallvec = "1.7" smallvec = "1.7"
@ -27,6 +27,7 @@ once_cell = "1.8"
arc-swap = "1" arc-swap = "1"
regex = "1" regex = "1"
log = "0.4"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
toml = "0.5" 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 crate::{Range, Rope, Selection, Tendril, Transaction};
use smallvec::SmallVec; use smallvec::SmallVec;

@ -1,3 +1,5 @@
//! Utility functions to categorize a `char`.
use crate::LineEnding; use crate::LineEnding;
#[derive(Debug, Eq, PartialEq)] #[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::{ use crate::{
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction, 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 token = token.unwrap_or("//");
let comment = Tendril::from(format!("{} ", token)); 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; let mut min_next_line = 0;
for selection in selection { for selection in selection {

@ -1,3 +1,6 @@
//! LSP diagnostic utility types.
/// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Eq, PartialEq)] #[derive(Debug, Eq, PartialEq)]
pub enum Severity { pub enum Severity {
Error, Error,
@ -6,12 +9,14 @@ pub enum Severity {
Hint, Hint,
} }
#[derive(Debug)] /// A range of `char`s within the text.
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub struct Range { pub struct Range {
pub start: usize, pub start: usize,
pub end: usize, pub end: usize,
} }
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug)] #[derive(Debug)]
pub struct Diagnostic { pub struct Diagnostic {
pub range: Range, 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 ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete}; use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr; use unicode_width::UnicodeWidthStr;

@ -4,48 +4,50 @@ use regex::Regex;
use std::num::NonZeroUsize; use std::num::NonZeroUsize;
use std::time::{Duration, Instant}; use std::time::{Duration, Instant};
// Stores the history of changes to a buffer. /// Stores the history of changes to a buffer.
// ///
// Currently the history is represented as a vector of revisions. The vector /// Currently the history is represented as a vector of revisions. The vector
// always has at least one element: the empty root revision. Each revision /// always has at least one element: the empty root revision. Each revision
// with the exception of the root has a parent revision, a [Transaction] /// 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, /// 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 /// and an inversion of that transaction to transition from the parent to its
// latest child. /// latest child.
// ///
// When using `u` to undo a change, an inverse of the stored transaction will /// 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. /// 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 /// 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 /// last child revision. When using `U` to redo a change, the last child transaction
// will be applied to the current state of the buffer. /// will be applied to the current state of the buffer.
// ///
// The current revision is the one currently displayed in 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 /// 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. /// 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 /// 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 /// 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 /// 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 /// given to the command. If a single integer is given, the editor will instead
// jump the given number of revisions in the vector. /// jump the given number of revisions in the vector.
// ///
// Limitations: /// Limitations:
// * Changes in selections currently don't commit history changes. The selection /// * Changes in selections currently don't commit history changes. The selection
// will only be updated to the state after a commited buffer change. /// will only be updated to the state after a commited buffer change.
// * The vector of history revisions is currently unbounded. This might /// * The vector of history revisions is currently unbounded. This might
// cause the memory consumption to grow significantly large during long /// cause the memory consumption to grow significantly large during long
// editing sessions. /// editing sessions.
// * Because delete transactions currently don't store the text that they /// * Because delete transactions currently don't store the text that they
// delete, we also store an inversion of the transaction. /// 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)] #[derive(Debug)]
pub struct History { pub struct History {
revisions: Vec<Revision>, revisions: Vec<Revision>,
current: usize, current: usize,
} }
// A single point in history. See [History] for more information. /// A single point in history. See [History] for more information.
#[derive(Debug)] #[derive(Debug)]
struct Revision { struct Revision {
parent: usize, parent: usize,
@ -111,6 +113,7 @@ impl History {
self.current == 0 self.current == 0
} }
/// Undo the last edit.
pub fn undo(&mut self) -> Option<&Transaction> { pub fn undo(&mut self) -> Option<&Transaction> {
if self.at_root() { if self.at_root() {
return None; return None;
@ -121,6 +124,7 @@ impl History {
Some(&current_revision.inversion) Some(&current_revision.inversion)
} }
/// Redo the last edit.
pub fn redo(&mut self) -> Option<&Transaction> { pub fn redo(&mut self) -> Option<&Transaction> {
let current_revision = &self.revisions[self.current]; let current_revision = &self.revisions[self.current];
let last_child = current_revision.last_child?; let last_child = current_revision.last_child?;
@ -147,8 +151,8 @@ impl History {
} }
} }
// List of nodes on the way from `n` to 'a`. Doesn`t include `a`. /// 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`. /// Includes `n` unless `a == n`. `a` must be an ancestor of `n`.
fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> { fn path_up(&self, mut n: usize, a: usize) -> Vec<usize> {
let mut path = Vec::new(); let mut path = Vec::new();
while n != a { while n != a {
@ -158,6 +162,7 @@ impl History {
path path
} }
/// Create a [`Transaction`] that will jump to a specific revision in the history.
fn jump_to(&mut self, to: usize) -> Vec<Transaction> { fn jump_to(&mut self, to: usize) -> Vec<Transaction> {
let lca = self.lowest_common_ancestor(self.current, to); let lca = self.lowest_common_ancestor(self.current, to);
let up = self.path_up(self.current, lca); let up = self.path_up(self.current, lca);
@ -171,10 +176,12 @@ impl History {
up_txns.chain(down_txns).collect() up_txns.chain(down_txns).collect()
} }
/// Creates a [`Transaction`] that will undo `delta` revisions.
fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> { fn jump_backward(&mut self, delta: usize) -> Vec<Transaction> {
self.jump_to(self.current.saturating_sub(delta)) 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> { fn jump_forward(&mut self, delta: usize) -> Vec<Transaction> {
self.jump_to( self.jump_to(
self.current self.current
@ -183,7 +190,7 @@ impl History {
) )
} }
// 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 { fn revision_closer_to_instant(&self, i: usize, instant: Instant) -> usize {
let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp); let dur_im1 = instant.duration_since(self.revisions[i - 1].timestamp);
let dur_i = self.revisions[i].timestamp.duration_since(instant); let dur_i = self.revisions[i].timestamp.duration_since(instant);
@ -194,6 +201,8 @@ impl History {
} }
} }
/// Creates a [`Transaction`] that will match a revision created at around
/// `instant`.
fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> { fn jump_instant(&mut self, instant: Instant) -> Vec<Transaction> {
let search_result = self let search_result = self
.revisions .revisions
@ -209,6 +218,8 @@ impl History {
self.jump_to(revision) 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> { fn jump_duration_backward(&mut self, duration: Duration) -> Vec<Transaction> {
match self.revisions[self.current].timestamp.checked_sub(duration) { match self.revisions[self.current].timestamp.checked_sub(duration) {
Some(instant) => self.jump_instant(instant), Some(instant) => self.jump_instant(instant),
@ -216,6 +227,8 @@ impl History {
} }
} }
/// 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> { fn jump_duration_forward(&mut self, duration: Duration) -> Vec<Transaction> {
match self.revisions[self.current].timestamp.checked_add(duration) { match self.revisions[self.current].timestamp.checked_add(duration) {
Some(instant) => self.jump_instant(instant), Some(instant) => self.jump_instant(instant),
@ -223,6 +236,7 @@ impl History {
} }
} }
/// Creates an undo [`Transaction`].
pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> { pub fn earlier(&mut self, uk: UndoKind) -> Vec<Transaction> {
use UndoKind::*; use UndoKind::*;
match uk { match uk {
@ -231,6 +245,7 @@ impl History {
} }
} }
/// Creates a redo [`Transaction`].
pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> { pub fn later(&mut self, uk: UndoKind) -> Vec<Transaction> {
use UndoKind::*; use UndoKind::*;
match uk { match uk {
@ -240,13 +255,14 @@ impl History {
} }
} }
/// Whether to undo by a number of edits or a duration of time.
#[derive(Debug, PartialEq)] #[derive(Debug, PartialEq)]
pub enum UndoKind { pub enum UndoKind {
Steps(usize), Steps(usize),
TimePeriod(std::time::Duration), 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)] = &[ const TIME_UNITS: &[(&[&str], &str, u64)] = &[
(&["seconds", "second", "sec", "s"], "seconds", 1), (&["seconds", "second", "sec", "s"], "seconds", 1),
(&["minutes", "minute", "min", "m"], "minutes", 60), (&["minutes", "minute", "min", "m"], "minutes", 60),
@ -254,11 +270,20 @@ const TIME_UNITS: &[(&[&str], &str, u64)] = &[
(&["days", "day", "d"], "days", 24 * 60 * 60), (&["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> = static DURATION_VALIDATION_REGEX: Lazy<Regex> =
Lazy::new(|| Regex::new(r"^(?:\d+\s*[a-z]+\s*)+$").unwrap()); 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()); 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> { fn parse_human_duration(s: &str) -> Result<Duration, String> {
if !DURATION_VALIDATION_REGEX.is_match(s) { if !DURATION_VALIDATION_REGEX.is_match(s) {
return Err("duration should be composed \ return Err("duration should be composed \

@ -464,6 +464,7 @@ where
unit: String::from(" "), unit: String::from(" "),
}), }),
indent_query: OnceCell::new(), indent_query: OnceCell::new(),
textobject_query: OnceCell::new(),
debugger: None, debugger: None,
}], }],
}); });

@ -35,6 +35,7 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option<usize> {
line.chars().position(|ch| !ch.is_whitespace()) line.chars().position(|ch| !ch.is_whitespace())
} }
/// Find `.git` root.
pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> { pub fn find_root(root: Option<&str>) -> Option<std::path::PathBuf> {
let current_dir = std::env::current_dir().expect("unable to determine current directory"); let current_dir = std::env::current_dir().expect("unable to determine current directory");
@ -193,7 +194,7 @@ pub use tendril::StrTendril as Tendril;
pub use {regex, tree_sitter}; pub use {regex, tree_sitter};
pub use graphemes::RopeGraphemes; 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 selection::{Range, Selection};
pub use smallvec::{smallvec, SmallVec}; pub use smallvec::{smallvec, SmallVec};
pub use syntax::Syntax; pub use syntax::Syntax;

@ -20,7 +20,7 @@ pub enum LineEnding {
impl LineEnding { impl LineEnding {
#[inline] #[inline]
pub fn len_chars(&self) -> usize { pub const fn len_chars(&self) -> usize {
match self { match self {
Self::Crlf => 2, Self::Crlf => 2,
_ => 1, _ => 1,
@ -28,7 +28,7 @@ impl LineEnding {
} }
#[inline] #[inline]
pub fn as_str(&self) -> &'static str { pub const fn as_str(&self) -> &'static str {
match self { match self {
Self::Crlf => "\u{000D}\u{000A}", Self::Crlf => "\u{000D}\u{000A}",
Self::LF => "\u{000A}", Self::LF => "\u{000A}",
@ -42,7 +42,7 @@ impl LineEnding {
} }
#[inline] #[inline]
pub fn from_char(ch: char) -> Option<LineEnding> { pub const fn from_char(ch: char) -> Option<LineEnding> {
match ch { match ch {
'\u{000A}' => Some(LineEnding::LF), '\u{000A}' => Some(LineEnding::LF),
'\u{000B}' => Some(LineEnding::VT), '\u{000B}' => Some(LineEnding::VT),

@ -53,6 +53,10 @@ pub fn move_vertically(
let pos = range.cursor(slice); let pos = range.cursor(slice);
// Compute the current position's 2d coordinates. // 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 Position { row, col } = coords_at_pos(slice, pos);
let horiz = range.horiz.unwrap_or(col as u32); 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 let parent = match tree
.root_node() .root_node()
.descendant_for_byte_range(from, to) .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, Some(parent) => parent,
None => return range, None => return range,
}; };

@ -2,6 +2,7 @@ use crate::{
chars::char_is_line_ending, chars::char_is_line_ending,
graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes}, graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
line_ending::line_end_char_index, line_ending::line_end_char_index,
unicode::width::UnicodeWidthChar,
RopeSlice, RopeSlice,
}; };
@ -54,11 +55,8 @@ impl From<Position> for tree_sitter::Point {
} }
/// Convert a character index to (line, column) coordinates. /// Convert a character index to (line, column) coordinates.
/// ///
/// TODO: this should be split into two methods: one for visual /// column in `char` count which can be used for row:column display in
/// row/column, and one for "objective" row/column (possibly with /// status line. See [`visual_coords_at_pos`] for a visual one.
/// 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.
pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position { pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
let line = text.char_to_line(pos); 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) 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. /// Convert (line, column) coordinates to a character index.
/// ///
/// If the `line` coordinate is beyond the end of the file, the EOF /// If the `line` coordinate is beyond the end of the file, the EOF
@ -130,7 +150,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
// Test with wide characters. // Test with wide characters.
// TODO: account for character width.
let text = Rope::from("今日はいい\n"); let text = Rope::from("今日はいい\n");
let slice = text.slice(..); let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@ -151,7 +170,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 9), (1, 0).into()); assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
// Test with wide-character grapheme clusters. // Test with wide-character grapheme clusters.
// TODO: account for character width.
let text = Rope::from("किमपि\n"); let text = Rope::from("किमपि\n");
let slice = text.slice(..); let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@ -161,7 +179,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 6), (1, 0).into()); assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
// Test with tabs. // Test with tabs.
// Todo: account for tab stops.
let text = Rope::from("\tHello\n"); let text = Rope::from("\tHello\n");
let slice = text.slice(..); let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into()); assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@ -169,6 +186,54 @@ mod test {
assert_eq!(coords_at_pos(slice, 2), (0, 2).into()); 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] #[test]
fn test_pos_at_coords() { fn test_pos_at_coords() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ"); let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");

@ -7,7 +7,7 @@ pub struct Register {
} }
impl Register { impl Register {
pub fn new(name: char) -> Self { pub const fn new(name: char) -> Self {
Self { Self {
name, name,
values: Vec::new(), values: Vec::new(),
@ -18,7 +18,7 @@ impl Register {
Self { name, values } Self { name, values }
} }
pub fn name(&self) -> char { pub const fn name(&self) -> char {
self.name self.name
} }

@ -362,6 +362,11 @@ impl Selection {
/// Adds a new range to the selection and makes it the primary range. /// Adds a new range to the selection and makes it the primary range.
pub fn remove(mut self, index: usize) -> Self { 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); self.ranges.remove(index);
if index < self.primary_index || self.primary_index == self.ranges.len() { if index < self.primary_index || self.primary_index == self.ranges.len() {
self.primary_index -= 1; self.primary_index -= 1;
@ -369,6 +374,12 @@ impl Selection {
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 /// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document. /// applying changes to a document.
pub fn map(self, changes: &ChangeSet) -> Self { pub fn map(self, changes: &ChangeSet) -> Self {

@ -49,7 +49,7 @@ pub struct Configuration {
#[serde(rename_all = "kebab-case")] #[serde(rename_all = "kebab-case")]
pub struct LanguageConfiguration { pub struct LanguageConfiguration {
#[serde(rename = "name")] #[serde(rename = "name")]
pub(crate) language_id: String, pub language_id: String,
pub scope: String, // source.rust pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc> pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml> pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
@ -76,6 +76,8 @@ pub struct LanguageConfiguration {
#[serde(skip)] #[serde(skip)]
pub(crate) indent_query: OnceCell<Option<IndentQuery>>, pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
#[serde(skip)]
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub debugger: Option<DebugAdapterConfig>, pub debugger: Option<DebugAdapterConfig>,
} }
@ -160,6 +162,32 @@ pub struct IndentQuery {
pub outdent: HashSet<String>, 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> { fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let path = crate::RUNTIME_DIR let path = crate::RUNTIME_DIR
.join("queries") .join("queries")
@ -208,13 +236,14 @@ impl LanguageConfiguration {
// highlights_query += "\n(ERROR) @error"; // highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm"); let injections_query = read_query(&language, "injections.scm");
let locals_query = read_query(&language, "locals.scm"); let locals_query = read_query(&language, "locals.scm");
if highlights_query.is_empty() { if highlights_query.is_empty() {
None None
} else { } 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( let config = HighlightConfiguration::new(
language, language,
&highlights_query, &highlights_query,
@ -258,6 +287,18 @@ impl LanguageConfiguration {
.as_ref() .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 { pub fn scope(&self) -> &str {
&self.scope &self.scope
} }
@ -451,7 +492,7 @@ impl Syntax {
/// Iterate over the highlighted regions for a given slice of source code. /// Iterate over the highlighted regions for a given slice of source code.
pub fn highlight_iter<'a>( pub fn highlight_iter<'a>(
&self, &'a self,
source: RopeSlice<'a>, source: RopeSlice<'a>,
range: Option<std::ops::Range<usize>>, range: Option<std::ops::Range<usize>>,
cancellation_flag: Option<&'a AtomicUsize>, cancellation_flag: Option<&'a AtomicUsize>,
@ -466,11 +507,10 @@ impl Syntax {
let highlighter = &mut ts_parser.borrow_mut(); let highlighter = &mut ts_parser.borrow_mut();
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new) 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 cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) }; let query_ref = &self.config.query;
let config_ref = let config_ref = self.config.as_ref();
unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
// if reusing cursors & no range this resets to whole range // if reusing cursors & no range this resets to whole range
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX)); cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
@ -582,39 +622,7 @@ impl LanguageLayer {
self.tree.as_ref(), self.tree.as_ref(),
) )
.ok_or(Error::Cancelled)?; .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) self.tree = Some(tree)
} }
Ok(()) Ok(())

@ -1,9 +1,13 @@
use std::fmt::Display;
use ropey::RopeSlice; use ropey::RopeSlice;
use tree_sitter::{Node, QueryCursor};
use crate::chars::{categorize_char, char_is_whitespace, CharCategory}; use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::next_grapheme_boundary; use crate::graphemes::next_grapheme_boundary;
use crate::movement::Direction; use crate::movement::Direction;
use crate::surround; use crate::surround;
use crate::syntax::LanguageConfiguration;
use crate::Range; use crate::Range;
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize { fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
@ -51,6 +55,15 @@ pub enum TextObject {
Inside, 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 // count doesn't do anything yet
pub fn textobject_word( pub fn textobject_word(
slice: RopeSlice, slice: RopeSlice,
@ -108,6 +121,44 @@ pub fn textobject_surround(
.unwrap_or(range) .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)] #[cfg(test)]
mod test { mod test {
use super::TextObject::*; use super::TextObject::*;

@ -132,6 +132,9 @@ impl ChangeSet {
if self.changes.is_empty() { if self.changes.is_empty() {
return other; return other;
} }
if other.changes.is_empty() {
return self;
}
let len = self.changes.len(); let len = self.changes.len();
@ -465,6 +468,13 @@ impl Transaction {
} }
} }
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 { pub fn with_selection(mut self, selection: Selection) -> Self {
self.selection = Some(selection); self.selection = Some(selection);
self self

@ -1,6 +1,6 @@
[package] [package]
name = "helix-dap" name = "helix-dap"
version = "0.4.1" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2018"
license = "MPL-2.0" license = "MPL-2.0"

@ -1,8 +1,8 @@
[package] [package]
name = "helix-lsp" name = "helix-lsp"
version = "0.4.1" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
description = "LSP client implementation for Helix project" description = "LSP client implementation for Helix project"
categories = ["editor"] 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 # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies] [dependencies]
helix-core = { version = "0.4", path = "../helix-core" } helix-core = { version = "0.5", path = "../helix-core" }
anyhow = "1.0" anyhow = "1.0"
futures-executor = "0.3" futures-executor = "0.3"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } 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 jsonrpc-core = { version = "18.0", default-features = false } # don't pull in all of futures
log = "0.4" log = "0.4"
lsp-types = { version = "0.90", features = ["proposed"] } lsp-types = { version = "0.91", features = ["proposed"] }
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0" serde_json = "1.0"
thiserror = "1.0" thiserror = "1.0"
tokio = { version = "1.12", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio = { version = "1.13", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] }
tokio-stream = "0.1.7" tokio-stream = "0.1.8"

@ -461,7 +461,7 @@ impl Client {
}; };
let changes = match sync_capabilities { let changes = match sync_capabilities {
lsp::TextDocumentSyncKind::Full => { lsp::TextDocumentSyncKind::FULL => {
vec![lsp::TextDocumentContentChangeEvent { vec![lsp::TextDocumentContentChangeEvent {
// range = None -> whole document // range = None -> whole document
range: None, //Some(Range) range: None, //Some(Range)
@ -469,10 +469,11 @@ impl Client {
text: new_text.to_string(), text: new_text.to_string(),
}] }]
} }
lsp::TextDocumentSyncKind::Incremental => { lsp::TextDocumentSyncKind::INCREMENTAL => {
Self::changeset_to_changes(old_text, new_text, changes, self.offset_encoding) 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>( Some(self.notify::<lsp::notification::DidChangeTextDocument>(

@ -1,8 +1,8 @@
[package] [package]
name = "helix-syntax" name = "helix-syntax"
version = "0.4.1" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
description = "Tree-sitter grammars support" description = "Tree-sitter grammars support"
categories = ["editor"] categories = ["editor"]

@ -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] [package]
name = "helix-term" name = "helix-term"
version = "0.4.1" version = "0.5.0"
description = "A post-modern text editor." description = "A post-modern text editor."
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
categories = ["editor", "command-line-utilities"] categories = ["editor", "command-line-utilities"]
repository = "https://github.com/helix-editor/helix" repository = "https://github.com/helix-editor/helix"
@ -21,10 +21,10 @@ name = "hx"
path = "src/main.rs" path = "src/main.rs"
[dependencies] [dependencies]
helix-core = { version = "0.4", path = "../helix-core" } helix-core = { version = "0.5", path = "../helix-core" }
helix-view = { version = "0.4", path = "../helix-view" } helix-view = { version = "0.5", path = "../helix-view" }
helix-lsp = { version = "0.4", path = "../helix-lsp" } helix-lsp = { version = "0.5", path = "../helix-lsp" }
helix-dap = { version = "0.4", path = "../helix-dap" } helix-dap = { version = "0.5", path = "../helix-dap" }
anyhow = "1" anyhow = "1"
once_cell = "1.8" 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"] } tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] }
num_cpus = "1" num_cpus = "1"
tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } 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" signal-hook = "0.3"
tokio-stream = "0.1" tokio-stream = "0.1"
futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false }
@ -45,10 +45,10 @@ log = "0.4"
# File picker # File picker
fuzzy-matcher = "0.3" fuzzy-matcher = "0.3"
ignore = "0.4" ignore = "0.4"
# shellexpand = "2.1"
# dirs-next = "2.0"
# markdown doc rendering # markdown doc rendering
pulldown-cmark = { version = "0.8", default-features = false } pulldown-cmark = { version = "0.8", default-features = false }
# file type detection
content_inspector = "0.2.4"
# config # config
toml = "0.5" toml = "0.5"

@ -99,12 +99,17 @@ impl Application {
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys))); let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
compositor.push(editor_view); 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 let first = &args.files[0]; // we know it's not empty
if first.is_dir() { if first.is_dir() {
std::env::set_current_dir(&first)?; std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit); editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(first.clone()))); compositor.push(Box::new(ui::file_picker(".".into())));
} else { } else {
let nr_of_files = args.files.len(); let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?; editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@ -240,7 +245,7 @@ impl Application {
} }
pub fn handle_idle_timeout(&mut self) { pub fn handle_idle_timeout(&mut self) {
use crate::commands::{completion, Context}; use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode; use helix_view::document::Mode;
if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion { if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
@ -267,7 +272,7 @@ impl Application {
callback: None, callback: None,
on_next_key_callback: None, on_next_key_callback: None,
}; };
completion(&mut cx); idle_completion(&mut cx);
self.render(); self.render();
} }
@ -548,10 +553,11 @@ impl Application {
message: diagnostic.message, message: diagnostic.message,
severity: diagnostic.severity.map( severity: diagnostic.severity.map(
|severity| match severity { |severity| match severity {
DiagnosticSeverity::Error => Error, DiagnosticSeverity::ERROR => Error,
DiagnosticSeverity::Warning => Warning, DiagnosticSeverity::WARNING => Warning,
DiagnosticSeverity::Information => Info, DiagnosticSeverity::INFORMATION => Info,
DiagnosticSeverity::Hint => Hint, DiagnosticSeverity::HINT => Hint,
severity => unimplemented!("{:?}", severity),
}, },
), ),
// code // code
@ -727,7 +733,9 @@ impl Application {
let mut stdout = stdout(); let mut stdout = stdout();
// reset cursor shape // reset cursor shape
write!(stdout, "\x1B[2 q")?; 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)?; execute!(stdout, terminal::LeaveAlternateScreen)?;
terminal::disable_raw_mode()?; terminal::disable_raw_mode()?;
Ok(()) Ok(())

@ -5,6 +5,7 @@ use std::path::PathBuf;
pub struct Args { pub struct Args {
pub display_help: bool, pub display_help: bool,
pub display_version: bool, pub display_version: bool,
pub load_tutor: bool,
pub verbosity: u64, pub verbosity: u64,
pub files: Vec<PathBuf>, pub files: Vec<PathBuf>,
} }
@ -22,6 +23,7 @@ impl Args {
"--" => break, // stop parsing at this point treat the remaining as files "--" => break, // stop parsing at this point treat the remaining as files
"--version" => args.display_version = true, "--version" => args.display_version = true,
"--help" => args.display_help = true, "--help" => args.display_help = true,
"--tutor" => args.load_tutor = true,
arg if arg.starts_with("--") => { arg if arg.starts_with("--") => {
return Err(Error::msg(format!( return Err(Error::msg(format!(
"unexpected double dash argument: {}", "unexpected double dash argument: {}",

File diff suppressed because it is too large Load Diff

@ -207,7 +207,7 @@ pub trait AnyComponent {
/// ///
/// ```rust /// ```rust
/// use helix_term::{ui::Text, compositor::Component}; /// 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(); /// let text: Box<Text> = boxed.as_boxed_any().downcast().unwrap();
/// ``` /// ```
fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>; fn as_boxed_any(self: Box<Self>) -> Box<dyn Any>;

@ -5,20 +5,20 @@ use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize; use serde::Deserialize;
use std::{ use std::{
borrow::Cow, borrow::Cow,
collections::HashMap, collections::{BTreeSet, HashMap},
ops::{Deref, DerefMut}, ops::{Deref, DerefMut},
}; };
#[macro_export] #[macro_export]
macro_rules! key { macro_rules! key {
($key:ident) => { ($key:ident) => {
KeyEvent { ::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key, code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::NONE, modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
} }
}; };
($($ch:tt)*) => { ($($ch:tt)*) => {
KeyEvent { ::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*), code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::NONE, modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
} }
@ -78,19 +78,30 @@ macro_rules! keymap {
}; };
} }
#[derive(Debug, Clone, Deserialize)] #[derive(Debug, Clone)]
pub struct KeyTrieNode { pub struct KeyTrieNode {
/// A label for keys coming under this node, like "Goto mode" /// A label for keys coming under this node, like "Goto mode"
#[serde(skip)]
name: String, name: String,
#[serde(flatten)]
map: HashMap<KeyEvent, KeyTrie>, map: HashMap<KeyEvent, KeyTrie>,
#[serde(skip)]
order: Vec<KeyEvent>, order: Vec<KeyEvent>,
#[serde(skip)]
pub is_sticky: bool, 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 { impl KeyTrieNode {
pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self { pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
Self { Self {
@ -118,7 +129,6 @@ impl KeyTrieNode {
} }
self.map.insert(key, trie); self.map.insert(key, trie);
} }
for &key in self.map.keys() { for &key in self.map.keys() {
if !self.order.contains(&key) { if !self.order.contains(&key) {
self.order.push(key); self.order.push(key);
@ -127,20 +137,29 @@ impl KeyTrieNode {
} }
pub fn infobox(&self) -> Info { 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() { for (&key, trie) in self.iter() {
let desc = match trie { 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(), KeyTrie::Node(n) => n.name(),
}; };
match body.iter().position(|(d, _)| d == &desc) { match body.iter().position(|(d, _)| d == &desc) {
// FIXME: multiple keys are ordered randomly (use BTreeSet) Some(pos) => {
Some(pos) => body[pos].1.push(key), body[pos].1.insert(key);
None => body.push((desc, vec![key])), }
None => body.push((desc, BTreeSet::from([key]))),
} }
} }
body.sort_unstable_by_key(|(_, keys)| { 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()); let prefix = format!("{} ", self.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) { if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
@ -151,6 +170,11 @@ impl KeyTrieNode {
} }
Info::new(self.name(), body) 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 { impl Default for KeyTrieNode {
@ -235,6 +259,7 @@ pub enum KeymapResultKind {
/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a /// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
/// reference to the sticky node if one is currently active. /// reference to the sticky node if one is currently active.
#[derive(Debug)]
pub struct KeymapResult<'a> { pub struct KeymapResult<'a> {
pub kind: KeymapResultKind, pub kind: KeymapResultKind,
pub sticky: Option<&'a KeyTrieNode>, pub sticky: Option<&'a KeyTrieNode>,
@ -395,6 +420,7 @@ impl Default for Keymaps {
"F" => find_prev_char, "F" => find_prev_char,
"r" => replace, "r" => replace,
"R" => replace_with_yanked, "R" => replace_with_yanked,
"A-." => repeat_last_motion,
"~" => switch_case, "~" => switch_case,
"`" => switch_to_lowercase, "`" => switch_to_lowercase,
@ -427,6 +453,8 @@ impl Default for Keymaps {
"m" => goto_window_middle, "m" => goto_window_middle,
"b" => goto_window_bottom, "b" => goto_window_bottom,
"a" => goto_last_accessed_file, "a" => goto_last_accessed_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
}, },
":" => command_mode, ":" => command_mode,
@ -476,10 +504,9 @@ impl Default for Keymaps {
}, },
"/" => search, "/" => search,
// ? for search_reverse "?" => rsearch,
"n" => search_next, "n" => search_next,
"N" => extend_search_next, "N" => search_prev,
// N for search_prev
"*" => search_selection, "*" => search_selection,
"u" => undo, "u" => undo,
@ -520,9 +547,13 @@ impl Default for Keymaps {
"C-w" => { "Window" "C-w" => { "Window"
"C-w" | "w" => rotate_view, "C-w" | "w" => rotate_view,
"C-h" | "h" => hsplit, "C-s" | "s" => hsplit,
"C-v" | "v" => vsplit, "C-v" | "v" => vsplit,
"C-q" | "q" => wclose, "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 // move under <space>c
@ -621,6 +652,9 @@ impl Default for Keymaps {
"B" => extend_prev_long_word_start, "B" => extend_prev_long_word_start,
"E" => extend_next_long_word_end, "E" => extend_next_long_word_end,
"n" => extend_search_next,
"N" => extend_search_prev,
"t" => extend_till_char, "t" => extend_till_char,
"f" => extend_next_char, "f" => extend_next_char,
"T" => extend_till_prev_char, "T" => extend_till_prev_char,
@ -669,8 +703,11 @@ pub fn merge_keys(mut config: Config) -> Config {
config config
} }
#[test] #[cfg(test)]
fn merge_partial_keys() { mod tests {
use super::*;
#[test]
fn merge_partial_keys() {
let config = Config { let config = Config {
keys: Keymaps(hashmap! { keys: Keymaps(hashmap! {
Mode::Normal => Keymap::new( Mode::Normal => Keymap::new(
@ -728,4 +765,39 @@ fn merge_partial_keys() {
assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1); assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0); 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 // 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() let file_config = fern::Dispatch::new()
.format(|out, message, record| { .format(|out, message, record| {
out.finish(format_args!( out.finish(format_args!(
@ -26,7 +31,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message message
)) ))
}) })
.chain(fern::log_file(logpath)?); .chain(file);
base_config.chain(file_config).apply()?; base_config.chain(file_config).apply()?;
@ -55,6 +60,7 @@ ARGS:
FLAGS: FLAGS:
-h, --help Prints help information -h, --help Prints help information
--tutor Loads the tutorial
-v Increases logging verbosity each use for up to 3 times -v Increases logging verbosity each use for up to 3 times
(default file: {}) (default file: {})
-V, --version Prints version information -V, --version Prints version information

@ -5,7 +5,7 @@ use tui::buffer::Buffer as Surface;
use std::borrow::Cow; use std::borrow::Cow;
use helix_core::Transaction; use helix_core::Transaction;
use helix_view::{graphics::Rect, Document, Editor, View}; use helix_view::{graphics::Rect, Document, Editor};
use crate::commands; use crate::commands;
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent}; use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
@ -30,31 +30,32 @@ impl menu::Item for CompletionItem {
menu::Row::new(vec![ menu::Row::new(vec![
menu::Cell::from(self.label.as_str()), menu::Cell::from(self.label.as_str()),
menu::Cell::from(match self.kind { menu::Cell::from(match self.kind {
Some(lsp::CompletionItemKind::Text) => "text", Some(lsp::CompletionItemKind::TEXT) => "text",
Some(lsp::CompletionItemKind::Method) => "method", Some(lsp::CompletionItemKind::METHOD) => "method",
Some(lsp::CompletionItemKind::Function) => "function", Some(lsp::CompletionItemKind::FUNCTION) => "function",
Some(lsp::CompletionItemKind::Constructor) => "constructor", Some(lsp::CompletionItemKind::CONSTRUCTOR) => "constructor",
Some(lsp::CompletionItemKind::Field) => "field", Some(lsp::CompletionItemKind::FIELD) => "field",
Some(lsp::CompletionItemKind::Variable) => "variable", Some(lsp::CompletionItemKind::VARIABLE) => "variable",
Some(lsp::CompletionItemKind::Class) => "class", Some(lsp::CompletionItemKind::CLASS) => "class",
Some(lsp::CompletionItemKind::Interface) => "interface", Some(lsp::CompletionItemKind::INTERFACE) => "interface",
Some(lsp::CompletionItemKind::Module) => "module", Some(lsp::CompletionItemKind::MODULE) => "module",
Some(lsp::CompletionItemKind::Property) => "property", Some(lsp::CompletionItemKind::PROPERTY) => "property",
Some(lsp::CompletionItemKind::Unit) => "unit", Some(lsp::CompletionItemKind::UNIT) => "unit",
Some(lsp::CompletionItemKind::Value) => "value", Some(lsp::CompletionItemKind::VALUE) => "value",
Some(lsp::CompletionItemKind::Enum) => "enum", Some(lsp::CompletionItemKind::ENUM) => "enum",
Some(lsp::CompletionItemKind::Keyword) => "keyword", Some(lsp::CompletionItemKind::KEYWORD) => "keyword",
Some(lsp::CompletionItemKind::Snippet) => "snippet", Some(lsp::CompletionItemKind::SNIPPET) => "snippet",
Some(lsp::CompletionItemKind::Color) => "color", Some(lsp::CompletionItemKind::COLOR) => "color",
Some(lsp::CompletionItemKind::File) => "file", Some(lsp::CompletionItemKind::FILE) => "file",
Some(lsp::CompletionItemKind::Reference) => "reference", Some(lsp::CompletionItemKind::REFERENCE) => "reference",
Some(lsp::CompletionItemKind::Folder) => "folder", Some(lsp::CompletionItemKind::FOLDER) => "folder",
Some(lsp::CompletionItemKind::EnumMember) => "enum_member", Some(lsp::CompletionItemKind::ENUM_MEMBER) => "enum_member",
Some(lsp::CompletionItemKind::Constant) => "constant", Some(lsp::CompletionItemKind::CONSTANT) => "constant",
Some(lsp::CompletionItemKind::Struct) => "struct", Some(lsp::CompletionItemKind::STRUCT) => "struct",
Some(lsp::CompletionItemKind::Event) => "event", Some(lsp::CompletionItemKind::EVENT) => "event",
Some(lsp::CompletionItemKind::Operator) => "operator", Some(lsp::CompletionItemKind::OPERATOR) => "operator",
Some(lsp::CompletionItemKind::TypeParameter) => "type_param", Some(lsp::CompletionItemKind::TYPE_PARAMETER) => "type_param",
Some(kind) => unimplemented!("{:?}", kind),
None => "", None => "",
}), }),
// self.detail.as_deref().unwrap_or("") // self.detail.as_deref().unwrap_or("")
@ -83,13 +84,13 @@ impl Completion {
start_offset: usize, start_offset: usize,
trigger_offset: usize, trigger_offset: usize,
) -> Self { ) -> Self {
// let items: Vec<CompletionItem> = Vec::new();
let menu = Menu::new(items, move |editor: &mut Editor, item, event| { let menu = Menu::new(items, move |editor: &mut Editor, item, event| {
fn item_to_transaction( fn item_to_transaction(
doc: &Document, doc: &Document,
view: &View,
item: &CompletionItem, item: &CompletionItem,
offset_encoding: helix_lsp::OffsetEncoding, offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize,
trigger_offset: usize,
) -> Transaction { ) -> Transaction {
if let Some(edit) = &item.text_edit { if let Some(edit) = &item.text_edit {
let edit = match edit { let edit = match edit {
@ -105,63 +106,52 @@ impl Completion {
) )
} else { } else {
let text = item.insert_text.as_ref().unwrap_or(&item.label); let text = item.insert_text.as_ref().unwrap_or(&item.label);
let cursor = doc // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯
.selection(view.id) // in these cases we need to check for a common prefix and remove it
.primary() let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset));
.cursor(doc.text().slice(..)); let text = text.trim_start_matches::<&str>(&prefix);
Transaction::change( Transaction::change(
doc.text(), 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 { match event {
PromptEvent::Abort => {} PromptEvent::Abort => {}
PromptEvent::Update => { PromptEvent::Update => {
let (view, doc) = current!(editor);
// always present here // always present here
let item = item.unwrap(); let item = item.unwrap();
// if more text was entered, remove it let transaction = item_to_transaction(
// TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes doc,
let cursor = doc item,
.selection(view.id) offset_encoding,
.primary() start_offset,
.cursor(doc.text().slice(..)); trigger_offset,
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(),
vec![(trigger_offset, cursor, None)].into_iter(),
); );
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); doc.apply(&transaction, view.id);
} }
PromptEvent::Validate => { PromptEvent::Validate => {
let (view, doc) = current!(editor);
// always present here // always present here
let item = item.unwrap(); let item = item.unwrap();
// if more text was entered, remove it let transaction = item_to_transaction(
// TODO: ideally to undo we should keep the last completion tx revert, and map it over new changes doc,
let cursor = doc item,
.selection(view.id) offset_encoding,
.primary() start_offset,
.cursor(doc.text().slice(..)); trigger_offset,
if trigger_offset < cursor {
let remove = Transaction::change(
doc.text(),
vec![(trigger_offset, cursor, None)].into_iter(),
); );
doc.apply(&remove, view.id);
}
let transaction = item_to_transaction(doc, view, item, offset_encoding);
doc.apply(&transaction, view.id); doc.apply(&transaction, view.id);
if let Some(additional_edits) = &item.additional_text_edits { if let Some(additional_edits) = &item.additional_text_edits {
@ -210,7 +200,7 @@ impl Completion {
.selection(view.id) .selection(view.id)
.primary() .primary()
.cursor(doc.text().slice(..)); .cursor(doc.text().slice(..));
if self.start_offset <= cursor { if self.trigger_offset <= cursor {
let fragment = doc.text().slice(self.start_offset..cursor); let fragment = doc.text().slice(self.start_offset..cursor);
let text = Cow::from(fragment); let text = Cow::from(fragment);
// TODO: logic is same as ui/picker // TODO: logic is same as ui/picker
@ -274,12 +264,10 @@ impl Component for Completion {
.language() .language()
.and_then(|scope| scope.strip_prefix("source.")) .and_then(|scope| scope.strip_prefix("source."))
.unwrap_or(""); .unwrap_or("");
let cursor_pos = doc let text = doc.text().slice(..);
.selection(view.id) let cursor_pos = doc.selection(view.id).primary().cursor(text);
.primary() let coords = helix_core::visual_coords_at_pos(text, cursor_pos, doc.tab_width());
.cursor(doc.text().slice(..)); let cursor_pos = (coords.row - view.offset.row) as u16;
let cursor_pos = (helix_core::coords_at_pos(doc.text().slice(..), cursor_pos).row
- view.offset.row) as u16;
let mut markdown_doc = match &option.documentation { let mut markdown_doc = match &option.documentation {
Some(lsp::Documentation::String(contents)) Some(lsp::Documentation::String(contents))
| Some(lsp::Documentation::MarkupContent(lsp::MarkupContent { | Some(lsp::Documentation::MarkupContent(lsp::MarkupContent {

@ -689,6 +689,8 @@ impl EditorView {
theme: &Theme, theme: &Theme,
is_focused: bool, is_focused: bool,
) { ) {
use tui::text::{Span, Spans};
//------------------------------- //-------------------------------
// Left side of the status line. // Left side of the status line.
//------------------------------- //-------------------------------
@ -707,17 +709,17 @@ impl EditorView {
}) })
.unwrap_or(""); .unwrap_or("");
let style = if is_focused { let base_style = if is_focused {
theme.get("ui.statusline") theme.get("ui.statusline")
} else { } else {
theme.get("ui.statusline.inactive") theme.get("ui.statusline.inactive")
}; };
// statusline // statusline
surface.set_style(viewport.with_height(1), style); surface.set_style(viewport.with_height(1), base_style);
if is_focused { 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() { if let Some(path) = doc.relative_path() {
let path = path.to_string_lossy(); let path = path.to_string_lossy();
@ -728,7 +730,7 @@ impl EditorView {
viewport.y, viewport.y,
title, title,
viewport.width.saturating_sub(6) as usize, viewport.width.saturating_sub(6) as usize,
style, base_style,
); );
} }
@ -736,8 +738,50 @@ impl EditorView {
// Right side of the status line. // Right side of the status line.
//------------------------------- //-------------------------------
// Compute the individual info strings. let mut right_side_text = Spans::default();
let diag_count = format!("{}", doc.diagnostics().len());
// 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 { // let indent_info = match doc.indent_style {
// IndentStyle::Tabs => "tabs", // IndentStyle::Tabs => "tabs",
// IndentStyle::Spaces(1) => "spaces:1", // IndentStyle::Spaces(1) => "spaces:1",
@ -750,29 +794,28 @@ impl EditorView {
// IndentStyle::Spaces(8) => "spaces:8", // IndentStyle::Spaces(8) => "spaces:8",
// _ => "indent:ERROR", // _ => "indent:ERROR",
// }; // };
let position_info = {
// Position
let pos = coords_at_pos( let pos = coords_at_pos(
doc.text().slice(..), doc.text().slice(..),
doc.selection(view.id) doc.selection(view.id)
.primary() .primary()
.cursor(doc.text().slice(..)), .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!(
"{} {} ", // Render to the statusline.
&diag_count[..diag_count.len().min(4)], surface.set_spans(
// indent_info, viewport.x
position_info + viewport
); .width
let text_len = right_side_text.len() as u16; .saturating_sub(right_side_text.width() as u16),
surface.set_string(
viewport.x + viewport.width.saturating_sub(text_len),
viewport.y, viewport.y,
right_side_text, &right_side_text,
style, right_side_text.width() as u16,
); );
} }
@ -984,7 +1027,7 @@ impl EditorView {
pub fn set_completion( pub fn set_completion(
&mut self, &mut self,
editor: &Editor, editor: &mut Editor,
items: Vec<helix_lsp::lsp::CompletionItem>, items: Vec<helix_lsp::lsp::CompletionItem>,
offset_encoding: helix_lsp::OffsetEncoding, offset_encoding: helix_lsp::OffsetEncoding,
start_offset: usize, start_offset: usize,
@ -999,10 +1042,21 @@ impl EditorView {
return; return;
} }
// Immediately initialize a savepoint
doc_mut!(editor).savepoint();
// TODO : propagate required size on resize to completion too // TODO : propagate required size on resize to completion too
completion.required_size((size.width, size.height)); completion.required_size((size.width, size.height));
self.completion = Some(completion); 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 { impl EditorView {
@ -1022,12 +1076,12 @@ impl EditorView {
let editor = &mut cxt.editor; let editor = &mut cxt.editor;
let result = editor.tree.views().find_map(|(view, _focus)| { 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)) .map(|pos| (pos, view.id))
}); });
if let Some((pos, view_id)) = result { 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 { if modifiers == crossterm::event::KeyModifiers::ALT {
let selection = doc.selection(view_id).clone(); let selection = doc.selection(view_id).clone();
@ -1096,7 +1150,7 @@ impl EditorView {
}; };
let result = cxt.editor.tree.views().find_map(|(view, _focus)| { 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) .map(|_| view.id)
}); });
@ -1182,12 +1236,12 @@ impl EditorView {
} }
let result = editor.tree.views().find_map(|(view, _focus)| { 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)) .map(|pos| (pos, view.id))
}); });
if let Some((pos, view_id)) = result { 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)); doc.set_selection(view_id, Selection::point(pos));
editor.tree.focus = view_id; editor.tree.focus = view_id;
commands::Command::paste_primary_clipboard_before.execute(cxt); commands::Command::paste_primary_clipboard_before.execute(cxt);
@ -1254,8 +1308,7 @@ impl Component for EditorView {
if callback.is_some() { if callback.is_some() {
// assume close_fn // assume close_fn
self.completion = None; self.clear_completion(cxt.editor);
cxt.editor.clear_idle_timer(); // don't retrigger
} }
} }
} }
@ -1268,8 +1321,7 @@ impl Component for EditorView {
if let Some(completion) = &mut self.completion { if let Some(completion) = &mut self.completion {
completion.update(&mut cxt); completion.update(&mut cxt);
if completion.is_empty() { if completion.is_empty() {
self.completion = None; self.clear_completion(cxt.editor);
cxt.editor.clear_idle_timer(); // don't retrigger
} }
} }
} }
@ -1397,9 +1449,11 @@ impl Component for EditorView {
info.render(area, surface, cx); info.render(area, surface, cx);
} }
if cx.editor.config.auto_info {
if let Some(ref mut info) = self.autoinfo { if let Some(ref mut info) = self.autoinfo {
info.render(area, surface, cx); info.render(area, surface, cx);
} }
}
let key_width = 15u16; // for showing pending keys let key_width = 15u16; // for showing pending keys
let mut status_msg_width = 0; let mut status_msg_width = 0;
@ -1469,7 +1523,7 @@ fn canonicalize_key(key: &mut KeyEvent) {
} }
#[inline] #[inline]
fn abs_diff(a: usize, b: usize) -> usize { const fn abs_diff(a: usize, b: usize) -> usize {
if a > b { if a > b {
a - b a - b
} else { } else {

@ -64,25 +64,23 @@ impl<T: Item> Menu<T> {
} }
pub fn score(&mut self, pattern: &str) { 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 // reuse the matches allocation
matches.clear(); self.matches.clear();
matches.extend(options.iter().enumerate().filter_map(|(index, option)| { self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
let text = option.filter_text(); let text = option.filter_text();
// TODO: using fuzzy_indices could give us the char idx for match highlighting // TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher self.matcher
.fuzzy_match(text, pattern) .fuzzy_match(text, pattern)
.map(|score| (index, score)) .map(|score| (index, score))
})); }),
);
// matches.sort_unstable_by_key(|(_, score)| -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 // reset cursor position
self.cursor = None; self.cursor = None;
@ -100,7 +98,8 @@ impl<T: Item> Menu<T> {
pub fn move_up(&mut self) { pub fn move_up(&mut self) {
let len = self.matches.len(); 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.cursor = Some(pos);
self.adjust_scroll(); self.adjust_scroll();
} }
@ -216,6 +215,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent { | KeyEvent {
code: KeyCode::Char('p'), code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => { } => {
self.move_up(); self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@ -233,6 +236,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent { | KeyEvent {
code: KeyCode::Char('n'), code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
} => { } => {
self.move_down(); self.move_down();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);

@ -29,6 +29,7 @@ pub fn regex_prompt(
cx: &mut crate::commands::Context, cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>, prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>, history_register: Option<char>,
completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static, fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
) -> Prompt { ) -> Prompt {
let (view, doc) = current!(cx.editor); let (view, doc) = current!(cx.editor);
@ -38,7 +39,7 @@ pub fn regex_prompt(
Prompt::new( Prompt::new(
prompt, prompt,
history_register, 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| { move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event { match event {
PromptEvent::Abort => { PromptEvent::Abort => {
@ -92,9 +93,25 @@ pub fn regex_prompt(
} }
pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> { pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
use ignore::Walk; use ignore::{types::TypesBuilder, WalkBuilder};
use std::time; 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()?; let entry = entry.ok()?;
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir // Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
if entry.path().is_dir() { if entry.path().is_dir() {

@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher; use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget; 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 crate::ui::{Prompt, PromptEvent};
use helix_core::Position; use helix_core::Position;
@ -23,18 +28,58 @@ use helix_view::{
}; };
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80; 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)>); type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> { pub struct FilePicker<T> {
picker: Picker<T>, picker: Picker<T>,
/// Caches paths to documents /// 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. /// Given an item in the picker, return the file path and line number to display.
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>, 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> { impl<T> FilePicker<T> {
pub fn new( pub fn new(
options: Vec<T>, options: Vec<T>,
@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
Self { Self {
picker: Picker::new(false, options, format_fn, callback_fn), picker: Picker::new(false, options, format_fn, callback_fn),
preview_cache: HashMap::new(), preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn), file_fn: Box::new(preview_fn),
} }
} }
@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
}) })
} }
fn calculate_preview(&mut self, editor: &Editor) { /// Get (cached) preview for a given path. If a document corresponding
if let Some((path, _line)) = self.current_file(editor) { /// to the path is already open in the editor, it is used instead.
if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() { fn get_preview<'picker, 'editor>(
// TODO: enable syntax highlighting; blocked by async rendering &'picker mut self,
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap(); path: &Path,
self.preview_cache.insert(path, doc); 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
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 @@ impl<T: 'static> Component for FilePicker<T> {
// |picker | | | // |picker | | |
// | | | | // | | | |
// +---------+ +---------+ // +---------+ +---------+
self.calculate_preview(cx.editor);
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW; let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
let area = inner_rect(area); let area = inner_rect(area);
// -- Render the frame: // -- Render the frame:
// clear area // clear area
let background = cx.editor.theme.get("ui.background"); let background = cx.editor.theme.get("ui.background");
let text = cx.editor.theme.get("ui.text");
surface.clear_with(area, background); surface.clear_with(area, background);
let picker_width = if render_preview { let picker_width = if render_preview {
@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
horizontal: 1, horizontal: 1,
}; };
let inner = inner.inner(&margin); let inner = inner.inner(&margin);
block.render(preview_area, surface); block.render(preview_area, surface);
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| { if let Some((path, range)) = self.current_file(cx.editor) {
cx.editor let preview = self.get_preview(&path, cx.editor);
.document_by_path(&path) let doc = match preview.document() {
.or_else(|| self.preview_cache.get(&path)) Some(doc) => doc,
.zip(Some(range)) 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 // align to middle
let first_line = line let first_line = range
.map(|(start, end)| { .map(|(start, end)| {
let height = end.saturating_sub(start) + 1; let height = end.saturating_sub(start) + 1;
let middle = start + (height.saturating_sub(1) / 2); let middle = start + (height.saturating_sub(1) / 2);
@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
); );
// highlight the line // highlight the line
if let Some((start, end)) = line { if let Some((start, end)) = range {
let offset = start.saturating_sub(first_line) as u16; let offset = start.saturating_sub(first_line) as u16;
surface.set_style( surface.set_style(
Rect::new( Rect::new(
@ -234,37 +317,28 @@ impl<T> Picker<T> {
} }
pub fn score(&mut self) { 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; let pattern = &self.prompt.line;
// reuse the matches allocation // reuse the matches allocation
matches.clear(); self.matches.clear();
matches.extend( self.matches.extend(
self.options self.options
.iter() .iter()
.enumerate() .enumerate()
.filter_map(|(index, option)| { .filter_map(|(index, option)| {
// filter options first before matching // filter options first before matching
if !filters.is_empty() { if !self.filters.is_empty() {
filters.binary_search(&index).ok()?; self.filters.binary_search(&index).ok()?;
} }
// TODO: maybe using format_fn isn't the best idea here // 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 // TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher self.matcher
.fuzzy_match(&text, pattern) .fuzzy_match(&text, pattern)
.map(|score| (index, score)) .map(|score| (index, score))
}), }),
); );
matches.sort_unstable_by_key(|(_, score)| -score); self.matches.sort_unstable_by_key(|(_, score)| -score);
// reset cursor position // reset cursor position
self.cursor = 0; self.cursor = 0;
@ -337,6 +411,10 @@ impl<T: 'static> Component for Picker<T> {
code: KeyCode::BackTab, code: KeyCode::BackTab,
.. ..
} }
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent { | KeyEvent {
code: KeyCode::Char('p'), code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
@ -350,6 +428,10 @@ impl<T: 'static> Component for Picker<T> {
| KeyEvent { | KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
} }
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent { | KeyEvent {
code: KeyCode::Char('n'), code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
@ -375,7 +457,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn; return close_fn;
} }
KeyEvent { KeyEvent {
code: KeyCode::Char('h'), code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,
} => { } => {
if let Some(option) = self.selection() { if let Some(option) = self.selection() {
@ -485,6 +567,7 @@ impl<T: 'static> Component for Picker<T> {
text_style text_style
}, },
true, true,
true,
); );
} }
} }

@ -186,6 +186,11 @@ impl Prompt {
self.exit_selection(); 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) { pub fn move_cursor(&mut self, movement: Movement) {
let pos = self.eval_movement(movement); let pos = self.eval_movement(movement);
self.cursor = pos self.cursor = pos
@ -474,6 +479,26 @@ impl Component for Prompt {
self.delete_char_backwards(); self.delete_char_backwards();
(self.callback_fn)(cx, &self.line, PromptEvent::Update); (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 { KeyEvent {
code: KeyCode::Enter, code: KeyCode::Enter,
.. ..
@ -502,6 +527,7 @@ impl Component for Prompt {
if let Some(register) = self.history_register { if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register); let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Backward); self.change_history(register.read(), CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
} }
KeyEvent { KeyEvent {
@ -515,15 +541,22 @@ impl Component for Prompt {
if let Some(register) = self.history_register { if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register); let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Forward); self.change_history(register.read(), CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
} }
} }
KeyEvent { KeyEvent {
code: KeyCode::Tab, .. code: KeyCode::Tab, ..
} => self.change_completion_selection(CompletionDirection::Forward), } => {
self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
KeyEvent { KeyEvent {
code: KeyCode::BackTab, code: KeyCode::BackTab,
.. ..
} => self.change_completion_selection(CompletionDirection::Backward), } => {
self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
KeyEvent { KeyEvent {
code: KeyCode::Char('q'), code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL, modifiers: KeyModifiers::CONTROL,

@ -1,11 +1,11 @@
[package] [package]
name = "helix-tui" name = "helix-tui"
version = "0.4.1" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
description = """ description = """
A library to build rich terminal user interfaces or dashboards A library to build rich terminal user interfaces or dashboards
""" """
edition = "2018" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
categories = ["editor"] categories = ["editor"]
repository = "https://github.com/helix-editor/helix" repository = "https://github.com/helix-editor/helix"
@ -19,7 +19,7 @@ default = ["crossterm"]
bitflags = "1.3" bitflags = "1.3"
cassowary = "0.3" cassowary = "0.3"
unicode-segmentation = "1.8" unicode-segmentation = "1.8"
crossterm = { version = "0.21", optional = true } crossterm = { version = "0.22", optional = true }
serde = { version = "1", "optional" = true, features = ["derive"]} serde = { version = "1", "optional" = true, features = ["derive"]}
helix-view = { version = "0.4", path = "../helix-view", features = ["term"] } helix-view = { version = "0.5", path = "../helix-view", features = ["term"] }
helix-core = { version = "0.4", path = "../helix-core" } helix-core = { version = "0.5", path = "../helix-core" }

@ -266,12 +266,14 @@ impl Buffer {
where where
S: AsRef<str>, 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 /// 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 /// until the end of the line. If `ellipsis` is true appends a `…` at the end of
/// truncated lines. /// 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>( pub fn set_string_truncated<S>(
&mut self, &mut self,
x: u16, x: u16,
@ -280,6 +282,7 @@ impl Buffer {
width: usize, width: usize,
style: Style, style: Style,
ellipsis: bool, ellipsis: bool,
truncate_start: bool,
) -> (u16, u16) ) -> (u16, u16)
where where
S: AsRef<str>, S: AsRef<str>,
@ -289,6 +292,7 @@ impl Buffer {
let width = if ellipsis { width - 1 } else { width }; let width = if ellipsis { width - 1 } else { width };
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true); let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize)); let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
if !truncate_start {
for s in graphemes { for s in graphemes {
let width = s.width(); let width = s.width();
if width == 0 { if width == 0 {
@ -312,6 +316,36 @@ impl Buffer {
if ellipsis && x_offset - (x as usize) < string.as_ref().width() { if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
self.content[index].set_symbol("…"); 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) (x_offset as u16, y)
} }

@ -1,8 +1,8 @@
[package] [package]
name = "helix-view" name = "helix-view"
version = "0.4.1" version = "0.5.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"] authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
edition = "2018" edition = "2021"
license = "MPL-2.0" license = "MPL-2.0"
description = "UI abstractions for use in backends" description = "UI abstractions for use in backends"
categories = ["editor"] categories = ["editor"]
@ -16,10 +16,10 @@ term = ["crossterm"]
[dependencies] [dependencies]
bitflags = "1.3" bitflags = "1.3"
anyhow = "1" anyhow = "1"
helix-core = { version = "0.4", path = "../helix-core" } helix-core = { version = "0.5", path = "../helix-core" }
helix-lsp = { version = "0.4", path = "../helix-lsp"} helix-lsp = { version = "0.5", path = "../helix-lsp"}
helix-dap = { version = "0.4", path = "../helix-dap"} helix-dap = { version = "0.5", path = "../helix-dap"}
crossterm = { version = "0.21", optional = true } crossterm = { version = "0.22", optional = true }
# Conversion traits # Conversion traits
once_cell = "1.8" once_cell = "1.8"

@ -116,7 +116,7 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
} }
} else { } else {
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
return Box::new(provider::WindowsProvider::new()); return Box::new(provider::WindowsProvider::default());
#[cfg(not(target_os = "windows"))] #[cfg(not(target_os = "windows"))]
return Box::new(provider::NopProvider::new()); return Box::new(provider::NopProvider::new());
@ -145,15 +145,15 @@ mod provider {
use anyhow::{bail, Context as _, Result}; use anyhow::{bail, Context as _, Result};
use std::borrow::Cow; use std::borrow::Cow;
#[cfg(not(target_os = "windows"))]
#[derive(Debug)] #[derive(Debug)]
pub struct NopProvider { pub struct NopProvider {
buf: String, buf: String,
primary_buf: String, primary_buf: String,
} }
#[cfg(not(target_os = "windows"))]
impl NopProvider { impl NopProvider {
#[allow(dead_code)]
// Only dead_code on Windows.
pub fn new() -> Self { pub fn new() -> Self {
Self { Self {
buf: String::new(), buf: String::new(),
@ -162,6 +162,7 @@ mod provider {
} }
} }
#[cfg(not(target_os = "windows"))]
impl ClipboardProvider for NopProvider { impl ClipboardProvider for NopProvider {
fn name(&self) -> Cow<str> { fn name(&self) -> Cow<str> {
Cow::Borrowed("none") Cow::Borrowed("none")
@ -186,19 +187,8 @@ mod provider {
} }
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
#[derive(Debug)] #[derive(Default, Debug)]
pub struct WindowsProvider { pub struct WindowsProvider;
selection_buf: String,
}
#[cfg(target_os = "windows")]
impl WindowsProvider {
pub fn new() -> Self {
Self {
selection_buf: String::new(),
}
}
}
#[cfg(target_os = "windows")] #[cfg(target_os = "windows")]
impl ClipboardProvider for WindowsProvider { impl ClipboardProvider for WindowsProvider {

@ -23,6 +23,8 @@ use crate::{DocumentId, Theme, ViewId};
/// 8kB of buffer space for encoding and decoding `Rope`s. /// 8kB of buffer space for encoding and decoding `Rope`s.
const BUF_SIZE: usize = 8192; const BUF_SIZE: usize = 8192;
const DEFAULT_INDENT: IndentStyle = IndentStyle::Spaces(4);
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)] #[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
pub enum Mode { pub enum Mode {
Normal, 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 // it back as it separated from the edits. We could split out the parts manually but that will
// be more troublesome. // be more troublesome.
history: Cell<History>, history: Cell<History>,
pub savepoint: Option<Transaction>,
last_saved_revision: usize, last_saved_revision: usize,
version: i32, // should be usize? version: i32, // should be usize?
@ -306,8 +311,7 @@ where
T: Default, T: Default,
F: FnOnce(T) -> T, F: FnOnce(T) -> T,
{ {
let t = mem::take(mut_ref); *mut_ref = f(mem::take(mut_ref));
let _ = mem::replace(mut_ref, f(t));
} }
use helix_lsp::lsp; use helix_lsp::lsp;
@ -325,7 +329,8 @@ impl Document {
encoding, encoding,
text, text,
selections: HashMap::default(), selections: HashMap::default(),
indent_style: IndentStyle::Spaces(4), indent_style: DEFAULT_INDENT,
line_ending: DEFAULT_LINE_ENDING,
mode: Mode::Normal, mode: Mode::Normal,
restore_cursor: false, restore_cursor: false,
syntax: None, syntax: None,
@ -335,9 +340,9 @@ impl Document {
diagnostics: Vec::new(), diagnostics: Vec::new(),
version: 0, version: 0,
history: Cell::new(History::default()), history: Cell::new(History::default()),
savepoint: None,
last_saved_revision: 0, last_saved_revision: 0,
language_server: None, language_server: None,
line_ending: DEFAULT_LINE_ENDING,
} }
} }
@ -363,7 +368,7 @@ impl Document {
let mut doc = Self::from(rope, Some(encoding)); let mut doc = Self::from(rope, Some(encoding));
// set the path and try detecting the language // set the path and try detecting the language
doc.set_path(path)?; doc.set_path(Some(path))?;
if let Some(loader) = config_loader { if let Some(loader) = config_loader {
doc.detect_language(theme, loader); doc.detect_language(theme, loader);
} }
@ -495,17 +500,15 @@ impl Document {
} }
/// Detect the indentation used in the file, or otherwise defaults to the language indentation /// 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 /// specified. Line ending is likewise auto-detected, and will fallback to the default OS
/// line ending. /// line ending.
pub fn detect_indent_and_line_ending(&mut self) { pub fn detect_indent_and_line_ending(&mut self) {
self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| { self.indent_style = auto_detect_indent_style(&self.text).unwrap_or_else(|| {
IndentStyle::from_str(
self.language self.language
.as_ref() .as_ref()
.and_then(|config| config.indent.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); self.line_ending = auto_detect_line_ending(&self.text).unwrap_or(DEFAULT_LINE_ENDING);
} }
@ -550,12 +553,14 @@ impl Document {
self.encoding self.encoding
} }
pub fn set_path(&mut self, path: &Path) -> Result<(), std::io::Error> { pub fn set_path(&mut self, path: Option<&Path>) -> Result<(), std::io::Error> {
let path = helix_core::path::get_canonicalized_path(path)?; let path = path
.map(helix_core::path::get_canonicalized_path)
.transpose()?;
// if parent doesn't exist we still want to open the document // if parent doesn't exist we still want to open the document
// and error out when document is saved // and error out when document is saved
self.path = Some(path); self.path = path;
Ok(()) Ok(())
} }
@ -635,6 +640,14 @@ impl Document {
if !transaction.changes().is_empty() { if !transaction.changes().is_empty() {
self.version += 1; 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 // update tree-sitter syntax tree
if let Some(syntax) = &mut self.syntax { if let Some(syntax) = &mut self.syntax {
// TODO: no unwrap // TODO: no unwrap
@ -644,14 +657,13 @@ impl Document {
} }
// map state.diagnostics over changes::map_pos too // 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 {
// for diagnostic in &mut self.diagnostics { use helix_core::Assoc;
// use helix_core::Assoc; let changes = transaction.changes();
// let changes = transaction.changes(); diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After);
// diagnostic.range.start = changes.map_pos(diagnostic.range.start, Assoc::After); diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After);
// diagnostic.range.end = changes.map_pos(diagnostic.range.end, Assoc::After); diagnostic.line = self.text.char_to_line(diagnostic.range.start);
// diagnostic.line = self.text.char_to_line(diagnostic.range.start); }
// }
// emit lsp notification // emit lsp notification
if let Some(language_server) = self.language_server() { if let Some(language_server) = self.language_server() {
@ -692,8 +704,8 @@ impl Document {
success success
} }
/// Undo the last modification to the [`Document`]. /// Undo the last modification to the [`Document`]. Returns whether the undo was successful.
pub fn undo(&mut self, view_id: ViewId) { pub fn undo(&mut self, view_id: ViewId) -> bool {
let mut history = self.history.take(); let mut history = self.history.take();
let success = if let Some(transaction) = history.undo() { let success = if let Some(transaction) = history.undo() {
self.apply_impl(transaction, view_id) self.apply_impl(transaction, view_id)
@ -706,10 +718,11 @@ impl Document {
// reset changeset to fix len // reset changeset to fix len
self.changes = ChangeSet::new(self.text()); self.changes = ChangeSet::new(self.text());
} }
success
} }
/// Redo the last modification to the [`Document`]. /// Redo the last modification to the [`Document`]. Returns whether the redo was sucessful.
pub fn redo(&mut self, view_id: ViewId) { pub fn redo(&mut self, view_id: ViewId) -> bool {
let mut history = self.history.take(); let mut history = self.history.take();
let success = if let Some(transaction) = history.redo() { let success = if let Some(transaction) = history.redo() {
self.apply_impl(transaction, view_id) self.apply_impl(transaction, view_id)
@ -722,6 +735,17 @@ impl Document {
// reset changeset to fix len // reset changeset to fix len
self.changes = ChangeSet::new(self.text()); 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`. /// Undo modifications to the [`Document`] according to `uk`.
@ -894,6 +918,9 @@ impl Document {
pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) { pub fn set_diagnostics(&mut self, diagnostics: Vec<Diagnostic>) {
self.diagnostics = diagnostics; self.diagnostics = diagnostics;
// sort by range
self.diagnostics
.sort_unstable_by_key(|diagnostic| diagnostic.range);
} }
} }

@ -2,7 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider}, clipboard::{get_clipboard_provider, ClipboardProvider},
graphics::{CursorKind, Rect}, graphics::{CursorKind, Rect},
theme::{self, Theme}, theme::{self, Theme},
tree::Tree, tree::{self, Tree},
Document, DocumentId, View, ViewId, Document, DocumentId, View, ViewId,
}; };
@ -12,6 +12,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{ use std::{
collections::HashMap, collections::HashMap,
collections::BTreeMap,
path::{Path, PathBuf}, path::{Path, PathBuf},
pin::Pin, pin::Pin,
sync::Arc, sync::Arc,
@ -19,8 +20,6 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep}; use tokio::time::{sleep, Duration, Instant, Sleep};
use slotmap::SlotMap;
use anyhow::Error; use anyhow::Error;
pub use helix_core::diagnostic::Severity; 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. /// 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")] #[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
pub idle_timeout: Duration, 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)] #[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@ -92,14 +94,29 @@ impl Default for Config {
auto_pairs: true, auto_pairs: true,
auto_completion: true, auto_completion: true,
idle_timeout: Duration::from_millis(400), 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)] #[derive(Debug)]
pub struct Editor { pub struct Editor {
pub tree: Tree, 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 count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>, pub selected_register: Option<char>,
pub registers: Registers, pub registers: Registers,
@ -124,6 +141,7 @@ pub struct Editor {
pub config: Config, pub config: Config,
pub idle_timer: Pin<Box<Sleep>>, pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>,
} }
#[derive(Debug, Copy, Clone)] #[derive(Debug, Copy, Clone)]
@ -148,7 +166,8 @@ impl Editor {
Self { Self {
tree: Tree::new(area), tree: Tree::new(area),
documents: SlotMap::with_key(), next_document_id: 0,
documents: BTreeMap::new(),
count: None, count: None,
selected_register: None, selected_register: None,
theme: themes.default(), theme: themes.default(),
@ -166,6 +185,7 @@ impl Editor {
clipboard_provider: get_clipboard_provider(), clipboard_provider: get_clipboard_provider(),
status_msg: None, status_msg: None,
idle_timer: Box::pin(sleep(config.idle_timeout)), idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
config, config,
} }
} }
@ -221,7 +241,7 @@ impl Editor {
fn _refresh(&mut self) { fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() { for (view, _) in self.tree.views_mut() {
let doc = &self.documents[view.doc]; let doc = &self.documents[&view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff) view.ensure_cursor_in_view(doc, self.config.scrolloff)
} }
} }
@ -230,22 +250,38 @@ impl Editor {
use crate::tree::Layout; use crate::tree::Layout;
use helix_core::Selection; 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)"); log::error!("cannot switch to document that does not exist (anymore)");
return; return;
} }
match action { match action {
Action::Replace => { Action::Replace => {
let view = view!(self); let (view, doc) = current_ref!(self);
let jump = ( // If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
view.doc, // Boolean value is determined before the call to `view_mut` because the operation requires a borrow
self.documents[view.doc].selection(view.id).clone(), // 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); 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.jumps.push(jump);
view.last_accessed_doc = Some(view.doc); view.last_accessed_doc = Some(view.doc);
}
view.doc = id; view.doc = id;
view.offset = Position::default(); view.offset = Position::default();
@ -272,14 +308,14 @@ impl Editor {
let view = View::new(id); let view = View::new(id);
let view_id = self.tree.split(view, Layout::Horizontal); let view_id = self.tree.split(view, Layout::Horizontal);
// initialize selection for view // 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)); doc.selections.insert(view_id, Selection::point(0));
} }
Action::VerticalSplit => { Action::VerticalSplit => {
let view = View::new(id); let view = View::new(id);
let view_id = self.tree.split(view, Layout::Vertical); let view_id = self.tree.split(view, Layout::Vertical);
// initialize selection for view // 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)); doc.selections.insert(view_id, Selection::point(0));
} }
} }
@ -288,9 +324,11 @@ impl Editor {
} }
pub fn new_file(&mut self, action: Action) -> DocumentId { pub fn new_file(&mut self, action: Action) -> DocumentId {
let doc = Document::default(); let id = DocumentId(self.next_document_id);
let id = self.documents.insert(doc); self.next_document_id += 1;
self.documents[id].id = id; let mut doc = Document::default();
doc.id = id;
self.documents.insert(id, doc);
self.switch(id, action); self.switch(id, action);
id id
} }
@ -313,7 +351,11 @@ impl Editor {
self.language_servers self.language_servers
.get(language) .get(language)
.map_err(|e| { .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() .ok()
}); });
@ -336,8 +378,10 @@ impl Editor {
doc.set_language_server(Some(language_server)); doc.set_language_server(Some(language_server));
} }
let id = self.documents.insert(doc); let id = DocumentId(self.next_document_id);
self.documents[id].id = id; self.next_document_id += 1;
doc.id = id;
self.documents.insert(id, doc);
id id
}; };
@ -348,16 +392,20 @@ impl Editor {
pub fn close(&mut self, id: ViewId, close_buffer: bool) { pub fn close(&mut self, id: ViewId, close_buffer: bool) {
let view = self.tree.get(self.tree.focus); let view = self.tree.get(self.tree.focus);
// remove selection // remove selection
self.documents[view.doc].selections.remove(&id); self.documents
.get_mut(&view.doc)
.unwrap()
.selections
.remove(&id);
if close_buffer { if close_buffer {
// get around borrowck issues // get around borrowck issues
let doc = &self.documents[view.doc]; let doc = &self.documents[&view.doc];
if let Some(language_server) = doc.language_server() { if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier())); tokio::spawn(language_server.text_document_did_close(doc.identifier()));
} }
self.documents.remove(view.doc); self.documents.remove(&view.doc);
} }
self.tree.remove(id); self.tree.remove(id);
@ -374,24 +422,40 @@ impl Editor {
self.tree.focus_next(); 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 { pub fn should_close(&self) -> bool {
self.tree.is_empty() self.tree.is_empty()
} }
pub fn ensure_cursor_in_view(&mut self, id: ViewId) { pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
let view = self.tree.get_mut(id); 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) view.ensure_cursor_in_view(doc, self.config.scrolloff)
} }
#[inline] #[inline]
pub fn document(&self, id: DocumentId) -> Option<&Document> { pub fn document(&self, id: DocumentId) -> Option<&Document> {
self.documents.get(id) self.documents.get(&id)
} }
#[inline] #[inline]
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> { pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
self.documents.get_mut(id) self.documents.get_mut(&id)
} }
#[inline] #[inline]
@ -416,7 +480,7 @@ impl Editor {
pub fn cursor(&self) -> (Option<Position>, CursorKind) { pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let view = view!(self); let view = view!(self);
let doc = &self.documents[view.doc]; let doc = &self.documents[&view.doc];
let cursor = doc let cursor = doc
.selection(view.id) .selection(view.id)
.primary() .primary()

@ -1,6 +1,6 @@
use crate::input::KeyEvent; use crate::input::KeyEvent;
use helix_core::unicode::width::UnicodeWidthStr; use helix_core::unicode::width::UnicodeWidthStr;
use std::fmt::Write; use std::{collections::BTreeSet, fmt::Write};
#[derive(Debug)] #[derive(Debug)]
/// Info box used in editor. Rendering logic will be in other crate. /// Info box used in editor. Rendering logic will be in other crate.
@ -16,7 +16,7 @@ pub struct Info {
} }
impl 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 let body = body
.into_iter() .into_iter()
.map(|(desc, events)| { .map(|(desc, events)| {

@ -8,7 +8,7 @@ use crate::keyboard::{KeyCode, KeyModifiers};
/// Represents a key event. /// Represents a key event.
// We use a newtype here because we want to customize Deserialize and Display. // 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 struct KeyEvent {
pub code: KeyCode, pub code: KeyCode,
pub modifiers: KeyModifiers, pub modifiers: KeyModifiers,

@ -54,7 +54,7 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
} }
/// Represents a key. /// 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))] #[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum KeyCode { pub enum KeyCode {
/// Backspace key. /// Backspace key.

@ -12,8 +12,10 @@ pub mod theme;
pub mod tree; pub mod tree;
pub mod view; pub mod view;
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Default, Debug)]
pub struct DocumentId(usize);
slotmap::new_key_type! { slotmap::new_key_type! {
pub struct DocumentId;
pub struct ViewId; pub struct ViewId;
} }

@ -13,7 +13,8 @@
macro_rules! current { macro_rules! current {
( $( $editor:ident ).+ ) => {{ ( $( $editor:ident ).+ ) => {{
let view = $crate::view_mut!( $( $editor ).+ ); 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) (view, doc)
}}; }};
} }
@ -56,7 +57,7 @@ macro_rules! doc {
macro_rules! current_ref { macro_rules! current_ref {
( $( $editor:ident ).+ ) => {{ ( $( $editor:ident ).+ ) => {{
let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus); let view = $( $editor ).+ .tree.get($( $editor ).+ .tree.focus);
let doc = &$( $editor ).+ .documents[view.doc]; let doc = &$( $editor ).+ .documents[&view.doc];
(view, doc) (view, doc)
}}; }};
} }

@ -1,6 +1,5 @@
use std::{ use std::{
collections::HashMap, collections::HashMap,
convert::TryFrom,
path::{Path, PathBuf}, path::{Path, PathBuf},
}; };

@ -47,13 +47,21 @@ impl Node {
// TODO: screen coord to container + container coordinate helpers // TODO: screen coord to container + container coordinate helpers
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Layout { pub enum Layout {
Horizontal, Horizontal,
Vertical, Vertical,
// could explore stacked/tabbed // could explore stacked/tabbed
} }
#[derive(Debug, Clone, Copy)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(Debug)] #[derive(Debug)]
pub struct Container { pub struct Container {
layout: Layout, layout: Layout,
@ -150,7 +158,6 @@ impl Tree {
} => container, } => container,
_ => unreachable!(), _ => unreachable!(),
}; };
if container.layout == layout { if container.layout == layout {
// insert node after the current item if there is children already // insert node after the current item if there is children already
let pos = if container.children.is_empty() { let pos = if container.children.is_empty() {
@ -393,6 +400,112 @@ impl Tree {
Traverse::new(self) 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) { pub fn focus_next(&mut self) {
// This function is very dumb, but that's because we don't store any parent links. // 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) // (we'd be able to go parent.next_sibling() recursively until we find something)
@ -420,13 +533,12 @@ impl Tree {
// if found = container -> found = first child // if found = container -> found = first child
// } // }
let iter = self.traverse(); let mut views = self
.traverse()
let mut iter = iter.skip_while(|&(key, _view)| key != self.focus); .skip_while(|&(id, _view)| id != self.focus)
iter.next(); // take the focused value .skip(1); // Skip focused value
if let Some((id, _)) = views.next() {
if let Some((key, _)) = iter.next() { self.focus = id;
self.focus = key;
} else { } else {
// extremely crude, take the first item again // extremely crude, take the first item again
let (key, _) = self.traverse().next().unwrap(); let (key, _) = self.traverse().next().unwrap();
@ -472,3 +584,64 @@ impl<'a> Iterator for Traverse<'a> {
} }
} }
} }
#[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 std::borrow::Cow;
use crate::{graphics::Rect, Document, DocumentId, ViewId}; use crate::{graphics::Rect, Document, DocumentId, ViewId};
use helix_core::{ use helix_core::{
coords_at_pos,
graphemes::{grapheme_width, RopeGraphemes}, graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index, line_ending::line_end_char_index,
Position, RopeSlice, Selection, visual_coords_at_pos, Position, RopeSlice, Selection,
}; };
type Jump = (DocumentId, Selection); type Jump = (DocumentId, Selection);
@ -91,7 +90,10 @@ impl View {
.selection(self.id) .selection(self.id)
.primary() .primary()
.cursor(doc.text().slice(..)); .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 inner_area = self.inner_area();
let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1); let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);

@ -312,7 +312,7 @@ injection-regex = "php"
file-types = ["php"] file-types = ["php"]
roots = [] roots = []
indent = { tab-width = 2, unit = " " } indent = { tab-width = 4, unit = " " }
[[language]] [[language]]
name = "latex" name = "latex"
@ -458,3 +458,12 @@ file-types = ["scm"]
roots = [] roots = []
comment-token = ";" comment-token = ";"
indent = { tab-width = 2, unit = " " } 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 (command_name) @function
(variable_name) @property (variable_name) @variable.other.member
[ [
"case" "case"
@ -31,7 +31,7 @@
(function_definition name: (word) @function) (function_definition name: (word) @function)
(file_descriptor) @number (file_descriptor) @constant.numeric.integer
[ [
(command_substitution) (command_substitution)

@ -20,16 +20,16 @@
] @type.builtin ] @type.builtin
;; Enum ;; Enum
(enum_member_declaration (identifier) @variable.property) (enum_member_declaration (identifier) @variable.other.member)
;; Literals ;; Literals
[ [
(real_literal) (real_literal)
(integer_literal) (integer_literal)
] @number ] @constant.numeric.integer
(character_literal) @constant.character
[ [
(character_literal)
(string_literal) (string_literal)
(verbatim_string_literal) (verbatim_string_literal)
(interpolated_string_text) (interpolated_string_text)
@ -40,8 +40,8 @@
"$@\"" "$@\""
] @string ] @string
(boolean_literal) @constant.builtin.boolean
[ [
(boolean_literal)
(null_literal) (null_literal)
(void_keyword) (void_keyword)
] @constant.builtin ] @constant.builtin
@ -98,7 +98,7 @@
;; Keywords ;; Keywords
(modifier) @keyword (modifier) @keyword
(this_expression) @keyword (this_expression) @keyword
(escape_sequence) @keyword (escape_sequence) @constant.character.escape
[ [
"as" "as"

@ -60,8 +60,8 @@
(system_lib_string) @string (system_lib_string) @string
(null) @constant (null) @constant
(number_literal) @number (number_literal) @constant.numeric.integer
(char_literal) @string (char_literal) @constant.character
(call_expression (call_expression
function: (identifier) @function) function: (identifier) @function)
@ -73,7 +73,7 @@
(preproc_function_def (preproc_function_def
name: (identifier) @function.special) name: (identifier) @function.special)
(field_identifier) @property (field_identifier) @variable.other.member
(statement_identifier) @label (statement_identifier) @label
(type_identifier) @type (type_identifier) @type
(primitive_type) @type (primitive_type) @type

@ -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 ; Functions
(call_expression (call_expression
function: (scoped_identifier function: (qualified_identifier
name: (identifier) @function)) name: (identifier) @function))
(template_function (template_function
@ -13,15 +13,14 @@
name: (field_identifier) @function) name: (field_identifier) @function)
(template_function (template_function
name: (scoped_identifier name: (identifier) @function)
name: (identifier) @function))
(function_declarator (function_declarator
declarator: (scoped_identifier declarator: (qualified_identifier
name: (identifier) @function)) name: (identifier) @function))
(function_declarator (function_declarator
declarator: (scoped_identifier declarator: (qualified_identifier
name: (identifier) @function)) name: (identifier) @function))
(function_declarator (function_declarator

@ -26,11 +26,11 @@
(pseudo_element_selector (tag_name) @attribute) (pseudo_element_selector (tag_name) @attribute)
(pseudo_class_selector (class_name) @attribute) (pseudo_class_selector (class_name) @attribute)
(class_name) @property (class_name) @variable.other.member
(id_name) @property (id_name) @variable.other.member
(namespace_name) @property (namespace_name) @variable.other.member
(property_name) @property (property_name) @variable.other.member
(feature_name) @property (feature_name) @variable.other.member
(attribute_name) @attribute (attribute_name) @attribute
@ -55,8 +55,8 @@
(string_value) @string (string_value) @string
(color_value) @string.special (color_value) @string.special
(integer_value) @number (integer_value) @constant.numeric.integer
(float_value) @number (float_value) @constant.numeric.float
(unit) @type (unit) @type
"#" @punctuation.delimiter "#" @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.
; Reserved keywords
["when" "and" "or" "not" "in" "not in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
; Operators
; * doc string
(unary_operator
operator: "@" @comment.block.documentation
operand: (call
target: (identifier) @comment.block.documentation.__attribute__
(arguments
[
(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
target: (identifier) @variable.other.member)
(boolean) @variable.other.member
(nil) @variable.other.member
])
; * capture operator
(unary_operator
operator: "&"
operand: [
(integer) @operator
(binary_operator
left: [
(call target: (dot left: (_) right: (identifier) @function))
(identifier) @function
] operator: "/" right: (integer) @operator)
])
[(true) (false) (nil)] @constant.builtin (operator_identifier) @operator
(keyword (unary_operator
[(keyword_literal) operator: _ @operator)
":"] @tag)
(keyword (binary_operator
(keyword_string operator: _ @operator)
[(string_start)
(string_content)
(string_end)] @tag))
[(atom_literal) (dot
(atom_start) operator: _ @operator)
(atom_content)
(atom_end)] @tag
[(comment) (stab_clause
(unused_identifier)] @comment operator: _ @operator)
(escape_sequence) @escape ; Literals
(call function: (function_identifier) @keyword (nil) @constant.builtin
(#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 (boolean) @constant.builtin.boolean
[(call (integer) @constant.numeric.integer
function: (function_identifier) @function (float) @constant.numeric.float
(arguments
[(identifier) @variable.parameter (alias) @type
(_ (identifier) @variable.parameter)
(_ (_ (identifier) @variable.parameter)) (call
(_ (_ (_ (identifier) @variable.parameter))) target: (dot
(_ (_ (_ (_ (identifier) @variable.parameter)))) left: (atom) @type))
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
(binary_op (char) @constant.character
left:
(call ; Quoted content
function: (function_identifier) @function
(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 (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)$"))
(call (function_identifier) @keyword
[(call
function: (function_identifier) @function)
(identifier) @function (identifier) @function
(binary_op (binary_operator
left: left: (identifier) @function
[(call operator: "when")
function: (function_identifier) @function) ])
(identifier) @function] (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
operator: "when")]
(#match? @keyword "^(defp|def|defmacrop|defmacro|defguardp|defguard|defdelegate)$"))
(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)))))])))
(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
operator: _ @operator)
(binary_op ; * pipe into identifier (definition)
operator: _ @operator) (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__)$")
)
(heredoc ; * unused
[(heredoc_start) (
(heredoc_content) (identifier) @comment
(heredoc_end)] @string) (#match? @comment "^_")
)
(string ; * regular
[(string_start) (identifier) @variable
(string_content)
(string_end)] @string)
(sigil_start) @string.special ; Comment
(sigil_content) @string
(sigil_end) @string.special
(interpolation (comment) @comment
"#{" @punctuation.special
"}" @punctuation.special) ; Punctuation
[
"%"
] @punctuation
[ [
"," ","
"->" ";"
"."
] @punctuation.delimiter ] @punctuation.delimiter
[ [
@ -133,6 +218,4 @@
">>" ">>"
] @punctuation.bracket ] @punctuation.bracket
(special_identifier) @function.special
(ERROR) @warning (ERROR) @warning

@ -25,7 +25,7 @@
(variadic_parameter_declaration (identifier) @variable.parameter) (variadic_parameter_declaration (identifier) @variable.parameter)
(type_identifier) @type (type_identifier) @type
(field_identifier) @property (field_identifier) @variable.other.member
(identifier) @variable (identifier) @variable
(package_identifier) @variable (package_identifier) @variable
@ -130,13 +130,13 @@
(rune_literal) (rune_literal)
] @string ] @string
(escape_sequence) @escape (escape_sequence) @constant.character.escape
[ [
(int_literal) (int_literal)
(float_literal) (float_literal)
(imaginary_literal) (imaginary_literal)
] @number ] @constant.numeric.integer
[ [
(true) (true)

@ -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) (constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class)) (class (class_head class: (class_name (type)) @class))
(instance (instance_head class: (class_name (type)) @class)) (instance (instance_head class: (class_name (type)) @class))
(integer) @number (integer) @constant.numeric.integer
(exp_literal (float)) @number (exp_literal (float)) @constant.numeric.float
(char) @literal (char) @constant.character
(con_unit) @literal (con_unit) @literal
(con_list) @literal (con_list) @literal
(tycon_arrow) @operator (tycon_arrow) @operator

@ -59,14 +59,15 @@
(hex_integer_literal) (hex_integer_literal)
(decimal_integer_literal) (decimal_integer_literal)
(octal_integer_literal) (octal_integer_literal)
] @constant.numeric.integer
[
(decimal_floating_point_literal) (decimal_floating_point_literal)
(hex_floating_point_literal) (hex_floating_point_literal)
] @number ] @constant.numeric.float
[ (character_literal) @constant.character
(character_literal) (string_literal) @string
(string_literal)
] @string
[ [
(true) (true)

@ -65,7 +65,7 @@
; Properties ; Properties
;----------- ;-----------
(property_identifier) @property (property_identifier) @variable.other.member
; Literals ; Literals
;--------- ;---------
@ -88,7 +88,7 @@
] @string ] @string
(regex) @string.regexp (regex) @string.regexp
(number) @number (number) @constant.numeric.integer
; Tokens ; Tokens
;------- ;-------

@ -1,9 +1,20 @@
[
(true)
(false)
] @constant.builtin.boolean
(null) @constant.builtin
(number) @constant.numeric
(pair (pair
key: (_) @keyword) key: (_) @keyword)
(string) @string (string) @string
(escape_sequence) @constant.character.escape
(ERROR) @error
(object "," @punctuation.delimiter
"{" @escape [
(_) "["
"}" @escape) "]"
"{"
"}"
] @punctuation.bracket

@ -15,7 +15,7 @@
(field_expression (field_expression
(identifier) (identifier)
(identifier) @field .) (identifier) @variable.other.member .)
(function_definition (function_definition
name: (identifier) @function) name: (identifier) @function)
@ -80,14 +80,14 @@
(struct_definition (struct_definition
name: (identifier) @type) name: (identifier) @type)
(number) @number (number) @constant.numeric.integer
(range_expression (range_expression
(identifier) @number (identifier) @constant.numeric.integer
(eq? @number "end")) (eq? @constant.numeric.integer "end"))
(range_expression (range_expression
(_ (_
(identifier) @number (identifier) @constant.numeric.integer
(eq? @number "end"))) (eq? @constant.numeric.integer "end")))
(coefficient_expression (coefficient_expression
(number) (number)
(identifier) @constant.builtin) (identifier) @constant.builtin)

@ -7,9 +7,9 @@
(date) (date)
(interval) (interval)
(quantity) (quantity)
] @number ] @constant.numeric.integer
((account) @field) ((account) @variable.other.member)
((commodity) @text.literal) ((commodity) @text.literal)
"include" @include "include" @include

@ -150,14 +150,14 @@
(table ["{" "}"] @constructor) (table ["{" "}"] @constructor)
(comment) @comment (comment) @comment
(string) @string (string) @string
(number) @number (number) @constant.numeric.integer
(label_statement) @label (label_statement) @label
; A bit of a tricky one, this will only match field names ; A bit of a tricky one, this will only match field names
(field . (identifier) @property (_)) (field . (identifier) @variable.other.member (_))
(shebang) @comment (shebang) @comment
;; Property ;; Property
(property_identifier) @property (property_identifier) @variable.other.member
;; Variable ;; Variable
(identifier) @variable (identifier) @variable

@ -33,16 +33,14 @@
(uri) @string.special.uri (uri) @string.special.uri
[ (integer) @constant.numeric.integer
(integer) (float) @constant.numeric.float
(float)
] @number
(interpolation (interpolation
"${" @punctuation.special "${" @punctuation.special
"}" @punctuation.special) @embedded "}" @punctuation.special) @embedded
(escape_sequence) @escape (escape_sequence) @constant.character.escape
(function (function
universal: (identifier) @variable.parameter universal: (identifier) @variable.parameter
@ -66,8 +64,8 @@
(binary (binary
operator: _ @operator) operator: _ @operator)
(attr_identifier) @property (attr_identifier) @variable.other.member
(inherit attrs: (attrs_inherited (identifier) @property) ) (inherit attrs: (attrs_inherited (identifier) @variable.other.member) )
[ [
";" ";"

@ -51,14 +51,14 @@
; Properties ; Properties
;----------- ;-----------
[(label_name) (field_name) (instance_variable_name)] @property [(label_name) (field_name) (instance_variable_name)] @variable.other.member
; Constants ; Constants
;---------- ;----------
[(boolean) (unit)] @constant [(boolean) (unit)] @constant
[(number) (signed_number)] @number [(number) (signed_number)] @constant.numeric.integer
(character) @constant.character (character) @constant.character
@ -66,7 +66,7 @@
(quoted_string "{" @string "}" @string) @string (quoted_string "{" @string "}" @string) @string
(escape_sequence) @string.escape (escape_sequence) @constant.character.escape
[ [
(conversion_specification) (conversion_specification)
@ -145,7 +145,7 @@
; Attributes ; Attributes
;----------- ;-----------
(attribute_id) @property (attribute_id) @variable.other.member
; Comments ; Comments
;--------- ;---------

@ -30,12 +30,12 @@
; Member ; Member
(property_element (property_element
(variable_name) @property) (variable_name) @variable.other.member)
(member_access_expression (member_access_expression
name: (variable_name (name)) @property) name: (variable_name (name)) @variable.other.member)
(member_access_expression (member_access_expression
name: (name) @property) name: (name) @variable.other.member)
; Variables ; Variables
@ -56,10 +56,10 @@
(string) @string (string) @string
(heredoc) @string (heredoc) @string
(boolean) @constant.builtin (boolean) @constant.builtin.boolean
(null) @constant.builtin (null) @constant.builtin
(integer) @number (integer) @constant.numeric.integer
(float) @number (float) @constant.numeric.float
(comment) @comment (comment) @comment
"$" @operator "$" @operator

@ -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) (fieldName)
(optionName) (optionName)
] @property ] @variable.other.member
(enumVariantName) @type.enum.variant (enumVariantName) @type.enum.variant
(fullIdent) @namespace (fullIdent) @namespace
[ (intLit) @constant.numeric.integer
(intLit) (floatLit) @constant.numeric.float
(floatLit) (boolLit) @constant.builtin.boolean
] @number
(boolLit) @constant.builtin
(strLit) @string (strLit) @string
(constant) @constant (constant) @constant

@ -29,7 +29,7 @@
name: (identifier) @function) name: (identifier) @function)
(identifier) @variable (identifier) @variable
(attribute attribute: (identifier) @property) (attribute attribute: (identifier) @variable.other.member)
(type (identifier) @type) (type (identifier) @type)
; Literals ; Literals
@ -40,14 +40,11 @@
(false) (false)
] @constant.builtin ] @constant.builtin
[ (integer) @constant.numeric.integer
(integer) (float) @constant.numeric.float
(float)
] @number
(comment) @comment (comment) @comment
(string) @string (string) @string
(escape_sequence) @escape (escape_sequence) @constant.character.escape
(interpolation (interpolation
"{" @punctuation.special "{" @punctuation.special

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

@ -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) (class_variable)
(instance_variable) (instance_variable)
] @property ] @variable.other.member
((identifier) @constant.builtin ((identifier) @constant.builtin
(#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$")) (#match? @constant.builtin "^__(FILE|LINE|ENCODING)__$"))
@ -101,12 +101,12 @@
] @string.special.symbol ] @string.special.symbol
(regex) @string.regexp (regex) @string.regexp
(escape_sequence) @escape (escape_sequence) @constant.character.escape
[ [
(integer) (integer)
(float) (float)
] @number ] @constant.numeric.integer
[ [
(nil) (nil)

@ -15,15 +15,13 @@
; Primitives ; Primitives
; --- ; ---
(escape_sequence) @escape (escape_sequence) @constant.character.escape
(primitive_type) @type.builtin (primitive_type) @type.builtin
(boolean_literal) @constant.builtin.boolean (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) (string_literal)
(raw_string_literal) (raw_string_literal)
] @string ] @string
@ -40,10 +38,10 @@
(enum_variant (identifier) @type.enum.variant) (enum_variant (identifier) @type.enum.variant)
(field_initializer (field_initializer
(field_identifier) @property) (field_identifier) @variable.other.member)
(shorthand_field_initializer (shorthand_field_initializer
(identifier) @variable.property) (identifier) @variable.other.member)
(shorthand_field_identifier) @variable.property (shorthand_field_identifier) @variable.other.member
(lifetime (lifetime
"'" @label "'" @label
@ -81,9 +79,24 @@
] @punctuation.bracket) ] @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 (parameter
pattern: (identifier) @variable.parameter) pattern: (identifier) @variable.parameter)
(closure_parameters (closure_parameters
@ -336,4 +349,4 @@
(type_identifier) @type (type_identifier) @type
(identifier) @variable (identifier) @variable
(field_identifier) @property (field_identifier) @variable.other.member

@ -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)$")) (#match? @_attr "^(href|src)$"))
(tag_name) @tag (tag_name) @tag
(attribute_name) @property (attribute_name) @variable.other.member
(erroneous_end_tag_name) @error (erroneous_end_tag_name) @error
(comment) @comment (comment) @comment

@ -1,17 +1,17 @@
; Properties ; Properties
;----------- ;-----------
(bare_key) @property (bare_key) @variable.other.member
(quoted_key) @string (quoted_key) @string
; Literals ; Literals
;--------- ;---------
(boolean) @constant.builtin (boolean) @constant.builtin.boolean
(comment) @comment (comment) @comment
(string) @string (string) @string
(integer) @number (integer) @constant.numeric.integer
(float) @number (float) @constant.numeric.float
(offset_date_time) @string.special (offset_date_time) @string.special
(local_date_time) @string.special (local_date_time) @string.special
(local_date) @string.special (local_date) @string.special

@ -35,12 +35,12 @@
(comment) @comment (comment) @comment
(field_name) @property (field_name) @variable.other.member
(capture) @label (capture) @label
(predicate_name) @function (predicate_name) @function
(escape_sequence) @escape (escape_sequence) @constant.character.escape
(node_name) @variable (node_name) @variable

@ -1,12 +1,12 @@
(block_mapping_pair key: (_) @property) (block_mapping_pair key: (_) @variable.other.member)
(flow_mapping (_ key: (_) @property)) (flow_mapping (_ key: (_) @variable.other.member))
(boolean_scalar) @constant.builtin.boolean (boolean_scalar) @constant.builtin.boolean
(null_scalar) @constant.builtin (null_scalar) @constant.builtin
(double_quote_scalar) @string (double_quote_scalar) @string
(single_quote_scalar) @string (single_quote_scalar) @string
(escape_sequence) @string.escape (escape_sequence) @constant.character.escape
(integer_scalar) @number (integer_scalar) @constant.numeric.integer
(float_scalar) @number (float_scalar) @constant.numeric.float
(comment) @comment (comment) @comment
(anchor_name) @type (anchor_name) @type
(alias_name) @type (alias_name) @type

Some files were not shown because too many files have changed in this diff Show More

Loading…
Cancel
Save