diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index d7d7d47e0..2c6267b7c 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -14,7 +14,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: helix-editor/rust-toolchain@v1 with: @@ -34,7 +34,7 @@ jobs: HELIX_LOG_LEVEL: info steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@1.65 @@ -63,7 +63,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@1.65 @@ -88,7 +88,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@1.65 diff --git a/.github/workflows/cachix.yml b/.github/workflows/cachix.yml index 7adb53269..f5408e4a0 100644 --- a/.github/workflows/cachix.yml +++ b/.github/workflows/cachix.yml @@ -11,10 +11,10 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install nix - uses: cachix/install-nix-action@v22 + uses: cachix/install-nix-action@v23 - name: Authenticate with Cachix uses: cachix/cachix-action@v12 diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml index 223f8450f..b288e0f66 100644 --- a/.github/workflows/gh-pages.yml +++ b/.github/workflows/gh-pages.yml @@ -11,7 +11,7 @@ jobs: deploy: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 + - uses: actions/checkout@v4 - name: Setup mdBook uses: peaceiris/actions-mdbook@v1 diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 785a5a4df..b645d94c6 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -23,7 +23,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Install stable toolchain uses: dtolnay/rust-toolchain@stable @@ -103,7 +103,7 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - name: Download grammars uses: actions/download-artifact@v3 @@ -231,7 +231,7 @@ jobs: runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v4 - uses: actions/download-artifact@v3 diff --git a/Cargo.lock b/Cargo.lock index 09b170ffa..dfa87f6ea 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -99,9 +99,9 @@ dependencies = [ [[package]] name = "aho-corasick" -version = "1.0.4" +version = "1.0.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6748e8def348ed4d14996fa801f4122cd763fff530258cdc03f64b25f89d3a5a" +checksum = "0c378d78423fdad8089616f827526ee33c19f2fddbd5de1629152c9593ba4783" dependencies = [ "memchr", ] @@ -225,9 +225,9 @@ dependencies = [ [[package]] name = "bstr" -version = "1.6.0" +version = "1.6.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6798148dccfbff0fae41c7574d2fa8f1ef3492fba0face179de5d8d447d67b05" +checksum = "4c2f7349907b712260e64b0afe2f84692af14a454be26187d9df565c7f69266a" dependencies = [ "memchr", "regex-automata", @@ -263,9 +263,9 @@ checksum = "14c189c53d098945499cdfa7ecc63567cf3886b3332b312a5b4585d8d3a6a610" [[package]] name = "bytes" -version = "1.4.0" +version = "1.5.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "89b2fd2a0dcf38d7971e2194b6b6eebab45ae01067456a7fd93d5547a61b70be" +checksum = "a2bd12c1caf447e69cd4528f47f94d203fd2582878ecb9e9465484c4148a8223" [[package]] name = "cassowary" @@ -301,14 +301,14 @@ dependencies = [ [[package]] name = "chrono" -version = "0.4.26" +version = "0.4.30" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ec837a71355b28f6556dbd569b37b3f363091c0bd4b2e735674521b4c5fd9bc5" +checksum = "defd4e7873dbddba6c7c91e199c7fcb946abc4a6a4ac3195400bcfb01b5de877" dependencies = [ "android-tzdata", "iana-time-zone", "num-traits", - "winapi", + "windows-targets", ] [[package]] @@ -394,6 +394,12 @@ version = "1.5.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "69f3b219d28b6e3b4ac87bc1fc522e0803ab22e055da177bff0068c4150c61a6" +[[package]] +name = "cov-mark" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9ffa3d3e0138386cd4361f63537765cac7ee40698028844635a54495a92f67f3" + [[package]] name = "crc32fast" version = "1.3.2" @@ -413,6 +419,30 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-deque" +version = "0.8.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ce6fd6f855243022dcecf8702fef0c297d4338e226845fe067f6341ad9fa0cef" +dependencies = [ + "cfg-if", + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae211234986c545741a7dc064309f67ee1e5ad243d0e48335adc0484d960bcc7" +dependencies = [ + "autocfg", + "cfg-if", + "crossbeam-utils", + "memoffset", + "scopeguard", +] + [[package]] name = "crossbeam-utils" version = "0.8.16" @@ -503,9 +533,9 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" [[package]] name = "errno" -version = "0.3.2" +version = "0.3.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b30f669a7961ef1631673d2766cc92f52d64f7ef354d4fe0ddfd30ed52f0f4f" +checksum = "136526188508e25c6fef639d7927dfb3e0e3084488bf202267829cf7fc23dbdd" dependencies = [ "errno-dragonfly", "libc", @@ -631,7 +661,7 @@ checksum = "89ca545a94061b6365f2c7355b4b32bd20df3ff95f02da9329b34ccc3bd6ee72" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.33", ] [[package]] @@ -654,15 +684,6 @@ dependencies = [ "slab", ] -[[package]] -name = "fuzzy-matcher" -version = "0.3.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "54614a3312934d066701a80f20f15fa3b56d67ac7722b39eea5b4c9dd1d66c94" -dependencies = [ - "thread_local", -] - [[package]] name = "fxhash" version = "0.2.1" @@ -871,9 +892,9 @@ dependencies = [ [[package]] name = "gix-date" -version = "0.7.3" +version = "0.7.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "01e476b4e156f6044d35bf1ce2079d97b7207515cfb5a2bb6fcd489bb697d700" +checksum = "0a825babda995d788e30d306a49dacd1e93d5f5d33d53c7682d0347cef40333c" dependencies = [ "bstr", "itoa", @@ -1333,7 +1354,7 @@ version = "0.4.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "759c97c1e17c55525b57192c06a267cda0ac5210b222d6b82189a2338fa1c13d" dependencies = [ - "aho-corasick 1.0.4", + "aho-corasick 1.0.5", "bstr", "fnv", "log", @@ -1421,7 +1442,9 @@ dependencies = [ "imara-diff", "indoc", "log", + "nucleo", "once_cell", + "parking_lot", "quickcheck", "regex", "ropey", @@ -1454,6 +1477,14 @@ dependencies = [ "which", ] +[[package]] +name = "helix-event" +version = "0.6.0" +dependencies = [ + "parking_lot", + "tokio", +] + [[package]] name = "helix-loader" version = "0.6.0" @@ -1510,11 +1541,11 @@ dependencies = [ "crossterm", "fern", "futures-util", - "fuzzy-matcher", "grep-regex", "grep-searcher", "helix-core", "helix-dap", + "helix-event", "helix-loader", "helix-lsp", "helix-tui", @@ -1524,6 +1555,7 @@ dependencies = [ "indoc", "libc", "log", + "nucleo", "once_cell", "pulldown-cmark", "serde", @@ -1564,6 +1596,7 @@ dependencies = [ "arc-swap", "gix", "helix-core", + "helix-event", "imara-diff", "log", "parking_lot", @@ -1584,6 +1617,7 @@ dependencies = [ "futures-util", "helix-core", "helix-dap", + "helix-event", "helix-loader", "helix-lsp", "helix-tui", @@ -1795,9 +1829,9 @@ checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646" [[package]] name = "libc" -version = "0.2.147" +version = "0.2.148" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4668fb0ea861c1df094127ac5f1da3409a82116a4ba74fca2e58ef927159bb3" +checksum = "9cdc71e17332e86d2e1d38c1f99edcb6288ee11b815fb1a4b049eaa2114d369b" [[package]] name = "libloading" @@ -1821,9 +1855,9 @@ dependencies = [ [[package]] name = "linux-raw-sys" -version = "0.4.5" +version = "0.4.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57bcfdad1b858c2db7c38303a6d2ad4dfaf5eb53dfeb0910128b2c26d6158503" +checksum = "1a9bad9f94746442c783ca431b22403b519cd7fbeed0533fdd6328b2f2212128" [[package]] name = "lock_api" @@ -1879,9 +1913,9 @@ dependencies = [ [[package]] name = "memchr" -version = "2.5.0" +version = "2.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2dffe52ecf27772e601905b7522cb4ef790d2cc203488bbd0e2fe85fcb74566d" +checksum = "8f232d6ef707e1956a43342693d2a31e72989554d58299d7a88738cc95b0d35c" [[package]] name = "memmap2" @@ -1901,6 +1935,15 @@ dependencies = [ "libc", ] +[[package]] +name = "memoffset" +version = "0.9.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" +dependencies = [ + "autocfg", +] + [[package]] name = "minimal-lexical" version = "0.2.1" @@ -1938,6 +1981,28 @@ dependencies = [ "minimal-lexical", ] +[[package]] +name = "nucleo" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ae5331f4bcce475cf28cb29c95366c3091af4b0aa7703f1a6bc858f29718fdf3" +dependencies = [ + "nucleo-matcher", + "parking_lot", + "rayon", +] + +[[package]] +name = "nucleo-matcher" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b702b402fe286162d1f00b552a046ce74365d2ac473a2607ff36ba650f9bd57" +dependencies = [ + "cov-mark", + "memchr", + "unicode-segmentation", +] + [[package]] name = "num" version = "0.4.1" @@ -2036,9 +2101,9 @@ dependencies = [ [[package]] name = "object" -version = "0.32.0" +version = "0.32.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "77ac5bbd07aea88c60a577a1ce218075ffd59208b2d7ca97adf9bfc5aeb21ebe" +checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0" dependencies = [ "memchr", ] @@ -2086,9 +2151,9 @@ checksum = "9b2a4787296e9989611394c33f193f676704af1686e70b8f8033ab5ba9a35a94" [[package]] name = "pin-project-lite" -version = "0.2.12" +version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12cc1b0bf1727a77a54b6654e7b5f1af8604923edc8b81885f8ec92f9e3f0a05" +checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" [[package]] name = "pin-utils" @@ -2115,9 +2180,9 @@ dependencies = [ [[package]] name = "proc-macro2" -version = "1.0.66" +version = "1.0.67" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "18fb31db3f9bddb2ea821cde30a9f70117e3f119938b5ee630b7403aa6e2ead9" +checksum = "3d433d9f1a3e8c1263d9456598b16fec66f4acc9a74dacffd35c7bb09b3a1328" dependencies = [ "unicode-ident", ] @@ -2212,6 +2277,28 @@ dependencies = [ "rand_core", ] +[[package]] +name = "rayon" +version = "1.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1d2df5196e37bcc87abebc0053e20787d73847bb33134a69841207dd0a47f03b" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.11.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4b8f95bd6966f5c87776639160a66bd8ab9895d9d4ab01ddba9fc60661aebe8d" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-utils", + "num_cpus", +] + [[package]] name = "redox_syscall" version = "0.3.5" @@ -2223,25 +2310,25 @@ dependencies = [ [[package]] name = "regex" -version = "1.9.3" +version = "1.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81bc1d4caf89fac26a70747fe603c130093b53c773888797a6329091246d651a" +checksum = "697061221ea1b4a94a624f67d0ae2bfe4e22b8a17b6a192afb11046542cc8c47" dependencies = [ - "aho-corasick 1.0.4", + "aho-corasick 1.0.5", "memchr", "regex-automata", - "regex-syntax 0.7.4", + "regex-syntax 0.7.5", ] [[package]] name = "regex-automata" -version = "0.3.6" +version = "0.3.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fed1ceff11a1dddaee50c9dc8e4938bd106e9d89ae372f192311e7da498e3b69" +checksum = "c2f401f4955220693b56f8ec66ee9c78abffd8d1c4f23dc41a23839eb88f0795" dependencies = [ - "aho-corasick 1.0.4", + "aho-corasick 1.0.5", "memchr", - "regex-syntax 0.7.4", + "regex-syntax 0.7.5", ] [[package]] @@ -2252,9 +2339,9 @@ checksum = "f162c6dd7b008981e4d40210aca20b4bd0f9b60ca9271061b07f78537722f2e1" [[package]] name = "regex-syntax" -version = "0.7.4" +version = "0.7.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e5ea92a5b6195c6ef2a0295ea818b312502c6fc94dde986c5553242e18fd4ce2" +checksum = "dbb5fb1acd8a1a18b3dd5be62d25485eb770e05afb408a9627d14d451bae12da" [[package]] name = "repr_offset" @@ -2292,9 +2379,9 @@ dependencies = [ [[package]] name = "rustix" -version = "0.38.8" +version = "0.38.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19ed4fa021d81c8392ce04db050a3da9a60299050b7ae1cf482d862b54a7218f" +checksum = "d7db8590df6dfcd144d22afd1b83b36c21a18d7cbc1dc4bb5295a8712e9eb662" dependencies = [ "bitflags 2.4.0", "errno", @@ -2332,29 +2419,29 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918" [[package]] name = "serde" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "be9b6f69f1dfd54c3b568ffa45c310d6973a5e5148fd40cf515acaf38cf5bc31" +checksum = "cf9e0fcba69a370eed61bcf2b728575f726b50b55cba78064753d708ddc7549e" dependencies = [ "serde_derive", ] [[package]] name = "serde_derive" -version = "1.0.185" +version = "1.0.188" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc59dfdcbad1437773485e0367fea4b090a2e0a16d9ffc46af47764536a298ec" +checksum = "4eca7ac642d82aa35b60049a6eccb4be6be75e599bd2e9adb5f875a737654af2" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.33", ] [[package]] name = "serde_json" -version = "1.0.105" +version = "1.0.107" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "693151e1ac27563d6dbcec9dee9fbd5da8539b20fa14ad3752b2e6d363ace360" +checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65" dependencies = [ "itoa", "ryu", @@ -2369,7 +2456,7 @@ checksum = "8725e1dfadb3a50f7e5ce0b1a540466f6ed3fe7a0fca2ac2b8b831d31316bd00" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.33", ] [[package]] @@ -2482,9 +2569,9 @@ checksum = "f67ad224767faa3c7d8b6d91985b78e70a1324408abcb1cfcc2be4c06bc06043" [[package]] name = "socket2" -version = "0.5.3" +version = "0.5.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2538b18701741680e0322a2302176d3253a35388e2e62f172f64f4f16605f877" +checksum = "4031e820eb552adee9295814c0ced9e5cf38ddf1e8b7d566d6de8e2538ea989e" dependencies = [ "libc", "windows-sys", @@ -2538,7 +2625,7 @@ version = "0.4.0" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.33", ] [[package]] @@ -2585,9 +2672,9 @@ dependencies = [ [[package]] name = "syn" -version = "2.0.29" +version = "2.0.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c324c494eba9d92503e6f1ef2e6df781e78f6a7705a0202d9801b198807d518a" +checksum = "9caece70c63bfba29ec2fed841a09851b14a235c60010fa4de58089b6c025668" dependencies = [ "proc-macro2", "quote", @@ -2638,22 +2725,22 @@ dependencies = [ [[package]] name = "thiserror" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "97a802ec30afc17eee47b2855fc72e0c4cd62be9b4efe6591edde0ec5bd68d8f" +checksum = "9d6d7a740b8a666a7e828dd00da9c0dc290dff53154ea77ac109281de90589b7" dependencies = [ "thiserror-impl", ] [[package]] name = "thiserror-impl" -version = "1.0.47" +version = "1.0.48" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6bb623b56e39ab7dcd4b1b98bb6c8f8d907ed255b18de254088016b27a8ee19b" +checksum = "49922ecae66cc8a249b77e68d1d0623c1b2c514f0060c27cdc68bd62a1219d35" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.33", ] [[package]] @@ -2677,9 +2764,9 @@ dependencies = [ [[package]] name = "time" -version = "0.3.27" +version = "0.3.28" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0bb39ee79a6d8de55f48f2293a830e040392f1c5f16e336bdd1788cd0aadce07" +checksum = "17f6bb557fd245c28e6411aa56b6403c689ad95061f50e4be16c274e70a17e48" dependencies = [ "deranged", "itoa", @@ -2698,9 +2785,9 @@ checksum = "7300fbefb4dadc1af235a9cef3737cea692a9d97e1b9cbcd4ebdae6f8868e6fb" [[package]] name = "time-macros" -version = "0.2.13" +version = "0.2.14" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "733d258752e9303d392b94b75230d07b0b9c489350c69b851fc6c065fde3e8f9" +checksum = "1a942f44339478ef67935ab2bbaec2fb0322496cf3cbe84b261e06ac3814c572" dependencies = [ "time-core", ] @@ -2747,7 +2834,7 @@ checksum = "630bdcf245f78637c13ec01ffae6187cca34625e8c63150d424b59e55af2675e" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.33", ] [[package]] @@ -2763,9 +2850,9 @@ dependencies = [ [[package]] name = "toml" -version = "0.7.6" +version = "0.7.8" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c17e963a819c331dcacd7ab957d80bc2b9a9c1e71c804826d2f283dd65306542" +checksum = "dd79e69d3b627db300ff956027cc6c3798cef26d22526befdfcd12feeb6d2257" dependencies = [ "serde", "serde_spanned", @@ -2784,9 +2871,9 @@ dependencies = [ [[package]] name = "toml_edit" -version = "0.19.14" +version = "0.19.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f8123f27e969974a3dfba720fdb560be359f57b44302d280ba72e76a74480e8a" +checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421" dependencies = [ "indexmap 2.0.0", "serde", @@ -2860,9 +2947,9 @@ checksum = "2281c8c1d221438e373249e065ca4989c4c36952c211ff21a0ee91c44a3869e7" [[package]] name = "unicode-ident" -version = "1.0.11" +version = "1.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "301abaae475aa91687eb82514b328ab47a211a533026cb25fc3e519b86adfc3c" +checksum = "3354b9ac3fae1ff6755cb6db53683adb661634f67557942dea4facebec0fee4b" [[package]] name = "unicode-linebreak" @@ -2893,9 +2980,9 @@ checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b" [[package]] name = "url" -version = "2.4.0" +version = "2.4.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50bff7831e19200a85b17131d085c25d7811bc4e186efdaf54bbd132994a88cb" +checksum = "143b538f18257fac9cad154828a57c6bf5157e1aa604d4816b5995bf6de87ae5" dependencies = [ "form_urlencoded", "idna", @@ -2911,9 +2998,9 @@ checksum = "49874b5167b65d7193b8aba1567f5c7d93d001cafc34600cee003eda787e483f" [[package]] name = "walkdir" -version = "2.3.3" +version = "2.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "36df944cda56c7d8d8b7496af378e6b16de9284591917d307c9b4d313c44e698" +checksum = "d71d857dc86794ca4c280d616f7da00d2dbfd8cd788846559a6813e6aa4b54ee" dependencies = [ "same-file", "winapi-util", @@ -2946,7 +3033,7 @@ dependencies = [ "once_cell", "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.33", "wasm-bindgen-shared", ] @@ -2968,7 +3055,7 @@ checksum = "54681b18a46765f095758388f2d0cf16eb8d4169b639ab575a8f5693af210c7b" dependencies = [ "proc-macro2", "quote", - "syn 2.0.29", + "syn 2.0.33", "wasm-bindgen-backend", "wasm-bindgen-shared", ] @@ -2987,13 +3074,14 @@ checksum = "323f4da9523e9a669e1eaf9c6e763892769b1d38c623913647bfdc1532fe4549" [[package]] name = "which" -version = "4.4.0" +version = "4.4.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2441c784c52b289a054b7201fc93253e288f094e2f4be9058343127c4226a269" +checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7" dependencies = [ "either", - "libc", + "home", "once_cell", + "rustix", ] [[package]] @@ -3104,9 +3192,9 @@ checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" [[package]] name = "winnow" -version = "0.5.14" +version = "0.5.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d09770118a7eb1ccaf4a594a221334119a44a814fcb0d31c5b85e83e97227a97" +checksum = "7c2e3184b9c4e92ad5167ca73039d0c42476302ab603e2fec4487511f38ccefc" dependencies = [ "memchr", ] diff --git a/Cargo.toml b/Cargo.toml index 8cd08df44..6dd59bf4f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,6 +5,7 @@ members = [ "helix-term", "helix-tui", "helix-lsp", + "helix-event", "helix-dap", "helix-loader", "helix-vcs", @@ -19,6 +20,7 @@ default-members = [ [workspace.dependencies] steel-core = { path = "../../steel/crates/steel-core", version = "0.5.0", features = ["modules", "anyhow", "dylibs", "colors"] } tree-sitter = { version = "0.20", git = "https://github.com/tree-sitter/tree-sitter", rev = "ab09ae20d640711174b8da8a654f6b3dec93da1a" } +nucleo = "0.2.0" [profile.release] lto = "thin" diff --git a/book/custom.css b/book/custom.css index 0e812090e..4b039125e 100644 --- a/book/custom.css +++ b/book/custom.css @@ -164,7 +164,7 @@ code.hljs { --searchresults-header-fg: #5f5f71; --searchresults-border-color: #5c5c68; --searchresults-li-bg: #242430; - --search-mark-bg: #acff5; + --search-mark-bg: #a2cff5; } .colibri .content .header { diff --git a/book/src/configuration.md b/book/src/configuration.md index eb2cf473c..3b78481e3 100644 --- a/book/src/configuration.md +++ b/book/src/configuration.md @@ -64,6 +64,7 @@ Its settings will be merged with the configuration directory `config.toml` and t | `text-width` | Maximum line length. Used for the `:reflow` command and soft-wrapping if `soft-wrap.wrap-at-text-width` is set | `80` | | `workspace-lsp-roots` | Directories relative to the workspace root that are treated as LSP roots. Should only be set in `.helix/config.toml` | `[]` | | `default-line-ending` | The line ending to use for new documents. Can be `native`, `lf`, `crlf`, `ff`, `cr` or `nel`. `native` uses the platform's native line ending (`crlf` on Windows, otherwise `lf`). | `native` | +| `insert-final-newline` | Whether to automatically insert a trailing line-ending on write if missing | `true` | ### `[editor.statusline]` Section diff --git a/book/src/generated/lang-support.md b/book/src/generated/lang-support.md index 1287c11ff..c1b0acfdb 100644 --- a/book/src/generated/lang-support.md +++ b/book/src/generated/lang-support.md @@ -45,6 +45,7 @@ | fortran | ✓ | | ✓ | `fortls` | | fsharp | ✓ | | | `fsautocomplete` | | gdscript | ✓ | ✓ | ✓ | | +| gemini | ✓ | | | | | git-attributes | ✓ | | | | | git-commit | ✓ | ✓ | | | | git-config | ✓ | | | | @@ -111,7 +112,7 @@ | pascal | ✓ | ✓ | | `pasls` | | passwd | ✓ | | | | | pem | ✓ | | | | -| perl | ✓ | | | `perlnavigator` | +| perl | ✓ | ✓ | ✓ | `perlnavigator` | | php | ✓ | ✓ | ✓ | `intelephense` | | po | ✓ | ✓ | | | | pod | ✓ | | | | diff --git a/book/src/generated/typable-cmd.md b/book/src/generated/typable-cmd.md index ce28a3ca6..4b737893d 100644 --- a/book/src/generated/typable-cmd.md +++ b/book/src/generated/typable-cmd.md @@ -55,6 +55,7 @@ | `:lsp-restart` | Restarts the language servers used by the current doc | | `:lsp-stop` | Stops the language servers that are used by the current doc | | `:tree-sitter-scopes` | Display tree sitter scopes, primarily for theming and development. | +| `:tree-sitter-highlight-name` | Display name of tree-sitter highlight scope under the cursor. | | `: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. | | `:debug-eval` | Evaluate expression in current debug context. | @@ -83,3 +84,4 @@ | `:run-shell-command`, `:sh` | Run a shell command | | `:reset-diff-change`, `:diffget`, `:diffg` | Reset the diff change at the cursor position. | | `:clear-register` | Clear given register. If no argument is provided, clear all registers. | +| `:redraw` | Clear and re-render the whole UI | diff --git a/book/src/languages.md b/book/src/languages.md index 5e56a332f..778489f8d 100644 --- a/book/src/languages.md +++ b/book/src/languages.md @@ -127,7 +127,7 @@ These are the available options for a language server. | `environment` | Any environment variables that will be used when starting the language server `{ "KEY1" = "Value1", "KEY2" = "Value2" }` | A `format` sub-table within `config` can be used to pass extra formatting options to -[Document Formatting Requests](https://github.com/microsoft/language-server-protocol/blob/gh-pages/_specifications/specification-3-17.md#document-formatting-request--leftwards_arrow_with_hook). +[Document Formatting Requests](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_formatting). For example, with typescript: ```toml diff --git a/book/src/themes.md b/book/src/themes.md index 41a3fe101..96d7c0eca 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -70,6 +70,7 @@ over it and is merged into the default palette. | Color Name | | --- | +| `default` | | `black` | | `red` | | `green` | diff --git a/docs/CONTRIBUTING.md b/docs/CONTRIBUTING.md index 982b2237e..2be8f77c7 100644 --- a/docs/CONTRIBUTING.md +++ b/docs/CONTRIBUTING.md @@ -29,9 +29,15 @@ files, run cargo xtask docgen ``` -inside the project. We use [xtask][xtask] as an ad-hoc task runner and -thus do not require any dependencies other than `cargo` (You don't have -to `cargo install` anything either). +inside the project. We use [xtask][xtask] as an ad-hoc task runner. + +To preview the book itself, install [mdbook][mdbook]. Then, run + +```shell +mdbook serve book +``` + +and visit [http://localhost:3000](http://localhost:3000). # Testing @@ -58,4 +64,5 @@ The current MSRV and future changes to the MSRV are listed in the [Firefox docum [architecture.md]: ./architecture.md [docs]: https://docs.helix-editor.com/ [xtask]: https://github.com/matklad/cargo-xtask +[mdbook]: https://rust-lang.github.io/mdBook/guide/installation.html [helpers.rs]: ../helix-term/tests/test/helpers.rs diff --git a/flake.nix b/flake.nix index 516fb89ac..e4a1c42c3 100644 --- a/flake.nix +++ b/flake.nix @@ -43,7 +43,6 @@ ".ignore" ".github" ".gitignore" - "logo.svg" "logo_dark.svg" "logo_light.svg" "rust-toolchain.toml" @@ -51,7 +50,6 @@ "runtime" "screenshot.png" "book" - "contrib" "docs" "README.md" "CHANGELOG.md" @@ -123,7 +121,8 @@ then ''$RUSTFLAGS -C link-arg=-fuse-ld=lld -C target-cpu=native -Clink-arg=-Wl,--no-rosegment'' else "$RUSTFLAGS"; rustToolchain = pkgs.pkgsBuildHost.rust-bin.fromRustupToolchainFile ./rust-toolchain.toml; - craneLib = (crane.mkLib pkgs).overrideToolchain rustToolchain; + craneLibMSRV = (crane.mkLib pkgs).overrideToolchain rustToolchain; + craneLibStable = (crane.mkLib pkgs).overrideToolchain pkgs.pkgsBuildHost.rust-bin.stable.latest.default; commonArgs = { inherit stdenv; @@ -135,13 +134,19 @@ doCheck = false; meta.mainProgram = "hx"; } - // craneLib.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;}; - cargoArtifacts = craneLib.buildDepsOnly commonArgs; + // craneLibMSRV.crateNameFromCargoToml {cargoToml = ./helix-term/Cargo.toml;}; + cargoArtifacts = craneLibMSRV.buildDepsOnly commonArgs; in { packages = { - helix-unwrapped = craneLib.buildPackage (commonArgs + helix-unwrapped = craneLibStable.buildPackage (commonArgs // { - inherit cargoArtifacts; + cargoArtifacts = craneLibStable.buildDepsOnly commonArgs; + postInstall = '' + mkdir -p $out/share/applications $out/share/icons/hicolor/scalable/apps $out/share/icons/hicolor/256x256/apps + cp contrib/Helix.desktop $out/share/applications + cp logo.svg $out/share/icons/hicolor/scalable/apps/helix.svg + cp contrib/helix.png $out/share/icons/hicolor/256x256/apps + ''; }); helix = makeOverridableHelix self.packages.${system}.helix-unwrapped {}; default = self.packages.${system}.helix; @@ -151,20 +156,20 @@ # Build the crate itself inherit (self.packages.${system}) helix; - clippy = craneLib.cargoClippy (commonArgs + clippy = craneLibMSRV.cargoClippy (commonArgs // { inherit cargoArtifacts; cargoClippyExtraArgs = "--all-targets -- --deny warnings"; }); - fmt = craneLib.cargoFmt commonArgs; + fmt = craneLibMSRV.cargoFmt commonArgs; - doc = craneLib.cargoDoc (commonArgs + doc = craneLibMSRV.cargoDoc (commonArgs // { inherit cargoArtifacts; }); - test = craneLib.cargoTest (commonArgs + test = craneLibMSRV.cargoTest (commonArgs // { inherit cargoArtifacts; }); diff --git a/helix-core/Cargo.toml b/helix-core/Cargo.toml index 25cfff997..bd785d1ae 100644 --- a/helix-core/Cargo.toml +++ b/helix-core/Cargo.toml @@ -50,6 +50,8 @@ etcetera = "0.8" textwrap = "0.16.0" steel-core = { workspace = true, optional = true } +nucleo.workspace = true +parking_lot = "0.12" [dev-dependencies] quickcheck = { version = "1", default-features = false } diff --git a/helix-core/src/fuzzy.rs b/helix-core/src/fuzzy.rs new file mode 100644 index 000000000..549c6b0e5 --- /dev/null +++ b/helix-core/src/fuzzy.rs @@ -0,0 +1,43 @@ +use std::ops::DerefMut; + +use nucleo::pattern::{Atom, AtomKind, CaseMatching}; +use nucleo::Config; +use parking_lot::Mutex; + +pub struct LazyMutex { + inner: Mutex>, + init: fn() -> T, +} + +impl LazyMutex { + pub const fn new(init: fn() -> T) -> Self { + Self { + inner: Mutex::new(None), + init, + } + } + + pub fn lock(&self) -> impl DerefMut + '_ { + parking_lot::MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init)) + } +} + +pub static MATCHER: LazyMutex = LazyMutex::new(nucleo::Matcher::default); + +/// convenience function to easily fuzzy match +/// on a (relatively small list of inputs). This is not recommended for building a full tui +/// application that can match large numbers of matches as all matching is done on the current +/// thread, effectively blocking the UI +pub fn fuzzy_match>( + pattern: &str, + items: impl IntoIterator, + path: bool, +) -> Vec<(T, u16)> { + let mut matcher = MATCHER.lock(); + matcher.config = Config::DEFAULT; + if path { + matcher.config.set_match_paths(); + } + let pattern = Atom::new(pattern, CaseMatching::Smart, AtomKind::Fuzzy, false); + pattern.match_list(items, &mut matcher) +} diff --git a/helix-core/src/lib.rs b/helix-core/src/lib.rs index c8d22052d..7266cd62a 100644 --- a/helix-core/src/lib.rs +++ b/helix-core/src/lib.rs @@ -7,6 +7,7 @@ pub mod config; pub mod diagnostic; pub mod diff; pub mod doc_formatter; +pub mod fuzzy; pub mod graphemes; pub mod history; pub mod increment; diff --git a/helix-core/src/syntax.rs b/helix-core/src/syntax.rs index bf167c28d..881b45098 100644 --- a/helix-core/src/syntax.rs +++ b/helix-core/src/syntax.rs @@ -962,7 +962,7 @@ impl Syntax { let res = syntax.update(source, source, &ChangeSet::new(source)); if res.is_err() { - log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}"); + log::error!("TS parser failed, disabling TS for the current buffer: {res:?}"); return None; } Some(syntax) diff --git a/helix-event/Cargo.toml b/helix-event/Cargo.toml new file mode 100644 index 000000000..5cd955588 --- /dev/null +++ b/helix-event/Cargo.toml @@ -0,0 +1,15 @@ +[package] +name = "helix-event" +version = "0.6.0" +authors = ["Blaž Hrastnik "] +edition = "2021" +license = "MPL-2.0" +categories = ["editor"] +repository = "https://github.com/helix-editor/helix" +homepage = "https://helix-editor.com" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html + +[dependencies] +tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot"] } +parking_lot = { version = "0.12", features = ["send_guard"] } diff --git a/helix-event/src/lib.rs b/helix-event/src/lib.rs new file mode 100644 index 000000000..9c082b93a --- /dev/null +++ b/helix-event/src/lib.rs @@ -0,0 +1,8 @@ +//! `helix-event` contains systems that allow (often async) communication between +//! different editor components without strongly coupling them. Currently this +//! crate only contains some smaller facilities but the intend is to add more +//! functionality in the future ( like a generic hook system) + +pub use redraw::{lock_frame, redraw_requested, request_redraw, start_frame, RenderLockGuard}; + +mod redraw; diff --git a/helix-event/src/redraw.rs b/helix-event/src/redraw.rs new file mode 100644 index 000000000..a99152238 --- /dev/null +++ b/helix-event/src/redraw.rs @@ -0,0 +1,49 @@ +//! Signals that control when/if the editor redraws + +use std::future::Future; + +use parking_lot::{RwLock, RwLockReadGuard}; +use tokio::sync::Notify; + +/// A `Notify` instance that can be used to (asynchronously) request +/// the editor the render a new frame. +static REDRAW_NOTIFY: Notify = Notify::const_new(); + +/// A `RwLock` that prevents the next frame from being +/// drawn until an exclusive (write) lock can be acquired. +/// This allows asynchsonous tasks to acquire `non-exclusive` +/// locks (read) to prevent the next frame from being drawn +/// until a certain computation has finished. +static RENDER_LOCK: RwLock<()> = RwLock::new(()); + +pub type RenderLockGuard = RwLockReadGuard<'static, ()>; + +/// Requests that the editor is redrawn. The redraws are debounced (currently to +/// 30FPS) so this can be called many times without causing a ton of frames to +/// be rendered. +pub fn request_redraw() { + REDRAW_NOTIFY.notify_one(); +} + +/// Returns a future that will yield once a redraw has been asynchronously +/// requested using [`request_redraw`]. +pub fn redraw_requested() -> impl Future { + REDRAW_NOTIFY.notified() +} + +/// Wait until all locks acquired with [`lock_frame`] have been released. +/// This function is called before rendering and is intended to allow the frame +/// to wait for async computations that should be included in the current frame. +pub fn start_frame() { + drop(RENDER_LOCK.write()); + // exhaust any leftover redraw notifications + let notify = REDRAW_NOTIFY.notified(); + tokio::pin!(notify); + notify.enable(); +} + +/// Acquires the render lock which will prevent the next frame from being drawn +/// until the returned guard is dropped. +pub fn lock_frame() -> RenderLockGuard { + RENDER_LOCK.read() +} diff --git a/helix-lsp/Cargo.toml b/helix-lsp/Cargo.toml index 47aeb1fcd..19bc94d6e 100644 --- a/helix-lsp/Cargo.toml +++ b/helix-lsp/Cargo.toml @@ -25,7 +25,7 @@ lsp-types = { version = "0.94" } serde = { version = "1.0", features = ["derive"] } serde_json = "1.0" thiserror = "1.0" -tokio = { version = "1.31", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } +tokio = { version = "1.32", features = ["rt", "rt-multi-thread", "io-util", "io-std", "time", "process", "macros", "fs", "parking_lot", "sync"] } tokio-stream = "0.1.14" which = "4.4" parking_lot = "0.12.1" diff --git a/helix-term/Cargo.toml b/helix-term/Cargo.toml index e2803ecfb..84c813e83 100644 --- a/helix-term/Cargo.toml +++ b/helix-term/Cargo.toml @@ -25,6 +25,7 @@ path = "src/main.rs" [dependencies] helix-core = { version = "0.6", path = "../helix-core" } +helix-event = { version = "0.6", path = "../helix-event" } helix-view = { version = "0.6", path = "../helix-view" } helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-dap = { version = "0.6", path = "../helix-dap" } @@ -50,7 +51,7 @@ chrono = { version = "0.4", default-features = false, features = ["clock"] } log = "0.4" # File picker -fuzzy-matcher = "0.3" +nucleo.workspace = true ignore = "0.4" # markdown doc rendering pulldown-cmark = { version = "0.9", default-features = false } diff --git a/helix-term/src/application.rs b/helix-term/src/application.rs index 179c21185..a40ef2628 100644 --- a/helix-term/src/application.rs +++ b/helix-term/src/application.rs @@ -264,22 +264,19 @@ impl Application { } async fn render(&mut self) { + if self.compositor.full_redraw { + self.terminal.clear().expect("Cannot clear the terminal"); + self.compositor.full_redraw = false; + } + let mut cx = crate::compositor::Context { editor: &mut self.editor, jobs: &mut self.jobs, scroll: None, }; - // Acquire mutable access to the redraw_handle lock - // to ensure that there are no tasks running that want to block rendering - drop(cx.editor.redraw_handle.1.write().await); + helix_event::start_frame(); cx.editor.needs_redraw = false; - { - // exhaust any leftover redraw notifications - let notify = cx.editor.redraw_handle.0.notified(); - tokio::pin!(notify); - notify.enable(); - } let area = self .terminal @@ -608,7 +605,7 @@ impl Application { EditorEvent::LanguageServerMessage((id, call)) => { self.handle_language_server_message(call, id).await; // limit render calls for fast language server messages - self.editor.redraw_handle.0.notify_one(); + helix_event::request_redraw(); } EditorEvent::DebuggerEvent(payload) => { let needs_render = self.editor.handle_debugger_message(payload).await; diff --git a/helix-term/src/commands.rs b/helix-term/src/commands.rs index 238902987..cf6bb2703 100644 --- a/helix-term/src/commands.rs +++ b/helix-term/src/commands.rs @@ -46,7 +46,6 @@ use helix_view::{ }; use anyhow::{anyhow, bail, ensure, Context as _}; -use fuzzy_matcher::FuzzyMatcher; use insert::*; use movement::Movement; @@ -63,7 +62,7 @@ use crate::{ }; use crate::job::{self, Jobs}; -use futures_util::{stream::FuturesUnordered, StreamExt, TryStreamExt}; +use futures_util::{stream::FuturesUnordered, TryStreamExt}; use std::{collections::HashMap, fmt, future::Future}; use std::{collections::HashSet, num::NonZeroUsize}; @@ -78,7 +77,6 @@ use serde::de::{self, Deserialize, Deserializer}; use grep_regex::RegexMatcherBuilder; use grep_searcher::{sinks, BinaryDetection, SearcherBuilder}; use ignore::{DirEntry, WalkBuilder, WalkState}; -use tokio_stream::wrappers::UnboundedReceiverStream; pub type OnKeyCallback = Box; @@ -1273,6 +1271,65 @@ fn extend_next_long_word_end(cx: &mut Context) { extend_word_impl(cx, movement::move_next_long_word_end) } +/// Separate branch to find_char designed only for char. +// +// This is necessary because the one document can have different line endings inside. And we +// cannot predict what character to find when is pressed. On the current line it can be `lf` +// but on the next line it can be `crlf`. That's why [`find_char_impl`] cannot be applied here. +fn find_char_line_ending( + cx: &mut Context, + count: usize, + direction: Direction, + inclusive: bool, + extend: bool, +) { + let (view, doc) = current!(cx.editor); + let text = doc.text().slice(..); + + let selection = doc.selection(view.id).clone().transform(|range| { + let cursor = range.cursor(text); + let cursor_line = range.cursor_line(text); + + // Finding the line where we're going to find . Depends mostly on + // `count`, but also takes into account edge cases where we're already at the end + // of a line or the beginning of a line + let find_on_line = match direction { + Direction::Forward => { + let on_edge = line_end_char_index(&text, cursor_line) == cursor; + let line = cursor_line + count - 1 + (on_edge as usize); + if line >= text.len_lines() - 1 { + return range; + } else { + line + } + } + Direction::Backward => { + let on_edge = text.line_to_char(cursor_line) == cursor && !inclusive; + let line = cursor_line as isize - (count as isize - 1 + on_edge as isize); + if line <= 0 { + return range; + } else { + line as usize + } + } + }; + + let pos = match (direction, inclusive) { + (Direction::Forward, true) => line_end_char_index(&text, find_on_line), + (Direction::Forward, false) => line_end_char_index(&text, find_on_line) - 1, + (Direction::Backward, true) => line_end_char_index(&text, find_on_line - 1), + (Direction::Backward, false) => text.line_to_char(find_on_line), + }; + + if extend { + range.put_cursor(text, pos, true) + } else { + Range::point(range.cursor(text)).put_cursor(text, pos, true) + } + }); + doc.set_selection(view.id, selection); +} + fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bool) { // TODO: count is reset to 1 before next key so we move it into the closure here. // Would be nice to carry over. @@ -1286,13 +1343,9 @@ fn find_char(cx: &mut Context, direction: Direction, inclusive: bool, extend: bo KeyEvent { code: KeyCode::Enter, .. - } => - // TODO: this isn't quite correct when CRLF is involved. - // This hack will work in most cases, since documents don't - // usually mix line endings. But we should fix it eventually - // anyway. - { - doc!(cx.editor).line_ending.as_str().chars().next().unwrap() + } => { + find_char_line_ending(cx, count, direction, inclusive, extend); + return; } KeyEvent { @@ -1735,8 +1788,8 @@ fn select_regex(cx: &mut Context) { "select:".into(), Some(reg), ui::completers::none, - move |editor, regex, event| { - let (view, doc) = current!(editor); + move |cx, regex, event| { + let (view, doc) = current!(cx.editor); if !matches!(event, PromptEvent::Update | PromptEvent::Validate) { return; } @@ -1757,8 +1810,8 @@ fn split_selection(cx: &mut Context) { "split:".into(), Some(reg), ui::completers::none, - move |editor, regex, event| { - let (view, doc) = current!(editor); + move |cx, regex, event| { + let (view, doc) = current!(cx.editor); if !matches!(event, PromptEvent::Update | PromptEvent::Validate) { return; } @@ -1922,14 +1975,14 @@ fn searcher(cx: &mut Context, direction: Direction) { .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) .collect() }, - move |editor, regex, event| { + move |cx, regex, event| { if event == PromptEvent::Validate { - editor.registers.last_search_register = reg; + cx.editor.registers.last_search_register = reg; } else if event != PromptEvent::Update { return; } search_impl( - editor, + cx.editor, &contents, ®ex, Movement::Move, @@ -2098,13 +2151,11 @@ fn global_search(cx: &mut Context) { } } - let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::(); let config = cx.editor.config(); let smart_case = config.search.smart_case; let file_picker_config = config.file_picker.clone(); let reg = cx.register.unwrap_or('/'); - let completions = search_completions(cx, Some(reg)); ui::regex_prompt( cx, @@ -2117,166 +2168,173 @@ fn global_search(cx: &mut Context) { .map(|comp| (0.., std::borrow::Cow::Owned(comp.clone()))) .collect() }, - move |editor, regex, event| { + move |cx, regex, event| { if event != PromptEvent::Validate { return; } - editor.registers.last_search_register = reg; + cx.editor.registers.last_search_register = reg; - let documents: Vec<_> = editor + let current_path = doc_mut!(cx.editor).path().cloned(); + let documents: Vec<_> = cx + .editor .documents() - .map(|doc| (doc.path(), doc.text())) + .map(|doc| (doc.path().cloned(), doc.text().to_owned())) .collect(); if let Ok(matcher) = RegexMatcherBuilder::new() .case_smart(smart_case) .build(regex.as_str()) { - let searcher = SearcherBuilder::new() - .binary_detection(BinaryDetection::quit(b'\x00')) - .build(); - let search_root = helix_loader::current_working_dir(); if !search_root.exists() { - editor.set_error("Current working directory does not exist"); + cx.editor + .set_error("Current working directory does not exist"); return; } + let (picker, injector) = Picker::stream(current_path); + let dedup_symlinks = file_picker_config.deduplicate_links; let absolute_root = search_root .canonicalize() .unwrap_or_else(|_| search_root.clone()); - - WalkBuilder::new(search_root) - .hidden(file_picker_config.hidden) - .parents(file_picker_config.parents) - .ignore(file_picker_config.ignore) - .follow_links(file_picker_config.follow_symlinks) - .git_ignore(file_picker_config.git_ignore) - .git_global(file_picker_config.git_global) - .git_exclude(file_picker_config.git_exclude) - .max_depth(file_picker_config.max_depth) - .filter_entry(move |entry| { - filter_picker_entry(entry, &absolute_root, dedup_symlinks) - }) - .build_parallel() - .run(|| { - let mut searcher = searcher.clone(); - let matcher = matcher.clone(); - let all_matches_sx = all_matches_sx.clone(); - let documents = &documents; - Box::new(move |entry: Result| -> WalkState { - let entry = match entry { - Ok(entry) => entry, - Err(_) => return WalkState::Continue, - }; - - match entry.file_type() { - Some(entry) if entry.is_file() => {} - // skip everything else - _ => return WalkState::Continue, - }; - - let sink = sinks::UTF8(|line_num, _| { - all_matches_sx - .send(FileResult::new(entry.path(), line_num as usize - 1)) - .unwrap(); - - Ok(true) - }); - let doc = documents.iter().find(|&(doc_path, _)| { - doc_path.map_or(false, |doc_path| doc_path == entry.path()) - }); - - let result = if let Some((_, doc)) = doc { - // there is already a buffer for this file - // search the buffer instead of the file because it's faster - // and captures new edits without requireing a save - if searcher.multi_line_with_matcher(&matcher) { - // in this case a continous buffer is required - // convert the rope to a string - let text = doc.to_string(); - searcher.search_slice(&matcher, text.as_bytes(), sink) + let injector_ = injector.clone(); + + std::thread::spawn(move || { + let searcher = SearcherBuilder::new() + .binary_detection(BinaryDetection::quit(b'\x00')) + .build(); + WalkBuilder::new(search_root) + .hidden(file_picker_config.hidden) + .parents(file_picker_config.parents) + .ignore(file_picker_config.ignore) + .follow_links(file_picker_config.follow_symlinks) + .git_ignore(file_picker_config.git_ignore) + .git_global(file_picker_config.git_global) + .git_exclude(file_picker_config.git_exclude) + .max_depth(file_picker_config.max_depth) + .filter_entry(move |entry| { + filter_picker_entry(entry, &absolute_root, dedup_symlinks) + }) + .build_parallel() + .run(|| { + let mut searcher = searcher.clone(); + let matcher = matcher.clone(); + let injector = injector_.clone(); + let documents = &documents; + Box::new(move |entry: Result| -> WalkState { + let entry = match entry { + Ok(entry) => entry, + Err(_) => return WalkState::Continue, + }; + + match entry.file_type() { + Some(entry) if entry.is_file() => {} + // skip everything else + _ => return WalkState::Continue, + }; + + let mut stop = false; + let sink = sinks::UTF8(|line_num, _| { + stop = injector + .push(FileResult::new(entry.path(), line_num as usize - 1)) + .is_err(); + + Ok(!stop) + }); + let doc = documents.iter().find(|&(doc_path, _)| { + doc_path + .as_ref() + .map_or(false, |doc_path| doc_path == entry.path()) + }); + + let result = if let Some((_, doc)) = doc { + // there is already a buffer for this file + // search the buffer instead of the file because it's faster + // and captures new edits without requiring a save + if searcher.multi_line_with_matcher(&matcher) { + // in this case a continous buffer is required + // convert the rope to a string + let text = doc.to_string(); + searcher.search_slice(&matcher, text.as_bytes(), sink) + } else { + searcher.search_reader( + &matcher, + RopeReader::new(doc.slice(..)), + sink, + ) + } } else { - searcher.search_reader( - &matcher, - RopeReader::new(doc.slice(..)), - sink, - ) + searcher.search_path(&matcher, entry.path(), sink) + }; + + if let Err(err) = result { + log::error!( + "Global search error: {}, {}", + entry.path().display(), + err + ); } - } else { - searcher.search_path(&matcher, entry.path(), sink) - }; - - if let Err(err) = result { - log::error!( - "Global search error: {}, {}", - entry.path().display(), - err - ); - } - WalkState::Continue - }) - }); + if stop { + WalkState::Quit + } else { + WalkState::Continue + } + }) + }); + }); + + cx.jobs.callback(async move { + let call = move |_: &mut Editor, compositor: &mut Compositor| { + let picker = Picker::with_stream( + picker, + injector, + move |cx, FileResult { path, line_num }, action| { + let doc = match cx.editor.open(path, action) { + Ok(id) => doc_mut!(cx.editor, &id), + Err(e) => { + cx.editor.set_error(format!( + "Failed to open file '{}': {}", + path.display(), + e + )); + return; + } + }; + + let line_num = *line_num; + let view = view_mut!(cx.editor); + let text = doc.text(); + if line_num >= text.len_lines() { + cx.editor.set_error( + "The line you jumped to does not exist anymore because the file has changed.", + ); + return; + } + let start = text.line_to_char(line_num); + let end = text.line_to_char((line_num + 1).min(text.len_lines())); + + doc.set_selection(view.id, Selection::single(start, end)); + if action.align_view(view, doc.id()) { + align_view(doc, view, Align::Center); + } + }, + ) + .with_preview( + |_editor, FileResult { path, line_num }| { + Some((path.clone().into(), Some((*line_num, *line_num)))) + }, + ); + compositor.push(Box::new(overlaid(picker))) + }; + Ok(Callback::EditorCompositor(Box::new(call))) + }) } else { // Otherwise do nothing // log::warn!("Global Search Invalid Pattern") } }, ); - - let current_path = doc_mut!(cx.editor).path().cloned(); - - let show_picker = async move { - let all_matches: Vec = - UnboundedReceiverStream::new(all_matches_rx).collect().await; - let call: job::Callback = Callback::EditorCompositor(Box::new( - move |editor: &mut Editor, compositor: &mut Compositor| { - if all_matches.is_empty() { - if !editor.is_err() { - editor.set_status("No matches found"); - } - return; - } - - let picker = Picker::new( - all_matches, - current_path, - move |cx, FileResult { path, line_num }, action| { - let doc = match cx.editor.open(path, action) { - Ok(id) => doc_mut!(cx.editor, &id), - Err(e) => { - cx.editor.set_error(format!( - "Failed to open file '{}': {}", - path.display(), - e - )); - return; - } - }; - let line_num = *line_num; - let view = view_mut!(cx.editor); - let text = doc.text(); - if line_num >= text.len_lines() { - cx.editor.set_error("The line you jumped to does not exist anymore because the file has changed."); - return; - } - let start = text.line_to_char(line_num); - let end = text.line_to_char((line_num + 1).min(text.len_lines())); - - doc.set_selection(view.id, Selection::single(start, end)); - if action.align_view(view, doc.id()){ - align_view(doc, view, Align::Center); - } - }).with_preview(|_editor, FileResult { path, line_num }| { - Some((path.clone().into(), Some((*line_num, *line_num)))) - }); - compositor.push(Box::new(overlaid(picker))); - }, - )); - Ok(call) - }; - cx.jobs.callback(show_picker); } enum Extend { @@ -4348,8 +4406,8 @@ fn keep_or_remove_selections_impl(cx: &mut Context, remove: bool) { if remove { "remove:" } else { "keep:" }.into(), Some(reg), ui::completers::none, - move |editor, regex, event| { - let (view, doc) = current!(editor); + move |cx, regex, event| { + let (view, doc) = current!(cx.editor); if !matches!(event, PromptEvent::Update | PromptEvent::Validate) { return; } @@ -4866,17 +4924,19 @@ fn transpose_view(cx: &mut Context) { cx.editor.transpose_view() } -// split helper, clear it later -fn split(cx: &mut Context, action: Action) { - let (view, doc) = current!(cx.editor); +/// Open a new split in the given direction specified by the action. +/// +/// Maintain the current view (both the cursor's position and view in document). +fn split(editor: &mut Editor, action: Action) { + let (view, doc) = current!(editor); let id = doc.id(); let selection = doc.selection(view.id).clone(); let offset = view.offset; - cx.editor.switch(id, action); + editor.switch(id, action); // match the selection in the previous view - let (view, doc) = current!(cx.editor); + let (view, doc) = current!(editor); doc.set_selection(view.id, selection); // match the view scroll offset (switch doesn't handle this fully // since the selection is only matched after the split) @@ -4884,7 +4944,7 @@ fn split(cx: &mut Context, action: Action) { } fn hsplit(cx: &mut Context) { - split(cx, Action::HorizontalSplit); + split(cx.editor, Action::HorizontalSplit); } fn hsplit_new(cx: &mut Context) { @@ -4892,7 +4952,7 @@ fn hsplit_new(cx: &mut Context) { } fn vsplit(cx: &mut Context) { - split(cx, Action::VerticalSplit); + split(cx.editor, Action::VerticalSplit); } fn vsplit_new(cx: &mut Context) { diff --git a/helix-term/src/commands/engine.rs b/helix-term/src/commands/engine.rs index 16824d022..4eff04862 100644 --- a/helix-term/src/commands/engine.rs +++ b/helix-term/src/commands/engine.rs @@ -141,13 +141,10 @@ impl ScriptingEngine { None } - pub fn fuzzy_match<'a>( - fuzzy_matcher: &'a fuzzy_matcher::skim::SkimMatcherV2, - input: &'a str, - ) -> Vec<(String, i64)> { + pub fn available_commands<'a>() -> Vec> { PLUGIN_PRECEDENCE .iter() - .flat_map(|kind| manual_dispatch!(kind, fuzzy_match(fuzzy_matcher, input))) + .flat_map(|kind| manual_dispatch!(kind, available_commands())) .collect() } } @@ -220,11 +217,7 @@ pub trait PluginSystem { } /// Fuzzy match the input against the fuzzy matcher, used for handling completions on typed commands - fn fuzzy_match<'a>( - &self, - _fuzzy_matcher: &'a fuzzy_matcher::skim::SkimMatcherV2, - _input: &'a str, - ) -> Vec<(String, i64)> { + fn available_commands<'a>(&self) -> Vec> { Vec::new() } } diff --git a/helix-term/src/commands/engine/scheme.rs b/helix-term/src/commands/engine/scheme.rs index 9dcc55dbf..47d06f620 100644 --- a/helix-term/src/commands/engine/scheme.rs +++ b/helix-term/src/commands/engine/scheme.rs @@ -1,4 +1,3 @@ -use fuzzy_matcher::FuzzyMatcher; use helix_core::{ extensions::steel_implementations::{rope_module, SteelRopeSlice}, graphemes, @@ -337,22 +336,15 @@ impl super::PluginSystem for SteelScriptingEngine { }) } - fn fuzzy_match<'a>( + fn available_commands<'a>( &self, - fuzzy_matcher: &'a fuzzy_matcher::skim::SkimMatcherV2, - input: &'a str, - ) -> Vec<(String, i64)> { + ) -> Vec> { EXPORTED_IDENTIFIERS .identifiers .read() .unwrap() .iter() - .filter_map(|name| { - fuzzy_matcher - .fuzzy_match(name, input) - .map(|score| (name, score)) - }) - .map(|x| (x.0.to_string(), x.1)) + .map(|x| x.clone().into()) .collect::>() } } @@ -694,22 +686,22 @@ impl StatusLineMessage { } } -impl Item for SteelVal { - type Data = (); +// impl Item for SteelVal { +// type Data = (); - // TODO: This shouldn't copy the data every time - fn format(&self, _data: &Self::Data) -> tui::widgets::Row { - let formatted = self.to_string(); +// // TODO: This shouldn't copy the data every time +// fn format(&self, _data: &Self::Data) -> tui::widgets::Row { +// let formatted = self.to_string(); - formatted - .strip_prefix("\"") - .unwrap_or(&formatted) - .strip_suffix("\"") - .unwrap_or(&formatted) - .to_owned() - .into() - } -} +// formatted +// .strip_prefix("\"") +// .unwrap_or(&formatted) +// .strip_suffix("\"") +// .unwrap_or(&formatted) +// .to_owned() +// .into() +// } +// } pub struct CallbackQueue { queue: Arc>>, diff --git a/helix-term/src/commands/typed.rs b/helix-term/src/commands/typed.rs index e62ce0d22..623769d77 100644 --- a/helix-term/src/commands/typed.rs +++ b/helix-term/src/commands/typed.rs @@ -4,7 +4,8 @@ use crate::job::Job; use super::*; -use helix_core::{encoding, shellwords::Shellwords}; +use helix_core::fuzzy::fuzzy_match; +use helix_core::{encoding, line_ending, shellwords::Shellwords}; use helix_view::document::DEFAULT_LANGUAGE_NAME; use helix_view::editor::{Action, CloseError, ConfigEvent}; use serde_json::Value; @@ -331,12 +332,16 @@ fn write_impl( path: Option<&Cow>, force: bool, ) -> anyhow::Result<()> { - let editor_auto_fmt = cx.editor.config().auto_format; + let config = cx.editor.config(); let jobs = &mut cx.jobs; let (view, doc) = current!(cx.editor); let path = path.map(AsRef::as_ref); - let fmt = if editor_auto_fmt { + if config.insert_final_newline { + insert_final_newline(doc, view); + } + + let fmt = if config.auto_format { doc.auto_format().map(|fmt| { let callback = make_format_callback( doc.id(), @@ -360,6 +365,16 @@ fn write_impl( Ok(()) } +fn insert_final_newline(doc: &mut Document, view: &mut View) { + let text = doc.text(); + if line_ending::get_line_ending(&text.slice(..)).is_none() { + let eof = Selection::point(text.len_chars()); + let insert = Transaction::insert(text, &eof, doc.line_ending.as_str().into()); + doc.apply(&insert, view.id); + doc.append_changes_to_history(view); + } +} + fn write( cx: &mut compositor::Context, args: &[Cow], @@ -661,11 +676,10 @@ pub fn write_all_impl( write_scratch: bool, ) -> anyhow::Result<()> { let mut errors: Vec<&'static str> = Vec::new(); - let auto_format = cx.editor.config().auto_format; + let config = cx.editor.config(); let jobs = &mut cx.jobs; let current_view = view!(cx.editor); - // save all documents let saves: Vec<_> = cx .editor .documents @@ -702,32 +716,35 @@ pub fn write_all_impl( current_view.id }; - let fmt = if auto_format { - doc.auto_format().map(|fmt| { - let callback = make_format_callback( - doc.id(), - doc.version(), - target_view, - fmt, - Some((None, force)), - ); - jobs.add(Job::with_callback(callback).wait_before_exiting()); - }) - } else { - None - }; + Some((doc.id(), target_view)) + }) + .collect(); - if fmt.is_none() { - return Some(doc.id()); - } + for (doc_id, target_view) in saves { + let doc = doc_mut!(cx.editor, &doc_id); + + if config.insert_final_newline { + insert_final_newline(doc, view_mut!(cx.editor, target_view)); + } + let fmt = if config.auto_format { + doc.auto_format().map(|fmt| { + let callback = make_format_callback( + doc_id, + doc.version(), + target_view, + fmt, + Some((None, force)), + ); + jobs.add(Job::with_callback(callback).wait_before_exiting()); + }) + } else { None - }) - .collect(); + }; - // manually call save for the rest of docs that don't have a formatter - for id in saves { - cx.editor.save::(id, None, force)?; + if fmt.is_none() { + cx.editor.save::(doc_id, None, force)?; + } } if !errors.is_empty() && !force { @@ -1275,12 +1292,10 @@ fn reload( } let scrolloff = cx.editor.config().scrolloff; - let redraw_handle = cx.editor.redraw_handle.clone(); let (view, doc) = current!(cx.editor); - doc.reload(view, &cx.editor.diff_providers, redraw_handle) - .map(|_| { - view.ensure_cursor_in_view(doc, scrolloff); - })?; + doc.reload(view, &cx.editor.diff_providers).map(|_| { + view.ensure_cursor_in_view(doc, scrolloff); + })?; if let Some(path) = doc.path() { cx.editor .language_servers @@ -1326,8 +1341,7 @@ fn reload_all( // Ensure that the view is synced with the document's history. view.sync_changes(doc); - let redraw_handle = cx.editor.redraw_handle.clone(); - doc.reload(view, &cx.editor.diff_providers, redraw_handle)?; + doc.reload(view, &cx.editor.diff_providers)?; if let Some(path) = doc.path() { cx.editor .language_servers @@ -1539,6 +1553,84 @@ fn tree_sitter_scopes( Ok(()) } +fn tree_sitter_highlight_name( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + fn find_highlight_at_cursor( + cx: &mut compositor::Context<'_>, + ) -> Option { + use helix_core::syntax::HighlightEvent; + + let (view, doc) = current!(cx.editor); + let syntax = doc.syntax()?; + let text = doc.text().slice(..); + let cursor = doc.selection(view.id).primary().cursor(text); + let byte = text.char_to_byte(cursor); + let node = syntax + .tree() + .root_node() + .descendant_for_byte_range(byte, byte)?; + // Query the same range as the one used in syntax highlighting. + let range = { + // Calculate viewport byte ranges: + let row = text.char_to_line(view.offset.anchor.min(text.len_chars())); + // Saturating subs to make it inclusive zero indexing. + let last_line = text.len_lines().saturating_sub(1); + let height = view.inner_area(doc).height; + let last_visible_line = (row + height as usize).saturating_sub(1).min(last_line); + let start = text.line_to_byte(row.min(last_line)); + let end = text.line_to_byte(last_visible_line + 1); + + start..end + }; + + let mut highlight = None; + + for event in syntax.highlight_iter(text, Some(range), None) { + match event.unwrap() { + HighlightEvent::Source { start, end } + if start == node.start_byte() && end == node.end_byte() => + { + return highlight; + } + HighlightEvent::HighlightStart(hl) => { + highlight = Some(hl); + } + _ => (), + } + } + + None + } + + if event != PromptEvent::Validate { + return Ok(()); + } + + let Some(highlight) = find_highlight_at_cursor(cx) else { + return Ok(()); + }; + + let content = cx.editor.theme.scope(highlight.0).to_string(); + + let callback = async move { + let call: job::Callback = Callback::EditorCompositor(Box::new( + move |editor: &mut Editor, compositor: &mut Compositor| { + let content = ui::Markdown::new(content, editor.syn_loader.clone()); + let popup = Popup::new("hover", content).auto_close(true); + compositor.replace_or_push("hover", popup); + }, + )); + Ok(call) + }; + + cx.jobs.callback(callback); + + Ok(()) +} + fn vsplit( cx: &mut compositor::Context, args: &[Cow], @@ -1548,10 +1640,8 @@ fn vsplit( return Ok(()); } - let id = view!(cx.editor).doc; - if args.is_empty() { - cx.editor.switch(id, Action::VerticalSplit); + split(cx.editor, Action::VerticalSplit); } else { for arg in args { cx.editor @@ -1571,10 +1661,8 @@ fn hsplit( return Ok(()); } - let id = view!(cx.editor).doc; - if args.is_empty() { - cx.editor.switch(id, Action::HorizontalSplit); + split(cx.editor, Action::HorizontalSplit); } else { for arg in args { cx.editor @@ -2344,6 +2432,29 @@ fn clear_register( Ok(()) } +fn redraw( + cx: &mut compositor::Context, + _args: &[Cow], + event: PromptEvent, +) -> anyhow::Result<()> { + if event != PromptEvent::Validate { + return Ok(()); + } + + let callback = Box::pin(async move { + let call: job::Callback = + job::Callback::EditorCompositor(Box::new(|_editor, compositor| { + compositor.need_full_redraw(); + })); + + Ok(call) + }); + + cx.jobs.callback(callback); + + Ok(()) +} + pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ TypableCommand { name: "quit", @@ -2735,6 +2846,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: tree_sitter_scopes, signature: CommandSignature::none(), }, + TypableCommand { + name: "tree-sitter-highlight-name", + aliases: &[], + doc: "Display name of tree-sitter highlight scope under the cursor.", + fun: tree_sitter_highlight_name, + signature: CommandSignature::none(), + }, TypableCommand { name: "debug-start", aliases: &["dbg"], @@ -2939,6 +3057,13 @@ pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[ fun: clear_register, signature: CommandSignature::none(), }, + TypableCommand { + name: "redraw", + aliases: &[], + doc: "Clear and re-render the whole UI", + fun: redraw, + signature: CommandSignature::none(), + }, ]; pub static TYPABLE_COMMAND_MAP: Lazy> = @@ -2958,35 +3083,41 @@ pub(super) fn command_mode(cx: &mut Context) { ":".into(), Some(':'), |editor: &Editor, input: &str| { - static FUZZY_MATCHER: Lazy = - Lazy::new(fuzzy_matcher::skim::SkimMatcherV2::default); - let shellwords = Shellwords::from(input); let words = shellwords.words(); if words.is_empty() || (words.len() == 1 && !shellwords.ends_with_whitespace()) { - let globals = - crate::commands::engine::ScriptingEngine::fuzzy_match(&FUZZY_MATCHER, input) - .into_iter() - .map(|x| (Cow::from(x.0), x.1)) - .collect::>(); + // let globals = + // crate::commands::engine::ScriptingEngine::fuzzy_match(&FUZZY_MATCHER, input) + // .into_iter() + // .map(|x| (Cow::from(x.0), x.1)) + // .collect::>(); + + // // If the command has not been finished yet, complete commands. + // let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST + // .iter() + // .filter_map(|command| { + // FUZZY_MATCHER + // .fuzzy_match(command.name, input) + // .map(|score| (Cow::from(command.name), score)) + // }) + // .chain(globals) + // .collect(); + + // matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score)); + // matches + // .into_iter() + // .map(|(name, _)| (0.., name.into())) + // .collect() + fuzzy_match( + input, + TYPABLE_COMMAND_LIST.iter().map(|command| Cow::from(command.name)).chain(crate::commands::engine::ScriptingEngine::available_commands()), + false, + ) + .into_iter() + .map(|(name, _)| (0.., name.into())) + .collect() - // If the command has not been finished yet, complete commands. - let mut matches: Vec<_> = typed::TYPABLE_COMMAND_LIST - .iter() - .filter_map(|command| { - FUZZY_MATCHER - .fuzzy_match(command.name, input) - .map(|score| (Cow::from(command.name), score)) - }) - .chain(globals) - .collect(); - - matches.sort_unstable_by_key(|(_file, score)| std::cmp::Reverse(*score)); - matches - .into_iter() - .map(|(name, _)| (0.., name.into())) - .collect() } else { // Otherwise, use the command's completer and the last shellword // as completion input. diff --git a/helix-term/src/compositor.rs b/helix-term/src/compositor.rs index bcb3e4490..3dcb5f2bf 100644 --- a/helix-term/src/compositor.rs +++ b/helix-term/src/compositor.rs @@ -16,6 +16,7 @@ pub enum EventResult { } use crate::job::Jobs; +use crate::ui::picker; use helix_view::Editor; pub use helix_view::input::Event; @@ -79,6 +80,7 @@ pub struct Compositor { area: Rect, pub(crate) last_picker: Option>, + pub(crate) full_redraw: bool, } impl Compositor { @@ -87,6 +89,7 @@ impl Compositor { layers: Vec::new(), area, last_picker: None, + full_redraw: false, } } @@ -100,6 +103,11 @@ impl Compositor { /// Add a layer to be rendered in front of all existing layers. pub fn push(&mut self, mut layer: Box) { + // immediately clear last_picker field to avoid excessive memory + // consumption for picker with many items + if layer.id() == Some(picker::ID) { + self.last_picker = None; + } let size = self.size(); // trigger required_size on init layer.required_size((size.width, size.height)); @@ -200,6 +208,10 @@ impl Compositor { .find(|component| component.id() == Some(id)) .and_then(|component| component.as_any_mut().downcast_mut()) } + + pub fn need_full_redraw(&mut self) { + self.full_redraw = true; + } } // View casting, taken straight from Cursive diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index 1a606bd62..1d15b89da 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -43,6 +43,8 @@ pub struct EditorView { pub(crate) last_insert: (commands::MappableCommand, Vec), pub(crate) completion: Option, spinners: ProgressSpinners, + /// Tracks if the terminal window is focused by reaction to terminal focus events + terminal_focused: bool, } #[derive(Debug, Clone)] @@ -71,6 +73,7 @@ impl EditorView { last_insert: (commands::MappableCommand::normal_mode, Vec::new()), completion: None, spinners: ProgressSpinners::default(), + terminal_focused: true, } } @@ -171,7 +174,7 @@ impl EditorView { view, view.area, theme, - is_focused, + is_focused & self.terminal_focused, &mut line_decorations, ); } @@ -1074,6 +1077,7 @@ impl EditorView { let editor = &mut cxt.editor; if let Some((pos, view_id)) = pos_and_view(editor, row, column, true) { + let prev_view_id = view!(editor).id; let doc = doc_mut!(editor, &view!(editor, view_id).doc); if modifiers == KeyModifiers::ALT { @@ -1083,6 +1087,10 @@ impl EditorView { doc.set_selection(view_id, Selection::point(pos)); } + if view_id != prev_view_id { + self.clear_completion(editor); + } + editor.focus(view_id); editor.ensure_cursor_in_view(view_id); @@ -1376,13 +1384,17 @@ impl Component for EditorView { Event::Mouse(event) => self.handle_mouse_event(event, &mut cx), Event::IdleTimeout => self.handle_idle_timeout(&mut cx), - Event::FocusGained => EventResult::Ignored(None), + Event::FocusGained => { + self.terminal_focused = true; + EventResult::Consumed(None) + } Event::FocusLost => { if context.editor.config().auto_save { if let Err(e) = commands::typed::write_all_impl(context, false, false) { context.editor.set_error(format!("{}", e)); } } + self.terminal_focused = false; EventResult::Consumed(None) } } diff --git a/helix-term/src/ui/fuzzy_match.rs b/helix-term/src/ui/fuzzy_match.rs deleted file mode 100644 index 22dc3a7fa..000000000 --- a/helix-term/src/ui/fuzzy_match.rs +++ /dev/null @@ -1,239 +0,0 @@ -use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; -use fuzzy_matcher::FuzzyMatcher; - -#[cfg(test)] -mod test; - -struct QueryAtom { - kind: QueryAtomKind, - atom: String, - ignore_case: bool, - inverse: bool, -} -impl QueryAtom { - fn new(atom: &str) -> Option { - let mut atom = atom.to_string(); - let inverse = atom.starts_with('!'); - if inverse { - atom.remove(0); - } - - let mut kind = match atom.chars().next() { - Some('^') => QueryAtomKind::Prefix, - Some('\'') => QueryAtomKind::Substring, - _ if inverse => QueryAtomKind::Substring, - _ => QueryAtomKind::Fuzzy, - }; - - if atom.starts_with(['^', '\'']) { - atom.remove(0); - } - - if atom.is_empty() { - return None; - } - - if atom.ends_with('$') && !atom.ends_with("\\$") { - atom.pop(); - kind = if kind == QueryAtomKind::Prefix { - QueryAtomKind::Exact - } else { - QueryAtomKind::Postfix - } - } - - Some(QueryAtom { - kind, - atom: atom.replace('\\', ""), - // not ideal but fuzzy_matches only knows ascii uppercase so more consistent - // to behave the same - ignore_case: kind != QueryAtomKind::Fuzzy - && atom.chars().all(|c| c.is_ascii_lowercase()), - inverse, - }) - } - - fn indices(&self, matcher: &Matcher, item: &str, indices: &mut Vec) -> bool { - // for inverse there are no indices to return - // just return whether we matched - if self.inverse { - return self.matches(matcher, item); - } - let buf; - let item = if self.ignore_case { - buf = item.to_ascii_lowercase(); - &buf - } else { - item - }; - let off = match self.kind { - QueryAtomKind::Fuzzy => { - if let Some((_, fuzzy_indices)) = matcher.fuzzy_indices(item, &self.atom) { - indices.extend_from_slice(&fuzzy_indices); - return true; - } else { - return false; - } - } - QueryAtomKind::Substring => { - if let Some(off) = item.find(&self.atom) { - off - } else { - return false; - } - } - QueryAtomKind::Prefix if item.starts_with(&self.atom) => 0, - QueryAtomKind::Postfix if item.ends_with(&self.atom) => item.len() - self.atom.len(), - QueryAtomKind::Exact if item == self.atom => 0, - _ => return false, - }; - - indices.extend(off..(off + self.atom.len())); - true - } - - fn matches(&self, matcher: &Matcher, item: &str) -> bool { - let buf; - let item = if self.ignore_case { - buf = item.to_ascii_lowercase(); - &buf - } else { - item - }; - let mut res = match self.kind { - QueryAtomKind::Fuzzy => matcher.fuzzy_match(item, &self.atom).is_some(), - QueryAtomKind::Substring => item.contains(&self.atom), - QueryAtomKind::Prefix => item.starts_with(&self.atom), - QueryAtomKind::Postfix => item.ends_with(&self.atom), - QueryAtomKind::Exact => item == self.atom, - }; - if self.inverse { - res = !res; - } - res - } -} - -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -enum QueryAtomKind { - /// Item is a fuzzy match of this behaviour - /// - /// Usage: `foo` - Fuzzy, - /// Item contains query atom as a continuous substring - /// - /// Usage `'foo` - Substring, - /// Item starts with query atom - /// - /// Usage: `^foo` - Prefix, - /// Item ends with query atom - /// - /// Usage: `foo$` - Postfix, - /// Item is equal to query atom - /// - /// Usage `^foo$` - Exact, -} - -#[derive(Default)] -pub struct FuzzyQuery { - first_fuzzy_atom: Option, - query_atoms: Vec, -} - -fn query_atoms(query: &str) -> impl Iterator + '_ { - let mut saw_backslash = false; - query.split(move |c| { - saw_backslash = match c { - ' ' if !saw_backslash => return true, - '\\' => true, - _ => false, - }; - false - }) -} - -impl FuzzyQuery { - pub fn refine(&self, query: &str, old_query: &str) -> (FuzzyQuery, bool) { - // TODO: we could be a lot smarter about this - let new_query = Self::new(query); - let mut is_refinement = query.starts_with(old_query); - - // if the last atom is an inverse atom adding more text to it - // will actually increase the number of matches and we can not refine - // the matches. - if is_refinement && !self.query_atoms.is_empty() { - let last_idx = self.query_atoms.len() - 1; - if self.query_atoms[last_idx].inverse - && self.query_atoms[last_idx].atom != new_query.query_atoms[last_idx].atom - { - is_refinement = false; - } - } - - (new_query, is_refinement) - } - - pub fn new(query: &str) -> FuzzyQuery { - let mut first_fuzzy_query = None; - let query_atoms = query_atoms(query) - .filter_map(|atom| { - let atom = QueryAtom::new(atom)?; - if atom.kind == QueryAtomKind::Fuzzy && first_fuzzy_query.is_none() { - first_fuzzy_query = Some(atom.atom); - None - } else { - Some(atom) - } - }) - .collect(); - FuzzyQuery { - first_fuzzy_atom: first_fuzzy_query, - query_atoms, - } - } - - pub fn fuzzy_match(&self, item: &str, matcher: &Matcher) -> Option { - // use the rank of the first fuzzzy query for the rank, because merging ranks is not really possible - // this behaviour matches fzf and skim - let score = self - .first_fuzzy_atom - .as_ref() - .map_or(Some(0), |atom| matcher.fuzzy_match(item, atom))?; - if self - .query_atoms - .iter() - .any(|atom| !atom.matches(matcher, item)) - { - return None; - } - Some(score) - } - - pub fn fuzzy_indices(&self, item: &str, matcher: &Matcher) -> Option<(i64, Vec)> { - let (score, mut indices) = self.first_fuzzy_atom.as_ref().map_or_else( - || Some((0, Vec::new())), - |atom| matcher.fuzzy_indices(item, atom), - )?; - - // fast path for the common case of just a single atom - if self.query_atoms.is_empty() { - return Some((score, indices)); - } - - for atom in &self.query_atoms { - if !atom.indices(matcher, item, &mut indices) { - return None; - } - } - - // deadup and remove duplicate matches - indices.sort_unstable(); - indices.dedup(); - - Some((score, indices)) - } -} diff --git a/helix-term/src/ui/fuzzy_match/test.rs b/helix-term/src/ui/fuzzy_match/test.rs deleted file mode 100644 index 5df79eeb1..000000000 --- a/helix-term/src/ui/fuzzy_match/test.rs +++ /dev/null @@ -1,47 +0,0 @@ -use crate::ui::fuzzy_match::FuzzyQuery; -use crate::ui::fuzzy_match::Matcher; - -fn run_test<'a>(query: &str, items: &'a [&'a str]) -> Vec { - let query = FuzzyQuery::new(query); - let matcher = Matcher::default(); - items - .iter() - .filter_map(|item| { - let (_, indices) = query.fuzzy_indices(item, &matcher)?; - let matched_string = indices - .iter() - .map(|&pos| item.chars().nth(pos).unwrap()) - .collect(); - Some(matched_string) - }) - .collect() -} - -#[test] -fn match_single_value() { - let matches = run_test("foo", &["foobar", "foo", "bar"]); - assert_eq!(matches, &["foo", "foo"]) -} - -#[test] -fn match_multiple_values() { - let matches = run_test( - "foo bar", - &["foo bar", "foo bar", "bar foo", "bar", "foo"], - ); - assert_eq!(matches, &["foobar", "foobar", "barfoo"]) -} - -#[test] -fn space_escape() { - let matches = run_test(r"foo\ bar", &["bar foo", "foo bar", "foobar"]); - assert_eq!(matches, &["foo bar"]) -} - -#[test] -fn trim() { - let matches = run_test(r" foo bar ", &["bar foo", "foo bar", "foobar"]); - assert_eq!(matches, &["barfoo", "foobar", "foobar"]); - let matches = run_test(r" foo bar\ ", &["bar foo", "foo bar", "foobar"]); - assert_eq!(matches, &["bar foo"]) -} diff --git a/helix-term/src/ui/menu.rs b/helix-term/src/ui/menu.rs index c73e7bed2..9704b1f2a 100644 --- a/helix-term/src/ui/menu.rs +++ b/helix-term/src/ui/menu.rs @@ -1,22 +1,22 @@ -use std::{borrow::Cow, path::PathBuf}; +use std::{borrow::Cow, cmp::Reverse, path::PathBuf}; use crate::{ compositor::{Callback, Component, Compositor, Context, Event, EventResult}, ctrl, key, shift, }; +use helix_core::fuzzy::MATCHER; +use nucleo::pattern::{Atom, AtomKind, CaseMatching}; +use nucleo::{Config, Utf32Str}; use tui::{buffer::Buffer as Surface, widgets::Table}; pub use tui::widgets::{Cell, Row}; -use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; -use fuzzy_matcher::FuzzyMatcher; - use helix_view::{editor::SmartTabConfig, graphics::Rect, Editor}; use tui::layout::Constraint; -pub trait Item { +pub trait Item: Sync + Send + 'static { /// Additional editor state that is used for label calculation. - type Data; + type Data: Sync + Send + 'static; fn format(&self, data: &Self::Data) -> Row; @@ -51,9 +51,8 @@ pub struct Menu { cursor: Option, - matcher: Box, /// (index, score) - matches: Vec<(usize, i64)>, + matches: Vec<(u32, u32)>, widths: Vec, @@ -75,11 +74,10 @@ impl Menu { editor_data: ::Data, callback_fn: impl Fn(&mut Editor, Option<&T>, MenuEvent) + 'static, ) -> Self { - let matches = (0..options.len()).map(|i| (i, 0)).collect(); + let matches = (0..options.len() as u32).map(|i| (i, 0)).collect(); Self { options, editor_data, - matcher: Box::new(Matcher::default().ignore_case()), matches, cursor: None, widths: Vec::new(), @@ -94,20 +92,19 @@ impl Menu { pub fn score(&mut self, pattern: &str) { // reuse the matches allocation self.matches.clear(); - self.matches.extend( - self.options - .iter() - .enumerate() - .filter_map(|(index, option)| { - let text = option.filter_text(&self.editor_data); - // TODO: using fuzzy_indices could give us the char idx for match highlighting - self.matcher - .fuzzy_match(&text, pattern) - .map(|score| (index, score)) - }), - ); - // Order of equal elements needs to be preserved as LSP preselected items come in order of high to low priority - self.matches.sort_by_key(|(_, score)| -score); + let mut matcher = MATCHER.lock(); + matcher.config = Config::DEFAULT; + let pattern = Atom::new(pattern, CaseMatching::Ignore, AtomKind::Fuzzy, false); + let mut buf = Vec::new(); + let matches = self.options.iter().enumerate().filter_map(|(i, option)| { + let text = option.filter_text(&self.editor_data); + pattern + .score(Utf32Str::new(&text, &mut buf), &mut matcher) + .map(|score| (i as u32, score as u32)) + }); + self.matches.extend(matches); + self.matches + .sort_unstable_by_key(|&(i, score)| (Reverse(score), i)); // reset cursor position self.cursor = None; @@ -201,7 +198,7 @@ impl Menu { self.cursor.and_then(|cursor| { self.matches .get(cursor) - .map(|(index, _score)| &self.options[*index]) + .map(|(index, _score)| &self.options[*index as usize]) }) } @@ -209,7 +206,7 @@ impl Menu { self.cursor.and_then(|cursor| { self.matches .get(cursor) - .map(|(index, _score)| &mut self.options[*index]) + .map(|(index, _score)| &mut self.options[*index as usize]) }) } @@ -332,7 +329,7 @@ impl Component for Menu { .iter() .map(|(index, _score)| { // (index, self.options.get(*index).unwrap()) // get_unchecked - &self.options[*index] // get_unchecked + &self.options[*index as usize] // get_unchecked }) .collect(); diff --git a/helix-term/src/ui/mod.rs b/helix-term/src/ui/mod.rs index c6320f102..cf20f80cb 100644 --- a/helix-term/src/ui/mod.rs +++ b/helix-term/src/ui/mod.rs @@ -2,13 +2,12 @@ mod completion; mod document; pub(crate) mod editor; mod extension; -mod fuzzy_match; mod info; pub mod lsp; mod markdown; pub mod menu; pub mod overlay; -mod picker; +pub mod picker; pub mod popup; mod prompt; mod spinner; @@ -65,7 +64,7 @@ pub fn regex_prompt( prompt: std::borrow::Cow<'static, str>, history_register: Option, completion_fn: impl FnMut(&Editor, &str) -> Vec + 'static, - fun: impl Fn(&mut Editor, Regex, PromptEvent) + 'static, + fun: impl Fn(&mut crate::compositor::Context, Regex, PromptEvent) + 'static, ) { let (view, doc) = current!(cx.editor); let doc_id = view.doc; @@ -112,7 +111,7 @@ pub fn regex_prompt( view.jumps.push((doc_id, snapshot.clone())); } - fun(cx.editor, regex, event); + fun(cx, regex, event); let (view, doc) = current!(cx.editor); view.ensure_cursor_in_view(doc, config.scrolloff); @@ -175,6 +174,7 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker .git_ignore(config.file_picker.git_ignore) .git_global(config.file_picker.git_global) .git_exclude(config.file_picker.git_exclude) + .sort_by_file_name(|name1, name2| name1.cmp(name2)) .max_depth(config.file_picker.max_depth) .filter_entry(move |entry| filter_picker_entry(entry, &absolute_root, dedup_symlinks)); @@ -191,32 +191,16 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker .build() .expect("failed to build excluded_types"); walk_builder.types(excluded_types); - - // We want files along with their modification date for sorting - let files = walk_builder.build().filter_map(|entry| { + let mut files = walk_builder.build().filter_map(|entry| { let entry = entry.ok()?; - // This is faster than entry.path().is_dir() since it uses cached fs::Metadata fetched by ignore/walkdir - if entry.file_type()?.is_file() { - Some(entry.into_path()) - } else { - None + if !entry.file_type()?.is_file() { + return None; } + Some(entry.into_path()) }); - - // Cap the number of files if we aren't in a git project, preventing - // hangs when using the picker in your home directory - let mut files: Vec = if root.join(".git").exists() { - files.collect() - } else { - // const MAX: usize = 8192; - const MAX: usize = 100_000; - files.take(MAX).collect() - }; - files.sort(); - log::debug!("file_picker init {:?}", Instant::now().duration_since(now)); - Picker::new(files, root, move |cx, path: &PathBuf, action| { + let picker = Picker::new(Vec::new(), root, move |cx, path: &PathBuf, action| { if let Err(e) = cx.editor.open(path, action) { let err = if let Some(err) = e.source() { format!("{}", err) @@ -226,20 +210,41 @@ pub fn file_picker(root: PathBuf, config: &helix_view::editor::Config) -> Picker cx.editor.set_error(err); } }) - .with_preview(|_editor, path| Some((path.clone().into(), None))) + .with_preview(|_editor, path| Some((path.clone().into(), None))); + let injector = picker.injector(); + let timeout = std::time::Instant::now() + std::time::Duration::from_millis(30); + + let mut hit_timeout = false; + for file in &mut files { + if injector.push(file).is_err() { + break; + } + if std::time::Instant::now() >= timeout { + hit_timeout = true; + break; + } + } + if hit_timeout { + std::thread::spawn(move || { + for file in files { + if injector.push(file).is_err() { + break; + } + } + }); + } + picker } pub mod completers { use crate::ui::prompt::Completion; - use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; - use fuzzy_matcher::FuzzyMatcher; + use helix_core::fuzzy::fuzzy_match; use helix_core::syntax::LanguageServerFeature; use helix_view::document::SCRATCH_BUFFER_NAME; use helix_view::theme; use helix_view::{editor::Config, Editor}; use once_cell::sync::Lazy; use std::borrow::Cow; - use std::cmp::Reverse; pub type Completer = fn(&Editor, &str) -> Vec; @@ -248,31 +253,16 @@ pub mod completers { } pub fn buffer(editor: &Editor, input: &str) -> Vec { - let mut names: Vec<_> = editor - .documents - .values() - .map(|doc| { - let name = doc - .relative_path() - .map(|p| p.display().to_string()) - .unwrap_or_else(|| String::from(SCRATCH_BUFFER_NAME)); - ((0..), Cow::from(name)) - }) - .collect(); - - let matcher = Matcher::default(); - - let mut matches: Vec<_> = names - .into_iter() - .filter_map(|(_range, name)| { - matcher.fuzzy_match(&name, input).map(|score| (name, score)) - }) - .collect(); - - matches.sort_unstable_by_key(|(_file, score)| Reverse(*score)); - names = matches.into_iter().map(|(name, _)| ((0..), name)).collect(); + let names = editor.documents.values().map(|doc| { + doc.relative_path() + .map(|p| p.display().to_string().into()) + .unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME)) + }); - names + fuzzy_match(input, names, true) + .into_iter() + .map(|(name, _)| ((0..), name)) + .collect() } pub fn theme(_editor: &Editor, input: &str) -> Vec { @@ -285,26 +275,10 @@ pub mod completers { names.sort(); names.dedup(); - let mut names: Vec<_> = names + fuzzy_match(input, names, false) .into_iter() - .map(|name| ((0..), Cow::from(name))) - .collect(); - - let matcher = Matcher::default(); - - let mut matches: Vec<_> = names - .into_iter() - .filter_map(|(_range, name)| { - matcher.fuzzy_match(&name, input).map(|score| (name, score)) - }) - .collect(); - - matches.sort_unstable_by(|(name1, score1), (name2, score2)| { - (Reverse(*score1), name1).cmp(&(Reverse(*score2), name2)) - }); - names = matches.into_iter().map(|(name, _)| ((0..), name)).collect(); - - names + .map(|(name, _)| ((0..), name.into())) + .collect() } /// Recursive function to get all keys from this value and add them to vec @@ -331,15 +305,7 @@ pub mod completers { keys }); - let matcher = Matcher::default(); - - let mut matches: Vec<_> = KEYS - .iter() - .filter_map(|name| matcher.fuzzy_match(name, input).map(|score| (name, score))) - .collect(); - - matches.sort_unstable_by_key(|(_file, score)| Reverse(*score)); - matches + fuzzy_match(input, &*KEYS, false) .into_iter() .map(|(name, _)| ((0..), name.into())) .collect() @@ -366,8 +332,6 @@ pub mod completers { } pub fn language(editor: &Editor, input: &str) -> Vec { - let matcher = Matcher::default(); - let text: String = "text".into(); let language_ids = editor @@ -376,27 +340,13 @@ pub mod completers { .map(|config| &config.language_id) .chain(std::iter::once(&text)); - let mut matches: Vec<_> = language_ids - .filter_map(|language_id| { - matcher - .fuzzy_match(language_id, input) - .map(|score| (language_id, score)) - }) - .collect(); - - matches.sort_unstable_by(|(language1, score1), (language2, score2)| { - (Reverse(*score1), language1).cmp(&(Reverse(*score2), language2)) - }); - - matches + fuzzy_match(input, language_ids, false) .into_iter() - .map(|(language, _score)| ((0..), language.clone().into())) + .map(|(name, _)| ((0..), name.to_owned().into())) .collect() } pub fn lsp_workspace_command(editor: &Editor, input: &str) -> Vec { - let matcher = Matcher::default(); - let Some(options) = doc!(editor) .language_servers_with_feature(LanguageServerFeature::WorkspaceCommand) .find_map(|ls| ls.capabilities().execute_command_provider.as_ref()) @@ -404,23 +354,9 @@ pub mod completers { return vec![]; }; - let mut matches: Vec<_> = options - .commands - .iter() - .filter_map(|command| { - matcher - .fuzzy_match(command, input) - .map(|score| (command, score)) - }) - .collect(); - - matches.sort_unstable_by(|(command1, score1), (command2, score2)| { - (Reverse(*score1), command1).cmp(&(Reverse(*score2), command2)) - }); - - matches + fuzzy_match(input, &options.commands, false) .into_iter() - .map(|(command, _score)| ((0..), command.clone().into())) + .map(|(name, _)| ((0..), name.to_owned().into())) .collect() } @@ -501,7 +437,7 @@ pub mod completers { let end = input.len()..; - let mut files: Vec<_> = WalkBuilder::new(&dir) + let files = WalkBuilder::new(&dir) .hidden(false) .follow_links(false) // We're scanning over depth 1 .git_ignore(git_ignore) @@ -533,43 +469,25 @@ pub mod completers { path.push(""); } - let path = path.to_str()?.to_owned(); - Some((end.clone(), Cow::from(path))) + let path = path.into_os_string().into_string().ok()?; + Some(Cow::from(path)) }) }) // TODO: unwrap or skip - .filter(|(_, path)| !path.is_empty()) // TODO - .collect(); + .filter(|path| !path.is_empty()); // if empty, return a list of dirs and files in current dir if let Some(file_name) = file_name { - let matcher = Matcher::default(); - - // inefficient, but we need to calculate the scores, filter out None, then sort. - let mut matches: Vec<_> = files - .into_iter() - .filter_map(|(_range, file)| { - matcher - .fuzzy_match(&file, &file_name) - .map(|score| (file, score)) - }) - .collect(); - let range = (input.len().saturating_sub(file_name.len()))..; - - matches.sort_unstable_by(|(file1, score1), (file2, score2)| { - (Reverse(*score1), file1).cmp(&(Reverse(*score2), file2)) - }); - - files = matches + fuzzy_match(&file_name, files, true) .into_iter() - .map(|(file, _)| (range.clone(), file)) - .collect(); + .map(|(name, _)| (range.clone(), name)) + .collect() // TODO: complete to longest common match } else { + let mut files: Vec<_> = files.map(|file| (end.clone(), file)).collect(); files.sort_unstable_by(|(_, path1), (_, path2)| path1.cmp(path2)); + files } - - files } } diff --git a/helix-term/src/ui/picker.rs b/helix-term/src/ui/picker.rs index b134eb47e..2c41fc12d 100644 --- a/helix-term/src/ui/picker.rs +++ b/helix-term/src/ui/picker.rs @@ -7,11 +7,12 @@ use crate::{ ui::{ self, document::{render_document, LineDecoration, LinePos, TextRenderer}, - fuzzy_match::FuzzyQuery, EditorView, }, }; use futures_util::{future::BoxFuture, FutureExt}; +use nucleo::pattern::CaseMatching; +use nucleo::{Config, Nucleo, Utf32String}; use tui::{ buffer::Buffer as Surface, layout::Constraint, @@ -19,16 +20,23 @@ use tui::{ widgets::{Block, BorderType, Borders, Cell, Table}, }; -use fuzzy_matcher::skim::SkimMatcherV2 as Matcher; use tui::widgets::Widget; -use std::cmp::{self, Ordering}; -use std::{collections::HashMap, io::Read, path::PathBuf}; +use std::{ + collections::HashMap, + io::Read, + path::PathBuf, + sync::{ + atomic::{self, AtomicBool}, + Arc, + }, +}; use crate::ui::{Prompt, PromptEvent}; use helix_core::{ - char_idx_at_visual_offset, movement::Direction, text_annotations::TextAnnotations, - unicode::segmentation::UnicodeSegmentation, Position, Syntax, + char_idx_at_visual_offset, fuzzy::MATCHER, movement::Direction, + text_annotations::TextAnnotations, unicode::segmentation::UnicodeSegmentation, Position, + Syntax, }; use helix_view::{ editor::Action, @@ -38,6 +46,7 @@ use helix_view::{ Document, DocumentId, Editor, }; +pub const ID: &str = "picker"; use super::{menu::Item, overlay::Overlay}; pub const MIN_AREA_WIDTH_FOR_PREVIEW: u16 = 72; @@ -103,9 +112,9 @@ impl Preview<'_, '_> { /// Alternate text to show for the preview. fn placeholder(&self) -> &str { match *self { - Self::EditorDocument(_) => "", + Self::EditorDocument(_) => "", Self::Cached(preview) => match preview { - CachedPreview::Document(_) => "", + CachedPreview::Document(_) => "", CachedPreview::Binary => "", CachedPreview::LargeFile => "", CachedPreview::NotFound => "", @@ -114,20 +123,71 @@ impl Preview<'_, '_> { } } +fn item_to_nucleo(item: T, editor_data: &T::Data) -> Option<(T, Utf32String)> { + let row = item.format(editor_data); + let mut cells = row.cells.iter(); + let mut text = String::with_capacity(row.cell_text().map(|cell| cell.len()).sum()); + let cell = cells.next()?; + if let Some(cell) = cell.content.lines.first() { + for span in &cell.0 { + text.push_str(&span.content); + } + } + + for cell in cells { + text.push(' '); + if let Some(cell) = cell.content.lines.first() { + for span in &cell.0 { + text.push_str(&span.content); + } + } + } + Some((item, text.into())) +} + +pub struct Injector { + dst: nucleo::Injector, + editor_data: Arc, + shutown: Arc, +} + +impl Clone for Injector { + fn clone(&self) -> Self { + Injector { + dst: self.dst.clone(), + editor_data: self.editor_data.clone(), + shutown: self.shutown.clone(), + } + } +} + +pub struct InjectorShutdown; + +impl Injector { + pub fn push(&self, item: T) -> Result<(), InjectorShutdown> { + if self.shutown.load(atomic::Ordering::Relaxed) { + return Err(InjectorShutdown); + } + + if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) { + self.dst.push(item, |dst| dst[0] = matcher_text); + } + Ok(()) + } +} + pub struct Picker { - options: Vec, - editor_data: T::Data, - // filter: String, - matcher: Box, - matches: Vec, + editor_data: Arc, + shutdown: Arc, + matcher: Nucleo, /// Current height of the completions box completion_height: u16, - cursor: usize, - // pattern: String, + cursor: u32, prompt: Prompt, - previous_pattern: (String, FuzzyQuery), + previous_pattern: String, + /// Whether to show the preview panel (default true) show_preview: bool, /// Constraints for tabular formatting @@ -144,10 +204,59 @@ pub struct Picker { } impl Picker { + pub fn stream(editor_data: T::Data) -> (Nucleo, Injector) { + let matcher = Nucleo::new( + Config::DEFAULT, + Arc::new(helix_event::request_redraw), + None, + 1, + ); + let streamer = Injector { + dst: matcher.injector(), + editor_data: Arc::new(editor_data), + shutown: Arc::new(AtomicBool::new(false)), + }; + (matcher, streamer) + } + pub fn new( options: Vec, editor_data: T::Data, callback_fn: impl Fn(&mut Context, &T, Action) + 'static, + ) -> Self { + let matcher = Nucleo::new( + Config::DEFAULT, + Arc::new(helix_event::request_redraw), + None, + 1, + ); + let injector = matcher.injector(); + for item in options { + if let Some((item, matcher_text)) = item_to_nucleo(item, &editor_data) { + injector.push(item, |dst| dst[0] = matcher_text); + } + } + Self::with( + matcher, + Arc::new(editor_data), + Arc::new(AtomicBool::new(false)), + callback_fn, + ) + } + + pub fn with_stream( + matcher: Nucleo, + injector: Injector, + callback_fn: impl Fn(&mut Context, &T, Action) + 'static, + ) -> Self { + Self::with(matcher, injector.editor_data, injector.shutown, callback_fn) + } + + fn with( + matcher: Nucleo, + editor_data: Arc, + shutdown: Arc, + callback_fn: impl Fn(&mut Context, &T, Action) + 'static, ) -> Self { let prompt = Prompt::new( "".into(), @@ -156,14 +265,13 @@ impl Picker { |_editor: &mut Context, _pattern: &str, _event: PromptEvent| {}, ); - let mut picker = Self { - options, + Self { + matcher, editor_data, - matcher: Box::default(), - matches: Vec::new(), + shutdown, cursor: 0, prompt, - previous_pattern: (String::new(), FuzzyQuery::default()), + previous_pattern: String::new(), truncate_start: true, show_preview: true, callback_fn: Box::new(callback_fn), @@ -172,24 +280,15 @@ impl Picker { preview_cache: HashMap::new(), read_buffer: Vec::with_capacity(1024), file_fn: None, - }; - - picker.calculate_column_widths(); - - // scoring on empty input - // TODO: just reuse score() - picker - .matches - .extend(picker.options.iter().enumerate().map(|(index, option)| { - let text = option.filter_text(&picker.editor_data); - PickerMatch { - index, - score: 0, - len: text.chars().count(), - } - })); + } + } - picker + pub fn injector(&self) -> Injector { + Injector { + dst: self.matcher.injector(), + editor_data: self.editor_data.clone(), + shutown: self.shutdown.clone(), + } } pub fn truncate_start(mut self, truncate_start: bool) -> Self { @@ -202,122 +301,25 @@ impl Picker { preview_fn: impl Fn(&Editor, &T) -> Option + 'static, ) -> Self { self.file_fn = Some(Box::new(preview_fn)); + // assumption: if we have a preview we are matching paths... If this is ever + // not true this could be a separate builder function + self.matcher.update_config(Config::DEFAULT.match_paths()); self } 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; - } + self.matcher.restart(false); + let injector = self.matcher.injector(); + for item in new_options { + if let Some((item, matcher_text)) = item_to_nucleo(item, &self.editor_data) { + injector.push(item, |dst| dst[0] = matcher_text); } - 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(); - - if pattern == &self.previous_pattern.0 { - return; } - - let (query, is_refined) = self - .previous_pattern - .1 - .refine(pattern, &self.previous_pattern.0); - - if pattern.is_empty() { - // Fast path for no pattern. - self.matches.clear(); - self.matches - .extend(self.options.iter().enumerate().map(|(index, option)| { - let text = option.filter_text(&self.editor_data); - PickerMatch { - index, - score: 0, - len: text.chars().count(), - } - })); - } else if is_refined { - // optimization: if the pattern is a more specific version of the previous one - // then we can score the filtered set. - self.matches.retain_mut(|pmatch| { - let option = &self.options[pmatch.index]; - let text = option.sort_text(&self.editor_data); - - match query.fuzzy_match(&text, &self.matcher) { - Some(s) => { - // Update the score - pmatch.score = s; - true - } - None => false, - } - }); - - self.matches.sort_unstable(); - } else { - self.force_score(); - } - - // reset cursor position - self.cursor = 0; - let pattern = self.prompt.line(); - self.previous_pattern.0.clone_from(pattern); - self.previous_pattern.1 = query; - } - - pub fn force_score(&mut self) { - let pattern = self.prompt.line(); - - let query = FuzzyQuery::new(pattern); - self.matches.clear(); - self.matches.extend( - self.options - .iter() - .enumerate() - .filter_map(|(index, option)| { - let text = option.filter_text(&self.editor_data); - - query - .fuzzy_match(&text, &self.matcher) - .map(|score| PickerMatch { - index, - score, - len: text.chars().count(), - }) - }), - ); - - self.matches.sort_unstable(); } /// Move the cursor by a number of lines, either down (`Forward`) or up (`Backward`) - pub fn move_by(&mut self, amount: usize, direction: Direction) { - let len = self.matches.len(); + pub fn move_by(&mut self, amount: u32, direction: Direction) { + let len = self.matcher.snapshot().matched_item_count(); if len == 0 { // No results, can't move. @@ -336,12 +338,12 @@ impl Picker { /// Move the cursor down by exactly one page. After the last page comes the first page. pub fn page_up(&mut self) { - self.move_by(self.completion_height as usize, Direction::Backward); + self.move_by(self.completion_height as u32, Direction::Backward); } /// Move the cursor up by exactly one page. After the first page comes the last page. pub fn page_down(&mut self) { - self.move_by(self.completion_height as usize, Direction::Forward); + self.move_by(self.completion_height as u32, Direction::Forward); } /// Move the cursor to the first entry @@ -351,13 +353,18 @@ impl Picker { /// Move the cursor to the last entry pub fn to_end(&mut self) { - self.cursor = self.matches.len().saturating_sub(1); + self.cursor = self + .matcher + .snapshot() + .matched_item_count() + .saturating_sub(1); } pub fn selection(&self) -> Option<&T> { - self.matches - .get(self.cursor) - .map(|pmatch| &self.options[pmatch.index]) + self.matcher + .snapshot() + .get_matched_item(self.cursor) + .map(|item| item.data) } pub fn toggle_preview(&mut self) { @@ -366,8 +373,17 @@ impl Picker { fn prompt_handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult { if let EventResult::Consumed(_) = self.prompt.handle_event(event, cx) { - // TODO: recalculate only if pattern changed - self.score(); + let pattern = self.prompt.line(); + // TODO: better track how the pattern has changed + if pattern != &self.previous_pattern { + self.matcher.pattern.reparse( + 0, + pattern, + CaseMatching::Smart, + pattern.starts_with(&self.previous_pattern), + ); + self.previous_pattern = pattern.clone(); + } } EventResult::Consumed(None) } @@ -411,12 +427,9 @@ impl Picker { (size, _) if size > MAX_FILE_SIZE_FOR_PREVIEW => { CachedPreview::LargeFile } - _ => { - // TODO: enable syntax highlighting; blocked by async rendering - Document::open(path, None, None, editor.config.clone()) - .map(|doc| CachedPreview::Document(Box::new(doc))) - .unwrap_or(CachedPreview::NotFound) - } + _ => Document::open(path, None, None, editor.config.clone()) + .map(|doc| CachedPreview::Document(Box::new(doc))) + .unwrap_or(CachedPreview::NotFound), }, ) .unwrap_or(CachedPreview::NotFound); @@ -461,9 +474,13 @@ impl Picker { log::info!("highlighting picker item failed"); return; }; - let Some(Overlay { - content: picker, .. - }) = compositor.find::>() + let picker = match compositor.find::>() { + Some(Overlay { content, .. }) => Some(content), + None => compositor + .find::>>() + .map(|overlay| &mut overlay.content.file_picker), + }; + let Some(picker) = picker else { log::info!("picker closed before syntax highlighting finished"); return; @@ -495,6 +512,14 @@ impl Picker { } fn render_picker(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { + let status = self.matcher.tick(10); + let snapshot = self.matcher.snapshot(); + if status.changed { + self.cursor = self + .cursor + .min(snapshot.matched_item_count().saturating_sub(1)) + } + let text_style = cx.editor.theme.get("ui.text"); let selected = cx.editor.theme.get("ui.text.focus"); let highlight_style = cx.editor.theme.get("special").add_modifier(Modifier::BOLD); @@ -515,8 +540,15 @@ impl Picker { // -- Render the input bar: let area = inner.clip_left(1).with_height(1); + // render the prompt first since it will clear its background + self.prompt.render(area, surface, cx); - let count = format!("{}/{}", self.matches.len(), self.options.len()); + let count = format!( + "{}{}/{}", + if status.running { "(running) " } else { "" }, + snapshot.matched_item_count(), + snapshot.item_count(), + ); surface.set_stringn( (area.x + area.width).saturating_sub(count.len() as u16 + 1), area.y, @@ -525,8 +557,6 @@ impl Picker { text_style, ); - self.prompt.render(area, surface, cx); - // -- Separator let sep_style = cx.editor.theme.get("ui.background.separator"); let borders = BorderType::line_symbols(BorderType::Plain); @@ -539,106 +569,89 @@ impl Picker { // -- Render the contents: // subtract area of prompt from top let inner = inner.clip_top(2); - - let rows = inner.height; - let offset = self.cursor - (self.cursor % std::cmp::max(1, rows as usize)); + let rows = inner.height as u32; + let offset = self.cursor - (self.cursor % std::cmp::max(1, rows)); let cursor = self.cursor.saturating_sub(offset); + let end = offset + .saturating_add(rows) + .min(snapshot.matched_item_count()); + let mut indices = Vec::new(); + let mut matcher = MATCHER.lock(); + matcher.config = Config::DEFAULT; + if self.file_fn.is_some() { + matcher.config.set_match_paths() + } - let options = self - .matches - .iter() - .skip(offset) - .take(rows as usize) - .map(|pmatch| &self.options[pmatch.index]) - .map(|option| option.format(&self.editor_data)) - .map(|mut row| { - const TEMP_CELL_SEP: &str = " "; - - let line = row.cell_text().fold(String::new(), |mut s, frag| { - s.push_str(&frag); - s.push_str(TEMP_CELL_SEP); - s - }); - - // Items are filtered by using the text returned by menu::Item::filter_text - // but we do highlighting here using the text in Row and therefore there - // might be inconsistencies. This is the best we can do since only the - // text in Row is displayed to the end user. - let (_score, highlights) = FuzzyQuery::new(self.prompt.line()) - .fuzzy_indices(&line, &self.matcher) - .unwrap_or_default(); - - let highlight_byte_ranges: Vec<_> = line - .char_indices() - .enumerate() - .filter_map(|(char_idx, (byte_offset, ch))| { - highlights - .contains(&char_idx) - .then(|| byte_offset..byte_offset + ch.len_utf8()) - }) - .collect(); - - // The starting byte index of the current (iterating) cell - let mut cell_start_byte_offset = 0; - for cell in row.cells.iter_mut() { - let spans = match cell.content.lines.get(0) { - Some(s) => s, - None => { - cell_start_byte_offset += TEMP_CELL_SEP.len(); - continue; - } - }; + let options = snapshot.matched_items(offset..end).map(|item| { + snapshot.pattern().column_pattern(0).indices( + item.matcher_columns[0].slice(..), + &mut matcher, + &mut indices, + ); + indices.sort_unstable(); + indices.dedup(); + let mut row = item.data.format(&self.editor_data); + + let mut grapheme_idx = 0u32; + let mut indices = indices.drain(..); + let mut next_highlight_idx = indices.next().unwrap_or(u32::MAX); + if self.widths.len() < row.cells.len() { + self.widths.resize(row.cells.len(), Constraint::Length(0)); + } + let mut widths = self.widths.iter_mut(); + for cell in &mut row.cells { + let Some(Constraint::Length(max_width)) = widths.next() else { + unreachable!(); + }; - let mut cell_len = 0; - - let graphemes_with_style: Vec<_> = spans - .0 - .iter() - .flat_map(|span| { - span.content - .grapheme_indices(true) - .zip(std::iter::repeat(span.style)) - }) - .map(|((grapheme_byte_offset, grapheme), style)| { - cell_len += grapheme.len(); - let start = cell_start_byte_offset; - - let grapheme_byte_range = - grapheme_byte_offset..grapheme_byte_offset + grapheme.len(); - - if highlight_byte_ranges.iter().any(|hl_rng| { - hl_rng.start >= start + grapheme_byte_range.start - && hl_rng.end <= start + grapheme_byte_range.end - }) { - (grapheme, style.patch(highlight_style)) - } else { - (grapheme, style) - } - }) - .collect(); - - let mut span_list: Vec<(String, Style)> = Vec::new(); - for (grapheme, style) in graphemes_with_style { - if span_list.last().map(|(_, sty)| sty) == Some(&style) { - let (string, _) = span_list.last_mut().unwrap(); - string.push_str(grapheme); + // merge index highlights on top of existing hightlights + let mut span_list = Vec::new(); + let mut current_span = String::new(); + let mut current_style = Style::default(); + let mut width = 0; + + let spans: &[Span] = cell.content.lines.first().map_or(&[], |it| it.0.as_slice()); + for span in spans { + // this looks like a bug on first glance, we are iterating + // graphemes but treating them as char indices. The reason that + // this is correct is that nucleo will only ever consider the first char + // of a grapheme (and discard the rest of the grapheme) so the indices + // returned by nucleo are essentially grapheme indecies + for grapheme in span.content.graphemes(true) { + let style = if grapheme_idx == next_highlight_idx { + next_highlight_idx = indices.next().unwrap_or(u32::MAX); + span.style.patch(highlight_style) } else { - span_list.push((String::from(grapheme), style)) + span.style + }; + if style != current_style { + if !current_span.is_empty() { + span_list.push(Span::styled(current_span, current_style)) + } + current_span = String::new(); + current_style = style; } + current_span.push_str(grapheme); + grapheme_idx += 1; } + width += span.width(); + } - let spans: Vec = span_list - .into_iter() - .map(|(string, style)| Span::styled(string, style)) - .collect(); - let spans: Spans = spans.into(); - *cell = Cell::from(spans); + span_list.push(Span::styled(current_span, current_style)); + if width as u16 > *max_width { + *max_width = width as u16; + } + *cell = Cell::from(Spans::from(span_list)); - cell_start_byte_offset += cell_len + TEMP_CELL_SEP.len(); + // spacer + if grapheme_idx == next_highlight_idx { + next_highlight_idx = indices.next().unwrap_or(u32::MAX); } + grapheme_idx += 1; + } - row - }); + row + }); let table = Table::new(options) .style(text_style) @@ -654,7 +667,7 @@ impl Picker { surface, &mut TableState { offset: 0, - selected: Some(cursor), + selected: Some(cursor as usize), }, self.truncate_start, ); @@ -680,8 +693,14 @@ impl Picker { if let Some((path, range)) = self.current_file(cx.editor) { let preview = self.get_preview(path, cx.editor); let doc = match preview.document() { - Some(doc) => doc, - None => { + Some(doc) + if range.map_or(true, |(start, end)| { + start <= end && end <= doc.text().len_lines() + }) => + { + doc + } + _ => { let alt_text = preview.placeholder(); let x = inner.x + inner.width.saturating_sub(alt_text.len() as u16) / 2; let y = inner.y + inner.height / 2; @@ -691,18 +710,30 @@ impl Picker { }; let mut offset = ViewPosition::default(); - if let Some(range) = range { - let text_fmt = doc.text_format(inner.width, None); - let annotations = TextAnnotations::default(); - (offset.anchor, offset.vertical_offset) = char_idx_at_visual_offset( - doc.text().slice(..), - doc.text().line_to_char(range.0), - // align to middle - -(inner.height as isize / 2), - 0, - &text_fmt, - &annotations, - ); + if let Some((start_line, end_line)) = range { + let height = end_line - start_line; + let text = doc.text().slice(..); + let start = text.line_to_char(start_line); + let middle = text.line_to_char(start_line + height / 2); + if height < inner.height as usize { + let text_fmt = doc.text_format(inner.width, None); + let annotations = TextAnnotations::default(); + (offset.anchor, offset.vertical_offset) = char_idx_at_visual_offset( + text, + middle, + // align to middle + -(inner.height as isize / 2), + 0, + &text_fmt, + &annotations, + ); + if start < offset.anchor { + offset.anchor = start; + offset.vertical_offset = 0; + } + } else { + offset.anchor = start; + } } let mut highlights = EditorView::doc_syntax_highlights( @@ -755,7 +786,7 @@ impl Picker { } } -impl Component for Picker { +impl Component for Picker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { // +---------+ +---------+ // |prompt | |preview | @@ -794,11 +825,28 @@ impl Component for Picker { _ => return EventResult::Ignored(None), }; - let close_fn = - EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| { - // remove the layer - compositor.last_picker = compositor.pop(); - }))); + let close_fn = |picker: &mut Self| { + // if the picker is very large don't store it as last_picker to avoid + // excessive memory consumption + let callback: compositor::Callback = if picker.matcher.snapshot().item_count() > 100_000 + { + Box::new(|compositor: &mut Compositor, _ctx| { + // remove the layer + compositor.pop(); + }) + } else { + // stop streaming in new items in the background, really we should + // be restarting the stream somehow once the picker gets + // reopened instead (like for an FS crawl) that would also remove the + // need for the special case above but that is pretty tricky + picker.shutdown.store(true, atomic::Ordering::Relaxed); + Box::new(|compositor: &mut Compositor, _ctx| { + // remove the layer + compositor.last_picker = compositor.pop(); + }) + }; + EventResult::Consumed(Some(callback)) + }; // So that idle timeout retriggers ctx.editor.reset_idle_timer(); @@ -822,9 +870,7 @@ impl Component for Picker { key!(End) => { self.to_end(); } - key!(Esc) | ctrl!('c') => { - return close_fn; - } + key!(Esc) | ctrl!('c') => return close_fn(self), alt!(Enter) => { if let Some(option) = self.selection() { (self.callback_fn)(ctx, option, Action::Load); @@ -834,19 +880,19 @@ impl Component for Picker { if let Some(option) = self.selection() { (self.callback_fn)(ctx, option, Action::Replace); } - return close_fn; + return close_fn(self); } ctrl!('s') => { if let Some(option) = self.selection() { (self.callback_fn)(ctx, option, Action::HorizontalSplit); } - return close_fn; + return close_fn(self); } ctrl!('v') => { if let Some(option) = self.selection() { (self.callback_fn)(ctx, option, Action::VerticalSplit); } - return close_fn; + return close_fn(self); } ctrl!('t') => { self.toggle_preview(); @@ -874,30 +920,15 @@ impl Component for Picker { self.completion_height = height.saturating_sub(4); Some((width, height)) } -} - -#[derive(PartialEq, Eq, Debug)] -struct PickerMatch { - score: i64, - index: usize, - len: usize, -} - -impl PickerMatch { - fn key(&self) -> impl Ord { - (cmp::Reverse(self.score), self.len, self.index) - } -} -impl PartialOrd for PickerMatch { - fn partial_cmp(&self, other: &Self) -> Option { - Some(self.cmp(other)) + fn id(&self) -> Option<&'static str> { + Some(ID) } } - -impl Ord for PickerMatch { - fn cmp(&self, other: &Self) -> Ordering { - self.key().cmp(&other.key()) +impl Drop for Picker { + fn drop(&mut self) { + // ensure we cancel any ongoing background threads streaming into the picker + self.shutdown.store(true, atomic::Ordering::Relaxed) } } @@ -910,15 +941,13 @@ pub type DynQueryCallback = /// A picker that updates its contents via a callback whenever the /// query string changes. Useful for live grep, workspace symbols, etc. -pub struct DynamicPicker { +pub struct DynamicPicker { file_picker: Picker, query_callback: DynQueryCallback, query: String, } -impl DynamicPicker { - pub const ID: &'static str = "dynamic-picker"; - +impl DynamicPicker { pub fn new(file_picker: Picker, query_callback: DynQueryCallback) -> Self { Self { file_picker, @@ -928,7 +957,7 @@ impl DynamicPicker { } } -impl Component for DynamicPicker { +impl Component for DynamicPicker { fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) { self.file_picker.render(area, surface, cx); } @@ -950,7 +979,7 @@ impl Component for DynamicPicker { let callback = Callback::EditorCompositor(Box::new(move |editor, compositor| { // Wrapping of pickers in overlay is done outside the picker code, // so this is fragile and will break if wrapped in some other widget. - let picker = match compositor.find_id::>>(Self::ID) { + let picker = match compositor.find_id::>>(ID) { Some(overlay) => &mut overlay.content.file_picker, None => return, }; @@ -971,6 +1000,6 @@ impl Component for DynamicPicker { } fn id(&self) -> Option<&'static str> { - Some(Self::ID) + Some(ID) } } diff --git a/helix-term/tests/integration.rs b/helix-term/tests/integration.rs index 9c0e6bbc7..35214bcb8 100644 --- a/helix-term/tests/integration.rs +++ b/helix-term/tests/integration.rs @@ -20,7 +20,6 @@ mod test { mod commands; mod languages; mod movement; - mod picker; mod prompt; mod splits; } diff --git a/helix-term/tests/test/commands/write.rs b/helix-term/tests/test/commands/write.rs index f33c8aaf6..376ba5e7b 100644 --- a/helix-term/tests/test/commands/write.rs +++ b/helix-term/tests/test/commands/write.rs @@ -93,7 +93,7 @@ async fn test_buffer_close_concurrent() -> anyhow::Result<()> { ) .await?; - helpers::assert_file_has_content(file.as_file_mut(), &RANGE.end().to_string())?; + helpers::assert_file_has_content(file.as_file_mut(), &platform_line(&RANGE.end().to_string()))?; Ok(()) } @@ -209,7 +209,7 @@ async fn test_write_concurrent() -> anyhow::Result<()> { let mut file_content = String::new(); file.as_file_mut().read_to_string(&mut file_content)?; - assert_eq!(RANGE.end().to_string(), file_content); + assert_eq!(platform_line(&RANGE.end().to_string()), file_content); Ok(()) } @@ -424,13 +424,132 @@ async fn test_write_utf_bom_file() -> anyhow::Result<()> { Ok(()) } +#[tokio::test(flavor = "multi_thread")] +async fn test_write_insert_final_newline_added_if_missing() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .with_input_text("#[h|]#ave you tried chamomile tea?") + .build()?; + + test_key_sequence(&mut app, Some(":w"), None, false).await?; + + helpers::assert_file_has_content( + file.as_file_mut(), + &helpers::platform_line("have you tried chamomile tea?\n"), + )?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_write_insert_final_newline_unchanged_if_not_missing() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .with_input_text(&helpers::platform_line("#[t|]#en minutes, please\n")) + .build()?; + + test_key_sequence(&mut app, Some(":w"), None, false).await?; + + helpers::assert_file_has_content( + file.as_file_mut(), + &helpers::platform_line("ten minutes, please\n"), + )?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_write_insert_final_newline_unchanged_if_missing_and_false() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_config(Config { + editor: helix_view::editor::Config { + insert_final_newline: false, + ..Default::default() + }, + ..Default::default() + }) + .with_file(file.path(), None) + .with_input_text("#[t|]#he quiet rain continued through the night") + .build()?; + + test_key_sequence(&mut app, Some(":w"), None, false).await?; + + helpers::assert_file_has_content( + file.as_file_mut(), + "the quiet rain continued through the night", + )?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_write_all_insert_final_newline_add_if_missing_and_modified() -> anyhow::Result<()> { + let mut file1 = tempfile::NamedTempFile::new()?; + let mut file2 = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file1.path(), None) + .with_input_text("#[w|]#e don't serve time travelers here") + .build()?; + + test_key_sequence( + &mut app, + Some(&format!( + ":o {}ia time traveler walks into a bar:wa", + file2.path().to_string_lossy() + )), + None, + false, + ) + .await?; + + helpers::assert_file_has_content( + file1.as_file_mut(), + &helpers::platform_line("we don't serve time travelers here\n"), + )?; + + helpers::assert_file_has_content( + file2.as_file_mut(), + &helpers::platform_line("a time traveler walks into a bar\n"), + )?; + + Ok(()) +} + +#[tokio::test(flavor = "multi_thread")] +async fn test_write_all_insert_final_newline_do_not_add_if_unmodified() -> anyhow::Result<()> { + let mut file = tempfile::NamedTempFile::new()?; + let mut app = helpers::AppBuilder::new() + .with_file(file.path(), None) + .build()?; + + file.write_all(b"i lost on Jeopardy!")?; + file.rewind()?; + + test_key_sequence(&mut app, Some(":wa"), None, false).await?; + + helpers::assert_file_has_content(file.as_file_mut(), "i lost on Jeopardy!")?; + + Ok(()) +} + async fn edit_file_with_content(file_content: &[u8]) -> anyhow::Result<()> { let mut file = tempfile::NamedTempFile::new()?; file.as_file_mut().write_all(&file_content)?; helpers::test_key_sequence( - &mut helpers::AppBuilder::new().build()?, + &mut helpers::AppBuilder::new() + .with_config(Config { + editor: helix_view::editor::Config { + insert_final_newline: false, + ..Default::default() + }, + ..Default::default() + }) + .build()?, Some(&format!(":o {}:x", file.path().to_string_lossy())), None, true, diff --git a/helix-term/tests/test/helpers.rs b/helix-term/tests/test/helpers.rs index 6466bc764..e6762baf9 100644 --- a/helix-term/tests/test/helpers.rs +++ b/helix-term/tests/test/helpers.rs @@ -350,7 +350,7 @@ pub fn assert_file_has_content(file: &mut File, content: &str) -> anyhow::Result let mut file_content = String::new(); file.read_to_string(&mut file_content)?; - assert_eq!(content, file_content); + assert_eq!(file_content, content); Ok(()) } diff --git a/helix-term/tests/test/movement.rs b/helix-term/tests/test/movement.rs index 9a48cdbcb..e3c2668da 100644 --- a/helix-term/tests/test/movement.rs +++ b/helix-term/tests/test/movement.rs @@ -513,3 +513,42 @@ async fn select_mode_tree_sitter_prev_function_goes_backwards_to_object() -> any Ok(()) } + +#[tokio::test(flavor = "multi_thread")] +async fn find_char_line_ending() -> anyhow::Result<()> { + test(( + helpers::platform_line(indoc! { + "\ + one + #[|t]#wo + three" + }), + "Tgll2f", + helpers::platform_line(indoc! { + "\ + one + two#[ + |]#three" + }), + )) + .await?; + + test(( + helpers::platform_line(indoc! { + "\ + #[|o]#ne + two + three" + }), + "f2tghTF", + helpers::platform_line(indoc! { + "\ + one#[| + t]#wo + three" + }), + )) + .await?; + + Ok(()) +} diff --git a/helix-term/tests/test/picker.rs b/helix-term/tests/test/picker.rs deleted file mode 100644 index 89e6531f9..000000000 --- a/helix-term/tests/test/picker.rs +++ /dev/null @@ -1,80 +0,0 @@ -use std::fs; - -use helix_core::{path::get_canonicalized_path, Range}; -use helix_loader::{current_working_dir, set_current_working_dir}; -use helix_view::{current_ref, editor::Action}; -use tempfile::{Builder, TempDir}; - -use super::*; - -#[tokio::test(flavor = "multi_thread")] -async fn test_picker_alt_ret() -> anyhow::Result<()> { - // Create two files, open the first and run a global search for a word - // from the second file. Press to have helix open the second file in the - // new buffer, but not change focus. Then check whether the word is highlighted - // correctly and the view of the first file has not changed. - let tmp_dir = TempDir::new()?; - set_current_working_dir(tmp_dir.path().into())?; - - let mut app = AppBuilder::new().build()?; - - log::debug!( - "set current working directory to {:?}", - current_working_dir() - ); - - // Add prefix so helix doesn't hide these files in a picker - let files = [ - Builder::new().prefix("1").tempfile_in(&tmp_dir)?, - Builder::new().prefix("2").tempfile_in(&tmp_dir)?, - ]; - let paths = files - .iter() - .map(|f| get_canonicalized_path(f.path())) - .collect::>(); - - fs::write(&paths[0], "1\n2\n3\n4")?; - fs::write(&paths[1], "first\nsecond")?; - - log::debug!( - "created and wrote two temporary files: {:?} & {:?}", - paths[0], - paths[1] - ); - - // Manually open to save the offset, otherwise we won't be able to change the state in the Fn trait - app.editor.open(files[0].path(), Action::Replace)?; - let view_offset = current_ref!(app.editor).0.offset; - - test_key_sequences( - &mut app, - vec![ - (Some("/"), None), - (Some("second"), None), - ( - Some(""), - Some(&|app| { - let (view, doc) = current_ref!(app.editor); - assert_eq!(doc.path().unwrap(), &paths[0]); - let select_ranges = doc.selection(view.id).ranges(); - assert_eq!(select_ranges[0], Range::new(0, 1)); - assert_eq!(view.offset, view_offset); - }), - ), - ( - Some(":buffernext"), - Some(&|app| { - let (view, doc) = current_ref!(app.editor); - assert_eq!(doc.path().unwrap(), &paths[1]); - let select_ranges = doc.selection(view.id).ranges(); - assert_eq!(select_ranges.len(), 1); - assert_eq!(select_ranges[0], Range::new(6, 12)); - }), - ), - ], - false, - ) - .await?; - - Ok(()) -} diff --git a/helix-term/tests/test/splits.rs b/helix-term/tests/test/splits.rs index 1d70f24a6..f010c86ba 100644 --- a/helix-term/tests/test/splits.rs +++ b/helix-term/tests/test/splits.rs @@ -62,9 +62,9 @@ async fn test_split_write_quit_all() -> anyhow::Result<()> { ) .await?; - helpers::assert_file_has_content(file1.as_file_mut(), "hello1")?; - helpers::assert_file_has_content(file2.as_file_mut(), "hello2")?; - helpers::assert_file_has_content(file3.as_file_mut(), "hello3")?; + helpers::assert_file_has_content(file1.as_file_mut(), &platform_line("hello1"))?; + helpers::assert_file_has_content(file2.as_file_mut(), &platform_line("hello2"))?; + helpers::assert_file_has_content(file3.as_file_mut(), &platform_line("hello3"))?; Ok(()) } diff --git a/helix-tui/src/backend/crossterm.rs b/helix-tui/src/backend/crossterm.rs index d276dea0c..52841f6ef 100644 --- a/helix-tui/src/backend/crossterm.rs +++ b/helix-tui/src/backend/crossterm.rs @@ -328,6 +328,9 @@ impl ModifierDiff { if removed.contains(Modifier::SLOW_BLINK) || removed.contains(Modifier::RAPID_BLINK) { queue!(w, SetAttribute(CAttribute::NoBlink))?; } + if removed.contains(Modifier::HIDDEN) { + queue!(w, SetAttribute(CAttribute::NoHidden))?; + } let added = self.to - self.from; if added.contains(Modifier::REVERSED) { @@ -351,6 +354,9 @@ impl ModifierDiff { if added.contains(Modifier::RAPID_BLINK) { queue!(w, SetAttribute(CAttribute::RapidBlink))?; } + if added.contains(Modifier::HIDDEN) { + queue!(w, SetAttribute(CAttribute::Hidden))?; + } Ok(()) } diff --git a/helix-vcs/Cargo.toml b/helix-vcs/Cargo.toml index eb0dd4763..1b6cd0635 100644 --- a/helix-vcs/Cargo.toml +++ b/helix-vcs/Cargo.toml @@ -12,6 +12,7 @@ homepage = "https://helix-editor.com" [dependencies] helix-core = { version = "0.6", path = "../helix-core" } +helix-event = { version = "0.6", path = "../helix-event" } tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "parking_lot", "macros"] } parking_lot = "0.12" diff --git a/helix-vcs/src/diff.rs b/helix-vcs/src/diff.rs index ca33dda44..c72deb7ea 100644 --- a/helix-vcs/src/diff.rs +++ b/helix-vcs/src/diff.rs @@ -2,10 +2,10 @@ use std::ops::Range; use std::sync::Arc; use helix_core::Rope; +use helix_event::RenderLockGuard; use imara_diff::Algorithm; use parking_lot::{Mutex, MutexGuard}; use tokio::sync::mpsc::{unbounded_channel, UnboundedSender}; -use tokio::sync::{Notify, OwnedRwLockReadGuard, RwLock}; use tokio::task::JoinHandle; use tokio::time::Instant; @@ -14,11 +14,9 @@ use crate::diff::worker::DiffWorker; mod line_cache; mod worker; -type RedrawHandle = (Arc, Arc>); - /// A rendering lock passed to the differ the prevents redraws from occurring struct RenderLock { - pub lock: OwnedRwLockReadGuard<()>, + pub lock: RenderLockGuard, pub timeout: Option, } @@ -38,28 +36,22 @@ struct DiffInner { #[derive(Clone, Debug)] pub struct DiffHandle { channel: UnboundedSender, - render_lock: Arc>, diff: Arc>, inverted: bool, } impl DiffHandle { - pub fn new(diff_base: Rope, doc: Rope, redraw_handle: RedrawHandle) -> DiffHandle { - DiffHandle::new_with_handle(diff_base, doc, redraw_handle).0 + pub fn new(diff_base: Rope, doc: Rope) -> DiffHandle { + DiffHandle::new_with_handle(diff_base, doc).0 } - fn new_with_handle( - diff_base: Rope, - doc: Rope, - redraw_handle: RedrawHandle, - ) -> (DiffHandle, JoinHandle<()>) { + fn new_with_handle(diff_base: Rope, doc: Rope) -> (DiffHandle, JoinHandle<()>) { let (sender, receiver) = unbounded_channel(); let diff: Arc> = Arc::default(); let worker = DiffWorker { channel: receiver, diff: diff.clone(), new_hunks: Vec::default(), - redraw_notify: redraw_handle.0, diff_finished_notify: Arc::default(), }; let handle = tokio::spawn(worker.run(diff_base, doc)); @@ -67,7 +59,6 @@ impl DiffHandle { channel: sender, diff, inverted: false, - render_lock: redraw_handle.1, }; (differ, handle) } @@ -87,11 +78,7 @@ impl DiffHandle { /// This function is only intended to be called from within the rendering loop /// if called from elsewhere it may fail to acquire the render lock and panic pub fn update_document(&self, doc: Rope, block: bool) -> bool { - // unwrap is ok here because the rendering lock is - // only exclusively locked during redraw. - // This function is only intended to be called - // from the core rendering loop where no redraw can happen in parallel - let lock = self.render_lock.clone().try_read_owned().unwrap(); + let lock = helix_event::lock_frame(); let timeout = if block { None } else { diff --git a/helix-vcs/src/diff/worker.rs b/helix-vcs/src/diff/worker.rs index 5406446fd..3a9b6462c 100644 --- a/helix-vcs/src/diff/worker.rs +++ b/helix-vcs/src/diff/worker.rs @@ -23,7 +23,6 @@ pub(super) struct DiffWorker { pub channel: UnboundedReceiver, pub diff: Arc>, pub new_hunks: Vec, - pub redraw_notify: Arc, pub diff_finished_notify: Arc, } @@ -32,11 +31,7 @@ impl DiffWorker { let mut accumulator = EventAccumulator::new(); accumulator.handle_event(event).await; accumulator - .accumulate_debounced_events( - &mut self.channel, - self.redraw_notify.clone(), - self.diff_finished_notify.clone(), - ) + .accumulate_debounced_events(&mut self.channel, self.diff_finished_notify.clone()) .await; (accumulator.doc, accumulator.diff_base) } @@ -137,7 +132,6 @@ impl<'a> EventAccumulator { async fn accumulate_debounced_events( &mut self, channel: &mut UnboundedReceiver, - redraw_notify: Arc, diff_finished_notify: Arc, ) { let async_debounce = Duration::from_millis(DIFF_DEBOUNCE_TIME_ASYNC); @@ -164,7 +158,7 @@ impl<'a> EventAccumulator { None => { tokio::spawn(async move { diff_finished_notify.notified().await; - redraw_notify.notify_one(); + helix_event::request_redraw(); }); } // diff is performed inside the rendering loop @@ -190,7 +184,7 @@ impl<'a> EventAccumulator { // and wait until the diff occurs to trigger an async redraw log::info!("Diff computation timed out, update of diffs might appear delayed"); diff_finished_notify.notified().await; - redraw_notify.notify_one(); + helix_event::request_redraw() }); } // a blocking diff is performed inside the rendering loop diff --git a/helix-vcs/src/diff/worker/test.rs b/helix-vcs/src/diff/worker/test.rs index 6a68d987c..a6cc89007 100644 --- a/helix-vcs/src/diff/worker/test.rs +++ b/helix-vcs/src/diff/worker/test.rs @@ -5,11 +5,7 @@ use crate::diff::{DiffHandle, Hunk}; impl DiffHandle { fn new_test(diff_base: &str, doc: &str) -> (DiffHandle, JoinHandle<()>) { - DiffHandle::new_with_handle( - Rope::from_str(diff_base), - Rope::from_str(doc), - Default::default(), - ) + DiffHandle::new_with_handle(Rope::from_str(diff_base), Rope::from_str(doc)) } async fn into_diff(self, handle: JoinHandle<()>) -> Vec { let diff = self.diff; diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index e1fc8f58d..1ecec223d 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -17,6 +17,7 @@ steel = ["dep:steel-core", "helix-core/steel"] bitflags = "2.4" anyhow = "1" helix-core = { version = "0.6", path = "../helix-core" } +helix-event = { version = "0.6", path = "../helix-event" } helix-loader = { version = "0.6", path = "../helix-loader" } helix-lsp = { version = "0.6", path = "../helix-lsp" } helix-dap = { version = "0.6", path = "../helix-dap" } diff --git a/helix-view/src/clipboard.rs b/helix-view/src/clipboard.rs index d639902f7..812c803e9 100644 --- a/helix-view/src/clipboard.rs +++ b/helix-view/src/clipboard.rs @@ -73,9 +73,14 @@ pub fn get_clipboard_provider() -> Box { #[cfg(target_os = "macos")] pub fn get_clipboard_provider() -> Box { - use crate::env::binary_exists; + use crate::env::{binary_exists, env_var_is_set}; - if binary_exists("pbcopy") && binary_exists("pbpaste") { + if env_var_is_set("TMUX") && binary_exists("tmux") { + command_provider! { + paste => "tmux", "save-buffer", "-"; + copy => "tmux", "load-buffer", "-w", "-"; + } + } else if binary_exists("pbcopy") && binary_exists("pbpaste") { command_provider! { paste => "pbpaste"; copy => "pbcopy"; diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index e2a89e72f..fd5294524 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -33,7 +33,7 @@ use helix_core::{ ChangeSet, Diagnostic, LineEnding, Range, Rope, RopeBuilder, Selection, Syntax, Transaction, }; -use crate::editor::{Config, RedrawHandle}; +use crate::editor::Config; use crate::{DocumentId, Editor, Theme, View, ViewId}; /// 8kB of buffer space for encoding and decoding `Rope`s. @@ -736,16 +736,16 @@ impl Document { // We can't use anyhow::Result here since the output of the future has to be // clonable to be used as shared future. So use a custom error type. pub fn format(&self) -> Option>> { - if let Some(formatter) = self + if let Some((fmt_cmd, fmt_args)) = self .language_config() - .and_then(|c| c.formatter.clone()) - .filter(|formatter| which::which(&formatter.command).is_ok()) + .and_then(|c| c.formatter.as_ref()) + .and_then(|formatter| Some((which::which(&formatter.command).ok()?, &formatter.args))) { use std::process::Stdio; let text = self.text().clone(); - let mut process = tokio::process::Command::new(&formatter.command); + let mut process = tokio::process::Command::new(&fmt_cmd); process - .args(&formatter.args) + .args(fmt_args) .stdin(Stdio::piped()) .stdout(Stdio::piped()) .stderr(Stdio::piped()); @@ -754,7 +754,7 @@ impl Document { let mut process = process .spawn() .map_err(|e| FormatterError::SpawningFailed { - command: formatter.command.clone(), + command: fmt_cmd.to_string_lossy().into(), error: e.kind(), })?; { @@ -998,7 +998,6 @@ impl Document { &mut self, view: &mut View, provider_registry: &DiffProviderRegistry, - redraw_handle: RedrawHandle, ) -> Result<(), Error> { let encoding = self.encoding; let path = self @@ -1026,7 +1025,7 @@ impl Document { self.detect_indent_and_line_ending(); match provider_registry.get_diff_base(&path) { - Some(diff_base) => self.set_diff_base(diff_base, redraw_handle), + Some(diff_base) => self.set_diff_base(diff_base), None => self.diff_handle = None, } @@ -1210,7 +1209,7 @@ impl Document { transaction.changes(), ); if res.is_err() { - log::error!("TS parser failed, disabeling TS for the current buffer: {res:?}"); + log::error!("TS parser failed, disabling TS for the current buffer: {res:?}"); self.syntax = None; } } @@ -1586,13 +1585,13 @@ impl Document { } /// Intialize/updates the differ for this document with a new base. - pub fn set_diff_base(&mut self, diff_base: Vec, redraw_handle: RedrawHandle) { + pub fn set_diff_base(&mut self, diff_base: Vec) { if let Ok((diff_base, ..)) = from_reader(&mut diff_base.as_slice(), Some(self.encoding)) { if let Some(differ) = &self.diff_handle { differ.update_diff_base(diff_base); return; } - self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone(), redraw_handle)) + self.diff_handle = Some(DiffHandle::new(diff_base, self.text.clone())) } else { self.diff_handle = None; } diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index a12456c65..7aa5aa483 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -32,7 +32,7 @@ use std::{ use tokio::{ sync::{ mpsc::{unbounded_channel, UnboundedReceiver, UnboundedSender}, - oneshot, Notify, RwLock, + oneshot, }, time::{sleep, Duration, Instant, Sleep}, }; @@ -244,7 +244,7 @@ pub struct Config { /// 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. + /// Used for autocompletion, set to 0 for instant. Defaults to 250ms. #[serde( serialize_with = "serialize_duration_millis", deserialize_with = "deserialize_duration_millis" @@ -287,6 +287,8 @@ pub struct Config { pub workspace_lsp_roots: Vec, /// Which line ending to choose for new documents. Defaults to `native`. i.e. `crlf` on Windows, otherwise `lf`. pub default_line_ending: LineEndingConfig, + /// Whether to automatically insert a trailing line-ending on write if missing. Defaults to `true`. + pub insert_final_newline: bool, /// Enables smart tab pub smart_tab: Option, } @@ -820,7 +822,7 @@ impl Default for Config { auto_completion: true, auto_format: true, auto_save: false, - idle_timeout: Duration::from_millis(400), + idle_timeout: Duration::from_millis(250), preview_completion_insert: true, completion_trigger_len: 2, auto_info: true, @@ -845,6 +847,7 @@ impl Default for Config { completion_replace: false, workspace_lsp_roots: Vec::new(), default_line_ending: LineEndingConfig::default(), + insert_final_newline: true, smart_tab: Some(SmartTabConfig::default()), } } @@ -929,10 +932,6 @@ pub struct Editor { pub exit_code: i32, pub config_events: (UnboundedSender, UnboundedReceiver), - /// Allows asynchronous tasks to control the rendering - /// The `Notify` allows asynchronous tasks to request the editor to perform a redraw - /// The `RwLock` blocks the editor from performing the render until an exclusive lock can be acquired - pub redraw_handle: RedrawHandle, pub needs_redraw: bool, /// Cached position of the cursor calculated during rendering. /// The content of `cursor_cache` is returned by `Editor::cursor` if @@ -959,8 +958,6 @@ pub struct Editor { pub type Motion = Box; -pub type RedrawHandle = (Arc, Arc>); - #[derive(Debug)] pub enum EditorEvent { DocumentSaved(DocumentSavedEventResult), @@ -1066,7 +1063,6 @@ impl Editor { auto_pairs, exit_code: 0, config_events: unbounded_channel(), - redraw_handle: Default::default(), needs_redraw: false, cursor_cache: Cell::new(None), completion_request_handle: None, @@ -1459,7 +1455,7 @@ impl Editor { )?; if let Some(diff_base) = self.diff_providers.get_diff_base(&path) { - doc.set_diff_base(diff_base, self.redraw_handle.clone()); + doc.set_diff_base(diff_base); } doc.set_version_control_head(self.diff_providers.get_current_head_name(&path)); @@ -1762,7 +1758,7 @@ impl Editor { return EditorEvent::DebuggerEvent(event) } - _ = self.redraw_handle.0.notified() => { + _ = helix_event::redraw_requested() => { if !self.needs_redraw{ self.needs_redraw = true; let timeout = Instant::now() + Duration::from_millis(33); diff --git a/helix-view/src/theme.rs b/helix-view/src/theme.rs index bf3379ca9..4acc56648 100644 --- a/helix-view/src/theme.rs +++ b/helix-view/src/theme.rs @@ -297,6 +297,11 @@ impl Theme { self.highlights[index] } + #[inline] + pub fn scope(&self, index: usize) -> &str { + &self.scopes[index] + } + pub fn name(&self) -> &str { &self.name } @@ -359,6 +364,7 @@ impl Default for ThemePalette { fn default() -> Self { Self { palette: hashmap! { + "default".to_string() => Color::Reset, "black".to_string() => Color::Black, "red".to_string() => Color::Red, "green".to_string() => Color::Green, diff --git a/languages.toml b/languages.toml index d8fb44a6e..37d2e94cf 100644 --- a/languages.toml +++ b/languages.toml @@ -38,6 +38,7 @@ jsonnet-language-server = { command = "jsonnet-language-server", args= ["-t", "- julia = { command = "julia", timeout = 60, args = [ "--startup-file=no", "--history-file=no", "--quiet", "-e", "using LanguageServer; runserver()", ] } kotlin-language-server = { command = "kotlin-language-server" } lean = { command = "lean", args = [ "--server" ] } +ltex-ls = { command = "ltex-ls" } markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] } marksman = { command = "marksman", args = ["server"] } metals = { command = "metals", config = { "isHttpEnabled" = true } } @@ -586,7 +587,7 @@ scope = "source.ts" injection-regex = "(ts|typescript)" file-types = ["ts", "mts", "cts"] language-id = "typescript" -shebangs = [] +shebangs = ["deno", "ts-node"] roots = [] language-servers = [ "typescript-language-server" ] indent = { tab-width = 2, unit = " " } @@ -820,6 +821,7 @@ name = "julia" scope = "source.julia" injection-regex = "julia" file-types = ["jl"] +shebangs = ["julia"] roots = ["Manifest.toml", "Project.toml"] comment-token = "#" language-servers = [ "julia" ] @@ -833,7 +835,7 @@ source = { git = "https://github.com/tree-sitter/tree-sitter-julia", rev = "8fb3 name = "java" scope = "source.java" injection-regex = "java" -file-types = ["java"] +file-types = ["java", "jav"] roots = ["pom.xml", "build.gradle", "build.gradle.kts"] language-servers = [ "jdtls" ] indent = { tab-width = 2, unit = " " } @@ -873,7 +875,7 @@ name = "ocaml" scope = "source.ocaml" injection-regex = "ocaml" file-types = ["ml"] -shebangs = [] +shebangs = ["ocaml", "ocamlrun", "ocamlscript"] roots = [] comment-token = "(**)" language-servers = [ "ocamllsp" ] @@ -884,7 +886,6 @@ indent = { tab-width = 2, unit = " " } '{' = '}' '[' = ']' '"' = '"' -'`' = '`' [[grammar]] name = "ocaml" @@ -905,7 +906,6 @@ indent = { tab-width = 2, unit = " " } '{' = '}' '[' = ']' '"' = '"' -'`' = '`' [[grammar]] name = "ocaml-interface" @@ -1118,7 +1118,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "perl" -source = { git = "https://github.com/tree-sitter-perl/tree-sitter-perl", rev = "ed21ecbcc128a6688770ebafd3ef68a1c9bc1ea9" } +source = { git = "https://github.com/tree-sitter-perl/tree-sitter-perl", rev = "9f3166800d40267fa68ed8273e96baf74f390517" } [[language]] name = "pod" @@ -1379,7 +1379,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev = name = "git-ignore" scope = "source.gitignore" roots = [] -file-types = [".gitignore", ".gitignore_global"] +file-types = [".gitignore", ".gitignore_global", ".ignore", ".prettierignore", ".eslintignore", ".npmignore", "CODEOWNERS"] injection-regex = "git-ignore" comment-token = "#" grammar = "gitignore" @@ -1540,10 +1540,11 @@ roots = ["gleam.toml"] comment-token = "//" indent = { tab-width = 2, unit = " " } language-servers = [ "gleam" ] +auto-format = true [[grammar]] name = "gleam" -source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "ae79782c00656945db69641378e688cdb78d52c1" } +source = { git = "https://github.com/gleam-lang/tree-sitter-gleam", rev = "a59aadf3d7c11702cad244e7cd6b67b34ca9c16a" } [[language]] name = "ron" @@ -1822,6 +1823,7 @@ name = "scheme" scope = "source.scheme" injection-regex = "scheme" file-types = ["ss", "scm"] +shebangs = ["scheme", "guile", "chicken"] roots = [] comment-token = ";" indent = { tab-width = 2, unit = " " } @@ -2067,7 +2069,7 @@ roots = ["edgedb.toml"] [[grammar]] name ="esdl" -source = { git = "https://github.com/greym0uth/tree-sitter-esdl", rev = "b840c8a8028127e0a7c6e6c45141adade2bd75cf" } +source = { git = "https://github.com/greym0uth/tree-sitter-esdl", rev = "df83acc8cacd0cfb139eecee0e718dc32c4f92e2" } [[language]] name = "pascal" @@ -2200,7 +2202,7 @@ source = { git = "https://github.com/Unoqwy/tree-sitter-kdl", rev = "e1cd292c6d1 name = "xml" scope = "source.xml" injection-regex = "xml" -file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg", "xsd", "gml", "xaml", "gir"] +file-types = ["xml", "mobileconfig", "plist", "xib", "storyboard", "svg", "xsd", "gml", "xaml", "gir", "rss", "atom", "opml"] indent = { tab-width = 2, unit = " " } roots = [] @@ -2350,7 +2352,7 @@ indent = { tab-width = 2, unit = " " } [[grammar]] name = "matlab" -source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "676117eafa64afedc8380a921a77cd9f2244bc6b" } +source = { git = "https://github.com/acristoffers/tree-sitter-matlab", rev = "6071891a8c39600203eba20513666cf93b4d650a" } [[language]] name = "ponylang" @@ -2663,7 +2665,7 @@ indent = { tab-width = 4, unit = " " } [[grammar]] name = "blueprint" -source = { git = "https://gitlab.com/gabmus/tree-sitter-blueprint", rev = "7f1a5df44861291d6951b6b2146a9fef4c226e14" } +source = { git = "https://gitlab.com/gabmus/tree-sitter-blueprint", rev = "863cea9f83ad5637300478e0559262f1e791684b" } [[language]] name = "forth" @@ -2792,4 +2794,14 @@ roots = [] [[grammar]] name = "strace" -source = { git = "https://github.com/sigmaSd/tree-sitter-strace", rev = "a0f6c50ae4087a9299f055d0f30fe94fd98189a4" } +source = { git = "https://github.com/sigmaSd/tree-sitter-strace", rev = "2b18fdf9a01e7ec292cc6006724942c81beb7fd5" } + +[[language]] +name = "gemini" +scope = "source.gmi" +file-types = ["gmi"] +roots = [] + +[[grammar]] +name = "gemini" +source = { git = "https://git.sr.ht/~sfr/tree-sitter-gemini", rev = "3cc5e4bdf572d5df4277fc2e54d6299bd59a54b3" } diff --git a/runtime/queries/blueprint/highlights.scm b/runtime/queries/blueprint/highlights.scm index 05533cea4..41e6aa8ce 100644 --- a/runtime/queries/blueprint/highlights.scm +++ b/runtime/queries/blueprint/highlights.scm @@ -15,6 +15,11 @@ (decorator) @attribute (property_definition (property_name) @variable.other.member) +(property_definition + (property_binding + "bind" @keyword + (property_name) @variable.other.member + ["no-sync-create" "bidirectional" "inverted"]* @keyword)) (object) @type diff --git a/runtime/queries/dart/highlights.scm b/runtime/queries/dart/highlights.scm index 9f667d6be..dab584962 100644 --- a/runtime/queries/dart/highlights.scm +++ b/runtime/queries/dart/highlights.scm @@ -200,6 +200,7 @@ "async" "async*" "await" + "base" "class" "covariant" "deferred" @@ -219,6 +220,7 @@ "operator" "part" "required" + "sealed" "set" "show" "static" @@ -230,7 +232,7 @@ ; when used as an identifier: ((identifier) @variable.builtin - (#match? @variable.builtin "^(abstract|as|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|set|static|typedef)$")) + (#match? @variable.builtin "^(abstract|as|base|covariant|deferred|dynamic|export|external|factory|Function|get|implements|import|interface|library|operator|mixin|part|sealed|set|static|typedef)$")) ; Error (ERROR) @error diff --git a/runtime/queries/gemini/highlights.scm b/runtime/queries/gemini/highlights.scm new file mode 100644 index 000000000..f98c85326 --- /dev/null +++ b/runtime/queries/gemini/highlights.scm @@ -0,0 +1,26 @@ +(link) @punctuation.bracket +(link + label: (text) @markup.link.label) +(link + uri: (uri) @markup.link.url) + +[ + (start_pre) + (pre) + (end_pre) +] @markup.raw.block +(start_pre + alt: (text) @label) + +(heading1 + (text) @markup.heading.1) @markup.heading.marker +(heading2 + (text) @markup.heading.2) @markup.heading.marker +(heading3 + (text) @markup.heading.3) @markup.heading.marker + +(ulist + (indicator) @markup.list.unnumbered) +(quote + (indicator) @markup.quote + (text) @markup.italic) diff --git a/runtime/queries/perl/indents.scm b/runtime/queries/perl/indents.scm new file mode 100644 index 000000000..03318243b --- /dev/null +++ b/runtime/queries/perl/indents.scm @@ -0,0 +1,29 @@ +[ + (block) + (conditional_statement) + (loop_statement) + (cstyle_for_statement) + (for_statement) + (elsif) + (array_element_expression) + (hash_element_expression) + (coderef_call_expression) + (anonymous_slice_expression) + (slice_expression) + (keyval_expression) + (anonymous_array_expression) + (anonymous_hash_expression) + (stub_expression) + (func0op_call_expression) + (func1op_call_expression) + (map_grep_expression) + (function_call_expression) + (method_call_expression) + (attribute) +] @indent + +[ + "}" + "]" + ")" +] @outdent diff --git a/runtime/queries/perl/textobjects.scm b/runtime/queries/perl/textobjects.scm new file mode 100644 index 000000000..1b0b5f076 --- /dev/null +++ b/runtime/queries/perl/textobjects.scm @@ -0,0 +1,14 @@ +(subroutine_declaration_statement + body: (_) @function.inside) @function.around +(anonymous_subroutine_expression + body: (_) @function.inside) @function.around + +(package_statement) @class.around +(package_statement + (block) @class.inside) + +(list_expression + (_) @parameter.inside) + +(comment) @comment.around +(pod) @comment.around diff --git a/runtime/queries/scheme/highlights.scm b/runtime/queries/scheme/highlights.scm index 468193745..c7050847f 100644 --- a/runtime/queries/scheme/highlights.scm +++ b/runtime/queries/scheme/highlights.scm @@ -97,3 +97,8 @@ ["(" ")" "[" "]" "{" "}"] @punctuation.bracket +(quote "'") @operator +(unquote_splicing ",@") @operator +(unquote ",") @operator +(quasiquote "`") @operator + diff --git a/runtime/queries/yaml/injections.scm b/runtime/queries/yaml/injections.scm index 321c90add..52b437a4e 100644 --- a/runtime/queries/yaml/injections.scm +++ b/runtime/queries/yaml/injections.scm @@ -1,2 +1,54 @@ ((comment) @injection.content (#set! injection.language "comment")) + +; The remaining code in this file incorporates work covered by the following +; copyright and permission notice: +; +; Copyright 2023 the nvim-treesitter authors +; +; Licensed under the Apache License, Version 2.0 (the "License"); +; you may not use this file except in compliance with the License. +; You may obtain a copy of the License at +; +; http://www.apache.org/licenses/LICENSE-2.0 +; +; Unless required by applicable law or agreed to in writing, software +; distributed under the License is distributed on an "AS IS" BASIS, +; WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +; See the License for the specific language governing permissions and +; limitations under the License. + +; Modified for Helix from https://github.com/nvim-treesitter/nvim-treesitter/blob/master/queries/yaml/injections.scm + +;; Github actions ("run") / Gitlab CI ("scripts") +(block_mapping_pair + key: (flow_node) @_run (#match? @_run "^(run|script|before_script|after_script)$") + value: (flow_node + (plain_scalar + (string_scalar) @injection.content) + (#set! injection.language "bash"))) + +(block_mapping_pair + key: (flow_node) @_run (#match? @_run "^(run|script|before_script|after_script)$") + value: (block_node + (block_scalar) @injection.content + (#set! injection.language "bash"))) + +(block_mapping_pair + key: (flow_node) @_run (#match? @_run "^(run|script|before_script|after_script)$") + value: (block_node + (block_sequence + (block_sequence_item + (flow_node + (plain_scalar + (string_scalar) @injection.content)) + (#set! injection.language "bash"))))) + +(block_mapping_pair + key: (flow_node) @_run (#match? @_run "^(run|script|before_script|after_script)$") + value: (block_node + (block_sequence + (block_sequence_item + (block_node + (block_scalar) @injection.content + (#set! injection.language "bash")))))) diff --git a/runtime/themes/catppuccin_frappe.toml b/runtime/themes/catppuccin_frappe.toml index eb1d47194..b63e5270e 100644 --- a/runtime/themes/catppuccin_frappe.toml +++ b/runtime/themes/catppuccin_frappe.toml @@ -34,3 +34,5 @@ crust = "#232634" # derived colors by blending existing palette colors cursorline = "#3b3f52" secondary_cursor = "#b8a5a6" +secondary_cursor_normal = "#9193be" +secondary_cursor_insert = "#83a275" diff --git a/runtime/themes/catppuccin_latte.toml b/runtime/themes/catppuccin_latte.toml index e1580c9d7..7a015168e 100644 --- a/runtime/themes/catppuccin_latte.toml +++ b/runtime/themes/catppuccin_latte.toml @@ -34,3 +34,5 @@ crust = "#dce0e8" # derived colors by blending existing palette colors cursorline = "#e9ebf1" secondary_cursor = "#e2a99e" +secondary_cursor_normal = "#98a7fb" +secondary_cursor_insert = "#75b868" diff --git a/runtime/themes/catppuccin_macchiato.toml b/runtime/themes/catppuccin_macchiato.toml index e09a82f9b..6203eaade 100644 --- a/runtime/themes/catppuccin_macchiato.toml +++ b/runtime/themes/catppuccin_macchiato.toml @@ -34,3 +34,5 @@ crust = "#181926" # derived colors by blending existing palette colors cursorline = "#303347" secondary_cursor = "#b6a5a7" +secondary_cursor_normal = "#8b90bf" +secondary_cursor_insert = "#7fa47a" diff --git a/runtime/themes/catppuccin_mocha.toml b/runtime/themes/catppuccin_mocha.toml index 126613bc7..1ea57e960 100644 --- a/runtime/themes/catppuccin_mocha.toml +++ b/runtime/themes/catppuccin_mocha.toml @@ -83,6 +83,7 @@ "ui.text" = "text" "ui.text.focus" = { fg = "text", bg = "surface0", modifiers = ["bold"] } +"ui.text.inactive" = { fg = "overlay1" } "ui.virtual" = "overlay0" "ui.virtual.ruler" = { bg = "surface0" } @@ -95,6 +96,14 @@ "ui.cursor.primary" = { fg = "base", bg = "rosewater" } "ui.cursor.match" = { fg = "peach", modifiers = ["bold"] } +"ui.cursor.primary.normal" = { fg = "base", bg = "lavender" } +"ui.cursor.primary.insert" = { fg = "base", bg = "green" } +"ui.cursor.primary.select" = { fg = "base", bg = "flamingo" } + +"ui.cursor.normal" = { fg = "base", bg = "secondary_cursor_normal" } +"ui.cursor.insert" = { fg = "base", bg = "secondary_cursor_insert" } +"ui.cursor.select" = { fg = "base", bg = "secondary_cursor" } + "ui.cursorline.primary" = { bg = "cursorline" } "ui.highlight" = { bg = "surface1", modifiers = ["bold"] } @@ -146,3 +155,5 @@ crust = "#11111b" # derived colors by blending existing palette colors cursorline = "#2a2b3c" secondary_cursor = "#b5a6a8" +secondary_cursor_normal = "#878ec0" +secondary_cursor_insert = "#7da87e" diff --git a/runtime/themes/dracula.toml b/runtime/themes/dracula.toml index 1ec5b4fe2..1253544f2 100644 --- a/runtime/themes/dracula.toml +++ b/runtime/themes/dracula.toml @@ -90,6 +90,7 @@ "ui.virtual.whitespace" = { fg = "current_line" } "ui.virtual.wrap" = { fg = "current_line" } "ui.virtual.ruler" = { bg = "black" } +"ui.virtual.indent-guide" = { fg = "indent" } "ui.virtual.inlay-hint" = { fg = "cyan" } "ui.virtual.inlay-hint.parameter" = { fg = "cyan", modifiers = ["italic", "dim"] } "ui.virtual.inlay-hint.type" = { fg = "cyan", modifiers = ["italic", "dim"] } @@ -123,6 +124,7 @@ black = "#191A21" grey = "#666771" comment = "#6272A4" current_line = "#44475a" +indent = "#56596a" selection = "#363848" red = "#ff5555" orange = "#ffb86c" diff --git a/runtime/themes/jellybeans.toml b/runtime/themes/jellybeans.toml index 592db9a92..55088c298 100644 --- a/runtime/themes/jellybeans.toml +++ b/runtime/themes/jellybeans.toml @@ -22,7 +22,7 @@ "constant.builtin.boolean" = "yellow" "constant.character" = "yellow" -"constant.characted.escape" = "red_error" +"constant.character.escape" = "red_error" "constant.numeric" = "dark_orange" "string" = "dark_green" "string.regexp" = "light_purple" diff --git a/runtime/themes/material_darker.toml b/runtime/themes/material_darker.toml new file mode 100644 index 000000000..f117f7194 --- /dev/null +++ b/runtime/themes/material_darker.toml @@ -0,0 +1,20 @@ +# Material Theme for Helix Editor + +inherits = "material_deep_ocean" + +[palette] +bg = "#212121" +text = "#b0bec5" + +gray = "#616161" +error = "#ff5370" + +disabled = "#474747" + +accent = "#ff9800" + +highlight = "#3f3f3f" + +comment = "#616161" + +selection = "#404040" diff --git a/runtime/themes/material_deep_ocean.toml b/runtime/themes/material_deep_ocean.toml new file mode 100644 index 000000000..b98a32e55 --- /dev/null +++ b/runtime/themes/material_deep_ocean.toml @@ -0,0 +1,123 @@ +# Material Theme for Helix Editor + +# Syntax Highlighting + +"type" = "purple" + +"constructor" = "blue" + +"constant" = "yellow" + +"string" = "green" +"string.regexp" = "yellow" +"string.special" = "blue" + +"comment" = { fg = "comment" } + +"variable" = "text" +"variable.parameter" = { fg = "orange" } +"variable.builtin" = "yellow" + +"label" = "orange" + +"punctuation" = "cyan" + +"keyword" = "purple" +"keyword.storage" = "cyan" + +"operator" = "cyan" + +"function" = "blue" +"function.macro" = "cyan" + +"tag" = "red" +"attribute" = "purple" + +"namespace" = { fg = "yellow" } + +"special" = "cyan" + +"markup.heading.marker" = { fg = "cyan", modifiers = ["bold"] } +"markup.heading.1" = "cyan" +"markup.heading.2" = "red" +"markup.heading.3" = "green" +"markup.heading.4" = "yellow" +"markup.heading.5" = "blue" +"markup.heading.6" = "orange" +"markup.list" = "purple" +"markup.bold" = { modifiers = ["bold"] } +"markup.italic" = { modifiers = ["italic"] } +"markup.strikethrough" = { modifiers = ["crossed_out"] } +"markup.link.url" = { fg = "green", modifiers = ["underlined"] } +"markup.link.text" = "blue" +"markup.raw" = "text" + +"diff.plus" = "green" +"diff.minus" = "red" +"diff.delta" = "blue" + +# User Interface + +"ui.background" = { bg = "bg", fg = "text" } +"ui.text" = { fg = "text" } + +"ui.statusline" = { bg = "bg", fg = "text" } +"ui.statusline.inactive" = { bg = "bg", fg = "disabled" } +"ui.statusline.normal" = { bg = "accent", fg = "text" } +"ui.statusline.insert" = { bg = "green", fg = "text" } +"ui.statusline.select" = { bg = "purple", fg = "text" } + + +"ui.selection" = { bg = "selection" } + +"ui.linenr" = { fg = "line-number" } +"ui.linenr.selected" = { fg = "accent" } + +"ui.cursor" = { bg = "highlight", fg = "white" } + +"ui.cursor.primary" = { bg = "white", fg = "gray" } +"ui.cursorline.primary" = { bg = "white" } + +"ui.virtual" = { fg = "gray" } +"ui.virtual.ruler" = { bg = "highlight" } +"ui.virtual.indent-guide" = { fg = "gray" } + +"ui.highlight" = { bg = "highlight" } + +"ui.menu" = { bg = "highlight", fg = "text" } + +"ui.help" = { bg = "highlight", fg = "text" } + +"ui.popup" = { bg = "highlight", fg = "text" } + +warning = "yellow" +error = "error" +info = "blue" +hint = "purple" + +[palette] +bg = "#0f111a" +text = "#a6accd" + +white = "#eeffff" +green = "#c3e88d" +yellow = "#ffcb6b" +blue = "#82aaff" +red = "#f07178" +purple = "#c792ea" +orange = "#f78c6c" +cyan = "#89ddff" +gray = "#717cb4" +error = "#ff5370" + +disabled = "#464b5d" + +accent = "#84ffff" + +highlight = "#1f2233" + +comment = "#464b5d" + +selection = "#1f2233" + +line-number = "#3b3f51" diff --git a/runtime/themes/material_oceanic.toml b/runtime/themes/material_oceanic.toml new file mode 100644 index 000000000..ea4930d30 --- /dev/null +++ b/runtime/themes/material_oceanic.toml @@ -0,0 +1,21 @@ +# Material Theme for Helix Editor + +inherits = "material_deep_ocean" + +[palette] +bg = "#25363b" +text = "#b0bec5" + +gray = "#546e7a" + +disabled = "#415967" + +accent = "#009688" + +highlight = "#425b67" + +comment = "#546e7a" + +selection = "#395b65" + +line-number = "#355058" \ No newline at end of file diff --git a/runtime/themes/material_palenight.toml b/runtime/themes/material_palenight.toml new file mode 100644 index 000000000..ef9c27e26 --- /dev/null +++ b/runtime/themes/material_palenight.toml @@ -0,0 +1,19 @@ +# Material Theme for Helix Editor + +inherits = "material_deep_ocean" + +[palette] +bg = "#292d3e" +text = "#a6accd" + +disabled = "#515772" + +accent = "#ab47bc" + +highlight = "#444267" + +comment = "#676e95" + +selection = "#444267" + +line-number = "#3a3f58" \ No newline at end of file diff --git a/runtime/themes/molokai.toml b/runtime/themes/molokai.toml index fc8e533d5..2d5c9455e 100644 --- a/runtime/themes/molokai.toml +++ b/runtime/themes/molokai.toml @@ -13,7 +13,7 @@ inherits = "monokai" "keyword.storage.modifier" = { fg = "#fd971f", modifiers = ["italic"] } "label" = "#e6db74" "operator" = "keyword" -"punctuation.delimeter" = "#8f8f8f" +"punctuation.delimiter" = "#8f8f8f" "type" = "light-blue" "variable.builtin" = { fg = "#ae81ff", modifiers = ["bold"] } "tag.builtin" = { fg = "#ae81ff", modifiers = ["bold"] } diff --git a/runtime/themes/nightfox.toml b/runtime/themes/nightfox.toml index 069b32ab4..6c5ed3501 100644 --- a/runtime/themes/nightfox.toml +++ b/runtime/themes/nightfox.toml @@ -125,7 +125,7 @@ "keyword.control.exception" = { fg = "magenta" } # `try`, `catch`, `raise`/`throw` and related. "keyword.operator" = { fg = "fg2", modifiers = ["bold"] } # 'or', 'and', 'in'. "keyword.directive" = { fg = "pink-bright" } # Preprocessor directives (#if in C...). -"keyword.function" = { fg = "red" } # The keyword to define a funtion: 'def', 'fun', 'fn'. +"keyword.function" = { fg = "red" } # The keyword to define a function: 'def', 'fun', 'fn'. "keyword.storage" = { fg = "magenta" } # Keywords describing how things are stored "keyword.storage.type" = { fg = "magenta" } # The type of something, class, function, var, let, etc. "keyword.storage.modifier" = { fg = "yellow" } # Storage modifiers like static, mut, const, ref, etc. @@ -183,6 +183,6 @@ bg4 = "#39506d" # Conceal, border fg fg0 = "#d6d6d7" # Lighter fg fg1 = "#cdcecf" # Default fg fg2 = "#aeafb0" # Darker fg (status line) -fg3 = "#71839b" # Darker fg (line numbers, fold colums) +fg3 = "#71839b" # Darker fg (line numbers, fold columns) sel0 = "#2b3b51" # Popup bg, visual selection bg sel1 = "#3c5372" # Popup sel bg, search bg diff --git a/runtime/themes/noctis.toml b/runtime/themes/noctis.toml index c7d33680e..f4c2d5b5a 100644 --- a/runtime/themes/noctis.toml +++ b/runtime/themes/noctis.toml @@ -100,14 +100,14 @@ 'variable' = { fg = "white" } # Variable names. 'variable.builtin' = { } # Language reserved variables: `this`, `self`, `super`, etc. -'variable.parameter' = { } # Funtion parameters. +'variable.parameter' = { } # Function parameters. 'variable.other.member' = { } # Fields of composite data types (e.g. structs, unions). 'variable.function' = { } # ? 'label' = { fg = "purple" } # Loop labels in rust. 'punctuation' = { fg = "yellow", modifiers = ["bold"] } # (){}[]:;,. -# 'punctuation.delimeter' = { fg = "yellow" } # Commas and colons. +# 'punctuation.delimiter' = { fg = "yellow" } # Commas and colons. # 'punctuation.bracket' = { fg = "yellow" } # Parentheses, angle brackets, etc. 'keyword' = { fg = "pink", modifiers = ["bold"] } # Language reserved keywords. @@ -119,7 +119,7 @@ 'keyword.control.exception' = {fg = "pink", modifiers = ["bold"] } # 'raise' in python. 'keyword.operator' = { } # 'or', 'and', 'in'. 'keyword.directive' = { fg = "purple" } # Preprocessor directives (#if in C). -'keyword.function' = { } # The keyword to define a funtion: 'def', 'fun', 'fn'. +'keyword.function' = { } # The keyword to define a function: 'def', 'fun', 'fn'. 'operator' = { fg = "pink", modifiers = ["bold"] } # Logical (&&, ||) and - I assume - Mathematical (+, %) operators diff --git a/runtime/themes/nord.toml b/runtime/themes/nord.toml index 5c3b6219c..e1e63b323 100644 --- a/runtime/themes/nord.toml +++ b/runtime/themes/nord.toml @@ -15,14 +15,14 @@ "constructor" = "nord8" # Diagnostics -"diagnostic" = "nord13" -"diagnostic.error" = "nord11" +"diagnostic" = { underline = { color = "nord13", style = "curl" } } +"diagnostic.error" = { underline = { color = "nord11", style = "curl" } } "error" = "nord11" -"diagnostic.hint" = "nord10" +"diagnostic.hint" = { underline = { color = "nord10", style = "curl" } } "hint" = "nord10" -"diagnostic.info" = "nord8" +"diagnostic.info" = { underline = { color = "nord8", style = "curl" } } "info" = "nord8" -"diagnostic.warning" = "nord13" +"diagnostic.warning" = { underline = { color = "nord13", style = "curl" } } "warning" = "nord13" # Diffs @@ -100,6 +100,7 @@ "ui.popup" = { bg = "nord1" } "ui.popup.info" = { bg = "nord1" } "ui.help" = { bg = "nord1" } +"ui.text.focus" = { fg = "nord8", bg = "nord2" } # Gutter "ui.gutter" = "nord5" diff --git a/runtime/themes/papercolor-dark.toml b/runtime/themes/papercolor-dark.toml index eaaa36dcf..6e6dc0c33 100644 --- a/runtime/themes/papercolor-dark.toml +++ b/runtime/themes/papercolor-dark.toml @@ -1,124 +1,75 @@ # Palette based on https://github.com/NLKNguyen/papercolor-theme # Author: Soc Virnyl Estela -"ui.linenr.selected" = { fg = "linenr_fg_selected" } -"ui.background" = {bg="background"} -"ui.text" = "foreground" -"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"} -"ui.statusline.inactive" = {bg="selection_foreground", fg="foreground"} -"ui.virtual.whitespace" = { fg = "regular5" } -"ui.virtual.ruler" = {bg="cursorline_background"} -"ui.cursor.match" = {bg = "regular5", fg = "regular0"} -"ui.cursor" = {bg = "regular5", fg = "background"} -"ui.window" = {bg = "#303030", fg = "bright2"} -"ui.help" = {bg = "background", fg = "bright2"} -"ui.popup" = {bg = "#303030", fg = "bright6"} -"ui.menu" = {bg = "#303030", fg = "bright6"} -"ui.menu.selected" = {bg = "#C6C6C6", fg="selection_foreground"} +inherits = "papercolor-light" -"markup.heading" = { fg = "regular4", modifiers = ["bold"] } -"markup.heading.1" = { fg = "bright2", modifiers = ["bold"] } -"markup.heading.2" = { fg = "bright5", modifiers = ["bold"] } -"markup.heading.3" = { fg = "bright3", modifiers = ["bold"] } -"markup.heading.4" = { fg = "bright5", modifiers = ["bold"] } -"markup.heading.5" = { fg = "bright5", modifiers = ["bold"] } -"markup.heading.6" = { fg = "bright5", modifiers = ["bold"] } -"markup.list" = "bright3" -"markup.bold" = { fg = "foreground", modifiers = ["bold"] } -"markup.italic" = { fg = "bright0", modifiers = ["italic"] } -"markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { fg = "bright6", modifiers = ["underlined"] } -"markup.link.text" = "bright2" -"markup.link.label" = { fg = "regular2", modifiers = ["bold"] } -"markup.raw" = "foreground" - -"string" = "foreground" -"attribute" = "bright7" -"keyword" = { fg = "regular4", modifiers = ["bold"]} -"keyword.directive" = "regular4" -"keyword.control.conditional" = "bright3" -"keyword.function" = "regular4" -"namespace" = "bright1" -"type" = "bright2" -"type.builtin" = { fg = "foreground", modifiers = ["bold"]} -"variable" = "foreground" -"variable.builtin" = "cyan" -"variable.other.member" = "cyan" -"variable.parameter" = "foreground" - -"special" = "#3E999F" -"function" = "bright6" -"constructor" = "regular4" -"function.builtin" = { fg = "foreground", modifiers = ["bold"]} -"function.macro" = { fg = "regular4", modifiers = ["bold"] } -"comment" = { fg = "#686868", modifiers = ["dim"] } -"ui.linenr" = { fg = "bright0" } -"module" = "regular4" -"constant" = "bright5" -"constant.builtin" = "bright6" -"constant.numeric" = "bright5" -"constant.character.escape" = { fg = "foreground", modifiers = ["bold"]} -"operator" = { fg = "regular4", modifiers = ["bold"]} - -"label" = { fg = "selection_background", modifiers = ["bold", "italic"] } - -"diff.plus" = "regular2" -"diff.delta" = "regular6" -"diff.minus" = "regular1" +[palette] +background = "#1c1c1c" +foreground = "#d0d0d0" -"warning" = "bright4" -"error" = "regular1" -"info" = "bright4" +regular0 = "#1c1c1c" # color00 "Background" +regular1 = "#af005f" # color01 "Negative" +regular2 = "#5faf00" # color02 "Positive" +regular3 = "#d7af5f" # color03 "Olive" +regular4 = "#5fafd7" # color04 "Neutral" / Aqua +regular5 = "#808080" # color05 "Comment" +regular6 = "#d7875f" # color06 "Navy" +regular7 = "#d0d0d0" # color07 "Foreground" +bright0 = "#585858" # color08 "Nontext" +bright1 = "#5faf5f" # color09 "Red" +bright2 = "#afd700" # color10 "Pink" +bright3 = "#af87d7" # color11 "Purple" +bright4 = "#ffaf00" # color12 "Accent" +bright5 = "#ff5faf" # color13 "Orange" +bright6 = "#00afaf" # color14 "Blue" +bright7 = "#5f8787" # color15 "Highlight" -"diagnostic.warning".underline = { color = "bright4", style = "curl" } -"diagnostic.error".underline = { color = "regular1", style = "curl" } -"diagnostic.info".underline = { color = "bright4", style = "curl" } -"diagnostic.hint".underline = { color = "bright4", style = "curl" } +selection_fg = "#000000" +selection_bg = "#8787af" +selection_secondary_fg = "#333333" +selection_secondary_bg = "#707097" +special = "#3e999f" +cursorline_bg = "#303030" +cursorline_secondary_bg = "#2a2a2a" +cursorcolumn_bg = "#303030" +cursorcolumn_secondary_bg = "#2a2a2a" +cursorlinenr_fg = "#ffff00" +popupmenu_fg = "#c6c6c6" +popupmenu_bg = "#303030" +linenumber_fg = "#585858" +vertsplit_fg = "#5f8787" +statusline_active_fg = "#1c1c1c" +statusline_active_bg = "#5f8787" +statusline_inactive_fg = "#bcbcbc" +statusline_inactive_bg = "#3a3a3a" +todo_fg = "#ff8700" +error_fg = "#af005f" +error_bg = "#5f0000" +matchparen_bg = "#4e4e4e" +matchparen_fg = "#c6c6c6" +wildmenu_fg = "#1c1c1c" +wildmenu_bg = "#afd700" +diffadd_fg = "#87d700" +diffadd_bg = "#005f00" +diffdelete_fg = "#af005f" +diffdelete_bg = "#5f0000" +diffchange_bg = "#005f5f" -[palette] -background="#1c1c1c" -foreground="#d0d0d0" -regular0="#1c1c1c" -regular1="#af005f" -regular2="#5faf00" -regular3="#d7af5f" -regular4="#5fafd7" -regular5="#808080" -regular6="#d7875f" -regular7="#d0d0d0" -bright0="#585858" -bright1="#5faf5f" -bright2="#afd700" -bright3="#af87d7" -bright4="#FFAF00" -bright5="#ff5faf" -bright6="#00afaf" -bright7="#5f8787" -selection_foreground="#585858" -selection_background="#8787AF" -cursorline_background="#303030" -paper_bar_bg="#5F8787" -black="#1c1c1c" -red="#af005f" -green="#5faf00" -yellow="#d7af5f" -blue="#5fafd7" -magenta="#808080" -cyan="#d7875f" -gray="#d0d0d0" -light-red="#5faf5f" -light-green="#afd700" -light-yellow="#af87d7" -light-blue="#FFAF00" -light-magenta="#ff5faf" -light-cyan="#00afaf" -light-gray="#5f8787" -white="#808080" -linenr_fg_selected="#FFFF00" +# 16 bit ANSI color names +black = "#1c1c1c" +red = "#af005f" +green = "#5faf00" +yellow = "#d7af5f" +blue = "#5fafd7" +magenta = "#808080" +cyan = "#d7875f" +white = "#d0d0d0" +light-black = "#585858" +light-red = "#5faf5f" +light-green = "#afd700" +light-yellow = "#af87d7" +light-blue = "#ffaf00" +light-magenta = "#ff5faf" +light-cyan = "#00afaf" +light-white = "#5f8787" diff --git a/runtime/themes/papercolor-light.toml b/runtime/themes/papercolor-light.toml index 63671e1b3..ae104e17e 100644 --- a/runtime/themes/papercolor-light.toml +++ b/runtime/themes/papercolor-light.toml @@ -1,31 +1,124 @@ # Palette based on https://github.com/NLKNguyen/papercolor-theme # Author: Soc Virnyl Estela -"ui.linenr.selected" = { fg = "linenr_fg_selected" } -"ui.background" = {bg="background"} +"ui.linenr.selected" = { fg = "cursorlinenr_fg", modifiers = ["bold"] } +"ui.linenr" = { fg = "linenumber_fg" } +"ui.background" = { bg = "background" } "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"} -"ui.statusline.normal" = {bg="background", fg="bright3"} -"ui.statusline.inactive" = {bg="bright0", fg="foreground"} -"ui.virtual" = "indent" +"ui.text.focus" = { fg = "selection_bg", modifiers = ["bold"] } +"ui.selection" = { bg = "selection_secondary_bg", fg = "selection_secondary_fg" } +"ui.selection.primary" = { bg = "selection_bg", fg = "selection_fg" } +"ui.highlight" = { bg = "cursorline_bg" } + +"ui.cursorline" = { bg = "cursorline_bg" } +"ui.cursorline.secondary" = { bg = "cursorline_secondary_bg" } +"ui.cursorcolumn" = { bg = "cursorline_bg" } +"ui.cursorcolumn.secondary" = { bg = "cursorcolumn_secondary_bg" } + +"ui.statusline" = { bg = "statusline_active_bg", fg = "statusline_active_fg" } +"ui.statusline.inactive" = { bg = "statusline_inactive_bg", fg = "statusline_inactive_fg" } +"ui.statusline.normal" = { bg = "statusline_inactive_bg", fg = "bright6" } +"ui.statusline.insert" = { bg = "statusline_inactive_bg", fg = "bright4" } +"ui.statusline.select" = { bg = "statusline_inactive_bg", fg = "regular3" } +"ui.statusline.separator" = { bg = "statusline_active_bg", fg = "statusline_active_bg" } + +"ui.virtual" = { fg = "cursorlinenr_fg" } "ui.virtual.whitespace" = { fg = "regular5" } -"ui.virtual.ruler" = {bg="cursorline_background"} -"ui.cursor.match" = {bg = "regular5", fg = "regular0"} -"ui.cursor" = {bg = "regular5", fg = "background"} -"ui.window" = {bg = "#D0D0D0", fg = "bright2"} -"ui.help" = {bg = "background", fg = "bright2"} -"ui.popup" = {bg = "#D0D0D0", fg = "bright7"} -"ui.menu" = {bg = "#D0D0D0", fg = "bright7"} -"ui.menu.selected" = {bg = "selection_background", fg="selection_foreground"} - -"markup.heading" = { fg = "bright7", modifiers = ["bold"] } +"ui.virtual.indent-guide" = { fg = "bright0" } +"ui.virtual.ruler" = { bg = "cursorline_secondary_bg", fg = "regular4" } +"ui.cursor.match" = { bg = "matchparen_bg", fg = "matchparen_fg" } +"ui.cursor" = { bg = "regular5", fg = "background" } +"ui.cursor.primary" = { bg = "foreground", fg = "background" } +"ui.window" = { fg = "vertsplit_fg" } +"ui.help" = { bg = "wildmenu_bg", fg = "wildmenu_fg" } +"ui.popup" = { bg = "popupmenu_bg", fg = "popupmenu_fg" } +"ui.popup.info" = { bg = "popupmenu_bg", fg = "bright7", modifiers = ["bold"] } +"ui.menu" = { bg = "popupmenu_bg", fg = "foreground" } +"ui.menu.selected" = { bg = "selection_bg", fg = "selection_fg" } + +"warning" = "bright5" +"error" = { bg = "error_bg", fg = "error_fg" } +"info" = "todo_fg" + +"diagnostic.warning" = { fg = "bright0", modifiers = [ + "dim", +], underline = { color = "bright5", style = "curl" } } +"diagnostic.error".underline = { color = "bright1", style = "curl" } +"diagnostic.info".underline = { color = "bright4", style = "curl" } +"diagnostic.hint".underline = { color = "bright6", style = "curl" } + +# Tree-sitter scopes for syntax highlighting +"attribute" = "bright4" + +"type" = { fg = "bright2", modifiers = ["bold"] } +"type.builtin" = { fg = "bright2", modifiers = ["bold"] } +"type.enum" = { fg = "foreground" } +"type.enum.variant" = { fg = "foreground" } + +"constructor" = "foreground" + +"constant" = "bright5" +"constant.builtin" = "regular3" +"constant.builtin.boolean" = { fg = "regular2", modifiers = ["bold"] } +"constant.character.escape" = { fg = "bright3", modifiers = ["bold"] } +"constant.character" = { fg = "regular3" } +"constant.numeric" = "bright5" + +"string" = "regular3" +"string.regexp" = "bright3" + +"comment" = { fg = "regular5", modifiers = ["italic"] } +"comment.line" = { fg = "regular5", modifiers = ["italic"] } +"comment.block" = { fg = "regular5", modifiers = ["italic"] } +"comment.block.documentation" = { fg = "regular5", modifiers = ["bold"] } + +"variable" = "foreground" +"variable.builtin" = "bright5" +"variable.other.member" = "foreground" +"variable.parameter" = "foreground" + +"label" = { fg = "selection_bg", modifiers = ["bold", "italic"] } + +"punctuation" = { fg = "foreground" } +"punctuation.delimiter" = { fg = "regular4", modifiers = ["bold"] } +"punctuation.bracket" = { fg = "foreground" } +"punctuation.special" = { fg = "bright1", modifiers = ["bold"] } + +"keyword" = { fg = "bright2" } +"keyword.control" = "bright1" +"keyword.control.conditional" = { fg = "bright3", modifiers = ["bold"] } +"keyword.control.repeat" = { fg = "bright3", modifiers = ["bold"] } +"keyword.control.import" = { fg = "bright2" } +"keyword.control.return" = { fg = "bright2" } +"keyword.control.exception" = { fg = "bright1" } + +"keyword.operator" = { fg = "regular4", modifiers = ["bold"] } +"keyword.directive" = "regular4" +"keyword.function" = "bright2" +"keyword.storage" = "bright2" +"keyword.storage.type" = { fg = "regular4", modifiers = ["bold"] } +"keyword.storage.modifier" = { fg = "regular6", modifiers = ["bold"] } +"keyword.storage.modifier.ref" = { fg = "regular4", modifiers = ["bold"] } +"keyword.special" = "bright1" + +"operator" = { fg = "regular4", modifiers = ["bold"] } + +"function" = { fg = "foreground" } +"function.builtin" = { fg = "bright6" } +"function.method" = { fg = "foreground" } +"function.macro" = { fg = "regular3", modifiers = ["bold"] } +"function.special" = { fg = "bright4" } + +"tag" = { fg = "regular4" } + +"namespace" = "bright6" + +"special" = "special" + +"markup.heading" = { fg = "bright4", modifiers = ["bold"] } +"markup.heading.marker" = { fg = "bright2", modifiers = ["bold"] } "markup.heading.1" = { fg = "bright2", modifiers = ["bold"] } -"markup.heading.2" = { fg = "bright4", modifiers = ["bold"] } +"markup.heading.2" = { fg = "bright5", modifiers = ["bold"] } "markup.heading.3" = { fg = "bright3", modifiers = ["bold"] } "markup.heading.4" = { fg = "bright4", modifiers = ["bold"] } "markup.heading.5" = { fg = "bright4", modifiers = ["bold"] } @@ -34,90 +127,84 @@ "markup.bold" = { fg = "foreground", modifiers = ["bold"] } "markup.italic" = { modifiers = ["italic"] } "markup.strikethrough" = { modifiers = ["crossed_out"] } -"markup.link.url" = { fg = "regular4", modifiers = ["underlined"] } +"markup.link.url" = { fg = "bright6", underline.style = "line" } "markup.link.text" = "bright2" -"markup.link.label" = { fg = "regular7", modifiers = ["bold"] } -"markup.raw" = "foreground" - -"string" = "foreground" -"attribute" = "bright7" -"keyword" = { fg = "regular4", modifiers = ["bold"]} -"keyword.directive" = "regular1" -"namespace" = "regular1" -"type" = "bright2" -"type.builtin" = { fg = "regular4", modifiers = ["bold"]} -"variable" = "foreground" -"variable.builtin" = "cyan" -"variable.other.member" = "regular4" -"variable.parameter" = "foreground" - -"special" = "#3E999F" -"function" = "bright1" -"constructor" = "bright1" -"function.builtin" = { fg = "regular4", modifiers = ["bold"]} -"function.macro" = { fg = "regular1" } -"comment" = { fg = "bright0", modifiers = ["dim"] } -"ui.linenr" = { fg = "bright0" } -"module" = "#af0000" -"constant" = "#5f8700" -"constant.builtin" = "#5f8700" -"constant.numeric" = "#d75f00" -"constant.character.escape" = { fg = "#8700af", modifiers = ["bold"]} -"operator" = { fg = "regular4", modifiers = ["bold"]} +"markup.link.label" = { fg = "regular2", modifiers = ["bold"] } +"markup.quote" = "regular4" +# Both inline and block code +"markup.raw" = "regular3" -"label" = { fg = "selection_background", modifiers = ["bold", "italic"] } +"diff.plus" = { bg = "diffadd_bg", fg = "diffadd_fg" } +"diff.delta" = { bg = "diffchange_bg" } +"diff.delta.moved" = { modifiers = ["italic"] } +"diff.minus" = { bg = "diffdelete_bg", fg = "diffdelete_fg" } -"diff.plus" = "regular2" -"diff.delta" = "bright0" -"diff.minus" = "bright1" - -"warning" = "bright4" -"error" = "regular1" -"info" = "#FFAF00" +[palette] +background = "#eeeeee" +foreground = "#444444" +regular0 = "#eeeeee" # color00 "Background" +regular1 = "#af0000" # color01 "Negative" +regular2 = "#008700" # color02 "Positive" +regular3 = "#5f8700" # color03 "Olve" +regular4 = "#0087af" # color04 "Neutral" / Aqua +regular5 = "#878787" # color05 "Comment" +regular6 = "#005f87" # color06 "Navy" +regular7 = "#444444" # color07 "Foreground" +bright0 = "#bcbcbc" # color08 "Nontext" +bright1 = "#d70000" # color09 "Red" +bright2 = "#d70087" # color10 "Pink" +bright3 = "#8700af" # color11 "Purple" +bright4 = "#d75f00" # color12 "Accent" +bright5 = "#d75f00" # color13 "Orange" +bright6 = "#005faf" # color14 "Blue" +bright7 = "#005f87" # color15 "Highlight" -"diagnostic.warning".underline = { color = "bright4", style = "curl" } -"diagnostic.error".underline = { color = "regular1", style = "curl" } -"diagnostic.info".underline = { color = "#FFAF00", style = "curl" } -"diagnostic.hint".underline = { color = "#FFAF00", style = "curl" } +selection_fg = "#eeeeee" +selection_bg = "#0087af" +selection_secondary_fg = "#d9d7d7" +selection_secondary_bg = "#2c687a" +special = "#3e999f" +cursorline_bg = "#e4e4e4" +cursorline_secondary_bg = "#eaeaea" +cursorcolumn_bg = "#e4e4e4" +cursorcolumn_secondary_bg = "#eaeaea" +cursorlinenr_fg = "#af5f00" +popupmenu_fg = "#444444" +popupmenu_bg = "#d0d0d0" +linenumber_fg = "#b2b2b2" +vertsplit_fg = "#005f87" +statusline_active_fg = "#e4e4e4" +statusline_active_bg = "#005f87" +statusline_inactive_fg = "#444444" +statusline_inactive_bg = "#d0d0d0" +todo_fg = "#00af5f" +error_fg = "#af0000" +error_bg = "#ffd7ff" +matchparen_bg = "#c6c6c6" +matchparen_fg = "#005f87" +wildmenu_fg = "#444444" +wildmenu_bg = "#ffff00" +diffadd_fg = "#008700" +diffadd_bg = "#afffaf" +diffdelete_fg = "#af0000" +diffdelete_bg = "#ffd7ff" +diffchange_bg = "#ffd787" -[palette] -background="#eeeeee" -foreground="#444444" -regular0="#eeeeee" -regular1="#af0000" -regular2="#008700" -regular3="#5f8700" -regular4="#0087af" -regular5="#878787" -regular6="#005f87" -regular7="#764e37" -bright0="#bcbcbc" -bright1="#d70000" -bright2="#d70087" -bright3="#8700af" -bright4="#d75f00" -bright5="#d75f00" -bright6="#4c7a5d" -bright7="#005faf" -selection_foreground="#eeeeee" -selection_background="#0087af" -cursorline_background="#fdfdfd" -paper_bar_bg="#005F87" -black="#eeeeee" -red="#d70000" -green="#008700" -yellow="#5f8700" -blue="#0087af" -magenta="#878787" -cyan="#005f87" -gray="#764e37" -light-red="#d70000" -light-green="#d70087" -light-yellow="#8700af" -light-blue="#d75f00" -light-magenta="#d75f00" -light-cyan="#4c7a4d" -light-gray="#005faf" -white="#444444" -linenr_fg_selected="#AF634D" +# 16 bit ANSI color names +black = "#eeeeee" +red = "#d70000" +green = "#008700" +yellow = "#5f8700" +blue = "#0087af" +magenta = "#878787" +cyan = "#005f87" +white = "#444444" +light-black = "#bcbcbc" +light-red = "#d70000" +light-green = "#d70087" +light-yellow = "#8700af" +light-blue = "#d75f00" +light-magenta = "#d75f00" +light-cyan = "#4c7a4d" +light-white = "#005faf" diff --git a/runtime/themes/rose_pine.toml b/runtime/themes/rose_pine.toml index e1d968941..4bbf219ed 100644 --- a/runtime/themes/rose_pine.toml +++ b/runtime/themes/rose_pine.toml @@ -89,7 +89,7 @@ "comment" = { fg = "muted", modifiers = ["italic"]} # "comment.line" = "" # "comment.block" = "" -# "comment.block.documenation" = "" +# "comment.block.documentation" = "" "variable" = "text" "variable.builtin" = "love" diff --git a/runtime/themes/zenburn.toml b/runtime/themes/zenburn.toml index 8518e78f8..9a4a7abcc 100644 --- a/runtime/themes/zenburn.toml +++ b/runtime/themes/zenburn.toml @@ -9,7 +9,7 @@ "ui.selection" = { bg = "#304a3d" } "ui.selection.primary" = { bg = "#2f2f2f" } "comment" = { fg = "comment" } -"ui.virtual.inlay-hint" = { fg = "comment" } +"ui.virtual.inlay-hint" = { fg = "#9f9f9f" } "comment.block.documentation" = { fg = "black", modifiers = ["bold"] } "ui.statusline" = { bg = "statusbg", fg = "#ccdc90" } "ui.statusline.inactive" = { fg = '#2e3330', bg = '#88b090' }