Compare commits
102 Commits
make-langu
...
run-comman
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
cfab93eb15 | ||
|
|
fa0df6da1c | ||
|
|
99102a84fa | ||
|
|
5f01f6d75f | ||
|
|
a66cd820b3 | ||
|
|
f07da9d9f2 | ||
|
|
8d05bb090c | ||
|
|
2325f14713 | ||
|
|
fe2aa3f4cb | ||
|
|
10989c702c | ||
|
|
3f80ac0127 | ||
|
|
4f1634f95c | ||
|
|
40eec32cb8 | ||
|
|
17499453f6 | ||
|
|
80a4746a46 | ||
|
|
01f5b73e3b | ||
|
|
a0081dd693 | ||
|
|
f522823988 | ||
|
|
5a8603bebb | ||
|
|
abac87c2f8 | ||
|
|
c3d065cecc | ||
|
|
e1a5d29972 | ||
|
|
d342da4e9a | ||
|
|
7ae8f81d74 | ||
|
|
36364b16a0 | ||
|
|
b35959f4c2 | ||
|
|
9450bcad25 | ||
|
|
69bdef38ec | ||
|
|
0e33a3afe0 | ||
|
|
76aaf6a8fe | ||
|
|
0ef7ee172f | ||
|
|
29def012a1 | ||
|
|
5c30578c49 | ||
|
|
1552afd8bf | ||
|
|
e04473dd26 | ||
|
|
84f166fc85 | ||
|
|
065518577e | ||
|
|
1d828b6ac6 | ||
|
|
777ce7cc97 | ||
|
|
1f37fbd051 | ||
|
|
8c9442ad11 | ||
|
|
47a475681f | ||
|
|
23dc1f5ea4 | ||
|
|
a6a111cadd | ||
|
|
6a7b84eb87 | ||
|
|
59bdbf5a5d | ||
|
|
64b6e8ba0f | ||
|
|
236b3e546e | ||
|
|
ea363466aa | ||
|
|
c45177e296 | ||
|
|
45fa034107 | ||
|
|
1c5c8552f2 | ||
|
|
5d374193bb | ||
|
|
b65fb06264 | ||
|
|
b3405c3bd1 | ||
|
|
638320b21e | ||
|
|
91ab0636ec | ||
|
|
fb6cc8794f | ||
|
|
3d37611b6f | ||
|
|
360e372b57 | ||
|
|
74e8afe9a8 | ||
|
|
e30f45cf64 | ||
|
|
16c4fd4fc5 | ||
|
|
ec58adca13 | ||
|
|
bed358718b | ||
|
|
4124bedab7 | ||
|
|
57c6dbd71e | ||
|
|
fded3fbcdb | ||
|
|
a660527036 | ||
|
|
0cb8a8983c | ||
|
|
c7902478c1 | ||
|
|
3c0183fa5e | ||
|
|
e982cb824a | ||
|
|
1b865a60f8 | ||
|
|
4c32d5bf13 | ||
|
|
ccae033d85 | ||
|
|
c2fa9d7981 | ||
|
|
5f03202b5c | ||
|
|
223fda2fe2 | ||
|
|
a85946eba8 | ||
|
|
9d94358971 | ||
|
|
9e11105483 | ||
|
|
caebd0cc4d | ||
|
|
6e2922367c | ||
|
|
25ee9b1013 | ||
|
|
0870a1fe80 | ||
|
|
e37efc1e9b | ||
|
|
1ae326432e | ||
|
|
a05f86f97b | ||
|
|
473bbd78cc | ||
|
|
28c78d2d85 | ||
|
|
fca44f89c1 | ||
|
|
b7ad20773c | ||
|
|
aa1629b544 | ||
|
|
69a5c45672 | ||
|
|
d0aaf04673 | ||
|
|
d677c98f43 | ||
|
|
ce362864db | ||
|
|
a1a6031c6a | ||
|
|
2d20b5d850 | ||
|
|
11ad0b5793 | ||
|
|
2755cd8ec7 |
@@ -19,8 +19,6 @@ rustflags = [
|
||||
"windows_slim_errors", # This cfg will reduce the size of `windows::core::Error` from 16 bytes to 4 bytes
|
||||
"-C",
|
||||
"target-feature=+crt-static", # This fixes the linking issue when compiling livekit on Windows
|
||||
"-C",
|
||||
"link-arg=-fuse-ld=lld",
|
||||
]
|
||||
|
||||
[env]
|
||||
|
||||
@@ -41,5 +41,4 @@ workspace-members = [
|
||||
"slash_commands_example",
|
||||
"zed_snippets",
|
||||
"zed_test_extension",
|
||||
"zed_toml",
|
||||
]
|
||||
|
||||
2
.gitattributes
vendored
2
.gitattributes
vendored
@@ -2,4 +2,4 @@
|
||||
*.json linguist-language=JSON-with-Comments
|
||||
|
||||
# Ensure the WSL script always has LF line endings, even on Windows
|
||||
crates/zed/resources/windows/zed-wsl text eol=lf
|
||||
crates/zed/resources/windows/zed.sh text eol=lf
|
||||
|
||||
13
.rules
13
.rules
@@ -12,6 +12,19 @@
|
||||
- Example: avoid `let _ = client.request(...).await?;` - use `client.request(...).await?;` instead
|
||||
* When implementing async operations that may fail, ensure errors propagate to the UI layer so users get meaningful feedback.
|
||||
* Never create files with `mod.rs` paths - prefer `src/some_module.rs` instead of `src/some_module/mod.rs`.
|
||||
* When creating new crates, prefer specifying the library root path in `Cargo.toml` using `[lib] path = "...rs"` instead of the default `lib.rs`, to maintain consistent and descriptive naming (e.g., `gpui.rs` or `main.rs`).
|
||||
* Avoid creative additions unless explicitly requested
|
||||
* Use full words for variable names (no abbreviations like "q" for "queue")
|
||||
* Use variable shadowing to scope clones in async contexts for clarity, minimizing the lifetime of borrowed references.
|
||||
Example:
|
||||
```rust
|
||||
executor.spawn({
|
||||
let task_ran = task_ran.clone();
|
||||
async move {
|
||||
*task_ran.borrow_mut() = true;
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
# GPUI
|
||||
|
||||
|
||||
@@ -65,7 +65,7 @@ If you would like to add a new icon to the Zed icon theme, [open a Discussion](h
|
||||
|
||||
## Bird's-eye view of Zed
|
||||
|
||||
We suggest you keep the [zed glossary](docs/src/development/GLOSSARY.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
|
||||
We suggest you keep the [zed glossary](docs/src/development/glossary.md) at your side when starting out. It lists and explains some of the structures and terms you will see throughout the codebase.
|
||||
|
||||
Zed is made up of several smaller crates - let's go over those you're most likely to interact with:
|
||||
|
||||
|
||||
224
Cargo.lock
generated
224
Cargo.lock
generated
@@ -26,7 +26,7 @@ dependencies = [
|
||||
"portable-pty",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -79,7 +79,7 @@ dependencies = [
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"text",
|
||||
@@ -172,7 +172,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"ref-cast",
|
||||
"rope",
|
||||
"schemars",
|
||||
@@ -308,22 +308,18 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"node_runtime",
|
||||
"paths",
|
||||
"project",
|
||||
"reqwest_client",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"task",
|
||||
"tempfile",
|
||||
"thiserror 2.0.12",
|
||||
"ui",
|
||||
"util",
|
||||
"watch",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -408,7 +404,7 @@ dependencies = [
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"rope",
|
||||
"rules_library",
|
||||
@@ -834,7 +830,7 @@ dependencies = [
|
||||
"project",
|
||||
"prompt_store",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rpc",
|
||||
"serde",
|
||||
@@ -933,7 +929,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -985,7 +981,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"prompt_store",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"reqwest_client",
|
||||
"rust-embed",
|
||||
@@ -2291,7 +2287,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-graphics"
|
||||
version = "0.6.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"ash",
|
||||
"ash-window",
|
||||
@@ -2324,7 +2320,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-macros"
|
||||
version = "0.3.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -2334,7 +2330,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "blade-util"
|
||||
version = "0.2.0"
|
||||
source = "git+https://github.com/kvark/blade?rev=e0ec4e720957edd51b945b64dd85605ea54bcfe5#e0ec4e720957edd51b945b64dd85605ea54bcfe5"
|
||||
source = "git+https://github.com/kvark/blade?rev=bfa594ea697d4b6326ea29f747525c85ecf933b9#bfa594ea697d4b6326ea29f747525c85ecf933b9"
|
||||
dependencies = [
|
||||
"blade-graphics",
|
||||
"bytemuck",
|
||||
@@ -2351,19 +2347,6 @@ dependencies = [
|
||||
"digest",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "blake3"
|
||||
version = "1.8.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3888aaa89e4b2a40fca9848e400f6a658a5a3978de7be858e209cafa8be9a4a0"
|
||||
dependencies = [
|
||||
"arrayref",
|
||||
"arrayvec",
|
||||
"cc",
|
||||
"cfg-if",
|
||||
"constant_time_eq 0.3.1",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "block"
|
||||
version = "0.1.6"
|
||||
@@ -2478,7 +2461,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"serde_json",
|
||||
"sum_tree",
|
||||
@@ -2899,7 +2882,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"settings",
|
||||
@@ -3070,7 +3053,6 @@ dependencies = [
|
||||
"clock",
|
||||
"cloud_api_client",
|
||||
"cloud_llm_client",
|
||||
"cocoa 0.26.0",
|
||||
"collections",
|
||||
"credentials_provider",
|
||||
"derive_more",
|
||||
@@ -3083,10 +3065,11 @@ dependencies = [
|
||||
"http_client_tls",
|
||||
"httparse",
|
||||
"log",
|
||||
"objc2-foundation",
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
@@ -3335,7 +3318,7 @@ dependencies = [
|
||||
"prometheus",
|
||||
"prompt_store",
|
||||
"prost 0.9.0",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"recent_projects",
|
||||
"release_channel",
|
||||
"remote",
|
||||
@@ -3515,6 +3498,7 @@ name = "component"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"documented",
|
||||
"gpui",
|
||||
"inventory",
|
||||
"parking_lot",
|
||||
@@ -3577,12 +3561,6 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "245097e9a4535ee1e3e3931fcfcd55a796a44c643e8596ff6566d68f09b87bbc"
|
||||
|
||||
[[package]]
|
||||
name = "constant_time_eq"
|
||||
version = "0.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "7c74b8349d32d297c9134b8c88677813a227df8f779daa29bfc29c183fe3dca6"
|
||||
|
||||
[[package]]
|
||||
name = "context_server"
|
||||
version = "0.1.0"
|
||||
@@ -4697,7 +4675,7 @@ dependencies = [
|
||||
"markdown",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -5066,9 +5044,10 @@ dependencies = [
|
||||
"multi_buffer",
|
||||
"ordered-float 2.10.1",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
@@ -5563,7 +5542,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"release_channel",
|
||||
"remote",
|
||||
"reqwest_client",
|
||||
@@ -6163,17 +6142,6 @@ dependencies = [
|
||||
"futures-util",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-batch"
|
||||
version = "0.6.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "6f444c45a1cb86f2a7e301469fd50a82084a60dadc25d94529a8312276ecb71a"
|
||||
dependencies = [
|
||||
"futures 0.3.31",
|
||||
"futures-timer",
|
||||
"pin-utils",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "futures-channel"
|
||||
version = "0.3.31"
|
||||
@@ -6269,12 +6237,6 @@ version = "0.3.31"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||
|
||||
[[package]]
|
||||
name = "futures-timer"
|
||||
version = "3.0.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||
|
||||
[[package]]
|
||||
name = "futures-util"
|
||||
version = "0.3.31"
|
||||
@@ -6412,7 +6374,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rope",
|
||||
"schemars",
|
||||
@@ -7465,7 +7427,7 @@ dependencies = [
|
||||
"pathfinder_geometry",
|
||||
"postage",
|
||||
"profiling",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"raw-window-handle",
|
||||
"refineable",
|
||||
"reqwest_client",
|
||||
@@ -9078,7 +9040,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rpc",
|
||||
"schemars",
|
||||
@@ -9220,6 +9182,19 @@ dependencies = [
|
||||
"x_ai",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_onboarding"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"db",
|
||||
"editor",
|
||||
"gpui",
|
||||
"project",
|
||||
"ui",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "language_selector"
|
||||
version = "0.1.0"
|
||||
@@ -9247,6 +9222,7 @@ dependencies = [
|
||||
"anyhow",
|
||||
"client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
"editor",
|
||||
"futures 0.3.31",
|
||||
@@ -9281,7 +9257,6 @@ dependencies = [
|
||||
"chrono",
|
||||
"collections",
|
||||
"dap",
|
||||
"feature_flags",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"http_client",
|
||||
@@ -9517,6 +9492,21 @@ dependencies = [
|
||||
"vcpkg",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "line_ending_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"editor",
|
||||
"gpui",
|
||||
"language",
|
||||
"picker",
|
||||
"project",
|
||||
"ui",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "link-cplusplus"
|
||||
version = "1.0.10"
|
||||
@@ -10392,7 +10382,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"serde",
|
||||
"settings",
|
||||
@@ -12618,12 +12608,13 @@ dependencies = [
|
||||
"postage",
|
||||
"prettier",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"remote",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"semver",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
@@ -12643,6 +12634,7 @@ dependencies = [
|
||||
"unindent",
|
||||
"url",
|
||||
"util",
|
||||
"watch",
|
||||
"which 6.0.3",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
@@ -13892,7 +13884,7 @@ dependencies = [
|
||||
"ctor",
|
||||
"gpui",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"smallvec",
|
||||
"sum_tree",
|
||||
@@ -13921,7 +13913,7 @@ dependencies = [
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"proto",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rsa",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -14356,6 +14348,19 @@ dependencies = [
|
||||
"windows-sys 0.59.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "scheduler"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"async-task",
|
||||
"chrono",
|
||||
"futures 0.3.31",
|
||||
"parking",
|
||||
"parking_lot",
|
||||
"rand 0.9.1",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "schema_generator"
|
||||
version = "0.1.0"
|
||||
@@ -14658,49 +14663,6 @@ version = "1.2.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0f7d95a54511e0c7be3f51e8867aa8cf35148d7b9445d44de2f943e2b206e749"
|
||||
|
||||
[[package]]
|
||||
name = "semantic_index"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"arrayvec",
|
||||
"blake3",
|
||||
"client",
|
||||
"clock",
|
||||
"collections",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"futures-batch",
|
||||
"gpui",
|
||||
"heed",
|
||||
"http_client",
|
||||
"language",
|
||||
"language_model",
|
||||
"languages",
|
||||
"log",
|
||||
"open_ai",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"reqwest_client",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sha2",
|
||||
"smol",
|
||||
"streaming-iterator",
|
||||
"tempfile",
|
||||
"theme",
|
||||
"tree-sitter",
|
||||
"ui",
|
||||
"unindent",
|
||||
"util",
|
||||
"workspace",
|
||||
"workspace-hack",
|
||||
"worktree",
|
||||
"zlog",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "semantic_version"
|
||||
version = "0.1.0"
|
||||
@@ -15318,6 +15280,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"indoc",
|
||||
"libsqlite3-sys",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"smol",
|
||||
"sqlformat",
|
||||
@@ -15655,7 +15618,7 @@ name = "streaming_diff"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"ordered-float 2.10.1",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rope",
|
||||
"util",
|
||||
"workspace-hack",
|
||||
@@ -15769,7 +15732,7 @@ dependencies = [
|
||||
"arrayvec",
|
||||
"ctor",
|
||||
"log",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rayon",
|
||||
"workspace-hack",
|
||||
"zlog",
|
||||
@@ -16360,7 +16323,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"libc",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"schemars",
|
||||
@@ -16408,7 +16371,7 @@ dependencies = [
|
||||
"language",
|
||||
"log",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"schemars",
|
||||
"search",
|
||||
@@ -16440,7 +16403,7 @@ dependencies = [
|
||||
"log",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rope",
|
||||
"smallvec",
|
||||
@@ -16971,10 +16934,15 @@ checksum = "bfb942dfe1d8e29a7ee7fcbde5bd2b9a25fb89aa70caea2eba3bee836ff41076"
|
||||
name = "toolchain_selector"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"convert_case 0.8.0",
|
||||
"editor",
|
||||
"file_finder",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"language",
|
||||
"menu",
|
||||
"picker",
|
||||
"project",
|
||||
"ui",
|
||||
@@ -17797,7 +17765,7 @@ dependencies = [
|
||||
"libc",
|
||||
"log",
|
||||
"nix 0.29.0",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"rust-embed",
|
||||
"schemars",
|
||||
@@ -17982,6 +17950,8 @@ version = "0.1.0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"gpui",
|
||||
"schemars",
|
||||
"serde",
|
||||
"settings",
|
||||
"workspace-hack",
|
||||
]
|
||||
@@ -18588,7 +18558,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"parking_lot",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"workspace-hack",
|
||||
"zlog",
|
||||
]
|
||||
@@ -20047,7 +20017,7 @@ dependencies = [
|
||||
"paths",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"rpc",
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -20408,7 +20378,6 @@ dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
"agent",
|
||||
"agent_servers",
|
||||
"agent_settings",
|
||||
"agent_ui",
|
||||
"anyhow",
|
||||
@@ -20472,10 +20441,12 @@ dependencies = [
|
||||
"language_extension",
|
||||
"language_model",
|
||||
"language_models",
|
||||
"language_onboarding",
|
||||
"language_selector",
|
||||
"language_tools",
|
||||
"languages",
|
||||
"libc",
|
||||
"line_ending_selector",
|
||||
"livekit_client",
|
||||
"log",
|
||||
"markdown",
|
||||
@@ -20493,6 +20464,7 @@ dependencies = [
|
||||
"parking_lot",
|
||||
"paths",
|
||||
"picker",
|
||||
"postage",
|
||||
"pretty_assertions",
|
||||
"profiling",
|
||||
"project",
|
||||
@@ -20627,7 +20599,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_snippets"
|
||||
version = "0.0.5"
|
||||
version = "0.0.6"
|
||||
dependencies = [
|
||||
"serde_json",
|
||||
"zed_extension_api 0.1.0",
|
||||
@@ -20640,13 +20612,6 @@ dependencies = [
|
||||
"zed_extension_api 0.6.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zed_toml"
|
||||
version = "0.1.4"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.1.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zeno"
|
||||
version = "0.3.2"
|
||||
@@ -20810,9 +20775,10 @@ dependencies = [
|
||||
"language_model",
|
||||
"log",
|
||||
"menu",
|
||||
"parking_lot",
|
||||
"postage",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"rand 0.9.1",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
@@ -20882,7 +20848,7 @@ dependencies = [
|
||||
"aes",
|
||||
"byteorder",
|
||||
"bzip2",
|
||||
"constant_time_eq 0.1.5",
|
||||
"constant_time_eq",
|
||||
"crc32fast",
|
||||
"crossbeam-utils",
|
||||
"flate2",
|
||||
|
||||
44
Cargo.toml
44
Cargo.toml
@@ -94,9 +94,11 @@ members = [
|
||||
"crates/language_extension",
|
||||
"crates/language_model",
|
||||
"crates/language_models",
|
||||
"crates/language_onboarding",
|
||||
"crates/language_selector",
|
||||
"crates/language_tools",
|
||||
"crates/languages",
|
||||
"crates/line_ending_selector",
|
||||
"crates/livekit_api",
|
||||
"crates/livekit_client",
|
||||
"crates/lmstudio",
|
||||
@@ -131,6 +133,7 @@ members = [
|
||||
"crates/refineable",
|
||||
"crates/refineable/derive_refineable",
|
||||
"crates/release_channel",
|
||||
"crates/scheduler",
|
||||
"crates/remote",
|
||||
"crates/remote_server",
|
||||
"crates/repl",
|
||||
@@ -141,7 +144,6 @@ members = [
|
||||
"crates/rules_library",
|
||||
"crates/schema_generator",
|
||||
"crates/search",
|
||||
"crates/semantic_index",
|
||||
"crates/semantic_version",
|
||||
"crates/session",
|
||||
"crates/settings",
|
||||
@@ -210,7 +212,6 @@ members = [
|
||||
"extensions/slash-commands-example",
|
||||
"extensions/snippets",
|
||||
"extensions/test-extension",
|
||||
"extensions/toml",
|
||||
|
||||
#
|
||||
# Tooling
|
||||
@@ -320,9 +321,11 @@ language = { path = "crates/language" }
|
||||
language_extension = { path = "crates/language_extension" }
|
||||
language_model = { path = "crates/language_model" }
|
||||
language_models = { path = "crates/language_models" }
|
||||
language_onboarding = { path = "crates/language_onboarding" }
|
||||
language_selector = { path = "crates/language_selector" }
|
||||
language_tools = { path = "crates/language_tools" }
|
||||
languages = { path = "crates/languages" }
|
||||
line_ending_selector = { path = "crates/line_ending_selector" }
|
||||
livekit_api = { path = "crates/livekit_api" }
|
||||
livekit_client = { path = "crates/livekit_client" }
|
||||
lmstudio = { path = "crates/lmstudio" }
|
||||
@@ -360,6 +363,7 @@ proto = { path = "crates/proto" }
|
||||
recent_projects = { path = "crates/recent_projects" }
|
||||
refineable = { path = "crates/refineable" }
|
||||
release_channel = { path = "crates/release_channel" }
|
||||
scheduler = { path = "crates/scheduler" }
|
||||
remote = { path = "crates/remote" }
|
||||
remote_server = { path = "crates/remote_server" }
|
||||
repl = { path = "crates/repl" }
|
||||
@@ -370,7 +374,6 @@ rope = { path = "crates/rope" }
|
||||
rpc = { path = "crates/rpc" }
|
||||
rules_library = { path = "crates/rules_library" }
|
||||
search = { path = "crates/search" }
|
||||
semantic_index = { path = "crates/semantic_index" }
|
||||
semantic_version = { path = "crates/semantic_version" }
|
||||
session = { path = "crates/session" }
|
||||
settings = { path = "crates/settings" }
|
||||
@@ -444,6 +447,7 @@ async-fs = "2.1"
|
||||
async-pipe = { git = "https://github.com/zed-industries/async-pipe-rs", rev = "82d00a04211cf4e1236029aa03e6b6ce2a74c553" }
|
||||
async-recursion = "1.0.0"
|
||||
async-tar = "0.5.0"
|
||||
async-task = "4.7"
|
||||
async-trait = "0.1"
|
||||
async-tungstenite = "0.29.1"
|
||||
async_zip = { version = "0.0.17", features = ["deflate", "deflate64"] }
|
||||
@@ -459,9 +463,9 @@ aws-smithy-types = { version = "1.3.0", features = ["http-body-1-x"] }
|
||||
base64 = "0.22"
|
||||
bincode = "1.2.1"
|
||||
bitflags = "2.6.0"
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "e0ec4e720957edd51b945b64dd85605ea54bcfe5" }
|
||||
blade-graphics = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-macros = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blade-util = { git = "https://github.com/kvark/blade", rev = "bfa594ea697d4b6326ea29f747525c85ecf933b9" }
|
||||
blake3 = "1.5.3"
|
||||
bytes = "1.0"
|
||||
cargo_metadata = "0.19"
|
||||
@@ -535,9 +539,35 @@ nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c80421
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
objc2-foundation = { version = "0.3", default-features = false, features = [
|
||||
"NSArray",
|
||||
"NSAttributedString",
|
||||
"NSBundle",
|
||||
"NSCoder",
|
||||
"NSData",
|
||||
"NSDate",
|
||||
"NSDictionary",
|
||||
"NSEnumerator",
|
||||
"NSError",
|
||||
"NSGeometry",
|
||||
"NSNotification",
|
||||
"NSNull",
|
||||
"NSObjCRuntime",
|
||||
"NSObject",
|
||||
"NSProcessInfo",
|
||||
"NSRange",
|
||||
"NSRunLoop",
|
||||
"NSString",
|
||||
"NSURL",
|
||||
"NSUndoManager",
|
||||
"NSValue",
|
||||
"objc2-core-foundation",
|
||||
"std"
|
||||
] }
|
||||
open = "5.0.0"
|
||||
ordered-float = "2.1.1"
|
||||
palette = { version = "0.7.5", default-features = false, features = ["std"] }
|
||||
parking = "2.0"
|
||||
parking_lot = "0.12.1"
|
||||
partial-json-fixer = "0.5.3"
|
||||
parse_int = "0.9"
|
||||
@@ -560,7 +590,7 @@ prost-build = "0.9"
|
||||
prost-types = "0.9"
|
||||
pulldown-cmark = { version = "0.12.0", default-features = false }
|
||||
quote = "1.0.9"
|
||||
rand = "0.8.5"
|
||||
rand = "0.9"
|
||||
rayon = "1.8"
|
||||
ref-cast = "1.0.24"
|
||||
regex = "1.5"
|
||||
|
||||
@@ -16,6 +16,7 @@
|
||||
"up": "menu::SelectPrevious",
|
||||
"enter": "menu::Confirm",
|
||||
"ctrl-enter": "menu::SecondaryConfirm",
|
||||
"ctrl-escape": "menu::Cancel",
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel",
|
||||
"alt-shift-enter": "menu::Restart",
|
||||
@@ -582,7 +583,7 @@
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
@@ -627,6 +628,7 @@
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-alt-s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
"ctrl-k ctrl-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
|
||||
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
|
||||
@@ -1027,6 +1029,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"bindings": {
|
||||
|
||||
@@ -649,7 +649,7 @@
|
||||
"alt-shift-enter": "toast::RunAction",
|
||||
"cmd-shift-s": "workspace::SaveAs",
|
||||
"cmd-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"cmd-1": ["workspace::ActivatePane", 0],
|
||||
"cmd-2": ["workspace::ActivatePane", 1],
|
||||
"cmd-3": ["workspace::ActivatePane", 2],
|
||||
@@ -690,6 +690,7 @@
|
||||
"cmd-?": "agent::ToggleFocus",
|
||||
"cmd-alt-s": "workspace::SaveAll",
|
||||
"cmd-k m": "language_selector::Toggle",
|
||||
"cmd-k cmd-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"cmd-k cmd-left": "workspace::ActivatePaneLeft",
|
||||
"cmd-k cmd-right": "workspace::ActivatePaneRight",
|
||||
@@ -1094,6 +1095,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
|
||||
@@ -25,7 +25,6 @@
|
||||
"ctrl-alt-enter": ["picker::ConfirmInput", { "secondary": true }],
|
||||
"ctrl-shift-w": "workspace::CloseWindow",
|
||||
"shift-escape": "workspace::ToggleZoom",
|
||||
"open": "workspace::Open",
|
||||
"ctrl-o": "workspace::Open",
|
||||
"ctrl-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
"ctrl-shift-=": ["zed::IncreaseBufferFontSize", { "persist": false }],
|
||||
@@ -68,18 +67,13 @@
|
||||
"ctrl-k q": "editor::Rewrap",
|
||||
"ctrl-backspace": ["editor::DeleteToPreviousWordStart", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"ctrl-delete": ["editor::DeleteToNextWordEnd", { "ignore_newlines": false, "ignore_brackets": false }],
|
||||
"cut": "editor::Cut",
|
||||
"shift-delete": "editor::Cut",
|
||||
"ctrl-x": "editor::Cut",
|
||||
"copy": "editor::Copy",
|
||||
"ctrl-insert": "editor::Copy",
|
||||
"ctrl-c": "editor::Copy",
|
||||
"paste": "editor::Paste",
|
||||
"shift-insert": "editor::Paste",
|
||||
"ctrl-v": "editor::Paste",
|
||||
"undo": "editor::Undo",
|
||||
"ctrl-z": "editor::Undo",
|
||||
"redo": "editor::Redo",
|
||||
"ctrl-y": "editor::Redo",
|
||||
"ctrl-shift-z": "editor::Redo",
|
||||
"up": "editor::MoveUp",
|
||||
@@ -138,7 +132,6 @@
|
||||
"ctrl-shift-enter": "editor::NewlineAbove",
|
||||
"ctrl-k ctrl-z": "editor::ToggleSoftWrap",
|
||||
"ctrl-k z": "editor::ToggleSoftWrap",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-f": "buffer_search::Deploy",
|
||||
"ctrl-h": "buffer_search::DeployReplace",
|
||||
"ctrl-shift-.": "assistant::QuoteSelection",
|
||||
@@ -177,7 +170,6 @@
|
||||
"context": "Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"copy": "markdown::Copy",
|
||||
"ctrl-c": "markdown::Copy"
|
||||
}
|
||||
},
|
||||
@@ -225,7 +217,6 @@
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"save": "workspace::Save",
|
||||
"ctrl-shift-,": "assistant::InsertIntoEditor",
|
||||
"shift-enter": "assistant::Split",
|
||||
"ctrl-r": "assistant::CycleMessageRole",
|
||||
@@ -272,7 +263,6 @@
|
||||
"context": "AgentPanel > Markdown",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"copy": "markdown::CopyAsMarkdown",
|
||||
"ctrl-c": "markdown::CopyAsMarkdown"
|
||||
}
|
||||
},
|
||||
@@ -367,7 +357,6 @@
|
||||
"context": "PromptLibrary",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"new": "rules_library::NewRule",
|
||||
"ctrl-n": "rules_library::NewRule",
|
||||
"ctrl-shift-s": "rules_library::ToggleDefaultRule"
|
||||
}
|
||||
@@ -381,7 +370,6 @@
|
||||
"enter": "search::SelectNextMatch",
|
||||
"shift-enter": "search::SelectPreviousMatch",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"find": "search::FocusSearch",
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"ctrl-h": "search::ToggleReplace",
|
||||
"ctrl-l": "search::ToggleSelection"
|
||||
@@ -408,7 +396,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"escape": "project_search::ToggleFocus",
|
||||
"shift-find": "search::FocusSearch",
|
||||
"ctrl-shift-f": "search::FocusSearch",
|
||||
"ctrl-shift-h": "search::ToggleReplace",
|
||||
"alt-r": "search::ToggleRegex" // vscode
|
||||
@@ -472,14 +459,12 @@
|
||||
"forward": "pane::GoForward",
|
||||
"f3": "search::SelectNextMatch",
|
||||
"shift-f3": "search::SelectPreviousMatch",
|
||||
"shift-find": "project_search::ToggleFocus",
|
||||
"ctrl-shift-f": "project_search::ToggleFocus",
|
||||
"shift-alt-h": "search::ToggleReplace",
|
||||
"alt-l": "search::ToggleSelection",
|
||||
"alt-enter": "search::SelectAllMatches",
|
||||
"alt-c": "search::ToggleCaseSensitive",
|
||||
"alt-w": "search::ToggleWholeWord",
|
||||
"alt-find": "project_search::ToggleFilters",
|
||||
"alt-f": "project_search::ToggleFilters",
|
||||
"alt-r": "search::ToggleRegex",
|
||||
// "ctrl-shift-alt-x": "search::ToggleRegex",
|
||||
@@ -579,27 +564,21 @@
|
||||
"context": "Workspace",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"alt-open": ["projects::OpenRecent", { "create_new_window": false }],
|
||||
// Change the default action on `menu::Confirm` by setting the parameter
|
||||
// "ctrl-alt-o": ["projects::OpenRecent", { "create_new_window": true }],
|
||||
"ctrl-r": ["projects::OpenRecent", { "create_new_window": false }],
|
||||
"shift-alt-open": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
||||
// Change to open path modal for existing remote connection by setting the parameter
|
||||
// "ctrl-shift-alt-o": "["projects::OpenRemote", { "from_existing_connection": true }]",
|
||||
"ctrl-shift-alt-o": ["projects::OpenRemote", { "from_existing_connection": false, "create_new_window": false }],
|
||||
"shift-alt-b": "branches::OpenRecent",
|
||||
"shift-alt-enter": "toast::RunAction",
|
||||
"ctrl-shift-`": "workspace::NewTerminal",
|
||||
"save": "workspace::Save",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-k ctrl-shift-s": "workspace::SaveWithoutFormat",
|
||||
"shift-save": "workspace::SaveAs",
|
||||
"ctrl-shift-s": "workspace::SaveAs",
|
||||
"new": "workspace::NewFile",
|
||||
"ctrl-n": "workspace::NewFile",
|
||||
"shift-new": "workspace::NewWindow",
|
||||
"ctrl-shift-n": "workspace::NewWindow",
|
||||
"ctrl-`": "terminal_panel::ToggleFocus",
|
||||
"ctrl-`": "terminal_panel::Toggle",
|
||||
"f10": ["app_menu::OpenApplicationMenu", "Zed"],
|
||||
"alt-1": ["workspace::ActivatePane", 0],
|
||||
"alt-2": ["workspace::ActivatePane", 1],
|
||||
@@ -621,7 +600,6 @@
|
||||
"shift-alt-0": "workspace::ResetOpenDocksSize",
|
||||
"ctrl-shift-alt--": ["workspace::DecreaseOpenDocksSize", { "px": 0 }],
|
||||
"ctrl-shift-alt-=": ["workspace::IncreaseOpenDocksSize", { "px": 0 }],
|
||||
"shift-find": "pane::DeploySearch",
|
||||
"ctrl-shift-f": "pane::DeploySearch",
|
||||
"ctrl-shift-h": ["pane::DeploySearch", { "replace_enabled": true }],
|
||||
"ctrl-shift-t": "pane::ReopenClosedItem",
|
||||
@@ -641,9 +619,9 @@
|
||||
"ctrl-shift-g": "git_panel::ToggleFocus",
|
||||
"ctrl-shift-d": "debug_panel::ToggleFocus",
|
||||
"ctrl-shift-/": "agent::ToggleFocus",
|
||||
"alt-save": "workspace::SaveAll",
|
||||
"ctrl-k s": "workspace::SaveAll",
|
||||
"ctrl-k m": "language_selector::Toggle",
|
||||
"ctrl-m ctrl-m": "toolchain::AddToolchain",
|
||||
"escape": "workspace::Unfollow",
|
||||
"ctrl-k ctrl-left": "workspace::ActivatePaneLeft",
|
||||
"ctrl-k ctrl-right": "workspace::ActivatePaneRight",
|
||||
@@ -848,9 +826,7 @@
|
||||
"bindings": {
|
||||
"left": "outline_panel::CollapseSelectedEntry",
|
||||
"right": "outline_panel::ExpandSelectedEntry",
|
||||
"alt-copy": "outline_panel::CopyPath",
|
||||
"shift-alt-c": "outline_panel::CopyPath",
|
||||
"shift-alt-copy": "workspace::CopyRelativePath",
|
||||
"ctrl-shift-alt-c": "workspace::CopyRelativePath",
|
||||
"ctrl-alt-r": "outline_panel::RevealInFileManager",
|
||||
"space": "outline_panel::OpenSelectedEntry",
|
||||
@@ -866,21 +842,14 @@
|
||||
"bindings": {
|
||||
"left": "project_panel::CollapseSelectedEntry",
|
||||
"right": "project_panel::ExpandSelectedEntry",
|
||||
"new": "project_panel::NewFile",
|
||||
"ctrl-n": "project_panel::NewFile",
|
||||
"alt-new": "project_panel::NewDirectory",
|
||||
"alt-n": "project_panel::NewDirectory",
|
||||
"cut": "project_panel::Cut",
|
||||
"ctrl-x": "project_panel::Cut",
|
||||
"copy": "project_panel::Copy",
|
||||
"ctrl-insert": "project_panel::Copy",
|
||||
"ctrl-c": "project_panel::Copy",
|
||||
"paste": "project_panel::Paste",
|
||||
"shift-insert": "project_panel::Paste",
|
||||
"ctrl-v": "project_panel::Paste",
|
||||
"alt-copy": "project_panel::CopyPath",
|
||||
"shift-alt-c": "project_panel::CopyPath",
|
||||
"shift-alt-copy": "workspace::CopyRelativePath",
|
||||
"ctrl-k ctrl-shift-c": "workspace::CopyRelativePath",
|
||||
"enter": "project_panel::Rename",
|
||||
"f2": "project_panel::Rename",
|
||||
@@ -892,7 +861,6 @@
|
||||
"ctrl-alt-r": "project_panel::RevealInFileManager",
|
||||
"ctrl-shift-enter": "project_panel::OpenWithSystem",
|
||||
"alt-d": "project_panel::CompareMarkedFiles",
|
||||
"shift-find": "project_panel::NewSearchInDirectory",
|
||||
"ctrl-k ctrl-shift-f": "project_panel::NewSearchInDirectory",
|
||||
"shift-down": "menu::SelectNext",
|
||||
"shift-up": "menu::SelectPrevious",
|
||||
@@ -1075,6 +1043,13 @@
|
||||
"tab": "channel_modal::ToggleMode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "ToolchainSelector",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-shift-a": "toolchain::AddToolchain"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "FileFinder || (FileFinder > Picker > Editor)",
|
||||
"use_key_equivalents": true,
|
||||
@@ -1110,10 +1085,8 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-alt-space": "terminal::ShowCharacterPalette",
|
||||
"copy": "terminal::Copy",
|
||||
"ctrl-insert": "terminal::Copy",
|
||||
"ctrl-shift-c": "terminal::Copy",
|
||||
"paste": "terminal::Paste",
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
@@ -1129,7 +1102,6 @@
|
||||
"ctrl-w": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-backspace": ["terminal::SendKeystroke", "ctrl-w"],
|
||||
"ctrl-shift-a": "editor::SelectAll",
|
||||
"find": "buffer_search::Deploy",
|
||||
"ctrl-shift-f": "buffer_search::Deploy",
|
||||
"ctrl-shift-l": "terminal::Clear",
|
||||
"ctrl-shift-w": "pane::CloseActiveItem",
|
||||
@@ -1210,7 +1182,6 @@
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-f": "search::FocusSearch",
|
||||
"alt-find": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-f": "keymap_editor::ToggleKeystrokeSearch",
|
||||
"alt-c": "keymap_editor::ToggleConflictFilter",
|
||||
"enter": "keymap_editor::EditBinding",
|
||||
|
||||
@@ -125,7 +125,7 @@
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"ctrl-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -127,7 +127,7 @@
|
||||
{
|
||||
"context": "Workspace || Editor",
|
||||
"bindings": {
|
||||
"alt-f12": "terminal_panel::ToggleFocus",
|
||||
"alt-f12": "terminal_panel::Toggle",
|
||||
"cmd-shift-k": "git::Push"
|
||||
}
|
||||
},
|
||||
|
||||
@@ -32,34 +32,6 @@
|
||||
"(": "vim::SentenceBackward",
|
||||
")": "vim::SentenceForward",
|
||||
"|": "vim::GoToColumn",
|
||||
"] ]": "vim::NextSectionStart",
|
||||
"] [": "vim::NextSectionEnd",
|
||||
"[ [": "vim::PreviousSectionStart",
|
||||
"[ ]": "vim::PreviousSectionEnd",
|
||||
"] m": "vim::NextMethodStart",
|
||||
"] shift-m": "vim::NextMethodEnd",
|
||||
"[ m": "vim::PreviousMethodStart",
|
||||
"[ shift-m": "vim::PreviousMethodEnd",
|
||||
"[ *": "vim::PreviousComment",
|
||||
"[ /": "vim::PreviousComment",
|
||||
"] *": "vim::NextComment",
|
||||
"] /": "vim::NextComment",
|
||||
"[ -": "vim::PreviousLesserIndent",
|
||||
"[ +": "vim::PreviousGreaterIndent",
|
||||
"[ =": "vim::PreviousSameIndent",
|
||||
"] -": "vim::NextLesserIndent",
|
||||
"] +": "vim::NextGreaterIndent",
|
||||
"] =": "vim::NextSameIndent",
|
||||
"] b": "pane::ActivateNextItem",
|
||||
"[ b": "pane::ActivatePreviousItem",
|
||||
"] shift-b": "pane::ActivateLastItem",
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
"] space": "vim::InsertEmptyLineBelow",
|
||||
"[ space": "vim::InsertEmptyLineAbove",
|
||||
"[ e": "editor::MoveLineUp",
|
||||
"] e": "editor::MoveLineDown",
|
||||
"[ f": "workspace::FollowNextCollaborator",
|
||||
"] f": "workspace::FollowNextCollaborator",
|
||||
|
||||
// Word motions
|
||||
"w": "vim::NextWordStart",
|
||||
@@ -83,10 +55,6 @@
|
||||
"n": "vim::MoveToNextMatch",
|
||||
"shift-n": "vim::MoveToPreviousMatch",
|
||||
"%": "vim::Matching",
|
||||
"] }": ["vim::UnmatchedForward", { "char": "}" }],
|
||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
|
||||
"] )": ["vim::UnmatchedForward", { "char": ")" }],
|
||||
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
|
||||
"f": ["vim::PushFindForward", { "before": false, "multiline": false }],
|
||||
"t": ["vim::PushFindForward", { "before": true, "multiline": false }],
|
||||
"shift-f": ["vim::PushFindBackward", { "after": false, "multiline": false }],
|
||||
@@ -219,6 +187,46 @@
|
||||
".": "vim::Repeat"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal || vim_mode == visual || vim_mode == operator",
|
||||
"bindings": {
|
||||
"] ]": "vim::NextSectionStart",
|
||||
"] [": "vim::NextSectionEnd",
|
||||
"[ [": "vim::PreviousSectionStart",
|
||||
"[ ]": "vim::PreviousSectionEnd",
|
||||
"] m": "vim::NextMethodStart",
|
||||
"] shift-m": "vim::NextMethodEnd",
|
||||
"[ m": "vim::PreviousMethodStart",
|
||||
"[ shift-m": "vim::PreviousMethodEnd",
|
||||
"[ *": "vim::PreviousComment",
|
||||
"[ /": "vim::PreviousComment",
|
||||
"] *": "vim::NextComment",
|
||||
"] /": "vim::NextComment",
|
||||
"[ -": "vim::PreviousLesserIndent",
|
||||
"[ +": "vim::PreviousGreaterIndent",
|
||||
"[ =": "vim::PreviousSameIndent",
|
||||
"] -": "vim::NextLesserIndent",
|
||||
"] +": "vim::NextGreaterIndent",
|
||||
"] =": "vim::NextSameIndent",
|
||||
"] b": "pane::ActivateNextItem",
|
||||
"[ b": "pane::ActivatePreviousItem",
|
||||
"] shift-b": "pane::ActivateLastItem",
|
||||
"[ shift-b": ["pane::ActivateItem", 0],
|
||||
"] space": "vim::InsertEmptyLineBelow",
|
||||
"[ space": "vim::InsertEmptyLineAbove",
|
||||
"[ e": "editor::MoveLineUp",
|
||||
"] e": "editor::MoveLineDown",
|
||||
"[ f": "workspace::FollowNextCollaborator",
|
||||
"] f": "workspace::FollowNextCollaborator",
|
||||
"] }": ["vim::UnmatchedForward", { "char": "}" }],
|
||||
"[ {": ["vim::UnmatchedBackward", { "char": "{" }],
|
||||
"] )": ["vim::UnmatchedForward", { "char": ")" }],
|
||||
"[ (": ["vim::UnmatchedBackward", { "char": "(" }],
|
||||
// tree-sitter related commands
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
"] x": "vim::SelectSmallerSyntaxNode"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_mode == normal",
|
||||
"bindings": {
|
||||
@@ -249,9 +257,6 @@
|
||||
"g w": "vim::PushRewrap",
|
||||
"g q": "vim::PushRewrap",
|
||||
"insert": "vim::InsertBefore",
|
||||
// tree-sitter related commands
|
||||
"[ x": "vim::SelectLargerSyntaxNode",
|
||||
"] x": "vim::SelectSmallerSyntaxNode",
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPreviousDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
@@ -317,10 +322,7 @@
|
||||
"g w": "vim::Rewrap",
|
||||
"g ?": "vim::ConvertToRot13",
|
||||
// "g ?": "vim::ConvertToRot47",
|
||||
"\"": "vim::PushRegister",
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode"
|
||||
"\"": "vim::PushRegister"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -397,6 +399,9 @@
|
||||
"ctrl-[": "editor::Cancel",
|
||||
";": "vim::HelixCollapseSelection",
|
||||
":": "command_palette::Toggle",
|
||||
"m": "vim::PushHelixMatch",
|
||||
"]": ["vim::PushHelixNext", { "around": true }],
|
||||
"[": ["vim::PushHelixPrevious", { "around": true }],
|
||||
"left": "vim::WrappingLeft",
|
||||
"right": "vim::WrappingRight",
|
||||
"h": "vim::WrappingLeft",
|
||||
@@ -419,13 +424,6 @@
|
||||
"insert": "vim::InsertBefore",
|
||||
"alt-.": "vim::RepeatFind",
|
||||
"alt-s": ["editor::SplitSelectionIntoLines", { "keep_selections": true }],
|
||||
// tree-sitter related commands
|
||||
"[ x": "editor::SelectLargerSyntaxNode",
|
||||
"] x": "editor::SelectSmallerSyntaxNode",
|
||||
"] d": "editor::GoToDiagnostic",
|
||||
"[ d": "editor::GoToPreviousDiagnostic",
|
||||
"] c": "editor::GoToHunk",
|
||||
"[ c": "editor::GoToPreviousHunk",
|
||||
// Goto mode
|
||||
"g n": "pane::ActivateNextItem",
|
||||
"g p": "pane::ActivatePreviousItem",
|
||||
@@ -469,9 +467,6 @@
|
||||
"space c": "editor::ToggleComments",
|
||||
"space y": "editor::Copy",
|
||||
"space p": "editor::Paste",
|
||||
// Match mode
|
||||
"m m": "vim::Matching",
|
||||
"m i w": ["workspace::SendKeystrokes", "v i w"],
|
||||
"shift-u": "editor::Redo",
|
||||
"ctrl-c": "editor::ToggleComments",
|
||||
"d": "vim::HelixDelete",
|
||||
@@ -540,7 +535,7 @@
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs",
|
||||
"context": "vim_operator == a || vim_operator == i || vim_operator == cs || vim_operator == helix_next || vim_operator == helix_previous",
|
||||
"bindings": {
|
||||
"w": "vim::Word",
|
||||
"shift-w": ["vim::Word", { "ignore_punctuation": true }],
|
||||
@@ -577,6 +572,48 @@
|
||||
"e": "vim::EntireFile"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_m",
|
||||
"bindings": {
|
||||
"m": "vim::Matching"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_next",
|
||||
"bindings": {
|
||||
"z": "vim::NextSectionStart",
|
||||
"shift-z": "vim::NextSectionEnd",
|
||||
"*": "vim::NextComment",
|
||||
"/": "vim::NextComment",
|
||||
"-": "vim::NextLesserIndent",
|
||||
"+": "vim::NextGreaterIndent",
|
||||
"=": "vim::NextSameIndent",
|
||||
"b": "pane::ActivateNextItem",
|
||||
"shift-b": "pane::ActivateLastItem",
|
||||
"x": "editor::SelectSmallerSyntaxNode",
|
||||
"d": "editor::GoToDiagnostic",
|
||||
"c": "editor::GoToHunk",
|
||||
"space": "vim::InsertEmptyLineBelow"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == helix_previous",
|
||||
"bindings": {
|
||||
"z": "vim::PreviousSectionStart",
|
||||
"shift-z": "vim::PreviousSectionEnd",
|
||||
"*": "vim::PreviousComment",
|
||||
"/": "vim::PreviousComment",
|
||||
"-": "vim::PreviousLesserIndent",
|
||||
"+": "vim::PreviousGreaterIndent",
|
||||
"=": "vim::PreviousSameIndent",
|
||||
"b": "pane::ActivatePreviousItem",
|
||||
"shift-b": ["pane::ActivateItem", 0],
|
||||
"x": "editor::SelectLargerSyntaxNode",
|
||||
"d": "editor::GoToPreviousDiagnostic",
|
||||
"c": "editor::GoToPreviousHunk",
|
||||
"space": "vim::InsertEmptyLineAbove"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "vim_operator == c",
|
||||
"bindings": {
|
||||
|
||||
@@ -962,7 +962,7 @@
|
||||
// Show git status colors in the editor tabs.
|
||||
"git_status": false,
|
||||
// Position of the close button on the editor tabs.
|
||||
// One of: ["right", "left", "hidden"]
|
||||
// One of: ["right", "left"]
|
||||
"close_position": "right",
|
||||
// Whether to show the file icon for a tab.
|
||||
"file_icons": false,
|
||||
|
||||
@@ -2114,7 +2114,7 @@ mod tests {
|
||||
use gpui::{App, AsyncApp, TestAppContext, WeakEntity};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Fs};
|
||||
use rand::Rng as _;
|
||||
use rand::{distr, prelude::*};
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt as _;
|
||||
@@ -2758,7 +2758,7 @@ mod tests {
|
||||
}));
|
||||
|
||||
let thread = cx
|
||||
.update(|cx| connection.new_thread(project, Path::new("/test"), cx))
|
||||
.update(|cx| connection.new_thread(project, Path::new(path!("/test")), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -3057,8 +3057,8 @@ mod tests {
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||
let session_id = acp::SessionId(
|
||||
rand::thread_rng()
|
||||
.sample_iter(&rand::distributions::Alphanumeric)
|
||||
rand::rng()
|
||||
.sample_iter(&distr::Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
|
||||
@@ -2218,7 +2218,7 @@ mod tests {
|
||||
action_log.update(cx, |log, cx| log.buffer_read(buffer.clone(), cx));
|
||||
|
||||
for _ in 0..operations {
|
||||
match rng.gen_range(0..100) {
|
||||
match rng.random_range(0..100) {
|
||||
0..25 => {
|
||||
action_log.update(cx, |log, cx| {
|
||||
let range = buffer.read(cx).random_byte_range(0, &mut rng);
|
||||
@@ -2237,7 +2237,7 @@ mod tests {
|
||||
.unwrap();
|
||||
}
|
||||
_ => {
|
||||
let is_agent_edit = rng.gen_bool(0.5);
|
||||
let is_agent_edit = rng.random_bool(0.5);
|
||||
if is_agent_edit {
|
||||
log::info!("agent edit");
|
||||
} else {
|
||||
@@ -2252,7 +2252,7 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
if rng.gen_bool(0.2) {
|
||||
if rng.random_bool(0.2) {
|
||||
quiesce(&action_log, &buffer, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -84,7 +84,6 @@ impl ActivityIndicator {
|
||||
) -> Entity<ActivityIndicator> {
|
||||
let project = workspace.project().clone();
|
||||
let auto_updater = AutoUpdater::get(cx);
|
||||
let workspace_handle = cx.entity();
|
||||
let this = cx.new(|cx| {
|
||||
let mut status_events = languages.language_server_binary_statuses();
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -102,20 +101,6 @@ impl ActivityIndicator {
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.subscribe_in(
|
||||
&workspace_handle,
|
||||
window,
|
||||
|activity_indicator, _, event, window, cx| {
|
||||
if let workspace::Event::ClearActivityIndicator = event
|
||||
&& activity_indicator.statuses.pop().is_some()
|
||||
{
|
||||
activity_indicator.dismiss_error_message(&DismissErrorMessage, window, cx);
|
||||
cx.notify();
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.subscribe(
|
||||
&project.read(cx).lsp_store(),
|
||||
|activity_indicator, _, event, cx| {
|
||||
@@ -227,7 +212,8 @@ impl ActivityIndicator {
|
||||
server_name,
|
||||
status,
|
||||
} => {
|
||||
let create_buffer = project.update(cx, |project, cx| project.create_buffer(cx));
|
||||
let create_buffer =
|
||||
project.update(cx, |project, cx| project.create_buffer(false, cx));
|
||||
let status = status.clone();
|
||||
let server_name = server_name.clone();
|
||||
cx.spawn_in(window, async move |workspace, cx| {
|
||||
|
||||
@@ -35,10 +35,15 @@ impl AgentServer for NativeAgentServer {
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
_root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn acp_thread::AgentConnection>>> {
|
||||
) -> Task<
|
||||
Result<(
|
||||
Rc<dyn acp_thread::AgentConnection>,
|
||||
Option<task::SpawnInTerminal>,
|
||||
)>,
|
||||
> {
|
||||
log::debug!(
|
||||
"NativeAgentServer::connect called for path: {:?}",
|
||||
_root_dir
|
||||
@@ -60,7 +65,10 @@ impl AgentServer for NativeAgentServer {
|
||||
let connection = NativeAgentConnection(agent);
|
||||
log::debug!("NativeAgentServer connection established successfully");
|
||||
|
||||
Ok(Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>)
|
||||
Ok((
|
||||
Rc::new(connection) as Rc<dyn acp_thread::AgentConnection>,
|
||||
None,
|
||||
))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -24,7 +24,11 @@ impl AgentTool for EchoTool {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Echo".into()
|
||||
}
|
||||
|
||||
@@ -55,7 +59,11 @@ impl AgentTool for DelayTool {
|
||||
"delay"
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Delay {}ms", input.ms).into()
|
||||
} else {
|
||||
@@ -100,7 +108,11 @@ impl AgentTool for ToolRequiringPermission {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"This tool requires permission".into()
|
||||
}
|
||||
|
||||
@@ -135,7 +147,11 @@ impl AgentTool for InfiniteTool {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Infinite Tool".into()
|
||||
}
|
||||
|
||||
@@ -186,7 +202,11 @@ impl AgentTool for WordListTool {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"List of random words".into()
|
||||
}
|
||||
|
||||
|
||||
@@ -741,7 +741,7 @@ impl Thread {
|
||||
return;
|
||||
};
|
||||
|
||||
let title = tool.initial_title(tool_use.input.clone());
|
||||
let title = tool.initial_title(tool_use.input.clone(), cx);
|
||||
let kind = tool.kind();
|
||||
stream.send_tool_call(&tool_use.id, title, kind, tool_use.input.clone());
|
||||
|
||||
@@ -1062,7 +1062,11 @@ impl Thread {
|
||||
self.action_log.clone(),
|
||||
));
|
||||
self.add_tool(DiagnosticsTool::new(self.project.clone()));
|
||||
self.add_tool(EditFileTool::new(cx.weak_entity(), language_registry));
|
||||
self.add_tool(EditFileTool::new(
|
||||
self.project.clone(),
|
||||
cx.weak_entity(),
|
||||
language_registry,
|
||||
));
|
||||
self.add_tool(FetchTool::new(self.project.read(cx).client().http_client()));
|
||||
self.add_tool(FindPathTool::new(self.project.clone()));
|
||||
self.add_tool(GrepTool::new(self.project.clone()));
|
||||
@@ -1514,7 +1518,7 @@ impl Thread {
|
||||
let mut title = SharedString::from(&tool_use.name);
|
||||
let mut kind = acp::ToolKind::Other;
|
||||
if let Some(tool) = tool.as_ref() {
|
||||
title = tool.initial_title(tool_use.input.clone());
|
||||
title = tool.initial_title(tool_use.input.clone(), cx);
|
||||
kind = tool.kind();
|
||||
}
|
||||
|
||||
@@ -2148,7 +2152,11 @@ where
|
||||
fn kind() -> acp::ToolKind;
|
||||
|
||||
/// The initial tool title to display. Can be updated during the tool run.
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString;
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
cx: &mut App,
|
||||
) -> SharedString;
|
||||
|
||||
/// Returns the JSON schema that describes the tool's input.
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Schema {
|
||||
@@ -2196,7 +2204,7 @@ pub trait AnyAgentTool {
|
||||
fn name(&self) -> SharedString;
|
||||
fn description(&self) -> SharedString;
|
||||
fn kind(&self) -> acp::ToolKind;
|
||||
fn initial_title(&self, input: serde_json::Value) -> SharedString;
|
||||
fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString;
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value>;
|
||||
fn supported_provider(&self, _provider: &LanguageModelProviderId) -> bool {
|
||||
true
|
||||
@@ -2232,9 +2240,9 @@ where
|
||||
T::kind()
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: serde_json::Value) -> SharedString {
|
||||
fn initial_title(&self, input: serde_json::Value, _cx: &mut App) -> SharedString {
|
||||
let parsed_input = serde_json::from_value(input.clone()).map_err(|_| input);
|
||||
self.0.initial_title(parsed_input)
|
||||
self.0.initial_title(parsed_input, _cx)
|
||||
}
|
||||
|
||||
fn input_schema(&self, format: LanguageModelToolSchemaFormat) -> Result<serde_json::Value> {
|
||||
|
||||
@@ -145,7 +145,7 @@ impl AnyAgentTool for ContextServerTool {
|
||||
ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: serde_json::Value) -> SharedString {
|
||||
fn initial_title(&self, _input: serde_json::Value, _cx: &mut App) -> SharedString {
|
||||
format!("Run MCP tool `{}`", self.tool.name).into()
|
||||
}
|
||||
|
||||
@@ -176,7 +176,7 @@ impl AnyAgentTool for ContextServerTool {
|
||||
return Task::ready(Err(anyhow!("Context server not found")));
|
||||
};
|
||||
let tool_name = self.tool.name.clone();
|
||||
let authorize = event_stream.authorize(self.initial_title(input.clone()), cx);
|
||||
let authorize = event_stream.authorize(self.initial_title(input.clone(), cx), cx);
|
||||
|
||||
cx.spawn(async move |_cx| {
|
||||
authorize.await?;
|
||||
|
||||
@@ -58,7 +58,11 @@ impl AgentTool for CopyPathTool {
|
||||
ToolKind::Move
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> ui::SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> ui::SharedString {
|
||||
if let Ok(input) = input {
|
||||
let src = MarkdownInlineCode(&input.source_path);
|
||||
let dest = MarkdownInlineCode(&input.destination_path);
|
||||
|
||||
@@ -49,7 +49,11 @@ impl AgentTool for CreateDirectoryTool {
|
||||
ToolKind::Read
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Create directory {}", MarkdownInlineCode(&input.path)).into()
|
||||
} else {
|
||||
|
||||
@@ -52,7 +52,11 @@ impl AgentTool for DeletePathTool {
|
||||
ToolKind::Delete
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Delete “`{}`”", input.path).into()
|
||||
} else {
|
||||
|
||||
@@ -71,7 +71,11 @@ impl AgentTool for DiagnosticsTool {
|
||||
acp::ToolKind::Read
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Some(path) = input.ok().and_then(|input| match input.path {
|
||||
Some(path) if !path.is_empty() => Some(path),
|
||||
_ => None,
|
||||
|
||||
@@ -83,6 +83,7 @@ struct EditFileToolPartialInput {
|
||||
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "lowercase")]
|
||||
#[schemars(inline)]
|
||||
pub enum EditFileMode {
|
||||
Edit,
|
||||
Create,
|
||||
@@ -119,11 +120,17 @@ impl From<EditFileToolOutput> for LanguageModelToolResultContent {
|
||||
pub struct EditFileTool {
|
||||
thread: WeakEntity<Thread>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
project: Entity<Project>,
|
||||
}
|
||||
|
||||
impl EditFileTool {
|
||||
pub fn new(thread: WeakEntity<Thread>, language_registry: Arc<LanguageRegistry>) -> Self {
|
||||
pub fn new(
|
||||
project: Entity<Project>,
|
||||
thread: WeakEntity<Thread>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
thread,
|
||||
language_registry,
|
||||
}
|
||||
@@ -194,22 +201,50 @@ impl AgentTool for EditFileTool {
|
||||
acp::ToolKind::Edit
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
cx: &mut App,
|
||||
) -> SharedString {
|
||||
match input {
|
||||
Ok(input) => input.display_description.into(),
|
||||
Ok(input) => self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&input.path, cx)
|
||||
.and_then(|project_path| {
|
||||
self.project
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
})
|
||||
.unwrap_or(Path::new(&input.path).into())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into(),
|
||||
Err(raw_input) => {
|
||||
if let Some(input) =
|
||||
serde_json::from_value::<EditFileToolPartialInput>(raw_input).ok()
|
||||
{
|
||||
let path = input.path.trim();
|
||||
if !path.is_empty() {
|
||||
return self
|
||||
.project
|
||||
.read(cx)
|
||||
.find_project_path(&input.path, cx)
|
||||
.and_then(|project_path| {
|
||||
self.project
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
})
|
||||
.unwrap_or(Path::new(&input.path).into())
|
||||
.to_string_lossy()
|
||||
.to_string()
|
||||
.into();
|
||||
}
|
||||
|
||||
let description = input.display_description.trim();
|
||||
if !description.is_empty() {
|
||||
return description.to_string().into();
|
||||
}
|
||||
|
||||
let path = input.path.trim().to_string();
|
||||
if !path.is_empty() {
|
||||
return path.into();
|
||||
}
|
||||
}
|
||||
|
||||
DEFAULT_UI_TEXT.into()
|
||||
@@ -544,7 +579,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -559,11 +594,12 @@ mod tests {
|
||||
path: "root/nonexistent_file.txt".into(),
|
||||
mode: EditFileMode::Edit,
|
||||
};
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
Arc::new(EditFileTool::new(
|
||||
project,
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
})
|
||||
.await;
|
||||
assert_eq!(
|
||||
@@ -742,7 +778,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -774,6 +810,7 @@ mod tests {
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry.clone(),
|
||||
))
|
||||
@@ -832,11 +869,12 @@ mod tests {
|
||||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
|
||||
// Stream the unformatted content
|
||||
@@ -884,7 +922,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -917,6 +955,7 @@ mod tests {
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry.clone(),
|
||||
))
|
||||
@@ -968,11 +1007,12 @@ mod tests {
|
||||
path: "root/src/main.rs".into(),
|
||||
mode: EditFileMode::Overwrite,
|
||||
};
|
||||
Arc::new(EditFileTool::new(thread.downgrade(), language_registry)).run(
|
||||
input,
|
||||
ToolCallEventStream::test().0,
|
||||
cx,
|
||||
)
|
||||
Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
))
|
||||
.run(input, ToolCallEventStream::test().0, cx)
|
||||
});
|
||||
|
||||
// Stream the content with trailing whitespace
|
||||
@@ -1011,7 +1051,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -1019,7 +1059,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
fs.insert_tree("/root", json!({})).await;
|
||||
|
||||
// Test 1: Path with .zed component should require confirmation
|
||||
@@ -1147,7 +1191,7 @@ mod tests {
|
||||
let model = Arc::new(FakeLanguageModel::default());
|
||||
let thread = cx.new(|cx| {
|
||||
Thread::new(
|
||||
project,
|
||||
project.clone(),
|
||||
cx.new(|_cx| ProjectContext::default()),
|
||||
context_server_registry,
|
||||
Templates::new(),
|
||||
@@ -1155,7 +1199,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
// Test global config paths - these should require confirmation if they exist and are outside the project
|
||||
let test_cases = vec![
|
||||
@@ -1263,7 +1311,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
// Test files in different worktrees
|
||||
let test_cases = vec![
|
||||
@@ -1343,7 +1395,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
// Test edge cases
|
||||
let test_cases = vec![
|
||||
@@ -1426,7 +1482,11 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
// Test different EditFileMode values
|
||||
let modes = vec![
|
||||
@@ -1506,48 +1566,67 @@ mod tests {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), language_registry));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project,
|
||||
thread.downgrade(),
|
||||
language_registry,
|
||||
));
|
||||
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
}))),
|
||||
"src/main.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
"path": "",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
}))),
|
||||
"Fix error handling"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
}))),
|
||||
"Fix error handling"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(json!({
|
||||
"path": "",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
}))),
|
||||
DEFAULT_UI_TEXT
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(serde_json::Value::Null)),
|
||||
DEFAULT_UI_TEXT
|
||||
);
|
||||
cx.update(|cx| {
|
||||
// ...
|
||||
assert_eq!(
|
||||
tool.initial_title(
|
||||
Err(json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
})),
|
||||
cx
|
||||
),
|
||||
"src/main.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(
|
||||
Err(json!({
|
||||
"path": "",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
})),
|
||||
cx
|
||||
),
|
||||
"Fix error handling"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(
|
||||
Err(json!({
|
||||
"path": "src/main.rs",
|
||||
"display_description": "Fix error handling",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
})),
|
||||
cx
|
||||
),
|
||||
"src/main.rs"
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(
|
||||
Err(json!({
|
||||
"path": "",
|
||||
"display_description": "",
|
||||
"old_string": "old code",
|
||||
"new_string": "new code"
|
||||
})),
|
||||
cx
|
||||
),
|
||||
DEFAULT_UI_TEXT
|
||||
);
|
||||
assert_eq!(
|
||||
tool.initial_title(Err(serde_json::Value::Null), cx),
|
||||
DEFAULT_UI_TEXT
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -1574,7 +1653,11 @@ mod tests {
|
||||
|
||||
// Ensure the diff is finalized after the edit completes.
|
||||
{
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
@@ -1599,7 +1682,11 @@ mod tests {
|
||||
// Ensure the diff is finalized if an error occurs while editing.
|
||||
{
|
||||
model.forbid_requests();
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
@@ -1622,7 +1709,11 @@ mod tests {
|
||||
|
||||
// Ensure the diff is finalized if the tool call gets dropped.
|
||||
{
|
||||
let tool = Arc::new(EditFileTool::new(thread.downgrade(), languages.clone()));
|
||||
let tool = Arc::new(EditFileTool::new(
|
||||
project.clone(),
|
||||
thread.downgrade(),
|
||||
languages.clone(),
|
||||
));
|
||||
let (stream_tx, mut stream_rx) = ToolCallEventStream::test();
|
||||
let edit = cx.update(|cx| {
|
||||
tool.run(
|
||||
|
||||
@@ -126,7 +126,11 @@ impl AgentTool for FetchTool {
|
||||
acp::ToolKind::Fetch
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
match input {
|
||||
Ok(input) => format!("Fetch {}", MarkdownEscaped(&input.url)).into(),
|
||||
Err(_) => "Fetch URL".into(),
|
||||
|
||||
@@ -93,7 +93,11 @@ impl AgentTool for FindPathTool {
|
||||
acp::ToolKind::Search
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
let mut title = "Find paths".to_string();
|
||||
if let Ok(input) = input {
|
||||
title.push_str(&format!(" matching “`{}`”", input.glob));
|
||||
|
||||
@@ -75,7 +75,11 @@ impl AgentTool for GrepTool {
|
||||
acp::ToolKind::Search
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
match input {
|
||||
Ok(input) => {
|
||||
let page = input.page();
|
||||
|
||||
@@ -59,7 +59,11 @@ impl AgentTool for ListDirectoryTool {
|
||||
ToolKind::Read
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let path = MarkdownInlineCode(&input.path);
|
||||
format!("List the {path} directory's contents").into()
|
||||
|
||||
@@ -60,7 +60,11 @@ impl AgentTool for MovePathTool {
|
||||
ToolKind::Move
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let src = MarkdownInlineCode(&input.source_path);
|
||||
let dest = MarkdownInlineCode(&input.destination_path);
|
||||
|
||||
@@ -11,6 +11,7 @@ use crate::{AgentTool, ToolCallEventStream};
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
#[schemars(inline)]
|
||||
pub enum Timezone {
|
||||
/// Use UTC for the datetime.
|
||||
Utc,
|
||||
@@ -40,7 +41,11 @@ impl AgentTool for NowTool {
|
||||
acp::ToolKind::Other
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Get current time".into()
|
||||
}
|
||||
|
||||
|
||||
@@ -45,7 +45,11 @@ impl AgentTool for OpenTool {
|
||||
ToolKind::Execute
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
format!("Open `{}`", MarkdownEscaped(&input.path_or_url)).into()
|
||||
} else {
|
||||
@@ -61,7 +65,7 @@ impl AgentTool for OpenTool {
|
||||
) -> Task<Result<Self::Output>> {
|
||||
// If path_or_url turns out to be a path in the project, make it absolute.
|
||||
let abs_path = to_absolute_path(&input.path_or_url, self.project.clone(), cx);
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
|
||||
cx.background_spawn(async move {
|
||||
authorize.await?;
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use project::{AgentLocation, ImageItem, Project, WorktreeSettings, image_store};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::Settings;
|
||||
use std::{path::Path, sync::Arc};
|
||||
use std::sync::Arc;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
|
||||
use crate::{AgentTool, ToolCallEventStream};
|
||||
@@ -68,13 +68,31 @@ impl AgentTool for ReadFileTool {
|
||||
acp::ToolKind::Read
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
input
|
||||
.ok()
|
||||
.as_ref()
|
||||
.and_then(|input| Path::new(&input.path).file_name())
|
||||
.map(|file_name| file_name.to_string_lossy().to_string().into())
|
||||
.unwrap_or_default()
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input
|
||||
&& let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx)
|
||||
&& let Some(path) = self
|
||||
.project
|
||||
.read(cx)
|
||||
.short_full_path_for_project_path(&project_path, cx)
|
||||
{
|
||||
match (input.start_line, input.end_line) {
|
||||
(Some(start), Some(end)) => {
|
||||
format!("Read file `{}` (lines {}-{})", path.display(), start, end,)
|
||||
}
|
||||
(Some(start), None) => {
|
||||
format!("Read file `{}` (from line {})", path.display(), start)
|
||||
}
|
||||
_ => format!("Read file `{}`", path.display()),
|
||||
}
|
||||
.into()
|
||||
} else {
|
||||
"Read file".into()
|
||||
}
|
||||
}
|
||||
|
||||
fn run(
|
||||
@@ -86,6 +104,12 @@ impl AgentTool for ReadFileTool {
|
||||
let Some(project_path) = self.project.read(cx).find_project_path(&input.path, cx) else {
|
||||
return Task::ready(Err(anyhow!("Path {} not found in project", &input.path)));
|
||||
};
|
||||
let Some(abs_path) = self.project.read(cx).absolute_path(&project_path, cx) else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"Failed to convert {} to absolute path",
|
||||
&input.path
|
||||
)));
|
||||
};
|
||||
|
||||
// Error out if this path is either excluded or private in global settings
|
||||
let global_settings = WorktreeSettings::get_global(cx);
|
||||
@@ -121,6 +145,14 @@ impl AgentTool for ReadFileTool {
|
||||
|
||||
let file_path = input.path.clone();
|
||||
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
line: input.start_line.map(|line| line.saturating_sub(1)),
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
if image_store::is_image_file(&self.project, &project_path, cx) {
|
||||
return cx.spawn(async move |cx| {
|
||||
let image_entity: Entity<ImageItem> = cx
|
||||
@@ -229,34 +261,25 @@ impl AgentTool for ReadFileTool {
|
||||
};
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
if let Some(abs_path) = project.absolute_path(&project_path, cx) {
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: anchor.unwrap_or(text::Anchor::MIN),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
project.set_agent_location(
|
||||
Some(AgentLocation {
|
||||
buffer: buffer.downgrade(),
|
||||
position: anchor.unwrap_or(text::Anchor::MIN),
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
|
||||
let markdown = MarkdownCodeBlock {
|
||||
tag: &input.path,
|
||||
text,
|
||||
}
|
||||
.to_string();
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
line: input.start_line.map(|line| line.saturating_sub(1)),
|
||||
content: Some(vec![acp::ToolCallContent::Content {
|
||||
content: markdown.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
if let Ok(LanguageModelToolResultContent::Text(text)) = &result {
|
||||
let markdown = MarkdownCodeBlock {
|
||||
tag: &input.path,
|
||||
text,
|
||||
}
|
||||
.to_string();
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
content: Some(vec![acp::ToolCallContent::Content {
|
||||
content: markdown.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
@@ -60,7 +60,11 @@ impl AgentTool for TerminalTool {
|
||||
acp::ToolKind::Execute
|
||||
}
|
||||
|
||||
fn initial_title(&self, input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
if let Ok(input) = input {
|
||||
let mut lines = input.command.lines();
|
||||
let first_line = lines.next().unwrap_or_default();
|
||||
@@ -93,7 +97,7 @@ impl AgentTool for TerminalTool {
|
||||
Err(err) => return Task::ready(Err(err)),
|
||||
};
|
||||
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone())), cx);
|
||||
let authorize = event_stream.authorize(self.initial_title(Ok(input.clone()), cx), cx);
|
||||
cx.spawn(async move |cx| {
|
||||
authorize.await?;
|
||||
|
||||
|
||||
@@ -29,7 +29,11 @@ impl AgentTool for ThinkingTool {
|
||||
acp::ToolKind::Think
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Thinking".into()
|
||||
}
|
||||
|
||||
|
||||
@@ -48,7 +48,11 @@ impl AgentTool for WebSearchTool {
|
||||
acp::ToolKind::Fetch
|
||||
}
|
||||
|
||||
fn initial_title(&self, _input: Result<Self::Input, serde_json::Value>) -> SharedString {
|
||||
fn initial_title(
|
||||
&self,
|
||||
_input: Result<Self::Input, serde_json::Value>,
|
||||
_cx: &mut App,
|
||||
) -> SharedString {
|
||||
"Searching the Web".into()
|
||||
}
|
||||
|
||||
|
||||
@@ -35,22 +35,18 @@ language.workspace = true
|
||||
language_model.workspace = true
|
||||
language_models.workspace = true
|
||||
log.workspace = true
|
||||
node_runtime.workspace = true
|
||||
paths.workspace = true
|
||||
project.workspace = true
|
||||
reqwest_client = { workspace = true, optional = true }
|
||||
schemars.workspace = true
|
||||
semver.workspace = true
|
||||
serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
task.workspace = true
|
||||
tempfile.workspace = true
|
||||
thiserror.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
watch.workspace = true
|
||||
which.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[target.'cfg(unix)'.dependencies]
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use crate::AgentServerCommand;
|
||||
use acp_thread::AgentConnection;
|
||||
use acp_tools::AcpConnectionRegistry;
|
||||
use action_log::ActionLog;
|
||||
@@ -8,8 +7,10 @@ use collections::HashMap;
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use serde::Deserialize;
|
||||
|
||||
use std::path::PathBuf;
|
||||
use std::{any::Any, cell::RefCell};
|
||||
use std::{path::Path, rc::Rc};
|
||||
use thiserror::Error;
|
||||
@@ -29,6 +30,7 @@ pub struct AcpConnection {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
auth_methods: Vec<acp::AuthMethod>,
|
||||
agent_capabilities: acp::AgentCapabilities,
|
||||
root_dir: PathBuf,
|
||||
_io_task: Task<Result<()>>,
|
||||
_wait_task: Task<Result<()>>,
|
||||
_stderr_task: Task<Result<()>>,
|
||||
@@ -43,9 +45,10 @@ pub async fn connect(
|
||||
server_name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Rc<dyn AgentConnection>> {
|
||||
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
|
||||
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, is_remote, cx).await?;
|
||||
Ok(Rc::new(conn) as _)
|
||||
}
|
||||
|
||||
@@ -56,17 +59,21 @@ impl AcpConnection {
|
||||
server_name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
root_dir: &Path,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
let mut child = util::command::new_smol_command(command.path)
|
||||
let mut child = util::command::new_smol_command(command.path);
|
||||
child
|
||||
.args(command.args.iter().map(|arg| arg.as_str()))
|
||||
.envs(command.env.iter().flatten())
|
||||
.current_dir(root_dir)
|
||||
.stdin(std::process::Stdio::piped())
|
||||
.stdout(std::process::Stdio::piped())
|
||||
.stderr(std::process::Stdio::piped())
|
||||
.kill_on_drop(true)
|
||||
.spawn()?;
|
||||
.kill_on_drop(true);
|
||||
if !is_remote {
|
||||
child.current_dir(root_dir);
|
||||
}
|
||||
let mut child = child.spawn()?;
|
||||
|
||||
let stdout = child.stdout.take().context("Failed to take stdout")?;
|
||||
let stdin = child.stdin.take().context("Failed to take stdin")?;
|
||||
@@ -145,6 +152,7 @@ impl AcpConnection {
|
||||
|
||||
Ok(Self {
|
||||
auth_methods: response.auth_methods,
|
||||
root_dir: root_dir.to_owned(),
|
||||
connection,
|
||||
server_name,
|
||||
sessions,
|
||||
@@ -158,6 +166,10 @@ impl AcpConnection {
|
||||
pub fn prompt_capabilities(&self) -> &acp::PromptCapabilities {
|
||||
&self.agent_capabilities.prompt_capabilities
|
||||
}
|
||||
|
||||
pub fn root_dir(&self) -> &Path {
|
||||
&self.root_dir
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentConnection for AcpConnection {
|
||||
@@ -171,29 +183,36 @@ impl AgentConnection for AcpConnection {
|
||||
let sessions = self.sessions.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
let command = configuration.command();
|
||||
Some(acp::McpServer {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
let mcp_servers = if project.read(cx).is_local() {
|
||||
context_server_store
|
||||
.configured_server_ids()
|
||||
.iter()
|
||||
.filter_map(|id| {
|
||||
let configuration = context_server_store.configuration_for_server(id)?;
|
||||
let command = configuration.command();
|
||||
Some(acp::McpServer {
|
||||
name: id.0.to_string(),
|
||||
command: command.path.clone(),
|
||||
args: command.args.clone(),
|
||||
env: if let Some(env) = command.env.as_ref() {
|
||||
env.iter()
|
||||
.map(|(name, value)| acp::EnvVariable {
|
||||
name: name.clone(),
|
||||
value: value.clone(),
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
vec![]
|
||||
},
|
||||
})
|
||||
})
|
||||
})
|
||||
.collect();
|
||||
.collect()
|
||||
} else {
|
||||
// In SSH projects, the external agent is running on the remote
|
||||
// machine, and currently we only run MCP servers on the local
|
||||
// machine. So don't pass any MCP servers to the agent in that case.
|
||||
Vec::new()
|
||||
};
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let response = conn
|
||||
|
||||
@@ -2,47 +2,25 @@ mod acp;
|
||||
mod claude;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
mod settings;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod e2e_tests;
|
||||
|
||||
use anyhow::Context as _;
|
||||
pub use claude::*;
|
||||
pub use custom::*;
|
||||
use fs::Fs;
|
||||
use fs::RemoveOptions;
|
||||
use fs::RenameOptions;
|
||||
use futures::StreamExt as _;
|
||||
pub use gemini::*;
|
||||
use gpui::AppContext;
|
||||
use node_runtime::NodeRuntime;
|
||||
pub use settings::*;
|
||||
use project::agent_server_store::AgentServerStore;
|
||||
|
||||
use acp_thread::AgentConnection;
|
||||
use acp_thread::LoadError;
|
||||
use anyhow::Result;
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AsyncApp, Entity, SharedString, Task};
|
||||
use gpui::{App, Entity, SharedString, Task};
|
||||
use project::Project;
|
||||
use schemars::JsonSchema;
|
||||
use semver::Version;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::str::FromStr as _;
|
||||
use std::{
|
||||
any::Any,
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use std::{any::Any, path::Path, rc::Rc};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
settings::init(cx);
|
||||
}
|
||||
pub use acp::AcpConnection;
|
||||
|
||||
pub struct AgentServerDelegate {
|
||||
store: Entity<AgentServerStore>,
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_available: Option<watch::Sender<Option<String>>>,
|
||||
@@ -50,11 +28,13 @@ pub struct AgentServerDelegate {
|
||||
|
||||
impl AgentServerDelegate {
|
||||
pub fn new(
|
||||
store: Entity<AgentServerStore>,
|
||||
project: Entity<Project>,
|
||||
status_tx: Option<watch::Sender<SharedString>>,
|
||||
new_version_tx: Option<watch::Sender<Option<String>>>,
|
||||
) -> Self {
|
||||
Self {
|
||||
store,
|
||||
project,
|
||||
status_tx,
|
||||
new_version_available: new_version_tx,
|
||||
@@ -64,188 +44,6 @@ impl AgentServerDelegate {
|
||||
pub fn project(&self) -> &Entity<Project> {
|
||||
&self.project
|
||||
}
|
||||
|
||||
fn get_or_npm_install_builtin_agent(
|
||||
self,
|
||||
binary_name: SharedString,
|
||||
package_name: SharedString,
|
||||
entrypoint_path: PathBuf,
|
||||
ignore_system_version: bool,
|
||||
minimum_version: Option<Version>,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<AgentServerCommand>> {
|
||||
let project = self.project;
|
||||
let fs = project.read(cx).fs().clone();
|
||||
let Some(node_runtime) = project.read(cx).node_runtime().cloned() else {
|
||||
return Task::ready(Err(anyhow!(
|
||||
"External agents are not yet available in remote projects."
|
||||
)));
|
||||
};
|
||||
let status_tx = self.status_tx;
|
||||
let new_version_available = self.new_version_available;
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
if !ignore_system_version {
|
||||
if let Some(bin) = find_bin_in_path(binary_name.clone(), &project, cx).await {
|
||||
return Ok(AgentServerCommand {
|
||||
path: bin,
|
||||
args: Vec::new(),
|
||||
env: Default::default(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let node_path = node_runtime.binary_path().await?;
|
||||
let dir = paths::data_dir()
|
||||
.join("external_agents")
|
||||
.join(binary_name.as_str());
|
||||
fs.create_dir(&dir).await?;
|
||||
|
||||
let mut stream = fs.read_dir(&dir).await?;
|
||||
let mut versions = Vec::new();
|
||||
let mut to_delete = Vec::new();
|
||||
while let Some(entry) = stream.next().await {
|
||||
let Ok(entry) = entry else { continue };
|
||||
let Some(file_name) = entry.file_name() else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(name) = file_name.to_str()
|
||||
&& let Some(version) = semver::Version::from_str(name).ok()
|
||||
&& fs
|
||||
.is_file(&dir.join(file_name).join(&entrypoint_path))
|
||||
.await
|
||||
{
|
||||
versions.push((version, file_name.to_owned()));
|
||||
} else {
|
||||
to_delete.push(file_name.to_owned())
|
||||
}
|
||||
}
|
||||
|
||||
versions.sort();
|
||||
let newest_version = if let Some((version, file_name)) = versions.last().cloned()
|
||||
&& minimum_version.is_none_or(|minimum_version| version >= minimum_version)
|
||||
{
|
||||
versions.pop();
|
||||
Some(file_name)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
log::debug!("existing version of {package_name}: {newest_version:?}");
|
||||
to_delete.extend(versions.into_iter().map(|(_, file_name)| file_name));
|
||||
|
||||
cx.background_spawn({
|
||||
let fs = fs.clone();
|
||||
let dir = dir.clone();
|
||||
async move {
|
||||
for file_name in to_delete {
|
||||
fs.remove_dir(
|
||||
&dir.join(file_name),
|
||||
RemoveOptions {
|
||||
recursive: true,
|
||||
ignore_if_not_exists: false,
|
||||
},
|
||||
)
|
||||
.await
|
||||
.ok();
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let version = if let Some(file_name) = newest_version {
|
||||
cx.background_spawn({
|
||||
let file_name = file_name.clone();
|
||||
let dir = dir.clone();
|
||||
let fs = fs.clone();
|
||||
async move {
|
||||
let latest_version =
|
||||
node_runtime.npm_package_latest_version(&package_name).await;
|
||||
if let Ok(latest_version) = latest_version
|
||||
&& &latest_version != &file_name.to_string_lossy()
|
||||
{
|
||||
Self::download_latest_version(
|
||||
fs,
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name,
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
if let Some(mut new_version_available) = new_version_available {
|
||||
new_version_available.send(Some(latest_version)).ok();
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
file_name
|
||||
} else {
|
||||
if let Some(mut status_tx) = status_tx {
|
||||
status_tx.send("Installing…".into()).ok();
|
||||
}
|
||||
let dir = dir.clone();
|
||||
cx.background_spawn(Self::download_latest_version(
|
||||
fs.clone(),
|
||||
dir.clone(),
|
||||
node_runtime,
|
||||
package_name,
|
||||
))
|
||||
.await?
|
||||
.into()
|
||||
};
|
||||
|
||||
let agent_server_path = dir.join(version).join(entrypoint_path);
|
||||
let agent_server_path_exists = fs.is_file(&agent_server_path).await;
|
||||
anyhow::ensure!(
|
||||
agent_server_path_exists,
|
||||
"Missing entrypoint path {} after installation",
|
||||
agent_server_path.to_string_lossy()
|
||||
);
|
||||
|
||||
anyhow::Ok(AgentServerCommand {
|
||||
path: node_path,
|
||||
args: vec![agent_server_path.to_string_lossy().to_string()],
|
||||
env: Default::default(),
|
||||
})
|
||||
})
|
||||
.await
|
||||
.map_err(|e| LoadError::FailedToInstall(e.to_string().into()).into())
|
||||
})
|
||||
}
|
||||
|
||||
async fn download_latest_version(
|
||||
fs: Arc<dyn Fs>,
|
||||
dir: PathBuf,
|
||||
node_runtime: NodeRuntime,
|
||||
package_name: SharedString,
|
||||
) -> Result<String> {
|
||||
log::debug!("downloading latest version of {package_name}");
|
||||
|
||||
let tmp_dir = tempfile::tempdir_in(&dir)?;
|
||||
|
||||
node_runtime
|
||||
.npm_install_packages(tmp_dir.path(), &[(&package_name, "latest")])
|
||||
.await?;
|
||||
|
||||
let version = node_runtime
|
||||
.npm_package_installed_version(tmp_dir.path(), &package_name)
|
||||
.await?
|
||||
.context("expected package to be installed")?;
|
||||
|
||||
fs.rename(
|
||||
&tmp_dir.keep(),
|
||||
&dir.join(&version),
|
||||
RenameOptions {
|
||||
ignore_if_exists: true,
|
||||
overwrite: false,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
|
||||
anyhow::Ok(version)
|
||||
}
|
||||
}
|
||||
|
||||
pub trait AgentServer: Send {
|
||||
@@ -255,10 +53,10 @@ pub trait AgentServer: Send {
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>>;
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>;
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
@@ -268,120 +66,3 @@ impl dyn AgentServer {
|
||||
self.into_any().downcast().ok()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for AgentServerCommand {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let filtered_env = self.env.as_ref().map(|env| {
|
||||
env.iter()
|
||||
.map(|(k, v)| {
|
||||
(
|
||||
k,
|
||||
if util::redact::should_redact(k) {
|
||||
"[REDACTED]"
|
||||
} else {
|
||||
v
|
||||
},
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
|
||||
f.debug_struct("AgentServerCommand")
|
||||
.field("path", &self.path)
|
||||
.field("args", &self.args)
|
||||
.field("env", &filtered_env)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, PartialEq, Eq, JsonSchema)]
|
||||
pub struct AgentServerCommand {
|
||||
#[serde(rename = "command")]
|
||||
pub path: PathBuf,
|
||||
#[serde(default)]
|
||||
pub args: Vec<String>,
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
}
|
||||
|
||||
impl AgentServerCommand {
|
||||
pub async fn resolve(
|
||||
path_bin_name: &'static str,
|
||||
extra_args: &[&'static str],
|
||||
fallback_path: Option<&Path>,
|
||||
settings: Option<BuiltinAgentServerSettings>,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<Self> {
|
||||
if let Some(settings) = settings
|
||||
&& let Some(command) = settings.custom_command()
|
||||
{
|
||||
Some(command)
|
||||
} else {
|
||||
match find_bin_in_path(path_bin_name.into(), project, cx).await {
|
||||
Some(path) => Some(Self {
|
||||
path,
|
||||
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
|
||||
env: None,
|
||||
}),
|
||||
None => fallback_path.and_then(|path| {
|
||||
if path.exists() {
|
||||
Some(Self {
|
||||
path: path.to_path_buf(),
|
||||
args: extra_args.iter().map(|arg| arg.to_string()).collect(),
|
||||
env: None,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn find_bin_in_path(
|
||||
bin_name: SharedString,
|
||||
project: &Entity<Project>,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Option<PathBuf> {
|
||||
let (env_task, root_dir) = project
|
||||
.update(cx, |project, cx| {
|
||||
let worktree = project.visible_worktrees(cx).next();
|
||||
match worktree {
|
||||
Some(worktree) => {
|
||||
let env_task = project.environment().update(cx, |env, cx| {
|
||||
env.get_worktree_environment(worktree.clone(), cx)
|
||||
});
|
||||
|
||||
let path = worktree.read(cx).abs_path();
|
||||
(env_task, path)
|
||||
}
|
||||
None => {
|
||||
let path: Arc<Path> = paths::home_dir().as_path().into();
|
||||
let env_task = project.environment().update(cx, |env, cx| {
|
||||
env.get_directory_environment(path.clone(), cx)
|
||||
});
|
||||
(env_task, path)
|
||||
}
|
||||
}
|
||||
})
|
||||
.log_err()?;
|
||||
|
||||
cx.background_executor()
|
||||
.spawn(async move {
|
||||
let which_result = if cfg!(windows) {
|
||||
which::which(bin_name.as_str())
|
||||
} else {
|
||||
let env = env_task.await.unwrap_or_default();
|
||||
let shell_path = env.get("PATH").cloned();
|
||||
which::which_in(bin_name.as_str(), shell_path.as_ref(), root_dir.as_ref())
|
||||
};
|
||||
|
||||
if let Err(which::Error::CannotFindBinaryPath) = which_result {
|
||||
return None;
|
||||
}
|
||||
|
||||
which_result.log_err()
|
||||
})
|
||||
.await
|
||||
}
|
||||
|
||||
@@ -1,61 +1,22 @@
|
||||
use language_models::provider::anthropic::AnthropicLanguageModelProvider;
|
||||
use settings::SettingsStore;
|
||||
use std::path::Path;
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, path::PathBuf};
|
||||
|
||||
use anyhow::Result;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use project::agent_server_store::CLAUDE_CODE_NAME;
|
||||
|
||||
use crate::{AgentServer, AgentServerDelegate, AllAgentServersSettings};
|
||||
use crate::{AgentServer, AgentServerDelegate};
|
||||
use acp_thread::AgentConnection;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ClaudeCode;
|
||||
|
||||
pub struct ClaudeCodeLoginCommand {
|
||||
pub struct AgentServerLoginCommand {
|
||||
pub path: PathBuf,
|
||||
pub arguments: Vec<String>,
|
||||
}
|
||||
|
||||
impl ClaudeCode {
|
||||
const BINARY_NAME: &'static str = "claude-code-acp";
|
||||
const PACKAGE_NAME: &'static str = "@zed-industries/claude-code-acp";
|
||||
|
||||
pub fn login_command(
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<ClaudeCodeLoginCommand>> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut command = if let Some(settings) = settings {
|
||||
settings.command
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
delegate.get_or_npm_install_builtin_agent(
|
||||
Self::BINARY_NAME.into(),
|
||||
Self::PACKAGE_NAME.into(),
|
||||
"node_modules/@anthropic-ai/claude-code/cli.js".into(),
|
||||
true,
|
||||
Some("0.2.5".parse().unwrap()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
command.args.push("/login".into());
|
||||
|
||||
Ok(ClaudeCodeLoginCommand {
|
||||
path: command.path,
|
||||
arguments: command.args,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl AgentServer for ClaudeCode {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"claude-code"
|
||||
@@ -71,53 +32,33 @@ impl AgentServer for ClaudeCode {
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let fs = delegate.project().read(cx).fs().clone();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let mut command = if let Some(settings) = settings {
|
||||
settings.command
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
delegate.get_or_npm_install_builtin_agent(
|
||||
Self::BINARY_NAME.into(),
|
||||
Self::PACKAGE_NAME.into(),
|
||||
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
|
||||
true,
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
|
||||
if let Some(api_key) = cx
|
||||
.update(AnthropicLanguageModelProvider::api_key)?
|
||||
.await
|
||||
.ok()
|
||||
{
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("ANTHROPIC_API_KEY".to_owned(), api_key.key);
|
||||
}
|
||||
|
||||
let root_dir_exists = fs.is_dir(&root_dir).await;
|
||||
anyhow::ensure!(
|
||||
root_dir_exists,
|
||||
"Session root {} does not exist or is not a directory",
|
||||
root_dir.to_string_lossy()
|
||||
);
|
||||
|
||||
crate::acp::connect(server_name, command.clone(), &root_dir, cx).await
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
let agent = store
|
||||
.get_external_agent(&CLAUDE_CODE_NAME.into())
|
||||
.context("Claude Code is not registered")?;
|
||||
anyhow::Ok(agent.get_command(
|
||||
root_dir.as_deref(),
|
||||
Default::default(),
|
||||
delegate.status_tx,
|
||||
delegate.new_version_available,
|
||||
&mut cx.to_async(),
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
let connection =
|
||||
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
|
||||
Ok((connection, login))
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
use crate::{AgentServerCommand, AgentServerDelegate};
|
||||
use crate::AgentServerDelegate;
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::Result;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use project::agent_server_store::ExternalAgentServerName;
|
||||
use std::{path::Path, rc::Rc};
|
||||
use ui::IconName;
|
||||
|
||||
/// A generic agent server implementation for custom user-defined agents
|
||||
pub struct CustomAgentServer {
|
||||
name: SharedString,
|
||||
command: AgentServerCommand,
|
||||
}
|
||||
|
||||
impl CustomAgentServer {
|
||||
pub fn new(name: SharedString, command: AgentServerCommand) -> Self {
|
||||
Self { name, command }
|
||||
pub fn new(name: SharedString) -> Self {
|
||||
Self { name }
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,14 +32,36 @@ impl crate::AgentServer for CustomAgentServer {
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
_delegate: AgentServerDelegate,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let server_name = self.name();
|
||||
let command = self.command.clone();
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
cx.spawn(async move |cx| crate::acp::connect(server_name, command, &root_dir, cx).await)
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
let agent = store
|
||||
.get_external_agent(&ExternalAgentServerName(name.clone()))
|
||||
.with_context(|| {
|
||||
format!("Custom agent server `{}` is not registered", name)
|
||||
})?;
|
||||
anyhow::Ok(agent.get_command(
|
||||
root_dir.as_deref(),
|
||||
Default::default(),
|
||||
delegate.status_tx,
|
||||
delegate.new_version_available,
|
||||
&mut cx.to_async(),
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
let connection =
|
||||
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
|
||||
Ok((connection, login))
|
||||
})
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
use crate::{AgentServer, AgentServerDelegate};
|
||||
#[cfg(test)]
|
||||
use crate::{AgentServerCommand, CustomAgentServerSettings};
|
||||
use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
|
||||
use agent_client_protocol as acp;
|
||||
use futures::{FutureExt, StreamExt, channel::mpsc, select};
|
||||
use gpui::{AppContext, Entity, TestAppContext};
|
||||
use indoc::indoc;
|
||||
use project::{FakeFs, Project};
|
||||
#[cfg(test)]
|
||||
use project::agent_server_store::{AgentServerCommand, CustomAgentServerSettings};
|
||||
use project::{FakeFs, Project, agent_server_store::AllAgentServersSettings};
|
||||
use std::{
|
||||
path::{Path, PathBuf},
|
||||
sync::Arc,
|
||||
@@ -449,7 +449,6 @@ pub use common_e2e_tests;
|
||||
// Helpers
|
||||
|
||||
pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
#[cfg(test)]
|
||||
use settings::Settings;
|
||||
|
||||
env_logger::try_init().ok();
|
||||
@@ -468,11 +467,11 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
language_model::init(client.clone(), cx);
|
||||
language_models::init(user_store, client, cx);
|
||||
agent_settings::init(cx);
|
||||
crate::settings::init(cx);
|
||||
AllAgentServersSettings::register(cx);
|
||||
|
||||
#[cfg(test)]
|
||||
crate::AllAgentServersSettings::override_global(
|
||||
crate::AllAgentServersSettings {
|
||||
AllAgentServersSettings::override_global(
|
||||
AllAgentServersSettings {
|
||||
claude: Some(CustomAgentServerSettings {
|
||||
command: AgentServerCommand {
|
||||
path: "claude-code-acp".into(),
|
||||
@@ -498,10 +497,11 @@ pub async fn new_test_thread(
|
||||
current_dir: impl AsRef<Path>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Entity<AcpThread> {
|
||||
let delegate = AgentServerDelegate::new(project.clone(), None, None);
|
||||
let store = project.read_with(cx, |project, _| project.agent_server_store().clone());
|
||||
let delegate = AgentServerDelegate::new(store, project.clone(), None, None);
|
||||
|
||||
let connection = cx
|
||||
.update(|cx| server.connect(current_dir.as_ref(), delegate, cx))
|
||||
let (connection, _) = cx
|
||||
.update(|cx| server.connect(Some(current_dir.as_ref()), delegate, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
|
||||
@@ -1,21 +1,17 @@
|
||||
use std::rc::Rc;
|
||||
use std::{any::Any, path::Path};
|
||||
|
||||
use crate::acp::AcpConnection;
|
||||
use crate::{AgentServer, AgentServerDelegate};
|
||||
use acp_thread::{AgentConnection, LoadError};
|
||||
use anyhow::Result;
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::{Context as _, Result};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, SharedString, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use settings::SettingsStore;
|
||||
|
||||
use crate::AllAgentServersSettings;
|
||||
use project::agent_server_store::GEMINI_NAME;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gemini;
|
||||
|
||||
const ACP_ARG: &str = "--experimental-acp";
|
||||
|
||||
impl AgentServer for Gemini {
|
||||
fn telemetry_id(&self) -> &'static str {
|
||||
"gemini-cli"
|
||||
@@ -31,120 +27,37 @@ impl AgentServer for Gemini {
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: &Path,
|
||||
root_dir: Option<&Path>,
|
||||
delegate: AgentServerDelegate,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Rc<dyn AgentConnection>>> {
|
||||
let root_dir = root_dir.to_path_buf();
|
||||
let fs = delegate.project().read(cx).fs().clone();
|
||||
let server_name = self.name();
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).gemini.clone()
|
||||
});
|
||||
) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
let name = self.name();
|
||||
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let store = delegate.store.downgrade();
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let ignore_system_version = settings
|
||||
.as_ref()
|
||||
.and_then(|settings| settings.ignore_system_version)
|
||||
.unwrap_or(true);
|
||||
let mut command = if let Some(settings) = settings
|
||||
&& let Some(command) = settings.custom_command()
|
||||
{
|
||||
command
|
||||
} else {
|
||||
cx.update(|cx| {
|
||||
delegate.get_or_npm_install_builtin_agent(
|
||||
Self::BINARY_NAME.into(),
|
||||
Self::PACKAGE_NAME.into(),
|
||||
format!("node_modules/{}/dist/index.js", Self::PACKAGE_NAME).into(),
|
||||
ignore_system_version,
|
||||
Some(Self::MINIMUM_VERSION.parse().unwrap()),
|
||||
cx,
|
||||
)
|
||||
})?
|
||||
.await?
|
||||
};
|
||||
if !command.args.contains(&ACP_ARG.into()) {
|
||||
command.args.push(ACP_ARG.into());
|
||||
}
|
||||
|
||||
let mut extra_env = HashMap::default();
|
||||
if let Some(api_key) = cx.update(GoogleLanguageModelProvider::api_key)?.await.ok() {
|
||||
command
|
||||
.env
|
||||
.get_or_insert_default()
|
||||
.insert("GEMINI_API_KEY".to_owned(), api_key.key);
|
||||
extra_env.insert("GEMINI_API_KEY".into(), api_key.key);
|
||||
}
|
||||
|
||||
let root_dir_exists = fs.is_dir(&root_dir).await;
|
||||
anyhow::ensure!(
|
||||
root_dir_exists,
|
||||
"Session root {} does not exist or is not a directory",
|
||||
root_dir.to_string_lossy()
|
||||
);
|
||||
|
||||
let result = crate::acp::connect(server_name, command.clone(), &root_dir, cx).await;
|
||||
match &result {
|
||||
Ok(connection) => {
|
||||
if let Some(connection) = connection.clone().downcast::<AcpConnection>()
|
||||
&& !connection.prompt_capabilities().image
|
||||
{
|
||||
let version_output = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output()
|
||||
.await;
|
||||
let current_version =
|
||||
String::from_utf8(version_output?.stdout)?.trim().to_owned();
|
||||
|
||||
log::error!("connected to gemini, but missing prompt_capabilities.image (version is {current_version})");
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
let version_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--version")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let help_fut = util::command::new_smol_command(&command.path)
|
||||
.args(command.args.iter())
|
||||
.arg("--help")
|
||||
.kill_on_drop(true)
|
||||
.output();
|
||||
|
||||
let (version_output, help_output) =
|
||||
futures::future::join(version_fut, help_fut).await;
|
||||
let Some(version_output) = version_output.ok().and_then(|output| String::from_utf8(output.stdout).ok()) else {
|
||||
return result;
|
||||
};
|
||||
let Some((help_stdout, help_stderr)) = help_output.ok().and_then(|output| String::from_utf8(output.stdout).ok().zip(String::from_utf8(output.stderr).ok())) else {
|
||||
return result;
|
||||
};
|
||||
|
||||
let current_version = version_output.trim().to_string();
|
||||
let supported = help_stdout.contains(ACP_ARG) || current_version.parse::<semver::Version>().is_ok_and(|version| version >= Self::MINIMUM_VERSION.parse::<semver::Version>().unwrap());
|
||||
|
||||
log::error!("failed to create ACP connection to gemini (version is {current_version}, supported: {supported}): {e}");
|
||||
log::debug!("gemini --help stdout: {help_stdout:?}");
|
||||
log::debug!("gemini --help stderr: {help_stderr:?}");
|
||||
if !supported {
|
||||
return Err(LoadError::Unsupported {
|
||||
current_version: current_version.into(),
|
||||
command: (command.path.to_string_lossy().to_string() + " " + &command.args.join(" ")).into(),
|
||||
minimum_version: Self::MINIMUM_VERSION.into(),
|
||||
}
|
||||
.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
result
|
||||
let (command, root_dir, login) = store
|
||||
.update(cx, |store, cx| {
|
||||
let agent = store
|
||||
.get_external_agent(&GEMINI_NAME.into())
|
||||
.context("Gemini CLI is not registered")?;
|
||||
anyhow::Ok(agent.get_command(
|
||||
root_dir.as_deref(),
|
||||
extra_env,
|
||||
delegate.status_tx,
|
||||
delegate.new_version_available,
|
||||
&mut cx.to_async(),
|
||||
))
|
||||
})??
|
||||
.await?;
|
||||
let connection =
|
||||
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
|
||||
Ok((connection, login))
|
||||
})
|
||||
}
|
||||
|
||||
@@ -153,18 +66,11 @@ impl AgentServer for Gemini {
|
||||
}
|
||||
}
|
||||
|
||||
impl Gemini {
|
||||
const PACKAGE_NAME: &str = "@google/gemini-cli";
|
||||
|
||||
const MINIMUM_VERSION: &str = "0.2.1";
|
||||
|
||||
const BINARY_NAME: &str = "gemini";
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) mod tests {
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
|
||||
use super::*;
|
||||
use crate::AgentServerCommand;
|
||||
use std::path::Path;
|
||||
|
||||
crate::common_e2e_tests!(async |_, _, _| Gemini, allow_option_id = "proceed_once");
|
||||
|
||||
@@ -1,111 +0,0 @@
|
||||
use std::path::PathBuf;
|
||||
|
||||
use crate::AgentServerCommand;
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, SharedString};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
|
||||
pub fn init(cx: &mut App) {
|
||||
AllAgentServersSettings::register(cx);
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct AllAgentServersSettings {
|
||||
pub gemini: Option<BuiltinAgentServerSettings>,
|
||||
pub claude: Option<CustomAgentServerSettings>,
|
||||
|
||||
/// Custom agent servers configured by the user
|
||||
#[serde(flatten)]
|
||||
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
|
||||
}
|
||||
|
||||
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct BuiltinAgentServerSettings {
|
||||
/// Absolute path to a binary to be used when launching this agent.
|
||||
///
|
||||
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
|
||||
#[serde(rename = "command")]
|
||||
pub path: Option<PathBuf>,
|
||||
/// If a binary is specified in `command`, it will be passed these arguments.
|
||||
pub args: Option<Vec<String>>,
|
||||
/// If a binary is specified in `command`, it will be passed these environment variables.
|
||||
pub env: Option<HashMap<String, String>>,
|
||||
/// Whether to skip searching `$PATH` for an agent server binary when
|
||||
/// launching this agent.
|
||||
///
|
||||
/// This has no effect if a `command` is specified. Otherwise, when this is
|
||||
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
|
||||
/// is found, use it for threads with this agent. If no agent binary is
|
||||
/// found on `$PATH`, Zed will automatically install and use its own binary.
|
||||
/// When this is `true`, Zed will not search `$PATH`, and will always use
|
||||
/// its own binary.
|
||||
///
|
||||
/// Default: true
|
||||
pub ignore_system_version: Option<bool>,
|
||||
}
|
||||
|
||||
impl BuiltinAgentServerSettings {
|
||||
pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
|
||||
self.path.map(|path| AgentServerCommand {
|
||||
path,
|
||||
args: self.args.unwrap_or_default(),
|
||||
env: self.env,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
|
||||
fn from(value: AgentServerCommand) -> Self {
|
||||
BuiltinAgentServerSettings {
|
||||
path: Some(value.path),
|
||||
args: Some(value.args),
|
||||
env: value.env,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
|
||||
pub struct CustomAgentServerSettings {
|
||||
#[serde(flatten)]
|
||||
pub command: AgentServerCommand,
|
||||
}
|
||||
|
||||
impl settings::Settings for AllAgentServersSettings {
|
||||
const KEY: Option<&'static str> = Some("agent_servers");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
let mut settings = AllAgentServersSettings::default();
|
||||
|
||||
for AllAgentServersSettings {
|
||||
gemini,
|
||||
claude,
|
||||
custom,
|
||||
} in sources.defaults_and_customizations()
|
||||
{
|
||||
if gemini.is_some() {
|
||||
settings.gemini = gemini.clone();
|
||||
}
|
||||
if claude.is_some() {
|
||||
settings.claude = claude.clone();
|
||||
}
|
||||
|
||||
// Merge custom agents
|
||||
for (name, config) in custom {
|
||||
// Skip built-in agent names to avoid conflicts
|
||||
if name != "gemini" && name != "claude" {
|
||||
settings.custom.insert(name.clone(), config.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(settings)
|
||||
}
|
||||
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
@@ -8,7 +8,7 @@ use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
@@ -223,7 +223,8 @@ impl AgentSettingsContent {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi)]
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "agent", fallback_key = "assistant")]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
@@ -399,10 +400,6 @@ pub struct ContextServerPresetContent {
|
||||
}
|
||||
|
||||
impl Settings for AgentSettings {
|
||||
const KEY: Option<&'static str> = Some("agent");
|
||||
|
||||
const FALLBACK_KEY: Option<&'static str> = Some("assistant");
|
||||
|
||||
const PRESERVED_KEYS: Option<&'static [&'static str]> = Some(&["version"]);
|
||||
|
||||
type FileContent = AgentSettingsContent;
|
||||
|
||||
@@ -1025,43 +1025,31 @@ impl SlashCommandCompletion {
|
||||
return None;
|
||||
}
|
||||
|
||||
let last_command_start = line.rfind('/')?;
|
||||
if last_command_start >= line.len() {
|
||||
return Some(Self::default());
|
||||
}
|
||||
if last_command_start > 0
|
||||
&& line
|
||||
.chars()
|
||||
.nth(last_command_start - 1)
|
||||
.is_some_and(|c| !c.is_whitespace())
|
||||
let (prefix, last_command) = line.rsplit_once('/')?;
|
||||
if prefix.chars().last().is_some_and(|c| !c.is_whitespace())
|
||||
|| last_command.starts_with(char::is_whitespace)
|
||||
{
|
||||
return None;
|
||||
}
|
||||
|
||||
let rest_of_line = &line[last_command_start + 1..];
|
||||
|
||||
let mut command = None;
|
||||
let mut argument = None;
|
||||
let mut end = last_command_start + 1;
|
||||
|
||||
if let Some(command_text) = rest_of_line.split_whitespace().next() {
|
||||
command = Some(command_text.to_string());
|
||||
end += command_text.len();
|
||||
|
||||
// Find the start of arguments after the command
|
||||
if let Some(args_start) =
|
||||
rest_of_line[command_text.len()..].find(|c: char| !c.is_whitespace())
|
||||
{
|
||||
let args = &rest_of_line[command_text.len() + args_start..].trim_end();
|
||||
if !args.is_empty() {
|
||||
argument = Some(args.to_string());
|
||||
end += args.len() + 1;
|
||||
}
|
||||
let mut command = None;
|
||||
if let Some((command_text, args)) = last_command.split_once(char::is_whitespace) {
|
||||
if !args.is_empty() {
|
||||
argument = Some(args.trim_end().to_string());
|
||||
}
|
||||
}
|
||||
command = Some(command_text.to_string());
|
||||
} else if !last_command.is_empty() {
|
||||
command = Some(last_command.to_string());
|
||||
};
|
||||
|
||||
Some(Self {
|
||||
source_range: last_command_start + offset_to_line..end + offset_to_line,
|
||||
source_range: prefix.len() + offset_to_line
|
||||
..line
|
||||
.rfind(|c: char| !c.is_whitespace())
|
||||
.unwrap_or_else(|| line.len())
|
||||
+ 1
|
||||
+ offset_to_line,
|
||||
command,
|
||||
argument,
|
||||
})
|
||||
@@ -1180,6 +1168,15 @@ mod tests {
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
SlashCommandCompletion::try_parse("/拿不到命令 拿不到命令 ", 0),
|
||||
Some(SlashCommandCompletion {
|
||||
source_range: 0..30,
|
||||
command: Some("拿不到命令".to_string()),
|
||||
argument: Some("拿不到命令".to_string()),
|
||||
})
|
||||
);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem Ipsum", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem /", 0), None);
|
||||
@@ -1187,6 +1184,8 @@ mod tests {
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem /help", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("Lorem/", 0), None);
|
||||
|
||||
assert_eq!(SlashCommandCompletion::try_parse("/ ", 0), None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
@@ -207,7 +207,7 @@ impl EntryViewState {
|
||||
self.entries.drain(range);
|
||||
}
|
||||
|
||||
pub fn settings_changed(&mut self, cx: &mut App) {
|
||||
pub fn agent_font_size_changed(&mut self, cx: &mut App) {
|
||||
for entry in self.entries.iter() {
|
||||
match entry {
|
||||
Entry::UserMessage { .. } | Entry::AssistantMessage { .. } => {}
|
||||
|
||||
@@ -493,14 +493,13 @@ impl MessageEditor {
|
||||
let Some(entry) = self.project.read(cx).entry_for_path(&project_path, cx) else {
|
||||
return Task::ready(Err(anyhow!("project entry not found")));
|
||||
};
|
||||
let Some(worktree) = self.project.read(cx).worktree_for_entry(entry.id, cx) else {
|
||||
let directory_path = entry.path.clone();
|
||||
let worktree_id = project_path.worktree_id;
|
||||
let Some(worktree) = self.project.read(cx).worktree_for_id(worktree_id, cx) else {
|
||||
return Task::ready(Err(anyhow!("worktree not found")));
|
||||
};
|
||||
let project = self.project.clone();
|
||||
cx.spawn(async move |_, cx| {
|
||||
let directory_path = entry.path.clone();
|
||||
|
||||
let worktree_id = worktree.read_with(cx, |worktree, _| worktree.id())?;
|
||||
let file_paths = worktree.read_with(cx, |worktree, _cx| {
|
||||
collect_files_in_path(worktree, &directory_path)
|
||||
})?;
|
||||
@@ -700,10 +699,15 @@ impl MessageEditor {
|
||||
self.project.read(cx).fs().clone(),
|
||||
self.history_store.clone(),
|
||||
));
|
||||
let delegate = AgentServerDelegate::new(self.project.clone(), None, None);
|
||||
let connection = server.connect(Path::new(""), delegate, cx);
|
||||
let delegate = AgentServerDelegate::new(
|
||||
self.project.read(cx).agent_server_store().clone(),
|
||||
self.project.clone(),
|
||||
None,
|
||||
None,
|
||||
);
|
||||
let connection = server.connect(None, delegate, cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
let agent = connection.await?;
|
||||
let (agent, _) = connection.await?;
|
||||
let agent = agent.downcast::<agent2::NativeAgentConnection>().unwrap();
|
||||
let summary = agent
|
||||
.0
|
||||
|
||||
@@ -192,8 +192,10 @@ impl PickerDelegate for AcpModelPickerDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.emit(DismissEvent);
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
|
||||
@@ -6,7 +6,7 @@ use acp_thread::{
|
||||
use acp_thread::{AgentConnection, Plan};
|
||||
use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, PromptCapabilities};
|
||||
use agent_servers::{AgentServer, AgentServerDelegate, ClaudeCode};
|
||||
use agent_servers::{AgentServer, AgentServerDelegate};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, CompletionMode, NotifyWhenAgentWaiting};
|
||||
use agent2::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
@@ -40,10 +40,9 @@ use std::path::Path;
|
||||
use std::sync::Arc;
|
||||
use std::time::Instant;
|
||||
use std::{collections::BTreeMap, rc::Rc, time::Duration};
|
||||
use task::SpawnInTerminal;
|
||||
use terminal_view::terminal_panel::TerminalPanel;
|
||||
use text::Anchor;
|
||||
use theme::ThemeSettings;
|
||||
use theme::{AgentFontSize, ThemeSettings};
|
||||
use ui::{
|
||||
Callout, CommonAnimationExt, Disclosure, Divider, DividerColor, ElevationIndex, KeyBinding,
|
||||
PopoverMenuHandle, Scrollbar, ScrollbarState, SpinnerLabel, TintColor, Tooltip, prelude::*,
|
||||
@@ -263,6 +262,7 @@ pub struct AcpThreadView {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
thread_state: ThreadState,
|
||||
login: Option<task::SpawnInTerminal>,
|
||||
history_store: Entity<HistoryStore>,
|
||||
hovered_recent_history_item: Option<usize>,
|
||||
entry_view_state: Entity<EntryViewState>,
|
||||
@@ -290,7 +290,7 @@ pub struct AcpThreadView {
|
||||
is_loading_contents: bool,
|
||||
new_server_version_available: Option<SharedString>,
|
||||
_cancel_task: Option<Task<()>>,
|
||||
_subscriptions: [Subscription; 3],
|
||||
_subscriptions: [Subscription; 4],
|
||||
}
|
||||
|
||||
enum ThreadState {
|
||||
@@ -380,7 +380,8 @@ impl AcpThreadView {
|
||||
});
|
||||
|
||||
let subscriptions = [
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::settings_changed),
|
||||
cx.observe_global_in::<SettingsStore>(window, Self::agent_font_size_changed),
|
||||
cx.observe_global_in::<AgentFontSize>(window, Self::agent_font_size_changed),
|
||||
cx.subscribe_in(&message_editor, window, Self::handle_message_editor_event),
|
||||
cx.subscribe_in(&entry_view_state, window, Self::handle_entry_view_event),
|
||||
];
|
||||
@@ -391,6 +392,7 @@ impl AcpThreadView {
|
||||
project: project.clone(),
|
||||
entry_view_state,
|
||||
thread_state: Self::initial_state(agent, resume_thread, workspace, project, window, cx),
|
||||
login: None,
|
||||
message_editor,
|
||||
model_selector: None,
|
||||
profile_selector: None,
|
||||
@@ -443,9 +445,11 @@ impl AcpThreadView {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ThreadState {
|
||||
if !project.read(cx).is_local() && agent.clone().downcast::<NativeAgentServer>().is_none() {
|
||||
if project.read(cx).is_via_collab()
|
||||
&& agent.clone().downcast::<NativeAgentServer>().is_none()
|
||||
{
|
||||
return ThreadState::LoadError(LoadError::Other(
|
||||
"External agents are not yet supported for remote projects.".into(),
|
||||
"External agents are not yet supported in shared projects.".into(),
|
||||
));
|
||||
}
|
||||
let mut worktrees = project.read(cx).visible_worktrees(cx).collect::<Vec<_>>();
|
||||
@@ -465,20 +469,23 @@ impl AcpThreadView {
|
||||
Some(worktree.read(cx).abs_path())
|
||||
}
|
||||
})
|
||||
.next()
|
||||
.unwrap_or_else(|| paths::home_dir().as_path().into());
|
||||
.next();
|
||||
let (status_tx, mut status_rx) = watch::channel("Loading…".into());
|
||||
let (new_version_available_tx, mut new_version_available_rx) = watch::channel(None);
|
||||
let delegate = AgentServerDelegate::new(
|
||||
project.read(cx).agent_server_store().clone(),
|
||||
project.clone(),
|
||||
Some(status_tx),
|
||||
Some(new_version_available_tx),
|
||||
);
|
||||
|
||||
let connect_task = agent.connect(&root_dir, delegate, cx);
|
||||
let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
|
||||
let load_task = cx.spawn_in(window, async move |this, cx| {
|
||||
let connection = match connect_task.await {
|
||||
Ok(connection) => connection,
|
||||
Ok((connection, login)) => {
|
||||
this.update(cx, |this, _| this.login = login).ok();
|
||||
connection
|
||||
}
|
||||
Err(err) => {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
if err.downcast_ref::<LoadError>().is_some() {
|
||||
@@ -505,6 +512,14 @@ impl AcpThreadView {
|
||||
})
|
||||
.log_err()
|
||||
} else {
|
||||
let root_dir = if let Some(acp_agent) = connection
|
||||
.clone()
|
||||
.downcast::<agent_servers::AcpConnection>()
|
||||
{
|
||||
acp_agent.root_dir().into()
|
||||
} else {
|
||||
root_dir.unwrap_or(paths::home_dir().as_path().into())
|
||||
};
|
||||
cx.update(|_, cx| {
|
||||
connection
|
||||
.clone()
|
||||
@@ -983,7 +998,7 @@ impl AcpThreadView {
|
||||
this,
|
||||
AuthRequired {
|
||||
description: None,
|
||||
provider_id: Some(language_model::ANTHROPIC_PROVIDER_ID),
|
||||
provider_id: None,
|
||||
},
|
||||
agent,
|
||||
connection,
|
||||
@@ -1461,9 +1476,12 @@ impl AcpThreadView {
|
||||
self.thread_error.take();
|
||||
configuration_view.take();
|
||||
pending_auth_method.replace(method.clone());
|
||||
let authenticate = if method.0.as_ref() == "claude-login" {
|
||||
let authenticate = if (method.0.as_ref() == "claude-login"
|
||||
|| method.0.as_ref() == "spawn-gemini-cli")
|
||||
&& let Some(login) = self.login.clone()
|
||||
{
|
||||
if let Some(workspace) = self.workspace.upgrade() {
|
||||
Self::spawn_claude_login(&workspace, window, cx)
|
||||
Self::spawn_external_agent_login(login, workspace, false, window, cx)
|
||||
} else {
|
||||
Task::ready(Ok(()))
|
||||
}
|
||||
@@ -1510,31 +1528,28 @@ impl AcpThreadView {
|
||||
}));
|
||||
}
|
||||
|
||||
fn spawn_claude_login(
|
||||
workspace: &Entity<Workspace>,
|
||||
fn spawn_external_agent_login(
|
||||
login: task::SpawnInTerminal,
|
||||
workspace: Entity<Workspace>,
|
||||
previous_attempt: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<()>> {
|
||||
let Some(terminal_panel) = workspace.read(cx).panel::<TerminalPanel>(cx) else {
|
||||
return Task::ready(Ok(()));
|
||||
};
|
||||
let project_entity = workspace.read(cx).project();
|
||||
let project = project_entity.read(cx);
|
||||
let cwd = project.first_project_directory(cx);
|
||||
let shell = project.terminal_settings(&cwd, cx).shell.clone();
|
||||
|
||||
let delegate = AgentServerDelegate::new(project_entity.clone(), None, None);
|
||||
let command = ClaudeCode::login_command(delegate, cx);
|
||||
let project = workspace.read(cx).project().clone();
|
||||
let cwd = project.read(cx).first_project_directory(cx);
|
||||
let shell = project.read(cx).terminal_settings(&cwd, cx).shell.clone();
|
||||
|
||||
window.spawn(cx, async move |cx| {
|
||||
let login_command = command.await?;
|
||||
let command = login_command
|
||||
.path
|
||||
.to_str()
|
||||
.with_context(|| format!("invalid login command: {:?}", login_command.path))?;
|
||||
let command = shlex::try_quote(command)?;
|
||||
let args = login_command
|
||||
.arguments
|
||||
let mut task = login.clone();
|
||||
task.command = task
|
||||
.command
|
||||
.map(|command| anyhow::Ok(shlex::try_quote(&command)?.to_string()))
|
||||
.transpose()?;
|
||||
task.args = task
|
||||
.args
|
||||
.iter()
|
||||
.map(|arg| {
|
||||
Ok(shlex::try_quote(arg)
|
||||
@@ -1542,26 +1557,16 @@ impl AcpThreadView {
|
||||
.to_string())
|
||||
})
|
||||
.collect::<Result<Vec<_>>>()?;
|
||||
task.full_label = task.label.clone();
|
||||
task.id = task::TaskId(format!("external-agent-{}-login", task.label));
|
||||
task.command_label = task.label.clone();
|
||||
task.use_new_terminal = true;
|
||||
task.allow_concurrent_runs = true;
|
||||
task.hide = task::HideStrategy::Always;
|
||||
task.shell = shell;
|
||||
|
||||
let terminal = terminal_panel.update_in(cx, |terminal_panel, window, cx| {
|
||||
terminal_panel.spawn_task(
|
||||
&SpawnInTerminal {
|
||||
id: task::TaskId("claude-login".into()),
|
||||
full_label: "claude /login".to_owned(),
|
||||
label: "claude /login".to_owned(),
|
||||
command: Some(command.into()),
|
||||
args,
|
||||
command_label: "claude /login".to_owned(),
|
||||
cwd,
|
||||
use_new_terminal: true,
|
||||
allow_concurrent_runs: true,
|
||||
hide: task::HideStrategy::Always,
|
||||
shell,
|
||||
..Default::default()
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
terminal_panel.spawn_task(&login, window, cx)
|
||||
})?;
|
||||
|
||||
let terminal = terminal.await?;
|
||||
@@ -1577,7 +1582,9 @@ impl AcpThreadView {
|
||||
cx.background_executor().timer(Duration::from_secs(1)).await;
|
||||
let content =
|
||||
terminal.update(cx, |terminal, _cx| terminal.get_content())?;
|
||||
if content.contains("Login successful") {
|
||||
if content.contains("Login successful")
|
||||
|| content.contains("Type your message")
|
||||
{
|
||||
return anyhow::Ok(());
|
||||
}
|
||||
}
|
||||
@@ -1593,6 +1600,9 @@ impl AcpThreadView {
|
||||
}
|
||||
}
|
||||
_ = exit_status => {
|
||||
if !previous_attempt && project.read_with(cx, |project, _| project.is_via_remote_server())? && login.label.contains("gemini") {
|
||||
return cx.update(|window, cx| Self::spawn_external_agent_login(login, workspace, true, window, cx))?.await
|
||||
}
|
||||
return Err(anyhow!("exited before logging in"));
|
||||
}
|
||||
}
|
||||
@@ -2023,35 +2033,34 @@ impl AcpThreadView {
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> Div {
|
||||
let has_location = tool_call.locations.len() == 1;
|
||||
let card_header_id = SharedString::from("inner-tool-call-header");
|
||||
|
||||
let tool_icon =
|
||||
if tool_call.kind == acp::ToolKind::Edit && tool_call.locations.len() == 1 {
|
||||
FileIcons::get_icon(&tool_call.locations[0].path, cx)
|
||||
.map(Icon::from_path)
|
||||
.unwrap_or(Icon::new(IconName::ToolPencil))
|
||||
} else {
|
||||
Icon::new(match tool_call.kind {
|
||||
acp::ToolKind::Read => IconName::ToolSearch,
|
||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
||||
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
||||
acp::ToolKind::Search => IconName::ToolSearch,
|
||||
acp::ToolKind::Execute => IconName::ToolTerminal,
|
||||
acp::ToolKind::Think => IconName::ToolThink,
|
||||
acp::ToolKind::Fetch => IconName::ToolWeb,
|
||||
acp::ToolKind::Other => IconName::ToolHammer,
|
||||
})
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted);
|
||||
let tool_icon = if tool_call.kind == acp::ToolKind::Edit && has_location {
|
||||
FileIcons::get_icon(&tool_call.locations[0].path, cx)
|
||||
.map(Icon::from_path)
|
||||
.unwrap_or(Icon::new(IconName::ToolPencil))
|
||||
} else {
|
||||
Icon::new(match tool_call.kind {
|
||||
acp::ToolKind::Read => IconName::ToolSearch,
|
||||
acp::ToolKind::Edit => IconName::ToolPencil,
|
||||
acp::ToolKind::Delete => IconName::ToolDeleteFile,
|
||||
acp::ToolKind::Move => IconName::ArrowRightLeft,
|
||||
acp::ToolKind::Search => IconName::ToolSearch,
|
||||
acp::ToolKind::Execute => IconName::ToolTerminal,
|
||||
acp::ToolKind::Think => IconName::ToolThink,
|
||||
acp::ToolKind::Fetch => IconName::ToolWeb,
|
||||
acp::ToolKind::Other => IconName::ToolHammer,
|
||||
})
|
||||
}
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted);
|
||||
|
||||
let failed_or_canceled = match &tool_call.status {
|
||||
ToolCallStatus::Rejected | ToolCallStatus::Canceled | ToolCallStatus::Failed => true,
|
||||
_ => false,
|
||||
};
|
||||
|
||||
let has_location = tool_call.locations.len() == 1;
|
||||
let needs_confirmation = matches!(
|
||||
tool_call.status,
|
||||
ToolCallStatus::WaitingForConfirmation { .. }
|
||||
@@ -2194,13 +2203,6 @@ impl AcpThreadView {
|
||||
.overflow_hidden()
|
||||
.child(tool_icon)
|
||||
.child(if has_location {
|
||||
let name = tool_call.locations[0]
|
||||
.path
|
||||
.file_name()
|
||||
.unwrap_or_default()
|
||||
.display()
|
||||
.to_string();
|
||||
|
||||
h_flex()
|
||||
.id(("open-tool-call-location", entry_ix))
|
||||
.w_full()
|
||||
@@ -2211,7 +2213,13 @@ impl AcpThreadView {
|
||||
this.text_color(cx.theme().colors().text_muted)
|
||||
}
|
||||
})
|
||||
.child(name)
|
||||
.child(self.render_markdown(
|
||||
tool_call.label.clone(),
|
||||
MarkdownStyle {
|
||||
prevent_mouse_interaction: true,
|
||||
..default_markdown_style(false, true, window, cx)
|
||||
},
|
||||
))
|
||||
.tooltip(Tooltip::text("Jump to File"))
|
||||
.on_click(cx.listener(move |this, _, window, cx| {
|
||||
this.open_tool_call_location(entry_ix, 0, window, cx);
|
||||
@@ -3010,6 +3018,8 @@ impl AcpThreadView {
|
||||
let show_description =
|
||||
configuration_view.is_none() && description.is_none() && pending_auth_method.is_none();
|
||||
|
||||
let auth_methods = connection.auth_methods();
|
||||
|
||||
v_flex().flex_1().size_full().justify_end().child(
|
||||
v_flex()
|
||||
.p_2()
|
||||
@@ -3040,21 +3050,23 @@ impl AcpThreadView {
|
||||
.cloned()
|
||||
.map(|view| div().w_full().child(view)),
|
||||
)
|
||||
.when(
|
||||
show_description,
|
||||
|el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}. Please choose one of the following options:",
|
||||
self.agent.name()
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
},
|
||||
)
|
||||
.when(show_description, |el| {
|
||||
el.child(
|
||||
Label::new(format!(
|
||||
"You are not currently authenticated with {}.{}",
|
||||
self.agent.name(),
|
||||
if auth_methods.len() > 1 {
|
||||
" Please choose one of the following options:"
|
||||
} else {
|
||||
""
|
||||
}
|
||||
))
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.mb_1()
|
||||
.ml_5(),
|
||||
)
|
||||
})
|
||||
.when_some(pending_auth_method, |el, _| {
|
||||
el.child(
|
||||
h_flex()
|
||||
@@ -3066,12 +3078,12 @@ impl AcpThreadView {
|
||||
Icon::new(IconName::ArrowCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Muted)
|
||||
.with_rotate_animation(2)
|
||||
.with_rotate_animation(2),
|
||||
)
|
||||
.child(Label::new("Authenticating…").size(LabelSize::Small)),
|
||||
)
|
||||
})
|
||||
.when(!connection.auth_methods().is_empty(), |this| {
|
||||
.when(!auth_methods.is_empty(), |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
@@ -3083,23 +3095,26 @@ impl AcpThreadView {
|
||||
.pt_2()
|
||||
.border_color(cx.theme().colors().border.opacity(0.8))
|
||||
})
|
||||
.children(
|
||||
connection
|
||||
.auth_methods()
|
||||
.iter()
|
||||
.enumerate()
|
||||
.rev()
|
||||
.map(|(ix, method)| {
|
||||
Button::new(
|
||||
SharedString::from(method.id.0.clone()),
|
||||
method.name.clone(),
|
||||
)
|
||||
.children(connection.auth_methods().iter().enumerate().rev().map(
|
||||
|(ix, method)| {
|
||||
let (method_id, name) = if self
|
||||
.project
|
||||
.read(cx)
|
||||
.is_via_remote_server()
|
||||
&& method.id.0.as_ref() == "oauth-personal"
|
||||
&& method.name == "Log in with Google"
|
||||
{
|
||||
("spawn-gemini-cli".into(), "Log in with Gemini CLI".into())
|
||||
} else {
|
||||
(method.id.0.clone(), method.name.clone())
|
||||
};
|
||||
|
||||
Button::new(SharedString::from(method_id.clone()), name)
|
||||
.when(ix == 0, |el| {
|
||||
el.style(ButtonStyle::Tinted(ui::TintColor::Warning))
|
||||
})
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click({
|
||||
let method_id = method.id.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
telemetry::event!(
|
||||
"Authenticate Agent Started",
|
||||
@@ -3107,14 +3122,17 @@ impl AcpThreadView {
|
||||
method = method_id
|
||||
);
|
||||
|
||||
this.authenticate(method_id.clone(), window, cx)
|
||||
this.authenticate(
|
||||
acp::AuthMethodId(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
}),
|
||||
),
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -4071,15 +4089,15 @@ impl AcpThreadView {
|
||||
MentionUri::PastedImage => {}
|
||||
MentionUri::Directory { abs_path } => {
|
||||
let project = workspace.project();
|
||||
let Some(entry) = project.update(cx, |project, cx| {
|
||||
let Some(entry_id) = project.update(cx, |project, cx| {
|
||||
let path = project.find_project_path(abs_path, cx)?;
|
||||
project.entry_for_path(&path, cx)
|
||||
project.entry_for_path(&path, cx).map(|entry| entry.id)
|
||||
}) else {
|
||||
return;
|
||||
};
|
||||
|
||||
project.update(cx, |_, cx| {
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry.id));
|
||||
cx.emit(project::Event::RevealInProjectPanel(entry_id));
|
||||
});
|
||||
}
|
||||
MentionUri::Symbol {
|
||||
@@ -4092,11 +4110,9 @@ impl AcpThreadView {
|
||||
line_range,
|
||||
} => {
|
||||
let project = workspace.project();
|
||||
let Some((path, _)) = project.update(cx, |project, cx| {
|
||||
let path = project.find_project_path(path, cx)?;
|
||||
let entry = project.entry_for_path(&path, cx)?;
|
||||
Some((path, entry))
|
||||
}) else {
|
||||
let Some(path) =
|
||||
project.update(cx, |project, cx| project.find_project_path(path, cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -4258,7 +4274,7 @@ impl AcpThreadView {
|
||||
}
|
||||
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), cx)
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
|
||||
});
|
||||
let buffer = cx.new(|cx| {
|
||||
MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone())
|
||||
@@ -4737,9 +4753,9 @@ impl AcpThreadView {
|
||||
)
|
||||
}
|
||||
|
||||
fn settings_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
fn agent_font_size_changed(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.entry_view_state.update(cx, |entry_view_state, cx| {
|
||||
entry_view_state.settings_changed(cx);
|
||||
entry_view_state.agent_font_size_changed(cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -5715,11 +5731,11 @@ pub(crate) mod tests {
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
_root_dir: &Path,
|
||||
_root_dir: Option<&Path>,
|
||||
_delegate: AgentServerDelegate,
|
||||
_cx: &mut App,
|
||||
) -> Task<gpui::Result<Rc<dyn AgentConnection>>> {
|
||||
Task::ready(Ok(Rc::new(self.connection.clone())))
|
||||
) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
|
||||
Task::ready(Ok((Rc::new(self.connection.clone()), None)))
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
|
||||
@@ -3585,7 +3585,7 @@ pub(crate) fn open_active_thread_as_markdown(
|
||||
}
|
||||
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), cx)
|
||||
project.create_local_buffer(&markdown, Some(markdown_language), true, cx)
|
||||
});
|
||||
let buffer =
|
||||
cx.new(|cx| MultiBuffer::singleton(buffer, cx).with_title(thread_summary.clone()));
|
||||
|
||||
@@ -5,7 +5,6 @@ mod tool_picker;
|
||||
|
||||
use std::{ops::Range, sync::Arc};
|
||||
|
||||
use agent_servers::{AgentServerCommand, AllAgentServersSettings, CustomAgentServerSettings};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{ToolSource, ToolWorkingSet};
|
||||
@@ -26,6 +25,10 @@ use language_model::{
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
agent_server_store::{
|
||||
AgentServerCommand, AgentServerStore, AllAgentServersSettings, CLAUDE_CODE_NAME,
|
||||
CustomAgentServerSettings, GEMINI_NAME,
|
||||
},
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
project_settings::{ContextServerSettings, ProjectSettings},
|
||||
};
|
||||
@@ -45,11 +48,13 @@ pub(crate) use manage_profiles_modal::ManageProfilesModal;
|
||||
use crate::{
|
||||
AddContextServer, ExternalAgent, NewExternalAgentThread,
|
||||
agent_configuration::add_llm_provider_modal::{AddLlmProviderModal, LlmCompatibleProvider},
|
||||
placeholder_command,
|
||||
};
|
||||
|
||||
pub struct AgentConfiguration {
|
||||
fs: Arc<dyn Fs>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
focus_handle: FocusHandle,
|
||||
configuration_views_by_provider: HashMap<LanguageModelProviderId, AnyView>,
|
||||
@@ -66,6 +71,7 @@ pub struct AgentConfiguration {
|
||||
impl AgentConfiguration {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
agent_server_store: Entity<AgentServerStore>,
|
||||
context_server_store: Entity<ContextServerStore>,
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
@@ -104,6 +110,7 @@ impl AgentConfiguration {
|
||||
workspace,
|
||||
focus_handle,
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
agent_server_store,
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
@@ -991,17 +998,30 @@ impl AgentConfiguration {
|
||||
}
|
||||
|
||||
fn render_agent_servers_section(&mut self, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let settings = AllAgentServersSettings::get_global(cx).clone();
|
||||
let user_defined_agents = settings
|
||||
let custom_settings = cx
|
||||
.global::<SettingsStore>()
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.iter()
|
||||
.map(|(name, settings)| {
|
||||
.clone();
|
||||
let user_defined_agents = self
|
||||
.agent_server_store
|
||||
.read(cx)
|
||||
.external_agents()
|
||||
.filter(|name| name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME)
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let user_defined_agents = user_defined_agents
|
||||
.into_iter()
|
||||
.map(|name| {
|
||||
self.render_agent_server(
|
||||
IconName::Ai,
|
||||
name.clone(),
|
||||
ExternalAgent::Custom {
|
||||
name: name.clone(),
|
||||
command: settings.command.clone(),
|
||||
name: name.clone().into(),
|
||||
command: custom_settings
|
||||
.get(&name.0)
|
||||
.map(|settings| settings.command.clone())
|
||||
.unwrap_or(placeholder_command()),
|
||||
},
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -251,6 +251,7 @@ pub struct ConfigureContextServerModal {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
source: ConfigurationSource,
|
||||
state: State,
|
||||
original_server_id: Option<ContextServerId>,
|
||||
}
|
||||
|
||||
impl ConfigureContextServerModal {
|
||||
@@ -348,6 +349,11 @@ impl ConfigureContextServerModal {
|
||||
context_server_store,
|
||||
workspace: workspace_handle,
|
||||
state: State::Idle,
|
||||
original_server_id: match &target {
|
||||
ConfigurationTarget::Existing { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::Extension { id, .. } => Some(id.clone()),
|
||||
ConfigurationTarget::New => None,
|
||||
},
|
||||
source: ConfigurationSource::from_target(
|
||||
target,
|
||||
language_registry,
|
||||
@@ -415,9 +421,19 @@ impl ConfigureContextServerModal {
|
||||
// When we write the settings to the file, the context server will be restarted.
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
update_settings_file::<ProjectSettings>(fs.clone(), cx, |project_settings, _| {
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
});
|
||||
let original_server_id = self.original_server_id.clone();
|
||||
update_settings_file::<ProjectSettings>(
|
||||
fs.clone(),
|
||||
cx,
|
||||
move |project_settings, _| {
|
||||
if let Some(original_id) = original_server_id {
|
||||
if original_id != id {
|
||||
project_settings.context_servers.remove(&original_id.0);
|
||||
}
|
||||
}
|
||||
project_settings.context_servers.insert(id.0, settings);
|
||||
},
|
||||
);
|
||||
});
|
||||
} else if let Some(existing_server) = existing_server {
|
||||
self.context_server_store
|
||||
|
||||
@@ -5,9 +5,11 @@ use std::sync::Arc;
|
||||
use std::time::Duration;
|
||||
|
||||
use acp_thread::AcpThread;
|
||||
use agent_servers::AgentServerCommand;
|
||||
use agent2::{DbThreadMetadata, HistoryEntry};
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use project::agent_server_store::{
|
||||
AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, GEMINI_NAME,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent};
|
||||
@@ -33,7 +35,9 @@ use crate::{
|
||||
thread_history::{HistoryEntryElement, ThreadHistory},
|
||||
ui::{AgentOnboardingModal, EndTrialUpsell},
|
||||
};
|
||||
use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
|
||||
use crate::{
|
||||
ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
|
||||
};
|
||||
use agent::{
|
||||
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
|
||||
context_store::ContextStore,
|
||||
@@ -62,7 +66,7 @@ use project::{DisableAiSettings, Project, ProjectPath, Worktree};
|
||||
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
|
||||
use rules_library::{RulesLibrary, open_rules_library};
|
||||
use search::{BufferSearchBar, buffer_search};
|
||||
use settings::{Settings, update_settings_file};
|
||||
use settings::{Settings, SettingsStore, update_settings_file};
|
||||
use theme::ThemeSettings;
|
||||
use time::UtcOffset;
|
||||
use ui::utils::WithRemSize;
|
||||
@@ -1094,7 +1098,7 @@ impl AgentPanel {
|
||||
let workspace = self.workspace.clone();
|
||||
let project = self.project.clone();
|
||||
let fs = self.fs.clone();
|
||||
let is_not_local = !self.project.read(cx).is_local();
|
||||
let is_via_collab = self.project.read(cx).is_via_collab();
|
||||
|
||||
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
|
||||
|
||||
@@ -1126,7 +1130,7 @@ impl AgentPanel {
|
||||
agent
|
||||
}
|
||||
None => {
|
||||
if is_not_local {
|
||||
if is_via_collab {
|
||||
ExternalAgent::NativeAgent
|
||||
} else {
|
||||
cx.background_spawn(async move {
|
||||
@@ -1503,6 +1507,7 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
pub(crate) fn open_configuration(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let agent_server_store = self.project.read(cx).agent_server_store().clone();
|
||||
let context_server_store = self.project.read(cx).context_server_store();
|
||||
let tools = self.thread_store.read(cx).tools();
|
||||
let fs = self.fs.clone();
|
||||
@@ -1511,6 +1516,7 @@ impl AgentPanel {
|
||||
self.configuration = Some(cx.new(|cx| {
|
||||
AgentConfiguration::new(
|
||||
fs,
|
||||
agent_server_store,
|
||||
context_server_store,
|
||||
tools,
|
||||
self.language_registry.clone(),
|
||||
@@ -2503,6 +2509,7 @@ impl AgentPanel {
|
||||
}
|
||||
|
||||
fn render_toolbar_new(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let agent_server_store = self.project.read(cx).agent_server_store().clone();
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
let active_thread = match &self.active_view {
|
||||
@@ -2535,8 +2542,10 @@ impl AgentPanel {
|
||||
.with_handle(self.new_thread_menu_handle.clone())
|
||||
.menu({
|
||||
let workspace = self.workspace.clone();
|
||||
let is_not_local = workspace
|
||||
.update(cx, |workspace, cx| !workspace.project().read(cx).is_local())
|
||||
let is_via_collab = workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
workspace.project().read(cx).is_via_collab()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
move |window, cx| {
|
||||
@@ -2628,7 +2637,7 @@ impl AgentPanel {
|
||||
ContextMenuEntry::new("New Gemini CLI Thread")
|
||||
.icon(IconName::AiGemini)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_not_local)
|
||||
.disabled(is_via_collab)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
move |window, cx| {
|
||||
@@ -2655,7 +2664,7 @@ impl AgentPanel {
|
||||
menu.item(
|
||||
ContextMenuEntry::new("New Claude Code Thread")
|
||||
.icon(IconName::AiClaude)
|
||||
.disabled(is_not_local)
|
||||
.disabled(is_via_collab)
|
||||
.icon_color(Color::Muted)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
@@ -2680,19 +2689,25 @@ impl AgentPanel {
|
||||
)
|
||||
})
|
||||
.when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
|
||||
// Add custom agents from settings
|
||||
let settings =
|
||||
agent_servers::AllAgentServersSettings::get_global(cx);
|
||||
for (agent_name, agent_settings) in &settings.custom {
|
||||
let agent_names = agent_server_store
|
||||
.read(cx)
|
||||
.external_agents()
|
||||
.filter(|name| {
|
||||
name.0 != GEMINI_NAME && name.0 != CLAUDE_CODE_NAME
|
||||
})
|
||||
.cloned()
|
||||
.collect::<Vec<_>>();
|
||||
let custom_settings = cx.global::<SettingsStore>().get::<AllAgentServersSettings>(None).custom.clone();
|
||||
for agent_name in agent_names {
|
||||
menu = menu.item(
|
||||
ContextMenuEntry::new(format!("New {} Thread", agent_name))
|
||||
.icon(IconName::Terminal)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(is_not_local)
|
||||
.disabled(is_via_collab)
|
||||
.handler({
|
||||
let workspace = workspace.clone();
|
||||
let agent_name = agent_name.clone();
|
||||
let agent_settings = agent_settings.clone();
|
||||
let custom_settings = custom_settings.clone();
|
||||
move |window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
@@ -2703,10 +2718,9 @@ impl AgentPanel {
|
||||
panel.new_agent_thread(
|
||||
AgentType::Custom {
|
||||
name: agent_name
|
||||
.clone(),
|
||||
command: agent_settings
|
||||
.command
|
||||
.clone(),
|
||||
.clone()
|
||||
.into(),
|
||||
command: custom_settings.get(&agent_name.0).map(|settings| settings.command.clone()).unwrap_or(placeholder_command())
|
||||
},
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -28,7 +28,6 @@ use std::rc::Rc;
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_servers::AgentServerCommand;
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
@@ -41,6 +40,7 @@ use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
|
||||
};
|
||||
use project::DisableAiSettings;
|
||||
use project::agent_server_store::AgentServerCommand;
|
||||
use prompt_store::PromptBuilder;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -174,6 +174,14 @@ enum ExternalAgent {
|
||||
},
|
||||
}
|
||||
|
||||
fn placeholder_command() -> AgentServerCommand {
|
||||
AgentServerCommand {
|
||||
path: "/placeholder".into(),
|
||||
args: vec![],
|
||||
env: None,
|
||||
}
|
||||
}
|
||||
|
||||
impl ExternalAgent {
|
||||
fn name(&self) -> &'static str {
|
||||
match self {
|
||||
@@ -193,10 +201,9 @@ impl ExternalAgent {
|
||||
Self::Gemini => Rc::new(agent_servers::Gemini),
|
||||
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
|
||||
Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
|
||||
Self::Custom { name, command } => Rc::new(agent_servers::CustomAgentServer::new(
|
||||
name.clone(),
|
||||
command.clone(),
|
||||
)),
|
||||
Self::Custom { name, command: _ } => {
|
||||
Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -337,8 +344,7 @@ fn update_command_palette_filter(cx: &mut App) {
|
||||
];
|
||||
filter.show_action_types(edit_prediction_actions.iter());
|
||||
|
||||
filter
|
||||
.show_action_types([TypeId::of::<zed_actions::OpenZedPredictOnboarding>()].iter());
|
||||
filter.show_action_types(&[TypeId::of::<zed_actions::OpenZedPredictOnboarding>()]);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1139,7 +1139,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
@@ -1208,7 +1208,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
@@ -1277,7 +1277,7 @@ mod tests {
|
||||
);
|
||||
while !new_text.is_empty() {
|
||||
let max_len = cmp::min(new_text.len(), 10);
|
||||
let len = rng.gen_range(1..=max_len);
|
||||
let len = rng.random_range(1..=max_len);
|
||||
let (chunk, suffix) = new_text.split_at(len);
|
||||
chunks_tx.unbounded_send(chunk.to_string()).unwrap();
|
||||
new_text = suffix;
|
||||
|
||||
@@ -987,7 +987,8 @@ impl MentionLink {
|
||||
.read(cx)
|
||||
.project()
|
||||
.read(cx)
|
||||
.entry_for_path(&project_path, cx)?;
|
||||
.entry_for_path(&project_path, cx)?
|
||||
.clone();
|
||||
Some(MentionLink::File(project_path, entry))
|
||||
}
|
||||
Self::SYMBOL => {
|
||||
|
||||
@@ -125,6 +125,7 @@ pub(crate) fn create_editor(
|
||||
cx,
|
||||
);
|
||||
editor.set_placeholder_text("Message the agent – @ to include context", cx);
|
||||
editor.disable_word_completions();
|
||||
editor.set_show_indent_guides(false, cx);
|
||||
editor.set_soft_wrap();
|
||||
editor.set_use_modal_editing(true);
|
||||
|
||||
@@ -2,10 +2,11 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
/// Settings for slash commands.
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi)]
|
||||
#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "slash_commands")]
|
||||
pub struct SlashCommandSettings {
|
||||
/// Settings for the `/cargo-workspace` slash command.
|
||||
#[serde(default)]
|
||||
@@ -21,8 +22,6 @@ pub struct CargoWorkspaceCommandSettings {
|
||||
}
|
||||
|
||||
impl Settings for SlashCommandSettings {
|
||||
const KEY: Option<&'static str> = Some("slash_commands");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -764,7 +764,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
let network = Arc::new(Mutex::new(Network::new(rng.clone())));
|
||||
let mut contexts = Vec::new();
|
||||
|
||||
let num_peers = rng.gen_range(min_peers..=max_peers);
|
||||
let num_peers = rng.random_range(min_peers..=max_peers);
|
||||
let context_id = ContextId::new();
|
||||
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
|
||||
for i in 0..num_peers {
|
||||
@@ -806,10 +806,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|| !network.lock().is_idle()
|
||||
|| network.lock().contains_disconnected_peers()
|
||||
{
|
||||
let context_index = rng.gen_range(0..contexts.len());
|
||||
let context_index = rng.random_range(0..contexts.len());
|
||||
let context = &contexts[context_index];
|
||||
|
||||
match rng.gen_range(0..100) {
|
||||
match rng.random_range(0..100) {
|
||||
0..=29 if mutation_count > 0 => {
|
||||
log::info!("Context {}: edit buffer", context_index);
|
||||
context.update(cx, |context, cx| {
|
||||
@@ -874,10 +874,10 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
merge_same_roles: true,
|
||||
})];
|
||||
|
||||
let num_sections = rng.gen_range(0..=3);
|
||||
let num_sections = rng.random_range(0..=3);
|
||||
let mut section_start = 0;
|
||||
for _ in 0..num_sections {
|
||||
let mut section_end = rng.gen_range(section_start..=output_text.len());
|
||||
let mut section_end = rng.random_range(section_start..=output_text.len());
|
||||
while !output_text.is_char_boundary(section_end) {
|
||||
section_end += 1;
|
||||
}
|
||||
@@ -924,7 +924,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
75..=84 if mutation_count > 0 => {
|
||||
context.update(cx, |context, cx| {
|
||||
if let Some(message) = context.messages(cx).choose(&mut rng) {
|
||||
let new_status = match rng.gen_range(0..3) {
|
||||
let new_status = match rng.random_range(0..3) {
|
||||
0 => MessageStatus::Done,
|
||||
1 => MessageStatus::Pending,
|
||||
_ => MessageStatus::Error(SharedString::from("Random error")),
|
||||
@@ -971,7 +971,7 @@ async fn test_random_context_collaboration(cx: &mut TestAppContext, mut rng: Std
|
||||
|
||||
network.lock().broadcast(replica_id, ops_to_send);
|
||||
context.update(cx, |context, cx| context.apply_ops(ops_to_receive, cx));
|
||||
} else if rng.gen_bool(0.1) && replica_id != 0 {
|
||||
} else if rng.random_bool(0.1) && replica_id != 0 {
|
||||
log::info!("Context {}: disconnecting", context_index);
|
||||
network.lock().disconnect_peer(replica_id);
|
||||
} else if network.lock().has_unreceived(replica_id) {
|
||||
|
||||
@@ -1315,17 +1315,17 @@ mod tests {
|
||||
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_random_indents(mut rng: StdRng) {
|
||||
let len = rng.gen_range(1..=100);
|
||||
let len = rng.random_range(1..=100);
|
||||
let new_text = util::RandomCharIter::new(&mut rng)
|
||||
.with_simple_text()
|
||||
.take(len)
|
||||
.collect::<String>();
|
||||
let new_text = new_text
|
||||
.split('\n')
|
||||
.map(|line| format!("{}{}", " ".repeat(rng.gen_range(0..=8)), line))
|
||||
.map(|line| format!("{}{}", " ".repeat(rng.random_range(0..=8)), line))
|
||||
.collect::<Vec<_>>()
|
||||
.join("\n");
|
||||
let delta = IndentDelta::Spaces(rng.gen_range(-4..=4));
|
||||
let delta = IndentDelta::Spaces(rng.random_range(-4i8..=4i8) as isize);
|
||||
|
||||
let chunks = to_random_chunks(&mut rng, &new_text);
|
||||
let new_text_chunks = stream::iter(chunks.iter().enumerate().map(|(index, chunk)| {
|
||||
@@ -1357,7 +1357,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -204,7 +204,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut CreateFileParser, rng: &mut StdRng) -> String {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -996,7 +996,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn parse_random_chunks(input: &str, parser: &mut EditParser, rng: &mut StdRng) -> Vec<Edit> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -1399,7 +1399,7 @@ fn eval(
|
||||
}
|
||||
|
||||
fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
|
||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_entropy());
|
||||
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
|
||||
let mut cx = TestAppContext::build(dispatcher, None);
|
||||
let output = cx.executor().block_test(async {
|
||||
let test = EditAgentTest::new(&mut cx).await;
|
||||
@@ -1707,7 +1707,7 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
|
||||
};
|
||||
|
||||
if let Some(retry_after) = retry_delay {
|
||||
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
let jitter = retry_after.mul_f64(rand::rng().random_range(0.0..1.0));
|
||||
eprintln!("Attempt #{attempt}: Retry after {retry_after:?} + jitter of {jitter:?}");
|
||||
Timer::after(retry_after + jitter).await;
|
||||
} else {
|
||||
|
||||
@@ -771,7 +771,7 @@ mod tests {
|
||||
}
|
||||
|
||||
fn to_random_chunks(rng: &mut StdRng, input: &str) -> Vec<String> {
|
||||
let chunk_count = rng.gen_range(1..=cmp::min(input.len(), 50));
|
||||
let chunk_count = rng.random_range(1..=cmp::min(input.len(), 50));
|
||||
let mut chunk_indices = (0..input.len()).choose_multiple(rng, chunk_count);
|
||||
chunk_indices.sort();
|
||||
chunk_indices.push(input.len());
|
||||
|
||||
@@ -2,9 +2,9 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
pub struct AudioSettings {
|
||||
/// Opt into the new audio system.
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
@@ -12,8 +12,9 @@ pub struct AudioSettings {
|
||||
}
|
||||
|
||||
/// Configuration of audio in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[serde(default)]
|
||||
#[settings_key(key = "audio")]
|
||||
pub struct AudioSettingsContent {
|
||||
/// Whether to use the experimental audio system
|
||||
#[serde(rename = "experimental.rodio_audio", default)]
|
||||
@@ -21,8 +22,6 @@ pub struct AudioSettingsContent {
|
||||
}
|
||||
|
||||
impl Settings for AudioSettings {
|
||||
const KEY: Option<&'static str> = Some("audio");
|
||||
|
||||
type FileContent = AudioSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -10,7 +10,7 @@ use paths::remote_servers_dir;
|
||||
use release_channel::{AppCommitSha, ReleaseChannel};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsStore, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsStore, SettingsUi};
|
||||
use smol::{fs, io::AsyncReadExt};
|
||||
use smol::{fs::File, process::Command};
|
||||
use std::{
|
||||
@@ -118,13 +118,12 @@ struct AutoUpdateSetting(bool);
|
||||
/// Whether or not to automatically check for updates.
|
||||
///
|
||||
/// Default: true
|
||||
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi)]
|
||||
#[derive(Clone, Copy, Default, JsonSchema, Deserialize, Serialize, SettingsUi, SettingsKey)]
|
||||
#[serde(transparent)]
|
||||
#[settings_key(key = "auto_update")]
|
||||
struct AutoUpdateSettingContent(bool);
|
||||
|
||||
impl Settings for AutoUpdateSetting {
|
||||
const KEY: Option<&'static str> = Some("auto_update");
|
||||
|
||||
type FileContent = AutoUpdateSettingContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -88,10 +88,7 @@ fn view_release_notes_locally(
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
let project = workspace.project().clone();
|
||||
let buffer = project.update(cx, |project, cx| {
|
||||
let buffer = project.create_local_buffer("", markdown, cx);
|
||||
project
|
||||
.mark_buffer_as_non_searchable(buffer.read(cx).remote_id(), cx);
|
||||
buffer
|
||||
project.create_local_buffer("", markdown, false, cx)
|
||||
});
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.edit([(0..0, body.release_notes)], None, cx)
|
||||
|
||||
@@ -2044,10 +2044,10 @@ mod tests {
|
||||
#[gpui::test(iterations = 100)]
|
||||
async fn test_staging_and_unstaging_hunks(cx: &mut TestAppContext, mut rng: StdRng) {
|
||||
fn gen_line(rng: &mut StdRng) -> String {
|
||||
if rng.gen_bool(0.2) {
|
||||
if rng.random_bool(0.2) {
|
||||
"\n".to_owned()
|
||||
} else {
|
||||
let c = rng.gen_range('A'..='Z');
|
||||
let c = rng.random_range('A'..='Z');
|
||||
format!("{c}{c}{c}\n")
|
||||
}
|
||||
}
|
||||
@@ -2066,7 +2066,7 @@ mod tests {
|
||||
old_lines.into_iter()
|
||||
};
|
||||
let mut result = String::new();
|
||||
let unchanged_count = rng.gen_range(0..=old_lines.len());
|
||||
let unchanged_count = rng.random_range(0..=old_lines.len());
|
||||
result +=
|
||||
&old_lines
|
||||
.by_ref()
|
||||
@@ -2076,14 +2076,14 @@ mod tests {
|
||||
s
|
||||
});
|
||||
while old_lines.len() > 0 {
|
||||
let deleted_count = rng.gen_range(0..=old_lines.len());
|
||||
let deleted_count = rng.random_range(0..=old_lines.len());
|
||||
let _advance = old_lines
|
||||
.by_ref()
|
||||
.take(deleted_count)
|
||||
.map(|line| line.len() + 1)
|
||||
.sum::<usize>();
|
||||
let minimum_added = if deleted_count == 0 { 1 } else { 0 };
|
||||
let added_count = rng.gen_range(minimum_added..=5);
|
||||
let added_count = rng.random_range(minimum_added..=5);
|
||||
let addition = (0..added_count).map(|_| gen_line(rng)).collect::<String>();
|
||||
result += &addition;
|
||||
|
||||
@@ -2092,7 +2092,8 @@ mod tests {
|
||||
if blank_lines == old_lines.len() {
|
||||
break;
|
||||
};
|
||||
let unchanged_count = rng.gen_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
let unchanged_count =
|
||||
rng.random_range((blank_lines + 1).max(1)..=old_lines.len());
|
||||
result += &old_lines.by_ref().take(unchanged_count).fold(
|
||||
String::new(),
|
||||
|mut s, line| {
|
||||
@@ -2149,7 +2150,7 @@ mod tests {
|
||||
)
|
||||
});
|
||||
let working_copy = working_copy.read_with(cx, |working_copy, _| working_copy.snapshot());
|
||||
let mut index_text = if rng.r#gen() {
|
||||
let mut index_text = if rng.random() {
|
||||
Rope::from(head_text.as_str())
|
||||
} else {
|
||||
working_copy.as_rope().clone()
|
||||
@@ -2165,7 +2166,7 @@ mod tests {
|
||||
}
|
||||
|
||||
for _ in 0..operations {
|
||||
let i = rng.gen_range(0..hunks.len());
|
||||
let i = rng.random_range(0..hunks.len());
|
||||
let hunk = &mut hunks[i];
|
||||
let hunk_to_change = hunk.clone();
|
||||
let stage = match hunk.secondary_status {
|
||||
|
||||
@@ -2,7 +2,7 @@ use anyhow::Result;
|
||||
use gpui::App;
|
||||
use schemars::JsonSchema;
|
||||
use serde_derive::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct CallSettings {
|
||||
@@ -11,7 +11,8 @@ pub struct CallSettings {
|
||||
}
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "calls")]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
@@ -25,8 +26,6 @@ pub struct CallSettingsContent {
|
||||
}
|
||||
|
||||
impl Settings for CallSettings {
|
||||
const KEY: Option<&'static str> = Some("calls");
|
||||
|
||||
type FileContent = CallSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
|
||||
@@ -129,7 +129,7 @@ impl ChannelChat {
|
||||
loaded_all_messages: false,
|
||||
next_pending_message_id: 0,
|
||||
last_acknowledged_id: None,
|
||||
rng: StdRng::from_entropy(),
|
||||
rng: StdRng::from_os_rng(),
|
||||
first_loaded_message_id: None,
|
||||
_subscription: subscription.set_entity(&cx.entity(), &cx.to_async()),
|
||||
}
|
||||
@@ -183,7 +183,7 @@ impl ChannelChat {
|
||||
|
||||
let channel_id = self.channel_id;
|
||||
let pending_id = ChannelMessageId::Pending(post_inc(&mut self.next_pending_message_id));
|
||||
let nonce = self.rng.r#gen();
|
||||
let nonce = self.rng.random();
|
||||
self.insert_messages(
|
||||
SumTree::from_item(
|
||||
ChannelMessage {
|
||||
@@ -257,7 +257,7 @@ impl ChannelChat {
|
||||
cx,
|
||||
);
|
||||
|
||||
let nonce: u128 = self.rng.r#gen();
|
||||
let nonce: u128 = self.rng.random();
|
||||
|
||||
let request = self.rpc.request(proto::UpdateChannelMessage {
|
||||
channel_id: self.channel_id.0,
|
||||
|
||||
@@ -75,7 +75,7 @@ util = { workspace = true, features = ["test-support"] }
|
||||
windows.workspace = true
|
||||
|
||||
[target.'cfg(target_os = "macos")'.dependencies]
|
||||
cocoa.workspace = true
|
||||
objc2-foundation.workspace = true
|
||||
|
||||
[target.'cfg(any(target_os = "windows", target_os = "macos"))'.dependencies]
|
||||
tokio-native-tls = "0.3"
|
||||
|
||||
@@ -31,7 +31,7 @@ use release_channel::{AppVersion, ReleaseChannel};
|
||||
use rpc::proto::{AnyTypedEnvelope, EnvelopedMessage, PeerId, RequestMessage};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
use std::{
|
||||
any::TypeId,
|
||||
convert::TryFrom,
|
||||
@@ -96,7 +96,8 @@ actions!(
|
||||
]
|
||||
);
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
|
||||
#[settings_key(None)]
|
||||
pub struct ClientSettingsContent {
|
||||
server_url: Option<String>,
|
||||
}
|
||||
@@ -107,8 +108,6 @@ pub struct ClientSettings {
|
||||
}
|
||||
|
||||
impl Settings for ClientSettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = ClientSettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
@@ -122,7 +121,8 @@ impl Settings for ClientSettings {
|
||||
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
|
||||
}
|
||||
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, SettingsUi, SettingsKey)]
|
||||
#[settings_key(None)]
|
||||
pub struct ProxySettingsContent {
|
||||
proxy: Option<String>,
|
||||
}
|
||||
@@ -133,8 +133,6 @@ pub struct ProxySettings {
|
||||
}
|
||||
|
||||
impl Settings for ProxySettings {
|
||||
const KEY: Option<&'static str> = None;
|
||||
|
||||
type FileContent = ProxySettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
@@ -527,7 +525,8 @@ pub struct TelemetrySettings {
|
||||
}
|
||||
|
||||
/// Control what info is collected by Zed.
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Default, Clone, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "telemetry")]
|
||||
pub struct TelemetrySettingsContent {
|
||||
/// Send debug info like crash reports.
|
||||
///
|
||||
@@ -540,8 +539,6 @@ pub struct TelemetrySettingsContent {
|
||||
}
|
||||
|
||||
impl settings::Settings for TelemetrySettings {
|
||||
const KEY: Option<&'static str> = Some("telemetry");
|
||||
|
||||
type FileContent = TelemetrySettingsContent;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
|
||||
@@ -691,7 +688,7 @@ impl Client {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
let mut rng = StdRng::seed_from_u64(0);
|
||||
#[cfg(not(any(test, feature = "test-support")))]
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let mut rng = StdRng::from_os_rng();
|
||||
|
||||
let mut delay = INITIAL_RECONNECTION_DELAY;
|
||||
loop {
|
||||
@@ -721,8 +718,9 @@ impl Client {
|
||||
},
|
||||
cx,
|
||||
);
|
||||
let jitter =
|
||||
Duration::from_millis(rng.gen_range(0..delay.as_millis() as u64));
|
||||
let jitter = Duration::from_millis(
|
||||
rng.random_range(0..delay.as_millis() as u64),
|
||||
);
|
||||
cx.background_executor().timer(delay + jitter).await;
|
||||
delay = cmp::min(delay * 2, MAX_RECONNECTION_DELAY);
|
||||
} else {
|
||||
|
||||
@@ -84,6 +84,10 @@ static DOTNET_PROJECT_FILES_REGEX: LazyLock<Regex> = LazyLock::new(|| {
|
||||
Regex::new(r"^(global\.json|Directory\.Build\.props|.*\.(csproj|fsproj|vbproj|sln))$").unwrap()
|
||||
});
|
||||
|
||||
#[cfg(target_os = "macos")]
|
||||
static MACOS_VERSION_REGEX: LazyLock<Regex> =
|
||||
LazyLock::new(|| Regex::new(r"(\s*\(Build [^)]*[0-9]\))").unwrap());
|
||||
|
||||
pub fn os_name() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
@@ -108,19 +112,16 @@ pub fn os_name() -> String {
|
||||
pub fn os_version() -> String {
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
use cocoa::base::nil;
|
||||
use cocoa::foundation::NSProcessInfo;
|
||||
|
||||
unsafe {
|
||||
let process_info = cocoa::foundation::NSProcessInfo::processInfo(nil);
|
||||
let version = process_info.operatingSystemVersion();
|
||||
gpui::SemanticVersion::new(
|
||||
version.majorVersion as usize,
|
||||
version.minorVersion as usize,
|
||||
version.patchVersion as usize,
|
||||
)
|
||||
use objc2_foundation::NSProcessInfo;
|
||||
let process_info = NSProcessInfo::processInfo();
|
||||
let version_nsstring = unsafe { process_info.operatingSystemVersionString() };
|
||||
// "Version 15.6.1 (Build 24G90)" -> "15.6.1 (Build 24G90)"
|
||||
let version_string = version_nsstring.to_string().replace("Version ", "");
|
||||
// "15.6.1 (Build 24G90)" -> "15.6.1"
|
||||
// "26.0.0 (Build 25A5349a)" -> unchanged (Beta or Rapid Security Response; ends with letter)
|
||||
MACOS_VERSION_REGEX
|
||||
.replace_all(&version_string, "")
|
||||
.to_string()
|
||||
}
|
||||
}
|
||||
#[cfg(any(target_os = "linux", target_os = "freebsd"))]
|
||||
{
|
||||
|
||||
@@ -227,7 +227,7 @@ pub async fn verify_access_token(
|
||||
|
||||
#[cfg(test)]
|
||||
mod test {
|
||||
use rand::thread_rng;
|
||||
use rand::prelude::*;
|
||||
use scrypt::password_hash::{PasswordHasher, SaltString};
|
||||
use sea_orm::EntityTrait;
|
||||
|
||||
@@ -358,9 +358,42 @@ mod test {
|
||||
None,
|
||||
None,
|
||||
params,
|
||||
&SaltString::generate(thread_rng()),
|
||||
&SaltString::generate(PasswordHashRngCompat::new()),
|
||||
)
|
||||
.map_err(anyhow::Error::new)?
|
||||
.to_string())
|
||||
}
|
||||
|
||||
// TODO: remove once we password_hash v0.6 is released.
|
||||
struct PasswordHashRngCompat(rand::rngs::ThreadRng);
|
||||
|
||||
impl PasswordHashRngCompat {
|
||||
fn new() -> Self {
|
||||
Self(rand::rng())
|
||||
}
|
||||
}
|
||||
|
||||
impl scrypt::password_hash::rand_core::RngCore for PasswordHashRngCompat {
|
||||
fn next_u32(&mut self) -> u32 {
|
||||
self.0.next_u32()
|
||||
}
|
||||
|
||||
fn next_u64(&mut self) -> u64 {
|
||||
self.0.next_u64()
|
||||
}
|
||||
|
||||
fn fill_bytes(&mut self, dest: &mut [u8]) {
|
||||
self.0.fill_bytes(dest);
|
||||
}
|
||||
|
||||
fn try_fill_bytes(
|
||||
&mut self,
|
||||
dest: &mut [u8],
|
||||
) -> Result<(), scrypt::password_hash::rand_core::Error> {
|
||||
self.fill_bytes(dest);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl scrypt::password_hash::rand_core::CryptoRng for PasswordHashRngCompat {}
|
||||
}
|
||||
|
||||
@@ -256,7 +256,7 @@ impl Database {
|
||||
let test_options = self.test_options.as_ref().unwrap();
|
||||
test_options.executor.simulate_random_delay().await;
|
||||
let fail_probability = *test_options.query_failure_probability.lock();
|
||||
if test_options.executor.rng().gen_bool(fail_probability) {
|
||||
if test_options.executor.rng().random_bool(fail_probability) {
|
||||
return Err(anyhow!("simulated query failure"))?;
|
||||
}
|
||||
|
||||
|
||||
@@ -75,10 +75,10 @@ impl TestDb {
|
||||
static LOCK: Mutex<()> = Mutex::new(());
|
||||
|
||||
let _guard = LOCK.lock();
|
||||
let mut rng = StdRng::from_entropy();
|
||||
let mut rng = StdRng::from_os_rng();
|
||||
let url = format!(
|
||||
"postgres://postgres@localhost/zed-test-{}",
|
||||
rng.r#gen::<u128>()
|
||||
rng.random::<u128>()
|
||||
);
|
||||
let runtime = tokio::runtime::Builder::new_current_thread()
|
||||
.enable_io()
|
||||
|
||||
@@ -2098,7 +2098,7 @@ async fn test_following_after_replacement(cx_a: &mut TestAppContext, cx_b: &mut
|
||||
share_workspace(&workspace, cx_a).await.unwrap();
|
||||
let buffer = workspace.update(cx_a, |workspace, cx| {
|
||||
workspace.project().update(cx, |project, cx| {
|
||||
project.create_local_buffer(&sample_text(26, 5, 'a'), None, cx)
|
||||
project.create_local_buffer(&sample_text(26, 5, 'a'), None, false, cx)
|
||||
})
|
||||
});
|
||||
let multibuffer = cx_a.new(|cx| {
|
||||
|
||||
@@ -2506,7 +2506,7 @@ async fn test_propagate_saves_and_fs_changes(
|
||||
});
|
||||
|
||||
let new_buffer_a = project_a
|
||||
.update(cx_a, |p, cx| p.create_buffer(cx))
|
||||
.update(cx_a, |p, cx| p.create_buffer(false, cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
@@ -5746,7 +5746,7 @@ async fn test_open_buffer_while_getting_definition_pointing_to_it(
|
||||
|
||||
let definitions;
|
||||
let buffer_b2;
|
||||
if rng.r#gen() {
|
||||
if rng.random() {
|
||||
cx_a.run_until_parked();
|
||||
cx_b.run_until_parked();
|
||||
definitions = project_b.update(cx_b, |p, cx| p.definitions(&buffer_b1, 23, cx));
|
||||
|
||||
@@ -84,7 +84,7 @@ impl RandomizedTest for RandomChannelBufferTest {
|
||||
}
|
||||
|
||||
loop {
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
0..=29 => {
|
||||
let channel_name = client.channel_store().read_with(cx, |store, cx| {
|
||||
store.ordered_channels().find_map(|(_, channel)| {
|
||||
|
||||
@@ -17,7 +17,7 @@ use project::{
|
||||
DEFAULT_COMPLETION_CONTEXT, Project, ProjectPath, search::SearchQuery, search::SearchResult,
|
||||
};
|
||||
use rand::{
|
||||
distributions::{Alphanumeric, DistString},
|
||||
distr::{self, SampleString},
|
||||
prelude::*,
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -168,19 +168,19 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
) -> ClientOperation {
|
||||
let call = cx.read(ActiveCall::global);
|
||||
loop {
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
// Mutate the call
|
||||
0..=29 => {
|
||||
// Respond to an incoming call
|
||||
if call.read_with(cx, |call, _| call.incoming().borrow().is_some()) {
|
||||
break if rng.gen_bool(0.7) {
|
||||
break if rng.random_bool(0.7) {
|
||||
ClientOperation::AcceptIncomingCall
|
||||
} else {
|
||||
ClientOperation::RejectIncomingCall
|
||||
};
|
||||
}
|
||||
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
// Invite a contact to the current call
|
||||
0..=70 => {
|
||||
let available_contacts =
|
||||
@@ -212,7 +212,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
|
||||
// Mutate projects
|
||||
30..=59 => match rng.gen_range(0..100_u32) {
|
||||
30..=59 => match rng.random_range(0..100_u32) {
|
||||
// Open a new project
|
||||
0..=70 => {
|
||||
// Open a remote project
|
||||
@@ -270,7 +270,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
|
||||
// Mutate project worktrees
|
||||
81.. => match rng.gen_range(0..100_u32) {
|
||||
81.. => match rng.random_range(0..100_u32) {
|
||||
// Add a worktree to a local project
|
||||
0..=50 => {
|
||||
let Some(project) = client.local_projects().choose(rng).cloned() else {
|
||||
@@ -279,7 +279,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let project_root_name = root_name_for_project(&project, cx);
|
||||
let mut paths = client.fs().paths(false);
|
||||
paths.remove(0);
|
||||
let new_root_path = if paths.is_empty() || rng.r#gen() {
|
||||
let new_root_path = if paths.is_empty() || rng.random() {
|
||||
Path::new(path!("/")).join(plan.next_root_dir_name())
|
||||
} else {
|
||||
paths.choose(rng).unwrap().clone()
|
||||
@@ -309,7 +309,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
.choose(rng)
|
||||
});
|
||||
let Some(worktree) = worktree else { continue };
|
||||
let is_dir = rng.r#gen::<bool>();
|
||||
let is_dir = rng.random::<bool>();
|
||||
let mut full_path =
|
||||
worktree.read_with(cx, |w, _| PathBuf::from(w.root_name()));
|
||||
full_path.push(gen_file_name(rng));
|
||||
@@ -334,7 +334,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let project_root_name = root_name_for_project(&project, cx);
|
||||
let is_local = project.read_with(cx, |project, _| project.is_local());
|
||||
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
// Manipulate an existing buffer
|
||||
0..=70 => {
|
||||
let Some(buffer) = client
|
||||
@@ -349,7 +349,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let full_path = buffer
|
||||
.read_with(cx, |buffer, cx| buffer.file().unwrap().full_path(cx));
|
||||
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
// Close the buffer
|
||||
0..=15 => {
|
||||
break ClientOperation::CloseBuffer {
|
||||
@@ -360,7 +360,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
// Save the buffer
|
||||
16..=29 if buffer.read_with(cx, |b, _| b.is_dirty()) => {
|
||||
let detach = rng.gen_bool(0.3);
|
||||
let detach = rng.random_bool(0.3);
|
||||
break ClientOperation::SaveBuffer {
|
||||
project_root_name,
|
||||
is_local,
|
||||
@@ -383,17 +383,17 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
_ => {
|
||||
let offset = buffer.read_with(cx, |buffer, _| {
|
||||
buffer.clip_offset(
|
||||
rng.gen_range(0..=buffer.len()),
|
||||
rng.random_range(0..=buffer.len()),
|
||||
language::Bias::Left,
|
||||
)
|
||||
});
|
||||
let detach = rng.r#gen();
|
||||
let detach = rng.random();
|
||||
break ClientOperation::RequestLspDataInBuffer {
|
||||
project_root_name,
|
||||
full_path,
|
||||
offset,
|
||||
is_local,
|
||||
kind: match rng.gen_range(0..5_u32) {
|
||||
kind: match rng.random_range(0..5_u32) {
|
||||
0 => LspRequestKind::Rename,
|
||||
1 => LspRequestKind::Highlights,
|
||||
2 => LspRequestKind::Definition,
|
||||
@@ -407,8 +407,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
}
|
||||
|
||||
71..=80 => {
|
||||
let query = rng.gen_range('a'..='z').to_string();
|
||||
let detach = rng.gen_bool(0.3);
|
||||
let query = rng.random_range('a'..='z').to_string();
|
||||
let detach = rng.random_bool(0.3);
|
||||
break ClientOperation::SearchProject {
|
||||
project_root_name,
|
||||
is_local,
|
||||
@@ -460,7 +460,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
|
||||
// Create or update a file or directory
|
||||
96.. => {
|
||||
let is_dir = rng.r#gen::<bool>();
|
||||
let is_dir = rng.random::<bool>();
|
||||
let content;
|
||||
let mut path;
|
||||
let dir_paths = client.fs().directories(false);
|
||||
@@ -470,11 +470,11 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
path = dir_paths.choose(rng).unwrap().clone();
|
||||
path.push(gen_file_name(rng));
|
||||
} else {
|
||||
content = Alphanumeric.sample_string(rng, 16);
|
||||
content = distr::Alphanumeric.sample_string(rng, 16);
|
||||
|
||||
// Create a new file or overwrite an existing file
|
||||
let file_paths = client.fs().files();
|
||||
if file_paths.is_empty() || rng.gen_bool(0.5) {
|
||||
if file_paths.is_empty() || rng.random_bool(0.5) {
|
||||
path = dir_paths.choose(rng).unwrap().clone();
|
||||
path.push(gen_file_name(rng));
|
||||
path.set_extension("rs");
|
||||
@@ -1090,7 +1090,7 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
move |_, cx| {
|
||||
let background = cx.background_executor();
|
||||
let mut rng = background.rng();
|
||||
let count = rng.gen_range::<usize, _>(1..3);
|
||||
let count = rng.random_range::<usize, _>(1..3);
|
||||
let files = fs.as_fake().files();
|
||||
let files = (0..count)
|
||||
.map(|_| files.choose(&mut rng).unwrap().clone())
|
||||
@@ -1117,12 +1117,12 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
let background = cx.background_executor();
|
||||
let mut rng = background.rng();
|
||||
|
||||
let highlight_count = rng.gen_range(1..=5);
|
||||
let highlight_count = rng.random_range(1..=5);
|
||||
for _ in 0..highlight_count {
|
||||
let start_row = rng.gen_range(0..100);
|
||||
let start_column = rng.gen_range(0..100);
|
||||
let end_row = rng.gen_range(0..100);
|
||||
let end_column = rng.gen_range(0..100);
|
||||
let start_row = rng.random_range(0..100);
|
||||
let start_column = rng.random_range(0..100);
|
||||
let end_row = rng.random_range(0..100);
|
||||
let end_column = rng.random_range(0..100);
|
||||
let start = PointUtf16::new(start_row, start_column);
|
||||
let end = PointUtf16::new(end_row, end_column);
|
||||
let range =
|
||||
@@ -1219,8 +1219,8 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
guest_project.remote_id(),
|
||||
);
|
||||
assert_eq!(
|
||||
guest_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false, 0).collect::<Vec<_>>(),
|
||||
guest_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
|
||||
host_snapshot.entries(false, 0).map(null_out_entry_size).collect::<Vec<_>>(),
|
||||
"{} has different snapshot than the host for worktree {:?} ({:?}) and project {:?}",
|
||||
client.username,
|
||||
host_snapshot.abs_path(),
|
||||
@@ -1248,6 +1248,18 @@ impl RandomizedTest for ProjectCollaborationTest {
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
// A hack to work around a hack in
|
||||
// https://github.com/zed-industries/zed/pull/16696 that wasn't
|
||||
// detected until we upgraded the rng crate. This whole crate is
|
||||
// going away with DeltaDB soon, so we hold our nose and
|
||||
// continue.
|
||||
fn null_out_entry_size(entry: &project::Entry) -> project::Entry {
|
||||
project::Entry {
|
||||
size: 0,
|
||||
..entry.clone()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let buffers = client.buffers().clone();
|
||||
@@ -1422,7 +1434,7 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
||||
.filter(|path| path.starts_with(repo_path))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let count = rng.gen_range(0..=paths.len());
|
||||
let count = rng.random_range(0..=paths.len());
|
||||
paths.shuffle(rng);
|
||||
paths.truncate(count);
|
||||
|
||||
@@ -1434,13 +1446,13 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
||||
|
||||
let repo_path = client.fs().directories(false).choose(rng).unwrap().clone();
|
||||
|
||||
match rng.gen_range(0..100_u32) {
|
||||
match rng.random_range(0..100_u32) {
|
||||
0..=25 => {
|
||||
let file_paths = generate_file_paths(&repo_path, rng, client);
|
||||
|
||||
let contents = file_paths
|
||||
.into_iter()
|
||||
.map(|path| (path, Alphanumeric.sample_string(rng, 16)))
|
||||
.map(|path| (path, distr::Alphanumeric.sample_string(rng, 16)))
|
||||
.collect();
|
||||
|
||||
GitOperation::WriteGitIndex {
|
||||
@@ -1449,7 +1461,8 @@ fn generate_git_operation(rng: &mut StdRng, client: &TestClient) -> GitOperation
|
||||
}
|
||||
}
|
||||
26..=63 => {
|
||||
let new_branch = (rng.gen_range(0..10) > 3).then(|| Alphanumeric.sample_string(rng, 8));
|
||||
let new_branch =
|
||||
(rng.random_range(0..10) > 3).then(|| distr::Alphanumeric.sample_string(rng, 8));
|
||||
|
||||
GitOperation::WriteGitBranch {
|
||||
repo_path,
|
||||
@@ -1596,7 +1609,7 @@ fn choose_random_project(client: &TestClient, rng: &mut StdRng) -> Option<Entity
|
||||
fn gen_file_name(rng: &mut StdRng) -> String {
|
||||
let mut name = String::new();
|
||||
for _ in 0..10 {
|
||||
let letter = rng.gen_range('a'..='z');
|
||||
let letter = rng.random_range('a'..='z');
|
||||
name.push(letter);
|
||||
}
|
||||
name
|
||||
@@ -1604,7 +1617,7 @@ fn gen_file_name(rng: &mut StdRng) -> String {
|
||||
|
||||
fn gen_status(rng: &mut StdRng) -> FileStatus {
|
||||
fn gen_tracked_status(rng: &mut StdRng) -> TrackedStatus {
|
||||
match rng.gen_range(0..3) {
|
||||
match rng.random_range(0..3) {
|
||||
0 => TrackedStatus {
|
||||
index_status: StatusCode::Unmodified,
|
||||
worktree_status: StatusCode::Unmodified,
|
||||
@@ -1626,7 +1639,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
|
||||
}
|
||||
|
||||
fn gen_unmerged_status_code(rng: &mut StdRng) -> UnmergedStatusCode {
|
||||
match rng.gen_range(0..3) {
|
||||
match rng.random_range(0..3) {
|
||||
0 => UnmergedStatusCode::Updated,
|
||||
1 => UnmergedStatusCode::Added,
|
||||
2 => UnmergedStatusCode::Deleted,
|
||||
@@ -1634,7 +1647,7 @@ fn gen_status(rng: &mut StdRng) -> FileStatus {
|
||||
}
|
||||
}
|
||||
|
||||
match rng.gen_range(0..2) {
|
||||
match rng.random_range(0..2) {
|
||||
0 => FileStatus::Unmerged(UnmergedStatus {
|
||||
first_head: gen_unmerged_status_code(rng),
|
||||
second_head: gen_unmerged_status_code(rng),
|
||||
|
||||
@@ -208,9 +208,9 @@ pub fn save_randomized_test_plan() {
|
||||
|
||||
impl<T: RandomizedTest> TestPlan<T> {
|
||||
pub async fn new(server: &mut TestServer, mut rng: StdRng) -> Arc<Mutex<Self>> {
|
||||
let allow_server_restarts = rng.gen_bool(0.7);
|
||||
let allow_client_reconnection = rng.gen_bool(0.7);
|
||||
let allow_client_disconnection = rng.gen_bool(0.1);
|
||||
let allow_server_restarts = rng.random_bool(0.7);
|
||||
let allow_client_reconnection = rng.random_bool(0.7);
|
||||
let allow_client_disconnection = rng.random_bool(0.1);
|
||||
|
||||
let mut users = Vec::new();
|
||||
for ix in 0..max_peers() {
|
||||
@@ -407,7 +407,7 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
}
|
||||
|
||||
Some(loop {
|
||||
break match self.rng.gen_range(0..100) {
|
||||
break match self.rng.random_range(0..100) {
|
||||
0..=29 if clients.len() < self.users.len() => {
|
||||
let user = self
|
||||
.users
|
||||
@@ -421,13 +421,13 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
}
|
||||
}
|
||||
30..=34 if clients.len() > 1 && self.allow_client_disconnection => {
|
||||
let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
|
||||
let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
|
||||
let user_id = client.current_user_id(cx);
|
||||
self.operation_ix += 1;
|
||||
ServerOperation::RemoveConnection { user_id }
|
||||
}
|
||||
35..=39 if clients.len() > 1 && self.allow_client_reconnection => {
|
||||
let (client, cx) = &clients[self.rng.gen_range(0..clients.len())];
|
||||
let (client, cx) = &clients[self.rng.random_range(0..clients.len())];
|
||||
let user_id = client.current_user_id(cx);
|
||||
self.operation_ix += 1;
|
||||
ServerOperation::BounceConnection { user_id }
|
||||
@@ -439,12 +439,12 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
_ if !clients.is_empty() => {
|
||||
let count = self
|
||||
.rng
|
||||
.gen_range(1..10)
|
||||
.random_range(1..10)
|
||||
.min(self.max_operations - self.operation_ix);
|
||||
let batch_id = util::post_inc(&mut self.next_batch_id);
|
||||
let mut user_ids = (0..count)
|
||||
.map(|_| {
|
||||
let ix = self.rng.gen_range(0..clients.len());
|
||||
let ix = self.rng.random_range(0..clients.len());
|
||||
let (client, cx) = &clients[ix];
|
||||
client.current_user_id(cx)
|
||||
})
|
||||
@@ -453,7 +453,7 @@ impl<T: RandomizedTest> TestPlan<T> {
|
||||
ServerOperation::MutateClients {
|
||||
user_ids,
|
||||
batch_id,
|
||||
quiesce: self.rng.gen_bool(0.7),
|
||||
quiesce: self.rng.random_bool(0.7),
|
||||
}
|
||||
}
|
||||
_ => continue,
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
use gpui::Pixels;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
use workspace::dock::DockPosition;
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
@@ -27,7 +27,8 @@ pub struct ChatPanelSettings {
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "chat_panel")]
|
||||
pub struct ChatPanelSettingsContent {
|
||||
/// When to show the panel button in the status bar.
|
||||
///
|
||||
@@ -43,14 +44,8 @@ pub struct ChatPanelSettingsContent {
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct NotificationPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "collaboration_panel")]
|
||||
pub struct PanelSettingsContent {
|
||||
/// Whether to show the panel button in the status bar.
|
||||
///
|
||||
@@ -66,7 +61,32 @@ pub struct PanelSettingsContent {
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi)]
|
||||
#[derive(Deserialize, Debug)]
|
||||
pub struct NotificationPanelSettings {
|
||||
pub button: bool,
|
||||
pub dock: DockPosition,
|
||||
pub default_width: Pixels,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "notification_panel")]
|
||||
pub struct NotificationPanelSettingsContent {
|
||||
/// Whether to show the panel button in the status bar.
|
||||
///
|
||||
/// Default: true
|
||||
pub button: Option<bool>,
|
||||
/// Where to dock the panel.
|
||||
///
|
||||
/// Default: right
|
||||
pub dock: Option<DockPosition>,
|
||||
/// Default width of the panel in pixels.
|
||||
///
|
||||
/// Default: 300
|
||||
pub default_width: Option<f32>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug, SettingsUi, SettingsKey)]
|
||||
#[settings_key(key = "message_editor")]
|
||||
pub struct MessageEditorSettings {
|
||||
/// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
/// For example: typing `:wave:` gets replaced with `👋`.
|
||||
@@ -76,8 +96,6 @@ pub struct MessageEditorSettings {
|
||||
}
|
||||
|
||||
impl Settings for CollaborationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("collaboration_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
@@ -91,8 +109,6 @@ impl Settings for CollaborationPanelSettings {
|
||||
}
|
||||
|
||||
impl Settings for ChatPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("chat_panel");
|
||||
|
||||
type FileContent = ChatPanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
@@ -106,9 +122,7 @@ impl Settings for ChatPanelSettings {
|
||||
}
|
||||
|
||||
impl Settings for NotificationPanelSettings {
|
||||
const KEY: Option<&'static str> = Some("notification_panel");
|
||||
|
||||
type FileContent = PanelSettingsContent;
|
||||
type FileContent = NotificationPanelSettingsContent;
|
||||
|
||||
fn load(
|
||||
sources: SettingsSources<Self::FileContent>,
|
||||
@@ -121,8 +135,6 @@ impl Settings for NotificationPanelSettings {
|
||||
}
|
||||
|
||||
impl Settings for MessageEditorSettings {
|
||||
const KEY: Option<&'static str> = Some("message_editor");
|
||||
|
||||
type FileContent = MessageEditorSettings;
|
||||
|
||||
fn load(
|
||||
|
||||
@@ -76,7 +76,7 @@ impl CommandPaletteFilter {
|
||||
}
|
||||
|
||||
/// Hides all actions with the given types.
|
||||
pub fn hide_action_types(&mut self, action_types: &[TypeId]) {
|
||||
pub fn hide_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
|
||||
for action_type in action_types {
|
||||
self.hidden_action_types.insert(*action_type);
|
||||
self.shown_action_types.remove(action_type);
|
||||
@@ -84,7 +84,7 @@ impl CommandPaletteFilter {
|
||||
}
|
||||
|
||||
/// Shows all actions with the given types.
|
||||
pub fn show_action_types<'a>(&mut self, action_types: impl Iterator<Item = &'a TypeId>) {
|
||||
pub fn show_action_types<'a>(&mut self, action_types: impl IntoIterator<Item = &'a TypeId>) {
|
||||
for action_type in action_types {
|
||||
self.shown_action_types.insert(*action_type);
|
||||
self.hidden_action_types.remove(action_type);
|
||||
|
||||
@@ -20,5 +20,8 @@ strum.workspace = true
|
||||
theme.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
documented.workspace = true
|
||||
|
||||
[features]
|
||||
default = []
|
||||
|
||||
@@ -227,6 +227,8 @@ pub trait Component {
|
||||
/// Example:
|
||||
///
|
||||
/// ```
|
||||
/// use documented::Documented;
|
||||
///
|
||||
/// /// This is a doc comment.
|
||||
/// #[derive(Documented)]
|
||||
/// struct MyComponent;
|
||||
|
||||
@@ -1095,7 +1095,7 @@ impl Copilot {
|
||||
_ => {
|
||||
filter.hide_action_types(&signed_in_actions);
|
||||
filter.hide_action_types(&auth_actions);
|
||||
filter.show_action_types(no_auth_actions.iter());
|
||||
filter.show_action_types(&no_auth_actions);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use dap_types::SteppingGranularity;
|
||||
use gpui::{App, Global};
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources, SettingsUi};
|
||||
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
|
||||
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq, Eq, SettingsUi)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -12,11 +12,12 @@ pub enum DebugPanelDockPosition {
|
||||
Right,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi)]
|
||||
#[derive(Serialize, Deserialize, JsonSchema, Clone, Copy, SettingsUi, SettingsKey)]
|
||||
#[serde(default)]
|
||||
// todo(settings_ui) @ben: I'm pretty sure not having the fields be optional here is a bug,
|
||||
// it means the defaults will override previously set values if a single key is missing
|
||||
#[settings_ui(group = "Debugger", path = "debugger")]
|
||||
#[settings_ui(group = "Debugger")]
|
||||
#[settings_key(key = "debugger")]
|
||||
pub struct DebuggerSettings {
|
||||
/// Determines the stepping granularity.
|
||||
///
|
||||
@@ -64,8 +65,6 @@ impl Default for DebuggerSettings {
|
||||
}
|
||||
|
||||
impl Settings for DebuggerSettings {
|
||||
const KEY: Option<&'static str> = Some("debugger");
|
||||
|
||||
type FileContent = Self;
|
||||
|
||||
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> anyhow::Result<Self> {
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use dap::{DapRegistry, DebugRequest};
|
||||
use fuzzy::{StringMatch, StringMatchCandidate};
|
||||
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render};
|
||||
use gpui::{AppContext, DismissEvent, Entity, EventEmitter, Focusable, Render, Task};
|
||||
use gpui::{Subscription, WeakEntity};
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use project::Project;
|
||||
use rpc::proto;
|
||||
use task::ZedDebugConfig;
|
||||
use util::debug_panic;
|
||||
|
||||
@@ -56,29 +58,28 @@ impl AttachModal {
|
||||
pub fn new(
|
||||
definition: ZedDebugConfig,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
modal: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mut processes: Box<[_]> = System::new_all()
|
||||
.processes()
|
||||
.values()
|
||||
.map(|process| {
|
||||
let name = process.name().to_string_lossy().into_owned();
|
||||
Candidate {
|
||||
name: name.into(),
|
||||
pid: process.pid().as_u32(),
|
||||
command: process
|
||||
.cmd()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
processes.sort_by_key(|k| k.name.clone());
|
||||
let processes = processes.into_iter().collect();
|
||||
Self::with_processes(workspace, definition, processes, modal, window, cx)
|
||||
let processes_task = get_processes_for_project(&project, cx);
|
||||
|
||||
let modal = Self::with_processes(workspace, definition, Arc::new([]), modal, window, cx);
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let processes = processes_task.await;
|
||||
this.update_in(cx, |modal, window, cx| {
|
||||
modal.picker.update(cx, |picker, cx| {
|
||||
picker.delegate.candidates = processes;
|
||||
picker.refresh(window, cx);
|
||||
});
|
||||
})?;
|
||||
anyhow::Ok(())
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
modal
|
||||
}
|
||||
|
||||
pub(super) fn with_processes(
|
||||
@@ -332,6 +333,57 @@ impl PickerDelegate for AttachModalDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
fn get_processes_for_project(project: &Entity<Project>, cx: &mut App) -> Task<Arc<[Candidate]>> {
|
||||
let project = project.read(cx);
|
||||
|
||||
if let Some(remote_client) = project.remote_client() {
|
||||
let proto_client = remote_client.read(cx).proto_client();
|
||||
cx.spawn(async move |_cx| {
|
||||
let response = proto_client
|
||||
.request(proto::GetProcesses {
|
||||
project_id: proto::REMOTE_SERVER_PROJECT_ID,
|
||||
})
|
||||
.await
|
||||
.unwrap_or_else(|_| proto::GetProcessesResponse {
|
||||
processes: Vec::new(),
|
||||
});
|
||||
|
||||
let mut processes: Vec<Candidate> = response
|
||||
.processes
|
||||
.into_iter()
|
||||
.map(|p| Candidate {
|
||||
pid: p.pid,
|
||||
name: p.name.into(),
|
||||
command: p.command,
|
||||
})
|
||||
.collect();
|
||||
|
||||
processes.sort_by_key(|k| k.name.clone());
|
||||
Arc::from(processes.into_boxed_slice())
|
||||
})
|
||||
} else {
|
||||
let mut processes: Box<[_]> = System::new_all()
|
||||
.processes()
|
||||
.values()
|
||||
.map(|process| {
|
||||
let name = process.name().to_string_lossy().into_owned();
|
||||
Candidate {
|
||||
name: name.into(),
|
||||
pid: process.pid().as_u32(),
|
||||
command: process
|
||||
.cmd()
|
||||
.iter()
|
||||
.map(|s| s.to_string_lossy().to_string())
|
||||
.collect::<Vec<_>>(),
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
processes.sort_by_key(|k| k.name.clone());
|
||||
let processes = processes.into_iter().collect();
|
||||
Task::ready(processes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub(crate) fn _process_names(modal: &AttachModal, cx: &mut Context<AttachModal>) -> Vec<String> {
|
||||
modal.picker.read_with(cx, |picker, _| {
|
||||
|
||||
@@ -13,11 +13,8 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use collections::IndexMap;
|
||||
use dap::adapters::DebugAdapterName;
|
||||
use dap::debugger_settings::DebugPanelDockPosition;
|
||||
use dap::{
|
||||
ContinuedEvent, LoadedSourceEvent, ModuleEvent, OutputEvent, StoppedEvent, ThreadEvent,
|
||||
client::SessionId, debugger_settings::DebuggerSettings,
|
||||
};
|
||||
use dap::{DapRegistry, StartDebuggingRequestArguments};
|
||||
use dap::{client::SessionId, debugger_settings::DebuggerSettings};
|
||||
use editor::Editor;
|
||||
use gpui::{
|
||||
Action, App, AsyncWindowContext, ClipboardItem, Context, DismissEvent, Entity, EntityId,
|
||||
@@ -46,23 +43,6 @@ use workspace::{
|
||||
};
|
||||
use zed_actions::ToggleFocus;
|
||||
|
||||
pub enum DebugPanelEvent {
|
||||
Exited(SessionId),
|
||||
Terminated(SessionId),
|
||||
Stopped {
|
||||
client_id: SessionId,
|
||||
event: StoppedEvent,
|
||||
go_to_stack_frame: bool,
|
||||
},
|
||||
Thread((SessionId, ThreadEvent)),
|
||||
Continued((SessionId, ContinuedEvent)),
|
||||
Output((SessionId, OutputEvent)),
|
||||
Module((SessionId, ModuleEvent)),
|
||||
LoadedSource((SessionId, LoadedSourceEvent)),
|
||||
ClientShutdown(SessionId),
|
||||
CapabilitiesChanged(SessionId),
|
||||
}
|
||||
|
||||
pub struct DebugPanel {
|
||||
size: Pixels,
|
||||
active_session: Option<Entity<DebugSession>>,
|
||||
@@ -1407,7 +1387,6 @@ async fn register_session_inner(
|
||||
}
|
||||
|
||||
impl EventEmitter<PanelEvent> for DebugPanel {}
|
||||
impl EventEmitter<DebugPanelEvent> for DebugPanel {}
|
||||
|
||||
impl Focusable for DebugPanel {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
|
||||
@@ -113,23 +113,6 @@ impl DebugPanel {
|
||||
}
|
||||
};
|
||||
session_entries.push(root_entry);
|
||||
|
||||
session_entries.extend(
|
||||
sessions_with_children
|
||||
.by_ref()
|
||||
.take_while(|(session, _)| {
|
||||
session
|
||||
.read(cx)
|
||||
.session(cx)
|
||||
.read(cx)
|
||||
.parent_id(cx)
|
||||
.is_some()
|
||||
})
|
||||
.map(|(session, _)| SessionListEntry {
|
||||
leaf: session.clone(),
|
||||
ancestors: vec![],
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
let weak = cx.weak_entity();
|
||||
|
||||
@@ -20,7 +20,7 @@ use gpui::{
|
||||
};
|
||||
use itertools::Itertools as _;
|
||||
use picker::{Picker, PickerDelegate, highlighted_match_with_paths::HighlightedMatch};
|
||||
use project::{DebugScenarioContext, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use project::{DebugScenarioContext, Project, TaskContexts, TaskSourceKind, task_store::TaskStore};
|
||||
use settings::Settings;
|
||||
use task::{DebugScenario, RevealTarget, ZedDebugConfig};
|
||||
use theme::ThemeSettings;
|
||||
@@ -88,8 +88,10 @@ impl NewProcessModal {
|
||||
})?;
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let workspace_handle = workspace.weak_handle();
|
||||
let project = workspace.project().clone();
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
let attach_mode = AttachMode::new(None, workspace_handle.clone(), window, cx);
|
||||
let attach_mode =
|
||||
AttachMode::new(None, workspace_handle.clone(), project, window, cx);
|
||||
|
||||
let debug_picker = cx.new(|cx| {
|
||||
let delegate =
|
||||
@@ -940,6 +942,7 @@ impl AttachMode {
|
||||
pub(super) fn new(
|
||||
debugger: Option<DebugAdapterName>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<NewProcessModal>,
|
||||
) -> Entity<Self> {
|
||||
@@ -950,7 +953,7 @@ impl AttachMode {
|
||||
stop_on_entry: Some(false),
|
||||
};
|
||||
let attach_picker = cx.new(|cx| {
|
||||
let modal = AttachModal::new(definition.clone(), workspace, false, window, cx);
|
||||
let modal = AttachModal::new(definition.clone(), workspace, project, false, window, cx);
|
||||
window.focus(&modal.focus_handle(cx));
|
||||
|
||||
modal
|
||||
|
||||
@@ -2,9 +2,7 @@ pub mod running;
|
||||
|
||||
use crate::{StackTraceView, persistence::SerializedLayout, session::running::DebugTerminal};
|
||||
use dap::client::SessionId;
|
||||
use gpui::{
|
||||
App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
|
||||
};
|
||||
use gpui::{App, Axis, Entity, EventEmitter, FocusHandle, Focusable, Task, WeakEntity};
|
||||
use project::debugger::session::Session;
|
||||
use project::worktree_store::WorktreeStore;
|
||||
use project::{Project, debugger::session::SessionQuirks};
|
||||
@@ -24,13 +22,6 @@ pub struct DebugSession {
|
||||
stack_trace_view: OnceCell<Entity<StackTraceView>>,
|
||||
_worktree_store: WeakEntity<WorktreeStore>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
_subscriptions: [Subscription; 1],
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum DebugPanelItemEvent {
|
||||
Close,
|
||||
Stopped { go_to_stack_frame: bool },
|
||||
}
|
||||
|
||||
impl DebugSession {
|
||||
@@ -59,9 +50,6 @@ impl DebugSession {
|
||||
let quirks = session.read(cx).quirks();
|
||||
|
||||
cx.new(|cx| Self {
|
||||
_subscriptions: [cx.subscribe(&running_state, |_, _, _, cx| {
|
||||
cx.notify();
|
||||
})],
|
||||
remote_id: None,
|
||||
running_state,
|
||||
quirks,
|
||||
@@ -133,7 +121,7 @@ impl DebugSession {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DebugPanelItemEvent> for DebugSession {}
|
||||
impl EventEmitter<()> for DebugSession {}
|
||||
|
||||
impl Focusable for DebugSession {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
@@ -142,7 +130,7 @@ impl Focusable for DebugSession {
|
||||
}
|
||||
|
||||
impl Item for DebugSession {
|
||||
type Event = DebugPanelItemEvent;
|
||||
type Event = ();
|
||||
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
||||
"Debugger".into()
|
||||
}
|
||||
|
||||
@@ -14,7 +14,6 @@ use crate::{
|
||||
session::running::memory_view::MemoryView,
|
||||
};
|
||||
|
||||
use super::DebugPanelItemEvent;
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
use breakpoint_list::BreakpointList;
|
||||
use collections::{HashMap, IndexMap};
|
||||
@@ -1826,8 +1825,6 @@ impl RunningState {
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DebugPanelItemEvent> for RunningState {}
|
||||
|
||||
impl Focusable for RunningState {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
|
||||
@@ -28,8 +28,8 @@ pub enum StackFrameListEvent {
|
||||
}
|
||||
|
||||
/// Represents the filter applied to the stack frame list
|
||||
#[derive(PartialEq, Eq, Copy, Clone)]
|
||||
enum StackFrameFilter {
|
||||
#[derive(PartialEq, Eq, Copy, Clone, Debug)]
|
||||
pub(crate) enum StackFrameFilter {
|
||||
/// Show all frames
|
||||
All,
|
||||
/// Show only frames from the user's code
|
||||
@@ -174,19 +174,29 @@ impl StackFrameList {
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn dap_stack_frames(&self, cx: &mut App) -> Vec<dap::StackFrame> {
|
||||
self.stack_frames(cx)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.filter(|(ix, _)| {
|
||||
self.list_filter == StackFrameFilter::All
|
||||
|| self
|
||||
.filter_entries_indices
|
||||
.binary_search_by_key(&ix, |ix| ix)
|
||||
.is_ok()
|
||||
})
|
||||
.map(|(_, stack_frame)| stack_frame.dap)
|
||||
.collect()
|
||||
match self.list_filter {
|
||||
StackFrameFilter::All => self
|
||||
.stack_frames(cx)
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.map(|stack_frame| stack_frame.dap)
|
||||
.collect(),
|
||||
StackFrameFilter::OnlyUserFrames => self
|
||||
.filter_entries_indices
|
||||
.iter()
|
||||
.map(|ix| match &self.entries[*ix] {
|
||||
StackFrameEntry::Label(label) => label,
|
||||
StackFrameEntry::Collapsed(_) => panic!("Collapsed tabs should not be visible"),
|
||||
StackFrameEntry::Normal(frame) => frame,
|
||||
})
|
||||
.cloned()
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn list_filter(&self) -> StackFrameFilter {
|
||||
self.list_filter
|
||||
}
|
||||
|
||||
pub fn opened_stack_frame_id(&self) -> Option<StackFrameId> {
|
||||
@@ -246,6 +256,7 @@ impl StackFrameList {
|
||||
self.entries.clear();
|
||||
self.selected_ix = None;
|
||||
self.list_state.reset(0);
|
||||
self.filter_entries_indices.clear();
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
cx.notify();
|
||||
return;
|
||||
@@ -263,7 +274,7 @@ impl StackFrameList {
|
||||
.unwrap_or_default();
|
||||
|
||||
let mut filter_entries_indices = Vec::default();
|
||||
for (ix, stack_frame) in stack_frames.iter().enumerate() {
|
||||
for stack_frame in stack_frames.iter() {
|
||||
let frame_in_visible_worktree = stack_frame.dap.source.as_ref().is_some_and(|source| {
|
||||
source.path.as_ref().is_some_and(|path| {
|
||||
worktree_prefixes
|
||||
@@ -273,10 +284,6 @@ impl StackFrameList {
|
||||
})
|
||||
});
|
||||
|
||||
if frame_in_visible_worktree {
|
||||
filter_entries_indices.push(ix);
|
||||
}
|
||||
|
||||
match stack_frame.dap.presentation_hint {
|
||||
Some(dap::StackFramePresentationHint::Deemphasize)
|
||||
| Some(dap::StackFramePresentationHint::Subtle) => {
|
||||
@@ -302,6 +309,9 @@ impl StackFrameList {
|
||||
first_stack_frame_with_path.get_or_insert(entries.len());
|
||||
}
|
||||
entries.push(StackFrameEntry::Normal(stack_frame.dap.clone()));
|
||||
if frame_in_visible_worktree {
|
||||
filter_entries_indices.push(entries.len() - 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -309,7 +319,6 @@ impl StackFrameList {
|
||||
let collapsed_entries = std::mem::take(&mut collapsed_entries);
|
||||
if !collapsed_entries.is_empty() {
|
||||
entries.push(StackFrameEntry::Collapsed(collapsed_entries));
|
||||
self.filter_entries_indices.push(entries.len() - 1);
|
||||
}
|
||||
self.entries = entries;
|
||||
self.filter_entries_indices = filter_entries_indices;
|
||||
@@ -612,7 +621,16 @@ impl StackFrameList {
|
||||
let entries = std::mem::take(stack_frames)
|
||||
.into_iter()
|
||||
.map(StackFrameEntry::Normal);
|
||||
// HERE
|
||||
let entries_len = entries.len();
|
||||
self.entries.splice(ix..ix + 1, entries);
|
||||
let (Ok(filtered_indices_start) | Err(filtered_indices_start)) =
|
||||
self.filter_entries_indices.binary_search(&ix);
|
||||
|
||||
for idx in &mut self.filter_entries_indices[filtered_indices_start..] {
|
||||
*idx += entries_len - 1;
|
||||
}
|
||||
|
||||
self.selected_ix = Some(ix);
|
||||
self.list_state.reset(self.entries.len());
|
||||
cx.emit(StackFrameListEvent::BuiltEntries);
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{
|
||||
debugger_panel::DebugPanel,
|
||||
session::running::stack_frame_list::StackFrameEntry,
|
||||
session::running::stack_frame_list::{StackFrameEntry, StackFrameFilter},
|
||||
tests::{active_debug_session_panel, init_test, init_test_workspace, start_debug_session},
|
||||
};
|
||||
use dap::{
|
||||
@@ -867,6 +867,28 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
},
|
||||
StackFrame {
|
||||
id: 4,
|
||||
name: "node:internal/modules/run_main2".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("run_main.js".into()),
|
||||
path: Some(path!("/usr/lib/node/internal/modules/run_main2.js").into()),
|
||||
source_reference: None,
|
||||
presentation_hint: None,
|
||||
origin: None,
|
||||
sources: None,
|
||||
adapter_data: None,
|
||||
checksums: None,
|
||||
}),
|
||||
line: 50,
|
||||
column: 1,
|
||||
end_line: None,
|
||||
end_column: None,
|
||||
can_restart: None,
|
||||
instruction_pointer_reference: None,
|
||||
module_id: None,
|
||||
presentation_hint: Some(dap::StackFramePresentationHint::Deemphasize),
|
||||
},
|
||||
StackFrame {
|
||||
id: 5,
|
||||
name: "doSomething".into(),
|
||||
source: Some(dap::Source {
|
||||
name: Some("test.js".into()),
|
||||
@@ -957,83 +979,119 @@ async fn test_stack_frame_filter(executor: BackgroundExecutor, cx: &mut TestAppC
|
||||
|
||||
cx.run_until_parked();
|
||||
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
|
||||
let stack_frame_list = debug_panel_item
|
||||
.running_state()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
let stack_frame_list =
|
||||
active_debug_session_panel(workspace, cx).update_in(cx, |debug_panel_item, window, cx| {
|
||||
let stack_frame_list = debug_panel_item
|
||||
.running_state()
|
||||
.update(cx, |state, _| state.stack_frame_list().clone());
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.build_entries(true, window, cx);
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list.build_entries(true, window, cx);
|
||||
|
||||
// Verify we have the expected collapsed structure
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Collapsed(vec![
|
||||
stack_frames_for_assertions[1].clone(),
|
||||
stack_frames_for_assertions[2].clone()
|
||||
]),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
]
|
||||
);
|
||||
// Verify we have the expected collapsed structure
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Collapsed(vec![
|
||||
stack_frames_for_assertions[1].clone(),
|
||||
stack_frames_for_assertions[2].clone(),
|
||||
stack_frames_for_assertions[3].clone()
|
||||
]),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
// Test 1: Verify filtering works
|
||||
let all_frames = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(all_frames.len(), 4, "Should see all 4 frames initially");
|
||||
|
||||
// Toggle to user frames only
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
let user_frames = stack_frame_list.dap_stack_frames(cx);
|
||||
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
|
||||
assert_eq!(user_frames[0].name, "main");
|
||||
assert_eq!(user_frames[1].name, "doSomething");
|
||||
|
||||
// Test 2: Verify filtering toggles correctly
|
||||
// Check we can toggle back and see all frames again
|
||||
|
||||
// Toggle back to all frames
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
let all_frames_again = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(
|
||||
all_frames_again.len(),
|
||||
4,
|
||||
"Should see all 4 frames after toggling back"
|
||||
);
|
||||
|
||||
// Test 3: Verify collapsed entries stay expanded
|
||||
stack_frame_list.expand_collapsed_entry(1, cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
]
|
||||
);
|
||||
|
||||
// Toggle filter twice
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
|
||||
// Verify entries remain expanded
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
],
|
||||
"Expanded entries should remain expanded after toggling filter"
|
||||
);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
let all_frames = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(all_frames.len(), 5, "Should see all 5 frames initially");
|
||||
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.list_filter(),
|
||||
StackFrameFilter::OnlyUserFrames
|
||||
);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
let user_frames = stack_frame_list.dap_stack_frames(cx);
|
||||
assert_eq!(user_frames.len(), 2, "Should only see 2 user frames");
|
||||
assert_eq!(user_frames[0].name, "main");
|
||||
assert_eq!(user_frames[1].name, "doSomething");
|
||||
|
||||
// Toggle back to all frames
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
let all_frames_again = stack_frame_list.flatten_entries(true, false);
|
||||
assert_eq!(
|
||||
all_frames_again.len(),
|
||||
5,
|
||||
"Should see all 5 frames after toggling back"
|
||||
);
|
||||
|
||||
// Test 3: Verify collapsed entries stay expanded
|
||||
stack_frame_list.expand_collapsed_entry(1, cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
|
||||
]
|
||||
);
|
||||
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.list_filter(),
|
||||
StackFrameFilter::OnlyUserFrames
|
||||
);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(stack_frame_list.list_filter(), StackFrameFilter::All);
|
||||
});
|
||||
|
||||
stack_frame_list.update(cx, |stack_frame_list, cx| {
|
||||
stack_frame_list
|
||||
.toggle_frame_filter(Some(project::debugger::session::ThreadStatus::Stopped), cx);
|
||||
assert_eq!(
|
||||
stack_frame_list.list_filter(),
|
||||
StackFrameFilter::OnlyUserFrames
|
||||
);
|
||||
|
||||
assert_eq!(
|
||||
stack_frame_list.dap_stack_frames(cx).as_slice(),
|
||||
&[
|
||||
stack_frames_for_assertions[0].clone(),
|
||||
stack_frames_for_assertions[4].clone()
|
||||
]
|
||||
);
|
||||
|
||||
// Verify entries remain expanded
|
||||
assert_eq!(
|
||||
stack_frame_list.entries(),
|
||||
&vec![
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[0].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[1].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[2].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[3].clone()),
|
||||
StackFrameEntry::Normal(stack_frames_for_assertions[4].clone()),
|
||||
],
|
||||
"Expanded entries should remain expanded after toggling filter"
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -96,7 +96,7 @@ impl Model {
|
||||
|
||||
pub fn max_token_count(&self) -> u64 {
|
||||
match self {
|
||||
Self::Chat | Self::Reasoner => 64_000,
|
||||
Self::Chat | Self::Reasoner => 128_000,
|
||||
Self::Custom { max_tokens, .. } => *max_tokens,
|
||||
}
|
||||
}
|
||||
@@ -104,7 +104,7 @@ impl Model {
|
||||
pub fn max_output_tokens(&self) -> Option<u64> {
|
||||
match self {
|
||||
Self::Chat => Some(8_192),
|
||||
Self::Reasoner => Some(8_192),
|
||||
Self::Reasoner => Some(64_000),
|
||||
Self::Custom {
|
||||
max_output_tokens, ..
|
||||
} => *max_output_tokens,
|
||||
|
||||
982
crates/diagnostics/src/buffer_diagnostics.rs
Normal file
982
crates/diagnostics/src/buffer_diagnostics.rs
Normal file
@@ -0,0 +1,982 @@
|
||||
use crate::{
|
||||
DIAGNOSTICS_UPDATE_DELAY, IncludeWarnings, ToggleWarnings, context_range_for_entry,
|
||||
diagnostic_renderer::{DiagnosticBlock, DiagnosticRenderer},
|
||||
toolbar_controls::DiagnosticsToolbarEditor,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use collections::HashMap;
|
||||
use editor::{
|
||||
Editor, EditorEvent, ExcerptRange, MultiBuffer, PathKey,
|
||||
display_map::{BlockPlacement, BlockProperties, BlockStyle, CustomBlockId},
|
||||
multibuffer_context_lines,
|
||||
};
|
||||
use gpui::{
|
||||
AnyElement, App, AppContext, Context, Entity, EntityId, EventEmitter, FocusHandle, Focusable,
|
||||
InteractiveElement, IntoElement, ParentElement, Render, SharedString, Styled, Subscription,
|
||||
Task, WeakEntity, Window, actions, div,
|
||||
};
|
||||
use language::{Buffer, DiagnosticEntry, Point};
|
||||
use project::{
|
||||
DiagnosticSummary, Event, Project, ProjectItem, ProjectPath,
|
||||
project_settings::{DiagnosticSeverity, ProjectSettings},
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::{
|
||||
any::{Any, TypeId},
|
||||
cmp::Ordering,
|
||||
sync::Arc,
|
||||
};
|
||||
use text::{Anchor, BufferSnapshot, OffsetRangeExt};
|
||||
use ui::{Button, ButtonStyle, Icon, IconName, Label, Tooltip, h_flex, prelude::*};
|
||||
use util::paths::PathExt;
|
||||
use workspace::{
|
||||
ItemHandle, ItemNavHistory, ToolbarItemLocation, Workspace,
|
||||
item::{BreadcrumbText, Item, ItemEvent, TabContentParams},
|
||||
};
|
||||
|
||||
actions!(
|
||||
diagnostics,
|
||||
[
|
||||
/// Opens the project diagnostics view for the currently focused file.
|
||||
DeployCurrentFile,
|
||||
]
|
||||
);
|
||||
|
||||
/// The `BufferDiagnosticsEditor` is meant to be used when dealing specifically
|
||||
/// with diagnostics for a single buffer, as only the excerpts of the buffer
|
||||
/// where diagnostics are available are displayed.
|
||||
pub(crate) struct BufferDiagnosticsEditor {
|
||||
pub project: Entity<Project>,
|
||||
focus_handle: FocusHandle,
|
||||
editor: Entity<Editor>,
|
||||
/// The current diagnostic entries in the `BufferDiagnosticsEditor`. Used to
|
||||
/// allow quick comparison of updated diagnostics, to confirm if anything
|
||||
/// has changed.
|
||||
pub(crate) diagnostics: Vec<DiagnosticEntry<Anchor>>,
|
||||
/// The blocks used to display the diagnostics' content in the editor, next
|
||||
/// to the excerpts where the diagnostic originated.
|
||||
blocks: Vec<CustomBlockId>,
|
||||
/// Multibuffer to contain all excerpts that contain diagnostics, which are
|
||||
/// to be rendered in the editor.
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
/// The buffer for which the editor is displaying diagnostics and excerpts
|
||||
/// for.
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
/// The path for which the editor is displaying diagnostics for.
|
||||
project_path: ProjectPath,
|
||||
/// Summary of the number of warnings and errors for the path. Used to
|
||||
/// display the number of warnings and errors in the tab's content.
|
||||
summary: DiagnosticSummary,
|
||||
/// Whether to include warnings in the list of diagnostics shown in the
|
||||
/// editor.
|
||||
pub(crate) include_warnings: bool,
|
||||
/// Keeps track of whether there's a background task already running to
|
||||
/// update the excerpts, in order to avoid firing multiple tasks for this purpose.
|
||||
pub(crate) update_excerpts_task: Option<Task<Result<()>>>,
|
||||
/// The project's subscription, responsible for processing events related to
|
||||
/// diagnostics.
|
||||
_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl BufferDiagnosticsEditor {
|
||||
/// Creates new instance of the `BufferDiagnosticsEditor` which can then be
|
||||
/// displayed by adding it to a pane.
|
||||
pub fn new(
|
||||
project_path: ProjectPath,
|
||||
project_handle: Entity<Project>,
|
||||
buffer: Option<Entity<Buffer>>,
|
||||
include_warnings: bool,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// Subscribe to project events related to diagnostics so the
|
||||
// `BufferDiagnosticsEditor` can update its state accordingly.
|
||||
let project_event_subscription = cx.subscribe_in(
|
||||
&project_handle,
|
||||
window,
|
||||
|buffer_diagnostics_editor, _project, event, window, cx| match event {
|
||||
Event::DiskBasedDiagnosticsStarted { .. } => {
|
||||
cx.notify();
|
||||
}
|
||||
Event::DiskBasedDiagnosticsFinished { .. } => {
|
||||
buffer_diagnostics_editor.update_all_excerpts(window, cx);
|
||||
}
|
||||
Event::DiagnosticsUpdated {
|
||||
paths,
|
||||
language_server_id,
|
||||
} => {
|
||||
// When diagnostics have been updated, the
|
||||
// `BufferDiagnosticsEditor` should update its state only if
|
||||
// one of the paths matches its `project_path`, otherwise
|
||||
// the event should be ignored.
|
||||
if paths.contains(&buffer_diagnostics_editor.project_path) {
|
||||
buffer_diagnostics_editor.update_diagnostic_summary(cx);
|
||||
|
||||
if buffer_diagnostics_editor.editor.focus_handle(cx).contains_focused(window, cx) || buffer_diagnostics_editor.focus_handle.contains_focused(window, cx) {
|
||||
log::debug!("diagnostics updated for server {language_server_id}. recording change");
|
||||
} else {
|
||||
log::debug!("diagnostics updated for server {language_server_id}. updating excerpts");
|
||||
buffer_diagnostics_editor.update_all_excerpts(window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
);
|
||||
|
||||
let focus_handle = cx.focus_handle();
|
||||
|
||||
cx.on_focus_in(
|
||||
&focus_handle,
|
||||
window,
|
||||
|buffer_diagnostics_editor, window, cx| buffer_diagnostics_editor.focus_in(window, cx),
|
||||
)
|
||||
.detach();
|
||||
|
||||
cx.on_focus_out(
|
||||
&focus_handle,
|
||||
window,
|
||||
|buffer_diagnostics_editor, _event, window, cx| {
|
||||
buffer_diagnostics_editor.focus_out(window, cx)
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let summary = project_handle
|
||||
.read(cx)
|
||||
.diagnostic_summary_for_path(&project_path, cx);
|
||||
|
||||
let multibuffer = cx.new(|cx| MultiBuffer::new(project_handle.read(cx).capability()));
|
||||
let max_severity = Self::max_diagnostics_severity(include_warnings);
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor = Editor::for_multibuffer(
|
||||
multibuffer.clone(),
|
||||
Some(project_handle.clone()),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
editor.set_vertical_scroll_margin(5, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.set_max_diagnostics_severity(max_severity, cx);
|
||||
editor.set_all_diagnostics_active(cx);
|
||||
editor
|
||||
});
|
||||
|
||||
// Subscribe to events triggered by the editor in order to correctly
|
||||
// update the buffer's excerpts.
|
||||
cx.subscribe_in(
|
||||
&editor,
|
||||
window,
|
||||
|buffer_diagnostics_editor, _editor, event: &EditorEvent, window, cx| {
|
||||
cx.emit(event.clone());
|
||||
|
||||
match event {
|
||||
// If the user tries to focus on the editor but there's actually
|
||||
// no excerpts for the buffer, focus back on the
|
||||
// `BufferDiagnosticsEditor` instance.
|
||||
EditorEvent::Focused => {
|
||||
if buffer_diagnostics_editor.multibuffer.read(cx).is_empty() {
|
||||
window.focus(&buffer_diagnostics_editor.focus_handle);
|
||||
}
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
buffer_diagnostics_editor.update_all_excerpts(window, cx)
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
},
|
||||
)
|
||||
.detach();
|
||||
|
||||
let diagnostics = vec![];
|
||||
let update_excerpts_task = None;
|
||||
let mut buffer_diagnostics_editor = Self {
|
||||
project: project_handle,
|
||||
focus_handle,
|
||||
editor,
|
||||
diagnostics,
|
||||
blocks: Default::default(),
|
||||
multibuffer,
|
||||
buffer,
|
||||
project_path,
|
||||
summary,
|
||||
include_warnings,
|
||||
update_excerpts_task,
|
||||
_subscription: project_event_subscription,
|
||||
};
|
||||
|
||||
buffer_diagnostics_editor.update_all_diagnostics(window, cx);
|
||||
buffer_diagnostics_editor
|
||||
}
|
||||
|
||||
fn deploy(
|
||||
workspace: &mut Workspace,
|
||||
_: &DeployCurrentFile,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) {
|
||||
// Determine the currently opened path by finding the active editor and
|
||||
// finding the project path for the buffer.
|
||||
// If there's no active editor with a project path, avoiding deploying
|
||||
// the buffer diagnostics view.
|
||||
if let Some(editor) = workspace.active_item_as::<Editor>(cx)
|
||||
&& let Some(project_path) = editor.project_path(cx)
|
||||
{
|
||||
// Check if there's already a `BufferDiagnosticsEditor` tab for this
|
||||
// same path, and if so, focus on that one instead of creating a new
|
||||
// one.
|
||||
let existing_editor = workspace
|
||||
.items_of_type::<BufferDiagnosticsEditor>(cx)
|
||||
.find(|editor| editor.read(cx).project_path == project_path);
|
||||
|
||||
if let Some(editor) = existing_editor {
|
||||
workspace.activate_item(&editor, true, true, window, cx);
|
||||
} else {
|
||||
let include_warnings = match cx.try_global::<IncludeWarnings>() {
|
||||
Some(include_warnings) => include_warnings.0,
|
||||
None => ProjectSettings::get_global(cx).diagnostics.include_warnings,
|
||||
};
|
||||
|
||||
let item = cx.new(|cx| {
|
||||
Self::new(
|
||||
project_path,
|
||||
workspace.project().clone(),
|
||||
editor.read(cx).buffer().read(cx).as_singleton(),
|
||||
include_warnings,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
workspace.add_item_to_active_pane(Box::new(item), None, true, window, cx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn register(
|
||||
workspace: &mut Workspace,
|
||||
_window: Option<&mut Window>,
|
||||
_: &mut Context<Workspace>,
|
||||
) {
|
||||
workspace.register_action(Self::deploy);
|
||||
}
|
||||
|
||||
fn update_all_diagnostics(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.update_all_excerpts(window, cx);
|
||||
}
|
||||
|
||||
fn update_diagnostic_summary(&mut self, cx: &mut Context<Self>) {
|
||||
let project = self.project.read(cx);
|
||||
|
||||
self.summary = project.diagnostic_summary_for_path(&self.project_path, cx);
|
||||
}
|
||||
|
||||
/// Enqueue an update to the excerpts and diagnostic blocks being shown in
|
||||
/// the editor.
|
||||
pub(crate) fn update_all_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// If there's already a task updating the excerpts, early return and let
|
||||
// the other task finish.
|
||||
if self.update_excerpts_task.is_some() {
|
||||
return;
|
||||
}
|
||||
|
||||
let buffer = self.buffer.clone();
|
||||
|
||||
self.update_excerpts_task = Some(cx.spawn_in(window, async move |editor, cx| {
|
||||
cx.background_executor()
|
||||
.timer(DIAGNOSTICS_UPDATE_DELAY)
|
||||
.await;
|
||||
|
||||
if let Some(buffer) = buffer {
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.update_excerpts(buffer, window, cx)
|
||||
})?
|
||||
.await?;
|
||||
};
|
||||
|
||||
let _ = editor.update(cx, |editor, cx| {
|
||||
editor.update_excerpts_task = None;
|
||||
cx.notify();
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}));
|
||||
}
|
||||
|
||||
/// Updates the excerpts in the `BufferDiagnosticsEditor` for a single
|
||||
/// buffer.
|
||||
fn update_excerpts(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let was_empty = self.multibuffer.read(cx).is_empty();
|
||||
let multibuffer_context = multibuffer_context_lines(cx);
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
let buffer_snapshot_max = buffer_snapshot.max_point();
|
||||
let max_severity = Self::max_diagnostics_severity(self.include_warnings)
|
||||
.into_lsp()
|
||||
.unwrap_or(lsp::DiagnosticSeverity::WARNING);
|
||||
|
||||
cx.spawn_in(window, async move |buffer_diagnostics_editor, mut cx| {
|
||||
// Fetch the diagnostics for the whole of the buffer
|
||||
// (`Point::zero()..buffer_snapshot.max_point()`) so we can confirm
|
||||
// if the diagnostics changed, if it didn't, early return as there's
|
||||
// nothing to update.
|
||||
let diagnostics = buffer_snapshot
|
||||
.diagnostics_in_range::<_, Anchor>(Point::zero()..buffer_snapshot_max, false)
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let unchanged =
|
||||
buffer_diagnostics_editor.update(cx, |buffer_diagnostics_editor, _cx| {
|
||||
if buffer_diagnostics_editor
|
||||
.diagnostics_are_unchanged(&diagnostics, &buffer_snapshot)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
buffer_diagnostics_editor.set_diagnostics(&diagnostics);
|
||||
return false;
|
||||
})?;
|
||||
|
||||
if unchanged {
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// Mapping between the Group ID and a vector of DiagnosticEntry.
|
||||
let mut grouped: HashMap<usize, Vec<_>> = HashMap::default();
|
||||
for entry in diagnostics {
|
||||
grouped
|
||||
.entry(entry.diagnostic.group_id)
|
||||
.or_default()
|
||||
.push(DiagnosticEntry {
|
||||
range: entry.range.to_point(&buffer_snapshot),
|
||||
diagnostic: entry.diagnostic,
|
||||
})
|
||||
}
|
||||
|
||||
let mut blocks: Vec<DiagnosticBlock> = Vec::new();
|
||||
for (_, group) in grouped {
|
||||
// If the minimum severity of the group is higher than the
|
||||
// maximum severity, or it doesn't even have severity, skip this
|
||||
// group.
|
||||
if group
|
||||
.iter()
|
||||
.map(|d| d.diagnostic.severity)
|
||||
.min()
|
||||
.is_none_or(|severity| severity > max_severity)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
let diagnostic_blocks = cx.update(|_window, cx| {
|
||||
DiagnosticRenderer::diagnostic_blocks_for_group(
|
||||
group,
|
||||
buffer_snapshot.remote_id(),
|
||||
Some(Arc::new(buffer_diagnostics_editor.clone())),
|
||||
cx,
|
||||
)
|
||||
})?;
|
||||
|
||||
// For each of the diagnostic blocks to be displayed in the
|
||||
// editor, figure out its index in the list of blocks.
|
||||
//
|
||||
// The following rules are used to determine the order:
|
||||
// 1. Blocks with a lower start position should come first.
|
||||
// 2. If two blocks have the same start position, the one with
|
||||
// the higher end position should come first.
|
||||
for diagnostic_block in diagnostic_blocks {
|
||||
let index = blocks.partition_point(|probe| {
|
||||
match probe
|
||||
.initial_range
|
||||
.start
|
||||
.cmp(&diagnostic_block.initial_range.start)
|
||||
{
|
||||
Ordering::Less => true,
|
||||
Ordering::Greater => false,
|
||||
Ordering::Equal => {
|
||||
probe.initial_range.end > diagnostic_block.initial_range.end
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
blocks.insert(index, diagnostic_block);
|
||||
}
|
||||
}
|
||||
|
||||
// Build the excerpt ranges for this specific buffer's diagnostics,
|
||||
// so those excerpts can later be used to update the excerpts shown
|
||||
// in the editor.
|
||||
// This is done by iterating over the list of diagnostic blocks and
|
||||
// determine what range does the diagnostic block span.
|
||||
let mut excerpt_ranges: Vec<ExcerptRange<Point>> = Vec::new();
|
||||
|
||||
for diagnostic_block in blocks.iter() {
|
||||
let excerpt_range = context_range_for_entry(
|
||||
diagnostic_block.initial_range.clone(),
|
||||
multibuffer_context,
|
||||
buffer_snapshot.clone(),
|
||||
&mut cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
let index = excerpt_ranges
|
||||
.binary_search_by(|probe| {
|
||||
probe
|
||||
.context
|
||||
.start
|
||||
.cmp(&excerpt_range.start)
|
||||
.then(probe.context.end.cmp(&excerpt_range.end))
|
||||
.then(
|
||||
probe
|
||||
.primary
|
||||
.start
|
||||
.cmp(&diagnostic_block.initial_range.start),
|
||||
)
|
||||
.then(probe.primary.end.cmp(&diagnostic_block.initial_range.end))
|
||||
.then(Ordering::Greater)
|
||||
})
|
||||
.unwrap_or_else(|index| index);
|
||||
|
||||
excerpt_ranges.insert(
|
||||
index,
|
||||
ExcerptRange {
|
||||
context: excerpt_range,
|
||||
primary: diagnostic_block.initial_range.clone(),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
// Finally, update the editor's content with the new excerpt ranges
|
||||
// for this editor, as well as the diagnostic blocks.
|
||||
buffer_diagnostics_editor.update_in(cx, |buffer_diagnostics_editor, window, cx| {
|
||||
// Remove the list of `CustomBlockId` from the editor's display
|
||||
// map, ensuring that if any diagnostics have been solved, the
|
||||
// associated block stops being shown.
|
||||
let block_ids = buffer_diagnostics_editor.blocks.clone();
|
||||
|
||||
buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
display_map.remove_blocks(block_ids.into_iter().collect(), cx);
|
||||
})
|
||||
});
|
||||
|
||||
let (anchor_ranges, _) =
|
||||
buffer_diagnostics_editor
|
||||
.multibuffer
|
||||
.update(cx, |multibuffer, cx| {
|
||||
multibuffer.set_excerpt_ranges_for_path(
|
||||
PathKey::for_buffer(&buffer, cx),
|
||||
buffer.clone(),
|
||||
&buffer_snapshot,
|
||||
excerpt_ranges,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
if was_empty {
|
||||
if let Some(anchor_range) = anchor_ranges.first() {
|
||||
let range_to_select = anchor_range.start..anchor_range.start;
|
||||
|
||||
buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
|
||||
editor.change_selections(Default::default(), window, cx, |selection| {
|
||||
selection.select_anchor_ranges([range_to_select])
|
||||
})
|
||||
});
|
||||
|
||||
// If the `BufferDiagnosticsEditor` is currently
|
||||
// focused, move focus to its editor.
|
||||
if buffer_diagnostics_editor.focus_handle.is_focused(window) {
|
||||
buffer_diagnostics_editor
|
||||
.editor
|
||||
.read(cx)
|
||||
.focus_handle(cx)
|
||||
.focus(window);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Cloning the blocks before moving ownership so these can later
|
||||
// be used to set the block contents for testing purposes.
|
||||
#[cfg(test)]
|
||||
let cloned_blocks = blocks.clone();
|
||||
|
||||
// Build new diagnostic blocks to be added to the editor's
|
||||
// display map for the new diagnostics. Update the `blocks`
|
||||
// property before finishing, to ensure the blocks are removed
|
||||
// on the next execution.
|
||||
let editor_blocks =
|
||||
anchor_ranges
|
||||
.into_iter()
|
||||
.zip(blocks.into_iter())
|
||||
.map(|(anchor, block)| {
|
||||
let editor = buffer_diagnostics_editor.editor.downgrade();
|
||||
|
||||
BlockProperties {
|
||||
placement: BlockPlacement::Near(anchor.start),
|
||||
height: Some(1),
|
||||
style: BlockStyle::Flex,
|
||||
render: Arc::new(move |block_context| {
|
||||
block.render_block(editor.clone(), block_context)
|
||||
}),
|
||||
priority: 1,
|
||||
}
|
||||
});
|
||||
|
||||
let block_ids = buffer_diagnostics_editor.editor.update(cx, |editor, cx| {
|
||||
editor.display_map.update(cx, |display_map, cx| {
|
||||
display_map.insert_blocks(editor_blocks, cx)
|
||||
})
|
||||
});
|
||||
|
||||
// In order to be able to verify which diagnostic blocks are
|
||||
// rendered in the editor, the `set_block_content_for_tests`
|
||||
// function must be used, so that the
|
||||
// `editor::test::editor_content_with_blocks` function can then
|
||||
// be called to fetch these blocks.
|
||||
#[cfg(test)]
|
||||
{
|
||||
for (block_id, block) in block_ids.iter().zip(cloned_blocks.iter()) {
|
||||
let markdown = block.markdown.clone();
|
||||
editor::test::set_block_content_for_tests(
|
||||
&buffer_diagnostics_editor.editor,
|
||||
*block_id,
|
||||
cx,
|
||||
move |cx| {
|
||||
markdown::MarkdownElement::rendered_text(
|
||||
markdown.clone(),
|
||||
cx,
|
||||
editor::hover_popover::diagnostics_markdown_style,
|
||||
)
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
buffer_diagnostics_editor.blocks = block_ids;
|
||||
cx.notify()
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn set_diagnostics(&mut self, diagnostics: &Vec<DiagnosticEntry<Anchor>>) {
|
||||
self.diagnostics = diagnostics.clone();
|
||||
}
|
||||
|
||||
fn diagnostics_are_unchanged(
|
||||
&self,
|
||||
diagnostics: &Vec<DiagnosticEntry<Anchor>>,
|
||||
snapshot: &BufferSnapshot,
|
||||
) -> bool {
|
||||
if self.diagnostics.len() != diagnostics.len() {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.diagnostics
|
||||
.iter()
|
||||
.zip(diagnostics.iter())
|
||||
.all(|(existing, new)| {
|
||||
existing.diagnostic.message == new.diagnostic.message
|
||||
&& existing.diagnostic.severity == new.diagnostic.severity
|
||||
&& existing.diagnostic.is_primary == new.diagnostic.is_primary
|
||||
&& existing.range.to_offset(snapshot) == new.range.to_offset(snapshot)
|
||||
})
|
||||
}
|
||||
|
||||
fn focus_in(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
// If the `BufferDiagnosticsEditor` is focused and the multibuffer is
|
||||
// not empty, focus on the editor instead, which will allow the user to
|
||||
// start interacting and editing the buffer's contents.
|
||||
if self.focus_handle.is_focused(window) && !self.multibuffer.read(cx).is_empty() {
|
||||
self.editor.focus_handle(cx).focus(window)
|
||||
}
|
||||
}
|
||||
|
||||
fn focus_out(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
if !self.focus_handle.is_focused(window) && !self.editor.focus_handle(cx).is_focused(window)
|
||||
{
|
||||
self.update_all_excerpts(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn toggle_warnings(
|
||||
&mut self,
|
||||
_: &ToggleWarnings,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let include_warnings = !self.include_warnings;
|
||||
let max_severity = Self::max_diagnostics_severity(include_warnings);
|
||||
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.set_max_diagnostics_severity(max_severity, cx);
|
||||
});
|
||||
|
||||
self.include_warnings = include_warnings;
|
||||
self.diagnostics.clear();
|
||||
self.update_all_diagnostics(window, cx);
|
||||
}
|
||||
|
||||
fn max_diagnostics_severity(include_warnings: bool) -> DiagnosticSeverity {
|
||||
match include_warnings {
|
||||
true => DiagnosticSeverity::Warning,
|
||||
false => DiagnosticSeverity::Error,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn editor(&self) -> &Entity<Editor> {
|
||||
&self.editor
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub fn summary(&self) -> &DiagnosticSummary {
|
||||
&self.summary
|
||||
}
|
||||
}
|
||||
|
||||
impl Focusable for BufferDiagnosticsEditor {
|
||||
fn focus_handle(&self, _: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<EditorEvent> for BufferDiagnosticsEditor {}
|
||||
|
||||
impl Item for BufferDiagnosticsEditor {
|
||||
type Event = EditorEvent;
|
||||
|
||||
fn act_as_type<'a>(
|
||||
&'a self,
|
||||
type_id: std::any::TypeId,
|
||||
self_handle: &'a Entity<Self>,
|
||||
_: &'a App,
|
||||
) -> Option<gpui::AnyView> {
|
||||
if type_id == TypeId::of::<Self>() {
|
||||
Some(self_handle.to_any())
|
||||
} else if type_id == TypeId::of::<Editor>() {
|
||||
Some(self.editor.to_any())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn added_to_workspace(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.added_to_workspace(workspace, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn breadcrumb_location(&self, _: &App) -> ToolbarItemLocation {
|
||||
ToolbarItemLocation::PrimaryLeft
|
||||
}
|
||||
|
||||
fn breadcrumbs(&self, theme: &theme::Theme, cx: &App) -> Option<Vec<BreadcrumbText>> {
|
||||
self.editor.breadcrumbs(theme, cx)
|
||||
}
|
||||
|
||||
fn can_save(&self, _cx: &App) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn clone_on_split(
|
||||
&self,
|
||||
_workspace_id: Option<workspace::WorkspaceId>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Entity<Self>>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new(|cx| {
|
||||
BufferDiagnosticsEditor::new(
|
||||
self.project_path.clone(),
|
||||
self.project.clone(),
|
||||
self.buffer.clone(),
|
||||
self.include_warnings,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}))
|
||||
}
|
||||
|
||||
fn deactivated(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.deactivated(window, cx));
|
||||
}
|
||||
|
||||
fn for_each_project_item(&self, cx: &App, f: &mut dyn FnMut(EntityId, &dyn ProjectItem)) {
|
||||
self.editor.for_each_project_item(cx, f);
|
||||
}
|
||||
|
||||
fn has_conflict(&self, cx: &App) -> bool {
|
||||
self.multibuffer.read(cx).has_conflict(cx)
|
||||
}
|
||||
|
||||
fn has_deleted_file(&self, cx: &App) -> bool {
|
||||
self.multibuffer.read(cx).has_deleted_file(cx)
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
self.multibuffer.read(cx).is_dirty(cx)
|
||||
}
|
||||
|
||||
fn is_singleton(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn navigate(
|
||||
&mut self,
|
||||
data: Box<dyn Any>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.editor
|
||||
.update(cx, |editor, cx| editor.navigate(data, window, cx))
|
||||
}
|
||||
|
||||
fn reload(
|
||||
&mut self,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.reload(project, window, cx)
|
||||
}
|
||||
|
||||
fn save(
|
||||
&mut self,
|
||||
options: workspace::item::SaveOptions,
|
||||
project: Entity<Project>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
self.editor.save(options, project, window, cx)
|
||||
}
|
||||
|
||||
fn save_as(
|
||||
&mut self,
|
||||
_project: Entity<Project>,
|
||||
_path: ProjectPath,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
unreachable!()
|
||||
}
|
||||
|
||||
fn set_nav_history(
|
||||
&mut self,
|
||||
nav_history: ItemNavHistory,
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.editor.update(cx, |editor, _| {
|
||||
editor.set_nav_history(Some(nav_history));
|
||||
})
|
||||
}
|
||||
|
||||
// Builds the content to be displayed in the tab.
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, _cx: &App) -> AnyElement {
|
||||
let error_count = self.summary.error_count;
|
||||
let warning_count = self.summary.warning_count;
|
||||
let label = Label::new(
|
||||
self.project_path
|
||||
.path
|
||||
.file_name()
|
||||
.map(|f| f.to_sanitized_string())
|
||||
.unwrap_or_else(|| self.project_path.path.to_sanitized_string()),
|
||||
);
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(label)
|
||||
.when(error_count == 0 && warning_count == 0, |parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Check).color(Color::Success)),
|
||||
)
|
||||
})
|
||||
.when(error_count > 0, |parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new(error_count.to_string()).color(params.text_color())),
|
||||
)
|
||||
})
|
||||
.when(warning_count > 0, |parent| {
|
||||
parent.child(
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.child(Icon::new(IconName::Warning).color(Color::Warning))
|
||||
.child(Label::new(warning_count.to_string()).color(params.text_color())),
|
||||
)
|
||||
})
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn tab_content_text(&self, _detail: usize, _app: &App) -> SharedString {
|
||||
"Buffer Diagnostics".into()
|
||||
}
|
||||
|
||||
fn tab_tooltip_text(&self, _: &App) -> Option<SharedString> {
|
||||
Some(
|
||||
format!(
|
||||
"Buffer Diagnostics - {}",
|
||||
self.project_path.path.to_sanitized_string()
|
||||
)
|
||||
.into(),
|
||||
)
|
||||
}
|
||||
|
||||
fn telemetry_event_text(&self) -> Option<&'static str> {
|
||||
Some("Buffer Diagnostics Opened")
|
||||
}
|
||||
|
||||
fn to_item_events(event: &EditorEvent, f: impl FnMut(ItemEvent)) {
|
||||
Editor::to_item_events(event, f)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for BufferDiagnosticsEditor {
|
||||
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let filename = self.project_path.path.to_sanitized_string();
|
||||
let error_count = self.summary.error_count;
|
||||
let warning_count = match self.include_warnings {
|
||||
true => self.summary.warning_count,
|
||||
false => 0,
|
||||
};
|
||||
|
||||
let child = if error_count + warning_count == 0 {
|
||||
let label = match warning_count {
|
||||
0 => "No problems in",
|
||||
_ => "No errors in",
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.key_context("EmptyPane")
|
||||
.size_full()
|
||||
.gap_1()
|
||||
.justify_center()
|
||||
.items_center()
|
||||
.text_center()
|
||||
.bg(cx.theme().colors().editor_background)
|
||||
.child(
|
||||
div()
|
||||
.h_flex()
|
||||
.child(Label::new(label).color(Color::Muted))
|
||||
.child(
|
||||
Button::new("open-file", filename)
|
||||
.style(ButtonStyle::Transparent)
|
||||
.tooltip(Tooltip::text("Open File"))
|
||||
.on_click(cx.listener(|buffer_diagnostics, _, window, cx| {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace
|
||||
.open_path(
|
||||
buffer_diagnostics.project_path.clone(),
|
||||
None,
|
||||
true,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
})
|
||||
}
|
||||
})),
|
||||
),
|
||||
)
|
||||
.when(self.summary.warning_count > 0, |div| {
|
||||
let label = match self.summary.warning_count {
|
||||
1 => "Show 1 warning".into(),
|
||||
warning_count => format!("Show {} warnings", warning_count),
|
||||
};
|
||||
|
||||
div.child(
|
||||
Button::new("diagnostics-show-warning-label", label).on_click(cx.listener(
|
||||
|buffer_diagnostics_editor, _, window, cx| {
|
||||
buffer_diagnostics_editor.toggle_warnings(
|
||||
&Default::default(),
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
},
|
||||
)),
|
||||
)
|
||||
})
|
||||
} else {
|
||||
div().size_full().child(self.editor.clone())
|
||||
};
|
||||
|
||||
div()
|
||||
.key_context("Diagnostics")
|
||||
.track_focus(&self.focus_handle(cx))
|
||||
.size_full()
|
||||
.child(child)
|
||||
}
|
||||
}
|
||||
|
||||
impl DiagnosticsToolbarEditor for WeakEntity<BufferDiagnosticsEditor> {
|
||||
fn include_warnings(&self, cx: &App) -> bool {
|
||||
self.read_with(cx, |buffer_diagnostics_editor, _cx| {
|
||||
buffer_diagnostics_editor.include_warnings
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn has_stale_excerpts(&self, _cx: &App) -> bool {
|
||||
false
|
||||
}
|
||||
|
||||
fn is_updating(&self, cx: &App) -> bool {
|
||||
self.read_with(cx, |buffer_diagnostics_editor, cx| {
|
||||
buffer_diagnostics_editor.update_excerpts_task.is_some()
|
||||
|| buffer_diagnostics_editor
|
||||
.project
|
||||
.read(cx)
|
||||
.language_servers_running_disk_based_diagnostics(cx)
|
||||
.next()
|
||||
.is_some()
|
||||
})
|
||||
.unwrap_or(false)
|
||||
}
|
||||
|
||||
fn stop_updating(&self, cx: &mut App) {
|
||||
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
|
||||
buffer_diagnostics_editor.update_excerpts_task = None;
|
||||
cx.notify();
|
||||
});
|
||||
}
|
||||
|
||||
fn refresh_diagnostics(&self, window: &mut Window, cx: &mut App) {
|
||||
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
|
||||
buffer_diagnostics_editor.update_all_excerpts(window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn toggle_warnings(&self, window: &mut Window, cx: &mut App) {
|
||||
let _ = self.update(cx, |buffer_diagnostics_editor, cx| {
|
||||
buffer_diagnostics_editor.toggle_warnings(&Default::default(), window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn get_diagnostics_for_buffer(
|
||||
&self,
|
||||
_buffer_id: text::BufferId,
|
||||
cx: &App,
|
||||
) -> Vec<language::DiagnosticEntry<text::Anchor>> {
|
||||
self.read_with(cx, |buffer_diagnostics_editor, _cx| {
|
||||
buffer_diagnostics_editor.diagnostics.clone()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
@@ -18,7 +18,7 @@ use ui::{
|
||||
};
|
||||
use util::maybe;
|
||||
|
||||
use crate::ProjectDiagnosticsEditor;
|
||||
use crate::toolbar_controls::DiagnosticsToolbarEditor;
|
||||
|
||||
pub struct DiagnosticRenderer;
|
||||
|
||||
@@ -26,7 +26,7 @@ impl DiagnosticRenderer {
|
||||
pub fn diagnostic_blocks_for_group(
|
||||
diagnostic_group: Vec<DiagnosticEntry<Point>>,
|
||||
buffer_id: BufferId,
|
||||
diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||
diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||
cx: &mut App,
|
||||
) -> Vec<DiagnosticBlock> {
|
||||
let Some(primary_ix) = diagnostic_group
|
||||
@@ -130,6 +130,7 @@ impl editor::DiagnosticRenderer for DiagnosticRenderer {
|
||||
cx: &mut App,
|
||||
) -> Vec<BlockProperties<Anchor>> {
|
||||
let blocks = Self::diagnostic_blocks_for_group(diagnostic_group, buffer_id, None, cx);
|
||||
|
||||
blocks
|
||||
.into_iter()
|
||||
.map(|block| {
|
||||
@@ -182,7 +183,7 @@ pub(crate) struct DiagnosticBlock {
|
||||
pub(crate) initial_range: Range<Point>,
|
||||
pub(crate) severity: DiagnosticSeverity,
|
||||
pub(crate) markdown: Entity<Markdown>,
|
||||
pub(crate) diagnostics_editor: Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||
pub(crate) diagnostics_editor: Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||
}
|
||||
|
||||
impl DiagnosticBlock {
|
||||
@@ -233,7 +234,7 @@ impl DiagnosticBlock {
|
||||
|
||||
pub fn open_link(
|
||||
editor: &mut Editor,
|
||||
diagnostics_editor: &Option<WeakEntity<ProjectDiagnosticsEditor>>,
|
||||
diagnostics_editor: &Option<Arc<dyn DiagnosticsToolbarEditor>>,
|
||||
link: SharedString,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
@@ -254,18 +255,10 @@ impl DiagnosticBlock {
|
||||
|
||||
if let Some(diagnostics_editor) = diagnostics_editor {
|
||||
if let Some(diagnostic) = diagnostics_editor
|
||||
.read_with(cx, |diagnostics, _| {
|
||||
diagnostics
|
||||
.diagnostics
|
||||
.get(&buffer_id)
|
||||
.cloned()
|
||||
.unwrap_or_default()
|
||||
.into_iter()
|
||||
.filter(|d| d.diagnostic.group_id == group_id)
|
||||
.nth(ix)
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
.get_diagnostics_for_buffer(buffer_id, cx)
|
||||
.into_iter()
|
||||
.filter(|d| d.diagnostic.group_id == group_id)
|
||||
.nth(ix)
|
||||
{
|
||||
let multibuffer = editor.buffer().read(cx);
|
||||
let Some(snapshot) = multibuffer
|
||||
@@ -297,9 +290,9 @@ impl DiagnosticBlock {
|
||||
};
|
||||
}
|
||||
|
||||
fn jump_to<T: ToOffset>(
|
||||
fn jump_to<I: ToOffset>(
|
||||
editor: &mut Editor,
|
||||
range: Range<T>,
|
||||
range: Range<I>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user