Compare commits
88 Commits
linux-sche
...
gemini
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9445013dd6 | ||
|
|
684d9dde56 | ||
|
|
315692d112 | ||
|
|
ba09eabfba | ||
|
|
70d983abe3 | ||
|
|
4a3097d4dd | ||
|
|
59ce3535d3 | ||
|
|
f8b5e42070 | ||
|
|
88c5eb550e | ||
|
|
e5dc6beace | ||
|
|
3a410942b4 | ||
|
|
89fbd6528f | ||
|
|
9ce989a704 | ||
|
|
dd63e25f23 | ||
|
|
489077befc | ||
|
|
21c5ce2bbd | ||
|
|
3deb000f70 | ||
|
|
fe3fe945a9 | ||
|
|
11178eacc7 | ||
|
|
59bc027750 | ||
|
|
0a718c65e2 | ||
|
|
85d77a3eec | ||
|
|
ca80343486 | ||
|
|
739038ddaf | ||
|
|
106e0623dd | ||
|
|
607ad6de3c | ||
|
|
ea26a01f5f | ||
|
|
8abc000553 | ||
|
|
9f5309cedd | ||
|
|
adf74fdc14 | ||
|
|
e402d7e96a | ||
|
|
12dfd4a2c2 | ||
|
|
b87d1eabcc | ||
|
|
ac528dda64 | ||
|
|
906688f012 | ||
|
|
c18e9aedcd | ||
|
|
cd4847ca22 | ||
|
|
4c63e8b203 | ||
|
|
d9d8c1f6d9 | ||
|
|
b0dbc80575 | ||
|
|
8e853e2b56 | ||
|
|
47a78907d6 | ||
|
|
0c1a3db87d | ||
|
|
3541a1175f | ||
|
|
e51d469025 | ||
|
|
018a2a29ea | ||
|
|
d49727ff10 | ||
|
|
c195c4ddff | ||
|
|
fd03454540 | ||
|
|
6eeec9b403 | ||
|
|
b558e8da1e | ||
|
|
1d7b28c658 | ||
|
|
de78eb44b1 | ||
|
|
c071e19899 | ||
|
|
37fc4ce09d | ||
|
|
99f56252be | ||
|
|
f61abe0247 | ||
|
|
45c54d189a | ||
|
|
2727f55772 | ||
|
|
291d64c803 | ||
|
|
6a11184ea3 | ||
|
|
ff1dcff2fb | ||
|
|
bef2586eed | ||
|
|
bdba8b23fa | ||
|
|
22900554d5 | ||
|
|
6db0b6c5ad | ||
|
|
ba11e9a9a8 | ||
|
|
de570133ff | ||
|
|
f1b1a9fd5e | ||
|
|
1b08f14c54 | ||
|
|
36d3b16279 | ||
|
|
945764e409 | ||
|
|
f1281c14dd | ||
|
|
3b823d4a0b | ||
|
|
46645b552f | ||
|
|
5bc3846d59 | ||
|
|
e6d608fa05 | ||
|
|
e106a39620 | ||
|
|
d32e9f759c | ||
|
|
15662f105e | ||
|
|
1887a6db53 | ||
|
|
fa9360f78d | ||
|
|
3ff738fa03 | ||
|
|
74cf3d2d92 | ||
|
|
427491a24f | ||
|
|
2781b1cce1 | ||
|
|
ab69c05d99 | ||
|
|
f06c3b5670 |
24
.git-blame-ignore-revs
Normal file
24
.git-blame-ignore-revs
Normal file
@@ -0,0 +1,24 @@
|
||||
# .git-blame-ignore-revs
|
||||
#
|
||||
# This file consists of a list of commits that should be ignored for
|
||||
# `git blame` purposes. This is useful for ignoring commits that only
|
||||
# changed whitespace / indentation / formatting, but did not change
|
||||
# the underlying syntax tree.
|
||||
#
|
||||
# GitHub will pick this up automatically for blame views:
|
||||
# https://docs.github.com/en/repositories/working-with-files/using-files/viewing-a-file#ignore-commits-in-the-blame-view
|
||||
# To use this file locally, run:
|
||||
# git blame --ignore-revs-file .git-blame-ignore-revs
|
||||
# To always use this file by default, run:
|
||||
# git config --local blame.ignoreRevsFile .git-blame-ignore-revs
|
||||
# To disable this functionality, run:
|
||||
# git config --local blame.ignoreRevsFile ""
|
||||
# Comments are optional, but may provide helpful context.
|
||||
|
||||
# 2023-04-20 Set default tab_size for JSON to 2 and apply new formatting
|
||||
# https://github.com/zed-industries/zed/pull/2394
|
||||
eca93c124a488b4e538946cd2d313bd571aa2b86
|
||||
|
||||
# 2024-07-05 Improved formatting of default keymaps (single line per bind)
|
||||
# https://github.com/zed-industries/zed/pull/13887
|
||||
813cc3f5e537372fc86720b5e71b6e1c815440ab
|
||||
187
Cargo.lock
generated
187
Cargo.lock
generated
@@ -87,7 +87,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f6d1ea4484c8676f295307a4892d478c70ac8da1dbd8c7c10830a504b7f1022f"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -110,7 +110,7 @@ version = "0.24.1-dev"
|
||||
source = "git+https://github.com/alacritty/alacritty?rev=cacdb5bb3b72bad2c729227537979d95af75978f#cacdb5bb3b72bad2c729227537979d95af75978f"
|
||||
dependencies = [
|
||||
"base64 0.22.0",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"home",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -1583,7 +1583,16 @@ version = "0.5.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
|
||||
dependencies = [
|
||||
"bit-vec",
|
||||
"bit-vec 0.6.3",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "bit-set"
|
||||
version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f0481a0e032742109b1133a095184ee93d88f3dc9e0d28a5d033dc77a073f44f"
|
||||
dependencies = [
|
||||
"bit-vec 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -1592,6 +1601,12 @@ version = "0.6.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
|
||||
|
||||
[[package]]
|
||||
name = "bit-vec"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d2c54ff287cfc0a34f38a6b832ea1bd8e448a330b3e40a50859e6488bee07f22"
|
||||
|
||||
[[package]]
|
||||
name = "bit_field"
|
||||
version = "0.10.2"
|
||||
@@ -1606,9 +1621,9 @@ checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a"
|
||||
|
||||
[[package]]
|
||||
name = "bitflags"
|
||||
version = "2.4.2"
|
||||
version = "2.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ed570934406eb16438a4e976b1b4500774099c13b8cb96eec99f620f05090ddf"
|
||||
checksum = "b048fb63fd8b5923fc5aa7b340d8e156aec7ec02f0c78fa8a6ddc2613f6f71de"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
@@ -1634,11 +1649,11 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.4.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
|
||||
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"block",
|
||||
"bytemuck",
|
||||
"codespan-reporting",
|
||||
@@ -1664,7 +1679,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
|
||||
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -1674,7 +1689,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.1.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7#21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7"
|
||||
source = "git+https://github.com/zed-industries/blade?rev=a477c2008db27db0b9f745715e119b3ee7ab7818#a477c2008db27db0b9f745715e119b3ee7ab7818"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
@@ -1922,7 +1937,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b99da2f8558ca23c71f4fd15dc57c906239752dd27ff3c00a1d56b685b7cbfec"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"log",
|
||||
"polling 3.3.2",
|
||||
"rustix 0.38.32",
|
||||
@@ -2391,16 +2406,6 @@ dependencies = [
|
||||
"worktree",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clipboard-win"
|
||||
version = "3.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9fdf5e01086b6be750428ba4a40619f847eb2e95756eee84b18e06e5f0b50342"
|
||||
dependencies = [
|
||||
"lazy-bytes-cast",
|
||||
"winapi",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "clock"
|
||||
version = "0.1.0"
|
||||
@@ -2859,7 +2864,7 @@ name = "cosmic-text"
|
||||
version = "0.11.2"
|
||||
source = "git+https://github.com/pop-os/cosmic-text?rev=542b20c#542b20ca4376a3b5de5fa629db1a4ace44e18e0c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"fontdb",
|
||||
"log",
|
||||
"rangemap",
|
||||
@@ -3605,6 +3610,7 @@ dependencies = [
|
||||
"linkify",
|
||||
"log",
|
||||
"lsp",
|
||||
"markdown",
|
||||
"multi_buffer",
|
||||
"ordered-float 2.10.0",
|
||||
"parking_lot",
|
||||
@@ -4012,7 +4018,7 @@ version = "0.12.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7493d4c459da9f84325ad297371a6b2b8a162800873a22e3b6b6512e61d18c05"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bit-set 0.5.3",
|
||||
"regex",
|
||||
]
|
||||
|
||||
@@ -4069,7 +4075,7 @@ name = "feedback"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"client",
|
||||
"db",
|
||||
"editor",
|
||||
@@ -4269,7 +4275,7 @@ checksum = "e32eac81c1135c1df01d4e6d4233c47ba11f6a6d07f33e0bba09d18797077770"
|
||||
dependencies = [
|
||||
"fontconfig-parser",
|
||||
"log",
|
||||
"memmap2 0.9.4",
|
||||
"memmap2",
|
||||
"slotmap",
|
||||
"tinyvec",
|
||||
"ttf-parser",
|
||||
@@ -4403,7 +4409,7 @@ dependencies = [
|
||||
name = "fsevent"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"core-foundation",
|
||||
"fsevent-sys 3.1.0",
|
||||
"parking_lot",
|
||||
@@ -4714,7 +4720,7 @@ version = "0.19.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b903b73e45dc0c6c596f2d37eccece7c1c8bb6e4407b001096387c63d0d93724"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"libc",
|
||||
"libgit2-sys",
|
||||
"log",
|
||||
@@ -4813,6 +4819,7 @@ name = "google_ai"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"chrono",
|
||||
"futures 0.3.28",
|
||||
"http 0.1.0",
|
||||
"serde",
|
||||
@@ -4825,7 +4832,7 @@ version = "0.6.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fbcd2dba93594b227a1f57ee09b8b9da8892c34d55aa332e034a228d0fe6a171"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"gpu-alloc-types",
|
||||
]
|
||||
|
||||
@@ -4846,7 +4853,7 @@ version = "0.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "98ff03b468aa837d70984d55f5d3f846f6ec31fe34bbb97c4f85219caeee1ca4"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4867,7 +4874,6 @@ dependencies = [
|
||||
"calloop",
|
||||
"calloop-wayland-source",
|
||||
"cbindgen",
|
||||
"clipboard-win",
|
||||
"cocoa",
|
||||
"collections",
|
||||
"core-foundation",
|
||||
@@ -5103,7 +5109,7 @@ version = "0.20.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f7acb9683d7c7068aa46d47557bfa4e35a277964b350d9504a87b03610163fd"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"byteorder",
|
||||
"heed-traits",
|
||||
"heed-types",
|
||||
@@ -6052,12 +6058,6 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "lazy-bytes-cast"
|
||||
version = "5.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "10257499f089cd156ad82d0a9cd57d9501fa2c989068992a97eb3c27836f206b"
|
||||
|
||||
[[package]]
|
||||
name = "lazy_static"
|
||||
version = "1.4.0"
|
||||
@@ -6524,15 +6524,6 @@ dependencies = [
|
||||
"rustix 0.38.32",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "43a5a03cefb0d953ec0be133036f14e109412fa594edc2f77227249db66cc3ed"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "memmap2"
|
||||
version = "0.9.4"
|
||||
@@ -6666,17 +6657,17 @@ checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a"
|
||||
|
||||
[[package]]
|
||||
name = "naga"
|
||||
version = "0.14.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ae585df4b6514cf8842ac0f1ab4992edc975892704835b549cf818dc0191249e"
|
||||
version = "0.20.0"
|
||||
source = "git+https://github.com/gfx-rs/wgpu?rev=425526828f738c95ec50b016c6a761bc00d2fb25#425526828f738c95ec50b016c6a761bc00d2fb25"
|
||||
dependencies = [
|
||||
"bit-set",
|
||||
"bitflags 2.4.2",
|
||||
"arrayvec",
|
||||
"bit-set 0.6.0",
|
||||
"bitflags 2.6.0",
|
||||
"cfg_aliases",
|
||||
"codespan-reporting",
|
||||
"hexf-parse",
|
||||
"indexmap 2.2.6",
|
||||
"log",
|
||||
"num-traits",
|
||||
"rustc-hash",
|
||||
"spirv",
|
||||
"termcolor",
|
||||
@@ -6772,7 +6763,7 @@ version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2eb04e9c688eff1c89d72b407f168cf79bb9e867a9d3323ed6c01519eb9cc053"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"libc",
|
||||
"memoffset",
|
||||
@@ -6784,7 +6775,7 @@ version = "0.28.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ab2156c4fce2f8df6c499cc1c763e4394b7482525bf2a9701c9d79d215f519e4"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"cfg_aliases",
|
||||
"libc",
|
||||
@@ -6853,7 +6844,7 @@ version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys 4.1.0",
|
||||
@@ -7066,6 +7057,15 @@ dependencies = [
|
||||
"syn 1.0.109",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "num_threads"
|
||||
version = "0.1.7"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5c7398b9c8b70908f6371f47ed36737907c87c52af34c268fed0bf0ceb92ead9"
|
||||
dependencies = [
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "nvim-rs"
|
||||
version = "0.6.0-pre"
|
||||
@@ -7215,7 +7215,7 @@ version = "0.10.57"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bac25ee399abb46215765b1cb35bc0212377e58a061560d8b29b024fd0430e7c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cfg-if",
|
||||
"foreign-types 0.3.2",
|
||||
"libc",
|
||||
@@ -8183,6 +8183,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"collections",
|
||||
"futures 0.3.28",
|
||||
"prost",
|
||||
"prost-build",
|
||||
"serde",
|
||||
@@ -8229,7 +8230,7 @@ version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "dce76ce678ffc8e5675b22aa1405de0b7037e2fdf8913fea40d1926c6fe1e6e7"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"memchr",
|
||||
"unicase",
|
||||
]
|
||||
@@ -8663,6 +8664,7 @@ dependencies = [
|
||||
"image",
|
||||
"language",
|
||||
"log",
|
||||
"multi_buffer",
|
||||
"project",
|
||||
"runtimelib",
|
||||
"schemars",
|
||||
@@ -9041,7 +9043,7 @@ version = "0.38.32"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "65e04861e65f21776e67888bfbea442b3642beaa0138fdb1dd7a84a52dffdb89"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"errno 0.3.8",
|
||||
"itoa",
|
||||
"libc",
|
||||
@@ -9116,7 +9118,7 @@ version = "0.14.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cfb9cf8877777222e4a3bc7eb247e398b56baba500c38c1c46842431adc8b55c"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"bytemuck",
|
||||
"libm",
|
||||
"smallvec",
|
||||
@@ -9339,7 +9341,7 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"any_vec",
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"client",
|
||||
"collections",
|
||||
"editor",
|
||||
@@ -9767,13 +9769,13 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "simplelog"
|
||||
version = "0.9.0"
|
||||
version = "0.12.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4bc0ffd69814a9b251d43afcabf96dad1b29f5028378056257be9e3fecc9f720"
|
||||
checksum = "16257adbfaef1ee58b1363bdc0664c9b8e1e30aed86049635fb5f147d065a9c0"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"log",
|
||||
"termcolor",
|
||||
"time",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -9926,12 +9928,11 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "spirv"
|
||||
version = "0.2.0+1.5.4"
|
||||
version = "0.3.0+sdk-1.3.268.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "246bfa38fe3db3f1dfc8ca5a2cdeb7348c78be2112740cc0ec8ef18b6d94f830"
|
||||
checksum = "eda41003dc44290527a59b13432d4a0379379fa074b70174882adfbdfd917844"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"num-traits",
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -10107,7 +10108,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.7",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"byteorder",
|
||||
"bytes 1.5.0",
|
||||
"chrono",
|
||||
@@ -10154,7 +10155,7 @@ dependencies = [
|
||||
"atoi",
|
||||
"base64 0.21.7",
|
||||
"bigdecimal",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"byteorder",
|
||||
"chrono",
|
||||
"crc",
|
||||
@@ -10576,7 +10577,7 @@ version = "0.27.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9aef1f9d4c1dbdd1cb3a63be9efd2f04d8ddbc919d46112982c76818ffc2f1a7"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cap-fs-ext",
|
||||
"cap-std",
|
||||
"fd-lock",
|
||||
@@ -10717,9 +10718,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "termcolor"
|
||||
version = "1.1.3"
|
||||
version = "1.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bab24d30b911b2376f3a13cc2cd443142f0c81dda04c118693e35b3835757755"
|
||||
checksum = "06794f8f6c5c898b3275aebefa6b8a1cb24cd2c6c79397ab15774837a0bc5755"
|
||||
dependencies = [
|
||||
"winapi-util",
|
||||
]
|
||||
@@ -10736,6 +10737,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"libc",
|
||||
"rand 0.8.5",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_derive",
|
||||
@@ -10879,18 +10881,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "thiserror"
|
||||
version = "1.0.60"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "579e9083ca58dd9dcf91a9923bb9054071b9ebbd800b342194c9feb0ee89fc18"
|
||||
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
|
||||
dependencies = [
|
||||
"thiserror-impl",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "thiserror-impl"
|
||||
version = "1.0.60"
|
||||
version = "1.0.61"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e2470041c06ec3ac1ab38d0356a6119054dedaea53e12fbefc0de730a1c08524"
|
||||
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -10941,7 +10943,9 @@ checksum = "5dfd88e563464686c916c7e46e623e520ddc6d79fa6641390f2e3fa86e83e885"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
"libc",
|
||||
"num-conv",
|
||||
"num_threads",
|
||||
"powerfmt",
|
||||
"serde",
|
||||
"time-core",
|
||||
@@ -11300,7 +11304,7 @@ version = "0.4.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "61c5bb1d698276a2443e5ecfabc1008bf15a36c12e6a7176e7bf089ea9131140"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"bytes 1.5.0",
|
||||
"futures-core",
|
||||
"futures-util",
|
||||
@@ -12071,7 +12075,7 @@ version = "0.13.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "40eb22ae96f050e0c0d6f7ce43feeae26c348fc4dea56928ca81537cfaa6188b"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"cursor-icon",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -12223,7 +12227,7 @@ version = "0.201.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"indexmap 2.2.6",
|
||||
"semver",
|
||||
]
|
||||
@@ -12490,7 +12494,7 @@ checksum = "371d828b6849ea06d598ae7dd1c316e8dd9e99b76f77d93d5886cb25c7f8e188"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"bytes 1.5.0",
|
||||
"cap-fs-ext",
|
||||
"cap-net-ext",
|
||||
@@ -12577,7 +12581,7 @@ version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "82fb96ee935c2cea6668ccb470fb7771f6215d1691746c2d896b447a00ad3f1f"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"rustix 0.38.32",
|
||||
"wayland-backend",
|
||||
"wayland-scanner",
|
||||
@@ -12600,7 +12604,7 @@ version = "0.31.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "8f81f365b8b4a97f422ac0e8737c438024b5951734506b0e1d775c73030561f4"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-scanner",
|
||||
@@ -12612,7 +12616,7 @@ version = "0.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23803551115ff9ea9bce586860c5c5a971e360825a0309264102a9495a5ff479"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"wayland-backend",
|
||||
"wayland-client",
|
||||
"wayland-protocols",
|
||||
@@ -12731,7 +12735,7 @@ checksum = "ae1136a209614ace00b0c11f04dc7cf42540773be3b22eff6ad165110aba29c1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"thiserror",
|
||||
"tracing",
|
||||
"wasmtime",
|
||||
@@ -13161,7 +13165,7 @@ version = "0.36.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f9643b83820c0cd246ecabe5fa454dd04ba4fa67996369466d0747472d337346"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
@@ -13180,7 +13184,7 @@ version = "0.22.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "288f992ea30e6b5c531b52cdd5f3be81c148554b09ea416f058d16556ba92c27"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"wit-bindgen-rt",
|
||||
"wit-bindgen-rust-macro",
|
||||
]
|
||||
@@ -13236,7 +13240,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
"indexmap 2.2.6",
|
||||
"log",
|
||||
"serde",
|
||||
@@ -13442,18 +13446,17 @@ name = "xim-parser"
|
||||
version = "0.2.1"
|
||||
source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
|
||||
dependencies = [
|
||||
"bitflags 2.4.2",
|
||||
"bitflags 2.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "xkbcommon"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "13867d259930edc7091a6c41b4ce6eee464328c6ff9659b7e4c668ca20d4c91e"
|
||||
source = "git+https://github.com/ConradIrwin/xkbcommon-rs?rev=2d4c4439160c7846ede0f0ece93bf73b1e613339#2d4c4439160c7846ede0f0ece93bf73b1e613339"
|
||||
dependencies = [
|
||||
"as-raw-xcb-connection",
|
||||
"libc",
|
||||
"memmap2 0.8.0",
|
||||
"memmap2",
|
||||
"xkeysym",
|
||||
]
|
||||
|
||||
@@ -13714,7 +13717,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_dart"
|
||||
version = "0.0.2"
|
||||
version = "0.0.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
@@ -13864,7 +13867,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_svelte"
|
||||
version = "0.0.1"
|
||||
version = "0.0.2"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.0.6",
|
||||
]
|
||||
|
||||
10
Cargo.toml
10
Cargo.toml
@@ -284,10 +284,10 @@ async-trait = "0.1"
|
||||
async-watch = "0.3.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
base64 = "0.13"
|
||||
bitflags = "2.4.2"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "21a56f780e21e4cb42c70a1dcf4b59842d1ad7f7" }
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
blade-macros = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
blade-util = { git = "https://github.com/zed-industries/blade", rev = "a477c2008db27db0b9f745715e119b3ee7ab7818" }
|
||||
cap-std = "3.0"
|
||||
cargo_toml = "0.20"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
@@ -365,6 +365,7 @@ shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
signal-hook = "0.3.17"
|
||||
similar = "1.3"
|
||||
simplelog = "0.12.2"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
smol = "1.2"
|
||||
strum = { version = "0.25.0", features = ["derive"] }
|
||||
@@ -451,6 +452,7 @@ features = [
|
||||
"Win32_System_Com_StructuredStorage",
|
||||
"Win32_System_DataExchange",
|
||||
"Win32_System_LibraryLoader",
|
||||
"Win32_System_Memory",
|
||||
"Win32_System_Ole",
|
||||
"Win32_System_SystemInformation",
|
||||
"Win32_System_SystemServices",
|
||||
|
||||
@@ -100,6 +100,7 @@
|
||||
"ctrl-k ctrl-r": "editor::RevertSelectedHunks",
|
||||
"ctrl-'": "editor::ToggleHunkDiff",
|
||||
"ctrl-\"": "editor::ExpandAllHunkDiffs",
|
||||
"ctrl-i": "editor::ShowSignatureHelp",
|
||||
"alt-g b": "editor::ToggleGitBlame"
|
||||
}
|
||||
},
|
||||
@@ -397,7 +398,7 @@
|
||||
"bindings": {
|
||||
"ctrl-shift-k": "editor::DeleteLine",
|
||||
"ctrl-shift-d": "editor::DuplicateLineDown",
|
||||
"ctrl-j": "editor::JoinLines",
|
||||
"ctrl-shift-j": "editor::JoinLines",
|
||||
"ctrl-alt-backspace": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-h": "editor::DeleteToPreviousSubwordStart",
|
||||
"ctrl-alt-delete": "editor::DeleteToNextSubwordEnd",
|
||||
|
||||
@@ -126,7 +126,8 @@
|
||||
"cmd-alt-z": "editor::RevertSelectedHunks",
|
||||
"cmd-'": "editor::ToggleHunkDiff",
|
||||
"cmd-\"": "editor::ExpandAllHunkDiffs",
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame"
|
||||
"cmd-alt-g b": "editor::ToggleGitBlame",
|
||||
"cmd-i": "editor::ShowSignatureHelp"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -24,6 +24,8 @@
|
||||
"ctrl-shift-f12": "editor::FindAllReferences",
|
||||
"ctrl-.": "editor::GoToHunk",
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"ctrl-k ctrl-u": "editor::ConvertToUpperCase",
|
||||
"ctrl-k ctrl-l": "editor::ConvertToLowerCase",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"ctrl-,": "editor::GoToPrevHunk",
|
||||
"cmd-k cmd-u": "editor::ConvertToUpperCase",
|
||||
"cmd-k cmd-l": "editor::ConvertToLowerCase",
|
||||
"cmd-shift-j": "editor::JoinLines",
|
||||
"shift-alt-m": "markdown::OpenPreviewToTheSide",
|
||||
"ctrl-backspace": "editor::DeleteToPreviousWordStart",
|
||||
"ctrl-delete": "editor::DeleteToNextWordEnd"
|
||||
|
||||
@@ -1,12 +1,6 @@
|
||||
[
|
||||
{
|
||||
"context": "ProjectPanel || Editor",
|
||||
"bindings": {
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimControl && !VimWaiting && !menu",
|
||||
"context": "VimControl && !menu",
|
||||
"bindings": {
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
@@ -18,6 +12,8 @@
|
||||
"down": "vim::Down",
|
||||
"enter": "vim::NextLineStart",
|
||||
"ctrl-m": "vim::NextLineStart",
|
||||
"+": "vim::NextLineStart",
|
||||
"-": "vim::PreviousLineStart",
|
||||
"tab": "vim::Tab",
|
||||
"shift-tab": "vim::Tab",
|
||||
"k": "vim::Up",
|
||||
@@ -198,20 +194,20 @@
|
||||
"ctrl-w g shift-d": "editor::GoToTypeDefinitionSplit",
|
||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||
"-": "pane::RevealInProjectPanel"
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
// escape is in its own section so that it cancels a pending count.
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"context": "VimControl && VimCount",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
"escape": "editor::Cancel",
|
||||
"ctrl-[": "editor::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"ctrl-[": "editor::Cancel",
|
||||
".": "vim::Repeat",
|
||||
"c": ["vim::PushOperator", "Change"],
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
@@ -255,127 +251,12 @@
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPrevDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
"[ c": "editor::GoToPrevHunk"
|
||||
"[ c": "editor::GoToPrevHunk",
|
||||
"g c c": "vim::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual && vim_operator == none && !VimWaiting",
|
||||
"bindings": {
|
||||
"\"": ["vim::PushOperator", "Register"],
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimCount && vim_mode != insert",
|
||||
"bindings": {
|
||||
"0": ["vim::Number", 0]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == c",
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine",
|
||||
"d": "editor::Rename" // zed specific
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == c",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == gu",
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == gU",
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == g~",
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == d",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && vim_operator == y",
|
||||
"bindings": {
|
||||
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == ys",
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == >",
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_operator == <",
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && VimObject",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
|
||||
"t": "vim::Tag",
|
||||
"s": "vim::Sentence",
|
||||
"p": "vim::Paragraph",
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"{": "vim::CurlyBrackets",
|
||||
"}": "vim::CurlyBrackets",
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual && !VimWaiting && !VimObject",
|
||||
"context": "vim_mode == visual",
|
||||
"bindings": {
|
||||
"u": "vim::ConvertToLowerCase",
|
||||
"U": "vim::ConvertToUpperCase",
|
||||
@@ -410,23 +291,16 @@
|
||||
">": "vim::Indent",
|
||||
"<": "vim::Outdent",
|
||||
"i": ["vim::PushOperator", { "Object": { "around": false } }],
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }]
|
||||
"a": ["vim::PushOperator", { "Object": { "around": true } }],
|
||||
"g c": "vim::ToggleComments",
|
||||
"\"": ["vim::PushOperator", "Register"],
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == normal && !VimWaiting",
|
||||
"bindings": {
|
||||
"g c c": "vim::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == visual",
|
||||
"bindings": {
|
||||
"g c": "vim::ToggleComments"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert",
|
||||
"context": "vim_mode == insert",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
@@ -445,30 +319,115 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == replace",
|
||||
"context": "vim_mode == replace",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-c": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore",
|
||||
"backspace": "vim::UndoReplace",
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"backspace": "vim::UndoReplace"
|
||||
"enter": "vim::Enter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode != replace && VimWaiting",
|
||||
"context": "vim_mode == waiting",
|
||||
"bindings": {
|
||||
"tab": "vim::Tab",
|
||||
"enter": "vim::Enter",
|
||||
"escape": ["vim::SwitchMode", "Normal"],
|
||||
"ctrl-[": ["vim::SwitchMode", "Normal"]
|
||||
"enter": "vim::Enter"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && vim_mode == insert && VimWaiting",
|
||||
"context": "vim_mode == operator",
|
||||
"bindings": {
|
||||
"escape": "vim::NormalBefore",
|
||||
"ctrl-[": "vim::NormalBefore"
|
||||
"escape": "vim::ClearOperators",
|
||||
"ctrl-c": "vim::ClearOperators",
|
||||
"ctrl-[": "vim::ClearOperators"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignorePunctuation": true }],
|
||||
"t": "vim::Tag",
|
||||
"s": "vim::Sentence",
|
||||
"p": "vim::Paragraph",
|
||||
"'": "vim::Quotes",
|
||||
"`": "vim::BackQuotes",
|
||||
"\"": "vim::DoubleQuotes",
|
||||
"|": "vim::VerticalBars",
|
||||
"(": "vim::Parentheses",
|
||||
")": "vim::Parentheses",
|
||||
"b": "vim::Parentheses",
|
||||
"[": "vim::SquareBrackets",
|
||||
"]": "vim::SquareBrackets",
|
||||
"{": "vim::CurlyBrackets",
|
||||
"}": "vim::CurlyBrackets",
|
||||
"shift-b": "vim::CurlyBrackets",
|
||||
"<": "vim::AngleBrackets",
|
||||
">": "vim::AngleBrackets",
|
||||
"a": "vim::Argument"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == c",
|
||||
"bindings": {
|
||||
"c": "vim::CurrentLine",
|
||||
"d": "editor::Rename", // zed specific
|
||||
"s": ["vim::PushOperator", { "ChangeSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == d",
|
||||
"bindings": {
|
||||
"d": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", "DeleteSurrounds"]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gu",
|
||||
"bindings": {
|
||||
"g u": "vim::CurrentLine",
|
||||
"u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == gU",
|
||||
"bindings": {
|
||||
"g shift-u": "vim::CurrentLine",
|
||||
"shift-u": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == g~",
|
||||
"bindings": {
|
||||
"g ~": "vim::CurrentLine",
|
||||
"~": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == y",
|
||||
"bindings": {
|
||||
"y": "vim::CurrentLine",
|
||||
"s": ["vim::PushOperator", { "AddSurrounds": {} }]
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == ys",
|
||||
"bindings": {
|
||||
"s": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == >",
|
||||
"bindings": {
|
||||
">": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == <",
|
||||
"bindings": {
|
||||
"<": "vim::CurrentLine"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -508,7 +467,8 @@
|
||||
"x": "project_panel::RevealInFileManager",
|
||||
"shift-g": "menu::SelectLast",
|
||||
"g g": "menu::SelectFirst",
|
||||
"-": "project_panel::SelectParent"
|
||||
"-": "project_panel::SelectParent",
|
||||
"ctrl-6": "pane::AlternateFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -47,7 +47,7 @@
|
||||
// },
|
||||
"buffer_line_height": "comfortable",
|
||||
// The name of a font to use for rendering text in the UI
|
||||
// (On macOS) You can set this to ".SysmtemUIFont" to use the system font
|
||||
// (On macOS) You can set this to ".SystemUIFont" to use the system font
|
||||
"ui_font_family": "Zed Plex Sans",
|
||||
// The OpenType features to enable for text in the UI
|
||||
"ui_font_features": {
|
||||
@@ -116,6 +116,11 @@
|
||||
// The debounce delay before re-querying the language server for completion
|
||||
// documentation when not included in original completion list.
|
||||
"completion_documentation_secondary_query_debounce": 300,
|
||||
// Show method signatures in the editor, when inside parentheses.
|
||||
"auto_signature_help": false,
|
||||
/// Whether to show the signature help after completion or a bracket pair inserted.
|
||||
/// If `auto_signature_help` is enabled, this setting will be treated as enabled also.
|
||||
"show_signature_help_after_edits": true,
|
||||
// Whether to show wrap guides (vertical rulers) in the editor.
|
||||
// Setting this to true will show a guide at the 'preferred_line_length' value
|
||||
// if softwrap is set to 'preferred_line_length', and will show any
|
||||
@@ -128,14 +133,7 @@
|
||||
// The default number of lines to expand excerpts in the multibuffer by.
|
||||
"expand_excerpt_lines": 3,
|
||||
// Globs to match against file paths to determine if a file is private.
|
||||
"private_files": [
|
||||
"**/.env*",
|
||||
"**/*.pem",
|
||||
"**/*.key",
|
||||
"**/*.cert",
|
||||
"**/*.crt",
|
||||
"**/secrets.yml"
|
||||
],
|
||||
"private_files": ["**/.env*", "**/*.pem", "**/*.key", "**/*.cert", "**/*.crt", "**/secrets.yml"],
|
||||
// Whether to use additional LSP queries to format (and amend) the code after
|
||||
// every "trigger" symbol input, defined by LSP server capabilities.
|
||||
"use_on_type_format": true,
|
||||
@@ -262,6 +260,8 @@
|
||||
// to both the horizontal and vertical delta values while scrolling.
|
||||
"scroll_sensitivity": 1.0,
|
||||
"relative_line_numbers": false,
|
||||
// If 'search_wrap' is disabled, search result do not wrap around the end of the file.
|
||||
"search_wrap": true,
|
||||
// When to populate a new search's query based on the text under the cursor.
|
||||
// This setting can take the following three values:
|
||||
//
|
||||
@@ -539,6 +539,14 @@
|
||||
// "delay_ms": 600
|
||||
}
|
||||
},
|
||||
// Configuration for how direnv configuration should be loaded. May take 2 values:
|
||||
// 1. Load direnv configuration through the shell hook, works for POSIX shells and fish.
|
||||
// "load_direnv": "shell_hook"
|
||||
// 2. Load direnv configuration using `direnv export json` directly.
|
||||
// This can help with some shells that otherwise would not detect
|
||||
// the direnv environment, such as nushell or elvish.
|
||||
// "load_direnv": "direct"
|
||||
"load_direnv": "shell_hook",
|
||||
"inline_completions": {
|
||||
// A list of globs representing files that inline completions should be disabled for.
|
||||
"disabled_globs": [".env"]
|
||||
@@ -714,10 +722,12 @@
|
||||
}
|
||||
},
|
||||
"C": {
|
||||
"format_on_save": "off"
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false
|
||||
},
|
||||
"C++": {
|
||||
"format_on_save": "off"
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false
|
||||
},
|
||||
"CSS": {
|
||||
"prettier": {
|
||||
@@ -769,6 +779,7 @@
|
||||
},
|
||||
"Markdown": {
|
||||
"format_on_save": "off",
|
||||
"use_on_type_format": false,
|
||||
"prettier": {
|
||||
"allowed": true
|
||||
}
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#10a793ff",
|
||||
"status_bar.background": "#262933ff",
|
||||
"title_bar.background": "#262933ff",
|
||||
"title_bar.inactive_background": "#21242bff",
|
||||
"toolbar.background": "#1e2025ff",
|
||||
"tab_bar.background": "#21242bff",
|
||||
"tab.inactive_background": "#21242bff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#566ddaff",
|
||||
"status_bar.background": "#3a353fff",
|
||||
"title_bar.background": "#3a353fff",
|
||||
"title_bar.inactive_background": "#221f26ff",
|
||||
"toolbar.background": "#19171cff",
|
||||
"tab_bar.background": "#221f26ff",
|
||||
"tab.inactive_background": "#221f26ff",
|
||||
@@ -422,6 +423,7 @@
|
||||
"icon.accent": "#586cdaff",
|
||||
"status_bar.background": "#bfbcc5ff",
|
||||
"title_bar.background": "#bfbcc5ff",
|
||||
"title_bar.inactive_background": "#e6e3ebff",
|
||||
"toolbar.background": "#efecf4ff",
|
||||
"tab_bar.background": "#e6e3ebff",
|
||||
"tab.inactive_background": "#e6e3ebff",
|
||||
@@ -806,6 +808,7 @@
|
||||
"icon.accent": "#6684e0ff",
|
||||
"status_bar.background": "#45433bff",
|
||||
"title_bar.background": "#45433bff",
|
||||
"title_bar.inactive_background": "#262622ff",
|
||||
"toolbar.background": "#20201dff",
|
||||
"tab_bar.background": "#262622ff",
|
||||
"tab.inactive_background": "#262622ff",
|
||||
@@ -1190,6 +1193,7 @@
|
||||
"icon.accent": "#6684dfff",
|
||||
"status_bar.background": "#cecab4ff",
|
||||
"title_bar.background": "#cecab4ff",
|
||||
"title_bar.inactive_background": "#eeebd7ff",
|
||||
"toolbar.background": "#fefbecff",
|
||||
"tab_bar.background": "#eeebd7ff",
|
||||
"tab.inactive_background": "#eeebd7ff",
|
||||
@@ -1574,6 +1578,7 @@
|
||||
"icon.accent": "#36a165ff",
|
||||
"status_bar.background": "#424136ff",
|
||||
"title_bar.background": "#424136ff",
|
||||
"title_bar.inactive_background": "#2c2b23ff",
|
||||
"toolbar.background": "#22221bff",
|
||||
"tab_bar.background": "#2c2b23ff",
|
||||
"tab.inactive_background": "#2c2b23ff",
|
||||
@@ -1958,6 +1963,7 @@
|
||||
"icon.accent": "#37a165ff",
|
||||
"status_bar.background": "#c5c4b9ff",
|
||||
"title_bar.background": "#c5c4b9ff",
|
||||
"title_bar.inactive_background": "#ebeae3ff",
|
||||
"toolbar.background": "#f4f3ecff",
|
||||
"tab_bar.background": "#ebeae3ff",
|
||||
"tab.inactive_background": "#ebeae3ff",
|
||||
@@ -2342,6 +2348,7 @@
|
||||
"icon.accent": "#407ee6ff",
|
||||
"status_bar.background": "#443c39ff",
|
||||
"title_bar.background": "#443c39ff",
|
||||
"title_bar.inactive_background": "#27211eff",
|
||||
"toolbar.background": "#1b1918ff",
|
||||
"tab_bar.background": "#27211eff",
|
||||
"tab.inactive_background": "#27211eff",
|
||||
@@ -2726,6 +2733,7 @@
|
||||
"icon.accent": "#407ee6ff",
|
||||
"status_bar.background": "#ccc7c5ff",
|
||||
"title_bar.background": "#ccc7c5ff",
|
||||
"title_bar.inactive_background": "#e9e6e4ff",
|
||||
"toolbar.background": "#f0eeedff",
|
||||
"tab_bar.background": "#e9e6e4ff",
|
||||
"tab.inactive_background": "#e9e6e4ff",
|
||||
@@ -3110,6 +3118,7 @@
|
||||
"icon.accent": "#5169ebff",
|
||||
"status_bar.background": "#433a43ff",
|
||||
"title_bar.background": "#433a43ff",
|
||||
"title_bar.inactive_background": "#252025ff",
|
||||
"toolbar.background": "#1b181bff",
|
||||
"tab_bar.background": "#252025ff",
|
||||
"tab.inactive_background": "#252025ff",
|
||||
@@ -3494,6 +3503,7 @@
|
||||
"icon.accent": "#5169ebff",
|
||||
"status_bar.background": "#c6b8c6ff",
|
||||
"title_bar.background": "#c6b8c6ff",
|
||||
"title_bar.inactive_background": "#e0d5e0ff",
|
||||
"toolbar.background": "#f7f3f7ff",
|
||||
"tab_bar.background": "#e0d5e0ff",
|
||||
"tab.inactive_background": "#e0d5e0ff",
|
||||
@@ -3878,6 +3888,7 @@
|
||||
"icon.accent": "#267eadff",
|
||||
"status_bar.background": "#33444dff",
|
||||
"title_bar.background": "#33444dff",
|
||||
"title_bar.inactive_background": "#1c2529ff",
|
||||
"toolbar.background": "#161b1dff",
|
||||
"tab_bar.background": "#1c2529ff",
|
||||
"tab.inactive_background": "#1c2529ff",
|
||||
@@ -4262,6 +4273,7 @@
|
||||
"icon.accent": "#267eadff",
|
||||
"status_bar.background": "#a6cadcff",
|
||||
"title_bar.background": "#a6cadcff",
|
||||
"title_bar.inactive_background": "#cdeaf9ff",
|
||||
"toolbar.background": "#ebf8ffff",
|
||||
"tab_bar.background": "#cdeaf9ff",
|
||||
"tab.inactive_background": "#cdeaf9ff",
|
||||
@@ -4646,6 +4658,7 @@
|
||||
"icon.accent": "#7272caff",
|
||||
"status_bar.background": "#3b3535ff",
|
||||
"title_bar.background": "#3b3535ff",
|
||||
"title_bar.inactive_background": "#252020ff",
|
||||
"toolbar.background": "#1b1818ff",
|
||||
"tab_bar.background": "#252020ff",
|
||||
"tab.inactive_background": "#252020ff",
|
||||
@@ -5030,6 +5043,7 @@
|
||||
"icon.accent": "#7272caff",
|
||||
"status_bar.background": "#c1bbbbff",
|
||||
"title_bar.background": "#c1bbbbff",
|
||||
"title_bar.inactive_background": "#ebe3e3ff",
|
||||
"toolbar.background": "#f4ececff",
|
||||
"tab_bar.background": "#ebe3e3ff",
|
||||
"tab.inactive_background": "#ebe3e3ff",
|
||||
@@ -5414,6 +5428,7 @@
|
||||
"icon.accent": "#468b8fff",
|
||||
"status_bar.background": "#353f39ff",
|
||||
"title_bar.background": "#353f39ff",
|
||||
"title_bar.inactive_background": "#1f2621ff",
|
||||
"toolbar.background": "#171c19ff",
|
||||
"tab_bar.background": "#1f2621ff",
|
||||
"tab.inactive_background": "#1f2621ff",
|
||||
@@ -5798,6 +5813,7 @@
|
||||
"icon.accent": "#488b90ff",
|
||||
"status_bar.background": "#bcc5bfff",
|
||||
"title_bar.background": "#bcc5bfff",
|
||||
"title_bar.inactive_background": "#e3ebe6ff",
|
||||
"toolbar.background": "#ecf4eeff",
|
||||
"tab_bar.background": "#e3ebe6ff",
|
||||
"tab.inactive_background": "#e3ebe6ff",
|
||||
@@ -6182,6 +6198,7 @@
|
||||
"icon.accent": "#3e62f4ff",
|
||||
"status_bar.background": "#3b453bff",
|
||||
"title_bar.background": "#3b453bff",
|
||||
"title_bar.inactive_background": "#1f231fff",
|
||||
"toolbar.background": "#131513ff",
|
||||
"tab_bar.background": "#1f231fff",
|
||||
"tab.inactive_background": "#1f231fff",
|
||||
@@ -6566,6 +6583,7 @@
|
||||
"icon.accent": "#3e61f4ff",
|
||||
"status_bar.background": "#b4ceb4ff",
|
||||
"title_bar.background": "#b4ceb4ff",
|
||||
"title_bar.inactive_background": "#daeedaff",
|
||||
"toolbar.background": "#f3faf3ff",
|
||||
"tab_bar.background": "#daeedaff",
|
||||
"tab.inactive_background": "#daeedaff",
|
||||
@@ -6950,6 +6968,7 @@
|
||||
"icon.accent": "#3e8ed0ff",
|
||||
"status_bar.background": "#3e4769ff",
|
||||
"title_bar.background": "#3e4769ff",
|
||||
"title_bar.inactive_background": "#262f51ff",
|
||||
"toolbar.background": "#202646ff",
|
||||
"tab_bar.background": "#262f51ff",
|
||||
"tab.inactive_background": "#262f51ff",
|
||||
@@ -7334,6 +7353,7 @@
|
||||
"icon.accent": "#3e8fd0ff",
|
||||
"status_bar.background": "#c1c5d8ff",
|
||||
"title_bar.background": "#c1c5d8ff",
|
||||
"title_bar.inactive_background": "#e5e8f5ff",
|
||||
"toolbar.background": "#f5f7ffff",
|
||||
"tab_bar.background": "#e5e8f5ff",
|
||||
"tab.inactive_background": "#e5e8f5ff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#5ac1feff",
|
||||
"status_bar.background": "#313337ff",
|
||||
"title_bar.background": "#313337ff",
|
||||
"title_bar.inactive_background": "#1f2127ff",
|
||||
"toolbar.background": "#0d1016ff",
|
||||
"tab_bar.background": "#1f2127ff",
|
||||
"tab.inactive_background": "#1f2127ff",
|
||||
@@ -407,6 +408,7 @@
|
||||
"icon.accent": "#3b9ee5ff",
|
||||
"status_bar.background": "#dcdddeff",
|
||||
"title_bar.background": "#dcdddeff",
|
||||
"title_bar.inactive_background": "#ececedff",
|
||||
"toolbar.background": "#fcfcfcff",
|
||||
"tab_bar.background": "#ececedff",
|
||||
"tab.inactive_background": "#ececedff",
|
||||
@@ -776,6 +778,7 @@
|
||||
"icon.accent": "#72cffeff",
|
||||
"status_bar.background": "#464a52ff",
|
||||
"title_bar.background": "#464a52ff",
|
||||
"title_bar.inactive_background": "#353944ff",
|
||||
"toolbar.background": "#242835ff",
|
||||
"tab_bar.background": "#353944ff",
|
||||
"tab.inactive_background": "#353944ff",
|
||||
|
||||
@@ -47,6 +47,7 @@
|
||||
"icon.accent": "#83a598ff",
|
||||
"status_bar.background": "#4c4642ff",
|
||||
"title_bar.background": "#4c4642ff",
|
||||
"title_bar.inactive_background": "#3a3735ff",
|
||||
"toolbar.background": "#282828ff",
|
||||
"tab_bar.background": "#3a3735ff",
|
||||
"tab.inactive_background": "#3a3735ff",
|
||||
@@ -430,6 +431,7 @@
|
||||
"icon.accent": "#83a598ff",
|
||||
"status_bar.background": "#4c4642ff",
|
||||
"title_bar.background": "#4c4642ff",
|
||||
"title_bar.inactive_background": "#393634ff",
|
||||
"toolbar.background": "#1d2021ff",
|
||||
"tab_bar.background": "#393634ff",
|
||||
"tab.inactive_background": "#393634ff",
|
||||
@@ -813,6 +815,7 @@
|
||||
"icon.accent": "#83a598ff",
|
||||
"status_bar.background": "#4c4642ff",
|
||||
"title_bar.background": "#4c4642ff",
|
||||
"title_bar.inactive_background": "#3b3735ff",
|
||||
"toolbar.background": "#32302fff",
|
||||
"tab_bar.background": "#3b3735ff",
|
||||
"tab.inactive_background": "#3b3735ff",
|
||||
@@ -1196,6 +1199,7 @@
|
||||
"icon.accent": "#0b6678ff",
|
||||
"status_bar.background": "#d9c8a4ff",
|
||||
"title_bar.background": "#d9c8a4ff",
|
||||
"title_bar.inactive_background": "#ecddb4ff",
|
||||
"toolbar.background": "#fbf1c7ff",
|
||||
"tab_bar.background": "#ecddb4ff",
|
||||
"tab.inactive_background": "#ecddb4ff",
|
||||
@@ -1579,6 +1583,7 @@
|
||||
"icon.accent": "#0b6678ff",
|
||||
"status_bar.background": "#d9c8a4ff",
|
||||
"title_bar.background": "#d9c8a4ff",
|
||||
"title_bar.inactive_background": "#ecddb5ff",
|
||||
"toolbar.background": "#f9f5d7ff",
|
||||
"tab_bar.background": "#ecddb5ff",
|
||||
"tab.inactive_background": "#ecddb5ff",
|
||||
@@ -1962,6 +1967,7 @@
|
||||
"icon.accent": "#0b6678ff",
|
||||
"status_bar.background": "#d9c8a4ff",
|
||||
"title_bar.background": "#d9c8a4ff",
|
||||
"title_bar.inactive_background": "#ecdcb3ff",
|
||||
"toolbar.background": "#f2e5bcff",
|
||||
"tab_bar.background": "#ecdcb3ff",
|
||||
"tab.inactive_background": "#ecdcb3ff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#74ade8ff",
|
||||
"status_bar.background": "#3b414dff",
|
||||
"title_bar.background": "#3b414dff",
|
||||
"title_bar.inactive_background": "#2e343eff",
|
||||
"toolbar.background": "#282c33ff",
|
||||
"tab_bar.background": "#2f343eff",
|
||||
"tab.inactive_background": "#2f343eff",
|
||||
@@ -412,6 +413,7 @@
|
||||
"icon.accent": "#5c78e2ff",
|
||||
"status_bar.background": "#dcdcddff",
|
||||
"title_bar.background": "#dcdcddff",
|
||||
"title_bar.inactive_background": "#ebebecff",
|
||||
"toolbar.background": "#fafafaff",
|
||||
"tab_bar.background": "#ebebecff",
|
||||
"tab.inactive_background": "#ebebecff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#9bced6ff",
|
||||
"status_bar.background": "#292738ff",
|
||||
"title_bar.background": "#292738ff",
|
||||
"title_bar.inactive_background": "#1c1b2aff",
|
||||
"toolbar.background": "#191724ff",
|
||||
"tab_bar.background": "#1c1b2aff",
|
||||
"tab.inactive_background": "#1c1b2aff",
|
||||
@@ -417,6 +418,7 @@
|
||||
"icon.accent": "#57949fff",
|
||||
"status_bar.background": "#dcd8d8ff",
|
||||
"title_bar.background": "#dcd8d8ff",
|
||||
"title_bar.inactive_background": "#fef9f2ff",
|
||||
"toolbar.background": "#faf4edff",
|
||||
"tab_bar.background": "#fef9f2ff",
|
||||
"tab.inactive_background": "#fef9f2ff",
|
||||
@@ -796,6 +798,7 @@
|
||||
"icon.accent": "#9bced6ff",
|
||||
"status_bar.background": "#38354eff",
|
||||
"title_bar.background": "#38354eff",
|
||||
"title_bar.inactive_background": "#28253cff",
|
||||
"toolbar.background": "#232136ff",
|
||||
"tab_bar.background": "#28253cff",
|
||||
"tab.inactive_background": "#28253cff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#518b8bff",
|
||||
"status_bar.background": "#333944ff",
|
||||
"title_bar.background": "#333944ff",
|
||||
"title_bar.inactive_background": "#2b3038ff",
|
||||
"toolbar.background": "#282c33ff",
|
||||
"tab_bar.background": "#2b3038ff",
|
||||
"tab.inactive_background": "#2b3038ff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#278ad1ff",
|
||||
"status_bar.background": "#073743ff",
|
||||
"title_bar.background": "#073743ff",
|
||||
"title_bar.inactive_background": "#04313bff",
|
||||
"toolbar.background": "#002a35ff",
|
||||
"tab_bar.background": "#04313bff",
|
||||
"tab.inactive_background": "#04313bff",
|
||||
@@ -407,6 +408,7 @@
|
||||
"icon.accent": "#288bd1ff",
|
||||
"status_bar.background": "#cfd0c4ff",
|
||||
"title_bar.background": "#cfd0c4ff",
|
||||
"title_bar.inactive_background": "#f3eddaff",
|
||||
"toolbar.background": "#fdf6e3ff",
|
||||
"tab_bar.background": "#f3eddaff",
|
||||
"tab.inactive_background": "#f3eddaff",
|
||||
|
||||
@@ -38,6 +38,7 @@
|
||||
"icon.accent": "#499befff",
|
||||
"status_bar.background": "#2a261cff",
|
||||
"title_bar.background": "#2a261cff",
|
||||
"title_bar.inactive_background": "#231f16ff",
|
||||
"toolbar.background": "#1b1810ff",
|
||||
"tab_bar.background": "#231f16ff",
|
||||
"tab.inactive_background": "#231f16ff",
|
||||
|
||||
@@ -189,8 +189,12 @@ pub struct LanguageModelRequest {
|
||||
pub messages: Vec<LanguageModelRequestMessage>,
|
||||
pub stop: Vec<String>,
|
||||
pub temperature: f32,
|
||||
pub cached_contents: Vec<CachedContentId>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize)]
|
||||
pub struct CachedContentId(String);
|
||||
|
||||
impl LanguageModelRequest {
|
||||
pub fn to_proto(&self) -> proto::CompleteWithLanguageModel {
|
||||
proto::CompleteWithLanguageModel {
|
||||
@@ -200,6 +204,7 @@ impl LanguageModelRequest {
|
||||
temperature: self.temperature,
|
||||
tool_choice: None,
|
||||
tools: Vec::new(),
|
||||
cached_contents: self.cached_contents.iter().map(|id| id.0.clone()).collect(),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2445,19 +2445,43 @@ fn render_docs_slash_command_trailer(
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
if !store.is_indexing(&package) {
|
||||
let mut children = Vec::new();
|
||||
|
||||
if store.is_indexing(&package) {
|
||||
children.push(
|
||||
div()
|
||||
.id(("crates-being-indexed", row.0))
|
||||
.child(Icon::new(IconName::ArrowCircle).with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
))
|
||||
.tooltip({
|
||||
let package = package.clone();
|
||||
move |cx| Tooltip::text(format!("Indexing {package}…"), cx)
|
||||
})
|
||||
.into_any_element(),
|
||||
);
|
||||
}
|
||||
|
||||
if let Some(latest_error) = store.latest_error_for_package(&package) {
|
||||
children.push(
|
||||
div()
|
||||
.id(("latest-error", row.0))
|
||||
.child(Icon::new(IconName::ExclamationTriangle).color(Color::Warning))
|
||||
.tooltip(move |cx| Tooltip::text(format!("failed to index: {latest_error}"), cx))
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
|
||||
let is_indexing = store.is_indexing(&package);
|
||||
let latest_error = store.latest_error_for_package(&package);
|
||||
|
||||
if !is_indexing && latest_error.is_none() {
|
||||
return Empty.into_any();
|
||||
}
|
||||
|
||||
div()
|
||||
.id(("crates-being-indexed", row.0))
|
||||
.child(Icon::new(IconName::ArrowCircle).with_animation(
|
||||
"arrow-circle",
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
|icon, delta| icon.transform(Transformation::rotate(percentage(delta))),
|
||||
))
|
||||
.tooltip(move |cx| Tooltip::text(format!("Indexing {package}…"), cx))
|
||||
.into_any_element()
|
||||
h_flex().gap_2().children(children).into_any_element()
|
||||
}
|
||||
|
||||
fn make_lsp_adapter_delegate(
|
||||
|
||||
@@ -27,6 +27,8 @@ pub enum CloudModel {
|
||||
Claude3Opus,
|
||||
Claude3Sonnet,
|
||||
Claude3Haiku,
|
||||
Gemini15Pro,
|
||||
Gemini15Flash,
|
||||
Custom(String),
|
||||
}
|
||||
|
||||
@@ -109,6 +111,8 @@ impl CloudModel {
|
||||
Self::Claude3Opus => "claude-3-opus",
|
||||
Self::Claude3Sonnet => "claude-3-sonnet",
|
||||
Self::Claude3Haiku => "claude-3-haiku",
|
||||
Self::Gemini15Pro => "gemini-1.5-pro",
|
||||
Self::Gemini15Flash => "gemini-1.5-flash",
|
||||
Self::Custom(id) => id,
|
||||
}
|
||||
}
|
||||
@@ -123,6 +127,8 @@ impl CloudModel {
|
||||
Self::Claude3Opus => "Claude 3 Opus",
|
||||
Self::Claude3Sonnet => "Claude 3 Sonnet",
|
||||
Self::Claude3Haiku => "Claude 3 Haiku",
|
||||
Self::Gemini15Pro => "Gemini 1.5 Pro",
|
||||
Self::Gemini15Flash => "Gemini 1.5 Flash",
|
||||
Self::Custom(id) => id.as_str(),
|
||||
}
|
||||
}
|
||||
@@ -136,6 +142,8 @@ impl CloudModel {
|
||||
| Self::Claude3Opus
|
||||
| Self::Claude3Sonnet
|
||||
| Self::Claude3Haiku => 200000,
|
||||
Self::Gemini15Pro => 128000,
|
||||
Self::Gemini15Flash => 32000,
|
||||
Self::Custom(_) => 4096, // TODO: Make this configurable
|
||||
}
|
||||
}
|
||||
|
||||
@@ -152,6 +152,11 @@ impl LanguageModelCompletionProvider for CloudCompletionProvider {
|
||||
temperature: request.temperature,
|
||||
tools: Vec::new(),
|
||||
tool_choice: None,
|
||||
cached_contents: request
|
||||
.cached_contents
|
||||
.iter()
|
||||
.map(|id| id.0.clone())
|
||||
.collect(),
|
||||
};
|
||||
|
||||
self.client
|
||||
|
||||
@@ -1258,6 +1258,7 @@ impl Context {
|
||||
messages: messages.collect(),
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
cached_contents: Vec::new(), // todo!("support context caching")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1513,6 +1514,7 @@ impl Context {
|
||||
messages: messages.collect(),
|
||||
stop: vec![],
|
||||
temperature: 1.0,
|
||||
cached_contents: Vec::new(), // todo!
|
||||
};
|
||||
|
||||
let stream = CompletionProvider::global(cx).complete(request, cx);
|
||||
|
||||
@@ -851,6 +851,7 @@ impl InlineAssistant {
|
||||
messages,
|
||||
stop: vec!["|END|>".to_string()],
|
||||
temperature,
|
||||
cached_contents: Vec::new(),
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
@@ -744,6 +744,7 @@ impl PromptLibrary {
|
||||
}],
|
||||
stop: Vec::new(),
|
||||
temperature: 1.,
|
||||
cached_contents: Vec::new(),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -8,7 +8,8 @@ use assistant_slash_command::{
|
||||
};
|
||||
use gpui::{AppContext, Model, Task, WeakView};
|
||||
use indexed_docs::{
|
||||
IndexedDocsRegistry, IndexedDocsStore, LocalProvider, PackageName, ProviderId, RustdocIndexer,
|
||||
DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
|
||||
ProviderId,
|
||||
};
|
||||
use language::LspAdapterDelegate;
|
||||
use project::{Project, ProjectPath};
|
||||
@@ -34,22 +35,22 @@ impl DocsSlashCommand {
|
||||
))
|
||||
}
|
||||
|
||||
/// Ensures that the rustdoc provider is registered.
|
||||
/// Ensures that the indexed doc providers for Rust are registered.
|
||||
///
|
||||
/// Ideally we would do this sooner, but we need to wait until we're able to
|
||||
/// access the workspace so we can read the project.
|
||||
fn ensure_rustdoc_provider_is_registered(
|
||||
fn ensure_rust_doc_providers_are_registered(
|
||||
&self,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) {
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
if indexed_docs_registry
|
||||
.get_provider_store(ProviderId::rustdoc())
|
||||
.get_provider_store(LocalRustdocProvider::id())
|
||||
.is_none()
|
||||
{
|
||||
let index_provider_deps = maybe!({
|
||||
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace.clone().ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
@@ -63,9 +64,29 @@ impl DocsSlashCommand {
|
||||
});
|
||||
|
||||
if let Some((fs, cargo_workspace_root)) = index_provider_deps.log_err() {
|
||||
indexed_docs_registry.register_provider(Box::new(RustdocIndexer::new(Box::new(
|
||||
LocalProvider::new(fs, cargo_workspace_root),
|
||||
))));
|
||||
indexed_docs_registry.register_provider(Box::new(LocalRustdocProvider::new(
|
||||
fs,
|
||||
cargo_workspace_root,
|
||||
)));
|
||||
}
|
||||
}
|
||||
|
||||
if indexed_docs_registry
|
||||
.get_provider_store(DocsDotRsProvider::id())
|
||||
.is_none()
|
||||
{
|
||||
let http_client = maybe!({
|
||||
let workspace = workspace.ok_or_else(|| anyhow!("no workspace"))?;
|
||||
let workspace = workspace
|
||||
.upgrade()
|
||||
.ok_or_else(|| anyhow!("workspace was dropped"))?;
|
||||
let project = workspace.read(cx).project().clone();
|
||||
anyhow::Ok(project.read(cx).client().http_client().clone())
|
||||
});
|
||||
|
||||
if let Some(http_client) = http_client.log_err() {
|
||||
indexed_docs_registry
|
||||
.register_provider(Box::new(DocsDotRsProvider::new(http_client)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -95,7 +116,7 @@ impl SlashCommand for DocsSlashCommand {
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Vec<ArgumentCompletion>>> {
|
||||
self.ensure_rustdoc_provider_is_registered(workspace, cx);
|
||||
self.ensure_rust_doc_providers_are_registered(workspace, cx);
|
||||
|
||||
let indexed_docs_registry = IndexedDocsRegistry::global(cx);
|
||||
let args = DocsSlashCommandArgs::parse(&query);
|
||||
@@ -121,6 +142,14 @@ impl SlashCommand for DocsSlashCommand {
|
||||
match args {
|
||||
DocsSlashCommandArgs::NoProvider => {
|
||||
let providers = indexed_docs_registry.list_providers();
|
||||
if providers.is_empty() {
|
||||
return Ok(vec![ArgumentCompletion {
|
||||
label: "No available docs providers.".to_string(),
|
||||
new_text: String::new(),
|
||||
run_command: false,
|
||||
}]);
|
||||
}
|
||||
|
||||
Ok(providers
|
||||
.into_iter()
|
||||
.map(|provider| ArgumentCompletion {
|
||||
@@ -171,46 +200,65 @@ impl SlashCommand for DocsSlashCommand {
|
||||
};
|
||||
|
||||
let args = DocsSlashCommandArgs::parse(argument);
|
||||
let text = cx.background_executor().spawn({
|
||||
let task = cx.background_executor().spawn({
|
||||
let store = args
|
||||
.provider()
|
||||
.ok_or_else(|| anyhow!("no docs provider specified"))
|
||||
.and_then(|provider| IndexedDocsStore::try_global(provider, cx));
|
||||
async move {
|
||||
match args {
|
||||
let (provider, key) = match args {
|
||||
DocsSlashCommandArgs::NoProvider => bail!("no docs provider specified"),
|
||||
DocsSlashCommandArgs::SearchPackageDocs {
|
||||
provider, package, ..
|
||||
} => {
|
||||
let store = store?;
|
||||
let item_docs = store.load(package.clone()).await?;
|
||||
|
||||
anyhow::Ok((provider, package, item_docs.to_string()))
|
||||
}
|
||||
} => (provider, package),
|
||||
DocsSlashCommandArgs::SearchItemDocs {
|
||||
provider,
|
||||
item_path,
|
||||
..
|
||||
} => {
|
||||
let store = store?;
|
||||
let item_docs = store.load(item_path.clone()).await?;
|
||||
} => (provider, item_path),
|
||||
};
|
||||
|
||||
anyhow::Ok((provider, item_path, item_docs.to_string()))
|
||||
let store = store?;
|
||||
let (text, ranges) = if let Some((prefix, _)) = key.split_once('*') {
|
||||
let docs = store.load_many_by_prefix(prefix.to_string()).await?;
|
||||
|
||||
let mut text = String::new();
|
||||
let mut ranges = Vec::new();
|
||||
|
||||
for (key, docs) in docs {
|
||||
let prev_len = text.len();
|
||||
|
||||
text.push_str(&docs.0);
|
||||
text.push_str("\n");
|
||||
ranges.push((key, prev_len..text.len()));
|
||||
text.push_str("\n");
|
||||
}
|
||||
}
|
||||
|
||||
(text, ranges)
|
||||
} else {
|
||||
let item_docs = store.load(key.clone()).await?;
|
||||
let text = item_docs.to_string();
|
||||
let range = 0..text.len();
|
||||
|
||||
(text, vec![(key, range)])
|
||||
};
|
||||
|
||||
anyhow::Ok((provider, text, ranges))
|
||||
}
|
||||
});
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let (provider, path, text) = text.await?;
|
||||
let range = 0..text.len();
|
||||
let (provider, text, ranges) = task.await?;
|
||||
Ok(SlashCommandOutput {
|
||||
text,
|
||||
sections: vec![SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::FileRust,
|
||||
label: format!("docs ({provider}): {path}",).into(),
|
||||
}],
|
||||
sections: ranges
|
||||
.into_iter()
|
||||
.map(|(key, range)| SlashCommandOutputSection {
|
||||
range,
|
||||
icon: IconName::FileDoc,
|
||||
label: format!("docs ({provider}): {key}",).into(),
|
||||
})
|
||||
.collect(),
|
||||
run_commands_in_text: false,
|
||||
})
|
||||
})
|
||||
|
||||
@@ -27,7 +27,7 @@ pub(crate) struct FetchSlashCommand;
|
||||
impl FetchSlashCommand {
|
||||
async fn build_message(http_client: Arc<HttpClientWithUrl>, url: &str) -> Result<String> {
|
||||
let mut url = url.to_owned();
|
||||
if !url.starts_with("https://") {
|
||||
if !url.starts_with("https://") && !url.starts_with("http://") {
|
||||
url = format!("https://{url}");
|
||||
}
|
||||
|
||||
|
||||
@@ -269,6 +269,7 @@ impl TerminalInlineAssistant {
|
||||
messages,
|
||||
stop: Vec::new(),
|
||||
temperature: 1.0,
|
||||
cached_contents: Vec::new(), // todo!
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -13,8 +13,9 @@ use async_tungstenite::tungstenite::{
|
||||
use clock::SystemClock;
|
||||
use collections::HashMap;
|
||||
use futures::{
|
||||
channel::oneshot, future::LocalBoxFuture, AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt,
|
||||
TryFutureExt as _, TryStreamExt,
|
||||
channel::oneshot,
|
||||
future::{BoxFuture, LocalBoxFuture},
|
||||
AsyncReadExt, FutureExt, SinkExt, Stream, StreamExt, TryFutureExt as _, TryStreamExt,
|
||||
};
|
||||
use gpui::{
|
||||
actions, AnyModel, AnyWeakModel, AppContext, AsyncAppContext, Global, Model, Task, WeakModel,
|
||||
@@ -23,6 +24,7 @@ use http::{HttpClient, HttpClientWithUrl};
|
||||
use lazy_static::lazy_static;
|
||||
use parking_lot::RwLock;
|
||||
use postage::watch;
|
||||
use proto::ProtoClient;
|
||||
use rand::prelude::*;
|
||||
use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EntityMessage, EnvelopedMessage, PeerId, RequestMessage};
|
||||
@@ -1408,6 +1410,11 @@ impl Client {
|
||||
self.peer.send(self.connection_id()?, message)
|
||||
}
|
||||
|
||||
fn send_dynamic(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
let connection_id = self.connection_id()?;
|
||||
self.peer.send_dynamic(connection_id, envelope)
|
||||
}
|
||||
|
||||
pub fn request<T: RequestMessage>(
|
||||
&self,
|
||||
request: T,
|
||||
@@ -1606,6 +1613,20 @@ impl Client {
|
||||
}
|
||||
}
|
||||
|
||||
impl ProtoClient for Client {
|
||||
fn request(
|
||||
&self,
|
||||
envelope: proto::Envelope,
|
||||
request_type: &'static str,
|
||||
) -> BoxFuture<'static, Result<proto::Envelope>> {
|
||||
self.request_dynamic(envelope, request_type).boxed()
|
||||
}
|
||||
|
||||
fn send(&self, envelope: proto::Envelope) -> Result<()> {
|
||||
self.send_dynamic(envelope)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct DevelopmentCredentials {
|
||||
user_id: u64,
|
||||
|
||||
@@ -101,6 +101,7 @@ pub fn language_model_request_to_google_ai(
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
generation_config: None,
|
||||
safety_settings: None,
|
||||
cached_content: request.cached_contents.into_iter().next(),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -125,6 +126,60 @@ pub fn language_model_request_message_to_google_ai(
|
||||
})
|
||||
}
|
||||
|
||||
pub fn cache_language_model_content_request_to_google_ai(
|
||||
request: proto::CacheLanguageModelContent,
|
||||
) -> Result<google_ai::CreateCachedContentRequest> {
|
||||
Ok(google_ai::CreateCachedContentRequest {
|
||||
contents: request
|
||||
.messages
|
||||
.into_iter()
|
||||
.map(language_model_request_message_to_google_ai)
|
||||
.collect::<Result<Vec<_>>>()?,
|
||||
tools: if request.tools.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some(
|
||||
request
|
||||
.tools
|
||||
.into_iter()
|
||||
.try_fold(Vec::new(), |mut acc, tool| {
|
||||
if let Some(variant) = tool.variant {
|
||||
match variant {
|
||||
proto::chat_completion_tool::Variant::Function(f) => {
|
||||
let description = f.description.ok_or_else(|| {
|
||||
anyhow!("Function tool is missing a description")
|
||||
})?;
|
||||
let parameters = f.parameters.ok_or_else(|| {
|
||||
anyhow!("Function tool is missing parameters")
|
||||
})?;
|
||||
let parsed_parameters = serde_json::from_str(¶meters)
|
||||
.map_err(|e| {
|
||||
anyhow!("Failed to parse parameters: {}", e)
|
||||
})?;
|
||||
acc.push(google_ai::Tool {
|
||||
function_declarations: vec![
|
||||
google_ai::FunctionDeclaration {
|
||||
name: f.name,
|
||||
description,
|
||||
parameters: parsed_parameters,
|
||||
},
|
||||
],
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
anyhow::Ok(acc)
|
||||
})?,
|
||||
)
|
||||
},
|
||||
ttl: request.ttl_seconds.map(|s| format!("{}s", s)),
|
||||
display_name: None,
|
||||
model: request.model,
|
||||
system_instruction: None,
|
||||
tool_config: None,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn count_tokens_request_to_google_ai(
|
||||
request: proto::CountTokensWithLanguageModel,
|
||||
) -> Result<google_ai::CountTokensRequest> {
|
||||
|
||||
@@ -616,6 +616,17 @@ impl Server {
|
||||
)
|
||||
}
|
||||
})
|
||||
.add_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
user_handler(move |request, response, session| {
|
||||
cache_language_model_content(
|
||||
request,
|
||||
response,
|
||||
session,
|
||||
app_state.config.google_ai_api_key.clone(),
|
||||
)
|
||||
})
|
||||
})
|
||||
.add_request_handler({
|
||||
let app_state = app_state.clone();
|
||||
user_handler(move |request, response, session| {
|
||||
@@ -4723,6 +4734,43 @@ async fn complete_with_anthropic(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn cache_language_model_content(
|
||||
request: proto::CacheLanguageModelContent,
|
||||
response: Response<proto::CacheLanguageModelContent>,
|
||||
session: UserSession,
|
||||
google_ai_api_key: Option<Arc<str>>,
|
||||
) -> Result<()> {
|
||||
authorize_access_to_language_models(&session).await?;
|
||||
|
||||
if !request.model.starts_with("gemini") {
|
||||
return Err(anyhow!(
|
||||
"caching content for model: {:?} is not supported",
|
||||
request.model
|
||||
))?;
|
||||
}
|
||||
|
||||
let api_key = google_ai_api_key
|
||||
.ok_or_else(|| anyhow!("no Google AI API key configured on the server"))?;
|
||||
|
||||
let cached_content = google_ai::create_cached_content(
|
||||
session.http_client.as_ref(),
|
||||
google_ai::API_URL,
|
||||
&api_key,
|
||||
crate::ai::cache_language_model_content_request_to_google_ai(request)?,
|
||||
)
|
||||
.await?;
|
||||
|
||||
response.send(proto::CacheLanguageModelContentResponse {
|
||||
name: cached_content.name,
|
||||
create_time: cached_content.create_time.timestamp() as u64,
|
||||
update_time: cached_content.update_time.timestamp() as u64,
|
||||
expire_time: cached_content.expire_time.map(|t| t.timestamp() as u64),
|
||||
total_token_count: cached_content.usage_metadata.total_token_count,
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
struct CountTokensWithLanguageModelRateLimit;
|
||||
|
||||
impl RateLimit for CountTokensWithLanguageModelRateLimit {
|
||||
|
||||
@@ -3327,7 +3327,7 @@ async fn test_local_settings(
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id().to_usize())
|
||||
.local_settings(worktree_b.entity_id().as_u64() as _)
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{"tab_size":2}"#.to_string()),
|
||||
@@ -3346,7 +3346,7 @@ async fn test_local_settings(
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id().to_usize())
|
||||
.local_settings(worktree_b.entity_id().as_u64() as _)
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("").into(), r#"{}"#.to_string()),
|
||||
@@ -3375,7 +3375,7 @@ async fn test_local_settings(
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id().to_usize())
|
||||
.local_settings(worktree_b.entity_id().as_u64() as _)
|
||||
.collect::<Vec<_>>(),
|
||||
&[
|
||||
(Path::new("a").into(), r#"{"tab_size":8}"#.to_string()),
|
||||
@@ -3407,7 +3407,7 @@ async fn test_local_settings(
|
||||
let store = cx.global::<SettingsStore>();
|
||||
assert_eq!(
|
||||
store
|
||||
.local_settings(worktree_b.read(cx).id().to_usize())
|
||||
.local_settings(worktree_b.entity_id().as_u64() as _)
|
||||
.collect::<Vec<_>>(),
|
||||
&[(Path::new("a").into(), r#"{"hard_tabs":true}"#.to_string()),]
|
||||
)
|
||||
|
||||
@@ -1237,7 +1237,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
}
|
||||
|
||||
for buffer in guest_project.opened_buffers() {
|
||||
for buffer in guest_project.opened_buffers(cx) {
|
||||
let buffer = buffer.read(cx);
|
||||
assert_eq!(
|
||||
buffer.deferred_ops_len(),
|
||||
@@ -1287,8 +1287,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
for guest_buffer in guest_buffers {
|
||||
let buffer_id =
|
||||
guest_buffer.read_with(client_cx, |buffer, _| buffer.remote_id());
|
||||
let host_buffer = host_project.read_with(host_cx, |project, _| {
|
||||
project.buffer_for_id(buffer_id).unwrap_or_else(|| {
|
||||
let host_buffer = host_project.read_with(host_cx, |project, cx| {
|
||||
project.buffer_for_id(buffer_id, cx).unwrap_or_else(|| {
|
||||
panic!(
|
||||
"host does not have buffer for guest:{}, peer:{:?}, id:{}",
|
||||
client.username,
|
||||
|
||||
@@ -49,6 +49,7 @@ lazy_static.workspace = true
|
||||
linkify.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
markdown.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
ordered-float.workspace = true
|
||||
parking_lot.workspace = true
|
||||
|
||||
@@ -286,12 +286,14 @@ gpui::actions!(
|
||||
SelectPageUp,
|
||||
ShowCharacterPalette,
|
||||
ShowInlineCompletion,
|
||||
ShowSignatureHelp,
|
||||
ShuffleLines,
|
||||
SortLinesCaseInsensitive,
|
||||
SortLinesCaseSensitive,
|
||||
SplitSelectionIntoLines,
|
||||
Tab,
|
||||
TabPrev,
|
||||
ToggleAutoSignatureHelp,
|
||||
ToggleGitBlame,
|
||||
ToggleGitBlameInline,
|
||||
ToggleSelectionMenu,
|
||||
|
||||
@@ -39,8 +39,10 @@ pub mod tasks;
|
||||
|
||||
#[cfg(test)]
|
||||
mod editor_tests;
|
||||
mod signature_help;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod test;
|
||||
|
||||
use ::git::diff::{DiffHunk, DiffHunkStatus};
|
||||
use ::git::{parse_git_remote_url, BuildPermalinkParams, GitHostingProviderRegistry};
|
||||
pub(crate) use actions::*;
|
||||
@@ -154,6 +156,7 @@ use workspace::{
|
||||
use workspace::{OpenInTerminal, OpenTerminal, TabBarSettings, Toast};
|
||||
|
||||
use crate::hover_links::find_url;
|
||||
use crate::signature_help::{SignatureHelpHiddenBy, SignatureHelpState};
|
||||
|
||||
pub const FILE_HEADER_HEIGHT: u8 = 1;
|
||||
pub const MULTI_BUFFER_EXCERPT_HEADER_HEIGHT: u8 = 1;
|
||||
@@ -501,6 +504,8 @@ pub struct Editor {
|
||||
context_menu: RwLock<Option<ContextMenu>>,
|
||||
mouse_context_menu: Option<MouseContextMenu>,
|
||||
completion_tasks: Vec<(CompletionId, Task<Option<()>>)>,
|
||||
signature_help_state: SignatureHelpState,
|
||||
auto_signature_help: Option<bool>,
|
||||
find_all_references_task_sources: Vec<Anchor>,
|
||||
next_completion_id: CompletionId,
|
||||
completion_documentation_pre_resolve_debounce: DebouncedDelay,
|
||||
@@ -1819,6 +1824,8 @@ impl Editor {
|
||||
context_menu: RwLock::new(None),
|
||||
mouse_context_menu: None,
|
||||
completion_tasks: Default::default(),
|
||||
signature_help_state: SignatureHelpState::default(),
|
||||
auto_signature_help: None,
|
||||
find_all_references_task_sources: Vec::new(),
|
||||
next_completion_id: 0,
|
||||
completion_documentation_pre_resolve_debounce: DebouncedDelay::new(),
|
||||
@@ -2156,6 +2163,10 @@ impl Editor {
|
||||
|
||||
pub fn set_cursor_shape(&mut self, cursor_shape: CursorShape, cx: &mut ViewContext<Self>) {
|
||||
self.cursor_shape = cursor_shape;
|
||||
|
||||
// Disrupt blink for immediate user feedback that the cursor shape has changed
|
||||
self.blink_manager.update(cx, BlinkManager::show_cursor);
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -2411,6 +2422,15 @@ impl Editor {
|
||||
self.request_autoscroll(autoscroll, cx);
|
||||
}
|
||||
self.selections_did_change(true, &old_cursor_position, request_completions, cx);
|
||||
|
||||
if self.should_open_signature_help_automatically(
|
||||
&old_cursor_position,
|
||||
self.signature_help_state.backspace_pressed(),
|
||||
cx,
|
||||
) {
|
||||
self.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
self.signature_help_state.set_backspace_pressed(false);
|
||||
}
|
||||
|
||||
result
|
||||
@@ -2866,6 +2886,10 @@ impl Editor {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.hide_signature_help(cx, SignatureHelpHiddenBy::Escape) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if self.hide_context_menu(cx).is_some() {
|
||||
return true;
|
||||
}
|
||||
@@ -2942,7 +2966,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let selections = self.selections.all_adjusted(cx);
|
||||
let mut brace_inserted = false;
|
||||
let mut bracket_inserted = false;
|
||||
let mut edits = Vec::new();
|
||||
let mut linked_edits = HashMap::<_, Vec<_>>::default();
|
||||
let mut new_selections = Vec::with_capacity(selections.len());
|
||||
@@ -3004,6 +3028,7 @@ impl Editor {
|
||||
),
|
||||
&bracket_pair.start[..prefix_len],
|
||||
));
|
||||
|
||||
if autoclose
|
||||
&& bracket_pair.close
|
||||
&& following_text_allows_autoclose
|
||||
@@ -3021,7 +3046,7 @@ impl Editor {
|
||||
selection.range(),
|
||||
format!("{}{}", text, bracket_pair.end).into(),
|
||||
));
|
||||
brace_inserted = true;
|
||||
bracket_inserted = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
@@ -3067,7 +3092,7 @@ impl Editor {
|
||||
selection.end..selection.end,
|
||||
bracket_pair.end.as_str().into(),
|
||||
));
|
||||
brace_inserted = true;
|
||||
bracket_inserted = true;
|
||||
new_selections.push((
|
||||
Selection {
|
||||
id: selection.id,
|
||||
@@ -3224,7 +3249,7 @@ impl Editor {
|
||||
s.select(new_selections)
|
||||
});
|
||||
|
||||
if !brace_inserted && EditorSettings::get_global(cx).use_on_type_format {
|
||||
if !bracket_inserted && EditorSettings::get_global(cx).use_on_type_format {
|
||||
if let Some(on_type_format_task) =
|
||||
this.trigger_on_type_formatting(text.to_string(), cx)
|
||||
{
|
||||
@@ -3232,6 +3257,14 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
let editor_settings = EditorSettings::get_global(cx);
|
||||
if bracket_inserted
|
||||
&& (editor_settings.auto_signature_help
|
||||
|| editor_settings.show_signature_help_after_edits)
|
||||
{
|
||||
this.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
|
||||
let trigger_in_words = !had_active_inline_completion;
|
||||
this.trigger_completion_on_input(&text, trigger_in_words, cx);
|
||||
linked_editing_ranges::refresh_linked_ranges(this, cx);
|
||||
@@ -4305,6 +4338,14 @@ impl Editor {
|
||||
true,
|
||||
cx,
|
||||
);
|
||||
|
||||
let editor_settings = EditorSettings::get_global(cx);
|
||||
if editor_settings.show_signature_help_after_edits || editor_settings.auto_signature_help {
|
||||
// After the code completion is finished, users often want to know what signatures are needed.
|
||||
// so we should automatically call signature_help
|
||||
self.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
apply_edits.await?;
|
||||
Ok(())
|
||||
@@ -5328,6 +5369,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
this.signature_help_state.set_backspace_pressed(true);
|
||||
this.change_selections(Some(Autoscroll::fit()), cx, |s| s.select(selections));
|
||||
this.insert("", cx);
|
||||
let empty_str: Arc<str> = Arc::from("");
|
||||
@@ -8487,7 +8529,7 @@ impl Editor {
|
||||
) -> Vec<(TaskSourceKind, TaskTemplate)> {
|
||||
let (inventory, worktree_id, file) = project.read_with(cx, |project, cx| {
|
||||
let (worktree_id, file) = project
|
||||
.buffer_for_id(runnable.buffer)
|
||||
.buffer_for_id(runnable.buffer, cx)
|
||||
.and_then(|buffer| buffer.read(cx).file())
|
||||
.map(|file| (WorktreeId::from_usize(file.worktree_id()), file.clone()))
|
||||
.unzip();
|
||||
@@ -11604,8 +11646,11 @@ impl Editor {
|
||||
if let Some(blame) = self.blame.as_ref() {
|
||||
blame.update(cx, GitBlame::blur)
|
||||
}
|
||||
if !self.hover_state.focused(cx) {
|
||||
hide_hover(self, cx);
|
||||
}
|
||||
|
||||
self.hide_context_menu(cx);
|
||||
hide_hover(self, cx);
|
||||
cx.emit(EditorEvent::Blurred);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -25,6 +25,9 @@ pub struct EditorSettings {
|
||||
pub expand_excerpt_lines: u32,
|
||||
#[serde(default)]
|
||||
pub double_click_in_multibuffer: DoubleClickInMultibuffer,
|
||||
pub search_wrap: bool,
|
||||
pub auto_signature_help: bool,
|
||||
pub show_signature_help_after_edits: bool,
|
||||
#[serde(default)]
|
||||
pub jupyter: Jupyter,
|
||||
}
|
||||
@@ -228,6 +231,20 @@ pub struct EditorSettingsContent {
|
||||
///
|
||||
/// Default: select
|
||||
pub double_click_in_multibuffer: Option<DoubleClickInMultibuffer>,
|
||||
/// Whether the editor search results will loop
|
||||
///
|
||||
/// Default: true
|
||||
pub search_wrap: Option<bool>,
|
||||
|
||||
/// Whether to automatically show a signature help pop-up or not.
|
||||
///
|
||||
/// Default: false
|
||||
pub auto_signature_help: Option<bool>,
|
||||
|
||||
/// Whether to show the signature help pop-up after completions or bracket pairs inserted.
|
||||
///
|
||||
/// Default: true
|
||||
pub show_signature_help_after_edits: Option<bool>,
|
||||
|
||||
/// Jupyter REPL settings.
|
||||
pub jupyter: Option<Jupyter>,
|
||||
|
||||
@@ -21,13 +21,16 @@ use language::{
|
||||
BracketPairConfig,
|
||||
Capability::ReadWrite,
|
||||
FakeLspAdapter, IndentGuide, LanguageConfig, LanguageConfigOverride, LanguageMatcher, Override,
|
||||
Point,
|
||||
ParsedMarkdown, Point,
|
||||
};
|
||||
use language_settings::IndentGuideSettings;
|
||||
use multi_buffer::MultiBufferIndentGuide;
|
||||
use parking_lot::Mutex;
|
||||
use project::project_settings::{LspSettings, ProjectSettings};
|
||||
use project::FakeFs;
|
||||
use project::{
|
||||
lsp_command::SIGNATURE_HELP_HIGHLIGHT_CURRENT,
|
||||
project_settings::{LspSettings, ProjectSettings},
|
||||
};
|
||||
use serde_json::{self, json};
|
||||
use std::sync::atomic;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
@@ -6831,6 +6834,626 @@ async fn test_strip_whitespace_and_format_via_lsp(cx: &mut gpui::TestAppContext)
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_handle_input_for_show_signature_help_auto_signature_help_true(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions {
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "/*".to_string(),
|
||||
end: " */".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "[".to_string(),
|
||||
end: "]".to_string(),
|
||||
close: false,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "\"".to_string(),
|
||||
end: "\"".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "<".to_string(),
|
||||
end: ">".to_string(),
|
||||
close: false,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
autoclose_before: "})]".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let language = Arc::new(language);
|
||||
|
||||
cx.language_registry().add(language.clone());
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(language), cx);
|
||||
});
|
||||
|
||||
cx.set_state(
|
||||
&r#"
|
||||
fn main() {
|
||||
sampleˇ
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input("(", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
fn main() {
|
||||
sample(ˇ)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
let signature_help_state = editor.signature_help_state.popover().cloned();
|
||||
assert!(signature_help_state.is_some());
|
||||
let ParsedMarkdown {
|
||||
text, highlights, ..
|
||||
} = signature_help_state.unwrap().parsed_content;
|
||||
assert_eq!(text, "param1: u8, param2: u8");
|
||||
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_handle_input_with_different_show_signature_settings(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(false);
|
||||
settings.show_signature_help_after_edits = Some(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions {
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let language = Language::new(
|
||||
LanguageConfig {
|
||||
name: "Rust".into(),
|
||||
brackets: BracketPairConfig {
|
||||
pairs: vec![
|
||||
BracketPair {
|
||||
start: "{".to_string(),
|
||||
end: "}".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "(".to_string(),
|
||||
end: ")".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "/*".to_string(),
|
||||
end: " */".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "[".to_string(),
|
||||
end: "]".to_string(),
|
||||
close: false,
|
||||
surround: false,
|
||||
newline: true,
|
||||
},
|
||||
BracketPair {
|
||||
start: "\"".to_string(),
|
||||
end: "\"".to_string(),
|
||||
close: true,
|
||||
surround: true,
|
||||
newline: false,
|
||||
},
|
||||
BracketPair {
|
||||
start: "<".to_string(),
|
||||
end: ">".to_string(),
|
||||
close: false,
|
||||
surround: true,
|
||||
newline: true,
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
},
|
||||
autoclose_before: "})]".to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
Some(tree_sitter_rust::language()),
|
||||
);
|
||||
let language = Arc::new(language);
|
||||
|
||||
cx.language_registry().add(language.clone());
|
||||
cx.update_buffer(|buffer, cx| {
|
||||
buffer.set_language(Some(language), cx);
|
||||
});
|
||||
|
||||
// Ensure that signature_help is not called when no signature help is enabled.
|
||||
cx.set_state(
|
||||
&r#"
|
||||
fn main() {
|
||||
sampleˇ
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input("(", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
fn main() {
|
||||
sample(ˇ)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.signature_help_state.task().is_none());
|
||||
});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
|
||||
// Ensure that signature_help is called when enabled afte edits
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(false);
|
||||
settings.show_signature_help_after_edits = Some(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.set_state(
|
||||
&r#"
|
||||
fn main() {
|
||||
sampleˇ
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input("(", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
fn main() {
|
||||
sample(ˇ)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.update_editor(|editor, _| {
|
||||
let signature_help_state = editor.signature_help_state.popover().cloned();
|
||||
assert!(signature_help_state.is_some());
|
||||
let ParsedMarkdown {
|
||||
text, highlights, ..
|
||||
} = signature_help_state.unwrap().parsed_content;
|
||||
assert_eq!(text, "param1: u8, param2: u8");
|
||||
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
|
||||
editor.signature_help_state = SignatureHelpState::default();
|
||||
});
|
||||
|
||||
// Ensure that signature_help is called when auto signature help override is enabled
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(true);
|
||||
settings.show_signature_help_after_edits = Some(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
cx.set_state(
|
||||
&r#"
|
||||
fn main() {
|
||||
sampleˇ
|
||||
}
|
||||
"#
|
||||
.unindent(),
|
||||
);
|
||||
cx.update_editor(|view, cx| {
|
||||
view.handle_input("(", cx);
|
||||
});
|
||||
cx.assert_editor_state(
|
||||
&"
|
||||
fn main() {
|
||||
sample(ˇ)
|
||||
}
|
||||
"
|
||||
.unindent(),
|
||||
);
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.editor(|editor, _| {
|
||||
let signature_help_state = editor.signature_help_state.popover().cloned();
|
||||
assert!(signature_help_state.is_some());
|
||||
let ParsedMarkdown {
|
||||
text, highlights, ..
|
||||
} = signature_help_state.unwrap().parsed_content;
|
||||
assert_eq!(text, "param1: u8, param2: u8");
|
||||
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_signature_help(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
cx.update(|cx| {
|
||||
cx.update_global::<SettingsStore, _>(|settings, cx| {
|
||||
settings.update_user_settings::<EditorSettings>(cx, |settings| {
|
||||
settings.auto_signature_help = Some(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
let mut cx = EditorLspTestContext::new_rust(
|
||||
lsp::ServerCapabilities {
|
||||
signature_help_provider: Some(lsp::SignatureHelpOptions {
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
// A test that directly calls `show_signature_help`
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.show_signature_help(&ShowSignatureHelp, cx);
|
||||
});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
let signature_help_state = editor.signature_help_state.popover().cloned();
|
||||
assert!(signature_help_state.is_some());
|
||||
let ParsedMarkdown {
|
||||
text, highlights, ..
|
||||
} = signature_help_state.unwrap().parsed_content;
|
||||
assert_eq!(text, "param1: u8, param2: u8");
|
||||
assert_eq!(highlights, vec![(0..10, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]);
|
||||
});
|
||||
|
||||
// When exiting outside from inside the brackets, `signature_help` is closed.
|
||||
cx.set_state(indoc! {"
|
||||
fn main() {
|
||||
sample(ˇ);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| s.select_ranges([0..0]));
|
||||
});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: Vec::new(),
|
||||
active_signature: None,
|
||||
active_parameter: None,
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
|
||||
cx.condition(|editor, _| !editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
assert!(!editor.signature_help_state.is_shown());
|
||||
});
|
||||
|
||||
// When entering inside the brackets from outside, `show_signature_help` is automatically called.
|
||||
cx.set_state(indoc! {"
|
||||
fn main() {
|
||||
sample(ˇ);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.signature_help_state.is_shown());
|
||||
});
|
||||
|
||||
// Restore the popover with more parameter input
|
||||
cx.set_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, param2ˇ);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
|
||||
// When selecting a range, the popover is gone.
|
||||
// Avoid using `cx.set_state` to not actually edit the document, just change its selections.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, «ˇparam2»);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
cx.editor(|editor, _| {
|
||||
assert!(!editor.signature_help_state.is_shown());
|
||||
});
|
||||
|
||||
// When unselecting again, the popover is back if within the brackets.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, ˇparam2);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
handle_signature_help_request(&mut cx, mocked_response).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.editor(|editor, _| {
|
||||
assert!(editor.signature_help_state.is_shown());
|
||||
});
|
||||
|
||||
// Test to confirm that SignatureHelp does not appear after deselecting multiple ranges when it was hidden by pressing Escape.
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(0, 0)..Point::new(0, 0)));
|
||||
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, ˇparam2);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
|
||||
let mocked_response = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn sample(param1: u8, param2: u8)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param1: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("param2: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
handle_signature_help_request(&mut cx, mocked_response.clone()).await;
|
||||
cx.condition(|editor, _| editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.hide_signature_help(cx, SignatureHelpHiddenBy::Escape);
|
||||
});
|
||||
cx.condition(|editor, _| !editor.signature_help_state.is_shown())
|
||||
.await;
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 25)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, «ˇparam2»);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
cx.update_editor(|editor, cx| {
|
||||
editor.change_selections(None, cx, |s| {
|
||||
s.select_ranges(Some(Point::new(1, 19)..Point::new(1, 19)));
|
||||
})
|
||||
});
|
||||
cx.assert_editor_state(indoc! {"
|
||||
fn main() {
|
||||
sample(param1, ˇparam2);
|
||||
}
|
||||
|
||||
fn sample(param1: u8, param2: u8) {}
|
||||
"});
|
||||
cx.condition(|editor, _| !editor.signature_help_state.is_shown()) // because hidden by escape
|
||||
.await;
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -12450,6 +13073,21 @@ fn assert_selection_ranges(marked_text: &str, view: &mut Editor, cx: &mut ViewCo
|
||||
);
|
||||
}
|
||||
|
||||
pub fn handle_signature_help_request(
|
||||
cx: &mut EditorLspTestContext,
|
||||
mocked_response: lsp::SignatureHelp,
|
||||
) -> impl Future<Output = ()> {
|
||||
let mut request =
|
||||
cx.handle_request::<lsp::request::SignatureHelpRequest, _, _>(move |_, _, _| {
|
||||
let mocked_response = mocked_response.clone();
|
||||
async move { Ok(Some(mocked_response)) }
|
||||
});
|
||||
|
||||
async move {
|
||||
request.next().await;
|
||||
}
|
||||
}
|
||||
|
||||
/// Handle completion request passing a marked string specifying where the completion
|
||||
/// should be triggered from using '|' character, what range should be replaced, and what completions
|
||||
/// should be returned using '<' and '>' to delimit the range
|
||||
|
||||
@@ -382,6 +382,7 @@ impl EditorElement {
|
||||
cx.propagate();
|
||||
}
|
||||
});
|
||||
register_action(view, cx, Editor::show_signature_help);
|
||||
register_action(view, cx, Editor::next_inline_completion);
|
||||
register_action(view, cx, Editor::previous_inline_completion);
|
||||
register_action(view, cx, Editor::show_inline_completion);
|
||||
@@ -709,18 +710,24 @@ impl EditorElement {
|
||||
let Some(hub) = editor.collaboration_hub() else {
|
||||
return;
|
||||
};
|
||||
let range = DisplayPoint::new(point.row(), point.column().saturating_sub(1))
|
||||
..DisplayPoint::new(
|
||||
let start = snapshot.display_snapshot.clip_point(
|
||||
DisplayPoint::new(point.row(), point.column().saturating_sub(1)),
|
||||
Bias::Left,
|
||||
);
|
||||
let end = snapshot.display_snapshot.clip_point(
|
||||
DisplayPoint::new(
|
||||
point.row(),
|
||||
(point.column() + 1).min(snapshot.line_len(point.row())),
|
||||
);
|
||||
),
|
||||
Bias::Right,
|
||||
);
|
||||
|
||||
let range = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_at(range.start.to_point(&snapshot.display_snapshot), Bias::Left)
|
||||
.anchor_at(start.to_point(&snapshot.display_snapshot), Bias::Left)
|
||||
..snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_at(range.end.to_point(&snapshot.display_snapshot), Bias::Right);
|
||||
.anchor_at(end.to_point(&snapshot.display_snapshot), Bias::Right);
|
||||
|
||||
let Some(selection) = snapshot.remote_selections_in_range(&range, hub, cx).next() else {
|
||||
return;
|
||||
@@ -2629,6 +2636,73 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn layout_signature_help(
|
||||
&self,
|
||||
hitbox: &Hitbox,
|
||||
content_origin: gpui::Point<Pixels>,
|
||||
scroll_pixel_position: gpui::Point<Pixels>,
|
||||
newest_selection_head: Option<DisplayPoint>,
|
||||
start_row: DisplayRow,
|
||||
line_layouts: &[LineWithInvisibles],
|
||||
line_height: Pixels,
|
||||
em_width: Pixels,
|
||||
cx: &mut WindowContext,
|
||||
) {
|
||||
let Some(newest_selection_head) = newest_selection_head else {
|
||||
return;
|
||||
};
|
||||
let selection_row = newest_selection_head.row();
|
||||
if selection_row < start_row {
|
||||
return;
|
||||
}
|
||||
let Some(cursor_row_layout) = line_layouts.get(selection_row.minus(start_row) as usize)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let start_x = cursor_row_layout.x_for_index(newest_selection_head.column() as usize)
|
||||
- scroll_pixel_position.x
|
||||
+ content_origin.x;
|
||||
let start_y =
|
||||
selection_row.as_f32() * line_height + content_origin.y - scroll_pixel_position.y;
|
||||
|
||||
let max_size = size(
|
||||
(120. * em_width) // Default size
|
||||
.min(hitbox.size.width / 2.) // Shrink to half of the editor width
|
||||
.max(MIN_POPOVER_CHARACTER_WIDTH * em_width), // Apply minimum width of 20 characters
|
||||
(16. * line_height) // Default size
|
||||
.min(hitbox.size.height / 2.) // Shrink to half of the editor height
|
||||
.max(MIN_POPOVER_LINE_HEIGHT * line_height), // Apply minimum height of 4 lines
|
||||
);
|
||||
|
||||
let maybe_element = self.editor.update(cx, |editor, cx| {
|
||||
if let Some(popover) = editor.signature_help_state.popover_mut() {
|
||||
let element = popover.render(
|
||||
&self.style,
|
||||
max_size,
|
||||
editor.workspace.as_ref().map(|(w, _)| w.clone()),
|
||||
cx,
|
||||
);
|
||||
Some(element)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
if let Some(mut element) = maybe_element {
|
||||
let window_size = cx.viewport_size();
|
||||
let size = element.layout_as_root(Size::<AvailableSpace>::default(), cx);
|
||||
let mut point = point(start_x, start_y - size.height);
|
||||
|
||||
// Adjusting to ensure the popover does not overflow in the X-axis direction.
|
||||
if point.x + size.width >= window_size.width {
|
||||
point.x = window_size.width - size.width;
|
||||
}
|
||||
|
||||
cx.defer_draw(element, point, 1)
|
||||
}
|
||||
}
|
||||
|
||||
fn paint_background(&self, layout: &EditorLayout, cx: &mut WindowContext) {
|
||||
cx.paint_layer(layout.hitbox.bounds, |cx| {
|
||||
let scroll_top = layout.position_map.snapshot.scroll_position().y;
|
||||
@@ -3734,6 +3808,9 @@ impl EditorElement {
|
||||
move |event: &MouseMoveEvent, phase, cx| {
|
||||
if phase == DispatchPhase::Bubble {
|
||||
editor.update(cx, |editor, cx| {
|
||||
if editor.hover_state.focused(cx) {
|
||||
return;
|
||||
}
|
||||
if event.pressed_button == Some(MouseButton::Left)
|
||||
|| event.pressed_button == Some(MouseButton::Middle)
|
||||
{
|
||||
@@ -5063,6 +5140,18 @@ impl Element for EditorElement {
|
||||
vec![]
|
||||
};
|
||||
|
||||
self.layout_signature_help(
|
||||
&hitbox,
|
||||
content_origin,
|
||||
scroll_pixel_position,
|
||||
newest_selection_head,
|
||||
start_row,
|
||||
&line_layouts,
|
||||
line_height,
|
||||
em_width,
|
||||
cx,
|
||||
);
|
||||
|
||||
if !cx.has_active_drag() {
|
||||
self.layout_hover_popovers(
|
||||
&snapshot,
|
||||
|
||||
@@ -5,24 +5,26 @@ use crate::{
|
||||
Anchor, AnchorRangeExt, DisplayPoint, DisplayRow, Editor, EditorSettings, EditorSnapshot,
|
||||
EditorStyle, Hover, RangeToAnchorExt,
|
||||
};
|
||||
use futures::{stream::FuturesUnordered, FutureExt};
|
||||
use gpui::{
|
||||
div, px, AnyElement, CursorStyle, Hsla, InteractiveElement, IntoElement, MouseButton,
|
||||
ParentElement, Pixels, ScrollHandle, SharedString, Size, StatefulInteractiveElement, Styled,
|
||||
Task, ViewContext, WeakView,
|
||||
div, px, AnyElement, AsyncWindowContext, CursorStyle, FontWeight, Hsla, InteractiveElement,
|
||||
IntoElement, MouseButton, ParentElement, Pixels, ScrollHandle, SharedString, Size,
|
||||
StatefulInteractiveElement, StyleRefinement, Styled, Task, TextStyleRefinement, View,
|
||||
ViewContext, WeakView,
|
||||
};
|
||||
use language::{markdown, DiagnosticEntry, Language, LanguageRegistry, ParsedMarkdown};
|
||||
|
||||
use itertools::Itertools;
|
||||
use language::{DiagnosticEntry, Language, LanguageRegistry};
|
||||
use lsp::DiagnosticSeverity;
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use multi_buffer::ToOffset;
|
||||
use project::{HoverBlock, HoverBlockKind, InlayHintLabelPart};
|
||||
use project::{HoverBlock, InlayHintLabelPart};
|
||||
use settings::Settings;
|
||||
use smol::stream::StreamExt;
|
||||
use std::rc::Rc;
|
||||
use std::{borrow::Cow, cell::RefCell};
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, window_is_transparent, Tooltip};
|
||||
use util::TryFutureExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
pub const HOVER_DELAY_MILLIS: u64 = 350;
|
||||
pub const HOVER_REQUEST_DELAY_MILLIS: u64 = 200;
|
||||
|
||||
@@ -40,6 +42,9 @@ pub fn hover(editor: &mut Editor, _: &Hover, cx: &mut ViewContext<Editor>) {
|
||||
/// depending on whether a point to hover over is provided.
|
||||
pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContext<Editor>) {
|
||||
if EditorSettings::get_global(cx).hover_popover_enabled {
|
||||
if show_keyboard_hover(editor, cx) {
|
||||
return;
|
||||
}
|
||||
if let Some(anchor) = anchor {
|
||||
show_hover(editor, anchor, false, cx);
|
||||
} else {
|
||||
@@ -48,6 +53,20 @@ pub fn hover_at(editor: &mut Editor, anchor: Option<Anchor>, cx: &mut ViewContex
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_keyboard_hover(editor: &mut Editor, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let info_popovers = editor.hover_state.info_popovers.clone();
|
||||
for p in info_popovers {
|
||||
let keyboard_grace = p.keyboard_grace.borrow();
|
||||
if *keyboard_grace {
|
||||
if let Some(anchor) = p.anchor {
|
||||
show_hover(editor, anchor, false, cx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
pub struct InlayHover {
|
||||
pub range: InlayHighlight,
|
||||
pub tooltip: HoverBlock,
|
||||
@@ -113,12 +132,14 @@ pub fn hover_at_inlay(editor: &mut Editor, inlay_hover: InlayHover, cx: &mut Vie
|
||||
|
||||
let language_registry = project.update(&mut cx, |p, _| p.languages().clone())?;
|
||||
let blocks = vec![inlay_hover.tooltip];
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, None).await;
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, None, &mut cx).await;
|
||||
|
||||
let hover_popover = InfoPopover {
|
||||
symbol_range: RangeInEditor::Inlay(inlay_hover.range.clone()),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
keyboard_grace: Rc::new(RefCell::new(false)),
|
||||
anchor: None,
|
||||
};
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
@@ -291,39 +312,40 @@ fn show_hover(
|
||||
let snapshot = this.update(&mut cx, |this, cx| this.snapshot(cx))?;
|
||||
let mut hover_highlights = Vec::with_capacity(hovers_response.len());
|
||||
let mut info_popovers = Vec::with_capacity(hovers_response.len());
|
||||
let mut info_popover_tasks = hovers_response
|
||||
.into_iter()
|
||||
.map(|hover_result| async {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = hover_result
|
||||
.range
|
||||
.and_then(|range| {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
let mut info_popover_tasks = Vec::with_capacity(hovers_response.len());
|
||||
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| anchor..anchor);
|
||||
for hover_result in hovers_response {
|
||||
// Create symbol range of anchors for highlighting and filtering of future requests.
|
||||
let range = hover_result
|
||||
.range
|
||||
.and_then(|range| {
|
||||
let start = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.start)?;
|
||||
let end = snapshot
|
||||
.buffer_snapshot
|
||||
.anchor_in_excerpt(excerpt_id, range.end)?;
|
||||
|
||||
let blocks = hover_result.contents;
|
||||
let language = hover_result.language;
|
||||
let parsed_content = parse_blocks(&blocks, &language_registry, language).await;
|
||||
Some(start..end)
|
||||
})
|
||||
.unwrap_or_else(|| anchor..anchor);
|
||||
|
||||
(
|
||||
range.clone(),
|
||||
InfoPopover {
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<FuturesUnordered<_>>();
|
||||
while let Some((highlight_range, info_popover)) = info_popover_tasks.next().await {
|
||||
let blocks = hover_result.contents;
|
||||
let language = hover_result.language;
|
||||
let parsed_content =
|
||||
parse_blocks(&blocks, &language_registry, language, &mut cx).await;
|
||||
info_popover_tasks.push((
|
||||
range.clone(),
|
||||
InfoPopover {
|
||||
symbol_range: RangeInEditor::Text(range),
|
||||
parsed_content,
|
||||
scroll_handle: ScrollHandle::new(),
|
||||
keyboard_grace: Rc::new(RefCell::new(ignore_timeout)),
|
||||
anchor: Some(anchor),
|
||||
},
|
||||
));
|
||||
}
|
||||
for (highlight_range, info_popover) in info_popover_tasks {
|
||||
hover_highlights.push(highlight_range);
|
||||
info_popovers.push(info_popover);
|
||||
}
|
||||
@@ -357,72 +379,81 @@ async fn parse_blocks(
|
||||
blocks: &[HoverBlock],
|
||||
language_registry: &Arc<LanguageRegistry>,
|
||||
language: Option<Arc<Language>>,
|
||||
) -> markdown::ParsedMarkdown {
|
||||
let mut text = String::new();
|
||||
let mut highlights = Vec::new();
|
||||
let mut region_ranges = Vec::new();
|
||||
let mut regions = Vec::new();
|
||||
cx: &mut AsyncWindowContext,
|
||||
) -> Option<View<Markdown>> {
|
||||
let fallback_language_name = if let Some(ref l) = language {
|
||||
let l = Arc::clone(l);
|
||||
Some(l.lsp_id().clone())
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
for block in blocks {
|
||||
match &block.kind {
|
||||
HoverBlockKind::PlainText => {
|
||||
markdown::new_paragraph(&mut text, &mut Vec::new());
|
||||
text.push_str(&block.text.replace("\\n", "\n"));
|
||||
let combined_text = blocks
|
||||
.iter()
|
||||
.map(|block| match &block.kind {
|
||||
project::HoverBlockKind::PlainText | project::HoverBlockKind::Markdown => {
|
||||
Cow::Borrowed(block.text.trim())
|
||||
}
|
||||
|
||||
HoverBlockKind::Markdown => {
|
||||
markdown::parse_markdown_block(
|
||||
&block.text.replace("\\n", "\n"),
|
||||
language_registry,
|
||||
language.clone(),
|
||||
&mut text,
|
||||
&mut highlights,
|
||||
&mut region_ranges,
|
||||
&mut regions,
|
||||
)
|
||||
.await
|
||||
project::HoverBlockKind::Code { language } => {
|
||||
Cow::Owned(format!("```{}\n{}\n```", language, block.text.trim()))
|
||||
}
|
||||
})
|
||||
.join("\n\n");
|
||||
|
||||
HoverBlockKind::Code { language } => {
|
||||
if let Some(language) = language_registry
|
||||
.language_for_name(language)
|
||||
.now_or_never()
|
||||
.and_then(Result::ok)
|
||||
{
|
||||
markdown::highlight_code(&mut text, &mut highlights, &block.text, &language);
|
||||
} else {
|
||||
text.push_str(&block.text);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let rendered_block = cx
|
||||
.new_view(|cx| {
|
||||
let settings = ThemeSettings::get_global(cx);
|
||||
let buffer_font_family = settings.buffer_font.family.clone();
|
||||
let mut base_style = cx.text_style();
|
||||
base_style.refine(&TextStyleRefinement {
|
||||
font_family: Some(buffer_font_family.clone()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let leading_space = text.chars().take_while(|c| c.is_whitespace()).count();
|
||||
if leading_space > 0 {
|
||||
highlights = highlights
|
||||
.into_iter()
|
||||
.map(|(range, style)| {
|
||||
(
|
||||
range.start.saturating_sub(leading_space)
|
||||
..range.end.saturating_sub(leading_space),
|
||||
style,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
region_ranges = region_ranges
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
range.start.saturating_sub(leading_space)..range.end.saturating_sub(leading_space)
|
||||
})
|
||||
.collect();
|
||||
}
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: base_style,
|
||||
code_block: StyleRefinement::default().mt(rems(1.)).mb(rems(1.)),
|
||||
inline_code: TextStyleRefinement {
|
||||
background_color: Some(cx.theme().colors().background),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Color::Muted.color(cx),
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: TextStyleRefinement {
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: TextStyleRefinement {
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: { cx.theme().players().local().selection },
|
||||
break_style: Default::default(),
|
||||
heading: StyleRefinement::default()
|
||||
.font_weight(FontWeight::BOLD)
|
||||
.text_base()
|
||||
.mt(rems(1.))
|
||||
.mb_0(),
|
||||
};
|
||||
|
||||
ParsedMarkdown {
|
||||
text: text.trim().to_string(),
|
||||
highlights,
|
||||
region_ranges,
|
||||
regions,
|
||||
}
|
||||
Markdown::new(
|
||||
combined_text,
|
||||
markdown_style.clone(),
|
||||
Some(language_registry.clone()),
|
||||
cx,
|
||||
fallback_language_name,
|
||||
)
|
||||
})
|
||||
.ok();
|
||||
|
||||
rendered_block
|
||||
}
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -444,7 +475,7 @@ impl HoverState {
|
||||
style: &EditorStyle,
|
||||
visible_rows: Range<DisplayRow>,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
_workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> Option<(DisplayPoint, Vec<AnyElement>)> {
|
||||
// If there is a diagnostic, position the popovers based on that.
|
||||
@@ -482,29 +513,39 @@ impl HoverState {
|
||||
elements.push(diagnostic_popover.render(style, max_size, cx));
|
||||
}
|
||||
for info_popover in &mut self.info_popovers {
|
||||
elements.push(info_popover.render(style, max_size, workspace.clone(), cx));
|
||||
elements.push(info_popover.render(max_size, cx));
|
||||
}
|
||||
|
||||
Some((point, elements))
|
||||
}
|
||||
|
||||
pub fn focused(&self, cx: &mut ViewContext<Editor>) -> bool {
|
||||
let mut hover_popover_is_focused = false;
|
||||
for info_popover in &self.info_popovers {
|
||||
for markdown_view in &info_popover.parsed_content {
|
||||
if markdown_view.focus_handle(cx).is_focused(cx) {
|
||||
hover_popover_is_focused = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
return hover_popover_is_focused;
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Debug, Clone)]
|
||||
|
||||
pub struct InfoPopover {
|
||||
pub symbol_range: RangeInEditor,
|
||||
pub parsed_content: ParsedMarkdown,
|
||||
pub parsed_content: Option<View<Markdown>>,
|
||||
pub scroll_handle: ScrollHandle,
|
||||
pub keyboard_grace: Rc<RefCell<bool>>,
|
||||
pub anchor: Option<Anchor>,
|
||||
}
|
||||
|
||||
impl InfoPopover {
|
||||
pub fn render(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
pub fn render(&mut self, max_size: Size<Pixels>, cx: &mut ViewContext<Editor>) -> AnyElement {
|
||||
let keyboard_grace = Rc::clone(&self.keyboard_grace);
|
||||
let mut d = div()
|
||||
.id("info_popover")
|
||||
.elevation_2(cx)
|
||||
.overflow_y_scroll()
|
||||
@@ -514,15 +555,17 @@ impl InfoPopover {
|
||||
// Prevent a mouse down/move on the popover from being propagated to the editor,
|
||||
// because that would dismiss the popover.
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(div().p_2().child(crate::render_parsed_markdown(
|
||||
"content",
|
||||
&self.parsed_content,
|
||||
style,
|
||||
workspace,
|
||||
cx,
|
||||
)))
|
||||
.into_any_element()
|
||||
.on_mouse_down(MouseButton::Left, move |_, cx| {
|
||||
let mut keyboard_grace = keyboard_grace.borrow_mut();
|
||||
*keyboard_grace = false;
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.p_2();
|
||||
|
||||
if let Some(markdown) = &self.parsed_content {
|
||||
d = d.child(markdown.clone());
|
||||
}
|
||||
d.into_any_element()
|
||||
}
|
||||
|
||||
pub fn scroll(&self, amount: &ScrollAmount, cx: &mut ViewContext<Editor>) {
|
||||
@@ -593,8 +636,6 @@ impl DiagnosticPopover {
|
||||
.when(window_is_transparent(cx), |this| {
|
||||
this.bg(gpui::transparent_black())
|
||||
})
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.cursor(CursorStyle::PointingHand)
|
||||
.tooltip(move |cx| Tooltip::for_action("Go To Diagnostic", &crate::GoToDiagnostic, cx))
|
||||
// Prevent a mouse move on the popover from being propagated to the editor,
|
||||
@@ -608,6 +649,8 @@ impl DiagnosticPopover {
|
||||
div()
|
||||
.id("diagnostic-inner")
|
||||
.overflow_y_scroll()
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.px_2()
|
||||
.py_1()
|
||||
.bg(diagnostic_colors.background)
|
||||
@@ -642,17 +685,33 @@ mod tests {
|
||||
InlayId, PointForPosition,
|
||||
};
|
||||
use collections::BTreeSet;
|
||||
use gpui::{FontWeight, HighlightStyle, UnderlineStyle};
|
||||
use indoc::indoc;
|
||||
use language::{language_settings::InlayHintSettings, Diagnostic, DiagnosticSet};
|
||||
use lsp::LanguageServerId;
|
||||
use project::{HoverBlock, HoverBlockKind};
|
||||
use markdown::parser::MarkdownEvent;
|
||||
use smol::stream::StreamExt;
|
||||
use std::sync::atomic;
|
||||
use std::sync::atomic::AtomicUsize;
|
||||
use text::Bias;
|
||||
use unindent::Unindent;
|
||||
use util::test::marked_text_ranges;
|
||||
|
||||
impl InfoPopover {
|
||||
fn get_rendered_text(&self, cx: &gpui::AppContext) -> String {
|
||||
let mut rendered_text = String::new();
|
||||
if let Some(parsed_content) = self.parsed_content.clone() {
|
||||
let markdown = parsed_content.read(cx);
|
||||
let text = markdown.parsed_markdown().source().to_string();
|
||||
let data = markdown.parsed_markdown().events();
|
||||
let slice = data;
|
||||
|
||||
for (range, event) in slice.iter() {
|
||||
if [MarkdownEvent::Text, MarkdownEvent::Code].contains(event) {
|
||||
rendered_text.push_str(&text[range.clone()])
|
||||
}
|
||||
}
|
||||
}
|
||||
rendered_text
|
||||
}
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_mouse_hover_info_popover_with_autocomplete_popover(
|
||||
@@ -736,7 +795,7 @@ mod tests {
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
requests.next().await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -744,14 +803,13 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// check that the completion menu is still visible and that there still has only been 1 completion request
|
||||
@@ -777,7 +835,7 @@ mod tests {
|
||||
assert_eq!(counter.load(atomic::Ordering::Acquire), 1);
|
||||
|
||||
//verify the information popover is still visible and unchanged
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -785,14 +843,14 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// Mouse moved with no hover response dismisses
|
||||
@@ -870,7 +928,7 @@ mod tests {
|
||||
.advance_clock(Duration::from_millis(HOVER_DELAY_MILLIS + 100));
|
||||
requests.next().await;
|
||||
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert!(editor.hover_state.visible());
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
@@ -878,14 +936,14 @@ mod tests {
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some basic docs".to_string())
|
||||
});
|
||||
|
||||
// Mouse moved with no hover response dismisses
|
||||
@@ -931,34 +989,49 @@ mod tests {
|
||||
let symbol_range = cx.lsp_range(indoc! {"
|
||||
«fn» test() { println!(); }
|
||||
"});
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: "some other basic docs".to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.editor(|editor, _cx| {
|
||||
assert!(!editor.hover_state.visible());
|
||||
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
0,
|
||||
"Expected no hovers but got but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
});
|
||||
|
||||
let mut requests =
|
||||
cx.handle_request::<lsp::request::HoverRequest, _, _>(move |_, _, _| async move {
|
||||
Ok(Some(lsp::Hover {
|
||||
contents: lsp::HoverContents::Markup(lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: "some other basic docs".to_string(),
|
||||
}),
|
||||
range: Some(symbol_range),
|
||||
}))
|
||||
});
|
||||
|
||||
requests.next().await;
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
assert_eq!(rendered.text, "some other basic docs".to_string())
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(rendered_text, "some other basic docs".to_string())
|
||||
});
|
||||
}
|
||||
|
||||
@@ -998,24 +1071,25 @@ mod tests {
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
rendered_text,
|
||||
"regular text for hover to show".to_string(),
|
||||
"No empty string hovers should be shown"
|
||||
);
|
||||
@@ -1063,24 +1137,25 @@ mod tests {
|
||||
.next()
|
||||
.await;
|
||||
|
||||
cx.dispatch_action(Hover);
|
||||
|
||||
cx.condition(|editor, _| editor.hover_state.visible()).await;
|
||||
cx.editor(|editor, _| {
|
||||
cx.editor(|editor, cx| {
|
||||
assert_eq!(
|
||||
editor.hover_state.info_popovers.len(),
|
||||
1,
|
||||
"Expected exactly one hover but got: {:?}",
|
||||
editor.hover_state.info_popovers
|
||||
);
|
||||
let rendered = editor
|
||||
let rendered_text = editor
|
||||
.hover_state
|
||||
.info_popovers
|
||||
.first()
|
||||
.cloned()
|
||||
.unwrap()
|
||||
.parsed_content;
|
||||
.get_rendered_text(cx);
|
||||
|
||||
assert_eq!(
|
||||
rendered.text,
|
||||
code_str.trim(),
|
||||
rendered_text, code_str,
|
||||
"Should not have extra line breaks at end of rendered hover"
|
||||
);
|
||||
});
|
||||
@@ -1156,153 +1231,6 @@ mod tests {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
fn test_render_blocks(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::test(cx.executor()));
|
||||
let editor = cx.add_window(|cx| Editor::single_line(cx));
|
||||
editor
|
||||
.update(cx, |editor, _cx| {
|
||||
let style = editor.style.clone().unwrap();
|
||||
|
||||
struct Row {
|
||||
blocks: Vec<HoverBlock>,
|
||||
expected_marked_text: String,
|
||||
expected_styles: Vec<HighlightStyle>,
|
||||
}
|
||||
|
||||
let rows = &[
|
||||
// Strong emphasis
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "one **two** three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
font_weight: Some(FontWeight::BOLD),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Links
|
||||
Row {
|
||||
blocks: vec three".to_string(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "one «two» three".to_string(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Lists
|
||||
Row {
|
||||
blocks: vec
|
||||
- d"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
lists:
|
||||
- one
|
||||
- a
|
||||
- b
|
||||
- two
|
||||
- «c»
|
||||
- d"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
// Multi-paragraph list items
|
||||
Row {
|
||||
blocks: vec![HoverBlock {
|
||||
text: "
|
||||
* one two
|
||||
three
|
||||
|
||||
* four five
|
||||
* six seven
|
||||
eight
|
||||
|
||||
nine
|
||||
* ten
|
||||
* six"
|
||||
.unindent(),
|
||||
kind: HoverBlockKind::Markdown,
|
||||
}],
|
||||
expected_marked_text: "
|
||||
- one two three
|
||||
- four five
|
||||
- six seven eight
|
||||
|
||||
nine
|
||||
- ten
|
||||
- six"
|
||||
.unindent(),
|
||||
expected_styles: vec![HighlightStyle {
|
||||
underline: Some(UnderlineStyle {
|
||||
thickness: 1.0.into(),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
}],
|
||||
},
|
||||
];
|
||||
|
||||
for Row {
|
||||
blocks,
|
||||
expected_marked_text,
|
||||
expected_styles,
|
||||
} in &rows[0..]
|
||||
{
|
||||
let rendered = smol::block_on(parse_blocks(&blocks, &languages, None));
|
||||
|
||||
let (expected_text, ranges) = marked_text_ranges(expected_marked_text, false);
|
||||
let expected_highlights = ranges
|
||||
.into_iter()
|
||||
.zip(expected_styles.iter().cloned())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
rendered.text, expected_text,
|
||||
"wrong text for input {blocks:?}"
|
||||
);
|
||||
|
||||
let rendered_highlights: Vec<_> = rendered
|
||||
.highlights
|
||||
.iter()
|
||||
.filter_map(|(range, highlight)| {
|
||||
let highlight = highlight.to_highlight_style(&style.syntax)?;
|
||||
Some((range.clone(), highlight))
|
||||
})
|
||||
.collect();
|
||||
|
||||
assert_eq!(
|
||||
rendered_highlights, expected_highlights,
|
||||
"wrong highlights for input {blocks:?}"
|
||||
);
|
||||
}
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_hover_inlay_label_parts(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
@@ -1546,9 +1474,8 @@ mod tests {
|
||||
"Popover range should match the new type label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover.parsed_content.text,
|
||||
format!("A tooltip for `{new_type_label}`"),
|
||||
"Rendered text should not anyhow alter backticks"
|
||||
popover.get_rendered_text(cx),
|
||||
format!("A tooltip for {new_type_label}"),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -1602,7 +1529,7 @@ mod tests {
|
||||
"Popover range should match the struct label part"
|
||||
);
|
||||
assert_eq!(
|
||||
popover.parsed_content.text,
|
||||
popover.get_rendered_text(cx),
|
||||
format!("A tooltip for {struct_label}"),
|
||||
"Rendered markdown element should remove backticks from text"
|
||||
);
|
||||
|
||||
@@ -371,7 +371,7 @@ async fn update_editor_from_message(
|
||||
continue;
|
||||
};
|
||||
let buffer_id = BufferId::new(excerpt.buffer_id)?;
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id) else {
|
||||
let Some(buffer) = project.read(cx).buffer_for_id(buffer_id, cx) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
|
||||
@@ -49,13 +49,11 @@ fn display_ranges<'a>(
|
||||
.pending
|
||||
.as_ref()
|
||||
.map(|pending| &pending.selection);
|
||||
selections.disjoint.iter().chain(pending).map(move |s| {
|
||||
if s.reversed {
|
||||
s.end.to_display_point(&display_map)..s.start.to_display_point(&display_map)
|
||||
} else {
|
||||
s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map)
|
||||
}
|
||||
})
|
||||
selections
|
||||
.disjoint
|
||||
.iter()
|
||||
.chain(pending)
|
||||
.map(move |s| s.start.to_display_point(&display_map)..s.end.to_display_point(&display_map))
|
||||
}
|
||||
|
||||
pub fn deploy_context_menu(
|
||||
|
||||
225
crates/editor/src/signature_help.rs
Normal file
225
crates/editor/src/signature_help.rs
Normal file
@@ -0,0 +1,225 @@
|
||||
mod popover;
|
||||
mod state;
|
||||
|
||||
use crate::actions::ShowSignatureHelp;
|
||||
use crate::{Editor, EditorSettings, ToggleAutoSignatureHelp};
|
||||
use gpui::{AppContext, ViewContext};
|
||||
use language::markdown::parse_markdown;
|
||||
use multi_buffer::{Anchor, ToOffset};
|
||||
use settings::Settings;
|
||||
use std::ops::Range;
|
||||
|
||||
pub use popover::SignatureHelpPopover;
|
||||
pub use state::SignatureHelpState;
|
||||
|
||||
// Language-specific settings may define quotes as "brackets", so filter them out separately.
|
||||
const QUOTE_PAIRS: [(&'static str, &'static str); 3] = [("'", "'"), ("\"", "\""), ("`", "`")];
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq)]
|
||||
pub enum SignatureHelpHiddenBy {
|
||||
AutoClose,
|
||||
Escape,
|
||||
Selection,
|
||||
}
|
||||
|
||||
impl Editor {
|
||||
pub fn toggle_auto_signature_help_menu(
|
||||
&mut self,
|
||||
_: &ToggleAutoSignatureHelp,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
self.auto_signature_help = self
|
||||
.auto_signature_help
|
||||
.map(|auto_signature_help| !auto_signature_help)
|
||||
.or_else(|| Some(!EditorSettings::get_global(cx).auto_signature_help));
|
||||
match self.auto_signature_help {
|
||||
Some(auto_signature_help) if auto_signature_help => {
|
||||
self.show_signature_help(&ShowSignatureHelp, cx);
|
||||
}
|
||||
Some(_) => {
|
||||
self.hide_signature_help(cx, SignatureHelpHiddenBy::AutoClose);
|
||||
}
|
||||
None => {}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub(super) fn hide_signature_help(
|
||||
&mut self,
|
||||
cx: &mut ViewContext<Self>,
|
||||
signature_help_hidden_by: SignatureHelpHiddenBy,
|
||||
) -> bool {
|
||||
if self.signature_help_state.is_shown() {
|
||||
self.signature_help_state.kill_task();
|
||||
self.signature_help_state.hide(signature_help_hidden_by);
|
||||
cx.notify();
|
||||
true
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
pub fn auto_signature_help_enabled(&self, cx: &AppContext) -> bool {
|
||||
if let Some(auto_signature_help) = self.auto_signature_help {
|
||||
auto_signature_help
|
||||
} else {
|
||||
EditorSettings::get_global(cx).auto_signature_help
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn should_open_signature_help_automatically(
|
||||
&mut self,
|
||||
old_cursor_position: &Anchor,
|
||||
backspace_pressed: bool,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> bool {
|
||||
if !(self.signature_help_state.is_shown() || self.auto_signature_help_enabled(cx)) {
|
||||
return false;
|
||||
}
|
||||
let newest_selection = self.selections.newest::<usize>(cx);
|
||||
let head = newest_selection.head();
|
||||
|
||||
// There are two cases where the head and tail of a selection are different: selecting multiple ranges and using backspace.
|
||||
// If we don’t exclude the backspace case, signature_help will blink every time backspace is pressed, so we need to prevent this.
|
||||
if !newest_selection.is_empty() && !backspace_pressed && head != newest_selection.tail() {
|
||||
self.signature_help_state
|
||||
.hide(SignatureHelpHiddenBy::Selection);
|
||||
return false;
|
||||
}
|
||||
|
||||
let buffer_snapshot = self.buffer().read(cx).snapshot(cx);
|
||||
let bracket_range = |position: usize| match (position, position + 1) {
|
||||
(0, b) if b <= buffer_snapshot.len() => 0..b,
|
||||
(0, b) => 0..b - 1,
|
||||
(a, b) if b <= buffer_snapshot.len() => a - 1..b,
|
||||
(a, b) => a - 1..b - 1,
|
||||
};
|
||||
let not_quote_like_brackets = |start: Range<usize>, end: Range<usize>| {
|
||||
let text = buffer_snapshot.text();
|
||||
let (text_start, text_end) = (text.get(start), text.get(end));
|
||||
QUOTE_PAIRS
|
||||
.into_iter()
|
||||
.all(|(start, end)| text_start != Some(start) && text_end != Some(end))
|
||||
};
|
||||
|
||||
let previous_position = old_cursor_position.to_offset(&buffer_snapshot);
|
||||
let previous_brackets_range = bracket_range(previous_position);
|
||||
let previous_brackets_surround = buffer_snapshot
|
||||
.innermost_enclosing_bracket_ranges(
|
||||
previous_brackets_range,
|
||||
Some(¬_quote_like_brackets),
|
||||
)
|
||||
.filter(|(start_bracket_range, end_bracket_range)| {
|
||||
start_bracket_range.start != previous_position
|
||||
&& end_bracket_range.end != previous_position
|
||||
});
|
||||
let current_brackets_range = bracket_range(head);
|
||||
let current_brackets_surround = buffer_snapshot
|
||||
.innermost_enclosing_bracket_ranges(
|
||||
current_brackets_range,
|
||||
Some(¬_quote_like_brackets),
|
||||
)
|
||||
.filter(|(start_bracket_range, end_bracket_range)| {
|
||||
start_bracket_range.start != head && end_bracket_range.end != head
|
||||
});
|
||||
|
||||
match (previous_brackets_surround, current_brackets_surround) {
|
||||
(None, None) => {
|
||||
self.signature_help_state
|
||||
.hide(SignatureHelpHiddenBy::AutoClose);
|
||||
false
|
||||
}
|
||||
(Some(_), None) => {
|
||||
self.signature_help_state
|
||||
.hide(SignatureHelpHiddenBy::AutoClose);
|
||||
false
|
||||
}
|
||||
(None, Some(_)) => true,
|
||||
(Some(previous), Some(current)) => {
|
||||
let condition = self.signature_help_state.hidden_by_selection()
|
||||
|| previous != current
|
||||
|| (previous == current && self.signature_help_state.is_shown());
|
||||
if !condition {
|
||||
self.signature_help_state
|
||||
.hide(SignatureHelpHiddenBy::AutoClose);
|
||||
}
|
||||
condition
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show_signature_help(&mut self, _: &ShowSignatureHelp, cx: &mut ViewContext<Self>) {
|
||||
if self.pending_rename.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let position = self.selections.newest_anchor().head();
|
||||
let Some((buffer, buffer_position)) =
|
||||
self.buffer.read(cx).text_anchor_for_position(position, cx)
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.signature_help_state
|
||||
.set_task(cx.spawn(move |editor, mut cx| async move {
|
||||
let signature_help = editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let language = editor.language_at(position, cx);
|
||||
let project = editor.project.clone()?;
|
||||
let (markdown, language_registry) = {
|
||||
project.update(cx, |project, mut cx| {
|
||||
let language_registry = project.languages().clone();
|
||||
(
|
||||
project.signature_help(&buffer, buffer_position, &mut cx),
|
||||
language_registry,
|
||||
)
|
||||
})
|
||||
};
|
||||
Some((markdown, language_registry, language))
|
||||
})
|
||||
.ok()
|
||||
.flatten();
|
||||
let signature_help_popover = if let Some((
|
||||
signature_help_task,
|
||||
language_registry,
|
||||
language,
|
||||
)) = signature_help
|
||||
{
|
||||
// TODO allow multiple signature helps inside the same popover
|
||||
if let Some(mut signature_help) = signature_help_task.await.into_iter().next() {
|
||||
let mut parsed_content = parse_markdown(
|
||||
signature_help.markdown.as_str(),
|
||||
&language_registry,
|
||||
language,
|
||||
)
|
||||
.await;
|
||||
parsed_content
|
||||
.highlights
|
||||
.append(&mut signature_help.highlights);
|
||||
Some(SignatureHelpPopover { parsed_content })
|
||||
} else {
|
||||
None
|
||||
}
|
||||
} else {
|
||||
None
|
||||
};
|
||||
editor
|
||||
.update(&mut cx, |editor, cx| {
|
||||
let previous_popover = editor.signature_help_state.popover();
|
||||
if previous_popover != signature_help_popover.as_ref() {
|
||||
if let Some(signature_help_popover) = signature_help_popover {
|
||||
editor
|
||||
.signature_help_state
|
||||
.set_popover(signature_help_popover);
|
||||
} else {
|
||||
editor
|
||||
.signature_help_state
|
||||
.hide(SignatureHelpHiddenBy::AutoClose);
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
})
|
||||
.ok();
|
||||
}));
|
||||
}
|
||||
}
|
||||
48
crates/editor/src/signature_help/popover.rs
Normal file
48
crates/editor/src/signature_help/popover.rs
Normal file
@@ -0,0 +1,48 @@
|
||||
use crate::{Editor, EditorStyle};
|
||||
use gpui::{
|
||||
div, AnyElement, InteractiveElement, IntoElement, MouseButton, ParentElement, Pixels, Size,
|
||||
StatefulInteractiveElement, Styled, ViewContext, WeakView,
|
||||
};
|
||||
use language::ParsedMarkdown;
|
||||
use ui::StyledExt;
|
||||
use workspace::Workspace;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct SignatureHelpPopover {
|
||||
pub parsed_content: ParsedMarkdown,
|
||||
}
|
||||
|
||||
impl PartialEq for SignatureHelpPopover {
|
||||
fn eq(&self, other: &Self) -> bool {
|
||||
let str_equality = self.parsed_content.text.as_str() == other.parsed_content.text.as_str();
|
||||
let highlight_equality = self.parsed_content.highlights == other.parsed_content.highlights;
|
||||
str_equality && highlight_equality
|
||||
}
|
||||
}
|
||||
|
||||
impl SignatureHelpPopover {
|
||||
pub fn render(
|
||||
&mut self,
|
||||
style: &EditorStyle,
|
||||
max_size: Size<Pixels>,
|
||||
workspace: Option<WeakView<Workspace>>,
|
||||
cx: &mut ViewContext<Editor>,
|
||||
) -> AnyElement {
|
||||
div()
|
||||
.id("signature_help_popover")
|
||||
.elevation_2(cx)
|
||||
.overflow_y_scroll()
|
||||
.max_w(max_size.width)
|
||||
.max_h(max_size.height)
|
||||
.on_mouse_move(|_, cx| cx.stop_propagation())
|
||||
.on_mouse_down(MouseButton::Left, |_, cx| cx.stop_propagation())
|
||||
.child(div().p_2().child(crate::render_parsed_markdown(
|
||||
"signature_help_popover_content",
|
||||
&self.parsed_content,
|
||||
style,
|
||||
workspace,
|
||||
cx,
|
||||
)))
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
65
crates/editor/src/signature_help/state.rs
Normal file
65
crates/editor/src/signature_help/state.rs
Normal file
@@ -0,0 +1,65 @@
|
||||
use crate::signature_help::popover::SignatureHelpPopover;
|
||||
use crate::signature_help::SignatureHelpHiddenBy;
|
||||
use gpui::Task;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
pub struct SignatureHelpState {
|
||||
task: Option<Task<()>>,
|
||||
popover: Option<SignatureHelpPopover>,
|
||||
hidden_by: Option<SignatureHelpHiddenBy>,
|
||||
backspace_pressed: bool,
|
||||
}
|
||||
|
||||
impl SignatureHelpState {
|
||||
pub fn set_task(&mut self, task: Task<()>) {
|
||||
self.task = Some(task);
|
||||
self.hidden_by = None;
|
||||
}
|
||||
|
||||
pub fn kill_task(&mut self) {
|
||||
self.task = None;
|
||||
}
|
||||
|
||||
pub fn popover(&self) -> Option<&SignatureHelpPopover> {
|
||||
self.popover.as_ref()
|
||||
}
|
||||
|
||||
pub fn popover_mut(&mut self) -> Option<&mut SignatureHelpPopover> {
|
||||
self.popover.as_mut()
|
||||
}
|
||||
|
||||
pub fn backspace_pressed(&self) -> bool {
|
||||
self.backspace_pressed
|
||||
}
|
||||
|
||||
pub fn set_backspace_pressed(&mut self, backspace_pressed: bool) {
|
||||
self.backspace_pressed = backspace_pressed;
|
||||
}
|
||||
|
||||
pub fn set_popover(&mut self, popover: SignatureHelpPopover) {
|
||||
self.popover = Some(popover);
|
||||
self.hidden_by = None;
|
||||
}
|
||||
|
||||
pub fn hide(&mut self, hidden_by: SignatureHelpHiddenBy) {
|
||||
if self.hidden_by.is_none() {
|
||||
self.popover = None;
|
||||
self.hidden_by = Some(hidden_by);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn hidden_by_selection(&self) -> bool {
|
||||
self.hidden_by == Some(SignatureHelpHiddenBy::Selection)
|
||||
}
|
||||
|
||||
pub fn is_shown(&self) -> bool {
|
||||
self.popover.is_some()
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
impl SignatureHelpState {
|
||||
pub fn task(&self) -> Option<&Task<()>> {
|
||||
self.task.as_ref()
|
||||
}
|
||||
}
|
||||
@@ -49,10 +49,9 @@ pub fn is_supported_wasm_api_version(
|
||||
/// Returns the Wasm API version range that is supported by the Wasm host.
|
||||
#[inline(always)]
|
||||
pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive<SemanticVersion> {
|
||||
let max_version = if release_channel == ReleaseChannel::Dev {
|
||||
latest::MAX_VERSION
|
||||
} else {
|
||||
since_v0_0_6::MAX_VERSION
|
||||
let max_version = match release_channel {
|
||||
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
|
||||
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_0_6::MAX_VERSION,
|
||||
};
|
||||
|
||||
since_v0_0_1::MIN_VERSION..=max_version
|
||||
|
||||
@@ -67,7 +67,10 @@ pub trait Fs: Send + Sync {
|
||||
self.remove_file(path, options).await
|
||||
}
|
||||
async fn open_sync(&self, path: &Path) -> Result<Box<dyn io::Read>>;
|
||||
async fn load(&self, path: &Path) -> Result<String>;
|
||||
async fn load(&self, path: &Path) -> Result<String> {
|
||||
Ok(String::from_utf8(self.load_bytes(path).await?)?)
|
||||
}
|
||||
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>>;
|
||||
async fn atomic_write(&self, path: PathBuf, text: String) -> Result<()>;
|
||||
async fn save(&self, path: &Path, text: &Rope, line_ending: LineEnding) -> Result<()>;
|
||||
async fn canonicalize(&self, path: &Path) -> Result<PathBuf>;
|
||||
@@ -318,6 +321,11 @@ impl Fs for RealFs {
|
||||
let text = smol::unblock(|| std::fs::read_to_string(path)).await?;
|
||||
Ok(text)
|
||||
}
|
||||
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
|
||||
let path = path.to_path_buf();
|
||||
let bytes = smol::unblock(|| std::fs::read(path)).await?;
|
||||
Ok(bytes)
|
||||
}
|
||||
|
||||
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||
smol::unblock(move || {
|
||||
@@ -1433,6 +1441,10 @@ impl Fs for FakeFs {
|
||||
Ok(String::from_utf8(content.clone())?)
|
||||
}
|
||||
|
||||
async fn load_bytes(&self, path: &Path) -> Result<Vec<u8>> {
|
||||
self.load_internal(path).await
|
||||
}
|
||||
|
||||
async fn atomic_write(&self, path: PathBuf, data: String) -> Result<()> {
|
||||
self.simulate_random_delay().await;
|
||||
let path = normalize_path(path.as_path());
|
||||
|
||||
@@ -14,3 +14,4 @@ futures.workspace = true
|
||||
http.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
chrono.workspace = true
|
||||
|
||||
@@ -97,6 +97,7 @@ pub struct GenerateContentRequest {
|
||||
pub contents: Vec<Content>,
|
||||
pub generation_config: Option<GenerationConfig>,
|
||||
pub safety_settings: Option<Vec<SafetySetting>>,
|
||||
pub cached_content: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
@@ -267,3 +268,115 @@ pub struct CountTokensRequest {
|
||||
pub struct CountTokensResponse {
|
||||
pub total_tokens: usize,
|
||||
}
|
||||
|
||||
pub async fn create_cached_content<T: HttpClient>(
|
||||
client: &T,
|
||||
api_url: &str,
|
||||
api_key: &str,
|
||||
request: CreateCachedContentRequest,
|
||||
) -> Result<CreateCachedContentResponse> {
|
||||
let uri = format!("{}/v1beta/cachedContents?key={}", api_url, api_key);
|
||||
let request = serde_json::to_string(&request)?;
|
||||
let mut response = client.post_json(&uri, request.into()).await?;
|
||||
let mut text = String::new();
|
||||
response.body_mut().read_to_string(&mut text).await?;
|
||||
if response.status().is_success() {
|
||||
Ok(serde_json::from_str::<CreateCachedContentResponse>(&text)?)
|
||||
} else {
|
||||
Err(anyhow!(
|
||||
"error during createCachedContent, status code: {:?}, body: {}",
|
||||
response.status(),
|
||||
text
|
||||
))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateCachedContentRequest {
|
||||
pub contents: Vec<Content>,
|
||||
pub tools: Option<Vec<Tool>>,
|
||||
pub ttl: Option<String>,
|
||||
pub display_name: Option<String>,
|
||||
pub model: String,
|
||||
pub system_instruction: Option<Content>,
|
||||
pub tool_config: Option<ToolConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct CreateCachedContentResponse {
|
||||
pub name: String,
|
||||
pub create_time: chrono::DateTime<chrono::Utc>,
|
||||
pub update_time: chrono::DateTime<chrono::Utc>,
|
||||
pub expire_time: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub usage_metadata: UsageMetadata,
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct UsageMetadata {
|
||||
pub total_token_count: u32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Tool {
|
||||
pub function_declarations: Vec<FunctionDeclaration>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FunctionDeclaration {
|
||||
pub name: String,
|
||||
pub description: String,
|
||||
pub parameters: Schema,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct Schema {
|
||||
#[serde(rename = "type")]
|
||||
pub schema_type: SchemaType,
|
||||
pub format: Option<String>,
|
||||
pub description: Option<String>,
|
||||
pub nullable: Option<bool>,
|
||||
pub enum_values: Option<Vec<String>>,
|
||||
pub properties: Option<std::collections::HashMap<String, Box<Schema>>>,
|
||||
pub required: Option<Vec<String>>,
|
||||
pub items: Option<Box<Schema>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum SchemaType {
|
||||
TypeUnspecified,
|
||||
String,
|
||||
Number,
|
||||
Integer,
|
||||
Boolean,
|
||||
Array,
|
||||
Object,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct ToolConfig {
|
||||
pub function_calling_config: Option<FunctionCallingConfig>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "camelCase")]
|
||||
pub struct FunctionCallingConfig {
|
||||
pub mode: Option<FunctionCallingMode>,
|
||||
pub allowed_function_names: Option<Vec<String>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "SCREAMING_SNAKE_CASE")]
|
||||
pub enum FunctionCallingMode {
|
||||
ModeUnspecified,
|
||||
Auto,
|
||||
Any,
|
||||
None,
|
||||
}
|
||||
|
||||
@@ -134,18 +134,22 @@ x11rb = { version = "0.13.0", features = [
|
||||
"resource_manager",
|
||||
"sync",
|
||||
] }
|
||||
xkbcommon = { version = "0.7", features = ["wayland", "x11"] }
|
||||
xkbcommon = { git = "https://github.com/ConradIrwin/xkbcommon-rs", rev = "2d4c4439160c7846ede0f0ece93bf73b1e613339", features = [
|
||||
"wayland",
|
||||
"x11",
|
||||
] }
|
||||
xim = { git = "https://github.com/npmania/xim-rs", rev = "27132caffc5b9bc9c432ca4afad184ab6e7c16af", features = [
|
||||
"x11rb-xcb",
|
||||
"x11rb-client",
|
||||
] }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = ["source-fontconfig-dlopen"] }
|
||||
font-kit = { git = "https://github.com/zed-industries/font-kit", rev = "5a5c4d4", features = [
|
||||
"source-fontconfig-dlopen",
|
||||
] }
|
||||
x11-clipboard = "0.9.2"
|
||||
|
||||
[target.'cfg(windows)'.dependencies]
|
||||
windows.workspace = true
|
||||
windows-core = "0.57"
|
||||
clipboard-win = "3.1.1"
|
||||
|
||||
[[example]]
|
||||
name = "hello_world"
|
||||
|
||||
@@ -193,6 +193,16 @@ impl TextInput {
|
||||
.find_map(|(idx, _)| (idx > offset).then_some(idx))
|
||||
.unwrap_or(self.content.len())
|
||||
}
|
||||
|
||||
fn reset(&mut self) {
|
||||
self.content = "".into();
|
||||
self.selected_range = 0..0;
|
||||
self.selection_reversed = false;
|
||||
self.marked_range = None;
|
||||
self.last_layout = None;
|
||||
self.last_bounds = None;
|
||||
self.is_selecting = false;
|
||||
}
|
||||
}
|
||||
|
||||
impl ViewInputHandler for TextInput {
|
||||
@@ -494,13 +504,47 @@ struct InputExample {
|
||||
recent_keystrokes: Vec<Keystroke>,
|
||||
}
|
||||
|
||||
impl InputExample {
|
||||
fn on_reset_click(&mut self, _: &MouseUpEvent, cx: &mut ViewContext<Self>) {
|
||||
self.recent_keystrokes.clear();
|
||||
self.text_input
|
||||
.update(cx, |text_input, _cx| text_input.reset());
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for InputExample {
|
||||
fn render(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
let num_keystrokes = self.recent_keystrokes.len();
|
||||
div()
|
||||
.bg(rgb(0xaaaaaa))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.size_full()
|
||||
.child(
|
||||
div()
|
||||
.bg(white())
|
||||
.border_b_1()
|
||||
.border_color(black())
|
||||
.flex()
|
||||
.flex_row()
|
||||
.justify_between()
|
||||
.child(format!("Keystrokes: {}", num_keystrokes))
|
||||
.child(
|
||||
div()
|
||||
.border_1()
|
||||
.border_color(black())
|
||||
.px_2()
|
||||
.bg(yellow())
|
||||
.child("Reset")
|
||||
.hover(|style| {
|
||||
style
|
||||
.bg(yellow().blend(opaque_grey(0.5, 0.5)))
|
||||
.cursor_pointer()
|
||||
})
|
||||
.on_mouse_up(MouseButton::Left, cx.listener(Self::on_reset_click)),
|
||||
),
|
||||
)
|
||||
.child(self.text_input.clone())
|
||||
.children(self.recent_keystrokes.iter().rev().map(|ks| {
|
||||
format!(
|
||||
|
||||
@@ -406,6 +406,8 @@ pub(crate) trait PlatformTextSystem: Send + Sync {
|
||||
raster_bounds: Bounds<DevicePixels>,
|
||||
) -> Result<(Size<DevicePixels>, Vec<u8>)>;
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout;
|
||||
#[cfg(target_os = "windows")]
|
||||
fn destroy(&self);
|
||||
}
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone)]
|
||||
|
||||
@@ -177,6 +177,9 @@ impl PlatformTextSystem for CosmicTextSystem {
|
||||
fn layout_line(&self, text: &str, font_size: Pixels, runs: &[FontRun]) -> LineLayout {
|
||||
self.0.write().layout_line(text, font_size, runs)
|
||||
}
|
||||
|
||||
#[cfg(target_os = "windows")]
|
||||
fn destroy(&self) {}
|
||||
}
|
||||
|
||||
impl CosmicTextSystemState {
|
||||
|
||||
@@ -24,7 +24,7 @@ use x11rb::xcb_ffi::XCBConnection;
|
||||
use xim::{x11rb::X11rbClient, Client};
|
||||
use xim::{AttributeName, InputStyle};
|
||||
use xkbc::x11::ffi::{XKB_X11_MIN_MAJOR_XKB_VERSION, XKB_X11_MIN_MINOR_XKB_VERSION};
|
||||
use xkbcommon::xkb as xkbc;
|
||||
use xkbcommon::xkb::{self as xkbc, LayoutIndex, ModMask};
|
||||
|
||||
use crate::platform::linux::LinuxClient;
|
||||
use crate::platform::{LinuxCommon, PlatformWindow};
|
||||
@@ -94,6 +94,13 @@ impl From<xim::ClientError> for EventHandlerError {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone)]
|
||||
struct XKBStateNotiy {
|
||||
depressed_layout: LayoutIndex,
|
||||
latched_layout: LayoutIndex,
|
||||
locked_layout: LayoutIndex,
|
||||
}
|
||||
|
||||
pub struct X11ClientState {
|
||||
pub(crate) loop_handle: LoopHandle<'static, X11Client>,
|
||||
pub(crate) event_loop: Option<calloop::EventLoop<'static, X11Client>>,
|
||||
@@ -113,6 +120,7 @@ pub struct X11ClientState {
|
||||
pub(crate) mouse_focused_window: Option<xproto::Window>,
|
||||
pub(crate) keyboard_focused_window: Option<xproto::Window>,
|
||||
pub(crate) xkb: xkbc::State,
|
||||
previous_xkb_state: XKBStateNotiy,
|
||||
pub(crate) ximc: Option<X11rbClient<Rc<XCBConnection>>>,
|
||||
pub(crate) xim_handler: Option<XimHandler>,
|
||||
pub modifiers: Modifiers,
|
||||
@@ -350,6 +358,7 @@ impl X11Client {
|
||||
mouse_focused_window: None,
|
||||
keyboard_focused_window: None,
|
||||
xkb: xkb_state,
|
||||
previous_xkb_state: XKBStateNotiy::default(),
|
||||
ximc,
|
||||
xim_handler,
|
||||
|
||||
@@ -622,7 +631,11 @@ impl X11Client {
|
||||
event.latched_group as u32,
|
||||
event.locked_group.into(),
|
||||
);
|
||||
|
||||
state.previous_xkb_state = XKBStateNotiy {
|
||||
depressed_layout: event.base_group as u32,
|
||||
latched_layout: event.latched_group as u32,
|
||||
locked_layout: event.locked_group.into(),
|
||||
};
|
||||
let modifiers = Modifiers::from_xkb(&state.xkb);
|
||||
if state.modifiers == modifiers {
|
||||
drop(state);
|
||||
@@ -644,11 +657,18 @@ impl X11Client {
|
||||
let modifiers = modifiers_from_state(event.state);
|
||||
state.modifiers = modifiers;
|
||||
state.pre_ime_key_down.take();
|
||||
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let xkb_state = state.previous_xkb_state.clone();
|
||||
state.xkb.update_mask(
|
||||
event.state.bits() as ModMask,
|
||||
0,
|
||||
0,
|
||||
xkb_state.depressed_layout,
|
||||
xkb_state.latched_layout,
|
||||
xkb_state.locked_layout,
|
||||
);
|
||||
let mut keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
state.xkb.update_key(code, xkbc::KeyDirection::Down);
|
||||
let keysym = state.xkb.key_get_one_sym(code);
|
||||
if keysym.is_modifier_key() {
|
||||
return Some(());
|
||||
@@ -707,8 +727,16 @@ impl X11Client {
|
||||
|
||||
let keystroke = {
|
||||
let code = event.detail.into();
|
||||
let xkb_state = state.previous_xkb_state.clone();
|
||||
state.xkb.update_mask(
|
||||
event.state.bits() as ModMask,
|
||||
0,
|
||||
0,
|
||||
xkb_state.depressed_layout,
|
||||
xkb_state.latched_layout,
|
||||
xkb_state.locked_layout,
|
||||
);
|
||||
let keystroke = crate::Keystroke::from_xkb(&state.xkb, modifiers, code);
|
||||
state.xkb.update_key(code, xkbc::KeyDirection::Up);
|
||||
let keysym = state.xkb.key_get_one_sym(code);
|
||||
if keysym.is_modifier_key() {
|
||||
return Some(());
|
||||
@@ -1169,10 +1197,13 @@ impl LinuxClient for X11Client {
|
||||
let cursor = match state.cursor_cache.get(&style) {
|
||||
Some(cursor) => *cursor,
|
||||
None => {
|
||||
let cursor = state
|
||||
let Some(cursor) = state
|
||||
.cursor_handle
|
||||
.load_cursor(&state.xcb_connection, &style.to_icon_name())
|
||||
.expect("failed to load cursor");
|
||||
.log_err()
|
||||
else {
|
||||
return;
|
||||
};
|
||||
state.cursor_cache.insert(style, cursor);
|
||||
cursor
|
||||
}
|
||||
|
||||
@@ -797,7 +797,7 @@ impl Platform for MacPlatform {
|
||||
CursorStyle::OpenHand => msg_send![class!(NSCursor), openHandCursor],
|
||||
CursorStyle::PointingHand => msg_send![class!(NSCursor), pointingHandCursor],
|
||||
CursorStyle::ResizeLeftRight => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), verticalResizeCursor],
|
||||
CursorStyle::ResizeUpDown => msg_send![class!(NSCursor), resizeUpDownCursor],
|
||||
CursorStyle::ResizeLeft => msg_send![class!(NSCursor), resizeLeftCursor],
|
||||
CursorStyle::ResizeRight => msg_send![class!(NSCursor), resizeRightCursor],
|
||||
CursorStyle::ResizeColumn => msg_send![class!(NSCursor), resizeLeftRightCursor],
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use std::{borrow::Cow, sync::Arc};
|
||||
use std::{borrow::Cow, mem::ManuallyDrop, sync::Arc};
|
||||
|
||||
use ::util::ResultExt;
|
||||
use anyhow::{anyhow, Result};
|
||||
@@ -39,7 +39,7 @@ pub(crate) struct DirectWriteTextSystem(RwLock<DirectWriteState>);
|
||||
struct DirectWriteComponent {
|
||||
locale: String,
|
||||
factory: IDWriteFactory5,
|
||||
bitmap_factory: IWICImagingFactory2,
|
||||
bitmap_factory: ManuallyDrop<IWICImagingFactory2>,
|
||||
d2d1_factory: ID2D1Factory,
|
||||
in_memory_loader: IDWriteInMemoryFontFileLoader,
|
||||
builder: IDWriteFontSetBuilder1,
|
||||
@@ -79,6 +79,7 @@ impl DirectWriteComponent {
|
||||
let factory: IDWriteFactory5 = DWriteCreateFactory(DWRITE_FACTORY_TYPE_SHARED)?;
|
||||
let bitmap_factory: IWICImagingFactory2 =
|
||||
CoCreateInstance(&CLSID_WICImagingFactory2, None, CLSCTX_INPROC_SERVER)?;
|
||||
let bitmap_factory = ManuallyDrop::new(bitmap_factory);
|
||||
let d2d1_factory: ID2D1Factory =
|
||||
D2D1CreateFactory(D2D1_FACTORY_TYPE_MULTI_THREADED, None)?;
|
||||
// The `IDWriteInMemoryFontFileLoader` here is supported starting from
|
||||
@@ -238,6 +239,11 @@ impl PlatformTextSystem for DirectWriteTextSystem {
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn destroy(&self) {
|
||||
let mut lock = self.0.write();
|
||||
unsafe { ManuallyDrop::drop(&mut lock.components.bitmap_factory) };
|
||||
}
|
||||
}
|
||||
|
||||
impl DirectWriteState {
|
||||
@@ -583,6 +589,18 @@ impl DirectWriteState {
|
||||
DWRITE_MEASURING_MODE_NATURAL,
|
||||
)?
|
||||
};
|
||||
// todo(windows)
|
||||
// This is a walkaround, deleted when figured out.
|
||||
let y_offset;
|
||||
let extra_height;
|
||||
if params.is_emoji {
|
||||
y_offset = 0;
|
||||
extra_height = 0;
|
||||
} else {
|
||||
// make some room for scaler.
|
||||
y_offset = -1;
|
||||
extra_height = 2;
|
||||
}
|
||||
|
||||
if bounds.right < bounds.left {
|
||||
Ok(Bounds {
|
||||
@@ -593,11 +611,13 @@ impl DirectWriteState {
|
||||
Ok(Bounds {
|
||||
origin: point(
|
||||
((bounds.left * params.scale_factor).ceil() as i32).into(),
|
||||
((bounds.top * params.scale_factor).ceil() as i32).into(),
|
||||
((bounds.top * params.scale_factor).ceil() as i32 + y_offset).into(),
|
||||
),
|
||||
size: size(
|
||||
(((bounds.right - bounds.left) * params.scale_factor).ceil() as i32).into(),
|
||||
(((bounds.bottom - bounds.top) * params.scale_factor).ceil() as i32).into(),
|
||||
(((bounds.bottom - bounds.top) * params.scale_factor).ceil() as i32
|
||||
+ extra_height)
|
||||
.into(),
|
||||
),
|
||||
})
|
||||
}
|
||||
|
||||
@@ -82,7 +82,7 @@ pub(crate) fn handle_msg(
|
||||
WM_IME_STARTCOMPOSITION => handle_ime_position(handle, state_ptr),
|
||||
WM_IME_COMPOSITION => handle_ime_composition(handle, lparam, state_ptr),
|
||||
WM_SETCURSOR => handle_set_cursor(lparam, state_ptr),
|
||||
WM_SETTINGCHANGE => handle_system_settings_changed(state_ptr),
|
||||
WM_SETTINGCHANGE => handle_system_settings_changed(handle, state_ptr),
|
||||
CURSOR_STYLE_CHANGED => handle_cursor_changed(lparam, state_ptr),
|
||||
_ => None,
|
||||
};
|
||||
@@ -732,7 +732,10 @@ fn handle_dpi_changed_msg(
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
let new_dpi = wparam.loword() as f32;
|
||||
state_ptr.state.borrow_mut().scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
lock.scale_factor = new_dpi / USER_DEFAULT_SCREEN_DPI as f32;
|
||||
lock.border_offset.udpate(handle).log_err();
|
||||
drop(lock);
|
||||
|
||||
let rect = unsafe { &*(lparam.0 as *const RECT) };
|
||||
let width = rect.right - rect.left;
|
||||
@@ -801,6 +804,9 @@ fn handle_hit_test_msg(
|
||||
lparam: LPARAM,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
if !state_ptr.is_movable {
|
||||
return None;
|
||||
}
|
||||
if !state_ptr.hide_title_bar {
|
||||
return None;
|
||||
}
|
||||
@@ -1047,12 +1053,17 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
|
||||
Some(1)
|
||||
}
|
||||
|
||||
fn handle_system_settings_changed(state_ptr: Rc<WindowsWindowStatePtr>) -> Option<isize> {
|
||||
fn handle_system_settings_changed(
|
||||
handle: HWND,
|
||||
state_ptr: Rc<WindowsWindowStatePtr>,
|
||||
) -> Option<isize> {
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
// mouse wheel
|
||||
lock.system_settings.mouse_wheel_settings.update();
|
||||
// mouse double click
|
||||
lock.click_state.system_update();
|
||||
// window border offset
|
||||
lock.border_offset.udpate(handle).log_err();
|
||||
Some(0)
|
||||
}
|
||||
|
||||
|
||||
@@ -12,7 +12,6 @@ use std::{
|
||||
|
||||
use ::util::ResultExt;
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use clipboard_win::{get_clipboard_string, set_clipboard_string};
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use itertools::Itertools;
|
||||
use parking_lot::RwLock;
|
||||
@@ -22,9 +21,22 @@ use windows::{
|
||||
core::*,
|
||||
Win32::{
|
||||
Foundation::*,
|
||||
Globalization::u_memcpy,
|
||||
Graphics::Gdi::*,
|
||||
Security::Credentials::*,
|
||||
System::{Com::*, LibraryLoader::*, Ole::*, SystemInformation::*, Threading::*, Time::*},
|
||||
System::{
|
||||
Com::*,
|
||||
DataExchange::{
|
||||
CloseClipboard, EmptyClipboard, GetClipboardData, OpenClipboard,
|
||||
RegisterClipboardFormatW, SetClipboardData,
|
||||
},
|
||||
LibraryLoader::*,
|
||||
Memory::{GlobalAlloc, GlobalLock, GlobalUnlock, GMEM_MOVEABLE},
|
||||
Ole::*,
|
||||
SystemInformation::*,
|
||||
Threading::*,
|
||||
Time::*,
|
||||
},
|
||||
UI::{Input::KeyboardAndMouse::*, Shell::*, WindowsAndMessaging::*},
|
||||
},
|
||||
UI::ViewManagement::UISettings,
|
||||
@@ -40,6 +52,8 @@ pub(crate) struct WindowsPlatform {
|
||||
background_executor: BackgroundExecutor,
|
||||
foreground_executor: ForegroundExecutor,
|
||||
text_system: Arc<dyn PlatformTextSystem>,
|
||||
clipboard_hash_format: u32,
|
||||
clipboard_metadata_format: u32,
|
||||
}
|
||||
|
||||
pub(crate) struct WindowsPlatformState {
|
||||
@@ -88,6 +102,9 @@ impl WindowsPlatform {
|
||||
let icon = load_icon().unwrap_or_default();
|
||||
let state = RefCell::new(WindowsPlatformState::new());
|
||||
let raw_window_handles = RwLock::new(SmallVec::new());
|
||||
let clipboard_hash_format = register_clipboard_format(CLIPBOARD_HASH_FORMAT).unwrap();
|
||||
let clipboard_metadata_format =
|
||||
register_clipboard_format(CLIPBOARD_METADATA_FORMAT).unwrap();
|
||||
|
||||
Self {
|
||||
state,
|
||||
@@ -96,6 +113,8 @@ impl WindowsPlatform {
|
||||
background_executor,
|
||||
foreground_executor,
|
||||
text_system,
|
||||
clipboard_hash_format,
|
||||
clipboard_metadata_format,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -288,7 +307,7 @@ impl Platform for WindowsPlatform {
|
||||
self.icon,
|
||||
self.foreground_executor.clone(),
|
||||
lock.current_cursor,
|
||||
);
|
||||
)?;
|
||||
drop(lock);
|
||||
let handle = window.get_raw_handle();
|
||||
self.raw_window_handles.write().push(handle);
|
||||
@@ -498,17 +517,15 @@ impl Platform for WindowsPlatform {
|
||||
}
|
||||
|
||||
fn write_to_clipboard(&self, item: ClipboardItem) {
|
||||
if item.text.len() > 0 {
|
||||
set_clipboard_string(item.text()).unwrap();
|
||||
}
|
||||
write_to_clipboard(
|
||||
item,
|
||||
self.clipboard_hash_format,
|
||||
self.clipboard_metadata_format,
|
||||
);
|
||||
}
|
||||
|
||||
fn read_from_clipboard(&self) -> Option<ClipboardItem> {
|
||||
let text = get_clipboard_string().ok()?;
|
||||
Some(ClipboardItem {
|
||||
text,
|
||||
metadata: None,
|
||||
})
|
||||
read_from_clipboard(self.clipboard_hash_format, self.clipboard_metadata_format)
|
||||
}
|
||||
|
||||
fn write_credentials(&self, url: &str, username: &str, password: &[u8]) -> Task<Result<()>> {
|
||||
@@ -586,9 +603,8 @@ impl Platform for WindowsPlatform {
|
||||
|
||||
impl Drop for WindowsPlatform {
|
||||
fn drop(&mut self) {
|
||||
unsafe {
|
||||
OleUninitialize();
|
||||
}
|
||||
self.text_system.destroy();
|
||||
unsafe { OleUninitialize() };
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,3 +696,133 @@ fn should_auto_hide_scrollbars() -> Result<bool> {
|
||||
let ui_settings = UISettings::new()?;
|
||||
Ok(ui_settings.AutoHideScrollBars()?)
|
||||
}
|
||||
|
||||
fn register_clipboard_format(format: PCWSTR) -> Result<u32> {
|
||||
let ret = unsafe { RegisterClipboardFormatW(format) };
|
||||
if ret == 0 {
|
||||
Err(anyhow::anyhow!(
|
||||
"Error when registering clipboard format: {}",
|
||||
std::io::Error::last_os_error()
|
||||
))
|
||||
} else {
|
||||
Ok(ret)
|
||||
}
|
||||
}
|
||||
|
||||
fn write_to_clipboard(item: ClipboardItem, hash_format: u32, metadata_format: u32) {
|
||||
write_to_clipboard_inner(item, hash_format, metadata_format).log_err();
|
||||
unsafe { CloseClipboard().log_err() };
|
||||
}
|
||||
|
||||
fn write_to_clipboard_inner(
|
||||
item: ClipboardItem,
|
||||
hash_format: u32,
|
||||
metadata_format: u32,
|
||||
) -> Result<()> {
|
||||
unsafe {
|
||||
OpenClipboard(None)?;
|
||||
EmptyClipboard()?;
|
||||
let encode_wide = item.text.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_data_to_clipboard(&encode_wide, CF_UNICODETEXT.0 as u32)?;
|
||||
|
||||
if let Some(ref metadata) = item.metadata {
|
||||
let hash_result = {
|
||||
let hash = ClipboardItem::text_hash(&item.text);
|
||||
hash.to_ne_bytes()
|
||||
};
|
||||
let encode_wide = std::slice::from_raw_parts(hash_result.as_ptr().cast::<u16>(), 4);
|
||||
set_data_to_clipboard(encode_wide, hash_format)?;
|
||||
|
||||
let metadata_wide = metadata.encode_utf16().chain(Some(0)).collect_vec();
|
||||
set_data_to_clipboard(&metadata_wide, metadata_format)?;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn set_data_to_clipboard(data: &[u16], format: u32) -> Result<()> {
|
||||
unsafe {
|
||||
let global = GlobalAlloc(GMEM_MOVEABLE, data.len() * 2)?;
|
||||
let handle = GlobalLock(global);
|
||||
u_memcpy(handle as _, data.as_ptr(), data.len() as _);
|
||||
let _ = GlobalUnlock(global);
|
||||
SetClipboardData(format, HANDLE(global.0 as isize))?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn read_from_clipboard(hash_format: u32, metadata_format: u32) -> Option<ClipboardItem> {
|
||||
let result = read_from_clipboard_inner(hash_format, metadata_format).log_err();
|
||||
unsafe { CloseClipboard().log_err() };
|
||||
result
|
||||
}
|
||||
|
||||
fn read_from_clipboard_inner(hash_format: u32, metadata_format: u32) -> Result<ClipboardItem> {
|
||||
unsafe {
|
||||
OpenClipboard(None)?;
|
||||
let text = {
|
||||
let handle = GetClipboardData(CF_UNICODETEXT.0 as u32)?;
|
||||
let text = PCWSTR(handle.0 as *const u16);
|
||||
String::from_utf16_lossy(text.as_wide())
|
||||
};
|
||||
let mut item = ClipboardItem {
|
||||
text,
|
||||
metadata: None,
|
||||
};
|
||||
let Some(hash) = read_hash_from_clipboard(hash_format) else {
|
||||
return Ok(item);
|
||||
};
|
||||
let Some(metadata) = read_metadata_from_clipboard(metadata_format) else {
|
||||
return Ok(item);
|
||||
};
|
||||
if hash == ClipboardItem::text_hash(&item.text) {
|
||||
item.metadata = Some(metadata);
|
||||
}
|
||||
Ok(item)
|
||||
}
|
||||
}
|
||||
|
||||
fn read_hash_from_clipboard(hash_format: u32) -> Option<u64> {
|
||||
unsafe {
|
||||
let handle = GetClipboardData(hash_format).log_err()?;
|
||||
let raw_ptr = handle.0 as *const u16;
|
||||
let hash_bytes: [u8; 8] = std::slice::from_raw_parts(raw_ptr.cast::<u8>(), 8)
|
||||
.to_vec()
|
||||
.try_into()
|
||||
.log_err()?;
|
||||
Some(u64::from_ne_bytes(hash_bytes))
|
||||
}
|
||||
}
|
||||
|
||||
fn read_metadata_from_clipboard(metadata_format: u32) -> Option<String> {
|
||||
unsafe {
|
||||
let handle = GetClipboardData(metadata_format).log_err()?;
|
||||
let text = PCWSTR(handle.0 as *const u16);
|
||||
Some(String::from_utf16_lossy(text.as_wide()))
|
||||
}
|
||||
}
|
||||
|
||||
// clipboard
|
||||
pub const CLIPBOARD_HASH_FORMAT: PCWSTR = windows::core::w!("zed-text-hash");
|
||||
pub const CLIPBOARD_METADATA_FORMAT: PCWSTR = windows::core::w!("zed-metadata");
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::{ClipboardItem, Platform, WindowsPlatform};
|
||||
|
||||
#[test]
|
||||
fn test_clipboard() {
|
||||
let platform = WindowsPlatform::new();
|
||||
let item = ClipboardItem::new("你好".to_string());
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
|
||||
let item = ClipboardItem::new("12345".to_string());
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
|
||||
let item = ClipboardItem::new("abcdef".to_string()).with_metadata(vec![3, 4]);
|
||||
platform.write_to_clipboard(item.clone());
|
||||
assert_eq!(platform.read_from_clipboard(), Some(item));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,7 @@ use std::{
|
||||
};
|
||||
|
||||
use ::util::ResultExt;
|
||||
use anyhow::Context;
|
||||
use anyhow::{Context, Result};
|
||||
use futures::channel::oneshot::{self, Receiver};
|
||||
use itertools::Itertools;
|
||||
use raw_window_handle as rwh;
|
||||
@@ -35,6 +35,7 @@ pub struct WindowsWindowState {
|
||||
pub origin: Point<Pixels>,
|
||||
pub logical_size: Size<Pixels>,
|
||||
pub fullscreen_restore_bounds: Bounds<Pixels>,
|
||||
pub border_offset: WindowBorderOffset,
|
||||
pub scale_factor: f32,
|
||||
|
||||
pub callbacks: Callbacks,
|
||||
@@ -57,6 +58,7 @@ pub(crate) struct WindowsWindowStatePtr {
|
||||
pub(crate) state: RefCell<WindowsWindowState>,
|
||||
pub(crate) handle: AnyWindowHandle,
|
||||
pub(crate) hide_title_bar: bool,
|
||||
pub(crate) is_movable: bool,
|
||||
pub(crate) executor: ForegroundExecutor,
|
||||
}
|
||||
|
||||
@@ -67,7 +69,7 @@ impl WindowsWindowState {
|
||||
cs: &CREATESTRUCTW,
|
||||
current_cursor: HCURSOR,
|
||||
display: WindowsDisplay,
|
||||
) -> Self {
|
||||
) -> Result<Self> {
|
||||
let scale_factor = {
|
||||
let monitor_dpi = unsafe { GetDpiForWindow(hwnd) } as f32;
|
||||
monitor_dpi / USER_DEFAULT_SCREEN_DPI as f32
|
||||
@@ -81,7 +83,8 @@ impl WindowsWindowState {
|
||||
origin,
|
||||
size: logical_size,
|
||||
};
|
||||
let renderer = windows_renderer::windows_renderer(hwnd, transparent);
|
||||
let border_offset = WindowBorderOffset::default();
|
||||
let renderer = windows_renderer::windows_renderer(hwnd, transparent)?;
|
||||
let callbacks = Callbacks::default();
|
||||
let input_handler = None;
|
||||
let click_state = ClickState::new();
|
||||
@@ -89,10 +92,11 @@ impl WindowsWindowState {
|
||||
let nc_button_pressed = None;
|
||||
let fullscreen = None;
|
||||
|
||||
Self {
|
||||
Ok(Self {
|
||||
origin,
|
||||
logical_size,
|
||||
fullscreen_restore_bounds,
|
||||
border_offset,
|
||||
scale_factor,
|
||||
callbacks,
|
||||
input_handler,
|
||||
@@ -104,7 +108,7 @@ impl WindowsWindowState {
|
||||
display,
|
||||
fullscreen,
|
||||
hwnd,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[inline]
|
||||
@@ -123,7 +127,8 @@ impl WindowsWindowState {
|
||||
}
|
||||
}
|
||||
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
// Calculate the bounds used for saving and whether the window is maximized.
|
||||
fn calculate_window_bounds(&self) -> (Bounds<Pixels>, bool) {
|
||||
let placement = unsafe {
|
||||
let mut placement = WINDOWPLACEMENT {
|
||||
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
|
||||
@@ -132,22 +137,22 @@ impl WindowsWindowState {
|
||||
GetWindowPlacement(self.hwnd, &mut placement).log_err();
|
||||
placement
|
||||
};
|
||||
let physical_size = size(
|
||||
DevicePixels(placement.rcNormalPosition.right - placement.rcNormalPosition.left),
|
||||
DevicePixels(placement.rcNormalPosition.bottom - placement.rcNormalPosition.top),
|
||||
);
|
||||
let bounds = Bounds {
|
||||
origin: logical_point(
|
||||
placement.rcNormalPosition.left as f32,
|
||||
placement.rcNormalPosition.top as f32,
|
||||
(
|
||||
calculate_client_rect(
|
||||
placement.rcNormalPosition,
|
||||
self.border_offset,
|
||||
self.scale_factor,
|
||||
),
|
||||
size: physical_size.to_pixels(self.scale_factor),
|
||||
};
|
||||
placement.showCmd == SW_SHOWMAXIMIZED.0 as u32,
|
||||
)
|
||||
}
|
||||
|
||||
fn window_bounds(&self) -> WindowBounds {
|
||||
let (bounds, maximized) = self.calculate_window_bounds();
|
||||
|
||||
if self.is_fullscreen() {
|
||||
WindowBounds::Fullscreen(self.fullscreen_restore_bounds)
|
||||
} else if placement.showCmd == SW_SHOWMAXIMIZED.0 as u32 {
|
||||
} else if maximized {
|
||||
WindowBounds::Maximized(bounds)
|
||||
} else {
|
||||
WindowBounds::Windowed(bounds)
|
||||
@@ -198,22 +203,23 @@ impl WindowsWindowState {
|
||||
}
|
||||
|
||||
impl WindowsWindowStatePtr {
|
||||
fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Rc<Self> {
|
||||
fn new(context: &WindowCreateContext, hwnd: HWND, cs: &CREATESTRUCTW) -> Result<Rc<Self>> {
|
||||
let state = RefCell::new(WindowsWindowState::new(
|
||||
hwnd,
|
||||
context.transparent,
|
||||
cs,
|
||||
context.current_cursor,
|
||||
context.display,
|
||||
));
|
||||
)?);
|
||||
|
||||
Rc::new(Self {
|
||||
Ok(Rc::new(Self {
|
||||
state,
|
||||
hwnd,
|
||||
handle: context.handle,
|
||||
hide_title_bar: context.hide_title_bar,
|
||||
is_movable: context.is_movable,
|
||||
executor: context.executor.clone(),
|
||||
})
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,11 +236,12 @@ pub(crate) struct Callbacks {
|
||||
}
|
||||
|
||||
struct WindowCreateContext {
|
||||
inner: Option<Rc<WindowsWindowStatePtr>>,
|
||||
inner: Option<Result<Rc<WindowsWindowStatePtr>>>,
|
||||
handle: AnyWindowHandle,
|
||||
hide_title_bar: bool,
|
||||
display: WindowsDisplay,
|
||||
transparent: bool,
|
||||
is_movable: bool,
|
||||
executor: ForegroundExecutor,
|
||||
current_cursor: HCURSOR,
|
||||
}
|
||||
@@ -246,7 +253,7 @@ impl WindowsWindow {
|
||||
icon: HICON,
|
||||
executor: ForegroundExecutor,
|
||||
current_cursor: HCURSOR,
|
||||
) -> Self {
|
||||
) -> Result<Self> {
|
||||
let classname = register_wnd_class(icon);
|
||||
let hide_title_bar = params
|
||||
.titlebar
|
||||
@@ -261,7 +268,14 @@ impl WindowsWindow {
|
||||
.map(|title| title.as_ref())
|
||||
.unwrap_or(""),
|
||||
);
|
||||
let dwstyle = WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX;
|
||||
let (dwexstyle, dwstyle) = if params.kind == WindowKind::PopUp {
|
||||
(WS_EX_TOOLWINDOW, WINDOW_STYLE(0x0))
|
||||
} else {
|
||||
(
|
||||
WS_EX_APPWINDOW,
|
||||
WS_THICKFRAME | WS_SYSMENU | WS_MAXIMIZEBOX | WS_MINIMIZEBOX,
|
||||
)
|
||||
};
|
||||
let hinstance = get_module_handle();
|
||||
let display = if let Some(display_id) = params.display_id {
|
||||
// if we obtain a display_id, then this ID must be valid.
|
||||
@@ -275,13 +289,14 @@ impl WindowsWindow {
|
||||
hide_title_bar,
|
||||
display,
|
||||
transparent: true,
|
||||
is_movable: params.is_movable,
|
||||
executor,
|
||||
current_cursor,
|
||||
};
|
||||
let lpparam = Some(&context as *const _ as *const _);
|
||||
let raw_hwnd = unsafe {
|
||||
CreateWindowExW(
|
||||
WS_EX_APPWINDOW,
|
||||
dwexstyle,
|
||||
classname,
|
||||
&windowname,
|
||||
dwstyle,
|
||||
@@ -295,32 +310,31 @@ impl WindowsWindow {
|
||||
lpparam,
|
||||
)
|
||||
};
|
||||
let state_ptr = Rc::clone(context.inner.as_ref().unwrap());
|
||||
register_drag_drop(state_ptr.clone());
|
||||
let wnd = Self(state_ptr);
|
||||
let state_ptr = context.inner.take().unwrap()?;
|
||||
register_drag_drop(state_ptr.clone())?;
|
||||
|
||||
unsafe {
|
||||
let mut placement = WINDOWPLACEMENT {
|
||||
length: std::mem::size_of::<WINDOWPLACEMENT>() as u32,
|
||||
..Default::default()
|
||||
};
|
||||
GetWindowPlacement(raw_hwnd, &mut placement).log_err();
|
||||
GetWindowPlacement(raw_hwnd, &mut placement)?;
|
||||
// the bounds may be not inside the display
|
||||
let bounds = if display.check_given_bounds(params.bounds) {
|
||||
params.bounds
|
||||
} else {
|
||||
display.default_bounds()
|
||||
};
|
||||
let bounds = bounds.to_device_pixels(wnd.0.state.borrow().scale_factor);
|
||||
placement.rcNormalPosition.left = bounds.left().0;
|
||||
placement.rcNormalPosition.right = bounds.right().0;
|
||||
placement.rcNormalPosition.top = bounds.top().0;
|
||||
placement.rcNormalPosition.bottom = bounds.bottom().0;
|
||||
SetWindowPlacement(raw_hwnd, &placement).log_err();
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
let bounds = bounds.to_device_pixels(lock.scale_factor);
|
||||
lock.border_offset.udpate(raw_hwnd)?;
|
||||
placement.rcNormalPosition = calcualte_window_rect(bounds, lock.border_offset);
|
||||
drop(lock);
|
||||
SetWindowPlacement(raw_hwnd, &placement)?;
|
||||
}
|
||||
unsafe { ShowWindow(raw_hwnd, SW_SHOW).ok().log_err() };
|
||||
unsafe { ShowWindow(raw_hwnd, SW_SHOW).ok()? };
|
||||
|
||||
wnd
|
||||
Ok(Self(state_ptr))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -536,10 +550,6 @@ impl PlatformWindow for WindowsWindow {
|
||||
.executor
|
||||
.spawn(async move {
|
||||
let mut lock = state_ptr.state.borrow_mut();
|
||||
lock.fullscreen_restore_bounds = Bounds {
|
||||
origin: lock.origin,
|
||||
size: lock.logical_size,
|
||||
};
|
||||
let StyleAndBounds {
|
||||
style,
|
||||
x,
|
||||
@@ -549,6 +559,8 @@ impl PlatformWindow for WindowsWindow {
|
||||
} = if let Some(state) = lock.fullscreen.take() {
|
||||
state
|
||||
} else {
|
||||
let (window_bounds, _) = lock.calculate_window_bounds();
|
||||
lock.fullscreen_restore_bounds = window_bounds;
|
||||
let style =
|
||||
WINDOW_STYLE(unsafe { get_window_long(state_ptr.hwnd, GWL_STYLE) } as _);
|
||||
let mut rc = RECT::default();
|
||||
@@ -853,6 +865,32 @@ struct StyleAndBounds {
|
||||
cy: i32,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Clone, Copy)]
|
||||
pub(crate) struct WindowBorderOffset {
|
||||
width_offset: i32,
|
||||
height_offset: i32,
|
||||
}
|
||||
|
||||
impl WindowBorderOffset {
|
||||
pub(crate) fn udpate(&mut self, hwnd: HWND) -> anyhow::Result<()> {
|
||||
let window_rect = unsafe {
|
||||
let mut rect = std::mem::zeroed();
|
||||
GetWindowRect(hwnd, &mut rect)?;
|
||||
rect
|
||||
};
|
||||
let client_rect = unsafe {
|
||||
let mut rect = std::mem::zeroed();
|
||||
GetClientRect(hwnd, &mut rect)?;
|
||||
rect
|
||||
};
|
||||
self.width_offset =
|
||||
(window_rect.right - window_rect.left) - (client_rect.right - client_rect.left);
|
||||
self.height_offset =
|
||||
(window_rect.bottom - window_rect.top) - (client_rect.bottom - client_rect.top);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
fn register_wnd_class(icon_handle: HICON) -> PCWSTR {
|
||||
const CLASS_NAME: PCWSTR = w!("Zed::Window");
|
||||
|
||||
@@ -883,10 +921,14 @@ unsafe extern "system" fn wnd_proc(
|
||||
let cs = unsafe { &*cs };
|
||||
let ctx = cs.lpCreateParams as *mut WindowCreateContext;
|
||||
let ctx = unsafe { &mut *ctx };
|
||||
let state_ptr = WindowsWindowStatePtr::new(ctx, hwnd, cs);
|
||||
let weak = Box::new(Rc::downgrade(&state_ptr));
|
||||
let creation_result = WindowsWindowStatePtr::new(ctx, hwnd, cs);
|
||||
if creation_result.is_err() {
|
||||
ctx.inner = Some(creation_result);
|
||||
return LRESULT(0);
|
||||
}
|
||||
let weak = Box::new(Rc::downgrade(creation_result.as_ref().unwrap()));
|
||||
unsafe { set_window_long(hwnd, GWLP_USERDATA, Box::into_raw(weak) as isize) };
|
||||
ctx.inner = Some(state_ptr);
|
||||
ctx.inner = Some(creation_result);
|
||||
return LRESULT(1);
|
||||
}
|
||||
let ptr = unsafe { get_window_long(hwnd, GWLP_USERDATA) } as *mut Weak<WindowsWindowStatePtr>;
|
||||
@@ -934,7 +976,7 @@ fn get_module_handle() -> HMODULE {
|
||||
}
|
||||
}
|
||||
|
||||
fn register_drag_drop(state_ptr: Rc<WindowsWindowStatePtr>) {
|
||||
fn register_drag_drop(state_ptr: Rc<WindowsWindowStatePtr>) -> Result<()> {
|
||||
let window_handle = state_ptr.hwnd;
|
||||
let handler = WindowsDragDropHandler(state_ptr);
|
||||
// The lifetime of `IDropTarget` is handled by Windows, it wont release untill
|
||||
@@ -943,8 +985,54 @@ fn register_drag_drop(state_ptr: Rc<WindowsWindowStatePtr>) {
|
||||
let drag_drop_handler: IDropTarget = handler.into();
|
||||
unsafe {
|
||||
RegisterDragDrop(window_handle, &drag_drop_handler)
|
||||
.expect("unable to register drag-drop event")
|
||||
.context("unable to register drag-drop event")?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn calcualte_window_rect(bounds: Bounds<DevicePixels>, border_offset: WindowBorderOffset) -> RECT {
|
||||
// NOTE:
|
||||
// The reason that not using `AdjustWindowRectEx()` here is
|
||||
// that the size reported by this function is incorrect.
|
||||
// You can test it, and there are similar discussions online.
|
||||
// See: https://stackoverflow.com/questions/12423584/how-to-set-exact-client-size-for-overlapped-window-winapi
|
||||
//
|
||||
// So we manually calculate these values here.
|
||||
let mut rect = RECT {
|
||||
left: bounds.left().0,
|
||||
top: bounds.top().0,
|
||||
right: bounds.right().0,
|
||||
bottom: bounds.bottom().0,
|
||||
};
|
||||
let left_offset = border_offset.width_offset / 2;
|
||||
let top_offset = border_offset.height_offset / 2;
|
||||
let right_offset = border_offset.width_offset - left_offset;
|
||||
let bottom_offet = border_offset.height_offset - top_offset;
|
||||
rect.left -= left_offset;
|
||||
rect.top -= top_offset;
|
||||
rect.right += right_offset;
|
||||
rect.bottom += bottom_offet;
|
||||
rect
|
||||
}
|
||||
|
||||
fn calculate_client_rect(
|
||||
rect: RECT,
|
||||
border_offset: WindowBorderOffset,
|
||||
scale_factor: f32,
|
||||
) -> Bounds<Pixels> {
|
||||
let left_offset = border_offset.width_offset / 2;
|
||||
let top_offset = border_offset.height_offset / 2;
|
||||
let right_offset = border_offset.width_offset - left_offset;
|
||||
let bottom_offet = border_offset.height_offset - top_offset;
|
||||
let left = rect.left + left_offset;
|
||||
let top = rect.top + top_offset;
|
||||
let right = rect.right - right_offset;
|
||||
let bottom = rect.bottom - bottom_offet;
|
||||
let physical_size = size(DevicePixels(right - left), DevicePixels(bottom - top));
|
||||
Bounds {
|
||||
origin: logical_point(left as f32, top as f32, scale_factor),
|
||||
size: physical_size.to_pixels(scale_factor),
|
||||
}
|
||||
}
|
||||
|
||||
// https://learn.microsoft.com/en-us/windows/win32/api/shellapi/nf-shellapi-dragqueryfilew
|
||||
@@ -962,7 +1050,7 @@ mod windows_renderer {
|
||||
platform::blade::{BladeRenderer, BladeSurfaceConfig},
|
||||
};
|
||||
|
||||
pub(super) fn windows_renderer(hwnd: HWND, transparent: bool) -> BladeRenderer {
|
||||
pub(super) fn windows_renderer(hwnd: HWND, transparent: bool) -> anyhow::Result<BladeRenderer> {
|
||||
let raw = RawWindow { hwnd: hwnd.0 };
|
||||
let gpu: Arc<gpu::Context> = Arc::new(
|
||||
unsafe {
|
||||
@@ -975,14 +1063,14 @@ mod windows_renderer {
|
||||
},
|
||||
)
|
||||
}
|
||||
.unwrap(),
|
||||
.map_err(|e| anyhow::anyhow!("{:?}", e))?,
|
||||
);
|
||||
let config = BladeSurfaceConfig {
|
||||
size: gpu::Extent::default(),
|
||||
transparent,
|
||||
};
|
||||
|
||||
BladeRenderer::new(gpu, config)
|
||||
Ok(BladeRenderer::new(gpu, config))
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
|
||||
@@ -212,6 +212,8 @@ impl Default for TextStyle {
|
||||
// todo(linux) make this configurable or choose better default
|
||||
font_family: if cfg!(target_os = "linux") {
|
||||
"FreeMono".into()
|
||||
} else if cfg!(target_os = "windows") {
|
||||
"Segoe UI".into()
|
||||
} else {
|
||||
"Helvetica".into()
|
||||
},
|
||||
|
||||
@@ -190,18 +190,10 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to justify flex items along the container's main axis
|
||||
/// such that there is an equal amount of space between each item.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
|
||||
fn justify_between(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::SpaceBetween);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to justify flex items along the center of the container's main axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#center)
|
||||
fn justify_center(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::Center);
|
||||
/// Sets the element to align flex items along the baseline of the container's cross axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/align-items#baseline)
|
||||
fn items_baseline(mut self) -> Self {
|
||||
self.style().align_items = Some(AlignItems::Baseline);
|
||||
self
|
||||
}
|
||||
|
||||
@@ -219,6 +211,21 @@ pub trait Styled: Sized {
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to justify flex items along the center of the container's main axis.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#center)
|
||||
fn justify_center(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::Center);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to justify flex items along the container's main axis
|
||||
/// such that there is an equal amount of space between each item.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#space-between)
|
||||
fn justify_between(mut self) -> Self {
|
||||
self.style().justify_content = Some(JustifyContent::SpaceBetween);
|
||||
self
|
||||
}
|
||||
|
||||
/// Sets the element to justify items along the container's main axis such
|
||||
/// that there is an equal amount of space on each side of each item.
|
||||
/// [Docs](https://tailwindcss.com/docs/justify-content#space-around)
|
||||
|
||||
@@ -66,6 +66,7 @@ impl TextSystem {
|
||||
// We should allow GPUI users to provide their own fallback font stack.
|
||||
font("Zed Plex Mono"),
|
||||
font("Helvetica"),
|
||||
font("Segoe UI"), // Windows
|
||||
font("Cantarell"), // Gnome
|
||||
font("Ubuntu"), // Gnome (Ubuntu)
|
||||
font("Noto Sans"), // KDE
|
||||
|
||||
@@ -109,7 +109,13 @@ fn paint_line(
|
||||
wrap_boundaries: &[WrapBoundary],
|
||||
cx: &mut WindowContext,
|
||||
) -> Result<()> {
|
||||
let line_bounds = Bounds::new(origin, size(layout.width, line_height));
|
||||
let line_bounds = Bounds::new(
|
||||
origin,
|
||||
size(
|
||||
layout.width,
|
||||
line_height * (wrap_boundaries.len() as f32 + 1.),
|
||||
),
|
||||
);
|
||||
cx.paint_layer(line_bounds, |cx| {
|
||||
let padding_top = (line_height - layout.ascent - layout.descent) / 2.;
|
||||
let baseline_offset = point(px(0.), padding_top + layout.ascent);
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
mod item;
|
||||
mod to_markdown;
|
||||
|
||||
use futures::future::BoxFuture;
|
||||
pub use item::*;
|
||||
pub use to_markdown::convert_rustdoc_to_markdown;
|
||||
|
||||
@@ -11,7 +12,7 @@ use anyhow::{bail, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::{HashSet, VecDeque};
|
||||
use fs::Fs;
|
||||
use futures::AsyncReadExt;
|
||||
use futures::{AsyncReadExt, FutureExt};
|
||||
use http::{AsyncBody, HttpClient, HttpClientWithUrl};
|
||||
|
||||
use crate::{IndexedDocsDatabase, IndexedDocsProvider, PackageName, ProviderId};
|
||||
@@ -23,124 +24,16 @@ struct RustdocItemWithHistory {
|
||||
pub history: Vec<String>,
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
pub trait RustdocProvider {
|
||||
async fn fetch_page(
|
||||
&self,
|
||||
package: &PackageName,
|
||||
item: Option<&RustdocItem>,
|
||||
) -> Result<Option<String>>;
|
||||
}
|
||||
|
||||
pub struct RustdocIndexer {
|
||||
provider: Box<dyn RustdocProvider + Send + Sync + 'static>,
|
||||
}
|
||||
|
||||
impl RustdocIndexer {
|
||||
pub fn new(provider: Box<dyn RustdocProvider + Send + Sync + 'static>) -> Self {
|
||||
Self { provider }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl IndexedDocsProvider for RustdocIndexer {
|
||||
fn id(&self) -> ProviderId {
|
||||
ProviderId::rustdoc()
|
||||
}
|
||||
|
||||
fn database_path(&self) -> PathBuf {
|
||||
paths::support_dir().join("docs/rust/rustdoc-db.1.mdb")
|
||||
}
|
||||
|
||||
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
|
||||
let Some(package_root_content) = self.provider.fetch_page(&package, None).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let (crate_root_markdown, items) =
|
||||
convert_rustdoc_to_markdown(package_root_content.as_bytes())?;
|
||||
|
||||
database
|
||||
.insert(package.to_string(), crate_root_markdown)
|
||||
.await?;
|
||||
|
||||
let mut seen_items = HashSet::from_iter(items.clone());
|
||||
let mut items_to_visit: VecDeque<RustdocItemWithHistory> =
|
||||
VecDeque::from_iter(items.into_iter().map(|item| RustdocItemWithHistory {
|
||||
item,
|
||||
#[cfg(debug_assertions)]
|
||||
history: Vec::new(),
|
||||
}));
|
||||
|
||||
while let Some(item_with_history) = items_to_visit.pop_front() {
|
||||
let item = &item_with_history.item;
|
||||
|
||||
let Some(result) = self
|
||||
.provider
|
||||
.fetch_page(&package, Some(&item))
|
||||
.await
|
||||
.with_context(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"failed to fetch {item:?}: {history:?}",
|
||||
history = item_with_history.history
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
format!("failed to fetch {item:?}")
|
||||
}
|
||||
})?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (markdown, referenced_items) = convert_rustdoc_to_markdown(result.as_bytes())?;
|
||||
|
||||
database
|
||||
.insert(format!("{package}::{}", item.display()), markdown)
|
||||
.await?;
|
||||
|
||||
let parent_item = item;
|
||||
for mut item in referenced_items {
|
||||
if seen_items.contains(&item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen_items.insert(item.clone());
|
||||
|
||||
item.path.extend(parent_item.path.clone());
|
||||
match parent_item.kind {
|
||||
RustdocItemKind::Mod => {
|
||||
item.path.push(parent_item.name.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
items_to_visit.push_back(RustdocItemWithHistory {
|
||||
#[cfg(debug_assertions)]
|
||||
history: {
|
||||
let mut history = item_with_history.history.clone();
|
||||
history.push(item.url_path());
|
||||
history
|
||||
},
|
||||
item,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
pub struct LocalProvider {
|
||||
pub struct LocalRustdocProvider {
|
||||
fs: Arc<dyn Fs>,
|
||||
cargo_workspace_root: PathBuf,
|
||||
}
|
||||
|
||||
impl LocalProvider {
|
||||
impl LocalRustdocProvider {
|
||||
pub fn id() -> ProviderId {
|
||||
ProviderId("rustdoc".into())
|
||||
}
|
||||
|
||||
pub fn new(fs: Arc<dyn Fs>, cargo_workspace_root: PathBuf) -> Self {
|
||||
Self {
|
||||
fs,
|
||||
@@ -150,25 +43,53 @@ impl LocalProvider {
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RustdocProvider for LocalProvider {
|
||||
async fn fetch_page(
|
||||
&self,
|
||||
crate_name: &PackageName,
|
||||
item: Option<&RustdocItem>,
|
||||
) -> Result<Option<String>> {
|
||||
let mut local_cargo_doc_path = self.cargo_workspace_root.join("target/doc");
|
||||
local_cargo_doc_path.push(crate_name.as_ref());
|
||||
if let Some(item) = item {
|
||||
local_cargo_doc_path.push(item.url_path());
|
||||
} else {
|
||||
local_cargo_doc_path.push("index.html");
|
||||
}
|
||||
impl IndexedDocsProvider for LocalRustdocProvider {
|
||||
fn id(&self) -> ProviderId {
|
||||
Self::id()
|
||||
}
|
||||
|
||||
let Ok(contents) = self.fs.load(&local_cargo_doc_path).await else {
|
||||
return Ok(None);
|
||||
};
|
||||
fn database_path(&self) -> PathBuf {
|
||||
paths::support_dir().join("docs/rust/rustdoc-db.1.mdb")
|
||||
}
|
||||
|
||||
Ok(Some(contents))
|
||||
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
|
||||
index_rustdoc(package, database, {
|
||||
move |crate_name, item| {
|
||||
let fs = self.fs.clone();
|
||||
let cargo_workspace_root = self.cargo_workspace_root.clone();
|
||||
let crate_name = crate_name.clone();
|
||||
let item = item.cloned();
|
||||
async move {
|
||||
let target_doc_path = cargo_workspace_root.join("target/doc");
|
||||
let mut local_cargo_doc_path = target_doc_path.join(crate_name.as_ref());
|
||||
|
||||
if !fs.is_dir(&local_cargo_doc_path).await {
|
||||
let cargo_doc_exists_at_all = fs.is_dir(&target_doc_path).await;
|
||||
if cargo_doc_exists_at_all {
|
||||
bail!(
|
||||
"no docs directory for '{crate_name}'. if this is a valid crate name, try running `cargo doc`"
|
||||
);
|
||||
} else {
|
||||
bail!("no cargo doc directory. run `cargo doc`");
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(item) = item {
|
||||
local_cargo_doc_path.push(item.url_path());
|
||||
} else {
|
||||
local_cargo_doc_path.push("index.html");
|
||||
}
|
||||
|
||||
let Ok(contents) = fs.load(&local_cargo_doc_path).await else {
|
||||
return Ok(None);
|
||||
};
|
||||
|
||||
Ok(Some(contents))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
@@ -177,50 +98,152 @@ pub struct DocsDotRsProvider {
|
||||
}
|
||||
|
||||
impl DocsDotRsProvider {
|
||||
pub fn id() -> ProviderId {
|
||||
ProviderId("docs-rs".into())
|
||||
}
|
||||
|
||||
pub fn new(http_client: Arc<HttpClientWithUrl>) -> Self {
|
||||
Self { http_client }
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait]
|
||||
impl RustdocProvider for DocsDotRsProvider {
|
||||
async fn fetch_page(
|
||||
&self,
|
||||
crate_name: &PackageName,
|
||||
item: Option<&RustdocItem>,
|
||||
) -> Result<Option<String>> {
|
||||
let version = "latest";
|
||||
let path = format!(
|
||||
"{crate_name}/{version}/{crate_name}{item_path}",
|
||||
item_path = item
|
||||
.map(|item| format!("/{}", item.url_path()))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
impl IndexedDocsProvider for DocsDotRsProvider {
|
||||
fn id(&self) -> ProviderId {
|
||||
Self::id()
|
||||
}
|
||||
|
||||
let mut response = self
|
||||
.http_client
|
||||
.get(
|
||||
&format!("https://docs.rs/{path}"),
|
||||
AsyncBody::default(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
fn database_path(&self) -> PathBuf {
|
||||
paths::support_dir().join("docs/rust/docs-rs-db.1.mdb")
|
||||
}
|
||||
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading docs.rs response body")?;
|
||||
async fn index(&self, package: PackageName, database: Arc<IndexedDocsDatabase>) -> Result<()> {
|
||||
index_rustdoc(package, database, {
|
||||
move |crate_name, item| {
|
||||
let http_client = self.http_client.clone();
|
||||
let crate_name = crate_name.clone();
|
||||
let item = item.cloned();
|
||||
async move {
|
||||
let version = "latest";
|
||||
let path = format!(
|
||||
"{crate_name}/{version}/{crate_name}{item_path}",
|
||||
item_path = item
|
||||
.map(|item| format!("/{}", item.url_path()))
|
||||
.unwrap_or_default()
|
||||
);
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
let mut response = http_client
|
||||
.get(
|
||||
&format!("https://docs.rs/{path}"),
|
||||
AsyncBody::default(),
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(Some(String::from_utf8(body)?))
|
||||
let mut body = Vec::new();
|
||||
response
|
||||
.body_mut()
|
||||
.read_to_end(&mut body)
|
||||
.await
|
||||
.context("error reading docs.rs response body")?;
|
||||
|
||||
if response.status().is_client_error() {
|
||||
let text = String::from_utf8_lossy(body.as_slice());
|
||||
bail!(
|
||||
"status error {}, response: {text:?}",
|
||||
response.status().as_u16()
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some(String::from_utf8(body)?))
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
})
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
async fn index_rustdoc(
|
||||
package: PackageName,
|
||||
database: Arc<IndexedDocsDatabase>,
|
||||
fetch_page: impl Fn(&PackageName, Option<&RustdocItem>) -> BoxFuture<'static, Result<Option<String>>>
|
||||
+ Send
|
||||
+ Sync,
|
||||
) -> Result<()> {
|
||||
let Some(package_root_content) = fetch_page(&package, None).await? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let (crate_root_markdown, items) =
|
||||
convert_rustdoc_to_markdown(package_root_content.as_bytes())?;
|
||||
|
||||
database
|
||||
.insert(package.to_string(), crate_root_markdown)
|
||||
.await?;
|
||||
|
||||
let mut seen_items = HashSet::from_iter(items.clone());
|
||||
let mut items_to_visit: VecDeque<RustdocItemWithHistory> =
|
||||
VecDeque::from_iter(items.into_iter().map(|item| RustdocItemWithHistory {
|
||||
item,
|
||||
#[cfg(debug_assertions)]
|
||||
history: Vec::new(),
|
||||
}));
|
||||
|
||||
while let Some(item_with_history) = items_to_visit.pop_front() {
|
||||
let item = &item_with_history.item;
|
||||
|
||||
let Some(result) = fetch_page(&package, Some(&item)).await.with_context(|| {
|
||||
#[cfg(debug_assertions)]
|
||||
{
|
||||
format!(
|
||||
"failed to fetch {item:?}: {history:?}",
|
||||
history = item_with_history.history
|
||||
)
|
||||
}
|
||||
|
||||
#[cfg(not(debug_assertions))]
|
||||
{
|
||||
format!("failed to fetch {item:?}")
|
||||
}
|
||||
})?
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
let (markdown, referenced_items) = convert_rustdoc_to_markdown(result.as_bytes())?;
|
||||
|
||||
database
|
||||
.insert(format!("{package}::{}", item.display()), markdown)
|
||||
.await?;
|
||||
|
||||
let parent_item = item;
|
||||
for mut item in referenced_items {
|
||||
if seen_items.contains(&item) {
|
||||
continue;
|
||||
}
|
||||
|
||||
seen_items.insert(item.clone());
|
||||
|
||||
item.path.extend(parent_item.path.clone());
|
||||
match parent_item.kind {
|
||||
RustdocItemKind::Mod => {
|
||||
item.path.push(parent_item.name.clone());
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
items_to_visit.push_back(RustdocItemWithHistory {
|
||||
#[cfg(debug_assertions)]
|
||||
history: {
|
||||
let mut history = item_with_history.history.clone();
|
||||
history.push(item.url_path());
|
||||
history
|
||||
},
|
||||
item,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -21,12 +21,6 @@ use crate::IndexedDocsRegistry;
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)]
|
||||
pub struct ProviderId(pub Arc<str>);
|
||||
|
||||
impl ProviderId {
|
||||
pub fn rustdoc() -> Self {
|
||||
Self("rustdoc".into())
|
||||
}
|
||||
}
|
||||
|
||||
/// The name of a package.
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Deref, Display)]
|
||||
pub struct PackageName(Arc<str>);
|
||||
@@ -57,6 +51,7 @@ pub struct IndexedDocsStore {
|
||||
provider: Box<dyn IndexedDocsProvider + Send + Sync + 'static>,
|
||||
indexing_tasks_by_package:
|
||||
RwLock<HashMap<PackageName, Shared<Task<Result<(), Arc<anyhow::Error>>>>>>,
|
||||
latest_errors_by_package: RwLock<HashMap<PackageName, Arc<str>>>,
|
||||
}
|
||||
|
||||
impl IndexedDocsStore {
|
||||
@@ -86,9 +81,14 @@ impl IndexedDocsStore {
|
||||
database_future,
|
||||
provider,
|
||||
indexing_tasks_by_package: RwLock::new(HashMap::default()),
|
||||
latest_errors_by_package: RwLock::new(HashMap::default()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn latest_error_for_package(&self, package: &PackageName) -> Option<Arc<str>> {
|
||||
self.latest_errors_by_package.read().get(package).cloned()
|
||||
}
|
||||
|
||||
/// Returns whether the package with the given name is currently being indexed.
|
||||
pub fn is_indexing(&self, package: &PackageName) -> bool {
|
||||
self.indexing_tasks_by_package.read().contains_key(package)
|
||||
@@ -103,6 +103,15 @@ impl IndexedDocsStore {
|
||||
.await
|
||||
}
|
||||
|
||||
pub async fn load_many_by_prefix(&self, prefix: String) -> Result<Vec<(String, MarkdownDocs)>> {
|
||||
self.database_future
|
||||
.clone()
|
||||
.await
|
||||
.map_err(|err| anyhow!(err))?
|
||||
.load_many_by_prefix(prefix)
|
||||
.await
|
||||
}
|
||||
|
||||
pub fn index(
|
||||
self: Arc<Self>,
|
||||
package: PackageName,
|
||||
@@ -125,16 +134,31 @@ impl IndexedDocsStore {
|
||||
}
|
||||
});
|
||||
|
||||
let index_task = async {
|
||||
let database = this
|
||||
.database_future
|
||||
.clone()
|
||||
.await
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
this.provider.index(package, database).await
|
||||
let index_task = {
|
||||
let package = package.clone();
|
||||
async {
|
||||
let database = this
|
||||
.database_future
|
||||
.clone()
|
||||
.await
|
||||
.map_err(|err| anyhow!(err))?;
|
||||
this.provider.index(package, database).await
|
||||
}
|
||||
};
|
||||
|
||||
index_task.await.map_err(Arc::new)
|
||||
let result = index_task.await.map_err(Arc::new);
|
||||
match &result {
|
||||
Ok(_) => {
|
||||
this.latest_errors_by_package.write().remove(&package);
|
||||
}
|
||||
Err(err) => {
|
||||
this.latest_errors_by_package
|
||||
.write()
|
||||
.insert(package, err.to_string().into());
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
})
|
||||
.shared();
|
||||
@@ -242,6 +266,28 @@ impl IndexedDocsDatabase {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn load_many_by_prefix(&self, prefix: String) -> Task<Result<Vec<(String, MarkdownDocs)>>> {
|
||||
let env = self.env.clone();
|
||||
let entries = self.entries;
|
||||
|
||||
self.executor.spawn(async move {
|
||||
let txn = env.read_txn()?;
|
||||
let results = entries
|
||||
.iter(&txn)?
|
||||
.filter_map(|entry| {
|
||||
let (key, value) = entry.ok()?;
|
||||
if key.starts_with(&prefix) {
|
||||
Some((key, value))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Ok(results)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn insert(&self, key: String, docs: String) -> Task<Result<()>> {
|
||||
let env = self.env.clone();
|
||||
let entries = self.entries;
|
||||
|
||||
@@ -122,7 +122,7 @@ lazy_static! {
|
||||
pub static ref PLAIN_TEXT: Arc<Language> = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Plain Text".into(),
|
||||
soft_wrap: Some(SoftWrap::PreferredLineLength),
|
||||
soft_wrap: Some(SoftWrap::EditorWidth),
|
||||
..Default::default()
|
||||
},
|
||||
None,
|
||||
|
||||
@@ -13,5 +13,4 @@ brackets = [
|
||||
]
|
||||
|
||||
tab_size = 2
|
||||
soft_wrap = "preferred_line_length"
|
||||
prettier_parser_name = "markdown"
|
||||
|
||||
@@ -1,9 +1,13 @@
|
||||
use anyhow::Result;
|
||||
use async_trait::async_trait;
|
||||
use gpui::AppContext;
|
||||
use gpui::AsyncAppContext;
|
||||
use language::{ContextProvider, LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::LanguageServerBinary;
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use serde_json::Value;
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
@@ -25,6 +29,8 @@ pub struct PythonLspAdapter {
|
||||
}
|
||||
|
||||
impl PythonLspAdapter {
|
||||
const SERVER_NAME: &'static str = "pyright";
|
||||
|
||||
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
|
||||
PythonLspAdapter { node }
|
||||
}
|
||||
@@ -33,14 +39,18 @@ impl PythonLspAdapter {
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for PythonLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("pyright".into())
|
||||
LanguageServerName(Self::SERVER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
&self,
|
||||
_: &dyn LspAdapterDelegate,
|
||||
) -> Result<Box<dyn 'static + Any + Send>> {
|
||||
Ok(Box::new(self.node.npm_package_latest_version("pyright").await?) as Box<_>)
|
||||
Ok(Box::new(
|
||||
self.node
|
||||
.npm_package_latest_version(Self::SERVER_NAME)
|
||||
.await?,
|
||||
) as Box<_>)
|
||||
}
|
||||
|
||||
async fn fetch_server_binary(
|
||||
@@ -51,7 +61,7 @@ impl LspAdapter for PythonLspAdapter {
|
||||
) -> Result<LanguageServerBinary> {
|
||||
let latest_version = latest_version.downcast::<String>().unwrap();
|
||||
let server_path = container_dir.join(SERVER_PATH);
|
||||
let package_name = "pyright";
|
||||
let package_name = Self::SERVER_NAME;
|
||||
|
||||
let should_install_language_server = self
|
||||
.node
|
||||
@@ -164,6 +174,20 @@ impl LspAdapter for PythonLspAdapter {
|
||||
filter_range,
|
||||
})
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
cx.update(|cx| {
|
||||
ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get(Self::SERVER_NAME)
|
||||
.and_then(|s| s.settings.clone())
|
||||
.unwrap_or_default()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_cached_server_binary(
|
||||
|
||||
@@ -575,12 +575,11 @@ fn retrieve_package_id_and_bin_name_from_metadata(
|
||||
metadata: CargoMetadata,
|
||||
abs_path: &Path,
|
||||
) -> Option<(String, String)> {
|
||||
let abs_path = abs_path.to_str()?;
|
||||
|
||||
for package in metadata.packages {
|
||||
for target in package.targets {
|
||||
let is_bin = target.kind.iter().any(|kind| kind == "bin");
|
||||
if target.src_path == abs_path && is_bin {
|
||||
let target_path = PathBuf::from(target.src_path);
|
||||
if target_path == abs_path && is_bin {
|
||||
return Some((package.id, target.name));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,10 +68,22 @@ pub struct TypeScriptLspAdapter {
|
||||
impl TypeScriptLspAdapter {
|
||||
const OLD_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.js";
|
||||
const NEW_SERVER_PATH: &'static str = "node_modules/typescript-language-server/lib/cli.mjs";
|
||||
|
||||
const SERVER_NAME: &'static str = "typescript-language-server";
|
||||
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
|
||||
TypeScriptLspAdapter { node }
|
||||
}
|
||||
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
|
||||
let is_yarn = adapter
|
||||
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
|
||||
.await
|
||||
.is_ok();
|
||||
|
||||
if is_yarn {
|
||||
".yarn/sdks/typescript/lib"
|
||||
} else {
|
||||
"node_modules/typescript/lib"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TypeScriptVersions {
|
||||
@@ -82,7 +94,7 @@ struct TypeScriptVersions {
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for TypeScriptLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("typescript-language-server".into())
|
||||
LanguageServerName(Self::SERVER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
@@ -196,13 +208,14 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
adapter: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let tsdk_path = Self::tsdk_path(adapter).await;
|
||||
Ok(Some(json!({
|
||||
"provideFormatter": true,
|
||||
"hostInfo": "zed",
|
||||
"tsserver": {
|
||||
"path": "node_modules/typescript/lib",
|
||||
"path": tsdk_path,
|
||||
},
|
||||
"preferences": {
|
||||
"includeInlayParameterNameHints": "all",
|
||||
@@ -220,8 +233,17 @@ impl LspAdapter for TypeScriptLspAdapter {
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_cx: &mut AsyncAppContext,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
let override_options = cx.update(|cx| {
|
||||
ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get(Self::SERVER_NAME)
|
||||
.and_then(|s| s.initialization_options.clone())
|
||||
})?;
|
||||
if let Some(options) = override_options {
|
||||
return Ok(options);
|
||||
}
|
||||
Ok(json!({
|
||||
"completions": {
|
||||
"completeFunctionCalls": true
|
||||
|
||||
@@ -5,7 +5,9 @@ use gpui::AsyncAppContext;
|
||||
use language::{LanguageServerName, LspAdapter, LspAdapterDelegate};
|
||||
use lsp::{CodeActionKind, LanguageServerBinary};
|
||||
use node_runtime::NodeRuntime;
|
||||
use project::project_settings::ProjectSettings;
|
||||
use serde_json::{json, Value};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::Any,
|
||||
ffi::OsString,
|
||||
@@ -28,6 +30,18 @@ impl VtslsLspAdapter {
|
||||
pub fn new(node: Arc<dyn NodeRuntime>) -> Self {
|
||||
VtslsLspAdapter { node }
|
||||
}
|
||||
async fn tsdk_path(adapter: &Arc<dyn LspAdapterDelegate>) -> &'static str {
|
||||
let is_yarn = adapter
|
||||
.read_text_file(PathBuf::from(".yarn/sdks/typescript/lib/typescript.js"))
|
||||
.await
|
||||
.is_ok();
|
||||
|
||||
if is_yarn {
|
||||
".yarn/sdks/typescript/lib"
|
||||
} else {
|
||||
"node_modules/typescript/lib"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct TypeScriptVersions {
|
||||
@@ -35,10 +49,11 @@ struct TypeScriptVersions {
|
||||
server_version: String,
|
||||
}
|
||||
|
||||
const SERVER_NAME: &'static str = "vtsls";
|
||||
#[async_trait(?Send)]
|
||||
impl LspAdapter for VtslsLspAdapter {
|
||||
fn name(&self) -> LanguageServerName {
|
||||
LanguageServerName("vtsls".into())
|
||||
LanguageServerName(SERVER_NAME.into())
|
||||
}
|
||||
|
||||
async fn fetch_latest_server_version(
|
||||
@@ -159,11 +174,12 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
|
||||
async fn initialization_options(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
adapter: &Arc<dyn LspAdapterDelegate>,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
let tsdk_path = Self::tsdk_path(&adapter).await;
|
||||
Ok(Some(json!({
|
||||
"typescript": {
|
||||
"tsdk": "node_modules/typescript/lib",
|
||||
"tsdk": tsdk_path,
|
||||
"format": {
|
||||
"enable": true
|
||||
},
|
||||
@@ -196,22 +212,33 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoUseWorkspaceTsdk": true
|
||||
}
|
||||
})))
|
||||
}
|
||||
|
||||
async fn workspace_configuration(
|
||||
self: Arc<Self>,
|
||||
_: &Arc<dyn LspAdapterDelegate>,
|
||||
_cx: &mut AsyncAppContext,
|
||||
adapter: &Arc<dyn LspAdapterDelegate>,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<Value> {
|
||||
let override_options = cx.update(|cx| {
|
||||
ProjectSettings::get_global(cx)
|
||||
.lsp
|
||||
.get(SERVER_NAME)
|
||||
.and_then(|s| s.initialization_options.clone())
|
||||
})?;
|
||||
if let Some(options) = override_options {
|
||||
return Ok(options);
|
||||
}
|
||||
let tsdk_path = Self::tsdk_path(&adapter).await;
|
||||
Ok(json!({
|
||||
"typescript": {
|
||||
"suggest": {
|
||||
"completeFunctionCalls": true
|
||||
},
|
||||
"tsdk": "node_modules/typescript/lib",
|
||||
"tsdk": tsdk_path,
|
||||
"format": {
|
||||
"enable": true
|
||||
},
|
||||
@@ -244,7 +271,8 @@ impl LspAdapter for VtslsLspAdapter {
|
||||
"enableServerSideFuzzyMatch": true,
|
||||
"entriesLimit": 5000,
|
||||
}
|
||||
}
|
||||
},
|
||||
"autoUseWorkspaceTsdk": true
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -57,7 +57,7 @@ gpui = { workspace = true, features = ["test-support"] }
|
||||
live_kit_server.workspace = true
|
||||
nanoid.workspace = true
|
||||
sha2.workspace = true
|
||||
simplelog = "0.9"
|
||||
simplelog.workspace = true
|
||||
|
||||
[build-dependencies]
|
||||
serde.workspace = true
|
||||
|
||||
@@ -645,7 +645,24 @@ impl LanguageServer {
|
||||
on_type_formatting: Some(DynamicRegistrationClientCapabilities {
|
||||
dynamic_registration: None,
|
||||
}),
|
||||
..Default::default()
|
||||
signature_help: Some(SignatureHelpClientCapabilities {
|
||||
signature_information: Some(SignatureInformationSettings {
|
||||
documentation_format: Some(vec![
|
||||
MarkupKind::Markdown,
|
||||
MarkupKind::PlainText,
|
||||
]),
|
||||
parameter_information: Some(ParameterInformationSettings {
|
||||
label_offset_support: Some(true),
|
||||
}),
|
||||
active_parameter_support: Some(true),
|
||||
}),
|
||||
..SignatureHelpClientCapabilities::default()
|
||||
}),
|
||||
synchronization: Some(TextDocumentSyncClientCapabilities {
|
||||
did_save: Some(true),
|
||||
..TextDocumentSyncClientCapabilities::default()
|
||||
}),
|
||||
..TextDocumentClientCapabilities::default()
|
||||
}),
|
||||
experimental: Some(json!({
|
||||
"serverStatusNotification": true,
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use assets::Assets;
|
||||
use gpui::{prelude::*, App, KeyBinding, Task, View, WindowOptions};
|
||||
use gpui::{prelude::*, rgb, App, KeyBinding, StyleRefinement, Task, View, WindowOptions};
|
||||
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
@@ -105,44 +105,49 @@ pub fn main() {
|
||||
cx.activate(true);
|
||||
cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|cx| {
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: gpui::TextStyle {
|
||||
font_family: "Zed Plex Sans".into(),
|
||||
color: cx.theme().colors().terminal_ansi_black,
|
||||
..Default::default()
|
||||
},
|
||||
code_block: StyleRefinement::default()
|
||||
.font_family("Zed Plex Mono")
|
||||
.m(rems(1.))
|
||||
.bg(rgb(0xAAAAAAA)),
|
||||
inline_code: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Mono".into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Color::Muted.color(cx),
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: {
|
||||
let mut selection = cx.theme().players().local().selection;
|
||||
selection.fade_out(0.7);
|
||||
selection
|
||||
},
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
MarkdownExample::new(
|
||||
MARKDOWN_EXAMPLE.to_string(),
|
||||
MarkdownStyle {
|
||||
code_block: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Plex Mono".into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Plex Mono".into()),
|
||||
// @nate: Could we add inline-code specific styles to the theme?
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Color::Muted.color(cx),
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: {
|
||||
let mut selection = cx.theme().players().local().selection;
|
||||
selection.fade_out(0.7);
|
||||
selection
|
||||
},
|
||||
},
|
||||
markdown_style,
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
@@ -163,7 +168,8 @@ impl MarkdownExample {
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut WindowContext,
|
||||
) -> Self {
|
||||
let markdown = cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx));
|
||||
let markdown =
|
||||
cx.new_view(|cx| Markdown::new(text, style, Some(language_registry), cx, None));
|
||||
Self { markdown }
|
||||
}
|
||||
}
|
||||
|
||||
120
crates/markdown/examples/markdown_as_child.rs
Normal file
120
crates/markdown/examples/markdown_as_child.rs
Normal file
@@ -0,0 +1,120 @@
|
||||
use assets::Assets;
|
||||
use gpui::*;
|
||||
use language::{language_settings::AllLanguageSettings, LanguageRegistry};
|
||||
use markdown::{Markdown, MarkdownStyle};
|
||||
use node_runtime::FakeNodeRuntime;
|
||||
use settings::SettingsStore;
|
||||
use std::sync::Arc;
|
||||
use theme::LoadThemes;
|
||||
use ui::div;
|
||||
use ui::prelude::*;
|
||||
|
||||
const MARKDOWN_EXAMPLE: &'static str = r#"
|
||||
this text should be selectable
|
||||
|
||||
wow so cool
|
||||
|
||||
## Heading 2
|
||||
"#;
|
||||
pub fn main() {
|
||||
env_logger::init();
|
||||
|
||||
App::new().with_assets(Assets).run(|cx| {
|
||||
let store = SettingsStore::test(cx);
|
||||
cx.set_global(store);
|
||||
language::init(cx);
|
||||
SettingsStore::update(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |_| {});
|
||||
});
|
||||
cx.bind_keys([KeyBinding::new("cmd-c", markdown::Copy, None)]);
|
||||
|
||||
let node_runtime = FakeNodeRuntime::new();
|
||||
let language_registry = Arc::new(LanguageRegistry::new(
|
||||
Task::ready(()),
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
languages::init(language_registry.clone(), node_runtime, cx);
|
||||
theme::init(LoadThemes::JustBase, cx);
|
||||
Assets.load_fonts(cx).unwrap();
|
||||
|
||||
cx.activate(true);
|
||||
let _ = cx.open_window(WindowOptions::default(), |cx| {
|
||||
cx.new_view(|cx| {
|
||||
let markdown_style = MarkdownStyle {
|
||||
base_text_style: gpui::TextStyle {
|
||||
font_family: "Zed Mono".into(),
|
||||
color: cx.theme().colors().text,
|
||||
..Default::default()
|
||||
},
|
||||
code_block: StyleRefinement {
|
||||
text: Some(gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Mono".into()),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
}),
|
||||
margin: gpui::EdgesRefinement {
|
||||
top: Some(Length::Definite(rems(4.).into())),
|
||||
left: Some(Length::Definite(rems(4.).into())),
|
||||
right: Some(Length::Definite(rems(4.).into())),
|
||||
bottom: Some(Length::Definite(rems(4.).into())),
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Mono".into()),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Color::Muted.color(cx),
|
||||
block_quote_border_color: Color::Muted.color(cx),
|
||||
block_quote: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Muted.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
link: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
underline: Some(gpui::UnderlineStyle {
|
||||
thickness: px(1.),
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
wavy: false,
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: {
|
||||
let mut selection = cx.theme().players().local().selection;
|
||||
selection.fade_out(0.7);
|
||||
selection
|
||||
},
|
||||
break_style: Default::default(),
|
||||
heading: Default::default(),
|
||||
};
|
||||
let markdown = cx.new_view(|cx| {
|
||||
Markdown::new(MARKDOWN_EXAMPLE.into(), markdown_style, None, cx, None)
|
||||
});
|
||||
|
||||
HelloWorld { markdown }
|
||||
})
|
||||
});
|
||||
});
|
||||
}
|
||||
struct HelloWorld {
|
||||
markdown: View<Markdown>,
|
||||
}
|
||||
|
||||
impl Render for HelloWorld {
|
||||
fn render(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.flex()
|
||||
.bg(rgb(0x2e7d32))
|
||||
.size(Length::Definite(Pixels(700.0).into()))
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.shadow_lg()
|
||||
.border_1()
|
||||
.border_color(rgb(0x0000ff))
|
||||
.text_xl()
|
||||
.text_color(rgb(0xffffff))
|
||||
.child(div().child(self.markdown.clone()).p_20())
|
||||
}
|
||||
}
|
||||
@@ -1,16 +1,17 @@
|
||||
mod parser;
|
||||
pub mod parser;
|
||||
|
||||
use crate::parser::CodeBlockKind;
|
||||
use futures::FutureExt;
|
||||
use gpui::{
|
||||
actions, point, quad, AnyElement, AppContext, Bounds, ClipboardItem, CursorStyle,
|
||||
DispatchPhase, Edges, FocusHandle, FocusableView, FontStyle, FontWeight, GlobalElementId,
|
||||
Hitbox, Hsla, KeyContext, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent, Point,
|
||||
Render, StrikethroughStyle, Style, StyledText, Task, TextLayout, TextRun, TextStyle,
|
||||
TextStyleRefinement, View,
|
||||
Hitbox, Hsla, KeyContext, Length, MouseDownEvent, MouseEvent, MouseMoveEvent, MouseUpEvent,
|
||||
Point, Render, StrikethroughStyle, StyleRefinement, StyledText, Task, TextLayout, TextRun,
|
||||
TextStyle, TextStyleRefinement, View,
|
||||
};
|
||||
use language::{Language, LanguageRegistry, Rope};
|
||||
use parser::{parse_markdown, MarkdownEvent, MarkdownTag, MarkdownTagEnd};
|
||||
|
||||
use std::{iter, mem, ops::Range, rc::Rc, sync::Arc};
|
||||
use theme::SyntaxTheme;
|
||||
use ui::prelude::*;
|
||||
@@ -18,7 +19,8 @@ use util::{ResultExt, TryFutureExt};
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct MarkdownStyle {
|
||||
pub code_block: TextStyleRefinement,
|
||||
pub base_text_style: TextStyle,
|
||||
pub code_block: StyleRefinement,
|
||||
pub inline_code: TextStyleRefinement,
|
||||
pub block_quote: TextStyleRefinement,
|
||||
pub link: TextStyleRefinement,
|
||||
@@ -26,8 +28,27 @@ pub struct MarkdownStyle {
|
||||
pub block_quote_border_color: Hsla,
|
||||
pub syntax: Arc<SyntaxTheme>,
|
||||
pub selection_background_color: Hsla,
|
||||
pub break_style: StyleRefinement,
|
||||
pub heading: StyleRefinement,
|
||||
}
|
||||
|
||||
impl Default for MarkdownStyle {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
base_text_style: Default::default(),
|
||||
code_block: Default::default(),
|
||||
inline_code: Default::default(),
|
||||
block_quote: Default::default(),
|
||||
link: Default::default(),
|
||||
rule_color: Default::default(),
|
||||
block_quote_border_color: Default::default(),
|
||||
syntax: Arc::new(SyntaxTheme::default()),
|
||||
selection_background_color: Default::default(),
|
||||
break_style: Default::default(),
|
||||
heading: Default::default(),
|
||||
}
|
||||
}
|
||||
}
|
||||
pub struct Markdown {
|
||||
source: String,
|
||||
selection: Selection,
|
||||
@@ -39,6 +60,7 @@ pub struct Markdown {
|
||||
pending_parse: Option<Task<Option<()>>>,
|
||||
focus_handle: FocusHandle,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
fallback_code_block_language: Option<String>,
|
||||
}
|
||||
|
||||
actions!(markdown, [Copy]);
|
||||
@@ -49,6 +71,7 @@ impl Markdown {
|
||||
style: MarkdownStyle,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
fallback_code_block_language: Option<String>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let mut this = Self {
|
||||
@@ -62,6 +85,7 @@ impl Markdown {
|
||||
pending_parse: None,
|
||||
focus_handle,
|
||||
language_registry,
|
||||
fallback_code_block_language,
|
||||
};
|
||||
this.parse(cx);
|
||||
this
|
||||
@@ -89,7 +113,14 @@ impl Markdown {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub fn parsed_markdown(&self) -> &ParsedMarkdown {
|
||||
&self.parsed_markdown
|
||||
}
|
||||
|
||||
fn copy(&self, text: &RenderedText, cx: &mut ViewContext<Self>) {
|
||||
if self.selection.end <= self.selection.start {
|
||||
return;
|
||||
}
|
||||
let text = text.text_for_range(self.selection.start..self.selection.end);
|
||||
cx.write_to_clipboard(ClipboardItem::new(text));
|
||||
}
|
||||
@@ -140,6 +171,7 @@ impl Render for Markdown {
|
||||
cx.view().clone(),
|
||||
self.style.clone(),
|
||||
self.language_registry.clone(),
|
||||
self.fallback_code_block_language.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -185,11 +217,21 @@ impl Selection {
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ParsedMarkdown {
|
||||
pub struct ParsedMarkdown {
|
||||
source: SharedString,
|
||||
events: Arc<[(Range<usize>, MarkdownEvent)]>,
|
||||
}
|
||||
|
||||
impl ParsedMarkdown {
|
||||
pub fn source(&self) -> &SharedString {
|
||||
&self.source
|
||||
}
|
||||
|
||||
pub fn events(&self) -> &Arc<[(Range<usize>, MarkdownEvent)]> {
|
||||
return &self.events;
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for ParsedMarkdown {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
@@ -203,6 +245,7 @@ pub struct MarkdownElement {
|
||||
markdown: View<Markdown>,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
fallback_code_block_language: Option<String>,
|
||||
}
|
||||
|
||||
impl MarkdownElement {
|
||||
@@ -210,19 +253,31 @@ impl MarkdownElement {
|
||||
markdown: View<Markdown>,
|
||||
style: MarkdownStyle,
|
||||
language_registry: Option<Arc<LanguageRegistry>>,
|
||||
fallback_code_block_language: Option<String>,
|
||||
) -> Self {
|
||||
Self {
|
||||
markdown,
|
||||
style,
|
||||
language_registry,
|
||||
fallback_code_block_language,
|
||||
}
|
||||
}
|
||||
|
||||
fn load_language(&self, name: &str, cx: &mut WindowContext) -> Option<Arc<Language>> {
|
||||
let language_test = self.language_registry.as_ref()?.language_for_name(name);
|
||||
|
||||
let language_name = match language_test.now_or_never() {
|
||||
Some(Ok(_)) => String::from(name),
|
||||
Some(Err(_)) if !name.is_empty() && self.fallback_code_block_language.is_some() => {
|
||||
self.fallback_code_block_language.clone().unwrap()
|
||||
}
|
||||
_ => String::new(),
|
||||
};
|
||||
|
||||
let language = self
|
||||
.language_registry
|
||||
.as_ref()?
|
||||
.language_for_name(name)
|
||||
.language_for_name(language_name.as_str())
|
||||
.map(|language| language.ok())
|
||||
.shared();
|
||||
|
||||
@@ -417,7 +472,7 @@ impl MarkdownElement {
|
||||
.update(cx, |markdown, _| markdown.autoscroll_request.take())?;
|
||||
let (position, line_height) = rendered_text.position_for_source_index(autoscroll_index)?;
|
||||
|
||||
let text_style = cx.text_style();
|
||||
let text_style = self.style.base_text_style.clone();
|
||||
let font_id = cx.text_system().resolve_font(&text_style.font());
|
||||
let font_size = text_style.font_size.to_pixels(cx.rem_size());
|
||||
let em_width = cx
|
||||
@@ -462,14 +517,26 @@ impl Element for MarkdownElement {
|
||||
_id: Option<&GlobalElementId>,
|
||||
cx: &mut WindowContext,
|
||||
) -> (gpui::LayoutId, Self::RequestLayoutState) {
|
||||
let mut builder = MarkdownElementBuilder::new(cx.text_style(), self.style.syntax.clone());
|
||||
let mut builder = MarkdownElementBuilder::new(
|
||||
self.style.base_text_style.clone(),
|
||||
self.style.syntax.clone(),
|
||||
);
|
||||
let parsed_markdown = self.markdown.read(cx).parsed_markdown.clone();
|
||||
let markdown_end = if let Some(last) = parsed_markdown.events.last() {
|
||||
last.0.end
|
||||
} else {
|
||||
0
|
||||
};
|
||||
for (range, event) in parsed_markdown.events.iter() {
|
||||
match event {
|
||||
MarkdownEvent::Start(tag) => {
|
||||
match tag {
|
||||
MarkdownTag::Paragraph => {
|
||||
builder.push_div(div().mb_2().line_height(rems(1.3)));
|
||||
builder.push_div(
|
||||
div().mb_2().line_height(rems(1.3)),
|
||||
range,
|
||||
markdown_end,
|
||||
);
|
||||
}
|
||||
MarkdownTag::Heading { level, .. } => {
|
||||
let mut heading = div().mb_2();
|
||||
@@ -480,7 +547,11 @@ impl Element for MarkdownElement {
|
||||
pulldown_cmark::HeadingLevel::H4 => heading.text_lg(),
|
||||
_ => heading,
|
||||
};
|
||||
builder.push_div(heading);
|
||||
heading.style().refine(&self.style.heading);
|
||||
builder.push_text_style(
|
||||
self.style.heading.text_style().clone().unwrap_or_default(),
|
||||
);
|
||||
builder.push_div(heading, range, markdown_end);
|
||||
}
|
||||
MarkdownTag::BlockQuote => {
|
||||
builder.push_text_style(self.style.block_quote.clone());
|
||||
@@ -490,6 +561,8 @@ impl Element for MarkdownElement {
|
||||
.mb_2()
|
||||
.border_l_4()
|
||||
.border_color(self.style.block_quote_border_color),
|
||||
range,
|
||||
markdown_end,
|
||||
);
|
||||
}
|
||||
MarkdownTag::CodeBlock(kind) => {
|
||||
@@ -499,17 +572,18 @@ impl Element for MarkdownElement {
|
||||
None
|
||||
};
|
||||
|
||||
let mut d = div().w_full().rounded_lg();
|
||||
d.style().refine(&self.style.code_block);
|
||||
if let Some(code_block_text_style) = &self.style.code_block.text {
|
||||
builder.push_text_style(code_block_text_style.to_owned());
|
||||
}
|
||||
builder.push_code_block(language);
|
||||
builder.push_text_style(self.style.code_block.clone());
|
||||
builder.push_div(div().rounded_lg().p_4().mb_2().w_full().when_some(
|
||||
self.style.code_block.background_color,
|
||||
|div, color| div.bg(color),
|
||||
));
|
||||
builder.push_div(d, range, markdown_end);
|
||||
}
|
||||
MarkdownTag::HtmlBlock => builder.push_div(div()),
|
||||
MarkdownTag::HtmlBlock => builder.push_div(div(), range, markdown_end),
|
||||
MarkdownTag::List(bullet_index) => {
|
||||
builder.push_list(*bullet_index);
|
||||
builder.push_div(div().pl_4());
|
||||
builder.push_div(div().pl_4(), range, markdown_end);
|
||||
}
|
||||
MarkdownTag::Item => {
|
||||
let bullet = if let Some(bullet_index) = builder.next_bullet_index() {
|
||||
@@ -525,9 +599,11 @@ impl Element for MarkdownElement {
|
||||
.items_start()
|
||||
.gap_1()
|
||||
.child(bullet),
|
||||
range,
|
||||
markdown_end,
|
||||
);
|
||||
// Without `w_0`, text doesn't wrap to the width of the container.
|
||||
builder.push_div(div().flex_1().w_0());
|
||||
builder.push_div(div().flex_1().w_0(), range, markdown_end);
|
||||
}
|
||||
MarkdownTag::Emphasis => builder.push_text_style(TextStyleRefinement {
|
||||
font_style: Some(FontStyle::Italic),
|
||||
@@ -552,6 +628,7 @@ impl Element for MarkdownElement {
|
||||
builder.push_text_style(self.style.link.clone())
|
||||
}
|
||||
}
|
||||
MarkdownTag::MetadataBlock(_) => {}
|
||||
_ => log::error!("unsupported markdown tag {:?}", tag),
|
||||
}
|
||||
}
|
||||
@@ -559,7 +636,10 @@ impl Element for MarkdownElement {
|
||||
MarkdownTagEnd::Paragraph => {
|
||||
builder.pop_div();
|
||||
}
|
||||
MarkdownTagEnd::Heading(_) => builder.pop_div(),
|
||||
MarkdownTagEnd::Heading(_) => {
|
||||
builder.pop_div();
|
||||
builder.pop_text_style()
|
||||
}
|
||||
MarkdownTagEnd::BlockQuote => {
|
||||
builder.pop_text_style();
|
||||
builder.pop_div()
|
||||
@@ -567,8 +647,10 @@ impl Element for MarkdownElement {
|
||||
MarkdownTagEnd::CodeBlock => {
|
||||
builder.trim_trailing_newline();
|
||||
builder.pop_div();
|
||||
builder.pop_text_style();
|
||||
builder.pop_code_block();
|
||||
if self.style.code_block.text.is_some() {
|
||||
builder.pop_text_style();
|
||||
}
|
||||
}
|
||||
MarkdownTagEnd::HtmlBlock => builder.pop_div(),
|
||||
MarkdownTagEnd::List(_) => {
|
||||
@@ -609,18 +691,24 @@ impl Element for MarkdownElement {
|
||||
.border_b_1()
|
||||
.my_2()
|
||||
.border_color(self.style.rule_color),
|
||||
range,
|
||||
markdown_end,
|
||||
);
|
||||
builder.pop_div()
|
||||
}
|
||||
MarkdownEvent::SoftBreak => builder.push_text("\n", range.start),
|
||||
MarkdownEvent::HardBreak => builder.push_text("\n", range.start),
|
||||
MarkdownEvent::SoftBreak => builder.push_text(" ", range.start),
|
||||
MarkdownEvent::HardBreak => {
|
||||
let mut d = div().py_3();
|
||||
d.style().refine(&self.style.break_style);
|
||||
builder.push_div(d, range, markdown_end);
|
||||
builder.pop_div()
|
||||
}
|
||||
_ => log::error!("unsupported markdown event {:?}", event),
|
||||
}
|
||||
}
|
||||
|
||||
let mut rendered_markdown = builder.build();
|
||||
let child_layout_id = rendered_markdown.element.request_layout(cx);
|
||||
let layout_id = cx.request_layout(Style::default(), [child_layout_id]);
|
||||
let layout_id = cx.request_layout(gpui::Style::default(), [child_layout_id]);
|
||||
(layout_id, rendered_markdown)
|
||||
}
|
||||
|
||||
@@ -732,8 +820,32 @@ impl MarkdownElementBuilder {
|
||||
self.text_style_stack.pop();
|
||||
}
|
||||
|
||||
fn push_div(&mut self, div: Div) {
|
||||
fn push_div(&mut self, mut div: Div, range: &Range<usize>, markdown_end: usize) {
|
||||
self.flush_text();
|
||||
|
||||
if range.start == 0 {
|
||||
//first element, remove top margin
|
||||
div.style().refine(&StyleRefinement {
|
||||
margin: gpui::EdgesRefinement {
|
||||
top: Some(Length::Definite(px(0.).into())),
|
||||
left: None,
|
||||
right: None,
|
||||
bottom: None,
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
if range.end == markdown_end {
|
||||
div.style().refine(&StyleRefinement {
|
||||
margin: gpui::EdgesRefinement {
|
||||
top: None,
|
||||
left: None,
|
||||
right: None,
|
||||
bottom: Some(Length::Definite(rems(0.).into())),
|
||||
},
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
self.div_stack.push(div);
|
||||
}
|
||||
|
||||
|
||||
@@ -7,11 +7,22 @@ use std::ops::Range;
|
||||
pub fn parse_markdown(text: &str) -> Vec<(Range<usize>, MarkdownEvent)> {
|
||||
let mut events = Vec::new();
|
||||
let mut within_link = false;
|
||||
let mut within_metadata = false;
|
||||
for (pulldown_event, mut range) in Parser::new_ext(text, Options::all()).into_offset_iter() {
|
||||
if within_metadata {
|
||||
if let pulldown_cmark::Event::End(pulldown_cmark::TagEnd::MetadataBlock { .. }) =
|
||||
pulldown_event
|
||||
{
|
||||
within_metadata = false;
|
||||
}
|
||||
continue;
|
||||
}
|
||||
match pulldown_event {
|
||||
pulldown_cmark::Event::Start(tag) => {
|
||||
if let pulldown_cmark::Tag::Link { .. } = tag {
|
||||
within_link = true;
|
||||
match tag {
|
||||
pulldown_cmark::Tag::Link { .. } => within_link = true,
|
||||
pulldown_cmark::Tag::MetadataBlock { .. } => within_metadata = true,
|
||||
_ => {}
|
||||
}
|
||||
events.push((range, MarkdownEvent::Start(tag.into())))
|
||||
}
|
||||
|
||||
@@ -131,11 +131,7 @@ impl AnchorRangeExt for Range<Anchor> {
|
||||
}
|
||||
|
||||
fn overlaps(&self, other: &Range<Anchor>, buffer: &MultiBufferSnapshot) -> bool {
|
||||
let start_cmp = self.start.cmp(&other.start, buffer);
|
||||
let end_cmp = self.end.cmp(&other.end, buffer);
|
||||
|
||||
(start_cmp == Ordering::Less || start_cmp == Ordering::Equal)
|
||||
&& (end_cmp == Ordering::Greater || end_cmp == Ordering::Equal)
|
||||
self.end.cmp(&other.start, buffer).is_ge() && self.start.cmp(&other.end, buffer).is_le()
|
||||
}
|
||||
|
||||
fn to_offset(&self, content: &MultiBufferSnapshot) -> Range<usize> {
|
||||
|
||||
@@ -1129,7 +1129,7 @@ impl OutlinePanel {
|
||||
EntryOwned::Entry(FsEntry::File(worktree_id, _, buffer_id, _)) => {
|
||||
let project = self.project.read(cx);
|
||||
let entry_id = project
|
||||
.buffer_for_id(buffer_id)
|
||||
.buffer_for_id(buffer_id, cx)
|
||||
.and_then(|buffer| buffer.read(cx).entry_id(cx));
|
||||
project
|
||||
.worktree_for_id(worktree_id, cx)
|
||||
@@ -1147,7 +1147,7 @@ impl OutlinePanel {
|
||||
.remove(&CollapsedEntry::Excerpt(buffer_id, excerpt_id));
|
||||
let project = self.project.read(cx);
|
||||
let entry_id = project
|
||||
.buffer_for_id(buffer_id)
|
||||
.buffer_for_id(buffer_id, cx)
|
||||
.and_then(|buffer| buffer.read(cx).entry_id(cx));
|
||||
|
||||
entry_id.and_then(|entry_id| {
|
||||
@@ -1622,11 +1622,7 @@ impl OutlinePanel {
|
||||
ExcerptOutlines::Invalidated(_) => ExcerptOutlines::NotFetched,
|
||||
ExcerptOutlines::NotFetched => ExcerptOutlines::NotFetched,
|
||||
},
|
||||
None => {
|
||||
new_collapsed_entries
|
||||
.insert(CollapsedEntry::Excerpt(buffer_id, excerpt_id));
|
||||
ExcerptOutlines::NotFetched
|
||||
}
|
||||
None => ExcerptOutlines::NotFetched,
|
||||
};
|
||||
new_excerpts.entry(buffer_id).or_default().insert(
|
||||
excerpt_id,
|
||||
@@ -1674,11 +1670,6 @@ impl OutlinePanel {
|
||||
.insert(CollapsedEntry::ExternalFile(buffer_id));
|
||||
}
|
||||
}
|
||||
|
||||
for excerpt_id in &excerpts {
|
||||
new_collapsed_entries
|
||||
.insert(CollapsedEntry::Excerpt(buffer_id, *excerpt_id));
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(worktree) = worktree {
|
||||
|
||||
@@ -84,7 +84,7 @@ impl Prettier {
|
||||
path_to_check.pop();
|
||||
}
|
||||
|
||||
let mut project_path_with_prettier_dependency = None;
|
||||
let mut closest_package_json_path = None;
|
||||
loop {
|
||||
if installed_prettiers.contains(&path_to_check) {
|
||||
log::debug!("Found prettier path {path_to_check:?} in installed prettiers");
|
||||
@@ -92,61 +92,44 @@ impl Prettier {
|
||||
} else if let Some(package_json_contents) =
|
||||
read_package_json(fs, &path_to_check).await?
|
||||
{
|
||||
if has_prettier_in_package_json(&package_json_contents) {
|
||||
if has_prettier_in_node_modules(fs, &path_to_check).await? {
|
||||
log::debug!("Found prettier path {path_to_check:?} in both package.json and node_modules");
|
||||
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
||||
} else if project_path_with_prettier_dependency.is_none() {
|
||||
project_path_with_prettier_dependency = Some(path_to_check.clone());
|
||||
}
|
||||
if has_prettier_in_node_modules(fs, &path_to_check).await? {
|
||||
log::debug!("Found prettier path {path_to_check:?} in the node_modules");
|
||||
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
||||
} else {
|
||||
match package_json_contents.get("workspaces") {
|
||||
Some(serde_json::Value::Array(workspaces)) => {
|
||||
match &project_path_with_prettier_dependency {
|
||||
Some(project_path_with_prettier_dependency) => {
|
||||
let subproject_path = project_path_with_prettier_dependency.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
|
||||
if workspaces.iter().filter_map(|value| {
|
||||
if let serde_json::Value::String(s) = value {
|
||||
Some(s.clone())
|
||||
} else {
|
||||
log::warn!("Skipping non-string 'workspaces' value: {value:?}");
|
||||
None
|
||||
}
|
||||
}).any(|workspace_definition| {
|
||||
if let Some(path_matcher) = PathMatcher::new(&[workspace_definition.clone()]).ok() {
|
||||
path_matcher.is_match(subproject_path)
|
||||
} else {
|
||||
workspace_definition == subproject_path.to_string_lossy()
|
||||
}
|
||||
}) {
|
||||
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}, but it's not installed into workspace root's node_modules");
|
||||
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {project_path_with_prettier_dependency:?}");
|
||||
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
||||
match &closest_package_json_path {
|
||||
None => closest_package_json_path = Some(path_to_check.clone()),
|
||||
Some(closest_package_json_path) => {
|
||||
match package_json_contents.get("workspaces") {
|
||||
Some(serde_json::Value::Array(workspaces)) => {
|
||||
let subproject_path = closest_package_json_path.strip_prefix(&path_to_check).expect("traversing path parents, should be able to strip prefix");
|
||||
if workspaces.iter().filter_map(|value| {
|
||||
if let serde_json::Value::String(s) = value {
|
||||
Some(s.clone())
|
||||
} else {
|
||||
log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but is not included in its package.json workspaces {workspaces:?}");
|
||||
log::warn!("Skipping non-string 'workspaces' value: {value:?}");
|
||||
None
|
||||
}
|
||||
}).any(|workspace_definition| {
|
||||
workspace_definition == subproject_path.to_string_lossy() || PathMatcher::new(&[workspace_definition]).ok().map_or(false, |path_matcher| path_matcher.is_match(subproject_path))
|
||||
}) {
|
||||
anyhow::ensure!(has_prettier_in_node_modules(fs, &path_to_check).await?, "Path {path_to_check:?} is the workspace root for project in {closest_package_json_path:?}, but it has no prettier installed");
|
||||
log::info!("Found prettier path {path_to_check:?} in the workspace root for project in {closest_package_json_path:?}");
|
||||
return Ok(ControlFlow::Continue(Some(path_to_check)));
|
||||
} else {
|
||||
log::warn!("Skipping path {path_to_check:?} workspace root with workspaces {workspaces:?} that have no prettier installed");
|
||||
}
|
||||
None => {
|
||||
log::warn!("Skipping path {path_to_check:?} that has prettier in its 'node_modules' subdirectory, but has no prettier in its package.json");
|
||||
}
|
||||
}
|
||||
},
|
||||
Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
|
||||
None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
|
||||
},
|
||||
Some(unknown) => log::error!("Failed to parse workspaces for {path_to_check:?} from package.json, got {unknown:?}. Skipping."),
|
||||
None => log::warn!("Skipping path {path_to_check:?} that has no prettier dependency and no workspaces section in its package.json"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if !path_to_check.pop() {
|
||||
match project_path_with_prettier_dependency {
|
||||
Some(closest_prettier_discovered) => {
|
||||
anyhow::bail!("No prettier found in node_modules for ancestors of {locate_from:?}, but discovered prettier package.json dependency in {closest_prettier_discovered:?}")
|
||||
}
|
||||
None => {
|
||||
log::debug!("Found no prettier in ancestors of {locate_from:?}");
|
||||
return Ok(ControlFlow::Continue(None));
|
||||
}
|
||||
}
|
||||
log::debug!("Found no prettier in ancestors of {locate_from:?}");
|
||||
return Ok(ControlFlow::Continue(None));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -448,22 +431,6 @@ async fn read_package_json(
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn has_prettier_in_package_json(
|
||||
package_json_contents: &HashMap<String, serde_json::Value>,
|
||||
) -> bool {
|
||||
if let Some(serde_json::Value::Object(o)) = package_json_contents.get("dependencies") {
|
||||
if o.contains_key(PRETTIER_PACKAGE_NAME) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
if let Some(serde_json::Value::Object(o)) = package_json_contents.get("devDependencies") {
|
||||
if o.contains_key(PRETTIER_PACKAGE_NAME) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
enum Format {}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize)]
|
||||
@@ -548,40 +515,36 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
matches!(
|
||||
Prettier::locate_prettier_installation(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/.config/zed/settings.json"),
|
||||
)
|
||||
.await,
|
||||
Ok(ControlFlow::Continue(None))
|
||||
),
|
||||
"Should successfully find no prettier for path hierarchy without it"
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_installation(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/.config/zed/settings.json"),
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
ControlFlow::Continue(None),
|
||||
"Should find no prettier for path hierarchy without it"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
Prettier::locate_prettier_installation(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/work/project/src/index.js")
|
||||
)
|
||||
.await,
|
||||
Ok(ControlFlow::Continue(None))
|
||||
),
|
||||
"Should successfully find no prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_installation(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/work/project/src/index.js")
|
||||
)
|
||||
.await.unwrap(),
|
||||
ControlFlow::Continue(Some(PathBuf::from("/root/work/project"))),
|
||||
"Should successfully find a prettier for path hierarchy that has node_modules with prettier, but no package.json mentions of it"
|
||||
);
|
||||
assert!(
|
||||
matches!(
|
||||
Prettier::locate_prettier_installation(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/work/project/node_modules/expect/build/print.js")
|
||||
)
|
||||
.await,
|
||||
Ok(ControlFlow::Break(()))
|
||||
),
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_installation(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/work/project/node_modules/expect/build/print.js")
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
ControlFlow::Break(()),
|
||||
"Should not format files inside node_modules/"
|
||||
);
|
||||
}
|
||||
@@ -691,18 +654,17 @@ mod tests {
|
||||
)
|
||||
.await;
|
||||
|
||||
match Prettier::locate_prettier_installation(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/work/web_blog/pages/[slug].tsx")
|
||||
)
|
||||
.await {
|
||||
Ok(path) => panic!("Expected to fail for prettier in package.json but not in node_modules found, but got path {path:?}"),
|
||||
Err(e) => {
|
||||
let message = e.to_string();
|
||||
assert!(message.contains("/root/work/web_blog"), "Error message should mention which project had prettier defined");
|
||||
},
|
||||
};
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_installation(
|
||||
fs.as_ref(),
|
||||
&HashSet::default(),
|
||||
Path::new("/root/work/web_blog/pages/[slug].tsx")
|
||||
)
|
||||
.await
|
||||
.unwrap(),
|
||||
ControlFlow::Continue(None),
|
||||
"Should find no prettier when node_modules don't have it"
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
Prettier::locate_prettier_installation(
|
||||
|
||||
928
crates/project/src/buffer_store.rs
Normal file
928
crates/project/src/buffer_store.rs
Normal file
@@ -0,0 +1,928 @@
|
||||
use crate::ProjectPath;
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use collections::{hash_map, HashMap};
|
||||
use futures::{channel::oneshot, StreamExt as _};
|
||||
use gpui::{
|
||||
AppContext, AsyncAppContext, Context as _, EventEmitter, Model, ModelContext, Task, WeakModel,
|
||||
};
|
||||
use language::{
|
||||
proto::{deserialize_version, serialize_version, split_operations},
|
||||
Buffer, Capability, Language, Operation,
|
||||
};
|
||||
use rpc::{
|
||||
proto::{self, AnyProtoClient, PeerId},
|
||||
ErrorExt as _, TypedEnvelope,
|
||||
};
|
||||
use std::{io, path::Path, sync::Arc};
|
||||
use text::BufferId;
|
||||
use util::{debug_panic, maybe, ResultExt as _};
|
||||
use worktree::{File, ProjectEntryId, RemoteWorktree, Worktree};
|
||||
|
||||
/// A set of open buffers.
|
||||
pub struct BufferStore {
|
||||
retain_buffers: bool,
|
||||
opened_buffers: HashMap<BufferId, OpenBuffer>,
|
||||
local_buffer_ids_by_path: HashMap<ProjectPath, BufferId>,
|
||||
local_buffer_ids_by_entry_id: HashMap<ProjectEntryId, BufferId>,
|
||||
#[allow(clippy::type_complexity)]
|
||||
loading_buffers_by_path: HashMap<
|
||||
ProjectPath,
|
||||
postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
|
||||
>,
|
||||
loading_remote_buffers_by_id: HashMap<BufferId, Model<Buffer>>,
|
||||
remote_buffer_listeners:
|
||||
HashMap<BufferId, Vec<oneshot::Sender<Result<Model<Buffer>, anyhow::Error>>>>,
|
||||
}
|
||||
|
||||
enum OpenBuffer {
|
||||
Strong(Model<Buffer>),
|
||||
Weak(WeakModel<Buffer>),
|
||||
Operations(Vec<Operation>),
|
||||
}
|
||||
|
||||
pub enum BufferStoreEvent {
|
||||
BufferAdded(Model<Buffer>),
|
||||
BufferChangedFilePath {
|
||||
buffer: Model<Buffer>,
|
||||
old_file: Option<Arc<File>>,
|
||||
},
|
||||
BufferSaved {
|
||||
buffer: Model<Buffer>,
|
||||
has_changed_file: bool,
|
||||
saved_version: clock::Global,
|
||||
},
|
||||
}
|
||||
|
||||
impl EventEmitter<BufferStoreEvent> for BufferStore {}
|
||||
|
||||
impl BufferStore {
|
||||
/// Creates a buffer store, optionally retaining its buffers.
|
||||
///
|
||||
/// If `retain_buffers` is `true`, then buffers are owned by the buffer store
|
||||
/// and won't be released unless they are explicitly removed, or `retain_buffers`
|
||||
/// is set to `false` via `set_retain_buffers`. Otherwise, buffers are stored as
|
||||
/// weak handles.
|
||||
pub fn new(retain_buffers: bool) -> Self {
|
||||
Self {
|
||||
retain_buffers,
|
||||
opened_buffers: Default::default(),
|
||||
remote_buffer_listeners: Default::default(),
|
||||
loading_remote_buffers_by_id: Default::default(),
|
||||
local_buffer_ids_by_path: Default::default(),
|
||||
local_buffer_ids_by_entry_id: Default::default(),
|
||||
loading_buffers_by_path: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn open_buffer(
|
||||
&mut self,
|
||||
project_path: ProjectPath,
|
||||
worktree: Model<Worktree>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
let existing_buffer = self.get_by_path(&project_path, cx);
|
||||
if let Some(existing_buffer) = existing_buffer {
|
||||
return Task::ready(Ok(existing_buffer));
|
||||
}
|
||||
|
||||
let loading_watch = match self.loading_buffers_by_path.entry(project_path.clone()) {
|
||||
// If the given path is already being loaded, then wait for that existing
|
||||
// task to complete and return the same buffer.
|
||||
hash_map::Entry::Occupied(e) => e.get().clone(),
|
||||
|
||||
// Otherwise, record the fact that this path is now being loaded.
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
let (mut tx, rx) = postage::watch::channel();
|
||||
entry.insert(rx.clone());
|
||||
|
||||
let project_path = project_path.clone();
|
||||
let load_buffer = match worktree.read(cx) {
|
||||
Worktree::Local(_) => {
|
||||
self.open_local_buffer_internal(project_path.path.clone(), worktree, cx)
|
||||
}
|
||||
Worktree::Remote(tree) => {
|
||||
self.open_remote_buffer_internal(&project_path.path, tree, cx)
|
||||
}
|
||||
};
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let load_result = load_buffer.await;
|
||||
*tx.borrow_mut() = Some(this.update(&mut cx, |this, _| {
|
||||
// Record the fact that the buffer is no longer loading.
|
||||
this.loading_buffers_by_path.remove(&project_path);
|
||||
let buffer = load_result.map_err(Arc::new)?;
|
||||
Ok(buffer)
|
||||
})?);
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach();
|
||||
rx
|
||||
}
|
||||
};
|
||||
|
||||
cx.background_executor().spawn(async move {
|
||||
Self::wait_for_loading_buffer(loading_watch)
|
||||
.await
|
||||
.map_err(|e| e.cloned())
|
||||
})
|
||||
}
|
||||
|
||||
fn open_local_buffer_internal(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
worktree: Model<Worktree>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
let load_buffer = worktree.update(cx, |worktree, cx| {
|
||||
let load_file = worktree.load_file(path.as_ref(), cx);
|
||||
let reservation = cx.reserve_model();
|
||||
let buffer_id = BufferId::from(reservation.entity_id().as_non_zero_u64());
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
let loaded = load_file.await?;
|
||||
let text_buffer = cx
|
||||
.background_executor()
|
||||
.spawn(async move { text::Buffer::new(0, buffer_id, loaded.text) })
|
||||
.await;
|
||||
cx.insert_model(reservation, |_| {
|
||||
Buffer::build(
|
||||
text_buffer,
|
||||
loaded.diff_base,
|
||||
Some(loaded.file),
|
||||
Capability::ReadWrite,
|
||||
)
|
||||
})
|
||||
})
|
||||
});
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let buffer = match load_buffer.await {
|
||||
Ok(buffer) => Ok(buffer),
|
||||
Err(error) if is_not_found_error(&error) => cx.new_model(|cx| {
|
||||
let buffer_id = BufferId::from(cx.entity_id().as_non_zero_u64());
|
||||
let text_buffer = text::Buffer::new(0, buffer_id, "".into());
|
||||
Buffer::build(
|
||||
text_buffer,
|
||||
None,
|
||||
Some(Arc::new(File {
|
||||
worktree,
|
||||
path,
|
||||
mtime: None,
|
||||
entry_id: None,
|
||||
is_local: true,
|
||||
is_deleted: false,
|
||||
is_private: false,
|
||||
})),
|
||||
Capability::ReadWrite,
|
||||
)
|
||||
}),
|
||||
Err(e) => Err(e),
|
||||
}?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.add_buffer(buffer.clone(), cx).log_err();
|
||||
})?;
|
||||
Ok(buffer)
|
||||
})
|
||||
}
|
||||
|
||||
fn open_remote_buffer_internal(
|
||||
&self,
|
||||
path: &Arc<Path>,
|
||||
worktree: &RemoteWorktree,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
let worktree_id = worktree.id().to_proto();
|
||||
let project_id = worktree.project_id();
|
||||
let client = worktree.client();
|
||||
let path_string = path.clone().to_string_lossy().to_string();
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let response = client
|
||||
.request(proto::OpenBufferByPath {
|
||||
project_id,
|
||||
worktree_id,
|
||||
path: path_string,
|
||||
})
|
||||
.await?;
|
||||
let buffer_id = BufferId::new(response.buffer_id)?;
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.wait_for_remote_buffer(buffer_id, cx)
|
||||
})?
|
||||
.await
|
||||
})
|
||||
}
|
||||
|
||||
pub fn create_buffer(
|
||||
&mut self,
|
||||
remote_client: Option<(AnyProtoClient, u64)>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
if let Some((remote_client, project_id)) = remote_client {
|
||||
let create = remote_client.request(proto::OpenNewBuffer { project_id });
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let response = create.await?;
|
||||
let buffer_id = BufferId::new(response.buffer_id)?;
|
||||
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.wait_for_remote_buffer(buffer_id, cx)
|
||||
})?
|
||||
.await
|
||||
})
|
||||
} else {
|
||||
Task::ready(Ok(self.create_local_buffer("", None, cx)))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn create_local_buffer(
|
||||
&mut self,
|
||||
text: &str,
|
||||
language: Option<Arc<Language>>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Model<Buffer> {
|
||||
let buffer = cx.new_model(|cx| {
|
||||
Buffer::local(text, cx)
|
||||
.with_language(language.unwrap_or_else(|| language::PLAIN_TEXT.clone()), cx)
|
||||
});
|
||||
self.add_buffer(buffer.clone(), cx).log_err();
|
||||
buffer
|
||||
}
|
||||
|
||||
pub fn save_buffer(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(file) = File::from_dyn(buffer.read(cx).file()) else {
|
||||
return Task::ready(Err(anyhow!("buffer doesn't have a file")));
|
||||
};
|
||||
match file.worktree.read(cx) {
|
||||
Worktree::Local(_) => {
|
||||
self.save_local_buffer(file.worktree.clone(), buffer, file.path.clone(), false, cx)
|
||||
}
|
||||
Worktree::Remote(tree) => self.save_remote_buffer(buffer, None, tree, cx),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn save_buffer_as(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
path: ProjectPath,
|
||||
worktree: Model<Worktree>,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let old_file = File::from_dyn(buffer.read(cx).file())
|
||||
.cloned()
|
||||
.map(Arc::new);
|
||||
|
||||
let task = match worktree.read(cx) {
|
||||
Worktree::Local(_) => {
|
||||
self.save_local_buffer(worktree, buffer.clone(), path.path, true, cx)
|
||||
}
|
||||
Worktree::Remote(tree) => {
|
||||
self.save_remote_buffer(buffer.clone(), Some(path.to_proto()), tree, cx)
|
||||
}
|
||||
};
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
task.await?;
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.emit(BufferStoreEvent::BufferChangedFilePath { buffer, old_file });
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn save_local_buffer(
|
||||
&self,
|
||||
worktree: Model<Worktree>,
|
||||
buffer_handle: Model<Buffer>,
|
||||
path: Arc<Path>,
|
||||
mut has_changed_file: bool,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let text = buffer.as_rope().clone();
|
||||
let line_ending = buffer.line_ending();
|
||||
let version = buffer.version();
|
||||
if buffer.file().is_some_and(|file| !file.is_created()) {
|
||||
has_changed_file = true;
|
||||
}
|
||||
|
||||
let save = worktree.update(cx, |worktree, cx| {
|
||||
worktree.write_file(path.as_ref(), text, line_ending, cx)
|
||||
});
|
||||
|
||||
cx.spawn(move |this, mut cx| async move {
|
||||
let new_file = save.await?;
|
||||
let mtime = new_file.mtime;
|
||||
buffer_handle.update(&mut cx, |buffer, cx| {
|
||||
if has_changed_file {
|
||||
buffer.file_updated(new_file, cx);
|
||||
}
|
||||
buffer.did_save(version.clone(), mtime, cx);
|
||||
})?;
|
||||
this.update(&mut cx, |_, cx| {
|
||||
cx.emit(BufferStoreEvent::BufferSaved {
|
||||
buffer: buffer_handle,
|
||||
has_changed_file,
|
||||
saved_version: version,
|
||||
})
|
||||
})?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn save_remote_buffer(
|
||||
&self,
|
||||
buffer_handle: Model<Buffer>,
|
||||
new_path: Option<proto::ProjectPath>,
|
||||
tree: &RemoteWorktree,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let buffer = buffer_handle.read(cx);
|
||||
let buffer_id = buffer.remote_id().into();
|
||||
let version = buffer.version();
|
||||
let rpc = tree.client();
|
||||
let project_id = tree.project_id();
|
||||
cx.spawn(move |_, mut cx| async move {
|
||||
let response = rpc
|
||||
.request(proto::SaveBuffer {
|
||||
project_id,
|
||||
buffer_id,
|
||||
new_path,
|
||||
version: serialize_version(&version),
|
||||
})
|
||||
.await?;
|
||||
let version = deserialize_version(&response.version);
|
||||
let mtime = response.mtime.map(|mtime| mtime.into());
|
||||
|
||||
buffer_handle.update(&mut cx, |buffer, cx| {
|
||||
buffer.did_save(version.clone(), mtime, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn add_buffer(&mut self, buffer: Model<Buffer>, cx: &mut ModelContext<Self>) -> Result<()> {
|
||||
let remote_id = buffer.read(cx).remote_id();
|
||||
let is_remote = buffer.read(cx).replica_id() != 0;
|
||||
let open_buffer = if self.retain_buffers {
|
||||
OpenBuffer::Strong(buffer.clone())
|
||||
} else {
|
||||
OpenBuffer::Weak(buffer.downgrade())
|
||||
};
|
||||
|
||||
match self.opened_buffers.entry(remote_id) {
|
||||
hash_map::Entry::Vacant(entry) => {
|
||||
entry.insert(open_buffer);
|
||||
}
|
||||
hash_map::Entry::Occupied(mut entry) => {
|
||||
if let OpenBuffer::Operations(operations) = entry.get_mut() {
|
||||
buffer.update(cx, |b, cx| b.apply_ops(operations.drain(..), cx))?;
|
||||
} else if entry.get().upgrade().is_some() {
|
||||
if is_remote {
|
||||
return Ok(());
|
||||
} else {
|
||||
debug_panic!("buffer {} was already registered", remote_id);
|
||||
Err(anyhow!("buffer {} was already registered", remote_id))?;
|
||||
}
|
||||
}
|
||||
entry.insert(open_buffer);
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(senders) = self.remote_buffer_listeners.remove(&remote_id) {
|
||||
for sender in senders {
|
||||
sender.send(Ok(buffer.clone())).ok();
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(file) = File::from_dyn(buffer.read(cx).file()) {
|
||||
if file.is_local {
|
||||
self.local_buffer_ids_by_path.insert(
|
||||
ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
},
|
||||
remote_id,
|
||||
);
|
||||
|
||||
if let Some(entry_id) = file.entry_id {
|
||||
self.local_buffer_ids_by_entry_id
|
||||
.insert(entry_id, remote_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
cx.emit(BufferStoreEvent::BufferAdded(buffer));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn buffers(&self) -> impl '_ + Iterator<Item = Model<Buffer>> {
|
||||
self.opened_buffers
|
||||
.values()
|
||||
.filter_map(|buffer| buffer.upgrade())
|
||||
}
|
||||
|
||||
pub fn loading_buffers(
|
||||
&self,
|
||||
) -> impl Iterator<
|
||||
Item = (
|
||||
&ProjectPath,
|
||||
postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
|
||||
),
|
||||
> {
|
||||
self.loading_buffers_by_path
|
||||
.iter()
|
||||
.map(|(path, rx)| (path, rx.clone()))
|
||||
}
|
||||
|
||||
pub fn get_by_path(&self, path: &ProjectPath, cx: &AppContext) -> Option<Model<Buffer>> {
|
||||
self.buffers().find_map(|buffer| {
|
||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
||||
if file.worktree_id(cx) == path.worktree_id && &file.path == &path.path {
|
||||
Some(buffer)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
pub fn get(&self, buffer_id: BufferId) -> Option<Model<Buffer>> {
|
||||
self.opened_buffers
|
||||
.get(&buffer_id)
|
||||
.and_then(|buffer| buffer.upgrade())
|
||||
}
|
||||
|
||||
pub fn get_existing(&self, buffer_id: BufferId) -> Result<Model<Buffer>> {
|
||||
self.get(buffer_id)
|
||||
.ok_or_else(|| anyhow!("unknown buffer id {}", buffer_id))
|
||||
}
|
||||
|
||||
pub fn get_possibly_incomplete(&self, buffer_id: BufferId) -> Option<Model<Buffer>> {
|
||||
self.get(buffer_id)
|
||||
.or_else(|| self.loading_remote_buffers_by_id.get(&buffer_id).cloned())
|
||||
}
|
||||
|
||||
fn get_or_remove_by_path(
|
||||
&mut self,
|
||||
entry_id: ProjectEntryId,
|
||||
project_path: &ProjectPath,
|
||||
) -> Option<(BufferId, Model<Buffer>)> {
|
||||
let buffer_id = match self.local_buffer_ids_by_entry_id.get(&entry_id) {
|
||||
Some(&buffer_id) => buffer_id,
|
||||
None => match self.local_buffer_ids_by_path.get(project_path) {
|
||||
Some(&buffer_id) => buffer_id,
|
||||
None => {
|
||||
return None;
|
||||
}
|
||||
},
|
||||
};
|
||||
let buffer = if let Some(buffer) = self.get(buffer_id) {
|
||||
buffer
|
||||
} else {
|
||||
self.opened_buffers.remove(&buffer_id);
|
||||
self.local_buffer_ids_by_path.remove(project_path);
|
||||
self.local_buffer_ids_by_entry_id.remove(&entry_id);
|
||||
return None;
|
||||
};
|
||||
Some((buffer_id, buffer))
|
||||
}
|
||||
|
||||
pub fn wait_for_remote_buffer(
|
||||
&mut self,
|
||||
id: BufferId,
|
||||
cx: &mut AppContext,
|
||||
) -> Task<Result<Model<Buffer>>> {
|
||||
let buffer = self.get(id);
|
||||
if let Some(buffer) = buffer {
|
||||
return Task::ready(Ok(buffer));
|
||||
}
|
||||
let (tx, rx) = oneshot::channel();
|
||||
self.remote_buffer_listeners.entry(id).or_default().push(tx);
|
||||
cx.background_executor().spawn(async move { rx.await? })
|
||||
}
|
||||
|
||||
pub fn buffer_version_info(
|
||||
&self,
|
||||
cx: &AppContext,
|
||||
) -> (Vec<proto::BufferVersion>, Vec<BufferId>) {
|
||||
let buffers = self
|
||||
.buffers()
|
||||
.map(|buffer| {
|
||||
let buffer = buffer.read(cx);
|
||||
proto::BufferVersion {
|
||||
id: buffer.remote_id().into(),
|
||||
version: language::proto::serialize_version(&buffer.version),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let incomplete_buffer_ids = self
|
||||
.loading_remote_buffers_by_id
|
||||
.keys()
|
||||
.copied()
|
||||
.collect::<Vec<_>>();
|
||||
(buffers, incomplete_buffer_ids)
|
||||
}
|
||||
|
||||
pub fn disconnected_from_host(&mut self, cx: &mut AppContext) {
|
||||
self.set_retain_buffers(false, cx);
|
||||
|
||||
for buffer in self.buffers() {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_capability(Capability::ReadOnly, cx)
|
||||
});
|
||||
}
|
||||
|
||||
// Wake up all futures currently waiting on a buffer to get opened,
|
||||
// to give them a chance to fail now that we've disconnected.
|
||||
self.remote_buffer_listeners.clear();
|
||||
}
|
||||
|
||||
pub fn set_retain_buffers(&mut self, retain_buffers: bool, cx: &mut AppContext) {
|
||||
self.retain_buffers = retain_buffers;
|
||||
for open_buffer in self.opened_buffers.values_mut() {
|
||||
if retain_buffers {
|
||||
if let OpenBuffer::Weak(buffer) = open_buffer {
|
||||
if let Some(buffer) = buffer.upgrade() {
|
||||
*open_buffer = OpenBuffer::Strong(buffer);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if let Some(buffer) = open_buffer.upgrade() {
|
||||
buffer.update(cx, |buffer, _| buffer.give_up_waiting());
|
||||
}
|
||||
if let OpenBuffer::Strong(buffer) = open_buffer {
|
||||
*open_buffer = OpenBuffer::Weak(buffer.downgrade());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn discard_incomplete(&mut self) {
|
||||
self.opened_buffers
|
||||
.retain(|_, buffer| !matches!(buffer, OpenBuffer::Operations(_)));
|
||||
}
|
||||
|
||||
pub fn file_changed(
|
||||
&mut self,
|
||||
path: Arc<Path>,
|
||||
entry_id: ProjectEntryId,
|
||||
worktree_handle: &Model<worktree::Worktree>,
|
||||
snapshot: &worktree::Snapshot,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Option<(Model<Buffer>, Arc<File>, Arc<File>)> {
|
||||
let (buffer_id, buffer) = self.get_or_remove_by_path(
|
||||
entry_id,
|
||||
&ProjectPath {
|
||||
worktree_id: snapshot.id(),
|
||||
path,
|
||||
},
|
||||
)?;
|
||||
|
||||
let result = buffer.update(cx, |buffer, cx| {
|
||||
let old_file = File::from_dyn(buffer.file())?;
|
||||
if old_file.worktree != *worktree_handle {
|
||||
return None;
|
||||
}
|
||||
|
||||
let new_file = if let Some(entry) = old_file
|
||||
.entry_id
|
||||
.and_then(|entry_id| snapshot.entry_for_id(entry_id))
|
||||
{
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: Some(entry.id),
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: false,
|
||||
is_private: entry.is_private,
|
||||
}
|
||||
} else if let Some(entry) = snapshot.entry_for_path(old_file.path.as_ref()) {
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: Some(entry.id),
|
||||
mtime: entry.mtime,
|
||||
path: entry.path.clone(),
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: false,
|
||||
is_private: entry.is_private,
|
||||
}
|
||||
} else {
|
||||
File {
|
||||
is_local: true,
|
||||
entry_id: old_file.entry_id,
|
||||
path: old_file.path.clone(),
|
||||
mtime: old_file.mtime,
|
||||
worktree: worktree_handle.clone(),
|
||||
is_deleted: true,
|
||||
is_private: old_file.is_private,
|
||||
}
|
||||
};
|
||||
|
||||
if new_file == *old_file {
|
||||
return None;
|
||||
}
|
||||
|
||||
let old_file = Arc::new(old_file.clone());
|
||||
let new_file = Arc::new(new_file);
|
||||
buffer.file_updated(new_file.clone(), cx);
|
||||
Some((cx.handle(), old_file, new_file))
|
||||
});
|
||||
|
||||
if let Some((buffer, old_file, new_file)) = &result {
|
||||
if new_file.path != old_file.path {
|
||||
self.local_buffer_ids_by_path.remove(&ProjectPath {
|
||||
path: old_file.path.clone(),
|
||||
worktree_id: old_file.worktree_id(cx),
|
||||
});
|
||||
self.local_buffer_ids_by_path.insert(
|
||||
ProjectPath {
|
||||
worktree_id: new_file.worktree_id(cx),
|
||||
path: new_file.path.clone(),
|
||||
},
|
||||
buffer_id,
|
||||
);
|
||||
cx.emit(BufferStoreEvent::BufferChangedFilePath {
|
||||
buffer: buffer.clone(),
|
||||
old_file: Some(old_file.clone()),
|
||||
});
|
||||
}
|
||||
|
||||
if new_file.entry_id != old_file.entry_id {
|
||||
if let Some(entry_id) = old_file.entry_id {
|
||||
self.local_buffer_ids_by_entry_id.remove(&entry_id);
|
||||
}
|
||||
if let Some(entry_id) = new_file.entry_id {
|
||||
self.local_buffer_ids_by_entry_id
|
||||
.insert(entry_id, buffer_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result
|
||||
}
|
||||
|
||||
pub fn buffer_changed_file(
|
||||
&mut self,
|
||||
buffer: Model<Buffer>,
|
||||
cx: &mut AppContext,
|
||||
) -> Option<()> {
|
||||
let file = File::from_dyn(buffer.read(cx).file())?;
|
||||
|
||||
let remote_id = buffer.read(cx).remote_id();
|
||||
if let Some(entry_id) = file.entry_id {
|
||||
match self.local_buffer_ids_by_entry_id.get(&entry_id) {
|
||||
Some(_) => {
|
||||
return None;
|
||||
}
|
||||
None => {
|
||||
self.local_buffer_ids_by_entry_id
|
||||
.insert(entry_id, remote_id);
|
||||
}
|
||||
}
|
||||
};
|
||||
self.local_buffer_ids_by_path.insert(
|
||||
ProjectPath {
|
||||
worktree_id: file.worktree_id(cx),
|
||||
path: file.path.clone(),
|
||||
},
|
||||
remote_id,
|
||||
);
|
||||
|
||||
Some(())
|
||||
}
|
||||
|
||||
pub async fn create_buffer_for_peer(
|
||||
this: Model<Self>,
|
||||
peer_id: PeerId,
|
||||
buffer_id: BufferId,
|
||||
project_id: u64,
|
||||
client: AnyProtoClient,
|
||||
cx: &mut AsyncAppContext,
|
||||
) -> Result<()> {
|
||||
let Some(buffer) = this.update(cx, |this, _| this.get(buffer_id))? else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
let operations = buffer.update(cx, |b, cx| b.serialize_ops(None, cx))?;
|
||||
let operations = operations.await;
|
||||
let state = buffer.update(cx, |buffer, _| buffer.to_proto())?;
|
||||
|
||||
let initial_state = proto::CreateBufferForPeer {
|
||||
project_id,
|
||||
peer_id: Some(peer_id),
|
||||
variant: Some(proto::create_buffer_for_peer::Variant::State(state)),
|
||||
};
|
||||
|
||||
if client.send(initial_state).log_err().is_some() {
|
||||
let client = client.clone();
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let mut chunks = split_operations(operations).peekable();
|
||||
while let Some(chunk) = chunks.next() {
|
||||
let is_last = chunks.peek().is_none();
|
||||
client.send(proto::CreateBufferForPeer {
|
||||
project_id,
|
||||
peer_id: Some(peer_id),
|
||||
variant: Some(proto::create_buffer_for_peer::Variant::Chunk(
|
||||
proto::BufferChunk {
|
||||
buffer_id: buffer_id.into(),
|
||||
operations: chunk,
|
||||
is_last,
|
||||
},
|
||||
)),
|
||||
})?;
|
||||
}
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.await
|
||||
.log_err();
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn handle_update_buffer(
|
||||
&mut self,
|
||||
envelope: TypedEnvelope<proto::UpdateBuffer>,
|
||||
is_remote: bool,
|
||||
cx: &mut AppContext,
|
||||
) -> Result<proto::Ack> {
|
||||
let payload = envelope.payload.clone();
|
||||
let buffer_id = BufferId::new(payload.buffer_id)?;
|
||||
let ops = payload
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>, _>>()?;
|
||||
match self.opened_buffers.entry(buffer_id) {
|
||||
hash_map::Entry::Occupied(mut e) => match e.get_mut() {
|
||||
OpenBuffer::Strong(buffer) => {
|
||||
buffer.update(cx, |buffer, cx| buffer.apply_ops(ops, cx))?;
|
||||
}
|
||||
OpenBuffer::Operations(operations) => operations.extend_from_slice(&ops),
|
||||
OpenBuffer::Weak(_) => {}
|
||||
},
|
||||
hash_map::Entry::Vacant(e) => {
|
||||
if !is_remote {
|
||||
debug_panic!(
|
||||
"received buffer update from {:?}",
|
||||
envelope.original_sender_id
|
||||
);
|
||||
return Err(anyhow!("received buffer update for non-remote project"));
|
||||
}
|
||||
e.insert(OpenBuffer::Operations(ops));
|
||||
}
|
||||
}
|
||||
Ok(proto::Ack {})
|
||||
}
|
||||
|
||||
pub fn handle_create_buffer_for_peer(
|
||||
&mut self,
|
||||
envelope: TypedEnvelope<proto::CreateBufferForPeer>,
|
||||
mut worktrees: impl Iterator<Item = Model<Worktree>>,
|
||||
replica_id: u16,
|
||||
capability: Capability,
|
||||
cx: &mut ModelContext<Self>,
|
||||
) -> Result<()> {
|
||||
match envelope
|
||||
.payload
|
||||
.variant
|
||||
.ok_or_else(|| anyhow!("missing variant"))?
|
||||
{
|
||||
proto::create_buffer_for_peer::Variant::State(mut state) => {
|
||||
let buffer_id = BufferId::new(state.id)?;
|
||||
|
||||
let buffer_result = maybe!({
|
||||
let mut buffer_file = None;
|
||||
if let Some(file) = state.file.take() {
|
||||
let worktree_id = worktree::WorktreeId::from_proto(file.worktree_id);
|
||||
let worktree = worktrees
|
||||
.find(|worktree| worktree.read(cx).id() == worktree_id)
|
||||
.ok_or_else(|| {
|
||||
anyhow!("no worktree found for id {}", file.worktree_id)
|
||||
})?;
|
||||
buffer_file = Some(Arc::new(File::from_proto(file, worktree.clone(), cx)?)
|
||||
as Arc<dyn language::File>);
|
||||
}
|
||||
Buffer::from_proto(replica_id, capability, state, buffer_file)
|
||||
});
|
||||
|
||||
match buffer_result {
|
||||
Ok(buffer) => {
|
||||
let buffer = cx.new_model(|_| buffer);
|
||||
self.loading_remote_buffers_by_id.insert(buffer_id, buffer);
|
||||
}
|
||||
Err(error) => {
|
||||
if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) {
|
||||
for listener in listeners {
|
||||
listener.send(Err(anyhow!(error.cloned()))).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
proto::create_buffer_for_peer::Variant::Chunk(chunk) => {
|
||||
let buffer_id = BufferId::new(chunk.buffer_id)?;
|
||||
let buffer = self
|
||||
.loading_remote_buffers_by_id
|
||||
.get(&buffer_id)
|
||||
.cloned()
|
||||
.ok_or_else(|| {
|
||||
anyhow!(
|
||||
"received chunk for buffer {} without initial state",
|
||||
chunk.buffer_id
|
||||
)
|
||||
})?;
|
||||
|
||||
let result = maybe!({
|
||||
let operations = chunk
|
||||
.operations
|
||||
.into_iter()
|
||||
.map(language::proto::deserialize_operation)
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
buffer.update(cx, |buffer, cx| buffer.apply_ops(operations, cx))
|
||||
});
|
||||
|
||||
if let Err(error) = result {
|
||||
self.loading_remote_buffers_by_id.remove(&buffer_id);
|
||||
if let Some(listeners) = self.remote_buffer_listeners.remove(&buffer_id) {
|
||||
for listener in listeners {
|
||||
listener.send(Err(error.cloned())).ok();
|
||||
}
|
||||
}
|
||||
} else if chunk.is_last {
|
||||
self.loading_remote_buffers_by_id.remove(&buffer_id);
|
||||
self.add_buffer(buffer, cx)?;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn handle_save_buffer(
|
||||
this: Model<Self>,
|
||||
project_id: u64,
|
||||
worktree: Option<Model<Worktree>>,
|
||||
envelope: TypedEnvelope<proto::SaveBuffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<proto::BufferSaved> {
|
||||
let buffer_id = BufferId::new(envelope.payload.buffer_id)?;
|
||||
let buffer = this.update(&mut cx, |this, _| this.get_existing(buffer_id))??;
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&envelope.payload.version))
|
||||
})?
|
||||
.await?;
|
||||
let buffer_id = buffer.update(&mut cx, |buffer, _| buffer.remote_id())?;
|
||||
|
||||
if let Some(new_path) = envelope.payload.new_path {
|
||||
let worktree = worktree.context("no such worktree")?;
|
||||
let new_path = ProjectPath::from_proto(new_path);
|
||||
this.update(&mut cx, |this, cx| {
|
||||
this.save_buffer_as(buffer.clone(), new_path, worktree, cx)
|
||||
})?
|
||||
.await?;
|
||||
} else {
|
||||
this.update(&mut cx, |this, cx| this.save_buffer(buffer.clone(), cx))?
|
||||
.await?;
|
||||
}
|
||||
|
||||
buffer.update(&mut cx, |buffer, _| proto::BufferSaved {
|
||||
project_id,
|
||||
buffer_id: buffer_id.into(),
|
||||
version: serialize_version(buffer.saved_version()),
|
||||
mtime: buffer.saved_mtime().map(|time| time.into()),
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn wait_for_loading_buffer(
|
||||
mut receiver: postage::watch::Receiver<Option<Result<Model<Buffer>, Arc<anyhow::Error>>>>,
|
||||
) -> Result<Model<Buffer>, Arc<anyhow::Error>> {
|
||||
loop {
|
||||
if let Some(result) = receiver.borrow().as_ref() {
|
||||
match result {
|
||||
Ok(buffer) => return Ok(buffer.to_owned()),
|
||||
Err(e) => return Err(e.to_owned()),
|
||||
}
|
||||
}
|
||||
receiver.next().await;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl OpenBuffer {
|
||||
fn upgrade(&self) -> Option<Model<Buffer>> {
|
||||
match self {
|
||||
OpenBuffer::Strong(handle) => Some(handle.clone()),
|
||||
OpenBuffer::Weak(handle) => handle.upgrade(),
|
||||
OpenBuffer::Operations(_) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn is_not_found_error(error: &anyhow::Error) -> bool {
|
||||
error
|
||||
.root_cause()
|
||||
.downcast_ref::<io::Error>()
|
||||
.is_some_and(|err| err.kind() == io::ErrorKind::NotFound)
|
||||
}
|
||||
@@ -1,3 +1,5 @@
|
||||
mod signature_help;
|
||||
|
||||
use crate::{
|
||||
CodeAction, CoreCompletion, DocumentHighlight, Hover, HoverBlock, HoverBlockKind, InlayHint,
|
||||
InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip, InlayHintTooltip, Location,
|
||||
@@ -6,6 +8,7 @@ use crate::{
|
||||
use anyhow::{anyhow, Context, Result};
|
||||
use async_trait::async_trait;
|
||||
use client::proto::{self, PeerId};
|
||||
use clock::Global;
|
||||
use futures::future;
|
||||
use gpui::{AppContext, AsyncAppContext, Model};
|
||||
use language::{
|
||||
@@ -20,9 +23,14 @@ use lsp::{
|
||||
DocumentHighlightKind, LanguageServer, LanguageServerId, LinkedEditingRangeServerCapabilities,
|
||||
OneOf, ServerCapabilities,
|
||||
};
|
||||
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
use text::{BufferId, LineEnding};
|
||||
|
||||
pub use signature_help::{
|
||||
SignatureHelp, SIGNATURE_HELP_HIGHLIGHT_CURRENT, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
|
||||
};
|
||||
|
||||
pub fn lsp_formatting_options(tab_size: u32) -> lsp::FormattingOptions {
|
||||
lsp::FormattingOptions {
|
||||
tab_size,
|
||||
@@ -121,6 +129,11 @@ pub(crate) struct GetDocumentHighlights {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct GetSignatureHelp {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct GetHover {
|
||||
pub position: PointUtf16,
|
||||
@@ -397,16 +410,18 @@ impl LspCommand for PerformRename {
|
||||
message: proto::PerformRenameResponse,
|
||||
project: Model<Project>,
|
||||
_: Model<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
cx: AsyncAppContext,
|
||||
) -> Result<ProjectTransaction> {
|
||||
let message = message
|
||||
.transaction
|
||||
.ok_or_else(|| anyhow!("missing transaction"))?;
|
||||
project
|
||||
.update(&mut cx, |project, cx| {
|
||||
project.deserialize_project_transaction(message, self.push_to_history, cx)
|
||||
})?
|
||||
.await
|
||||
Project::deserialize_project_transaction(
|
||||
project.downgrade(),
|
||||
message,
|
||||
self.push_to_history,
|
||||
cx,
|
||||
)
|
||||
.await
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::PerformRename) -> Result<BufferId> {
|
||||
@@ -1225,6 +1240,116 @@ impl LspCommand for GetDocumentHighlights {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetSignatureHelp {
|
||||
type Response = Option<SignatureHelp>;
|
||||
type LspRequest = lsp::SignatureHelpRequest;
|
||||
type ProtoRequest = proto::GetSignatureHelp;
|
||||
|
||||
fn check_capabilities(&self, capabilities: &ServerCapabilities) -> bool {
|
||||
capabilities.signature_help_provider.is_some()
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: &Buffer,
|
||||
_: &Arc<LanguageServer>,
|
||||
_cx: &AppContext,
|
||||
) -> lsp::SignatureHelpParams {
|
||||
let url_result = lsp::Url::from_file_path(path);
|
||||
if url_result.is_err() {
|
||||
log::error!("an invalid file path has been specified");
|
||||
}
|
||||
|
||||
lsp::SignatureHelpParams {
|
||||
text_document_position_params: lsp::TextDocumentPositionParams {
|
||||
text_document: lsp::TextDocumentIdentifier {
|
||||
uri: url_result.expect("invalid file path"),
|
||||
},
|
||||
position: point_to_lsp(self.position),
|
||||
},
|
||||
context: None,
|
||||
work_done_progress_params: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
message: Option<lsp::SignatureHelp>,
|
||||
_: Model<Project>,
|
||||
buffer: Model<Buffer>,
|
||||
_: LanguageServerId,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Self::Response> {
|
||||
let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?;
|
||||
Ok(message.and_then(|message| SignatureHelp::new(message, language)))
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> Self::ProtoRequest {
|
||||
let offset = buffer.point_utf16_to_offset(self.position);
|
||||
proto::GetSignatureHelp {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().to_proto(),
|
||||
position: Some(serialize_anchor(&buffer.anchor_after(offset))),
|
||||
version: serialize_version(&buffer.version()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
payload: Self::ProtoRequest,
|
||||
_: Model<Project>,
|
||||
buffer: Model<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Self> {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&payload.version))
|
||||
})?
|
||||
.await
|
||||
.with_context(|| format!("waiting for version for buffer {}", buffer.entity_id()))?;
|
||||
let buffer_snapshot = buffer.update(&mut cx, |buffer, _| buffer.snapshot())?;
|
||||
Ok(Self {
|
||||
position: payload
|
||||
.position
|
||||
.and_then(deserialize_anchor)
|
||||
.context("invalid position")?
|
||||
.to_point_utf16(&buffer_snapshot),
|
||||
})
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Self::Response,
|
||||
_: &mut Project,
|
||||
_: PeerId,
|
||||
_: &Global,
|
||||
_: &mut AppContext,
|
||||
) -> proto::GetSignatureHelpResponse {
|
||||
proto::GetSignatureHelpResponse {
|
||||
signature_help: response
|
||||
.map(|signature_help| lsp_to_proto_signature(signature_help.original_data)),
|
||||
}
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
response: proto::GetSignatureHelpResponse,
|
||||
_: Model<Project>,
|
||||
buffer: Model<Buffer>,
|
||||
mut cx: AsyncAppContext,
|
||||
) -> Result<Self::Response> {
|
||||
let language = buffer.update(&mut cx, |buffer, _| buffer.language().cloned())?;
|
||||
Ok(response
|
||||
.signature_help
|
||||
.map(|proto_help| proto_to_lsp_signature(proto_help))
|
||||
.and_then(|lsp_help| SignatureHelp::new(lsp_help, language)))
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &Self::ProtoRequest) -> Result<BufferId> {
|
||||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetHover {
|
||||
type Response = Option<Hover>;
|
||||
|
||||
636
crates/project/src/lsp_command/signature_help.rs
Normal file
636
crates/project/src/lsp_command/signature_help.rs
Normal file
@@ -0,0 +1,636 @@
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use gpui::FontWeight;
|
||||
use language::{
|
||||
markdown::{MarkdownHighlight, MarkdownHighlightStyle},
|
||||
Language,
|
||||
};
|
||||
use rpc::proto::{self, documentation};
|
||||
|
||||
pub const SIGNATURE_HELP_HIGHLIGHT_CURRENT: MarkdownHighlight =
|
||||
MarkdownHighlight::Style(MarkdownHighlightStyle {
|
||||
italic: false,
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
weight: FontWeight::EXTRA_BOLD,
|
||||
});
|
||||
|
||||
pub const SIGNATURE_HELP_HIGHLIGHT_OVERLOAD: MarkdownHighlight =
|
||||
MarkdownHighlight::Style(MarkdownHighlightStyle {
|
||||
italic: true,
|
||||
underline: false,
|
||||
strikethrough: false,
|
||||
weight: FontWeight::NORMAL,
|
||||
});
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct SignatureHelp {
|
||||
pub markdown: String,
|
||||
pub highlights: Vec<(Range<usize>, MarkdownHighlight)>,
|
||||
pub(super) original_data: lsp::SignatureHelp,
|
||||
}
|
||||
|
||||
impl SignatureHelp {
|
||||
pub fn new(help: lsp::SignatureHelp, language: Option<Arc<Language>>) -> Option<Self> {
|
||||
let function_options_count = help.signatures.len();
|
||||
|
||||
let signature_information = help
|
||||
.active_signature
|
||||
.and_then(|active_signature| help.signatures.get(active_signature as usize))
|
||||
.or_else(|| help.signatures.first())?;
|
||||
|
||||
let str_for_join = ", ";
|
||||
let parameter_length = signature_information
|
||||
.parameters
|
||||
.as_ref()
|
||||
.map_or(0, |parameters| parameters.len());
|
||||
let mut highlight_start = 0;
|
||||
let (markdown, mut highlights): (Vec<_>, Vec<_>) = signature_information
|
||||
.parameters
|
||||
.as_ref()?
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, parameter_information)| {
|
||||
let label = match parameter_information.label.clone() {
|
||||
lsp::ParameterLabel::Simple(string) => string,
|
||||
lsp::ParameterLabel::LabelOffsets(offset) => signature_information
|
||||
.label
|
||||
.chars()
|
||||
.skip(offset[0] as usize)
|
||||
.take((offset[1] - offset[0]) as usize)
|
||||
.collect::<String>(),
|
||||
};
|
||||
let label_length = label.len();
|
||||
|
||||
let highlights = help.active_parameter.and_then(|active_parameter| {
|
||||
if i == active_parameter as usize {
|
||||
Some((
|
||||
highlight_start..(highlight_start + label_length),
|
||||
SIGNATURE_HELP_HIGHLIGHT_CURRENT,
|
||||
))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
if i != parameter_length {
|
||||
highlight_start += label_length + str_for_join.len();
|
||||
}
|
||||
|
||||
(label, highlights)
|
||||
})
|
||||
.unzip();
|
||||
|
||||
if markdown.is_empty() {
|
||||
None
|
||||
} else {
|
||||
let markdown = markdown.join(str_for_join);
|
||||
let language_name = language
|
||||
.map(|n| n.name().to_lowercase())
|
||||
.unwrap_or_default();
|
||||
|
||||
let markdown = if function_options_count >= 2 {
|
||||
let suffix = format!("(+{} overload)", function_options_count - 1);
|
||||
let highlight_start = markdown.len() + 1;
|
||||
highlights.push(Some((
|
||||
highlight_start..(highlight_start + suffix.len()),
|
||||
SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
|
||||
)));
|
||||
format!("```{language_name}\n{markdown} {suffix}")
|
||||
} else {
|
||||
format!("```{language_name}\n{markdown}")
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
markdown,
|
||||
highlights: highlights.into_iter().flatten().collect(),
|
||||
original_data: help,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn lsp_to_proto_signature(lsp_help: lsp::SignatureHelp) -> proto::SignatureHelp {
|
||||
proto::SignatureHelp {
|
||||
signatures: lsp_help
|
||||
.signatures
|
||||
.into_iter()
|
||||
.map(|signature| proto::SignatureInformation {
|
||||
label: signature.label,
|
||||
documentation: signature
|
||||
.documentation
|
||||
.map(|documentation| lsp_to_proto_documentation(documentation)),
|
||||
parameters: signature
|
||||
.parameters
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|parameter_info| proto::ParameterInformation {
|
||||
label: Some(match parameter_info.label {
|
||||
lsp::ParameterLabel::Simple(label) => {
|
||||
proto::parameter_information::Label::Simple(label)
|
||||
}
|
||||
lsp::ParameterLabel::LabelOffsets(offsets) => {
|
||||
proto::parameter_information::Label::LabelOffsets(
|
||||
proto::LabelOffsets {
|
||||
start: offsets[0],
|
||||
end: offsets[1],
|
||||
},
|
||||
)
|
||||
}
|
||||
}),
|
||||
documentation: parameter_info.documentation.map(lsp_to_proto_documentation),
|
||||
})
|
||||
.collect(),
|
||||
active_parameter: signature.active_parameter,
|
||||
})
|
||||
.collect(),
|
||||
active_signature: lsp_help.active_signature,
|
||||
active_parameter: lsp_help.active_parameter,
|
||||
}
|
||||
}
|
||||
|
||||
fn lsp_to_proto_documentation(documentation: lsp::Documentation) -> proto::Documentation {
|
||||
proto::Documentation {
|
||||
content: Some(match documentation {
|
||||
lsp::Documentation::String(string) => proto::documentation::Content::Value(string),
|
||||
lsp::Documentation::MarkupContent(content) => {
|
||||
proto::documentation::Content::MarkupContent(proto::MarkupContent {
|
||||
is_markdown: matches!(content.kind, lsp::MarkupKind::Markdown),
|
||||
value: content.value,
|
||||
})
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn proto_to_lsp_signature(proto_help: proto::SignatureHelp) -> lsp::SignatureHelp {
|
||||
lsp::SignatureHelp {
|
||||
signatures: proto_help
|
||||
.signatures
|
||||
.into_iter()
|
||||
.map(|signature| lsp::SignatureInformation {
|
||||
label: signature.label,
|
||||
documentation: signature.documentation.and_then(proto_to_lsp_documentation),
|
||||
parameters: Some(
|
||||
signature
|
||||
.parameters
|
||||
.into_iter()
|
||||
.filter_map(|parameter_info| {
|
||||
Some(lsp::ParameterInformation {
|
||||
label: match parameter_info.label? {
|
||||
proto::parameter_information::Label::Simple(string) => {
|
||||
lsp::ParameterLabel::Simple(string)
|
||||
}
|
||||
proto::parameter_information::Label::LabelOffsets(offsets) => {
|
||||
lsp::ParameterLabel::LabelOffsets([
|
||||
offsets.start,
|
||||
offsets.end,
|
||||
])
|
||||
}
|
||||
},
|
||||
documentation: parameter_info
|
||||
.documentation
|
||||
.and_then(proto_to_lsp_documentation),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
active_parameter: signature.active_parameter,
|
||||
})
|
||||
.collect(),
|
||||
active_signature: proto_help.active_signature,
|
||||
active_parameter: proto_help.active_parameter,
|
||||
}
|
||||
}
|
||||
|
||||
fn proto_to_lsp_documentation(documentation: proto::Documentation) -> Option<lsp::Documentation> {
|
||||
let documentation = {
|
||||
Some(match documentation.content? {
|
||||
documentation::Content::Value(string) => lsp::Documentation::String(string),
|
||||
documentation::Content::MarkupContent(markup) => {
|
||||
lsp::Documentation::MarkupContent(if markup.is_markdown {
|
||||
lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::Markdown,
|
||||
value: markup.value,
|
||||
}
|
||||
} else {
|
||||
lsp::MarkupContent {
|
||||
kind: lsp::MarkupKind::PlainText,
|
||||
value: markup.value,
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
};
|
||||
documentation
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::lsp_command::signature_help::{
|
||||
SignatureHelp, SIGNATURE_HELP_HIGHLIGHT_CURRENT, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD,
|
||||
};
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_1() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn test(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let markdown = (markdown.markdown, markdown.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
"```\nfoo: u8, bar: &str".to_string(),
|
||||
vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_2() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn test(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let markdown = (markdown.markdown, markdown.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
"```\nfoo: u8, bar: &str".to_string(),
|
||||
vec![(9..18, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_3() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let markdown = (markdown.markdown, markdown.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
"```\nfoo: u8, bar: &str (+1 overload)".to_string(),
|
||||
vec![
|
||||
(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
|
||||
(19..32, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_4() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(1),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let markdown = (markdown.markdown, markdown.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
"```\nhoge: String, fuga: bool (+1 overload)".to_string(),
|
||||
vec![
|
||||
(0..12, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
|
||||
(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_5() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(1),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let markdown = (markdown.markdown, markdown.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
"```\nhoge: String, fuga: bool (+1 overload)".to_string(),
|
||||
vec![
|
||||
(14..24, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
|
||||
(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_6() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(1),
|
||||
active_parameter: None,
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let markdown = (markdown.markdown, markdown.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
"```\nhoge: String, fuga: bool (+1 overload)".to_string(),
|
||||
vec![(25..38, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_7() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test1(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("foo: u8".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("bar: &str".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test2(hoge: String, fuga: bool)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("hoge: String".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("fuga: bool".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
lsp::SignatureInformation {
|
||||
label: "fn test3(one: usize, two: u32)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("one: usize".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::Simple("two: u32".to_string()),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
},
|
||||
],
|
||||
active_signature: Some(2),
|
||||
active_parameter: Some(1),
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let markdown = (markdown.markdown, markdown.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
"```\none: usize, two: u32 (+2 overload)".to_string(),
|
||||
vec![
|
||||
(12..20, SIGNATURE_HELP_HIGHLIGHT_CURRENT),
|
||||
(21..34, SIGNATURE_HELP_HIGHLIGHT_OVERLOAD)
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_8() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![],
|
||||
active_signature: None,
|
||||
active_parameter: None,
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_create_signature_help_markdown_string_9() {
|
||||
let signature_help = lsp::SignatureHelp {
|
||||
signatures: vec![lsp::SignatureInformation {
|
||||
label: "fn test(foo: u8, bar: &str)".to_string(),
|
||||
documentation: None,
|
||||
parameters: Some(vec![
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::LabelOffsets([8, 15]),
|
||||
documentation: None,
|
||||
},
|
||||
lsp::ParameterInformation {
|
||||
label: lsp::ParameterLabel::LabelOffsets([17, 26]),
|
||||
documentation: None,
|
||||
},
|
||||
]),
|
||||
active_parameter: None,
|
||||
}],
|
||||
active_signature: Some(0),
|
||||
active_parameter: Some(0),
|
||||
};
|
||||
let maybe_markdown = SignatureHelp::new(signature_help, None);
|
||||
assert!(maybe_markdown.is_some());
|
||||
|
||||
let markdown = maybe_markdown.unwrap();
|
||||
let markdown = (markdown.markdown, markdown.highlights);
|
||||
assert_eq!(
|
||||
markdown,
|
||||
(
|
||||
"```\nfoo: u8, bar: &str".to_string(),
|
||||
vec![(0..7, SIGNATURE_HELP_HIGHLIGHT_CURRENT)]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -20,6 +20,23 @@ pub struct ProjectSettings {
|
||||
/// Configuration for Git-related features
|
||||
#[serde(default)]
|
||||
pub git: GitSettings,
|
||||
|
||||
/// Configuration for how direnv configuration should be loaded
|
||||
#[serde(default)]
|
||||
pub load_direnv: DirenvSettings,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub enum DirenvSettings {
|
||||
/// Load direnv configuration through a shell hook
|
||||
#[default]
|
||||
ShellHook,
|
||||
/// Load direnv configuration directly using `direnv export json`
|
||||
///
|
||||
/// Warning: This option is experimental and might cause some inconsistent behaviour compared to using the shell hook.
|
||||
/// If it does, please report it to GitHub
|
||||
Direct,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, Default, Serialize, Deserialize, JsonSchema)]
|
||||
|
||||
@@ -322,6 +322,12 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
trigger_characters: Some(vec![".".to_string(), "::".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
|
||||
lsp::TextDocumentSyncOptions {
|
||||
save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -336,6 +342,12 @@ async fn test_managing_language_servers(cx: &mut gpui::TestAppContext) {
|
||||
trigger_characters: Some(vec![":".to_string()]),
|
||||
..Default::default()
|
||||
}),
|
||||
text_document_sync: Some(lsp::TextDocumentSyncCapability::Options(
|
||||
lsp::TextDocumentSyncOptions {
|
||||
save: Some(lsp::TextDocumentSyncSaveOptions::Supported(true)),
|
||||
..Default::default()
|
||||
},
|
||||
)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
@@ -3056,15 +3068,8 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
});
|
||||
|
||||
let remote = cx.update(|cx| {
|
||||
Worktree::remote(
|
||||
0,
|
||||
1,
|
||||
metadata,
|
||||
Box::new(CollabRemoteWorktreeClient(project.read(cx).client())),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let remote =
|
||||
cx.update(|cx| Worktree::remote(0, 1, metadata, project.read(cx).client().into(), cx));
|
||||
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
|
||||
@@ -180,6 +180,7 @@ impl Project {
|
||||
settings.max_scroll_history_lines,
|
||||
window,
|
||||
completion_tx,
|
||||
cx,
|
||||
)
|
||||
.map(|builder| {
|
||||
let terminal_handle = cx.new_model(|cx| builder.subscribe(cx));
|
||||
|
||||
177
crates/project/src/yarn.rs
Normal file
177
crates/project/src/yarn.rs
Normal file
@@ -0,0 +1,177 @@
|
||||
//! This module deals with everything related to path handling for Yarn, the package manager for Web ecosystem.
|
||||
//! Yarn is a bit peculiar, because it references paths within .zip files, which we obviously can't handle.
|
||||
//! It also uses virtual paths for peer dependencies.
|
||||
//!
|
||||
//! Long story short, before we attempt to resolve a path as a "real" path, we try to treat is as a yarn path;
|
||||
//! for .zip handling, we unpack the contents into the temp directory (yes, this is bad, against the spirit of Yarn and what-not)
|
||||
|
||||
use std::{
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use fs::Fs;
|
||||
use gpui::{AppContext, Context, Model, ModelContext, Task};
|
||||
use util::ResultExt;
|
||||
|
||||
pub(crate) struct YarnPathStore {
|
||||
temp_dirs: HashMap<Arc<Path>, tempfile::TempDir>,
|
||||
fs: Arc<dyn Fs>,
|
||||
}
|
||||
|
||||
/// Returns `None` when passed path is a malformed virtual path or it's not a virtual path at all.
|
||||
fn resolve_virtual(path: &Path) -> Option<Arc<Path>> {
|
||||
let components: Vec<_> = path.components().collect();
|
||||
let mut non_virtual_path = PathBuf::new();
|
||||
|
||||
let mut i = 0;
|
||||
let mut is_virtual = false;
|
||||
while i < components.len() {
|
||||
if let Some(os_str) = components[i].as_os_str().to_str() {
|
||||
// Detect the __virtual__ segment
|
||||
if os_str == "__virtual__" {
|
||||
let pop_count = components
|
||||
.get(i + 2)?
|
||||
.as_os_str()
|
||||
.to_str()?
|
||||
.parse::<usize>()
|
||||
.ok()?;
|
||||
|
||||
// Apply dirname operation pop_count times
|
||||
for _ in 0..pop_count {
|
||||
non_virtual_path.pop();
|
||||
}
|
||||
i += 3; // Skip hash and pop_count components
|
||||
is_virtual = true;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
non_virtual_path.push(&components[i]);
|
||||
i += 1;
|
||||
}
|
||||
|
||||
is_virtual.then(|| Arc::from(non_virtual_path))
|
||||
}
|
||||
|
||||
impl YarnPathStore {
|
||||
pub(crate) fn new(fs: Arc<dyn Fs>, cx: &mut AppContext) -> Model<Self> {
|
||||
cx.new_model(|_| Self {
|
||||
temp_dirs: Default::default(),
|
||||
fs,
|
||||
})
|
||||
}
|
||||
pub(crate) fn process_path(
|
||||
&mut self,
|
||||
path: &Path,
|
||||
protocol: &str,
|
||||
cx: &ModelContext<Self>,
|
||||
) -> Task<Option<(Arc<Path>, Arc<Path>)>> {
|
||||
let mut is_zip = protocol.eq("zip");
|
||||
|
||||
let path: &Path = if let Some(non_zip_part) = path
|
||||
.as_os_str()
|
||||
.as_encoded_bytes()
|
||||
.strip_prefix("/zip:".as_bytes())
|
||||
{
|
||||
// typescript-language-server prepends the paths with zip:, which is messy.
|
||||
is_zip = true;
|
||||
Path::new(OsStr::new(
|
||||
std::str::from_utf8(non_zip_part).expect("Invalid UTF-8"),
|
||||
))
|
||||
} else {
|
||||
path
|
||||
};
|
||||
|
||||
let as_virtual = resolve_virtual(&path);
|
||||
let Some(path) = as_virtual.or_else(|| is_zip.then(|| Arc::from(path))) else {
|
||||
return Task::ready(None);
|
||||
};
|
||||
if let Some(zip_file) = zip_path(&path) {
|
||||
let zip_file: Arc<Path> = Arc::from(zip_file);
|
||||
cx.spawn(|this, mut cx| async move {
|
||||
let dir = this
|
||||
.update(&mut cx, |this, _| {
|
||||
this.temp_dirs
|
||||
.get(&zip_file)
|
||||
.map(|temp| temp.path().to_owned())
|
||||
})
|
||||
.ok()?;
|
||||
let zip_root = if let Some(dir) = dir {
|
||||
dir
|
||||
} else {
|
||||
let fs = this.update(&mut cx, |this, _| this.fs.clone()).ok()?;
|
||||
let tempdir = dump_zip(zip_file.clone(), fs).await.log_err()?;
|
||||
let new_path = tempdir.path().to_owned();
|
||||
this.update(&mut cx, |this, _| {
|
||||
this.temp_dirs.insert(zip_file.clone(), tempdir);
|
||||
})
|
||||
.ok()?;
|
||||
new_path
|
||||
};
|
||||
// Rebase zip-path onto new temp path.
|
||||
let as_relative = path.strip_prefix(zip_file).ok()?.into();
|
||||
Some((zip_root.into(), as_relative))
|
||||
})
|
||||
} else {
|
||||
Task::ready(None)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn zip_path(path: &Path) -> Option<&Path> {
|
||||
let path_str = path.to_str()?;
|
||||
let zip_end = path_str.find(".zip/")?;
|
||||
let zip_path = &path_str[..zip_end + 4]; // ".zip" is 4 characters long
|
||||
Some(Path::new(zip_path))
|
||||
}
|
||||
|
||||
async fn dump_zip(path: Arc<Path>, fs: Arc<dyn Fs>) -> Result<tempfile::TempDir> {
|
||||
let dir = tempfile::tempdir()?;
|
||||
let contents = fs.load_bytes(&path).await?;
|
||||
node_runtime::extract_zip(dir.path(), futures::io::Cursor::new(contents)).await?;
|
||||
Ok(dir)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use std::path::Path;
|
||||
|
||||
#[test]
|
||||
fn test_resolve_virtual() {
|
||||
let test_cases = vec![
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/0/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/e4f5a0b1/0/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/folder/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/1/subpath/to/file.dat",
|
||||
Some(Path::new("/path/to/some/subpath/to/file.dat")),
|
||||
),
|
||||
(
|
||||
"/path/to/some/folder/__virtual__/a0b1c2d3/3/subpath/to/file.dat",
|
||||
Some(Path::new("/path/subpath/to/file.dat")),
|
||||
),
|
||||
("/path/to/nonvirtual/", None),
|
||||
("/path/to/malformed/__virtual__", None),
|
||||
("/path/to/malformed/__virtual__/a0b1c2d3", None),
|
||||
(
|
||||
"/path/to/malformed/__virtual__/a0b1c2d3/this-should-be-a-number",
|
||||
None,
|
||||
),
|
||||
];
|
||||
|
||||
for (input, expected) in test_cases {
|
||||
let input_path = Path::new(input);
|
||||
let resolved_path = resolve_virtual(input_path);
|
||||
assert_eq!(resolved_path.as_deref(), expected);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2293,7 +2293,7 @@ impl ProjectPanel {
|
||||
.right_0()
|
||||
.top_0()
|
||||
.bottom_0()
|
||||
.w_3()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.child(ProjectPanelScrollbar::new(
|
||||
percentage as f32..end_offset as f32,
|
||||
|
||||
@@ -19,6 +19,7 @@ doctest = false
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
collections.workspace = true
|
||||
futures.workspace = true
|
||||
prost.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
|
||||
@@ -203,6 +203,10 @@ message Envelope {
|
||||
|
||||
CompleteWithLanguageModel complete_with_language_model = 166;
|
||||
LanguageModelResponse language_model_response = 167;
|
||||
|
||||
CacheLanguageModelContent cache_language_model_content = 219;
|
||||
CacheLanguageModelContentResponse cache_language_model_content_response = 220; // current max
|
||||
|
||||
CountTokensWithLanguageModel count_tokens_with_language_model = 168;
|
||||
CountTokensResponse count_tokens_response = 169;
|
||||
GetCachedEmbeddings get_cached_embeddings = 189;
|
||||
@@ -262,7 +266,10 @@ message Envelope {
|
||||
OpenContextResponse open_context_response = 213;
|
||||
UpdateContext update_context = 214;
|
||||
SynchronizeContexts synchronize_contexts = 215;
|
||||
SynchronizeContextsResponse synchronize_contexts_response = 216; // current max
|
||||
SynchronizeContextsResponse synchronize_contexts_response = 216;
|
||||
|
||||
GetSignatureHelp get_signature_help = 217;
|
||||
GetSignatureHelpResponse get_signature_help_response = 218;
|
||||
}
|
||||
|
||||
reserved 158 to 161;
|
||||
@@ -934,6 +941,55 @@ message GetCodeActionsResponse {
|
||||
repeated VectorClockEntry version = 2;
|
||||
}
|
||||
|
||||
message GetSignatureHelp {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
Anchor position = 3;
|
||||
repeated VectorClockEntry version = 4;
|
||||
}
|
||||
|
||||
message GetSignatureHelpResponse {
|
||||
optional SignatureHelp signature_help = 1;
|
||||
}
|
||||
|
||||
message SignatureHelp {
|
||||
repeated SignatureInformation signatures = 1;
|
||||
optional uint32 active_signature = 2;
|
||||
optional uint32 active_parameter = 3;
|
||||
}
|
||||
|
||||
message SignatureInformation {
|
||||
string label = 1;
|
||||
optional Documentation documentation = 2;
|
||||
repeated ParameterInformation parameters = 3;
|
||||
optional uint32 active_parameter = 4;
|
||||
}
|
||||
|
||||
message Documentation {
|
||||
oneof content {
|
||||
string value = 1;
|
||||
MarkupContent markup_content = 2;
|
||||
}
|
||||
}
|
||||
|
||||
enum MarkupKind {
|
||||
PlainText = 0;
|
||||
Markdown = 1;
|
||||
}
|
||||
|
||||
message ParameterInformation {
|
||||
oneof label {
|
||||
string simple = 1;
|
||||
LabelOffsets label_offsets = 2;
|
||||
}
|
||||
optional Documentation documentation = 3;
|
||||
}
|
||||
|
||||
message LabelOffsets {
|
||||
uint32 start = 1;
|
||||
uint32 end = 2;
|
||||
}
|
||||
|
||||
message GetHover {
|
||||
uint64 project_id = 1;
|
||||
uint64 buffer_id = 2;
|
||||
@@ -1973,6 +2029,25 @@ message CompleteWithLanguageModel {
|
||||
float temperature = 4;
|
||||
repeated ChatCompletionTool tools = 5;
|
||||
optional string tool_choice = 6;
|
||||
repeated string cached_contents = 7;
|
||||
}
|
||||
|
||||
message CacheLanguageModelContent {
|
||||
string model = 1;
|
||||
repeated LanguageModelRequestMessage messages = 2;
|
||||
repeated string stop = 3;
|
||||
float temperature = 4;
|
||||
repeated ChatCompletionTool tools = 5;
|
||||
optional string tool_choice = 6;
|
||||
optional uint64 ttl_seconds = 7;
|
||||
}
|
||||
|
||||
message CacheLanguageModelContentResponse {
|
||||
string name = 1;
|
||||
uint64 create_time = 2;
|
||||
uint64 update_time = 3;
|
||||
optional uint64 expire_time = 4;
|
||||
uint32 total_token_count = 5;
|
||||
}
|
||||
|
||||
// A tool presented to the language model for its use
|
||||
@@ -2135,6 +2210,7 @@ message MultiLspQuery {
|
||||
oneof request {
|
||||
GetHover get_hover = 5;
|
||||
GetCodeActions get_code_actions = 6;
|
||||
GetSignatureHelp get_signature_help = 7;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2153,6 +2229,7 @@ message LspResponse {
|
||||
oneof response {
|
||||
GetHoverResponse get_hover_response = 1;
|
||||
GetCodeActionsResponse get_code_actions_response = 2;
|
||||
GetSignatureHelpResponse get_signature_help_response = 3;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -7,18 +7,19 @@ mod typed_envelope;
|
||||
pub use error::*;
|
||||
pub use typed_envelope::*;
|
||||
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use futures::{future::BoxFuture, Future};
|
||||
pub use prost::{DecodeError, Message};
|
||||
use serde::Serialize;
|
||||
use std::any::{Any, TypeId};
|
||||
use std::time::Instant;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp,
|
||||
fmt::Debug,
|
||||
iter,
|
||||
time::{Duration, SystemTime, UNIX_EPOCH},
|
||||
fmt::{self, Debug},
|
||||
iter, mem,
|
||||
sync::Arc,
|
||||
time::{Duration, Instant, SystemTime, UNIX_EPOCH},
|
||||
};
|
||||
use std::{fmt, mem};
|
||||
|
||||
include!(concat!(env!("OUT_DIR"), "/zed.messages.rs"));
|
||||
|
||||
@@ -59,6 +60,51 @@ pub enum MessagePriority {
|
||||
Background,
|
||||
}
|
||||
|
||||
pub trait ProtoClient: Send + Sync {
|
||||
fn request(
|
||||
&self,
|
||||
envelope: Envelope,
|
||||
request_type: &'static str,
|
||||
) -> BoxFuture<'static, anyhow::Result<Envelope>>;
|
||||
|
||||
fn send(&self, envelope: Envelope) -> anyhow::Result<()>;
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct AnyProtoClient(Arc<dyn ProtoClient>);
|
||||
|
||||
impl<T> From<Arc<T>> for AnyProtoClient
|
||||
where
|
||||
T: ProtoClient + 'static,
|
||||
{
|
||||
fn from(client: Arc<T>) -> Self {
|
||||
Self(client)
|
||||
}
|
||||
}
|
||||
|
||||
impl AnyProtoClient {
|
||||
pub fn new<T: ProtoClient + 'static>(client: Arc<T>) -> Self {
|
||||
Self(client)
|
||||
}
|
||||
|
||||
pub fn request<T: RequestMessage>(
|
||||
&self,
|
||||
request: T,
|
||||
) -> impl Future<Output = anyhow::Result<T::Response>> {
|
||||
let envelope = request.into_envelope(0, None, None);
|
||||
let response = self.0.request(envelope, T::NAME);
|
||||
async move {
|
||||
T::Response::from_envelope(response.await?)
|
||||
.ok_or_else(|| anyhow!("received response of the wrong type"))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn send<T: EnvelopedMessage>(&self, request: T) -> anyhow::Result<()> {
|
||||
let envelope = request.into_envelope(0, None, None);
|
||||
self.0.send(envelope)
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: EnvelopedMessage> AnyTypedEnvelope for TypedEnvelope<T> {
|
||||
fn payload_type_id(&self) -> TypeId {
|
||||
TypeId::of::<T>()
|
||||
@@ -148,6 +194,8 @@ messages!(
|
||||
(ApplyCompletionAdditionalEditsResponse, Background),
|
||||
(BufferReloaded, Foreground),
|
||||
(BufferSaved, Foreground),
|
||||
(CacheLanguageModelContent, Background),
|
||||
(CacheLanguageModelContentResponse, Background),
|
||||
(Call, Foreground),
|
||||
(CallCanceled, Foreground),
|
||||
(CancelCall, Foreground),
|
||||
@@ -204,6 +252,8 @@ messages!(
|
||||
(GetProjectSymbolsResponse, Background),
|
||||
(GetReferences, Background),
|
||||
(GetReferencesResponse, Background),
|
||||
(GetSignatureHelp, Background),
|
||||
(GetSignatureHelpResponse, Background),
|
||||
(GetSupermavenApiKey, Background),
|
||||
(GetSupermavenApiKeyResponse, Background),
|
||||
(GetTypeDefinition, Background),
|
||||
@@ -352,6 +402,7 @@ request_messages!(
|
||||
ApplyCompletionAdditionalEdits,
|
||||
ApplyCompletionAdditionalEditsResponse
|
||||
),
|
||||
(CacheLanguageModelContent, CacheLanguageModelContentResponse),
|
||||
(Call, Ack),
|
||||
(CancelCall, Ack),
|
||||
(CopyProjectEntry, ProjectEntryResponse),
|
||||
@@ -382,6 +433,7 @@ request_messages!(
|
||||
(GetPrivateUserInfo, GetPrivateUserInfoResponse),
|
||||
(GetProjectSymbols, GetProjectSymbolsResponse),
|
||||
(GetReferences, GetReferencesResponse),
|
||||
(GetSignatureHelp, GetSignatureHelpResponse),
|
||||
(GetSupermavenApiKey, GetSupermavenApiKeyResponse),
|
||||
(GetTypeDefinition, GetTypeDefinitionResponse),
|
||||
(LinkedEditingRange, LinkedEditingRangeResponse),
|
||||
@@ -482,6 +534,7 @@ entity_messages!(
|
||||
GetHover,
|
||||
GetProjectSymbols,
|
||||
GetReferences,
|
||||
GetSignatureHelp,
|
||||
GetTypeDefinition,
|
||||
InlayHints,
|
||||
JoinProject,
|
||||
|
||||
@@ -102,18 +102,21 @@ impl Render for QuickActionBar {
|
||||
inlay_hints_enabled,
|
||||
supports_inlay_hints,
|
||||
git_blame_inline_enabled,
|
||||
auto_signature_help_enabled,
|
||||
) = {
|
||||
let editor = editor.read(cx);
|
||||
let selection_menu_enabled = editor.selection_menu_enabled(cx);
|
||||
let inlay_hints_enabled = editor.inlay_hints_enabled();
|
||||
let supports_inlay_hints = editor.supports_inlay_hints(cx);
|
||||
let git_blame_inline_enabled = editor.git_blame_inline_enabled();
|
||||
let auto_signature_help_enabled = editor.auto_signature_help_enabled(cx);
|
||||
|
||||
(
|
||||
selection_menu_enabled,
|
||||
inlay_hints_enabled,
|
||||
supports_inlay_hints,
|
||||
git_blame_inline_enabled,
|
||||
auto_signature_help_enabled,
|
||||
)
|
||||
};
|
||||
|
||||
@@ -265,6 +268,23 @@ impl Render for QuickActionBar {
|
||||
},
|
||||
);
|
||||
|
||||
menu = menu.toggleable_entry(
|
||||
"Auto Signature Help",
|
||||
auto_signature_help_enabled,
|
||||
Some(editor::actions::ToggleAutoSignatureHelp.boxed_clone()),
|
||||
{
|
||||
let editor = editor.clone();
|
||||
move |cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.toggle_auto_signature_help_menu(
|
||||
&editor::actions::ToggleAutoSignatureHelp,
|
||||
cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
menu
|
||||
});
|
||||
cx.subscribe(&menu, |quick_action_bar, _, _: &DismissEvent, _cx| {
|
||||
|
||||
@@ -114,25 +114,31 @@ impl DevServerProjects {
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
let mut base_style = cx.text_style();
|
||||
base_style.refine(&gpui::TextStyleRefinement {
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let markdown_style = MarkdownStyle {
|
||||
code_block: gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Plex Mono".into()),
|
||||
color: Some(cx.theme().colors().editor_foreground),
|
||||
background_color: Some(cx.theme().colors().editor_background),
|
||||
base_text_style: base_style,
|
||||
code_block: gpui::StyleRefinement {
|
||||
text: Some(gpui::TextStyleRefinement {
|
||||
font_family: Some("Zed Plex Mono".into()),
|
||||
..Default::default()
|
||||
}),
|
||||
..Default::default()
|
||||
},
|
||||
inline_code: Default::default(),
|
||||
block_quote: Default::default(),
|
||||
link: gpui::TextStyleRefinement {
|
||||
color: Some(Color::Accent.color(cx)),
|
||||
..Default::default()
|
||||
},
|
||||
rule_color: Default::default(),
|
||||
block_quote_border_color: Default::default(),
|
||||
syntax: cx.theme().syntax().clone(),
|
||||
selection_background_color: cx.theme().players().local().selection,
|
||||
..Default::default()
|
||||
};
|
||||
let markdown = cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx));
|
||||
let markdown =
|
||||
cx.new_view(|cx| Markdown::new("".to_string(), markdown_style, None, cx, None));
|
||||
|
||||
Self {
|
||||
mode: Mode::Default(None),
|
||||
|
||||
@@ -24,6 +24,7 @@ futures.workspace = true
|
||||
image.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
multi_buffer.workspace = true
|
||||
project.workspace = true
|
||||
runtimelib.workspace = true
|
||||
schemars.workspace = true
|
||||
|
||||
@@ -11,7 +11,8 @@ use gpui::{
|
||||
actions, prelude::*, AppContext, AsyncWindowContext, EntityId, EventEmitter, FocusHandle,
|
||||
FocusOutEvent, FocusableView, Subscription, Task, View, WeakView,
|
||||
};
|
||||
use language::Point;
|
||||
use language::{Language, Point};
|
||||
use multi_buffer::MultiBufferRow;
|
||||
use project::Fs;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use std::{ops::Range, sync::Arc};
|
||||
@@ -174,34 +175,14 @@ impl RuntimePanel {
|
||||
let range = if selection.is_empty() {
|
||||
let cursor = selection.head();
|
||||
|
||||
let line_start = multi_buffer_snapshot.offset_to_point(cursor).row;
|
||||
let mut start_offset = multi_buffer_snapshot.point_to_offset(Point::new(line_start, 0));
|
||||
let cursor_row = multi_buffer_snapshot.offset_to_point(cursor).row;
|
||||
let start_offset = multi_buffer_snapshot.point_to_offset(Point::new(cursor_row, 0));
|
||||
|
||||
// Iterate backwards to find the start of the line
|
||||
while start_offset > 0 {
|
||||
let ch = multi_buffer_snapshot
|
||||
.chars_at(start_offset - 1)
|
||||
.next()
|
||||
.unwrap_or('\0');
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
start_offset -= 1;
|
||||
}
|
||||
|
||||
let mut end_offset = cursor;
|
||||
|
||||
// Iterate forwards to find the end of the line
|
||||
while end_offset < multi_buffer_snapshot.len() {
|
||||
let ch = multi_buffer_snapshot
|
||||
.chars_at(end_offset)
|
||||
.next()
|
||||
.unwrap_or('\0');
|
||||
if ch == '\n' {
|
||||
break;
|
||||
}
|
||||
end_offset += 1;
|
||||
}
|
||||
let end_point = Point::new(
|
||||
cursor_row,
|
||||
multi_buffer_snapshot.line_len(MultiBufferRow(cursor_row)),
|
||||
);
|
||||
let end_offset = start_offset.saturating_add(end_point.column as usize);
|
||||
|
||||
// Create a range from the start to the end of the line
|
||||
start_offset..end_offset
|
||||
@@ -216,7 +197,7 @@ impl RuntimePanel {
|
||||
&self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<(String, Arc<str>, Range<Anchor>)> {
|
||||
) -> Option<(String, Arc<Language>, Range<Anchor>)> {
|
||||
let editor = editor.upgrade()?;
|
||||
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
@@ -226,30 +207,24 @@ impl RuntimePanel {
|
||||
.text_for_range(anchor_range.clone())
|
||||
.collect::<String>();
|
||||
|
||||
let start_language = buffer.language_at(anchor_range.start);
|
||||
let end_language = buffer.language_at(anchor_range.end);
|
||||
|
||||
let language_name = if start_language == end_language {
|
||||
start_language
|
||||
.map(|language| language.code_fence_block_name())
|
||||
.filter(|lang| **lang != *"markdown")?
|
||||
} else {
|
||||
// If the selection spans multiple languages, don't run it
|
||||
let start_language = buffer.language_at(anchor_range.start)?;
|
||||
let end_language = buffer.language_at(anchor_range.end)?;
|
||||
if start_language != end_language {
|
||||
return None;
|
||||
};
|
||||
}
|
||||
|
||||
Some((selected_text, language_name, anchor_range))
|
||||
Some((selected_text, start_language.clone(), anchor_range))
|
||||
}
|
||||
|
||||
pub fn language(
|
||||
&self,
|
||||
editor: WeakView<Editor>,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<Arc<str>> {
|
||||
match self.snippet(editor, cx) {
|
||||
Some((_, language, _)) => Some(language),
|
||||
None => None,
|
||||
}
|
||||
) -> Option<Arc<Language>> {
|
||||
let editor = editor.upgrade()?;
|
||||
let selection = editor.read(cx).selections.newest::<usize>(cx);
|
||||
let buffer = editor.read(cx).buffer().read(cx).snapshot(cx);
|
||||
buffer.language_at(selection.head()).cloned()
|
||||
}
|
||||
|
||||
pub fn refresh_kernelspecs(&mut self, cx: &mut ViewContext<Self>) -> Task<anyhow::Result<()>> {
|
||||
@@ -266,11 +241,12 @@ impl RuntimePanel {
|
||||
|
||||
pub fn kernelspec(
|
||||
&self,
|
||||
language_name: &str,
|
||||
language: &Language,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) -> Option<KernelSpecification> {
|
||||
let settings = JupyterSettings::get_global(cx);
|
||||
let selected_kernel = settings.kernel_selections.get(language_name);
|
||||
let language_name = language.code_fence_block_name();
|
||||
let selected_kernel = settings.kernel_selections.get(language_name.as_ref());
|
||||
|
||||
self.kernel_specifications
|
||||
.iter()
|
||||
@@ -296,7 +272,7 @@ impl RuntimePanel {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
let (selected_text, language_name, anchor_range) = match self.snippet(editor.clone(), cx) {
|
||||
let (selected_text, language, anchor_range) = match self.snippet(editor.clone(), cx) {
|
||||
Some(snippet) => snippet,
|
||||
None => return Ok(()),
|
||||
};
|
||||
@@ -304,8 +280,8 @@ impl RuntimePanel {
|
||||
let entity_id = editor.entity_id();
|
||||
|
||||
let kernel_specification = self
|
||||
.kernelspec(&language_name, cx)
|
||||
.with_context(|| format!("No kernel found for language: {language_name}"))?;
|
||||
.kernelspec(&language, cx)
|
||||
.with_context(|| format!("No kernel found for language: {}", language.name()))?;
|
||||
|
||||
let session = self.sessions.entry(entity_id).or_insert_with(|| {
|
||||
let view =
|
||||
@@ -320,7 +296,6 @@ impl RuntimePanel {
|
||||
panel.sessions.remove(&shutdown_event.entity_id());
|
||||
}
|
||||
}
|
||||
//
|
||||
},
|
||||
);
|
||||
|
||||
@@ -350,7 +325,7 @@ impl RuntimePanel {
|
||||
pub enum SessionSupport {
|
||||
ActiveSession(View<Session>),
|
||||
Inactive(KernelSpecification),
|
||||
RequiresSetup(String),
|
||||
RequiresSetup(Arc<str>),
|
||||
Unsupported,
|
||||
}
|
||||
|
||||
@@ -377,11 +352,12 @@ impl RuntimePanel {
|
||||
match kernelspec {
|
||||
Some(kernelspec) => SessionSupport::Inactive(kernelspec),
|
||||
None => {
|
||||
let language: String = language.to_lowercase();
|
||||
// If no kernelspec but language is one of typescript, python, r, or julia
|
||||
// If no kernelspec but language is one of typescript or python
|
||||
// then we return RequiresSetup
|
||||
match language.as_str() {
|
||||
"typescript" | "python" => SessionSupport::RequiresSetup(language),
|
||||
match language.name().as_ref() {
|
||||
"TypeScript" | "Python" => {
|
||||
SessionSupport::RequiresSetup(language.name())
|
||||
}
|
||||
_ => SessionSupport::Unsupported,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,10 +7,13 @@ use editor::{
|
||||
display_map::{
|
||||
BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, RenderBlock,
|
||||
},
|
||||
Anchor, AnchorRangeExt as _, Editor,
|
||||
Anchor, AnchorRangeExt as _, Editor, MultiBuffer, ToPoint,
|
||||
};
|
||||
use futures::{FutureExt as _, StreamExt as _};
|
||||
use gpui::{div, prelude::*, EventEmitter, Render, Task, View, ViewContext, WeakView};
|
||||
use gpui::{
|
||||
div, prelude::*, EventEmitter, Model, Render, Subscription, Task, View, ViewContext, WeakView,
|
||||
};
|
||||
use language::Point;
|
||||
use project::Fs;
|
||||
use runtimelib::{
|
||||
ExecuteRequest, InterruptRequest, JupyterMessage, JupyterMessageContent, KernelInfoRequest,
|
||||
@@ -27,11 +30,13 @@ pub struct Session {
|
||||
blocks: HashMap<String, EditorBlock>,
|
||||
pub messaging_task: Task<()>,
|
||||
pub kernel_specification: KernelSpecification,
|
||||
_buffer_subscription: Subscription,
|
||||
}
|
||||
|
||||
struct EditorBlock {
|
||||
editor: WeakView<Editor>,
|
||||
code_range: Range<Anchor>,
|
||||
invalidation_anchor: Anchor,
|
||||
block_id: BlockId,
|
||||
execution_view: View<ExecutionView>,
|
||||
}
|
||||
@@ -45,7 +50,25 @@ impl EditorBlock {
|
||||
) -> anyhow::Result<Self> {
|
||||
let execution_view = cx.new_view(|cx| ExecutionView::new(status, cx));
|
||||
|
||||
let block_id = editor.update(cx, |editor, cx| {
|
||||
let (block_id, invalidation_anchor) = editor.update(cx, |editor, cx| {
|
||||
let buffer = editor.buffer().clone();
|
||||
let buffer_snapshot = buffer.read(cx).snapshot(cx);
|
||||
let end_point = code_range.end.to_point(&buffer_snapshot);
|
||||
let next_row_start = end_point + Point::new(1, 0);
|
||||
if next_row_start > buffer_snapshot.max_point() {
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit(
|
||||
[(
|
||||
buffer_snapshot.max_point()..buffer_snapshot.max_point(),
|
||||
"\n",
|
||||
)],
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
}
|
||||
|
||||
let invalidation_anchor = buffer.read(cx).read(cx).anchor_before(next_row_start);
|
||||
let block = BlockProperties {
|
||||
position: code_range.end,
|
||||
height: execution_view.num_lines(cx).saturating_add(1),
|
||||
@@ -54,12 +77,14 @@ impl EditorBlock {
|
||||
disposition: BlockDisposition::Below,
|
||||
};
|
||||
|
||||
editor.insert_blocks([block], None, cx)[0]
|
||||
let block_id = editor.insert_blocks([block], None, cx)[0];
|
||||
(block_id, invalidation_anchor)
|
||||
})?;
|
||||
|
||||
anyhow::Ok(Self {
|
||||
editor,
|
||||
code_range,
|
||||
invalidation_anchor,
|
||||
block_id,
|
||||
execution_view,
|
||||
})
|
||||
@@ -179,15 +204,55 @@ impl Session {
|
||||
})
|
||||
.shared();
|
||||
|
||||
let subscription = match editor.upgrade() {
|
||||
Some(editor) => {
|
||||
let buffer = editor.read(cx).buffer().clone();
|
||||
cx.subscribe(&buffer, Self::on_buffer_event)
|
||||
}
|
||||
None => Subscription::new(|| {}),
|
||||
};
|
||||
|
||||
return Self {
|
||||
editor,
|
||||
kernel: Kernel::StartingKernel(pending_kernel),
|
||||
messaging_task: Task::ready(()),
|
||||
blocks: HashMap::default(),
|
||||
kernel_specification,
|
||||
_buffer_subscription: subscription,
|
||||
};
|
||||
}
|
||||
|
||||
fn on_buffer_event(
|
||||
&mut self,
|
||||
buffer: Model<MultiBuffer>,
|
||||
event: &multi_buffer::Event,
|
||||
cx: &mut ViewContext<Self>,
|
||||
) {
|
||||
if let multi_buffer::Event::Edited { .. } = event {
|
||||
let snapshot = buffer.read(cx).snapshot(cx);
|
||||
|
||||
let mut blocks_to_remove: HashSet<BlockId> = HashSet::default();
|
||||
|
||||
self.blocks.retain(|_id, block| {
|
||||
if block.invalidation_anchor.is_valid(&snapshot) {
|
||||
true
|
||||
} else {
|
||||
blocks_to_remove.insert(block.block_id);
|
||||
false
|
||||
}
|
||||
});
|
||||
|
||||
if !blocks_to_remove.is_empty() {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| {
|
||||
editor.remove_blocks(blocks_to_remove, None, cx);
|
||||
})
|
||||
.ok();
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn send(&mut self, message: JupyterMessage, _cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
|
||||
match &mut self.kernel {
|
||||
Kernel::RunningKernel(kernel) => {
|
||||
|
||||
@@ -557,6 +557,14 @@ impl Peer {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn send_dynamic(&self, receiver_id: ConnectionId, message: proto::Envelope) -> Result<()> {
|
||||
let connection = self.connection_state(receiver_id)?;
|
||||
connection
|
||||
.outgoing_tx
|
||||
.unbounded_send(proto::Message::Envelope(message))?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn forward_send<T: EnvelopedMessage>(
|
||||
&self,
|
||||
sender_id: ConnectionId,
|
||||
|
||||
@@ -9,7 +9,7 @@ use any_vec::AnyVec;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
actions::{Tab, TabPrev},
|
||||
DisplayPoint, Editor, EditorElement, EditorStyle,
|
||||
DisplayPoint, Editor, EditorElement, EditorSettings, EditorStyle,
|
||||
};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{
|
||||
@@ -777,6 +777,15 @@ impl BufferSearchBar {
|
||||
.get(&searchable_item.downgrade())
|
||||
.filter(|matches| !matches.is_empty())
|
||||
{
|
||||
// If 'wrapscan' is disabled, searches do not wrap around the end of the file.
|
||||
if !EditorSettings::get_global(cx).search_wrap {
|
||||
if (direction == Direction::Next && index + count >= matches.len())
|
||||
|| (direction == Direction::Prev && index < count)
|
||||
{
|
||||
crate::show_no_more_matches(cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
let new_match_index = searchable_item
|
||||
.match_index_for_direction(matches, index, direction, count, cx);
|
||||
|
||||
|
||||
@@ -8,7 +8,8 @@ use editor::{
|
||||
actions::SelectAll,
|
||||
items::active_match_index,
|
||||
scroll::{Autoscroll, Axis},
|
||||
Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBuffer, MAX_TAB_TITLE_LEN,
|
||||
Anchor, Editor, EditorElement, EditorEvent, EditorSettings, EditorStyle, MultiBuffer,
|
||||
MAX_TAB_TITLE_LEN,
|
||||
};
|
||||
use gpui::{
|
||||
actions, div, Action, AnyElement, AnyView, AppContext, Context as _, Element, EntityId,
|
||||
@@ -143,7 +144,6 @@ pub struct ProjectSearchView {
|
||||
panels_with_errors: HashSet<InputPanel>,
|
||||
active_match_index: Option<usize>,
|
||||
search_id: usize,
|
||||
query_editor_was_focused: bool,
|
||||
included_files_editor: View<Editor>,
|
||||
excluded_files_editor: View<Editor>,
|
||||
filters_enabled: bool,
|
||||
@@ -714,7 +714,6 @@ impl ProjectSearchView {
|
||||
search_options: options,
|
||||
panels_with_errors: HashSet::default(),
|
||||
active_match_index: None,
|
||||
query_editor_was_focused: false,
|
||||
included_files_editor,
|
||||
excluded_files_editor,
|
||||
filters_enabled,
|
||||
@@ -970,6 +969,16 @@ impl ProjectSearchView {
|
||||
fn select_match(&mut self, direction: Direction, cx: &mut ViewContext<Self>) {
|
||||
if let Some(index) = self.active_match_index {
|
||||
let match_ranges = self.model.read(cx).match_ranges.clone();
|
||||
|
||||
if !EditorSettings::get_global(cx).search_wrap {
|
||||
if (direction == Direction::Next && index + 1 >= match_ranges.len())
|
||||
|| (direction == Direction::Prev && index == 0)
|
||||
{
|
||||
crate::show_no_more_matches(cx);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let new_index = self.results_editor.update(cx, |editor, cx| {
|
||||
editor.match_index_for_direction(&match_ranges, index, direction, 1, cx)
|
||||
});
|
||||
@@ -989,7 +998,6 @@ impl ProjectSearchView {
|
||||
self.query_editor.update(cx, |query_editor, cx| {
|
||||
query_editor.select_all(&SelectAll, cx);
|
||||
});
|
||||
self.query_editor_was_focused = true;
|
||||
let editor_handle = self.query_editor.focus_handle(cx);
|
||||
cx.focus(&editor_handle);
|
||||
}
|
||||
@@ -1004,7 +1012,6 @@ impl ProjectSearchView {
|
||||
let cursor = query_editor.selections.newest_anchor().head();
|
||||
query_editor.change_selections(None, cx, |s| s.select_ranges([cursor..cursor]));
|
||||
});
|
||||
self.query_editor_was_focused = false;
|
||||
let results_handle = self.results_editor.focus_handle(cx);
|
||||
cx.focus(&results_handle);
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user