Compare commits
131 Commits
split-agen
...
acp-thinki
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ab8c9b7edc | ||
|
|
fb1b761021 | ||
|
|
733e5dd8f2 | ||
|
|
fdc365ecf1 | ||
|
|
0770ae2612 | ||
|
|
a1ad858fb6 | ||
|
|
ea197d4e6d | ||
|
|
1ab78a7544 | ||
|
|
df3d956e63 | ||
|
|
4e3f66c5c2 | ||
|
|
d6c76d8d33 | ||
|
|
f1613afb22 | ||
|
|
405f7cf64f | ||
|
|
73ac553316 | ||
|
|
136423da94 | ||
|
|
28baedd935 | ||
|
|
756358b9c7 | ||
|
|
54040188bb | ||
|
|
4755d6fa9d | ||
|
|
135143d51b | ||
|
|
450604b4a1 | ||
|
|
348bc52a3f | ||
|
|
d16c595d57 | ||
|
|
975a7e6f7f | ||
|
|
7d2f7cb70e | ||
|
|
5f9afdf7ba | ||
|
|
7a3105b0c6 | ||
|
|
ab0b16939d | ||
|
|
28d992487d | ||
|
|
fde15a5a68 | ||
|
|
780db30e0b | ||
|
|
7c992adfe1 | ||
|
|
825aecfd28 | ||
|
|
f2f32fb3bd | ||
|
|
d9fd8d5eee | ||
|
|
8137b3318f | ||
|
|
3ceeefe460 | ||
|
|
6f768aefa2 | ||
|
|
28ac84ed01 | ||
|
|
4d803fa628 | ||
|
|
17b2dd9a93 | ||
|
|
7abf635e20 | ||
|
|
92adcb6e63 | ||
|
|
5ed001e0df | ||
|
|
f12fffd1ba | ||
|
|
0068de0386 | ||
|
|
a11647d07f | ||
|
|
274f2e90da | ||
|
|
31b7786be7 | ||
|
|
351ba5023b | ||
|
|
3041de0cdf | ||
|
|
52c42125a7 | ||
|
|
62e8f45304 | ||
|
|
0fe73a99e5 | ||
|
|
6e9c6c5684 | ||
|
|
42f788185a | ||
|
|
a5b2428897 | ||
|
|
0629804390 | ||
|
|
3151b5efc1 | ||
|
|
782fbfad90 | ||
|
|
2caa19214b | ||
|
|
bff5d85ff4 | ||
|
|
abe5d523e1 | ||
|
|
8fb3199a84 | ||
|
|
0d809c21ba | ||
|
|
93b1e95a5d | ||
|
|
49bc2e61da | ||
|
|
9a4bcd11a2 | ||
|
|
2ee5bedfa9 | ||
|
|
d497f52e17 | ||
|
|
f022a13091 | ||
|
|
c74ecb4654 | ||
|
|
7609ca7a8d | ||
|
|
32906bfa7c | ||
|
|
5fafab6e52 | ||
|
|
a2e786e0f9 | ||
|
|
b0086b472f | ||
|
|
d10cc13924 | ||
|
|
2680a78f9c | ||
|
|
197828980c | ||
|
|
7c4da37322 | ||
|
|
ce164f5e65 | ||
|
|
42c59014a9 | ||
|
|
3db452eec7 | ||
|
|
6e77e8405b | ||
|
|
465f64da7e | ||
|
|
e5a8cc7aab | ||
|
|
bdf29bf76f | ||
|
|
402c61c00d | ||
|
|
59e88ce82b | ||
|
|
22ab4c53d1 | ||
|
|
f106ea7641 | ||
|
|
e37ef2a991 | ||
|
|
1c05062482 | ||
|
|
8c04f12499 | ||
|
|
aa7ccecc49 | ||
|
|
f4aeeda2d9 | ||
|
|
ca0bd53bed | ||
|
|
ae6237178c | ||
|
|
ac3328adb6 | ||
|
|
991ba08711 | ||
|
|
c728731099 | ||
|
|
ddab1cbd71 | ||
|
|
f383a7626f | ||
|
|
ee1df65569 | ||
|
|
3be45822be | ||
|
|
3b6f30a6fd | ||
|
|
779a68f868 | ||
|
|
79c37284e0 | ||
|
|
0a053cf55d | ||
|
|
fc59d9cbf3 | ||
|
|
678a42e920 | ||
|
|
75bcaf743c | ||
|
|
47c875f6b5 | ||
|
|
81b4d7e35a | ||
|
|
33ee0c3093 | ||
|
|
d68f86052f | ||
|
|
a74ffd9ee4 | ||
|
|
8b9ad1cfae | ||
|
|
adbccb1ad0 | ||
|
|
f4e2d38c29 | ||
|
|
5f10be7791 | ||
|
|
d47a920c05 | ||
|
|
24b72be154 | ||
|
|
de779a45ce | ||
|
|
b094a636cf | ||
|
|
318709b60d | ||
|
|
f1bd531a32 | ||
|
|
549eb4d826 | ||
|
|
c1e53b7fa5 | ||
|
|
ec376e0b61 |
12
.github/workflows/ci.yml
vendored
12
.github/workflows/ci.yml
vendored
@@ -30,6 +30,7 @@ jobs:
|
||||
run_tests: ${{ steps.filter.outputs.run_tests }}
|
||||
run_license: ${{ steps.filter.outputs.run_license }}
|
||||
run_docs: ${{ steps.filter.outputs.run_docs }}
|
||||
run_nix: ${{ steps.filter.outputs.run_nix }}
|
||||
runs-on:
|
||||
- ubuntu-latest
|
||||
steps:
|
||||
@@ -69,6 +70,12 @@ jobs:
|
||||
else
|
||||
echo "run_license=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
NIX_REGEX='^(nix/|flake\.|Cargo\.|rust-toolchain.toml|\.cargo/config.toml)'
|
||||
if [[ $(git diff --name-only $COMPARE_REV ${{ github.sha }} | grep "$NIX_REGEX") ]]; then
|
||||
echo "run_nix=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "run_nix=false" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
|
||||
migration_checks:
|
||||
name: Check Postgres and Protobuf migrations, mergability
|
||||
@@ -746,7 +753,10 @@ jobs:
|
||||
nix-build:
|
||||
name: Build with Nix
|
||||
uses: ./.github/workflows/nix.yml
|
||||
if: github.repository_owner == 'zed-industries' && contains(github.event.pull_request.labels.*.name, 'run-nix')
|
||||
needs: [job_spec]
|
||||
if: github.repository_owner == 'zed-industries' &&
|
||||
(contains(github.event.pull_request.labels.*.name, 'run-nix') ||
|
||||
needs.job_spec.outputs.run_nix == 'true')
|
||||
secrets: inherit
|
||||
with:
|
||||
flake-output: debug
|
||||
|
||||
89
Cargo.lock
generated
89
Cargo.lock
generated
@@ -2,6 +2,38 @@
|
||||
# It is not intended for manual editing.
|
||||
version = 4
|
||||
|
||||
[[package]]
|
||||
name = "acp"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"agentic-coding-protocol",
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"collections",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"language",
|
||||
"log",
|
||||
"markdown",
|
||||
"parking_lot",
|
||||
"project",
|
||||
"proto",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"smol",
|
||||
"theme",
|
||||
"ui",
|
||||
"util",
|
||||
"uuid",
|
||||
"workspace-hack",
|
||||
"zed_actions",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "activity_indicator"
|
||||
version = "0.1.0"
|
||||
@@ -130,6 +162,7 @@ dependencies = [
|
||||
name = "agent_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"acp",
|
||||
"agent",
|
||||
"agent_settings",
|
||||
"anyhow",
|
||||
@@ -212,6 +245,21 @@ dependencies = [
|
||||
"zed_llm_client",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agentic-coding-protocol"
|
||||
version = "0.0.1"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"async-trait",
|
||||
"chrono",
|
||||
"futures 0.3.31",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"schemars",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "ahash"
|
||||
version = "0.7.8"
|
||||
@@ -1911,7 +1959,6 @@ dependencies = [
|
||||
"serde_json",
|
||||
"strum 0.27.1",
|
||||
"thiserror 2.0.12",
|
||||
"tokio",
|
||||
"workspace-hack",
|
||||
]
|
||||
|
||||
@@ -4133,7 +4180,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "dap-types"
|
||||
version = "0.0.1"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=b40956a7f4d1939da67429d941389ee306a3a308#b40956a7f4d1939da67429d941389ee306a3a308"
|
||||
source = "git+https://github.com/zed-industries/dap-types?rev=7f39295b441614ca9dbf44293e53c32f666897f9#7f39295b441614ca9dbf44293e53c32f666897f9"
|
||||
dependencies = [
|
||||
"schemars",
|
||||
"serde",
|
||||
@@ -4148,6 +4195,8 @@ dependencies = [
|
||||
"async-trait",
|
||||
"collections",
|
||||
"dap",
|
||||
"dotenvy",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"json_dotpath",
|
||||
@@ -4676,12 +4725,6 @@ dependencies = [
|
||||
"syn 2.0.101",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "dotenv"
|
||||
version = "0.15.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "77c90badedccf4105eca100756a0b1289e191f6fcbdadd3cee1d2f614f97da8f"
|
||||
|
||||
[[package]]
|
||||
name = "dotenvy"
|
||||
version = "0.15.7"
|
||||
@@ -4814,6 +4857,7 @@ dependencies = [
|
||||
"pretty_assertions",
|
||||
"project",
|
||||
"rand 0.8.5",
|
||||
"regex",
|
||||
"release_channel",
|
||||
"rpc",
|
||||
"schemars",
|
||||
@@ -5114,7 +5158,7 @@ dependencies = [
|
||||
"collections",
|
||||
"debug_adapter_extension",
|
||||
"dirs 4.0.0",
|
||||
"dotenv",
|
||||
"dotenvy",
|
||||
"env_logger 0.11.8",
|
||||
"extension",
|
||||
"fs",
|
||||
@@ -8847,6 +8891,7 @@ dependencies = [
|
||||
"http_client",
|
||||
"imara-diff",
|
||||
"indoc",
|
||||
"inventory",
|
||||
"itertools 0.14.0",
|
||||
"log",
|
||||
"lsp",
|
||||
@@ -8945,8 +8990,10 @@ dependencies = [
|
||||
"aws-credential-types",
|
||||
"aws_http_client",
|
||||
"bedrock",
|
||||
"chrono",
|
||||
"client",
|
||||
"collections",
|
||||
"component",
|
||||
"copilot",
|
||||
"credentials_provider",
|
||||
"deepseek",
|
||||
@@ -14053,12 +14100,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars"
|
||||
version = "0.8.22"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3fbf2ae1b8bc8e02df939598064d22402220cd5bbcca1c76f7d6a310974d5615"
|
||||
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
|
||||
dependencies = [
|
||||
"chrono",
|
||||
"dyn-clone",
|
||||
"indexmap",
|
||||
"ref-cast",
|
||||
"schemars_derive",
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -14066,9 +14115,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "schemars_derive"
|
||||
version = "0.8.22"
|
||||
version = "1.0.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "32e265784ad618884abaea0600a9adf15393368d840e0222d101a072f3f7534d"
|
||||
checksum = "6ca9fcb757952f8e8629b9ab066fc62da523c46c2b247b1708a3be06dd82530b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
@@ -14567,13 +14616,22 @@ dependencies = [
|
||||
name = "settings_ui"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"collections",
|
||||
"command_palette",
|
||||
"command_palette_hooks",
|
||||
"component",
|
||||
"db",
|
||||
"editor",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"log",
|
||||
"menu",
|
||||
"paths",
|
||||
"project",
|
||||
"schemars",
|
||||
"search",
|
||||
"serde",
|
||||
"settings",
|
||||
"theme",
|
||||
@@ -16010,6 +16068,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"indexmap",
|
||||
"inventory",
|
||||
"log",
|
||||
"palette",
|
||||
"parking_lot",
|
||||
@@ -20127,9 +20186,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_llm_client"
|
||||
version = "0.8.4"
|
||||
version = "0.8.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "de7d9523255f4e00ee3d0918e5407bd252d798a4a8e71f6d37f23317a1588203"
|
||||
checksum = "c740e29260b8797ad252c202ea09a255b3cbc13f30faaf92fb6b2490336106e0"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"serde",
|
||||
|
||||
19
Cargo.toml
19
Cargo.toml
@@ -2,6 +2,7 @@
|
||||
resolver = "2"
|
||||
members = [
|
||||
"crates/activity_indicator",
|
||||
"crates/acp",
|
||||
"crates/agent_ui",
|
||||
"crates/agent",
|
||||
"crates/agent_settings",
|
||||
@@ -215,8 +216,9 @@ edition = "2024"
|
||||
# Workspace member crates
|
||||
#
|
||||
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
acp = { path = "crates/acp" }
|
||||
agent = { path = "crates/agent" }
|
||||
activity_indicator = { path = "crates/activity_indicator" }
|
||||
agent_ui = { path = "crates/agent_ui" }
|
||||
agent_settings = { path = "crates/agent_settings" }
|
||||
ai = { path = "crates/ai" }
|
||||
@@ -398,6 +400,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agentic-coding-protocol = { path = "../agentic-coding-protocol" }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
|
||||
any_vec = "0.14"
|
||||
@@ -444,12 +447,12 @@ core-video = { version = "0.4.3", features = ["metal"] }
|
||||
cpal = "0.16"
|
||||
criterion = { version = "0.5", features = ["html_reports"] }
|
||||
ctor = "0.4.0"
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "b40956a7f4d1939da67429d941389ee306a3a308" }
|
||||
dap-types = { git = "https://github.com/zed-industries/dap-types", rev = "7f39295b441614ca9dbf44293e53c32f666897f9" }
|
||||
dashmap = "6.0"
|
||||
derive_more = "0.99.17"
|
||||
dirs = "4.0"
|
||||
documented = "0.9.1"
|
||||
dotenv = "0.15.0"
|
||||
dotenvy = "0.15.0"
|
||||
ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
@@ -480,7 +483,7 @@ json_dotpath = "1.1"
|
||||
jsonschema = "0.30.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed" ,rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
jupyter-websocket-client = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
libc = "0.2"
|
||||
libsqlite3-sys = { version = "0.30.1", features = ["bundled"] }
|
||||
linkify = "0.10.0"
|
||||
@@ -491,7 +494,7 @@ metal = "0.29"
|
||||
moka = { version = "0.12.10", features = ["sync"] }
|
||||
naga = { version = "25.0", features = ["wgsl-in"] }
|
||||
nanoid = "0.4"
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nbformat = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734" }
|
||||
nix = "0.29"
|
||||
num-format = "0.4.4"
|
||||
objc = "0.2"
|
||||
@@ -531,7 +534,7 @@ reqwest = { git = "https://github.com/zed-industries/reqwest.git", rev = "951c77
|
||||
"stream",
|
||||
] }
|
||||
rsa = "0.9.6"
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
runtimelib = { git = "https://github.com/ConradIrwin/runtimed", rev = "7130c804216b6914355d15d0b91ea91f6babd734", default-features = false, features = [
|
||||
"async-dispatcher-runtime",
|
||||
] }
|
||||
rust-embed = { version = "8.4", features = ["include-exclude"] }
|
||||
@@ -540,7 +543,7 @@ rustc-hash = "2.1.0"
|
||||
rustls = { version = "0.23.26" }
|
||||
rustls-platform-verifier = "0.5.0"
|
||||
scap = { git = "https://github.com/zed-industries/scap", rev = "08f0a01417505cc0990b9931a37e5120db92e0d0", default-features = false }
|
||||
schemars = { version = "0.8", features = ["impl_json_schema", "indexmap2"] }
|
||||
schemars = { version = "1.0", features = ["indexmap2"] }
|
||||
semver = "1.0"
|
||||
serde = { version = "1.0", features = ["derive", "rc"] }
|
||||
serde_derive = { version = "1.0", features = ["deserialize_in_place"] }
|
||||
@@ -625,7 +628,7 @@ wasmtime = { version = "29", default-features = false, features = [
|
||||
wasmtime-wasi = "29"
|
||||
which = "6.0.0"
|
||||
workspace-hack = "0.1.0"
|
||||
zed_llm_client = "0.8.4"
|
||||
zed_llm_client = "0.8.5"
|
||||
zstd = "0.11"
|
||||
|
||||
[workspace.dependencies.async-stripe]
|
||||
|
||||
@@ -1067,5 +1067,12 @@
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-f": "search::FocusSearch"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -1167,5 +1167,12 @@
|
||||
"ctrl-tab": "pane::ActivateNextItem",
|
||||
"ctrl-shift-tab": "pane::ActivatePreviousItem"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "KeymapEditor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"cmd-f": "search::FocusSearch"
|
||||
}
|
||||
}
|
||||
]
|
||||
|
||||
@@ -210,7 +210,8 @@
|
||||
"ctrl-w space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-w g space": "editor::OpenExcerptsSplit",
|
||||
"ctrl-6": "pane::AlternateFile",
|
||||
"ctrl-^": "pane::AlternateFile"
|
||||
"ctrl-^": "pane::AlternateFile",
|
||||
".": "vim::Repeat"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -219,7 +220,6 @@
|
||||
"ctrl-[": "editor::Cancel",
|
||||
"escape": "editor::Cancel",
|
||||
":": "command_palette::Toggle",
|
||||
".": "vim::Repeat",
|
||||
"c": "vim::PushChange",
|
||||
"shift-c": "vim::ChangeToEndOfLine",
|
||||
"d": "vim::PushDelete",
|
||||
@@ -849,6 +849,25 @@
|
||||
"shift-u": "git::UnstageAll"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && mode == auto_height && VimControl",
|
||||
"bindings": {
|
||||
// TODO: Implement search
|
||||
"/": null,
|
||||
"?": null,
|
||||
"#": null,
|
||||
"*": null,
|
||||
"n": null,
|
||||
"shift-n": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "GitCommit > Editor && VimControl && vim_mode == normal",
|
||||
"bindings": {
|
||||
"ctrl-c": "menu::Cancel",
|
||||
"escape": "menu::Cancel"
|
||||
}
|
||||
},
|
||||
{
|
||||
"context": "Editor && edit_prediction",
|
||||
"bindings": {
|
||||
@@ -860,14 +879,7 @@
|
||||
{
|
||||
"context": "MessageEditor > Editor && VimControl",
|
||||
"bindings": {
|
||||
"enter": "agent::Chat",
|
||||
// TODO: Implement search
|
||||
"/": null,
|
||||
"?": null,
|
||||
"#": null,
|
||||
"*": null,
|
||||
"n": null,
|
||||
"shift-n": null
|
||||
"enter": "agent::Chat"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
50
crates/acp/Cargo.toml
Normal file
50
crates/acp/Cargo.toml
Normal file
@@ -0,0 +1,50 @@
|
||||
[package]
|
||||
name = "acp"
|
||||
version = "0.1.0"
|
||||
edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
[lib]
|
||||
path = "src/acp.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["gpui/test-support", "project/test-support"]
|
||||
|
||||
[dependencies]
|
||||
agentic-coding-protocol = { path = "../../../agentic-coding-protocol" }
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
base64.workspace = true
|
||||
buffer_diff.workspace = true
|
||||
chrono.workspace = true
|
||||
collections.workspace = true
|
||||
editor.workspace = true
|
||||
futures.workspace = true
|
||||
gpui.workspace = true
|
||||
language.workspace = true
|
||||
log.workspace = true
|
||||
markdown.workspace = true
|
||||
parking_lot.workspace = true
|
||||
project.workspace = true
|
||||
proto.workspace = true
|
||||
settings.workspace = true
|
||||
smol.workspace = true
|
||||
theme.workspace = true
|
||||
ui.workspace = true
|
||||
util.workspace = true
|
||||
uuid.workspace = true
|
||||
workspace-hack.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
env_logger.workspace = true
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
project = { workspace = true, "features" = ["test-support"] }
|
||||
serde_json.workspace = true
|
||||
util.workspace = true
|
||||
settings.workspace = true
|
||||
1
crates/acp/LICENSE-GPL
Symbolic link
1
crates/acp/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
../../LICENSE-GPL
|
||||
893
crates/acp/src/acp.rs
Normal file
893
crates/acp/src/acp.rs
Normal file
@@ -0,0 +1,893 @@
|
||||
mod server;
|
||||
mod thread_view;
|
||||
|
||||
use agentic_coding_protocol::{self as acp};
|
||||
use anyhow::{Context as _, Result};
|
||||
use buffer_diff::BufferDiff;
|
||||
use chrono::{DateTime, Utc};
|
||||
use editor::{MultiBuffer, PathKey};
|
||||
use futures::channel::oneshot;
|
||||
use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
|
||||
use language::{Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _};
|
||||
use markdown::Markdown;
|
||||
use project::Project;
|
||||
use std::{mem, ops::Range, path::PathBuf, sync::Arc};
|
||||
use ui::{App, IconName};
|
||||
use util::{ResultExt, debug_panic};
|
||||
|
||||
pub use server::AcpServer;
|
||||
pub use thread_view::AcpThreadView;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
|
||||
pub struct ThreadId(SharedString);
|
||||
|
||||
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FileVersion(u64);
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AgentThreadSummary {
|
||||
pub id: ThreadId,
|
||||
pub title: String,
|
||||
pub created_at: DateTime<Utc>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
pub struct FileContent {
|
||||
pub path: PathBuf,
|
||||
pub version: FileVersion,
|
||||
pub content: SharedString,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct UserMessage {
|
||||
pub chunks: Vec<UserMessageChunk>,
|
||||
}
|
||||
|
||||
impl UserMessage {
|
||||
fn into_acp(self, cx: &App) -> acp::UserMessage {
|
||||
acp::UserMessage {
|
||||
chunks: self
|
||||
.chunks
|
||||
.into_iter()
|
||||
.map(|chunk| chunk.into_acp(cx))
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum UserMessageChunk {
|
||||
Text {
|
||||
chunk: Entity<Markdown>,
|
||||
},
|
||||
File {
|
||||
content: FileContent,
|
||||
},
|
||||
Directory {
|
||||
path: PathBuf,
|
||||
contents: Vec<FileContent>,
|
||||
},
|
||||
Symbol {
|
||||
path: PathBuf,
|
||||
range: Range<u64>,
|
||||
version: FileVersion,
|
||||
name: SharedString,
|
||||
content: SharedString,
|
||||
},
|
||||
Fetch {
|
||||
url: SharedString,
|
||||
content: SharedString,
|
||||
},
|
||||
}
|
||||
|
||||
impl UserMessageChunk {
|
||||
pub fn into_acp(self, cx: &App) -> acp::UserMessageChunk {
|
||||
match self {
|
||||
Self::Text { chunk } => acp::UserMessageChunk::Text {
|
||||
chunk: chunk.read(cx).source().to_string(),
|
||||
},
|
||||
Self::File { .. } => todo!(),
|
||||
Self::Directory { .. } => todo!(),
|
||||
Self::Symbol { .. } => todo!(),
|
||||
Self::Fetch { .. } => todo!(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
|
||||
Self::Text {
|
||||
chunk: cx.new(|cx| {
|
||||
Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub struct AssistantMessage {
|
||||
pub chunks: Vec<AssistantMessageChunk>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, PartialEq)]
|
||||
pub enum AssistantMessageChunk {
|
||||
Text { chunk: Entity<Markdown> },
|
||||
Thought { chunk: Entity<Markdown> },
|
||||
}
|
||||
|
||||
impl AssistantMessageChunk {
|
||||
pub fn from_acp(
|
||||
chunk: acp::AssistantMessageChunk,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
match chunk {
|
||||
acp::AssistantMessageChunk::Text { chunk } => Self::Text {
|
||||
chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
|
||||
},
|
||||
acp::AssistantMessageChunk::Thought { chunk } => Self::Thought {
|
||||
chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
|
||||
Self::Text {
|
||||
chunk: cx.new(|cx| {
|
||||
Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
|
||||
}),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum AgentThreadEntryContent {
|
||||
UserMessage(UserMessage),
|
||||
AssistantMessage(AssistantMessage),
|
||||
ToolCall(ToolCall),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolCall {
|
||||
id: ToolCallId,
|
||||
label: Entity<Markdown>,
|
||||
icon: IconName,
|
||||
content: Option<ToolCallContent>,
|
||||
status: ToolCallStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ToolCallStatus {
|
||||
WaitingForConfirmation {
|
||||
confirmation: ToolCallConfirmation,
|
||||
respond_tx: oneshot::Sender<acp::ToolCallConfirmationOutcome>,
|
||||
},
|
||||
Allowed {
|
||||
status: acp::ToolCallStatus,
|
||||
},
|
||||
Rejected,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ToolCallConfirmation {
|
||||
Edit {
|
||||
description: Option<Entity<Markdown>>,
|
||||
},
|
||||
Execute {
|
||||
command: String,
|
||||
root_command: String,
|
||||
description: Option<Entity<Markdown>>,
|
||||
},
|
||||
Mcp {
|
||||
server_name: String,
|
||||
tool_name: String,
|
||||
tool_display_name: String,
|
||||
description: Option<Entity<Markdown>>,
|
||||
},
|
||||
Fetch {
|
||||
urls: Vec<String>,
|
||||
description: Option<Entity<Markdown>>,
|
||||
},
|
||||
Other {
|
||||
description: Entity<Markdown>,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToolCallConfirmation {
|
||||
pub fn from_acp(
|
||||
confirmation: acp::ToolCallConfirmation,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let to_md = |description: String, cx: &mut App| -> Entity<Markdown> {
|
||||
cx.new(|cx| {
|
||||
Markdown::new(
|
||||
description.into(),
|
||||
Some(language_registry.clone()),
|
||||
None,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
};
|
||||
|
||||
match confirmation {
|
||||
acp::ToolCallConfirmation::Edit { description } => Self::Edit {
|
||||
description: description.map(|description| to_md(description, cx)),
|
||||
},
|
||||
acp::ToolCallConfirmation::Execute {
|
||||
command,
|
||||
root_command,
|
||||
description,
|
||||
} => Self::Execute {
|
||||
command,
|
||||
root_command,
|
||||
description: description.map(|description| to_md(description, cx)),
|
||||
},
|
||||
acp::ToolCallConfirmation::Mcp {
|
||||
server_name,
|
||||
tool_name,
|
||||
tool_display_name,
|
||||
description,
|
||||
} => Self::Mcp {
|
||||
server_name,
|
||||
tool_name,
|
||||
tool_display_name,
|
||||
description: description.map(|description| to_md(description, cx)),
|
||||
},
|
||||
acp::ToolCallConfirmation::Fetch { urls, description } => Self::Fetch {
|
||||
urls,
|
||||
description: description.map(|description| to_md(description, cx)),
|
||||
},
|
||||
acp::ToolCallConfirmation::Other { description } => Self::Other {
|
||||
description: to_md(description, cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ToolCallContent {
|
||||
Markdown { markdown: Entity<Markdown> },
|
||||
Diff { diff: Diff },
|
||||
}
|
||||
|
||||
impl ToolCallContent {
|
||||
pub fn from_acp(
|
||||
content: acp::ToolCallContent,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
match content {
|
||||
acp::ToolCallContent::Markdown { markdown } => Self::Markdown {
|
||||
markdown: cx.new(|cx| Markdown::new_text(markdown.into(), cx)),
|
||||
},
|
||||
acp::ToolCallContent::Diff { diff } => Self::Diff {
|
||||
diff: Diff::from_acp(diff, language_registry, cx),
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct Diff {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
path: PathBuf,
|
||||
_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
impl Diff {
|
||||
pub fn from_acp(
|
||||
diff: acp::Diff,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let acp::Diff {
|
||||
path,
|
||||
old_text,
|
||||
new_text,
|
||||
} = diff;
|
||||
|
||||
let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
|
||||
|
||||
let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
|
||||
let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
|
||||
let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
|
||||
let old_buffer_snapshot = old_buffer.read(cx).snapshot();
|
||||
let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
|
||||
let diff_task = buffer_diff.update(cx, |diff, cx| {
|
||||
diff.set_base_text(
|
||||
old_buffer_snapshot,
|
||||
Some(language_registry.clone()),
|
||||
new_buffer_snapshot,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let task = cx.spawn({
|
||||
let multibuffer = multibuffer.clone();
|
||||
let path = path.clone();
|
||||
async move |cx| {
|
||||
diff_task.await?;
|
||||
|
||||
multibuffer
|
||||
.update(cx, |multibuffer, cx| {
|
||||
let hunk_ranges = {
|
||||
let buffer = new_buffer.read(cx);
|
||||
let diff = buffer_diff.read(cx);
|
||||
diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
|
||||
.map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
|
||||
multibuffer.set_excerpts_for_path(
|
||||
PathKey::for_buffer(&new_buffer, cx),
|
||||
new_buffer.clone(),
|
||||
hunk_ranges,
|
||||
editor::DEFAULT_MULTIBUFFER_CONTEXT,
|
||||
cx,
|
||||
);
|
||||
multibuffer.add_diff(buffer_diff.clone(), cx);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
if let Some(language) = language_registry
|
||||
.language_for_file_path(&path)
|
||||
.await
|
||||
.log_err()
|
||||
{
|
||||
new_buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx))?;
|
||||
}
|
||||
|
||||
anyhow::Ok(())
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
multibuffer,
|
||||
path,
|
||||
_task: task,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A `ThreadEntryId` that is known to be a ToolCall
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ToolCallId(ThreadEntryId);
|
||||
|
||||
impl ToolCallId {
|
||||
pub fn as_u64(&self) -> u64 {
|
||||
self.0.0
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
|
||||
pub struct ThreadEntryId(pub u64);
|
||||
|
||||
impl ThreadEntryId {
|
||||
pub fn post_inc(&mut self) -> Self {
|
||||
let id = *self;
|
||||
self.0 += 1;
|
||||
id
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ThreadEntry {
|
||||
pub id: ThreadEntryId,
|
||||
pub content: AgentThreadEntryContent,
|
||||
}
|
||||
|
||||
pub struct AcpThread {
|
||||
id: ThreadId,
|
||||
next_entry_id: ThreadEntryId,
|
||||
entries: Vec<ThreadEntry>,
|
||||
server: Arc<AcpServer>,
|
||||
title: SharedString,
|
||||
project: Entity<Project>,
|
||||
}
|
||||
|
||||
enum AcpThreadEvent {
|
||||
NewEntry,
|
||||
EntryUpdated(usize),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
|
||||
impl AcpThread {
|
||||
pub fn new(
|
||||
server: Arc<AcpServer>,
|
||||
thread_id: ThreadId,
|
||||
entries: Vec<AgentThreadEntryContent>,
|
||||
project: Entity<Project>,
|
||||
_: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let mut next_entry_id = ThreadEntryId(0);
|
||||
Self {
|
||||
title: "A new agent2 thread".into(),
|
||||
entries: entries
|
||||
.into_iter()
|
||||
.map(|entry| ThreadEntry {
|
||||
id: next_entry_id.post_inc(),
|
||||
content: entry,
|
||||
})
|
||||
.collect(),
|
||||
server,
|
||||
id: thread_id,
|
||||
next_entry_id,
|
||||
project,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn title(&self) -> SharedString {
|
||||
self.title.clone()
|
||||
}
|
||||
|
||||
pub fn entries(&self) -> &[ThreadEntry] {
|
||||
&self.entries
|
||||
}
|
||||
|
||||
pub fn push_entry(
|
||||
&mut self,
|
||||
entry: AgentThreadEntryContent,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ThreadEntryId {
|
||||
let id = self.next_entry_id.post_inc();
|
||||
self.entries.push(ThreadEntry { id, content: entry });
|
||||
cx.emit(AcpThreadEvent::NewEntry);
|
||||
id
|
||||
}
|
||||
|
||||
pub fn push_assistant_chunk(
|
||||
&mut self,
|
||||
chunk: acp::AssistantMessageChunk,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let entries_len = self.entries.len();
|
||||
if let Some(last_entry) = self.entries.last_mut()
|
||||
&& let AgentThreadEntryContent::AssistantMessage(AssistantMessage { ref mut chunks }) =
|
||||
last_entry.content
|
||||
{
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
|
||||
|
||||
match (chunks.last_mut(), &chunk) {
|
||||
(
|
||||
Some(AssistantMessageChunk::Text { chunk: old_chunk }),
|
||||
acp::AssistantMessageChunk::Text { chunk: new_chunk },
|
||||
)
|
||||
| (
|
||||
Some(AssistantMessageChunk::Thought { chunk: old_chunk }),
|
||||
acp::AssistantMessageChunk::Thought { chunk: new_chunk },
|
||||
) => {
|
||||
old_chunk.update(cx, |old_chunk, cx| {
|
||||
old_chunk.append(&new_chunk, cx);
|
||||
});
|
||||
}
|
||||
_ => {
|
||||
chunks.push(AssistantMessageChunk::from_acp(
|
||||
chunk,
|
||||
self.project.read(cx).languages().clone(),
|
||||
cx,
|
||||
));
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let chunk = AssistantMessageChunk::from_acp(
|
||||
chunk,
|
||||
self.project.read(cx).languages().clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
self.push_entry(
|
||||
AgentThreadEntryContent::AssistantMessage(AssistantMessage {
|
||||
chunks: vec![chunk],
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn request_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
confirmation: acp::ToolCallConfirmation,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolCallRequest {
|
||||
let (tx, rx) = oneshot::channel();
|
||||
|
||||
let status = ToolCallStatus::WaitingForConfirmation {
|
||||
confirmation: ToolCallConfirmation::from_acp(
|
||||
confirmation,
|
||||
self.project.read(cx).languages().clone(),
|
||||
cx,
|
||||
),
|
||||
respond_tx: tx,
|
||||
};
|
||||
|
||||
let id = self.insert_tool_call(label, status, icon, content, cx);
|
||||
ToolCallRequest { id, outcome: rx }
|
||||
}
|
||||
|
||||
pub fn push_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolCallId {
|
||||
let status = ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
};
|
||||
|
||||
self.insert_tool_call(label, status, icon, content, cx)
|
||||
}
|
||||
|
||||
fn insert_tool_call(
|
||||
&mut self,
|
||||
label: String,
|
||||
status: ToolCallStatus,
|
||||
icon: acp::Icon,
|
||||
content: Option<acp::ToolCallContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> ToolCallId {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
|
||||
let entry_id = self.push_entry(
|
||||
AgentThreadEntryContent::ToolCall(ToolCall {
|
||||
// todo! clean up id creation
|
||||
id: ToolCallId(ThreadEntryId(self.entries.len() as u64)),
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new(label.into(), Some(language_registry.clone()), None, cx)
|
||||
}),
|
||||
icon: acp_icon_to_ui_icon(icon),
|
||||
content: content
|
||||
.map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
|
||||
status,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
|
||||
ToolCallId(entry_id)
|
||||
}
|
||||
|
||||
pub fn authorize_tool_call(
|
||||
&mut self,
|
||||
id: ToolCallId,
|
||||
outcome: acp::ToolCallConfirmationOutcome,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(entry) = self.entry_mut(id.0) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let AgentThreadEntryContent::ToolCall(call) = &mut entry.content else {
|
||||
debug_panic!("expected ToolCall");
|
||||
return;
|
||||
};
|
||||
|
||||
let new_status = if outcome == acp::ToolCallConfirmationOutcome::Reject {
|
||||
ToolCallStatus::Rejected
|
||||
} else {
|
||||
ToolCallStatus::Allowed {
|
||||
status: acp::ToolCallStatus::Running,
|
||||
}
|
||||
};
|
||||
|
||||
let curr_status = mem::replace(&mut call.status, new_status);
|
||||
|
||||
if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
|
||||
respond_tx.send(outcome).log_err();
|
||||
} else {
|
||||
debug_panic!("tried to authorize an already authorized tool call");
|
||||
}
|
||||
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(id.as_u64() as usize));
|
||||
}
|
||||
|
||||
pub fn update_tool_call(
|
||||
&mut self,
|
||||
id: ToolCallId,
|
||||
new_status: acp::ToolCallStatus,
|
||||
new_content: Option<acp::ToolCallContent>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Result<()> {
|
||||
let language_registry = self.project.read(cx).languages().clone();
|
||||
let entry = self.entry_mut(id.0).context("Entry not found")?;
|
||||
|
||||
match &mut entry.content {
|
||||
AgentThreadEntryContent::ToolCall(call) => {
|
||||
call.content = new_content.map(|new_content| {
|
||||
ToolCallContent::from_acp(new_content, language_registry, cx)
|
||||
});
|
||||
|
||||
match &mut call.status {
|
||||
ToolCallStatus::Allowed { status } => {
|
||||
*status = new_status;
|
||||
}
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => {
|
||||
anyhow::bail!("Tool call hasn't been authorized yet")
|
||||
}
|
||||
ToolCallStatus::Rejected => {
|
||||
anyhow::bail!("Tool call was rejected and therefore can't be updated")
|
||||
}
|
||||
}
|
||||
}
|
||||
_ => anyhow::bail!("Entry is not a tool call"),
|
||||
}
|
||||
|
||||
cx.emit(AcpThreadEvent::EntryUpdated(id.as_u64() as usize));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn entry_mut(&mut self, id: ThreadEntryId) -> Option<&mut ThreadEntry> {
|
||||
let entry = self.entries.get_mut(id.0 as usize);
|
||||
debug_assert!(
|
||||
entry.is_some(),
|
||||
"We shouldn't give out ids to entries that don't exist"
|
||||
);
|
||||
entry
|
||||
}
|
||||
|
||||
/// Returns true if the last turn is awaiting tool authorization
|
||||
pub fn waiting_for_tool_confirmation(&self) -> bool {
|
||||
for entry in self.entries.iter().rev() {
|
||||
match &entry.content {
|
||||
AgentThreadEntryContent::ToolCall(call) => match call.status {
|
||||
ToolCallStatus::WaitingForConfirmation { .. } => return true,
|
||||
ToolCallStatus::Allowed { .. } | ToolCallStatus::Rejected => continue,
|
||||
},
|
||||
AgentThreadEntryContent::UserMessage(_)
|
||||
| AgentThreadEntryContent::AssistantMessage(_) => {
|
||||
// Reached the beginning of the turn
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
pub fn send(&mut self, message: &str, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let agent = self.server.clone();
|
||||
let id = self.id.clone();
|
||||
let chunk =
|
||||
UserMessageChunk::from_str(message, self.project.read(cx).languages().clone(), cx);
|
||||
let message = UserMessage {
|
||||
chunks: vec![chunk],
|
||||
};
|
||||
self.push_entry(AgentThreadEntryContent::UserMessage(message.clone()), cx);
|
||||
let acp_message = message.into_acp(cx);
|
||||
cx.spawn(async move |_, cx| {
|
||||
agent.send_message(id, acp_message, cx).await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn acp_icon_to_ui_icon(icon: acp::Icon) -> IconName {
|
||||
match icon {
|
||||
acp::Icon::FileSearch => IconName::FileSearch,
|
||||
acp::Icon::Folder => IconName::Folder,
|
||||
acp::Icon::Globe => IconName::Globe,
|
||||
acp::Icon::Hammer => IconName::Hammer,
|
||||
acp::Icon::LightBulb => IconName::LightBulb,
|
||||
acp::Icon::Pencil => IconName::Pencil,
|
||||
acp::Icon::Regex => IconName::Regex,
|
||||
acp::Icon::Terminal => IconName::Terminal,
|
||||
}
|
||||
}
|
||||
|
||||
pub struct ToolCallRequest {
|
||||
pub id: ToolCallId,
|
||||
pub outcome: oneshot::Receiver<acp::ToolCallConfirmationOutcome>,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use futures::{FutureExt as _, channel::mpsc, select};
|
||||
use gpui::TestAppContext;
|
||||
use project::FakeFs;
|
||||
use serde_json::json;
|
||||
use settings::SettingsStore;
|
||||
use smol::stream::StreamExt as _;
|
||||
use std::{env, path::Path, process::Stdio, time::Duration};
|
||||
use util::path;
|
||||
|
||||
fn init_test(cx: &mut TestAppContext) {
|
||||
env_logger::try_init().ok();
|
||||
cx.update(|cx| {
|
||||
let settings_store = SettingsStore::test(cx);
|
||||
cx.set_global(settings_store);
|
||||
Project::init_settings(cx);
|
||||
language::init(cx);
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gemini_basic(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let server = gemini_acp_server(project.clone(), cx).await;
|
||||
let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send("Hello from Zed!", cx))
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, _| {
|
||||
assert_eq!(thread.entries.len(), 2);
|
||||
assert!(matches!(
|
||||
thread.entries[0].content,
|
||||
AgentThreadEntryContent::UserMessage(_)
|
||||
));
|
||||
assert!(matches!(
|
||||
thread.entries[1].content,
|
||||
AgentThreadEntryContent::AssistantMessage(_)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gemini_tool_call(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/private/tmp"),
|
||||
json!({"foo": "Lorem ipsum dolor", "bar": "bar", "baz": "baz"}),
|
||||
)
|
||||
.await;
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let server = gemini_acp_server(project.clone(), cx).await;
|
||||
let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
"Read the '/private/tmp/foo' file and tell me what you see.",
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
thread.read_with(cx, |thread, _cx| {
|
||||
assert!(matches!(
|
||||
&thread.entries()[2].content,
|
||||
AgentThreadEntryContent::ToolCall(ToolCall {
|
||||
status: ToolCallStatus::Allowed { .. },
|
||||
..
|
||||
})
|
||||
));
|
||||
|
||||
assert!(matches!(
|
||||
thread.entries[3].content,
|
||||
AgentThreadEntryContent::AssistantMessage(_)
|
||||
));
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_gemini_tool_call_with_confirmation(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
cx.executor().allow_parking();
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [path!("/private/tmp").as_ref()], cx).await;
|
||||
let server = gemini_acp_server(project.clone(), cx).await;
|
||||
let thread = server.create_thread(&mut cx.to_async()).await.unwrap();
|
||||
let full_turn = thread.update(cx, |thread, cx| {
|
||||
thread.send(r#"Run `echo "Hello, world!"`"#, cx)
|
||||
});
|
||||
|
||||
run_until_tool_call(&thread, cx).await;
|
||||
|
||||
let tool_call_id = thread.read_with(cx, |thread, _cx| {
|
||||
let AgentThreadEntryContent::ToolCall(ToolCall {
|
||||
id,
|
||||
status:
|
||||
ToolCallStatus::WaitingForConfirmation {
|
||||
confirmation: ToolCallConfirmation::Execute { root_command, .. },
|
||||
..
|
||||
},
|
||||
..
|
||||
}) = &thread.entries()[2].content
|
||||
else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
assert_eq!(root_command, "echo");
|
||||
|
||||
*id
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx);
|
||||
|
||||
assert!(matches!(
|
||||
&thread.entries()[2].content,
|
||||
AgentThreadEntryContent::ToolCall(ToolCall {
|
||||
status: ToolCallStatus::Allowed { .. },
|
||||
..
|
||||
})
|
||||
));
|
||||
});
|
||||
|
||||
full_turn.await.unwrap();
|
||||
|
||||
thread.read_with(cx, |thread, cx| {
|
||||
let AgentThreadEntryContent::ToolCall(ToolCall {
|
||||
content: Some(ToolCallContent::Markdown { markdown }),
|
||||
status: ToolCallStatus::Allowed { .. },
|
||||
..
|
||||
}) = &thread.entries()[2].content
|
||||
else {
|
||||
panic!();
|
||||
};
|
||||
|
||||
markdown.read_with(cx, |md, _cx| {
|
||||
assert!(
|
||||
md.source().contains("Hello, world!"),
|
||||
r#"Expected '{}' to contain "Hello, world!""#,
|
||||
md.source()
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
async fn run_until_tool_call(thread: &Entity<AcpThread>, cx: &mut TestAppContext) {
|
||||
let (mut tx, mut rx) = mpsc::channel::<()>(1);
|
||||
|
||||
let subscription = cx.update(|cx| {
|
||||
cx.subscribe(thread, move |thread, _, cx| {
|
||||
if thread
|
||||
.read(cx)
|
||||
.entries
|
||||
.iter()
|
||||
.any(|e| matches!(e.content, AgentThreadEntryContent::ToolCall(_)))
|
||||
{
|
||||
tx.try_send(()).unwrap();
|
||||
}
|
||||
})
|
||||
});
|
||||
|
||||
select! {
|
||||
_ = futures::FutureExt::fuse(smol::Timer::after(Duration::from_secs(10))) => {
|
||||
panic!("Timeout waiting for tool call")
|
||||
}
|
||||
_ = rx.next().fuse() => {
|
||||
drop(subscription);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub async fn gemini_acp_server(
|
||||
project: Entity<Project>,
|
||||
cx: &mut TestAppContext,
|
||||
) -> Arc<AcpServer> {
|
||||
let cli_path =
|
||||
Path::new(env!("CARGO_MANIFEST_DIR")).join("../../../gemini-cli/packages/cli");
|
||||
let mut command = util::command::new_smol_command("node");
|
||||
command
|
||||
.arg(cli_path)
|
||||
.arg("--acp")
|
||||
.current_dir("/private/tmp")
|
||||
.stdin(Stdio::piped())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(Stdio::inherit())
|
||||
.kill_on_drop(true);
|
||||
|
||||
if let Ok(gemini_key) = std::env::var("GEMINI_API_KEY") {
|
||||
command.env("GEMINI_API_KEY", gemini_key);
|
||||
}
|
||||
|
||||
let child = command.spawn().unwrap();
|
||||
let server = cx.update(|cx| AcpServer::stdio(child, project, cx));
|
||||
server.initialize().await.unwrap();
|
||||
server
|
||||
}
|
||||
}
|
||||
262
crates/acp/src/server.rs
Normal file
262
crates/acp/src/server.rs
Normal file
@@ -0,0 +1,262 @@
|
||||
use crate::{AcpThread, ThreadEntryId, ThreadId, ToolCallId, ToolCallRequest};
|
||||
use agentic_coding_protocol as acp;
|
||||
use anyhow::{Context as _, Result};
|
||||
use async_trait::async_trait;
|
||||
use collections::HashMap;
|
||||
use gpui::{App, AppContext, AsyncApp, Context, Entity, Task, WeakEntity};
|
||||
use parking_lot::Mutex;
|
||||
use project::Project;
|
||||
use smol::process::Child;
|
||||
use std::{process::ExitStatus, sync::Arc};
|
||||
use util::ResultExt;
|
||||
|
||||
pub struct AcpServer {
|
||||
connection: Arc<acp::AgentConnection>,
|
||||
threads: Arc<Mutex<HashMap<ThreadId, WeakEntity<AcpThread>>>>,
|
||||
project: Entity<Project>,
|
||||
exit_status: Arc<Mutex<Option<ExitStatus>>>,
|
||||
_handler_task: Task<()>,
|
||||
_io_task: Task<()>,
|
||||
}
|
||||
|
||||
struct AcpClientDelegate {
|
||||
project: Entity<Project>,
|
||||
threads: Arc<Mutex<HashMap<ThreadId, WeakEntity<AcpThread>>>>,
|
||||
cx: AsyncApp,
|
||||
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
|
||||
}
|
||||
|
||||
impl AcpClientDelegate {
|
||||
fn new(
|
||||
project: Entity<Project>,
|
||||
threads: Arc<Mutex<HashMap<ThreadId, WeakEntity<AcpThread>>>>,
|
||||
cx: AsyncApp,
|
||||
) -> Self {
|
||||
Self {
|
||||
project,
|
||||
threads,
|
||||
cx: cx,
|
||||
}
|
||||
}
|
||||
|
||||
fn update_thread<R>(
|
||||
&self,
|
||||
thread_id: &ThreadId,
|
||||
cx: &mut App,
|
||||
callback: impl FnOnce(&mut AcpThread, &mut Context<AcpThread>) -> R,
|
||||
) -> Option<R> {
|
||||
let thread = self.threads.lock().get(&thread_id)?.clone();
|
||||
let Some(thread) = thread.upgrade() else {
|
||||
self.threads.lock().remove(&thread_id);
|
||||
return None;
|
||||
};
|
||||
Some(thread.update(cx, callback))
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl acp::Client for AcpClientDelegate {
|
||||
async fn stream_assistant_message_chunk(
|
||||
&self,
|
||||
params: acp::StreamAssistantMessageChunkParams,
|
||||
) -> Result<acp::StreamAssistantMessageChunkResponse> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.update_thread(¶ms.thread_id.into(), cx, |thread, cx| {
|
||||
thread.push_assistant_chunk(params.chunk, cx)
|
||||
});
|
||||
})?;
|
||||
|
||||
Ok(acp::StreamAssistantMessageChunkResponse)
|
||||
}
|
||||
|
||||
async fn request_tool_call_confirmation(
|
||||
&self,
|
||||
request: acp::RequestToolCallConfirmationParams,
|
||||
) -> Result<acp::RequestToolCallConfirmationResponse> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let ToolCallRequest { id, outcome } = cx
|
||||
.update(|cx| {
|
||||
self.update_thread(&request.thread_id.into(), cx, |thread, cx| {
|
||||
thread.request_tool_call(
|
||||
request.label,
|
||||
request.icon,
|
||||
request.content,
|
||||
request.confirmation,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp::RequestToolCallConfirmationResponse {
|
||||
id: id.into(),
|
||||
outcome: outcome.await?,
|
||||
})
|
||||
}
|
||||
|
||||
async fn push_tool_call(
|
||||
&self,
|
||||
request: acp::PushToolCallParams,
|
||||
) -> Result<acp::PushToolCallResponse> {
|
||||
let cx = &mut self.cx.clone();
|
||||
let entry_id = cx
|
||||
.update(|cx| {
|
||||
self.update_thread(&request.thread_id.into(), cx, |thread, cx| {
|
||||
thread.push_tool_call(request.label, request.icon, request.content, cx)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")?;
|
||||
|
||||
Ok(acp::PushToolCallResponse {
|
||||
id: entry_id.into(),
|
||||
})
|
||||
}
|
||||
|
||||
async fn update_tool_call(
|
||||
&self,
|
||||
request: acp::UpdateToolCallParams,
|
||||
) -> Result<acp::UpdateToolCallResponse> {
|
||||
let cx = &mut self.cx.clone();
|
||||
|
||||
cx.update(|cx| {
|
||||
self.update_thread(&request.thread_id.into(), cx, |thread, cx| {
|
||||
thread.update_tool_call(
|
||||
request.tool_call_id.into(),
|
||||
request.status,
|
||||
request.content,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})?
|
||||
.context("Failed to update thread")??;
|
||||
|
||||
Ok(acp::UpdateToolCallResponse)
|
||||
}
|
||||
}
|
||||
|
||||
impl AcpServer {
|
||||
pub fn stdio(mut process: Child, project: Entity<Project>, cx: &mut App) -> Arc<Self> {
|
||||
let stdin = process.stdin.take().expect("process didn't have stdin");
|
||||
let stdout = process.stdout.take().expect("process didn't have stdout");
|
||||
|
||||
let threads: Arc<Mutex<HashMap<ThreadId, WeakEntity<AcpThread>>>> = Default::default();
|
||||
let (connection, handler_fut, io_fut) = acp::AgentConnection::connect_to_agent(
|
||||
AcpClientDelegate::new(project.clone(), threads.clone(), cx.to_async()),
|
||||
stdin,
|
||||
stdout,
|
||||
);
|
||||
|
||||
let exit_status: Arc<Mutex<Option<ExitStatus>>> = Default::default();
|
||||
let io_task = cx.background_spawn({
|
||||
let exit_status = exit_status.clone();
|
||||
async move {
|
||||
io_fut.await.log_err();
|
||||
let result = process.status().await.log_err();
|
||||
*exit_status.lock() = result;
|
||||
}
|
||||
});
|
||||
|
||||
Arc::new(Self {
|
||||
project,
|
||||
connection: Arc::new(connection),
|
||||
threads,
|
||||
exit_status,
|
||||
_handler_task: cx.foreground_executor().spawn(handler_fut),
|
||||
_io_task: io_task,
|
||||
})
|
||||
}
|
||||
|
||||
pub async fn initialize(&self) -> Result<acp::InitializeResponse> {
|
||||
self.connection
|
||||
.request(acp::InitializeParams)
|
||||
.await
|
||||
.map_err(to_anyhow)
|
||||
}
|
||||
|
||||
pub async fn authenticate(&self) -> Result<()> {
|
||||
self.connection
|
||||
.request(acp::AuthenticateParams)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn create_thread(self: Arc<Self>, cx: &mut AsyncApp) -> Result<Entity<AcpThread>> {
|
||||
let response = self
|
||||
.connection
|
||||
.request(acp::CreateThreadParams)
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
|
||||
let thread_id: ThreadId = response.thread_id.into();
|
||||
let server = self.clone();
|
||||
let thread = cx.new(|_| AcpThread {
|
||||
// todo!
|
||||
title: "ACP Thread".into(),
|
||||
id: thread_id.clone(), // Either<ErrorState, Id>
|
||||
next_entry_id: ThreadEntryId(0),
|
||||
entries: Vec::default(),
|
||||
project: self.project.clone(),
|
||||
server,
|
||||
})?;
|
||||
self.threads.lock().insert(thread_id, thread.downgrade());
|
||||
Ok(thread)
|
||||
}
|
||||
|
||||
pub async fn send_message(
|
||||
&self,
|
||||
thread_id: ThreadId,
|
||||
message: acp::UserMessage,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<()> {
|
||||
self.connection
|
||||
.request(acp::SendUserMessageParams {
|
||||
thread_id: thread_id.clone().into(),
|
||||
message,
|
||||
})
|
||||
.await
|
||||
.map_err(to_anyhow)?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn exit_status(&self) -> Option<ExitStatus> {
|
||||
*self.exit_status.lock()
|
||||
}
|
||||
}
|
||||
|
||||
#[track_caller]
|
||||
fn to_anyhow(e: acp::Error) -> anyhow::Error {
|
||||
log::error!(
|
||||
"failed to send message: {code}: {message}",
|
||||
code = e.code,
|
||||
message = e.message
|
||||
);
|
||||
anyhow::anyhow!(e.message)
|
||||
}
|
||||
|
||||
impl From<acp::ThreadId> for ThreadId {
|
||||
fn from(thread_id: acp::ThreadId) -> Self {
|
||||
Self(thread_id.0.into())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ThreadId> for acp::ThreadId {
|
||||
fn from(thread_id: ThreadId) -> Self {
|
||||
acp::ThreadId(thread_id.0.to_string())
|
||||
}
|
||||
}
|
||||
|
||||
impl From<acp::ToolCallId> for ToolCallId {
|
||||
fn from(tool_call_id: acp::ToolCallId) -> Self {
|
||||
Self(ThreadEntryId(tool_call_id.0))
|
||||
}
|
||||
}
|
||||
|
||||
impl From<ToolCallId> for acp::ToolCallId {
|
||||
fn from(tool_call_id: ToolCallId) -> Self {
|
||||
acp::ToolCallId(tool_call_id.as_u64())
|
||||
}
|
||||
}
|
||||
1252
crates/acp/src/thread_view.rs
Normal file
1252
crates/acp/src/thread_view.rs
Normal file
File diff suppressed because it is too large
Load Diff
@@ -68,7 +68,6 @@ zstd.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant_tools.workspace = true
|
||||
assistant_tool = { workspace = true, "features" = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
indoc.workspace = true
|
||||
language = { workspace = true, "features" = ["test-support"] }
|
||||
|
||||
@@ -5,12 +5,13 @@ pub mod context_store;
|
||||
pub mod history_store;
|
||||
pub mod thread;
|
||||
pub mod thread_store;
|
||||
pub mod tool_use;
|
||||
|
||||
pub use context::{AgentContext, ContextId, ContextLoadResult};
|
||||
pub use context_store::ContextStore;
|
||||
pub use thread::{
|
||||
LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, ThreadError,
|
||||
ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio, ZedAgentThread,
|
||||
LastRestoreCheckpoint, Message, MessageCrease, MessageId, MessageSegment, Thread, ThreadError,
|
||||
ThreadEvent, ThreadFeedback, ThreadId, ThreadSummary, TokenUsageRatio,
|
||||
};
|
||||
pub use thread_store::{SerializedThread, TextThreadStore, ThreadStore};
|
||||
|
||||
|
||||
@@ -96,16 +96,11 @@ impl AgentProfile {
|
||||
fn is_enabled(settings: &AgentProfileSettings, source: ToolSource, name: String) -> bool {
|
||||
match source {
|
||||
ToolSource::Native => *settings.tools.get(name.as_str()).unwrap_or(&false),
|
||||
ToolSource::ContextServer { id } => {
|
||||
if settings.enable_all_context_servers {
|
||||
return true;
|
||||
}
|
||||
|
||||
let Some(preset) = settings.context_servers.get(id.as_ref()) else {
|
||||
return false;
|
||||
};
|
||||
*preset.tools.get(name.as_str()).unwrap_or(&false)
|
||||
}
|
||||
ToolSource::ContextServer { id } => settings
|
||||
.context_servers
|
||||
.get(id.as_ref())
|
||||
.and_then(|preset| preset.tools.get(name.as_str()).copied())
|
||||
.unwrap_or(settings.enable_all_context_servers),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::thread::ZedAgentThread;
|
||||
use crate::thread::Thread;
|
||||
use assistant_context::AssistantContext;
|
||||
use assistant_tool::outline;
|
||||
use collections::HashSet;
|
||||
@@ -560,7 +560,7 @@ impl Display for FetchedUrlContext {
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct ThreadContextHandle {
|
||||
pub agent: Entity<ZedAgentThread>,
|
||||
pub thread: Entity<Thread>,
|
||||
pub context_id: ContextId,
|
||||
}
|
||||
|
||||
@@ -573,23 +573,23 @@ pub struct ThreadContext {
|
||||
|
||||
impl ThreadContextHandle {
|
||||
pub fn eq_for_key(&self, other: &Self) -> bool {
|
||||
self.agent == other.agent
|
||||
self.thread == other.thread
|
||||
}
|
||||
|
||||
pub fn hash_for_key<H: Hasher>(&self, state: &mut H) {
|
||||
self.agent.hash(state)
|
||||
self.thread.hash(state)
|
||||
}
|
||||
|
||||
pub fn title(&self, cx: &App) -> SharedString {
|
||||
self.agent.read(cx).summary().or_default()
|
||||
self.thread.read(cx).summary().or_default()
|
||||
}
|
||||
|
||||
fn load(self, cx: &App) -> Task<Option<(AgentContext, Vec<Entity<Buffer>>)>> {
|
||||
cx.spawn(async move |cx| {
|
||||
let text = ZedAgentThread::wait_for_detailed_summary_or_text(&self.agent, cx).await?;
|
||||
let text = Thread::wait_for_detailed_summary_or_text(&self.thread, cx).await?;
|
||||
let title = self
|
||||
.agent
|
||||
.read_with(cx, |thread, _| thread.summary().or_default())
|
||||
.thread
|
||||
.read_with(cx, |thread, _cx| thread.summary().or_default())
|
||||
.ok()?;
|
||||
let context = AgentContext::Thread(ThreadContext {
|
||||
title,
|
||||
|
||||
@@ -4,7 +4,7 @@ use crate::{
|
||||
FetchedUrlContext, FileContextHandle, ImageContext, RulesContextHandle,
|
||||
SelectionContextHandle, SymbolContextHandle, TextThreadContextHandle, ThreadContextHandle,
|
||||
},
|
||||
thread::{MessageId, ThreadId, ZedAgentThread},
|
||||
thread::{MessageId, Thread, ThreadId},
|
||||
thread_store::ThreadStore,
|
||||
};
|
||||
use anyhow::{Context as _, Result, anyhow};
|
||||
@@ -66,9 +66,8 @@ impl ContextStore {
|
||||
|
||||
pub fn new_context_for_thread(
|
||||
&self,
|
||||
thread: &ZedAgentThread,
|
||||
thread: &Thread,
|
||||
exclude_messages_from_id: Option<MessageId>,
|
||||
_cx: &App,
|
||||
) -> Vec<AgentContextHandle> {
|
||||
let existing_context = thread
|
||||
.messages()
|
||||
@@ -207,15 +206,12 @@ impl ContextStore {
|
||||
|
||||
pub fn add_thread(
|
||||
&mut self,
|
||||
thread: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
remove_if_exists: bool,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<AgentContextHandle> {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
let context = AgentContextHandle::Thread(ThreadContextHandle {
|
||||
agent: thread,
|
||||
context_id,
|
||||
});
|
||||
let context = AgentContextHandle::Thread(ThreadContextHandle { thread, context_id });
|
||||
|
||||
if let Some(existing) = self.context_set.get(AgentContextKey::ref_cast(&context)) {
|
||||
if remove_if_exists {
|
||||
@@ -391,10 +387,7 @@ impl ContextStore {
|
||||
if let Some(thread) = thread.upgrade() {
|
||||
let context_id = self.next_context_id.post_inc();
|
||||
self.insert_context(
|
||||
AgentContextHandle::Thread(ThreadContextHandle {
|
||||
agent: thread,
|
||||
context_id,
|
||||
}),
|
||||
AgentContextHandle::Thread(ThreadContextHandle { thread, context_id }),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -418,11 +411,11 @@ impl ContextStore {
|
||||
match &context {
|
||||
AgentContextHandle::Thread(thread_context) => {
|
||||
if let Some(thread_store) = self.thread_store.clone() {
|
||||
thread_context.agent.update(cx, |thread, cx| {
|
||||
thread_context.thread.update(cx, |thread, cx| {
|
||||
thread.start_generating_detailed_summary_if_needed(thread_store, cx);
|
||||
});
|
||||
self.context_thread_ids
|
||||
.insert(thread_context.agent.read(cx).id().clone());
|
||||
.insert(thread_context.thread.read(cx).id().clone());
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
@@ -448,7 +441,7 @@ impl ContextStore {
|
||||
match context {
|
||||
AgentContextHandle::Thread(thread_context) => {
|
||||
self.context_thread_ids
|
||||
.remove(thread_context.agent.read(cx).id());
|
||||
.remove(thread_context.thread.read(cx).id());
|
||||
}
|
||||
AgentContextHandle::TextThread(text_thread_context) => {
|
||||
if let Some(path) = text_thread_context.context.read(cx).path() {
|
||||
@@ -577,7 +570,7 @@ pub enum SuggestedContext {
|
||||
},
|
||||
Thread {
|
||||
name: SharedString,
|
||||
thread: WeakEntity<ZedAgentThread>,
|
||||
thread: WeakEntity<Thread>,
|
||||
},
|
||||
TextThread {
|
||||
name: SharedString,
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
||||
use crate::{
|
||||
context_server_tool::ContextServerTool,
|
||||
thread::{
|
||||
DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, ThreadId, ZedAgentThread,
|
||||
DetailedSummaryState, ExceededWindowError, MessageId, ProjectSnapshot, Thread, ThreadId,
|
||||
},
|
||||
};
|
||||
use agent_settings::{AgentProfileId, CompletionMode};
|
||||
@@ -400,9 +400,9 @@ impl ThreadStore {
|
||||
self.threads.iter()
|
||||
}
|
||||
|
||||
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<ZedAgentThread> {
|
||||
pub fn create_thread(&mut self, cx: &mut Context<Self>) -> Entity<Thread> {
|
||||
cx.new(|cx| {
|
||||
ZedAgentThread::new(
|
||||
Thread::new(
|
||||
self.project.clone(),
|
||||
self.tools.clone(),
|
||||
self.prompt_builder.clone(),
|
||||
@@ -416,9 +416,9 @@ impl ThreadStore {
|
||||
&mut self,
|
||||
serialized: SerializedThread,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Entity<ZedAgentThread> {
|
||||
) -> Entity<Thread> {
|
||||
cx.new(|cx| {
|
||||
ZedAgentThread::deserialize(
|
||||
Thread::deserialize(
|
||||
ThreadId::new(),
|
||||
serialized,
|
||||
self.project.clone(),
|
||||
@@ -436,7 +436,7 @@ impl ThreadStore {
|
||||
id: &ThreadId,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<Entity<ZedAgentThread>>> {
|
||||
) -> Task<Result<Entity<Thread>>> {
|
||||
let id = id.clone();
|
||||
let database_future = ThreadsDatabase::global_future(cx);
|
||||
let this = cx.weak_entity();
|
||||
@@ -449,7 +449,7 @@ impl ThreadStore {
|
||||
|
||||
let thread = this.update_in(cx, |this, window, cx| {
|
||||
cx.new(|cx| {
|
||||
ZedAgentThread::deserialize(
|
||||
Thread::deserialize(
|
||||
id.clone(),
|
||||
thread,
|
||||
this.project.clone(),
|
||||
@@ -466,14 +466,9 @@ impl ThreadStore {
|
||||
})
|
||||
}
|
||||
|
||||
pub fn save_thread(
|
||||
&self,
|
||||
thread: &Entity<ZedAgentThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Task<Result<()>> {
|
||||
let (metadata, serialized_thread) = thread.update(cx, |thread, cx| {
|
||||
(thread.id().clone(), thread.serialize(cx))
|
||||
});
|
||||
pub fn save_thread(&self, thread: &Entity<Thread>, cx: &mut Context<Self>) -> Task<Result<()>> {
|
||||
let (metadata, serialized_thread) =
|
||||
thread.update(cx, |thread, cx| (thread.id().clone(), thread.serialize(cx)));
|
||||
|
||||
let database_future = ThreadsDatabase::global_future(cx);
|
||||
cx.spawn(async move |this, cx| {
|
||||
@@ -705,7 +700,7 @@ impl SerializedThreadV0_1_0 {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SerializedMessage {
|
||||
pub id: MessageId,
|
||||
pub role: Role,
|
||||
@@ -719,9 +714,11 @@ pub struct SerializedMessage {
|
||||
pub context: String,
|
||||
#[serde(default)]
|
||||
pub creases: Vec<SerializedCrease>,
|
||||
#[serde(default)]
|
||||
pub is_hidden: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
#[serde(tag = "type")]
|
||||
pub enum SerializedMessageSegment {
|
||||
#[serde(rename = "text")]
|
||||
@@ -739,14 +736,14 @@ pub enum SerializedMessageSegment {
|
||||
},
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SerializedToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: SharedString,
|
||||
pub input: serde_json::Value,
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SerializedToolResult {
|
||||
pub tool_use_id: LanguageModelToolUseId,
|
||||
pub is_error: bool,
|
||||
@@ -804,11 +801,12 @@ impl LegacySerializedMessage {
|
||||
tool_results: self.tool_results,
|
||||
context: String::new(),
|
||||
creases: Vec::new(),
|
||||
is_hidden: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq, Clone)]
|
||||
#[derive(Debug, Serialize, Deserialize, PartialEq)]
|
||||
pub struct SerializedCrease {
|
||||
pub start: usize,
|
||||
pub end: usize,
|
||||
@@ -1107,6 +1105,7 @@ mod tests {
|
||||
tool_results: vec![],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false
|
||||
}],
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
initial_project_snapshot: None,
|
||||
@@ -1139,6 +1138,7 @@ mod tests {
|
||||
tool_results: vec![],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
},
|
||||
SerializedMessage {
|
||||
id: MessageId(2),
|
||||
@@ -1154,6 +1154,7 @@ mod tests {
|
||||
tool_results: vec![],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
},
|
||||
SerializedMessage {
|
||||
id: MessageId(1),
|
||||
@@ -1170,6 +1171,7 @@ mod tests {
|
||||
}],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
},
|
||||
],
|
||||
version: SerializedThreadV0_1_0::VERSION.to_string(),
|
||||
@@ -1201,6 +1203,7 @@ mod tests {
|
||||
tool_results: vec![],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false
|
||||
},
|
||||
SerializedMessage {
|
||||
id: MessageId(2),
|
||||
@@ -1221,6 +1224,7 @@ mod tests {
|
||||
}],
|
||||
context: "".to_string(),
|
||||
creases: vec![],
|
||||
is_hidden: false,
|
||||
},
|
||||
],
|
||||
version: SerializedThread::VERSION.to_string(),
|
||||
|
||||
567
crates/agent/src/tool_use.rs
Normal file
567
crates/agent/src/tool_use.rs
Normal file
@@ -0,0 +1,567 @@
|
||||
use crate::{
|
||||
thread::{MessageId, PromptId, ThreadId},
|
||||
thread_store::SerializedMessage,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use assistant_tool::{
|
||||
AnyToolCard, Tool, ToolResultContent, ToolResultOutput, ToolUseStatus, ToolWorkingSet,
|
||||
};
|
||||
use collections::HashMap;
|
||||
use futures::{FutureExt as _, future::Shared};
|
||||
use gpui::{App, Entity, SharedString, Task, Window};
|
||||
use icons::IconName;
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModel, LanguageModelRequest, LanguageModelToolResult,
|
||||
LanguageModelToolResultContent, LanguageModelToolUse, LanguageModelToolUseId, Role,
|
||||
};
|
||||
use project::Project;
|
||||
use std::sync::Arc;
|
||||
use util::truncate_lines_to_byte_limit;
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct ToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
pub name: SharedString,
|
||||
pub ui_text: SharedString,
|
||||
pub status: ToolUseStatus,
|
||||
pub input: serde_json::Value,
|
||||
pub icon: icons::IconName,
|
||||
pub needs_confirmation: bool,
|
||||
}
|
||||
|
||||
pub struct ToolUseState {
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
tool_uses_by_assistant_message: HashMap<MessageId, Vec<LanguageModelToolUse>>,
|
||||
tool_results: HashMap<LanguageModelToolUseId, LanguageModelToolResult>,
|
||||
pending_tool_uses_by_id: HashMap<LanguageModelToolUseId, PendingToolUse>,
|
||||
tool_result_cards: HashMap<LanguageModelToolUseId, AnyToolCard>,
|
||||
tool_use_metadata_by_id: HashMap<LanguageModelToolUseId, ToolUseMetadata>,
|
||||
}
|
||||
|
||||
impl ToolUseState {
|
||||
pub fn new(tools: Entity<ToolWorkingSet>) -> Self {
|
||||
Self {
|
||||
tools,
|
||||
tool_uses_by_assistant_message: HashMap::default(),
|
||||
tool_results: HashMap::default(),
|
||||
pending_tool_uses_by_id: HashMap::default(),
|
||||
tool_result_cards: HashMap::default(),
|
||||
tool_use_metadata_by_id: HashMap::default(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Constructs a [`ToolUseState`] from the given list of [`SerializedMessage`]s.
|
||||
///
|
||||
/// Accepts a function to filter the tools that should be used to populate the state.
|
||||
///
|
||||
/// If `window` is `None` (e.g., when in headless mode or when running evals),
|
||||
/// tool cards won't be deserialized
|
||||
pub fn from_serialized_messages(
|
||||
tools: Entity<ToolWorkingSet>,
|
||||
messages: &[SerializedMessage],
|
||||
project: Entity<Project>,
|
||||
window: Option<&mut Window>, // None in headless mode
|
||||
cx: &mut App,
|
||||
) -> Self {
|
||||
let mut this = Self::new(tools);
|
||||
let mut tool_names_by_id = HashMap::default();
|
||||
let mut window = window;
|
||||
|
||||
for message in messages {
|
||||
match message.role {
|
||||
Role::Assistant => {
|
||||
if !message.tool_uses.is_empty() {
|
||||
let tool_uses = message
|
||||
.tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| LanguageModelToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
raw_input: tool_use.input.to_string(),
|
||||
input: tool_use.input.clone(),
|
||||
is_input_complete: true,
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
tool_names_by_id.extend(
|
||||
tool_uses
|
||||
.iter()
|
||||
.map(|tool_use| (tool_use.id.clone(), tool_use.name.clone())),
|
||||
);
|
||||
|
||||
this.tool_uses_by_assistant_message
|
||||
.insert(message.id, tool_uses);
|
||||
|
||||
for tool_result in &message.tool_results {
|
||||
let tool_use_id = tool_result.tool_use_id.clone();
|
||||
let Some(tool_use) = tool_names_by_id.get(&tool_use_id) else {
|
||||
log::warn!("no tool name found for tool use: {tool_use_id:?}");
|
||||
continue;
|
||||
};
|
||||
|
||||
this.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name: tool_use.clone(),
|
||||
is_error: tool_result.is_error,
|
||||
content: tool_result.content.clone(),
|
||||
output: tool_result.output.clone(),
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(window) = &mut window {
|
||||
if let Some(tool) = this.tools.read(cx).tool(tool_use, cx) {
|
||||
if let Some(output) = tool_result.output.clone() {
|
||||
if let Some(card) = tool.deserialize_card(
|
||||
output,
|
||||
project.clone(),
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
this.tool_result_cards.insert(tool_use_id, card);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
Role::System | Role::User => {}
|
||||
}
|
||||
}
|
||||
|
||||
this
|
||||
}
|
||||
|
||||
pub fn cancel_pending(&mut self) -> Vec<PendingToolUse> {
|
||||
let mut cancelled_tool_uses = Vec::new();
|
||||
self.pending_tool_uses_by_id
|
||||
.retain(|tool_use_id, tool_use| {
|
||||
if matches!(tool_use.status, PendingToolUseStatus::Error { .. }) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let content = "Tool canceled by user".into();
|
||||
self.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name: tool_use.name.clone(),
|
||||
content,
|
||||
output: None,
|
||||
is_error: true,
|
||||
},
|
||||
);
|
||||
cancelled_tool_uses.push(tool_use.clone());
|
||||
false
|
||||
});
|
||||
cancelled_tool_uses
|
||||
}
|
||||
|
||||
pub fn pending_tool_uses(&self) -> Vec<&PendingToolUse> {
|
||||
self.pending_tool_uses_by_id.values().collect()
|
||||
}
|
||||
|
||||
pub fn tool_uses_for_message(&self, id: MessageId, cx: &App) -> Vec<ToolUse> {
|
||||
let Some(tool_uses_for_message) = &self.tool_uses_by_assistant_message.get(&id) else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
let mut tool_uses = Vec::new();
|
||||
|
||||
for tool_use in tool_uses_for_message.iter() {
|
||||
let tool_result = self.tool_results.get(&tool_use.id);
|
||||
|
||||
let status = (|| {
|
||||
if let Some(tool_result) = tool_result {
|
||||
let content = tool_result
|
||||
.content
|
||||
.to_str()
|
||||
.map(|str| str.to_owned().into())
|
||||
.unwrap_or_default();
|
||||
|
||||
return if tool_result.is_error {
|
||||
ToolUseStatus::Error(content)
|
||||
} else {
|
||||
ToolUseStatus::Finished(content)
|
||||
};
|
||||
}
|
||||
|
||||
if let Some(pending_tool_use) = self.pending_tool_uses_by_id.get(&tool_use.id) {
|
||||
match pending_tool_use.status {
|
||||
PendingToolUseStatus::Idle => ToolUseStatus::Pending,
|
||||
PendingToolUseStatus::NeedsConfirmation { .. } => {
|
||||
ToolUseStatus::NeedsConfirmation
|
||||
}
|
||||
PendingToolUseStatus::Running { .. } => ToolUseStatus::Running,
|
||||
PendingToolUseStatus::Error(ref err) => {
|
||||
ToolUseStatus::Error(err.clone().into())
|
||||
}
|
||||
PendingToolUseStatus::InputStillStreaming => {
|
||||
ToolUseStatus::InputStillStreaming
|
||||
}
|
||||
}
|
||||
} else {
|
||||
ToolUseStatus::Pending
|
||||
}
|
||||
})();
|
||||
|
||||
let (icon, needs_confirmation) =
|
||||
if let Some(tool) = self.tools.read(cx).tool(&tool_use.name, cx) {
|
||||
(tool.icon(), tool.needs_confirmation(&tool_use.input, cx))
|
||||
} else {
|
||||
(IconName::Cog, false)
|
||||
};
|
||||
|
||||
tool_uses.push(ToolUse {
|
||||
id: tool_use.id.clone(),
|
||||
name: tool_use.name.clone().into(),
|
||||
ui_text: self.tool_ui_label(
|
||||
&tool_use.name,
|
||||
&tool_use.input,
|
||||
tool_use.is_input_complete,
|
||||
cx,
|
||||
),
|
||||
input: tool_use.input.clone(),
|
||||
status,
|
||||
icon,
|
||||
needs_confirmation,
|
||||
})
|
||||
}
|
||||
|
||||
tool_uses
|
||||
}
|
||||
|
||||
pub fn tool_ui_label(
|
||||
&self,
|
||||
tool_name: &str,
|
||||
input: &serde_json::Value,
|
||||
is_input_complete: bool,
|
||||
cx: &App,
|
||||
) -> SharedString {
|
||||
if let Some(tool) = self.tools.read(cx).tool(tool_name, cx) {
|
||||
if is_input_complete {
|
||||
tool.ui_text(input).into()
|
||||
} else {
|
||||
tool.still_streaming_ui_text(input).into()
|
||||
}
|
||||
} else {
|
||||
format!("Unknown tool {tool_name:?}").into()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn tool_results_for_message(
|
||||
&self,
|
||||
assistant_message_id: MessageId,
|
||||
) -> Vec<&LanguageModelToolResult> {
|
||||
let Some(tool_uses) = self
|
||||
.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
tool_uses
|
||||
.iter()
|
||||
.filter_map(|tool_use| self.tool_results.get(&tool_use.id))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn message_has_tool_results(&self, assistant_message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)
|
||||
.map_or(false, |results| !results.is_empty())
|
||||
}
|
||||
|
||||
pub fn tool_result(
|
||||
&self,
|
||||
tool_use_id: &LanguageModelToolUseId,
|
||||
) -> Option<&LanguageModelToolResult> {
|
||||
self.tool_results.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn tool_result_card(&self, tool_use_id: &LanguageModelToolUseId) -> Option<&AnyToolCard> {
|
||||
self.tool_result_cards.get(tool_use_id)
|
||||
}
|
||||
|
||||
pub fn insert_tool_result_card(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
card: AnyToolCard,
|
||||
) {
|
||||
self.tool_result_cards.insert(tool_use_id, card);
|
||||
}
|
||||
|
||||
pub fn request_tool_use(
|
||||
&mut self,
|
||||
assistant_message_id: MessageId,
|
||||
tool_use: LanguageModelToolUse,
|
||||
metadata: ToolUseMetadata,
|
||||
cx: &App,
|
||||
) -> Arc<str> {
|
||||
let tool_uses = self
|
||||
.tool_uses_by_assistant_message
|
||||
.entry(assistant_message_id)
|
||||
.or_default();
|
||||
|
||||
let mut existing_tool_use_found = false;
|
||||
|
||||
for existing_tool_use in tool_uses.iter_mut() {
|
||||
if existing_tool_use.id == tool_use.id {
|
||||
*existing_tool_use = tool_use.clone();
|
||||
existing_tool_use_found = true;
|
||||
}
|
||||
}
|
||||
|
||||
if !existing_tool_use_found {
|
||||
tool_uses.push(tool_use.clone());
|
||||
}
|
||||
|
||||
let status = if tool_use.is_input_complete {
|
||||
self.tool_use_metadata_by_id
|
||||
.insert(tool_use.id.clone(), metadata);
|
||||
|
||||
PendingToolUseStatus::Idle
|
||||
} else {
|
||||
PendingToolUseStatus::InputStillStreaming
|
||||
};
|
||||
|
||||
let ui_text: Arc<str> = self
|
||||
.tool_ui_label(
|
||||
&tool_use.name,
|
||||
&tool_use.input,
|
||||
tool_use.is_input_complete,
|
||||
cx,
|
||||
)
|
||||
.into();
|
||||
|
||||
let may_perform_edits = self
|
||||
.tools
|
||||
.read(cx)
|
||||
.tool(&tool_use.name, cx)
|
||||
.is_some_and(|tool| tool.may_perform_edits());
|
||||
|
||||
self.pending_tool_uses_by_id.insert(
|
||||
tool_use.id.clone(),
|
||||
PendingToolUse {
|
||||
assistant_message_id,
|
||||
id: tool_use.id,
|
||||
name: tool_use.name.clone(),
|
||||
ui_text: ui_text.clone(),
|
||||
input: tool_use.input,
|
||||
may_perform_edits,
|
||||
status,
|
||||
},
|
||||
);
|
||||
|
||||
ui_text
|
||||
}
|
||||
|
||||
pub fn run_pending_tool(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
ui_text: SharedString,
|
||||
task: Task<()>,
|
||||
) {
|
||||
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||
tool_use.ui_text = ui_text.into();
|
||||
tool_use.status = PendingToolUseStatus::Running {
|
||||
_task: task.shared(),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
pub fn confirm_tool_use(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
ui_text: impl Into<Arc<str>>,
|
||||
input: serde_json::Value,
|
||||
request: Arc<LanguageModelRequest>,
|
||||
tool: Arc<dyn Tool>,
|
||||
) {
|
||||
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||
let ui_text = ui_text.into();
|
||||
tool_use.ui_text = ui_text.clone();
|
||||
let confirmation = Confirmation {
|
||||
tool_use_id,
|
||||
input,
|
||||
request,
|
||||
tool,
|
||||
ui_text,
|
||||
};
|
||||
tool_use.status = PendingToolUseStatus::NeedsConfirmation(Arc::new(confirmation));
|
||||
}
|
||||
}
|
||||
|
||||
pub fn insert_tool_output(
|
||||
&mut self,
|
||||
tool_use_id: LanguageModelToolUseId,
|
||||
tool_name: Arc<str>,
|
||||
output: Result<ToolResultOutput>,
|
||||
configured_model: Option<&ConfiguredModel>,
|
||||
) -> Option<PendingToolUse> {
|
||||
let metadata = self.tool_use_metadata_by_id.remove(&tool_use_id);
|
||||
|
||||
telemetry::event!(
|
||||
"Agent Tool Finished",
|
||||
model = metadata
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.model.telemetry_id()),
|
||||
model_provider = metadata
|
||||
.as_ref()
|
||||
.map(|metadata| metadata.model.provider_id().to_string()),
|
||||
thread_id = metadata.as_ref().map(|metadata| metadata.thread_id.clone()),
|
||||
prompt_id = metadata.as_ref().map(|metadata| metadata.prompt_id.clone()),
|
||||
tool_name,
|
||||
success = output.is_ok()
|
||||
);
|
||||
|
||||
match output {
|
||||
Ok(output) => {
|
||||
let tool_result = output.content;
|
||||
const BYTES_PER_TOKEN_ESTIMATE: usize = 3;
|
||||
|
||||
let old_use = self.pending_tool_uses_by_id.remove(&tool_use_id);
|
||||
|
||||
// Protect from overly large output
|
||||
let tool_output_limit = configured_model
|
||||
.map(|model| model.model.max_token_count() as usize * BYTES_PER_TOKEN_ESTIMATE)
|
||||
.unwrap_or(usize::MAX);
|
||||
|
||||
let content = match tool_result {
|
||||
ToolResultContent::Text(text) => {
|
||||
let text = if text.len() < tool_output_limit {
|
||||
text
|
||||
} else {
|
||||
let truncated = truncate_lines_to_byte_limit(&text, tool_output_limit);
|
||||
format!(
|
||||
"Tool result too long. The first {} bytes:\n\n{}",
|
||||
truncated.len(),
|
||||
truncated
|
||||
)
|
||||
};
|
||||
LanguageModelToolResultContent::Text(text.into())
|
||||
}
|
||||
ToolResultContent::Image(language_model_image) => {
|
||||
if language_model_image.estimate_tokens() < tool_output_limit {
|
||||
LanguageModelToolResultContent::Image(language_model_image)
|
||||
} else {
|
||||
self.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: "Tool responded with an image that would exceeded the remaining tokens".into(),
|
||||
is_error: true,
|
||||
output: None,
|
||||
},
|
||||
);
|
||||
|
||||
return old_use;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
self.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content,
|
||||
is_error: false,
|
||||
output: output.output,
|
||||
},
|
||||
);
|
||||
|
||||
old_use
|
||||
}
|
||||
Err(err) => {
|
||||
self.tool_results.insert(
|
||||
tool_use_id.clone(),
|
||||
LanguageModelToolResult {
|
||||
tool_use_id: tool_use_id.clone(),
|
||||
tool_name,
|
||||
content: LanguageModelToolResultContent::Text(err.to_string().into()),
|
||||
is_error: true,
|
||||
output: None,
|
||||
},
|
||||
);
|
||||
|
||||
if let Some(tool_use) = self.pending_tool_uses_by_id.get_mut(&tool_use_id) {
|
||||
tool_use.status = PendingToolUseStatus::Error(err.to_string().into());
|
||||
}
|
||||
|
||||
self.pending_tool_uses_by_id.get(&tool_use_id).cloned()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn has_tool_results(&self, assistant_message_id: MessageId) -> bool {
|
||||
self.tool_uses_by_assistant_message
|
||||
.contains_key(&assistant_message_id)
|
||||
}
|
||||
|
||||
pub fn tool_results(
|
||||
&self,
|
||||
assistant_message_id: MessageId,
|
||||
) -> impl Iterator<Item = (&LanguageModelToolUse, Option<&LanguageModelToolResult>)> {
|
||||
self.tool_uses_by_assistant_message
|
||||
.get(&assistant_message_id)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
.map(|tool_use| (tool_use, self.tool_results.get(&tool_use.id)))
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PendingToolUse {
|
||||
pub id: LanguageModelToolUseId,
|
||||
/// The ID of the Assistant message in which the tool use was requested.
|
||||
#[allow(unused)]
|
||||
pub assistant_message_id: MessageId,
|
||||
pub name: Arc<str>,
|
||||
pub ui_text: Arc<str>,
|
||||
pub input: serde_json::Value,
|
||||
pub status: PendingToolUseStatus,
|
||||
pub may_perform_edits: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct Confirmation {
|
||||
pub tool_use_id: LanguageModelToolUseId,
|
||||
pub input: serde_json::Value,
|
||||
pub ui_text: Arc<str>,
|
||||
pub request: Arc<LanguageModelRequest>,
|
||||
pub tool: Arc<dyn Tool>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub enum PendingToolUseStatus {
|
||||
InputStillStreaming,
|
||||
Idle,
|
||||
NeedsConfirmation(Arc<Confirmation>),
|
||||
Running { _task: Shared<Task<()>> },
|
||||
Error(#[allow(unused)] Arc<str>),
|
||||
}
|
||||
|
||||
impl PendingToolUseStatus {
|
||||
pub fn is_idle(&self) -> bool {
|
||||
matches!(self, PendingToolUseStatus::Idle)
|
||||
}
|
||||
|
||||
pub fn is_error(&self) -> bool {
|
||||
matches!(self, PendingToolUseStatus::Error(_))
|
||||
}
|
||||
|
||||
pub fn needs_confirmation(&self) -> bool {
|
||||
matches!(self, PendingToolUseStatus::NeedsConfirmation { .. })
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct ToolUseMetadata {
|
||||
pub model: Arc<dyn LanguageModel>,
|
||||
pub thread_id: ThreadId,
|
||||
pub prompt_id: PromptId,
|
||||
}
|
||||
@@ -6,9 +6,10 @@ use anyhow::{Result, bail};
|
||||
use collections::IndexMap;
|
||||
use gpui::{App, Pixels, SharedString};
|
||||
use language_model::LanguageModel;
|
||||
use schemars::{JsonSchema, schema::Schema};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{Settings, SettingsSources};
|
||||
use std::borrow::Cow;
|
||||
|
||||
pub use crate::agent_profile::*;
|
||||
|
||||
@@ -49,7 +50,7 @@ pub struct AgentSettings {
|
||||
pub dock: AgentDockPosition,
|
||||
pub default_width: Pixels,
|
||||
pub default_height: Pixels,
|
||||
pub default_model: LanguageModelSelection,
|
||||
pub default_model: Option<LanguageModelSelection>,
|
||||
pub inline_assistant_model: Option<LanguageModelSelection>,
|
||||
pub commit_message_model: Option<LanguageModelSelection>,
|
||||
pub thread_summary_model: Option<LanguageModelSelection>,
|
||||
@@ -211,7 +212,6 @@ impl AgentSettingsContent {
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, JsonSchema, Debug, Default)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct AgentSettingsContent {
|
||||
/// Whether the Agent is enabled.
|
||||
///
|
||||
@@ -321,29 +321,27 @@ pub struct LanguageModelSelection {
|
||||
pub struct LanguageModelProviderSetting(pub String);
|
||||
|
||||
impl JsonSchema for LanguageModelProviderSetting {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"LanguageModelProviderSetting".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
schemars::schema::SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
"anthropic".into(),
|
||||
"amazon-bedrock".into(),
|
||||
"google".into(),
|
||||
"lmstudio".into(),
|
||||
"ollama".into(),
|
||||
"openai".into(),
|
||||
"zed.dev".into(),
|
||||
"copilot_chat".into(),
|
||||
"deepseek".into(),
|
||||
"openrouter".into(),
|
||||
"mistral".into(),
|
||||
"vercel".into(),
|
||||
]),
|
||||
..Default::default()
|
||||
}
|
||||
.into()
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"enum": [
|
||||
"anthropic",
|
||||
"amazon-bedrock",
|
||||
"google",
|
||||
"lmstudio",
|
||||
"ollama",
|
||||
"openai",
|
||||
"zed.dev",
|
||||
"copilot_chat",
|
||||
"deepseek",
|
||||
"openrouter",
|
||||
"mistral",
|
||||
"vercel"
|
||||
]
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -359,15 +357,6 @@ impl From<&str> for LanguageModelProviderSetting {
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LanguageModelSelection {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
provider: LanguageModelProviderSetting("openai".to_string()),
|
||||
model: "gpt-4".to_string(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct AgentProfileContent {
|
||||
pub name: Arc<str>,
|
||||
@@ -411,7 +400,10 @@ impl Settings for AgentSettings {
|
||||
&mut settings.default_height,
|
||||
value.default_height.map(Into::into),
|
||||
);
|
||||
merge(&mut settings.default_model, value.default_model.clone());
|
||||
settings.default_model = value
|
||||
.default_model
|
||||
.clone()
|
||||
.or(settings.default_model.take());
|
||||
settings.inline_assistant_model = value
|
||||
.inline_assistant_model
|
||||
.clone()
|
||||
|
||||
@@ -13,12 +13,10 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = [
|
||||
"gpui/test-support",
|
||||
"language/test-support",
|
||||
]
|
||||
test-support = ["gpui/test-support", "language/test-support"]
|
||||
|
||||
[dependencies]
|
||||
acp.workspace = true
|
||||
agent.workspace = true
|
||||
agent_settings.workspace = true
|
||||
anyhow.workspace = true
|
||||
@@ -96,7 +94,6 @@ zed_llm_client.workspace = true
|
||||
|
||||
[dev-dependencies]
|
||||
assistant_tools.workspace = true
|
||||
assistant_tool = { workspace = true, "features" = ["test-support"] }
|
||||
buffer_diff = { workspace = true, features = ["test-support"] }
|
||||
editor = { workspace = true, features = ["test-support"] }
|
||||
gpui = { workspace = true, "features" = ["test-support"] }
|
||||
|
||||
@@ -5,17 +5,16 @@ use crate::ui::{
|
||||
AddedContext, AgentNotification, AgentNotificationEvent, AnimatedLabel, ContextPill,
|
||||
};
|
||||
use crate::{AgentPanel, ModelUsageContext};
|
||||
use agent::thread::{ToolUseSegment, UserMessageParams};
|
||||
use agent::{
|
||||
ContextStore, LastRestoreCheckpoint, MessageCrease, MessageId, MessageSegment, TextThreadStore,
|
||||
ThreadError, ThreadEvent, ThreadFeedback, ThreadStore, ThreadSummary, ZedAgentThread,
|
||||
Thread, ThreadError, ThreadEvent, ThreadFeedback, ThreadStore, ThreadSummary,
|
||||
context::{self, AgentContextHandle, RULES_ICON},
|
||||
thread::{PendingToolUseStatus, ToolUse},
|
||||
thread_store::RulesLoadingError,
|
||||
tool_use::{PendingToolUseStatus, ToolUse},
|
||||
};
|
||||
use agent_settings::{AgentSettings, NotifyWhenAgentWaiting};
|
||||
use anyhow::Context as _;
|
||||
use assistant_tool::{AnyToolCard, ToolUseStatus, ToolWorkingSet};
|
||||
use assistant_tool::ToolUseStatus;
|
||||
use audio::{Audio, Sound};
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::actions::{MoveUp, Paste};
|
||||
@@ -31,14 +30,13 @@ use gpui::{
|
||||
};
|
||||
use language::{Buffer, Language, LanguageRegistry};
|
||||
use language_model::{
|
||||
LanguageModelRequestMessage, LanguageModelToolResultContent, LanguageModelToolUseId,
|
||||
MessageContent, Role, StopReason,
|
||||
LanguageModelRequestMessage, LanguageModelToolUseId, MessageContent, Role, StopReason,
|
||||
};
|
||||
use markdown::parser::{CodeBlockKind, CodeBlockMetadata};
|
||||
use markdown::{
|
||||
HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle, ParsedMarkdown, PathWithRange,
|
||||
};
|
||||
use project::{Project, ProjectEntryId, ProjectItem as _};
|
||||
use project::{ProjectEntryId, ProjectItem as _};
|
||||
use rope::Point;
|
||||
use settings::{Settings as _, SettingsStore, update_settings_file};
|
||||
use std::ffi::OsStr;
|
||||
@@ -49,26 +47,26 @@ use std::time::Duration;
|
||||
use text::ToPoint;
|
||||
use theme::ThemeSettings;
|
||||
use ui::{
|
||||
Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize, Tooltip,
|
||||
prelude::*,
|
||||
Banner, Disclosure, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState, TextSize,
|
||||
Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use util::markdown::MarkdownCodeBlock;
|
||||
use util::{ResultExt as _, debug_panic};
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
const CODEBLOCK_CONTAINER_GROUP: &str = "codeblock_container";
|
||||
const EDIT_PREVIOUS_MESSAGE_MIN_LINES: usize = 1;
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
pub struct ActiveThread {
|
||||
context_store: Entity<ContextStore>,
|
||||
language_registry: Arc<LanguageRegistry>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
text_thread_store: Entity<TextThreadStore>,
|
||||
agent: Entity<ZedAgentThread>,
|
||||
// thread: Entity<Thread>,
|
||||
thread: Entity<Thread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
project: Entity<Project>,
|
||||
save_thread_task: Option<Task<()>>,
|
||||
messages: Vec<MessageId>,
|
||||
list_state: ListState,
|
||||
@@ -95,7 +93,7 @@ struct RenderedMessage {
|
||||
segments: Vec<RenderedMessageSegment>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
#[derive(Clone)]
|
||||
struct RenderedToolUse {
|
||||
label: Entity<Markdown>,
|
||||
input: Entity<Markdown>,
|
||||
@@ -165,103 +163,17 @@ impl RenderedMessage {
|
||||
cx,
|
||||
)))
|
||||
}
|
||||
MessageSegment::ToolUse { .. } => {
|
||||
todo!()
|
||||
}
|
||||
MessageSegment::RedactedThinking(_) => {}
|
||||
};
|
||||
}
|
||||
|
||||
fn update_tool_call(
|
||||
&mut self,
|
||||
segment_index: usize,
|
||||
segment: &ToolUseSegment,
|
||||
_tools: &Entity<ToolWorkingSet>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
if let Some(card) = segment.card.clone() {
|
||||
if self.segments.len() < segment_index {
|
||||
self.segments.push(RenderedMessageSegment::ToolUseCard(
|
||||
segment.status.clone(),
|
||||
card,
|
||||
))
|
||||
}
|
||||
return;
|
||||
}
|
||||
if self.segments.len() <= segment_index {
|
||||
self.segments
|
||||
.push(RenderedMessageSegment::ToolUseMarkdown(RenderedToolUse {
|
||||
label: cx.new(|cx| {
|
||||
Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
|
||||
}),
|
||||
input: cx.new(|cx| {
|
||||
Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
|
||||
}),
|
||||
output: cx.new(|cx| {
|
||||
Markdown::new("".into(), Some(self.language_registry.clone()), None, cx)
|
||||
}),
|
||||
}))
|
||||
}
|
||||
|
||||
dbg!(&self.segments);
|
||||
|
||||
let RenderedMessageSegment::ToolUseMarkdown(rendered) = &self.segments[segment_index]
|
||||
else {
|
||||
panic!()
|
||||
};
|
||||
|
||||
// todo!()
|
||||
// let ui_label = if let Some(tool) = tools.read(cx).tool(segment.name, cx) {
|
||||
// if segment.is_input_complete {
|
||||
// tool.ui_text(segment.input).into()
|
||||
// } else {
|
||||
// tool.still_streaming_ui_text(segment.input).into()
|
||||
// }
|
||||
// } else {
|
||||
// format!("Unknown tool {:?}", segment.name).into()
|
||||
// };
|
||||
|
||||
rendered.label.update(cx, |this, cx| {
|
||||
this.replace(segment.name.clone(), cx);
|
||||
});
|
||||
rendered.input.update(cx, |this, cx| {
|
||||
this.replace(
|
||||
MarkdownCodeBlock {
|
||||
tag: "json",
|
||||
text: &serde_json::to_string_pretty(&segment.input).unwrap_or_default(),
|
||||
}
|
||||
.to_string(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
rendered.output.update(cx, |_this, _cx| {
|
||||
match &segment.output {
|
||||
Some(Ok(LanguageModelToolResultContent::Text(_text))) => {
|
||||
// todo!
|
||||
}
|
||||
Some(Ok(LanguageModelToolResultContent::Image(_image))) => {
|
||||
// todo!
|
||||
}
|
||||
Some(Err(_error)) => {
|
||||
// todo!
|
||||
}
|
||||
None => {
|
||||
// todo!
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
enum RenderedMessageSegment {
|
||||
Thinking {
|
||||
content: Entity<Markdown>,
|
||||
scroll_handle: ScrollHandle,
|
||||
},
|
||||
Text(Entity<Markdown>),
|
||||
ToolUseCard(ToolUseStatus, AnyToolCard),
|
||||
ToolUseMarkdown(RenderedToolUse),
|
||||
}
|
||||
|
||||
fn parse_markdown(
|
||||
@@ -854,7 +766,7 @@ struct EditingMessageState {
|
||||
|
||||
impl ActiveThread {
|
||||
pub fn new(
|
||||
agent: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
thread_store: Entity<ThreadStore>,
|
||||
text_thread_store: Entity<TextThreadStore>,
|
||||
context_store: Entity<ContextStore>,
|
||||
@@ -864,8 +776,8 @@ impl ActiveThread {
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let subscriptions = vec![
|
||||
cx.observe(&agent, |_, _, cx| cx.notify()),
|
||||
cx.subscribe_in(&agent, window, Self::handle_thread_event),
|
||||
cx.observe(&thread, |_, _, cx| cx.notify()),
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event),
|
||||
cx.subscribe(&thread_store, Self::handle_rules_loading_error),
|
||||
cx.observe_global::<SettingsStore>(|_, cx| cx.notify()),
|
||||
];
|
||||
@@ -877,14 +789,12 @@ impl ActiveThread {
|
||||
.unwrap()
|
||||
}
|
||||
});
|
||||
let project = agent.read(cx).project().clone();
|
||||
let mut this = Self {
|
||||
language_registry,
|
||||
thread_store,
|
||||
text_thread_store,
|
||||
context_store,
|
||||
agent: agent.clone(),
|
||||
project,
|
||||
thread: thread.clone(),
|
||||
workspace,
|
||||
save_thread_task: None,
|
||||
messages: Vec::new(),
|
||||
@@ -907,8 +817,7 @@ impl ActiveThread {
|
||||
_load_edited_message_context_task: None,
|
||||
};
|
||||
|
||||
// todo! hold on to thread entity and get messages directly
|
||||
for message in agent.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
for message in thread.read(cx).messages().cloned().collect::<Vec<_>>() {
|
||||
let rendered_message = RenderedMessage::from_segments(
|
||||
&message.segments,
|
||||
this.language_registry.clone(),
|
||||
@@ -916,7 +825,7 @@ impl ActiveThread {
|
||||
);
|
||||
this.push_rendered_message(message.id, rendered_message);
|
||||
|
||||
for tool_use in agent.read(cx).tool_uses_for_message(message.id, cx) {
|
||||
for tool_use in thread.read(cx).tool_uses_for_message(message.id, cx) {
|
||||
this.render_tool_use_markdown(
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
@@ -930,8 +839,8 @@ impl ActiveThread {
|
||||
this
|
||||
}
|
||||
|
||||
pub fn agent(&self) -> &Entity<ZedAgentThread> {
|
||||
&self.agent
|
||||
pub fn thread(&self) -> &Entity<Thread> {
|
||||
&self.thread
|
||||
}
|
||||
|
||||
pub fn is_empty(&self) -> bool {
|
||||
@@ -939,17 +848,17 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
pub fn summary<'a>(&'a self, cx: &'a App) -> &'a ThreadSummary {
|
||||
self.agent.read(cx).summary()
|
||||
self.thread.read(cx).summary()
|
||||
}
|
||||
|
||||
pub fn regenerate_summary(&self, cx: &mut App) {
|
||||
self.agent.update(cx, |agent, cx| agent.summarize(cx))
|
||||
self.thread.update(cx, |thread, cx| thread.summarize(cx))
|
||||
}
|
||||
|
||||
pub fn cancel_last_completion(&mut self, window: &mut Window, cx: &mut App) -> bool {
|
||||
self.last_error.take();
|
||||
self.agent.update(cx, |agent, cx| {
|
||||
agent.cancel_last_completion(Some(window.window_handle()), cx)
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1039,7 +948,7 @@ impl ActiveThread {
|
||||
|
||||
fn handle_thread_event(
|
||||
&mut self,
|
||||
_agent: &Entity<ZedAgentThread>,
|
||||
_thread: &Entity<Thread>,
|
||||
event: &ThreadEvent,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
@@ -1057,8 +966,10 @@ impl ActiveThread {
|
||||
cx.notify();
|
||||
}
|
||||
ThreadEvent::CompletionCanceled => {
|
||||
self.project.update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.project().update(cx, |project, cx| {
|
||||
project.set_agent_location(None, cx);
|
||||
})
|
||||
});
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
@@ -1076,7 +987,7 @@ impl ActiveThread {
|
||||
}
|
||||
ThreadEvent::Stopped(reason) => match reason {
|
||||
Ok(StopReason::EndTurn | StopReason::MaxTokens) => {
|
||||
let used_tools = self.agent.read(cx).used_tools_since_last_user_message(cx);
|
||||
let used_tools = self.thread.read(cx).used_tools_since_last_user_message();
|
||||
self.play_notification_sound(window, cx);
|
||||
self.show_notification(
|
||||
if used_tools {
|
||||
@@ -1114,28 +1025,9 @@ impl ActiveThread {
|
||||
rendered_message.append_thinking(text, cx);
|
||||
}
|
||||
}
|
||||
ThreadEvent::StreamedToolUse2 {
|
||||
message_id,
|
||||
segment_index,
|
||||
} => {
|
||||
if let Some(rendered_message) = self.rendered_messages_by_id.get_mut(&message_id) {
|
||||
self.agent.update(cx, |agent, cx| {
|
||||
if let Some(message) = agent.message(*message_id) {
|
||||
let MessageSegment::ToolUse(tool_use) =
|
||||
&message.segments[*segment_index]
|
||||
else {
|
||||
debug_panic!("segment index mismatch");
|
||||
return;
|
||||
};
|
||||
let tools = self.agent.read(cx).tools().clone();
|
||||
rendered_message.update_tool_call(*segment_index, tool_use, &tools, cx);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
ThreadEvent::MessageAdded(message_id) => {
|
||||
if let Some(rendered_message) = self.agent.update(cx, |agent, cx| {
|
||||
agent.message(*message_id).map(|message| {
|
||||
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
|
||||
thread.message(*message_id).map(|message| {
|
||||
RenderedMessage::from_segments(
|
||||
&message.segments,
|
||||
self.language_registry.clone(),
|
||||
@@ -1151,8 +1043,8 @@ impl ActiveThread {
|
||||
}
|
||||
ThreadEvent::MessageEdited(message_id) => {
|
||||
if let Some(index) = self.messages.iter().position(|id| id == message_id) {
|
||||
if let Some(rendered_message) = self.agent.update(cx, |agent, cx| {
|
||||
agent.message(*message_id).map(|message| {
|
||||
if let Some(rendered_message) = self.thread.update(cx, |thread, cx| {
|
||||
thread.message(*message_id).map(|message| {
|
||||
let mut rendered_message = RenderedMessage {
|
||||
language_registry: self.language_registry.clone(),
|
||||
segments: Vec::with_capacity(message.segments.len()),
|
||||
@@ -1209,7 +1101,7 @@ impl ActiveThread {
|
||||
tool_use.id.clone(),
|
||||
tool_use.ui_text.clone(),
|
||||
&serde_json::to_string_pretty(&tool_use.input).unwrap_or_default(),
|
||||
self.agent
|
||||
self.thread
|
||||
.read(cx)
|
||||
.output_for_tool(&tool_use.id)
|
||||
.map(|output| output.clone().into())
|
||||
@@ -1229,7 +1121,7 @@ impl ActiveThread {
|
||||
tool_use_id.clone(),
|
||||
ui_text,
|
||||
invalid_input_json,
|
||||
self.agent
|
||||
self.thread
|
||||
.read(cx)
|
||||
.output_for_tool(tool_use_id)
|
||||
.map(|output| output.clone().into())
|
||||
@@ -1245,7 +1137,7 @@ impl ActiveThread {
|
||||
tool_use_id.clone(),
|
||||
ui_text,
|
||||
"",
|
||||
self.agent
|
||||
self.thread
|
||||
.read(cx)
|
||||
.output_for_tool(tool_use_id)
|
||||
.map(|output| output.clone().into())
|
||||
@@ -1294,7 +1186,7 @@ impl ActiveThread {
|
||||
return;
|
||||
}
|
||||
|
||||
let title = self.agent.read(cx).summary().unwrap_or("Agent Panel");
|
||||
let title = self.thread.read(cx).summary().unwrap_or("Agent Panel");
|
||||
|
||||
match AgentSettings::get_global(cx).notify_when_agent_waiting {
|
||||
NotifyWhenAgentWaiting::PrimaryScreen => {
|
||||
@@ -1405,12 +1297,12 @@ impl ActiveThread {
|
||||
///
|
||||
/// Only one task to save the thread will be in flight at a time.
|
||||
fn save_thread(&mut self, cx: &mut Context<Self>) {
|
||||
let agent = self.agent.clone();
|
||||
let thread = self.thread.clone();
|
||||
self.save_thread_task = Some(cx.spawn(async move |this, cx| {
|
||||
let task = this
|
||||
.update(cx, |this, cx| {
|
||||
this.thread_store
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&agent, cx))
|
||||
.update(cx, |thread_store, cx| thread_store.save_thread(&thread, cx))
|
||||
})
|
||||
.ok();
|
||||
|
||||
@@ -1460,7 +1352,7 @@ impl ActiveThread {
|
||||
Some(self.text_thread_store.downgrade()),
|
||||
context_picker_menu_handle.clone(),
|
||||
SuggestContextKind::File,
|
||||
ModelUsageContext::Thread(self.agent.clone()),
|
||||
ModelUsageContext::Thread(self.thread.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1512,13 +1404,13 @@ impl ActiveThread {
|
||||
cx.emit(ActiveThreadEvent::EditingMessageTokenCountChanged);
|
||||
state._update_token_count_task.take();
|
||||
|
||||
let Some(configured_model) = self.agent.read(cx).configured_model() else {
|
||||
let Some(configured_model) = self.thread.read(cx).configured_model() else {
|
||||
state.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
|
||||
let editor = state.editor.clone();
|
||||
let agent = self.agent.clone();
|
||||
let thread = self.thread.clone();
|
||||
let message_id = *message_id;
|
||||
|
||||
state._update_token_count_task = Some(cx.spawn(async move |this, cx| {
|
||||
@@ -1530,7 +1422,7 @@ impl ActiveThread {
|
||||
|
||||
let token_count = if let Some(task) = cx
|
||||
.update(|cx| {
|
||||
let Some(message) = agent.read(cx).message(message_id) else {
|
||||
let Some(message) = thread.read(cx).message(message_id) else {
|
||||
log::error!("Message that was being edited no longer exists");
|
||||
return None;
|
||||
};
|
||||
@@ -1662,8 +1554,8 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
let Some(model) = self
|
||||
.agent
|
||||
.update(cx, |agent, cx| agent.get_or_init_configured_model(cx))
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
|
||||
else {
|
||||
return;
|
||||
};
|
||||
@@ -1677,13 +1569,12 @@ impl ActiveThread {
|
||||
|
||||
let creases = state.editor.update(cx, extract_message_creases);
|
||||
|
||||
let new_context = self.context_store.read(cx).new_context_for_thread(
|
||||
self.agent.read(cx),
|
||||
Some(message_id),
|
||||
cx,
|
||||
);
|
||||
let new_context = self
|
||||
.context_store
|
||||
.read(cx)
|
||||
.new_context_for_thread(self.thread.read(cx), Some(message_id));
|
||||
|
||||
let project = self.project.clone();
|
||||
let project = self.thread.read(cx).project().clone();
|
||||
let prompt_store = self.thread_store.read(cx).prompt_store().clone();
|
||||
|
||||
let git_store = project.read(cx).git_store().clone();
|
||||
@@ -1696,24 +1587,32 @@ impl ActiveThread {
|
||||
futures::future::join(load_context_task, checkpoint).await;
|
||||
let _ = this
|
||||
.update_in(cx, |this, window, cx| {
|
||||
this.agent.update(cx, |agent, cx| {
|
||||
agent.truncate(message_id, cx);
|
||||
agent.send_message(
|
||||
UserMessageParams {
|
||||
text: edited_text,
|
||||
creases,
|
||||
checkpoint: checkpoint.ok(),
|
||||
context,
|
||||
},
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.edit_message(
|
||||
message_id,
|
||||
Role::User,
|
||||
vec![MessageSegment::Text(edited_text)],
|
||||
creases,
|
||||
Some(context.loaded_context),
|
||||
checkpoint.ok(),
|
||||
cx,
|
||||
);
|
||||
for message_id in this.messages_after(message_id) {
|
||||
thread.delete_message(*message_id, cx);
|
||||
}
|
||||
});
|
||||
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx);
|
||||
thread.send_to_model(
|
||||
model.model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
// todo! do we need this?
|
||||
this._load_edited_message_context_task = None;
|
||||
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
@@ -1728,6 +1627,14 @@ impl ActiveThread {
|
||||
}
|
||||
}
|
||||
|
||||
fn messages_after(&self, message_id: MessageId) -> &[MessageId] {
|
||||
self.messages
|
||||
.iter()
|
||||
.position(|id| *id == message_id)
|
||||
.map(|index| &self.messages[index + 1..])
|
||||
.unwrap_or(&[])
|
||||
}
|
||||
|
||||
fn handle_cancel_click(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.cancel_editing_message(&menu::Cancel, window, cx);
|
||||
}
|
||||
@@ -1748,7 +1655,7 @@ impl ActiveThread {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let report = self.agent.update(cx, |thread, cx| {
|
||||
let report = self.thread.update(cx, |thread, cx| {
|
||||
thread.report_message_feedback(message_id, feedback, cx)
|
||||
});
|
||||
|
||||
@@ -1807,17 +1714,17 @@ impl ActiveThread {
|
||||
return;
|
||||
};
|
||||
|
||||
let report_task = self.agent.update(cx, |thread, cx| {
|
||||
let report_task = self.thread.update(cx, |thread, cx| {
|
||||
thread.report_message_feedback(message_id, ThreadFeedback::Negative, cx)
|
||||
});
|
||||
|
||||
let comments = editor.read(cx).text(cx);
|
||||
if !comments.is_empty() {
|
||||
let thread_id = self.agent.read(cx).id().clone();
|
||||
let thread_id = self.thread.read(cx).id().clone();
|
||||
let comments_value = String::from(comments.as_str());
|
||||
|
||||
let message_content = self
|
||||
.agent
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(message_id)
|
||||
.map(|msg| msg.to_string())
|
||||
@@ -1893,42 +1800,45 @@ impl ActiveThread {
|
||||
fn render_message(&self, ix: usize, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
|
||||
let message_id = self.messages[ix];
|
||||
let workspace = self.workspace.clone();
|
||||
let agent = self.agent.read(cx);
|
||||
let thread = self.thread.read(cx);
|
||||
|
||||
let is_first_message = ix == 0;
|
||||
let is_last_message = ix == self.messages.len() - 1;
|
||||
|
||||
let Some(message) = agent.message(message_id) else {
|
||||
let Some(message) = thread.message(message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
let is_generating = agent.is_generating();
|
||||
let is_generating_stale = agent.is_generation_stale().unwrap_or(false);
|
||||
let is_generating = thread.is_generating();
|
||||
let is_generating_stale = thread.is_generation_stale().unwrap_or(false);
|
||||
|
||||
let loading_dots = (is_generating && is_last_message).then(|| {
|
||||
h_flex()
|
||||
.h_8()
|
||||
.my_3()
|
||||
.mx_5()
|
||||
.when(is_generating_stale, |this| {
|
||||
.when(is_generating_stale || message.is_hidden, |this| {
|
||||
this.child(AnimatedLabel::new("").size(LabelSize::Small))
|
||||
})
|
||||
});
|
||||
|
||||
if message.is_hidden {
|
||||
return div().children(loading_dots).into_any();
|
||||
}
|
||||
|
||||
let Some(rendered_message) = self.rendered_messages_by_id.get(&message_id) else {
|
||||
return Empty.into_any();
|
||||
};
|
||||
|
||||
// Get all the data we need from thread before we start using it in closures
|
||||
let checkpoint = agent.checkpoint_for_message(message_id);
|
||||
let configured_model = agent.configured_model().map(|m| m.model);
|
||||
let added_context = agent
|
||||
let checkpoint = thread.checkpoint_for_message(message_id);
|
||||
let configured_model = thread.configured_model().map(|m| m.model);
|
||||
let added_context = thread
|
||||
.context_for_message(message_id)
|
||||
.map(|context| AddedContext::new_attached(context, configured_model.as_ref(), cx))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
// let tool_uses = message.segments
|
||||
let tool_uses = agent.tool_uses_for_message(message_id, cx);
|
||||
let tool_uses = thread.tool_uses_for_message(message_id, cx);
|
||||
let has_tool_uses = !tool_uses.is_empty();
|
||||
|
||||
let editing_message_state = self
|
||||
@@ -1947,11 +1857,11 @@ impl ActiveThread {
|
||||
.icon_color(Color::Ignored)
|
||||
.tooltip(Tooltip::text("Open Thread as Markdown"))
|
||||
.on_click({
|
||||
let agent = self.agent.clone();
|
||||
let thread = self.thread.clone();
|
||||
let workspace = self.workspace.clone();
|
||||
move |_, window, cx| {
|
||||
if let Some(workspace) = workspace.upgrade() {
|
||||
open_active_thread_as_markdown(agent.clone(), workspace, window, cx)
|
||||
open_active_thread_as_markdown(thread.clone(), workspace, window, cx)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
}
|
||||
@@ -1965,10 +1875,7 @@ impl ActiveThread {
|
||||
this.scroll_to_top(cx);
|
||||
}));
|
||||
|
||||
// For all items that should be aligned with the LLM's response.
|
||||
const RESPONSE_PADDING_X: Pixels = px(19.);
|
||||
|
||||
let show_feedback = self.agent.read(cx).is_turn_end(ix);
|
||||
let show_feedback = thread.is_turn_end(ix);
|
||||
let feedback_container = h_flex()
|
||||
.group("feedback_container")
|
||||
.mt_1()
|
||||
@@ -1980,7 +1887,7 @@ impl ActiveThread {
|
||||
.gap_1p5()
|
||||
.flex_wrap()
|
||||
.justify_end();
|
||||
let feedback_items = match self.agent.read(cx).message_feedback(message_id) {
|
||||
let feedback_items = match self.thread.read(cx).message_feedback(message_id) {
|
||||
Some(feedback) => feedback_container
|
||||
.child(
|
||||
div().visible_on_hover("feedback_container").child(
|
||||
@@ -2086,9 +1993,6 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
let message_is_empty = message.should_display_content();
|
||||
let message_is_ui_only = message.ui_only;
|
||||
let message_creases = message.creases.clone();
|
||||
let role = message.role;
|
||||
let has_content = !message_is_empty || !added_context.is_empty();
|
||||
|
||||
let message_content = has_content.then(|| {
|
||||
@@ -2131,10 +2035,10 @@ impl ActiveThread {
|
||||
}
|
||||
});
|
||||
|
||||
let styled_message = if message_is_ui_only {
|
||||
let styled_message = if message.ui_only {
|
||||
self.render_ui_notification(message_content, ix, cx)
|
||||
} else {
|
||||
match role {
|
||||
match message.role {
|
||||
Role::User => {
|
||||
let colors = cx.theme().colors();
|
||||
v_flex()
|
||||
@@ -2239,9 +2143,10 @@ impl ActiveThread {
|
||||
}),
|
||||
)
|
||||
.on_click(cx.listener({
|
||||
let message_creases = message.creases.clone();
|
||||
move |this, _, window, cx| {
|
||||
if let Some(message_text) =
|
||||
this.agent.read(cx).message(message_id).and_then(|message| {
|
||||
this.thread.read(cx).message(message_id).and_then(|message| {
|
||||
message.segments.first().and_then(|segment| {
|
||||
match segment {
|
||||
MessageSegment::Text(message_text) => {
|
||||
@@ -2312,7 +2217,7 @@ impl ActiveThread {
|
||||
let mut is_pending = false;
|
||||
let mut error = None;
|
||||
if let Some(last_restore_checkpoint) =
|
||||
self.agent.read(cx).last_restore_checkpoint()
|
||||
self.thread.read(cx).last_restore_checkpoint()
|
||||
{
|
||||
if last_restore_checkpoint.message_id() == message_id {
|
||||
match last_restore_checkpoint {
|
||||
@@ -2341,7 +2246,7 @@ impl ActiveThread {
|
||||
.label_size(LabelSize::XSmall)
|
||||
.disabled(is_pending)
|
||||
.on_click(cx.listener(move |this, _, _window, cx| {
|
||||
this.agent.update(cx, |thread, cx| {
|
||||
this.thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.restore_checkpoint(checkpoint.clone(), cx)
|
||||
.detach_and_log_err(cx);
|
||||
@@ -2475,11 +2380,11 @@ impl ActiveThread {
|
||||
rendered_message: &RenderedMessage,
|
||||
has_tool_uses: bool,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
window: &Window,
|
||||
cx: &Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
let is_last_message = self.messages.last() == Some(&message_id);
|
||||
let is_generating = self.agent.read(cx).is_generating();
|
||||
let is_generating = self.thread.read(cx).is_generating();
|
||||
let pending_thinking_segment_index = if is_generating && is_last_message && !has_tool_uses {
|
||||
rendered_message
|
||||
.segments
|
||||
@@ -2493,7 +2398,7 @@ impl ActiveThread {
|
||||
};
|
||||
|
||||
let message_role = self
|
||||
.agent
|
||||
.thread
|
||||
.read(cx)
|
||||
.message(message_id)
|
||||
.map(|m| m.role)
|
||||
@@ -2608,23 +2513,6 @@ impl ActiveThread {
|
||||
}))
|
||||
.into_any_element()
|
||||
}
|
||||
RenderedMessageSegment::ToolUseCard(status, card) => {
|
||||
card.render(status, window, workspace.clone(), cx)
|
||||
}
|
||||
RenderedMessageSegment::ToolUseMarkdown(rendered) => v_flex()
|
||||
.child(MarkdownElement::new(
|
||||
rendered.label.clone(),
|
||||
default_markdown_style(window, cx),
|
||||
))
|
||||
.child(MarkdownElement::new(
|
||||
rendered.input.clone(),
|
||||
default_markdown_style(window, cx),
|
||||
))
|
||||
.child(MarkdownElement::new(
|
||||
rendered.output.clone(),
|
||||
default_markdown_style(window, cx),
|
||||
))
|
||||
.into_any(), // todo!()
|
||||
},
|
||||
),
|
||||
)
|
||||
@@ -2647,34 +2535,18 @@ impl ActiveThread {
|
||||
ix: usize,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Stateful<Div> {
|
||||
let colors = cx.theme().colors();
|
||||
div().id(("message-container", ix)).py_1().px_2().child(
|
||||
v_flex()
|
||||
.w_full()
|
||||
.bg(colors.editor_background)
|
||||
.rounded_sm()
|
||||
.child(
|
||||
h_flex()
|
||||
.w_full()
|
||||
.p_2()
|
||||
.gap_2()
|
||||
.child(
|
||||
div().flex_none().child(
|
||||
Icon::new(IconName::Warning)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Warning),
|
||||
),
|
||||
)
|
||||
.child(
|
||||
v_flex()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_size(TextSize::Small.rems(cx))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.children(message_content),
|
||||
),
|
||||
),
|
||||
)
|
||||
let message = div()
|
||||
.flex_1()
|
||||
.min_w_0()
|
||||
.text_size(TextSize::XSmall.rems(cx))
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.children(message_content);
|
||||
|
||||
div()
|
||||
.id(("message-container", ix))
|
||||
.py_1()
|
||||
.px_2p5()
|
||||
.child(Banner::new().severity(ui::Severity::Warning).child(message))
|
||||
}
|
||||
|
||||
fn render_message_thinking_segment(
|
||||
@@ -2894,7 +2766,7 @@ impl ActiveThread {
|
||||
workspace: WeakEntity<Workspace>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement + use<> {
|
||||
if let Some(card) = self.agent.read(cx).card_for_tool(&tool_use.id) {
|
||||
if let Some(card) = self.thread.read(cx).card_for_tool(&tool_use.id) {
|
||||
return card.render(&tool_use.status, window, workspace, cx);
|
||||
}
|
||||
|
||||
@@ -3375,7 +3247,7 @@ impl ActiveThread {
|
||||
}
|
||||
|
||||
fn render_rules_item(&self, cx: &Context<Self>) -> AnyElement {
|
||||
let project_context = self.agent.read(cx).project_context();
|
||||
let project_context = self.thread.read(cx).project_context();
|
||||
let project_context = project_context.borrow();
|
||||
let Some(project_context) = project_context.as_ref() else {
|
||||
return div().into_any();
|
||||
@@ -3499,12 +3371,12 @@ impl ActiveThread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Some(PendingToolUseStatus::NeedsConfirmation(c)) = self
|
||||
.agent
|
||||
.thread
|
||||
.read(cx)
|
||||
.pending_tool(&tool_use_id)
|
||||
.map(|tool_use| tool_use.status.clone())
|
||||
{
|
||||
self.agent.update(cx, |thread, cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
if let Some(configured) = thread.get_or_init_configured_model(cx) {
|
||||
thread.run_tool(
|
||||
c.tool_use_id.clone(),
|
||||
@@ -3530,13 +3402,13 @@ impl ActiveThread {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let window_handle = window.window_handle();
|
||||
self.agent.update(cx, |thread, cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.deny_tool_use(tool_use_id, tool_name, Some(window_handle), cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_open_rules(&mut self, _: &ClickEvent, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let project_context = self.agent.read(cx).project_context();
|
||||
let project_context = self.thread.read(cx).project_context();
|
||||
let project_context = project_context.borrow();
|
||||
let Some(project_context) = project_context.as_ref() else {
|
||||
return;
|
||||
@@ -3698,7 +3570,7 @@ impl Render for ActiveThread {
|
||||
}
|
||||
|
||||
pub(crate) fn open_active_thread_as_markdown(
|
||||
agent: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
workspace: Entity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -3713,7 +3585,7 @@ pub(crate) fn open_active_thread_as_markdown(
|
||||
let markdown_language = markdown_language_task.await?;
|
||||
|
||||
workspace.update_in(cx, |workspace, window, cx| {
|
||||
let thread = agent.read(cx);
|
||||
let thread = thread.read(cx);
|
||||
let markdown = thread.to_markdown(cx)?;
|
||||
let thread_summary = thread.summary().or_default().to_string();
|
||||
|
||||
@@ -3802,7 +3674,7 @@ pub(crate) fn open_context(
|
||||
AgentContextHandle::Thread(thread_context) => workspace.update(cx, |workspace, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
panel.update(cx, |panel, cx| {
|
||||
panel.open_thread(thread_context.agent.clone(), window, cx);
|
||||
panel.open_thread(thread_context.thread.clone(), window, cx);
|
||||
});
|
||||
}
|
||||
}),
|
||||
@@ -3889,9 +3761,7 @@ fn open_editor_at_position(
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use agent::{
|
||||
MessageSegment, context::ContextLoadResult, thread::UserMessageParams, thread_store,
|
||||
};
|
||||
use agent::{MessageSegment, context::ContextLoadResult, thread_store};
|
||||
use assistant_tool::{ToolRegistry, ToolWorkingSet};
|
||||
use editor::EditorSettings;
|
||||
use fs::FakeFs;
|
||||
@@ -3906,7 +3776,6 @@ mod tests {
|
||||
use settings::SettingsStore;
|
||||
use util::path;
|
||||
use workspace::CollaboratorId;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_agent_is_unfollowed_after_cancelling_completion(cx: &mut TestAppContext) {
|
||||
@@ -3923,12 +3792,13 @@ mod tests {
|
||||
|
||||
// Insert user message without any context (empty context vector)
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.send_message(
|
||||
thread.insert_user_message(
|
||||
"What is the best way to learn Rust?",
|
||||
model.clone(),
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
)
|
||||
);
|
||||
});
|
||||
|
||||
// Stream response to user message
|
||||
@@ -3969,7 +3839,7 @@ mod tests {
|
||||
registry.set_default_model(
|
||||
Some(ConfiguredModel {
|
||||
provider: Arc::new(FakeLanguageModelProvider),
|
||||
model: model.clone(),
|
||||
model,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
@@ -3983,19 +3853,15 @@ mod tests {
|
||||
context: None,
|
||||
}];
|
||||
|
||||
let message = thread.update(cx, |agent, cx| {
|
||||
let message_id = agent.send_message(
|
||||
UserMessageParams {
|
||||
text: "Tell me about @foo.txt".to_string(),
|
||||
creases,
|
||||
checkpoint: None,
|
||||
context: ContextLoadResult::default(),
|
||||
},
|
||||
model.clone(),
|
||||
let message = thread.update(cx, |thread, cx| {
|
||||
let message_id = thread.insert_user_message(
|
||||
"Tell me about @foo.txt",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
creases,
|
||||
cx,
|
||||
);
|
||||
agent.message(message_id).cloned().unwrap()
|
||||
thread.message(message_id).cloned().unwrap()
|
||||
});
|
||||
|
||||
active_thread.update_in(cx, |active_thread, window, cx| {
|
||||
@@ -4087,8 +3953,20 @@ mod tests {
|
||||
|
||||
// Insert a user message and start streaming a response
|
||||
let message = thread.update(cx, |thread, cx| {
|
||||
let message_id =
|
||||
thread.send_message("Hello, how are you?", model.clone(), cx.active_window(), cx);
|
||||
let message_id = thread.insert_user_message(
|
||||
"Hello, how are you?",
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
vec![],
|
||||
cx,
|
||||
);
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(
|
||||
model.clone(),
|
||||
CompletionIntent::UserPrompt,
|
||||
cx.active_window(),
|
||||
cx,
|
||||
);
|
||||
thread.message(message_id).cloned().unwrap()
|
||||
});
|
||||
|
||||
@@ -4175,7 +4053,7 @@ mod tests {
|
||||
&mut VisualTestContext,
|
||||
Entity<ActiveThread>,
|
||||
Entity<Workspace>,
|
||||
Entity<ZedAgentThread>,
|
||||
Entity<Thread>,
|
||||
Arc<dyn LanguageModel>,
|
||||
) {
|
||||
let (workspace, cx) =
|
||||
|
||||
@@ -16,7 +16,9 @@ use gpui::{
|
||||
Focusable, ScrollHandle, Subscription, Task, Transformation, WeakEntity, percentage,
|
||||
};
|
||||
use language::LanguageRegistry;
|
||||
use language_model::{LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry};
|
||||
use language_model::{
|
||||
LanguageModelProvider, LanguageModelProviderId, LanguageModelRegistry, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
use notifications::status_toast::{StatusToast, ToastIcon};
|
||||
use project::{
|
||||
context_server_store::{ContextServerConfiguration, ContextServerStatus, ContextServerStore},
|
||||
@@ -86,6 +88,14 @@ impl AgentConfiguration {
|
||||
let scroll_handle = ScrollHandle::new();
|
||||
let scrollbar_state = ScrollbarState::new(scroll_handle.clone());
|
||||
|
||||
let mut expanded_provider_configurations = HashMap::default();
|
||||
if LanguageModelRegistry::read_global(cx)
|
||||
.provider(&ZED_CLOUD_PROVIDER_ID)
|
||||
.map_or(false, |cloud_provider| cloud_provider.must_accept_terms(cx))
|
||||
{
|
||||
expanded_provider_configurations.insert(ZED_CLOUD_PROVIDER_ID, true);
|
||||
}
|
||||
|
||||
let mut this = Self {
|
||||
fs,
|
||||
language_registry,
|
||||
@@ -94,7 +104,7 @@ impl AgentConfiguration {
|
||||
configuration_views_by_provider: HashMap::default(),
|
||||
context_server_store,
|
||||
expanded_context_server_tools: HashMap::default(),
|
||||
expanded_provider_configurations: HashMap::default(),
|
||||
expanded_provider_configurations,
|
||||
tools,
|
||||
_registry_subscription: registry_subscription,
|
||||
scroll_handle,
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
|
||||
use agent::{ThreadEvent, ZedAgentThread};
|
||||
use agent::{Thread, ThreadEvent};
|
||||
use agent_settings::AgentSettings;
|
||||
use anyhow::Result;
|
||||
use assistant_tool::ActionLog;
|
||||
use buffer_diff::DiffHunkStatus;
|
||||
use collections::{HashMap, HashSet};
|
||||
use editor::{
|
||||
@@ -42,8 +41,7 @@ use zed_actions::assistant::ToggleFocus;
|
||||
pub struct AgentDiffPane {
|
||||
multibuffer: Entity<MultiBuffer>,
|
||||
editor: Entity<Editor>,
|
||||
agent: Entity<ZedAgentThread>,
|
||||
action_log: Entity<ActionLog>,
|
||||
thread: Entity<Thread>,
|
||||
focus_handle: FocusHandle,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
title: SharedString,
|
||||
@@ -52,71 +50,70 @@ pub struct AgentDiffPane {
|
||||
|
||||
impl AgentDiffPane {
|
||||
pub fn deploy(
|
||||
agent: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> Result<Entity<Self>> {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
Self::deploy_in_workspace(agent, workspace, window, cx)
|
||||
Self::deploy_in_workspace(thread, workspace, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
pub fn deploy_in_workspace(
|
||||
agent: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
workspace: &mut Workspace,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Workspace>,
|
||||
) -> Entity<Self> {
|
||||
let existing_diff = workspace
|
||||
.items_of_type::<AgentDiffPane>(cx)
|
||||
.find(|diff| diff.read(cx).agent == agent);
|
||||
.find(|diff| diff.read(cx).thread == thread);
|
||||
if let Some(existing_diff) = existing_diff {
|
||||
workspace.activate_item(&existing_diff, true, true, window, cx);
|
||||
existing_diff
|
||||
} else {
|
||||
let agent_diff =
|
||||
cx.new(|cx| AgentDiffPane::new(agent.clone(), workspace.weak_handle(), window, cx));
|
||||
let agent_diff = cx
|
||||
.new(|cx| AgentDiffPane::new(thread.clone(), workspace.weak_handle(), window, cx));
|
||||
workspace.add_item_to_center(Box::new(agent_diff.clone()), window, cx);
|
||||
agent_diff
|
||||
}
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
agent: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let focus_handle = cx.focus_handle();
|
||||
let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
|
||||
let action_log = agent.read(cx).action_log();
|
||||
let project = agent.read(cx).project().clone();
|
||||
|
||||
let project = thread.read(cx).project().clone();
|
||||
let editor = cx.new(|cx| {
|
||||
let mut editor =
|
||||
Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
|
||||
editor.disable_inline_diagnostics();
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.set_render_diff_hunk_controls(diff_hunk_controls(&action_log), cx);
|
||||
editor.set_render_diff_hunk_controls(diff_hunk_controls(&thread), cx);
|
||||
editor.register_addon(AgentDiffAddon);
|
||||
editor
|
||||
});
|
||||
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
let mut this = Self {
|
||||
_subscriptions: vec![
|
||||
cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
|
||||
this.update_excerpts(window, cx)
|
||||
}),
|
||||
cx.subscribe(&agent, |this, _thread, event, cx| {
|
||||
cx.subscribe(&thread, |this, _thread, event, cx| {
|
||||
this.handle_thread_event(event, cx)
|
||||
}),
|
||||
],
|
||||
title: SharedString::default(),
|
||||
action_log,
|
||||
multibuffer,
|
||||
editor,
|
||||
agent,
|
||||
thread,
|
||||
focus_handle,
|
||||
workspace,
|
||||
};
|
||||
@@ -126,8 +123,8 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let agent = self.agent.read(cx);
|
||||
let changed_buffers = agent.action_log().read(cx).changed_buffers(cx);
|
||||
let thread = self.thread.read(cx);
|
||||
let changed_buffers = thread.action_log().read(cx).changed_buffers(cx);
|
||||
let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
|
||||
|
||||
for (buffer, diff_handle) in changed_buffers {
|
||||
@@ -214,7 +211,7 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn update_title(&mut self, cx: &mut Context<Self>) {
|
||||
let new_title = self.agent.read(cx).summary().unwrap_or("Agent Changes");
|
||||
let new_title = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
if new_title != self.title {
|
||||
self.title = new_title;
|
||||
cx.emit(EditorEvent::TitleChanged);
|
||||
@@ -251,14 +248,14 @@ impl AgentDiffPane {
|
||||
fn keep(&mut self, _: &Keep, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
keep_edits_in_selection(editor, &snapshot, &self.action_log, window, cx);
|
||||
keep_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn reject(&mut self, _: &Reject, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
reject_edits_in_selection(editor, &snapshot, &self.action_log, window, cx);
|
||||
reject_edits_in_selection(editor, &snapshot, &self.thread, window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -268,7 +265,7 @@ impl AgentDiffPane {
|
||||
reject_edits_in_ranges(
|
||||
editor,
|
||||
&snapshot,
|
||||
&self.action_log,
|
||||
&self.thread,
|
||||
vec![editor::Anchor::min()..editor::Anchor::max()],
|
||||
window,
|
||||
cx,
|
||||
@@ -277,15 +274,15 @@ impl AgentDiffPane {
|
||||
}
|
||||
|
||||
fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.action_log
|
||||
.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
|
||||
self.thread
|
||||
.update(cx, |thread, cx| thread.keep_all_edits(cx));
|
||||
}
|
||||
}
|
||||
|
||||
fn keep_edits_in_selection(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
action_log: &Entity<ActionLog>,
|
||||
thread: &Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -294,13 +291,13 @@ fn keep_edits_in_selection(
|
||||
.disjoint_anchor_ranges()
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
keep_edits_in_ranges(editor, buffer_snapshot, &action_log, ranges, window, cx)
|
||||
keep_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx)
|
||||
}
|
||||
|
||||
fn reject_edits_in_selection(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
action_log: &Entity<ActionLog>,
|
||||
thread: &Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
) {
|
||||
@@ -308,13 +305,13 @@ fn reject_edits_in_selection(
|
||||
.selections
|
||||
.disjoint_anchor_ranges()
|
||||
.collect::<Vec<_>>();
|
||||
reject_edits_in_ranges(editor, buffer_snapshot, &action_log, ranges, window, cx)
|
||||
reject_edits_in_ranges(editor, buffer_snapshot, &thread, ranges, window, cx)
|
||||
}
|
||||
|
||||
fn keep_edits_in_ranges(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
action_log: &Entity<ActionLog>,
|
||||
thread: &Entity<Thread>,
|
||||
ranges: Vec<Range<editor::Anchor>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
@@ -329,8 +326,8 @@ fn keep_edits_in_ranges(
|
||||
for hunk in &diff_hunks_in_ranges {
|
||||
let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
|
||||
if let Some(buffer) = buffer {
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -339,7 +336,7 @@ fn keep_edits_in_ranges(
|
||||
fn reject_edits_in_ranges(
|
||||
editor: &mut Editor,
|
||||
buffer_snapshot: &MultiBufferSnapshot,
|
||||
action_log: &Entity<ActionLog>,
|
||||
thread: &Entity<Thread>,
|
||||
ranges: Vec<Range<editor::Anchor>>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Editor>,
|
||||
@@ -364,9 +361,9 @@ fn reject_edits_in_ranges(
|
||||
}
|
||||
|
||||
for (buffer, ranges) in ranges_by_buffer {
|
||||
action_log
|
||||
.update(cx, |action_log, cx| {
|
||||
action_log.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.reject_edits_in_ranges(buffer, ranges, cx)
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
@@ -464,7 +461,7 @@ impl Item for AgentDiffPane {
|
||||
}
|
||||
|
||||
fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
|
||||
let summary = self.agent.read(cx).summary().or_default();
|
||||
let summary = self.thread.read(cx).summary().unwrap_or("Agent Changes");
|
||||
Label::new(format!("Review: {}", summary))
|
||||
.color(if params.selected {
|
||||
Color::Default
|
||||
@@ -514,7 +511,7 @@ impl Item for AgentDiffPane {
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
Some(cx.new(|cx| Self::new(self.agent.clone(), self.workspace.clone(), window, cx)))
|
||||
Some(cx.new(|cx| Self::new(self.thread.clone(), self.workspace.clone(), window, cx)))
|
||||
}
|
||||
|
||||
fn is_dirty(&self, cx: &App) -> bool {
|
||||
@@ -644,8 +641,8 @@ impl Render for AgentDiffPane {
|
||||
}
|
||||
}
|
||||
|
||||
fn diff_hunk_controls(action_log: &Entity<ActionLog>) -> editor::RenderDiffHunkControlsFn {
|
||||
let action_log = action_log.clone();
|
||||
fn diff_hunk_controls(thread: &Entity<Thread>) -> editor::RenderDiffHunkControlsFn {
|
||||
let thread = thread.clone();
|
||||
|
||||
Arc::new(
|
||||
move |row,
|
||||
@@ -663,7 +660,7 @@ fn diff_hunk_controls(action_log: &Entity<ActionLog>) -> editor::RenderDiffHunkC
|
||||
hunk_range,
|
||||
is_created_file,
|
||||
line_height,
|
||||
&action_log,
|
||||
&thread,
|
||||
editor,
|
||||
window,
|
||||
cx,
|
||||
@@ -679,7 +676,7 @@ fn render_diff_hunk_controls(
|
||||
hunk_range: Range<editor::Anchor>,
|
||||
is_created_file: bool,
|
||||
line_height: Pixels,
|
||||
action_log: &Entity<ActionLog>,
|
||||
thread: &Entity<Thread>,
|
||||
editor: &Entity<Editor>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
@@ -714,14 +711,14 @@ fn render_diff_hunk_controls(
|
||||
)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let action_log = action_log.clone();
|
||||
let thread = thread.clone();
|
||||
move |_event, window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
reject_edits_in_ranges(
|
||||
editor,
|
||||
&snapshot,
|
||||
&action_log,
|
||||
&thread,
|
||||
vec![hunk_range.start..hunk_range.start],
|
||||
window,
|
||||
cx,
|
||||
@@ -736,14 +733,14 @@ fn render_diff_hunk_controls(
|
||||
)
|
||||
.on_click({
|
||||
let editor = editor.clone();
|
||||
let action_log = action_log.clone();
|
||||
let thread = thread.clone();
|
||||
move |_event, window, cx| {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
keep_edits_in_ranges(
|
||||
editor,
|
||||
&snapshot,
|
||||
&action_log,
|
||||
&thread,
|
||||
vec![hunk_range.start..hunk_range.start],
|
||||
window,
|
||||
cx,
|
||||
@@ -1117,7 +1114,7 @@ impl Render for AgentDiffToolbar {
|
||||
|
||||
let has_pending_edit_tool_use = agent_diff
|
||||
.read(cx)
|
||||
.agent
|
||||
.thread
|
||||
.read(cx)
|
||||
.has_pending_edit_tool_uses();
|
||||
|
||||
@@ -1190,7 +1187,7 @@ pub enum EditorState {
|
||||
}
|
||||
|
||||
struct WorkspaceThread {
|
||||
agent: WeakEntity<ZedAgentThread>,
|
||||
thread: WeakEntity<Thread>,
|
||||
_thread_subscriptions: [Subscription; 2],
|
||||
singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
|
||||
_settings_subscription: Subscription,
|
||||
@@ -1215,7 +1212,7 @@ impl AgentDiff {
|
||||
|
||||
pub fn set_active_thread(
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
thread: &Entity<ZedAgentThread>,
|
||||
thread: &Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
@@ -1227,11 +1224,11 @@ impl AgentDiff {
|
||||
fn register_active_thread_impl(
|
||||
&mut self,
|
||||
workspace: &WeakEntity<Workspace>,
|
||||
agent: &Entity<ZedAgentThread>,
|
||||
thread: &Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let action_log = agent.read(cx).action_log().clone();
|
||||
let action_log = thread.read(cx).action_log().clone();
|
||||
|
||||
let action_log_subscription = cx.observe_in(&action_log, window, {
|
||||
let workspace = workspace.clone();
|
||||
@@ -1240,7 +1237,7 @@ impl AgentDiff {
|
||||
}
|
||||
});
|
||||
|
||||
let thread_subscription = cx.subscribe_in(&agent, window, {
|
||||
let thread_subscription = cx.subscribe_in(&thread, window, {
|
||||
let workspace = workspace.clone();
|
||||
move |this, _thread, event, window, cx| {
|
||||
this.handle_thread_event(&workspace, event, window, cx)
|
||||
@@ -1249,7 +1246,7 @@ impl AgentDiff {
|
||||
|
||||
if let Some(workspace_thread) = self.workspace_threads.get_mut(&workspace) {
|
||||
// replace thread and action log subscription, but keep editors
|
||||
workspace_thread.agent = agent.downgrade();
|
||||
workspace_thread.thread = thread.downgrade();
|
||||
workspace_thread._thread_subscriptions = [action_log_subscription, thread_subscription];
|
||||
self.update_reviewing_editors(&workspace, window, cx);
|
||||
return;
|
||||
@@ -1274,7 +1271,7 @@ impl AgentDiff {
|
||||
self.workspace_threads.insert(
|
||||
workspace.clone(),
|
||||
WorkspaceThread {
|
||||
agent: agent.downgrade(),
|
||||
thread: thread.downgrade(),
|
||||
_thread_subscriptions: [action_log_subscription, thread_subscription],
|
||||
singleton_editors: HashMap::default(),
|
||||
_settings_subscription: settings_subscription,
|
||||
@@ -1322,7 +1319,7 @@ impl AgentDiff {
|
||||
|
||||
fn register_review_action<T: Action>(
|
||||
workspace: &mut Workspace,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<ZedAgentThread>, &mut Window, &mut App) -> PostReviewState
|
||||
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState
|
||||
+ 'static,
|
||||
this: &Entity<AgentDiff>,
|
||||
) {
|
||||
@@ -1365,7 +1362,6 @@ impl AgentDiff {
|
||||
| ThreadEvent::StreamedAssistantText(_, _)
|
||||
| ThreadEvent::StreamedAssistantThinking(_, _)
|
||||
| ThreadEvent::StreamedToolUse { .. }
|
||||
| ThreadEvent::StreamedToolUse2 { .. }
|
||||
| ThreadEvent::InvalidToolInput { .. }
|
||||
| ThreadEvent::MissingToolUse { .. }
|
||||
| ThreadEvent::MessageAdded(_)
|
||||
@@ -1485,11 +1481,11 @@ impl AgentDiff {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(agent) = workspace_thread.agent.upgrade() else {
|
||||
let Some(thread) = workspace_thread.thread.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let action_log = agent.read(cx).action_log();
|
||||
let action_log = thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
let mut unaffected = self.reviewing_editors.clone();
|
||||
@@ -1514,7 +1510,7 @@ impl AgentDiff {
|
||||
multibuffer.add_diff(diff_handle.clone(), cx);
|
||||
});
|
||||
|
||||
let new_state = if agent.read(cx).is_generating() {
|
||||
let new_state = if thread.read(cx).is_generating() {
|
||||
EditorState::Generating
|
||||
} else {
|
||||
EditorState::Reviewing
|
||||
@@ -1527,7 +1523,7 @@ impl AgentDiff {
|
||||
if previous_state.is_none() {
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.start_temporary_diff_override();
|
||||
editor.set_render_diff_hunk_controls(diff_hunk_controls(&action_log), cx);
|
||||
editor.set_render_diff_hunk_controls(diff_hunk_controls(&thread), cx);
|
||||
editor.set_expand_all_diff_hunks(cx);
|
||||
editor.register_addon(EditorAgentDiffAddon);
|
||||
});
|
||||
@@ -1595,22 +1591,22 @@ impl AgentDiff {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(WorkspaceThread { agent, .. }) =
|
||||
let Some(WorkspaceThread { thread, .. }) =
|
||||
self.workspace_threads.get(&workspace.downgrade())
|
||||
else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some(agent) = agent.upgrade() else {
|
||||
let Some(thread) = thread.upgrade() else {
|
||||
return;
|
||||
};
|
||||
|
||||
AgentDiffPane::deploy(agent, workspace.downgrade(), window, cx).log_err();
|
||||
AgentDiffPane::deploy(thread, workspace.downgrade(), window, cx).log_err();
|
||||
}
|
||||
|
||||
fn keep_all(
|
||||
editor: &Entity<Editor>,
|
||||
agent: &Entity<ZedAgentThread>,
|
||||
thread: &Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1619,7 +1615,7 @@ impl AgentDiff {
|
||||
keep_edits_in_ranges(
|
||||
editor,
|
||||
&snapshot,
|
||||
&agent.read(cx).action_log(),
|
||||
thread,
|
||||
vec![editor::Anchor::min()..editor::Anchor::max()],
|
||||
window,
|
||||
cx,
|
||||
@@ -1630,7 +1626,7 @@ impl AgentDiff {
|
||||
|
||||
fn reject_all(
|
||||
editor: &Entity<Editor>,
|
||||
thread: &Entity<ZedAgentThread>,
|
||||
thread: &Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
@@ -1639,7 +1635,7 @@ impl AgentDiff {
|
||||
reject_edits_in_ranges(
|
||||
editor,
|
||||
&snapshot,
|
||||
&thread.read(cx).action_log(),
|
||||
thread,
|
||||
vec![editor::Anchor::min()..editor::Anchor::max()],
|
||||
window,
|
||||
cx,
|
||||
@@ -1650,26 +1646,26 @@ impl AgentDiff {
|
||||
|
||||
fn keep(
|
||||
editor: &Entity<Editor>,
|
||||
agent: &Entity<ZedAgentThread>,
|
||||
thread: &Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
keep_edits_in_selection(editor, &snapshot, &agent.read(cx).action_log(), window, cx);
|
||||
keep_edits_in_selection(editor, &snapshot, thread, window, cx);
|
||||
Self::post_review_state(&snapshot)
|
||||
})
|
||||
}
|
||||
|
||||
fn reject(
|
||||
editor: &Entity<Editor>,
|
||||
agent: &Entity<ZedAgentThread>,
|
||||
thread: &Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> PostReviewState {
|
||||
editor.update(cx, |editor, cx| {
|
||||
let snapshot = editor.buffer().read(cx).snapshot(cx);
|
||||
reject_edits_in_selection(editor, &snapshot, &agent.read(cx).action_log(), window, cx);
|
||||
reject_edits_in_selection(editor, &snapshot, thread, window, cx);
|
||||
Self::post_review_state(&snapshot)
|
||||
})
|
||||
}
|
||||
@@ -1686,7 +1682,7 @@ impl AgentDiff {
|
||||
fn review_in_active_editor(
|
||||
&mut self,
|
||||
workspace: &mut Workspace,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<ZedAgentThread>, &mut Window, &mut App) -> PostReviewState,
|
||||
review: impl Fn(&Entity<Editor>, &Entity<Thread>, &mut Window, &mut App) -> PostReviewState,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Option<Task<Result<()>>> {
|
||||
@@ -1700,13 +1696,14 @@ impl AgentDiff {
|
||||
return None;
|
||||
}
|
||||
|
||||
let WorkspaceThread { agent, .. } = self.workspace_threads.get(&workspace.weak_handle())?;
|
||||
let WorkspaceThread { thread, .. } =
|
||||
self.workspace_threads.get(&workspace.weak_handle())?;
|
||||
|
||||
let agent = agent.upgrade()?;
|
||||
let thread = thread.upgrade()?;
|
||||
|
||||
if let PostReviewState::AllReviewed = review(&editor, &agent, window, cx) {
|
||||
if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx) {
|
||||
if let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton() {
|
||||
let changed_buffers = agent.read(cx).action_log().read(cx).changed_buffers(cx);
|
||||
let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
|
||||
|
||||
let mut keys = changed_buffers.keys().cycle();
|
||||
keys.find(|k| *k == &curr_buffer);
|
||||
@@ -1804,13 +1801,13 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let agent = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = agent.read_with(cx, |agent, _| agent.action_log().clone());
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let agent_diff = cx.new_window_entity(|window, cx| {
|
||||
AgentDiffPane::new(agent.clone(), workspace.downgrade(), window, cx)
|
||||
AgentDiffPane::new(thread.clone(), workspace.downgrade(), window, cx)
|
||||
});
|
||||
let editor = agent_diff.read_with(cx, |diff, _cx| diff.editor.clone());
|
||||
|
||||
@@ -1898,7 +1895,7 @@ mod tests {
|
||||
keep_edits_in_ranges(
|
||||
editor,
|
||||
&snapshot,
|
||||
&agent.read(cx).action_log(),
|
||||
&thread,
|
||||
vec![position..position],
|
||||
window,
|
||||
cx,
|
||||
@@ -1969,8 +1966,8 @@ mod tests {
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
let agent = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = agent.read_with(cx, |agent, _| agent.action_log().clone());
|
||||
let thread = thread_store.update(cx, |store, cx| store.create_thread(cx));
|
||||
let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
|
||||
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
@@ -1992,7 +1989,7 @@ mod tests {
|
||||
|
||||
// Set the active thread
|
||||
cx.update(|window, cx| {
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), &agent, window, cx)
|
||||
AgentDiff::set_active_thread(&workspace.downgrade(), &thread, window, cx)
|
||||
});
|
||||
|
||||
let buffer1 = project
|
||||
@@ -2149,7 +2146,7 @@ mod tests {
|
||||
keep_edits_in_ranges(
|
||||
editor,
|
||||
&snapshot,
|
||||
&agent.read(cx).action_log(),
|
||||
&thread,
|
||||
vec![position..position],
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -7,6 +7,7 @@ use std::time::Duration;
|
||||
use db::kvp::{Dismissable, KEY_VALUE_STORE};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::NewGeminiThread;
|
||||
use crate::language_model_selector::ToggleModelSelector;
|
||||
use crate::{
|
||||
AddContextServer, AgentDiffPane, ContinueThread, ContinueWithBurnMode,
|
||||
@@ -26,7 +27,7 @@ use crate::{
|
||||
ui::AgentOnboardingModal,
|
||||
};
|
||||
use agent::{
|
||||
ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio, ZedAgentThread,
|
||||
Thread, ThreadError, ThreadEvent, ThreadId, ThreadSummary, TokenUsageRatio,
|
||||
context_store::ContextStore,
|
||||
history_store::{HistoryEntryId, HistoryStore},
|
||||
thread_store::{TextThreadStore, ThreadStore},
|
||||
@@ -41,7 +42,7 @@ use editor::{Anchor, AnchorRangeExt as _, Editor, EditorEvent, MultiBuffer};
|
||||
use fs::Fs;
|
||||
use gpui::{
|
||||
Action, Animation, AnimationExt as _, AnyElement, App, AsyncWindowContext, ClipboardItem,
|
||||
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, FontWeight,
|
||||
Corner, DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, Hsla,
|
||||
KeyContext, Pixels, Subscription, Task, UpdateGlobal, WeakEntity, linear_color_stop,
|
||||
linear_gradient, prelude::*, pulsating_between,
|
||||
};
|
||||
@@ -59,7 +60,7 @@ use theme::ThemeSettings;
|
||||
use time::UtcOffset;
|
||||
use ui::utils::WithRemSize;
|
||||
use ui::{
|
||||
Banner, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
Banner, Callout, CheckboxWithLabel, ContextMenu, ElevationIndex, KeyBinding, PopoverMenu,
|
||||
PopoverMenuHandle, ProgressBar, Tab, Tooltip, Vector, VectorName, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
@@ -72,7 +73,7 @@ use zed_actions::{
|
||||
agent::{OpenConfiguration, OpenOnboardingModal, ResetOnboarding},
|
||||
assistant::{OpenRulesLibrary, ToggleFocus},
|
||||
};
|
||||
use zed_llm_client::UsageLimit;
|
||||
use zed_llm_client::{CompletionIntent, UsageLimit};
|
||||
|
||||
const AGENT_PANEL_KEY: &str = "agent_panel";
|
||||
|
||||
@@ -109,6 +110,12 @@ pub fn init(cx: &mut App) {
|
||||
panel.update(cx, |panel, cx| panel.new_prompt_editor(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, _: &NewGeminiThread, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
panel.update(cx, |panel, cx| panel.new_gemini_thread(window, cx));
|
||||
}
|
||||
})
|
||||
.register_action(|workspace, action: &OpenRulesLibrary, window, cx| {
|
||||
if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
@@ -122,9 +129,10 @@ pub fn init(cx: &mut App) {
|
||||
workspace.focus_panel::<AgentPanel>(window, cx);
|
||||
match &panel.read(cx).active_view {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
let agent = thread.read(cx).agent().clone();
|
||||
AgentDiffPane::deploy_in_workspace(agent, workspace, window, cx);
|
||||
let thread = thread.read(cx).thread().clone();
|
||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx);
|
||||
}
|
||||
ActiveView::AcpThread { .. } => todo!(),
|
||||
ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
@@ -188,6 +196,9 @@ enum ActiveView {
|
||||
message_editor: Entity<MessageEditor>,
|
||||
_subscriptions: Vec<gpui::Subscription>,
|
||||
},
|
||||
AcpThread {
|
||||
thread_view: Entity<acp::AcpThreadView>,
|
||||
},
|
||||
TextThread {
|
||||
context_editor: Entity<TextThreadEditor>,
|
||||
title_editor: Entity<Editor>,
|
||||
@@ -207,7 +218,9 @@ enum WhichFontSize {
|
||||
impl ActiveView {
|
||||
pub fn which_font_size_used(&self) -> WhichFontSize {
|
||||
match self {
|
||||
ActiveView::Thread { .. } | ActiveView::History => WhichFontSize::AgentFont,
|
||||
ActiveView::Thread { .. } | ActiveView::AcpThread { .. } | ActiveView::History => {
|
||||
WhichFontSize::AgentFont
|
||||
}
|
||||
ActiveView::TextThread { .. } => WhichFontSize::BufferFont,
|
||||
ActiveView::Configuration => WhichFontSize::None,
|
||||
}
|
||||
@@ -238,6 +251,9 @@ impl ActiveView {
|
||||
thread.scroll_to_bottom(cx);
|
||||
});
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
}
|
||||
ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
@@ -251,9 +267,9 @@ impl ActiveView {
|
||||
let new_summary = editor.read(cx).text(cx);
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.agent().update(cx, |agent, cx| {
|
||||
agent.set_summary(new_summary, cx);
|
||||
})
|
||||
thread.thread().update(cx, |thread, cx| {
|
||||
thread.set_summary(new_summary, cx);
|
||||
});
|
||||
})
|
||||
}
|
||||
EditorEvent::Blurred => {
|
||||
@@ -274,11 +290,11 @@ impl ActiveView {
|
||||
cx.notify();
|
||||
}
|
||||
}),
|
||||
cx.subscribe_in(&active_thread.read(cx).agent().clone(), window, {
|
||||
cx.subscribe_in(&active_thread.read(cx).thread().clone(), window, {
|
||||
let editor = editor.clone();
|
||||
move |_, agent, event, window, cx| match event {
|
||||
move |_, thread, event, window, cx| match event {
|
||||
ThreadEvent::SummaryGenerated => {
|
||||
let summary = agent.read(cx).summary().or_default();
|
||||
let summary = thread.read(cx).summary().or_default();
|
||||
|
||||
editor.update(cx, |editor, cx| {
|
||||
editor.set_text(summary, window, cx);
|
||||
@@ -524,7 +540,7 @@ impl AgentPanel {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let agent = thread_store.update(cx, |this, cx| this.create_thread(cx));
|
||||
let thread = thread_store.update(cx, |this, cx| this.create_thread(cx));
|
||||
let fs = workspace.app_state().fs.clone();
|
||||
let user_store = workspace.app_state().user_store.clone();
|
||||
let project = workspace.project();
|
||||
@@ -546,13 +562,13 @@ impl AgentPanel {
|
||||
prompt_store.clone(),
|
||||
thread_store.downgrade(),
|
||||
context_store.downgrade(),
|
||||
agent.clone(),
|
||||
thread.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let thread_id = agent.read(cx).id().clone();
|
||||
let thread_id = thread.read(cx).id().clone();
|
||||
let history_store = cx.new(|cx| {
|
||||
HistoryStore::new(
|
||||
thread_store.clone(),
|
||||
@@ -566,7 +582,7 @@ impl AgentPanel {
|
||||
|
||||
let active_thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
agent.clone(),
|
||||
thread.clone(),
|
||||
thread_store.clone(),
|
||||
context_store.clone(),
|
||||
message_editor_context_store.clone(),
|
||||
@@ -607,7 +623,7 @@ impl AgentPanel {
|
||||
}
|
||||
};
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, &agent, window, cx);
|
||||
AgentDiff::set_active_thread(&workspace, &thread, window, cx);
|
||||
|
||||
let weak_panel = weak_self.clone();
|
||||
|
||||
@@ -649,9 +665,12 @@ impl AgentPanel {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
thread
|
||||
.read(cx)
|
||||
.agent()
|
||||
.thread()
|
||||
.clone()
|
||||
.update(cx, |agent, cx| agent.get_or_init_configured_model(cx));
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx));
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
}
|
||||
ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
@@ -733,6 +752,9 @@ impl AgentPanel {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
thread.update(cx, |thread, cx| thread.cancel_last_completion(window, cx));
|
||||
}
|
||||
ActiveView::AcpThread { thread_view, .. } => {
|
||||
thread_view.update(cx, |thread_element, _cx| thread_element.cancel());
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
}
|
||||
@@ -740,6 +762,10 @@ impl AgentPanel {
|
||||
fn active_message_editor(&self) -> Option<&Entity<MessageEditor>> {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { message_editor, .. } => Some(message_editor),
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
None
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
|
||||
}
|
||||
}
|
||||
@@ -753,7 +779,7 @@ impl AgentPanel {
|
||||
None
|
||||
};
|
||||
|
||||
let agent = self
|
||||
let thread = self
|
||||
.thread_store
|
||||
.update(cx, |this, cx| this.create_thread(cx));
|
||||
|
||||
@@ -786,7 +812,7 @@ impl AgentPanel {
|
||||
|
||||
let active_thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
agent.clone(),
|
||||
thread.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.context_store.clone(),
|
||||
context_store.clone(),
|
||||
@@ -806,7 +832,7 @@ impl AgentPanel {
|
||||
self.prompt_store.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
self.context_store.downgrade(),
|
||||
agent.clone(),
|
||||
thread.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -823,7 +849,7 @@ impl AgentPanel {
|
||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
|
||||
AgentDiff::set_active_thread(&self.workspace, &agent, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
||||
}
|
||||
|
||||
fn new_prompt_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
@@ -862,6 +888,19 @@ impl AgentPanel {
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
|
||||
fn new_gemini_thread(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let project = self.project.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let thread_view =
|
||||
cx.new_window_entity(|window, cx| acp::AcpThreadView::new(project, window, cx))?;
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.set_active_view(ActiveView::AcpThread { thread_view }, window, cx);
|
||||
})
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn deploy_rules_library(
|
||||
&mut self,
|
||||
action: &OpenRulesLibrary,
|
||||
@@ -971,7 +1010,7 @@ impl AgentPanel {
|
||||
|
||||
pub(crate) fn open_thread(
|
||||
&mut self,
|
||||
agent: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -984,7 +1023,7 @@ impl AgentPanel {
|
||||
|
||||
let active_thread = cx.new(|cx| {
|
||||
ActiveThread::new(
|
||||
agent.clone(),
|
||||
thread.clone(),
|
||||
self.thread_store.clone(),
|
||||
self.context_store.clone(),
|
||||
context_store.clone(),
|
||||
@@ -994,6 +1033,7 @@ impl AgentPanel {
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let message_editor = cx.new(|cx| {
|
||||
MessageEditor::new(
|
||||
self.fs.clone(),
|
||||
@@ -1003,7 +1043,7 @@ impl AgentPanel {
|
||||
self.prompt_store.clone(),
|
||||
self.thread_store.downgrade(),
|
||||
self.context_store.downgrade(),
|
||||
agent.clone(),
|
||||
thread.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -1012,12 +1052,13 @@ impl AgentPanel {
|
||||
|
||||
let thread_view = ActiveView::thread(active_thread.clone(), message_editor, window, cx);
|
||||
self.set_active_view(thread_view, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, &agent, window, cx);
|
||||
AgentDiff::set_active_thread(&self.workspace, &thread, window, cx);
|
||||
}
|
||||
|
||||
pub fn go_back(&mut self, _: &workspace::GoBack, window: &mut Window, cx: &mut Context<Self>) {
|
||||
match self.active_view {
|
||||
ActiveView::Configuration | ActiveView::History => {
|
||||
// todo! check go back works correctly
|
||||
if let Some(previous_view) = self.previous_view.take() {
|
||||
self.active_view = previous_view;
|
||||
|
||||
@@ -1025,6 +1066,9 @@ impl AgentPanel {
|
||||
ActiveView::Thread { message_editor, .. } => {
|
||||
message_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
todo!()
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
context_editor.focus_handle(cx).focus(window);
|
||||
}
|
||||
@@ -1137,13 +1181,14 @@ impl AgentPanel {
|
||||
) {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
let agent = thread.read(cx).agent().clone();
|
||||
let thread = thread.read(cx).thread().clone();
|
||||
self.workspace
|
||||
.update(cx, |workspace, cx| {
|
||||
AgentDiffPane::deploy_in_workspace(agent, workspace, window, cx)
|
||||
AgentDiffPane::deploy_in_workspace(thread, workspace, window, cx)
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
ActiveView::AcpThread { .. } => todo!(),
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
}
|
||||
@@ -1190,13 +1235,16 @@ impl AgentPanel {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
active_thread::open_active_thread_as_markdown(
|
||||
thread.read(cx).agent().clone(),
|
||||
thread.read(cx).thread().clone(),
|
||||
workspace,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.detach_and_log_err(cx);
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
todo!()
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {}
|
||||
}
|
||||
}
|
||||
@@ -1228,9 +1276,13 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<ZedAgentThread>> {
|
||||
pub(crate) fn active_thread(&self, cx: &App) -> Option<Entity<Thread>> {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => Some(thread.read(cx).agent().clone()),
|
||||
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
None
|
||||
}
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
@@ -1249,16 +1301,23 @@ impl AgentPanel {
|
||||
return;
|
||||
};
|
||||
|
||||
let agent_state = thread.read(cx).agent().read(cx);
|
||||
if !agent_state.tool_use_limit_reached() {
|
||||
let thread_state = thread.read(cx).thread().read(cx);
|
||||
if !thread_state.tool_use_limit_reached() {
|
||||
return;
|
||||
}
|
||||
|
||||
let model = agent_state.configured_model().map(|cm| cm.model.clone());
|
||||
let model = thread_state.configured_model().map(|cm| cm.model.clone());
|
||||
if let Some(model) = model {
|
||||
thread.update(cx, |active_thread, cx| {
|
||||
active_thread.agent().update(cx, |agent, cx| {
|
||||
agent.send_continue_message(model, Some(window.window_handle()), cx);
|
||||
active_thread.thread().update(cx, |thread, cx| {
|
||||
thread.insert_invisible_continue_message(cx);
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(
|
||||
model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window.window_handle()),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
@@ -1277,10 +1336,10 @@ impl AgentPanel {
|
||||
};
|
||||
|
||||
thread.update(cx, |active_thread, cx| {
|
||||
active_thread.agent().update(cx, |agent, _cx| {
|
||||
let current_mode = agent.completion_mode();
|
||||
active_thread.thread().update(cx, |thread, _cx| {
|
||||
let current_mode = thread.completion_mode();
|
||||
|
||||
agent.set_completion_mode(match current_mode {
|
||||
thread.set_completion_mode(match current_mode {
|
||||
CompletionMode::Burn => CompletionMode::Normal,
|
||||
CompletionMode::Normal => CompletionMode::Burn,
|
||||
});
|
||||
@@ -1323,18 +1382,21 @@ impl AgentPanel {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
let thread = thread.read(cx);
|
||||
if thread.is_empty() {
|
||||
let id = thread.agent().read(cx).id().clone();
|
||||
let id = thread.thread().read(cx).id().clone();
|
||||
self.history_store.update(cx, |store, cx| {
|
||||
store.remove_recently_opened_thread(id, cx);
|
||||
});
|
||||
}
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
match &new_view {
|
||||
ActiveView::Thread { thread, .. } => self.history_store.update(cx, |store, cx| {
|
||||
let id = thread.read(cx).agent().read(cx).id().clone();
|
||||
let id = thread.read(cx).thread().read(cx).id().clone();
|
||||
store.push_recently_opened_entry(HistoryEntryId::Thread(id), cx);
|
||||
}),
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
@@ -1344,6 +1406,9 @@ impl AgentPanel {
|
||||
}
|
||||
})
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo! push history entry
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
|
||||
@@ -1430,6 +1495,7 @@ impl Focusable for AgentPanel {
|
||||
fn focus_handle(&self, cx: &App) -> FocusHandle {
|
||||
match &self.active_view {
|
||||
ActiveView::Thread { message_editor, .. } => message_editor.focus_handle(cx),
|
||||
ActiveView::AcpThread { thread_view, .. } => thread_view.focus_handle(cx),
|
||||
ActiveView::History => self.history.focus_handle(cx),
|
||||
ActiveView::TextThread { context_editor, .. } => context_editor.focus_handle(cx),
|
||||
ActiveView::Configuration => {
|
||||
@@ -1586,6 +1652,9 @@ impl AgentPanel {
|
||||
.into_any_element(),
|
||||
}
|
||||
}
|
||||
ActiveView::AcpThread { thread_view } => Label::new(thread_view.read(cx).title(cx))
|
||||
.truncate()
|
||||
.into_any_element(),
|
||||
ActiveView::TextThread {
|
||||
title_editor,
|
||||
context_editor,
|
||||
@@ -1719,7 +1788,11 @@ impl AgentPanel {
|
||||
};
|
||||
|
||||
let active_thread = match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => Some(thread.clone()),
|
||||
ActiveView::Thread { thread, .. } => Some(thread.read(cx).thread().clone()),
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
None
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => None,
|
||||
};
|
||||
|
||||
@@ -1748,13 +1821,14 @@ impl AgentPanel {
|
||||
menu = menu
|
||||
.action("New Thread", NewThread::default().boxed_clone())
|
||||
.action("New Text Thread", NewTextThread.boxed_clone())
|
||||
.action("New Gemini Thread", NewGeminiThread.boxed_clone())
|
||||
.when_some(active_thread, |this, active_thread| {
|
||||
let thread = active_thread.read(cx);
|
||||
if !thread.is_empty() {
|
||||
this.action(
|
||||
"New From Summary",
|
||||
Box::new(NewThread {
|
||||
from_thread_id: Some(thread.agent().read(cx).id().clone()),
|
||||
from_thread_id: Some(thread.id().clone()),
|
||||
}),
|
||||
)
|
||||
} else {
|
||||
@@ -1886,6 +1960,10 @@ impl AgentPanel {
|
||||
message_editor,
|
||||
..
|
||||
} => (thread.read(cx), message_editor.read(cx)),
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
return None;
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||
return None;
|
||||
}
|
||||
@@ -1897,14 +1975,14 @@ impl AgentPanel {
|
||||
return None;
|
||||
}
|
||||
|
||||
let agent = active_thread.agent().read(cx);
|
||||
let is_generating = agent.is_generating();
|
||||
let conversation_token_usage = agent.total_token_usage(cx)?;
|
||||
let thread = active_thread.thread().read(cx);
|
||||
let is_generating = thread.is_generating();
|
||||
let conversation_token_usage = thread.total_token_usage()?;
|
||||
|
||||
let (total_token_usage, is_estimating) =
|
||||
if let Some((editing_message_id, unsent_tokens)) = active_thread.editing_message_id() {
|
||||
let combined = agent
|
||||
.token_usage_up_to_message(editing_message_id, cx)
|
||||
let combined = thread
|
||||
.token_usage_up_to_message(editing_message_id)
|
||||
.add(unsent_tokens);
|
||||
|
||||
(combined, unsent_tokens > 0)
|
||||
@@ -2015,17 +2093,19 @@ impl AgentPanel {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
let is_using_zed_provider = thread
|
||||
.read(cx)
|
||||
.agent()
|
||||
.thread()
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(false, |model| {
|
||||
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
||||
});
|
||||
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID);
|
||||
|
||||
if !is_using_zed_provider {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
return false;
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||
return false;
|
||||
}
|
||||
@@ -2593,7 +2673,7 @@ impl AgentPanel {
|
||||
Some(ConfigurationError::ProviderPendingTermsAcceptance(provider)) => {
|
||||
parent.child(Banner::new().severity(ui::Severity::Warning).child(
|
||||
h_flex().w_full().children(provider.render_accept_terms(
|
||||
LanguageModelProviderTosView::ThreadtEmptyState,
|
||||
LanguageModelProviderTosView::ThreadEmptyState,
|
||||
cx,
|
||||
)),
|
||||
))
|
||||
@@ -2610,19 +2690,23 @@ impl AgentPanel {
|
||||
) -> Option<AnyElement> {
|
||||
let active_thread = match &self.active_view {
|
||||
ActiveView::Thread { thread, .. } => thread,
|
||||
ActiveView::AcpThread { .. } => {
|
||||
// todo!
|
||||
return None;
|
||||
}
|
||||
ActiveView::TextThread { .. } | ActiveView::History | ActiveView::Configuration => {
|
||||
return None;
|
||||
}
|
||||
};
|
||||
|
||||
let agent = active_thread.read(cx).agent().read(cx);
|
||||
let thread = active_thread.read(cx).thread().read(cx);
|
||||
|
||||
let tool_use_limit_reached = agent.tool_use_limit_reached();
|
||||
let tool_use_limit_reached = thread.tool_use_limit_reached();
|
||||
if !tool_use_limit_reached {
|
||||
return None;
|
||||
}
|
||||
|
||||
let model = agent.configured_model()?.model;
|
||||
let model = thread.configured_model()?.model;
|
||||
|
||||
let focus_handle = self.focus_handle(cx);
|
||||
|
||||
@@ -2670,8 +2754,8 @@ impl AgentPanel {
|
||||
let active_thread = active_thread.clone();
|
||||
cx.listener(move |this, _, window, cx| {
|
||||
active_thread.update(cx, |active_thread, cx| {
|
||||
active_thread.agent().update(cx, |agent, _cx| {
|
||||
agent.set_completion_mode(CompletionMode::Burn);
|
||||
active_thread.thread().update(cx, |thread, _cx| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
});
|
||||
this.continue_conversation(window, cx);
|
||||
@@ -2684,58 +2768,90 @@ impl AgentPanel {
|
||||
Some(div().px_2().pb_2().child(banner).into_any_element())
|
||||
}
|
||||
|
||||
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
||||
let message = message.into();
|
||||
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
})
|
||||
}
|
||||
|
||||
fn dismiss_error_button(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
IconButton::new("dismiss", IconName::Close)
|
||||
.icon_size(IconSize::Small)
|
||||
.icon_color(Color::Muted)
|
||||
.tooltip(Tooltip::text("Dismiss Error"))
|
||||
.on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn upgrade_button(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> impl IntoElement {
|
||||
Button::new("upgrade", "Upgrade")
|
||||
.label_size(LabelSize::Small)
|
||||
.style(ButtonStyle::Tinted(ui::TintColor::Accent))
|
||||
.on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
fn error_callout_bg(&self, cx: &Context<Self>) -> Hsla {
|
||||
cx.theme().status().error.opacity(0.08)
|
||||
}
|
||||
|
||||
fn render_payment_required_error(
|
||||
&self,
|
||||
thread: &Entity<ActiveThread>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
const ERROR_MESSAGE: &str = "Free tier exceeded. Subscribe and add payment to continue using Zed LLMs. You'll be billed at cost for tokens used.";
|
||||
const ERROR_MESSAGE: &str =
|
||||
"You reached your free usage limit. Upgrade to Zed Pro for more prompts.";
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Free Usage Exceeded").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(ERROR_MESSAGE)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(ERROR_MESSAGE))
|
||||
.child(Button::new("subscribe", "Subscribe").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
})))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title("Free Usage Exceeded")
|
||||
.description(ERROR_MESSAGE)
|
||||
.tertiary_action(self.upgrade_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(ERROR_MESSAGE))
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_model_request_limit_reached_error(
|
||||
@@ -2745,67 +2861,28 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let error_message = match plan {
|
||||
Plan::ZedPro => {
|
||||
"Model request limit reached. Upgrade to usage-based billing for more requests."
|
||||
}
|
||||
Plan::ZedProTrial => {
|
||||
"Model request limit reached. Upgrade to Zed Pro for more requests."
|
||||
}
|
||||
Plan::Free => "Model request limit reached. Upgrade to Zed Pro for more requests.",
|
||||
};
|
||||
let call_to_action = match plan {
|
||||
Plan::ZedPro => "Upgrade to usage-based billing",
|
||||
Plan::ZedProTrial => "Upgrade to Zed Pro",
|
||||
Plan::Free => "Upgrade to Zed Pro",
|
||||
Plan::ZedPro => "Upgrade to usage-based billing for more prompts.",
|
||||
Plan::ZedProTrial | Plan::Free => "Upgrade to Zed Pro for more prompts.",
|
||||
};
|
||||
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new("Model Request Limit Reached").weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_24()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(error_message)),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(error_message))
|
||||
.child(
|
||||
Button::new("subscribe", call_to_action).on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
cx.open_url(&zed_urls::account_url(cx));
|
||||
cx.notify();
|
||||
}
|
||||
})),
|
||||
)
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title("Model Prompt Limit Reached")
|
||||
.description(error_message)
|
||||
.tertiary_action(self.upgrade_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(error_message))
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_error_message(
|
||||
@@ -2816,40 +2893,24 @@ impl AgentPanel {
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyElement {
|
||||
let message_with_header = format!("{}\n{}", header, message);
|
||||
v_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
h_flex()
|
||||
.gap_1p5()
|
||||
.items_center()
|
||||
.child(Icon::new(IconName::XCircle).color(Color::Error))
|
||||
.child(Label::new(header).weight(FontWeight::MEDIUM)),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.id("error-message")
|
||||
.max_h_32()
|
||||
.overflow_y_scroll()
|
||||
.child(Label::new(message.clone())),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.mt_1()
|
||||
.gap_1()
|
||||
.child(self.create_copy_button(message_with_header))
|
||||
.child(Button::new("dismiss", "Dismiss").on_click(cx.listener({
|
||||
let thread = thread.clone();
|
||||
move |_, _, _, cx| {
|
||||
thread.update(cx, |this, _cx| {
|
||||
this.clear_last_error();
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
}))),
|
||||
let icon = Icon::new(IconName::XCircle)
|
||||
.size(IconSize::Small)
|
||||
.color(Color::Error);
|
||||
|
||||
div()
|
||||
.border_t_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.child(
|
||||
Callout::new()
|
||||
.icon(icon)
|
||||
.title(header)
|
||||
.description(message.clone())
|
||||
.primary_action(self.dismiss_error_button(thread, cx))
|
||||
.secondary_action(self.create_copy_button(message_with_header))
|
||||
.bg_color(self.error_callout_bg(cx)),
|
||||
)
|
||||
.into_any()
|
||||
.into_any_element()
|
||||
}
|
||||
|
||||
fn render_prompt_editor(
|
||||
@@ -2979,6 +3040,9 @@ impl AgentPanel {
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
unimplemented!()
|
||||
}
|
||||
ActiveView::TextThread { context_editor, .. } => {
|
||||
context_editor.update(cx, |context_editor, cx| {
|
||||
TextThreadEditor::insert_dragged_files(
|
||||
@@ -2994,15 +3058,6 @@ impl AgentPanel {
|
||||
}
|
||||
}
|
||||
|
||||
fn create_copy_button(&self, message: impl Into<String>) -> impl IntoElement {
|
||||
let message = message.into();
|
||||
IconButton::new("copy", IconName::Copy)
|
||||
.on_click(move |_, _, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(message.clone()))
|
||||
})
|
||||
.tooltip(Tooltip::text("Copy Error Message"))
|
||||
}
|
||||
|
||||
fn key_context(&self) -> KeyContext {
|
||||
let mut key_context = KeyContext::new_with_defaults();
|
||||
key_context.add("AgentPanel");
|
||||
@@ -3055,12 +3110,15 @@ impl Render for AgentPanel {
|
||||
match &this.active_view {
|
||||
ActiveView::Thread { thread, .. } => {
|
||||
thread.update(cx, |active_thread, cx| {
|
||||
active_thread.agent().update(cx, |agent, _cx| {
|
||||
agent.set_completion_mode(CompletionMode::Burn);
|
||||
active_thread.thread().update(cx, |thread, _cx| {
|
||||
thread.set_completion_mode(CompletionMode::Burn);
|
||||
});
|
||||
});
|
||||
this.continue_conversation(window, cx);
|
||||
}
|
||||
ActiveView::AcpThread { .. } => {
|
||||
todo!()
|
||||
}
|
||||
ActiveView::TextThread { .. }
|
||||
| ActiveView::History
|
||||
| ActiveView::Configuration => {}
|
||||
@@ -3084,18 +3142,9 @@ impl Render for AgentPanel {
|
||||
thread.clone().into_any_element()
|
||||
})
|
||||
.children(self.render_tool_use_limit_reached(window, cx))
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.when_some(thread.read(cx).last_error(), |this, last_error| {
|
||||
this.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_3()
|
||||
.bottom_12()
|
||||
.max_w_96()
|
||||
.py_2()
|
||||
.px_3()
|
||||
.elevation_2(cx)
|
||||
.occlude()
|
||||
.child(match last_error {
|
||||
ThreadError::PaymentRequired => {
|
||||
self.render_payment_required_error(thread, cx)
|
||||
@@ -3109,6 +3158,13 @@ impl Render for AgentPanel {
|
||||
.into_any(),
|
||||
)
|
||||
})
|
||||
.child(h_flex().child(message_editor.clone()))
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::AcpThread { thread_view, .. } => parent
|
||||
.relative()
|
||||
.child(thread_view.clone())
|
||||
// todo!
|
||||
// .child(h_flex().child(self.message_editor.clone()))
|
||||
.child(self.render_drag_target(cx)),
|
||||
ActiveView::History => parent.child(self.history.clone()),
|
||||
ActiveView::TextThread {
|
||||
|
||||
@@ -26,7 +26,7 @@ mod ui;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
use agent::{ThreadId, ZedAgentThread};
|
||||
use agent::{Thread, ThreadId};
|
||||
use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
|
||||
use assistant_slash_command::SlashCommandRegistry;
|
||||
use client::Client;
|
||||
@@ -55,6 +55,7 @@ actions!(
|
||||
agent,
|
||||
[
|
||||
NewTextThread,
|
||||
NewGeminiThread,
|
||||
ToggleContextPicker,
|
||||
ToggleNavigationMenu,
|
||||
ToggleOptionsMenu,
|
||||
@@ -65,7 +66,6 @@ actions!(
|
||||
OpenHistory,
|
||||
AddContextServer,
|
||||
RemoveSelectedThread,
|
||||
Chat,
|
||||
ChatWithFollow,
|
||||
CycleNextInlineAssist,
|
||||
CyclePreviousInlineAssist,
|
||||
@@ -92,6 +92,7 @@ actions!(
|
||||
|
||||
#[derive(Default, Clone, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct NewThread {
|
||||
#[serde(default)]
|
||||
from_thread_id: Option<ThreadId>,
|
||||
@@ -99,6 +100,7 @@ pub struct NewThread {
|
||||
|
||||
#[derive(PartialEq, Clone, Default, Debug, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = agent)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct ManageProfiles {
|
||||
#[serde(default)]
|
||||
pub customize_tools: Option<AgentProfileId>,
|
||||
@@ -114,7 +116,7 @@ impl ManageProfiles {
|
||||
|
||||
#[derive(Clone)]
|
||||
pub(crate) enum ModelUsageContext {
|
||||
Thread(Entity<ZedAgentThread>),
|
||||
Thread(Entity<Thread>),
|
||||
InlineAssistant,
|
||||
}
|
||||
|
||||
@@ -209,7 +211,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
}
|
||||
}
|
||||
|
||||
let default = to_selected_model(&settings.default_model);
|
||||
let default = settings.default_model.as_ref().map(to_selected_model);
|
||||
let inline_assistant = settings
|
||||
.inline_assistant_model
|
||||
.as_ref()
|
||||
@@ -229,7 +231,7 @@ fn update_active_language_model_from_settings(cx: &mut App) {
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
|
||||
registry.select_default_model(Some(&default), cx);
|
||||
registry.select_default_model(default.as_ref(), cx);
|
||||
registry.select_inline_assistant_model(inline_assistant.as_ref(), cx);
|
||||
registry.select_commit_message_model(commit_message.as_ref(), cx);
|
||||
registry.select_thread_summary_model(thread_summary.as_ref(), cx);
|
||||
|
||||
@@ -22,7 +22,7 @@ use util::ResultExt as _;
|
||||
use workspace::Workspace;
|
||||
|
||||
use agent::{
|
||||
ZedAgentThread,
|
||||
Thread,
|
||||
context::{AgentContextHandle, AgentContextKey, RULES_ICON},
|
||||
thread_store::{TextThreadStore, ThreadStore},
|
||||
};
|
||||
@@ -449,7 +449,7 @@ impl ContextPickerCompletionProvider {
|
||||
let context_store = context_store.clone();
|
||||
let thread_store = thread_store.clone();
|
||||
window.spawn::<_, Option<_>>(cx, async move |cx| {
|
||||
let thread: Entity<ZedAgentThread> = thread_store
|
||||
let thread: Entity<Thread> = thread_store
|
||||
.update_in(cx, |thread_store, window, cx| {
|
||||
thread_store.open_thread(&thread_id, window, cx)
|
||||
})
|
||||
|
||||
@@ -399,7 +399,7 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let all_models = self.all_models.clone();
|
||||
let current_index = self.selected_index;
|
||||
let active_model = (self.get_active_model)(cx);
|
||||
let bg_executor = cx.background_executor();
|
||||
|
||||
let language_model_registry = LanguageModelRegistry::global(cx);
|
||||
@@ -441,12 +441,9 @@ impl PickerDelegate for LanguageModelPickerDelegate {
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries = filtered_models.entries();
|
||||
// Preserve selection focus
|
||||
let new_index = if current_index >= this.delegate.filtered_entries.len() {
|
||||
0
|
||||
} else {
|
||||
current_index
|
||||
};
|
||||
// Finds the currently selected model in the list
|
||||
let new_index =
|
||||
Self::get_active_model_index(&this.delegate.filtered_entries, active_model);
|
||||
this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
|
||||
@@ -9,7 +9,6 @@ use crate::ui::{
|
||||
MaxModeTooltip,
|
||||
preview::{AgentPreview, UsageCallout},
|
||||
};
|
||||
use agent::thread::UserMessageParams;
|
||||
use agent::{
|
||||
context::{AgentContextKey, ContextLoadResult, load_context},
|
||||
context_store::ContextStoreEvent,
|
||||
@@ -32,7 +31,7 @@ use gpui::{
|
||||
Animation, AnimationExt, App, Entity, EventEmitter, Focusable, Subscription, Task, TextStyle,
|
||||
WeakEntity, linear_color_stop, linear_gradient, point, pulsating_between,
|
||||
};
|
||||
use language::{Buffer, Language};
|
||||
use language::{Buffer, Language, Point};
|
||||
use language_model::{
|
||||
ConfiguredModel, LanguageModelRequestMessage, MessageContent, ZED_CLOUD_PROVIDER_ID,
|
||||
};
|
||||
@@ -48,24 +47,26 @@ use ui::{
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
use workspace::{CollaboratorId, Workspace};
|
||||
use zed_actions::agent::Chat;
|
||||
use zed_llm_client::CompletionIntent;
|
||||
|
||||
use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider, crease_for_mention};
|
||||
use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
|
||||
use crate::profile_selector::ProfileSelector;
|
||||
use crate::{
|
||||
ActiveThread, AgentDiffPane, Chat, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
|
||||
ActiveThread, AgentDiffPane, ChatWithFollow, ExpandMessageEditor, Follow, KeepAll,
|
||||
ModelUsageContext, NewThread, OpenAgentDiff, RejectAll, RemoveAllContext, ToggleBurnMode,
|
||||
ToggleContextPicker, ToggleProfileSelector, register_agent_preview,
|
||||
};
|
||||
use agent::{
|
||||
MessageCrease, TokenUsageRatio, ZedAgentThread,
|
||||
MessageCrease, Thread, TokenUsageRatio,
|
||||
context_store::ContextStore,
|
||||
thread_store::{TextThreadStore, ThreadStore},
|
||||
};
|
||||
|
||||
#[derive(RegisterComponent)]
|
||||
pub struct MessageEditor {
|
||||
agent: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
incompatible_tools_state: Entity<IncompatibleToolsState>,
|
||||
editor: Entity<Editor>,
|
||||
workspace: WeakEntity<Workspace>,
|
||||
@@ -156,7 +157,7 @@ impl MessageEditor {
|
||||
prompt_store: Option<Entity<PromptStore>>,
|
||||
thread_store: WeakEntity<ThreadStore>,
|
||||
text_thread_store: WeakEntity<TextThreadStore>,
|
||||
agent: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
@@ -182,13 +183,13 @@ impl MessageEditor {
|
||||
Some(text_thread_store.clone()),
|
||||
context_picker_menu_handle.clone(),
|
||||
SuggestContextKind::File,
|
||||
ModelUsageContext::Thread(agent.clone()),
|
||||
ModelUsageContext::Thread(thread.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let incompatible_tools = cx.new(|cx| IncompatibleToolsState::new(agent.clone(), cx));
|
||||
let incompatible_tools = cx.new(|cx| IncompatibleToolsState::new(thread.clone(), cx));
|
||||
|
||||
let subscriptions = vec![
|
||||
cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
|
||||
@@ -200,7 +201,9 @@ impl MessageEditor {
|
||||
// When context changes, reload it for token counting.
|
||||
let _ = this.reload_context(cx);
|
||||
}),
|
||||
cx.observe(&agent.read(cx).action_log().clone(), |_, _, cx| cx.notify()),
|
||||
cx.observe(&thread.read(cx).action_log().clone(), |_, _, cx| {
|
||||
cx.notify()
|
||||
}),
|
||||
];
|
||||
|
||||
let model_selector = cx.new(|cx| {
|
||||
@@ -208,20 +211,20 @@ impl MessageEditor {
|
||||
fs.clone(),
|
||||
model_selector_menu_handle,
|
||||
editor.focus_handle(cx),
|
||||
ModelUsageContext::Thread(agent.clone()),
|
||||
ModelUsageContext::Thread(thread.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
|
||||
let profile_selector =
|
||||
cx.new(|cx| ProfileSelector::new(fs, agent.clone(), editor.focus_handle(cx), cx));
|
||||
cx.new(|cx| ProfileSelector::new(fs, thread.clone(), editor.focus_handle(cx), cx));
|
||||
|
||||
Self {
|
||||
editor: editor.clone(),
|
||||
project: agent.read(cx).project().clone(),
|
||||
project: thread.read(cx).project().clone(),
|
||||
user_store,
|
||||
agent,
|
||||
thread,
|
||||
incompatible_tools_state: incompatible_tools.clone(),
|
||||
workspace,
|
||||
context_store,
|
||||
@@ -311,11 +314,11 @@ impl MessageEditor {
|
||||
return;
|
||||
}
|
||||
|
||||
self.agent.update(cx, |thread, cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_editing(cx);
|
||||
});
|
||||
|
||||
if self.agent.read(cx).is_generating() {
|
||||
if self.thread.read(cx).is_generating() {
|
||||
self.stop_current_and_send_new_message(window, cx);
|
||||
return;
|
||||
}
|
||||
@@ -352,7 +355,7 @@ impl MessageEditor {
|
||||
|
||||
fn send_to_model(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let Some(ConfiguredModel { model, provider }) = self
|
||||
.agent
|
||||
.thread
|
||||
.update(cx, |thread, cx| thread.get_or_init_configured_model(cx))
|
||||
else {
|
||||
return;
|
||||
@@ -373,7 +376,7 @@ impl MessageEditor {
|
||||
self.last_estimated_token_count.take();
|
||||
cx.emit(MessageEditorEvent::EstimatedTokenCount);
|
||||
|
||||
let agent = self.agent.clone();
|
||||
let thread = self.thread.clone();
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
let checkpoint = git_store.update(cx, |git_store, cx| git_store.checkpoint(cx));
|
||||
let context_task = self.reload_context(cx);
|
||||
@@ -383,16 +386,24 @@ impl MessageEditor {
|
||||
let (checkpoint, loaded_context) = future::join(checkpoint, context_task).await;
|
||||
let loaded_context = loaded_context.unwrap_or_default();
|
||||
|
||||
agent
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send_message(
|
||||
UserMessageParams {
|
||||
text: user_message,
|
||||
creases: user_message_creases,
|
||||
checkpoint: checkpoint.ok(),
|
||||
context: loaded_context,
|
||||
},
|
||||
thread.insert_user_message(
|
||||
user_message,
|
||||
loaded_context,
|
||||
checkpoint.ok(),
|
||||
user_message_creases,
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.log_err();
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.advance_prompt_id();
|
||||
thread.send_to_model(
|
||||
model,
|
||||
CompletionIntent::UserPrompt,
|
||||
Some(window_handle),
|
||||
cx,
|
||||
);
|
||||
@@ -403,11 +414,11 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn stop_current_and_send_new_message(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.agent.update(cx, |thread, cx| {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_editing(cx);
|
||||
});
|
||||
|
||||
let cancelled = self.agent.update(cx, |thread, cx| {
|
||||
let cancelled = self.thread.update(cx, |thread, cx| {
|
||||
thread.cancel_last_completion(Some(window.window_handle()), cx)
|
||||
});
|
||||
|
||||
@@ -449,7 +460,7 @@ impl MessageEditor {
|
||||
|
||||
fn handle_review_click(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.edits_expanded = true;
|
||||
AgentDiffPane::deploy(self.agent.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx).log_err();
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -465,7 +476,7 @@ impl MessageEditor {
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if let Ok(diff) =
|
||||
AgentDiffPane::deploy(self.agent.clone(), self.workspace.clone(), window, cx)
|
||||
AgentDiffPane::deploy(self.thread.clone(), self.workspace.clone(), window, cx)
|
||||
{
|
||||
let path_key = multi_buffer::PathKey::for_buffer(&buffer, cx);
|
||||
diff.update(cx, |diff, cx| diff.move_to_path(path_key, window, cx));
|
||||
@@ -478,7 +489,7 @@ impl MessageEditor {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.agent.update(cx, |thread, _cx| {
|
||||
self.thread.update(cx, |thread, _cx| {
|
||||
let active_completion_mode = thread.completion_mode();
|
||||
|
||||
thread.set_completion_mode(match active_completion_mode {
|
||||
@@ -489,22 +500,36 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn handle_accept_all(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.agent.read(cx).has_pending_edit_tool_uses() {
|
||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
||||
return;
|
||||
}
|
||||
|
||||
let action_log = self.agent.read(cx).action_log();
|
||||
action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
thread.keep_all_edits(cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn handle_reject_all(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
if self.agent.read(cx).has_pending_edit_tool_uses() {
|
||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
||||
return;
|
||||
}
|
||||
|
||||
let action_log = self.agent.read(cx).action_log();
|
||||
action_log.update(cx, |action_log, cx| action_log.reject_all_edits(cx));
|
||||
// Since there's no reject_all_edits method in the thread API,
|
||||
// we need to iterate through all buffers and reject their edits
|
||||
let action_log = self.thread.read(cx).action_log().clone();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
for (buffer, _) in changed_buffers {
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
let buffer_snapshot = buffer.read(cx);
|
||||
let start = buffer_snapshot.anchor_before(Point::new(0, 0));
|
||||
let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
|
||||
thread
|
||||
.reject_edits_in_ranges(buffer, vec![start..end], cx)
|
||||
.detach();
|
||||
});
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
@@ -514,13 +539,17 @@ impl MessageEditor {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.agent.read(cx).has_pending_edit_tool_uses() {
|
||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
||||
return;
|
||||
}
|
||||
|
||||
let action_log = self.agent.read(cx).action_log();
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.reject_buffer_edits(buffer, cx)
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
let buffer_snapshot = buffer.read(cx);
|
||||
let start = buffer_snapshot.anchor_before(Point::new(0, 0));
|
||||
let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
|
||||
thread
|
||||
.reject_edits_in_ranges(buffer, vec![start..end], cx)
|
||||
.detach();
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
@@ -531,19 +560,21 @@ impl MessageEditor {
|
||||
_window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self.agent.read(cx).has_pending_edit_tool_uses() {
|
||||
if self.thread.read(cx).has_pending_edit_tool_uses() {
|
||||
return;
|
||||
}
|
||||
|
||||
let action_log = self.agent.read(cx).action_log();
|
||||
action_log.update(cx, |action_log, cx| {
|
||||
action_log.keep_buffer_edits(buffer, cx)
|
||||
self.thread.update(cx, |thread, cx| {
|
||||
let buffer_snapshot = buffer.read(cx);
|
||||
let start = buffer_snapshot.anchor_before(Point::new(0, 0));
|
||||
let end = buffer_snapshot.anchor_after(buffer_snapshot.max_point());
|
||||
thread.keep_edits_in_range(buffer, start..end, cx);
|
||||
});
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn render_burn_mode_toggle(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
||||
let thread = self.agent.read(cx);
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
if !model?.model.supports_burn_mode() {
|
||||
return None;
|
||||
@@ -614,7 +645,7 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn render_editor(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
||||
let thread = self.agent.read(cx);
|
||||
let thread = self.thread.read(cx);
|
||||
let model = thread.configured_model();
|
||||
|
||||
let editor_bg_color = cx.theme().colors().editor_background;
|
||||
@@ -915,7 +946,7 @@ impl MessageEditor {
|
||||
let bg_edit_files_disclosure = editor_bg_color.blend(active_color.opacity(0.3));
|
||||
|
||||
let is_edit_changes_expanded = self.edits_expanded;
|
||||
let thread = self.agent.read(cx);
|
||||
let thread = self.thread.read(cx);
|
||||
let pending_edits = thread.has_pending_edit_tool_uses();
|
||||
|
||||
const EDIT_NOT_READY_TOOLTIP_LABEL: &str = "Wait until file edits are complete.";
|
||||
@@ -1217,12 +1248,10 @@ impl MessageEditor {
|
||||
}
|
||||
|
||||
fn is_using_zed_provider(&self, cx: &App) -> bool {
|
||||
self.agent
|
||||
self.thread
|
||||
.read(cx)
|
||||
.configured_model()
|
||||
.map_or(false, |model| {
|
||||
model.provider.id().0 == ZED_CLOUD_PROVIDER_ID
|
||||
})
|
||||
.map_or(false, |model| model.provider.id() == ZED_CLOUD_PROVIDER_ID)
|
||||
}
|
||||
|
||||
fn render_usage_callout(&self, line_height: Pixels, cx: &mut Context<Self>) -> Option<Div> {
|
||||
@@ -1295,7 +1324,7 @@ impl MessageEditor {
|
||||
Button::new("start-new-thread", "Start New Thread")
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
let from_thread_id = Some(this.agent.read(cx).id().clone());
|
||||
let from_thread_id = Some(this.thread.read(cx).id().clone());
|
||||
window.dispatch_action(Box::new(NewThread { from_thread_id }), cx);
|
||||
})),
|
||||
);
|
||||
@@ -1329,11 +1358,10 @@ impl MessageEditor {
|
||||
fn reload_context(&mut self, cx: &mut Context<Self>) -> Task<Option<ContextLoadResult>> {
|
||||
let load_task = cx.spawn(async move |this, cx| {
|
||||
let Ok(load_task) = this.update(cx, |this, cx| {
|
||||
let new_context = this.context_store.read(cx).new_context_for_thread(
|
||||
this.agent.read(cx),
|
||||
None,
|
||||
cx,
|
||||
);
|
||||
let new_context = this
|
||||
.context_store
|
||||
.read(cx)
|
||||
.new_context_for_thread(this.thread.read(cx), None);
|
||||
load_context(new_context, &this.project, &this.prompt_store, cx)
|
||||
}) else {
|
||||
return;
|
||||
@@ -1365,7 +1393,7 @@ impl MessageEditor {
|
||||
cx.emit(MessageEditorEvent::Changed);
|
||||
self.update_token_count_task.take();
|
||||
|
||||
let Some(model) = self.agent.read(cx).configured_model() else {
|
||||
let Some(model) = self.thread.read(cx).configured_model() else {
|
||||
self.last_estimated_token_count.take();
|
||||
return;
|
||||
};
|
||||
@@ -1570,16 +1598,16 @@ impl Focusable for MessageEditor {
|
||||
|
||||
impl Render for MessageEditor {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let agent = self.agent.read(cx);
|
||||
let token_usage_ratio = agent
|
||||
.total_token_usage(cx)
|
||||
let thread = self.thread.read(cx);
|
||||
let token_usage_ratio = thread
|
||||
.total_token_usage()
|
||||
.map_or(TokenUsageRatio::Normal, |total_token_usage| {
|
||||
total_token_usage.ratio()
|
||||
});
|
||||
|
||||
let burn_mode_enabled = agent.completion_mode() == CompletionMode::Burn;
|
||||
let burn_mode_enabled = thread.completion_mode() == CompletionMode::Burn;
|
||||
|
||||
let action_log = agent.action_log();
|
||||
let action_log = self.thread.read(cx).action_log();
|
||||
let changed_buffers = action_log.read(cx).changed_buffers(cx);
|
||||
|
||||
let line_height = TextSize::Small.rems(cx).to_pixels(window.rem_size()) * 1.5;
|
||||
@@ -1662,7 +1690,7 @@ impl AgentPreview for MessageEditor {
|
||||
let weak_project = project.downgrade();
|
||||
let context_store = cx.new(|_cx| ContextStore::new(weak_project, None));
|
||||
let active_thread = active_thread.read(cx);
|
||||
let agent = active_thread.agent().clone();
|
||||
let thread = active_thread.thread().clone();
|
||||
let thread_store = active_thread.thread_store().clone();
|
||||
let text_thread_store = active_thread.text_thread_store().clone();
|
||||
|
||||
@@ -1675,7 +1703,7 @@ impl AgentPreview for MessageEditor {
|
||||
None,
|
||||
thread_store.downgrade(),
|
||||
text_thread_store.downgrade(),
|
||||
agent,
|
||||
thread,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use crate::{ManageProfiles, ToggleProfileSelector};
|
||||
use agent::{
|
||||
ZedAgentThread,
|
||||
Thread,
|
||||
agent_profile::{AgentProfile, AvailableProfiles},
|
||||
};
|
||||
use agent_settings::{AgentDockPosition, AgentProfileId, AgentSettings, builtin_profiles};
|
||||
@@ -17,7 +17,7 @@ use ui::{
|
||||
pub struct ProfileSelector {
|
||||
profiles: AvailableProfiles,
|
||||
fs: Arc<dyn Fs>,
|
||||
thread: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
menu_handle: PopoverMenuHandle<ContextMenu>,
|
||||
focus_handle: FocusHandle,
|
||||
_subscriptions: Vec<Subscription>,
|
||||
@@ -26,7 +26,7 @@ pub struct ProfileSelector {
|
||||
impl ProfileSelector {
|
||||
pub fn new(
|
||||
fs: Arc<dyn Fs>,
|
||||
thread: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
focus_handle: FocusHandle,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use agent::{ThreadEvent, ZedAgentThread};
|
||||
use agent::{Thread, ThreadEvent};
|
||||
use assistant_tool::{Tool, ToolSource};
|
||||
use collections::HashMap;
|
||||
use gpui::{App, Context, Entity, IntoElement, Render, Subscription, Window};
|
||||
@@ -8,12 +8,12 @@ use ui::prelude::*;
|
||||
|
||||
pub struct IncompatibleToolsState {
|
||||
cache: HashMap<LanguageModelToolSchemaFormat, Vec<Arc<dyn Tool>>>,
|
||||
thread: Entity<ZedAgentThread>,
|
||||
thread: Entity<Thread>,
|
||||
_thread_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl IncompatibleToolsState {
|
||||
pub fn new(thread: Entity<ZedAgentThread>, cx: &mut Context<Self>) -> Self {
|
||||
pub fn new(thread: Entity<Thread>, cx: &mut Context<Self>) -> Self {
|
||||
let _tool_working_set_subscription =
|
||||
cx.subscribe(&thread, |this, _, event, _| match event {
|
||||
ThreadEvent::ProfileChanged => {
|
||||
|
||||
@@ -488,7 +488,7 @@ impl AddedContext {
|
||||
parent: None,
|
||||
tooltip: None,
|
||||
icon_path: None,
|
||||
status: if handle.agent.read(cx).is_generating_detailed_summary() {
|
||||
status: if handle.thread.read(cx).is_generating_detailed_summary() {
|
||||
ContextStatus::Loading {
|
||||
message: "Summarizing…".into(),
|
||||
}
|
||||
@@ -496,9 +496,9 @@ impl AddedContext {
|
||||
ContextStatus::Ready
|
||||
},
|
||||
render_hover: {
|
||||
let agent = handle.agent.clone();
|
||||
let thread = handle.thread.clone();
|
||||
Some(Rc::new(move |_, cx| {
|
||||
let text = agent.read(cx).latest_detailed_summary_or_text(cx);
|
||||
let text = thread.read(cx).latest_detailed_summary_or_text();
|
||||
ContextPillHover::new_text(text.clone(), cx).into()
|
||||
}))
|
||||
},
|
||||
|
||||
@@ -6,7 +6,7 @@ use anyhow::{Context as _, Result, anyhow};
|
||||
use chrono::{DateTime, Utc};
|
||||
use futures::{AsyncBufReadExt, AsyncReadExt, StreamExt, io::BufReader, stream::BoxStream};
|
||||
use http_client::http::{self, HeaderMap, HeaderValue};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest};
|
||||
use http_client::{AsyncBody, HttpClient, Method, Request as HttpRequest, StatusCode};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use strum::{EnumIter, EnumString};
|
||||
use thiserror::Error;
|
||||
@@ -356,7 +356,7 @@ pub async fn complete(
|
||||
.send(request)
|
||||
.await
|
||||
.map_err(AnthropicError::HttpSend)?;
|
||||
let status = response.status();
|
||||
let status_code = response.status();
|
||||
let mut body = String::new();
|
||||
response
|
||||
.body_mut()
|
||||
@@ -364,12 +364,12 @@ pub async fn complete(
|
||||
.await
|
||||
.map_err(AnthropicError::ReadResponse)?;
|
||||
|
||||
if status.is_success() {
|
||||
if status_code.is_success() {
|
||||
Ok(serde_json::from_str(&body).map_err(AnthropicError::DeserializeResponse)?)
|
||||
} else {
|
||||
Err(AnthropicError::HttpResponseError {
|
||||
status: status.as_u16(),
|
||||
body,
|
||||
status_code,
|
||||
message: body,
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -444,11 +444,7 @@ impl RateLimitInfo {
|
||||
}
|
||||
|
||||
Self {
|
||||
retry_after: headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs),
|
||||
retry_after: parse_retry_after(headers),
|
||||
requests: RateLimit::from_headers("requests", headers).ok(),
|
||||
tokens: RateLimit::from_headers("tokens", headers).ok(),
|
||||
input_tokens: RateLimit::from_headers("input-tokens", headers).ok(),
|
||||
@@ -457,6 +453,17 @@ impl RateLimitInfo {
|
||||
}
|
||||
}
|
||||
|
||||
/// Parses the Retry-After header value as an integer number of seconds (anthropic always uses
|
||||
/// seconds). Note that other services might specify an HTTP date or some other format for this
|
||||
/// header. Returns `None` if the header is not present or cannot be parsed.
|
||||
pub fn parse_retry_after(headers: &HeaderMap<HeaderValue>) -> Option<Duration> {
|
||||
headers
|
||||
.get("retry-after")
|
||||
.and_then(|v| v.to_str().ok())
|
||||
.and_then(|v| v.parse::<u64>().ok())
|
||||
.map(Duration::from_secs)
|
||||
}
|
||||
|
||||
fn get_header<'a>(key: &str, headers: &'a HeaderMap) -> anyhow::Result<&'a str> {
|
||||
Ok(headers
|
||||
.get(key)
|
||||
@@ -520,6 +527,10 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
})
|
||||
.boxed();
|
||||
Ok((stream, Some(rate_limits)))
|
||||
} else if response.status().as_u16() == 529 {
|
||||
Err(AnthropicError::ServerOverloaded {
|
||||
retry_after: rate_limits.retry_after,
|
||||
})
|
||||
} else if let Some(retry_after) = rate_limits.retry_after {
|
||||
Err(AnthropicError::RateLimit { retry_after })
|
||||
} else {
|
||||
@@ -532,10 +543,9 @@ pub async fn stream_completion_with_rate_limit_info(
|
||||
|
||||
match serde_json::from_str::<Event>(&body) {
|
||||
Ok(Event::Error { error }) => Err(AnthropicError::ApiError(error)),
|
||||
Ok(_) => Err(AnthropicError::UnexpectedResponseFormat(body)),
|
||||
Err(_) => Err(AnthropicError::HttpResponseError {
|
||||
status: response.status().as_u16(),
|
||||
body: body,
|
||||
Ok(_) | Err(_) => Err(AnthropicError::HttpResponseError {
|
||||
status_code: response.status(),
|
||||
message: body,
|
||||
}),
|
||||
}
|
||||
}
|
||||
@@ -801,16 +811,19 @@ pub enum AnthropicError {
|
||||
ReadResponse(io::Error),
|
||||
|
||||
/// HTTP error response from the API
|
||||
HttpResponseError { status: u16, body: String },
|
||||
HttpResponseError {
|
||||
status_code: StatusCode,
|
||||
message: String,
|
||||
},
|
||||
|
||||
/// Rate limit exceeded
|
||||
RateLimit { retry_after: Duration },
|
||||
|
||||
/// Server overloaded
|
||||
ServerOverloaded { retry_after: Option<Duration> },
|
||||
|
||||
/// API returned an error response
|
||||
ApiError(ApiError),
|
||||
|
||||
/// Unexpected response format
|
||||
UnexpectedResponseFormat(String),
|
||||
}
|
||||
|
||||
#[derive(Debug, Serialize, Deserialize, Error)]
|
||||
|
||||
@@ -2140,7 +2140,8 @@ impl AssistantContext {
|
||||
);
|
||||
}
|
||||
LanguageModelCompletionEvent::ToolUse(_) |
|
||||
LanguageModelCompletionEvent::UsageUpdate(_) => {}
|
||||
LanguageModelCompletionEvent::ToolUseJsonParseError { .. } |
|
||||
LanguageModelCompletionEvent::UsageUpdate(_) => {}
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -5,9 +5,6 @@ edition.workspace = true
|
||||
publish.workspace = true
|
||||
license = "GPL-3.0-or-later"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
|
||||
[lints]
|
||||
workspace = true
|
||||
|
||||
|
||||
@@ -495,10 +495,6 @@ impl ActionLog {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn keep_buffer_edits(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.keep_edits_in_range(buffer, Anchor::MIN..Anchor::MAX, cx);
|
||||
}
|
||||
|
||||
pub fn keep_edits_in_range(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
@@ -559,19 +555,6 @@ impl ActionLog {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) {
|
||||
let changed_buffers = self.changed_buffers(cx);
|
||||
for (buffer, _) in changed_buffers {
|
||||
self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx)
|
||||
.detach();
|
||||
}
|
||||
}
|
||||
|
||||
pub fn reject_buffer_edits(&mut self, buffer: Entity<Buffer>, cx: &mut Context<Self>) {
|
||||
self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx)
|
||||
.detach()
|
||||
}
|
||||
|
||||
pub fn reject_edits_in_ranges(
|
||||
&mut self,
|
||||
buffer: Entity<Buffer>,
|
||||
|
||||
@@ -70,7 +70,7 @@ pub struct ToolResultOutput {
|
||||
pub output: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq)]
|
||||
#[derive(Debug, PartialEq, Eq)]
|
||||
pub enum ToolResultContent {
|
||||
Text(String),
|
||||
Image(LanguageModelImage),
|
||||
@@ -135,8 +135,7 @@ pub trait ToolCard: 'static + Sized {
|
||||
) -> impl IntoElement;
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(any(test, feature = "test-support"), derive(PartialEq, Eq))]
|
||||
#[derive(Clone)]
|
||||
pub struct AnyToolCard {
|
||||
entity: gpui::AnyEntity,
|
||||
render: fn(
|
||||
|
||||
@@ -29,6 +29,7 @@ use std::{
|
||||
path::Path,
|
||||
str::FromStr,
|
||||
sync::mpsc,
|
||||
time::Duration,
|
||||
};
|
||||
use util::path;
|
||||
|
||||
@@ -1658,12 +1659,14 @@ async fn retry_on_rate_limit<R>(mut request: impl AsyncFnMut() -> Result<R>) ->
|
||||
match request().await {
|
||||
Ok(result) => return Ok(result),
|
||||
Err(err) => match err.downcast::<LanguageModelCompletionError>() {
|
||||
Ok(err) => match err {
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after } => {
|
||||
Ok(err) => match &err {
|
||||
LanguageModelCompletionError::RateLimitExceeded { retry_after, .. }
|
||||
| LanguageModelCompletionError::ServerOverloaded { retry_after, .. } => {
|
||||
let retry_after = retry_after.unwrap_or(Duration::from_secs(5));
|
||||
// Wait for the duration supplied, with some jitter to avoid all requests being made at the same time.
|
||||
let jitter = retry_after.mul_f64(rand::thread_rng().gen_range(0.0..1.0));
|
||||
eprintln!(
|
||||
"Attempt #{attempt}: Rate limit exceeded. Retry after {retry_after:?} + jitter of {jitter:?}"
|
||||
"Attempt #{attempt}: {err}. Retry after {retry_after:?} + jitter of {jitter:?}"
|
||||
);
|
||||
Timer::after(retry_after + jitter).await;
|
||||
continue;
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
use anyhow::Result;
|
||||
use language_model::LanguageModelToolSchemaFormat;
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
schema::{RootSchema, Schema, SchemaObject},
|
||||
JsonSchema, Schema,
|
||||
generate::SchemaSettings,
|
||||
transform::{Transform, transform_subschemas},
|
||||
};
|
||||
|
||||
pub fn json_schema_for<T: JsonSchema>(
|
||||
@@ -13,7 +14,7 @@ pub fn json_schema_for<T: JsonSchema>(
|
||||
}
|
||||
|
||||
fn schema_to_json(
|
||||
schema: &RootSchema,
|
||||
schema: &Schema,
|
||||
format: LanguageModelToolSchemaFormat,
|
||||
) -> Result<serde_json::Value> {
|
||||
let mut value = serde_json::to_value(schema)?;
|
||||
@@ -21,58 +22,42 @@ fn schema_to_json(
|
||||
Ok(value)
|
||||
}
|
||||
|
||||
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> RootSchema {
|
||||
fn root_schema_for<T: JsonSchema>(format: LanguageModelToolSchemaFormat) -> Schema {
|
||||
let mut generator = match format {
|
||||
LanguageModelToolSchemaFormat::JsonSchema => schemars::SchemaGenerator::default(),
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => {
|
||||
schemars::r#gen::SchemaSettings::default()
|
||||
.with(|settings| {
|
||||
settings.meta_schema = None;
|
||||
settings.inline_subschemas = true;
|
||||
settings
|
||||
.visitors
|
||||
.push(Box::new(TransformToJsonSchemaSubsetVisitor));
|
||||
})
|
||||
.into_generator()
|
||||
}
|
||||
LanguageModelToolSchemaFormat::JsonSchema => SchemaSettings::draft07().into_generator(),
|
||||
// TODO: Gemini docs mention using a subset of OpenAPI 3, so this may benefit from using
|
||||
// `SchemaSettings::openapi3()`.
|
||||
LanguageModelToolSchemaFormat::JsonSchemaSubset => SchemaSettings::draft07()
|
||||
.with(|settings| {
|
||||
settings.meta_schema = None;
|
||||
settings.inline_subschemas = true;
|
||||
})
|
||||
.with_transform(ToJsonSchemaSubsetTransform)
|
||||
.into_generator(),
|
||||
};
|
||||
generator.root_schema_for::<T>()
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct TransformToJsonSchemaSubsetVisitor;
|
||||
struct ToJsonSchemaSubsetTransform;
|
||||
|
||||
impl schemars::visit::Visitor for TransformToJsonSchemaSubsetVisitor {
|
||||
fn visit_root_schema(&mut self, root: &mut RootSchema) {
|
||||
schemars::visit::visit_root_schema(self, root)
|
||||
}
|
||||
|
||||
fn visit_schema(&mut self, schema: &mut Schema) {
|
||||
schemars::visit::visit_schema(self, schema)
|
||||
}
|
||||
|
||||
fn visit_schema_object(&mut self, schema: &mut SchemaObject) {
|
||||
impl Transform for ToJsonSchemaSubsetTransform {
|
||||
fn transform(&mut self, schema: &mut Schema) {
|
||||
// Ensure that the type field is not an array, this happens when we use
|
||||
// Option<T>, the type will be [T, "null"].
|
||||
if let Some(instance_type) = schema.instance_type.take() {
|
||||
schema.instance_type = match instance_type {
|
||||
schemars::schema::SingleOrVec::Single(t) => {
|
||||
Some(schemars::schema::SingleOrVec::Single(t))
|
||||
if let Some(type_field) = schema.get_mut("type") {
|
||||
if let Some(types) = type_field.as_array() {
|
||||
if let Some(first_type) = types.first() {
|
||||
*type_field = first_type.clone();
|
||||
}
|
||||
schemars::schema::SingleOrVec::Vec(items) => items
|
||||
.into_iter()
|
||||
.next()
|
||||
.map(schemars::schema::SingleOrVec::from),
|
||||
};
|
||||
}
|
||||
|
||||
// One of is not supported, use anyOf instead.
|
||||
if let Some(subschema) = schema.subschemas.as_mut() {
|
||||
if let Some(one_of) = subschema.one_of.take() {
|
||||
subschema.any_of = Some(one_of);
|
||||
}
|
||||
}
|
||||
|
||||
schemars::visit::visit_schema_object(self, schema)
|
||||
// oneOf is not supported, use anyOf instead
|
||||
if let Some(one_of) = schema.remove("oneOf") {
|
||||
schema.insert("anyOf".to_string(), one_of);
|
||||
}
|
||||
|
||||
transform_subschemas(self, schema);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -25,5 +25,4 @@ serde.workspace = true
|
||||
serde_json.workspace = true
|
||||
strum.workspace = true
|
||||
thiserror.workspace = true
|
||||
tokio = { workspace = true, features = ["rt", "rt-multi-thread"] }
|
||||
workspace-hack.workspace = true
|
||||
|
||||
@@ -1,9 +1,6 @@
|
||||
mod models;
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::pin::Pin;
|
||||
|
||||
use anyhow::{Context as _, Error, Result, anyhow};
|
||||
use anyhow::{Context, Error, Result, anyhow};
|
||||
use aws_sdk_bedrockruntime as bedrock;
|
||||
pub use aws_sdk_bedrockruntime as bedrock_client;
|
||||
pub use aws_sdk_bedrockruntime::types::{
|
||||
@@ -24,9 +21,10 @@ pub use bedrock::types::{
|
||||
ToolResultContentBlock as BedrockToolResultContentBlock,
|
||||
ToolResultStatus as BedrockToolResultStatus, ToolUseBlock as BedrockToolUseBlock,
|
||||
};
|
||||
use futures::stream::{self, BoxStream, Stream};
|
||||
use futures::stream::{self, BoxStream};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::{Number, Value};
|
||||
use std::collections::HashMap;
|
||||
use thiserror::Error;
|
||||
|
||||
pub use crate::models::*;
|
||||
@@ -34,70 +32,59 @@ pub use crate::models::*;
|
||||
pub async fn stream_completion(
|
||||
client: bedrock::Client,
|
||||
request: Request,
|
||||
handle: tokio::runtime::Handle,
|
||||
) -> Result<BoxStream<'static, Result<BedrockStreamingResponse, BedrockError>>, Error> {
|
||||
handle
|
||||
.spawn(async move {
|
||||
let mut response = bedrock::Client::converse_stream(&client)
|
||||
.model_id(request.model.clone())
|
||||
.set_messages(request.messages.into());
|
||||
let mut response = bedrock::Client::converse_stream(&client)
|
||||
.model_id(request.model.clone())
|
||||
.set_messages(request.messages.into());
|
||||
|
||||
if let Some(Thinking::Enabled {
|
||||
budget_tokens: Some(budget_tokens),
|
||||
}) = request.thinking
|
||||
{
|
||||
response =
|
||||
response.additional_model_request_fields(Document::Object(HashMap::from([(
|
||||
"thinking".to_string(),
|
||||
Document::from(HashMap::from([
|
||||
("type".to_string(), Document::String("enabled".to_string())),
|
||||
(
|
||||
"budget_tokens".to_string(),
|
||||
Document::Number(AwsNumber::PosInt(budget_tokens)),
|
||||
),
|
||||
])),
|
||||
)])));
|
||||
}
|
||||
if let Some(Thinking::Enabled {
|
||||
budget_tokens: Some(budget_tokens),
|
||||
}) = request.thinking
|
||||
{
|
||||
let thinking_config = HashMap::from([
|
||||
("type".to_string(), Document::String("enabled".to_string())),
|
||||
(
|
||||
"budget_tokens".to_string(),
|
||||
Document::Number(AwsNumber::PosInt(budget_tokens)),
|
||||
),
|
||||
]);
|
||||
response = response.additional_model_request_fields(Document::Object(HashMap::from([(
|
||||
"thinking".to_string(),
|
||||
Document::from(thinking_config),
|
||||
)])));
|
||||
}
|
||||
|
||||
if request.tools.is_some() && !request.tools.as_ref().unwrap().tools.is_empty() {
|
||||
response = response.set_tool_config(request.tools);
|
||||
}
|
||||
if request
|
||||
.tools
|
||||
.as_ref()
|
||||
.map_or(false, |t| !t.tools.is_empty())
|
||||
{
|
||||
response = response.set_tool_config(request.tools);
|
||||
}
|
||||
|
||||
let response = response.send().await;
|
||||
let output = response
|
||||
.send()
|
||||
.await
|
||||
.context("Failed to send API request to Bedrock");
|
||||
|
||||
match response {
|
||||
Ok(output) => {
|
||||
let stream: Pin<
|
||||
Box<
|
||||
dyn Stream<Item = Result<BedrockStreamingResponse, BedrockError>>
|
||||
+ Send,
|
||||
>,
|
||||
> = Box::pin(stream::unfold(output.stream, |mut stream| async move {
|
||||
match stream.recv().await {
|
||||
Ok(Some(output)) => Some(({ Ok(output) }, stream)),
|
||||
Ok(None) => None,
|
||||
Err(err) => {
|
||||
Some((
|
||||
// TODO: Figure out how we can capture Throttling Exceptions
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
))
|
||||
}
|
||||
}
|
||||
}));
|
||||
Ok(stream)
|
||||
}
|
||||
Err(err) => Err(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
let stream = Box::pin(stream::unfold(
|
||||
output?.stream,
|
||||
move |mut stream| async move {
|
||||
match stream.recv().await {
|
||||
Ok(Some(output)) => Some((Ok(output), stream)),
|
||||
Ok(None) => None,
|
||||
Err(err) => Some((
|
||||
Err(BedrockError::ClientError(anyhow!(
|
||||
"{:?}",
|
||||
aws_sdk_bedrockruntime::error::DisplayErrorContext(err)
|
||||
))),
|
||||
stream,
|
||||
)),
|
||||
}
|
||||
})
|
||||
.await
|
||||
.context("spawning a task")?
|
||||
},
|
||||
));
|
||||
|
||||
Ok(stream)
|
||||
}
|
||||
|
||||
pub fn aws_document_to_value(document: &Document) -> Value {
|
||||
|
||||
@@ -12,7 +12,6 @@ pub struct CallSettings {
|
||||
|
||||
/// Configuration of voice calls in Zed.
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct CallSettingsContent {
|
||||
/// Whether the microphone should be muted when joining a channel or a call.
|
||||
///
|
||||
|
||||
@@ -22,9 +22,7 @@ use gpui::{
|
||||
use language::{
|
||||
Diagnostic, DiagnosticEntry, DiagnosticSourceKind, FakeLspAdapter, Language, LanguageConfig,
|
||||
LanguageMatcher, LineEnding, OffsetRangeExt, Point, Rope,
|
||||
language_settings::{
|
||||
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
|
||||
},
|
||||
language_settings::{AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter},
|
||||
tree_sitter_rust, tree_sitter_typescript,
|
||||
};
|
||||
use lsp::{LanguageServerId, OneOf};
|
||||
@@ -4591,15 +4589,13 @@ async fn test_formatting_buffer(
|
||||
cx_a.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::External {
|
||||
file.defaults.formatter =
|
||||
Some(SelectedFormatter::List(vec![Formatter::External {
|
||||
command: "awk".into(),
|
||||
arguments: Some(
|
||||
vec!["{sub(/two/,\"{buffer_path}\")}1".to_string()].into(),
|
||||
),
|
||||
}]
|
||||
.into(),
|
||||
)));
|
||||
}]));
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -4699,9 +4695,10 @@ async fn test_prettier_formatting_buffer(
|
||||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into(),
|
||||
)));
|
||||
file.defaults.formatter =
|
||||
Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
|
||||
name: None,
|
||||
}]));
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
|
||||
@@ -14,8 +14,7 @@ use http_client::BlockedHttpClient;
|
||||
use language::{
|
||||
FakeLspAdapter, Language, LanguageConfig, LanguageMatcher, LanguageRegistry,
|
||||
language_settings::{
|
||||
AllLanguageSettings, Formatter, FormatterList, PrettierSettings, SelectedFormatter,
|
||||
language_settings,
|
||||
AllLanguageSettings, Formatter, PrettierSettings, SelectedFormatter, language_settings,
|
||||
},
|
||||
tree_sitter_typescript,
|
||||
};
|
||||
@@ -505,9 +504,10 @@ async fn test_ssh_collaboration_formatting_with_prettier(
|
||||
cx_b.update(|cx| {
|
||||
SettingsStore::update_global(cx, |store, cx| {
|
||||
store.update_user_settings::<AllLanguageSettings>(cx, |file| {
|
||||
file.defaults.formatter = Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into(),
|
||||
)));
|
||||
file.defaults.formatter =
|
||||
Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
|
||||
name: None,
|
||||
}]));
|
||||
file.defaults.prettier = Some(PrettierSettings {
|
||||
allowed: true,
|
||||
..PrettierSettings::default()
|
||||
|
||||
@@ -28,7 +28,6 @@ pub struct ChatPanelSettings {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct ChatPanelSettingsContent {
|
||||
/// When to show the panel button in the status bar.
|
||||
///
|
||||
@@ -52,7 +51,6 @@ pub struct NotificationPanelSettings {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct PanelSettingsContent {
|
||||
/// Whether to show the panel button in the status bar.
|
||||
///
|
||||
@@ -69,7 +67,6 @@ pub struct PanelSettingsContent {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema, Debug)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct MessageEditorSettings {
|
||||
/// Whether to automatically replace emoji shortcodes with emoji characters.
|
||||
/// For example: typing `:wave:` gets replaced with `👋`.
|
||||
|
||||
@@ -41,7 +41,7 @@ pub struct CommandPalette {
|
||||
/// Removes subsequent whitespace characters and double colons from the query.
|
||||
///
|
||||
/// This improves the likelihood of a match by either humanized name or keymap-style name.
|
||||
fn normalize_query(input: &str) -> String {
|
||||
pub fn normalize_action_query(input: &str) -> String {
|
||||
let mut result = String::with_capacity(input.len());
|
||||
let mut last_char = None;
|
||||
|
||||
@@ -297,7 +297,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
let mut commands = self.all_commands.clone();
|
||||
let hit_counts = self.hit_counts();
|
||||
let executor = cx.background_executor().clone();
|
||||
let query = normalize_query(query.as_str());
|
||||
let query = normalize_action_query(query.as_str());
|
||||
async move {
|
||||
commands.sort_by_key(|action| {
|
||||
(
|
||||
@@ -311,29 +311,17 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
.enumerate()
|
||||
.map(|(ix, command)| StringMatchCandidate::new(ix, &command.name))
|
||||
.collect::<Vec<_>>();
|
||||
let matches = if query.is_empty() {
|
||||
candidates
|
||||
.into_iter()
|
||||
.enumerate()
|
||||
.map(|(index, candidate)| StringMatch {
|
||||
candidate_id: index,
|
||||
string: candidate.string,
|
||||
positions: Vec::new(),
|
||||
score: 0.0,
|
||||
})
|
||||
.collect()
|
||||
} else {
|
||||
fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await
|
||||
};
|
||||
|
||||
let matches = fuzzy::match_strings(
|
||||
&candidates,
|
||||
&query,
|
||||
true,
|
||||
true,
|
||||
10000,
|
||||
&Default::default(),
|
||||
executor,
|
||||
)
|
||||
.await;
|
||||
|
||||
tx.send((commands, matches)).await.log_err();
|
||||
}
|
||||
@@ -422,8 +410,8 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
let r#match = self.matches.get(ix)?;
|
||||
let command = self.commands.get(r#match.candidate_id)?;
|
||||
let matching_command = self.matches.get(ix)?;
|
||||
let command = self.commands.get(matching_command.candidate_id)?;
|
||||
Some(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
@@ -436,7 +424,7 @@ impl PickerDelegate for CommandPaletteDelegate {
|
||||
.justify_between()
|
||||
.child(HighlightedLabel::new(
|
||||
command.name.clone(),
|
||||
r#match.positions.clone(),
|
||||
matching_command.positions.clone(),
|
||||
))
|
||||
.children(KeyBinding::for_action_in(
|
||||
&*command.action,
|
||||
@@ -512,19 +500,28 @@ mod tests {
|
||||
|
||||
#[test]
|
||||
fn test_normalize_query() {
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(normalize_query("editor: backspace"), "editor: backspace");
|
||||
assert_eq!(
|
||||
normalize_query("editor::GoToDefinition"),
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor: backspace"),
|
||||
"editor: backspace"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_action_query("editor::GoToDefinition"),
|
||||
"editor:GoToDefinition"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query("editor::::GoToDefinition"),
|
||||
normalize_action_query("editor::::GoToDefinition"),
|
||||
"editor:GoToDefinition"
|
||||
);
|
||||
assert_eq!(
|
||||
normalize_query("editor: :GoToDefinition"),
|
||||
normalize_action_query("editor: :GoToDefinition"),
|
||||
"editor: :GoToDefinition"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ use gpui::{AsyncApp, SharedString};
|
||||
pub use http_client::{HttpClient, github::latest_github_release};
|
||||
use language::{LanguageName, LanguageToolchainStore};
|
||||
use node_runtime::NodeRuntime;
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::WorktreeId;
|
||||
use smol::fs::File;
|
||||
@@ -47,7 +48,10 @@ pub trait DapDelegate: Send + Sync + 'static {
|
||||
async fn shell_env(&self) -> collections::HashMap<String, String>;
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize)]
|
||||
#[derive(
|
||||
Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Deserialize, Serialize, JsonSchema,
|
||||
)]
|
||||
#[serde(transparent)]
|
||||
pub struct DebugAdapterName(pub SharedString);
|
||||
|
||||
impl Deref for DebugAdapterName {
|
||||
|
||||
@@ -25,7 +25,9 @@ anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
collections.workspace = true
|
||||
dap.workspace = true
|
||||
dotenvy.workspace = true
|
||||
futures.workspace = true
|
||||
fs.workspace = true
|
||||
gpui.workspace = true
|
||||
json_dotpath.workspace = true
|
||||
language.workspace = true
|
||||
|
||||
@@ -22,17 +22,16 @@ impl CodeLldbDebugAdapter {
|
||||
async fn request_args(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
task_definition: &DebugTaskDefinition,
|
||||
mut configuration: Value,
|
||||
label: &str,
|
||||
) -> Result<dap::StartDebuggingRequestArguments> {
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
let mut configuration = task_definition.config.clone();
|
||||
|
||||
let obj = configuration
|
||||
.as_object_mut()
|
||||
.context("CodeLLDB is not a valid json object")?;
|
||||
|
||||
// CodeLLDB uses `name` for a terminal label.
|
||||
obj.entry("name")
|
||||
.or_insert(Value::String(String::from(task_definition.label.as_ref())));
|
||||
.or_insert(Value::String(String::from(label)));
|
||||
|
||||
obj.entry("cwd")
|
||||
.or_insert(delegate.worktree_root_path().to_string_lossy().into());
|
||||
@@ -361,17 +360,31 @@ impl DebugAdapter for CodeLldbDebugAdapter {
|
||||
self.path_to_codelldb.set(path.clone()).ok();
|
||||
command = Some(path);
|
||||
};
|
||||
|
||||
let mut json_config = config.config.clone();
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(command.unwrap()),
|
||||
cwd: Some(delegate.worktree_root_path().to_path_buf()),
|
||||
arguments: user_args.unwrap_or_else(|| {
|
||||
vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": ["cpp", "rust"]}).to_string(),
|
||||
]
|
||||
if let Some(config) = json_config.as_object_mut()
|
||||
&& let Some(source_languages) = config.get("sourceLanguages").filter(|value| {
|
||||
value
|
||||
.as_array()
|
||||
.map_or(false, |array| array.iter().all(Value::is_string))
|
||||
})
|
||||
{
|
||||
let ret = vec![
|
||||
"--settings".into(),
|
||||
json!({"sourceLanguages": source_languages}).to_string(),
|
||||
];
|
||||
config.remove("sourceLanguages");
|
||||
ret
|
||||
} else {
|
||||
vec![]
|
||||
}
|
||||
}),
|
||||
request_args: self.request_args(delegate, &config).await?,
|
||||
request_args: self
|
||||
.request_args(delegate, json_config, &config.label)
|
||||
.await?,
|
||||
envs: HashMap::default(),
|
||||
connection: None,
|
||||
})
|
||||
|
||||
@@ -4,7 +4,6 @@ mod go;
|
||||
mod javascript;
|
||||
mod php;
|
||||
mod python;
|
||||
mod ruby;
|
||||
|
||||
use std::sync::Arc;
|
||||
|
||||
@@ -25,7 +24,6 @@ use gpui::{App, BorrowAppContext};
|
||||
use javascript::JsDebugAdapter;
|
||||
use php::PhpDebugAdapter;
|
||||
use python::PythonDebugAdapter;
|
||||
use ruby::RubyDebugAdapter;
|
||||
use serde_json::json;
|
||||
use task::{DebugScenario, ZedDebugConfig};
|
||||
|
||||
@@ -35,7 +33,6 @@ pub fn init(cx: &mut App) {
|
||||
registry.add_adapter(Arc::from(PythonDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(PhpDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(JsDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(RubyDebugAdapter));
|
||||
registry.add_adapter(Arc::from(GoDebugAdapter::default()));
|
||||
registry.add_adapter(Arc::from(GdbDebugAdapter));
|
||||
|
||||
|
||||
@@ -7,13 +7,22 @@ use dap::{
|
||||
latest_github_release,
|
||||
},
|
||||
};
|
||||
|
||||
use fs::Fs;
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use std::{env::consts, ffi::OsStr, path::PathBuf, sync::OnceLock};
|
||||
use log::warn;
|
||||
use serde_json::{Map, Value};
|
||||
use task::TcpArgumentsTemplate;
|
||||
use util;
|
||||
|
||||
use std::{
|
||||
env::consts,
|
||||
ffi::OsStr,
|
||||
path::{Path, PathBuf},
|
||||
str::FromStr,
|
||||
sync::OnceLock,
|
||||
};
|
||||
|
||||
use crate::*;
|
||||
|
||||
#[derive(Default, Debug)]
|
||||
@@ -437,22 +446,34 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
adapter_path.join("dlv").to_string_lossy().to_string()
|
||||
};
|
||||
|
||||
let cwd = task_definition
|
||||
.config
|
||||
.get("cwd")
|
||||
.and_then(|s| s.as_str())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf());
|
||||
let cwd = Some(
|
||||
task_definition
|
||||
.config
|
||||
.get("cwd")
|
||||
.and_then(|s| s.as_str())
|
||||
.map(PathBuf::from)
|
||||
.unwrap_or_else(|| delegate.worktree_root_path().to_path_buf()),
|
||||
);
|
||||
|
||||
let arguments;
|
||||
let command;
|
||||
let connection;
|
||||
|
||||
let mut configuration = task_definition.config.clone();
|
||||
let mut envs = HashMap::default();
|
||||
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
|
||||
handle_envs(
|
||||
configuration,
|
||||
&mut envs,
|
||||
cwd.as_deref(),
|
||||
delegate.fs().clone(),
|
||||
)
|
||||
.await;
|
||||
}
|
||||
|
||||
if let Some(connection_options) = &task_definition.tcp_connection {
|
||||
@@ -494,8 +515,8 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
Ok(DebugAdapterBinary {
|
||||
command,
|
||||
arguments,
|
||||
cwd: Some(cwd),
|
||||
envs: HashMap::default(),
|
||||
cwd,
|
||||
envs,
|
||||
connection,
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
configuration,
|
||||
@@ -504,3 +525,44 @@ impl DebugAdapter for GoDebugAdapter {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// delve doesn't do anything with the envFile setting, so we intercept it
|
||||
async fn handle_envs(
|
||||
config: &mut Map<String, Value>,
|
||||
envs: &mut HashMap<String, String>,
|
||||
cwd: Option<&Path>,
|
||||
fs: Arc<dyn Fs>,
|
||||
) -> Option<()> {
|
||||
let env_files = match config.get("envFile")? {
|
||||
Value::Array(arr) => arr.iter().map(|v| v.as_str()).collect::<Vec<_>>(),
|
||||
Value::String(s) => vec![Some(s.as_str())],
|
||||
_ => return None,
|
||||
};
|
||||
|
||||
let rebase_path = |path: PathBuf| {
|
||||
if path.is_absolute() {
|
||||
Some(path)
|
||||
} else {
|
||||
cwd.map(|p| p.join(path))
|
||||
}
|
||||
};
|
||||
|
||||
for path in env_files {
|
||||
let Some(path) = path
|
||||
.and_then(|s| PathBuf::from_str(s).ok())
|
||||
.and_then(rebase_path)
|
||||
else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Ok(file) = fs.open_sync(&path).await {
|
||||
envs.extend(dotenvy::from_read_iter(file).filter_map(Result::ok))
|
||||
} else {
|
||||
warn!("While starting Go debug session: failed to read env file {path:?}");
|
||||
};
|
||||
}
|
||||
|
||||
// remove envFile now that it's been handled
|
||||
config.remove("entry");
|
||||
Some(())
|
||||
}
|
||||
|
||||
@@ -282,6 +282,10 @@ impl DebugAdapter for JsDebugAdapter {
|
||||
"description": "Automatically stop program after launch",
|
||||
"default": false
|
||||
},
|
||||
"attachSimplePort": {
|
||||
"type": "number",
|
||||
"description": "If set, attaches to the process via the given port. This is generally no longer necessary for Node.js programs and loses the ability to debug child processes, but can be useful in more esoteric scenarios such as with Deno and Docker launches. If set to 0, a random port will be chosen and --inspect-brk added to the launch arguments automatically."
|
||||
},
|
||||
"runtimeExecutable": {
|
||||
"type": ["string", "null"],
|
||||
"description": "Runtime to use, an absolute path or the name of a runtime available on PATH",
|
||||
|
||||
@@ -1,208 +0,0 @@
|
||||
use anyhow::{Result, bail};
|
||||
use async_trait::async_trait;
|
||||
use collections::FxHashMap;
|
||||
use dap::{
|
||||
DebugRequest, StartDebuggingRequestArguments, StartDebuggingRequestArgumentsRequest,
|
||||
adapters::{
|
||||
DapDelegate, DebugAdapter, DebugAdapterBinary, DebugAdapterName, DebugTaskDefinition,
|
||||
},
|
||||
};
|
||||
use gpui::{AsyncApp, SharedString};
|
||||
use language::LanguageName;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use serde_json::json;
|
||||
use std::path::PathBuf;
|
||||
use std::{ffi::OsStr, sync::Arc};
|
||||
use task::{DebugScenario, ZedDebugConfig};
|
||||
use util::command::new_smol_command;
|
||||
|
||||
#[derive(Default)]
|
||||
pub(crate) struct RubyDebugAdapter;
|
||||
|
||||
impl RubyDebugAdapter {
|
||||
const ADAPTER_NAME: &'static str = "Ruby";
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
struct RubyDebugConfig {
|
||||
script_or_command: Option<String>,
|
||||
script: Option<String>,
|
||||
command: Option<String>,
|
||||
#[serde(default)]
|
||||
args: Vec<String>,
|
||||
#[serde(default)]
|
||||
env: FxHashMap<String, String>,
|
||||
cwd: Option<PathBuf>,
|
||||
}
|
||||
|
||||
#[async_trait(?Send)]
|
||||
impl DebugAdapter for RubyDebugAdapter {
|
||||
fn name(&self) -> DebugAdapterName {
|
||||
DebugAdapterName(Self::ADAPTER_NAME.into())
|
||||
}
|
||||
|
||||
fn adapter_language_name(&self) -> Option<LanguageName> {
|
||||
Some(SharedString::new_static("Ruby").into())
|
||||
}
|
||||
|
||||
async fn request_kind(
|
||||
&self,
|
||||
_: &serde_json::Value,
|
||||
) -> Result<StartDebuggingRequestArgumentsRequest> {
|
||||
Ok(StartDebuggingRequestArgumentsRequest::Launch)
|
||||
}
|
||||
|
||||
fn dap_schema(&self) -> serde_json::Value {
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"command": {
|
||||
"type": "string",
|
||||
"description": "Command name (ruby, rake, bin/rails, bundle exec ruby, etc)",
|
||||
},
|
||||
"script": {
|
||||
"type": "string",
|
||||
"description": "Absolute path to a Ruby file."
|
||||
},
|
||||
"cwd": {
|
||||
"type": "string",
|
||||
"description": "Directory to execute the program in",
|
||||
"default": "${ZED_WORKTREE_ROOT}"
|
||||
},
|
||||
"args": {
|
||||
"type": "array",
|
||||
"description": "Command line arguments passed to the program",
|
||||
"items": {
|
||||
"type": "string"
|
||||
},
|
||||
"default": []
|
||||
},
|
||||
"env": {
|
||||
"type": "object",
|
||||
"description": "Additional environment variables to pass to the debugging (and debugged) process",
|
||||
"default": {}
|
||||
},
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
async fn config_from_zed_format(&self, zed_scenario: ZedDebugConfig) -> Result<DebugScenario> {
|
||||
match zed_scenario.request {
|
||||
DebugRequest::Launch(launch) => {
|
||||
let config = RubyDebugConfig {
|
||||
script_or_command: Some(launch.program),
|
||||
script: None,
|
||||
command: None,
|
||||
args: launch.args,
|
||||
env: launch.env,
|
||||
cwd: launch.cwd.clone(),
|
||||
};
|
||||
|
||||
let config = serde_json::to_value(config)?;
|
||||
|
||||
Ok(DebugScenario {
|
||||
adapter: zed_scenario.adapter,
|
||||
label: zed_scenario.label,
|
||||
config,
|
||||
tcp_connection: None,
|
||||
build: None,
|
||||
})
|
||||
}
|
||||
DebugRequest::Attach(_) => {
|
||||
anyhow::bail!("Attach requests are unsupported");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async fn get_binary(
|
||||
&self,
|
||||
delegate: &Arc<dyn DapDelegate>,
|
||||
definition: &DebugTaskDefinition,
|
||||
_user_installed_path: Option<PathBuf>,
|
||||
_user_args: Option<Vec<String>>,
|
||||
_cx: &mut AsyncApp,
|
||||
) -> Result<DebugAdapterBinary> {
|
||||
let adapter_path = paths::debug_adapters_dir().join(self.name().as_ref());
|
||||
let mut rdbg_path = adapter_path.join("rdbg");
|
||||
if !delegate.fs().is_file(&rdbg_path).await {
|
||||
match delegate.which("rdbg".as_ref()).await {
|
||||
Some(path) => rdbg_path = path,
|
||||
None => {
|
||||
delegate.output_to_console(
|
||||
"rdbg not found on path, trying `gem install debug`".to_string(),
|
||||
);
|
||||
let output = new_smol_command("gem")
|
||||
.arg("install")
|
||||
.arg("--no-document")
|
||||
.arg("--bindir")
|
||||
.arg(adapter_path)
|
||||
.arg("debug")
|
||||
.output()
|
||||
.await?;
|
||||
anyhow::ensure!(
|
||||
output.status.success(),
|
||||
"Failed to install rdbg:\n{}",
|
||||
String::from_utf8_lossy(&output.stderr).to_string()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let tcp_connection = definition.tcp_connection.clone().unwrap_or_default();
|
||||
let (host, port, timeout) = crate::configure_tcp_connection(tcp_connection).await?;
|
||||
let ruby_config = serde_json::from_value::<RubyDebugConfig>(definition.config.clone())?;
|
||||
|
||||
let mut arguments = vec![
|
||||
"--open".to_string(),
|
||||
format!("--port={}", port),
|
||||
format!("--host={}", host),
|
||||
];
|
||||
|
||||
if let Some(script) = &ruby_config.script {
|
||||
arguments.push(script.clone());
|
||||
} else if let Some(command) = &ruby_config.command {
|
||||
arguments.push("--command".to_string());
|
||||
arguments.push(command.clone());
|
||||
} else if let Some(command_or_script) = &ruby_config.script_or_command {
|
||||
if delegate
|
||||
.which(OsStr::new(&command_or_script))
|
||||
.await
|
||||
.is_some()
|
||||
{
|
||||
arguments.push("--command".to_string());
|
||||
}
|
||||
arguments.push(command_or_script.clone());
|
||||
} else {
|
||||
bail!("Ruby debug config must have 'script' or 'command' args");
|
||||
}
|
||||
|
||||
arguments.extend(ruby_config.args);
|
||||
|
||||
let mut configuration = definition.config.clone();
|
||||
if let Some(configuration) = configuration.as_object_mut() {
|
||||
configuration
|
||||
.entry("cwd")
|
||||
.or_insert_with(|| delegate.worktree_root_path().to_string_lossy().into());
|
||||
}
|
||||
|
||||
Ok(DebugAdapterBinary {
|
||||
command: Some(rdbg_path.to_string_lossy().to_string()),
|
||||
arguments,
|
||||
connection: Some(dap::adapters::TcpArguments {
|
||||
host,
|
||||
port,
|
||||
timeout,
|
||||
}),
|
||||
cwd: Some(
|
||||
ruby_config
|
||||
.cwd
|
||||
.unwrap_or(delegate.worktree_root_path().to_owned()),
|
||||
),
|
||||
envs: ruby_config.env.into_iter().collect(),
|
||||
request_args: StartDebuggingRequestArguments {
|
||||
request: self.request_kind(&definition.config).await?,
|
||||
configuration,
|
||||
},
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -21,7 +21,7 @@ use project::{
|
||||
use settings::Settings as _;
|
||||
use std::{
|
||||
borrow::Cow,
|
||||
collections::{HashMap, VecDeque},
|
||||
collections::{BTreeMap, HashMap, VecDeque},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::maybe;
|
||||
@@ -32,13 +32,6 @@ use workspace::{
|
||||
ui::{Button, Clickable, ContextMenu, Label, LabelCommon, PopoverMenu, h_flex},
|
||||
};
|
||||
|
||||
// TODO:
|
||||
// - [x] stop sorting by session ID
|
||||
// - [x] pick the most recent session by default (logs if available, RPC messages otherwise)
|
||||
// - [ ] dump the launch/attach request somewhere (logs?)
|
||||
|
||||
const MAX_SESSIONS: usize = 10;
|
||||
|
||||
struct DapLogView {
|
||||
editor: Entity<Editor>,
|
||||
focus_handle: FocusHandle,
|
||||
@@ -49,14 +42,34 @@ struct DapLogView {
|
||||
_subscriptions: Vec<Subscription>,
|
||||
}
|
||||
|
||||
struct LogStoreEntryIdentifier<'a> {
|
||||
session_id: SessionId,
|
||||
project: Cow<'a, WeakEntity<Project>>,
|
||||
}
|
||||
impl LogStoreEntryIdentifier<'_> {
|
||||
fn to_owned(&self) -> LogStoreEntryIdentifier<'static> {
|
||||
LogStoreEntryIdentifier {
|
||||
session_id: self.session_id,
|
||||
project: Cow::Owned(self.project.as_ref().clone()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct LogStoreMessage {
|
||||
id: LogStoreEntryIdentifier<'static>,
|
||||
kind: IoKind,
|
||||
command: Option<SharedString>,
|
||||
message: SharedString,
|
||||
}
|
||||
|
||||
pub struct LogStore {
|
||||
projects: HashMap<WeakEntity<Project>, ProjectState>,
|
||||
debug_sessions: VecDeque<DebugAdapterState>,
|
||||
rpc_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
|
||||
adapter_log_tx: UnboundedSender<(SessionId, IoKind, Option<SharedString>, SharedString)>,
|
||||
rpc_tx: UnboundedSender<LogStoreMessage>,
|
||||
adapter_log_tx: UnboundedSender<LogStoreMessage>,
|
||||
}
|
||||
|
||||
struct ProjectState {
|
||||
debug_sessions: BTreeMap<SessionId, DebugAdapterState>,
|
||||
_subscriptions: [gpui::Subscription; 2],
|
||||
}
|
||||
|
||||
@@ -122,13 +135,12 @@ impl DebugAdapterState {
|
||||
|
||||
impl LogStore {
|
||||
pub fn new(cx: &Context<Self>) -> Self {
|
||||
let (rpc_tx, mut rpc_rx) =
|
||||
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
|
||||
let (rpc_tx, mut rpc_rx) = unbounded::<LogStoreMessage>();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((session_id, io_kind, command, message)) = rpc_rx.next().await {
|
||||
while let Some(message) = rpc_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_debug_adapter_message(session_id, io_kind, command, message, cx);
|
||||
this.add_debug_adapter_message(message, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -138,13 +150,12 @@ impl LogStore {
|
||||
})
|
||||
.detach_and_log_err(cx);
|
||||
|
||||
let (adapter_log_tx, mut adapter_log_rx) =
|
||||
unbounded::<(SessionId, IoKind, Option<SharedString>, SharedString)>();
|
||||
let (adapter_log_tx, mut adapter_log_rx) = unbounded::<LogStoreMessage>();
|
||||
cx.spawn(async move |this, cx| {
|
||||
while let Some((session_id, io_kind, _, message)) = adapter_log_rx.next().await {
|
||||
while let Some(message) = adapter_log_rx.next().await {
|
||||
if let Some(this) = this.upgrade() {
|
||||
this.update(cx, |this, cx| {
|
||||
this.add_debug_adapter_log(session_id, io_kind, message, cx);
|
||||
this.add_debug_adapter_log(message, cx);
|
||||
})?;
|
||||
}
|
||||
|
||||
@@ -157,57 +168,76 @@ impl LogStore {
|
||||
rpc_tx,
|
||||
adapter_log_tx,
|
||||
projects: HashMap::new(),
|
||||
debug_sessions: Default::default(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn add_project(&mut self, project: &Entity<Project>, cx: &mut Context<Self>) {
|
||||
let weak_project = project.downgrade();
|
||||
self.projects.insert(
|
||||
project.downgrade(),
|
||||
ProjectState {
|
||||
_subscriptions: [
|
||||
cx.observe_release(project, move |this, _, _| {
|
||||
this.projects.remove(&weak_project);
|
||||
cx.observe_release(project, {
|
||||
let weak_project = project.downgrade();
|
||||
move |this, _, _| {
|
||||
this.projects.remove(&weak_project);
|
||||
}
|
||||
}),
|
||||
cx.subscribe(
|
||||
&project.read(cx).dap_store(),
|
||||
|this, dap_store, event, cx| match event {
|
||||
cx.subscribe(&project.read(cx).dap_store(), {
|
||||
let weak_project = project.downgrade();
|
||||
move |this, dap_store, event, cx| match event {
|
||||
dap_store::DapStoreEvent::DebugClientStarted(session_id) => {
|
||||
let session = dap_store.read(cx).session_by_id(session_id);
|
||||
if let Some(session) = session {
|
||||
this.add_debug_session(*session_id, session, cx);
|
||||
this.add_debug_session(
|
||||
LogStoreEntryIdentifier {
|
||||
project: Cow::Owned(weak_project.clone()),
|
||||
session_id: *session_id,
|
||||
},
|
||||
session,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
}
|
||||
dap_store::DapStoreEvent::DebugClientShutdown(session_id) => {
|
||||
this.get_debug_adapter_state(*session_id)
|
||||
.iter_mut()
|
||||
.for_each(|state| state.is_terminated = true);
|
||||
let id = LogStoreEntryIdentifier {
|
||||
project: Cow::Borrowed(&weak_project),
|
||||
session_id: *session_id,
|
||||
};
|
||||
if let Some(state) = this.get_debug_adapter_state(&id) {
|
||||
state.is_terminated = true;
|
||||
}
|
||||
|
||||
this.clean_sessions(cx);
|
||||
}
|
||||
_ => {}
|
||||
},
|
||||
),
|
||||
}
|
||||
}),
|
||||
],
|
||||
debug_sessions: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
fn get_debug_adapter_state(&mut self, id: SessionId) -> Option<&mut DebugAdapterState> {
|
||||
self.debug_sessions
|
||||
.iter_mut()
|
||||
.find(|adapter_state| adapter_state.id == id)
|
||||
fn get_debug_adapter_state(
|
||||
&mut self,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
) -> Option<&mut DebugAdapterState> {
|
||||
self.projects
|
||||
.get_mut(&id.project)
|
||||
.and_then(|state| state.debug_sessions.get_mut(&id.session_id))
|
||||
}
|
||||
|
||||
fn add_debug_adapter_message(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
command: Option<SharedString>,
|
||||
message: SharedString,
|
||||
LogStoreMessage {
|
||||
id,
|
||||
kind: io_kind,
|
||||
command,
|
||||
message,
|
||||
}: LogStoreMessage,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(id) else {
|
||||
let Some(debug_client_state) = self.get_debug_adapter_state(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -229,7 +259,7 @@ impl LogStore {
|
||||
if rpc_messages.last_message_kind != Some(kind) {
|
||||
Self::get_debug_adapter_entry(
|
||||
&mut rpc_messages.messages,
|
||||
id,
|
||||
id.to_owned(),
|
||||
kind.label().into(),
|
||||
LogKind::Rpc,
|
||||
cx,
|
||||
@@ -239,7 +269,7 @@ impl LogStore {
|
||||
|
||||
let entry = Self::get_debug_adapter_entry(
|
||||
&mut rpc_messages.messages,
|
||||
id,
|
||||
id.to_owned(),
|
||||
message,
|
||||
LogKind::Rpc,
|
||||
cx,
|
||||
@@ -260,12 +290,15 @@ impl LogStore {
|
||||
|
||||
fn add_debug_adapter_log(
|
||||
&mut self,
|
||||
id: SessionId,
|
||||
io_kind: IoKind,
|
||||
message: SharedString,
|
||||
LogStoreMessage {
|
||||
id,
|
||||
kind: io_kind,
|
||||
message,
|
||||
..
|
||||
}: LogStoreMessage,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let Some(debug_adapter_state) = self.get_debug_adapter_state(id) else {
|
||||
let Some(debug_adapter_state) = self.get_debug_adapter_state(&id) else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -276,7 +309,7 @@ impl LogStore {
|
||||
|
||||
Self::get_debug_adapter_entry(
|
||||
&mut debug_adapter_state.log_messages,
|
||||
id,
|
||||
id.to_owned(),
|
||||
message,
|
||||
LogKind::Adapter,
|
||||
cx,
|
||||
@@ -286,13 +319,17 @@ impl LogStore {
|
||||
|
||||
fn get_debug_adapter_entry(
|
||||
log_lines: &mut VecDeque<SharedString>,
|
||||
id: SessionId,
|
||||
id: LogStoreEntryIdentifier<'static>,
|
||||
message: SharedString,
|
||||
kind: LogKind,
|
||||
cx: &mut Context<Self>,
|
||||
) -> SharedString {
|
||||
while log_lines.len() >= RpcMessages::MESSAGE_QUEUE_LIMIT {
|
||||
log_lines.pop_front();
|
||||
if let Some(excess) = log_lines
|
||||
.len()
|
||||
.checked_sub(RpcMessages::MESSAGE_QUEUE_LIMIT)
|
||||
&& excess > 0
|
||||
{
|
||||
log_lines.drain(..excess);
|
||||
}
|
||||
|
||||
let format_messages = DebuggerSettings::get_global(cx).format_dap_log_messages;
|
||||
@@ -322,118 +359,116 @@ impl LogStore {
|
||||
|
||||
fn add_debug_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: LogStoreEntryIdentifier<'static>,
|
||||
session: Entity<Session>,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
if self
|
||||
.debug_sessions
|
||||
.iter_mut()
|
||||
.any(|adapter_state| adapter_state.id == session_id)
|
||||
{
|
||||
return;
|
||||
}
|
||||
maybe!({
|
||||
let project_entry = self.projects.get_mut(&id.project)?;
|
||||
let std::collections::btree_map::Entry::Vacant(state) =
|
||||
project_entry.debug_sessions.entry(id.session_id)
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map(|client| client.has_adapter_logs())
|
||||
.unwrap_or(false),
|
||||
)
|
||||
let (adapter_name, has_adapter_logs) = session.read_with(cx, |session, _| {
|
||||
(
|
||||
session.adapter(),
|
||||
session
|
||||
.adapter_client()
|
||||
.map_or(false, |client| client.has_adapter_logs()),
|
||||
)
|
||||
});
|
||||
|
||||
state.insert(DebugAdapterState::new(
|
||||
id.session_id,
|
||||
adapter_name,
|
||||
has_adapter_logs,
|
||||
));
|
||||
|
||||
self.clean_sessions(cx);
|
||||
|
||||
let io_tx = self.rpc_tx.clone();
|
||||
|
||||
let client = session.read(cx).adapter_client()?;
|
||||
let project = id.project.clone();
|
||||
let session_id = id.session_id;
|
||||
client.add_log_handler(
|
||||
move |kind, command, message| {
|
||||
io_tx
|
||||
.unbounded_send(LogStoreMessage {
|
||||
id: LogStoreEntryIdentifier {
|
||||
session_id,
|
||||
project: project.clone(),
|
||||
},
|
||||
kind,
|
||||
command: command.map(|command| command.to_owned().into()),
|
||||
message: message.to_owned().into(),
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
LogKind::Rpc,
|
||||
);
|
||||
|
||||
let log_io_tx = self.adapter_log_tx.clone();
|
||||
let project = id.project;
|
||||
client.add_log_handler(
|
||||
move |kind, command, message| {
|
||||
log_io_tx
|
||||
.unbounded_send(LogStoreMessage {
|
||||
id: LogStoreEntryIdentifier {
|
||||
session_id,
|
||||
project: project.clone(),
|
||||
},
|
||||
kind,
|
||||
command: command.map(|command| command.to_owned().into()),
|
||||
message: message.to_owned().into(),
|
||||
})
|
||||
.ok();
|
||||
},
|
||||
LogKind::Adapter,
|
||||
);
|
||||
Some(())
|
||||
});
|
||||
|
||||
self.debug_sessions.push_back(DebugAdapterState::new(
|
||||
session_id,
|
||||
adapter_name,
|
||||
has_adapter_logs,
|
||||
));
|
||||
|
||||
self.clean_sessions(cx);
|
||||
|
||||
let io_tx = self.rpc_tx.clone();
|
||||
|
||||
let Some(client) = session.read(cx).adapter_client() else {
|
||||
return;
|
||||
};
|
||||
|
||||
client.add_log_handler(
|
||||
move |io_kind, command, message| {
|
||||
io_tx
|
||||
.unbounded_send((
|
||||
session_id,
|
||||
io_kind,
|
||||
command.map(|command| command.to_owned().into()),
|
||||
message.to_owned().into(),
|
||||
))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Rpc,
|
||||
);
|
||||
|
||||
let log_io_tx = self.adapter_log_tx.clone();
|
||||
client.add_log_handler(
|
||||
move |io_kind, command, message| {
|
||||
log_io_tx
|
||||
.unbounded_send((
|
||||
session_id,
|
||||
io_kind,
|
||||
command.map(|command| command.to_owned().into()),
|
||||
message.to_owned().into(),
|
||||
))
|
||||
.ok();
|
||||
},
|
||||
LogKind::Adapter,
|
||||
);
|
||||
}
|
||||
|
||||
fn clean_sessions(&mut self, cx: &mut Context<Self>) {
|
||||
let mut to_remove = self.debug_sessions.len().saturating_sub(MAX_SESSIONS);
|
||||
self.debug_sessions.retain(|session| {
|
||||
if to_remove > 0 && session.is_terminated {
|
||||
to_remove -= 1;
|
||||
return false;
|
||||
}
|
||||
true
|
||||
self.projects.values_mut().for_each(|project| {
|
||||
let mut allowed_terminated_sessions = 10u32;
|
||||
project.debug_sessions.retain(|_, session| {
|
||||
if !session.is_terminated {
|
||||
return true;
|
||||
}
|
||||
allowed_terminated_sessions = allowed_terminated_sessions.saturating_sub(1);
|
||||
allowed_terminated_sessions > 0
|
||||
});
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn log_messages_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
) -> Option<&mut VecDeque<SharedString>> {
|
||||
self.debug_sessions
|
||||
.iter_mut()
|
||||
.find(|session| session.id == session_id)
|
||||
self.get_debug_adapter_state(id)
|
||||
.map(|state| &mut state.log_messages)
|
||||
}
|
||||
|
||||
fn rpc_messages_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
) -> Option<&mut VecDeque<SharedString>> {
|
||||
self.debug_sessions.iter_mut().find_map(|state| {
|
||||
if state.id == session_id {
|
||||
Some(&mut state.rpc_messages.messages)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
self.get_debug_adapter_state(id)
|
||||
.map(|state| &mut state.rpc_messages.messages)
|
||||
}
|
||||
|
||||
fn initialization_sequence_for_session(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
) -> Option<&mut Vec<SharedString>> {
|
||||
self.debug_sessions.iter_mut().find_map(|state| {
|
||||
if state.id == session_id {
|
||||
Some(&mut state.rpc_messages.initialization_sequence)
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
) -> Option<&Vec<SharedString>> {
|
||||
self.get_debug_adapter_state(&id)
|
||||
.map(|state| &state.rpc_messages.initialization_sequence)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -453,10 +488,11 @@ impl Render for DapLogToolbarItemView {
|
||||
return Empty.into_any_element();
|
||||
};
|
||||
|
||||
let (menu_rows, current_session_id) = log_view.update(cx, |log_view, cx| {
|
||||
let (menu_rows, current_session_id, project) = log_view.update(cx, |log_view, cx| {
|
||||
(
|
||||
log_view.menu_items(cx),
|
||||
log_view.current_view.map(|(session_id, _)| session_id),
|
||||
log_view.project.downgrade(),
|
||||
)
|
||||
});
|
||||
|
||||
@@ -484,6 +520,7 @@ impl Render for DapLogToolbarItemView {
|
||||
.menu(move |mut window, cx| {
|
||||
let log_view = log_view.clone();
|
||||
let menu_rows = menu_rows.clone();
|
||||
let project = project.clone();
|
||||
ContextMenu::build(&mut window, cx, move |mut menu, window, _cx| {
|
||||
for row in menu_rows.into_iter() {
|
||||
menu = menu.custom_row(move |_window, _cx| {
|
||||
@@ -509,8 +546,15 @@ impl Render for DapLogToolbarItemView {
|
||||
.child(Label::new(ADAPTER_LOGS))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_log_messages_for_adapter(row.session_id, window, cx);
|
||||
window.handler_for(&log_view, {
|
||||
let project = project.clone();
|
||||
let id = LogStoreEntryIdentifier {
|
||||
project: Cow::Owned(project),
|
||||
session_id: row.session_id,
|
||||
};
|
||||
move |view, window, cx| {
|
||||
view.show_log_messages_for_adapter(&id, window, cx);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -524,8 +568,15 @@ impl Render for DapLogToolbarItemView {
|
||||
.child(Label::new(RPC_MESSAGES))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_rpc_trace_for_server(row.session_id, window, cx);
|
||||
window.handler_for(&log_view, {
|
||||
let project = project.clone();
|
||||
let id = LogStoreEntryIdentifier {
|
||||
project: Cow::Owned(project),
|
||||
session_id: row.session_id,
|
||||
};
|
||||
move |view, window, cx| {
|
||||
view.show_rpc_trace_for_server(&id, window, cx);
|
||||
}
|
||||
}),
|
||||
)
|
||||
.custom_entry(
|
||||
@@ -536,12 +587,17 @@ impl Render for DapLogToolbarItemView {
|
||||
.child(Label::new(INITIALIZATION_SEQUENCE))
|
||||
.into_any_element()
|
||||
},
|
||||
window.handler_for(&log_view, move |view, window, cx| {
|
||||
view.show_initialization_sequence_for_server(
|
||||
row.session_id,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
window.handler_for(&log_view, {
|
||||
let project = project.clone();
|
||||
let id = LogStoreEntryIdentifier {
|
||||
project: Cow::Owned(project),
|
||||
session_id: row.session_id,
|
||||
};
|
||||
move |view, window, cx| {
|
||||
view.show_initialization_sequence_for_server(
|
||||
&id, window, cx,
|
||||
);
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
@@ -613,7 +669,9 @@ impl DapLogView {
|
||||
|
||||
let events_subscriptions = cx.subscribe(&log_store, |log_view, _, event, cx| match event {
|
||||
Event::NewLogEntry { id, entry, kind } => {
|
||||
if log_view.current_view == Some((*id, *kind)) {
|
||||
if log_view.current_view == Some((id.session_id, *kind))
|
||||
&& log_view.project == *id.project
|
||||
{
|
||||
log_view.editor.update(cx, |editor, cx| {
|
||||
editor.set_read_only(false);
|
||||
let last_point = editor.buffer().read(cx).len(cx);
|
||||
@@ -629,12 +687,18 @@ impl DapLogView {
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
let weak_project = project.downgrade();
|
||||
let state_info = log_store
|
||||
.read(cx)
|
||||
.debug_sessions
|
||||
.back()
|
||||
.map(|session| (session.id, session.has_adapter_logs));
|
||||
.projects
|
||||
.get(&weak_project)
|
||||
.and_then(|project| {
|
||||
project
|
||||
.debug_sessions
|
||||
.values()
|
||||
.next_back()
|
||||
.map(|session| (session.id, session.has_adapter_logs))
|
||||
});
|
||||
|
||||
let mut this = Self {
|
||||
editor,
|
||||
@@ -647,10 +711,14 @@ impl DapLogView {
|
||||
};
|
||||
|
||||
if let Some((session_id, have_adapter_logs)) = state_info {
|
||||
let id = LogStoreEntryIdentifier {
|
||||
session_id,
|
||||
project: Cow::Owned(weak_project),
|
||||
};
|
||||
if have_adapter_logs {
|
||||
this.show_log_messages_for_adapter(session_id, window, cx);
|
||||
this.show_log_messages_for_adapter(&id, window, cx);
|
||||
} else {
|
||||
this.show_rpc_trace_for_server(session_id, window, cx);
|
||||
this.show_rpc_trace_for_server(&id, window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -690,31 +758,38 @@ impl DapLogView {
|
||||
fn menu_items(&self, cx: &App) -> Vec<DapMenuItem> {
|
||||
self.log_store
|
||||
.read(cx)
|
||||
.debug_sessions
|
||||
.iter()
|
||||
.rev()
|
||||
.map(|state| DapMenuItem {
|
||||
session_id: state.id,
|
||||
adapter_name: state.adapter_name.clone(),
|
||||
has_adapter_logs: state.has_adapter_logs,
|
||||
selected_entry: self.current_view.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
.projects
|
||||
.get(&self.project.downgrade())
|
||||
.map_or_else(Vec::new, |state| {
|
||||
state
|
||||
.debug_sessions
|
||||
.values()
|
||||
.rev()
|
||||
.map(|state| DapMenuItem {
|
||||
session_id: state.id,
|
||||
adapter_name: state.adapter_name.clone(),
|
||||
has_adapter_logs: state.has_adapter_logs,
|
||||
selected_entry: self
|
||||
.current_view
|
||||
.map_or(LogKind::Adapter, |(_, kind)| kind),
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
fn show_rpc_trace_for_server(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.rpc_messages_for_session(session_id)
|
||||
.rpc_messages_for_session(id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((session_id, LogKind::Rpc));
|
||||
self.current_view = Some((id.session_id, LogKind::Rpc));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -725,8 +800,7 @@ impl DapLogView {
|
||||
.expect("log buffer should be a singleton")
|
||||
.update(cx, |_, cx| {
|
||||
cx.spawn({
|
||||
let buffer = cx.entity();
|
||||
async move |_, cx| {
|
||||
async move |buffer, cx| {
|
||||
let language = language.await.ok();
|
||||
buffer.update(cx, |buffer, cx| {
|
||||
buffer.set_language(language, cx);
|
||||
@@ -746,17 +820,17 @@ impl DapLogView {
|
||||
|
||||
fn show_log_messages_for_adapter(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let message_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.log_messages_for_session(session_id)
|
||||
.log_messages_for_session(id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(message_log) = message_log {
|
||||
self.current_view = Some((session_id, LogKind::Adapter));
|
||||
self.current_view = Some((id.session_id, LogKind::Adapter));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(message_log, window, cx);
|
||||
editor
|
||||
.read(cx)
|
||||
@@ -775,17 +849,17 @@ impl DapLogView {
|
||||
|
||||
fn show_initialization_sequence_for_server(
|
||||
&mut self,
|
||||
session_id: SessionId,
|
||||
id: &LogStoreEntryIdentifier<'_>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
let rpc_log = self.log_store.update(cx, |log_store, _| {
|
||||
log_store
|
||||
.initialization_sequence_for_session(session_id)
|
||||
.initialization_sequence_for_session(id)
|
||||
.map(|state| log_contents(state.iter().cloned()))
|
||||
});
|
||||
if let Some(rpc_log) = rpc_log {
|
||||
self.current_view = Some((session_id, LogKind::Rpc));
|
||||
self.current_view = Some((id.session_id, LogKind::Rpc));
|
||||
let (editor, editor_subscriptions) = Self::editor_for_logs(rpc_log, window, cx);
|
||||
let language = self.project.read(cx).languages().language_for_name("JSON");
|
||||
editor
|
||||
@@ -993,9 +1067,9 @@ impl Focusable for DapLogView {
|
||||
}
|
||||
}
|
||||
|
||||
pub enum Event {
|
||||
enum Event {
|
||||
NewLogEntry {
|
||||
id: SessionId,
|
||||
id: LogStoreEntryIdentifier<'static>,
|
||||
entry: SharedString,
|
||||
kind: LogKind,
|
||||
},
|
||||
@@ -1008,31 +1082,30 @@ impl EventEmitter<SearchEvent> for DapLogView {}
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
impl LogStore {
|
||||
pub fn contained_session_ids(&self) -> Vec<SessionId> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.map(|session| session.id)
|
||||
.collect()
|
||||
pub fn has_projects(&self) -> bool {
|
||||
!self.projects.is_empty()
|
||||
}
|
||||
|
||||
pub fn rpc_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.find(|adapter_state| adapter_state.id == session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.rpc_messages
|
||||
.messages
|
||||
.clone()
|
||||
.into()
|
||||
pub fn contained_session_ids(&self, project: &WeakEntity<Project>) -> Vec<SessionId> {
|
||||
self.projects.get(project).map_or(vec![], |state| {
|
||||
state.debug_sessions.keys().copied().collect()
|
||||
})
|
||||
}
|
||||
|
||||
pub fn log_messages_for_session_id(&self, session_id: SessionId) -> Vec<SharedString> {
|
||||
self.debug_sessions
|
||||
.iter()
|
||||
.find(|adapter_state| adapter_state.id == session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.log_messages
|
||||
.clone()
|
||||
.into()
|
||||
pub fn rpc_messages_for_session_id(
|
||||
&self,
|
||||
project: &WeakEntity<Project>,
|
||||
session_id: SessionId,
|
||||
) -> Vec<SharedString> {
|
||||
self.projects.get(&project).map_or(vec![], |state| {
|
||||
state
|
||||
.debug_sessions
|
||||
.get(&session_id)
|
||||
.expect("This session should exist if a test is calling")
|
||||
.rpc_messages
|
||||
.messages
|
||||
.clone()
|
||||
.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1298,6 +1298,11 @@ impl Render for DebugPanel {
|
||||
}
|
||||
|
||||
v_flex()
|
||||
.when_else(
|
||||
self.position(window, cx) == DockPosition::Bottom,
|
||||
|this| this.max_h(self.size),
|
||||
|this| this.max_w(self.size),
|
||||
)
|
||||
.size_full()
|
||||
.key_context("DebugPanel")
|
||||
.child(h_flex().children(self.top_controls_strip(window, cx)))
|
||||
|
||||
@@ -900,7 +900,7 @@ impl RunningState {
|
||||
|
||||
|
||||
let config_is_valid = request_type.is_ok();
|
||||
|
||||
let mut extra_config = Value::Null;
|
||||
let build_output = if let Some(build) = build {
|
||||
let (task_template, locator_name) = match build {
|
||||
BuildTaskDefinition::Template {
|
||||
@@ -930,6 +930,7 @@ impl RunningState {
|
||||
};
|
||||
|
||||
let locator_name = if let Some(locator_name) = locator_name {
|
||||
extra_config = config.clone();
|
||||
debug_assert!(!config_is_valid);
|
||||
Some(locator_name)
|
||||
} else if !config_is_valid {
|
||||
@@ -945,6 +946,7 @@ impl RunningState {
|
||||
});
|
||||
if let Ok(t) = task {
|
||||
t.await.and_then(|scenario| {
|
||||
extra_config = scenario.config;
|
||||
match scenario.build {
|
||||
Some(BuildTaskDefinition::Template {
|
||||
locator_name, ..
|
||||
@@ -1008,13 +1010,13 @@ impl RunningState {
|
||||
if !exit_status.success() {
|
||||
anyhow::bail!("Build failed");
|
||||
}
|
||||
Some((task.resolved.clone(), locator_name))
|
||||
Some((task.resolved.clone(), locator_name, extra_config))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
if config_is_valid {
|
||||
} else if let Some((task, locator_name)) = build_output {
|
||||
} else if let Some((task, locator_name, extra_config)) = build_output {
|
||||
let locator_name =
|
||||
locator_name.with_context(|| {
|
||||
format!("Could not find a valid locator for a build task and configure is invalid with error: {}", request_type.err()
|
||||
@@ -1039,6 +1041,8 @@ impl RunningState {
|
||||
.with_context(|| anyhow!("{}: is not a valid adapter name", &adapter))?.config_from_zed_format(zed_config)
|
||||
.await?;
|
||||
config = scenario.config;
|
||||
util::merge_non_null_json_value_into(extra_config, &mut config);
|
||||
|
||||
Self::substitute_variables_in_config(&mut config, &task_context);
|
||||
} else {
|
||||
let Err(e) = request_type else {
|
||||
|
||||
@@ -877,9 +877,27 @@ impl LineBreakpoint {
|
||||
})
|
||||
.cursor_pointer()
|
||||
.child(
|
||||
Label::new(format!("{}:{}", self.name, self.line))
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
h_flex()
|
||||
.gap_0p5()
|
||||
.child(
|
||||
Label::new(format!("{}:{}", self.name, self.line))
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel),
|
||||
)
|
||||
.children(self.dir.as_ref().and_then(|dir| {
|
||||
let path_without_root = Path::new(dir.as_ref())
|
||||
.components()
|
||||
.skip(1)
|
||||
.collect::<PathBuf>();
|
||||
path_without_root.components().next()?;
|
||||
Some(
|
||||
Label::new(path_without_root.to_string_lossy().into_owned())
|
||||
.color(Color::Muted)
|
||||
.size(LabelSize::Small)
|
||||
.line_height_style(ui::LineHeightStyle::UiLabel)
|
||||
.truncate(),
|
||||
)
|
||||
})),
|
||||
)
|
||||
.when_some(self.dir.as_ref(), |this, parent_dir| {
|
||||
this.tooltip(Tooltip::text(format!("Worktree parent path: {parent_dir}")))
|
||||
@@ -1227,14 +1245,15 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
};
|
||||
|
||||
h_flex()
|
||||
.gap_2()
|
||||
.gap_1()
|
||||
.child(
|
||||
div() .map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
|
||||
div().map(self.add_border(ActiveBreakpointStripMode::Log, supports_logs, window, cx))
|
||||
.child(
|
||||
IconButton::new(
|
||||
SharedString::from(format!("{id}-log-toggle")),
|
||||
IconName::ScrollText,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.style(style_for_toggle(ActiveBreakpointStripMode::Log, has_logs))
|
||||
.icon_color(color_for_toggle(has_logs))
|
||||
.disabled(!supports_logs)
|
||||
@@ -1254,6 +1273,7 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
SharedString::from(format!("{id}-condition-toggle")),
|
||||
IconName::SplitAlt,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.style(style_for_toggle(
|
||||
ActiveBreakpointStripMode::Condition,
|
||||
has_condition
|
||||
@@ -1267,7 +1287,7 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
.when(!has_condition && !self.is_selected, |this| this.invisible()),
|
||||
)
|
||||
.child(
|
||||
div() .map(self.add_border(
|
||||
div().map(self.add_border(
|
||||
ActiveBreakpointStripMode::HitCondition,
|
||||
supports_hit_condition,window, cx
|
||||
))
|
||||
@@ -1276,6 +1296,7 @@ impl RenderOnce for BreakpointOptionsStrip {
|
||||
SharedString::from(format!("{id}-hit-condition-toggle")),
|
||||
IconName::ArrowDown10,
|
||||
)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.style(style_for_toggle(
|
||||
ActiveBreakpointStripMode::HitCondition,
|
||||
has_hit_condition,
|
||||
|
||||
@@ -37,15 +37,23 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
|
||||
.await;
|
||||
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids()
|
||||
.is_empty()),
|
||||
"log_store shouldn't contain any session IDs before any sessions were created"
|
||||
log_store.read_with(cx, |log_store, _| !log_store.has_projects()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
|
||||
let project = Project::test(fs, [path!("/project").as_ref()], cx).await;
|
||||
|
||||
let workspace = init_test_workspace(&project, cx).await;
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store.has_projects()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.is_empty()),
|
||||
"log_store shouldn't contain any projects before any projects were created"
|
||||
);
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
|
||||
// Start a debug session
|
||||
@@ -54,20 +62,22 @@ async fn test_dap_logger_captures_all_session_rpc_messages(
|
||||
let client = session.update(cx, |session, _| session.adapter_client().unwrap());
|
||||
|
||||
assert_eq!(
|
||||
log_store.read_with(cx, |log_store, _| log_store.contained_session_ids().len()),
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.len()),
|
||||
1,
|
||||
);
|
||||
|
||||
assert!(
|
||||
log_store.read_with(cx, |log_store, _| log_store
|
||||
.contained_session_ids()
|
||||
.contained_session_ids(&project.downgrade())
|
||||
.contains(&session_id)),
|
||||
"log_store should contain the session IDs of the started session"
|
||||
);
|
||||
|
||||
assert!(
|
||||
!log_store.read_with(cx, |log_store, _| log_store
|
||||
.rpc_messages_for_session_id(session_id)
|
||||
.rpc_messages_for_session_id(&project.downgrade(), session_id)
|
||||
.is_empty()),
|
||||
"We should have the initialization sequence in the log store"
|
||||
);
|
||||
|
||||
@@ -267,7 +267,6 @@ async fn test_dap_adapter_config_conversion_and_validation(cx: &mut TestAppConte
|
||||
"Debugpy",
|
||||
"PHP",
|
||||
"JavaScript",
|
||||
"Ruby",
|
||||
"Delve",
|
||||
"GDB",
|
||||
"fake-adapter",
|
||||
|
||||
@@ -61,6 +61,7 @@ parking_lot.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
project.workspace = true
|
||||
rand.workspace = true
|
||||
regex.workspace = true
|
||||
rpc.workspace = true
|
||||
schemars.workspace = true
|
||||
serde.workspace = true
|
||||
|
||||
@@ -37,7 +37,9 @@ pub use block_map::{
|
||||
use block_map::{BlockRow, BlockSnapshot};
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use crease_map::*;
|
||||
pub use fold_map::{ChunkRenderer, ChunkRendererContext, Fold, FoldId, FoldPlaceholder, FoldPoint};
|
||||
pub use fold_map::{
|
||||
ChunkRenderer, ChunkRendererContext, ChunkRendererId, Fold, FoldId, FoldPlaceholder, FoldPoint,
|
||||
};
|
||||
use fold_map::{FoldMap, FoldSnapshot};
|
||||
use gpui::{App, Context, Entity, Font, HighlightStyle, LineLayout, Pixels, UnderlineStyle};
|
||||
pub use inlay_map::Inlay;
|
||||
@@ -538,7 +540,7 @@ impl DisplayMap {
|
||||
|
||||
pub fn update_fold_widths(
|
||||
&mut self,
|
||||
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
|
||||
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
let snapshot = self.buffer.read(cx).snapshot(cx);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::display_map::inlay_map::InlayChunk;
|
||||
use crate::{InlayId, display_map::inlay_map::InlayChunk};
|
||||
|
||||
use super::{
|
||||
Highlights,
|
||||
@@ -277,13 +277,16 @@ impl FoldMapWriter<'_> {
|
||||
|
||||
pub(crate) fn update_fold_widths(
|
||||
&mut self,
|
||||
new_widths: impl IntoIterator<Item = (FoldId, Pixels)>,
|
||||
new_widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
|
||||
) -> (FoldSnapshot, Vec<FoldEdit>) {
|
||||
let mut edits = Vec::new();
|
||||
let inlay_snapshot = self.0.snapshot.inlay_snapshot.clone();
|
||||
let buffer = &inlay_snapshot.buffer;
|
||||
|
||||
for (id, new_width) in new_widths {
|
||||
let ChunkRendererId::Fold(id) = id else {
|
||||
continue;
|
||||
};
|
||||
if let Some(metadata) = self.0.snapshot.fold_metadata_by_id.get(&id).cloned() {
|
||||
if Some(new_width) != metadata.width {
|
||||
let buffer_start = metadata.range.start.to_offset(buffer);
|
||||
@@ -529,7 +532,7 @@ impl FoldMap {
|
||||
placeholder: Some(TransformPlaceholder {
|
||||
text: ELLIPSIS,
|
||||
renderer: ChunkRenderer {
|
||||
id: fold.id,
|
||||
id: ChunkRendererId::Fold(fold.id),
|
||||
render: Arc::new(move |cx| {
|
||||
(fold.placeholder.render)(
|
||||
fold_id,
|
||||
@@ -1267,11 +1270,17 @@ pub struct Chunk<'a> {
|
||||
pub renderer: Option<ChunkRenderer>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord)]
|
||||
pub enum ChunkRendererId {
|
||||
Fold(FoldId),
|
||||
Inlay(InlayId),
|
||||
}
|
||||
|
||||
/// A recipe for how the chunk should be presented.
|
||||
#[derive(Clone)]
|
||||
pub struct ChunkRenderer {
|
||||
/// The id of the fold associated with this chunk.
|
||||
pub id: FoldId,
|
||||
/// The id of the renderer associated with this chunk.
|
||||
pub id: ChunkRendererId,
|
||||
/// Creates a custom element to represent this chunk.
|
||||
pub render: Arc<dyn Send + Sync + Fn(&mut ChunkRendererContext) -> AnyElement>,
|
||||
/// If true, the element is constrained to the shaped width of the text.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::{ChunkRenderer, HighlightStyles, InlayId, display_map::FoldId};
|
||||
use crate::{ChunkRenderer, HighlightStyles, InlayId};
|
||||
use collections::BTreeSet;
|
||||
use gpui::{Hsla, Rgba};
|
||||
use language::{Chunk, Edit, Point, TextSummary};
|
||||
@@ -14,7 +14,7 @@ use sum_tree::{Bias, Cursor, SumTree};
|
||||
use text::{Patch, Rope};
|
||||
use ui::{ActiveTheme, IntoElement as _, ParentElement as _, Styled as _, div};
|
||||
|
||||
use super::{Highlights, custom_highlights::CustomHighlightsChunks};
|
||||
use super::{Highlights, custom_highlights::CustomHighlightsChunks, fold_map::ChunkRendererId};
|
||||
|
||||
/// Decides where the [`Inlay`]s should be displayed.
|
||||
///
|
||||
@@ -338,22 +338,20 @@ impl<'a> Iterator for InlayChunks<'a> {
|
||||
}
|
||||
InlayId::Hint(_) => self.highlight_styles.inlay_hint,
|
||||
InlayId::DebuggerValue(_) => self.highlight_styles.inlay_hint,
|
||||
InlayId::Color(id) => {
|
||||
InlayId::Color(_) => {
|
||||
if let Some(color) = inlay.color {
|
||||
renderer = Some(ChunkRenderer {
|
||||
id: FoldId(id),
|
||||
id: ChunkRendererId::Inlay(inlay.id),
|
||||
render: Arc::new(move |cx| {
|
||||
div()
|
||||
.w_4()
|
||||
.h_4()
|
||||
.relative()
|
||||
.size_3p5()
|
||||
.child(
|
||||
div()
|
||||
.absolute()
|
||||
.right_1()
|
||||
.w_3p5()
|
||||
.h_3p5()
|
||||
.border_2()
|
||||
.size_3()
|
||||
.border_1()
|
||||
.border_color(cx.theme().colors().border)
|
||||
.bg(color),
|
||||
)
|
||||
|
||||
@@ -11541,66 +11541,90 @@ impl Editor {
|
||||
let language_settings = buffer.language_settings_at(selection.head(), cx);
|
||||
let language_scope = buffer.language_scope_at(selection.head());
|
||||
|
||||
let indent_and_prefix_for_row =
|
||||
|row: u32| -> (IndentSize, Option<String>, Option<String>) {
|
||||
let indent = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
let (comment_prefix, rewrap_prefix) =
|
||||
if let Some(language_scope) = &language_scope {
|
||||
let indent_end = Point::new(row, indent.len);
|
||||
let comment_prefix = language_scope
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.map(|prefix| prefix.to_string());
|
||||
let line_end = Point::new(row, buffer.line_len(MultiBufferRow(row)));
|
||||
let line_text_after_indent = buffer
|
||||
.text_for_range(indent_end..line_end)
|
||||
.collect::<String>();
|
||||
let rewrap_prefix = language_scope
|
||||
.rewrap_prefixes()
|
||||
.iter()
|
||||
.find_map(|prefix_regex| {
|
||||
prefix_regex.find(&line_text_after_indent).map(|mat| {
|
||||
if mat.start() == 0 {
|
||||
Some(mat.as_str().to_string())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.flatten();
|
||||
(comment_prefix, rewrap_prefix)
|
||||
} else {
|
||||
(None, None)
|
||||
};
|
||||
(indent, comment_prefix, rewrap_prefix)
|
||||
};
|
||||
|
||||
let mut ranges = Vec::new();
|
||||
let mut current_range_start = first_row;
|
||||
let from_empty_selection = selection.is_empty();
|
||||
|
||||
let mut current_range_start = first_row;
|
||||
let mut prev_row = first_row;
|
||||
let mut prev_indent = buffer.indent_size_for_line(MultiBufferRow(first_row));
|
||||
let mut prev_comment_prefix = if let Some(language_scope) = &language_scope {
|
||||
let indent = buffer.indent_size_for_line(MultiBufferRow(first_row));
|
||||
let indent_end = Point::new(first_row, indent.len);
|
||||
language_scope
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (
|
||||
mut current_range_indent,
|
||||
mut current_range_comment_prefix,
|
||||
mut current_range_rewrap_prefix,
|
||||
) = indent_and_prefix_for_row(first_row);
|
||||
|
||||
for row in non_blank_rows_iter.skip(1) {
|
||||
let has_paragraph_break = row > prev_row + 1;
|
||||
|
||||
let row_indent = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
let row_comment_prefix = if let Some(language_scope) = &language_scope {
|
||||
let indent = buffer.indent_size_for_line(MultiBufferRow(row));
|
||||
let indent_end = Point::new(row, indent.len);
|
||||
language_scope
|
||||
.line_comment_prefixes()
|
||||
.iter()
|
||||
.find(|prefix| buffer.contains_str_at(indent_end, prefix))
|
||||
.cloned()
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let (row_indent, row_comment_prefix, row_rewrap_prefix) =
|
||||
indent_and_prefix_for_row(row);
|
||||
|
||||
let has_boundary_change =
|
||||
row_indent != prev_indent || row_comment_prefix != prev_comment_prefix;
|
||||
let has_indent_change = row_indent != current_range_indent;
|
||||
let has_comment_change = row_comment_prefix != current_range_comment_prefix;
|
||||
|
||||
let has_boundary_change = has_comment_change
|
||||
|| row_rewrap_prefix.is_some()
|
||||
|| (has_indent_change && current_range_comment_prefix.is_some());
|
||||
|
||||
if has_paragraph_break || has_boundary_change {
|
||||
ranges.push((
|
||||
language_settings.clone(),
|
||||
Point::new(current_range_start, 0)
|
||||
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
|
||||
prev_indent,
|
||||
prev_comment_prefix.clone(),
|
||||
current_range_indent,
|
||||
current_range_comment_prefix.clone(),
|
||||
current_range_rewrap_prefix.clone(),
|
||||
from_empty_selection,
|
||||
));
|
||||
current_range_start = row;
|
||||
current_range_indent = row_indent;
|
||||
current_range_comment_prefix = row_comment_prefix;
|
||||
current_range_rewrap_prefix = row_rewrap_prefix;
|
||||
}
|
||||
|
||||
prev_row = row;
|
||||
prev_indent = row_indent;
|
||||
prev_comment_prefix = row_comment_prefix;
|
||||
}
|
||||
|
||||
ranges.push((
|
||||
language_settings.clone(),
|
||||
Point::new(current_range_start, 0)
|
||||
..Point::new(prev_row, buffer.line_len(MultiBufferRow(prev_row))),
|
||||
prev_indent,
|
||||
prev_comment_prefix,
|
||||
current_range_indent,
|
||||
current_range_comment_prefix,
|
||||
current_range_rewrap_prefix,
|
||||
from_empty_selection,
|
||||
));
|
||||
|
||||
@@ -11610,8 +11634,14 @@ impl Editor {
|
||||
let mut edits = Vec::new();
|
||||
let mut rewrapped_row_ranges = Vec::<RangeInclusive<u32>>::new();
|
||||
|
||||
for (language_settings, wrap_range, indent_size, comment_prefix, from_empty_selection) in
|
||||
wrap_ranges
|
||||
for (
|
||||
language_settings,
|
||||
wrap_range,
|
||||
indent_size,
|
||||
comment_prefix,
|
||||
rewrap_prefix,
|
||||
from_empty_selection,
|
||||
) in wrap_ranges
|
||||
{
|
||||
let mut start_row = wrap_range.start.row;
|
||||
let mut end_row = wrap_range.end.row;
|
||||
@@ -11627,12 +11657,16 @@ impl Editor {
|
||||
|
||||
let tab_size = language_settings.tab_size;
|
||||
|
||||
let mut line_prefix = indent_size.chars().collect::<String>();
|
||||
let indent_prefix = indent_size.chars().collect::<String>();
|
||||
let mut line_prefix = indent_prefix.clone();
|
||||
let mut inside_comment = false;
|
||||
if let Some(prefix) = &comment_prefix {
|
||||
line_prefix.push_str(prefix);
|
||||
inside_comment = true;
|
||||
}
|
||||
if let Some(prefix) = &rewrap_prefix {
|
||||
line_prefix.push_str(prefix);
|
||||
}
|
||||
|
||||
let allow_rewrap_based_on_language = match language_settings.allow_rewrap {
|
||||
RewrapBehavior::InComments => inside_comment,
|
||||
@@ -11679,12 +11713,18 @@ impl Editor {
|
||||
let selection_text = buffer.text_for_range(start..end).collect::<String>();
|
||||
let Some(lines_without_prefixes) = selection_text
|
||||
.lines()
|
||||
.map(|line| {
|
||||
line.strip_prefix(&line_prefix)
|
||||
.or_else(|| line.trim_start().strip_prefix(&line_prefix.trim_start()))
|
||||
.with_context(|| {
|
||||
format!("line did not start with prefix {line_prefix:?}: {line:?}")
|
||||
})
|
||||
.enumerate()
|
||||
.map(|(ix, line)| {
|
||||
let line_trimmed = line.trim_start();
|
||||
if rewrap_prefix.is_some() && ix > 0 {
|
||||
Ok(line_trimmed)
|
||||
} else {
|
||||
line_trimmed
|
||||
.strip_prefix(&line_prefix.trim_start())
|
||||
.with_context(|| {
|
||||
format!("line did not start with prefix {line_prefix:?}: {line:?}")
|
||||
})
|
||||
}
|
||||
})
|
||||
.collect::<Result<Vec<_>, _>>()
|
||||
.log_err()
|
||||
@@ -11697,8 +11737,16 @@ impl Editor {
|
||||
.language_settings_at(Point::new(start_row, 0), cx)
|
||||
.preferred_line_length as usize
|
||||
});
|
||||
|
||||
let subsequent_lines_prefix = if let Some(rewrap_prefix_str) = &rewrap_prefix {
|
||||
format!("{}{}", indent_prefix, " ".repeat(rewrap_prefix_str.len()))
|
||||
} else {
|
||||
line_prefix.clone()
|
||||
};
|
||||
|
||||
let wrapped_text = wrap_with_prefix(
|
||||
line_prefix,
|
||||
subsequent_lines_prefix,
|
||||
lines_without_prefixes.join("\n"),
|
||||
wrap_column,
|
||||
tab_size,
|
||||
@@ -17333,9 +17381,9 @@ impl Editor {
|
||||
self.active_indent_guides_state.dirty = true;
|
||||
}
|
||||
|
||||
pub fn update_fold_widths(
|
||||
pub fn update_renderer_widths(
|
||||
&mut self,
|
||||
widths: impl IntoIterator<Item = (FoldId, Pixels)>,
|
||||
widths: impl IntoIterator<Item = (ChunkRendererId, Pixels)>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> bool {
|
||||
self.display_map
|
||||
@@ -21200,18 +21248,22 @@ fn test_word_breaking_tokenizer() {
|
||||
}
|
||||
|
||||
fn wrap_with_prefix(
|
||||
line_prefix: String,
|
||||
first_line_prefix: String,
|
||||
subsequent_lines_prefix: String,
|
||||
unwrapped_text: String,
|
||||
wrap_column: usize,
|
||||
tab_size: NonZeroU32,
|
||||
preserve_existing_whitespace: bool,
|
||||
) -> String {
|
||||
let line_prefix_len = char_len_with_expanded_tabs(0, &line_prefix, tab_size);
|
||||
let first_line_prefix_len = char_len_with_expanded_tabs(0, &first_line_prefix, tab_size);
|
||||
let subsequent_lines_prefix_len =
|
||||
char_len_with_expanded_tabs(0, &subsequent_lines_prefix, tab_size);
|
||||
let mut wrapped_text = String::new();
|
||||
let mut current_line = line_prefix.clone();
|
||||
let mut current_line = first_line_prefix.clone();
|
||||
let mut is_first_line = true;
|
||||
|
||||
let tokenizer = WordBreakingTokenizer::new(&unwrapped_text);
|
||||
let mut current_line_len = line_prefix_len;
|
||||
let mut current_line_len = first_line_prefix_len;
|
||||
let mut in_whitespace = false;
|
||||
for token in tokenizer {
|
||||
let have_preceding_whitespace = in_whitespace;
|
||||
@@ -21221,13 +21273,19 @@ fn wrap_with_prefix(
|
||||
grapheme_len,
|
||||
} => {
|
||||
in_whitespace = false;
|
||||
let current_prefix_len = if is_first_line {
|
||||
first_line_prefix_len
|
||||
} else {
|
||||
subsequent_lines_prefix_len
|
||||
};
|
||||
if current_line_len + grapheme_len > wrap_column
|
||||
&& current_line_len != line_prefix_len
|
||||
&& current_line_len != current_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
is_first_line = false;
|
||||
current_line = subsequent_lines_prefix.clone();
|
||||
current_line_len = subsequent_lines_prefix_len;
|
||||
}
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
@@ -21244,32 +21302,46 @@ fn wrap_with_prefix(
|
||||
token = " ";
|
||||
grapheme_len = 1;
|
||||
}
|
||||
let current_prefix_len = if is_first_line {
|
||||
first_line_prefix_len
|
||||
} else {
|
||||
subsequent_lines_prefix_len
|
||||
};
|
||||
if current_line_len + grapheme_len > wrap_column {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len || preserve_existing_whitespace {
|
||||
is_first_line = false;
|
||||
current_line = subsequent_lines_prefix.clone();
|
||||
current_line_len = subsequent_lines_prefix_len;
|
||||
} else if current_line_len != current_prefix_len || preserve_existing_whitespace {
|
||||
current_line.push_str(token);
|
||||
current_line_len += grapheme_len;
|
||||
}
|
||||
}
|
||||
WordBreakToken::Newline => {
|
||||
in_whitespace = true;
|
||||
let current_prefix_len = if is_first_line {
|
||||
first_line_prefix_len
|
||||
} else {
|
||||
subsequent_lines_prefix_len
|
||||
};
|
||||
if preserve_existing_whitespace {
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
is_first_line = false;
|
||||
current_line = subsequent_lines_prefix.clone();
|
||||
current_line_len = subsequent_lines_prefix_len;
|
||||
} else if have_preceding_whitespace {
|
||||
continue;
|
||||
} else if current_line_len + 1 > wrap_column && current_line_len != line_prefix_len
|
||||
} else if current_line_len + 1 > wrap_column
|
||||
&& current_line_len != current_prefix_len
|
||||
{
|
||||
wrapped_text.push_str(current_line.trim_end());
|
||||
wrapped_text.push('\n');
|
||||
current_line.truncate(line_prefix.len());
|
||||
current_line_len = line_prefix_len;
|
||||
} else if current_line_len != line_prefix_len {
|
||||
is_first_line = false;
|
||||
current_line = subsequent_lines_prefix.clone();
|
||||
current_line_len = subsequent_lines_prefix_len;
|
||||
} else if current_line_len != current_prefix_len {
|
||||
current_line.push(' ');
|
||||
current_line_len += 1;
|
||||
}
|
||||
@@ -21287,6 +21359,7 @@ fn wrap_with_prefix(
|
||||
fn test_wrap_with_prefix() {
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"# ".to_string(),
|
||||
"# ".to_string(),
|
||||
"abcdefg".to_string(),
|
||||
4,
|
||||
@@ -21297,6 +21370,7 @@ fn test_wrap_with_prefix() {
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"".to_string(),
|
||||
"".to_string(),
|
||||
"\thello world".to_string(),
|
||||
8,
|
||||
@@ -21307,6 +21381,7 @@ fn test_wrap_with_prefix() {
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
"// ".to_string(),
|
||||
"// ".to_string(),
|
||||
"xx \nyy zz aa bb cc".to_string(),
|
||||
12,
|
||||
@@ -21317,6 +21392,7 @@ fn test_wrap_with_prefix() {
|
||||
);
|
||||
assert_eq!(
|
||||
wrap_with_prefix(
|
||||
String::new(),
|
||||
String::new(),
|
||||
"这是什么 \n 钢笔".to_string(),
|
||||
3,
|
||||
|
||||
@@ -378,7 +378,6 @@ pub enum SnippetSortOrder {
|
||||
}
|
||||
|
||||
#[derive(Clone, Default, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct EditorSettingsContent {
|
||||
/// Whether the cursor blinks in the editor.
|
||||
///
|
||||
|
||||
@@ -3,7 +3,7 @@ use std::sync::Arc;
|
||||
use gpui::{App, FontFeatures, FontWeight};
|
||||
use project::project_settings::{InlineBlameSettings, ProjectSettings};
|
||||
use settings::{EditableSettingControl, Settings};
|
||||
use theme::{FontFamilyCache, ThemeSettings};
|
||||
use theme::{FontFamilyCache, FontFamilyName, ThemeSettings};
|
||||
use ui::{
|
||||
CheckboxWithLabel, ContextMenu, DropdownMenu, NumericStepper, SettingsContainer, SettingsGroup,
|
||||
prelude::*,
|
||||
@@ -75,7 +75,7 @@ impl EditableSettingControl for BufferFontFamilyControl {
|
||||
value: Self::Value,
|
||||
_cx: &App,
|
||||
) {
|
||||
settings.buffer_font_family = Some(value.to_string());
|
||||
settings.buffer_font_family = Some(FontFamilyName(value.into()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -30,7 +30,7 @@ use language::{
|
||||
},
|
||||
tree_sitter_python,
|
||||
};
|
||||
use language_settings::{Formatter, FormatterList, IndentGuideSettings};
|
||||
use language_settings::{Formatter, IndentGuideSettings};
|
||||
use lsp::CompletionParams;
|
||||
use multi_buffer::{IndentGuide, PathKey};
|
||||
use parking_lot::Mutex;
|
||||
@@ -3567,7 +3567,7 @@ async fn test_indent_outdent_with_hard_tabs(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
fn test_indent_outdent_with_excerpts(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.languages.extend([
|
||||
settings.languages.0.extend([
|
||||
(
|
||||
"TOML".into(),
|
||||
LanguageSettingsContent {
|
||||
@@ -5145,7 +5145,7 @@ fn test_transpose(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.languages.extend([
|
||||
settings.languages.0.extend([
|
||||
(
|
||||
"Markdown".into(),
|
||||
LanguageSettingsContent {
|
||||
@@ -5210,6 +5210,10 @@ async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
let markdown_language = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "Markdown".into(),
|
||||
rewrap_prefixes: vec![
|
||||
regex::Regex::new("\\d+\\.\\s+").unwrap(),
|
||||
regex::Regex::new("[-*+]\\s+").unwrap(),
|
||||
],
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
None,
|
||||
@@ -5372,7 +5376,82 @@ async fn test_rewrap(cx: &mut TestAppContext) {
|
||||
A long long long line of markdown text
|
||||
to wrap.ˇ
|
||||
"},
|
||||
markdown_language,
|
||||
markdown_language.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// Test that rewrapping boundary works and preserves relative indent for Markdown documents
|
||||
assert_rewrap(
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is very long and needs to be wrapped properly.
|
||||
2. This is a numbered list item that is very long and needs to be wrapped properly.
|
||||
- This is an unordered list item that is also very long and should not merge with the numbered item.ˇ»
|
||||
"},
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
2. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
- This is an unordered list item that is
|
||||
also very long and should not merge
|
||||
with the numbered item.ˇ»
|
||||
"},
|
||||
markdown_language.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// Test that rewrapping add indents for rewrapping boundary if not exists already.
|
||||
assert_rewrap(
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
2. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
- This is an unordered list item that is
|
||||
also very long and should not merge with
|
||||
the numbered item.ˇ»
|
||||
"},
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
2. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
- This is an unordered list item that is
|
||||
also very long and should not merge
|
||||
with the numbered item.ˇ»
|
||||
"},
|
||||
markdown_language.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
// Test that rewrapping maintain indents even when they already exists.
|
||||
assert_rewrap(
|
||||
indoc! {"
|
||||
«1. This is a numbered list
|
||||
item that is very long and needs to be wrapped properly.
|
||||
2. This is a numbered list
|
||||
item that is very long and needs to be wrapped properly.
|
||||
- This is an unordered list item that is also very long and
|
||||
should not merge with the numbered item.ˇ»
|
||||
"},
|
||||
indoc! {"
|
||||
«1. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
2. This is a numbered list item that is
|
||||
very long and needs to be wrapped
|
||||
properly.
|
||||
- This is an unordered list item that is
|
||||
also very long and should not merge
|
||||
with the numbered item.ˇ»
|
||||
"},
|
||||
markdown_language.clone(),
|
||||
&mut cx,
|
||||
);
|
||||
|
||||
@@ -9326,7 +9405,7 @@ async fn test_document_format_during_save(cx: &mut TestAppContext) {
|
||||
|
||||
// Set rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
settings.languages.0.insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: NonZeroU32::new(8),
|
||||
@@ -9890,7 +9969,7 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
|
||||
|
||||
// Set Rust language override and assert overridden tabsize is sent to language server
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.languages.insert(
|
||||
settings.languages.0.insert(
|
||||
"Rust".into(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: NonZeroU32::new(8),
|
||||
@@ -9933,9 +10012,9 @@ async fn test_range_format_during_save(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
|
||||
))
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
]))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -10062,21 +10141,17 @@ async fn test_document_format_manual_trigger(cx: &mut TestAppContext) {
|
||||
async fn test_multiple_formatters(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.remove_trailing_whitespace_on_save = Some(true);
|
||||
settings.defaults.formatter =
|
||||
Some(language_settings::SelectedFormatter::List(FormatterList(
|
||||
vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::CodeActions(
|
||||
[
|
||||
("code-action-1".into(), true),
|
||||
("code-action-2".into(), true),
|
||||
]
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::CodeActions(
|
||||
[
|
||||
("code-action-1".into(), true),
|
||||
("code-action-2".into(), true),
|
||||
]
|
||||
.into(),
|
||||
)))
|
||||
.into_iter()
|
||||
.collect(),
|
||||
),
|
||||
]))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -10328,9 +10403,9 @@ async fn test_multiple_formatters(cx: &mut TestAppContext) {
|
||||
#[gpui::test]
|
||||
async fn test_organize_imports_manual_trigger(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::LanguageServer { name: None }].into()),
|
||||
))
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
]))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
@@ -14905,7 +14980,7 @@ async fn test_language_server_restart_due_to_settings_change(cx: &mut TestAppCon
|
||||
.unwrap();
|
||||
let _fake_server = fake_servers.next().await.unwrap();
|
||||
update_test_language_settings(cx, |language_settings| {
|
||||
language_settings.languages.insert(
|
||||
language_settings.languages.0.insert(
|
||||
language_name.clone(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: NonZeroU32::new(8),
|
||||
@@ -15803,9 +15878,9 @@ fn completion_menu_entries(menu: &CompletionsMenu) -> Vec<String> {
|
||||
#[gpui::test]
|
||||
async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(
|
||||
FormatterList(vec![Formatter::Prettier].into()),
|
||||
))
|
||||
settings.defaults.formatter = Some(language_settings::SelectedFormatter::List(vec![
|
||||
Formatter::Prettier,
|
||||
]))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
|
||||
@@ -12,8 +12,8 @@ use crate::{
|
||||
ToggleFold,
|
||||
code_context_menus::{CodeActionsMenu, MENU_ASIDE_MAX_WIDTH, MENU_ASIDE_MIN_WIDTH, MENU_GAP},
|
||||
display_map::{
|
||||
Block, BlockContext, BlockStyle, DisplaySnapshot, EditorMargins, FoldId, HighlightKey,
|
||||
HighlightedChunk, ToDisplayPoint,
|
||||
Block, BlockContext, BlockStyle, ChunkRendererId, DisplaySnapshot, EditorMargins,
|
||||
HighlightKey, HighlightedChunk, ToDisplayPoint,
|
||||
},
|
||||
editor_settings::{
|
||||
CurrentLineHighlight, DocumentColorsRenderMode, DoubleClickInMultibuffer, Minimap,
|
||||
@@ -7119,7 +7119,7 @@ pub(crate) struct LineWithInvisibles {
|
||||
enum LineFragment {
|
||||
Text(ShapedLine),
|
||||
Element {
|
||||
id: FoldId,
|
||||
id: ChunkRendererId,
|
||||
element: Option<AnyElement>,
|
||||
size: Size<Pixels>,
|
||||
len: usize,
|
||||
@@ -8297,7 +8297,7 @@ impl Element for EditorElement {
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
let new_fold_widths = line_layouts
|
||||
let new_renrerer_widths = line_layouts
|
||||
.iter()
|
||||
.flat_map(|layout| &layout.fragments)
|
||||
.filter_map(|fragment| {
|
||||
@@ -8308,7 +8308,7 @@ impl Element for EditorElement {
|
||||
}
|
||||
});
|
||||
if self.editor.update(cx, |editor, cx| {
|
||||
editor.update_fold_widths(new_fold_widths, cx)
|
||||
editor.update_renderer_widths(new_renrerer_widths, cx)
|
||||
}) {
|
||||
// If the fold widths have changed, we need to prepaint
|
||||
// the element again to account for any changes in
|
||||
|
||||
@@ -19,18 +19,21 @@ use crate::{
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(super) struct LspColorData {
|
||||
cache_version_used: usize,
|
||||
buffer_colors: HashMap<BufferId, BufferColors>,
|
||||
render_mode: DocumentColorsRenderMode,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default)]
|
||||
struct BufferColors {
|
||||
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
|
||||
inlay_colors: HashMap<InlayId, usize>,
|
||||
render_mode: DocumentColorsRenderMode,
|
||||
cache_version_used: usize,
|
||||
}
|
||||
|
||||
impl LspColorData {
|
||||
pub fn new(cx: &App) -> Self {
|
||||
Self {
|
||||
cache_version_used: 0,
|
||||
colors: Vec::new(),
|
||||
inlay_colors: HashMap::default(),
|
||||
buffer_colors: HashMap::default(),
|
||||
render_mode: EditorSettings::get_global(cx).lsp_document_colors,
|
||||
}
|
||||
}
|
||||
@@ -47,8 +50,9 @@ impl LspColorData {
|
||||
DocumentColorsRenderMode::Inlay => Some(InlaySplice {
|
||||
to_remove: Vec::new(),
|
||||
to_insert: self
|
||||
.colors
|
||||
.buffer_colors
|
||||
.iter()
|
||||
.flat_map(|(_, buffer_colors)| buffer_colors.colors.iter())
|
||||
.map(|(range, color, id)| {
|
||||
Inlay::color(
|
||||
id.id(),
|
||||
@@ -63,33 +67,49 @@ impl LspColorData {
|
||||
})
|
||||
.collect(),
|
||||
}),
|
||||
DocumentColorsRenderMode::None => {
|
||||
self.colors.clear();
|
||||
Some(InlaySplice {
|
||||
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
|
||||
to_insert: Vec::new(),
|
||||
})
|
||||
}
|
||||
DocumentColorsRenderMode::None => Some(InlaySplice {
|
||||
to_remove: self
|
||||
.buffer_colors
|
||||
.drain()
|
||||
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors)
|
||||
.map(|(id, _)| id)
|
||||
.collect(),
|
||||
to_insert: Vec::new(),
|
||||
}),
|
||||
DocumentColorsRenderMode::Border | DocumentColorsRenderMode::Background => {
|
||||
Some(InlaySplice {
|
||||
to_remove: self.inlay_colors.drain().map(|(id, _)| id).collect(),
|
||||
to_remove: self
|
||||
.buffer_colors
|
||||
.iter_mut()
|
||||
.flat_map(|(_, buffer_colors)| buffer_colors.inlay_colors.drain())
|
||||
.map(|(id, _)| id)
|
||||
.collect(),
|
||||
to_insert: Vec::new(),
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn set_colors(&mut self, colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>) -> bool {
|
||||
if self.colors == colors {
|
||||
fn set_colors(
|
||||
&mut self,
|
||||
buffer_id: BufferId,
|
||||
colors: Vec<(Range<Anchor>, DocumentColor, InlayId)>,
|
||||
cache_version: Option<usize>,
|
||||
) -> bool {
|
||||
let buffer_colors = self.buffer_colors.entry(buffer_id).or_default();
|
||||
if let Some(cache_version) = cache_version {
|
||||
buffer_colors.cache_version_used = cache_version;
|
||||
}
|
||||
if buffer_colors.colors == colors {
|
||||
return false;
|
||||
}
|
||||
|
||||
self.inlay_colors = colors
|
||||
buffer_colors.inlay_colors = colors
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, (_, _, id))| (*id, i))
|
||||
.collect();
|
||||
self.colors = colors;
|
||||
buffer_colors.colors = colors;
|
||||
true
|
||||
}
|
||||
|
||||
@@ -103,8 +123,9 @@ impl LspColorData {
|
||||
{
|
||||
Vec::new()
|
||||
} else {
|
||||
self.colors
|
||||
self.buffer_colors
|
||||
.iter()
|
||||
.flat_map(|(_, buffer_colors)| &buffer_colors.colors)
|
||||
.map(|(range, color, _)| {
|
||||
let display_range = range.clone().to_display_points(snapshot);
|
||||
let color = Hsla::from(Rgba {
|
||||
@@ -162,10 +183,9 @@ impl Editor {
|
||||
ColorFetchStrategy::IgnoreCache
|
||||
} else {
|
||||
ColorFetchStrategy::UseCache {
|
||||
known_cache_version: self
|
||||
.colors
|
||||
.as_ref()
|
||||
.map(|colors| colors.cache_version_used),
|
||||
known_cache_version: self.colors.as_ref().and_then(|colors| {
|
||||
Some(colors.buffer_colors.get(&buffer_id)?.cache_version_used)
|
||||
}),
|
||||
}
|
||||
};
|
||||
let colors_task = lsp_store.document_colors(fetch_strategy, buffer, cx)?;
|
||||
@@ -201,15 +221,13 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut cache_version = None;
|
||||
let mut new_editor_colors = Vec::<(Range<Anchor>, DocumentColor)>::new();
|
||||
let mut new_editor_colors = HashMap::default();
|
||||
for (buffer_id, colors) in all_colors {
|
||||
let Some(excerpts) = editor_excerpts.get(&buffer_id) else {
|
||||
continue;
|
||||
};
|
||||
match colors {
|
||||
Ok(colors) => {
|
||||
cache_version = colors.cache_version;
|
||||
for color in colors.colors {
|
||||
let color_start = point_from_lsp(color.lsp_range.start);
|
||||
let color_end = point_from_lsp(color.lsp_range.end);
|
||||
@@ -243,8 +261,15 @@ impl Editor {
|
||||
continue;
|
||||
};
|
||||
|
||||
let new_entry =
|
||||
new_editor_colors.entry(buffer_id).or_insert_with(|| {
|
||||
(Vec::<(Range<Anchor>, DocumentColor)>::new(), None)
|
||||
});
|
||||
new_entry.1 = colors.cache_version;
|
||||
let new_buffer_colors = &mut new_entry.0;
|
||||
|
||||
let (Ok(i) | Err(i)) =
|
||||
new_editor_colors.binary_search_by(|(probe, _)| {
|
||||
new_buffer_colors.binary_search_by(|(probe, _)| {
|
||||
probe
|
||||
.start
|
||||
.cmp(&color_start_anchor, &multi_buffer_snapshot)
|
||||
@@ -254,7 +279,7 @@ impl Editor {
|
||||
.cmp(&color_end_anchor, &multi_buffer_snapshot)
|
||||
})
|
||||
});
|
||||
new_editor_colors
|
||||
new_buffer_colors
|
||||
.insert(i, (color_start_anchor..color_end_anchor, color));
|
||||
break;
|
||||
}
|
||||
@@ -267,45 +292,70 @@ impl Editor {
|
||||
editor
|
||||
.update(cx, |editor, cx| {
|
||||
let mut colors_splice = InlaySplice::default();
|
||||
let mut new_color_inlays = Vec::with_capacity(new_editor_colors.len());
|
||||
let Some(colors) = &mut editor.colors else {
|
||||
return;
|
||||
};
|
||||
let mut existing_colors = colors.colors.iter().peekable();
|
||||
for (new_range, new_color) in new_editor_colors {
|
||||
let rgba_color = Rgba {
|
||||
r: new_color.color.red,
|
||||
g: new_color.color.green,
|
||||
b: new_color.color.blue,
|
||||
a: new_color.color.alpha,
|
||||
};
|
||||
let mut updated = false;
|
||||
for (buffer_id, (new_buffer_colors, new_cache_version)) in new_editor_colors {
|
||||
let mut new_buffer_color_inlays =
|
||||
Vec::with_capacity(new_buffer_colors.len());
|
||||
let mut existing_buffer_colors = colors
|
||||
.buffer_colors
|
||||
.entry(buffer_id)
|
||||
.or_default()
|
||||
.colors
|
||||
.iter()
|
||||
.peekable();
|
||||
for (new_range, new_color) in new_buffer_colors {
|
||||
let rgba_color = Rgba {
|
||||
r: new_color.color.red,
|
||||
g: new_color.color.green,
|
||||
b: new_color.color.blue,
|
||||
a: new_color.color.alpha,
|
||||
};
|
||||
|
||||
loop {
|
||||
match existing_colors.peek() {
|
||||
Some((existing_range, existing_color, existing_inlay_id)) => {
|
||||
match existing_range
|
||||
.start
|
||||
.cmp(&new_range.start, &multi_buffer_snapshot)
|
||||
.then_with(|| {
|
||||
existing_range
|
||||
.end
|
||||
.cmp(&new_range.end, &multi_buffer_snapshot)
|
||||
}) {
|
||||
cmp::Ordering::Less => {
|
||||
colors_splice.to_remove.push(*existing_inlay_id);
|
||||
existing_colors.next();
|
||||
continue;
|
||||
}
|
||||
cmp::Ordering::Equal => {
|
||||
if existing_color == &new_color {
|
||||
new_color_inlays.push((
|
||||
new_range,
|
||||
new_color,
|
||||
*existing_inlay_id,
|
||||
));
|
||||
} else {
|
||||
loop {
|
||||
match existing_buffer_colors.peek() {
|
||||
Some((existing_range, existing_color, existing_inlay_id)) => {
|
||||
match existing_range
|
||||
.start
|
||||
.cmp(&new_range.start, &multi_buffer_snapshot)
|
||||
.then_with(|| {
|
||||
existing_range
|
||||
.end
|
||||
.cmp(&new_range.end, &multi_buffer_snapshot)
|
||||
}) {
|
||||
cmp::Ordering::Less => {
|
||||
colors_splice.to_remove.push(*existing_inlay_id);
|
||||
existing_buffer_colors.next();
|
||||
continue;
|
||||
}
|
||||
cmp::Ordering::Equal => {
|
||||
if existing_color == &new_color {
|
||||
new_buffer_color_inlays.push((
|
||||
new_range,
|
||||
new_color,
|
||||
*existing_inlay_id,
|
||||
));
|
||||
} else {
|
||||
colors_splice
|
||||
.to_remove
|
||||
.push(*existing_inlay_id);
|
||||
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
rgba_color,
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_buffer_color_inlays
|
||||
.push((new_range, new_color, inlay_id));
|
||||
}
|
||||
existing_buffer_colors.next();
|
||||
break;
|
||||
}
|
||||
cmp::Ordering::Greater => {
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
@@ -313,49 +363,40 @@ impl Editor {
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_color_inlays
|
||||
new_buffer_color_inlays
|
||||
.push((new_range, new_color, inlay_id));
|
||||
break;
|
||||
}
|
||||
existing_colors.next();
|
||||
break;
|
||||
}
|
||||
cmp::Ordering::Greater => {
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
rgba_color,
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_color_inlays.push((new_range, new_color, inlay_id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
None => {
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
rgba_color,
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_color_inlays.push((new_range, new_color, inlay_id));
|
||||
break;
|
||||
None => {
|
||||
let inlay = Inlay::color(
|
||||
post_inc(&mut editor.next_color_inlay_id),
|
||||
new_range.start,
|
||||
rgba_color,
|
||||
);
|
||||
let inlay_id = inlay.id;
|
||||
colors_splice.to_insert.push(inlay);
|
||||
new_buffer_color_inlays
|
||||
.push((new_range, new_color, inlay_id));
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if existing_colors.peek().is_some() {
|
||||
colors_splice
|
||||
.to_remove
|
||||
.extend(existing_colors.map(|(_, _, id)| *id));
|
||||
|
||||
if existing_buffer_colors.peek().is_some() {
|
||||
colors_splice
|
||||
.to_remove
|
||||
.extend(existing_buffer_colors.map(|(_, _, id)| *id));
|
||||
}
|
||||
updated |= colors.set_colors(
|
||||
buffer_id,
|
||||
new_buffer_color_inlays,
|
||||
new_cache_version,
|
||||
);
|
||||
}
|
||||
|
||||
let mut updated = colors.set_colors(new_color_inlays);
|
||||
if let Some(cache_version) = cache_version {
|
||||
colors.cache_version_used = cache_version;
|
||||
}
|
||||
if colors.render_mode == DocumentColorsRenderMode::Inlay
|
||||
&& (!colors_splice.to_insert.is_empty()
|
||||
|| !colors_splice.to_remove.is_empty())
|
||||
|
||||
@@ -32,7 +32,7 @@ client.workspace = true
|
||||
collections.workspace = true
|
||||
debug_adapter_extension.workspace = true
|
||||
dirs.workspace = true
|
||||
dotenv.workspace = true
|
||||
dotenvy.workspace = true
|
||||
env_logger.workspace = true
|
||||
extension.workspace = true
|
||||
fs.workspace = true
|
||||
|
||||
@@ -63,7 +63,7 @@ struct Args {
|
||||
}
|
||||
|
||||
fn main() {
|
||||
dotenv::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
|
||||
dotenvy::from_filename(CARGO_MANIFEST_DIR.join(".env")).ok();
|
||||
|
||||
env_logger::init();
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ use crate::{
|
||||
ToolMetrics,
|
||||
assertions::{AssertionsReport, RanAssertion, RanAssertionResult},
|
||||
};
|
||||
use agent::{ThreadEvent, ZedAgentThread};
|
||||
use agent::{ContextLoadResult, Thread, ThreadEvent};
|
||||
use agent_settings::AgentProfileId;
|
||||
use anyhow::{Result, anyhow};
|
||||
use async_trait::async_trait;
|
||||
@@ -89,7 +89,7 @@ impl Error for FailedAssertion {}
|
||||
pub struct ExampleContext {
|
||||
meta: ExampleMetadata,
|
||||
log_prefix: String,
|
||||
agent_thread: Entity<agent::ZedAgentThread>,
|
||||
agent_thread: Entity<agent::Thread>,
|
||||
app: AsyncApp,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
pub assertions: AssertionsReport,
|
||||
@@ -100,7 +100,7 @@ impl ExampleContext {
|
||||
pub fn new(
|
||||
meta: ExampleMetadata,
|
||||
log_prefix: String,
|
||||
agent_thread: Entity<ZedAgentThread>,
|
||||
agent_thread: Entity<Thread>,
|
||||
model: Arc<dyn LanguageModel>,
|
||||
app: AsyncApp,
|
||||
) -> Self {
|
||||
@@ -120,7 +120,13 @@ impl ExampleContext {
|
||||
pub fn push_user_message(&mut self, text: impl ToString) {
|
||||
self.app
|
||||
.update_entity(&self.agent_thread, |thread, cx| {
|
||||
thread.insert_user_message(text.to_string(), cx);
|
||||
thread.insert_user_message(
|
||||
text.to_string(),
|
||||
ContextLoadResult::default(),
|
||||
None,
|
||||
Vec::new(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
.unwrap();
|
||||
}
|
||||
@@ -244,7 +250,6 @@ impl ExampleContext {
|
||||
| ThreadEvent::UsePendingTools { .. }
|
||||
| ThreadEvent::CompletionCanceled => {}
|
||||
ThreadEvent::ToolUseLimitReached => {}
|
||||
ThreadEvent::StreamedToolUse2 { .. } => {}
|
||||
ThreadEvent::ToolFinished {
|
||||
tool_use_id,
|
||||
pending_tool_use,
|
||||
@@ -307,10 +312,10 @@ impl ExampleContext {
|
||||
|
||||
let model = self.model.clone();
|
||||
|
||||
let message_count_before = self.app.update_entity(&self.agent_thread, |agent, cx| {
|
||||
agent.set_remaining_turns(iterations);
|
||||
agent.send_to_model(model, CompletionIntent::UserPrompt, None, cx);
|
||||
agent.messages().len()
|
||||
let message_count_before = self.app.update_entity(&self.agent_thread, |thread, cx| {
|
||||
thread.set_remaining_turns(iterations);
|
||||
thread.send_to_model(model, CompletionIntent::UserPrompt, None, cx);
|
||||
thread.messages().len()
|
||||
})?;
|
||||
|
||||
loop {
|
||||
@@ -328,13 +333,13 @@ impl ExampleContext {
|
||||
}
|
||||
}
|
||||
|
||||
let messages = self.app.read_entity(&self.agent_thread, |agent, cx| {
|
||||
let messages = self.app.read_entity(&self.agent_thread, |thread, cx| {
|
||||
let mut messages = Vec::new();
|
||||
for message in agent.messages().skip(message_count_before) {
|
||||
for message in thread.messages().skip(message_count_before) {
|
||||
messages.push(Message {
|
||||
_role: message.role,
|
||||
text: message.to_string(),
|
||||
tool_use: agent
|
||||
tool_use: thread
|
||||
.tool_uses_for_message(message.id, cx)
|
||||
.into_iter()
|
||||
.map(|tool_use| ToolUse {
|
||||
@@ -382,7 +387,7 @@ impl ExampleContext {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
pub fn agent_thread(&self) -> Entity<ZedAgentThread> {
|
||||
pub fn agent_thread(&self) -> Entity<Thread> {
|
||||
self.agent_thread.clone()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,9 +32,9 @@ impl Example for CommentTranslation {
|
||||
cx.run_to_end().await?;
|
||||
|
||||
let mut create_or_overwrite_count = 0;
|
||||
cx.agent_thread().read_with(cx, |agent, cx| {
|
||||
for message in agent.messages() {
|
||||
for tool_use in agent.tool_uses_for_message(message.id, cx) {
|
||||
cx.agent_thread().read_with(cx, |thread, cx| {
|
||||
for message in thread.messages() {
|
||||
for tool_use in thread.tool_uses_for_message(message.id, cx) {
|
||||
if tool_use.name == "edit_file" {
|
||||
let input: EditFileToolInput = serde_json::from_value(tool_use.input)?;
|
||||
if !matches!(input.mode, EditFileMode::Edit) {
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
use agent::thread::ToolUseSegment;
|
||||
use agent::{Message, MessageSegment, SerializedThread, ThreadStore};
|
||||
use anyhow::{Context as _, Result, anyhow, bail};
|
||||
use assistant_tool::ToolWorkingSet;
|
||||
@@ -308,7 +307,7 @@ impl ExampleInstance {
|
||||
let thread_store = thread_store.await?;
|
||||
|
||||
|
||||
let agent =
|
||||
let thread =
|
||||
thread_store.update(cx, |thread_store, cx| {
|
||||
let thread = if let Some(json) = &meta.existing_thread_json {
|
||||
let serialized = SerializedThread::from_json(json.as_bytes()).expect("Can't read serialized thread");
|
||||
@@ -323,7 +322,7 @@ impl ExampleInstance {
|
||||
})?;
|
||||
|
||||
|
||||
agent.update(cx, |thread, _cx| {
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let mut request_count = 0;
|
||||
let previous_diff = Rc::new(RefCell::new("".to_string()));
|
||||
let example_output_dir = this.run_directory.clone();
|
||||
@@ -371,7 +370,7 @@ impl ExampleInstance {
|
||||
let mut example_cx = ExampleContext::new(
|
||||
meta.clone(),
|
||||
this.log_prefix.clone(),
|
||||
agent.clone(),
|
||||
thread.clone(),
|
||||
model.clone(),
|
||||
cx.clone(),
|
||||
);
|
||||
@@ -420,12 +419,11 @@ impl ExampleInstance {
|
||||
fs::write(this.run_directory.join("diagnostics_after.txt"), diagnostics_after)?;
|
||||
}
|
||||
|
||||
agent.update(cx, |agent, _cx| {
|
||||
let response_count = agent
|
||||
thread.update(cx, |thread, _cx| {
|
||||
let response_count = thread
|
||||
.messages()
|
||||
.filter(|message| message.role == language_model::Role::Assistant)
|
||||
.count();
|
||||
let all_messages = messages_to_markdown(agent.messages());
|
||||
RunOutput {
|
||||
repository_diff,
|
||||
diagnostic_summary_before,
|
||||
@@ -433,9 +431,9 @@ impl ExampleInstance {
|
||||
diagnostics_before,
|
||||
diagnostics_after,
|
||||
response_count,
|
||||
token_usage: agent.cumulative_token_usage(),
|
||||
token_usage: thread.cumulative_token_usage(),
|
||||
tool_metrics: example_cx.tool_metrics.lock().unwrap().clone(),
|
||||
all_messages,
|
||||
all_messages: messages_to_markdown(thread.messages()),
|
||||
programmatic_assertions: example_cx.assertions,
|
||||
}
|
||||
})
|
||||
@@ -850,9 +848,11 @@ fn messages_to_markdown<'a>(message_iter: impl IntoIterator<Item = &'a Message>)
|
||||
messages.push_str(&text);
|
||||
messages.push_str("\n");
|
||||
}
|
||||
MessageSegment::ToolUse(ToolUseSegment { name, input, .. }) => {
|
||||
messages.push_str(&format!("**Tool Use**: {}\n\n", name));
|
||||
messages.push_str(&format!("Input: {:?}\n\n", input));
|
||||
MessageSegment::RedactedThinking(items) => {
|
||||
messages.push_str(&format!(
|
||||
"**Redacted Thinking**: {} item(s)\n\n",
|
||||
items.len()
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1054,6 +1054,15 @@ pub fn response_events_to_markdown(
|
||||
| LanguageModelCompletionEvent::StartMessage { .. }
|
||||
| LanguageModelCompletionEvent::StatusUpdate { .. },
|
||||
) => {}
|
||||
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
json_parse_error, ..
|
||||
}) => {
|
||||
flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
|
||||
response.push_str(&format!(
|
||||
"**Error**: parse error in tool use JSON: {}\n\n",
|
||||
json_parse_error
|
||||
));
|
||||
}
|
||||
Err(error) => {
|
||||
flush_buffers(&mut response, &mut text_buffer, &mut thinking_buffer);
|
||||
response.push_str(&format!("**Error**: {}\n\n", error));
|
||||
@@ -1132,6 +1141,17 @@ impl ThreadDialog {
|
||||
| Ok(LanguageModelCompletionEvent::StartMessage { .. })
|
||||
| Ok(LanguageModelCompletionEvent::Stop(_)) => {}
|
||||
|
||||
Ok(LanguageModelCompletionEvent::ToolUseJsonParseError {
|
||||
json_parse_error,
|
||||
..
|
||||
}) => {
|
||||
flush_text(&mut current_text, &mut content);
|
||||
content.push(MessageContent::Text(format!(
|
||||
"ERROR: parse error in tool use JSON: {}",
|
||||
json_parse_error
|
||||
)));
|
||||
}
|
||||
|
||||
Err(error) => {
|
||||
flush_text(&mut current_text, &mut content);
|
||||
content.push(MessageContent::Text(format!("ERROR: {}", error)));
|
||||
|
||||
@@ -70,6 +70,7 @@ const SUGGESTIONS_BY_EXTENSION_ID: &[(&str, &[&str])] = &[
|
||||
("templ", &["templ"]),
|
||||
("terraform", &["tf", "tfvars", "hcl"]),
|
||||
("toml", &["Cargo.lock", "toml"]),
|
||||
("typst", &["typ"]),
|
||||
("vue", &["vue"]),
|
||||
("wgsl", &["wgsl"]),
|
||||
("wit", &["wit"]),
|
||||
|
||||
@@ -65,6 +65,7 @@ actions!(
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = git, deprecated_aliases = ["editor::RevertFile"])]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RestoreFile {
|
||||
#[serde(default)]
|
||||
pub skip_prompt: bool,
|
||||
|
||||
@@ -388,6 +388,7 @@ pub(crate) fn commit_message_editor(
|
||||
commit_editor.set_collaboration_hub(Box::new(project));
|
||||
commit_editor.set_use_autoclose(false);
|
||||
commit_editor.set_show_gutter(false, cx);
|
||||
commit_editor.set_use_modal_editing(true);
|
||||
commit_editor.set_show_wrap_guides(false, cx);
|
||||
commit_editor.set_show_indent_guides(false, cx);
|
||||
let placeholder = placeholder.unwrap_or("Enter commit message".into());
|
||||
|
||||
@@ -12,7 +12,7 @@ license = "Apache-2.0"
|
||||
workspace = true
|
||||
|
||||
[features]
|
||||
default = ["http_client", "font-kit", "wayland", "x11"]
|
||||
default = ["http_client", "font-kit", "wayland", "x11", "windows-manifest"]
|
||||
test-support = [
|
||||
"leak-detection",
|
||||
"collections/test-support",
|
||||
@@ -69,7 +69,7 @@ x11 = [
|
||||
"open",
|
||||
"scap",
|
||||
]
|
||||
|
||||
windows-manifest = []
|
||||
|
||||
[lib]
|
||||
path = "src/gpui.rs"
|
||||
|
||||
@@ -17,7 +17,7 @@ fn main() {
|
||||
#[cfg(target_os = "macos")]
|
||||
macos::build();
|
||||
}
|
||||
#[cfg(target_os = "windows")]
|
||||
#[cfg(all(target_os = "windows", feature = "windows-manifest"))]
|
||||
Ok("windows") => {
|
||||
let manifest = std::path::Path::new("resources/windows/gpui.manifest.xml");
|
||||
let rc_file = std::path::Path::new("resources/windows/gpui.rc");
|
||||
|
||||
@@ -125,9 +125,7 @@ pub trait Action: Any + Send {
|
||||
Self: Sized;
|
||||
|
||||
/// Optional JSON schema for the action's input data.
|
||||
fn action_json_schema(
|
||||
_: &mut schemars::r#gen::SchemaGenerator,
|
||||
) -> Option<schemars::schema::Schema>
|
||||
fn action_json_schema(_: &mut schemars::SchemaGenerator) -> Option<schemars::Schema>
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
@@ -238,7 +236,7 @@ impl Default for ActionRegistry {
|
||||
|
||||
struct ActionData {
|
||||
pub build: ActionBuilder,
|
||||
pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
|
||||
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
|
||||
}
|
||||
|
||||
/// This type must be public so that our macros can build it in other crates.
|
||||
@@ -253,7 +251,7 @@ pub struct MacroActionData {
|
||||
pub name: &'static str,
|
||||
pub type_id: TypeId,
|
||||
pub build: ActionBuilder,
|
||||
pub json_schema: fn(&mut schemars::r#gen::SchemaGenerator) -> Option<schemars::schema::Schema>,
|
||||
pub json_schema: fn(&mut schemars::SchemaGenerator) -> Option<schemars::Schema>,
|
||||
pub deprecated_aliases: &'static [&'static str],
|
||||
pub deprecation_message: Option<&'static str>,
|
||||
}
|
||||
@@ -357,8 +355,8 @@ impl ActionRegistry {
|
||||
|
||||
pub fn action_schemas(
|
||||
&self,
|
||||
generator: &mut schemars::r#gen::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::schema::Schema>)> {
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::Schema>)> {
|
||||
// Use the order from all_names so that the resulting schema has sensible order.
|
||||
self.all_names
|
||||
.iter()
|
||||
|
||||
@@ -1334,6 +1334,11 @@ impl App {
|
||||
self.pending_effects.push_back(Effect::RefreshWindows);
|
||||
}
|
||||
|
||||
/// Get all key bindings in the app.
|
||||
pub fn key_bindings(&self) -> Rc<RefCell<Keymap>> {
|
||||
self.keymap.clone()
|
||||
}
|
||||
|
||||
/// Register a global listener for actions invoked via the keyboard.
|
||||
pub fn on_action<A: Action>(&mut self, listener: impl Fn(&A, &mut Self) + 'static) {
|
||||
self.global_action_listeners
|
||||
@@ -1388,8 +1393,8 @@ impl App {
|
||||
/// Get all non-internal actions that have been registered, along with their schemas.
|
||||
pub fn action_schemas(
|
||||
&self,
|
||||
generator: &mut schemars::r#gen::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::schema::Schema>)> {
|
||||
generator: &mut schemars::SchemaGenerator,
|
||||
) -> Vec<(&'static str, Option<schemars::Schema>)> {
|
||||
self.actions.action_schemas(generator)
|
||||
}
|
||||
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
use anyhow::{Context as _, bail};
|
||||
use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize, Serializer,
|
||||
de::{self, Visitor},
|
||||
};
|
||||
use std::borrow::Cow;
|
||||
use std::{
|
||||
fmt::{self, Display, Formatter},
|
||||
hash::{Hash, Hasher},
|
||||
@@ -99,22 +100,14 @@ impl Visitor<'_> for RgbaVisitor {
|
||||
}
|
||||
|
||||
impl JsonSchema for Rgba {
|
||||
fn schema_name() -> String {
|
||||
"Rgba".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Rgba".into()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
string: Some(Box::new(StringValidation {
|
||||
pattern: Some(
|
||||
r"^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$".to_string(),
|
||||
),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": "^#([0-9a-fA-F]{3}|[0-9a-fA-F]{4}|[0-9a-fA-F]{6}|[0-9a-fA-F]{8})$"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -629,11 +622,11 @@ impl From<Rgba> for Hsla {
|
||||
}
|
||||
|
||||
impl JsonSchema for Hsla {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
Rgba::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut SchemaGenerator) -> Schema {
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
Rgba::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -613,10 +613,10 @@ pub trait InteractiveElement: Sized {
|
||||
/// Track the focus state of the given focus handle on this element.
|
||||
/// If the focus handle is focused by the application, this element will
|
||||
/// apply its focused styles.
|
||||
fn track_focus(mut self, focus_handle: &FocusHandle) -> FocusableWrapper<Self> {
|
||||
fn track_focus(mut self, focus_handle: &FocusHandle) -> Self {
|
||||
self.interactivity().focusable = true;
|
||||
self.interactivity().tracked_focus_handle = Some(focus_handle.clone());
|
||||
FocusableWrapper { element: self }
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the keymap context for this element. This will be used to determine
|
||||
@@ -980,15 +980,35 @@ pub trait InteractiveElement: Sized {
|
||||
self.interactivity().block_mouse_except_scroll();
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given styles to be applied when this element, specifically, is focused.
|
||||
/// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
|
||||
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given styles to be applied when this element is inside another element that is focused.
|
||||
/// Requires that the element is focusable. Elements can be made focusable using [`InteractiveElement::track_focus`].
|
||||
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for elements that want to use the standard GPUI interactivity features
|
||||
/// that require state.
|
||||
pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
/// Set this element to focusable.
|
||||
fn focusable(mut self) -> FocusableWrapper<Self> {
|
||||
fn focusable(mut self) -> Self {
|
||||
self.interactivity().focusable = true;
|
||||
FocusableWrapper { element: self }
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the overflow x and y to scroll.
|
||||
@@ -1118,27 +1138,6 @@ pub trait StatefulInteractiveElement: InteractiveElement {
|
||||
}
|
||||
}
|
||||
|
||||
/// A trait for providing focus related APIs to interactive elements
|
||||
pub trait FocusableElement: InteractiveElement {
|
||||
/// Set the given styles to be applied when this element, specifically, is focused.
|
||||
fn focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().focus_style = Some(Box::new(f(StyleRefinement::default())));
|
||||
self
|
||||
}
|
||||
|
||||
/// Set the given styles to be applied when this element is inside another element that is focused.
|
||||
fn in_focus(mut self, f: impl FnOnce(StyleRefinement) -> StyleRefinement) -> Self
|
||||
where
|
||||
Self: Sized,
|
||||
{
|
||||
self.interactivity().in_focus_style = Some(Box::new(f(StyleRefinement::default())));
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) type MouseDownListener =
|
||||
Box<dyn Fn(&MouseDownEvent, DispatchPhase, &Hitbox, &mut Window, &mut App) + 'static>;
|
||||
pub(crate) type MouseUpListener =
|
||||
@@ -2777,126 +2776,6 @@ impl GroupHitboxes {
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an element that can be focused.
|
||||
pub struct FocusableWrapper<E> {
|
||||
/// The element that is focusable
|
||||
pub element: E,
|
||||
}
|
||||
|
||||
impl<E: InteractiveElement> FocusableElement for FocusableWrapper<E> {}
|
||||
|
||||
impl<E> InteractiveElement for FocusableWrapper<E>
|
||||
where
|
||||
E: InteractiveElement,
|
||||
{
|
||||
fn interactivity(&mut self) -> &mut Interactivity {
|
||||
self.element.interactivity()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: StatefulInteractiveElement> StatefulInteractiveElement for FocusableWrapper<E> {}
|
||||
|
||||
impl<E> Styled for FocusableWrapper<E>
|
||||
where
|
||||
E: Styled,
|
||||
{
|
||||
fn style(&mut self) -> &mut StyleRefinement {
|
||||
self.element.style()
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableWrapper<Div> {
|
||||
/// Add a listener to be called when the children of this `Div` are prepainted.
|
||||
/// This allows you to store the [`Bounds`] of the children for later use.
|
||||
pub fn on_children_prepainted(
|
||||
mut self,
|
||||
listener: impl Fn(Vec<Bounds<Pixels>>, &mut Window, &mut App) + 'static,
|
||||
) -> Self {
|
||||
self.element = self.element.on_children_prepainted(listener);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> Element for FocusableWrapper<E>
|
||||
where
|
||||
E: Element,
|
||||
{
|
||||
type RequestLayoutState = E::RequestLayoutState;
|
||||
type PrepaintState = E::PrepaintState;
|
||||
|
||||
fn id(&self) -> Option<ElementId> {
|
||||
self.element.id()
|
||||
}
|
||||
|
||||
fn source_location(&self) -> Option<&'static core::panic::Location<'static>> {
|
||||
self.element.source_location()
|
||||
}
|
||||
|
||||
fn request_layout(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> (LayoutId, Self::RequestLayoutState) {
|
||||
self.element.request_layout(id, inspector_id, window, cx)
|
||||
}
|
||||
|
||||
fn prepaint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
state: &mut Self::RequestLayoutState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) -> E::PrepaintState {
|
||||
self.element
|
||||
.prepaint(id, inspector_id, bounds, state, window, cx)
|
||||
}
|
||||
|
||||
fn paint(
|
||||
&mut self,
|
||||
id: Option<&GlobalElementId>,
|
||||
inspector_id: Option<&InspectorElementId>,
|
||||
bounds: Bounds<Pixels>,
|
||||
request_layout: &mut Self::RequestLayoutState,
|
||||
prepaint: &mut Self::PrepaintState,
|
||||
window: &mut Window,
|
||||
cx: &mut App,
|
||||
) {
|
||||
self.element.paint(
|
||||
id,
|
||||
inspector_id,
|
||||
bounds,
|
||||
request_layout,
|
||||
prepaint,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> IntoElement for FocusableWrapper<E>
|
||||
where
|
||||
E: IntoElement,
|
||||
{
|
||||
type Element = E::Element;
|
||||
|
||||
fn into_element(self) -> Self::Element {
|
||||
self.element.into_element()
|
||||
}
|
||||
}
|
||||
|
||||
impl<E> ParentElement for FocusableWrapper<E>
|
||||
where
|
||||
E: ParentElement,
|
||||
{
|
||||
fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
|
||||
self.element.extend(elements)
|
||||
}
|
||||
}
|
||||
|
||||
/// A wrapper around an element that can store state, produced after assigning an ElementId.
|
||||
pub struct Stateful<E> {
|
||||
pub(crate) element: E,
|
||||
@@ -2927,8 +2806,6 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
impl<E: FocusableElement> FocusableElement for Stateful<E> {}
|
||||
|
||||
impl<E> Element for Stateful<E>
|
||||
where
|
||||
E: Element,
|
||||
|
||||
@@ -25,7 +25,7 @@ use std::{
|
||||
use thiserror::Error;
|
||||
use util::ResultExt;
|
||||
|
||||
use super::{FocusableElement, Stateful, StatefulInteractiveElement};
|
||||
use super::{Stateful, StatefulInteractiveElement};
|
||||
|
||||
/// The delay before showing the loading state.
|
||||
pub const LOADING_DELAY: Duration = Duration::from_millis(200);
|
||||
@@ -509,8 +509,6 @@ impl IntoElement for Img {
|
||||
}
|
||||
}
|
||||
|
||||
impl FocusableElement for Img {}
|
||||
|
||||
impl StatefulInteractiveElement for Img {}
|
||||
|
||||
impl ImageSource {
|
||||
|
||||
@@ -10,8 +10,8 @@
|
||||
use crate::{
|
||||
AnyElement, App, AvailableSpace, Bounds, ContentMask, DispatchPhase, Edges, Element, EntityId,
|
||||
FocusHandle, GlobalElementId, Hitbox, HitboxBehavior, InspectorElementId, IntoElement,
|
||||
Overflow, Pixels, Point, ScrollWheelEvent, Size, Style, StyleRefinement, Styled, Window, point,
|
||||
px, size,
|
||||
Overflow, Pixels, Point, ScrollDelta, ScrollWheelEvent, Size, Style, StyleRefinement, Styled,
|
||||
Window, point, px, size,
|
||||
};
|
||||
use collections::VecDeque;
|
||||
use refineable::Refineable as _;
|
||||
@@ -962,12 +962,15 @@ impl Element for List {
|
||||
let height = bounds.size.height;
|
||||
let scroll_top = prepaint.layout.scroll_top;
|
||||
let hitbox_id = prepaint.hitbox.id;
|
||||
let mut accumulated_scroll_delta = ScrollDelta::default();
|
||||
window.on_mouse_event(move |event: &ScrollWheelEvent, phase, window, cx| {
|
||||
if phase == DispatchPhase::Bubble && hitbox_id.should_handle_scroll(window) {
|
||||
accumulated_scroll_delta = accumulated_scroll_delta.coalesce(event.delta);
|
||||
let pixel_delta = accumulated_scroll_delta.pixel_delta(px(20.));
|
||||
list_state.0.borrow_mut().scroll(
|
||||
&scroll_top,
|
||||
height,
|
||||
event.delta.pixel_delta(px(20.)),
|
||||
pixel_delta,
|
||||
current_view,
|
||||
window,
|
||||
cx,
|
||||
|
||||
@@ -6,8 +6,9 @@ use anyhow::{Context as _, anyhow};
|
||||
use core::fmt::Debug;
|
||||
use derive_more::{Add, AddAssign, Div, DivAssign, Mul, Neg, Sub, SubAssign};
|
||||
use refineable::Refineable;
|
||||
use schemars::{JsonSchema, SchemaGenerator, schema::Schema};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
use std::borrow::Cow;
|
||||
use std::{
|
||||
cmp::{self, PartialOrd},
|
||||
fmt::{self, Display},
|
||||
@@ -3229,20 +3230,14 @@ impl TryFrom<&'_ str> for AbsoluteLength {
|
||||
}
|
||||
|
||||
impl JsonSchema for AbsoluteLength {
|
||||
fn schema_name() -> String {
|
||||
"AbsoluteLength".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"AbsoluteLength".into()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
string: Some(Box::new(StringValidation {
|
||||
pattern: Some(r"^-?\d+(\.\d+)?(px|rem)$".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": r"^-?\d+(\.\d+)?(px|rem)$"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3366,20 +3361,14 @@ impl TryFrom<&'_ str> for DefiniteLength {
|
||||
}
|
||||
|
||||
impl JsonSchema for DefiniteLength {
|
||||
fn schema_name() -> String {
|
||||
"DefiniteLength".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"DefiniteLength".into()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
string: Some(Box::new(StringValidation {
|
||||
pattern: Some(r"^-?\d+(\.\d+)?(px|rem|%)$".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": r"^-?\d+(\.\d+)?(px|rem|%)$"
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -3480,20 +3469,14 @@ impl TryFrom<&'_ str> for Length {
|
||||
}
|
||||
|
||||
impl JsonSchema for Length {
|
||||
fn schema_name() -> String {
|
||||
"Length".to_string()
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Length".into()
|
||||
}
|
||||
|
||||
fn json_schema(_generator: &mut SchemaGenerator) -> Schema {
|
||||
use schemars::schema::{InstanceType, SchemaObject, StringValidation};
|
||||
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
string: Some(Box::new(StringValidation {
|
||||
pattern: Some(r"^(auto|-?\d+(\.\d+)?(px|rem|%))$".to_string()),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
fn json_schema(_generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string",
|
||||
"pattern": r"^(auto|-?\d+(\.\d+)?(px|rem|%))$"
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ use std::rc::Rc;
|
||||
|
||||
use collections::HashMap;
|
||||
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke};
|
||||
use crate::{Action, InvalidKeystrokeError, KeyBindingContextPredicate, Keystroke, SharedString};
|
||||
use smallvec::SmallVec;
|
||||
|
||||
/// A keybinding and its associated metadata, from the keymap.
|
||||
@@ -11,6 +11,8 @@ pub struct KeyBinding {
|
||||
pub(crate) keystrokes: SmallVec<[Keystroke; 2]>,
|
||||
pub(crate) context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
pub(crate) meta: Option<KeyBindingMetaIndex>,
|
||||
/// The json input string used when building the keybinding, if any
|
||||
pub(crate) action_input: Option<SharedString>,
|
||||
}
|
||||
|
||||
impl Clone for KeyBinding {
|
||||
@@ -20,6 +22,7 @@ impl Clone for KeyBinding {
|
||||
keystrokes: self.keystrokes.clone(),
|
||||
context_predicate: self.context_predicate.clone(),
|
||||
meta: self.meta,
|
||||
action_input: self.action_input.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -32,7 +35,7 @@ impl KeyBinding {
|
||||
} else {
|
||||
None
|
||||
};
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None).unwrap()
|
||||
Self::load(keystrokes, Box::new(action), context_predicate, None, None).unwrap()
|
||||
}
|
||||
|
||||
/// Load a keybinding from the given raw data.
|
||||
@@ -41,6 +44,7 @@ impl KeyBinding {
|
||||
action: Box<dyn Action>,
|
||||
context_predicate: Option<Rc<KeyBindingContextPredicate>>,
|
||||
key_equivalents: Option<&HashMap<char, char>>,
|
||||
action_input: Option<SharedString>,
|
||||
) -> std::result::Result<Self, InvalidKeystrokeError> {
|
||||
let mut keystrokes: SmallVec<[Keystroke; 2]> = keystrokes
|
||||
.split_whitespace()
|
||||
@@ -62,6 +66,7 @@ impl KeyBinding {
|
||||
action,
|
||||
context_predicate,
|
||||
meta: None,
|
||||
action_input,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -110,6 +115,11 @@ impl KeyBinding {
|
||||
pub fn meta(&self) -> Option<KeyBindingMetaIndex> {
|
||||
self.meta
|
||||
}
|
||||
|
||||
/// Get the action input associated with the action for this binding
|
||||
pub fn action_input(&self) -> Option<SharedString> {
|
||||
self.action_input.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Debug for KeyBinding {
|
||||
|
||||
@@ -151,7 +151,7 @@ pub fn guess_compositor() -> &'static str {
|
||||
pub(crate) fn current_platform(_headless: bool) -> Rc<dyn Platform> {
|
||||
Rc::new(
|
||||
WindowsPlatform::new()
|
||||
.inspect_err(|err| show_error("Error: Zed failed to launch", err.to_string()))
|
||||
.inspect_err(|err| show_error("Failed to launch", err.to_string()))
|
||||
.unwrap(),
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1299,12 +1299,8 @@ mod windows_renderer {
|
||||
size: Default::default(),
|
||||
transparent,
|
||||
};
|
||||
BladeRenderer::new(context, &raw, config).inspect_err(|err| {
|
||||
show_error(
|
||||
"Error: Zed failed to initialize BladeRenderer",
|
||||
err.to_string(),
|
||||
)
|
||||
})
|
||||
BladeRenderer::new(context, &raw, config)
|
||||
.inspect_err(|err| show_error("Failed to initialize BladeRenderer", err.to_string()))
|
||||
}
|
||||
|
||||
struct RawWindow {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
//! application to avoid having to import each trait individually.
|
||||
|
||||
pub use crate::{
|
||||
AppContext as _, BorrowAppContext, Context, Element, FocusableElement, InteractiveElement,
|
||||
IntoElement, ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled,
|
||||
StyledImage, VisualContext, util::FluentBuilder,
|
||||
AppContext as _, BorrowAppContext, Context, Element, InteractiveElement, IntoElement,
|
||||
ParentElement, Refineable, Render, RenderOnce, StatefulInteractiveElement, Styled, StyledImage,
|
||||
VisualContext, util::FluentBuilder,
|
||||
};
|
||||
|
||||
@@ -2,7 +2,10 @@ use derive_more::{Deref, DerefMut};
|
||||
|
||||
use schemars::JsonSchema;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{borrow::Borrow, sync::Arc};
|
||||
use std::{
|
||||
borrow::{Borrow, Cow},
|
||||
sync::Arc,
|
||||
};
|
||||
use util::arc_cow::ArcCow;
|
||||
|
||||
/// A shared string is an immutable string that can be cheaply cloned in GPUI
|
||||
@@ -23,12 +26,16 @@ impl SharedString {
|
||||
}
|
||||
|
||||
impl JsonSchema for SharedString {
|
||||
fn schema_name() -> String {
|
||||
fn inline_schema() -> bool {
|
||||
String::inline_schema()
|
||||
}
|
||||
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
String::schema_name()
|
||||
}
|
||||
|
||||
fn json_schema(r#gen: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
String::json_schema(r#gen)
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
String::json_schema(generator)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::borrow::Cow;
|
||||
use std::sync::Arc;
|
||||
|
||||
use schemars::schema::{InstanceType, SchemaObject};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
|
||||
/// The OpenType features that can be configured for a given font.
|
||||
#[derive(Default, Clone, Eq, PartialEq, Hash)]
|
||||
@@ -128,36 +129,23 @@ impl serde::Serialize for FontFeatures {
|
||||
}
|
||||
}
|
||||
|
||||
impl schemars::JsonSchema for FontFeatures {
|
||||
fn schema_name() -> String {
|
||||
impl JsonSchema for FontFeatures {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"FontFeatures".into()
|
||||
}
|
||||
|
||||
fn json_schema(_: &mut schemars::r#gen::SchemaGenerator) -> schemars::schema::Schema {
|
||||
let mut schema = SchemaObject::default();
|
||||
schema.instance_type = Some(schemars::schema::SingleOrVec::Single(Box::new(
|
||||
InstanceType::Object,
|
||||
)));
|
||||
{
|
||||
let mut property = SchemaObject {
|
||||
instance_type: Some(schemars::schema::SingleOrVec::Vec(vec![
|
||||
InstanceType::Boolean,
|
||||
InstanceType::Integer,
|
||||
])),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
{
|
||||
let mut number_constraints = property.number();
|
||||
number_constraints.multiple_of = Some(1.0);
|
||||
number_constraints.minimum = Some(0.0);
|
||||
}
|
||||
schema
|
||||
.object()
|
||||
.pattern_properties
|
||||
.insert("[0-9a-zA-Z]{4}$".into(), property.into());
|
||||
}
|
||||
schema.into()
|
||||
fn json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "object",
|
||||
"patternProperties": {
|
||||
"[0-9a-zA-Z]{4}$": {
|
||||
"type": ["boolean", "integer"],
|
||||
"minimum": 0,
|
||||
"multipleOf": 1
|
||||
}
|
||||
},
|
||||
"additionalProperties": false
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,11 @@ fn test_action_macros() {
|
||||
|
||||
#[derive(PartialEq, Clone, Deserialize, JsonSchema, Action)]
|
||||
#[action(namespace = test_only)]
|
||||
struct AnotherSomeAction;
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct AnotherAction;
|
||||
|
||||
#[derive(PartialEq, Clone, gpui::private::serde_derive::Deserialize)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
struct RegisterableAction {}
|
||||
|
||||
register_action!(RegisterableAction);
|
||||
|
||||
@@ -159,8 +159,8 @@ pub(crate) fn derive_action(input: TokenStream) -> TokenStream {
|
||||
}
|
||||
|
||||
fn action_json_schema(
|
||||
_generator: &mut gpui::private::schemars::r#gen::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::schema::Schema> {
|
||||
_generator: &mut gpui::private::schemars::SchemaGenerator,
|
||||
) -> Option<gpui::private::schemars::Schema> {
|
||||
#json_schema_fn_body
|
||||
}
|
||||
|
||||
|
||||
@@ -967,6 +967,7 @@ fn toggle_show_inline_completions_for_language(
|
||||
all_language_settings(None, cx).show_edit_predictions(Some(&language), cx);
|
||||
update_settings_file::<AllLanguageSettings>(fs, cx, move |file, _| {
|
||||
file.languages
|
||||
.0
|
||||
.entry(language.name())
|
||||
.or_default()
|
||||
.show_edit_predictions = Some(!show_edit_predictions);
|
||||
|
||||
@@ -39,6 +39,7 @@ globset.workspace = true
|
||||
gpui.workspace = true
|
||||
http_client.workspace = true
|
||||
imara-diff.workspace = true
|
||||
inventory.workspace = true
|
||||
itertools.workspace = true
|
||||
log.workspace = true
|
||||
lsp.workspace = true
|
||||
|
||||
@@ -2006,7 +2006,7 @@ fn test_autoindent_language_without_indents_query(cx: &mut App) {
|
||||
#[gpui::test]
|
||||
fn test_autoindent_with_injected_languages(cx: &mut App) {
|
||||
init_settings(cx, |settings| {
|
||||
settings.languages.extend([
|
||||
settings.languages.0.extend([
|
||||
(
|
||||
"HTML".into(),
|
||||
LanguageSettingsContent {
|
||||
|
||||
@@ -39,11 +39,7 @@ use lsp::{CodeActionKind, InitializeParams, LanguageServerBinary, LanguageServer
|
||||
pub use manifest::{ManifestDelegate, ManifestName, ManifestProvider, ManifestQuery};
|
||||
use parking_lot::Mutex;
|
||||
use regex::Regex;
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
r#gen::SchemaGenerator,
|
||||
schema::{InstanceType, Schema, SchemaObject},
|
||||
};
|
||||
use schemars::{JsonSchema, SchemaGenerator, json_schema};
|
||||
use serde::{Deserialize, Deserializer, Serialize, Serializer, de};
|
||||
use serde_json::Value;
|
||||
use settings::WorktreeId;
|
||||
@@ -694,7 +690,6 @@ pub struct LanguageConfig {
|
||||
pub matcher: LanguageMatcher,
|
||||
/// List of bracket types in a language.
|
||||
#[serde(default)]
|
||||
#[schemars(schema_with = "bracket_pair_config_json_schema")]
|
||||
pub brackets: BracketPairConfig,
|
||||
/// If set to true, auto indentation uses last non empty line to determine
|
||||
/// the indentation level for a new line.
|
||||
@@ -735,6 +730,13 @@ pub struct LanguageConfig {
|
||||
/// Starting and closing characters of a block comment.
|
||||
#[serde(default)]
|
||||
pub block_comment: Option<(Arc<str>, Arc<str>)>,
|
||||
/// A list of additional regex patterns that should be treated as prefixes
|
||||
/// for creating boundaries during rewrapping, ensuring content from one
|
||||
/// prefixed section doesn't merge with another (e.g., markdown list items).
|
||||
/// By default, Zed treats as paragraph and comment prefixes as boundaries.
|
||||
#[serde(default, deserialize_with = "deserialize_regex_vec")]
|
||||
#[schemars(schema_with = "regex_vec_json_schema")]
|
||||
pub rewrap_prefixes: Vec<Regex>,
|
||||
/// A list of language servers that are allowed to run on subranges of a given language.
|
||||
#[serde(default)]
|
||||
pub scope_opt_in_language_servers: Vec<LanguageServerName>,
|
||||
@@ -914,6 +916,7 @@ impl Default for LanguageConfig {
|
||||
autoclose_before: Default::default(),
|
||||
line_comments: Default::default(),
|
||||
block_comment: Default::default(),
|
||||
rewrap_prefixes: Default::default(),
|
||||
scope_opt_in_language_servers: Default::default(),
|
||||
overrides: Default::default(),
|
||||
word_characters: Default::default(),
|
||||
@@ -944,10 +947,9 @@ fn deserialize_regex<'de, D: Deserializer<'de>>(d: D) -> Result<Option<Regex>, D
|
||||
}
|
||||
}
|
||||
|
||||
fn regex_json_schema(_: &mut SchemaGenerator) -> Schema {
|
||||
Schema::Object(SchemaObject {
|
||||
instance_type: Some(InstanceType::String.into()),
|
||||
..Default::default()
|
||||
fn regex_json_schema(_: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "string"
|
||||
})
|
||||
}
|
||||
|
||||
@@ -961,6 +963,22 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
fn deserialize_regex_vec<'de, D: Deserializer<'de>>(d: D) -> Result<Vec<Regex>, D::Error> {
|
||||
let sources = Vec::<String>::deserialize(d)?;
|
||||
let mut regexes = Vec::new();
|
||||
for source in sources {
|
||||
regexes.push(regex::Regex::new(&source).map_err(de::Error::custom)?);
|
||||
}
|
||||
Ok(regexes)
|
||||
}
|
||||
|
||||
fn regex_vec_json_schema(_: &mut SchemaGenerator) -> schemars::Schema {
|
||||
json_schema!({
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
})
|
||||
}
|
||||
|
||||
#[doc(hidden)]
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub struct FakeLspAdapter {
|
||||
@@ -988,12 +1006,12 @@ pub struct FakeLspAdapter {
|
||||
/// This struct includes settings for defining which pairs of characters are considered brackets and
|
||||
/// also specifies any language-specific scopes where these pairs should be ignored for bracket matching purposes.
|
||||
#[derive(Clone, Debug, Default, JsonSchema)]
|
||||
#[schemars(with = "Vec::<BracketPairContent>")]
|
||||
pub struct BracketPairConfig {
|
||||
/// A list of character pairs that should be treated as brackets in the context of a given language.
|
||||
pub pairs: Vec<BracketPair>,
|
||||
/// A list of tree-sitter scopes for which a given bracket should not be active.
|
||||
/// N-th entry in `[Self::disabled_scopes_by_bracket_ix]` contains a list of disabled scopes for an n-th entry in `[Self::pairs]`
|
||||
#[serde(skip)]
|
||||
pub disabled_scopes_by_bracket_ix: Vec<Vec<String>>,
|
||||
}
|
||||
|
||||
@@ -1003,10 +1021,6 @@ impl BracketPairConfig {
|
||||
}
|
||||
}
|
||||
|
||||
fn bracket_pair_config_json_schema(r#gen: &mut SchemaGenerator) -> Schema {
|
||||
Option::<Vec<BracketPairContent>>::json_schema(r#gen)
|
||||
}
|
||||
|
||||
#[derive(Deserialize, JsonSchema)]
|
||||
pub struct BracketPairContent {
|
||||
#[serde(flatten)]
|
||||
@@ -1841,6 +1855,14 @@ impl LanguageScope {
|
||||
.map(|e| (&e.0, &e.1))
|
||||
}
|
||||
|
||||
/// Returns additional regex patterns that act as prefix markers for creating
|
||||
/// boundaries during rewrapping.
|
||||
///
|
||||
/// By default, Zed treats as paragraph and comment prefixes as boundaries.
|
||||
pub fn rewrap_prefixes(&self) -> &[Regex] {
|
||||
&self.language.config.rewrap_prefixes
|
||||
}
|
||||
|
||||
/// Returns a list of language-specific word characters.
|
||||
///
|
||||
/// By default, Zed treats alphanumeric characters (and '_') as word characters for
|
||||
|
||||
@@ -1170,7 +1170,7 @@ impl LanguageRegistryState {
|
||||
if let Some(theme) = self.theme.as_ref() {
|
||||
language.set_theme(theme.syntax());
|
||||
}
|
||||
self.language_settings.languages.insert(
|
||||
self.language_settings.languages.0.insert(
|
||||
language.name(),
|
||||
LanguageSettingsContent {
|
||||
tab_size: language.config.tab_size,
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
use crate::{File, Language, LanguageName, LanguageServerName};
|
||||
use anyhow::Result;
|
||||
use collections::{FxHashMap, HashMap, HashSet};
|
||||
use core::slice;
|
||||
use ec4rs::{
|
||||
Properties as EditorconfigProperties,
|
||||
property::{FinalNewline, IndentSize, IndentStyle, TabWidth, TrimTrailingWs},
|
||||
@@ -11,17 +10,15 @@ use ec4rs::{
|
||||
use globset::{Glob, GlobMatcher, GlobSet, GlobSetBuilder};
|
||||
use gpui::{App, Modifiers};
|
||||
use itertools::{Either, Itertools};
|
||||
use schemars::{
|
||||
JsonSchema,
|
||||
schema::{InstanceType, ObjectValidation, Schema, SchemaObject, SingleOrVec},
|
||||
};
|
||||
use schemars::{JsonSchema, json_schema};
|
||||
use serde::{
|
||||
Deserialize, Deserializer, Serialize,
|
||||
de::{self, IntoDeserializer, MapAccess, SeqAccess, Visitor},
|
||||
};
|
||||
use serde_json::Value;
|
||||
|
||||
use settings::{
|
||||
Settings, SettingsLocation, SettingsSources, SettingsStore, add_references_to_properties,
|
||||
ParameterizedJsonSchema, Settings, SettingsLocation, SettingsSources, SettingsStore,
|
||||
replace_subschema,
|
||||
};
|
||||
use shellexpand;
|
||||
use std::{borrow::Cow, num::NonZeroU32, path::Path, sync::Arc};
|
||||
@@ -306,13 +303,42 @@ pub struct AllLanguageSettingsContent {
|
||||
pub defaults: LanguageSettingsContent,
|
||||
/// The settings for individual languages.
|
||||
#[serde(default)]
|
||||
pub languages: HashMap<LanguageName, LanguageSettingsContent>,
|
||||
pub languages: LanguageToSettingsMap,
|
||||
/// Settings for associating file extensions and filenames
|
||||
/// with languages.
|
||||
#[serde(default)]
|
||||
pub file_types: HashMap<Arc<str>, Vec<String>>,
|
||||
}
|
||||
|
||||
/// Map from language name to settings. Its `ParameterizedJsonSchema` allows only known language
|
||||
/// names in the keys.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct LanguageToSettingsMap(pub HashMap<LanguageName, LanguageSettingsContent>);
|
||||
|
||||
inventory::submit! {
|
||||
ParameterizedJsonSchema {
|
||||
add_and_get_ref: |generator, params, _cx| {
|
||||
let language_settings_content_ref = generator
|
||||
.subschema_for::<LanguageSettingsContent>()
|
||||
.to_value();
|
||||
let schema = json_schema!({
|
||||
"type": "object",
|
||||
"properties": params
|
||||
.language_names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
language_settings_content_ref.clone(),
|
||||
)
|
||||
})
|
||||
.collect::<serde_json::Map<_, _>>()
|
||||
});
|
||||
replace_subschema::<LanguageToSettingsMap>(generator, schema)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls how completions are processed for this language.
|
||||
#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
@@ -384,7 +410,6 @@ fn default_lsp_fetch_timeout_ms() -> u64 {
|
||||
|
||||
/// The settings for a particular language.
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize, JsonSchema)]
|
||||
#[schemars(deny_unknown_fields)]
|
||||
pub struct LanguageSettingsContent {
|
||||
/// How many columns a tab should occupy.
|
||||
///
|
||||
@@ -648,45 +673,30 @@ pub enum FormatOnSave {
|
||||
On,
|
||||
/// Files should not be formatted on save.
|
||||
Off,
|
||||
List(FormatterList),
|
||||
List(Vec<Formatter>),
|
||||
}
|
||||
|
||||
impl JsonSchema for FormatOnSave {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"OnSaveFormatter".into()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
let mut schema = SchemaObject::default();
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let formatter_schema = Formatter::json_schema(generator);
|
||||
schema.instance_type = Some(
|
||||
vec![
|
||||
InstanceType::Object,
|
||||
InstanceType::String,
|
||||
InstanceType::Array,
|
||||
|
||||
json_schema!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": formatter_schema
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["on", "off", "prettier", "language_server"]
|
||||
},
|
||||
formatter_schema
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
|
||||
let valid_raw_values = SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
Value::String("on".into()),
|
||||
Value::String("off".into()),
|
||||
Value::String("prettier".into()),
|
||||
Value::String("language_server".into()),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
let mut nested_values = SchemaObject::default();
|
||||
|
||||
nested_values.array().items = Some(formatter_schema.clone().into());
|
||||
|
||||
schema.subschemas().any_of = Some(vec![
|
||||
nested_values.into(),
|
||||
valid_raw_values.into(),
|
||||
formatter_schema,
|
||||
]);
|
||||
schema.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -725,11 +735,11 @@ impl<'de> Deserialize<'de> for FormatOnSave {
|
||||
} else if v == "off" {
|
||||
Ok(Self::Value::Off)
|
||||
} else if v == "language_server" {
|
||||
Ok(Self::Value::List(FormatterList(
|
||||
Formatter::LanguageServer { name: None }.into(),
|
||||
)))
|
||||
Ok(Self::Value::List(vec![Formatter::LanguageServer {
|
||||
name: None,
|
||||
}]))
|
||||
} else {
|
||||
let ret: Result<FormatterList, _> =
|
||||
let ret: Result<Vec<Formatter>, _> =
|
||||
Deserialize::deserialize(v.into_deserializer());
|
||||
ret.map(Self::Value::List)
|
||||
}
|
||||
@@ -738,7 +748,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
|
||||
where
|
||||
A: MapAccess<'d>,
|
||||
{
|
||||
let ret: Result<FormatterList, _> =
|
||||
let ret: Result<Vec<Formatter>, _> =
|
||||
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
|
||||
ret.map(Self::Value::List)
|
||||
}
|
||||
@@ -746,7 +756,7 @@ impl<'de> Deserialize<'de> for FormatOnSave {
|
||||
where
|
||||
A: SeqAccess<'d>,
|
||||
{
|
||||
let ret: Result<FormatterList, _> =
|
||||
let ret: Result<Vec<Formatter>, _> =
|
||||
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
|
||||
ret.map(Self::Value::List)
|
||||
}
|
||||
@@ -783,45 +793,30 @@ pub enum SelectedFormatter {
|
||||
/// or falling back to formatting via language server.
|
||||
#[default]
|
||||
Auto,
|
||||
List(FormatterList),
|
||||
List(Vec<Formatter>),
|
||||
}
|
||||
|
||||
impl JsonSchema for SelectedFormatter {
|
||||
fn schema_name() -> String {
|
||||
fn schema_name() -> Cow<'static, str> {
|
||||
"Formatter".into()
|
||||
}
|
||||
|
||||
fn json_schema(generator: &mut schemars::r#gen::SchemaGenerator) -> Schema {
|
||||
let mut schema = SchemaObject::default();
|
||||
fn json_schema(generator: &mut schemars::SchemaGenerator) -> schemars::Schema {
|
||||
let formatter_schema = Formatter::json_schema(generator);
|
||||
schema.instance_type = Some(
|
||||
vec![
|
||||
InstanceType::Object,
|
||||
InstanceType::String,
|
||||
InstanceType::Array,
|
||||
|
||||
json_schema!({
|
||||
"oneOf": [
|
||||
{
|
||||
"type": "array",
|
||||
"items": formatter_schema
|
||||
},
|
||||
{
|
||||
"type": "string",
|
||||
"enum": ["auto", "prettier", "language_server"]
|
||||
},
|
||||
formatter_schema
|
||||
]
|
||||
.into(),
|
||||
);
|
||||
|
||||
let valid_raw_values = SchemaObject {
|
||||
enum_values: Some(vec![
|
||||
Value::String("auto".into()),
|
||||
Value::String("prettier".into()),
|
||||
Value::String("language_server".into()),
|
||||
]),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let mut nested_values = SchemaObject::default();
|
||||
|
||||
nested_values.array().items = Some(formatter_schema.clone().into());
|
||||
|
||||
schema.subschemas().any_of = Some(vec![
|
||||
nested_values.into(),
|
||||
valid_raw_values.into(),
|
||||
formatter_schema,
|
||||
]);
|
||||
schema.into()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -836,6 +831,7 @@ impl Serialize for SelectedFormatter {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error>
|
||||
where
|
||||
@@ -856,11 +852,11 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
if v == "auto" {
|
||||
Ok(Self::Value::Auto)
|
||||
} else if v == "language_server" {
|
||||
Ok(Self::Value::List(FormatterList(
|
||||
Formatter::LanguageServer { name: None }.into(),
|
||||
)))
|
||||
Ok(Self::Value::List(vec![Formatter::LanguageServer {
|
||||
name: None,
|
||||
}]))
|
||||
} else {
|
||||
let ret: Result<FormatterList, _> =
|
||||
let ret: Result<Vec<Formatter>, _> =
|
||||
Deserialize::deserialize(v.into_deserializer());
|
||||
ret.map(SelectedFormatter::List)
|
||||
}
|
||||
@@ -869,7 +865,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
where
|
||||
A: MapAccess<'d>,
|
||||
{
|
||||
let ret: Result<FormatterList, _> =
|
||||
let ret: Result<Vec<Formatter>, _> =
|
||||
Deserialize::deserialize(de::value::MapAccessDeserializer::new(map));
|
||||
ret.map(SelectedFormatter::List)
|
||||
}
|
||||
@@ -877,7 +873,7 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
where
|
||||
A: SeqAccess<'d>,
|
||||
{
|
||||
let ret: Result<FormatterList, _> =
|
||||
let ret: Result<Vec<Formatter>, _> =
|
||||
Deserialize::deserialize(de::value::SeqAccessDeserializer::new(map));
|
||||
ret.map(SelectedFormatter::List)
|
||||
}
|
||||
@@ -885,19 +881,6 @@ impl<'de> Deserialize<'de> for SelectedFormatter {
|
||||
deserializer.deserialize_any(FormatDeserializer)
|
||||
}
|
||||
}
|
||||
/// Controls which formatter should be used when formatting code.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
#[serde(rename_all = "snake_case", transparent)]
|
||||
pub struct FormatterList(pub SingleOrVec<Formatter>);
|
||||
|
||||
impl AsRef<[Formatter]> for FormatterList {
|
||||
fn as_ref(&self) -> &[Formatter] {
|
||||
match &self.0 {
|
||||
SingleOrVec::Single(single) => slice::from_ref(single),
|
||||
SingleOrVec::Vec(v) => v,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Controls which formatter should be used when formatting code. If there are multiple formatters, they are executed in the order of declaration.
|
||||
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq, JsonSchema)]
|
||||
@@ -1209,7 +1192,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
serde_json::from_value(serde_json::to_value(&default_value.defaults)?)?;
|
||||
|
||||
let mut languages = HashMap::default();
|
||||
for (language_name, settings) in &default_value.languages {
|
||||
for (language_name, settings) in &default_value.languages.0 {
|
||||
let mut language_settings = defaults.clone();
|
||||
merge_settings(&mut language_settings, settings);
|
||||
languages.insert(language_name.clone(), language_settings);
|
||||
@@ -1310,7 +1293,7 @@ impl settings::Settings for AllLanguageSettings {
|
||||
}
|
||||
|
||||
// A user's language-specific settings override default language-specific settings.
|
||||
for (language_name, user_language_settings) in &user_settings.languages {
|
||||
for (language_name, user_language_settings) in &user_settings.languages.0 {
|
||||
merge_settings(
|
||||
languages
|
||||
.entry(language_name.clone())
|
||||
@@ -1366,51 +1349,6 @@ impl settings::Settings for AllLanguageSettings {
|
||||
})
|
||||
}
|
||||
|
||||
fn json_schema(
|
||||
generator: &mut schemars::r#gen::SchemaGenerator,
|
||||
params: &settings::SettingsJsonSchemaParams,
|
||||
_: &App,
|
||||
) -> schemars::schema::RootSchema {
|
||||
let mut root_schema = generator.root_schema_for::<Self::FileContent>();
|
||||
|
||||
// Create a schema for a 'languages overrides' object, associating editor
|
||||
// settings with specific languages.
|
||||
assert!(
|
||||
root_schema
|
||||
.definitions
|
||||
.contains_key("LanguageSettingsContent")
|
||||
);
|
||||
|
||||
let languages_object_schema = SchemaObject {
|
||||
instance_type: Some(InstanceType::Object.into()),
|
||||
object: Some(Box::new(ObjectValidation {
|
||||
properties: params
|
||||
.language_names
|
||||
.iter()
|
||||
.map(|name| {
|
||||
(
|
||||
name.clone(),
|
||||
Schema::new_ref("#/definitions/LanguageSettingsContent".into()),
|
||||
)
|
||||
})
|
||||
.collect(),
|
||||
..Default::default()
|
||||
})),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
root_schema
|
||||
.definitions
|
||||
.extend([("Languages".into(), languages_object_schema.into())]);
|
||||
|
||||
add_references_to_properties(
|
||||
&mut root_schema,
|
||||
&[("languages", "#/definitions/Languages")],
|
||||
);
|
||||
|
||||
root_schema
|
||||
}
|
||||
|
||||
fn import_from_vscode(vscode: &settings::VsCodeSettings, current: &mut Self::FileContent) {
|
||||
let d = &mut current.defaults;
|
||||
if let Some(size) = vscode
|
||||
@@ -1674,29 +1612,26 @@ mod tests {
|
||||
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
|
||||
assert_eq!(
|
||||
settings.formatter,
|
||||
Some(SelectedFormatter::List(FormatterList(
|
||||
Formatter::LanguageServer { name: None }.into()
|
||||
)))
|
||||
Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
|
||||
name: None
|
||||
}]))
|
||||
);
|
||||
let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}]}";
|
||||
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
|
||||
assert_eq!(
|
||||
settings.formatter,
|
||||
Some(SelectedFormatter::List(FormatterList(
|
||||
vec![Formatter::LanguageServer { name: None }].into()
|
||||
)))
|
||||
Some(SelectedFormatter::List(vec![Formatter::LanguageServer {
|
||||
name: None
|
||||
}]))
|
||||
);
|
||||
let raw = "{\"formatter\": [{\"language_server\": {\"name\": null}}, \"prettier\"]}";
|
||||
let settings: LanguageSettingsContent = serde_json::from_str(raw).unwrap();
|
||||
assert_eq!(
|
||||
settings.formatter,
|
||||
Some(SelectedFormatter::List(FormatterList(
|
||||
vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::Prettier
|
||||
]
|
||||
.into()
|
||||
)))
|
||||
Some(SelectedFormatter::List(vec![
|
||||
Formatter::LanguageServer { name: None },
|
||||
Formatter::Prettier
|
||||
]))
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user