diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index bc72bb78..20035678 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -17,7 +17,7 @@ jobs: uses: cachix/install-nix-action@v18 - name: Authenticate with Cachix - uses: cachix/cachix-action@v11 + uses: cachix/cachix-action@v12 with: name: helix authToken: ${{ secrets.CACHIX_AUTH_TOKEN }} diff --git a/Cargo.lock b/Cargo.lock index ec48c596..93459aa0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,17 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "ahash" +version = "0.7.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fcb51a0695d8f838b1ee009b3fbf66bda078cd64590202a864a8f3e8c4315c47" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "aho-corasick" version = "0.7.18" @@ -22,9 +33,9 @@ dependencies = [ [[package]] name = "anyhow" -version = "1.0.65" +version = "1.0.66" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "98161a4e3e2184da77bb14f02184cdd111e83bbbcc9979dfee3c44b9a85f5602" +checksum = "216261ddc8289130e551ddcd5ce8a064710c0d064a4d2895c67151c92b5443f6" [[package]] name = "arc-swap" @@ -81,9 +92,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" [[package]] name = "cc" -version = "1.0.73" +version = "1.0.74" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2fff2a6927b3bb87f9595d67196a70493f627687a71d87a0d692242c33f58c11" +checksum = "581f5dba903aac52ea3feb5ec4810848460ee833876f1f9b0fdeab1f19091574" [[package]] name = "cfg-if" @@ -277,15 +288,15 @@ dependencies = [ [[package]] name = "futures-core" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4e5aa3de05362c3fb88de6531e6296e85cde7739cccad4b9dfeeb7f6ebce56bf" +checksum = "04909a7a7e4633ae6c4a9ab280aeb86da1236243a77b694a49eacd659a4bd3ac" [[package]] name = "futures-executor" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9ff63c23854bee61b6e9cd331d523909f238fc7636290b96826e9cfa5faa00ab" +checksum = "7acc85df6714c176ab5edf386123fafe217be88c0840ec11f199441134a074e2" dependencies = [ "futures-core", "futures-task", @@ -294,15 +305,15 @@ dependencies = [ [[package]] name = "futures-task" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6508c467c73851293f390476d4491cf4d227dbabcd4170f3bb6044959b294f1" +checksum = "2ffb393ac5d9a6eaa9d3fdf37ae2776656b706e200c8e16b1bdb227f5198e6ea" [[package]] name = "futures-util" -version = "0.3.24" +version = "0.3.25" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44fb6cb1be61cc1d2e43b262516aafcf63b241cffdb1d3fa115f91d9c7b09c90" +checksum = "197676987abd2f9cadff84926f410af1c183608d36641465df73ae8211dc65d6" dependencies = [ "futures-core", "futures-task", @@ -383,6 +394,15 @@ dependencies = [ "memmap2", ] +[[package]] +name = "hashbrown" +version = "0.12.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8a9ee70c43aaf417c914396645a0fa852624801b24ebb7ae78fe8272889ac888" +dependencies = [ + "ahash", +] + [[package]] name = "helix-core" version = "0.6.0" @@ -664,9 +684,9 @@ dependencies = [ [[package]] name = "lsp-types" -version = "0.93.1" +version = "0.93.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a3bcfee315dde785ba887edb540b08765fd7df75a7d948844be6bf5712246734" +checksum = "9be6e9c7e2d18f651974370d7aff703f9513e0df6e464fd795660edc77e6ca51" dependencies = [ "bitflags", "serde", @@ -733,9 +753,9 @@ dependencies = [ [[package]] name = "once_cell" -version = "1.15.0" +version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e82dad04139b71a90c080c8463fe0dc7902db5192d939bd0950f074d014339e1" +checksum = "86f0b0d4bf799edbc74508c1e8bf170ff5f41238e5f8225603ca7caaae2b7860" [[package]] name = "parking_lot" @@ -919,18 +939,18 @@ checksum = "d29ab0c6d3fc0ee92fe66e2d99f700eab17a8d57d1c1d3b748380fb20baa78cd" [[package]] name = "serde" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "728eb6351430bccb993660dfffc5a72f91ccc1295abaa8ce19b27ebe4f75568b" +checksum = "d193d69bae983fc11a79df82342761dfbf28a99fc8d203dca4c3c1b590948965" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.145" +version = "1.0.147" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81fa1584d3d1bcacd84c277a0dfe21f5b0f6accf4a23d04d4c6d61f1af522b4c" +checksum = "4f1d362ca8fc9c3e3a7484440752472d68a6caa98f1ab81d99b5dfe517cec852" dependencies = [ "proc-macro2", "quote", @@ -939,9 +959,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.86" +version = "1.0.87" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41feea4228a6f1cd09ec7a3593a682276702cd67b5273544757dae23c096f074" +checksum = "6ce777b7b150d76b9cf60d28b55f5847135a003f7d7350c6be7a773508ce7d45" dependencies = [ "itoa", "ryu", @@ -1103,18 +1123,18 @@ dependencies = [ [[package]] name = "termini" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "394766021ef3dae8077f080518cdf5360831990f77f5708d5e3594c9b3efa2f9" +checksum = "8c0f7ecb9c2a380d2686a747e4fc574043712326e8d39fbd220ab3bd29768a12" dependencies = [ "dirs-next", ] [[package]] name = "textwrap" -version = "0.15.1" +version = "0.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "949517c0cf1bf4ee812e2e07e08ab448e3ae0d23472aee8a06c985f0c8815b16" +checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d" dependencies = [ "smawk", "unicode-linebreak", @@ -1264,10 +1284,11 @@ checksum = "c4f5b37a154999a8f3f98cc23a628d850e154479cd94decf3414696e12e31aaf" [[package]] name = "unicode-linebreak" -version = "0.1.2" +version = "0.1.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3a52dcaab0c48d931f7cc8ef826fa51690a08e1ea55117ef26f89864f532383f" +checksum = "c5faade31a542b8b35855fff6e8def199853b2da8da256da52f52f1316ee3137" dependencies = [ + "hashbrown", "regex", ] diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index caf5d525..411e67b8 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -27,6 +27,7 @@ | elixir | ✓ | ✓ | | `elixir-ls` | | elm | ✓ | | | `elm-language-server` | | elvish | ✓ | | | `elvish` | +| env | ✓ | | | | | erb | ✓ | | | | | erlang | ✓ | ✓ | | `erlang_ls` | | esdl | ✓ | | | | @@ -53,6 +54,7 @@ | html | ✓ | | | `vscode-html-language-server` | | idris | | | | `idris2-lsp` | | iex | ✓ | | | | +| ini | ✓ | | | | | java | ✓ | | | `jdtls` | | javascript | ✓ | ✓ | ✓ | `typescript-language-server` | | jsdoc | ✓ | | | | @@ -60,6 +62,7 @@ | jsonnet | ✓ | | | `jsonnet-language-server` | | jsx | ✓ | ✓ | ✓ | `typescript-language-server` | | julia | ✓ | | | `julia` | +| kdl | ✓ | | | | | kotlin | ✓ | | | `kotlin-language-server` | | latex | ✓ | ✓ | | `texlab` | | lean | ✓ | | | `lean` | @@ -120,10 +123,13 @@ | v | ✓ | | | `vls` | | vala | ✓ | | | `vala-language-server` | | verilog | ✓ | ✓ | | `svlangserver` | +| vhs | ✓ | | | | | vue | ✓ | | | `vls` | | wast | ✓ | | | | | wat | ✓ | | | | | wgsl | ✓ | | | `wgsl_analyzer` | +| wit | ✓ | | ✓ | | | xit | ✓ | | | | +| xml | ✓ | | ✓ | | | yaml | ✓ | | ✓ | `yaml-language-server` | | zig | ✓ | ✓ | ✓ | `zls` | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index 317d3ced..adf1b4c6 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -28,7 +28,7 @@ | `:quit-all!`, `:qa!` | Force close all views ignoring unsaved changes. | | `:cquit`, `:cq` | Quit with exit code (default 1). Accepts an optional integer exit code (:cq 2). | | `:cquit!`, `:cq!` | Force quit with exit code (default 1) ignoring unsaved changes. Accepts an optional integer exit code (:cq! 2). | -| `:theme` | Change the editor theme. | +| `:theme` | Change the editor theme (show current theme if no name specified). | | `: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. | @@ -44,6 +44,7 @@ | `: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. | +| `:update` | Write changes only if the file has been modified. | | `:lsp-restart` | Restarts the Language Server that is in use by the current doc | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | | `:debug-start`, `:dbg` | Start a debug session from a given template with given parameters. | diff --git a/book/src/install.md b/book/src/install.md index 6e2a1f3d..a041d651 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -52,7 +52,8 @@ sudo xbps-install helix ## Windows -Helix can be installed using [Scoop](https://scoop.sh/) or [Chocolatey](https://chocolatey.org/). +Helix can be installed using [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/) +or [MSYS2](https://msys2.org/). **Scoop:** @@ -66,6 +67,23 @@ scoop install helix choco install helix ``` +**MSYS2:** + +``` +pacman -S mingw-w64-i686-helix +``` + +or + +``` +pacman -S mingw-w64-x86_64-helix +``` + +or + +``` +pacman -S mingw-w64-ucrt-x86_64-helix +``` ## Build from source diff --git a/flake.lock b/flake.lock index f28ec884..74206d2b 100644 --- a/flake.lock +++ b/flake.lock @@ -1,5 +1,22 @@ { "nodes": { + "all-cabal-json": { + "flake": false, + "locked": { + "lastModified": 1665552503, + "narHash": "sha256-r14RmRSwzv5c+bWKUDaze6pXM7nOsiz1H8nvFHJvufc=", + "owner": "nix-community", + "repo": "all-cabal-json", + "rev": "d7c0434eebffb305071404edcf9d5cd99703878e", + "type": "github" + }, + "original": { + "owner": "nix-community", + "ref": "hackage", + "repo": "all-cabal-json", + "type": "github" + } + }, "crane": { "flake": false, "locked": { @@ -19,11 +36,11 @@ "devshell": { "flake": false, "locked": { - "lastModified": 1660811669, - "narHash": "sha256-V6lmsaLNFz41myppL0yxglta92ijkSvpZ+XVygAh+bU=", + "lastModified": 1667210711, + "narHash": "sha256-IoErjXZAkzYWHEpQqwu/DeRNJGFdR7X2OGbkhMqMrpw=", "owner": "numtide", "repo": "devshell", - "rev": "c2feacb46ee69949124c835419861143c4016fb5", + "rev": "96a9dd12b8a447840cc246e17a47b81a4268bba7", "type": "github" }, "original": { @@ -38,6 +55,7 @@ "nci", "nixpkgs" ], + "all-cabal-json": "all-cabal-json", "crane": "crane", "devshell": [ "nci", @@ -47,6 +65,7 @@ "nci", "nixpkgs" ], + "ghc-utils": "ghc-utils", "gomod2nix": [ "nci", "nixpkgs" @@ -69,11 +88,11 @@ ] }, "locked": { - "lastModified": 1662176993, - "narHash": "sha256-Sy7DsGAveDUFBb6YDsUSYZd/AcXfP/MOMIwMt/NgY84=", + "lastModified": 1667429039, + "narHash": "sha256-Lu6da25JioHzerkLHAHSO9suCQFzJ/XBjkcGCIbasLM=", "owner": "nix-community", "repo": "dream2nix", - "rev": "809bc5940214744eb29778a9a0b03f161979c1b2", + "rev": "5252794e58eedb02d607fa3187ffead7becc81b0", "type": "github" }, "original": { @@ -84,11 +103,11 @@ }, "flake-utils": { "locked": { - "lastModified": 1656928814, - "narHash": "sha256-RIFfgBuKz6Hp89yRr7+NR5tzIAbn52h8vT6vXkYjZoM=", + "lastModified": 1659877975, + "narHash": "sha256-zllb8aq3YO3h8B/U0/J1WBgAL8EX5yWf5pMj3G0NAmc=", "owner": "numtide", "repo": "flake-utils", - "rev": "7e2a3b3dfd9af950a856d66b0a7d01e3c18aa249", + "rev": "c0e246b9b83f637f4681389ecabcb2681b4f3af0", "type": "github" }, "original": { @@ -97,6 +116,22 @@ "type": "github" } }, + "ghc-utils": { + "flake": false, + "locked": { + "lastModified": 1662774800, + "narHash": "sha256-1Rd2eohGUw/s1tfvkepeYpg8kCEXiIot0RijapUjAkE=", + "ref": "refs/heads/master", + "rev": "bb3a2d3dc52ff0253fb9c2812bd7aa2da03e0fea", + "revCount": 1072, + "type": "git", + "url": "https://gitlab.haskell.org/bgamari/ghc-utils" + }, + "original": { + "type": "git", + "url": "https://gitlab.haskell.org/bgamari/ghc-utils" + } + }, "nci": { "inputs": { "devshell": "devshell", @@ -109,11 +144,11 @@ ] }, "locked": { - "lastModified": 1662177071, - "narHash": "sha256-x6XF//RdZlw81tFAYM1TkjY+iQIpyMCWZ46r9o4wVQY=", + "lastModified": 1667542401, + "narHash": "sha256-mdWjP5tjSf8n6FAtpSgL23kX4+eWBwLrSYo9iY3mA8Q=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "65270dea87bb82fc02102a15221677eea237680e", + "rev": "cd5e5cbd81c80dc219455dd3b1e0ddb55fae51ec", "type": "github" }, "original": { @@ -124,11 +159,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1662019588, - "narHash": "sha256-oPEjHKGGVbBXqwwL+UjsveJzghWiWV0n9ogo1X6l4cw=", + "lastModified": 1667482890, + "narHash": "sha256-pua0jp87iwN7NBY5/ypx0s9L9CG49Ju/NI4wGwurHc4=", "owner": "nixos", "repo": "nixpkgs", - "rev": "2da64a81275b68fdad38af669afeda43d401e94b", + "rev": "a2a777538d971c6b01c6e54af89ddd6567c055e8", "type": "github" }, "original": { @@ -153,11 +188,11 @@ ] }, "locked": { - "lastModified": 1662087605, - "narHash": "sha256-Gpf2gp2JenKGf+TylX/YJpttY2bzsnvAMLdLaxoZRyU=", + "lastModified": 1667487142, + "narHash": "sha256-bVuzLs1ZVggJAbJmEDVO9G6p8BH3HRaolK70KXvnWnU=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "60c2cfaa8b90ed8cebd18b214fac8682dcf222dd", + "rev": "cf668f737ac986c0a89e83b6b2e3c5ddbd8cf33b", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 8cb4b663..b1d3f01e 100644 --- a/flake.nix +++ b/flake.nix @@ -21,57 +21,124 @@ ... }: let lib = nixpkgs.lib; + ncl = nci.lib.nci-lib; mkRootPath = rel: builtins.path { path = "${toString ./.}/${rel}"; name = rel; }; + filteredSource = let + pathsToIgnore = [ + ".envrc" + ".ignore" + ".github" + "runtime" + "screenshot.png" + "book" + "contrib" + "docs" + "README.md" + "CHANGELOG.md" + "shell.nix" + "default.nix" + "grammars.nix" + "flake.nix" + "flake.lock" + ]; + ignorePaths = path: type: let + # split the nix store path into its components + components = lib.splitString "/" path; + # drop off the `/nix/hash-source` section from the path + relPathComponents = lib.drop 4 components; + # reassemble the path components + relPath = lib.concatStringsSep "/" relPathComponents; + in + lib.all (p: ! (lib.hasPrefix p relPath)) pathsToIgnore; + in + builtins.path { + name = "helix-source"; + path = toString ./.; + # filter out unnecessary paths + filter = ignorePaths; + }; outputs = nci.lib.makeOutputs { root = ./.; - renameOutputs = {"helix-term" = "helix";}; - # Set default app to hx (binary is from helix-term release build) - # Set default package to helix-term release build - defaultOutputs = { - app = "hx"; - package = "helix"; + config = common: { + outputs = { + # rename helix-term to helix since it's our main package + rename = {"helix-term" = "helix";}; + # Set default app to hx (binary is from helix-term release build) + # Set default package to helix-term release build + defaults = { + app = "hx"; + package = "helix"; + }; + }; + cCompiler.package = with common.pkgs; + if stdenv.isLinux + then gcc + else clang; + shell = { + packages = with common.pkgs; + [lld_13 cargo-flamegraph rust-analyzer] + ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin) + ++ (lib.optional stdenv.isLinux lldb); + env = [ + { + name = "HELIX_RUNTIME"; + eval = "$PWD/runtime"; + } + { + name = "RUST_BACKTRACE"; + value = "1"; + } + { + name = "RUSTFLAGS"; + value = + if common.pkgs.stdenv.isLinux + then "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment" + else ""; + } + ]; + }; }; - overrides = { - cCompiler = common: - with common.pkgs; - if stdenv.isLinux - then gcc - else clang; - crateOverrides = common: _: { - helix-term = prev: { - src = builtins.path { - name = "helix-source"; - path = toString ./.; - # filter out unneeded stuff that cause rebuilds - filter = path: type: - lib.all - (n: builtins.baseNameOf path != n) - [ - ".envrc" - ".ignore" - ".github" - "runtime" - "screenshot.png" - "book" - "contrib" - "docs" - "README.md" - "shell.nix" - "default.nix" - "grammars.nix" - "flake.nix" - "flake.lock" - ]; - }; + pkgConfig = common: { + helix-term = { + # Wrap helix with runtime + wrapper = _: old: let + inherit (common) pkgs; + makeOverridableHelix = old: config: let + grammars = pkgs.callPackage ./grammars.nix config; + runtimeDir = pkgs.runCommand "helix-runtime" {} '' + mkdir -p $out + ln -s ${mkRootPath "runtime"}/* $out + rm -r $out/grammars + ln -s ${grammars} $out/grammars + ''; + helix-wrapped = + common.internal.pkgsSet.utils.wrapDerivation old + { + nativeBuildInputs = [pkgs.makeWrapper]; + makeWrapperArgs = config.makeWrapperArgs or []; + } + '' + rm -rf $out/bin + mkdir -p $out/bin + ln -sf ${old}/bin/* $out/bin/ + wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}" + ''; + in + helix-wrapped + // {override = makeOverridableHelix old;}; + in + makeOverridableHelix old {}; + overrides.fix-build.overrideAttrs = prev: { + src = filteredSource; # disable fetching and building of tree-sitter grammars in the helix-term build.rs HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1"; - buildInputs = (prev.buildInputs or []) ++ [common.cCompiler.cc.lib]; + buildInputs = ncl.addBuildInputs prev [common.config.cCompiler.package.cc.lib]; # link languages and theme toml files since helix-term expects them (for tests) preConfigure = '' @@ -87,88 +154,20 @@ meta.mainProgram = "hx"; }; }; - shell = common: prev: { - packages = - prev.packages - ++ ( - with common.pkgs; - [lld_13 cargo-flamegraph rust-analyzer] - ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) cargo-tarpaulin) - ++ (lib.optional stdenv.isLinux lldb) - ); - env = - prev.env - ++ [ - { - name = "HELIX_RUNTIME"; - eval = "$PWD/runtime"; - } - { - name = "RUST_BACKTRACE"; - value = "1"; - } - { - name = "RUSTFLAGS"; - value = - if common.pkgs.stdenv.isLinux - then "-C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment" - else ""; - } - ]; - }; }; }; - makeOverridableHelix = system: old: config: let - pkgs = nixpkgs.legacyPackages.${system}; - grammars = pkgs.callPackage ./grammars.nix config; - runtimeDir = pkgs.runCommand "helix-runtime" {} '' - mkdir -p $out - ln -s ${mkRootPath "runtime"}/* $out - rm -r $out/grammars - ln -s ${grammars} $out/grammars - ''; - helix-wrapped = - pkgs.runCommand "${old.name}-wrapped" - { - inherit (old) pname version meta; - - nativeBuildInputs = [pkgs.makeWrapper]; - makeWrapperArgs = config.makeWrapperArgs or []; - } - '' - mkdir -p $out - cp -r --no-preserve=mode,ownership ${old}/* $out/ - chmod +x $out/bin/* - wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}" - ''; - in - helix-wrapped - // {override = makeOverridableHelix system old;}; in outputs // { - apps = - lib.mapAttrs - ( - system: apps: rec { - default = hx; - hx = { - type = "app"; - program = lib.getExe self.${system}.packages.helix; - }; - } - ) - outputs.apps; packages = lib.mapAttrs ( - system: packages: rec { - default = helix; - helix = makeOverridableHelix system helix-unwrapped {}; - helix-debug = makeOverridableHelix system helix-unwrapped-debug {}; - helix-unwrapped = packages.helix; - helix-unwrapped-debug = packages.helix-debug; - } + system: packages: + packages + // { + helix-unwrapped = packages.helix.passthru.unwrapped; + helix-unwrapped-debug = packages.helix-debug.passthru.unwrapped; + } ) outputs.packages; }; diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 4eaadd1e..45272f98 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -26,7 +26,7 @@ unicode-general-category = "0.6" # slab = "0.4.2" slotmap = "1.0" tree-sitter = "0.20" -once_cell = "1.15" +once_cell = "1.16" arc-swap = "1" regex = "1" bitflags = "1.3" @@ -43,7 +43,7 @@ encoding_rs = "0.8" chrono = { version = "0.4", default-features = false, features = ["alloc", "std"] } etcetera = "0.4" -textwrap = "0.15.1" +textwrap = "0.16.0" [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/history.rs b/helix-core/src/history.rs index b608097c..5cd72b07 100644 --- a/helix-core/src/history.rs +++ b/helix-core/src/history.rs @@ -282,7 +282,7 @@ impl History { } /// Whether to undo by a number of edits or a duration of time. -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Eq, Clone, Copy)] pub enum UndoKind { Steps(usize), TimePeriod(std::time::Duration), diff --git a/helix-core/src/increment/number.rs b/helix-core/src/increment/number.rs index 62b4a19d..91268729 100644 --- a/helix-core/src/increment/number.rs +++ b/helix-core/src/increment/number.rs @@ -110,8 +110,8 @@ impl<'a> Increment for NumberIncrementor<'a> { let (lower_count, upper_count): (usize, usize) = old_text.chars().skip(2).fold((0, 0), |(lower, upper), c| { ( - lower + c.is_ascii_lowercase().then(|| 1).unwrap_or(0), - upper + c.is_ascii_uppercase().then(|| 1).unwrap_or(0), + lower + usize::from(c.is_ascii_lowercase()), + upper + usize::from(c.is_ascii_uppercase()), ) }); if upper_count > lower_count { diff --git a/helix-core/src/line_ending.rs b/helix-core/src/line_ending.rs index 3e8a6cae..09e92523 100644 --- a/helix-core/src/line_ending.rs +++ b/helix-core/src/line_ending.rs @@ -6,7 +6,7 @@ pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::Crlf; pub const DEFAULT_LINE_ENDING: LineEnding = LineEnding::LF; /// Represents one of the valid Unicode line endings. -#[derive(PartialEq, Copy, Clone, Debug)] +#[derive(PartialEq, Eq, Copy, Clone, Debug)] pub enum LineEnding { Crlf, // CarriageReturn followed by LineFeed LF, // U+000A -- LineFeed diff --git a/helix-core/src/shellwords.rs b/helix-core/src/shellwords.rs index afc83496..e8c5945b 100644 --- a/helix-core/src/shellwords.rs +++ b/helix-core/src/shellwords.rs @@ -1,5 +1,22 @@ use std::borrow::Cow; +/// Auto escape for shellwords usage. +pub fn escape(input: &str) -> Cow<'_, str> { + if !input.chars().any(|x| x.is_ascii_whitespace()) { + Cow::Borrowed(input) + } else if cfg!(unix) { + Cow::Owned(input.chars().fold(String::new(), |mut buf, c| { + if c.is_ascii_whitespace() { + buf.push('\\'); + } + buf.push(c); + buf + })) + } else { + Cow::Owned(format!("\"{}\"", input)) + } +} + /// Get the vec of escaped / quoted / doublequoted filenames from the input str pub fn shellwords(input: &str) -> Vec> { enum State { @@ -226,4 +243,19 @@ mod test { ]; assert_eq!(expected, result); } + + #[test] + #[cfg(unix)] + fn test_escaping_unix() { + assert_eq!(escape("foobar"), Cow::Borrowed("foobar")); + assert_eq!(escape("foo bar"), Cow::Borrowed("foo\\ bar")); + assert_eq!(escape("foo\tbar"), Cow::Borrowed("foo\\\tbar")); + } + + #[test] + #[cfg(windows)] + fn test_escaping_windows() { + assert_eq!(escape("foobar"), Cow::Borrowed("foobar")); + assert_eq!(escape("foo bar"), Cow::Borrowed("\"foo bar\"")); + } } diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index 6244b380..a3de3cd1 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -13,7 +13,7 @@ pub const PAIRS: &[(char, char)] = &[ ('(', ')'), ]; -#[derive(Debug, PartialEq)] +#[derive(Debug, PartialEq, Eq)] pub enum Error { PairNotFound, CursorOverlap, diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index c17655a9..0f62577f 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -218,7 +218,7 @@ pub struct FormatterConfiguration { pub args: Vec, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct AdvancedCompletion { pub name: Option, @@ -226,14 +226,14 @@ pub struct AdvancedCompletion { pub default: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case", untagged)] pub enum DebugConfigCompletion { Named(String), Advanced(AdvancedCompletion), } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum DebugArgumentValue { String(String), @@ -241,7 +241,7 @@ pub enum DebugArgumentValue { Boolean(bool), } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct DebugTemplate { pub name: String, @@ -250,7 +250,7 @@ pub struct DebugTemplate { pub args: HashMap, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "kebab-case")] pub struct DebugAdapterConfig { pub name: String, @@ -266,7 +266,7 @@ pub struct DebugAdapterConfig { } // Different workarounds for adapters' differences -#[derive(Debug, Default, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct DebuggerQuirks { #[serde(default)] pub absolute_paths: bool, @@ -280,7 +280,7 @@ pub struct IndentationConfiguration { } /// Configuration for auto pairs -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", deny_unknown_fields, untagged)] pub enum AutoPairConfig { /// Enables or disables auto pairing. False means disabled. True means to use the default pairs. diff --git a/helix-dap/src/transport.rs b/helix-dap/src/transport.rs index 783a6f5d..dd03e568 100644 --- a/helix-dap/src/transport.rs +++ b/helix-dap/src/transport.rs @@ -22,7 +22,7 @@ pub struct Request { pub arguments: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct Response { // seq is omitted as unused and is not sent by some implementations pub request_seq: u64, diff --git a/helix-dap/src/types.rs b/helix-dap/src/types.rs index 51ecfe1b..0a9ebe5e 100644 --- a/helix-dap/src/types.rs +++ b/helix-dap/src/types.rs @@ -22,7 +22,7 @@ pub trait Request { const COMMAND: &'static str; } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ColumnDescriptor { pub attribute_name: String, @@ -35,7 +35,7 @@ pub struct ColumnDescriptor { pub width: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ExceptionBreakpointsFilter { pub filter: String, @@ -50,7 +50,7 @@ pub struct ExceptionBreakpointsFilter { pub condition_description: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct DebuggerCapabilities { #[serde(skip_serializing_if = "Option::is_none")] @@ -131,14 +131,14 @@ pub struct DebuggerCapabilities { pub supported_checksum_algorithms: Option>, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Checksum { pub algorithm: String, pub checksum: String, } -#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Source { #[serde(skip_serializing_if = "Option::is_none")] @@ -159,7 +159,7 @@ pub struct Source { pub checksums: Option>, } -#[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SourceBreakpoint { pub line: usize, @@ -173,7 +173,7 @@ pub struct SourceBreakpoint { pub log_message: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Breakpoint { #[serde(skip_serializing_if = "Option::is_none")] @@ -197,7 +197,7 @@ pub struct Breakpoint { pub offset: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct StackFrameFormat { #[serde(skip_serializing_if = "Option::is_none")] @@ -216,7 +216,7 @@ pub struct StackFrameFormat { pub include_all: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct StackFrame { pub id: usize, @@ -239,14 +239,14 @@ pub struct StackFrame { pub presentation_hint: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Thread { pub id: ThreadId, pub name: String, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Scope { pub name: String, @@ -270,14 +270,14 @@ pub struct Scope { pub end_column: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ValueFormat { #[serde(skip_serializing_if = "Option::is_none")] pub hex: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct VariablePresentationHint { #[serde(skip_serializing_if = "Option::is_none")] @@ -288,7 +288,7 @@ pub struct VariablePresentationHint { pub visibility: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Variable { pub name: String, @@ -308,7 +308,7 @@ pub struct Variable { pub memory_reference: Option, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Module { pub id: String, // TODO: || number @@ -333,7 +333,7 @@ pub struct Module { pub mod requests { use super::*; - #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct InitializeArguments { #[serde(rename = "clientID", skip_serializing_if = "Option::is_none")] @@ -409,7 +409,7 @@ pub mod requests { const COMMAND: &'static str = "configurationDone"; } - #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SetBreakpointsArguments { pub source: Source, @@ -420,7 +420,7 @@ pub mod requests { pub source_modified: Option, } - #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SetBreakpointsResponse { #[serde(skip_serializing_if = "Option::is_none")] @@ -436,13 +436,13 @@ pub mod requests { const COMMAND: &'static str = "setBreakpoints"; } - #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ContinueArguments { pub thread_id: ThreadId, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ContinueResponse { #[serde(skip_serializing_if = "Option::is_none")] @@ -458,7 +458,7 @@ pub mod requests { const COMMAND: &'static str = "continue"; } - #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct StackTraceArguments { pub thread_id: ThreadId, @@ -470,7 +470,7 @@ pub mod requests { pub format: Option, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct StackTraceResponse { #[serde(skip_serializing_if = "Option::is_none")] @@ -487,7 +487,7 @@ pub mod requests { const COMMAND: &'static str = "stackTrace"; } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ThreadsResponse { pub threads: Vec, @@ -502,13 +502,13 @@ pub mod requests { const COMMAND: &'static str = "threads"; } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ScopesArguments { pub frame_id: usize, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct ScopesResponse { pub scopes: Vec, @@ -523,7 +523,7 @@ pub mod requests { const COMMAND: &'static str = "scopes"; } - #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct VariablesArguments { pub variables_reference: usize, @@ -537,7 +537,7 @@ pub mod requests { pub format: Option, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct VariablesResponse { pub variables: Vec, @@ -552,7 +552,7 @@ pub mod requests { const COMMAND: &'static str = "variables"; } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct StepInArguments { pub thread_id: ThreadId, @@ -571,7 +571,7 @@ pub mod requests { const COMMAND: &'static str = "stepIn"; } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct StepOutArguments { pub thread_id: ThreadId, @@ -588,7 +588,7 @@ pub mod requests { const COMMAND: &'static str = "stepOut"; } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct NextArguments { pub thread_id: ThreadId, @@ -605,7 +605,7 @@ pub mod requests { const COMMAND: &'static str = "next"; } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct PauseArguments { pub thread_id: ThreadId, @@ -620,7 +620,7 @@ pub mod requests { const COMMAND: &'static str = "pause"; } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EvaluateArguments { pub expression: String, @@ -632,7 +632,7 @@ pub mod requests { pub format: Option, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct EvaluateResponse { pub result: String, @@ -658,7 +658,7 @@ pub mod requests { const COMMAND: &'static str = "evaluate"; } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SetExceptionBreakpointsArguments { pub filters: Vec, @@ -666,7 +666,7 @@ pub mod requests { // pub exceptionOptions: Option>, // needs capability } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct SetExceptionBreakpointsResponse { #[serde(skip_serializing_if = "Option::is_none")] @@ -684,7 +684,7 @@ pub mod requests { // Reverse Requests - #[derive(Debug, Default, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, Default, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct RunInTerminalResponse { #[serde(skip_serializing_if = "Option::is_none")] @@ -693,7 +693,7 @@ pub mod requests { pub shell_process_id: Option, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct RunInTerminalArguments { #[serde(skip_serializing_if = "Option::is_none")] @@ -745,7 +745,7 @@ pub mod events { Memory(Memory), } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Stopped { pub reason: String, @@ -763,7 +763,7 @@ pub mod events { pub hit_breakpoint_ids: Option>, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Continued { pub thread_id: ThreadId, @@ -771,27 +771,27 @@ pub mod events { pub all_threads_continued: Option, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Exited { pub exit_code: usize, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Terminated { #[serde(skip_serializing_if = "Option::is_none")] pub restart: Option, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Thread { pub reason: String, pub thread_id: ThreadId, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Output { pub output: String, @@ -811,28 +811,28 @@ pub mod events { pub data: Option, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Breakpoint { pub reason: String, pub breakpoint: super::Breakpoint, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Module { pub reason: String, pub module: super::Module, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct LoadedSource { pub reason: String, pub source: super::Source, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Process { pub name: String, @@ -846,13 +846,13 @@ pub mod events { pub pointer_size: Option, } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Capabilities { pub capabilities: super::DebuggerCapabilities, } - // #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + // #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] // #[serde(rename_all = "camelCase")] // pub struct Invalidated { // pub areas: Vec, @@ -860,7 +860,7 @@ pub mod events { // pub stack_frame_id: Option, // } - #[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] + #[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(rename_all = "camelCase")] pub struct Memory { pub memory_reference: String, diff --git a/helix-loader/Cargo.toml b/helix-loader/Cargo.toml index b4541de5..760205e1 100644 --- a/helix-loader/Cargo.toml +++ b/helix-loader/Cargo.toml @@ -19,7 +19,7 @@ serde = { version = "1.0", features = ["derive"] } toml = "0.5" etcetera = "0.4" tree-sitter = "0.20" -once_cell = "1.15" +once_cell = "1.16" log = "0.4" # TODO: these two should be on !wasm32 only diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index a92cadb6..833616e0 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -67,7 +67,7 @@ pub fn get_language(name: &str) -> Result { #[cfg(not(target_arch = "wasm32"))] pub fn get_language(name: &str) -> Result { use libloading::{Library, Symbol}; - let mut library_path = crate::runtime_dir().join("grammars").join(&name); + let mut library_path = crate::runtime_dir().join("grammars").join(name); library_path.set_extension(DYLIB_EXTENSION); let library = unsafe { Library::new(&library_path) } @@ -429,7 +429,7 @@ fn build_tree_sitter_library( if cfg!(all(windows, target_env = "msvc")) { command - .args(&["/nologo", "/LD", "/I"]) + .args(["/nologo", "/LD", "/I"]) .arg(header_path) .arg("/Od") .arg("/utf-8"); diff --git a/helix-lsp/src/jsonrpc.rs b/helix-lsp/src/jsonrpc.rs index b9b3fd2c..75ac9309 100644 --- a/helix-lsp/src/jsonrpc.rs +++ b/helix-lsp/src/jsonrpc.rs @@ -13,7 +13,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value; // https://www.jsonrpc.org/specification#error_object -#[derive(Debug, PartialEq, Clone)] +#[derive(Debug, PartialEq, Eq, Clone)] pub enum ErrorCode { ParseError, InvalidRequest, @@ -68,7 +68,7 @@ impl Serialize for ErrorCode { } } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Error { pub code: ErrorCode, pub message: String, @@ -100,7 +100,7 @@ impl std::error::Error for Error {} // https://www.jsonrpc.org/specification#request_object /// Request ID -#[derive(Debug, PartialEq, Clone, Hash, Eq, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Hash, Deserialize, Serialize)] #[serde(untagged)] pub enum Id { Null, @@ -109,7 +109,7 @@ pub enum Id { } /// Protocol Version -#[derive(Debug, PartialEq, Clone, Copy, Hash, Eq)] +#[derive(Debug, PartialEq, Eq, Clone, Copy, Hash)] pub enum Version { V2, } @@ -153,7 +153,7 @@ impl<'de> Deserialize<'de> for Version { } } -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] #[serde(untagged)] pub enum Params { None, @@ -182,7 +182,7 @@ impl From for Value { } } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct MethodCall { pub jsonrpc: Option, @@ -192,7 +192,7 @@ pub struct MethodCall { pub id: Id, } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] pub struct Notification { pub jsonrpc: Option, @@ -201,7 +201,7 @@ pub struct Notification { pub params: Params, } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] #[serde(untagged)] pub enum Call { @@ -235,7 +235,7 @@ impl From for Call { } } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(deny_unknown_fields)] #[serde(untagged)] pub enum Request { @@ -245,7 +245,7 @@ pub enum Request { // https://www.jsonrpc.org/specification#response_object -#[derive(Debug, PartialEq, Clone, Serialize, Deserialize)] +#[derive(Debug, PartialEq, Eq, Clone, Serialize, Deserialize)] pub struct Success { #[serde(skip_serializing_if = "Option::is_none")] pub jsonrpc: Option, @@ -253,7 +253,7 @@ pub struct Success { pub id: Id, } -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] pub struct Failure { #[serde(skip_serializing_if = "Option::is_none")] pub jsonrpc: Option, @@ -264,7 +264,7 @@ pub struct Failure { // Note that failure comes first because we're not using // #[serde(deny_unknown_field)]: we want a request that contains // both `result` and `error` to be a `Failure`. -#[derive(Debug, PartialEq, Clone, Deserialize, Serialize)] +#[derive(Debug, PartialEq, Eq, Clone, Deserialize, Serialize)] #[serde(untagged)] pub enum Output { Failure(Failure), @@ -280,7 +280,7 @@ impl From for Result { } } -#[derive(Clone, Debug, PartialEq, Deserialize, Serialize)] +#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)] #[serde(untagged)] pub enum Response { Single(Output), diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 30de5589..485cabe9 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -32,7 +32,7 @@ helix-dap = { version = "0.6", path = "../helix-dap" } helix-loader = { version = "0.6", path = "../helix-loader" } anyhow = "1" -once_cell = "1.15" +once_cell = "1.16" which = "4.2" diff --git a/helix-term/build.rs b/helix-term/build.rs index 74c35a3a..719113ff 100644 --- a/helix-term/build.rs +++ b/helix-term/build.rs @@ -6,7 +6,7 @@ const VERSION: &str = include_str!("../VERSION"); fn main() { let git_hash = Command::new("git") - .args(&["rev-parse", "HEAD"]) + .args(["rev-parse", "HEAD"]) .output() .ok() .filter(|output| output.status.success()) diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 660e64dd..feb6c0c7 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -180,7 +180,7 @@ impl Application { } else if !args.files.is_empty() { let first = &args.files[0].0; // we know it's not empty if first.is_dir() { - std::env::set_current_dir(&first).context("set current dir")?; + std::env::set_current_dir(first).context("set current dir")?; editor.new_file(Action::VerticalSplit); let picker = ui::file_picker(".".into(), &config.load().editor); compositor.push(Box::new(overlayed(picker))); @@ -240,7 +240,7 @@ impl Application { #[cfg(windows)] let signals = futures_util::stream::empty(); #[cfg(not(windows))] - let signals = Signals::new(&[signal::SIGTSTP, signal::SIGCONT, signal::SIGUSR1]) + let signals = Signals::new([signal::SIGTSTP, signal::SIGCONT, signal::SIGUSR1]) .context("build signal handler")?; let app = Self { diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index dab8c81d..8882b8d6 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -8,7 +8,7 @@ use tui::text::Spans; pub use typed::*; use helix_core::{ - comment, coords_at_pos, find_first_non_whitespace_char, find_root, graphemes, + comment, coords_at_pos, encoding, find_first_non_whitespace_char, find_root, graphemes, history::UndoKind, increment::date_time::DateTimeIncrementor, increment::{number::NumberIncrementor, Increment}, @@ -1027,6 +1027,7 @@ fn goto_file_vsplit(cx: &mut Context) { goto_file_impl(cx, Action::VerticalSplit); } +/// Goto files in selection. fn goto_file_impl(cx: &mut Context, action: Action) { let (view, doc) = current_ref!(cx.editor); let text = doc.text(); @@ -1036,15 +1037,25 @@ fn goto_file_impl(cx: &mut Context, action: Action) { .map(|r| text.slice(r.from()..r.to()).to_string()) .collect(); let primary = selections.primary(); - if selections.len() == 1 && primary.to() - primary.from() == 1 { - let current_word = movement::move_next_long_word_start( - text.slice(..), - movement::move_prev_long_word_start(text.slice(..), primary, 1), - 1, + // Checks whether there is only one selection with a width of 1 + if selections.len() == 1 && primary.len() == 1 { + let count = cx.count(); + let text_slice = text.slice(..); + // In this case it selects the WORD under the cursor + let current_word = textobject::textobject_word( + text_slice, + primary, + textobject::TextObject::Inside, + count, + true, ); + // Trims some surrounding chars so that the actual file is opened. + let surrounding_chars: &[_] = &['\'', '"', '(', ')']; paths.clear(); paths.push( - text.slice(current_word.from()..current_word.to()) + current_word + .fragment(text_slice) + .trim_matches(surrounding_chars) .to_string(), ); } @@ -1297,6 +1308,7 @@ fn replace(cx: &mut Context) { }); apply_transaction(&transaction, doc, view); + exit_select_mode(cx); } }) } @@ -1353,7 +1365,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { let range = doc.selection(view.id).primary(); let text = doc.text().slice(..); - let cursor = coords_at_pos(text, range.cursor(text)); + let cursor = visual_coords_at_pos(text, range.cursor(text), doc.tab_width()); let doc_last_line = doc.text().len_lines().saturating_sub(1); let last_line = view.last_line(doc); @@ -1385,7 +1397,7 @@ pub fn scroll(cx: &mut Context, offset: usize, direction: Direction) { // If cursor needs moving, replace primary selection if line != cursor.row { - let head = pos_at_coords(text, Position::new(line, cursor.col), true); // this func will properly truncate to line end + let head = pos_at_visual_coords(text, Position::new(line, cursor.col), doc.tab_width()); // this func will properly truncate to line end let anchor = if cx.editor.mode == Mode::Select { range.anchor @@ -2473,8 +2485,8 @@ impl ui::menu::Item for MappableCommand { match self { MappableCommand::Typable { doc, name, .. } => match keymap.get(name as &String) { - Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), - None => format!("{} [{}]", doc, name).into(), + Some(bindings) => format!("{} ({}) [:{}]", doc, fmt_binding(bindings), name).into(), + None => format!("{} [:{}]", doc, name).into(), }, MappableCommand::Static { doc, name, .. } => match keymap.get(*name) { Some(bindings) => format!("{} ({}) [{}]", doc, fmt_binding(bindings), name).into(), @@ -3252,6 +3264,7 @@ pub mod insert { (Some(_x), Some(_y), Some(ap)) if range.is_single_grapheme(text) && ap.get(_x).is_some() + && ap.get(_x).unwrap().open == _x && ap.get(_x).unwrap().close == _y => // delete both autopaired characters { @@ -3499,7 +3512,12 @@ enum Paste { } fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Paste, count: usize) { + if values.is_empty() { + return; + } + let repeat = std::iter::repeat( + // `values` is asserted to have at least one entry above. values .last() .map(|value| Tendril::from(value.repeat(count))) @@ -3523,6 +3541,8 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa let text = doc.text(); let selection = doc.selection(view.id); + let mut ranges = SmallVec::with_capacity(selection.len()); + let transaction = Transaction::change_by_selection(text, selection, |range| { let pos = match (action, linewise) { // paste linewise before @@ -3539,8 +3559,21 @@ fn paste_impl(values: &[String], doc: &mut Document, view: &mut View, action: Pa // paste at cursor (Paste::Cursor, _) => range.cursor(text.slice(..)), }; - (pos, pos, values.next()) + + let value = values.next(); + + let value_len = value + .as_ref() + .map(|content| content.chars().count()) + .unwrap_or_default(); + + ranges.push(Range::new(pos, pos + value_len)); + + (pos, pos, value) }); + + let transaction = transaction.with_selection(Selection::new(ranges, selection.primary_index())); + apply_transaction(&transaction, doc, view); } @@ -3634,18 +3667,19 @@ fn replace_with_yanked(cx: &mut Context) { }); apply_transaction(&transaction, doc, view); + exit_select_mode(cx); } } } fn replace_selections_with_clipboard_impl( - editor: &mut Editor, + cx: &mut Context, clipboard_type: ClipboardType, - count: usize, ) -> anyhow::Result<()> { - let (view, doc) = current!(editor); + let count = cx.count(); + let (view, doc) = current!(cx.editor); - match editor.clipboard_provider.get_contents(clipboard_type) { + match cx.editor.clipboard_provider.get_contents(clipboard_type) { Ok(contents) => { let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(doc.text(), selection, |range| { @@ -3658,18 +3692,20 @@ fn replace_selections_with_clipboard_impl( apply_transaction(&transaction, doc, view); doc.append_changes_to_history(view.id); - Ok(()) } - Err(e) => Err(e.context("Couldn't get system clipboard contents")), + Err(e) => return Err(e.context("Couldn't get system clipboard contents")), } + + exit_select_mode(cx); + Ok(()) } fn replace_selections_with_clipboard(cx: &mut Context) { - let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Clipboard, cx.count()); + let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Clipboard); } fn replace_selections_with_primary_clipboard(cx: &mut Context) { - let _ = replace_selections_with_clipboard_impl(cx.editor, ClipboardType::Selection, cx.count()); + let _ = replace_selections_with_clipboard_impl(cx, ClipboardType::Selection); } fn paste(cx: &mut Context, pos: Paste) { @@ -4005,11 +4041,13 @@ pub fn completion(cx: &mut Context) { }; if !prefix.is_empty() { - items.retain(|item| match &item.filter_text { - Some(t) => t.starts_with(&prefix), - None => item.label.starts_with(&prefix), + items.retain(|item| { + item.filter_text + .as_ref() + .unwrap_or(&item.label) + .starts_with(&prefix) }); - }; + } if items.is_empty() { // editor.set_error("No completion available".to_string()); @@ -4703,7 +4741,7 @@ fn shell_keep_pipe(cx: &mut Context) { for (i, range) in selection.ranges().iter().enumerate() { let fragment = range.slice(text); - let (_output, success) = match shell_impl(shell, input, Some(fragment)) { + let (_output, success) = match shell_impl(shell, input, Some(fragment.into())) { Ok(result) => result, Err(err) => { cx.editor.set_error(err.to_string()); @@ -4731,13 +4769,17 @@ fn shell_keep_pipe(cx: &mut Context) { ); } -fn shell_impl( +fn shell_impl(shell: &[String], cmd: &str, input: Option) -> anyhow::Result<(Tendril, bool)> { + tokio::task::block_in_place(|| helix_lsp::block_on(shell_impl_async(shell, cmd, input))) +} + +async fn shell_impl_async( shell: &[String], cmd: &str, - input: Option, + input: Option, ) -> anyhow::Result<(Tendril, bool)> { - use std::io::Write; - use std::process::{Command, Stdio}; + use std::process::Stdio; + use tokio::process::Command; ensure!(!shell.is_empty(), "No shell set"); let mut process = Command::new(&shell[0]); @@ -4749,6 +4791,8 @@ fn shell_impl( if input.is_some() || cfg!(windows) { process.stdin(Stdio::piped()); + } else { + process.stdin(Stdio::null()); } let mut process = match process.spawn() { @@ -4758,13 +4802,22 @@ fn shell_impl( return Err(e.into()); } }; - if let Some(input) = input { - let mut stdin = process.stdin.take().unwrap(); - for chunk in input.chunks() { - stdin.write_all(chunk.as_bytes())?; - } - } - let output = process.wait_with_output()?; + let output = if let Some(mut stdin) = process.stdin.take() { + let input_task = tokio::spawn(async move { + if let Some(input) = input { + helix_view::document::to_writer(&mut stdin, encoding::UTF_8, &input).await?; + } + Ok::<_, anyhow::Error>(()) + }); + let (output, _) = tokio::join! { + process.wait_with_output(), + input_task, + }; + output? + } else { + // Process has no stdin, so we just take the output + process.wait_with_output().await? + }; if !output.status.success() { if !output.stderr.is_empty() { @@ -4798,11 +4851,12 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { let selection = doc.selection(view.id); let mut changes = Vec::with_capacity(selection.len()); + let mut ranges = SmallVec::with_capacity(selection.len()); let text = doc.text().slice(..); for range in selection.ranges() { let fragment = range.slice(text); - let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment)) { + let (output, success) = match shell_impl(shell, cmd, pipe.then(|| fragment.into())) { Ok(result) => result, Err(err) => { cx.editor.set_error(err.to_string()); @@ -4821,11 +4875,13 @@ fn shell(cx: &mut compositor::Context, cmd: &str, behavior: &ShellBehavior) { ShellBehavior::Append => (range.to(), range.to()), _ => (range.from(), range.from()), }; + ranges.push(Range::new(to, to + output.chars().count())); changes.push((from, to, Some(output))); } if behavior != &ShellBehavior::Ignore { - let transaction = Transaction::change(doc.text(), changes.into_iter()); + let transaction = Transaction::change(doc.text(), changes.into_iter()) + .with_selection(Selection::new(ranges, selection.primary_index())); apply_transaction(&transaction, doc, view); doc.append_changes_to_history(view.id); } diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 316321f3..aa9dd69f 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -9,7 +9,7 @@ use tui::text::{Span, Spans}; use super::{align_view, push_jump, Align, Context, Editor, Open}; use helix_core::{path, Selection}; -use helix_view::{apply_transaction, editor::Action, theme::Style}; +use helix_view::{apply_transaction, document::Mode, editor::Action, theme::Style}; use crate::{ compositor::{self, Compositor}, @@ -57,7 +57,7 @@ impl ui::menu::Item for lsp::Location { // allocation, for `to_file_path`, else there will be two (2), with `to_string_lossy`. let mut write_path_to_res = || -> Option<()> { let path = self.uri.to_file_path().ok()?; - res.push_str(&path.strip_prefix(&cwdir).unwrap_or(&path).to_string_lossy()); + res.push_str(&path.strip_prefix(cwdir).unwrap_or(&path).to_string_lossy()); Some(()) }; write_path_to_res(); @@ -670,7 +670,7 @@ pub fn apply_document_resource_op(op: &lsp::ResourceOp) -> std::io::Result<()> { // Create directory if it does not exist if let Some(dir) = path.parent() { if !dir.is_dir() { - fs::create_dir_all(&dir)?; + fs::create_dir_all(dir)?; } } @@ -946,7 +946,7 @@ pub fn goto_reference(cx: &mut Context) { ); } -#[derive(PartialEq)] +#[derive(PartialEq, Eq)] pub enum SignatureHelpInvoked { Manual, Automatic, @@ -993,6 +993,14 @@ pub fn signature_help_impl(cx: &mut Context, invoked: SignatureHelpInvoked) { return; } + // If the signature help invocation is automatic, don't show it outside of Insert Mode: + // it very probably means the server was a little slow to respond and the user has + // already moved on to something else, making a signature help popup will just be an + // annoyance, see https://github.com/helix-editor/helix/issues/3112 + if !was_manually_invoked && editor.mode != Mode::Insert { + return; + } + let response = match response { // According to the spec the response should be None if there // are no signatures, but some servers don't follow this. diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index 6d5a95ab..15c91d62 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -797,16 +797,21 @@ fn theme( }; } PromptEvent::Validate => { - let theme_name = args.first().with_context(|| "Theme name not provided")?; - let theme = cx - .editor - .theme_loader - .load(theme_name) - .with_context(|| "Theme does not exist")?; - if !(true_color || theme.is_16_color()) { - bail!("Unsupported theme: theme requires true color support"); + if let Some(theme_name) = args.first() { + let theme = cx + .editor + .theme_loader + .load(theme_name) + .with_context(|| "Theme does not exist")?; + if !(true_color || theme.is_16_color()) { + bail!("Unsupported theme: theme requires true color support"); + } + cx.editor.set_theme(theme); + } else { + let name = cx.editor.theme.name().to_string(); + + cx.editor.set_status(name); } - cx.editor.set_theme(theme); } }; @@ -1054,6 +1059,24 @@ fn reload( }) } +/// Update the [`Document`] if it has been modified. +fn update( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let (_view, doc) = current!(cx.editor); + if doc.is_modified() { + write(cx, args, event) + } else { + Ok(()) + } +} + fn lsp_restart( cx: &mut compositor::Context, _args: &[Cow], @@ -1880,7 +1903,7 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "theme", aliases: &[], - doc: "Change the editor theme.", + doc: "Change the editor theme (show current theme if no name specified).", fun: theme, completer: Some(completers::theme), }, @@ -1989,6 +2012,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: reload, completer: None, }, + TypableCommand { + name: "update", + aliases: &[], + doc: "Write changes only if the file has been modified.", + fun: update, + completer: None, + }, TypableCommand { name: "lsp-restart", aliases: &[], @@ -2192,12 +2222,10 @@ pub(super) fn command_mode(cx: &mut Context) { static FUZZY_MATCHER: Lazy = Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); - // we use .this over split_whitespace() because we care about empty segments - let parts = input.split(' ').collect::>(); - // simple heuristic: if there's no just one part, complete command name. // if there's a space, per command completion kicks in. - if parts.len() <= 1 { + // we use .this over split_whitespace() because we care about empty segments + if input.split(' ').count() <= 1 { let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST .iter() .filter_map(|command| { @@ -2213,12 +2241,13 @@ pub(super) fn command_mode(cx: &mut Context) { .map(|(name, _)| (0.., name.into())) .collect() } else { + let parts = shellwords::shellwords(input); let part = parts.last().unwrap(); if let Some(typed::TypableCommand { completer: Some(completer), .. - }) = typed::TYPABLE_COMMAND_MAP.get(parts[0]) + }) = typed::TYPABLE_COMMAND_MAP.get(&parts[0] as &str) { completer(editor, part) .into_iter() diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index 616af8bd..00253388 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -92,11 +92,15 @@ impl Completion { pub fn new( editor: &Editor, - items: Vec, + mut items: Vec, offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, trigger_offset: usize, ) -> Self { + // Sort completion items according to their preselect status (given by the LSP server) + items.sort_by_key(|item| !item.preselect.unwrap_or(false)); + + // Then create the menu let menu = Menu::new(items, true, (), move |editor: &mut Editor, item, event| { fn item_to_transaction( doc: &Document, diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index b63989dc..d206cde0 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -25,7 +25,7 @@ use helix_view::{ keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{borrow::Cow, cmp::min, path::PathBuf}; +use std::{borrow::Cow, cmp::min, num::NonZeroUsize, path::PathBuf}; use tui::buffer::Buffer as Surface; @@ -229,16 +229,16 @@ impl EditorView { _theme: &Theme, ) -> Box + 'doc> { let text = doc.text().slice(..); - let last_line = std::cmp::min( - // Saturating subs to make it inclusive zero indexing. - (offset.row + height as usize).saturating_sub(1), - doc.text().len_lines().saturating_sub(1), - ); let range = { - // calculate viewport byte ranges - let start = text.line_to_byte(offset.row); - let end = text.line_to_byte(last_line + 1); + // Calculate viewport byte ranges: + // Saturating subs to make it inclusive zero indexing. + let last_line = doc.text().len_lines().saturating_sub(1); + let last_visible_line = (offset.row + height as usize) + .saturating_sub(1) + .min(last_line); + let start = text.line_to_byte(offset.row.min(last_line)); + let end = text.line_to_byte(last_visible_line + 1); start..end }; @@ -1025,37 +1025,40 @@ impl EditorView { } // special handling for repeat operator (key!('.'), _) if self.keymaps.pending().is_empty() => { - // first execute whatever put us into insert mode - self.last_insert.0.execute(cxt); - // then replay the inputs - for key in self.last_insert.1.clone() { - match key { - InsertEvent::Key(key) => self.insert_mode(cxt, key), - InsertEvent::CompletionApply(compl) => { - let (view, doc) = current!(cxt.editor); - - doc.restore(view); - - let text = doc.text().slice(..); - let cursor = doc.selection(view.id).primary().cursor(text); - - let shift_position = - |pos: usize| -> usize { pos + cursor - compl.trigger_offset }; - - let tx = Transaction::change( - doc.text(), - compl.changes.iter().cloned().map(|(start, end, t)| { - (shift_position(start), shift_position(end), t) - }), - ); - apply_transaction(&tx, doc, view); - } - InsertEvent::TriggerCompletion => { - let (_, doc) = current!(cxt.editor); - doc.savepoint(); + for _ in 0..cxt.editor.count.map_or(1, NonZeroUsize::into) { + // first execute whatever put us into insert mode + self.last_insert.0.execute(cxt); + // then replay the inputs + for key in self.last_insert.1.clone() { + match key { + InsertEvent::Key(key) => self.insert_mode(cxt, key), + InsertEvent::CompletionApply(compl) => { + let (view, doc) = current!(cxt.editor); + + doc.restore(view); + + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + + let shift_position = + |pos: usize| -> usize { pos + cursor - compl.trigger_offset }; + + let tx = Transaction::change( + doc.text(), + compl.changes.iter().cloned().map(|(start, end, t)| { + (shift_position(start), shift_position(end), t) + }), + ); + apply_transaction(&tx, doc, view); + } + InsertEvent::TriggerCompletion => { + let (_, doc) = current!(cxt.editor); + doc.savepoint(); + } } } } + cxt.editor.count = None; } _ => { // set the count diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index 9ae12a94..df03baf5 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -40,7 +40,7 @@ impl Item for PathBuf { type Data = PathBuf; fn label(&self, root_path: &Self::Data) -> Spans { - self.strip_prefix(&root_path) + self.strip_prefix(root_path) .unwrap_or(self) .to_string_lossy() .into() @@ -118,7 +118,8 @@ impl Menu { .map(|score| (index, score)) }), ); - self.matches.sort_unstable_by_key(|(_, score)| -score); + // Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority + self.matches.sort_by_key(|(_, score)| -score); // reset cursor position self.cursor = None; diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index c7149c61..2505f219 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -248,8 +248,14 @@ impl Component for FilePicker { let offset = Position::new(first_line, 0); - let highlights = + let mut highlights = EditorView::doc_syntax_highlights(doc, offset, area.height, &cx.editor.theme); + for spans in EditorView::doc_diagnostics_highlights(doc, &cx.editor.theme) { + if spans.is_empty() { + continue; + } + highlights = Box::new(helix_core::syntax::merge(highlights, spans)); + } EditorView::render_text_highlights( doc, offset, diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index db3bd62d..ca2872a7 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -1,5 +1,6 @@ use crate::compositor::{Component, Compositor, Context, Event, EventResult}; use crate::{alt, ctrl, key, shift, ui}; +use helix_core::shellwords; use helix_view::input::KeyEvent; use helix_view::keyboard::KeyCode; use std::{borrow::Cow, ops::RangeFrom}; @@ -31,7 +32,7 @@ pub struct Prompt { next_char_handler: Option, } -#[derive(Clone, Copy, PartialEq)] +#[derive(Clone, Copy, PartialEq, Eq)] pub enum PromptEvent { /// The prompt input has been updated. Update, @@ -335,7 +336,10 @@ impl Prompt { let (range, item) = &self.completion[index]; - self.line.replace_range(range.clone(), item); + // since we are using shellwords to parse arguments, make sure + // that whitespace in files is properly escaped. + let item = shellwords::escape(item); + self.line.replace_range(range.clone(), &item); self.move_end(); } @@ -404,7 +408,7 @@ impl Prompt { surface.set_stringn( area.x + col * (1 + col_width), area.y + row, - &completion, + completion, col_width.saturating_sub(1) as usize, color, ); diff --git a/helix-term/tests/test/commands.rs b/helix-term/tests/test/commands.rs index e24ee3e0..aadf104b 100644 --- a/helix-term/tests/test/commands.rs +++ b/helix-term/tests/test/commands.rs @@ -1,6 +1,7 @@ use std::ops::RangeInclusive; use helix_core::diagnostic::Severity; +use helix_term::application::Application; use super::*; @@ -133,3 +134,62 @@ async fn test_selection_duplication() -> anyhow::Result<()> { .await?; Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn test_goto_file_impl() -> anyhow::Result<()> { + let file = tempfile::NamedTempFile::new()?; + + fn match_paths(app: &Application, matches: Vec<&str>) -> usize { + app.editor + .documents() + .filter_map(|d| d.path()?.file_name()) + .filter(|n| matches.iter().any(|m| *m == n.to_string_lossy())) + .count() + } + + // Single selection + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("ione.js%gf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one.js"])); + }), + false, + ) + .await?; + + // Multiple selection + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("ione.jstwo.js%gf"), + Some(&|app| { + assert_eq!(2, match_paths(app, vec!["one.js", "two.js"])); + }), + false, + ) + .await?; + + // Cursor on first quote + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("iimport 'one.js'B;gf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one.js"])); + }), + false, + ) + .await?; + + // Cursor on last quote + test_key_sequence( + &mut AppBuilder::new().with_file(file.path(), None).build()?, + Some("iimport 'one.js'bgf"), + Some(&|app| { + assert_eq!(1, match_paths(app, vec!["one.js"])); + }), + false, + ) + .await?; + + Ok(()) +} diff --git a/helix-tui/src/buffer.rs b/helix-tui/src/buffer.rs index 424e6d32..5169196a 100644 --- a/helix-tui/src/buffer.rs +++ b/helix-tui/src/buffer.rs @@ -6,7 +6,7 @@ use unicode_segmentation::UnicodeSegmentation; use helix_view::graphics::{Color, Modifier, Rect, Style, UnderlineStyle}; /// A buffer cell -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Cell { pub symbol: String, pub fg: Color, @@ -119,7 +119,7 @@ impl Default for Cell { /// buf[(5, 0)].set_char('x'); /// assert_eq!(buf[(5, 0)].symbol, "x"); /// ``` -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Buffer { /// The area represented by this buffer pub area: Rect, @@ -137,7 +137,7 @@ impl Buffer { /// Returns a Buffer with all cells initialized with the attributes of the given Cell pub fn filled(area: Rect, cell: &Cell) -> Buffer { - let size = area.area() as usize; + let size = area.area(); let mut content = Vec::with_capacity(size); for _ in 0..size { content.push(cell.clone()); @@ -239,7 +239,7 @@ impl Buffer { y, self.area ); - ((y - self.area.y) * self.area.width + (x - self.area.x)) as usize + ((y - self.area.y) as usize) * (self.area.width as usize) + ((x - self.area.x) as usize) } /// Returns the index in the Vec for the given global (x, y) coordinates, @@ -278,8 +278,8 @@ impl Buffer { self.content.len() ); ( - self.area.x + i as u16 % self.area.width, - self.area.y + i as u16 / self.area.width, + (self.area.x as usize + (i % self.area.width as usize)) as u16, + (self.area.y as usize + (i / self.area.width as usize)) as u16, ) } @@ -480,7 +480,7 @@ impl Buffer { /// Resize the buffer so that the mapped area matches the given area and that the buffer /// length is equal to area.width * area.height pub fn resize(&mut self, area: Rect) { - let length = area.area() as usize; + let length = area.area(); if self.content.len() > length { self.content.truncate(length); } else { @@ -587,8 +587,8 @@ impl Buffer { let mut to_skip: usize = 0; for (i, (current, previous)) in next_buffer.iter().zip(previous_buffer.iter()).enumerate() { if (current != previous || invalidated > 0) && to_skip == 0 { - let x = i as u16 % width; - let y = i as u16 / width; + let x = (i % width as usize) as u16; + let y = (i / width as usize) as u16; updates.push((x, y, &next_buffer[i])); } diff --git a/helix-tui/src/layout.rs b/helix-tui/src/layout.rs index 7c72a778..1f3ddc6e 100644 --- a/helix-tui/src/layout.rs +++ b/helix-tui/src/layout.rs @@ -46,7 +46,7 @@ impl Constraint { } } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Alignment { Left, Center, diff --git a/helix-tui/src/text.rs b/helix-tui/src/text.rs index 1bfe5ee1..ccdafad5 100644 --- a/helix-tui/src/text.rs +++ b/helix-tui/src/text.rs @@ -53,14 +53,14 @@ use std::borrow::Cow; use unicode_segmentation::UnicodeSegmentation; /// A grapheme associated to a style. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct StyledGrapheme<'a> { pub symbol: &'a str, pub style: Style, } /// A string where all graphemes have the same style. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Span<'a> { pub content: Cow<'a, str>, pub style: Style, @@ -209,7 +209,7 @@ impl<'a> From> for Span<'a> { } /// A string composed of clusters of graphemes, each with their own style. -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Spans<'a>(pub Vec>); impl<'a> Spans<'a> { @@ -297,7 +297,7 @@ impl<'a> From<&Spans<'a>> for String { /// text.extend(Text::styled("Some more lines\nnow with more style!", style)); /// assert_eq!(6, text.height()); /// ``` -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Text<'a> { pub lines: Vec>, } diff --git a/helix-tui/src/widgets/block.rs b/helix-tui/src/widgets/block.rs index bd025a31..98f84abe 100644 --- a/helix-tui/src/widgets/block.rs +++ b/helix-tui/src/widgets/block.rs @@ -7,7 +7,7 @@ use crate::{ use helix_view::graphics::{Rect, Style}; /// Border render type. Defaults to [`BorderType::Plain`]. -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum BorderType { Plain, Rounded, @@ -47,7 +47,7 @@ impl Default for BorderType { /// .border_type(BorderType::Rounded) /// .style(Style::default().bg(Color::Black)); /// ``` -#[derive(Debug, Default, Clone, PartialEq)] +#[derive(Debug, Default, Clone, PartialEq, Eq)] pub struct Block<'a> { /// Optional title place on the upper left of the block title: Option>, @@ -187,16 +187,8 @@ impl<'a> Widget for Block<'a> { } if let Some(title) = self.title { - let lx = if self.borders.intersects(Borders::LEFT) { - 1 - } else { - 0 - }; - let rx = if self.borders.intersects(Borders::RIGHT) { - 1 - } else { - 0 - }; + let lx = u16::from(self.borders.intersects(Borders::LEFT)); + let rx = u16::from(self.borders.intersects(Borders::RIGHT)); let width = area.width.saturating_sub(lx).saturating_sub(rx); buf.set_spans(area.left() + lx, area.top(), &title, width); } diff --git a/helix-tui/src/widgets/table.rs b/helix-tui/src/widgets/table.rs index eb03704e..a8f428a7 100644 --- a/helix-tui/src/widgets/table.rs +++ b/helix-tui/src/widgets/table.rs @@ -34,7 +34,7 @@ use std::collections::HashMap; /// /// You can apply a [`Style`] on the entire [`Cell`] using [`Cell::style`] or rely on the styling /// capabilities of [`Text`]. -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Cell<'a> { pub content: Text<'a>, style: Style, @@ -79,7 +79,7 @@ where /// ``` /// /// By default, a row has a height of 1 but you can change this using [`Row::height`]. -#[derive(Debug, Clone, PartialEq, Default)] +#[derive(Debug, Clone, PartialEq, Eq, Default)] pub struct Row<'a> { pub cells: Vec>, height: u16, @@ -179,7 +179,7 @@ impl<'a> Row<'a> { /// // ...and potentially show a symbol in front of the selection. /// .highlight_symbol(">>"); /// ``` -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct Table<'a> { /// A block to wrap the widget in block: Option>, diff --git a/helix-tui/tests/terminal.rs b/helix-tui/tests/terminal.rs index 3dd3b0b0..2824c9f2 100644 --- a/helix-tui/tests/terminal.rs +++ b/helix-tui/tests/terminal.rs @@ -4,12 +4,12 @@ use helix_tui::{ }; #[test] -fn terminal_buffer_size_should_be_limited() { +fn terminal_buffer_size_should_not_be_limited() { let backend = TestBackend::new(400, 400); let terminal = Terminal::new(backend).unwrap(); let size = terminal.backend().size().unwrap(); - assert_eq!(size.width, 255); - assert_eq!(size.height, 255); + assert_eq!(size.width, 400); + assert_eq!(size.height, 400); } // #[test] diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index b96a537d..a2a88001 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -23,7 +23,7 @@ helix-dap = { version = "0.6", path = "../helix-dap" } crossterm = { version = "0.25", optional = true } # Conversion traits -once_cell = "1.15" +once_cell = "1.16" url = "2" arc-swap = { version = "1.5.1" } diff --git a/helix-view/src/base64.rs b/helix-view/src/base64.rs new file mode 100644 index 00000000..a0dc167f --- /dev/null +++ b/helix-view/src/base64.rs @@ -0,0 +1,163 @@ +// A minimal base64 implementation to keep from pulling in a crate for just that. It's based on +// https://github.com/marshallpierce/rust-base64 but without all the customization options. +// The biggest portion comes from +// https://github.com/marshallpierce/rust-base64/blob/a675443d327e175f735a37f574de803d6a332591/src/engine/naive.rs#L42 +// Thanks, rust-base64! + +// The MIT License (MIT) + +// Copyright (c) 2015 Alice Maz + +// Permission is hereby granted, free of charge, to any person obtaining a copy +// of this software and associated documentation files (the "Software"), to deal +// in the Software without restriction, including without limitation the rights +// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +// copies of the Software, and to permit persons to whom the Software is +// furnished to do so, subject to the following conditions: + +// The above copyright notice and this permission notice shall be included in +// all copies or substantial portions of the Software. + +// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +// THE SOFTWARE. + +use std::ops::{BitAnd, BitOr, Shl, Shr}; + +const PAD_BYTE: u8 = b'='; +const ENCODE_TABLE: &[u8] = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".as_bytes(); +const LOW_SIX_BITS: u32 = 0x3F; + +pub fn encode(input: &[u8]) -> String { + let rem = input.len() % 3; + let complete_chunks = input.len() / 3; + let remainder_chunk = if rem == 0 { 0 } else { 1 }; + let encoded_size = (complete_chunks + remainder_chunk) * 4; + + let mut output = vec![0; encoded_size]; + + // complete chunks first + let complete_chunk_len = input.len() - rem; + + let mut input_index = 0_usize; + let mut output_index = 0_usize; + while input_index < complete_chunk_len { + let chunk = &input[input_index..input_index + 3]; + + // populate low 24 bits from 3 bytes + let chunk_int: u32 = + (chunk[0] as u32).shl(16) | (chunk[1] as u32).shl(8) | (chunk[2] as u32); + // encode 4x 6-bit output bytes + output[output_index] = ENCODE_TABLE[chunk_int.shr(18) as usize]; + output[output_index + 1] = ENCODE_TABLE[chunk_int.shr(12_u8).bitand(LOW_SIX_BITS) as usize]; + output[output_index + 2] = ENCODE_TABLE[chunk_int.shr(6_u8).bitand(LOW_SIX_BITS) as usize]; + output[output_index + 3] = ENCODE_TABLE[chunk_int.bitand(LOW_SIX_BITS) as usize]; + + input_index += 3; + output_index += 4; + } + + // then leftovers + if rem == 2 { + let chunk = &input[input_index..input_index + 2]; + + // high six bits of chunk[0] + output[output_index] = ENCODE_TABLE[chunk[0].shr(2) as usize]; + // bottom 2 bits of [0], high 4 bits of [1] + output[output_index + 1] = ENCODE_TABLE + [(chunk[0].shl(4_u8).bitor(chunk[1].shr(4_u8)) as u32).bitand(LOW_SIX_BITS) as usize]; + // bottom 4 bits of [1], with the 2 bottom bits as zero + output[output_index + 2] = + ENCODE_TABLE[(chunk[1].shl(2_u8) as u32).bitand(LOW_SIX_BITS) as usize]; + output[output_index + 3] = PAD_BYTE; + } else if rem == 1 { + let byte = input[input_index]; + output[output_index] = ENCODE_TABLE[byte.shr(2) as usize]; + output[output_index + 1] = + ENCODE_TABLE[(byte.shl(4_u8) as u32).bitand(LOW_SIX_BITS) as usize]; + output[output_index + 2] = PAD_BYTE; + output[output_index + 3] = PAD_BYTE; + } + String::from_utf8(output).expect("Invalid UTF8") +} + +#[cfg(test)] +mod tests { + fn compare_encode(expected: &str, target: &[u8]) { + assert_eq!(expected, super::encode(target)); + } + + #[test] + fn encode_rfc4648_0() { + compare_encode("", b""); + } + + #[test] + fn encode_rfc4648_1() { + compare_encode("Zg==", b"f"); + } + + #[test] + fn encode_rfc4648_2() { + compare_encode("Zm8=", b"fo"); + } + + #[test] + fn encode_rfc4648_3() { + compare_encode("Zm9v", b"foo"); + } + + #[test] + fn encode_rfc4648_4() { + compare_encode("Zm9vYg==", b"foob"); + } + + #[test] + fn encode_rfc4648_5() { + compare_encode("Zm9vYmE=", b"fooba"); + } + + #[test] + fn encode_rfc4648_6() { + compare_encode("Zm9vYmFy", b"foobar"); + } + + #[test] + fn encode_all_ascii() { + let mut ascii = Vec::::with_capacity(128); + + for i in 0..128 { + ascii.push(i); + } + + compare_encode( + "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\ + D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn8\ + =", + &ascii, + ); + } + + #[test] + fn encode_all_bytes() { + let mut bytes = Vec::::with_capacity(256); + + for i in 0..255 { + bytes.push(i); + } + bytes.push(255); //bug with "overflowing" ranges? + + compare_encode( + "AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8gISIjJCUmJygpKissLS4vMDEyMzQ1Njc4OTo7P\ + D0+P0BBQkNERUZHSElKS0xNTk9QUVJTVFVWV1hZWltcXV5fYGFiY2RlZmdoaWprbG1ub3BxcnN0dXZ3eHl6e3x9fn\ + +AgYKDhIWGh4iJiouMjY6PkJGSk5SVlpeYmZqbnJ2en6ChoqOkpaanqKmqq6ytrq+wsbKztLW2t7i5uru8vb6\ + /wMHCw8TFxsfIycrLzM3Oz9DR0tPU1dbX2Nna29zd3t/g4eLj5OXm5+jp6uvs7e7v8PHy8/T19vf4+fr7/P3+/w==", + &bytes, + ); + } +} diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index ad6f621a..19fade69 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -3,6 +3,7 @@ use anyhow::Result; use std::borrow::Cow; +#[derive(Clone, Copy, Debug)] pub enum ClipboardType { Clipboard, Selection, @@ -72,45 +73,48 @@ pub fn get_clipboard_provider() -> Box { #[cfg(target_os = "macos")] pub fn get_clipboard_provider() -> Box { - use provider::command::exists; + use crate::env::binary_exists; - if exists("pbcopy") && exists("pbpaste") { + if binary_exists("pbcopy") && binary_exists("pbpaste") { command_provider! { paste => "pbpaste"; copy => "pbcopy"; } } else { - Box::new(provider::NopProvider::new()) + Box::new(provider::FallbackProvider::new()) } } #[cfg(target_os = "wasm32")] pub fn get_clipboard_provider() -> Box { // TODO: - Box::new(provider::NopProvider::new()) + Box::new(provider::FallbackProvider::new()) } #[cfg(not(any(windows, target_os = "wasm32", target_os = "macos")))] pub fn get_clipboard_provider() -> Box { - use provider::command::{env_var_is_set, exists, is_exit_success}; + use crate::env::{binary_exists, env_var_is_set}; + use provider::command::is_exit_success; // TODO: support for user-defined provider, probably when we have plugin support by setting a // variable? - if env_var_is_set("WAYLAND_DISPLAY") && exists("wl-copy") && exists("wl-paste") { + if env_var_is_set("WAYLAND_DISPLAY") && binary_exists("wl-copy") && binary_exists("wl-paste") { command_provider! { paste => "wl-paste", "--no-newline"; copy => "wl-copy", "--type", "text/plain"; primary_paste => "wl-paste", "-p", "--no-newline"; primary_copy => "wl-copy", "-p", "--type", "text/plain"; } - } else if env_var_is_set("DISPLAY") && exists("xclip") { + } else if env_var_is_set("DISPLAY") && binary_exists("xclip") { command_provider! { paste => "xclip", "-o", "-selection", "clipboard"; copy => "xclip", "-i", "-selection", "clipboard"; primary_paste => "xclip", "-o"; primary_copy => "xclip", "-i"; } - } else if env_var_is_set("DISPLAY") && exists("xsel") && is_exit_success("xsel", &["-o", "-b"]) + } else if env_var_is_set("DISPLAY") + && binary_exists("xsel") + && is_exit_success("xsel", &["-o", "-b"]) { // FIXME: check performance of is_exit_success command_provider! { @@ -119,43 +123,78 @@ pub fn get_clipboard_provider() -> Box { primary_paste => "xsel", "-o"; primary_copy => "xsel", "-i"; } - } else if exists("win32yank.exe") { + } else if binary_exists("win32yank.exe") { command_provider! { paste => "win32yank.exe", "-o", "--lf"; copy => "win32yank.exe", "-i", "--crlf"; } - } else if exists("termux-clipboard-set") && exists("termux-clipboard-get") { + } else if binary_exists("termux-clipboard-set") && binary_exists("termux-clipboard-get") { command_provider! { paste => "termux-clipboard-get"; copy => "termux-clipboard-set"; } - } else if env_var_is_set("TMUX") && exists("tmux") { + } else if env_var_is_set("TMUX") && binary_exists("tmux") { command_provider! { paste => "tmux", "save-buffer", "-"; copy => "tmux", "load-buffer", "-"; } } else { - Box::new(provider::NopProvider::new()) + Box::new(provider::FallbackProvider::new()) } } +#[cfg(not(target_os = "windows"))] pub mod provider { use super::{ClipboardProvider, ClipboardType}; use anyhow::Result; use std::borrow::Cow; - #[cfg(not(target_os = "windows"))] + #[cfg(feature = "term")] + mod osc52 { + use {super::ClipboardType, crate::base64, crossterm}; + + #[derive(Debug)] + pub struct SetClipboardCommand { + encoded_content: String, + clipboard_type: ClipboardType, + } + + impl SetClipboardCommand { + pub fn new(content: &str, clipboard_type: ClipboardType) -> Self { + Self { + encoded_content: base64::encode(content.as_bytes()), + clipboard_type, + } + } + } + + impl crossterm::Command for SetClipboardCommand { + fn write_ansi(&self, f: &mut impl std::fmt::Write) -> std::fmt::Result { + let kind = match &self.clipboard_type { + ClipboardType::Clipboard => "c", + ClipboardType::Selection => "p", + }; + // Send an OSC 52 set command: https://terminalguide.namepad.de/seq/osc-52/ + write!(f, "\x1b]52;{};{}\x1b\\", kind, &self.encoded_content) + } + } + } + #[derive(Debug)] - pub struct NopProvider { + pub struct FallbackProvider { buf: String, primary_buf: String, } - #[cfg(not(target_os = "windows"))] - impl NopProvider { + impl FallbackProvider { pub fn new() -> Self { + #[cfg(feature = "term")] + log::debug!( + "No native clipboard provider found. Yanking by OSC 52 and pasting will be internal to Helix" + ); + #[cfg(not(feature = "term"))] log::warn!( - "No clipboard provider found! Yanking and pasting will be internal to Helix" + "No native clipboard provider found! Yanking and pasting will be internal to Helix" ); Self { buf: String::new(), @@ -164,20 +203,27 @@ pub mod provider { } } - #[cfg(not(target_os = "windows"))] - impl Default for NopProvider { + impl Default for FallbackProvider { fn default() -> Self { Self::new() } } - #[cfg(not(target_os = "windows"))] - impl ClipboardProvider for NopProvider { + impl ClipboardProvider for FallbackProvider { + #[cfg(feature = "term")] + fn name(&self) -> Cow { + Cow::Borrowed("termcode") + } + + #[cfg(not(feature = "term"))] fn name(&self) -> Cow { Cow::Borrowed("none") } fn get_contents(&self, clipboard_type: ClipboardType) -> Result { + // This is the same noop if term is enabled or not. + // We don't use the get side of OSC 52 as it isn't often enabled, it's a security hole, + // and it would require this to be async to listen for the response let value = match clipboard_type { ClipboardType::Clipboard => self.buf.clone(), ClipboardType::Selection => self.primary_buf.clone(), @@ -187,6 +233,12 @@ pub mod provider { } fn set_contents(&mut self, content: String, clipboard_type: ClipboardType) -> Result<()> { + #[cfg(feature = "term")] + crossterm::execute!( + std::io::stdout(), + osc52::SetClipboardCommand::new(&content, clipboard_type) + )?; + // Set our internal variables to use in get_content regardless of using OSC 52 match clipboard_type { ClipboardType::Clipboard => self.buf = content, ClipboardType::Selection => self.primary_buf = content, @@ -195,52 +247,11 @@ pub mod provider { } } - #[cfg(target_os = "windows")] - #[derive(Default, Debug)] - pub struct WindowsProvider; - - #[cfg(target_os = "windows")] - impl ClipboardProvider for WindowsProvider { - fn name(&self) -> Cow { - log::info!("Using clipboard-win to interact with the system clipboard"); - Cow::Borrowed("clipboard-win") - } - - fn get_contents(&self, clipboard_type: ClipboardType) -> Result { - match clipboard_type { - ClipboardType::Clipboard => { - let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?; - Ok(contents) - } - ClipboardType::Selection => Ok(String::new()), - } - } - - fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> { - match clipboard_type { - ClipboardType::Clipboard => { - clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?; - } - ClipboardType::Selection => {} - }; - Ok(()) - } - } - #[cfg(not(target_arch = "wasm32"))] pub mod command { use super::*; use anyhow::{bail, Context as _, Result}; - pub fn exists(executable_name: &str) -> bool { - which::which(executable_name).is_ok() - } - - #[cfg(not(windows))] - pub fn env_var_is_set(env_var_name: &str) -> bool { - std::env::var_os(env_var_name).is_some() - } - #[cfg(not(any(windows, target_os = "macos")))] pub fn is_exit_success(program: &str, args: &[&str]) -> bool { std::process::Command::new(program) @@ -343,3 +354,40 @@ pub mod provider { } } } + +#[cfg(target_os = "windows")] +mod provider { + use super::{ClipboardProvider, ClipboardType}; + use anyhow::Result; + use std::borrow::Cow; + + #[derive(Default, Debug)] + pub struct WindowsProvider; + + impl ClipboardProvider for WindowsProvider { + fn name(&self) -> Cow { + log::info!("Using clipboard-win to interact with the system clipboard"); + Cow::Borrowed("clipboard-win") + } + + fn get_contents(&self, clipboard_type: ClipboardType) -> Result { + match clipboard_type { + ClipboardType::Clipboard => { + let contents = clipboard_win::get_clipboard(clipboard_win::formats::Unicode)?; + Ok(contents) + } + ClipboardType::Selection => Ok(String::new()), + } + } + + fn set_contents(&mut self, contents: String, clipboard_type: ClipboardType) -> Result<()> { + match clipboard_type { + ClipboardType::Clipboard => { + clipboard_win::set_clipboard(clipboard_win::formats::Unicode, contents)?; + } + ClipboardType::Selection => {} + }; + Ok(()) + } + } +} diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index 79fc80d5..613fc559 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -111,21 +111,21 @@ impl Default for FilePickerConfig { } } -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ExplorerStyle { Tree, List, } -#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Copy, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case")] pub enum ExplorerPosition { Embed, Overlay, } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, Eq, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct ExplorerConfig { pub style: ExplorerStyle, @@ -162,7 +162,7 @@ impl Default for ExplorerConfig { } } -#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] +#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "kebab-case", default, deny_unknown_fields)] pub struct Config { /// Padding to keep between the edge of the screen and the cursor when scrolling. Defaults to 5. @@ -241,9 +241,9 @@ pub struct TerminalConfig { #[cfg(windows)] pub fn get_terminal_provider() -> Option { - use crate::clipboard::provider::command::exists; + use crate::env::binary_exists; - if exists("wt") { + if binary_exists("wt") { return Some(TerminalConfig { command: "wt".to_string(), args: vec![ @@ -264,16 +264,16 @@ pub fn get_terminal_provider() -> Option { #[cfg(not(any(windows, target_os = "wasm32")))] pub fn get_terminal_provider() -> Option { - use crate::clipboard::provider::command::{env_var_is_set, exists}; + use crate::env::{binary_exists, env_var_is_set}; - if env_var_is_set("TMUX") && exists("tmux") { + if env_var_is_set("TMUX") && binary_exists("tmux") { return Some(TerminalConfig { command: "tmux".to_string(), args: vec!["split-window".to_string()], }); } - if env_var_is_set("WEZTERM_UNIX_SOCKET") && exists("wezterm") { + if env_var_is_set("WEZTERM_UNIX_SOCKET") && binary_exists("wezterm") { return Some(TerminalConfig { command: "wezterm".to_string(), args: vec!["cli".to_string(), "split-pane".to_string()], @@ -409,7 +409,7 @@ pub enum StatusLineElement { // Cursor shape is read and used on every rendered frame and so needs // to be fast. Therefore we avoid a hashmap and use an enum indexed array. -#[derive(Debug, Clone, PartialEq)] +#[derive(Debug, Clone, PartialEq, Eq)] pub struct CursorShapeConfig([CursorKind; 3]); impl CursorShapeConfig { @@ -1328,9 +1328,11 @@ impl Editor { pub fn focus(&mut self, view_id: ViewId) { let prev_id = std::mem::replace(&mut self.tree.focus, view_id); - // if leaving the view: mode should reset + // if leaving the view: mode should reset and the cursor should be + // within view if prev_id != view_id { self.mode = Mode::Normal; + self.ensure_cursor_in_view(view_id); } } @@ -1339,9 +1341,11 @@ impl Editor { self.tree.focus_next(); let id = self.tree.focus; - // if leaving the view: mode should reset + // if leaving the view: mode should reset and the cursor should be + // within view if prev_id != id { self.mode = Mode::Normal; + self.ensure_cursor_in_view(id); } } diff --git a/helix-view/src/env.rs b/helix-view/src/env.rs new file mode 100644 index 00000000..c68cc609 --- /dev/null +++ b/helix-view/src/env.rs @@ -0,0 +1,8 @@ +pub fn binary_exists(binary_name: &str) -> bool { + which::which(binary_name).is_ok() +} + +#[cfg(not(windows))] +pub fn env_var_is_set(env_var_name: &str) -> bool { + std::env::var_os(env_var_name).is_some() +} diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index 4374a537..9264c50f 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -5,7 +5,7 @@ use std::{ str::FromStr, }; -#[derive(Debug, Clone, Copy, PartialEq, Deserialize, Serialize)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] #[serde(rename_all = "lowercase")] /// UNSTABLE pub enum CursorKind { @@ -95,31 +95,19 @@ pub struct Rect { } impl Rect { - /// Creates a new rect, with width and height limited to keep the area under max u16. - /// If clipped, aspect ratio will be preserved. + /// Creates a new rect, with width and height pub fn new(x: u16, y: u16, width: u16, height: u16) -> Rect { - let max_area = u16::max_value(); - let (clipped_width, clipped_height) = - if u32::from(width) * u32::from(height) > u32::from(max_area) { - let aspect_ratio = f64::from(width) / f64::from(height); - let max_area_f = f64::from(max_area); - let height_f = (max_area_f / aspect_ratio).sqrt(); - let width_f = height_f * aspect_ratio; - (width_f as u16, height_f as u16) - } else { - (width, height) - }; Rect { x, y, - width: clipped_width, - height: clipped_height, + width, + height, } } #[inline] - pub fn area(self) -> u16 { - self.width * self.height + pub fn area(self) -> usize { + (self.width as usize) * (self.height as usize) } #[inline] @@ -262,7 +250,7 @@ impl Rect { } } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub enum Color { Reset, @@ -315,7 +303,7 @@ impl From for crossterm::style::Color { } } -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum UnderlineStyle { Reset, Line, @@ -461,7 +449,7 @@ impl FromStr for Modifier { /// buffer[(0, 0)].style(), /// ); /// ``` -#[derive(Debug, Clone, Copy, PartialEq)] +#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))] pub struct Style { pub fg: Option, @@ -630,33 +618,6 @@ impl Style { mod tests { use super::*; - #[test] - fn test_rect_size_truncation() { - for width in 256u16..300u16 { - for height in 256u16..300u16 { - let rect = Rect::new(0, 0, width, height); - rect.area(); // Should not panic. - assert!(rect.width < width || rect.height < height); - // The target dimensions are rounded down so the math will not be too precise - // but let's make sure the ratios don't diverge crazily. - assert!( - (f64::from(rect.width) / f64::from(rect.height) - - f64::from(width) / f64::from(height)) - .abs() - < 1.0 - ) - } - } - - // One dimension below 255, one above. Area above max u16. - let width = 900; - let height = 100; - let rect = Rect::new(0, 0, width, height); - assert_ne!(rect.width, 900); - assert_ne!(rect.height, 100); - assert!(rect.width < width || rect.height < height); - } - #[test] fn test_rect_size_preservation() { for width in 0..256u16 { diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index 276be441..52044ac7 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -4,12 +4,14 @@ pub mod macros; pub mod clipboard; pub mod document; pub mod editor; +pub mod env; pub mod graphics; pub mod gutter; pub mod handlers { pub mod dap; pub mod lsp; } +pub mod base64; pub mod info; pub mod input; pub mod keyboard; diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index 4bdf36af..71b96396 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -14,19 +14,14 @@ use toml::{map::Map, Value}; use crate::graphics::UnderlineStyle; pub use crate::graphics::{Color, Modifier, Style}; -pub static DEFAULT_THEME: Lazy = Lazy::new(|| { - // let raw_theme: Value = toml::from_slice(include_bytes!("../../theme.toml")) - // .expect("Failed to parse default theme"); - // Theme::from(raw_theme) - - toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") +pub static DEFAULT_THEME: Lazy = Lazy::new(|| Theme { + name: "default".into(), + ..toml::from_slice(include_bytes!("../../theme.toml")).expect("Failed to parse default theme") }); -pub static BASE16_DEFAULT_THEME: Lazy = Lazy::new(|| { - // let raw_theme: Value = toml::from_slice(include_bytes!("../../base16_theme.toml")) - // .expect("Failed to parse base 16 default theme"); - // Theme::from(raw_theme) - toml::from_slice(include_bytes!("../../base16_theme.toml")) +pub static BASE16_DEFAULT_THEME: Lazy = Lazy::new(|| Theme { + name: "base16_theme".into(), + ..toml::from_slice(include_bytes!("../../base16_theme.toml")) .expect("Failed to parse base 16 default theme") }); @@ -53,10 +48,12 @@ impl Loader { return Ok(self.base16_default()); } - let value = self.load_theme(name, name, false)?; - let theme = Theme::deserialize(value)?; + let theme = self.load_theme(name, name, false).map(Theme::from)?; - Ok(theme) + Ok(Theme { + name: name.into(), + ..theme + }) } // load the theme and its parent recursively and merge them @@ -183,8 +180,10 @@ impl Loader { } } -#[derive(Clone, Debug)] +#[derive(Clone, Debug, Default)] pub struct Theme { + name: String, + // UI styles are stored in a HashMap styles: HashMap, // tree-sitter highlight styles are stored in a Vec to optimize lookups @@ -193,25 +192,26 @@ pub struct Theme { rainbow_length: usize, } -// impl From for Theme { -// fn from(value: Value) -> Self { -// let values: Result> = -// toml::from_str(&value.to_string()).context("Failed to load theme"); - -// let (styles, scopes, highlights) = build_theme_values(values); -// let rainbow_length = styles -// .iter() -// .filter(|s| s.0.starts_with("rainbow.")) -// .count(); - -// Self { -// styles, -// scopes, -// highlights, -// rainbow_length, -// } -// } -// } +impl From for Theme { + fn from(value: Value) -> Self { + let values: Result> = + toml::from_str(&value.to_string()).context("Failed to load theme"); + + let (styles, scopes, highlights) = build_theme_values(values); + let rainbow_length = styles + .iter() + .filter(|s| s.0.starts_with("rainbow.")) + .count(); + + Self { + styles, + scopes, + highlights, + rainbow_length, + ..Default::default() + } + } +} impl<'de> Deserialize<'de> for Theme { fn deserialize(deserializer: D) -> Result @@ -277,16 +277,59 @@ impl<'de> Deserialize<'de> for Theme { scopes, highlights, rainbow_length, + ..Default::default() }) } } +fn build_theme_values( + values: Result>, +) -> (HashMap, Vec, Vec