Merge remote-tracking branch 'origin' into mwp-steel-integration

pull/8675/merge^2
mattwparas 1 year ago
commit db3e9ed31d

@ -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

@ -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

@ -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

@ -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

266
Cargo.lock generated

@ -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",
]

@ -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"

@ -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 {

@ -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

@ -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 | ✓ | | | |

@ -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 |

@ -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

@ -70,6 +70,7 @@ over it and is merged into the default palette.
| Color Name |
| --- |
| `default` |
| `black` |
| `red` |
| `green` |

@ -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

@ -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;
});

@ -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 }

@ -0,0 +1,43 @@
use std::ops::DerefMut;
use nucleo::pattern::{Atom, AtomKind, CaseMatching};
use nucleo::Config;
use parking_lot::Mutex;
pub struct LazyMutex<T> {
inner: Mutex<Option<T>>,
init: fn() -> T,
}
impl<T> LazyMutex<T> {
pub const fn new(init: fn() -> T) -> Self {
Self {
inner: Mutex::new(None),
init,
}
}
pub fn lock(&self) -> impl DerefMut<Target = T> + '_ {
parking_lot::MutexGuard::map(self.inner.lock(), |val| val.get_or_insert_with(self.init))
}
}
pub static MATCHER: LazyMutex<nucleo::Matcher> = 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<T: AsRef<str>>(
pattern: &str,
items: impl IntoIterator<Item = T>,
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)
}

@ -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;

@ -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)

@ -0,0 +1,15 @@
[package]
name = "helix-event"
version = "0.6.0"
authors = ["Blaž Hrastnik <blaz@mxxn.io>"]
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"] }

@ -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;

@ -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<Output = ()> {
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()
}

@ -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"

@ -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 }

@ -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;

@ -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<dyn FnOnce(&mut Context, KeyEvent)>;
@ -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 <ret> char.
//
// This is necessary because the one document can have different line endings inside. And we
// cannot predict what character to find when <ret> 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 <ret>. 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,
&regex,
Movement::Move,
@ -2098,13 +2151,11 @@ fn global_search(cx: &mut Context) {
}
}
let (all_matches_sx, all_matches_rx) = tokio::sync::mpsc::unbounded_channel::<FileResult>();
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,36 +2168,42 @@ 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());
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)
@ -2163,7 +2220,7 @@ fn global_search(cx: &mut Context) {
.run(|| {
let mut searcher = searcher.clone();
let matcher = matcher.clone();
let all_matches_sx = all_matches_sx.clone();
let injector = injector_.clone();
let documents = &documents;
Box::new(move |entry: Result<DirEntry, ignore::Error>| -> WalkState {
let entry = match entry {
@ -2177,21 +2234,24 @@ fn global_search(cx: &mut Context) {
_ => return WalkState::Continue,
};
let mut stop = false;
let sink = sinks::UTF8(|line_num, _| {
all_matches_sx
.send(FileResult::new(entry.path(), line_num as usize - 1))
.unwrap();
stop = injector
.push(FileResult::new(entry.path(), line_num as usize - 1))
.is_err();
Ok(true)
Ok(!stop)
});
let doc = documents.iter().find(|&(doc_path, _)| {
doc_path.map_or(false, |doc_path| doc_path == entry.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 requireing a save
// 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
@ -2215,33 +2275,20 @@ fn global_search(cx: &mut Context) {
err
);
}
if stop {
WalkState::Quit
} else {
WalkState::Continue
}
})
});
} 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<FileResult> =
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,
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),
@ -2254,29 +2301,40 @@ fn global_search(cx: &mut Context) {
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.");
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()){
if action.align_view(view, doc.id()) {
align_view(doc, view, Align::Center);
}
}).with_preview(|_editor, FileResult { path, line_num }| {
},
)
.with_preview(
|_editor, FileResult { path, line_num }| {
Some((path.clone().into(), Some((*line_num, *line_num))))
});
compositor.push(Box::new(overlaid(picker)));
},
));
Ok(call)
);
compositor.push(Box::new(overlaid(picker)))
};
cx.jobs.callback(show_picker);
Ok(Callback::EditorCompositor(Box::new(call)))
})
} else {
// Otherwise do nothing
// log::warn!("Global Search Invalid Pattern")
}
},
);
}
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) {

@ -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<Cow<'a, str>> {
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<Cow<'a, str>> {
Vec::new()
}
}

