Compare commits
75 Commits
hide-curso
...
review-onl
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5a69335491 | ||
|
|
7354ef91e1 | ||
|
|
26e461b4d4 | ||
|
|
93e3780ffc | ||
|
|
e45151b893 | ||
|
|
926d10cc45 | ||
|
|
a7697be857 | ||
|
|
97392a23e3 | ||
|
|
3f40e0f433 | ||
|
|
3e6d5c0814 | ||
|
|
2bc91e8c59 | ||
|
|
bbc80c78fd | ||
|
|
24ab5afa10 | ||
|
|
af8acba353 | ||
|
|
231e9c2000 | ||
|
|
47b94e5ef0 | ||
|
|
29e2e13e6d | ||
|
|
e635798fe0 | ||
|
|
6924720b35 | ||
|
|
1e8b50f471 | ||
|
|
5f8c53ffe8 | ||
|
|
6e82bbf367 | ||
|
|
0ac717c3a8 | ||
|
|
44aff7cd46 | ||
|
|
2b5095ac91 | ||
|
|
9e02fee98d | ||
|
|
999ad77a59 | ||
|
|
780d0eb427 | ||
|
|
7b40ab30d7 | ||
|
|
0a3c8a6790 | ||
|
|
1463b4d201 | ||
|
|
77856bf017 | ||
|
|
848a99c605 | ||
|
|
435a36b9f9 | ||
|
|
8b3ddcd545 | ||
|
|
13bf179aae | ||
|
|
7d6cc2e028 | ||
|
|
c06d004fce | ||
|
|
cdaad2655a | ||
|
|
137485819a | ||
|
|
fd17b4486c | ||
|
|
af0011b9bd | ||
|
|
65d687458c | ||
|
|
b0568d24ed | ||
|
|
53227bb847 | ||
|
|
7e4320f587 | ||
|
|
130abc8998 | ||
|
|
cc76992a55 | ||
|
|
365817370a | ||
|
|
9db4c8b710 | ||
|
|
e67ad1a1b6 | ||
|
|
82536f5243 | ||
|
|
9eacac62a9 | ||
|
|
82b0881dcb | ||
|
|
0a49ccbebf | ||
|
|
d232150d67 | ||
|
|
9a2dfa687d | ||
|
|
52aaa6c561 | ||
|
|
36ba2ac238 | ||
|
|
1e22faebc9 | ||
|
|
1d9c581ae0 | ||
|
|
39af3b434a | ||
|
|
64b3eea3cd | ||
|
|
72318df4b5 | ||
|
|
d52291bac1 | ||
|
|
7462e74fbf | ||
|
|
807b261403 | ||
|
|
f83a11741c | ||
|
|
2b84d34591 | ||
|
|
9984694ca7 | ||
|
|
a9744d6c00 | ||
|
|
4d13db41b3 | ||
|
|
05093d988b | ||
|
|
933048e867 | ||
|
|
99ba285738 |
147
Cargo.lock
generated
147
Cargo.lock
generated
@@ -453,6 +453,7 @@ dependencies = [
|
||||
"assistant_slash_command",
|
||||
"assistant_tool",
|
||||
"async-watch",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
@@ -799,9 +800,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "async-compression"
|
||||
version = "0.4.21"
|
||||
version = "0.4.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c0cf008e5e1a9e9e22a7d3c9a4992e21a350290069e36d8fb72304ed17e8f2d2"
|
||||
checksum = "59a194f9d963d8099596278594b3107448656ba73831c9d8c783e613ce86da64"
|
||||
dependencies = [
|
||||
"deflate64",
|
||||
"flate2",
|
||||
@@ -2360,7 +2361,7 @@ dependencies = [
|
||||
"cap-primitives",
|
||||
"cap-std",
|
||||
"io-lifetimes",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -2388,7 +2389,7 @@ dependencies = [
|
||||
"ipnet",
|
||||
"maybe-owned",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
"winx",
|
||||
]
|
||||
|
||||
@@ -2668,9 +2669,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap"
|
||||
version = "4.5.32"
|
||||
version = "4.5.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6088f3ae8c3608d19260cd7445411865a485688711b78b5be70d78cd96136f83"
|
||||
checksum = "e958897981290da2a852763fe9cdb89cd36977a5d729023127095fa94d95e2ff"
|
||||
dependencies = [
|
||||
"clap_builder",
|
||||
"clap_derive",
|
||||
@@ -2678,9 +2679,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "clap_builder"
|
||||
version = "4.5.32"
|
||||
version = "4.5.34"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22a7ef7f676155edfb82daa97f99441f3ebf4a58d5e32f295a56259f1b6facc8"
|
||||
checksum = "83b0f35019843db2160b5bb19ae09b4e6411ac33fc6a712003c33e03090e2489"
|
||||
dependencies = [
|
||||
"anstream",
|
||||
"anstyle",
|
||||
@@ -4589,7 +4590,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "33d852cb9b869c2a9b3df2f71a3074817f01e1844f839a144f5fcef059a4eb5d"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -4749,6 +4750,7 @@ dependencies = [
|
||||
"env_logger 0.11.7",
|
||||
"extension",
|
||||
"fs",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"reqwest_client",
|
||||
@@ -5228,7 +5230,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"libc",
|
||||
"log",
|
||||
"notify 6.1.1",
|
||||
"notify 8.0.0 (git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96)",
|
||||
"objc",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
@@ -5252,7 +5254,7 @@ checksum = "5e2e6123af26f0f2c51cc66869137080199406754903cc926a7690401ce09cb4"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6833,17 +6835,6 @@ dependencies = [
|
||||
"zeta",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.9.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff"
|
||||
dependencies = [
|
||||
"bitflags 1.3.2",
|
||||
"inotify-sys",
|
||||
"libc",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "inotify"
|
||||
version = "0.11.0"
|
||||
@@ -6920,7 +6911,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2285ddfe3054097ef4b2fe909ef8c3bcd1ea52a8f0d274416caebeef39f04a65"
|
||||
dependencies = [
|
||||
"io-lifetimes",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6949,7 +6940,7 @@ dependencies = [
|
||||
"fnv",
|
||||
"lazy_static",
|
||||
"libc",
|
||||
"mio 1.0.3",
|
||||
"mio",
|
||||
"rand 0.8.5",
|
||||
"serde",
|
||||
"tempfile",
|
||||
@@ -7566,7 +7557,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fc2f4eb4bc735547cfed7c0a4922cbd04a4655978c09b54f1f7b228750664c34"
|
||||
dependencies = [
|
||||
"cfg-if",
|
||||
"windows-targets 0.52.6",
|
||||
"windows-targets 0.48.5",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -8151,7 +8142,7 @@ dependencies = [
|
||||
"ignore",
|
||||
"log",
|
||||
"memchr",
|
||||
"notify 8.0.0",
|
||||
"notify 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify-debouncer-mini",
|
||||
"once_cell",
|
||||
"opener",
|
||||
@@ -8300,18 +8291,6 @@ version = "0.5.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e53debba6bda7a793e5f99b8dacf19e626084f525f7829104ba9898f367d85ff"
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "0.8.11"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c"
|
||||
dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"wasi 0.11.0+wasi-snapshot-preview1",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "mio"
|
||||
version = "1.0.3"
|
||||
@@ -8589,25 +8568,6 @@ dependencies = [
|
||||
"workspace",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "6.1.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"crossbeam-channel",
|
||||
"filetime",
|
||||
"fsevent-sys 4.1.0",
|
||||
"inotify 0.9.6",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 0.8.11",
|
||||
"walkdir",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.0.0"
|
||||
@@ -8617,12 +8577,30 @@ dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"filetime",
|
||||
"fsevent-sys 4.1.0",
|
||||
"inotify 0.11.0",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio 1.0.3",
|
||||
"notify-types",
|
||||
"mio",
|
||||
"notify-types 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"walkdir",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "notify"
|
||||
version = "8.0.0"
|
||||
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"filetime",
|
||||
"fsevent-sys 4.1.0",
|
||||
"inotify",
|
||||
"kqueue",
|
||||
"libc",
|
||||
"log",
|
||||
"mio",
|
||||
"notify-types 2.0.0 (git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96)",
|
||||
"walkdir",
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
@@ -8634,8 +8612,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a689eb4262184d9a1727f9087cd03883ea716682ab03ed24efec57d7716dccb8"
|
||||
dependencies = [
|
||||
"log",
|
||||
"notify 8.0.0",
|
||||
"notify-types",
|
||||
"notify 8.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"notify-types 2.0.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"tempfile",
|
||||
]
|
||||
|
||||
@@ -8645,6 +8623,11 @@ version = "2.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "5e0826a989adedc2a244799e823aece04662b66609d96af8dff7ac6df9a8925d"
|
||||
|
||||
[[package]]
|
||||
name = "notify-types"
|
||||
version = "2.0.0"
|
||||
source = "git+https://github.com/zed-industries/notify.git?rev=bbb9ea5ae52b253e095737847e367c30653a2e96#bbb9ea5ae52b253e095737847e367c30653a2e96"
|
||||
|
||||
[[package]]
|
||||
name = "ntapi"
|
||||
version = "0.4.1"
|
||||
@@ -10245,9 +10228,9 @@ checksum = "953ec861398dccce10c670dfeaf3ec4911ca479e9c02154b3a215178c5f566f2"
|
||||
|
||||
[[package]]
|
||||
name = "plist"
|
||||
version = "1.7.0"
|
||||
version = "1.7.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "42cf17e9a1800f5f396bc67d193dc9411b59012a5876445ef450d449881e1016"
|
||||
checksum = "eac26e981c03a6e53e0aee43c113e3202f5581d5360dae7bd2c70e800dd0451d"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"indexmap",
|
||||
@@ -10729,7 +10712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
||||
dependencies = [
|
||||
"bytes 1.10.1",
|
||||
"heck 0.5.0",
|
||||
"heck 0.4.1",
|
||||
"itertools 0.12.1",
|
||||
"log",
|
||||
"multimap 0.10.0",
|
||||
@@ -10957,7 +10940,7 @@ dependencies = [
|
||||
"once_cell",
|
||||
"socket2",
|
||||
"tracing",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11882,7 +11865,7 @@ dependencies = [
|
||||
"libc",
|
||||
"linux-raw-sys",
|
||||
"once_cell",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -11993,7 +11976,7 @@ dependencies = [
|
||||
"security-framework 3.0.1",
|
||||
"security-framework-sys",
|
||||
"webpki-root-certs",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -12415,18 +12398,18 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "serde"
|
||||
version = "1.0.218"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "e8dfc9d19bdbf6d17e22319da49161d5d0108e4188e8b680aef6299eed22df60"
|
||||
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
|
||||
dependencies = [
|
||||
"serde_derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_derive"
|
||||
version = "1.0.218"
|
||||
version = "1.0.219"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f09503e191f4e797cb8aac08e9a4a4695c5edf6a2e70e376d961ddd5c969f82b"
|
||||
checksum = "5b0276cf7f2c73365f7157c8123c21cd9a50fbbd844757af28ca1f5925fc2a00"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -13657,7 +13640,7 @@ dependencies = [
|
||||
"fd-lock",
|
||||
"io-lifetimes",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
"winx",
|
||||
]
|
||||
|
||||
@@ -13801,7 +13784,7 @@ dependencies = [
|
||||
"getrandom 0.3.1",
|
||||
"once_cell",
|
||||
"rustix",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -14075,9 +14058,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "time"
|
||||
version = "0.3.40"
|
||||
version = "0.3.41"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "9d9c75b47bdff86fa3334a3db91356b8d7d86a9b839dab7d0bdc5c3d3a077618"
|
||||
checksum = "8a7619e19bc266e0f9c5e6686659d394bc57973859340060a69221e57dbc0c40"
|
||||
dependencies = [
|
||||
"deranged",
|
||||
"itoa",
|
||||
@@ -14098,9 +14081,9 @@ checksum = "c9e9a38711f559d9e3ce1cdb06dd7c5b8ea546bc90052da6d06bb76da74bb07c"
|
||||
|
||||
[[package]]
|
||||
name = "time-macros"
|
||||
version = "0.2.21"
|
||||
version = "0.2.22"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "29aa485584182073ed57fd5004aa09c371f021325014694e432313345865fd04"
|
||||
checksum = "3526739392ec93fd8b359c8e98514cb3e8e021beb4e5f597b00a0221f8ed8a49"
|
||||
dependencies = [
|
||||
"num-conv",
|
||||
"time-core",
|
||||
@@ -14241,7 +14224,7 @@ dependencies = [
|
||||
"backtrace",
|
||||
"bytes 1.10.1",
|
||||
"libc",
|
||||
"mio 1.0.3",
|
||||
"mio",
|
||||
"parking_lot",
|
||||
"pin-project-lite",
|
||||
"signal-hook-registry",
|
||||
@@ -16190,7 +16173,7 @@ version = "0.1.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "cf221c93e13a30d793f7645a0e7762c55d169dbb0a49671918a2319d289b10bb"
|
||||
dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.48.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -16750,7 +16733,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3f3fd376f71958b862e7afb20cfe5a22830e1963462f3a17f49d82a6c1d1f42d"
|
||||
dependencies = [
|
||||
"bitflags 2.8.0",
|
||||
"windows-sys 0.59.0",
|
||||
"windows-sys 0.52.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -17272,7 +17255,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.180.0"
|
||||
version = "0.181.0"
|
||||
dependencies = [
|
||||
"activity_indicator",
|
||||
"anyhow",
|
||||
|
||||
5
assets/icons/file_create.svg
Normal file
5
assets/icons/file_create.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6 8H10" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M8 10V6" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 762 B |
5
assets/icons/file_delete.svg
Normal file
5
assets/icons/file_delete.svg
Normal file
@@ -0,0 +1,5 @@
|
||||
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path d="M10.0001 1.33334H4.00008C3.64646 1.33334 3.30732 1.47382 3.05727 1.72387C2.80722 1.97392 2.66675 2.31305 2.66675 2.66668V13.3333C2.66675 13.687 2.80722 14.0261 3.05727 14.2762C3.30732 14.5262 3.64646 14.6667 4.00008 14.6667H12.0001C12.3537 14.6667 12.6928 14.5262 12.9429 14.2762C13.1929 14.0261 13.3334 13.687 13.3334 13.3333V4.66668L10.0001 1.33334Z" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M9.66659 6.5L6.33325 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<path d="M6.33325 6.5L9.66659 9.83333" stroke="black" stroke-width="1.33333" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 804 B |
@@ -754,8 +754,11 @@
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"ctrl-enter": "git::Commit",
|
||||
"alt-enter": "menu::SecondaryConfirm",
|
||||
"shift-delete": "git::RestoreFile",
|
||||
"ctrl-delete": "git::RestoreFile"
|
||||
"delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"shift-delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"ctrl-backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"ctrl-delete": ["git::RestoreFile", { "skip_prompt": false }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -803,7 +803,10 @@
|
||||
"shift-tab": "git_panel::FocusEditor",
|
||||
"escape": "git_panel::ToggleFocus",
|
||||
"cmd-enter": "git::Commit",
|
||||
"cmd-backspace": "git::RestoreFile"
|
||||
"backspace": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"delete": ["git::RestoreFile", { "skip_prompt": false }],
|
||||
"cmd-backspace": ["git::RestoreFile", { "skip_prompt": true }],
|
||||
"cmd-delete": ["git::RestoreFile", { "skip_prompt": true }]
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -8,6 +8,8 @@ It will be up to you to decide which of these you are doing based on what the us
|
||||
You should only perform actions that modify the user’s system if explicitly requested by the user:
|
||||
- If the user asks a question about how to accomplish a task, provide guidance or information, and use read-only tools (e.g., search) to assist. You may suggest potential actions, but do not directly modify the user’s system without explicit instruction.
|
||||
- If the user clearly requests that you perform an action, carry out the action directly without explaining why you are doing so.
|
||||
- The editing actions you perform might produce errors or warnings. At the end of your changes, check whether you introduced any problems, and fix them before providing a summary of the changes you made.
|
||||
- Do not fix errors unrelated to your changes unless the user explicitly asks you to do so.
|
||||
|
||||
Be concise and direct in your responses.
|
||||
|
||||
|
||||
@@ -429,6 +429,8 @@
|
||||
"project_panel": {
|
||||
// Whether to show the project panel button in the status bar
|
||||
"button": true,
|
||||
// Whether to hide the gitignore entries in the project panel.
|
||||
"hide_gitignore": false,
|
||||
// Default width of the project panel.
|
||||
"default_width": 240,
|
||||
// Where to dock the project panel. Can be 'left' or 'right'.
|
||||
@@ -622,6 +624,7 @@
|
||||
// The model to use.
|
||||
"model": "claude-3-5-sonnet-latest"
|
||||
},
|
||||
"default_profile": "code-writer",
|
||||
"profiles": {
|
||||
"read-only": {
|
||||
"name": "Read-only",
|
||||
|
||||
@@ -3712,7 +3712,7 @@ mod tests {
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
@@ -4091,6 +4091,7 @@ mod tests {
|
||||
future::ready(Ok(LanguageModelTextStream {
|
||||
message_id: None,
|
||||
stream: chunks_rx.map(Ok).boxed(),
|
||||
last_token_usage: Arc::new(Mutex::new(TokenUsage::default())),
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -25,6 +25,7 @@ assistant_settings.workspace = true
|
||||
assistant_slash_command.workspace = true
|
||||
assistant_tool.workspace = true
|
||||
async-watch.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
client.workspace = true
|
||||
clock.workspace = true
|
||||
@@ -85,6 +86,7 @@ workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
|
||||
@@ -5,16 +5,16 @@ use crate::thread::{
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_use::{PendingToolUseStatus, ToolUse, ToolUseStatus};
|
||||
use crate::ui::{ContextPill, ToolReadyPopUp, ToolReadyPopupEvent};
|
||||
|
||||
use crate::AssistantPanel;
|
||||
use assistant_settings::AssistantSettings;
|
||||
use collections::HashMap;
|
||||
use editor::{Editor, MultiBuffer};
|
||||
use gpui::{
|
||||
linear_color_stop, linear_gradient, list, percentage, pulsating_between, AbsoluteLength,
|
||||
Animation, AnimationExt, AnyElement, App, ClickEvent, DefiniteLength, EdgesRefinement, Empty,
|
||||
Entity, Focusable, Length, ListAlignment, ListOffset, ListState, ScrollHandle, StyleRefinement,
|
||||
Subscription, Task, TextStyleRefinement, Transformation, UnderlineStyle, WeakEntity,
|
||||
WindowHandle,
|
||||
Entity, Focusable, Hsla, Length, ListAlignment, ListOffset, ListState, MouseButton,
|
||||
ScrollHandle, Stateful, StyleRefinement, Subscription, Task, TextStyleRefinement,
|
||||
Transformation, UnderlineStyle, WeakEntity, WindowHandle,
|
||||
};
|
||||
use language::{Buffer, LanguageRegistry};
|
||||
use language_model::{LanguageModelRegistry, LanguageModelToolUseId, Role};
|
||||
@@ -23,7 +23,7 @@ use settings::Settings as _;
|
||||
use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Tooltip};
|
||||
use ui::{prelude::*, Disclosure, IconButton, KeyBinding, Scrollbar, ScrollbarState, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{OpenOptions, Workspace};
|
||||
|
||||
@@ -38,6 +38,7 @@ pub struct ActiveThread {
|
||||
save_thread_task: Option<Task<()>>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
scrollbar_state: ScrollbarState,
|
||||
rendered_messages_by_id: HashMap<MessageId, RenderedMessage>,
|
||||
rendered_tool_use_labels: HashMap<LanguageModelToolUseId, Entity<Markdown>>,
|
||||
editing_message: Option<(MessageId, EditMessageState)>,
|
||||
@@ -226,6 +227,14 @@ impl ActiveThread {
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event),
|
||||
];
|
||||
|
||||
let list_state = ListState::new(0, ListAlignment::Bottom, px(2048.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
language_registry,
|
||||
thread_store,
|
||||
@@ -238,13 +247,8 @@ impl ActiveThread {
|
||||
rendered_tool_use_labels: HashMap::default(),
|
||||
expanded_tool_uses: HashMap::default(),
|
||||
expanded_thinking_segments: HashMap::default(),
|
||||
list_state: ListState::new(0, ListAlignment::Bottom, px(1024.), {
|
||||
let this = cx.entity().downgrade();
|
||||
move |ix, window: &mut Window, cx: &mut App| {
|
||||
this.update(cx, |this, cx| this.render_message(ix, window, cx))
|
||||
.unwrap()
|
||||
}
|
||||
}),
|
||||
list_state: list_state.clone(),
|
||||
scrollbar_state: ScrollbarState::new(list_state),
|
||||
editing_message: None,
|
||||
last_error: None,
|
||||
pop_ups: Vec::new(),
|
||||
@@ -376,11 +380,23 @@ impl ActiveThread {
|
||||
}
|
||||
ThreadEvent::DoneStreaming => {
|
||||
if !self.thread().read(cx).is_generating() {
|
||||
self.show_notification("Your changes have been applied.", window, cx);
|
||||
self.show_notification(
|
||||
"Your changes have been applied.",
|
||||
IconName::Check,
|
||||
Color::Success,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolConfirmationNeeded => {
|
||||
self.show_notification("There's a tool confirmation needed.", window, cx);
|
||||
self.show_notification(
|
||||
"There's a tool confirmation needed.",
|
||||
IconName::Info,
|
||||
Color::Muted,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
ThreadEvent::StreamedAssistantText(message_id, text) => {
|
||||
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
|
||||
@@ -505,12 +521,18 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
ThreadEvent::CheckpointChanged => cx.notify(),
|
||||
ThreadEvent::DiffChanged => {
|
||||
// todo!("update list of changed files")
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn show_notification(
|
||||
&mut self,
|
||||
caption: impl Into<SharedString>,
|
||||
icon: IconName,
|
||||
icon_color: Color,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<'_, ActiveThread>,
|
||||
) {
|
||||
@@ -525,7 +547,7 @@ impl ActiveThread {
|
||||
|
||||
if let Some(screen_window) = cx
|
||||
.open_window(options, |_, cx| {
|
||||
cx.new(|_| ToolReadyPopUp::new(caption.clone()))
|
||||
cx.new(|_| ToolReadyPopUp::new(caption.clone(), icon, icon_color))
|
||||
})
|
||||
.log_err()
|
||||
{
|
||||
@@ -536,11 +558,22 @@ impl ActiveThread {
|
||||
let handle = window.window_handle();
|
||||
cx.activate(true); // Switch back to the Zed application
|
||||
|
||||
let workspace_handle = this.workspace.clone();
|
||||
|
||||
// If there are multiple Zed windows, activate the correct one.
|
||||
cx.defer(move |cx| {
|
||||
handle
|
||||
.update(cx, |_view, window, _cx| {
|
||||
window.activate_window();
|
||||
|
||||
if let Some(workspace) = workspace_handle.upgrade()
|
||||
{
|
||||
workspace.update(_cx, |workspace, cx| {
|
||||
workspace.focus_panel::<AssistantPanel>(
|
||||
window, cx,
|
||||
);
|
||||
});
|
||||
}
|
||||
})
|
||||
.log_err();
|
||||
});
|
||||
@@ -1145,6 +1178,17 @@ impl ActiveThread {
|
||||
)
|
||||
}
|
||||
|
||||
fn tool_card_border_color(&self, cx: &Context<Self>) -> Hsla {
|
||||
cx.theme().colors().border.opacity(0.5)
|
||||
}
|
||||
|
||||
fn tool_card_header_bg(&self, cx: &Context<Self>) -> Hsla {
|
||||
cx.theme()
|
||||
.colors()
|
||||
.element_background
|
||||
.blend(cx.theme().colors().editor_foreground.opacity(0.025))
|
||||
}
|
||||
|
||||
fn render_message_thinking_segment(
|
||||
&self,
|
||||
message_id: MessageId,
|
||||
@@ -1160,26 +1204,25 @@ impl ActiveThread {
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let lighter_border = cx.theme().colors().border.opacity(0.5);
|
||||
let editor_bg = cx.theme().colors().editor_background;
|
||||
|
||||
div().py_2().child(
|
||||
v_flex()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(lighter_border)
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.group("disclosure-header")
|
||||
.justify_between()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
|
||||
.bg(self.tool_card_header_bg(cx))
|
||||
.map(|this| {
|
||||
if pending || is_open {
|
||||
this.rounded_t_md()
|
||||
.border_b_1()
|
||||
.border_color(lighter_border)
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
} else {
|
||||
this.rounded_md()
|
||||
}
|
||||
@@ -1314,21 +1357,21 @@ impl ActiveThread {
|
||||
.copied()
|
||||
.unwrap_or_default();
|
||||
|
||||
let lighter_border = cx.theme().colors().border.opacity(0.5);
|
||||
|
||||
div().py_2().child(
|
||||
v_flex()
|
||||
.rounded_lg()
|
||||
.border_1()
|
||||
.border_color(lighter_border)
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.overflow_hidden()
|
||||
.child(
|
||||
h_flex()
|
||||
.group("disclosure-header")
|
||||
.relative()
|
||||
.gap_1p5()
|
||||
.justify_between()
|
||||
.py_1()
|
||||
.px_2()
|
||||
.bg(cx.theme().colors().editor_foreground.opacity(0.025))
|
||||
.bg(self.tool_card_header_bg(cx))
|
||||
.map(|element| {
|
||||
if is_open {
|
||||
element.border_b_1().rounded_t_md()
|
||||
@@ -1336,25 +1379,22 @@ impl ActiveThread {
|
||||
element.rounded_md()
|
||||
}
|
||||
})
|
||||
.border_color(lighter_border)
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
h_flex()
|
||||
.id("tool-label-container")
|
||||
.relative()
|
||||
.gap_1p5()
|
||||
.max_w_full()
|
||||
.overflow_x_scroll()
|
||||
.child(
|
||||
Icon::new(tool_use.icon)
|
||||
.size(IconSize::XSmall)
|
||||
.color(Color::Muted),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_ui_sm(cx)
|
||||
.children(
|
||||
self.rendered_tool_use_labels
|
||||
.get(&tool_use.id)
|
||||
.cloned(),
|
||||
)
|
||||
.truncate(),
|
||||
),
|
||||
.child(h_flex().pr_8().text_ui_sm(cx).children(
|
||||
self.rendered_tool_use_labels.get(&tool_use.id).cloned(),
|
||||
)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
@@ -1412,7 +1452,14 @@ impl ActiveThread {
|
||||
icon.into_any_element()
|
||||
}
|
||||
}),
|
||||
),
|
||||
)
|
||||
.child(div().h_full().absolute().w_8().bottom_0().right_12().bg(
|
||||
linear_gradient(
|
||||
90.,
|
||||
linear_color_stop(self.tool_card_header_bg(cx), 1.),
|
||||
linear_color_stop(self.tool_card_header_bg(cx).opacity(0.2), 0.),
|
||||
),
|
||||
)),
|
||||
)
|
||||
.map(|parent| {
|
||||
if !is_open {
|
||||
@@ -1429,7 +1476,7 @@ impl ActiveThread {
|
||||
.child(
|
||||
content_container()
|
||||
.border_b_1()
|
||||
.border_color(lighter_border)
|
||||
.border_color(self.tool_card_border_color(cx))
|
||||
.child(
|
||||
Label::new("Input")
|
||||
.size(LabelSize::XSmall)
|
||||
@@ -1709,13 +1756,48 @@ impl ActiveThread {
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
|
||||
fn render_vertical_scrollbar(&self, cx: &mut Context<Self>) -> Stateful<Div> {
|
||||
div()
|
||||
.occlude()
|
||||
.id("active-thread-scrollbar")
|
||||
.on_mouse_move(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
cx.stop_propagation()
|
||||
}))
|
||||
.on_hover(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_any_mouse_down(|_, _, cx| {
|
||||
cx.stop_propagation();
|
||||
})
|
||||
.on_mouse_up(
|
||||
MouseButton::Left,
|
||||
cx.listener(|_, _, _, cx| {
|
||||
cx.stop_propagation();
|
||||
}),
|
||||
)
|
||||
.on_scroll_wheel(cx.listener(|_, _, _, cx| {
|
||||
cx.notify();
|
||||
}))
|
||||
.h_full()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.top_1()
|
||||
.bottom_0()
|
||||
.w(px(12.))
|
||||
.cursor_default()
|
||||
.children(Scrollbar::vertical(self.scrollbar_state.clone()))
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ActiveThread {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.size_full()
|
||||
.relative()
|
||||
.child(list(self.list_state.clone()).flex_grow())
|
||||
.children(self.render_confirmations(cx))
|
||||
.child(self.render_vertical_scrollbar(cx))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,12 +11,13 @@ mod history_store;
|
||||
mod inline_assistant;
|
||||
mod inline_prompt_editor;
|
||||
mod message_editor;
|
||||
mod profile_selector;
|
||||
mod terminal_codegen;
|
||||
mod terminal_inline_assistant;
|
||||
mod thread;
|
||||
mod thread_diff;
|
||||
mod thread_history;
|
||||
mod thread_store;
|
||||
mod tool_selector;
|
||||
mod tool_use;
|
||||
mod ui;
|
||||
|
||||
@@ -32,10 +33,11 @@ use prompt_store::PromptBuilder;
|
||||
use settings::Settings as _;
|
||||
|
||||
pub use crate::active_thread::ActiveThread;
|
||||
use crate::assistant_configuration::AddContextServerModal;
|
||||
use crate::assistant_configuration::{AddContextServerModal, ManageProfilesModal};
|
||||
pub use crate::assistant_panel::{AssistantPanel, ConcreteAssistantPanelDelegate};
|
||||
pub use crate::inline_assistant::InlineAssistant;
|
||||
pub use crate::thread::{Message, RequestKind, Thread, ThreadEvent};
|
||||
pub(crate) use crate::thread_diff::*;
|
||||
pub use crate::thread_store::ThreadStore;
|
||||
|
||||
actions!(
|
||||
@@ -47,6 +49,7 @@ actions!(
|
||||
RemoveAllContext,
|
||||
OpenHistory,
|
||||
OpenConfiguration,
|
||||
ManageProfiles,
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
@@ -89,6 +92,7 @@ pub fn init(
|
||||
cx,
|
||||
);
|
||||
cx.observe_new(AddContextServerModal::register).detach();
|
||||
cx.observe_new(ManageProfilesModal::register).detach();
|
||||
|
||||
feature_gate_assistant2_actions(cx);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
mod add_context_server_modal;
|
||||
mod manage_profiles_modal;
|
||||
mod profile_picker;
|
||||
mod tool_picker;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -12,6 +15,7 @@ use util::ResultExt as _;
|
||||
use zed_actions::ExtensionCategoryFilter;
|
||||
|
||||
pub(crate) use add_context_server_modal::AddContextServerModal;
|
||||
pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
|
||||
use crate::AddContextServer;
|
||||
|
||||
|
||||
@@ -0,0 +1,201 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::*, DismissEvent, Entity, EventEmitter, FocusHandle, Focusable};
|
||||
use settings::Settings as _;
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing, Navigable, NavigableEntry};
|
||||
use workspace::{ModalView, Workspace};
|
||||
|
||||
use crate::assistant_configuration::profile_picker::{ProfilePicker, ProfilePickerDelegate};
|
||||
use crate::assistant_configuration::tool_picker::{ToolPicker, ToolPickerDelegate};
|
||||
use crate::{AssistantPanel, ManageProfiles};
|
||||
|
||||
enum Mode {
|
||||
ChooseProfile(Entity<ProfilePicker>),
|
||||
ViewProfile(ViewProfileMode),
|
||||
ConfigureTools(Entity<ToolPicker>),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ViewProfileMode {
|
||||
profile_id: Arc<str>,
|
||||
configure_tools: NavigableEntry,
|
||||
}
|
||||
|
||||
pub struct ManageProfilesModal {
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
focus_handle: FocusHandle,
|
||||
mode: Mode,
|
||||
}
|
||||
|
||||
impl ManageProfilesModal {
|
||||
pub fn register(
|
||||
workspace: &mut Workspace,
|
||||
_window: Option<&mut Window>,
|
||||
_cx: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(|workspace, _: &ManageProfiles, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let thread_store = panel.read(cx).thread_store().read(cx);
|
||||
let tools = thread_store.tools();
|
||||
workspace.toggle_modal(window, cx, |window, cx| Self::new(fs, tools, window, cx))
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let handle = cx.entity();
|
||||
|
||||
Self {
|
||||
fs,
|
||||
tools,
|
||||
focus_handle,
|
||||
mode: Mode::ChooseProfile(cx.new(|cx| {
|
||||
let delegate = ProfilePickerDelegate::new(
|
||||
move |profile_id, window, cx| {
|
||||
handle.update(cx, |this, cx| {
|
||||
this.view_profile(profile_id.clone(), window, cx);
|
||||
})
|
||||
},
|
||||
cx,
|
||||
);
|
||||
ProfilePicker::new(delegate, window, cx)
|
||||
})),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn view_profile(
|
||||
&mut self,
|
||||
profile_id: Arc<str>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.mode = Mode::ViewProfile(ViewProfileMode {
|
||||
profile_id,
|
||||
configure_tools: NavigableEntry::focusable(cx),
|
||||
});
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn configure_tools(
|
||||
&mut self,
|
||||
profile_id: Arc<str>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let Some(profile) = settings.profiles.get(&profile_id).cloned() else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.mode = Mode::ConfigureTools(cx.new(|cx| {
|
||||
let delegate = ToolPickerDelegate::new(
|
||||
self.fs.clone(),
|
||||
self.tools.clone(),
|
||||
profile_id,
|
||||
profile,
|
||||
cx,
|
||||
);
|
||||
ToolPicker::new(delegate, window, cx)
|
||||
}));
|
||||
self.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
|
||||
|
||||
fn cancel(&mut self, _window: &mut Window, _cx: &mut Context<Self>) {}
|
||||
}
|
||||
|
||||
impl ModalView for ManageProfilesModal {}
|
||||
|
||||
impl Focusable for ManageProfilesModal {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.mode {
|
||||
Mode::ChooseProfile(profile_picker) => profile_picker.focus_handle(cx),
|
||||
Mode::ConfigureTools(tool_picker) => tool_picker.focus_handle(cx),
|
||||
Mode::ViewProfile(_) => self.focus_handle.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ManageProfilesModal {}
|
||||
|
||||
impl ManageProfilesModal {
|
||||
fn render_view_profile(
|
||||
&mut self,
|
||||
mode: ViewProfileMode,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Navigable::new(
|
||||
div()
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.child(
|
||||
v_flex().child(
|
||||
div()
|
||||
.id("configure-tools")
|
||||
.track_focus(&mode.configure_tools.focus_handle)
|
||||
.on_action({
|
||||
let profile_id = mode.profile_id.clone();
|
||||
cx.listener(move |this, _: &menu::Confirm, window, cx| {
|
||||
this.configure_tools(profile_id.clone(), window, cx);
|
||||
})
|
||||
})
|
||||
.child(
|
||||
ListItem::new("configure-tools")
|
||||
.toggle_state(
|
||||
mode.configure_tools
|
||||
.focus_handle
|
||||
.contains_focused(window, cx),
|
||||
)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.start_slot(Icon::new(IconName::Cog))
|
||||
.child(Label::new("Configure Tools"))
|
||||
.on_click({
|
||||
let profile_id = mode.profile_id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
this.configure_tools(profile_id.clone(), window, cx);
|
||||
})
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
.entry(mode.configure_tools)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ManageProfilesModal {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
div()
|
||||
.elevation_3(cx)
|
||||
.w(rems(34.))
|
||||
.key_context("ManageProfilesModal")
|
||||
.on_action(cx.listener(|this, _: &menu::Cancel, window, cx| this.cancel(window, cx)))
|
||||
.on_action(cx.listener(|this, _: &menu::Confirm, window, cx| this.confirm(window, cx)))
|
||||
.capture_any_mouse_down(cx.listener(|this, _, window, cx| {
|
||||
this.focus_handle(cx).focus(window);
|
||||
}))
|
||||
.on_mouse_down_out(cx.listener(|_this, _, _, cx| cx.emit(DismissEvent)))
|
||||
.child(match &self.mode {
|
||||
Mode::ChooseProfile(profile_picker) => profile_picker.clone().into_any_element(),
|
||||
Mode::ViewProfile(mode) => self
|
||||
.render_view_profile(mode.clone(), window, cx)
|
||||
.into_any_element(),
|
||||
Mode::ConfigureTools(tool_picker) => tool_picker.clone().into_any_element(),
|
||||
})
|
||||
}
|
||||
}
|
||||
194
crates/assistant2/src/assistant_configuration/profile_picker.rs
Normal file
194
crates/assistant2/src/assistant_configuration/profile_picker.rs
Normal file
@@ -0,0 +1,194 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::AssistantSettings;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{
|
||||
App, Context, DismissEvent, Entity, EventEmitter, Focusable, SharedString, Task, WeakEntity,
|
||||
Window,
|
||||
};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::Settings;
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct ProfilePicker {
|
||||
picker: Entity<Picker<ProfilePickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ProfilePicker {
|
||||
pub fn new(
|
||||
delegate: ProfilePickerDelegate,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ProfilePicker {}
|
||||
|
||||
impl Focusable for ProfilePicker {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProfilePicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ProfileEntry {
|
||||
pub id: Arc<str>,
|
||||
pub name: SharedString,
|
||||
}
|
||||
|
||||
pub struct ProfilePickerDelegate {
|
||||
profile_picker: WeakEntity<ProfilePicker>,
|
||||
profiles: Vec<ProfileEntry>,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
on_confirm: Arc<dyn Fn(&Arc<str>, &mut Window, &mut App) + 'static>,
|
||||
}
|
||||
|
||||
impl ProfilePickerDelegate {
|
||||
pub fn new(
|
||||
on_confirm: impl Fn(&Arc<str>, &mut Window, &mut App) + 'static,
|
||||
cx: &mut Context<ProfilePicker>,
|
||||
) -> Self {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
let profiles = settings
|
||||
.profiles
|
||||
.iter()
|
||||
.map(|(id, profile)| ProfileEntry {
|
||||
id: id.clone(),
|
||||
name: profile.name.clone(),
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
Self {
|
||||
profile_picker: cx.entity().downgrade(),
|
||||
profiles,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
on_confirm: Arc::new(on_confirm),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ProfilePickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search profiles…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let background = cx.background_executor().clone();
|
||||
let candidates = self
|
||||
.profiles
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.selected_index
|
||||
.min(this.delegate.matches.len().saturating_sub(1));
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.matches.is_empty() {
|
||||
self.dismissed(window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let candidate_id = self.matches[self.selected_index].candidate_id;
|
||||
let profile = &self.profiles[candidate_id];
|
||||
|
||||
(self.on_confirm)(&profile.id, window, cx);
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.profile_picker
|
||||
.update(cx, |_this, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let profile_match = &self.matches[ix];
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(HighlightedLabel::new(
|
||||
profile_match.string.clone(),
|
||||
profile_match.positions.clone(),
|
||||
)),
|
||||
)
|
||||
}
|
||||
}
|
||||
267
crates/assistant2/src/assistant_configuration/tool_picker.rs
Normal file
267
crates/assistant2/src/assistant_configuration/tool_picker.rs
Normal file
@@ -0,0 +1,267 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{
|
||||
AgentProfile, AssistantSettings, AssistantSettingsContent, VersionedAssistantSettingsContent,
|
||||
};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use fs::Fs;
|
||||
use fuzzy::{match_strings, StringMatch, StringMatchCandidate};
|
||||
use gpui::{App, Context, DismissEvent, Entity, EventEmitter, Focusable, Task, WeakEntity, Window};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::update_settings_file;
|
||||
use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing};
|
||||
use util::ResultExt as _;
|
||||
|
||||
pub struct ToolPicker {
|
||||
picker: Entity<Picker<ToolPickerDelegate>>,
|
||||
}
|
||||
|
||||
impl ToolPicker {
|
||||
pub fn new(delegate: ToolPickerDelegate, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let picker = cx.new(|cx| Picker::uniform_list(delegate, window, cx));
|
||||
Self { picker }
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for ToolPicker {}
|
||||
|
||||
impl Focusable for ToolPicker {
|
||||
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
|
||||
self.picker.focus_handle(cx)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ToolPicker {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex().w(rems(34.)).child(self.picker.clone())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ToolEntry {
|
||||
pub name: Arc<str>,
|
||||
pub source: ToolSource,
|
||||
}
|
||||
|
||||
pub struct ToolPickerDelegate {
|
||||
tool_picker: WeakEntity<ToolPicker>,
|
||||
fs: Arc<dyn Fs>,
|
||||
tools: Vec<ToolEntry>,
|
||||
profile_id: Arc<str>,
|
||||
profile: AgentProfile,
|
||||
matches: Vec<StringMatch>,
|
||||
selected_index: usize,
|
||||
}
|
||||
|
||||
impl ToolPickerDelegate {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
tool_set: Arc<ToolWorkingSet>,
|
||||
profile_id: Arc<str>,
|
||||
profile: AgentProfile,
|
||||
cx: &mut Context<ToolPicker>,
|
||||
) -> Self {
|
||||
let mut tool_entries = Vec::new();
|
||||
|
||||
for (source, tools) in tool_set.tools_by_source(cx) {
|
||||
tool_entries.extend(tools.into_iter().map(|tool| ToolEntry {
|
||||
name: tool.name().into(),
|
||||
source: source.clone(),
|
||||
}));
|
||||
}
|
||||
|
||||
Self {
|
||||
tool_picker: cx.entity().downgrade(),
|
||||
fs,
|
||||
tools: tool_entries,
|
||||
profile_id,
|
||||
profile,
|
||||
matches: Vec::new(),
|
||||
selected_index: 0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ToolPickerDelegate {
|
||||
type ListItem = ListItem;
|
||||
|
||||
fn match_count(&self) -> usize {
|
||||
self.matches.len()
|
||||
}
|
||||
|
||||
fn selected_index(&self) -> usize {
|
||||
self.selected_index
|
||||
}
|
||||
|
||||
fn set_selected_index(
|
||||
&mut self,
|
||||
ix: usize,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) {
|
||||
self.selected_index = ix;
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Search tools…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let background = cx.background_executor().clone();
|
||||
let candidates = self
|
||||
.tools
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(id, profile)| StringMatchCandidate::new(id, profile.name.as_ref()))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
false,
|
||||
100,
|
||||
&Default::default(),
|
||||
background,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
this.update(cx, |this, _cx| {
|
||||
this.delegate.matches = matches;
|
||||
this.delegate.selected_index = this
|
||||
.delegate
|
||||
.selected_index
|
||||
.min(this.delegate.matches.len().saturating_sub(1));
|
||||
})
|
||||
.log_err();
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
if self.matches.is_empty() {
|
||||
self.dismissed(window, cx);
|
||||
return;
|
||||
}
|
||||
|
||||
let candidate_id = self.matches[self.selected_index].candidate_id;
|
||||
let tool = &self.tools[candidate_id];
|
||||
|
||||
let is_enabled = match &tool.source {
|
||||
ToolSource::Native => {
|
||||
let is_enabled = self.profile.tools.entry(tool.name.clone()).or_default();
|
||||
*is_enabled = !*is_enabled;
|
||||
*is_enabled
|
||||
}
|
||||
ToolSource::ContextServer { id } => {
|
||||
let preset = self
|
||||
.profile
|
||||
.context_servers
|
||||
.entry(id.clone().into())
|
||||
.or_default();
|
||||
let is_enabled = preset.tools.entry(tool.name.clone()).or_default();
|
||||
*is_enabled = !*is_enabled;
|
||||
*is_enabled
|
||||
}
|
||||
};
|
||||
|
||||
update_settings_file::<AssistantSettings>(self.fs.clone(), cx, {
|
||||
let profile_id = self.profile_id.clone();
|
||||
let tool = tool.clone();
|
||||
move |settings, _cx| match settings {
|
||||
AssistantSettingsContent::Versioned(VersionedAssistantSettingsContent::V2(
|
||||
settings,
|
||||
)) => {
|
||||
if let Some(profiles) = &mut settings.profiles {
|
||||
if let Some(profile) = profiles.get_mut(&profile_id) {
|
||||
match tool.source {
|
||||
ToolSource::Native => {
|
||||
*profile.tools.entry(tool.name).or_default() = is_enabled;
|
||||
}
|
||||
ToolSource::ContextServer { id } => {
|
||||
let preset = profile
|
||||
.context_servers
|
||||
.entry(id.clone().into())
|
||||
.or_default();
|
||||
*preset.tools.entry(tool.name.clone()).or_default() =
|
||||
is_enabled;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
self.tool_picker
|
||||
.update(cx, |_this, cx| cx.emit(DismissEvent))
|
||||
.log_err();
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let tool_match = &self.matches[ix];
|
||||
let tool = &self.tools[tool_match.candidate_id];
|
||||
|
||||
let is_enabled = match &tool.source {
|
||||
ToolSource::Native => self.profile.tools.get(&tool.name).copied().unwrap_or(false),
|
||||
ToolSource::ContextServer { id } => self
|
||||
.profile
|
||||
.context_servers
|
||||
.get(id.as_ref())
|
||||
.and_then(|preset| preset.tools.get(&tool.name))
|
||||
.copied()
|
||||
.unwrap_or(false),
|
||||
};
|
||||
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.child(HighlightedLabel::new(
|
||||
tool_match.string.clone(),
|
||||
tool_match.positions.clone(),
|
||||
))
|
||||
.map(|parent| match &tool.source {
|
||||
ToolSource::Native => parent,
|
||||
ToolSource::ContextServer { id } => parent
|
||||
.child(Label::new(id).size(LabelSize::XSmall).color(Color::Muted)),
|
||||
}),
|
||||
)
|
||||
.end_slot::<Icon>(is_enabled.then(|| {
|
||||
Icon::new(IconName::Check)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Success)
|
||||
})),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -482,11 +482,17 @@ impl CodegenAlternative {
|
||||
|
||||
self.generation = cx.spawn(async move |codegen, cx| {
|
||||
let stream = stream.await;
|
||||
let token_usage = stream
|
||||
.as_ref()
|
||||
.ok()
|
||||
.map(|stream| stream.last_token_usage.clone());
|
||||
let message_id = stream
|
||||
.as_ref()
|
||||
.ok()
|
||||
.and_then(|stream| stream.message_id.clone());
|
||||
let generate = async {
|
||||
let model_telemetry_id = model_telemetry_id.clone();
|
||||
let model_provider_id = model_provider_id.clone();
|
||||
let (mut diff_tx, mut diff_rx) = mpsc::channel(1);
|
||||
let executor = cx.background_executor().clone();
|
||||
let message_id = message_id.clone();
|
||||
@@ -596,7 +602,7 @@ impl CodegenAlternative {
|
||||
kind: AssistantKind::Inline,
|
||||
phase: AssistantPhase::Response,
|
||||
model: model_telemetry_id,
|
||||
model_provider: model_provider_id.to_string(),
|
||||
model_provider: model_provider_id,
|
||||
response_latency,
|
||||
error_message,
|
||||
language_name: language_name.map(|name| name.to_proto()),
|
||||
@@ -677,6 +683,16 @@ impl CodegenAlternative {
|
||||
}
|
||||
this.elapsed_time = Some(elapsed_time);
|
||||
this.completion = Some(completion.lock().clone());
|
||||
if let Some(usage) = token_usage {
|
||||
let usage = usage.lock();
|
||||
telemetry::event!(
|
||||
"Inline Assistant Completion",
|
||||
model = model_telemetry_id,
|
||||
model_provider = model_provider_id,
|
||||
input_tokens = usage.input_tokens,
|
||||
output_tokens = usage.output_tokens,
|
||||
)
|
||||
}
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
@@ -1021,7 +1037,7 @@ mod tests {
|
||||
language_settings, tree_sitter_rust, Buffer, Language, LanguageConfig, LanguageMatcher,
|
||||
Point,
|
||||
};
|
||||
use language_model::LanguageModelRegistry;
|
||||
use language_model::{LanguageModelRegistry, TokenUsage};
|
||||
use rand::prelude::*;
|
||||
use serde::Serialize;
|
||||
use settings::SettingsStore;
|
||||
@@ -1405,6 +1421,7 @@ mod tests {
|
||||
future::ready(Ok(LanguageModelTextStream {
|
||||
message_id: None,
|
||||
stream: chunks_rx.map(Ok).boxed(),
|
||||
last_token_usage: Arc::new(Mutex::new(TokenUsage::default())),
|
||||
})),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -544,6 +544,10 @@ impl CompletionProvider for ContextPickerCompletionProvider {
|
||||
fn sort_completions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn filter_completions(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
fn confirm_completion_callback(
|
||||
|
||||
@@ -282,7 +282,10 @@ pub fn render_file_context_entry(
|
||||
cx: &App,
|
||||
) -> Stateful<Div> {
|
||||
let (file_name, directory) = if path == Path::new("") {
|
||||
(SharedString::from(path_prefix.clone()), None)
|
||||
(
|
||||
SharedString::from(path_prefix.trim_end_matches('/').to_string()),
|
||||
None,
|
||||
)
|
||||
} else {
|
||||
let file_name = path
|
||||
.file_name()
|
||||
@@ -291,8 +294,10 @@ pub fn render_file_context_entry(
|
||||
.to_string()
|
||||
.into();
|
||||
|
||||
let mut directory = format!("{}/", path_prefix);
|
||||
|
||||
let mut directory = path_prefix.to_string();
|
||||
if !directory.ends_with('/') {
|
||||
directory.push('/');
|
||||
}
|
||||
if let Some(parent) = path.parent().filter(|parent| parent != &Path::new("")) {
|
||||
directory.push_str(&parent.to_string_lossy());
|
||||
directory.push('/');
|
||||
|
||||
@@ -26,9 +26,9 @@ use crate::assistant_model_selector::AssistantModelSelector;
|
||||
use crate::context_picker::{ConfirmBehavior, ContextPicker, ContextPickerCompletionProvider};
|
||||
use crate::context_store::{refresh_context_store_text, ContextStore};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::thread::{RequestKind, Thread};
|
||||
use crate::thread_store::ThreadStore;
|
||||
use crate::tool_selector::ToolSelector;
|
||||
use crate::{Chat, ChatMode, RemoveAllContext, ThreadEvent, ToggleContextPicker};
|
||||
|
||||
pub struct MessageEditor {
|
||||
@@ -43,7 +43,7 @@ pub struct MessageEditor {
|
||||
inline_context_picker: Entity<ContextPicker>,
|
||||
inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
|
||||
model_selector: Entity<AssistantModelSelector>,
|
||||
tool_selector: Entity<ToolSelector>,
|
||||
profile_selector: Entity<ProfileSelector>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
@@ -57,7 +57,6 @@ impl MessageEditor {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let tools = thread.read(cx).tools().clone();
|
||||
let context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let inline_context_picker_menu_handle = PopoverMenuHandle::default();
|
||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||
@@ -129,14 +128,14 @@ impl MessageEditor {
|
||||
inline_context_picker_menu_handle,
|
||||
model_selector: cx.new(|cx| {
|
||||
AssistantModelSelector::new(
|
||||
fs,
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}),
|
||||
tool_selector: cx.new(|cx| ToolSelector::new(tools, cx)),
|
||||
profile_selector: cx.new(|cx| ProfileSelector::new(fs, thread_store, cx)),
|
||||
_subscriptions: subscriptions,
|
||||
}
|
||||
}
|
||||
@@ -318,7 +317,7 @@ impl Render for MessageEditor {
|
||||
|
||||
let project = self.thread.read(cx).project();
|
||||
let changed_files = if let Some(repository) = project.read(cx).active_repository(cx) {
|
||||
repository.read(cx).status().count()
|
||||
repository.read(cx).cached_status().count()
|
||||
} else {
|
||||
0
|
||||
};
|
||||
@@ -624,7 +623,7 @@ impl Render for MessageEditor {
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_between()
|
||||
.child(h_flex().gap_2().child(self.tool_selector.clone()))
|
||||
.child(h_flex().gap_2().child(self.profile_selector.clone()))
|
||||
.child(
|
||||
h_flex().gap_1().child(self.model_selector.clone()).child(
|
||||
ButtonLike::new("submit-message")
|
||||
|
||||
121
crates/assistant2/src/profile_selector.rs
Normal file
121
crates/assistant2/src/profile_selector.rs
Normal file
@@ -0,0 +1,121 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use fs::Fs;
|
||||
use gpui::{prelude::*, Action, Entity, Subscription, WeakEntity};
|
||||
use indexmap::IndexMap;
|
||||
use settings::{update_settings_file, Settings as _, SettingsStore};
|
||||
use ui::{prelude::*, ContextMenu, ContextMenuEntry, PopoverMenu, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::{ManageProfiles, ThreadStore};
|
||||
|
||||
pub struct ProfileSelector {
|
||||
profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ProfileSelector {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.refresh_profiles(cx);
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
profiles: IndexMap::default(),
|
||||
fs,
|
||||
thread_store,
|
||||
_subscriptions: vec![settings_subscription],
|
||||
};
|
||||
this.refresh_profiles(cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.profiles = settings.profiles.clone();
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
ContextMenu::build(window, cx, |mut menu, _window, cx| {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let icon_position = IconPosition::Start;
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (profile_id, profile) in self.profiles.clone() {
|
||||
menu = menu.toggleable_entry(
|
||||
profile.name.clone(),
|
||||
profile_id == settings.default_profile,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
let thread_store = self.thread_store.clone();
|
||||
move |_window, cx| {
|
||||
update_settings_file::<AssistantSettings>(fs.clone(), cx, {
|
||||
let profile_id = profile_id.clone();
|
||||
move |settings, _cx| {
|
||||
settings.set_profile(profile_id.clone());
|
||||
}
|
||||
});
|
||||
|
||||
thread_store
|
||||
.update(cx, |this, cx| {
|
||||
this.load_profile_by_id(&profile_id, cx);
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new("Configure Profiles")
|
||||
.icon(IconName::Pencil)
|
||||
.icon_color(Color::Muted)
|
||||
.handler(move |window, cx| {
|
||||
window.dispatch_action(ManageProfiles.boxed_clone(), cx);
|
||||
}),
|
||||
);
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ProfileSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
let profile = settings
|
||||
.profiles
|
||||
.get(&settings.default_profile)
|
||||
.map(|profile| profile.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let this = cx.entity().clone();
|
||||
PopoverMenu::new("tool-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
Button::new("profile-selector-button", profile)
|
||||
.style(ButtonStyle::Filled)
|
||||
.label_size(LabelSize::Small),
|
||||
Tooltip::text("Change Profile"),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
}
|
||||
}
|
||||
@@ -34,6 +34,7 @@ use crate::thread_store::{
|
||||
SerializedToolUse,
|
||||
};
|
||||
use crate::tool_use::{PendingToolUse, ToolUse, ToolUseState};
|
||||
use crate::{ChangeAuthor, ThreadDiff};
|
||||
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub enum RequestKind {
|
||||
@@ -190,6 +191,7 @@ pub struct Thread {
|
||||
initial_project_snapshot: Shared<Task<Option<Arc<ProjectSnapshot>>>>,
|
||||
cumulative_token_usage: TokenUsage,
|
||||
feedback: Option<ThreadFeedback>,
|
||||
diff: Entity<ThreadDiff>,
|
||||
}
|
||||
|
||||
impl Thread {
|
||||
@@ -220,13 +222,14 @@ impl Thread {
|
||||
tool_use: ToolUseState::new(tools.clone()),
|
||||
action_log: cx.new(|_| ActionLog::new()),
|
||||
initial_project_snapshot: {
|
||||
let project_snapshot = Self::project_snapshot(project, cx);
|
||||
let project_snapshot = Self::project_snapshot(project.clone(), cx);
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { Some(project_snapshot.await) })
|
||||
.shared()
|
||||
},
|
||||
cumulative_token_usage: TokenUsage::default(),
|
||||
feedback: None,
|
||||
diff: cx.new(|cx| ThreadDiff::new(project.clone(), cx)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,6 +283,7 @@ impl Thread {
|
||||
pending_completions: Vec::new(),
|
||||
last_restore_checkpoint: None,
|
||||
pending_checkpoint: None,
|
||||
diff: cx.new(|cx| ThreadDiff::new(project.clone(), cx)),
|
||||
project,
|
||||
prompt_builder,
|
||||
tools,
|
||||
@@ -873,17 +877,23 @@ impl Thread {
|
||||
request.messages.push(context_message);
|
||||
}
|
||||
|
||||
self.attach_stale_files(&mut request.messages, cx);
|
||||
self.attached_tracked_files_state(&mut request.messages, cx);
|
||||
|
||||
request
|
||||
}
|
||||
|
||||
fn attach_stale_files(&self, messages: &mut Vec<LanguageModelRequestMessage>, cx: &App) {
|
||||
fn attached_tracked_files_state(
|
||||
&self,
|
||||
messages: &mut Vec<LanguageModelRequestMessage>,
|
||||
cx: &App,
|
||||
) {
|
||||
const STALE_FILES_HEADER: &str = "These files changed since last read:";
|
||||
|
||||
let mut stale_message = String::new();
|
||||
|
||||
for stale_file in self.action_log.read(cx).stale_buffers(cx) {
|
||||
let action_log = self.action_log.read(cx);
|
||||
|
||||
for stale_file in action_log.stale_buffers(cx) {
|
||||
let Some(file) = stale_file.read(cx).file() else {
|
||||
continue;
|
||||
};
|
||||
@@ -895,10 +905,22 @@ impl Thread {
|
||||
writeln!(&mut stale_message, "- {}", file.path().display()).ok();
|
||||
}
|
||||
|
||||
let mut content = Vec::with_capacity(2);
|
||||
|
||||
if !stale_message.is_empty() {
|
||||
content.push(stale_message.into());
|
||||
}
|
||||
|
||||
if action_log.has_edited_files_since_project_diagnostics_check() {
|
||||
content.push(
|
||||
"When you're done making changes, make sure to check project diagnostics and fix all errors AND warnings you introduced!".into(),
|
||||
);
|
||||
}
|
||||
|
||||
if !content.is_empty() {
|
||||
let context_message = LanguageModelRequestMessage {
|
||||
role: Role::User,
|
||||
content: vec![stale_message.into()],
|
||||
content,
|
||||
cache: false,
|
||||
};
|
||||
|
||||
@@ -1213,6 +1235,9 @@ impl Thread {
|
||||
tool: Arc<dyn Tool>,
|
||||
cx: &mut Context<Thread>,
|
||||
) -> Task<()> {
|
||||
self.diff
|
||||
.update(cx, |diff, cx| diff.compute_changes(ChangeAuthor::User, cx));
|
||||
|
||||
let run_tool = tool.run(
|
||||
input,
|
||||
messages,
|
||||
@@ -1227,6 +1252,10 @@ impl Thread {
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread
|
||||
.diff
|
||||
.update(cx, |diff, cx| diff.compute_changes(ChangeAuthor::Agent, cx));
|
||||
|
||||
let pending_tool_use = thread
|
||||
.tool_use
|
||||
.insert_tool_output(tool_use_id.clone(), output);
|
||||
@@ -1392,7 +1421,7 @@ impl Thread {
|
||||
git_store
|
||||
.repositories()
|
||||
.values()
|
||||
.find(|repo| repo.read(cx).worktree_id == snapshot.id())
|
||||
.find(|repo| repo.read(cx).worktree_id == Some(snapshot.id()))
|
||||
.and_then(|repo| {
|
||||
let repo = repo.read(cx);
|
||||
Some((repo.branch().cloned(), repo.local_repository()?))
|
||||
@@ -1411,7 +1440,7 @@ impl Thread {
|
||||
|
||||
// Get diff asynchronously
|
||||
let diff = repo
|
||||
.diff(git::repository::DiffType::HeadToWorktree, cx.clone())
|
||||
.diff(git::repository::DiffType::HeadToWorktree)
|
||||
.await
|
||||
.ok();
|
||||
|
||||
@@ -1544,6 +1573,7 @@ pub enum ThreadEvent {
|
||||
canceled: bool,
|
||||
},
|
||||
CheckpointChanged,
|
||||
DiffChanged,
|
||||
ToolConfirmationNeeded,
|
||||
}
|
||||
|
||||
|
||||
125
crates/assistant2/src/thread_diff.rs
Normal file
125
crates/assistant2/src/thread_diff.rs
Normal file
@@ -0,0 +1,125 @@
|
||||
use anyhow::Result;
|
||||
use buffer_diff::BufferDiff;
|
||||
use collections::HashMap;
|
||||
use futures::{future::Shared, FutureExt};
|
||||
use gpui::{prelude::*, App, Entity, Task};
|
||||
use language::Buffer;
|
||||
use project::{
|
||||
git_store::{GitStore, GitStoreCheckpoint, GitStoreIndex, GitStoreStatus},
|
||||
Project,
|
||||
};
|
||||
use util::TryFutureExt;
|
||||
|
||||
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
|
||||
pub enum ChangeAuthor {
|
||||
User,
|
||||
Agent,
|
||||
}
|
||||
|
||||
pub struct ThreadDiff {
|
||||
base: Shared<Task<Option<GitStoreIndex>>>,
|
||||
diffs_by_buffer: HashMap<Entity<Buffer>, Entity<BufferDiff>>,
|
||||
last_checkpoint: Option<Task<Result<GitStoreCheckpoint>>>,
|
||||
project: Entity<Project>,
|
||||
git_store: Entity<GitStore>,
|
||||
}
|
||||
|
||||
impl ThreadDiff {
|
||||
pub fn new(project: Entity<Project>, cx: &mut Context<Self>) -> Self {
|
||||
let mut this = Self {
|
||||
base: cx
|
||||
.background_spawn(
|
||||
project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.create_index(cx)
|
||||
.log_err(),
|
||||
)
|
||||
.shared(),
|
||||
diffs_by_buffer: HashMap::default(),
|
||||
last_checkpoint: None,
|
||||
git_store: project.read(cx).git_store().clone(),
|
||||
project,
|
||||
};
|
||||
this.compute_changes(ChangeAuthor::User, cx);
|
||||
this
|
||||
}
|
||||
|
||||
pub fn compute_changes(&mut self, author: ChangeAuthor, cx: &mut Context<Self>) {
|
||||
let last_checkpoint = self.last_checkpoint.take();
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
let checkpoint = git_store.read(cx).checkpoint(cx);
|
||||
let base = self.base.clone();
|
||||
self.last_checkpoint = Some(cx.spawn(async move |this, cx| {
|
||||
let checkpoint = checkpoint.await?;
|
||||
|
||||
if let Some(base) = base.await {
|
||||
if let Some(last_checkpoint) = last_checkpoint {
|
||||
if let Ok(last_checkpoint) = last_checkpoint.await {
|
||||
if author == ChangeAuthor::User {
|
||||
let diff = git_store
|
||||
.read_with(cx, |store, cx| {
|
||||
store.diff_checkpoints(last_checkpoint, checkpoint.clone(), cx)
|
||||
})?
|
||||
.await;
|
||||
|
||||
if let Ok(diff) = diff {
|
||||
_ = git_store
|
||||
.read_with(cx, |store, cx| {
|
||||
store.apply_diff(base.clone(), diff, cx)
|
||||
})?
|
||||
.await;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let status = git_store
|
||||
.read_with(cx, |store, cx| store.status(Some(base), cx))?
|
||||
.await
|
||||
.unwrap_or_default();
|
||||
this.update(cx, |this, cx| this.set_status(status, cx))?;
|
||||
}
|
||||
|
||||
Ok(checkpoint)
|
||||
}));
|
||||
}
|
||||
|
||||
pub fn set_status(&mut self, status: GitStoreStatus, cx: &mut Context<Self>) {}
|
||||
}
|
||||
|
||||
struct ThreadDiffSource {
|
||||
thread_diff: Entity<ThreadDiff>,
|
||||
git_store: Entity<GitStore>,
|
||||
}
|
||||
|
||||
impl git_ui::project_diff::DiffSource for ThreadDiff {
|
||||
fn status(&self, cx: &App) -> Vec<(project::ProjectPath, git::status::FileStatus, bool)> {
|
||||
let mut results = Vec::new();
|
||||
|
||||
todo!();
|
||||
// for (repo, repo_path, change) in self.changes.iter(&self.git_store, cx) {
|
||||
// let Some(project_path) = repo.read(cx).repo_path_to_project_path(repo_path) else {
|
||||
// continue;
|
||||
// };
|
||||
|
||||
// results.push((
|
||||
// project_path,
|
||||
// // todo!("compute the correct status")
|
||||
// git::status::FileStatus::worktree(git::status::StatusCode::Modified),
|
||||
// false,
|
||||
// ))
|
||||
// }
|
||||
|
||||
results
|
||||
}
|
||||
|
||||
fn open_uncommitted_diff(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
todo!()
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,8 @@ use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::{ToolId, ToolWorkingSet};
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use assistant_tool::{ToolId, ToolSource, ToolWorkingSet};
|
||||
use chrono::{DateTime, Utc};
|
||||
use collections::HashMap;
|
||||
use context_server::manager::ContextServerManager;
|
||||
@@ -19,6 +20,7 @@ use language_model::{LanguageModelToolUseId, Role};
|
||||
use project::Project;
|
||||
use prompt_store::PromptBuilder;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings as _;
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::thread::{MessageId, ProjectSnapshot, Thread, ThreadEvent, ThreadId};
|
||||
@@ -57,6 +59,7 @@ impl ThreadStore {
|
||||
context_server_tool_ids: HashMap::default(),
|
||||
threads: Vec::new(),
|
||||
};
|
||||
this.load_default_profile(cx);
|
||||
this.register_context_server_handlers(cx);
|
||||
this.reload(cx).detach_and_log_err(cx);
|
||||
|
||||
@@ -184,6 +187,45 @@ impl ThreadStore {
|
||||
})
|
||||
}
|
||||
|
||||
fn load_default_profile(&self, cx: &Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.load_profile_by_id(&assistant_settings.default_profile, cx);
|
||||
}
|
||||
|
||||
pub fn load_profile_by_id(&self, profile_id: &Arc<str>, cx: &Context<Self>) {
|
||||
let assistant_settings = AssistantSettings::get_global(cx);
|
||||
|
||||
if let Some(profile) = assistant_settings.profiles.get(profile_id) {
|
||||
self.load_profile(profile);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn load_profile(&self, profile: &AgentProfile) {
|
||||
self.tools.disable_all_tools();
|
||||
self.tools.enable(
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
self.tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn register_context_server_handlers(&self, cx: &mut Context<Self>) {
|
||||
cx.subscribe(
|
||||
&self.context_server_manager.clone(),
|
||||
|
||||
@@ -1,172 +0,0 @@
|
||||
use std::sync::Arc;
|
||||
|
||||
use assistant_settings::{AgentProfile, AssistantSettings};
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
use gpui::{Entity, Subscription};
|
||||
use indexmap::IndexMap;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
|
||||
|
||||
pub struct ToolSelector {
|
||||
profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
tools: Arc<ToolWorkingSet>,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
impl ToolSelector {
|
||||
pub fn new(tools: Arc<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
|
||||
let settings_subscription = cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
this.refresh_profiles(cx);
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
profiles: IndexMap::default(),
|
||||
tools,
|
||||
_subscriptions: vec![settings_subscription],
|
||||
};
|
||||
this.refresh_profiles(cx);
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
fn refresh_profiles(&mut self, cx: &mut Context<Self>) {
|
||||
let settings = AssistantSettings::get_global(cx);
|
||||
|
||||
self.profiles = settings.profiles.clone();
|
||||
}
|
||||
|
||||
fn build_context_menu(
|
||||
&self,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ContextMenu> {
|
||||
let profiles = self.profiles.clone();
|
||||
let tool_set = self.tools.clone();
|
||||
ContextMenu::build_persistent(window, cx, move |mut menu, _window, cx| {
|
||||
let icon_position = IconPosition::End;
|
||||
|
||||
menu = menu.header("Profiles");
|
||||
for (_id, profile) in profiles.clone() {
|
||||
menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
|
||||
let tools = tool_set.clone();
|
||||
move |_window, cx| {
|
||||
tools.disable_all_tools(cx);
|
||||
|
||||
tools.enable(
|
||||
ToolSource::Native,
|
||||
&profile
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
);
|
||||
|
||||
for (context_server_id, preset) in &profile.context_servers {
|
||||
tools.enable(
|
||||
ToolSource::ContextServer {
|
||||
id: context_server_id.clone().into(),
|
||||
},
|
||||
&preset
|
||||
.tools
|
||||
.iter()
|
||||
.filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
|
||||
.collect::<Vec<_>>(),
|
||||
)
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
menu = menu.separator();
|
||||
|
||||
let tools_by_source = tool_set.tools_by_source(cx);
|
||||
|
||||
let all_tools_enabled = tool_set.are_all_tools_enabled();
|
||||
menu = menu.toggleable_entry("All Tools", all_tools_enabled, icon_position, None, {
|
||||
let tools = tool_set.clone();
|
||||
move |_window, cx| {
|
||||
if all_tools_enabled {
|
||||
tools.disable_all_tools(cx);
|
||||
} else {
|
||||
tools.enable_all_tools();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
for (source, tools) in tools_by_source {
|
||||
let mut tools = tools
|
||||
.into_iter()
|
||||
.map(|tool| {
|
||||
let source = tool.source();
|
||||
let name = tool.name().into();
|
||||
let is_enabled = tool_set.is_enabled(&source, &name);
|
||||
|
||||
(source, name, is_enabled)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
if ToolSource::Native == source {
|
||||
tools.sort_by(|(_, name_a, _), (_, name_b, _)| name_a.cmp(name_b));
|
||||
}
|
||||
|
||||
menu = match &source {
|
||||
ToolSource::Native => menu.separator().header("Zed Tools"),
|
||||
ToolSource::ContextServer { id } => {
|
||||
let all_tools_from_source_enabled =
|
||||
tool_set.are_all_tools_from_source_enabled(&source);
|
||||
|
||||
menu.separator().header(id).toggleable_entry(
|
||||
"All Tools",
|
||||
all_tools_from_source_enabled,
|
||||
icon_position,
|
||||
None,
|
||||
{
|
||||
let tools = tool_set.clone();
|
||||
let source = source.clone();
|
||||
move |_window, cx| {
|
||||
if all_tools_from_source_enabled {
|
||||
tools.disable_source(source.clone(), cx);
|
||||
} else {
|
||||
tools.enable_source(&source);
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
};
|
||||
|
||||
for (source, name, is_enabled) in tools {
|
||||
menu = menu.toggleable_entry(name.clone(), is_enabled, icon_position, None, {
|
||||
let tools = tool_set.clone();
|
||||
move |_window, _cx| {
|
||||
if is_enabled {
|
||||
tools.disable(source.clone(), &[name.clone()]);
|
||||
} else {
|
||||
tools.enable(source.clone(), &[name.clone()]);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
menu
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ToolSelector {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<'_, Self>) -> impl IntoElement {
|
||||
let this = cx.entity().clone();
|
||||
PopoverMenu::new("tool-selector")
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
})
|
||||
.trigger_with_tooltip(
|
||||
IconButton::new("tool-selector-button", IconName::SettingsAlt)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted),
|
||||
Tooltip::text("Customize Tools"),
|
||||
)
|
||||
.anchor(gpui::Corner::BottomLeft)
|
||||
}
|
||||
}
|
||||
@@ -9,12 +9,16 @@ use ui::{prelude::*, Render};
|
||||
|
||||
pub struct ToolReadyPopUp {
|
||||
caption: SharedString,
|
||||
icon: IconName,
|
||||
icon_color: Color,
|
||||
}
|
||||
|
||||
impl ToolReadyPopUp {
|
||||
pub fn new(caption: impl Into<SharedString>) -> Self {
|
||||
pub fn new(caption: impl Into<SharedString>, icon: IconName, icon_color: Color) -> Self {
|
||||
Self {
|
||||
caption: caption.into(),
|
||||
icon,
|
||||
icon_color,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -82,9 +86,9 @@ impl Render for ToolReadyPopUp {
|
||||
.gap_2()
|
||||
.child(
|
||||
h_flex().h(line_height).justify_center().child(
|
||||
Icon::new(IconName::Info)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted),
|
||||
Icon::new(self.icon)
|
||||
.color(self.icon_color)
|
||||
.size(IconSize::Small),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
|
||||
@@ -149,7 +149,10 @@ pub fn init(cx: &mut App) -> Arc<HeadlessAppState> {
|
||||
cx.set_http_client(client.http_client().clone());
|
||||
|
||||
let git_binary_path = None;
|
||||
let fs = Arc::new(RealFs::new(git_binary_path));
|
||||
let fs = Arc::new(RealFs::new(
|
||||
git_binary_path,
|
||||
cx.background_executor().clone(),
|
||||
));
|
||||
|
||||
let languages = Arc::new(LanguageRegistry::new(cx.background_executor().clone()));
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@ pub struct AgentProfile {
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPreset>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[derive(Debug, Clone, Default)]
|
||||
pub struct ContextServerPreset {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
}
|
||||
|
||||
@@ -71,6 +71,7 @@ pub struct AssistantSettings {
|
||||
pub inline_alternatives: Vec<LanguageModelSelection>,
|
||||
pub using_outdated_settings_version: bool,
|
||||
pub enable_experimental_live_diffs: bool,
|
||||
pub default_profile: Arc<str>,
|
||||
pub profiles: IndexMap<Arc<str>, AgentProfile>,
|
||||
pub always_allow_tool_actions: bool,
|
||||
pub notify_when_agent_waiting: bool,
|
||||
@@ -174,6 +175,7 @@ impl AssistantSettingsContent {
|
||||
editor_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -198,6 +200,7 @@ impl AssistantSettingsContent {
|
||||
editor_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -307,6 +310,18 @@ impl AssistantSettingsContent {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_profile(&mut self, profile_id: Arc<str>) {
|
||||
match self {
|
||||
AssistantSettingsContent::Versioned(settings) => match settings {
|
||||
VersionedAssistantSettingsContent::V2(settings) => {
|
||||
settings.default_profile = Some(profile_id);
|
||||
}
|
||||
VersionedAssistantSettingsContent::V1(_) => {}
|
||||
},
|
||||
AssistantSettingsContent::Legacy(_) => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
@@ -330,6 +345,7 @@ impl Default for VersionedAssistantSettingsContent {
|
||||
editor_model: None,
|
||||
inline_alternatives: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
@@ -370,7 +386,9 @@ pub struct AssistantSettingsContentV2 {
|
||||
/// Default: false
|
||||
enable_experimental_live_diffs: Option<bool>,
|
||||
#[schemars(skip)]
|
||||
profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
|
||||
default_profile: Option<Arc<str>>,
|
||||
#[schemars(skip)]
|
||||
pub profiles: Option<IndexMap<Arc<str>, AgentProfileContent>>,
|
||||
/// Whenever a tool action would normally wait for your confirmation
|
||||
/// that you allow it, always choose to allow it.
|
||||
///
|
||||
@@ -424,7 +442,7 @@ pub struct AgentProfileContent {
|
||||
pub context_servers: IndexMap<Arc<str>, ContextServerPresetContent>,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
#[derive(Debug, PartialEq, Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ContextServerPresetContent {
|
||||
pub tools: IndexMap<Arc<str>, bool>,
|
||||
}
|
||||
@@ -531,6 +549,7 @@ impl Settings for AssistantSettings {
|
||||
&mut settings.notify_when_agent_waiting,
|
||||
value.notify_when_agent_waiting,
|
||||
);
|
||||
merge(&mut settings.default_profile, value.default_profile);
|
||||
|
||||
if let Some(profiles) = value.profiles {
|
||||
settings
|
||||
@@ -621,6 +640,7 @@ mod tests {
|
||||
default_width: None,
|
||||
default_height: None,
|
||||
enable_experimental_live_diffs: None,
|
||||
default_profile: None,
|
||||
profiles: None,
|
||||
always_allow_tool_actions: None,
|
||||
notify_when_agent_waiting: None,
|
||||
|
||||
@@ -80,6 +80,8 @@ pub struct ActionLog {
|
||||
stale_buffers_in_context: HashSet<Entity<Buffer>>,
|
||||
/// Buffers that we want to notify the model about when they change.
|
||||
tracked_buffers: HashMap<Entity<Buffer>, TrackedBuffer>,
|
||||
/// Has the model edited a file since it last checked diagnostics?
|
||||
edited_since_project_diagnostics_check: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
@@ -93,6 +95,7 @@ impl ActionLog {
|
||||
Self {
|
||||
stale_buffers_in_context: HashSet::default(),
|
||||
tracked_buffers: HashMap::default(),
|
||||
edited_since_project_diagnostics_check: false,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -110,6 +113,12 @@ impl ActionLog {
|
||||
}
|
||||
|
||||
self.stale_buffers_in_context.extend(buffers);
|
||||
self.edited_since_project_diagnostics_check = true;
|
||||
}
|
||||
|
||||
/// Notifies a diagnostics check
|
||||
pub fn checked_project_diagnostics(&mut self) {
|
||||
self.edited_since_project_diagnostics_check = false;
|
||||
}
|
||||
|
||||
/// Iterate over buffers changed since last read or edited by the model
|
||||
@@ -120,6 +129,11 @@ impl ActionLog {
|
||||
.map(|(buffer, _)| buffer)
|
||||
}
|
||||
|
||||
/// Returns true if any files have been edited since the last project diagnostics check
|
||||
pub fn has_edited_files_since_project_diagnostics_check(&self) -> bool {
|
||||
self.edited_since_project_diagnostics_check
|
||||
}
|
||||
|
||||
/// Takes and returns the set of buffers pending refresh, clearing internal state.
|
||||
pub fn take_stale_buffers_in_context(&mut self) -> HashSet<Entity<Buffer>> {
|
||||
std::mem::take(&mut self.stale_buffers_in_context)
|
||||
|
||||
@@ -19,7 +19,7 @@ pub struct ToolWorkingSet {
|
||||
struct WorkingSetState {
|
||||
context_server_tools_by_id: HashMap<ToolId, Arc<dyn Tool>>,
|
||||
context_server_tools_by_name: HashMap<String, Arc<dyn Tool>>,
|
||||
disabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
|
||||
enabled_tools_by_source: HashMap<ToolSource, HashSet<Arc<str>>>,
|
||||
next_tool_id: ToolId,
|
||||
}
|
||||
|
||||
@@ -41,38 +41,23 @@ impl ToolWorkingSet {
|
||||
self.state.lock().tools_by_source(cx)
|
||||
}
|
||||
|
||||
pub fn are_all_tools_enabled(&self) -> bool {
|
||||
let state = self.state.lock();
|
||||
state.disabled_tools_by_source.is_empty()
|
||||
}
|
||||
|
||||
pub fn are_all_tools_from_source_enabled(&self, source: &ToolSource) -> bool {
|
||||
let state = self.state.lock();
|
||||
!state.disabled_tools_by_source.contains_key(source)
|
||||
}
|
||||
|
||||
pub fn enabled_tools(&self, cx: &App) -> Vec<Arc<dyn Tool>> {
|
||||
self.state.lock().enabled_tools(cx)
|
||||
}
|
||||
|
||||
pub fn enable_all_tools(&self) {
|
||||
pub fn disable_all_tools(&self) {
|
||||
let mut state = self.state.lock();
|
||||
state.disabled_tools_by_source.clear();
|
||||
state.disable_all_tools();
|
||||
}
|
||||
|
||||
pub fn disable_all_tools(&self, cx: &App) {
|
||||
pub fn enable_source(&self, source: ToolSource, cx: &App) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable_all_tools(cx);
|
||||
state.enable_source(source, cx);
|
||||
}
|
||||
|
||||
pub fn enable_source(&self, source: &ToolSource) {
|
||||
pub fn disable_source(&self, source: &ToolSource) {
|
||||
let mut state = self.state.lock();
|
||||
state.enable_source(source);
|
||||
}
|
||||
|
||||
pub fn disable_source(&self, source: ToolSource, cx: &App) {
|
||||
let mut state = self.state.lock();
|
||||
state.disable_source(source, cx);
|
||||
state.disable_source(source);
|
||||
}
|
||||
|
||||
pub fn insert(&self, tool: Arc<dyn Tool>) -> ToolId {
|
||||
@@ -159,40 +144,36 @@ impl WorkingSetState {
|
||||
}
|
||||
|
||||
fn is_enabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
!self.is_disabled(source, name)
|
||||
self.enabled_tools_by_source
|
||||
.get(source)
|
||||
.map_or(false, |enabled_tools| enabled_tools.contains(name))
|
||||
}
|
||||
|
||||
fn is_disabled(&self, source: &ToolSource, name: &Arc<str>) -> bool {
|
||||
self.disabled_tools_by_source
|
||||
.get(source)
|
||||
.map_or(false, |disabled_tools| disabled_tools.contains(name))
|
||||
!self.is_enabled(source, name)
|
||||
}
|
||||
|
||||
fn enable(&mut self, source: ToolSource, tools_to_enable: &[Arc<str>]) {
|
||||
self.disabled_tools_by_source
|
||||
self.enabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.retain(|name| !tools_to_enable.contains(name));
|
||||
.extend(tools_to_enable.into_iter().cloned());
|
||||
}
|
||||
|
||||
fn disable(&mut self, source: ToolSource, tools_to_disable: &[Arc<str>]) {
|
||||
self.disabled_tools_by_source
|
||||
self.enabled_tools_by_source
|
||||
.entry(source)
|
||||
.or_default()
|
||||
.extend(tools_to_disable.into_iter().cloned());
|
||||
.retain(|name| !tools_to_disable.contains(name));
|
||||
}
|
||||
|
||||
fn enable_source(&mut self, source: &ToolSource) {
|
||||
self.disabled_tools_by_source.remove(source);
|
||||
}
|
||||
|
||||
fn disable_source(&mut self, source: ToolSource, cx: &App) {
|
||||
fn enable_source(&mut self, source: ToolSource, cx: &App) {
|
||||
let tools_by_source = self.tools_by_source(cx);
|
||||
let Some(tools) = tools_by_source.get(&source) else {
|
||||
return;
|
||||
};
|
||||
|
||||
self.disabled_tools_by_source.insert(
|
||||
self.enabled_tools_by_source.insert(
|
||||
source,
|
||||
tools
|
||||
.into_iter()
|
||||
@@ -201,16 +182,11 @@ impl WorkingSetState {
|
||||
);
|
||||
}
|
||||
|
||||
fn disable_all_tools(&mut self, cx: &App) {
|
||||
let tools = self.tools_by_source(cx);
|
||||
fn disable_source(&mut self, source: &ToolSource) {
|
||||
self.enabled_tools_by_source.remove(source);
|
||||
}
|
||||
|
||||
for (source, tools) in tools {
|
||||
let tool_names = tools
|
||||
.into_iter()
|
||||
.map(|tool| tool.name().into())
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
self.disable(source, &tool_names);
|
||||
}
|
||||
fn disable_all_tools(&mut self) {
|
||||
self.enabled_tools_by_source.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod bash_tool;
|
||||
mod copy_path_tool;
|
||||
mod create_directory_tool;
|
||||
mod create_file_tool;
|
||||
mod delete_path_tool;
|
||||
mod diagnostics_tool;
|
||||
@@ -24,6 +25,7 @@ use http_client::HttpClientWithUrl;
|
||||
use move_path_tool::MovePathTool;
|
||||
|
||||
use crate::bash_tool::BashTool;
|
||||
use crate::create_directory_tool::CreateDirectoryTool;
|
||||
use crate::create_file_tool::CreateFileTool;
|
||||
use crate::delete_path_tool::DeletePathTool;
|
||||
use crate::diagnostics_tool::DiagnosticsTool;
|
||||
@@ -43,6 +45,7 @@ pub fn init(http_client: Arc<HttpClientWithUrl>, cx: &mut App) {
|
||||
|
||||
let registry = ToolRegistry::global(cx);
|
||||
registry.register_tool(BashTool);
|
||||
registry.register_tool(CreateDirectoryTool);
|
||||
registry.register_tool(CreateFileTool);
|
||||
registry.register_tool(CopyPathTool);
|
||||
registry.register_tool(DeletePathTool);
|
||||
|
||||
@@ -8,6 +8,7 @@ use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::command::new_smol_command;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct BashToolInput {
|
||||
@@ -43,7 +44,13 @@ impl Tool for BashTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<BashToolInput>(input.clone()) {
|
||||
Ok(input) => format!("`{}`", input.command),
|
||||
Ok(input) => {
|
||||
if input.command.contains('\n') {
|
||||
MarkdownString::code_block("bash", &input.command).0
|
||||
} else {
|
||||
MarkdownString::inline_code(&input.command).0
|
||||
}
|
||||
}
|
||||
Err(_) => "Run bash command".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CopyPathToolInput {
|
||||
@@ -60,9 +61,9 @@ impl Tool for CopyPathTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CopyPathToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let src = input.source_path.as_str();
|
||||
let dest = input.destination_path.as_str();
|
||||
format!("Copy `{src}` to `{dest}`")
|
||||
let src = MarkdownString::inline_code(&input.source_path);
|
||||
let dest = MarkdownString::inline_code(&input.destination_path);
|
||||
format!("Copy {src} to {dest}")
|
||||
}
|
||||
Err(_) => "Copy path".to_string(),
|
||||
}
|
||||
|
||||
92
crates/assistant_tools/src/create_directory_tool.rs
Normal file
92
crates/assistant_tools/src/create_directory_tool.rs
Normal file
@@ -0,0 +1,92 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use assistant_tool::{ActionLog, Tool};
|
||||
use gpui::{App, Entity, Task};
|
||||
use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateDirectoryToolInput {
|
||||
/// The path of the new directory.
|
||||
///
|
||||
/// <example>
|
||||
/// If the project has the following structure:
|
||||
///
|
||||
/// - directory1/
|
||||
/// - directory2/
|
||||
///
|
||||
/// You can create a new directory by providing a path of "directory1/new_directory"
|
||||
/// </example>
|
||||
pub path: String,
|
||||
}
|
||||
|
||||
pub struct CreateDirectoryTool;
|
||||
|
||||
impl Tool for CreateDirectoryTool {
|
||||
fn name(&self) -> String {
|
||||
"create-directory".into()
|
||||
}
|
||||
|
||||
fn needs_confirmation(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn description(&self) -> String {
|
||||
include_str!("./create_directory_tool/description.md").into()
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Folder
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
let schema = schemars::schema_for!(CreateDirectoryToolInput);
|
||||
serde_json::to_value(&schema).unwrap()
|
||||
}
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CreateDirectoryToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
format!(
|
||||
"Create directory {}",
|
||||
MarkdownString::inline_code(&input.path)
|
||||
)
|
||||
}
|
||||
Err(_) => "Create directory".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
self: Arc<Self>,
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
let input = match serde_json::from_value::<CreateDirectoryToolInput>(input) {
|
||||
Ok(input) => input,
|
||||
Err(err) => return Task::ready(Err(anyhow!(err))),
|
||||
};
|
||||
let project_path = match project.read(cx).find_project_path(&input.path, cx) {
|
||||
Some(project_path) => project_path,
|
||||
None => return Task::ready(Err(anyhow!("Path to create was outside the project"))),
|
||||
};
|
||||
let destination_path: Arc<str> = input.path.as_str().into();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
project
|
||||
.update(cx, |project, cx| {
|
||||
project.create_entry(project_path.clone(), true, cx)
|
||||
})?
|
||||
.await
|
||||
.map_err(|err| anyhow!("Unable to create directory {destination_path}: {err}"))?;
|
||||
|
||||
Ok(format!("Created directory {destination_path}"))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
Creates a new directory at the specified path within the project. Returns confirmation that the directory was created.
|
||||
|
||||
This tool creates a directory and all necessary parent directories (similar to `mkdir -p`). It should be used whenever you need to create new directories within the project.
|
||||
@@ -7,6 +7,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::sync::Arc;
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct CreateFileToolInput {
|
||||
@@ -46,7 +47,7 @@ impl Tool for CreateFileTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::File
|
||||
IconName::FileCreate
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
@@ -57,8 +58,8 @@ impl Tool for CreateFileTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<CreateFileToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let path = input.path.as_str();
|
||||
format!("Create file `{path}`")
|
||||
let path = MarkdownString::inline_code(&input.path);
|
||||
format!("Create file {path}")
|
||||
}
|
||||
Err(_) => "Create file".to_string(),
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ impl Tool for DeletePathTool {
|
||||
}
|
||||
|
||||
fn icon(&self) -> IconName {
|
||||
IconName::Trash
|
||||
IconName::FileDelete
|
||||
}
|
||||
|
||||
fn input_schema(&self) -> serde_json::Value {
|
||||
|
||||
@@ -6,12 +6,9 @@ use language_model::LanguageModelRequestMessage;
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct DiagnosticsToolInput {
|
||||
@@ -28,7 +25,17 @@ pub struct DiagnosticsToolInput {
|
||||
///
|
||||
/// If you wanna access diagnostics for `dolor.txt` in `ipsum`, you should use the path `ipsum/dolor.txt`.
|
||||
/// </example>
|
||||
pub path: Option<PathBuf>,
|
||||
#[serde(deserialize_with = "deserialize_path")]
|
||||
pub path: Option<String>,
|
||||
}
|
||||
|
||||
fn deserialize_path<'de, D>(deserializer: D) -> Result<Option<String>, D::Error>
|
||||
where
|
||||
D: serde::Deserializer<'de>,
|
||||
{
|
||||
let opt = Option::<String>::deserialize(deserializer)?;
|
||||
// The model passes an empty string sometimes
|
||||
Ok(opt.filter(|s| !s.is_empty()))
|
||||
}
|
||||
|
||||
pub struct DiagnosticsTool;
|
||||
@@ -58,9 +65,12 @@ impl Tool for DiagnosticsTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input.clone())
|
||||
.ok()
|
||||
.and_then(|input| input.path)
|
||||
.and_then(|input| match input.path {
|
||||
Some(path) if !path.is_empty() => Some(MarkdownString::inline_code(&path)),
|
||||
_ => None,
|
||||
})
|
||||
{
|
||||
format!("Check diagnostics for “`{}`”", path.display())
|
||||
format!("Check diagnostics for {path}")
|
||||
} else {
|
||||
"Check project diagnostics".to_string()
|
||||
}
|
||||
@@ -71,78 +81,84 @@ impl Tool for DiagnosticsTool {
|
||||
input: serde_json::Value,
|
||||
_messages: &[LanguageModelRequestMessage],
|
||||
project: Entity<Project>,
|
||||
_action_log: Entity<ActionLog>,
|
||||
action_log: Entity<ActionLog>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
if let Some(path) = serde_json::from_value::<DiagnosticsToolInput>(input)
|
||||
match serde_json::from_value::<DiagnosticsToolInput>(input)
|
||||
.ok()
|
||||
.and_then(|input| input.path)
|
||||
{
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Could not find path {} in project",
|
||||
path.display()
|
||||
)));
|
||||
};
|
||||
let buffer = project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
Some(path) if !path.is_empty() => {
|
||||
let Some(project_path) = project.read(cx).find_project_path(&path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Could not find path {path} in project",)));
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut output = String::new();
|
||||
let buffer = buffer.await?;
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
let buffer =
|
||||
project.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let severity = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => "error",
|
||||
DiagnosticSeverity::WARNING => "warning",
|
||||
_ => continue,
|
||||
};
|
||||
cx.spawn(async move |cx| {
|
||||
let mut output = String::new();
|
||||
let buffer = buffer.await?;
|
||||
let snapshot = buffer.read_with(cx, |buffer, _cx| buffer.snapshot())?;
|
||||
|
||||
writeln!(
|
||||
output,
|
||||
"{} at line {}: {}",
|
||||
severity,
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
}
|
||||
for (_, group) in snapshot.diagnostic_groups(None) {
|
||||
let entry = &group.entries[group.primary_ix];
|
||||
let range = entry.range.to_point(&snapshot);
|
||||
let severity = match entry.diagnostic.severity {
|
||||
DiagnosticSeverity::ERROR => "error",
|
||||
DiagnosticSeverity::WARNING => "warning",
|
||||
_ => continue,
|
||||
};
|
||||
|
||||
if output.is_empty() {
|
||||
Ok("File doesn't have errors or warnings!".to_string())
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
let project = project.read(cx);
|
||||
let mut output = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
writeln!(
|
||||
output,
|
||||
"{} at line {}: {}",
|
||||
severity,
|
||||
range.start.row + 1,
|
||||
entry.diagnostic.message
|
||||
)?;
|
||||
}
|
||||
|
||||
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
}
|
||||
if output.is_empty() {
|
||||
Ok("File doesn't have errors or warnings!".to_string())
|
||||
} else {
|
||||
Ok(output)
|
||||
}
|
||||
})
|
||||
}
|
||||
_ => {
|
||||
let project = project.read(cx);
|
||||
let mut output = String::new();
|
||||
let mut has_diagnostics = false;
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output))
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
for (project_path, _, summary) in project.diagnostic_summaries(true, cx) {
|
||||
if summary.error_count > 0 || summary.warning_count > 0 {
|
||||
let Some(worktree) = project.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
has_diagnostics = true;
|
||||
output.push_str(&format!(
|
||||
"{}: {} error(s), {} warning(s)\n",
|
||||
Path::new(worktree.read(cx).root_name())
|
||||
.join(project_path.path)
|
||||
.display(),
|
||||
summary.error_count,
|
||||
summary.warning_count
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
action_log.update(cx, |action_log, _cx| {
|
||||
action_log.checked_project_diagnostics();
|
||||
});
|
||||
|
||||
if has_diagnostics {
|
||||
Task::ready(Ok(output))
|
||||
} else {
|
||||
Task::ready(Ok("No errors or warnings found in the project.".to_string()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,3 +14,5 @@ To get diagnostics for a specific file:
|
||||
To get a project-wide diagnostic summary:
|
||||
{}
|
||||
</example>
|
||||
|
||||
IMPORTANT: When you're done making changes, you **MUST** get the **project** diagnostics (input: `{}`) at the end of your edits so you can fix any problems you might have introduced. **DO NOT** tell the user you're done before doing this!
|
||||
|
||||
@@ -13,6 +13,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
|
||||
enum ContentType {
|
||||
@@ -133,7 +134,7 @@ impl Tool for FetchTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<FetchToolInput>(input.clone()) {
|
||||
Ok(input) => format!("Fetch `{}`", input.url),
|
||||
Ok(input) => format!("Fetch {}", MarkdownString::escape(&input.url)),
|
||||
Err(_) => "Fetch URL".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,10 +33,10 @@ pub struct FindReplaceFileToolInput {
|
||||
/// </example>
|
||||
pub path: PathBuf,
|
||||
|
||||
/// A user-friendly description of what's being replaced. This will be shown in the UI.
|
||||
/// A user-friendly markdown description of what's being replaced. This will be shown in the UI.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
pub display_description: String,
|
||||
|
||||
/// The unique string to find in the file. This string cannot be empty;
|
||||
|
||||
@@ -7,6 +7,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{fmt::Write, path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ListDirectoryToolInput {
|
||||
@@ -61,7 +62,10 @@ impl Tool for ListDirectoryTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<ListDirectoryToolInput>(input.clone()) {
|
||||
Ok(input) => format!("List the `{}` directory's contents", input.path),
|
||||
Ok(input) => {
|
||||
let path = MarkdownString::inline_code(&input.path);
|
||||
format!("List the {path} directory's contents")
|
||||
}
|
||||
Err(_) => "List directory".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::Path, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct MovePathToolInput {
|
||||
@@ -60,20 +61,21 @@ impl Tool for MovePathTool {
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<MovePathToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let src = input.source_path.as_str();
|
||||
let dest = input.destination_path.as_str();
|
||||
let src_path = Path::new(src);
|
||||
let dest_path = Path::new(dest);
|
||||
let src = MarkdownString::inline_code(&input.source_path);
|
||||
let dest = MarkdownString::inline_code(&input.destination_path);
|
||||
let src_path = Path::new(&input.source_path);
|
||||
let dest_path = Path::new(&input.destination_path);
|
||||
|
||||
match dest_path
|
||||
.file_name()
|
||||
.and_then(|os_str| os_str.to_os_string().into_string().ok())
|
||||
{
|
||||
Some(filename) if src_path.parent() == dest_path.parent() => {
|
||||
format!("Rename `{src}` to `{filename}`")
|
||||
let filename = MarkdownString::inline_code(&filename);
|
||||
format!("Rename {src} to {filename}")
|
||||
}
|
||||
_ => {
|
||||
format!("Move `{src}` to `{dest}`")
|
||||
format!("Move {src} to {dest}")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ReadFileToolInput {
|
||||
@@ -64,7 +65,10 @@ impl Tool for ReadFileTool {
|
||||
|
||||
fn ui_text(&self, input: &serde_json::Value) -> String {
|
||||
match serde_json::from_value::<ReadFileToolInput>(input.clone()) {
|
||||
Ok(input) => format!("Read file `{}`", input.path.display()),
|
||||
Ok(input) => {
|
||||
let path = MarkdownString::inline_code(&input.path.display().to_string());
|
||||
format!("Read file {path}")
|
||||
}
|
||||
Err(_) => "Read file".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{cmp, fmt::Write, sync::Arc};
|
||||
use ui::IconName;
|
||||
use util::markdown::MarkdownString;
|
||||
use util::paths::PathMatcher;
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
@@ -63,14 +64,12 @@ impl Tool for RegexSearchTool {
|
||||
match serde_json::from_value::<RegexSearchToolInput>(input.clone()) {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
let regex = MarkdownString::inline_code(&input.regex);
|
||||
|
||||
if page > 1 {
|
||||
format!(
|
||||
"Get page {page} of search results for regex “`{}`”",
|
||||
input.regex
|
||||
)
|
||||
format!("Get page {page} of search results for regex “{regex}”")
|
||||
} else {
|
||||
format!("Search files for regex “`{}`”", input.regex)
|
||||
format!("Search files for regex “{regex}”")
|
||||
}
|
||||
}
|
||||
Err(_) => "Search with regex".to_string(),
|
||||
|
||||
@@ -3,9 +3,7 @@ use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task};
|
||||
use language::{Language, LanguageRegistry};
|
||||
use rope::Rope;
|
||||
use std::cmp::Ordering;
|
||||
use std::mem;
|
||||
use std::{future::Future, iter, ops::Range, sync::Arc};
|
||||
use std::{cmp::Ordering, future::Future, iter, mem, ops::Range, sync::Arc};
|
||||
use sum_tree::SumTree;
|
||||
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _};
|
||||
use util::ResultExt;
|
||||
@@ -195,7 +193,7 @@ impl BufferDiffInner {
|
||||
hunks: &[DiffHunk],
|
||||
buffer: &text::BufferSnapshot,
|
||||
file_exists: bool,
|
||||
) -> (Option<Rope>, SumTree<PendingHunk>) {
|
||||
) -> Option<Rope> {
|
||||
let head_text = self
|
||||
.base_text_exists
|
||||
.then(|| self.base_text.as_rope().clone());
|
||||
@@ -208,7 +206,7 @@ impl BufferDiffInner {
|
||||
let (index_text, head_text) = match (index_text, head_text) {
|
||||
(Some(index_text), Some(head_text)) if file_exists || !stage => (index_text, head_text),
|
||||
(index_text, head_text) => {
|
||||
let (rope, new_status) = if stage {
|
||||
let (new_index_text, new_status) = if stage {
|
||||
log::debug!("stage all");
|
||||
(
|
||||
file_exists.then(|| buffer.as_rope().clone()),
|
||||
@@ -228,15 +226,13 @@ impl BufferDiffInner {
|
||||
buffer_version: buffer.version().clone(),
|
||||
new_status,
|
||||
};
|
||||
let tree = SumTree::from_item(hunk, buffer);
|
||||
return (rope, tree);
|
||||
self.pending_hunks = SumTree::from_item(hunk, buffer);
|
||||
return new_index_text;
|
||||
}
|
||||
};
|
||||
|
||||
let mut pending_hunks = SumTree::new(buffer);
|
||||
let mut old_pending_hunks = unstaged_diff
|
||||
.pending_hunks
|
||||
.cursor::<DiffHunkSummary>(buffer);
|
||||
let mut old_pending_hunks = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
|
||||
// first, merge new hunks into pending_hunks
|
||||
for DiffHunk {
|
||||
@@ -261,7 +257,6 @@ impl BufferDiffInner {
|
||||
old_pending_hunks.next(buffer);
|
||||
}
|
||||
|
||||
// merge into pending hunks
|
||||
if (stage && secondary_status == DiffHunkSecondaryStatus::NoSecondaryHunk)
|
||||
|| (!stage && secondary_status == DiffHunkSecondaryStatus::HasSecondaryHunk)
|
||||
{
|
||||
@@ -288,56 +283,71 @@ impl BufferDiffInner {
|
||||
let mut unstaged_hunk_cursor = unstaged_diff.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
unstaged_hunk_cursor.next(buffer);
|
||||
|
||||
let mut prev_unstaged_hunk_buffer_offset = 0;
|
||||
let mut prev_unstaged_hunk_base_text_offset = 0;
|
||||
let mut edits = Vec::<(Range<usize>, String)>::new();
|
||||
|
||||
// then, iterate over all pending hunks (both new ones and the existing ones) and compute the edits
|
||||
for PendingHunk {
|
||||
let mut prev_unstaged_hunk_buffer_end = 0;
|
||||
let mut prev_unstaged_hunk_base_text_end = 0;
|
||||
let mut edits = Vec::<(Range<usize>, String)>::new();
|
||||
let mut pending_hunks_iter = pending_hunks.iter().cloned().peekable();
|
||||
while let Some(PendingHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
..
|
||||
} in pending_hunks.iter().cloned()
|
||||
}) = pending_hunks_iter.next()
|
||||
{
|
||||
let skipped_hunks = unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
|
||||
// Advance unstaged_hunk_cursor to skip unstaged hunks before current hunk
|
||||
let skipped_unstaged =
|
||||
unstaged_hunk_cursor.slice(&buffer_range.start, Bias::Left, buffer);
|
||||
|
||||
if let Some(secondary_hunk) = skipped_hunks.last() {
|
||||
prev_unstaged_hunk_base_text_offset = secondary_hunk.diff_base_byte_range.end;
|
||||
prev_unstaged_hunk_buffer_offset =
|
||||
secondary_hunk.buffer_range.end.to_offset(buffer);
|
||||
if let Some(unstaged_hunk) = skipped_unstaged.last() {
|
||||
prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
|
||||
prev_unstaged_hunk_buffer_end = unstaged_hunk.buffer_range.end.to_offset(buffer);
|
||||
}
|
||||
|
||||
// Find where this hunk is in the index if it doesn't overlap
|
||||
let mut buffer_offset_range = buffer_range.to_offset(buffer);
|
||||
let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_offset;
|
||||
let mut index_start = prev_unstaged_hunk_base_text_offset + start_overshoot;
|
||||
let start_overshoot = buffer_offset_range.start - prev_unstaged_hunk_buffer_end;
|
||||
let mut index_start = prev_unstaged_hunk_base_text_end + start_overshoot;
|
||||
|
||||
while let Some(unstaged_hunk) = unstaged_hunk_cursor.item().filter(|item| {
|
||||
item.buffer_range
|
||||
.start
|
||||
.cmp(&buffer_range.end, buffer)
|
||||
.is_le()
|
||||
}) {
|
||||
let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
|
||||
prev_unstaged_hunk_base_text_offset = unstaged_hunk.diff_base_byte_range.end;
|
||||
prev_unstaged_hunk_buffer_offset = unstaged_hunk_offset_range.end;
|
||||
loop {
|
||||
// Merge this hunk with any overlapping unstaged hunks.
|
||||
if let Some(unstaged_hunk) = unstaged_hunk_cursor.item() {
|
||||
let unstaged_hunk_offset_range = unstaged_hunk.buffer_range.to_offset(buffer);
|
||||
if unstaged_hunk_offset_range.start <= buffer_offset_range.end {
|
||||
prev_unstaged_hunk_base_text_end = unstaged_hunk.diff_base_byte_range.end;
|
||||
prev_unstaged_hunk_buffer_end = unstaged_hunk_offset_range.end;
|
||||
|
||||
index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
|
||||
buffer_offset_range.start = buffer_offset_range
|
||||
.start
|
||||
.min(unstaged_hunk_offset_range.start);
|
||||
index_start = index_start.min(unstaged_hunk.diff_base_byte_range.start);
|
||||
buffer_offset_range.start = buffer_offset_range
|
||||
.start
|
||||
.min(unstaged_hunk_offset_range.start);
|
||||
buffer_offset_range.end =
|
||||
buffer_offset_range.end.max(unstaged_hunk_offset_range.end);
|
||||
|
||||
unstaged_hunk_cursor.next(buffer);
|
||||
unstaged_hunk_cursor.next(buffer);
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If any unstaged hunks were merged, then subsequent pending hunks may
|
||||
// now overlap this hunk. Merge them.
|
||||
if let Some(next_pending_hunk) = pending_hunks_iter.peek() {
|
||||
let next_pending_hunk_offset_range =
|
||||
next_pending_hunk.buffer_range.to_offset(buffer);
|
||||
if next_pending_hunk_offset_range.start <= buffer_offset_range.end {
|
||||
buffer_offset_range.end = next_pending_hunk_offset_range.end;
|
||||
pending_hunks_iter.next();
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
let end_overshoot = buffer_offset_range
|
||||
.end
|
||||
.saturating_sub(prev_unstaged_hunk_buffer_offset);
|
||||
let index_end = prev_unstaged_hunk_base_text_offset + end_overshoot;
|
||||
|
||||
let index_range = index_start..index_end;
|
||||
buffer_offset_range.end = buffer_offset_range
|
||||
.end
|
||||
.max(prev_unstaged_hunk_buffer_offset);
|
||||
.saturating_sub(prev_unstaged_hunk_buffer_end);
|
||||
let index_end = prev_unstaged_hunk_base_text_end + end_overshoot;
|
||||
let index_byte_range = index_start..index_end;
|
||||
|
||||
let replacement_text = if stage {
|
||||
log::debug!("stage hunk {:?}", buffer_offset_range);
|
||||
@@ -351,8 +361,11 @@ impl BufferDiffInner {
|
||||
.collect::<String>()
|
||||
};
|
||||
|
||||
edits.push((index_range, replacement_text));
|
||||
edits.push((index_byte_range, replacement_text));
|
||||
}
|
||||
drop(pending_hunks_iter);
|
||||
drop(old_pending_hunks);
|
||||
self.pending_hunks = pending_hunks;
|
||||
|
||||
#[cfg(debug_assertions)] // invariants: non-overlapping and sorted
|
||||
{
|
||||
@@ -371,7 +384,7 @@ impl BufferDiffInner {
|
||||
new_index_text.push(&replacement_text);
|
||||
}
|
||||
new_index_text.append(index_cursor.suffix());
|
||||
(Some(new_index_text), pending_hunks)
|
||||
Some(new_index_text)
|
||||
}
|
||||
|
||||
fn hunks_intersecting_range<'a>(
|
||||
@@ -408,15 +421,14 @@ impl BufferDiffInner {
|
||||
]
|
||||
});
|
||||
|
||||
let mut pending_hunks_cursor = self.pending_hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
pending_hunks_cursor.next(buffer);
|
||||
|
||||
let mut secondary_cursor = None;
|
||||
let mut pending_hunks_cursor = None;
|
||||
if let Some(secondary) = secondary.as_ref() {
|
||||
let mut cursor = secondary.hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
secondary_cursor = Some(cursor);
|
||||
let mut cursor = secondary.pending_hunks.cursor::<DiffHunkSummary>(buffer);
|
||||
cursor.next(buffer);
|
||||
pending_hunks_cursor = Some(cursor);
|
||||
}
|
||||
|
||||
let max_point = buffer.max_point();
|
||||
@@ -438,29 +450,27 @@ impl BufferDiffInner {
|
||||
let mut secondary_status = DiffHunkSecondaryStatus::NoSecondaryHunk;
|
||||
|
||||
let mut has_pending = false;
|
||||
if let Some(pending_cursor) = pending_hunks_cursor.as_mut() {
|
||||
if start_anchor
|
||||
.cmp(&pending_cursor.start().buffer_range.start, buffer)
|
||||
.is_gt()
|
||||
{
|
||||
pending_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
|
||||
if start_anchor
|
||||
.cmp(&pending_hunks_cursor.start().buffer_range.start, buffer)
|
||||
.is_gt()
|
||||
{
|
||||
pending_hunks_cursor.seek_forward(&start_anchor, Bias::Left, buffer);
|
||||
}
|
||||
|
||||
if let Some(pending_hunk) = pending_hunks_cursor.item() {
|
||||
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
|
||||
if pending_range.end.column > 0 {
|
||||
pending_range.end.row += 1;
|
||||
pending_range.end.column = 0;
|
||||
}
|
||||
|
||||
if let Some(pending_hunk) = pending_cursor.item() {
|
||||
let mut pending_range = pending_hunk.buffer_range.to_point(buffer);
|
||||
if pending_range.end.column > 0 {
|
||||
pending_range.end.row += 1;
|
||||
pending_range.end.column = 0;
|
||||
}
|
||||
|
||||
if pending_range == (start_point..end_point) {
|
||||
if !buffer.has_edits_since_in_range(
|
||||
&pending_hunk.buffer_version,
|
||||
start_anchor..end_anchor,
|
||||
) {
|
||||
has_pending = true;
|
||||
secondary_status = pending_hunk.new_status;
|
||||
}
|
||||
if pending_range == (start_point..end_point) {
|
||||
if !buffer.has_edits_since_in_range(
|
||||
&pending_hunk.buffer_version,
|
||||
start_anchor..end_anchor,
|
||||
) {
|
||||
has_pending = true;
|
||||
secondary_status = pending_hunk.new_status;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -839,10 +849,8 @@ impl BufferDiff {
|
||||
}
|
||||
|
||||
pub fn clear_pending_hunks(&mut self, cx: &mut Context<Self>) {
|
||||
if let Some(secondary_diff) = &self.secondary_diff {
|
||||
secondary_diff.update(cx, |diff, _| {
|
||||
diff.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
|
||||
});
|
||||
if self.secondary_diff.is_some() {
|
||||
self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary::default());
|
||||
cx.emit(BufferDiffEvent::DiffChanged {
|
||||
changed_range: Some(Anchor::MIN..Anchor::MAX),
|
||||
});
|
||||
@@ -857,7 +865,7 @@ impl BufferDiff {
|
||||
file_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Rope> {
|
||||
let (new_index_text, new_pending_hunks) = self.inner.stage_or_unstage_hunks_impl(
|
||||
let new_index_text = self.inner.stage_or_unstage_hunks_impl(
|
||||
&self.secondary_diff.as_ref()?.read(cx).inner,
|
||||
stage,
|
||||
&hunks,
|
||||
@@ -865,11 +873,6 @@ impl BufferDiff {
|
||||
file_exists,
|
||||
);
|
||||
|
||||
if let Some(unstaged_diff) = &self.secondary_diff {
|
||||
unstaged_diff.update(cx, |diff, _| {
|
||||
diff.inner.pending_hunks = new_pending_hunks;
|
||||
});
|
||||
}
|
||||
cx.emit(BufferDiffEvent::HunksStagedOrUnstaged(
|
||||
new_index_text.clone(),
|
||||
));
|
||||
@@ -1649,6 +1652,75 @@ mod tests {
|
||||
"
|
||||
.unindent(),
|
||||
},
|
||||
Example {
|
||||
name: "one unstaged hunk that contains two uncommitted hunks",
|
||||
head_text: "
|
||||
one
|
||||
two
|
||||
|
||||
three
|
||||
four
|
||||
"
|
||||
.unindent(),
|
||||
index_text: "
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
"
|
||||
.unindent(),
|
||||
buffer_marked_text: "
|
||||
«one
|
||||
|
||||
three // modified
|
||||
four»
|
||||
"
|
||||
.unindent(),
|
||||
final_index_text: "
|
||||
one
|
||||
|
||||
three // modified
|
||||
four
|
||||
"
|
||||
.unindent(),
|
||||
},
|
||||
Example {
|
||||
name: "one uncommitted hunk that contains two unstaged hunks",
|
||||
head_text: "
|
||||
one
|
||||
two
|
||||
three
|
||||
four
|
||||
five
|
||||
"
|
||||
.unindent(),
|
||||
index_text: "
|
||||
ZERO
|
||||
one
|
||||
TWO
|
||||
THREE
|
||||
FOUR
|
||||
five
|
||||
"
|
||||
.unindent(),
|
||||
buffer_marked_text: "
|
||||
«one
|
||||
TWO_HUNDRED
|
||||
THREE
|
||||
FOUR_HUNDRED
|
||||
five»
|
||||
"
|
||||
.unindent(),
|
||||
final_index_text: "
|
||||
ZERO
|
||||
one
|
||||
TWO_HUNDRED
|
||||
THREE
|
||||
FOUR_HUNDRED
|
||||
five
|
||||
"
|
||||
.unindent(),
|
||||
},
|
||||
];
|
||||
|
||||
for example in table {
|
||||
|
||||
@@ -43,10 +43,10 @@ telemetry.workspace = true
|
||||
util.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
livekit_client_macos = { workspace = true }
|
||||
livekit_client_macos.workspace = true
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
livekit_client = { workspace = true }
|
||||
livekit_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
client = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -115,7 +115,7 @@ notifications = { workspace = true, features = ["test-support"] }
|
||||
pretty_assertions.workspace = true
|
||||
project = { workspace = true, features = ["test-support"] }
|
||||
prompt_store.workspace = true
|
||||
recent_projects = { workspace = true }
|
||||
recent_projects.workspace = true
|
||||
release_channel.workspace = true
|
||||
remote = { workspace = true, features = ["test-support"] }
|
||||
remote_server.workspace = true
|
||||
|
||||
@@ -304,6 +304,7 @@ impl Server {
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetReferences>)
|
||||
.add_request_handler(forward_find_search_candidates_request)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetDocumentHighlights>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetDocumentSymbols>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::GetProjectSymbols>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferForSymbol>)
|
||||
.add_request_handler(forward_read_only_project_request::<proto::OpenBufferById>)
|
||||
|
||||
@@ -26,7 +26,7 @@ use language::{
|
||||
tree_sitter_rust, tree_sitter_typescript, Diagnostic, DiagnosticEntry, FakeLspAdapter,
|
||||
Language, LanguageConfig, LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
};
|
||||
use lsp::LanguageServerId;
|
||||
use lsp::{LanguageServerId, OneOf};
|
||||
use parking_lot::Mutex;
|
||||
use pretty_assertions::assert_eq;
|
||||
use project::{
|
||||
@@ -2892,15 +2892,17 @@ async fn test_git_branch_name(
|
||||
#[track_caller]
|
||||
fn assert_branch(branch_name: Option<impl Into<String>>, project: &Project, cx: &App) {
|
||||
let branch_name = branch_name.map(Into::into);
|
||||
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
let worktree = worktrees[0].clone();
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let repo = snapshot.repositories().first().unwrap();
|
||||
let repositories = project.repositories(cx).values().collect::<Vec<_>>();
|
||||
assert_eq!(repositories.len(), 1);
|
||||
let repository = repositories[0].clone();
|
||||
assert_eq!(
|
||||
repo.branch().map(|branch| branch.name.to_string()),
|
||||
repository
|
||||
.read(cx)
|
||||
.repository_entry
|
||||
.branch()
|
||||
.map(|branch| branch.name.to_string()),
|
||||
branch_name
|
||||
);
|
||||
)
|
||||
}
|
||||
|
||||
// Smoke test branch reading
|
||||
@@ -3022,11 +3024,20 @@ async fn test_git_status_sync(
|
||||
cx: &App,
|
||||
) {
|
||||
let file = file.as_ref();
|
||||
let worktrees = project.visible_worktrees(cx).collect::<Vec<_>>();
|
||||
assert_eq!(worktrees.len(), 1);
|
||||
let worktree = worktrees[0].clone();
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
assert_eq!(snapshot.status_for_file(file), status);
|
||||
let repos = project
|
||||
.repositories(cx)
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(repos.len(), 1);
|
||||
let repo = repos.into_iter().next().unwrap();
|
||||
assert_eq!(
|
||||
repo.read(cx)
|
||||
.repository_entry
|
||||
.status_for_path(&file.into())
|
||||
.map(|entry| entry.status),
|
||||
status
|
||||
);
|
||||
}
|
||||
|
||||
project_local.read_with(cx_a, |project, cx| {
|
||||
@@ -3094,6 +3105,27 @@ async fn test_git_status_sync(
|
||||
assert_status("b.txt", Some(B_STATUS_END), project, cx);
|
||||
assert_status("c.txt", Some(C_STATUS_END), project, cx);
|
||||
});
|
||||
|
||||
// Now remove the original git repository and check that collaborators are notified.
|
||||
client_a
|
||||
.fs()
|
||||
.remove_dir("/dir/.git".as_ref(), RemoveOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
executor.run_until_parked();
|
||||
project_remote.update(cx_b, |project, cx| {
|
||||
pretty_assertions::assert_eq!(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
HashMap::default()
|
||||
);
|
||||
});
|
||||
project_remote_c.update(cx_c, |project, cx| {
|
||||
pretty_assertions::assert_eq!(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
HashMap::default()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test(iterations = 10)]
|
||||
@@ -5399,9 +5431,16 @@ async fn test_project_symbols(
|
||||
let active_call_a = cx_a.read(ActiveCall::global);
|
||||
|
||||
client_a.language_registry().add(rust_lang());
|
||||
let mut fake_language_servers = client_a
|
||||
.language_registry()
|
||||
.register_fake_lsp("Rust", Default::default());
|
||||
let mut fake_language_servers = client_a.language_registry().register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
workspace_symbol_provider: Some(OneOf::Left(true)),
|
||||
..Default::default()
|
||||
},
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
client_a
|
||||
.fs()
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use crate::tests::TestServer;
|
||||
use call::ActiveCall;
|
||||
use collections::HashSet;
|
||||
use collections::{HashMap, HashSet};
|
||||
use extension::ExtensionHostProxy;
|
||||
use fs::{FakeFs, Fs as _};
|
||||
use fs::{FakeFs, Fs as _, RemoveOptions};
|
||||
use futures::StreamExt as _;
|
||||
use gpui::{
|
||||
AppContext as _, BackgroundExecutor, SemanticVersion, TestAppContext, UpdateGlobal as _,
|
||||
@@ -356,6 +356,26 @@ async fn test_ssh_collaboration_git_branches(
|
||||
});
|
||||
|
||||
assert_eq!(server_branch.name, "totally-new-branch");
|
||||
|
||||
// Remove the git repository and check that all participants get the update.
|
||||
remote_fs
|
||||
.remove_dir("/project/.git".as_ref(), RemoveOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
executor.run_until_parked();
|
||||
|
||||
project_a.update(cx_a, |project, cx| {
|
||||
pretty_assertions::assert_eq!(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
HashMap::default()
|
||||
);
|
||||
});
|
||||
project_b.update(cx_b, |project, cx| {
|
||||
pretty_assertions::assert_eq!(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
HashMap::default()
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
|
||||
@@ -3411,6 +3411,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline(&mut self, _: &Newline, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
let (edits, selection_fixup_info): (Vec<_>, Vec<_>) = {
|
||||
let selections = this.selections.all::<usize>(cx);
|
||||
@@ -3526,6 +3527,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline_above(&mut self, _: &NewlineAbove, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
@@ -3583,6 +3586,8 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn newline_below(&mut self, _: &NewlineBelow, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
|
||||
@@ -4315,6 +4320,10 @@ impl Editor {
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.sort_completions());
|
||||
|
||||
let filter_completions = provider
|
||||
.as_ref()
|
||||
.map_or(true, |provider| provider.filter_completions());
|
||||
|
||||
let id = post_inc(&mut self.next_completion_id);
|
||||
let task = cx.spawn_in(window, async move |editor, cx| {
|
||||
async move {
|
||||
@@ -4363,8 +4372,15 @@ impl Editor {
|
||||
completions.into(),
|
||||
);
|
||||
|
||||
menu.filter(query.as_deref(), cx.background_executor().clone())
|
||||
.await;
|
||||
menu.filter(
|
||||
if filter_completions {
|
||||
query.as_deref()
|
||||
} else {
|
||||
None
|
||||
},
|
||||
cx.background_executor().clone(),
|
||||
)
|
||||
.await;
|
||||
|
||||
menu.visible().then_some(menu)
|
||||
};
|
||||
@@ -6112,35 +6128,6 @@ impl Editor {
|
||||
return breakpoint_display_points;
|
||||
};
|
||||
|
||||
if let Some(buffer) = self.buffer.read(cx).as_singleton() {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
|
||||
for breakpoint in
|
||||
breakpoint_store
|
||||
.read(cx)
|
||||
.breakpoints(&buffer, None, &buffer_snapshot, cx)
|
||||
{
|
||||
let point = buffer_snapshot.summary_for_anchor::<Point>(&breakpoint.0);
|
||||
let mut anchor = multi_buffer_snapshot.anchor_before(point);
|
||||
anchor.text_anchor = breakpoint.0;
|
||||
|
||||
breakpoint_display_points.insert(
|
||||
snapshot
|
||||
.point_to_display_point(
|
||||
MultiBufferPoint {
|
||||
row: point.row,
|
||||
column: point.column,
|
||||
},
|
||||
Bias::Left,
|
||||
)
|
||||
.row(),
|
||||
(anchor, breakpoint.1.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
return breakpoint_display_points;
|
||||
}
|
||||
|
||||
let range = snapshot.display_point_to_point(DisplayPoint::new(range.start, 0), Bias::Left)
|
||||
..snapshot.display_point_to_point(DisplayPoint::new(range.end, 0), Bias::Right);
|
||||
|
||||
@@ -7773,6 +7760,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn backspace(&mut self, _: &Backspace, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.select_autoclose_pair(window, cx);
|
||||
let mut linked_ranges = HashMap::<_, Vec<_>>::default();
|
||||
@@ -7871,6 +7859,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
pub fn delete(&mut self, _: &Delete, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.transact(window, cx, |this, window, cx| {
|
||||
this.change_selections(Some(Autoscroll::fit()), window, cx, |s| {
|
||||
let line_mode = s.line_mode;
|
||||
@@ -7892,7 +7881,7 @@ impl Editor {
|
||||
if self.move_to_prev_snippet_tabstop(window, cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
self.outdent(&Outdent, window, cx);
|
||||
}
|
||||
|
||||
@@ -7900,7 +7889,7 @@ impl Editor {
|
||||
if self.move_to_next_snippet_tabstop(window, cx) || self.read_only(cx) {
|
||||
return;
|
||||
}
|
||||
|
||||
self.mouse_cursor_hidden = self.hide_mouse_while_typing;
|
||||
let mut selections = self.selections.all_adjusted(cx);
|
||||
let buffer = self.buffer.read(cx);
|
||||
let snapshot = buffer.snapshot(cx);
|
||||
@@ -18024,6 +18013,10 @@ pub trait CompletionProvider {
|
||||
fn sort_completions(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn filter_completions(&self) -> bool {
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
pub trait CodeActionProvider {
|
||||
|
||||
@@ -1955,7 +1955,12 @@ impl EditorElement {
|
||||
.filter_map(|(display_row, (text_anchor, bp))| {
|
||||
if row_infos
|
||||
.get((display_row.0.saturating_sub(range.start.0)) as usize)
|
||||
.is_some_and(|row_info| row_info.expand_info.is_some())
|
||||
.is_some_and(|row_info| {
|
||||
row_info.expand_info.is_some()
|
||||
|| row_info
|
||||
.diff_status
|
||||
.is_some_and(|status| status.is_deleted())
|
||||
})
|
||||
{
|
||||
return None;
|
||||
}
|
||||
@@ -4308,7 +4313,7 @@ impl EditorElement {
|
||||
let is_singleton = self.editor.read(cx).is_singleton(cx);
|
||||
|
||||
let line_height = layout.position_map.line_height;
|
||||
window.set_cursor_style(CursorStyle::Arrow, &layout.gutter_hitbox);
|
||||
window.set_cursor_style(CursorStyle::Arrow, Some(&layout.gutter_hitbox));
|
||||
|
||||
for LineNumberLayout {
|
||||
shaped_line,
|
||||
@@ -4341,9 +4346,9 @@ impl EditorElement {
|
||||
// In singleton buffers, we select corresponding lines on the line number click, so use | -like cursor.
|
||||
// In multi buffers, we open file at the line number clicked, so use a pointing hand cursor.
|
||||
if is_singleton {
|
||||
window.set_cursor_style(CursorStyle::IBeam, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::IBeam, Some(&hitbox));
|
||||
} else {
|
||||
window.set_cursor_style(CursorStyle::PointingHand, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(&hitbox));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4565,7 +4570,7 @@ impl EditorElement {
|
||||
.read(cx)
|
||||
.all_diff_hunks_expanded()
|
||||
{
|
||||
window.set_cursor_style(CursorStyle::PointingHand, hunk_hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(hunk_hitbox));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -4638,18 +4643,23 @@ impl EditorElement {
|
||||
}),
|
||||
|window| {
|
||||
let editor = self.editor.read(cx);
|
||||
let cursor_style = if editor.mouse_cursor_hidden {
|
||||
CursorStyle::None
|
||||
if editor.mouse_cursor_hidden {
|
||||
window.set_cursor_style(CursorStyle::None, None);
|
||||
} else if editor
|
||||
.hovered_link_state
|
||||
.as_ref()
|
||||
.is_some_and(|hovered_link_state| !hovered_link_state.links.is_empty())
|
||||
{
|
||||
CursorStyle::PointingHand
|
||||
window.set_cursor_style(
|
||||
CursorStyle::PointingHand,
|
||||
Some(&layout.position_map.text_hitbox),
|
||||
);
|
||||
} else {
|
||||
CursorStyle::IBeam
|
||||
window.set_cursor_style(
|
||||
CursorStyle::IBeam,
|
||||
Some(&layout.position_map.text_hitbox),
|
||||
);
|
||||
};
|
||||
window.set_cursor_style(cursor_style, &layout.position_map.text_hitbox);
|
||||
|
||||
self.paint_lines_background(layout, window, cx);
|
||||
let invisible_display_ranges = self.paint_highlights(layout, window);
|
||||
@@ -4844,7 +4854,7 @@ impl EditorElement {
|
||||
));
|
||||
})
|
||||
}
|
||||
window.set_cursor_style(CursorStyle::Arrow, &hitbox);
|
||||
window.set_cursor_style(CursorStyle::Arrow, Some(&hitbox));
|
||||
}
|
||||
|
||||
window.on_mouse_event({
|
||||
|
||||
@@ -150,7 +150,7 @@ impl GitBlame {
|
||||
this.generate(cx);
|
||||
}
|
||||
}
|
||||
project::Event::WorktreeUpdatedGitRepositories(_) => {
|
||||
project::Event::GitStateUpdated => {
|
||||
log::debug!("Status of git repositories updated. Regenerating blame data...",);
|
||||
this.generate(cx);
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
use crate::actions::FormatSelections;
|
||||
use crate::CopyAndTrim;
|
||||
use crate::{
|
||||
actions::Format, selections_collection::SelectionsCollection, Copy, CopyPermalinkToLine, Cut,
|
||||
DisplayPoint, DisplaySnapshot, Editor, EditorMode, FindAllReferences, GoToDeclaration,
|
||||
@@ -191,6 +192,7 @@ pub fn deploy_context_menu(
|
||||
.separator()
|
||||
.action("Cut", Box::new(Cut))
|
||||
.action("Copy", Box::new(Copy))
|
||||
.action("Copy and trim", Box::new(CopyAndTrim))
|
||||
.action("Paste", Box::new(Paste))
|
||||
.separator()
|
||||
.map(|builder| {
|
||||
|
||||
@@ -339,7 +339,8 @@ impl EditorTestContext {
|
||||
let mut found = None;
|
||||
fs.with_git_state(&Self::root_path().join(".git"), false, |git_state| {
|
||||
found = git_state.index_contents.get(path.as_ref()).cloned();
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
assert_eq!(expected, found.as_deref());
|
||||
}
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ async fn run_evaluation(
|
||||
let repos_dir = Path::new(EVAL_REPOS_DIR);
|
||||
let db_path = Path::new(EVAL_DB_PATH);
|
||||
let api_key = std::env::var("OPENAI_API_KEY").unwrap();
|
||||
let fs = Arc::new(RealFs::new(None)) as Arc<dyn Fs>;
|
||||
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone())) as Arc<dyn Fs>;
|
||||
let clock = Arc::new(RealSystemClock);
|
||||
let client = cx
|
||||
.update(|cx| {
|
||||
|
||||
@@ -18,6 +18,7 @@ clap = { workspace = true, features = ["derive"] }
|
||||
env_logger.workspace = true
|
||||
extension.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
reqwest_client.workspace = true
|
||||
|
||||
@@ -34,7 +34,7 @@ async fn main() -> Result<()> {
|
||||
env_logger::init();
|
||||
|
||||
let args = Args::parse();
|
||||
let fs = Arc::new(RealFs::default());
|
||||
let fs = Arc::new(RealFs::new(None, gpui::background_executor()));
|
||||
let engine = wasmtime::Engine::default();
|
||||
let mut wasm_store = WasmStore::new(&engine)?;
|
||||
|
||||
|
||||
@@ -477,7 +477,7 @@ async fn test_extension_store_with_test_extension(cx: &mut TestAppContext) {
|
||||
let test_extension_id = "test-extension";
|
||||
let test_extension_dir = root_dir.join("extensions").join(test_extension_id);
|
||||
|
||||
let fs = Arc::new(RealFs::default());
|
||||
let fs = Arc::new(RealFs::new(None, cx.executor()));
|
||||
let extensions_dir = TempTree::new(json!({
|
||||
"installed": {},
|
||||
"work": {}
|
||||
|
||||
@@ -90,13 +90,13 @@ pub fn authorize_access_to_unreleased_wasm_api_version(
|
||||
}
|
||||
|
||||
pub enum Extension {
|
||||
V040(since_v0_4_0::Extension),
|
||||
V030(since_v0_3_0::Extension),
|
||||
V020(since_v0_2_0::Extension),
|
||||
V010(since_v0_1_0::Extension),
|
||||
V006(since_v0_0_6::Extension),
|
||||
V004(since_v0_0_4::Extension),
|
||||
V001(since_v0_0_1::Extension),
|
||||
V0_4_0(since_v0_4_0::Extension),
|
||||
V0_3_0(since_v0_3_0::Extension),
|
||||
V0_2_0(since_v0_2_0::Extension),
|
||||
V0_1_0(since_v0_1_0::Extension),
|
||||
V0_0_6(since_v0_0_6::Extension),
|
||||
V0_0_4(since_v0_0_4::Extension),
|
||||
V0_0_1(since_v0_0_1::Extension),
|
||||
}
|
||||
|
||||
impl Extension {
|
||||
@@ -116,7 +116,7 @@ impl Extension {
|
||||
latest::Extension::instantiate_async(store, component, latest::linker())
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V040(extension))
|
||||
Ok(Self::V0_4_0(extension))
|
||||
} else if version >= since_v0_3_0::MIN_VERSION {
|
||||
let extension = since_v0_3_0::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -125,7 +125,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V030(extension))
|
||||
Ok(Self::V0_3_0(extension))
|
||||
} else if version >= since_v0_2_0::MIN_VERSION {
|
||||
let extension = since_v0_2_0::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -134,7 +134,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V020(extension))
|
||||
Ok(Self::V0_2_0(extension))
|
||||
} else if version >= since_v0_1_0::MIN_VERSION {
|
||||
let extension = since_v0_1_0::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -143,7 +143,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V010(extension))
|
||||
Ok(Self::V0_1_0(extension))
|
||||
} else if version >= since_v0_0_6::MIN_VERSION {
|
||||
let extension = since_v0_0_6::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -152,7 +152,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V006(extension))
|
||||
Ok(Self::V0_0_6(extension))
|
||||
} else if version >= since_v0_0_4::MIN_VERSION {
|
||||
let extension = since_v0_0_4::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -161,7 +161,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V004(extension))
|
||||
Ok(Self::V0_0_4(extension))
|
||||
} else {
|
||||
let extension = since_v0_0_1::Extension::instantiate_async(
|
||||
store,
|
||||
@@ -170,19 +170,19 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
.context("failed to instantiate wasm extension")?;
|
||||
Ok(Self::V001(extension))
|
||||
Ok(Self::V0_0_1(extension))
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
|
||||
match self {
|
||||
Extension::V040(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V030(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V020(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V010(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V006(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V004(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V001(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_4_0(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_3_0(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_2_0(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_1_0(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_0_6(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_0_4(ext) => ext.call_init_extension(store).await,
|
||||
Extension::V0_0_1(ext) => ext.call_init_extension(store).await,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -194,27 +194,27 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => Ok(ext
|
||||
Extension::V0_2_0(ext) => Ok(ext
|
||||
.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V010(ext) => Ok(ext
|
||||
Extension::V0_1_0(ext) => Ok(ext
|
||||
.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V006(ext) => Ok(ext
|
||||
Extension::V0_0_6(ext) => Ok(ext
|
||||
.call_language_server_command(store, &language_server_id.0, resource)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V004(ext) => Ok(ext
|
||||
Extension::V0_0_4(ext) => Ok(ext
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
@@ -225,7 +225,7 @@ impl Extension {
|
||||
)
|
||||
.await?
|
||||
.map(|command| command.into())),
|
||||
Extension::V001(ext) => Ok(ext
|
||||
Extension::V0_0_1(ext) => Ok(ext
|
||||
.call_language_server_command(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
@@ -248,7 +248,7 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -256,7 +256,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -264,7 +264,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -272,7 +272,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -280,7 +280,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => {
|
||||
Extension::V0_0_6(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -288,7 +288,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V004(ext) => {
|
||||
Extension::V0_0_4(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
@@ -299,7 +299,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V001(ext) => {
|
||||
Extension::V0_0_1(ext) => {
|
||||
ext.call_language_server_initialization_options(
|
||||
store,
|
||||
&LanguageServerConfig {
|
||||
@@ -321,7 +321,7 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -329,7 +329,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -337,7 +337,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -345,7 +345,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -353,7 +353,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V006(ext) => {
|
||||
Extension::V0_0_6(ext) => {
|
||||
ext.call_language_server_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -361,7 +361,7 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V004(_) | Extension::V001(_) => Ok(Ok(None)),
|
||||
Extension::V0_0_4(_) | Extension::V0_0_1(_) => Ok(Ok(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -373,7 +373,7 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_additional_initialization_options(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -382,12 +382,12 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V030(_)
|
||||
| Extension::V020(_)
|
||||
| Extension::V010(_)
|
||||
| Extension::V006(_)
|
||||
| Extension::V004(_)
|
||||
| Extension::V001(_) => Ok(Ok(None)),
|
||||
Extension::V0_3_0(_)
|
||||
| Extension::V0_2_0(_)
|
||||
| Extension::V0_1_0(_)
|
||||
| Extension::V0_0_6(_)
|
||||
| Extension::V0_0_4(_)
|
||||
| Extension::V0_0_1(_) => Ok(Ok(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -399,7 +399,7 @@ impl Extension {
|
||||
resource: Resource<Arc<dyn WorktreeDelegate>>,
|
||||
) -> Result<Result<Option<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_language_server_additional_workspace_configuration(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -408,12 +408,12 @@ impl Extension {
|
||||
)
|
||||
.await
|
||||
}
|
||||
Extension::V030(_)
|
||||
| Extension::V020(_)
|
||||
| Extension::V010(_)
|
||||
| Extension::V006(_)
|
||||
| Extension::V004(_)
|
||||
| Extension::V001(_) => Ok(Ok(None)),
|
||||
Extension::V0_3_0(_)
|
||||
| Extension::V0_2_0(_)
|
||||
| Extension::V0_1_0(_)
|
||||
| Extension::V0_0_6(_)
|
||||
| Extension::V0_0_4(_)
|
||||
| Extension::V0_0_1(_) => Ok(Ok(None)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -424,11 +424,11 @@ impl Extension {
|
||||
completions: Vec<latest::Completion>,
|
||||
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => Ok(ext
|
||||
Extension::V0_3_0(ext) => Ok(ext
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -441,7 +441,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V020(ext) => Ok(ext
|
||||
Extension::V0_2_0(ext) => Ok(ext
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -454,7 +454,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V010(ext) => Ok(ext
|
||||
Extension::V0_1_0(ext) => Ok(ext
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -467,7 +467,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V006(ext) => Ok(ext
|
||||
Extension::V0_0_6(ext) => Ok(ext
|
||||
.call_labels_for_completions(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -480,7 +480,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -491,11 +491,11 @@ impl Extension {
|
||||
symbols: Vec<latest::Symbol>,
|
||||
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => Ok(ext
|
||||
Extension::V0_3_0(ext) => Ok(ext
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -508,7 +508,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V020(ext) => Ok(ext
|
||||
Extension::V0_2_0(ext) => Ok(ext
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -521,7 +521,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V010(ext) => Ok(ext
|
||||
Extension::V0_1_0(ext) => Ok(ext
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -534,7 +534,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V006(ext) => Ok(ext
|
||||
Extension::V0_0_6(ext) => Ok(ext
|
||||
.call_labels_for_symbols(
|
||||
store,
|
||||
&language_server_id.0,
|
||||
@@ -547,7 +547,7 @@ impl Extension {
|
||||
.map(|label| label.map(Into::into))
|
||||
.collect()
|
||||
})),
|
||||
Extension::V001(_) | Extension::V004(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) => Ok(Ok(Vec::new())),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -558,23 +558,25 @@ impl Extension {
|
||||
arguments: &[String],
|
||||
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, arguments)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, arguments)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, arguments)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_complete_slash_command_argument(store, command, arguments)
|
||||
.await
|
||||
}
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Ok(Ok(Vec::new())),
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
|
||||
Ok(Ok(Vec::new()))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -586,23 +588,23 @@ impl Extension {
|
||||
resource: Option<Resource<Arc<dyn WorktreeDelegate>>>,
|
||||
) -> Result<Result<SlashCommandOutput, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_run_slash_command(store, command, arguments, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_run_slash_command(store, command, arguments, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_run_slash_command(store, command, arguments, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_run_slash_command(store, command, arguments, resource)
|
||||
.await
|
||||
}
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
|
||||
Err(anyhow!("`run_slash_command` not available prior to v0.1.0"))
|
||||
}
|
||||
}
|
||||
@@ -615,23 +617,24 @@ impl Extension {
|
||||
project: Resource<ExtensionProject>,
|
||||
) -> Result<Result<Command, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_context_server_command(store, &context_server_id, project)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_context_server_command(store, &context_server_id, project)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => Ok(ext
|
||||
Extension::V0_2_0(ext) => Ok(ext
|
||||
.call_context_server_command(store, &context_server_id, project)
|
||||
.await?
|
||||
.map(Into::into)),
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) | Extension::V010(_) => {
|
||||
Err(anyhow!(
|
||||
"`context_server_command` not available prior to v0.2.0"
|
||||
))
|
||||
}
|
||||
Extension::V0_0_1(_)
|
||||
| Extension::V0_0_4(_)
|
||||
| Extension::V0_0_6(_)
|
||||
| Extension::V0_1_0(_) => Err(anyhow!(
|
||||
"`context_server_command` not available prior to v0.2.0"
|
||||
)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -641,11 +644,11 @@ impl Extension {
|
||||
provider: &str,
|
||||
) -> Result<Result<Vec<String>, String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V030(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V020(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V010(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => Err(anyhow!(
|
||||
Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V0_3_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V0_2_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V0_1_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => Err(anyhow!(
|
||||
"`suggest_docs_packages` not available prior to v0.1.0"
|
||||
)),
|
||||
}
|
||||
@@ -659,23 +662,23 @@ impl Extension {
|
||||
kv_store: Resource<Arc<dyn KeyValueStoreDelegate>>,
|
||||
) -> Result<Result<(), String>> {
|
||||
match self {
|
||||
Extension::V040(ext) => {
|
||||
Extension::V0_4_0(ext) => {
|
||||
ext.call_index_docs(store, provider, package_name, kv_store)
|
||||
.await
|
||||
}
|
||||
Extension::V030(ext) => {
|
||||
Extension::V0_3_0(ext) => {
|
||||
ext.call_index_docs(store, provider, package_name, kv_store)
|
||||
.await
|
||||
}
|
||||
Extension::V020(ext) => {
|
||||
Extension::V0_2_0(ext) => {
|
||||
ext.call_index_docs(store, provider, package_name, kv_store)
|
||||
.await
|
||||
}
|
||||
Extension::V010(ext) => {
|
||||
Extension::V0_1_0(ext) => {
|
||||
ext.call_index_docs(store, provider, package_name, kv_store)
|
||||
.await
|
||||
}
|
||||
Extension::V001(_) | Extension::V004(_) | Extension::V006(_) => {
|
||||
Extension::V0_0_1(_) | Extension::V0_0_4(_) | Extension::V0_0_6(_) => {
|
||||
Err(anyhow!("`index_docs` not available prior to v0.1.0"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ objc = "0.2"
|
||||
cocoa = "0.26"
|
||||
|
||||
[target.'cfg(not(target_os = "macos"))'.dependencies]
|
||||
notify = "6.1.1"
|
||||
notify = { git = "https://github.com/zed-industries/notify.git", rev = "bbb9ea5ae52b253e095737847e367c30653a2e96" }
|
||||
|
||||
[target.'cfg(target_os = "windows")'.dependencies]
|
||||
windows.workspace = true
|
||||
|
||||
@@ -5,8 +5,8 @@ use futures::future::{self, BoxFuture};
|
||||
use git::{
|
||||
blame::Blame,
|
||||
repository::{
|
||||
AskPassSession, Branch, CommitDetails, GitRepository, GitRepositoryCheckpoint, PushOptions,
|
||||
Remote, RepoPath, ResetMode,
|
||||
AskPassSession, Branch, CommitDetails, GitIndex, GitRepository, GitRepositoryCheckpoint,
|
||||
PushOptions, Remote, RepoPath, ResetMode,
|
||||
},
|
||||
status::{FileStatus, GitStatus, StatusCode, TrackedStatus, UnmergedStatus},
|
||||
};
|
||||
@@ -57,12 +57,14 @@ impl FakeGitRepository {
|
||||
where
|
||||
F: FnOnce(&mut FakeGitRepositoryState) -> T,
|
||||
{
|
||||
self.fs.with_git_state(&self.dot_git_path, false, f)
|
||||
self.fs
|
||||
.with_git_state(&self.dot_git_path, false, f)
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<T>
|
||||
fn with_state_async<F, T>(&self, write: bool, f: F) -> BoxFuture<'static, Result<T>>
|
||||
where
|
||||
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> T,
|
||||
F: 'static + Send + FnOnce(&mut FakeGitRepositoryState) -> Result<T>,
|
||||
T: Send,
|
||||
{
|
||||
let fs = self.fs.clone();
|
||||
@@ -70,7 +72,7 @@ impl FakeGitRepository {
|
||||
let dot_git_path = self.dot_git_path.clone();
|
||||
async move {
|
||||
executor.simulate_random_delay().await;
|
||||
fs.with_git_state(&dot_git_path, write, f)
|
||||
fs.with_git_state(&dot_git_path, write, f)?
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
@@ -79,16 +81,38 @@ impl FakeGitRepository {
|
||||
impl GitRepository for FakeGitRepository {
|
||||
fn reload_index(&self) {}
|
||||
|
||||
fn load_index_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture<Option<String>> {
|
||||
self.with_state_async(false, move |state| {
|
||||
state.index_contents.get(path.as_ref()).cloned()
|
||||
})
|
||||
fn load_index_text(
|
||||
&self,
|
||||
index: Option<GitIndex>,
|
||||
path: RepoPath,
|
||||
) -> BoxFuture<Option<String>> {
|
||||
async {
|
||||
self.with_state_async(false, move |state| {
|
||||
state
|
||||
.index_contents
|
||||
.get(path.as_ref())
|
||||
.ok_or_else(|| anyhow!("not present in index"))
|
||||
.cloned()
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn load_committed_text(&self, path: RepoPath, _cx: AsyncApp) -> BoxFuture<Option<String>> {
|
||||
self.with_state_async(false, move |state| {
|
||||
state.head_contents.get(path.as_ref()).cloned()
|
||||
})
|
||||
fn load_committed_text(&self, path: RepoPath) -> BoxFuture<Option<String>> {
|
||||
async {
|
||||
self.with_state_async(false, move |state| {
|
||||
state
|
||||
.head_contents
|
||||
.get(path.as_ref())
|
||||
.ok_or_else(|| anyhow!("not present in HEAD"))
|
||||
.cloned()
|
||||
})
|
||||
.await
|
||||
.ok()
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
fn set_index_text(
|
||||
@@ -96,7 +120,6 @@ impl GitRepository for FakeGitRepository {
|
||||
path: RepoPath,
|
||||
content: Option<String>,
|
||||
_env: HashMap<String, String>,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<anyhow::Result<()>> {
|
||||
self.with_state_async(true, move |state| {
|
||||
if let Some(message) = state.simulated_index_write_error_message.clone() {
|
||||
@@ -122,7 +145,7 @@ impl GitRepository for FakeGitRepository {
|
||||
vec![]
|
||||
}
|
||||
|
||||
fn show(&self, _commit: String, _cx: AsyncApp) -> BoxFuture<Result<CommitDetails>> {
|
||||
fn show(&self, _commit: String) -> BoxFuture<Result<CommitDetails>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -152,7 +175,16 @@ impl GitRepository for FakeGitRepository {
|
||||
self.path()
|
||||
}
|
||||
|
||||
fn status(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||
fn status(
|
||||
&self,
|
||||
index: Option<GitIndex>,
|
||||
path_prefixes: &[RepoPath],
|
||||
) -> BoxFuture<'static, Result<GitStatus>> {
|
||||
let status = self.status_blocking(path_prefixes);
|
||||
async move { status }.boxed()
|
||||
}
|
||||
|
||||
fn status_blocking(&self, path_prefixes: &[RepoPath]) -> Result<GitStatus> {
|
||||
let workdir_path = self.dot_git_path.parent().unwrap();
|
||||
|
||||
// Load gitignores
|
||||
@@ -194,7 +226,7 @@ impl GitRepository for FakeGitRepository {
|
||||
})
|
||||
.collect();
|
||||
|
||||
self.with_state(|state| {
|
||||
self.fs.with_git_state(&self.dot_git_path, false, |state| {
|
||||
let mut entries = Vec::new();
|
||||
let paths = state
|
||||
.head_contents
|
||||
@@ -278,7 +310,7 @@ impl GitRepository for FakeGitRepository {
|
||||
Ok(GitStatus {
|
||||
entries: entries.into(),
|
||||
})
|
||||
})
|
||||
})?
|
||||
}
|
||||
|
||||
fn branches(&self) -> BoxFuture<Result<Vec<Branch>>> {
|
||||
@@ -297,26 +329,21 @@ impl GitRepository for FakeGitRepository {
|
||||
})
|
||||
}
|
||||
|
||||
fn change_branch(&self, name: String, _cx: AsyncApp) -> BoxFuture<Result<()>> {
|
||||
fn change_branch(&self, name: String) -> BoxFuture<Result<()>> {
|
||||
self.with_state_async(true, |state| {
|
||||
state.current_branch_name = Some(name);
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn create_branch(&self, name: String, _: AsyncApp) -> BoxFuture<Result<()>> {
|
||||
fn create_branch(&self, name: String) -> BoxFuture<Result<()>> {
|
||||
self.with_state_async(true, move |state| {
|
||||
state.branches.insert(name.to_owned());
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
|
||||
fn blame(
|
||||
&self,
|
||||
path: RepoPath,
|
||||
_content: Rope,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> BoxFuture<Result<git::blame::Blame>> {
|
||||
fn blame(&self, path: RepoPath, _content: Rope) -> BoxFuture<Result<git::blame::Blame>> {
|
||||
self.with_state_async(false, move |state| {
|
||||
state
|
||||
.blames
|
||||
@@ -330,7 +357,6 @@ impl GitRepository for FakeGitRepository {
|
||||
&self,
|
||||
_paths: Vec<RepoPath>,
|
||||
_env: HashMap<String, String>,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -339,7 +365,6 @@ impl GitRepository for FakeGitRepository {
|
||||
&self,
|
||||
_paths: Vec<RepoPath>,
|
||||
_env: HashMap<String, String>,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -349,7 +374,6 @@ impl GitRepository for FakeGitRepository {
|
||||
_message: gpui::SharedString,
|
||||
_name_and_email: Option<(gpui::SharedString, gpui::SharedString)>,
|
||||
_env: HashMap<String, String>,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
@@ -386,38 +410,23 @@ impl GitRepository for FakeGitRepository {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn get_remotes(
|
||||
&self,
|
||||
_branch: Option<String>,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<Vec<Remote>>> {
|
||||
fn get_remotes(&self, _branch: Option<String>) -> BoxFuture<Result<Vec<Remote>>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn check_for_pushed_commit(
|
||||
&self,
|
||||
_cx: gpui::AsyncApp,
|
||||
) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
|
||||
fn check_for_pushed_commit(&self) -> BoxFuture<Result<Vec<gpui::SharedString>>> {
|
||||
future::ready(Ok(Vec::new())).boxed()
|
||||
}
|
||||
|
||||
fn diff(
|
||||
&self,
|
||||
_diff: git::repository::DiffType,
|
||||
_cx: gpui::AsyncApp,
|
||||
) -> BoxFuture<Result<String>> {
|
||||
fn diff(&self, _diff: git::repository::DiffType) -> BoxFuture<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn checkpoint(&self, _cx: AsyncApp) -> BoxFuture<Result<GitRepositoryCheckpoint>> {
|
||||
fn checkpoint(&self) -> BoxFuture<'static, Result<GitRepositoryCheckpoint>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn restore_checkpoint(
|
||||
&self,
|
||||
_checkpoint: GitRepositoryCheckpoint,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
fn restore_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
@@ -425,16 +434,27 @@ impl GitRepository for FakeGitRepository {
|
||||
&self,
|
||||
_left: GitRepositoryCheckpoint,
|
||||
_right: GitRepositoryCheckpoint,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<bool>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn delete_checkpoint(
|
||||
fn delete_checkpoint(&self, _checkpoint: GitRepositoryCheckpoint) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn diff_checkpoints(
|
||||
&self,
|
||||
_checkpoint: GitRepositoryCheckpoint,
|
||||
_cx: AsyncApp,
|
||||
) -> BoxFuture<Result<()>> {
|
||||
_base_checkpoint: GitRepositoryCheckpoint,
|
||||
_target_checkpoint: GitRepositoryCheckpoint,
|
||||
) -> BoxFuture<Result<String>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn create_index(&self) -> BoxFuture<Result<GitIndex>> {
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
fn apply_diff(&self, _index: GitIndex, _diff: String) -> BoxFuture<Result<()>> {
|
||||
unimplemented!()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
use ashpd::desktop::trash;
|
||||
use gpui::App;
|
||||
use gpui::BackgroundExecutor;
|
||||
use gpui::Global;
|
||||
use gpui::ReadGlobal as _;
|
||||
use std::borrow::Cow;
|
||||
@@ -240,9 +241,9 @@ impl From<MTime> for proto::Timestamp {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct RealFs {
|
||||
git_binary_path: Option<PathBuf>,
|
||||
executor: BackgroundExecutor,
|
||||
}
|
||||
|
||||
pub trait FileHandle: Send + Sync + std::fmt::Debug {
|
||||
@@ -294,8 +295,11 @@ impl FileHandle for std::fs::File {
|
||||
pub struct RealWatcher {}
|
||||
|
||||
impl RealFs {
|
||||
pub fn new(git_binary_path: Option<PathBuf>) -> Self {
|
||||
Self { git_binary_path }
|
||||
pub fn new(git_binary_path: Option<PathBuf>, executor: BackgroundExecutor) -> Self {
|
||||
Self {
|
||||
git_binary_path,
|
||||
executor,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -754,6 +758,7 @@ impl Fs for RealFs {
|
||||
Some(Arc::new(RealGitRepository::new(
|
||||
dotgit_path,
|
||||
self.git_binary_path.clone(),
|
||||
self.executor.clone(),
|
||||
)?))
|
||||
}
|
||||
|
||||
@@ -1248,12 +1253,12 @@ impl FakeFs {
|
||||
.boxed()
|
||||
}
|
||||
|
||||
pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> T
|
||||
pub fn with_git_state<T, F>(&self, dot_git: &Path, emit_git_event: bool, f: F) -> Result<T>
|
||||
where
|
||||
F: FnOnce(&mut FakeGitRepositoryState) -> T,
|
||||
{
|
||||
let mut state = self.state.lock();
|
||||
let entry = state.read_path(dot_git).unwrap();
|
||||
let entry = state.read_path(dot_git).context("open .git")?;
|
||||
let mut entry = entry.lock();
|
||||
|
||||
if let FakeFsEntry::Dir { git_repo_state, .. } = &mut *entry {
|
||||
@@ -1271,9 +1276,9 @@ impl FakeFs {
|
||||
state.emit_event([(dot_git, None)]);
|
||||
}
|
||||
|
||||
result
|
||||
Ok(result)
|
||||
} else {
|
||||
panic!("not a directory");
|
||||
Err(anyhow!("not a directory"))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1283,6 +1288,7 @@ impl FakeFs {
|
||||
state.branches.extend(branch.clone());
|
||||
state.current_branch_name = branch
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn insert_branches(&self, dot_git: &Path, branches: &[&str]) {
|
||||
@@ -1296,6 +1302,7 @@ impl FakeFs {
|
||||
.branches
|
||||
.extend(branches.iter().map(ToString::to_string));
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_unmerged_paths_for_repo(
|
||||
@@ -1310,7 +1317,8 @@ impl FakeFs {
|
||||
.iter()
|
||||
.map(|(path, content)| (path.clone(), *content)),
|
||||
);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_index_for_repo(&self, dot_git: &Path, index_state: &[(RepoPath, String)]) {
|
||||
@@ -1321,7 +1329,8 @@ impl FakeFs {
|
||||
.iter()
|
||||
.map(|(path, content)| (path.clone(), content.clone())),
|
||||
);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_head_for_repo(&self, dot_git: &Path, head_state: &[(RepoPath, String)]) {
|
||||
@@ -1332,7 +1341,8 @@ impl FakeFs {
|
||||
.iter()
|
||||
.map(|(path, content)| (path.clone(), content.clone())),
|
||||
);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_git_content_for_repo(
|
||||
@@ -1356,7 +1366,8 @@ impl FakeFs {
|
||||
)
|
||||
},
|
||||
));
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_head_and_index_for_repo(
|
||||
@@ -1371,14 +1382,16 @@ impl FakeFs {
|
||||
state
|
||||
.index_contents
|
||||
.extend(contents_by_path.iter().cloned());
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn set_blame_for_repo(&self, dot_git: &Path, blames: Vec<(RepoPath, git::blame::Blame)>) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.blames.clear();
|
||||
state.blames.extend(blames);
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
/// Put the given git repository into a state with the given status,
|
||||
@@ -1460,13 +1473,14 @@ impl FakeFs {
|
||||
state.head_contents.insert(repo_path.clone(), content);
|
||||
}
|
||||
}
|
||||
});
|
||||
}).unwrap();
|
||||
}
|
||||
|
||||
pub fn set_error_message_for_index_write(&self, dot_git: &Path, message: Option<String>) {
|
||||
self.with_git_state(dot_git, true, |state| {
|
||||
state.simulated_index_write_error_message = message;
|
||||
});
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
|
||||
pub fn paths(&self, include_dot_git: bool) -> Vec<PathBuf> {
|
||||
|
||||
@@ -105,7 +105,14 @@ static FS_WATCHER_INSTANCE: OnceLock<anyhow::Result<GlobalWatcher, notify::Error
|
||||
OnceLock::new();
|
||||
|
||||
fn handle_event(event: Result<notify::Event, notify::Error>) {
|
||||
let Some(event) = event.log_err() else { return };
|
||||
// Filter out access events, which could lead to a weird bug on Linux after upgrading notify
|
||||
// https://github.com/zed-industries/zed/actions/runs/14085230504/job/39449448832
|
||||
let Some(event) = event
|
||||
.log_err()
|
||||
.filter(|event| !matches!(event.kind, EventKind::Access(_)))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
global::<()>(move |watcher| {
|
||||
for f in watcher.watchers.lock().iter() {
|
||||
f(&event)
|
||||
|
||||
@@ -11,7 +11,9 @@ use anyhow::{anyhow, Context as _, Result};
|
||||
pub use git2 as libgit;
|
||||
use gpui::action_with_deprecated_aliases;
|
||||
use gpui::actions;
|
||||
use gpui::impl_action_with_deprecated_aliases;
|
||||
pub use repository::WORK_DIRECTORY_REPO_PATH;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::ffi::OsStr;
|
||||
use std::fmt;
|
||||
@@ -54,7 +56,13 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema)]
|
||||
pub struct RestoreFile {
|
||||
#[serde(default)]
|
||||
pub skip_prompt: bool,
|
||||
}
|
||||
|
||||
impl_action_with_deprecated_aliases!(git, RestoreFile, ["editor::RevertFile"]);
|
||||
action_with_deprecated_aliases!(git, Restore, ["editor::RevertSelectedHunks"]);
|
||||
action_with_deprecated_aliases!(git, Blame, ["editor::ToggleGitBlame"]);
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
use crate::repository::RepoPath;
|
||||
use anyhow::{anyhow, Result};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{path::Path, process::Stdio, sync::Arc};
|
||||
use std::{path::Path, str::FromStr, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
|
||||
@@ -438,50 +438,16 @@ impl std::ops::Sub for GitSummary {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct GitStatus {
|
||||
pub entries: Arc<[(RepoPath, FileStatus)]>,
|
||||
}
|
||||
|
||||
impl GitStatus {
|
||||
pub(crate) fn new(
|
||||
git_binary: &Path,
|
||||
working_directory: &Path,
|
||||
path_prefixes: &[RepoPath],
|
||||
) -> Result<Self> {
|
||||
let child = util::command::new_std_command(git_binary)
|
||||
.current_dir(working_directory)
|
||||
.args([
|
||||
"--no-optional-locks",
|
||||
"status",
|
||||
"--porcelain=v1",
|
||||
"--untracked-files=all",
|
||||
"--no-renames",
|
||||
"-z",
|
||||
])
|
||||
.args(path_prefixes.iter().map(|path_prefix| {
|
||||
if path_prefix.0.as_ref() == Path::new("") {
|
||||
Path::new(".")
|
||||
} else {
|
||||
path_prefix
|
||||
}
|
||||
}))
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::piped())
|
||||
.spawn()
|
||||
.map_err(|e| anyhow!("Failed to start git status process: {e}"))?;
|
||||
impl FromStr for GitStatus {
|
||||
type Err = anyhow::Error;
|
||||
|
||||
let output = child
|
||||
.wait_with_output()
|
||||
.map_err(|e| anyhow!("Failed to read git status output: {e}"))?;
|
||||
|
||||
if !output.status.success() {
|
||||
let stderr = String::from_utf8_lossy(&output.stderr);
|
||||
return Err(anyhow!("git status process failed: {stderr}"));
|
||||
}
|
||||
let stdout = String::from_utf8_lossy(&output.stdout);
|
||||
let mut entries = stdout
|
||||
fn from_str(s: &str) -> Result<Self> {
|
||||
let mut entries = s
|
||||
.split('\0')
|
||||
.filter_map(|entry| {
|
||||
let sep = entry.get(2..3)?;
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::commit_modal::CommitModal;
|
||||
use crate::git_panel_settings::StatusStyle;
|
||||
use crate::project_diff::Diff;
|
||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||
use crate::repository_selector::filtered_repository_entries;
|
||||
|
||||
use crate::{branch_picker, render_remote_button};
|
||||
use crate::{
|
||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||
@@ -63,7 +63,7 @@ use ui::{
|
||||
Tooltip,
|
||||
};
|
||||
use util::{maybe, post_inc, ResultExt, TryFutureExt};
|
||||
use workspace::{AppState, OpenOptions, OpenVisible};
|
||||
use workspace::AppState;
|
||||
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use workspace::{
|
||||
@@ -195,7 +195,6 @@ impl GitListEntry {
|
||||
#[derive(Debug, PartialEq, Eq, Clone)]
|
||||
pub struct GitStatusEntry {
|
||||
pub(crate) repo_path: RepoPath,
|
||||
pub(crate) worktree_path: Arc<Path>,
|
||||
pub(crate) abs_path: PathBuf,
|
||||
pub(crate) status: FileStatus,
|
||||
pub(crate) staging: StageStatus,
|
||||
@@ -203,14 +202,14 @@ pub struct GitStatusEntry {
|
||||
|
||||
impl GitStatusEntry {
|
||||
fn display_name(&self) -> String {
|
||||
self.worktree_path
|
||||
self.repo_path
|
||||
.file_name()
|
||||
.map(|name| name.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| self.worktree_path.to_string_lossy().into_owned())
|
||||
.unwrap_or_else(|| self.repo_path.to_string_lossy().into_owned())
|
||||
}
|
||||
|
||||
fn parent_dir(&self) -> Option<String> {
|
||||
self.worktree_path
|
||||
self.repo_path
|
||||
.parent()
|
||||
.map(|parent| parent.to_string_lossy().into_owned())
|
||||
}
|
||||
@@ -652,7 +651,7 @@ impl GitPanel {
|
||||
let Some(git_repo) = self.active_repository.as_ref() else {
|
||||
return;
|
||||
};
|
||||
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path) else {
|
||||
let Some(repo_path) = git_repo.read(cx).project_path_to_repo_path(&path, cx) else {
|
||||
return;
|
||||
};
|
||||
let Some(ix) = self.entry_by_path(&repo_path) else {
|
||||
@@ -865,7 +864,7 @@ impl GitPanel {
|
||||
if Some(&entry.repo_path)
|
||||
== git_repo
|
||||
.read(cx)
|
||||
.project_path_to_repo_path(&project_path)
|
||||
.project_path_to_repo_path(&project_path, cx)
|
||||
.as_ref()
|
||||
{
|
||||
project_diff.focus_handle(cx).focus(window);
|
||||
@@ -875,31 +874,12 @@ impl GitPanel {
|
||||
}
|
||||
};
|
||||
|
||||
if entry.worktree_path.starts_with("..") {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_abs_path(
|
||||
entry.abs_path.clone(),
|
||||
OpenOptions {
|
||||
visible: Some(OpenVisible::All),
|
||||
focus: Some(false),
|
||||
..Default::default()
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
.ok();
|
||||
} else {
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
|
||||
})
|
||||
.ok();
|
||||
self.focus_handle.focus(window);
|
||||
}
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
ProjectDiff::deploy_at(workspace, Some(entry.clone()), window, cx);
|
||||
})
|
||||
.ok();
|
||||
self.focus_handle.focus(window);
|
||||
|
||||
Some(())
|
||||
});
|
||||
@@ -916,7 +896,7 @@ impl GitPanel {
|
||||
let active_repo = self.active_repository.as_ref()?;
|
||||
let path = active_repo
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path)?;
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
if entry.status.is_deleted() {
|
||||
return None;
|
||||
}
|
||||
@@ -935,14 +915,49 @@ impl GitPanel {
|
||||
|
||||
fn revert_selected(
|
||||
&mut self,
|
||||
_: &git::RestoreFile,
|
||||
action: &git::RestoreFile,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
maybe!({
|
||||
let skip_prompt = action.skip_prompt;
|
||||
let list_entry = self.entries.get(self.selected_entry?)?.clone();
|
||||
let entry = list_entry.status_entry()?;
|
||||
self.revert_entry(&entry, window, cx);
|
||||
let entry = list_entry.status_entry()?.to_owned();
|
||||
|
||||
let prompt = if skip_prompt {
|
||||
Task::ready(Ok(0))
|
||||
} else {
|
||||
let prompt = window.prompt(
|
||||
PromptLevel::Warning,
|
||||
&format!(
|
||||
"Are you sure you want to restore {}?",
|
||||
entry
|
||||
.repo_path
|
||||
.file_name()
|
||||
.unwrap_or(entry.repo_path.as_os_str())
|
||||
.to_string_lossy()
|
||||
),
|
||||
None,
|
||||
&["Restore", "Cancel"],
|
||||
cx,
|
||||
);
|
||||
cx.background_spawn(prompt)
|
||||
};
|
||||
|
||||
let this = cx.weak_entity();
|
||||
window
|
||||
.spawn(cx, async move |cx| {
|
||||
if prompt.await? != 0 {
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.revert_entry(&entry, window, cx);
|
||||
})?;
|
||||
|
||||
Ok(())
|
||||
})
|
||||
.detach();
|
||||
Some(())
|
||||
});
|
||||
}
|
||||
@@ -957,7 +972,7 @@ impl GitPanel {
|
||||
let active_repo = self.active_repository.clone()?;
|
||||
let path = active_repo
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path)?;
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
let workspace = self.workspace.clone();
|
||||
|
||||
if entry.status.staging().has_staged() {
|
||||
@@ -1017,7 +1032,7 @@ impl GitPanel {
|
||||
.filter_map(|entry| {
|
||||
let path = active_repository
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path)?;
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
Some(project.open_buffer(path, cx))
|
||||
})
|
||||
.collect()
|
||||
@@ -1183,7 +1198,7 @@ impl GitPanel {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
let project_path = active_repo
|
||||
.read(cx)
|
||||
.repo_path_to_project_path(&entry.repo_path)?;
|
||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
||||
project.delete_file(project_path, true, cx)
|
||||
})
|
||||
})
|
||||
@@ -2244,7 +2259,7 @@ impl GitPanel {
|
||||
|
||||
let repo = repo.read(cx);
|
||||
|
||||
for entry in repo.status() {
|
||||
for entry in repo.cached_status() {
|
||||
let is_conflict = repo.has_conflict(&entry.repo_path);
|
||||
let is_new = entry.status.is_created();
|
||||
let staging = entry.status.staging();
|
||||
@@ -2260,16 +2275,12 @@ impl GitPanel {
|
||||
continue;
|
||||
}
|
||||
|
||||
// dot_git_abs path always has at least one component, namely .git.
|
||||
let abs_path = repo
|
||||
.dot_git_abs_path
|
||||
.parent()
|
||||
.unwrap()
|
||||
.join(&entry.repo_path);
|
||||
let worktree_path = repo.repository_entry.unrelativize(&entry.repo_path);
|
||||
.repository_entry
|
||||
.work_directory_abs_path
|
||||
.join(&entry.repo_path.0);
|
||||
let entry = GitStatusEntry {
|
||||
repo_path: entry.repo_path.clone(),
|
||||
worktree_path,
|
||||
abs_path,
|
||||
status: entry.status,
|
||||
staging,
|
||||
@@ -2495,7 +2506,6 @@ impl GitPanel {
|
||||
{
|
||||
return; // Hide the cancelled by user message
|
||||
} else {
|
||||
let project = self.project.clone();
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let workspace_weak = cx.weak_entity();
|
||||
let toast =
|
||||
@@ -2503,13 +2513,10 @@ impl GitPanel {
|
||||
this.icon(ToastIcon::new(IconName::XCircle).color(Color::Error))
|
||||
.action("View Log", move |window, cx| {
|
||||
let message = message.clone();
|
||||
let project = project.clone();
|
||||
let action = action.clone();
|
||||
workspace_weak
|
||||
.update(cx, move |workspace, cx| {
|
||||
Self::open_output(
|
||||
project, action, workspace, &message, window, cx,
|
||||
)
|
||||
Self::open_output(action, workspace, &message, window, cx)
|
||||
})
|
||||
.ok();
|
||||
})
|
||||
@@ -2531,21 +2538,17 @@ impl GitPanel {
|
||||
|
||||
let status_toast = StatusToast::new(message, cx, move |this, _cx| {
|
||||
use remote_output::SuccessStyle::*;
|
||||
let project = self.project.clone();
|
||||
match style {
|
||||
Toast { .. } => this,
|
||||
ToastWithLog { output } => this
|
||||
.icon(ToastIcon::new(IconName::GitBranchSmall).color(Color::Muted))
|
||||
.action("View Log", move |window, cx| {
|
||||
let output = output.clone();
|
||||
let project = project.clone();
|
||||
let output =
|
||||
format!("stdout:\n{}\nstderr:\n{}", output.stdout, output.stderr);
|
||||
workspace_weak
|
||||
.update(cx, move |workspace, cx| {
|
||||
Self::open_output(
|
||||
project, operation, workspace, &output, window, cx,
|
||||
)
|
||||
Self::open_output(operation, workspace, &output, window, cx)
|
||||
})
|
||||
.ok();
|
||||
}),
|
||||
@@ -2559,7 +2562,6 @@ impl GitPanel {
|
||||
}
|
||||
|
||||
fn open_output(
|
||||
project: Entity<Project>,
|
||||
operation: impl Into<SharedString>,
|
||||
workspace: &mut Workspace,
|
||||
output: &str,
|
||||
@@ -2568,8 +2570,11 @@ impl GitPanel {
|
||||
) {
|
||||
let operation = operation.into();
|
||||
let buffer = cx.new(|cx| Buffer::local(output, cx));
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_capability(language::Capability::ReadOnly, cx);
|
||||
});
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::for_buffer(buffer, Some(project), window, cx);
|
||||
let mut editor = Editor::for_buffer(buffer, None, window, cx);
|
||||
editor.buffer().update(cx, |buffer, cx| {
|
||||
buffer.set_title(format!("Output from git {operation}"), cx);
|
||||
});
|
||||
@@ -2854,7 +2859,6 @@ impl GitPanel {
|
||||
) -> Option<impl IntoElement> {
|
||||
let active_repository = self.active_repository.clone()?;
|
||||
let (can_commit, tooltip) = self.configure_commit_button(cx);
|
||||
let project = self.project.clone().read(cx);
|
||||
let panel_editor_style = panel_editor_style(true, window, cx);
|
||||
|
||||
let enable_coauthors = self.render_co_authors(cx);
|
||||
@@ -2878,7 +2882,7 @@ impl GitPanel {
|
||||
let display_name = SharedString::from(Arc::from(
|
||||
active_repository
|
||||
.read(cx)
|
||||
.display_name(project, cx)
|
||||
.display_name()
|
||||
.trim_end_matches("/"),
|
||||
));
|
||||
let editor_is_long = self.commit_editor.update(cx, |editor, cx| {
|
||||
@@ -3207,7 +3211,8 @@ impl GitPanel {
|
||||
cx: &App,
|
||||
) -> Option<AnyElement> {
|
||||
let repo = self.active_repository.as_ref()?.read(cx);
|
||||
let repo_path = repo.worktree_id_path_to_repo_path(file.worktree_id(cx), file.path())?;
|
||||
let project_path = (file.worktree_id(cx), file.path()).into();
|
||||
let repo_path = repo.project_path_to_repo_path(&project_path, cx)?;
|
||||
let ix = self.entry_by_path(&repo_path)?;
|
||||
let entry = self.entries.get(ix)?;
|
||||
|
||||
@@ -3466,7 +3471,7 @@ impl GitPanel {
|
||||
context_menu
|
||||
.context(self.focus_handle.clone())
|
||||
.action(stage_title, ToggleStaged.boxed_clone())
|
||||
.action(restore_title, git::RestoreFile.boxed_clone())
|
||||
.action(restore_title, git::RestoreFile::default().boxed_clone())
|
||||
.separator()
|
||||
.action("Open Diff", Confirm.boxed_clone())
|
||||
.action("Open File", SecondaryConfirm.boxed_clone())
|
||||
@@ -4027,9 +4032,7 @@ impl RenderOnce for PanelRepoFooter {
|
||||
|
||||
let single_repo = project
|
||||
.as_ref()
|
||||
.map(|project| {
|
||||
filtered_repository_entries(project.read(cx).git_store().read(cx), cx).len() == 1
|
||||
})
|
||||
.map(|project| project.read(cx).git_store().read(cx).repositories().len() == 1)
|
||||
.unwrap_or(true);
|
||||
|
||||
const MAX_BRANCH_LEN: usize = 16;
|
||||
@@ -4529,66 +4532,65 @@ mod tests {
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
|
||||
repo_path: "crates/gpui/gpui.rs".into(),
|
||||
worktree_path: Path::new("gpui.rs").into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
staging: StageStatus::Unstaged,
|
||||
}),
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
||||
repo_path: "crates/util/util.rs".into(),
|
||||
worktree_path: Path::new("../util/util.rs").into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
staging: StageStatus::Unstaged,
|
||||
},),
|
||||
],
|
||||
);
|
||||
|
||||
cx.update_window_entity(&panel, |panel, window, cx| {
|
||||
panel.select_last(&Default::default(), window, cx);
|
||||
assert_eq!(panel.selected_entry, Some(2));
|
||||
panel.open_diff(&Default::default(), window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
// TODO(cole) restore this once repository deduplication is implemented properly.
|
||||
//cx.update_window_entity(&panel, |panel, window, cx| {
|
||||
// panel.select_last(&Default::default(), window, cx);
|
||||
// assert_eq!(panel.selected_entry, Some(2));
|
||||
// panel.open_diff(&Default::default(), window, cx);
|
||||
//});
|
||||
//cx.run_until_parked();
|
||||
|
||||
let worktree_roots = workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.worktrees(cx)
|
||||
.map(|worktree| worktree.read(cx).abs_path())
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
pretty_assertions::assert_eq!(
|
||||
worktree_roots,
|
||||
vec![
|
||||
Path::new(path!("/root/zed/crates/gpui")).into(),
|
||||
Path::new(path!("/root/zed/crates/util/util.rs")).into(),
|
||||
]
|
||||
);
|
||||
//let worktree_roots = workspace.update(cx, |workspace, cx| {
|
||||
// workspace
|
||||
// .worktrees(cx)
|
||||
// .map(|worktree| worktree.read(cx).abs_path())
|
||||
// .collect::<Vec<_>>()
|
||||
//});
|
||||
//pretty_assertions::assert_eq!(
|
||||
// worktree_roots,
|
||||
// vec![
|
||||
// Path::new(path!("/root/zed/crates/gpui")).into(),
|
||||
// Path::new(path!("/root/zed/crates/util/util.rs")).into(),
|
||||
// ]
|
||||
//);
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let git_store = project.git_store().read(cx);
|
||||
// The repo that comes from the single-file worktree can't be selected through the UI.
|
||||
let filtered_entries = filtered_repository_entries(git_store, cx)
|
||||
.iter()
|
||||
.map(|repo| repo.read(cx).worktree_abs_path.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
filtered_entries,
|
||||
[Path::new(path!("/root/zed/crates/gpui")).into()]
|
||||
);
|
||||
// But we can select it artificially here.
|
||||
let repo_from_single_file_worktree = git_store
|
||||
.repositories()
|
||||
.values()
|
||||
.find(|repo| {
|
||||
repo.read(cx).worktree_abs_path.as_ref()
|
||||
== Path::new(path!("/root/zed/crates/util/util.rs"))
|
||||
})
|
||||
.unwrap()
|
||||
.clone();
|
||||
//project.update(cx, |project, cx| {
|
||||
// let git_store = project.git_store().read(cx);
|
||||
// // The repo that comes from the single-file worktree can't be selected through the UI.
|
||||
// let filtered_entries = filtered_repository_entries(git_store, cx)
|
||||
// .iter()
|
||||
// .map(|repo| repo.read(cx).worktree_abs_path.clone())
|
||||
// .collect::<Vec<_>>();
|
||||
// assert_eq!(
|
||||
// filtered_entries,
|
||||
// [Path::new(path!("/root/zed/crates/gpui")).into()]
|
||||
// );
|
||||
// // But we can select it artificially here.
|
||||
// let repo_from_single_file_worktree = git_store
|
||||
// .repositories()
|
||||
// .values()
|
||||
// .find(|repo| {
|
||||
// repo.read(cx).worktree_abs_path.as_ref()
|
||||
// == Path::new(path!("/root/zed/crates/util/util.rs"))
|
||||
// })
|
||||
// .unwrap()
|
||||
// .clone();
|
||||
|
||||
// Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
|
||||
repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
|
||||
});
|
||||
// // Paths still make sense when we somehow activate a repo that comes from a single-file worktree.
|
||||
// repo_from_single_file_worktree.update(cx, |repo, cx| repo.set_as_active_repository(cx));
|
||||
//});
|
||||
|
||||
let handle = cx.update_window_entity(&panel, |panel, _, _| {
|
||||
std::mem::replace(&mut panel.update_visible_entries_task, Task::ready(()))
|
||||
@@ -4605,14 +4607,12 @@ mod tests {
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/gpui/gpui.rs").into(),
|
||||
repo_path: "crates/gpui/gpui.rs".into(),
|
||||
worktree_path: Path::new("../../gpui/gpui.rs").into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
staging: StageStatus::Unstaged,
|
||||
}),
|
||||
GitListEntry::GitStatusEntry(GitStatusEntry {
|
||||
abs_path: path!("/root/zed/crates/util/util.rs").into(),
|
||||
repo_path: "crates/util/util.rs".into(),
|
||||
worktree_path: Path::new("util.rs").into(),
|
||||
status: StatusCode::Modified.worktree(),
|
||||
staging: StageStatus::Unstaged,
|
||||
},),
|
||||
|
||||
@@ -26,7 +26,11 @@ use project::{
|
||||
git_store::{GitEvent, GitStore},
|
||||
Project, ProjectPath,
|
||||
};
|
||||
use std::any::{Any, TypeId};
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
path::Path,
|
||||
sync::Arc,
|
||||
};
|
||||
use theme::ActiveTheme;
|
||||
use ui::{prelude::*, vertical_divider, KeyBinding, Tooltip};
|
||||
use util::ResultExt as _;
|
||||
@@ -39,7 +43,47 @@ use workspace::{
|
||||
|
||||
actions!(git, [Diff, Add]);
|
||||
|
||||
pub trait DiffSource {
|
||||
// todo!("return a struct here")
|
||||
fn status(&self, cx: &App) -> Vec<(ProjectPath, FileStatus, bool)>;
|
||||
fn open_uncommitted_diff(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<BufferDiff>>>;
|
||||
// todo!("add an observe method")
|
||||
}
|
||||
|
||||
pub struct ProjectDiffSource(Entity<Project>);
|
||||
|
||||
impl DiffSource for ProjectDiffSource {
|
||||
fn status(&self, cx: &App) -> Vec<(ProjectPath, FileStatus, bool)> {
|
||||
let mut result = Vec::new();
|
||||
if let Some(git_repo) = self.0.read(cx).git_store().read(cx).active_repository() {
|
||||
let git_repo = git_repo.read(cx);
|
||||
for entry in git_repo.cached_status() {
|
||||
if let Some(project_path) = git_repo.repo_path_to_project_path(&entry.repo_path, cx)
|
||||
{
|
||||
let has_conflict = git_repo.has_conflict(&entry.repo_path);
|
||||
result.push((project_path, entry.status, has_conflict));
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
}
|
||||
|
||||
fn open_uncommitted_diff(
|
||||
&self,
|
||||
buffer: Entity<Buffer>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Entity<BufferDiff>>> {
|
||||
self.0
|
||||
.update(cx, |project, cx| project.open_uncommitted_diff(buffer, cx))
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ProjectDiff {
|
||||
source: Arc<dyn DiffSource>,
|
||||
project: Entity<Project>,
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
@@ -102,8 +146,16 @@ impl ProjectDiff {
|
||||
existing
|
||||
} else {
|
||||
let workspace_handle = cx.entity();
|
||||
let project_diff =
|
||||
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx));
|
||||
let source = Arc::new(ProjectDiffSource(workspace.project().clone()));
|
||||
let project_diff = cx.new(|cx| {
|
||||
Self::new(
|
||||
source,
|
||||
workspace.project().clone(),
|
||||
workspace_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
workspace.add_item_to_active_pane(
|
||||
Box::new(project_diff.clone()),
|
||||
None,
|
||||
@@ -127,6 +179,7 @@ impl ProjectDiff {
|
||||
}
|
||||
|
||||
fn new(
|
||||
source: Arc<dyn DiffSource>,
|
||||
project: Entity<Project>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
@@ -171,6 +224,7 @@ impl ProjectDiff {
|
||||
*send.borrow_mut() = ();
|
||||
|
||||
Self {
|
||||
source,
|
||||
project,
|
||||
git_store: git_store.clone(),
|
||||
workspace: workspace.downgrade(),
|
||||
@@ -328,55 +382,53 @@ impl ProjectDiff {
|
||||
}
|
||||
|
||||
fn load_buffers(&mut self, cx: &mut Context<Self>) -> Vec<Task<Result<DiffBuffer>>> {
|
||||
let Some(repo) = self.git_store.read(cx).active_repository() else {
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
multibuffer.clear(cx);
|
||||
});
|
||||
return vec![];
|
||||
};
|
||||
|
||||
let mut previous_paths = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
|
||||
let mut result = vec![];
|
||||
repo.update(cx, |repo, cx| {
|
||||
for entry in repo.status() {
|
||||
if !entry.status.has_changes() {
|
||||
continue;
|
||||
}
|
||||
let Some(project_path) = repo.repo_path_to_project_path(&entry.repo_path) else {
|
||||
continue;
|
||||
};
|
||||
let namespace = if repo.has_conflict(&entry.repo_path) {
|
||||
CONFLICT_NAMESPACE
|
||||
} else if entry.status.is_created() {
|
||||
NEW_NAMESPACE
|
||||
} else {
|
||||
TRACKED_NAMESPACE
|
||||
};
|
||||
let path_key = PathKey::namespaced(namespace, entry.repo_path.0.clone());
|
||||
|
||||
previous_paths.remove(&path_key);
|
||||
let load_buffer = self
|
||||
.project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
let project = self.project.clone();
|
||||
result.push(cx.spawn(async move |_, cx| {
|
||||
let buffer = load_buffer.await?;
|
||||
let changes = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||
})?
|
||||
.await?;
|
||||
Ok(DiffBuffer {
|
||||
path_key,
|
||||
buffer,
|
||||
diff: changes,
|
||||
file_status: entry.status,
|
||||
})
|
||||
}));
|
||||
for (project_path, status, has_conflict) in self.source.status(cx) {
|
||||
if !status.has_changes() {
|
||||
continue;
|
||||
}
|
||||
});
|
||||
|
||||
let Some(worktree) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.worktree_for_id(project_path.worktree_id, cx)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
let full_path =
|
||||
Arc::from(Path::new(worktree.read(cx).root_name()).join(&project_path.path));
|
||||
|
||||
let namespace = if has_conflict {
|
||||
CONFLICT_NAMESPACE
|
||||
} else if status.is_created() {
|
||||
NEW_NAMESPACE
|
||||
} else {
|
||||
TRACKED_NAMESPACE
|
||||
};
|
||||
let path_key = PathKey::namespaced(namespace, full_path);
|
||||
|
||||
previous_paths.remove(&path_key);
|
||||
let load_buffer = self
|
||||
.project
|
||||
.update(cx, |project, cx| project.open_buffer(project_path, cx));
|
||||
|
||||
let source = self.source.clone();
|
||||
result.push(cx.spawn(async move |_, cx| {
|
||||
let buffer = load_buffer.await?;
|
||||
let changes = cx
|
||||
.update(|cx| source.open_uncommitted_diff(buffer.clone(), cx))?
|
||||
.await?;
|
||||
Ok(DiffBuffer {
|
||||
path_key,
|
||||
buffer,
|
||||
diff: changes,
|
||||
file_status: status,
|
||||
})
|
||||
}));
|
||||
}
|
||||
|
||||
self.multibuffer.update(cx, |multibuffer, cx| {
|
||||
for path in previous_paths {
|
||||
multibuffer.remove_excerpts_for_path(path, cx);
|
||||
@@ -585,7 +637,15 @@ impl Item for ProjectDiff {
|
||||
Self: Sized,
|
||||
{
|
||||
let workspace = self.workspace.upgrade()?;
|
||||
Some(cx.new(|cx| ProjectDiff::new(self.project.clone(), workspace, window, cx)))
|
||||
Some(cx.new(|cx| {
|
||||
ProjectDiff::new(
|
||||
self.source.clone(),
|
||||
self.project.clone(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
@@ -743,7 +803,7 @@ impl SerializableItem for ProjectDiff {
|
||||
}
|
||||
|
||||
fn deserialize(
|
||||
_project: Entity<Project>,
|
||||
project: Entity<Project>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_workspace_id: workspace::WorkspaceId,
|
||||
_item_id: workspace::ItemId,
|
||||
@@ -753,7 +813,16 @@ impl SerializableItem for ProjectDiff {
|
||||
window.spawn(cx, async move |cx| {
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let workspace_handle = cx.entity();
|
||||
cx.new(|cx| Self::new(workspace.project().clone(), workspace_handle, window, cx))
|
||||
let diff = Arc::new(ProjectDiffSource(project));
|
||||
cx.new(|cx| {
|
||||
Self::new(
|
||||
diff,
|
||||
workspace.project().clone(),
|
||||
workspace_handle,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -1337,8 +1406,9 @@ mod tests {
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let source = Arc::new(ProjectDiffSource(project.clone()));
|
||||
let diff = cx.new_window_entity(|window, cx| {
|
||||
ProjectDiff::new(project.clone(), workspace, window, cx)
|
||||
ProjectDiff::new(source, project.clone(), workspace, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1391,8 +1461,9 @@ mod tests {
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let source = Arc::new(ProjectDiffSource(project.clone()));
|
||||
let diff = cx.new_window_entity(|window, cx| {
|
||||
ProjectDiff::new(project.clone(), workspace, window, cx)
|
||||
ProjectDiff::new(source, project.clone(), workspace, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -1464,6 +1535,7 @@ mod tests {
|
||||
let project = Project::test(fs.clone(), [path!("/project").as_ref()], cx).await;
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let source = Arc::new(ProjectDiffSource(project.clone()));
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/project/foo"), cx)
|
||||
@@ -1474,7 +1546,7 @@ mod tests {
|
||||
Editor::for_buffer(buffer, Some(project.clone()), window, cx)
|
||||
});
|
||||
let diff = cx.new_window_entity(|window, cx| {
|
||||
ProjectDiff::new(project.clone(), workspace, window, cx)
|
||||
ProjectDiff::new(source, project.clone(), workspace, window, cx)
|
||||
});
|
||||
cx.run_until_parked();
|
||||
|
||||
|
||||
@@ -3,10 +3,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::{
|
||||
git_store::{GitStore, Repository},
|
||||
Project,
|
||||
};
|
||||
use project::{git_store::Repository, Project};
|
||||
use std::sync::Arc;
|
||||
use ui::{prelude::*, ListItem, ListItemSpacing};
|
||||
use workspace::{ModalView, Workspace};
|
||||
@@ -40,21 +37,23 @@ impl RepositorySelector {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let git_store = project_handle.read(cx).git_store().clone();
|
||||
let repository_entries = git_store.update(cx, |git_store, cx| {
|
||||
filtered_repository_entries(git_store, cx)
|
||||
let repository_entries = git_store.update(cx, |git_store, _cx| {
|
||||
git_store
|
||||
.repositories()
|
||||
.values()
|
||||
.cloned()
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
let project = project_handle.read(cx);
|
||||
let filtered_repositories = repository_entries.clone();
|
||||
|
||||
let widest_item_ix = repository_entries.iter().position_max_by(|a, b| {
|
||||
a.read(cx)
|
||||
.display_name(project, cx)
|
||||
.display_name()
|
||||
.len()
|
||||
.cmp(&b.read(cx).display_name(project, cx).len())
|
||||
.cmp(&b.read(cx).display_name().len())
|
||||
});
|
||||
|
||||
let delegate = RepositorySelectorDelegate {
|
||||
project: project_handle.downgrade(),
|
||||
repository_selector: cx.entity().downgrade(),
|
||||
repository_entries,
|
||||
filtered_repositories,
|
||||
@@ -71,36 +70,36 @@ impl RepositorySelector {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn filtered_repository_entries(
|
||||
git_store: &GitStore,
|
||||
cx: &App,
|
||||
) -> Vec<Entity<Repository>> {
|
||||
let repositories = git_store
|
||||
.repositories()
|
||||
.values()
|
||||
.sorted_by_key(|repo| {
|
||||
let repo = repo.read(cx);
|
||||
(
|
||||
repo.dot_git_abs_path.clone(),
|
||||
repo.worktree_abs_path.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<&Entity<Repository>>>();
|
||||
|
||||
repositories
|
||||
.chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path)
|
||||
.flat_map(|chunk| {
|
||||
let has_non_single_file_worktree = chunk
|
||||
.iter()
|
||||
.any(|repo| !repo.read(cx).is_from_single_file_worktree);
|
||||
chunk.iter().filter(move |repo| {
|
||||
// Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree.
|
||||
!repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree
|
||||
})
|
||||
})
|
||||
.map(|&repo| repo.clone())
|
||||
.collect()
|
||||
}
|
||||
//pub(crate) fn filtered_repository_entries(
|
||||
// git_store: &GitStore,
|
||||
// cx: &App,
|
||||
//) -> Vec<Entity<Repository>> {
|
||||
// let repositories = git_store
|
||||
// .repositories()
|
||||
// .values()
|
||||
// .sorted_by_key(|repo| {
|
||||
// let repo = repo.read(cx);
|
||||
// (
|
||||
// repo.dot_git_abs_path.clone(),
|
||||
// repo.worktree_abs_path.clone(),
|
||||
// )
|
||||
// })
|
||||
// .collect::<Vec<&Entity<Repository>>>();
|
||||
//
|
||||
// repositories
|
||||
// .chunk_by(|a, b| a.read(cx).dot_git_abs_path == b.read(cx).dot_git_abs_path)
|
||||
// .flat_map(|chunk| {
|
||||
// let has_non_single_file_worktree = chunk
|
||||
// .iter()
|
||||
// .any(|repo| !repo.read(cx).is_from_single_file_worktree);
|
||||
// chunk.iter().filter(move |repo| {
|
||||
// // Remove any entry that comes from a single file worktree and represents a repository that is also represented by a non-single-file worktree.
|
||||
// !repo.read(cx).is_from_single_file_worktree || !has_non_single_file_worktree
|
||||
// })
|
||||
// })
|
||||
// .map(|&repo| repo.clone())
|
||||
// .collect()
|
||||
//}
|
||||
|
||||
impl EventEmitter<DismissEvent> for RepositorySelector {}
|
||||
|
||||
@@ -119,7 +118,6 @@ impl Render for RepositorySelector {
|
||||
impl ModalView for RepositorySelector {}
|
||||
|
||||
pub struct RepositorySelectorDelegate {
|
||||
project: WeakEntity<Project>,
|
||||
repository_selector: WeakEntity<RepositorySelector>,
|
||||
repository_entries: Vec<Entity<Repository>>,
|
||||
filtered_repositories: Vec<Entity<Repository>>,
|
||||
@@ -225,9 +223,8 @@ impl PickerDelegate for RepositorySelectorDelegate {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let project = self.project.upgrade()?;
|
||||
let repo_info = self.filtered_repositories.get(ix)?;
|
||||
let display_name = repo_info.read(cx).display_name(project.read(cx), cx);
|
||||
let display_name = repo_info.read(cx).display_name();
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
|
||||
@@ -61,7 +61,7 @@ impl Render for WindowShadow {
|
||||
CursorStyle::ResizeUpRightDownLeft
|
||||
}
|
||||
},
|
||||
&hitbox,
|
||||
Some(&hitbox),
|
||||
);
|
||||
},
|
||||
)
|
||||
|
||||
@@ -375,16 +375,50 @@ macro_rules! action_with_deprecated_aliases {
|
||||
$name,
|
||||
$name,
|
||||
fn build(
|
||||
_: gpui::private::serde_json::Value,
|
||||
value: gpui::private::serde_json::Value,
|
||||
) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
|
||||
Ok(Box::new(Self))
|
||||
},
|
||||
|
||||
fn action_json_schema(
|
||||
generator: &mut gpui::private::schemars::gen::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::schema::Schema> {
|
||||
None
|
||||
|
||||
},
|
||||
|
||||
fn deprecated_aliases() -> &'static [&'static str] {
|
||||
&[
|
||||
$($alias),*
|
||||
]
|
||||
}
|
||||
);
|
||||
|
||||
gpui::register_action!($name);
|
||||
};
|
||||
}
|
||||
|
||||
/// Defines and registers a unit struct that can be used as an action, with some deprecated aliases.
|
||||
#[macro_export]
|
||||
macro_rules! impl_action_with_deprecated_aliases {
|
||||
($namespace:path, $name:ident, [$($alias:literal),* $(,)?]) => {
|
||||
gpui::__impl_action!(
|
||||
$namespace,
|
||||
$name,
|
||||
$name,
|
||||
fn build(
|
||||
value: gpui::private::serde_json::Value,
|
||||
) -> gpui::Result<::std::boxed::Box<dyn gpui::Action>> {
|
||||
Ok(std::boxed::Box::new(gpui::private::serde_json::from_value::<Self>(value)?))
|
||||
},
|
||||
|
||||
fn action_json_schema(
|
||||
generator: &mut gpui::private::schemars::gen::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::schema::Schema> {
|
||||
Some(<Self as gpui::private::schemars::JsonSchema>::json_schema(
|
||||
generator,
|
||||
))
|
||||
},
|
||||
|
||||
fn deprecated_aliases() -> &'static [&'static str] {
|
||||
&[
|
||||
$($alias),*
|
||||
|
||||
@@ -1617,7 +1617,7 @@ impl Interactivity {
|
||||
|
||||
if !cx.has_active_drag() {
|
||||
if let Some(mouse_cursor) = style.mouse_cursor {
|
||||
window.set_cursor_style(mouse_cursor, hitbox);
|
||||
window.set_cursor_style(mouse_cursor, Some(hitbox));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -46,6 +46,12 @@ impl List {
|
||||
#[derive(Clone)]
|
||||
pub struct ListState(Rc<RefCell<StateInner>>);
|
||||
|
||||
impl std::fmt::Debug for ListState {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
f.write_str("ListState")
|
||||
}
|
||||
}
|
||||
|
||||
struct StateInner {
|
||||
last_layout_bounds: Option<Bounds<Pixels>>,
|
||||
last_padding: Option<Edges<Pixels>>,
|
||||
@@ -57,6 +63,7 @@ struct StateInner {
|
||||
reset: bool,
|
||||
#[allow(clippy::type_complexity)]
|
||||
scroll_handler: Option<Box<dyn FnMut(&ListScrollEvent, &mut Window, &mut App)>>,
|
||||
scrollbar_drag_start_height: Option<Pixels>,
|
||||
}
|
||||
|
||||
/// Whether the list is scrolling from top to bottom or bottom to top.
|
||||
@@ -198,6 +205,7 @@ impl ListState {
|
||||
overdraw,
|
||||
scroll_handler: None,
|
||||
reset: false,
|
||||
scrollbar_drag_start_height: None,
|
||||
})));
|
||||
this.splice(0..0, item_count);
|
||||
this
|
||||
@@ -211,6 +219,7 @@ impl ListState {
|
||||
let state = &mut *self.0.borrow_mut();
|
||||
state.reset = true;
|
||||
state.logical_scroll_top = None;
|
||||
state.scrollbar_drag_start_height = None;
|
||||
state.items.summary().count
|
||||
};
|
||||
|
||||
@@ -355,6 +364,62 @@ impl ListState {
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Call this method when the user starts dragging the scrollbar.
|
||||
///
|
||||
/// This will prevent the height reported to the scrollbar from changing during the drag
|
||||
/// as items in the overdraw get measured, and help offset scroll position changes accordingly.
|
||||
pub fn scrollbar_drag_started(&self) {
|
||||
let mut state = self.0.borrow_mut();
|
||||
state.scrollbar_drag_start_height = Some(state.items.summary().height);
|
||||
}
|
||||
|
||||
/// Called when the user stops dragging the scrollbar.
|
||||
///
|
||||
/// See `scrollbar_drag_started`.
|
||||
pub fn scrollbar_drag_ended(&self) {
|
||||
self.0.borrow_mut().scrollbar_drag_start_height.take();
|
||||
}
|
||||
|
||||
/// Set the offset from the scrollbar
|
||||
pub fn set_offset_from_scrollbar(&self, point: Point<Pixels>) {
|
||||
self.0.borrow_mut().set_offset_from_scrollbar(point);
|
||||
}
|
||||
|
||||
/// Returns the size of items we have measured.
|
||||
/// This value remains constant while dragging to prevent the scrollbar from moving away unexpectedly.
|
||||
pub fn content_size_for_scrollbar(&self) -> Size<Pixels> {
|
||||
let state = self.0.borrow();
|
||||
let bounds = state.last_layout_bounds.unwrap_or_default();
|
||||
|
||||
let height = state
|
||||
.scrollbar_drag_start_height
|
||||
.unwrap_or_else(|| state.items.summary().height);
|
||||
|
||||
Size::new(bounds.size.width, height)
|
||||
}
|
||||
|
||||
/// Returns the current scroll offset adjusted for the scrollbar
|
||||
pub fn scroll_px_offset_for_scrollbar(&self) -> Point<Pixels> {
|
||||
let state = &self.0.borrow();
|
||||
let logical_scroll_top = state.logical_scroll_top();
|
||||
|
||||
let mut cursor = state.items.cursor::<ListItemSummary>(&());
|
||||
let summary: ListItemSummary =
|
||||
cursor.summary(&Count(logical_scroll_top.item_ix), Bias::Right, &());
|
||||
let content_height = state.items.summary().height;
|
||||
let drag_offset =
|
||||
// if dragging the scrollbar, we want to offset the point if the height changed
|
||||
content_height - state.scrollbar_drag_start_height.unwrap_or(content_height);
|
||||
let offset = summary.height + logical_scroll_top.offset_in_item - drag_offset;
|
||||
|
||||
Point::new(px(0.), -offset)
|
||||
}
|
||||
|
||||
/// Return the bounds of the viewport in pixels.
|
||||
pub fn viewport_bounds(&self) -> Bounds<Pixels> {
|
||||
self.0.borrow().last_layout_bounds.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
impl StateInner {
|
||||
@@ -695,6 +760,37 @@ impl StateInner {
|
||||
Ok(layout_response)
|
||||
})
|
||||
}
|
||||
|
||||
// Scrollbar support
|
||||
|
||||
fn set_offset_from_scrollbar(&mut self, point: Point<Pixels>) {
|
||||
let Some(bounds) = self.last_layout_bounds else {
|
||||
return;
|
||||
};
|
||||
let height = bounds.size.height;
|
||||
|
||||
let padding = self.last_padding.unwrap_or_default();
|
||||
let content_height = self.items.summary().height;
|
||||
let scroll_max = (content_height + padding.top + padding.bottom - height).max(px(0.));
|
||||
let drag_offset =
|
||||
// if dragging the scrollbar, we want to offset the point if the height changed
|
||||
content_height - self.scrollbar_drag_start_height.unwrap_or(content_height);
|
||||
let new_scroll_top = (point.y - drag_offset).abs().max(px(0.)).min(scroll_max);
|
||||
|
||||
if self.alignment == ListAlignment::Bottom && new_scroll_top == scroll_max {
|
||||
self.logical_scroll_top = None;
|
||||
} else {
|
||||
let mut cursor = self.items.cursor::<ListItemSummary>(&());
|
||||
cursor.seek(&Height(new_scroll_top), Bias::Right, &());
|
||||
|
||||
let item_ix = cursor.start().count;
|
||||
let offset_in_item = new_scroll_top - cursor.start().height;
|
||||
self.logical_scroll_top = Some(ListOffset {
|
||||
item_ix,
|
||||
offset_in_item,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for ListItem {
|
||||
|
||||
@@ -700,7 +700,7 @@ impl Element for InteractiveText {
|
||||
.iter()
|
||||
.any(|range| range.contains(&ix))
|
||||
{
|
||||
window.set_cursor_style(crate::CursorStyle::PointingHand, hitbox)
|
||||
window.set_cursor_style(crate::CursorStyle::PointingHand, Some(hitbox))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -74,6 +74,11 @@ pub(crate) use windows::*;
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub use test::TestScreenCaptureSource;
|
||||
|
||||
/// Returns a background executor for the current platform.
|
||||
pub fn background_executor() -> BackgroundExecutor {
|
||||
current_platform(true).background_executor()
|
||||
}
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
pub(crate) fn current_platform(headless: bool) -> Rc<dyn Platform> {
|
||||
Rc::new(MacPlatform::new(headless))
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
use anyhow::Context as _;
|
||||
use blade_graphics as gpu;
|
||||
use std::sync::Arc;
|
||||
use util::ResultExt;
|
||||
|
||||
#[cfg_attr(target_os = "macos", derive(Clone))]
|
||||
pub struct BladeContext {
|
||||
@@ -8,12 +10,24 @@ pub struct BladeContext {
|
||||
|
||||
impl BladeContext {
|
||||
pub fn new() -> anyhow::Result<Self> {
|
||||
let device_id_forced = match std::env::var("ZED_DEVICE_ID") {
|
||||
Ok(val) => val
|
||||
.parse()
|
||||
.context("Failed to parse device ID from `ZED_DEVICE_ID` environment variable")
|
||||
.log_err(),
|
||||
Err(std::env::VarError::NotPresent) => None,
|
||||
err => {
|
||||
err.context("Failed to read value of `ZED_DEVICE_ID` environment variable")
|
||||
.log_err();
|
||||
None
|
||||
}
|
||||
};
|
||||
let gpu = Arc::new(
|
||||
unsafe {
|
||||
gpu::Context::init(gpu::ContextDesc {
|
||||
presentation: true,
|
||||
validation: false,
|
||||
device_id: 0, //TODO: hook up to user settings
|
||||
device_id: device_id_forced.unwrap_or(0),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
@@ -532,6 +532,12 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
quad.border_widths.top,
|
||||
center_to_point.y < 0.0));
|
||||
|
||||
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
|
||||
// The purpose of this is to not draw antialiasing pixels in this case.
|
||||
let reduced_border =
|
||||
vec2<f32>(select(border.x, -antialias_threshold, border.x == 0.0),
|
||||
select(border.y, -antialias_threshold, border.y == 0.0));
|
||||
|
||||
// Vector from the corner of the quad bounds to the point, after mirroring
|
||||
// the point into the bottom right quadrant. Both components are <= 0.
|
||||
let corner_to_point = abs(center_to_point) - half_size;
|
||||
@@ -546,15 +552,15 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
corner_center_to_point.y >= 0;
|
||||
|
||||
// Vector from straight border inner corner to point.
|
||||
let straight_border_inner_corner_to_point = corner_to_point + border;
|
||||
let straight_border_inner_corner_to_point = corner_to_point + reduced_border;
|
||||
|
||||
// Whether the point is beyond the inner edge of the straight border.
|
||||
let is_beyond_inner_straight_border =
|
||||
straight_border_inner_corner_to_point.x > 0 ||
|
||||
straight_border_inner_corner_to_point.y > 0;
|
||||
|
||||
// Whether the point is far enough inside the straight border such that
|
||||
// pixels are not affected by it.
|
||||
// Whether the point is far enough inside the quad, such that the pixels are
|
||||
// not affected by the straight border.
|
||||
let is_within_inner_straight_border =
|
||||
straight_border_inner_corner_to_point.x < -antialias_threshold &&
|
||||
straight_border_inner_corner_to_point.y < -antialias_threshold;
|
||||
@@ -589,11 +595,11 @@ fn fs_quad(input: QuadVarying) -> @location(0) vec4<f32> {
|
||||
} else if (is_beyond_inner_straight_border) {
|
||||
// Fast path for points that must be outside the inner edge.
|
||||
inner_sdf = -1.0;
|
||||
} else if (border.x == border.y) {
|
||||
} else if (reduced_border.x == reduced_border.y) {
|
||||
// Fast path for circular inner edge.
|
||||
inner_sdf = -(outer_sdf + border.x);
|
||||
inner_sdf = -(outer_sdf + reduced_border.x);
|
||||
} else {
|
||||
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - border);
|
||||
let ellipse_radii = max(vec2<f32>(0.0), corner_radius - reduced_border);
|
||||
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
|
||||
}
|
||||
|
||||
|
||||
@@ -133,6 +133,12 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
center_to_point.y < 0.0 ? quad.border_widths.top : quad.border_widths.bottom
|
||||
);
|
||||
|
||||
// 0-width borders are reduced so that `inner_sdf >= antialias_threshold`.
|
||||
// The purpose of this is to not draw antialiasing pixels in this case.
|
||||
float2 reduced_border = float2(
|
||||
border.x == 0.0 ? -antialias_threshold : border.x,
|
||||
border.y == 0.0 ? -antialias_threshold : border.y);
|
||||
|
||||
// Vector from the corner of the quad bounds to the point, after mirroring
|
||||
// the point into the bottom right quadrant. Both components are <= 0.
|
||||
float2 corner_to_point = fabs(center_to_point) - half_size;
|
||||
@@ -146,16 +152,20 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
corner_center_to_point.x >= 0.0 &&
|
||||
corner_center_to_point.y >= 0.0;
|
||||
|
||||
// Vector from straight border inner corner to point
|
||||
float2 straight_border_inner_corner_to_point = corner_to_point + border;
|
||||
// Vector from straight border inner corner to point.
|
||||
//
|
||||
// 0-width borders are turned into width -1 so that inner_sdf is > 1.0 near
|
||||
// the border. Without this, antialiasing pixels would be drawn.
|
||||
float2 straight_border_inner_corner_to_point = corner_to_point + reduced_border;
|
||||
|
||||
// Whether the point is beyond the inner edge of the straight border
|
||||
bool is_beyond_inner_straight_border =
|
||||
straight_border_inner_corner_to_point.x > 0.0 ||
|
||||
straight_border_inner_corner_to_point.y > 0.0;
|
||||
|
||||
// Whether the point is far enough inside the straight border such that
|
||||
// pixels are not affected by it
|
||||
|
||||
// Whether the point is far enough inside the quad, such that the pixels are
|
||||
// not affected by the straight border.
|
||||
bool is_within_inner_straight_border =
|
||||
straight_border_inner_corner_to_point.x < -antialias_threshold &&
|
||||
straight_border_inner_corner_to_point.y < -antialias_threshold;
|
||||
@@ -184,11 +194,11 @@ fragment float4 quad_fragment(QuadFragmentInput input [[stage_in]],
|
||||
} else if (is_beyond_inner_straight_border) {
|
||||
// Fast path for points that must be outside the inner edge
|
||||
inner_sdf = -1.0;
|
||||
} else if (border.x == border.y) {
|
||||
} else if (reduced_border.x == reduced_border.y) {
|
||||
// Fast path for circular inner edge.
|
||||
inner_sdf = -(outer_sdf + border.x);
|
||||
inner_sdf = -(outer_sdf + reduced_border.x);
|
||||
} else {
|
||||
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - border);
|
||||
float2 ellipse_radii = max(float2(0.0), float2(corner_radius) - reduced_border);
|
||||
inner_sdf = quarter_ellipse_sdf(corner_center_to_point, ellipse_radii);
|
||||
}
|
||||
|
||||
|
||||
@@ -1137,7 +1137,7 @@ fn handle_cursor_changed(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -
|
||||
};
|
||||
|
||||
if had_cursor != state.current_cursor.is_some() {
|
||||
unsafe { SetCursor(state.current_cursor.as_ref()) };
|
||||
unsafe { SetCursor(state.current_cursor) };
|
||||
}
|
||||
|
||||
Some(0)
|
||||
@@ -1151,7 +1151,7 @@ fn handle_set_cursor(lparam: LPARAM, state_ptr: Rc<WindowsWindowStatePtr>) -> Op
|
||||
return None;
|
||||
}
|
||||
unsafe {
|
||||
SetCursor(state_ptr.state.borrow().current_cursor.as_ref());
|
||||
SetCursor(state_ptr.state.borrow().current_cursor);
|
||||
};
|
||||
Some(1)
|
||||
}
|
||||
|
||||
@@ -407,7 +407,7 @@ pub(crate) type AnyMouseListener =
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) struct CursorStyleRequest {
|
||||
pub(crate) hitbox_id: HitboxId,
|
||||
pub(crate) hitbox_id: Option<HitboxId>, // None represents whole window
|
||||
pub(crate) style: CursorStyle,
|
||||
}
|
||||
|
||||
@@ -1928,10 +1928,10 @@ impl Window {
|
||||
|
||||
/// Updates the cursor style at the platform level. This method should only be called
|
||||
/// during the prepaint phase of element drawing.
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: &Hitbox) {
|
||||
pub fn set_cursor_style(&mut self, style: CursorStyle, hitbox: Option<&Hitbox>) {
|
||||
self.invalidator.debug_assert_paint();
|
||||
self.next_frame.cursor_styles.push(CursorStyleRequest {
|
||||
hitbox_id: hitbox.id,
|
||||
hitbox_id: hitbox.map(|hitbox| hitbox.id),
|
||||
style,
|
||||
});
|
||||
}
|
||||
@@ -2984,7 +2984,11 @@ impl Window {
|
||||
.cursor_styles
|
||||
.iter()
|
||||
.rev()
|
||||
.find(|request| request.hitbox_id.is_hovered(self))
|
||||
.find(|request| {
|
||||
request
|
||||
.hitbox_id
|
||||
.map_or(true, |hitbox_id| hitbox_id.is_hovered(self))
|
||||
})
|
||||
.map(|request| request.style)
|
||||
.unwrap_or(CursorStyle::Arrow);
|
||||
cx.platform.set_cursor_style(style);
|
||||
|
||||
@@ -100,6 +100,8 @@ pub enum IconName {
|
||||
Eye,
|
||||
File,
|
||||
FileCode,
|
||||
FileCreate,
|
||||
FileDelete,
|
||||
FileDoc,
|
||||
FileDiff,
|
||||
FileGeneric,
|
||||
|
||||
@@ -14,6 +14,7 @@ use futures::FutureExt;
|
||||
use futures::{future::BoxFuture, stream::BoxStream, StreamExt, TryStreamExt as _};
|
||||
use gpui::{AnyElement, AnyView, App, AsyncApp, SharedString, Task, Window};
|
||||
use icons::IconName;
|
||||
use parking_lot::Mutex;
|
||||
use proto::Plan;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{de::DeserializeOwned, Deserialize, Serialize};
|
||||
@@ -141,6 +142,8 @@ pub struct LanguageModelToolUse {
|
||||
pub struct LanguageModelTextStream {
|
||||
pub message_id: Option<String>,
|
||||
pub stream: BoxStream<'static, Result<String>>,
|
||||
// Has complete token usage after the stream has finished
|
||||
pub last_token_usage: Arc<Mutex<TokenUsage>>,
|
||||
}
|
||||
|
||||
impl Default for LanguageModelTextStream {
|
||||
@@ -148,6 +151,7 @@ impl Default for LanguageModelTextStream {
|
||||
Self {
|
||||
message_id: None,
|
||||
stream: Box::pin(futures::stream::empty()),
|
||||
last_token_usage: Arc::new(Mutex::new(TokenUsage::default())),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -200,6 +204,7 @@ pub trait LanguageModel: Send + Sync {
|
||||
let mut events = events.await?.fuse();
|
||||
let mut message_id = None;
|
||||
let mut first_item_text = None;
|
||||
let last_token_usage = Arc::new(Mutex::new(TokenUsage::default()));
|
||||
|
||||
if let Some(first_event) = events.next().await {
|
||||
match first_event {
|
||||
@@ -214,20 +219,33 @@ pub trait LanguageModel: Send + Sync {
|
||||
}
|
||||
|
||||
let stream = futures::stream::iter(first_item_text.map(Ok))
|
||||
.chain(events.filter_map(|result| async move {
|
||||
match result {
|
||||
Ok(LanguageModelCompletionEvent::StartMessage { .. }) => None,
|
||||
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
|
||||
Ok(LanguageModelCompletionEvent::Thinking(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(_)) => None,
|
||||
Err(err) => Some(Err(err)),
|
||||
.chain(events.filter_map({
|
||||
let last_token_usage = last_token_usage.clone();
|
||||
move |result| {
|
||||
let last_token_usage = last_token_usage.clone();
|
||||
async move {
|
||||
match result {
|
||||
Ok(LanguageModelCompletionEvent::StartMessage { .. }) => None,
|
||||
Ok(LanguageModelCompletionEvent::Text(text)) => Some(Ok(text)),
|
||||
Ok(LanguageModelCompletionEvent::Thinking(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::Stop(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::ToolUse(_)) => None,
|
||||
Ok(LanguageModelCompletionEvent::UsageUpdate(token_usage)) => {
|
||||
*last_token_usage.lock() = token_usage;
|
||||
None
|
||||
}
|
||||
Err(err) => Some(Err(err)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}))
|
||||
.boxed();
|
||||
|
||||
Ok(LanguageModelTextStream { message_id, stream })
|
||||
Ok(LanguageModelTextStream {
|
||||
message_id,
|
||||
stream,
|
||||
last_token_usage,
|
||||
})
|
||||
}
|
||||
.boxed()
|
||||
}
|
||||
|
||||
@@ -29,6 +29,7 @@ use std::{
|
||||
any::Any,
|
||||
borrow::Cow,
|
||||
ffi::OsString,
|
||||
fmt::Write,
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
};
|
||||
@@ -588,6 +589,28 @@ fn python_module_name_from_relative_path(relative_path: &str) -> String {
|
||||
.to_string()
|
||||
}
|
||||
|
||||
fn python_env_kind_display(k: &PythonEnvironmentKind) -> &'static str {
|
||||
match k {
|
||||
PythonEnvironmentKind::Conda => "Conda",
|
||||
PythonEnvironmentKind::Pixi => "pixi",
|
||||
PythonEnvironmentKind::Homebrew => "Homebrew",
|
||||
PythonEnvironmentKind::Pyenv => "global (Pyenv)",
|
||||
PythonEnvironmentKind::GlobalPaths => "global",
|
||||
PythonEnvironmentKind::PyenvVirtualEnv => "Pyenv",
|
||||
PythonEnvironmentKind::Pipenv => "Pipenv",
|
||||
PythonEnvironmentKind::Poetry => "Poetry",
|
||||
PythonEnvironmentKind::MacPythonOrg => "global (Python.org)",
|
||||
PythonEnvironmentKind::MacCommandLineTools => "global (Command Line Tools for Xcode)",
|
||||
PythonEnvironmentKind::LinuxGlobal => "global",
|
||||
PythonEnvironmentKind::MacXCode => "global (Xcode)",
|
||||
PythonEnvironmentKind::Venv => "venv",
|
||||
PythonEnvironmentKind::VirtualEnv => "virtualenv",
|
||||
PythonEnvironmentKind::VirtualEnvWrapper => "virtualenvwrapper",
|
||||
PythonEnvironmentKind::WindowsStore => "global (Windows Store)",
|
||||
PythonEnvironmentKind::WindowsRegistry => "global (Windows Registry)",
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) struct PythonToolchainProvider {
|
||||
term: SharedString,
|
||||
}
|
||||
@@ -683,14 +706,26 @@ impl ToolchainLister for PythonToolchainProvider {
|
||||
let mut toolchains: Vec<_> = toolchains
|
||||
.into_iter()
|
||||
.filter_map(|toolchain| {
|
||||
let name = if let Some(version) = &toolchain.version {
|
||||
format!("Python {version} ({:?})", toolchain.kind?)
|
||||
} else {
|
||||
format!("{:?}", toolchain.kind?)
|
||||
let mut name = String::from("Python");
|
||||
if let Some(ref version) = toolchain.version {
|
||||
_ = write!(name, " {version}");
|
||||
}
|
||||
.into();
|
||||
|
||||
let name_and_kind = match (&toolchain.name, &toolchain.kind) {
|
||||
(Some(name), Some(kind)) => {
|
||||
Some(format!("({name}; {})", python_env_kind_display(kind)))
|
||||
}
|
||||
(Some(name), None) => Some(format!("({name})")),
|
||||
(None, Some(kind)) => Some(format!("({})", python_env_kind_display(kind))),
|
||||
(None, None) => None,
|
||||
};
|
||||
|
||||
if let Some(nk) = name_and_kind {
|
||||
_ = write!(name, " {nk}");
|
||||
}
|
||||
|
||||
Some(Toolchain {
|
||||
name,
|
||||
name: name.into(),
|
||||
path: toolchain.executable.as_ref()?.to_str()?.to_owned().into(),
|
||||
language_name: LanguageName::new("Python"),
|
||||
as_json: serde_json::to_value(toolchain).ok()?,
|
||||
|
||||
@@ -44,9 +44,9 @@ postage.workspace = true
|
||||
core-foundation.workspace = true
|
||||
|
||||
[target.'cfg(all(not(target_os = "macos")))'.dependencies]
|
||||
async-trait = { workspace = true }
|
||||
collections = { workspace = true }
|
||||
gpui = { workspace = true }
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
gpui.workspace = true
|
||||
livekit_api.workspace = true
|
||||
nanoid.workspace = true
|
||||
|
||||
|
||||
@@ -774,6 +774,10 @@ impl LanguageServer {
|
||||
code_lens: Some(CodeLensClientCapabilities {
|
||||
dynamic_registration: Some(false),
|
||||
}),
|
||||
document_symbol: Some(DocumentSymbolClientCapabilities {
|
||||
hierarchical_document_symbol_support: Some(true),
|
||||
..DocumentSymbolClientCapabilities::default()
|
||||
}),
|
||||
..TextDocumentClientCapabilities::default()
|
||||
}),
|
||||
experimental: Some(json!({
|
||||
@@ -1479,6 +1483,7 @@ impl LanguageServer {
|
||||
document_formatting_provider: Some(OneOf::Left(true)),
|
||||
document_range_formatting_provider: Some(OneOf::Left(true)),
|
||||
definition_provider: Some(OneOf::Left(true)),
|
||||
workspace_symbol_provider: Some(OneOf::Left(true)),
|
||||
implementation_provider: Some(ImplementationProviderCapability::Simple(true)),
|
||||
type_definition_provider: Some(TypeDefinitionProviderCapability::Simple(true)),
|
||||
..Default::default()
|
||||
|
||||
@@ -411,9 +411,9 @@ impl MarkdownElement {
|
||||
.is_some();
|
||||
|
||||
if is_hovering_link {
|
||||
window.set_cursor_style(CursorStyle::PointingHand, hitbox);
|
||||
window.set_cursor_style(CursorStyle::PointingHand, Some(hitbox));
|
||||
} else {
|
||||
window.set_cursor_style(CursorStyle::IBeam, hitbox);
|
||||
window.set_cursor_style(CursorStyle::IBeam, Some(hitbox));
|
||||
}
|
||||
|
||||
self.on_mouse_event(window, cx, {
|
||||
|
||||
@@ -14,4 +14,4 @@ doctest = false
|
||||
|
||||
[dependencies]
|
||||
gpui.workspace = true
|
||||
serde = { workspace = true }
|
||||
serde.workspace = true
|
||||
|
||||
@@ -2555,6 +2555,9 @@ impl OutlinePanel {
|
||||
let auto_fold_dirs = OutlinePanelSettings::get_global(cx).auto_fold_dirs;
|
||||
let active_multi_buffer = active_editor.read(cx).buffer().clone();
|
||||
let new_entries = self.new_entries_for_fs_update.clone();
|
||||
let repo_snapshots = self.project.update(cx, |project, cx| {
|
||||
project.git_store().read(cx).repo_snapshots(cx)
|
||||
});
|
||||
self.updating_fs_entries = true;
|
||||
self.fs_entries_update_task = cx.spawn_in(window, async move |outline_panel, cx| {
|
||||
if let Some(debounce) = debounce {
|
||||
@@ -2679,13 +2682,15 @@ impl OutlinePanel {
|
||||
.unwrap_or_default(),
|
||||
entry,
|
||||
};
|
||||
let mut traversal =
|
||||
GitTraversal::new(worktree.traverse_from_path(
|
||||
let mut traversal = GitTraversal::new(
|
||||
&repo_snapshots,
|
||||
worktree.traverse_from_path(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
entry.path.as_ref(),
|
||||
));
|
||||
),
|
||||
);
|
||||
|
||||
let mut entries_to_add = HashMap::default();
|
||||
worktree_excerpts
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,23 +1,28 @@
|
||||
use collections::HashMap;
|
||||
use git::status::GitSummary;
|
||||
use std::{ops::Deref, path::Path};
|
||||
use sum_tree::Cursor;
|
||||
use text::Bias;
|
||||
use worktree::{Entry, PathProgress, PathTarget, RepositoryEntry, StatusEntry, Traversal};
|
||||
use worktree::{
|
||||
Entry, PathProgress, PathTarget, ProjectEntryId, RepositoryEntry, StatusEntry, Traversal,
|
||||
};
|
||||
|
||||
/// Walks the worktree entries and their associated git statuses.
|
||||
pub struct GitTraversal<'a> {
|
||||
traversal: Traversal<'a>,
|
||||
current_entry_summary: Option<GitSummary>,
|
||||
repo_location: Option<(
|
||||
&'a RepositoryEntry,
|
||||
Cursor<'a, StatusEntry, PathProgress<'a>>,
|
||||
)>,
|
||||
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
repo_location: Option<(ProjectEntryId, Cursor<'a, StatusEntry, PathProgress<'a>>)>,
|
||||
}
|
||||
|
||||
impl<'a> GitTraversal<'a> {
|
||||
pub fn new(traversal: Traversal<'a>) -> GitTraversal<'a> {
|
||||
pub fn new(
|
||||
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
traversal: Traversal<'a>,
|
||||
) -> GitTraversal<'a> {
|
||||
let mut this = GitTraversal {
|
||||
traversal,
|
||||
repo_snapshots,
|
||||
current_entry_summary: None,
|
||||
repo_location: None,
|
||||
};
|
||||
@@ -32,7 +37,20 @@ impl<'a> GitTraversal<'a> {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(repo) = self.traversal.snapshot().repository_for_path(&entry.path) else {
|
||||
let Ok(abs_path) = self.traversal.snapshot().absolutize(&entry.path) else {
|
||||
self.repo_location = None;
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((repo, repo_path)) = self
|
||||
.repo_snapshots
|
||||
.values()
|
||||
.filter_map(|repo_snapshot| {
|
||||
let relative_path = repo_snapshot.relativize_abs_path(&abs_path)?;
|
||||
Some((repo_snapshot, relative_path))
|
||||
})
|
||||
.max_by_key(|(repo, _)| repo.work_directory_abs_path.clone())
|
||||
else {
|
||||
self.repo_location = None;
|
||||
return;
|
||||
};
|
||||
@@ -42,18 +60,19 @@ impl<'a> GitTraversal<'a> {
|
||||
|| self
|
||||
.repo_location
|
||||
.as_ref()
|
||||
.map(|(prev_repo, _)| &prev_repo.work_directory)
|
||||
!= Some(&repo.work_directory)
|
||||
.map(|(prev_repo_id, _)| *prev_repo_id)
|
||||
!= Some(repo.work_directory_id())
|
||||
{
|
||||
self.repo_location = Some((repo, repo.statuses_by_path.cursor::<PathProgress>(&())));
|
||||
self.repo_location = Some((
|
||||
repo.work_directory_id(),
|
||||
repo.statuses_by_path.cursor::<PathProgress>(&()),
|
||||
));
|
||||
}
|
||||
|
||||
let Some((repo, statuses)) = &mut self.repo_location else {
|
||||
let Some((_, statuses)) = &mut self.repo_location else {
|
||||
return;
|
||||
};
|
||||
|
||||
let repo_path = repo.relativize(&entry.path).unwrap();
|
||||
|
||||
if entry.is_dir() {
|
||||
let mut statuses = statuses.clone();
|
||||
statuses.seek_forward(&PathTarget::Path(repo_path.as_ref()), Bias::Left, &());
|
||||
@@ -128,9 +147,15 @@ pub struct ChildEntriesGitIter<'a> {
|
||||
}
|
||||
|
||||
impl<'a> ChildEntriesGitIter<'a> {
|
||||
pub fn new(snapshot: &'a worktree::Snapshot, parent_path: &'a Path) -> Self {
|
||||
let mut traversal =
|
||||
GitTraversal::new(snapshot.traverse_from_path(true, true, true, parent_path));
|
||||
pub fn new(
|
||||
repo_snapshots: &'a HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
worktree_snapshot: &'a worktree::Snapshot,
|
||||
parent_path: &'a Path,
|
||||
) -> Self {
|
||||
let mut traversal = GitTraversal::new(
|
||||
repo_snapshots,
|
||||
worktree_snapshot.traverse_from_path(true, true, true, parent_path),
|
||||
);
|
||||
traversal.advance();
|
||||
ChildEntriesGitIter {
|
||||
parent_path,
|
||||
@@ -215,6 +240,8 @@ impl AsRef<Entry> for GitEntry {
|
||||
mod tests {
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::Project;
|
||||
|
||||
use super::*;
|
||||
use fs::FakeFs;
|
||||
use git::status::{FileStatus, StatusCode, TrackedSummary, UnmergedStatus, UnmergedStatusCode};
|
||||
@@ -222,7 +249,7 @@ mod tests {
|
||||
use serde_json::json;
|
||||
use settings::{Settings as _, SettingsStore};
|
||||
use util::path;
|
||||
use worktree::{Worktree, WorktreeSettings};
|
||||
use worktree::WorktreeSettings;
|
||||
|
||||
const CONFLICT: FileStatus = FileStatus::Unmerged(UnmergedStatus {
|
||||
first_head: UnmergedStatusCode::Updated,
|
||||
@@ -282,44 +309,35 @@ mod tests {
|
||||
&[(Path::new("z2.txt"), StatusCode::Added.index())],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
||||
(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
||||
)
|
||||
});
|
||||
|
||||
let mut traversal =
|
||||
GitTraversal::new(snapshot.traverse_from_path(true, false, true, Path::new("x")));
|
||||
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/x1.txt"));
|
||||
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/x2.txt"));
|
||||
assert_eq!(entry.git_summary, MODIFIED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y1.txt"));
|
||||
assert_eq!(entry.git_summary, GitSummary::CONFLICT);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/y/y2.txt"));
|
||||
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("x/z.txt"));
|
||||
assert_eq!(entry.git_summary, ADDED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("z/z1.txt"));
|
||||
assert_eq!(entry.git_summary, GitSummary::UNCHANGED);
|
||||
let entry = traversal.next().unwrap();
|
||||
assert_eq!(entry.path.as_ref(), Path::new("z/z2.txt"));
|
||||
assert_eq!(entry.git_summary, ADDED);
|
||||
let traversal = GitTraversal::new(
|
||||
&repo_snapshots,
|
||||
worktree_snapshot.traverse_from_path(true, false, true, Path::new("x")),
|
||||
);
|
||||
let entries = traversal
|
||||
.map(|entry| (entry.path.clone(), entry.git_summary))
|
||||
.collect::<Vec<_>>();
|
||||
pretty_assertions::assert_eq!(
|
||||
entries,
|
||||
[
|
||||
(Path::new("x/x1.txt").into(), GitSummary::UNCHANGED),
|
||||
(Path::new("x/x2.txt").into(), MODIFIED),
|
||||
(Path::new("x/y/y1.txt").into(), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y2.txt").into(), GitSummary::UNCHANGED),
|
||||
(Path::new("x/z.txt").into(), ADDED),
|
||||
(Path::new("z/z1.txt").into(), GitSummary::UNCHANGED),
|
||||
(Path::new("z/z2.txt").into(), ADDED),
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -366,23 +384,20 @@ mod tests {
|
||||
&[(Path::new("z2.txt"), StatusCode::Added.index())],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
||||
(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
||||
)
|
||||
});
|
||||
|
||||
// Sanity check the propagation for x/y and z
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||
(Path::new("x/y/y1.txt"), GitSummary::CONFLICT),
|
||||
@@ -390,7 +405,8 @@ mod tests {
|
||||
],
|
||||
);
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("z"), ADDED),
|
||||
(Path::new("z/z1.txt"), GitSummary::UNCHANGED),
|
||||
@@ -400,7 +416,8 @@ mod tests {
|
||||
|
||||
// Test one of the fundamental cases of propagation blocking, the transition from one git repository to another
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/y"), GitSummary::CONFLICT),
|
||||
@@ -410,7 +427,8 @@ mod tests {
|
||||
|
||||
// Sanity check everything around it
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
(Path::new("x/x1.txt"), GitSummary::UNCHANGED),
|
||||
@@ -424,7 +442,8 @@ mod tests {
|
||||
|
||||
// Test the other fundamental case, transitioning from git repository to non-git repository
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new(""), GitSummary::UNCHANGED),
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
@@ -434,7 +453,8 @@ mod tests {
|
||||
|
||||
// And all together now
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new(""), GitSummary::UNCHANGED),
|
||||
(Path::new("x"), MODIFIED + ADDED),
|
||||
@@ -490,21 +510,19 @@ mod tests {
|
||||
],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
||||
(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
||||
)
|
||||
});
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new(""), GitSummary::CONFLICT + MODIFIED + ADDED),
|
||||
(Path::new("g"), GitSummary::CONFLICT),
|
||||
@@ -513,7 +531,8 @@ mod tests {
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new(""), GitSummary::CONFLICT + ADDED + MODIFIED),
|
||||
(Path::new("a"), ADDED + MODIFIED),
|
||||
@@ -530,7 +549,8 @@ mod tests {
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("a/b"), ADDED),
|
||||
(Path::new("a/b/c1.txt"), ADDED),
|
||||
@@ -545,7 +565,8 @@ mod tests {
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("a/b/c1.txt"), ADDED),
|
||||
(Path::new("a/b/c2.txt"), GitSummary::UNCHANGED),
|
||||
@@ -598,26 +619,25 @@ mod tests {
|
||||
&[(Path::new("z2.txt"), StatusCode::Modified.index())],
|
||||
);
|
||||
|
||||
let tree = Worktree::local(
|
||||
Path::new(path!("/root")),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let project = Project::test(fs, [path!("/root").as_ref()], cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
||||
(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
||||
)
|
||||
});
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("y"), GitSummary::CONFLICT + MODIFIED),
|
||||
(Path::new("y/y1.txt"), GitSummary::CONFLICT),
|
||||
@@ -626,7 +646,8 @@ mod tests {
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("z"), MODIFIED),
|
||||
(Path::new("z/z2.txt"), MODIFIED),
|
||||
@@ -634,12 +655,14 @@ mod tests {
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[(Path::new("x"), ADDED), (Path::new("x/x1.txt"), ADDED)],
|
||||
);
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new("x"), ADDED),
|
||||
(Path::new("x/x1.txt"), ADDED),
|
||||
@@ -689,18 +712,11 @@ mod tests {
|
||||
);
|
||||
cx.run_until_parked();
|
||||
|
||||
let tree = Worktree::local(
|
||||
path!("/root").as_ref(),
|
||||
true,
|
||||
fs.clone(),
|
||||
Default::default(),
|
||||
&mut cx.to_async(),
|
||||
)
|
||||
.await
|
||||
.unwrap();
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let (old_entry_ids, old_mtimes) = tree.read_with(cx, |tree, _| {
|
||||
let (old_entry_ids, old_mtimes) = project.read_with(cx, |project, cx| {
|
||||
let tree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
(
|
||||
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
|
||||
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
|
||||
@@ -713,7 +729,8 @@ mod tests {
|
||||
fs.touch_path(path!("/root")).await;
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
let (new_entry_ids, new_mtimes) = tree.read_with(cx, |tree, _| {
|
||||
let (new_entry_ids, new_mtimes) = project.read_with(cx, |project, cx| {
|
||||
let tree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
(
|
||||
tree.entries(true, 0).map(|e| e.id).collect::<Vec<_>>(),
|
||||
tree.entries(true, 0).map(|e| e.mtime).collect::<Vec<_>>(),
|
||||
@@ -734,10 +751,16 @@ mod tests {
|
||||
cx.executor().run_until_parked();
|
||||
cx.executor().advance_clock(Duration::from_secs(1));
|
||||
|
||||
let snapshot = tree.read_with(cx, |tree, _| tree.snapshot());
|
||||
let (repo_snapshots, worktree_snapshot) = project.read_with(cx, |project, cx| {
|
||||
(
|
||||
project.git_store().read(cx).repo_snapshots(cx),
|
||||
project.worktrees(cx).next().unwrap().read(cx).snapshot(),
|
||||
)
|
||||
});
|
||||
|
||||
check_git_statuses(
|
||||
&snapshot,
|
||||
&repo_snapshots,
|
||||
&worktree_snapshot,
|
||||
&[
|
||||
(Path::new(""), MODIFIED),
|
||||
(Path::new("a.txt"), GitSummary::UNCHANGED),
|
||||
@@ -748,11 +771,14 @@ mod tests {
|
||||
|
||||
#[track_caller]
|
||||
fn check_git_statuses(
|
||||
snapshot: &worktree::Snapshot,
|
||||
repo_snapshots: &HashMap<ProjectEntryId, RepositoryEntry>,
|
||||
worktree_snapshot: &worktree::Snapshot,
|
||||
expected_statuses: &[(&Path, GitSummary)],
|
||||
) {
|
||||
let mut traversal =
|
||||
GitTraversal::new(snapshot.traverse_from_path(true, true, false, "".as_ref()));
|
||||
let mut traversal = GitTraversal::new(
|
||||
repo_snapshots,
|
||||
worktree_snapshot.traverse_from_path(true, true, false, "".as_ref()),
|
||||
);
|
||||
let found_statuses = expected_statuses
|
||||
.iter()
|
||||
.map(|&(path, _)| {
|
||||
@@ -762,6 +788,6 @@ mod tests {
|
||||
(path, git_entry.git_summary)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(found_statuses, expected_statuses);
|
||||
pretty_assertions::assert_eq!(found_statuses, expected_statuses);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ mod signature_help;
|
||||
|
||||
use crate::{
|
||||
lsp_store::{LocalLspStore, LspStore},
|
||||
CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, Hover, HoverBlock,
|
||||
HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart, InlayHintLabelPartTooltip,
|
||||
InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent, PrepareRenameResponse,
|
||||
ProjectTransaction, ResolveState,
|
||||
CodeAction, CompletionSource, CoreCompletion, DocumentHighlight, DocumentSymbol, Hover,
|
||||
HoverBlock, HoverBlockKind, InlayHint, InlayHintLabel, InlayHintLabelPart,
|
||||
InlayHintLabelPartTooltip, InlayHintTooltip, Location, LocationLink, LspAction, MarkupContent,
|
||||
PrepareRenameResponse, ProjectTransaction, ResolveState,
|
||||
};
|
||||
use anyhow::{anyhow, Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
@@ -28,7 +28,7 @@ use lsp::{
|
||||
ServerCapabilities,
|
||||
};
|
||||
use signature_help::{lsp_to_proto_signature, proto_to_lsp_signature};
|
||||
use std::{cmp::Reverse, ops::Range, path::Path, sync::Arc};
|
||||
use std::{cmp::Reverse, mem, ops::Range, path::Path, sync::Arc};
|
||||
use text::{BufferId, LineEnding};
|
||||
|
||||
pub use signature_help::SignatureHelp;
|
||||
@@ -199,6 +199,9 @@ pub(crate) struct GetDocumentHighlights {
|
||||
pub position: PointUtf16,
|
||||
}
|
||||
|
||||
#[derive(Debug, Copy, Clone)]
|
||||
pub(crate) struct GetDocumentSymbols;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub(crate) struct GetSignatureHelp {
|
||||
pub position: PointUtf16,
|
||||
@@ -1488,6 +1491,205 @@ impl LspCommand for GetDocumentHighlights {
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetDocumentSymbols {
|
||||
type Response = Vec<DocumentSymbol>;
|
||||
type LspRequest = lsp::request::DocumentSymbolRequest;
|
||||
type ProtoRequest = proto::GetDocumentSymbols;
|
||||
|
||||
fn display_name(&self) -> &str {
|
||||
"Get document symbols"
|
||||
}
|
||||
|
||||
fn check_capabilities(&self, capabilities: AdapterServerCapabilities) -> bool {
|
||||
capabilities
|
||||
.server_capabilities
|
||||
.document_symbol_provider
|
||||
.is_some()
|
||||
}
|
||||
|
||||
fn to_lsp(
|
||||
&self,
|
||||
path: &Path,
|
||||
_: &Buffer,
|
||||
_: &Arc<LanguageServer>,
|
||||
_: &App,
|
||||
) -> Result<lsp::DocumentSymbolParams> {
|
||||
Ok(lsp::DocumentSymbolParams {
|
||||
text_document: make_text_document_identifier(path)?,
|
||||
work_done_progress_params: Default::default(),
|
||||
partial_result_params: Default::default(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn response_from_lsp(
|
||||
self,
|
||||
lsp_symbols: Option<lsp::DocumentSymbolResponse>,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: LanguageServerId,
|
||||
_: AsyncApp,
|
||||
) -> Result<Vec<DocumentSymbol>> {
|
||||
let Some(lsp_symbols) = lsp_symbols else {
|
||||
return Ok(Vec::new());
|
||||
};
|
||||
|
||||
let symbols: Vec<_> = match lsp_symbols {
|
||||
lsp::DocumentSymbolResponse::Flat(symbol_information) => symbol_information
|
||||
.into_iter()
|
||||
.map(|lsp_symbol| DocumentSymbol {
|
||||
name: lsp_symbol.name,
|
||||
kind: lsp_symbol.kind,
|
||||
range: range_from_lsp(lsp_symbol.location.range),
|
||||
selection_range: range_from_lsp(lsp_symbol.location.range),
|
||||
children: Vec::new(),
|
||||
})
|
||||
.collect(),
|
||||
lsp::DocumentSymbolResponse::Nested(nested_responses) => {
|
||||
fn convert_symbol(lsp_symbol: lsp::DocumentSymbol) -> DocumentSymbol {
|
||||
DocumentSymbol {
|
||||
name: lsp_symbol.name,
|
||||
kind: lsp_symbol.kind,
|
||||
range: range_from_lsp(lsp_symbol.range),
|
||||
selection_range: range_from_lsp(lsp_symbol.selection_range),
|
||||
children: lsp_symbol
|
||||
.children
|
||||
.map(|children| {
|
||||
children.into_iter().map(convert_symbol).collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
nested_responses.into_iter().map(convert_symbol).collect()
|
||||
}
|
||||
};
|
||||
Ok(symbols)
|
||||
}
|
||||
|
||||
fn to_proto(&self, project_id: u64, buffer: &Buffer) -> proto::GetDocumentSymbols {
|
||||
proto::GetDocumentSymbols {
|
||||
project_id,
|
||||
buffer_id: buffer.remote_id().into(),
|
||||
version: serialize_version(&buffer.version()),
|
||||
}
|
||||
}
|
||||
|
||||
async fn from_proto(
|
||||
message: proto::GetDocumentSymbols,
|
||||
_: Entity<LspStore>,
|
||||
buffer: Entity<Buffer>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<Self> {
|
||||
buffer
|
||||
.update(&mut cx, |buffer, _| {
|
||||
buffer.wait_for_version(deserialize_version(&message.version))
|
||||
})?
|
||||
.await?;
|
||||
Ok(Self)
|
||||
}
|
||||
|
||||
fn response_to_proto(
|
||||
response: Vec<DocumentSymbol>,
|
||||
_: &mut LspStore,
|
||||
_: PeerId,
|
||||
_: &clock::Global,
|
||||
_: &mut App,
|
||||
) -> proto::GetDocumentSymbolsResponse {
|
||||
let symbols = response
|
||||
.into_iter()
|
||||
.map(|symbol| {
|
||||
fn convert_symbol_to_proto(symbol: DocumentSymbol) -> proto::DocumentSymbol {
|
||||
proto::DocumentSymbol {
|
||||
name: symbol.name.clone(),
|
||||
kind: unsafe { mem::transmute::<lsp::SymbolKind, i32>(symbol.kind) },
|
||||
start: Some(proto::PointUtf16 {
|
||||
row: symbol.range.start.0.row,
|
||||
column: symbol.range.start.0.column,
|
||||
}),
|
||||
end: Some(proto::PointUtf16 {
|
||||
row: symbol.range.end.0.row,
|
||||
column: symbol.range.end.0.column,
|
||||
}),
|
||||
selection_start: Some(proto::PointUtf16 {
|
||||
row: symbol.selection_range.start.0.row,
|
||||
column: symbol.selection_range.start.0.column,
|
||||
}),
|
||||
selection_end: Some(proto::PointUtf16 {
|
||||
row: symbol.selection_range.end.0.row,
|
||||
column: symbol.selection_range.end.0.column,
|
||||
}),
|
||||
children: symbol
|
||||
.children
|
||||
.into_iter()
|
||||
.map(convert_symbol_to_proto)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
convert_symbol_to_proto(symbol)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
proto::GetDocumentSymbolsResponse { symbols }
|
||||
}
|
||||
|
||||
async fn response_from_proto(
|
||||
self,
|
||||
message: proto::GetDocumentSymbolsResponse,
|
||||
_: Entity<LspStore>,
|
||||
_: Entity<Buffer>,
|
||||
_: AsyncApp,
|
||||
) -> Result<Vec<DocumentSymbol>> {
|
||||
let mut symbols = Vec::with_capacity(message.symbols.len());
|
||||
for serialized_symbol in message.symbols {
|
||||
fn deserialize_symbol_with_children(
|
||||
serialized_symbol: proto::DocumentSymbol,
|
||||
) -> Result<DocumentSymbol> {
|
||||
let kind =
|
||||
unsafe { mem::transmute::<i32, lsp::SymbolKind>(serialized_symbol.kind) };
|
||||
|
||||
let start = serialized_symbol
|
||||
.start
|
||||
.ok_or_else(|| anyhow!("invalid start"))?;
|
||||
let end = serialized_symbol
|
||||
.end
|
||||
.ok_or_else(|| anyhow!("invalid end"))?;
|
||||
|
||||
let selection_start = serialized_symbol
|
||||
.selection_start
|
||||
.ok_or_else(|| anyhow!("invalid selection start"))?;
|
||||
let selection_end = serialized_symbol
|
||||
.selection_end
|
||||
.ok_or_else(|| anyhow!("invalid selection end"))?;
|
||||
|
||||
Ok(DocumentSymbol {
|
||||
name: serialized_symbol.name,
|
||||
kind,
|
||||
range: Unclipped(PointUtf16::new(start.row, start.column))
|
||||
..Unclipped(PointUtf16::new(end.row, end.column)),
|
||||
selection_range: Unclipped(PointUtf16::new(
|
||||
selection_start.row,
|
||||
selection_start.column,
|
||||
))
|
||||
..Unclipped(PointUtf16::new(selection_end.row, selection_end.column)),
|
||||
children: serialized_symbol
|
||||
.children
|
||||
.into_iter()
|
||||
.filter_map(|symbol| deserialize_symbol_with_children(symbol).ok())
|
||||
.collect::<Vec<_>>(),
|
||||
})
|
||||
}
|
||||
|
||||
symbols.push(deserialize_symbol_with_children(serialized_symbol)?);
|
||||
}
|
||||
|
||||
Ok(symbols)
|
||||
}
|
||||
|
||||
fn buffer_id_from_proto(message: &proto::GetDocumentSymbols) -> Result<BufferId> {
|
||||
BufferId::new(message.buffer_id)
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl LspCommand for GetSignatureHelp {
|
||||
type Response = Option<SignatureHelp>;
|
||||
|
||||
@@ -3432,6 +3432,7 @@ impl LspStore {
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetDeclaration>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetTypeDefinition>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentHighlights>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetDocumentSymbols>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<GetReferences>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<PrepareRename>);
|
||||
client.add_entity_request_handler(Self::handle_lsp_command::<PerformRename>);
|
||||
@@ -5790,48 +5791,57 @@ impl LspStore {
|
||||
|
||||
_ => continue 'next_server,
|
||||
};
|
||||
let supports_workspace_symbol_request =
|
||||
match server.capabilities().workspace_symbol_provider {
|
||||
Some(OneOf::Left(supported)) => supported,
|
||||
Some(OneOf::Right(_)) => true,
|
||||
None => false,
|
||||
};
|
||||
if !supports_workspace_symbol_request {
|
||||
continue 'next_server;
|
||||
}
|
||||
let worktree_abs_path = worktree.abs_path().clone();
|
||||
let worktree_handle = worktree_handle.clone();
|
||||
let server_id = server.server_id();
|
||||
requests.push(
|
||||
server
|
||||
.request::<lsp::request::WorkspaceSymbolRequest>(
|
||||
lsp::WorkspaceSymbolParams {
|
||||
query: query.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.log_err()
|
||||
.map(move |response| {
|
||||
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
|
||||
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
|
||||
flat_responses.into_iter().map(|lsp_symbol| {
|
||||
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
|
||||
nested_responses.into_iter().filter_map(|lsp_symbol| {
|
||||
let location = match lsp_symbol.location {
|
||||
OneOf::Left(location) => location,
|
||||
OneOf::Right(_) => {
|
||||
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
|
||||
return None
|
||||
}
|
||||
};
|
||||
Some((lsp_symbol.name, lsp_symbol.kind, location))
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
}).unwrap_or_default();
|
||||
|
||||
WorkspaceSymbolsResult {
|
||||
server_id,
|
||||
lsp_adapter,
|
||||
worktree: worktree_handle.downgrade(),
|
||||
worktree_abs_path,
|
||||
lsp_symbols,
|
||||
server
|
||||
.request::<lsp::request::WorkspaceSymbolRequest>(
|
||||
lsp::WorkspaceSymbolParams {
|
||||
query: query.to_string(),
|
||||
..Default::default()
|
||||
},
|
||||
)
|
||||
.log_err()
|
||||
.map(move |response| {
|
||||
let lsp_symbols = response.flatten().map(|symbol_response| match symbol_response {
|
||||
lsp::WorkspaceSymbolResponse::Flat(flat_responses) => {
|
||||
flat_responses.into_iter().map(|lsp_symbol| {
|
||||
(lsp_symbol.name, lsp_symbol.kind, lsp_symbol.location)
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
}),
|
||||
);
|
||||
lsp::WorkspaceSymbolResponse::Nested(nested_responses) => {
|
||||
nested_responses.into_iter().filter_map(|lsp_symbol| {
|
||||
let location = match lsp_symbol.location {
|
||||
OneOf::Left(location) => location,
|
||||
OneOf::Right(_) => {
|
||||
log::error!("Unexpected: client capabilities forbid symbol resolutions in workspace.symbol.resolveSupport");
|
||||
return None
|
||||
}
|
||||
};
|
||||
Some((lsp_symbol.name, lsp_symbol.kind, location))
|
||||
}).collect::<Vec<_>>()
|
||||
}
|
||||
}).unwrap_or_default();
|
||||
|
||||
WorkspaceSymbolsResult {
|
||||
server_id,
|
||||
lsp_adapter,
|
||||
worktree: worktree_handle.downgrade(),
|
||||
worktree_abs_path,
|
||||
lsp_symbols,
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
requested_servers.append(&mut servers_to_query);
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ mod direnv;
|
||||
mod environment;
|
||||
use buffer_diff::BufferDiff;
|
||||
pub use environment::{EnvironmentErrorMessage, ProjectEnvironmentEvent};
|
||||
use git_store::Repository;
|
||||
use git_store::{GitEvent, Repository};
|
||||
pub mod search_history;
|
||||
mod yarn;
|
||||
|
||||
@@ -270,7 +270,6 @@ pub enum Event {
|
||||
WorktreeOrderChanged,
|
||||
WorktreeRemoved(WorktreeId),
|
||||
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
|
||||
WorktreeUpdatedGitRepositories(WorktreeId),
|
||||
DiskBasedDiagnosticsStarted {
|
||||
language_server_id: LanguageServerId,
|
||||
},
|
||||
@@ -300,6 +299,8 @@ pub enum Event {
|
||||
RevealInProjectPanel(ProjectEntryId),
|
||||
SnippetEdit(BufferId, Vec<(lsp::Range, Snippet)>),
|
||||
ExpandedAllForEntry(WorktreeId, ProjectEntryId),
|
||||
GitStateUpdated,
|
||||
ActiveRepositoryChanged,
|
||||
}
|
||||
|
||||
pub enum DebugAdapterClientState {
|
||||
@@ -659,6 +660,15 @@ pub struct Symbol {
|
||||
pub signature: [u8; 32],
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct DocumentSymbol {
|
||||
pub name: String,
|
||||
pub kind: lsp::SymbolKind,
|
||||
pub range: Range<Unclipped<PointUtf16>>,
|
||||
pub selection_range: Range<Unclipped<PointUtf16>>,
|
||||
pub children: Vec<DocumentSymbol>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct HoverBlock {
|
||||
pub text: String,
|
||||
@@ -784,8 +794,6 @@ impl Project {
|
||||
client.add_entity_message_handler(Self::handle_unshare_project);
|
||||
client.add_entity_request_handler(Self::handle_update_buffer);
|
||||
client.add_entity_message_handler(Self::handle_update_worktree);
|
||||
client.add_entity_message_handler(Self::handle_update_repository);
|
||||
client.add_entity_message_handler(Self::handle_remove_repository);
|
||||
client.add_entity_request_handler(Self::handle_synchronize_buffers);
|
||||
|
||||
client.add_entity_request_handler(Self::handle_search_candidate_buffers);
|
||||
@@ -913,6 +921,7 @@ impl Project {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
cx.subscribe(&git_store, Self::on_git_store_event).detach();
|
||||
|
||||
cx.subscribe(&lsp_store, Self::on_lsp_store_event).detach();
|
||||
|
||||
@@ -1127,8 +1136,6 @@ impl Project {
|
||||
|
||||
ssh_proto.add_entity_message_handler(Self::handle_create_buffer_for_peer);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_update_worktree);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_update_repository);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_remove_repository);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_update_project);
|
||||
ssh_proto.add_entity_message_handler(Self::handle_toast);
|
||||
ssh_proto.add_entity_request_handler(Self::handle_language_server_prompt_request);
|
||||
@@ -1469,7 +1476,7 @@ impl Project {
|
||||
) -> Entity<Project> {
|
||||
use clock::FakeSystemClock;
|
||||
|
||||
let fs = Arc::new(RealFs::default());
|
||||
let fs = Arc::new(RealFs::new(None, cx.background_executor().clone()));
|
||||
let languages = LanguageRegistry::test(cx.background_executor().clone());
|
||||
let clock = Arc::new(FakeSystemClock::new());
|
||||
let http_client = http_client::FakeHttpClient::with_404_response();
|
||||
@@ -2031,6 +2038,11 @@ impl Project {
|
||||
self.worktree_store.update(cx, |worktree_store, cx| {
|
||||
worktree_store.send_project_updates(cx);
|
||||
});
|
||||
if let Some(remote_id) = self.remote_id() {
|
||||
self.git_store.update(cx, |git_store, cx| {
|
||||
git_store.shared(remote_id, self.client.clone().into(), cx)
|
||||
});
|
||||
}
|
||||
cx.emit(Event::Reshared);
|
||||
Ok(())
|
||||
}
|
||||
@@ -2698,6 +2710,19 @@ impl Project {
|
||||
}
|
||||
}
|
||||
|
||||
fn on_git_store_event(
|
||||
&mut self,
|
||||
_: Entity<GitStore>,
|
||||
event: &GitEvent,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
match event {
|
||||
GitEvent::GitStateUpdated => cx.emit(Event::GitStateUpdated),
|
||||
GitEvent::ActiveRepositoryChanged => cx.emit(Event::ActiveRepositoryChanged),
|
||||
GitEvent::FileSystemUpdated | GitEvent::IndexWriteError(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
fn on_ssh_event(
|
||||
&mut self,
|
||||
_: Entity<SshRemoteClient>,
|
||||
@@ -2783,12 +2808,11 @@ impl Project {
|
||||
.report_discovered_project_events(*worktree_id, changes);
|
||||
cx.emit(Event::WorktreeUpdatedEntries(*worktree_id, changes.clone()))
|
||||
}
|
||||
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(worktree_id) => {
|
||||
cx.emit(Event::WorktreeUpdatedGitRepositories(*worktree_id))
|
||||
}
|
||||
WorktreeStoreEvent::WorktreeDeletedEntry(worktree_id, id) => {
|
||||
cx.emit(Event::DeletedEntry(*worktree_id, *id))
|
||||
}
|
||||
// Listen to the GitStore instead.
|
||||
WorktreeStoreEvent::WorktreeUpdatedGitRepositories(_, _) => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3222,6 +3246,19 @@ impl Project {
|
||||
self.document_highlights_impl(buffer, position, cx)
|
||||
}
|
||||
|
||||
pub fn document_symbols(
|
||||
&mut self,
|
||||
buffer: &Entity<Buffer>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Vec<DocumentSymbol>>> {
|
||||
self.request_lsp(
|
||||
buffer.clone(),
|
||||
LanguageServerToQuery::FirstCapable,
|
||||
GetDocumentSymbols,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
|
||||
pub fn symbols(&self, query: &str, cx: &mut Context<Self>) -> Task<Result<Vec<Symbol>>> {
|
||||
self.lsp_store
|
||||
.update(cx, |lsp_store, cx| lsp_store.symbols(query, cx))
|
||||
@@ -4287,43 +4324,7 @@ impl Project {
|
||||
if let Some(worktree) = this.worktree_for_id(worktree_id, cx) {
|
||||
worktree.update(cx, |worktree, _| {
|
||||
let worktree = worktree.as_remote_mut().unwrap();
|
||||
worktree.update_from_remote(envelope.payload.into());
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
|
||||
async fn handle_update_repository(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::UpdateRepository>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some((worktree, _relative_path)) =
|
||||
this.find_worktree(envelope.payload.abs_path.as_ref(), cx)
|
||||
{
|
||||
worktree.update(cx, |worktree, _| {
|
||||
let worktree = worktree.as_remote_mut().unwrap();
|
||||
worktree.update_from_remote(envelope.payload.into());
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
})?
|
||||
}
|
||||
|
||||
async fn handle_remove_repository(
|
||||
this: Entity<Self>,
|
||||
envelope: TypedEnvelope<proto::RemoveRepository>,
|
||||
mut cx: AsyncApp,
|
||||
) -> Result<()> {
|
||||
this.update(&mut cx, |this, cx| {
|
||||
if let Some(worktree) =
|
||||
this.worktree_for_entry(ProjectEntryId::from_proto(envelope.payload.id), cx)
|
||||
{
|
||||
worktree.update(cx, |worktree, _| {
|
||||
let worktree = worktree.as_remote_mut().unwrap();
|
||||
worktree.update_from_remote(envelope.payload.into());
|
||||
worktree.update_from_remote(envelope.payload);
|
||||
});
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -6,7 +6,8 @@ use buffer_diff::{
|
||||
};
|
||||
use fs::FakeFs;
|
||||
use futures::{future, StreamExt};
|
||||
use gpui::{App, SemanticVersion, UpdateGlobal};
|
||||
use git::repository::RepoPath;
|
||||
use gpui::{App, BackgroundExecutor, SemanticVersion, UpdateGlobal};
|
||||
use http_client::Url;
|
||||
use language::{
|
||||
language_settings::{language_settings, AllLanguageSettings, LanguageSettingsContent},
|
||||
@@ -34,6 +35,7 @@ use util::{
|
||||
test::{marked_text_offsets, TempTree},
|
||||
uri, TryFutureExt as _,
|
||||
};
|
||||
use worktree::WorktreeModelHandle as _;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_block_via_channel(cx: &mut gpui::TestAppContext) {
|
||||
@@ -97,7 +99,12 @@ async fn test_symlinks(cx: &mut gpui::TestAppContext) {
|
||||
)
|
||||
.unwrap();
|
||||
|
||||
let project = Project::test(Arc::new(RealFs::default()), [root_link_path.as_ref()], cx).await;
|
||||
let project = Project::test(
|
||||
Arc::new(RealFs::new(None, cx.executor())),
|
||||
[root_link_path.as_ref()],
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
let tree = project.worktrees(cx).next().unwrap().read(cx);
|
||||
@@ -3330,7 +3337,7 @@ async fn test_rescan_and_remote_updates(cx: &mut gpui::TestAppContext) {
|
||||
}
|
||||
}));
|
||||
|
||||
let project = Project::test(Arc::new(RealFs::default()), [dir.path()], cx).await;
|
||||
let project = Project::test(Arc::new(RealFs::new(None, cx.executor())), [dir.path()], cx).await;
|
||||
|
||||
let buffer_for_path = |path: &'static str, cx: &mut gpui::TestAppContext| {
|
||||
let buffer = project.update(cx, |p, cx| p.open_local_buffer(dir.path().join(path), cx));
|
||||
@@ -6769,6 +6776,158 @@ async fn test_single_file_diffs(cx: &mut gpui::TestAppContext) {
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_repository_and_path_for_project_path(
|
||||
background_executor: BackgroundExecutor,
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(background_executor);
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"c.txt": "",
|
||||
"dir1": {
|
||||
".git": {},
|
||||
"deps": {
|
||||
"dep1": {
|
||||
".git": {},
|
||||
"src": {
|
||||
"a.txt": ""
|
||||
}
|
||||
}
|
||||
},
|
||||
"src": {
|
||||
"b.txt": ""
|
||||
}
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let tree_id = tree.read_with(cx, |tree, _| tree.id());
|
||||
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
project.read_with(cx, |project, cx| {
|
||||
let git_store = project.git_store().read(cx);
|
||||
let pairs = [
|
||||
("c.txt", None),
|
||||
("dir1/src/b.txt", Some((path!("/root/dir1"), "src/b.txt"))),
|
||||
(
|
||||
"dir1/deps/dep1/src/a.txt",
|
||||
Some((path!("/root/dir1/deps/dep1"), "src/a.txt")),
|
||||
),
|
||||
];
|
||||
let expected = pairs
|
||||
.iter()
|
||||
.map(|(path, result)| {
|
||||
(
|
||||
path,
|
||||
result.map(|(repo, repo_path)| {
|
||||
(Path::new(repo).to_owned(), RepoPath::from(repo_path))
|
||||
}),
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let actual = pairs
|
||||
.iter()
|
||||
.map(|(path, _)| {
|
||||
let project_path = (tree_id, Path::new(path)).into();
|
||||
let result = maybe!({
|
||||
let (repo, repo_path) =
|
||||
git_store.repository_and_path_for_project_path(&project_path, cx)?;
|
||||
Some((
|
||||
repo.read(cx)
|
||||
.repository_entry
|
||||
.work_directory_abs_path
|
||||
.clone(),
|
||||
repo_path,
|
||||
))
|
||||
});
|
||||
(path, result)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
pretty_assertions::assert_eq!(expected, actual);
|
||||
});
|
||||
|
||||
fs.remove_dir(path!("/root/dir1/.git").as_ref(), RemoveOptions::default())
|
||||
.await
|
||||
.unwrap();
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
project.read_with(cx, |project, cx| {
|
||||
let git_store = project.git_store().read(cx);
|
||||
assert_eq!(
|
||||
git_store.repository_and_path_for_project_path(
|
||||
&(tree_id, Path::new("dir1/src/b.txt")).into(),
|
||||
cx
|
||||
),
|
||||
None
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_home_dir_as_git_repository(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx);
|
||||
let fs = FakeFs::new(cx.background_executor.clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"home": {
|
||||
".git": {},
|
||||
"project": {
|
||||
"a.txt": "A"
|
||||
},
|
||||
},
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
fs.set_home_dir(Path::new(path!("/root/home")).to_owned());
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root/home/project").as_ref()], cx).await;
|
||||
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let tree_id = tree.read_with(cx, |tree, _| tree.id());
|
||||
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
project.read_with(cx, |project, cx| {
|
||||
let containing = project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(&(tree_id, "a.txt").into(), cx);
|
||||
assert!(containing.is_none());
|
||||
});
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root/home").as_ref()], cx).await;
|
||||
let tree = project.read_with(cx, |project, cx| project.worktrees(cx).next().unwrap());
|
||||
let tree_id = tree.read_with(cx, |tree, _| tree.id());
|
||||
tree.read_with(cx, |tree, _| tree.as_local().unwrap().scan_complete())
|
||||
.await;
|
||||
tree.flush_fs_events(cx).await;
|
||||
|
||||
project.read_with(cx, |project, cx| {
|
||||
let containing = project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_project_path(&(tree_id, "project/a.txt").into(), cx);
|
||||
assert_eq!(
|
||||
containing
|
||||
.unwrap()
|
||||
.0
|
||||
.read(cx)
|
||||
.repository_entry
|
||||
.work_directory_abs_path,
|
||||
Path::new(path!("/root/home"))
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
async fn search(
|
||||
project: &Entity<Project>,
|
||||
query: SearchQuery,
|
||||
|
||||
@@ -26,7 +26,10 @@ use smol::{
|
||||
};
|
||||
use text::ReplicaId;
|
||||
use util::{paths::SanitizedPath, ResultExt};
|
||||
use worktree::{Entry, ProjectEntryId, UpdatedEntriesSet, Worktree, WorktreeId, WorktreeSettings};
|
||||
use worktree::{
|
||||
Entry, ProjectEntryId, UpdatedEntriesSet, UpdatedGitRepositoriesSet, Worktree, WorktreeId,
|
||||
WorktreeSettings,
|
||||
};
|
||||
|
||||
use crate::{search::SearchQuery, ProjectPath};
|
||||
|
||||
@@ -66,7 +69,7 @@ pub enum WorktreeStoreEvent {
|
||||
WorktreeOrderChanged,
|
||||
WorktreeUpdateSent(Entity<Worktree>),
|
||||
WorktreeUpdatedEntries(WorktreeId, UpdatedEntriesSet),
|
||||
WorktreeUpdatedGitRepositories(WorktreeId),
|
||||
WorktreeUpdatedGitRepositories(WorktreeId, UpdatedGitRepositoriesSet),
|
||||
WorktreeDeletedEntry(WorktreeId, ProjectEntryId),
|
||||
}
|
||||
|
||||
@@ -156,6 +159,11 @@ impl WorktreeStore {
|
||||
None
|
||||
}
|
||||
|
||||
pub fn absolutize(&self, project_path: &ProjectPath, cx: &App) -> Option<PathBuf> {
|
||||
let worktree = self.worktree_for_id(project_path.worktree_id, cx)?;
|
||||
worktree.read(cx).absolutize(&project_path.path).ok()
|
||||
}
|
||||
|
||||
pub fn find_or_create_worktree(
|
||||
&mut self,
|
||||
abs_path: impl AsRef<Path>,
|
||||
@@ -367,9 +375,10 @@ impl WorktreeStore {
|
||||
changes.clone(),
|
||||
));
|
||||
}
|
||||
worktree::Event::UpdatedGitRepositories(_) => {
|
||||
worktree::Event::UpdatedGitRepositories(set) => {
|
||||
cx.emit(WorktreeStoreEvent::WorktreeUpdatedGitRepositories(
|
||||
worktree_id,
|
||||
set.clone(),
|
||||
));
|
||||
}
|
||||
worktree::Event::DeletedEntry(id) => {
|
||||
@@ -561,44 +570,12 @@ impl WorktreeStore {
|
||||
let client = client.clone();
|
||||
async move {
|
||||
if client.is_via_collab() {
|
||||
match update {
|
||||
proto::WorktreeRelatedMessage::UpdateWorktree(
|
||||
update,
|
||||
) => {
|
||||
client
|
||||
.request(update)
|
||||
.map(|result| result.log_err().is_some())
|
||||
.await
|
||||
}
|
||||
proto::WorktreeRelatedMessage::UpdateRepository(
|
||||
update,
|
||||
) => {
|
||||
client
|
||||
.request(update)
|
||||
.map(|result| result.log_err().is_some())
|
||||
.await
|
||||
}
|
||||
proto::WorktreeRelatedMessage::RemoveRepository(
|
||||
update,
|
||||
) => {
|
||||
client
|
||||
.request(update)
|
||||
.map(|result| result.log_err().is_some())
|
||||
.await
|
||||
}
|
||||
}
|
||||
client
|
||||
.request(update)
|
||||
.map(|result| result.log_err().is_some())
|
||||
.await
|
||||
} else {
|
||||
match update {
|
||||
proto::WorktreeRelatedMessage::UpdateWorktree(
|
||||
update,
|
||||
) => client.send(update).log_err().is_some(),
|
||||
proto::WorktreeRelatedMessage::UpdateRepository(
|
||||
update,
|
||||
) => client.send(update).log_err().is_some(),
|
||||
proto::WorktreeRelatedMessage::RemoveRepository(
|
||||
update,
|
||||
) => client.send(update).log_err().is_some(),
|
||||
}
|
||||
client.send(update).log_err().is_some()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ use project_panel_settings::{
|
||||
};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsStore};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
use smallvec::SmallVec;
|
||||
use std::any::TypeId;
|
||||
use std::{
|
||||
@@ -197,6 +197,7 @@ actions!(
|
||||
Open,
|
||||
OpenPermanent,
|
||||
ToggleFocus,
|
||||
ToggleHideGitIgnore,
|
||||
NewSearchInDirectory,
|
||||
UnfoldDirectory,
|
||||
FoldDirectory,
|
||||
@@ -233,6 +234,13 @@ pub fn init(cx: &mut App) {
|
||||
workspace.register_action(|workspace, _: &ToggleFocus, window, cx| {
|
||||
workspace.toggle_panel_focus::<ProjectPanel>(window, cx);
|
||||
});
|
||||
|
||||
workspace.register_action(|workspace, _: &ToggleHideGitIgnore, _, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ProjectPanelSettings>(fs, cx, move |setting, _| {
|
||||
setting.hide_gitignore = Some(!setting.hide_gitignore.unwrap_or(false));
|
||||
})
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
@@ -326,7 +334,8 @@ impl ProjectPanel {
|
||||
this.update_visible_entries(None, cx);
|
||||
cx.notify();
|
||||
}
|
||||
project::Event::WorktreeUpdatedGitRepositories(_)
|
||||
project::Event::GitStateUpdated
|
||||
| project::Event::ActiveRepositoryChanged
|
||||
| project::Event::WorktreeUpdatedEntries(_, _)
|
||||
| project::Event::WorktreeAdded(_)
|
||||
| project::Event::WorktreeOrderChanged => {
|
||||
@@ -414,6 +423,9 @@ impl ProjectPanel {
|
||||
cx.observe_global::<SettingsStore>(move |this, cx| {
|
||||
let new_settings = *ProjectPanelSettings::get_global(cx);
|
||||
if project_panel_settings != new_settings {
|
||||
if project_panel_settings.hide_gitignore != new_settings.hide_gitignore {
|
||||
this.update_visible_entries(None, cx);
|
||||
}
|
||||
project_panel_settings = new_settings;
|
||||
this.update_diagnostics(cx);
|
||||
cx.notify();
|
||||
@@ -1536,13 +1548,13 @@ impl ProjectPanel {
|
||||
if sanitized_entries.is_empty() {
|
||||
return None;
|
||||
}
|
||||
|
||||
let project = self.project.read(cx);
|
||||
let (worktree_id, worktree) = sanitized_entries
|
||||
.iter()
|
||||
.map(|entry| entry.worktree_id)
|
||||
.filter_map(|id| project.worktree_for_id(id, cx).map(|w| (id, w.read(cx))))
|
||||
.max_by(|(_, a), (_, b)| a.root_name().cmp(b.root_name()))?;
|
||||
let git_store = project.git_store().read(cx);
|
||||
|
||||
let marked_entries_in_worktree = sanitized_entries
|
||||
.iter()
|
||||
@@ -1567,17 +1579,20 @@ impl ProjectPanel {
|
||||
let parent_entry = worktree.entry_for_path(parent_path)?;
|
||||
|
||||
// Remove all siblings that are being deleted except the last marked entry
|
||||
let snapshot = worktree.snapshot();
|
||||
let mut siblings: Vec<_> = ChildEntriesGitIter::new(&snapshot, parent_path)
|
||||
.filter(|sibling| {
|
||||
sibling.id == latest_entry.id
|
||||
|| !marked_entries_in_worktree.contains(&&SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: sibling.id,
|
||||
})
|
||||
})
|
||||
.map(|entry| entry.to_owned())
|
||||
.collect();
|
||||
let repo_snapshots = git_store.repo_snapshots(cx);
|
||||
let worktree_snapshot = worktree.snapshot();
|
||||
let hide_gitignore = ProjectPanelSettings::get_global(cx).hide_gitignore;
|
||||
let mut siblings: Vec<_> =
|
||||
ChildEntriesGitIter::new(&repo_snapshots, &worktree_snapshot, parent_path)
|
||||
.filter(|sibling| {
|
||||
(sibling.id == latest_entry.id)
|
||||
|| (!marked_entries_in_worktree.contains(&&SelectedEntry {
|
||||
worktree_id,
|
||||
entry_id: sibling.id,
|
||||
}) && (!hide_gitignore || !sibling.is_ignored))
|
||||
})
|
||||
.map(|entry| entry.to_owned())
|
||||
.collect();
|
||||
|
||||
project::sort_worktree_entries(&mut siblings);
|
||||
let sibling_entry_index = siblings
|
||||
@@ -2590,8 +2605,11 @@ impl ProjectPanel {
|
||||
new_selected_entry: Option<(WorktreeId, ProjectEntryId)>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let auto_collapse_dirs = ProjectPanelSettings::get_global(cx).auto_fold_dirs;
|
||||
let settings = ProjectPanelSettings::get_global(cx);
|
||||
let auto_collapse_dirs = settings.auto_fold_dirs;
|
||||
let hide_gitignore = settings.hide_gitignore;
|
||||
let project = self.project.read(cx);
|
||||
let repo_snapshots = project.git_store().read(cx).repo_snapshots(cx);
|
||||
self.last_worktree_root_id = project
|
||||
.visible_worktrees(cx)
|
||||
.next_back()
|
||||
@@ -2602,15 +2620,15 @@ impl ProjectPanel {
|
||||
self.visible_entries.clear();
|
||||
let mut max_width_item = None;
|
||||
for worktree in project.visible_worktrees(cx) {
|
||||
let snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_id = snapshot.id();
|
||||
let worktree_snapshot = worktree.read(cx).snapshot();
|
||||
let worktree_id = worktree_snapshot.id();
|
||||
|
||||
let expanded_dir_ids = match self.expanded_dir_ids.entry(worktree_id) {
|
||||
hash_map::Entry::Occupied(e) => e.into_mut(),
|
||||
hash_map::Entry::Vacant(e) => {
|
||||
// The first time a worktree's root entry becomes available,
|
||||
// mark that root entry as expanded.
|
||||
if let Some(entry) = snapshot.root_entry() {
|
||||
if let Some(entry) = worktree_snapshot.root_entry() {
|
||||
e.insert(vec![entry.id]).as_slice()
|
||||
} else {
|
||||
&[]
|
||||
@@ -2632,14 +2650,15 @@ impl ProjectPanel {
|
||||
}
|
||||
|
||||
let mut visible_worktree_entries = Vec::new();
|
||||
let mut entry_iter = GitTraversal::new(snapshot.entries(true, 0));
|
||||
let mut entry_iter =
|
||||
GitTraversal::new(&repo_snapshots, worktree_snapshot.entries(true, 0));
|
||||
let mut auto_folded_ancestors = vec![];
|
||||
while let Some(entry) = entry_iter.entry() {
|
||||
if auto_collapse_dirs && entry.kind.is_dir() {
|
||||
auto_folded_ancestors.push(entry.id);
|
||||
if !self.unfolded_dir_ids.contains(&entry.id) {
|
||||
if let Some(root_path) = snapshot.root_entry() {
|
||||
let mut child_entries = snapshot.child_entries(&entry.path);
|
||||
if let Some(root_path) = worktree_snapshot.root_entry() {
|
||||
let mut child_entries = worktree_snapshot.child_entries(&entry.path);
|
||||
if let Some(child) = child_entries.next() {
|
||||
if entry.path != root_path.path
|
||||
&& child_entries.next().is_none()
|
||||
@@ -2675,7 +2694,9 @@ impl ProjectPanel {
|
||||
}
|
||||
}
|
||||
auto_folded_ancestors.clear();
|
||||
visible_worktree_entries.push(entry.to_owned());
|
||||
if !hide_gitignore || !entry.is_ignored {
|
||||
visible_worktree_entries.push(entry.to_owned());
|
||||
}
|
||||
let precedes_new_entry = if let Some(new_entry_id) = new_entry_parent_id {
|
||||
entry.id == new_entry_id || {
|
||||
self.ancestors.get(&entry.id).map_or(false, |entries| {
|
||||
@@ -2688,7 +2709,7 @@ impl ProjectPanel {
|
||||
} else {
|
||||
false
|
||||
};
|
||||
if precedes_new_entry {
|
||||
if precedes_new_entry && (!hide_gitignore || !entry.is_ignored) {
|
||||
visible_worktree_entries.push(GitEntry {
|
||||
entry: Entry {
|
||||
id: NEW_ENTRY_ID,
|
||||
@@ -3282,10 +3303,16 @@ impl ProjectPanel {
|
||||
.cloned();
|
||||
}
|
||||
|
||||
let repo_snapshots = self
|
||||
.project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repo_snapshots(cx);
|
||||
let worktree = self.project.read(cx).worktree_for_id(worktree_id, cx)?;
|
||||
worktree.update(cx, |tree, _| {
|
||||
utils::ReversibleIterable::new(
|
||||
GitTraversal::new(tree.entries(true, 0usize)),
|
||||
GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize)),
|
||||
reverse_search,
|
||||
)
|
||||
.find_single_ended(|ele| predicate(*ele, worktree_id))
|
||||
@@ -3305,6 +3332,12 @@ impl ProjectPanel {
|
||||
.iter()
|
||||
.map(|(worktree_id, _, _)| *worktree_id)
|
||||
.collect();
|
||||
let repo_snapshots = self
|
||||
.project
|
||||
.read(cx)
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repo_snapshots(cx);
|
||||
|
||||
let mut last_found: Option<SelectedEntry> = None;
|
||||
|
||||
@@ -3319,12 +3352,10 @@ impl ProjectPanel {
|
||||
let root_entry = tree.root_entry()?;
|
||||
let tree_id = tree.id();
|
||||
|
||||
let mut first_iter = GitTraversal::new(tree.traverse_from_path(
|
||||
true,
|
||||
true,
|
||||
true,
|
||||
entry.path.as_ref(),
|
||||
));
|
||||
let mut first_iter = GitTraversal::new(
|
||||
&repo_snapshots,
|
||||
tree.traverse_from_path(true, true, true, entry.path.as_ref()),
|
||||
);
|
||||
|
||||
if reverse_search {
|
||||
first_iter.next();
|
||||
@@ -3337,7 +3368,7 @@ impl ProjectPanel {
|
||||
.find(|ele| predicate(*ele, tree_id))
|
||||
.map(|ele| ele.to_owned());
|
||||
|
||||
let second_iter = GitTraversal::new(tree.entries(true, 0usize));
|
||||
let second_iter = GitTraversal::new(&repo_snapshots, tree.entries(true, 0usize));
|
||||
|
||||
let second = if reverse_search {
|
||||
second_iter
|
||||
|
||||
@@ -31,6 +31,7 @@ pub enum EntrySpacing {
|
||||
#[derive(Deserialize, Debug, Clone, Copy, PartialEq)]
|
||||
pub struct ProjectPanelSettings {
|
||||
pub button: bool,
|
||||
pub hide_gitignore: bool,
|
||||
pub default_width: Pixels,
|
||||
pub dock: ProjectPanelDockPosition,
|
||||
pub entry_spacing: EntrySpacing,
|
||||
@@ -93,6 +94,10 @@ pub struct ProjectPanelSettingsContent {
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Whether to hide gitignore files in the project panel.
|
||||
///
|
||||
/// Default: false
|
||||
pub hide_gitignore: Option<bool>,
|
||||
/// Customize default width (in pixels) taken by project panel
|
||||
///
|
||||
/// Default: 240
|
||||
|
||||
@@ -3735,6 +3735,172 @@ async fn test_basic_file_deletion_scenarios(cx: &mut gpui::TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_deletion_gitignored(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"aa": "// Testing 1",
|
||||
"bb": "// Testing 2",
|
||||
"cc": "// Testing 3",
|
||||
"dd": "// Testing 4",
|
||||
"ee": "// Testing 5",
|
||||
"ff": "// Testing 6",
|
||||
"gg": "// Testing 7",
|
||||
"hh": "// Testing 8",
|
||||
"ii": "// Testing 8",
|
||||
".gitignore": "bb\ndd\nee\nff\nii\n'",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
// Test 1: Auto selection with one gitignored file next to the deleted file
|
||||
cx.update(|_, cx| {
|
||||
let settings = *ProjectPanelSettings::get_global(cx);
|
||||
ProjectPanelSettings::override_global(
|
||||
ProjectPanelSettings {
|
||||
hide_gitignore: true,
|
||||
..settings
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
select_path(&panel, "root/aa", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" .gitignore",
|
||||
" aa <== selected",
|
||||
" cc",
|
||||
" gg",
|
||||
" hh"
|
||||
],
|
||||
"Initial state should hide files on .gitignore"
|
||||
);
|
||||
|
||||
submit_deletion(&panel, cx);
|
||||
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" .gitignore",
|
||||
" cc <== selected",
|
||||
" gg",
|
||||
" hh"
|
||||
],
|
||||
"Should select next entry not on .gitignore"
|
||||
);
|
||||
|
||||
// Test 2: Auto selection with many gitignored files next to the deleted file
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" .gitignore",
|
||||
" gg <== selected",
|
||||
" hh"
|
||||
],
|
||||
"Should select next entry not on .gitignore"
|
||||
);
|
||||
|
||||
// Test 3: Auto selection of entry before deleted file
|
||||
select_path(&panel, "root/hh", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" .gitignore",
|
||||
" gg",
|
||||
" hh <== selected"
|
||||
],
|
||||
"Should select next entry not on .gitignore"
|
||||
);
|
||||
submit_deletion(&panel, cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&["v root", " .gitignore", " gg <== selected"],
|
||||
"Should select next entry not on .gitignore"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_nested_deletion_gitignore(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
let fs = FakeFs::new(cx.executor().clone());
|
||||
fs.insert_tree(
|
||||
path!("/root"),
|
||||
json!({
|
||||
"dir1": {
|
||||
"file1": "// Testing",
|
||||
"file2": "// Testing",
|
||||
"file3": "// Testing"
|
||||
},
|
||||
"aa": "// Testing",
|
||||
".gitignore": "file1\nfile3\n",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs.clone(), [path!("/root").as_ref()], cx).await;
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
cx.update(|_, cx| {
|
||||
let settings = *ProjectPanelSettings::get_global(cx);
|
||||
ProjectPanelSettings::override_global(
|
||||
ProjectPanelSettings {
|
||||
hide_gitignore: true,
|
||||
..settings
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let panel = workspace.update(cx, ProjectPanel::new).unwrap();
|
||||
|
||||
// Test 1: Visible items should exclude files on gitignore
|
||||
toggle_expand_dir(&panel, "root/dir1", cx);
|
||||
select_path(&panel, "root/dir1/file2", cx);
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1",
|
||||
" file2 <== selected",
|
||||
" .gitignore",
|
||||
" aa"
|
||||
],
|
||||
"Initial state should hide files on .gitignore"
|
||||
);
|
||||
submit_deletion(&panel, cx);
|
||||
|
||||
// Test 2: Auto selection should go to the parent
|
||||
assert_eq!(
|
||||
visible_entries_as_strings(&panel, 0..10, cx),
|
||||
&[
|
||||
"v root",
|
||||
" v dir1 <== selected",
|
||||
" .gitignore",
|
||||
" aa"
|
||||
],
|
||||
"Initial state should hide files on .gitignore"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_complex_selection_scenarios(cx: &mut gpui::TestAppContext) {
|
||||
init_test_with_editor(cx);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user