From 46d9f49cf9464ece983e534c873f0e8af0f49bff Mon Sep 17 00:00:00 2001 From: Omnikar Date: Fri, 24 Dec 2021 05:30:33 -0500 Subject: [PATCH] Merge branch 'master' into 'help-command' --- .cargo/config | 2 + .github/workflows/build.yml | 48 ++ .github/workflows/release.yml | 2 +- .gitmodules | 22 +- Cargo.lock | 66 +- Cargo.toml | 1 + README.md | 20 +- base16_theme.toml | 38 ++ book/src/SUMMARY.md | 4 +- book/src/commands.md | 5 + book/src/configuration.md | 1 + book/src/generated/lang-support.md | 47 ++ book/src/generated/typable-cmd.md | 43 ++ book/src/guides/adding_languages.md | 12 +- book/src/install.md | 9 + book/src/keymap.md | 7 +- book/src/lang-support.md | 10 + book/src/remapping.md | 10 +- book/src/themes.md | 16 + docs/CONTRIBUTING.md | 37 ++ flake.lock | 24 +- helix-core/Cargo.toml | 2 +- helix-core/src/auto_pairs.rs | 604 +++++++++++++++--- helix-core/src/lib.rs | 1 + helix-core/src/match_brackets.rs | 2 +- helix-core/src/movement.rs | 52 +- helix-core/src/selection.rs | 20 + helix-core/src/shellwords.rs | 164 +++++ helix-core/src/syntax.rs | 2 +- helix-core/src/transaction.rs | 2 +- helix-lsp/Cargo.toml | 2 +- helix-lsp/src/client.rs | 14 +- helix-lsp/src/lib.rs | 7 + helix-syntax/README.md | 13 + helix-syntax/languages/tree-sitter-comment | 1 + helix-syntax/languages/tree-sitter-dart | 1 + helix-syntax/languages/tree-sitter-dockerfile | 1 + helix-syntax/languages/tree-sitter-fish | 1 + helix-syntax/languages/tree-sitter-llvm | 1 + helix-syntax/languages/tree-sitter-markdown | 1 + helix-syntax/languages/tree-sitter-scala | 2 +- helix-term/Cargo.toml | 3 +- helix-term/src/application.rs | 76 ++- helix-term/src/commands.rs | 401 ++++++++---- helix-term/src/compositor.rs | 26 +- helix-term/src/keymap.rs | 5 +- helix-term/src/lib.rs | 11 + helix-term/src/ui/completion.rs | 6 +- helix-term/src/ui/editor.rs | 26 +- helix-term/src/ui/markdown.rs | 5 - helix-term/src/ui/menu.rs | 4 +- helix-term/src/ui/mod.rs | 1 + helix-term/src/ui/picker.rs | 4 +- helix-term/src/ui/popup.rs | 29 +- helix-term/src/ui/prompt.rs | 6 +- helix-view/Cargo.toml | 2 +- helix-view/src/document.rs | 4 + helix-view/src/editor.rs | 17 +- helix-view/src/gutter.rs | 3 +- helix-view/src/input.rs | 44 +- helix-view/src/keyboard.rs | 5 +- helix-view/src/theme.rs | 23 +- languages.toml | 59 +- runtime/queries/c/injections.scm | 2 + runtime/queries/cmake/injections.scm | 4 + runtime/queries/comment/highlights.scm | 30 + runtime/queries/cpp/injections.scm | 1 + runtime/queries/dart/highlights.scm | 237 +++++++ runtime/queries/dart/indents.toml | 20 + runtime/queries/dart/locals.scm | 20 + runtime/queries/dockerfile/highlights.scm | 51 ++ runtime/queries/dockerfile/injections.scm | 6 + runtime/queries/elixir/injections.scm | 2 + runtime/queries/fish/highlights.scm | 156 +++++ runtime/queries/fish/indents.toml | 12 + runtime/queries/fish/injections.scm | 2 + runtime/queries/fish/textobjects.scm | 1 + runtime/queries/glsl/injections.scm | 5 +- runtime/queries/haskell/highlights.scm | 2 +- runtime/queries/julia/injections.scm | 8 +- runtime/queries/julia/locals.scm | 22 +- runtime/queries/latex/highlights.scm | 28 +- runtime/queries/ledger/highlights.scm | 2 +- runtime/queries/ledger/injections.scm | 4 +- runtime/queries/llvm/injections.scm | 2 + runtime/queries/markdown/highlights.scm | 36 ++ runtime/queries/markdown/injections.scm | 8 + runtime/queries/ocaml/highlights.scm | 2 +- runtime/queries/ocaml/indents.toml | 2 +- runtime/queries/perl/indents.toml | 17 + runtime/queries/python/injections.scm | 2 + runtime/queries/rust/highlights.scm | 14 +- runtime/queries/rust/injections.scm | 3 + runtime/queries/scala/highlights.scm | 203 ++++++ runtime/queries/scala/indents.toml | 23 + runtime/queries/scala/injections.scm | 1 + runtime/queries/svelte/highlights.scm | 4 +- runtime/queries/svelte/injections.scm | 4 +- runtime/queries/tsq/injections.scm | 2 + runtime/queries/yaml/highlights.scm | 16 +- runtime/themes/base16_default_dark.toml | 37 +- runtime/themes/base16_default_light.toml | 60 ++ runtime/themes/base16_terminal.toml | 39 ++ runtime/themes/dracula.toml | 50 ++ runtime/themes/onedark.toml | 19 +- runtime/themes/solarized_dark.toml | 30 +- runtime/themes/solarized_light.toml | 46 +- theme.toml | 6 + xtask/Cargo.toml | 11 + xtask/src/main.rs | 277 ++++++++ 110 files changed, 3130 insertions(+), 476 deletions(-) create mode 100644 .cargo/config create mode 100644 base16_theme.toml create mode 100644 book/src/commands.md create mode 100644 book/src/generated/lang-support.md create mode 100644 book/src/generated/typable-cmd.md create mode 100644 book/src/lang-support.md create mode 100644 docs/CONTRIBUTING.md create mode 100644 helix-core/src/shellwords.rs create mode 100644 helix-syntax/README.md create mode 160000 helix-syntax/languages/tree-sitter-comment create mode 160000 helix-syntax/languages/tree-sitter-dart create mode 160000 helix-syntax/languages/tree-sitter-dockerfile create mode 160000 helix-syntax/languages/tree-sitter-fish create mode 160000 helix-syntax/languages/tree-sitter-llvm create mode 160000 helix-syntax/languages/tree-sitter-markdown create mode 100644 runtime/queries/c/injections.scm create mode 100644 runtime/queries/cmake/injections.scm create mode 100644 runtime/queries/comment/highlights.scm create mode 100644 runtime/queries/cpp/injections.scm create mode 100644 runtime/queries/dart/highlights.scm create mode 100644 runtime/queries/dart/indents.toml create mode 100644 runtime/queries/dart/locals.scm create mode 100644 runtime/queries/dockerfile/highlights.scm create mode 100644 runtime/queries/dockerfile/injections.scm create mode 100644 runtime/queries/elixir/injections.scm create mode 100644 runtime/queries/fish/highlights.scm create mode 100644 runtime/queries/fish/indents.toml create mode 100644 runtime/queries/fish/injections.scm create mode 100644 runtime/queries/fish/textobjects.scm create mode 100644 runtime/queries/llvm/injections.scm create mode 100644 runtime/queries/markdown/highlights.scm create mode 100644 runtime/queries/markdown/injections.scm create mode 100644 runtime/queries/perl/indents.toml create mode 100644 runtime/queries/python/injections.scm create mode 100644 runtime/queries/scala/highlights.scm create mode 100644 runtime/queries/scala/indents.toml create mode 100644 runtime/queries/scala/injections.scm create mode 100644 runtime/queries/tsq/injections.scm create mode 100644 runtime/themes/base16_default_light.toml create mode 100644 runtime/themes/base16_terminal.toml create mode 100644 runtime/themes/dracula.toml create mode 100644 xtask/Cargo.toml create mode 100644 xtask/src/main.rs diff --git a/.cargo/config b/.cargo/config new file mode 100644 index 000000000..35049cbcb --- /dev/null +++ b/.cargo/config @@ -0,0 +1,2 @@ +[alias] +xtask = "run --package xtask --" diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 216291804..7f18da6a4 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -137,3 +137,51 @@ jobs: with: command: clippy args: -- -D warnings + + docs: + name: Docs + runs-on: ubuntu-latest + steps: + - name: Checkout sources + uses: actions/checkout@v2 + with: + submodules: true + + - name: Install stable toolchain + uses: actions-rs/toolchain@v1 + with: + profile: minimal + toolchain: stable + override: true + + - name: Cache cargo registry + uses: actions/cache@v2.1.6 + with: + path: ~/.cargo/registry + 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 }}-v2-cargo-index-${{ hashFiles('**/Cargo.lock') }} + + - name: Cache cargo target dir + uses: actions/cache@v2.1.6 + with: + path: target + key: ${{ runner.os }}-v2-cargo-build-target-${{ hashFiles('**/Cargo.lock') }} + + - name: Generate docs + uses: actions-rs/cargo@v1 + with: + command: xtask + args: docgen + + - name: Check uncommitted documentation changes + run: | + git diff + git diff-files --quiet \ + || (echo "Run 'cargo xtask docgen', commit the changes and push again" \ + && exit 1) + diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index b16fa428a..7b0b7ee24 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -102,7 +102,7 @@ jobs: fi cp -r runtime dist - - uses: actions/upload-artifact@v2.2.4 + - uses: actions/upload-artifact@v2.3.1 with: name: bins-${{ matrix.build }} path: dist diff --git a/.gitmodules b/.gitmodules index 6295b9e95..edfe3c391 100644 --- a/.gitmodules +++ b/.gitmodules @@ -142,11 +142,31 @@ path = helix-syntax/languages/tree-sitter-perl url = https://github.com/ganezdragon/tree-sitter-perl shallow = true +[submodule "helix-syntax/languages/tree-sitter-comment"] + path = helix-syntax/languages/tree-sitter-comment + url = https://github.com/stsewd/tree-sitter-comment + shallow = true [submodule "helix-syntax/languages/tree-sitter-wgsl"] path = helix-syntax/languages/tree-sitter-wgsl url = https://github.com/szebniok/tree-sitter-wgsl shallow = true -[submodule "helix-syntax/tree-sitter-llvm"] +[submodule "helix-syntax/languages/tree-sitter-llvm"] path = helix-syntax/languages/tree-sitter-llvm url = https://github.com/benwilliamgraham/tree-sitter-llvm shallow = true +[submodule "helix-syntax/languages/tree-sitter-markdown"] + path = helix-syntax/languages/tree-sitter-markdown + url = https://github.com/MDeiml/tree-sitter-markdown + shallow = true +[submodule "helix-syntax/languages/tree-sitter-dart"] + path = helix-syntax/languages/tree-sitter-dart + url = https://github.com/UserNobody14/tree-sitter-dart.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-dockerfile"] + path = helix-syntax/languages/tree-sitter-dockerfile + url = https://github.com/camdencheek/tree-sitter-dockerfile.git + shallow = true +[submodule "helix-syntax/languages/tree-sitter-fish"] + path = helix-syntax/languages/tree-sitter-fish + url = https://github.com/ram02z/tree-sitter-fish + shallow = true diff --git a/Cargo.lock b/Cargo.lock index 47a6c01e0..27268feb0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -184,9 +184,9 @@ checksum = "e78d4f1cc4ae33bbfc157ed5d5a5ef3bc29227303d595861deb238fcec4e9457" [[package]] name = "encoding_rs" -version = "0.8.29" +version = "0.8.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a74ea89a0a1b98f6332de42c95baff457ada66d1cb4030f9ff151b2041a1c746" +checksum = "7896dc8abb250ffdda33912550faa54c88ec8b998dec0b2c55ab224921ce11df" dependencies = [ "cfg-if", ] @@ -258,15 +258,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "629316e42fe7c2a0b9a65b47d159ceaa5453ab14e8f0a3c5eedbb8cd55b4a445" +checksum = "d0c8ff0461b82559810cdccfde3215c3f373807f5e5232b71479bff7bb2583d7" [[package]] name = "futures-executor" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7b808bf53348a36cab739d7e04755909b9fcaaa69b7d7e588b37b6ec62704c97" +checksum = "29d6d2ff5bb10fb95c85b8ce46538a2e5f5e7fdc755623a7d4529ab8a4ed9d2a" dependencies = [ "futures-core", "futures-task", @@ -275,15 +275,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dabf1872aaab32c886832f2276d2f5399887e2bd613698a02359e4ea83f8de12" +checksum = "6ee7c6485c30167ce4dfb83ac568a849fe53274c831081476ee13e0dce1aad72" [[package]] name = "futures-util" -version = "0.3.18" +version = "0.3.19" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d22213122356472061ac0f1ab2cee28d2bac8491410fd68c2af53d1cedb83e" +checksum = "d9b5cf40b47a271f77a8b1bec03ca09044d99d2372c0de244e66430761127164" dependencies = [ "futures-core", "futures-task", @@ -535,9 +535,9 @@ dependencies = [ [[package]] name = "itoa" -version = "0.4.8" +version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b71991ff56294aa922b450139ee08b3bfc70982c6b2c7562771375cf73542dd4" +checksum = "1aab8fc367588b89dcee83ab0fd66b72b50b72fa1904d7095045ace2b0c81c35" [[package]] name = "jsonrpc-core" @@ -690,9 +690,9 @@ dependencies = [ [[package]] name = "num_cpus" -version = "1.13.0" +version = "1.13.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "05499f3756671c15885fee9034446956fff3f243d6077b91e5767df161f766b3" +checksum = "19e64526ebdee182341572e50e9ad03965aa510cd94427a4549448f285e957a1" dependencies = [ "hermit-abi", "libc", @@ -700,9 +700,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.8.0" +version = "1.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "692fcb63b64b1758029e0a96ee63e049ce8c5948587f2f7208df04625e5f6b56" +checksum = "da32515d9f6e6e489d7bc9d84c71b060db7247dc035bbe44eac88cf87486d8d5" [[package]] name = "parking_lot" @@ -877,18 +877,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.130" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f12d06de37cf59146fbdecab66aa99f9fe4f78722e3607577a5375d66bd0c913" +checksum = "8b9875c23cf305cd1fd7eb77234cbb705f21ea6a72c637a5c6db5fe4b8e7f008" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.130" +version = "1.0.132" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d7bc1a1ab1961464eae040d96713baa5a724a8152c1222492465b54322ec508b" +checksum = "ecc0db5cb2556c0e558887d9bbdcf6ac4471e83ff66cf696e5419024d1606276" dependencies = [ "proc-macro2", "quote", @@ -897,9 +897,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.72" +version = "1.0.73" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d0ffa0837f2dfa6fb90868c2b5468cad482e175f7dad97e7421951e663f2b527" +checksum = "bcbd0344bc6533bc7ec56df11d42fb70f1b912351c0825ccb7211b59d8af7cf5" dependencies = [ "itoa", "ryu", @@ -919,9 +919,9 @@ dependencies = [ [[package]] name = "signal-hook" -version = "0.3.10" +version = "0.3.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9c98891d737e271a2954825ef19e46bd16bdb98e2746f2eec4f7a4ef7946efd1" +checksum = "647c97df271007dcea485bb74ffdb57f2e683f1306c854f468a0c244badabf2d" dependencies = [ "libc", "signal-hook-registry", @@ -1069,11 +1069,10 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.14.0" +version = "1.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "70e992e41e0d2fb9f755b37446f20900f64446ef54874f40a60c78f021ac6144" +checksum = "fbbf1c778ec206785635ce8ad57fe52b3009ae9e0c9f574a728f3049d3e55838" dependencies = [ - "autocfg", "bytes", "libc", "memchr", @@ -1089,9 +1088,9 @@ dependencies = [ [[package]] name = "tokio-macros" -version = "1.6.0" +version = "1.7.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9efc1aba077437943f7515666aa2b882dfabfbfdf89c819ea75a8d6e9eaba5e" +checksum = "b557f72f448c511a979e2564e55d74e6c4432fc96ff4f6241bc6bded342643b7" dependencies = [ "proc-macro2", "quote", @@ -1259,3 +1258,12 @@ name = "winapi-x86_64-pc-windows-gnu" version = "0.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" + +[[package]] +name = "xtask" +version = "0.5.0" +dependencies = [ + "helix-core", + "helix-term", + "toml", +] diff --git a/Cargo.toml b/Cargo.toml index 580cccd64..8c3ee6717 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -6,6 +6,7 @@ members = [ "helix-tui", "helix-syntax", "helix-lsp", + "xtask", ] # Build helix-syntax in release mode to make the code path faster in development. diff --git a/README.md b/README.md index 3f4087b9f..71010cc82 100644 --- a/README.md +++ b/README.md @@ -44,8 +44,8 @@ cargo install --path helix-term This will install the `hx` binary to `$HOME/.cargo/bin`. Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the -config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overriden -via the `HELIX_RUNTIME` environment variable. +config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows). +This location can be overriden via the `HELIX_RUNTIME` environment variable. Packages already solve this for you by wrapping the `hx` binary with a wrapper that sets the variable to the install dir. @@ -65,21 +65,7 @@ brew install helix # Contributing -Contributors are very welcome! **No contribution is too small and all contributions are valued.** - -Some suggestions to get started: - -- You can look at the [good first issue](https://github.com/helix-editor/helix/issues?q=is%3Aopen+label%3AE-easy+label%3AE-good-first-issue) label on the issue tracker. -- Help with packaging on various distributions needed! -- 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 - it and defining syntax highlight queries for it is straight forward and - doesn't require much knowledge of the internals. - -We provide an [architecture.md](./docs/architecture.md) that should give you -a good overview of the internals. +Contributing guidelines can be found [here](./docs/CONTRIBUTING.md). # Getting help diff --git a/base16_theme.toml b/base16_theme.toml new file mode 100644 index 000000000..5ec74bcc6 --- /dev/null +++ b/base16_theme.toml @@ -0,0 +1,38 @@ +# Author: NNB + +"ui.menu" = "black" +"ui.menu.selected" = { modifiers = ["reversed"] } +"ui.linenr" = { fg = "gray", bg = "black" } +"ui.popup" = { modifiers = ["reversed"] } +"ui.linenr.selected" = { fg = "white", bg = "black", modifiers = ["bold"] } +"ui.selection" = { fg = "black", bg = "blue" } +"ui.selection.primary" = { fg = "white", bg = "blue" } +"comment" = { fg = "gray" } +"ui.statusline" = { fg = "black", bg = "white" } +"ui.statusline.inactive" = { fg = "gray", bg = "white" } +"ui.help" = { modifiers = ["reversed"] } +"ui.cursor" = { modifiers = ["reversed"] } +"variable" = "red" +"constant.numeric" = "yellow" +"constant" = "yellow" +"attributes" = "yellow" +"type" = "yellow" +"ui.cursor.match" = { fg = "yellow", modifiers = ["underlined"] } +"string" = "green" +"variable.other.member" = "green" +"constant.character.escape" = "cyan" +"function" = "blue" +"constructor" = "blue" +"special" = "blue" +"keyword" = "magenta" +"label" = "magenta" +"namespace" = "magenta" +"ui.help" = { fg = "white", bg = "black" } + +"diagnostic" = { modifiers = ["underlined"] } +"ui.gutter" = { bg = "black" } +"info" = "blue" +"hint" = "gray" +"debug" = "gray" +"warning" = "yellow" +"error" = "red" diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index 8cadb663f..a8f165c01 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -2,10 +2,12 @@ - [Installation](./install.md) - [Usage](./usage.md) + - [Keymap](./keymap.md) + - [Commands](./commands.md) + - [Language Support](./lang-support.md) - [Migrating from Vim](./from-vim.md) - [Configuration](./configuration.md) - [Themes](./themes.md) - - [Keymap](./keymap.md) - [Key Remapping](./remapping.md) - [Hooks](./hooks.md) - [Languages](./languages.md) diff --git a/book/src/commands.md b/book/src/commands.md new file mode 100644 index 000000000..4c4a5c05c --- /dev/null +++ b/book/src/commands.md @@ -0,0 +1,5 @@ +# Commands + +Command mode can be activated by pressing `:`, similar to vim. Built-in commands: + +{{#include ./generated/typable-cmd.md}} diff --git a/book/src/configuration.md b/book/src/configuration.md index 2ed48d51f..33a933b2b 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -23,6 +23,7 @@ To override global configuration parameters, create a `config.toml` file located | `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` | +| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` | `[editor.filepicker]` section of the config. Sets options for file picker and global search. All but the last key listed in the default file-picker configuration below are IgnoreOptions: whether hidden files and files listed within ignore files are ignored by (not visible in) the helix file picker and global search. There is also one other key, `max-depth` available, which is not defined by default. diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md new file mode 100644 index 000000000..c70542011 --- /dev/null +++ b/book/src/generated/lang-support.md @@ -0,0 +1,47 @@ +| Language | Syntax Highlighting | Treesitter Textobjects | Auto Indent | Default LSP | +| --- | --- | --- | --- | --- | +| bash | ✓ | | | `bash-language-server` | +| c | ✓ | | | `clangd` | +| c-sharp | ✓ | | | | +| cmake | ✓ | | | `cmake-language-server` | +| comment | ✓ | | | | +| cpp | ✓ | | | `clangd` | +| css | ✓ | | | | +| dart | ✓ | | ✓ | `dart` | +| dockerfile | ✓ | | | `docker-langserver` | +| elixir | ✓ | | | `elixir-ls` | +| fish | ✓ | ✓ | ✓ | | +| glsl | ✓ | | ✓ | | +| go | ✓ | ✓ | ✓ | `gopls` | +| html | ✓ | | | | +| java | ✓ | | | | +| javascript | ✓ | | ✓ | | +| json | ✓ | | ✓ | | +| julia | ✓ | | | `julia` | +| latex | ✓ | | | | +| ledger | ✓ | | | | +| llvm | ✓ | | | | +| lua | ✓ | | ✓ | | +| markdown | ✓ | | | | +| mint | | | | `mint` | +| nix | ✓ | | ✓ | `rnix-lsp` | +| ocaml | ✓ | | ✓ | | +| ocaml-interface | ✓ | | | | +| perl | ✓ | ✓ | ✓ | | +| php | ✓ | | ✓ | | +| prolog | | | | `swipl` | +| protobuf | ✓ | | ✓ | | +| python | ✓ | ✓ | ✓ | `pylsp` | +| racket | | | | `racket` | +| ruby | ✓ | | | `solargraph` | +| rust | ✓ | ✓ | ✓ | `rust-analyzer` | +| scala | ✓ | | ✓ | `metals` | +| svelte | ✓ | | ✓ | `svelteserver` | +| toml | ✓ | | | | +| tsq | ✓ | | | | +| tsx | ✓ | | | `typescript-language-server` | +| typescript | ✓ | | ✓ | `typescript-language-server` | +| vue | ✓ | | | | +| wgsl | ✓ | | | | +| yaml | ✓ | | ✓ | | +| zig | ✓ | | ✓ | `zls` | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md new file mode 100644 index 000000000..bb21fd6b3 --- /dev/null +++ b/book/src/generated/typable-cmd.md @@ -0,0 +1,43 @@ +| Name | Description | +| --- | --- | +| `:quit`, `:q` | Close the current view. | +| `:quit!`, `:q!` | Close the current view forcefully (ignoring unsaved changes). | +| `:open`, `:o` | Open a file from disk into the current view. | +| `:buffer-close`, `:bc`, `:bclose` | Close the current buffer. | +| `:buffer-close!`, `:bc!`, `:bclose!` | Close the current buffer forcefully (ignoring unsaved changes). | +| `:write`, `:w` | Write changes to disk. Accepts an optional path (:write some/path.txt) | +| `:new`, `:n` | Create a new scratch buffer. | +| `:format`, `:fmt` | Format the file using the LSP formatter. | +| `:indent-style` | Set the indentation style for editing. ('t' for tabs or 1-8 for number of spaces.) | +| `:line-ending` | Set the document's default line ending. Options: crlf, lf, cr, ff, nel. | +| `:earlier`, `:ear` | Jump back to an earlier point in edit history. Accepts a number of steps or a time span. | +| `:later`, `:lat` | Jump to a later point in edit history. Accepts a number of steps or a time span. | +| `:write-quit`, `:wq`, `:x` | Write changes to disk and close the current view. Accepts an optional path (:wq some/path.txt) | +| `:write-quit!`, `:wq!`, `:x!` | Write changes to disk and close the current view forcefully. Accepts an optional path (:wq! some/path.txt) | +| `:write-all`, `:wa` | Write changes from all views to disk. | +| `:write-quit-all`, `:wqa`, `:xa` | Write changes from all views to disk and close all views. | +| `:write-quit-all!`, `:wqa!`, `:xa!` | Write changes from all views to disk and close all views forcefully (ignoring unsaved changes). | +| `:quit-all`, `:qa` | Close all views. | +| `:quit-all!`, `:qa!` | Close all views forcefully (ignoring unsaved changes). | +| `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | +| `:theme` | Change the editor theme. | +| `:clipboard-yank` | Yank main selection into system clipboard. | +| `:clipboard-yank-join` | Yank joined selections into system clipboard. A separator can be provided as first argument. Default value is newline. | +| `:primary-clipboard-yank` | Yank main selection into system primary clipboard. | +| `:primary-clipboard-yank-join` | Yank joined selections into system primary clipboard. A separator can be provided as first argument. Default value is newline. | +| `:clipboard-paste-after` | Paste system clipboard after selections. | +| `:clipboard-paste-before` | Paste system clipboard before selections. | +| `:clipboard-paste-replace` | Replace selections with content of system clipboard. | +| `:primary-clipboard-paste-after` | Paste primary clipboard after selections. | +| `:primary-clipboard-paste-before` | Paste primary clipboard before selections. | +| `:primary-clipboard-paste-replace` | Replace selections with content of system primary clipboard. | +| `:show-clipboard-provider` | Show clipboard provider name in status bar. | +| `:change-current-directory`, `:cd` | Change the current working directory. | +| `:show-directory`, `:pwd` | Show the current working directory. | +| `:encoding` | Set encoding based on `https://encoding.spec.whatwg.org` | +| `:reload` | Discard changes and reload from the source file. | +| `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | +| `:vsplit`, `:vs` | Open the file in a vertical split. | +| `:hsplit`, `:hs`, `:sp` | Open the file in a horizontal split. | +| `:tutor` | Open the tutorial. | +| `:goto`, `:g` | Go to line number. | diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index 9ad2c2859..987ad088c 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -42,7 +42,16 @@ These are the available keys and descriptions for the file. ## Queries -For a language to have syntax-highlighting and indentation among other things, you have to add queries. Add a directory for your language with the path `runtime/queries//`. The tree-sitter [website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) gives more info on how to write queries. +For a language to have syntax-highlighting and indentation among +other things, you have to add queries. Add a directory for your +language with the path `runtime/queries//`. The tree-sitter +[website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) +gives more info on how to write queries. + +> NOTE: When evaluating queries, the first matching query takes +precedence, which is different from other editors like neovim where +the last matching query supercedes the ones before it. See +[this issue][neovim-query-precedence] for an example. ## Common Issues @@ -58,3 +67,4 @@ For a language to have syntax-highlighting and indentation among other things, y [treesitter-language-injection]: https://tree-sitter.github.io/tree-sitter/syntax-highlighting#language-injection [languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml +[neovim-query-precedence]: https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090 diff --git a/book/src/install.md b/book/src/install.md index d831934c4..1a5a9daa9 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -27,6 +27,15 @@ Releases are available in the `community` repository. A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch. +### Fedora Linux + +You can install the COPR package for Helix via + +``` +sudo dnf copr enable varlad/helix +sudo dnf install helix +``` + ## Build from source ``` diff --git a/book/src/keymap.md b/book/src/keymap.md index 59353db90..d84b17b8e 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -34,6 +34,7 @@ | `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` | +| `Ctrl-s` | Save the current selection to the jumplist | `save_selection` | | `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 | @@ -69,13 +70,15 @@ | `"` `` | Select a register to yank to or paste from | `select_register` | | `>` | Indent selection | `indent` | | `<` | Unindent selection | `unindent` | -| `=` | Format selection (**LSP**) | `format_selections` | +| `=` | Format selection (currently nonfunctional/disabled) (**LSP**) | `format_selections` | | `d` | Delete selection | `delete_selection` | | `Alt-d` | Delete selection, without yanking | `delete_selection_noyank` | | `c` | Change selection (delete and enter insert mode) | `change_selection` | | `Alt-c` | Change selection (delete and enter insert mode, without yanking) | `change_selection_noyank` | | `Ctrl-a` | Increment object (number) under cursor | `increment` | | `Ctrl-x` | Decrement object (number) under cursor | `decrement` | +| `q` | Start/stop macro recording to the selected register | `record_macro` | +| `Q` | Play back a recorded macro from the selected register | `play_macro` | #### Shell @@ -158,7 +161,7 @@ Jumps to various locations. | Key | Description | Command | | ----- | ----------- | ------- | -| `g` | Go to the start of the file | `goto_file_start` | +| `g` | Go to line number `` else start of file | `goto_file_start` | | `e` | Go to the end of the file | `goto_last_line` | | `f` | Go to files in the selection | `goto_file` | | `h` | Go to the start of the line | `goto_line_start` | diff --git a/book/src/lang-support.md b/book/src/lang-support.md new file mode 100644 index 000000000..3920f3424 --- /dev/null +++ b/book/src/lang-support.md @@ -0,0 +1,10 @@ +# Language Support + +For more information like arguments passed to default LSP server, +extensions assosciated with a filetype, custom LSP settings, filetype +specific indent settings, etc see the default +[`languages.toml`][languages.toml] file. + +{{#include ./generated/lang-support.md}} + +[languages.toml]: https://github.com/helix-editor/helix/blob/master/languages.toml diff --git a/book/src/remapping.md b/book/src/remapping.md index fffd189b7..1cdf9b1f2 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -11,6 +11,8 @@ this: ```toml # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select' [keys.normal] +C-s = ":w" # Maps the Control-s to the typable command :w which is an alias for :write (save file) +C-o = ":open ~/.config/helix/config.toml" # Maps the Control-o to opening of the helix config file a = "move_char_left" # Maps the 'a' key to the move_char_left command w = "move_line_up" # Maps the 'w' key move_line_up "C-S-esc" = "extend_line" # Maps Control-Shift-Escape to extend_line @@ -21,6 +23,7 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions "A-x" = "normal_mode" # Maps Alt-X to enter normal mode j = { k = "normal_mode" } # Maps `jk` to exit insert mode ``` +> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command. Control, Shift and Alt modifiers are encoded respectively with the prefixes `C-`, `S-` and `A-`. Special keys are encoded as follows: @@ -42,10 +45,9 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes | Down | `"down"` | | Home | `"home"` | | End | `"end"` | -| Page | `"pageup"` | -| Page | `"pagedown"` | +| Page Up | `"pageup"` | +| Page Down | `"pagedown"` | | Tab | `"tab"` | -| Back | `"backtab"` | | Delete | `"del"` | | Insert | `"ins"` | | Null | `"null"` | @@ -54,4 +56,4 @@ Control, Shift and Alt modifiers are encoded respectively with the prefixes Keys can be disabled by binding them to the `no_op` command. Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands. -> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `commands!` macro. +> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`. diff --git a/book/src/themes.md b/book/src/themes.md index fd3f5b1eb..fce2c0ca4 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -105,6 +105,7 @@ We use a similar set of scopes as - `type` - Types - `builtin` - Primitive types provided by the language (`int`, `usize`) +- `constructor` - `constant` (TODO: constant.other.placeholder for %v) - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) @@ -162,6 +163,21 @@ We use a similar set of scopes as - `namespace` +- `markup` + - `heading` + - `list` + - `unnumbered` + - `numbered` + - `bold` + - `italic` + - `link` + - `url` + - `label` + - `quote` + - `raw` + - `inline` + - `block` + #### Interface These scopes are used for theming the editor interface. diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md new file mode 100644 index 000000000..bdd771aaf --- /dev/null +++ b/docs/CONTRIBUTING.md @@ -0,0 +1,37 @@ +# Contributing + +Contributors are very welcome! **No contribution is too small and all contributions are valued.** + +Some suggestions to get started: + +- You can look at the [good first issue][good-first-issue] label on the issue tracker. +- Help with packaging on various distributions needed! +- To use print debugging to the [Helix log file][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 + it and defining syntax highlight queries for it is straight forward and + doesn't require much knowledge of the internals. + +We provide an [architecture.md][architecture.md] that should give you +a good overview of the internals. + +# Auto generated documentation + +Some parts of [the book][docs] are autogenerated from the code itself, +like the list of `:commands` and supported languages. To generate these +files, run + +```shell +cargo xtask docgen +``` + +inside the project. We use [xtask][xtask] as an ad-hoc task runner and +thus do not require any dependencies other than `cargo` (You don't have +to `cargo install` anything either). + +[good-first-issue]: https://github.com/helix-editor/helix/labels/E-easy +[log-file]: https://github.com/helix-editor/helix/wiki/FAQ#access-the-log-file +[architecture.md]: ./architecture.md +[docs]: https://docs.helix-editor.com/ +[xtask]: https://github.com/matklad/cargo-xtask diff --git a/flake.lock b/flake.lock index 2c59f9931..acd10f766 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "devshell": { "locked": { - "lastModified": 1637575296, - "narHash": "sha256-ZY8YR5u8aglZPe27+AJMnPTG6645WuavB+w0xmhTarw=", + "lastModified": 1639692811, + "narHash": "sha256-wOOBH0fVsfNqw/5ZWRoKspyesoXBgiwEOUBH4c7JKEo=", "owner": "numtide", "repo": "devshell", - "rev": "0e56ef21ba1a717169953122c7415fa6a8cd2618", + "rev": "d3a1f5bec3632b33346865b1c165bf2420bb2f52", "type": "github" }, "original": { @@ -41,11 +41,11 @@ ] }, "locked": { - "lastModified": 1638425401, - "narHash": "sha256-xc8ayvR3u90hSCMEy0zHHKav7lEgljAFXL4oIkWRp3M=", + "lastModified": 1639807801, + "narHash": "sha256-y32tMq1LTRVbMW3QN5i98iOQjQt2QSsif3ayUkD1o3g=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "1f8b511bb30f7d7b9051dfbb4784390bc0d48d37", + "rev": "b5bbaa4f5239e6f0619846f9a5380f07baa853d3", "type": "github" }, "original": { @@ -56,11 +56,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1638376152, - "narHash": "sha256-ucgLpVqhFnClH7YRUHBHnmiOd82RZdFR3XJt36ks5fE=", + "lastModified": 1639699734, + "narHash": "sha256-tlX6WebGmiHb2Hmniff+ltYp+7dRfdsBxw9YczLsP60=", "owner": "nixos", "repo": "nixpkgs", - "rev": "6daa4a5c045d40e6eae60a3b6e427e8700f1c07f", + "rev": "03ec468b14067729a285c2c7cfa7b9434a04816c", "type": "github" }, "original": { @@ -99,11 +99,11 @@ "nixpkgs": "nixpkgs_2" }, "locked": { - "lastModified": 1638497756, - "narHash": "sha256-zKOvMKqGp71ZBnR+hBlPcv4TwNN82COW9EF+6ygrFs8=", + "lastModified": 1639880499, + "narHash": "sha256-/BibDmFwgWuuTUkNVO6YlvuTSWM9dpBvlZoTAPs7ORI=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "783722a22ee5d762ac5c1c7b418b57b3010c827a", + "rev": "c6c83589ae048af20d93d01eb07a4176012093d0", "type": "github" }, "original": { diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 0a2a56d9e..839b07ac1 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -23,7 +23,7 @@ unicode-width = "0.1" unicode-general-category = "0.4" # slab = "0.4.2" tree-sitter = "0.20" -once_cell = "1.8" +once_cell = "1.9" arc-swap = "1" regex = "1" diff --git a/helix-core/src/auto_pairs.rs b/helix-core/src/auto_pairs.rs index cc9668529..1b3de6ea0 100644 --- a/helix-core/src/auto_pairs.rs +++ b/helix-core/src/auto_pairs.rs @@ -1,7 +1,8 @@ //! When typing the opening character of one of the possible pairs defined below, //! this module provides the functionality to insert the paired closing character. -use crate::{Range, Rope, Selection, Tendril, Transaction}; +use crate::{movement::Direction, Range, Rope, Selection, Tendril, Transaction}; +use log::debug; use smallvec::SmallVec; // Heavily based on https://github.com/codemirror/closebrackets/ @@ -15,7 +16,9 @@ pub const PAIRS: &[(char, char)] = &[ ('`', '`'), ]; -const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines +// [TODO] build this dynamically in language config. see #992 +const OPEN_BEFORE: &str = "([{'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; +const CLOSE_BEFORE: &str = ")]}'\":;,> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{2029}"; // includes space and newlines // insert hook: // Fn(doc, selection, char) => Option @@ -25,14 +28,19 @@ const CLOSE_BEFORE: &str = ")]}'\":;> \n\r\u{000B}\u{000C}\u{0085}\u{2028}\u{202 // // to simplify, maybe return Option and just reimplement the default -// TODO: delete implementation where it erases the whole bracket (|) -> | +// [TODO] +// * delete implementation where it erases the whole bracket (|) -> | +// * change to multi character pairs to handle cases like placing the cursor in the +// middle of triple quotes, and more exotic pairs like Jinja's {% %} #[must_use] pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option { + debug!("autopairs hook selection: {:#?}", selection); + for &(open, close) in PAIRS { if open == ch { if open == close { - return handle_same(doc, selection, open); + return Some(handle_same(doc, selection, open, CLOSE_BEFORE, OPEN_BEFORE)); } else { return Some(handle_open(doc, selection, open, close, CLOSE_BEFORE)); } @@ -47,18 +55,44 @@ pub fn hook(doc: &Rope, selection: &Selection, ch: char) -> Option None } -// TODO: special handling for lifetimes in rust: if preceeded by & or < don't auto close ' -// for example "&'a mut", or "fn<'a>" - -fn next_char(doc: &Rope, pos: usize) -> Option { - if pos >= doc.len_chars() { +fn prev_char(doc: &Rope, pos: usize) -> Option { + if pos == 0 { return None; } - Some(doc.char(pos)) + + doc.get_char(pos - 1) +} + +/// calculate what the resulting range should be for an auto pair insertion +fn get_next_range( + start_range: &Range, + offset: usize, + typed_char: char, + len_inserted: usize, +) -> Range { + let end_head = start_range.head + offset + typed_char.len_utf8(); + + let end_anchor = match (start_range.len(), start_range.direction()) { + // if we have a zero width cursor, it shifts to the same number + (0, _) => end_head, + + // if we are inserting for a regular one-width cursor, the anchor + // moves with the head + (1, Direction::Forward) => end_head - 1, + (1, Direction::Backward) => end_head + 1, + + // if we are appending, the anchor stays where it is; only offset + // for multiple range insertions + (_, Direction::Forward) => start_range.anchor + offset, + + // when we are inserting in front of a selection, we need to move + // the anchor over by however many characters were inserted overall + (_, Direction::Backward) => start_range.anchor + offset + len_inserted, + }; + + Range::new(end_anchor, end_head) } -// TODO: selections should be extended if range, moved if point. -// TODO: if not cursor but selection, wrap on both sides of selection (surround) fn handle_open( doc: &Rope, selection: &Selection, @@ -66,98 +100,510 @@ fn handle_open( close: char, close_before: &str, ) -> Transaction { - let mut ranges = SmallVec::with_capacity(selection.len()); - + let mut end_ranges = SmallVec::with_capacity(selection.len()); let mut offs = 0; - let transaction = Transaction::change_by_selection(doc, selection, |range| { - let pos = range.head; - let next = next_char(doc, pos); - - let head = pos + offs + open.len_utf8(); - // if selection, retain anchor, if cursor, move over - ranges.push(Range::new( - if range.is_empty() { - head - } else { - range.anchor + offs - }, - head, - )); + let transaction = Transaction::change_by_selection(doc, selection, |start_range| { + let cursor = start_range.cursor(doc.slice(..)); + let next_char = doc.get_char(cursor); + let len_inserted; - match next { + let change = match next_char { Some(ch) if !close_before.contains(ch) => { - offs += 1; - // TODO: else return (use default handler that inserts open) - (pos, pos, Some(Tendril::from_char(open))) + len_inserted = open.len_utf8(); + (cursor, cursor, Some(Tendril::from_char(open))) } // None | Some(ch) if close_before.contains(ch) => {} _ => { // insert open & close - let mut pair = Tendril::with_capacity(2); - pair.push_char(open); - pair.push_char(close); + let pair = Tendril::from_iter([open, close]); + len_inserted = open.len_utf8() + close.len_utf8(); + (cursor, cursor, Some(pair)) + } + }; - offs += 2; + let next_range = get_next_range(start_range, offs, open, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; - (pos, pos, Some(pair)) - } - } + change }); - transaction.with_selection(Selection::new(ranges, selection.primary_index())) + let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); + debug!("auto pair transaction: {:#?}", t); + t } fn handle_close(doc: &Rope, selection: &Selection, _open: char, close: char) -> Transaction { - let mut ranges = SmallVec::with_capacity(selection.len()); + let mut end_ranges = SmallVec::with_capacity(selection.len()); let mut offs = 0; - let transaction = Transaction::change_by_selection(doc, selection, |range| { - let pos = range.head; - let next = next_char(doc, pos); + let transaction = Transaction::change_by_selection(doc, selection, |start_range| { + let cursor = start_range.cursor(doc.slice(..)); + let next_char = doc.get_char(cursor); + let mut len_inserted = 0; - let head = pos + offs + close.len_utf8(); - // if selection, retain anchor, if cursor, move over - ranges.push(Range::new( - if range.is_empty() { - head - } else { - range.anchor + offs - }, - head, - )); + let change = if next_char == Some(close) { + // return transaction that moves past close + (cursor, cursor, None) // no-op + } else { + len_inserted += close.len_utf8(); + (cursor, cursor, Some(Tendril::from_char(close))) + }; - if next == Some(close) { + let next_range = get_next_range(start_range, offs, close, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; + + change + }); + + let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); + debug!("auto pair transaction: {:#?}", t); + t +} + +/// handle cases where open and close is the same, or in triples ("""docstring""") +fn handle_same( + doc: &Rope, + selection: &Selection, + token: char, + close_before: &str, + open_before: &str, +) -> Transaction { + let mut end_ranges = SmallVec::with_capacity(selection.len()); + + let mut offs = 0; + + let transaction = Transaction::change_by_selection(doc, selection, |start_range| { + let cursor = start_range.cursor(doc.slice(..)); + let mut len_inserted = 0; + + let next_char = doc.get_char(cursor); + let prev_char = prev_char(doc, cursor); + + let change = if next_char == Some(token) { // return transaction that moves past close - (pos, pos, None) // no-op + (cursor, cursor, None) // no-op } else { - offs += close.len_utf8(); + let mut pair = Tendril::with_capacity(2 * token.len_utf8() as u32); + pair.push_char(token); - // TODO: else return (use default handler that inserts close) - (pos, pos, Some(Tendril::from_char(close))) - } + // for equal pairs, don't insert both open and close if either + // side has a non-pair char + if (next_char.is_none() || close_before.contains(next_char.unwrap())) + && (prev_char.is_none() || open_before.contains(prev_char.unwrap())) + { + pair.push_char(token); + } + + len_inserted += pair.len(); + (cursor, cursor, Some(pair)) + }; + + let next_range = get_next_range(start_range, offs, token, len_inserted); + end_ranges.push(next_range); + offs += len_inserted; + + change }); - transaction.with_selection(Selection::new(ranges, selection.primary_index())) + let t = transaction.with_selection(Selection::new(end_ranges, selection.primary_index())); + debug!("auto pair transaction: {:#?}", t); + t } -// handle cases where open and close is the same, or in triples ("""docstring""") -fn handle_same(_doc: &Rope, _selection: &Selection, _token: char) -> Option { - // if not cursor but selection, wrap - // let next = next char - - // if next == bracket { - // // if start of syntax node, insert token twice (new pair because node is complete) - // // elseif colsedBracketAt - // // is_triple == allow triple && next 3 is equal - // // cursor jump over - // } - //} else if allow_triple && followed by triple { - //} - //} else if next != word char && prev != bracket && prev != word char { - // // condition checks for cases like I' where you don't want I'' (or I'm) - // insert pair ("") - //} - None +#[cfg(test)] +mod test { + use super::*; + use smallvec::smallvec; + + fn differing_pairs() -> impl Iterator { + PAIRS.iter().filter(|(open, close)| open != close) + } + + fn matching_pairs() -> impl Iterator { + PAIRS.iter().filter(|(open, close)| open == close) + } + + fn test_hooks( + in_doc: &Rope, + in_sel: &Selection, + ch: char, + expected_doc: &Rope, + expected_sel: &Selection, + ) { + let trans = hook(&in_doc, &in_sel, ch).unwrap(); + let mut actual_doc = in_doc.clone(); + assert!(trans.apply(&mut actual_doc)); + assert_eq!(expected_doc, &actual_doc); + assert_eq!(expected_sel, trans.selection().unwrap()); + } + + fn test_hooks_with_pairs( + in_doc: &Rope, + in_sel: &Selection, + pairs: I, + get_expected_doc: F, + actual_sel: &Selection, + ) where + I: IntoIterator, + F: Fn(char, char) -> R, + R: Into, + Rope: From, + { + pairs.into_iter().for_each(|(open, close)| { + test_hooks( + in_doc, + in_sel, + *open, + &Rope::from(get_expected_doc(*open, *close)), + actual_sel, + ) + }); + } + + // [] indicates range + + /// [] -> insert ( -> ([]) + #[test] + fn test_insert_blank() { + test_hooks_with_pairs( + &Rope::new(), + &Selection::single(1, 0), + PAIRS, + |open, close| format!("{}{}", open, close), + &Selection::single(2, 1), + ); + } + + /// [] -> append ( -> ([]) + #[test] + fn test_append_blank() { + test_hooks_with_pairs( + // this is what happens when you have a totally blank document and then append + &Rope::from("\n\n"), + &Selection::single(0, 2), + PAIRS, + |open, close| format!("\n{}{}\n", open, close), + &Selection::single(0, 3), + ); + } + + /// [] ([]) + /// [] -> insert -> ([]) + /// [] ([]) + #[test] + fn test_insert_blank_multi_cursor() { + test_hooks_with_pairs( + &Rope::from("\n\n\n"), + &Selection::new( + smallvec!(Range::new(1, 0), Range::new(2, 1), Range::new(3, 2),), + 0, + ), + PAIRS, + |open, close| { + format!( + "{open}{close}\n{open}{close}\n{open}{close}\n", + open = open, + close = close + ) + }, + &Selection::new( + smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),), + 0, + ), + ); + } + + /// fo[o] -> append ( -> fo[o(]) + #[test] + fn test_append() { + test_hooks_with_pairs( + &Rope::from("foo\n"), + &Selection::single(2, 4), + differing_pairs(), + |open, close| format!("foo{}{}\n", open, close), + &Selection::single(2, 5), + ); + } + + /// fo[o] fo[o(]) + /// fo[o] -> append ( -> fo[o(]) + /// fo[o] fo[o(]) + #[test] + fn test_append_multi() { + test_hooks_with_pairs( + &Rope::from("foo\nfoo\nfoo\n"), + &Selection::new( + smallvec!(Range::new(2, 4), Range::new(6, 8), Range::new(10, 12)), + 0, + ), + differing_pairs(), + |open, close| { + format!( + "foo{open}{close}\nfoo{open}{close}\nfoo{open}{close}\n", + open = open, + close = close + ) + }, + &Selection::new( + smallvec!(Range::new(2, 5), Range::new(8, 11), Range::new(14, 17)), + 0, + ), + ); + } + + /// ([]) -> insert ) -> ()[] + #[test] + fn test_insert_close_inside_pair() { + for (open, close) in PAIRS { + let doc = Rope::from(format!("{}{}", open, close)); + + test_hooks( + &doc, + &Selection::single(2, 1), + *close, + &doc, + &Selection::single(3, 2), + ); + } + } + + /// [(]) -> append ) -> [()] + #[test] + fn test_append_close_inside_pair() { + for (open, close) in PAIRS { + let doc = Rope::from(format!("{}{}\n", open, close)); + + test_hooks( + &doc, + &Selection::single(0, 2), + *close, + &doc, + &Selection::single(0, 3), + ); + } + } + + /// ([]) ()[] + /// ([]) -> insert ) -> ()[] + /// ([]) ()[] + #[test] + fn test_insert_close_inside_pair_multi_cursor() { + let sel = Selection::new( + smallvec!(Range::new(2, 1), Range::new(5, 4), Range::new(8, 7),), + 0, + ); + + let expected_sel = Selection::new( + smallvec!(Range::new(3, 2), Range::new(6, 5), Range::new(9, 8),), + 0, + ); + + for (open, close) in PAIRS { + let doc = Rope::from(format!( + "{open}{close}\n{open}{close}\n{open}{close}\n", + open = open, + close = close + )); + + test_hooks(&doc, &sel, *close, &doc, &expected_sel); + } + } + + /// [(]) [()] + /// [(]) -> append ) -> [()] + /// [(]) [()] + #[test] + fn test_append_close_inside_pair_multi_cursor() { + let sel = Selection::new( + smallvec!(Range::new(0, 2), Range::new(3, 5), Range::new(6, 8),), + 0, + ); + + let expected_sel = Selection::new( + smallvec!(Range::new(0, 3), Range::new(3, 6), Range::new(6, 9),), + 0, + ); + + for (open, close) in PAIRS { + let doc = Rope::from(format!( + "{open}{close}\n{open}{close}\n{open}{close}\n", + open = open, + close = close + )); + + test_hooks(&doc, &sel, *close, &doc, &expected_sel); + } + } + + /// ([]) -> insert ( -> (([])) + #[test] + fn test_insert_open_inside_pair() { + let sel = Selection::single(2, 1); + let expected_sel = Selection::single(3, 2); + + for (open, close) in differing_pairs() { + let doc = Rope::from(format!("{}{}", open, close)); + let expected_doc = Rope::from(format!( + "{open}{open}{close}{close}", + open = open, + close = close + )); + + test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel); + } + } + + /// [word(]) -> append ( -> [word((])) + #[test] + fn test_append_open_inside_pair() { + let sel = Selection::single(0, 6); + let expected_sel = Selection::single(0, 7); + + for (open, close) in differing_pairs() { + let doc = Rope::from(format!("word{}{}", open, close)); + let expected_doc = Rope::from(format!( + "word{open}{open}{close}{close}", + open = open, + close = close + )); + + test_hooks(&doc, &sel, *open, &expected_doc, &expected_sel); + } + } + + /// ([]) -> insert " -> ("[]") + #[test] + fn test_insert_nested_open_inside_pair() { + let sel = Selection::single(2, 1); + let expected_sel = Selection::single(3, 2); + + for (outer_open, outer_close) in differing_pairs() { + let doc = Rope::from(format!("{}{}", outer_open, outer_close,)); + + for (inner_open, inner_close) in matching_pairs() { + let expected_doc = Rope::from(format!( + "{}{}{}{}", + outer_open, inner_open, inner_close, outer_close + )); + + test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel); + } + } + } + + /// [(]) -> append " -> [("]") + #[test] + fn test_append_nested_open_inside_pair() { + let sel = Selection::single(0, 2); + let expected_sel = Selection::single(0, 3); + + for (outer_open, outer_close) in differing_pairs() { + let doc = Rope::from(format!("{}{}", outer_open, outer_close,)); + + for (inner_open, inner_close) in matching_pairs() { + let expected_doc = Rope::from(format!( + "{}{}{}{}", + outer_open, inner_open, inner_close, outer_close + )); + + test_hooks(&doc, &sel, *inner_open, &expected_doc, &expected_sel); + } + } + } + + /// []word -> insert ( -> ([]word + #[test] + fn test_insert_open_before_non_pair() { + test_hooks_with_pairs( + &Rope::from("word"), + &Selection::single(1, 0), + PAIRS, + |open, _| format!("{}word", open), + &Selection::single(2, 1), + ) + } + + /// [wor]d -> insert ( -> ([wor]d + #[test] + fn test_insert_open_with_selection() { + test_hooks_with_pairs( + &Rope::from("word"), + &Selection::single(3, 0), + PAIRS, + |open, _| format!("{}word", open), + &Selection::single(4, 1), + ) + } + + /// [wor]d -> append ) -> [wor)]d + #[test] + fn test_append_close_inside_non_pair_with_selection() { + let sel = Selection::single(0, 4); + let expected_sel = Selection::single(0, 5); + + for (_, close) in PAIRS { + let doc = Rope::from("word"); + let expected_doc = Rope::from(format!("wor{}d", close)); + test_hooks(&doc, &sel, *close, &expected_doc, &expected_sel); + } + } + + /// foo[ wor]d -> insert ( -> foo([) wor]d + #[test] + fn test_insert_open_trailing_word_with_selection() { + test_hooks_with_pairs( + &Rope::from("foo word"), + &Selection::single(7, 3), + differing_pairs(), + |open, close| format!("foo{}{} word", open, close), + &Selection::single(9, 4), + ) + } + + /// we want pairs that are *not* the same char to be inserted after + /// a non-pair char, for cases like functions, but for pairs that are + /// the same char, we want to *not* insert a pair to handle cases like "I'm" + /// + /// word[] -> insert ( -> word([]) + /// word[] -> insert ' -> word'[] + #[test] + fn test_insert_open_after_non_pair() { + let doc = Rope::from("word"); + let sel = Selection::single(5, 4); + let expected_sel = Selection::single(6, 5); + + test_hooks_with_pairs( + &doc, + &sel, + differing_pairs(), + |open, close| format!("word{}{}", open, close), + &expected_sel, + ); + + test_hooks_with_pairs( + &doc, + &sel, + matching_pairs(), + |open, _| format!("word{}", open), + &expected_sel, + ); + } + + /// appending with only a cursor should stay a cursor + /// + /// [] -> append to end "foo -> "foo[]" + #[test] + fn test_append_single_cursor() { + test_hooks_with_pairs( + &Rope::from("\n"), + &Selection::single(0, 1), + PAIRS, + |open, close| format!("{}{}\n", open, close), + &Selection::single(1, 2), + ); + } } diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index 4ae044cce..92a59f31e 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -17,6 +17,7 @@ mod position; pub mod register; pub mod search; pub mod selection; +pub mod shellwords; mod state; pub mod surround; pub mod syntax; diff --git a/helix-core/src/match_brackets.rs b/helix-core/src/match_brackets.rs index cd554005a..0189deddb 100644 --- a/helix-core/src/match_brackets.rs +++ b/helix-core/src/match_brackets.rs @@ -11,7 +11,7 @@ const PAIRS: &[(char, char)] = &[ ('\"', '\"'), ]; -// limit matching pairs to only ( ) { } [ ] < > +// limit matching pairs to only ( ) { } [ ] < > ' ' " " // Returns the position of the matching bracket under cursor. // diff --git a/helix-core/src/movement.rs b/helix-core/src/movement.rs index 01a8f890e..47fe68272 100644 --- a/helix-core/src/movement.rs +++ b/helix-core/src/movement.rs @@ -307,8 +307,6 @@ fn reached_target(target: WordMotionTarget, prev_ch: char, next_ch: char) -> boo #[cfg(test)] mod test { - use std::array::{self, IntoIter}; - use ropey::Rope; use super::*; @@ -360,7 +358,7 @@ mod test { ((Direction::Backward, 999usize), (0, 0)), // |This is a simple alphabetic line ]; - for ((direction, amount), coordinates) in IntoIter::new(moves_and_expected_coordinates) { + for ((direction, amount), coordinates) in moves_and_expected_coordinates { range = move_horizontally(slice, range, direction, amount, Movement::Move); assert_eq!(coords_at_pos(slice, range.head), coordinates.into()) } @@ -374,7 +372,7 @@ mod test { let mut range = Range::point(position); - let moves_and_expected_coordinates = IntoIter::new([ + let moves_and_expected_coordinates = [ ((Direction::Forward, 11usize), (1, 1)), // Multiline\nt|ext sample\n... ((Direction::Backward, 1usize), (1, 0)), // Multiline\n|text sample\n... ((Direction::Backward, 5usize), (0, 5)), // Multi|line\ntext sample\n... @@ -384,7 +382,7 @@ mod test { ((Direction::Backward, 0usize), (0, 3)), // Mul|tiline\ntext sample\n... ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n| ((Direction::Forward, 999usize), (5, 0)), // ...and whitespaced\n| - ]); + ]; for ((direction, amount), coordinates) in moves_and_expected_coordinates { range = move_horizontally(slice, range, direction, amount, Movement::Move); @@ -402,11 +400,11 @@ mod test { let mut range = Range::point(position); let original_anchor = range.anchor; - let moves = IntoIter::new([ + let moves = [ (Direction::Forward, 1usize), (Direction::Forward, 5usize), (Direction::Backward, 3usize), - ]); + ]; for (direction, amount) in moves { range = move_horizontally(slice, range, direction, amount, Movement::Extend); @@ -420,7 +418,7 @@ mod test { let slice = text.slice(..); let position = pos_at_coords(slice, (0, 0).into(), true); let mut range = Range::point(position); - let moves_and_expected_coordinates = IntoIter::new([ + let moves_and_expected_coordinates = [ ((Direction::Forward, 1usize), (1, 0)), ((Direction::Forward, 2usize), (3, 0)), ((Direction::Forward, 1usize), (4, 0)), @@ -430,7 +428,7 @@ mod test { ((Direction::Backward, 0usize), (4, 0)), ((Direction::Forward, 5), (5, 0)), ((Direction::Forward, 999usize), (5, 0)), - ]); + ]; for ((direction, amount), coordinates) in moves_and_expected_coordinates { range = move_vertically(slice, range, direction, amount, Movement::Move); @@ -450,7 +448,7 @@ mod test { H, V, } - let moves_and_expected_coordinates = IntoIter::new([ + let moves_and_expected_coordinates = [ // Places cursor at the end of line ((Axis::H, Direction::Forward, 8usize), (0, 8)), // First descent preserves column as the target line is wider @@ -463,7 +461,7 @@ mod test { ((Axis::V, Direction::Backward, 999usize), (0, 8)), ((Axis::V, Direction::Forward, 4usize), (4, 8)), ((Axis::V, Direction::Forward, 999usize), (5, 0)), - ]); + ]; for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates { range = match axis { @@ -489,7 +487,7 @@ mod test { H, V, } - let moves_and_expected_coordinates = IntoIter::new([ + let moves_and_expected_coordinates = [ // Places cursor at the fourth kana. ((Axis::H, Direction::Forward, 4), (0, 4)), // Descent places cursor at the 4th character. @@ -498,7 +496,7 @@ mod test { ((Axis::H, Direction::Backward, 1usize), (1, 3)), // Jumping back up 1 line. ((Axis::V, Direction::Backward, 1usize), (0, 3)), - ]); + ]; for ((axis, direction, amount), coordinates) in moves_and_expected_coordinates { range = match axis { @@ -530,7 +528,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_start_of_next_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic forward motion stops at the first space", vec![(1, Range::new(0, 0), Range::new(0, 6))]), (" Starting from a boundary advances the anchor", @@ -604,7 +602,7 @@ mod test { vec![ (1, Range::new(0, 0), Range::new(0, 6)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -616,7 +614,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_start_of_next_long_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic forward motion stops at the first space", vec![(1, Range::new(0, 0), Range::new(0, 6))]), (" Starting from a boundary advances the anchor", @@ -688,7 +686,7 @@ mod test { vec![ (1, Range::new(0, 0), Range::new(0, 8)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -700,7 +698,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_start_of_previous_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic backward motion from the middle of a word", vec![(1, Range::new(3, 3), Range::new(4, 0))]), @@ -773,7 +771,7 @@ mod test { vec![ (1, Range::new(0, 6), Range::new(6, 0)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -785,7 +783,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_start_of_previous_long_words() { - let tests = array::IntoIter::new([ + let tests = [ ( "Basic backward motion from the middle of a word", vec![(1, Range::new(3, 3), Range::new(4, 0))], @@ -870,7 +868,7 @@ mod test { vec![ (1, Range::new(0, 8), Range::new(8, 0)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -882,7 +880,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_end_of_next_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic forward motion from the start of a word to the end of it", vec![(1, Range::new(0, 0), Range::new(0, 5))]), ("Basic forward motion from the end of a word to the end of the next", @@ -954,7 +952,7 @@ mod test { vec![ (1, Range::new(0, 0), Range::new(0, 5)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -966,7 +964,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_end_of_previous_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic backward motion from the middle of a word", vec![(1, Range::new(9, 9), Range::new(10, 5))]), ("Starting from after boundary retreats the anchor", @@ -1036,7 +1034,7 @@ mod test { vec![ (1, Range::new(0, 10), Range::new(10, 4)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { @@ -1048,7 +1046,7 @@ mod test { #[test] fn test_behaviour_when_moving_to_end_of_next_long_words() { - let tests = array::IntoIter::new([ + let tests = [ ("Basic forward motion from the start of a word to the end of it", vec![(1, Range::new(0, 0), Range::new(0, 5))]), ("Basic forward motion from the end of a word to the end of the next", @@ -1118,7 +1116,7 @@ mod test { vec![ (1, Range::new(0, 0), Range::new(0, 7)), ]), - ]); + ]; for (sample, scenario) in tests { for (count, begin, expected_end) in scenario.into_iter() { diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 116a1c7c0..884c98acf 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -7,6 +7,7 @@ use crate::{ ensure_grapheme_boundary_next, ensure_grapheme_boundary_prev, next_grapheme_boundary, prev_grapheme_boundary, }, + movement::Direction, Assoc, ChangeSet, RopeSlice, }; use smallvec::{smallvec, SmallVec}; @@ -82,6 +83,13 @@ impl Range { std::cmp::max(self.anchor, self.head) } + /// Total length of the range. + #[inline] + #[must_use] + pub fn len(&self) -> usize { + self.to() - self.from() + } + /// The (inclusive) range of lines that the range overlaps. #[inline] #[must_use] @@ -102,6 +110,18 @@ impl Range { self.anchor == self.head } + /// `Direction::Backward` when head < anchor. + /// `Direction::Backward` otherwise. + #[inline] + #[must_use] + pub fn direction(&self) -> Direction { + if self.head < self.anchor { + Direction::Backward + } else { + Direction::Forward + } + } + /// Check two ranges for overlap. #[must_use] pub fn overlaps(&self, other: &Self) -> bool { diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs new file mode 100644 index 000000000..13f6f3e99 --- /dev/null +++ b/helix-core/src/shellwords.rs @@ -0,0 +1,164 @@ +use std::borrow::Cow; + +/// Get the vec of escaped / quoted / doublequoted filenames from the input str +pub fn shellwords(input: &str) -> Vec> { + enum State { + Normal, + NormalEscaped, + Quoted, + QuoteEscaped, + Dquoted, + DquoteEscaped, + } + + use State::*; + + let mut state = Normal; + let mut args: Vec> = Vec::new(); + let mut escaped = String::with_capacity(input.len()); + + let mut start = 0; + let mut end = 0; + + for (i, c) in input.char_indices() { + state = match state { + Normal => match c { + '\\' => { + escaped.push_str(&input[start..i]); + start = i + 1; + NormalEscaped + } + '"' => { + end = i; + Dquoted + } + '\'' => { + end = i; + Quoted + } + c if c.is_ascii_whitespace() => { + end = i; + Normal + } + _ => Normal, + }, + NormalEscaped => Normal, + Quoted => match c { + '\\' => { + escaped.push_str(&input[start..i]); + start = i + 1; + QuoteEscaped + } + '\'' => { + end = i; + Normal + } + _ => Quoted, + }, + QuoteEscaped => Quoted, + Dquoted => match c { + '\\' => { + escaped.push_str(&input[start..i]); + start = i + 1; + DquoteEscaped + } + '"' => { + end = i; + Normal + } + _ => Dquoted, + }, + DquoteEscaped => Dquoted, + }; + + if i >= input.len() - 1 && end == 0 { + end = i + 1; + } + + if end > 0 { + let esc_trim = escaped.trim(); + let inp = &input[start..end]; + + if !(esc_trim.is_empty() && inp.trim().is_empty()) { + if esc_trim.is_empty() { + args.push(inp.into()); + } else { + args.push([escaped, inp.into()].concat().into()); + escaped = "".to_string(); + } + } + start = i + 1; + end = 0; + } + } + args +} + +#[cfg(test)] +mod test { + use super::*; + + #[test] + fn test_normal() { + let input = r#":o single_word twó wörds \three\ \"with\ escaping\\"#; + let result = shellwords(input); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó"), + Cow::from("wörds"), + Cow::from(r#"three "with escaping\"#), + ]; + // TODO test is_owned and is_borrowed, once they get stabilized. + assert_eq!(expected, result); + } + + #[test] + fn test_quoted() { + let quoted = + r#":o 'single_word' 'twó wörds' '' ' ''\three\' \"with\ escaping\\' 'quote incomplete"#; + let result = shellwords(quoted); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó wörds"), + Cow::from(r#"three' "with escaping\"#), + Cow::from("quote incomplete"), + ]; + assert_eq!(expected, result); + } + + #[test] + fn test_dquoted() { + let dquoted = r#":o "single_word" "twó wörds" "" " ""\three\' \"with\ escaping\\" "dquote incomplete"#; + let result = shellwords(dquoted); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó wörds"), + Cow::from(r#"three' "with escaping\"#), + Cow::from("dquote incomplete"), + ]; + assert_eq!(expected, result); + } + + #[test] + fn test_mixed() { + let dquoted = r#":o single_word 'twó wörds' "\three\' \"with\ escaping\\""no space before"'and after' $#%^@ "%^&(%^" ')(*&^%''a\\\\\b' '"#; + let result = shellwords(dquoted); + let expected = vec![ + Cow::from(":o"), + Cow::from("single_word"), + Cow::from("twó wörds"), + Cow::from("three' \"with escaping\\"), + Cow::from("no space before"), + Cow::from("and after"), + Cow::from("$#%^@"), + Cow::from("%^&(%^"), + Cow::from(")(*&^%"), + Cow::from(r#"a\\b"#), + //last ' just changes to quoted but since we dont have anything after it, it should be ignored + ]; + assert_eq!(expected, result); + } +} diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index ba78adaaa..ef35fc756 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -50,7 +50,7 @@ pub struct Configuration { #[serde(rename_all = "kebab-case", deny_unknown_fields)] pub struct LanguageConfiguration { #[serde(rename = "name")] - pub language_id: String, + pub language_id: String, // c-sharp, rust pub scope: String, // source.rust pub file_types: Vec, // filename ends_with? #[serde(default)] diff --git a/helix-core/src/transaction.rs b/helix-core/src/transaction.rs index b62f4a9bd..d8d389f3b 100644 --- a/helix-core/src/transaction.rs +++ b/helix-core/src/transaction.rs @@ -409,7 +409,7 @@ impl ChangeSet { /// Transaction represents a single undoable unit of changes. Several changes can be grouped into /// a single transaction. -#[derive(Debug, Default, Clone)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Transaction { changes: ChangeSet, selection: Option, diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 5e4619ef8..b7fe94ab6 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -23,5 +23,5 @@ lsp-types = { version = "0.91", features = ["proposed"] } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.14", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } +tokio = { version = "1.15", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tokio-stream = "0.1.8" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 271fd9d59..f1de87524 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -202,7 +202,7 @@ impl Client { Ok(result) => Output::Success(Success { jsonrpc: Some(Version::V2), id, - result, + result: serde_json::to_value(result)?, }), Err(error) => Output::Failure(Failure { jsonrpc: Some(Version::V2), @@ -800,4 +800,16 @@ impl Client { let response = self.request::(params).await?; Ok(response.unwrap_or_default()) } + + pub fn command(&self, command: lsp::Command) -> impl Future> { + let params = lsp::ExecuteCommandParams { + command: command.command, + arguments: command.arguments.unwrap_or_default(), + work_done_progress_params: lsp::WorkDoneProgressParams { + work_done_token: None, + }, + }; + + self.call::(params) + } } diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 15cae582b..8fb321bcc 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -203,6 +203,7 @@ pub mod util { #[derive(Debug, PartialEq, Clone)] pub enum MethodCall { WorkDoneProgressCreate(lsp::WorkDoneProgressCreateParams), + ApplyWorkspaceEdit(lsp::ApplyWorkspaceEditParams), } impl MethodCall { @@ -215,6 +216,12 @@ impl MethodCall { .expect("Failed to parse WorkDoneCreate params"); Self::WorkDoneProgressCreate(params) } + lsp::request::ApplyWorkspaceEdit::METHOD => { + let params: lsp::ApplyWorkspaceEditParams = params + .parse() + .expect("Failed to parse ApplyWorkspaceEdit params"); + Self::ApplyWorkspaceEdit(params) + } _ => { log::warn!("unhandled lsp request: {}", method); return None; diff --git a/helix-syntax/README.md b/helix-syntax/README.md new file mode 100644 index 000000000..bba2197a3 --- /dev/null +++ b/helix-syntax/README.md @@ -0,0 +1,13 @@ +helix-syntax +============ + +Syntax highlighting for helix, (shallow) submodules resides here. + +Differences from nvim-treesitter +-------------------------------- + +As the syntax are commonly ported from +. + +Note that we do not support the custom `#any-of` predicate which is +supported by neovim so one needs to change it to `#match` with regex. diff --git a/helix-syntax/languages/tree-sitter-comment b/helix-syntax/languages/tree-sitter-comment new file mode 160000 index 000000000..5dd3c62f1 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-comment @@ -0,0 +1 @@ +Subproject commit 5dd3c62f1bbe378b220fe16b317b85247898639e diff --git a/helix-syntax/languages/tree-sitter-dart b/helix-syntax/languages/tree-sitter-dart new file mode 160000 index 000000000..6a2537668 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-dart @@ -0,0 +1 @@ +Subproject commit 6a25376685d1d47968c2cef06d4db8d84a70025e diff --git a/helix-syntax/languages/tree-sitter-dockerfile b/helix-syntax/languages/tree-sitter-dockerfile new file mode 160000 index 000000000..7af32bc04 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-dockerfile @@ -0,0 +1 @@ +Subproject commit 7af32bc04a66ab196f5b9f92ac471f29372ae2ce diff --git a/helix-syntax/languages/tree-sitter-fish b/helix-syntax/languages/tree-sitter-fish new file mode 160000 index 000000000..04e54ab65 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-fish @@ -0,0 +1 @@ +Subproject commit 04e54ab6585dfd4fee6ddfe5849af56f101b6d4f diff --git a/helix-syntax/languages/tree-sitter-llvm b/helix-syntax/languages/tree-sitter-llvm new file mode 160000 index 000000000..d4f61bed8 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-llvm @@ -0,0 +1 @@ +Subproject commit d4f61bed8ecb632addcd5e088c4f4cb9c1bf1c5b diff --git a/helix-syntax/languages/tree-sitter-markdown b/helix-syntax/languages/tree-sitter-markdown new file mode 160000 index 000000000..ad8c32917 --- /dev/null +++ b/helix-syntax/languages/tree-sitter-markdown @@ -0,0 +1 @@ +Subproject commit ad8c32917a16dfbb387d1da567bf0c3fb6fffde2 diff --git a/helix-syntax/languages/tree-sitter-scala b/helix-syntax/languages/tree-sitter-scala index fb23ed9a9..0a3dd53a7 160000 --- a/helix-syntax/languages/tree-sitter-scala +++ b/helix-syntax/languages/tree-sitter-scala @@ -1 +1 @@ -Subproject commit fb23ed9a99da012d86b7a5059b9d8928607cce29 +Subproject commit 0a3dd53a7fc4b352a538397d054380aaa28be54c diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index a0079febe..58c713cb5 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -9,6 +9,7 @@ categories = ["editor", "command-line-utilities"] repository = "https://github.com/helix-editor/helix" homepage = "https://helix-editor.com" include = ["src/**/*", "README.md"] +default-run = "hx" [package.metadata.nix] build = true @@ -26,7 +27,7 @@ helix-view = { version = "0.5", path = "../helix-view" } helix-lsp = { version = "0.5", path = "../helix-lsp" } anyhow = "1" -once_cell = "1.8" +once_cell = "1.9" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } num_cpus = "1" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 90330751a..9a46a7fe6 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -1,8 +1,12 @@ use helix_core::{merge_toml_values, syntax}; use helix_lsp::{lsp, util::lsp_pos_to_pos, LspProgressMap}; use helix_view::{theme, Editor}; +use serde_json::json; -use crate::{args::Args, compositor::Compositor, config::Config, job::Jobs, ui}; +use crate::{ + args::Args, commands::apply_workspace_edit, compositor::Compositor, config::Config, job::Jobs, + ui, +}; use log::{error, warn}; @@ -76,17 +80,27 @@ impl Application { None => Ok(def_lang_conf), }; - let theme = if let Some(theme) = &config.theme { - match theme_loader.load(theme) { - Ok(theme) => theme, - Err(e) => { - log::warn!("failed to load theme `{}` - {}", theme, e); + let true_color = config.editor.true_color || crate::true_color(); + let theme = config + .theme + .as_ref() + .and_then(|theme| { + theme_loader + .load(theme) + .map_err(|e| { + log::warn!("failed to load theme `{}` - {}", theme, e); + e + }) + .ok() + .filter(|theme| (true_color || theme.is_16_color())) + }) + .unwrap_or_else(|| { + if true_color { theme_loader.default() + } else { + theme_loader.base16_default() } - } - } else { - theme_loader.default() - }; + }); let syn_loader_conf: helix_core::syntax::Configuration = lang_conf .and_then(|conf| conf.try_into()) @@ -520,14 +534,6 @@ impl Application { Call::MethodCall(helix_lsp::jsonrpc::MethodCall { method, params, id, .. }) => { - let language_server = match self.editor.language_servers.get_by_id(server_id) { - Some(language_server) => language_server, - None => { - warn!("can't find language server with id `{}`", server_id); - return; - } - }; - let call = match MethodCall::parse(&method, params) { Some(call) => call, None => { @@ -557,8 +563,42 @@ impl Application { if spinner.is_stopped() { spinner.start(); } + let language_server = + match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + tokio::spawn(language_server.reply(id, Ok(serde_json::Value::Null))); } + MethodCall::ApplyWorkspaceEdit(params) => { + apply_workspace_edit( + &mut self.editor, + helix_lsp::OffsetEncoding::Utf8, + ¶ms.edit, + ); + + let language_server = + match self.editor.language_servers.get_by_id(server_id) { + Some(language_server) => language_server, + None => { + warn!("can't find language server with id `{}`", server_id); + return; + } + }; + + tokio::spawn(language_server.reply( + id, + Ok(json!(lsp::ApplyWorkspaceEditResponse { + applied: true, + failure_reason: None, + failed_change: None, + })), + )); + } } } e => unreachable!("{:?}", e), diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 57dd9716b..dae01a007 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -10,7 +10,7 @@ use helix_core::{ movement::{self, Direction}, object, pos_at_coords, regex::{self, Regex, RegexBuilder}, - search, selection, surround, textobject, + search, selection, shellwords, surround, textobject, unicode::width::UnicodeWidthChar, LineEnding, Position, Range, Rope, RopeGraphemes, RopeSlice, Selection, SmallVec, Tendril, Transaction, @@ -70,7 +70,7 @@ pub struct Context<'a> { impl<'a> Context<'a> { /// Push a new component onto the compositor. pub fn push_layer(&mut self, component: Box) { - self.callback = Some(Box::new(|compositor: &mut Compositor| { + self.callback = Some(Box::new(|compositor: &mut Compositor, _| { compositor.push(component) })); } @@ -173,14 +173,14 @@ impl MappableCommand { pub fn execute(&self, cx: &mut Context) { match &self { MappableCommand::Typable { name, args, doc: _ } => { - let args: Vec<&str> = args.iter().map(|arg| arg.as_str()).collect(); + let args: Vec> = args.iter().map(Cow::from).collect(); if let Some(command) = cmd::TYPABLE_COMMAND_MAP.get(name.as_str()) { let mut cx = compositor::Context { editor: cx.editor, jobs: cx.jobs, scroll: None, }; - if let Err(e) = (command.fun)(&mut cx, &args, PromptEvent::Validate) { + if let Err(e) = (command.fun)(&mut cx, &args[..], PromptEvent::Validate) { cx.editor.set_error(format!("{}", e)); } } @@ -287,11 +287,11 @@ impl MappableCommand { add_newline_below, "Add newline below", goto_type_definition, "Goto type definition", goto_implementation, "Goto implementation", - goto_file_start, "Goto file start/line", + goto_file_start, "Goto line number else file start", goto_file_end, "Goto file end", - goto_file, "Goto files in the selection", - goto_file_hsplit, "Goto files in the selection in horizontal splits", - goto_file_vsplit, "Goto files in the selection in vertical splits", + goto_file, "Goto files in selection", + goto_file_hsplit, "Goto files in selection (hsplit)", + goto_file_vsplit, "Goto files in selection (vsplit)", goto_reference, "Goto references", goto_window_top, "Goto window top", goto_window_center, "Goto window center", @@ -362,6 +362,7 @@ impl MappableCommand { expand_selection, "Expand selection to parent syntax node", jump_forward, "Jump forward on jumplist", jump_backward, "Jump backward on jumplist", + save_selection, "Save the current selection to the 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", @@ -394,6 +395,8 @@ impl MappableCommand { rename_symbol, "Rename symbol", increment, "Increment", decrement, "Decrement", + record_macro, "Record macro", + play_macro, "Play macro", ); } @@ -670,8 +673,15 @@ fn kill_to_line_end(cx: &mut Context) { let selection = doc.selection(view.id).clone().transform(|range| { let line = range.cursor_line(text); - let pos = line_end_char_index(&text, line); - range.put_cursor(text, pos, true) + let line_end_pos = line_end_char_index(&text, line); + let pos = range.cursor(text); + + let mut new_range = range.put_cursor(text, line_end_pos, true); + // don't want to remove the line separator itself if the cursor doesn't reach the end of line. + if pos != line_end_pos { + new_range.head = line_end_pos; + } + new_range }); delete_selection_insert_mode(doc, view, &selection); } @@ -1928,7 +1938,7 @@ fn append_mode(cx: &mut Context) { if !last_range.is_empty() && last_range.head == end { let transaction = Transaction::change( doc.text(), - std::array::IntoIter::new([(end, end, Some(doc.line_ending.as_str().into()))]), + [(end, end, Some(doc.line_ending.as_str().into()))].into_iter(), ); doc.apply(&transaction, view.id); } @@ -1942,7 +1952,7 @@ fn append_mode(cx: &mut Context) { doc.set_selection(view.id, selection); } -mod cmd { +pub mod cmd { use super::*; use std::collections::HashMap; @@ -1955,13 +1965,13 @@ mod cmd { pub aliases: &'static [&'static str], pub doc: &'static str, // params, flags, helper, completer - pub fun: fn(&mut compositor::Context, &[&str], PromptEvent) -> anyhow::Result<()>, + pub fun: fn(&mut compositor::Context, &[Cow], PromptEvent) -> anyhow::Result<()>, pub completer: Option, } fn quit( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { // last view and we have unsaved changes @@ -1976,7 +1986,7 @@ mod cmd { fn force_quit( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor.close(view!(cx.editor).id); @@ -1986,17 +1996,19 @@ mod cmd { fn open( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let path = args.get(0).context("wrong argument count")?; - let _ = cx.editor.open(path.into(), Action::Replace)?; + ensure!(!args.is_empty(), "wrong argument count"); + for arg in args { + let _ = cx.editor.open(arg.as_ref().into(), Action::Replace)?; + } Ok(()) } fn buffer_close( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let view = view!(cx.editor); @@ -2007,7 +2019,7 @@ mod cmd { fn force_buffer_close( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let view = view!(cx.editor); @@ -2016,15 +2028,12 @@ mod cmd { Ok(()) } - fn write_impl>( - cx: &mut compositor::Context, - path: Option