@ -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<Cow<'a, str>> {
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::<Vec<_>>()
}
}
@ -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<Mutex<VecDeque<String>>>,

@ -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<str>>,
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<str>],
@ -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,10 +716,21 @@ pub fn write_all_impl(
current_view.id
};
let fmt = if auto_format {
Some((doc.id(), target_view))
})
.collect();
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_id,
doc.version(),
target_view,
fmt,
@ -718,16 +743,8 @@ pub fn write_all_impl(
};
if fmt.is_none() {
return Some(doc.id());
cx.editor.save::<PathBuf>(doc_id, None, force)?;
}
None
})
.collect();
// manually call save for the rest of docs that don't have a formatter
for id in saves {
cx.editor.save::<PathBuf>(id, None, force)?;
}
if !errors.is_empty() && !force {
@ -1275,10 +1292,8 @@ 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(|_| {
doc.reload(view, &cx.editor.diff_providers).map(|_| {
view.ensure_cursor_in_view(doc, scrolloff);
})?;
if let Some(path) = doc.path() {
@ -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<str>],
event: PromptEvent,
) -> anyhow::Result<()> {
fn find_highlight_at_cursor(
cx: &mut compositor::Context<'_>,
) -> Option<helix_core::syntax::Highlight> {
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<str>],
@ -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<str>],
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<HashMap<&'static str, &'static TypableCommand>> =
@ -2958,35 +3083,41 @@ pub(super) fn command_mode(cx: &mut Context) {
":".into(),
Some(':'),
|editor: &Editor, input: &str| {
static FUZZY_MATCHER: Lazy<fuzzy_matcher::skim::SkimMatcherV2> =
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::<Vec<_>>();
// 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
// let globals =
// crate::commands::engine::ScriptingEngine::fuzzy_match(&FUZZY_MATCHER, input)
// .into_iter()
// .map(|x| (Cow::from(x.0), x.1))
// .collect::<Vec<_>>();
// // 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()
} else {
// Otherwise, use the command's completer and the last shellword
// as completion input.

@ -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<Box<dyn Component>>,
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<dyn Component>) {
// 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

@ -43,6 +43,8 @@ pub struct EditorView {
pub(crate) last_insert: (commands::MappableCommand, Vec<InsertEvent>),
pub(crate) completion: Option<Completion>,
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)
}
}

@ -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<QueryAtom> {
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<usize>) -> 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<String>,
query_atoms: Vec<QueryAtom>,
}
fn query_atoms(query: &str) -> impl Iterator<Item = &str> + '_ {
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<i64> {
// 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<usize>)> {
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))
}
}

@ -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<String> {
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"])
}

