diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 3d82c609e..d4822f706 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/.gitmodules b/.gitmodules index a8e6481ea..7ed34ad39 100644 --- a/.gitmodules +++ b/.gitmodules @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index 03f573076..52ca2d602 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,85 @@ +# 0.5.0 (2021-10-28) + +A big shout out to all the contributors! We had 46 contributors in this release. + +Helix has popped up in [Scoop, FreeBSD Ports and Gentu GURU](https://repology.org/project/helix/versions)! + +The following is a quick rundown of the larger changes, there were many more +(check the git history for more details). + +Breaking changes: + +- A couple of keymaps moved to resolve a few conflicting keybinds. + - Documentation popups were moved from `K` to `space+k` + - `K` is now `keep_selections` which filters selections to only keeps ones matching the regex + - `keep_primary_selection` moved from `space+space` to `,` + - `Alt-,` is now `remove_primary_selection` which keeps all selections except the primary one + - Opening files in a split moved from `C-h` to `C-s` +- Some configuration options moved from a `[terminal]` section to `[editor]`. [Consult the documentation for more information.](https://docs.helix-editor.com/configuration.html) + +Features: + +- LSP compatibility greatly improved for some implementations (Julia, Python, Typescript) +- Autocompletion! Completion now triggers automatically after a set idle timeout +- Completion documentation is now displayed next to the popup ([#691](https://github.com/helix-editor/helix/pull/691)) +- Treesitter textobjects (select a function via `mf`, class via `mc`) ([#728](https://github.com/helix-editor/helix/pull/728)) +- Global search across entire workspace `space+/` ([#651](https://github.com/helix-editor/helix/pull/651)) +- Relative line number support ([#485](https://github.com/helix-editor/helix/pull/485)) +- Prompts now store a history (72cf86e) +- `:vsplit` and `:hsplit` commands ([#639](https://github.com/helix-editor/helix/pull/639)) +- `C-w h/j/k/l` can now be used to navigate between splits ([#860](https://github.com/helix-editor/helix/pull/860)) +- `C-j` and `C-k` are now alternative keybindings to `C-n` and `C-p` in the UI ([#876](https://github.com/helix-editor/helix/pull/876)) +- Shell commands (shell-pipe, pipe-to, shell-insert-output, shell-append-output, keep-pipe) ([#547](https://github.com/helix-editor/helix/pull/547)) +- Searching now defaults to smart case search (case insensitive unless uppercase is used) ([#761](https://github.com/helix-editor/helix/pull/761)) +- The preview pane was improved to highlight and center line ranges +- The user `languages.toml` is now merged into defaults, no longer need to copy the entire file (dc57f8dc) +- Show hidden files in completions ([#648](https://github.com/helix-editor/helix/pull/648)) +- Grammar injections are now properly handled (dd0b15e) +- `v` in select mode now switches back to normal mode ([#660](https://github.com/helix-editor/helix/pull/660)) +- View mode can now be triggered as a "sticky" mode ([#719](https://github.com/helix-editor/helix/pull/719)) +- `f`/`t` and object selection motions can now be repeated via `Alt-.` ([#891](https://github.com/helix-editor/helix/pull/891)) +- Statusline now displays total selection count and diagnostics counts for both errors and warnings ([#916](https://github.com/helix-editor/helix/pull/916)) + +New grammars: + +- Ledger ([#572](https://github.com/helix-editor/helix/pull/572)) +- Protobuf ([#614](https://github.com/helix-editor/helix/pull/614)) +- Zig ([#631](https://github.com/helix-editor/helix/pull/631)) +- YAML ([#667](https://github.com/helix-editor/helix/pull/667)) +- Lua ([#665](https://github.com/helix-editor/helix/pull/665)) +- OCaml ([#666](https://github.com/helix-editor/helix/pull/666)) +- Svelte ([#733](https://github.com/helix-editor/helix/pull/733)) +- Vue ([#787](https://github.com/helix-editor/helix/pull/787)) +- Tree-sitter queries ([#845](https://github.com/helix-editor/helix/pull/845)) +- CMake ([#888](https://github.com/helix-editor/helix/pull/888)) +- Elixir (we switched over to the official grammar) (6c0786e) +- Language server definitions for Nix and Elixir ([#725](https://github.com/helix-editor/helix/pull/725)) +- Python now uses `pylsp` instead of `pyls` +- Python now supports indentation + +New themes: + +- Monokai ([#628](https://github.com/helix-editor/helix/pull/628)) +- Everforest Dark ([#760](https://github.com/helix-editor/helix/pull/760)) +- Nord ([#799](https://github.com/helix-editor/helix/pull/799)) +- Base16 Default Dark ([#833](https://github.com/helix-editor/helix/pull/833)) +- Rose Pine ([#897](https://github.com/helix-editor/helix/pull/897)) + +Fixes: + +- Fix crash on empty rust file ([#592](https://github.com/helix-editor/helix/pull/592)) +- Exit select mode after toggle comment ([#598](https://github.com/helix-editor/helix/pull/598)) +- Pin popups with no positioning to the initial position (12ea3888) +- xsel copy should not freeze the editor (6dd7dc4) +- `*` now only sets the search register and doesn't jump to the next occurrence (3426285) +- Goto line start/end commands extend when in select mode ([#739](https://github.com/helix-editor/helix/pull/739)) +- Fix documentation popups sometimes not getting fully highlighted (066367c) +- Refactor apply_workspace_edit to remove assert (b02d872) +- Wrap around the top of the picker menu when scrolling (c7d6e44) +- Don't allow closing the last split if there's unsaved changes (3ff5b00) +- Indentation used different default on hx vs hx new_file.txt (c913bad) + # 0.4.1 (2021-08-14) A minor release that includes: @@ -7,6 +88,8 @@ 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.. diff --git a/Cargo.lock b/Cargo.lock index ef9d74bda..d200cc275 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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" diff --git a/README.md b/README.md index 7cd58d590..faf5851ed 100644 --- a/README.md +++ b/README.md @@ -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//` 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 ` for info, more `v`s for higher severity inclusive) - If your preferred language is missing, integrating a tree-sitter grammar for diff --git a/TODO.md b/TODO.md index 90e7e4509..80a9be05e 100644 --- a/TODO.md +++ b/TODO.md @@ -6,10 +6,6 @@ - clojure - erlang -as you type completion! -- [ ] use signature_help_provider and completion_provider trigger characters in - a hook to trigger signature help text / autocompletion -- [ ] document.on_type provider triggers - [ ] completion isIncomplete support 1 @@ -18,8 +14,6 @@ as you type completion! - [ ] = for auto indent line/selection - [ ] :x for closing buffers -- [ ] repeat selection - - [ ] lsp: signature help 2 diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 3fa8e0676..56f50e212 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -8,3 +8,5 @@ - [Keymap](./keymap.md) - [Key Remapping](./remapping.md) - [Hooks](./hooks.md) +- [Guides](./guides/README.md) + - [Adding Textobject Queries](./guides/textobject.md) diff --git a/book/src/configuration.md b/book/src/configuration.md index d47f95d91..be25441f5 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.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 diff --git a/book/src/guides/README.md b/book/src/guides/README.md new file mode 100644 index 000000000..96e629783 --- /dev/null +++ b/book/src/guides/README.md @@ -0,0 +1,4 @@ +# Guides + +This section contains guides for adding new language server configurations, +tree-sitter grammers, textobject queries, etc. diff --git a/book/src/guides/textobject.md b/book/src/guides/textobject.md new file mode 100644 index 000000000..50b3b574a --- /dev/null +++ b/book/src/guides/textobject.md @@ -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= diff --git a/book/src/keymap.md b/book/src/keymap.md index 156b1d99f..5a6aee411 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -6,38 +6,39 @@ > NOTE: Unlike vim, `f`, `F`, `t` and `T` are not confined to the current line. -| 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` | -| `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` | -| `W` | Move next WORD start | `move_next_long_word_start` | -| `B` | Move previous WORD start | `move_prev_long_word_start` | -| `E` | Move next WORD end | `move_next_long_word_end` | -| `t` | Find 'till next char | `find_till_char` | -| `f` | Find next char | `find_next_char` | -| `T` | Find 'till previous char | `till_prev_char` | -| `F` | Find previous char | `find_prev_char` | -| `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` | -| `PageDown` | Move page down | `page_down` | -| `Ctrl-u` | Move half page up | `half_page_up` | -| `Ctrl-d` | Move half page down | `half_page_down` | -| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | -| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | -| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | -| `g` | Enter [goto mode](#goto-mode) | N/A | -| `m` | Enter [match mode](#match-mode) | N/A | -| `:` | Enter command mode | `command_mode` | -| `z` | Enter [view mode](#view-mode) | N/A | -| `Z` | Enter sticky [view mode](#view-mode) | N/A | -| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | -| `Space` | Enter [space mode](#space-mode) | N/A | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `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` | +| `W` | Move next WORD start | `move_next_long_word_start` | +| `B` | Move previous WORD start | `move_prev_long_word_start` | +| `E` | Move next WORD end | `move_next_long_word_end` | +| `t` | Find 'till next char | `find_till_char` | +| `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` | +| `PageDown` | Move page down | `page_down` | +| `Ctrl-u` | Move half page up | `half_page_up` | +| `Ctrl-d` | Move half page down | `half_page_down` | +| `Ctrl-i` | Jump forward on the jumplist | `jump_forward` | +| `Ctrl-o` | Jump backward on the jumplist | `jump_backward` | +| `v` | Enter [select (extend) mode](#select--extend-mode) | `select_mode` | +| `g` | Enter [goto mode](#goto-mode) | N/A | +| `m` | Enter [match mode](#match-mode) | N/A | +| `:` | Enter command mode | `command_mode` | +| `z` | Enter [view mode](#view-mode) | N/A | +| `Z` | Enter sticky [view mode](#view-mode) | N/A | +| `Ctrl-w` | Enter [window mode](#window-mode) | N/A | +| `Space` | Enter [space mode](#space-mode) | N/A | ### Changes @@ -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 @@ -180,12 +185,16 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`). This layer is similar to vim keybindings as kakoune does not support window. -| Key | Description | Command | -| ----- | ------------- | ------- | -| `w`, `Ctrl-w` | Switch to next window | `rotate_view` | -| `v`, `Ctrl-v` | Vertical right split | `vsplit` | -| `h`, `Ctrl-h` | Horizontal bottom split | `hsplit` | -| `q`, `Ctrl-q` | Close current window | `wclose` | +| Key | Description | Command | +| ----- | ------------- | ------- | +| `w`, `Ctrl-w` | Switch to next window | `rotate_view` | +| `v`, `Ctrl-v` | Vertical right split | `vsplit` | +| `s`, `Ctrl-s` | Horizontal bottom split | `hsplit` | +| `h`, `Ctrl-h`, `left` | Move to left split | `jump_view_left` | +| `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 @@ -213,12 +222,12 @@ This layer is a kludge of mappings, mostly pickers. Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). -| Key | Description | Command | -| ----- | ----------- | ------- | -| `[d` | Go to previous diagnostic | `goto_prev_diag` | -| `]d` | Go to next diagnostic | `goto_next_diag` | -| `[D` | Go to first diagnostic in document | `goto_first_diag` | -| `]D` | Go to last diagnostic in document | `goto_last_diag` | +| Key | Description | Command | +| ----- | ----------- | ------- | +| `[d` | Go to previous diagnostic | `goto_prev_diag` | +| `]d` | Go to next diagnostic | `goto_next_diag` | +| `[D` | Go to first diagnostic in document | `goto_first_diag` | +| `]D` | Go to last diagnostic in document | `goto_last_diag` | | `[space` | Add newline above | `add_newline_above` | | `]space` | Add newline below | `add_newline_below` | @@ -242,12 +251,34 @@ commands (including goto) to extend the existing selection instead of replacing Keys to use within picker. Remapping currently not supported. -| Key | Description | -| ----- | ------------- | -| `Up`, `Ctrl-p` | Previous entry | -| `Down`, `Ctrl-n` | Next entry | -| `Ctrl-space` | Filter options | -| `Enter` | Open selected | -| `Ctrl-h` | Open horizontally | -| `Ctrl-v` | Open vertically | -| `Escape`, `Ctrl-c` | Close picker | +| Key | Description | +| ----- | ------------- | +| `Up`, `Ctrl-k`, `Ctrl-p` | Previous entry | +| `Down`, `Ctrl-j`, `Ctrl-n` | Next entry | +| `Ctrl-space` | Filter options | +| `Enter` | Open selected | +| `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 | + diff --git a/book/src/remapping.md b/book/src/remapping.md index 81f45da3f..3369f0315 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -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 diff --git a/book/src/themes.md b/book/src/themes.md index a99e3a59e..ecbbb6e97 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -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` diff --git a/book/src/usage.md b/book/src/usage.md index 9ee8634c6..71730fa8b 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -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, `` in kakoune) - `mi` - Select inside the object (`vi` in vim, `` 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 ! diff --git a/flake.lock b/flake.lock index 21e44c6e9..2029d5809 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "devshell": { "locked": { - "lastModified": 1630239564, - "narHash": "sha256-lv7atkVE1+dFw0llmzONsbSIo5ao85KpNSRoFk4K8vU=", + "lastModified": 1632436039, + "narHash": "sha256-OtITeVWcKXn1SpVEnImpTGH91FycCskGBPqmlxiykv4=", "owner": "numtide", "repo": "devshell", - "rev": "bd86d3a2bb28ce4d223315e0eca0d59fef8a0a73", + "rev": "7a7a7aa0adebe5488e5abaec688fd9ae0f8ea9c6", "type": "github" }, "original": { @@ -15,6 +15,21 @@ "type": "github" } }, + "flake-utils": { + "locked": { + "lastModified": 1623875721, + "narHash": "sha256-A8BU7bjS5GirpAUv4QA+QnJ4CceLHkcXdRp4xITDB0s=", + "owner": "numtide", + "repo": "flake-utils", + "rev": "f7e004a55b120c02ecb6219596820fcd32ca8772", + "type": "github" + }, + "original": { + "owner": "numtide", + "repo": "flake-utils", + "type": "github" + } + }, "flakeCompat": { "flake": false, "locked": { @@ -37,14 +52,16 @@ "nixpkgs": [ "nixpkgs" ], - "rustOverlay": "rustOverlay" + "rustOverlay": [ + "rust-overlay" + ] }, "locked": { - "lastModified": 1631254163, - "narHash": "sha256-8+nOGLH1fXwWnNMTQq/Igk434BzZF5Vld45xLDLiNDQ=", + "lastModified": 1634796585, + "narHash": "sha256-CW4yx6omk5qCXUIwXHp/sztA7u0SpyLq9NEACPnkiz8=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "432d8504a32232e8d74710024d5bf5cc31767651", + "rev": "a84a2137a396f303978f1d48341e0390b0e16a8b", "type": "github" }, "original": { @@ -55,11 +72,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1631206977, - "narHash": "sha256-o3Dct9aJ5ht5UaTUBzXrRcK1RZt2eG5/xSlWJuUCVZM=", + "lastModified": 1634782485, + "narHash": "sha256-psfh4OQSokGXG0lpq3zKFbhOo3QfoeudRcaUnwMRkQo=", "owner": "nixos", "repo": "nixpkgs", - "rev": "4f6d8095fd51954120a1d08ea5896fe42dc3923b", + "rev": "34ad3ffe08adfca17fcb4e4a47bb5f3b113687be", "type": "github" }, "original": { @@ -69,21 +86,40 @@ "type": "github" } }, + "nixpkgs_2": { + "locked": { + "lastModified": 1628186154, + "narHash": "sha256-r2d0wvywFnL9z4iptztdFMhaUIAaGzrSs7kSok0PgmE=", + "owner": "NixOS", + "repo": "nixpkgs", + "rev": "06552b72346632b6943c8032e57e702ea12413bf", + "type": "github" + }, + "original": { + "owner": "NixOS", + "repo": "nixpkgs", + "type": "github" + } + }, "root": { "inputs": { "flakeCompat": "flakeCompat", "nixCargoIntegration": "nixCargoIntegration", - "nixpkgs": "nixpkgs" + "nixpkgs": "nixpkgs", + "rust-overlay": "rust-overlay" } }, - "rustOverlay": { - "flake": false, + "rust-overlay": { + "inputs": { + "flake-utils": "flake-utils", + "nixpkgs": "nixpkgs_2" + }, "locked": { - "lastModified": 1631240108, - "narHash": "sha256-ffsTkAGyQLxu4E28nVcqwc8xFL/H1UEwrRw2ITI43Aw=", + "lastModified": 1634869268, + "narHash": "sha256-RVAcEFlFU3877Mm4q/nbXGEYTDg/wQNhzmXGMTV6wBs=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "3a29d5e726b855d9463eb5dfe04f1ec14d413289", + "rev": "c02c2d86354327317546501af001886fbb53d374", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index bcc9383e7..296a68d5a 100644 --- a/flake.nix +++ b/flake.nix @@ -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"; } diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 510964536..ea695d34a 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "helix-core" -version = "0.4.1" +version = "0.5.0" authors = ["Blaž Hrastnik "] -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" diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index 9b901e9b1..cc9668529 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -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; diff --git a/helix-core/src/chars.rs b/helix-core/src/chars.rs index 24133dd33..c8e5efbde 100644 --- a/helix-core/src/chars.rs +++ b/helix-core/src/chars.rs @@ -1,3 +1,5 @@ +//! Utility functions to categorize a `char`. + use crate::LineEnding; #[derive(Debug, Eq, PartialEq)] diff --git a/helix-core/src/comment.rs b/helix-core/src/comment.rs index 3d8e1ce38..b22a95a65 100644 --- a/helix-core/src/comment.rs +++ b/helix-core/src/comment.rs @@ -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 = Vec::new(); + let mut lines: Vec = Vec::with_capacity(selection.len()); let mut min_next_line = 0; for selection in selection { diff --git a/helix-core/src/diagnostic.rs b/helix-core/src/diagnostic.rs index e08a71e7b..ad1ba16ab 100644 --- a/helix-core/src/diagnostic.rs +++ b/helix-core/src/diagnostic.rs @@ -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, diff --git a/helix-core/src/graphemes.rs b/helix-core/src/graphemes.rs index 0465fe510..c63988757 100644 --- a/helix-core/src/graphemes.rs +++ b/helix-core/src/graphemes.rs @@ -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 use ropey::{iter::Chunks, str_utils::byte_to_char_idx, RopeSlice}; use unicode_segmentation::{GraphemeCursor, GraphemeIncomplete}; use unicode_width::UnicodeWidthStr; diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index 67ded1661..b53c01fe7 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -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: #[derive(Debug)] pub struct History { revisions: Vec, 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(¤t_revision.inversion) } + /// Redo the last edit. pub fn redo(&mut self) -> Option<&Transaction> { let current_revision = &self.revisions[self.current]; let last_child = current_revision.last_child?; @@ -147,8 +151,8 @@ 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 { 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 { 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 { self.jump_to(self.current.saturating_sub(delta)) } + /// Creates a [`Transaction`] that will redo `delta` revisions. fn jump_forward(&mut self, delta: usize) -> Vec { 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 { 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 { 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 { 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 { use UndoKind::*; match uk { @@ -231,6 +245,7 @@ impl History { } } + /// Creates a redo [`Transaction`]. pub fn later(&mut self, uk: UndoKind) -> Vec { 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 = 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 = 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 { if !DURATION_VALIDATION_REGEX.is_match(s) { return Err("duration should be composed \ diff --git a/helix-core/src/indent.rs b/helix-core/src/indent.rs index 1f32d0389..df1583636 100644 --- a/helix-core/src/indent.rs +++ b/helix-core/src/indent.rs @@ -464,6 +464,7 @@ where unit: String::from(" "), }), indent_query: OnceCell::new(), + textobject_query: OnceCell::new(), debugger: None, }], }); diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 0854eb04a..f42841392 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -35,6 +35,7 @@ pub fn find_first_non_whitespace_char(line: RopeSlice) -> Option { line.chars().position(|ch| !ch.is_whitespace()) } +/// Find `.git` root. pub fn find_root(root: Option<&str>) -> Option { 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; diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs index 18ea5f9f5..3541305c3 100644 --- a/helix-core/src/line_ending.rs +++ b/helix-core/src/line_ending.rs @@ -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 { + pub const fn from_char(ch: char) -> Option { match ch { '\u{000A}' => Some(LineEnding::LF), '\u{000B}' => Some(LineEnding::VT), diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 5d080545d..9e85bd217 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -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); diff --git a/helix-core/src/object.rs b/helix-core/src/object.rs index d9558dd83..717c59947 100644 --- a/helix-core/src/object.rs +++ b/helix-core/src/object.rs @@ -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, }; diff --git a/helix-core/src/position.rs b/helix-core/src/position.rs index 08a8aed54..c6018ce69 100644 --- a/helix-core/src/position.rs +++ b/helix-core/src/position.rs @@ -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 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ẅöṛḷḋ"); diff --git a/helix-core/src/register.rs b/helix-core/src/register.rs index c3e6652e6..c5444eb73 100644 --- a/helix-core/src/register.rs +++ b/helix-core/src/register.rs @@ -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 } diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 18af4d089..f3b5d2c83 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -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 { diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 441802a58..18504c212 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -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, // filename ends_with? pub roots: Vec, // these indicate project roots <.git, Cargo.toml> @@ -76,6 +76,8 @@ pub struct LanguageConfiguration { #[serde(skip)] pub(crate) indent_query: OnceCell>, + #[serde(skip)] + pub(crate) textobject_query: OnceCell>, #[serde(skip_serializing_if = "Option::is_none")] pub debugger: Option, } @@ -160,6 +162,32 @@ pub struct IndentQuery { pub outdent: HashSet, } +#[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>> { + 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 { 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 { + 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>, 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(()) diff --git a/helix-core/src/textobject.rs b/helix-core/src/textobject.rs index b965f6dfc..975ed115b 100644 --- a/helix-core/src/textobject.rs +++ b/helix-core/src/textobject.rs @@ -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 { + 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::*; diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index d682f0582..dfc18fbea 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -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 diff --git a/helix-dap/Cargo.toml b/helix-dap/Cargo.toml index 2b922e84a..6870adb67 100644 --- a/helix-dap/Cargo.toml +++ b/helix-dap/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "helix-dap" -version = "0.4.1" +version = "0.5.0" authors = ["Blaž Hrastnik "] edition = "2018" license = "MPL-2.0" diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 1252172af..0192ba1e4 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "helix-lsp" -version = "0.4.1" +version = "0.5.0" authors = ["Blaž Hrastnik "] -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" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 4068ae1fe..b810feef3 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -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::( diff --git a/helix-syntax/Cargo.toml b/helix-syntax/Cargo.toml index 9c2b82759..cceec4127 100644 --- a/helix-syntax/Cargo.toml +++ b/helix-syntax/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "helix-syntax" -version = "0.4.1" +version = "0.5.0" authors = ["Blaž Hrastnik "] -edition = "2018" +edition = "2021" license = "MPL-2.0" description = "Tree-sitter grammars support" categories = ["editor"] diff --git a/helix-syntax/languages/tree-sitter-cmake b/helix-syntax/languages/tree-sitter-cmake new file mode 160000 index 000000000..f6616f1e4 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-cmake @@ -0,0 +1 @@ +Subproject commit f6616f1e417ee8b62daf251aa1daa5d73781c596 diff --git a/helix-syntax/languages/tree-sitter-cpp b/helix-syntax/languages/tree-sitter-cpp index c61212414..e8dcc9d2b 160000 --- a/helix-syntax/languages/tree-sitter-cpp +++ b/helix-syntax/languages/tree-sitter-cpp @@ -1 +1 @@ -Subproject commit c61212414a3e95b5f7507f98e83de1d638044adc +Subproject commit e8dcc9d2b404c542fd236ea5f7208f90be8a6e89 diff --git a/helix-syntax/languages/tree-sitter-elixir b/helix-syntax/languages/tree-sitter-elixir index 295e62a43..f5d7bda54 160000 --- a/helix-syntax/languages/tree-sitter-elixir +++ b/helix-syntax/languages/tree-sitter-elixir @@ -1 +1 @@ -Subproject commit 295e62a43b92cea909cfabe57e8818d177f4857b +Subproject commit f5d7bda543da788bd507b05bd722627dde66c9ec diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 68ff260d1..43268291b 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -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 "] -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" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 27062a360..0fb4e4799 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -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(()) diff --git a/helix-term/src/args.rs b/helix-term/src/args.rs index f0ef09eb0..40113db92 100644 --- a/helix-term/src/args.rs +++ b/helix-term/src/args.rs @@ -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, } @@ -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: {}", diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index f3761d7d2..3616d6a87 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -16,8 +16,13 @@ use helix_core::{ }; use helix_view::{ - clipboard::ClipboardType, document::Mode, editor::Action, input::KeyEvent, keyboard::KeyCode, - view::View, Document, DocumentId, Editor, ViewId, + clipboard::ClipboardType, + document::Mode, + editor::{Action, Motion}, + input::KeyEvent, + keyboard::KeyCode, + view::View, + Document, DocumentId, Editor, ViewId, }; use anyhow::{anyhow, bail, Context as _}; @@ -202,6 +207,7 @@ impl Command { find_prev_char, "Move to previous occurance of char", extend_till_prev_char, "Extend till previous occurance of char", extend_prev_char, "Extend to previous occurance of char", + repeat_last_motion, "repeat last motion(extend_next_char, extend_till_char, find_next_char, find_till_char...)", replace, "Replace with new char", switch_case, "Switch (toggle) case", switch_to_uppercase, "Switch to uppercase", @@ -215,8 +221,11 @@ impl Command { split_selection, "Split selection into subselections on regex matches", split_selection_on_newline, "Split selection on newlines", search, "Search for regex pattern", + rsearch, "Reverse search for regex pattern", search_next, "Select next search match", + search_prev, "Select previous search match", extend_search_next, "Add next search match to selection", + extend_search_prev, "Add previous search match to selection", search_selection, "Use current selection as search pattern", global_search, "Global Search in workspace folder", extend_line, "Select current line, if already selected, extend to next line", @@ -260,6 +269,8 @@ impl Command { goto_prev_diag, "Goto previous diagnostic", goto_line_start, "Goto line start", goto_line_end, "Goto line end", + goto_next_buffer, "Goto next buffer", + goto_previous_buffer, "Goto previous buffer", // TODO: different description ? goto_line_end_newline, "Goto line end", goto_first_nonwhitespace, "Goto first non-blank in line", @@ -305,6 +316,10 @@ impl Command { expand_selection, "Expand selection to parent syntax node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", + jump_view_right, "Jump to the split to the right", + jump_view_left, "Jump to the split to the left", + jump_view_up, "Jump to the split above", + jump_view_down, "Jump to the split below", rotate_view, "Goto next window", hsplit, "Horizontal bottom split", vsplit, "Vertical right split", @@ -528,6 +543,39 @@ fn goto_line_start(cx: &mut Context) { ) } +fn goto_next_buffer(cx: &mut Context) { + goto_buffer(cx, Direction::Forward); +} + +fn goto_previous_buffer(cx: &mut Context) { + goto_buffer(cx, Direction::Backward); +} + +fn goto_buffer(cx: &mut Context, direction: Direction) { + let current = view!(cx.editor).doc; + + let id = match direction { + Direction::Forward => { + let iter = cx.editor.documents.keys(); + let mut iter = iter.skip_while(|id| *id != ¤t); + iter.next(); // skip current item + iter.next().or_else(|| cx.editor.documents.keys().next()) + } + Direction::Backward => { + let iter = cx.editor.documents.keys(); + let mut iter = iter.rev().skip_while(|id| *id != ¤t); + iter.next(); // skip current item + iter.next() + .or_else(|| cx.editor.documents.keys().rev().next()) + } + } + .unwrap(); + + let id = *id; + + cx.editor.switch(id, Action::Replace); +} + fn extend_to_line_start(cx: &mut Context) { let (view, doc) = current!(cx.editor); goto_line_start_impl(view, doc, Movement::Extend) @@ -631,14 +679,25 @@ fn goto_file_start(cx: &mut Context) { } else { push_jump(cx.editor); let (view, doc) = current!(cx.editor); - doc.set_selection(view.id, Selection::point(0)); + let text = doc.text().slice(..); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, 0, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } } fn goto_file_end(cx: &mut Context) { push_jump(cx.editor); let (view, doc) = current!(cx.editor); - doc.set_selection(view.id, Selection::point(doc.text().len_chars())); + let text = doc.text().slice(..); + let pos = doc.text().len_chars(); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } fn extend_word_impl(cx: &mut Context, extend_fn: F) @@ -681,8 +740,7 @@ fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) } -#[inline] -fn find_char_impl(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) +fn will_find_char(cx: &mut Context, search_fn: F, inclusive: bool, extend: bool) where F: Fn(RopeSlice, char, usize, usize, bool) -> Option + 'static, { @@ -704,13 +762,7 @@ where // usually mix line endings. But we should fix it eventually // anyway. { - current!(cx.editor) - .1 - .line_ending - .as_str() - .chars() - .next() - .unwrap() + doc!(cx.editor).line_ending.as_str().chars().next().unwrap() } KeyEvent { @@ -720,29 +772,48 @@ where _ => return, }; - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + find_char_impl(cx.editor, &search_fn, inclusive, extend, ch, count); + cx.editor.last_motion = Some(Motion(Box::new(move |editor: &mut Editor| { + find_char_impl(editor, &search_fn, inclusive, true, ch, 1); + }))); + }) +} - let selection = doc.selection(view.id).clone().transform(|range| { - // TODO: use `Range::cursor()` here instead. However, that works in terms of - // graphemes, whereas this function doesn't yet. So we're doing the same logic - // here, but just in terms of chars instead. - let search_start_pos = if range.anchor < range.head { - range.head - 1 - } else { - range.head - }; +// - search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| { - if extend { - range.put_cursor(text, pos, true) - } else { - Range::point(range.cursor(text)).put_cursor(text, pos, true) - } - }) - }); - doc.set_selection(view.id, selection); - }) +#[inline] +fn find_char_impl( + editor: &mut Editor, + search_fn: &F, + inclusive: bool, + extend: bool, + ch: char, + count: usize, +) where + F: Fn(RopeSlice, char, usize, usize, bool) -> Option + 'static, +{ + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + // TODO: use `Range::cursor()` here instead. However, that works in terms of + // graphemes, whereas this function doesn't yet. So we're doing the same logic + // here, but just in terms of chars instead. + let search_start_pos = if range.anchor < range.head { + range.head - 1 + } else { + range.head + }; + + search_fn(text, ch, search_start_pos, count, inclusive).map_or(range, |pos| { + if extend { + range.put_cursor(text, pos, true) + } else { + Range::point(range.cursor(text)).put_cursor(text, pos, true) + } + }) + }); + doc.set_selection(view.id, selection); } fn find_next_char_impl( @@ -756,6 +827,10 @@ fn find_next_char_impl( if inclusive { search::find_nth_next(text, ch, pos, n) } else { + let n = match text.get_char(pos) { + Some(next_ch) if next_ch == ch => n + 1, + _ => n, + }; search::find_nth_next(text, ch, pos, n).map(|n| n.saturating_sub(1)) } } @@ -770,80 +845,52 @@ fn find_prev_char_impl( if inclusive { search::find_nth_prev(text, ch, pos, n) } else { + let n = match text.get_char(pos.saturating_sub(1)) { + Some(next_ch) if next_ch == ch => n + 1, + _ => n, + }; search::find_nth_prev(text, ch, pos, n).map(|n| (n + 1).min(text.len_chars())) } } fn find_till_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - false, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_next_char_impl, false, false) } fn find_next_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - true, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_next_char_impl, true, false) } fn extend_till_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - false, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_next_char_impl, false, true) } fn extend_next_char(cx: &mut Context) { - find_char_impl( - cx, - find_next_char_impl, - true, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_next_char_impl, true, true) } fn till_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - false, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, false, false) } fn find_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - true, /* inclusive */ - false, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, true, false) } fn extend_till_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - false, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, false, true) } fn extend_prev_char(cx: &mut Context) { - find_char_impl( - cx, - find_prev_char_impl, - true, /* inclusive */ - true, /* extend */ - ) + will_find_char(cx, find_prev_char_impl, true, true) +} + +fn repeat_last_motion(cx: &mut Context) { + let last_motion = cx.editor.last_motion.take(); + if let Some(m) = &last_motion { + m.run(cx.editor); + cx.editor.last_motion = last_motion; + } } fn replace(cx: &mut Context) { @@ -1091,6 +1138,7 @@ fn select_regex(cx: &mut Context) { cx, "select:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1113,6 +1161,7 @@ fn split_selection(cx: &mut Context) { cx, "split:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -1137,35 +1186,68 @@ fn split_selection_on_newline(cx: &mut Context) { doc.set_selection(view.id, selection); } -fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Regex, extend: bool) { +fn search_impl( + doc: &mut Document, + view: &mut View, + contents: &str, + regex: &Regex, + movement: Movement, + direction: Direction, +) { let text = doc.text().slice(..); let selection = doc.selection(view.id); - // Get the right side of the primary block cursor. - let start = text.char_to_byte(graphemes::next_grapheme_boundary( - text, - selection.primary().cursor(text), - )); + // Get the right side of the primary block cursor for forward search, or the + //grapheme before the start of the selection for reverse search. + let start = match direction { + Direction::Forward => text.char_to_byte(graphemes::next_grapheme_boundary( + text, + selection.primary().to(), + )), + Direction::Backward => text.char_to_byte(graphemes::prev_grapheme_boundary( + text, + selection.primary().from(), + )), + }; + + //A regex::Match returns byte-positions in the str. In the case where we + //do a reverse search and wraparound to the end, we don't need to search + //the text before the current cursor position for matches, but by slicing + //it out, we need to add it back to the position of the selection. + let mut offset = 0; // use find_at to find the next match after the cursor, loop around the end // Careful, `Regex` uses `bytes` as offsets, not character indices! - let mat = regex - .find_at(contents, start) - .or_else(|| regex.find(contents)); + let mat = match direction { + Direction::Forward => regex + .find_at(contents, start) + .or_else(|| regex.find(contents)), + Direction::Backward => regex.find_iter(&contents[..start]).last().or_else(|| { + offset = start; + regex.find_iter(&contents[start..]).last() + }), + }; // TODO: message on wraparound if let Some(mat) = mat { - let start = text.byte_to_char(mat.start()); - let end = text.byte_to_char(mat.end()); + let start = text.byte_to_char(mat.start() + offset); + let end = text.byte_to_char(mat.end() + offset); if end == 0 { // skip empty matches that don't make sense return; } - let selection = if extend { - selection.clone().push(Range::new(start, end)) + // Determine range direction based on the primary range + let primary = selection.primary(); + let range = if primary.head < primary.anchor { + Range::new(end, start) } else { - Selection::single(start, end) + Range::new(start, end) + }; + + let selection = match movement { + Movement::Extend => selection.clone().push(range), + Movement::Move => selection.clone().replace(selection.primary_index(), range), }; doc.set_selection(view.id, selection); @@ -1173,8 +1255,25 @@ fn search_impl(doc: &mut Document, view: &mut View, contents: &str, regex: &Rege }; } +fn search_completions(cx: &mut Context, reg: Option) -> Vec { + let mut items = reg + .and_then(|reg| cx.editor.registers.get(reg)) + .map_or(Vec::new(), |reg| reg.read().iter().take(200).collect()); + items.sort_unstable(); + items.dedup(); + items.into_iter().cloned().collect() +} + // TODO: use one function for search vs extend fn search(cx: &mut Context) { + searcher(cx, Direction::Forward) +} + +fn rsearch(cx: &mut Context) { + searcher(cx, Direction::Backward) +} +// TODO: use one function for search vs extend +fn searcher(cx: &mut Context, direction: Direction) { let reg = cx.register.unwrap_or('/'); let (_, doc) = current!(cx.editor); @@ -1183,23 +1282,31 @@ fn search(cx: &mut Context) { // HAXX: sadly we can't avoid allocating a single string for the whole buffer since we can't // feed chunks into the regex yet let contents = doc.text().slice(..).to_string(); + let completions = search_completions(cx, Some(reg)); let prompt = ui::regex_prompt( cx, "search:".into(), Some(reg), + move |input: &str| { + completions + .iter() + .filter(|comp| comp.starts_with(input)) + .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) + .collect() + }, move |view, doc, regex, event| { if event != PromptEvent::Update { return; } - search_impl(doc, view, &contents, ®ex, false); + search_impl(doc, view, &contents, ®ex, Movement::Move, direction); }, ); cx.push_layer(Box::new(prompt)); } -fn search_next_impl(cx: &mut Context, extend: bool) { +fn search_next_or_prev_impl(cx: &mut Context, movement: Movement, direction: Direction) { let (view, doc) = current!(cx.editor); let registers = &cx.editor.registers; if let Some(query) = registers.read('/') { @@ -1214,7 +1321,7 @@ fn search_next_impl(cx: &mut Context, extend: bool) { .case_insensitive(case_insensitive) .build() { - search_impl(doc, view, &contents, ®ex, extend); + search_impl(doc, view, &contents, ®ex, movement, direction); } else { // get around warning `mutable_borrow_reservation_conflict` // which will be a hard error in the future @@ -1226,11 +1333,18 @@ fn search_next_impl(cx: &mut Context, extend: bool) { } fn search_next(cx: &mut Context) { - search_next_impl(cx, false); + search_next_or_prev_impl(cx, Movement::Move, Direction::Forward); } +fn search_prev(cx: &mut Context) { + search_next_or_prev_impl(cx, Movement::Move, Direction::Backward); +} fn extend_search_next(cx: &mut Context) { - search_next_impl(cx, true); + search_next_or_prev_impl(cx, Movement::Extend, Direction::Forward); +} + +fn extend_search_prev(cx: &mut Context) { + search_next_or_prev_impl(cx, Movement::Extend, Direction::Backward); } fn search_selection(cx: &mut Context) { @@ -1247,10 +1361,19 @@ fn global_search(cx: &mut Context) { let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<(usize, PathBuf)>(); let smart_case = cx.editor.config.smart_case; + + let completions = search_completions(cx, None); let prompt = ui::regex_prompt( cx, "global search:".into(), None, + move |input: &str| { + completions + .iter() + .filter(|comp| comp.starts_with(input)) + .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) + .collect() + }, move |_view, _doc, regex, event| { if event != PromptEvent::Validate { return; @@ -1572,7 +1695,8 @@ mod cmd { let (_, doc) = current!(cx.editor); if let Some(path) = path { - doc.set_path(path.as_ref()).context("invalid filepath")?; + doc.set_path(Some(path.as_ref())) + .context("invalid filepath")?; } if doc.path().is_none() { bail!("cannot write a buffer without a filename"); @@ -1635,7 +1759,7 @@ mod cmd { // If no argument, report current indent style. if args.is_empty() { - let style = current!(cx.editor).1.indent_style; + let style = doc!(cx.editor).indent_style; cx.editor.set_status(match style { Tabs => "tabs".into(), Spaces(1) => "1 space".into(), @@ -1674,7 +1798,7 @@ mod cmd { // If no argument, report current line ending setting. if args.is_empty() { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; cx.editor.set_status(match line_ending { Crlf => "crlf".into(), LF => "line feed".into(), @@ -1790,7 +1914,7 @@ mod cmd { let mut errors = String::new(); // save all documents - for (_, doc) in &mut cx.editor.documents { + for doc in &mut cx.editor.documents.values_mut() { if doc.path().is_none() { errors.push_str("cannot write a buffer without a filename\n"); continue; @@ -2085,8 +2209,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - let id = doc.id(); + let id = view!(cx.editor).doc; if let Some(path) = args.get(0) { cx.editor.open(path.into(), Action::VerticalSplit)?; @@ -2102,8 +2225,7 @@ mod cmd { args: &[&str], _event: PromptEvent, ) -> anyhow::Result<()> { - let (_, doc) = current!(cx.editor); - let id = doc.id(); + let id = view!(cx.editor).doc; if let Some(path) = args.get(0) { cx.editor.open(path.into(), Action::HorizontalSplit)?; @@ -2188,6 +2310,18 @@ mod cmd { Ok(()) } + fn tutor( + cx: &mut compositor::Context, + _args: &[&str], + _event: PromptEvent, + ) -> anyhow::Result<()> { + let path = helix_core::runtime_dir().join("tutor.txt"); + cx.editor.open(path, Action::Replace)?; + // Unset path to prevent accidentally saving to the original tutor file. + doc_mut!(cx.editor).set_path(None)?; + Ok(()) + } + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2199,7 +2333,7 @@ mod cmd { TypableCommand { name: "quit!", aliases: &["q!"], - doc: "Close the current view.", + doc: "Close the current view forcefully (ignoring unsaved changes).", fun: force_quit, completer: None, }, @@ -2262,35 +2396,35 @@ mod cmd { TypableCommand { name: "write-quit", aliases: &["wq", "x"], - doc: "Writes changes to disk and closes the current view. Accepts an optional path (:wq some/path.txt)", + doc: "Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt)", fun: write_quit, completer: Some(completers::filename), }, TypableCommand { name: "write-quit!", aliases: &["wq!", "x!"], - doc: "Writes changes to disk and closes the current view forcefully. Accepts an optional path (:wq! some/path.txt)", + doc: "Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt)", fun: force_write_quit, completer: Some(completers::filename), }, TypableCommand { name: "write-all", aliases: &["wa"], - doc: "Writes changes from all views to disk.", + doc: "Write changes from all views to disk.", fun: write_all, completer: None, }, TypableCommand { name: "write-quit-all", aliases: &["wqa", "xa"], - doc: "Writes changes from all views to disk and close all views.", + doc: "Write changes from all views to disk and close all views.", fun: write_all_quit, completer: None, }, TypableCommand { name: "write-quit-all!", aliases: &["wqa!", "xa!"], - doc: "Writes changes from all views to disk and close all views forcefully (ignoring unsaved changes).", + doc: "Write changes from all views to disk and close all views forcefully (ignoring unsaved changes).", fun: force_write_all_quit, completer: None, }, @@ -2461,7 +2595,14 @@ mod cmd { doc: "Open the file in a horizontal split.", fun: hsplit, completer: Some(completers::filename), - } + }, + TypableCommand { + name: "tutor", + aliases: &[], + doc: "Open the tutorial.", + fun: tutor, + completer: None, + }, ]; pub static COMMANDS: Lazy> = Lazy::new(|| { @@ -2561,7 +2702,7 @@ fn buffer_picker(cx: &mut Context) { cx.editor .documents .iter() - .map(|(id, doc)| (id, doc.path().cloned())) + .map(|(id, doc)| (*id, doc.path().cloned())) .collect(), move |(id, path): &(DocumentId, Option)| { let path = path.as_deref().map(helix_core::path::get_relative_path); @@ -2580,7 +2721,7 @@ fn buffer_picker(cx: &mut Context) { editor.switch(*id, Action::Replace); }, |editor, (id, path)| { - let doc = &editor.documents.get(*id)?; + let doc = &editor.documents.get(id)?; let &view_id = doc.selections().keys().next()?; let line = doc .selection(view_id) @@ -2996,8 +3137,13 @@ fn goto_line(cx: &mut Context) { doc.text().len_lines() - 1 }; let line_idx = std::cmp::min(count.get() - 1, max_line); + let text = doc.text().slice(..); let pos = doc.text().line_to_char(line_idx); - doc.set_selection(view.id, Selection::point(pos)); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } } @@ -3011,8 +3157,13 @@ fn goto_last_line(cx: &mut Context) { } else { doc.text().len_lines() - 1 }; + let text = doc.text().slice(..); let pos = doc.text().line_to_char(line_idx); - doc.set_selection(view.id, Selection::point(pos)); + let selection = doc + .selection(view.id) + .clone() + .transform(|range| range.put_cursor(text, pos, doc.mode == Mode::Select)); + doc.set_selection(view.id, selection); } fn goto_last_accessed_file(cx: &mut Context) { @@ -3306,26 +3457,24 @@ fn goto_first_diag(cx: &mut Context) { let editor = &mut cx.editor; let (_, doc) = current!(editor); - let diag = if let Some(diag) = doc.diagnostics().first() { - diag.range.start - } else { - return; + let pos = match doc.diagnostics().first() { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_last_diag(cx: &mut Context) { let editor = &mut cx.editor; let (_, doc) = current!(editor); - let diag = if let Some(diag) = doc.diagnostics().last() { - diag.range.start - } else { - return; + let pos = match doc.diagnostics().last() { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_next_diag(cx: &mut Context) { @@ -3336,20 +3485,19 @@ fn goto_next_diag(cx: &mut Context) { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - let diag = if let Some(diag) = doc + + let diag = doc .diagnostics() .iter() - .map(|diag| diag.range.start) - .find(|&pos| pos > cursor_pos) - { - diag - } else if let Some(diag) = doc.diagnostics().first() { - diag.range.start - } else { - return; + .find(|diag| diag.range.start > cursor_pos) + .or_else(|| doc.diagnostics().first()); + + let pos = match diag { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn goto_prev_diag(cx: &mut Context) { @@ -3360,21 +3508,20 @@ fn goto_prev_diag(cx: &mut Context) { .selection(view.id) .primary() .cursor(doc.text().slice(..)); - let diag = if let Some(diag) = doc + + let diag = doc .diagnostics() .iter() .rev() - .map(|diag| diag.range.start) - .find(|&pos| pos < cursor_pos) - { - diag - } else if let Some(diag) = doc.diagnostics().last() { - diag.range.start - } else { - return; + .find(|diag| diag.range.start < cursor_pos) + .or_else(|| doc.diagnostics().last()); + + let pos = match diag { + Some(diag) => diag.range.start, + None => return, }; - goto_pos(editor, diag); + goto_pos(editor, pos); } fn signature_help(cx: &mut Context) { @@ -3423,7 +3570,26 @@ pub mod insert { pub type Hook = fn(&Rope, &Selection, char) -> Option; pub type PostHook = fn(&mut Context, char); - fn completion(cx: &mut Context, ch: char) { + // It trigger completion when idle timer reaches deadline + // Only trigger completion if the word under cursor is longer than n characters + pub fn idle_completion(cx: &mut Context) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + + use helix_core::chars::char_is_word; + let mut iter = text.chars_at(cursor); + iter.reverse(); + for _ in 0..cx.editor.config.completion_trigger_len { + match iter.next() { + Some(c) if char_is_word(c) => {} + _ => return, + } + } + super::completion(cx); + } + + fn language_server_completion(cx: &mut Context, ch: char) { // if ch matches completion char, trigger completion let doc = doc_mut!(cx.editor); let language_server = match doc.language_server() { @@ -3433,19 +3599,14 @@ pub mod insert { let capabilities = language_server.capabilities(); - if let lsp::ServerCapabilities { - completion_provider: - Some(lsp::CompletionOptions { - trigger_characters: Some(triggers), - .. - }), + if let Some(lsp::CompletionOptions { + trigger_characters: Some(triggers), .. - } = capabilities + }) = &capabilities.completion_provider { // TODO: what if trigger is multiple chars long - let is_trigger = triggers.iter().any(|trigger| trigger.contains(ch)); - - if is_trigger { + if triggers.iter().any(|trigger| trigger.contains(ch)) { + cx.editor.clear_idle_timer(); super::completion(cx); } } @@ -3527,7 +3688,8 @@ pub mod insert { // TODO: need a post insert hook too for certain triggers (autocomplete, signature help, etc) // this could also generically look at Transaction, but it's a bit annoying to look at // Operation instead of Change. - for hook in &[completion, signature_help] { + for hook in &[language_server_completion, signature_help] { + // for hook in &[signature_help] { hook(cx, c); } } @@ -3668,13 +3830,19 @@ pub mod insert { fn undo(cx: &mut Context) { let (view, doc) = current!(cx.editor); let view_id = view.id; - doc.undo(view_id); + let success = doc.undo(view_id); + if !success { + cx.editor.set_status("Already at oldest change".to_owned()); + } } fn redo(cx: &mut Context) { let (view, doc) = current!(cx.editor); let view_id = view.id; - doc.redo(view_id); + let success = doc.redo(view_id); + if !success { + cx.editor.set_status("Already at newest change".to_owned()); + } } // Yank / Paste @@ -3735,7 +3903,7 @@ fn yank_joined_to_clipboard_impl( } fn yank_joined_to_clipboard(cx: &mut Context) { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; let _ = yank_joined_to_clipboard_impl( &mut cx.editor, line_ending.as_str(), @@ -3769,7 +3937,7 @@ fn yank_main_selection_to_clipboard(cx: &mut Context) { } fn yank_joined_to_primary_clipboard(cx: &mut Context) { - let line_ending = current!(cx.editor).1.line_ending; + let line_ending = doc!(cx.editor).line_ending; let _ = yank_joined_to_clipboard_impl( &mut cx.editor, line_ending.as_str(), @@ -3882,11 +4050,21 @@ fn replace_with_yanked(cx: &mut Context) { let registers = &mut cx.editor.registers; if let Some(values) = registers.read(reg_name) { - if let Some(yank) = values.first() { + if !values.is_empty() { + let repeat = std::iter::repeat( + values + .last() + .map(|value| Tendril::from_slice(value)) + .unwrap(), + ); + let mut values = values + .iter() + .map(|value| Tendril::from_slice(value)) + .chain(repeat); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { if !range.is_empty() { - (range.from(), range.to(), Some(yank.as_str().into())) + (range.from(), range.to(), Some(values.next().unwrap())) } else { (range.from(), range.to(), None) } @@ -4128,6 +4306,7 @@ fn keep_selections(cx: &mut Context) { cx, "keep:".into(), Some(reg), + |_input: &str| Vec::new(), move |view, doc, regex, event| { if event != PromptEvent::Update { return; @@ -4228,6 +4407,7 @@ pub fn completion(cx: &mut Context) { iter.reverse(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let start_offset = cursor.saturating_sub(offset); + let prefix = text.slice(start_offset..cursor).to_string(); cx.callback( future, @@ -4240,7 +4420,7 @@ pub fn completion(cx: &mut Context) { return; } - let items = match response { + let mut items = match response { Some(lsp::CompletionResponse::Array(items)) => items, // TODO: do something with is_incomplete Some(lsp::CompletionResponse::List(lsp::CompletionList { @@ -4250,6 +4430,18 @@ pub fn completion(cx: &mut Context) { None => Vec::new(), }; + if !prefix.is_empty() { + items = items + .into_iter() + .filter(|item| { + item.filter_text + .as_ref() + .unwrap_or(&item.label) + .starts_with(&prefix) + }) + .collect(); + } + if items.is_empty() { // editor.set_error("No completion available".to_string()); return; @@ -4401,27 +4593,32 @@ fn rotate_selection_contents_backward(cx: &mut Context) { // tree sitter node selection fn expand_selection(cx: &mut Context) { - let (view, doc) = current!(cx.editor); + let motion = |editor: &mut Editor| { + let (view, doc) = current!(editor); - if let Some(syntax) = doc.syntax() { - let text = doc.text().slice(..); - let selection = object::expand_selection(syntax, text, doc.selection(view.id)); - doc.set_selection(view.id, selection); - } + if let Some(syntax) = doc.syntax() { + let text = doc.text().slice(..); + let selection = object::expand_selection(syntax, text, doc.selection(view.id)); + doc.set_selection(view.id, selection); + } + }; + motion(&mut cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(motion))); } fn match_brackets(cx: &mut Context) { let (view, doc) = current!(cx.editor); if let Some(syntax) = doc.syntax() { - let pos = doc - .selection(view.id) - .primary() - .cursor(doc.text().slice(..)); - if let Some(pos) = match_brackets::find(syntax, doc.text(), pos) { - let selection = Selection::point(pos); - doc.set_selection(view.id, selection); - }; + let text = doc.text().slice(..); + let selection = doc.selection(view.id).clone().transform(|range| { + if let Some(pos) = match_brackets::find(syntax, doc.text(), range.anchor) { + range.put_cursor(text, pos, doc.mode == Mode::Select) + } else { + range + } + }); + doc.set_selection(view.id, selection); } } @@ -4429,7 +4626,7 @@ fn match_brackets(cx: &mut Context) { fn jump_forward(cx: &mut Context) { let count = cx.count(); - let (view, _doc) = current!(cx.editor); + let view = view_mut!(cx.editor); if let Some((id, selection)) = view.jumps.forward(count) { view.doc = *id; @@ -4463,6 +4660,22 @@ fn rotate_view(cx: &mut Context) { cx.editor.focus_next() } +fn jump_view_right(cx: &mut Context) { + cx.editor.focus_right() +} + +fn jump_view_left(cx: &mut Context) { + cx.editor.focus_left() +} + +fn jump_view_up(cx: &mut Context) { + cx.editor.focus_up() +} + +fn jump_view_down(cx: &mut Context) { + cx.editor.focus_down() +} + // split helper, clear it later fn split(cx: &mut Context, action: Action) { let (view, doc) = current!(cx.editor); @@ -4552,20 +4765,43 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let count = cx.count(); cx.on_next_key(move |cx, event| { if let Some(ch) = event.char() { - let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); + let textobject = move |editor: &mut Editor| { + let (view, doc) = current!(editor); + let text = doc.text().slice(..); + + let textobject_treesitter = |obj_name: &str, range: Range| -> Range { + let (lang_config, syntax) = match doc.language_config().zip(doc.syntax()) { + Some(t) => t, + None => return range, + }; + textobject::textobject_treesitter( + text, + range, + objtype, + obj_name, + syntax.tree().root_node(), + lang_config, + count, + ) + }; - let selection = doc.selection(view.id).clone().transform(|range| { - match ch { - 'w' => textobject::textobject_word(text, range, objtype, count), - // TODO: cancel new ranges if inconsistent surround matches across lines - ch if !ch.is_ascii_alphanumeric() => { - textobject::textobject_surround(text, range, objtype, ch, count) + let selection = doc.selection(view.id).clone().transform(|range| { + match ch { + 'w' => textobject::textobject_word(text, range, objtype, count), + 'c' => textobject_treesitter("class", range), + 'f' => textobject_treesitter("function", range), + 'p' => textobject_treesitter("parameter", range), + // TODO: cancel new ranges if inconsistent surround matches across lines + ch if !ch.is_ascii_alphanumeric() => { + textobject::textobject_surround(text, range, objtype, ch, count) + } + _ => range, } - _ => range, - } - }); - doc.set_selection(view.id, selection); + }); + doc.set_selection(view.id, selection); + }; + textobject(&mut cx.editor); + cx.editor.last_motion = Some(Motion(Box::new(textobject))); } }) } @@ -4577,7 +4813,7 @@ fn surround_add(cx: &mut Context) { let selection = doc.selection(view.id); let (open, close) = surround::get_pair(ch); - let mut changes = Vec::new(); + let mut changes = Vec::with_capacity(selection.len() * 2); for range in selection.iter() { changes.push((range.from(), range.from(), Some(Tendril::from_char(open)))); changes.push((range.to(), range.to(), Some(Tendril::from_char(close)))); diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index cad1df054..dc8b91d75 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -207,7 +207,7 @@ pub trait AnyComponent { /// /// ```rust /// use helix_term::{ui::Text, compositor::Component}; - /// let boxed: Box = Box::new(Text::new("text".to_string())); + /// let boxed: Box = Box::new(Text::new("text".to_string())); /// let text: Box = boxed.as_boxed_any().downcast().unwrap(); /// ``` fn as_boxed_any(self: Box) -> Box; diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index e344457cb..35dbce2ff 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -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, - #[serde(skip)] order: Vec, - #[serde(skip)] pub is_sticky: bool, } +impl<'de> Deserialize<'de> for KeyTrieNode { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let map = HashMap::::deserialize(deserializer)?; + let order = map.keys().copied().collect::>(); // NOTE: map.keys() has arbitrary order + Ok(Self { + map, + order, + ..Default::default() + }) + } +} + impl KeyTrieNode { pub fn new(name: &str, map: HashMap, order: Vec) -> 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)> = Vec::with_capacity(self.len()); + let mut body: Vec<(&str, BTreeSet)> = 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 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,63 +703,101 @@ pub fn merge_keys(mut config: Config) -> Config { config } -#[test] -fn merge_partial_keys() { - let config = Config { - keys: Keymaps(hashmap! { - Mode::Normal => Keymap::new( - keymap!({ "Normal mode" - "i" => normal_mode, - "无" => insert_mode, - "z" => jump_backward, - "g" => { "Merge into goto mode" - "$" => goto_line_end, - "g" => delete_char_forward, - }, - }) - ) - }), - ..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(); - assert_eq!( - keymap.get(key!('i')).kind, - KeymapResultKind::Matched(Command::normal_mode), - "Leaf should replace leaf" - ); - assert_eq!( - keymap.get(key!('无')).kind, - KeymapResultKind::Matched(Command::insert_mode), - "New leaf should be present in merged keymap" - ); - // Assumes that z is a node in the default keymap - assert_eq!( - keymap.get(key!('z')).kind, - KeymapResultKind::Matched(Command::jump_backward), - "Leaf should replace node" - ); - // Assumes that `g` is a node in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('$')]).unwrap(), - &KeyTrie::Leaf(Command::goto_line_end), - "Leaf should be present in merged subnode" - ); - // Assumes that `gg` is in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('g')]).unwrap(), - &KeyTrie::Leaf(Command::delete_char_forward), - "Leaf should replace old leaf in merged subnode" - ); - // Assumes that `ge` is in default keymap - assert_eq!( - keymap.root().search(&[key!('g'), key!('e')]).unwrap(), - &KeyTrie::Leaf(Command::goto_last_line), - "Old leaves in subnode should be present in merged node" - ); - - assert!(merged_config.keys.0.get(&Mode::Normal).unwrap().len() > 1); - assert!(merged_config.keys.0.get(&Mode::Insert).unwrap().len() > 0); +#[cfg(test)] +mod tests { + use super::*; + #[test] + fn merge_partial_keys() { + let config = Config { + keys: Keymaps(hashmap! { + Mode::Normal => Keymap::new( + keymap!({ "Normal mode" + "i" => normal_mode, + "无" => insert_mode, + "z" => jump_backward, + "g" => { "Merge into goto mode" + "$" => goto_line_end, + "g" => delete_char_forward, + }, + }) + ) + }), + ..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(); + assert_eq!( + keymap.get(key!('i')).kind, + KeymapResultKind::Matched(Command::normal_mode), + "Leaf should replace leaf" + ); + assert_eq!( + keymap.get(key!('无')).kind, + KeymapResultKind::Matched(Command::insert_mode), + "New leaf should be present in merged keymap" + ); + // Assumes that z is a node in the default keymap + assert_eq!( + keymap.get(key!('z')).kind, + KeymapResultKind::Matched(Command::jump_backward), + "Leaf should replace node" + ); + // Assumes that `g` is a node in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('$')]).unwrap(), + &KeyTrie::Leaf(Command::goto_line_end), + "Leaf should be present in merged subnode" + ); + // Assumes that `gg` is in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('g')]).unwrap(), + &KeyTrie::Leaf(Command::delete_char_forward), + "Leaf should replace old leaf in merged subnode" + ); + // Assumes that `ge` is in default keymap + assert_eq!( + keymap.root().search(&[key!('g'), key!('e')]).unwrap(), + &KeyTrie::Leaf(Command::goto_last_line), + "Old leaves in subnode should be present in merged node" + ); + + 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()) + } } diff --git a/helix-term/src/main.rs b/helix-term/src/main.rs index 180dacd1f..f746895cf 100644 --- a/helix-term/src/main.rs +++ b/helix-term/src/main.rs @@ -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 diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index c75b24f1a..dd782d29d 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -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 = 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(), - ); - doc.apply(&remove, view.id); - } + let transaction = item_to_transaction( + doc, + item, + offset_encoding, + start_offset, + trigger_offset, + ); + + // initialize a savepoint + doc.savepoint(); - let transaction = item_to_transaction(doc, view, item, offset_encoding); 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(), - ); - doc.apply(&remove, view.id); - } - - let transaction = item_to_transaction(doc, view, item, offset_encoding); + let transaction = item_to_transaction( + doc, + item, + offset_encoding, + start_offset, + trigger_offset, + ); 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 { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 037f04b8e..26a0358d1 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -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 = { - 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 + // Position + let pos = coords_at_pos( + doc.text().slice(..), + doc.selection(view.id) + .primary() + .cursor(doc.text().slice(..)), ); - 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, 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,8 +1449,10 @@ impl Component for EditorView { info.render(area, surface, cx); } - if let Some(ref mut info) = self.autoinfo { - 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 @@ -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 { diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 055593fda..3c492d149 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -64,25 +64,23 @@ impl Menu { } 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)| { - let text = option.filter_text(); - // TODO: using fuzzy_indices could give us the char idx for match highlighting - matcher - .fuzzy_match(text, pattern) - .map(|score| (index, score)) - })); + 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 + 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 Menu { 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 Component for Menu { | 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 Component for Menu { | 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); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index e66673ca2..00c70cea5 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -29,6 +29,7 @@ pub fn regex_prompt( cx: &mut crate::commands::Context, prompt: std::borrow::Cow<'static, str>, history_register: Option, + completion_fn: impl FnMut(&str) -> Vec + '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 { - 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() { diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 341235ee3..3e805facc 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -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 { picker: Picker, /// Caches paths to documents - preview_cache: HashMap, + preview_cache: HashMap, + read_buffer: Vec, /// Given an item in the picker, return the file path and line number to display. file_fn: Box Option>, } +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(_) => "", + Self::Cached(preview) => match preview { + CachedPreview::Document(_) => "", + CachedPreview::Binary => "", + CachedPreview::LargeFile => "", + CachedPreview::NotFound => "", + }, + } + } +} + impl FilePicker { pub fn new( options: Vec, @@ -45,6 +90,7 @@ impl FilePicker { 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 FilePicker { }) } - 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 Component for FilePicker { // |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 Component for FilePicker { 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 Component for FilePicker { ); // 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 Picker { } 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 Component for Picker { code: KeyCode::BackTab, .. } + | KeyEvent { + code: KeyCode::Char('k'), + modifiers: KeyModifiers::CONTROL, + } | KeyEvent { code: KeyCode::Char('p'), modifiers: KeyModifiers::CONTROL, @@ -350,6 +428,10 @@ impl Component for Picker { | 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 Component for Picker { 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 Component for Picker { text_style }, true, + true, ); } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index 56335fb36..593fd934b 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -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, diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index 80a772a46..6df65d360 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -1,11 +1,11 @@ [package] name = "helix-tui" -version = "0.4.1" +version = "0.5.0" authors = ["Blaž Hrastnik "] 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" } diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 377e3e395..f480bc2f3 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -266,12 +266,14 @@ impl Buffer { where S: AsRef, { - 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( &mut self, x: u16, @@ -280,6 +282,7 @@ impl Buffer { width: usize, style: Style, ellipsis: bool, + truncate_start: bool, ) -> (u16, u16) where S: AsRef, @@ -289,28 +292,59 @@ 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)); - for s in graphemes { - let width = s.width(); - if width == 0 { - continue; + if !truncate_start { + for s in graphemes { + let width = s.width(); + if width == 0 { + continue; + } + // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we + // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32. + if width > max_offset.saturating_sub(x_offset) { + break; + } + + self.content[index].set_symbol(s); + self.content[index].set_style(style); + // Reset following cells if multi-width (they would be hidden by the grapheme), + for i in index + 1..index + width { + self.content[i].reset(); + } + index += width; + x_offset += width; } - // `x_offset + width > max_offset` could be integer overflow on 32-bit machines if we - // change dimenstions to usize or u32 and someone resizes the terminal to 1x2^32. - if width > max_offset.saturating_sub(x_offset) { - break; + if ellipsis && x_offset - (x as usize) < string.as_ref().width() { + self.content[index].set_symbol("…"); } - - self.content[index].set_symbol(s); - self.content[index].set_style(style); - // Reset following cells if multi-width (they would be hidden by the grapheme), - for i in index + 1..index + width { - self.content[i].reset(); + } 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; } - index += width; - x_offset += width; - } - if ellipsis && x_offset - (x as usize) < string.as_ref().width() { - self.content[index].set_symbol("…"); } (x_offset as u16, y) } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 1f55a36b5..ffe6a111c 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "helix-view" -version = "0.4.1" +version = "0.5.0" authors = ["Blaž Hrastnik "] -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" diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index a11224ace..a492652d8 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -116,7 +116,7 @@ pub fn get_clipboard_provider() -> Box { } } 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 { 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 { diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 1f1b1f5f1..ce5df8ee8 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -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, + + pub savepoint: Option, + 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. - ) + self.language + .as_ref() + .and_then(|config| config.indent.as_ref()) + .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) { self.diagnostics = diagnostics; + // sort by range + self.diagnostics + .sort_unstable_by_key(|diagnostic| diagnostic.range); } } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 60864e9e1..591e0492b 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -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); +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, + pub next_document_id: usize, + pub documents: BTreeMap, pub count: Option, pub selected_register: Option, pub registers: Registers, @@ -124,6 +141,7 @@ pub struct Editor { pub config: Config, pub idle_timer: Pin>, + pub last_motion: Option, } #[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); - view.jumps.push(jump); - view.last_accessed_doc = Some(view.doc); + 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, CursorKind) { let view = view!(self); - let doc = &self.documents[view.doc]; + let doc = &self.documents[&view.doc]; let cursor = doc .selection(view.id) .primary() diff --git a/helix-view/src/info.rs b/helix-view/src/info.rs index 629a3112f..b5a002fa4 100644 --- a/helix-view/src/info.rs +++ b/helix-view/src/info.rs @@ -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)>) -> Info { + pub fn new(title: &str, body: Vec<(&str, BTreeSet)>) -> Info { let body = body .into_iter() .map(|(desc, events)| { diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 1e0ddfe25..580204ccc 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -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, diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index 26a4d6d2c..810aa0635 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -54,7 +54,7 @@ impl From 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. diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index c37474d65..3e779356c 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -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; } diff --git a/helix-view/src/macros.rs b/helix-view/src/macros.rs index 0bebd02fc..63d76a420 100644 --- a/helix-view/src/macros.rs +++ b/helix-view/src/macros.rs @@ -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) }}; } diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 9c33685be..757316bde 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,6 +1,5 @@ use std::{ collections::HashMap, - convert::TryFrom, path::{Path, PathBuf}, }; diff --git a/helix-view/src/tree.rs b/helix-view/src/tree.rs index 576f64f08..064334b12 100644 --- a/helix-view/src/tree.rs +++ b/helix-view/src/tree.rs @@ -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 { + 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 { + 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)); + } +} diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index 8a7d33746..ee236e94c 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -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); diff --git a/languages.toml b/languages.toml index 050f1b4fd..56d91a81e 100644 --- a/languages.toml +++ b/languages.toml @@ -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" } diff --git a/runtime/queries/bash/highlights.scm b/runtime/queries/bash/highlights.scm index 754faedac..57898f277 100644 --- a/runtime/queries/bash/highlights.scm +++ b/runtime/queries/bash/highlights.scm @@ -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) diff --git a/runtime/queries/c-sharp/highlights.scm b/runtime/queries/c-sharp/highlights.scm index b76f4e601..6e84ad837 100644 --- a/runtime/queries/c-sharp/highlights.scm +++ b/runtime/queries/c-sharp/highlights.scm @@ -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" diff --git a/runtime/queries/c/highlights.scm b/runtime/queries/c/highlights.scm index 2c42710f9..918f3f66b 100644 --- a/runtime/queries/c/highlights.scm +++ b/runtime/queries/c/highlights.scm @@ -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 diff --git a/runtime/queries/cmake/highlights.scm b/runtime/queries/cmake/highlights.scm new file mode 100644 index 000000000..71e9b5d9c --- /dev/null +++ b/runtime/queries/cmake/highlights.scm @@ -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)$") + ) + diff --git a/runtime/queries/cpp/highlights.scm b/runtime/queries/cpp/highlights.scm index 3315fde05..3348ef3ca 100644 --- a/runtime/queries/cpp/highlights.scm +++ b/runtime/queries/cpp/highlights.scm @@ -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 diff --git a/runtime/queries/css/highlights.scm b/runtime/queries/css/highlights.scm index 763661af7..4dfc0c66d 100644 --- a/runtime/queries/css/highlights.scm +++ b/runtime/queries/css/highlights.scm @@ -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 diff --git a/runtime/queries/elixir/highlights.scm b/runtime/queries/elixir/highlights.scm index 6bf93a210..76fd2af9d 100644 --- a/runtime/queries/elixir/highlights.scm +++ b/runtime/queries/elixir/highlights.scm @@ -1,125 +1,210 @@ -["when" "and" "or" "not in" "not" "in" "fn" "do" "end" "catch" "rescue" "after" "else"] @keyword - -[(true) (false) (nil)] @constant.builtin - -(keyword - [(keyword_literal) - ":"] @tag) - -(keyword - (keyword_string - [(string_start) - (string_content) - (string_end)] @tag)) - -[(atom_literal) - (atom_start) - (atom_content) - (atom_end)] @tag - -[(comment) - (unused_identifier)] @comment - -(escape_sequence) @escape - -(call function: (function_identifier) @keyword - (#match? @keyword "^(defmodule|defexception|defp|def|with|case|cond|raise|import|require|use|defmacrop|defmacro|defguardp|defguard|defdelegate|defstruct|alias|defimpl|defprotocol|defoverridable|receive|if|for|try|throw|unless|reraise|super|quote|unquote|unquote_splicing)$")) - -(call function: (function_identifier) @keyword - [(call - function: (function_identifier) @function - (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 - (arguments - [(identifier) @variable.parameter - (_ (identifier) @variable.parameter) - (_ (_ (identifier) @variable.parameter)) - (_ (_ (_ (identifier) @variable.parameter))) - (_ (_ (_ (_ (identifier) @variable.parameter)))) - (_ (_ (_ (_ (_ (identifier) @variable.parameter)))))])) +; 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) + +(binary_operator + operator: _ @operator) + +(dot + operator: _ @operator) + +(stab_clause + operator: _ @operator) + +; Literals + +(nil) @constant.builtin + +(boolean) @constant.builtin.boolean +(integer) @constant.numeric.integer +(float) @constant.numeric.float + +(alias) @type + +(call + target: (dot + left: (atom) @type)) + +(char) @constant.character + +; Quoted content + +(interpolation "#{" @punctuation.special "}" @punctuation.special) @embedded + +(escape_sequence) @constant.character.escape + +[ + (atom) + (quoted_atom) + (keyword) + (quoted_keyword) +] @string.special.symbol + +[ + (string) + (charlist) +] @string + +; Note that we explicitly target sigil quoted start/end, so they are not overridden by delimiters + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string + quoted_end: _ @string + (#match? @__name__ "^[sS]$")) @string + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string.regexp + quoted_end: _ @string.regexp + (#match? @__name__ "^[rR]$")) @string.regexp + +(sigil + (sigil_name) @__name__ + quoted_start: _ @string.special + quoted_end: _ @string.special) @string.special + +; Calls + +; * definition keyword +(call + target: (identifier) @keyword + (#match? @keyword "^(def|defdelegate|defexception|defguard|defguardp|defimpl|defmacro|defmacrop|defmodule|defn|defnp|defoverridable|defp|defprotocol|defstruct)$")) + +; * kernel or special forms keyword +(call + target: (identifier) @keyword + (#match? @keyword "^(alias|case|cond|else|for|if|import|quote|raise|receive|require|reraise|super|throw|try|unless|unquote|unquote_splicing|use|with)$")) + +; * function call +(call + target: [ + ; local + (identifier) @function + ; remote + (dot + right: (identifier) @function) + ]) + +; * just identifier in function definition +(call + target: (identifier) @keyword + (arguments + [ + (identifier) @function + (binary_operator + left: (identifier) @function operator: "when") - (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_op - operator: _ @operator) - -(heredoc - [(heredoc_start) - (heredoc_content) - (heredoc_end)] @string) - -(string - [(string_start) - (string_content) - (string_end)] @string) - -(sigil_start) @string.special -(sigil_content) @string -(sigil_end) @string.special - -(interpolation - "#{" @punctuation.special - "}" @punctuation.special) + ]) + (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) + +; * pipe into identifier (definition) +(call + target: (identifier) @keyword + (arguments + (binary_operator + operator: "|>" + right: (identifier) @variable)) + (#match? @keyword "^(def|defdelegate|defguard|defguardp|defmacro|defmacrop|defn|defnp|defp)$")) + +; * pipe into identifier (function call) +(binary_operator + operator: "|>" + right: (identifier) @function) + +; Identifiers + +; * special +( + (identifier) @constant.builtin + (#match? @constant.builtin "^(__MODULE__|__DIR__|__ENV__|__CALLER__|__STACKTRACE__)$") +) + +; * unused +( + (identifier) @comment + (#match? @comment "^_") +) + +; * regular +(identifier) @variable + +; Comment + +(comment) @comment + +; Punctuation + +[ + "%" +] @punctuation [ "," - "->" - "." + ";" ] @punctuation.delimiter [ @@ -133,6 +218,4 @@ ">>" ] @punctuation.bracket -(special_identifier) @function.special - (ERROR) @warning diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm index 3129c4b2b..56384d4d7 100644 --- a/runtime/queries/go/highlights.scm +++ b/runtime/queries/go/highlights.scm @@ -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) diff --git a/runtime/queries/go/textobjects.scm b/runtime/queries/go/textobjects.scm new file mode 100644 index 000000000..9bcfc6903 --- /dev/null +++ b/runtime/queries/go/textobjects.scm @@ -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) diff --git a/runtime/queries/haskell/highlights.scm b/runtime/queries/haskell/highlights.scm index dada80b60..721878769 100644 --- a/runtime/queries/haskell/highlights.scm +++ b/runtime/queries/haskell/highlights.scm @@ -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 diff --git a/runtime/queries/java/highlights.scm b/runtime/queries/java/highlights.scm index e7d793df9..77902fce3 100644 --- a/runtime/queries/java/highlights.scm +++ b/runtime/queries/java/highlights.scm @@ -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) diff --git a/runtime/queries/javascript/highlights.scm b/runtime/queries/javascript/highlights.scm index e29829bfc..6163b680d 100644 --- a/runtime/queries/javascript/highlights.scm +++ b/runtime/queries/javascript/highlights.scm @@ -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 ;------- diff --git a/runtime/queries/json/highlights.scm b/runtime/queries/json/highlights.scm index b08ea4393..6df6c9ebc 100644 --- a/runtime/queries/json/highlights.scm +++ b/runtime/queries/json/highlights.scm @@ -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 diff --git a/runtime/queries/julia/highlights.scm b/runtime/queries/julia/highlights.scm index 7b7d426c1..7c4479853 100644 --- a/runtime/queries/julia/highlights.scm +++ b/runtime/queries/julia/highlights.scm @@ -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) diff --git a/runtime/queries/ledger/highlights.scm b/runtime/queries/ledger/highlights.scm index 86c609c2a..bdf5f2dbb 100644 --- a/runtime/queries/ledger/highlights.scm +++ b/runtime/queries/ledger/highlights.scm @@ -7,9 +7,9 @@ (date) (interval) (quantity) -] @number +] @constant.numeric.integer -((account) @field) +((account) @variable.other.member) ((commodity) @text.literal) "include" @include diff --git a/runtime/queries/lua/highlights.scm b/runtime/queries/lua/highlights.scm index 40c2be709..e73b32d6e 100644 --- a/runtime/queries/lua/highlights.scm +++ b/runtime/queries/lua/highlights.scm @@ -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 diff --git a/runtime/queries/nix/highlights.scm b/runtime/queries/nix/highlights.scm index 741b73b5d..66719e876 100644 --- a/runtime/queries/nix/highlights.scm +++ b/runtime/queries/nix/highlights.scm @@ -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) ) [ ";" diff --git a/runtime/queries/ocaml/highlights.scm b/runtime/queries/ocaml/highlights.scm index 160f2cb41..15f46cc14 100644 --- a/runtime/queries/ocaml/highlights.scm +++ b/runtime/queries/ocaml/highlights.scm @@ -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 ;--------- diff --git a/runtime/queries/php/highlights.scm b/runtime/queries/php/highlights.scm index 029045558..46b5d26c2 100644 --- a/runtime/queries/php/highlights.scm +++ b/runtime/queries/php/highlights.scm @@ -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 diff --git a/runtime/queries/php/indents.toml b/runtime/queries/php/indents.toml new file mode 100644 index 000000000..85c104db5 --- /dev/null +++ b/runtime/queries/php/indents.toml @@ -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 = [ + "}", + ")", +] diff --git a/runtime/queries/protobuf/highlights.scm b/runtime/queries/protobuf/highlights.scm index cd021be1a..c35c430e3 100644 --- a/runtime/queries/protobuf/highlights.scm +++ b/runtime/queries/protobuf/highlights.scm @@ -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 diff --git a/runtime/queries/python/highlights.scm b/runtime/queries/python/highlights.scm index f64fecb2c..9131acc5b 100644 --- a/runtime/queries/python/highlights.scm +++ b/runtime/queries/python/highlights.scm @@ -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 diff --git a/runtime/queries/python/indents.toml b/runtime/queries/python/indents.toml new file mode 100644 index 000000000..6bc684864 --- /dev/null +++ b/runtime/queries/python/indents.toml @@ -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"] diff --git a/runtime/queries/python/textobjects.scm b/runtime/queries/python/textobjects.scm new file mode 100644 index 000000000..a52538afd --- /dev/null +++ b/runtime/queries/python/textobjects.scm @@ -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) diff --git a/runtime/queries/ruby/highlights.scm b/runtime/queries/ruby/highlights.scm index 8617d6f0c..898f8f794 100644 --- a/runtime/queries/ruby/highlights.scm +++ b/runtime/queries/ruby/highlights.scm @@ -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) diff --git a/runtime/queries/rust/highlights.scm b/runtime/queries/rust/highlights.scm index 956a5dacd..539d95500 100644 --- a/runtime/queries/rust/highlights.scm +++ b/runtime/queries/rust/highlights.scm @@ -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 diff --git a/runtime/queries/rust/textobjects.scm b/runtime/queries/rust/textobjects.scm new file mode 100644 index 000000000..e3132687a --- /dev/null +++ b/runtime/queries/rust/textobjects.scm @@ -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) diff --git a/runtime/queries/svelte/highlights.scm b/runtime/queries/svelte/highlights.scm index 4c6f5f356..4fcdfd669 100644 --- a/runtime/queries/svelte/highlights.scm +++ b/runtime/queries/svelte/highlights.scm @@ -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 diff --git a/runtime/queries/toml/highlights.scm b/runtime/queries/toml/highlights.scm index e4d6966fc..2742b2be6 100644 --- a/runtime/queries/toml/highlights.scm +++ b/runtime/queries/toml/highlights.scm @@ -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 diff --git a/runtime/queries/tsq/highlights.scm b/runtime/queries/tsq/highlights.scm index 9ba5699a8..549895c15 100644 --- a/runtime/queries/tsq/highlights.scm +++ b/runtime/queries/tsq/highlights.scm @@ -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 diff --git a/runtime/queries/yaml/highlights.scm b/runtime/queries/yaml/highlights.scm index 2955a4ce0..a7efb5e71 100644 --- a/runtime/queries/yaml/highlights.scm +++ b/runtime/queries/yaml/highlights.scm @@ -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 diff --git a/runtime/queries/zig/highlights.scm b/runtime/queries/zig/highlights.scm index 404a8682a..34dbeacd0 100644 --- a/runtime/queries/zig/highlights.scm +++ b/runtime/queries/zig/highlights.scm @@ -14,7 +14,7 @@ parameter: (IDENTIFIER) @variable.parameter [ field_member: (IDENTIFIER) field_access: (IDENTIFIER) -] @variable.property +] @variable.other.member ;; assume TitleCase is a type ( @@ -75,9 +75,9 @@ field_constant: (IDENTIFIER) @constant ((BUILTINIDENTIFIER) @keyword.control.import (#any-of? @keyword.control.import "@import" "@cImport")) -(INTEGER) @number +(INTEGER) @constant.numeric.integer -(FLOAT) @number +(FLOAT) @constant.numeric.float [ (LINESTRING) @@ -85,7 +85,7 @@ field_constant: (IDENTIFIER) @constant ] @string (CHAR_LITERAL) @constant.character -(EscapeSequence) @escape +(EscapeSequence) @constant.character.escape (FormatSequence) @string.special [ diff --git a/runtime/themes/base16_default_dark.toml b/runtime/themes/base16_default_dark.toml new file mode 100644 index 000000000..d65995c05 --- /dev/null +++ b/runtime/themes/base16_default_dark.toml @@ -0,0 +1,59 @@ +# Author: RayGervais + +"ui.background" = { bg = "base00" } +"ui.menu" = "base01" +"ui.menu.selected" = { fg = "base04", bg = "base01" } +"ui.linenr" = {fg = "base01" } +"ui.popup" = { bg = "base01" } +"ui.window" = { bg = "base01" } +"ui.liner.selected" = "base02" +"ui.selection" = "base02" +"comment" = "base03" +"ui.statusline" = {fg = "base04", bg = "base01" } +"ui.help" = { fg = "base04", bg = "base01" } +"ui.cursor" = { fg = "base05", modifiers = ["reversed"] } +"ui.text" = { fg = "base05" } +"operator" = "base05" +"ui.text.focus" = { fg = "base05" } +"variable" = "base08" +"constant.numeric" = "base09" +"constant" = "base09" +"attributes" = "base09" +"type" = "base0A" +"ui.cursor.match" = { fg = "base0A", modifiers = ["underlined"] } +"strings" = "base0B" +"variable.other.member" = "base0B" +"constant.character.escape" = "base0C" +"function" = "base0D" +"constructor" = "base0D" +"special" = "base0D" +"keyword" = "base0E" +"label" = "base0E" +"namespace" = "base0E" +"ui.popup" = { bg = "base01" } +"ui.window" = { bg = "base00" } +"ui.help" = { bg = "base01", fg = "base06" } + +"info" = "base03" +"hint" = "base03" +"debug" = "base03" +"diagnostic" = "base03" +"error" = "base0E" + +[palette] +base00 = "#181818" # Default Background +base01 = "#282828" # Lighter Background (Used for status bars, line number and folding marks) +base02 = "#383838" # Selection Background +base03 = "#585858" # Comments, Invisibles, Line Highlighting +base04 = "#b8b8b8" # Dark Foreground (Used for status bars) +base05 = "#d8d8d8" # Default Foreground, Caret, Delimiters, Operators +base06 = "#e8e8e8" # Light Foreground (Not often used) +base07 = "#f8f8f8" # Light Background (Not often used) +base08 = "#ab4642" # Variables, XML Tags, Markup Link Text, Markup Lists, Diff Deleted +base09 = "#dc9656" # Integers, Boolean, Constants, XML Attributes, Markup Link Url +base0A = "#f7ca88" # Classes, Markup Bold, Search Text Background +base0B = "#a1b56c" # Strings, Inherited Class, Markup Code, Diff Inserted +base0C = "#86c1b9" # Support, Regular Expressions, Escape Characters, Markup Quotes +base0D = "#7cafc2" # Functions, Methods, Attribute IDs, Headings +base0E = "#ba8baf" # Keywords, Storage, Selector, Markup Italic, Diff Changed +base0F = "#a16946" # Deprecated, Opening/Closing Embedded Language Tags, e.g. diff --git a/runtime/themes/bogster.toml b/runtime/themes/bogster.toml index 37b9adbfd..86a6c34bf 100644 --- a/runtime/themes/bogster.toml +++ b/runtime/themes/bogster.toml @@ -8,7 +8,7 @@ "punctuation.delimiter" = "#dc7759" "operator" = { fg = "#dc7759", modifiers = ["bold"] } "special" = "#7fdc59" -"property" = "#c6b8ad" +"variable.other.member" = "#c6b8ad" "variable" = "#c6b8ad" "variable.parameter" = "#c6b8ad" "type" = "#dc597f" @@ -22,8 +22,8 @@ "constant" = "#59dcb7" "constant.builtin" = "#59dcb7" "string" = "#59dcb7" -"number" = "#59c0dc" -"escape" = { fg = "#7fdc59", modifiers = ["bold"] } +"constant.numeric" = "#59c0dc" +"constant.character.escape" = { fg = "#7fdc59", modifiers = ["bold"] } "label" = "#59c0dc" "module" = "#d32c5d" diff --git a/runtime/themes/dark_plus.toml b/runtime/themes/dark_plus.toml index c48a7e286..0554f827f 100644 --- a/runtime/themes/dark_plus.toml +++ b/runtime/themes/dark_plus.toml @@ -7,7 +7,7 @@ "type.builtin" = { fg = "type" } "type.enum.variant" = { fg = "constant" } "constructor" = { fg = "constant" } -"property" = { fg = "variable" } +"variable.other.member" = { fg = "variable" } "keyword" = { fg = "keyword" } "keyword.directive" = { fg = "keyword" } @@ -31,47 +31,64 @@ "function.macro" = { fg = "keyword" } "attribute" = { fg = "fn_declaration" } -"comment" = { fg = "#6A9955" } +"comment" = { fg = "dark_green" } -"string" = { fg = "#ce9178" } -"string.regexp" = { fg = "regex" } -"number" = { fg = "#b5cea8" } -"escape" = { fg = "#d7ba7d" } +"string" = { fg = "orange" } +"constant.character" = { fg = "orange" } +"string.regexp" = { fg = "gold" } +"constant.numeric" = { fg = "pale_green" } +"constant.character.escape" = { fg = "gold" } -"ui.background" = { fg = "#d4d4d4", bg = "#1e1e1e" } +"ui.background" = { fg = "light_gray", bg = "dark_gray2" } "ui.window" = { bg = "widget" } "ui.popup" = { bg = "widget" } "ui.help" = { bg = "widget" } "ui.menu.selected" = { bg = "widget" } +# TODO: Alternate bg colour for `ui.cursor.match` and `ui.selection`. "ui.cursor" = { fg = "cursor", modifiers = ["reversed"] } "ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] } "ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] } "ui.selection" = { bg = "#3a3d41" } -"ui.selection.primary" = { bg = "#264f78" } +"ui.selection.primary" = { bg = "dark_blue" } -"ui.linenr" = { fg = "#858585" } -"ui.linenr.selected" = { fg = "#c6c6c6" } +"ui.linenr" = { fg = "dark_gray" } +"ui.linenr.selected" = { fg = "light_gray2" } -"ui.statusline" = { fg = "#ffffff", bg = "#007acc" } -"ui.statusline.inactive" = { fg = "#ffffff", bg = "#007acc" } +"ui.statusline" = { fg = "white", bg = "blue" } +"ui.statusline.inactive" = { fg = "white", bg = "blue" } "ui.text" = { fg = "text", bg = "background" } -"ui.text.focus" = { fg = "#ffffff" } +"ui.text.focus" = { fg = "white" } -"warning" = { fg = "#cca700" } -"error" = { fg = "#ff1212" } -"info" = { fg = "#75beff" } -"hint" = { fg = "#eeeeeeb3" } +"warning" = { fg = "gold2" } +"error" = { fg = "red" } +"info" = { fg = "light_blue" } +"hint" = { fg = "light_gray3" } diagnostic = { modifiers = ["underlined"] } [palette] +white = "#ffffff" +orange = "#ce9178" +gold = "#d7ba7d" +gold2 = "#cca700" +pale_green = "#b5cea8" +dark_green = "#6A9955" +light_gray = "#d4d4d4" +light_gray2 = "#c6c6c6" +light_gray3 = "#eeeeee" +dark_gray = "#858585" +dark_gray2 = "#1e1e1e" +blue = "#007acc" +light_blue = "#75beff" +dark_blue = "#264f78" +red = "#ff1212" + type = "#4EC9B0" keyword = "#569CD6" -regex = "#CE9178" special = "#C586C0" variable = "#9CDCFE" fn_declaration = "#DCDCAA" diff --git a/runtime/themes/everforest_dark.toml b/runtime/themes/everforest_dark.toml index 462c82651..bbd005e6a 100644 --- a/runtime/themes/everforest_dark.toml +++ b/runtime/themes/everforest_dark.toml @@ -8,16 +8,16 @@ # Email: sainnhe@gmail.com # License: MIT License -"escape" = "orange" +"constant.character.escape" = "orange" "type" = "yellow" "constant" = "purple" -"number" = "purple" +"constant.numeric" = "purple" "string" = "grey2" "comment" = "grey0" "variable" = "fg" "variable.builtin" = "blue" "variable.parameter" = "fg" -"variable.property" = "fg" +"variable.other.member" = "fg" "label" = "aqua" "punctuation" = "grey2" "punctuation.delimiter" = "grey2" @@ -32,7 +32,6 @@ "attribute" = "aqua" "constructor" = "yellow" "module" = "blue" -"property" = "fg" "special" = "orange" "ui.background" = { bg = "bg0" } diff --git a/runtime/themes/gruvbox.toml b/runtime/themes/gruvbox.toml index 0a6eec07b..0ff039eab 100644 --- a/runtime/themes/gruvbox.toml +++ b/runtime/themes/gruvbox.toml @@ -9,8 +9,7 @@ "punctuation.delimiter" = "orange1" "operator" = "purple1" "special" = "purple0" -"property" = "blue1" -"variable.property" = "blue1" +"variable.other.member" = "blue1" "variable" = "fg1" "variable.builtin" = "orange1" "variable.parameter" = "fg2" @@ -24,8 +23,8 @@ "constant" = { fg = "purple1" } "constant.builtin" = { fg = "purple1", modifiers = ["bold"] } "string" = "green1" -"number" = "purple1" -"escape" = { fg = "fg2", modifiers = ["bold"] } +"constant.numeric" = "purple1" +"constant.character.escape" = { fg = "fg2", modifiers = ["bold"] } "label" = "aqua1" "module" = "aqua1" diff --git a/runtime/themes/ingrid.toml b/runtime/themes/ingrid.toml index 6a177ec7c..308294759 100644 --- a/runtime/themes/ingrid.toml +++ b/runtime/themes/ingrid.toml @@ -8,7 +8,7 @@ "punctuation.delimiter" = "#C97270" "operator" = { fg = "#D74E50", modifiers = ["bold"] } "special" = "#D68482" -"property" = "#89BEB7" +"variable.other.member" = "#89BEB7" "variable" = "#A6B6CE" "variable.parameter" = "#89BEB7" "type" = { fg = "#A6B6CE", modifiers = ["bold"] } @@ -22,8 +22,8 @@ "constant" = "#D4A520" "constant.builtin" = "#D4A520" "string" = "#D74E50" -"number" = "#D74E50" -"escape" = { fg = "#D74E50", modifiers = ["bold"] } +"constant.numeric" = "#D74E50" +"constant.character.escape" = { fg = "#D74E50", modifiers = ["bold"] } "label" = "#D68482" "module" = "#839A53" diff --git a/runtime/themes/monokai.toml b/runtime/themes/monokai.toml index a8f03ff3c..38f9f1707 100644 --- a/runtime/themes/monokai.toml +++ b/runtime/themes/monokai.toml @@ -7,7 +7,7 @@ "type.builtin" = { fg = "#66D9EF" } "type.enum.variant" = { fg = "text" } "constructor" = { fg = "text" } -"property" = { fg = "variable" } +"variable.other.member" = { fg = "variable" } "keyword" = { fg = "keyword" } "keyword.directive" = { fg = "keyword" } @@ -34,9 +34,10 @@ "comment" = { fg = "#88846F" } "string" = { fg = "#e6db74" } +"constant.character" = { fg = "#e6db74" } "string.regexp" = { fg = "regex" } -"number" = { fg = "#ae81ff" } -"escape" = { fg = "#ae81ff" } +"constant.numeric" = { fg = "#ae81ff" } +"constant.character.escape" = { fg = "#ae81ff" } "ui.background" = { fg = "text", bg = "background" } diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml index ee7c88650..78736c3bd 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -1,84 +1,102 @@ # Author : RayGervais -# "ui.linenr.selected" = { fg = "#d8dee9" } -"ui.text.focus" = { fg = "#88c0d0", modifiers= ["bold"] } -# "ui.menu.selected" = { fg = "#e5ded6", bg = "#313f4e" } +"ui.linenr.selected" = { fg = "nord4" } +"ui.text.focus" = { fg = "nord8", modifiers= ["bold"] } +"ui.menu.selected" = { fg = "nord8", bg = "nord2" } -# "info" = "#b48ead" -# "hint" = "#a3be8c" +"info" = "nord8" +"hint" = "nord8" # Polar Night # nord0 - background color -"ui.background" = { bg = "#2e3440" } -"ui.statusline.inactive" = { fg = "#d8dEE9", bg = "#2e3440" } +"ui.background" = { bg = "nord0" } +"ui.statusline.inactive" = { fg = "nord4", bg = "nord0" } # nord1 - status bars, panels, modals, autocompletion -"ui.statusline" = { fg = "#88c0d0", bg = "#3b4252" } +"ui.statusline" = { fg = "nord8", bg = "nord1" } "ui.popup" = { bg = "#232d38" } "ui.window" = { bg = "#232d38" } -"ui.help" = { bg = "#232d38", fg = "#e5ded6" } +"ui.help" = { bg = "#232d38", fg = "nord4" } # nord2 - active line, highlighting -"ui.selection" = { bg = "#434c5e" } -"ui.cursor.match" = { bg = "434c5e" } +"ui.selection" = { bg = "nord2" } +"ui.cursor.match" = { bg = "nord2" } -# nord3 - comments -"comment" = "#616E88" -"ui.linenr" = { fg = "#616E88" } +# nord3 - comments, nord3 based lighter color +# relative: https://github.com/arcticicestudio/nord/issues/94 +"comment" = "gray" +"ui.linenr" = { fg = "gray" } # Snow Storm # nord4 - cursor, variables, constants, attributes, fields -"ui.cursor.primary" = { fg = "#d8dee9", modifiers = ["reversed"] } -"attribute" = "#d8dee9" -"variable" = "#d8dee9" -"constant" = "#d8dee9" -"variable.builtin" = "#d8dee9" -"constant.builtin" = "#d8dee9" -"namespace" = "#d8dee9" +"ui.cursor.primary" = { fg = "nord4", modifiers = ["reversed"] } +"attribute" = "nord4" +"variable" = "nord4" +"constant" = "nord4" +"variable.builtin" = "nord4" +"constant.builtin" = "nord4" +"namespace" = "nord4" # nord5 - suble UI text # nord6 - base text, punctuation -"ui.text" = { fg = "#eceff4" } -"punctuation" = "#eceff4" +"ui.text" = { fg = "nord6" } +"punctuation" = "nord6" # Frost # nord7 - classes, types, primiatives -"type" = "#8fbcbb" -"type.builtin" = { fg = "#8fbcbb"} -"label" = "#8fbcbb" +"type" = "nord7" +"type.builtin" = { fg = "nord7"} +"label" = "nord7" # nord8 - declaration, methods, routines -"constructor" = "#88c0d0" -"function" = "#88c0d0" -"function.macro" = { fg = "#88c0d0" } -"function.builtin" = { fg = "#88c0d0" } +"constructor" = "nord8" +"function" = "nord8" +"function.macro" = { fg = "nord8" } +"function.builtin" = { fg = "nord8" } # nord9 - operator, tags, units, punctuations -"punctuation.delimiter" = "#81a1c1" -"operator" = { fg = "#81a1c1" } -"property" = "#81a1c1" +"punctuation.delimiter" = "nord9" +"operator" = { fg = "nord9" } +"variable.other.member" = "nord9" # nord10 - keywords, special -"keyword" = { fg = "#5e81ac" } -"keyword.directive" = "#5e81ac" -"variable.parameter" = "#5e81ac" +"keyword" = { fg = "nord10" } +"keyword.directive" = "nord10" +"variable.parameter" = "nord10" # Aurora # nord11 - error -"error" = "#bf616a" +"error" = "nord11" # nord12 - annotations, decorators -"special" = "#d08770" -"module" = "#d08770" +"special" = "nord12" +"module" = "nord12" # nord13 - warnings, escape characters, regex -"warning" = "#ebcb8b" -"escape" = { fg = "#ebcb8b" } +"warning" = "nord13" +"constant.character.escape" = { fg = "nord13" } # nord14 - strings -"string" = "#a3be8c" +"string" = "nord14" # nord15 - integer, floating point -"number" = "#b48ead" +"constant.numeric" = "nord15" + +[palette] +nord0 = "#2e3440" +nord1 = "#3b4252" +nord2 = "#434c5e" +nord4 = "#d8dee9" +nord6 = "#eceff4" +nord7 = "#8fbcbb" +nord8 = "#88c0d0" +nord9 = "#81a1c1" +nord10 = "#5e81ac" +nord11 = "#bf616a" +nord12 = "#d08770" +nord13 = "#ebcb8b" +nord14 = "#a3be8c" +nord15 = "#b48ead" +gray = "#616e88" diff --git a/runtime/themes/onedark.toml b/runtime/themes/onedark.toml index 37057f583..40ed1abe4 100644 --- a/runtime/themes/onedark.toml +++ b/runtime/themes/onedark.toml @@ -1,54 +1,71 @@ # Author : Gokul Soumya -"attribute" = { fg = "#E5C07B" } -"comment" = { fg = "#5C6370", modifiers = ['italic'] } -"constant" = { fg = "#56B6C2" } -"constant.builtin" = { fg = "#61AFEF" } -"constructor" = { fg = "#61AFEF" } -"escape" = { fg = "#D19A66" } -"function" = { fg = "#61AFEF" } -"function.builtin" = { fg = "#61AFEF" } -"function.macro" = { fg = "#C678DD" } -"keyword" = { fg = "#E06C75" } -"keyword.directive" = { fg = "#C678DD" } -"label" = { fg = "#C678DD" } -"namespace" = { fg = "#61AFEF" } -"number" = { fg = "#D19A66" } -"operator" = { fg = "#C678DD" } -"property" = { fg = "#E06C75" } -"special" = { fg = "#61AFEF" } -"string" = { fg = "#98C379" } -"type" = { fg = "#E5C07B" } -"type.builtin" = { fg = "#E5C07B" } -"variable" = { fg = "#61AFEF" } -"variable.builtin" = { fg = "#61AFEF" } -"variable.parameter" = { fg = "#E06C75" } +"attribute" = { fg = "yellow" } +"comment" = { fg = "light-gray", modifiers = ["italic"] } +"constant" = { fg = "cyan" } +"constant.builtin" = { fg = "blue" } +"constructor" = { fg = "blue" } +"escape" = { fg = "gold" } +"function" = { fg = "blue" } +"function.builtin" = { fg = "blue" } +"function.macro" = { fg = "purple" } +"keyword" = { fg = "red" } +"keyword.control" = { fg = "purple" } +"keyword.directive" = { fg = "purple" } +"label" = { fg = "purple" } +"namespace" = { fg = "blue" } +"number" = { fg = "gold" } +"operator" = { fg = "purple" } +"property" = { fg = "red" } +"special" = { fg = "blue" } +"string" = { fg = "green" } +"type" = { fg = "yellow" } +"type.builtin" = { fg = "yellow" } +# "variable" = { fg = "blue" } +"variable.builtin" = { fg = "blue" } +"variable.parameter" = { fg = "red" } diagnostic = { modifiers = ["underlined"] } -"info" = { fg = "#61afef", modifiers = ['bold'] } -"hint" = { fg = "#98c379", modifiers = ['bold'] } -"warning" = { fg = "#e5c07b", modifiers = ['bold'] } -"error" = { fg = "#e06c75", modifiers = ['bold'] } +"info" = { fg = "blue", modifiers = ["bold"] } +"hint" = { fg = "green", modifiers = ["bold"] } +"warning" = { fg = "yellow", modifiers = ["bold"] } +"error" = { fg = "red", modifiers = ["bold"] } -"ui.background" = { bg = "#282C34" } +"ui.background" = { bg = "black" } -"ui.cursor" = { fg = "#ABB2BF", modifiers = ["reversed"] } -"ui.cursor.primary" = { fg = "#ABB2BF", modifiers = ["reversed"] } -"ui.cursor.match" = { fg = "#61AFEF", modifiers = ['underlined']} +"ui.cursor" = { fg = "white", modifiers = ["reversed"] } +"ui.cursor.primary" = { fg = "white", modifiers = ["reversed"] } +"ui.cursor.match" = { fg = "blue", modifiers = ["underlined"]} -"ui.selection" = { bg = "#5C6370" } -"ui.selection.primary" = { bg = "#3E4452" } +"ui.selection" = { bg = "light-gray" } +"ui.selection.primary" = { bg = "gray" } -"ui.linenr" = { fg = "#4B5263", modifiers = ['dim'] } -"ui.linenr.selected" = { fg = "#ABB2BF" } +"ui.linenr" = { fg = "linenr", modifiers = ["dim"] } +"ui.linenr.selected" = { fg = "white" } -"ui.statusline" = { fg = "#ABB2BF", bg = "#2C323C" } -"ui.statusline.inactive" = { fg = "#5C6370", bg = "#2C323C" } +"ui.statusline" = { fg = "white", bg = "light-black" } +"ui.statusline.inactive" = { fg = "light-gray", bg = "light-black" } -"ui.text" = { fg = "#ABB2BF" } -"ui.text.focus" = { fg = "#ABB2BF", bg = "#2C323C", modifiers = ['bold'] } +"ui.text" = { fg = "white" } +"ui.text.focus" = { fg = "white", bg = "light-black", modifiers = ["bold"] } -"ui.help" = { bg = "#3E4452" } -"ui.popup" = { bg = "#3E4452" } -"ui.window" = { bg = "#3E4452" } -"ui.menu.selected" = { fg = "#282C34", bg = "#61AFEF" } +"ui.help" = { bg = "gray" } +"ui.popup" = { bg = "gray" } +"ui.window" = { bg = "gray" } +"ui.menu.selected" = { fg = "black", bg = "blue" } + +[palette] + +yellow = "#E5C07B" +blue = "#61AFEF" +red = "#E06C75" +purple = "#C678DD" +green = "#98C379" +gold = "#D19A66" +cyan = "#56B6C2" +white = "#ABB2BF" +black = "#282C34" +light-black = "#2C323C" +gray = "#3E4452" +light-gray = "#5C6370" +linenr = "#4B5263" diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml new file mode 100644 index 000000000..537770084 --- /dev/null +++ b/runtime/themes/rose_pine.toml @@ -0,0 +1,61 @@ +# Author: RayGervais + +"ui.background" = { bg = "base" } +"ui.menu" = "surface" +"ui.menu.selected" = { fg = "iris", bg = "surface" } +"ui.linenr" = {fg = "subtle" } +"ui.popup" = { bg = "overlay" } +"ui.window" = { bg = "overlay" } +"ui.liner.selected" = "highlightOverlay" +"ui.selection" = "highlight" +"comment" = "subtle" +"ui.statusline" = {fg = "foam", bg = "surface" } +"ui.help" = { fg = "foam", bg = "surface" } +"ui.cursor" = { fg = "rose", modifiers = ["reversed"] } +"ui.text" = { fg = "text" } +"operator" = "rose" +"ui.text.focus" = { fg = "base05" } +"variable" = "text" +"constant.numeric" = "iris" +"constant" = "gold" +"attributes" = "gold" +"type" = "foam" +"ui.cursor.match" = { fg = "gold", modifiers = ["underlined"] } +"string" = "gold" +"property" = "foam" +"constant.character.escape" = "subtle" +"function" = "rose" +"function.builtin" = "rose" +"function.method" = "foam" +"constructor" = "gold" +"special" = "gold" +"keyword" = "pine" +"label" = "iris" +"namespace" = "pine" +"ui.popup" = { bg = "overlay" } +"ui.window" = { bg = "base" } +"ui.help" = { bg = "overlay", fg = "foam" } +"text" = "text" + +"info" = "gold" +"hint" = "gold" +"debug" = "rose" +"diagnostic" = "rose" +"error" = "love" + +[palette] +base = "#191724" +surface = "#1f1d2e" +overlay = "#26233a" +inactive = "#555169" +subtle = "#6e6a86" +text = "#e0def4" +love = "#eb6f92" +gold = "#f6c177" +rose = "#ebbcba" +pine = "#31748f" +foam = "#9ccfd8" +iris = "#c4a7e7" +highlight = "#2a2837" +highlightInactive = "#211f2d" +highlightOverlay = "#3a384a" diff --git a/theme.toml b/theme.toml index 82b71a7dd..3956e25e6 100644 --- a/theme.toml +++ b/theme.toml @@ -6,7 +6,7 @@ punctuation = "lavender" "punctuation.delimiter" = "lavender" operator = "lilac" special = "honey" -property = "white" +variable.other.member = "white" variable = "lavender" # variable = "almond" # TODO: metavariables only # "variable.parameter" = { fg = "lavender", modifiers = ["underlined"] }