mirror of
https://github.com/helix-editor/helix
synced 2024-05-08 11:56:04 +02:00
Compare commits
58 Commits
078a4d77c6
...
44e607479b
Author | SHA1 | Date | |
---|---|---|---|
GiM | 44e607479b | ||
Diogenesoftoronto | 5ee7411450 | ||
Keir Lawson | 31248d4e2f | ||
David Else | 109f53fb60 | ||
woojiq | 839ec4ad39 | ||
woojiq | 81dc8e8d6b | ||
Yoav Lavi | 50c90cb47c | ||
David Else | 22960e0d70 | ||
Krishan | 89a9f2be78 | ||
Kirawi | e18b772654 | ||
Pascal Kuthe | 38ee845b05 | ||
Pascal Kuthe | b834806dbc | ||
Matouš Dzivjak | d140072fdc | ||
Simran Kedia | 26d9610e78 | ||
Triton171 | efae85ec20 | ||
dependabot[bot] | 35b6aef5fb | ||
Chris Sergienko | 345e687573 | ||
Ben Fekih, Hichem | 4b8bcd2773 | ||
Ben Fekih, Hichem | af4ff80524 | ||
Michael Davis | 211f368064 | ||
Kevin Vigor | 18d5cacea6 | ||
RoloEdits | 94405f3d07 | ||
urly3 | 98b4df23a3 | ||
Nuke | 2209effb02 | ||
Rolo | 34291f0f3b | ||
Rolo | 4e16956007 | ||
Rolo | bb57686854 | ||
Rolo | ccb0c40b5e | ||
Rolo | 785d09e38f | ||
Rolo | 6fdc1d6a95 | ||
Rolo | c0aadfd4ce | ||
Rolo | 368b29ca72 | ||
Rolo | be8dc22272 | ||
Rolo | a5a9827f32 | ||
Rolo | 88da9e857c | ||
Rolo | 4713eb06b1 | ||
Rolo | 6bdc6f460e | ||
ves | 97f683b336 | ||
Blaž Hrastnik | 8924691c5d | ||
Blaž Hrastnik | f06a166962 | ||
Daniel O'Brien | 1d23796ad1 | ||
Sean Perry | 30baff907d | ||
ath3 | 521accaf00 | ||
Gaëtan Lehmann | ab203b5f53 | ||
Pascal Kuthe | 1cce693bef | ||
Clara Smyth | 43dff1c772 | ||
Jonathan Lebon | 36ee9ba7d6 | ||
Hichem | 69e08d9e91 | ||
Rowan Lovejoy | 7775b35cba | ||
Kieran Moy | 50470f755f | ||
Idobenhamo | 68765f51c9 | ||
Matthew Bourke | 8e161723ee | ||
Alexis-Lapierre | 8256ca7bc3 | ||
blinxen | 70459b2b66 | ||
Pedro Fedricci | 0546273570 | ||
Sufian | 1245760595 | ||
Christopher Kaster | 9df1266376 | ||
GiM | ced96cff2d |
|
@ -62,9 +62,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "anyhow"
|
||||
version = "1.0.81"
|
||||
version = "1.0.82"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0952808a6c2afd1aa8947271f3a60f1a6763c7b912d210184c5149b5cf147247"
|
||||
checksum = "f538837af36e6f6a9be0faa67f9a314f8119e4e4b5867c6ab40ed60360142519"
|
||||
|
||||
[[package]]
|
||||
name = "arc-swap"
|
||||
|
@ -136,9 +136,9 @@ checksum = "df8670b8c7b9dae1793364eafadf7239c40d669904660c5960d74cfd80b46a53"
|
|||
|
||||
[[package]]
|
||||
name = "cc"
|
||||
version = "1.0.90"
|
||||
version = "1.0.95"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8cd6604a82acf3039f1144f54b8eb34e91ffba622051189e71b781822d5ee1f5"
|
||||
checksum = "d32a725bc159af97c3e629873bb9f88fb8cf8a4867175f76dc987815ea07c83b"
|
||||
|
||||
[[package]]
|
||||
name = "cfg-if"
|
||||
|
@ -159,9 +159,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "chrono"
|
||||
version = "0.4.37"
|
||||
version = "0.4.38"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8a0d04d43504c61aa6c7531f1871dd0d418d91130162063b789da00fd7057a5e"
|
||||
checksum = "a21f936df1771bf62b77f047b726c4625ff2e8aa607c01ec06e5a05bd8463401"
|
||||
dependencies = [
|
||||
"android-tzdata",
|
||||
"iana-time-zone",
|
||||
|
@ -171,9 +171,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "5.3.0"
|
||||
version = "5.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d517d4b86184dbb111d3556a10f1c8a04da7428d2987bf1081602bf11c3aa9ee"
|
||||
checksum = "79f4473f5144e20d9aceaf2972478f06ddf687831eafeeb434fbaf0acc4144ad"
|
||||
dependencies = [
|
||||
"error-code",
|
||||
]
|
||||
|
@ -365,9 +365,9 @@ checksum = "a26ae43d7bcc3b814de94796a5e736d4029efb0ee900c12e2d54c993ad1a1e07"
|
|||
|
||||
[[package]]
|
||||
name = "encoding_rs"
|
||||
version = "0.8.33"
|
||||
version = "0.8.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7268b386296a025e474d5140678f75d6de9493ae55a5d709eeb9dd08149945e1"
|
||||
checksum = "b45de904aa0b010bce2ab45264d0631681847fa7b6f2eaa7dab7619943bc4f59"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
]
|
||||
|
@ -538,9 +538,9 @@ checksum = "b6c80984affa11d98d1b88b66ac8853f143217b399d3c74116778ff8fdb4ed2e"
|
|||
|
||||
[[package]]
|
||||
name = "gix"
|
||||
version = "0.61.0"
|
||||
version = "0.62.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e4e0e59a44bf00de058ee98d6ecf3c9ed8f8842c1da642258ae4120d41ded8f7"
|
||||
checksum = "5631c64fb4cd48eee767bf98a3cbc5c9318ef3bb71074d4c099a2371510282b6"
|
||||
dependencies = [
|
||||
"gix-actor",
|
||||
"gix-attributes",
|
||||
|
@ -663,9 +663,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-config"
|
||||
version = "0.36.0"
|
||||
version = "0.36.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "62129c75e4b6229fe15fb9838cdc00c655e87105b651e4edd7c183fc5288b5d1"
|
||||
checksum = "7580e05996e893347ad04e1eaceb92e1c0e6a3ffe517171af99bf6b6df0ca6e5"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-config-value",
|
||||
|
@ -709,9 +709,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-diff"
|
||||
version = "0.42.0"
|
||||
version = "0.43.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "78e605593c2ef74980a534ade0909c7dc57cca72baa30cbb67d2dda621f99ac4"
|
||||
checksum = "a5fbc24115b957346cd23fb0f47d830eb799c46c89cdcf2f5acc9bf2938c2d01"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-command",
|
||||
|
@ -729,9 +729,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-dir"
|
||||
version = "0.3.0"
|
||||
version = "0.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3413ccd29130900c17574678aee640e4847909acae9febf6424dc77b782c6d32"
|
||||
checksum = "d6943a1f213ad7a060a0548ece229be53f3c2151534b126446ce3533eaf5f14c"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-discover",
|
||||
|
@ -784,9 +784,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-filter"
|
||||
version = "0.11.0"
|
||||
version = "0.11.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bd71bf3e64d8fb5d5635d4166ca5a36fe56b292ffff06eab1d93ea47fd5beb89"
|
||||
checksum = "5c0d1f01af62bfd2fb3dd291acc2b29d4ab3e96ad52a679174626508ce98ef12"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"encoding_rs",
|
||||
|
@ -805,9 +805,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-fs"
|
||||
version = "0.10.1"
|
||||
version = "0.10.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "634b8a743b0aae03c1a74ee0ea24e8c5136895efac64ce52b3ea106e1c6f0613"
|
||||
checksum = "e2184c40e7910529677831c8b481acf788ffd92427ed21fad65b6aa637e631b8"
|
||||
dependencies = [
|
||||
"gix-features",
|
||||
"gix-utils",
|
||||
|
@ -861,9 +861,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-index"
|
||||
version = "0.31.1"
|
||||
version = "0.32.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "549621f13d9ccf325a7de45506a3266af0d08f915181c5687abb5e8669bfd2e6"
|
||||
checksum = "3383122cf18655ef4c097c0b935bba5eb56983947959aaf3b0ceb1949d4dd371"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bstr",
|
||||
|
@ -929,9 +929,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-odb"
|
||||
version = "0.59.0"
|
||||
version = "0.60.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "81b55378c719693380f66d9dd21ce46721eed2981d8789fc698ec1ada6fa176e"
|
||||
checksum = "e8bbb43d2fefdc4701ffdf9224844d05b136ae1b9a73c2f90710c8dd27a93503"
|
||||
dependencies = [
|
||||
"arc-swap",
|
||||
"gix-date",
|
||||
|
@ -949,9 +949,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-pack"
|
||||
version = "0.49.0"
|
||||
version = "0.50.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6391aeaa030ad64aba346a9f5c69bb1c4e5c6fb4411705b03b40b49d8614ec30"
|
||||
checksum = "b58bad27c7677fa6b587aab3a1aca0b6c97373bd371a0a4290677c838c9bcaf1"
|
||||
dependencies = [
|
||||
"clru",
|
||||
"gix-chunk",
|
||||
|
@ -969,9 +969,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-packetline-blocking"
|
||||
version = "0.17.3"
|
||||
version = "0.17.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca8ef6dd3ea50e26f3bf572e90c034d033c804d340cd1eb386392f184a9ba2f7"
|
||||
checksum = "c31d42378a3d284732e4d589979930d0d253360eccf7ec7a80332e5ccb77e14a"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"faster-hex",
|
||||
|
@ -994,9 +994,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-pathspec"
|
||||
version = "0.7.2"
|
||||
version = "0.7.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1a96ed0e71ce9084a471fddfa74e842576a7cbf02fe8bd50388017ac461aed97"
|
||||
checksum = "d479789f3abd10f68a709454ce04cd68b54092ee882c8622ae3aa1bb9bf8496c"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"bstr",
|
||||
|
@ -1099,9 +1099,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-status"
|
||||
version = "0.8.0"
|
||||
version = "0.9.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ca216db89947eca709f69ec5851aa76f9628e7c7aab7aa5a927d0c619d046bf2"
|
||||
checksum = "50c413bfd2952e4ee92e48438dac3c696f3555e586a34d184a427f6bedd1e4f9"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"filetime",
|
||||
|
@ -1150,16 +1150,17 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-trace"
|
||||
version = "0.1.8"
|
||||
version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9b838b2db8f62c9447d483a4c28d251b67fee32741a82cb4d35e9eb4e9fdc5ab"
|
||||
checksum = "f924267408915fddcd558e3f37295cc7d6a3e50f8bd8b606cee0808c3915157e"
|
||||
|
||||
[[package]]
|
||||
name = "gix-traverse"
|
||||
version = "0.38.0"
|
||||
version = "0.39.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "95aef84bc777025403a09788b1e4815c06a19332e9e5d87a955e1ed7da9bf0cf"
|
||||
checksum = "f4029ec209b0cc480d209da3837a42c63801dd8548f09c1f4502c60accb62aeb"
|
||||
dependencies = [
|
||||
"bitflags 2.5.0",
|
||||
"gix-commitgraph",
|
||||
"gix-date",
|
||||
"gix-hash",
|
||||
|
@ -1172,9 +1173,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-url"
|
||||
version = "0.27.2"
|
||||
version = "0.27.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f0b24f3ecc79a5a53539de9c2e99425d0ef23feacdcf3faac983aa9a2f26849"
|
||||
checksum = "0db829ebdca6180fbe32be7aed393591df6db4a72dbbc0b8369162390954d1cf"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-features",
|
||||
|
@ -1186,9 +1187,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-utils"
|
||||
version = "0.1.11"
|
||||
version = "0.1.12"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0066432d4c277f9877f091279a597ea5331f68ca410efc874f0bdfb1cd348f92"
|
||||
checksum = "35192df7fd0fa112263bad8021e2df7167df4cc2a6e6d15892e1e55621d3d4dc"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"fastrand",
|
||||
|
@ -1207,9 +1208,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "gix-worktree"
|
||||
version = "0.32.0"
|
||||
version = "0.33.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe78e03af9eec168eb187e05463a981c57f0a915f64b1788685a776bd2ef969c"
|
||||
checksum = "359a87dfef695b5f91abb9a424c947edca82768f34acfc269659f66174a510b4"
|
||||
dependencies = [
|
||||
"bstr",
|
||||
"gix-attributes",
|
||||
|
@ -1392,6 +1393,7 @@ dependencies = [
|
|||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"slotmap",
|
||||
"thiserror",
|
||||
"tokio",
|
||||
"tokio-stream",
|
||||
|
@ -2092,18 +2094,18 @@ checksum = "1792db035ce95be60c3f8853017b3999209281c24e2ba5bc8e59bf97a0c590c1"
|
|||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.197"
|
||||
version = "1.0.198"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fb1c873e1b9b056a4dc4c0c198b24c3ffa059243875552b2bd0933b1aee4ce2"
|
||||
checksum = "9846a40c979031340571da2545a4e5b7c4163bdae79b301d5f86d03979451fcc"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.197"
|
||||
version = "1.0.198"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7eb0b34b42edc17f6b7cac84a52a1c5f0e1bb2227e997ca9011ea3dd34e8610b"
|
||||
checksum = "e88edab869b01783ba905e7d0153f9fc1a6505a96e4ad3018011eedb838566d9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
|
@ -2112,9 +2114,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "serde_json"
|
||||
version = "1.0.115"
|
||||
version = "1.0.116"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd"
|
||||
checksum = "3e17db7126d17feb94eb3fad46bf1a96b034e8aacbc2e775fe81505f8b0b2813"
|
||||
dependencies = [
|
||||
"itoa",
|
||||
"ryu",
|
||||
|
@ -2471,9 +2473,9 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "tree-sitter"
|
||||
version = "0.22.2"
|
||||
version = "0.22.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bdb9c9f15eae91dcd00ee0d86a281d16e6263786991b662b34fa9632c21a046b"
|
||||
checksum = "688200d842c76dd88f9a7719ecb0483f79f5a766fb1c100756d5d8a059abc71b"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"regex",
|
||||
|
|
|
@ -39,6 +39,7 @@ package.helix-term.opt-level = 2
|
|||
[workspace.dependencies]
|
||||
tree-sitter = { version = "0.22" }
|
||||
nucleo = "0.2.0"
|
||||
slotmap = "1.0.7"
|
||||
|
||||
[workspace.package]
|
||||
version = "24.3.0"
|
||||
|
|
|
@ -29,6 +29,7 @@
|
|||
"namespace" = "magenta"
|
||||
"ui.help" = { fg = "white", bg = "black" }
|
||||
"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] }
|
||||
"ui.virtual.ruler" = { bg = "black" }
|
||||
|
||||
"markup.heading" = "blue"
|
||||
"markup.list" = "red"
|
||||
|
|
|
@ -122,6 +122,7 @@
|
|||
| mermaid | ✓ | | | |
|
||||
| meson | ✓ | | ✓ | |
|
||||
| mint | | | | `mint` |
|
||||
| move | ✓ | | | |
|
||||
| msbuild | ✓ | | ✓ | |
|
||||
| nasm | ✓ | ✓ | | |
|
||||
| nickel | ✓ | | ✓ | `nls` |
|
||||
|
@ -197,7 +198,7 @@
|
|||
| tsx | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| twig | ✓ | | | |
|
||||
| typescript | ✓ | ✓ | ✓ | `typescript-language-server` |
|
||||
| typst | ✓ | | | `typst-lsp` |
|
||||
| typst | ✓ | | | `tinymist`, `typst-lsp` |
|
||||
| ungrammar | ✓ | | | |
|
||||
| unison | ✓ | | ✓ | |
|
||||
| uxntal | ✓ | | | |
|
||||
|
@ -215,6 +216,7 @@
|
|||
| wren | ✓ | ✓ | ✓ | |
|
||||
| xit | ✓ | | | |
|
||||
| xml | ✓ | | ✓ | |
|
||||
| xtc | ✓ | | | |
|
||||
| yaml | ✓ | | ✓ | `yaml-language-server`, `ansible-language-server` |
|
||||
| yuck | ✓ | | | |
|
||||
| zig | ✓ | ✓ | ✓ | `zls` |
|
||||
|
|
|
@ -87,3 +87,4 @@
|
|||
| `:redraw` | Clear and re-render the whole UI |
|
||||
| `:move` | Move the current buffer and its corresponding file to a different path |
|
||||
| `:yank-diagnostic` | Yank diagnostic(s) under primary cursor to register, or clipboard by default |
|
||||
| `:read`, `:r` | Load a file into buffer |
|
||||
|
|
|
@ -24,6 +24,8 @@ # Keymap
|
|||
|
||||
> 💡 Mappings marked (**TS**) require a tree-sitter grammar for the file type.
|
||||
|
||||
> ⚠️ Some terminals' default key mappings conflict with Helix's. If any of the mappings described on this page do not work as expected, check your terminal's mappings to ensure they do not conflict. See the (wiki)[https://github.com/helix-editor/helix/wiki/Terminal-Support] for known conflicts.
|
||||
|
||||
## Normal mode
|
||||
|
||||
Normal mode is the default mode when you launch helix. You can return to it from other modes by pressing the `Escape` key.
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
# Language Support
|
||||
|
||||
The following languages and Language Servers are supported. To use
|
||||
Language Server features, you must first [install][lsp-install-wiki] the
|
||||
Language Server features, you must first [configure][lsp-config-wiki] the
|
||||
appropriate Language Server.
|
||||
|
||||
You can check the language support in your installed helix version with `hx --health`.
|
||||
|
@ -11,6 +11,6 @@ # Language Support
|
|||
|
||||
{{#include ./generated/lang-support.md}}
|
||||
|
||||
[lsp-install-wiki]: https://github.com/helix-editor/helix/wiki/How-to-install-the-default-language-servers
|
||||
[lsp-config-wiki]: https://github.com/helix-editor/helix/wiki/Language-Server-Configurations
|
||||
[lang-config]: ./languages.md
|
||||
[adding-languages]: ./guides/adding_languages.md
|
||||
|
|
|
@ -25,8 +25,7 @@ smartstring = "1.0.1"
|
|||
unicode-segmentation = "1.11"
|
||||
unicode-width = "0.1"
|
||||
unicode-general-category = "0.6"
|
||||
# slab = "0.4.2"
|
||||
slotmap = "1.0"
|
||||
slotmap.workspace = true
|
||||
tree-sitter.workspace = true
|
||||
once_cell = "1.19"
|
||||
arc-swap = "1"
|
||||
|
|
|
@ -1,4 +1,6 @@
|
|||
//! LSP diagnostic utility types.
|
||||
use std::fmt;
|
||||
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Describes the severity level of a [`Diagnostic`].
|
||||
|
@ -47,8 +49,25 @@ pub struct Diagnostic {
|
|||
pub message: String,
|
||||
pub severity: Option<Severity>,
|
||||
pub code: Option<NumberOrString>,
|
||||
pub language_server_id: usize,
|
||||
pub provider: DiagnosticProvider,
|
||||
pub tags: Vec<DiagnosticTag>,
|
||||
pub source: Option<String>,
|
||||
pub data: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
// TODO turn this into an enum + feature flag when lsp becomes optional
|
||||
pub type DiagnosticProvider = LanguageServerId;
|
||||
|
||||
// while I would prefer having this in helix-lsp that necessitates a bunch of
|
||||
// conversions I would rather not add. I think its fine since this just a very
|
||||
// trivial newtype wrapper and we would need something similar once we define
|
||||
// completions in core
|
||||
slotmap::new_key_type! {
|
||||
pub struct LanguageServerId;
|
||||
}
|
||||
|
||||
impl fmt::Display for LanguageServerId {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(f, "{:?}", self.0)
|
||||
}
|
||||
}
|
||||
|
|
|
@ -247,41 +247,18 @@ fn add_indent_level(
|
|||
}
|
||||
}
|
||||
|
||||
/// Computes for node and all ancestors whether they are the first node on their line.
|
||||
/// The first entry in the return value represents the root node, the last one the node itself
|
||||
fn get_first_in_line(mut node: Node, new_line_byte_pos: Option<usize>) -> Vec<bool> {
|
||||
let mut first_in_line = Vec::new();
|
||||
loop {
|
||||
if let Some(prev) = node.prev_sibling() {
|
||||
// If we insert a new line, the first node at/after the cursor is considered to be the first in its line
|
||||
let first = prev.end_position().row != node.start_position().row
|
||||
|| new_line_byte_pos.map_or(false, |byte_pos| {
|
||||
node.start_byte() >= byte_pos && prev.start_byte() < byte_pos
|
||||
});
|
||||
first_in_line.push(Some(first));
|
||||
} else {
|
||||
// Nodes that have no previous siblings are first in their line if and only if their parent is
|
||||
// (which we don't know yet)
|
||||
first_in_line.push(None);
|
||||
}
|
||||
if let Some(parent) = node.parent() {
|
||||
node = parent;
|
||||
} else {
|
||||
break;
|
||||
/// Return true if only whitespace comes before the node on its line.
|
||||
/// If given, new_line_byte_pos is treated the same way as any existing newline.
|
||||
fn is_first_in_line(node: Node, text: RopeSlice, new_line_byte_pos: Option<usize>) -> bool {
|
||||
let mut line_start_byte_pos = text.line_to_byte(node.start_position().row);
|
||||
if let Some(pos) = new_line_byte_pos {
|
||||
if line_start_byte_pos < pos && pos <= node.start_byte() {
|
||||
line_start_byte_pos = pos;
|
||||
}
|
||||
}
|
||||
|
||||
let mut result = Vec::with_capacity(first_in_line.len());
|
||||
let mut parent_is_first = true; // The root node is by definition the first node in its line
|
||||
for first in first_in_line.into_iter().rev() {
|
||||
if let Some(first) = first {
|
||||
result.push(first);
|
||||
parent_is_first = first;
|
||||
} else {
|
||||
result.push(parent_is_first);
|
||||
}
|
||||
}
|
||||
result
|
||||
text.byte_slice(line_start_byte_pos..node.start_byte())
|
||||
.chars()
|
||||
.all(|c| c.is_whitespace())
|
||||
}
|
||||
|
||||
/// The total indent for some line of code.
|
||||
|
@ -852,7 +829,6 @@ pub fn treesitter_indent_for_pos<'a>(
|
|||
byte_pos,
|
||||
new_line_byte_pos,
|
||||
)?;
|
||||
let mut first_in_line = get_first_in_line(node, new_line.then_some(byte_pos));
|
||||
|
||||
let mut result = Indentation::default();
|
||||
// We always keep track of all the indent changes on one line, in order to only indent once
|
||||
|
@ -861,9 +837,7 @@ pub fn treesitter_indent_for_pos<'a>(
|
|||
let mut indent_for_line_below = Indentation::default();
|
||||
|
||||
loop {
|
||||
// This can safely be unwrapped because `first_in_line` contains
|
||||
// one entry for each ancestor of the node (which is what we iterate over)
|
||||
let is_first = *first_in_line.last().unwrap();
|
||||
let is_first = is_first_in_line(node, text, new_line_byte_pos);
|
||||
|
||||
// Apply all indent definitions for this node.
|
||||
// Since we only iterate over each node once, we can remove the
|
||||
|
@ -906,7 +880,6 @@ pub fn treesitter_indent_for_pos<'a>(
|
|||
}
|
||||
|
||||
node = parent;
|
||||
first_in_line.pop();
|
||||
} else {
|
||||
// Only add the indentation for the line below if that line
|
||||
// is not after the line that the indentation is calculated for.
|
||||
|
|
|
@ -9,16 +9,32 @@
|
|||
const MAX_PLAINTEXT_SCAN: usize = 10000;
|
||||
const MATCH_LIMIT: usize = 16;
|
||||
|
||||
// Limit matching pairs to only ( ) { } [ ] < > ' ' " "
|
||||
const PAIRS: &[(char, char)] = &[
|
||||
pub const BRACKETS: [(char, char); 7] = [
|
||||
('(', ')'),
|
||||
('{', '}'),
|
||||
('[', ']'),
|
||||
('<', '>'),
|
||||
('\'', '\''),
|
||||
('\"', '\"'),
|
||||
('«', '»'),
|
||||
('「', '」'),
|
||||
('(', ')'),
|
||||
];
|
||||
|
||||
// The difference between BRACKETS and PAIRS is that we can find matching
|
||||
// BRACKETS in a plain text file, but we can't do the same for PAIRs.
|
||||
// PAIRS also contains all BRACKETS.
|
||||
pub const PAIRS: [(char, char); BRACKETS.len() + 3] = {
|
||||
let mut pairs = [(' ', ' '); BRACKETS.len() + 3];
|
||||
let mut idx = 0;
|
||||
while idx < BRACKETS.len() {
|
||||
pairs[idx] = BRACKETS[idx];
|
||||
idx += 1;
|
||||
}
|
||||
pairs[idx] = ('"', '"');
|
||||
pairs[idx + 1] = ('\'', '\'');
|
||||
pairs[idx + 2] = ('`', '`');
|
||||
pairs
|
||||
};
|
||||
|
||||
/// Returns the position of the matching bracket under cursor.
|
||||
///
|
||||
/// If the cursor is on the opening bracket, the position of
|
||||
|
@ -30,7 +46,7 @@
|
|||
/// If no matching bracket is found, `None` is returned.
|
||||
#[must_use]
|
||||
pub fn find_matching_bracket(syntax: &Syntax, doc: RopeSlice, pos: usize) -> Option<usize> {
|
||||
if pos >= doc.len_chars() || !is_valid_bracket(doc.char(pos)) {
|
||||
if pos >= doc.len_chars() || !is_valid_pair(doc.char(pos)) {
|
||||
return None;
|
||||
}
|
||||
find_pair(syntax, doc, pos, false)
|
||||
|
@ -67,7 +83,7 @@ fn find_pair(
|
|||
let (start_byte, end_byte) = surrounding_bytes(doc, &node)?;
|
||||
let (start_char, end_char) = (doc.byte_to_char(start_byte), doc.byte_to_char(end_byte));
|
||||
|
||||
if is_valid_pair(doc, start_char, end_char) {
|
||||
if is_valid_pair_on_pos(doc, start_char, end_char) {
|
||||
if end_byte == pos {
|
||||
return Some(start_char);
|
||||
}
|
||||
|
@ -140,14 +156,22 @@ fn find_pair(
|
|||
/// If no matching bracket is found, `None` is returned.
|
||||
#[must_use]
|
||||
pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Option<usize> {
|
||||
// Don't do anything when the cursor is not on top of a bracket.
|
||||
let bracket = doc.get_char(cursor_pos)?;
|
||||
let matching_bracket = {
|
||||
let pair = get_pair(bracket);
|
||||
if pair.0 == bracket {
|
||||
pair.1
|
||||
} else {
|
||||
pair.0
|
||||
}
|
||||
};
|
||||
// Don't do anything when the cursor is not on top of a bracket.
|
||||
if !is_valid_bracket(bracket) {
|
||||
return None;
|
||||
}
|
||||
|
||||
// Determine the direction of the matching.
|
||||
let is_fwd = is_forward_bracket(bracket);
|
||||
let is_fwd = is_open_bracket(bracket);
|
||||
let chars_iter = if is_fwd {
|
||||
doc.chars_at(cursor_pos + 1)
|
||||
} else {
|
||||
|
@ -159,19 +183,7 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt
|
|||
for (i, candidate) in chars_iter.take(MAX_PLAINTEXT_SCAN).enumerate() {
|
||||
if candidate == bracket {
|
||||
open_cnt += 1;
|
||||
} else if is_valid_pair(
|
||||
doc,
|
||||
if is_fwd {
|
||||
cursor_pos
|
||||
} else {
|
||||
cursor_pos - i - 1
|
||||
},
|
||||
if is_fwd {
|
||||
cursor_pos + i + 1
|
||||
} else {
|
||||
cursor_pos
|
||||
},
|
||||
) {
|
||||
} else if candidate == matching_bracket {
|
||||
// Return when all pending brackets have been closed.
|
||||
if open_cnt == 1 {
|
||||
return Some(if is_fwd {
|
||||
|
@ -187,15 +199,49 @@ pub fn find_matching_bracket_plaintext(doc: RopeSlice, cursor_pos: usize) -> Opt
|
|||
None
|
||||
}
|
||||
|
||||
fn is_valid_bracket(c: char) -> bool {
|
||||
PAIRS.iter().any(|(l, r)| *l == c || *r == c)
|
||||
/// Returns the open and closing chars pair. If not found in
|
||||
/// [`BRACKETS`] returns (ch, ch).
|
||||
///
|
||||
/// ```
|
||||
/// use helix_core::match_brackets::get_pair;
|
||||
///
|
||||
/// assert_eq!(get_pair('['), ('[', ']'));
|
||||
/// assert_eq!(get_pair('}'), ('{', '}'));
|
||||
/// assert_eq!(get_pair('"'), ('"', '"'));
|
||||
/// ```
|
||||
pub fn get_pair(ch: char) -> (char, char) {
|
||||
PAIRS
|
||||
.iter()
|
||||
.find(|(open, close)| *open == ch || *close == ch)
|
||||
.copied()
|
||||
.unwrap_or((ch, ch))
|
||||
}
|
||||
|
||||
fn is_forward_bracket(c: char) -> bool {
|
||||
PAIRS.iter().any(|(l, _)| *l == c)
|
||||
pub fn is_open_bracket(ch: char) -> bool {
|
||||
BRACKETS.iter().any(|(l, _)| *l == ch)
|
||||
}
|
||||
|
||||
fn is_valid_pair(doc: RopeSlice, start_char: usize, end_char: usize) -> bool {
|
||||
pub fn is_close_bracket(ch: char) -> bool {
|
||||
BRACKETS.iter().any(|(_, r)| *r == ch)
|
||||
}
|
||||
|
||||
pub fn is_valid_bracket(ch: char) -> bool {
|
||||
BRACKETS.iter().any(|(l, r)| *l == ch || *r == ch)
|
||||
}
|
||||
|
||||
pub fn is_open_pair(ch: char) -> bool {
|
||||
PAIRS.iter().any(|(l, _)| *l == ch)
|
||||
}
|
||||
|
||||
pub fn is_close_pair(ch: char) -> bool {
|
||||
PAIRS.iter().any(|(_, r)| *r == ch)
|
||||
}
|
||||
|
||||
pub fn is_valid_pair(ch: char) -> bool {
|
||||
PAIRS.iter().any(|(l, r)| *l == ch || *r == ch)
|
||||
}
|
||||
|
||||
fn is_valid_pair_on_pos(doc: RopeSlice, start_char: usize, end_char: usize) -> bool {
|
||||
PAIRS.contains(&(doc.char(start_char), doc.char(end_char)))
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
|
||||
use crate::{movement::Direction, syntax::TreeCursor, Range, RopeSlice, Selection, Syntax};
|
||||
|
||||
pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
let cursor = &mut syntax.walk();
|
||||
|
@ -25,19 +25,31 @@ pub fn expand_selection(syntax: &Syntax, text: RopeSlice, selection: Selection)
|
|||
}
|
||||
|
||||
pub fn shrink_selection(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(syntax, text, selection, |cursor| {
|
||||
cursor.goto_first_child();
|
||||
})
|
||||
select_node_impl(
|
||||
syntax,
|
||||
text,
|
||||
selection,
|
||||
|cursor| {
|
||||
cursor.goto_first_child();
|
||||
},
|
||||
None,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn select_next_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(syntax, text, selection, |cursor| {
|
||||
while !cursor.goto_next_sibling() {
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
select_node_impl(
|
||||
syntax,
|
||||
text,
|
||||
selection,
|
||||
|cursor| {
|
||||
while !cursor.goto_next_sibling() {
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
Some(Direction::Forward),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn select_all_siblings(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
|
@ -81,13 +93,19 @@ fn select_children<'n>(
|
|||
}
|
||||
|
||||
pub fn select_prev_sibling(syntax: &Syntax, text: RopeSlice, selection: Selection) -> Selection {
|
||||
select_node_impl(syntax, text, selection, |cursor| {
|
||||
while !cursor.goto_prev_sibling() {
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
select_node_impl(
|
||||
syntax,
|
||||
text,
|
||||
selection,
|
||||
|cursor| {
|
||||
while !cursor.goto_prev_sibling() {
|
||||
if !cursor.goto_parent() {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
},
|
||||
Some(Direction::Backward),
|
||||
)
|
||||
}
|
||||
|
||||
fn select_node_impl<F>(
|
||||
|
@ -95,6 +113,7 @@ fn select_node_impl<F>(
|
|||
text: RopeSlice,
|
||||
selection: Selection,
|
||||
motion: F,
|
||||
direction: Option<Direction>,
|
||||
) -> Selection
|
||||
where
|
||||
F: Fn(&mut TreeCursor),
|
||||
|
@ -113,6 +132,6 @@ fn select_node_impl<F>(
|
|||
let from = text.byte_to_char(node.start_byte());
|
||||
let to = text.byte_to_char(node.end_byte());
|
||||
|
||||
Range::new(from, to).with_direction(range.direction())
|
||||
Range::new(from, to).with_direction(direction.unwrap_or_else(|| range.direction()))
|
||||
})
|
||||
}
|
||||
|
|
|
@ -122,7 +122,7 @@ pub fn is_empty(&self) -> bool {
|
|||
}
|
||||
|
||||
/// `Direction::Backward` when head < anchor.
|
||||
/// `Direction::Backward` otherwise.
|
||||
/// `Direction::Forward` otherwise.
|
||||
#[inline]
|
||||
#[must_use]
|
||||
pub fn direction(&self) -> Direction {
|
||||
|
|
|
@ -1,18 +1,16 @@
|
|||
use std::fmt::Display;
|
||||
|
||||
use crate::{movement::Direction, search, Range, Selection};
|
||||
use crate::{
|
||||
graphemes::next_grapheme_boundary,
|
||||
match_brackets::{
|
||||
find_matching_bracket, find_matching_bracket_fuzzy, get_pair, is_close_bracket,
|
||||
is_open_bracket,
|
||||
},
|
||||
movement::Direction,
|
||||
search, Range, Selection, Syntax,
|
||||
};
|
||||
use ropey::RopeSlice;
|
||||
|
||||
pub const PAIRS: &[(char, char)] = &[
|
||||
('(', ')'),
|
||||
('[', ']'),
|
||||
('{', '}'),
|
||||
('<', '>'),
|
||||
('«', '»'),
|
||||
('「', '」'),
|
||||
('(', ')'),
|
||||
];
|
||||
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum Error {
|
||||
PairNotFound,
|
||||
|
@ -34,32 +32,68 @@ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|||
|
||||
type Result<T> = std::result::Result<T, Error>;
|
||||
|
||||
/// Given any char in [PAIRS], return the open and closing chars. If not found in
|
||||
/// [PAIRS] return (ch, ch).
|
||||
/// Finds the position of surround pairs of any [`crate::match_brackets::PAIRS`]
|
||||
/// using tree-sitter when possible.
|
||||
///
|
||||
/// ```
|
||||
/// use helix_core::surround::get_pair;
|
||||
/// # Returns
|
||||
///
|
||||
/// assert_eq!(get_pair('['), ('[', ']'));
|
||||
/// assert_eq!(get_pair('}'), ('{', '}'));
|
||||
/// assert_eq!(get_pair('"'), ('"', '"'));
|
||||
/// ```
|
||||
pub fn get_pair(ch: char) -> (char, char) {
|
||||
PAIRS
|
||||
.iter()
|
||||
.find(|(open, close)| *open == ch || *close == ch)
|
||||
.copied()
|
||||
.unwrap_or((ch, ch))
|
||||
/// Tuple `(anchor, head)`, meaning it is not always ordered.
|
||||
pub fn find_nth_closest_pairs_pos(
|
||||
syntax: Option<&Syntax>,
|
||||
text: RopeSlice,
|
||||
range: Range,
|
||||
skip: usize,
|
||||
) -> Result<(usize, usize)> {
|
||||
match syntax {
|
||||
Some(syntax) => find_nth_closest_pairs_ts(syntax, text, range, skip),
|
||||
None => find_nth_closest_pairs_plain(text, range, skip),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn find_nth_closest_pairs_pos(
|
||||
fn find_nth_closest_pairs_ts(
|
||||
syntax: &Syntax,
|
||||
text: RopeSlice,
|
||||
range: Range,
|
||||
mut skip: usize,
|
||||
) -> Result<(usize, usize)> {
|
||||
let is_open_pair = |ch| PAIRS.iter().any(|(open, _)| *open == ch);
|
||||
let is_close_pair = |ch| PAIRS.iter().any(|(_, close)| *close == ch);
|
||||
let mut opening = range.from();
|
||||
// We want to expand the selection if we are already on the found pair,
|
||||
// otherwise we would need to subtract "-1" from "range.to()".
|
||||
let mut closing = range.to();
|
||||
|
||||
while skip > 0 {
|
||||
closing = find_matching_bracket_fuzzy(syntax, text, closing).ok_or(Error::PairNotFound)?;
|
||||
opening = find_matching_bracket(syntax, text, closing).ok_or(Error::PairNotFound)?;
|
||||
// If we're already on a closing bracket "find_matching_bracket_fuzzy" will return
|
||||
// the position of the opening bracket.
|
||||
if closing < opening {
|
||||
(opening, closing) = (closing, opening);
|
||||
}
|
||||
|
||||
// In case found brackets are partially inside current selection.
|
||||
if range.from() < opening || closing < range.to() - 1 {
|
||||
closing = next_grapheme_boundary(text, closing);
|
||||
} else {
|
||||
skip -= 1;
|
||||
if skip != 0 {
|
||||
closing = next_grapheme_boundary(text, closing);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Keep the original direction.
|
||||
if let Direction::Forward = range.direction() {
|
||||
Ok((opening, closing))
|
||||
} else {
|
||||
Ok((closing, opening))
|
||||
}
|
||||
}
|
||||
|
||||
fn find_nth_closest_pairs_plain(
|
||||
text: RopeSlice,
|
||||
range: Range,
|
||||
mut skip: usize,
|
||||
) -> Result<(usize, usize)> {
|
||||
let mut stack = Vec::with_capacity(2);
|
||||
let pos = range.from();
|
||||
let mut close_pos = pos.saturating_sub(1);
|
||||
|
@ -67,7 +101,7 @@ pub fn find_nth_closest_pairs_pos(
|
|||
for ch in text.chars_at(pos) {
|
||||
close_pos += 1;
|
||||
|
||||
if is_open_pair(ch) {
|
||||
if is_open_bracket(ch) {
|
||||
// Track open pairs encountered so that we can step over
|
||||
// the corresponding close pairs that will come up further
|
||||
// down the loop. We want to find a lone close pair whose
|
||||
|
@ -76,7 +110,7 @@ pub fn find_nth_closest_pairs_pos(
|
|||
continue;
|
||||
}
|
||||
|
||||
if !is_close_pair(ch) {
|
||||
if !is_close_bracket(ch) {
|
||||
// We don't care if this character isn't a brace pair item,
|
||||
// so short circuit here.
|
||||
continue;
|
||||
|
@ -157,7 +191,11 @@ pub fn find_nth_pairs_pos(
|
|||
)
|
||||
};
|
||||
|
||||
Option::zip(open, close).ok_or(Error::PairNotFound)
|
||||
// preserve original direction
|
||||
match range.direction() {
|
||||
Direction::Forward => Option::zip(open, close).ok_or(Error::PairNotFound),
|
||||
Direction::Backward => Option::zip(close, open).ok_or(Error::PairNotFound),
|
||||
}
|
||||
}
|
||||
|
||||
fn find_nth_open_pair(
|
||||
|
@ -249,6 +287,7 @@ fn find_nth_close_pair(
|
|||
/// are automatically detected around each cursor (note that this may result
|
||||
/// in them selecting different surround characters for each selection).
|
||||
pub fn get_surround_pos(
|
||||
syntax: Option<&Syntax>,
|
||||
text: RopeSlice,
|
||||
selection: &Selection,
|
||||
ch: Option<char>,
|
||||
|
@ -257,9 +296,13 @@ pub fn get_surround_pos(
|
|||
let mut change_pos = Vec::new();
|
||||
|
||||
for &range in selection {
|
||||
let (open_pos, close_pos) = match ch {
|
||||
Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?,
|
||||
None => find_nth_closest_pairs_pos(text, range, skip)?,
|
||||
let (open_pos, close_pos) = {
|
||||
let range_raw = match ch {
|
||||
Some(ch) => find_nth_pairs_pos(text, ch, range, skip)?,
|
||||
None => find_nth_closest_pairs_pos(syntax, text, range, skip)?,
|
||||
};
|
||||
let range = Range::new(range_raw.0, range_raw.1);
|
||||
(range.from(), range.to())
|
||||
};
|
||||
if change_pos.contains(&open_pos) || change_pos.contains(&close_pos) {
|
||||
return Err(Error::CursorOverlap);
|
||||
|
@ -288,7 +331,7 @@ fn test_get_surround_pos() {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
get_surround_pos(doc.slice(..), &selection, Some('('), 1).unwrap(),
|
||||
get_surround_pos(None, doc.slice(..), &selection, Some('('), 1).unwrap(),
|
||||
expectations
|
||||
);
|
||||
}
|
||||
|
@ -303,7 +346,7 @@ fn test_get_surround_pos_bail_different_surround_chars() {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
get_surround_pos(doc.slice(..), &selection, Some('('), 1),
|
||||
get_surround_pos(None, doc.slice(..), &selection, Some('('), 1),
|
||||
Err(Error::PairNotFound)
|
||||
);
|
||||
}
|
||||
|
@ -318,7 +361,7 @@ fn test_get_surround_pos_bail_overlapping_surround_chars() {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
get_surround_pos(doc.slice(..), &selection, Some('('), 1),
|
||||
get_surround_pos(None, doc.slice(..), &selection, Some('('), 1),
|
||||
Err(Error::PairNotFound) // overlapping surround chars
|
||||
);
|
||||
}
|
||||
|
@ -333,7 +376,7 @@ fn test_get_surround_pos_bail_cursor_overlap() {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
get_surround_pos(doc.slice(..), &selection, Some('['), 1),
|
||||
get_surround_pos(None, doc.slice(..), &selection, Some('['), 1),
|
||||
Err(Error::CursorOverlap)
|
||||
);
|
||||
}
|
||||
|
@ -397,7 +440,7 @@ fn test_find_nth_closest_pairs_pos_index_range_panic() {
|
|||
);
|
||||
|
||||
assert_eq!(
|
||||
find_nth_closest_pairs_pos(doc.slice(..), selection.primary(), 1),
|
||||
find_nth_closest_pairs_pos(None, doc.slice(..), selection.primary(), 1),
|
||||
Err(Error::PairNotFound)
|
||||
)
|
||||
}
|
||||
|
|
|
@ -2765,10 +2765,10 @@ fn test_textobject_queries() {
|
|||
)
|
||||
};
|
||||
|
||||
test("quantified_nodes", 1..36);
|
||||
test("quantified_nodes", 1..37);
|
||||
// NOTE: Enable after implementing proper node group capturing
|
||||
// test("quantified_nodes_grouped", 1..36);
|
||||
// test("multiple_nodes_grouped", 1..36);
|
||||
// test("quantified_nodes_grouped", 1..37);
|
||||
// test("multiple_nodes_grouped", 1..37);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
@ -2939,7 +2939,7 @@ fn assert_pretty_print(
|
|||
|
||||
#[test]
|
||||
fn test_pretty_print() {
|
||||
let source = r#"/// Hello"#;
|
||||
let source = r#"// Hello"#;
|
||||
assert_pretty_print("rust", source, "(line_comment)", 0, source.len());
|
||||
|
||||
// A large tree should be indented with fields:
|
||||
|
@ -2958,7 +2958,8 @@ fn test_pretty_print() {
|
|||
" (macro_invocation\n",
|
||||
" macro: (identifier)\n",
|
||||
" (token_tree\n",
|
||||
" (string_literal))))))",
|
||||
" (string_literal\n",
|
||||
" (string_content)))))))",
|
||||
),
|
||||
0,
|
||||
source.len(),
|
||||
|
|
|
@ -7,9 +7,9 @@
|
|||
use crate::graphemes::{next_grapheme_boundary, prev_grapheme_boundary};
|
||||
use crate::line_ending::rope_is_line_ending;
|
||||
use crate::movement::Direction;
|
||||
use crate::surround;
|
||||
use crate::syntax::LanguageConfiguration;
|
||||
use crate::Range;
|
||||
use crate::{surround, Syntax};
|
||||
|
||||
fn find_word_boundary(slice: RopeSlice, mut pos: usize, direction: Direction, long: bool) -> usize {
|
||||
use CharCategory::{Eol, Whitespace};
|
||||
|
@ -199,25 +199,28 @@ pub fn textobject_paragraph(
|
|||
}
|
||||
|
||||
pub fn textobject_pair_surround(
|
||||
syntax: Option<&Syntax>,
|
||||
slice: RopeSlice,
|
||||
range: Range,
|
||||
textobject: TextObject,
|
||||
ch: char,
|
||||
count: usize,
|
||||
) -> Range {
|
||||
textobject_pair_surround_impl(slice, range, textobject, Some(ch), count)
|
||||
textobject_pair_surround_impl(syntax, slice, range, textobject, Some(ch), count)
|
||||
}
|
||||
|
||||
pub fn textobject_pair_surround_closest(
|
||||
syntax: Option<&Syntax>,
|
||||
slice: RopeSlice,
|
||||
range: Range,
|
||||
textobject: TextObject,
|
||||
count: usize,
|
||||
) -> Range {
|
||||
textobject_pair_surround_impl(slice, range, textobject, None, count)
|
||||
textobject_pair_surround_impl(syntax, slice, range, textobject, None, count)
|
||||
}
|
||||
|
||||
fn textobject_pair_surround_impl(
|
||||
syntax: Option<&Syntax>,
|
||||
slice: RopeSlice,
|
||||
range: Range,
|
||||
textobject: TextObject,
|
||||
|
@ -226,8 +229,7 @@ fn textobject_pair_surround_impl(
|
|||
) -> Range {
|
||||
let pair_pos = match ch {
|
||||
Some(ch) => surround::find_nth_pairs_pos(slice, ch, range, count),
|
||||
// Automatically find the closest surround pairs
|
||||
None => surround::find_nth_closest_pairs_pos(slice, range, count),
|
||||
None => surround::find_nth_closest_pairs_pos(syntax, slice, range, count),
|
||||
};
|
||||
pair_pos
|
||||
.map(|(anchor, head)| match textobject {
|
||||
|
@ -574,7 +576,8 @@ fn test_textobject_surround() {
|
|||
let slice = doc.slice(..);
|
||||
for &case in scenario {
|
||||
let (pos, objtype, expected_range, ch, count) = case;
|
||||
let result = textobject_pair_surround(slice, Range::point(pos), objtype, ch, count);
|
||||
let result =
|
||||
textobject_pair_surround(None, slice, Range::point(pos), objtype, ch, count);
|
||||
assert_eq!(
|
||||
result,
|
||||
expected_range.into(),
|
||||
|
|
|
@ -31,3 +31,4 @@ tokio = { version = "1.37", features = ["rt", "rt-multi-thread", "io-util", "io-
|
|||
tokio-stream = "0.1.15"
|
||||
parking_lot = "0.12.1"
|
||||
arc-swap = "1"
|
||||
slotmap.workspace = true
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
file_operations::FileOperationsInterest,
|
||||
find_lsp_workspace, jsonrpc,
|
||||
transport::{Payload, Transport},
|
||||
Call, Error, OffsetEncoding, Result,
|
||||
Call, Error, LanguageServerId, OffsetEncoding, Result,
|
||||
};
|
||||
|
||||
use helix_core::{find_workspace, syntax::LanguageServerFeature, ChangeSet, Rope};
|
||||
|
@ -46,7 +46,7 @@ fn workspace_for_uri(uri: lsp::Url) -> WorkspaceFolder {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Client {
|
||||
id: usize,
|
||||
id: LanguageServerId,
|
||||
name: String,
|
||||
_process: Child,
|
||||
server_tx: UnboundedSender<Payload>,
|
||||
|
@ -179,10 +179,14 @@ pub fn start(
|
|||
server_environment: HashMap<String, String>,
|
||||
root_path: PathBuf,
|
||||
root_uri: Option<lsp::Url>,
|
||||
id: usize,
|
||||
id: LanguageServerId,
|
||||
name: String,
|
||||
req_timeout: u64,
|
||||
) -> Result<(Self, UnboundedReceiver<(usize, Call)>, Arc<Notify>)> {
|
||||
) -> Result<(
|
||||
Self,
|
||||
UnboundedReceiver<(LanguageServerId, Call)>,
|
||||
Arc<Notify>,
|
||||
)> {
|
||||
// Resolve path to the binary
|
||||
let cmd = helix_stdx::env::which(cmd)?;
|
||||
|
||||
|
@ -234,7 +238,7 @@ pub fn name(&self) -> &str {
|
|||
&self.name
|
||||
}
|
||||
|
||||
pub fn id(&self) -> usize {
|
||||
pub fn id(&self) -> LanguageServerId {
|
||||
self.id
|
||||
}
|
||||
|
||||
|
@ -393,6 +397,16 @@ fn call<R: lsp::request::Request>(
|
|||
&self,
|
||||
params: R::Params,
|
||||
) -> impl Future<Output = Result<Value>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
self.call_with_ref::<R>(¶ms)
|
||||
}
|
||||
|
||||
fn call_with_ref<R: lsp::request::Request>(
|
||||
&self,
|
||||
params: &R::Params,
|
||||
) -> impl Future<Output = Result<Value>>
|
||||
where
|
||||
R::Params: serde::Serialize,
|
||||
{
|
||||
|
@ -401,7 +415,7 @@ fn call<R: lsp::request::Request>(
|
|||
|
||||
fn call_with_timeout<R: lsp::request::Request>(
|
||||
&self,
|
||||
params: R::Params,
|
||||
params: &R::Params,
|
||||
timeout_secs: u64,
|
||||
) -> impl Future<Output = Result<Value>>
|
||||
where
|
||||
|
@ -410,17 +424,16 @@ fn call_with_timeout<R: lsp::request::Request>(
|
|||
let server_tx = self.server_tx.clone();
|
||||
let id = self.next_request_id();
|
||||
|
||||
let params = serde_json::to_value(params);
|
||||
async move {
|
||||
use std::time::Duration;
|
||||
use tokio::time::timeout;
|
||||
|
||||
let params = serde_json::to_value(params)?;
|
||||
|
||||
let request = jsonrpc::MethodCall {
|
||||
jsonrpc: Some(jsonrpc::Version::V2),
|
||||
id: id.clone(),
|
||||
method: R::METHOD.to_string(),
|
||||
params: Self::value_into_params(params),
|
||||
params: Self::value_into_params(params?),
|
||||
};
|
||||
|
||||
let (tx, mut rx) = channel::<Result<Value>>(1);
|
||||
|
@ -737,7 +750,7 @@ pub fn will_rename(
|
|||
new_uri: url_from_path(new_path)?,
|
||||
}];
|
||||
let request = self.call_with_timeout::<lsp::request::WillRenameFiles>(
|
||||
lsp::RenameFilesParams { files },
|
||||
&lsp::RenameFilesParams { files },
|
||||
5,
|
||||
);
|
||||
|
||||
|
@ -1022,21 +1035,10 @@ pub fn completion(
|
|||
|
||||
pub fn resolve_completion_item(
|
||||
&self,
|
||||
completion_item: lsp::CompletionItem,
|
||||
) -> Option<impl Future<Output = Result<lsp::CompletionItem>>> {
|
||||
let capabilities = self.capabilities.get().unwrap();
|
||||
|
||||
// Return early if the server does not support resolving completion items.
|
||||
match capabilities.completion_provider {
|
||||
Some(lsp::CompletionOptions {
|
||||
resolve_provider: Some(true),
|
||||
..
|
||||
}) => (),
|
||||
_ => return None,
|
||||
}
|
||||
|
||||
let res = self.call::<lsp::request::ResolveCompletionItem>(completion_item);
|
||||
Some(async move { Ok(serde_json::from_value(res.await?)?) })
|
||||
completion_item: &lsp::CompletionItem,
|
||||
) -> impl Future<Output = Result<lsp::CompletionItem>> {
|
||||
let res = self.call_with_ref::<lsp::request::ResolveCompletionItem>(completion_item);
|
||||
async move { Ok(serde_json::from_value(res.await?)?) }
|
||||
}
|
||||
|
||||
pub fn resolve_code_action(
|
||||
|
|
|
@ -3,24 +3,24 @@
|
|||
use globset::{GlobBuilder, GlobSetBuilder};
|
||||
use tokio::sync::mpsc;
|
||||
|
||||
use crate::{lsp, Client};
|
||||
use crate::{lsp, Client, LanguageServerId};
|
||||
|
||||
enum Event {
|
||||
FileChanged {
|
||||
path: PathBuf,
|
||||
},
|
||||
Register {
|
||||
client_id: usize,
|
||||
client_id: LanguageServerId,
|
||||
client: Weak<Client>,
|
||||
registration_id: String,
|
||||
options: lsp::DidChangeWatchedFilesRegistrationOptions,
|
||||
},
|
||||
Unregister {
|
||||
client_id: usize,
|
||||
client_id: LanguageServerId,
|
||||
registration_id: String,
|
||||
},
|
||||
RemoveClient {
|
||||
client_id: usize,
|
||||
client_id: LanguageServerId,
|
||||
},
|
||||
}
|
||||
|
||||
|
@ -59,7 +59,7 @@ pub fn new() -> Self {
|
|||
|
||||
pub fn register(
|
||||
&self,
|
||||
client_id: usize,
|
||||
client_id: LanguageServerId,
|
||||
client: Weak<Client>,
|
||||
registration_id: String,
|
||||
options: lsp::DidChangeWatchedFilesRegistrationOptions,
|
||||
|
@ -72,7 +72,7 @@ pub fn register(
|
|||
});
|
||||
}
|
||||
|
||||
pub fn unregister(&self, client_id: usize, registration_id: String) {
|
||||
pub fn unregister(&self, client_id: LanguageServerId, registration_id: String) {
|
||||
let _ = self.tx.send(Event::Unregister {
|
||||
client_id,
|
||||
registration_id,
|
||||
|
@ -83,12 +83,12 @@ pub fn file_changed(&self, path: PathBuf) {
|
|||
let _ = self.tx.send(Event::FileChanged { path });
|
||||
}
|
||||
|
||||
pub fn remove_client(&self, client_id: usize) {
|
||||
pub fn remove_client(&self, client_id: LanguageServerId) {
|
||||
let _ = self.tx.send(Event::RemoveClient { client_id });
|
||||
}
|
||||
|
||||
async fn run(mut rx: mpsc::UnboundedReceiver<Event>) {
|
||||
let mut state: HashMap<usize, ClientState> = HashMap::new();
|
||||
let mut state: HashMap<LanguageServerId, ClientState> = HashMap::new();
|
||||
while let Some(event) = rx.recv().await {
|
||||
match event {
|
||||
Event::FileChanged { path } => {
|
||||
|
|
|
@ -17,6 +17,7 @@
|
|||
LanguageConfiguration, LanguageServerConfiguration, LanguageServerFeatures,
|
||||
};
|
||||
use helix_stdx::path;
|
||||
use slotmap::SlotMap;
|
||||
use tokio::sync::mpsc::UnboundedReceiver;
|
||||
|
||||
use std::{
|
||||
|
@ -28,8 +29,9 @@
|
|||
use thiserror::Error;
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
pub type Result<T> = core::result::Result<T, Error>;
|
||||
pub type Result<T, E = Error> = core::result::Result<T, E>;
|
||||
pub type LanguageServerName = String;
|
||||
pub use helix_core::diagnostic::LanguageServerId;
|
||||
|
||||
#[derive(Error, Debug)]
|
||||
pub enum Error {
|
||||
|
@ -651,38 +653,42 @@ pub fn parse(method: &str, params: jsonrpc::Params) -> Result<Notification> {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Registry {
|
||||
inner: HashMap<LanguageServerName, Vec<Arc<Client>>>,
|
||||
inner: SlotMap<LanguageServerId, Arc<Client>>,
|
||||
inner_by_name: HashMap<LanguageServerName, Vec<Arc<Client>>>,
|
||||
syn_loader: Arc<ArcSwap<helix_core::syntax::Loader>>,
|
||||
counter: usize,
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<(usize, Call)>>,
|
||||
pub incoming: SelectAll<UnboundedReceiverStream<(LanguageServerId, Call)>>,
|
||||
pub file_event_handler: file_event::Handler,
|
||||
}
|
||||
|
||||
impl Registry {
|
||||
pub fn new(syn_loader: Arc<ArcSwap<helix_core::syntax::Loader>>) -> Self {
|
||||
Self {
|
||||
inner: HashMap::new(),
|
||||
inner: SlotMap::with_key(),
|
||||
inner_by_name: HashMap::new(),
|
||||
syn_loader,
|
||||
counter: 0,
|
||||
incoming: SelectAll::new(),
|
||||
file_event_handler: file_event::Handler::new(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_by_id(&self, id: usize) -> Option<&Client> {
|
||||
self.inner
|
||||
.values()
|
||||
.flatten()
|
||||
.find(|client| client.id() == id)
|
||||
.map(|client| &**client)
|
||||
pub fn get_by_id(&self, id: LanguageServerId) -> Option<&Arc<Client>> {
|
||||
self.inner.get(id)
|
||||
}
|
||||
|
||||
pub fn remove_by_id(&mut self, id: usize) {
|
||||
pub fn remove_by_id(&mut self, id: LanguageServerId) {
|
||||
let Some(client) = self.inner.remove(id) else {
|
||||
log::error!("client was already removed");
|
||||
return
|
||||
};
|
||||
self.file_event_handler.remove_client(id);
|
||||
self.inner.retain(|_, language_servers| {
|
||||
language_servers.retain(|ls| id != ls.id());
|
||||
!language_servers.is_empty()
|
||||
});
|
||||
let instances = self
|
||||
.inner_by_name
|
||||
.get_mut(client.name())
|
||||
.expect("inner and inner_by_name must be synced");
|
||||
instances.retain(|ls| id != ls.id());
|
||||
if instances.is_empty() {
|
||||
self.inner_by_name.remove(client.name());
|
||||
}
|
||||
}
|
||||
|
||||
fn start_client(
|
||||
|
@ -692,28 +698,28 @@ fn start_client(
|
|||
doc_path: Option<&std::path::PathBuf>,
|
||||
root_dirs: &[PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> Result<Option<Arc<Client>>> {
|
||||
) -> Result<Arc<Client>, StartupError> {
|
||||
let syn_loader = self.syn_loader.load();
|
||||
let config = syn_loader
|
||||
.language_server_configs()
|
||||
.get(&name)
|
||||
.ok_or_else(|| anyhow::anyhow!("Language server '{name}' not defined"))?;
|
||||
let id = self.counter;
|
||||
self.counter += 1;
|
||||
if let Some(NewClient(client, incoming)) = start_client(
|
||||
id,
|
||||
name,
|
||||
ls_config,
|
||||
config,
|
||||
doc_path,
|
||||
root_dirs,
|
||||
enable_snippets,
|
||||
)? {
|
||||
self.incoming.push(UnboundedReceiverStream::new(incoming));
|
||||
Ok(Some(client))
|
||||
} else {
|
||||
Ok(None)
|
||||
}
|
||||
let id = self.inner.try_insert_with_key(|id| {
|
||||
start_client(
|
||||
id,
|
||||
name,
|
||||
ls_config,
|
||||
config,
|
||||
doc_path,
|
||||
root_dirs,
|
||||
enable_snippets,
|
||||
)
|
||||
.map(|client| {
|
||||
self.incoming.push(UnboundedReceiverStream::new(client.1));
|
||||
client.0
|
||||
})
|
||||
})?;
|
||||
Ok(self.inner[id].clone())
|
||||
}
|
||||
|
||||
/// If this method is called, all documents that have a reference to language servers used by the language config have to refresh their language servers,
|
||||
|
@ -730,7 +736,7 @@ pub fn restart(
|
|||
.language_servers
|
||||
.iter()
|
||||
.filter_map(|LanguageServerFeatures { name, .. }| {
|
||||
if self.inner.contains_key(name) {
|
||||
if self.inner_by_name.contains_key(name) {
|
||||
let client = match self.start_client(
|
||||
name.clone(),
|
||||
language_config,
|
||||
|
@ -738,16 +744,18 @@ pub fn restart(
|
|||
root_dirs,
|
||||
enable_snippets,
|
||||
) {
|
||||
Ok(client) => client?,
|
||||
Err(error) => return Some(Err(error)),
|
||||
Ok(client) => client,
|
||||
Err(StartupError::NoRequiredRootFound) => return None,
|
||||
Err(StartupError::Error(err)) => return Some(Err(err)),
|
||||
};
|
||||
let old_clients = self
|
||||
.inner
|
||||
.inner_by_name
|
||||
.insert(name.clone(), vec![client.clone()])
|
||||
.unwrap();
|
||||
|
||||
for old_client in old_clients {
|
||||
self.file_event_handler.remove_client(old_client.id());
|
||||
self.inner.remove(client.id());
|
||||
tokio::spawn(async move {
|
||||
let _ = old_client.force_shutdown().await;
|
||||
});
|
||||
|
@ -762,9 +770,10 @@ pub fn restart(
|
|||
}
|
||||
|
||||
pub fn stop(&mut self, name: &str) {
|
||||
if let Some(clients) = self.inner.remove(name) {
|
||||
if let Some(clients) = self.inner_by_name.remove(name) {
|
||||
for client in clients {
|
||||
self.file_event_handler.remove_client(client.id());
|
||||
self.inner.remove(client.id());
|
||||
tokio::spawn(async move {
|
||||
let _ = client.force_shutdown().await;
|
||||
});
|
||||
|
@ -781,7 +790,7 @@ pub fn get<'a>(
|
|||
) -> impl Iterator<Item = (LanguageServerName, Result<Arc<Client>>)> + 'a {
|
||||
language_config.language_servers.iter().filter_map(
|
||||
move |LanguageServerFeatures { name, .. }| {
|
||||
if let Some(clients) = self.inner.get(name) {
|
||||
if let Some(clients) = self.inner_by_name.get(name) {
|
||||
if let Some((_, client)) = clients.iter().enumerate().find(|(i, client)| {
|
||||
client.try_add_doc(&language_config.roots, root_dirs, doc_path, *i == 0)
|
||||
}) {
|
||||
|
@ -796,21 +805,21 @@ pub fn get<'a>(
|
|||
enable_snippets,
|
||||
) {
|
||||
Ok(client) => {
|
||||
let client = client?;
|
||||
self.inner
|
||||
self.inner_by_name
|
||||
.entry(name.to_owned())
|
||||
.or_default()
|
||||
.push(client.clone());
|
||||
Some((name.clone(), Ok(client)))
|
||||
}
|
||||
Err(err) => Some((name.to_owned(), Err(err))),
|
||||
Err(StartupError::NoRequiredRootFound) => None,
|
||||
Err(StartupError::Error(err)) => Some((name.to_owned(), Err(err))),
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
pub fn iter_clients(&self) -> impl Iterator<Item = &Arc<Client>> {
|
||||
self.inner.values().flatten()
|
||||
self.inner.values()
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -833,7 +842,7 @@ pub fn progress(&self) -> Option<&lsp::WorkDoneProgress> {
|
|||
/// Acts as a container for progress reported by language servers. Each server
|
||||
/// has a unique id assigned at creation through [`Registry`]. This id is then used
|
||||
/// to store the progress in this map.
|
||||
pub struct LspProgressMap(HashMap<usize, HashMap<lsp::ProgressToken, ProgressStatus>>);
|
||||
pub struct LspProgressMap(HashMap<LanguageServerId, HashMap<lsp::ProgressToken, ProgressStatus>>);
|
||||
|
||||
impl LspProgressMap {
|
||||
pub fn new() -> Self {
|
||||
|
@ -841,28 +850,35 @@ pub fn new() -> Self {
|
|||
}
|
||||
|
||||
/// Returns a map of all tokens corresponding to the language server with `id`.
|
||||
pub fn progress_map(&self, id: usize) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
|
||||
pub fn progress_map(
|
||||
&self,
|
||||
id: LanguageServerId,
|
||||
) -> Option<&HashMap<lsp::ProgressToken, ProgressStatus>> {
|
||||
self.0.get(&id)
|
||||
}
|
||||
|
||||
pub fn is_progressing(&self, id: usize) -> bool {
|
||||
pub fn is_progressing(&self, id: LanguageServerId) -> bool {
|
||||
self.0.get(&id).map(|it| !it.is_empty()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Returns last progress status for a given server with `id` and `token`.
|
||||
pub fn progress(&self, id: usize, token: &lsp::ProgressToken) -> Option<&ProgressStatus> {
|
||||
pub fn progress(
|
||||
&self,
|
||||
id: LanguageServerId,
|
||||
token: &lsp::ProgressToken,
|
||||
) -> Option<&ProgressStatus> {
|
||||
self.0.get(&id).and_then(|values| values.get(token))
|
||||
}
|
||||
|
||||
/// Checks if progress `token` for server with `id` is created.
|
||||
pub fn is_created(&mut self, id: usize, token: &lsp::ProgressToken) -> bool {
|
||||
pub fn is_created(&mut self, id: LanguageServerId, token: &lsp::ProgressToken) -> bool {
|
||||
self.0
|
||||
.get(&id)
|
||||
.map(|values| values.get(token).is_some())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn create(&mut self, id: usize, token: lsp::ProgressToken) {
|
||||
pub fn create(&mut self, id: LanguageServerId, token: lsp::ProgressToken) {
|
||||
self.0
|
||||
.entry(id)
|
||||
.or_default()
|
||||
|
@ -872,7 +888,7 @@ pub fn create(&mut self, id: usize, token: lsp::ProgressToken) {
|
|||
/// Ends the progress by removing the `token` from server with `id`, if removed returns the value.
|
||||
pub fn end_progress(
|
||||
&mut self,
|
||||
id: usize,
|
||||
id: LanguageServerId,
|
||||
token: &lsp::ProgressToken,
|
||||
) -> Option<ProgressStatus> {
|
||||
self.0.get_mut(&id).and_then(|vals| vals.remove(token))
|
||||
|
@ -881,7 +897,7 @@ pub fn end_progress(
|
|||
/// Updates the progress of `token` for server with `id` to `status`, returns the value replaced or `None`.
|
||||
pub fn update(
|
||||
&mut self,
|
||||
id: usize,
|
||||
id: LanguageServerId,
|
||||
token: lsp::ProgressToken,
|
||||
status: lsp::WorkDoneProgress,
|
||||
) -> Option<ProgressStatus> {
|
||||
|
@ -892,19 +908,30 @@ pub fn update(
|
|||
}
|
||||
}
|
||||
|
||||
struct NewClient(Arc<Client>, UnboundedReceiver<(usize, Call)>);
|
||||
struct NewClient(Arc<Client>, UnboundedReceiver<(LanguageServerId, Call)>);
|
||||
|
||||
enum StartupError {
|
||||
NoRequiredRootFound,
|
||||
Error(Error),
|
||||
}
|
||||
|
||||
impl<T: Into<Error>> From<T> for StartupError {
|
||||
fn from(value: T) -> Self {
|
||||
StartupError::Error(value.into())
|
||||
}
|
||||
}
|
||||
|
||||
/// start_client takes both a LanguageConfiguration and a LanguageServerConfiguration to ensure that
|
||||
/// it is only called when it makes sense.
|
||||
fn start_client(
|
||||
id: usize,
|
||||
id: LanguageServerId,
|
||||
name: String,
|
||||
config: &LanguageConfiguration,
|
||||
ls_config: &LanguageServerConfiguration,
|
||||
doc_path: Option<&std::path::PathBuf>,
|
||||
root_dirs: &[PathBuf],
|
||||
enable_snippets: bool,
|
||||
) -> Result<Option<NewClient>> {
|
||||
) -> Result<NewClient, StartupError> {
|
||||
let (workspace, workspace_is_cwd) = helix_loader::find_workspace();
|
||||
let workspace = path::normalize(workspace);
|
||||
let root = find_lsp_workspace(
|
||||
|
@ -929,7 +956,7 @@ fn start_client(
|
|||
.map(|entry| entry.file_name())
|
||||
.any(|entry| globset.is_match(entry))
|
||||
{
|
||||
return Ok(None);
|
||||
return Err(StartupError::NoRequiredRootFound);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -981,7 +1008,7 @@ fn start_client(
|
|||
initialize_notify.notify_one();
|
||||
});
|
||||
|
||||
Ok(Some(NewClient(client, incoming)))
|
||||
Ok(NewClient(client, incoming))
|
||||
}
|
||||
|
||||
/// Find an LSP workspace of a file using the following mechanism:
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
use crate::{jsonrpc, Error, Result};
|
||||
use crate::{jsonrpc, Error, LanguageServerId, Result};
|
||||
use anyhow::Context;
|
||||
use log::{error, info};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
@ -37,7 +37,7 @@ enum ServerMessage {
|
|||
|
||||
#[derive(Debug)]
|
||||
pub struct Transport {
|
||||
id: usize,
|
||||
id: LanguageServerId,
|
||||
name: String,
|
||||
pending_requests: Mutex<HashMap<jsonrpc::Id, Sender<Result<Value>>>>,
|
||||
}
|
||||
|
@ -47,10 +47,10 @@ pub fn start(
|
|||
server_stdout: BufReader<ChildStdout>,
|
||||
server_stdin: BufWriter<ChildStdin>,
|
||||
server_stderr: BufReader<ChildStderr>,
|
||||
id: usize,
|
||||
id: LanguageServerId,
|
||||
name: String,
|
||||
) -> (
|
||||
UnboundedReceiver<(usize, jsonrpc::Call)>,
|
||||
UnboundedReceiver<(LanguageServerId, jsonrpc::Call)>,
|
||||
UnboundedSender<Payload>,
|
||||
Arc<Notify>,
|
||||
) {
|
||||
|
@ -194,7 +194,7 @@ async fn send_string_to_server(
|
|||
|
||||
async fn process_server_message(
|
||||
&self,
|
||||
client_tx: &UnboundedSender<(usize, jsonrpc::Call)>,
|
||||
client_tx: &UnboundedSender<(LanguageServerId, jsonrpc::Call)>,
|
||||
msg: ServerMessage,
|
||||
language_server_name: &str,
|
||||
) -> Result<()> {
|
||||
|
@ -251,7 +251,7 @@ async fn process_request_response(
|
|||
async fn recv(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdout: BufReader<ChildStdout>,
|
||||
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
|
||||
client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>,
|
||||
) {
|
||||
let mut recv_buffer = String::new();
|
||||
loop {
|
||||
|
@ -329,7 +329,7 @@ async fn err(transport: Arc<Self>, mut server_stderr: BufReader<ChildStderr>) {
|
|||
async fn send(
|
||||
transport: Arc<Self>,
|
||||
mut server_stdin: BufWriter<ChildStdin>,
|
||||
client_tx: UnboundedSender<(usize, jsonrpc::Call)>,
|
||||
client_tx: UnboundedSender<(LanguageServerId, jsonrpc::Call)>,
|
||||
mut client_rx: UnboundedReceiver<Payload>,
|
||||
initialize_notify: Arc<Notify>,
|
||||
) {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
use helix_lsp::{
|
||||
lsp::{self, notification::Notification},
|
||||
util::lsp_range_to_range,
|
||||
LspProgressMap,
|
||||
LanguageServerId, LspProgressMap,
|
||||
};
|
||||
use helix_stdx::path::get_relative_path;
|
||||
use helix_view::{
|
||||
|
@ -655,7 +655,7 @@ pub async fn handle_terminal_events(&mut self, event: std::io::Result<CrosstermE
|
|||
pub async fn handle_language_server_message(
|
||||
&mut self,
|
||||
call: helix_lsp::Call,
|
||||
server_id: usize,
|
||||
server_id: LanguageServerId,
|
||||
) {
|
||||
use helix_lsp::{Call, MethodCall, Notification};
|
||||
|
||||
|
@ -1030,12 +1030,7 @@ macro_rules! language_server {
|
|||
Ok(json!(result))
|
||||
}
|
||||
Ok(MethodCall::RegisterCapability(params)) => {
|
||||
if let Some(client) = self
|
||||
.editor
|
||||
.language_servers
|
||||
.iter_clients()
|
||||
.find(|client| client.id() == server_id)
|
||||
{
|
||||
if let Some(client) = self.editor.language_servers.get_by_id(server_id) {
|
||||
for reg in params.registrations {
|
||||
match reg.method.as_str() {
|
||||
lsp::notification::DidChangeWatchedFiles::METHOD => {
|
||||
|
|
|
@ -799,28 +799,29 @@ fn goto_line_start(cx: &mut Context) {
|
|||
}
|
||||
|
||||
fn goto_next_buffer(cx: &mut Context) {
|
||||
goto_buffer(cx.editor, Direction::Forward);
|
||||
goto_buffer(cx.editor, Direction::Forward, cx.count());
|
||||
}
|
||||
|
||||
fn goto_previous_buffer(cx: &mut Context) {
|
||||
goto_buffer(cx.editor, Direction::Backward);
|
||||
goto_buffer(cx.editor, Direction::Backward, cx.count());
|
||||
}
|
||||
|
||||
fn goto_buffer(editor: &mut Editor, direction: Direction) {
|
||||
fn goto_buffer(editor: &mut Editor, direction: Direction, count: usize) {
|
||||
let current = view!(editor).doc;
|
||||
|
||||
let id = match direction {
|
||||
Direction::Forward => {
|
||||
let iter = editor.documents.keys();
|
||||
let mut iter = iter.skip_while(|id| *id != ¤t);
|
||||
iter.next(); // skip current item
|
||||
iter.next().or_else(|| editor.documents.keys().next())
|
||||
// skip 'count' times past current buffer
|
||||
iter.cycle().skip_while(|id| *id != ¤t).nth(count)
|
||||
}
|
||||
Direction::Backward => {
|
||||
let iter = editor.documents.keys();
|
||||
let mut iter = iter.rev().skip_while(|id| *id != ¤t);
|
||||
iter.next(); // skip current item
|
||||
iter.next().or_else(|| editor.documents.keys().next_back())
|
||||
// skip 'count' times past current buffer
|
||||
iter.rev()
|
||||
.cycle()
|
||||
.skip_while(|id| *id != ¤t)
|
||||
.nth(count)
|
||||
}
|
||||
}
|
||||
.unwrap();
|
||||
|
@ -2079,6 +2080,11 @@ fn searcher(cx: &mut Context, direction: Direction) {
|
|||
let config = cx.editor.config();
|
||||
let scrolloff = config.scrolloff;
|
||||
let wrap_around = config.search.wrap_around;
|
||||
let movement = if cx.editor.mode() == Mode::Select {
|
||||
Movement::Extend
|
||||
} else {
|
||||
Movement::Move
|
||||
};
|
||||
|
||||
// TODO: could probably share with select_on_matches?
|
||||
let completions = search_completions(cx, Some(reg));
|
||||
|
@ -2103,7 +2109,7 @@ fn searcher(cx: &mut Context, direction: Direction) {
|
|||
search_impl(
|
||||
cx.editor,
|
||||
®ex,
|
||||
Movement::Move,
|
||||
movement,
|
||||
direction,
|
||||
scrolloff,
|
||||
wrap_around,
|
||||
|
@ -3446,33 +3452,35 @@ fn push_jump(view: &mut View, doc: &Document) {
|
|||
}
|
||||
|
||||
fn goto_line(cx: &mut Context) {
|
||||
if cx.count.is_some() {
|
||||
let (view, doc) = current!(cx.editor);
|
||||
push_jump(view, doc);
|
||||
let (view, doc) = current!(cx.editor);
|
||||
push_jump(view, doc);
|
||||
|
||||
goto_line_without_jumplist(cx.editor, cx.count);
|
||||
}
|
||||
goto_line_without_jumplist(cx.editor, cx.count);
|
||||
}
|
||||
|
||||
fn goto_line_without_jumplist(editor: &mut Editor, count: Option<NonZeroUsize>) {
|
||||
if let Some(count) = count {
|
||||
let (view, doc) = current!(editor);
|
||||
let text = doc.text().slice(..);
|
||||
let max_line = if text.line(text.len_lines() - 1).len_chars() == 0 {
|
||||
// If the last line is blank, don't jump to it.
|
||||
text.len_lines().saturating_sub(2)
|
||||
} else {
|
||||
text.len_lines() - 1
|
||||
};
|
||||
let line_idx = std::cmp::min(count.get() - 1, max_line);
|
||||
let pos = text.line_to_char(line_idx);
|
||||
let selection = doc
|
||||
.selection(view.id)
|
||||
.clone()
|
||||
.transform(|range| range.put_cursor(text, pos, editor.mode == Mode::Select));
|
||||
let (view, doc) = current!(editor);
|
||||
let text = doc.text().slice(..);
|
||||
let max_line = if text.line(text.len_lines() - 1).len_chars() == 0 {
|
||||
// If the last line is blank, don't jump to it.
|
||||
text.len_lines().saturating_sub(2)
|
||||
} else {
|
||||
text.len_lines() - 1
|
||||
};
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
let line_idx = if let Some(count) = count {
|
||||
std::cmp::min(count.get() - 1, max_line)
|
||||
} else {
|
||||
max_line
|
||||
};
|
||||
|
||||
let pos = text.line_to_char(line_idx);
|
||||
let selection = doc
|
||||
.selection(view.id)
|
||||
.clone()
|
||||
.transform(|range| range.put_cursor(text, pos, editor.mode == Mode::Select));
|
||||
|
||||
doc.set_selection(view.id, selection);
|
||||
}
|
||||
|
||||
fn goto_last_line(cx: &mut Context) {
|
||||
|
@ -5403,13 +5411,22 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
|
|||
'e' => textobject_treesitter("entry", range),
|
||||
'p' => textobject::textobject_paragraph(text, range, objtype, count),
|
||||
'm' => textobject::textobject_pair_surround_closest(
|
||||
text, range, objtype, count,
|
||||
doc.syntax(),
|
||||
text,
|
||||
range,
|
||||
objtype,
|
||||
count,
|
||||
),
|
||||
'g' => textobject_change(range),
|
||||
// TODO: cancel new ranges if inconsistent surround matches across lines
|
||||
ch if !ch.is_ascii_alphanumeric() => {
|
||||
textobject::textobject_pair_surround(text, range, objtype, ch, count)
|
||||
}
|
||||
ch if !ch.is_ascii_alphanumeric() => textobject::textobject_pair_surround(
|
||||
doc.syntax(),
|
||||
text,
|
||||
range,
|
||||
objtype,
|
||||
ch,
|
||||
count,
|
||||
),
|
||||
_ => range,
|
||||
}
|
||||
});
|
||||
|
@ -5434,7 +5451,8 @@ fn select_textobject(cx: &mut Context, objtype: textobject::TextObject) {
|
|||
("c", "Comment (tree-sitter)"),
|
||||
("T", "Test (tree-sitter)"),
|
||||
("e", "Data structure entry (tree-sitter)"),
|
||||
("m", "Closest surrounding pair"),
|
||||
("m", "Closest surrounding pair (tree-sitter)"),
|
||||
("g", "Change"),
|
||||
(" ", "... or any character acting as a pair"),
|
||||
];
|
||||
|
||||
|
@ -5447,7 +5465,7 @@ fn surround_add(cx: &mut Context) {
|
|||
// surround_len is the number of new characters being added.
|
||||
let (open, close, surround_len) = match event.char() {
|
||||
Some(ch) => {
|
||||
let (o, c) = surround::get_pair(ch);
|
||||
let (o, c) = match_brackets::get_pair(ch);
|
||||
let mut open = Tendril::new();
|
||||
open.push(o);
|
||||
let mut close = Tendril::new();
|
||||
|
@ -5498,13 +5516,14 @@ fn surround_replace(cx: &mut Context) {
|
|||
let text = doc.text().slice(..);
|
||||
let selection = doc.selection(view.id);
|
||||
|
||||
let change_pos = match surround::get_surround_pos(text, selection, surround_ch, count) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
cx.editor.set_error(err.to_string());
|
||||
return;
|
||||
}
|
||||
};
|
||||
let change_pos =
|
||||
match surround::get_surround_pos(doc.syntax(), text, selection, surround_ch, count) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
cx.editor.set_error(err.to_string());
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
let selection = selection.clone();
|
||||
let ranges: SmallVec<[Range; 1]> = change_pos.iter().map(|&p| Range::point(p)).collect();
|
||||
|
@ -5519,7 +5538,7 @@ fn surround_replace(cx: &mut Context) {
|
|||
Some(to) => to,
|
||||
None => return doc.set_selection(view.id, selection),
|
||||
};
|
||||
let (open, close) = surround::get_pair(to);
|
||||
let (open, close) = match_brackets::get_pair(to);
|
||||
|
||||
// the changeset has to be sorted to allow nested surrounds
|
||||
let mut sorted_pos: Vec<(usize, char)> = Vec::new();
|
||||
|
@ -5556,13 +5575,14 @@ fn surround_delete(cx: &mut Context) {
|
|||
let text = doc.text().slice(..);
|
||||
let selection = doc.selection(view.id);
|
||||
|
||||
let mut change_pos = match surround::get_surround_pos(text, selection, surround_ch, count) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
cx.editor.set_error(err.to_string());
|
||||
return;
|
||||
}
|
||||
};
|
||||
let mut change_pos =
|
||||
match surround::get_surround_pos(doc.syntax(), text, selection, surround_ch, count) {
|
||||
Ok(c) => c,
|
||||
Err(err) => {
|
||||
cx.editor.set_error(err.to_string());
|
||||
return;
|
||||
}
|
||||
};
|
||||
change_pos.sort_unstable(); // the changeset has to be sorted to allow nested surrounds
|
||||
let transaction =
|
||||
Transaction::change(doc.text(), change_pos.into_iter().map(|p| (p, p + 1, None)));
|
||||
|
|
|
@ -6,7 +6,7 @@
|
|||
NumberOrString,
|
||||
},
|
||||
util::{diagnostic_to_lsp_diagnostic, lsp_range_to_range, range_to_lsp_range},
|
||||
Client, OffsetEncoding,
|
||||
Client, LanguageServerId, OffsetEncoding,
|
||||
};
|
||||
use tokio_stream::StreamExt;
|
||||
use tui::{
|
||||
|
@ -266,7 +266,7 @@ enum DiagnosticsFormat {
|
|||
|
||||
fn diag_picker(
|
||||
cx: &Context,
|
||||
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, usize)>>,
|
||||
diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
format: DiagnosticsFormat,
|
||||
) -> Picker<PickerDiagnostic> {
|
||||
// TODO: drop current_path comparison and instead use workspace: bool flag?
|
||||
|
@ -497,7 +497,7 @@ pub fn workspace_diagnostics_picker(cx: &mut Context) {
|
|||
|
||||
struct CodeActionOrCommandItem {
|
||||
lsp_item: lsp::CodeActionOrCommand,
|
||||
language_server_id: usize,
|
||||
language_server_id: LanguageServerId,
|
||||
}
|
||||
|
||||
impl ui::menu::Item for CodeActionOrCommandItem {
|
||||
|
@ -757,7 +757,11 @@ fn format(&self, _data: &Self::Data) -> Row {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn execute_lsp_command(editor: &mut Editor, language_server_id: usize, cmd: lsp::Command) {
|
||||
pub fn execute_lsp_command(
|
||||
editor: &mut Editor,
|
||||
language_server_id: LanguageServerId,
|
||||
cmd: lsp::Command,
|
||||
) {
|
||||
// the command is executed on the server and communicated back
|
||||
// to the client asynchronously using workspace edits
|
||||
let future = match editor
|
||||
|
@ -1034,7 +1038,7 @@ fn get_prefill_from_lsp_response(
|
|||
fn create_rename_prompt(
|
||||
editor: &Editor,
|
||||
prefill: String,
|
||||
language_server_id: Option<usize>,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
) -> Box<ui::Prompt> {
|
||||
let prompt = ui::Prompt::new(
|
||||
"rename-to:".into(),
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use std::fmt::Write;
|
||||
use std::io::BufReader;
|
||||
use std::ops::Deref;
|
||||
|
||||
use crate::job::Job;
|
||||
|
@ -8,7 +9,7 @@
|
|||
use helix_core::fuzzy::fuzzy_match;
|
||||
use helix_core::indent::MAX_INDENT;
|
||||
use helix_core::{line_ending, shellwords::Shellwords};
|
||||
use helix_view::document::DEFAULT_LANGUAGE_NAME;
|
||||
use helix_view::document::{read_to_string, DEFAULT_LANGUAGE_NAME};
|
||||
use helix_view::editor::{CloseError, ConfigEvent};
|
||||
use serde_json::Value;
|
||||
use ui::completers::{self, Completer};
|
||||
|
@ -309,7 +310,7 @@ fn buffer_next(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
goto_buffer(cx.editor, Direction::Forward);
|
||||
goto_buffer(cx.editor, Direction::Forward, 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -322,7 +323,7 @@ fn buffer_previous(
|
|||
return Ok(());
|
||||
}
|
||||
|
||||
goto_buffer(cx.editor, Direction::Backward);
|
||||
goto_buffer(cx.editor, Direction::Backward, 1);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -2454,6 +2455,39 @@ fn yank_diagnostic(
|
|||
Ok(())
|
||||
}
|
||||
|
||||
fn read(cx: &mut compositor::Context, args: &[Cow<str>], event: PromptEvent) -> anyhow::Result<()> {
|
||||
if event != PromptEvent::Validate {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let scrolloff = cx.editor.config().scrolloff;
|
||||
let (view, doc) = current!(cx.editor);
|
||||
|
||||
ensure!(!args.is_empty(), "file name is expected");
|
||||
ensure!(args.len() == 1, "only the file name is expected");
|
||||
|
||||
let filename = args.get(0).unwrap();
|
||||
let path = PathBuf::from(filename.to_string());
|
||||
ensure!(
|
||||
path.exists() && path.is_file(),
|
||||
"path is not a file: {:?}",
|
||||
path
|
||||
);
|
||||
|
||||
let file = std::fs::File::open(path).map_err(|err| anyhow!("error opening file: {}", err))?;
|
||||
let mut reader = BufReader::new(file);
|
||||
let (contents, _, _) = read_to_string(&mut reader, Some(doc.encoding()))
|
||||
.map_err(|err| anyhow!("error reading file: {}", err))?;
|
||||
let contents = Tendril::from(contents);
|
||||
let selection = doc.selection(view.id);
|
||||
let transaction = Transaction::insert(doc.text(), selection, contents);
|
||||
doc.apply(&transaction, view.id);
|
||||
doc.append_changes_to_history(view);
|
||||
view.ensure_cursor_in_view(doc, scrolloff);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub const TYPABLE_COMMAND_LIST: &[TypableCommand] = &[
|
||||
TypableCommand {
|
||||
name: "quit",
|
||||
|
@ -3068,6 +3102,13 @@ fn yank_diagnostic(
|
|||
fun: yank_diagnostic,
|
||||
signature: CommandSignature::all(completers::register),
|
||||
},
|
||||
TypableCommand {
|
||||
name: "read",
|
||||
aliases: &["r"],
|
||||
doc: "Load a file into buffer",
|
||||
fun: read,
|
||||
signature: CommandSignature::positional(&[completers::filename]),
|
||||
},
|
||||
];
|
||||
|
||||
pub static TYPABLE_COMMAND_MAP: Lazy<HashMap<&'static str, &'static TypableCommand>> =
|
||||
|
|
|
@ -11,7 +11,7 @@
|
|||
pub use completion::trigger_auto_completion;
|
||||
pub use helix_view::handlers::Handlers;
|
||||
|
||||
mod completion;
|
||||
pub mod completion;
|
||||
mod signature_help;
|
||||
|
||||
pub fn setup(config: Arc<ArcSwap<Config>>) -> Handlers {
|
||||
|
|
|
@ -30,6 +30,8 @@
|
|||
use crate::ui::{self, CompletionItem, Popup};
|
||||
|
||||
use super::Handlers;
|
||||
pub use resolve::ResolveHandler;
|
||||
mod resolve;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, Clone, Copy)]
|
||||
enum TriggerKind {
|
||||
|
@ -251,7 +253,7 @@ fn request_completion(
|
|||
.into_iter()
|
||||
.map(|item| CompletionItem {
|
||||
item,
|
||||
language_server_id,
|
||||
provider: language_server_id,
|
||||
resolved: false,
|
||||
})
|
||||
.collect();
|
||||
|
|
|
@ -0,0 +1,153 @@
|
|||
use std::sync::Arc;
|
||||
|
||||
use helix_lsp::lsp;
|
||||
use tokio::sync::mpsc::Sender;
|
||||
use tokio::time::{Duration, Instant};
|
||||
|
||||
use helix_event::{send_blocking, AsyncHook, CancelRx};
|
||||
use helix_view::Editor;
|
||||
|
||||
use crate::handlers::completion::CompletionItem;
|
||||
use crate::job;
|
||||
|
||||
/// A hook for resolving incomplete completion items.
|
||||
///
|
||||
/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion):
|
||||
///
|
||||
/// > If computing full completion items is expensive, servers can additionally provide a
|
||||
/// > handler for the completion item resolve request. ...
|
||||
/// > A typical use case is for example: the `textDocument/completion` request doesn't fill
|
||||
/// > in the `documentation` property for returned completion items since it is expensive
|
||||
/// > to compute. When the item is selected in the user interface then a
|
||||
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
|
||||
/// > The returned completion item should have the documentation property filled in.
|
||||
pub struct ResolveHandler {
|
||||
last_request: Option<Arc<CompletionItem>>,
|
||||
resolver: Sender<ResolveRequest>,
|
||||
}
|
||||
|
||||
impl ResolveHandler {
|
||||
pub fn new() -> ResolveHandler {
|
||||
ResolveHandler {
|
||||
last_request: None,
|
||||
resolver: ResolveTimeout {
|
||||
next_request: None,
|
||||
in_flight: None,
|
||||
}
|
||||
.spawn(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn ensure_item_resolved(&mut self, editor: &mut Editor, item: &mut CompletionItem) {
|
||||
if item.resolved {
|
||||
return;
|
||||
}
|
||||
let needs_resolve = item.item.documentation.is_none()
|
||||
|| item.item.detail.is_none()
|
||||
|| item.item.additional_text_edits.is_none();
|
||||
if !needs_resolve {
|
||||
item.resolved = true;
|
||||
return;
|
||||
}
|
||||
if self.last_request.as_deref().is_some_and(|it| it == item) {
|
||||
return;
|
||||
}
|
||||
let Some(ls) = editor.language_servers.get_by_id(item.provider).cloned() else {
|
||||
item.resolved = true;
|
||||
return;
|
||||
};
|
||||
if matches!(
|
||||
ls.capabilities().completion_provider,
|
||||
Some(lsp::CompletionOptions {
|
||||
resolve_provider: Some(true),
|
||||
..
|
||||
})
|
||||
) {
|
||||
let item = Arc::new(item.clone());
|
||||
self.last_request = Some(item.clone());
|
||||
send_blocking(&self.resolver, ResolveRequest { item, ls })
|
||||
} else {
|
||||
item.resolved = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ResolveRequest {
|
||||
item: Arc<CompletionItem>,
|
||||
ls: Arc<helix_lsp::Client>,
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct ResolveTimeout {
|
||||
next_request: Option<ResolveRequest>,
|
||||
in_flight: Option<(helix_event::CancelTx, Arc<CompletionItem>)>,
|
||||
}
|
||||
|
||||
impl AsyncHook for ResolveTimeout {
|
||||
type Event = ResolveRequest;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
request: Self::Event,
|
||||
timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<tokio::time::Instant> {
|
||||
if self
|
||||
.next_request
|
||||
.as_ref()
|
||||
.is_some_and(|old_request| old_request.item == request.item)
|
||||
{
|
||||
timeout
|
||||
} else if self
|
||||
.in_flight
|
||||
.as_ref()
|
||||
.is_some_and(|(_, old_request)| old_request.item == request.item.item)
|
||||
{
|
||||
self.next_request = None;
|
||||
None
|
||||
} else {
|
||||
self.next_request = Some(request);
|
||||
Some(Instant::now() + Duration::from_millis(150))
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let Some(request) = self.next_request.take() else { return };
|
||||
let (tx, rx) = helix_event::cancelation();
|
||||
self.in_flight = Some((tx, request.item.clone()));
|
||||
tokio::spawn(request.execute(rx));
|
||||
}
|
||||
}
|
||||
|
||||
impl ResolveRequest {
|
||||
async fn execute(self, cancel: CancelRx) {
|
||||
let future = self.ls.resolve_completion_item(&self.item.item);
|
||||
let Some(resolved_item) = helix_event::cancelable_future(future, cancel).await else {
|
||||
return;
|
||||
};
|
||||
job::dispatch(move |_, compositor| {
|
||||
if let Some(completion) = &mut compositor
|
||||
.find::<crate::ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
{
|
||||
let resolved_item = match resolved_item {
|
||||
Ok(item) => CompletionItem {
|
||||
item,
|
||||
resolved: true,
|
||||
..*self.item
|
||||
},
|
||||
Err(err) => {
|
||||
log::error!("completion resolve request failed: {err}");
|
||||
// set item to resolved so we don't request it again
|
||||
// we could also remove it but that oculd be odd ui
|
||||
let mut item = (*self.item).clone();
|
||||
item.resolved = true;
|
||||
item
|
||||
}
|
||||
};
|
||||
completion.replace_item(&self.item, resolved_item);
|
||||
};
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
|
@ -5,7 +5,7 @@
|
|||
use helix_event::{
|
||||
cancelable_future, cancelation, register_hook, send_blocking, CancelRx, CancelTx,
|
||||
};
|
||||
use helix_lsp::lsp;
|
||||
use helix_lsp::lsp::{self, SignatureInformation};
|
||||
use helix_stdx::rope::RopeSliceExt;
|
||||
use helix_view::document::Mode;
|
||||
use helix_view::events::{DocumentDidChange, SelectionDidChange};
|
||||
|
@ -18,7 +18,7 @@
|
|||
use crate::compositor::Compositor;
|
||||
use crate::events::{OnModeSwitch, PostInsertChar};
|
||||
use crate::handlers::Handlers;
|
||||
use crate::ui::lsp::SignatureHelp;
|
||||
use crate::ui::lsp::{Signature, SignatureHelp};
|
||||
use crate::ui::Popup;
|
||||
use crate::{job, ui};
|
||||
|
||||
|
@ -82,6 +82,7 @@ fn handle_event(
|
|||
}
|
||||
}
|
||||
self.state = if open { State::Open } else { State::Closed };
|
||||
|
||||
return timeout;
|
||||
}
|
||||
}
|
||||
|
@ -138,6 +139,31 @@ pub fn request_signature_help(
|
|||
});
|
||||
}
|
||||
|
||||
fn active_param_range(
|
||||
signature: &SignatureInformation,
|
||||
response_active_parameter: Option<u32>,
|
||||
) -> Option<(usize, usize)> {
|
||||
let param_idx = signature
|
||||
.active_parameter
|
||||
.or(response_active_parameter)
|
||||
.unwrap_or(0) as usize;
|
||||
let param = signature.parameters.as_ref()?.get(param_idx)?;
|
||||
match ¶m.label {
|
||||
lsp::ParameterLabel::Simple(string) => {
|
||||
let start = signature.label.find(string.as_str())?;
|
||||
Some((start, start + string.len()))
|
||||
}
|
||||
lsp::ParameterLabel::LabelOffsets([start, end]) => {
|
||||
// LS sends offsets based on utf-16 based string representation
|
||||
// but highlighting in helix is done using byte offset.
|
||||
use helix_core::str_utils::char_to_byte_idx;
|
||||
let from = char_to_byte_idx(&signature.label, *start as usize);
|
||||
let to = char_to_byte_idx(&signature.label, *end as usize);
|
||||
Some((from, to))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_signature_help(
|
||||
editor: &mut Editor,
|
||||
compositor: &mut Compositor,
|
||||
|
@ -184,54 +210,50 @@ pub fn show_signature_help(
|
|||
let doc = doc!(editor);
|
||||
let language = doc.language_name().unwrap_or("");
|
||||
|
||||
let signature = match response
|
||||
if response.signatures.is_empty() {
|
||||
return;
|
||||
}
|
||||
|
||||
let signatures: Vec<Signature> = response
|
||||
.signatures
|
||||
.get(response.active_signature.unwrap_or(0) as usize)
|
||||
{
|
||||
Some(s) => s,
|
||||
None => return,
|
||||
};
|
||||
let mut contents = SignatureHelp::new(
|
||||
signature.label.clone(),
|
||||
language.to_string(),
|
||||
Arc::clone(&editor.syn_loader),
|
||||
);
|
||||
.into_iter()
|
||||
.map(|s| {
|
||||
let active_param_range = active_param_range(&s, response.active_parameter);
|
||||
|
||||
let signature_doc = if config.lsp.display_signature_help_docs {
|
||||
signature.documentation.as_ref().map(|doc| match doc {
|
||||
lsp::Documentation::String(s) => s.clone(),
|
||||
lsp::Documentation::MarkupContent(markup) => markup.value.clone(),
|
||||
let signature_doc = if config.lsp.display_signature_help_docs {
|
||||
s.documentation.map(|doc| match doc {
|
||||
lsp::Documentation::String(s) => s,
|
||||
lsp::Documentation::MarkupContent(markup) => markup.value,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Signature {
|
||||
signature: s.label,
|
||||
signature_doc,
|
||||
active_param_range,
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
contents.set_signature_doc(signature_doc);
|
||||
|
||||
let active_param_range = || -> Option<(usize, usize)> {
|
||||
let param_idx = signature
|
||||
.active_parameter
|
||||
.or(response.active_parameter)
|
||||
.unwrap_or(0) as usize;
|
||||
let param = signature.parameters.as_ref()?.get(param_idx)?;
|
||||
match ¶m.label {
|
||||
lsp::ParameterLabel::Simple(string) => {
|
||||
let start = signature.label.find(string.as_str())?;
|
||||
Some((start, start + string.len()))
|
||||
}
|
||||
lsp::ParameterLabel::LabelOffsets([start, end]) => {
|
||||
// LS sends offsets based on utf-16 based string representation
|
||||
// but highlighting in helix is done using byte offset.
|
||||
use helix_core::str_utils::char_to_byte_idx;
|
||||
let from = char_to_byte_idx(&signature.label, *start as usize);
|
||||
let to = char_to_byte_idx(&signature.label, *end as usize);
|
||||
Some((from, to))
|
||||
}
|
||||
}
|
||||
};
|
||||
contents.set_active_param_range(active_param_range());
|
||||
.collect();
|
||||
|
||||
let old_popup = compositor.find_id::<Popup<SignatureHelp>>(SignatureHelp::ID);
|
||||
let mut active_signature = old_popup
|
||||
.as_ref()
|
||||
.map(|popup| popup.contents().active_signature())
|
||||
.unwrap_or_else(|| response.active_signature.unwrap_or_default() as usize);
|
||||
|
||||
if active_signature >= signatures.len() {
|
||||
active_signature = signatures.len() - 1;
|
||||
}
|
||||
|
||||
let contents = SignatureHelp::new(
|
||||
language.to_string(),
|
||||
Arc::clone(&editor.syn_loader),
|
||||
active_signature,
|
||||
signatures,
|
||||
);
|
||||
|
||||
let mut popup = Popup::new(SignatureHelp::ID, contents)
|
||||
.position(old_popup.and_then(|p| p.get_position()))
|
||||
.position_bias(Open::Above)
|
||||
|
|
|
@ -51,7 +51,7 @@ fn filter_picker_entry(entry: &DirEntry, root: &Path, dedup_symlinks: bool) -> b
|
|||
// in our picker.
|
||||
if matches!(
|
||||
entry.file_name().to_str(),
|
||||
Some(".git" | ".pijul" | ".jj" | ".hg")
|
||||
Some(".git" | ".pijul" | ".jj" | ".hg" | ".svn")
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
use crate::{
|
||||
compositor::{Component, Context, Event, EventResult},
|
||||
handlers::trigger_auto_completion,
|
||||
job,
|
||||
handlers::{completion::ResolveHandler, trigger_auto_completion},
|
||||
};
|
||||
use helix_event::AsyncHook;
|
||||
use helix_view::{
|
||||
document::SavePoint,
|
||||
editor::CompleteAction,
|
||||
|
@ -12,17 +10,16 @@
|
|||
theme::{Modifier, Style},
|
||||
ViewId,
|
||||
};
|
||||
use tokio::time::Instant;
|
||||
use tui::{buffer::Buffer as Surface, text::Span};
|
||||
|
||||
use std::{borrow::Cow, sync::Arc, time::Duration};
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
|
||||
use helix_core::{chars, Change, Transaction};
|
||||
use helix_view::{graphics::Rect, Document, Editor};
|
||||
|
||||
use crate::ui::{menu, Markdown, Menu, Popup, PromptEvent};
|
||||
|
||||
use helix_lsp::{lsp, util, OffsetEncoding};
|
||||
use helix_lsp::{lsp, util, LanguageServerId, OffsetEncoding};
|
||||
|
||||
impl menu::Item for CompletionItem {
|
||||
type Data = ();
|
||||
|
@ -94,7 +91,7 @@ fn format(&self, _data: &Self::Data) -> menu::Row {
|
|||
#[derive(Debug, PartialEq, Default, Clone)]
|
||||
pub struct CompletionItem {
|
||||
pub item: lsp::CompletionItem,
|
||||
pub language_server_id: usize,
|
||||
pub provider: LanguageServerId,
|
||||
pub resolved: bool,
|
||||
}
|
||||
|
||||
|
@ -104,7 +101,7 @@ pub struct Completion {
|
|||
#[allow(dead_code)]
|
||||
trigger_offset: usize,
|
||||
filter: String,
|
||||
resolve_handler: tokio::sync::mpsc::Sender<CompletionItem>,
|
||||
resolve_handler: ResolveHandler,
|
||||
}
|
||||
|
||||
impl Completion {
|
||||
|
@ -224,7 +221,7 @@ macro_rules! language_server {
|
|||
($item:expr) => {
|
||||
match editor
|
||||
.language_servers
|
||||
.get_by_id($item.language_server_id)
|
||||
.get_by_id($item.provider)
|
||||
{
|
||||
Some(ls) => ls,
|
||||
None => {
|
||||
|
@ -285,7 +282,6 @@ macro_rules! language_server {
|
|||
let language_server = language_server!(item);
|
||||
let offset_encoding = language_server.offset_encoding();
|
||||
|
||||
// resolve item if not yet resolved
|
||||
if !item.resolved {
|
||||
if let Some(resolved) =
|
||||
Self::resolve_completion_item(language_server, item.item.clone())
|
||||
|
@ -366,7 +362,7 @@ macro_rules! language_server {
|
|||
// TODO: expand nucleo api to allow moving straight to a Utf32String here
|
||||
// and avoid allocation during matching
|
||||
filter: String::from(fragment),
|
||||
resolve_handler: ResolveHandler::default().spawn(),
|
||||
resolve_handler: ResolveHandler::new(),
|
||||
};
|
||||
|
||||
// need to recompute immediately in case start_offset != trigger_offset
|
||||
|
@ -384,7 +380,16 @@ fn resolve_completion_item(
|
|||
language_server: &helix_lsp::Client,
|
||||
completion_item: lsp::CompletionItem,
|
||||
) -> Option<lsp::CompletionItem> {
|
||||
let future = language_server.resolve_completion_item(completion_item)?;
|
||||
if !matches!(
|
||||
language_server.capabilities().completion_provider,
|
||||
Some(lsp::CompletionOptions {
|
||||
resolve_provider: Some(true),
|
||||
..
|
||||
})
|
||||
) {
|
||||
return None;
|
||||
}
|
||||
let future = language_server.resolve_completion_item(&completion_item);
|
||||
let response = helix_lsp::block_on(future);
|
||||
match response {
|
||||
Ok(item) => Some(item),
|
||||
|
@ -417,7 +422,7 @@ pub fn is_empty(&self) -> bool {
|
|||
self.popup.contents().is_empty()
|
||||
}
|
||||
|
||||
fn replace_item(&mut self, old_item: CompletionItem, new_item: CompletionItem) {
|
||||
pub fn replace_item(&mut self, old_item: &CompletionItem, new_item: CompletionItem) {
|
||||
self.popup.contents_mut().replace_option(old_item, new_item);
|
||||
}
|
||||
|
||||
|
@ -439,12 +444,12 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||
self.popup.render(area, surface, cx);
|
||||
|
||||
// if we have a selection, render a markdown popup on top/below with info
|
||||
let option = match self.popup.contents().selection() {
|
||||
let option = match self.popup.contents_mut().selection_mut() {
|
||||
Some(option) => option,
|
||||
None => return,
|
||||
};
|
||||
if !option.resolved {
|
||||
helix_event::send_blocking(&self.resolve_handler, option.clone());
|
||||
self.resolve_handler.ensure_item_resolved(cx.editor, option);
|
||||
}
|
||||
// need to render:
|
||||
// option.detail
|
||||
|
@ -493,12 +498,7 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||
None => return,
|
||||
};
|
||||
|
||||
let popup_area = {
|
||||
let (popup_x, popup_y) = self.popup.get_rel_position(area, cx.editor);
|
||||
let (popup_width, popup_height) = self.popup.get_size();
|
||||
Rect::new(popup_x, popup_y, popup_width, popup_height)
|
||||
};
|
||||
|
||||
let popup_area = self.popup.area(area, cx.editor);
|
||||
let doc_width_available = area.width.saturating_sub(popup_area.right());
|
||||
let doc_area = if doc_width_available > 30 {
|
||||
let mut doc_width = doc_width_available;
|
||||
|
@ -547,88 +547,3 @@ fn render(&mut self, area: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||
markdown_doc.render(doc_area, surface, cx);
|
||||
}
|
||||
}
|
||||
|
||||
/// A hook for resolving incomplete completion items.
|
||||
///
|
||||
/// From the [LSP spec](https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#textDocument_completion):
|
||||
///
|
||||
/// > If computing full completion items is expensive, servers can additionally provide a
|
||||
/// > handler for the completion item resolve request. ...
|
||||
/// > A typical use case is for example: the `textDocument/completion` request doesn't fill
|
||||
/// > in the `documentation` property for returned completion items since it is expensive
|
||||
/// > to compute. When the item is selected in the user interface then a
|
||||
/// > 'completionItem/resolve' request is sent with the selected completion item as a parameter.
|
||||
/// > The returned completion item should have the documentation property filled in.
|
||||
#[derive(Debug, Default)]
|
||||
struct ResolveHandler {
|
||||
trigger: Option<CompletionItem>,
|
||||
request: Option<helix_event::CancelTx>,
|
||||
}
|
||||
|
||||
impl AsyncHook for ResolveHandler {
|
||||
type Event = CompletionItem;
|
||||
|
||||
fn handle_event(
|
||||
&mut self,
|
||||
item: Self::Event,
|
||||
timeout: Option<tokio::time::Instant>,
|
||||
) -> Option<tokio::time::Instant> {
|
||||
if self
|
||||
.trigger
|
||||
.as_ref()
|
||||
.is_some_and(|trigger| trigger == &item)
|
||||
{
|
||||
timeout
|
||||
} else {
|
||||
self.trigger = Some(item);
|
||||
self.request = None;
|
||||
Some(Instant::now() + Duration::from_millis(150))
|
||||
}
|
||||
}
|
||||
|
||||
fn finish_debounce(&mut self) {
|
||||
let Some(item) = self.trigger.take() else { return };
|
||||
let (tx, rx) = helix_event::cancelation();
|
||||
self.request = Some(tx);
|
||||
job::dispatch_blocking(move |editor, _| resolve_completion_item(editor, item, rx))
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_completion_item(
|
||||
editor: &mut Editor,
|
||||
item: CompletionItem,
|
||||
cancel: helix_event::CancelRx,
|
||||
) {
|
||||
let Some(language_server) = editor.language_server_by_id(item.language_server_id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(future) = language_server.resolve_completion_item(item.item.clone()) else {
|
||||
return;
|
||||
};
|
||||
|
||||
tokio::spawn(async move {
|
||||
match helix_event::cancelable_future(future, cancel).await {
|
||||
Some(Ok(resolved_item)) => {
|
||||
job::dispatch(move |_, compositor| {
|
||||
if let Some(completion) = &mut compositor
|
||||
.find::<crate::ui::EditorView>()
|
||||
.unwrap()
|
||||
.completion
|
||||
{
|
||||
let resolved_item = CompletionItem {
|
||||
item: resolved_item,
|
||||
language_server_id: item.language_server_id,
|
||||
resolved: true,
|
||||
};
|
||||
|
||||
completion.replace_item(item, resolved_item);
|
||||
};
|
||||
})
|
||||
.await
|
||||
}
|
||||
Some(Err(err)) => log::error!("completion resolve request failed: {err}"),
|
||||
None => (),
|
||||
}
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1034,7 +1034,6 @@ pub fn set_completion(
|
|||
self.last_insert.1.push(InsertEvent::TriggerCompletion);
|
||||
|
||||
// TODO : propagate required size on resize to completion too
|
||||
completion.required_size((size.width, size.height));
|
||||
self.completion = Some(completion);
|
||||
Some(area)
|
||||
}
|
||||
|
|
|
@ -3,60 +3,95 @@
|
|||
use arc_swap::ArcSwap;
|
||||
use helix_core::syntax;
|
||||
use helix_view::graphics::{Margin, Rect, Style};
|
||||
use helix_view::input::Event;
|
||||
use tui::buffer::Buffer;
|
||||
use tui::layout::Alignment;
|
||||
use tui::text::Text;
|
||||
use tui::widgets::{BorderType, Paragraph, Widget, Wrap};
|
||||
|
||||
use crate::compositor::{Component, Compositor, Context};
|
||||
use crate::compositor::{Component, Compositor, Context, EventResult};
|
||||
|
||||
use crate::alt;
|
||||
use crate::ui::Markdown;
|
||||
|
||||
use super::Popup;
|
||||
|
||||
pub struct SignatureHelp {
|
||||
signature: String,
|
||||
signature_doc: Option<String>,
|
||||
pub struct Signature {
|
||||
pub signature: String,
|
||||
pub signature_doc: Option<String>,
|
||||
/// Part of signature text
|
||||
active_param_range: Option<(usize, usize)>,
|
||||
pub active_param_range: Option<(usize, usize)>,
|
||||
}
|
||||
|
||||
pub struct SignatureHelp {
|
||||
language: String,
|
||||
config_loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
active_signature: usize,
|
||||
signatures: Vec<Signature>,
|
||||
}
|
||||
|
||||
impl SignatureHelp {
|
||||
pub const ID: &'static str = "signature-help";
|
||||
|
||||
pub fn new(
|
||||
signature: String,
|
||||
language: String,
|
||||
config_loader: Arc<ArcSwap<syntax::Loader>>,
|
||||
active_signature: usize,
|
||||
signatures: Vec<Signature>,
|
||||
) -> Self {
|
||||
Self {
|
||||
signature,
|
||||
signature_doc: None,
|
||||
active_param_range: None,
|
||||
language,
|
||||
config_loader,
|
||||
active_signature,
|
||||
signatures,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_signature_doc(&mut self, signature_doc: Option<String>) {
|
||||
self.signature_doc = signature_doc;
|
||||
}
|
||||
|
||||
pub fn set_active_param_range(&mut self, offset: Option<(usize, usize)>) {
|
||||
self.active_param_range = offset;
|
||||
pub fn active_signature(&self) -> usize {
|
||||
self.active_signature
|
||||
}
|
||||
|
||||
pub fn visible_popup(compositor: &mut Compositor) -> Option<&mut Popup<Self>> {
|
||||
compositor.find_id::<Popup<Self>>(Self::ID)
|
||||
}
|
||||
|
||||
fn signature_index(&self) -> String {
|
||||
format!("({}/{})", self.active_signature + 1, self.signatures.len())
|
||||
}
|
||||
}
|
||||
|
||||
impl Component for SignatureHelp {
|
||||
fn handle_event(&mut self, event: &Event, _cx: &mut Context) -> EventResult {
|
||||
let Event::Key(event) = event else {
|
||||
return EventResult::Ignored(None);
|
||||
};
|
||||
|
||||
if self.signatures.len() <= 1 {
|
||||
return EventResult::Ignored(None);
|
||||
}
|
||||
|
||||
match event {
|
||||
alt!('p') => {
|
||||
self.active_signature = self
|
||||
.active_signature
|
||||
.checked_sub(1)
|
||||
.unwrap_or(self.signatures.len() - 1);
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
alt!('n') => {
|
||||
self.active_signature = (self.active_signature + 1) % self.signatures.len();
|
||||
EventResult::Consumed(None)
|
||||
}
|
||||
_ => EventResult::Ignored(None),
|
||||
}
|
||||
}
|
||||
|
||||
fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
|
||||
let margin = Margin::horizontal(1);
|
||||
|
||||
let active_param_span = self.active_param_range.map(|(start, end)| {
|
||||
let signature = &self.signatures[self.active_signature];
|
||||
|
||||
let active_param_span = signature.active_param_range.map(|(start, end)| {
|
||||
vec![(
|
||||
cx.editor
|
||||
.theme
|
||||
|
@ -66,21 +101,29 @@ fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
|
|||
)]
|
||||
});
|
||||
|
||||
let sig = &self.signatures[self.active_signature];
|
||||
let sig_text = crate::ui::markdown::highlighted_code_block(
|
||||
&self.signature,
|
||||
sig.signature.as_str(),
|
||||
&self.language,
|
||||
Some(&cx.editor.theme),
|
||||
Arc::clone(&self.config_loader),
|
||||
active_param_span,
|
||||
);
|
||||
|
||||
if self.signatures.len() > 1 {
|
||||
let signature_index = self.signature_index();
|
||||
let text = Text::from(signature_index);
|
||||
let paragraph = Paragraph::new(&text).alignment(Alignment::Right);
|
||||
paragraph.render(area.clip_top(1).with_height(1).clip_right(1), surface);
|
||||
}
|
||||
|
||||
let (_, sig_text_height) = crate::ui::text::required_size(&sig_text, area.width);
|
||||
let sig_text_area = area.clip_top(1).with_height(sig_text_height);
|
||||
let sig_text_area = sig_text_area.inner(&margin).intersection(surface.area);
|
||||
let sig_text_para = Paragraph::new(&sig_text).wrap(Wrap { trim: false });
|
||||
sig_text_para.render(sig_text_area, surface);
|
||||
|
||||
if self.signature_doc.is_none() {
|
||||
if sig.signature_doc.is_none() {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -92,7 +135,7 @@ fn render(&mut self, area: Rect, surface: &mut Buffer, cx: &mut Context) {
|
|||
}
|
||||
}
|
||||
|
||||
let sig_doc = match &self.signature_doc {
|
||||
let sig_doc = match &sig.signature_doc {
|
||||
None => return,
|
||||
Some(doc) => Markdown::new(doc.clone(), Arc::clone(&self.config_loader)),
|
||||
};
|
||||
|
@ -110,13 +153,15 @@ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
|||
const PADDING: u16 = 2;
|
||||
const SEPARATOR_HEIGHT: u16 = 1;
|
||||
|
||||
let sig = &self.signatures[self.active_signature];
|
||||
|
||||
if PADDING >= viewport.1 || PADDING >= viewport.0 {
|
||||
return None;
|
||||
}
|
||||
let max_text_width = (viewport.0 - PADDING).min(120);
|
||||
|
||||
let signature_text = crate::ui::markdown::highlighted_code_block(
|
||||
&self.signature,
|
||||
sig.signature.as_str(),
|
||||
&self.language,
|
||||
None,
|
||||
Arc::clone(&self.config_loader),
|
||||
|
@ -125,7 +170,7 @@ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
|||
let (sig_width, sig_height) =
|
||||
crate::ui::text::required_size(&signature_text, max_text_width);
|
||||
|
||||
let (width, height) = match self.signature_doc {
|
||||
let (width, height) = match sig.signature_doc {
|
||||
Some(ref doc) => {
|
||||
let doc_md = Markdown::new(doc.clone(), Arc::clone(&self.config_loader));
|
||||
let doc_text = doc_md.parse(None);
|
||||
|
@ -139,6 +184,12 @@ fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
|||
None => (sig_width, sig_height),
|
||||
};
|
||||
|
||||
Some((width + PADDING, height + PADDING))
|
||||
let sig_index_width = if self.signatures.len() > 1 {
|
||||
self.signature_index().len() + 1
|
||||
} else {
|
||||
0
|
||||
};
|
||||
|
||||
Some((width + PADDING + sig_index_width as u16, height + PADDING))
|
||||
}
|
||||
}
|
||||
|
|
|
@ -241,9 +241,9 @@ pub fn len(&self) -> usize {
|
|||
}
|
||||
|
||||
impl<T: Item + PartialEq> Menu<T> {
|
||||
pub fn replace_option(&mut self, old_option: T, new_option: T) {
|
||||
pub fn replace_option(&mut self, old_option: &T, new_option: T) {
|
||||
for option in &mut self.options {
|
||||
if old_option == *option {
|
||||
if old_option == option {
|
||||
*option = new_option;
|
||||
break;
|
||||
}
|
||||
|
|
|
@ -13,7 +13,7 @@
|
|||
mod statusline;
|
||||
mod text;
|
||||
|
||||
use crate::compositor::{Component, Compositor};
|
||||
use crate::compositor::Compositor;
|
||||
use crate::filter_picker_entry;
|
||||
use crate::job::{self, Callback};
|
||||
pub use completion::{Completion, CompletionItem};
|
||||
|
@ -143,14 +143,12 @@ pub fn raw_regex_prompt(
|
|||
move |_editor: &mut Editor, compositor: &mut Compositor| {
|
||||
let contents = Text::new(format!("{}", err));
|
||||
let size = compositor.size();
|
||||
let mut popup = Popup::new("invalid-regex", contents)
|
||||
let popup = Popup::new("invalid-regex", contents)
|
||||
.position(Some(helix_core::Position::new(
|
||||
size.height as usize - 2, // 2 = statusline + commandline
|
||||
0,
|
||||
)))
|
||||
.auto_close(true);
|
||||
popup.required_size((size.width, size.height));
|
||||
|
||||
compositor.replace_or_push("invalid-regex", popup);
|
||||
},
|
||||
));
|
||||
|
|
|
@ -15,6 +15,8 @@
|
|||
Editor,
|
||||
};
|
||||
|
||||
const MIN_HEIGHT: u16 = 4;
|
||||
|
||||
// TODO: share logic with Menu, it's essentially Popup(render_fn), but render fn needs to return
|
||||
// a width/height hint. maybe Popup(Box<Component>)
|
||||
|
||||
|
@ -22,11 +24,9 @@ pub struct Popup<T: Component> {
|
|||
contents: T,
|
||||
position: Option<Position>,
|
||||
margin: Margin,
|
||||
size: (u16, u16),
|
||||
child_size: (u16, u16),
|
||||
area: Rect,
|
||||
position_bias: Open,
|
||||
scroll: usize,
|
||||
scroll_half_pages: usize,
|
||||
auto_close: bool,
|
||||
ignore_escape_key: bool,
|
||||
id: &'static str,
|
||||
|
@ -39,11 +39,9 @@ pub fn new(id: &'static str, contents: T) -> Self {
|
|||
contents,
|
||||
position: None,
|
||||
margin: Margin::none(),
|
||||
size: (0, 0),
|
||||
position_bias: Open::Below,
|
||||
child_size: (0, 0),
|
||||
area: Rect::new(0, 0, 0, 0),
|
||||
scroll: 0,
|
||||
scroll_half_pages: 0,
|
||||
auto_close: false,
|
||||
ignore_escape_key: false,
|
||||
id,
|
||||
|
@ -95,66 +93,12 @@ pub fn ignore_escape_key(mut self, ignore: bool) -> Self {
|
|||
self
|
||||
}
|
||||
|
||||
/// Calculate the position where the popup should be rendered and return the coordinates of the
|
||||
/// top left corner.
|
||||
pub fn get_rel_position(&mut self, viewport: Rect, editor: &Editor) -> (u16, u16) {
|
||||
let position = self
|
||||
.position
|
||||
.get_or_insert_with(|| editor.cursor().0.unwrap_or_default());
|
||||
|
||||
let (width, height) = self.size;
|
||||
|
||||
// if there's a orientation preference, use that
|
||||
// if we're on the top part of the screen, do below
|
||||
// if we're on the bottom part, do above
|
||||
|
||||
// -- make sure frame doesn't stick out of bounds
|
||||
let mut rel_x = position.col as u16;
|
||||
let mut rel_y = position.row as u16;
|
||||
if viewport.width <= rel_x + width {
|
||||
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
|
||||
}
|
||||
|
||||
let can_put_below = viewport.height > rel_y + height;
|
||||
let can_put_above = rel_y.checked_sub(height).is_some();
|
||||
let final_pos = match self.position_bias {
|
||||
Open::Below => match can_put_below {
|
||||
true => Open::Below,
|
||||
false => Open::Above,
|
||||
},
|
||||
Open::Above => match can_put_above {
|
||||
true => Open::Above,
|
||||
false => Open::Below,
|
||||
},
|
||||
};
|
||||
|
||||
rel_y = match final_pos {
|
||||
Open::Above => rel_y.saturating_sub(height),
|
||||
Open::Below => rel_y + 1,
|
||||
};
|
||||
|
||||
(rel_x, rel_y)
|
||||
}
|
||||
|
||||
pub fn get_size(&self) -> (u16, u16) {
|
||||
(self.size.0, self.size.1)
|
||||
}
|
||||
|
||||
pub fn scroll(&mut self, offset: usize, direction: bool) {
|
||||
if direction {
|
||||
let max_offset = self.child_size.1.saturating_sub(self.size.1);
|
||||
self.scroll = (self.scroll + offset).min(max_offset as usize);
|
||||
} else {
|
||||
self.scroll = self.scroll.saturating_sub(offset);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn scroll_half_page_down(&mut self) {
|
||||
self.scroll(self.size.1 as usize / 2, true)
|
||||
self.scroll_half_pages += 1;
|
||||
}
|
||||
|
||||
pub fn scroll_half_page_up(&mut self) {
|
||||
self.scroll(self.size.1 as usize / 2, false)
|
||||
self.scroll_half_pages = self.scroll_half_pages.saturating_sub(1);
|
||||
}
|
||||
|
||||
/// Toggles the Popup's scrollbar.
|
||||
|
@ -174,13 +118,62 @@ pub fn contents_mut(&mut self) -> &mut T {
|
|||
}
|
||||
|
||||
pub fn area(&mut self, viewport: Rect, editor: &Editor) -> Rect {
|
||||
// trigger required_size so we recalculate if the child changed
|
||||
self.required_size((viewport.width, viewport.height));
|
||||
let child_size = self
|
||||
.contents
|
||||
.required_size((viewport.width, viewport.height))
|
||||
.expect("Component needs required_size implemented in order to be embedded in a popup");
|
||||
|
||||
let (rel_x, rel_y) = self.get_rel_position(viewport, editor);
|
||||
self.area_internal(viewport, editor, child_size)
|
||||
}
|
||||
|
||||
// clip to viewport
|
||||
viewport.intersection(Rect::new(rel_x, rel_y, self.size.0, self.size.1))
|
||||
pub fn area_internal(
|
||||
&mut self,
|
||||
viewport: Rect,
|
||||
editor: &Editor,
|
||||
child_size: (u16, u16),
|
||||
) -> Rect {
|
||||
let width = child_size.0.min(viewport.width);
|
||||
let height = child_size.1.min(viewport.height.saturating_sub(2)); // add some spacing in the viewport
|
||||
|
||||
let position = self
|
||||
.position
|
||||
.get_or_insert_with(|| editor.cursor().0.unwrap_or_default());
|
||||
|
||||
// if there's a orientation preference, use that
|
||||
// if we're on the top part of the screen, do below
|
||||
// if we're on the bottom part, do above
|
||||
|
||||
// -- make sure frame doesn't stick out of bounds
|
||||
let mut rel_x = position.col as u16;
|
||||
let mut rel_y = position.row as u16;
|
||||
if viewport.width <= rel_x + width {
|
||||
rel_x = rel_x.saturating_sub((rel_x + width).saturating_sub(viewport.width));
|
||||
}
|
||||
|
||||
let can_put_below = viewport.height > rel_y + MIN_HEIGHT;
|
||||
let can_put_above = rel_y.checked_sub(MIN_HEIGHT).is_some();
|
||||
let final_pos = match self.position_bias {
|
||||
Open::Below => match can_put_below {
|
||||
true => Open::Below,
|
||||
false => Open::Above,
|
||||
},
|
||||
Open::Above => match can_put_above {
|
||||
true => Open::Above,
|
||||
false => Open::Below,
|
||||
},
|
||||
};
|
||||
|
||||
match final_pos {
|
||||
Open::Above => {
|
||||
rel_y = rel_y.saturating_sub(height);
|
||||
Rect::new(rel_x, rel_y, width, position.row as u16 - rel_y)
|
||||
}
|
||||
Open::Below => {
|
||||
rel_y += 1;
|
||||
let y_max = viewport.bottom().min(height + rel_y);
|
||||
Rect::new(rel_x, rel_y, width, y_max - rel_y)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn handle_mouse_event(
|
||||
|
@ -266,38 +259,41 @@ fn handle_event(&mut self, event: &Event, cx: &mut Context) -> EventResult {
|
|||
// tab/enter/ctrl-k or whatever will confirm the selection/ ctrl-n/ctrl-p for scroll.
|
||||
}
|
||||
|
||||
fn required_size(&mut self, viewport: (u16, u16)) -> Option<(u16, u16)> {
|
||||
let max_width = 120.min(viewport.0);
|
||||
let max_height = 26.min(viewport.1.saturating_sub(2)); // add some spacing in the viewport
|
||||
fn required_size(&mut self, _viewport: (u16, u16)) -> Option<(u16, u16)> {
|
||||
const MAX_WIDTH: u16 = 120;
|
||||
const MAX_HEIGHT: u16 = 26;
|
||||
|
||||
let inner = Rect::new(0, 0, max_width, max_height).inner(&self.margin);
|
||||
let inner = Rect::new(0, 0, MAX_WIDTH, MAX_HEIGHT).inner(&self.margin);
|
||||
|
||||
let (width, height) = self
|
||||
.contents
|
||||
.required_size((inner.width, inner.height))
|
||||
.expect("Component needs required_size implemented in order to be embedded in a popup");
|
||||
|
||||
self.child_size = (width, height);
|
||||
self.size = (
|
||||
(width + self.margin.width()).min(max_width),
|
||||
(height + self.margin.height()).min(max_height),
|
||||
let size = (
|
||||
(width + self.margin.width()).min(MAX_WIDTH),
|
||||
(height + self.margin.height()).min(MAX_HEIGHT),
|
||||
);
|
||||
|
||||
Some(self.size)
|
||||
Some(size)
|
||||
}
|
||||
|
||||
fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
|
||||
let area = self.area(viewport, cx.editor);
|
||||
let child_size = self
|
||||
.contents
|
||||
.required_size((viewport.width, viewport.height))
|
||||
.expect("Component needs required_size implemented in order to be embedded in a popup");
|
||||
|
||||
let area = self.area_internal(viewport, cx.editor, child_size);
|
||||
self.area = area;
|
||||
|
||||
// required_size() calculates the popup size without taking account of self.position
|
||||
// so we need to correct the popup height to correctly calculate the scroll
|
||||
self.size.1 = area.height;
|
||||
|
||||
// re-clamp scroll offset
|
||||
let max_offset = self.child_size.1.saturating_sub(self.size.1);
|
||||
self.scroll = self.scroll.min(max_offset as usize);
|
||||
cx.scroll = Some(self.scroll);
|
||||
let max_offset = child_size.1.saturating_sub(area.height) as usize;
|
||||
let half_page_size = (area.height / 2) as usize;
|
||||
let scroll = max_offset.min(self.scroll_half_pages * half_page_size);
|
||||
if half_page_size > 0 {
|
||||
self.scroll_half_pages = scroll / half_page_size;
|
||||
}
|
||||
cx.scroll = Some(scroll);
|
||||
|
||||
// clear area
|
||||
let background = cx.editor.theme.get("ui.popup");
|
||||
|
@ -325,9 +321,8 @@ fn render(&mut self, viewport: Rect, surface: &mut Surface, cx: &mut Context) {
|
|||
// render scrollbar if contents do not fit
|
||||
if self.has_scrollbar {
|
||||
let win_height = (inner.height as usize).saturating_sub(2 * border);
|
||||
let len = (self.child_size.1 as usize).saturating_sub(2 * border);
|
||||
let len = (child_size.1 as usize).saturating_sub(2 * border);
|
||||
let fits = len <= win_height;
|
||||
let scroll = self.scroll;
|
||||
let scroll_style = cx.editor.theme.get("ui.menu.scroll");
|
||||
|
||||
const fn div_ceil(a: usize, b: usize) -> usize {
|
||||
|
|
|
@ -1,16 +1,18 @@
|
|||
use std::{collections::HashMap, time::Instant};
|
||||
|
||||
use helix_lsp::LanguageServerId;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct ProgressSpinners {
|
||||
inner: HashMap<usize, Spinner>,
|
||||
inner: HashMap<LanguageServerId, Spinner>,
|
||||
}
|
||||
|
||||
impl ProgressSpinners {
|
||||
pub fn get(&self, id: usize) -> Option<&Spinner> {
|
||||
pub fn get(&self, id: LanguageServerId) -> Option<&Spinner> {
|
||||
self.inner.get(&id)
|
||||
}
|
||||
|
||||
pub fn get_or_create(&mut self, id: usize) -> &mut Spinner {
|
||||
pub fn get_or_create(&mut self, id: LanguageServerId) -> &mut Spinner {
|
||||
self.inner.entry(id).or_default()
|
||||
}
|
||||
}
|
||||
|
|
|
@ -640,3 +640,87 @@ async fn test_join_selections_space() -> anyhow::Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_read_file() -> anyhow::Result<()> {
|
||||
let mut file = tempfile::NamedTempFile::new()?;
|
||||
let contents_to_read = "some contents";
|
||||
let output_file = helpers::temp_file_with_contents(contents_to_read)?;
|
||||
|
||||
test_key_sequence(
|
||||
&mut helpers::AppBuilder::new()
|
||||
.with_file(file.path(), None)
|
||||
.build()?,
|
||||
Some(&format!(":r {:?}<ret><esc>:w<ret>", output_file.path())),
|
||||
Some(&|app| {
|
||||
assert!(!app.editor.is_err(), "error: {:?}", app.editor.get_status());
|
||||
}),
|
||||
false,
|
||||
)
|
||||
.await?;
|
||||
|
||||
let expected_contents = LineFeedHandling::Native.apply(contents_to_read);
|
||||
helpers::assert_file_has_content(&mut file, &expected_contents)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn surround_delete() -> anyhow::Result<()> {
|
||||
// Test `surround_delete` when head < anchor
|
||||
test(("(#[| ]#)", "mdm", "#[| ]#")).await?;
|
||||
test(("(#[| ]#)", "md(", "#[| ]#")).await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn surround_replace_ts() -> anyhow::Result<()> {
|
||||
const INPUT: &str = r#"\
|
||||
fn foo() {
|
||||
if let Some(_) = None {
|
||||
todo!("f#[|o]#o)");
|
||||
}
|
||||
}
|
||||
"#;
|
||||
test((
|
||||
INPUT,
|
||||
":lang rust<ret>mrm'",
|
||||
r#"\
|
||||
fn foo() {
|
||||
if let Some(_) = None {
|
||||
todo!('f#[|o]#o)');
|
||||
}
|
||||
}
|
||||
"#,
|
||||
))
|
||||
.await?;
|
||||
|
||||
test((
|
||||
INPUT,
|
||||
":lang rust<ret>3mrm[",
|
||||
r#"\
|
||||
fn foo() {
|
||||
if let Some(_) = None [
|
||||
todo!("f#[|o]#o)");
|
||||
]
|
||||
}
|
||||
"#,
|
||||
))
|
||||
.await?;
|
||||
|
||||
test((
|
||||
INPUT,
|
||||
":lang rust<ret>2mrm{",
|
||||
r#"\
|
||||
fn foo() {
|
||||
if let Some(_) = None {
|
||||
todo!{"f#[|o]#o)"};
|
||||
}
|
||||
}
|
||||
"#,
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -726,3 +726,83 @@ async fn select_all_children() -> anyhow::Result<()> {
|
|||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_select_next_sibling() -> anyhow::Result<()> {
|
||||
let tests = vec![
|
||||
// basic test
|
||||
(
|
||||
indoc! {r##"
|
||||
fn inc(x: usize) -> usize { x + 1 #[}|]#
|
||||
fn dec(x: usize) -> usize { x - 1 }
|
||||
fn ident(x: usize) -> usize { x }
|
||||
"##},
|
||||
"<A-n>",
|
||||
indoc! {r##"
|
||||
fn inc(x: usize) -> usize { x + 1 }
|
||||
#[fn dec(x: usize) -> usize { x - 1 }|]#
|
||||
fn ident(x: usize) -> usize { x }
|
||||
"##},
|
||||
),
|
||||
// direction is not preserved and is always forward.
|
||||
(
|
||||
indoc! {r##"
|
||||
fn inc(x: usize) -> usize { x + 1 #[}|]#
|
||||
fn dec(x: usize) -> usize { x - 1 }
|
||||
fn ident(x: usize) -> usize { x }
|
||||
"##},
|
||||
"<A-n><A-;><A-n>",
|
||||
indoc! {r##"
|
||||
fn inc(x: usize) -> usize { x + 1 }
|
||||
fn dec(x: usize) -> usize { x - 1 }
|
||||
#[fn ident(x: usize) -> usize { x }|]#
|
||||
"##},
|
||||
),
|
||||
];
|
||||
|
||||
for test in tests {
|
||||
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn test_select_prev_sibling() -> anyhow::Result<()> {
|
||||
let tests = vec![
|
||||
// basic test
|
||||
(
|
||||
indoc! {r##"
|
||||
fn inc(x: usize) -> usize { x + 1 }
|
||||
fn dec(x: usize) -> usize { x - 1 }
|
||||
#[|f]#n ident(x: usize) -> usize { x }
|
||||
"##},
|
||||
"<A-p>",
|
||||
indoc! {r##"
|
||||
fn inc(x: usize) -> usize { x + 1 }
|
||||
#[|fn dec(x: usize) -> usize { x - 1 }]#
|
||||
fn ident(x: usize) -> usize { x }
|
||||
"##},
|
||||
),
|
||||
// direction is not preserved and is always backward.
|
||||
(
|
||||
indoc! {r##"
|
||||
fn inc(x: usize) -> usize { x + 1 }
|
||||
fn dec(x: usize) -> usize { x - 1 }
|
||||
#[|f]#n ident(x: usize) -> usize { x }
|
||||
"##},
|
||||
"<A-p><A-;><A-p>",
|
||||
indoc! {r##"
|
||||
#[|fn inc(x: usize) -> usize { x + 1 }]#
|
||||
fn dec(x: usize) -> usize { x - 1 }
|
||||
fn ident(x: usize) -> usize { x }
|
||||
"##},
|
||||
),
|
||||
];
|
||||
|
||||
for test in tests {
|
||||
test_with_config(AppBuilder::new().with_file("foo.rs", None), test).await?;
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -107,6 +107,14 @@ async fn surround_by_character() -> anyhow::Result<()> {
|
|||
))
|
||||
.await?;
|
||||
|
||||
// Selection direction is preserved
|
||||
test((
|
||||
"(so [many {go#[|od]#} text] here)",
|
||||
"mi{",
|
||||
"(so [many {#[|good]#} text] here)",
|
||||
))
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
@ -366,6 +374,41 @@ async fn surround_around_pair() -> anyhow::Result<()> {
|
|||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
async fn match_around_closest_ts() -> anyhow::Result<()> {
|
||||
test_with_config(
|
||||
AppBuilder::new().with_file("foo.rs", None),
|
||||
(
|
||||
r#"fn main() {todo!{"f#[|oo]#)"};}"#,
|
||||
"mam",
|
||||
r#"fn main() {todo!{#[|"foo)"]#};}"#,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
test_with_config(
|
||||
AppBuilder::new().with_file("foo.rs", None),
|
||||
(
|
||||
r##"fn main() { let _ = ("#[|1]#23", "#(|1)#23"); } "##,
|
||||
"3mam",
|
||||
r##"fn main() #[|{ let _ = ("123", "123"); }]# "##,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
test_with_config(
|
||||
AppBuilder::new().with_file("foo.rs", None),
|
||||
(
|
||||
r##" fn main() { let _ = ("12#[|3", "12]#3"); } "##,
|
||||
"1mam",
|
||||
r##" fn main() { let _ = #[|("123", "123")]#; } "##,
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Ensure the very initial cursor in an opened file is the width of
|
||||
/// the first grapheme
|
||||
#[tokio::test(flavor = "multi_thread")]
|
||||
|
@ -666,7 +709,7 @@ async fn tree_sitter_motions_work_across_injections() -> anyhow::Result<()> {
|
|||
(
|
||||
"<script>let #[|x]# = 1;</script>",
|
||||
"<A-n>",
|
||||
"<script>let x #[|=]# 1;</script>",
|
||||
"<script>let x #[=|]# 1;</script>",
|
||||
),
|
||||
)
|
||||
.await?;
|
||||
|
|
|
@ -91,6 +91,10 @@ impl<W> CrosstermBackend<W>
|
|||
W: Write,
|
||||
{
|
||||
pub fn new(buffer: W, config: &EditorConfig) -> CrosstermBackend<W> {
|
||||
// helix is not usable without colors, but crossterm will disable
|
||||
// them by default if NO_COLOR is set in the environment. Override
|
||||
// this behaviour.
|
||||
crossterm::style::force_color_output(true);
|
||||
CrosstermBackend {
|
||||
buffer,
|
||||
capabilities: Capabilities::from_env_or_default(config),
|
||||
|
|
|
@ -19,7 +19,7 @@ tokio = { version = "1", features = ["rt", "rt-multi-thread", "time", "sync", "p
|
|||
parking_lot = "0.12"
|
||||
arc-swap = { version = "1.7.1" }
|
||||
|
||||
gix = { version = "0.61.0", features = ["attributes", "status"], default-features = false, optional = true }
|
||||
gix = { version = "0.62.0", features = ["attributes", "status"], default-features = false, optional = true }
|
||||
imara-diff = "0.1.5"
|
||||
anyhow = "1"
|
||||
|
||||
|
|
|
@ -624,7 +624,7 @@ fn take_with<T, F>(mut_ref: &mut T, f: F)
|
|||
*mut_ref = f(mem::take(mut_ref));
|
||||
}
|
||||
|
||||
use helix_lsp::{lsp, Client, LanguageServerName};
|
||||
use helix_lsp::{lsp, Client, LanguageServerId, LanguageServerName};
|
||||
use url::Url;
|
||||
|
||||
impl Document {
|
||||
|
@ -1296,11 +1296,7 @@ fn apply_impl(
|
|||
});
|
||||
|
||||
self.diagnostics.sort_unstable_by_key(|diagnostic| {
|
||||
(
|
||||
diagnostic.range,
|
||||
diagnostic.severity,
|
||||
diagnostic.language_server_id,
|
||||
)
|
||||
(diagnostic.range, diagnostic.severity, diagnostic.provider)
|
||||
});
|
||||
|
||||
// Update the inlay hint annotations' positions, helping ensure they are displayed in the proper place
|
||||
|
@ -1644,7 +1640,7 @@ pub fn language_servers_with_feature(
|
|||
})
|
||||
}
|
||||
|
||||
pub fn supports_language_server(&self, id: usize) -> bool {
|
||||
pub fn supports_language_server(&self, id: LanguageServerId) -> bool {
|
||||
self.language_servers().any(|l| l.id() == id)
|
||||
}
|
||||
|
||||
|
@ -1767,7 +1763,7 @@ pub fn lsp_diagnostic_to_diagnostic(
|
|||
text: &Rope,
|
||||
language_config: Option<&LanguageConfiguration>,
|
||||
diagnostic: &helix_lsp::lsp::Diagnostic,
|
||||
language_server_id: usize,
|
||||
language_server_id: LanguageServerId,
|
||||
offset_encoding: helix_lsp::OffsetEncoding,
|
||||
) -> Option<Diagnostic> {
|
||||
use helix_core::diagnostic::{Range, Severity::*};
|
||||
|
@ -1844,7 +1840,7 @@ pub fn lsp_diagnostic_to_diagnostic(
|
|||
tags,
|
||||
source: diagnostic.source.clone(),
|
||||
data: diagnostic.data.clone(),
|
||||
language_server_id,
|
||||
provider: language_server_id,
|
||||
})
|
||||
}
|
||||
|
||||
|
@ -1857,13 +1853,13 @@ pub fn replace_diagnostics(
|
|||
&mut self,
|
||||
diagnostics: impl IntoIterator<Item = Diagnostic>,
|
||||
unchanged_sources: &[String],
|
||||
language_server_id: Option<usize>,
|
||||
language_server_id: Option<LanguageServerId>,
|
||||
) {
|
||||
if unchanged_sources.is_empty() {
|
||||
self.clear_diagnostics(language_server_id);
|
||||
} else {
|
||||
self.diagnostics.retain(|d| {
|
||||
if language_server_id.map_or(false, |id| id != d.language_server_id) {
|
||||
if language_server_id.map_or(false, |id| id != d.provider) {
|
||||
return true;
|
||||
}
|
||||
|
||||
|
@ -1876,18 +1872,14 @@ pub fn replace_diagnostics(
|
|||
}
|
||||
self.diagnostics.extend(diagnostics);
|
||||
self.diagnostics.sort_unstable_by_key(|diagnostic| {
|
||||
(
|
||||
diagnostic.range,
|
||||
diagnostic.severity,
|
||||
diagnostic.language_server_id,
|
||||
)
|
||||
(diagnostic.range, diagnostic.severity, diagnostic.provider)
|
||||
});
|
||||
}
|
||||
|
||||
/// clears diagnostics for a given language server id if set, otherwise all diagnostics are cleared
|
||||
pub fn clear_diagnostics(&mut self, language_server_id: Option<usize>) {
|
||||
pub fn clear_diagnostics(&mut self, language_server_id: Option<LanguageServerId>) {
|
||||
if let Some(id) = language_server_id {
|
||||
self.diagnostics.retain(|d| d.language_server_id != id);
|
||||
self.diagnostics.retain(|d| d.provider != id);
|
||||
} else {
|
||||
self.diagnostics.clear();
|
||||
}
|
||||
|
|
|
@ -16,7 +16,7 @@
|
|||
|
||||
use futures_util::stream::select_all::SelectAll;
|
||||
use futures_util::{future, StreamExt};
|
||||
use helix_lsp::Call;
|
||||
use helix_lsp::{Call, LanguageServerId};
|
||||
use tokio_stream::wrappers::UnboundedReceiverStream;
|
||||
|
||||
use std::{
|
||||
|
@ -960,7 +960,7 @@ pub struct Editor {
|
|||
pub macro_recording: Option<(char, Vec<KeyEvent>)>,
|
||||
pub macro_replaying: Vec<char>,
|
||||
pub language_servers: helix_lsp::Registry,
|
||||
pub diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, usize)>>,
|
||||
pub diagnostics: BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
pub diff_providers: DiffProviderRegistry,
|
||||
|
||||
pub debugger: Option<dap::Client>,
|
||||
|
@ -1020,7 +1020,7 @@ pub struct Editor {
|
|||
pub enum EditorEvent {
|
||||
DocumentSaved(DocumentSavedEventResult),
|
||||
ConfigEvent(ConfigEvent),
|
||||
LanguageServerMessage((usize, Call)),
|
||||
LanguageServerMessage((LanguageServerId, Call)),
|
||||
DebuggerEvent(dap::Payload),
|
||||
IdleTimer,
|
||||
Redraw,
|
||||
|
@ -1260,8 +1260,13 @@ fn set_theme_impl(&mut self, theme: Theme, preview: ThemeAction) {
|
|||
}
|
||||
|
||||
#[inline]
|
||||
pub fn language_server_by_id(&self, language_server_id: usize) -> Option<&helix_lsp::Client> {
|
||||
self.language_servers.get_by_id(language_server_id)
|
||||
pub fn language_server_by_id(
|
||||
&self,
|
||||
language_server_id: LanguageServerId,
|
||||
) -> Option<&helix_lsp::Client> {
|
||||
self.language_servers
|
||||
.get_by_id(language_server_id)
|
||||
.map(|client| &**client)
|
||||
}
|
||||
|
||||
/// Refreshes the language server for a given document
|
||||
|
@ -1861,7 +1866,7 @@ pub fn document_by_path_mut<P: AsRef<Path>>(&mut self, path: P) -> Option<&mut D
|
|||
/// Returns all supported diagnostics for the document
|
||||
pub fn doc_diagnostics<'a>(
|
||||
language_servers: &'a helix_lsp::Registry,
|
||||
diagnostics: &'a BTreeMap<PathBuf, Vec<(lsp::Diagnostic, usize)>>,
|
||||
diagnostics: &'a BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
document: &Document,
|
||||
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
||||
Editor::doc_diagnostics_with_filter(language_servers, diagnostics, document, |_, _| true)
|
||||
|
@ -1871,10 +1876,9 @@ pub fn doc_diagnostics<'a>(
|
|||
/// filtered by `filter` which is invocated with the raw `lsp::Diagnostic` and the language server id it came from
|
||||
pub fn doc_diagnostics_with_filter<'a>(
|
||||
language_servers: &'a helix_lsp::Registry,
|
||||
diagnostics: &'a BTreeMap<PathBuf, Vec<(lsp::Diagnostic, usize)>>,
|
||||
|
||||
diagnostics: &'a BTreeMap<PathBuf, Vec<(lsp::Diagnostic, LanguageServerId)>>,
|
||||
document: &Document,
|
||||
filter: impl Fn(&lsp::Diagnostic, usize) -> bool + 'a,
|
||||
filter: impl Fn(&lsp::Diagnostic, LanguageServerId) -> bool + 'a,
|
||||
) -> impl Iterator<Item = helix_core::Diagnostic> + 'a {
|
||||
let text = document.text().clone();
|
||||
let language_config = document.language.clone();
|
||||
|
|
|
@ -71,7 +71,7 @@ pub fn diagnostic<'doc>(
|
|||
d.line == line
|
||||
&& doc
|
||||
.language_servers_with_feature(LanguageServerFeature::Diagnostics)
|
||||
.any(|ls| ls.id() == d.language_server_id)
|
||||
.any(|ls| ls.id() == d.provider)
|
||||
});
|
||||
diagnostics_on_line.max_by_key(|d| d.severity).map(|d| {
|
||||
write!(out, "●").ok();
|
||||
|
|
|
@ -53,7 +53,7 @@ ltex-ls = { command = "ltex-ls" }
|
|||
markdoc-ls = { command = "markdoc-ls", args = ["--stdio"] }
|
||||
markdown-oxide = { command = "markdown-oxide" }
|
||||
marksman = { command = "marksman", args = ["server"] }
|
||||
metals = { command = "metals", config = { "isHttpEnabled" = true } }
|
||||
metals = { command = "metals", config = { "isHttpEnabled" = true, metals = { inlayHints = { typeParameters = {enable = true} , hintsInPatternMatch = {enable = true} } } } }
|
||||
mint = { command = "mint", args = ["ls"] }
|
||||
nil = { command = "nil" }
|
||||
nimlangserver = { command = "nimlangserver" }
|
||||
|
@ -102,6 +102,7 @@ yaml-language-server = { command = "yaml-language-server", args = ["--stdio"] }
|
|||
zls = { command = "zls" }
|
||||
blueprint-compiler = { command = "blueprint-compiler", args = ["lsp"] }
|
||||
typst-lsp = { command = "typst-lsp" }
|
||||
tinymist = { command = "tinymist" }
|
||||
pkgbuild-language-server = { command = "pkgbuild-language-server" }
|
||||
helm_ls = { command = "helm_ls", args = ["serve"] }
|
||||
ember-language-server = { command = "ember-language-server", args = ["--stdio"] }
|
||||
|
@ -201,6 +202,7 @@ scope = "source.rust"
|
|||
injection-regex = "rust"
|
||||
file-types = ["rs"]
|
||||
roots = ["Cargo.toml", "Cargo.lock"]
|
||||
shebangs = ["rust-script", "cargo"]
|
||||
auto-format = true
|
||||
comment-tokens = ["//", "///", "//!"]
|
||||
block-comment-tokens = [
|
||||
|
@ -250,7 +252,7 @@ args = { attachCommands = [ "platform select remote-gdb-server", "platform conne
|
|||
|
||||
[[grammar]]
|
||||
name = "rust"
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "0431a2c60828731f27491ee9fdefe25e250ce9c9" }
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-rust", rev = "473634230435c18033384bebaa6d6a17c2523281" }
|
||||
|
||||
[[language]]
|
||||
name = "sway"
|
||||
|
@ -934,7 +936,7 @@ indent = { tab-width = 2, unit = " " }
|
|||
|
||||
[[grammar]]
|
||||
name = "bash"
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "275effdfc0edce774acf7d481f9ea195c6c403cd" }
|
||||
source = { git = "https://github.com/tree-sitter/tree-sitter-bash", rev = "f8fb3274f72a30896075585b32b0c54cad65c086" }
|
||||
|
||||
[[language]]
|
||||
name = "php"
|
||||
|
@ -1641,7 +1643,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-gitattributes", rev =
|
|||
[[language]]
|
||||
name = "git-ignore"
|
||||
scope = "source.gitignore"
|
||||
file-types = [{ glob = ".gitignore" }, { glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = ".prettierignore" }, { glob = ".eslintignore" }, { glob = ".npmignore"}, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }]
|
||||
file-types = [{ glob = ".gitignore_global" }, { glob = ".ignore" }, { glob = "CODEOWNERS" }, { glob = ".config/helix/ignore" }, { glob = ".helix/ignore" }, { glob = ".*ignore" }]
|
||||
injection-regex = "git-ignore"
|
||||
comment-token = "#"
|
||||
grammar = "gitignore"
|
||||
|
@ -2052,6 +2054,29 @@ block-comment-tokens = { start = "/*", end = "*/" }
|
|||
indent = { tab-width = 4, unit = "\t" }
|
||||
formatter = { command = "odinfmt", args = [ "-stdin", "true" ] }
|
||||
|
||||
[language.debugger]
|
||||
name = "lldb-dap"
|
||||
transport = "stdio"
|
||||
command = "lldb-dap"
|
||||
|
||||
[[language.debugger.templates]]
|
||||
name = "binary"
|
||||
request = "launch"
|
||||
completion = [ { name = "binary", completion = "filename" } ]
|
||||
args = { console = "internalConsole", program = "{0}" }
|
||||
|
||||
[[language.debugger.templates]]
|
||||
name = "attach"
|
||||
request = "attach"
|
||||
completion = [ "pid" ]
|
||||
args = { console = "internalConsole", pid = "{0}" }
|
||||
|
||||
[[language.debugger.templates]]
|
||||
name = "gdbserver attach"
|
||||
request = "attach"
|
||||
completion = [ { name = "lldb connect url", default = "connect://localhost:3333" }, { name = "file", completion = "filename" }, "pid" ]
|
||||
args = { console = "internalConsole", attachCommands = [ "platform select remote-gdb-server", "platform connect {0}", "file {1}", "attach {2}" ] }
|
||||
|
||||
[[grammar]]
|
||||
name = "odin"
|
||||
source = { git = "https://github.com/ap29600/tree-sitter-odin", rev = "b219207e49ffca2952529d33e94ed63b1b75c4f1" }
|
||||
|
@ -3071,7 +3096,7 @@ scope = "source.typst"
|
|||
injection-regex = "typst"
|
||||
file-types = ["typst", "typ"]
|
||||
comment-token = "//"
|
||||
language-servers = ["typst-lsp"]
|
||||
language-servers = ["tinymist", "typst-lsp"]
|
||||
indent = { tab-width = 2, unit = " " }
|
||||
|
||||
[language.auto-pairs]
|
||||
|
@ -3083,7 +3108,7 @@ indent = { tab-width = 2, unit = " " }
|
|||
|
||||
[[grammar]]
|
||||
name = "typst"
|
||||
source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "ecf8596336857adfcd5f7cbb3b2aa11a67badc37" }
|
||||
source = { git = "https://github.com/uben0/tree-sitter-typst", rev = "13863ddcbaa7b68ee6221cea2e3143415e64aea4" }
|
||||
|
||||
[[language]]
|
||||
name = "nunjucks"
|
||||
|
@ -3366,7 +3391,7 @@ source = { git = "https://github.com/mtoohey31/tree-sitter-ld", rev = "0e9695ae0
|
|||
name = "hyprlang"
|
||||
scope = "source.hyprlang"
|
||||
roots = ["hyprland.conf"]
|
||||
file-types = [ { glob = "hyprland.conf"} ]
|
||||
file-types = [ { glob = "hyprland.conf" }, { glob = "hyprpaper.conf" }, { glob = "hypridle.conf" }, { glob = "hyprlock.conf" } ]
|
||||
comment-token = "#"
|
||||
grammar = "hyprlang"
|
||||
|
||||
|
@ -3478,7 +3503,7 @@ language-servers = ["earthlyls"]
|
|||
|
||||
[[grammar]]
|
||||
name = "earthfile"
|
||||
source = { git = "https://github.com/glehmann/tree-sitter-earthfile", rev = "2a6ab191f5f962562e495a818aa4e7f45f8a556a" }
|
||||
source = { git = "https://github.com/glehmann/tree-sitter-earthfile", rev = "a079e6c472eeedd6b9a1e03ca0b6c82cd6a112a4" }
|
||||
|
||||
[[language]]
|
||||
name = "adl"
|
||||
|
@ -3508,3 +3533,28 @@ comment-token = "#"
|
|||
[[grammar]]
|
||||
name = "ldif"
|
||||
source = { git = "https://github.com/kepet19/tree-sitter-ldif", rev = "0a917207f65ba3e3acfa9cda16142ee39c4c1aaa" }
|
||||
|
||||
[[language]]
|
||||
name = "xtc"
|
||||
scope = "source.xtc"
|
||||
# Accept Xena Traffic Configuration, Xena Port Configuration and Xena OpenAutomation
|
||||
file-types = [ "xtc", "xpc", "xoa" ]
|
||||
comment-token = ";"
|
||||
|
||||
[[grammar]]
|
||||
name = "xtc"
|
||||
source = { git = "https://github.com/Alexis-Lapierre/tree-sitter-xtc", rev = "7bc11b736250c45e25cfb0215db2f8393779957e" }
|
||||
|
||||
[[language]]
|
||||
name = "move"
|
||||
scope = "source.move"
|
||||
injection-regex = "move"
|
||||
roots = ["Move.toml"]
|
||||
file-types = ["move"]
|
||||
comment-token = "//"
|
||||
indent = { tab-width = 4, unit = " " }
|
||||
language-servers = []
|
||||
|
||||
[[grammar]]
|
||||
name = "move"
|
||||
source = { git = "https://github.com/tzakian/tree-sitter-move", rev = "8bc0d1692caa8763fef54d48068238d9bf3c0264" }
|
||||
|
|
|
@ -60,7 +60,6 @@
|
|||
">>"
|
||||
"<"
|
||||
"|"
|
||||
(expansion_flags)
|
||||
] @operator
|
||||
|
||||
(
|
||||
|
|
|
@ -1,42 +1,48 @@
|
|||
(string_array "," @punctuation.delimiter)
|
||||
(string_array ["[" "]"] @punctuation.bracket)
|
||||
|
||||
(arg_command "ARG" @keyword)
|
||||
(build_command "BUILD" @keyword)
|
||||
(cache_command "CACHE" @keyword)
|
||||
(cmd_command "CMD" @keyword)
|
||||
(copy_command "COPY" @keyword)
|
||||
(do_command "DO" @keyword)
|
||||
(entrypoint_command "ENTRYPOINT" @keyword)
|
||||
(env_command "ENV" @keyword)
|
||||
(expose_command "EXPOSE" @keyword)
|
||||
(from_command "FROM" @keyword)
|
||||
(from_dockerfile_command "FROM DOCKERFILE" @keyword)
|
||||
(function_command "FUNCTION" @keyword)
|
||||
(git_clone_command "GIT CLONE" @keyword)
|
||||
(host_command "HOST" @keyword)
|
||||
(import_command "IMPORT" @keyword)
|
||||
(label_command "LABEL" @keyword)
|
||||
(let_command "LET" @keyword)
|
||||
(project_command "PROJECT" @keyword)
|
||||
(run_command "RUN" @keyword)
|
||||
(save_artifact_command ["SAVE ARTIFACT" "AS LOCAL"] @keyword)
|
||||
(save_image_command "SAVE IMAGE" @keyword)
|
||||
(set_command "SET" @keyword)
|
||||
(user_command "USER" @keyword)
|
||||
(version_command "VERSION" @keyword)
|
||||
(volume_command "VOLUME" @keyword)
|
||||
(with_docker_command "WITH DOCKER" @keyword)
|
||||
(workdir_command "WORKDIR" @keyword)
|
||||
[
|
||||
"ARG"
|
||||
"AS LOCAL"
|
||||
"BUILD"
|
||||
"CACHE"
|
||||
"CMD"
|
||||
"COPY"
|
||||
"DO"
|
||||
"ENTRYPOINT"
|
||||
"ENV"
|
||||
"EXPOSE"
|
||||
"FROM DOCKERFILE"
|
||||
"FROM"
|
||||
"FUNCTION"
|
||||
"GIT CLONE"
|
||||
"HOST"
|
||||
"IMPORT"
|
||||
"LABEL"
|
||||
"LET"
|
||||
"PROJECT"
|
||||
"RUN"
|
||||
"SAVE ARTIFACT"
|
||||
"SAVE IMAGE"
|
||||
"SET"
|
||||
"USER"
|
||||
"VERSION"
|
||||
"VOLUME"
|
||||
"WORKDIR"
|
||||
] @keyword
|
||||
|
||||
(for_command ["FOR" "IN" "END"] @keyword.control.repeat)
|
||||
|
||||
(if_command ["IF" "END"] @keyword.control.conditional)
|
||||
(elif_block ["ELSE IF"] @keyword.control.conditional)
|
||||
(else_block ["ELSE"] @keyword.control.conditional)
|
||||
(import_command ["IMPORT" "AS"] @keyword.control.import)
|
||||
(try_command ["TRY" "FINALLY" "END"] @keyword.control.exception)
|
||||
(wait_command ["WAIT" "END"] @keyword.control)
|
||||
|
||||
(import_command ["IMPORT" "AS"] @keyword.control.import)
|
||||
|
||||
(try_command ["TRY" "FINALLY" "END"] @keyword.control.exception)
|
||||
|
||||
(wait_command ["WAIT" "END"] @keyword.control)
|
||||
(with_docker_command ["WITH DOCKER" "END"] @keyword.control)
|
||||
|
||||
[
|
||||
(comment)
|
||||
|
@ -65,10 +71,4 @@
|
|||
(build_arg) @variable
|
||||
(options (_) @variable.parameter)
|
||||
|
||||
(options (_ "=" @operator))
|
||||
(build_arg "=" @operator)
|
||||
(arg_command "=" @operator)
|
||||
(env_command "=" @operator)
|
||||
(label "=" @operator)
|
||||
(set_command "=" @operator)
|
||||
(let_command "=" @operator)
|
||||
"=" @operator
|
||||
|
|
|
@ -0,0 +1,157 @@
|
|||
(ability) @keyword
|
||||
|
||||
; ---
|
||||
; Primitives
|
||||
; ---
|
||||
|
||||
(address_literal) @constant
|
||||
(bool_literal) @constant.builtin.boolean
|
||||
(num_literal) @constant.numeric
|
||||
[
|
||||
(hex_string_literal)
|
||||
(byte_string_literal)
|
||||
] @string
|
||||
; TODO: vector_literal
|
||||
|
||||
[
|
||||
(line_comment)
|
||||
(block_comment)
|
||||
] @comment
|
||||
|
||||
(annotation) @function.macro
|
||||
|
||||
(borrow_expression "&" @keyword.storage.modifier.ref)
|
||||
(borrow_expression "&mut" @keyword.storage.modifier.mut)
|
||||
|
||||
(constant_identifier) @constant
|
||||
((identifier) @constant
|
||||
(#match? @constant "^[A-Z][A-Z\\d_]*$"))
|
||||
|
||||
(function_identifier) @function
|
||||
|
||||
(struct_identifier) @type
|
||||
(pack_expression
|
||||
access: (module_access
|
||||
member: (identifier) @type))
|
||||
(apply_type
|
||||
(module_access
|
||||
member: (identifier) @type))
|
||||
(field_identifier) @variable.other.member
|
||||
|
||||
; -------
|
||||
; Functions
|
||||
; -------
|
||||
|
||||
(call_expression
|
||||
access: (module_access
|
||||
member: (identifier) @function))
|
||||
|
||||
(macro_call_expression
|
||||
access: (macro_module_access
|
||||
access: (module_access
|
||||
member: [(identifier) @function.macro])
|
||||
"!" @function.macro))
|
||||
|
||||
; -------
|
||||
; Paths
|
||||
; -------
|
||||
|
||||
(module_identifier) @namespace
|
||||
|
||||
; -------
|
||||
; Operators
|
||||
; -------
|
||||
|
||||
[
|
||||
"*"
|
||||
"="
|
||||
"!"
|
||||
] @operator
|
||||
(binary_operator) @operator
|
||||
|
||||
; ---
|
||||
; Punctuation
|
||||
; ---
|
||||
|
||||
[
|
||||
"::"
|
||||
"."
|
||||
";"
|
||||
","
|
||||
] @punctuation.delimiter
|
||||
|
||||
[
|
||||
"("
|
||||
")"
|
||||
"["
|
||||
"]"
|
||||
"{"
|
||||
"}"
|
||||
] @punctuation.bracket
|
||||
|
||||
[
|
||||
"abort"
|
||||
; "acquires"
|
||||
"as"
|
||||
"break"
|
||||
"const"
|
||||
"continue"
|
||||
"copy"
|
||||
"else"
|
||||
"false"
|
||||
"friend"
|
||||
"fun"
|
||||
"has"
|
||||
"if"
|
||||
; "invariant"
|
||||
"let"
|
||||
"loop"
|
||||
"module"
|
||||
"move"
|
||||
"native"
|
||||
"public"
|
||||
"return"
|
||||
; "script"
|
||||
"spec"
|
||||
"struct"
|
||||
"true"
|
||||
"use"
|
||||
"while"
|
||||
|
||||
"entry"
|
||||
|
||||
; "aborts_if"
|
||||
; "aborts_with"
|
||||
"address"
|
||||
"apply"
|
||||
"assume"
|
||||
; "axiom"
|
||||
; "choose"
|
||||
"decreases"
|
||||
; "emits"
|
||||
"ensures"
|
||||
"except"
|
||||
; "forall"
|
||||
"global"
|
||||
"include"
|
||||
"internal"
|
||||
"local"
|
||||
; "min"
|
||||
; "modifies"
|
||||
"mut"
|
||||
"phantom"
|
||||
"post"
|
||||
"pragma"
|
||||
; "requires"
|
||||
; "Self"
|
||||
"schema"
|
||||
"succeeds_if"
|
||||
"to"
|
||||
; "update"
|
||||
"where"
|
||||
"with"
|
||||
] @keyword
|
||||
|
||||
(primitive_type) @type.buildin
|
||||
|
||||
(identifier) @variable
|
|
@ -51,7 +51,7 @@
|
|||
(lifetime
|
||||
"'" @label
|
||||
(identifier) @label)
|
||||
(loop_label
|
||||
(label
|
||||
"'" @label
|
||||
(identifier) @label)
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
; Special identifiers
|
||||
;--------------------
|
||||
|
||||
(tag_name) @tag
|
||||
(attribute_name) @variable.other.member
|
||||
(erroneous_end_tag_name) @error
|
||||
(comment) @comment
|
||||
|
||||
; TODO:
|
||||
((element (start_tag (tag_name) @_tag) (text) @markup.heading)
|
||||
(#match? @_tag "^(h[0-9]|title)$"))
|
||||
|
@ -28,11 +33,6 @@
|
|||
(quoted_attribute_value (attribute_value) @markup.link.url))
|
||||
(#match? @_attr "^(href|src)$"))
|
||||
|
||||
(tag_name) @tag
|
||||
(attribute_name) @variable.other.member
|
||||
(erroneous_end_tag_name) @error
|
||||
(comment) @comment
|
||||
|
||||
[
|
||||
(attribute_value)
|
||||
(quoted_attribute_value)
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
|
||||
; OPERATOR
|
||||
(in ["in" "not"] @keyword.operator)
|
||||
(context "context" @keyword.control)
|
||||
(and "and" @keyword.operator)
|
||||
(or "or" @keyword.operator)
|
||||
(not "not" @keyword.operator)
|
||||
|
@ -45,12 +46,9 @@
|
|||
(string) @string
|
||||
(content ["[" "]"] @operator)
|
||||
(bool) @constant.builtin.boolean
|
||||
(builtin) @constant.builtin
|
||||
(none) @constant.builtin
|
||||
(auto) @constant.builtin
|
||||
(ident) @variable
|
||||
(call
|
||||
item: (builtin) @function.builtin)
|
||||
|
||||
; MARKUP
|
||||
(item "-" @markup.list)
|
||||
|
|
|
@ -0,0 +1,27 @@
|
|||
(parameter) @keyword
|
||||
|
||||
(change_port) @function.special
|
||||
|
||||
(template) @variable
|
||||
|
||||
[
|
||||
(hex_argument)
|
||||
(ipv4_argument)
|
||||
] @attribute
|
||||
|
||||
(numeric_argument) @constant.numeric
|
||||
|
||||
(index) @tag
|
||||
|
||||
(string_literal_argument) @string
|
||||
|
||||
(string_argument) @constant.character
|
||||
|
||||
(comment) @comment
|
||||
|
||||
(port_comment) @label
|
||||
|
||||
[
|
||||
("[")
|
||||
("]")
|
||||
] @punctuation.bracket
|
|
@ -3,6 +3,7 @@
|
|||
"ui.background" = { bg = "base00" }
|
||||
"ui.virtual.whitespace" = "base03"
|
||||
"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] }
|
||||
"ui.virtual.ruler" = { bg = "base01" }
|
||||
"ui.menu" = { fg = "base05", bg = "base01" }
|
||||
"ui.menu.selected" = { fg = "base01", bg = "base04" }
|
||||
"ui.linenr" = { fg = "base03", bg = "base01" }
|
||||
|
|
|
@ -14,6 +14,7 @@
|
|||
"ui.cursor.primary" = { fg = "base05", modifiers = ["reversed"] }
|
||||
"ui.virtual.whitespace" = "base03"
|
||||
"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] }
|
||||
"ui.virtual.ruler" = { bg = "base01" }
|
||||
"ui.text" = "base05"
|
||||
"operator" = "base05"
|
||||
"ui.text.focus" = "base05"
|
||||
|
|
|
@ -15,6 +15,7 @@
|
|||
"ui.cursor.primary" = { fg = "light-gray", modifiers = ["reversed"] }
|
||||
"ui.virtual.whitespace" = "light-gray"
|
||||
"ui.virtual.jump-label" = { fg = "blue", modifiers = ["bold", "underlined"] }
|
||||
"ui.virtual.ruler" = { bg = "black" }
|
||||
"variable" = "light-red"
|
||||
"constant.numeric" = "yellow"
|
||||
"constant" = "yellow"
|
||||
|
|
|
@ -1,106 +1,93 @@
|
|||
# Author: Shafkath Shuhan <shafkathshuhannyc@gmail.com>
|
||||
# Author: David Else <12832280+David-Else@users.noreply.github.com>
|
||||
|
||||
"namespace" = { fg = "type" }
|
||||
"module" = { fg = "type" }
|
||||
# SYNTAX
|
||||
"attribute" = "fn_declaration"
|
||||
"comment" = "dark_green"
|
||||
"constant" = "constant"
|
||||
"constant.builtin" = "blue2"
|
||||
"constant.character" = "orange"
|
||||
"constant.character.escape" = "gold"
|
||||
"constant.numeric" = "pale_green"
|
||||
"constructor" = "type"
|
||||
"diff.delta" = "blue4"
|
||||
"diff.minus" = "orange_red"
|
||||
"diff.plus" = "dark_green2"
|
||||
"function" = "fn_declaration"
|
||||
"function.builtin" = "fn_declaration"
|
||||
"function.macro" = "blue2"
|
||||
"keyword" = "blue2"
|
||||
"keyword.control" = "special"
|
||||
"keyword.directive" = "special"
|
||||
"label" = "blue2"
|
||||
"module" = "type"
|
||||
"namespace" = "type"
|
||||
"operator" = "text"
|
||||
"punctuation" = "text"
|
||||
"punctuation.delimiter" = "text"
|
||||
"special" = "text"
|
||||
"string" = "orange"
|
||||
"string.regexp" = "gold"
|
||||
"tag" = "blue2"
|
||||
"type" = "type"
|
||||
"type.builtin" = "type"
|
||||
"type.enum.variant" = "constant"
|
||||
"variable" = "variable"
|
||||
"variable.builtin" = "blue2"
|
||||
"variable.other.member" = "variable"
|
||||
"variable.parameter" = "variable"
|
||||
|
||||
"type" = { fg = "type" }
|
||||
"type.builtin" = { fg = "type" }
|
||||
"type.enum.variant" = { fg = "constant" }
|
||||
"constructor" = { fg = "type" }
|
||||
"variable.other.member" = { fg = "variable" }
|
||||
|
||||
"keyword" = { fg = "blue2" }
|
||||
"keyword.directive" = { fg = "blue2" }
|
||||
"keyword.control" = { fg = "special" }
|
||||
"label" = { fg = "blue2" }
|
||||
"tag" = "blue2"
|
||||
|
||||
"special" = { fg = "text" }
|
||||
"operator" = { fg = "text" }
|
||||
|
||||
"punctuation" = { fg = "text" }
|
||||
"punctuation.delimiter" = { fg = "text" }
|
||||
|
||||
"variable" = { fg = "variable" }
|
||||
"variable.parameter" = { fg = "variable" }
|
||||
"variable.builtin" = { fg = "blue2" }
|
||||
"constant" = { fg = "constant" }
|
||||
"constant.builtin" = { fg = "blue2" }
|
||||
|
||||
"function" = { fg = "fn_declaration" }
|
||||
"function.builtin" = { fg = "fn_declaration" }
|
||||
"function.macro" = { fg = "blue2" }
|
||||
"attribute" = { fg = "fn_declaration" }
|
||||
|
||||
"comment" = { fg = "dark_green" }
|
||||
|
||||
"string" = { fg = "orange" }
|
||||
"constant.character" = { fg = "orange" }
|
||||
"string.regexp" = { fg = "gold" }
|
||||
"constant.numeric" = { fg = "pale_green" }
|
||||
"constant.character.escape" = { fg = "gold" }
|
||||
|
||||
"markup.heading" = { fg = "blue2", modifiers = ["bold"] }
|
||||
"markup.list" = "blue3"
|
||||
"markup.bold" = { fg = "blue2", modifiers = ["bold"] }
|
||||
"markup.italic" = { modifiers = ["italic"] }
|
||||
# MARKUP
|
||||
"markup.heading" = { fg = "blue2", modifiers = ["bold"] }
|
||||
"markup.list" = "blue3"
|
||||
"markup.bold" = { fg = "blue2", modifiers = ["bold"] }
|
||||
"markup.italic" = { modifiers = ["italic"] }
|
||||
"markup.strikethrough" = { modifiers = ["crossed_out"] }
|
||||
"markup.link.url" = { modifiers = ["underlined"] }
|
||||
"markup.link.text" = "orange"
|
||||
"markup.quote" = "dark_green"
|
||||
"markup.raw" = "orange"
|
||||
|
||||
"diff.plus" = { fg = "dark_green2" }
|
||||
"diff.delta" = { fg = "blue4" }
|
||||
"diff.minus" = { fg = "orange_red" }
|
||||
|
||||
"ui.background" = { fg = "light_gray", bg = "dark_gray2" }
|
||||
|
||||
"ui.window" = { bg = "widget" }
|
||||
"ui.popup" = { fg = "text", bg = "widget" }
|
||||
"ui.help" = { fg = "text", bg = "widget" }
|
||||
"ui.menu" = { fg = "text", bg = "widget" }
|
||||
"ui.menu.selected" = { bg = "dark_blue2" }
|
||||
"markup.link.url" = { modifiers = ["underlined"] }
|
||||
"markup.link.text" = "orange"
|
||||
"markup.quote" = "dark_green"
|
||||
"markup.raw" = "orange"
|
||||
|
||||
# UI
|
||||
"ui.background" = { fg = "light_gray", bg = "dark_gray2" }
|
||||
"ui.window" = { bg = "widget" }
|
||||
"ui.popup" = { fg = "text", bg = "widget" }
|
||||
"ui.help" = { fg = "text", bg = "widget" }
|
||||
"ui.menu" = { fg = "text", bg = "widget" }
|
||||
"ui.menu.selected" = { bg = "dark_blue2" }
|
||||
# TODO: Alternate bg colour for `ui.cursor.match` and `ui.selection`.
|
||||
"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] }
|
||||
"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] }
|
||||
"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] }
|
||||
|
||||
"ui.selection" = { bg = "#3a3d41" }
|
||||
"ui.selection.primary" = { bg = "dark_blue" }
|
||||
|
||||
"ui.linenr" = { fg = "dark_gray" }
|
||||
"ui.linenr.selected" = { fg = "light_gray2" }
|
||||
|
||||
"ui.cursorline.primary" = { bg = "dark_gray3" }
|
||||
"ui.statusline" = { fg = "white", bg = "blue" }
|
||||
"ui.statusline.inactive" = { fg = "white", bg = "blue" }
|
||||
"ui.statusline.insert" = { fg = "white", bg = "yellow" }
|
||||
"ui.statusline.select" = { fg = "white", bg = "magenta" }
|
||||
|
||||
"ui.bufferline" = { fg = "text", bg = "widget" }
|
||||
"ui.bufferline.active" = { fg = "white", bg = "blue" }
|
||||
"ui.bufferline.background" = { bg = "background" }
|
||||
|
||||
"ui.text" = { fg = "text" }
|
||||
"ui.text.focus" = { fg = "white" }
|
||||
|
||||
"ui.virtual.whitespace" = { fg = "dark_gray" }
|
||||
"ui.virtual.ruler" = { bg = "borders" }
|
||||
"ui.virtual.indent-guide" = { fg = "dark_gray4" }
|
||||
"ui.virtual.inlay-hint" = { fg = "dark_gray5"}
|
||||
"ui.virtual.jump-label" = { fg = "dark_gray", modifiers = ["bold"] }
|
||||
|
||||
"warning" = { fg = "gold2" }
|
||||
"error" = { fg = "red" }
|
||||
"info" = { fg = "light_blue" }
|
||||
"hint" = { fg = "light_gray3" }
|
||||
|
||||
"ui.cursor" = { fg = "cursor", modifiers = ["reversed"] }
|
||||
"ui.cursor.primary" = { fg = "cursor", modifiers = ["reversed"] }
|
||||
"ui.cursor.match" = { bg = "#3a3d41", modifiers = ["underlined"] }
|
||||
"ui.selection" = { bg = "#3a3d41" }
|
||||
"ui.selection.primary" = { bg = "dark_blue" }
|
||||
"ui.linenr" = { fg = "dark_gray" }
|
||||
"ui.linenr.selected" = { fg = "light_gray2" }
|
||||
"ui.cursorline.primary" = { bg = "dark_gray3" }
|
||||
"ui.statusline" = { fg = "white", bg = "blue" }
|
||||
"ui.statusline.inactive" = { fg = "white", bg = "widget" }
|
||||
"ui.statusline.insert" = { fg = "white", bg = "yellow" }
|
||||
"ui.statusline.select" = { fg = "white", bg = "magenta" }
|
||||
"ui.bufferline" = { fg = "text", bg = "widget" }
|
||||
"ui.bufferline.active" = { fg = "white", bg = "blue" }
|
||||
"ui.bufferline.background" = { bg = "background" }
|
||||
"ui.text" = { fg = "text" }
|
||||
"ui.text.focus" = { fg = "white" }
|
||||
"ui.virtual.whitespace" = { fg = "#3e3e3d" }
|
||||
"ui.virtual.ruler" = { bg = "borders" }
|
||||
"ui.virtual.indent-guide" = { fg = "dark_gray4" }
|
||||
"ui.virtual.inlay-hint" = { fg = "dark_gray5"}
|
||||
"ui.virtual.jump-label" = { fg = "dark_gray", modifiers = ["bold"] }
|
||||
"ui.highlight.frameline" = { bg = "#4b4b18" }
|
||||
"ui.debug.active" = { fg = "#ffcc00" }
|
||||
"ui.debug.breakpoint" = { fg = "#e51400" }
|
||||
"warning" = { fg = "gold2" }
|
||||
"error" = { fg = "red" }
|
||||
"info" = { fg = "light_blue" }
|
||||
"hint" = { fg = "light_gray3" }
|
||||
"diagnostic.error".underline = { color = "red", style = "curl" }
|
||||
"diagnostic".underline = { color = "gold", style = "curl" }
|
||||
"diagnostic.unnecessary" = { modifiers = ["dim"] }
|
||||
"diagnostic.deprecated" = { modifiers = ["crossed_out"] }
|
||||
"diagnostic".underline = { color = "gold", style = "curl" }
|
||||
"diagnostic.unnecessary" = { modifiers = ["dim"] }
|
||||
"diagnostic.deprecated" = { modifiers = ["crossed_out"] }
|
||||
|
||||
[palette]
|
||||
white = "#ffffff"
|
||||
|
|
|
@ -61,7 +61,7 @@
|
|||
"ui.statusline.inactive" = { fg = "foreground", bg = "background" }
|
||||
"ui.statusline.normal" = { fg = "white", bg = "background" }
|
||||
"ui.statusline.insert" = { fg = "blue", bg = "background" }
|
||||
"ui.statusline.select" = { fg = "cyan", bg = "magenta" }
|
||||
"ui.statusline.select" = { fg = "magenta", bg = "background" }
|
||||
"ui.text" = { fg = "foreground" }
|
||||
"ui.text.focus" = { fg = "blue" }
|
||||
"ui.virtual.ruler" = { bg = "cursorline" }
|
||||
|
|
|
@ -33,7 +33,7 @@
|
|||
"diff.delta" = "orange1"
|
||||
"diff.minus" = "red1"
|
||||
|
||||
"warning" = "orange1"
|
||||
"warning" = "yellow1"
|
||||
"error" = "red1"
|
||||
"info" = "aqua1"
|
||||
"hint" = "blue1"
|
||||
|
@ -67,7 +67,7 @@
|
|||
"ui.virtual.wrap" = { fg = "bg2" }
|
||||
"ui.virtual.jump-label" = { fg = "purple0", modifiers = ["bold"] }
|
||||
|
||||
"diagnostic.warning" = { underline = { color = "orange1", style = "curl" } }
|
||||
"diagnostic.warning" = { underline = { color = "yellow1", style = "curl" } }
|
||||
"diagnostic.error" = { underline = { color = "red1", style = "curl" } }
|
||||
"diagnostic.info" = { underline = { color = "aqua1", style = "curl" } }
|
||||
"diagnostic.hint" = { underline = { color = "blue1", style = "curl" } }
|
||||
|
|
|
@ -4,15 +4,19 @@ constant = "purple"
|
|||
"constant.numeric" = "orange"
|
||||
"constant.builtin" = "orange"
|
||||
variable = "red"
|
||||
attribute = "brown"
|
||||
comment = "light-gray"
|
||||
special = "purple"
|
||||
"punctuation" = "red"
|
||||
"punctuation.bracket" = "purple"
|
||||
"punctuation.delimiter" = "white"
|
||||
keyword = "purple"
|
||||
function = "blue"
|
||||
label = "orange"
|
||||
type = "orange"
|
||||
constructor = "orange"
|
||||
namespace = "orange"
|
||||
tag = "red"
|
||||
|
||||
# User Interface
|
||||
"ui.background" = { bg = "bg", fg = "gray" }
|
||||
|
@ -29,6 +33,7 @@ namespace = "orange"
|
|||
"ui.selection" = { bg = "selection" }
|
||||
"ui.virtual.indent-guide" = { fg = "gray" }
|
||||
"ui.virtual.whitespace" = { fg = "light-gray" }
|
||||
"ui.virtual.ruler" = { bg ="dark-bg" }
|
||||
"ui.statusline" = { bg = "dark-bg", fg = "light-gray" }
|
||||
"ui.popup" = { bg = "dark-bg", fg = "orange" }
|
||||
"ui.help" = { bg = "dark-bg", fg = "orange" }
|
||||
|
@ -79,6 +84,7 @@ pink = "#EE64AE"
|
|||
selection = "#353747"
|
||||
green = "#27D796"
|
||||
orange = "#FAB795"
|
||||
brown = "#F09383"
|
||||
purple = "#B877DB"
|
||||
red = "#E95678"
|
||||
blue = "#25B2BC"
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
|
||||
## User interface
|
||||
"ui.selection" = { bg = "waveBlue2" }
|
||||
"ui.selection.primary" = { bg = "sumiInk5" }
|
||||
"ui.selection.primary" = { bg = "waveBlue2" }
|
||||
"ui.background" = { fg = "fujiWhite", bg = "sumiInk1" }
|
||||
|
||||
"ui.linenr" = { fg = "sumiInk4" }
|
||||
|
@ -123,7 +123,6 @@ sumiInk1 = "#1F1F28" # default background
|
|||
sumiInk2 = "#2A2A37" # lighter background, e.g. colorcolumns, folds
|
||||
sumiInk3 = "#363646" # lighter background, e.g. cursorline
|
||||
sumiInk4 = "#54546D" # darker foreground, e.g. linenumbers, fold column
|
||||
sumiInk5 = "#363646" # current selection
|
||||
waveBlue1 = "#223249" # popup background, visual selection background
|
||||
waveBlue2 = "#2D4F67" # popup selection background, search background
|
||||
winterGreen = "#2B3328" # diff add background
|
||||
|
|
|
@ -79,6 +79,7 @@
|
|||
"ui.text.focus" = { fg = "fg" }
|
||||
|
||||
"ui.virtual" = { fg = "gray02" }
|
||||
"ui.virtual.ruler" = { bg ="gray02" }
|
||||
"ui.virtual.indent-guide" = { fg = "gray02" }
|
||||
"ui.virtual.inlay-hint" = { fg = "gray04" }
|
||||
|
||||
|
|
|
@ -80,6 +80,7 @@ punctuation = "fg-dim"
|
|||
"ui.virtual" = "bg-active"
|
||||
"ui.virtual.ruler" = { bg = "bg-dim" }
|
||||
"ui.virtual.inlay-hint" = { fg = "fg-dim", modifiers = ["italic"] }
|
||||
"ui.virtual.jump-label" = { fg = "yellow-cooler", modifiers = ["bold"] }
|
||||
|
||||
"ui.selection" = { fg = "fg-main", bg = "bg-inactive" }
|
||||
"ui.selection.primary" = { fg = "fg-main", bg = "bg-active" }
|
||||
|
|
|
@ -83,6 +83,7 @@ punctuation = "fg-dim"
|
|||
"ui.virtual" = "bg-active"
|
||||
"ui.virtual.ruler" = { bg = "bg-dim" }
|
||||
"ui.virtual.inlay-hint" = { fg = "fg-dim", modifiers = ["italic"] }
|
||||
"ui.virtual.jump-label" = { fg = "yellow-cooler", modifiers = ["bold"] }
|
||||
|
||||
"ui.selection" = { fg = "fg-main", bg = "bg-inactive" }
|
||||
"ui.selection.primary" = { fg = "fg-main", bg = "bg-active" }
|
||||
|
|
|
@ -58,6 +58,7 @@ string = { fg = "brightMint" }
|
|||
"ui.text.inactive" = "darkerGray"
|
||||
"ui.virtual" = { fg = "darkerGray.b0" }
|
||||
"ui.virtual.indent-guide" = "#303442"
|
||||
"ui.virtual.ruler" = { bg ="selection" }
|
||||
|
||||
"ui.selection" = { bg = "focus" }
|
||||
"ui.selection.primary" = { bg = "selection" }
|
||||
|
|
|
@ -90,7 +90,7 @@
|
|||
"ui.selection.primary" = { bg = "base015" }
|
||||
|
||||
"ui.virtual.indent-guide" = { fg = "base02" }
|
||||
"ui.virtual.ruler" = { fg = "red" }
|
||||
"ui.virtual.ruler" = { bg = "base02" }
|
||||
|
||||
# normal模式的光标
|
||||
"ui.cursor" = {fg = "base02", bg = "cyan"}
|
||||
|
|
|
@ -91,9 +91,6 @@
|
|||
# 影响 picker列表选中, 快捷键帮助窗口文本
|
||||
# Affects picker list selection, shortcut key help window text
|
||||
"ui.text.focus" = { fg = "blue", modifiers = ["bold"]}
|
||||
# file picker中, 预览的当前选中项
|
||||
# In file picker, the currently selected item of the preview
|
||||
"ui.highlight" = { fg = "red", modifiers = ["bold", "italic", "underlined"] }
|
||||
|
||||
# 主光标/selection
|
||||
# main cursor/selection
|
||||
|
@ -107,7 +104,7 @@
|
|||
"ui.selection.primary" = { bg = "base015" }
|
||||
|
||||
"ui.virtual.indent-guide" = { fg = "base02" }
|
||||
"ui.virtual.ruler" = { fg = "red" }
|
||||
"ui.virtual.ruler" = { bg = "base02" }
|
||||
|
||||
# normal模式的光标
|
||||
# normal mode cursor
|
||||
|
|
|
@ -67,6 +67,7 @@
|
|||
"ui.statusline.select" = { bg = "blue", fg = "bg2" }
|
||||
"ui.virtual.wrap" = { fg = "grey0" }
|
||||
"ui.virtual.inlay-hint" = { fg = "grey1" }
|
||||
"ui.virtual.ruler" = { bg = "bg2"}
|
||||
|
||||
"hint" = "blue"
|
||||
"info" = "aqua"
|
||||
|
|
|
@ -1,7 +1,10 @@
|
|||
"ui.background" = { bg = "black" }
|
||||
"ui.bufferline" = { bg = "black" }
|
||||
"ui.bufferline.active" = { fg = "light-magenta", bg = "dark-magenta" }
|
||||
"ui.cursor" = { fg = "green", modifiers = ["reversed"] }
|
||||
"ui.cursor.match" = { fg = "light-cyan", bg = "dark-cyan" }
|
||||
"ui.cursor.primary" = { fg = "light-green", modifiers = ["reversed"] }
|
||||
"ui.cursorline.primary" = { bg = "gray" }
|
||||
"ui.menu" = { bg = "dark-white" }
|
||||
"ui.menu.selected" = { fg = "yellow" }
|
||||
"ui.popup" = { bg = "dark-white" }
|
||||
|
@ -15,6 +18,7 @@
|
|||
"ui.text.focus" = { fg = "yellow" }
|
||||
"ui.virtual.wrap" = { fg = "dark-blue" }
|
||||
"ui.virtual.indent-guide" = { fg = "dark-blue" }
|
||||
"ui.virtual.ruler" = { bg = "dark-white" }
|
||||
"ui.window" = { bg = "dark-white" }
|
||||
|
||||
"diagnostic.error" = { bg = "dark-red" }
|
||||
|
@ -50,6 +54,7 @@
|
|||
black = "#000000"
|
||||
red = "#ed5f74"
|
||||
green = "#1ea672"
|
||||
gray = "#111111"
|
||||
yellow = "#d97917"
|
||||
blue = "#688ef1"
|
||||
magenta = "#c96ed0"
|
||||
|
|
|
@ -539,7 +539,7 @@
|
|||
will now affect both cursors.
|
||||
3. Use Insert mode to correct the lines. The two cursors will
|
||||
fix both lines simultaneously.
|
||||
4. Type , to remove the second cursor.
|
||||
4. Type , to remove the first cursor.
|
||||
|
||||
--> Fix th two nes at same ime.
|
||||
-->
|
||||
|
|
|
@ -40,7 +40,7 @@ label = "honey"
|
|||
"diff.minus" = "#f22c86"
|
||||
"diff.delta" = "#6f44f0"
|
||||
|
||||
# TODO: diferentiate doc comment
|
||||
# TODO: differentiate doc comment
|
||||
# concat (ERROR) @error.syntax and "MISSING ;" selectors for errors
|
||||
|
||||
"ui.background" = { bg = "midnight" }
|
||||
|
@ -56,6 +56,7 @@ label = "honey"
|
|||
"ui.text.focus" = { fg = "white" }
|
||||
"ui.text.inactive" = "sirocco"
|
||||
"ui.virtual" = { fg = "comet" }
|
||||
"ui.virtual.ruler" = { bg = "revolver" }
|
||||
"ui.virtual.jump-label" = { fg = "apricot", modifiers = ["bold"] }
|
||||
|
||||
"ui.virtual.indent-guide" = { fg = "comet" }
|
||||
|
@ -65,6 +66,8 @@ label = "honey"
|
|||
# TODO: namespace ui.cursor as ui.selection.cursor?
|
||||
"ui.cursor.select" = { bg = "delta" }
|
||||
"ui.cursor.insert" = { bg = "white" }
|
||||
"ui.cursor.primary.select" = { bg = "delta" }
|
||||
"ui.cursor.primary.insert" = { bg = "white" }
|
||||
"ui.cursor.match" = { fg = "#212121", bg = "#6C6999" }
|
||||
"ui.cursor" = { modifiers = ["reversed"] }
|
||||
"ui.cursorline.primary" = { bg = "bossanova" }
|
||||
|
|
Loading…
Reference in New Issue