Compare commits

..

1 Commits

Author SHA1 Message Date
Anthony Eid
96fbc43cef WIP
Co-authored-by: Matt Miller <mattrx@gmail.com>
2025-12-02 17:30:09 -05:00
165 changed files with 4207 additions and 6501 deletions

View File

@@ -25,6 +25,33 @@ on:
description: The app secret for the corresponding app ID
required: true
jobs:
check_extension:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- id: cache-zed-extension-cli
name: extension_tests::cache_zed_extension_cli
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: zed-extension
key: zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}
- name: extension_tests::download_zed_extension_cli
if: steps.cache-zed-extension-cli.outputs.cache-hit != 'true'
run: |
wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension"
chmod +x zed-extension
shell: bash -euxo pipefail {0}
- name: extension_tests::check
run: |
mkdir -p /tmp/ext-scratch
mkdir -p /tmp/ext-output
./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
shell: bash -euxo pipefail {0}
timeout-minutes: 2
check_bump_needed:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
@@ -62,6 +89,7 @@ jobs:
timeout-minutes: 1
bump_extension_version:
needs:
- check_extension
- check_bump_needed
if: |-
(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') &&
@@ -116,6 +144,7 @@ jobs:
timeout-minutes: 1
create_version_label:
needs:
- check_extension
- check_bump_needed
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && github.event_name == 'push' && github.ref == 'refs/heads/main' && needs.check_bump_needed.outputs.needs_bump == 'false'
runs-on: namespace-profile-8x16-ubuntu-2204

132
Cargo.lock generated
View File

@@ -159,7 +159,6 @@ dependencies = [
"derive_more 0.99.20",
"editor",
"env_logger 0.11.8",
"eval_utils",
"fs",
"futures 0.3.31",
"git",
@@ -216,9 +215,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.8.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e639d6b544ad39f5b4e05802db5eb04e1518284eb05fda1839931003e0244c8"
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -227,15 +226,16 @@ dependencies = [
"derive_more 2.0.1",
"futures 0.3.31",
"log",
"parking_lot",
"serde",
"serde_json",
]
[[package]]
name = "agent-client-protocol-schema"
version = "0.9.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f182f5e14bef8232b239719bd99166bb11e986c08fc211f28e392f880d3093ba"
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -328,7 +328,6 @@ dependencies = [
"buffer_diff",
"chrono",
"client",
"clock",
"cloud_llm_client",
"collections",
"command_palette_hooks",
@@ -336,7 +335,6 @@ dependencies = [
"context_server",
"db",
"editor",
"eval_utils",
"extension",
"extension_host",
"feature_flags",
@@ -345,7 +343,6 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
"gpui_tokio",
"html_to_markdown",
"http_client",
"image",
@@ -373,7 +370,6 @@ dependencies = [
"proto",
"rand 0.9.2",
"release_channel",
"reqwest_client",
"rope",
"rules_library",
"schemars",
@@ -2130,15 +2126,30 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec 0.6.3",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
"bit-vec 0.8.0",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit-vec"
version = "0.8.0"
@@ -2317,9 +2328,9 @@ dependencies = [
[[package]]
name = "borrow-or-share"
version = "0.2.4"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
[[package]]
name = "borsh"
@@ -4173,7 +4184,6 @@ dependencies = [
"serde_json",
"smol",
"system_specs",
"windows 0.61.3",
"zstd 0.11.2+zstd.1.5.2",
]
@@ -5765,15 +5775,6 @@ dependencies = [
"watch",
]
[[package]]
name = "eval_utils"
version = "0.1.0"
dependencies = [
"gpui",
"serde",
"smol",
]
[[package]]
name = "event-listener"
version = "2.5.3"
@@ -5993,11 +5994,22 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy-regex"
version = "0.16.2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set",
"bit-set 0.5.3",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fancy-regex"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"regex-automata",
"regex-syntax",
]
@@ -6219,9 +6231,9 @@ checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]]
name = "fluent-uri"
version = "0.4.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
dependencies = [
"borrow-or-share",
"ref-cast",
@@ -6960,7 +6972,7 @@ dependencies = [
[[package]]
name = "gh-workflow"
version = "0.8.0"
source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c"
source = "git+https://github.com/zed-industries/gh-workflow?rev=e5f883040530b4df36437f140084ee5cc7c1c9be#e5f883040530b4df36437f140084ee5cc7c1c9be"
dependencies = [
"async-trait",
"derive_more 2.0.1",
@@ -6977,7 +6989,7 @@ dependencies = [
[[package]]
name = "gh-workflow-macros"
version = "0.8.0"
source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c"
source = "git+https://github.com/zed-industries/gh-workflow?rev=e5f883040530b4df36437f140084ee5cc7c1c9be#e5f883040530b4df36437f140084ee5cc7c1c9be"
dependencies = [
"heck 0.5.0",
"quote",
@@ -7517,17 +7529,6 @@ dependencies = [
"serde",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "hashlink"
version = "0.8.4"
@@ -8617,21 +8618,21 @@ dependencies = [
[[package]]
name = "jsonschema"
version = "0.37.4"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e"
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
dependencies = [
"ahash 0.8.12",
"base64 0.22.1",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex",
"fancy-regex 0.14.0",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"once_cell",
"percent-encoding",
"referencing",
"regex",
@@ -8639,7 +8640,6 @@ dependencies = [
"reqwest 0.12.24",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
@@ -10188,7 +10188,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
dependencies = [
"arrayvec",
"bit-set",
"bit-set 0.8.0",
"bitflags 2.9.4",
"cfg_aliases 0.2.1",
"codespan-reporting 0.12.0",
@@ -13044,7 +13044,7 @@ dependencies = [
"dap",
"dap_adapters",
"extension",
"fancy-regex",
"fancy-regex 0.14.0",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -13915,14 +13915,13 @@ dependencies = [
[[package]]
name = "referencing"
version = "0.37.4"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c"
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
dependencies = [
"ahash 0.8.12",
"fluent-uri",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"once_cell",
"parking_lot",
"percent-encoding",
"serde_json",
@@ -17116,7 +17115,7 @@ dependencies = [
"alacritty_terminal",
"anyhow",
"collections",
"fancy-regex",
"fancy-regex 0.14.0",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
@@ -17350,12 +17349,12 @@ dependencies = [
[[package]]
name = "tiktoken-rs"
version = "0.9.1"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=2570c4387a8505fb8f1d3f3557454b474f1e8271#2570c4387a8505fb8f1d3f3557454b474f1e8271"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=7249f999c5fdf9bf3cc5c288c964454e4dac0c00#7249f999c5fdf9bf3cc5c288c964454e4dac0c00"
dependencies = [
"anyhow",
"base64 0.22.1",
"bstr",
"fancy-regex",
"fancy-regex 0.13.0",
"lazy_static",
"regex",
"rustc-hash 1.1.0",
@@ -18007,9 +18006,9 @@ dependencies = [
[[package]]
name = "tree-sitter-bash"
version = "0.25.1"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062"
checksum = "871b0606e667e98a1237ebdc1b0d7056e0aebfdc3141d12b399865d4cb6ed8a6"
dependencies = [
"cc",
"tree-sitter-language",
@@ -18487,12 +18486,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.19"
@@ -18727,6 +18720,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
@@ -21211,7 +21205,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.217.0"
version = "0.216.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21501,8 +21495,6 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
dependencies = [
"serde",
"serde_json",
@@ -21511,7 +21503,9 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.8.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
dependencies = [
"serde",
"serde_json",
@@ -21529,7 +21523,7 @@ dependencies = [
name = "zed_html"
version = "0.2.3"
dependencies = [
"zed_extension_api 0.7.0",
"zed_extension_api 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
@@ -21543,7 +21537,7 @@ dependencies = [
name = "zed_test_extension"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.8.0",
"zed_extension_api 0.7.0",
]
[[package]]

View File

@@ -59,7 +59,6 @@ members = [
"crates/zeta2_tools",
"crates/editor",
"crates/eval",
"crates/eval_utils",
"crates/explorer_command_injector",
"crates/extension",
"crates/extension_api",
@@ -289,7 +288,6 @@ deepseek = { path = "crates/deepseek" }
derive_refineable = { path = "crates/refineable/derive_refineable" }
diagnostics = { path = "crates/diagnostics" }
editor = { path = "crates/editor" }
eval_utils = { path = "crates/eval_utils" }
extension = { path = "crates/extension" }
extension_host = { path = "crates/extension_host" }
extensions_ui = { path = "crates/extensions_ui" }
@@ -441,7 +439,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "=0.8.0", features = ["unstable"] }
agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = "0.25.1-rc1"
any_vec = "0.14"
@@ -505,12 +503,12 @@ ec4rs = "1.1"
emojis = "0.6.1"
env_logger = "0.11"
exec = "0.3.1"
fancy-regex = "0.16.0"
fancy-regex = "0.14.0"
fork = "0.4.0"
futures = "0.3"
futures-batch = "0.6.1"
futures-lite = "1.13"
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "09acfdf2bd5c1d6254abefd609c808ff73547b2c" }
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "e5f883040530b4df36437f140084ee5cc7c1c9be" }
git2 = { version = "0.20.1", default-features = false }
globset = "0.4"
handlebars = "4.3"
@@ -531,7 +529,7 @@ indoc = "2"
inventory = "0.3.19"
itertools = "0.14.0"
json_dotpath = "1.1"
jsonschema = "0.37.0"
jsonschema = "0.30.0"
jsonwebtoken = "9.3"
jupyter-protocol = "0.10.0"
jupyter-websocket-client = "0.15.0"
@@ -658,7 +656,7 @@ sysinfo = "0.37.0"
take-until = "0.2.0"
tempfile = "3.20.0"
thiserror = "2.0.12"
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "2570c4387a8505fb8f1d3f3557454b474f1e8271" }
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "7249f999c5fdf9bf3cc5c288c964454e4dac0c00" }
time = { version = "0.3", features = [
"macros",
"parsing",
@@ -674,7 +672,7 @@ toml = "0.8"
toml_edit = { version = "0.22", default-features = false, features = ["display", "parse", "serde"] }
tower-http = "0.4.4"
tree-sitter = { version = "0.25.10", features = ["wasm"] }
tree-sitter-bash = "0.25.1"
tree-sitter-bash = "0.25.0"
tree-sitter-c = "0.23"
tree-sitter-cpp = { git = "https://github.com/tree-sitter/tree-sitter-cpp", rev = "5cb9b693cfd7bfacab1d9ff4acac1a4150700609" }
tree-sitter-css = "0.23"

View File

@@ -53,10 +53,6 @@ extension
git
= @cole-miller
= @danilo-leal
= @dvdsk
= @kubkon
= @Anthony-Eid
= @cameron1024
gpui
= @Anthony-Eid

View File

@@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M14 11.333A6 6 0 0 0 4 6.867l-1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M2 4.667v4h4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 0 0-1.333A.667.667 0 0 0 8 12Z"/></svg>

After

Width:  |  Height:  |  Size: 467 B

View File

@@ -1,5 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 13H5" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 13H14" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11.5 8.5L8 12M8 12L4.5 8.5M8 12L8 3" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 10 8 14.667 12.667 10M8 5.333v9.334"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 2.667a.667.667 0 1 0 0-1.334.667.667 0 0 0 0 1.334Z"/></svg>

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 374 B

View File

@@ -1,5 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M4.5 6.5L8 3M8 3L11.5 6.5M8 3V12" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M2 13H5" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M11 13H14" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M3.333 6 8 1.333 12.667 6M8 10.667V1.333"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 13.333a.667.667 0 1 1 0 1.334.667.667 0 0 1 0-1.334Z"/></svg>

Before

Width:  |  Height:  |  Size: 439 B

After

Width:  |  Height:  |  Size: 373 B

View File

@@ -1,5 +1 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M2 11.333C2.00118 10.1752 2.33729 9.04258 2.96777 8.07159C3.59826 7.10059 4.49621 6.33274 5.55331 5.86064C6.61041 5.38853 7.78152 5.23235 8.9254 5.41091C10.0693 5.58947 11.1371 6.09516 12 6.86698L13 7.76698" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M14 4.66699V8.66699H10" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
<path d="M7 13H10" stroke="#C6CAD0" stroke-width="1.2" stroke-linecap="round" stroke-linejoin="round"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="none"><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M2 11.333a6 6 0 0 1 10-4.466l1 .9"/><path stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.333" d="M14 4.667v4h-4"/><path fill="#000" stroke="#000" stroke-linecap="round" stroke-linejoin="round" stroke-width="1.2" d="M8 12a.667.667 0 1 1 0-1.333A.667.667 0 0 1 8 12Z"/></svg>

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 468 B

View File

@@ -1100,22 +1100,13 @@
"preview_tabs": {
// Whether preview tabs should be enabled.
// Preview tabs allow you to open files in preview mode, where they close automatically
// when you open another preview tab.
// when you switch to another file unless you explicitly pin them.
// This is useful for quickly viewing files without cluttering your workspace.
"enabled": true,
// Whether to open tabs in preview mode when opened from the project panel with a single click.
"enable_preview_from_project_panel": true,
// Whether to open tabs in preview mode when selected from the file finder.
"enable_preview_from_file_finder": false,
// Whether to open tabs in preview mode when opened from a multibuffer.
"enable_preview_from_multibuffer": true,
// Whether to open tabs in preview mode when code navigation is used to open a multibuffer.
"enable_preview_multibuffer_from_code_navigation": false,
// Whether to open tabs in preview mode when code navigation is used to open a single file.
"enable_preview_file_from_code_navigation": true,
// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
"enable_keep_preview_on_code_navigation": false
// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
"enable_preview_from_code_navigation": false
},
// Settings related to the file finder.
"file_finder": {

View File

@@ -45,7 +45,6 @@
"tab.inactive_background": "#1f2127ff",
"tab.active_background": "#0d1016ff",
"search.match_background": "#5ac2fe66",
"search.active_match_background": "#ea570166",
"panel.background": "#1f2127ff",
"panel.focused_border": "#5ac1feff",
"pane.focused_border": null,
@@ -437,7 +436,6 @@
"tab.inactive_background": "#ececedff",
"tab.active_background": "#fcfcfcff",
"search.match_background": "#3b9ee566",
"search.active_match_background": "#f88b3666",
"panel.background": "#ececedff",
"panel.focused_border": "#3b9ee5ff",
"pane.focused_border": null,
@@ -829,7 +827,6 @@
"tab.inactive_background": "#353944ff",
"tab.active_background": "#242835ff",
"search.match_background": "#73cffe66",
"search.active_match_background": "#fd722b66",
"panel.background": "#353944ff",
"panel.focused_border": null,
"pane.focused_border": null,

View File

@@ -46,7 +46,6 @@
"tab.inactive_background": "#3a3735ff",
"tab.active_background": "#282828ff",
"search.match_background": "#83a59866",
"search.active_match_background": "#c09f3f66",
"panel.background": "#3a3735ff",
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
@@ -453,7 +452,6 @@
"tab.inactive_background": "#393634ff",
"tab.active_background": "#1d2021ff",
"search.match_background": "#83a59866",
"search.active_match_background": "#c9653666",
"panel.background": "#393634ff",
"panel.focused_border": "#83a598ff",
"pane.focused_border": null,
@@ -860,7 +858,6 @@
"tab.inactive_background": "#3b3735ff",
"tab.active_background": "#32302fff",
"search.match_background": "#83a59866",
"search.active_match_background": "#aea85166",
"panel.background": "#3b3735ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -1267,7 +1264,6 @@
"tab.inactive_background": "#ecddb4ff",
"tab.active_background": "#fbf1c7ff",
"search.match_background": "#0b667866",
"search.active_match_background": "#ba2d1166",
"panel.background": "#ecddb4ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -1674,7 +1670,6 @@
"tab.inactive_background": "#ecddb5ff",
"tab.active_background": "#f9f5d7ff",
"search.match_background": "#0b667866",
"search.active_match_background": "#dc351466",
"panel.background": "#ecddb5ff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -2081,7 +2076,6 @@
"tab.inactive_background": "#ecdcb3ff",
"tab.active_background": "#f2e5bcff",
"search.match_background": "#0b667866",
"search.active_match_background": "#d7331466",
"panel.background": "#ecdcb3ff",
"panel.focused_border": null,
"pane.focused_border": null,

View File

@@ -45,7 +45,6 @@
"tab.inactive_background": "#2f343eff",
"tab.active_background": "#282c33ff",
"search.match_background": "#74ade866",
"search.active_match_background": "#e8af7466",
"panel.background": "#2f343eff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -97,11 +96,11 @@
"terminal.ansi.bright_white": "#fafafaff",
"terminal.ansi.dim_white": "#575d65ff",
"link_text.hover": "#74ade8ff",
"version_control.added": "#27a657ff",
"version_control.added": "#2EA04833",
"version_control.modified": "#d3b020ff",
"version_control.word_added": "#2EA04859",
"version_control.word_deleted": "#78081BCC",
"version_control.deleted": "#e06c76ff",
"version_control.word_added": "#2EA0483D",
"version_control.word_deleted": "#78081BB3",
"version_control.deleted": "#78081B66",
"version_control.conflict_marker.ours": "#a1c1811a",
"version_control.conflict_marker.theirs": "#74ade81a",
"conflict": "#dec184ff",
@@ -449,7 +448,6 @@
"tab.inactive_background": "#ebebecff",
"tab.active_background": "#fafafaff",
"search.match_background": "#5c79e266",
"search.active_match_background": "#d0a92366",
"panel.background": "#ebebecff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -501,11 +499,11 @@
"terminal.ansi.bright_white": "#ffffffff",
"terminal.ansi.dim_white": "#aaaaaaff",
"link_text.hover": "#5c78e2ff",
"version_control.added": "#27a657ff",
"version_control.added": "#2EA04833",
"version_control.modified": "#d3b020ff",
"version_control.word_added": "#2EA04859",
"version_control.word_deleted": "#F85149CC",
"version_control.deleted": "#e06c76ff",
"version_control.word_added": "#2EA0483D",
"version_control.word_deleted": "#F851493D",
"version_control.deleted": "#F8514929",
"conflict": "#a48819ff",
"conflict.background": "#faf2e6ff",
"conflict.border": "#f4e7d1ff",

View File

@@ -201,19 +201,17 @@ impl ToolCall {
};
let mut content = Vec::with_capacity(tool_call.content.len());
for item in tool_call.content {
if let Some(item) = ToolCallContent::from_acp(
content.push(ToolCallContent::from_acp(
item,
language_registry.clone(),
path_style,
terminals,
cx,
)? {
content.push(item);
}
)?);
}
let result = Self {
id: tool_call.tool_call_id,
id: tool_call.id,
label: cx
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
kind: tool_call.kind,
@@ -243,7 +241,6 @@ impl ToolCall {
locations,
raw_input,
raw_output,
..
} = fields;
if let Some(kind) = kind {
@@ -265,29 +262,21 @@ impl ToolCall {
}
if let Some(content) = content {
let mut new_content_len = content.len();
let new_content_len = content.len();
let mut content = content.into_iter();
// Reuse existing content if we can
for (old, new) in self.content.iter_mut().zip(content.by_ref()) {
let valid_content =
old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
if !valid_content {
new_content_len -= 1;
}
old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
}
for new in content {
if let Some(new) = ToolCallContent::from_acp(
self.content.push(ToolCallContent::from_acp(
new,
language_registry.clone(),
path_style,
terminals,
cx,
)? {
self.content.push(new);
} else {
new_content_len -= 1;
}
)?)
}
self.content.truncate(new_content_len);
}
@@ -436,7 +425,6 @@ impl From<acp::ToolCallStatus> for ToolCallStatus {
acp::ToolCallStatus::InProgress => Self::InProgress,
acp::ToolCallStatus::Completed => Self::Completed,
acp::ToolCallStatus::Failed => Self::Failed,
_ => Self::Pending,
}
}
}
@@ -549,7 +537,7 @@ impl ContentBlock {
..
}) => Self::resource_link_md(&uri, path_style),
acp::ContentBlock::Image(image) => Self::image_md(&image),
_ => String::new(),
acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(),
}
}
@@ -603,17 +591,15 @@ impl ToolCallContent {
path_style: PathStyle,
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
) -> Result<Option<Self>> {
) -> Result<Self> {
match content {
acp::ToolCallContent::Content(acp::Content { content, .. }) => {
Ok(Some(Self::ContentBlock(ContentBlock::new(
content,
&language_registry,
path_style,
cx,
))))
}
acp::ToolCallContent::Diff(diff) => Ok(Some(Self::Diff(cx.new(|cx| {
acp::ToolCallContent::Content { content } => Ok(Self::ContentBlock(ContentBlock::new(
content,
&language_registry,
path_style,
cx,
))),
acp::ToolCallContent::Diff { diff } => Ok(Self::Diff(cx.new(|cx| {
Diff::finalized(
diff.path.to_string_lossy().into_owned(),
diff.old_text,
@@ -621,13 +607,12 @@ impl ToolCallContent {
language_registry,
cx,
)
})))),
acp::ToolCallContent::Terminal(acp::Terminal { terminal_id, .. }) => terminals
}))),
acp::ToolCallContent::Terminal { terminal_id } => terminals
.get(&terminal_id)
.cloned()
.map(|terminal| Some(Self::Terminal(terminal)))
.map(Self::Terminal)
.ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)),
_ => Ok(None),
}
}
@@ -638,9 +623,9 @@ impl ToolCallContent {
path_style: PathStyle,
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
cx: &mut App,
) -> Result<bool> {
) -> Result<()> {
let needs_update = match (&self, &new) {
(Self::Diff(old_diff), acp::ToolCallContent::Diff(new_diff)) => {
(Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
old_diff.read(cx).needs_update(
new_diff.old_text.as_deref().unwrap_or(""),
&new_diff.new_text,
@@ -650,14 +635,10 @@ impl ToolCallContent {
_ => true,
};
if let Some(update) = Self::from_acp(new, language_registry, path_style, terminals, cx)? {
if needs_update {
*self = update;
}
Ok(true)
} else {
Ok(false)
if needs_update {
*self = Self::from_acp(new, language_registry, path_style, terminals, cx)?;
}
Ok(())
}
pub fn to_markdown(&self, cx: &App) -> String {
@@ -679,7 +660,7 @@ pub enum ToolCallUpdate {
impl ToolCallUpdate {
fn id(&self) -> &acp::ToolCallId {
match self {
Self::UpdateFields(update) => &update.tool_call_id,
Self::UpdateFields(update) => &update.id,
Self::UpdateDiff(diff) => &diff.id,
Self::UpdateTerminal(terminal) => &terminal.id,
}
@@ -751,7 +732,6 @@ impl Plan {
acp::PlanEntryStatus::Completed => {
stats.completed += 1;
}
_ => {}
}
}
@@ -1174,7 +1154,6 @@ impl AcpThread {
current_mode_id,
..
}) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
_ => {}
}
Ok(())
}
@@ -1308,7 +1287,11 @@ impl AcpThread {
label: cx.new(|cx| Markdown::new("Tool call not found".into(), None, None, cx)),
kind: acp::ToolKind::Fetch,
content: vec![ToolCallContent::ContentBlock(ContentBlock::new(
"Tool call not found".into(),
acp::ContentBlock::Text(acp::TextContent {
text: "Tool call not found".to_string(),
annotations: None,
meta: None,
}),
&languages,
path_style,
cx,
@@ -1332,7 +1315,7 @@ impl AcpThread {
let location_updated = update.fields.locations.is_some();
call.update_fields(update.fields, languages, path_style, &self.terminals, cx)?;
if location_updated {
self.resolve_locations(update.tool_call_id, cx);
self.resolve_locations(update.id, cx);
}
}
ToolCallUpdate::UpdateDiff(update) => {
@@ -1370,7 +1353,7 @@ impl AcpThread {
) -> Result<(), acp::Error> {
let language_registry = self.project.read(cx).languages().clone();
let path_style = self.project.read(cx).path_style(cx);
let id = update.tool_call_id.clone();
let id = update.id.clone();
let agent = self.connection().telemetry_id();
let session = self.session_id();
@@ -1535,16 +1518,16 @@ impl AcpThread {
// some tools would (incorrectly) continue to auto-accept.
if let Some(allow_once_option) = options.iter().find_map(|option| {
if matches!(option.kind, acp::PermissionOptionKind::AllowOnce) {
Some(option.option_id.clone())
Some(option.id.clone())
} else {
None
}
}) {
self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?;
return Ok(async {
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
allow_once_option,
))
acp::RequestPermissionOutcome::Selected {
option_id: allow_once_option,
}
}
.boxed());
}
@@ -1560,9 +1543,7 @@ impl AcpThread {
let fut = async {
match rx.await {
Ok(option) => acp::RequestPermissionOutcome::Selected(
acp::SelectedPermissionOutcome::new(option),
),
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
}
}
@@ -1589,7 +1570,6 @@ impl AcpThread {
acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => {
ToolCallStatus::InProgress
}
_ => ToolCallStatus::InProgress,
};
let curr_status = mem::replace(&mut call.status, new_status);
@@ -1668,7 +1648,14 @@ impl AcpThread {
message: &str,
cx: &mut Context<Self>,
) -> BoxFuture<'static, Result<()>> {
self.send(vec![message.into()], cx)
self.send(
vec![acp::ContentBlock::Text(acp::TextContent {
text: message.to_string(),
annotations: None,
meta: None,
})],
cx,
)
}
pub fn send(
@@ -1682,7 +1669,11 @@ impl AcpThread {
self.project.read(cx).path_style(cx),
cx,
);
let request = acp::PromptRequest::new(self.session_id.clone(), message.clone());
let request = acp::PromptRequest {
prompt: message.clone(),
session_id: self.session_id.clone(),
meta: None,
};
let git_store = self.project.read(cx).git_store().clone();
let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
@@ -1774,7 +1765,7 @@ impl AcpThread {
result,
Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
..
meta: None,
}))
);
@@ -1790,7 +1781,7 @@ impl AcpThread {
// Handle refusal - distinguish between user prompt and tool call refusals
if let Ok(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
..
meta: _,
})) = result
{
if let Some((user_msg_ix, _)) = this.last_user_message() {
@@ -2026,7 +2017,7 @@ impl AcpThread {
})?;
Ok(project.open_buffer(path, cx))
})
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
.map_err(|e| acp::Error::internal_error().with_data(e.to_string()))
.flatten()?;
let buffer = load.await?;
@@ -2059,7 +2050,7 @@ impl AcpThread {
let start_position = Point::new(line, 0);
if start_position > max_point {
return Err(acp::Error::invalid_params().data(format!(
return Err(acp::Error::invalid_params().with_data(format!(
"Attempting to read beyond the end of the file, line {}:{}",
max_point.row + 1,
max_point.column
@@ -2211,7 +2202,7 @@ impl AcpThread {
let language_registry = project.read(cx).languages().clone();
let is_windows = project.read(cx).path_style(cx).is_windows();
let terminal_id = acp::TerminalId::new(Uuid::new_v4().to_string());
let terminal_id = acp::TerminalId(Uuid::new_v4().to_string().into());
let terminal_task = cx.spawn({
let terminal_id = terminal_id.clone();
async move |_this, cx| {
@@ -2421,7 +2412,7 @@ mod tests {
.await
.unwrap();
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
// Send Output BEFORE Created - should be buffered by acp_thread
thread.update(cx, |thread, cx| {
@@ -2483,7 +2474,7 @@ mod tests {
.await
.unwrap();
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
// Send Output BEFORE Created
thread.update(cx, |thread, cx| {
@@ -2501,7 +2492,11 @@ mod tests {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id.clone(),
status: acp::TerminalExitStatus::new().exit_code(0),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
@@ -2558,7 +2553,15 @@ mod tests {
// Test creating a new user message
thread.update(cx, |thread, cx| {
thread.push_user_content_block(None, "Hello, ".into(), cx);
thread.push_user_content_block(
None,
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Hello, ".to_string(),
meta: None,
}),
cx,
);
});
thread.update(cx, |thread, cx| {
@@ -2574,7 +2577,15 @@ mod tests {
// Test appending to existing user message
let message_1_id = UserMessageId::new();
thread.update(cx, |thread, cx| {
thread.push_user_content_block(Some(message_1_id.clone()), "world!".into(), cx);
thread.push_user_content_block(
Some(message_1_id.clone()),
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "world!".to_string(),
meta: None,
}),
cx,
);
});
thread.update(cx, |thread, cx| {
@@ -2589,14 +2600,26 @@ mod tests {
// Test creating new user message after assistant message
thread.update(cx, |thread, cx| {
thread.push_assistant_content_block("Assistant response".into(), false, cx);
thread.push_assistant_content_block(
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "Assistant response".to_string(),
meta: None,
}),
false,
cx,
);
});
let message_2_id = UserMessageId::new();
thread.update(cx, |thread, cx| {
thread.push_user_content_block(
Some(message_2_id.clone()),
"New user message".into(),
acp::ContentBlock::Text(acp::TextContent {
annotations: None,
text: "New user message".to_string(),
meta: None,
}),
cx,
);
});
@@ -2624,22 +2647,27 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
"Thinking ".into(),
)),
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
content: "Thinking ".into(),
meta: None,
}),
cx,
)
.unwrap();
thread
.handle_session_update(
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
"hard!".into(),
)),
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
content: "hard!".into(),
meta: None,
}),
cx,
)
.unwrap();
})?;
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
},
@@ -2707,7 +2735,10 @@ mod tests {
.unwrap()
.await
.unwrap();
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
},
@@ -2938,7 +2969,7 @@ mod tests {
let fs = FakeFs::new(cx.executor());
let project = Project::test(fs, [], cx).await;
let id = acp::ToolCallId::new("test");
let id = acp::ToolCallId("test".into());
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
let id = id.clone();
@@ -2948,17 +2979,26 @@ mod tests {
thread
.update(&mut cx, |thread, cx| {
thread.handle_session_update(
acp::SessionUpdate::ToolCall(
acp::ToolCall::new(id.clone(), "Label")
.kind(acp::ToolKind::Fetch)
.status(acp::ToolCallStatus::InProgress),
),
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: id.clone(),
title: "Label".into(),
kind: acp::ToolKind::Fetch,
status: acp::ToolCallStatus::InProgress,
content: vec![],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
}),
cx,
)
})
.unwrap()
.unwrap();
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
}
@@ -3000,10 +3040,14 @@ mod tests {
thread
.update(cx, |thread, cx| {
thread.handle_session_update(
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
id,
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
)),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
..Default::default()
},
meta: None,
}),
cx,
)
})
@@ -3035,21 +3079,33 @@ mod tests {
thread
.update(&mut cx, |thread, cx| {
thread.handle_session_update(
acp::SessionUpdate::ToolCall(
acp::ToolCall::new("test", "Label")
.kind(acp::ToolKind::Edit)
.status(acp::ToolCallStatus::Completed)
.content(vec![acp::ToolCallContent::Diff(acp::Diff::new(
"/test/test.txt",
"foo",
))]),
),
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("test".into()),
title: "Label".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Completed,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/test/test.txt".into(),
old_text: None,
new_text: "foo".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
}),
cx,
)
})
.unwrap()
.unwrap();
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
}
@@ -3102,14 +3158,18 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
content.text.to_uppercase().into(),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: content.text.to_uppercase().into(),
meta: None,
}),
cx,
)
.unwrap();
})?;
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
}
@@ -3265,22 +3325,34 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::ToolCall(
acp::ToolCall::new("tool1", "Test Tool")
.kind(acp::ToolKind::Fetch)
.status(acp::ToolCallStatus::Completed)
.raw_input(serde_json::json!({"query": "test"}))
.raw_output(serde_json::json!({"result": "inappropriate content"})),
),
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool1".into()),
title: "Test Tool".into(),
kind: acp::ToolKind::Fetch,
status: acp::ToolCallStatus::Completed,
content: vec![],
locations: vec![],
raw_input: Some(serde_json::json!({"query": "test"})),
raw_output: Some(
serde_json::json!({"result": "inappropriate content"}),
),
meta: None,
}),
cx,
)
.unwrap();
})?;
// Now return refusal because of the tool result
Ok(acp::PromptResponse::new(acp::StopReason::Refusal))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
})
} else {
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
}
.boxed_local()
@@ -3308,7 +3380,16 @@ mod tests {
});
// Send a user message - this will trigger tool call and then refusal
let send_task = thread.update(cx, |thread, cx| thread.send(vec!["Hello".into()], cx));
let send_task = thread.update(cx, |thread, cx| {
thread.send(
vec![acp::ContentBlock::Text(acp::TextContent {
text: "Hello".into(),
annotations: None,
meta: None,
})],
cx,
)
});
cx.background_executor.spawn(send_task).detach();
cx.run_until_parked();
@@ -3354,11 +3435,21 @@ mod tests {
let refuse_next = refuse_next.clone();
move |_request, _thread, _cx| {
if refuse_next.load(SeqCst) {
async move { Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) }
.boxed_local()
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
})
}
.boxed_local()
} else {
async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }
.boxed_local()
async move {
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
}
}
}));
@@ -3415,7 +3506,10 @@ mod tests {
let refuse_next = refuse_next.clone();
async move {
if refuse_next.load(SeqCst) {
return Ok(acp::PromptResponse::new(acp::StopReason::Refusal));
return Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
});
}
let acp::ContentBlock::Text(content) = &request.prompt[0] else {
@@ -3424,14 +3518,18 @@ mod tests {
thread.update(&mut cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
content.text.to_uppercase().into(),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: content.text.to_uppercase().into(),
meta: None,
}),
cx,
)
.unwrap();
})?;
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
}
.boxed_local()
}
@@ -3570,12 +3668,13 @@ mod tests {
_cwd: &Path,
cx: &mut App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId::new(
let session_id = acp::SessionId(
rand::rng()
.sample_iter(&distr::Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>(),
.collect::<String>()
.into(),
);
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|cx| {
@@ -3585,12 +3684,12 @@ mod tests {
project,
action_log,
session_id.clone(),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
});
@@ -3619,7 +3718,10 @@ mod tests {
let thread = thread.clone();
cx.spawn(async move |cx| handler(params, thread, cx.clone()).await)
} else {
Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
}))
}
}
@@ -3674,13 +3776,17 @@ mod tests {
.unwrap();
// Try to update a tool call that doesn't exist
let nonexistent_id = acp::ToolCallId::new("nonexistent-tool-call");
let nonexistent_id = acp::ToolCallId("nonexistent-tool-call".into());
thread.update(cx, |thread, cx| {
let result = thread.handle_session_update(
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
nonexistent_id.clone(),
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
)),
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
id: nonexistent_id.clone(),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
..Default::default()
},
meta: None,
}),
cx,
);
@@ -3755,7 +3861,7 @@ mod tests {
.unwrap();
// Create 2 terminals BEFORE the checkpoint that have completed running
let terminal_id_1 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let terminal_id_1 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal_1 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
@@ -3794,13 +3900,17 @@ mod tests {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_1.clone(),
status: acp::TerminalExitStatus::new().exit_code(0),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
});
let terminal_id_2 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let terminal_id_2 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal_2 = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
@@ -3839,7 +3949,11 @@ mod tests {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id: terminal_id_2.clone(),
status: acp::TerminalExitStatus::new().exit_code(0),
status: acp::TerminalExitStatus {
exit_code: Some(0),
signal: None,
meta: None,
},
},
cx,
);
@@ -3859,7 +3973,7 @@ mod tests {
// Create a terminal AFTER the checkpoint we'll restore to.
// This simulates the AI agent starting a long-running terminal command.
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let mock_terminal = cx.new(|cx| {
let builder = ::terminal::TerminalBuilder::new_display_only(
::terminal::terminal_settings::CursorShape::default(),
@@ -3901,15 +4015,21 @@ mod tests {
thread.update(cx, |thread, cx| {
thread
.handle_session_update(
acp::SessionUpdate::ToolCall(
acp::ToolCall::new("terminal-tool-1", "Running command")
.kind(acp::ToolKind::Execute)
.status(acp::ToolCallStatus::InProgress)
.content(vec![acp::ToolCallContent::Terminal(acp::Terminal::new(
terminal_id.clone(),
))])
.raw_input(serde_json::json!({"command": "sleep 1000", "cd": "/test"})),
),
acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("terminal-tool-1".into()),
title: "Running command".into(),
kind: acp::ToolKind::Execute,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Terminal {
terminal_id: terminal_id.clone(),
}],
locations: vec![],
raw_input: Some(
serde_json::json!({"command": "sleep 1000", "cd": "/test"}),
),
raw_output: None,
meta: None,
}),
cx,
)
.unwrap();

View File

@@ -336,7 +336,7 @@ mod test_support {
_cwd: &Path,
cx: &mut gpui::App,
) -> Task<gpui::Result<Entity<AcpThread>>> {
let session_id = acp::SessionId::new(self.sessions.lock().len().to_string());
let session_id = acp::SessionId(self.sessions.lock().len().to_string().into());
let action_log = cx.new(|_| ActionLog::new(project.clone()));
let thread = cx.new(|cx| {
AcpThread::new(
@@ -345,12 +345,12 @@ mod test_support {
project,
action_log,
session_id.clone(),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
});
@@ -389,7 +389,10 @@ mod test_support {
response_tx.replace(tx);
cx.spawn(async move |_| {
let stop_reason = rx.await?;
Ok(acp::PromptResponse::new(stop_reason))
Ok(acp::PromptResponse {
stop_reason,
meta: None,
})
})
} else {
for update in self.next_prompt_updates.lock().drain(..) {
@@ -397,7 +400,7 @@ mod test_support {
let update = update.clone();
let permission_request = if let acp::SessionUpdate::ToolCall(tool_call) =
&update
&& let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
&& let Some(options) = self.permission_requests.get(&tool_call.id)
{
Some((tool_call.clone(), options.clone()))
} else {
@@ -426,7 +429,10 @@ mod test_support {
cx.spawn(async move |_| {
try_join_all(tasks).await?;
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
})
}
}

View File

@@ -108,7 +108,7 @@ impl MentionUri {
if let Some(thread_id) = path.strip_prefix("/agent/thread/") {
let name = single_query_param(&url, "name")?.context("Missing thread name")?;
Ok(Self::Thread {
id: acp::SessionId::new(thread_id),
id: acp::SessionId(thread_id.into()),
name,
})
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {

View File

@@ -75,15 +75,11 @@ impl Terminal {
let exit_status = exit_status.map(portable_pty::ExitStatus::from);
let mut status = acp::TerminalExitStatus::new();
if let Some(exit_status) = exit_status.as_ref() {
status = status.exit_code(exit_status.exit_code());
if let Some(signal) = exit_status.signal() {
status = status.signal(signal);
}
acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
meta: None,
}
status
})
.shared(),
}
@@ -105,23 +101,27 @@ impl Terminal {
pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
if let Some(output) = self.output.as_ref() {
let mut exit_status = acp::TerminalExitStatus::new();
if let Some(status) = output.exit_status.map(portable_pty::ExitStatus::from) {
exit_status = exit_status.exit_code(status.exit_code());
if let Some(signal) = status.signal() {
exit_status = exit_status.signal(signal);
}
}
let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
acp::TerminalOutputResponse::new(
output.content.clone(),
output.original_content_len > output.content.len(),
)
.exit_status(exit_status)
acp::TerminalOutputResponse {
output: output.content.clone(),
truncated: output.original_content_len > output.content.len(),
exit_status: Some(acp::TerminalExitStatus {
exit_code: exit_status.as_ref().map(|e| e.exit_code()),
signal: exit_status.and_then(|e| e.signal().map(Into::into)),
meta: None,
}),
meta: None,
}
} else {
let (current_content, original_len) = self.truncated_output(cx);
let truncated = current_content.len() < original_len;
acp::TerminalOutputResponse::new(current_content, truncated)
acp::TerminalOutputResponse {
truncated: current_content.len() < original_len,
output: current_content,
exit_status: None,
meta: None,
}
}
}

View File

@@ -83,7 +83,6 @@ ctor.workspace = true
db = { workspace = true, "features" = ["test-support"] }
editor = { workspace = true, "features" = ["test-support"] }
env_logger.workspace = true
eval_utils.workspace = true
fs = { workspace = true, "features" = ["test-support"] }
git = { workspace = true, "features" = ["test-support"] }
gpui = { workspace = true, "features" = ["test-support"] }

View File

@@ -170,7 +170,7 @@ impl LanguageModels {
}
fn model_id(model: &Arc<dyn LanguageModel>) -> acp::ModelId {
acp::ModelId::new(format!("{}/{}", model.provider_id().0, model.id().0))
acp::ModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
}
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
@@ -789,12 +789,28 @@ impl NativeAgentConnection {
}
ThreadEvent::AgentText(text) => {
acp_thread.update(cx, |thread, cx| {
thread.push_assistant_content_block(text.into(), false, cx)
thread.push_assistant_content_block(
acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
false,
cx,
)
})?;
}
ThreadEvent::AgentThinking(text) => {
acp_thread.update(cx, |thread, cx| {
thread.push_assistant_content_block(text.into(), true, cx)
thread.push_assistant_content_block(
acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
true,
cx,
)
})?;
}
ThreadEvent::ToolCallAuthorization(ToolCallAuthorization {
@@ -808,9 +824,8 @@ impl NativeAgentConnection {
)
})??;
cx.background_spawn(async move {
if let acp::RequestPermissionOutcome::Selected(
acp::SelectedPermissionOutcome { option_id, .. },
) = outcome_task.await
if let acp::RequestPermissionOutcome::Selected { option_id } =
outcome_task.await
{
response
.send(option_id)
@@ -837,7 +852,10 @@ impl NativeAgentConnection {
}
ThreadEvent::Stop(stop_reason) => {
log::debug!("Assistant message complete: {:?}", stop_reason);
return Ok(acp::PromptResponse::new(stop_reason));
return Ok(acp::PromptResponse {
stop_reason,
meta: None,
});
}
}
}
@@ -849,7 +867,10 @@ impl NativeAgentConnection {
}
log::debug!("Response stream completed");
anyhow::Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
anyhow::Ok(acp::PromptResponse {
stop_reason: acp::StopReason::EndTurn,
meta: None,
})
})
}
}
@@ -1353,7 +1374,7 @@ mod internal_tests {
IndexMap::from_iter([(
AgentModelGroupName("Fake".into()),
vec![AgentModelInfo {
id: acp::ModelId::new("fake/fake"),
id: acp::ModelId("fake/fake".into()),
name: "Fake".into(),
description: None,
icon: Some(ui::IconName::ZedAssistant),
@@ -1414,7 +1435,7 @@ mod internal_tests {
// Select a model
let selector = connection.model_selector(&session_id).unwrap();
let model_id = acp::ModelId::new("fake/fake");
let model_id = acp::ModelId("fake/fake".into());
cx.update(|cx| selector.select_model(model_id.clone(), cx))
.await
.unwrap();
@@ -1500,14 +1521,20 @@ mod internal_tests {
thread.send(
vec![
"What does ".into(),
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
"b.md",
MentionUri::File {
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: "b.md".into(),
uri: MentionUri::File {
abs_path: path!("/a/b.md").into(),
}
.to_uri()
.to_string(),
)),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
" mean?".into(),
],
cx,

View File

@@ -366,7 +366,7 @@ impl ThreadsDatabase {
for (id, summary, updated_at) in rows {
threads.push(DbThreadMetadata {
id: acp::SessionId::new(id),
id: acp::SessionId(id),
title: summary.into(),
updated_at: DateTime::parse_from_rfc3339(&updated_at)?.with_timezone(&Utc),
});

View File

@@ -4,7 +4,7 @@ use crate::{
};
use Role::*;
use client::{Client, UserStore};
use eval_utils::{EvalOutput, EvalOutputProcessor, OutcomeKind};
use collections::HashMap;
use fs::FakeFs;
use futures::{FutureExt, future::LocalBoxFuture};
use gpui::{AppContext, TestAppContext, Timer};
@@ -20,62 +20,16 @@ use rand::prelude::*;
use reqwest_client::ReqwestClient;
use serde_json::json;
use std::{
cmp::Reverse,
fmt::{self, Display},
io::Write as _,
path::Path,
str::FromStr,
sync::mpsc,
time::Duration,
};
use util::path;
#[derive(Default, Clone, Debug)]
struct EditAgentOutputProcessor {
mismatched_tag_threshold: f32,
cumulative_tags: usize,
cumulative_mismatched_tags: usize,
eval_outputs: Vec<EvalOutput<EditEvalMetadata>>,
}
fn mismatched_tag_threshold(mismatched_tag_threshold: f32) -> EditAgentOutputProcessor {
EditAgentOutputProcessor {
mismatched_tag_threshold,
cumulative_tags: 0,
cumulative_mismatched_tags: 0,
eval_outputs: Vec::new(),
}
}
#[derive(Clone, Debug)]
struct EditEvalMetadata {
tags: usize,
mismatched_tags: usize,
}
impl EvalOutputProcessor for EditAgentOutputProcessor {
type Metadata = EditEvalMetadata;
fn process(&mut self, output: &EvalOutput<Self::Metadata>) {
if matches!(output.outcome, OutcomeKind::Passed | OutcomeKind::Failed) {
self.cumulative_mismatched_tags += output.metadata.mismatched_tags;
self.cumulative_tags += output.metadata.tags;
self.eval_outputs.push(output.clone());
}
}
fn assert(&mut self) {
let mismatched_tag_ratio =
self.cumulative_mismatched_tags as f32 / self.cumulative_tags as f32;
if mismatched_tag_ratio > self.mismatched_tag_threshold {
for eval_output in &self.eval_outputs {
println!("{}", eval_output.data);
}
panic!(
"Too many mismatched tags: {:?}",
self.cumulative_mismatched_tags
);
}
}
}
#[test]
#[cfg_attr(not(feature = "unit-eval"), ignore)]
fn eval_extract_handle_command_output() {
@@ -101,19 +55,22 @@ fn eval_extract_handle_command_output() {
include_str!("evals/fixtures/extract_handle_command_output/possible-07.diff"),
];
let edit_description = "Extract `handle_command_output` method from `run_git_blame`.";
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
Read the `{input_file_path}` file and extract a method in
the final stanza of `run_git_blame` to deal with command failures,
call it `handle_command_output` and take the std::process::Output as the only parameter.
Do not document the method and do not add any comments.
Read the `{input_file_path}` file and extract a method in
the final stanza of `run_git_blame` to deal with command failures,
call it `handle_command_output` and take the std::process::Output as the only parameter.
Do not document the method and do not add any comments.
Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`.
"})],
Add it right next to `run_git_blame` and copy it verbatim from `run_git_blame`.
"})],
),
message(
Assistant,
@@ -145,9 +102,9 @@ fn eval_extract_handle_command_output() {
),
],
Some(input_file_content.into()),
EvalAssertion::assert_diff_any(possible_diffs.clone()),
))
});
EvalAssertion::assert_diff_any(possible_diffs),
),
);
}
#[test]
@@ -165,16 +122,18 @@ fn eval_delete_run_git_blame() {
let input_file_content = include_str!("evals/fixtures/delete_run_git_blame/before.rs");
let output_file_content = include_str!("evals/fixtures/delete_run_git_blame/after.rs");
let edit_description = "Delete the `run_git_blame` function.";
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
Read the `{input_file_path}` file and delete `run_git_blame`. Just that
one function, not its usages.
"})],
Read the `{input_file_path}` file and delete `run_git_blame`. Just that
one function, not its usages.
"})],
),
message(
Assistant,
@@ -207,8 +166,8 @@ fn eval_delete_run_git_blame() {
],
Some(input_file_content.into()),
EvalAssertion::assert_eq(output_file_content),
))
});
),
);
}
#[test]
@@ -226,16 +185,18 @@ fn eval_translate_doc_comments() {
let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/translate_doc_comments/before.rs");
let edit_description = "Translate all doc comments to Italian";
eval_utils::eval(200, 1., mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
eval(
200,
1.,
0.05,
EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
Read the {input_file_path} file and edit it (without overwriting it),
translating all the doc comments to italian.
"})],
Read the {input_file_path} file and edit it (without overwriting it),
translating all the doc comments to italian.
"})],
),
message(
Assistant,
@@ -268,8 +229,8 @@ fn eval_translate_doc_comments() {
],
Some(input_file_content.into()),
EvalAssertion::judge_diff("Doc comments were translated to Italian"),
))
});
),
);
}
#[test]
@@ -288,31 +249,33 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
let input_file_content =
include_str!("evals/fixtures/use_wasi_sdk_in_compile_parser_to_wasm/before.rs");
let edit_description = "Update compile_parser_to_wasm to use wasi-sdk instead of emscripten";
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
eval(
100,
0.95,
0.05,
EvalInput::from_conversation(
vec![
message(
User,
[text(formatdoc! {"
Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten.
Use `ureq` to download the SDK for the current platform and architecture.
Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir.
Compile the parser to wasm using the `bin/clang` executable (or `bin/clang.exe` on windows)
that's inside of the archive.
Don't re-download the SDK if that executable already exists.
Read the `{input_file_path}` file and change `compile_parser_to_wasm` to use `wasi-sdk` instead of emscripten.
Use `ureq` to download the SDK for the current platform and architecture.
Extract the archive into a sibling of `lib` inside the `tree-sitter` directory in the cache_dir.
Compile the parser to wasm using the `bin/clang` executable (or `bin/clang.exe` on windows)
that's inside of the archive.
Don't re-download the SDK if that executable already exists.
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{{language_name}}
Use these clang flags: -fPIC -shared -Os -Wl,--export=tree_sitter_{{language_name}}
Here are the available wasi-sdk assets:
- wasi-sdk-25.0-x86_64-macos.tar.gz
- wasi-sdk-25.0-arm64-macos.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-windows.tar.gz
"})],
Here are the available wasi-sdk assets:
- wasi-sdk-25.0-x86_64-macos.tar.gz
- wasi-sdk-25.0-arm64-macos.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-linux.tar.gz
- wasi-sdk-25.0-arm64-linux.tar.gz
- wasi-sdk-25.0-x86_64-windows.tar.gz
"})],
),
message(
Assistant,
@@ -389,11 +352,11 @@ fn eval_use_wasi_sdk_in_compile_parser_to_wasm() {
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The compile_parser_to_wasm method has been changed to use wasi-sdk
- ureq is used to download the SDK for current platform and architecture
"}),
))
});
- The compile_parser_to_wasm method has been changed to use wasi-sdk
- ureq is used to download the SDK for current platform and architecture
"}),
),
);
}
#[test]
@@ -417,8 +380,11 @@ fn eval_disable_cursor_blinking() {
include_str!("evals/fixtures/disable_cursor_blinking/possible-03.diff"),
include_str!("evals/fixtures/disable_cursor_blinking/possible-04.diff"),
];
eval_utils::eval(100, 0.51, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
eval(
100,
0.51,
0.05,
EvalInput::from_conversation(
vec![
message(User, [text("Let's research how to cursor blinking works.")]),
message(
@@ -455,10 +421,10 @@ fn eval_disable_cursor_blinking() {
message(
User,
[text(indoc! {"
Comment out the lines that interact with the BlinkManager.
Keep the outer `update` blocks, but comments everything that's inside (including if statements).
Don't add additional comments.
"})],
Comment out the lines that interact with the BlinkManager.
Keep the outer `update` blocks, but comments everything that's inside (including if statements).
Don't add additional comments.
"})],
),
message(
Assistant,
@@ -474,9 +440,9 @@ fn eval_disable_cursor_blinking() {
),
],
Some(input_file_content.into()),
EvalAssertion::assert_diff_any(possible_diffs.clone()),
))
});
EvalAssertion::assert_diff_any(possible_diffs),
),
);
}
#[test]
@@ -501,16 +467,20 @@ fn eval_from_pixels_constructor() {
let input_file_path = "root/canvas.rs";
let input_file_content = include_str!("evals/fixtures/from_pixels_constructor/before.rs");
let edit_description = "Implement from_pixels constructor and add tests.";
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.25), move || {
run_eval(EvalInput::from_conversation(
eval(
100,
0.95,
// For whatever reason, this eval produces more mismatched tags.
// Increasing for now, let's see if we can bring this down.
0.25,
EvalInput::from_conversation(
vec![
message(
User,
[text(indoc! {"
Introduce a new `from_pixels` constructor in Canvas and
also add tests for it in the same file.
"})],
Introduce a new `from_pixels` constructor in Canvas and
also add tests for it in the same file.
"})],
),
message(
Assistant,
@@ -575,92 +545,92 @@ fn eval_from_pixels_constructor() {
"tool_4",
"grep",
indoc! {"
Found 6 matches:
Found 6 matches:
## Matches in font-kit/src/loaders/core_text.rs
## Matches in font-kit/src/loaders/core_text.rs
### mod test L926-936
```
mod test {
use super::Font;
use crate::properties::{Stretch, Weight};
### mod test L926-936
```
mod test {
use super::Font;
use crate::properties::{Stretch, Weight};
#[cfg(feature = \"source\")]
use crate::source::SystemSource;
#[cfg(feature = \"source\")]
use crate::source::SystemSource;
static TEST_FONT_POSTSCRIPT_NAME: &'static str = \"ArialMT\";
static TEST_FONT_POSTSCRIPT_NAME: &'static str = \"ArialMT\";
#[cfg(feature = \"source\")]
#[test]
```
#[cfg(feature = \"source\")]
#[test]
```
55 lines remaining in ancestor node. Read the file to see all.
55 lines remaining in ancestor node. Read the file to see all.
### mod test L947-951
```
}
### mod test L947-951
```
}
#[test]
fn test_core_text_to_css_font_weight() {
// Exact matches
```
#[test]
fn test_core_text_to_css_font_weight() {
// Exact matches
```
### mod test L959-963
```
}
### mod test L959-963
```
}
#[test]
fn test_core_text_to_css_font_stretch() {
// Exact matches
```
#[test]
fn test_core_text_to_css_font_stretch() {
// Exact matches
```
## Matches in font-kit/src/loaders/freetype.rs
## Matches in font-kit/src/loaders/freetype.rs
### mod test L1238-1248
```
mod test {
use crate::loaders::freetype::Font;
### mod test L1238-1248
```
mod test {
use crate::loaders::freetype::Font;
static PCF_FONT_PATH: &str = \"resources/tests/times-roman-pcf/timR12.pcf\";
static PCF_FONT_POSTSCRIPT_NAME: &str = \"Times-Roman\";
static PCF_FONT_PATH: &str = \"resources/tests/times-roman-pcf/timR12.pcf\";
static PCF_FONT_POSTSCRIPT_NAME: &str = \"Times-Roman\";
#[test]
fn get_pcf_postscript_name() {
let font = Font::from_path(PCF_FONT_PATH, 0).unwrap();
assert_eq!(font.postscript_name().unwrap(), PCF_FONT_POSTSCRIPT_NAME);
}
```
#[test]
fn get_pcf_postscript_name() {
let font = Font::from_path(PCF_FONT_PATH, 0).unwrap();
assert_eq!(font.postscript_name().unwrap(), PCF_FONT_POSTSCRIPT_NAME);
}
```
1 lines remaining in ancestor node. Read the file to see all.
1 lines remaining in ancestor node. Read the file to see all.
## Matches in font-kit/src/sources/core_text.rs
## Matches in font-kit/src/sources/core_text.rs
### mod test L265-275
```
mod test {
use crate::properties::{Stretch, Weight};
### mod test L265-275
```
mod test {
use crate::properties::{Stretch, Weight};
#[test]
fn test_css_to_core_text_font_weight() {
// Exact matches
assert_eq!(super::css_to_core_text_font_weight(Weight(100.0)), -0.7);
assert_eq!(super::css_to_core_text_font_weight(Weight(400.0)), 0.0);
assert_eq!(super::css_to_core_text_font_weight(Weight(700.0)), 0.4);
assert_eq!(super::css_to_core_text_font_weight(Weight(900.0)), 0.8);
#[test]
fn test_css_to_core_text_font_weight() {
// Exact matches
assert_eq!(super::css_to_core_text_font_weight(Weight(100.0)), -0.7);
assert_eq!(super::css_to_core_text_font_weight(Weight(400.0)), 0.0);
assert_eq!(super::css_to_core_text_font_weight(Weight(700.0)), 0.4);
assert_eq!(super::css_to_core_text_font_weight(Weight(900.0)), 0.8);
```
```
27 lines remaining in ancestor node. Read the file to see all.
27 lines remaining in ancestor node. Read the file to see all.
### mod test L278-282
```
}
### mod test L278-282
```
}
#[test]
fn test_css_to_core_text_font_stretch() {
// Exact matches
```
"},
#[test]
fn test_css_to_core_text_font_stretch() {
// Exact matches
```
"},
)],
),
message(
@@ -678,11 +648,11 @@ fn eval_from_pixels_constructor() {
],
Some(input_file_content.into()),
EvalAssertion::judge_diff(indoc! {"
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
))
});
- The diff contains a new `from_pixels` constructor
- The diff contains new tests for the `from_pixels` constructor
"}),
),
);
}
#[test]
@@ -700,9 +670,11 @@ fn eval_zode() {
let input_file_path = "root/zode.py";
let input_content = None;
let edit_description = "Create the main Zode CLI script";
eval_utils::eval(50, 1., mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
eval(
50,
1.,
0.05,
EvalInput::from_conversation(
vec![
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
message(
@@ -761,7 +733,7 @@ fn eval_zode() {
],
),
],
input_content.clone(),
input_content,
EvalAssertion::new(async move |sample, _, _cx| {
let invalid_starts = [' ', '`', '\n'];
let mut message = String::new();
@@ -786,8 +758,8 @@ fn eval_zode() {
})
}
}),
))
});
),
);
}
#[test]
@@ -805,17 +777,19 @@ fn eval_add_overwrite_test() {
let input_file_path = "root/action_log.rs";
let input_file_content = include_str!("evals/fixtures/add_overwrite_test/before.rs");
let edit_description = "Add a new test for overwriting a file in action_log.rs";
eval_utils::eval(200, 0.5, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
eval(
200,
0.5, // TODO: make this eval better
0.05,
EvalInput::from_conversation(
vec![
message(
User,
[text(indoc! {"
Introduce a new test in `action_log.rs` to test overwriting a file.
That is, a file already exists, but we call `buffer_created` as if the file were new.
Take inspiration from all the other tests in the file.
"})],
Introduce a new test in `action_log.rs` to test overwriting a file.
That is, a file already exists, but we call `buffer_created` as if the file were new.
Take inspiration from all the other tests in the file.
"})],
),
message(
Assistant,
@@ -835,81 +809,81 @@ fn eval_add_overwrite_test() {
"tool_1",
"read_file",
indoc! {"
pub struct ActionLog [L13-20]
tracked_buffers [L15]
edited_since_project_diagnostics_check [L17]
project [L19]
impl ActionLog [L22-498]
pub fn new [L24-30]
pub fn project [L32-34]
pub fn checked_project_diagnostics [L37-39]
pub fn has_edited_files_since_project_diagnostics_check [L42-44]
fn track_buffer_internal [L46-101]
fn handle_buffer_event [L103-116]
fn handle_buffer_edited [L118-123]
fn handle_buffer_file_changed [L125-158]
async fn maintain_diff [L160-264]
pub fn buffer_read [L267-269]
pub fn buffer_created [L272-276]
pub fn buffer_edited [L279-287]
pub fn will_delete_buffer [L289-304]
pub fn keep_edits_in_range [L306-364]
pub fn reject_edits_in_ranges [L366-459]
pub fn keep_all_edits [L461-473]
pub fn changed_buffers [L476-482]
pub fn stale_buffers [L485-497]
fn apply_non_conflicting_edits [L500-561]
fn diff_snapshots [L563-585]
fn point_to_row_edit [L587-614]
enum ChangeAuthor [L617-620]
User [L618]
Agent [L619]
enum TrackedBufferStatus [L623-627]
Created [L624]
Modified [L625]
Deleted [L626]
struct TrackedBuffer [L629-641]
buffer [L630]
base_text [L631]
unreviewed_changes [L632]
status [L633]
version [L634]
diff [L635]
snapshot [L636]
diff_update [L637]
_open_lsp_handle [L638]
_maintain_diff [L639]
_subscription [L640]
impl TrackedBuffer [L643-657]
fn has_changes [L644-650]
fn schedule_diff_update [L652-656]
pub struct ChangedBuffer [L659-661]
pub diff [L660]
mod tests [L664-1574]
fn init_logger [L678-682]
fn init_test [L684-691]
async fn test_keep_edits [L694-769]
async fn test_deletions [L772-854]
async fn test_overlapping_user_edits [L857-951]
async fn test_creating_files [L954-1010]
async fn test_deleting_files [L1013-1120]
async fn test_reject_edits [L1123-1255]
async fn test_reject_multiple_edits [L1258-1331]
async fn test_reject_deleted_file [L1334-1388]
async fn test_reject_created_file [L1391-1443]
async fn test_random_diffs [L1446-1535]
fn quiesce [L1510-1534]
struct HunkStatus [L1538-1542]
range [L1539]
diff_status [L1540]
old_text [L1541]
fn unreviewed_hunks [L1544-1573]
pub struct ActionLog [L13-20]
tracked_buffers [L15]
edited_since_project_diagnostics_check [L17]
project [L19]
impl ActionLog [L22-498]
pub fn new [L24-30]
pub fn project [L32-34]
pub fn checked_project_diagnostics [L37-39]
pub fn has_edited_files_since_project_diagnostics_check [L42-44]
fn track_buffer_internal [L46-101]
fn handle_buffer_event [L103-116]
fn handle_buffer_edited [L118-123]
fn handle_buffer_file_changed [L125-158]
async fn maintain_diff [L160-264]
pub fn buffer_read [L267-269]
pub fn buffer_created [L272-276]
pub fn buffer_edited [L279-287]
pub fn will_delete_buffer [L289-304]
pub fn keep_edits_in_range [L306-364]
pub fn reject_edits_in_ranges [L366-459]
pub fn keep_all_edits [L461-473]
pub fn changed_buffers [L476-482]
pub fn stale_buffers [L485-497]
fn apply_non_conflicting_edits [L500-561]
fn diff_snapshots [L563-585]
fn point_to_row_edit [L587-614]
enum ChangeAuthor [L617-620]
User [L618]
Agent [L619]
enum TrackedBufferStatus [L623-627]
Created [L624]
Modified [L625]
Deleted [L626]
struct TrackedBuffer [L629-641]
buffer [L630]
base_text [L631]
unreviewed_changes [L632]
status [L633]
version [L634]
diff [L635]
snapshot [L636]
diff_update [L637]
_open_lsp_handle [L638]
_maintain_diff [L639]
_subscription [L640]
impl TrackedBuffer [L643-657]
fn has_changes [L644-650]
fn schedule_diff_update [L652-656]
pub struct ChangedBuffer [L659-661]
pub diff [L660]
mod tests [L664-1574]
fn init_logger [L678-682]
fn init_test [L684-691]
async fn test_keep_edits [L694-769]
async fn test_deletions [L772-854]
async fn test_overlapping_user_edits [L857-951]
async fn test_creating_files [L954-1010]
async fn test_deleting_files [L1013-1120]
async fn test_reject_edits [L1123-1255]
async fn test_reject_multiple_edits [L1258-1331]
async fn test_reject_deleted_file [L1334-1388]
async fn test_reject_created_file [L1391-1443]
async fn test_random_diffs [L1446-1535]
fn quiesce [L1510-1534]
struct HunkStatus [L1538-1542]
range [L1539]
diff_status [L1540]
old_text [L1541]
fn unreviewed_hunks [L1544-1573]
Showing symbols 1-69 (total symbols: 69)
Showing symbols 1-69 (total symbols: 69)
Using the line numbers in this outline, you can call this tool again while specifying
the start_line and end_line fields to see the implementations of symbols in the outline.
"},
Using the line numbers in this outline, you can call this tool again while specifying
the start_line and end_line fields to see the implementations of symbols in the outline.
"},
)],
),
message(
@@ -989,17 +963,17 @@ fn eval_add_overwrite_test() {
Assistant,
[
text(indoc! {"
Based on my understanding of the existing tests and code, I'll now create a new test for overwriting a file. In this test, we'll:
Based on my understanding of the existing tests and code, I'll now create a new test for overwriting a file. In this test, we'll:
1. Create a file
2. Open a buffer for it
3. Track it in the action log
4. Modify the buffer
5. Call `buffer_created` again as if the file were new
6. Check that the changes are properly tracked
1. Create a file
2. Open a buffer for it
3. Track it in the action log
4. Modify the buffer
5. Call `buffer_created` again as if the file were new
6. Check that the changes are properly tracked
Let's write the test:
"}),
Let's write the test:
"}),
tool_use(
"tool_5",
"edit_file",
@@ -1016,8 +990,8 @@ fn eval_add_overwrite_test() {
EvalAssertion::judge_diff(
"A new test for overwritten files was created, without changing any previous test",
),
))
});
),
);
}
#[test]
@@ -1042,18 +1016,20 @@ fn eval_create_empty_file() {
let input_file_content = None;
let expected_output_content = String::new();
eval_utils::eval(100, 0.99, mismatched_tag_threshold(0.05), move || {
run_eval(EvalInput::from_conversation(
eval(
100,
0.99,
0.05,
EvalInput::from_conversation(
vec![
message(User, [text("Create a second empty todo file ")]),
message(
Assistant,
[
text(formatdoc! {"
I'll help you create a second empty todo file.
First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one.
"}),
I'll help you create a second empty todo file.
First, let me examine the project structure to see if there's already a todo file, which will help me determine the appropriate name and location for the second one.
"}),
tool_use(
"toolu_01GAF8TtsgpjKxCr8fgQLDgR",
"list_directory",
@@ -1075,8 +1051,8 @@ fn eval_create_empty_file() {
Assistant,
[
text(formatdoc! {"
I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory:
"}),
I can see there's already a `TODO` file in the `root` directory. Let me create a second empty todo file called `TODO3` in the same directory:
"}),
tool_use(
"toolu_01Tb3iQ9griqSYMmVuykQPWU",
"edit_file",
@@ -1089,12 +1065,12 @@ fn eval_create_empty_file() {
],
),
],
input_file_content.clone(),
input_file_content,
// Bad behavior is to write something like
// "I'll create an empty TODO3 file as requested."
EvalAssertion::assert_eq(expected_output_content.clone()),
))
});
EvalAssertion::assert_eq(expected_output_content),
),
);
}
fn message(
@@ -1336,44 +1312,115 @@ impl EvalAssertion {
}
}
fn run_eval(eval: EvalInput) -> eval_utils::EvalOutput<EditEvalMetadata> {
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
let mut cx = TestAppContext::build(dispatcher, None);
let result = cx.executor().block_test(async {
let test = EditAgentTest::new(&mut cx).await;
test.eval(eval, &mut cx).await
});
match result {
Ok(output) => eval_utils::EvalOutput {
data: output.to_string(),
outcome: if output.assertion.score < 80 {
eval_utils::OutcomeKind::Failed
} else {
eval_utils::OutcomeKind::Passed
},
metadata: EditEvalMetadata {
tags: output.sample.edit_output.parser_metrics.tags,
mismatched_tags: output.sample.edit_output.parser_metrics.mismatched_tags,
},
},
Err(e) => eval_utils::EvalOutput {
data: format!("{e:?}"),
outcome: eval_utils::OutcomeKind::Error,
metadata: EditEvalMetadata {
tags: 0,
mismatched_tags: 0,
},
},
fn eval(
iterations: usize,
expected_pass_ratio: f32,
mismatched_tag_threshold: f32,
mut eval: EvalInput,
) {
let mut evaluated_count = 0;
let mut failed_count = 0;
report_progress(evaluated_count, failed_count, iterations);
let (tx, rx) = mpsc::channel();
// Cache the last message in the conversation, and run one instance of the eval so that
// all the next ones are cached.
eval.conversation.last_mut().unwrap().cache = true;
run_eval(eval.clone(), tx.clone());
let executor = gpui::background_executor();
let semaphore = Arc::new(smol::lock::Semaphore::new(32));
for _ in 1..iterations {
let eval = eval.clone();
let tx = tx.clone();
let semaphore = semaphore.clone();
executor
.spawn(async move {
let _guard = semaphore.acquire().await;
run_eval(eval, tx)
})
.detach();
}
drop(tx);
let mut failed_evals = HashMap::default();
let mut errored_evals = HashMap::default();
let mut eval_outputs = Vec::new();
let mut cumulative_parser_metrics = EditParserMetrics::default();
while let Ok(output) = rx.recv() {
match output {
Ok(output) => {
cumulative_parser_metrics += output.sample.edit_output.parser_metrics.clone();
eval_outputs.push(output.clone());
if output.assertion.score < 80 {
failed_count += 1;
failed_evals
.entry(output.sample.text_after.clone())
.or_insert(Vec::new())
.push(output);
}
}
Err(error) => {
failed_count += 1;
*errored_evals.entry(format!("{:?}", error)).or_insert(0) += 1;
}
}
evaluated_count += 1;
report_progress(evaluated_count, failed_count, iterations);
}
let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32;
println!("Actual pass ratio: {}\n", actual_pass_ratio);
if actual_pass_ratio < expected_pass_ratio {
let mut errored_evals = errored_evals.into_iter().collect::<Vec<_>>();
errored_evals.sort_by_key(|(_, count)| Reverse(*count));
for (error, count) in errored_evals {
println!("Eval errored {} times. Error: {}", count, error);
}
let mut failed_evals = failed_evals.into_iter().collect::<Vec<_>>();
failed_evals.sort_by_key(|(_, evals)| Reverse(evals.len()));
for (_buffer_output, failed_evals) in failed_evals {
let eval_output = failed_evals.first().unwrap();
println!("Eval failed {} times", failed_evals.len());
println!("{}", eval_output);
}
panic!(
"Actual pass ratio: {}\nExpected pass ratio: {}",
actual_pass_ratio, expected_pass_ratio
);
}
let mismatched_tag_ratio =
cumulative_parser_metrics.mismatched_tags as f32 / cumulative_parser_metrics.tags as f32;
if mismatched_tag_ratio > mismatched_tag_threshold {
for eval_output in eval_outputs {
println!("{}", eval_output);
}
panic!("Too many mismatched tags: {:?}", cumulative_parser_metrics);
}
}
fn run_eval(eval: EvalInput, tx: mpsc::Sender<Result<EvalOutput>>) {
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
let mut cx = TestAppContext::build(dispatcher, None);
let output = cx.executor().block_test(async {
let test = EditAgentTest::new(&mut cx).await;
test.eval(eval, &mut cx).await
});
tx.send(output).unwrap();
}
#[derive(Clone)]
struct EditEvalOutput {
struct EvalOutput {
sample: EvalSample,
assertion: EvalAssertionOutcome,
}
impl Display for EditEvalOutput {
impl Display for EvalOutput {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
writeln!(f, "Score: {:?}", self.assertion.score)?;
if let Some(message) = self.assertion.message.as_ref() {
@@ -1392,6 +1439,22 @@ impl Display for EditEvalOutput {
}
}
fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) {
let passed_count = evaluated_count - failed_count;
let passed_ratio = if evaluated_count == 0 {
0.0
} else {
passed_count as f64 / evaluated_count as f64
};
print!(
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
evaluated_count,
iterations,
passed_ratio * 100.0
);
std::io::stdout().flush().unwrap();
}
struct EditAgentTest {
agent: EditAgent,
project: Entity<Project>,
@@ -1487,10 +1550,7 @@ impl EditAgentTest {
})
}
async fn eval(&self, mut eval: EvalInput, cx: &mut TestAppContext) -> Result<EditEvalOutput> {
// Make sure the last message in the conversation is cached.
eval.conversation.last_mut().unwrap().cache = true;
async fn eval(&self, eval: EvalInput, cx: &mut TestAppContext) -> Result<EvalOutput> {
let path = self
.project
.read_with(cx, |project, cx| {
@@ -1596,7 +1656,7 @@ impl EditAgentTest {
.run(&sample, self.judge_model.clone(), cx)
.await?;
Ok(EditEvalOutput { assertion, sample })
Ok(EvalOutput { assertion, sample })
}
}

View File

@@ -354,9 +354,9 @@ impl HistoryStore {
.into_iter()
.take(MAX_RECENTLY_OPENED_ENTRIES)
.flat_map(|entry| match entry {
SerializedRecentOpen::AcpThread(id) => {
Some(HistoryEntryId::AcpThread(acp::SessionId::new(id.as_str())))
}
SerializedRecentOpen::AcpThread(id) => Some(HistoryEntryId::AcpThread(
acp::SessionId(id.as_str().into()),
)),
SerializedRecentOpen::TextThread(file_name) => Some(
HistoryEntryId::TextThread(text_threads_dir().join(file_name).into()),
),

View File

@@ -493,14 +493,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
// Approve the first
tool_call_auth_1
.response
.send(tool_call_auth_1.options[1].option_id.clone())
.send(tool_call_auth_1.options[1].id.clone())
.unwrap();
cx.run_until_parked();
// Reject the second
tool_call_auth_2
.response
.send(tool_call_auth_1.options[2].option_id.clone())
.send(tool_call_auth_1.options[2].id.clone())
.unwrap();
cx.run_until_parked();
@@ -510,14 +510,14 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
message.content,
vec![
language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_1.tool_call.tool_call_id.0.to_string().into(),
tool_use_id: tool_call_auth_1.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission::name().into(),
is_error: false,
content: "Allowed".into(),
output: Some("Allowed".into())
}),
language_model::MessageContent::ToolResult(LanguageModelToolResult {
tool_use_id: tool_call_auth_2.tool_call.tool_call_id.0.to_string().into(),
tool_use_id: tool_call_auth_2.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission::name().into(),
is_error: true,
content: "Permission to run tool denied by user".into(),
@@ -543,7 +543,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
let tool_call_auth_3 = next_tool_call_authorization(&mut events).await;
tool_call_auth_3
.response
.send(tool_call_auth_3.options[0].option_id.clone())
.send(tool_call_auth_3.options[0].id.clone())
.unwrap();
cx.run_until_parked();
let completion = fake_model.pending_completions().pop().unwrap();
@@ -552,7 +552,7 @@ async fn test_tool_authorization(cx: &mut TestAppContext) {
message.content,
vec![language_model::MessageContent::ToolResult(
LanguageModelToolResult {
tool_use_id: tool_call_auth_3.tool_call.tool_call_id.0.to_string().into(),
tool_use_id: tool_call_auth_3.tool_call.id.0.to_string().into(),
tool_name: ToolRequiringPermission::name().into(),
is_error: false,
content: "Allowed".into(),
@@ -1353,20 +1353,20 @@ async fn test_cancellation(cx: &mut TestAppContext) {
ThreadEvent::ToolCall(tool_call) => {
assert_eq!(tool_call.title, expected_tools.remove(0));
if tool_call.title == "Echo" {
echo_id = Some(tool_call.tool_call_id);
echo_id = Some(tool_call.id);
}
}
ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
acp::ToolCallUpdate {
tool_call_id,
id,
fields:
acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
..
},
..
meta: None,
},
)) if Some(&tool_call_id) == echo_id.as_ref() => {
)) if Some(&id) == echo_id.as_ref() => {
echo_completed = true;
}
_ => {}
@@ -1995,7 +1995,11 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
.update(|cx| {
connection.prompt(
Some(acp_thread::UserMessageId::new()),
acp::PromptRequest::new(session_id.clone(), vec!["ghi".into()]),
acp::PromptRequest {
session_id: session_id.clone(),
prompt: vec!["ghi".into()],
meta: None,
},
cx,
)
})
@@ -2052,50 +2056,68 @@ async fn test_tool_updates_to_completion(cx: &mut TestAppContext) {
let tool_call = expect_tool_call(&mut events).await;
assert_eq!(
tool_call,
acp::ToolCall::new("1", "Thinking")
.kind(acp::ToolKind::Think)
.raw_input(json!({}))
.meta(acp::Meta::from_iter([(
"tool_name".into(),
"thinking".into()
)]))
acp::ToolCall {
id: acp::ToolCallId("1".into()),
title: "Thinking".into(),
kind: acp::ToolKind::Think,
status: acp::ToolCallStatus::Pending,
content: vec![],
locations: vec![],
raw_input: Some(json!({})),
raw_output: None,
meta: Some(json!({ "tool_name": "thinking" })),
}
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate::new(
"1",
acp::ToolCallUpdateFields::new()
.title("Thinking")
.kind(acp::ToolKind::Think)
.raw_input(json!({ "content": "Thinking hard!"}))
)
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
title: Some("Thinking".into()),
kind: Some(acp::ToolKind::Think),
raw_input: Some(json!({ "content": "Thinking hard!" })),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate::new(
"1",
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress)
)
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate::new(
"1",
acp::ToolCallUpdateFields::new().content(vec!["Thinking hard!".into()])
)
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
content: Some(vec!["Thinking hard!".into()]),
..Default::default()
},
meta: None,
}
);
let update = expect_tool_call_update_fields(&mut events).await;
assert_eq!(
update,
acp::ToolCallUpdate::new(
"1",
acp::ToolCallUpdateFields::new()
.status(acp::ToolCallStatus::Completed)
.raw_output("Finished thinking.".into())
)
acp::ToolCallUpdate {
id: acp::ToolCallId("1".into()),
fields: acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::Completed),
raw_output: Some("Finished thinking.".into()),
..Default::default()
},
meta: None,
}
);
}

View File

@@ -619,9 +619,12 @@ pub struct Thread {
impl Thread {
fn prompt_capabilities(model: Option<&dyn LanguageModel>) -> acp::PromptCapabilities {
let image = model.map_or(true, |model| model.supports_images());
acp::PromptCapabilities::new()
.image(image)
.embedded_context(true)
acp::PromptCapabilities {
meta: None,
image,
audio: false,
embedded_context: true,
}
}
pub fn new(
@@ -637,7 +640,7 @@ impl Thread {
let (prompt_capabilities_tx, prompt_capabilities_rx) =
watch::channel(Self::prompt_capabilities(model.as_deref()));
Self {
id: acp::SessionId::new(uuid::Uuid::new_v4().to_string()),
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
prompt_id: PromptId::new(),
updated_at: Utc::now(),
title: None,
@@ -734,11 +737,17 @@ impl Thread {
let Some(tool) = tool else {
stream
.0
.unbounded_send(Ok(ThreadEvent::ToolCall(
acp::ToolCall::new(tool_use.id.to_string(), tool_use.name.to_string())
.status(acp::ToolCallStatus::Failed)
.raw_input(tool_use.input.clone()),
)))
.unbounded_send(Ok(ThreadEvent::ToolCall(acp::ToolCall {
meta: None,
id: acp::ToolCallId(tool_use.id.to_string().into()),
title: tool_use.name.to_string(),
kind: acp::ToolKind::Other,
status: acp::ToolCallStatus::Failed,
content: Vec::new(),
locations: Vec::new(),
raw_input: Some(tool_use.input.clone()),
raw_output: None,
})))
.ok();
return;
};
@@ -766,20 +775,24 @@ impl Thread {
.log_err();
}
let mut fields = acp::ToolCallUpdateFields::new().status(tool_result.as_ref().map_or(
acp::ToolCallStatus::Failed,
|result| {
if result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}
stream.update_tool_call_fields(
&tool_use.id,
acp::ToolCallUpdateFields {
status: Some(
tool_result
.as_ref()
.map_or(acp::ToolCallStatus::Failed, |result| {
if result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}
}),
),
raw_output: output,
..Default::default()
},
));
if let Some(output) = output {
fields = fields.raw_output(output);
}
stream.update_tool_call_fields(&tool_use.id, fields);
);
}
pub fn from_db(
@@ -1259,15 +1272,18 @@ impl Thread {
while let Some(tool_result) = tool_results.next().await {
log::debug!("Tool finished {:?}", tool_result);
let mut fields = acp::ToolCallUpdateFields::new().status(if tool_result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
});
if let Some(output) = &tool_result.output {
fields = fields.raw_output(output.clone());
}
event_stream.update_tool_call_fields(&tool_result.tool_use_id, fields);
event_stream.update_tool_call_fields(
&tool_result.tool_use_id,
acp::ToolCallUpdateFields {
status: Some(if tool_result.is_error {
acp::ToolCallStatus::Failed
} else {
acp::ToolCallStatus::Completed
}),
raw_output: tool_result.output.clone(),
..Default::default()
},
);
this.update(cx, |this, _cx| {
this.pending_message()
.tool_results
@@ -1544,10 +1560,12 @@ impl Thread {
} else {
event_stream.update_tool_call_fields(
&tool_use.id,
acp::ToolCallUpdateFields::new()
.title(title)
.kind(kind)
.raw_input(tool_use.input.clone()),
acp::ToolCallUpdateFields {
title: Some(title.into()),
kind: Some(kind),
raw_input: Some(tool_use.input.clone()),
..Default::default()
},
);
}
@@ -1569,9 +1587,10 @@ impl Thread {
let fs = self.project.read(cx).fs().clone();
let tool_event_stream =
ToolCallEventStream::new(tool_use.id.clone(), event_stream.clone(), Some(fs));
tool_event_stream.update_fields(
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress),
);
tool_event_stream.update_fields(acp::ToolCallUpdateFields {
status: Some(acp::ToolCallStatus::InProgress),
..Default::default()
});
let supports_images = self.model().is_some_and(|model| model.supports_images());
let tool_result = tool.run(tool_use.input, tool_event_stream, cx);
log::debug!("Running tool {}", tool_use.name);
@@ -2362,13 +2381,19 @@ impl ThreadEventStream {
kind: acp::ToolKind,
input: serde_json::Value,
) -> acp::ToolCall {
acp::ToolCall::new(id.to_string(), title)
.kind(kind)
.raw_input(input)
.meta(acp::Meta::from_iter([(
"tool_name".into(),
tool_name.into(),
)]))
acp::ToolCall {
meta: Some(serde_json::json!({
"tool_name": tool_name
})),
id: acp::ToolCallId(id.to_string().into()),
title,
kind,
status: acp::ToolCallStatus::Pending,
content: vec![],
locations: vec![],
raw_input: Some(input),
raw_output: None,
}
}
fn update_tool_call_fields(
@@ -2378,7 +2403,12 @@ impl ThreadEventStream {
) {
self.0
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
acp::ToolCallUpdate::new(tool_use_id.to_string(), fields).into(),
acp::ToolCallUpdate {
meta: None,
id: acp::ToolCallId(tool_use_id.to_string().into()),
fields,
}
.into(),
)))
.ok();
}
@@ -2441,7 +2471,7 @@ impl ToolCallEventStream {
.0
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
acp_thread::ToolCallUpdateDiff {
id: acp::ToolCallId::new(self.tool_use_id.to_string()),
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
diff,
}
.into(),
@@ -2459,26 +2489,33 @@ impl ToolCallEventStream {
.0
.unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
ToolCallAuthorization {
tool_call: acp::ToolCallUpdate::new(
self.tool_use_id.to_string(),
acp::ToolCallUpdateFields::new().title(title),
),
tool_call: acp::ToolCallUpdate {
meta: None,
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
fields: acp::ToolCallUpdateFields {
title: Some(title.into()),
..Default::default()
},
},
options: vec![
acp::PermissionOption::new(
acp::PermissionOptionId::new("always_allow"),
"Always Allow",
acp::PermissionOptionKind::AllowAlways,
),
acp::PermissionOption::new(
acp::PermissionOptionId::new("allow"),
"Allow",
acp::PermissionOptionKind::AllowOnce,
),
acp::PermissionOption::new(
acp::PermissionOptionId::new("deny"),
"Deny",
acp::PermissionOptionKind::RejectOnce,
),
acp::PermissionOption {
id: acp::PermissionOptionId("always_allow".into()),
name: "Always Allow".into(),
kind: acp::PermissionOptionKind::AllowAlways,
meta: None,
},
acp::PermissionOption {
id: acp::PermissionOptionId("allow".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
},
acp::PermissionOption {
id: acp::PermissionOptionId("deny".into()),
name: "Deny".into(),
kind: acp::PermissionOptionKind::RejectOnce,
meta: None,
},
],
response: response_tx,
},
@@ -2623,15 +2660,7 @@ impl UserMessageContent {
// TODO
Self::Text("[blob]".to_string())
}
other => {
log::warn!("Unexpected content type: {:?}", other);
Self::Text("[unknown]".to_string())
}
},
other => {
log::warn!("Unexpected content type: {:?}", other);
Self::Text("[unknown]".to_string())
}
}
}
}
@@ -2639,15 +2668,32 @@ impl UserMessageContent {
impl From<UserMessageContent> for acp::ContentBlock {
fn from(content: UserMessageContent) -> Self {
match content {
UserMessageContent::Text(text) => text.into(),
UserMessageContent::Image(image) => {
acp::ContentBlock::Image(acp::ImageContent::new(image.source, "image/png"))
UserMessageContent::Text(text) => acp::ContentBlock::Text(acp::TextContent {
text,
annotations: None,
meta: None,
}),
UserMessageContent::Image(image) => acp::ContentBlock::Image(acp::ImageContent {
data: image.source.to_string(),
mime_type: "image/png".to_string(),
meta: None,
annotations: None,
uri: None,
}),
UserMessageContent::Mention { uri, content } => {
acp::ContentBlock::Resource(acp::EmbeddedResource {
meta: None,
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
meta: None,
mime_type: None,
text: content,
uri: uri.to_uri().to_string(),
},
),
annotations: None,
})
}
UserMessageContent::Mention { uri, content } => acp::ContentBlock::Resource(
acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(content, uri.to_uri().to_string()),
)),
),
}
}
}

View File

@@ -273,9 +273,14 @@ impl AgentTool for EditFileTool {
};
let abs_path = project.read(cx).absolute_path(&project_path, cx);
if let Some(abs_path) = abs_path.clone() {
event_stream.update_fields(
ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path)]),
);
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path,
line: None,
meta: None,
}]),
..Default::default()
});
}
let authorize = self.authorize(&input, &event_stream, cx);
@@ -384,11 +389,10 @@ impl AgentTool for EditFileTool {
range.start.to_point(&buffer.snapshot()).row
}).ok();
if let Some(abs_path) = abs_path.clone() {
let mut location = ToolCallLocation::new(abs_path);
if let Some(line) = line {
location = location.line(line);
}
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![ToolCallLocation { path: abs_path, line, meta: None }]),
..Default::default()
});
}
emitted_location = true;
}

View File

@@ -118,29 +118,33 @@ impl AgentTool for FindPathTool {
let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len())
..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
event_stream.update_fields(
acp::ToolCallUpdateFields::new()
.title(if paginated_matches.is_empty() {
"No matches".into()
} else if paginated_matches.len() == 1 {
"1 match".into()
} else {
format!("{} matches", paginated_matches.len())
})
.content(
paginated_matches
.iter()
.map(|path| {
acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
path.to_string_lossy(),
format!("file://{}", path.display()),
)),
))
})
.collect(),
),
);
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(if paginated_matches.is_empty() {
"No matches".into()
} else if paginated_matches.len() == 1 {
"1 match".into()
} else {
format!("{} matches", paginated_matches.len())
}),
content: Some(
paginated_matches
.iter()
.map(|path| acp::ToolCallContent::Content {
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: format!("file://{}", path.display()),
name: path.to_string_lossy().into(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
})
.collect(),
),
..Default::default()
});
Ok(FindPathToolOutput {
offset: input.offset,

View File

@@ -152,12 +152,15 @@ impl AgentTool for ReadFileTool {
}
let file_path = input.path.clone();
let mut location = acp::ToolCallLocation::new(&abs_path);
if let Some(line) = input.start_line {
location = location.line(line.saturating_sub(1));
}
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: input.start_line.map(|line| line.saturating_sub(1)),
meta: None,
}]),
..Default::default()
});
if image_store::is_image_file(&self.project, &project_path, cx) {
return cx.spawn(async move |cx| {
@@ -286,9 +289,12 @@ impl AgentTool for ReadFileTool {
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields::new().content(vec![
acp::ToolCallContent::Content(acp::Content::new(markdown)),
]));
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
})
}
})?;

View File

@@ -112,9 +112,10 @@ impl AgentTool for TerminalTool {
.await?;
let terminal_id = terminal.id(cx)?;
event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![
acp::ToolCallContent::Terminal(acp::Terminal::new(terminal_id)),
]));
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
..Default::default()
});
let exit_status = terminal.wait_for_exit(cx)?.await;
let output = terminal.current_output(cx)?;

View File

@@ -43,8 +43,10 @@ impl AgentTool for ThinkingTool {
event_stream: ToolCallEventStream,
_cx: &mut App,
) -> Task<Result<String>> {
event_stream
.update_fields(acp::ToolCallUpdateFields::new().content(vec![input.content.into()]));
event_stream.update_fields(acp::ToolCallUpdateFields {
content: Some(vec![input.content.into()]),
..Default::default()
});
Task::ready(Ok("Finished thinking.".to_string()))
}
}

View File

@@ -76,8 +76,10 @@ impl AgentTool for WebSearchTool {
let response = match search_task.await {
Ok(response) => response,
Err(err) => {
event_stream
.update_fields(acp::ToolCallUpdateFields::new().title("Web Search Failed"));
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some("Web Search Failed".to_string()),
..Default::default()
});
return Err(err);
}
};
@@ -105,23 +107,26 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
} else {
format!("{} results", response.results.len())
};
event_stream.update_fields(
acp::ToolCallUpdateFields::new()
.title(format!("Searched the web: {result_text}"))
.content(
response
.results
.iter()
.map(|result| {
acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::ResourceLink(
acp::ResourceLink::new(result.title.clone(), result.url.clone())
.title(result.title.clone())
.description(result.text.clone()),
),
))
})
.collect(),
),
);
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(format!("Searched the web: {result_text}")),
content: Some(
response
.results
.iter()
.map(|result| acp::ToolCallContent::Content {
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: result.title.clone(),
uri: result.url.clone(),
title: Some(result.title.clone()),
description: Some(result.text.clone()),
mime_type: None,
annotations: None,
size: None,
meta: None,
}),
})
.collect(),
),
..Default::default()
});
}

View File

@@ -76,7 +76,7 @@ pub async fn connect(
Ok(Rc::new(conn) as _)
}
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1;
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
impl AcpConnection {
pub async fn stdio(
@@ -173,27 +173,29 @@ impl AcpConnection {
});
})?;
let mut client_info = acp::Implementation::new("zed", version);
if let Some(release_channel) = release_channel {
client_info = client_info.title(release_channel);
}
let response = connection
.initialize(
acp::InitializeRequest::new(acp::ProtocolVersion::V1)
.client_capabilities(
acp::ClientCapabilities::new()
.fs(acp::FileSystemCapability::new()
.read_text_file(true)
.write_text_file(true))
.terminal(true)
// Experimental: Allow for rendering terminal output from the agents
.meta(acp::Meta::from_iter([
("terminal_output".into(), true.into()),
("terminal-auth".into(), true.into()),
])),
)
.client_info(client_info),
)
.initialize(acp::InitializeRequest {
protocol_version: acp::VERSION,
client_capabilities: acp::ClientCapabilities {
fs: acp::FileSystemCapability {
read_text_file: true,
write_text_file: true,
meta: None,
},
terminal: true,
meta: Some(serde_json::json!({
// Experimental: Allow for rendering terminal output from the agents
"terminal_output": true,
"terminal-auth": true,
})),
},
client_info: Some(acp::Implementation {
name: "zed".to_owned(),
title: release_channel.map(|c| c.to_owned()),
version,
}),
meta: None,
})
.await?;
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
@@ -251,13 +253,14 @@ impl AgentConnection for AcpConnection {
let default_model = self.default_model.clone();
let cwd = cwd.to_path_buf();
let context_server_store = project.read(cx).context_server_store().read(cx);
let mcp_servers = if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
match &*configuration {
let mcp_servers =
if project.read(cx).is_local() {
context_server_store
.configured_server_ids()
.iter()
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
match &*configuration {
project::context_server_store::ContextServerConfiguration::Custom {
command,
..
@@ -265,41 +268,47 @@ impl AgentConnection for AcpConnection {
| project::context_server_store::ContextServerConfiguration::Extension {
command,
..
} => Some(acp::McpServer::Stdio(
acp::McpServerStdio::new(id.0.to_string(), &command.path)
.args(command.args.clone())
.env(if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable::new(name, value))
.collect()
} else {
vec![]
}),
)),
} => Some(acp::McpServer::Stdio {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
env: if let Some(env) = command.env.as_ref() {
env.iter()
.map(|(name, value)| acp::EnvVariable {
name: name.clone(),
value: value.clone(),
meta: None,
})
.collect()
} else {
vec![]
},
}),
project::context_server_store::ContextServerConfiguration::Http {
url,
headers,
} => Some(acp::McpServer::Http(
acp::McpServerHttp::new(id.0.to_string(), url.to_string()).headers(
headers
.iter()
.map(|(name, value)| acp::HttpHeader::new(name, value))
.collect(),
),
)),
} => Some(acp::McpServer::Http {
name: id.0.to_string(),
url: url.to_string(),
headers: headers.iter().map(|(name, value)| acp::HttpHeader {
name: name.clone(),
value: value.clone(),
meta: None,
}).collect(),
}),
}
})
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
})
.collect()
} else {
// In SSH projects, the external agent is running on the remote
// machine, and currently we only run MCP servers on the local
// machine. So don't pass any MCP servers to the agent in that case.
Vec::new()
};
cx.spawn(async move |cx| {
let response = conn
.new_session(acp::NewSessionRequest::new(cwd).mcp_servers(mcp_servers))
.new_session(acp::NewSessionRequest { mcp_servers, cwd, meta: None })
.await
.map_err(|err| {
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
@@ -332,7 +341,11 @@ impl AgentConnection for AcpConnection {
let modes = modes.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_mode(acp::SetSessionModeRequest::new(session_id, default_mode))
let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id: default_mode,
meta: None,
})
.await.log_err();
if result.is_none() {
@@ -375,7 +388,11 @@ impl AgentConnection for AcpConnection {
let models = models.clone();
let conn = conn.clone();
async move |_| {
let result = conn.set_session_model(acp::SetSessionModelRequest::new(session_id, default_model))
let result = conn.set_session_model(acp::SetSessionModelRequest {
session_id,
model_id: default_model,
meta: None,
})
.await.log_err();
if result.is_none() {
@@ -439,8 +456,12 @@ impl AgentConnection for AcpConnection {
fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
let conn = self.connection.clone();
cx.foreground_executor().spawn(async move {
conn.authenticate(acp::AuthenticateRequest::new(method_id))
.await?;
conn.authenticate(acp::AuthenticateRequest {
method_id: method_id.clone(),
meta: None,
})
.await?;
Ok(())
})
}
@@ -494,7 +515,10 @@ impl AgentConnection for AcpConnection {
&& (details.contains("This operation was aborted")
|| details.contains("The user aborted a request"))
{
Ok(acp::PromptResponse::new(acp::StopReason::Cancelled))
Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Cancelled,
meta: None,
})
} else {
Err(anyhow!(details))
}
@@ -511,7 +535,10 @@ impl AgentConnection for AcpConnection {
session.suppress_abort_err = true;
}
let conn = self.connection.clone();
let params = acp::CancelNotification::new(session_id.clone());
let params = acp::CancelNotification {
session_id: session_id.clone(),
meta: None,
};
cx.foreground_executor()
.spawn(async move { conn.cancel(params).await })
.detach();
@@ -592,7 +619,11 @@ impl acp_thread::AgentSessionModes for AcpSessionModes {
let state = self.state.clone();
cx.foreground_executor().spawn(async move {
let result = connection
.set_session_mode(acp::SetSessionModeRequest::new(session_id, mode_id))
.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id,
meta: None,
})
.await;
if result.is_err() {
@@ -651,7 +682,11 @@ impl acp_thread::AgentModelSelector for AcpModelSelector {
let state = self.state.clone();
cx.foreground_executor().spawn(async move {
let result = connection
.set_session_model(acp::SetSessionModelRequest::new(session_id, model_id))
.set_session_model(acp::SetSessionModelRequest {
session_id,
model_id,
meta: None,
})
.await;
if result.is_err() {
@@ -713,7 +748,10 @@ impl acp::Client for ClientDelegate {
let outcome = task.await;
Ok(acp::RequestPermissionResponse::new(outcome))
Ok(acp::RequestPermissionResponse {
outcome,
meta: None,
})
}
async fn write_text_file(
@@ -745,7 +783,10 @@ impl acp::Client for ClientDelegate {
let content = task.await?;
Ok(acp::ReadTextFileResponse::new(content))
Ok(acp::ReadTextFileResponse {
content,
meta: None,
})
}
async fn session_notification(
@@ -780,7 +821,7 @@ impl acp::Client for ClientDelegate {
if let Some(terminal_info) = meta.get("terminal_info") {
if let Some(id_str) = terminal_info.get("terminal_id").and_then(|v| v.as_str())
{
let terminal_id = acp::TerminalId::new(id_str);
let terminal_id = acp::TerminalId(id_str.into());
let cwd = terminal_info
.get("cwd")
.and_then(|v| v.as_str().map(PathBuf::from));
@@ -796,7 +837,7 @@ impl acp::Client for ClientDelegate {
let lower = cx.new(|cx| builder.subscribe(cx));
thread.on_terminal_provider_event(
TerminalProviderEvent::Created {
terminal_id,
terminal_id: terminal_id.clone(),
label: tc.title.clone(),
cwd,
output_byte_limit: None,
@@ -821,12 +862,15 @@ impl acp::Client for ClientDelegate {
if let Some(meta) = &tcu.meta {
if let Some(term_out) = meta.get("terminal_output") {
if let Some(id_str) = term_out.get("terminal_id").and_then(|v| v.as_str()) {
let terminal_id = acp::TerminalId::new(id_str);
let terminal_id = acp::TerminalId(id_str.into());
if let Some(s) = term_out.get("data").and_then(|v| v.as_str()) {
let data = s.as_bytes().to_vec();
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Output { terminal_id, data },
TerminalProviderEvent::Output {
terminal_id: terminal_id.clone(),
data,
},
cx,
);
});
@@ -837,19 +881,21 @@ impl acp::Client for ClientDelegate {
// terminal_exit
if let Some(term_exit) = meta.get("terminal_exit") {
if let Some(id_str) = term_exit.get("terminal_id").and_then(|v| v.as_str()) {
let terminal_id = acp::TerminalId::new(id_str);
let mut status = acp::TerminalExitStatus::new();
if let Some(code) = term_exit.get("exit_code").and_then(|v| v.as_u64()) {
status = status.exit_code(code as u32)
}
if let Some(signal) = term_exit.get("signal").and_then(|v| v.as_str()) {
status = status.signal(signal);
}
let terminal_id = acp::TerminalId(id_str.into());
let status = acp::TerminalExitStatus {
exit_code: term_exit
.get("exit_code")
.and_then(|v| v.as_u64())
.map(|i| i as u32),
signal: term_exit
.get("signal")
.and_then(|v| v.as_str().map(|s| s.to_string())),
meta: None,
};
let _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.on_terminal_provider_event(
TerminalProviderEvent::Exit {
terminal_id,
terminal_id: terminal_id.clone(),
status,
},
cx,
@@ -886,7 +932,7 @@ impl acp::Client for ClientDelegate {
// Register with renderer
let terminal_entity = thread.update(&mut self.cx.clone(), |thread, cx| {
thread.register_terminal_created(
acp::TerminalId::new(uuid::Uuid::new_v4().to_string()),
acp::TerminalId(uuid::Uuid::new_v4().to_string().into()),
format!("{} {}", args.command, args.args.join(" ")),
args.cwd.clone(),
args.output_byte_limit,
@@ -896,7 +942,10 @@ impl acp::Client for ClientDelegate {
})?;
let terminal_id =
terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone())?;
Ok(acp::CreateTerminalResponse::new(terminal_id))
Ok(acp::CreateTerminalResponse {
terminal_id,
meta: None,
})
}
async fn kill_terminal_command(
@@ -957,7 +1006,10 @@ impl acp::Client for ClientDelegate {
})??
.await;
Ok(acp::WaitForTerminalExitResponse::new(exit_status))
Ok(acp::WaitForTerminalExitResponse {
exit_status,
meta: None,
})
}
}

View File

@@ -41,7 +41,7 @@ impl AgentServer for ClaudeCode {
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -62,7 +62,7 @@ impl AgentServer for ClaudeCode {
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(acp::ModelId::new))
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

View File

@@ -42,7 +42,7 @@ impl AgentServer for Codex {
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -63,7 +63,7 @@ impl AgentServer for Codex {
settings
.as_ref()
.and_then(|s| s.default_model.clone().map(acp::ModelId::new))
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

View File

@@ -44,7 +44,7 @@ impl crate::AgentServer for CustomAgentServer {
settings
.as_ref()
.and_then(|s| s.default_mode().map(acp::SessionModeId::new))
.and_then(|s| s.default_mode().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
@@ -80,7 +80,7 @@ impl crate::AgentServer for CustomAgentServer {
settings
.as_ref()
.and_then(|s| s.default_model().map(acp::ModelId::new))
.and_then(|s| s.default_model().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {

View File

@@ -82,9 +82,26 @@ where
.update(cx, |thread, cx| {
thread.send(
vec![
"Read the file ".into(),
acp::ContentBlock::ResourceLink(acp::ResourceLink::new("foo.rs", "foo.rs")),
" and tell me what the content of the println! is".into(),
acp::ContentBlock::Text(acp::TextContent {
text: "Read the file ".into(),
annotations: None,
meta: None,
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: "foo.rs".into(),
name: "foo.rs".into(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
acp::ContentBlock::Text(acp::TextContent {
text: " and tell me what the content of the println! is".into(),
annotations: None,
meta: None,
}),
],
cx,
)
@@ -412,7 +429,7 @@ macro_rules! common_e2e_tests {
async fn tool_call_with_permission(cx: &mut ::gpui::TestAppContext) {
$crate::e2e_tests::test_tool_call_with_permission(
$server,
::agent_client_protocol::PermissionOptionId::new($allow_option_id),
::agent_client_protocol::PermissionOptionId($allow_option_id.into()),
cx,
)
.await;

View File

@@ -13,8 +13,7 @@ path = "src/agent_ui.rs"
doctest = false
[features]
test-support = ["gpui/test-support", "language/test-support", "reqwest_client"]
unit-eval = []
test-support = ["gpui/test-support", "language/test-support"]
[dependencies]
acp_thread.workspace = true
@@ -48,7 +47,6 @@ fs.workspace = true
futures.workspace = true
fuzzy.workspace = true
gpui.workspace = true
gpui_tokio.workspace = true
html_to_markdown.workspace = true
http_client.workspace = true
indoc.workspace = true
@@ -100,17 +98,14 @@ workspace.workspace = true
zed_actions.workspace = true
image.workspace = true
async-fs.workspace = true
reqwest_client = { workspace = true, optional = true }
[dev-dependencies]
acp_thread = { workspace = true, features = ["test-support"] }
agent = { workspace = true, features = ["test-support"] }
assistant_text_thread = { workspace = true, features = ["test-support"] }
buffer_diff = { workspace = true, features = ["test-support"] }
clock.workspace = true
db = { workspace = true, features = ["test-support"] }
editor = { workspace = true, features = ["test-support"] }
eval_utils.workspace = true
gpui = { workspace = true, "features" = ["test-support"] }
indoc.workspace = true
language = { workspace = true, "features" = ["test-support"] }
@@ -120,6 +115,5 @@ pretty_assertions.workspace = true
project = { workspace = true, features = ["test-support"] }
semver.workspace = true
rand.workspace = true
reqwest_client.workspace = true
tree-sitter-md.workspace = true
unindent.workspace = true

View File

@@ -432,11 +432,24 @@ mod tests {
let (workspace, cx) =
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
let tool_call = acp::ToolCall::new("tool", "Tool call")
.status(acp::ToolCallStatus::InProgress)
.content(vec![acp::ToolCallContent::Diff(
acp::Diff::new("/project/hello.txt", "hello world").old_text("hi world"),
)]);
let tool_call = acp::ToolCall {
id: acp::ToolCallId("tool".into()),
title: "Tool call".into(),
kind: acp::ToolKind::Other,
status: acp::ToolCallStatus::InProgress,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/hello.txt".into(),
old_text: Some("hi world".into()),
new_text: "hello world".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let connection = Rc::new(StubAgentConnection::new());
let thread = cx
.update(|_, cx| {

View File

@@ -225,13 +225,8 @@ impl MessageEditor {
.iter()
.find(|command| command.name == command_name)?;
let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {
mut hint,
..
}) = available_command.input.clone()?
else {
return None;
};
let acp::AvailableCommandInput::Unstructured { mut hint } =
available_command.input.clone()?;
let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
if hint_pos > snapshot.len() {
@@ -408,28 +403,34 @@ impl MessageEditor {
} => {
all_tracked_buffers.extend(tracked_buffers.iter().cloned());
if supports_embedded_context {
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(
content.clone(),
uri.to_uri().to_string(),
acp::ContentBlock::Resource(acp::EmbeddedResource {
annotations: None,
resource:
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
mime_type: None,
text: content.clone(),
uri: uri.to_uri().to_string(),
meta: None,
},
),
),
))
meta: None,
})
} else {
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
uri.name(),
uri.to_uri().to_string(),
))
acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
})
}
}
Mention::Image(mention_image) => {
let mut image = acp::ImageContent::new(
mention_image.data.clone(),
mention_image.format.mime_type(),
);
if let Some(uri) = match uri {
let uri = match uri {
MentionUri::File { .. } => Some(uri.to_uri().to_string()),
MentionUri::PastedImage => None,
other => {
@@ -439,14 +440,25 @@ impl MessageEditor {
);
None
}
} {
image = image.uri(uri)
};
acp::ContentBlock::Image(image)
acp::ContentBlock::Image(acp::ImageContent {
annotations: None,
data: mention_image.data.to_string(),
mime_type: mention_image.format.mime_type().into(),
uri,
meta: None,
})
}
Mention::Link => acp::ContentBlock::ResourceLink(
acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
),
Mention::Link => acp::ContentBlock::ResourceLink(acp::ResourceLink {
name: uri.name(),
uri: uri.to_uri().to_string(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
};
chunks.push(chunk);
ix = crease_range.end.0;
@@ -734,7 +746,8 @@ impl MessageEditor {
uri,
data,
mime_type,
..
annotations: _,
meta: _,
}) => {
let mention_uri = if let Some(uri) = uri {
MentionUri::parse(&uri, path_style)
@@ -760,7 +773,7 @@ impl MessageEditor {
}),
));
}
_ => {}
acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
}
}
@@ -1079,7 +1092,12 @@ mod tests {
assert!(error_message.contains("Available commands: none"));
// Now simulate Claude providing its list of available commands (which doesn't include file)
available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
available_commands.replace(vec![acp::AvailableCommand {
name: "help".to_string(),
description: "Get help".to_string(),
input: None,
meta: None,
}]);
// Test that unsupported slash commands trigger an error when we have a list of available commands
editor.update_in(cx, |editor, window, cx| {
@@ -1193,12 +1211,20 @@ mod tests {
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
let prompt_capabilities = Rc::new(RefCell::new(acp::PromptCapabilities::default()));
let available_commands = Rc::new(RefCell::new(vec![
acp::AvailableCommand::new("quick-math", "2 + 2 = 4 - 1 = 3"),
acp::AvailableCommand::new("say-hello", "Say hello to whoever you want").input(
acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput::new(
"<name>",
)),
),
acp::AvailableCommand {
name: "quick-math".to_string(),
description: "2 + 2 = 4 - 1 = 3".to_string(),
input: None,
meta: None,
},
acp::AvailableCommand {
name: "say-hello".to_string(),
description: "Say hello to whoever you want".to_string(),
input: Some(acp::AvailableCommandInput::Unstructured {
hint: "<name>".to_string(),
}),
meta: None,
},
]));
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
@@ -1478,12 +1504,12 @@ mod tests {
editor.set_text("", window, cx);
});
prompt_capabilities.replace(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
);
prompt_capabilities.replace(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
});
cx.simulate_input("Lorem ");
@@ -1934,9 +1960,11 @@ mod tests {
cx,
);
// Enable embedded context so files are actually included
editor
.prompt_capabilities
.replace(acp::PromptCapabilities::new().embedded_context(true));
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
meta: None,
..Default::default()
});
editor
})
});
@@ -2015,7 +2043,7 @@ mod tests {
// Create a thread metadata to insert as summary
let thread_metadata = agent::DbThreadMetadata {
id: acp::SessionId::new("thread-123"),
id: acp::SessionId("thread-123".into()),
title: "Previous Conversation".into(),
updated_at: chrono::Utc::now(),
};
@@ -2122,7 +2150,14 @@ mod tests {
.await
.unwrap();
assert_eq!(content, vec!["してhello world".into()]);
assert_eq!(
content,
vec![acp::ContentBlock::Text(acp::TextContent {
text: "してhello world".into(),
annotations: None,
meta: None
})]
);
}
#[gpui::test]
@@ -2201,24 +2236,38 @@ mod tests {
.0;
let main_rs_uri = if cfg!(windows) {
"file:///C:/project/src/main.rs"
"file:///C:/project/src/main.rs".to_string()
} else {
"file:///project/src/main.rs"
"file:///project/src/main.rs".to_string()
};
// When embedded context is `false` we should get a resource link
pretty_assertions::assert_eq!(
content,
vec![
"What is in ".into(),
acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: main_rs_uri.clone(),
name: "main.rs".to_string(),
annotations: None,
meta: None,
description: None,
mime_type: None,
size: None,
title: None,
})
]
);
message_editor.update(cx, |editor, _cx| {
editor
.prompt_capabilities
.replace(acp::PromptCapabilities::new().embedded_context(true))
editor.prompt_capabilities.replace(acp::PromptCapabilities {
embedded_context: true,
..Default::default()
})
});
let content = message_editor
@@ -2231,12 +2280,23 @@ mod tests {
pretty_assertions::assert_eq!(
content,
vec![
"What is in ".into(),
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents::new(file_content, main_rs_uri)
)
))
acp::ContentBlock::Text(acp::TextContent {
text: "What is in ".to_string(),
annotations: None,
meta: None
}),
acp::ContentBlock::Resource(acp::EmbeddedResource {
resource: acp::EmbeddedResourceResource::TextResourceContents(
acp::TextResourceContents {
text: file_content.to_string(),
uri: main_rs_uri,
mime_type: None,
meta: None
}
),
annotations: None,
meta: None
})
]
);
}

View File

@@ -161,7 +161,7 @@ impl Render for ModeSelector {
.map(|mode| mode.name.clone())
.unwrap_or_else(|| "Unknown".into());
let this = cx.weak_entity();
let this = cx.entity();
let icon = if self.menu_handle.is_deployed() {
IconName::ChevronUp
@@ -222,8 +222,7 @@ impl Render for ModeSelector {
y: px(-2.0),
})
.menu(move |window, cx| {
this.update(cx, |this, cx| this.build_context_menu(window, cx))
.ok()
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
}
}

View File

@@ -464,7 +464,7 @@ mod tests {
models
.into_iter()
.map(|model| acp_thread::AgentModelInfo {
id: acp::ModelId::new(model.to_string()),
id: acp::ModelId(model.to_string().into()),
name: model.to_string().into(),
description: None,
icon: None,

View File

@@ -498,7 +498,17 @@ impl AcpThreadView {
Some(new_version_available_tx),
);
let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
let agent_name = agent.name();
let timeout = cx.background_executor().timer(Duration::from_secs(30));
let connect_task = smol::future::or(
agent.connect(root_dir.as_deref(), delegate, cx),
async move {
timeout.await;
Err(anyhow::Error::new(LoadError::Other(
format!("{agent_name} is unable to initialize after 30 seconds.").into(),
)))
},
);
let load_task = cx.spawn_in(window, async move |this, cx| {
let connection = match connect_task.await {
Ok((connection, login)) => {
@@ -1476,8 +1486,18 @@ impl AcpThreadView {
.iter()
.any(|method| method.id.0.as_ref() == "claude-login")
{
available_commands.push(acp::AvailableCommand::new("login", "Authenticate"));
available_commands.push(acp::AvailableCommand::new("logout", "Authenticate"));
available_commands.push(acp::AvailableCommand {
name: "login".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
available_commands.push(acp::AvailableCommand {
name: "logout".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
}
let has_commands = !available_commands.is_empty();
@@ -2552,7 +2572,7 @@ impl AcpThreadView {
acp::ToolKind::Think => IconName::ToolThink,
acp::ToolKind::Fetch => IconName::ToolWeb,
acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
acp::ToolKind::Other | _ => IconName::ToolHammer,
acp::ToolKind::Other => IconName::ToolHammer,
})
}
.size(IconSize::Small)
@@ -2804,7 +2824,7 @@ impl AcpThreadView {
})
.gap_0p5()
.children(options.iter().map(move |option| {
let option_id = SharedString::from(option.option_id.0.clone());
let option_id = SharedString::from(option.id.0.clone());
Button::new((option_id, entry_ix), option.name.clone())
.map(|this| {
let (this, action) = match option.kind {
@@ -2820,7 +2840,7 @@ impl AcpThreadView {
this.icon(IconName::Close).icon_color(Color::Error),
Some(&RejectOnce as &dyn Action),
),
acp::PermissionOptionKind::RejectAlways | _ => {
acp::PermissionOptionKind::RejectAlways => {
(this.icon(IconName::Close).icon_color(Color::Error), None)
}
};
@@ -2845,7 +2865,7 @@ impl AcpThreadView {
.label_size(LabelSize::Small)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
let option_id = option.option_id.clone();
let option_id = option.id.clone();
let option_kind = option.kind;
move |this, _, window, cx| {
this.authorize_tool_call(
@@ -3533,7 +3553,7 @@ impl AcpThreadView {
);
this.authenticate(
acp::AuthMethodId::new(method_id.clone()),
acp::AuthMethodId(method_id.clone()),
window,
cx,
)
@@ -3827,6 +3847,10 @@ impl AcpThreadView {
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(match entry.status {
acp::PlanEntryStatus::Pending => Icon::new(IconName::TodoPending)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element(),
acp::PlanEntryStatus::InProgress => {
Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
@@ -3840,12 +3864,6 @@ impl AcpThreadView {
.color(Color::Success)
.into_any_element()
}
acp::PlanEntryStatus::Pending | _ => {
Icon::new(IconName::TodoPending)
.size(IconSize::Small)
.color(Color::Muted)
.into_any_element()
}
})
.child(MarkdownElement::new(
entry.content.clone(),
@@ -4419,7 +4437,7 @@ impl AcpThreadView {
self.authorize_tool_call(
tool_call.id.clone(),
option.option_id.clone(),
option.id.clone(),
option.kind,
window,
cx,
@@ -6235,18 +6253,27 @@ pub(crate) mod tests {
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
init_test(cx);
let tool_call_id = acp::ToolCallId::new("1");
let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
.kind(acp::ToolKind::Edit)
.content(vec!["hi".into()]);
let tool_call_id = acp::ToolCallId("1".into());
let tool_call = acp::ToolCall {
id: tool_call_id.clone(),
title: "Label".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Pending,
content: vec!["hi".into()],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let connection =
StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
tool_call_id,
vec![acp::PermissionOption::new(
"1".into(),
"Allow",
acp::PermissionOptionKind::AllowOnce,
)],
vec![acp::PermissionOption {
id: acp::PermissionOptionId("1".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
}],
)]));
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
@@ -6465,7 +6492,10 @@ pub(crate) mod tests {
fn default_response() -> Self {
let conn = StubAgentConnection::new();
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Default response".into()),
acp::ContentChunk {
content: "Default response".into(),
meta: None,
},
)]);
Self::new(conn)
}
@@ -6522,13 +6552,13 @@ pub(crate) mod tests {
self,
project,
action_log,
SessionId::new("test"),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
SessionId("test".into()),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
})))
@@ -6586,13 +6616,13 @@ pub(crate) mod tests {
self,
project,
action_log,
SessionId::new("test"),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
SessionId("test".into()),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
})))
@@ -6616,7 +6646,10 @@ pub(crate) mod tests {
_params: acp::PromptRequest,
_cx: &mut App,
) -> Task<gpui::Result<acp::PromptResponse>> {
Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
}))
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
@@ -6684,14 +6717,24 @@ pub(crate) mod tests {
.unwrap();
// First user message
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
acp::ToolCall::new("tool1", "Edit file 1")
.kind(acp::ToolKind::Edit)
.status(acp::ToolCallStatus::Completed)
.content(vec![acp::ToolCallContent::Diff(
acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
)]),
)]);
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool1".into()),
title: "Edit file 1".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Completed,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/test1.txt".into(),
old_text: Some("old content 1".into()),
new_text: "new content 1".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
thread
.update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
@@ -6717,14 +6760,24 @@ pub(crate) mod tests {
});
// Second user message
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
acp::ToolCall::new("tool2", "Edit file 2")
.kind(acp::ToolKind::Edit)
.status(acp::ToolCallStatus::Completed)
.content(vec![acp::ToolCallContent::Diff(
acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
)]),
)]);
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool2".into()),
title: "Edit file 2".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Completed,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/test2.txt".into(),
old_text: Some("old content 2".into()),
new_text: "new content 2".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
thread
.update(cx, |thread, cx| thread.send_raw("Another one", cx))
@@ -6798,7 +6851,14 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -6884,7 +6944,14 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
let (thread_view, cx) =
@@ -6924,7 +6991,14 @@ pub(crate) mod tests {
// Send
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("New Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "New Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
user_message_editor.update_in(cx, |_editor, window, cx| {
@@ -7012,7 +7086,14 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
}),
cx,
);
connection.end_turn(session_id, acp::StopReason::EndTurn);
@@ -7064,9 +7145,10 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"Message 1 resp".into(),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "Message 1 resp".into(),
meta: None,
}),
cx,
);
});
@@ -7100,7 +7182,10 @@ pub(crate) mod tests {
// Simulate a response sent after beginning to cancel
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "onse".into(),
meta: None,
}),
cx,
);
});
@@ -7131,9 +7216,10 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"Message 2 response".into(),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "Message 2 response".into(),
meta: None,
}),
cx,
);
connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
@@ -7172,7 +7258,14 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -7251,7 +7344,14 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -7299,4 +7399,54 @@ pub(crate) mod tests {
assert_eq!(text, expected_txt);
})
}
#[gpui::test]
async fn test_initialize_timeout(cx: &mut TestAppContext) {
init_test(cx);
struct InfiniteInitialize;
impl AgentServer for InfiniteInitialize {
fn telemetry_id(&self) -> &'static str {
"test"
}
fn logo(&self) -> ui::IconName {
ui::IconName::Ai
}
fn name(&self) -> SharedString {
"Test".into()
}
fn connect(
&self,
_root_dir: Option<&Path>,
_delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>
{
cx.spawn(async |_| futures::future::pending().await)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
let (thread_view, cx) = setup_thread_view(InfiniteInitialize, cx).await;
cx.executor().advance_clock(Duration::from_secs(31));
cx.run_until_parked();
let error = thread_view.read_with(cx, |thread_view, _| match &thread_view.thread_state {
ThreadState::LoadError(err) => err.clone(),
_ => panic!("Incorrect thread state"),
});
match error {
LoadError::Other(str) => assert!(str.contains("initialize")),
_ => panic!("Unexpected load error"),
}
}
}

View File

@@ -2685,17 +2685,16 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
return;
};
let project = workspace.read(cx).project().downgrade();
let thread_store = panel.read(cx).thread_store().clone();
assistant.assist(
prompt_editor,
self.workspace.clone(),
project,
thread_store,
panel.read(cx).thread_store().clone(),
None,
initial_prompt,
window,
cx,
);
)
})
}

View File

@@ -7,8 +7,6 @@ mod buffer_codegen;
mod completion_provider;
mod context;
mod context_server_configuration;
#[cfg(test)]
mod evals;
mod inline_assistant;
mod inline_prompt_editor;
mod language_model_selector;

View File

@@ -719,7 +719,6 @@ impl CodegenAlternative {
output_tokens = usage.output_tokens,
)
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})

View File

@@ -1114,6 +1114,7 @@ impl<T: PromptCompletionProviderDelegate> CompletionProvider for PromptCompletio
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);

View File

@@ -1,89 +0,0 @@
use std::str::FromStr;
use crate::inline_assistant::test::run_inline_assistant_test;
use eval_utils::{EvalOutput, NoProcessor};
use gpui::TestAppContext;
use language_model::{LanguageModelRegistry, SelectedModel};
use rand::{SeedableRng as _, rngs::StdRng};
#[test]
#[cfg_attr(not(feature = "unit-eval"), ignore)]
fn eval_single_cursor_edit() {
eval_utils::eval(20, 1.0, NoProcessor, move || {
run_eval(
&EvalInput {
prompt: "Rename this variable to buffer_text".to_string(),
buffer: indoc::indoc! {"
struct EvalExampleStruct {
text: Strˇing,
prompt: String,
}
"}
.to_string(),
},
&|_, output| {
let expected = indoc::indoc! {"
struct EvalExampleStruct {
buffer_text: String,
prompt: String,
}
"};
if output == expected {
EvalOutput {
outcome: eval_utils::OutcomeKind::Passed,
data: "Passed!".to_string(),
metadata: (),
}
} else {
EvalOutput {
outcome: eval_utils::OutcomeKind::Failed,
data: format!("Failed to rename variable, output: {}", output),
metadata: (),
}
}
},
)
});
}
struct EvalInput {
buffer: String,
prompt: String,
}
fn run_eval(
input: &EvalInput,
judge: &dyn Fn(&EvalInput, &str) -> eval_utils::EvalOutput<()>,
) -> eval_utils::EvalOutput<()> {
let dispatcher = gpui::TestDispatcher::new(StdRng::from_os_rng());
let mut cx = TestAppContext::build(dispatcher, None);
cx.skip_drawing();
let buffer_text = run_inline_assistant_test(
input.buffer.clone(),
input.prompt.clone(),
|cx| {
// Reconfigure to use a real model instead of the fake one
let model_name = std::env::var("ZED_AGENT_MODEL")
.unwrap_or("anthropic/claude-sonnet-4-latest".into());
let selected_model = SelectedModel::from_str(&model_name)
.expect("Invalid model format. Use 'provider/model-id'");
log::info!("Selected model: {selected_model:?}");
cx.update(|_, cx| {
LanguageModelRegistry::global(cx).update(cx, |registry, cx| {
registry.select_inline_assistant_model(Some(&selected_model), cx);
});
});
},
|_cx| {
log::info!("Waiting for actual response from the LLM...");
},
&mut cx,
);
judge(input, &buffer_text)
}

View File

@@ -32,7 +32,7 @@ use editor::{
},
};
use fs::Fs;
use futures::{FutureExt, channel::mpsc};
use futures::FutureExt;
use gpui::{
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
WeakEntity, Window, point,
@@ -102,7 +102,6 @@ pub struct InlineAssistant {
prompt_builder: Arc<PromptBuilder>,
telemetry: Arc<Telemetry>,
fs: Arc<dyn Fs>,
_inline_assistant_completions: Option<mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>>,
}
impl Global for InlineAssistant {}
@@ -124,18 +123,9 @@ impl InlineAssistant {
prompt_builder,
telemetry,
fs,
_inline_assistant_completions: None,
}
}
#[cfg(any(test, feature = "test-support"))]
pub fn set_completion_receiver(
&mut self,
sender: mpsc::UnboundedSender<anyhow::Result<InlineAssistId>>,
) {
self._inline_assistant_completions = Some(sender);
}
pub fn register_workspace(
&mut self,
workspace: &Entity<Workspace>,
@@ -297,7 +287,7 @@ impl InlineAssistant {
action.prompt.clone(),
window,
cx,
);
)
})
}
InlineAssistTarget::Terminal(active_terminal) => {
@@ -311,8 +301,8 @@ impl InlineAssistant {
action.prompt.clone(),
window,
cx,
);
});
)
})
}
};
@@ -608,13 +598,13 @@ impl InlineAssistant {
initial_prompt: Option<String>,
window: &mut Window,
cx: &mut App,
) -> Option<InlineAssistId> {
) {
let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
let Some((codegen_ranges, newest_selection)) =
self.codegen_ranges(editor, &snapshot, window, cx)
else {
return None;
return;
};
let assist_to_focus = self.batch_assist(
@@ -634,8 +624,6 @@ impl InlineAssistant {
if let Some(assist_id) = assist_to_focus {
self.focus_assist(assist_id, window, cx);
}
assist_to_focus
}
pub fn suggest_assist(
@@ -1752,16 +1740,6 @@ impl InlineAssist {
&& assist.decorations.is_none()
&& let Some(workspace) = assist.workspace.upgrade()
{
#[cfg(any(test, feature = "test-support"))]
if let Some(sender) = &mut this._inline_assistant_completions {
sender
.unbounded_send(Err(anyhow::anyhow!(
"Inline assistant error: {}",
error
)))
.ok();
}
let error = format!("Inline assistant error: {}", error);
workspace.update(cx, |workspace, cx| {
struct InlineAssistantError;
@@ -1772,11 +1750,6 @@ impl InlineAssist {
workspace.show_toast(Toast::new(id, error), cx);
})
} else {
#[cfg(any(test, feature = "test-support"))]
if let Some(sender) = &mut this._inline_assistant_completions {
sender.unbounded_send(Ok(assist_id)).ok();
}
}
if assist.decorations.is_none() {
@@ -1970,160 +1943,3 @@ fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
}
}
}
#[cfg(any(test, feature = "test-support"))]
pub mod test {
use std::sync::Arc;
use agent::HistoryStore;
use assistant_text_thread::TextThreadStore;
use client::{Client, UserStore};
use editor::{Editor, MultiBuffer, MultiBufferOffset};
use fs::FakeFs;
use futures::channel::mpsc;
use gpui::{AppContext, TestAppContext, UpdateGlobal as _};
use language::Buffer;
use language_model::LanguageModelRegistry;
use project::Project;
use prompt_store::PromptBuilder;
use smol::stream::StreamExt as _;
use util::test::marked_text_ranges;
use workspace::Workspace;
use crate::InlineAssistant;
pub fn run_inline_assistant_test<SetupF, TestF>(
base_buffer: String,
prompt: String,
setup: SetupF,
test: TestF,
cx: &mut TestAppContext,
) -> String
where
SetupF: FnOnce(&mut gpui::VisualTestContext),
TestF: FnOnce(&mut gpui::VisualTestContext),
{
let fs = FakeFs::new(cx.executor());
let app_state = cx.update(|cx| workspace::AppState::test(cx));
let prompt_builder = Arc::new(PromptBuilder::new(None).unwrap());
let http = Arc::new(reqwest_client::ReqwestClient::user_agent("agent tests").unwrap());
let client = cx.update(|cx| {
cx.set_http_client(http);
Client::production(cx)
});
let mut inline_assistant =
InlineAssistant::new(fs.clone(), prompt_builder, client.telemetry().clone());
let (tx, mut completion_rx) = mpsc::unbounded();
inline_assistant.set_completion_receiver(tx);
// Initialize settings and client
cx.update(|cx| {
gpui_tokio::init(cx);
settings::init(cx);
client::init(&client, cx);
workspace::init(app_state.clone(), cx);
let user_store = cx.new(|cx| UserStore::new(client.clone(), cx));
language_model::init(client.clone(), cx);
language_models::init(user_store, client.clone(), cx);
cx.set_global(inline_assistant);
});
let project = cx
.executor()
.block_test(async { Project::test(fs.clone(), [], cx).await });
// Create workspace with window
let (workspace, cx) = cx.add_window_view(|window, cx| {
window.activate_window();
Workspace::new(None, project.clone(), app_state.clone(), window, cx)
});
setup(cx);
let (_editor, buffer) = cx.update(|window, cx| {
let buffer = cx.new(|cx| Buffer::local("", cx));
let multibuffer = cx.new(|cx| MultiBuffer::singleton(buffer.clone(), cx));
let editor = cx.new(|cx| Editor::for_multibuffer(multibuffer, None, window, cx));
editor.update(cx, |editor, cx| {
let (unmarked_text, selection_ranges) = marked_text_ranges(&base_buffer, true);
editor.set_text(unmarked_text, window, cx);
editor.change_selections(Default::default(), window, cx, |s| {
s.select_ranges(
selection_ranges.into_iter().map(|range| {
MultiBufferOffset(range.start)..MultiBufferOffset(range.end)
}),
)
})
});
let text_thread_store = cx.new(|cx| TextThreadStore::fake(project.clone(), cx));
let history_store = cx.new(|cx| HistoryStore::new(text_thread_store, cx));
// Add editor to workspace
workspace.update(cx, |workspace, cx| {
workspace.add_item_to_active_pane(Box::new(editor.clone()), None, true, window, cx);
});
// Call assist method
InlineAssistant::update_global(cx, |inline_assistant, cx| {
let assist_id = inline_assistant
.assist(
&editor,
workspace.downgrade(),
project.downgrade(),
history_store, // thread_store
None, // prompt_store
Some(prompt),
window,
cx,
)
.unwrap();
inline_assistant.start_assist(assist_id, window, cx);
});
(editor, buffer)
});
cx.run_until_parked();
test(cx);
cx.executor()
.block_test(async { completion_rx.next().await });
buffer.read_with(cx, |buffer, _| buffer.text())
}
#[allow(unused)]
pub fn test_inline_assistant(
base_buffer: &'static str,
llm_output: &'static str,
cx: &mut TestAppContext,
) -> String {
run_inline_assistant_test(
base_buffer.to_string(),
"Prompt doesn't matter because we're using a fake model".to_string(),
|cx| {
cx.update(|_, cx| LanguageModelRegistry::test(cx));
},
|cx| {
let fake_model = cx.update(|_, cx| {
LanguageModelRegistry::global(cx)
.update(cx, |registry, _| registry.fake_model())
});
let fake = fake_model.as_fake();
// let fake = fake_model;
fake.send_last_completion_stream_text_chunk(llm_output.to_string());
fake.end_last_completion_stream();
// Run again to process the model's response
cx.run_until_parked();
},
cx,
)
}
}

View File

@@ -341,6 +341,7 @@ impl CompletionProvider for SlashCommandCompletionProvider {
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let buffer = buffer.read(cx);

View File

@@ -2622,13 +2622,11 @@ impl SearchableItem for TextThreadEditor {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.update_matches(matches, active_match_index, window, cx)
});
self.editor
.update(cx, |editor, cx| editor.update_matches(matches, window, cx));
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

@@ -25,7 +25,6 @@ use gpui::{
use indoc::indoc;
use language::FakeLspAdapter;
use lsp::LSP_REQUEST_TIMEOUT;
use pretty_assertions::assert_eq;
use project::{
ProgressToken, ProjectPath, SERVER_PROGRESS_THROTTLE_TIMEOUT,
lsp_store::lsp_ext_command::{ExpandedMacro, LspExtExpandMacro},
@@ -3193,12 +3192,13 @@ async fn test_lsp_pull_diagnostics(
.collect::<Vec<_>>();
let expected_messages = [
expected_pull_diagnostic_lib_message,
expected_push_diagnostic_lib_message,
// TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
// expected_push_diagnostic_lib_message,
];
assert_eq!(
all_diagnostics.len(),
2,
"Expected pull and push diagnostics, but got: {all_diagnostics:?}"
1,
"Expected pull diagnostics, but got: {all_diagnostics:?}"
);
for diagnostic in all_diagnostics {
assert!(
@@ -3258,15 +3258,14 @@ async fn test_lsp_pull_diagnostics(
.diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
.collect::<Vec<_>>();
let expected_messages = [
// Despite workspace diagnostics provided,
// the currently open file's diagnostics should be preferred, as LSP suggests.
expected_pull_diagnostic_lib_message,
expected_push_diagnostic_lib_message,
expected_workspace_pull_diagnostics_lib_message,
// TODO bug: the pushed diagnostics are not being sent to the client when they open the corresponding buffer.
// expected_push_diagnostic_lib_message,
];
assert_eq!(
all_diagnostics.len(),
2,
"Expected pull and push diagnostics, but got: {all_diagnostics:?}"
1,
"Expected pull diagnostics, but got: {all_diagnostics:?}"
);
for diagnostic in all_diagnostics {
assert!(
@@ -3379,9 +3378,8 @@ async fn test_lsp_pull_diagnostics(
"Another workspace diagnostics pull should happen after the diagnostics refresh server request"
);
{
assert_eq!(
diagnostics_pulls_result_ids.lock().await.len(),
diagnostic_pulls_result_ids,
assert!(
diagnostics_pulls_result_ids.lock().await.len() == diagnostic_pulls_result_ids,
"Pulls should not happen hence no extra ids should appear"
);
assert!(
@@ -3399,7 +3397,7 @@ async fn test_lsp_pull_diagnostics(
expected_pull_diagnostic_lib_message,
expected_push_diagnostic_lib_message,
];
assert_eq!(all_diagnostics.len(), 2);
assert_eq!(all_diagnostics.len(), 1);
for diagnostic in &all_diagnostics {
assert!(
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),

View File

@@ -23,9 +23,6 @@ zstd.workspace = true
[target.'cfg(target_os = "macos")'.dependencies]
mach2.workspace = true
[target.'cfg(target_os = "windows")'.dependencies]
windows.workspace = true
[lints]
workspace = true

View File

@@ -3,8 +3,6 @@ use log::info;
use minidumper::{Client, LoopAction, MinidumpBinary};
use release_channel::{RELEASE_CHANNEL, ReleaseChannel};
use serde::{Deserialize, Serialize};
#[cfg(not(target_os = "windows"))]
use smol::process::Command;
#[cfg(target_os = "macos")]
@@ -72,16 +70,11 @@ pub async fn init(crash_init: InitCrashHandler) {
// used by the crash handler isn't destroyed correctly which causes it to stay on the file
// system and block further attempts to initialize crash handlers with that socket path.
let socket_name = paths::temp_dir().join(format!("zed-crash-handler-{zed_pid}"));
#[cfg(not(target_os = "windows"))]
let _crash_handler = Command::new(exe)
.arg("--crash-handler")
.arg(&socket_name)
.spawn()
.expect("unable to spawn server process");
#[cfg(target_os = "windows")]
spawn_crash_handler_windows(&exe, &socket_name);
#[cfg(target_os = "linux")]
let server_pid = _crash_handler.id();
info!("spawning crash handler process");
@@ -349,57 +342,6 @@ pub fn panic_hook(info: &PanicHookInfo) {
}
}
#[cfg(target_os = "windows")]
fn spawn_crash_handler_windows(exe: &Path, socket_name: &Path) {
use std::ffi::OsStr;
use std::iter::once;
use std::os::windows::ffi::OsStrExt;
use windows::Win32::System::Threading::{
CreateProcessW, PROCESS_CREATION_FLAGS, PROCESS_INFORMATION, STARTF_FORCEOFFFEEDBACK,
STARTUPINFOW,
};
use windows::core::PWSTR;
let mut command_line: Vec<u16> = OsStr::new(&format!(
"\"{}\" --crash-handler \"{}\"",
exe.display(),
socket_name.display()
))
.encode_wide()
.chain(once(0))
.collect();
let mut startup_info = STARTUPINFOW::default();
startup_info.cb = std::mem::size_of::<STARTUPINFOW>() as u32;
// By default, Windows enables a "busy" cursor when a GUI application is launched.
// This cursor is disabled once the application starts processing window messages.
// Since the crash handler process doesn't process messages, this "busy" cursor stays enabled for a long time.
// Disable the cursor feedback to prevent this from happening.
startup_info.dwFlags = STARTF_FORCEOFFFEEDBACK;
let mut process_info = PROCESS_INFORMATION::default();
unsafe {
CreateProcessW(
None,
Some(PWSTR(command_line.as_mut_ptr())),
None,
None,
false,
PROCESS_CREATION_FLAGS(0),
None,
None,
&startup_info,
&mut process_info,
)
.expect("unable to spawn server process");
windows::Win32::Foundation::CloseHandle(process_info.hProcess).ok();
windows::Win32::Foundation::CloseHandle(process_info.hThread).ok();
}
}
pub fn crash_server(socket: &Path) {
let Ok(mut server) = minidumper::Server::with_name(socket) else {
log::info!("Couldn't create socket, there may already be a running crash server");

View File

@@ -1017,13 +1017,11 @@ impl SearchableItem for DapLogView {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |e, cx| {
e.update_matches(matches, active_match_index, window, cx)
})
self.editor
.update(cx, |e, cx| e.update_matches(matches, window, cx))
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

@@ -740,7 +740,7 @@ impl DebugPanel {
}
})
.child(
IconButton::new("step-over", IconName::DebugStepOver)
IconButton::new("debug-step-over", IconName::ArrowRight)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
running_state,
@@ -762,29 +762,32 @@ impl DebugPanel {
}),
)
.child(
IconButton::new("step-into", IconName::DebugStepInto)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
running_state,
|this, _, _window, cx| {
this.step_in(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Step In",
&StepInto,
&focus_handle,
cx,
)
}
}),
IconButton::new(
"debug-step-into",
IconName::ArrowDownRight,
)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
running_state,
|this, _, _window, cx| {
this.step_in(cx);
},
))
.disabled(thread_status != ThreadStatus::Stopped)
.tooltip({
let focus_handle = focus_handle.clone();
move |_window, cx| {
Tooltip::for_action_in(
"Step In",
&StepInto,
&focus_handle,
cx,
)
}
}),
)
.child(
IconButton::new("step-out", IconName::DebugStepOut)
IconButton::new("debug-step-out", IconName::ArrowUpRight)
.icon_size(IconSize::Small)
.on_click(window.listener_for(
running_state,

View File

@@ -252,11 +252,10 @@ impl Console {
let start_offset = range.start;
let range = buffer.anchor_after(MultiBufferOffset(range.start))
..buffer.anchor_before(MultiBufferOffset(range.end));
let color_fn = color_fetcher(color);
console.highlight_background_key::<ConsoleAnsiHighlight>(
start_offset,
&[range],
move |_, theme| color_fn(theme),
color_fetcher(color),
cx,
);
}
@@ -560,6 +559,7 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
position: language::Anchor,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let mut chars = text.chars();
@@ -570,6 +570,9 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
};
let snapshot = buffer.read(cx).snapshot();
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
return false;
}
let classifier = snapshot
.char_classifier_at(position)

View File

@@ -333,19 +333,6 @@ where
&bracket_colors_markup(&mut cx),
"All markdown brackets should be colored based on their depth"
);
cx.set_state(indoc! {r#"ˇ{{}}"#});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
r#"«1{«2{}2»}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
2 hsla(29.00, 54.00%, 65.88%, 1.00)
"#,
&bracket_colors_markup(&mut cx),
"All markdown brackets should be colored based on their depth, again"
);
}
#[gpui::test]

View File

@@ -726,10 +726,7 @@ impl EditorActionId {
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
type BackgroundHighlight = (
Arc<dyn Fn(&usize, &Theme) -> Hsla + Send + Sync>,
Arc<[Range<Anchor>]>,
);
type BackgroundHighlight = (fn(&Theme) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
#[derive(Default)]
@@ -1175,7 +1172,6 @@ pub struct Editor {
gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
hovered_diff_hunk_row: Option<DisplayRow>,
pull_diagnostics_task: Task<()>,
pull_diagnostics_background_task: Task<()>,
in_project_search: bool,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
breadcrumb_header: Option<String>,
@@ -2320,7 +2316,6 @@ impl Editor {
.unwrap_or_default(),
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
pull_diagnostics_background_task: Task::ready(()),
colors: None,
refresh_colors_task: Task::ready(()),
inlay_hints: None,
@@ -2497,6 +2492,7 @@ impl Editor {
if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
editor.register_buffer(buffer.read(cx).remote_id(), cx);
}
editor.update_lsp_data(None, window, cx);
editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
}
@@ -5513,22 +5509,6 @@ impl Editor {
};
let buffer_snapshot = buffer.read(cx).snapshot();
let menu_is_open = matches!(
self.context_menu.borrow().as_ref(),
Some(CodeContextMenu::Completions(_))
);
let language = buffer_snapshot
.language_at(buffer_position.text_anchor)
.map(|language| language.name());
let language_settings = language_settings(language.clone(), buffer_snapshot.file(), cx);
let completion_settings = language_settings.completions.clone();
if !menu_is_open && trigger.is_some() && !language_settings.show_completions_on_input {
return;
}
let query: Option<Arc<String>> =
Self::completion_query(&multibuffer_snapshot, buffer_position)
.map(|query| query.into());
@@ -5537,8 +5517,14 @@ impl Editor {
// Hide the current completions menu when query is empty. Without this, cached
// completions from before the trigger char may be reused (#32774).
if query.is_none() && menu_is_open {
self.hide_context_menu(window, cx);
if query.is_none() {
let menu_is_open = matches!(
self.context_menu.borrow().as_ref(),
Some(CodeContextMenu::Completions(_))
);
if menu_is_open {
self.hide_context_menu(window, cx);
}
}
let mut ignore_word_threshold = false;
@@ -5627,6 +5613,14 @@ impl Editor {
(buffer_position..buffer_position, None)
};
let language = buffer_snapshot
.language_at(buffer_position)
.map(|language| language.name());
let completion_settings = language_settings(language.clone(), buffer_snapshot.file(), cx)
.completions
.clone();
let show_completion_documentation = buffer_snapshot
.settings_at(buffer_position, cx)
.show_completion_documentation;
@@ -5657,6 +5651,7 @@ impl Editor {
position.text_anchor,
trigger,
trigger_in_words,
completions_source.is_some(),
cx,
)
})
@@ -6613,7 +6608,7 @@ impl Editor {
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
&ranges_to_highlight,
|_, theme| theme.colors().editor_highlighted_line_background,
|theme| theme.colors().editor_highlighted_line_background,
cx,
);
});
@@ -6814,9 +6809,6 @@ impl Editor {
return;
};
if self.blame.is_none() {
self.start_git_blame(true, window, cx);
}
let Some(blame) = self.blame.as_ref() else {
return;
};
@@ -7015,12 +7007,12 @@ impl Editor {
this.highlight_background::<DocumentHighlightRead>(
&read_ranges,
|_, theme| theme.colors().editor_document_highlight_read_background,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
this.highlight_background::<DocumentHighlightWrite>(
&write_ranges,
|_, theme| theme.colors().editor_document_highlight_write_background,
|theme| theme.colors().editor_document_highlight_write_background,
cx,
);
cx.notify();
@@ -7128,7 +7120,7 @@ impl Editor {
if !match_ranges.is_empty() {
editor.highlight_background::<SelectedTextHighlight>(
&match_ranges,
|_, theme| theme.colors().editor_document_highlight_bracket_background,
|theme| theme.colors().editor_document_highlight_bracket_background,
cx,
)
}
@@ -17015,9 +17007,7 @@ impl Editor {
})
.collect();
let Some(workspace) = self.workspace() else {
return Task::ready(Ok(Navigated::No));
};
let workspace = self.workspace();
cx.spawn_in(window, async move |editor, cx| {
let locations: Vec<Location> = future::join_all(definitions)
@@ -17043,6 +17033,10 @@ impl Editor {
}
if num_locations > 1 {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
let tab_kind = match kind {
Some(GotoDefinitionKind::Implementation) => "Implementations",
Some(GotoDefinitionKind::Symbol) | None => "Definitions",
@@ -17074,14 +17068,11 @@ impl Editor {
let opened = workspace
.update_in(cx, |workspace, window, cx| {
let allow_preview = PreviewTabsSettings::get_global(cx)
.enable_preview_multibuffer_from_code_navigation;
Self::open_locations_in_multibuffer(
workspace,
locations,
title,
split,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
@@ -17098,9 +17089,10 @@ impl Editor {
Ok(Navigated::Yes)
}
Some(Either::Right(path)) => {
// TODO(andrew): respect preview tab settings
// `enable_keep_preview_on_code_navigation` and
// `enable_preview_file_from_code_navigation`
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_resolved_path(path, window, cx)
@@ -17111,6 +17103,10 @@ impl Editor {
None => Ok(Navigated::No),
}
} else {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
let (target_buffer, target_ranges) = locations.into_iter().next().unwrap();
let target_range = target_ranges.first().unwrap().clone();
@@ -17134,19 +17130,11 @@ impl Editor {
workspace.active_pane().clone()
};
let preview_tabs_settings = PreviewTabsSettings::get_global(cx);
let keep_old_preview = preview_tabs_settings
.enable_keep_preview_on_code_navigation;
let allow_new_preview = preview_tabs_settings
.enable_preview_file_from_code_navigation;
workspace.open_project_item(
pane,
target_buffer.clone(),
true,
true,
keep_old_preview,
allow_new_preview,
window,
cx,
)
@@ -17423,14 +17411,11 @@ impl Editor {
} else {
format!("References to {target}")
};
let allow_preview = PreviewTabsSettings::get_global(cx)
.enable_preview_multibuffer_from_code_navigation;
Self::open_locations_in_multibuffer(
workspace,
locations,
title,
false,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
@@ -17446,7 +17431,6 @@ impl Editor {
locations: std::collections::HashMap<Entity<Buffer>, Vec<Range<Point>>>,
title: String,
split: bool,
allow_preview: bool,
multibuffer_selection_mode: MultibufferSelectionMode,
window: &mut Window,
cx: &mut Context<Workspace>,
@@ -17494,7 +17478,6 @@ impl Editor {
.is_some_and(|it| *it == key)
})
});
let was_existing = existing.is_some();
let editor = existing.unwrap_or_else(|| {
cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
@@ -17522,7 +17505,7 @@ impl Editor {
}
editor.highlight_background::<Self>(
&ranges,
|_, theme| theme.colors().editor_highlighted_line_background,
|theme| theme.colors().editor_highlighted_line_background,
cx,
);
}
@@ -17535,23 +17518,29 @@ impl Editor {
});
let item = Box::new(editor);
let item_id = item.item_id();
let pane = if split {
workspace.adjacent_pane(window, cx)
if split {
let pane = workspace.adjacent_pane(window, cx);
workspace.add_item(pane, item, None, true, true, window, cx);
} else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
let (preview_item_id, preview_item_idx) =
workspace.active_pane().read_with(cx, |pane, _| {
(pane.preview_item_id(), pane.preview_item_idx())
});
workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx);
if let Some(preview_item_id) = preview_item_id {
workspace.active_pane().update(cx, |pane, cx| {
pane.remove_item(preview_item_id, false, false, window, cx);
});
}
} else {
workspace.active_pane().clone()
};
let activate_pane = split;
let mut destination_index = None;
pane.update(cx, |pane, cx| {
if allow_preview && !was_existing {
destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
}
if was_existing && !allow_preview {
pane.unpreview_item_if_preview(item.item_id());
}
pane.add_item(item, activate_pane, true, destination_index, window, cx);
workspace.add_item_to_active_pane(item, None, true, window, cx);
}
workspace.active_pane().update(cx, |pane, cx| {
pane.set_preview_item_id(Some(item_id), cx);
});
}
@@ -18407,101 +18396,54 @@ impl Editor {
return None;
}
let project = self.project()?.downgrade();
let mut edited_buffer_ids = HashSet::default();
let mut edited_worktree_ids = HashSet::default();
let edited_buffers = match buffer_id {
Some(buffer_id) => {
let buffer = self.buffer().read(cx).buffer(buffer_id)?;
let worktree_id = buffer.read(cx).file().map(|f| f.worktree_id(cx))?;
edited_buffer_ids.insert(buffer.read(cx).remote_id());
edited_worktree_ids.insert(worktree_id);
vec![buffer]
}
None => self
.buffer()
.read(cx)
.all_buffers()
.into_iter()
.filter(|buffer| {
let buffer = buffer.read(cx);
match buffer.file().map(|f| f.worktree_id(cx)) {
Some(worktree_id) => {
edited_buffer_ids.insert(buffer.remote_id());
edited_worktree_ids.insert(worktree_id);
true
}
None => false,
}
})
.collect::<Vec<_>>(),
};
if edited_buffers.is_empty() {
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
let mut buffers = self.buffer.read(cx).all_buffers();
buffers.retain(|buffer| {
let buffer_id_to_retain = buffer.read(cx).remote_id();
buffer_id.is_none_or(|buffer_id| buffer_id == buffer_id_to_retain)
&& self.registered_buffers.contains_key(&buffer_id_to_retain)
});
if buffers.is_empty() {
self.pull_diagnostics_task = Task::ready(());
self.pull_diagnostics_background_task = Task::ready(());
return None;
}
let mut already_used_buffers = HashSet::default();
let related_open_buffers = self
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade())
.into_iter()
.flat_map(|workspace| workspace.read(cx).panes())
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
.filter(|editor| editor != &cx.entity())
.flat_map(|editor| editor.read(cx).buffer().read(cx).all_buffers())
.filter(|buffer| {
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
if already_used_buffers.insert(buffer_id) {
if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) {
return !edited_buffer_ids.contains(&buffer_id)
&& !edited_worktree_ids.contains(&worktree_id);
}
}
false
})
.collect::<Vec<_>>();
self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| {
cx.background_executor().timer(debounce).await;
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
let make_spawn = |buffers: Vec<Entity<Buffer>>, delay: Duration| {
if buffers.is_empty() {
return Task::ready(());
}
let project_weak = project.clone();
cx.spawn_in(window, async move |_, cx| {
cx.background_executor().timer(delay).await;
let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
buffers
.into_iter()
.filter_map(|buffer| {
project_weak
.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.pull_diagnostics_for_buffer(buffer, cx)
})
let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
buffers
.into_iter()
.filter_map(|buffer| {
project
.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.pull_diagnostics_for_buffer(buffer, cx)
})
.ok()
})
.collect::<FuturesUnordered<_>>()
}) else {
return;
};
})
.ok()
})
.collect::<FuturesUnordered<_>>()
}) else {
return;
};
while let Some(pull_task) = pull_diagnostics_tasks.next().await {
if let Err(e) = pull_task {
log::error!("Failed to update project diagnostics: {e:#}");
while let Some(pull_task) = pull_diagnostics_tasks.next().await {
match pull_task {
Ok(()) => {
if editor
.update_in(cx, |editor, window, cx| {
editor.update_diagnostics_state(window, cx);
})
.is_err()
{
return;
}
}
Err(e) => log::error!("Failed to update project diagnostics: {e:#}"),
}
})
};
self.pull_diagnostics_task = make_spawn(edited_buffers, debounce);
self.pull_diagnostics_background_task = make_spawn(related_open_buffers, debounce * 2);
}
});
Some(())
}
@@ -20789,7 +20731,6 @@ impl Editor {
locations,
format!("Selections for '{title}'"),
false,
false,
MultibufferSelectionMode::All,
window,
cx,
@@ -20992,7 +20933,7 @@ impl Editor {
pub fn set_search_within_ranges(&mut self, ranges: &[Range<Anchor>], cx: &mut Context<Self>) {
self.highlight_background::<SearchWithinRange>(
ranges,
|_, colors| colors.colors().editor_document_highlight_read_background,
|colors| colors.colors().editor_document_highlight_read_background,
cx,
)
}
@@ -21008,12 +20949,12 @@ impl Editor {
pub fn highlight_background<T: 'static>(
&mut self,
ranges: &[Range<Anchor>],
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
color_fetcher: fn(&Theme) -> Hsla,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::Type(TypeId::of::<T>()),
(Arc::new(color_fetcher), Arc::from(ranges)),
(color_fetcher, Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
@@ -21023,12 +20964,12 @@ impl Editor {
&mut self,
key: usize,
ranges: &[Range<Anchor>],
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
color_fetcher: fn(&Theme) -> Hsla,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::TypePlus(TypeId::of::<T>(), key),
(Arc::new(color_fetcher), Arc::from(ranges)),
(color_fetcher, Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
@@ -21253,6 +21194,7 @@ impl Editor {
) -> Vec<(Range<DisplayPoint>, Hsla)> {
let mut results = Vec::new();
for (color_fetcher, ranges) in self.background_highlights.values() {
let color = color_fetcher(theme);
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe
.end
@@ -21265,7 +21207,7 @@ impl Editor {
}) {
Ok(i) | Err(i) => i,
};
for (index, range) in ranges[start_ix..].iter().enumerate() {
for range in &ranges[start_ix..] {
if range
.start
.cmp(&search_range.end, &display_snapshot.buffer_snapshot())
@@ -21274,7 +21216,6 @@ impl Editor {
break;
}
let color = color_fetcher(&(start_ix + index), theme);
let start = range.start.to_display_point(display_snapshot);
let end = range.end.to_display_point(display_snapshot);
results.push((start..end, color))
@@ -21994,64 +21935,43 @@ impl Editor {
};
for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer {
let buffer_read = buffer.read(cx);
let (has_file, is_project_file) = if let Some(file) = buffer_read.file() {
(true, project::File::from_dyn(Some(file)).is_some())
} else {
(false, false)
};
// If project file is none workspace.open_project_item will fail to open the excerpt
// in a pre existing workspace item if one exists, because Buffer entity_id will be None
// so we check if there's a tab match in that case first
let editor = (!has_file || !is_project_file)
let editor = buffer
.read(cx)
.file()
.is_none()
.then(|| {
// Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
// so `workspace.open_project_item` will never find them, always opening a new editor.
// Instead, we try to activate the existing editor in the pane first.
let (editor, pane_item_index, pane_item_id) =
let (editor, pane_item_index) =
pane.read(cx).items().enumerate().find_map(|(i, item)| {
let editor = item.downcast::<Editor>()?;
let singleton_buffer =
editor.read(cx).buffer().read(cx).as_singleton()?;
if singleton_buffer == buffer {
Some((editor, i, item.item_id()))
Some((editor, i))
} else {
None
}
})?;
pane.update(cx, |pane, cx| {
pane.activate_item(pane_item_index, true, true, window, cx);
if !PreviewTabsSettings::get_global(cx)
.enable_preview_from_multibuffer
{
pane.unpreview_item_if_preview(pane_item_id);
}
pane.activate_item(pane_item_index, true, true, window, cx)
});
Some(editor)
})
.flatten()
.unwrap_or_else(|| {
let keep_old_preview = PreviewTabsSettings::get_global(cx)
.enable_keep_preview_on_code_navigation;
let allow_new_preview =
PreviewTabsSettings::get_global(cx).enable_preview_from_multibuffer;
workspace.open_project_item::<Self>(
pane.clone(),
buffer,
true,
true,
keep_old_preview,
allow_new_preview,
window,
cx,
)
});
editor.update(cx, |editor, cx| {
if has_file && !is_project_file {
editor.set_read_only(true);
}
let autoscroll = match scroll_offset {
Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize),
None => Autoscroll::newest(),
@@ -22075,11 +21995,10 @@ impl Editor {
});
}
// Allow opening excerpts for buffers that either belong to the current project
// or represent synthetic/non-local files (e.g., git blobs). File-less buffers
// are also supported so tests and other in-memory views keep working.
// For now, don't allow opening excerpts in buffers that aren't backed by
// regular project files.
fn can_open_excerpts_in_file(file: Option<&Arc<dyn language::File>>) -> bool {
file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some() || !file.is_local())
file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some())
}
fn marked_text_ranges(&self, cx: &App) -> Option<Vec<Range<MultiBufferOffsetUtf16>>> {
@@ -22678,10 +22597,6 @@ impl Editor {
}
}
pub fn last_gutter_dimensions(&self) -> &GutterDimensions {
&self.gutter_dimensions
}
pub fn wait_for_diff_to_load(&self) -> Option<Shared<Task<()>>> {
self.load_diff_task.clone()
}
@@ -23571,6 +23486,7 @@ pub trait CompletionProvider {
position: language::Anchor,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool;
@@ -23949,6 +23865,7 @@ impl CompletionProvider for Entity<Project> {
position: language::Anchor,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let mut chars = text.chars();
@@ -23963,6 +23880,9 @@ impl CompletionProvider for Entity<Project> {
let buffer = buffer.read(cx);
let snapshot = buffer.snapshot();
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
return false;
}
let classifier = snapshot
.char_classifier_at(position)
.scope_context(Some(CharScopeContext::Completion));

View File

@@ -16978,7 +16978,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
anchor_range(Point::new(6, 3)..Point::new(6, 5)),
anchor_range(Point::new(8, 4)..Point::new(8, 6)),
],
|_, _| Hsla::red(),
|_| Hsla::red(),
cx,
);
editor.highlight_background::<Type2>(
@@ -16988,7 +16988,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
anchor_range(Point::new(7, 4)..Point::new(7, 7)),
anchor_range(Point::new(9, 5)..Point::new(9, 8)),
],
|_, _| Hsla::green(),
|_| Hsla::green(),
cx,
);
@@ -23973,7 +23973,7 @@ async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|_, theme| theme.colors().editor_document_highlight_read_background,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -24051,7 +24051,7 @@ async fn test_rename_without_prepare(cx: &mut TestAppContext) {
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|_, theme| theme.colors().editor_document_highlight_read_background,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -26589,7 +26589,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
}
});
let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
project.update(cx, |project, cx| {
let buffer_id = editor
.read(cx)
@@ -26602,7 +26602,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
let buffer_result_id = project
.lsp_store()
.read(cx)
.result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
.result_id(server_id, buffer_id, cx);
assert_eq!(expected, buffer_result_id);
});
};
@@ -26619,7 +26619,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
.next()
.await
.expect("should have sent the first diagnostics pull request");
ensure_result_id(Some(SharedString::new("1")), cx);
ensure_result_id(Some("1".to_string()), cx);
// Editing should trigger diagnostics
editor.update_in(cx, |editor, window, cx| {
@@ -26632,7 +26632,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
2,
"Editing should trigger diagnostic request"
);
ensure_result_id(Some(SharedString::new("2")), cx);
ensure_result_id(Some("2".to_string()), cx);
// Moving cursor should not trigger diagnostic request
editor.update_in(cx, |editor, window, cx| {
@@ -26647,7 +26647,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
2,
"Cursor movement should not trigger diagnostic request"
);
ensure_result_id(Some(SharedString::new("2")), cx);
ensure_result_id(Some("2".to_string()), cx);
// Multiple rapid edits should be debounced
for _ in 0..5 {
editor.update_in(cx, |editor, window, cx| {
@@ -26662,7 +26662,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
final_requests <= 4,
"Multiple rapid edits should be debounced (got {final_requests} requests)",
);
ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
ensure_result_id(Some(final_requests.to_string()), cx);
}
#[gpui::test]
@@ -27299,7 +27299,7 @@ let result = variable * 2;",
editor.highlight_background::<DocumentHighlightRead>(
&anchor_ranges,
|_, theme| theme.colors().editor_document_highlight_read_background,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});

View File

@@ -1227,13 +1227,7 @@ impl EditorElement {
editor.hide_blame_popover(false, cx);
}
} else {
let keyboard_grace = editor
.inline_blame_popover
.as_ref()
.is_some_and(|state| state.keyboard_grace);
if !keyboard_grace {
editor.hide_blame_popover(false, cx);
}
editor.hide_blame_popover(false, cx);
}
let breakpoint_indicator = if gutter_hovered {
@@ -2517,6 +2511,7 @@ impl EditorElement {
scroll_position: gpui::Point<ScrollOffset>,
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
line_height: Pixels,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut App,
) -> Option<InlineBlameLayout> {
@@ -2585,6 +2580,16 @@ impl EditorElement {
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let bounds = Bounds::new(absolute_offset, size);
self.layout_blame_entry_popover(
entry.clone(),
blame,
line_height,
text_hitbox,
row_info.buffer_id?,
window,
cx,
);
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
Some(InlineBlameLayout {
@@ -2595,48 +2600,16 @@ impl EditorElement {
})
}
fn layout_blame_popover(
fn layout_blame_entry_popover(
&self,
editor_snapshot: &EditorSnapshot,
text_hitbox: &Hitbox,
blame_entry: BlameEntry,
blame: Entity<GitBlame>,
line_height: Pixels,
text_hitbox: &Hitbox,
buffer: BufferId,
window: &mut Window,
cx: &mut App,
) {
if !self.editor.read(cx).inline_blame_popover.is_some() {
return;
}
let Some(blame) = self.editor.read(cx).blame.clone() else {
return;
};
let cursor_point = self
.editor
.read(cx)
.selections
.newest::<language::Point>(&editor_snapshot.display_snapshot)
.head();
let Some((buffer, buffer_point, _)) = editor_snapshot
.buffer_snapshot()
.point_to_buffer_point(cursor_point)
else {
return;
};
let row_info = RowInfo {
buffer_id: Some(buffer.remote_id()),
buffer_row: Some(buffer_point.row),
..Default::default()
};
let Some((buffer_id, blame_entry)) = blame
.update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
.flatten()
else {
return;
};
let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| {
editor
.inline_blame_popover
@@ -2658,7 +2631,7 @@ impl EditorElement {
popover_state.markdown,
workspace,
&blame,
buffer_id,
buffer,
window,
cx,
)
@@ -9240,18 +9213,34 @@ impl Element for EditorElement {
continue;
};
let background_color = match diff_status.kind {
DiffHunkStatusKind::Added =>
cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted =>
cx.theme().colors().version_control_deleted,
DiffHunkStatusKind::Modified => {
debug_panic!("modified diff status for row info");
continue;
}
};
let background_color = dbg!(match diff_status.kind {
DiffHunkStatusKind::Added =>
cx.theme().colors().version_control_added,
DiffHunkStatusKind::Deleted =>
cx.theme().colors().version_control_deleted,
DiffHunkStatusKind::Modified => {
debug_panic!("modified diff status for row info");
continue;
}
});
let hunk_opacity = if is_light { 0.16 } else { 0.12 };
// Potential fix with new tokens:
// diff.added = "#2EA048"
// diff.added_word = @diff.added/24%
// diff.added_background = @diff.added/20%
//
//
// Ideal State:
// version_control.added = "#2EA048"
// version_control.added_word = @version_control.added/24%
// version_control.added_background = @version_control.added/20%
// version_control.added_icon = "something"
// let hunk_opacity = if background_color.a < 1.0 {
// background_color.a // Use existing alpha
// } else {
// if is_light { 0.16 } else { 0.20 }
// };
let hollow_highlight = LineHighlight {
background: (background_color.opacity(if is_light {
@@ -9270,7 +9259,7 @@ impl Element for EditorElement {
};
let filled_highlight = LineHighlight {
background: solid_background(background_color.opacity(hunk_opacity)),
background: solid_background(background_color),
border: None,
include_gutter: true,
type_id: None,
@@ -9840,6 +9829,7 @@ impl Element for EditorElement {
scroll_position,
scroll_pixel_position,
line_height,
&text_hitbox,
window,
cx,
) {
@@ -10037,8 +10027,6 @@ impl Element for EditorElement {
window,
cx,
);
self.layout_blame_popover(&snapshot, &hitbox, line_height, window, cx);
}
let mouse_context_menu = self.layout_mouse_context_menu(

View File

@@ -518,7 +518,7 @@ fn show_hover(
// Highlight the selected symbol using a background highlight
editor.highlight_background::<HoverState>(
&hover_highlights,
|_, theme| theme.colors().element_hover, // todo update theme
|theme| theme.colors().element_hover, // todo update theme
cx,
);
}

View File

@@ -1487,7 +1487,6 @@ impl SearchableItem for Editor {
fn update_matches(
&mut self,
matches: &[Range<Anchor>],
active_match_index: Option<usize>,
_: &mut Window,
cx: &mut Context<Self>,
) {
@@ -1498,13 +1497,7 @@ impl SearchableItem for Editor {
let updated = existing_range != Some(matches);
self.highlight_background::<BufferSearchHighlights>(
matches,
move |index, theme| {
if active_match_index == Some(*index) {
theme.colors().search_active_match_background
} else {
theme.colors().search_match_background
}
},
|theme| theme.colors().search_match_background,
cx,
);
if updated {
@@ -1898,20 +1891,15 @@ fn path_for_buffer<'a>(
cx: &'a App,
) -> Option<Cow<'a, str>> {
let file = buffer.read(cx).as_singleton()?.read(cx).file()?;
path_for_file(file, height, include_filename, cx)
path_for_file(file.as_ref(), height, include_filename, cx)
}
fn path_for_file<'a>(
file: &'a Arc<dyn language::File>,
file: &'a dyn language::File,
mut height: usize,
include_filename: bool,
cx: &'a App,
) -> Option<Cow<'a, str>> {
if project::File::from_dyn(Some(file)).is_none() {
return None;
}
let file = file.as_ref();
// Ensure we always render at least the filename.
height += 1;
@@ -1958,11 +1946,11 @@ mod tests {
#[gpui::test]
fn test_path_for_file(cx: &mut App) {
let file: Arc<dyn language::File> = Arc::new(TestFile {
let file = TestFile {
path: RelPath::empty().into(),
root_name: String::new(),
local_root: None,
});
};
assert_eq!(path_for_file(&file, 0, false, cx), None);
}

View File

@@ -261,7 +261,7 @@ impl ExampleContext {
.expect("Unknown tool_name content in meta");
tool_uses_by_id.insert(
tool_call.tool_call_id,
tool_call.id,
ToolUse {
name: tool_name.to_string(),
value: tool_call.raw_input.unwrap_or_default(),
@@ -277,9 +277,7 @@ impl ExampleContext {
ThreadEvent::ToolCallUpdate(tool_call_update) => {
if let acp_thread::ToolCallUpdate::UpdateFields(update) = tool_call_update {
if let Some(raw_input) = update.fields.raw_input {
if let Some(tool_use) =
tool_uses_by_id.get_mut(&update.tool_call_id)
{
if let Some(tool_use) = tool_uses_by_id.get_mut(&update.id) {
tool_use.value = raw_input;
}
}
@@ -292,7 +290,7 @@ impl ExampleContext {
update.fields.status == Some(acp::ToolCallStatus::Completed);
let tool_use = tool_uses_by_id
.remove(&update.tool_call_id)
.remove(&update.id)
.expect("Unrecognized tool call completed");
let log_message = if succeeded {
@@ -339,7 +337,10 @@ impl ExampleContext {
acp::StopReason::MaxTurnRequests => {
return Err(anyhow!("Exceeded maximum turn requests"));
}
stop_reason => return Err(anyhow!("{stop_reason:?}")),
acp::StopReason::Refusal => {
return Err(anyhow!("Refusal"));
}
acp::StopReason::Cancelled => return Err(anyhow!("Cancelled")),
},
}
}

View File

@@ -303,12 +303,13 @@ impl ExampleInstance {
let context_server_registry = cx.new(|cx| ContextServerRegistry::new(project.read(cx).context_server_store(), cx));
let thread = if let Some(json) = &meta.existing_thread_json {
let session_id = acp::SessionId::new(
let session_id = acp::SessionId(
rand::rng()
.sample_iter(&distr::Alphanumeric)
.take(7)
.map(char::from)
.collect::<String>(),
.collect::<String>()
.into(),
);
let db_thread = agent::DbThread::from_json(json.as_bytes()).expect("Can't read serialized thread");
@@ -639,7 +640,7 @@ impl agent::ThreadEnvironment for EvalThreadEnvironment {
cx.spawn(async move |cx| {
let language_registry =
project.read_with(cx, |project, _cx| project.languages().clone())?;
let id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
let id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
let terminal =
acp_thread::create_terminal_entity(command, &[], vec![], cwd.clone(), &project, cx)
.await?;

View File

@@ -1,18 +0,0 @@
[package]
name = "eval_utils"
version = "0.1.0"
edition.workspace = true
publish.workspace = true
license = "GPL-3.0-or-later"
[lints]
workspace = true
[lib]
path = "src/eval_utils.rs"
doctest = false
[dependencies]
gpui.workspace = true
serde.workspace = true
smol.workspace = true

View File

@@ -1 +0,0 @@
LICENSE-GPL

View File

@@ -1,3 +0,0 @@
# eval_utils
Utilities for evals of agents.

View File

@@ -1,128 +0,0 @@
//! Utilities for evaluation and benchmarking.
use std::{
collections::HashMap,
sync::{Arc, mpsc},
};
fn report_progress(evaluated_count: usize, failed_count: usize, iterations: usize) {
let passed_count = evaluated_count - failed_count;
let passed_ratio = if evaluated_count == 0 {
0.0
} else {
passed_count as f64 / evaluated_count as f64
};
println!(
"\r\x1b[KEvaluated {}/{} ({:.2}% passed)",
evaluated_count,
iterations,
passed_ratio * 100.0
)
}
#[derive(Clone, Debug, Eq, PartialEq)]
pub enum OutcomeKind {
Passed,
Failed,
Error,
}
pub trait EvalOutputProcessor {
type Metadata: 'static + Send;
fn process(&mut self, output: &EvalOutput<Self::Metadata>);
fn assert(&mut self);
}
#[derive(Clone, Debug)]
pub struct EvalOutput<M> {
pub outcome: OutcomeKind,
pub data: String,
pub metadata: M,
}
pub struct NoProcessor;
impl EvalOutputProcessor for NoProcessor {
type Metadata = ();
fn process(&mut self, _output: &EvalOutput<Self::Metadata>) {}
fn assert(&mut self) {}
}
pub fn eval<P>(
iterations: usize,
expected_pass_ratio: f32,
mut processor: P,
evalf: impl Fn() -> EvalOutput<P::Metadata> + Send + Sync + 'static,
) where
P: EvalOutputProcessor,
{
let mut evaluated_count = 0;
let mut failed_count = 0;
let evalf = Arc::new(evalf);
report_progress(evaluated_count, failed_count, iterations);
let (tx, rx) = mpsc::channel();
let executor = gpui::background_executor();
let semaphore = Arc::new(smol::lock::Semaphore::new(32));
let evalf = Arc::new(evalf);
// Warm the cache once
let first_output = evalf();
tx.send(first_output).ok();
for _ in 1..iterations {
let tx = tx.clone();
let semaphore = semaphore.clone();
let evalf = evalf.clone();
executor
.spawn(async move {
let _guard = semaphore.acquire().await;
let output = evalf();
tx.send(output).ok();
})
.detach();
}
drop(tx);
let mut failed_evals = Vec::new();
let mut errored_evals = HashMap::new();
while let Ok(output) = rx.recv() {
processor.process(&output);
match output.outcome {
OutcomeKind::Passed => {}
OutcomeKind::Failed => {
failed_count += 1;
failed_evals.push(output);
}
OutcomeKind::Error => {
failed_count += 1;
*errored_evals.entry(output.data).or_insert(0) += 1;
}
}
evaluated_count += 1;
report_progress(evaluated_count, failed_count, iterations);
}
let actual_pass_ratio = (iterations - failed_count) as f32 / iterations as f32;
println!("Actual pass ratio: {}\n", actual_pass_ratio);
if actual_pass_ratio < expected_pass_ratio {
for (error, count) in errored_evals {
println!("Eval errored {} times. Error: {}", count, error);
}
for failed in failed_evals {
println!("Eval failed");
println!("{}", failed.data);
}
panic!(
"Actual pass ratio: {}\nExpected pass ratio: {}",
actual_pass_ratio, expected_pass_ratio
);
}
processor.assert();
}

View File

@@ -1,13 +1,12 @@
[package]
name = "zed_extension_api"
version = "0.8.0"
version = "0.7.0"
description = "APIs for creating Zed extensions in Rust"
repository = "https://github.com/zed-industries/zed"
documentation = "https://docs.rs/zed_extension_api"
keywords = ["zed", "extension"]
edition.workspace = true
# Change back to `true` when we're ready to publish v0.8.0.
publish = false
publish = true
license = "Apache-2.0"
[lints]

View File

@@ -334,7 +334,7 @@ mod wit {
wit_bindgen::generate!({
skip: ["init-extension"],
path: "./wit/since_v0.8.0",
path: "./wit/since_v0.6.0",
});
}

View File

@@ -1,12 +0,0 @@
interface common {
/// A (half-open) range (`[start, end)`).
record range {
/// The start of the range (inclusive).
start: u32,
/// The end of the range (exclusive).
end: u32,
}
/// A list of environment variables.
type env-vars = list<tuple<string, string>>;
}

View File

@@ -1,11 +0,0 @@
interface context-server {
/// Configuration for context server setup and installation.
record context-server-configuration {
/// Installation instructions in Markdown format.
installation-instructions: string,
/// JSON schema for settings validation.
settings-schema: string,
/// Default settings template.
default-settings: string,
}
}

View File

@@ -1,123 +0,0 @@
interface dap {
use common.{env-vars};
/// Resolves a specified TcpArgumentsTemplate into TcpArguments
resolve-tcp-template: func(template: tcp-arguments-template) -> result<tcp-arguments, string>;
record launch-request {
program: string,
cwd: option<string>,
args: list<string>,
envs: env-vars,
}
record attach-request {
process-id: option<u32>,
}
variant debug-request {
launch(launch-request),
attach(attach-request)
}
record tcp-arguments {
port: u16,
host: u32,
timeout: option<u64>,
}
record tcp-arguments-template {
port: option<u16>,
host: option<u32>,
timeout: option<u64>,
}
/// Debug Config is the "highest-level" configuration for a debug session.
/// It comes from a new process modal UI; thus, it is essentially debug-adapter-agnostic.
/// It is expected of the extension to translate this generic configuration into something that can be debugged by the adapter (debug scenario).
record debug-config {
/// Name of the debug task
label: string,
/// The debug adapter to use
adapter: string,
request: debug-request,
stop-on-entry: option<bool>,
}
record task-template {
/// Human readable name of the task to display in the UI.
label: string,
/// Executable command to spawn.
command: string,
args: list<string>,
env: env-vars,
cwd: option<string>,
}
/// A task template with substituted task variables.
type resolved-task = task-template;
/// A task template for building a debug target.
type build-task-template = task-template;
variant build-task-definition {
by-name(string),
template(build-task-definition-template-payload )
}
record build-task-definition-template-payload {
locator-name: option<string>,
template: build-task-template
}
/// Debug Scenario is the user-facing configuration type (used in debug.json). It is still concerned with what to debug and not necessarily how to do it (except for any
/// debug-adapter-specific configuration options).
record debug-scenario {
/// Unsubstituted label for the task.DebugAdapterBinary
label: string,
/// Name of the Debug Adapter this configuration is intended for.
adapter: string,
/// An optional build step to be ran prior to starting a debug session. Build steps are used by Zed's locators to locate the executable to debug.
build: option<build-task-definition>,
/// JSON-encoded configuration for a given debug adapter.
config: string,
/// TCP connection parameters (if they were specified by user)
tcp-connection: option<tcp-arguments-template>,
}
enum start-debugging-request-arguments-request {
launch,
attach,
}
record debug-task-definition {
/// Unsubstituted label for the task.DebugAdapterBinary
label: string,
/// Name of the Debug Adapter this configuration is intended for.
adapter: string,
/// JSON-encoded configuration for a given debug adapter.
config: string,
/// TCP connection parameters (if they were specified by user)
tcp-connection: option<tcp-arguments-template>,
}
record start-debugging-request-arguments {
/// JSON-encoded configuration for a given debug adapter. It is specific to each debug adapter.
/// `configuration` will have it's Zed variable references substituted prior to being passed to the debug adapter.
configuration: string,
request: start-debugging-request-arguments-request,
}
/// The lowest-level representation of a debug session, which specifies:
/// - How to start a debug adapter process
/// - How to start a debug session with it (using DAP protocol)
/// for a given debug scenario.
record debug-adapter-binary {
command: option<string>,
arguments: list<string>,
envs: env-vars,
cwd: option<string>,
/// Zed will use TCP transport if `connection` is specified.
connection: option<tcp-arguments>,
request-args: start-debugging-request-arguments
}
}

View File

@@ -1,167 +0,0 @@
package zed:extension;
world extension {
import context-server;
import dap;
import github;
import http-client;
import platform;
import process;
import nodejs;
use common.{env-vars, range};
use context-server.{context-server-configuration};
use dap.{attach-request, build-task-template, debug-config, debug-adapter-binary, debug-task-definition, debug-request, debug-scenario, launch-request, resolved-task, start-debugging-request-arguments-request};
use lsp.{completion, symbol};
use process.{command};
use slash-command.{slash-command, slash-command-argument-completion, slash-command-output};
/// Initializes the extension.
export init-extension: func();
/// The type of a downloaded file.
enum downloaded-file-type {
/// A gzipped file (`.gz`).
gzip,
/// A gzipped tar archive (`.tar.gz`).
gzip-tar,
/// A ZIP file (`.zip`).
zip,
/// An uncompressed file.
uncompressed,
}
/// The installation status for a language server.
variant language-server-installation-status {
/// The language server has no installation status.
none,
/// The language server is being downloaded.
downloading,
/// The language server is checking for updates.
checking-for-update,
/// The language server installation failed for specified reason.
failed(string),
}
record settings-location {
worktree-id: u64,
path: string,
}
import get-settings: func(path: option<settings-location>, category: string, key: option<string>) -> result<string, string>;
/// Downloads a file from the given URL and saves it to the given path within the extension's
/// working directory.
///
/// The file will be extracted according to the given file type.
import download-file: func(url: string, file-path: string, file-type: downloaded-file-type) -> result<_, string>;
/// Makes the file at the given path executable.
import make-file-executable: func(filepath: string) -> result<_, string>;
/// Updates the installation status for the given language server.
import set-language-server-installation-status: func(language-server-name: string, status: language-server-installation-status);
/// A Zed worktree.
resource worktree {
/// Returns the ID of the worktree.
id: func() -> u64;
/// Returns the root path of the worktree.
root-path: func() -> string;
/// Returns the textual contents of the specified file in the worktree.
read-text-file: func(path: string) -> result<string, string>;
/// Returns the path to the given binary name, if one is present on the `$PATH`.
which: func(binary-name: string) -> option<string>;
/// Returns the current shell environment.
shell-env: func() -> env-vars;
}
/// A Zed project.
resource project {
/// Returns the IDs of all of the worktrees in this project.
worktree-ids: func() -> list<u64>;
}
/// A key-value store.
resource key-value-store {
/// Inserts an entry under the specified key.
insert: func(key: string, value: string) -> result<_, string>;
}
/// Returns the command used to start up the language server.
export language-server-command: func(language-server-id: string, worktree: borrow<worktree>) -> result<command, string>;
/// Returns the initialization options to pass to the language server on startup.
///
/// The initialization options are represented as a JSON string.
export language-server-initialization-options: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the workspace configuration options to pass to the language server.
export language-server-workspace-configuration: func(language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the initialization options to pass to the other language server.
export language-server-additional-initialization-options: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// Returns the workspace configuration options to pass to the other language server.
export language-server-additional-workspace-configuration: func(language-server-id: string, target-language-server-id: string, worktree: borrow<worktree>) -> result<option<string>, string>;
/// A label containing some code.
record code-label {
/// The source code to parse with Tree-sitter.
code: string,
/// The spans to display in the label.
spans: list<code-label-span>,
/// The range of the displayed label to include when filtering.
filter-range: range,
}
/// A span within a code label.
variant code-label-span {
/// A range into the parsed code.
code-range(range),
/// A span containing a code literal.
literal(code-label-span-literal),
}
/// A span containing a code literal.
record code-label-span-literal {
/// The literal text.
text: string,
/// The name of the highlight to use for this literal.
highlight-name: option<string>,
}
export labels-for-completions: func(language-server-id: string, completions: list<completion>) -> result<list<option<code-label>>, string>;
export labels-for-symbols: func(language-server-id: string, symbols: list<symbol>) -> result<list<option<code-label>>, string>;
/// Returns the completions that should be shown when completing the provided slash command with the given query.
export complete-slash-command-argument: func(command: slash-command, args: list<string>) -> result<list<slash-command-argument-completion>, string>;
/// Returns the output from running the provided slash command.
export run-slash-command: func(command: slash-command, args: list<string>, worktree: option<borrow<worktree>>) -> result<slash-command-output, string>;
/// Returns the command used to start up a context server.
export context-server-command: func(context-server-id: string, project: borrow<project>) -> result<command, string>;
/// Returns the configuration for a context server.
export context-server-configuration: func(context-server-id: string, project: borrow<project>) -> result<option<context-server-configuration>, string>;
/// Returns a list of packages as suggestions to be included in the `/docs`
/// search results.
///
/// This can be used to provide completions for known packages (e.g., from the
/// local project or a registry) before a package has been indexed.
export suggest-docs-packages: func(provider-name: string) -> result<list<string>, string>;
/// Indexes the docs for the specified package.
export index-docs: func(provider-name: string, package-name: string, database: borrow<key-value-store>) -> result<_, string>;
/// Returns a configured debug adapter binary for a given debug task.
export get-dap-binary: func(adapter-name: string, config: debug-task-definition, user-installed-path: option<string>, worktree: borrow<worktree>) -> result<debug-adapter-binary, string>;
/// Returns the kind of a debug scenario (launch or attach).
export dap-request-kind: func(adapter-name: string, config: string) -> result<start-debugging-request-arguments-request, string>;
export dap-config-to-scenario: func(config: debug-config) -> result<debug-scenario, string>;
export dap-locator-create-scenario: func(locator-name: string, build-config-template: build-task-template, resolved-label: string, debug-adapter-name: string) -> option<debug-scenario>;
export run-dap-locator: func(locator-name: string, config: resolved-task) -> result<debug-request, string>;
}

View File

@@ -1,35 +0,0 @@
interface github {
/// A GitHub release.
record github-release {
/// The version of the release.
version: string,
/// The list of assets attached to the release.
assets: list<github-release-asset>,
}
/// An asset from a GitHub release.
record github-release-asset {
/// The name of the asset.
name: string,
/// The download URL for the asset.
download-url: string,
}
/// The options used to filter down GitHub releases.
record github-release-options {
/// Whether releases without assets should be included.
require-assets: bool,
/// Whether pre-releases should be included.
pre-release: bool,
}
/// Returns the latest release for the given GitHub repository.
///
/// Takes repo as a string in the form "<owner-name>/<repo-name>", for example: "zed-industries/zed".
latest-github-release: func(repo: string, options: github-release-options) -> result<github-release, string>;
/// Returns the GitHub release with the specified tag name for the given GitHub repository.
///
/// Returns an error if a release with the given tag name does not exist.
github-release-by-tag-name: func(repo: string, tag: string) -> result<github-release, string>;
}

View File

@@ -1,67 +0,0 @@
interface http-client {
/// An HTTP request.
record http-request {
/// The HTTP method for the request.
method: http-method,
/// The URL to which the request should be made.
url: string,
/// The headers for the request.
headers: list<tuple<string, string>>,
/// The request body.
body: option<list<u8>>,
/// The policy to use for redirects.
redirect-policy: redirect-policy,
}
/// HTTP methods.
enum http-method {
/// `GET`
get,
/// `HEAD`
head,
/// `POST`
post,
/// `PUT`
put,
/// `DELETE`
delete,
/// `OPTIONS`
options,
/// `PATCH`
patch,
}
/// The policy for dealing with redirects received from the server.
variant redirect-policy {
/// Redirects from the server will not be followed.
///
/// This is the default behavior.
no-follow,
/// Redirects from the server will be followed up to the specified limit.
follow-limit(u32),
/// All redirects from the server will be followed.
follow-all,
}
/// An HTTP response.
record http-response {
/// The response headers.
headers: list<tuple<string, string>>,
/// The response body.
body: list<u8>,
}
/// Performs an HTTP request and returns the response.
fetch: func(req: http-request) -> result<http-response, string>;
/// An HTTP response stream.
resource http-response-stream {
/// Retrieves the next chunk of data from the response stream.
///
/// Returns `Ok(None)` if the stream has ended.
next-chunk: func() -> result<option<list<u8>>, string>;
}
/// Performs an HTTP request and returns a response stream.
fetch-stream: func(req: http-request) -> result<http-response-stream, string>;
}

View File

@@ -1,90 +0,0 @@
interface lsp {
/// An LSP completion.
record completion {
label: string,
label-details: option<completion-label-details>,
detail: option<string>,
kind: option<completion-kind>,
insert-text-format: option<insert-text-format>,
}
/// The kind of an LSP completion.
variant completion-kind {
text,
method,
function,
%constructor,
field,
variable,
class,
%interface,
module,
property,
unit,
value,
%enum,
keyword,
snippet,
color,
file,
reference,
folder,
enum-member,
constant,
struct,
event,
operator,
type-parameter,
other(s32),
}
/// Label details for an LSP completion.
record completion-label-details {
detail: option<string>,
description: option<string>,
}
/// Defines how to interpret the insert text in a completion item.
variant insert-text-format {
plain-text,
snippet,
other(s32),
}
/// An LSP symbol.
record symbol {
kind: symbol-kind,
name: string,
}
/// The kind of an LSP symbol.
variant symbol-kind {
file,
module,
namespace,
%package,
class,
method,
property,
field,
%constructor,
%enum,
%interface,
function,
variable,
constant,
%string,
number,
boolean,
array,
object,
key,
null,
enum-member,
struct,
event,
operator,
type-parameter,
other(s32),
}
}

View File

@@ -1,13 +0,0 @@
interface nodejs {
/// Returns the path to the Node binary used by Zed.
node-binary-path: func() -> result<string, string>;
/// Returns the latest version of the given NPM package.
npm-package-latest-version: func(package-name: string) -> result<string, string>;
/// Returns the installed version of the given NPM package, if it exists.
npm-package-installed-version: func(package-name: string) -> result<option<string>, string>;
/// Installs the specified NPM package.
npm-install-package: func(package-name: string, version: string) -> result<_, string>;
}

View File

@@ -1,24 +0,0 @@
interface platform {
/// An operating system.
enum os {
/// macOS.
mac,
/// Linux.
linux,
/// Windows.
windows,
}
/// A platform architecture.
enum architecture {
/// AArch64 (e.g., Apple Silicon).
aarch64,
/// x86.
x86,
/// x86-64.
x8664,
}
/// Gets the current operating system and architecture.
current-platform: func() -> tuple<os, architecture>;
}

View File

@@ -1,29 +0,0 @@
interface process {
use common.{env-vars};
/// A command.
record command {
/// The command to execute.
command: string,
/// The arguments to pass to the command.
args: list<string>,
/// The environment variables to set for the command.
env: env-vars,
}
/// The output of a finished process.
record output {
/// The status (exit code) of the process.
///
/// On Unix, this will be `None` if the process was terminated by a signal.
status: option<s32>,
/// The data that the process wrote to stdout.
stdout: list<u8>,
/// The data that the process wrote to stderr.
stderr: list<u8>,
}
/// Executes the given command as a child process, waiting for it to finish
/// and collecting all of its output.
run-command: func(command: command) -> result<output, string>;
}

View File

@@ -1,40 +0,0 @@
use serde::{Deserialize, Serialize};
use std::{collections::HashMap, num::NonZeroU32};
/// The settings for a particular language.
#[derive(Debug, Serialize, Deserialize)]
pub struct LanguageSettings {
/// How many columns a tab should occupy.
pub tab_size: NonZeroU32,
}
/// The settings for a particular language server.
#[derive(Default, Debug, Serialize, Deserialize)]
pub struct LspSettings {
/// The settings for the language server binary.
pub binary: Option<CommandSettings>,
/// The initialization options to pass to the language server.
pub initialization_options: Option<serde_json::Value>,
/// The settings to pass to language server.
pub settings: Option<serde_json::Value>,
}
/// The settings for a particular context server.
#[derive(Default, Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct ContextServerSettings {
/// The settings for the context server binary.
pub command: Option<CommandSettings>,
/// The settings to pass to the context server.
pub settings: Option<serde_json::Value>,
}
/// The settings for a command.
#[derive(Debug, Serialize, Deserialize, PartialEq, Eq)]
pub struct CommandSettings {
/// The path to the command.
pub path: Option<String>,
/// The arguments to pass to the command.
pub arguments: Option<Vec<String>>,
/// The environment variables.
pub env: Option<HashMap<String, String>>,
}

View File

@@ -1,41 +0,0 @@
interface slash-command {
use common.{range};
/// A slash command for use in the Assistant.
record slash-command {
/// The name of the slash command.
name: string,
/// The description of the slash command.
description: string,
/// The tooltip text to display for the run button.
tooltip-text: string,
/// Whether this slash command requires an argument.
requires-argument: bool,
}
/// The output of a slash command.
record slash-command-output {
/// The text produced by the slash command.
text: string,
/// The list of sections to show in the slash command placeholder.
sections: list<slash-command-output-section>,
}
/// A section in the slash command output.
record slash-command-output-section {
/// The range this section occupies.
range: range,
/// The label to display in the placeholder for this section.
label: string,
}
/// A completion for a slash command argument.
record slash-command-argument-completion {
/// The label to display for this completion.
label: string,
/// The new text that should be inserted into the command when this completion is accepted.
new-text: string,
/// Whether the command should be run when accepting this completion.
run-command: bool,
}
}

View File

@@ -7,7 +7,6 @@ mod since_v0_3_0;
mod since_v0_4_0;
mod since_v0_5_0;
mod since_v0_6_0;
mod since_v0_8_0;
use dap::DebugRequest;
use extension::{DebugTaskDefinition, KeyValueStoreDelegate, WorktreeDelegate};
use gpui::BackgroundExecutor;
@@ -21,7 +20,7 @@ use crate::wasm_host::wit::since_v0_6_0::dap::StartDebuggingRequestArgumentsRequ
use super::{WasmState, wasm_engine};
use anyhow::{Context as _, Result, anyhow};
use semver::Version;
use since_v0_8_0 as latest;
use since_v0_6_0 as latest;
use std::{ops::RangeInclusive, path::PathBuf, sync::Arc};
use wasmtime::{
Store,
@@ -67,7 +66,7 @@ pub fn wasm_api_version_range(release_channel: ReleaseChannel) -> RangeInclusive
let max_version = match release_channel {
ReleaseChannel::Dev | ReleaseChannel::Nightly => latest::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => since_v0_6_0::MAX_VERSION,
ReleaseChannel::Stable | ReleaseChannel::Preview => latest::MAX_VERSION,
};
since_v0_0_1::MIN_VERSION..=max_version
@@ -96,7 +95,6 @@ pub fn authorize_access_to_unreleased_wasm_api_version(
}
pub enum Extension {
V0_8_0(since_v0_8_0::Extension),
V0_6_0(since_v0_6_0::Extension),
V0_5_0(since_v0_5_0::Extension),
V0_4_0(since_v0_4_0::Extension),
@@ -120,21 +118,10 @@ impl Extension {
let _ = release_channel;
if version >= latest::MIN_VERSION {
authorize_access_to_unreleased_wasm_api_version(release_channel)?;
let extension =
latest::Extension::instantiate_async(store, component, latest::linker(executor))
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V0_8_0(extension))
} else if version >= since_v0_6_0::MIN_VERSION {
let extension = since_v0_6_0::Extension::instantiate_async(
store,
component,
since_v0_6_0::linker(executor),
)
.await
.context("failed to instantiate wasm extension")?;
Ok(Self::V0_6_0(extension))
} else if version >= since_v0_5_0::MIN_VERSION {
let extension = since_v0_5_0::Extension::instantiate_async(
@@ -213,7 +200,6 @@ impl Extension {
pub async fn call_init_extension(&self, store: &mut Store<WasmState>) -> Result<()> {
match self {
Extension::V0_8_0(ext) => ext.call_init_extension(store).await,
Extension::V0_6_0(ext) => ext.call_init_extension(store).await,
Extension::V0_5_0(ext) => ext.call_init_extension(store).await,
Extension::V0_4_0(ext) => ext.call_init_extension(store).await,
@@ -234,10 +220,6 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Command, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
}
Extension::V0_6_0(ext) => {
ext.call_language_server_command(store, &language_server_id.0, resource)
.await
@@ -300,14 +282,6 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_language_server_initialization_options(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V0_6_0(ext) => {
ext.call_language_server_initialization_options(
store,
@@ -397,14 +371,6 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
&language_server_id.0,
resource,
)
.await
}
Extension::V0_6_0(ext) => {
ext.call_language_server_workspace_configuration(
store,
@@ -473,15 +439,6 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_language_server_additional_initialization_options(
store,
&language_server_id.0,
&target_language_server_id.0,
resource,
)
.await
}
Extension::V0_6_0(ext) => {
ext.call_language_server_additional_initialization_options(
store,
@@ -526,15 +483,6 @@ impl Extension {
resource: Resource<Arc<dyn WorktreeDelegate>>,
) -> Result<Result<Option<String>, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_language_server_additional_workspace_configuration(
store,
&language_server_id.0,
&target_language_server_id.0,
resource,
)
.await
}
Extension::V0_6_0(ext) => {
ext.call_language_server_additional_workspace_configuration(
store,
@@ -578,23 +526,10 @@ impl Extension {
completions: Vec<latest::Completion>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V0_8_0(ext) => {
Extension::V0_6_0(ext) => {
ext.call_labels_for_completions(store, &language_server_id.0, &completions)
.await
}
Extension::V0_6_0(ext) => Ok(ext
.call_labels_for_completions(
store,
&language_server_id.0,
&completions.into_iter().collect::<Vec<_>>(),
)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V0_5_0(ext) => Ok(ext
.call_labels_for_completions(
store,
@@ -684,23 +619,10 @@ impl Extension {
symbols: Vec<latest::Symbol>,
) -> Result<Result<Vec<Option<CodeLabel>>, String>> {
match self {
Extension::V0_8_0(ext) => {
Extension::V0_6_0(ext) => {
ext.call_labels_for_symbols(store, &language_server_id.0, &symbols)
.await
}
Extension::V0_6_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
&language_server_id.0,
&symbols.into_iter().collect::<Vec<_>>(),
)
.await?
.map(|labels| {
labels
.into_iter()
.map(|label| label.map(Into::into))
.collect()
})),
Extension::V0_5_0(ext) => Ok(ext
.call_labels_for_symbols(
store,
@@ -790,10 +712,6 @@ impl Extension {
arguments: &[String],
) -> Result<Result<Vec<SlashCommandArgumentCompletion>, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
}
Extension::V0_6_0(ext) => {
ext.call_complete_slash_command_argument(store, command, arguments)
.await
@@ -832,10 +750,6 @@ impl Extension {
resource: Option<Resource<Arc<dyn WorktreeDelegate>>>,
) -> Result<Result<SlashCommandOutput, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
}
Extension::V0_6_0(ext) => {
ext.call_run_slash_command(store, command, arguments, resource)
.await
@@ -873,10 +787,6 @@ impl Extension {
project: Resource<ExtensionProject>,
) -> Result<Result<Command, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_context_server_command(store, &context_server_id, project)
.await
}
Extension::V0_6_0(ext) => {
ext.call_context_server_command(store, &context_server_id, project)
.await
@@ -913,10 +823,6 @@ impl Extension {
project: Resource<ExtensionProject>,
) -> Result<Result<Option<ContextServerConfiguration>, String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_context_server_configuration(store, &context_server_id, project)
.await
}
Extension::V0_6_0(ext) => {
ext.call_context_server_configuration(store, &context_server_id, project)
.await
@@ -943,7 +849,6 @@ impl Extension {
provider: &str,
) -> Result<Result<Vec<String>, String>> {
match self {
Extension::V0_8_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_6_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_5_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
Extension::V0_4_0(ext) => ext.call_suggest_docs_packages(store, provider).await,
@@ -964,10 +869,6 @@ impl Extension {
kv_store: Resource<Arc<dyn KeyValueStoreDelegate>>,
) -> Result<Result<(), String>> {
match self {
Extension::V0_8_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
}
Extension::V0_6_0(ext) => {
ext.call_index_docs(store, provider, package_name, kv_store)
.await
@@ -997,7 +898,6 @@ impl Extension {
}
}
}
pub async fn call_get_dap_binary(
&self,
store: &mut Store<WasmState>,
@@ -1024,7 +924,6 @@ impl Extension {
_ => anyhow::bail!("`get_dap_binary` not available prior to v0.6.0"),
}
}
pub async fn call_dap_request_kind(
&self,
store: &mut Store<WasmState>,
@@ -1045,7 +944,6 @@ impl Extension {
_ => anyhow::bail!("`dap_request_kind` not available prior to v0.6.0"),
}
}
pub async fn call_dap_config_to_scenario(
&self,
store: &mut Store<WasmState>,
@@ -1064,7 +962,6 @@ impl Extension {
_ => anyhow::bail!("`dap_config_to_scenario` not available prior to v0.6.0"),
}
}
pub async fn call_dap_locator_create_scenario(
&self,
store: &mut Store<WasmState>,
@@ -1091,7 +988,6 @@ impl Extension {
_ => anyhow::bail!("`dap_locator_create_scenario` not available prior to v0.6.0"),
}
}
pub async fn call_run_dap_locator(
&self,
store: &mut Store<WasmState>,

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@@ -229,10 +229,8 @@ enum Feature {
AgentClaude,
AgentCodex,
AgentGemini,
ExtensionBasedpyright,
ExtensionRuff,
ExtensionTailwind,
ExtensionTy,
Git,
LanguageBash,
LanguageC,
@@ -253,13 +251,8 @@ fn keywords_by_feature() -> &'static BTreeMap<Feature, Vec<&'static str>> {
(Feature::AgentClaude, vec!["claude", "claude code"]),
(Feature::AgentCodex, vec!["codex", "codex cli"]),
(Feature::AgentGemini, vec!["gemini", "gemini cli"]),
(
Feature::ExtensionBasedpyright,
vec!["basedpyright", "pyright"],
),
(Feature::ExtensionRuff, vec!["ruff"]),
(Feature::ExtensionTailwind, vec!["tail", "tailwind"]),
(Feature::ExtensionTy, vec!["ty"]),
(Feature::Git, vec!["git"]),
(Feature::LanguageBash, vec!["sh", "bash"]),
(Feature::LanguageC, vec!["c", "clang"]),
@@ -739,7 +732,7 @@ impl ExtensionsPage {
extension: &ExtensionMetadata,
cx: &mut Context<Self>,
) -> ExtensionCard {
let this = cx.weak_entity();
let this = cx.entity();
let status = Self::extension_status(&extension.id, cx);
let has_dev_extension = Self::dev_extension_exists(&extension.id, cx);
@@ -889,15 +882,13 @@ impl ExtensionsPage {
y: px(2.0),
})
.menu(move |window, cx| {
this.upgrade().map(|this| {
Self::render_remote_extension_context_menu(
&this,
extension_id.clone(),
authors.clone(),
window,
cx,
)
})
Some(Self::render_remote_extension_context_menu(
&this,
extension_id.clone(),
authors.clone(),
window,
cx,
))
}),
),
),
@@ -1373,23 +1364,6 @@ impl ExtensionsPage {
return;
};
if let Some(id) = search.strip_prefix("id:") {
self.upsells.clear();
let upsell = match id.to_lowercase().as_str() {
"ruff" => Some(Feature::ExtensionRuff),
"basedpyright" => Some(Feature::ExtensionBasedpyright),
"ty" => Some(Feature::ExtensionTy),
_ => None,
};
if let Some(upsell) = upsell {
self.upsells.insert(upsell);
}
return;
}
let search = search.to_lowercase();
let search_terms = search
.split_whitespace()
@@ -1508,12 +1482,6 @@ impl ExtensionsPage {
false,
cx,
),
Feature::ExtensionBasedpyright => self.render_feature_upsell_banner(
"Basedpyright (Python language server) support is built-in to Zed!".into(),
"https://zed.dev/docs/languages/python#basedpyright".into(),
false,
cx,
),
Feature::ExtensionRuff => self.render_feature_upsell_banner(
"Ruff (linter for Python) support is built-in to Zed!".into(),
"https://zed.dev/docs/languages/python#code-formatting--linting".into(),
@@ -1526,12 +1494,6 @@ impl ExtensionsPage {
false,
cx,
),
Feature::ExtensionTy => self.render_feature_upsell_banner(
"Ty (Python language server) support is built-in to Zed!".into(),
"https://zed.dev/docs/languages/python".into(),
false,
cx,
),
Feature::Git => self.render_feature_upsell_banner(
"Zed comes with basic Git support—more features are coming in the future."
.into(),

View File

@@ -152,8 +152,8 @@ impl GitRepository for FakeGitRepository {
})
}
fn remote_url(&self, _name: &str) -> BoxFuture<'_, Option<String>> {
async move { None }.boxed()
fn remote_url(&self, _name: &str) -> Option<String> {
None
}
fn diff_tree(&self, _request: DiffTreeType) -> BoxFuture<'_, Result<TreeDiff>> {

View File

@@ -420,7 +420,7 @@ pub trait GitRepository: Send + Sync {
) -> BoxFuture<'_, anyhow::Result<()>>;
/// Returns the URL of the remote with the given name.
fn remote_url(&self, name: &str) -> BoxFuture<'_, Option<String>>;
fn remote_url(&self, name: &str) -> Option<String>;
/// Resolve a list of refs to SHAs.
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>>;
@@ -967,15 +967,7 @@ impl GitRepository for RealGitRepository {
index.read(false)?;
const STAGE_NORMAL: i32 = 0;
let path = path.as_std_path();
// `RepoPath` contains a `RelPath` which normalizes `.` into an empty path
// `get_path` unwraps on empty paths though, so undo that normalization here
let path = if path.components().next().is_none() {
".".as_ref()
} else {
path
};
let oid = match index.get_path(path, STAGE_NORMAL) {
let oid = match index.get_path(path.as_std_path(), STAGE_NORMAL) {
Some(entry) if entry.mode != GIT_MODE_SYMLINK => entry.id,
_ => return Ok(None),
};
@@ -1085,16 +1077,10 @@ impl GitRepository for RealGitRepository {
.boxed()
}
fn remote_url(&self, name: &str) -> BoxFuture<'_, Option<String>> {
let repo = self.repository.clone();
let name = name.to_owned();
self.executor
.spawn(async move {
let repo = repo.lock();
let remote = repo.find_remote(&name).ok()?;
remote.url().map(|url| url.to_string())
})
.boxed()
fn remote_url(&self, name: &str) -> Option<String> {
let repo = self.repository.lock();
let remote = repo.find_remote(name).ok()?;
remote.url().map(|url| url.to_string())
}
fn revparse_batch(&self, revs: Vec<String>) -> BoxFuture<'_, Result<Vec<Option<String>>>> {
@@ -1471,30 +1457,23 @@ impl GitRepository for RealGitRepository {
fn blame(&self, path: RepoPath, content: Rope) -> BoxFuture<'_, Result<crate::blame::Blame>> {
let working_directory = self.working_directory();
let git_binary_path = self.any_git_binary_path.clone();
let executor = self.executor.clone();
async move {
let remote_url = if let Some(remote_url) = self.remote_url("upstream").await {
Some(remote_url)
} else if let Some(remote_url) = self.remote_url("origin").await {
Some(remote_url)
} else {
None
};
executor
.spawn(async move {
crate::blame::Blame::for_path(
&git_binary_path,
&working_directory?,
&path,
&content,
remote_url,
)
.await
})
let remote_url = self
.remote_url("upstream")
.or_else(|| self.remote_url("origin"));
self.executor
.spawn(async move {
crate::blame::Blame::for_path(
&git_binary_path,
&working_directory?,
&path,
&content,
remote_url,
)
.await
}
.boxed()
})
.boxed()
}
fn file_history(&self, path: RepoPath) -> BoxFuture<'_, Result<FileHistory>> {

View File

@@ -33,11 +33,11 @@ pub fn init(cx: &mut App) {
///
/// These require information from the Git repository to construct, so their
/// registration is deferred until we have a Git repository initialized.
pub async fn register_additional_providers(
pub fn register_additional_providers(
provider_registry: Arc<GitHostingProviderRegistry>,
repository: Arc<dyn GitRepository>,
) {
let Some(origin_url) = repository.remote_url("origin").await else {
let Some(origin_url) = repository.remote_url("origin") else {
return;
};

View File

@@ -198,6 +198,9 @@ impl BlameRenderer for GitBlameRenderer {
let link_color = cx.theme().colors().text_accent;
let markdown_style = {
let mut style = hover_markdown_style(window, cx);
if let Some(code_block) = &style.code_block.text {
style.base_text_style.refine(code_block);
}
style.link.refine(&TextStyleRefinement {
color: Some(link_color),
underline: Some(UnderlineStyle {

View File

@@ -197,7 +197,10 @@ impl Render for CommitTooltip {
time_format::TimestampFormat::MediumAbsolute,
);
let markdown_style = {
let style = hover_markdown_style(window, cx);
let mut style = hover_markdown_style(window, cx);
if let Some(code_block) = &style.code_block.text {
style.base_text_style.refine(code_block);
}
style
};

View File

@@ -1,18 +1,19 @@
use anyhow::{Context as _, Result};
use buffer_diff::{BufferDiff, BufferDiffSnapshot};
use editor::display_map::{BlockPlacement, BlockProperties, BlockStyle};
use editor::{Addon, Editor, EditorEvent, ExcerptId, ExcerptRange, MultiBuffer};
use editor::{Addon, Editor, EditorEvent, MultiBuffer};
use git::repository::{CommitDetails, CommitDiff, RepoPath};
use git::{GitHostingProviderRegistry, GitRemote, parse_git_remote_url};
use gpui::{
AnyElement, App, AppContext as _, Asset, AsyncApp, AsyncWindowContext, Context, Element,
Entity, EventEmitter, FocusHandle, Focusable, InteractiveElement, IntoElement, ParentElement,
PromptLevel, Render, Styled, Task, WeakEntity, Window, actions,
PromptLevel, Render, Styled, Task, TextStyleRefinement, UnderlineStyle, WeakEntity, Window,
actions, px,
};
use language::{
Anchor, Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope,
TextBuffer, ToPoint,
Buffer, Capability, DiskState, File, LanguageRegistry, LineEnding, ReplicaId, Rope, TextBuffer,
ToPoint,
};
use markdown::{Markdown, MarkdownElement, MarkdownStyle};
use multi_buffer::ExcerptInfo;
use multi_buffer::PathKey;
use project::{Project, WorktreeId, git_store::Repository};
@@ -62,13 +63,13 @@ pub struct CommitView {
multibuffer: Entity<MultiBuffer>,
repository: Entity<Repository>,
remote: Option<GitRemote>,
markdown: Entity<Markdown>,
}
struct GitBlob {
path: RepoPath,
worktree_id: WorktreeId,
is_deleted: bool,
display_name: Arc<str>,
}
const FILE_NAMESPACE_SORT_PREFIX: u64 = 1;
@@ -158,7 +159,6 @@ impl CommitView {
});
editor
});
let commit_sha = Arc::<str>::from(commit.sha.as_ref());
let first_worktree_id = project
.read(cx)
@@ -167,8 +167,6 @@ impl CommitView {
.map(|worktree| worktree.read(cx).id());
let repository_clone = repository.clone();
let commit_message = commit.message.clone();
cx.spawn(async move |this, cx| {
for file in commit_diff.files {
let is_deleted = file.new_text.is_none();
@@ -182,20 +180,10 @@ impl CommitView {
.or(first_worktree_id)
})?
.context("project has no worktrees")?;
let short_sha = commit_sha.get(0..7).unwrap_or(&commit_sha);
let file_name = file
.path
.file_name()
.map(|name| name.to_string())
.unwrap_or_else(|| file.path.display(PathStyle::Posix).to_string());
let display_name: Arc<str> =
Arc::from(format!("{short_sha} - {file_name}").into_boxed_str());
let file = Arc::new(GitBlob {
path: file.path.clone(),
is_deleted,
worktree_id,
display_name,
}) as Arc<dyn language::File>;
let buffer = build_buffer(new_text, file, &language_registry, cx).await?;
@@ -239,58 +227,6 @@ impl CommitView {
});
})?;
}
let message_buffer = cx.new(|cx| {
let mut buffer = Buffer::local(commit_message, cx);
buffer.set_capability(Capability::ReadOnly, cx);
buffer
})?;
this.update(cx, |this, cx| {
this.multibuffer.update(cx, |multibuffer, cx| {
let range = ExcerptRange {
context: Anchor::MIN..Anchor::MAX,
primary: Anchor::MIN..Anchor::MAX,
};
multibuffer.insert_excerpts_after(
ExcerptId::min(),
message_buffer.clone(),
[range],
cx,
)
});
this.editor.update(cx, |editor, cx| {
editor.disable_header_for_buffer(message_buffer.read(cx).remote_id(), cx);
editor.insert_blocks(
[BlockProperties {
placement: BlockPlacement::Above(editor::Anchor::min()),
height: Some(1),
style: BlockStyle::Sticky,
render: Arc::new(|_| gpui::Empty.into_any_element()),
priority: 0,
}]
.into_iter()
.chain(
editor
.buffer()
.read(cx)
.buffer_anchor_to_anchor(&message_buffer, Anchor::MAX, cx)
.map(|anchor| BlockProperties {
placement: BlockPlacement::Below(anchor),
height: Some(1),
style: BlockStyle::Sticky,
render: Arc::new(|_| gpui::Empty.into_any_element()),
priority: 0,
}),
),
None,
cx,
)
});
})?;
anyhow::Ok(())
})
.detach();
@@ -310,6 +246,14 @@ impl CommitView {
})
});
let processed_message = if let Some(ref remote) = remote {
Self::process_github_issues(&commit.message, remote)
} else {
commit.message.to_string()
};
let markdown = cx.new(|cx| Markdown::new(processed_message.into(), None, None, cx));
Self {
commit,
editor,
@@ -317,9 +261,18 @@ impl CommitView {
stash,
repository,
remote,
markdown,
}
}
fn fallback_commit_avatar() -> AnyElement {
Icon::new(IconName::Person)
.color(Color::Muted)
.size(IconSize::Medium)
.into_element()
.into_any()
}
fn render_commit_avatar(
&self,
sha: &SharedString,
@@ -327,34 +280,21 @@ impl CommitView {
window: &mut Window,
cx: &mut App,
) -> AnyElement {
let size = size.into();
let remote = self.remote.as_ref().filter(|r| r.host_supports_avatars());
if let Some(remote) = remote {
let avatar_asset = CommitAvatarAsset::new(remote.clone(), sha.clone());
if let Some(Some(url)) = window.use_asset::<CommitAvatarAsset>(&avatar_asset, cx) {
return Avatar::new(url.to_string())
Avatar::new(url.to_string())
.size(size)
.into_element()
.into_any();
.into_any()
} else {
Self::fallback_commit_avatar()
}
} else {
Self::fallback_commit_avatar()
}
v_flex()
.w(size)
.h(size)
.border_1()
.border_color(cx.theme().colors().border)
.rounded_full()
.justify_center()
.items_center()
.child(
Icon::new(IconName::Person)
.color(Color::Muted)
.size(IconSize::Medium)
.into_element(),
)
.into_any()
}
fn render_header(&self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
@@ -382,24 +322,14 @@ impl CommitView {
v_flex()
.p_4()
.pl_0()
.gap_4()
.border_b_1()
.border_color(cx.theme().colors().border)
.child(
h_flex()
.items_start()
.child(
h_flex()
.w(self.editor.read(cx).last_gutter_dimensions().full_width())
.justify_center()
.child(self.render_commit_avatar(
&commit.sha,
gpui::rems(3.0),
window,
cx,
)),
)
.gap_3()
.child(self.render_commit_avatar(&commit.sha, gpui::rems(3.0), window, cx))
.child(
v_flex()
.gap_1()
@@ -423,6 +353,66 @@ impl CommitView {
.on_click(move |_, _, cx| cx.open_url(&url))
})),
)
.child(self.render_commit_message(window, cx))
}
fn process_github_issues(message: &str, remote: &GitRemote) -> String {
let mut result = String::new();
let chars: Vec<char> = message.chars().collect();
let mut i = 0;
while i < chars.len() {
if chars[i] == '#' && i + 1 < chars.len() && chars[i + 1].is_ascii_digit() {
let mut j = i + 1;
while j < chars.len() && chars[j].is_ascii_digit() {
j += 1;
}
let issue_number = &message[i + 1..i + (j - i)];
let url = format!(
"{}/{}/{}/issues/{}",
remote.host.base_url().as_str().trim_end_matches('/'),
remote.owner,
remote.repo,
issue_number
);
result.push_str(&format!("[#{}]({})", issue_number, url));
i = j;
} else if i + 3 < chars.len()
&& chars[i] == 'G'
&& chars[i + 1] == 'H'
&& chars[i + 2] == '-'
&& chars[i + 3].is_ascii_digit()
{
let mut j = i + 3;
while j < chars.len() && chars[j].is_ascii_digit() {
j += 1;
}
let issue_number = &message[i + 3..i + (j - i)];
let url = format!(
"{}/{}/{}/issues/{}",
remote.host.base_url().as_str().trim_end_matches('/'),
remote.owner,
remote.repo,
issue_number
);
result.push_str(&format!("[GH-{}]({})", issue_number, url));
i = j;
} else {
result.push(chars[i]);
i += 1;
}
}
result
}
fn render_commit_message(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> impl IntoElement {
let style = hover_markdown_style(window, cx);
MarkdownElement::new(self.markdown.clone(), style)
}
fn apply_stash(workspace: &mut Workspace, window: &mut Window, cx: &mut App) {
@@ -659,7 +649,7 @@ impl language::File for GitBlob {
}
fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
self.display_name.as_ref()
self.path.file_name().unwrap()
}
fn worktree_id(&self, _: &App) -> WorktreeId {
@@ -973,6 +963,12 @@ impl Item for CommitView {
.update(cx, |editor, cx| editor.clone(window, cx))
});
let multibuffer = editor.read(cx).buffer().clone();
let processed_message = if let Some(ref remote) = self.remote {
Self::process_github_issues(&self.commit.message, remote)
} else {
self.commit.message.to_string()
};
let markdown = cx.new(|cx| Markdown::new(processed_message.into(), None, None, cx));
Self {
editor,
multibuffer,
@@ -980,6 +976,7 @@ impl Item for CommitView {
stash: self.stash,
repository: self.repository.clone(),
remote: self.remote.clone(),
markdown,
}
})))
}
@@ -1049,3 +1046,117 @@ fn stash_matches_index(sha: &str, stash_index: usize, repo: &Repository) -> bool
.map(|entry| entry.oid.to_string() == sha)
.unwrap_or(false)
}
fn hover_markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
let colors = cx.theme().colors();
let mut style = MarkdownStyle::default();
style.base_text_style = window.text_style();
style.syntax = cx.theme().syntax().clone();
style.selection_background_color = colors.element_selection_background;
style.link = TextStyleRefinement {
color: Some(colors.text_accent),
underline: Some(UnderlineStyle {
thickness: px(1.0),
color: Some(colors.text_accent),
wavy: false,
}),
..Default::default()
};
style
}
#[cfg(test)]
mod tests {
use super::*;
use git_hosting_providers::Github;
fn create_test_remote() -> GitRemote {
GitRemote {
host: Arc::new(Github::public_instance()),
owner: "zed-industries".into(),
repo: "zed".into(),
}
}
#[test]
fn test_process_github_issues_simple_issue_number() {
let remote = create_test_remote();
let message = "Fix bug #123";
let result = CommitView::process_github_issues(message, &remote);
assert_eq!(
result,
"Fix bug [#123](https://github.com/zed-industries/zed/issues/123)"
);
}
#[test]
fn test_process_github_issues_multiple_issue_numbers() {
let remote = create_test_remote();
let message = "Fix #123 and #456";
let result = CommitView::process_github_issues(message, &remote);
assert_eq!(
result,
"Fix [#123](https://github.com/zed-industries/zed/issues/123) and [#456](https://github.com/zed-industries/zed/issues/456)"
);
}
#[test]
fn test_process_github_issues_gh_format() {
let remote = create_test_remote();
let message = "Fix GH-789";
let result = CommitView::process_github_issues(message, &remote);
assert_eq!(
result,
"Fix [GH-789](https://github.com/zed-industries/zed/issues/789)"
);
}
#[test]
fn test_process_github_issues_mixed_formats() {
let remote = create_test_remote();
let message = "Fix #123 and GH-456";
let result = CommitView::process_github_issues(message, &remote);
assert_eq!(
result,
"Fix [#123](https://github.com/zed-industries/zed/issues/123) and [GH-456](https://github.com/zed-industries/zed/issues/456)"
);
}
#[test]
fn test_process_github_issues_no_issues() {
let remote = create_test_remote();
let message = "This is a commit message without any issues";
let result = CommitView::process_github_issues(message, &remote);
assert_eq!(result, message);
}
#[test]
fn test_process_github_issues_hash_without_number() {
let remote = create_test_remote();
let message = "Use # for comments";
let result = CommitView::process_github_issues(message, &remote);
assert_eq!(result, message);
}
#[test]
fn test_process_github_issues_consecutive_issues() {
let remote = create_test_remote();
let message = "#123#456";
let result = CommitView::process_github_issues(message, &remote);
assert_eq!(
result,
"[#123](https://github.com/zed-industries/zed/issues/123)[#456](https://github.com/zed-industries/zed/issues/456)"
);
}
#[test]
fn test_process_github_issues_multiline() {
let remote = create_test_remote();
let message = "Fix #123\n\nThis also fixes #456";
let result = CommitView::process_github_issues(message, &remote);
assert_eq!(
result,
"Fix [#123](https://github.com/zed-industries/zed/issues/123)\n\nThis also fixes [#456](https://github.com/zed-industries/zed/issues/456)"
);
}
}

View File

@@ -6,8 +6,7 @@ use crate::project_diff::{self, Diff, ProjectDiff};
use crate::remote_output::{self, RemoteAction, SuccessMessage};
use crate::{branch_picker, picker_prompt, render_remote_button};
use crate::{
file_history_view::FileHistoryView, git_panel_settings::GitPanelSettings, git_status_icon,
repository_selector::RepositorySelector,
git_panel_settings::GitPanelSettings, git_status_icon, repository_selector::RepositorySelector,
};
use agent_settings::AgentSettings;
use anyhow::Context as _;
@@ -843,26 +842,6 @@ impl GitPanel {
});
}
fn file_history(&mut self, _: &git::FileHistory, window: &mut Window, cx: &mut Context<Self>) {
maybe!({
let entry = self.entries.get(self.selected_entry?)?.status_entry()?;
let active_repo = self.active_repository.as_ref()?;
let repo_path = entry.repo_path.clone();
let git_store = self.project.read(cx).git_store();
FileHistoryView::open(
repo_path,
git_store.downgrade(),
active_repo.downgrade(),
self.workspace.clone(),
window,
cx,
);
Some(())
});
}
fn open_file(
&mut self,
_: &menu::SecondaryConfirm,
@@ -4004,21 +3983,20 @@ impl GitPanel {
"Restore File"
};
let context_menu = ContextMenu::build(window, cx, |context_menu, _, _| {
let is_created = entry.status.is_created();
context_menu
let mut context_menu = context_menu
.context(self.focus_handle.clone())
.action(stage_title, ToggleStaged.boxed_clone())
.action(restore_title, git::RestoreFile::default().boxed_clone())
.action_disabled_when(
!is_created,
"Add to .gitignore",
git::AddToGitignore.boxed_clone(),
)
.action(restore_title, git::RestoreFile::default().boxed_clone());
if entry.status.is_created() {
context_menu =
context_menu.action("Add to .gitignore", git::AddToGitignore.boxed_clone());
}
context_menu
.separator()
.action("Open Diff", Confirm.boxed_clone())
.action("Open File", SecondaryConfirm.boxed_clone())
.separator()
.action_disabled_when(is_created, "File History", Box::new(git::FileHistory))
});
self.selected_entry = Some(ix);
self.set_context_menu(context_menu, position, window, cx);
@@ -4521,7 +4499,6 @@ impl Render for GitPanel {
.on_action(cx.listener(Self::close_panel))
.on_action(cx.listener(Self::open_diff))
.on_action(cx.listener(Self::open_file))
.on_action(cx.listener(Self::file_history))
.on_action(cx.listener(Self::focus_changes_list))
.on_action(cx.listener(Self::focus_editor))
.on_action(cx.listener(Self::expand_commit_editor))

View File

@@ -551,39 +551,12 @@ impl SystemWindowTabController {
}
}
pub(crate) enum GpuiMode {
#[cfg(any(test, feature = "test-support"))]
Test {
skip_drawing: bool,
},
Production,
}
impl GpuiMode {
#[cfg(any(test, feature = "test-support"))]
pub fn test() -> Self {
GpuiMode::Test {
skip_drawing: false,
}
}
#[inline]
pub(crate) fn skip_drawing(&self) -> bool {
match self {
#[cfg(any(test, feature = "test-support"))]
GpuiMode::Test { skip_drawing } => *skip_drawing,
GpuiMode::Production => false,
}
}
}
/// Contains the state of the full application, and passed as a reference to a variety of callbacks.
/// Other [Context] derefs to this type.
/// You need a reference to an `App` to access the state of a [Entity].
pub struct App {
pub(crate) this: Weak<AppCell>,
pub(crate) platform: Rc<dyn Platform>,
pub(crate) mode: GpuiMode,
text_system: Arc<TextSystem>,
flushing_effects: bool,
pending_updates: usize,
@@ -662,7 +635,6 @@ impl App {
this: this.clone(),
platform: platform.clone(),
text_system,
mode: GpuiMode::Production,
actions: Rc::new(ActionRegistry::default()),
flushing_effects: false,
pending_updates: 0,

View File

@@ -5,7 +5,7 @@ use crate::{
ModifiersChangedEvent, MouseButton, MouseDownEvent, MouseMoveEvent, MouseUpEvent, Pixels,
Platform, Point, Render, Result, Size, Task, TestDispatcher, TestPlatform,
TestScreenCaptureSource, TestWindow, TextSystem, VisualContext, Window, WindowBounds,
WindowHandle, WindowOptions, app::GpuiMode,
WindowHandle, WindowOptions,
};
use anyhow::{anyhow, bail};
use futures::{Stream, StreamExt, channel::oneshot};
@@ -132,11 +132,8 @@ impl TestAppContext {
let http_client = http_client::FakeHttpClient::with_404_response();
let text_system = Arc::new(TextSystem::new(platform.text_system()));
let mut app = App::new_app(platform.clone(), asset_source, http_client);
app.borrow_mut().mode = GpuiMode::test();
Self {
app,
app: App::new_app(platform.clone(), asset_source, http_client),
background_executor,
foreground_executor,
dispatcher,
@@ -147,11 +144,6 @@ impl TestAppContext {
}
}
/// Skip all drawing operations for the duration of this test.
pub fn skip_drawing(&mut self) {
self.app.borrow_mut().mode = GpuiMode::Test { skip_drawing: true };
}
/// Create a single TestAppContext, for non-multi-client tests
pub fn single() -> Self {
let dispatcher = TestDispatcher::new(StdRng::seed_from_u64(0));

View File

@@ -8,7 +8,6 @@ use std::{fmt::Debug, ops::Range};
use taffy::{
TaffyTree, TraversePartialTree as _,
geometry::{Point as TaffyPoint, Rect as TaffyRect, Size as TaffySize},
prelude::min_content,
style::AvailableSpace as TaffyAvailableSpace,
tree::NodeId,
};
@@ -296,7 +295,7 @@ trait ToTaffy<Output> {
impl ToTaffy<taffy::style::Style> for Style {
fn to_taffy(&self, rem_size: Pixels, scale_factor: f32) -> taffy::style::Style {
use taffy::style_helpers::{length, minmax, repeat};
use taffy::style_helpers::{fr, length, minmax, repeat};
fn to_grid_line(
placement: &Range<crate::GridPlacement>,
@@ -310,8 +309,8 @@ impl ToTaffy<taffy::style::Style> for Style {
fn to_grid_repeat<T: taffy::style::CheapCloneStr>(
unit: &Option<u16>,
) -> Vec<taffy::GridTemplateComponent<T>> {
// grid-template-columns: repeat(<number>, minmax(0, min-content));
unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), min_content())])])
// grid-template-columns: repeat(<number>, minmax(0, 1fr));
unit.map(|count| vec![repeat(count, vec![minmax(length(0.0), fr(1.0))])])
.unwrap_or_default()
}

View File

@@ -2006,9 +2006,7 @@ impl Window {
if let Some(input_handler) = self.platform_window.take_input_handler() {
self.rendered_frame.input_handlers.push(Some(input_handler));
}
if !cx.mode.skip_drawing() {
self.draw_roots(cx);
}
self.draw_roots(cx);
self.dirty_views.clear();
self.next_frame.window_active = self.active.get();

View File

@@ -1,4 +1,4 @@
use std::{path::Path, pin::Pin, task::Poll};
use std::{future::Future, path::Path, pin::Pin, task::Poll};
use anyhow::{Context, Result};
use async_compression::futures::bufread::GzipDecoder;
@@ -85,6 +85,65 @@ pub async fn download_server_binary(
Ok(())
}
pub async fn fetch_github_binary_with_digest_check<ValidityCheck, ValidityCheckFuture>(
binary_path: &Path,
metadata_path: &Path,
expected_digest: Option<String>,
url: &str,
asset_kind: AssetKind,
download_destination: &Path,
http_client: &dyn HttpClient,
validity_check: ValidityCheck,
) -> Result<()>
where
ValidityCheck: FnOnce() -> ValidityCheckFuture,
ValidityCheckFuture: Future<Output = Result<()>>,
{
let metadata = GithubBinaryMetadata::read_from_file(metadata_path)
.await
.ok();
if let Some(metadata) = metadata {
let validity_check_result = validity_check().await;
if let (Some(actual_digest), Some(expected_digest_ref)) =
(&metadata.digest, &expected_digest)
{
if actual_digest == expected_digest_ref {
if validity_check_result.is_ok() {
return Ok(());
}
} else {
log::info!(
"SHA-256 mismatch for {binary_path:?} asset, downloading new asset. Expected: {expected_digest_ref}, Got: {actual_digest}"
);
}
} else if validity_check_result.is_ok() {
return Ok(());
}
}
download_server_binary(
http_client,
url,
expected_digest.as_deref(),
download_destination,
asset_kind,
)
.await?;
GithubBinaryMetadata::write_to_file(
&GithubBinaryMetadata {
metadata_version: 1,
digest: expected_digest,
},
metadata_path,
)
.await?;
Ok(())
}
async fn stream_response_archive(
response: impl AsyncRead + Unpin,
url: &str,

View File

@@ -408,7 +408,6 @@ impl FakeHttpClient {
}
pub fn with_404_response() -> Arc<HttpClientWithUrl> {
log::warn!("Using fake HTTP client with 404 response");
Self::create(|_| async move {
Ok(Response::builder()
.status(404)
@@ -418,7 +417,6 @@ impl FakeHttpClient {
}
pub fn with_200_response() -> Arc<HttpClientWithUrl> {
log::warn!("Using fake HTTP client with 200 response");
Self::create(|_| async move {
Ok(Response::builder()
.status(200)

View File

@@ -86,6 +86,7 @@ pub enum IconName {
DebugIgnoreBreakpoints,
DebugLogBreakpoint,
DebugPause,
DebugStepBack,
DebugStepInto,
DebugStepOut,
DebugStepOver,

View File

@@ -686,6 +686,7 @@ impl CompletionProvider for RustStyleCompletionProvider {
position: language::Anchor,
_text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
completion_replace_range(&buffer.read(cx).snapshot(), &position).is_some()

View File

@@ -3001,6 +3001,7 @@ impl CompletionProvider for KeyContextCompletionProvider {
_position: language::Anchor,
text: &str,
_trigger_in_words: bool,
_menu_is_open: bool,
_cx: &mut Context<Editor>,
) -> bool {
text.chars()

Some files were not shown because too many files have changed in this diff Show More