@ -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<T: Item> {
cursor: Option<usize>,
matcher: Box<Matcher>,
/// (index, score)
matches: Vec<(usize, i64)>,
matches: Vec<(u32, u32)>,
widths: Vec<Constraint>,
@ -75,11 +74,10 @@ impl<T: Item> Menu<T> {
editor_data: <T as Item>::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<T: Item> Menu<T> {
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 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);
// 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);
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<T: Item> Menu<T> {
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<T: Item> Menu<T> {
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<T: Item + 'static> Component for Menu<T> {
.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();

@ -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<char>,
completion_fn: impl FnMut(&Editor, &str) -> Vec<prompt::Completion> + '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<PathBuf> = 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<Completion>;
@ -248,31 +253,16 @@ pub mod completers {
}
pub fn buffer(editor: &Editor, input: &str) -> Vec<Completion> {
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 names = editor.documents.values().map(|doc| {
doc.relative_path()
.map(|p| p.display().to_string().into())
.unwrap_or_else(|| Cow::from(SCRATCH_BUFFER_NAME))
});
let mut matches: Vec<_> = names
fuzzy_match(input, names, true)
.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();
names
.map(|(name, _)| ((0..), name))
.collect()
}
pub fn theme(_editor: &Editor, input: &str) -> Vec<Completion> {
@ -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<Completion> {
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<Completion> {
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
}
}
}

@ -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(_) => "<File preview>",
Self::EditorDocument(_) => "<Invalid file location>",
Self::Cached(preview) => match preview {
CachedPreview::Document(_) => "<File preview>",
CachedPreview::Document(_) => "<Invalid file location>",
CachedPreview::Binary => "<Binary file>",
CachedPreview::LargeFile => "<File too large to preview>",
CachedPreview::NotFound => "<File not found>",
@ -114,20 +123,71 @@ impl Preview<'_, '_> {
}
}
fn item_to_nucleo<T: Item>(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<T: Item> {
dst: nucleo::Injector<T>,
editor_data: Arc<T::Data>,
shutown: Arc<AtomicBool>,
}
impl<T: Item> Clone for Injector<T> {
fn clone(&self) -> Self {
Injector {
dst: self.dst.clone(),
editor_data: self.editor_data.clone(),
shutown: self.shutown.clone(),
}
}
}
pub struct InjectorShutdown;
impl<T: Item> Injector<T> {
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<T: Item> {
options: Vec<T>,
editor_data: T::Data,
// filter: String,
matcher: Box<Matcher>,
matches: Vec<PickerMatch>,
editor_data: Arc<T::Data>,
shutdown: Arc<AtomicBool>,
matcher: Nucleo<T>,
/// 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<T: Item> {
}
impl<T: Item + 'static> Picker<T> {
pub fn stream(editor_data: T::Data) -> (Nucleo<T>, Injector<T>) {
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<T>,
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<T>,
injector: Injector<T>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
Self::with(matcher, injector.editor_data, injector.shutown, callback_fn)
}
fn with(
matcher: Nucleo<T>,
editor_data: Arc<T::Data>,
shutdown: Arc<AtomicBool>,
callback_fn: impl Fn(&mut Context, &T, Action) + 'static,
) -> Self {
let prompt = Prompt::new(
"".into(),
@ -156,14 +265,13 @@ impl<T: Item + 'static> Picker<T> {
|_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<T: Item + 'static> Picker<T> {
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<T> {
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<T: Item + 'static> Picker<T> {
preview_fn: impl Fn(&Editor, &T) -> Option<FileLocation> + '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<T>) {
self.options = new_options;
self.cursor = 0;
self.force_score();
self.calculate_column_widths();
}
/// Calculate the width constraints using the maximum widths of each column
/// for the current options.
fn calculate_column_widths(&mut self) {
let n = self
.options
.first()
.map(|option| option.format(&self.editor_data).cells.len())
.unwrap_or_default();
let max_lens = self.options.iter().fold(vec![0; n], |mut acc, option| {
let row = option.format(&self.editor_data);
// maintain max for each column
for (acc, cell) in acc.iter_mut().zip(row.cells.iter()) {
let width = cell.content.width();
if width > *acc {
*acc = width;
}
}
acc
});
self.widths = max_lens
.into_iter()
.map(|len| Constraint::Length(len as u16))
.collect();
}
pub fn score(&mut self) {
let pattern = self.prompt.line();
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(),
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);
}
}));
} 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<T: Item + 'static> Picker<T> {
/// 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<T: Item + 'static> Picker<T> {
/// 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<T: Item + 'static> Picker<T> {
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<T: Item + 'static> Picker<T> {
(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())
_ => Document::open(path, None, None, editor.config.clone())
.map(|doc| CachedPreview::Document(Box::new(doc)))
.unwrap_or(CachedPreview::NotFound)
}
.unwrap_or(CachedPreview::NotFound),
},
)
.unwrap_or(CachedPreview::NotFound);
@ -461,9 +474,13 @@ impl<T: Item + 'static> Picker<T> {
log::info!("highlighting picker item failed");
return;
};
let Some(Overlay {
content: picker, ..
}) = compositor.find::<Overlay<Self>>()
let picker = match compositor.find::<Overlay<Self>>() {
Some(Overlay { content, .. }) => Some(content),
None => compositor
.find::<Overlay<DynamicPicker<T>>>()
.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<T: Item + 'static> Picker<T> {
}
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<T: Item + 'static> Picker<T> {
// -- 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<T: Item + 'static> Picker<T> {
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,102 +569,85 @@ impl<T: Item + 'static> Picker<T> {
// -- 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 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 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 = 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))
// 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 {
(grapheme, style)
span.style
};
if style != current_style {
if !current_span.is_empty() {
span_list.push(Span::styled(current_span, current_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);
} else {
span_list.push((String::from(grapheme), style))
current_span = String::new();
current_style = style;
}
current_span.push_str(grapheme);
grapheme_idx += 1;
}
width += span.width();
}
let spans: Vec<Span> = 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
@ -654,7 +667,7 @@ impl<T: Item + 'static> Picker<T> {
surface,
&mut TableState {
offset: 0,
selected: Some(cursor),
selected: Some(cursor as usize),
},
self.truncate_start,
);
@ -680,8 +693,14 @@ impl<T: Item + 'static> Picker<T> {
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<T: Item + 'static> Picker<T> {
};
let mut offset = ViewPosition::default();
if let Some(range) = range {
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(
doc.text().slice(..),
doc.text().line_to_char(range.0),
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<T: Item + 'static> Picker<T> {
}
}
impl<T: Item + 'static> Component for Picker<T> {
impl<T: Item + 'static + Send + Sync> Component for Picker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
// +---------+ +---------+
// |prompt | |preview |
@ -794,11 +825,28 @@ impl<T: Item + 'static> Component for Picker<T> {
_ => return EventResult::Ignored(None),
};
let close_fn =
EventResult::Consumed(Some(Box::new(|compositor: &mut Compositor, _ctx| {
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<T: Item + 'static> Component for Picker<T> {
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<T: Item + 'static> Component for Picker<T> {
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<T: Item + 'static> Component for Picker<T> {
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<Ordering> {
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<T: Item> Drop for Picker<T> {
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<T> =
/// A picker that updates its contents via a callback whenever the
/// query string changes. Useful for live grep, workspace symbols, etc.
pub struct DynamicPicker<T: ui::menu::Item + Send> {
pub struct DynamicPicker<T: ui::menu::Item + Send + Sync> {
file_picker: Picker<T>,
query_callback: DynQueryCallback<T>,
query: String,
}
impl<T: ui::menu::Item + Send> DynamicPicker<T> {
pub const ID: &'static str = "dynamic-picker";
impl<T: ui::menu::Item + Send + Sync> DynamicPicker<T> {
pub fn new(file_picker: Picker<T>, query_callback: DynQueryCallback<T>) -> Self {
Self {
file_picker,
@ -928,7 +957,7 @@ impl<T: ui::menu::Item + Send> DynamicPicker<T> {
}
}
impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
impl<T: Item + Send + Sync + 'static> Component for DynamicPicker<T> {
fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
self.file_picker.render(area, surface, cx);
}
@ -950,7 +979,7 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
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::<Overlay<DynamicPicker<T>>>(Self::ID) {
let picker = match compositor.find_id::<Overlay<DynamicPicker<T>>>(ID) {
Some(overlay) => &mut overlay.content.file_picker,
None => return,
};
@ -971,6 +1000,6 @@ impl<T: Item + Send + 'static> Component for DynamicPicker<T> {
}
fn id(&self) -> Option<&'static str> {
Some(Self::ID)
Some(ID)
}
}

@ -20,7 +20,6 @@ mod test {
mod commands;
mod languages;
mod movement;
mod picker;
mod prompt;
mod splits;
}

@ -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<ret>"), 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<ret>"), 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<ret>"), 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 {}<ret>ia time traveler walks into a bar<esc>:wa<ret>",
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<ret>"), 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 {}<ret>:x<ret>", file.path().to_string_lossy())),
None,
true,

@ -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(())
}

@ -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"
}),
"T<ret>gll2f<ret>",
helpers::platform_line(indoc! {
"\
one
two#[
|]#three"
}),
))
.await?;
test((
helpers::platform_line(indoc! {
"\
#[|o]#ne
two
three"
}),
"f<ret>2t<ret>ghT<ret>F<ret>",
helpers::platform_line(indoc! {
"\
one#[|
t]#wo
three"
}),
))
.await?;
Ok(())
}

