Compare commits
1 Commits
fix-linux-
...
improve-ve
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96fbc43cef |
29
.github/workflows/extension_bump.yml
vendored
@@ -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
@@ -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]]
|
||||
|
||||
14
Cargo.toml
@@ -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"
|
||||
|
||||
@@ -53,10 +53,6 @@ extension
|
||||
git
|
||||
= @cole-miller
|
||||
= @danilo-leal
|
||||
= @dvdsk
|
||||
= @kubkon
|
||||
= @Anthony-Eid
|
||||
= @cameron1024
|
||||
|
||||
gpui
|
||||
= @Anthony-Eid
|
||||
|
||||
1
assets/icons/debug_step_back.svg
Normal 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 |
@@ -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 |
@@ -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 |
@@ -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 |
@@ -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": {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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/") {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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"] }
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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),
|
||||
});
|
||||
|
||||
@@ -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 })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
),
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -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()),
|
||||
)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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()
|
||||
})
|
||||
}
|
||||
})?;
|
||||
|
||||
|
||||
@@ -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)?;
|
||||
|
||||
@@ -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()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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| {
|
||||
|
||||
@@ -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
|
||||
})
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -719,7 +719,6 @@ impl CodegenAlternative {
|
||||
output_tokens = usage.output_tokens,
|
||||
)
|
||||
}
|
||||
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()),
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
|
||||
@@ -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")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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?;
|
||||
|
||||
@@ -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
|
||||
@@ -1 +0,0 @@
|
||||
LICENSE-GPL
|
||||
@@ -1,3 +0,0 @@
|
||||
# eval_utils
|
||||
|
||||
Utilities for evals of agents.
|
||||
@@ -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();
|
||||
}
|
||||
@@ -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]
|
||||
|
||||
@@ -334,7 +334,7 @@ mod wit {
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.8.0",
|
||||
path: "./wit/since_v0.6.0",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -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>>;
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>;
|
||||
}
|
||||
@@ -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>>,
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
@@ -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>,
|
||||
|
||||
@@ -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(),
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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>> {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
};
|
||||
|
||||
|
||||
@@ -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)"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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))
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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));
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -86,6 +86,7 @@ pub enum IconName {
|
||||
DebugIgnoreBreakpoints,
|
||||
DebugLogBreakpoint,
|
||||
DebugPause,
|
||||
DebugStepBack,
|
||||
DebugStepInto,
|
||||
DebugStepOut,
|
||||
DebugStepOver,
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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()
|
||||
|
||||