mirror of
https://github.com/zed-industries/zed.git
synced 2026-06-01 03:14:56 +07:00
Compare commits
15 commits
53fd8ba0d0
...
c557685ebe
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c557685ebe | ||
|
|
09165c15dc | ||
|
|
e2e7a6769e | ||
|
|
5d3b9e467e | ||
|
|
06826ef10f | ||
|
|
7f4a99aa95 | ||
|
|
122619624d | ||
|
|
2ea99a81f1 | ||
|
|
c30d18b10d | ||
|
|
18051ab399 | ||
|
|
654a864b3a | ||
|
|
5fba9b0cba | ||
|
|
906bff792c | ||
|
|
c029cc4354 | ||
|
|
0bafd1938c |
43 changed files with 2224 additions and 986 deletions
63
Cargo.lock
generated
63
Cargo.lock
generated
|
|
@ -109,7 +109,6 @@ dependencies = [
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"portable-pty",
|
"portable-pty",
|
||||||
"project",
|
"project",
|
||||||
"prompt_store",
|
|
||||||
"rand 0.9.4",
|
"rand 0.9.4",
|
||||||
"sandbox",
|
"sandbox",
|
||||||
"serde",
|
"serde",
|
||||||
|
|
@ -406,7 +405,7 @@ dependencies = [
|
||||||
"agent-client-protocol",
|
"agent-client-protocol",
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
"gpui",
|
"gpui",
|
||||||
|
|
@ -2163,7 +2162,7 @@ dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"prettyplease",
|
"prettyplease",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
|
|
@ -2183,7 +2182,7 @@ dependencies = [
|
||||||
"bitflags 2.10.0",
|
"bitflags 2.10.0",
|
||||||
"cexpr",
|
"cexpr",
|
||||||
"clang-sys",
|
"clang-sys",
|
||||||
"itertools 0.11.0",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"regex",
|
"regex",
|
||||||
|
|
@ -5313,7 +5312,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"option-ext",
|
"option-ext",
|
||||||
"redox_users 0.5.2",
|
"redox_users 0.5.2",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -5783,7 +5782,7 @@ dependencies = [
|
||||||
"client",
|
"client",
|
||||||
"clock",
|
"clock",
|
||||||
"collections",
|
"collections",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"criterion",
|
"criterion",
|
||||||
"ctor",
|
"ctor",
|
||||||
"dap",
|
"dap",
|
||||||
|
|
@ -6148,7 +6147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -7604,7 +7603,7 @@ dependencies = [
|
||||||
"gobject-sys",
|
"gobject-sys",
|
||||||
"libc",
|
"libc",
|
||||||
"system-deps",
|
"system-deps",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9065,7 +9064,7 @@ dependencies = [
|
||||||
"libc",
|
"libc",
|
||||||
"percent-encoding",
|
"percent-encoding",
|
||||||
"pin-project-lite",
|
"pin-project-lite",
|
||||||
"socket2 0.6.3",
|
"socket2 0.5.10",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tower-service",
|
"tower-service",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -9083,7 +9082,7 @@ dependencies = [
|
||||||
"js-sys",
|
"js-sys",
|
||||||
"log",
|
"log",
|
||||||
"wasm-bindgen",
|
"wasm-bindgen",
|
||||||
"windows-core 0.62.2",
|
"windows-core 0.56.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -9337,7 +9336,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
checksum = "4b0f83760fb341a774ed326568e19f5a863af4a952def8c39f9ab92fd95b88e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"equivalent",
|
"equivalent",
|
||||||
"hashbrown 0.16.1",
|
"hashbrown 0.15.5",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_core",
|
"serde_core",
|
||||||
]
|
]
|
||||||
|
|
@ -10147,7 +10146,7 @@ dependencies = [
|
||||||
"cloud_api_types",
|
"cloud_api_types",
|
||||||
"collections",
|
"collections",
|
||||||
"component",
|
"component",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"copilot",
|
"copilot",
|
||||||
"copilot_chat",
|
"copilot_chat",
|
||||||
"copilot_ui",
|
"copilot_ui",
|
||||||
|
|
@ -10480,7 +10479,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "libwebrtc"
|
name = "libwebrtc"
|
||||||
version = "0.3.26"
|
version = "0.3.26"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cxx",
|
"cxx",
|
||||||
"glib",
|
"glib",
|
||||||
|
|
@ -10590,7 +10589,7 @@ checksum = "11d3d7f243d5c5a8b9bb5d6dd2b1602c0cb0b9db1621bafc7ed66e35ff9fe092"
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit"
|
name = "livekit"
|
||||||
version = "0.7.32"
|
version = "0.7.32"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"bmrng",
|
"bmrng",
|
||||||
|
|
@ -10616,7 +10615,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-api"
|
name = "livekit-api"
|
||||||
version = "0.4.14"
|
version = "0.4.14"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"base64 0.21.7",
|
"base64 0.21.7",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
|
|
@ -10643,7 +10642,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-protocol"
|
name = "livekit-protocol"
|
||||||
version = "0.7.1"
|
version = "0.7.1"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"livekit-runtime",
|
"livekit-runtime",
|
||||||
|
|
@ -10659,7 +10658,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "livekit-runtime"
|
name = "livekit-runtime"
|
||||||
version = "0.4.0"
|
version = "0.4.0"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"tokio",
|
"tokio",
|
||||||
"tokio-stream",
|
"tokio-stream",
|
||||||
|
|
@ -11365,7 +11364,7 @@ version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"collections",
|
"collections",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"log",
|
"log",
|
||||||
"pretty_assertions",
|
"pretty_assertions",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
|
|
@ -11951,7 +11950,7 @@ version = "0.50.3"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
checksum = "7957b9740744892f114936ab4a57b3f487491bbeafaf8083688b16841a4240e5"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -14483,7 +14482,6 @@ dependencies = [
|
||||||
"db",
|
"db",
|
||||||
"fs",
|
"fs",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
"fuzzy",
|
|
||||||
"gpui",
|
"gpui",
|
||||||
"handlebars 4.5.0",
|
"handlebars 4.5.0",
|
||||||
"heed",
|
"heed",
|
||||||
|
|
@ -14491,7 +14489,6 @@ dependencies = [
|
||||||
"log",
|
"log",
|
||||||
"parking_lot",
|
"parking_lot",
|
||||||
"paths",
|
"paths",
|
||||||
"rope",
|
|
||||||
"serde",
|
"serde",
|
||||||
"serde_json",
|
"serde_json",
|
||||||
"strum 0.27.2",
|
"strum 0.27.2",
|
||||||
|
|
@ -14590,7 +14587,7 @@ checksum = "22505a5c94da8e3b7c2996394d1c933236c4d743e81a410bcca4e6989fc066a4"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"bytes 1.11.1",
|
"bytes 1.11.1",
|
||||||
"heck 0.5.0",
|
"heck 0.5.0",
|
||||||
"itertools 0.11.0",
|
"itertools 0.10.5",
|
||||||
"log",
|
"log",
|
||||||
"multimap",
|
"multimap",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
|
|
@ -14623,7 +14620,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
checksum = "81bddcdb20abf9501610992b6759a4c888aef7d1a7247ef75e2404275ac24af1"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"itertools 0.11.0",
|
"itertools 0.10.5",
|
||||||
"proc-macro2",
|
"proc-macro2",
|
||||||
"quote",
|
"quote",
|
||||||
"syn 2.0.117",
|
"syn 2.0.117",
|
||||||
|
|
@ -14885,7 +14882,7 @@ dependencies = [
|
||||||
"quinn-udp",
|
"quinn-udp",
|
||||||
"rustc-hash 2.1.1",
|
"rustc-hash 2.1.1",
|
||||||
"rustls 0.23.40",
|
"rustls 0.23.40",
|
||||||
"socket2 0.6.3",
|
"socket2 0.5.10",
|
||||||
"thiserror 2.0.17",
|
"thiserror 2.0.17",
|
||||||
"tokio",
|
"tokio",
|
||||||
"tracing",
|
"tracing",
|
||||||
|
|
@ -14922,9 +14919,9 @@ dependencies = [
|
||||||
"cfg_aliases 0.2.1",
|
"cfg_aliases 0.2.1",
|
||||||
"libc",
|
"libc",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"socket2 0.6.3",
|
"socket2 0.5.10",
|
||||||
"tracing",
|
"tracing",
|
||||||
"windows-sys 0.60.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -16149,7 +16146,7 @@ dependencies = [
|
||||||
"errno 0.3.14",
|
"errno 0.3.14",
|
||||||
"libc",
|
"libc",
|
||||||
"linux-raw-sys 0.11.0",
|
"linux-raw-sys 0.11.0",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -18771,7 +18768,7 @@ dependencies = [
|
||||||
"getrandom 0.3.4",
|
"getrandom 0.3.4",
|
||||||
"once_cell",
|
"once_cell",
|
||||||
"rustix 1.1.2",
|
"rustix 1.1.2",
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.59.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -19488,7 +19485,7 @@ name = "toolchain_selector"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"convert_case 0.8.0",
|
"convert_case 0.11.0",
|
||||||
"editor",
|
"editor",
|
||||||
"futures 0.3.32",
|
"futures 0.3.32",
|
||||||
"fuzzy",
|
"fuzzy",
|
||||||
|
|
@ -19696,7 +19693,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f"
|
checksum = "2fb391ac70462b3097a755618fbf9c8f95ecc1eb379a414f7b46f202ed10db1f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"windows-targets 0.52.6",
|
"windows-targets 0.48.5",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -21489,7 +21486,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-sys"
|
name = "webrtc-sys"
|
||||||
version = "0.3.23"
|
version = "0.3.23"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"cc",
|
"cc",
|
||||||
"cxx",
|
"cxx",
|
||||||
|
|
@ -21503,7 +21500,7 @@ dependencies = [
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "webrtc-sys-build"
|
name = "webrtc-sys-build"
|
||||||
version = "0.3.13"
|
version = "0.3.13"
|
||||||
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1#147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1"
|
source = "git+https://github.com/zed-industries/livekit-rust-sdks?rev=c3a55bbc207008f1ca3474b6037fdd3c443cad0f#c3a55bbc207008f1ca3474b6037fdd3c443cad0f"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"anyhow",
|
"anyhow",
|
||||||
"fs2",
|
"fs2",
|
||||||
|
|
@ -21801,7 +21798,7 @@ version = "0.1.11"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"windows-sys 0.61.2",
|
"windows-sys 0.48.0",
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
|
||||||
|
|
@ -559,7 +559,7 @@ clap = { version = "4.4", features = ["derive", "wrap_help"] }
|
||||||
cocoa = "=0.26.0"
|
cocoa = "=0.26.0"
|
||||||
cocoa-foundation = "=0.2.0"
|
cocoa-foundation = "=0.2.0"
|
||||||
const_format = "0.2"
|
const_format = "0.2"
|
||||||
convert_case = "0.8.0"
|
convert_case = "0.11.0"
|
||||||
core-foundation = "=0.10.0"
|
core-foundation = "=0.10.0"
|
||||||
core-foundation-sys = "0.8.6"
|
core-foundation-sys = "0.8.6"
|
||||||
core-video = { version = "0.5.2", features = ["metal"] }
|
core-video = { version = "0.5.2", features = ["metal"] }
|
||||||
|
|
@ -891,9 +891,9 @@ notify = { git = "https://github.com/zed-industries/notify.git", rev = "ce58c24c
|
||||||
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "ce58c24cad542c28e04ced02e20325a4ec28a31d" }
|
notify-types = { git = "https://github.com/zed-industries/notify.git", rev = "ce58c24cad542c28e04ced02e20325a4ec28a31d" }
|
||||||
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
windows-capture = { git = "https://github.com/zed-industries/windows-capture.git", rev = "f0d6c1b6691db75461b732f6d5ff56eed002eeb9" }
|
||||||
calloop = { git = "https://github.com/zed-industries/calloop" }
|
calloop = { git = "https://github.com/zed-industries/calloop" }
|
||||||
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1" }
|
livekit = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "c3a55bbc207008f1ca3474b6037fdd3c443cad0f" }
|
||||||
libwebrtc = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1" }
|
libwebrtc = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "c3a55bbc207008f1ca3474b6037fdd3c443cad0f" }
|
||||||
webrtc-sys = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "147fbca3d4b592d96d33f5e6a84b59fc0b5d9bf1" }
|
webrtc-sys = { git = "https://github.com/zed-industries/livekit-rust-sdks", rev = "c3a55bbc207008f1ca3474b6037fdd3c443cad0f" }
|
||||||
|
|
||||||
[profile.dev]
|
[profile.dev]
|
||||||
split-debuginfo = "unpacked"
|
split-debuginfo = "unpacked"
|
||||||
|
|
|
||||||
|
|
@ -38,7 +38,6 @@ parking_lot = { workspace = true, optional = true }
|
||||||
image.workspace = true
|
image.workspace = true
|
||||||
portable-pty.workspace = true
|
portable-pty.workspace = true
|
||||||
project.workspace = true
|
project.workspace = true
|
||||||
prompt_store.workspace = true
|
|
||||||
sandbox.workspace = true
|
sandbox.workspace = true
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -1,7 +1,6 @@
|
||||||
use agent_client_protocol::schema as acp;
|
use agent_client_protocol::schema as acp;
|
||||||
use anyhow::{Context as _, Result, bail};
|
use anyhow::{Context as _, Result, bail};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use prompt_store::{PromptId, UserPromptId};
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{
|
||||||
borrow::Cow,
|
borrow::Cow,
|
||||||
|
|
@ -37,10 +36,6 @@ pub enum MentionUri {
|
||||||
id: acp::SessionId,
|
id: acp::SessionId,
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
Rule {
|
|
||||||
id: PromptId,
|
|
||||||
name: String,
|
|
||||||
},
|
|
||||||
Diagnostics {
|
Diagnostics {
|
||||||
#[serde(default = "default_include_errors")]
|
#[serde(default = "default_include_errors")]
|
||||||
include_errors: bool,
|
include_errors: bool,
|
||||||
|
|
@ -205,13 +200,6 @@ impl MentionUri {
|
||||||
id: acp::SessionId::new(thread_id),
|
id: acp::SessionId::new(thread_id),
|
||||||
name,
|
name,
|
||||||
})
|
})
|
||||||
} else if let Some(rule_id) = path.strip_prefix("/agent/rule/") {
|
|
||||||
let name = single_query_param(&url, "name")?.context("Missing rule name")?;
|
|
||||||
let rule_id = UserPromptId(rule_id.parse()?);
|
|
||||||
Ok(Self::Rule {
|
|
||||||
id: rule_id.into(),
|
|
||||||
name,
|
|
||||||
})
|
|
||||||
} else if path == "/agent/diagnostics" {
|
} else if path == "/agent/diagnostics" {
|
||||||
let mut include_errors = default_include_errors();
|
let mut include_errors = default_include_errors();
|
||||||
let mut include_warnings = false;
|
let mut include_warnings = false;
|
||||||
|
|
@ -342,7 +330,6 @@ impl MentionUri {
|
||||||
MentionUri::PastedImage { name } => name.clone(),
|
MentionUri::PastedImage { name } => name.clone(),
|
||||||
MentionUri::Symbol { name, .. } => name.clone(),
|
MentionUri::Symbol { name, .. } => name.clone(),
|
||||||
MentionUri::Thread { name, .. } => name.clone(),
|
MentionUri::Thread { name, .. } => name.clone(),
|
||||||
MentionUri::Rule { name, .. } => name.clone(),
|
|
||||||
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
|
MentionUri::Diagnostics { .. } => "Diagnostics".to_string(),
|
||||||
MentionUri::TerminalSelection { line_count } => {
|
MentionUri::TerminalSelection { line_count } => {
|
||||||
if *line_count == 1 {
|
if *line_count == 1 {
|
||||||
|
|
@ -443,7 +430,6 @@ impl MentionUri {
|
||||||
.unwrap_or_else(|| IconName::Folder.path().into()),
|
.unwrap_or_else(|| IconName::Folder.path().into()),
|
||||||
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
MentionUri::Symbol { .. } => IconName::Code.path().into(),
|
||||||
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
MentionUri::Thread { .. } => IconName::Thread.path().into(),
|
||||||
MentionUri::Rule { .. } => IconName::Reader.path().into(),
|
|
||||||
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
|
MentionUri::Diagnostics { .. } => IconName::Warning.path().into(),
|
||||||
MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
|
MentionUri::TerminalSelection { .. } => IconName::Terminal.path().into(),
|
||||||
MentionUri::Selection { .. } => IconName::Reader.path().into(),
|
MentionUri::Selection { .. } => IconName::Reader.path().into(),
|
||||||
|
|
@ -526,12 +512,6 @@ impl MentionUri {
|
||||||
url.query_pairs_mut().append_pair("name", name);
|
url.query_pairs_mut().append_pair("name", name);
|
||||||
url
|
url
|
||||||
}
|
}
|
||||||
MentionUri::Rule { name, id } => {
|
|
||||||
let mut url = Url::parse("zed:///").unwrap();
|
|
||||||
url.set_path(&format!("/agent/rule/{id}"));
|
|
||||||
url.query_pairs_mut().append_pair("name", name);
|
|
||||||
url
|
|
||||||
}
|
|
||||||
MentionUri::Diagnostics {
|
MentionUri::Diagnostics {
|
||||||
include_errors,
|
include_errors,
|
||||||
include_warnings,
|
include_warnings,
|
||||||
|
|
@ -811,20 +791,6 @@ mod tests {
|
||||||
assert_eq!(parsed.to_uri().to_string(), thread_uri);
|
assert_eq!(parsed.to_uri().to_string(), thread_uri);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_parse_rule_uri() {
|
|
||||||
let rule_uri = "zed:///agent/rule/d8694ff2-90d5-4b6f-be33-33c1763acd52?name=Some+rule";
|
|
||||||
let parsed = MentionUri::parse(rule_uri, PathStyle::local()).unwrap();
|
|
||||||
match &parsed {
|
|
||||||
MentionUri::Rule { id, name } => {
|
|
||||||
assert_eq!(id.to_string(), "d8694ff2-90d5-4b6f-be33-33c1763acd52");
|
|
||||||
assert_eq!(name, "Some rule");
|
|
||||||
}
|
|
||||||
_ => panic!("Expected Rule variant"),
|
|
||||||
}
|
|
||||||
assert_eq!(parsed.to_uri().to_string(), rule_uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn test_parse_skill_uri_round_trip() {
|
fn test_parse_skill_uri_round_trip() {
|
||||||
let skill_uri = MentionUri::Skill {
|
let skill_uri = MentionUri::Skill {
|
||||||
|
|
|
||||||
|
|
@ -316,17 +316,6 @@ impl UserMessage {
|
||||||
MentionUri::Thread { .. } => {
|
MentionUri::Thread { .. } => {
|
||||||
write!(&mut thread_context, "\n{}\n", content).ok();
|
write!(&mut thread_context, "\n{}\n", content).ok();
|
||||||
}
|
}
|
||||||
MentionUri::Rule { .. } => {
|
|
||||||
write!(
|
|
||||||
&mut rules_context,
|
|
||||||
"\n{}",
|
|
||||||
MarkdownCodeBlock {
|
|
||||||
tag: "",
|
|
||||||
text: content
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.ok();
|
|
||||||
}
|
|
||||||
MentionUri::Fetch { url } => {
|
MentionUri::Fetch { url } => {
|
||||||
write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
|
write!(&mut fetch_context, "\nFetch: {}\n\n{}", url, content).ok();
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -78,7 +78,6 @@ use gpui::{
|
||||||
use language::LanguageRegistry;
|
use language::LanguageRegistry;
|
||||||
use language_model::LanguageModelRegistry;
|
use language_model::LanguageModelRegistry;
|
||||||
use project::{Project, ProjectPath, Worktree};
|
use project::{Project, ProjectPath, Worktree};
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use settings::TerminalDockPosition;
|
use settings::TerminalDockPosition;
|
||||||
use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file};
|
use settings::{NotifyWhenAgentWaiting, Settings, update_settings_file};
|
||||||
use skill_creator::{SkillCreatorOpenMode, is_supported_skill_url, open_skill_creator};
|
use skill_creator::{SkillCreatorOpenMode, is_supported_skill_url, open_skill_creator};
|
||||||
|
|
@ -1049,7 +1048,6 @@ pub struct AgentPanel {
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
language_registry: Arc<LanguageRegistry>,
|
language_registry: Arc<LanguageRegistry>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
connection_store: Entity<AgentConnectionStore>,
|
connection_store: Entity<AgentConnectionStore>,
|
||||||
context_server_registry: Entity<ContextServerRegistry>,
|
context_server_registry: Entity<ContextServerRegistry>,
|
||||||
configuration: Option<Entity<AgentConfiguration>>,
|
configuration: Option<Entity<AgentConfiguration>>,
|
||||||
|
|
@ -1170,13 +1168,8 @@ impl AgentPanel {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
mut cx: AsyncWindowContext,
|
mut cx: AsyncWindowContext,
|
||||||
) -> Task<Result<Entity<Self>>> {
|
) -> Task<Result<Entity<Self>>> {
|
||||||
let prompt_store = cx.update(|_window, cx| PromptStore::global(cx));
|
|
||||||
let kvp = cx.update(|_window, cx| KeyValueStore::global(cx)).ok();
|
let kvp = cx.update(|_window, cx| KeyValueStore::global(cx)).ok();
|
||||||
cx.spawn(async move |cx| {
|
cx.spawn(async move |cx| {
|
||||||
let prompt_store = match prompt_store {
|
|
||||||
Ok(prompt_store) => prompt_store.await.ok(),
|
|
||||||
Err(_) => None,
|
|
||||||
};
|
|
||||||
let workspace_id = workspace
|
let workspace_id = workspace
|
||||||
.read_with(cx, |workspace, _| workspace.database_id())
|
.read_with(cx, |workspace, _| workspace.database_id())
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -1301,7 +1294,7 @@ impl AgentPanel {
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| Self::new(workspace, prompt_store, window, cx));
|
let panel = cx.new(|cx| Self::new(workspace, window, cx));
|
||||||
|
|
||||||
panel.update(cx, |panel, cx| {
|
panel.update(cx, |panel, cx| {
|
||||||
let is_via_collab = panel.project.read(cx).is_via_collab();
|
let is_via_collab = panel.project.read(cx).is_via_collab();
|
||||||
|
|
@ -1381,12 +1374,7 @@ impl AgentPanel {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn new(
|
pub(crate) fn new(workspace: &Workspace, _window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
workspace: &Workspace,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
_window: &mut Window,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Self {
|
|
||||||
let fs = workspace.app_state().fs.clone();
|
let fs = workspace.app_state().fs.clone();
|
||||||
let user_store = workspace.app_state().user_store.clone();
|
let user_store = workspace.app_state().user_store.clone();
|
||||||
let project = workspace.project();
|
let project = workspace.project();
|
||||||
|
|
@ -1468,7 +1456,6 @@ impl AgentPanel {
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
fs: fs.clone(),
|
fs: fs.clone(),
|
||||||
language_registry,
|
language_registry,
|
||||||
prompt_store,
|
|
||||||
connection_store,
|
connection_store,
|
||||||
configuration: None,
|
configuration: None,
|
||||||
configuration_subscription: None,
|
configuration_subscription: None,
|
||||||
|
|
@ -1547,10 +1534,6 @@ impl AgentPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub(crate) fn prompt_store(&self) -> &Option<Entity<PromptStore>> {
|
|
||||||
&self.prompt_store
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn thread_store(&self) -> &Entity<ThreadStore> {
|
pub fn thread_store(&self) -> &Entity<ThreadStore> {
|
||||||
&self.thread_store
|
&self.thread_store
|
||||||
}
|
}
|
||||||
|
|
@ -4395,7 +4378,6 @@ impl AgentPanel {
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
project,
|
project,
|
||||||
thread_store,
|
thread_store,
|
||||||
self.prompt_store.clone(),
|
|
||||||
source,
|
source,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -6087,7 +6069,7 @@ impl Dismissable for TrialEndUpsell {
|
||||||
#[cfg(any(test, feature = "test-support"))]
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
impl AgentPanel {
|
impl AgentPanel {
|
||||||
pub fn test_new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
pub fn test_new(workspace: &Workspace, window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||||
Self::new(workspace, None, window, cx)
|
Self::new(workspace, window, cx)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Drops a thread's `ConversationView` from `retained_threads` without
|
/// Drops a thread's `ConversationView` from `retained_threads` without
|
||||||
|
|
@ -6594,7 +6576,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up workspace A: with an active thread.
|
// Set up workspace A: with an active thread.
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
panel_a.update_in(cx, |panel, window, cx| {
|
panel_a.update_in(cx, |panel, window, cx| {
|
||||||
|
|
@ -6620,7 +6602,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up workspace B: ClaudeCode, no active thread.
|
// Set up workspace B: ClaudeCode, no active thread.
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
panel_b.update(cx, |panel, _cx| {
|
panel_b.update(cx, |panel, _cx| {
|
||||||
|
|
@ -6723,7 +6705,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.update_in(cx, |panel, window, cx| {
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
|
@ -6918,7 +6900,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
panel_a
|
panel_a
|
||||||
.update_in(cx, |panel, window, cx| {
|
.update_in(cx, |panel, window, cx| {
|
||||||
|
|
@ -6995,7 +6977,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
panel.update_in(cx, |panel, window, cx| {
|
panel.update_in(cx, |panel, window, cx| {
|
||||||
|
|
@ -7087,7 +7069,7 @@ mod tests {
|
||||||
});
|
});
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open a restored thread using a flaky server so the initial connect
|
// Open a restored thread using a flaky server so the initial connect
|
||||||
|
|
@ -7186,7 +7168,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -7286,12 +7268,12 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -7666,7 +7648,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -7853,7 +7835,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -8082,7 +8064,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -8168,7 +8150,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -8258,7 +8240,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
(panel, cx)
|
(panel, cx)
|
||||||
|
|
@ -8305,7 +8287,7 @@ mod tests {
|
||||||
register_test_sidebar(threads_list_active, &mut cx);
|
register_test_sidebar(threads_list_active, &mut cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||||
panel
|
panel
|
||||||
|
|
@ -8435,7 +8417,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -8548,7 +8530,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -9791,7 +9773,7 @@ mod tests {
|
||||||
|
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10383,7 +10365,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
// Open thread A and send a message. With empty next_prompt_updates it
|
// Open thread A and send a message. With empty next_prompt_updates it
|
||||||
|
|
@ -10652,7 +10634,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up workspace A with agent_a
|
// Set up workspace A with agent_a
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
panel_a.update(cx, |panel, _cx| {
|
panel_a.update(cx, |panel, _cx| {
|
||||||
panel.selected_agent = agent_a.clone();
|
panel.selected_agent = agent_a.clone();
|
||||||
|
|
@ -10660,7 +10642,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up workspace B with agent_b
|
// Set up workspace B with agent_b
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
panel_b.update(cx, |panel, _cx| {
|
panel_b.update(cx, |panel, _cx| {
|
||||||
panel.selected_agent = agent_b.clone();
|
panel.selected_agent = agent_b.clone();
|
||||||
|
|
@ -10731,7 +10713,7 @@ mod tests {
|
||||||
};
|
};
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10788,7 +10770,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10878,7 +10860,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -10966,7 +10948,7 @@ mod tests {
|
||||||
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -11076,7 +11058,7 @@ mod tests {
|
||||||
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
workspace.update(cx, |workspace, _cx| workspace.set_random_database_id());
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -11182,7 +11164,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -11681,7 +11663,7 @@ mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
cx.new(|cx| AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
|
|
||||||
cx.run_until_parked();
|
cx.run_until_parked();
|
||||||
|
|
@ -11782,7 +11764,7 @@ mod tests {
|
||||||
|
|
||||||
// Create the agent panel and add it to the workspace.
|
// Create the agent panel and add it to the workspace.
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -11992,7 +11974,7 @@ mod tests {
|
||||||
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut cx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
let panel = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12229,7 +12211,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up panel_a with an active thread and type draft text.
|
// Set up panel_a with an active thread and type draft text.
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12253,7 +12235,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up panel_b on workspace_b — starts as a fresh, empty panel.
|
// Set up panel_b on workspace_b — starts as a fresh, empty panel.
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12323,7 +12305,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up panel_a with draft text.
|
// Set up panel_a with draft text.
|
||||||
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
let panel_a = workspace_a.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12347,7 +12329,7 @@ mod tests {
|
||||||
|
|
||||||
// Set up panel_b with its OWN content — this is a non-fresh panel.
|
// Set up panel_b with its OWN content — this is a non-fresh panel.
|
||||||
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
let panel_b = workspace_b.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
panel
|
panel
|
||||||
});
|
});
|
||||||
|
|
@ -12391,42 +12373,4 @@ mod tests {
|
||||||
);
|
);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Regression test: NewThread must produce a connected thread even when
|
|
||||||
/// the PromptStore fails to initialize (e.g. LMDB permission error).
|
|
||||||
/// Before the fix, `NativeAgentServer::connect` propagated the
|
|
||||||
/// PromptStore error with `?`, which put every new ConversationView
|
|
||||||
/// into LoadError and made it impossible to start any native-agent
|
|
||||||
/// thread.
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_new_thread_with_prompt_store_error(cx: &mut TestAppContext) {
|
|
||||||
let (panel, mut cx) = setup_panel(cx).await;
|
|
||||||
|
|
||||||
// NativeAgentServer::connect needs a global Fs.
|
|
||||||
let fs = FakeFs::new(cx.executor());
|
|
||||||
cx.update(|_, cx| {
|
|
||||||
<dyn fs::Fs>::set_global(fs.clone(), cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
// Dispatch NewThread, which goes through the real NativeAgentServer
|
|
||||||
// path. In tests the PromptStore LMDB open fails with
|
|
||||||
// "Permission denied"; the fix (.log_err() instead of ?) lets
|
|
||||||
// the connection succeed anyway.
|
|
||||||
panel.update_in(&mut cx, |panel, window, cx| {
|
|
||||||
panel.new_thread(&NewThread, window, cx);
|
|
||||||
});
|
|
||||||
cx.run_until_parked();
|
|
||||||
|
|
||||||
panel.read_with(&cx, |panel, cx| {
|
|
||||||
assert!(
|
|
||||||
panel.active_conversation_view().is_some(),
|
|
||||||
"panel should have a conversation view after NewThread"
|
|
||||||
);
|
|
||||||
assert!(
|
|
||||||
panel.active_agent_thread(cx).is_some(),
|
|
||||||
"panel should have an active, connected agent thread"
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,6 @@ use markdown::{
|
||||||
};
|
};
|
||||||
use parking_lot::{Mutex, RwLock};
|
use parking_lot::{Mutex, RwLock};
|
||||||
use project::{AgentId, AgentServerStore, Project, ProjectEntryId, ProjectPath};
|
use project::{AgentId, AgentServerStore, Project, ProjectEntryId, ProjectPath};
|
||||||
use prompt_store::{PromptId, PromptStore};
|
|
||||||
|
|
||||||
use crate::message_editor::SessionCapabilities;
|
use crate::message_editor::SessionCapabilities;
|
||||||
use crate::{AgentThreadSource, DEFAULT_THREAD_TITLE, resolve_agent_image};
|
use crate::{AgentThreadSource, DEFAULT_THREAD_TITLE, resolve_agent_image};
|
||||||
|
|
@ -75,7 +74,6 @@ use workspace::{
|
||||||
path_link::sanitize_path_text,
|
path_link::sanitize_path_text,
|
||||||
};
|
};
|
||||||
use zed_actions::agent::{Chat, ToggleModelSelector};
|
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||||
use zed_actions::assistant::OpenRulesLibrary;
|
|
||||||
|
|
||||||
use super::config_options::ConfigOptionsView;
|
use super::config_options::ConfigOptionsView;
|
||||||
use super::entry_view_state::EntryViewState;
|
use super::entry_view_state::EntryViewState;
|
||||||
|
|
@ -531,7 +529,6 @@ pub struct ConversationView {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
pub(crate) thread_id: ThreadId,
|
pub(crate) thread_id: ThreadId,
|
||||||
pub(crate) root_session_id: Option<acp::SessionId>,
|
pub(crate) root_session_id: Option<acp::SessionId>,
|
||||||
server_state: ServerState,
|
server_state: ServerState,
|
||||||
|
|
@ -738,7 +735,6 @@ impl ConversationView {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: Entity<Project>,
|
project: Entity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
source: AgentThreadSource,
|
source: AgentThreadSource,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
|
|
@ -795,7 +791,6 @@ impl ConversationView {
|
||||||
workspace,
|
workspace,
|
||||||
project: project.clone(),
|
project: project.clone(),
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
thread_id,
|
thread_id,
|
||||||
root_session_id: resume_session_id.clone(),
|
root_session_id: resume_session_id.clone(),
|
||||||
server_state: Self::initial_state(
|
server_state: Self::initial_state(
|
||||||
|
|
@ -1104,7 +1099,6 @@ impl ConversationView {
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.project.downgrade(),
|
self.project.downgrade(),
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.prompt_store.clone(),
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
self.agent.agent_id(),
|
self.agent.agent_id(),
|
||||||
)
|
)
|
||||||
|
|
@ -1273,7 +1267,6 @@ impl ConversationView {
|
||||||
self.project.downgrade(),
|
self.project.downgrade(),
|
||||||
self.code_span_resolver.clone(),
|
self.code_span_resolver.clone(),
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.prompt_store.clone(),
|
|
||||||
initial_content,
|
initial_content,
|
||||||
subscriptions,
|
subscriptions,
|
||||||
window,
|
window,
|
||||||
|
|
@ -2492,7 +2485,6 @@ impl ConversationView {
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
agent_name.clone(),
|
agent_name.clone(),
|
||||||
"",
|
"",
|
||||||
|
|
@ -3721,7 +3713,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -3858,7 +3849,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -3940,7 +3930,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4079,7 +4068,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4364,7 +4352,7 @@ pub(crate) mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
||||||
|
|
||||||
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
let panel = workspace.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
workspace.focus_panel::<crate::AgentPanel>(window, cx);
|
workspace.focus_panel::<crate::AgentPanel>(window, cx);
|
||||||
panel
|
panel
|
||||||
|
|
@ -4405,7 +4393,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4504,7 +4491,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4580,7 +4566,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4648,7 +4633,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4724,7 +4708,7 @@ pub(crate) mod tests {
|
||||||
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
let cx = &mut VisualTestContext::from_window(multi_workspace_handle.into(), cx);
|
||||||
|
|
||||||
let panel = workspace1.update_in(cx, |workspace, window, cx| {
|
let panel = workspace1.update_in(cx, |workspace, window, cx| {
|
||||||
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx));
|
let panel = cx.new(|cx| crate::AgentPanel::new(workspace, window, cx));
|
||||||
workspace.add_panel(panel.clone(), window, cx);
|
workspace.add_panel(panel.clone(), window, cx);
|
||||||
|
|
||||||
// Open the dock and activate the agent panel so it's visible
|
// Open the dock and activate the agent panel so it's visible
|
||||||
|
|
@ -4770,7 +4754,6 @@ pub(crate) mod tests {
|
||||||
workspace1.downgrade(),
|
workspace1.downgrade(),
|
||||||
project1.clone(),
|
project1.clone(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -4992,7 +4975,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -5651,7 +5633,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
Some(thread_store.clone()),
|
Some(thread_store.clone()),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -8113,9 +8094,17 @@ pub(crate) mod tests {
|
||||||
async fn test_permission_row_hidden_when_inline_bounds_unavailable(cx: &mut TestAppContext) {
|
async fn test_permission_row_hidden_when_inline_bounds_unavailable(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
||||||
let (_view, thread_view, _entry_ix, cx) =
|
let (_view, thread_view, entry_ix, cx) =
|
||||||
setup_pending_permission_thread("perm-no-bounds", cx).await;
|
setup_pending_permission_thread("perm-no-bounds", cx).await;
|
||||||
|
|
||||||
|
// Pin the scroll top to the entry so it isn't treated as above the
|
||||||
|
// viewport, forcing the unmeasured-bounds path we want to exercise.
|
||||||
|
thread_view.read_with(cx, |view, _cx| {
|
||||||
|
view.list_state.scroll_to(ListOffset {
|
||||||
|
item_ix: entry_ix,
|
||||||
|
offset_in_item: px(0.0),
|
||||||
|
});
|
||||||
|
});
|
||||||
thread_view.update_in(cx, |view, window, cx| {
|
thread_view.update_in(cx, |view, window, cx| {
|
||||||
assert!(
|
assert!(
|
||||||
view.render_main_agent_awaiting_permission(window, cx)
|
view.render_main_agent_awaiting_permission(window, cx)
|
||||||
|
|
@ -8176,8 +8165,8 @@ pub(crate) mod tests {
|
||||||
let (_view, thread_view, entry_ix, cx) =
|
let (_view, thread_view, entry_ix, cx) =
|
||||||
setup_pending_permission_thread("perm-scroll", cx).await;
|
setup_pending_permission_thread("perm-scroll", cx).await;
|
||||||
|
|
||||||
// Start off-screen below the viewport — row visible because the item
|
// Start off-screen below the viewport. The row is visible because the
|
||||||
// has bounds that do not intersect the viewport.
|
// item has bounds that do not intersect the viewport.
|
||||||
draw_thread_list_at(
|
draw_thread_list_at(
|
||||||
&thread_view,
|
&thread_view,
|
||||||
ListOffset {
|
ListOffset {
|
||||||
|
|
@ -8221,6 +8210,69 @@ pub(crate) mod tests {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_permission_row_shown_when_inline_prompt_is_above_viewport(
|
||||||
|
cx: &mut TestAppContext,
|
||||||
|
) {
|
||||||
|
init_test(cx);
|
||||||
|
|
||||||
|
let (_view, thread_view, entry_ix, cx) =
|
||||||
|
setup_pending_permission_thread("perm-above", cx).await;
|
||||||
|
|
||||||
|
let thread = thread_view.read_with(cx, |view, _cx| view.thread.clone());
|
||||||
|
thread.update(cx, |thread, cx| {
|
||||||
|
let result = thread.handle_session_update(
|
||||||
|
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
|
||||||
|
"More content".into(),
|
||||||
|
)),
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
result.is_ok(),
|
||||||
|
"following assistant message should be accepted"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
draw_thread_list_at(
|
||||||
|
&thread_view,
|
||||||
|
ListOffset {
|
||||||
|
item_ix: entry_ix + 1,
|
||||||
|
offset_in_item: px(0.0),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
thread_view.read_with(cx, |view, _cx| {
|
||||||
|
assert!(
|
||||||
|
entry_ix < view.list_state.logical_scroll_top().item_ix,
|
||||||
|
"The tool call entry should be above the logical scroll top"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
thread_view.update_in(cx, |view, window, cx| {
|
||||||
|
assert!(
|
||||||
|
view.render_main_agent_awaiting_permission(window, cx)
|
||||||
|
.is_some(),
|
||||||
|
"Floating row should be visible when the inline prompt is above the viewport"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Scrolling up to the entry brings it back into view.
|
||||||
|
draw_thread_list_at(
|
||||||
|
&thread_view,
|
||||||
|
ListOffset {
|
||||||
|
item_ix: entry_ix,
|
||||||
|
offset_in_item: px(0.0),
|
||||||
|
},
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
thread_view.update_in(cx, |view, window, cx| {
|
||||||
|
assert!(
|
||||||
|
view.render_main_agent_awaiting_permission(window, cx)
|
||||||
|
.is_none(),
|
||||||
|
"Floating row should disappear after scrolling brings the inline prompt into view"
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_permission_row_disappears_when_authorized(cx: &mut TestAppContext) {
|
async fn test_permission_row_disappears_when_authorized(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
@ -8556,7 +8608,6 @@ pub(crate) mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project,
|
project,
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
AgentThreadSource::AgentPanel,
|
AgentThreadSource::AgentPanel,
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
||||||
|
|
@ -683,7 +683,6 @@ impl ThreadView {
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
code_span_resolver: AgentCodeSpanResolver,
|
code_span_resolver: AgentCodeSpanResolver,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_content: Option<AgentInitialContent>,
|
initial_content: Option<AgentInitialContent>,
|
||||||
mut subscriptions: Vec<Subscription>,
|
mut subscriptions: Vec<Subscription>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
@ -703,7 +702,6 @@ impl ThreadView {
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
project.clone(),
|
project.clone(),
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
agent_id.clone(),
|
agent_id.clone(),
|
||||||
&placeholder,
|
&placeholder,
|
||||||
|
|
@ -3047,15 +3045,6 @@ impl ThreadView {
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true when the entry has been measured and sits entirely below
|
|
||||||
/// the current viewport.
|
|
||||||
fn entry_is_below_viewport(&self, entry_ix: usize) -> bool {
|
|
||||||
let viewport_bounds = self.list_state.viewport_bounds();
|
|
||||||
self.list_state
|
|
||||||
.bounds_for_item(entry_ix)
|
|
||||||
.is_some_and(|entry_bounds| entry_bounds.top() >= viewport_bounds.bottom())
|
|
||||||
}
|
|
||||||
|
|
||||||
pub(crate) fn render_main_agent_awaiting_permission(
|
pub(crate) fn render_main_agent_awaiting_permission(
|
||||||
&self,
|
&self,
|
||||||
window: &Window,
|
window: &Window,
|
||||||
|
|
@ -3073,9 +3062,13 @@ impl ThreadView {
|
||||||
let thread = self.thread.read(cx);
|
let thread = self.thread.read(cx);
|
||||||
let (entry_ix, tool_call) = thread.tool_call(&tool_call_id)?;
|
let (entry_ix, tool_call) = thread.tool_call(&tool_call_id)?;
|
||||||
|
|
||||||
if !self.entry_is_below_viewport(entry_ix) {
|
let scroll_icon = if self.list_state.item_is_above_viewport(entry_ix)? {
|
||||||
|
IconName::ArrowUp
|
||||||
|
} else if self.list_state.item_is_below_viewport(entry_ix)? {
|
||||||
|
IconName::ArrowDown
|
||||||
|
} else {
|
||||||
return None;
|
return None;
|
||||||
}
|
};
|
||||||
|
|
||||||
let focus_handle = self.focus_handle(cx);
|
let focus_handle = self.focus_handle(cx);
|
||||||
|
|
||||||
|
|
@ -3118,7 +3111,7 @@ impl ThreadView {
|
||||||
Button::new("main-agent-permission-scroll-to", "Scroll")
|
Button::new("main-agent-permission-scroll-to", "Scroll")
|
||||||
.label_size(LabelSize::Small)
|
.label_size(LabelSize::Small)
|
||||||
.end_icon(
|
.end_icon(
|
||||||
Icon::new(IconName::ArrowDown)
|
Icon::new(scroll_icon)
|
||||||
.size(IconSize::XSmall)
|
.size(IconSize::XSmall)
|
||||||
.color(Color::Default),
|
.color(Color::Default),
|
||||||
)
|
)
|
||||||
|
|
@ -10014,17 +10007,6 @@ pub(crate) fn open_link(
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
MentionUri::Rule { id, .. } => {
|
|
||||||
let PromptId::User { uuid } = id else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
window.dispatch_action(
|
|
||||||
Box::new(OpenRulesLibrary {
|
|
||||||
prompt_to_select: Some(uuid.0),
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
MentionUri::Fetch { url } => {
|
MentionUri::Fetch { url } => {
|
||||||
cx.open_url(url.as_str());
|
cx.open_url(url.as_str());
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,7 +11,6 @@ use gpui::{
|
||||||
};
|
};
|
||||||
use language::language_settings::SoftWrap;
|
use language::language_settings::SoftWrap;
|
||||||
use project::{AgentId, Project, project_settings::DiagnosticSeverity};
|
use project::{AgentId, Project, project_settings::DiagnosticSeverity};
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::Settings as _;
|
use settings::Settings as _;
|
||||||
use terminal_view::TerminalView;
|
use terminal_view::TerminalView;
|
||||||
|
|
@ -25,7 +24,6 @@ pub struct EntryViewState {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
entries: Vec<Entry>,
|
entries: Vec<Entry>,
|
||||||
session_capabilities: SharedSessionCapabilities,
|
session_capabilities: SharedSessionCapabilities,
|
||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
|
|
@ -36,7 +34,6 @@ impl EntryViewState {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
session_capabilities: SharedSessionCapabilities,
|
session_capabilities: SharedSessionCapabilities,
|
||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
|
|
@ -44,7 +41,6 @@ impl EntryViewState {
|
||||||
workspace,
|
workspace,
|
||||||
project,
|
project,
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
entries: Vec::new(),
|
entries: Vec::new(),
|
||||||
session_capabilities,
|
session_capabilities,
|
||||||
agent_id,
|
agent_id,
|
||||||
|
|
@ -86,7 +82,6 @@ impl EntryViewState {
|
||||||
self.workspace.clone(),
|
self.workspace.clone(),
|
||||||
self.project.clone(),
|
self.project.clone(),
|
||||||
self.thread_store.clone(),
|
self.thread_store.clone(),
|
||||||
self.prompt_store.clone(),
|
|
||||||
self.session_capabilities.clone(),
|
self.session_capabilities.clone(),
|
||||||
self.agent_id.clone(),
|
self.agent_id.clone(),
|
||||||
"Edit message - @ to include context",
|
"Edit message - @ to include context",
|
||||||
|
|
@ -546,7 +541,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store,
|
thread_store,
|
||||||
None,
|
|
||||||
Arc::new(RwLock::new(SessionCapabilities::default())),
|
Arc::new(RwLock::new(SessionCapabilities::default())),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -43,7 +43,7 @@ use language_model::{ConfigurationError, ConfiguredModel, LanguageModelRegistry}
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::{DisableAiSettings, Project};
|
use project::{DisableAiSettings, Project};
|
||||||
use prompt_store::{PromptBuilder, PromptStore};
|
use prompt_store::PromptBuilder;
|
||||||
use settings::{Settings, SettingsStore};
|
use settings::{Settings, SettingsStore};
|
||||||
|
|
||||||
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
use terminal_view::{TerminalView, terminal_panel::TerminalPanel};
|
||||||
|
|
@ -228,7 +228,6 @@ impl InlineAssistant {
|
||||||
};
|
};
|
||||||
let agent_panel = agent_panel.read(cx);
|
let agent_panel = agent_panel.read(cx);
|
||||||
|
|
||||||
let prompt_store = agent_panel.prompt_store().as_ref().cloned();
|
|
||||||
let thread_store = agent_panel.thread_store().clone();
|
let thread_store = agent_panel.thread_store().clone();
|
||||||
|
|
||||||
let handle_assist =
|
let handle_assist =
|
||||||
|
|
@ -240,7 +239,6 @@ impl InlineAssistant {
|
||||||
cx.entity().downgrade(),
|
cx.entity().downgrade(),
|
||||||
workspace.project().downgrade(),
|
workspace.project().downgrade(),
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
action.prompt.clone(),
|
action.prompt.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -254,7 +252,6 @@ impl InlineAssistant {
|
||||||
cx.entity().downgrade(),
|
cx.entity().downgrade(),
|
||||||
workspace.project().downgrade(),
|
workspace.project().downgrade(),
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
action.prompt.clone(),
|
action.prompt.clone(),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
@ -437,7 +434,6 @@ impl InlineAssistant {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
codegen_ranges: &[Range<Anchor>],
|
codegen_ranges: &[Range<Anchor>],
|
||||||
|
|
@ -483,7 +479,6 @@ impl InlineAssistant {
|
||||||
session_id,
|
session_id,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
prompt_store.clone(),
|
|
||||||
project.clone(),
|
project.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
window,
|
window,
|
||||||
|
|
@ -574,7 +569,6 @@ impl InlineAssistant {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
|
@ -592,7 +586,6 @@ impl InlineAssistant {
|
||||||
workspace,
|
workspace,
|
||||||
project,
|
project,
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
initial_prompt,
|
initial_prompt,
|
||||||
window,
|
window,
|
||||||
&codegen_ranges,
|
&codegen_ranges,
|
||||||
|
|
@ -1915,7 +1908,6 @@ pub mod evals {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store,
|
thread_store,
|
||||||
None,
|
|
||||||
Some(prompt),
|
Some(prompt),
|
||||||
window,
|
window,
|
||||||
cx,
|
cx,
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,6 @@ use language_model::{LanguageModel, LanguageModelRegistry};
|
||||||
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
|
||||||
use parking_lot::Mutex;
|
use parking_lot::Mutex;
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::cmp;
|
use std::cmp;
|
||||||
use std::ops::Range;
|
use std::ops::Range;
|
||||||
|
|
@ -1237,7 +1236,6 @@ impl PromptEditor<BufferCodegen> {
|
||||||
session_id: Uuid,
|
session_id: Uuid,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
@ -1276,8 +1274,7 @@ impl PromptEditor<BufferCodegen> {
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let mention_set = cx
|
let mention_set = cx.new(|_cx| MentionSet::new(project, Some(thread_store.clone())));
|
||||||
.new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
|
|
||||||
|
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
|
|
@ -1393,7 +1390,6 @@ impl PromptEditor<TerminalCodegen> {
|
||||||
session_id: Uuid,
|
session_id: Uuid,
|
||||||
fs: Arc<dyn Fs>,
|
fs: Arc<dyn Fs>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
|
|
@ -1427,8 +1423,7 @@ impl PromptEditor<TerminalCodegen> {
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
|
|
||||||
let mention_set = cx
|
let mention_set = cx.new(|_cx| MentionSet::new(project, Some(thread_store.clone())));
|
||||||
.new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
|
|
||||||
|
|
||||||
let model_selector_menu_handle = PopoverMenuHandle::default();
|
let model_selector_menu_handle = PopoverMenuHandle::default();
|
||||||
|
|
||||||
|
|
@ -1705,7 +1700,6 @@ mod tests {
|
||||||
session_id,
|
session_id,
|
||||||
fs,
|
fs,
|
||||||
thread_store,
|
thread_store,
|
||||||
None,
|
|
||||||
project,
|
project,
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
window,
|
window,
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,6 @@ use language_model::{LanguageModelImage, LanguageModelImageExt};
|
||||||
use multi_buffer::MultiBufferRow;
|
use multi_buffer::MultiBufferRow;
|
||||||
use postage::stream::Stream as _;
|
use postage::stream::Stream as _;
|
||||||
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
use project::{Project, ProjectItem, ProjectPath, Worktree};
|
||||||
use prompt_store::{PromptId, PromptStore};
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use std::{
|
use std::{
|
||||||
cell::RefCell,
|
cell::RefCell,
|
||||||
|
|
@ -61,21 +60,15 @@ pub struct MentionImage {
|
||||||
pub struct MentionSet {
|
pub struct MentionSet {
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
|
mentions: HashMap<CreaseId, (MentionUri, MentionTask)>,
|
||||||
crease_entities: HashMap<CreaseId, Entity<LoadingContext>>,
|
crease_entities: HashMap<CreaseId, Entity<LoadingContext>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MentionSet {
|
impl MentionSet {
|
||||||
pub fn new(
|
pub fn new(project: WeakEntity<Project>, thread_store: Option<Entity<ThreadStore>>) -> Self {
|
||||||
project: WeakEntity<Project>,
|
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
) -> Self {
|
|
||||||
Self {
|
Self {
|
||||||
project,
|
project,
|
||||||
thread_store,
|
thread_store,
|
||||||
prompt_store,
|
|
||||||
mentions: HashMap::default(),
|
mentions: HashMap::default(),
|
||||||
crease_entities: HashMap::default(),
|
crease_entities: HashMap::default(),
|
||||||
}
|
}
|
||||||
|
|
@ -153,7 +146,6 @@ impl MentionSet {
|
||||||
line_range,
|
line_range,
|
||||||
..
|
..
|
||||||
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
||||||
MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
|
|
||||||
MentionUri::Skill {
|
MentionUri::Skill {
|
||||||
skill_file_path, ..
|
skill_file_path, ..
|
||||||
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
||||||
|
|
@ -327,7 +319,6 @@ impl MentionSet {
|
||||||
line_range,
|
line_range,
|
||||||
..
|
..
|
||||||
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
} => self.confirm_mention_for_symbol(abs_path, line_range, cx),
|
||||||
MentionUri::Rule { id, .. } => self.confirm_mention_for_rule(id, cx),
|
|
||||||
MentionUri::Skill {
|
MentionUri::Skill {
|
||||||
skill_file_path, ..
|
skill_file_path, ..
|
||||||
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
} => self.confirm_mention_for_skill(skill_file_path, cx),
|
||||||
|
|
@ -515,24 +506,6 @@ impl MentionSet {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
fn confirm_mention_for_rule(
|
|
||||||
&mut self,
|
|
||||||
id: PromptId,
|
|
||||||
cx: &mut Context<Self>,
|
|
||||||
) -> Task<Result<Mention>> {
|
|
||||||
let Some(prompt_store) = self.prompt_store.as_ref() else {
|
|
||||||
return Task::ready(Err(anyhow!("Missing prompt store")));
|
|
||||||
};
|
|
||||||
let prompt = prompt_store.read(cx).load(id, cx);
|
|
||||||
cx.spawn(async move |_, _| {
|
|
||||||
let prompt = prompt.await?;
|
|
||||||
Ok(Mention::Text {
|
|
||||||
content: prompt,
|
|
||||||
tracked_buffers: Vec::new(),
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn confirm_mention_for_selection(
|
pub fn confirm_mention_for_selection(
|
||||||
&mut self,
|
&mut self,
|
||||||
source_range: Range<text::Anchor>,
|
source_range: Range<text::Anchor>,
|
||||||
|
|
@ -773,7 +746,7 @@ mod tests {
|
||||||
fs.insert_tree("/project", json!({"file": ""})).await;
|
fs.insert_tree("/project", json!({"file": ""})).await;
|
||||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||||
let thread_store = None;
|
let thread_store = None;
|
||||||
let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), thread_store, None));
|
let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), thread_store));
|
||||||
|
|
||||||
let task = mention_set.update(cx, |mention_set, cx| {
|
let task = mention_set.update(cx, |mention_set, cx| {
|
||||||
mention_set.confirm_mention_for_thread(acp::SessionId::new("thread-1"), cx)
|
mention_set.confirm_mention_for_thread(acp::SessionId::new("thread-1"), cx)
|
||||||
|
|
@ -799,7 +772,7 @@ mod tests {
|
||||||
)
|
)
|
||||||
.await;
|
.await;
|
||||||
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
let project = Project::test(fs, [Path::new(path!("/project"))], cx).await;
|
||||||
let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), None, None));
|
let mention_set = cx.new(|_cx| MentionSet::new(project.downgrade(), None));
|
||||||
|
|
||||||
let mention_task = mention_set.update(cx, |mention_set, cx| {
|
let mention_task = mention_set.update(cx, |mention_set, cx| {
|
||||||
let http_client = project.read(cx).client().http_client();
|
let http_client = project.read(cx).client().http_client();
|
||||||
|
|
|
||||||
|
|
@ -33,7 +33,6 @@ use project::AgentId;
|
||||||
use project::{
|
use project::{
|
||||||
CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectPath, Worktree,
|
CompletionIntent, InlayHint, InlayHintLabel, InlayId, Project, ProjectPath, Worktree,
|
||||||
};
|
};
|
||||||
use prompt_store::PromptStore;
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use std::{cmp::min, fmt::Write, ops::Range, rc::Rc, sync::Arc};
|
use std::{cmp::min, fmt::Write, ops::Range, rc::Rc, sync::Arc};
|
||||||
|
|
@ -453,7 +452,6 @@ impl MessageEditor {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Option<Entity<ThreadStore>>,
|
thread_store: Option<Entity<ThreadStore>>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
session_capabilities: SharedSessionCapabilities,
|
session_capabilities: SharedSessionCapabilities,
|
||||||
agent_id: AgentId,
|
agent_id: AgentId,
|
||||||
placeholder: &str,
|
placeholder: &str,
|
||||||
|
|
@ -506,8 +504,7 @@ impl MessageEditor {
|
||||||
|
|
||||||
editor
|
editor
|
||||||
});
|
});
|
||||||
let mention_set =
|
let mention_set = cx.new(|_cx| MentionSet::new(project, thread_store.clone()));
|
||||||
cx.new(|_cx| MentionSet::new(project, thread_store.clone(), prompt_store.clone()));
|
|
||||||
let completion_provider = Rc::new(PromptCompletionProvider::new(
|
let completion_provider = Rc::new(PromptCompletionProvider::new(
|
||||||
MessageEditorCompletionDelegate {
|
MessageEditorCompletionDelegate {
|
||||||
session_capabilities: session_capabilities.clone(),
|
session_capabilities: session_capabilities.clone(),
|
||||||
|
|
@ -2475,7 +2472,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -2576,7 +2572,6 @@ mod tests {
|
||||||
workspace_handle.clone(),
|
workspace_handle.clone(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
"Claude Agent".into(),
|
"Claude Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -2742,7 +2737,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -2915,7 +2909,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3064,7 +3057,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
session_capabilities.clone(),
|
session_capabilities.clone(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3556,7 +3548,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3657,7 +3648,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3726,7 +3716,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3779,7 +3768,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3836,7 +3824,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3894,7 +3881,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -3956,7 +3942,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4116,7 +4101,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4236,7 +4220,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store.clone()),
|
Some(thread_store.clone()),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4315,7 +4298,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4493,7 +4475,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -4905,7 +4886,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -5160,7 +5140,6 @@ mod tests {
|
||||||
workspace_handle,
|
workspace_handle,
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
Some(thread_store),
|
Some(thread_store),
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -5253,7 +5232,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
@ -5402,7 +5380,6 @@ mod tests {
|
||||||
workspace.downgrade(),
|
workspace.downgrade(),
|
||||||
project.downgrade(),
|
project.downgrade(),
|
||||||
None,
|
None,
|
||||||
None,
|
|
||||||
Default::default(),
|
Default::default(),
|
||||||
"Test Agent".into(),
|
"Test Agent".into(),
|
||||||
"Test",
|
"Test",
|
||||||
|
|
|
||||||
|
|
@ -22,7 +22,7 @@ use language_models::provider::anthropic::telemetry::{
|
||||||
AnthropicCompletionType, AnthropicEventData, AnthropicEventType, report_anthropic_event,
|
AnthropicCompletionType, AnthropicEventData, AnthropicEventType, report_anthropic_event,
|
||||||
};
|
};
|
||||||
use project::Project;
|
use project::Project;
|
||||||
use prompt_store::{PromptBuilder, PromptStore};
|
use prompt_store::PromptBuilder;
|
||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
use terminal_view::TerminalView;
|
use terminal_view::TerminalView;
|
||||||
use ui::prelude::*;
|
use ui::prelude::*;
|
||||||
|
|
@ -64,7 +64,6 @@ impl TerminalInlineAssistant {
|
||||||
workspace: WeakEntity<Workspace>,
|
workspace: WeakEntity<Workspace>,
|
||||||
project: WeakEntity<Project>,
|
project: WeakEntity<Project>,
|
||||||
thread_store: Entity<ThreadStore>,
|
thread_store: Entity<ThreadStore>,
|
||||||
prompt_store: Option<Entity<PromptStore>>,
|
|
||||||
initial_prompt: Option<String>,
|
initial_prompt: Option<String>,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut App,
|
cx: &mut App,
|
||||||
|
|
@ -89,7 +88,6 @@ impl TerminalInlineAssistant {
|
||||||
session_id,
|
session_id,
|
||||||
self.fs.clone(),
|
self.fs.clone(),
|
||||||
thread_store.clone(),
|
thread_store.clone(),
|
||||||
prompt_store.clone(),
|
|
||||||
project.clone(),
|
project.clone(),
|
||||||
workspace.clone(),
|
workspace.clone(),
|
||||||
window,
|
window,
|
||||||
|
|
|
||||||
|
|
@ -1888,7 +1888,7 @@ mod tests {
|
||||||
.unwrap();
|
.unwrap();
|
||||||
let mut vcx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
let mut vcx = VisualTestContext::from_window(multi_workspace.into(), cx);
|
||||||
let panel = workspace_entity.update_in(&mut vcx, |workspace, window, cx| {
|
let panel = workspace_entity.update_in(&mut vcx, |workspace, window, cx| {
|
||||||
cx.new(|cx| crate::AgentPanel::new(workspace, None, window, cx))
|
cx.new(|cx| crate::AgentPanel::new(workspace, window, cx))
|
||||||
});
|
});
|
||||||
(panel, vcx)
|
(panel, vcx)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,6 @@ use gpui::{
|
||||||
pulsating_between,
|
pulsating_between,
|
||||||
};
|
};
|
||||||
use language::Buffer;
|
use language::Buffer;
|
||||||
use prompt_store::PromptId;
|
|
||||||
use rope::Point;
|
use rope::Point;
|
||||||
use settings::Settings;
|
use settings::Settings;
|
||||||
use theme_settings::ThemeSettings;
|
use theme_settings::ThemeSettings;
|
||||||
|
|
@ -195,9 +194,6 @@ fn open_mention_uri(
|
||||||
MentionUri::Thread { id, name } => {
|
MentionUri::Thread { id, name } => {
|
||||||
open_thread(workspace, id, name, window, cx);
|
open_thread(workspace, id, name, window, cx);
|
||||||
}
|
}
|
||||||
MentionUri::Rule { id, .. } => {
|
|
||||||
open_rule(workspace, id, window, cx);
|
|
||||||
}
|
|
||||||
MentionUri::Skill {
|
MentionUri::Skill {
|
||||||
skill_file_path, ..
|
skill_file_path, ..
|
||||||
} => {
|
} => {
|
||||||
|
|
@ -360,23 +356,3 @@ fn open_thread(
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_rule(
|
|
||||||
_workspace: &mut Workspace,
|
|
||||||
id: PromptId,
|
|
||||||
window: &mut Window,
|
|
||||||
cx: &mut Context<Workspace>,
|
|
||||||
) {
|
|
||||||
use zed_actions::assistant::OpenRulesLibrary;
|
|
||||||
|
|
||||||
let PromptId::User { uuid } = id else {
|
|
||||||
return;
|
|
||||||
};
|
|
||||||
|
|
||||||
window.dispatch_action(
|
|
||||||
Box::new(OpenRulesLibrary {
|
|
||||||
prompt_to_select: Some(uuid.0),
|
|
||||||
}),
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
|
||||||
|
|
@ -12,9 +12,12 @@ mod real_implementation {
|
||||||
|
|
||||||
impl Default for EchoCanceller {
|
impl Default for EchoCanceller {
|
||||||
fn default() -> Self {
|
fn default() -> Self {
|
||||||
Self(Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
// Sound-effect playback only feeds this APM through `process_reverse_stream`
|
||||||
true, false, false, false,
|
// for AEC reference; gain/HPF/NS would be no-ops here, so we keep the
|
||||||
))))
|
// original (echo only) configuration via the legacy flag form.
|
||||||
|
Self(Arc::new(Mutex::new(
|
||||||
|
apm::AudioProcessingModule::from_flags(true, false, false, false),
|
||||||
|
)))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -143,7 +143,7 @@ pub enum ChannelEvent {
|
||||||
|
|
||||||
impl EventEmitter<ChannelEvent> for ChannelStore {}
|
impl EventEmitter<ChannelEvent> for ChannelStore {}
|
||||||
|
|
||||||
enum OpenEntityHandle<E> {
|
enum OpenEntityHandle<E: 'static> {
|
||||||
Open(WeakEntity<E>),
|
Open(WeakEntity<E>),
|
||||||
Loading(Shared<Task<Result<Entity<E>, Arc<anyhow::Error>>>>),
|
Loading(Shared<Task<Result<Entity<E>, Arc<anyhow::Error>>>>),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -164,10 +164,12 @@ pub fn lsp_tasks(
|
||||||
},
|
},
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
lsp_tasks
|
if !new_lsp_tasks.is_empty() {
|
||||||
.entry(source_kind)
|
lsp_tasks
|
||||||
.or_insert_with(Vec::new)
|
.entry(source_kind)
|
||||||
.append(&mut new_lsp_tasks);
|
.or_insert_with(Vec::new)
|
||||||
|
.append(&mut new_lsp_tasks);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
lsp_tasks.into_iter().collect()
|
lsp_tasks.into_iter().collect()
|
||||||
|
|
|
||||||
|
|
@ -35,6 +35,18 @@ impl FeatureFlag for AgentSharingFeatureFlag {
|
||||||
}
|
}
|
||||||
register_feature_flag!(AgentSharingFeatureFlag);
|
register_feature_flag!(AgentSharingFeatureFlag);
|
||||||
|
|
||||||
|
pub struct HandoffFeatureFlag;
|
||||||
|
|
||||||
|
impl FeatureFlag for HandoffFeatureFlag {
|
||||||
|
const NAME: &'static str = "handoff";
|
||||||
|
type Value = PresenceFlag;
|
||||||
|
|
||||||
|
fn enabled_for_staff() -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
register_feature_flag!(HandoffFeatureFlag);
|
||||||
|
|
||||||
pub struct DiffReviewFeatureFlag;
|
pub struct DiffReviewFeatureFlag;
|
||||||
|
|
||||||
impl FeatureFlag for DiffReviewFeatureFlag {
|
impl FeatureFlag for DiffReviewFeatureFlag {
|
||||||
|
|
|
||||||
|
|
@ -5,6 +5,7 @@ use crate::commit_view::CommitView;
|
||||||
use crate::git_panel_settings::GitPanelScrollbarAccessor;
|
use crate::git_panel_settings::GitPanelScrollbarAccessor;
|
||||||
use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
|
use crate::project_diff::{self, BranchDiff, Diff, ProjectDiff};
|
||||||
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
use crate::remote_output::{self, RemoteAction, SuccessMessage};
|
||||||
|
use crate::solo_diff_view::SoloDiffView;
|
||||||
use crate::{branch_picker, picker_prompt, render_remote_button};
|
use crate::{branch_picker, picker_prompt, render_remote_button};
|
||||||
use crate::{
|
use crate::{
|
||||||
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
|
||||||
|
|
@ -15,10 +16,7 @@ use anyhow::Context as _;
|
||||||
use askpass::AskPassDelegate;
|
use askpass::AskPassDelegate;
|
||||||
use collections::{BTreeMap, HashMap, HashSet};
|
use collections::{BTreeMap, HashMap, HashSet};
|
||||||
use db::kvp::KeyValueStore;
|
use db::kvp::KeyValueStore;
|
||||||
use editor::{
|
use editor::{Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior};
|
||||||
Direction, Editor, EditorElement, EditorMode, MultiBuffer, MultiBufferOffset, SizingBehavior,
|
|
||||||
actions::ExpandAllDiffHunks,
|
|
||||||
};
|
|
||||||
use editor::{EditorStyle, RewrapOptions};
|
use editor::{EditorStyle, RewrapOptions};
|
||||||
use file_icons::FileIcons;
|
use file_icons::FileIcons;
|
||||||
use futures::StreamExt as _;
|
use futures::StreamExt as _;
|
||||||
|
|
@ -62,7 +60,7 @@ use project::{
|
||||||
},
|
},
|
||||||
project_settings::{GitPathStyle, ProjectSettings},
|
project_settings::{GitPathStyle, ProjectSettings},
|
||||||
};
|
};
|
||||||
use prompt_store::{BuiltInPrompt, PromptId, PromptStore, RULES_FILE_NAMES};
|
use prompt_store::RULES_FILE_NAMES;
|
||||||
use proto::RpcError;
|
use proto::RpcError;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use settings::{Settings, SettingsStore, StatusStyle, update_settings_file};
|
use settings::{Settings, SettingsStore, StatusStyle, update_settings_file};
|
||||||
|
|
@ -85,7 +83,7 @@ use workspace::SERIALIZATION_THROTTLE_TIME;
|
||||||
use workspace::{
|
use workspace::{
|
||||||
Item, Workspace,
|
Item, Workspace,
|
||||||
dock::{DockPosition, Panel, PanelEvent},
|
dock::{DockPosition, Panel, PanelEvent},
|
||||||
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyResultExt},
|
notifications::{DetachAndPromptErr, ErrorMessagePrompt, NotificationId, NotifyTaskExt},
|
||||||
};
|
};
|
||||||
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
use zed_actions::{DecreaseBufferFontSize, IncreaseBufferFontSize, ResetBufferFontSize};
|
||||||
|
|
||||||
|
|
@ -1385,63 +1383,22 @@ impl GitPanel {
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
fn open_file(
|
fn open_solo_diff(
|
||||||
&mut self,
|
&mut self,
|
||||||
_: &menu::SecondaryConfirm,
|
_: &menu::SecondaryConfirm,
|
||||||
window: &mut Window,
|
window: &mut Window,
|
||||||
cx: &mut Context<Self>,
|
cx: &mut Context<Self>,
|
||||||
) {
|
) {
|
||||||
maybe!({
|
maybe!({
|
||||||
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
|
let entry = self
|
||||||
let active_repo = self.active_repository.as_ref()?;
|
.entries
|
||||||
let path = active_repo
|
.get(self.selected_entry?)?
|
||||||
.read(cx)
|
.status_entry()?
|
||||||
.repo_path_to_project_path(&entry.repo_path, cx)?;
|
.clone();
|
||||||
if entry.status.is_deleted() {
|
let repository = self.active_repository.clone()?;
|
||||||
return None;
|
|
||||||
}
|
|
||||||
|
|
||||||
let open_task = self
|
SoloDiffView::open_or_focus(entry, repository, self.workspace.clone(), window, cx)
|
||||||
.workspace
|
.detach_and_notify_err(self.workspace.clone(), window, cx);
|
||||||
.update(cx, |workspace, cx| {
|
|
||||||
workspace.open_path_preview(path, None, false, false, true, window, cx)
|
|
||||||
})
|
|
||||||
.ok()?;
|
|
||||||
|
|
||||||
let workspace = self.workspace.clone();
|
|
||||||
cx.spawn_in(window, async move |_, mut cx| {
|
|
||||||
let item = open_task
|
|
||||||
.await
|
|
||||||
.notify_workspace_async_err(workspace, &mut cx)
|
|
||||||
.ok_or_else(|| anyhow::anyhow!("Failed to open file"))?;
|
|
||||||
if let Some(active_editor) = item.downcast::<Editor>() {
|
|
||||||
if let Some(diff_task) =
|
|
||||||
active_editor.update(cx, |editor, _cx| editor.wait_for_diff_to_load())
|
|
||||||
{
|
|
||||||
diff_task.await;
|
|
||||||
}
|
|
||||||
|
|
||||||
cx.update(|window, cx| {
|
|
||||||
active_editor.update(cx, |editor, cx| {
|
|
||||||
editor.expand_all_diff_hunks(&ExpandAllDiffHunks, window, cx);
|
|
||||||
|
|
||||||
let snapshot = editor.snapshot(window, cx);
|
|
||||||
editor.go_to_hunk_before_or_after_position(
|
|
||||||
&snapshot,
|
|
||||||
language::Point::new(0, 0),
|
|
||||||
Direction::Next,
|
|
||||||
true,
|
|
||||||
window,
|
|
||||||
cx,
|
|
||||||
);
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.log_err();
|
|
||||||
}
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
.detach();
|
|
||||||
|
|
||||||
Some(())
|
Some(())
|
||||||
});
|
});
|
||||||
|
|
@ -2685,20 +2642,6 @@ impl GitPanel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async fn load_commit_message_prompt(cx: &mut AsyncApp) -> String {
|
|
||||||
let load = async {
|
|
||||||
let store = cx.update(|cx| PromptStore::global(cx)).await.ok()?;
|
|
||||||
store
|
|
||||||
.update(cx, |s, cx| {
|
|
||||||
s.load(PromptId::BuiltIn(BuiltInPrompt::CommitMessage), cx)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.ok()
|
|
||||||
};
|
|
||||||
load.await
|
|
||||||
.unwrap_or_else(|| BuiltInPrompt::CommitMessage.default_content().to_string())
|
|
||||||
}
|
|
||||||
|
|
||||||
fn build_commit_message_prompt(
|
fn build_commit_message_prompt(
|
||||||
prompt: &str,
|
prompt: &str,
|
||||||
user_agents_md: Option<&str>,
|
user_agents_md: Option<&str>,
|
||||||
|
|
@ -2803,7 +2746,7 @@ impl GitPanel {
|
||||||
.and_then(|user_agents_md| user_agents_md.content().cloned())
|
.and_then(|user_agents_md| user_agents_md.content().cloned())
|
||||||
});
|
});
|
||||||
|
|
||||||
let prompt = Self::load_commit_message_prompt(&mut cx).await;
|
let prompt = include_str!("../src/commit_message_prompt.txt");
|
||||||
|
|
||||||
let subject = this.update(cx, |this, cx| {
|
let subject = this.update(cx, |this, cx| {
|
||||||
this.commit_editor
|
this.commit_editor
|
||||||
|
|
@ -5984,7 +5927,7 @@ impl GitPanel {
|
||||||
)
|
)
|
||||||
.separator()
|
.separator()
|
||||||
.action("Open Diff", menu::Confirm.boxed_clone())
|
.action("Open Diff", menu::Confirm.boxed_clone())
|
||||||
.action("Open File", menu::SecondaryConfirm.boxed_clone())
|
.action("Open Diff (File)", menu::SecondaryConfirm.boxed_clone())
|
||||||
.when(!is_created, |context_menu| {
|
.when(!is_created, |context_menu| {
|
||||||
context_menu
|
context_menu
|
||||||
.separator()
|
.separator()
|
||||||
|
|
@ -6263,7 +6206,7 @@ impl GitPanel {
|
||||||
this.selected_entry = Some(ix);
|
this.selected_entry = Some(ix);
|
||||||
cx.notify();
|
cx.notify();
|
||||||
if event.click_count() > 1 || event.modifiers().secondary() {
|
if event.click_count() > 1 || event.modifiers().secondary() {
|
||||||
this.open_file(&Default::default(), window, cx)
|
this.open_solo_diff(&Default::default(), window, cx)
|
||||||
} else {
|
} else {
|
||||||
this.open_diff(&Default::default(), window, cx);
|
this.open_diff(&Default::default(), window, cx);
|
||||||
this.focus_handle.focus(window, cx);
|
this.focus_handle.focus(window, cx);
|
||||||
|
|
@ -6713,7 +6656,7 @@ impl Render for GitPanel {
|
||||||
.on_action(cx.listener(Self::last_entry))
|
.on_action(cx.listener(Self::last_entry))
|
||||||
.on_action(cx.listener(Self::close_panel))
|
.on_action(cx.listener(Self::close_panel))
|
||||||
.on_action(cx.listener(Self::open_diff))
|
.on_action(cx.listener(Self::open_diff))
|
||||||
.on_action(cx.listener(Self::open_file))
|
.on_action(cx.listener(Self::open_solo_diff))
|
||||||
.on_action(cx.listener(Self::focus_changes_list))
|
.on_action(cx.listener(Self::focus_changes_list))
|
||||||
.on_action(cx.listener(Self::focus_editor))
|
.on_action(cx.listener(Self::focus_editor))
|
||||||
.on_action(cx.listener(Self::expand_commit_editor))
|
.on_action(cx.listener(Self::expand_commit_editor))
|
||||||
|
|
|
||||||
|
|
@ -45,6 +45,7 @@ pub mod picker_prompt;
|
||||||
pub mod project_diff;
|
pub mod project_diff;
|
||||||
pub(crate) mod remote_output;
|
pub(crate) mod remote_output;
|
||||||
pub mod repository_selector;
|
pub mod repository_selector;
|
||||||
|
pub mod solo_diff_view;
|
||||||
pub mod stash_picker;
|
pub mod stash_picker;
|
||||||
pub mod text_diff_view;
|
pub mod text_diff_view;
|
||||||
pub mod worktree_names;
|
pub mod worktree_names;
|
||||||
|
|
|
||||||
787
crates/git_ui/src/solo_diff_view.rs
Normal file
787
crates/git_ui/src/solo_diff_view.rs
Normal file
|
|
@ -0,0 +1,787 @@
|
||||||
|
use crate::{git_panel::GitStatusEntry, git_status_icon};
|
||||||
|
use anyhow::{Context as _, Result};
|
||||||
|
use buffer_diff::DiffHunkSecondaryStatus;
|
||||||
|
use editor::{
|
||||||
|
Direction, Editor, EditorEvent, EditorSettings, SplittableEditor, ToggleSplitDiff,
|
||||||
|
actions::{GoToHunk, GoToPreviousHunk},
|
||||||
|
};
|
||||||
|
use fs::Fs;
|
||||||
|
use git::{
|
||||||
|
Commit, Restore, StageAndNext, StageFile, ToggleStaged, UnstageAndNext, UnstageFile,
|
||||||
|
repository::RepoPath, status::StageStatus,
|
||||||
|
};
|
||||||
|
use gpui::{
|
||||||
|
Action, AnyElement, App, AppContext as _, Context, Entity, EventEmitter, FocusHandle,
|
||||||
|
Focusable, IntoElement, Render, Subscription, Task, WeakEntity, Window,
|
||||||
|
};
|
||||||
|
use language::{Buffer, HighlightedText};
|
||||||
|
use multi_buffer::MultiBuffer;
|
||||||
|
use project::{
|
||||||
|
Project,
|
||||||
|
git_store::{Repository, RepositoryId},
|
||||||
|
};
|
||||||
|
use settings::{DiffViewStyle, Settings, SettingsStore, update_settings_file};
|
||||||
|
use std::{
|
||||||
|
any::{Any, TypeId},
|
||||||
|
sync::Arc,
|
||||||
|
};
|
||||||
|
use ui::{
|
||||||
|
Color, DiffStat, Divider, Icon, IconButton, IconButtonShape, IconName, Label, LabelCommon as _,
|
||||||
|
SharedString, Tooltip, prelude::*, vertical_divider,
|
||||||
|
};
|
||||||
|
use util::paths::{PathExt as _, PathStyle};
|
||||||
|
use workspace::{
|
||||||
|
Item, ItemHandle, ItemNavHistory, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView,
|
||||||
|
Workspace,
|
||||||
|
item::{ItemEvent, SaveOptions, TabContentParams},
|
||||||
|
notifications::NotifyTaskExt,
|
||||||
|
searchable::SearchableItemHandle,
|
||||||
|
};
|
||||||
|
|
||||||
|
pub struct SoloDiffView {
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
repository_id: RepositoryId,
|
||||||
|
repo_path: RepoPath,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
editor: Entity<SplittableEditor>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
_settings_subscription: Subscription,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoloDiffView {
|
||||||
|
pub fn open_or_focus(
|
||||||
|
entry: GitStatusEntry,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
workspace: WeakEntity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut App,
|
||||||
|
) -> Task<Result<Entity<Self>>> {
|
||||||
|
let Some(workspace_entity) = workspace.upgrade() else {
|
||||||
|
return Task::ready(Err(anyhow::anyhow!("workspace was dropped")));
|
||||||
|
};
|
||||||
|
|
||||||
|
let existing = workspace_entity
|
||||||
|
.read(cx)
|
||||||
|
.items_of_type::<SoloDiffView>(cx)
|
||||||
|
.find(|item| item.read(cx).matches(&repository, &entry.repo_path, cx));
|
||||||
|
if let Some(existing) = existing {
|
||||||
|
workspace_entity.update(cx, |workspace, cx| {
|
||||||
|
workspace.activate_item(&existing, true, true, window, cx);
|
||||||
|
});
|
||||||
|
existing.focus_handle(cx).focus(window, cx);
|
||||||
|
return Task::ready(Ok(existing));
|
||||||
|
}
|
||||||
|
|
||||||
|
let Some(project_path) = repository
|
||||||
|
.read(cx)
|
||||||
|
.repo_path_to_project_path(&entry.repo_path, cx)
|
||||||
|
else {
|
||||||
|
return Task::ready(Err(anyhow::anyhow!(
|
||||||
|
"could not resolve repository path {:?}",
|
||||||
|
entry.repo_path
|
||||||
|
)));
|
||||||
|
};
|
||||||
|
|
||||||
|
let project = workspace_entity.read(cx).project().clone();
|
||||||
|
let repo_path = entry.repo_path;
|
||||||
|
window.spawn(cx, async move |cx| {
|
||||||
|
let buffer = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_buffer(project_path.clone(), cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
let diff = project
|
||||||
|
.update(cx, |project, cx| {
|
||||||
|
project.open_uncommitted_diff(buffer.clone(), cx)
|
||||||
|
})
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
workspace_entity.update_in(cx, |workspace, window, cx| {
|
||||||
|
let workspace_handle = cx.entity();
|
||||||
|
let view = cx.new(|cx| {
|
||||||
|
Self::new(
|
||||||
|
project,
|
||||||
|
repository,
|
||||||
|
repo_path,
|
||||||
|
buffer,
|
||||||
|
diff,
|
||||||
|
workspace_handle,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
workspace.add_item_to_active_pane(Box::new(view.clone()), None, true, window, cx);
|
||||||
|
view
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn new(
|
||||||
|
project: Entity<Project>,
|
||||||
|
repository: Entity<Repository>,
|
||||||
|
repo_path: RepoPath,
|
||||||
|
buffer: Entity<Buffer>,
|
||||||
|
diff: Entity<buffer_diff::BufferDiff>,
|
||||||
|
workspace: Entity<Workspace>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Self {
|
||||||
|
let repository_id = repository.read(cx).id;
|
||||||
|
let multibuffer = cx.new(|cx| {
|
||||||
|
let mut multibuffer = MultiBuffer::singleton(buffer.clone(), cx);
|
||||||
|
multibuffer.add_diff(diff, cx);
|
||||||
|
multibuffer.set_all_diff_hunks_expanded(cx);
|
||||||
|
multibuffer
|
||||||
|
});
|
||||||
|
let editor = cx.new(|cx| {
|
||||||
|
let editor = SplittableEditor::new(
|
||||||
|
EditorSettings::get_global(cx).diff_view_style,
|
||||||
|
multibuffer,
|
||||||
|
project.clone(),
|
||||||
|
workspace.clone(),
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
editor.rhs_editor().update(cx, |editor, cx| {
|
||||||
|
editor.set_should_serialize(false, cx);
|
||||||
|
let snapshot = editor.snapshot(window, cx);
|
||||||
|
editor.go_to_hunk_before_or_after_position(
|
||||||
|
&snapshot,
|
||||||
|
language::Point::new(0, 0),
|
||||||
|
Direction::Next,
|
||||||
|
true,
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
);
|
||||||
|
});
|
||||||
|
editor
|
||||||
|
});
|
||||||
|
|
||||||
|
let mut previous_diff_view_style = EditorSettings::get_global(cx).diff_view_style;
|
||||||
|
let settings_subscription =
|
||||||
|
cx.observe_global_in::<SettingsStore>(window, move |this, window, cx| {
|
||||||
|
let diff_view_style = EditorSettings::get_global(cx).diff_view_style;
|
||||||
|
if diff_view_style != previous_diff_view_style {
|
||||||
|
this.editor.update(cx, |editor, cx| {
|
||||||
|
if editor.diff_view_style() != diff_view_style {
|
||||||
|
editor.toggle_split(&ToggleSplitDiff, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
previous_diff_view_style = diff_view_style;
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
Self {
|
||||||
|
repository,
|
||||||
|
repository_id,
|
||||||
|
repo_path,
|
||||||
|
buffer,
|
||||||
|
editor,
|
||||||
|
workspace: workspace.downgrade(),
|
||||||
|
_settings_subscription: settings_subscription,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn matches(&self, repository: &Entity<Repository>, repo_path: &RepoPath, cx: &App) -> bool {
|
||||||
|
self.repository_id == repository.read(cx).id && &self.repo_path == repo_path
|
||||||
|
}
|
||||||
|
|
||||||
|
fn button_states(&self, cx: &App) -> SoloDiffButtonStates {
|
||||||
|
let editor = self.editor.read(cx).rhs_editor().read(cx);
|
||||||
|
let multibuffer = editor.buffer().read(cx);
|
||||||
|
let snapshot = multibuffer.snapshot(cx);
|
||||||
|
let prev_next = snapshot.diff_hunks().nth(1).is_some();
|
||||||
|
let mut selection = true;
|
||||||
|
|
||||||
|
let mut ranges = editor
|
||||||
|
.selections
|
||||||
|
.disjoint_anchor_ranges()
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
if !ranges.iter().any(|range| range.start != range.end) {
|
||||||
|
selection = false;
|
||||||
|
let anchor = editor.selections.newest_anchor().head();
|
||||||
|
if let Some((_, excerpt_range)) = snapshot.excerpt_containing(anchor..anchor)
|
||||||
|
&& let Some(range) = snapshot
|
||||||
|
.anchor_in_buffer(excerpt_range.context.start)
|
||||||
|
.zip(snapshot.anchor_in_buffer(excerpt_range.context.end))
|
||||||
|
.map(|(start, end)| start..end)
|
||||||
|
{
|
||||||
|
ranges = vec![range];
|
||||||
|
} else {
|
||||||
|
ranges = Vec::new();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut stage = false;
|
||||||
|
let mut unstage = false;
|
||||||
|
for hunk in editor.diff_hunks_in_ranges(&ranges, &snapshot) {
|
||||||
|
match hunk.status.secondary {
|
||||||
|
DiffHunkSecondaryStatus::HasSecondaryHunk
|
||||||
|
| DiffHunkSecondaryStatus::SecondaryHunkAdditionPending => {
|
||||||
|
stage = true;
|
||||||
|
}
|
||||||
|
DiffHunkSecondaryStatus::OverlapsWithSecondaryHunk => {
|
||||||
|
stage = true;
|
||||||
|
unstage = true;
|
||||||
|
}
|
||||||
|
DiffHunkSecondaryStatus::NoSecondaryHunk
|
||||||
|
| DiffHunkSecondaryStatus::SecondaryHunkRemovalPending => {
|
||||||
|
unstage = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let stage_status = self
|
||||||
|
.repository
|
||||||
|
.read(cx)
|
||||||
|
.status_for_path(&self.repo_path)
|
||||||
|
.map(|entry| entry.status.staging())
|
||||||
|
.unwrap_or(StageStatus::Unstaged);
|
||||||
|
|
||||||
|
SoloDiffButtonStates {
|
||||||
|
stage,
|
||||||
|
unstage,
|
||||||
|
restore: stage || unstage,
|
||||||
|
prev_next,
|
||||||
|
selection,
|
||||||
|
stage_file: stage_status.has_unstaged(),
|
||||||
|
unstage_file: stage_status.has_staged(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut App) {
|
||||||
|
self.focus_handle(cx).focus(window, cx);
|
||||||
|
let action = action.boxed_clone();
|
||||||
|
cx.defer(move |cx| {
|
||||||
|
cx.dispatch_action(action.as_ref());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn change_file_stage(&self, stage: bool, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
let repository = self.repository.clone();
|
||||||
|
let repo_path = self.repo_path.clone();
|
||||||
|
let workspace = self.workspace.clone();
|
||||||
|
let task = cx.spawn(async move |_, cx| {
|
||||||
|
repository
|
||||||
|
.update(cx, |repository, cx| {
|
||||||
|
if stage {
|
||||||
|
repository.stage_entries(vec![repo_path], cx)
|
||||||
|
} else {
|
||||||
|
repository.unstage_entries(vec![repo_path], cx)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.with_context(|| {
|
||||||
|
if stage {
|
||||||
|
"failed to stage file"
|
||||||
|
} else {
|
||||||
|
"failed to unstage file"
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
task.detach_and_notify_err(workspace, window, cx);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<EditorEvent> for SoloDiffView {}
|
||||||
|
|
||||||
|
impl Focusable for SoloDiffView {
|
||||||
|
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||||
|
self.editor.focus_handle(cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Item for SoloDiffView {
|
||||||
|
type Event = EditorEvent;
|
||||||
|
|
||||||
|
fn tab_icon(&self, _window: &Window, _cx: &App) -> Option<Icon> {
|
||||||
|
Some(Icon::new(IconName::Diff).color(Color::Muted))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||||
|
Label::new(self.tab_content_text(params.detail.unwrap_or_default(), cx))
|
||||||
|
.color(if params.selected {
|
||||||
|
Color::Default
|
||||||
|
} else {
|
||||||
|
Color::Muted
|
||||||
|
})
|
||||||
|
.into_any_element()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_content_text(&self, _detail: usize, cx: &App) -> SharedString {
|
||||||
|
self.buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.and_then(|file| {
|
||||||
|
Some(
|
||||||
|
file.full_path(cx)
|
||||||
|
.file_name()?
|
||||||
|
.to_string_lossy()
|
||||||
|
.to_string(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.repo_path
|
||||||
|
.as_ref()
|
||||||
|
.display(PathStyle::local())
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
.into()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn tab_tooltip_text(&self, cx: &App) -> Option<SharedString> {
|
||||||
|
Some(
|
||||||
|
self.buffer
|
||||||
|
.read(cx)
|
||||||
|
.file()
|
||||||
|
.map(|file| file.full_path(cx).compact().to_string_lossy().into_owned())
|
||||||
|
.unwrap_or_else(|| {
|
||||||
|
self.repo_path
|
||||||
|
.as_ref()
|
||||||
|
.display(PathStyle::local())
|
||||||
|
.into_owned()
|
||||||
|
})
|
||||||
|
.into(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn to_item_events(event: &EditorEvent, f: &mut dyn FnMut(ItemEvent)) {
|
||||||
|
Editor::to_item_events(event, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||||
|
Some("Solo Diff View Opened")
|
||||||
|
}
|
||||||
|
|
||||||
|
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
self.editor.deactivated(window, cx);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn act_as_type<'a>(
|
||||||
|
&'a self,
|
||||||
|
type_id: TypeId,
|
||||||
|
self_handle: &'a Entity<Self>,
|
||||||
|
cx: &'a App,
|
||||||
|
) -> Option<gpui::AnyEntity> {
|
||||||
|
if type_id == TypeId::of::<Self>() {
|
||||||
|
Some(self_handle.clone().into())
|
||||||
|
} else {
|
||||||
|
self.editor.act_as_type(type_id, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
|
||||||
|
Some(Box::new(self.editor.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn for_each_project_item(
|
||||||
|
&self,
|
||||||
|
cx: &App,
|
||||||
|
f: &mut dyn FnMut(gpui::EntityId, &dyn project::ProjectItem),
|
||||||
|
) {
|
||||||
|
self.editor.for_each_project_item(cx, f)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_nav_history(
|
||||||
|
&mut self,
|
||||||
|
nav_history: ItemNavHistory,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.rhs_editor().update(cx, |editor, _| {
|
||||||
|
editor.set_nav_history(Some(nav_history));
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn navigate(
|
||||||
|
&mut self,
|
||||||
|
data: Arc<dyn Any + Send>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> bool {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor
|
||||||
|
.rhs_editor()
|
||||||
|
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||||
|
ToolbarItemLocation::PrimaryLeft
|
||||||
|
}
|
||||||
|
|
||||||
|
fn breadcrumbs(&self, cx: &App) -> Option<(Vec<HighlightedText>, Option<gpui::Font>)> {
|
||||||
|
self.editor.breadcrumbs(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn added_to_workspace(
|
||||||
|
&mut self,
|
||||||
|
workspace: &mut Workspace,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
self.editor.update(cx, |editor, cx| {
|
||||||
|
editor.rhs_editor().update(cx, |editor, cx| {
|
||||||
|
editor.added_to_workspace(workspace, window, cx)
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn can_save(&self, cx: &App) -> bool {
|
||||||
|
self.editor.read(cx).rhs_editor().read(cx).can_save(cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn save(
|
||||||
|
&mut self,
|
||||||
|
options: SaveOptions,
|
||||||
|
project: Entity<Project>,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> Task<Result<()>> {
|
||||||
|
self.editor.save(options, project, window, cx)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SoloDiffView {
|
||||||
|
fn render(&mut self, _: &mut Window, _: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
self.editor.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SoloDiffStyleToolbar {
|
||||||
|
solo_diff: Option<WeakEntity<SoloDiffView>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct SoloDiffGitToolbar {
|
||||||
|
solo_diff: Option<WeakEntity<SoloDiffView>>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoloDiffStyleToolbar {
|
||||||
|
pub fn new(_: &mut Context<Self>) -> Self {
|
||||||
|
Self { solo_diff: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solo_diff(&self) -> Option<Entity<SoloDiffView>> {
|
||||||
|
self.solo_diff.as_ref()?.upgrade()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_diff_view_style(
|
||||||
|
&mut self,
|
||||||
|
diff_view_style: DiffViewStyle,
|
||||||
|
window: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) {
|
||||||
|
let Some(solo_diff) = self.solo_diff() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let workspace = solo_diff.read(cx).workspace.clone();
|
||||||
|
|
||||||
|
update_settings_file(<dyn Fs>::global(cx), cx, move |settings, _| {
|
||||||
|
settings.editor.diff_view_style = Some(diff_view_style);
|
||||||
|
});
|
||||||
|
|
||||||
|
if let Some(workspace) = workspace.upgrade() {
|
||||||
|
let splittable_editors = {
|
||||||
|
workspace
|
||||||
|
.read(cx)
|
||||||
|
.items(cx)
|
||||||
|
.filter_map(|item| item.act_as_type(TypeId::of::<SplittableEditor>(), cx))
|
||||||
|
.filter_map(|item| item.downcast::<SplittableEditor>().ok())
|
||||||
|
.collect::<Vec<_>>()
|
||||||
|
};
|
||||||
|
|
||||||
|
for editor in splittable_editors {
|
||||||
|
editor.update(cx, |editor, cx| {
|
||||||
|
if editor.diff_view_style() != diff_view_style {
|
||||||
|
editor.toggle_split(&ToggleSplitDiff, window, cx);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
cx.notify();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ToolbarItemEvent> for SoloDiffStyleToolbar {}
|
||||||
|
|
||||||
|
impl ToolbarItemView for SoloDiffStyleToolbar {
|
||||||
|
fn set_active_pane_item(
|
||||||
|
&mut self,
|
||||||
|
active_pane_item: Option<&dyn ItemHandle>,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> ToolbarItemLocation {
|
||||||
|
self.solo_diff = active_pane_item
|
||||||
|
.and_then(|item| item.act_as::<SoloDiffView>(cx))
|
||||||
|
.map(|entity| entity.downgrade());
|
||||||
|
if self.solo_diff.is_some() {
|
||||||
|
ToolbarItemLocation::PrimaryLeft
|
||||||
|
} else {
|
||||||
|
ToolbarItemLocation::Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SoloDiffStyleToolbar {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let Some(solo_diff) = self.solo_diff() else {
|
||||||
|
return div();
|
||||||
|
};
|
||||||
|
let editor_entity = solo_diff.read(cx).editor.clone();
|
||||||
|
let editor = editor_entity.read(cx);
|
||||||
|
let diff_view_style = editor.diff_view_style();
|
||||||
|
let is_split_set = diff_view_style == DiffViewStyle::Split;
|
||||||
|
let split_icon = if is_split_set && !editor.is_split() {
|
||||||
|
IconName::DiffSplitAuto
|
||||||
|
} else {
|
||||||
|
IconName::DiffSplit
|
||||||
|
};
|
||||||
|
|
||||||
|
h_flex()
|
||||||
|
.h_8()
|
||||||
|
.items_center()
|
||||||
|
.gap_1()
|
||||||
|
.child(
|
||||||
|
IconButton::new("solo-diff-unified", IconName::DiffUnified)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.toggle_state(diff_view_style == DiffViewStyle::Unified)
|
||||||
|
.tooltip(Tooltip::text("Unified"))
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.set_diff_view_style(DiffViewStyle::Unified, window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("solo-diff-split", split_icon)
|
||||||
|
.icon_size(IconSize::Small)
|
||||||
|
.toggle_state(diff_view_style == DiffViewStyle::Split)
|
||||||
|
.tooltip(Tooltip::text("Split"))
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.set_diff_view_style(DiffViewStyle::Split, window, cx);
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(vertical_divider())
|
||||||
|
.child(div().w_1())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SoloDiffGitToolbar {
|
||||||
|
pub fn new(_: &mut Context<Self>) -> Self {
|
||||||
|
Self { solo_diff: None }
|
||||||
|
}
|
||||||
|
|
||||||
|
fn solo_diff(&self) -> Option<Entity<SoloDiffView>> {
|
||||||
|
self.solo_diff.as_ref()?.upgrade()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn dispatch_action(&self, action: &dyn Action, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(solo_diff) = self.solo_diff() {
|
||||||
|
solo_diff.update(cx, |solo_diff, cx| {
|
||||||
|
solo_diff.dispatch_action(action, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn stage_file(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(solo_diff) = self.solo_diff() {
|
||||||
|
solo_diff.update(cx, |solo_diff, cx| {
|
||||||
|
solo_diff.change_file_stage(true, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn unstage_file(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||||
|
if let Some(solo_diff) = self.solo_diff() {
|
||||||
|
solo_diff.update(cx, |solo_diff, cx| {
|
||||||
|
solo_diff.change_file_stage(false, window, cx);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EventEmitter<ToolbarItemEvent> for SoloDiffGitToolbar {}
|
||||||
|
|
||||||
|
impl ToolbarItemView for SoloDiffGitToolbar {
|
||||||
|
fn set_active_pane_item(
|
||||||
|
&mut self,
|
||||||
|
active_pane_item: Option<&dyn ItemHandle>,
|
||||||
|
_: &mut Window,
|
||||||
|
cx: &mut Context<Self>,
|
||||||
|
) -> ToolbarItemLocation {
|
||||||
|
self.solo_diff = active_pane_item
|
||||||
|
.and_then(|item| item.act_as::<SoloDiffView>(cx))
|
||||||
|
.map(|entity| entity.downgrade());
|
||||||
|
if self.solo_diff.is_some() {
|
||||||
|
ToolbarItemLocation::PrimaryRight
|
||||||
|
} else {
|
||||||
|
ToolbarItemLocation::Hidden
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct SoloDiffButtonStates {
|
||||||
|
stage: bool,
|
||||||
|
unstage: bool,
|
||||||
|
restore: bool,
|
||||||
|
prev_next: bool,
|
||||||
|
selection: bool,
|
||||||
|
stage_file: bool,
|
||||||
|
unstage_file: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for SoloDiffGitToolbar {
|
||||||
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let Some(solo_diff) = self.solo_diff() else {
|
||||||
|
return div();
|
||||||
|
};
|
||||||
|
let focus_handle = solo_diff.focus_handle(cx);
|
||||||
|
let solo_diff = solo_diff.read(cx);
|
||||||
|
let button_states = solo_diff.button_states(cx);
|
||||||
|
let status_entry = solo_diff
|
||||||
|
.repository
|
||||||
|
.read(cx)
|
||||||
|
.status_for_path(&solo_diff.repo_path);
|
||||||
|
let status = status_entry.as_ref().map(|entry| entry.status);
|
||||||
|
let diff_stat = status_entry.and_then(|entry| entry.diff_stat);
|
||||||
|
|
||||||
|
h_group_xl()
|
||||||
|
.my_neg_1()
|
||||||
|
.py_1()
|
||||||
|
.items_center()
|
||||||
|
.flex_wrap()
|
||||||
|
.justify_between()
|
||||||
|
.children(status.map(|status| git_status_icon(status).into_any_element()))
|
||||||
|
.children(diff_stat.map(|stat| {
|
||||||
|
DiffStat::new("solo-diff-stat", stat.added as usize, stat.deleted as usize)
|
||||||
|
.into_any_element()
|
||||||
|
}))
|
||||||
|
.child(
|
||||||
|
h_group_sm()
|
||||||
|
.when(button_states.selection, |el| {
|
||||||
|
el.child(
|
||||||
|
Button::new("stage", "Toggle Staged")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Toggle Staged",
|
||||||
|
&ToggleStaged,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.stage && !button_states.unstage)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&ToggleStaged, window, cx)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.when(!button_states.selection, |el| {
|
||||||
|
el.child(
|
||||||
|
Button::new("stage", "Stage")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Stage and go to next hunk",
|
||||||
|
&StageAndNext,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.stage)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&StageAndNext, window, cx)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("unstage", "Unstage")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Unstage and go to next hunk",
|
||||||
|
&UnstageAndNext,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.unstage)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&UnstageAndNext, window, cx)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.child(
|
||||||
|
Button::new("restore", "Restore")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Restore selected hunk",
|
||||||
|
&Restore,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.restore)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&Restore, window, cx)
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
h_group_sm()
|
||||||
|
.child(
|
||||||
|
IconButton::new("up", IconName::ArrowUp)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Go to previous hunk",
|
||||||
|
&GoToPreviousHunk,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.prev_next)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&GoToPreviousHunk, window, cx)
|
||||||
|
})),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
IconButton::new("down", IconName::ArrowDown)
|
||||||
|
.shape(IconButtonShape::Square)
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Go to next hunk",
|
||||||
|
&GoToHunk,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.prev_next)
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&GoToHunk, window, cx)
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(vertical_divider())
|
||||||
|
.child(
|
||||||
|
h_group_sm()
|
||||||
|
.child(
|
||||||
|
Button::new("stage-file", "Stage File")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Stage file",
|
||||||
|
&StageFile,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.stage_file)
|
||||||
|
.on_click(
|
||||||
|
cx.listener(|this, _, window, cx| this.stage_file(window, cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
Button::new("unstage-file", "Unstage File")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Unstage file",
|
||||||
|
&UnstageFile,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.disabled(!button_states.unstage_file)
|
||||||
|
.on_click(
|
||||||
|
cx.listener(|this, _, window, cx| this.unstage_file(window, cx)),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.child(Divider::vertical())
|
||||||
|
.child(
|
||||||
|
Button::new("commit", "Commit")
|
||||||
|
.tooltip(Tooltip::for_action_title_in(
|
||||||
|
"Commit",
|
||||||
|
&Commit,
|
||||||
|
&focus_handle,
|
||||||
|
))
|
||||||
|
.on_click(cx.listener(|this, _, window, cx| {
|
||||||
|
this.dispatch_action(&Commit, window, cx);
|
||||||
|
})),
|
||||||
|
),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -336,6 +336,20 @@ impl TestAppContext {
|
||||||
self.test_platform.simulate_new_path_selection(select_path);
|
self.test_platform.simulate_new_path_selection(select_path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Simulates responding to a `prompt_for_paths` ("Open") dialog.
|
||||||
|
pub fn simulate_path_prompt_response(
|
||||||
|
&self,
|
||||||
|
select_paths: impl FnOnce(&crate::PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
|
||||||
|
) {
|
||||||
|
self.test_platform
|
||||||
|
.simulate_path_prompt_response(select_paths);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns true if there's a path selection dialog pending.
|
||||||
|
pub fn did_prompt_for_paths(&self) -> bool {
|
||||||
|
self.test_platform.did_prompt_for_paths()
|
||||||
|
}
|
||||||
|
|
||||||
/// Simulates clicking a button in an platform-level alert dialog.
|
/// Simulates clicking a button in an platform-level alert dialog.
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub fn simulate_prompt_answer(&self, button: &str) {
|
pub fn simulate_prompt_answer(&self, button: &str) {
|
||||||
|
|
@ -1098,3 +1112,54 @@ impl AnyWindowHandle {
|
||||||
.unwrap()
|
.unwrap()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use crate::{PathPromptOptions, TestAppContext};
|
||||||
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_simulate_path_prompt_response(cx: &mut TestAppContext) {
|
||||||
|
assert!(!cx.did_prompt_for_paths());
|
||||||
|
|
||||||
|
let receiver = cx.update(|cx| {
|
||||||
|
cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: false,
|
||||||
|
directories: true,
|
||||||
|
multiple: true,
|
||||||
|
prompt: None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
assert!(cx.did_prompt_for_paths());
|
||||||
|
|
||||||
|
let selected = vec![PathBuf::from("/a"), PathBuf::from("/b")];
|
||||||
|
cx.simulate_path_prompt_response({
|
||||||
|
let selected = selected.clone();
|
||||||
|
move |options| {
|
||||||
|
assert!(options.multiple);
|
||||||
|
Some(selected)
|
||||||
|
}
|
||||||
|
});
|
||||||
|
assert!(!cx.did_prompt_for_paths());
|
||||||
|
|
||||||
|
let response = receiver.await.unwrap().unwrap();
|
||||||
|
assert_eq!(response, Some(selected));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_simulate_path_prompt_cancellation(cx: &mut TestAppContext) {
|
||||||
|
let receiver = cx.update(|cx| {
|
||||||
|
cx.prompt_for_paths(PathPromptOptions {
|
||||||
|
files: true,
|
||||||
|
directories: false,
|
||||||
|
multiple: false,
|
||||||
|
prompt: None,
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
cx.simulate_path_prompt_response(|_options| None);
|
||||||
|
|
||||||
|
let response = receiver.await.unwrap().unwrap();
|
||||||
|
assert_eq!(response, None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,9 +6,7 @@ use scheduler::Instant;
|
||||||
use scheduler::Scheduler;
|
use scheduler::Scheduler;
|
||||||
use std::{future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc, time::Duration};
|
use std::{future::Future, marker::PhantomData, mem, pin::Pin, rc::Rc, sync::Arc, time::Duration};
|
||||||
|
|
||||||
pub use scheduler::{
|
pub use scheduler::{FallibleTask, LocalExecutor as SchedulerLocalExecutor, Priority, Task};
|
||||||
FallibleTask, ForegroundExecutor as SchedulerForegroundExecutor, Priority, Task,
|
|
||||||
};
|
|
||||||
|
|
||||||
/// A pointer to the executor that is currently running,
|
/// A pointer to the executor that is currently running,
|
||||||
/// for spawning background tasks.
|
/// for spawning background tasks.
|
||||||
|
|
@ -22,7 +20,7 @@ pub struct BackgroundExecutor {
|
||||||
/// for spawning tasks on the main thread.
|
/// for spawning tasks on the main thread.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ForegroundExecutor {
|
pub struct ForegroundExecutor {
|
||||||
inner: scheduler::ForegroundExecutor,
|
inner: scheduler::LocalExecutor,
|
||||||
dispatcher: Arc<dyn PlatformDispatcher>,
|
dispatcher: Arc<dyn PlatformDispatcher>,
|
||||||
not_send: PhantomData<Rc<()>>,
|
not_send: PhantomData<Rc<()>>,
|
||||||
}
|
}
|
||||||
|
|
@ -280,18 +278,29 @@ impl ForegroundExecutor {
|
||||||
)
|
)
|
||||||
} else {
|
} else {
|
||||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||||
let session_id = platform_scheduler.allocate_session_id();
|
let inner = platform_scheduler.foreground_executor();
|
||||||
(platform_scheduler, session_id)
|
return Self {
|
||||||
|
inner,
|
||||||
|
dispatcher,
|
||||||
|
not_send: PhantomData,
|
||||||
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
#[cfg(not(any(test, feature = "test-support")))]
|
#[cfg(not(any(test, feature = "test-support")))]
|
||||||
let (scheduler, session_id): (Arc<dyn Scheduler>, _) = {
|
let inner = {
|
||||||
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
let platform_scheduler = Arc::new(PlatformScheduler::new(dispatcher.clone()));
|
||||||
let session_id = platform_scheduler.allocate_session_id();
|
platform_scheduler.foreground_executor()
|
||||||
(platform_scheduler, session_id)
|
|
||||||
};
|
};
|
||||||
|
|
||||||
let inner = scheduler::ForegroundExecutor::new(session_id, scheduler);
|
#[cfg(any(test, feature = "test-support"))]
|
||||||
|
let inner = {
|
||||||
|
let scheduler_for_dispatch = Arc::downgrade(&scheduler);
|
||||||
|
scheduler::LocalExecutor::new(session_id, scheduler, move |runnable| {
|
||||||
|
if let Some(scheduler) = scheduler_for_dispatch.upgrade() {
|
||||||
|
scheduler.schedule_local(session_id, runnable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
inner,
|
inner,
|
||||||
|
|
@ -366,7 +375,7 @@ impl ForegroundExecutor {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[doc(hidden)]
|
#[doc(hidden)]
|
||||||
pub fn scheduler_executor(&self) -> SchedulerForegroundExecutor {
|
pub fn scheduler_executor(&self) -> SchedulerLocalExecutor {
|
||||||
self.inner.clone()
|
self.inner.clone()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -139,8 +139,7 @@ impl PlatformDispatcher for TestDispatcher {
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
|
fn dispatch_on_main_thread(&self, runnable: RunnableVariant, _priority: Priority) {
|
||||||
self.scheduler
|
self.scheduler.schedule_local(self.session_id, runnable);
|
||||||
.schedule_foreground(self.session_id, runnable);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
||||||
|
|
|
||||||
|
|
@ -1,9 +1,10 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
AnyWindowHandle, BackgroundExecutor, ClipboardItem, CursorStyle, DevicePixels,
|
||||||
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, Platform, PlatformDisplay,
|
DummyKeyboardMapper, ForegroundExecutor, Keymap, NoopTextSystem, PathPromptOptions, Platform,
|
||||||
PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper, PlatformTextSystem,
|
PlatformDisplay, PlatformHeadlessRenderer, PlatformKeyboardLayout, PlatformKeyboardMapper,
|
||||||
PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream, SourceMetadata,
|
PlatformTextSystem, PromptButton, ScreenCaptureFrame, ScreenCaptureSource, ScreenCaptureStream,
|
||||||
Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams, size,
|
SourceMetadata, Task, TestDisplay, TestWindow, ThermalState, WindowAppearance, WindowParams,
|
||||||
|
size,
|
||||||
};
|
};
|
||||||
use anyhow::Result;
|
use anyhow::Result;
|
||||||
use collections::VecDeque;
|
use collections::VecDeque;
|
||||||
|
|
@ -85,6 +86,10 @@ struct TestPrompt {
|
||||||
pub(crate) struct TestPrompts {
|
pub(crate) struct TestPrompts {
|
||||||
multiple_choice: VecDeque<TestPrompt>,
|
multiple_choice: VecDeque<TestPrompt>,
|
||||||
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
new_path: VecDeque<(PathBuf, oneshot::Sender<Result<Option<PathBuf>>>)>,
|
||||||
|
paths: VecDeque<(
|
||||||
|
PathPromptOptions,
|
||||||
|
oneshot::Sender<Result<Option<Vec<PathBuf>>>>,
|
||||||
|
)>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl TestPlatform {
|
impl TestPlatform {
|
||||||
|
|
@ -147,6 +152,33 @@ impl TestPlatform {
|
||||||
tx.send(Ok(select_path(&path))).ok();
|
tx.send(Ok(select_path(&path))).ok();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn simulate_path_prompt_response(
|
||||||
|
&self,
|
||||||
|
select_paths: impl FnOnce(&PathPromptOptions) -> Option<Vec<std::path::PathBuf>>,
|
||||||
|
) {
|
||||||
|
let (options, tx) = self
|
||||||
|
.prompts
|
||||||
|
.borrow_mut()
|
||||||
|
.paths
|
||||||
|
.pop_front()
|
||||||
|
.expect("no pending paths prompt");
|
||||||
|
let selection = select_paths(&options);
|
||||||
|
if let Some(paths) = &selection
|
||||||
|
&& !options.multiple
|
||||||
|
&& paths.len() > 1
|
||||||
|
{
|
||||||
|
panic!(
|
||||||
|
"selected {} paths for a prompt that does not allow multiple selection",
|
||||||
|
paths.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
tx.send(Ok(selection)).ok();
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn did_prompt_for_paths(&self) -> bool {
|
||||||
|
!self.prompts.borrow().paths.is_empty()
|
||||||
|
}
|
||||||
|
|
||||||
#[track_caller]
|
#[track_caller]
|
||||||
pub(crate) fn simulate_prompt_answer(&self, response: &str) {
|
pub(crate) fn simulate_prompt_answer(&self, response: &str) {
|
||||||
let prompt = self
|
let prompt = self
|
||||||
|
|
@ -348,9 +380,11 @@ impl Platform for TestPlatform {
|
||||||
|
|
||||||
fn prompt_for_paths(
|
fn prompt_for_paths(
|
||||||
&self,
|
&self,
|
||||||
_options: crate::PathPromptOptions,
|
options: crate::PathPromptOptions,
|
||||||
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
) -> oneshot::Receiver<Result<Option<Vec<std::path::PathBuf>>>> {
|
||||||
unimplemented!()
|
let (tx, rx) = oneshot::channel();
|
||||||
|
self.prompts.borrow_mut().paths.push_back((options, tx));
|
||||||
|
rx
|
||||||
}
|
}
|
||||||
|
|
||||||
fn prompt_for_new_path(
|
fn prompt_for_new_path(
|
||||||
|
|
|
||||||
|
|
@ -3,10 +3,14 @@ use async_task::Runnable;
|
||||||
use chrono::{DateTime, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use scheduler::Instant;
|
use scheduler::Instant;
|
||||||
use scheduler::{Clock, Priority, Scheduler, SessionId, TestScheduler, Timer};
|
use scheduler::{
|
||||||
|
Clock, LocalExecutor, Priority, Scheduler, SessionId, Task, TestScheduler, Timer,
|
||||||
|
spawn_dedicated_thread,
|
||||||
|
};
|
||||||
#[cfg(not(target_family = "wasm"))]
|
#[cfg(not(target_family = "wasm"))]
|
||||||
use std::task::{Context, Poll};
|
use std::task::{Context, Poll};
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
future::Future,
|
future::Future,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::{
|
sync::{
|
||||||
|
|
@ -35,7 +39,17 @@ impl PlatformScheduler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn allocate_session_id(&self) -> SessionId {
|
pub fn foreground_executor(self: &Arc<Self>) -> LocalExecutor {
|
||||||
|
let session_id = self.next_session_id();
|
||||||
|
let scheduler = Arc::downgrade(self);
|
||||||
|
LocalExecutor::new(session_id, self.clone(), move |runnable| {
|
||||||
|
if let Some(scheduler) = scheduler.upgrade() {
|
||||||
|
scheduler.schedule_local(session_id, runnable);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
fn next_session_id(&self) -> SessionId {
|
||||||
SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst))
|
SessionId::new(self.next_session_id.fetch_add(1, Ordering::SeqCst))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -90,7 +104,7 @@ impl Scheduler for PlatformScheduler {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_foreground(&self, _session_id: SessionId, runnable: Runnable<RunnableMeta>) {
|
fn schedule_local(&self, _session_id: SessionId, runnable: Runnable<RunnableMeta>) {
|
||||||
self.dispatcher
|
self.dispatcher
|
||||||
.dispatch_on_main_thread(runnable, Priority::default());
|
.dispatch_on_main_thread(runnable, Priority::default());
|
||||||
}
|
}
|
||||||
|
|
@ -133,6 +147,21 @@ impl Scheduler for PlatformScheduler {
|
||||||
self.clock.clone()
|
self.clock.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn spawn_dedicated(
|
||||||
|
self: Arc<Self>,
|
||||||
|
f: Box<
|
||||||
|
dyn FnOnce(
|
||||||
|
LocalExecutor,
|
||||||
|
)
|
||||||
|
-> Pin<Box<dyn Future<Output = Box<dyn Any + Send + Sync>> + 'static>>
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
>,
|
||||||
|
) -> Task<Box<dyn Any + Send + Sync>> {
|
||||||
|
let session_id = self.next_session_id();
|
||||||
|
spawn_dedicated_thread(session_id, self, move |executor| f(executor))
|
||||||
|
}
|
||||||
|
|
||||||
fn as_test(&self) -> Option<&TestScheduler> {
|
fn as_test(&self) -> Option<&TestScheduler> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|
@ -152,3 +181,261 @@ impl Clock for PlatformClock {
|
||||||
self.dispatcher.now()
|
self.dispatcher.now()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(all(test, not(target_family = "wasm")))]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
use crate::{RunnableVariant, ThreadTaskTimings};
|
||||||
|
use scheduler::BackgroundExecutor;
|
||||||
|
use std::time::Instant as StdInstant;
|
||||||
|
|
||||||
|
// `spawn_dedicated` shouldn't touch the platform dispatcher at all;
|
||||||
|
// panicking on every method ensures the test catches it if it does.
|
||||||
|
struct SmokeDispatcher;
|
||||||
|
|
||||||
|
impl PlatformDispatcher for SmokeDispatcher {
|
||||||
|
fn get_all_timings(&self) -> Vec<ThreadTaskTimings> {
|
||||||
|
Vec::new()
|
||||||
|
}
|
||||||
|
fn get_current_thread_timings(&self) -> ThreadTaskTimings {
|
||||||
|
ThreadTaskTimings {
|
||||||
|
thread_name: None,
|
||||||
|
thread_id: std::thread::current().id(),
|
||||||
|
timings: Vec::new(),
|
||||||
|
total_pushed: 0,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
fn is_main_thread(&self) -> bool {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
fn dispatch(&self, _runnable: RunnableVariant, _priority: Priority) {
|
||||||
|
panic!("SmokeDispatcher should not be asked to dispatch in this test");
|
||||||
|
}
|
||||||
|
fn dispatch_on_main_thread(&self, _runnable: RunnableVariant, _priority: Priority) {
|
||||||
|
panic!("SmokeDispatcher does not implement a main thread");
|
||||||
|
}
|
||||||
|
fn dispatch_after(&self, _duration: Duration, _runnable: RunnableVariant) {
|
||||||
|
panic!("SmokeDispatcher does not implement timers");
|
||||||
|
}
|
||||||
|
fn spawn_realtime(&self, _f: Box<dyn FnOnce() + Send>) {
|
||||||
|
panic!("SmokeDispatcher does not implement realtime");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_runs_on_a_real_separate_thread() {
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
let started = StdInstant::now();
|
||||||
|
let task = background.spawn_dedicated(|_executor| async move {
|
||||||
|
// A genuine blocking syscall on the dedicated thread. If
|
||||||
|
// `spawn_dedicated` were running the future on any shared
|
||||||
|
// executor, this would stall that executor.
|
||||||
|
let thread_id_before = std::thread::current().id();
|
||||||
|
std::thread::sleep(Duration::from_millis(50));
|
||||||
|
let thread_id_after = std::thread::current().id();
|
||||||
|
assert_eq!(thread_id_before, thread_id_after);
|
||||||
|
(thread_id_before, "slept")
|
||||||
|
});
|
||||||
|
let (dedicated_thread_id, message) = futures::executor::block_on(task);
|
||||||
|
let elapsed = started.elapsed();
|
||||||
|
assert_eq!(message, "slept");
|
||||||
|
assert_ne!(
|
||||||
|
dedicated_thread_id,
|
||||||
|
std::thread::current().id(),
|
||||||
|
"dedicated future ran on the test thread"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
elapsed >= Duration::from_millis(40),
|
||||||
|
"expected the dedicated thread to genuinely sleep, elapsed = {:?}",
|
||||||
|
elapsed
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_returns_not_send_future_output() {
|
||||||
|
// The whole point of `spawn_dedicated` is that the future can be
|
||||||
|
// `!Send`. Constructing one with `Rc<RefCell<_>>` ensures the
|
||||||
|
// signature actually permits it.
|
||||||
|
use std::cell::RefCell;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
let task = background.spawn_dedicated(|_executor| async move {
|
||||||
|
let state = Rc::new(RefCell::new(0_i32));
|
||||||
|
for _ in 0..3 {
|
||||||
|
*state.borrow_mut() += 1;
|
||||||
|
}
|
||||||
|
*state.borrow()
|
||||||
|
});
|
||||||
|
let output = futures::executor::block_on(task);
|
||||||
|
assert_eq!(output, 3);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_dropping_task_cancels_future() {
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
|
||||||
|
let (started_tx, started_rx) = mpsc::channel::<()>();
|
||||||
|
let (after_park_tx, after_park_rx) = mpsc::channel::<()>();
|
||||||
|
let observed_post_await_write = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
let task = {
|
||||||
|
let observed_post_await_write = observed_post_await_write.clone();
|
||||||
|
background.spawn_dedicated(move |_executor| async move {
|
||||||
|
// Announce that the future is live on the dedicated thread.
|
||||||
|
started_tx
|
||||||
|
.send(())
|
||||||
|
.expect("started signal must be received");
|
||||||
|
// Park forever. Dropping the `Task` must cancel us here so
|
||||||
|
// the code below this `await` never runs.
|
||||||
|
futures::future::pending::<()>().await;
|
||||||
|
*observed_post_await_write.lock() = true;
|
||||||
|
after_park_tx
|
||||||
|
.send(())
|
||||||
|
.expect("after-park signal must be received");
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Wait until the dedicated future is actually parked at the await.
|
||||||
|
started_rx
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.expect("dedicated future failed to start");
|
||||||
|
|
||||||
|
// Drop the root Task: this must cancel the future.
|
||||||
|
drop(task);
|
||||||
|
|
||||||
|
// If cancellation works, the future never advances past `pending`,
|
||||||
|
// so this recv must time out.
|
||||||
|
assert!(
|
||||||
|
after_park_rx
|
||||||
|
.recv_timeout(Duration::from_millis(100))
|
||||||
|
.is_err(),
|
||||||
|
"dedicated future advanced past the await after its Task was dropped"
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
!*observed_post_await_write.lock(),
|
||||||
|
"dedicated future ran code past the cancellation point"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_thread_tears_down_after_work_completes() {
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
// Fires from `Drop` so we observe teardown of the dedicated future's
|
||||||
|
// captured state on whichever thread runs its destructor.
|
||||||
|
struct DropSignal {
|
||||||
|
tx: Option<mpsc::Sender<std::thread::ThreadId>>,
|
||||||
|
}
|
||||||
|
impl Drop for DropSignal {
|
||||||
|
fn drop(&mut self) {
|
||||||
|
if let Some(tx) = self.tx.take() {
|
||||||
|
let _ = tx.send(std::thread::current().id());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
let (started_tx, started_rx) = mpsc::channel::<std::thread::ThreadId>();
|
||||||
|
let (drop_tx, drop_rx) = mpsc::channel::<std::thread::ThreadId>();
|
||||||
|
|
||||||
|
let task = background.spawn_dedicated(move |_executor| async move {
|
||||||
|
// Captured by the future's state. When the future completes and
|
||||||
|
// its state is dropped on the dedicated thread, this guard's
|
||||||
|
// `Drop` fires and reports the thread id it ran on.
|
||||||
|
let _guard = DropSignal { tx: Some(drop_tx) };
|
||||||
|
started_tx
|
||||||
|
.send(std::thread::current().id())
|
||||||
|
.expect("started signal must be received");
|
||||||
|
// Future returns immediately. The dedicated thread should then
|
||||||
|
// drop the future (firing _guard), exit the recv loop, and exit.
|
||||||
|
});
|
||||||
|
|
||||||
|
let dedicated_thread_id = started_rx
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.expect("dedicated future failed to start");
|
||||||
|
assert_ne!(
|
||||||
|
dedicated_thread_id,
|
||||||
|
std::thread::current().id(),
|
||||||
|
"dedicated future ran on the test thread"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Drive the root task to completion so its body finishes.
|
||||||
|
futures::executor::block_on(task);
|
||||||
|
|
||||||
|
// The guard's drop runs from the dedicated thread as it tears down
|
||||||
|
// the future's captured state. If the executor/recv-loop were
|
||||||
|
// keeping the future alive past task completion, this would hang.
|
||||||
|
let drop_thread_id = drop_rx
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.expect("dedicated future's captured state was not dropped after task completion");
|
||||||
|
assert_eq!(
|
||||||
|
drop_thread_id, dedicated_thread_id,
|
||||||
|
"dedicated future's captured state must be dropped on the dedicated thread, not elsewhere"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn spawn_dedicated_detached_child_outlives_root() {
|
||||||
|
use std::sync::mpsc;
|
||||||
|
|
||||||
|
let background =
|
||||||
|
BackgroundExecutor::new(Arc::new(PlatformScheduler::new(Arc::new(SmokeDispatcher))));
|
||||||
|
|
||||||
|
// `gate_rx` lets the detached child park until the test explicitly
|
||||||
|
// releases it — after we've already observed the root completing.
|
||||||
|
let (gate_tx, gate_rx) = mpsc::channel::<()>();
|
||||||
|
let (child_done_tx, child_done_rx) = mpsc::channel::<std::thread::ThreadId>();
|
||||||
|
|
||||||
|
let task = background.spawn_dedicated(move |executor| async move {
|
||||||
|
executor
|
||||||
|
.spawn(async move {
|
||||||
|
// Blocking on `recv` is normally wrong inside an
|
||||||
|
// executor, but the dedicated thread is exclusive to
|
||||||
|
// this session, so blocking the only future on it is
|
||||||
|
// fine — this is the property `spawn_dedicated` is
|
||||||
|
// designed to provide.
|
||||||
|
gate_rx
|
||||||
|
.recv()
|
||||||
|
.expect("gate sender dropped before child resumed");
|
||||||
|
child_done_tx
|
||||||
|
.send(std::thread::current().id())
|
||||||
|
.expect("child_done receiver dropped");
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
// Root finishes here. The detached child must keep the
|
||||||
|
// dedicated thread alive until it completes.
|
||||||
|
});
|
||||||
|
|
||||||
|
futures::executor::block_on(task);
|
||||||
|
|
||||||
|
// Negative assertion: the child has not finished, because the gate
|
||||||
|
// hasn't been released yet.
|
||||||
|
assert!(
|
||||||
|
child_done_rx
|
||||||
|
.recv_timeout(Duration::from_millis(50))
|
||||||
|
.is_err(),
|
||||||
|
"detached child finished before being released"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Release the gate. The detached child should now complete on the
|
||||||
|
// dedicated thread.
|
||||||
|
gate_tx.send(()).expect("gate receiver dropped");
|
||||||
|
|
||||||
|
let child_thread_id = child_done_rx
|
||||||
|
.recv_timeout(Duration::from_secs(2))
|
||||||
|
.expect("detached child failed to complete after gate was released");
|
||||||
|
assert_ne!(
|
||||||
|
child_thread_id,
|
||||||
|
std::thread::current().id(),
|
||||||
|
"detached child ran on the test thread instead of the dedicated thread"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,10 +6,10 @@ use gpui::{AnyView, App, AsyncApp, Context, Entity, Task, TaskExt, Window};
|
||||||
use http_client::HttpClient;
|
use http_client::HttpClient;
|
||||||
use language_model::{
|
use language_model::{
|
||||||
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
ApiKeyState, AuthenticateError, EnvVar, IconOrSvg, LanguageModel, LanguageModelCompletionError,
|
||||||
LanguageModelCompletionEvent, LanguageModelId, LanguageModelName, LanguageModelProvider,
|
LanguageModelCompletionEvent, LanguageModelEffortLevel, LanguageModelId, LanguageModelName,
|
||||||
LanguageModelProviderId, LanguageModelProviderName, LanguageModelProviderState,
|
LanguageModelProvider, LanguageModelProviderId, LanguageModelProviderName,
|
||||||
LanguageModelRequest, LanguageModelToolChoice, LanguageModelToolSchemaFormat, RateLimiter,
|
LanguageModelProviderState, LanguageModelRequest, LanguageModelToolChoice,
|
||||||
env_var,
|
LanguageModelToolSchemaFormat, RateLimiter, env_var,
|
||||||
};
|
};
|
||||||
use open_ai::ResponseStreamEvent;
|
use open_ai::ResponseStreamEvent;
|
||||||
pub use settings::XaiAvailableModel as AvailableModel;
|
pub use settings::XaiAvailableModel as AvailableModel;
|
||||||
|
|
@ -255,6 +255,75 @@ impl XAiLanguageModel {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn x_ai_reasoning_efforts(model: &x_ai::Model) -> &'static [open_ai::ReasoningEffort] {
|
||||||
|
if model.supports_reasoning_effort() {
|
||||||
|
&[
|
||||||
|
open_ai::ReasoningEffort::None,
|
||||||
|
open_ai::ReasoningEffort::Low,
|
||||||
|
open_ai::ReasoningEffort::Medium,
|
||||||
|
open_ai::ReasoningEffort::High,
|
||||||
|
]
|
||||||
|
} else {
|
||||||
|
&[]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_thinking_reasoning_effort(model: &x_ai::Model) -> Option<open_ai::ReasoningEffort> {
|
||||||
|
if model.supports_reasoning_effort() {
|
||||||
|
Some(open_ai::ReasoningEffort::Low)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn reasoning_effort_for_request(
|
||||||
|
request: &LanguageModelRequest,
|
||||||
|
model: &x_ai::Model,
|
||||||
|
) -> Option<open_ai::ReasoningEffort> {
|
||||||
|
let supported_efforts = x_ai_reasoning_efforts(model);
|
||||||
|
if supported_efforts.is_empty() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
|
||||||
|
if request.thinking_allowed {
|
||||||
|
request
|
||||||
|
.thinking_effort
|
||||||
|
.as_deref()
|
||||||
|
.and_then(|effort| effort.parse::<open_ai::ReasoningEffort>().ok())
|
||||||
|
.filter(|effort| supported_efforts.contains(effort))
|
||||||
|
.filter(|effort| *effort != open_ai::ReasoningEffort::None)
|
||||||
|
.or_else(|| default_thinking_reasoning_effort(model))
|
||||||
|
} else if supported_efforts.contains(&open_ai::ReasoningEffort::None) {
|
||||||
|
Some(open_ai::ReasoningEffort::None)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_thinking_effort_levels(model: &x_ai::Model) -> Vec<LanguageModelEffortLevel> {
|
||||||
|
let default_effort = default_thinking_reasoning_effort(model);
|
||||||
|
x_ai_reasoning_efforts(model)
|
||||||
|
.iter()
|
||||||
|
.copied()
|
||||||
|
.filter_map(|effort| {
|
||||||
|
let (name, value) = match effort {
|
||||||
|
open_ai::ReasoningEffort::None => return None,
|
||||||
|
open_ai::ReasoningEffort::Minimal => ("Minimal", "minimal"),
|
||||||
|
open_ai::ReasoningEffort::Low => ("Low", "low"),
|
||||||
|
open_ai::ReasoningEffort::Medium => ("Medium", "medium"),
|
||||||
|
open_ai::ReasoningEffort::High => ("High", "high"),
|
||||||
|
open_ai::ReasoningEffort::XHigh => ("Extra High", "xhigh"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Some(LanguageModelEffortLevel {
|
||||||
|
name: name.into(),
|
||||||
|
value: value.into(),
|
||||||
|
is_default: Some(effort) == default_effort,
|
||||||
|
})
|
||||||
|
})
|
||||||
|
.collect()
|
||||||
|
}
|
||||||
|
|
||||||
impl LanguageModel for XAiLanguageModel {
|
impl LanguageModel for XAiLanguageModel {
|
||||||
fn id(&self) -> LanguageModelId {
|
fn id(&self) -> LanguageModelId {
|
||||||
self.id.clone()
|
self.id.clone()
|
||||||
|
|
@ -291,6 +360,15 @@ impl LanguageModel for XAiLanguageModel {
|
||||||
| LanguageModelToolChoice::None => true,
|
| LanguageModelToolChoice::None => true,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn supports_thinking(&self) -> bool {
|
||||||
|
self.model.supports_reasoning_effort()
|
||||||
|
}
|
||||||
|
|
||||||
|
fn supported_effort_levels(&self) -> Vec<LanguageModelEffortLevel> {
|
||||||
|
supported_thinking_effort_levels(&self.model)
|
||||||
|
}
|
||||||
|
|
||||||
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
fn tool_input_format(&self) -> LanguageModelToolSchemaFormat {
|
||||||
if self.model.requires_json_schema_subset() {
|
if self.model.requires_json_schema_subset() {
|
||||||
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
LanguageModelToolSchemaFormat::JsonSchemaSubset
|
||||||
|
|
@ -329,13 +407,14 @@ impl LanguageModel for XAiLanguageModel {
|
||||||
LanguageModelCompletionError,
|
LanguageModelCompletionError,
|
||||||
>,
|
>,
|
||||||
> {
|
> {
|
||||||
|
let reasoning_effort = reasoning_effort_for_request(&request, &self.model);
|
||||||
let request = crate::provider::open_ai::into_open_ai(
|
let request = crate::provider::open_ai::into_open_ai(
|
||||||
request,
|
request,
|
||||||
self.model.id(),
|
self.model.id(),
|
||||||
self.model.supports_parallel_tool_calls(),
|
self.model.supports_parallel_tool_calls(),
|
||||||
self.model.supports_prompt_cache_key(),
|
self.model.supports_prompt_cache_key(),
|
||||||
self.max_output_tokens(),
|
self.max_output_tokens(),
|
||||||
None,
|
reasoning_effort,
|
||||||
false,
|
false,
|
||||||
);
|
);
|
||||||
let completions = self.stream_completion(request, cx);
|
let completions = self.stream_completion(request, cx);
|
||||||
|
|
@ -428,6 +507,56 @@ impl ConfigurationView {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_43_supports_selectable_thinking_effort_levels() {
|
||||||
|
let effort_levels = supported_thinking_effort_levels(&x_ai::Model::Grok43);
|
||||||
|
let values = effort_levels
|
||||||
|
.iter()
|
||||||
|
.map(|level| level.value.as_ref())
|
||||||
|
.collect::<Vec<_>>();
|
||||||
|
|
||||||
|
assert_eq!(values, ["low", "medium", "high"]);
|
||||||
|
assert_eq!(
|
||||||
|
effort_levels
|
||||||
|
.iter()
|
||||||
|
.find(|level| level.is_default)
|
||||||
|
.map(|level| level.value.as_ref()),
|
||||||
|
Some("low")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_43_request_uses_selected_reasoning_effort() {
|
||||||
|
let request = LanguageModelRequest {
|
||||||
|
thinking_allowed: true,
|
||||||
|
thinking_effort: Some("high".to_string()),
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reasoning_effort_for_request(&request, &x_ai::Model::Grok43),
|
||||||
|
Some(open_ai::ReasoningEffort::High)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn grok_43_request_uses_none_when_thinking_is_disabled() {
|
||||||
|
let request = LanguageModelRequest {
|
||||||
|
thinking_allowed: false,
|
||||||
|
..Default::default()
|
||||||
|
};
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
reasoning_effort_for_request(&request, &x_ai::Model::Grok43),
|
||||||
|
Some(open_ai::ReasoningEffort::None)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
impl Render for ConfigurationView {
|
impl Render for ConfigurationView {
|
||||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
|
let env_var_set = self.state.read(cx).api_key_state.is_from_env_var();
|
||||||
|
|
|
||||||
|
|
@ -49,8 +49,27 @@ pub(crate) struct AudioStack {
|
||||||
|
|
||||||
impl AudioStack {
|
impl AudioStack {
|
||||||
pub(crate) fn new(executor: BackgroundExecutor) -> Self {
|
pub(crate) fn new(executor: BackgroundExecutor) -> Self {
|
||||||
|
// AGC2's `adaptive_digital` is what actually levels speech toward a target;
|
||||||
|
// the `gain_controller2.enabled` master switch alone leaves it off, which
|
||||||
|
// historically meant capture was effectively unleveled. Defaults match
|
||||||
|
// what Chrome/Meet ship with -- in particular `max_gain_db = 50` paired
|
||||||
|
// with `max_output_noise_level_dbfs = -50`, which lets the AGC reach
|
||||||
|
// very quiet talkers while the noise-level estimator backs off before
|
||||||
|
// boosting amplifies the noise floor.
|
||||||
let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
let apm = Arc::new(Mutex::new(apm::AudioProcessingModule::new(
|
||||||
true, true, true, true,
|
apm::AudioProcessingConfig {
|
||||||
|
echo_canceller_enabled: true,
|
||||||
|
gain_controller2: apm::GainController2Config {
|
||||||
|
enabled: true,
|
||||||
|
adaptive_digital: apm::AdaptiveDigitalConfig {
|
||||||
|
enabled: true,
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
high_pass_filter_enabled: true,
|
||||||
|
noise_suppression_enabled: true,
|
||||||
|
},
|
||||||
)));
|
)));
|
||||||
let mixer = Arc::new(Mutex::new(audio_mixer::AudioMixer::new()));
|
let mixer = Arc::new(Mutex::new(audio_mixer::AudioMixer::new()));
|
||||||
Self {
|
Self {
|
||||||
|
|
|
||||||
|
|
@ -21,7 +21,6 @@ db.workspace = true
|
||||||
|
|
||||||
fs.workspace = true
|
fs.workspace = true
|
||||||
futures.workspace = true
|
futures.workspace = true
|
||||||
fuzzy.workspace = true
|
|
||||||
gpui.workspace = true
|
gpui.workspace = true
|
||||||
handlebars.workspace = true
|
handlebars.workspace = true
|
||||||
heed.workspace = true
|
heed.workspace = true
|
||||||
|
|
@ -29,7 +28,6 @@ language.workspace = true
|
||||||
log.workspace = true
|
log.workspace = true
|
||||||
parking_lot.workspace = true
|
parking_lot.workspace = true
|
||||||
paths.workspace = true
|
paths.workspace = true
|
||||||
rope.workspace = true
|
|
||||||
serde.workspace = true
|
serde.workspace = true
|
||||||
serde_json.workspace = true
|
serde_json.workspace = true
|
||||||
strum.workspace = true
|
strum.workspace = true
|
||||||
|
|
|
||||||
|
|
@ -6,24 +6,17 @@ use chrono::{DateTime, Utc};
|
||||||
use collections::HashMap;
|
use collections::HashMap;
|
||||||
use futures::FutureExt as _;
|
use futures::FutureExt as _;
|
||||||
use futures::future::Shared;
|
use futures::future::Shared;
|
||||||
use fuzzy::StringMatchCandidate;
|
|
||||||
use gpui::{
|
use gpui::{App, AppContext, Entity, Global, ReadGlobal, SharedString, Task};
|
||||||
App, AppContext, Context, Entity, EventEmitter, Global, ReadGlobal, SharedString, Task,
|
|
||||||
};
|
|
||||||
use heed::{
|
use heed::{
|
||||||
Database, RoTxn,
|
Database, RoTxn,
|
||||||
types::{SerdeBincode, SerdeJson, Str},
|
types::{SerdeBincode, SerdeJson, Str},
|
||||||
};
|
};
|
||||||
use parking_lot::RwLock;
|
use parking_lot::RwLock;
|
||||||
pub use prompts::*;
|
pub use prompts::*;
|
||||||
use rope::Rope;
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::{
|
use std::{future::Future, path::PathBuf, sync::Arc};
|
||||||
cmp::Reverse,
|
|
||||||
future::Future,
|
|
||||||
path::PathBuf,
|
|
||||||
sync::{Arc, atomic::AtomicBool},
|
|
||||||
};
|
|
||||||
use strum::{EnumIter, IntoEnumIterator as _};
|
use strum::{EnumIter, IntoEnumIterator as _};
|
||||||
use text::LineEnding;
|
use text::LineEnding;
|
||||||
use util::ResultExt;
|
use util::ResultExt;
|
||||||
|
|
@ -122,15 +115,6 @@ impl PromptId {
|
||||||
pub fn is_built_in(&self) -> bool {
|
pub fn is_built_in(&self) -> bool {
|
||||||
matches!(self, Self::BuiltIn { .. })
|
matches!(self, Self::BuiltIn { .. })
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn can_edit(&self) -> bool {
|
|
||||||
match self {
|
|
||||||
Self::User { .. } => true,
|
|
||||||
Self::BuiltIn(builtin) => match builtin {
|
|
||||||
BuiltInPrompt::CommitMessage => true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<BuiltInPrompt> for PromptId {
|
impl From<BuiltInPrompt> for PromptId {
|
||||||
|
|
@ -173,14 +157,9 @@ impl std::fmt::Display for PromptId {
|
||||||
pub struct PromptStore {
|
pub struct PromptStore {
|
||||||
env: heed::Env,
|
env: heed::Env,
|
||||||
metadata_cache: RwLock<MetadataCache>,
|
metadata_cache: RwLock<MetadataCache>,
|
||||||
metadata: Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
|
|
||||||
bodies: Database<SerdeJson<PromptId>, Str>,
|
bodies: Database<SerdeJson<PromptId>, Str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct PromptsUpdatedEvent;
|
|
||||||
|
|
||||||
impl EventEmitter<PromptsUpdatedEvent> for PromptStore {}
|
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
struct MetadataCache {
|
struct MetadataCache {
|
||||||
metadata: Vec<PromptMetadata>,
|
metadata: Vec<PromptMetadata>,
|
||||||
|
|
@ -220,21 +199,6 @@ impl MetadataCache {
|
||||||
Ok(cache)
|
Ok(cache)
|
||||||
}
|
}
|
||||||
|
|
||||||
fn insert(&mut self, metadata: PromptMetadata) {
|
|
||||||
self.metadata_by_id.insert(metadata.id, metadata.clone());
|
|
||||||
if let Some(old_metadata) = self.metadata.iter_mut().find(|m| m.id == metadata.id) {
|
|
||||||
*old_metadata = metadata;
|
|
||||||
} else {
|
|
||||||
self.metadata.push(metadata);
|
|
||||||
}
|
|
||||||
self.sort();
|
|
||||||
}
|
|
||||||
|
|
||||||
fn remove(&mut self, id: PromptId) {
|
|
||||||
self.metadata.retain(|metadata| metadata.id != id);
|
|
||||||
self.metadata_by_id.remove(&id);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn sort(&mut self) {
|
fn sort(&mut self) {
|
||||||
self.metadata.sort_unstable_by(|a, b| {
|
self.metadata.sort_unstable_by(|a, b| {
|
||||||
a.title
|
a.title
|
||||||
|
|
@ -275,7 +239,6 @@ impl PromptStore {
|
||||||
Ok(PromptStore {
|
Ok(PromptStore {
|
||||||
env: db_env,
|
env: db_env,
|
||||||
metadata_cache: RwLock::new(metadata_cache),
|
metadata_cache: RwLock::new(metadata_cache),
|
||||||
metadata,
|
|
||||||
bodies,
|
bodies,
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
@ -363,219 +326,6 @@ impl PromptStore {
|
||||||
pub fn all_prompt_metadata(&self) -> Vec<PromptMetadata> {
|
pub fn all_prompt_metadata(&self) -> Vec<PromptMetadata> {
|
||||||
self.metadata_cache.read().metadata.clone()
|
self.metadata_cache.read().metadata.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn default_prompt_metadata(&self) -> Vec<PromptMetadata> {
|
|
||||||
return self
|
|
||||||
.metadata_cache
|
|
||||||
.read()
|
|
||||||
.metadata
|
|
||||||
.iter()
|
|
||||||
.filter(|metadata| metadata.default)
|
|
||||||
.cloned()
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn delete(&self, id: PromptId, cx: &Context<Self>) -> Task<Result<()>> {
|
|
||||||
self.metadata_cache.write().remove(id);
|
|
||||||
|
|
||||||
let db_connection = self.env.clone();
|
|
||||||
let bodies = self.bodies;
|
|
||||||
let metadata = self.metadata;
|
|
||||||
|
|
||||||
let task = cx.background_spawn(async move {
|
|
||||||
let mut txn = db_connection.write_txn()?;
|
|
||||||
|
|
||||||
metadata.delete(&mut txn, &id)?;
|
|
||||||
bodies.delete(&mut txn, &id)?;
|
|
||||||
|
|
||||||
if let PromptId::User { uuid } = id {
|
|
||||||
let prompt_id_v1 = PromptIdV1::from(uuid);
|
|
||||||
|
|
||||||
if let Some(metadata_v1_db) = db_connection
|
|
||||||
.open_database::<SerdeBincode<PromptIdV1>, SerdeBincode<()>>(
|
|
||||||
&txn,
|
|
||||||
Some("metadata"),
|
|
||||||
)?
|
|
||||||
{
|
|
||||||
metadata_v1_db.delete(&mut txn, &prompt_id_v1)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(bodies_v1_db) = db_connection
|
|
||||||
.open_database::<SerdeBincode<PromptIdV1>, SerdeBincode<()>>(
|
|
||||||
&txn,
|
|
||||||
Some("bodies"),
|
|
||||||
)?
|
|
||||||
{
|
|
||||||
bodies_v1_db.delete(&mut txn, &prompt_id_v1)?;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
txn.commit()?;
|
|
||||||
anyhow::Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
task.await?;
|
|
||||||
this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok();
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
|
|
||||||
self.metadata_cache.read().metadata_by_id.get(&id).cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn first(&self) -> Option<PromptMetadata> {
|
|
||||||
self.metadata_cache.read().metadata.first().cloned()
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn id_for_title(&self, title: &str) -> Option<PromptId> {
|
|
||||||
let metadata_cache = self.metadata_cache.read();
|
|
||||||
let metadata = metadata_cache
|
|
||||||
.metadata
|
|
||||||
.iter()
|
|
||||||
.find(|metadata| metadata.title.as_deref() == Some(title))?;
|
|
||||||
Some(metadata.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn search(
|
|
||||||
&self,
|
|
||||||
query: String,
|
|
||||||
cancellation_flag: Arc<AtomicBool>,
|
|
||||||
cx: &App,
|
|
||||||
) -> Task<Vec<PromptMetadata>> {
|
|
||||||
let cached_metadata = self.metadata_cache.read().metadata.clone();
|
|
||||||
let executor = cx.background_executor().clone();
|
|
||||||
cx.background_spawn(async move {
|
|
||||||
let mut matches = if query.is_empty() {
|
|
||||||
cached_metadata
|
|
||||||
} else {
|
|
||||||
let candidates = cached_metadata
|
|
||||||
.iter()
|
|
||||||
.enumerate()
|
|
||||||
.filter_map(|(ix, metadata)| {
|
|
||||||
Some(StringMatchCandidate::new(ix, metadata.title.as_ref()?))
|
|
||||||
})
|
|
||||||
.collect::<Vec<_>>();
|
|
||||||
let matches = fuzzy::match_strings(
|
|
||||||
&candidates,
|
|
||||||
&query,
|
|
||||||
false,
|
|
||||||
true,
|
|
||||||
100,
|
|
||||||
&cancellation_flag,
|
|
||||||
executor,
|
|
||||||
)
|
|
||||||
.await;
|
|
||||||
matches
|
|
||||||
.into_iter()
|
|
||||||
.map(|mat| cached_metadata[mat.candidate_id].clone())
|
|
||||||
.collect()
|
|
||||||
};
|
|
||||||
matches.sort_by_key(|metadata| Reverse(metadata.default));
|
|
||||||
matches
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save(
|
|
||||||
&self,
|
|
||||||
id: PromptId,
|
|
||||||
title: Option<SharedString>,
|
|
||||||
default: bool,
|
|
||||||
body: Rope,
|
|
||||||
cx: &Context<Self>,
|
|
||||||
) -> Task<Result<()>> {
|
|
||||||
if !id.can_edit() {
|
|
||||||
return Task::ready(Err(anyhow!("this prompt cannot be edited")));
|
|
||||||
}
|
|
||||||
|
|
||||||
let body = body.to_string();
|
|
||||||
let is_default_content = id
|
|
||||||
.as_built_in()
|
|
||||||
.is_some_and(|builtin| body.trim() == builtin.default_content().trim());
|
|
||||||
|
|
||||||
let metadata = if let Some(builtin) = id.as_built_in() {
|
|
||||||
PromptMetadata::builtin(builtin)
|
|
||||||
} else {
|
|
||||||
PromptMetadata {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
default,
|
|
||||||
saved_at: Utc::now(),
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
self.metadata_cache.write().insert(metadata.clone());
|
|
||||||
|
|
||||||
let db_connection = self.env.clone();
|
|
||||||
let bodies = self.bodies;
|
|
||||||
let metadata_db = self.metadata;
|
|
||||||
|
|
||||||
let task = cx.background_spawn(async move {
|
|
||||||
let mut txn = db_connection.write_txn()?;
|
|
||||||
|
|
||||||
if is_default_content {
|
|
||||||
metadata_db.delete(&mut txn, &id)?;
|
|
||||||
bodies.delete(&mut txn, &id)?;
|
|
||||||
} else {
|
|
||||||
metadata_db.put(&mut txn, &id, &metadata)?;
|
|
||||||
bodies.put(&mut txn, &id, &body)?;
|
|
||||||
}
|
|
||||||
|
|
||||||
txn.commit()?;
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
task.await?;
|
|
||||||
this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok();
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn save_metadata(
|
|
||||||
&self,
|
|
||||||
id: PromptId,
|
|
||||||
mut title: Option<SharedString>,
|
|
||||||
default: bool,
|
|
||||||
cx: &Context<Self>,
|
|
||||||
) -> Task<Result<()>> {
|
|
||||||
let mut cache = self.metadata_cache.write();
|
|
||||||
|
|
||||||
if !id.can_edit() {
|
|
||||||
title = cache
|
|
||||||
.metadata_by_id
|
|
||||||
.get(&id)
|
|
||||||
.and_then(|metadata| metadata.title.clone());
|
|
||||||
}
|
|
||||||
|
|
||||||
let prompt_metadata = PromptMetadata {
|
|
||||||
id,
|
|
||||||
title,
|
|
||||||
default,
|
|
||||||
saved_at: Utc::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
cache.insert(prompt_metadata.clone());
|
|
||||||
|
|
||||||
let db_connection = self.env.clone();
|
|
||||||
let metadata = self.metadata;
|
|
||||||
|
|
||||||
let task = cx.background_spawn(async move {
|
|
||||||
let mut txn = db_connection.write_txn()?;
|
|
||||||
metadata.put(&mut txn, &id, &prompt_metadata)?;
|
|
||||||
txn.commit()?;
|
|
||||||
|
|
||||||
anyhow::Ok(())
|
|
||||||
});
|
|
||||||
|
|
||||||
cx.spawn(async move |this, cx| {
|
|
||||||
task.await?;
|
|
||||||
this.update(cx, |_, cx| cx.emit(PromptsUpdatedEvent)).ok();
|
|
||||||
anyhow::Ok(())
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Deprecated: Legacy V1 prompt ID format, used only for migrating data from old databases. Use `PromptId` instead.
|
/// Deprecated: Legacy V1 prompt ID format, used only for migrating data from old databases. Use `PromptId` instead.
|
||||||
|
|
@ -608,7 +358,7 @@ mod tests {
|
||||||
use gpui::TestAppContext;
|
use gpui::TestAppContext;
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_built_in_prompt_load_save(cx: &mut TestAppContext) {
|
async fn test_built_in_prompt_load(cx: &mut TestAppContext) {
|
||||||
cx.executor().allow_parking();
|
cx.executor().allow_parking();
|
||||||
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
let temp_dir = tempfile::tempdir().unwrap();
|
||||||
|
|
@ -632,265 +382,14 @@ mod tests {
|
||||||
"Loading a built-in prompt not in DB should return default content"
|
"Loading a built-in prompt not in DB should return default content"
|
||||||
);
|
);
|
||||||
|
|
||||||
let metadata = store.read_with(cx, |store, _| store.metadata(commit_message_id));
|
|
||||||
assert!(
|
|
||||||
metadata.is_some(),
|
|
||||||
"Built-in prompt should always have metadata"
|
|
||||||
);
|
|
||||||
assert!(
|
assert!(
|
||||||
store.read_with(cx, |store, _| {
|
store.read_with(cx, |store, _| {
|
||||||
store
|
store
|
||||||
.metadata_cache
|
.all_prompt_metadata()
|
||||||
.read()
|
.iter()
|
||||||
.metadata_by_id
|
.any(|metadata| metadata.id == commit_message_id)
|
||||||
.contains_key(&commit_message_id)
|
|
||||||
}),
|
}),
|
||||||
"Built-in prompt should always be in cache"
|
"Built-in prompt should always be in cache"
|
||||||
);
|
);
|
||||||
|
|
||||||
let custom_content = "Custom commit message prompt";
|
|
||||||
store
|
|
||||||
.update(cx, |store, cx| {
|
|
||||||
store.save(
|
|
||||||
commit_message_id,
|
|
||||||
Some("Commit message".into()),
|
|
||||||
false,
|
|
||||||
Rope::from(custom_content),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let loaded_custom = store
|
|
||||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
assert_eq!(
|
|
||||||
loaded_custom.trim(),
|
|
||||||
custom_content.trim(),
|
|
||||||
"Custom content should be loaded after saving"
|
|
||||||
);
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
store
|
|
||||||
.read_with(cx, |store, _| store.metadata(commit_message_id))
|
|
||||||
.is_some(),
|
|
||||||
"Built-in prompt should have metadata after customization"
|
|
||||||
);
|
|
||||||
|
|
||||||
store
|
|
||||||
.update(cx, |store, cx| {
|
|
||||||
store.save(
|
|
||||||
commit_message_id,
|
|
||||||
Some("Commit message".into()),
|
|
||||||
false,
|
|
||||||
Rope::from(BuiltInPrompt::CommitMessage.default_content()),
|
|
||||||
cx,
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
let metadata_after_reset =
|
|
||||||
store.read_with(cx, |store, _| store.metadata(commit_message_id));
|
|
||||||
assert!(
|
|
||||||
metadata_after_reset.is_some(),
|
|
||||||
"Built-in prompt should still have metadata after reset"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
metadata_after_reset
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|m| m.title.as_ref().map(|t| t.as_ref())),
|
|
||||||
Some("Commit message"),
|
|
||||||
"Built-in prompt should have default title after reset"
|
|
||||||
);
|
|
||||||
|
|
||||||
let loaded_after_reset = store
|
|
||||||
.update(cx, |store, cx| store.load(commit_message_id, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let mut expected_content_after_reset =
|
|
||||||
BuiltInPrompt::CommitMessage.default_content().to_string();
|
|
||||||
LineEnding::normalize(&mut expected_content_after_reset);
|
|
||||||
assert_eq!(
|
|
||||||
loaded_after_reset.trim(),
|
|
||||||
expected_content_after_reset.trim(),
|
|
||||||
"Content should be back to default after saving default content"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Test that the prompt store initializes successfully even when the database
|
|
||||||
/// contains records with incompatible/undecodable PromptId keys (e.g., from
|
|
||||||
/// a different branch that used a different serialization format).
|
|
||||||
///
|
|
||||||
/// This is a regression test for the "fail-open" behavior: we should skip
|
|
||||||
/// bad records rather than failing the entire store initialization.
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_prompt_store_handles_incompatible_db_records(cx: &mut TestAppContext) {
|
|
||||||
cx.executor().allow_parking();
|
|
||||||
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
|
||||||
let db_path = temp_dir.path().join("prompts-db-with-bad-records");
|
|
||||||
std::fs::create_dir_all(&db_path).unwrap();
|
|
||||||
|
|
||||||
// First, create the DB and write an incompatible record directly.
|
|
||||||
// We simulate a record written by a different branch that used
|
|
||||||
// `{"kind":"CommitMessage"}` instead of `{"kind":"BuiltIn", ...}`.
|
|
||||||
{
|
|
||||||
let db_env = unsafe {
|
|
||||||
heed::EnvOpenOptions::new()
|
|
||||||
.map_size(1024 * 1024 * 1024)
|
|
||||||
.max_dbs(4)
|
|
||||||
.open(&db_path)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut txn = db_env.write_txn().unwrap();
|
|
||||||
// Create the metadata.v2 database with raw bytes so we can write
|
|
||||||
// an incompatible key format.
|
|
||||||
let metadata_db: Database<heed::types::Bytes, heed::types::Bytes> = db_env
|
|
||||||
.create_database(&mut txn, Some("metadata.v2"))
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Write an incompatible PromptId key: `{"kind":"CommitMessage"}`
|
|
||||||
// This is the old/branch format that current code can't decode.
|
|
||||||
let bad_key = br#"{"kind":"CommitMessage"}"#;
|
|
||||||
let dummy_metadata = br#"{"id":{"kind":"CommitMessage"},"title":"Bad Record","default":false,"saved_at":"2024-01-01T00:00:00Z"}"#;
|
|
||||||
metadata_db.put(&mut txn, bad_key, dummy_metadata).unwrap();
|
|
||||||
|
|
||||||
// Also write a valid record to ensure we can still read good data.
|
|
||||||
let good_key = br#"{"kind":"User","uuid":"550e8400-e29b-41d4-a716-446655440000"}"#;
|
|
||||||
let good_metadata = br#"{"id":{"kind":"User","uuid":"550e8400-e29b-41d4-a716-446655440000"},"title":"Good Record","default":false,"saved_at":"2024-01-01T00:00:00Z"}"#;
|
|
||||||
metadata_db.put(&mut txn, good_key, good_metadata).unwrap();
|
|
||||||
|
|
||||||
txn.commit().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Now try to create a PromptStore from this DB.
|
|
||||||
// With fail-open behavior, this should succeed and skip the bad record.
|
|
||||||
// Without fail-open, this would return an error.
|
|
||||||
let store_result = cx.update(|cx| PromptStore::new(db_path, cx)).await;
|
|
||||||
|
|
||||||
assert!(
|
|
||||||
store_result.is_ok(),
|
|
||||||
"PromptStore should initialize successfully even with incompatible DB records. \
|
|
||||||
Got error: {:?}",
|
|
||||||
store_result.err()
|
|
||||||
);
|
|
||||||
|
|
||||||
let store = cx.new(|_cx| store_result.unwrap());
|
|
||||||
|
|
||||||
// Verify the good record was loaded.
|
|
||||||
let good_id = PromptId::User {
|
|
||||||
uuid: UserPromptId("550e8400-e29b-41d4-a716-446655440000".parse().unwrap()),
|
|
||||||
};
|
|
||||||
let metadata = store.read_with(cx, |store, _| store.metadata(good_id));
|
|
||||||
assert!(
|
|
||||||
metadata.is_some(),
|
|
||||||
"Valid records should still be loaded after skipping bad ones"
|
|
||||||
);
|
|
||||||
assert_eq!(
|
|
||||||
metadata
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|m| m.title.as_ref().map(|t| t.as_ref())),
|
|
||||||
Some("Good Record"),
|
|
||||||
"Valid record should have correct title"
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[gpui::test]
|
|
||||||
async fn test_deleted_prompt_does_not_reappear_after_migration(cx: &mut TestAppContext) {
|
|
||||||
cx.executor().allow_parking();
|
|
||||||
|
|
||||||
let temp_dir = tempfile::tempdir().unwrap();
|
|
||||||
let db_path = temp_dir.path().join("prompts-db-v1-migration");
|
|
||||||
std::fs::create_dir_all(&db_path).unwrap();
|
|
||||||
|
|
||||||
let prompt_uuid: Uuid = "550e8400-e29b-41d4-a716-446655440001".parse().unwrap();
|
|
||||||
let prompt_id_v1 = PromptIdV1(prompt_uuid);
|
|
||||||
let prompt_id_v2 = PromptId::User {
|
|
||||||
uuid: UserPromptId(prompt_uuid),
|
|
||||||
};
|
|
||||||
|
|
||||||
// Create V1 database with a prompt
|
|
||||||
{
|
|
||||||
let db_env = unsafe {
|
|
||||||
heed::EnvOpenOptions::new()
|
|
||||||
.map_size(1024 * 1024 * 1024)
|
|
||||||
.max_dbs(4)
|
|
||||||
.open(&db_path)
|
|
||||||
.unwrap()
|
|
||||||
};
|
|
||||||
|
|
||||||
let mut txn = db_env.write_txn().unwrap();
|
|
||||||
|
|
||||||
let metadata_v1_db: Database<SerdeBincode<PromptIdV1>, SerdeBincode<PromptMetadataV1>> =
|
|
||||||
db_env.create_database(&mut txn, Some("metadata")).unwrap();
|
|
||||||
|
|
||||||
let bodies_v1_db: Database<SerdeBincode<PromptIdV1>, SerdeBincode<String>> =
|
|
||||||
db_env.create_database(&mut txn, Some("bodies")).unwrap();
|
|
||||||
|
|
||||||
let metadata_v1 = PromptMetadataV1 {
|
|
||||||
id: prompt_id_v1.clone(),
|
|
||||||
title: Some("V1 Prompt".into()),
|
|
||||||
default: false,
|
|
||||||
saved_at: Utc::now(),
|
|
||||||
};
|
|
||||||
|
|
||||||
metadata_v1_db
|
|
||||||
.put(&mut txn, &prompt_id_v1, &metadata_v1)
|
|
||||||
.unwrap();
|
|
||||||
bodies_v1_db
|
|
||||||
.put(&mut txn, &prompt_id_v1, &"V1 prompt body".to_string())
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
txn.commit().unwrap();
|
|
||||||
}
|
|
||||||
|
|
||||||
// Migrate V1 to V2 by creating PromptStore
|
|
||||||
let store = cx
|
|
||||||
.update(|cx| PromptStore::new(db_path.clone(), cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
let store = cx.new(|_cx| store);
|
|
||||||
|
|
||||||
// Verify the prompt was migrated
|
|
||||||
let metadata = store.read_with(cx, |store, _| store.metadata(prompt_id_v2));
|
|
||||||
assert!(metadata.is_some(), "V1 prompt should be migrated to V2");
|
|
||||||
assert_eq!(
|
|
||||||
metadata
|
|
||||||
.as_ref()
|
|
||||||
.and_then(|m| m.title.as_ref().map(|t| t.as_ref())),
|
|
||||||
Some("V1 Prompt"),
|
|
||||||
"Migrated prompt should have correct title"
|
|
||||||
);
|
|
||||||
|
|
||||||
// Delete the prompt
|
|
||||||
store
|
|
||||||
.update(cx, |store, cx| store.delete(prompt_id_v2, cx))
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
// Verify prompt is deleted
|
|
||||||
let metadata_after_delete = store.read_with(cx, |store, _| store.metadata(prompt_id_v2));
|
|
||||||
assert!(
|
|
||||||
metadata_after_delete.is_none(),
|
|
||||||
"Prompt should be deleted from V2"
|
|
||||||
);
|
|
||||||
|
|
||||||
drop(store);
|
|
||||||
|
|
||||||
// "Restart" by creating a new PromptStore from the same path
|
|
||||||
let store_after_restart = cx.update(|cx| PromptStore::new(db_path, cx)).await.unwrap();
|
|
||||||
let store_after_restart = cx.new(|_cx| store_after_restart);
|
|
||||||
|
|
||||||
// Test the prompt does not reappear
|
|
||||||
let metadata_after_restart =
|
|
||||||
store_after_restart.read_with(cx, |store, _| store.metadata(prompt_id_v2));
|
|
||||||
assert!(
|
|
||||||
metadata_after_restart.is_none(),
|
|
||||||
"Deleted prompt should NOT reappear after restart/migration"
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1555,7 +1555,7 @@ type ResponseChannels = Mutex<HashMap<MessageId, oneshot::Sender<(Envelope, ones
|
||||||
type StreamResponseChannels =
|
type StreamResponseChannels =
|
||||||
Arc<Mutex<HashMap<MessageId, UnboundedSender<(Result<Envelope>, oneshot::Sender<()>)>>>>;
|
Arc<Mutex<HashMap<MessageId, UnboundedSender<(Result<Envelope>, oneshot::Sender<()>)>>>>;
|
||||||
|
|
||||||
struct Signal<T> {
|
struct Signal<T: 'static> {
|
||||||
tx: Mutex<Option<oneshot::Sender<T>>>,
|
tx: Mutex<Option<oneshot::Sender<T>>>,
|
||||||
rx: Shared<Task<Option<T>>>,
|
rx: Shared<Task<Option<T>>>,
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,7 @@
|
||||||
use crate::{Instant, Priority, RunnableMeta, Scheduler, SessionId, Timer};
|
use crate::{Instant, Priority, RunnableMeta, Scheduler, SessionId, Timer};
|
||||||
|
use async_task::Runnable;
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
future::Future,
|
future::Future,
|
||||||
marker::PhantomData,
|
marker::PhantomData,
|
||||||
mem::ManuallyDrop,
|
mem::ManuallyDrop,
|
||||||
|
|
@ -12,18 +14,39 @@ use std::{
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/// A `!Send` executor pinned to a single session. Tasks spawned on it run in
|
||||||
|
/// order on whichever thread drains the dispatch destination supplied at
|
||||||
|
/// construction time — typically the main thread for the default session, or
|
||||||
|
/// a dedicated OS thread for sessions created by `spawn_dedicated_thread`.
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ForegroundExecutor {
|
pub struct LocalExecutor {
|
||||||
session_id: SessionId,
|
session_id: SessionId,
|
||||||
scheduler: Arc<dyn Scheduler>,
|
scheduler: Arc<dyn Scheduler>,
|
||||||
|
// Spawned tasks' schedule callbacks each hold an `Arc` clone of this
|
||||||
|
// closure, so the destination it captures stays alive as long as work
|
||||||
|
// could still land on it.
|
||||||
|
dispatch: Arc<dyn Fn(Runnable<RunnableMeta>) + Send + Sync>,
|
||||||
not_send: PhantomData<Rc<()>>,
|
not_send: PhantomData<Rc<()>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ForegroundExecutor {
|
impl LocalExecutor {
|
||||||
pub fn new(session_id: SessionId, scheduler: Arc<dyn Scheduler>) -> Self {
|
/// Constructs a local executor that runs spawned tasks by sending their
|
||||||
|
/// runnables through `dispatch`. The `scheduler` is retained for access to
|
||||||
|
/// clocks, timers, and other scheduler-level services.
|
||||||
|
///
|
||||||
|
/// For the common case of routing runnables through
|
||||||
|
/// `Scheduler::schedule_local`, callers pass a closure that does exactly
|
||||||
|
/// that. `spawn_dedicated_thread` instead passes a closure that sends to
|
||||||
|
/// the dedicated thread's channel.
|
||||||
|
pub fn new(
|
||||||
|
session_id: SessionId,
|
||||||
|
scheduler: Arc<dyn Scheduler>,
|
||||||
|
dispatch: impl Fn(Runnable<RunnableMeta>) + Send + Sync + 'static,
|
||||||
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
session_id,
|
session_id,
|
||||||
scheduler,
|
scheduler,
|
||||||
|
dispatch: Arc::new(dispatch),
|
||||||
not_send: PhantomData,
|
not_send: PhantomData,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -42,16 +65,11 @@ impl ForegroundExecutor {
|
||||||
F: Future + 'static,
|
F: Future + 'static,
|
||||||
F::Output: 'static,
|
F::Output: 'static,
|
||||||
{
|
{
|
||||||
let session_id = self.session_id;
|
let dispatch = self.dispatch.clone();
|
||||||
let scheduler = Arc::downgrade(&self.scheduler);
|
|
||||||
let location = Location::caller();
|
let location = Location::caller();
|
||||||
let (runnable, task) = spawn_local_with_source_location(
|
let (runnable, task) = spawn_local_with_source_location(
|
||||||
future,
|
future,
|
||||||
move |runnable| {
|
move |runnable| dispatch(runnable),
|
||||||
if let Some(scheduler) = scheduler.upgrade() {
|
|
||||||
scheduler.schedule_foreground(session_id, runnable);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
RunnableMeta { location },
|
RunnableMeta { location },
|
||||||
);
|
);
|
||||||
runnable.schedule();
|
runnable.schedule();
|
||||||
|
|
@ -110,6 +128,48 @@ impl ForegroundExecutor {
|
||||||
pub fn now(&self) -> Instant {
|
pub fn now(&self) -> Instant {
|
||||||
self.scheduler.clock().now()
|
self.scheduler.clock().now()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`].
|
||||||
|
/// The closure runs on a new OS thread under `PlatformScheduler`, or on
|
||||||
|
/// the test scheduler's loop under `TestScheduler`.
|
||||||
|
///
|
||||||
|
/// The returned `Task` represents the dedicated work: dropping it cancels
|
||||||
|
/// the dedicated closure, `.await`ing it yields the closure's return
|
||||||
|
/// value, `.detach()`ing it lets the dedicated work run independently of
|
||||||
|
/// the caller.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn spawn_dedicated<F, Fut>(&self, f: F) -> Task<Fut::Output>
|
||||||
|
where
|
||||||
|
F: FnOnce(LocalExecutor) -> Fut + Send + 'static,
|
||||||
|
Fut: Future + 'static,
|
||||||
|
Fut::Output: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.scheduler
|
||||||
|
.clone()
|
||||||
|
.spawn_dedicated(box_dedicated(f))
|
||||||
|
.downcast::<Fut::Output>()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Boxes the user-supplied dedicated closure into the type-erased shape
|
||||||
|
/// expected by [`Scheduler::spawn_dedicated`]. The user's `Fut::Output` is
|
||||||
|
/// boxed as `Box<dyn Any + Send + Sync>` on the dedicated side and downcast
|
||||||
|
/// back to `Fut::Output` by [`Task::downcast`] in the wrapper.
|
||||||
|
fn box_dedicated<F, Fut>(
|
||||||
|
f: F,
|
||||||
|
) -> Box<
|
||||||
|
dyn FnOnce(LocalExecutor) -> Pin<Box<dyn Future<Output = Box<dyn Any + Send + Sync>> + 'static>>
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
>
|
||||||
|
where
|
||||||
|
F: FnOnce(LocalExecutor) -> Fut + Send + 'static,
|
||||||
|
Fut: Future + 'static,
|
||||||
|
Fut::Output: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Box::new(move |executor| {
|
||||||
|
Box::pin(async move { Box::new(f(executor).await) as Box<dyn Any + Send + Sync> })
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
|
|
@ -193,6 +253,27 @@ impl BackgroundExecutor {
|
||||||
pub fn scheduler(&self) -> &Arc<dyn Scheduler> {
|
pub fn scheduler(&self) -> &Arc<dyn Scheduler> {
|
||||||
&self.scheduler
|
&self.scheduler
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`].
|
||||||
|
/// The closure runs on a new OS thread under `PlatformScheduler`, or on
|
||||||
|
/// the test scheduler's loop under `TestScheduler`.
|
||||||
|
///
|
||||||
|
/// The returned `Task` represents the dedicated work: dropping it cancels
|
||||||
|
/// the dedicated closure, `.await`ing it yields the closure's return
|
||||||
|
/// value, `.detach()`ing it lets the dedicated work run independently of
|
||||||
|
/// the caller.
|
||||||
|
#[track_caller]
|
||||||
|
pub fn spawn_dedicated<F, Fut>(&self, f: F) -> Task<Fut::Output>
|
||||||
|
where
|
||||||
|
F: FnOnce(LocalExecutor) -> Fut + Send + 'static,
|
||||||
|
Fut: Future + 'static,
|
||||||
|
Fut::Output: Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
self.scheduler
|
||||||
|
.clone()
|
||||||
|
.spawn_dedicated(box_dedicated(f))
|
||||||
|
.downcast::<Fut::Output>()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Task is a primitive that allows work to happen in the background.
|
/// Task is a primitive that allows work to happen in the background.
|
||||||
|
|
@ -202,16 +283,22 @@ impl BackgroundExecutor {
|
||||||
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
|
/// If you drop a task it will be cancelled immediately. Calling [`Task::detach`] allows
|
||||||
/// the task to continue running, but with no way to return a value.
|
/// the task to continue running, but with no way to return a value.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
#[derive(Debug)]
|
|
||||||
pub struct Task<T>(TaskState<T>);
|
pub struct Task<T>(TaskState<T>);
|
||||||
|
|
||||||
#[derive(Debug)]
|
|
||||||
enum TaskState<T> {
|
enum TaskState<T> {
|
||||||
/// A task that is ready to return a value
|
/// A task that is ready to return a value
|
||||||
Ready(Option<T>),
|
Ready(Option<T>),
|
||||||
|
|
||||||
/// A task that is currently running.
|
/// A task that is currently running.
|
||||||
Spawned(async_task::Task<T, RunnableMeta>),
|
Spawned(async_task::Task<T, RunnableMeta>),
|
||||||
|
|
||||||
|
/// A typed view of a [`Task<Box<dyn Any + Send + Sync>>`] obtained via
|
||||||
|
/// [`Task::downcast`]. The inner task drives the actual work; the
|
||||||
|
/// downcast layer just unwraps the `Box<dyn Any + Send + Sync>` on poll.
|
||||||
|
Downcast {
|
||||||
|
inner: Box<Task<Box<dyn Any + Send + Sync>>>,
|
||||||
|
marker: PhantomData<fn() -> T>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Task<T> {
|
impl<T> Task<T> {
|
||||||
|
|
@ -229,6 +316,7 @@ impl<T> Task<T> {
|
||||||
match &self.0 {
|
match &self.0 {
|
||||||
TaskState::Ready(_) => true,
|
TaskState::Ready(_) => true,
|
||||||
TaskState::Spawned(task) => task.is_finished(),
|
TaskState::Spawned(task) => task.is_finished(),
|
||||||
|
TaskState::Downcast { inner, .. } => inner.is_ready(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -237,6 +325,7 @@ impl<T> Task<T> {
|
||||||
match self {
|
match self {
|
||||||
Task(TaskState::Ready(_)) => {}
|
Task(TaskState::Ready(_)) => {}
|
||||||
Task(TaskState::Spawned(task)) => task.detach(),
|
Task(TaskState::Spawned(task)) => task.detach(),
|
||||||
|
Task(TaskState::Downcast { inner, .. }) => inner.detach(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -245,10 +334,43 @@ impl<T> Task<T> {
|
||||||
FallibleTask(match self.0 {
|
FallibleTask(match self.0 {
|
||||||
TaskState::Ready(val) => FallibleTaskState::Ready(val),
|
TaskState::Ready(val) => FallibleTaskState::Ready(val),
|
||||||
TaskState::Spawned(task) => FallibleTaskState::Spawned(task.fallible()),
|
TaskState::Spawned(task) => FallibleTaskState::Spawned(task.fallible()),
|
||||||
|
TaskState::Downcast { inner, .. } => FallibleTaskState::Downcast {
|
||||||
|
inner: Box::new(inner.fallible()),
|
||||||
|
marker: PhantomData,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Task<Box<dyn Any + Send + Sync>> {
|
||||||
|
/// Reinterprets the boxed output as a concrete `T` via downcast on
|
||||||
|
/// completion. Used by [`LocalExecutor::spawn_dedicated`] and
|
||||||
|
/// [`BackgroundExecutor::spawn_dedicated`] to recover the user closure's
|
||||||
|
/// `Fut::Output` from the dyn-safe [`Scheduler::spawn_dedicated`].
|
||||||
|
///
|
||||||
|
/// Panics on poll if the inner output is not in fact a `T` -- a logic
|
||||||
|
/// error in whatever produced the inner task, since the downcast type is
|
||||||
|
/// chosen by the caller of `downcast`.
|
||||||
|
pub fn downcast<T: Send + Sync + 'static>(self) -> Task<T> {
|
||||||
|
Task(TaskState::Downcast {
|
||||||
|
inner: Box::new(self),
|
||||||
|
marker: PhantomData,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl<T> std::fmt::Debug for Task<T> {
|
||||||
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
|
match &self.0 {
|
||||||
|
TaskState::Ready(_) => f.debug_tuple("Task::Ready").finish(),
|
||||||
|
TaskState::Spawned(task) => f.debug_tuple("Task::Spawned").field(task).finish(),
|
||||||
|
TaskState::Downcast { inner, .. } => {
|
||||||
|
f.debug_tuple("Task::Downcast").field(inner).finish()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// A task that returns `Option<T>` instead of panicking when cancelled.
|
/// A task that returns `Option<T>` instead of panicking when cancelled.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub struct FallibleTask<T>(FallibleTaskState<T>);
|
pub struct FallibleTask<T>(FallibleTaskState<T>);
|
||||||
|
|
@ -259,6 +381,12 @@ enum FallibleTaskState<T> {
|
||||||
|
|
||||||
/// A task that is currently running (wraps async_task::FallibleTask).
|
/// A task that is currently running (wraps async_task::FallibleTask).
|
||||||
Spawned(async_task::FallibleTask<T, RunnableMeta>),
|
Spawned(async_task::FallibleTask<T, RunnableMeta>),
|
||||||
|
|
||||||
|
/// Mirror of [`TaskState::Downcast`] for fallible tasks.
|
||||||
|
Downcast {
|
||||||
|
inner: Box<FallibleTask<Box<dyn Any + Send + Sync>>>,
|
||||||
|
marker: PhantomData<fn() -> T>,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> FallibleTask<T> {
|
impl<T> FallibleTask<T> {
|
||||||
|
|
@ -272,17 +400,29 @@ impl<T> FallibleTask<T> {
|
||||||
match self.0 {
|
match self.0 {
|
||||||
FallibleTaskState::Ready(_) => {}
|
FallibleTaskState::Ready(_) => {}
|
||||||
FallibleTaskState::Spawned(task) => task.detach(),
|
FallibleTaskState::Spawned(task) => task.detach(),
|
||||||
|
FallibleTaskState::Downcast { inner, .. } => inner.detach(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Future for FallibleTask<T> {
|
impl<T: 'static> Future for FallibleTask<T> {
|
||||||
type Output = Option<T>;
|
type Output = Option<T>;
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||||
match unsafe { self.get_unchecked_mut() } {
|
match unsafe { self.get_unchecked_mut() } {
|
||||||
FallibleTask(FallibleTaskState::Ready(val)) => Poll::Ready(val.take()),
|
FallibleTask(FallibleTaskState::Ready(val)) => Poll::Ready(val.take()),
|
||||||
FallibleTask(FallibleTaskState::Spawned(task)) => Pin::new(task).poll(cx),
|
FallibleTask(FallibleTaskState::Spawned(task)) => Pin::new(task).poll(cx),
|
||||||
|
FallibleTask(FallibleTaskState::Downcast { inner, .. }) => {
|
||||||
|
match Pin::new(inner.as_mut()).poll(cx) {
|
||||||
|
Poll::Ready(Some(boxed_any)) => Poll::Ready(Some(
|
||||||
|
*boxed_any
|
||||||
|
.downcast::<T>()
|
||||||
|
.expect("FallibleTask::poll: downcast type mismatch"),
|
||||||
|
)),
|
||||||
|
Poll::Ready(None) => Poll::Ready(None),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -294,17 +434,29 @@ impl<T> std::fmt::Debug for FallibleTask<T> {
|
||||||
FallibleTaskState::Spawned(task) => {
|
FallibleTaskState::Spawned(task) => {
|
||||||
f.debug_tuple("FallibleTask::Spawned").field(task).finish()
|
f.debug_tuple("FallibleTask::Spawned").field(task).finish()
|
||||||
}
|
}
|
||||||
|
FallibleTaskState::Downcast { inner, .. } => f
|
||||||
|
.debug_tuple("FallibleTask::Downcast")
|
||||||
|
.field(inner)
|
||||||
|
.finish(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<T> Future for Task<T> {
|
impl<T: 'static> Future for Task<T> {
|
||||||
type Output = T;
|
type Output = T;
|
||||||
|
|
||||||
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
|
||||||
match unsafe { self.get_unchecked_mut() } {
|
match unsafe { self.get_unchecked_mut() } {
|
||||||
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()),
|
Task(TaskState::Ready(val)) => Poll::Ready(val.take().unwrap()),
|
||||||
Task(TaskState::Spawned(task)) => Pin::new(task).poll(cx),
|
Task(TaskState::Spawned(task)) => Pin::new(task).poll(cx),
|
||||||
|
Task(TaskState::Downcast { inner, .. }) => match Pin::new(inner.as_mut()).poll(cx) {
|
||||||
|
Poll::Ready(boxed_any) => Poll::Ready(
|
||||||
|
*boxed_any
|
||||||
|
.downcast::<T>()
|
||||||
|
.expect("Task::poll: downcast type mismatch"),
|
||||||
|
),
|
||||||
|
Poll::Pending => Poll::Pending,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -11,11 +11,13 @@ pub use test_scheduler::*;
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
use futures::channel::oneshot;
|
use futures::channel::oneshot;
|
||||||
use std::{
|
use std::{
|
||||||
|
any::Any,
|
||||||
future::Future,
|
future::Future,
|
||||||
panic::Location,
|
panic::Location,
|
||||||
pin::Pin,
|
pin::Pin,
|
||||||
sync::Arc,
|
sync::Arc,
|
||||||
task::{Context, Poll},
|
task::{Context, Poll},
|
||||||
|
thread,
|
||||||
time::Duration,
|
time::Duration,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|
@ -82,7 +84,11 @@ pub trait Scheduler: Send + Sync {
|
||||||
timeout: Option<Duration>,
|
timeout: Option<Duration>,
|
||||||
) -> bool;
|
) -> bool;
|
||||||
|
|
||||||
fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable<RunnableMeta>);
|
/// Schedule a runnable on the local (session-pinned) queue for `session_id`.
|
||||||
|
/// Runnables scheduled here run in order on whichever thread drains the
|
||||||
|
/// session — the main thread for ordinary sessions, or a dedicated OS
|
||||||
|
/// thread for sessions created via `spawn_dedicated_thread`.
|
||||||
|
fn schedule_local(&self, session_id: SessionId, runnable: Runnable<RunnableMeta>);
|
||||||
|
|
||||||
/// Schedule a background task with the given priority.
|
/// Schedule a background task with the given priority.
|
||||||
fn schedule_background_with_priority(
|
fn schedule_background_with_priority(
|
||||||
|
|
@ -103,11 +109,87 @@ pub trait Scheduler: Send + Sync {
|
||||||
fn timer(&self, timeout: Duration) -> Timer;
|
fn timer(&self, timeout: Duration) -> Timer;
|
||||||
fn clock(&self) -> Arc<dyn Clock>;
|
fn clock(&self) -> Arc<dyn Clock>;
|
||||||
|
|
||||||
|
/// Spawn a closure on a fresh session pinned to its own [`LocalExecutor`].
|
||||||
|
///
|
||||||
|
/// `PlatformScheduler` runs the closure on a new OS thread (see
|
||||||
|
/// [`spawn_dedicated_thread`]). `TestScheduler` runs it on the test
|
||||||
|
/// scheduler's loop alongside everything else so determinism under
|
||||||
|
/// `TestScheduler::many` is preserved.
|
||||||
|
///
|
||||||
|
/// This is the dyn-safe entry point: the closure's output is type-erased
|
||||||
|
/// as `Box<dyn Any + Send + Sync>` so the trait stays object-safe.
|
||||||
|
/// Callers typically reach for the type-safe wrappers on
|
||||||
|
/// [`LocalExecutor::spawn_dedicated`] and
|
||||||
|
/// [`BackgroundExecutor::spawn_dedicated`], which compose this method
|
||||||
|
/// with [`Task::downcast`] to recover the closure's concrete return type.
|
||||||
|
fn spawn_dedicated(
|
||||||
|
self: Arc<Self>,
|
||||||
|
f: Box<
|
||||||
|
dyn FnOnce(
|
||||||
|
LocalExecutor,
|
||||||
|
)
|
||||||
|
-> Pin<Box<dyn Future<Output = Box<dyn Any + Send + Sync>> + 'static>>
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
>,
|
||||||
|
) -> Task<Box<dyn Any + Send + Sync>>;
|
||||||
|
|
||||||
fn as_test(&self) -> Option<&TestScheduler> {
|
fn as_test(&self) -> Option<&TestScheduler> {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Spawn work on a fresh OS thread that's exclusive to the returned task and
|
||||||
|
/// anything spawned on the executor it provides. Blocking syscalls inside that
|
||||||
|
/// work don't disturb any other executor in the process.
|
||||||
|
///
|
||||||
|
/// `f` is called on the dedicated thread with a [`LocalExecutor`] pinned
|
||||||
|
/// to it. The future `f` returns may freely be `!Send`. The returned `Task` is
|
||||||
|
/// that future's task: dropping it cancels the root, but detached children
|
||||||
|
/// keep running until they finish. The thread shuts down once the executor and
|
||||||
|
/// every task on it are gone.
|
||||||
|
///
|
||||||
|
/// The caller is responsible for supplying a `session_id` that's distinct from
|
||||||
|
/// every other live session on `scheduler`. Concrete schedulers typically wrap
|
||||||
|
/// this in an inherent method that allocates the id from their own counter.
|
||||||
|
pub fn spawn_dedicated_thread<F, Fut>(
|
||||||
|
session_id: SessionId,
|
||||||
|
scheduler: Arc<dyn Scheduler>,
|
||||||
|
f: F,
|
||||||
|
) -> Task<Fut::Output>
|
||||||
|
where
|
||||||
|
F: FnOnce(LocalExecutor) -> Fut + Send + 'static,
|
||||||
|
Fut: Future + 'static,
|
||||||
|
Fut::Output: Send + 'static,
|
||||||
|
{
|
||||||
|
let (runnable_sender, runnable_receiver) = flume::unbounded::<Runnable<RunnableMeta>>();
|
||||||
|
let (task_sender, task_receiver) = flume::bounded::<Task<Fut::Output>>(1);
|
||||||
|
|
||||||
|
thread::Builder::new()
|
||||||
|
.name(format!("spawn_dedicated session {:?}", session_id))
|
||||||
|
.spawn(move || {
|
||||||
|
let dispatch = move |runnable: Runnable<RunnableMeta>| {
|
||||||
|
let _ = runnable_sender.send(runnable);
|
||||||
|
};
|
||||||
|
let executor = LocalExecutor::new(session_id, scheduler, dispatch);
|
||||||
|
let root_task = executor.spawn(f(executor.clone()));
|
||||||
|
let _ = task_sender.send(root_task);
|
||||||
|
// After this drop, every strong reference to the runnable sender
|
||||||
|
// lives inside a spawned task or a user-held executor clone. The
|
||||||
|
// recv loop exits once all of those are gone.
|
||||||
|
drop(executor);
|
||||||
|
|
||||||
|
while let Ok(runnable) = runnable_receiver.recv() {
|
||||||
|
runnable.run();
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.expect("failed to spawn dedicated thread");
|
||||||
|
|
||||||
|
task_receiver
|
||||||
|
.recv()
|
||||||
|
.expect("dedicated thread failed to produce root task")
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
#[derive(Copy, Clone, Debug, Eq, PartialEq, Ord, PartialOrd, Hash)]
|
||||||
pub struct SessionId(u16);
|
pub struct SessionId(u16);
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
use crate::{
|
use crate::{
|
||||||
BackgroundExecutor, Clock, ForegroundExecutor, Instant, Priority, RunnableMeta, Scheduler,
|
BackgroundExecutor, Clock, Instant, LocalExecutor, Priority, RunnableMeta, Scheduler,
|
||||||
SessionId, TestClock, Timer,
|
SessionId, Task, TestClock, Timer,
|
||||||
};
|
};
|
||||||
use async_task::Runnable;
|
use async_task::Runnable;
|
||||||
use backtrace::{Backtrace, BacktraceFrame};
|
use backtrace::{Backtrace, BacktraceFrame};
|
||||||
|
|
@ -10,6 +10,7 @@ use rand::{
|
||||||
distr::{StandardUniform, uniform::SampleRange, uniform::SampleUniform},
|
distr::{StandardUniform, uniform::SampleRange, uniform::SampleUniform},
|
||||||
prelude::*,
|
prelude::*,
|
||||||
};
|
};
|
||||||
|
use std::any::Any;
|
||||||
use std::{
|
use std::{
|
||||||
any::type_name_of_val,
|
any::type_name_of_val,
|
||||||
collections::{BTreeMap, HashSet, VecDeque},
|
collections::{BTreeMap, HashSet, VecDeque},
|
||||||
|
|
@ -152,18 +153,21 @@ impl TestScheduler {
|
||||||
self.state.lock().is_main_thread
|
self.state.lock().is_main_thread
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Allocate a new session ID for foreground task scheduling.
|
|
||||||
/// This is used by GPUI's TestDispatcher to map dispatcher instances to sessions.
|
|
||||||
pub fn allocate_session_id(&self) -> SessionId {
|
pub fn allocate_session_id(&self) -> SessionId {
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
state.next_session_id.0 += 1;
|
state.next_session_id.0 += 1;
|
||||||
state.next_session_id
|
state.next_session_id
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a foreground executor for this scheduler
|
/// Create a local executor for this scheduler.
|
||||||
pub fn foreground(self: &Arc<Self>) -> ForegroundExecutor {
|
pub fn foreground(self: &Arc<Self>) -> LocalExecutor {
|
||||||
let session_id = self.allocate_session_id();
|
let session_id = self.allocate_session_id();
|
||||||
ForegroundExecutor::new(session_id, self.clone())
|
let scheduler = Arc::downgrade(self);
|
||||||
|
LocalExecutor::new(session_id, self.clone(), move |runnable| {
|
||||||
|
if let Some(scheduler) = scheduler.upgrade() {
|
||||||
|
scheduler.schedule_local(session_id, runnable);
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Create a background executor for this scheduler
|
/// Create a background executor for this scheduler
|
||||||
|
|
@ -585,7 +589,7 @@ impl Scheduler for TestScheduler {
|
||||||
completed
|
completed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn schedule_foreground(&self, session_id: SessionId, runnable: Runnable<RunnableMeta>) {
|
fn schedule_local(&self, session_id: SessionId, runnable: Runnable<RunnableMeta>) {
|
||||||
assert_correct_thread(&self.thread, &self.state);
|
assert_correct_thread(&self.thread, &self.state);
|
||||||
let mut state = self.state.lock();
|
let mut state = self.state.lock();
|
||||||
let ix = if state.randomize_order {
|
let ix = if state.randomize_order {
|
||||||
|
|
@ -660,6 +664,31 @@ impl Scheduler for TestScheduler {
|
||||||
self.clock.clone()
|
self.clock.clone()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// In the test world, dedicated work is just a fresh local session driven
|
||||||
|
/// by the test scheduler's run loop alongside everything else. No real
|
||||||
|
/// thread is spawned, so determinism under `TestScheduler::many` is
|
||||||
|
/// preserved.
|
||||||
|
fn spawn_dedicated(
|
||||||
|
self: Arc<Self>,
|
||||||
|
f: Box<
|
||||||
|
dyn FnOnce(
|
||||||
|
LocalExecutor,
|
||||||
|
)
|
||||||
|
-> Pin<Box<dyn Future<Output = Box<dyn Any + Send + Sync>> + 'static>>
|
||||||
|
+ Send
|
||||||
|
+ 'static,
|
||||||
|
>,
|
||||||
|
) -> Task<Box<dyn Any + Send + Sync>> {
|
||||||
|
let session_id = self.allocate_session_id();
|
||||||
|
let scheduler = Arc::downgrade(&self);
|
||||||
|
let executor = LocalExecutor::new(session_id, self, move |runnable| {
|
||||||
|
if let Some(scheduler) = scheduler.upgrade() {
|
||||||
|
scheduler.schedule_local(session_id, runnable);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
executor.spawn(f(executor.clone()))
|
||||||
|
}
|
||||||
|
|
||||||
fn as_test(&self) -> Option<&TestScheduler> {
|
fn as_test(&self) -> Option<&TestScheduler> {
|
||||||
Some(self)
|
Some(self)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -728,3 +728,234 @@ fn test_background_priority_scheduling() {
|
||||||
iterations
|
iterations
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spawn_dedicated_basic_round_trip() {
|
||||||
|
let result = TestScheduler::once(async |scheduler| {
|
||||||
|
scheduler
|
||||||
|
.background()
|
||||||
|
.spawn_dedicated(|_executor| async { 42 })
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
assert_eq!(result, 42);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spawn_dedicated_not_send_future() {
|
||||||
|
let result = TestScheduler::once(async |scheduler| {
|
||||||
|
scheduler
|
||||||
|
.background()
|
||||||
|
.spawn_dedicated(|_executor| async move {
|
||||||
|
// `Rc<RefCell<_>>` is `!Send`. If `spawn_dedicated` required
|
||||||
|
// the returned future to be `Send`, this wouldn't compile.
|
||||||
|
let state = Rc::new(RefCell::new(0_i32));
|
||||||
|
for _ in 0..5 {
|
||||||
|
*state.borrow_mut() += 1;
|
||||||
|
}
|
||||||
|
*state.borrow()
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
assert_eq!(result, 5);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spawn_dedicated_send_closure_captures() {
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
let observed = TestScheduler::once(async |scheduler| {
|
||||||
|
let shared = Arc::new(Mutex::new(0_i32));
|
||||||
|
let shared_for_closure = shared.clone();
|
||||||
|
let returned = scheduler
|
||||||
|
.background()
|
||||||
|
.spawn_dedicated(move |_executor| {
|
||||||
|
// `shared_for_closure` crossed the `Send` boundary of the
|
||||||
|
// closure; we then mutate it from inside the !Send future.
|
||||||
|
let local = shared_for_closure;
|
||||||
|
async move {
|
||||||
|
*local.lock() = 7;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.await;
|
||||||
|
let _: () = returned;
|
||||||
|
*shared.lock()
|
||||||
|
});
|
||||||
|
assert_eq!(observed, 7);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spawn_dedicated_inner_spawn_local() {
|
||||||
|
let result = TestScheduler::once(async |scheduler| {
|
||||||
|
scheduler
|
||||||
|
.background()
|
||||||
|
.spawn_dedicated(|executor| async move {
|
||||||
|
// The provided executor can spawn additional `!Send` work
|
||||||
|
// onto the same dedicated session.
|
||||||
|
let inner = Rc::new(RefCell::new(0_i32));
|
||||||
|
let inner_for_child = inner.clone();
|
||||||
|
let child = executor.spawn(async move {
|
||||||
|
*inner_for_child.borrow_mut() = 99;
|
||||||
|
*inner_for_child.borrow()
|
||||||
|
});
|
||||||
|
child.await
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
});
|
||||||
|
assert_eq!(result, 99);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spawn_dedicated_determinism_under_many() {
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
let outcomes = TestScheduler::many(if cfg!(miri) { 4 } else { 20 }, async |scheduler| {
|
||||||
|
let trace = Arc::new(Mutex::new(Vec::<u32>::new()));
|
||||||
|
|
||||||
|
let background = scheduler.background();
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
for id in 0..4_u32 {
|
||||||
|
let trace = trace.clone();
|
||||||
|
let task = background.spawn_dedicated(move |executor| async move {
|
||||||
|
for step in 0..3 {
|
||||||
|
trace.lock().push(id * 100 + step);
|
||||||
|
executor.spawn(async {}).await;
|
||||||
|
}
|
||||||
|
id
|
||||||
|
});
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
for task in tasks {
|
||||||
|
outputs.push(task.await);
|
||||||
|
}
|
||||||
|
|
||||||
|
(trace.lock().clone(), outputs)
|
||||||
|
});
|
||||||
|
|
||||||
|
// Re-running with the same seed should produce the same trace. Run a
|
||||||
|
// second pass with identical seeds and compare to the first.
|
||||||
|
let outcomes_replay = TestScheduler::many(if cfg!(miri) { 4 } else { 20 }, async |scheduler| {
|
||||||
|
let trace = Arc::new(Mutex::new(Vec::<u32>::new()));
|
||||||
|
|
||||||
|
let background = scheduler.background();
|
||||||
|
let mut tasks = Vec::new();
|
||||||
|
for id in 0..4_u32 {
|
||||||
|
let trace = trace.clone();
|
||||||
|
let task = background.spawn_dedicated(move |executor| async move {
|
||||||
|
for step in 0..3 {
|
||||||
|
trace.lock().push(id * 100 + step);
|
||||||
|
executor.spawn(async {}).await;
|
||||||
|
}
|
||||||
|
id
|
||||||
|
});
|
||||||
|
tasks.push(task);
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut outputs = Vec::new();
|
||||||
|
for task in tasks {
|
||||||
|
outputs.push(task.await);
|
||||||
|
}
|
||||||
|
|
||||||
|
(trace.lock().clone(), outputs)
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
outcomes, outcomes_replay,
|
||||||
|
"per-seed outcomes should be reproducible"
|
||||||
|
);
|
||||||
|
|
||||||
|
// Sanity: at least one seed produced a non-monotonic trace,
|
||||||
|
// demonstrating that dedicated tasks really do interleave under the
|
||||||
|
// scheduler's randomization.
|
||||||
|
let any_interleaved = outcomes.iter().any(|(trace, _)| {
|
||||||
|
trace
|
||||||
|
.windows(2)
|
||||||
|
.any(|window| window[0] / 100 != window[1] / 100)
|
||||||
|
});
|
||||||
|
assert!(
|
||||||
|
any_interleaved,
|
||||||
|
"expected at least one seed to interleave dedicated tasks"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spawn_dedicated_dropping_task_cancels_future() {
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
let counter_after = TestScheduler::once(async |scheduler| {
|
||||||
|
let counter = Arc::new(Mutex::new(0_u32));
|
||||||
|
let (resume_tx, resume_rx) = oneshot::channel::<()>();
|
||||||
|
|
||||||
|
let task = {
|
||||||
|
let counter = counter.clone();
|
||||||
|
scheduler
|
||||||
|
.background()
|
||||||
|
.spawn_dedicated(move |_executor| async move {
|
||||||
|
*counter.lock() = 1;
|
||||||
|
// Park here until the test resumes us. If the task is
|
||||||
|
// dropped before this resolves, the second assignment
|
||||||
|
// below must never happen.
|
||||||
|
let _ = resume_rx.await;
|
||||||
|
*counter.lock() = 2;
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
// Let the dedicated future make its first observable step.
|
||||||
|
scheduler.run();
|
||||||
|
assert_eq!(*counter.lock(), 1);
|
||||||
|
|
||||||
|
// Cancel by dropping the root task, then unblock the parked oneshot.
|
||||||
|
// The future must not advance past the await: counter stays at 1.
|
||||||
|
drop(task);
|
||||||
|
let _ = resume_tx.send(());
|
||||||
|
scheduler.run();
|
||||||
|
|
||||||
|
*counter.lock()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
counter_after, 1,
|
||||||
|
"dropping the dedicated task must cancel the root future before its second write"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn test_spawn_dedicated_detached_child_runs_after_root_completes() {
|
||||||
|
use parking_lot::Mutex;
|
||||||
|
|
||||||
|
let child_ran = TestScheduler::once(async |scheduler| {
|
||||||
|
let child_ran = Arc::new(Mutex::new(false));
|
||||||
|
|
||||||
|
let task = {
|
||||||
|
let child_ran = child_ran.clone();
|
||||||
|
scheduler
|
||||||
|
.background()
|
||||||
|
.spawn_dedicated(move |executor| async move {
|
||||||
|
executor
|
||||||
|
.spawn(async move {
|
||||||
|
*child_ran.lock() = true;
|
||||||
|
})
|
||||||
|
.detach();
|
||||||
|
// Root returns immediately, before the child has had a
|
||||||
|
// chance to run.
|
||||||
|
})
|
||||||
|
};
|
||||||
|
|
||||||
|
task.await;
|
||||||
|
|
||||||
|
// Drain the dedicated session. The detached child must run.
|
||||||
|
scheduler.run();
|
||||||
|
|
||||||
|
*child_ran.lock()
|
||||||
|
});
|
||||||
|
|
||||||
|
assert!(
|
||||||
|
child_ran,
|
||||||
|
"detached child must complete after the root, not be cancelled with it"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// The production smoke test for `spawn_dedicated` lives in the `gpui` crate
|
||||||
|
// alongside `PlatformScheduler`, which is the real production implementation
|
||||||
|
// of the `Scheduler` trait. See `crates/gpui/src/platform_scheduler.rs`.
|
||||||
|
|
|
||||||
|
|
@ -734,11 +734,14 @@ mod tests {
|
||||||
use std::{path::PathBuf, sync::Arc};
|
use std::{path::PathBuf, sync::Arc};
|
||||||
|
|
||||||
use editor::{Editor, SelectionEffects};
|
use editor::{Editor, SelectionEffects};
|
||||||
use gpui::{TestAppContext, VisualTestContext};
|
use gpui::{App, Entity, Task, TestAppContext, VisualTestContext};
|
||||||
use language::{Language, LanguageConfig, LanguageMatcher, Point};
|
use language::{
|
||||||
|
Buffer, ContextProvider, FakeLspAdapter, Language, LanguageConfig, LanguageMatcher,
|
||||||
|
LanguageServerName, Point,
|
||||||
|
};
|
||||||
use project::{ContextProviderWithTasks, FakeFs, Project};
|
use project::{ContextProviderWithTasks, FakeFs, Project};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use task::TaskTemplates;
|
use task::{TaskTemplate, TaskTemplates};
|
||||||
use util::path;
|
use util::path;
|
||||||
use workspace::{CloseInactiveTabsAndPanes, MultiWorkspace, OpenOptions, OpenVisible};
|
use workspace::{CloseInactiveTabsAndPanes, MultiWorkspace, OpenOptions, OpenVisible};
|
||||||
|
|
||||||
|
|
@ -1033,6 +1036,80 @@ mod tests {
|
||||||
cx.executor().run_until_parked();
|
cx.executor().run_until_parked();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[gpui::test]
|
||||||
|
async fn test_empty_lsp_task_response_keeps_language_tasks_in_modal(cx: &mut TestAppContext) {
|
||||||
|
init_test(cx);
|
||||||
|
let fs = FakeFs::new(cx.executor());
|
||||||
|
fs.insert_tree(path!("/dir"), json!({ "main.test": "test" }))
|
||||||
|
.await;
|
||||||
|
|
||||||
|
let project = Project::test(fs, [path!("/dir").as_ref()], cx).await;
|
||||||
|
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||||
|
language_registry.add(Arc::new(
|
||||||
|
Language::new(
|
||||||
|
LanguageConfig {
|
||||||
|
name: "Test".into(),
|
||||||
|
matcher: LanguageMatcher {
|
||||||
|
path_suffixes: vec!["test".to_string()],
|
||||||
|
..LanguageMatcher::default()
|
||||||
|
},
|
||||||
|
..LanguageConfig::default()
|
||||||
|
},
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
.with_context_provider(Some(Arc::new(
|
||||||
|
ContextProviderWithLspTaskSource::new(ContextProviderWithTasks::new(
|
||||||
|
TaskTemplates(vec![TaskTemplate {
|
||||||
|
label: "Run language task".to_string(),
|
||||||
|
command: "echo".to_string(),
|
||||||
|
args: vec!["language task".to_string()],
|
||||||
|
..TaskTemplate::default()
|
||||||
|
}]),
|
||||||
|
)),
|
||||||
|
))),
|
||||||
|
));
|
||||||
|
let mut fake_servers = language_registry.register_fake_lsp(
|
||||||
|
"Test",
|
||||||
|
FakeLspAdapter {
|
||||||
|
name: TEST_LSP_NAME,
|
||||||
|
..FakeLspAdapter::default()
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let (multi_workspace, cx) =
|
||||||
|
cx.add_window_view(|window, cx| MultiWorkspace::test_new(project, window, cx));
|
||||||
|
let workspace =
|
||||||
|
multi_workspace.read_with(cx, |multi_workspace, _| multi_workspace.workspace().clone());
|
||||||
|
let _item = workspace
|
||||||
|
.update_in(cx, |workspace, window, cx| {
|
||||||
|
workspace.open_abs_path(
|
||||||
|
PathBuf::from(path!("/dir/main.test")),
|
||||||
|
OpenOptions {
|
||||||
|
visible: Some(OpenVisible::All),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
window,
|
||||||
|
cx,
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.await
|
||||||
|
.unwrap();
|
||||||
|
cx.executor().run_until_parked();
|
||||||
|
let fake_server = fake_servers
|
||||||
|
.try_recv()
|
||||||
|
.expect("fake LSP server should have started");
|
||||||
|
use project::lsp_store::lsp_ext_command::Runnables;
|
||||||
|
fake_server
|
||||||
|
.set_request_handler::<Runnables, _, _>(move |_, _| async move { Ok(Vec::new()) });
|
||||||
|
|
||||||
|
let tasks_picker = open_spawn_tasks(&workspace, cx);
|
||||||
|
assert_eq!(
|
||||||
|
task_names(&tasks_picker, cx),
|
||||||
|
vec!["Run language task"],
|
||||||
|
"An empty LSP task response should not suppress language tasks in the modal"
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
#[gpui::test]
|
#[gpui::test]
|
||||||
async fn test_language_task_filtering(cx: &mut TestAppContext) {
|
async fn test_language_task_filtering(cx: &mut TestAppContext) {
|
||||||
init_test(cx);
|
init_test(cx);
|
||||||
|
|
@ -1238,6 +1315,32 @@ mod tests {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const TEST_LSP_NAME: &str = "test-lsp";
|
||||||
|
|
||||||
|
struct ContextProviderWithLspTaskSource {
|
||||||
|
tasks: ContextProviderWithTasks,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextProviderWithLspTaskSource {
|
||||||
|
fn new(tasks: ContextProviderWithTasks) -> Self {
|
||||||
|
Self { tasks }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ContextProvider for ContextProviderWithLspTaskSource {
|
||||||
|
fn associated_tasks(
|
||||||
|
&self,
|
||||||
|
buffer: Option<Entity<Buffer>>,
|
||||||
|
cx: &App,
|
||||||
|
) -> Task<Option<TaskTemplates>> {
|
||||||
|
self.tasks.associated_tasks(buffer, cx)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn lsp_task_source(&self) -> Option<LanguageServerName> {
|
||||||
|
Some(LanguageServerName::new_static(TEST_LSP_NAME))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
fn emulate_task_schedule(
|
fn emulate_task_schedule(
|
||||||
tasks_picker: Entity<Picker<TasksModalDelegate>>,
|
tasks_picker: Entity<Picker<TasksModalDelegate>>,
|
||||||
project: &Entity<Project>,
|
project: &Entity<Project>,
|
||||||
|
|
|
||||||
|
|
@ -122,4 +122,11 @@ impl Model {
|
||||||
Self::Custom { .. } => false,
|
Self::Custom { .. } => false,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn supports_reasoning_effort(&self) -> bool {
|
||||||
|
match self {
|
||||||
|
Self::Grok43 => true,
|
||||||
|
Self::Grok420Reasoning | Self::Grok420NonReasoning | Self::Custom { .. } => false,
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -34,6 +34,7 @@ use futures::{StreamExt, channel::mpsc, select_biased};
|
||||||
use git_ui::commit_view::CommitViewToolbar;
|
use git_ui::commit_view::CommitViewToolbar;
|
||||||
use git_ui::git_panel::GitPanel;
|
use git_ui::git_panel::GitPanel;
|
||||||
use git_ui::project_diff::{BranchDiffToolbar, ProjectDiffToolbar};
|
use git_ui::project_diff::{BranchDiffToolbar, ProjectDiffToolbar};
|
||||||
|
use git_ui::solo_diff_view::{SoloDiffGitToolbar, SoloDiffStyleToolbar};
|
||||||
use gpui::{
|
use gpui::{
|
||||||
Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
|
Action, App, AppContext as _, AsyncWindowContext, ClipboardItem, Context, DismissEvent,
|
||||||
Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement,
|
Element, Entity, FocusHandle, Focusable, Image, ImageFormat, KeyBinding, ParentElement,
|
||||||
|
|
@ -1305,6 +1306,8 @@ fn initialize_pane(
|
||||||
pane.toolbar().update(cx, |toolbar, cx| {
|
pane.toolbar().update(cx, |toolbar, cx| {
|
||||||
let multibuffer_hint = cx.new(|_| MultibufferHint::new());
|
let multibuffer_hint = cx.new(|_| MultibufferHint::new());
|
||||||
toolbar.add_item(multibuffer_hint, window, cx);
|
toolbar.add_item(multibuffer_hint, window, cx);
|
||||||
|
let solo_diff_style_toolbar = cx.new(SoloDiffStyleToolbar::new);
|
||||||
|
toolbar.add_item(solo_diff_style_toolbar, window, cx);
|
||||||
let breadcrumbs = cx.new(|_| Breadcrumbs::new());
|
let breadcrumbs = cx.new(|_| Breadcrumbs::new());
|
||||||
toolbar.add_item(breadcrumbs, window, cx);
|
toolbar.add_item(breadcrumbs, window, cx);
|
||||||
let buffer_search_bar = cx.new(|cx| {
|
let buffer_search_bar = cx.new(|cx| {
|
||||||
|
|
@ -1343,6 +1346,8 @@ fn initialize_pane(
|
||||||
toolbar.add_item(project_diff_toolbar, window, cx);
|
toolbar.add_item(project_diff_toolbar, window, cx);
|
||||||
let branch_diff_toolbar = cx.new(BranchDiffToolbar::new);
|
let branch_diff_toolbar = cx.new(BranchDiffToolbar::new);
|
||||||
toolbar.add_item(branch_diff_toolbar, window, cx);
|
toolbar.add_item(branch_diff_toolbar, window, cx);
|
||||||
|
let solo_diff_git_toolbar = cx.new(SoloDiffGitToolbar::new);
|
||||||
|
toolbar.add_item(solo_diff_git_toolbar, window, cx);
|
||||||
let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new());
|
let commit_view_toolbar = cx.new(|_| CommitViewToolbar::new());
|
||||||
toolbar.add_item(commit_view_toolbar, window, cx);
|
toolbar.add_item(commit_view_toolbar, window, cx);
|
||||||
let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
|
let agent_diff_toolbar = cx.new(AgentDiffToolbar::new);
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Markdown Preview",
|
"name": "Markdown Preview",
|
||||||
"labels": ["area:preview/markdown", "area:preview/mermaid"]
|
"labels": ["area:preview/markdown", "area:preview/mermaid", "area:preview/csv"]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "NixOS",
|
"name": "NixOS",
|
||||||
|
|
@ -88,9 +88,11 @@
|
||||||
"labels": [
|
"labels": [
|
||||||
"area:command palette",
|
"area:command palette",
|
||||||
"area:file finder",
|
"area:file finder",
|
||||||
|
"area:fs",
|
||||||
"area:navigation",
|
"area:navigation",
|
||||||
"area:outline",
|
"area:outline",
|
||||||
"area:project panel",
|
"area:project panel",
|
||||||
|
"area:scanning",
|
||||||
"area:workspace"
|
"area:workspace"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -100,6 +102,7 @@
|
||||||
"area:code folding",
|
"area:code folding",
|
||||||
"area:editor",
|
"area:editor",
|
||||||
"area:editor/brackets",
|
"area:editor/brackets",
|
||||||
|
"area:editor/bookmarks",
|
||||||
"area:editor/linked edits",
|
"area:editor/linked edits",
|
||||||
"area:multi-buffer",
|
"area:multi-buffer",
|
||||||
"area:multi-cursor",
|
"area:multi-cursor",
|
||||||
|
|
@ -135,6 +138,7 @@
|
||||||
"area:ai",
|
"area:ai",
|
||||||
"area:ai/acp",
|
"area:ai/acp",
|
||||||
"area:ai/agent thread",
|
"area:ai/agent thread",
|
||||||
|
"area:ai/agent thread/skills",
|
||||||
"area:ai/anthropic",
|
"area:ai/anthropic",
|
||||||
"area:ai/assistant",
|
"area:ai/assistant",
|
||||||
"area:ai/bedrock",
|
"area:ai/bedrock",
|
||||||
|
|
@ -154,6 +158,7 @@
|
||||||
"area:ai/opencode",
|
"area:ai/opencode",
|
||||||
"area:ai/openrouter",
|
"area:ai/openrouter",
|
||||||
"area:ai/qwen",
|
"area:ai/qwen",
|
||||||
|
"area:ai/terminal threads",
|
||||||
"area:ai/text thread"
|
"area:ai/text thread"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
@ -222,6 +227,7 @@
|
||||||
"name": "Performance & Catch-all",
|
"name": "Performance & Catch-all",
|
||||||
"labels": [
|
"labels": [
|
||||||
"area:cli",
|
"area:cli",
|
||||||
|
"area:crashes",
|
||||||
"area:discoverability",
|
"area:discoverability",
|
||||||
"area:installer-updater",
|
"area:installer-updater",
|
||||||
"area:internationalization",
|
"area:internationalization",
|
||||||
|
|
@ -236,6 +242,7 @@
|
||||||
"area:performance",
|
"area:performance",
|
||||||
"area:performance/memory leak",
|
"area:performance/memory leak",
|
||||||
"area:release notes",
|
"area:release notes",
|
||||||
|
"area:scripts",
|
||||||
"area:security & privacy",
|
"area:security & privacy",
|
||||||
"area:security & privacy/workspace trust",
|
"area:security & privacy/workspace trust",
|
||||||
"area:serialization",
|
"area:serialization",
|
||||||
|
|
|
||||||
|
|
@ -11,4 +11,5 @@ dependencies = [
|
||||||
"typer>=0.15.1",
|
"typer>=0.15.1",
|
||||||
"types-pytz>=2025.1.0.20250204",
|
"types-pytz>=2025.1.0.20250204",
|
||||||
"types-requests>=2.32.0",
|
"types-requests>=2.32.0",
|
||||||
|
"urllib3>=2.7.0",
|
||||||
]
|
]
|
||||||
|
|
|
||||||
|
|
@ -134,7 +134,7 @@ wheels = [
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "requests"
|
name = "requests"
|
||||||
version = "2.32.3"
|
version = "2.34.2"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
dependencies = [
|
dependencies = [
|
||||||
{ name = "certifi" },
|
{ name = "certifi" },
|
||||||
|
|
@ -142,9 +142,9 @@ dependencies = [
|
||||||
{ name = "idna" },
|
{ name = "idna" },
|
||||||
{ name = "urllib3" },
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/63/70/2bf7780ad2d390a8d301ad0b550f1581eadbd9a20f896afe06353c2a2913/requests-2.32.3.tar.gz", hash = "sha256:55365417734eb18255590a9ff9eb97e9e1da868d4ccd6402399eaf68af20a760", size = 131218, upload-time = "2024-05-29T15:37:49.536Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/ac/c3/e2a2b89f2d3e2179abd6d00ebd70bff6273f37fb3e0cc209f48b39d00cbf/requests-2.34.2.tar.gz", hash = "sha256:f288924cae4e29463698d6d60bc6a4da69c89185ad1e0bcc4104f584e960b9ed", size = 142856, upload-time = "2026-05-14T19:25:27.735Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/f9/9b/335f9764261e915ed497fcdeb11df5dfd6f7bf257d4a6a2a686d80da4d54/requests-2.32.3-py3-none-any.whl", hash = "sha256:70761cfe03c773ceb22aa2f671b4757976145175cdfca038c02654d061d6dcc6", size = 64928, upload-time = "2024-05-29T15:37:47.027Z" },
|
{ url = "https://files.pythonhosted.org/packages/a0/f4/c67b0b3f1b9245e8d266f0f112c500d50e5b4e83cb6f3b71b6528104182a/requests-2.34.2-py3-none-any.whl", hash = "sha256:2a0d60c172f83ac6ab31e4554906c0f3b3588d37b5cb939b1c061f4907e278e0", size = 73075, upload-time = "2026-05-14T19:25:26.443Z" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
|
|
@ -252,6 +252,7 @@ dependencies = [
|
||||||
{ name = "typer" },
|
{ name = "typer" },
|
||||||
{ name = "types-pytz" },
|
{ name = "types-pytz" },
|
||||||
{ name = "types-requests" },
|
{ name = "types-requests" },
|
||||||
|
{ name = "urllib3" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[package.metadata]
|
[package.metadata]
|
||||||
|
|
@ -263,13 +264,14 @@ requires-dist = [
|
||||||
{ name = "typer", specifier = ">=0.15.1" },
|
{ name = "typer", specifier = ">=0.15.1" },
|
||||||
{ name = "types-pytz", specifier = ">=2025.1.0.20250204" },
|
{ name = "types-pytz", specifier = ">=2025.1.0.20250204" },
|
||||||
{ name = "types-requests", specifier = ">=2.32.0" },
|
{ name = "types-requests", specifier = ">=2.32.0" },
|
||||||
|
{ name = "urllib3", specifier = ">=2.7.0" },
|
||||||
]
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "urllib3"
|
name = "urllib3"
|
||||||
version = "2.2.3"
|
version = "2.7.0"
|
||||||
source = { registry = "https://pypi.org/simple" }
|
source = { registry = "https://pypi.org/simple" }
|
||||||
sdist = { url = "https://files.pythonhosted.org/packages/ed/63/22ba4ebfe7430b76388e7cd448d5478814d3032121827c12a2cc287e2260/urllib3-2.2.3.tar.gz", hash = "sha256:e7d814a81dad81e6caf2ec9fdedb284ecc9c73076b62654547cc64ccdcae26e9", size = 300677, upload-time = "2024-09-12T10:52:18.401Z" }
|
sdist = { url = "https://files.pythonhosted.org/packages/53/0c/06f8b233b8fd13b9e5ee11424ef85419ba0d8ba0b3138bf360be2ff56953/urllib3-2.7.0.tar.gz", hash = "sha256:231e0ec3b63ceb14667c67be60f2f2c40a518cb38b03af60abc813da26505f4c", size = 433602, upload-time = "2026-05-07T16:13:18.596Z" }
|
||||||
wheels = [
|
wheels = [
|
||||||
{ url = "https://files.pythonhosted.org/packages/ce/d9/5f4c13cecde62396b0d3fe530a50ccea91e7dfc1ccf0e09c228841bb5ba8/urllib3-2.2.3-py3-none-any.whl", hash = "sha256:ca899ca043dcb1bafa3e262d73aa25c465bfb49e0bd9dd5d59f1d0acba2f8fac", size = 126338, upload-time = "2024-09-12T10:52:16.589Z" },
|
{ url = "https://files.pythonhosted.org/packages/7f/3e/5db95bcf282c52709639744ca2a8b149baccf648e39c8cc87553df9eae0c/urllib3-2.7.0-py3-none-any.whl", hash = "sha256:9fb4c81ebbb1ce9531cce37674bbc6f1360472bc18ca9a553ede278ef7276897", size = 131087, upload-time = "2026-05-07T16:13:17.151Z" },
|
||||||
]
|
]
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue