diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 44d267884d23..d7d7d47e02ca 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -37,7 +37,7 @@ jobs: uses: actions/checkout@v3 - name: Install stable toolchain - uses: dtolnay/rust-toolchain@1.63 + uses: dtolnay/rust-toolchain@1.65 - uses: Swatinem/rust-cache@v2 @@ -66,7 +66,7 @@ jobs: uses: actions/checkout@v3 - name: Install stable toolchain - uses: dtolnay/rust-toolchain@1.63 + uses: dtolnay/rust-toolchain@1.65 with: components: rustfmt, clippy @@ -91,7 +91,7 @@ jobs: uses: actions/checkout@v3 - name: Install stable toolchain - uses: dtolnay/rust-toolchain@1.63 + uses: dtolnay/rust-toolchain@1.65 - uses: Swatinem/rust-cache@v2 diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index bcdea3197dec..7d2f734aa20c 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -14,7 +14,7 @@ jobs: uses: actions/checkout@v3 - name: Install nix - uses: cachix/install-nix-action@v19 + uses: cachix/install-nix-action@v20 - name: Authenticate with Cachix uses: cachix/cachix-action@v12 diff --git a/Cargo.lock b/Cargo.lock index 9f8302e2873a..a1a9eae4dd27 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -61,15 +61,6 @@ version = "1.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bddcadddf5e9015d310179a59bb28c4d4b9920ad0f11e8e14dbadf654890c9a6" -[[package]] -name = "atoi" -version = "2.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f28d99ec8bfea296261ca1af174f24225171fea9664ba9003cbebee704810528" -dependencies = [ - "num-traits", -] - [[package]] name = "autocfg" version = "1.1.0" @@ -84,20 +75,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" [[package]] name = "bstr" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ba3569f383e8f1598449f1a423e72e99569137b47740b1da11ef19af3d5c3223" -dependencies = [ - "lazy_static", - "memchr", - "regex-automata", -] - -[[package]] -name = "bstr" -version = "1.0.1" +version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fca0852af221f458706eb0725c03e4ed6c46af9ac98e6a689d5e634215d594dd" +checksum = "5ffdb39cb703212f3c11973452c2861b972f757b021158f3516ba10f2fa8b2c1" dependencies = [ "memchr", "once_cell", @@ -107,9 +87,9 @@ dependencies = [ [[package]] name = "btoi" -version = "0.4.2" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97c0869a9faa81f8bbf8102371105d6d0a7b79167a04c340b04ab16892246a11" +checksum = "9dd6407f73a9b8b6162d8a2ef999fe6afd7cc15902ebf42c5cd296addf17e0ad" dependencies = [ "num-traits", ] @@ -132,27 +112,12 @@ version = "1.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfb24e866b15a1af2a1b663f10c6b6b8f397a84aadb828f12e5b289ec23a3a3c" -[[package]] -name = "bytesize" -version = "1.1.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6c58ec36aac5066d5ca17df51b3e70279f5670a72102f5752cb7e7c856adfc70" - [[package]] name = "cassowary" version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53" -[[package]] -name = "castaway" -version = "0.2.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8a17ed5635fc8536268e5d4de1e22e81ac34419e5f052d4d51f4e01dcc263fcc" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.0.79" @@ -215,17 +180,6 @@ dependencies = [ "unicode-width", ] -[[package]] -name = "compact_str" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5138945395949e7dfba09646dc9e766b548ff48e23deb5246890e6b64ae9e1b9" -dependencies = [ - "castaway", - "itoa", - "ryu", -] - [[package]] name = "content_inspector" version = "0.2.4" @@ -250,27 +204,18 @@ dependencies = [ "cfg-if", ] -[[package]] -name = "crossbeam-utils" -version = "0.8.14" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4fb766fa798726286dbbb842f174001dab8abc7b627a1dd86e0b7222a95d929f" -dependencies = [ - "cfg-if", -] - [[package]] name = "crossterm" -version = "0.25.0" +version = "0.26.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e64e6c0fbe2c17357405f7c758c1ef960fce08bdfb2c03d88d2a18d7e09c4b67" +checksum = "a84cda67535339806297f1b331d6dd6320470d2a0fe65381e79ee9e156dd3d13" dependencies = [ "bitflags", "crossterm_winapi", "futures-core", "libc", "mio", - "parking_lot 0.12.1", + "parking_lot", "signal-hook", "signal-hook-mio", "winapi", @@ -329,19 +274,6 @@ dependencies = [ "syn", ] -[[package]] -name = "dashmap" -version = "5.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "907076dfda823b0b36d2a1bb5f90c96660a5bbcd7729e10727f07858f22c4edc" -dependencies = [ - "cfg-if", - "hashbrown 0.12.3", - "lock_api", - "once_cell", - "parking_lot_core 0.9.4", -] - [[package]] name = "dirs" version = "4.0.0" @@ -383,6 +315,12 @@ dependencies = [ "winapi", ] +[[package]] +name = "dunce" +version = "1.0.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0bd4b30a6560bbd9b4620f4de34c3f14f60848e58a9b7216801afcb4c7b31c3c" + [[package]] name = "either" version = "1.8.0" @@ -407,6 +345,27 @@ dependencies = [ "encoding_rs", ] +[[package]] +name = "errno" +version = "0.2.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f639046355ee4f37944e44f60642c6f3a7efa3cf6b78c78a0d989a8ce6c396a1" +dependencies = [ + "errno-dragonfly", + "libc", + "winapi", +] + +[[package]] +name = "errno-dragonfly" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "aa68f1b12764fab894d2755d2518754e71b4fd80ecfb822714a1206c2aab39bf" +dependencies = [ + "cc", + "libc", +] + [[package]] name = "error-code" version = "2.3.1" @@ -455,7 +414,7 @@ dependencies = [ "cfg-if", "libc", "redox_syscall", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -540,75 +499,116 @@ dependencies = [ ] [[package]] -name = "git-actor" -version = "0.17.0" +name = "gix" +version = "0.39.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dabfac58aecb4a38cdd2568de66eb1f0d968fd6726f5a80cb8bea7944ef10cc0" +dependencies = [ + "gix-actor", + "gix-attributes", + "gix-config", + "gix-credentials", + "gix-date", + "gix-diff", + "gix-discover", + "gix-features", + "gix-glob", + "gix-hash", + "gix-hashtable", + "gix-index", + "gix-lock", + "gix-mailmap", + "gix-object", + "gix-odb", + "gix-pack", + "gix-path", + "gix-prompt", + "gix-ref", + "gix-refspec", + "gix-revision", + "gix-sec", + "gix-tempfile", + "gix-traverse", + "gix-url", + "gix-validate", + "gix-worktree", + "log", + "once_cell", + "signal-hook", + "smallvec", + "thiserror", + "unicode-normalization", +] + +[[package]] +name = "gix-actor" +version = "0.19.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9e5fd7bc63ad527d64584f8d01f99b89c051f5fbb8144b58ae5f812775065cf" +checksum = "dc22b0cdc52237667c301dd7cdc6ead8f8f73c9f824e9942c8ebd6b764f6c0bf" dependencies = [ - "bstr 1.0.1", + "bstr", "btoi", - "git-date", + "gix-date", "itoa", "nom", - "quick-error", + "thiserror", ] [[package]] -name = "git-attributes" -version = "0.8.0" +name = "gix-attributes" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8013dfce47c1e29236d732308933e2c77af5355ec5105755d26faf7764d3f7b" +checksum = "2231a25934a240d0a4b6f4478401c73ee81d8be52de0293eedbc172334abf3e1" dependencies = [ - "bstr 1.0.1", - "compact_str", - "git-features", - "git-glob", - "git-path", - "git-quote", + "bstr", + "gix-features", + "gix-glob", + "gix-path", + "gix-quote", "thiserror", "unicode-bom", ] [[package]] -name = "git-bitmap" -version = "0.2.0" +name = "gix-bitmap" +version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "44304093ac66a0ada1b243c15c3a503a165a1d0f50bec748f4e5a9b84a0d0722" +checksum = "024bca0c7187517bda5ea24ab148c9ca8208dd0c3e2bea88cdb2008f91791a6d" dependencies = [ - "quick-error", + "thiserror", ] [[package]] -name = "git-chunk" -version = "0.4.0" +name = "gix-chunk" +version = "0.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3090baa2f4a3fe488a9b3e31090b83259aaf930bf0634af34c18117274f8f1a8" +checksum = "b0d39583cab06464b8bf73b3f1707458270f0e7383cb24c3c9c1a16e6f792978" dependencies = [ "thiserror", ] [[package]] -name = "git-command" -version = "0.2.1" +name = "gix-command" +version = "0.2.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "215145cc1686a45bc6f9872b153a0d3f3c40a1b94173a928325e1b53dfa5e2af" +checksum = "b2c6f75c1e0f924de39e750880a6e21307194bb1ab773efe3c7d2d787277f8ab" dependencies = [ - "bstr 1.0.1", + "bstr", ] [[package]] -name = "git-config" -version = "0.15.0" +name = "gix-config" +version = "0.18.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c9da662fd64ac69772158dcf04777da6266f0f36bc9a310b3eb2d805bb696315" +checksum = "52c62e26ce11f607712e4f49a0a192ed87675d30187fd61be070abbd607d12f1" dependencies = [ - "bstr 1.0.1", - "git-config-value", - "git-features", - "git-glob", - "git-path", - "git-ref", - "git-sec", + "bstr", + "gix-config-value", + "gix-features", + "gix-glob", + "gix-path", + "gix-ref", + "gix-sec", "memchr", "nom", "once_cell", @@ -618,135 +618,137 @@ dependencies = [ ] [[package]] -name = "git-config-value" -version = "0.10.0" +name = "gix-config-value" +version = "0.10.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "989a90c1c630513a153c685b4249b96fdf938afc75bf7ef2ae1ccbd3d799f5db" +checksum = "693d4a4ba0531e46fe558459557a5b29fb86c3e4b2666c1c0861d93c7c678331" dependencies = [ "bitflags", - "bstr 1.0.1", - "git-path", + "bstr", + "gix-path", "libc", "thiserror", ] [[package]] -name = "git-credentials" -version = "0.9.0" +name = "gix-credentials" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97cd6bbe001afd6356b35ef13f2a6b0f0abc0133d1b2ecaec1033bdd769616d6" +checksum = "5be32b5fe339a31b8e53fa854081dc914c45020dcb64637f3c21baf69c96fc1b" dependencies = [ - "bstr 1.0.1", - "git-command", - "git-config-value", - "git-path", - "git-prompt", - "git-sec", - "git-url", + "bstr", + "gix-command", + "gix-config-value", + "gix-path", + "gix-prompt", + "gix-sec", + "gix-url", "thiserror", ] [[package]] -name = "git-date" -version = "0.4.0" +name = "gix-date" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "412c9b89026505bd24d5f8acafa578de6eea3b271ece307a73b8e646e671302a" +checksum = "b96271912ce39822501616f177dea7218784e6c63be90d5f36322ff3a722aae2" dependencies = [ - "bstr 1.0.1", + "bstr", "itoa", "thiserror", "time", ] [[package]] -name = "git-diff" -version = "0.26.0" +name = "gix-diff" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca87474422d26d606d04cec6bedfabcd92a0a74102cd7936785358ced6a4a25a" +checksum = "585b0834d4b6791a848637c4e109545fda9b0f29b591ba55edb33ceda6e7856b" dependencies = [ - "git-hash", - "git-object", + "gix-hash", + "gix-object", "imara-diff", "thiserror", ] [[package]] -name = "git-discover" -version = "0.12.0" +name = "gix-discover" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1e9e26e0bc434643228cd418185bd28ca5c7cf831bde1da434807391c27ac40e" +checksum = "91c204adba5ebd211c74735cbb65817d277e154486bac0dffa3701f163b80350" dependencies = [ - "bstr 1.0.1", - "git-hash", - "git-path", - "git-ref", - "git-sec", + "bstr", + "dunce", + "gix-hash", + "gix-path", + "gix-ref", + "gix-sec", "thiserror", ] [[package]] -name = "git-features" -version = "0.26.0" +name = "gix-features" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5ff74064fa007c5beefa89a64bb72834f32b3c497750a56c79c6802bbdb311f9" +checksum = "5e6a9dfa7b3c1a99315203e8b97f8f99f3bd95731590607abeaa5ca31bc41fe3" dependencies = [ "crc32fast", "flate2", - "git-hash", + "gix-hash", "libc", "once_cell", "prodash", - "quick-error", "sha1_smol", + "thiserror", "walkdir", ] [[package]] -name = "git-glob" -version = "0.5.1" +name = "gix-glob" +version = "0.5.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3908404c9b76ac7b3f636a104142378d3eaa78623cbc6eb7c7f0651979d48e8a" +checksum = "93e43efd776bc543f46f0fd0ca3d920c37af71a764a16f2aebd89765e9ff2993" dependencies = [ "bitflags", - "bstr 1.0.1", + "bstr", ] [[package]] -name = "git-hash" -version = "0.10.1" +name = "gix-hash" +version = "0.10.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1532d82bf830532f8d545c5b7b568e311e3593f16cf7ee9dd0ce03c74b12b99d" +checksum = "0c0c5a9f4d621d4f4ea046bb331df5c746ca735b8cae5b234cc2be70ee4dbef0" dependencies = [ "hex", "thiserror", ] [[package]] -name = "git-hashtable" -version = "0.1.0" +name = "gix-hashtable" +version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c52b625ad8cc360a0b7f426266f21fb07bd49b8f4ccf1b3ca7bc89424db1dec4" +checksum = "9609c1b8f36f12968e6a6098f7cdb52004f7d42d570f47a2d6d7c16612f19acb" dependencies = [ - "git-hash", + "gix-hash", "hashbrown 0.13.2", + "parking_lot", ] [[package]] -name = "git-index" -version = "0.12.1" +name = "gix-index" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "485da97dd4f69c7d9a8dc238cd6f4a726387ffc34573489e8e0d2bee266e3454" +checksum = "c12caf7886c7ba06f2b28835cdc2be1dca86bd047d00299d2d49e707ce1c2616" dependencies = [ - "atoi", "bitflags", - "bstr 1.0.1", + "bstr", + "btoi", "filetime", - "git-bitmap", - "git-features", - "git-hash", - "git-lock", - "git-object", - "git-traverse", + "gix-bitmap", + "gix-features", + "gix-hash", + "gix-lock", + "gix-object", + "gix-traverse", "itoa", "memmap2", "smallvec", @@ -754,39 +756,39 @@ dependencies = [ ] [[package]] -name = "git-lock" -version = "3.0.0" +name = "gix-lock" +version = "4.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89e4f05b8a68c3a5dd83a6651c76be384e910fe283072184fdab9d77f87ccec2" +checksum = "66119ff8a4a395d0ea033fef718bc85f8b4f0855874f4ce1e005fc16cfe1f66e" dependencies = [ "fastrand", - "git-tempfile", - "quick-error", + "gix-tempfile", + "thiserror", ] [[package]] -name = "git-mailmap" -version = "0.9.0" +name = "gix-mailmap" +version = "0.11.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0316b4346f3e162ade368209efb8a609b587793c74aa3b8de0ec01a4f3580120" +checksum = "2b66aea5e52875cd4915f4957a6f4b75831a36981e2ec3f5fad9e370e444fe1a" dependencies = [ - "bstr 1.0.1", - "git-actor", - "quick-error", + "bstr", + "gix-actor", + "thiserror", ] [[package]] -name = "git-object" -version = "0.26.0" +name = "gix-object" +version = "0.28.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f8563e2d6f524d7053f3106714f99ecdc3adbba2cb7108c09d71a02579f2e19" +checksum = "8df068db9180ee935fbb70504848369e270bdcb576b05c0faa8b9fd3b86fc017" dependencies = [ - "bstr 1.0.1", + "bstr", "btoi", - "git-actor", - "git-features", - "git-hash", - "git-validate", + "gix-actor", + "gix-features", + "gix-hash", + "gix-validate", "hex", "itoa", "nom", @@ -795,260 +797,215 @@ dependencies = [ ] [[package]] -name = "git-odb" -version = "0.40.0" +name = "gix-odb" +version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "616115a0e3daff6e08842758d24547b37a6eb6d0e2eedd95a740c3aaa2750333" +checksum = "e9a5f9e1afbd509761977a2ea02869cedaaba500b4e783deb2e4de5179a55a80" dependencies = [ "arc-swap", - "git-features", - "git-hash", - "git-object", - "git-pack", - "git-path", - "git-quote", - "parking_lot 0.12.1", + "gix-features", + "gix-hash", + "gix-object", + "gix-pack", + "gix-path", + "gix-quote", + "parking_lot", "tempfile", "thiserror", ] [[package]] -name = "git-pack" -version = "0.30.0" +name = "gix-pack" +version = "0.32.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7cd16b88f4b66041f41ca510c28bd81c4ee7363c5a544b3d62b4170432965871" +checksum = "e51db84e1459a8022e518d40a8778028d793dbb28e4d35c9a5eaf92658fb0775" dependencies = [ - "bytesize", "clru", - "dashmap", - "git-chunk", - "git-diff", - "git-features", - "git-hash", - "git-hashtable", - "git-object", - "git-path", - "git-tempfile", - "git-traverse", + "gix-chunk", + "gix-diff", + "gix-features", + "gix-hash", + "gix-hashtable", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-traverse", "memmap2", - "parking_lot 0.12.1", + "parking_lot", "smallvec", "thiserror", ] [[package]] -name = "git-path" -version = "0.7.0" +name = "gix-path" +version = "0.7.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e40e68481a06da243d3f4dfd86a4be39c24eefb535017a862e845140dcdb878a" +checksum = "f6c104a66dec149cb8f7aaafc6ab797654cf82d67f050fd0cb7e7294e328354b" dependencies = [ - "bstr 1.0.1", + "bstr", "thiserror", ] [[package]] -name = "git-prompt" -version = "0.3.0" +name = "gix-prompt" +version = "0.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3612a486e507dd431ef0f7108eeaafc8fd1ed7bd0f205a88554f6f91fe5dccbf" +checksum = "a20cebf73229debaa82574c4fd20dcaf00fa8d4bfce823a862c4e990d7a0b5b4" dependencies = [ - "git-command", - "git-config-value", + "gix-command", + "gix-config-value", "nix", - "parking_lot 0.12.1", + "parking_lot", "thiserror", ] [[package]] -name = "git-quote" -version = "0.4.0" +name = "gix-quote" +version = "0.4.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1dd11f4e7f251ab297545faa4c5a4517f4985a43b9c16bf96fa49107f58e837f" +checksum = "a282f5a8d9ee0b09ec47390ac727350c48f2f5c76d803cd8da6b3e7ad56e0bcb" dependencies = [ - "bstr 1.0.1", + "bstr", "btoi", - "quick-error", + "thiserror", ] [[package]] -name = "git-ref" -version = "0.23.0" +name = "gix-ref" +version = "0.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e6767925a6fc4af5c5a81e348d1d851c1b3ab2b512bd7f562ac11be37c14468" +checksum = "90a0ed29e581f04b904ecd0c32b11f33b8209b5a0af9c43f415249a4f2fba632" dependencies = [ - "git-actor", - "git-features", - "git-hash", - "git-lock", - "git-object", - "git-path", - "git-tempfile", - "git-validate", + "gix-actor", + "gix-features", + "gix-hash", + "gix-lock", + "gix-object", + "gix-path", + "gix-tempfile", + "gix-validate", "memmap2", "nom", "thiserror", ] [[package]] -name = "git-refspec" -version = "0.7.0" +name = "gix-refspec" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ddf310ed5f2829ac0af96e7d4aebd4ae4b89f0718a7ae3666d09b02b2c5a1dfd" +checksum = "aba332462bda2e8efeae4302b39a6ed01ad56ef772fd5b7ef197cf2798294d65" dependencies = [ - "bstr 1.0.1", - "git-hash", - "git-revision", - "git-validate", + "bstr", + "gix-hash", + "gix-revision", + "gix-validate", "smallvec", "thiserror", ] [[package]] -name = "git-repository" -version = "0.32.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "993277960cb7e2d3991a11c1ec6951c1d142de052c26a18d2db64304e52d3741" -dependencies = [ - "git-actor", - "git-attributes", - "git-config", - "git-credentials", - "git-date", - "git-diff", - "git-discover", - "git-features", - "git-glob", - "git-hash", - "git-hashtable", - "git-index", - "git-lock", - "git-mailmap", - "git-object", - "git-odb", - "git-pack", - "git-path", - "git-prompt", - "git-ref", - "git-refspec", - "git-revision", - "git-sec", - "git-tempfile", - "git-traverse", - "git-url", - "git-validate", - "git-worktree", - "log", - "once_cell", - "prodash", - "signal-hook", - "smallvec", - "thiserror", - "unicode-normalization", -] - -[[package]] -name = "git-revision" -version = "0.10.0" +name = "gix-revision" +version = "0.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4f9a6bd28c9d1676bb96f428cd09614ae18a0087d7cea1cebfd177e25f99b2af" +checksum = "ed98e4a0254953c64bc913bd23146a1de662067d5cf974cbdde396958b39e5b0" dependencies = [ - "bstr 1.0.1", - "git-date", - "git-hash", - "git-hashtable", - "git-object", + "bstr", + "gix-date", + "gix-hash", + "gix-hashtable", + "gix-object", "thiserror", ] [[package]] -name = "git-sec" -version = "0.6.0" +name = "gix-sec" +version = "0.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9e1802e8252fa223b0ad89a393aed461132174ced1e6842a41f56dc92a3fc14f" +checksum = "e8ffa5bf0772f9b01de501c035b6b084cf9b8bb07dec41e3afc6a17336a65f47" dependencies = [ "bitflags", "dirs", - "git-path", + "gix-path", "libc", "windows", ] [[package]] -name = "git-tempfile" -version = "3.0.0" +name = "gix-tempfile" +version = "4.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a6bb4dee86c8cae5a078cfaac3b004ef99c31548ed86218f23a7ff9b4b74f3be" +checksum = "a8e0227bd284cd16105e8479602bb8af6bddcb800427e881c1feee4806310a31" dependencies = [ - "dashmap", "libc", "once_cell", + "parking_lot", "signal-hook", "signal-hook-registry", "tempfile", ] [[package]] -name = "git-traverse" -version = "0.22.0" +name = "gix-traverse" +version = "0.24.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd356da21ec00f69b9d4f105df4cb85543c746b18f4b7fc81529ce77713cdb29" +checksum = "dd9a4a07bb22168dc79c60e1a6a41919d198187ca83d8a5940ad8d7122a45df3" dependencies = [ - "git-hash", - "git-hashtable", - "git-object", + "gix-hash", + "gix-hashtable", + "gix-object", "thiserror", ] [[package]] -name = "git-url" -version = "0.13.0" +name = "gix-url" +version = "0.15.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c85af407ed0dbb8d8da2a7241827d2fd5681186d9dab3570fc8dd8d6152ec48f" +checksum = "044072b7ce8601b62dcec841b92129f5cc677072823324121b395d766ac5f528" dependencies = [ - "bstr 1.0.1", - "git-features", - "git-path", + "bstr", + "gix-features", + "gix-path", "home", "thiserror", "url", ] [[package]] -name = "git-validate" -version = "0.7.1" +name = "gix-validate" +version = "0.7.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0431cf9352c596dc7c8ec9066ee551ce54e63c86c3c767e5baf763f6019ff3c2" +checksum = "b69ddb780ea1465255e66818d75b7098371c58dbc9560da4488a44b9f5c7e443" dependencies = [ - "bstr 1.0.1", + "bstr", "thiserror", ] [[package]] -name = "git-worktree" -version = "0.12.0" +name = "gix-worktree" +version = "0.14.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ca3bc63878f134e08ed52dba5d82422798c01a3f2e48c38ae9a2f7ff9194f362" +checksum = "b7cb9af6e56152953d8fe113c4f9d7cf60cf7a982362711e9200a255579b49cb" dependencies = [ - "bstr 1.0.1", - "git-attributes", - "git-features", - "git-glob", - "git-hash", - "git-index", - "git-object", - "git-path", + "bstr", + "gix-attributes", + "gix-features", + "gix-glob", + "gix-hash", + "gix-index", + "gix-object", + "gix-path", "io-close", "thiserror", ] [[package]] name = "globset" -version = "0.4.9" +version = "0.4.10" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0a1e17342619edbc21a964c2afbeb6c820c6a2560032872f397bb97ea127bd0a" +checksum = "029d74589adefde59de1a0c4f4732695c32805624aec7b68d91503d4dba79afc" dependencies = [ "aho-corasick", - "bstr 0.2.17", + "bstr", "fnv", "log", "regex", @@ -1056,21 +1013,21 @@ dependencies = [ [[package]] name = "grep-matcher" -version = "0.1.5" +version = "0.1.6" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6d27563c33062cd33003b166ade2bb4fd82db1fd6a86db764dfdad132d46c1cc" +checksum = "3902ca28f26945fe35cad349d776f163981d777fee382ccd6ef451126f51b319" dependencies = [ "memchr", ] [[package]] name = "grep-regex" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1345f8d33c89f2d5b081f2f2a41175adef9fd0bed2fea6a26c96c2deb027e58e" +checksum = "997598b41d53a37a2e3fc5300d5c11d825368c054420a9c65125b8fe1078463f" dependencies = [ "aho-corasick", - "bstr 0.2.17", + "bstr", "grep-matcher", "log", "regex", @@ -1080,11 +1037,11 @@ dependencies = [ [[package]] name = "grep-searcher" -version = "0.1.10" +version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "48852bd08f9b4eb3040ecb6d2f4ade224afe880a9a0909c5563cc59fa67932cc" +checksum = "5601c4b9f480f0c9ebb40b1f6cbf447b8a50c5369223937a6c5214368c58779f" dependencies = [ - "bstr 0.2.17", + "bstr", "bytecount", "encoding_rs", "encoding_rs_io", @@ -1182,6 +1139,7 @@ dependencies = [ "futures-util", "helix-core", "helix-loader", + "helix-parsec", "log", "lsp-types", "serde", @@ -1192,6 +1150,10 @@ dependencies = [ "which", ] +[[package]] +name = "helix-parsec" +version = "0.6.0" + [[package]] name = "helix-term" version = "0.6.0" @@ -1239,6 +1201,8 @@ dependencies = [ "crossterm", "helix-core", "helix-view", + "log", + "once_cell", "serde", "termini", "unicode-segmentation", @@ -1248,11 +1212,11 @@ dependencies = [ name = "helix-vcs" version = "0.6.0" dependencies = [ - "git-repository", + "gix", "helix-core", "imara-diff", "log", - "parking_lot 0.12.1", + "parking_lot", "tempfile", "tokio", ] @@ -1277,6 +1241,7 @@ dependencies = [ "libc", "log", "once_cell", + "parking_lot", "serde", "serde_json", "slotmap", @@ -1311,12 +1276,6 @@ dependencies = [ "winapi", ] -[[package]] -name = "human_format" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "86cce260d758a9aa3d7c4b99d55c815a540f8a37514ba6046ab6be402a157cb0" - [[package]] name = "iana-time-zone" version = "0.1.53" @@ -1353,11 +1312,10 @@ dependencies = [ [[package]] name = "ignore" -version = "0.4.18" +version = "0.4.20" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "713f1b139373f96a2e0ce3ac931cd01ee973c3c5dd7c40c0c2efe96ad2b6751d" +checksum = "dbe7873dab538a9a44ad79ede1faf5f30d49f9a5c883ddbab48bce81b64b7492" dependencies = [ - "crossbeam-utils", "globset", "lazy_static", "log", @@ -1414,6 +1372,16 @@ dependencies = [ "winapi", ] +[[package]] +name = "io-lifetimes" +version = "1.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e7d6c6f8c91b4b9ed43484ad1a938e393caf35960fce7f82a040497207bd8e9e" +dependencies = [ + "libc", + "windows-sys 0.42.0", +] + [[package]] name = "itoa" version = "1.0.4" @@ -1460,6 +1428,12 @@ dependencies = [ "cc", ] +[[package]] +name = "linux-raw-sys" +version = "0.1.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f051f77a7c8e6957c0696eac88f26b0117e54f52d3fc682ab19397a8812846a4" + [[package]] name = "lock_api" version = "0.4.9" @@ -1531,7 +1505,7 @@ dependencies = [ "libc", "log", "wasi", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1609,17 +1583,6 @@ version = "1.17.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b7e5500299e16ebb147ae15a00a942af264cf3688f47923b8fc2cd5858f23ad3" -[[package]] -name = "parking_lot" -version = "0.11.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7d17b78036a60663b797adeaee46f5c9dfebb86948d1255007a1d6be0271ff99" -dependencies = [ - "instant", - "lock_api", - "parking_lot_core 0.8.6", -] - [[package]] name = "parking_lot" version = "0.12.1" @@ -1627,21 +1590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3742b2c103b9f06bc9fff0a37ff4912935851bee6d36f3c02bcc755bcfec228f" dependencies = [ "lock_api", - "parking_lot_core 0.9.4", -] - -[[package]] -name = "parking_lot_core" -version = "0.8.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60a2cfe6f0ad2bfc16aefa463b497d5c7a5ecd44a23efa72aa342d90177356dc" -dependencies = [ - "cfg-if", - "instant", - "libc", - "redox_syscall", - "smallvec", - "winapi", + "parking_lot_core", ] [[package]] @@ -1654,7 +1603,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-sys", + "windows-sys 0.42.0", ] [[package]] @@ -1686,15 +1635,9 @@ dependencies = [ [[package]] name = "prodash" -version = "23.0.0" +version = "23.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5d8c414345b4a98cbcd0e8d8829c8f54b47a7ed4fb771c45b7c5c6c0ae23dc4c" -dependencies = [ - "bytesize", - "dashmap", - "human_format", - "parking_lot 0.11.2", -] +checksum = "d73c6b64cb5b99eb63ca97d378685712617ec0172ff5c04cd47a489d3e2c51f8" [[package]] name = "pulldown-cmark" @@ -1707,12 +1650,6 @@ dependencies = [ "unicase", ] -[[package]] -name = "quick-error" -version = "2.0.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a993555f31e5a609f617c12db6250dedcac1b0a85076912c436e6fc9b2c8e6a3" - [[package]] name = "quickcheck" version = "1.0.3" @@ -1792,15 +1729,6 @@ version = "0.6.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "456c603be3e8d448b072f410900c09faf164fbce2d480456f50eea6e25f9c848" -[[package]] -name = "remove_dir_all" -version = "0.5.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3acd125665422973a33ac9d3dd2df85edad0f4ae9b00dafb1a05e43a9f5ef8e7" -dependencies = [ - "winapi", -] - [[package]] name = "ropey" version = "1.6.0" @@ -1812,10 +1740,18 @@ dependencies = [ ] [[package]] -name = "rustversion" -version = "1.0.9" +name = "rustix" +version = "0.36.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97477e48b4cf8603ad5f7aaf897467cf42ab4218a38ef76fb14c2d6773a6d6a8" +checksum = "d4fdebc4b395b7fbb9ab11e462e20ed9051e7b16e42d24042c776eca0ac81b03" +dependencies = [ + "bitflags", + "errno", + "io-lifetimes", + "libc", + "linux-raw-sys", + "windows-sys 0.42.0", +] [[package]] name = "ryu" @@ -1866,9 +1802,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.93" +version = "1.0.94" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "cad406b69c91885b5107daf2c29572f6c8cdb3c66826821e286c533490c0bc76" +checksum = "1c533a59c9d8a93a09c6ab31f0fd5e5f4dd1b8fc9434804029839884765d04ea" dependencies = [ "itoa", "ryu", @@ -2025,16 +1961,15 @@ dependencies = [ [[package]] name = "tempfile" -version = "3.3.0" +version = "3.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5cdb1ef4eaeeaddc8fbd371e5017057064af0911902ef36b39801f67cc6d79e4" +checksum = "af18f7ae1acd354b992402e9ec5864359d693cd8a79dcbef59f76891701c1e95" dependencies = [ "cfg-if", "fastrand", - "libc", "redox_syscall", - "remove_dir_all", - "winapi", + "rustix", + "windows-sys 0.42.0", ] [[package]] @@ -2068,18 +2003,18 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6a9cd18aa97d5c45c6603caea1da6628790b37f7a34b6ca89522331c5180fed0" +checksum = "a5ab016db510546d856297882807df8da66a16fb8c4101cb8b30054b0d5b2d9c" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.38" +version = "1.0.39" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1fb327af4685e4d03fa8cbcf1716380da910eeb2bb8be417e7f9fd3fb164f36f" +checksum = "5420d42e90af0c38c3290abcca25b9b3bdf379fc9f55c528f53a269d9c9a267e" dependencies = [ "proc-macro2", "quote", @@ -2150,9 +2085,9 @@ checksum = "cda74da7e1a664f795bb1f8a87ec406fb89a02522cf6e50620d016add6dbbf5c" [[package]] name = "tokio" -version = "1.25.0" +version = "1.26.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8e00990ebabbe4c14c08aca901caed183ecd5c09562a12c824bb53d3c3fd3af" +checksum = "03201d01c3c27a29c8a5cee5b55a93ddae1ccf6f08f65365c2c918f8c1b76f64" dependencies = [ "autocfg", "bytes", @@ -2160,12 +2095,12 @@ dependencies = [ "memchr", "mio", "num_cpus", - "parking_lot 0.12.1", + "parking_lot", "pin-project-lite", "signal-hook-registry", "socket2", "tokio-macros", - "windows-sys", + "windows-sys 0.45.0", ] [[package]] @@ -2227,8 +2162,7 @@ dependencies = [ [[package]] name = "tree-sitter" version = "0.20.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d4423c784fe11398ca91e505cdc71356b07b1a924fc8735cfab5333afe3e18bc" +source = "git+https://github.com/tree-sitter/tree-sitter?rev=c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14#c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" dependencies = [ "cc", "regex", @@ -2431,17 +2365,17 @@ checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" [[package]] name = "windows" -version = "0.40.0" +version = "0.43.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e30acc718a52fb130fec72b1cb5f55ffeeec9253e1b785e94db222178a6acaa1" +checksum = "04662ed0e3e5630dfa9b26e4cb823b817f1a9addda855d973a9458c236556244" dependencies = [ - "windows_aarch64_gnullvm 0.40.0", - "windows_aarch64_msvc 0.40.0", - "windows_i686_gnu 0.40.0", - "windows_i686_msvc 0.40.0", - "windows_x86_64_gnu 0.40.0", - "windows_x86_64_gnullvm 0.40.0", - "windows_x86_64_msvc 0.40.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] @@ -2450,98 +2384,80 @@ version = "0.42.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5a3e1820f08b8513f676f7ab6c1f99ff312fb97b553d30ff4dd86f9f15728aa7" dependencies = [ - "windows_aarch64_gnullvm 0.42.0", - "windows_aarch64_msvc 0.42.0", - "windows_i686_gnu 0.42.0", - "windows_i686_msvc 0.42.0", - "windows_x86_64_gnu 0.42.0", - "windows_x86_64_gnullvm 0.42.0", - "windows_x86_64_msvc 0.42.0", + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", ] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.40.0" +name = "windows-sys" +version = "0.45.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f3caa4a1a16561b714323ca6b0817403738583033a6a92e04c5d10d4ba37ca10" +checksum = "75283be5efb2831d37ea142365f009c02ec203cd29a3ebecbc093d52315b66d0" +dependencies = [ + "windows-targets", +] [[package]] -name = "windows_aarch64_gnullvm" -version = "0.42.0" +name = "windows-targets" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "41d2aa71f6f0cbe00ae5167d90ef3cfe66527d6f613ca78ac8024c3ccab9a19e" +checksum = "8e2522491fbfcd58cc84d47aeb2958948c4b8982e9a2d8a2a35bbaed431390e7" +dependencies = [ + "windows_aarch64_gnullvm", + "windows_aarch64_msvc", + "windows_i686_gnu", + "windows_i686_msvc", + "windows_x86_64_gnu", + "windows_x86_64_gnullvm", + "windows_x86_64_msvc", +] [[package]] -name = "windows_aarch64_msvc" -version = "0.40.0" +name = "windows_aarch64_gnullvm" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "328973c62dfcc50fb1aaa8e7100676e0b642fe56bac6bafff3327902db843ab4" +checksum = "8c9864e83243fdec7fc9c5444389dcbbfd258f745e7853198f365e3c4968a608" [[package]] name = "windows_aarch64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dd0f252f5a35cac83d6311b2e795981f5ee6e67eb1f9a7f64eb4500fbc4dcdb4" +checksum = "4c8b1b673ffc16c47a9ff48570a9d85e25d265735c503681332589af6253c6c7" [[package]] name = "windows_i686_gnu" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aa5b09fad70f0df85dea2ac2a525537e415e2bf63ee31cf9b8e263645ee9f3c1" - -[[package]] -name = "windows_i686_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fbeae19f6716841636c28d695375df17562ca208b2b7d0dc47635a50ae6c5de7" +checksum = "de3887528ad530ba7bdbb1faa8275ec7a1155a45ffa57c37993960277145d640" [[package]] name = "windows_i686_msvc" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a1ad4031c1a98491fa195d8d43d7489cb749f135f2e5c4eed58da094bd0d876" - -[[package]] -name = "windows_i686_msvc" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "84c12f65daa39dd2babe6e442988fc329d6243fdce47d7d2d155b8d874862246" +checksum = "bf4d1122317eddd6ff351aa852118a2418ad4214e6613a50e0191f7004372605" [[package]] name = "windows_x86_64_gnu" -version = "0.40.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "520ff37edd72da8064b49d2281182898e17f0688ae9f4070bca27e4b5c162ac7" - -[[package]] -name = "windows_x86_64_gnu" -version = "0.42.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bf7b1b21b5362cbc318f686150e5bcea75ecedc74dd157d874d754a2ca44b0ed" - -[[package]] -name = "windows_x86_64_gnullvm" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "046e5b82215102c44fd75f488f1b9158973d02aa34d06ed85c23d6f5520a2853" +checksum = "c1040f221285e17ebccbc2591ffdc2d44ee1f9186324dd3e84e99ac68d699c45" [[package]] name = "windows_x86_64_gnullvm" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "09d525d2ba30eeb3297665bd434a54297e4170c7f1a44cad4ef58095b4cd2028" +checksum = "628bfdf232daa22b0d64fdb62b09fcc36bb01f05a3939e20ab73aaf9470d0463" [[package]] name = "windows_x86_64_msvc" -version = "0.40.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2a0c9c6df55dd1bfa76e131cef44bdd8ec9c819ef3611f04dfe453fd5bfeda28" - -[[package]] -name = "windows_x86_64_msvc" -version = "0.42.0" +version = "0.42.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f40009d85759725a34da6d89a94e63d7bdc50a862acf0dbc7c8e488f1edcb6f5" +checksum = "447660ad36a13288b1db4d4248e857b510e8c3a225c822ba4fb748c0aafecffd" [[package]] name = "xtask" diff --git a/Cargo.toml b/Cargo.toml index c7e254728c31..aaa21659ada3 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ members = [ "helix-dap", "helix-loader", "helix-vcs", + "helix-parsec", "xtask", ] @@ -31,3 +32,6 @@ inherits = "test" package.helix-core.opt-level = 2 package.helix-tui.opt-level = 2 package.helix-term.opt-level = 2 + +[patch.crates-io] +tree-sitter = { git = "https://github.com/tree-sitter/tree-sitter", rev = "c51896d32dcc11a38e41f36e3deb1a6a9c4f4b14" } diff --git a/README.md b/README.md index a26673219cb7..cba52a7a1e86 100644 --- a/README.md +++ b/README.md @@ -45,92 +45,10 @@ Note: Only certain languages have indentation definitions at the moment. Check # Installation -Packages are available for various distributions (see [Installation docs](https://docs.helix-editor.com/install.html)). - -If you would like to build from source: - -```shell -git clone https://github.com/helix-editor/helix -cd helix -cargo install --locked --path helix-term -``` - -This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars in `./runtime/grammars`. - -Helix needs its runtime files so make sure to copy/symlink the `runtime/` directory into the -config directory (for example `~/.config/helix/runtime` on Linux/macOS, or `%AppData%/helix/runtime` on Windows). - -| OS | Command | -| -------------------- | ------------------------------------------------ | -| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` | -| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` | -| Linux / macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` | - -Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires -elevated privileges - i.e. PowerShell or Cmd must be run as administrator. - -**PowerShell:** - -```powershell -New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime" -``` -Note: "runtime" must be absolute path to the runtime directory. - -**Cmd:** - -```cmd -cd %appdata%\helix -mklink /D runtime "\runtime" -``` - -The runtime location can be overridden via the `HELIX_RUNTIME` environment variable. - -> NOTE: if `HELIX_RUNTIME` is set prior to calling `cargo install --locked --path helix-term`, -> tree-sitter grammars will be built in `$HELIX_RUNTIME/grammars`. - -If you plan on keeping the repo locally, an alternative to copying/symlinking -runtime files is to set `HELIX_RUNTIME=/path/to/helix/runtime` -(`HELIX_RUNTIME=$PWD/runtime` if you're in the helix repo directory). - -Packages already solve this for you by wrapping the `hx` binary with a wrapper -that sets the variable to the install dir. - -> NOTE: running via cargo also doesn't require setting explicit `HELIX_RUNTIME` path, it will automatically -> detect the `runtime` directory in the project root. - -If you want to customize your `languages.toml` config, -tree-sitter grammars may be manually fetched and built with `hx --grammar fetch` and `hx --grammar build`. - -In order to use LSP features like auto-complete, you will need to -[install the appropriate Language Server](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) -for a language. +[Installation documentation](https://docs.helix-editor.com/install.html). [![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) -## Adding Helix to your desktop environment - -If installing from source, to use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder: - -```bash -cp contrib/Helix.desktop ~/.local/share/applications -cp contrib/helix.png ~/.local/share/icons -``` - -To use another terminal than the default, you will need to modify the `.desktop` file. For example, to use `kitty`: - -```bash -sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop -sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop -``` - -## macOS - -Helix can be installed on macOS through homebrew: - -``` -brew install helix -``` - # Contributing Contributing guidelines can be found [here](./docs/CONTRIBUTING.md). diff --git a/book/src/SUMMARY.md b/book/src/SUMMARY.md index eaf0c4f483f1..6e780b87fc19 100644 --- a/book/src/SUMMARY.md +++ b/book/src/SUMMARY.md @@ -6,13 +6,13 @@ - [Usage](./usage.md) - [Keymap](./keymap.md) - [Commands](./commands.md) - - [Language Support](./lang-support.md) + - [Language support](./lang-support.md) - [Migrating from Vim](./from-vim.md) - [Configuration](./configuration.md) - [Themes](./themes.md) - - [Key Remapping](./remapping.md) + - [Key remapping](./remapping.md) - [Languages](./languages.md) - [Guides](./guides/README.md) - - [Adding Languages](./guides/adding_languages.md) - - [Adding Textobject Queries](./guides/textobject.md) - - [Adding Indent Queries](./guides/indent.md) + - [Adding languages](./guides/adding_languages.md) + - [Adding textobject queries](./guides/textobject.md) + - [Adding indent queries](./guides/indent.md) diff --git a/book/src/commands.md b/book/src/commands.md index d9a113866b2f..047a30a91c83 100644 --- a/book/src/commands.md +++ b/book/src/commands.md @@ -1,5 +1,5 @@ # Commands -Command mode can be activated by pressing `:`, similar to Vim. Built-in commands: +Command mode can be activated by pressing `:`. The built-in commands are: {{#include ./generated/typable-cmd.md}} diff --git a/book/src/configuration.md b/book/src/configuration.md index bf9c1a017f33..e00b4481e3ef 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -2,10 +2,10 @@ To override global configuration parameters, create a `config.toml` file located in your config directory: -* Linux and Mac: `~/.config/helix/config.toml` -* Windows: `%AppData%\helix\config.toml` +- Linux and Mac: `~/.config/helix/config.toml` +- Windows: `%AppData%\helix\config.toml` -> Hint: You can easily open the config file by typing `:config-open` within Helix normal mode. +> 💡 You can easily open the config file by typing `:config-open` within Helix normal mode. Example config: @@ -25,12 +25,10 @@ select = "underline" hidden = false ``` -You may also specify a file to use for configuration with the `-c` or -`--config` CLI argument: `hx -c path/to/custom-config.toml`. - -It is also possible to trigger configuration file reloading by sending the `USR1` -signal to the helix process, e.g. via `pkill -USR1 hx`. This is only supported -on unix operating systems. +You can use a custom configuration file by specifying it with the `-c` or +`--config` command line argument, for example `hx -c path/to/custom-config.toml`. +Additionally, you can reload the configuration file by sending the USR1 +signal to the Helix process on Unix operating systems, such as by using the command `pkill -USR1 hx`. ## Editor @@ -38,25 +36,26 @@ on unix operating systems. | Key | Description | Default | |--|--|---------| -| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling. | `5` | -| `mouse` | Enable mouse mode. | `true` | -| `middle-click-paste` | Middle click paste support. | `true` | -| `scroll-lines` | Number of lines to scroll per scroll wheel step. | `3` | -| `shell` | Shell to use when running external commands. | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | -| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers. | `absolute` | -| `cursorline` | Highlight all lines with a cursor. | `false` | -| `cursorcolumn` | Highlight all columns with a cursor. | `false` | +| `scrolloff` | Number of lines of padding around the edge of the screen when scrolling | `5` | +| `mouse` | Enable mouse mode | `true` | +| `middle-click-paste` | Middle click paste support | `true` | +| `scroll-lines` | Number of lines to scroll per scroll wheel step | `3` | +| `shell` | Shell to use when running external commands | Unix: `["sh", "-c"]`
Windows: `["cmd", "/C"]` | +| `line-number` | Line number display: `absolute` simply shows each line's number, while `relative` shows the distance from the current line. When unfocused or in insert mode, `relative` will still show absolute line numbers | `absolute` | +| `cursorline` | Highlight all lines with a cursor | `false` | +| `cursorcolumn` | Highlight all columns with a cursor | `false` | | `gutters` | Gutters to display: Available are `diagnostics` and `diff` and `line-numbers` and `spacer`, note that `diagnostics` also includes other features like breakpoints, 1-width padding will be inserted if gutters is non-empty | `["diagnostics", "spacer", "line-numbers", "spacer", "diff"]` | -| `auto-completion` | Enable automatic pop up of auto-completion. | `true` | -| `auto-format` | Enable automatic formatting on save. | `true` | -| `auto-save` | Enable automatic saving on focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal. | `false` | -| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant. | `400` | +| `auto-completion` | Enable automatic pop up of auto-completion | `true` | +| `auto-format` | Enable automatic formatting on save | `true` | +| `auto-save` | Enable automatic saving on the focus moving away from Helix. Requires [focus event support](https://github.com/helix-editor/helix/wiki/Terminal-Support) from your terminal | `false` | +| `idle-timeout` | Time in milliseconds since last keypress before idle timers trigger. Used for autocompletion, set to 0 for instant | `400` | | `completion-trigger-len` | The min-length of word under cursor to trigger autocompletion | `2` | -| `auto-info` | Whether to display infoboxes | `true` | -| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative. | `false` | -| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file. | `[]` | +| `auto-info` | Whether to display info boxes | `true` | +| `true-color` | Set to `true` to override automatic detection of terminal truecolor support in the event of a false negative | `false` | +| `rulers` | List of column positions at which to display the rulers. Can be overridden by language specific `rulers` in `languages.toml` file | `[]` | | `bufferline` | Renders a line at the top of the editor displaying open buffers. Can be `always`, `never` or `multiple` (only shown if more than one buffer is in use) | `never` | | `color-modes` | Whether to color the mode indicator with different colors depending on the mode itself | `false` | +| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set | `80` | ### `[editor.statusline]` Section @@ -98,6 +97,7 @@ The following statusline elements can be configured: | `spinner` | A progress spinner indicating LSP activity | | `file-name` | The path/name of the opened file | | `file-base-name` | The basename of the opened file | +| `file-modification-indicator` | The indicator to show whether the file is modified (a `[+]` appears when there are unsaved changes) | | `file-encoding` | The encoding of the opened file if it differs from UTF-8 | | `file-line-ending` | The file line endings (CRLF or LF) | | `total-line-numbers` | The total line numbers of the opened file | @@ -124,10 +124,12 @@ The following statusline elements can be configured: ### `[editor.cursor-shape]` Section -Defines the shape of cursor in each mode. Note that due to limitations -of the terminal environment, only the primary cursor can change shape. +Defines the shape of cursor in each mode. Valid values for these options are `block`, `bar`, `underline`, or `hidden`. +> 💡 Due to limitations of the terminal environment, only the primary cursor can +> change shape. + | Key | Description | Default | | --- | ----------- | ------- | | `normal` | Cursor shape in [normal mode][normal mode] | `block` | @@ -140,25 +142,22 @@ Valid values for these options are `block`, `bar`, `underline`, or `hidden`. ### `[editor.file-picker]` Section -Sets options for file picker and global search. All but the last key listed in -the default file-picker configuration below are IgnoreOptions: whether hidden -files and files listed within ignore files are ignored by (not visible in) the -helix file picker and global search. There is also one other key, `max-depth` -available, which is not defined by default. +Set options for file picker and global search. Ignoring a file means it is +not visible in the Helix file picker and global search. All git related options are only enabled in a git repository. | Key | Description | Default | |--|--|---------| -|`hidden` | Enables ignoring hidden files. | true +|`hidden` | Enables ignoring hidden files | true |`follow-links` | Follow symlinks instead of ignoring them | true |`deduplicate-links` | Ignore symlinks that point at files already shown in the picker | true -|`parents` | Enables reading ignore files from parent directories. | true -|`ignore` | Enables reading `.ignore` files. | true -|`git-ignore` | Enables reading `.gitignore` files. | true -|`git-global` | Enables reading global .gitignore, whose path is specified in git's config: `core.excludefile` option. | true -|`git-exclude` | Enables reading `.git/info/exclude` files. | true -|`max-depth` | Set with an integer value for maximum depth to recurse. | Defaults to `None`. +|`parents` | Enables reading ignore files from parent directories | true +|`ignore` | Enables reading `.ignore` files | true +|`git-ignore` | Enables reading `.gitignore` files | true +|`git-global` | Enables reading global `.gitignore`, whose path is specified in git's config: `core.excludefile` option | true +|`git-exclude` | Enables reading `.git/info/exclude` files | true +|`max-depth` | Set with an integer value for maximum depth to recurse | Defaults to `None`. ### `[editor.auto-pairs]` Section @@ -210,7 +209,7 @@ Search specific options. | Key | Description | Default | |--|--|---------| -| `smart-case` | Enable smart case regex searching (case insensitive unless pattern contains upper case characters) | `true` | +| `smart-case` | Enable smart case regex searching (case-insensitive unless pattern contains upper case characters) | `true` | | `wrap-around`| Whether the search should wrap after depleting the matches | `true` | ### `[editor.whitespace]` Section @@ -219,7 +218,7 @@ Options for rendering whitespace with visible characters. Use `:set whitespace.r | Key | Description | Default | |-----|-------------|---------| -| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline`. | `"none"` | +| `render` | Whether to render whitespace. May either be `"all"` or `"none"`, or a table with sub-keys `space`, `nbsp`, `tab`, and `newline` | `"none"` | | `characters` | Literal characters to use when rendering whitespace. Sub-keys may be any of `tab`, `space`, `nbsp`, `newline` or `tabpad` | See example below | Example @@ -247,7 +246,7 @@ Options for rendering vertical indent guides. | Key | Description | Default | | --- | --- | --- | -| `render` | Whether to render indent guides. | `false` | +| `render` | Whether to render indent guides | `false` | | `character` | Literal character to use for rendering the indent guide | `│` | | `skip-levels` | Number of indent levels to skip | `0` | @@ -272,7 +271,7 @@ gutters = ["diff", "diagnostics", "line-numbers", "spacer"] To customize the behavior of gutters, the `[editor.gutters]` section must be used. This section contains top level settings, as well as settings for -specific gutter components as sub-sections. +specific gutter components as subsections. | Key | Description | Default | | --- | --- | --- | @@ -314,14 +313,15 @@ Currently unused ### `[editor.soft-wrap]` Section -Options for soft wrapping lines that exceed the view width +Options for soft wrapping lines that exceed the view width: -| Key | Description | Default | -| --- | --- | --- | -| `enable` | Whether soft wrapping is enabled. | `false` | -| `max-wrap` | Maximum free space left at the end of the line. | `20` | -| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` | -| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` | +| Key | Description | Default | +| --- | --- | --- | +| `enable` | Whether soft wrapping is enabled. | `false` | +| `max-wrap` | Maximum free space left at the end of the line. | `20` | +| `max-indent-retain` | Maximum indentation to carry over when soft wrapping a line. | `40` | +| `wrap-indicator` | Text inserted before soft wrapped lines, highlighted with `ui.virtual.wrap` | `↪ ` | +| `wrap-at-text-width` | Soft wrap at `text-width` instead of using the full viewport size. | `false` | Example: diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index cdecb9b04832..48cb66f1f515 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -30,7 +30,7 @@ | eex | ✓ | | | | | ejs | ✓ | | | | | elixir | ✓ | ✓ | ✓ | `elixir-ls` | -| elm | ✓ | | | `elm-language-server` | +| elm | ✓ | ✓ | | `elm-language-server` | | elvish | ✓ | | | `elvish` | | env | ✓ | | | | | erb | ✓ | | | | @@ -85,6 +85,7 @@ | meson | ✓ | | ✓ | | | mint | | | | `mint` | | msbuild | ✓ | | ✓ | | +| nasm | ✓ | ✓ | | | | nickel | ✓ | | ✓ | `nls` | | nix | ✓ | | | `nil` | | nu | ✓ | | | | @@ -103,6 +104,7 @@ | prisma | ✓ | | | `prisma-language-server` | | prolog | | | | `swipl` | | protobuf | ✓ | | ✓ | | +| prql | ✓ | | | | | purescript | ✓ | | | `purescript-language-server` | | python | ✓ | ✓ | ✓ | `pylsp` | | qml | ✓ | | ✓ | `qmlls` | @@ -112,6 +114,7 @@ | rescript | ✓ | ✓ | | `rescript-language-server` | | rmarkdown | ✓ | | ✓ | `R` | | ron | ✓ | | ✓ | | +| rst | ✓ | | | | | ruby | ✓ | ✓ | ✓ | `solargraph` | | rust | ✓ | ✓ | ✓ | `rust-analyzer` | | sage | ✓ | ✓ | | | @@ -125,6 +128,7 @@ | sshclientconfig | ✓ | | | | | starlark | ✓ | ✓ | | | | svelte | ✓ | | | `svelteserver` | +| sway | ✓ | ✓ | ✓ | `forc` | | swift | ✓ | | | `sourcekit-lsp` | | tablegen | ✓ | ✓ | ✓ | | | task | ✓ | | | | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index ab36997c7016..8b367aad8bc4 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -49,6 +49,7 @@ | `:update` | Write changes only if the file has been modified. | | `:lsp-workspace-command` | Open workspace command picker | | `:lsp-restart` | Restarts the Language Server that is in use by the current doc | +| `:lsp-stop` | Stops 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. | | `:debug-remote`, `:dbg-tcp` | Connect to a debug adapter by TCP address and start a debugging session from a given template with given parameters. | @@ -75,3 +76,4 @@ | `:pipe` | Pipe each selection to the shell command. | | `:pipe-to` | Pipe each selection to the shell command, ignoring output. | | `:run-shell-command`, `:sh` | Run a shell command | +| `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. | diff --git a/book/src/guides/README.md b/book/src/guides/README.md index e0c44ce7d1f1..c25768e68961 100644 --- a/book/src/guides/README.md +++ b/book/src/guides/README.md @@ -1,4 +1,4 @@ # Guides This section contains guides for adding new language server configurations, -tree-sitter grammars, textobject queries, etc. +tree-sitter grammars, textobject queries, and other similar items. diff --git a/book/src/guides/adding_languages.md b/book/src/guides/adding_languages.md index 6598b9bf7488..b92af4028e9a 100644 --- a/book/src/guides/adding_languages.md +++ b/book/src/guides/adding_languages.md @@ -1,45 +1,52 @@ -# Adding languages +# Adding new languages to Helix + +In order to add a new language to Helix, you will need to follow the steps +below. ## Language configuration -To add a new language, you need to add a `[[language]]` entry to the -`languages.toml` (see the [language configuration section]). +1. Add a new `[[language]]` entry in the `languages.toml` file and provide the + necessary configuration for the new language. For more information on + language configuration, refer to the + [language configuration section](../languages.md) of the documentation. +2. If you are adding a new language or updating an existing language server + configuration, run the command `cargo xtask docgen` to update the + [Language Support](../lang-support.md) documentation. -When adding a new language or Language Server configuration for an existing -language, run `cargo xtask docgen` to add the new configuration to the -[Language Support][lang-support] docs before creating a pull request. -When adding a Language Server configuration, be sure to update the -[Language Server Wiki][install-lsp-wiki] with installation notes. +> 💡 If you are adding a new Language Server configuration, make sure to update +> the +> [Language Server Wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) +> with the installation instructions. ## Grammar configuration -If a tree-sitter grammar is available for the language, add a new `[[grammar]]` -entry to `languages.toml`. - -You may use the `source.path` key rather than `source.git` with an absolute path -to a locally available grammar for testing, but switch to `source.git` before -submitting a pull request. +1. If a tree-sitter grammar is available for the new language, add a new + `[[grammar]]` entry to the `languages.toml` file. +2. If you are testing the grammar locally, you can use the `source.path` key + with an absolute path to the grammar. However, before submitting a pull + request, make sure to switch to using `source.git`. ## Queries -For a language to have syntax-highlighting and indentation among -other things, you have to add queries. Add a directory for your -language with the path `runtime/queries//`. The tree-sitter -[website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) -gives more info on how to write queries. - -> NOTE: When evaluating queries, the first matching query takes -precedence, which is different from other editors like Neovim where -the last matching query supersedes the ones before it. See -[this issue][neovim-query-precedence] for an example. - -## Common Issues - -- If you get errors when running after switching branches, you may have to update the tree-sitter grammars. Run `hx --grammar fetch` to fetch the grammars and `hx --grammar build` to build any out-of-date grammars. - -- If a parser is segfaulting or you want to remove the parser, make sure to remove the compiled parser in `runtime/grammar/.so` - -[language configuration section]: ../languages.md -[neovim-query-precedence]: https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090 -[install-lsp-wiki]: https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers -[lang-support]: ../lang-support.md +1. In order to provide syntax highlighting and indentation for the new language, + you will need to add queries. +2. Create a new directory for the language with the path + `runtime/queries//`. +3. Refer to the + [tree-sitter website](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#queries) + for more information on writing queries. + +> 💡 In Helix, the first matching query takes precedence when evaluating +> queries, which is different from other editors such as Neovim where the last +> matching query supersedes the ones before it. See +> [this issue](https://github.com/helix-editor/helix/pull/1170#issuecomment-997294090) +> for an example. + +## Common issues + +- If you encounter errors when running Helix after switching branches, you may + need to update the tree-sitter grammars. Run the command `hx --grammar fetch` + to fetch the grammars and `hx --grammar build` to build any out-of-date + grammars. +- If a parser is causing a segfault, or you want to remove it, make sure to + remove the compiled parser located at `runtime/grammars/.so`. diff --git a/book/src/guides/indent.md b/book/src/guides/indent.md index 0e259289781e..b660d785734e 100644 --- a/book/src/guides/indent.md +++ b/book/src/guides/indent.md @@ -1,4 +1,4 @@ -# Adding Indent Queries +# Adding indent queries Helix uses tree-sitter to correctly indent new lines. This requires a tree-sitter grammar and an `indent.scm` query file placed in @@ -36,7 +36,7 @@ changed by using a `#set!` declaration anywhere in the pattern: (#set! "scope" "all")) ``` -## Capture Types +## Capture types - `@indent` (default scope `tail`): Increase the indent level by 1. Multiple occurrences in the same line diff --git a/book/src/guides/textobject.md b/book/src/guides/textobject.md index 8a2173547541..405f11c1b00a 100644 --- a/book/src/guides/textobject.md +++ b/book/src/guides/textobject.md @@ -1,14 +1,14 @@ -# Adding Textobject Queries +# Adding textobject queries -Textobjects that are language specific ([like functions, classes, etc][textobjects]) -require an accompanying tree-sitter grammar and a `textobjects.scm` query file +Helix supports textobjects that are language specific, such as functions, classes, etc. +These textobjects require an accompanying tree-sitter grammar and a `textobjects.scm` query file to work properly. Tree-sitter allows us to query the source code syntax tree and capture specific parts of it. The queries are written in a lisp dialect. More information on how to write queries can be found in the [official tree-sitter documentation][tree-sitter-queries]. Query files should be placed in `runtime/queries/{language}/textobjects.scm` -when contributing. Note that to test the query files locally you should put +when contributing to Helix. Note that to test the query files locally you should put them under your local runtime directory (`~/.config/helix/runtime` on Linux for example). @@ -28,9 +28,9 @@ The following [captures][tree-sitter-captures] are recognized: [Example query files][textobject-examples] can be found in the helix GitHub repository. -## Queries for Textobject Based Navigation +## Queries for textobject based navigation -[Tree-sitter based navigation][textobjects-nav] is done using captures in the +Tree-sitter based navigation in Helix is done using captures in the following order: - `object.movement` @@ -38,12 +38,10 @@ following order: - `object.inside` For example if a `function.around` capture has been already defined for a language -in it's `textobjects.scm` file, function navigation should also work automatically. +in its `textobjects.scm` file, function navigation should also work automatically. `function.movement` should be defined only if the node captured by `function.around` doesn't make sense in a navigation context. -[textobjects]: ../usage.md#textobjects -[textobjects-nav]: ../usage.md#tree-sitter-textobject-based-navigation [tree-sitter-queries]: https://tree-sitter.github.io/tree-sitter/using-parsers#query-syntax [tree-sitter-captures]: https://tree-sitter.github.io/tree-sitter/using-parsers#capturing-nodes [textobject-examples]: https://github.com/search?q=repo%3Ahelix-editor%2Fhelix+filename%3Atextobjects.scm&type=Code&ref=advsearch&l=&l= diff --git a/book/src/install.md b/book/src/install.md index 7df9e6c778ea..bd3f502b6e95 100644 --- a/book/src/install.md +++ b/book/src/install.md @@ -1,180 +1,239 @@ -# Installation - -We provide pre-built binaries on the [GitHub Releases page](https://github.com/helix-editor/helix/releases). +# Installing Helix + + +- [Pre-built binaries](#pre-built-binaries) +- [Linux, macOS, Windows and OpenBSD packaging status](#linux-macos-windows-and-openbsd-packaging-status) +- [Linux](#linux) + - [Ubuntu](#ubuntu) + - [Fedora/RHEL](#fedorarhel) + - [Arch Linux community](#arch-linux-community) + - [NixOS](#nixos) +- [macOS](#macos) + - [Homebrew Core](#homebrew-core) +- [Windows](#windows) + - [Scoop](#scoop) + - [Chocolatey](#chocolatey) + - [MSYS2](#msys2) +- [Building from source](#building-from-source) + - [Configuring Helix's runtime files](#configuring-helixs-runtime-files) + - [Validating the installation](#validating-the-installation) + - [Configure the desktop shortcut](#configure-the-desktop-shortcut) + + +To install Helix, follow the instructions specific to your operating system. +Note that: + +- To get the latest nightly version of Helix, you need to + [build from source](#building-from-source). + +- To take full advantage of Helix, install the language servers for your + preferred programming languages. See the + [wiki](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) + for instructions. + +## Pre-built binaries + +Download pre-built binaries from the +[GitHub Releases page](https://github.com/helix-editor/helix/releases). Add the binary to your system's `$PATH` to use it from the command +line. + +## Linux, macOS, Windows and OpenBSD packaging status + +Helix is available for Linux, macOS and Windows via the official repositories listed below. [![Packaging status](https://repology.org/badge/vertical-allrepos/helix.svg)](https://repology.org/project/helix/versions) -## OSX +## Linux -Helix is available in homebrew-core: +The following third party repositories are available: -``` -brew install helix -``` +### Ubuntu -## Linux +Helix is available via [Maveonair's PPA](https://launchpad.net/~maveonair/+archive/ubuntu/helix-editor): -### NixOS +```sh +sudo add-apt-repository ppa:maveonair/helix-editor +sudo apt update +sudo apt install helix +``` -A [flake](https://nixos.wiki/wiki/Flakes) containing the package is available in -the project root. The flake can also be used to spin up a reproducible development -shell for working on Helix with `nix develop`. +### Fedora/RHEL -Flake outputs are cached for each push to master using -[Cachix](https://www.cachix.org/). The flake is configured to -automatically make use of this cache assuming the user accepts -the new settings on first use. +Helix is available via `copr`: -If you are using a version of Nix without flakes enabled you can -[install Cachix cli](https://docs.cachix.org/installation); `cachix use helix` will -configure Nix to use cached outputs when possible. +```sh +sudo dnf copr enable varlad/helix +sudo dnf install helix +``` -### Arch Linux +### Arch Linux community -Releases are available in the `community` repository. +Releases are available in the `community` repository: -A [helix-git](https://aur.archlinux.org/packages/helix-git/) package is also available on the AUR, which builds the master branch. +```sh +sudo pacman -S helix +``` +Additionally, a [helix-git](https://aur.archlinux.org/packages/helix-git/) package is available +in the AUR, which builds the master branch. -### Fedora Linux +### NixOS -You can install the COPR package for Helix via +Helix is available as a [flake](https://nixos.wiki/wiki/Flakes) in the project +root. Use `nix develop` to spin up a reproducible development shell. Outputs are +cached for each push to master using [Cachix](https://www.cachix.org/). The +flake is configured to automatically make use of this cache assuming the user +accepts the new settings on first use. -``` -sudo dnf copr enable varlad/helix -sudo dnf install helix -``` +If you are using a version of Nix without flakes enabled, +[install Cachix CLI](https://docs.cachix.org/installation) and use +`cachix use helix` to configure Nix to use cached outputs when possible. + +## macOS -### Void Linux +### Homebrew Core -``` -sudo xbps-install helix +```sh +brew install helix ``` ## Windows -Helix can be installed using [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/) +Install on Windows using [Scoop](https://scoop.sh/), [Chocolatey](https://chocolatey.org/) or [MSYS2](https://msys2.org/). -**Scoop:** +### Scoop -``` +```sh scoop install helix ``` -**Chocolatey:** +### Chocolatey -``` +```sh choco install helix ``` -**MSYS2:** - -Choose the [proper command](https://www.msys2.org/docs/package-naming/) for your system from below: +### MSYS2 - - For 32 bit Windows 7 or above: +For 64-bit Windows 8.1 or above: +```sh +pacman -S mingw-w64-ucrt-x86_64-helix ``` -pacman -S mingw-w64-i686-helix -``` - - - For 64 bit Windows 7 or above: -``` -pacman -S mingw-w64-x86_64-helix -``` +## Building from source - - For 64 bit Windows 8.1 or above: +Clone the repository: -``` -pacman -S mingw-w64-ucrt-x86_64-helix +```sh +git clone https://github.com/helix-editor/helix +cd helix ``` -## Build from source +Compile from source: -``` -git clone https://github.com/helix-editor/helix -cd helix +```sh cargo install --path helix-term --locked ``` -This will install the `hx` binary to `$HOME/.cargo/bin` and build tree-sitter grammars in `./runtime/grammars`. +This command will create the `hx` executable and construct the tree-sitter +grammars in the local `runtime` folder. To build the tree-sitter grammars requires +a c++ compiler to be installed, for example `gcc-c++`. -If you are using the musl-libc instead of glibc the following environment variable must be set during the build -to ensure tree sitter grammars can be loaded correctly: +> 💡 If you are using the musl-libc instead of glibc the following environment variable must be set during the build +> to ensure tree-sitter grammars can be loaded correctly: +> +> ```sh +> RUSTFLAGS="-C target-feature=-crt-static" +> ``` -``` -RUSTFLAGS="-C target-feature=-crt-static" -``` +> 💡 Tree-sitter grammars can be fetched and compiled if not pre-packaged. Fetch +> grammars with `hx --grammar fetch` (requires `git`) and compile them with +> `hx --grammar build` (requires a C++ compiler). This will install them in +> the `runtime` directory within the user's helix config directory (more +> [details below](#multiple-runtime-directories)). +### Configuring Helix's runtime files -Helix also needs its runtime files so make sure to copy/symlink the `runtime/` directory into the -config directory (for example `~/.config/helix/runtime` on Linux/macOS). This location can be overridden -via the `HELIX_RUNTIME` environment variable. +#### Linux and macOS -| OS | Command | -| -------------------- | ------------------------------------------------ | -| Windows (Cmd) | `xcopy /e /i runtime %AppData%\helix\runtime` | -| Windows (PowerShell) | `xcopy /e /i runtime $Env:AppData\helix\runtime` | -| Linux / macOS | `ln -s $PWD/runtime ~/.config/helix/runtime` | +Either set the `HELIX_RUNTIME` environment variable to point to the runtime files and add it to your `~/.bashrc` or equivalent: -Starting with Windows Vista you can also create symbolic links on Windows. Note that this requires -elevated privileges - i.e. PowerShell or Cmd must be run as administrator. +```sh +HELIX_RUNTIME=/home/user-name/src/helix/runtime +``` -**PowerShell:** +Or, create a symlink in `~/.config/helix` that links to the source code directory: -```powershell -New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime" +```sh +ln -s $PWD/runtime ~/.config/helix/runtime ``` -Note: "runtime" must be the absolute path to the runtime directory. -**Cmd:** +#### Windows + +Either set the `HELIX_RUNTIME` environment variable to point to the runtime files using the Windows setting (search for +`Edit environment variables for your account`) or use the `setx` command in +Cmd: -```cmd -cd %appdata%\helix -mklink /D runtime "\runtime" +```sh +setx HELIX_RUNTIME "%userprofile%\source\repos\helix\runtime" ``` -The runtime location can be overridden via the `HELIX_RUNTIME` environment variable. +> 💡 `%userprofile%` resolves to your user directory like +> `C:\Users\Your-Name\` for example. -> NOTE: if `HELIX_RUNTIME` is set prior to calling `cargo install --path helix-term --locked`, -> tree-sitter grammars will be built in `$HELIX_RUNTIME/grammars`. +Or, create a symlink in `%appdata%\helix\` that links to the source code directory: -If you plan on keeping the repo locally, an alternative to copying/symlinking -runtime files is to set `HELIX_RUNTIME=/path/to/helix/runtime` -(`HELIX_RUNTIME=$PWD/runtime` if you're in the helix repo directory). +| Method | Command | +| ---------- | -------------------------------------------------------------------------------------- | +| PowerShell | `New-Item -ItemType Junction -Target "runtime" -Path "$Env:AppData\helix\runtime"` | +| Cmd | `cd %appdata%\helix`
`mklink /D runtime "%userprofile%\src\helix\runtime"` | -To use Helix in desktop environments that supports [XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html), including Gnome and KDE, copy the provided `.desktop` file to the correct folder: +> 💡 On Windows, creating a symbolic link may require running PowerShell or +> Cmd as an administrator. -```bash -cp contrib/Helix.desktop ~/.local/share/applications -``` +#### Multiple runtime directories -To use another terminal than the default, you will need to modify the `.desktop` file. For example, to use `kitty`: +When Helix finds multiple runtime directories it will search through them for files in the +following order: -```bash -sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop -sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop -``` +1. `runtime/` sibling directory to `$CARGO_MANIFEST_DIR` directory (this is intended for + developing and testing helix only). +2. `runtime/` subdirectory of OS-dependent helix user config directory. +3. `$HELIX_RUNTIME`. +4. `runtime/` subdirectory of path to Helix executable. -Please note: there is no icon for Helix yet, so the system default will be used. +This order also sets the priority for selecting which file will be used if multiple runtime +directories have files with the same name. -## Finishing up the installation +### Validating the installation -To make sure everything is set up as expected you should finally run the helix healthcheck via +To make sure everything is set up as expected you should run the Helix health +check: -``` +```sh hx --health ``` -For more information on the information displayed in the health check results refer to [Healthcheck](https://github.com/helix-editor/helix/wiki/Healthcheck). +For more information on the health check results refer to +[Health check](https://github.com/helix-editor/helix/wiki/Healthcheck). -### Building tree-sitter grammars +### Configure the desktop shortcut -Tree-sitter grammars must be fetched and compiled if not pre-packaged. -Fetch grammars with `hx --grammar fetch` (requires `git`) and compile them -with `hx --grammar build` (requires a C++ compiler). +If your desktop environment supports the +[XDG desktop menu](https://specifications.freedesktop.org/menu-spec/menu-spec-latest.html) +you can configure Helix to show up in the application menu by copying the +provided `.desktop` and icon files to their correct folders: -### Installing language servers +```sh +cp contrib/Helix.desktop ~/.local/share/applications +cp contrib/helix.png ~/.icons # or ~/.local/share/icons +``` + +To use another terminal than the system default, you can modify the `.desktop` +file. For example, to use `kitty`: -Language servers can optionally be installed if you want their features (auto-complete, diagnostics etc.). -Follow the [instructions on the wiki page](https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers) to add your language servers of choice. +```sh +sed -i "s|Exec=hx %F|Exec=kitty hx %F|g" ~/.local/share/applications/Helix.desktop +sed -i "s|Terminal=true|Terminal=false|g" ~/.local/share/applications/Helix.desktop +``` diff --git a/book/src/keymap.md b/book/src/keymap.md index 8864ab69617d..173728f278db 100644 --- a/book/src/keymap.md +++ b/book/src/keymap.md @@ -14,14 +14,14 @@ - [Space mode](#space-mode) - [Popup](#popup) - [Unimpaired](#unimpaired) -- [Insert Mode](#insert-mode) -- [Select / extend mode](#select--extend-mode) +- [Insert mode](#insert-mode) +- [Select / extend mode](#select-extend-mode) - [Picker](#picker) - [Prompt](#prompt) > 💡 Mappings marked (**LSP**) require an active language server for the file. -> 💡 Mappings marked (**TS**) require a tree-sitter grammar for the filetype. +> 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type. ## Normal mode @@ -109,7 +109,7 @@ | Key | Description | Command | | ----- | ----------- | ------- | | `s` | Select all regex matches inside selections | `select_regex` | -| `S` | Split selection into subselections on regex matches | `split_selection` | +| `S` | Split selection into sub selections on regex matches | `split_selection` | | `Alt-s` | Split selection on newlines | `split_selection_on_newline` | | `Alt-_ ` | Merge consecutive selections | `merge_consecutive_selections` | | `&` | Align selection in columns | `align_selections` | @@ -141,7 +141,7 @@ ### Search -Search commands all operate on the `/` register by default. Use `"` to operate on a different one. +Search commands all operate on the `/` register by default. To use a different register, use `"`. | Key | Description | Command | | ----- | ----------- | ------- | @@ -175,9 +175,8 @@ Accessed by typing `z` in [normal mode](#normal-mode). View mode is intended for scrolling and manipulating the view without changing the selection. The "sticky" variant of this mode (accessed by typing `Z` in -normal mode) is persistent; use the Escape key to return to normal mode after -usage (useful when you're simply looking over text and not actively editing -it). +normal mode) is persistent and can be exited using the escape key. This is +useful when you're simply looking over text and not actively editing it. | Key | Description | Command | @@ -225,7 +224,7 @@ Jumps to various locations. Accessed by typing `m` in [normal mode](#normal-mode). See the relevant section in [Usage](./usage.md) for an explanation about -[surround](./usage.md#surround) and [textobject](./usage.md#textobjects) usage. +[surround](./usage.md#surround) and [textobject](./usage.md#navigating-using-tree-sitter-textobjects) usage. | Key | Description | Command | | ----- | ----------- | ------- | @@ -242,7 +241,7 @@ TODO: Mappings for selecting syntax nodes (a superset of `[`). Accessed by typing `Ctrl-w` in [normal mode](#normal-mode). -This layer is similar to Vim keybindings as Kakoune does not support window. +This layer is similar to Vim keybindings as Kakoune does not support windows. | Key | Description | Command | | ----- | ------------- | ------- | @@ -268,30 +267,32 @@ Accessed by typing `Space` in [normal mode](#normal-mode). This layer is a kludge of mappings, mostly pickers. -| Key | Description | Command | -| ----- | ----------- | ------- | -| `f` | Open file picker | `file_picker` | -| `F` | Open file picker at current working directory | `file_picker_in_current_directory` | -| `b` | Open buffer picker | `buffer_picker` | -| `j` | Open jumplist picker | `jumplist_picker` | -| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` | -| `s` | Open document symbol picker (**LSP**) | `symbol_picker` | -| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` | -| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` | -| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` | -| `r` | Rename symbol (**LSP**) | `rename_symbol` | -| `a` | Apply code action (**LSP**) | `code_action` | -| `'` | Open last fuzzy picker | `last_picker` | -| `w` | Enter [window mode](#window-mode) | N/A | -| `p` | Paste system clipboard after selections | `paste_clipboard_after` | -| `P` | Paste system clipboard before selections | `paste_clipboard_before` | -| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | -| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | -| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | -| `/` | Global search in workspace folder | `global_search` | -| `?` | Open command palette | `command_palette` | - -> TIP: Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file. +| Key | Description | Command | +| ----- | ----------- | ------- | +| `f` | Open file picker | `file_picker` | +| `F` | Open file picker at current working directory | `file_picker_in_current_directory` | +| `b` | Open buffer picker | `buffer_picker` | +| `j` | Open jumplist picker | `jumplist_picker` | +| `g` | Debug (experimental) | N/A | +| `k` | Show documentation for item under cursor in a [popup](#popup) (**LSP**) | `hover` | +| `s` | Open document symbol picker (**LSP**) | `symbol_picker` | +| `S` | Open workspace symbol picker (**LSP**) | `workspace_symbol_picker` | +| `d` | Open document diagnostics picker (**LSP**) | `diagnostics_picker` | +| `D` | Open workspace diagnostics picker (**LSP**) | `workspace_diagnostics_picker` | +| `r` | Rename symbol (**LSP**) | `rename_symbol` | +| `a` | Apply code action (**LSP**) | `code_action` | +| `h` | Select symbol references (**LSP**) | `select_references_to_symbol_under_cursor` | +| `'` | Open last fuzzy picker | `last_picker` | +| `w` | Enter [window mode](#window-mode) | N/A | +| `p` | Paste system clipboard after selections | `paste_clipboard_after` | +| `P` | Paste system clipboard before selections | `paste_clipboard_before` | +| `y` | Join and yank selections to clipboard | `yank_joined_to_clipboard` | +| `Y` | Yank main selection to clipboard | `yank_main_selection_to_clipboard` | +| `R` | Replace selections by clipboard contents | `replace_selections_with_clipboard` | +| `/` | Global search in workspace folder | `global_search` | +| `?` | Open command palette | `command_palette` | + +> 💡 Global search displays results in a fuzzy picker, use `Space + '` to bring it back up after opening a file. ##### Popup @@ -304,7 +305,7 @@ Displays documentation for item under cursor. #### Unimpaired -Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). +These mappings are in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaired). | Key | Description | Command | | ----- | ----------- | ------- | @@ -333,12 +334,13 @@ Mappings in the style of [vim-unimpaired](https://github.com/tpope/vim-unimpaire ## Insert mode -Insert mode bindings are somewhat minimal by default. Helix is designed to +Insert mode bindings are minimal by default. Helix is designed to be a modal editor, and this is reflected in the user experience and internal -mechanics. For example, changes to the text are only saved for undos when -escaping from insert mode to normal mode. For this reason, new users are -strongly encouraged to learn the modal editing paradigm to get the smoothest -experience. +mechanics. Changes to the text are only saved for undos when +escaping from insert mode to normal mode. + +> 💡 New users are strongly encouraged to learn the modal editing paradigm +> to get the smoothest experience. | Key | Description | Command | | ----- | ----------- | ------- | @@ -350,7 +352,7 @@ experience. | `Alt-d`, `Alt-Delete` | Delete next word | `delete_word_forward` | | `Ctrl-u` | Delete to start of line | `kill_to_line_start` | | `Ctrl-k` | Delete to end of line | `kill_to_line_end` | -| `Ctrl-h`, `Backspace` | Delete previous char | `delete_char_backward` | +| `Ctrl-h`, `Backspace`, `Shift-Backspace` | Delete previous char | `delete_char_backward` | | `Ctrl-d`, `Delete` | Delete next char | `delete_char_forward` | | `Ctrl-j`, `Enter` | Insert new line | `insert_newline` | @@ -368,8 +370,8 @@ with modal editors. | `Home` | Move to line start | `goto_line_start` | | `End` | Move to line end | `goto_line_end_newline` | -If you want to disable them in insert mode as you become more comfortable with modal editing, you can use -the following in your `config.toml`: +As you become more comfortable with modal editing, you may want to disable some +insert mode bindings. You can do this by editing your `config.toml` file. ```toml [keys.insert] @@ -385,7 +387,7 @@ end = "no_op" ## Select / extend mode -This mode echoes Normal mode, but changes any movements to extend +Select mode echoes Normal mode, but changes any movements to extend selections rather than replace them. Goto motions are also changed to extend, so that `vgl` for example extends the selection to the end of the line. @@ -431,7 +433,7 @@ Keys to use within prompt, Remapping currently not supported. | `Alt-d`, `Alt-Delete`, `Ctrl-Delete` | Delete next word | | `Ctrl-u` | Delete to start of line | | `Ctrl-k` | Delete to end of line | -| `Backspace`, `Ctrl-h` | Delete previous char | +| `Backspace`, `Ctrl-h`, `Shift-Backspace` | Delete previous char | | `Delete`, `Ctrl-d` | Delete next char | | `Ctrl-s` | Insert a word under doc cursor, may be changed to Ctrl-r Ctrl-w later | | `Ctrl-p`, `Up` | Select previous history | diff --git a/book/src/lang-support.md b/book/src/lang-support.md index 6a08cd699813..3f96673bc953 100644 --- a/book/src/lang-support.md +++ b/book/src/lang-support.md @@ -1,10 +1,10 @@ # Language Support -The following languages and Language Servers are supported. In order to use +The following languages and Language Servers are supported. To use Language Server features, you must first [install][lsp-install-wiki] the appropriate Language Server. -Check the language support in your installed helix version with `hx --health`. +You can check the language support in your installed helix version with `hx --health`. Also see the [Language Configuration][lang-config] docs and the [Adding Languages][adding-languages] guide for more language configuration information. diff --git a/book/src/languages.md b/book/src/languages.md index 0646b9af9580..5ed69505d130 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -5,13 +5,15 @@ in `languages.toml` files. ## `languages.toml` files -There are three possible `languages.toml` files. The first is compiled into -Helix and lives in the [Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml). -This provides the default configurations for languages and language servers. +There are three possible locations for a `languages.toml` file: -You may define a `languages.toml` in your [configuration directory](./configuration.md) -which overrides values from the built-in language configuration. For example -to disable auto-LSP-formatting in Rust: +1. In the Helix source code, this lives in the + [Helix repository](https://github.com/helix-editor/helix/blob/master/languages.toml). + It provides the default configurations for languages and language servers. + +2. In your [configuration directory](./configuration.md). This overrides values + from the built-in language configuration. For example to disable + auto-LSP-formatting in Rust: ```toml # in /helix/languages.toml @@ -21,10 +23,10 @@ name = "rust" auto-format = false ``` -Language configuration may also be overridden local to a project by creating -a `languages.toml` file under a `.helix` directory. Its settings will be merged -with the language configuration in the configuration directory and the built-in -configuration. +3. In a `.helix` folder in your project. Language configuration may also be + overridden local to a project by creating a `languages.toml` file in a + `.helix` folder. Its settings will be merged with the language configuration + in the configuration directory and the built-in configuration. ## Language configuration @@ -56,16 +58,16 @@ These configuration keys are available: | `auto-format` | Whether to autoformat this language when saving | | `diagnostic-severity` | Minimal severity of diagnostic for it to be displayed. (Allowed values: `Error`, `Warning`, `Info`, `Hint`) | | `comment-token` | The token to use as a comment-token | -| `indent` | The indent to use. Has sub keys `tab-width` and `unit` | +| `indent` | The indent to use. Has sub keys `unit` (the text inserted into the document when indenting; usually set to N spaces or `"\t"` for tabs) and `tab-width` (the number of spaces rendered for a tab) | | `language-server` | The Language Server to run. See the Language Server configuration section below. | | `config` | Language Server configuration | | `grammar` | The tree-sitter grammar to use (defaults to the value of `name`) | | `formatter` | The formatter for the language, it will take precedence over the lsp when defined. The formatter must be able to take the original file as input from stdin and write the formatted file to stdout | -| `max-line-length` | Maximum line length. Used for the `:reflow` command and soft-wrapping | +| `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap_at_text_width` is set, defaults to `editor.text-width` | ### File-type detection and the `file-types` key -Helix determines which language configuration to use with the `file-types` key +Helix determines which language configuration to use based on the `file-types` key from the above section. `file-types` is a list of strings or tables, for example: diff --git a/book/src/remapping.md b/book/src/remapping.md index 8339e05fce8c..d762c6add4fc 100644 --- a/book/src/remapping.md +++ b/book/src/remapping.md @@ -1,18 +1,18 @@ -# Key Remapping +# Key remapping -One-way key remapping is temporarily supported via a simple TOML configuration +Helix currently supports one-way key remapping through a simple TOML configuration file. (More powerful solutions such as rebinding via commands will be available in the future). -To remap keys, write a `config.toml` file in your `helix` configuration -directory (default `~/.config/helix` in Linux systems) with a structure like +To remap keys, create a `config.toml` file in your `helix` configuration +directory (default `~/.config/helix` on Linux systems) with a structure like this: ```toml # At most one section each of 'keys.normal', 'keys.insert' and 'keys.select' [keys.normal] -C-s = ":w" # Maps the Ctrl-s to the typable command :w which is an alias for :write (save file) -C-o = ":open ~/.config/helix/config.toml" # Maps the Ctrl-o to opening of the helix config file +C-s = ":w" # Maps Ctrl-s to the typable command :w which is an alias for :write (save file) +C-o = ":open ~/.config/helix/config.toml" # Maps Ctrl-o to opening of the helix config file a = "move_char_left" # Maps the 'a' key to the move_char_left command w = "move_line_up" # Maps the 'w' key move_line_up "C-S-esc" = "extend_line" # Maps Ctrl-Shift-Escape to extend_line @@ -20,10 +20,9 @@ g = { a = "code_action" } # Maps `ga` to show possible code actions "ret" = ["open_below", "normal_mode"] # Maps the enter key to open_below then re-enter normal mode [keys.insert] -"A-x" = "normal_mode" # Maps Alt-X to enter normal mode +"A-x" = "normal_mode" # Maps Alt-X to enter normal mode j = { k = "normal_mode" } # Maps `jk` to exit insert mode ``` -> NOTE: Typable commands can also be remapped, remember to keep the `:` prefix to indicate it's a typable command. ## Minor modes @@ -76,5 +75,5 @@ Ctrl, Shift and Alt modifiers are encoded respectively with the prefixes Keys can be disabled by binding them to the `no_op` command. -Commands can be found at [Keymap](https://docs.helix-editor.com/keymap.html) Commands. -> Commands can also be found in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`. +A list of commands is available in the [Keymap](https://docs.helix-editor.com/keymap.html) documentation + and in the source code at [`helix-term/src/commands.rs`](https://github.com/helix-editor/helix/blob/master/helix-term/src/commands.rs) at the invocation of `static_commands!` macro and the `TypableCommandList`. diff --git a/book/src/themes.md b/book/src/themes.md index 9b7e97a1c9ff..af238e949b83 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -1,14 +1,15 @@ # Themes -To use a theme add `theme = ""` to your [`config.toml`](./configuration.md) at the very top of the file before the first section or select it during runtime using `:theme `. +To use a theme add `theme = ""` to the top of your [`config.toml`](./configuration.md) file, or select it during runtime using `:theme `. ## Creating a theme -Create a file with the name of your theme as file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes`). The directory might have to be created beforehand. +Create a file with the name of your theme as the file name (i.e `mytheme.toml`) and place it in your `themes` directory (i.e `~/.config/helix/themes` or `%AppData%\helix\themes` on Windows). The directory might have to be created beforehand. -The names "default" and "base16_default" are reserved for the builtin themes and cannot be overridden by user defined themes. +> 💡 The names "default" and "base16_default" are reserved for built-in themes +> and cannot be overridden by user-defined themes. -The default theme.toml can be found [here](https://github.com/helix-editor/helix/blob/master/theme.toml), and user submitted themes [here](https://github.com/helix-editor/helix/blob/master/runtime/themes). +### Overview Each line in the theme file is specified as below: @@ -16,7 +17,7 @@ Each line in the theme file is specified as below: key = { fg = "#ffffff", bg = "#000000", underline = { color = "#ff0000", style = "curl"}, modifiers = ["bold", "italic"] } ``` -where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline `style`/`color`, and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. +Where `key` represents what you want to style, `fg` specifies the foreground color, `bg` the background color, `underline` the underline `style`/`color`, and `modifiers` is a list of style modifiers. `bg`, `underline` and `modifiers` can be omitted to defer to the defaults. To specify only the foreground color: @@ -24,15 +25,30 @@ To specify only the foreground color: key = "#ffffff" ``` -if the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys). +If the key contains a dot `'.'`, it must be quoted to prevent it being parsed as a [dotted key](https://toml.io/en/v1.0.0#keys). ```toml "key.key" = "#ffffff" ``` +For inspiration, you can find the default `theme.toml` +[here](https://github.com/helix-editor/helix/blob/master/theme.toml) and +user-submitted themes +[here](https://github.com/helix-editor/helix/blob/master/runtime/themes). + +### Using the linter + +Use the supplied linting tool to check for errors and missing scopes: + +```sh +cargo xtask themelint onedark # replace onedark with +``` + +## The details of theme creation + ### Color palettes -It's recommended define a palette of named colors, and refer to them from the +It's recommended to define a palette of named colors, and refer to them in the configuration values in your theme. To do this, add a table called `palette` to your theme file: @@ -45,8 +61,8 @@ white = "#ffffff" black = "#000000" ``` -Remember that the `[palette]` table includes all keys after its header, -so you should define the palette after normal theme options. +Keep in mind that the `[palette]` table includes all keys after its header, +so it should be defined after the normal theme options. The default palette uses the terminal's default 16 colors, and the colors names are listed below. The `[palette]` section in the config file takes precedence @@ -73,9 +89,8 @@ over it and is merged into the default palette. ### Modifiers -The following values may be used as modifiers. - -Less common modifiers might not be supported by your terminal emulator. +The following values may be used as modifier, provided they are supported by +your terminal emulator. | Modifier | | --- | @@ -89,14 +104,13 @@ Less common modifiers might not be supported by your terminal emulator. | `hidden` | | `crossed_out` | -> Note: The `underlined` modifier is deprecated and only available for backwards compatibility. +> 💡 The `underlined` modifier is deprecated and only available for backwards compatibility. > Its behavior is equivalent to setting `underline.style="line"`. -### Underline Style - -One of the following values may be used as a value for `underline.style`. +### Underline style -Some styles might not be supported by your terminal emulator. +One of the following values may be used as a value for `underline.style`, providing it is +supported by your terminal emulator. | Modifier | | --- | @@ -109,7 +123,7 @@ Some styles might not be supported by your terminal emulator. ### Inheritance -Extend upon other themes by setting the `inherits` property to an existing theme. +Extend other themes by setting the `inherits` property to an existing theme. ```toml inherits = "boo_berry" @@ -124,19 +138,19 @@ berry = "#2A2A4D" ### Scopes -The following is a list of scopes available to use for styling. +The following is a list of scopes available to use for styling: #### Syntax highlighting These keys match [tree-sitter scopes](https://tree-sitter.github.io/tree-sitter/syntax-highlighting#theme). -For a given highlight produced, styling will be determined based on the longest matching theme key. For example, the highlight `function.builtin.static` would match the key `function.builtin` rather than `function`. +When determining styling for a highlight, the longest matching theme key will be used. For example, if the highlight is `function.builtin.static`, the key `function.builtin` will be used instead of `function`. We use a similar set of scopes as -[SublimeText](https://www.sublimetext.com/docs/scope_naming.html). See also +[Sublime Text](https://www.sublimetext.com/docs/scope_naming.html). See also [TextMate](https://macromates.com/manual/en/language_grammars) scopes. -- `attribute` - Class attributes, html tag attributes +- `attribute` - Class attributes, HTML tag attributes - `type` - Types - `builtin` - Primitive types provided by the language (`int`, `usize`) @@ -144,7 +158,7 @@ We use a similar set of scopes as - `variant` - `constructor` -- `constant` (TODO: constant.other.placeholder for %v) +- `constant` (TODO: constant.other.placeholder for `%v`) - `builtin` Special constants provided by the language (`true`, `false`, `nil` etc) - `boolean` - `character` @@ -162,11 +176,11 @@ We use a similar set of scopes as - `comment` - Code comments - `line` - Single line comments (`//`) - - `block` - Block comments (e.g. (`/* */`) + - `block` - Block comments (e.g. (`/* */`) - `documentation` - Documentation comments (e.g. `///` in Rust) - `variable` - Variables - - `builtin` - Reserved language variables (`self`, `this`, `super`, etc) + - `builtin` - Reserved language variables (`self`, `this`, `super`, etc.) - `parameter` - Function parameters - `other` - `member` - Fields of composite data types (e.g. structs, unions) @@ -186,10 +200,10 @@ We use a similar set of scopes as - `return` - `exception` - `operator` - `or`, `in` - - `directive` - Preprocessor directives (`#if` in C) + - `directive` - Preprocessor directives (`#if` in C) - `function` - `fn`, `func` - `storage` - Keywords describing how things are stored - - `type` - The type of something, `class`, `function`, `var`, `let`, etc. + - `type` - The type of something, `class`, `function`, `var`, `let`, etc. - `modifier` - Storage modifiers like `static`, `mut`, `const`, `ref`, etc. - `operator` - `||`, `+=`, `>` @@ -216,9 +230,9 @@ We use a similar set of scopes as - `bold` - `italic` - `link` - - `url` - urls pointed to by links - - `label` - non-url link references - - `text` - url and image descriptions in links + - `url` - URLs pointed to by links + - `label` - non-URL link references + - `text` - URL and image descriptions in links - `quote` - `raw` - `inline` @@ -232,19 +246,19 @@ We use a similar set of scopes as #### Interface -These scopes are used for theming the editor interface. +These scopes are used for theming the editor interface: - `markup` - `normal` - - `completion` - for completion doc popup ui - - `hover` - for hover popup ui + - `completion` - for completion doc popup UI + - `hover` - for hover popup UI - `heading` - - `completion` - for completion doc popup ui - - `hover` - for hover popup ui + - `completion` - for completion doc popup UI + - `hover` - for hover popup UI - `raw` - `inline` - - `completion` - for completion doc popup ui - - `hover` - for hover popup ui + - `completion` - for completion doc popup UI + - `hover` - for hover popup UI | Key | Notes | @@ -270,9 +284,9 @@ These scopes are used for theming the editor interface. | `ui.statusline.insert` | Statusline mode during insert mode ([only if `editor.color-modes` is enabled][editor-section]) | | `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) | | `ui.statusline.separator` | Separator character in statusline | -| `ui.popup` | Documentation popups (e.g Space + k) | +| `ui.popup` | Documentation popups (e.g. Space + k) | | `ui.popup.info` | Prompt for multiple key options | -| `ui.window` | Border lines separating splits | +| `ui.window` | Borderlines separating splits | | `ui.help` | Description box for commands | | `ui.text` | Command prompts, popup text, etc. | | `ui.text.focus` | | @@ -301,10 +315,4 @@ These scopes are used for theming the editor interface. | `diagnostic.warning` | Diagnostics warning (editing area) | | `diagnostic.error` | Diagnostics error (editing area) | -You can check compliance to spec with - -```shell -cargo xtask themelint onedark # replace onedark with -``` - [editor-section]: ./configuration.md#editor-section diff --git a/book/src/usage.md b/book/src/usage.md index a6eb9ec1d4f1..81cf83725609 100644 --- a/book/src/usage.md +++ b/book/src/usage.md @@ -1,22 +1,43 @@ -# Usage +# Using Helix -(Currently not fully documented, see the [keymappings](./keymap.md) list for more.) + +- [Registers](#registers) + - [User-defined registers](#user-defined-registers) + - [Special registers](#special-registers) +- [Surround](#surround) +- [Selecting and manipulating text with textobjects](#selecting-and-manipulating-text-with-textobjects) +- [Navigating using tree-sitter textobjects](#navigating-using-tree-sitter-textobjects) +- [Moving the selection with syntax-aware motions](#moving-the-selection-with-syntax-aware-motions) + -See [tutor](https://github.com/helix-editor/helix/blob/master/runtime/tutor) (accessible via `hx --tutor` or `:tutor`) for a vimtutor-like introduction. +For a full interactive introduction to Helix, refer to the +[tutor](https://github.com/helix-editor/helix/blob/master/runtime/tutor) which +can be accessed via the command `hx --tutor` or `:tutor`. + +> 💡 Currently, not all functionality is fully documented, please refer to the +> [key mappings](./keymap.md) list. ## Registers -Vim-like registers can be used to yank and store text to be pasted later. Usage is similar, with `"` being used to select a register: +In Helix, registers are storage locations for text and other data, such as the +result of a search. Registers can be used to cut, copy, and paste text, similar +to the clipboard in other text editors. Usage is similar to Vim, with `"` being +used to select a register. + +### User-defined registers + +Helix allows you to create your own named registers for storing text, for +example: - `"ay` - Yank the current selection to register `a`. - `"op` - Paste the text in register `o` after the selection. -If there is a selected register before invoking a change or delete command, the selection will be stored in the register and the action will be carried out: +If a register is selected before invoking a change or delete command, the selection will be stored in the register and the action will be carried out: - `"hc` - Store the selection in register `h` and then change it (delete and enter insert mode). - `"md` - Store the selection in register `m` and delete it. -### Special Registers +### Special registers | Register character | Contains | | --- | --- | @@ -25,41 +46,90 @@ If there is a selected register before invoking a change or delete command, the | `"` | Last yanked text | | `_` | Black hole | -> There is no special register for copying to system clipboard, instead special commands and keybindings are provided. See the [keymap](keymap.md#space-mode) for the specifics. -> The black hole register works as a no-op register, meaning no data will be written to / read from it. +The system clipboard is not directly supported by a special register. Instead, special commands and keybindings are provided. Refer to the +[key map](keymap.md#space-mode) for more details. + +The black hole register is a no-op register, meaning that no data will be read or written to it. ## Surround -Functionality similar to [vim-surround](https://github.com/tpope/vim-surround) is built into -helix. The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich): +Helix includes built-in functionality similar to [vim-surround](https://github.com/tpope/vim-surround). +The keymappings have been inspired from [vim-sandwich](https://github.com/machakann/vim-sandwich): -![surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif) +![Surround demo](https://user-images.githubusercontent.com/23398472/122865801-97073180-d344-11eb-8142-8f43809982c6.gif) -- `ms` - Add surround characters -- `mr` - Replace surround characters -- `md` - Delete surround characters +| Key Sequence | Action | +| --------------------------------- | --------------------------------------- | +| `ms` (after selecting text) | Add surround characters to selection | +| `mr` | Replace the closest surround characters | +| `md` | Delete the closest surround characters | -`ms` acts on a selection, so select the text first and use `ms`. `mr` and `md` work -on the closest pairs found and selections are not required; use counts to act in outer pairs. +You can use counts to act on outer pairs. -It can also act on multiple selections (yay!). For example, to change every occurrence of `(use)` to `[use]`: +Surround can also act on multiple selections. For example, to change every occurrence of `(use)` to `[use]`: -- `%` to select the whole file -- `s` to split the selections on a search term -- Input `use` and hit Enter -- `mr([` to replace the parens with square brackets +1. `%` to select the whole file +2. `s` to split the selections on a search term +3. Input `use` and hit Enter +4. `mr([` to replace the parentheses with square brackets -Multiple characters are currently not supported, but planned. +Multiple characters are currently not supported, but planned for future release. -## Syntax-tree Motions +## Selecting and manipulating text with textobjects -`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) move the primary -selection according to the selection's place in the syntax tree. Let's walk -through an example to get familiar with them. Many languages have a syntax like -so for function calls: +In Helix, textobjects are a way to select, manipulate and operate on a piece of +text in a structured way. They allow you to refer to blocks of text based on +their structure or purpose, such as a word, sentence, paragraph, or even a +function or block of code. -``` -func(arg1, arg2, arg3) +![Textobject demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif) +![Textobject tree-sitter demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif) + +- `ma` - Select around the object (`va` in Vim, `` in Kakoune) +- `mi` - Select inside the object (`vi` in Vim, `` in Kakoune) + +| Key after `mi` or `ma` | Textobject selected | +| --- | --- | +| `w` | Word | +| `W` | WORD | +| `p` | Paragraph | +| `(`, `[`, `'`, etc. | Specified surround pairs | +| `m` | The closest surround pair | +| `f` | Function | +| `c` | Class | +| `a` | Argument/parameter | +| `o` | Comment | +| `t` | Test | +| `g` | Change | + +> 💡 `f`, `c`, etc. need a tree-sitter grammar active for the current +document and a special tree-sitter query file to work properly. [Only +some grammars][lang-support] currently have the query file implemented. +Contributions are welcome! + +## Navigating using tree-sitter textobjects + +Navigating between functions, classes, parameters, and other elements is +possible using tree-sitter and textobject queries. For +example to move to the next function use `]f`, to move to previous +class use `[c`, and so on. + +![Tree-sitter-nav-demo][tree-sitter-nav-demo] + +For the full reference see the [unimpaired][unimpaired-keybinds] section of the key bind +documentation. + +> 💡 This feature relies on tree-sitter textobjects +> and requires the corresponding query file to work properly. + +## Moving the selection with syntax-aware motions + +`Alt-p`, `Alt-o`, `Alt-i`, and `Alt-n` (or `Alt` and arrow keys) allow you to move the +selection according to its location in the syntax tree. For example, many languages have the +following syntax for function calls: + +```js +func(arg1, arg2, arg3); ``` A function call might be parsed by tree-sitter into a tree like the following. @@ -93,77 +163,29 @@ a more intuitive tree format: └──────────┘ └──────────┘ └──────────┘ ``` -Say we have a selection that wraps `arg1`. The selection is on the `arg1` leaf -in the tree above. +If you have a selection that wraps `arg1` (see the tree above), and you use +`Alt-n`, it will select the next sibling in the syntax tree: `arg2`. -``` +```js +// before func([arg1], arg2, arg3) +// after +func(arg1, [arg2], arg3); ``` -Using `Alt-n` would select the next sibling in the syntax tree: `arg2`. +Similarly, `Alt-o` will expand the selection to the parent node, in this case, the +arguments node. -``` -func(arg1, [arg2], arg3) -``` - -While `Alt-o` would expand the selection to the parent node. In the tree above we -can see that we would select the `arguments` node. - -``` -func[(arg1, arg2, arg3)] +```js +func[(arg1, arg2, arg3)]; ``` There is also some nuanced behavior that prevents you from getting stuck on a -node with no sibling. If we have a selection on `arg1`, `Alt-p` would bring us -to the previous child node. Since `arg1` doesn't have a sibling to its left, -though, we climb the syntax tree and then take the previous selection. So -`Alt-p` will move the selection over to the "func" `identifier`. - -``` -[func](arg1, arg2, arg3) -``` - -## Textobjects - -![textobject-demo](https://user-images.githubusercontent.com/23398472/124231131-81a4bb00-db2d-11eb-9d10-8e577ca7b177.gif) -![textobject-treesitter-demo](https://user-images.githubusercontent.com/23398472/132537398-2a2e0a54-582b-44ab-a77f-eb818942203d.gif) - -- `ma` - Select around the object (`va` in Vim, `` in Kakoune) -- `mi` - Select inside the object (`vi` in Vim, `` in Kakoune) - -| Key after `mi` or `ma` | Textobject selected | -| --- | --- | -| `w` | Word | -| `W` | WORD | -| `p` | Paragraph | -| `(`, `[`, `'`, etc | Specified surround pairs | -| `m` | Closest surround pair | -| `f` | Function | -| `c` | Class | -| `a` | Argument/parameter | -| `o` | Comment | -| `t` | Test | -| `g` | Change | - -> NOTE: `f`, `c`, etc need a tree-sitter grammar active for the current -document and a special tree-sitter query file to work properly. [Only -some grammars][lang-support] currently have the query file implemented. -Contributions are welcome! - -## Tree-sitter Textobject Based Navigation - -Navigating between functions, classes, parameters, etc is made -possible by leveraging tree-sitter and textobjects queries. For -example to move to the next function use `]f`, to move to previous -class use `[c`, and so on. - -![tree-sitter-nav-demo][tree-sitter-nav-demo] - -See the [unimpaired][unimpaired-keybinds] section of the keybind -documentation for the full reference. - -> NOTE: This feature is dependent on tree-sitter based textobjects -and therefore requires the corresponding query file to work properly. +node with no sibling. When using `Alt-p` with a selection on `arg1`, the previous +child node will be selected. In the event that `arg1` does not have a previous +sibling, the selection will move up the syntax tree and select the previous +element. As a result, using `Alt-p` with a selection on `arg1` will move the +selection to the "func" `identifier`. [lang-support]: ./lang-support.md [unimpaired-keybinds]: ./keymap.md#unimpaired diff --git a/flake.lock b/flake.lock index 4cf1018c4999..fa292273af6e 100644 --- a/flake.lock +++ b/flake.lock @@ -16,22 +16,6 @@ "type": "github" } }, - "devshell": { - "flake": false, - "locked": { - "lastModified": 1667210711, - "narHash": "sha256-IoErjXZAkzYWHEpQqwu/DeRNJGFdR7X2OGbkhMqMrpw=", - "owner": "numtide", - "repo": "devshell", - "rev": "96a9dd12b8a447840cc246e17a47b81a4268bba7", - "type": "github" - }, - "original": { - "owner": "numtide", - "repo": "devshell", - "type": "github" - } - }, "dream2nix": { "inputs": { "alejandra": [ @@ -42,10 +26,12 @@ ], "crane": "crane", "devshell": [ + "nci" + ], + "flake-parts": [ "nci", - "devshell" + "parts" ], - "flake-parts": "flake-parts", "flake-utils-pre-commit": [ "nci" ], @@ -70,14 +56,17 @@ ], "pre-commit-hooks": [ "nci" + ], + "pruned-racket-catalog": [ + "nci" ] }, "locked": { - "lastModified": 1671323629, - "narHash": "sha256-9KHTPjIDjfnzZ4NjpE3gGIVHVHopy6weRDYO/7Y3hF8=", + "lastModified": 1677289985, + "narHash": "sha256-lUp06cTTlWubeBGMZqPl9jODM99LpWMcwxRiscFAUJg=", "owner": "nix-community", "repo": "dream2nix", - "rev": "2d7d68505c8619410df2c6b6463985f97cbcba6e", + "rev": "28b973a8d4c30cc1cbb3377ea2023a76bc3fb889", "type": "github" }, "original": { @@ -86,24 +75,6 @@ "type": "github" } }, - "flake-parts": { - "inputs": { - "nixpkgs-lib": "nixpkgs-lib" - }, - "locked": { - "lastModified": 1668450977, - "narHash": "sha256-cfLhMhnvXn6x1vPm+Jow3RiFAUSCw/l1utktCw5rVA4=", - "owner": "hercules-ci", - "repo": "flake-parts", - "rev": "d591857e9d7dd9ddbfba0ea02b43b927c3c0f1fa", - "type": "github" - }, - "original": { - "owner": "hercules-ci", - "repo": "flake-parts", - "type": "github" - } - }, "flake-utils": { "locked": { "lastModified": 1659877975, @@ -119,23 +90,40 @@ "type": "github" } }, + "mk-naked-shell": { + "flake": false, + "locked": { + "lastModified": 1676572903, + "narHash": "sha256-oQoDHHUTxNVSURfkFcYLuAK+btjs30T4rbEUtCUyKy8=", + "owner": "yusdacra", + "repo": "mk-naked-shell", + "rev": "aeca9f8aa592f5e8f71f407d081cb26fd30c5a57", + "type": "github" + }, + "original": { + "owner": "yusdacra", + "repo": "mk-naked-shell", + "type": "github" + } + }, "nci": { "inputs": { - "devshell": "devshell", "dream2nix": "dream2nix", + "mk-naked-shell": "mk-naked-shell", "nixpkgs": [ "nixpkgs" ], + "parts": "parts", "rust-overlay": [ "rust-overlay" ] }, "locked": { - "lastModified": 1671430291, - "narHash": "sha256-UIc7H8F3N8rK72J/Vj5YJdV72tvDvYjH+UPsOFvlcsE=", + "lastModified": 1677297103, + "narHash": "sha256-ArlJIbp9NGV9yvhZdV0SOUFfRlI/kHeKoCk30NbSiLc=", "owner": "yusdacra", "repo": "nix-cargo-integration", - "rev": "b1b0d38b8c3b0d0e6a38638d5bbe10b0bc67522c", + "rev": "a79272a2cb0942392bb3a5bf9a3ec6bc568795b2", "type": "github" }, "original": { @@ -146,11 +134,11 @@ }, "nixpkgs": { "locked": { - "lastModified": 1671359686, - "narHash": "sha256-3MpC6yZo+Xn9cPordGz2/ii6IJpP2n8LE8e/ebUXLrs=", + "lastModified": 1677063315, + "narHash": "sha256-qiB4ajTeAOVnVSAwCNEEkoybrAlA+cpeiBxLobHndE8=", "owner": "nixos", "repo": "nixpkgs", - "rev": "04f574a1c0fde90b51bf68198e2297ca4e7cccf4", + "rev": "988cc958c57ce4350ec248d2d53087777f9e1949", "type": "github" }, "original": { @@ -163,11 +151,11 @@ "nixpkgs-lib": { "locked": { "dir": "lib", - "lastModified": 1665349835, - "narHash": "sha256-UK4urM3iN80UXQ7EaOappDzcisYIuEURFRoGQ/yPkug=", + "lastModified": 1675183161, + "narHash": "sha256-Zq8sNgAxDckpn7tJo7V1afRSk2eoVbu3OjI1QklGLNg=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "34c5293a71ffdb2fe054eb5288adc1882c1eb0b1", + "rev": "e1e1b192c1a5aab2960bf0a0bd53a2e8124fa18e", "type": "github" }, "original": { @@ -178,10 +166,50 @@ "type": "github" } }, + "parts": { + "inputs": { + "nixpkgs-lib": [ + "nci", + "nixpkgs" + ] + }, + "locked": { + "lastModified": 1675933616, + "narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "47478a4a003e745402acf63be7f9a092d51b83d7", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, + "parts_2": { + "inputs": { + "nixpkgs-lib": "nixpkgs-lib" + }, + "locked": { + "lastModified": 1675933616, + "narHash": "sha256-/rczJkJHtx16IFxMmAWu5nNYcSXNg1YYXTHoGjLrLUA=", + "owner": "hercules-ci", + "repo": "flake-parts", + "rev": "47478a4a003e745402acf63be7f9a092d51b83d7", + "type": "github" + }, + "original": { + "owner": "hercules-ci", + "repo": "flake-parts", + "type": "github" + } + }, "root": { "inputs": { "nci": "nci", "nixpkgs": "nixpkgs", + "parts": "parts_2", "rust-overlay": "rust-overlay" } }, @@ -193,11 +221,11 @@ ] }, "locked": { - "lastModified": 1671416426, - "narHash": "sha256-kpSH1Jrxfk2qd0pRPJn1eQdIOseGv5JuE+YaOrqU9s4=", + "lastModified": 1677292251, + "narHash": "sha256-D+6q5Z2MQn3UFJtqsM5/AvVHi3NXKZTIMZt1JGq/spA=", "owner": "oxalica", "repo": "rust-overlay", - "rev": "fbaaff24f375ac25ec64268b0a0d63f91e474b7d", + "rev": "34cdbf6ad480ce13a6a526f57d8b9e609f3d65dc", "type": "github" }, "original": { diff --git a/flake.nix b/flake.nix index 734ac78e2eec..2ac764888daa 100644 --- a/flake.nix +++ b/flake.nix @@ -12,16 +12,10 @@ inputs.nixpkgs.follows = "nixpkgs"; inputs.rust-overlay.follows = "rust-overlay"; }; + parts.url = "github:hercules-ci/flake-parts"; }; - outputs = { - self, - nixpkgs, - nci, - ... - }: let - lib = nixpkgs.lib; - ncl = nci.lib.nci-lib; + outputs = inp: let mkRootPath = rel: builtins.path { path = "${toString ./.}/${rel}"; @@ -32,6 +26,12 @@ ".envrc" ".ignore" ".github" + ".gitignore" + "logo.svg" + "logo_dark.svg" + "logo_light.svg" + "rust-toolchain.toml" + "rustfmt.toml" "runtime" "screenshot.png" "book" @@ -46,6 +46,7 @@ "flake.lock" ]; ignorePaths = path: type: let + inherit (inp.nixpkgs) lib; # split the nix store path into its components components = lib.splitString "/" path; # drop off the `/nix/hash-source` section from the path @@ -61,123 +62,107 @@ # filter out unnecessary paths filter = ignorePaths; }; - outputs = nci.lib.makeOutputs { - root = ./.; - 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) - ++ (lib.optional stdenv.isDarwin darwin.apple_sdk.frameworks.CoreFoundation); - env = [ - { - name = "HELIX_RUNTIME"; - eval = "$PWD/runtime"; - } - { - name = "RUST_BACKTRACE"; - value = "1"; - } + in + inp.parts.lib.mkFlake {inputs = inp;} { + imports = [inp.nci.flakeModule]; + systems = [ + "x86_64-linux" + "x86_64-darwin" + "aarch64-linux" + "aarch64-darwin" + "i686-linux" + ]; + perSystem = { + config, + pkgs, + lib, + ... + }: let + 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 = + pkgs.runCommand + old.name { - name = "RUSTFLAGS"; - eval = - if common.pkgs.stdenv.isLinux - then "$RUSTFLAGS\" -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment\"" - else "$RUSTFLAGS"; + inherit (old) pname version; + meta = old.meta or {}; + passthru = + (old.passthru or {}) + // { + unwrapped = old; + }; + nativeBuildInputs = [pkgs.makeWrapper]; + makeWrapperArgs = config.makeWrapperArgs or []; } - ]; - }; - }; - pkgConfig = common: { - helix-term = let - # 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 + '' + cp -rs --no-preserve=mode,ownership ${old} $out + wrapProgram "$out/bin/hx" ''${makeWrapperArgs[@]} --set HELIX_RUNTIME "${runtimeDir}" + ''; + in + helix-wrapped + // { + override = makeOverridableHelix old; + passthru = + helix-wrapped.passthru // { - override = makeOverridableHelix old; - passthru = helix-wrapped.passthru // {wrapper = wrapper {};}; + wrapper = old: makeOverridableHelix old config; }; - in - makeOverridableHelix old {}; - in { - inherit wrapper; - 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 = ncl.addBuildInputs prev [common.config.cCompiler.package.cc.lib]; - - # link languages and theme toml files since helix-term expects them (for tests) - preConfigure = '' - ${prev.preConfigure or ""} - ${ - lib.concatMapStringsSep - "\n" - (path: "ln -sf ${mkRootPath path} ..") - ["languages.toml" "theme.toml" "base16_theme.toml"] - } - ''; - checkPhase = ":"; - - meta.mainProgram = "hx"; + }; + stdenv = + if pkgs.stdenv.isLinux + then pkgs.stdenv + else pkgs.clangStdenv; + rustFlagsEnv = + if stdenv.isLinux + then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment'' + else "$RUSTFLAGS"; + in { + # by default NCI adds rust-analyzer component, but helix toolchain doesn't have rust-analyzer + nci.toolchains.shell.components = ["rust-src" "rustfmt" "clippy"]; + nci.projects."helix-project".relPath = ""; + nci.crates."helix-term" = { + overrides = { + add-meta.override = _: {meta.mainProgram = "hx";}; + add-inputs.overrideAttrs = prev: { + buildInputs = (prev.buildInputs or []) ++ [stdenv.cc.cc.lib]; + }; + disable-grammar-builds = { + # disable fetching and building of tree-sitter grammars in the helix-term build.rs + HELIX_DISABLE_AUTO_GRAMMAR_BUILD = "1"; + }; + disable-tests = {checkPhase = ":";}; + set-stdenv.override = _: {inherit stdenv;}; + set-filtered-src.override = _: {src = filteredSource;}; }; }; + + packages.helix-unwrapped = config.nci.outputs."helix-term".packages.release; + packages.helix-unwrapped-dev = config.nci.outputs."helix-term".packages.dev; + packages.helix = makeOverridableHelix config.packages.helix-unwrapped {}; + packages.helix-dev = makeOverridableHelix config.packages.helix-unwrapped-dev {}; + packages.default = config.packages.helix; + + devShells.default = config.nci.outputs."helix-project".devShell.overrideAttrs (old: { + nativeBuildInputs = + (old.nativeBuildInputs or []) + ++ (with pkgs; [lld_13 cargo-flamegraph rust-analyzer]) + ++ (lib.optional (stdenv.isx86_64 && stdenv.isLinux) pkgs.cargo-tarpaulin) + ++ (lib.optional stdenv.isLinux pkgs.lldb) + ++ (lib.optional stdenv.isDarwin pkgs.darwin.apple_sdk.frameworks.CoreFoundation); + shellHook = '' + export HELIX_RUNTIME="$PWD/runtime" + export RUST_BACKTRACE="1" + export RUSTFLAGS="${rustFlagsEnv}" + ''; + }); }; }; - in - outputs - // { - packages = - lib.mapAttrs - ( - system: packages: - packages - // { - helix-unwrapped = packages.helix.passthru.unwrapped; - helix-unwrapped-dev = packages.helix-dev.passthru.unwrapped; - } - ) - outputs.packages; - }; nixConfig = { extra-substituters = ["https://helix.cachix.org"]; diff --git a/helix-core/src/selection.rs b/helix-core/src/selection.rs index 0ac2c680283a..0eb2b755ec4f 100644 --- a/helix-core/src/selection.rs +++ b/helix-core/src/selection.rs @@ -578,6 +578,16 @@ impl Selection { self.normalize() } + /// Takes a closure and maps each `Range` over the closure to multiple `Range`s. + pub fn transform_iter(mut self, f: F) -> Self + where + F: FnMut(Range) -> I, + I: Iterator, + { + self.ranges = self.ranges.into_iter().flat_map(f).collect(); + self.normalize() + } + // Ensures the selection adheres to the following invariants: // 1. All ranges are grapheme aligned. // 2. All ranges are at least 1 character wide, unless at the @@ -615,11 +625,6 @@ impl Selection { // returns true if self ⊇ other pub fn contains(&self, other: &Selection) -> bool { - // can't contain other if it is larger - if other.len() > self.len() { - return false; - } - let (mut iter_self, mut iter_other) = (self.iter(), other.iter()); let (mut ele_self, mut ele_other) = (iter_self.next(), iter_other.next()); @@ -1230,5 +1235,11 @@ mod test { vec!((3, 4), (7, 9)) )); assert!(!contains(vec!((1, 1), (5, 6)), vec!((1, 6)))); + + // multiple ranges of other are all contained in some ranges of self, + assert!(contains( + vec!((1, 4), (7, 10)), + vec!((1, 2), (3, 4), (7, 9)) + )); } } diff --git a/helix-core/src/surround.rs b/helix-core/src/surround.rs index 64d48c13a918..f430aee8a152 100644 --- a/helix-core/src/surround.rs +++ b/helix-core/src/surround.rs @@ -275,51 +275,138 @@ mod test { #[test] fn test_get_surround_pos() { - let doc = Rope::from("(some) (chars)\n(newline)"); - let slice = doc.slice(..); - let selection = Selection::new( - SmallVec::from_slice(&[Range::point(2), Range::point(9), Range::point(20)]), - 0, - ); + #[rustfmt::skip] + let (doc, selection, expectations) = + rope_with_selections_and_expectations( + "(some) (chars)\n(newline)", + "_ ^ _ _ ^ _\n_ ^ _" + ); - // cursor on s[o]me, c[h]ars, newl[i]ne assert_eq!( - get_surround_pos(slice, &selection, Some('('), 1) - .unwrap() - .as_slice(), - &[0, 5, 7, 13, 15, 23] + get_surround_pos(doc.slice(..), &selection, Some('('), 1).unwrap(), + expectations ); } #[test] - fn test_get_surround_pos_bail() { - let doc = Rope::from("[some]\n(chars)xx\n(newline)"); - let slice = doc.slice(..); + fn test_get_surround_pos_bail_different_surround_chars() { + #[rustfmt::skip] + let (doc, selection, _) = + rope_with_selections_and_expectations( + "[some]\n(chars)xx\n(newline)", + " ^ \n ^ \n " + ); - let selection = - Selection::new(SmallVec::from_slice(&[Range::point(2), Range::point(9)]), 0); - // cursor on s[o]me, c[h]ars assert_eq!( - get_surround_pos(slice, &selection, Some('('), 1), - Err(Error::PairNotFound) // different surround chars + get_surround_pos(doc.slice(..), &selection, Some('('), 1), + Err(Error::PairNotFound) ); + } + + #[test] + fn test_get_surround_pos_bail_overlapping_surround_chars() { + #[rustfmt::skip] + let (doc, selection, _) = + rope_with_selections_and_expectations( + "[some]\n(chars)xx\n(newline)", + " \n ^ \n ^ " + ); - let selection = Selection::new( - SmallVec::from_slice(&[Range::point(14), Range::point(24)]), - 0, - ); - // cursor on [x]x, newli[n]e assert_eq!( - get_surround_pos(slice, &selection, Some('('), 1), + get_surround_pos(doc.slice(..), &selection, Some('('), 1), Err(Error::PairNotFound) // overlapping surround chars ); + } + + #[test] + fn test_get_surround_pos_bail_cursor_overlap() { + #[rustfmt::skip] + let (doc, selection, _) = + rope_with_selections_and_expectations( + "[some]\n(chars)xx\n(newline)", + " ^^ \n \n " + ); - let selection = - Selection::new(SmallVec::from_slice(&[Range::point(2), Range::point(3)]), 0); - // cursor on s[o][m]e assert_eq!( - get_surround_pos(slice, &selection, Some('['), 1), + get_surround_pos(doc.slice(..), &selection, Some('['), 1), Err(Error::CursorOverlap) ); } + + #[test] + fn test_find_nth_pairs_pos_quote_success() { + #[rustfmt::skip] + let (doc, selection, expectations) = + rope_with_selections_and_expectations( + "some 'quoted text' on this 'line'\n'and this one'", + " _ ^ _ \n " + ); + + assert_eq!(2, expectations.len()); + assert_eq!( + find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 1) + .expect("find should succeed"), + (expectations[0], expectations[1]) + ) + } + + #[test] + fn test_find_nth_pairs_pos_nested_quote_success() { + #[rustfmt::skip] + let (doc, selection, expectations) = + rope_with_selections_and_expectations( + "some 'nested 'quoted' text' on this 'line'\n'and this one'", + " _ ^ _ \n " + ); + + assert_eq!(2, expectations.len()); + assert_eq!( + find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 2) + .expect("find should succeed"), + (expectations[0], expectations[1]) + ) + } + + #[test] + fn test_find_nth_pairs_pos_inside_quote_ambiguous() { + #[rustfmt::skip] + let (doc, selection, _) = + rope_with_selections_and_expectations( + "some 'nested 'quoted' text' on this 'line'\n'and this one'", + " ^ \n " + ); + + assert_eq!( + find_nth_pairs_pos(doc.slice(..), '\'', selection.primary(), 1), + Err(Error::CursorOnAmbiguousPair) + ) + } + + // Create a Rope and a matching Selection using a specification language. + // ^ is a single-point selection. + // _ is an expected index. These are returned as a Vec for use in assertions. + fn rope_with_selections_and_expectations( + text: &str, + spec: &str, + ) -> (Rope, Selection, Vec) { + if text.len() != spec.len() { + panic!("specification must match text length -- are newlines aligned?"); + } + + let rope = Rope::from(text); + + let selections: SmallVec<[Range; 1]> = spec + .match_indices('^') + .into_iter() + .map(|(i, _)| Range::point(i)) + .collect(); + + let expectations: Vec = spec + .match_indices('_') + .into_iter() + .map(|(i, _)| i) + .collect(); + + (rope, Selection::new(selections, 0), expectations) + } } diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index 81c2b8599e51..73d9db921efc 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -82,7 +82,8 @@ pub struct LanguageConfiguration { pub shebangs: Vec, // interpreter(s) associated with language pub roots: Vec, // these indicate project roots <.git, Cargo.toml> pub comment_token: Option, - pub max_line_length: Option, + pub text_width: Option, + pub soft_wrap: Option, #[serde(default, skip_serializing, deserialize_with = "deserialize_lsp_config")] pub config: Option, @@ -549,6 +550,33 @@ impl LanguageConfiguration { .ok() } } +#[derive(Debug, Clone, Default, PartialEq, Eq, Serialize, Deserialize)] +#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] +pub struct SoftWrap { + /// Soft wrap lines that exceed viewport width. Default to off + pub enable: Option, + /// Maximum space left free at the end of the line. + /// This space is used to wrap text at word boundaries. If that is not possible within this limit + /// the word is simply split at the end of the line. + /// + /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. + /// + /// Default to 20 + pub max_wrap: Option, + /// Maximum number of indentation that can be carried over from the previous line when softwrapping. + /// If a line is indented further then this limit it is rendered at the start of the viewport instead. + /// + /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. + /// + /// Default to 40 + pub max_indent_retain: Option, + /// Indicator placed at the beginning of softwrapped lines + /// + /// Defaults to ↪ + pub wrap_indicator: Option, + /// Softwrap at `text_width` instead of viewport width if it is shorter + pub wrap_at_text_width: Option, +} // Expose loader as Lazy<> global since it's always static? diff --git a/helix-core/src/wrap.rs b/helix-core/src/wrap.rs index eabc47d471dd..2ba8d173ee1d 100644 --- a/helix-core/src/wrap.rs +++ b/helix-core/src/wrap.rs @@ -2,6 +2,6 @@ use smartstring::{LazyCompact, SmartString}; /// Given a slice of text, return the text re-wrapped to fit it /// within the given width. -pub fn reflow_hard_wrap(text: &str, max_line_len: usize) -> SmartString { - textwrap::refill(text, max_line_len).into() +pub fn reflow_hard_wrap(text: &str, text_width: usize) -> SmartString { + textwrap::refill(text, text_width).into() } diff --git a/helix-dap/src/client.rs b/helix-dap/src/client.rs index f6d8a0696713..ff727d00abe2 100644 --- a/helix-dap/src/client.rs +++ b/helix-dap/src/client.rs @@ -33,6 +33,7 @@ pub struct Client { server_tx: UnboundedSender, request_counter: AtomicU64, connection_type: Option, + starting_request_args: Option, pub caps: Option, // thread_id -> frames pub stack_frames: HashMap>, @@ -87,6 +88,7 @@ impl Client { request_counter: AtomicU64::new(0), caps: None, connection_type: None, + starting_request_args: None, stack_frames: HashMap::new(), thread_states: HashMap::new(), thread_id: None, @@ -158,6 +160,10 @@ impl Client { ) } + pub fn starting_request_args(&self) -> &Option { + &self.starting_request_args + } + pub async fn tcp_process( cmd: &str, args: Vec<&str>, @@ -356,14 +362,25 @@ impl Client { pub fn launch(&mut self, args: serde_json::Value) -> impl Future> { self.connection_type = Some(ConnectionType::Launch); + self.starting_request_args = Some(args.clone()); self.call::(args) } pub fn attach(&mut self, args: serde_json::Value) -> impl Future> { self.connection_type = Some(ConnectionType::Attach); + self.starting_request_args = Some(args.clone()); self.call::(args) } + pub fn restart(&self) -> impl Future> { + let args = if let Some(args) = &self.starting_request_args { + args.clone() + } else { + Value::Null + }; + self.call::(args) + } + pub async fn set_breakpoints( &self, file: PathBuf, diff --git a/helix-dap/src/types.rs b/helix-dap/src/types.rs index c598790b225c..bbaf53a602c1 100644 --- a/helix-dap/src/types.rs +++ b/helix-dap/src/types.rs @@ -378,7 +378,7 @@ pub mod requests { impl Request for Launch { type Arguments = Value; - type Result = Value; + type Result = (); const COMMAND: &'static str = "launch"; } @@ -387,7 +387,7 @@ pub mod requests { impl Request for Attach { type Arguments = Value; - type Result = Value; + type Result = (); const COMMAND: &'static str = "attach"; } @@ -402,6 +402,15 @@ pub mod requests { pub suspend_debuggee: Option, } + #[derive(Debug)] + pub enum Restart {} + + impl Request for Restart { + type Arguments = Value; + type Result = (); + const COMMAND: &'static str = "restart"; + } + #[derive(Debug)] pub enum Disconnect {} diff --git a/helix-loader/src/grammar.rs b/helix-loader/src/grammar.rs index 01c966c8c4bd..a85cb274cfb8 100644 --- a/helix-loader/src/grammar.rs +++ b/helix-loader/src/grammar.rs @@ -67,8 +67,9 @@ 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); - library_path.set_extension(DYLIB_EXTENSION); + let mut rel_library_path = PathBuf::new().join("grammars").join(name); + rel_library_path.set_extension(DYLIB_EXTENSION); + let library_path = crate::runtime_file(&rel_library_path); let library = unsafe { Library::new(&library_path) } .with_context(|| format!("Error opening dynamic library {:?}", library_path))?; @@ -252,7 +253,9 @@ fn fetch_grammar(grammar: GrammarConfiguration) -> Result { remote, revision, .. } = grammar.source { - let grammar_dir = crate::runtime_dir() + let grammar_dir = crate::runtime_dirs() + .first() + .expect("No runtime directories provided") // guaranteed by post-condition .join("grammars") .join("sources") .join(&grammar.grammar_id); @@ -350,7 +353,9 @@ fn build_grammar(grammar: GrammarConfiguration, target: Option<&str>) -> Result< let grammar_dir = if let GrammarSource::Local { path } = &grammar.source { PathBuf::from(&path) } else { - crate::runtime_dir() + crate::runtime_dirs() + .first() + .expect("No runtime directories provided") // guaranteed by post-condition .join("grammars") .join("sources") .join(&grammar.grammar_id) @@ -401,7 +406,10 @@ fn build_tree_sitter_library( None } }; - let parser_lib_path = crate::runtime_dir().join("grammars"); + let parser_lib_path = crate::runtime_dirs() + .first() + .expect("No runtime directories provided") // guaranteed by post-condition + .join("grammars"); let mut library_path = parser_lib_path.join(&grammar.grammar_id); library_path.set_extension(DYLIB_EXTENSION); @@ -511,9 +519,6 @@ fn mtime(path: &Path) -> Result { /// Gives the contents of a file from a language's `runtime/queries/` /// directory pub fn load_runtime_file(language: &str, filename: &str) -> Result { - let path = crate::RUNTIME_DIR - .join("queries") - .join(language) - .join(filename); + let path = crate::runtime_file(&PathBuf::new().join("queries").join(language).join(filename)); std::fs::read_to_string(path) } diff --git a/helix-loader/src/lib.rs b/helix-loader/src/lib.rs index 8dc2928adc9f..04b44b5aa4ee 100644 --- a/helix-loader/src/lib.rs +++ b/helix-loader/src/lib.rs @@ -2,11 +2,12 @@ pub mod config; pub mod grammar; use etcetera::base_strategy::{choose_base_strategy, BaseStrategy}; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; pub const VERSION_AND_GIT_HASH: &str = env!("VERSION_AND_GIT_HASH"); -pub static RUNTIME_DIR: once_cell::sync::Lazy = once_cell::sync::Lazy::new(runtime_dir); +static RUNTIME_DIRS: once_cell::sync::Lazy> = + once_cell::sync::Lazy::new(prioritize_runtime_dirs); static CONFIG_FILE: once_cell::sync::OnceCell = once_cell::sync::OnceCell::new(); @@ -25,31 +26,83 @@ pub fn initialize_config_file(specified_file: Option) { CONFIG_FILE.set(config_file).ok(); } -pub fn runtime_dir() -> PathBuf { - if let Ok(dir) = std::env::var("HELIX_RUNTIME") { - return dir.into(); - } - +/// A list of runtime directories from highest to lowest priority +/// +/// The priority is: +/// +/// 1. sibling directory to `CARGO_MANIFEST_DIR` (if environment variable is set) +/// 2. subdirectory of user config directory (always included) +/// 3. `HELIX_RUNTIME` (if environment variable is set) +/// 4. subdirectory of path to helix executable (always included) +/// +/// Postcondition: returns at least two paths (they might not exist). +fn prioritize_runtime_dirs() -> Vec { + const RT_DIR: &str = "runtime"; + // Adding higher priority first + let mut rt_dirs = Vec::new(); if let Ok(dir) = std::env::var("CARGO_MANIFEST_DIR") { // this is the directory of the crate being run by cargo, we need the workspace path so we take the parent let path = std::path::PathBuf::from(dir).parent().unwrap().join(RT_DIR); log::debug!("runtime dir: {}", path.to_string_lossy()); - return path; + rt_dirs.push(path); } - const RT_DIR: &str = "runtime"; - let conf_dir = config_dir().join(RT_DIR); - if conf_dir.exists() { - return conf_dir; + let conf_rt_dir = config_dir().join(RT_DIR); + rt_dirs.push(conf_rt_dir); + + if let Ok(dir) = std::env::var("HELIX_RUNTIME") { + rt_dirs.push(dir.into()); } // fallback to location of the executable being run // canonicalize the path in case the executable is symlinked - std::env::current_exe() + let exe_rt_dir = std::env::current_exe() .ok() .and_then(|path| std::fs::canonicalize(path).ok()) .and_then(|path| path.parent().map(|path| path.to_path_buf().join(RT_DIR))) - .unwrap() + .unwrap(); + rt_dirs.push(exe_rt_dir); + rt_dirs +} + +/// Runtime directories ordered from highest to lowest priority +/// +/// All directories should be checked when looking for files. +/// +/// Postcondition: returns at least one path (it might not exist). +pub fn runtime_dirs() -> &'static [PathBuf] { + &RUNTIME_DIRS +} + +/// Find file with path relative to runtime directory +/// +/// `rel_path` should be the relative path from within the `runtime/` directory. +/// The valid runtime directories are searched in priority order and the first +/// file found to exist is returned, otherwise None. +fn find_runtime_file(rel_path: &Path) -> Option { + RUNTIME_DIRS.iter().find_map(|rt_dir| { + let path = rt_dir.join(rel_path); + if path.exists() { + Some(path) + } else { + None + } + }) +} + +/// Find file with path relative to runtime directory +/// +/// `rel_path` should be the relative path from within the `runtime/` directory. +/// The valid runtime directories are searched in priority order and the first +/// file found to exist is returned, otherwise the path to the final attempt +/// that failed. +pub fn runtime_file(rel_path: &Path) -> PathBuf { + find_runtime_file(rel_path).unwrap_or_else(|| { + RUNTIME_DIRS + .last() + .map(|dir| dir.join(rel_path)) + .unwrap_or_default() + }) } pub fn config_dir() -> PathBuf { diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 133ead298754..9d76822dc96b 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -14,6 +14,7 @@ homepage = "https://helix-editor.com" [dependencies] helix-core = { version = "0.6", path = "../helix-core" } helix-loader = { version = "0.6", path = "../helix-loader" } +helix-parsec = { version = "0.6", path = "../helix-parsec" } anyhow = "1.0" futures-executor = "0.3" @@ -23,6 +24,6 @@ lsp-types = { version = "0.94" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.25", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.26", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.12" which = "4.4" diff --git a/helix-lsp/src/client.rs b/helix-lsp/src/client.rs index 3f88b3523f80..9fa118fbda22 100644 --- a/helix-lsp/src/client.rs +++ b/helix-lsp/src/client.rs @@ -320,7 +320,7 @@ impl Client { text_document: Some(lsp::TextDocumentClientCapabilities { completion: Some(lsp::CompletionClientCapabilities { completion_item: Some(lsp::CompletionItemCapability { - snippet_support: Some(false), + snippet_support: Some(true), resolve_support: Some(lsp::CompletionItemCapabilityResolveSupport { properties: vec![ String::from("documentation"), @@ -359,7 +359,7 @@ impl Client { }), rename: Some(lsp::RenameClientCapabilities { dynamic_registration: Some(false), - prepare_support: Some(false), + prepare_support: Some(true), prepare_support_default_behavior: None, honors_change_annotations: Some(false), }), @@ -1034,6 +1034,29 @@ impl Client { Some(self.call::(params)) } + pub fn prepare_rename( + &self, + text_document: lsp::TextDocumentIdentifier, + position: lsp::Position, + ) -> Option>> { + let capabilities = self.capabilities.get().unwrap(); + + match capabilities.rename_provider { + Some(lsp::OneOf::Right(lsp::RenameOptions { + prepare_provider: Some(true), + .. + })) => (), + _ => return None, + } + + let params = lsp::TextDocumentPositionParams { + text_document, + position, + }; + + Some(self.call::(params)) + } + // empty string to get all symbols pub fn workspace_symbols(&self, query: String) -> Option>> { let capabilities = self.capabilities.get().unwrap(); diff --git a/helix-lsp/src/lib.rs b/helix-lsp/src/lib.rs index 341d4a547b35..147b381c2a4e 100644 --- a/helix-lsp/src/lib.rs +++ b/helix-lsp/src/lib.rs @@ -1,5 +1,6 @@ mod client; pub mod jsonrpc; +pub mod snippet; mod transport; pub use client::Client; @@ -59,6 +60,7 @@ pub mod util { use super::*; use helix_core::line_ending::{line_end_byte_index, line_end_char_index}; use helix_core::{diagnostic::NumberOrString, Range, Rope, Selection, Tendril, Transaction}; + use helix_core::{smallvec, SmallVec}; /// Converts a diagnostic in the document to [`lsp::Diagnostic`]. /// @@ -250,26 +252,17 @@ pub mod util { pub fn generate_transaction_from_completion_edit( doc: &Rope, selection: &Selection, - edit: lsp::TextEdit, - offset_encoding: OffsetEncoding, + start_offset: i128, + end_offset: i128, + new_text: String, ) -> Transaction { - let replacement: Option = if edit.new_text.is_empty() { + let replacement: Option = if new_text.is_empty() { None } else { - Some(edit.new_text.into()) + Some(new_text.into()) }; let text = doc.slice(..); - let primary_cursor = selection.primary().cursor(text); - - let start_offset = match lsp_pos_to_pos(doc, edit.range.start, offset_encoding) { - Some(start) => start as i128 - primary_cursor as i128, - None => return Transaction::new(doc), - }; - let end_offset = match lsp_pos_to_pos(doc, edit.range.end, offset_encoding) { - Some(end) => end as i128 - primary_cursor as i128, - None => return Transaction::new(doc), - }; Transaction::change_by_selection(doc, selection, |range| { let cursor = range.cursor(text); @@ -281,6 +274,74 @@ pub mod util { }) } + /// Creates a [Transaction] from the [snippet::Snippet] in a completion response. + /// The transaction applies the edit to all cursors. + pub fn generate_transaction_from_snippet( + doc: &Rope, + selection: &Selection, + start_offset: i128, + end_offset: i128, + snippet: snippet::Snippet, + line_ending: &str, + include_placeholder: bool, + ) -> Transaction { + let text = doc.slice(..); + + // For each cursor store offsets for the first tabstop + let mut cursor_tabstop_offsets = Vec::>::new(); + let transaction = Transaction::change_by_selection(doc, selection, |range| { + let cursor = range.cursor(text); + let replacement_start = (cursor as i128 + start_offset) as usize; + let replacement_end = (cursor as i128 + end_offset) as usize; + let newline_with_offset = format!( + "{line_ending}{blank:width$}", + line_ending = line_ending, + width = replacement_start - doc.line_to_char(doc.char_to_line(replacement_start)), + blank = "" + ); + + let (replacement, tabstops) = + snippet::render(&snippet, newline_with_offset, include_placeholder); + + let replacement_len = replacement.chars().count(); + cursor_tabstop_offsets.push( + tabstops + .first() + .unwrap_or(&smallvec![(replacement_len, replacement_len)]) + .iter() + .map(|(from, to)| -> (i128, i128) { + ( + *from as i128 - replacement_len as i128, + *to as i128 - replacement_len as i128, + ) + }) + .collect(), + ); + + (replacement_start, replacement_end, Some(replacement.into())) + }); + + // Create new selection based on the cursor tabstop from above + let mut cursor_tabstop_offsets_iter = cursor_tabstop_offsets.iter(); + let selection = selection + .clone() + .map(transaction.changes()) + .transform_iter(|range| { + cursor_tabstop_offsets_iter + .next() + .unwrap() + .iter() + .map(move |(from, to)| { + Range::new( + (range.anchor as i128 + *from) as usize, + (range.anchor as i128 + *to) as usize, + ) + }) + }); + + transaction.with_selection(selection) + } + pub fn generate_transaction_from_edits( doc: &Rope, mut edits: Vec, @@ -476,6 +537,16 @@ impl Registry { } } + pub fn stop(&mut self, language_config: &LanguageConfiguration) { + let scope = language_config.scope.clone(); + + if let Some((_, client)) = self.inner.remove(&scope) { + tokio::spawn(async move { + let _ = client.force_shutdown().await; + }); + } + } + pub fn get( &mut self, language_config: &LanguageConfiguration, diff --git a/helix-lsp/src/snippet.rs b/helix-lsp/src/snippet.rs new file mode 100644 index 000000000000..b27077e7068c --- /dev/null +++ b/helix-lsp/src/snippet.rs @@ -0,0 +1,525 @@ +use std::borrow::Cow; + +use anyhow::{anyhow, Result}; +use helix_core::{smallvec, SmallVec}; + +#[derive(Debug, PartialEq, Eq)] +pub enum CaseChange { + Upcase, + Downcase, + Capitalize, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum FormatItem<'a> { + Text(&'a str), + Capture(usize), + CaseChange(usize, CaseChange), + Conditional(usize, Option<&'a str>, Option<&'a str>), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Regex<'a> { + value: &'a str, + replacement: Vec>, + options: Option<&'a str>, +} + +#[derive(Debug, PartialEq, Eq)] +pub enum SnippetElement<'a> { + Tabstop { + tabstop: usize, + }, + Placeholder { + tabstop: usize, + value: Vec>, + }, + Choice { + tabstop: usize, + choices: Vec<&'a str>, + }, + Variable { + name: &'a str, + default: Option<&'a str>, + regex: Option>, + }, + Text(&'a str), +} + +#[derive(Debug, PartialEq, Eq)] +pub struct Snippet<'a> { + elements: Vec>, +} + +pub fn parse(s: &str) -> Result> { + parser::parse(s).map_err(|rest| anyhow!("Failed to parse snippet. Remaining input: {}", rest)) +} + +fn render_elements( + snippet_elements: &[SnippetElement<'_>], + insert: &mut String, + offset: &mut usize, + tabstops: &mut Vec<(usize, (usize, usize))>, + newline_with_offset: &String, + include_placeholer: bool, +) { + use SnippetElement::*; + + for element in snippet_elements { + match element { + &Text(text) => { + // small optimization to avoid calling replace when it's unnecessary + let text = if text.contains('\n') { + Cow::Owned(text.replace('\n', newline_with_offset)) + } else { + Cow::Borrowed(text) + }; + *offset += text.chars().count(); + insert.push_str(&text); + } + &Variable { + name: _, + regex: _, + r#default, + } => { + // TODO: variables. For now, fall back to the default, which defaults to "". + let text = r#default.unwrap_or_default(); + *offset += text.chars().count(); + insert.push_str(text); + } + &Tabstop { tabstop } => { + tabstops.push((tabstop, (*offset, *offset))); + } + Placeholder { + tabstop, + value: inner_snippet_elements, + } => { + let start_offset = *offset; + if include_placeholer { + render_elements( + inner_snippet_elements, + insert, + offset, + tabstops, + newline_with_offset, + include_placeholer, + ); + } + tabstops.push((*tabstop, (start_offset, *offset))); + } + &Choice { + tabstop, + choices: _, + } => { + // TODO: choices + tabstops.push((tabstop, (*offset, *offset))); + } + } + } +} + +#[allow(clippy::type_complexity)] // only used one time +pub fn render( + snippet: &Snippet<'_>, + newline_with_offset: String, + include_placeholer: bool, +) -> (String, Vec>) { + let mut insert = String::new(); + let mut tabstops = Vec::new(); + let mut offset = 0; + + render_elements( + &snippet.elements, + &mut insert, + &mut offset, + &mut tabstops, + &newline_with_offset, + include_placeholer, + ); + + // sort in ascending order (except for 0, which should always be the last one (per lsp doc)) + tabstops.sort_unstable_by_key(|(n, _)| if *n == 0 { usize::MAX } else { *n }); + + // merge tabstops with the same index (we take advantage of the fact that we just sorted them + // above to simply look backwards) + let mut ntabstops = Vec::>::new(); + { + let mut prev = None; + for (tabstop, r) in tabstops { + if prev == Some(tabstop) { + let len_1 = ntabstops.len() - 1; + ntabstops[len_1].push(r); + } else { + prev = Some(tabstop); + ntabstops.push(smallvec![r]); + } + } + } + + (insert, ntabstops) +} + +mod parser { + use helix_parsec::*; + + use super::{CaseChange, FormatItem, Regex, Snippet, SnippetElement}; + + /* + https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#snippet_syntax + + any ::= tabstop | placeholder | choice | variable | text + tabstop ::= '$' int | '${' int '}' + placeholder ::= '${' int ':' any '}' + choice ::= '${' int '|' text (',' text)* '|}' + variable ::= '$' var | '${' var }' + | '${' var ':' any '}' + | '${' var '/' regex '/' (format | text)+ '/' options '}' + format ::= '$' int | '${' int '}' + | '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' + | '${' int ':+' if '}' + | '${' int ':?' if ':' else '}' + | '${' int ':-' else '}' | '${' int ':' else '}' + regex ::= Regular Expression value (ctor-string) + options ::= Regular Expression option (ctor-options) + var ::= [_a-zA-Z] [_a-zA-Z0-9]* + int ::= [0-9]+ + text ::= .* + if ::= text + else ::= text + */ + + fn var<'a>() -> impl Parser<'a, Output = &'a str> { + // var = [_a-zA-Z][_a-zA-Z0-9]* + move |input: &'a str| match input + .char_indices() + .take_while(|(p, c)| { + *c == '_' + || if *p == 0 { + c.is_ascii_alphabetic() + } else { + c.is_ascii_alphanumeric() + } + }) + .last() + { + Some((index, c)) if index >= 1 => { + let index = index + c.len_utf8(); + Ok((&input[index..], &input[0..index])) + } + _ => Err(input), + } + } + + fn text<'a, const SIZE: usize>(cs: [char; SIZE]) -> impl Parser<'a, Output = &'a str> { + take_while(move |c| cs.into_iter().all(|c1| c != c1)) + } + + fn digit<'a>() -> impl Parser<'a, Output = usize> { + filter_map(take_while(|c| c.is_ascii_digit()), |s| s.parse().ok()) + } + + fn case_change<'a>() -> impl Parser<'a, Output = CaseChange> { + use CaseChange::*; + + choice!( + map("upcase", |_| Upcase), + map("downcase", |_| Downcase), + map("capitalize", |_| Capitalize), + ) + } + + fn format<'a>() -> impl Parser<'a, Output = FormatItem<'a>> { + use FormatItem::*; + + choice!( + // '$' int + map(right("$", digit()), Capture), + // '${' int '}' + map(seq!("${", digit(), "}"), |seq| Capture(seq.1)), + // '${' int ':' '/upcase' | '/downcase' | '/capitalize' '}' + map(seq!("${", digit(), ":/", case_change(), "}"), |seq| { + CaseChange(seq.1, seq.3) + }), + // '${' int ':+' if '}' + map( + seq!("${", digit(), ":+", take_until(|c| c == '}'), "}"), + |seq| { Conditional(seq.1, Some(seq.3), None) } + ), + // '${' int ':?' if ':' else '}' + map( + seq!( + "${", + digit(), + ":?", + take_until(|c| c == ':'), + ":", + take_until(|c| c == '}'), + "}" + ), + |seq| { Conditional(seq.1, Some(seq.3), Some(seq.5)) } + ), + // '${' int ':-' else '}' | '${' int ':' else '}' + map( + seq!( + "${", + digit(), + ":", + optional("-"), + take_until(|c| c == '}'), + "}" + ), + |seq| { Conditional(seq.1, None, Some(seq.4)) } + ), + ) + } + + fn regex<'a>() -> impl Parser<'a, Output = Regex<'a>> { + let text = map(text(['$', '/']), FormatItem::Text); + let replacement = reparse_as( + take_until(|c| c == '/'), + one_or_more(choice!(format(), text)), + ); + + map( + seq!( + "/", + take_until(|c| c == '/'), + "/", + replacement, + "/", + optional(take_until(|c| c == '}')), + ), + |(_, value, _, replacement, _, options)| Regex { + value, + replacement, + options, + }, + ) + } + + fn tabstop<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + map( + or( + right("$", digit()), + map(seq!("${", digit(), "}"), |values| values.1), + ), + |digit| SnippetElement::Tabstop { tabstop: digit }, + ) + } + + fn placeholder<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + let text = map(text(['$', '}']), SnippetElement::Text); + map( + seq!( + "${", + digit(), + ":", + one_or_more(choice!(anything(), text)), + "}" + ), + |seq| SnippetElement::Placeholder { + tabstop: seq.1, + value: seq.3, + }, + ) + } + + fn choice<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + map( + seq!( + "${", + digit(), + "|", + sep(take_until(|c| c == ',' || c == '|'), ","), + "|}", + ), + |seq| SnippetElement::Choice { + tabstop: seq.1, + choices: seq.3, + }, + ) + } + + fn variable<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + choice!( + // $var + map(right("$", var()), |name| SnippetElement::Variable { + name, + default: None, + regex: None, + }), + // ${var:default} + map( + seq!("${", var(), ":", take_until(|c| c == '}'), "}",), + |values| SnippetElement::Variable { + name: values.1, + default: Some(values.3), + regex: None, + } + ), + // ${var/value/format/options} + map(seq!("${", var(), regex(), "}"), |values| { + SnippetElement::Variable { + name: values.1, + default: None, + regex: Some(values.2), + } + }), + ) + } + + fn anything<'a>() -> impl Parser<'a, Output = SnippetElement<'a>> { + // The parser has to be constructed lazily to avoid infinite opaque type recursion + |input: &'a str| { + let parser = choice!(tabstop(), placeholder(), choice(), variable()); + parser.parse(input) + } + } + + fn snippet<'a>() -> impl Parser<'a, Output = Snippet<'a>> { + let text = map(text(['$']), SnippetElement::Text); + map(one_or_more(choice!(anything(), text)), |parts| Snippet { + elements: parts, + }) + } + + pub fn parse(s: &str) -> Result { + snippet().parse(s).map(|(_input, elements)| elements) + } + + #[cfg(test)] + mod test { + use super::SnippetElement::*; + use super::*; + + #[test] + fn empty_string_is_error() { + assert_eq!(Err(""), parse("")); + } + + #[test] + fn parse_placeholders_in_function_call() { + assert_eq!( + Ok(Snippet { + elements: vec![ + Text("match("), + Placeholder { + tabstop: 1, + value: vec!(Text("Arg1")), + }, + Text(")") + ] + }), + parse("match(${1:Arg1})") + ) + } + + #[test] + fn parse_placeholders_in_statement() { + assert_eq!( + Ok(Snippet { + elements: vec![ + Text("local "), + Placeholder { + tabstop: 1, + value: vec!(Text("var")), + }, + Text(" = "), + Placeholder { + tabstop: 1, + value: vec!(Text("value")), + }, + ] + }), + parse("local ${1:var} = ${1:value}") + ) + } + + #[test] + fn parse_tabstop_nested_in_placeholder() { + assert_eq!( + Ok(Snippet { + elements: vec![Placeholder { + tabstop: 1, + value: vec!(Text("var, "), Tabstop { tabstop: 2 },), + },] + }), + parse("${1:var, $2}") + ) + } + + #[test] + fn parse_placeholder_nested_in_placeholder() { + assert_eq!( + Ok(Snippet { + elements: vec![Placeholder { + tabstop: 1, + value: vec!( + Text("foo "), + Placeholder { + tabstop: 2, + value: vec!(Text("bar")), + }, + ), + },] + }), + parse("${1:foo ${2:bar}}") + ) + } + + #[test] + fn parse_all() { + assert_eq!( + Ok(Snippet { + elements: vec![ + Text("hello "), + Tabstop { tabstop: 1 }, + Tabstop { tabstop: 2 }, + Text(" "), + Choice { + tabstop: 1, + choices: vec!["one", "two", "three"] + }, + Text(" "), + Variable { + name: "name", + default: Some("foo"), + regex: None + }, + Text(" "), + Variable { + name: "var", + default: None, + regex: None + }, + Text(" "), + Variable { + name: "TM", + default: None, + regex: None + }, + ] + }), + parse("hello $1${2} ${1|one,two,three|} ${name:foo} $var $TM") + ); + } + + #[test] + fn regex_capture_replace() { + assert_eq!( + Ok(Snippet { + elements: vec![Variable { + name: "TM_FILENAME", + default: None, + regex: Some(Regex { + value: "(.*).+$", + replacement: vec![FormatItem::Capture(1)], + options: None, + }), + }] + }), + parse("${TM_FILENAME/(.*).+$/$1/}") + ); + } + } +} diff --git a/helix-parsec/Cargo.toml b/helix-parsec/Cargo.toml new file mode 100644 index 000000000000..505a4247e3ef --- /dev/null +++ b/helix-parsec/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "helix-parsec" +version = "0.6.0" +authors = ["Blaž Hrastnik "] +edition = "2021" +license = "MPL-2.0" +description = "Parser combinators for Helix" +categories = ["editor"] +repository = "https://github.com/helix-editor/helix" +homepage = "https://helix-editor.com" +include = ["src/**/*", "README.md"] + +[dependencies] diff --git a/helix-parsec/src/lib.rs b/helix-parsec/src/lib.rs new file mode 100644 index 000000000000..e09814b81f5b --- /dev/null +++ b/helix-parsec/src/lib.rs @@ -0,0 +1,561 @@ +//! Parser-combinator functions +//! +//! This module provides parsers and parser combinators which can be used +//! together to build parsers by functional composition. + +// This module implements parser combinators following https://bodil.lol/parser-combinators/. +// `sym` (trait implementation for `&'static str`), `map`, `pred` (filter), `one_or_more`, +// `zero_or_more`, as well as the `Parser` trait originate mostly from that post. +// The remaining parsers and parser combinators are either based on +// https://github.com/archseer/snippets.nvim/blob/a583da6ef130d2a4888510afd8c4e5ffd62d0dce/lua/snippet/parser.lua#L5-L138 +// or are novel. + +// When a parser matches the input successfully, it returns `Ok((next_input, some_value))` +// where the type of the returned value depends on the parser. If the parser fails to match, +// it returns `Err(input)`. +type ParseResult<'a, Output> = Result<(&'a str, Output), &'a str>; + +/// A parser or parser-combinator. +/// +/// Parser-combinators compose multiple parsers together to parse input. +/// For example, two basic parsers (`&'static str`s) may be combined with +/// a parser-combinator like [or] to produce a new parser. +/// +/// ``` +/// use helix_parsec::{or, Parser}; +/// let foo = "foo"; // matches "foo" literally +/// let bar = "bar"; // matches "bar" literally +/// let foo_or_bar = or(foo, bar); // matches either "foo" or "bar" +/// assert_eq!(Ok(("", "foo")), foo_or_bar.parse("foo")); +/// assert_eq!(Ok(("", "bar")), foo_or_bar.parse("bar")); +/// assert_eq!(Err("baz"), foo_or_bar.parse("baz")); +/// ``` +pub trait Parser<'a> { + type Output; + + fn parse(&self, input: &'a str) -> ParseResult<'a, Self::Output>; +} + +// Most parser-combinators are written as higher-order functions which take some +// parser(s) as input and return a new parser: a function that takes input and returns +// a parse result. The underlying implementation of [Parser::parse] for these functions +// is simply application. +#[doc(hidden)] +impl<'a, F, T> Parser<'a> for F +where + F: Fn(&'a str) -> ParseResult, +{ + type Output = T; + + fn parse(&self, input: &'a str) -> ParseResult<'a, Self::Output> { + self(input) + } +} + +/// A parser which matches the string literal exactly. +/// +/// This parser succeeds if the next characters in the input are equal to the given +/// string literal. +/// +/// Note that [str::parse] interferes with calling [Parser::parse] on string literals +/// directly; this trait implementation works when used within any parser combinator +/// but does not work on its own. To call [Parser::parse] on a parser for a string +/// literal, use the [token] parser. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{or, Parser}; +/// let parser = or("foo", "bar"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foo")); +/// assert_eq!(Ok(("", "bar")), parser.parse("bar")); +/// assert_eq!(Err("baz"), parser.parse("baz")); +/// ``` +impl<'a> Parser<'a> for &'static str { + type Output = &'a str; + + fn parse(&self, input: &'a str) -> ParseResult<'a, Self::Output> { + match input.get(0..self.len()) { + Some(actual) if actual == *self => Ok((&input[self.len()..], &input[0..self.len()])), + _ => Err(input), + } + } +} + +// Parsers + +/// A parser which matches the given string literally. +/// +/// This function is a convenience for interpreting string literals as parsers +/// and is only necessary to avoid conflict with [str::parse]. See the documentation +/// for the `&'static str` implementation of [Parser]. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{token, Parser}; +/// let parser = token("foo"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foo")); +/// assert_eq!(Err("bar"), parser.parse("bar")); +/// ``` +pub fn token<'a>(literal: &'static str) -> impl Parser<'a, Output = &'a str> { + literal +} + +/// A parser which matches all values until the specified pattern is found. +/// +/// If the pattern is not found, this parser does not match. The input up to the +/// character which returns `true` is returned but not that character itself. +/// +/// If the pattern function returns true on the first input character, this +/// parser fails. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{take_until, Parser}; +/// let parser = take_until(|c| c == '.'); +/// assert_eq!(Ok((".bar", "foo")), parser.parse("foo.bar")); +/// assert_eq!(Err(".foo"), parser.parse(".foo")); +/// assert_eq!(Err("foo"), parser.parse("foo")); +/// ``` +pub fn take_until<'a, F>(pattern: F) -> impl Parser<'a, Output = &'a str> +where + F: Fn(char) -> bool, +{ + move |input: &'a str| match input.find(&pattern) { + Some(index) if index != 0 => Ok((&input[index..], &input[0..index])), + _ => Err(input), + } +} + +/// A parser which matches all values until the specified pattern no longer match. +/// +/// This parser only ever fails if the input has a length of zero. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{take_while, Parser}; +/// let parser = take_while(|c| c == '1'); +/// assert_eq!(Ok(("2", "11")), parser.parse("112")); +/// assert_eq!(Err("22"), parser.parse("22")); +/// ``` +pub fn take_while<'a, F>(pattern: F) -> impl Parser<'a, Output = &'a str> +where + F: Fn(char) -> bool, +{ + move |input: &'a str| match input + .char_indices() + .take_while(|(_p, c)| pattern(*c)) + .last() + { + Some((index, c)) => { + let index = index + c.len_utf8(); + Ok((&input[index..], &input[0..index])) + } + _ => Err(input), + } +} + +// Variadic parser combinators + +/// A parser combinator which matches a sequence of parsers in an all-or-nothing fashion. +/// +/// The returned value is a tuple containing the outputs of all parsers in order. Each +/// parser in the sequence may be typed differently. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{seq, Parser}; +/// let parser = seq!("<", "a", ">"); +/// assert_eq!(Ok(("", ("<", "a", ">"))), parser.parse("")); +/// assert_eq!(Err(""), parser.parse("")); +/// ``` +#[macro_export] +macro_rules! seq { + ($($parsers: expr),+ $(,)?) => { + ($($parsers),+) + } +} + +// Seq is implemented using trait-implementations of Parser for various size tuples. +// This allows sequences to be typed heterogeneously. +macro_rules! seq_impl { + ($($parser:ident),+) => { + #[allow(non_snake_case)] + impl<'a, $($parser),+> Parser<'a> for ($($parser),+) + where + $($parser: Parser<'a>),+ + { + type Output = ($($parser::Output),+); + + fn parse(&self, input: &'a str) -> ParseResult<'a, Self::Output> { + let ($($parser),+) = self; + seq_body_impl!(input, input, $($parser),+ ; ) + } + } + } +} + +macro_rules! seq_body_impl { + ($input:expr, $next_input:expr, $head:ident, $($tail:ident),+ ; $(,)? $($acc:ident),*) => { + match $head.parse($next_input) { + Ok((next_input, $head)) => seq_body_impl!($input, next_input, $($tail),+ ; $($acc),*, $head), + Err(_) => Err($input), + } + }; + ($input:expr, $next_input:expr, $last:ident ; $(,)? $($acc:ident),*) => { + match $last.parse($next_input) { + Ok((next_input, last)) => Ok((next_input, ($($acc),+, last))), + Err(_) => Err($input), + } + } +} + +seq_impl!(A, B); +seq_impl!(A, B, C); +seq_impl!(A, B, C, D); +seq_impl!(A, B, C, D, E); +seq_impl!(A, B, C, D, E, F); +seq_impl!(A, B, C, D, E, F, G); +seq_impl!(A, B, C, D, E, F, G, H); +seq_impl!(A, B, C, D, E, F, G, H, I); +seq_impl!(A, B, C, D, E, F, G, H, I, J); + +/// A parser combinator which chooses the first of the input parsers which matches +/// successfully. +/// +/// All input parsers must have the same output type. This is a variadic form for [or]. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{choice, or, Parser}; +/// let parser = choice!("foo", "bar", "baz"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foo")); +/// assert_eq!(Ok(("", "bar")), parser.parse("bar")); +/// assert_eq!(Err("quiz"), parser.parse("quiz")); +/// ``` +#[macro_export] +macro_rules! choice { + ($parser: expr $(,)?) => { + $parser + }; + ($parser: expr, $($rest: expr),+ $(,)?) => { + or($parser, choice!($($rest),+)) + } +} + +// Ordinary parser combinators + +/// A parser combinator which takes a parser as input and maps the output using the +/// given transformation function. +/// +/// This corresponds to [Result::map]. The value is only mapped if the input parser +/// matches against input. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{map, Parser}; +/// let parser = map("123", |s| s.parse::().unwrap()); +/// assert_eq!(Ok(("", 123)), parser.parse("123")); +/// assert_eq!(Err("abc"), parser.parse("abc")); +/// ``` +pub fn map<'a, P, F, T>(parser: P, map_fn: F) -> impl Parser<'a, Output = T> +where + P: Parser<'a>, + F: Fn(P::Output) -> T, +{ + move |input| { + parser + .parse(input) + .map(|(next_input, result)| (next_input, map_fn(result))) + } +} + +/// A parser combinator which succeeds if the given parser matches the input and +/// the given `filter_map_fn` returns `Some`. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{filter_map, take_until, Parser}; +/// let parser = filter_map(take_until(|c| c == '.'), |s| s.parse::().ok()); +/// assert_eq!(Ok((".456", 123)), parser.parse("123.456")); +/// assert_eq!(Err("abc.def"), parser.parse("abc.def")); +/// ``` +pub fn filter_map<'a, P, F, T>(parser: P, filter_map_fn: F) -> impl Parser<'a, Output = T> +where + P: Parser<'a>, + F: Fn(P::Output) -> Option, +{ + move |input| match parser.parse(input) { + Ok((next_input, value)) => match filter_map_fn(value) { + Some(value) => Ok((next_input, value)), + None => Err(input), + }, + Err(_) => Err(input), + } +} + +/// A parser combinator which succeeds if the first given parser matches the input and +/// the second given parse also matches. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{reparse_as, take_until, one_or_more, Parser}; +/// let parser = reparse_as(take_until(|c| c == '/'), one_or_more("a")); +/// assert_eq!(Ok(("/bb", vec!["a", "a"])), parser.parse("aa/bb")); +/// ``` +pub fn reparse_as<'a, P1, P2, T>(parser1: P1, parser2: P2) -> impl Parser<'a, Output = T> +where + P1: Parser<'a, Output = &'a str>, + P2: Parser<'a, Output = T>, +{ + filter_map(parser1, move |str| { + parser2.parse(str).map(|(_, value)| value).ok() + }) +} + +/// A parser combinator which only matches the input when the predicate function +/// returns true. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{filter, take_until, Parser}; +/// let parser = filter(take_until(|c| c == '.'), |s| s == &"123"); +/// assert_eq!(Ok((".456", "123")), parser.parse("123.456")); +/// assert_eq!(Err("456.123"), parser.parse("456.123")); +/// ``` +pub fn filter<'a, P, F, T>(parser: P, pred_fn: F) -> impl Parser<'a, Output = T> +where + P: Parser<'a, Output = T>, + F: Fn(&P::Output) -> bool, +{ + move |input| { + if let Ok((next_input, value)) = parser.parse(input) { + if pred_fn(&value) { + return Ok((next_input, value)); + } + } + Err(input) + } +} + +/// A parser combinator which matches either of the input parsers. +/// +/// Both parsers must have the same output type. For a variadic form which +/// can take any number of parsers, use `choice!`. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{or, Parser}; +/// let parser = or("foo", "bar"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foo")); +/// assert_eq!(Ok(("", "bar")), parser.parse("bar")); +/// assert_eq!(Err("baz"), parser.parse("baz")); +/// ``` +pub fn or<'a, P1, P2, T>(parser1: P1, parser2: P2) -> impl Parser<'a, Output = T> +where + P1: Parser<'a, Output = T>, + P2: Parser<'a, Output = T>, +{ + move |input| match parser1.parse(input) { + ok @ Ok(_) => ok, + Err(_) => parser2.parse(input), + } +} + +/// A parser combinator which attempts to match the given parser, returning a +/// `None` output value if the parser does not match. +/// +/// The parser produced with this combinator always succeeds. If the given parser +/// succeeds, `Some(value)` is returned where `value` is the output of the given +/// parser. Otherwise, `None`. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{optional, Parser}; +/// let parser = optional("foo"); +/// assert_eq!(Ok(("bar", Some("foo"))), parser.parse("foobar")); +/// assert_eq!(Ok(("bar", None)), parser.parse("bar")); +/// ``` +pub fn optional<'a, P, T>(parser: P) -> impl Parser<'a, Output = Option> +where + P: Parser<'a, Output = T>, +{ + move |input| match parser.parse(input) { + Ok((next_input, value)) => Ok((next_input, Some(value))), + Err(_) => Ok((input, None)), + } +} + +/// A parser combinator which runs the given parsers in sequence and returns the +/// value of `left` if both are matched. +/// +/// This is useful for two-element sequences in which you only want the output +/// value of the `left` parser. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{left, Parser}; +/// let parser = left("foo", "bar"); +/// assert_eq!(Ok(("", "foo")), parser.parse("foobar")); +/// ``` +pub fn left<'a, L, R, T>(left: L, right: R) -> impl Parser<'a, Output = T> +where + L: Parser<'a, Output = T>, + R: Parser<'a>, +{ + map(seq!(left, right), |(left_value, _)| left_value) +} + +/// A parser combinator which runs the given parsers in sequence and returns the +/// value of `right` if both are matched. +/// +/// This is useful for two-element sequences in which you only want the output +/// value of the `right` parser. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{right, Parser}; +/// let parser = right("foo", "bar"); +/// assert_eq!(Ok(("", "bar")), parser.parse("foobar")); +/// ``` +pub fn right<'a, L, R, T>(left: L, right: R) -> impl Parser<'a, Output = T> +where + L: Parser<'a>, + R: Parser<'a, Output = T>, +{ + map(seq!(left, right), |(_, right_value)| right_value) +} + +/// A parser combinator which matches the given parser against the input zero or +/// more times. +/// +/// This parser always succeeds and returns the empty Vec when it matched zero +/// times. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{zero_or_more, Parser}; +/// let parser = zero_or_more("a"); +/// assert_eq!(Ok(("", vec![])), parser.parse("")); +/// assert_eq!(Ok(("", vec!["a"])), parser.parse("a")); +/// assert_eq!(Ok(("", vec!["a", "a"])), parser.parse("aa")); +/// assert_eq!(Ok(("bb", vec![])), parser.parse("bb")); +/// ``` +pub fn zero_or_more<'a, P, T>(parser: P) -> impl Parser<'a, Output = Vec> +where + P: Parser<'a, Output = T>, +{ + move |mut input| { + let mut values = Vec::new(); + + while let Ok((next_input, value)) = parser.parse(input) { + input = next_input; + values.push(value); + } + + Ok((input, values)) + } +} + +/// A parser combinator which matches the given parser against the input one or +/// more times. +/// +/// This parser combinator acts the same as [zero_or_more] but must match at +/// least once. +/// +/// # Examples +/// +/// ``` +/// use helix_parsec::{one_or_more, Parser}; +/// let parser = one_or_more("a"); +/// assert_eq!(Err(""), parser.parse("")); +/// assert_eq!(Ok(("", vec!["a"])), parser.parse("a")); +/// assert_eq!(Ok(("", vec!["a", "a"])), parser.parse("aa")); +/// assert_eq!(Err("bb"), parser.parse("bb")); +/// ``` +pub fn one_or_more<'a, P, T>(parser: P) -> impl Parser<'a, Output = Vec> +where + P: Parser<'a, Output = T>, +{ + move |mut input| { + let mut values = Vec::new(); + + match parser.parse(input) { + Ok((next_input, value)) => { + input = next_input; + values.push(value); + } + Err(err) => return Err(err), + } + + while let Ok((next_input, value)) = parser.parse(input) { + input = next_input; + values.push(value); + } + + Ok((input, values)) + } +} + +/// A parser combinator which matches one or more instances of the given parser +/// interspersed with the separator parser. +/// +/// Output values of the separator parser are discarded. +/// +/// This is typically used to parse function arguments or list items. +/// +/// # Examples +/// +/// ```rust +/// use helix_parsec::{sep, Parser}; +/// let parser = sep("a", ","); +/// assert_eq!(Ok(("", vec!["a", "a", "a"])), parser.parse("a,a,a")); +/// ``` +pub fn sep<'a, P, S, T>(parser: P, separator: S) -> impl Parser<'a, Output = Vec> +where + P: Parser<'a, Output = T>, + S: Parser<'a>, +{ + move |mut input| { + let mut values = Vec::new(); + + match parser.parse(input) { + Ok((next_input, value)) => { + input = next_input; + values.push(value); + } + Err(err) => return Err(err), + } + + loop { + match separator.parse(input) { + Ok((next_input, _)) => input = next_input, + Err(_) => break, + } + + match parser.parse(input) { + Ok((next_input, value)) => { + input = next_input; + values.push(value); + } + Err(_) => break, + } + } + + Ok((input, values)) + } +} diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index 603f37d39ab8..bca567c28688 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -10,11 +10,7 @@ repository = "https://github.com/helix-editor/helix" homepage = "https://helix-editor.com" include = ["src/**/*", "README.md"] default-run = "hx" -rust-version = "1.57" - -[package.metadata.nix] -build = true -app = true +rust-version = "1.65" [features] default = ["git"] @@ -41,7 +37,7 @@ which = "4.4" tokio = { version = "1", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot"] } tui = { path = "../helix-tui", package = "helix-tui", default-features = false, features = ["crossterm"] } -crossterm = { version = "0.25", features = ["event-stream"] } +crossterm = { version = "0.26", features = ["event-stream"] } signal-hook = "0.3" tokio-stream = "0.1" futures-util = { version = "0.3", features = ["std", "async-await"], default-features = false } @@ -67,8 +63,8 @@ serde_json = "1.0" serde = { version = "1.0", features = ["derive"] } # ripgrep for global search -grep-regex = "0.1.10" -grep-searcher = "0.1.10" +grep-regex = "0.1.11" +grep-searcher = "0.1.11" [target.'cfg(not(windows))'.dependencies] # https://github.com/vorner/signal-hook/issues/100 signal-hook-tokio = { version = "0.3", features = ["futures-v0_3"] } @@ -79,4 +75,4 @@ helix-loader = { version = "0.6", path = "../helix-loader" } [dev-dependencies] smallvec = "1.10" indoc = "2.0.0" -tempfile = "3.3.0" +tempfile = "3.4.0" diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index c8e8ecb1ae61..c7e939959ca4 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -30,21 +30,15 @@ use crate::{ use log::{debug, error, warn}; use std::{ - io::{stdin, stdout, Write}, + io::{stdin, stdout}, + path::Path, sync::Arc, time::{Duration, Instant}, }; use anyhow::{Context, Error}; -use crossterm::{ - event::{ - DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, - EnableFocusChange, EnableMouseCapture, Event as CrosstermEvent, - }, - execute, terminal, - tty::IsTty, -}; +use crossterm::{event::Event as CrosstermEvent, tty::IsTty}; #[cfg(not(windows))] use { signal_hook::{consts::signal, low_level}, @@ -62,10 +56,12 @@ use tui::backend::CrosstermBackend; use tui::backend::TestBackend; #[cfg(not(feature = "integration"))] -type Terminal = tui::terminal::Terminal>; +type TerminalBackend = CrosstermBackend; #[cfg(feature = "integration")] -type Terminal = tui::terminal::Terminal; +type TerminalBackend = TestBackend; + +type Terminal = tui::terminal::Terminal; pub struct Application { compositor: Compositor, @@ -107,23 +103,6 @@ fn setup_integration_logging() { .apply(); } -fn restore_term() -> Result<(), Error> { - let mut stdout = stdout(); - // reset cursor shape - write!(stdout, "\x1B[0 q")?; - // Ignore errors on disabling, this might trigger on windows if we call - // disable without calling enable previously - let _ = execute!(stdout, DisableMouseCapture); - execute!( - stdout, - DisableBracketedPaste, - DisableFocusChange, - terminal::LeaveAlternateScreen - )?; - terminal::disable_raw_mode()?; - Ok(()) -} - impl Application { pub fn new( args: Args, @@ -135,10 +114,9 @@ impl Application { use helix_view::editor::Action; - let theme_loader = std::sync::Arc::new(theme::Loader::new( - &helix_loader::config_dir(), - &helix_loader::runtime_dir(), - )); + let mut theme_parent_dirs = vec![helix_loader::config_dir()]; + theme_parent_dirs.extend(helix_loader::runtime_dirs().iter().cloned()); + let theme_loader = std::sync::Arc::new(theme::Loader::new(&theme_parent_dirs)); let true_color = config.editor.true_color || crate::true_color(); let theme = config @@ -184,7 +162,7 @@ impl Application { compositor.push(editor_view); if args.load_tutor { - let path = helix_loader::runtime_dir().join("tutor"); + let path = helix_loader::runtime_file(Path::new("tutor")); editor.open(&path, Action::VerticalSplit)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(editor).set_path(None)?; @@ -341,12 +319,12 @@ impl Application { tokio::select! { biased; - Some(event) = input_stream.next() => { - self.handle_terminal_events(event).await; - } Some(signal) = self.signals.next() => { self.handle_signals(signal).await; } + Some(event) = input_stream.next() => { + self.handle_terminal_events(event).await; + } Some(callback) = self.jobs.futures.next() => { self.jobs.handle_callback(&mut self.editor, &mut self.compositor, callback); self.render().await; @@ -468,13 +446,7 @@ impl Application { pub async fn handle_signals(&mut self, signal: i32) { match signal { signal::SIGTSTP => { - // restore cursor - use helix_view::graphics::CursorKind; - self.terminal - .backend_mut() - .show_cursor(CursorKind::Block) - .ok(); - restore_term().unwrap(); + self.restore_term().unwrap(); low_level::emulate_default_handler(signal::SIGTSTP).unwrap(); } signal::SIGCONT => { @@ -634,6 +606,11 @@ impl Application { self.compositor .handle_event(&Event::Resize(width, height), &mut cx) } + // Ignore keyboard release events. + CrosstermEvent::Key(crossterm::event::KeyEvent { + kind: crossterm::event::KeyEventKind::Release, + .. + }) => false, event => self.compositor.handle_event(&event.into(), &mut cx), }; @@ -951,24 +928,32 @@ impl Application { Call::MethodCall(helix_lsp::jsonrpc::MethodCall { method, params, id, .. }) => { - let call = match MethodCall::parse(&method, params) { - Ok(call) => call, + let reply = match MethodCall::parse(&method, params) { Err(helix_lsp::Error::Unhandled) => { - error!("Language Server: Method not found {}", method); - return; + error!( + "Language Server: Method {} not found in request {}", + method, id + ); + Err(helix_lsp::jsonrpc::Error { + code: helix_lsp::jsonrpc::ErrorCode::MethodNotFound, + message: format!("Method not found: {}", method), + data: None, + }) } Err(err) => { log::error!( - "received malformed method call from Language Server: {}: {}", + "Language Server: Received malformed method call {} in request {}: {}", method, + id, err ); - return; + Err(helix_lsp::jsonrpc::Error { + code: helix_lsp::jsonrpc::ErrorCode::ParseError, + message: format!("Malformed method call: {}", method), + data: None, + }) } - }; - - let reply = match call { - MethodCall::WorkDoneProgressCreate(params) => { + Ok(MethodCall::WorkDoneProgressCreate(params)) => { self.lsp_progress.create(server_id, params.token); let editor_view = self @@ -982,7 +967,7 @@ impl Application { Ok(serde_json::Value::Null) } - MethodCall::ApplyWorkspaceEdit(params) => { + Ok(MethodCall::ApplyWorkspaceEdit(params)) => { apply_workspace_edit( &mut self.editor, helix_lsp::OffsetEncoding::Utf8, @@ -995,13 +980,13 @@ impl Application { failed_change: None, })) } - MethodCall::WorkspaceFolders => { + Ok(MethodCall::WorkspaceFolders) => { let language_server = self.editor.language_servers.get_by_id(server_id).unwrap(); Ok(json!(language_server.workspace_folders())) } - MethodCall::WorkspaceConfiguration(params) => { + Ok(MethodCall::WorkspaceConfiguration(params)) => { let result: Vec<_> = params .items .iter() @@ -1045,24 +1030,19 @@ impl Application { } } - async fn claim_term(&mut self) -> Result<(), Error> { + async fn claim_term(&mut self) -> std::io::Result<()> { + let terminal_config = self.config.load().editor.clone().into(); + self.terminal.claim(terminal_config) + } + + fn restore_term(&mut self) -> std::io::Result<()> { + let terminal_config = self.config.load().editor.clone().into(); use helix_view::graphics::CursorKind; - terminal::enable_raw_mode()?; - if self.terminal.cursor_kind() == CursorKind::Hidden { - self.terminal.backend_mut().hide_cursor().ok(); - } - let mut stdout = stdout(); - execute!( - stdout, - terminal::EnterAlternateScreen, - EnableBracketedPaste, - EnableFocusChange - )?; - execute!(stdout, terminal::Clear(terminal::ClearType::All))?; - if self.config.load().editor.mouse { - execute!(stdout, EnableMouseCapture)?; - } - Ok(()) + self.terminal + .backend_mut() + .show_cursor(CursorKind::Block) + .ok(); + self.terminal.restore(terminal_config) } pub async fn run(&mut self, input_stream: &mut S) -> Result @@ -1077,7 +1057,7 @@ impl Application { // We can't handle errors properly inside this closure. And it's // probably not a good idea to `unwrap()` inside a panic handler. // So we just ignore the `Result`. - let _ = restore_term(); + let _ = TerminalBackend::force_restore(); hook(info); })); @@ -1085,13 +1065,7 @@ impl Application { let close_errs = self.close().await; - // restore cursor - use helix_view::graphics::CursorKind; - self.terminal - .backend_mut() - .show_cursor(CursorKind::Block) - .ok(); - restore_term()?; + self.restore_term()?; for err in close_errs { self.editor.exit_code = 1; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index fb55ca2a8e72..803f4051d074 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -5,6 +5,7 @@ pub(crate) mod typed; pub use dap::*; use helix_vcs::Hunk; pub use lsp::*; +use tokio::sync::oneshot; use tui::widgets::Row; pub use typed::*; @@ -52,7 +53,10 @@ use crate::{ filter_picker_entry, job::Callback, keymap::ReverseKeymap, - ui::{self, overlay::overlayed, FilePicker, Picker, Popup, Prompt, PromptEvent}, + ui::{ + self, editor::InsertEvent, overlay::overlayed, FilePicker, Picker, Popup, Prompt, + PromptEvent, + }, }; use crate::job::{self, Jobs}; @@ -430,6 +434,7 @@ impl MappableCommand { goto_next_paragraph, "Goto next paragraph", goto_prev_paragraph, "Goto previous paragraph", dap_launch, "Launch debug target", + dap_restart, "Restart debugging session", dap_toggle_breakpoint, "Toggle breakpoint", dap_continue, "Continue program execution", dap_pause, "Pause program execution", @@ -2827,10 +2832,15 @@ fn push_jump(view: &mut View, doc: &Document) { } fn goto_line(cx: &mut Context) { - goto_line_impl(cx.editor, cx.count) + if cx.count.is_some() { + let (view, doc) = current!(cx.editor); + push_jump(view, doc); + + goto_line_without_jumplist(cx.editor, cx.count); + } } -fn goto_line_impl(editor: &mut Editor, count: Option) { +fn goto_line_without_jumplist(editor: &mut Editor, count: Option) { if let Some(count) = count { let (view, doc) = current!(editor); let text = doc.text().slice(..); @@ -2847,7 +2857,6 @@ fn goto_line_impl(editor: &mut Editor, count: Option) { .clone() .transform(|range| range.put_cursor(text, pos, editor.mode == Mode::Select)); - push_jump(view, doc); doc.set_selection(view.id, selection); } } @@ -2941,7 +2950,6 @@ fn goto_first_diag(cx: &mut Context) { None => return, }; doc.set_selection(view.id, selection); - align_view(doc, view, Align::Center); } fn goto_last_diag(cx: &mut Context) { @@ -2951,7 +2959,6 @@ fn goto_last_diag(cx: &mut Context) { None => return, }; doc.set_selection(view.id, selection); - align_view(doc, view, Align::Center); } fn goto_next_diag(cx: &mut Context) { @@ -2973,7 +2980,6 @@ fn goto_next_diag(cx: &mut Context) { None => return, }; doc.set_selection(view.id, selection); - align_view(doc, view, Align::Center); } fn goto_prev_diag(cx: &mut Context) { @@ -2998,7 +3004,6 @@ fn goto_prev_diag(cx: &mut Context) { None => return, }; doc.set_selection(view.id, selection); - align_view(doc, view, Align::Center); } fn goto_first_change(cx: &mut Context) { @@ -3014,13 +3019,13 @@ fn goto_first_change_impl(cx: &mut Context, reverse: bool) { let (view, doc) = current!(editor); if let Some(handle) = doc.diff_handle() { let hunk = { - let hunks = handle.hunks(); + let diff = handle.load(); let idx = if reverse { - hunks.len().saturating_sub(1) + diff.len().saturating_sub(1) } else { 0 }; - hunks.nth_hunk(idx) + diff.nth_hunk(idx) }; if hunk != Hunk::NONE { let range = hunk_range(hunk, doc.text().slice(..)); @@ -3052,22 +3057,19 @@ fn goto_next_change_impl(cx: &mut Context, direction: Direction) { let selection = doc.selection(view.id).clone().transform(|range| { let cursor_line = range.cursor_line(doc_text) as u32; - let hunks = diff_handle.hunks(); + let diff = diff_handle.load(); let hunk_idx = match direction { - Direction::Forward => hunks + Direction::Forward => diff .next_hunk(cursor_line) - .map(|idx| (idx + count).min(hunks.len() - 1)), - Direction::Backward => hunks + .map(|idx| (idx + count).min(diff.len() - 1)), + Direction::Backward => diff .prev_hunk(cursor_line) .map(|idx| idx.saturating_sub(count)), }; - // TODO refactor with let..else once MSRV reaches 1.65 - let hunk_idx = if let Some(hunk_idx) = hunk_idx { - hunk_idx - } else { + let Some(hunk_idx) = hunk_idx else { return range; }; - let hunk = hunks.nth_hunk(hunk_idx); + let hunk = diff.nth_hunk(hunk_idx); let new_range = hunk_range(hunk, doc_text); if editor.mode == Mode::Select { let head = if new_range.head < range.anchor { @@ -4173,6 +4175,24 @@ pub fn completion(cx: &mut Context) { None => return, }; + // setup a chanel that allows the request to be canceled + let (tx, rx) = oneshot::channel(); + // set completion_request so that this request can be canceled + // by setting completion_request, the old channel stored there is dropped + // and the associated request is automatically dropped + cx.editor.completion_request_handle = Some(tx); + let future = async move { + tokio::select! { + biased; + _ = rx => { + Ok(serde_json::Value::Null) + } + res = future => { + res + } + } + }; + let trigger_offset = cursor; // TODO: trigger_offset should be the cursor offset but we also need a starting offset from where we want to apply @@ -4183,12 +4203,35 @@ pub fn completion(cx: &mut Context) { iter.reverse(); let offset = iter.take_while(|ch| chars::char_is_word(*ch)).count(); let start_offset = cursor.saturating_sub(offset); + let savepoint = doc.savepoint(view); + + let trigger_doc = doc.id(); + let trigger_view = view.id; + + // FIXME: The commands Context can only have a single callback + // which means it gets overwritten when executing keybindings + // with multiple commands or macros. This would mean that completion + // might be incorrectly applied when repeating the insertmode action + // + // TODO: to solve this either make cx.callback a Vec of callbacks or + // alternatively move `last_insert` to `helix_view::Editor` + cx.callback = Some(Box::new( + move |compositor: &mut Compositor, _cx: &mut compositor::Context| { + let ui = compositor.find::().unwrap(); + ui.last_insert.1.push(InsertEvent::RequestCompletion); + }, + )); cx.callback( future, move |editor, compositor, response: Option| { - if editor.mode != Mode::Insert { - // we're not in insert mode anymore + let (view, doc) = current_ref!(editor); + // check if the completion request is stale. + // + // Completions are completed asynchrounsly and therefore the user could + //switch document/view or leave insert mode. In all of thoise cases the + // completion should be discarded + if editor.mode != Mode::Insert || view.id != trigger_view || doc.id() != trigger_doc { return; } @@ -4210,6 +4253,7 @@ pub fn completion(cx: &mut Context) { let ui = compositor.find::().unwrap(); ui.set_completion( editor, + savepoint, items, offset_encoding, start_offset, @@ -4327,7 +4371,6 @@ fn shrink_selection(cx: &mut Context) { // try to restore previous selection if let Some(prev_selection) = view.object_selections.pop() { if current_selection.contains(&prev_selection) { - // allow shrinking the selection only if current selection contains the previous object selection doc.set_selection(view.id, prev_selection); return; } else { @@ -4727,14 +4770,14 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) { let textobject_change = |range: Range| -> Range { let diff_handle = doc.diff_handle().unwrap(); - let hunks = diff_handle.hunks(); + let diff = diff_handle.load(); let line = range.cursor_line(text); - let hunk_idx = if let Some(hunk_idx) = hunks.hunk_at(line as u32, false) { + let hunk_idx = if let Some(hunk_idx) = diff.hunk_at(line as u32, false) { hunk_idx } else { return range; }; - let hunk = hunks.nth_hunk(hunk_idx).after; + let hunk = diff.nth_hunk(hunk_idx).after; let start = text.line_to_char(hunk.start as usize); let end = text.line_to_char(hunk.end as usize); diff --git a/helix-term/src/commands/dap.rs b/helix-term/src/commands/dap.rs index 023ed37794df..dac1e9d5258d 100644 --- a/helix-term/src/commands/dap.rs +++ b/helix-term/src/commands/dap.rs @@ -289,6 +289,36 @@ pub fn dap_launch(cx: &mut Context) { )))); } +pub fn dap_restart(cx: &mut Context) { + let debugger = match &cx.editor.debugger { + Some(debugger) => debugger, + None => { + cx.editor.set_error("Debugger is not running"); + return; + } + }; + if !debugger + .capabilities() + .supports_restart_request + .unwrap_or(false) + { + cx.editor + .set_error("Debugger does not support session restarts"); + return; + } + if debugger.starting_request_args().is_none() { + cx.editor + .set_error("No arguments found with which to restart the sessions"); + return; + } + + dap_callback( + cx.jobs, + debugger.restart(), + |editor, _compositor, _resp: ()| editor.set_status("Debugging session restarted"), + ); +} + fn debug_parameter_prompt( completions: Vec, config_name: String, diff --git a/helix-term/src/commands/lsp.rs b/helix-term/src/commands/lsp.rs index 3b94c9bd5558..08519366b257 100644 --- a/helix-term/src/commands/lsp.rs +++ b/helix-term/src/commands/lsp.rs @@ -5,7 +5,7 @@ use helix_lsp::{ self, CodeAction, CodeActionOrCommand, CodeActionTriggerKind, DiagnosticSeverity, NumberOrString, }, - util::{diagnostic_to_lsp_diagnostic, lsp_pos_to_pos, lsp_range_to_range, range_to_lsp_range}, + util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range}, OffsetEncoding, }; use tui::{ @@ -196,15 +196,15 @@ fn jump_to_location( } } let (view, doc) = current!(editor); - let definition_pos = location.range.start; // TODO: convert inside server - let new_pos = if let Some(new_pos) = lsp_pos_to_pos(doc.text(), definition_pos, offset_encoding) - { - new_pos - } else { - return; - }; - doc.set_selection(view.id, Selection::point(new_pos)); + let new_range = + if let Some(new_range) = lsp_range_to_range(doc.text(), location.range, offset_encoding) { + new_range + } else { + log::warn!("lsp position out of bounds - {:?}", location.range); + return; + }; + doc.set_selection(view.id, Selection::single(new_range.anchor, new_range.head)); align_view(doc, view, Align::Center); } @@ -1232,49 +1232,115 @@ pub fn hover(cx: &mut Context) { } pub fn rename_symbol(cx: &mut Context) { - let (view, doc) = current_ref!(cx.editor); - let text = doc.text().slice(..); - let primary_selection = doc.selection(view.id).primary(); - let prefill = if primary_selection.len() > 1 { - primary_selection - } else { - use helix_core::textobject::{textobject_word, TextObject}; - textobject_word(text, primary_selection, TextObject::Inside, 1, false) + fn get_prefill_from_word_boundary(editor: &Editor) -> String { + let (view, doc) = current_ref!(editor); + let text = doc.text().slice(..); + let primary_selection = doc.selection(view.id).primary(); + if primary_selection.len() > 1 { + primary_selection + } else { + use helix_core::textobject::{textobject_word, TextObject}; + textobject_word(text, primary_selection, TextObject::Inside, 1, false) + } + .fragment(text) + .into() } - .fragment(text) - .into(); - ui::prompt_with_input( - cx, - "rename-to:".into(), - prefill, - None, - ui::completers::none, - move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { - if event != PromptEvent::Validate { - return; + + fn get_prefill_from_lsp_response( + editor: &Editor, + offset_encoding: OffsetEncoding, + response: Option, + ) -> Result { + match response { + Some(lsp::PrepareRenameResponse::Range(range)) => { + let text = doc!(editor).text(); + + Ok(lsp_range_to_range(text, range, offset_encoding) + .ok_or("lsp sent invalid selection range for rename")? + .fragment(text.slice(..)) + .into()) + } + Some(lsp::PrepareRenameResponse::RangeWithPlaceholder { placeholder, .. }) => { + Ok(placeholder) } + Some(lsp::PrepareRenameResponse::DefaultBehavior { .. }) => { + Ok(get_prefill_from_word_boundary(editor)) + } + None => Err("lsp did not respond to prepare rename request"), + } + } - let (view, doc) = current!(cx.editor); - let language_server = language_server!(cx.editor, doc); - let offset_encoding = language_server.offset_encoding(); + fn create_rename_prompt(editor: &Editor, prefill: String) -> Box { + let prompt = ui::Prompt::new( + "rename-to:".into(), + None, + ui::completers::none, + move |cx: &mut compositor::Context, input: &str, event: PromptEvent| { + if event != PromptEvent::Validate { + return; + } + + let (view, doc) = current!(cx.editor); + let language_server = language_server!(cx.editor, doc); + let offset_encoding = language_server.offset_encoding(); + + let pos = doc.position(view.id, offset_encoding); + + let future = + match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) { + Some(future) => future, + None => { + cx.editor + .set_error("Language server does not support symbol renaming"); + return; + } + }; + match block_on(future) { + Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits), + Err(err) => cx.editor.set_error(err.to_string()), + } + }, + ) + .with_line(prefill, editor); - let pos = doc.position(view.id, offset_encoding); + Box::new(prompt) + } - let future = - match language_server.rename_symbol(doc.identifier(), pos, input.to_string()) { - Some(future) => future, - None => { - cx.editor - .set_error("Language server does not support symbol renaming"); + let (view, doc) = current!(cx.editor); + let language_server = language_server!(cx.editor, doc); + let offset_encoding = language_server.offset_encoding(); + + let pos = doc.position(view.id, offset_encoding); + + match language_server.prepare_rename(doc.identifier(), pos) { + // Language server supports textDocument/prepareRename, use it. + Some(future) => cx.callback( + future, + move |editor, compositor, response: Option| { + let prefill = match get_prefill_from_lsp_response(editor, offset_encoding, response) + { + Ok(p) => p, + Err(e) => { + editor.set_error(e); return; } }; - match block_on(future) { - Ok(edits) => apply_workspace_edit(cx.editor, offset_encoding, &edits), - Err(err) => cx.editor.set_error(err.to_string()), - } - }, - ); + + let prompt = create_rename_prompt(editor, prefill); + + compositor.push(prompt); + }, + ), + // Language server does not support textDocument/prepareRename, fall back + // to word boundary selection. + None => { + let prefill = get_prefill_from_word_boundary(cx.editor); + + let prompt = create_rename_prompt(cx.editor, prefill); + + cx.push_layer(prompt); + } + }; } pub fn select_references_to_symbol_under_cursor(cx: &mut Context) { diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index b0fd18a76b79..e9a7222586a9 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -1354,6 +1354,37 @@ fn lsp_restart( Ok(()) } +fn lsp_stop( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let doc = doc!(cx.editor); + + let ls_id = doc + .language_server() + .map(|ls| ls.id()) + .context("LSP not running for the current document")?; + + let config = doc + .language_config() + .context("LSP not defined for the current document")?; + cx.editor.language_servers.stop(config); + + for doc in cx.editor.documents_mut() { + if doc.language_server().map_or(false, |ls| ls.id() == ls_id) { + doc.set_language_server(None); + doc.set_diagnostics(Default::default()); + } + } + + Ok(()) +} + fn tree_sitter_scopes( cx: &mut compositor::Context, _args: &[Cow], @@ -1534,54 +1565,74 @@ fn tutor( return Ok(()); } - let path = helix_loader::runtime_dir().join("tutor"); + let path = helix_loader::runtime_file(Path::new("tutor")); cx.editor.open(&path, Action::Replace)?; // Unset path to prevent accidentally saving to the original tutor file. doc_mut!(cx.editor).set_path(None)?; Ok(()) } +fn abort_goto_line_number_preview(cx: &mut compositor::Context) { + if let Some(last_selection) = cx.editor.last_selection.take() { + let scrolloff = cx.editor.config().scrolloff; + + let (view, doc) = current!(cx.editor); + doc.set_selection(view.id, last_selection); + view.ensure_cursor_in_view(doc, scrolloff); + } +} + +fn update_goto_line_number_preview( + cx: &mut compositor::Context, + args: &[Cow], +) -> anyhow::Result<()> { + cx.editor.last_selection.get_or_insert_with(|| { + let (view, doc) = current!(cx.editor); + doc.selection(view.id).clone() + }); + + let scrolloff = cx.editor.config().scrolloff; + let line = args[0].parse::()?; + goto_line_without_jumplist(cx.editor, NonZeroUsize::new(line)); + + let (view, doc) = current!(cx.editor); + view.ensure_cursor_in_view(doc, scrolloff); + + Ok(()) +} + pub(super) fn goto_line_number( cx: &mut compositor::Context, args: &[Cow], event: PromptEvent, ) -> anyhow::Result<()> { match event { - PromptEvent::Abort => { - if let Some(line_number) = cx.editor.last_line_number { - goto_line_impl(cx.editor, NonZeroUsize::new(line_number)); - let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, line_number); - cx.editor.last_line_number = None; - } - return Ok(()); - } + PromptEvent::Abort => abort_goto_line_number_preview(cx), PromptEvent::Validate => { ensure!(!args.is_empty(), "Line number required"); - cx.editor.last_line_number = None; - } - PromptEvent::Update => { - if args.is_empty() { - if let Some(line_number) = cx.editor.last_line_number { - // When a user hits backspace and there are no numbers left, - // we can bring them back to their original line - goto_line_impl(cx.editor, NonZeroUsize::new(line_number)); - let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, line_number); - cx.editor.last_line_number = None; - } - return Ok(()); - } + + // If we are invoked directly via a keybinding, Validate is + // sent without any prior Update events. Ensure the cursor + // is moved to the appropriate location. + update_goto_line_number_preview(cx, args)?; + + let last_selection = cx + .editor + .last_selection + .take() + .expect("update_goto_line_number_preview should always set last_selection"); + let (view, doc) = current!(cx.editor); - let text = doc.text().slice(..); - let line = doc.selection(view.id).primary().cursor_line(text); - cx.editor.last_line_number.get_or_insert(line + 1); + view.jumps.push((doc.id(), last_selection)); } + + // When a user hits backspace and there are no numbers left, + // we can bring them back to their original selection. If they + // begin typing numbers again, we'll start a new preview session. + PromptEvent::Update if args.is_empty() => abort_goto_line_number_preview(cx), + PromptEvent::Update => update_goto_line_number_preview(cx, args)?, } - let line = args[0].parse::()?; - goto_line_impl(cx.editor, NonZeroUsize::new(line)); - let (view, doc) = current!(cx.editor); - view.ensure_cursor_in_view(doc, line); + Ok(()) } @@ -1789,30 +1840,26 @@ fn reflow( } let scrolloff = cx.editor.config().scrolloff; + let cfg_text_width: usize = cx.editor.config().text_width; let (view, doc) = current!(cx.editor); - const DEFAULT_MAX_LEN: usize = 79; - - // Find the max line length by checking the following sources in order: + // Find the text_width by checking the following sources in order: // - The passed argument in `args` - // - The configured max_line_len for this language in languages.toml - // - The const default we set above - let max_line_len: usize = args + // - The configured text-width for this language in languages.toml + // - The configured text-width in the config.toml + let text_width: usize = args .get(0) .map(|num| num.parse::()) .transpose()? - .or_else(|| { - doc.language_config() - .and_then(|config| config.max_line_length) - }) - .unwrap_or(DEFAULT_MAX_LEN); + .or_else(|| doc.language_config().and_then(|config| config.text_width)) + .unwrap_or(cfg_text_width); let rope = doc.text(); let selection = doc.selection(view.id); let transaction = Transaction::change_by_selection(rope, selection, |range| { let fragment = range.fragment(rope.slice(..)); - let reflowed_text = helix_core::wrap::reflow_hard_wrap(&fragment, max_line_len); + let reflowed_text = helix_core::wrap::reflow_hard_wrap(&fragment, text_width); (range.from(), range.to(), Some(reflowed_text)) }); @@ -2002,6 +2049,64 @@ fn run_shell_command( Ok(()) } +fn reset_diff_change( + cx: &mut compositor::Context, + args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + ensure!(args.is_empty(), ":reset-diff-change takes no arguments"); + + let editor = &mut cx.editor; + let scrolloff = editor.config().scrolloff; + + let (view, doc) = current!(editor); + // TODO refactor to use let..else once MSRV is raised to 1.65 + let handle = match doc.diff_handle() { + Some(handle) => handle, + None => bail!("Diff is not available in the current buffer"), + }; + + let diff = handle.load(); + let doc_text = doc.text().slice(..); + let line = doc.selection(view.id).primary().cursor_line(doc_text); + + // TODO refactor to use let..else once MSRV is raised to 1.65 + let hunk_idx = match diff.hunk_at(line as u32, true) { + Some(hunk_idx) => hunk_idx, + None => bail!("There is no change at the cursor"), + }; + let hunk = diff.nth_hunk(hunk_idx); + let diff_base = diff.diff_base(); + let before_start = diff_base.line_to_char(hunk.before.start as usize); + let before_end = diff_base.line_to_char(hunk.before.end as usize); + let text: Tendril = diff + .diff_base() + .slice(before_start..before_end) + .chunks() + .collect(); + let anchor = doc_text.line_to_char(hunk.after.start as usize); + let transaction = Transaction::change( + doc.text(), + [( + anchor, + doc_text.line_to_char(hunk.after.end as usize), + (!text.is_empty()).then_some(text), + )] + .into_iter(), + ); + drop(diff); // make borrow check happy + doc.apply(&transaction, view.id); + // select inserted text + let text_len = before_end - before_start; + doc.set_selection(view.id, Selection::single(anchor, anchor + text_len)); + doc.append_changes_to_history(view); + view.ensure_cursor_in_view(doc, scrolloff); + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2349,6 +2454,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: lsp_restart, completer: None, }, + TypableCommand { + name: "lsp-stop", + aliases: &[], + doc: "Stops the Language Server that is in use by the current doc", + fun: lsp_stop, + completer: None, + }, TypableCommand { name: "tree-sitter-scopes", aliases: &[], @@ -2531,6 +2643,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: run_shell_command, completer: Some(completers::filename), }, + TypableCommand { + name: "reset-diff-change", + aliases: &["diffget", "diffg"], + doc: "Reset the diff change at the cursor position.", + fun: reset_diff_change, + completer: None, + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = diff --git a/helix-term/src/health.rs b/helix-term/src/health.rs index 6558fe19fb4c..480c2c67579b 100644 --- a/helix-term/src/health.rs +++ b/helix-term/src/health.rs @@ -52,7 +52,7 @@ pub fn general() -> std::io::Result<()> { let config_file = helix_loader::config_file(); let lang_file = helix_loader::lang_config_file(); let log_file = helix_loader::log_file(); - let rt_dir = helix_loader::runtime_dir(); + let rt_dirs = helix_loader::runtime_dirs(); let clipboard_provider = get_clipboard_provider(); if config_file.exists() { @@ -66,17 +66,31 @@ pub fn general() -> std::io::Result<()> { writeln!(stdout, "Language file: default")?; } writeln!(stdout, "Log file: {}", log_file.display())?; - writeln!(stdout, "Runtime directory: {}", rt_dir.display())?; - - if let Ok(path) = std::fs::read_link(&rt_dir) { - let msg = format!("Runtime directory is symlinked to {}", path.display()); - writeln!(stdout, "{}", msg.yellow())?; - } - if !rt_dir.exists() { - writeln!(stdout, "{}", "Runtime directory does not exist.".red())?; - } - if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { - writeln!(stdout, "{}", "Runtime directory is empty.".red())?; + writeln!( + stdout, + "Runtime directories: {}", + rt_dirs + .iter() + .map(|d| d.to_string_lossy()) + .collect::>() + .join(";") + )?; + for rt_dir in rt_dirs.iter() { + if let Ok(path) = std::fs::read_link(rt_dir) { + let msg = format!( + "Runtime directory {} is symlinked to: {}", + rt_dir.display(), + path.display() + ); + writeln!(stdout, "{}", msg.yellow())?; + } + if !rt_dir.exists() { + let msg = format!("Runtime directory does not exist: {}", rt_dir.display()); + writeln!(stdout, "{}", msg.yellow())?; + } else if rt_dir.read_dir().ok().map(|it| it.count()) == Some(0) { + let msg = format!("Runtime directory is empty: {}", rt_dir.display()); + writeln!(stdout, "{}", msg.yellow())?; + } } writeln!(stdout, "Clipboard provider: {}", clipboard_provider.name())?; diff --git a/helix-term/src/keymap/default.rs b/helix-term/src/keymap/default.rs index 01184f80edcd..9bd002809d61 100644 --- a/helix-term/src/keymap/default.rs +++ b/helix-term/src/keymap/default.rs @@ -223,6 +223,7 @@ pub fn default() -> HashMap { "'" => last_picker, "g" => { "Debug (experimental)" sticky=true "l" => dap_launch, + "r" => dap_restart, "b" => dap_toggle_breakpoint, "c" => dap_continue, "h" => dap_pause, @@ -363,7 +364,7 @@ pub fn default() -> HashMap { "A-d" | "A-del" => delete_word_forward, "C-u" => kill_to_line_start, "C-k" => kill_to_line_end, - "C-h" | "backspace" => delete_char_backward, + "C-h" | "backspace" | "S-backspace" => delete_char_backward, "C-d" | "del" => delete_char_forward, "C-j" | "ret" => insert_newline, "tab" => insert_tab, diff --git a/helix-term/src/ui/completion.rs b/helix-term/src/ui/completion.rs index a24da20a9fac..ef88938fe33a 100644 --- a/helix-term/src/ui/completion.rs +++ b/helix-term/src/ui/completion.rs @@ -1,12 +1,13 @@ use crate::compositor::{Component, Context, Event, EventResult}; use helix_view::{ + document::SavePoint, editor::CompleteAction, theme::{Modifier, Style}, ViewId, }; use tui::{buffer::Buffer as Surface, text::Span}; -use std::borrow::Cow; +use std::{borrow::Cow, sync::Arc}; use helix_core::{Change, Transaction}; use helix_view::{graphics::Rect, Document, Editor}; @@ -101,6 +102,7 @@ impl Completion { pub fn new( editor: &Editor, + savepoint: Arc, mut items: Vec, offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, @@ -118,8 +120,12 @@ impl Completion { offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, trigger_offset: usize, + include_placeholder: bool, ) -> Transaction { - let transaction = if let Some(edit) = &item.text_edit { + use helix_lsp::snippet; + let selection = doc.selection(view_id); + + let (start_offset, end_offset, new_text) = if let Some(edit) = &item.text_edit { let edit = match edit { lsp::CompletionTextEdit::Edit(edit) => edit.clone(), lsp::CompletionTextEdit::InsertAndReplace(item) => { @@ -127,19 +133,27 @@ impl Completion { lsp::TextEdit::new(item.replace, item.new_text.clone()) } }; - - util::generate_transaction_from_completion_edit( - doc.text(), - doc.selection(view_id), - edit, - offset_encoding, // TODO: should probably transcode in Client - ) + let text = doc.text().slice(..); + let primary_cursor = selection.primary().cursor(text); + + let start_offset = + match util::lsp_pos_to_pos(doc.text(), edit.range.start, offset_encoding) { + Some(start) => start as i128 - primary_cursor as i128, + None => return Transaction::new(doc.text()), + }; + let end_offset = + match util::lsp_pos_to_pos(doc.text(), edit.range.end, offset_encoding) { + Some(end) => end as i128 - primary_cursor as i128, + None => return Transaction::new(doc.text()), + }; + + (start_offset, end_offset, edit.new_text) } else { - let text = item.insert_text.as_ref().unwrap_or(&item.label); + let new_text = item.insert_text.as_ref().unwrap_or(&item.label); // Some LSPs just give you an insertText with no offset ¯\_(ツ)_/¯ // in these cases we need to check for a common prefix and remove it let prefix = Cow::from(doc.text().slice(start_offset..trigger_offset)); - let text = text.trim_start_matches::<&str>(&prefix); + let new_text = new_text.trim_start_matches::<&str>(&prefix); // TODO: this needs to be true for the numbers to work out correctly // in the closure below. It's passed in to a callback as this same @@ -152,14 +166,43 @@ impl Completion { == trigger_offset ); - Transaction::change_by_selection(doc.text(), doc.selection(view_id), |range| { - let cursor = range.cursor(doc.text().slice(..)); - - (cursor, cursor, Some(text.into())) - }) + (0, 0, new_text.into()) }; - transaction + if matches!(item.kind, Some(lsp::CompletionItemKind::SNIPPET)) + || matches!( + item.insert_text_format, + Some(lsp::InsertTextFormat::SNIPPET) + ) + { + match snippet::parse(&new_text) { + Ok(snippet) => util::generate_transaction_from_snippet( + doc.text(), + selection, + start_offset, + end_offset, + snippet, + doc.line_ending.as_str(), + include_placeholder, + ), + Err(err) => { + log::error!( + "Failed to parse snippet: {:?}, remaining output: {}", + &new_text, + err + ); + Transaction::new(doc.text()) + } + } + } else { + util::generate_transaction_from_completion_edit( + doc.text(), + selection, + start_offset, + end_offset, + new_text, + ) + } } fn completion_changes(transaction: &Transaction, trigger_offset: usize) -> Vec { @@ -172,11 +215,10 @@ impl Completion { let (view, doc) = current!(editor); // if more text was entered, remove it - doc.restore(view); + doc.restore(view, &savepoint); match event { PromptEvent::Abort => { - doc.restore(view); editor.last_completion = None; } PromptEvent::Update => { @@ -190,10 +232,10 @@ impl Completion { offset_encoding, start_offset, trigger_offset, + true, ); // initialize a savepoint - doc.savepoint(); doc.apply(&transaction, view.id); editor.last_completion = Some(CompleteAction { @@ -212,6 +254,7 @@ impl Completion { offset_encoding, start_offset, trigger_offset, + false, ); doc.apply(&transaction, view.id); diff --git a/helix-term/src/ui/document.rs b/helix-term/src/ui/document.rs index ed4b1de90e91..28a52f74d3f5 100644 --- a/helix-term/src/ui/document.rs +++ b/helix-term/src/ui/document.rs @@ -202,10 +202,7 @@ pub fn render_text<'t>( // formattter.line_pos returns to line index of the next grapheme // so it must be called before formatter.next let doc_line = formatter.line_pos(); - // TODO refactor with let .. else once MSRV reaches 1.65 - let (grapheme, mut pos) = if let Some(it) = formatter.next() { - it - } else { + let Some((grapheme, mut pos)) = formatter.next() else { let mut last_pos = formatter.visual_pos(); if last_pos.row >= row_off { last_pos.col -= 1; @@ -226,7 +223,6 @@ pub fn render_text<'t>( // skip any graphemes on visual lines before the block start if pos.row < row_off { if char_pos >= style_span.1 { - // TODO refactor using let..else once MSRV reaches 1.65 style_span = if let Some(style_span) = styles.next() { style_span } else { @@ -266,12 +262,7 @@ pub fn render_text<'t>( // aquire the correct grapheme style if char_pos >= style_span.1 { - // TODO refactor using let..else once MSRV reaches 1.65 - style_span = if let Some(style_span) = styles.next() { - style_span - } else { - (Style::default(), usize::MAX) - } + style_span = styles.next().unwrap_or((Style::default(), usize::MAX)); } char_pos += grapheme.doc_chars(); @@ -322,7 +313,7 @@ pub struct TextRenderer<'a> { pub nbsp: String, pub space: String, pub tab: String, - pub tab_width: u16, + pub indent_width: u16, pub starting_indent: usize, pub draw_indent_guides: bool, pub col_offset: usize, @@ -370,16 +361,19 @@ impl<'a> TextRenderer<'a> { let text_style = theme.get("ui.text"); + let indent_width = doc.indent_style.indent_width(tab_width) as u16; + TextRenderer { surface, indent_guide_char: editor_config.indent_guides.character.into(), newline, nbsp, space, - tab_width: tab_width as u16, tab, whitespace_style: theme.get("ui.virtual.whitespace"), - starting_indent: (col_offset / tab_width) + indent_width, + starting_indent: col_offset / indent_width as usize + + (col_offset % indent_width as usize != 0) as usize + editor_config.indent_guides.skip_levels as usize, indent_guide_style: text_style.patch( theme @@ -461,14 +455,14 @@ impl<'a> TextRenderer<'a> { // Don't draw indent guides outside of view let end_indent = min( indent_level, - // Add tab_width - 1 to round up, since the first visible + // Add indent_width - 1 to round up, since the first visible // indent might be a bit after offset.col - self.col_offset + self.viewport.width as usize + (self.tab_width - 1) as usize, - ) / self.tab_width as usize; + self.col_offset + self.viewport.width as usize + (self.indent_width as usize - 1), + ) / self.indent_width as usize; for i in self.starting_indent..end_indent { - let x = - (self.viewport.x as usize + (i * self.tab_width as usize) - self.col_offset) as u16; + let x = (self.viewport.x as usize + (i * self.indent_width as usize) - self.col_offset) + as u16; let y = self.viewport.y + row; debug_assert!(self.surface.in_bounds(x, y)); self.surface diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index f80173e737ae..1cf1d12e191e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -21,14 +21,14 @@ use helix_core::{ visual_offset_from_block, Position, Range, Selection, Transaction, }; use helix_view::{ - document::{Mode, SCRATCH_BUFFER_NAME}, + document::{Mode, SavePoint, SCRATCH_BUFFER_NAME}, editor::{CompleteAction, CursorShapeConfig}, graphics::{Color, CursorKind, Modifier, Rect, Style}, input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind}, keyboard::{KeyCode, KeyModifiers}, Document, Editor, Theme, View, }; -use std::{num::NonZeroUsize, path::PathBuf, rc::Rc}; +use std::{mem::take, num::NonZeroUsize, path::PathBuf, rc::Rc, sync::Arc}; use tui::buffer::Buffer as Surface; @@ -47,7 +47,7 @@ pub struct EditorView { pub keymaps: Keymaps, on_next_key: Option, pseudo_pending: Vec, - last_insert: (commands::MappableCommand, Vec), + pub(crate) last_insert: (commands::MappableCommand, Vec), pub(crate) completion: Option, spinners: ProgressSpinners, sticky_nodes: Option>, @@ -58,6 +58,7 @@ pub enum InsertEvent { Key(KeyEvent), CompletionApply(CompleteAction), TriggerCompletion, + RequestCompletion, } impl Default for EditorView { @@ -1051,6 +1052,7 @@ impl EditorView { (Mode::Insert, Mode::Normal) => { // if exiting insert mode, remove completion self.completion = None; + cxt.editor.completion_request_handle = None; // TODO: Use an on_mode_change hook to remove signature help cxt.jobs.callback(async { @@ -1121,6 +1123,8 @@ impl EditorView { 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); + let mut last_savepoint = None; + let mut last_request_savepoint = None; // then replay the inputs for key in self.last_insert.1.clone() { match key { @@ -1128,7 +1132,9 @@ impl EditorView { InsertEvent::CompletionApply(compl) => { let (view, doc) = current!(cxt.editor); - doc.restore(view); + if let Some(last_savepoint) = last_savepoint.as_deref() { + doc.restore(view, last_savepoint); + } let text = doc.text().slice(..); let cursor = doc.selection(view.id).primary().cursor(text); @@ -1145,8 +1151,11 @@ impl EditorView { doc.apply(&tx, view.id); } InsertEvent::TriggerCompletion => { - let (_, doc) = current!(cxt.editor); - doc.savepoint(); + last_savepoint = take(&mut last_request_savepoint); + } + InsertEvent::RequestCompletion => { + let (view, doc) = current!(cxt.editor); + last_request_savepoint = Some(doc.savepoint(view)); } } } @@ -1171,26 +1180,31 @@ impl EditorView { } } + #[allow(clippy::too_many_arguments)] pub fn set_completion( &mut self, editor: &mut Editor, + savepoint: Arc, items: Vec, offset_encoding: helix_lsp::OffsetEncoding, start_offset: usize, trigger_offset: usize, size: Rect, ) { - let mut completion = - Completion::new(editor, items, offset_encoding, start_offset, trigger_offset); + let mut completion = Completion::new( + editor, + savepoint, + items, + offset_encoding, + start_offset, + trigger_offset, + ); if completion.is_empty() { // skip if we got no completion results return; } - // Immediately initialize a savepoint - doc_mut!(editor).savepoint(); - editor.last_completion = None; self.last_insert.1.push(InsertEvent::TriggerCompletion); @@ -1203,8 +1217,6 @@ impl EditorView { self.completion = None; // Clear any savepoints - let doc = doc_mut!(editor); - doc.savepoint = None; editor.clear_idle_timer(); // don't retrigger } diff --git a/helix-term/src/ui/markdown.rs b/helix-term/src/ui/markdown.rs index 923dd73a16ab..87136992c43b 100644 --- a/helix-term/src/ui/markdown.rs +++ b/helix-term/src/ui/markdown.rs @@ -342,13 +342,10 @@ impl Component for Markdown { fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> { let padding = 2; - if padding >= viewport.1 || padding >= viewport.0 { - return None; - } let contents = self.parse(None); // TODO: account for tab width - let max_text_width = (viewport.0 - padding).min(120); + let max_text_width = (viewport.0.saturating_sub(padding)).min(120); let (width, height) = crate::ui::text::required_size(&contents, max_text_width); Some((width + padding, height + padding)) diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index d7717f8cf59c..3e9a14b06307 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -280,10 +280,10 @@ pub mod completers { } pub fn theme(_editor: &Editor, input: &str) -> Vec { - let mut names = theme::Loader::read_names(&helix_loader::runtime_dir().join("themes")); - names.extend(theme::Loader::read_names( - &helix_loader::config_dir().join("themes"), - )); + let mut names = theme::Loader::read_names(&helix_loader::config_dir().join("themes")); + for rt_dir in helix_loader::runtime_dirs() { + names.extend(theme::Loader::read_names(&rt_dir.join("themes"))); + } names.push("default".into()); names.push("base16_default".into()); names.sort(); diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index 803e2d65bb78..ec8b1c7f4ae6 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -435,26 +435,6 @@ impl Picker { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, ); - let n = options - .first() - .map(|option| option.format(&editor_data).cells.len()) - .unwrap_or_default(); - let max_lens = options.iter().fold(vec![0; n], |mut acc, option| { - let row = option.format(&editor_data); - // maintain max for each column - for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { - let width = cell.content.width(); - if width > *acc { - *acc = width; - } - } - acc - }); - let widths = max_lens - .into_iter() - .map(|len| Constraint::Length(len as u16)) - .collect(); - let mut picker = Self { options, editor_data, @@ -467,10 +447,12 @@ impl Picker { show_preview: true, callback_fn: Box::new(callback_fn), completion_height: 0, - widths, + widths: Vec::new(), }; - // scoring on empty input: + picker.calculate_column_widths(); + + // scoring on empty input // TODO: just reuse score() picker .matches @@ -486,6 +468,38 @@ impl Picker { picker } + pub fn set_options(&mut self, new_options: Vec) { + self.options = new_options; + self.cursor = 0; + self.force_score(); + self.calculate_column_widths(); + } + + /// Calculate the width constraints using the maximum widths of each column + /// for the current options. + fn calculate_column_widths(&mut self) { + let n = self + .options + .first() + .map(|option| option.format(&self.editor_data).cells.len()) + .unwrap_or_default(); + let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| { + let row = option.format(&self.editor_data); + // maintain max for each column + for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) { + let width = cell.content.width(); + if width > *acc { + *acc = width; + } + } + acc + }); + self.widths = max_lens + .into_iter() + .map(|len| Constraint::Length(len as u16)) + .collect(); + } + pub fn score(&mut self) { let pattern = self.prompt.line(); @@ -931,9 +945,7 @@ impl Component for DynamicPicker { Some(overlay) => &mut overlay.content.file_picker.picker, None => return, }; - picker.options = new_options; - picker.cursor = 0; - picker.force_score(); + picker.set_options(new_options); editor.reset_idle_timer(); })); anyhow::Ok(callback) diff --git a/helix-term/src/ui/prompt.rs b/helix-term/src/ui/prompt.rs index f438231fa9c0..35ae8c2a8e0b 100644 --- a/helix-term/src/ui/prompt.rs +++ b/helix-term/src/ui/prompt.rs @@ -516,7 +516,7 @@ impl Component for Prompt { alt!('d') | alt!(Delete) | ctrl!(Delete) => self.delete_word_forwards(cx.editor), ctrl!('k') => self.kill_to_end_of_line(cx.editor), ctrl!('u') => self.kill_to_start_of_line(cx.editor), - ctrl!('h') | key!(Backspace) => { + ctrl!('h') | key!(Backspace) | shift!(Backspace) => { self.delete_char_backwards(cx.editor); (self.callback_fn)(cx, &self.line, PromptEvent::Update); } diff --git a/helix-term/src/ui/text.rs b/helix-term/src/ui/text.rs index c318052b2dd0..a379536f8b62 100644 --- a/helix-term/src/ui/text.rs +++ b/helix-term/src/ui/text.rs @@ -58,7 +58,7 @@ pub fn required_size(text: &tui::text::Text, max_text_width: u16) -> (u16, u16) let content_width = content.width() as u16; if content_width > max_text_width { text_width = max_text_width; - height += content_width / max_text_width; + height += content_width.checked_div(max_text_width).unwrap_or(0); } else if content_width > text_width { text_width = content_width; } diff --git a/helix-tui/Cargo.toml b/helix-tui/Cargo.toml index a4a1c389f34d..8a6d5367d82e 100644 --- a/helix-tui/Cargo.toml +++ b/helix-tui/Cargo.toml @@ -19,8 +19,10 @@ default = ["crossterm"] bitflags = "1.3" cassowary = "0.3" unicode-segmentation = "1.10" -crossterm = { version = "0.25", optional = true } +crossterm = { version = "0.26", optional = true } termini = "0.1" serde = { version = "1", "optional" = true, features = ["derive"]} +once_cell = "1.17" +log = "~0.4" helix-view = { version = "0.6", path = "../helix-view", features = ["term"] } helix-core = { version = "0.6", path = "../helix-core" } diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index c00e1f406e7c..fba1f029c244 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -1,6 +1,11 @@ -use crate::{backend::Backend, buffer::Cell}; +use crate::{backend::Backend, buffer::Cell, terminal::Config}; use crossterm::{ - cursor::{CursorShape, Hide, MoveTo, SetCursorShape, Show}, + cursor::{Hide, MoveTo, SetCursorStyle, Show}, + event::{ + DisableBracketedPaste, DisableFocusChange, DisableMouseCapture, EnableBracketedPaste, + EnableFocusChange, EnableMouseCapture, KeyboardEnhancementFlags, + PopKeyboardEnhancementFlags, PushKeyboardEnhancementFlags, + }, execute, queue, style::{ Attribute as CAttribute, Color as CColor, Print, SetAttribute, SetBackgroundColor, @@ -10,6 +15,7 @@ use crossterm::{ Command, }; use helix_view::graphics::{Color, CursorKind, Modifier, Rect, UnderlineStyle}; +use once_cell::sync::OnceCell; use std::{ fmt, io::{self, Write}, @@ -52,6 +58,7 @@ impl Capabilities { pub struct CrosstermBackend { buffer: W, capabilities: Capabilities, + supports_keyboard_enhancement_protocol: OnceCell, } impl CrosstermBackend @@ -62,8 +69,27 @@ where CrosstermBackend { buffer, capabilities: Capabilities::from_env_or_default(), + supports_keyboard_enhancement_protocol: OnceCell::new(), } } + + #[inline] + fn supports_keyboard_enhancement_protocol(&self) -> io::Result { + self.supports_keyboard_enhancement_protocol + .get_or_try_init(|| { + use std::time::Instant; + + let now = Instant::now(); + let support = terminal::supports_keyboard_enhancement(); + log::debug!( + "The keyboard enhancement protocol is {}supported in this terminal (checked in {:?})", + if matches!(support, Ok(true)) { "" } else { "not " }, + Instant::now().duration_since(now) + ); + support + }) + .copied() + } } impl Write for CrosstermBackend @@ -83,6 +109,66 @@ impl Backend for CrosstermBackend where W: Write, { + fn claim(&mut self, config: Config) -> io::Result<()> { + terminal::enable_raw_mode()?; + execute!( + self.buffer, + terminal::EnterAlternateScreen, + EnableBracketedPaste, + EnableFocusChange + )?; + execute!(self.buffer, terminal::Clear(terminal::ClearType::All))?; + if config.enable_mouse_capture { + execute!(self.buffer, EnableMouseCapture)?; + } + if self.supports_keyboard_enhancement_protocol()? { + execute!( + self.buffer, + PushKeyboardEnhancementFlags( + KeyboardEnhancementFlags::DISAMBIGUATE_ESCAPE_CODES + | KeyboardEnhancementFlags::REPORT_ALTERNATE_KEYS + ) + )?; + } + Ok(()) + } + + fn restore(&mut self, config: Config) -> io::Result<()> { + // reset cursor shape + write!(self.buffer, "\x1B[0 q")?; + if config.enable_mouse_capture { + execute!(self.buffer, DisableMouseCapture)?; + } + if self.supports_keyboard_enhancement_protocol()? { + execute!(self.buffer, PopKeyboardEnhancementFlags)?; + } + execute!( + self.buffer, + DisableBracketedPaste, + DisableFocusChange, + terminal::LeaveAlternateScreen + )?; + terminal::disable_raw_mode() + } + + fn force_restore() -> io::Result<()> { + let mut stdout = io::stdout(); + + // reset cursor shape + write!(stdout, "\x1B[0 q")?; + // Ignore errors on disabling, this might trigger on windows if we call + // disable without calling enable previously + let _ = execute!(stdout, DisableMouseCapture); + let _ = execute!(stdout, PopKeyboardEnhancementFlags); + execute!( + stdout, + DisableBracketedPaste, + DisableFocusChange, + terminal::LeaveAlternateScreen + )?; + terminal::disable_raw_mode() + } + fn draw<'a, I>(&mut self, content: I) -> io::Result<()> where I: Iterator, @@ -156,12 +242,12 @@ where fn show_cursor(&mut self, kind: CursorKind) -> io::Result<()> { let shape = match kind { - CursorKind::Block => CursorShape::Block, - CursorKind::Bar => CursorShape::Line, - CursorKind::Underline => CursorShape::UnderScore, + CursorKind::Block => SetCursorStyle::SteadyBlock, + CursorKind::Bar => SetCursorStyle::SteadyBar, + CursorKind::Underline => SetCursorStyle::SteadyUnderScore, CursorKind::Hidden => unreachable!(), }; - map_error(execute!(self.buffer, Show, SetCursorShape(shape))) + map_error(execute!(self.buffer, Show, shape)) } fn get_cursor(&mut self) -> io::Result<(u16, u16)> { diff --git a/helix-tui/src/backend/mod.rs b/helix-tui/src/backend/mod.rs index c6c11019de8c..6d7c3894258b 100644 --- a/helix-tui/src/backend/mod.rs +++ b/helix-tui/src/backend/mod.rs @@ -1,6 +1,6 @@ use std::io; -use crate::buffer::Cell; +use crate::{buffer::Cell, terminal::Config}; use helix_view::graphics::{CursorKind, Rect}; @@ -13,6 +13,9 @@ mod test; pub use self::test::TestBackend; pub trait Backend { + fn claim(&mut self, config: Config) -> Result<(), io::Error>; + fn restore(&mut self, config: Config) -> Result<(), io::Error>; + fn force_restore() -> Result<(), io::Error>; fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error> where I: Iterator; diff --git a/helix-tui/src/backend/test.rs b/helix-tui/src/backend/test.rs index 52474148e7bc..ff133ff3e846 100644 --- a/helix-tui/src/backend/test.rs +++ b/helix-tui/src/backend/test.rs @@ -1,6 +1,7 @@ use crate::{ backend::Backend, buffer::{Buffer, Cell}, + terminal::Config, }; use helix_core::unicode::width::UnicodeWidthStr; use helix_view::graphics::{CursorKind, Rect}; @@ -106,6 +107,18 @@ impl TestBackend { } impl Backend for TestBackend { + fn claim(&mut self, _config: Config) -> Result<(), io::Error> { + Ok(()) + } + + fn restore(&mut self, _config: Config) -> Result<(), io::Error> { + Ok(()) + } + + fn force_restore() -> Result<(), io::Error> { + Ok(()) + } + fn draw<'a, I>(&mut self, content: I) -> Result<(), io::Error> where I: Iterator, diff --git a/helix-tui/src/terminal.rs b/helix-tui/src/terminal.rs index 22e9232f3f86..802a8c1d9f53 100644 --- a/helix-tui/src/terminal.rs +++ b/helix-tui/src/terminal.rs @@ -1,4 +1,5 @@ use crate::{backend::Backend, buffer::Buffer}; +use helix_view::editor::Config as EditorConfig; use helix_view::graphics::{CursorKind, Rect}; use std::io; @@ -16,6 +17,19 @@ pub struct Viewport { resize_behavior: ResizeBehavior, } +#[derive(Debug)] +pub struct Config { + pub enable_mouse_capture: bool, +} + +impl From for Config { + fn from(config: EditorConfig) -> Self { + Self { + enable_mouse_capture: config.mouse, + } + } +} + impl Viewport { /// UNSTABLE pub fn fixed(area: Rect) -> Viewport { @@ -98,6 +112,14 @@ where }) } + pub fn claim(&mut self, config: Config) -> io::Result<()> { + self.backend.claim(config) + } + + pub fn restore(&mut self, config: Config) -> io::Result<()> { + self.backend.restore(config) + } + // /// Get a Frame object which provides a consistent view into the terminal state for rendering. // pub fn get_frame(&mut self) -> Frame { // Frame { diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index 19b660a60f54..789ee79519a0 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -16,13 +16,13 @@ helix-core = { version = "0.6", path = "../helix-core" } tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } parking_lot = "0.12" -git-repository = { version = "0.32", default-features = false , optional = true } +gix = { version = "0.39.0", default-features = false , optional = true } imara-diff = "0.1.5" log = "0.4" [features] -git = ["git-repository"] +git = ["gix"] [dev-dependencies] -tempfile = "3.3" \ No newline at end of file +tempfile = "3.4" \ No newline at end of file diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index 9c6a362f7db8..ca33dda44f29 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -28,11 +28,18 @@ struct Event { render_lock: Option, } +#[derive(Clone, Debug, Default)] +struct DiffInner { + diff_base: Rope, + doc: Rope, + hunks: Vec, +} + #[derive(Clone, Debug)] pub struct DiffHandle { channel: UnboundedSender, render_lock: Arc>, - hunks: Arc>>, + diff: Arc>, inverted: bool, } @@ -47,10 +54,10 @@ impl DiffHandle { redraw_handle: RedrawHandle, ) -> (DiffHandle, JoinHandle<()>) { let (sender, receiver) = unbounded_channel(); - let hunks: Arc>> = Arc::default(); + let diff: Arc> = Arc::default(); let worker = DiffWorker { channel: receiver, - hunks: hunks.clone(), + diff: diff.clone(), new_hunks: Vec::default(), redraw_notify: redraw_handle.0, diff_finished_notify: Arc::default(), @@ -58,7 +65,7 @@ impl DiffHandle { let handle = tokio::spawn(worker.run(diff_base, doc)); let differ = DiffHandle { channel: sender, - hunks, + diff, inverted: false, render_lock: redraw_handle.1, }; @@ -69,9 +76,9 @@ impl DiffHandle { self.inverted = !self.inverted; } - pub fn hunks(&self) -> FileHunks { - FileHunks { - hunks: self.hunks.lock(), + pub fn load(&self) -> Diff { + Diff { + diff: self.diff.lock(), inverted: self.inverted, } } @@ -168,12 +175,28 @@ impl Hunk { /// A list of changes in a file sorted in ascending /// non-overlapping order #[derive(Debug)] -pub struct FileHunks<'a> { - hunks: MutexGuard<'a, Vec>, +pub struct Diff<'a> { + diff: MutexGuard<'a, DiffInner>, inverted: bool, } -impl FileHunks<'_> { +impl Diff<'_> { + pub fn diff_base(&self) -> &Rope { + if self.inverted { + &self.diff.doc + } else { + &self.diff.diff_base + } + } + + pub fn doc(&self) -> &Rope { + if self.inverted { + &self.diff.diff_base + } else { + &self.diff.doc + } + } + pub fn is_inverted(&self) -> bool { self.inverted } @@ -181,7 +204,7 @@ impl FileHunks<'_> { /// Returns the `Hunk` for the `n`th change in this file. /// if there is no `n`th change `Hunk::NONE` is returned instead. pub fn nth_hunk(&self, n: u32) -> Hunk { - match self.hunks.get(n as usize) { + match self.diff.hunks.get(n as usize) { Some(hunk) if self.inverted => hunk.invert(), Some(hunk) => hunk.clone(), None => Hunk::NONE, @@ -189,7 +212,7 @@ impl FileHunks<'_> { } pub fn len(&self) -> u32 { - self.hunks.len() as u32 + self.diff.hunks.len() as u32 } pub fn is_empty(&self) -> bool { @@ -204,19 +227,20 @@ impl FileHunks<'_> { }; let res = self + .diff .hunks .binary_search_by_key(&line, |hunk| hunk_range(hunk).start); match res { // Search found a hunk that starts exactly at this line, return the next hunk if it exists. - Ok(pos) if pos + 1 == self.hunks.len() => None, + Ok(pos) if pos + 1 == self.diff.hunks.len() => None, Ok(pos) => Some(pos as u32 + 1), // No hunk starts exactly at this line, so the search returns // the position where a hunk starting at this line should be inserted. // That position is exactly the position of the next hunk or the end // of the list if no such hunk exists - Err(pos) if pos == self.hunks.len() => None, + Err(pos) if pos == self.diff.hunks.len() => None, Err(pos) => Some(pos as u32), } } @@ -228,6 +252,7 @@ impl FileHunks<'_> { |hunk: &Hunk| hunk.after.clone() }; let res = self + .diff .hunks .binary_search_by_key(&line, |hunk| hunk_range(hunk).end); @@ -237,7 +262,7 @@ impl FileHunks<'_> { // which represents a pure removal. // Removals are technically empty but are still shown as single line hunks // and as such we must jump to the previous hunk (if it exists) if we are already inside the removal - Ok(pos) if !hunk_range(&self.hunks[pos]).is_empty() => Some(pos as u32), + Ok(pos) if !hunk_range(&self.diff.hunks[pos]).is_empty() => Some(pos as u32), // No hunk ends exactly at this line, so the search returns // the position where a hunk ending at this line should be inserted. @@ -255,6 +280,7 @@ impl FileHunks<'_> { }; let res = self + .diff .hunks .binary_search_by_key(&line, |hunk| hunk_range(hunk).start); @@ -267,7 +293,7 @@ impl FileHunks<'_> { // The previous hunk contains this hunk if it exists and doesn't end before this line Err(0) => None, Err(pos) => { - let hunk = hunk_range(&self.hunks[pos - 1]); + let hunk = hunk_range(&self.diff.hunks[pos - 1]); if hunk.end > line || include_removal && hunk.start == line && hunk.is_empty() { Some(pos as u32 - 1) } else { diff --git a/helix-vcs/src/diff/line_cache.rs b/helix-vcs/src/diff/line_cache.rs index c3ee5daa3e79..8e48250f157e 100644 --- a/helix-vcs/src/diff/line_cache.rs +++ b/helix-vcs/src/diff/line_cache.rs @@ -43,6 +43,14 @@ impl InternedRopeLines { res } + pub fn doc(&self) -> Rope { + self.doc.clone() + } + + pub fn diff_base(&self) -> Rope { + self.diff_base.clone() + } + /// Updates the `diff_base` and optionally the document if `doc` is not None pub fn update_diff_base(&mut self, diff_base: Rope, doc: Option) { self.interned.clear(); diff --git a/helix-vcs/src/diff/worker.rs b/helix-vcs/src/diff/worker.rs index f4bb4dbfb07c..5406446fd745 100644 --- a/helix-vcs/src/diff/worker.rs +++ b/helix-vcs/src/diff/worker.rs @@ -10,7 +10,7 @@ use tokio::sync::Notify; use tokio::time::{timeout, timeout_at, Duration}; use crate::diff::{ - Event, RenderLock, ALGORITHM, DIFF_DEBOUNCE_TIME_ASYNC, DIFF_DEBOUNCE_TIME_SYNC, + DiffInner, Event, RenderLock, ALGORITHM, DIFF_DEBOUNCE_TIME_ASYNC, DIFF_DEBOUNCE_TIME_SYNC, }; use super::line_cache::InternedRopeLines; @@ -21,7 +21,7 @@ mod test; pub(super) struct DiffWorker { pub channel: UnboundedReceiver, - pub hunks: Arc>>, + pub diff: Arc>, pub new_hunks: Vec, pub redraw_notify: Arc, pub diff_finished_notify: Arc, @@ -46,7 +46,7 @@ impl DiffWorker { if let Some(lines) = interner.interned_lines() { self.perform_diff(lines); } - self.apply_hunks(); + self.apply_hunks(interner.diff_base(), interner.doc()); while let Some(event) = self.channel.recv().await { let (doc, diff_base) = self.accumulate_events(event).await; @@ -70,15 +70,18 @@ impl DiffWorker { #[cfg(not(test))] tokio::task::block_in_place(process_accumulated_events); - self.apply_hunks(); + self.apply_hunks(interner.diff_base(), interner.doc()); } } /// update the hunks (used by the gutter) by replacing it with `self.new_hunks`. /// `self.new_hunks` is always empty after this function runs. /// To improve performance this function tries to reuse the allocation of the old diff previously stored in `self.line_diffs` - fn apply_hunks(&mut self) { - swap(&mut *self.hunks.lock(), &mut self.new_hunks); + fn apply_hunks(&mut self, diff_base: Rope, doc: Rope) { + let mut diff = self.diff.lock(); + diff.diff_base = diff_base; + diff.doc = doc; + swap(&mut diff.hunks, &mut self.new_hunks); self.diff_finished_notify.notify_waiters(); self.new_hunks.clear(); } diff --git a/helix-vcs/src/diff/worker/test.rs b/helix-vcs/src/diff/worker/test.rs index 1444242651c8..6a68d987c220 100644 --- a/helix-vcs/src/diff/worker/test.rs +++ b/helix-vcs/src/diff/worker/test.rs @@ -12,12 +12,12 @@ impl DiffHandle { ) } async fn into_diff(self, handle: JoinHandle<()>) -> Vec { - let hunks = self.hunks; + let diff = self.diff; // dropping the channel terminates the task drop(self.channel); handle.await.unwrap(); - let hunks = hunks.lock(); - Vec::clone(&*hunks) + let diff = diff.lock(); + Vec::clone(&diff.hunks) } } diff --git a/helix-vcs/src/git.rs b/helix-vcs/src/git.rs index 432159b6cdb8..2a540c8d12ff 100644 --- a/helix-vcs/src/git.rs +++ b/helix-vcs/src/git.rs @@ -1,9 +1,8 @@ use std::path::Path; -use git::objs::tree::EntryMode; -use git::sec::trust::DefaultForLevel; -use git::{Commit, ObjectId, Repository, ThreadSafeRepository}; -use git_repository as git; +use gix::objs::tree::EntryMode; +use gix::sec::trust::DefaultForLevel; +use gix::{Commit, ObjectId, Repository, ThreadSafeRepository}; use crate::DiffProvider; @@ -15,13 +14,13 @@ pub struct Git; impl Git { fn open_repo(path: &Path, ceiling_dir: Option<&Path>) -> Option { // custom open options - let mut git_open_opts_map = git::sec::trust::Mapping::::default(); + let mut git_open_opts_map = gix::sec::trust::Mapping::::default(); // On windows various configuration options are bundled as part of the installations // This path depends on the install location of git and therefore requires some overhead to lookup // This is basically only used on windows and has some overhead hence it's disabled on other platforms. // `gitoxide` doesn't use this as default - let config = git::permissions::Config { + let config = gix::permissions::Config { system: true, git: true, user: true, @@ -30,16 +29,16 @@ impl Git { git_binary: cfg!(windows), }; // change options for config permissions without touching anything else - git_open_opts_map.reduced = git_open_opts_map.reduced.permissions(git::Permissions { + git_open_opts_map.reduced = git_open_opts_map.reduced.permissions(gix::Permissions { config, - ..git::Permissions::default_for_level(git::sec::Trust::Reduced) + ..gix::Permissions::default_for_level(gix::sec::Trust::Reduced) }); - git_open_opts_map.full = git_open_opts_map.full.permissions(git::Permissions { + git_open_opts_map.full = git_open_opts_map.full.permissions(gix::Permissions { config, - ..git::Permissions::default_for_level(git::sec::Trust::Full) + ..gix::Permissions::default_for_level(gix::sec::Trust::Full) }); - let mut open_options = git::discover::upwards::Options::default(); + let mut open_options = gix::discover::upwards::Options::default(); if let Some(ceiling_dir) = ceiling_dir { open_options.ceiling_dirs = vec![ceiling_dir.to_owned()]; } diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index 17e07e9a2dd6..e3f98a8d3c29 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -20,7 +20,7 @@ helix-core = { version = "0.6", path = "../helix-core" } helix-loader = { version = "0.6", path = "../helix-loader" } helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-dap = { version = "0.6", path = "../helix-dap" } -crossterm = { version = "0.25", optional = true } +crossterm = { version = "0.26", optional = true } helix-vcs = { version = "0.6", path = "../helix-vcs" } # Conversion traits @@ -43,6 +43,7 @@ toml = "0.7" log = "~0.4" which = "4.4" +parking_lot = "0.12.1" [target.'cfg(windows)'.dependencies] diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 579c6725063b..db12fb92bdcf 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -9,6 +9,7 @@ use helix_core::text_annotations::TextAnnotations; use helix_core::Range; use helix_vcs::{DiffHandle, DiffProviderRegistry}; +use ::parking_lot::Mutex; use serde::de::{self, Deserialize, Deserializer}; use serde::Serialize; use std::borrow::Cow; @@ -18,7 +19,7 @@ use std::fmt::Display; use std::future::Future; use std::path::{Path, PathBuf}; use std::str::FromStr; -use std::sync::Arc; +use std::sync::{Arc, Weak}; use std::time::SystemTime; use helix_core::{ @@ -105,6 +106,13 @@ pub struct DocumentSavedEvent { pub type DocumentSavedEventResult = Result; pub type DocumentSavedEventFuture = BoxFuture<'static, DocumentSavedEventResult>; +#[derive(Debug)] +pub struct SavePoint { + /// The view this savepoint is associated with + pub view: ViewId, + revert: Mutex, +} + pub struct Document { pub(crate) id: DocumentId, text: Rope, @@ -136,7 +144,7 @@ pub struct Document { pub history: Cell, pub config: Arc>, - pub savepoint: Option, + savepoints: Vec>, // Last time we wrote to the file. This will carry the time the file was last opened if there // were no saves. @@ -389,7 +397,7 @@ impl Document { diagnostics: Vec::new(), version: 0, history: Cell::new(History::default()), - savepoint: None, + savepoints: Vec::new(), last_saved_time: SystemTime::now(), last_saved_revision: 0, modified_since_accessed: false, @@ -846,11 +854,18 @@ impl Document { } // generate revert to savepoint - if self.savepoint.is_some() { - take_with(&mut self.savepoint, |prev_revert| { - let revert = transaction.invert(&old_doc); - Some(revert.compose(prev_revert.unwrap())) - }); + if !self.savepoints.is_empty() { + let revert = transaction.invert(&old_doc); + self.savepoints + .retain_mut(|save_point| match save_point.upgrade() { + Some(savepoint) => { + let mut revert_to_savepoint = savepoint.revert.lock(); + *revert_to_savepoint = + revert.clone().compose(mem::take(&mut revert_to_savepoint)); + true + } + None => false, + }) } // update tree-sitter syntax tree @@ -940,14 +955,39 @@ impl Document { self.undo_redo_impl(view, false) } - pub fn savepoint(&mut self) { - self.savepoint = Some(Transaction::new(self.text())); + /// Creates a reference counted snapshot (called savpepoint) of the document. + /// + /// The snapshot will remain valid (and updated) idenfinitly as long as ereferences to it exist. + /// Restoring the snapshot will restore the selection and the contents of the document to + /// the state it had when this function was called. + pub fn savepoint(&mut self, view: &View) -> Arc { + let revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone()); + let savepoint = Arc::new(SavePoint { + view: view.id, + revert: Mutex::new(revert), + }); + self.savepoints.push(Arc::downgrade(&savepoint)); + savepoint } - pub fn restore(&mut self, view: &mut View) { - if let Some(revert) = self.savepoint.take() { - self.apply(&revert, view.id); - } + pub fn restore(&mut self, view: &mut View, savepoint: &SavePoint) { + assert_eq!( + savepoint.view, view.id, + "Savepoint must not be used with a different view!" + ); + // search and remove savepoint using a ptr comparison + // this avoids a deadlock as we need to lock the mutex + let savepoint_idx = self + .savepoints + .iter() + .position(|savepoint_ref| savepoint_ref.as_ptr() == savepoint as *const _) + .expect("Savepoint must belong to this document"); + + let savepoint_ref = self.savepoints.remove(savepoint_idx); + let mut revert = savepoint.revert.lock(); + self.apply(&revert, view.id); + *revert = Transaction::new(self.text()).with_selection(self.selection(view.id).clone()); + self.savepoints.push(savepoint_ref) } fn earlier_later_impl(&mut self, view: &mut View, uk: UndoKind, earlier: bool) -> bool { @@ -1238,24 +1278,61 @@ impl Document { } pub fn text_format(&self, mut viewport_width: u16, theme: Option<&Theme>) -> TextFormat { - if let Some(max_line_len) = self + let config = self.config.load(); + let text_width = self .language_config() - .and_then(|config| config.max_line_length) - { - viewport_width = viewport_width.min(max_line_len as u16) + .and_then(|config| config.text_width) + .unwrap_or(config.text_width); + let soft_wrap_at_text_width = self + .language_config() + .and_then(|config| { + config + .soft_wrap + .as_ref() + .and_then(|soft_wrap| soft_wrap.wrap_at_text_width) + }) + .or(config.soft_wrap.wrap_at_text_width) + .unwrap_or(false); + if soft_wrap_at_text_width { + // We increase max_line_len by 1 because softwrap considers the newline character + // as part of the line length while the "typical" expectation is that this is not the case. + // In particular other commands like :reflow do not count the line terminator. + // This is technically inconsistent for the last line as that line never has a line terminator + // but having the last visual line exceed the width by 1 seems like a rare edge case. + viewport_width = viewport_width.min(text_width as u16 + 1) } let config = self.config.load(); - let soft_wrap = &config.soft_wrap; + let editor_soft_wrap = &config.soft_wrap; + let language_soft_wrap = self + .language + .as_ref() + .and_then(|config| config.soft_wrap.as_ref()); + let enable_soft_wrap = language_soft_wrap + .and_then(|soft_wrap| soft_wrap.enable) + .or(editor_soft_wrap.enable) + .unwrap_or(false); + let max_wrap = language_soft_wrap + .and_then(|soft_wrap| soft_wrap.max_wrap) + .or(config.soft_wrap.max_wrap) + .unwrap_or(20); + let max_indent_retain = language_soft_wrap + .and_then(|soft_wrap| soft_wrap.max_indent_retain) + .or(editor_soft_wrap.max_indent_retain) + .unwrap_or(40); + let wrap_indicator = language_soft_wrap + .and_then(|soft_wrap| soft_wrap.wrap_indicator.clone()) + .or_else(|| config.soft_wrap.wrap_indicator.clone()) + .unwrap_or_else(|| "↪ ".into()); let tab_width = self.tab_width() as u16; TextFormat { - soft_wrap: soft_wrap.enable && viewport_width > 10, + soft_wrap: enable_soft_wrap && viewport_width > 10, tab_width, - max_wrap: soft_wrap.max_wrap.min(viewport_width / 4), - max_indent_retain: soft_wrap.max_indent_retain.min(viewport_width * 2 / 5), + max_wrap: max_wrap.min(viewport_width / 4), + max_indent_retain: max_indent_retain.min(viewport_width * 2 / 5), // avoid spinning forever when the window manager // sets the size to something tiny viewport_width, - wrap_indicator: soft_wrap.wrap_indicator.clone().into_boxed_str(), + wrap_indicator: wrap_indicator.into_boxed_str(), wrap_indicator_highlight: theme .and_then(|theme| theme.find_scope_index("ui.virtual.wrap")) .map(Highlight), diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index d25d89d7aed4..31840f688a48 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -31,7 +31,7 @@ use std::{ use tokio::{ sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - Notify, RwLock, + oneshot, Notify, RwLock, }, time::{sleep, Duration, Instant, Sleep}, }; @@ -40,12 +40,12 @@ use anyhow::{anyhow, bail, Error}; pub use helix_core::diagnostic::Severity; pub use helix_core::register::Registers; -use helix_core::Position; use helix_core::{ auto_pairs::AutoPairs, - syntax::{self, AutoPairConfig}, + syntax::{self, AutoPairConfig, SoftWrap}, Change, }; +use helix_core::{Position, Selection}; use helix_dap as dap; use helix_lsp::lsp; @@ -241,6 +241,8 @@ pub struct Config { pub auto_format: bool, /// Automatic save on focus lost. Defaults to false. pub auto_save: bool, + /// Set a global text_width + pub text_width: usize, /// Time in milliseconds since last keypress before idle timers trigger. /// Used for autocompletion, set to 0 for instant. Defaults to 400ms. #[serde( @@ -296,43 +298,6 @@ pub struct StickyContextConfig { pub max_lines: u16, } -#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)] -#[serde(default, rename_all = "kebab-case", deny_unknown_fields)] -pub struct SoftWrap { - /// Soft wrap lines that exceed viewport width. Default to off - pub enable: bool, - /// Maximum space left free at the end of the line. - /// This space is used to wrap text at word boundaries. If that is not possible within this limit - /// the word is simply split at the end of the line. - /// - /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. - /// - /// Default to 20 - pub max_wrap: u16, - /// Maximum number of indentation that can be carried over from the previous line when softwrapping. - /// If a line is indented further then this limit it is rendered at the start of the viewport instead. - /// - /// This is automatically hard-limited to a quarter of the viewport to ensure correct display on small views. - /// - /// Default to 40 - pub max_indent_retain: u16, - /// Indicator placed at the beginning of softwrapped lines - /// - /// Defaults to ↪ - pub wrap_indicator: String, -} - -impl Default for SoftWrap { - fn default() -> Self { - SoftWrap { - enable: false, - max_wrap: 20, - max_indent_retain: 40, - wrap_indicator: "↪ ".into(), - } - } -} - #[derive(Debug, Default, Clone, PartialEq, Eq, Serialize, Deserialize)] #[serde(default, rename_all = "kebab-case", deny_unknown_fields)] pub struct TerminalConfig { @@ -793,6 +758,7 @@ impl Default for Config { color_modes: false, soft_wrap: SoftWrap::default(), sticky_context: StickyContextConfig::default(), + text_width: 80, } } } @@ -869,7 +835,12 @@ pub struct Editor { /// The currently applied editor theme. While previewing a theme, the previewed theme /// is set here. pub theme: Theme, - pub last_line_number: Option, + + /// The primary Selection prior to starting a goto_line_number preview. This is + /// restored when the preview is aborted, or added to the jumplist when it is + /// confirmed. + pub last_selection: Option, + pub status_msg: Option<(Cow<'static, str>, Severity)>, pub autoinfo: Option, @@ -902,6 +873,14 @@ pub struct Editor { /// avoid calculating the cursor position multiple /// times during rendering and should not be set by other functions. pub cursor_cache: Cell>>, + /// When a new completion request is sent to the server old + /// unifinished request must be dropped. Each completion + /// request is associated with a channel that cancels + /// when the channel is dropped. That channel is stored + /// here. When a new completion request is sent this + /// field is set and any old requests are automatically + /// canceled as a result + pub completion_request_handle: Option>, } pub type RedrawHandle = (Arc, Arc>); @@ -985,7 +964,7 @@ impl Editor { syn_loader, theme_loader, last_theme: None, - last_line_number: None, + last_selection: None, registers: Registers::default(), clipboard_provider: get_clipboard_provider(), status_msg: None, @@ -1000,6 +979,7 @@ impl Editor { redraw_handle: Default::default(), needs_redraw: false, cursor_cache: Cell::new(None), + completion_request_handle: None, } } diff --git a/helix-view/src/graphics.rs b/helix-view/src/graphics.rs index a0b645fae5b8..e813fb5604b8 100644 --- a/helix-view/src/graphics.rs +++ b/helix-view/src/graphics.rs @@ -237,8 +237,8 @@ impl Rect { Rect { x: x1, y: y1, - width: x2 - x1, - height: y2 - y1, + width: x2.saturating_sub(x1), + height: y2.saturating_sub(y1), } } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 09c3e6ba9542..9e7e5ca836c3 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -102,7 +102,7 @@ pub fn diff<'doc>( let deleted = theme.get("diff.minus"); let modified = theme.get("diff.delta"); if let Some(diff_handle) = doc.diff_handle() { - let hunks = diff_handle.hunks(); + let hunks = diff_handle.load(); let mut hunk_i = 0; let mut hunk = hunks.nth_hunk(hunk_i); Box::new( diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index ce061babead8..5d79ff26b7dd 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -1,5 +1,5 @@ use std::{ - collections::HashMap, + collections::{HashMap, HashSet}, path::{Path, PathBuf}, str, }; @@ -37,19 +37,21 @@ pub static BASE16_DEFAULT_THEME: Lazy = Lazy::new(|| Theme { #[derive(Clone, Debug)] pub struct Loader { - user_dir: PathBuf, - default_dir: PathBuf, + /// Theme directories to search from highest to lowest priority + theme_dirs: Vec, } impl Loader { - /// Creates a new loader that can load themes from two directories. - pub fn new>(user_dir: P, default_dir: P) -> Self { + /// Creates a new loader that can load themes from multiple directories. + /// + /// The provided directories should be ordered from highest to lowest priority. + /// The directories will have their "themes" subdirectory searched. + pub fn new(dirs: &[PathBuf]) -> Self { Self { - user_dir: user_dir.as_ref().join("themes"), - default_dir: default_dir.as_ref().join("themes"), + theme_dirs: dirs.iter().map(|p| p.join("themes")).collect(), } } - /// Loads a theme first looking in the `user_dir` then in `default_dir` + /// Loads a theme searching directories in priority order. pub fn load(&self, name: &str) -> Result { if name == "default" { return Ok(self.default()); @@ -58,7 +60,8 @@ impl Loader { return Ok(self.base16_default()); } - let theme = self.load_theme(name, name, false).map(Theme::from)?; + let mut visited_paths = HashSet::new(); + let theme = self.load_theme(name, &mut visited_paths).map(Theme::from)?; Ok(Theme { name: name.into(), @@ -66,16 +69,18 @@ impl Loader { }) } - // load the theme and its parent recursively and merge them - // `base_theme_name` is the theme from the config.toml, - // used to prevent some circular loading scenarios - fn load_theme( - &self, - name: &str, - base_theme_name: &str, - only_default_dir: bool, - ) -> Result { - let path = self.path(name, only_default_dir); + /// Recursively load a theme, merging with any inherited parent themes. + /// + /// The paths that have been visited in the inheritance hierarchy are tracked + /// to detect and avoid cycling. + /// + /// It is possible for one file to inherit from another file with the same name + /// so long as the second file is in a themes directory with lower priority. + /// However, it is not recommended that users do this as it will make tracing + /// errors more difficult. + fn load_theme(&self, name: &str, visited_paths: &mut HashSet) -> Result { + let path = self.path(name, visited_paths)?; + let theme_toml = self.load_toml(path)?; let inherits = theme_toml.get("inherits"); @@ -92,11 +97,7 @@ impl Loader { // load default themes's toml from const. "default" => DEFAULT_THEME_DATA.clone(), "base16_default" => BASE16_DEFAULT_THEME_DATA.clone(), - _ => self.load_theme( - parent_theme_name, - base_theme_name, - base_theme_name == parent_theme_name, - )?, + _ => self.load_theme(parent_theme_name, visited_paths)?, }; self.merge_themes(parent_theme_toml, theme_toml) @@ -148,7 +149,7 @@ impl Loader { merge_toml_values(theme, palette.into(), 1) } - // Loads the theme data as `toml::Value` first from the user_dir then in default_dir + // Loads the theme data as `toml::Value` fn load_toml(&self, path: PathBuf) -> Result { let data = std::fs::read_to_string(path)?; let value = toml::from_str(&data)?; @@ -156,25 +157,35 @@ impl Loader { Ok(value) } - // Returns the path to the theme with the name - // With `only_default_dir` as false the path will first search for the user path - // disabled it ignores the user path and returns only the default path - fn path(&self, name: &str, only_default_dir: bool) -> PathBuf { + /// Returns the path to the theme with the given name + /// + /// Ignores paths already visited and follows directory priority order. + fn path(&self, name: &str, visited_paths: &mut HashSet) -> Result { let filename = format!("{}.toml", name); - let user_path = self.user_dir.join(&filename); - if !only_default_dir && user_path.exists() { - user_path - } else { - self.default_dir.join(filename) - } - } - - /// Lists all theme names available in default and user directory - pub fn names(&self) -> Vec { - let mut names = Self::read_names(&self.user_dir); - names.extend(Self::read_names(&self.default_dir)); - names + let mut cycle_found = false; // track if there was a path, but it was in a cycle + self.theme_dirs + .iter() + .find_map(|dir| { + let path = dir.join(&filename); + if !path.exists() { + None + } else if visited_paths.contains(&path) { + // Avoiding cycle, continuing to look in lower priority directories + cycle_found = true; + None + } else { + visited_paths.insert(path.clone()); + Some(path) + } + }) + .ok_or_else(|| { + if cycle_found { + anyhow!("Theme: cycle found in inheriting: {}", name) + } else { + anyhow!("Theme: file not found for: {}", name) + } + }) } pub fn default_theme(&self, true_color: bool) -> Theme { diff --git a/helix-view/src/view.rs b/helix-view/src/view.rs index f793cbe364f5..7bfbb2418379 100644 --- a/helix-view/src/view.rs +++ b/helix-view/src/view.rs @@ -242,7 +242,7 @@ impl View { at_top = true; true } - Some((visual_pos, _)) if visual_pos.row >= vertical_viewport_end - scrolloff => { + Some((visual_pos, _)) if visual_pos.row + scrolloff + 1 >= vertical_viewport_end => { if CENTERING && visual_pos.row >= vertical_viewport_end { // cursor out of view return None; @@ -257,7 +257,7 @@ impl View { let v_off = if at_top { scrolloff as isize } else { - viewport.height as isize - scrolloff as isize + viewport.height as isize - scrolloff as isize - 1 }; (offset.anchor, offset.vertical_offset) = char_idx_at_visual_offset(doc_text, cursor, -v_off, 0, &text_fmt, &annotations); diff --git a/languages.toml b/languages.toml index 78dfd50a92b2..099162341c72 100644 --- a/languages.toml +++ b/languages.toml @@ -53,6 +53,20 @@ args = { attachCommands = [ "platform select remote-gdb-server", "platform conne name = "rust" source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" } +[[language]] +name = "sway" +scope = "source.sway" +injection-regex = "sway" +file-types = ["sw"] +language-server = { command = "forc", args = ["lsp"] } +roots = ["Forc.toml", "Forc.lock"] +indent = { tab-width = 4, unit = " " } +comment-token = "//" + +[[grammar]] +name = "sway" +source = { git = "https://github.com/FuelLabs/tree-sitter-sway", rev = "e491a005ee1d310f4c138bf215afd44cfebf959c" } + [[language]] name = "toml" scope = "source.toml" @@ -311,7 +325,7 @@ args = { mode = "local", processId = "{0}" } [[grammar]] name = "go" -source = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "05900faa3cdb5d2d8c8bd5e77ee698487e0a8611" } +source = { git = "https://github.com/tree-sitter/tree-sitter-go", rev = "64457ea6b73ef5422ed1687178d4545c3e91334a" } [[language]] name = "gomod" @@ -558,7 +572,7 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "php" -source = { git = "https://github.com/tree-sitter/tree-sitter-php", rev = "57f855461aeeca73bd4218754fb26b5ac143f98f" } +source = { git = "https://github.com/tree-sitter/tree-sitter-php", rev = "f860e598194f4a71747f91789bf536b393ad4a56" } [[language]] name = "twig" @@ -1027,7 +1041,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "markdown" -source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "a7de4be29783a6e25f3240c90afea52f2417faa3", subpath = "tree-sitter-markdown" } +source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "7e7aa9a25ca9729db9fe22912f8f47bdb403a979", subpath = "tree-sitter-markdown" } [[language]] name = "markdown.inline" @@ -1039,7 +1053,7 @@ grammar = "markdown_inline" [[grammar]] name = "markdown_inline" -source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "a7de4be29783a6e25f3240c90afea52f2417faa3", subpath = "tree-sitter-markdown-inline" } +source = { git = "https://github.com/MDeiml/tree-sitter-markdown", rev = "7e7aa9a25ca9729db9fe22912f8f47bdb403a979", subpath = "tree-sitter-markdown-inline" } [[language]] name = "dart" @@ -1091,7 +1105,7 @@ file-types = ["COMMIT_EDITMSG"] comment-token = "#" indent = { tab-width = 2, unit = " " } rulers = [50, 72] -max-line-length = 72 +text-width = 72 [[grammar]] name = "git-commit" @@ -1177,7 +1191,7 @@ source = { git = "https://github.com/shunsambongi/tree-sitter-gitignore", rev = name = "graphql" scope = "source.graphql" injection-regex = "graphql" -file-types = ["gql", "graphql"] +file-types = ["gql", "graphql", "graphqls"] roots = [] indent = { tab-width = 2, unit = " " } @@ -1450,7 +1464,7 @@ indent = { tab-width = 4, unit = "\t" } [[grammar]] name = "gdscript" -source = { git = "https://github.com/PrestonKnopp/tree-sitter-gdscript", rev = "a56a6fcec3fb63ec3324bf9373aae7298861eb30" } +source = { git = "https://github.com/PrestonKnopp/tree-sitter-gdscript", rev = "a4b57cc3bcbfc24550e858159647e9238e7ad1ac" } [[language]] name = "godot-resource" @@ -2195,6 +2209,19 @@ indent = { tab-width = 2, unit = " " } name = "yuck" source = { git = "https://github.com/Philipp-M/tree-sitter-yuck", rev = "9e97da5773f82123a8c8cccf8f7e795d140ed7d1" } +[[language]] +name = "prql" +scope = "source.prql" +injection-regex = "prql" +file-types = ["prql"] +roots = [] +comment-token = "#" +indent = { tab-width = 4, unit = " " } + +[[grammar]] +name = "prql" +source = { git = "https://github.com/PRQL/tree-sitter-prql", rev = "3f27cac466f030ee7d985d91eba5470e01dd21ea" } + [[language]] name = "po" scope = "source.po" @@ -2205,3 +2232,27 @@ comment-token = "#" [[grammar]] name = "po" source = { git = "https://github.com/erasin/tree-sitter-po", rev = "417cee9abb2053ed26b19e7de972398f2da9b29e" } + +[[language]] +name = "nasm" +scope = "source.nasm" +file-types = ["asm", "s", "S", "nasm"] +injection-regex = "n?asm" +roots = [] +comment-token = ";" +indent = { tab-width = 8, unit = " " } + +[[grammar]] +name = "nasm" +source = { git = "https://github.com/naclsn/tree-sitter-nasm", rev = "a0db15db6fcfb1bf2cc8702500e55e558825c48b" } + +[[language]] +name = "rst" +scope = "source.rst" +comment-token = ".." +file-types = ["rst"] +roots = [] + +[[grammar]] +name = "rst" +source = { git = "https://github.com/stsewd/tree-sitter-rst", rev = "25e6328872ac3a764ba8b926aea12719741103f1" } diff --git a/runtime/queries/comment/highlights.scm b/runtime/queries/comment/highlights.scm index 88685d59a1aa..3b25531a8d96 100644 --- a/runtime/queries/comment/highlights.scm +++ b/runtime/queries/comment/highlights.scm @@ -5,17 +5,33 @@ ":" @punctuation.delimiter +; Hint level tags +((tag (name) @hint) + (#match? @hint "^(HINT|MARK)$")) + +("text" @hint + (#match? @hint "^(HINT|MARK)$")) + +; Info level tags +((tag (name) @info) + (#match? @info "^(INFO|NOTE|TODO)$")) + +("text" @info + (#match? @info "^(INFO|NOTE|TODO)$")) + +; Warning level tags ((tag (name) @warning) - (#match? @warning "^(TODO|HACK|WARNING)$")) + (#match? @warning "^(HACK|WARN|WARNING)$")) ("text" @warning - (#match? @warning "^(TODO|HACK|WARNING)$")) + (#match? @warning "^(HACK|WARN|WARNING)$")) +; Error level tags ((tag (name) @error) - (match? @error "^(FIXME|XXX|BUG)$")) + (match? @error "^(BUG|FIXME|ISSUE|XXX)$")) ("text" @error - (match? @error "^(FIXME|XXX|BUG)$")) + (match? @error "^(BUG|FIXME|ISSUE|XXX)$")) (tag (name) @ui.text diff --git a/runtime/queries/ecma/highlights.scm b/runtime/queries/ecma/highlights.scm index 212bb8754555..7285ab9603ba 100644 --- a/runtime/queries/ecma/highlights.scm +++ b/runtime/queries/ecma/highlights.scm @@ -167,55 +167,76 @@ ] @punctuation.bracket [ - "as" "async" "debugger" "delete" "extends" "from" - "function" "get" - "in" - "instanceof" "new" - "of" "set" - "static" "target" - "try" "typeof" + "instanceof" "void" "with" ] @keyword +[ + "of" + "as" + "in" +] @keyword.operator + +[ + "function" +] @keyword.function + [ "class" "let" - "const" "var" ] @keyword.storage.type [ - "switch" - "case" + "const" + "static" +] @keyword.storage.modifier + +[ "default" - "if" - "else" "yield" - "throw" "finally" - "return" - "catch" - "continue" - "while" - "break" - "for" "do" "await" ] @keyword.control +[ + "if" + "else" + "switch" + "case" + "while" +] @keyword.control.conditional + +[ + "for" +] @keyword.control.repeat + [ "import" "export" ] @keyword.control.import +[ + "return" + "break" + "continue" +] @keyword.control.return + +[ + "throw" + "try" + "catch" +] @keyword.control.exception + diff --git a/runtime/queries/elm/textobjects.scm b/runtime/queries/elm/textobjects.scm new file mode 100644 index 000000000000..d212e9c3b3cb --- /dev/null +++ b/runtime/queries/elm/textobjects.scm @@ -0,0 +1,63 @@ +(line_comment) @comment.inside +(line_comment)+ @comment.around +(block_comment) @comment.inside +(block_comment)+ @comment.around + +((type_annotation)? + (value_declaration + (function_declaration_left (lower_case_identifier)) + (eq) + (_) @function.inside + ) +) @function.around + +(parenthesized_expr + (anonymous_function_expr + ( + (arrow) + (_) @function.inside + ) + ) +) @function.around + +(value_declaration + (function_declaration_left + (lower_pattern + (lower_case_identifier) @parameter.inside @parameter.around + ) + ) +) + +(value_declaration + (function_declaration_left + (pattern) @parameter.inside @parameter.around + ) +) + +(value_declaration + (function_declaration_left + (tuple_pattern + (pattern) @parameter.inside + ) @parameter.around + ) +) + +(value_declaration + (function_declaration_left + (record_pattern + (lower_pattern + (lower_case_identifier) @parameter.inside + ) + ) @parameter.around + ) +) + +(parenthesized_expr + (anonymous_function_expr + ( + (backslash) + (pattern) @parameter.inside + (arrow) + ) + ) +) diff --git a/runtime/queries/gdscript/highlights.scm b/runtime/queries/gdscript/highlights.scm index f36f4e35c15f..88f2a1875832 100644 --- a/runtime/queries/gdscript/highlights.scm +++ b/runtime/queries/gdscript/highlights.scm @@ -1,7 +1,8 @@ ; Identifier naming conventions -((identifier) @constant - (#match? @constant "^[A-Z][A-Z_]*$")) +( + (identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]+$")) ; class (class_name_statement (name) @type) @@ -11,32 +12,35 @@ ; Function calls (attribute_call (identifier) @function) - (base_call (identifier) @function) - (call (identifier) @function) ; Function definitions (function_definition (name) @function) - (constructor_definition "_init" @function) + ;; Literals -(integer) @constant.numeric.integer -(float) @constant.numeric.float (comment) @comment (string) @string -(escape_sequence) @constant.character.escape -(identifier) @variable + (type) @type +(expression_statement (array (identifier) @type)) +(binary_operator (identifier) @type) -;; Literals +(variable_statement (identifier) @variable) +(get_node) @label + +(const_statement (name) @constant) +(integer) @constant.numeric.integer +(float) @constant.numeric.float +(escape_sequence) @constant.character.escape [ (true) (false) - (null) -] @constant.builtin +] @constant.builtin.boolean +(null) @constant.builtin [ "+" @@ -62,37 +66,67 @@ "~" "<<" ">>" - "and" - "or" - "not" ] @operator +(annotation (identifier) @keyword.storage.modifier) + [ - (static_keyword) - (remote_keyword) - (tool_statement) - "var" - "func" - "setget" - "in" - "is" - "as" "if" "else" "elif" +] @keyword.control.conditional + +[ "while" "for" +] @keyword.control.repeat + +[ "return" + "pass" "break" "continue" - "pass" +] @keyword.control.return + +[ + "func" +] @keyword.control.function + +[ + "export" +] @keyword.control.import + +[ + "in" + "is" + "as" "match" + "and" + "or" + "not" +] @keyword.operator + +[ + "var" "class" "class_name" "enum" +] @keyword.storage.type + + +[ + (remote_keyword) + (static_keyword) + "const" "signal" + "@" +] @keyword.storage.modifier + +[ + "setget" "onready" - "export" "extends" - "const" + "set" + "get" ] @keyword + diff --git a/runtime/queries/gdscript/indents.scm b/runtime/queries/gdscript/indents.scm index 1ab41a44d289..c969eb7cf5a8 100644 --- a/runtime/queries/gdscript/indents.scm +++ b/runtime/queries/gdscript/indents.scm @@ -12,6 +12,7 @@ (dictionary (_)) (array (_)) + (setget) ] @indent [ @@ -25,7 +26,6 @@ (class_definition) ] @extend - [ (return_statement) (break_statement) diff --git a/runtime/queries/go/highlights.scm b/runtime/queries/go/highlights.scm index 927bd95b06a7..b2d81e45d4e7 100644 --- a/runtime/queries/go/highlights.scm +++ b/runtime/queries/go/highlights.scm @@ -19,6 +19,9 @@ (method_declaration name: (field_identifier) @function.method) +(method_spec + name: (field_identifier) @function.method) + ; Identifiers ((identifier) @constant (match? @constant "^[A-Z][A-Z\\d_]+$")) @@ -32,10 +35,19 @@ (match? @type.builtin "^(any|bool|byte|comparable|complex128|complex64|error|float32|float64|int|int16|int32|int64|int8|rune|string|uint|uint16|uint32|uint64|uint8|uintptr)$")) (type_identifier) @type +(type_spec + name: (type_identifier) @constructor) (field_identifier) @variable.other.member (identifier) @variable -(package_identifier) @variable +(package_identifier) @namespace + +(parameter_declaration (identifier) @variable.parameter) +(variadic_parameter_declaration (identifier) @variable.parameter) +(label_name) @label + +(const_spec + name: (identifier) @constant) ; Operators @@ -82,36 +94,57 @@ ; Keywords [ - "break" - "case" - "chan" - "const" - "continue" "default" - "defer" + "type" +] @keyword + +[ + "if" "else" - "fallthrough" + "switch" + "select" + "case" +] @keyword.control.conditional + +[ "for" - "func" - "go" - "goto" - "if" - "interface" - "map" "range" - "return" - "select" - "struct" - "switch" - "type" - "var" -] @keyword +] @keyword.control.repeat [ "import" "package" ] @keyword.control.import +[ + "return" + "continue" + "break" + "fallthrough" +] @keyword.control.return + +[ + "func" +] @keyword.function + +[ + "var" + "chan" + "interface" + "map" + "struct" +] @keyword.storage.type + +[ + "const" +] @keyword.storage.modifier + +[ + "defer" + "goto" + "go" +] @function.macro + ; Delimiters [ diff --git a/runtime/queries/godot-resource/injections.scm b/runtime/queries/godot-resource/injections.scm index 321c90add371..7929d63cd812 100644 --- a/runtime/queries/godot-resource/injections.scm +++ b/runtime/queries/godot-resource/injections.scm @@ -1,2 +1,18 @@ ((comment) @injection.content (#set! injection.language "comment")) + +; ((section) @injection.content +; (#set! injection.language "comment")) + +((section + (attribute + (identifier) @_type + (string) @_is_shader) + (property + (path) @_is_code + (string) @injection.content)) + (#match? @_type "type") + (#match? @_is_shader "Shader") + (#eq? @_is_code "code") + (#set! injection.language "glsl") +) diff --git a/runtime/queries/markdown.inline/injections.scm b/runtime/queries/markdown.inline/injections.scm index 2dd149d9062a..c2e7012caa04 100644 --- a/runtime/queries/markdown.inline/injections.scm +++ b/runtime/queries/markdown.inline/injections.scm @@ -1,2 +1,4 @@ ((html_tag) @injection.content (#set! injection.language "html") (#set! injection.include-unnamed-children)) + +((latex_block) @injection.content (#set! injection.language "latex") (#set! injection.include-unnamed-children)) diff --git a/runtime/queries/markdown/highlights.scm b/runtime/queries/markdown/highlights.scm index 25f22ba73861..80c9f9583f6a 100644 --- a/runtime/queries/markdown/highlights.scm +++ b/runtime/queries/markdown/highlights.scm @@ -39,7 +39,7 @@ (list_marker_parenthesis) ] @markup.list.numbered -(thematic_break) @punctuation.delimiter +(thematic_break) @punctuation.special [ (block_continuation) @@ -51,3 +51,9 @@ ] @string.escape (block_quote) @markup.quote + +(pipe_table_row + "|" @punctuation.special) +(pipe_table_header + "|" @punctuation.special) +(pipe_table_delimiter_row) @punctuation.special diff --git a/runtime/queries/nasm/highlights.scm b/runtime/queries/nasm/highlights.scm new file mode 100644 index 000000000000..5e3cfebe6f5b --- /dev/null +++ b/runtime/queries/nasm/highlights.scm @@ -0,0 +1,126 @@ +(comment) @comment + +(label) @label + +(preproc_expression) @keyword.directive + +[ + (line_here_token) + (section_here_token) +] @variable.builtin + +(unary_expression + operator: _ @operator) +(binary_expression + operator: _ @operator) +(conditional_expression + "?" @operator + ":" @operator) + +[ + ":" + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" +] @punctuation.bracket + +(instruction_prefix) @keyword +(actual_instruction + instruction: (word) @function) + +(call_syntax_expression + base: (word) @function) + +(size_hint) @type +(struc_declaration + name: (word) @type) +(struc_instance + name: (word) @type) + +(effective_address + hint: _ @type) +(effective_address + segment: _ @constant.builtin) + +(register) @constant.builtin + +(number_literal) @constant.numeric.integer +(string_literal) @string +(float_literal) @constant.numeric.float +(packed_bcd_literal) @constant.numeric.integer + +((word) @constant + (#match? @constant "^[A-Z_][?A-Z_0-9]+$")) +((word) @constant.builtin + (#match? @constant.builtin "^__\\?[A-Z_a-z0-9]+\\?__$")) +(word) @variable + +(preproc_arg) @keyword.directive + +[ + (preproc_def) + (preproc_function_def) + (preproc_undef) + (preproc_alias) + (preproc_multiline_macro) + (preproc_multiline_unmacro) + (preproc_if) + (preproc_rotate) + (preproc_rep_loop) + (preproc_include) + (preproc_pathsearch) + (preproc_depend) + (preproc_use) + (preproc_push) + (preproc_pop) + (preproc_repl) + (preproc_arg) + (preproc_stacksize) + (preproc_local) + (preproc_reporting) + (preproc_pragma) + (preproc_line) + (preproc_clear) +] @keyword.directive +[ + (pseudo_instruction_dx) + (pseudo_instruction_resx) + (pseudo_instruction_incbin_command) + (pseudo_instruction_equ_command) + (pseudo_instruction_times_prefix) + (pseudo_instruction_alignx_macro) +] @function.special +[ + (assembl_directive_target) + (assembl_directive_defaults) + (assembl_directive_sections) + (assembl_directive_absolute) + (assembl_directive_symbols) + (assembl_directive_common) + (assembl_directive_symbolfixes) + (assembl_directive_cpu) + (assembl_directive_floathandling) + (assembl_directive_org) + (assembl_directive_sectalign) + + (assembl_directive_primitive_target) + (assembl_directive_primitive_defaults) + (assembl_directive_primitive_sections) + (assembl_directive_primitive_absolute) + (assembl_directive_primitive_symbols) + (assembl_directive_primitive_common) + (assembl_directive_primitive_symbolfixes) + (assembl_directive_primitive_cpu) + (assembl_directive_primitive_floathandling) + (assembl_directive_primitive_org) + (assembl_directive_primitive_sectalign) + (assembl_directive_primitive_warning) + (assembl_directive_primitive_map) +] @keyword diff --git a/runtime/queries/nasm/injections.scm b/runtime/queries/nasm/injections.scm new file mode 100644 index 000000000000..2f0e58eb6431 --- /dev/null +++ b/runtime/queries/nasm/injections.scm @@ -0,0 +1,2 @@ +((comment) @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/nasm/textobjects.scm b/runtime/queries/nasm/textobjects.scm new file mode 100644 index 000000000000..ddfbad78cb30 --- /dev/null +++ b/runtime/queries/nasm/textobjects.scm @@ -0,0 +1,15 @@ +(preproc_multiline_macro + body: (body) @function.inside) @function.around +(struc_declaration + body: (struc_declaration_body) @class.inside) @class.around +(struc_instance + body: (struc_instance_body) @class.inside) @class.around + +(preproc_function_def_parameters + (word) @parameter.inside) +(call_syntax_arguments + (_) @parameter.inside) +(operand) @parameter.inside + +(comment) @comment.inside +(comment)+ @comment.around diff --git a/runtime/queries/php/highlights.scm b/runtime/queries/php/highlights.scm index 77c9424fef64..4bf313d84656 100644 --- a/runtime/queries/php/highlights.scm +++ b/runtime/queries/php/highlights.scm @@ -2,15 +2,63 @@ "?>" @tag ; Types +[ + (primitive_type) + (cast_type) +] @type.builtin + +(named_type + [ (name) @type + (qualified_name (name) @type)]) + +(base_clause + [ (name) @type + (qualified_name (name) @type)]) + +(enum_declaration + name: (name) @type.enum) -(primitive_type) @type.builtin -(cast_type) @type.builtin -(named_type (name) @type) @type -(named_type (qualified_name) @type) @type +(interface_declaration + name: (name) @constructor) + +(class_declaration + name: (name) @constructor) + +(trait_declaration + name:(name) @constructor) (namespace_definition name: (namespace_name (name) @namespace)) +(namespace_name_as_prefix + (namespace_name (name) @namespace)) + +(namespace_use_clause + [ (name) @namespace + (qualified_name (name) @type) ]) + +(namespace_aliasing_clause (name) @namespace) + +(class_interface_clause + [(name) @type + (qualified_name (name) @type)]) + +(scoped_call_expression + scope: [(name) @type + (qualified_name (name) @type)]) + +(class_constant_access_expression + . [(name) @constructor + (qualified_name (name) @constructor)] + (name) @constant) + +(use_declaration (name) @type) + +(binary_expression + operator: "instanceof" + right: [(name) @type + (qualified_name (name) @type)]) + ; Superglobals (subscript_expression (variable_name(name) @constant.builtin @@ -36,6 +84,21 @@ (function_definition name: (name) @function) +(nullsafe_member_call_expression + name: (name) @function.method) + +(object_creation_expression + [(name) @constructor + (qualified_name (name) @constructor)]) + +; Parameters +[ + (simple_parameter) + (variadic_parameter) +] @variable.parameter + +(argument + (name) @variable.parameter) ; Member @@ -62,68 +125,192 @@ (variable_name) @variable +; Attributes +(attribute_list) @attribute + ; Basic tokens -(string) @string -(heredoc) @string +[ + (string) + (encapsed_string) + (heredoc_body) + (nowdoc_body) + (shell_command_expression) +] @string +(escape_sequence) @constant.character.escape + (boolean) @constant.builtin.boolean (null) @constant.builtin (integer) @constant.numeric.integer (float) @constant.numeric.float (comment) @comment -"$" @operator +(goto_statement (name) @label) +(named_label_statement (name) @label) ; Keywords [ - "abstract" - "as" - "break" - "case" - "catch" - "class" - "const" - "continue" - "declare" "default" - "do" "echo" - "else" - "elseif" - "enddeclare" - "endforeach" - "endif" - "endswitch" - "endwhile" "enum" "extends" "final" - "finally" - "foreach" - "fn" - "function" + "goto" "global" - "if" "implements" - "include_once" - "include" "insteadof" - "interface" - "match" - "namespace" "new" "private" "protected" "public" +] @keyword + +[ + "if" + "else" + "elseif" + "endif" + "switch" + "endswitch" + "case" + "match" + "declare" + "enddeclare" + "??" +] @keyword.control.conditional + +[ + "for" + "endfor" + "foreach" + "endforeach" + "while" + "endwhile" + "do" +] @keyword.control.repeat + +[ + + "include_once" + "include" "require_once" "require" + "use" +] @keyword.control.import + +[ "return" - "static" - "switch" + "break" + "continue" + "yield" +] @keyword.control.return + +[ "throw" - "trait" "try" - "use" - "while" -] @keyword + "catch" + "finally" +] @keyword.control.exception + +[ + "as" + "or" + "xor" + "and" + "instanceof" +] @keyword.operator + +[ + "fn" + "function" +] @keyword.function + +[ + "namespace" + "class" + "interface" + "trait" + "abstract" +] @keyword.storage.type + +[ + "static" + "const" +] @keyword.storage.modifier + +[ + "," + ";" + ":" + "\\" + ] @punctuation.delimiter + +[ + (php_tag) + "?>" + "(" + ")" + "[" + "]" + "{" + "}" + "#[" +] @punctuation.bracket + +[ + "=" + + "." + "-" + "*" + "/" + "+" + "%" + "**" + + "~" + "|" + "^" + "&" + "<<" + ">>" + + "->" + "?->" + + "=>" + + "<" + "<=" + ">=" + ">" + "<>" + "==" + "!=" + "===" + "!==" + + "!" + "&&" + "||" + + ".=" + "-=" + "+=" + "*=" + "/=" + "%=" + "**=" + "&=" + "|=" + "^=" + "<<=" + ">>=" + "??=" + "--" + "++" + + "@" + "::" +] @operator diff --git a/runtime/queries/prql/highlights.scm b/runtime/queries/prql/highlights.scm new file mode 100644 index 000000000000..5cfedee4853c --- /dev/null +++ b/runtime/queries/prql/highlights.scm @@ -0,0 +1,136 @@ +[ + (keyword_from) + (keyword_filter) + (keyword_derive) + (keyword_group) + (keyword_aggregate) + (keyword_sort) + (keyword_take) + (keyword_window) + (keyword_join) + (keyword_select) + (keyword_switch) + (keyword_append) + (keyword_remove) + (keyword_intersect) + (keyword_rolling) + (keyword_rows) + (keyword_expanding) + (keyword_let) + (keyword_prql) + (keyword_from_text) +] @keyword + +(literal) @string + +(assignment + alias: (field) @variable.other.member) + +alias: (identifier) @variable.other.member + +(f_string) @string.special +(s_string) @string.special + +(comment) @comment + +(keyword_func) @keyword.function + +(function_call + (identifier) @function) + +[ + "+" + "-" + "*" + "/" + "=" + "==" + "<" + "<=" + "!=" + ">=" + ">" + "->" + (bang) +] @operator + +[ + "(" + ")" + "[" + "]" +] @punctuation.bracket + +[ + "," + "." + (pipe) +] @punctuation.delimiter + +(literal + (integer) @constant.numeric.integer) + +(integer) @constant.numeric.integer + +(literal + (decimal_number) @constant.numeric.float) + +(decimal_number) @constant.numeric.float + +[ + (keyword_min) + (keyword_max) + (keyword_count) + (keyword_count_distinct) + (keyword_average) + (keyword_avg) + (keyword_sum) + (keyword_stddev) + (keyword_count) +] @function + +[ + (keyword_side) + (keyword_version) + (keyword_target) + (keyword_null) + (keyword_format) +] @attribute + +(target) @function.builtin + + [ + (date) + (time) + (timestamp) +] @string.special + +[ + (keyword_left) + (keyword_inner) + (keyword_right) + (keyword_full) + (keyword_csv) + (keyword_json) +] @function.method + +[ + (keyword_true) + (keyword_false) +] @constant.builtin.boolean + +[ + (keyword_and) + (keyword_or) +] @keyword.operator + +(function_definition + (keyword_func) + name: (identifier) @function) + +(parameter + (identifier) @variable.parameter) + +(variable + (keyword_let) + name: (identifier) @constant) diff --git a/runtime/queries/prql/injections.scm b/runtime/queries/prql/injections.scm new file mode 100644 index 000000000000..02a8919f8951 --- /dev/null +++ b/runtime/queries/prql/injections.scm @@ -0,0 +1,8 @@ +((s_string) @injection.content + (#set! injection.language "sql")) + +(from_text + (keyword_from_text) + (keyword_json) + (literal) @injection.content + (#set! injection.language "json")) diff --git a/runtime/queries/rst/highlights.scm b/runtime/queries/rst/highlights.scm new file mode 100644 index 000000000000..73c0def431e9 --- /dev/null +++ b/runtime/queries/rst/highlights.scm @@ -0,0 +1,38 @@ +(comment) @comment + +[ + (title) +] @markup.heading.1 + +[ + "adornment" +] @markup.heading.marker + +[ + (target) + (reference) +] @markup.link.url + +[ + "bullet" +] @markup.list.unnumbered + +(strong) @markup.bold +(emphasis) @markup.italic +(literal) @markup.raw.inline + +(list_item + (term) @markup.bold + (classifier)? @markup.italic) + +(directive + [".." (type) "::"] @function +) + +(field + [":" (field_name) ":"] @variable.other.member +) + +(interpreted_text) @markup.raw.inline + +(interpreted_text (role)) @keyword diff --git a/runtime/queries/sway/highlights.scm b/runtime/queries/sway/highlights.scm new file mode 100644 index 000000000000..98f4d4493f02 --- /dev/null +++ b/runtime/queries/sway/highlights.scm @@ -0,0 +1,336 @@ +; ------- +; Tree-Sitter doesn't allow overrides in regards to captures, +; though it is possible to affect the child node of a captured +; node. Thus, the approach here is to flip the order so that +; overrides are unnecessary. +; ------- + +; ------- +; Types +; ------- + +; --- +; Primitives +; --- + +(escape_sequence) @constant.character.escape +(primitive_type) @type.builtin +(boolean_literal) @constant.builtin.boolean +(integer_literal) @constant.numeric.integer +(float_literal) @constant.numeric.float +(char_literal) @constant.character +[ + (string_literal) + (raw_string_literal) +] @string +[ + (line_comment) + (block_comment) +] @comment + +; --- +; Extraneous +; --- + +(self) @variable.builtin +(enum_variant (identifier) @type.enum.variant) + +(field_initializer + (field_identifier) @variable.other.member) +(shorthand_field_initializer + (identifier) @variable.other.member) +(shorthand_field_identifier) @variable.other.member + +(loop_label + "'" @label + (identifier) @label) + +; --- +; Punctuation +; --- + +[ + "::" + "." + ";" + "," +] @punctuation.delimiter + +[ + "(" + ")" + "[" + "]" + "{" + "}" + "#" +] @punctuation.bracket +(type_arguments + [ + "<" + ">" + ] @punctuation.bracket) +(type_parameters + [ + "<" + ">" + ] @punctuation.bracket) +(closure_parameters + "|" @punctuation.bracket) + +; --- +; Variables +; --- + +(let_declaration + pattern: [ + ((identifier) @variable) + ((tuple_pattern + (identifier) @variable)) + ]) + +; It needs to be anonymous to not conflict with `call_expression` further below. +(_ + value: (field_expression + value: (identifier)? @variable + field: (field_identifier) @variable.other.member)) + +(parameter + pattern: (identifier) @variable.parameter) +(closure_parameters + (identifier) @variable.parameter) + +; ------- +; Keywords +; ------- + +(for_expression + "for" @keyword.control.repeat) +((identifier) @keyword.control + (#match? @keyword.control "^yield$")) + +"in" @keyword.control + +[ + "match" + "if" + "else" +] @keyword.control.conditional + +[ + "while" +] @keyword.control.repeat + +[ + "break" + "continue" + "return" +] @keyword.control.return + +[ + "contract" + "script" + "predicate" +] @keyword.other + +"use" @keyword.control.import +(dep_item "dep" @keyword.control.import !body) +(use_as_clause "as" @keyword.control.import) + +(type_cast_expression "as" @keyword.operator) + +[ + "as" + "pub" + "dep" + + "abi" + "impl" + "where" + "trait" + "for" +] @keyword + +[ + "struct" + "enum" + "storage" + "configurable" +] @keyword.storage.type + +"let" @keyword.storage +"fn" @keyword.function +"abi" @keyword.function + +(mutable_specifier) @keyword.storage.modifier.mut + +(reference_type "&" @keyword.storage.modifier.ref) +(self_parameter "&" @keyword.storage.modifier.ref) + +[ + "const" + "ref" + "deref" + "move" +] @keyword.storage.modifier + +; TODO: variable.mut to highlight mutable identifiers via locals.scm + +; ------- +; Guess Other Types +; ------- + +((identifier) @constant + (#match? @constant "^[A-Z][A-Z\\d_]*$")) + +; --- +; PascalCase identifiers in call_expressions (e.g. `Ok()`) +; are assumed to be enum constructors. +; --- + +(call_expression + function: [ + ((identifier) @type.enum.variant + (#match? @type.enum.variant "^[A-Z]")) + (scoped_identifier + name: ((identifier) @type.enum.variant + (#match? @type.enum.variant "^[A-Z]"))) + ]) + +; --- +; Assume that types in match arms are enums and not +; tuple structs. Same for `if let` expressions. +; --- + +(match_pattern + (scoped_identifier + name: (identifier) @constructor)) +(tuple_struct_pattern + type: [ + ((identifier) @constructor) + (scoped_identifier + name: (identifier) @constructor) + ]) +(struct_pattern + type: [ + ((type_identifier) @constructor) + (scoped_type_identifier + name: (type_identifier) @constructor) + ]) + +; --- +; Other PascalCase identifiers are assumed to be structs. +; --- + +((identifier) @type + (#match? @type "^[A-Z]")) + +; ------- +; Functions +; ------- + +(call_expression + function: [ + ((identifier) @function) + (scoped_identifier + name: (identifier) @function) + (field_expression + field: (field_identifier) @function) + ]) +(generic_function + function: [ + ((identifier) @function) + (scoped_identifier + name: (identifier) @function) + (field_expression + field: (field_identifier) @function.method) + ]) + +(function_item + name: (identifier) @function) + +(function_signature_item + name: (identifier) @function) + +; ------- +; Operators +; ------- + +[ + "*" + "'" + "->" + "=>" + "<=" + "=" + "==" + "!" + "!=" + "%" + "%=" + "&" + "&=" + "&&" + "|" + "|=" + "||" + "^" + "^=" + "*" + "*=" + "-" + "-=" + "+" + "+=" + "/" + "/=" + ">" + "<" + ">=" + ">>" + "<<" + ">>=" + "<<=" + "@" + ".." + "..=" + "'" +] @operator + +; ------- +; Paths +; ------- + +(use_declaration + argument: (identifier) @namespace) +(use_wildcard + (identifier) @namespace) +(dep_item + name: (identifier) @namespace) +(scoped_use_list + path: (identifier)? @namespace) +(use_list + (identifier) @namespace) +(use_as_clause + path: (identifier)? @namespace + alias: (identifier) @namespace) + +; --- +; Remaining Paths +; --- + +(scoped_identifier + path: (identifier)? @namespace + name: (identifier) @namespace) +(scoped_type_identifier + path: (identifier) @namespace) + +; ------- +; Remaining Identifiers +; ------- + +"?" @special + +(type_identifier) @type +(identifier) @variable +(field_identifier) @variable.other.member diff --git a/runtime/queries/sway/indents.scm b/runtime/queries/sway/indents.scm new file mode 100644 index 000000000000..e6902b62cb34 --- /dev/null +++ b/runtime/queries/sway/indents.scm @@ -0,0 +1,71 @@ +[ + (use_list) + (block) + (match_block) + (arguments) + (parameters) + (declaration_list) + (field_declaration_list) + (field_initializer_list) + (struct_pattern) + (tuple_pattern) + (unit_expression) + (enum_variant_list) + (call_expression) + (binary_expression) + (field_expression) + (tuple_expression) + (array_expression) + (where_clause) + + (token_tree) +] @indent + +[ + "}" + "]" + ")" +] @outdent + +; Indent the right side of assignments. +; The #not-same-line? predicate is required to prevent an extra indent for e.g. +; an else-clause where the previous if-clause starts on the same line as the assignment. +(assignment_expression + . + (_) @expr-start + right: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(compound_assignment_expr + . + (_) @expr-start + right: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(let_declaration + . + (_) @expr-start + value: (_) @indent + alternative: (_)? @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) +(if_expression + . + (_) @expr-start + condition: (_) @indent + (#not-same-line? @indent @expr-start) + (#set! "scope" "all") +) + +; Some field expressions where the left part is a multiline expression are not +; indented by cargo fmt. +; Because this multiline expression might be nested in an arbitrary number of +; field expressions, this can only be matched using a Regex. +(field_expression + value: (_) @val + "." @outdent + (#match? @val "(\\A[^\\n\\r]+\\([\\t ]*(\\n|\\r).*)|(\\A[^\\n\\r]*\\{[\\t ]*(\\n|\\r))") +) diff --git a/runtime/queries/sway/injections.scm b/runtime/queries/sway/injections.scm new file mode 100644 index 000000000000..e4509a5fd317 --- /dev/null +++ b/runtime/queries/sway/injections.scm @@ -0,0 +1,2 @@ +([(line_comment) (block_comment)] @injection.content + (#set! injection.language "comment")) diff --git a/runtime/queries/sway/locals.scm b/runtime/queries/sway/locals.scm new file mode 100644 index 000000000000..262d609e910c --- /dev/null +++ b/runtime/queries/sway/locals.scm @@ -0,0 +1,17 @@ +; Scopes + +[ + (function_item) + (closure_expression) + (block) +] @local.scope + +; Definitions + +(parameter + (identifier) @local.definition) + +(closure_parameters (identifier) @local.definition) + +; References +(identifier) @local.reference diff --git a/runtime/queries/sway/textobjects.scm b/runtime/queries/sway/textobjects.scm new file mode 100644 index 000000000000..15740bc8554b --- /dev/null +++ b/runtime/queries/sway/textobjects.scm @@ -0,0 +1,52 @@ +(function_item + body: (_) @function.inside) @function.around(closure_expression body: (_) @function.inside) @function.around + +(struct_item + body: (_) @class.inside) @class.around + +(enum_item + body: (_) @class.inside) @class.around + +(trait_item + body: (_) @class.inside) @class.around + +(impl_item + body: (_) @class.inside) @class.around + +(parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(type_arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(closure_parameters + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +(arguments + ((_) @parameter.inside . ","? @parameter.around) @parameter.around) + +[ + (line_comment) + (block_comment) +] @comment.inside + +(line_comment)+ @comment.around + +(block_comment) @comment.around + +(; #[test] + (attribute_item + (attribute + (identifier) @_test_attribute)) + ; allow other attributes like #[should_panic] and comments + [ + (attribute_item) + (line_comment) + ]* + ; the test function + (function_item + body: (_) @test.inside) @test.around + (#eq? @_test_attribute "test")) diff --git a/runtime/themes/adwaita-dark.toml b/runtime/themes/adwaita-dark.toml new file mode 100644 index 000000000000..f339e33c1f13 --- /dev/null +++ b/runtime/themes/adwaita-dark.toml @@ -0,0 +1,169 @@ +# Author: Mofiqul Islam + +"attribute" = "orange_4" + +"type" = "teal_2" +"type.builtin" = "teal_2" + +"constructor" = "blue_2" + +"constant" = "violet_2" +"constant.builtin" = { fg = "violet_2", modifiers = ["bold"] } +"constant.character" = "teal_3" +"constant.numeric" = { fg = "teal_3", modifiers = ["bold"] } +"constant.character.escape" = "violet_2" + +"string" = "teal_2" +"string.regexp" = "purple_2" +"string.special" = "blue_2" + +"comment" = "dark_2" + +"variable" = "light_4" +"variable.parameter" = "orange_2" +"variable.builtin" = "orange_2" +"variable.other" = "teal_2" +"variable.other.member" = "teal_2" + +"label" = "purple_2" + +"punctuation" = "light_4" +"punctuation.delimiter" = "light_4" +"punctuation.bracket" = "light_4" +"punctuation.special" = "red_3" + +"keyword" = { fg = "orange_2", modifiers = ["bold"] } +"keyword.control" = { fg = "orange_2", modifiers = ["bold"] } +"keyword.operator" = "purple_2" +"keyword.directive" = { fg = "orange_2", modifiers = ["bold"] } +"keyword.function" = "orange_2" +"keyword.storage" = { fg = "orange_2", modifiers = ["bold"] } + +"operator" = "purple_2" + +"function" = "blue_2" +"function.builtin" = "blue_2" +"function.macro" = { fg = "blue_2", modifiers = ["bold"] } +"function.special" = { fg = "blue_2", modifiers = ["bold"] } + +"tag" = "teal_2" + +"namespace" = "orange_2" + +"markup" = "light_4" +"markup.heading" = { fg = "teal_2", modifiers = ["bold"] } +"markup.list" = { fg = "orange_2", modifiers = ["bold"] } +"markup.bold" = { fg = "light_4", modifiers = ["bold"] } +"markup.italic" = { fg = "light_4", modifiers = ["italic"] } +"markup.link" = { fg = "blue_3", modifiers = ["underlined"] } +"markup.quote" = { fg = "light_3", modifiers = ["italic"] } +"diff.plus" = "teal_3" +"diff.minus" = "red_1" +"diff.delta" = "orange_3" +"diff.delta.moved" = "orange_2" + +"ui.background" = { fg = "light_4", bg = "libadwaita_dark" } +"ui.background.separator" = { fg = "split_and_borders", bg = "libadwaita_dark" } +"ui.cursor" = { fg = "libadwaita_dark", bg = "light_5" } +"ui.cursor.insert" = { fg = "libadwaita_dark", bg = "light_5" } +"ui.cursor.select" = { fg = "libadwaita_dark", bg = "light_5" } +"ui.cursor.match" = { fg = "libadwaita_dark", bg = "blue_2" } +"ui.cursor.primary" = { fg = "libadwaita_dark", bg = "light_7" } +"ui.linenr" = "dark_2" +"ui.linenr.selected" = { fg = "light_7", bg = "libadwaita_dark_alt", modifiers = [ + "bold", +] } +"ui.statusline" = { fg = "light_4", bg = "libadwaita_dark_alt" } +"ui.statusline.inactive" = { fg = "light_4", bg = "libadwaita_dark_alt" } +"ui.statusline.insert" = { fg = "light_4", bg = "teal_4" } +"ui.statusline.select" = { fg = "light_4", bg = "blue_4" } +"ui.popup" = { bg = "libadwaita_popup" } +"ui.window" = "split_and_borders" +"ui.help" = { bg = "libadwaita_dark_alt" } +"ui.text" = "light_4" +"ui.virtual" = "dark_1" +"ui.menu" = { fg = "light_4", bg = "libadwaita_popup" } +"ui.menu.selected" = { fg = "light_4", bg = "blue_5" } +"ui.menu.scroll" = { fg = "light_7", bg = "dark_3" } +"ui.selection" = { bg = "blue_7" } +"ui.selection.primary" = { bg = "blue_7" } +"ui.cursorline.primary" = { bg = "libadwaita_dark_alt" } + +"warning" = "yellow_2" +"error" = "red_4" +"info" = "purple_2" +"hint" = "blue_2" + +"diagnostic.hint" = { fg = "blue_2", modifiers = ["dim"] } +"diagnostic.info" = { fg = "purple_2", modifiers = ["dim"] } +"diagnostic.error" = { fg = "red_4", modifiers = ["underlined"] } +"diagnostic.warning" = { fg = "yellow_2", modifiers = ["underlined"] } + +[palette] +blue_1 = "#99C1F1" +blue_2 = "#62A0EA" +blue_3 = "#3584E4" +blue_4 = "#1C71D8" +blue_5 = "#1A5FB4" +blue_6 = "#1B497E" +blue_7 = "#193D66" +brown_1 = "#CDAB8F" +brown_2 = "#B5835A" +brown_3 = "#986A44" +brown_4 = "#865E3C" +brown_5 = "#63452C" +chameleon_3 = "#4E9A06" +dark_1 = "#77767B" +dark_2 = "#5E5C64" +dark_3 = "#504E55" +dark_4 = "#3D3846" +dark_5 = "#241F31" +dark_6 = "#000000" +dark_7 = "#1c1c1c" +green_1 = "#8FF0A4" +green_2 = "#57E389" +green_3 = "#33D17A" +green_4 = "#2EC27E" +green_5 = "#26A269" +green_6 = "#1F7F56" +green_7 = "#1C6849" +libadwaita_dark = "#1D1D1D" +libadwaita_dark_alt = "#303030" +libadwaita_popup = "#282828" +light_1 = "#FFFFFF" +light_2 = "#FCFCFC" +light_3 = "#F6F5F4" +light_4 = "#DEDDDA" +light_5 = "#C0BFBC" +light_6 = "#B0AFAC" +light_7 = "#9A9996" +orange_1 = "#FFBE6F" +orange_2 = "#FFA348" +orange_3 = "#FF7800" +orange_4 = "#E66100" +orange_5 = "#C64600" +purple_1 = "#DC8ADD" +purple_2 = "#C061CB" +purple_3 = "#9141AC" +purple_4 = "#813D9C" +purple_5 = "#613583" +red_1 = "#F66151" +red_2 = "#ED333B" +red_3 = "#E01B24" +red_4 = "#C01C28" +red_5 = "#A51D2D" +teal_1 = "#93DDC2" +teal_2 = "#5BC8AF" +teal_3 = "#33B2A4" +teal_4 = "#26A1A2" +teal_5 = "#218787" +violet_2 = "#7D8AC7" +violet_3 = "#6362C8" +violet_4 = "#4E57BA" +yellow_1 = "#F9F06B" +yellow_2 = "#F8E45C" +yellow_3 = "#F6D32D" +yellow_4 = "#F5C211" +yellow_5 = "#E5A50A" +yellow_6 = "#D38B09" +split_and_borders = "#4F4F4F" diff --git a/runtime/themes/ayu_evolve.toml b/runtime/themes/ayu_evolve.toml index 2c545beb14d4..b57235a6a23a 100644 --- a/runtime/themes/ayu_evolve.toml +++ b/runtime/themes/ayu_evolve.toml @@ -25,6 +25,8 @@ inherits = 'ayu_dark' "ui.cursor.primary.select" = { fg = "dark_gray", bg = "magenta" } "ui.cursor.primary.insert" = { fg = "dark_gray", bg = "green" } "ui.text.inactive" = "gray" +"ui.bufferline" = { fg = "light_gray", bg = "background" } +"ui.bufferline.active" = { fg = "light_gray", bg = "dark_gray" } [palette] background = '#020202' diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 2504fec87f63..59fa430f8eb0 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -103,10 +103,10 @@ "ui.menu" = { fg = "overlay2", bg = "surface0" } "ui.menu.selected" = { fg = "text", bg = "surface1", modifiers = ["bold"] } -"diagnostic.error" = { fg = "red", underline = { color = "red", style = "curl" } } -"diagnostic.warning" = { fg = "yellow", underline = { color = "yellow", style = "curl" } } -"diagnostic.info" = { fg = "sky", underline = { color = "sky", style = "curl" } } -"diagnostic.hint" = { fg = "teal", underline = { color = "teal", style = "curl" } } +"diagnostic.error" = { underline = { color = "red", style = "curl" } } +"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } +"diagnostic.info" = { underline = { color = "sky", style = "curl" } } +"diagnostic.hint" = { underline = { color = "teal", style = "curl" } } error = "red" warning = "yellow" diff --git a/runtime/themes/everblush.toml b/runtime/themes/everblush.toml new file mode 100644 index 000000000000..edcdaf41bd90 --- /dev/null +++ b/runtime/themes/everblush.toml @@ -0,0 +1,114 @@ +# Author: Isotoxal + +"attribute" = { fg = "blue" } +"comment" = { fg = "comment", modifiers = ["italic"] } +"constant" = { fg = "cyan" } +"constant.builtin.boolean" = { fg = "cyan" } +"constant.character" = { fg = "blue" } +"constant.numeric.float" = { fg = "black-light" } +"constant.builtin" = { fg = "blue" } +"constant.numeric" = { fg = "yellow" } +"constructor" = { fg = "blue" } +"function" = { fg = "red" } +"function.builtin" = { fg = "cyan-light" } +"function.macro" = { fg = "green" } +"function.method" = { fg = "blue-light" } +"keyword" = { fg = "blue" } +"keyword.function" = { fg = "blue" } +"keyword.operator" = { fg = "blue-light" } +"keyword.control.conditional" = { fg = "red" } +"keyword.control.import" = { fg = "red-light" } +"keyword.control.return" = { fg = "blue" } +"keyword.control.repeat" = { fg = "yellow-light" } +"keyword.control.exception" = { fg = "black-light" } +"label" = { fg = "blue" } +"namespace" = { fg = "red-light" } +"operator" = { fg = "white" } +#"parameter.reference" = { fg = "red-light" } +#"property" = { fg = "red" } +"punctuation.bracket" = { fg = "white" } +"punctuation.delimiter" = { fg = "white" } +"punctuation.special" = { fg = "white" } +"string" = { fg = "green" } +"string.escape" = { fg = "blue" } +"string.regex" = { fg = "green" } +"string.special" = { fg = "blue" } +"string.special.symbol" = { fg = "red" } +"tag" = { fg = "blue" } +"type" = { fg = "yellow" } +"type.builtin" = { fg = "yellow" } +"variable" = { fg = "white" } +"variable.builtin" = { fg = "blue" } +"variable.parameter" = { fg = "red" } +"variable.other.member" = { fg = "red" } + +"diff.plus" = { fg = "blue" } +"diff.delta" = { fg = "magenta" } +"diff.minus" = { fg = "red" } + +"ui.background" = { fg = "foreground", bg = "background" } +"ui.cursor" = { modifiers = ["reversed"] } +"ui.cursorline.primary" = { bg = "cursorline" } +"ui.help" = { fg = "foreground", bg = "contrast" } +"ui.linenr" = { fg = "comment" } +"ui.linenr.selected" = { fg = "foreground" } +"ui.menu" = { fg = "foreground", bg = "contrast" } +"ui.menu.selected" = { bg = "black" } +"ui.popup" = { fg = "foreground", bg = "contrast" } +"ui.selection" = { bg = "black" } +"ui.selection.primary" = { bg = "black" } +"ui.statusline" = { fg = "foreground", bg = "background" } +"ui.statusline.inactive" = { fg = "foreground", bg = "background" } +"ui.statusline.normal" = { fg = "white", bg = "background" } +"ui.statusline.insert" = { fg = "blue", bg = "background" } +"ui.statusline.select" = { fg = "cyan", bg = "magenta" } +"ui.text" = { fg = "foreground" } +"ui.text.focus" = { fg = "blue" } +"ui.virtual.ruler" = { bg = "cursorline" } +"ui.virtual.whitespace" = { fg = "comment" } +"ui.virtual.wrap" = { fg = "comment" } +"ui.virtual.indent-guide" = { fg = "comment" } +"ui.window" = { fg = "black" } + +"error" = { fg = "red" } +"hint" = { fg = "green" } +"warning" = { fg = "yellow" } +"info" = { fg = "blue" } +"diagnostic.error" = { underline = { style = "curl", color = "red" } } +"diagnostic.warning" = { underline = { style = "curl", color = "yellow" } } +"diagnostic.info" = { underline = { style = "curl", color = "blue" } } +"diagnostic.hint" = { underline = { style = "curl", color = "green" } } +"special" = { fg = "red-light" } + +"markup.heading" = { fg = "blue", modifiers = ["bold"] } +"markup.list" = { fg = "cyan" } +"markup.bold" = { fg = "magenta", modifiers = ["bold"] } +"markup.italic" = { fg = "yellow", modifiers = ["italic"] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.link.url" = { fg = "green" } +"markup.link.text" = { fg = "black-light" } +"markup.quote" = { fg = "yellow", modifiers = ["italic"] } +"markup.raw" = { fg = "cyan" } + +[palette] +black = "#232a2d" +red = "#e57474" +green = "#8ccf7e" +yellow = "#e5c76b" +blue = "#67b0e8" +magenta = "#c47fd5" +cyan = "#6cbfbf" +white = "#b3b9b8" +black-light = "#2d3437" +red-light = "#ef7e7e" +green-light = "#96d988" +yellow-light = "#f4d67a" +blue-light = "#71baf2" +magenta-light = "#ce89df" +cyan-light = "#67cbe7" +white-light = "#bdc3c2" +comment = "#404749" +contrast = "#161d1f" +background = "#141b1e" +foreground = "#dadada" +cursorline = "#2c3333" diff --git a/runtime/themes/kanagawa.toml b/runtime/themes/kanagawa.toml index 16e27362f918..a7d33f3ee5b8 100644 --- a/runtime/themes/kanagawa.toml +++ b/runtime/themes/kanagawa.toml @@ -14,9 +14,8 @@ "ui.linenr" = { fg = "sumiInk4" } "ui.linenr.selected" = { fg = "roninYellow" } +"ui.virtual" = "sumiInk4" "ui.virtual.ruler" = { bg = "sumiInk2" } -"ui.virtual.whitespace" = "waveBlue1" -"ui.virtual.indent-guide" = "sumiInk4" "ui.statusline" = { fg = "oldWhite", bg = "sumiInk0" } "ui.statusline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } @@ -24,8 +23,9 @@ "ui.statusline.insert" = { fg = "sumiInk0", bg = "autumnGreen", modifiers = ["bold"] } "ui.statusline.select" = { fg = "sumiInk0", bg = "oniViolet", modifiers = ["bold"] } -"ui.bufferline" = { fg = "oldWhite", bg = "sumiInk0" } -"ui.bufferline.inactive" = { fg = "fujiGray", bg = "sumiInk0" } +"ui.bufferline" = { fg = "fujiGray", bg = "sumiInk0" } +"ui.bufferline.active" = { fg = "oldWhite", bg = "sumiInk0" } +"ui.bufferline.background" = { bg = "sumiInk0" } "ui.popup" = { fg = "fujiWhite", bg = "sumiInk0" } "ui.window" = { fg = "sumiInk0" } @@ -33,14 +33,16 @@ "ui.text" = "fujiWhite" "ui.text.focus" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } -"ui.cursor" = { fg = "waveBlue1", bg = "fujiWhite"} -"ui.cursor.primary" = { fg = "waveBlue1", bg = "seaFoam" } -"ui.cursor.match" = { fg = "seaFoam", modifiers = ["bold"] } +"ui.cursor" = { fg = "waveBlue1", bg = "waveAqua2"} +"ui.cursor.primary" = { fg = "waveBlue1", bg = "fujiWhite" } +"ui.cursor.match" = { fg = "waveRed", modifiers = ["bold"] } "ui.highlight" = { fg = "fujiWhite", bg = "waveBlue2" } -"ui.menu" = { fg = "fujiWhite", bg = "sumiInk0" } -"ui.menu.selected" = { fg = "fujiWhite", bg = "waveBlue1", modifiers = ["bold"] } +"ui.menu" = { fg = "fujiWhite", bg = "waveBlue1" } +"ui.menu.selected" = { fg = "fujiWhite", bg = "waveBlue2", modifiers = ["bold"] } +"ui.menu.scroll" = { fg = "oldWhite", bg = "waveBlue1" } "ui.cursorline.primary" = { bg = "sumiInk3"} +"ui.cursorcolumn.primary" = { bg = "sumiInk3" } "diagnostic.error" = { underline = { color = "samuraiRed", style = "curl" } } "diagnostic.warning" = { underline = { color = "roninYellow", style = "curl" } } @@ -58,12 +60,16 @@ hint = "dragonBlue" "diff.delta" = "autumnYellow" ## Syntax highlighting +"attribute" = "waveRed" "type" = "waveAqua2" +"constructor" = "springBlue" "constant" = "surimiOrange" "constant.numeric" = "sakuraPink" "constant.character.escape" = "springBlue" "string" = "springGreen" "string.regexp" = "boatYellow2" +"string.special.url" = "springBlue" +"string.special.symbol" = "oniViolet" "comment" = "fujiGray" "variable" = "fujiWhite" "variable.builtin" = "waveRed" @@ -71,37 +77,36 @@ hint = "dragonBlue" "variable.other.member" = "carpYellow" "label" = "springBlue" "punctuation" = "springViolet2" -"punctuation.delimiter" = "springViolet2" -"punctuation.bracket" = "springViolet2" "keyword" = "oniViolet" +"keyword.control.return" = "peachRed" +"keyword.control.exception" = "peachRed" "keyword.directive" = "peachRed" "operator" = "boatYellow2" "function" = "crystalBlue" -"function.builtin" = "peachRed" +"function.builtin" = "springBlue" "function.macro" = "waveRed" -"tag" = "springBlue" +"tag" = "waveAqua2" "namespace" = "surimiOrange" -"attribute" = "peachRed" -"constructor" = "springBlue" -"module" = "waveAqua2" "special" = "peachRed" ## Markup modifiers -"markup.heading.marker" = "fujiGray" +"markup.heading.marker" = "springViolet2" "markup.heading.1" = { fg = "surimiOrange", modifiers = ["bold"] } "markup.heading.2" = { fg = "carpYellow", modifiers = ["bold"] } "markup.heading.3" = { fg = "waveAqua2", modifiers = ["bold"] } -"markup.heading.4" = { fg = "springGreen", modifiers = ["bold"] } -"markup.heading.5" = { fg = "waveRed", modifiers = ["bold"] } -"markup.heading.6" = { fg = "autumnRed", modifiers = ["bold"] } -"markup.list" = "oniViolet" +"markup.heading.4" = { fg = "lightBlue", modifiers = ["bold"] } +"markup.heading.5" = { fg = "oniViolet", modifiers = ["bold"] } +"markup.heading.6" = { fg = "springViolet1", modifiers = ["bold"] } +"markup.list.numbered" = "sakuraPink" +"markup.list.unnumbered" = "waveRed" "markup.bold" = { modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { fg = "springBlue", modifiers = ["underlined"] } "markup.link.text" = "crystalBlue" -"markup.quote" = "seaFoam" -"markup.raw" = "seaFoam" +"markup.link.url" = { fg = "springBlue", underline.style = "line" } +"markup.link.label" = "surimiOrange" +"markup.quote" = "springViolet1" +"markup.raw" = "springGreen" [palette] seaFoam = "#C7CCD1" # custom lighter foreground diff --git a/runtime/themes/onelight.toml b/runtime/themes/onelight.toml index 7c2669793c2d..c2d4e16e3cac 100644 --- a/runtime/themes/onelight.toml +++ b/runtime/themes/onelight.toml @@ -1,129 +1,184 @@ -# One Light # Author : erasin "attribute" = { fg = "yellow" } -"comment" = { fg = "gray", modifiers = ["italic"] } +"constructor" = { fg = "brown" } +"label" = { fg = "cyan" } +"operator" = { fg = "red" } +"tag" = { fg = "cyan" } +"namespace" = { fg = "blue" } +"special" = { fg = "deep-purple" } +"property" = { fg = "purple" } +"module" = { fg = "cyan" } -"constant" = { fg = "cyan" } +"type" = { fg = "gold" } +"type.builtin" = { fg = "light-blue" } +"type.enum" = { fg = "cyan" } +"type.enum.variant" = { fg = "cyan" } + +"constant" = { fg = "cyan", modifiers = ["bold"] } +"constant.builtin" = { fg = "deep-purple" } +"constant.builtin.boolean" = { fg = "deep-purple" } +"constant.character" = { fg = "green" } +"constant.character.escape" = { fg = "brown" } "constant.numeric" = { fg = "gold" } -"constant.builtin" = { fg = "gold" } -"constant.character.escape" = { fg = "gold" } +"constant.numeric.integer" = { fg = "gold" } +"constant.numeric.float" = { fg = "gold" } -"constructor" = { fg = "yellow" } +"string" = { fg = "green" } +"string.regexp" = { fg = "purple" } +"string.special" = { fg = "green" } +"string.special.path" = { fg = "blue" } +"string.special.url" = { fg = "light-blue" } +"string.special.symbol" = { fg = "pink" } + +"comment" = { fg = "grey", modifiers = ["italic"] } +"comment.line" = { fg = "grey", modifiers = ["italic"] } +"comment.block" = { fg = "grey", modifiers = ["italic"] } +"comment.block.documentation" = { fg = "grey", modifiers = ["italic"] } + +# "variable" = { fg = "black" } +"variable.builtin" = { fg = "light-blue" } +"variable.parameter" = { fg = "red" } +"variable.other" = { fg = "pink" } +"variable.other.member" = { fg = "pink" } -"function" = { fg = "blue" } -"function.builtin" = { fg = "cyan" } -"function.macro" = { fg = "red" } +"punctuation" = { fg = "black" } +"punctuation.delimiter" = { fg = "purple" } +"punctuation.bracket" = { fg = "brown" } +"punctuation.special" = { fg = "brown" } "keyword" = { fg = "purple" } -"keyword.function" = { fg = "purple" } "keyword.control" = { fg = "purple" } -"keyword.control.import" = { fg = "purple" } +"keyword.control.conditional" = { fg = "red", modifiers = ["bold"] } +"keyword.control.repeat" = { fg = "pink", modifiers = ["bold"] } +"keyword.control.import" = { fg = "red" } +"keyword.control.return" = { fg = "deep-purple", modifiers = ["bold"] } +"keyword.control.exception" = { fg = "purple" } +"keyword.operator" = { fg = "red" } "keyword.directive" = { fg = "purple" } -"keyword.operator" = { fg = "purple" } +"keyword.function" = { fg = "purple" } +"keyword.storage" = { fg = "purple" } "keyword.storage.type" = { fg = "purple" } +"keyword.storage.modifier" = { fg = "purple", modifiers = ["bold"] } -"tag" = "cyan" -"label" = { fg = "cyan" } -"namespace" = { fg = "red" } -"operator" = { fg = "red" } -"special" = { fg = "purple" } -"string" = { fg = "green" } -"module" = { fg = "cyan" } - -"type" = { fg = "yellow" } -"type.builtin" = { fg = "purple" } - -"punctuation" = { fg = "gray" } -"punctuation.delimiter" = { fg = "black" } -"punctuation.bracket" = { fg = "gray" } - -"variable" = { fg = "black" } -"variable.builtin" = { fg = "light-blue" } -"variable.parameter" = { fg = "red" } -"variable.other.member" = { fg = "red" } +"function" = { fg = "blue" } +"function.builtin" = { fg = "cyan" } +"function.method" = { fg = "light-blue" } +"function.macro" = { fg = "pink", modifiers = ["bold"] } +"function.special" = { fg = "cyan" } "markup.heading" = { fg = "red" } -"markup.raw" = { fg = "gray" } -"markup.raw.inline" = { fg = "green", bg = "grey-200" } -"markup.bold" = { fg = "yellow", modifiers = ["bold"] } -"markup.italic" = { fg = "purple", modifiers = ["italic"] } -"markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.list" = { fg = "light-blue" } -"markup.quote" = { fg = "gray" } -"markup.link.url" = { fg = "cyan", modifiers = ["underlined"] } -"markup.link.text" = { fg = "light-blue" } +"markup.heading.marker" = { fg = "red" } "markup.heading.1" = { fg = "red", modifiers = ["bold"] } -"markup.heading.2" = { fg = "gold", modifiers = ["bold"] } +"markup.heading.2" = { fg = "gold", modifiers = [ + "bold", +], underline = { style = "line" } } "markup.heading.3" = { fg = "yellow", modifiers = ["bold"] } "markup.heading.4" = { fg = "green", modifiers = ["bold"] } "markup.heading.5" = { fg = "blue", modifiers = ["bold"] } "markup.heading.6" = { fg = "purple", modifiers = ["bold"] } +"markup.list" = { fg = "light-blue" } +"markup.list.unnumbered" = { fg = "light-blue" } +"markup.list.numbered" = { fg = "light-blue" } +"markup.bold" = { fg = "yellow", modifiers = ["bold"] } +"markup.italic" = { fg = "purple", modifiers = ["italic"] } +"markup.link" = { fg = "light-blue" } +"markup.link.url" = { fg = "cyan", modifiers = ["underlined"] } +"markup.link.text" = { fg = "light-blue" } +"markup.quote" = { fg = "grey" } +"markup.raw" = { fg = "brown" } +"markup.raw.inline" = { fg = "green" } +"markup.raw.block" = { fg = "grey" } -"diff.plus" = "green" -"diff.delta" = "gold" -"diff.minus" = "red" - -"diagnostic.info".underline = { color = "blue", style = "curl" } -"diagnostic.hint".underline = { color = "green", style = "curl" } -"diagnostic.warning".underline = { color = "yellow", style = "curl" } -"diagnostic.error".underline = { color = "red", style = "curl" } - -"info" = { fg = "blue", modifiers = ["bold"] } -"hint" = { fg = "green", modifiers = ["bold"] } -"warning" = { fg = "yellow", modifiers = ["bold"] } -"error" = { fg = "red", modifiers = ["bold"] } +"diff" = { fg = "red" } +"diff.plus" = { fg = "green" } +"diff.minus" = { fg = "red" } +"diff.delta" = { fg = "cyan" } +"diff.delta.moved" = { fg = "cyan" } "ui.background" = { bg = "white" } +"ui.background.separator" = { bg = "white" } -"ui.cursor" = { fg = "white", bg = "gray" } +"ui.cursor" = { fg = "white", bg = "grey" } +"ui.cursor.normal" = { fg = "white", bg = "grey" } +"ui.cursor.insert" = { fg = "white", bg = "grey" } +"ui.cursor.select" = { fg = "white", bg = "grey" } +"ui.cursor.match" = { bg = "light-white", modifiers = ["bold"] } "ui.cursor.primary" = { fg = "white", bg = "black" } -"ui.cursor.match" = { bg = "light-gray" } +"ui.cursor.primary.normal" = { fg = "white", bg = "black" } +"ui.cursor.primary.insert" = { fg = "red", bg = "black" } +"ui.cursor.primary.select" = { fg = "white", bg = "black" } -"ui.cursorline.primary" = { fg = "white", bg = "grey-100" } -# "ui.cursorline.secondary" = { fg = "white", bg = "grey-200" } - -"ui.highlight" = { bg = "light-white" } - -"ui.selection" = { bg = "light-white", modifiers = ["dim"] } -"ui.selection.primary" = { bg = "light-white" } - -"ui.virtual" = { fg = "light-white" } -"ui.virtual.indent-guide" = { fg = "grey-500" } -"ui.virtual.ruler" = { bg = "light-white" } -"ui.virtual.whitespace" = { fg = "light-white" } +"ui.gutter" = { fg = "grey-500" } +"ui.gutter.selected" = { fg = "black" } "ui.linenr" = { fg = "grey-500" } -"ui.linenr.selected" = { fg = "black", modifiers = ["dim"] } +"ui.linenr.selected" = { fg = "black", modifiers = ["bold"] } "ui.statusline" = { fg = "black", bg = "light-white" } -"ui.statusline.inactive" = { fg = "gray", bg = "light-white" } +"ui.statusline.inactive" = { fg = "grey", bg = "grey-200" } "ui.statusline.normal" = { fg = "light-white", bg = "light-blue" } "ui.statusline.insert" = { fg = "light-white", bg = "green" } "ui.statusline.select" = { fg = "light-white", bg = "purple" } +"ui.popup" = { fg = "black", bg = "grey-200" } +"ui.popup.info" = { fg = "black", bg = "grey-200" } +"ui.window" = { fg = "grey-500", bg = "grey-100" } +"ui.help" = { fg = "black", bg = "grey-200" } + "ui.text" = { fg = "black" } "ui.text.focus" = { fg = "red", bg = "light-white", modifiers = ["bold"] } +"ui.text.inactive" = { fg = "grey" } +"ui.text.info" = { fg = "black" } + +"ui.virtual" = { fg = "light-white" } +"ui.virtual.ruler" = { bg = "light-white" } +"ui.virtual.wrap" = { bg = "light-white" } +"ui.virtual.whitespace" = { fg = "light-white" } +"ui.virtual.indent-guide" = { fg = "grey-500" } +"ui.virtual.inlay-hint" = { fg = "grey" } -"ui.help" = { fg = "black", bg = "grey-200" } -"ui.popup" = { fg = "black", bg = "grey-200" } -"ui.window" = { fg = "black", bg = "light-white" } "ui.menu" = { fg = "black", bg = "light-white" } "ui.menu.selected" = { fg = "white", bg = "light-blue" } +"ui.menu.scroll" = { fg = "light-blue", bg = "white" } + +"ui.selection" = { bg = "light-white", modifiers = ["dim"] } +"ui.selection.primary" = { bg = "light-white" } + +"ui.cursorline.primary" = { fg = "white", bg = "grey-100" } +"ui.cursorline.secondary" = { fg = "white", bg = "grey-200" } + +"ui.cursorcolumn.primary" = { fg = "white", bg = "grey-100" } +"ui.cursorcolumn.secondary" = { fg = "white", bg = "grey-200" } + +"ui.highlight" = { bg = "light-white" } + +"diagnostic.info" = { underline = { color = "blue", style = "dotted" } } +"diagnostic.hint" = { underline = { color = "green", style = "dashed" } } +"diagnostic.warning" = { underline = { color = "yellow", style = "curl" } } +"diagnostic.error" = { underline = { color = "red", style = "curl" } } + +"info" = { fg = "blue", modifiers = ["bold"] } +"hint" = { fg = "green", modifiers = ["bold"] } +"warning" = { fg = "yellow", modifiers = ["bold"] } +"error" = { fg = "red", modifiers = ["bold"] } [palette] white = "#FAFAFA" -yellow = "#A06600" +yellow = "#FF6F00" +gold = "#D35400" +brown = "#4E342E" blue = "#0061FF" -light-blue = "#1877F2" -red = "#DC003F" +light-blue = "#0091EA" +red = "#D50000" +pink = "#C2185B" purple = "#B500A9" +deep-purple = "#651FFF" green = "#24A443" -gold = "#D35400" cyan = "#0086C1" black = "#282C34" light-white = "#E3E3E3" -gray = "#5C6370" +grey = "#5C6370" grey-100 = "#F3F3F3" grey-200 = "#EDEDED" grey-500 = "#9E9E9E" diff --git a/runtime/themes/papercolor-dark.toml b/runtime/themes/papercolor-dark.toml index 088658e9ebb3..eaaa36dcf52c 100644 --- a/runtime/themes/papercolor-dark.toml +++ b/runtime/themes/papercolor-dark.toml @@ -7,6 +7,7 @@ "ui.text.focus" = { fg = "selection_background", modifiers = ["bold"]} "ui.selection" = {bg="selection_background", fg="selection_foreground"} "ui.cursorline" = {bg="cursorline_background"} +"ui.highlight" = {bg="cursorline_background"} "ui.statusline" = {bg="paper_bar_bg", fg="regular0"} "ui.statusline.select" = {bg="background", fg="bright7"} "ui.statusline.normal" = {bg="background", fg="bright3"} diff --git a/runtime/themes/papercolor-light.toml b/runtime/themes/papercolor-light.toml index c44c670914a0..63671e1b3ff7 100644 --- a/runtime/themes/papercolor-light.toml +++ b/runtime/themes/papercolor-light.toml @@ -6,6 +6,7 @@ "ui.text" = "foreground" "ui.text.focus" = { fg = "selection_background", modifiers = ["bold"]} "ui.selection" = {bg="selection_background", fg="selection_foreground"} +"ui.highlight" = {bg="cursorline_background"} "ui.cursorline" = {bg="cursorline_background"} "ui.statusline" = {bg="paper_bar_bg", fg="regular0"} "ui.statusline.select" = {bg="background", fg="bright7"} diff --git a/runtime/themes/pop-dark.toml b/runtime/themes/pop-dark.toml index 57db2d2c5e93..17621d2af330 100644 --- a/runtime/themes/pop-dark.toml +++ b/runtime/themes/pop-dark.toml @@ -28,8 +28,11 @@ error = { fg = 'brownD', bg = 'redE', modifiers = ['bold'] } 'ui.linenr' = { bg = 'brownU', fg = 'greyL' } 'ui.linenr.selected' = { fg = 'orangeH' } 'ui.cursorline' = { bg = 'brownH' } -'ui.statusline' = { fg = 'greyT', bg = 'brownD' } +'ui.statusline' = { fg = 'greyT', bg = 'brownU' } 'ui.statusline.inactive' = { fg = 'greyT', bg = 'brownN' } +'ui.statusline.normal' = { fg = 'greyT', bg = 'brownD', modifiers = ['bold'] } +'ui.statusline.select' = { bg = 'blueL', fg = 'brownD', modifiers = ['bold'] } +'ui.statusline.insert' = { bg = 'orangeL', fg = 'brownD', modifiers = ['bold'] } 'ui.help' = { fg = 'greyT', bg = 'brownD' } 'ui.highlight' = { bg = 'brownH' } 'ui.virtual' = { fg = 'brownV' } diff --git a/rust-toolchain.toml b/rust-toolchain.toml index ace4f5f96e55..2abc5665236b 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.63.0" +channel = "1.65.0" components = ["rustfmt", "rust-src"]