, - ) -> anyhow::Result<()> { + fn write_impl(cx: &mut compositor::Context, path: Option<&Cow>) -> anyhow::Result<()> { let jobs = &mut cx.jobs; let (_, doc) = current!(cx.editor); if let Some(ref path) = path { - doc.set_path(Some(path.as_ref())) + doc.set_path(Some(path.as_ref().as_ref())) .context("invalid filepath")?; } if doc.path().is_none() { @@ -2053,7 +2062,7 @@ mod cmd { fn write( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first()) @@ -2061,7 +2070,7 @@ mod cmd { fn new_file( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor.new_file(Action::Replace); @@ -2071,7 +2080,7 @@ mod cmd { fn format( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); @@ -2086,7 +2095,7 @@ mod cmd { } fn set_indent_style( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { use IndentStyle::*; @@ -2106,7 +2115,7 @@ mod cmd { // Attempt to parse argument as an indent style. let style = match args.get(0) { Some(arg) if "tabs".starts_with(&arg.to_lowercase()) => Some(Tabs), - Some(&"0") => Some(Tabs), + Some(Cow::Borrowed("0")) => Some(Tabs), Some(arg) => arg .parse::() .ok() @@ -2125,7 +2134,7 @@ mod cmd { /// Sets or reports the current document's line ending setting. fn set_line_ending( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { use LineEnding::*; @@ -2169,7 +2178,7 @@ mod cmd { fn earlier( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; @@ -2185,7 +2194,7 @@ mod cmd { fn later( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let uk = args.join(" ").parse::().map_err(|s| anyhow!(s))?; @@ -2200,7 +2209,7 @@ mod cmd { fn write_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first())?; @@ -2209,7 +2218,7 @@ mod cmd { fn force_write_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_impl(cx, args.first())?; @@ -2240,7 +2249,7 @@ mod cmd { fn write_all_impl( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, quit: bool, force: bool, @@ -2276,7 +2285,7 @@ mod cmd { fn write_all( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, false, false) @@ -2284,7 +2293,7 @@ mod cmd { fn write_all_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, true, false) @@ -2292,7 +2301,7 @@ mod cmd { fn force_write_all_quit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { write_all_impl(cx, args, event, true, true) @@ -2300,7 +2309,7 @@ mod cmd { fn quit_all_impl( editor: &mut Editor, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, force: bool, ) -> anyhow::Result<()> { @@ -2319,7 +2328,7 @@ mod cmd { fn quit_all( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { quit_all_impl(cx.editor, args, event, false) @@ -2327,7 +2336,7 @@ mod cmd { fn force_quit_all( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { quit_all_impl(cx.editor, args, event, true) @@ -2335,7 +2344,7 @@ mod cmd { fn cquit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let exit_code = args @@ -2354,16 +2363,26 @@ mod cmd { fn theme( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - let theme = args.first().context("theme not provided")?; - cx.editor.set_theme_from_name(theme) + let theme = args.first().context("Theme not provided")?; + let theme = cx + .editor + .theme_loader + .load(theme) + .with_context(|| format!("Failed setting theme {}", theme))?; + let true_color = cx.editor.config.true_color || crate::true_color(); + if !(true_color || theme.is_16_color()) { + bail!("Unsupported theme: theme requires true color support"); + } + cx.editor.set_theme(theme); + Ok(()) } fn yank_main_selection_to_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Clipboard) @@ -2371,20 +2390,18 @@ mod cmd { fn yank_joined_to_clipboard( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); - let separator = args - .first() - .copied() - .unwrap_or_else(|| doc.line_ending.as_str()); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Clipboard) } fn yank_main_selection_to_primary_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { yank_main_selection_to_clipboard_impl(cx.editor, ClipboardType::Selection) @@ -2392,47 +2409,45 @@ mod cmd { fn yank_joined_to_primary_clipboard( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); - let separator = args - .first() - .copied() - .unwrap_or_else(|| doc.line_ending.as_str()); + let default_sep = Cow::Borrowed(doc.line_ending.as_str()); + let separator = args.first().unwrap_or(&default_sep); yank_joined_to_clipboard_impl(cx.editor, separator, ClipboardType::Selection) } fn paste_clipboard_after( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard) + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1) } fn paste_clipboard_before( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard) + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard, 1) } fn paste_primary_clipboard_after( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection) + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1) } fn paste_primary_clipboard_before( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { - paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection) + paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection, 1) } fn replace_selections_with_clipboard_impl( @@ -2459,7 +2474,7 @@ mod cmd { fn replace_selections_with_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard) @@ -2467,7 +2482,7 @@ mod cmd { fn replace_selections_with_primary_clipboard( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { replace_selections_with_clipboard_impl(cx, ClipboardType::Selection) @@ -2475,7 +2490,7 @@ mod cmd { fn show_clipboard_provider( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { cx.editor @@ -2485,12 +2500,13 @@ mod cmd { fn change_current_directory( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let dir = helix_core::path::expand_tilde( args.first() .context("target directory not provided")? + .as_ref() .as_ref(), ); @@ -2508,7 +2524,7 @@ mod cmd { fn show_current_directory( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let cwd = std::env::current_dir().context("Couldn't get the new working directory")?; @@ -2520,7 +2536,7 @@ mod cmd { /// Sets the [`Document`]'s encoding.. fn set_encoding( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (_, doc) = current!(cx.editor); @@ -2536,7 +2552,7 @@ mod cmd { /// Reload the [`Document`] from its source file. fn reload( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (view, doc) = current!(cx.editor); @@ -2545,7 +2561,7 @@ mod cmd { fn tree_sitter_scopes( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let (view, doc) = current!(cx.editor); @@ -2559,15 +2575,18 @@ mod cmd { fn vsplit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let id = view!(cx.editor).doc; - if let Some(path) = args.get(0) { - cx.editor.open(path.into(), Action::VerticalSplit)?; - } else { + if args.is_empty() { cx.editor.switch(id, Action::VerticalSplit); + } else { + for arg in args { + cx.editor + .open(PathBuf::from(arg.as_ref()), Action::VerticalSplit)?; + } } Ok(()) @@ -2575,15 +2594,18 @@ mod cmd { fn hsplit( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let id = view!(cx.editor).doc; - if let Some(path) = args.get(0) { - cx.editor.open(path.into(), Action::HorizontalSplit)?; - } else { + if args.is_empty() { cx.editor.switch(id, Action::HorizontalSplit); + } else { + for arg in args { + cx.editor + .open(PathBuf::from(arg.as_ref()), Action::HorizontalSplit)?; + } } Ok(()) @@ -2591,7 +2613,7 @@ mod cmd { fn tutor( cx: &mut compositor::Context, - _args: &[&str], + _args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { let path = helix_core::runtime_dir().join("tutor.txt"); @@ -2603,7 +2625,7 @@ mod cmd { pub(super) fn goto_line_number( cx: &mut compositor::Context, - args: &[&str], + args: &[Cow], _event: PromptEvent, ) -> anyhow::Result<()> { ensure!(!args.is_empty(), "Line number required"); @@ -2709,7 +2731,7 @@ mod cmd { TypableCommand { name: "format", aliases: &["fmt"], - doc: "Format the file using a formatter.", + doc: "Format the file using the LSP formatter.", fun: format, completer: None, }, @@ -2800,7 +2822,7 @@ mod cmd { TypableCommand { name: "theme", aliases: &[], - doc: "Change the theme of current view. Requires theme name as argument (:theme )", + doc: "Change the editor theme.", fun: theme, completer: Some(completers::theme), }, @@ -2884,7 +2906,7 @@ mod cmd { TypableCommand { name: "change-current-directory", aliases: &["cd"], - doc: "Change the current working directory (:cd

).", + doc: "Change the current working directory.", fun: change_current_directory, completer: Some(completers::directory), }, @@ -3016,7 +3038,7 @@ fn command_mode(cx: &mut Context) { // If command is numeric, interpret as line number and go there. if parts.len() == 1 && parts[0].parse::().ok().is_some() { - if let Err(e) = cmd::goto_line_number(cx, &parts[0..], event) { + if let Err(e) = cmd::goto_line_number(cx, &[Cow::from(parts[0])], event) { cx.editor.set_error(format!("{}", e)); } return; @@ -3024,7 +3046,8 @@ fn command_mode(cx: &mut Context) { // Handle typable commands if let Some(cmd) = cmd::TYPABLE_COMMAND_MAP.get(parts[0]) { - if let Err(e) = (cmd.fun)(cx, &parts[1..], event) { + let args = shellwords::shellwords(input); + if let Err(e) = (cmd.fun)(cx, &args[1..], event) { cx.editor.set_error(format!("{}", e)); } } else { @@ -3298,12 +3321,19 @@ pub fn code_action(cx: &mut Context) { move |editor, code_action, _action| match code_action { lsp::CodeActionOrCommand::Command(command) => { log::debug!("code action command: {:?}", command); - editor.set_error(String::from("Handling code action command is not implemented yet, see https://github.com/helix-editor/helix/issues/183")); + execute_lsp_command(editor, command.clone()); } lsp::CodeActionOrCommand::CodeAction(code_action) => { log::debug!("code action: {:?}", code_action); if let Some(ref workspace_edit) = code_action.edit { - apply_workspace_edit(editor, offset_encoding, workspace_edit) + log::debug!("edit: {:?}", workspace_edit); + apply_workspace_edit(editor, offset_encoding, workspace_edit); + } + + // if code action provides both edit and command first the edit + // should be applied and then the command + if let Some(command) = &code_action.command { + execute_lsp_command(editor, command.clone()); } } }, @@ -3314,6 +3344,26 @@ pub fn code_action(cx: &mut Context) { ) } +pub fn execute_lsp_command(editor: &mut Editor, cmd: lsp::Command) { + let (_view, doc) = current!(editor); + + let language_server = match doc.language_server() { + Some(language_server) => language_server, + None => return, + }; + + // the command is executed on the server and communicated back + // to the client asynchronously using workspace edits + let command_future = language_server.command(cmd); + tokio::spawn(async move { + let res = command_future.await; + + if let Err(e) = res { + log::error!("execute LSP command: {}", e); + } + }); +} + pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { use lsp::ResourceOp; use std::fs; @@ -3367,7 +3417,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { } } -fn apply_workspace_edit( +pub fn apply_workspace_edit( editor: &mut Editor, offset_encoding: OffsetEncoding, workspace_edit: &lsp::WorkspaceEdit, @@ -3474,7 +3524,7 @@ fn apply_workspace_edit( fn last_picker(cx: &mut Context) { // TODO: last picker does not seem to work well with buffer_picker - cx.callback = Some(Box::new(|compositor: &mut Compositor| { + cx.callback = Some(Box::new(|compositor: &mut Compositor, _| { if let Some(picker) = compositor.last_picker.take() { compositor.push(picker); } @@ -3631,6 +3681,7 @@ fn normal_mode(cx: &mut Context) { doc.mode = Mode::Normal; + try_restore_indent(doc, view.id); doc.append_changes_to_history(view.id); // if leaving append mode, move cursor back by 1 @@ -3648,6 +3699,36 @@ fn normal_mode(cx: &mut Context) { } } +fn try_restore_indent(doc: &mut Document, view_id: ViewId) { + let doc_changes = doc.changes().changes(); + let text = doc.text().slice(..); + let pos = doc.selection(view_id).primary().cursor(text); + let mut can_restore_indent = false; + + // Removes trailing whitespace if insert mode is exited after starting a blank new line. + use helix_core::chars::char_is_whitespace; + use helix_core::Operation; + if let [Operation::Retain(move_pos), Operation::Insert(ref inserted_str), Operation::Retain(_)] = + doc_changes + { + if move_pos + inserted_str.len32() as usize == pos + && inserted_str.starts_with('\n') + && inserted_str.chars().skip(1).all(char_is_whitespace) + { + can_restore_indent = true; + } + } + + if can_restore_indent { + let transaction = + Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| { + let line_start_pos = text.line_to_char(range.cursor_line(text)); + (line_start_pos, pos, None) + }); + doc.apply(&transaction, view_id); + } +} + // Store a jump on the jumplist. fn push_jump(editor: &mut Editor) { let (view, doc) = current!(editor); @@ -4220,8 +4301,9 @@ pub mod insert { // The default insert hook: simply insert the character #[allow(clippy::unnecessary_wraps)] // need to use Option<> because of the Hook signature fn insert(doc: &Rope, selection: &Selection, ch: char) -> Option { + let cursors = selection.clone().cursors(doc.slice(..)); let t = Tendril::from_char(ch); - let transaction = Transaction::insert(doc, selection, t); + let transaction = Transaction::insert(doc, &cursors, t); Some(transaction) } @@ -4236,11 +4318,11 @@ pub mod insert { }; let text = doc.text(); - let selection = doc.selection(view.id).clone().cursors(text.slice(..)); + let selection = doc.selection(view.id); // run through insert hooks, stopping on the first one that returns Some(t) for hook in hooks { - if let Some(transaction) = hook(text, &selection, c) { + if let Some(transaction) = hook(text, selection, c) { doc.apply(&transaction, view.id); break; } @@ -4606,11 +4688,12 @@ fn paste_impl( doc: &mut Document, view: &View, action: Paste, + count: usize, ) -> Option { let repeat = std::iter::repeat( values .last() - .map(|value| Tendril::from_slice(value)) + .map(|value| Tendril::from(value.repeat(count))) .unwrap(), ); @@ -4625,7 +4708,7 @@ fn paste_impl( let mut values = values .iter() .map(|value| REGEX.replace_all(value, doc.line_ending.as_str())) - .map(|value| Tendril::from(value.as_ref())) + .map(|value| Tendril::from(value.as_ref().repeat(count))) .chain(repeat); let text = doc.text(); @@ -4645,7 +4728,7 @@ fn paste_impl( // paste append (Paste::After, false) => range.to(), }; - (pos, pos, Some(values.next().unwrap())) + (pos, pos, values.next()) }); Some(transaction) @@ -4655,13 +4738,14 @@ fn paste_clipboard_impl( editor: &mut Editor, action: Paste, clipboard_type: ClipboardType, + count: usize, ) -> anyhow::Result<()> { let (view, doc) = current!(editor); match editor .clipboard_provider .get_contents(clipboard_type) - .map(|contents| paste_impl(&[contents], doc, view, action)) + .map(|contents| paste_impl(&[contents], doc, view, action, count)) { Ok(Some(transaction)) => { doc.apply(&transaction, view.id); @@ -4674,22 +4758,43 @@ fn paste_clipboard_impl( } fn paste_clipboard_after(cx: &mut Context) { - let _ = paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Clipboard); + let _ = paste_clipboard_impl( + cx.editor, + Paste::After, + ClipboardType::Clipboard, + cx.count(), + ); } fn paste_clipboard_before(cx: &mut Context) { - let _ = paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Clipboard); + let _ = paste_clipboard_impl( + cx.editor, + Paste::Before, + ClipboardType::Clipboard, + cx.count(), + ); } fn paste_primary_clipboard_after(cx: &mut Context) { - let _ = paste_clipboard_impl(cx.editor, Paste::After, ClipboardType::Selection); + let _ = paste_clipboard_impl( + cx.editor, + Paste::After, + ClipboardType::Selection, + cx.count(), + ); } fn paste_primary_clipboard_before(cx: &mut Context) { - let _ = paste_clipboard_impl(cx.editor, Paste::Before, ClipboardType::Selection); + let _ = paste_clipboard_impl( + cx.editor, + Paste::Before, + ClipboardType::Selection, + cx.count(), + ); } fn replace_with_yanked(cx: &mut Context) { + let count = cx.count(); let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; @@ -4699,12 +4804,12 @@ fn replace_with_yanked(cx: &mut Context) { let repeat = std::iter::repeat( values .last() - .map(|value| Tendril::from_slice(value)) + .map(|value| Tendril::from_slice(&value.repeat(count))) .unwrap(), ); let mut values = values .iter() - .map(|value| Tendril::from_slice(value)) + .map(|value| Tendril::from_slice(&value.repeat(count))) .chain(repeat); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { @@ -4724,6 +4829,7 @@ fn replace_with_yanked(cx: &mut Context) { fn replace_selections_with_clipboard_impl( editor: &mut Editor, clipboard_type: ClipboardType, + count: usize, ) -> anyhow::Result<()> { let (view, doc) = current!(editor); @@ -4731,7 +4837,11 @@ fn replace_selections_with_clipboard_impl( Ok(contents) => { let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { - (range.from(), range.to(), Some(contents.as_str().into())) + ( + range.from(), + range.to(), + Some(contents.repeat(count).as_str().into()), + ) }); doc.apply(&transaction, view.id); @@ -4743,21 +4853,22 @@ fn replace_selections_with_clipboard_impl( } fn replace_selections_with_clipboard(cx: &mut Context) { - let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard); + let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count()); } fn replace_selections_with_primary_clipboard(cx: &mut Context) { - let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection); + let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count()); } fn paste_after(cx: &mut Context) { + let count = cx.count(); let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; if let Some(transaction) = registers .read(reg_name) - .and_then(|values| paste_impl(values, doc, view, Paste::After)) + .and_then(|values| paste_impl(values, doc, view, Paste::After, count)) { doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -4765,13 +4876,14 @@ fn paste_after(cx: &mut Context) { } fn paste_before(cx: &mut Context) { + let count = cx.count(); let reg_name = cx.register.unwrap_or('"'); let (view, doc) = current!(cx.editor); let registers = &mut cx.editor.registers; if let Some(transaction) = registers .read(reg_name) - .and_then(|values| paste_impl(values, doc, view, Paste::Before)) + .and_then(|values| paste_impl(values, doc, view, Paste::Before, count)) { doc.apply(&transaction, view.id); doc.append_changes_to_history(view.id); @@ -5167,8 +5279,12 @@ fn hover(cx: &mut Context) { // skip if contents empty let contents = ui::Markdown::new(contents, editor.syn_loader.clone()); - let popup = Popup::new(contents); - compositor.push(Box::new(popup)); + let popup = Popup::new("documentation", contents); + if let Some(doc_popup) = compositor.find_id("documentation") { + *doc_popup = popup; + } else { + compositor.push(Box::new(popup)); + } } }, ); @@ -5318,6 +5434,12 @@ fn jump_backward(cx: &mut Context) { }; } +fn save_selection(cx: &mut Context) { + push_jump(cx.editor); + cx.editor + .set_status("Selection saved to jumplist".to_owned()); +} + fn rotate_view(cx: &mut Context) { cx.editor.focus_next() } @@ -5893,3 +6015,56 @@ fn increment_impl(cx: &mut Context, amount: i64) { doc.append_changes_to_history(view.id); } } + +fn record_macro(cx: &mut Context) { + if let Some((reg, mut keys)) = cx.editor.macro_recording.take() { + // Remove the keypress which ends the recording + keys.pop(); + let s = keys + .into_iter() + .map(|key| format!("{}", key)) + .collect::>() + .join(" "); + cx.editor.registers.get_mut(reg).write(vec![s]); + cx.editor + .set_status(format!("Recorded to register {}", reg)); + } else { + let reg = cx.register.take().unwrap_or('@'); + cx.editor.macro_recording = Some((reg, Vec::new())); + cx.editor + .set_status(format!("Recording to register {}", reg)); + } +} + +fn play_macro(cx: &mut Context) { + let reg = cx.register.unwrap_or('@'); + let keys = match cx + .editor + .registers + .get(reg) + .and_then(|reg| reg.read().get(0)) + .context("Register empty") + .and_then(|s| { + s.split_whitespace() + .map(str::parse::) + .collect::, _>>() + .context("Failed to parse macro") + }) { + Ok(keys) => keys, + Err(e) => { + cx.editor.set_error(format!("{}", e)); + return; + } + }; + let count = cx.count(); + + cx.callback = Some(Box::new( + move |compositor: &mut Compositor, cx: &mut compositor::Context| { + for _ in 0..count { + for &key in keys.iter() { + compositor.handle_event(crossterm::event::Event::Key(key.into()), cx); + } + } + }, + )); +} diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index 3a644750e..321f56a5e 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -7,7 +7,7 @@ use helix_view::graphics::{CursorKind, Rect}; use crossterm::event::Event; use tui::buffer::Buffer as Surface; -pub type Callback = Box; +pub type Callback = Box; // --> EventResult should have a callback that takes a context with methods like .popup(), // .prompt() etc. That way we can abstract it from the renderer. @@ -55,15 +55,20 @@ pub trait Component: Any + AnyComponent { /// May be used by the parent component to compute the child area. /// viewport is the maximum allowed area, and the child should stay within those bounds. + /// + /// The returned size might be larger than the viewport if the child is too big to fit. + /// In this case the parent can use the values to calculate scroll. fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { - // TODO: for scrolling, the scroll wrapper should place a size + offset on the Context - // that way render can use it None } fn type_name(&self) -> &'static str { std::any::type_name::() } + + fn id(&self) -> Option<&'static str> { + None + } } use anyhow::Error; @@ -126,12 +131,17 @@ impl Compositor { } pub fn handle_event(&mut self, event: Event, cx: &mut Context) -> bool { + // If it is a key event and a macro is being recorded, push the key event to the recording. + if let (Event::Key(key), Some((_, keys))) = (event, &mut cx.editor.macro_recording) { + keys.push(key.into()); + } + // propagate events through the layers until we either find a layer that consumes it or we // run out of layers (event bubbling) for layer in self.layers.iter_mut().rev() { match layer.handle_event(event, cx) { EventResult::Consumed(Some(callback)) => { - callback(self); + callback(self, cx); return true; } EventResult::Consumed(None) => return true, @@ -184,6 +194,14 @@ impl Compositor { .find(|component| component.type_name() == type_name) .and_then(|component| component.as_any_mut().downcast_mut()) } + + pub fn find_id(&mut self, id: &'static str) -> Option<&mut T> { + let type_name = std::any::type_name::(); + self.layers + .iter_mut() + .find(|component| component.type_name() == type_name && component.id() == Some(id)) + .and_then(|component| component.as_any_mut().downcast_mut()) + } } // View casting, taken straight from Cursive diff --git a/helix-term/src/keymap.rs b/helix-term/src/keymap.rs index 9debbbacf..257d5f296 100644 --- a/helix-term/src/keymap.rs +++ b/helix-term/src/keymap.rs @@ -593,6 +593,9 @@ impl Default for Keymaps { // paste_all "P" => paste_before, + "q" => record_macro, + "Q" => play_macro, + ">" => indent, "<" => unindent, "=" => format_selections, @@ -641,7 +644,7 @@ impl Default for Keymaps { "tab" => jump_forward, // tab == "C-o" => jump_backward, - // "C-s" => save_selection, + "C-s" => save_selection, "space" => { "Space" "f" => file_picker, diff --git a/helix-term/src/lib.rs b/helix-term/src/lib.rs index f5e3a8cdd..58cb139c7 100644 --- a/helix-term/src/lib.rs +++ b/helix-term/src/lib.rs @@ -9,3 +9,14 @@ pub mod config; pub mod job; pub mod keymap; pub mod ui; + +#[cfg(not(windows))] +fn true_color() -> bool { + std::env::var("COLORTERM") + .map(|v| matches!(v.as_str(), "truecolor" | "24bit")) + .unwrap_or(false) +} +#[cfg(windows)] +fn true_color() -> bool { + true +} diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index dd782d29d..a55201ff2 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -168,7 +168,7 @@ impl Completion { } }; }); - let popup = Popup::new(menu); + let popup = Popup::new("completion", menu); let mut completion = Self { popup, start_offset, @@ -328,8 +328,8 @@ impl Component for Completion { let y = popup_y; if let Some((rel_width, rel_height)) = markdown_doc.required_size((width, height)) { - width = rel_width; - height = rel_height; + width = rel_width.min(width); + height = rel_height.min(height); } Rect::new(x, y, width, height) } else { diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 39ee15b4c..c5b438986 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -425,6 +425,8 @@ impl EditorView { let mut offset = 0; + let gutter_style = theme.get("ui.gutter"); + // avoid lots of small allocations by reusing a text buffer for each line let mut text = String::with_capacity(8); @@ -440,7 +442,7 @@ impl EditorView { viewport.y + i as u16, &text, *width, - style, + gutter_style.patch(style), ); } text.clear(); @@ -1100,13 +1102,31 @@ impl Component for EditorView { disp.push_str(&s); } } + let style = cx.editor.theme.get("ui.text"); + let macro_width = if cx.editor.macro_recording.is_some() { + 3 + } else { + 0 + }; surface.set_string( - area.x + area.width.saturating_sub(key_width), + area.x + area.width.saturating_sub(key_width + macro_width), area.y + area.height.saturating_sub(1), disp.get(disp.len().saturating_sub(key_width as usize)..) .unwrap_or(&disp), - cx.editor.theme.get("ui.text"), + style, ); + if let Some((reg, _)) = cx.editor.macro_recording { + let disp = format!("[{}]", reg); + let style = style + .fg(helix_view::graphics::Color::Yellow) + .add_modifier(Modifier::BOLD); + surface.set_string( + area.x + area.width.saturating_sub(3), + area.y + area.height.saturating_sub(1), + &disp, + style, + ); + } } if let Some(completion) = self.completion.as_mut() { diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index ca8303dd9..46657fb9a 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -241,11 +241,6 @@ impl Component for Markdown { } else if content_width > text_width { text_width = content_width; } - - if height >= viewport.1 { - height = viewport.1; - break; - } } Some((text_width + padding, height)) diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index e891c1492..69053db3e 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -190,7 +190,7 @@ impl Component for Menu { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); @@ -202,7 +202,7 @@ impl Component for Menu { return close_fn; } // arrow up/ctrl-p/shift-tab prev completion choice (including updating the doc) - shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => { + shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => { self.move_up(); (self.callback_fn)(cx.editor, self.selection(), MenuEvent::Update); return EventResult::Consumed(None); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index df64d9e31..fa1082281 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -186,6 +186,7 @@ pub mod completers { &helix_core::config_dir().join("themes"), )); names.push("default".into()); + names.push("base16_default".into()); let mut names: Vec<_> = names .into_iter() diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 16bf08a3e..1ef94df01 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -404,13 +404,13 @@ impl Component for Picker { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.last_picker = compositor.pop(); }))); match key_event.into() { - shift!(BackTab) | key!(Up) | ctrl!('p') | ctrl!('k') => { + shift!(Tab) | key!(Up) | ctrl!('p') | ctrl!('k') => { self.move_up(); } key!(Tab) | key!(Down) | ctrl!('n') | ctrl!('j') => { diff --git a/helix-term/src/ui/popup.rs b/helix-term/src/ui/popup.rs index 8f7921a11..bf7510a25 100644 --- a/helix-term/src/ui/popup.rs +++ b/helix-term/src/ui/popup.rs @@ -15,16 +15,20 @@ pub struct Popup { contents: T, position: Option, size: (u16, u16), + child_size: (u16, u16), scroll: usize, + id: &'static str, } impl Popup { - pub fn new(contents: T) -> Self { + pub fn new(id: &'static str, contents: T) -> Self { Self { contents, position: None, size: (0, 0), + child_size: (0, 0), scroll: 0, + id, } } @@ -68,6 +72,9 @@ impl Popup { pub fn scroll(&mut self, offset: usize, direction: bool) { if direction { self.scroll += offset; + + let max_offset = self.child_size.1.saturating_sub(self.size.1); + self.scroll = (self.scroll + offset).min(max_offset as usize); } else { self.scroll = self.scroll.saturating_sub(offset); } @@ -93,7 +100,7 @@ impl Component for Popup { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); @@ -115,13 +122,21 @@ impl Component for Popup { // tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll. } - fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> { + fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { + let max_width = 120.min(viewport.0); + let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport + let (width, height) = self .contents - .required_size((120, 26)) // max width, max height + .required_size((max_width, max_height)) .expect("Component needs required_size implemented in order to be embedded in a popup"); - self.size = (width, height); + self.child_size = (width, height); + self.size = (width.min(max_width), height.min(max_height)); + + // re-clamp scroll offset + let max_offset = self.child_size.1.saturating_sub(self.size.1); + self.scroll = self.scroll.min(max_offset as usize); Some(self.size) } @@ -143,4 +158,8 @@ impl Component for Popup { self.contents.render(area, surface, cx); } + + fn id(&self) -> Option<&'static str> { + Some(self.id) + } } diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index e90b07727..29e0339ae 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -127,7 +127,7 @@ impl Prompt { let mut char_position = char_indices .iter() .position(|(idx, _)| *idx == self.cursor) - .unwrap_or_else(|| char_indices.len()); + .unwrap_or(char_indices.len()); for _ in 0..rep { // Skip any non-whitespace characters @@ -426,7 +426,7 @@ impl Component for Prompt { _ => return EventResult::Ignored, }; - let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor| { + let close_fn = EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _| { // remove the layer compositor.pop(); }))); @@ -505,7 +505,7 @@ impl Component for Prompt { self.change_completion_selection(CompletionDirection::Forward); (self.callback_fn)(cx, &self.line, PromptEvent::Update) } - shift!(BackTab) => { + shift!(Tab) => { self.change_completion_selection(CompletionDirection::Backward); (self.callback_fn)(cx, &self.line, PromptEvent::Update) } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 34f55eb60..9c3a83f81 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -21,7 +21,7 @@ helix-lsp = { version = "0.5", path = "../helix-lsp"} crossterm = { version = "0.22", optional = true } # Conversion traits -once_cell = "1.8" +once_cell = "1.9" url = "2" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 2cb33fe32..c71d1850a 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -889,6 +889,10 @@ impl Document { self.indent_style.as_str() } + pub fn changes(&self) -> &ChangeSet { + &self.changes + } + #[inline] /// File path on disk. pub fn path(&self) -> Option<&PathBuf> { diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 9034d12c8..fd6eb4d57 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -2,6 +2,7 @@ use crate::{ clipboard::{get_clipboard_provider, ClipboardProvider}, document::SCRATCH_BUFFER_NAME, graphics::{CursorKind, Rect}, + input::KeyEvent, theme::{self, Theme}, tree::{self, Tree}, Document, DocumentId, View, ViewId, @@ -19,7 +20,7 @@ use std::{ use tokio::time::{sleep, Duration, Instant, Sleep}; -use anyhow::{bail, Context, Error}; +use anyhow::{bail, Error}; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; @@ -104,6 +105,8 @@ pub struct Config { /// Whether to display infoboxes. Defaults to true. pub auto_info: bool, pub file_picker: FilePickerConfig, + /// Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. Defaults to `false`. + pub true_color: bool, } #[derive(Debug, Copy, Clone, PartialEq, Eq, Deserialize)] @@ -136,6 +139,7 @@ impl Default for Config { completion_trigger_len: 2, auto_info: true, file_picker: FilePickerConfig::default(), + true_color: false, } } } @@ -160,6 +164,7 @@ pub struct Editor { pub count: Option, pub selected_register: Option, pub registers: Registers, + pub macro_recording: Option<(char, Vec)>, pub theme: Theme, pub language_servers: helix_lsp::Registry, pub clipboard_provider: Box, @@ -203,6 +208,7 @@ impl Editor { documents: BTreeMap::new(), count: None, selected_register: None, + macro_recording: None, theme: theme_loader.default(), language_servers, syn_loader, @@ -262,15 +268,6 @@ impl Editor { self._refresh(); } - pub fn set_theme_from_name(&mut self, theme: &str) -> anyhow::Result<()> { - let theme = self - .theme_loader - .load(theme.as_ref()) - .with_context(|| format!("failed setting theme `{}`", theme))?; - self.set_theme(theme); - Ok(()) - } - /// Refreshes the language server for a given document pub fn refresh_language_server(&mut self, doc_id: DocumentId) -> Option<()> { let doc = self.documents.get_mut(&doc_id)?; diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 86773c1db..af016c56e 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -22,7 +22,8 @@ pub fn diagnostic<'doc>( Box::new(move |line: usize, _selected: bool, out: &mut String| { use helix_core::diagnostic::Severity; - if let Some(diagnostic) = diagnostics.iter().find(|d| d.line == line) { + if let Ok(index) = diagnostics.binary_search_by_key(&line, |d| d.line) { + let diagnostic = &diagnostics[index]; write!(out, "●").unwrap(); return Some(match diagnostic.severity { Some(Severity::Error) => error, diff --git a/helix-view/src/input.rs b/helix-view/src/input.rs index 580204ccc..92caa5176 100644 --- a/helix-view/src/input.rs +++ b/helix-view/src/input.rs @@ -36,7 +36,6 @@ pub(crate) mod keys { pub(crate) const PAGEUP: &str = "pageup"; pub(crate) const PAGEDOWN: &str = "pagedown"; pub(crate) const TAB: &str = "tab"; - pub(crate) const BACKTAB: &str = "backtab"; pub(crate) const DELETE: &str = "del"; pub(crate) const INSERT: &str = "ins"; pub(crate) const NULL: &str = "null"; @@ -82,7 +81,6 @@ impl fmt::Display for KeyEvent { KeyCode::PageUp => f.write_str(keys::PAGEUP)?, KeyCode::PageDown => f.write_str(keys::PAGEDOWN)?, KeyCode::Tab => f.write_str(keys::TAB)?, - KeyCode::BackTab => f.write_str(keys::BACKTAB)?, KeyCode::Delete => f.write_str(keys::DELETE)?, KeyCode::Insert => f.write_str(keys::INSERT)?, KeyCode::Null => f.write_str(keys::NULL)?, @@ -116,7 +114,6 @@ impl UnicodeWidthStr for KeyEvent { KeyCode::PageUp => keys::PAGEUP.len(), KeyCode::PageDown => keys::PAGEDOWN.len(), KeyCode::Tab => keys::TAB.len(), - KeyCode::BackTab => keys::BACKTAB.len(), KeyCode::Delete => keys::DELETE.len(), KeyCode::Insert => keys::INSERT.len(), KeyCode::Null => keys::NULL.len(), @@ -166,7 +163,6 @@ impl std::str::FromStr for KeyEvent { keys::PAGEUP => KeyCode::PageUp, keys::PAGEDOWN => KeyCode::PageDown, keys::TAB => KeyCode::Tab, - keys::BACKTAB => KeyCode::BackTab, keys::DELETE => KeyCode::Delete, keys::INSERT => KeyCode::Insert, keys::NULL => KeyCode::Null, @@ -220,12 +216,40 @@ impl<'de> Deserialize<'de> for KeyEvent { #[cfg(feature = "term")] impl From for KeyEvent { - fn from( - crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent, - ) -> KeyEvent { - KeyEvent { - code: code.into(), - modifiers: modifiers.into(), + fn from(crossterm::event::KeyEvent { code, modifiers }: crossterm::event::KeyEvent) -> Self { + if code == crossterm::event::KeyCode::BackTab { + // special case for BackTab -> Shift-Tab + let mut modifiers: KeyModifiers = modifiers.into(); + modifiers.insert(KeyModifiers::SHIFT); + Self { + code: KeyCode::Tab, + modifiers, + } + } else { + Self { + code: code.into(), + modifiers: modifiers.into(), + } + } + } +} + +#[cfg(feature = "term")] +impl From for crossterm::event::KeyEvent { + fn from(KeyEvent { code, modifiers }: KeyEvent) -> Self { + if code == KeyCode::Tab && modifiers.contains(KeyModifiers::SHIFT) { + // special case for Shift-Tab -> BackTab + let mut modifiers = modifiers; + modifiers.remove(KeyModifiers::SHIFT); + crossterm::event::KeyEvent { + code: crossterm::event::KeyCode::BackTab, + modifiers: modifiers.into(), + } + } else { + crossterm::event::KeyEvent { + code: code.into(), + modifiers: modifiers.into(), + } } } } diff --git a/helix-view/src/keyboard.rs b/helix-view/src/keyboard.rs index 810aa0635..f17172091 100644 --- a/helix-view/src/keyboard.rs +++ b/helix-view/src/keyboard.rs @@ -79,8 +79,6 @@ pub enum KeyCode { PageDown, /// Tab key. Tab, - /// Shift + Tab key. - BackTab, /// Delete key. Delete, /// Insert key. @@ -116,7 +114,6 @@ impl From for crossterm::event::KeyCode { KeyCode::PageUp => CKeyCode::PageUp, KeyCode::PageDown => CKeyCode::PageDown, KeyCode::Tab => CKeyCode::Tab, - KeyCode::BackTab => CKeyCode::BackTab, KeyCode::Delete => CKeyCode::Delete, KeyCode::Insert => CKeyCode::Insert, KeyCode::F(f_number) => CKeyCode::F(f_number), @@ -144,7 +141,7 @@ impl From for KeyCode { CKeyCode::PageUp => KeyCode::PageUp, CKeyCode::PageDown => KeyCode::PageDown, CKeyCode::Tab => KeyCode::Tab, - CKeyCode::BackTab => KeyCode::BackTab, + CKeyCode::BackTab => unreachable!("BackTab should have been handled on KeyEvent level"), CKeyCode::Delete => KeyCode::Delete, CKeyCode::Insert => KeyCode::Insert, CKeyCode::F(f_number) => KeyCode::F(f_number), diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 47fc6262f..4a2ecbba9 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -15,6 +15,10 @@ pub use crate::graphics::{Color, Modifier, Style}; pub static DEFAULT_THEME: Lazy = Lazy::new(|| { toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") }); +pub static BASE16_DEFAULT_THEME: Lazy = Lazy::new(|| { + toml::from_slice(include_bytes!("../../base16_theme.toml")) + .expect("Failed to parse base 16 default theme") +}); #[derive(Clone, Debug)] pub struct Loader { @@ -35,6 +39,9 @@ impl Loader { if name == "default" { return Ok(self.default()); } + if name == "base16_default" { + return Ok(self.base16_default()); + } let filename = format!("{}.toml", name); let user_path = self.user_dir.join(&filename); @@ -74,6 +81,11 @@ impl Loader { pub fn default(&self) -> Theme { DEFAULT_THEME.clone() } + + /// Returns the alternative 16-color default theme + pub fn base16_default(&self) -> Theme { + BASE16_DEFAULT_THEME.clone() + } } #[derive(Clone, Debug)] @@ -138,8 +150,7 @@ impl Theme { } pub fn get(&self, scope: &str) -> Style { - self.try_get(scope) - .unwrap_or_else(|| Style::default().fg(Color::Rgb(0, 0, 255))) + self.try_get(scope).unwrap_or_default() } pub fn try_get(&self, scope: &str) -> Option