@ -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 <alt-ret> 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::<Vec<_>>();
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("<space>/"), None),
(Some("second<ret>"), None),
(
Some("<A-ret><esc>"),
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(":buffer<minus>next<ret>"),
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(())
}

@ -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(())
}

@ -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(())
}

@ -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"

@ -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<Notify>, Arc<RwLock<()>>);
/// A rendering lock passed to the differ the prevents redraws from occurring
struct RenderLock {
pub lock: OwnedRwLockReadGuard<()>,
pub lock: RenderLockGuard,
pub timeout: Option<Instant>,
}
@ -38,28 +36,22 @@ struct DiffInner {
#[derive(Clone, Debug)]
pub struct DiffHandle {
channel: UnboundedSender<Event>,
render_lock: Arc<RwLock<()>>,
diff: Arc<Mutex<DiffInner>>,
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<Mutex<DiffInner>> = 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 {

@ -23,7 +23,6 @@ pub(super) struct DiffWorker {
pub channel: UnboundedReceiver<Event>,
pub diff: Arc<Mutex<DiffInner>>,
pub new_hunks: Vec<Hunk>,
pub redraw_notify: Arc<Notify>,
pub diff_finished_notify: Arc<Notify>,
}
@ -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<Event>,
redraw_notify: Arc<Notify>,
diff_finished_notify: Arc<Notify>,
) {
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

@ -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<Hunk> {
let diff = self.diff;

@ -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" }

@ -73,9 +73,14 @@ pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
#[cfg(target_os = "macos")]
pub fn get_clipboard_provider() -> Box<dyn ClipboardProvider> {
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";

@ -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<BoxFuture<'static, Result<Transaction, FormatterError>>> {
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<u8>, redraw_handle: RedrawHandle) {
pub fn set_diff_base(&mut self, diff_base: Vec<u8>) {
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;
}

@ -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<PathBuf>,
/// 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<SmartTabConfig>,
}
@ -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<ConfigEvent>, UnboundedReceiver<ConfigEvent>),
/// 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<dyn Fn(&mut Editor)>;
pub type RedrawHandle = (Arc<Notify>, Arc<RwLock<()>>);
#[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);

@ -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,

@ -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" }

@ -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

@ -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

@ -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)

@ -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

@ -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

@ -97,3 +97,8 @@
["(" ")" "[" "]" "{" "}"] @punctuation.bracket
(quote "'") @operator
(unquote_splicing ",@") @operator
(unquote ",") @operator
(quasiquote "`") @operator

@ -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"))))))

@ -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"

@ -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"

@ -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"

@ -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"

@ -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"

@ -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"

@ -0,0 +1,20 @@
# Material Theme <https://material-theme.com/> 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"

@ -0,0 +1,123 @@
# Material Theme <https://material-theme.com/> 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"

@ -0,0 +1,21 @@
# Material Theme <https://material-theme.com/> 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"

@ -0,0 +1,19 @@
# Material Theme <https://material-theme.com/> 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"

@ -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"] }

@ -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

@ -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

@ -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"

@ -1,124 +1,75 @@
# Palette based on https://github.com/NLKNguyen/papercolor-theme
# Author: Soc Virnyl Estela <socvirnyl.estela@gmail.com>
"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"

@ -1,31 +1,124 @@
# Palette based on https://github.com/NLKNguyen/papercolor-theme
# Author: Soc Virnyl Estela <socvirnyl.estela@gmail.com>
"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"

@ -89,7 +89,7 @@
"comment" = { fg = "muted", modifiers = ["italic"]}
# "comment.line" = ""
# "comment.block" = ""
# "comment.block.documenation" = ""
# "comment.block.documentation" = ""
"variable" = "text"
"variable.builtin" = "love"

@ -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' }

Loading…
Cancel
Save