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
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo check
uses: actions-rs/cargo@v1
@ -67,19 +67,19 @@ jobs:
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo test
uses: actions-rs/cargo@v1
@ -112,19 +112,19 @@ jobs:
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/registry
key: ${{ runner.os }}-v1-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo index
uses: actions/cache@v2.1.6
with:
path: ~/.cargo/git
key: ${{ runner.os }}-v1-cargo-index-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }}
- name: Cache cargo target dir
uses: actions/cache@v2.1.6
with:
path: target
key: ${{ runner.os }}-v1-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }}
- name: Run cargo fmt
uses: actions-rs/cargo@v1

6
.gitmodules vendored

@ -84,7 +84,7 @@
shallow = true
[submodule "helix-syntax/languages/tree-sitter-elixir"]
path = helix-syntax/languages/tree-sitter-elixir
url = https://github.com/IceDragon200/tree-sitter-elixir
url = https://github.com/elixir-lang/tree-sitter-elixir
shallow = true
[submodule "helix-syntax/languages/tree-sitter-nix"]
path = helix-syntax/languages/tree-sitter-nix
@ -130,3 +130,7 @@
path = helix-syntax/languages/tree-sitter-tsq
url = https://github.com/tree-sitter/tree-sitter-tsq
shallow = true
[submodule "helix-syntax/languages/tree-sitter-cmake"]
path = helix-syntax/languages/tree-sitter-cmake
url = https://github.com/uyha/tree-sitter-cmake
shallow = true

@ -1,4 +1,85 @@
# 0.5.0 (2021-10-28)
A big shout out to all the contributors! We had 46 contributors in this release.
Helix has popped up in [Scoop, FreeBSD Ports and Gentu GURU](https://repology.org/project/helix/versions)!
The following is a quick rundown of the larger changes, there were many more
(check the git history for more details).
Breaking changes:
- A couple of keymaps moved to resolve a few conflicting keybinds.
- Documentation popups were moved from `K` to `space+k`
- `K` is now `keep_selections` which filters selections to only keeps ones matching the regex
- `keep_primary_selection` moved from `space+space` to `,`
- `Alt-,` is now `remove_primary_selection` which keeps all selections except the primary one
- Opening files in a split moved from `C-h` to `C-s`
- Some configuration options moved from a `[terminal]` section to `[editor]`. [Consult the documentation for more information.](https://docs.helix-editor.com/configuration.html)
Features:
- LSP compatibility greatly improved for some implementations (Julia, Python, Typescript)
- Autocompletion! Completion now triggers automatically after a set idle timeout
- Completion documentation is now displayed next to the popup ([#691](https://github.com/helix-editor/helix/pull/691))
- Treesitter textobjects (select a function via `mf`, class via `mc`) ([#728](https://github.com/helix-editor/helix/pull/728))
- Global search across entire workspace `space+/` ([#651](https://github.com/helix-editor/helix/pull/651))
- Relative line number support ([#485](https://github.com/helix-editor/helix/pull/485))
- Prompts now store a history (72cf86e)
- `:vsplit` and `:hsplit` commands ([#639](https://github.com/helix-editor/helix/pull/639))
- `C-w h/j/k/l` can now be used to navigate between splits ([#860](https://github.com/helix-editor/helix/pull/860))
- `C-j` and `C-k` are now alternative keybindings to `C-n` and `C-p` in the UI ([#876](https://github.com/helix-editor/helix/pull/876))
- Shell commands (shell-pipe, pipe-to, shell-insert-output, shell-append-output, keep-pipe) ([#547](https://github.com/helix-editor/helix/pull/547))
- Searching now defaults to smart case search (case insensitive unless uppercase is used) ([#761](https://github.com/helix-editor/helix/pull/761))
- The preview pane was improved to highlight and center line ranges
- The user `languages.toml` is now merged into defaults, no longer need to copy the entire file (dc57f8dc)
- Show hidden files in completions ([#648](https://github.com/helix-editor/helix/pull/648))
- Grammar injections are now properly handled (dd0b15e)
- `v` in select mode now switches back to normal mode ([#660](https://github.com/helix-editor/helix/pull/660))
- View mode can now be triggered as a "sticky" mode ([#719](https://github.com/helix-editor/helix/pull/719))
- `f`/`t` and object selection motions can now be repeated via `Alt-.` ([#891](https://github.com/helix-editor/helix/pull/891))
- Statusline now displays total selection count and diagnostics counts for both errors and warnings ([#916](https://github.com/helix-editor/helix/pull/916))
New grammars:
- Ledger ([#572](https://github.com/helix-editor/helix/pull/572))
- Protobuf ([#614](https://github.com/helix-editor/helix/pull/614))
- Zig ([#631](https://github.com/helix-editor/helix/pull/631))
- YAML ([#667](https://github.com/helix-editor/helix/pull/667))
- Lua ([#665](https://github.com/helix-editor/helix/pull/665))
- OCaml ([#666](https://github.com/helix-editor/helix/pull/666))
- Svelte ([#733](https://github.com/helix-editor/helix/pull/733))
- Vue ([#787](https://github.com/helix-editor/helix/pull/787))
- Tree-sitter queries ([#845](https://github.com/helix-editor/helix/pull/845))
- CMake ([#888](https://github.com/helix-editor/helix/pull/888))
- Elixir (we switched over to the official grammar) (6c0786e)
- Language server definitions for Nix and Elixir ([#725](https://github.com/helix-editor/helix/pull/725))
- Python now uses `pylsp` instead of `pyls`
- Python now supports indentation
New themes:
- Monokai ([#628](https://github.com/helix-editor/helix/pull/628))
- Everforest Dark ([#760](https://github.com/helix-editor/helix/pull/760))
- Nord ([#799](https://github.com/helix-editor/helix/pull/799))
- Base16 Default Dark ([#833](https://github.com/helix-editor/helix/pull/833))
- Rose Pine ([#897](https://github.com/helix-editor/helix/pull/897))
Fixes:
- Fix crash on empty rust file ([#592](https://github.com/helix-editor/helix/pull/592))
- Exit select mode after toggle comment ([#598](https://github.com/helix-editor/helix/pull/598))
- Pin popups with no positioning to the initial position (12ea3888)
- xsel copy should not freeze the editor (6dd7dc4)
- `*` now only sets the search register and doesn't jump to the next occurrence (3426285)
- Goto line start/end commands extend when in select mode ([#739](https://github.com/helix-editor/helix/pull/739))
- Fix documentation popups sometimes not getting fully highlighted (066367c)
- Refactor apply_workspace_edit to remove assert (b02d872)
- Wrap around the top of the picker menu when scrolling (c7d6e44)
- Don't allow closing the last split if there's unsaved changes (3ff5b00)
- Indentation used different default on hx vs hx new_file.txt (c913bad)
# 0.4.1 (2021-08-14)
A minor release that includes:
@ -7,6 +88,8 @@ A minor release that includes:
# 0.4.0 (2021-08-13)
A big shout out to all the contributors! We had 28 contributors in this release.
Two months have passed, so this is another big release. A big thank you to all
the contributors and package maintainers!
@ -44,6 +127,8 @@ selections in the future as well as resolves many bugs and edge cases.
# 0.3.0 (2021-06-27)
A big shout out to all the contributors! We had 24 contributors in this release.
Another big release.
Highlights:
@ -90,6 +175,8 @@ Includes a fix where wq/wqa could exit before file saving completed.
# 0.2.0
A big shout out to all the contributors! We had 18 contributors in this release.
Enough has changed to bump the version. We're skipping 0.1.x because
previously the CLI would always report version as 0.1.0, and we'd like
to distinguish it in bug reports..

91
Cargo.lock generated

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

@ -27,11 +27,11 @@ 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
(similar to emacs) in wgpu or skulpin.
# Installation
Note: Only certain languages have indentation definitions at the moment. Check
`runtime/queries/<lang>/` for `indents.toml`.
# Installation
We provide packaging for various distributions, but here's a quick method to
build from source.
@ -71,7 +71,7 @@ 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.
- Help with packaging on various distributions needed!
- To use print debugging to the `~/.cache/helix/helix.log` file, you must:
- To use print debugging to the [Helix log file](https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file), you must:
* Print using `log::info!`, `warn!`, or `error!`. (`log::info!("helix!")`)
* Pass the appropriate verbosity level option for the desired log level. (`hx -v <file>` for info, more `v`s for higher severity inclusive)
- If your preferred language is missing, integrating a tree-sitter grammar for

@ -6,10 +6,6 @@
- clojure
- erlang
as you type completion!
- [ ] use signature_help_provider and completion_provider trigger characters in
a hook to trigger signature help text / autocompletion
- [ ] document.on_type provider triggers
- [ ] completion isIncomplete support
1
@ -18,8 +14,6 @@ as you type completion!
- [ ] = for auto indent line/selection
- [ ] :x for closing buffers
- [ ] repeat selection
- [ ] lsp: signature help
2

@ -8,3 +8,5 @@
- [Keymap](./keymap.md)
- [Key Remapping](./remapping.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-completion` | Enable automatic pop up of auto-completion. | `true` |
| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` |
| `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` |
| `auto-info` | Whether to display infoboxes | `true` |
## LSP

@ -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 |
| ----- | ----------- | ------- |
| `h`, `Left` | Move left | `move_char_left` |
| `j`, `Down` | Move down | `move_char_right` |
| `k`, `Up` | Move up | `move_line_up` |
| `l`, `Right` | Move right | `move_line_down` |
| `h`/`Left` | Move left | `move_char_left` |
| `j`/`Down` | Move down | `move_line_down` |
| `k`/`Up` | Move up | `move_line_up` |
| `l`/`Right` | Move right | `move_char_right` |
| `w` | Move next word start | `move_next_word_start` |
| `b` | Move previous word start | `move_prev_word_start` |
| `e` | Move next word end | `move_next_word_end` |
@ -22,6 +22,7 @@
| `f` | Find next char | `find_next_char` |
| `T` | Find 'till previous char | `till_prev_char` |
| `F` | Find previous char | `find_prev_char` |
| `Alt-.` | Repeat last motion (`f`, `t` or `m`) | `repeat_last_motion` |
| `Home` | Move to the start of the line | `goto_line_start` |
| `End` | Move to the end of the line | `goto_line_end` |
| `PageUp` | Move page up | `page_up` |
@ -54,6 +55,7 @@
| `A` | Insert at the end of the line | `append_to_line` |
| `o` | Open new line below selection | `open_below` |
| `O` | Open new line above selection | `open_above` |
| `.` | Repeat last change | N/A |
| `u` | Undo change | `undo` |
| `U` | Redo change | `redo` |
| `y` | Yank selection | `yank` |
@ -86,8 +88,9 @@
| `;` | Collapse selection onto a single cursor | `collapse_selection` |
| `Alt-;` | Flip selection cursor and anchor | `flip_selections` |
| `,` | Keep only the primary selection | `keep_primary_selection` |
| `C` | Copy selection onto the next line | `copy_selection_on_next_line` |
| `Alt-C` | Copy selection onto the previous line | `copy_selection_on_prev_line` |
| `Alt-,` | Remove the primary selection | `remove_primary_selection` |
| `C` | Copy selection onto the next line (Add cursor below) | `copy_selection_on_next_line` |
| `Alt-C` | Copy selection onto the previous line (Add cursor above) | `copy_selection_on_prev_line` |
| `(` | Rotate main selection backward | `rotate_selections_backward` |
| `)` | Rotate main selection forward | `rotate_selections_forward` |
| `Alt-(` | Rotate selection contents backward | `rotate_selection_contents_backward` |
@ -103,13 +106,13 @@
### Search
> TODO: The search implementation isn't ideal yet -- we don't support searching in reverse.
| Key | Description | Command |
| ----- | ----------- | ------- |
| `/` | Search for regex pattern | `search` |
| `?` | Search for previous pattern | `rsearch` |
| `n` | Select next search match | `search_next` |
| `N` | Add next search match to selection | `extend_search_next` |
| `N` | Select previous search match | `search_prev` |
| `*` | Use current selection as the search pattern | `search_selection` |
### Minor modes
@ -158,6 +161,8 @@ Jumps to various locations.
| `r` | Go to references | `goto_reference` |
| `i` | Go to implementation | `goto_implementation` |
| `a` | Go to the last accessed/alternate file | `goto_last_accessed_file` |
| `n` | Go to next buffer | `goto_next_buffer` |
| `p` | Go to previous buffer | `goto_previous_buffer` |
#### Match mode
@ -184,7 +189,11 @@ This layer is similar to vim keybindings as kakoune does not support window.
| ----- | ------------- | ------- |
| `w`, `Ctrl-w` | Switch to next window | `rotate_view` |
| `v`, `Ctrl-v` | Vertical right split | `vsplit` |
| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` |
| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` |
| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` |
| `j`, `Ctrl-j`, `down` | Move to split below | `jump_view_down` |
| `k`, `Ctrl-k`, `up` | Move to split above | `jump_view_up` |
| `l`, `Ctrl-l`, `right` | Move to right split | `jump_view_right` |
| `q`, `Ctrl-q` | Close current window | `wclose` |
#### Space mode
@ -244,10 +253,32 @@ Keys to use within picker. Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Up`, `Ctrl-p` | Previous entry |
| `Down`, `Ctrl-n` | Next entry |
| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry |
| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry |
| `Ctrl-space` | Filter options |
| `Enter` | Open selected |
| `Ctrl-h` | Open horizontally |
| `Ctrl-s` | Open horizontally |
| `Ctrl-v` | Open vertically |
| `Escape`, `Ctrl-c` | Close picker |
# Prompt
Keys to use within prompt, Remapping currently not supported.
| Key | Description |
| ----- | ------------- |
| `Escape`, `Ctrl-c` | Close prompt |
| `Alt-b`, `Alt-Left` | Backward a word |
| `Ctrl-b`, `Left` | Backward a char |
| `Alt-f`, `Alt-Right` | Forward a word |
| `Ctrl-f`, `Right` | Forward a char |
| `Ctrl-e`, `End` | move prompt end |
| `Ctrl-a`, `Home` | move prompt start |
| `Ctrl-w` | delete previous word |
| `Ctrl-k` | delete to end of line |
| `backspace` | delete previous char |
| `Ctrl-s` | insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later |
| `Ctrl-p`, `Up` | select previous history |
| `Ctrl-n`, `Down` | select next history |
| `Tab` | slect next completion item |
| `BackTab` | slect previous completion item |
| `Enter` | Open selected |

@ -2,7 +2,7 @@
One-way key remapping is temporarily supported via a simple TOML configuration
file. (More powerful solutions such as rebinding via commands will be
available in the feature).
available in the future).
To remap keys, write a `config.toml` file in your `helix` configuration
directory (default `~/.config/helix` in Linux systems) with a structure like

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

@ -2,6 +2,8 @@
(Currently not fully documented, see the [keymappings](./keymap.md) list for more.)
See [tutor.txt](https://github.com/helix-editor/helix/blob/master/runtime/tutor.txt) (accessible via `hx --tutor` or `:tutor`) for a vimtutor-like introduction.
## Registers
Vim-like registers can be used to yank and store text to be pasted later. Usage is similar, with `"` being used to select a register:
@ -49,9 +51,10 @@ Multiple characters are currently not supported, but planned.
## Textobjects
Currently supported: `word`, `surround`.
Currently supported: `word`, `surround`, `function`, `class`, `parameter`.
![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif)
![textobject-treesitter-demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif)
- `ma` - Select around the object (`va` in vim, `<alt-a>` in kakoune)
- `mi` - Select inside the object (`vi` in vim, `<alt-i>` in kakoune)
@ -60,5 +63,11 @@ Currently supported: `word`, `surround`.
| --- | --- |
| `w` | Word |
| `(`, `[`, `'`, etc | Specified surround pairs |
Textobjects based on treesitter, like `function`, `class`, etc are planned.
| `f` | Function |
| `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": {
"devshell": {
"locked": {
"lastModified": 1630239564,
"narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=",
"lastModified": 1632436039,
"narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=",
"owner": "numtide",
"repo": "devshell",
"rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73",
"rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6",
"type": "github"
},
"original": {
@ -15,6 +15,21 @@
"type": "github"
}
},
"flake-utils": {
"locked": {
"lastModified": 1623875721,
"narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=",
"owner": "numtide",
"repo": "flake-utils",
"rev": "f7e004a55b120c02ecb6219596820fcd32ca8772",
"type": "github"
},
"original": {
"owner": "numtide",
"repo": "flake-utils",
"type": "github"
}
},
"flakeCompat": {
"flake": false,
"locked": {
@ -37,14 +52,16 @@
"nixpkgs": [
"nixpkgs"
],
"rustOverlay": "rustOverlay"
"rustOverlay": [
"rust-overlay"
]
},
"locked": {
"lastModified": 1631254163,
"narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=",
"lastModified": 1634796585,
"narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=",
"owner": "yusdacra",
"repo": "nix-cargo-integration",
"rev": "432d8504a32232e8d74710024d5bf5cc31767651",
"rev": "a84a2137a396f303978f1d48341e0390b0e16a8b",
"type": "github"
},
"original": {
@ -55,11 +72,11 @@
},
"nixpkgs": {
"locked": {
"lastModified": 1631206977,
"narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=",
"lastModified": 1634782485,
"narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=",
"owner": "nixos",
"repo": "nixpkgs",
"rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b",
"rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be",
"type": "github"
},
"original": {
@ -69,21 +86,40 @@
"type": "github"
}
},
"nixpkgs_2": {
"locked": {
"lastModified": 1628186154,
"narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=",
"owner": "NixOS",
"repo": "nixpkgs",
"rev": "06552b72346632b6943c8032e57e702ea12413bf",
"type": "github"
},
"original": {
"owner": "NixOS",
"repo": "nixpkgs",
"type": "github"
}
},
"root": {
"inputs": {
"flakeCompat": "flakeCompat",
"nixCargoIntegration": "nixCargoIntegration",
"nixpkgs": "nixpkgs"
"nixpkgs": "nixpkgs",
"rust-overlay": "rust-overlay"
}
},
"rustOverlay": {
"flake": false,
"rust-overlay": {
"inputs": {
"flake-utils": "flake-utils",
"nixpkgs": "nixpkgs_2"
},
"locked": {
"lastModified": 1631240108,
"narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=",
"lastModified": 1634869268,
"narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=",
"owner": "oxalica",
"repo": "rust-overlay",
"rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289",
"rev": "c02c2d86354327317546501af001886fbb53d374",
"type": "github"
},
"original": {

@ -3,9 +3,11 @@
inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
rust-overlay.url = "github:oxalica/rust-overlay";
nixCargoIntegration = {
url = "github:yusdacra/nix-cargo-integration";
inputs.nixpkgs.follows = "nixpkgs";
inputs.rustOverlay.follows = "rust-overlay";
};
flakeCompat = {
url = "github:edolstra/flake-compat";
@ -61,7 +63,7 @@
'';
};
shell = common: prev: {
packages = prev.packages ++ (with common.pkgs; [ lld_10 lldb cargo-tarpaulin ]);
packages = prev.packages ++ (with common.pkgs; [ lld_12 lldb cargo-tarpaulin ]);
env = prev.env ++ [
{ name = "HELIX_RUNTIME"; eval = "$PWD/runtime"; }
{ name = "RUST_BACKTRACE"; value = "1"; }

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

@ -1,3 +1,6 @@
//! When typing the opening character of one of the possible pairs defined below,
//! this module provides the functionality to insert the paired closing character.
use crate::{Range, Rope, Selection, Tendril, Transaction};
use smallvec::SmallVec;

@ -1,3 +1,5 @@
//! Utility functions to categorize a `char`.
use crate::LineEnding;
#[derive(Debug, Eq, PartialEq)]

@ -1,3 +1,6 @@
//! This module contains the functionality toggle comments on lines over the selection
//! using the comment character defined in the user's `languages.toml`
use crate::{
find_first_non_whitespace_char, Change, Rope, RopeSlice, Selection, Tendril, Transaction,
};
@ -60,7 +63,7 @@ pub fn toggle_line_comments(doc: &Rope, selection: &Selection, token: Option<&st
let token = token.unwrap_or("//");
let comment = Tendril::from(format!("{} ", token));
let mut lines: Vec<usize> = Vec::new();
let mut lines: Vec<usize> = Vec::with_capacity(selection.len());
let mut min_next_line = 0;
for selection in selection {

@ -1,3 +1,6 @@
//! LSP diagnostic utility types.
/// Describes the severity level of a [`Diagnostic`].
#[derive(Debug, Eq, PartialEq)]
pub enum Severity {
Error,
@ -6,12 +9,14 @@ pub enum Severity {
Hint,
}
#[derive(Debug)]
/// A range of `char`s within the text.
#[derive(Debug, Clone, Copy, PartialOrd, Ord, PartialEq, Eq)]
pub struct Range {
pub start: usize,
pub end: usize,
}
/// Corresponds to [`lsp_types::Diagnostic`](https://docs.rs/lsp-types/0.91.0/lsp_types/struct.Diagnostic.html)
#[derive(Debug)]
pub struct Diagnostic {
pub range: Range,

@ -1,4 +1,6 @@
// Based on https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs
//! Utility functions to traverse the unicode graphemes of a `Rope`'s text contents.
//!
//! Based on <https://github.com/cessen/led/blob/c4fa72405f510b7fd16052f90a598c429b3104a6/src/graphemes.rs>
use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice};
use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete};
use unicode_width::UnicodeWidthStr;

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

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

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

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

@ -53,6 +53,10 @@ pub fn move_vertically(
let pos = range.cursor(slice);
// Compute the current position's 2d coordinates.
// TODO: switch this to use `visual_coords_at_pos` rather than
// `coords_at_pos` as this will cause a jerky movement when the visual
// position does not match, like moving from a line with tabs/CJK to
// a line without
let Position { row, col } = coords_at_pos(slice, pos);
let horiz = range.horiz.unwrap_or(col as u32);

@ -13,8 +13,13 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: &Selection)
let parent = match tree
.root_node()
.descendant_for_byte_range(from, to)
.and_then(|node| node.parent())
{
.and_then(|node| {
if node.child_count() == 0 || (node.start_byte() == from && node.end_byte() == to) {
node.parent()
} else {
Some(node)
}
}) {
Some(parent) => parent,
None => return range,
};

@ -2,6 +2,7 @@ use crate::{
chars::char_is_line_ending,
graphemes::{ensure_grapheme_boundary_prev, RopeGraphemes},
line_ending::line_end_char_index,
unicode::width::UnicodeWidthChar,
RopeSlice,
};
@ -54,11 +55,8 @@ impl From<Position> for tree_sitter::Point {
}
/// Convert a character index to (line, column) coordinates.
///
/// TODO: this should be split into two methods: one for visual
/// row/column, and one for "objective" row/column (possibly with
/// the column specified in `char`s). The former would be used
/// for cursor movement, and the latter would be used for e.g. the
/// row:column display in the status line.
/// column in `char` count which can be used for row:column display in
/// status line. See [`visual_coords_at_pos`] for a visual one.
pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
let line = text.char_to_line(pos);
@ -69,6 +67,28 @@ pub fn coords_at_pos(text: RopeSlice, pos: usize) -> Position {
Position::new(line, col)
}
/// Convert a character index to (line, column) coordinates visually.
///
/// Takes \t, double-width characters (CJK) into account as well as text
/// not in the document in the future.
/// See [`coords_at_pos`] for an "objective" one.
pub fn visual_coords_at_pos(text: RopeSlice, pos: usize, tab_width: usize) -> Position {
let line = text.char_to_line(pos);
let line_start = text.line_to_char(line);
let pos = ensure_grapheme_boundary_prev(text, pos);
let col = text
.slice(line_start..pos)
.chars()
.flat_map(|c| match c {
'\t' => Some(tab_width),
c => UnicodeWidthChar::width(c),
})
.sum();
Position::new(line, col)
}
/// Convert (line, column) coordinates to a character index.
///
/// If the `line` coordinate is beyond the end of the file, the EOF
@ -130,7 +150,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 10), (1, 4).into()); // position on d
// Test with wide characters.
// TODO: account for character width.
let text = Rope::from("今日はいい\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@ -151,7 +170,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 9), (1, 0).into());
// Test with wide-character grapheme clusters.
// TODO: account for character width.
let text = Rope::from("किमपि\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@ -161,7 +179,6 @@ mod test {
assert_eq!(coords_at_pos(slice, 6), (1, 0).into());
// Test with tabs.
// Todo: account for tab stops.
let text = Rope::from("\tHello\n");
let slice = text.slice(..);
assert_eq!(coords_at_pos(slice, 0), (0, 0).into());
@ -169,6 +186,54 @@ mod test {
assert_eq!(coords_at_pos(slice, 2), (0, 2).into());
}
#[test]
fn test_visual_coords_at_pos() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");
let slice = text.slice(..);
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into()); // position on \n
assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into()); // position on w
assert_eq!(visual_coords_at_pos(slice, 7, 8), (1, 1).into()); // position on o
assert_eq!(visual_coords_at_pos(slice, 10, 8), (1, 4).into()); // position on d
// Test with wide characters.
let text = Rope::from("今日はいい\n");
let slice = text.slice(..);
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 2).into());
assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 4).into());
assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 6).into());
assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 8).into());
assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 10).into());
assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
// Test with grapheme clusters.
let text = Rope::from("a̐éö̲\r\n");
let slice = text.slice(..);
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 1).into());
assert_eq!(visual_coords_at_pos(slice, 4, 8), (0, 2).into());
assert_eq!(visual_coords_at_pos(slice, 7, 8), (0, 3).into());
assert_eq!(visual_coords_at_pos(slice, 9, 8), (1, 0).into());
// Test with wide-character grapheme clusters.
// TODO: account for cluster.
let text = Rope::from("किमपि\n");
let slice = text.slice(..);
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 2).into());
assert_eq!(visual_coords_at_pos(slice, 3, 8), (0, 3).into());
assert_eq!(visual_coords_at_pos(slice, 5, 8), (0, 5).into());
assert_eq!(visual_coords_at_pos(slice, 6, 8), (1, 0).into());
// Test with tabs.
let text = Rope::from("\tHello\n");
let slice = text.slice(..);
assert_eq!(visual_coords_at_pos(slice, 0, 8), (0, 0).into());
assert_eq!(visual_coords_at_pos(slice, 1, 8), (0, 8).into());
assert_eq!(visual_coords_at_pos(slice, 2, 8), (0, 9).into());
}
#[test]
fn test_pos_at_coords() {
let text = Rope::from("ḧëḷḷö\nẅöṛḷḋ");

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

@ -362,6 +362,11 @@ impl Selection {
/// Adds a new range to the selection and makes it the primary range.
pub fn remove(mut self, index: usize) -> Self {
assert!(
self.ranges.len() > 1,
"can't remove the last range from a selection!"
);
self.ranges.remove(index);
if index < self.primary_index || self.primary_index == self.ranges.len() {
self.primary_index -= 1;
@ -369,6 +374,12 @@ impl Selection {
self
}
/// Replace a range in the selection with a new range.
pub fn replace(mut self, index: usize, range: Range) -> Self {
self.ranges[index] = range;
self.normalize()
}
/// Map selections over a set of changes. Useful for adjusting the selection position after
/// applying changes to a document.
pub fn map(self, changes: &ChangeSet) -> Self {

@ -49,7 +49,7 @@ pub struct Configuration {
#[serde(rename_all = "kebab-case")]
pub struct LanguageConfiguration {
#[serde(rename = "name")]
pub(crate) language_id: String,
pub language_id: String,
pub scope: String, // source.rust
pub file_types: Vec<String>, // filename ends_with? <Gemfile, rb, etc>
pub roots: Vec<String>, // these indicate project roots <.git, Cargo.toml>
@ -76,6 +76,8 @@ pub struct LanguageConfiguration {
#[serde(skip)]
pub(crate) indent_query: OnceCell<Option<IndentQuery>>,
#[serde(skip)]
pub(crate) textobject_query: OnceCell<Option<TextObjectQuery>>,
#[serde(skip_serializing_if = "Option::is_none")]
pub debugger: Option<DebugAdapterConfig>,
}
@ -160,6 +162,32 @@ pub struct IndentQuery {
pub outdent: HashSet<String>,
}
#[derive(Debug)]
pub struct TextObjectQuery {
pub query: Query,
}
impl TextObjectQuery {
/// Run the query on the given node and return sub nodes which match given
/// capture ("function.inside", "class.around", etc).
pub fn capture_nodes<'a>(
&'a self,
capture_name: &str,
node: Node<'a>,
slice: RopeSlice<'a>,
cursor: &'a mut QueryCursor,
) -> Option<impl Iterator<Item = Node<'a>>> {
let capture_idx = self.query.capture_index_for_name(capture_name)?;
let captures = cursor.captures(&self.query, node, RopeProvider(slice));
captures
.filter_map(move |(mat, idx)| {
(mat.captures[idx].index == capture_idx).then(|| mat.captures[idx].node)
})
.into()
}
}
fn load_runtime_file(language: &str, filename: &str) -> Result<String, std::io::Error> {
let path = crate::RUNTIME_DIR
.join("queries")
@ -208,13 +236,14 @@ impl LanguageConfiguration {
// highlights_query += "\n(ERROR) @error";
let injections_query = read_query(&language, "injections.scm");
let locals_query = read_query(&language, "locals.scm");
if highlights_query.is_empty() {
None
} else {
let language = get_language(&crate::RUNTIME_DIR, &self.language_id).ok()?;
let language = get_language(&crate::RUNTIME_DIR, &self.language_id)
.map_err(|e| log::info!("{}", e))
.ok()?;
let config = HighlightConfiguration::new(
language,
&highlights_query,
@ -258,6 +287,18 @@ impl LanguageConfiguration {
.as_ref()
}
pub fn textobject_query(&self) -> Option<&TextObjectQuery> {
self.textobject_query
.get_or_init(|| -> Option<TextObjectQuery> {
let lang_name = self.language_id.to_ascii_lowercase();
let query_text = read_query(&lang_name, "textobjects.scm");
let lang = self.highlight_config.get()?.as_ref()?.language;
let query = Query::new(lang, &query_text).ok()?;
Some(TextObjectQuery { query })
})
.as_ref()
}
pub fn scope(&self) -> &str {
&self.scope
}
@ -451,7 +492,7 @@ impl Syntax {
/// Iterate over the highlighted regions for a given slice of source code.
pub fn highlight_iter<'a>(
&self,
&'a self,
source: RopeSlice<'a>,
range: Option<std::ops::Range<usize>>,
cancellation_flag: Option<&'a AtomicUsize>,
@ -466,11 +507,10 @@ impl Syntax {
let highlighter = &mut ts_parser.borrow_mut();
highlighter.cursors.pop().unwrap_or_else(QueryCursor::new)
});
let tree_ref = unsafe { mem::transmute::<_, &'static Tree>(self.tree()) };
let tree_ref = self.tree();
let cursor_ref = unsafe { mem::transmute::<_, &'static mut QueryCursor>(&mut cursor) };
let query_ref = unsafe { mem::transmute::<_, &'static Query>(&self.config.query) };
let config_ref =
unsafe { mem::transmute::<_, &'static HighlightConfiguration>(self.config.as_ref()) };
let query_ref = &self.config.query;
let config_ref = self.config.as_ref();
// if reusing cursors & no range this resets to whole range
cursor_ref.set_byte_range(range.clone().unwrap_or(0..usize::MAX));
@ -582,39 +622,7 @@ impl LanguageLayer {
self.tree.as_ref(),
)
.ok_or(Error::Cancelled)?;
// unsafe { syntax.parser.set_cancellation_flag(None) };
// let mut cursor = syntax.cursors.pop().unwrap_or_else(QueryCursor::new);
// Process combined injections. (ERB, EJS, etc https://github.com/tree-sitter/tree-sitter/pull/526)
// if let Some(combined_injections_query) = &config.combined_injections_query {
// let mut injections_by_pattern_index =
// vec![(None, Vec::new(), false); combined_injections_query.pattern_count()];
// let matches =
// cursor.matches(combined_injections_query, tree.root_node(), RopeProvider(source));
// for mat in matches {
// let entry = &mut injections_by_pattern_index[mat.pattern_index];
// let (language_name, content_node, include_children) =
// injection_for_match(config, combined_injections_query, &mat, source);
// if language_name.is_some() {
// entry.0 = language_name;
// }
// if let Some(content_node) = content_node {
// entry.1.push(content_node);
// }
// entry.2 = include_children;
// }
// for (lang_name, content_nodes, includes_children) in injections_by_pattern_index {
// if let (Some(lang_name), false) = (lang_name, content_nodes.is_empty()) {
// if let Some(next_config) = (injection_callback)(lang_name) {
// let ranges =
// Self::intersect_ranges(&ranges, &content_nodes, includes_children);
// if !ranges.is_empty() {
// queue.push((next_config, depth + 1, ranges));
// }
// }
// }
// }
// }
self.tree = Some(tree)
}
Ok(())

@ -1,9 +1,13 @@
use std::fmt::Display;
use ropey::RopeSlice;
use tree_sitter::{Node, QueryCursor};
use crate::chars::{categorize_char, char_is_whitespace, CharCategory};
use crate::graphemes::next_grapheme_boundary;
use crate::movement::Direction;
use crate::surround;
use crate::syntax::LanguageConfiguration;
use crate::Range;
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction) -> usize {
@ -51,6 +55,15 @@ pub enum TextObject {
Inside,
}
impl Display for TextObject {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(match self {
Self::Around => "around",
Self::Inside => "inside",
})
}
}
// count doesn't do anything yet
pub fn textobject_word(
slice: RopeSlice,
@ -108,6 +121,44 @@ pub fn textobject_surround(
.unwrap_or(range)
}
/// Transform the given range to select text objects based on tree-sitter.
/// `object_name` is a query capture base name like "function", "class", etc.
/// `slice_tree` is the tree-sitter node corresponding to given text slice.
pub fn textobject_treesitter(
slice: RopeSlice,
range: Range,
textobject: TextObject,
object_name: &str,
slice_tree: Node,
lang_config: &LanguageConfiguration,
_count: usize,
) -> Range {
let get_range = move || -> Option<Range> {
let byte_pos = slice.char_to_byte(range.cursor(slice));
let capture_name = format!("{}.{}", object_name, textobject); // eg. function.inner
let mut cursor = QueryCursor::new();
let node = lang_config
.textobject_query()?
.capture_nodes(&capture_name, slice_tree, slice, &mut cursor)?
.filter(|node| node.byte_range().contains(&byte_pos))
.min_by_key(|node| node.byte_range().len())?;
let len = slice.len_bytes();
let start_byte = node.start_byte();
let end_byte = node.end_byte();
if start_byte >= len || end_byte >= len {
return None;
}
let start_char = slice.byte_to_char(start_byte);
let end_char = slice.byte_to_char(end_byte);
Some(Range::new(start_char, end_char))
};
get_range().unwrap_or(range)
}
#[cfg(test)]
mod test {
use super::TextObject::*;

@ -132,6 +132,9 @@ impl ChangeSet {
if self.changes.is_empty() {
return other;
}
if other.changes.is_empty() {
return self;
}
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 {
self.selection = Some(selection);
self

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

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

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

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

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

@ -1 +1 @@
Subproject commit c61212414a3e95b5f7507f98e83de1d638044adc
Subproject commit e8dcc9d2b404c542fd236ea5f7208f90be8a6e89

@ -1 +1 @@
Subproject commit 295e62a43b92cea909cfabe57e8818d177f4857b
Subproject commit f5d7bda543da788bd507b05bd722627dde66c9ec

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

@ -99,12 +99,17 @@ impl Application {
let editor_view = Box::new(ui::EditorView::new(std::mem::take(&mut config.keys)));
compositor.push(editor_view);
if !args.files.is_empty() {
if args.load_tutor {
let path = helix_core::runtime_dir().join("tutor.txt");
editor.open(path, Action::VerticalSplit)?;
// Unset path to prevent accidentally saving to the original tutor file.
doc_mut!(editor).set_path(None)?;
} else if !args.files.is_empty() {
let first = &args.files[0]; // we know it's not empty
if first.is_dir() {
std::env::set_current_dir(&first)?;
editor.new_file(Action::VerticalSplit);
compositor.push(Box::new(ui::file_picker(first.clone())));
compositor.push(Box::new(ui::file_picker(".".into())));
} else {
let nr_of_files = args.files.len();
editor.open(first.to_path_buf(), Action::VerticalSplit)?;
@ -240,7 +245,7 @@ impl Application {
}
pub fn handle_idle_timeout(&mut self) {
use crate::commands::{completion, Context};
use crate::commands::{insert::idle_completion, Context};
use helix_view::document::Mode;
if doc_mut!(self.editor).mode != Mode::Insert || !self.config.editor.auto_completion {
@ -267,7 +272,7 @@ impl Application {
callback: None,
on_next_key_callback: None,
};
completion(&mut cx);
idle_completion(&mut cx);
self.render();
}
@ -548,10 +553,11 @@ impl Application {
message: diagnostic.message,
severity: diagnostic.severity.map(
|severity| match severity {
DiagnosticSeverity::Error => Error,
DiagnosticSeverity::Warning => Warning,
DiagnosticSeverity::Information => Info,
DiagnosticSeverity::Hint => Hint,
DiagnosticSeverity::ERROR => Error,
DiagnosticSeverity::WARNING => Warning,
DiagnosticSeverity::INFORMATION => Info,
DiagnosticSeverity::HINT => Hint,
severity => unimplemented!("{:?}", severity),
},
),
// code
@ -727,7 +733,9 @@ impl Application {
let mut stdout = stdout();
// reset cursor shape
write!(stdout, "\x1B[2 q")?;
execute!(stdout, DisableMouseCapture)?;
// Ignore errors on disabling, this might trigger on windows if we call
// disable without calling enable previously
let _ = execute!(stdout, DisableMouseCapture);
execute!(stdout, terminal::LeaveAlternateScreen)?;
terminal::disable_raw_mode()?;
Ok(())

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

File diff suppressed because it is too large Load Diff

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

@ -5,20 +5,20 @@ use helix_view::{document::Mode, info::Info, input::KeyEvent};
use serde::Deserialize;
use std::{
borrow::Cow,
collections::HashMap,
collections::{BTreeSet, HashMap},
ops::{Deref, DerefMut},
};
#[macro_export]
macro_rules! key {
($key:ident) => {
KeyEvent {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::$key,
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
}
};
($($ch:tt)*) => {
KeyEvent {
::helix_view::input::KeyEvent {
code: ::helix_view::keyboard::KeyCode::Char($($ch)*),
modifiers: ::helix_view::keyboard::KeyModifiers::NONE,
}
@ -78,19 +78,30 @@ macro_rules! keymap {
};
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone)]
pub struct KeyTrieNode {
/// A label for keys coming under this node, like "Goto mode"
#[serde(skip)]
name: String,
#[serde(flatten)]
map: HashMap<KeyEvent, KeyTrie>,
#[serde(skip)]
order: Vec<KeyEvent>,
#[serde(skip)]
pub is_sticky: bool,
}
impl<'de> Deserialize<'de> for KeyTrieNode {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let map = HashMap::<KeyEvent, KeyTrie>::deserialize(deserializer)?;
let order = map.keys().copied().collect::<Vec<_>>(); // NOTE: map.keys() has arbitrary order
Ok(Self {
map,
order,
..Default::default()
})
}
}
impl KeyTrieNode {
pub fn new(name: &str, map: HashMap<KeyEvent, KeyTrie>, order: Vec<KeyEvent>) -> Self {
Self {
@ -118,7 +129,6 @@ impl KeyTrieNode {
}
self.map.insert(key, trie);
}
for &key in self.map.keys() {
if !self.order.contains(&key) {
self.order.push(key);
@ -127,20 +137,29 @@ impl KeyTrieNode {
}
pub fn infobox(&self) -> Info {
let mut body: Vec<(&str, Vec<KeyEvent>)> = Vec::with_capacity(self.len());
let mut body: Vec<(&str, BTreeSet<KeyEvent>)> = Vec::with_capacity(self.len());
for (&key, trie) in self.iter() {
let desc = match trie {
KeyTrie::Leaf(cmd) => cmd.doc(),
KeyTrie::Leaf(cmd) => {
if cmd.name() == "no_op" {
continue;
}
cmd.doc()
}
KeyTrie::Node(n) => n.name(),
};
match body.iter().position(|(d, _)| d == &desc) {
// FIXME: multiple keys are ordered randomly (use BTreeSet)
Some(pos) => body[pos].1.push(key),
None => body.push((desc, vec![key])),
Some(pos) => {
body[pos].1.insert(key);
}
None => body.push((desc, BTreeSet::from([key]))),
}
}
body.sort_unstable_by_key(|(_, keys)| {
self.order.iter().position(|&k| k == keys[0]).unwrap()
self.order
.iter()
.position(|&k| k == *keys.iter().next().unwrap())
.unwrap()
});
let prefix = format!("{} ", self.name());
if body.iter().all(|(desc, _)| desc.starts_with(&prefix)) {
@ -151,6 +170,11 @@ impl KeyTrieNode {
}
Info::new(self.name(), body)
}
/// Get a reference to the key trie node's order.
pub fn order(&self) -> &[KeyEvent] {
self.order.as_slice()
}
}
impl Default for KeyTrieNode {
@ -235,6 +259,7 @@ pub enum KeymapResultKind {
/// Returned after looking up a key in [`Keymap`]. The `sticky` field has a
/// reference to the sticky node if one is currently active.
#[derive(Debug)]
pub struct KeymapResult<'a> {
pub kind: KeymapResultKind,
pub sticky: Option<&'a KeyTrieNode>,
@ -395,6 +420,7 @@ impl Default for Keymaps {
"F" => find_prev_char,
"r" => replace,
"R" => replace_with_yanked,
"A-." => repeat_last_motion,
"~" => switch_case,
"`" => switch_to_lowercase,
@ -427,6 +453,8 @@ impl Default for Keymaps {
"m" => goto_window_middle,
"b" => goto_window_bottom,
"a" => goto_last_accessed_file,
"n" => goto_next_buffer,
"p" => goto_previous_buffer,
},
":" => command_mode,
@ -476,10 +504,9 @@ impl Default for Keymaps {
},
"/" => search,
// ? for search_reverse
"?" => rsearch,
"n" => search_next,
"N" => extend_search_next,
// N for search_prev
"N" => search_prev,
"*" => search_selection,
"u" => undo,
@ -520,9 +547,13 @@ impl Default for Keymaps {
"C-w" => { "Window"
"C-w" | "w" => rotate_view,
"C-h" | "h" => hsplit,
"C-s" | "s" => hsplit,
"C-v" | "v" => vsplit,
"C-q" | "q" => wclose,
"C-h" | "h" | "left" => jump_view_left,
"C-j" | "j" | "down" => jump_view_down,
"C-k" | "k" | "up" => jump_view_up,
"C-l" | "l" | "right" => jump_view_right,
},
// move under <space>c
@ -621,6 +652,9 @@ impl Default for Keymaps {
"B" => extend_prev_long_word_start,
"E" => extend_next_long_word_end,
"n" => extend_search_next,
"N" => extend_search_prev,
"t" => extend_till_char,
"f" => extend_next_char,
"T" => extend_till_prev_char,
@ -669,6 +703,9 @@ pub fn merge_keys(mut config: Config) -> Config {
config
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn merge_partial_keys() {
let config = Config {
@ -729,3 +766,38 @@ fn merge_partial_keys() {
assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1);
assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0);
}
#[test]
fn order_should_be_set() {
let config = Config {
keys: Keymaps(hashmap! {
Mode::Normal => Keymap::new(
keymap!({ "Normal mode"
"space" => { ""
"s" => { ""
"v" => vsplit,
"c" => hsplit,
},
},
})
)
}),
..Default::default()
};
let mut merged_config = merge_keys(config.clone());
assert_ne!(config, merged_config);
let keymap = merged_config.keys.0.get_mut(&Mode::Normal).unwrap();
// Make sure mapping works
assert_eq!(
keymap
.root()
.search(&[key!(' '), key!('s'), key!('v')])
.unwrap(),
&KeyTrie::Leaf(Command::vsplit),
"Leaf should be present in merged subnode"
);
// Make sure an order was set during merge
let node = keymap.root().search(&[crate::key!(' ')]).unwrap();
assert!(!node.node().unwrap().order().is_empty())
}
}

@ -16,6 +16,11 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
};
// Separate file config so we can include year, month and day in file logs
let file = std::fs::OpenOptions::new()
.write(true)
.create(true)
.truncate(true)
.open(logpath)?;
let file_config = fern::Dispatch::new()
.format(|out, message, record| {
out.finish(format_args!(
@ -26,7 +31,7 @@ fn setup_logging(logpath: PathBuf, verbosity: u64) -> Result<()> {
message
))
})
.chain(fern::log_file(logpath)?);
.chain(file);
base_config.chain(file_config).apply()?;
@ -55,6 +60,7 @@ ARGS:
FLAGS:
-h, --help Prints help information
--tutor Loads the tutorial
-v Increases logging verbosity each use for up to 3 times
(default file: {})
-V, --version Prints version information

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

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

@ -64,25 +64,23 @@ impl<T: Item> Menu<T> {
}
pub fn score(&mut self, pattern: &str) {
// need to borrow via pattern match otherwise it complains about simultaneous borrow
let Self {
ref mut matcher,
ref mut matches,
ref options,
..
} = *self;
// reuse the matches allocation
matches.clear();
matches.extend(options.iter().enumerate().filter_map(|(index, option)| {
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
let text = option.filter_text();
// TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher
self.matcher
.fuzzy_match(text, pattern)
.map(|score| (index, score))
}));
}),
);
// matches.sort_unstable_by_key(|(_, score)| -score);
matches.sort_unstable_by_key(|(index, _score)| options[*index].sort_text());
self.matches
.sort_unstable_by_key(|(index, _score)| self.options[*index].sort_text());
// reset cursor position
self.cursor = None;
@ -100,7 +98,8 @@ impl<T: Item> Menu<T> {
pub fn move_up(&mut self) {
let len = self.matches.len();
let pos = self.cursor.map_or(0, |i| (i + len.saturating_sub(1)) % len) % len;
let max_index = len.saturating_sub(1);
let pos = self.cursor.map_or(max_index, |i| (i + max_index) % len) % len;
self.cursor = Some(pos);
self.adjust_scroll();
}
@ -216,6 +215,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_up();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);
@ -233,6 +236,10 @@ impl<T: Item + 'static> Component for Menu<T> {
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
} => {
self.move_down();
(self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update);

@ -29,6 +29,7 @@ pub fn regex_prompt(
cx: &mut crate::commands::Context,
prompt: std::borrow::Cow<'static, str>,
history_register: Option<char>,
completion_fn: impl FnMut(&str) -> Vec<prompt::Completion> + 'static,
fun: impl Fn(&mut View, &mut Document, Regex, PromptEvent) + 'static,
) -> Prompt {
let (view, doc) = current!(cx.editor);
@ -38,7 +39,7 @@ pub fn regex_prompt(
Prompt::new(
prompt,
history_register,
|_input: &str| Vec::new(), // this is fine because Vec::new() doesn't allocate
completion_fn,
move |cx: &mut crate::compositor::Context, input: &str, event: PromptEvent| {
match event {
PromptEvent::Abort => {
@ -92,9 +93,25 @@ pub fn regex_prompt(
}
pub fn file_picker(root: PathBuf) -> FilePicker<PathBuf> {
use ignore::Walk;
use ignore::{types::TypesBuilder, WalkBuilder};
use std::time;
let files = Walk::new(&root).filter_map(|entry| {
// We want to exclude files that the editor can't handle yet
let mut type_builder = TypesBuilder::new();
let mut walk_builder = WalkBuilder::new(&root);
let walk_builder = match type_builder.add(
"compressed",
"*.{zip,gz,bz2,zst,lzo,sz,tgz,tbz2,lz,lz4,lzma,lzo,z,Z,xz,7z,rar,cab}",
) {
Err(_) => &walk_builder,
_ => {
type_builder.negate("all");
let excluded_types = type_builder.build().unwrap();
walk_builder.types(excluded_types)
}
};
let files = walk_builder.build().filter_map(|entry| {
let entry = entry.ok()?;
// Path::is_dir() traverses symlinks, so we use it over DirEntry::is_dir
if entry.path().is_dir() {

@ -12,7 +12,12 @@ use fuzzy_matcher::skim::SkimMatcherV2 as Matcher;
use fuzzy_matcher::FuzzyMatcher;
use tui::widgets::Widget;
use std::{borrow::Cow, collections::HashMap, path::PathBuf};
use std::{
borrow::Cow,
collections::HashMap,
io::Read,
path::{Path, PathBuf},
};
use crate::ui::{Prompt, PromptEvent};
use helix_core::Position;
@ -23,18 +28,58 @@ use helix_view::{
};
pub const MIN_SCREEN_WIDTH_FOR_PREVIEW: u16 = 80;
/// Biggest file size to preview in bytes
pub const MAX_FILE_SIZE_FOR_PREVIEW: u64 = 10 * 1024 * 1024;
/// File path and line number (used to align and highlight a line)
/// File path and range of lines (used to align and highlight lines)
type FileLocation = (PathBuf, Option<(usize, usize)>);
pub struct FilePicker<T> {
picker: Picker<T>,
/// Caches paths to documents
preview_cache: HashMap<PathBuf, Document>,
preview_cache: HashMap<PathBuf, CachedPreview>,
read_buffer: Vec<u8>,
/// Given an item in the picker, return the file path and line number to display.
file_fn: Box<dyn Fn(&Editor, &T) -> Option<FileLocation>>,
}
pub enum CachedPreview {
Document(Document),
Binary,
LargeFile,
NotFound,
}
// We don't store this enum in the cache so as to avoid lifetime constraints
// from borrowing a document already opened in the editor.
pub enum Preview<'picker, 'editor> {
Cached(&'picker CachedPreview),
EditorDocument(&'editor Document),
}
impl Preview<'_, '_> {
fn document(&self) -> Option<&Document> {
match self {
Preview::EditorDocument(doc) => Some(doc),
Preview::Cached(CachedPreview::Document(doc)) => Some(doc),
_ => None,
}
}
/// Alternate text to show for the preview.
fn placeholder(&self) -> &str {
match *self {
Self::EditorDocument(_) => "<File preview>",
Self::Cached(preview) => match preview {
CachedPreview::Document(_) => "<File preview>",
CachedPreview::Binary => "<Binary file>",
CachedPreview::LargeFile => "<File too large to preview>",
CachedPreview::NotFound => "<File not found>",
},
}
}
}
impl<T> FilePicker<T> {
pub fn new(
options: Vec<T>,
@ -45,6 +90,7 @@ impl<T> FilePicker<T> {
Self {
picker: Picker::new(false, options, format_fn, callback_fn),
preview_cache: HashMap::new(),
read_buffer: Vec::with_capacity(1024),
file_fn: Box::new(preview_fn),
}
}
@ -60,14 +106,45 @@ impl<T> FilePicker<T> {
})
}
fn calculate_preview(&mut self, editor: &Editor) {
if let Some((path, _line)) = self.current_file(editor) {
if !self.preview_cache.contains_key(&path) && editor.document_by_path(&path).is_none() {
// TODO: enable syntax highlighting; blocked by async rendering
let doc = Document::open(&path, None, Some(&editor.theme), None).unwrap();
self.preview_cache.insert(path, doc);
/// Get (cached) preview for a given path. If a document corresponding
/// to the path is already open in the editor, it is used instead.
fn get_preview<'picker, 'editor>(
&'picker mut self,
path: &Path,
editor: &'editor Editor,
) -> Preview<'picker, 'editor> {
if let Some(doc) = editor.document_by_path(path) {
return Preview::EditorDocument(doc);
}
if self.preview_cache.contains_key(path) {
return Preview::Cached(&self.preview_cache[path]);
}
let data = std::fs::File::open(path).and_then(|file| {
let metadata = file.metadata()?;
// Read up to 1kb to detect the content type
let n = file.take(1024).read_to_end(&mut self.read_buffer)?;
let content_type = content_inspector::inspect(&self.read_buffer[..n]);
self.read_buffer.clear();
Ok((metadata, content_type))
});
let preview = data
.map(
|(metadata, content_type)| match (metadata.len(), content_type) {
(_, content_inspector::ContentType::BINARY) => CachedPreview::Binary,
(size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => CachedPreview::LargeFile,
_ => {
// TODO: enable syntax highlighting; blocked by async rendering
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 | | |
// | | | |
// +---------+ +---------+
self.calculate_preview(cx.editor);
let render_preview = area.width > MIN_SCREEN_WIDTH_FOR_PREVIEW;
let area = inner_rect(area);
// -- Render the frame:
// clear area
let background = cx.editor.theme.get("ui.background");
let text = cx.editor.theme.get("ui.text");
surface.clear_with(area, background);
let picker_width = if render_preview {
@ -113,17 +190,23 @@ impl<T: 'static> Component for FilePicker<T> {
horizontal: 1,
};
let inner = inner.inner(&margin);
block.render(preview_area, surface);
if let Some((doc, line)) = self.current_file(cx.editor).and_then(|(path, range)| {
cx.editor
.document_by_path(&path)
.or_else(|| self.preview_cache.get(&path))
.zip(Some(range))
}) {
if let Some((path, range)) = self.current_file(cx.editor) {
let preview = self.get_preview(&path, cx.editor);
let doc = match preview.document() {
Some(doc) => doc,
None => {
let alt_text = preview.placeholder();
let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2;
let y = inner.y + inner.height / 2;
surface.set_stringn(x, y, alt_text, inner.width as usize, text);
return;
}
};
// align to middle
let first_line = line
let first_line = range
.map(|(start, end)| {
let height = end.saturating_sub(start) + 1;
let middle = start + (height.saturating_sub(1) / 2);
@ -150,7 +233,7 @@ impl<T: 'static> Component for FilePicker<T> {
);
// highlight the line
if let Some((start, end)) = line {
if let Some((start, end)) = range {
let offset = start.saturating_sub(first_line) as u16;
surface.set_style(
Rect::new(
@ -234,37 +317,28 @@ impl<T> Picker<T> {
}
pub fn score(&mut self) {
// need to borrow via pattern match otherwise it complains about simultaneous borrow
let Self {
ref mut matcher,
ref mut matches,
ref filters,
ref format_fn,
..
} = *self;
let pattern = &self.prompt.line;
// reuse the matches allocation
matches.clear();
matches.extend(
self.matches.clear();
self.matches.extend(
self.options
.iter()
.enumerate()
.filter_map(|(index, option)| {
// filter options first before matching
if !filters.is_empty() {
filters.binary_search(&index).ok()?;
if !self.filters.is_empty() {
self.filters.binary_search(&index).ok()?;
}
// TODO: maybe using format_fn isn't the best idea here
let text = (format_fn)(option);
let text = (self.format_fn)(option);
// TODO: using fuzzy_indices could give us the char idx for match highlighting
matcher
self.matcher
.fuzzy_match(&text, pattern)
.map(|score| (index, score))
}),
);
matches.sort_unstable_by_key(|(_, score)| -score);
self.matches.sort_unstable_by_key(|(_, score)| -score);
// reset cursor position
self.cursor = 0;
@ -337,6 +411,10 @@ impl<T: 'static> Component for Picker<T> {
code: KeyCode::BackTab,
..
}
| KeyEvent {
code: KeyCode::Char('k'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('p'),
modifiers: KeyModifiers::CONTROL,
@ -350,6 +428,10 @@ impl<T: 'static> Component for Picker<T> {
| KeyEvent {
code: KeyCode::Tab, ..
}
| KeyEvent {
code: KeyCode::Char('j'),
modifiers: KeyModifiers::CONTROL,
}
| KeyEvent {
code: KeyCode::Char('n'),
modifiers: KeyModifiers::CONTROL,
@ -375,7 +457,7 @@ impl<T: 'static> Component for Picker<T> {
return close_fn;
}
KeyEvent {
code: KeyCode::Char('h'),
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
if let Some(option) = self.selection() {
@ -485,6 +567,7 @@ impl<T: 'static> Component for Picker<T> {
text_style
},
true,
true,
);
}
}

@ -186,6 +186,11 @@ impl Prompt {
self.exit_selection();
}
pub fn insert_str(&mut self, s: &str) {
self.line.insert_str(self.cursor, s);
self.cursor += s.len();
}
pub fn move_cursor(&mut self, movement: Movement) {
let pos = self.eval_movement(movement);
self.cursor = pos
@ -474,6 +479,26 @@ impl Component for Prompt {
self.delete_char_backwards();
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
KeyEvent {
code: KeyCode::Char('s'),
modifiers: KeyModifiers::CONTROL,
} => {
let (view, doc) = current!(cx.editor);
let text = doc.text().slice(..);
use helix_core::textobject;
let range = textobject::textobject_word(
text,
doc.selection(view.id).primary(),
textobject::TextObject::Inside,
1,
);
let line = text.slice(range.from()..range.to()).to_string();
if !line.is_empty() {
self.insert_str(line.as_str());
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
KeyEvent {
code: KeyCode::Enter,
..
@ -502,6 +527,7 @@ impl Component for Prompt {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
KeyEvent {
@ -515,15 +541,22 @@ impl Component for Prompt {
if let Some(register) = self.history_register {
let register = cx.editor.registers.get_mut(register);
self.change_history(register.read(), CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update);
}
}
KeyEvent {
code: KeyCode::Tab, ..
} => self.change_completion_selection(CompletionDirection::Forward),
} => {
self.change_completion_selection(CompletionDirection::Forward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
KeyEvent {
code: KeyCode::BackTab,
..
} => self.change_completion_selection(CompletionDirection::Backward),
} => {
self.change_completion_selection(CompletionDirection::Backward);
(self.callback_fn)(cx, &self.line, PromptEvent::Update)
}
KeyEvent {
code: KeyCode::Char('q'),
modifiers: KeyModifiers::CONTROL,

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

@ -266,12 +266,14 @@ impl Buffer {
where
S: AsRef<str>,
{
self.set_string_truncated(x, y, string, width, style, false)
self.set_string_truncated(x, y, string, width, style, false, false)
}
/// Print at most the first `width` characters of a string if enough space is available
/// until the end of the line. If `markend` is true appends a `…` at the end of
/// truncated lines.
/// until the end of the line. If `ellipsis` is true appends a `…` at the end of
/// truncated lines. If `truncate_start` is `true`, truncate the beginning of the string
/// instead of the end.
#[allow(clippy::too_many_arguments)]
pub fn set_string_truncated<S>(
&mut self,
x: u16,
@ -280,6 +282,7 @@ impl Buffer {
width: usize,
style: Style,
ellipsis: bool,
truncate_start: bool,
) -> (u16, u16)
where
S: AsRef<str>,
@ -289,6 +292,7 @@ impl Buffer {
let width = if ellipsis { width - 1 } else { width };
let graphemes = UnicodeSegmentation::graphemes(string.as_ref(), true);
let max_offset = min(self.area.right() as usize, width.saturating_add(x as usize));
if !truncate_start {
for s in graphemes {
let width = s.width();
if width == 0 {
@ -312,6 +316,36 @@ impl Buffer {
if ellipsis && x_offset - (x as usize) < string.as_ref().width() {
self.content[index].set_symbol("…");
}
} else {
let mut start_index = self.index_of(x, y);
let mut index = self.index_of(max_offset as u16, y);
let total_width = string.as_ref().width();
let truncated = total_width > width;
if ellipsis && truncated {
self.content[start_index].set_symbol("…");
start_index += 1;
}
if !truncated {
index -= width - total_width;
}
for s in graphemes.rev() {
let width = s.width();
if width == 0 {
continue;
}
let start = index - width;
if start < start_index {
break;
}
self.content[start].set_symbol(s);
self.content[start].set_style(style);
for i in start + 1..index {
self.content[i].reset();
}
index -= width;
}
}
(x_offset as u16, y)
}

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

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

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

@ -2,7 +2,7 @@ use crate::{
clipboard::{get_clipboard_provider, ClipboardProvider},
graphics::{CursorKind, Rect},
theme::{self, Theme},
tree::Tree,
tree::{self, Tree},
Document, DocumentId, View, ViewId,
};
@ -12,6 +12,7 @@ use tokio_stream::wrappers::UnboundedReceiverStream;
use std::{
collections::HashMap,
collections::BTreeMap,
path::{Path, PathBuf},
pin::Pin,
sync::Arc,
@ -19,8 +20,6 @@ use std::{
use tokio::time::{sleep, Duration, Instant, Sleep};
use slotmap::SlotMap;
use anyhow::Error;
pub use helix_core::diagnostic::Severity;
@ -63,6 +62,9 @@ pub struct Config {
/// Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. Defaults to 400ms.
#[serde(skip_serializing, deserialize_with = "deserialize_duration_millis")]
pub idle_timeout: Duration,
pub completion_trigger_len: u8,
/// Whether to display infoboxes. Defaults to true.
pub auto_info: bool,
}
#[derive(Debug, Clone, PartialEq, Eq, Deserialize)]
@ -92,14 +94,29 @@ impl Default for Config {
auto_pairs: true,
auto_completion: true,
idle_timeout: Duration::from_millis(400),
completion_trigger_len: 2,
auto_info: true,
}
}
}
pub struct Motion(pub Box<dyn Fn(&mut Editor)>);
impl Motion {
pub fn run(&self, e: &mut Editor) {
(self.0)(e)
}
}
impl std::fmt::Debug for Motion {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("motion")
}
}
#[derive(Debug)]
pub struct Editor {
pub tree: Tree,
pub documents: SlotMap<DocumentId, Document>,
pub next_document_id: usize,
pub documents: BTreeMap<DocumentId, Document>,
pub count: Option<std::num::NonZeroUsize>,
pub selected_register: Option<char>,
pub registers: Registers,
@ -124,6 +141,7 @@ pub struct Editor {
pub config: Config,
pub idle_timer: Pin<Box<Sleep>>,
pub last_motion: Option<Motion>,
}
#[derive(Debug, Copy, Clone)]
@ -148,7 +166,8 @@ impl Editor {
Self {
tree: Tree::new(area),
documents: SlotMap::with_key(),
next_document_id: 0,
documents: BTreeMap::new(),
count: None,
selected_register: None,
theme: themes.default(),
@ -166,6 +185,7 @@ impl Editor {
clipboard_provider: get_clipboard_provider(),
status_msg: None,
idle_timer: Box::pin(sleep(config.idle_timeout)),
last_motion: None,
config,
}
}
@ -221,7 +241,7 @@ impl Editor {
fn _refresh(&mut self) {
for (view, _) in self.tree.views_mut() {
let doc = &self.documents[view.doc];
let doc = &self.documents[&view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff)
}
}
@ -230,22 +250,38 @@ impl Editor {
use crate::tree::Layout;
use helix_core::Selection;
if !self.documents.contains_key(id) {
if !self.documents.contains_key(&id) {
log::error!("cannot switch to document that does not exist (anymore)");
return;
}
match action {
Action::Replace => {
let view = view!(self);
let jump = (
view.doc,
self.documents[view.doc].selection(view.id).clone(),
);
let (view, doc) = current_ref!(self);
// If the current view is an empty scratch buffer and is not displayed in any other views, delete it.
// Boolean value is determined before the call to `view_mut` because the operation requires a borrow
// of `self.tree`, which is mutably borrowed when `view_mut` is called.
let remove_empty_scratch = !doc.is_modified()
// If the buffer has no path and is not modified, it is an empty scratch buffer.
&& doc.path().is_none()
// If the buffer we are changing to is not this buffer
&& id != doc.id
// Ensure the buffer is not displayed in any other splits.
&& !self
.tree
.traverse()
.any(|(_, v)| v.doc == doc.id && v.id != view.id);
let view = view_mut!(self);
if remove_empty_scratch {
// Copy `doc.id` into a variable before calling `self.documents.remove`, which requires a mutable
// borrow, invalidating direct access to `doc.id`.
let id = doc.id;
self.documents.remove(&id);
} else {
let jump = (view.doc, doc.selection(view.id).clone());
view.jumps.push(jump);
view.last_accessed_doc = Some(view.doc);
}
view.doc = id;
view.offset = Position::default();
@ -272,14 +308,14 @@ impl Editor {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Horizontal);
// initialize selection for view
let doc = &mut self.documents[id];
let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
}
Action::VerticalSplit => {
let view = View::new(id);
let view_id = self.tree.split(view, Layout::Vertical);
// initialize selection for view
let doc = &mut self.documents[id];
let doc = self.documents.get_mut(&id).unwrap();
doc.selections.insert(view_id, Selection::point(0));
}
}
@ -288,9 +324,11 @@ impl Editor {
}
pub fn new_file(&mut self, action: Action) -> DocumentId {
let doc = Document::default();
let id = self.documents.insert(doc);
self.documents[id].id = id;
let id = DocumentId(self.next_document_id);
self.next_document_id += 1;
let mut doc = Document::default();
doc.id = id;
self.documents.insert(id, doc);
self.switch(id, action);
id
}
@ -313,7 +351,11 @@ impl Editor {
self.language_servers
.get(language)
.map_err(|e| {
log::error!("Failed to get LSP, {}, for `{}`", e, language.scope())
log::error!(
"Failed to initialize the LSP for `{}` {{ {} }}",
language.scope(),
e
)
})
.ok()
});
@ -336,8 +378,10 @@ impl Editor {
doc.set_language_server(Some(language_server));
}
let id = self.documents.insert(doc);
self.documents[id].id = id;
let id = DocumentId(self.next_document_id);
self.next_document_id += 1;
doc.id = id;
self.documents.insert(id, doc);
id
};
@ -348,16 +392,20 @@ impl Editor {
pub fn close(&mut self, id: ViewId, close_buffer: bool) {
let view = self.tree.get(self.tree.focus);
// remove selection
self.documents[view.doc].selections.remove(&id);
self.documents
.get_mut(&view.doc)
.unwrap()
.selections
.remove(&id);
if close_buffer {
// get around borrowck issues
let doc = &self.documents[view.doc];
let doc = &self.documents[&view.doc];
if let Some(language_server) = doc.language_server() {
tokio::spawn(language_server.text_document_did_close(doc.identifier()));
}
self.documents.remove(view.doc);
self.documents.remove(&view.doc);
}
self.tree.remove(id);
@ -374,24 +422,40 @@ impl Editor {
self.tree.focus_next();
}
pub fn focus_right(&mut self) {
self.tree.focus_direction(tree::Direction::Right);
}
pub fn focus_left(&mut self) {
self.tree.focus_direction(tree::Direction::Left);
}
pub fn focus_up(&mut self) {
self.tree.focus_direction(tree::Direction::Up);
}
pub fn focus_down(&mut self) {
self.tree.focus_direction(tree::Direction::Down);
}
pub fn should_close(&self) -> bool {
self.tree.is_empty()
}
pub fn ensure_cursor_in_view(&mut self, id: ViewId) {
let view = self.tree.get_mut(id);
let doc = &self.documents[view.doc];
let doc = &self.documents[&view.doc];
view.ensure_cursor_in_view(doc, self.config.scrolloff)
}
#[inline]
pub fn document(&self, id: DocumentId) -> Option<&Document> {
self.documents.get(id)
self.documents.get(&id)
}
#[inline]
pub fn document_mut(&mut self, id: DocumentId) -> Option<&mut Document> {
self.documents.get_mut(id)
self.documents.get_mut(&id)
}
#[inline]
@ -416,7 +480,7 @@ impl Editor {
pub fn cursor(&self) -> (Option<Position>, CursorKind) {
let view = view!(self);
let doc = &self.documents[view.doc];
let doc = &self.documents[&view.doc];
let cursor = doc
.selection(view.id)
.primary()

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

@ -8,7 +8,7 @@ use crate::keyboard::{KeyCode, KeyModifiers};
/// Represents a key event.
// We use a newtype here because we want to customize Deserialize and Display.
#[derive(Debug, PartialEq, Eq, PartialOrd, Clone, Copy, Hash)]
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Clone, Copy, Hash)]
pub struct KeyEvent {
pub code: KeyCode,
pub modifiers: KeyModifiers,

@ -54,7 +54,7 @@ impl From<crossterm::event::KeyModifiers> for KeyModifiers {
}
/// Represents a key.
#[derive(Debug, PartialOrd, PartialEq, Eq, Clone, Copy, Hash)]
#[derive(Debug, PartialOrd, Ord, PartialEq, Eq, Clone, Copy, Hash)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub enum KeyCode {
/// Backspace key.

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

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

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

@ -47,13 +47,21 @@ impl Node {
// TODO: screen coord to container + container coordinate helpers
#[derive(Debug, PartialEq, Eq)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Layout {
Horizontal,
Vertical,
// could explore stacked/tabbed
}
#[derive(Debug, Clone, Copy)]
pub enum Direction {
Up,
Down,
Left,
Right,
}
#[derive(Debug)]
pub struct Container {
layout: Layout,
@ -150,7 +158,6 @@ impl Tree {
} => container,
_ => unreachable!(),
};
if container.layout == layout {
// insert node after the current item if there is children already
let pos = if container.children.is_empty() {
@ -393,6 +400,112 @@ impl Tree {
Traverse::new(self)
}
// Finds the split in the given direction if it exists
pub fn find_split_in_direction(&self, id: ViewId, direction: Direction) -> Option<ViewId> {
let parent = self.nodes[id].parent;
// Base case, we found the root of the tree
if parent == id {
return None;
}
// Parent must always be a container
let parent_container = match &self.nodes[parent].content {
Content::Container(container) => container,
Content::View(_) => unreachable!(),
};
match (direction, parent_container.layout) {
(Direction::Up, Layout::Vertical)
| (Direction::Left, Layout::Horizontal)
| (Direction::Right, Layout::Horizontal)
| (Direction::Down, Layout::Vertical) => {
// The desired direction of movement is not possible within
// the parent container so the search must continue closer to
// the root of the split tree.
self.find_split_in_direction(parent, direction)
}
(Direction::Up, Layout::Horizontal)
| (Direction::Down, Layout::Horizontal)
| (Direction::Left, Layout::Vertical)
| (Direction::Right, Layout::Vertical) => {
// It's possible to move in the desired direction within
// the parent container so an attempt is made to find the
// correct child.
match self.find_child(id, &parent_container.children, direction) {
// Child is found, search is ended
Some(id) => Some(id),
// A child is not found. This could be because of either two scenarios
// 1. Its not possible to move in the desired direction, and search should end
// 2. A layout like the following with focus at X and desired direction Right
// | _ | x | |
// | _ _ _ | |
// | _ _ _ | |
// The container containing X ends at X so no rightward movement is possible
// however there still exists another view/container to the right that hasn't
// been explored. Thus another search is done here in the parent container
// before concluding it's not possible to move in the desired direction.
None => self.find_split_in_direction(parent, direction),
}
}
}
}
fn find_child(&self, id: ViewId, children: &[ViewId], direction: Direction) -> Option<ViewId> {
let mut child_id = match direction {
// index wise in the child list the Up and Left represents a -1
// thus reversed iterator.
Direction::Up | Direction::Left => children
.iter()
.rev()
.skip_while(|i| **i != id)
.copied()
.nth(1)?,
// Down and Right => +1 index wise in the child list
Direction::Down | Direction::Right => {
children.iter().skip_while(|i| **i != id).copied().nth(1)?
}
};
let (current_x, current_y) = match &self.nodes[self.focus].content {
Content::View(current_view) => (current_view.area.left(), current_view.area.top()),
Content::Container(_) => unreachable!(),
};
// If the child is a container the search finds the closest container child
// visually based on screen location.
while let Content::Container(container) = &self.nodes[child_id].content {
match (direction, container.layout) {
(_, Layout::Vertical) => {
// find closest split based on x because y is irrelevant
// in a vertical container (and already correct based on previous search)
child_id = *container.children.iter().min_by_key(|id| {
let x = match &self.nodes[**id].content {
Content::View(view) => view.inner_area().left(),
Content::Container(container) => container.area.left(),
};
(current_x as i16 - x as i16).abs()
})?;
}
(_, Layout::Horizontal) => {
// find closest split based on y because x is irrelevant
// in a horizontal container (and already correct based on previous search)
child_id = *container.children.iter().min_by_key(|id| {
let y = match &self.nodes[**id].content {
Content::View(view) => view.inner_area().top(),
Content::Container(container) => container.area.top(),
};
(current_y as i16 - y as i16).abs()
})?;
}
}
}
Some(child_id)
}
pub fn focus_direction(&mut self, direction: Direction) {
if let Some(id) = self.find_split_in_direction(self.focus, direction) {
self.focus = id;
}
}
pub fn focus_next(&mut self) {
// This function is very dumb, but that's because we don't store any parent links.
// (we'd be able to go parent.next_sibling() recursively until we find something)
@ -420,13 +533,12 @@ impl Tree {
// if found = container -> found = first child
// }
let iter = self.traverse();
let mut iter = iter.skip_while(|&(key, _view)| key != self.focus);
iter.next(); // take the focused value
if let Some((key, _)) = iter.next() {
self.focus = key;
let mut views = self
.traverse()
.skip_while(|&(id, _view)| id != self.focus)
.skip(1); // Skip focused value
if let Some((id, _)) = views.next() {
self.focus = id;
} else {
// extremely crude, take the first item again
let (key, _) = self.traverse().next().unwrap();
@ -472,3 +584,64 @@ 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 helix_core::{
coords_at_pos,
graphemes::{grapheme_width, RopeGraphemes},
line_ending::line_end_char_index,
Position, RopeSlice, Selection,
visual_coords_at_pos, Position, RopeSlice, Selection,
};
type Jump = (DocumentId, Selection);
@ -91,7 +90,10 @@ impl View {
.selection(self.id)
.primary()
.cursor(doc.text().slice(..));
let Position { col, row: line } = coords_at_pos(doc.text().slice(..), cursor);
let Position { col, row: line } =
visual_coords_at_pos(doc.text().slice(..), cursor, doc.tab_width());
let inner_area = self.inner_area();
let last_line = (self.offset.row + inner_area.height as usize).saturating_sub(1);

@ -312,7 +312,7 @@ injection-regex = "php"
file-types = ["php"]
roots = []
indent = { tab-width = 2, unit = " " }
indent = { tab-width = 4, unit = " " }
[[language]]
name = "latex"
@ -458,3 +458,12 @@ file-types = ["scm"]
roots = []
comment-token = ";"
indent = { tab-width = 2, unit = " " }
[[language]]
name = "cmake"
scope = "source.cmake"
file-types = ["cmake", "CMakeLists.txt"]
roots = []
comment-token = "#"
indent = { tab-width = 2, unit = " " }
language-server = { command = "cmake-language-server" }

@ -7,7 +7,7 @@
(command_name) @function
(variable_name) @property
(variable_name) @variable.other.member
[
"case"
@ -31,7 +31,7 @@
(function_definition name: (word) @function)
(file_descriptor) @number
(file_descriptor) @constant.numeric.integer
[
(command_substitution)

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

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

@ -0,0 +1,97 @@
[
(quoted_argument)
(bracket_argument)
] @string
(variable) @variable
[
(bracket_comment)
(line_comment)
] @comment
(normal_command (identifier) @function)
["ENV" "CACHE"] @string.special.symbol
["$" "{" "}" "<" ">"] @punctuation
["(" ")"] @punctuation.bracket
[
(function)
(endfunction)
(macro)
(endmacro)
] @keyword.function
[
(if)
(elseif)
(else)
(endif)
] @keyword.control.conditional
[
(foreach)
(endforeach)
(while)
(endwhile)
] @keyword.control.repeat
(function_command
(function)
. (argument) @function
(argument)* @variable.parameter
)
(macro_command
(macro)
. (argument) @function.macro
(argument)* @variable.parameter
)
(normal_command
(identifier) @function.builtin
. (argument) @variable
(#match? @function.builtin "^(?i)(set)$"))
(normal_command
(identifier) @function.builtin
. (argument)
(argument) @constant
(#match? @constant "^(?:PARENT_SCOPE|CACHE)$")
(#match? @function.builtin "^(?i)(unset)$"))
(normal_command
(identifier) @function.builtin
. (argument)
. (argument)
(argument) @constant
(#match? @constant "^(?:PARENT_SCOPE|CACHE|FORCE)$")
(#match? @function.builtin "^(?i)(set)$")
)
((argument) @constant.builtin.boolean
(#match? @constant.builtin.boolean "^(?i)(?:1|on|yes|true|y|0|off|no|false|n|ignore|notfound|.*-notfound)$")
)
(if_command
(if)
(argument) @operator
(#match? @operator "^(?:NOT|AND|OR|COMMAND|POLICY|TARGET|TEST|DEFINED|IN_LIST|EXISTS|IS_NEWER_THAN|IS_DIRECTORY|IS_SYMLINK|IS_ABSOLUTE|MATCHES|LESS|GREATER|EQUAL|LESS_EQUAL|GREATER_EQUAL|STRLESS|STRGREATER|STREQUAL|STRLESS_EQUAL|STRGREATER_EQUAL|VERSION_LESS|VERSION_GREATER|VERSION_EQUAL|VERSION_LESS_EQUAL|VERSION_GREATER_EQUAL)$")
)
(normal_command
(identifier) @function.builtin
. (argument)
(argument) @constant
(#match? @constant "^(?:ALL|COMMAND|DEPENDS|BYPRODUCTS|WORKING_DIRECTORY|COMMENT|JOB_POOL|VERBATIM|USES_TERMINAL|COMMAND_EXPAND_LISTS|SOURCES)$")
(#match? @function.builtin "^(?i)(add_custom_target)$")
)
(normal_command
(identifier) @function.builtin
(argument) @constant
(#match? @constant "^(?:OUTPUT|COMMAND|MAIN_DEPENDENCY|DEPENDS|BYPRODUCTS|IMPLICIT_DEPENDS|WORKING_DIRECTORY|COMMENT|DEPFILE|JOB_POOL|VERBATIM|APPEND|USES_TERMINAL|COMMAND_EXPAND_LISTS)$")
(#match? @function.builtin "^(?i)(add_custom_command)$")
)

@ -3,7 +3,7 @@
; Functions
(call_expression
function: (scoped_identifier
function: (qualified_identifier
name: (identifier) @function))
(template_function
@ -13,15 +13,14 @@
name: (field_identifier) @function)
(template_function
name: (scoped_identifier
name: (identifier) @function))
name: (identifier) @function)
(function_declarator
declarator: (scoped_identifier
declarator: (qualified_identifier
name: (identifier) @function))
(function_declarator
declarator: (scoped_identifier
declarator: (qualified_identifier
name: (identifier) @function))
(function_declarator

@ -26,11 +26,11 @@
(pseudo_element_selector (tag_name) @attribute)
(pseudo_class_selector (class_name) @attribute)
(class_name) @property
(id_name) @property
(namespace_name) @property
(property_name) @property
(feature_name) @property
(class_name) @variable.other.member
(id_name) @variable.other.member
(namespace_name) @variable.other.member
(property_name) @variable.other.member
(feature_name) @variable.other.member
(attribute_name) @attribute
@ -55,8 +55,8 @@
(string_value) @string
(color_value) @string.special
(integer_value) @number
(float_value) @number
(integer_value) @constant.numeric.integer
(float_value) @constant.numeric.float
(unit) @type
"#" @punctuation.delimiter

@ -1,125 +1,210 @@
["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword
; The following code originates mostly from
; https://github.com/elixir-lang/tree-sitter-elixir, with minor edits to
; align the captures with helix. The following should be considered
; Copyright 2021 The Elixir Team
;
; Licensed under the Apache License, Version 2.0 (the "License");
; you may not use this file except in compliance with the License.
; You may obtain a copy of the License at
;
; https://www.apache.org/licenses/LICENSE-2.0
;
; Unless required by applicable law or agreed to in writing, software
; distributed under the License is distributed on an "AS IS" BASIS,
; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
; See the License for the specific language governing permissions and
; limitations under the License.
; 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)
])
(operator_identifier) @operator
(unary_operator
operator: _ @operator)
[(true) (false) (nil)] @constant.builtin
(binary_operator
operator: _ @operator)
(keyword
[(keyword_literal)
":"] @tag)
(dot
operator: _ @operator)
(keyword
(keyword_string
[(string_start)
(string_content)
(string_end)] @tag))
(stab_clause
operator: _ @operator)
[(atom_literal)
(atom_start)
(atom_content)
(atom_end)] @tag
; Literals
[(comment)
(unused_identifier)] @comment
(nil) @constant.builtin
(escape_sequence) @escape
(boolean) @constant.builtin.boolean
(integer) @constant.numeric.integer
(float) @constant.numeric.float
(call function: (function_identifier) @keyword
(#match? @keyword "^(defmodule|defexception|defp|def|with|case|cond|raise|import|require|use|defmacrop|defmacro|defguardp|defguard|defdelegate|defstruct|alias|defimpl|defprotocol|defoverridable|receive|if|for|try|throw|unless|reraise|super|quote|unquote|unquote_splicing)$"))
(alias) @type
(call function: (function_identifier) @keyword
[(call
function: (function_identifier) @function
(arguments
[(identifier) @variable.parameter
(_ (identifier) @variable.parameter)
(_ (_ (identifier) @variable.parameter))
(_ (_ (_ (identifier) @variable.parameter)))
(_ (_ (_ (_ (identifier) @variable.parameter))))
(_ (_ (_ (_ (_ (identifier) @variable.parameter)))))]))
(binary_op
left:
(call
function: (function_identifier) @function
target: (dot
left: (atom) @type))
(char) @constant.character
; Quoted content
(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded
(escape_sequence) @constant.character.escape
[
(atom)
(quoted_atom)
(keyword)
(quoted_keyword)
] @string.special.symbol
[
(string)
(charlist)
] @string
; Note that we explicitly target sigil quoted start/end, so they are not overridden by delimiters
(sigil
(sigil_name) @__name__
quoted_start: _ @string
quoted_end: _ @string
(#match? @__name__ "^[sS]$")) @string
(sigil
(sigil_name) @__name__
quoted_start: _ @string.regexp
quoted_end: _ @string.regexp
(#match? @__name__ "^[rR]$")) @string.regexp
(sigil
(sigil_name) @__name__
quoted_start: _ @string.special
quoted_end: _ @string.special) @string.special
; Calls
; * definition keyword
(call
target: (identifier) @keyword
(#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$"))
; * kernel or special forms keyword
(call
target: (identifier) @keyword
(#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$"))
; * function call
(call
target: [
; local
(identifier) @function
; remote
(dot
right: (identifier) @function)
])
; * just identifier in function definition
(call
target: (identifier) @keyword
(arguments
[(identifier) @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
(binary_op
left:
[(call
function: (function_identifier) @function)
(identifier) @function]
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_operator
left: (identifier) @function
operator: "when")
])
(#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
(binary_op
operator: _ @operator)
; * pipe into identifier (definition)
(call
target: (identifier) @keyword
(arguments
(binary_operator
operator: "|>"
right: (identifier) @variable))
(#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$"))
; * pipe into identifier (function call)
(binary_operator
operator: "|>"
right: (identifier) @function)
; Identifiers
(heredoc
[(heredoc_start)
(heredoc_content)
(heredoc_end)] @string)
; * special
(
(identifier) @constant.builtin
(#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$")
)
(string
[(string_start)
(string_content)
(string_end)] @string)
; * unused
(
(identifier) @comment
(#match? @comment "^_")
)
(sigil_start) @string.special
(sigil_content) @string
(sigil_end) @string.special
; * regular
(identifier) @variable
(interpolation
"#{" @punctuation.special
"}" @punctuation.special)
; Comment
(comment) @comment
; Punctuation
[
"%"
] @punctuation
[
","
"->"
"."
";"
] @punctuation.delimiter
[
@ -133,6 +218,4 @@
">>"
] @punctuation.bracket
(special_identifier) @function.special
(ERROR) @warning

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

@ -0,0 +1,21 @@
(function_declaration
body: (block)? @function.inside) @function.around
(func_literal
(_)? @function.inside) @function.around
(method_declaration
body: (block)? @function.inside) @function.around
;; struct and interface declaration as class textobject?
(type_declaration
(type_spec (type_identifier) (struct_type (field_declaration_list (_)?) @class.inside))) @class.around
(type_declaration
(type_spec (type_identifier) (interface_type (method_spec_list (_)?) @class.inside))) @class.around
(parameter_list
(_) @parameter.inside)
(argument_list
(_) @parameter.inside)

@ -13,9 +13,9 @@
(constraint class: (class_name (type)) @class)
(class (class_head class: (class_name (type)) @class))
(instance (instance_head class: (class_name (type)) @class))
(integer) @number
(exp_literal (float)) @number
(char) @literal
(integer) @constant.numeric.integer
(exp_literal (float)) @constant.numeric.float
(char) @constant.character
(con_unit) @literal
(con_list) @literal
(tycon_arrow) @operator

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

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

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

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

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

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

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

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

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

@ -0,0 +1,17 @@
indent = [
"array_creation_expression",
"arguments",
"formal_parameters",
"compound_statement",
"declaration_list",
"binary_expression",
"return_statement",
"expression_statement",
"switch_block",
"anonymous_function_use_clause",
]
oudent = [
"}",
")",
]

@ -34,16 +34,14 @@
[
(fieldName)
(optionName)
] @property
] @variable.other.member
(enumVariantName) @type.enum.variant
(fullIdent) @namespace
[
(intLit)
(floatLit)
] @number
(boolLit) @constant.builtin
(intLit) @constant.numeric.integer
(floatLit) @constant.numeric.float
(boolLit) @constant.builtin.boolean
(strLit) @string
(constant) @constant

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

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

@ -15,15 +15,13 @@
; Primitives
; ---
(escape_sequence) @escape
(escape_sequence) @constant.character.escape
(primitive_type) @type.builtin
(boolean_literal) @constant.builtin.boolean
(integer_literal) @constant.numeric.integer
(float_literal) @constant.numeric.float
(char_literal) @constant.character
[
(integer_literal)
(float_literal)
] @number
[
(char_literal)
(string_literal)
(raw_string_literal)
] @string
@ -40,10 +38,10 @@
(enum_variant (identifier) @type.enum.variant)
(field_initializer
(field_identifier) @property)
(field_identifier) @variable.other.member)
(shorthand_field_initializer
(identifier) @variable.property)
(shorthand_field_identifier) @variable.property
(identifier) @variable.other.member)
(shorthand_field_identifier) @variable.other.member
(lifetime
"'" @label
@ -81,9 +79,24 @@
] @punctuation.bracket)
; ---
; Parameters
; Variables
; ---
(let_declaration
pattern: [
((identifier) @variable)
((tuple_pattern
(identifier) @variable))
])
; It needs to be anonymous to not conflict with `call_expression` further below.
(_
value: (field_expression
value: (identifier)? @variable
field: (field_identifier) @variable.other.member))
(arguments
(identifier) @variable.parameter)
(parameter
pattern: (identifier) @variable.parameter)
(closure_parameters
@ -336,4 +349,4 @@
(type_identifier) @type
(identifier) @variable
(field_identifier) @property
(field_identifier) @variable.other.member

@ -0,0 +1,26 @@
(function_item
body: (_) @function.inside) @function.around
(struct_item
body: (_) @class.inside) @class.around
(enum_item
body: (_) @class.inside) @class.around
(union_item
body: (_) @class.inside) @class.around
(trait_item
body: (_) @class.inside) @class.around
(impl_item
body: (_) @class.inside) @class.around
(parameters
(_) @parameter.inside)
(closure_parameters
(_) @parameter.inside)
(arguments
(_) @parameter.inside)

@ -29,7 +29,7 @@
(#match? @_attr "^(href|src)$"))
(tag_name) @tag
(attribute_name) @property
(attribute_name) @variable.other.member
(erroneous_end_tag_name) @error
(comment) @comment

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

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

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

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

Loading…
Cancel
Save