Compare commits
116 Commits
fix-34466
...
extension-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ea7a36a00e | ||
|
|
014c49df4f | ||
|
|
43f977c6b9 | ||
|
|
bdb8caa42e | ||
|
|
9ae77ec3c9 | ||
|
|
d5ed9d3e3a | ||
|
|
74a1b5d14d | ||
|
|
07af011eb4 | ||
|
|
c357dc25fc | ||
|
|
93bc6616c6 | ||
|
|
a33e881906 | ||
|
|
c978db8626 | ||
|
|
2dad46c5c0 | ||
|
|
4c51fffbb5 | ||
|
|
0d80b452fb | ||
|
|
bad6bde03a | ||
|
|
4ec2d04ad9 | ||
|
|
0f0017dc8e | ||
|
|
9db0d66251 | ||
|
|
b07389d9f3 | ||
|
|
db2e26f67b | ||
|
|
391c92b07a | ||
|
|
1e4d80a21f | ||
|
|
f90d9d26a5 | ||
|
|
40a611bf34 | ||
|
|
8ad3a150c8 | ||
|
|
87976e91cf | ||
|
|
290a1550aa | ||
|
|
92dcfdef76 | ||
|
|
4ef8433396 | ||
|
|
a51e975b81 | ||
|
|
493cfadb42 | ||
|
|
0818cedded | ||
|
|
6b46a71dd0 | ||
|
|
575ea49aad | ||
|
|
85ccd7c98b | ||
|
|
b168679c18 | ||
|
|
621ac16e35 | ||
|
|
c248a956e0 | ||
|
|
7e177c496c | ||
|
|
e39dd2af67 | ||
|
|
904d90bee7 | ||
|
|
1e09cbfefa | ||
|
|
8ca2571367 | ||
|
|
95a553ea94 | ||
|
|
bf878e9a95 | ||
|
|
a688239113 | ||
|
|
4e8f6ddae9 | ||
|
|
0f67f08795 | ||
|
|
fe6fa1bbdc | ||
|
|
50d0f29624 | ||
|
|
9857fd233d | ||
|
|
ad51017f20 | ||
|
|
2bf47879de | ||
|
|
65b4e9b10a | ||
|
|
98dec9246e | ||
|
|
39536cae83 | ||
|
|
b4e1d86a16 | ||
|
|
8a12ecf849 | ||
|
|
22bf449b9e | ||
|
|
bcf9142bbc | ||
|
|
a2d57fc7b6 | ||
|
|
96a917091a | ||
|
|
a2ddb0f1cb | ||
|
|
23e5477a4c | ||
|
|
4e043cd56b | ||
|
|
d283338885 | ||
|
|
b1af02ca71 | ||
|
|
59b5de5532 | ||
|
|
efa98a12fd | ||
|
|
7bea1ba555 | ||
|
|
7c95834b7b | ||
|
|
3d58738548 | ||
|
|
2db237aa52 | ||
|
|
305e73ebbb | ||
|
|
ec6e7b84b8 | ||
|
|
4f5cc0a24b | ||
|
|
a2f69cd5bd | ||
|
|
6a097298b0 | ||
|
|
0df86e406a | ||
|
|
a74aac88c9 | ||
|
|
e5105ccdbe | ||
|
|
876b258088 | ||
|
|
19aba43f3e | ||
|
|
8d09610748 | ||
|
|
5b6663ef97 | ||
|
|
f445f22fe6 | ||
|
|
6216af9b5a | ||
|
|
464c0be2b7 | ||
|
|
2df5993eb0 | ||
|
|
04e92fb2d2 | ||
|
|
e27590432f | ||
|
|
a675eb1667 | ||
|
|
b27ad98520 | ||
|
|
9c4e16088c | ||
|
|
34a2bfd6b7 | ||
|
|
99d8d34d48 | ||
|
|
bd79edee71 | ||
|
|
0bb1c6ad3e | ||
|
|
fd146757cf | ||
|
|
6eb9f9add7 | ||
|
|
3d3d124e01 | ||
|
|
de392cda39 | ||
|
|
33513292af | ||
|
|
1535e95066 | ||
|
|
26f77032a2 | ||
|
|
efff602909 | ||
|
|
58c9cbae40 | ||
|
|
7881551dda | ||
|
|
ff6bd7d82e | ||
|
|
bfb876c782 | ||
|
|
64b432e4ac | ||
|
|
33ecb0a68f | ||
|
|
7b7ddbd1e8 | ||
|
|
ed81ef0442 | ||
|
|
88fffae9dd |
16
.github/ISSUE_TEMPLATE/11_crash_report.yml
vendored
@@ -6,28 +6,18 @@ body:
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Reproduction steps
|
||||
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. **Be verbose**. **Issues with insufficient detail may be summarily closed**.
|
||||
description: A step-by-step description of how to reproduce the crash from a **clean Zed install**. The more context you provide, the easier it is to find and fix the problem fast.
|
||||
placeholder: |
|
||||
1. Start Zed
|
||||
2. Perform an action
|
||||
3. Zed crashes
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Current vs. Expected behavior
|
||||
description: |
|
||||
Go into depth about what actions you’re performing in Zed to trigger the crash. If Zed crashes before it loads any windows, make sure to mention that. Again, **be verbose**.
|
||||
|
||||
**Skipping this/failure to provide complete information will result in the issue being closed.**
|
||||
placeholder: "Based on my reproduction steps above, when I perform said action, I expect this to happen, but instead Zed crashes."
|
||||
validations:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: Zed version and system specs
|
||||
description: |
|
||||
Open the command palette in Zed, then type “zed: copy system specs into clipboard”. **Skipping this/failure to provide complete information will result in the issue being closed**.
|
||||
Open the command palette in Zed, then type “zed: copy system specs into clipboard”.
|
||||
placeholder: |
|
||||
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
|
||||
OS: macOS 15.1
|
||||
@@ -37,7 +27,7 @@ body:
|
||||
required: true
|
||||
- type: textarea
|
||||
attributes:
|
||||
label: If applicable, attach your Zed log file to this issue
|
||||
label: Attach Zed log file
|
||||
description: |
|
||||
Open the command palette in Zed, then type `zed: open log` to see the last 1000 lines. Or type `zed: reveal log in file manager` in the command palette to reveal the log file itself.
|
||||
value: |
|
||||
|
||||
48
.github/workflows/extension_bump.yml
vendored
@@ -25,33 +25,6 @@ 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: 1
|
||||
check_bump_needed:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
runs-on: namespace-profile-2x4-ubuntu-2404
|
||||
@@ -89,7 +62,6 @@ jobs:
|
||||
timeout-minutes: 1
|
||||
bump_extension_version:
|
||||
needs:
|
||||
- check_extension
|
||||
- check_bump_needed
|
||||
if: |-
|
||||
(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') &&
|
||||
@@ -114,21 +86,18 @@ jobs:
|
||||
run: |
|
||||
OLD_VERSION="${{ needs.check_bump_needed.outputs.current_version }}"
|
||||
|
||||
cat <<EOF > .bumpversion.cfg
|
||||
[bumpversion]
|
||||
current_version = "$OLD_VERSION"
|
||||
BUMP_FILES=("extension.toml")
|
||||
if [[ -f "Cargo.toml" ]]; then
|
||||
BUMP_FILES+=("Cargo.toml")
|
||||
fi
|
||||
|
||||
[bumpversion:file:Cargo.toml]
|
||||
bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files ${{ inputs.bump-type }} "${BUMP_FILES[@]}"
|
||||
|
||||
[bumpversion:file:extension.toml]
|
||||
if [[ -f "Cargo.toml" ]]; then
|
||||
cargo update --workspace
|
||||
fi
|
||||
|
||||
EOF
|
||||
|
||||
bump2version --verbose ${{ inputs.bump-type }}
|
||||
NEW_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
|
||||
cargo update --workspace
|
||||
|
||||
rm .bumpversion.cfg
|
||||
|
||||
echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
|
||||
shell: bash -euxo pipefail {0}
|
||||
@@ -147,7 +116,6 @@ 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
|
||||
|
||||
2
.github/workflows/extension_release.yml
vendored
@@ -21,6 +21,8 @@ jobs:
|
||||
with:
|
||||
app-id: ${{ secrets.app-id }}
|
||||
private-key: ${{ secrets.app-secret }}
|
||||
owner: zed-industries
|
||||
repositories: extensions
|
||||
- name: steps::checkout_repo
|
||||
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
|
||||
with:
|
||||
|
||||
13
.github/workflows/extension_tests.yml
vendored
@@ -7,12 +7,7 @@ env:
|
||||
CARGO_INCREMENTAL: '0'
|
||||
ZED_EXTENSION_CLI_SHA: 7cfce605704d41ca247e3f84804bf323f6c6caaf
|
||||
on:
|
||||
workflow_call:
|
||||
inputs:
|
||||
run_tests:
|
||||
description: Whether the workflow should run rust tests
|
||||
required: true
|
||||
type: boolean
|
||||
workflow_call: {}
|
||||
jobs:
|
||||
orchestrate:
|
||||
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
|
||||
@@ -73,12 +68,12 @@ jobs:
|
||||
run: cargo clippy --release --all-targets --all-features -- --deny warnings
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: steps::cargo_install_nextest
|
||||
if: inputs.run_tests
|
||||
uses: taiki-e/install-action@nextest
|
||||
- name: steps::cargo_nextest
|
||||
if: inputs.run_tests
|
||||
run: cargo nextest run --workspace --no-fail-fast
|
||||
shell: bash -euxo pipefail {0}
|
||||
env:
|
||||
NEXTEST_NO_TESTS: warn
|
||||
timeout-minutes: 3
|
||||
check_extension:
|
||||
needs:
|
||||
@@ -108,7 +103,7 @@ jobs:
|
||||
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: 1
|
||||
timeout-minutes: 2
|
||||
tests_pass:
|
||||
needs:
|
||||
- orchestrate
|
||||
|
||||
3
.github/workflows/run_tests.yml
vendored
@@ -84,7 +84,7 @@ jobs:
|
||||
run: ./script/check-keymaps
|
||||
shell: bash -euxo pipefail {0}
|
||||
- name: run_tests::check_style::check_for_typos
|
||||
uses: crate-ci/typos@80c8a4945eec0f6d464eaf9e65ed98ef085283d1
|
||||
uses: crate-ci/typos@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
|
||||
with:
|
||||
config: ./typos.toml
|
||||
- name: steps::cargo_fmt
|
||||
@@ -520,6 +520,7 @@ jobs:
|
||||
uses: bufbuild/buf-setup-action@v1
|
||||
with:
|
||||
version: v1.29.0
|
||||
github_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
- name: run_tests::check_postgres_and_protobuf_migrations::bufbuild_breaking_action
|
||||
uses: bufbuild/buf-breaking-action@v1
|
||||
with:
|
||||
|
||||
137
Cargo.lock
generated
@@ -159,6 +159,7 @@ dependencies = [
|
||||
"derive_more 0.99.20",
|
||||
"editor",
|
||||
"env_logger 0.11.8",
|
||||
"eval_utils",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"git",
|
||||
@@ -215,9 +216,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol"
|
||||
version = "0.7.0"
|
||||
version = "0.8.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
|
||||
checksum = "3e639d6b544ad39f5b4e05802db5eb04e1518284eb05fda1839931003e0244c8"
|
||||
dependencies = [
|
||||
"agent-client-protocol-schema",
|
||||
"anyhow",
|
||||
@@ -226,16 +227,15 @@ dependencies = [
|
||||
"derive_more 2.0.1",
|
||||
"futures 0.3.31",
|
||||
"log",
|
||||
"parking_lot",
|
||||
"serde",
|
||||
"serde_json",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "agent-client-protocol-schema"
|
||||
version = "0.6.2"
|
||||
version = "0.9.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
|
||||
checksum = "f182f5e14bef8232b239719bd99166bb11e986c08fc211f28e392f880d3093ba"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"derive_more 2.0.1",
|
||||
@@ -328,6 +328,7 @@ dependencies = [
|
||||
"buffer_diff",
|
||||
"chrono",
|
||||
"client",
|
||||
"clock",
|
||||
"cloud_llm_client",
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
@@ -335,6 +336,7 @@ dependencies = [
|
||||
"context_server",
|
||||
"db",
|
||||
"editor",
|
||||
"eval_utils",
|
||||
"extension",
|
||||
"extension_host",
|
||||
"feature_flags",
|
||||
@@ -343,6 +345,7 @@ dependencies = [
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
"gpui",
|
||||
"gpui_tokio",
|
||||
"html_to_markdown",
|
||||
"http_client",
|
||||
"image",
|
||||
@@ -370,6 +373,7 @@ dependencies = [
|
||||
"proto",
|
||||
"rand 0.9.2",
|
||||
"release_channel",
|
||||
"reqwest_client",
|
||||
"rope",
|
||||
"rules_library",
|
||||
"schemars",
|
||||
@@ -2126,30 +2130,15 @@ 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 0.8.0",
|
||||
"bit-vec",
|
||||
]
|
||||
|
||||
[[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"
|
||||
@@ -2328,9 +2317,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "borrow-or-share"
|
||||
version = "0.2.2"
|
||||
version = "0.2.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
|
||||
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
|
||||
|
||||
[[package]]
|
||||
name = "borsh"
|
||||
@@ -2423,6 +2412,7 @@ dependencies = [
|
||||
"rand 0.9.2",
|
||||
"rope",
|
||||
"serde_json",
|
||||
"settings",
|
||||
"sum_tree",
|
||||
"text",
|
||||
"unindent",
|
||||
@@ -4183,6 +4173,7 @@ dependencies = [
|
||||
"serde_json",
|
||||
"smol",
|
||||
"system_specs",
|
||||
"windows 0.61.3",
|
||||
"zstd 0.11.2+zstd.1.5.2",
|
||||
]
|
||||
|
||||
@@ -5299,6 +5290,7 @@ dependencies = [
|
||||
"indoc",
|
||||
"language",
|
||||
"lsp",
|
||||
"menu",
|
||||
"paths",
|
||||
"project",
|
||||
"regex",
|
||||
@@ -5308,6 +5300,7 @@ dependencies = [
|
||||
"telemetry",
|
||||
"theme",
|
||||
"ui",
|
||||
"ui_input",
|
||||
"util",
|
||||
"workspace",
|
||||
"zed_actions",
|
||||
@@ -5412,6 +5405,7 @@ dependencies = [
|
||||
"tree-sitter-bash",
|
||||
"tree-sitter-c",
|
||||
"tree-sitter-html",
|
||||
"tree-sitter-md",
|
||||
"tree-sitter-python",
|
||||
"tree-sitter-rust",
|
||||
"tree-sitter-typescript",
|
||||
@@ -5772,6 +5766,15 @@ dependencies = [
|
||||
"watch",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "eval_utils"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"gpui",
|
||||
"serde",
|
||||
"smol",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "event-listener"
|
||||
version = "2.5.3"
|
||||
@@ -5991,22 +5994,11 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||
|
||||
[[package]]
|
||||
name = "fancy-regex"
|
||||
version = "0.13.0"
|
||||
version = "0.16.2"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
|
||||
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
|
||||
dependencies = [
|
||||
"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",
|
||||
"bit-set",
|
||||
"regex-automata",
|
||||
"regex-syntax",
|
||||
]
|
||||
@@ -6228,9 +6220,9 @@ checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
|
||||
|
||||
[[package]]
|
||||
name = "fluent-uri"
|
||||
version = "0.3.2"
|
||||
version = "0.4.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
|
||||
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
|
||||
dependencies = [
|
||||
"borrow-or-share",
|
||||
"ref-cast",
|
||||
@@ -6969,7 +6961,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gh-workflow"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/zed-industries/gh-workflow?rev=e5f883040530b4df36437f140084ee5cc7c1c9be#e5f883040530b4df36437f140084ee5cc7c1c9be"
|
||||
source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"derive_more 2.0.1",
|
||||
@@ -6986,7 +6978,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "gh-workflow-macros"
|
||||
version = "0.8.0"
|
||||
source = "git+https://github.com/zed-industries/gh-workflow?rev=e5f883040530b4df36437f140084ee5cc7c1c9be#e5f883040530b4df36437f140084ee5cc7c1c9be"
|
||||
source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c"
|
||||
dependencies = [
|
||||
"heck 0.5.0",
|
||||
"quote",
|
||||
@@ -7526,6 +7518,17 @@ 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"
|
||||
@@ -8615,21 +8618,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "jsonschema"
|
||||
version = "0.30.0"
|
||||
version = "0.37.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
|
||||
checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"base64 0.22.1",
|
||||
"bytecount",
|
||||
"data-encoding",
|
||||
"email_address",
|
||||
"fancy-regex 0.14.0",
|
||||
"fancy-regex",
|
||||
"fraction",
|
||||
"getrandom 0.3.4",
|
||||
"idna",
|
||||
"itoa",
|
||||
"num-cmp",
|
||||
"num-traits",
|
||||
"once_cell",
|
||||
"percent-encoding",
|
||||
"referencing",
|
||||
"regex",
|
||||
@@ -8637,6 +8640,7 @@ dependencies = [
|
||||
"reqwest 0.12.24",
|
||||
"serde",
|
||||
"serde_json",
|
||||
"unicode-general-category",
|
||||
"uuid-simd",
|
||||
]
|
||||
|
||||
@@ -10185,7 +10189,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
|
||||
dependencies = [
|
||||
"arrayvec",
|
||||
"bit-set 0.8.0",
|
||||
"bit-set",
|
||||
"bitflags 2.9.4",
|
||||
"cfg_aliases 0.2.1",
|
||||
"codespan-reporting 0.12.0",
|
||||
@@ -13041,7 +13045,7 @@ dependencies = [
|
||||
"dap",
|
||||
"dap_adapters",
|
||||
"extension",
|
||||
"fancy-regex 0.14.0",
|
||||
"fancy-regex",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"fuzzy",
|
||||
@@ -13912,13 +13916,14 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "referencing"
|
||||
version = "0.30.0"
|
||||
version = "0.37.4"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
|
||||
checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c"
|
||||
dependencies = [
|
||||
"ahash 0.8.12",
|
||||
"fluent-uri",
|
||||
"once_cell",
|
||||
"getrandom 0.3.4",
|
||||
"hashbrown 0.16.1",
|
||||
"parking_lot",
|
||||
"percent-encoding",
|
||||
"serde_json",
|
||||
@@ -17112,7 +17117,7 @@ dependencies = [
|
||||
"alacritty_terminal",
|
||||
"anyhow",
|
||||
"collections",
|
||||
"fancy-regex 0.14.0",
|
||||
"fancy-regex",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
"itertools 0.14.0",
|
||||
@@ -17346,12 +17351,12 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "tiktoken-rs"
|
||||
version = "0.9.1"
|
||||
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=7249f999c5fdf9bf3cc5c288c964454e4dac0c00#7249f999c5fdf9bf3cc5c288c964454e4dac0c00"
|
||||
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=2570c4387a8505fb8f1d3f3557454b474f1e8271#2570c4387a8505fb8f1d3f3557454b474f1e8271"
|
||||
dependencies = [
|
||||
"anyhow",
|
||||
"base64 0.22.1",
|
||||
"bstr",
|
||||
"fancy-regex 0.13.0",
|
||||
"fancy-regex",
|
||||
"lazy_static",
|
||||
"regex",
|
||||
"rustc-hash 1.1.0",
|
||||
@@ -18003,9 +18008,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "tree-sitter-bash"
|
||||
version = "0.25.0"
|
||||
version = "0.25.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "871b0606e667e98a1237ebdc1b0d7056e0aebfdc3141d12b399865d4cb6ed8a6"
|
||||
checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062"
|
||||
dependencies = [
|
||||
"cc",
|
||||
"tree-sitter-language",
|
||||
@@ -18483,6 +18488,12 @@ 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"
|
||||
@@ -18717,7 +18728,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
|
||||
dependencies = [
|
||||
"outref",
|
||||
"uuid",
|
||||
"vsimd",
|
||||
]
|
||||
|
||||
@@ -21202,7 +21212,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed"
|
||||
version = "0.216.0"
|
||||
version = "0.217.0"
|
||||
dependencies = [
|
||||
"acp_tools",
|
||||
"activity_indicator",
|
||||
@@ -21492,6 +21502,8 @@ 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",
|
||||
@@ -21500,9 +21512,7 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "zed_extension_api"
|
||||
version = "0.7.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
|
||||
version = "0.8.0"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"serde_json",
|
||||
@@ -21520,7 +21530,7 @@ dependencies = [
|
||||
name = "zed_html"
|
||||
version = "0.2.3"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
|
||||
"zed_extension_api 0.7.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -21534,7 +21544,7 @@ dependencies = [
|
||||
name = "zed_test_extension"
|
||||
version = "0.1.0"
|
||||
dependencies = [
|
||||
"zed_extension_api 0.7.0",
|
||||
"zed_extension_api 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -21678,6 +21688,7 @@ dependencies = [
|
||||
"collections",
|
||||
"command_palette_hooks",
|
||||
"copilot",
|
||||
"credentials_provider",
|
||||
"ctor",
|
||||
"db",
|
||||
"edit_prediction",
|
||||
|
||||
15
Cargo.toml
@@ -59,6 +59,7 @@ members = [
|
||||
"crates/zeta2_tools",
|
||||
"crates/editor",
|
||||
"crates/eval",
|
||||
"crates/eval_utils",
|
||||
"crates/explorer_command_injector",
|
||||
"crates/extension",
|
||||
"crates/extension_api",
|
||||
@@ -288,6 +289,7 @@ 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" }
|
||||
@@ -439,7 +441,7 @@ zlog_settings = { path = "crates/zlog_settings" }
|
||||
# External crates
|
||||
#
|
||||
|
||||
agent-client-protocol = { version = "0.7.0", features = ["unstable"] }
|
||||
agent-client-protocol = { version = "=0.8.0", features = ["unstable"] }
|
||||
aho-corasick = "1.1"
|
||||
alacritty_terminal = "0.25.1-rc1"
|
||||
any_vec = "0.14"
|
||||
@@ -503,12 +505,12 @@ ec4rs = "1.1"
|
||||
emojis = "0.6.1"
|
||||
env_logger = "0.11"
|
||||
exec = "0.3.1"
|
||||
fancy-regex = "0.14.0"
|
||||
fancy-regex = "0.16.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 = "e5f883040530b4df36437f140084ee5cc7c1c9be" }
|
||||
gh-workflow = { git = "https://github.com/zed-industries/gh-workflow", rev = "09acfdf2bd5c1d6254abefd609c808ff73547b2c" }
|
||||
git2 = { version = "0.20.1", default-features = false }
|
||||
globset = "0.4"
|
||||
handlebars = "4.3"
|
||||
@@ -529,7 +531,7 @@ indoc = "2"
|
||||
inventory = "0.3.19"
|
||||
itertools = "0.14.0"
|
||||
json_dotpath = "1.1"
|
||||
jsonschema = "0.30.0"
|
||||
jsonschema = "0.37.0"
|
||||
jsonwebtoken = "9.3"
|
||||
jupyter-protocol = "0.10.0"
|
||||
jupyter-websocket-client = "0.15.0"
|
||||
@@ -639,6 +641,7 @@ serde_urlencoded = "0.7"
|
||||
sha2 = "0.10"
|
||||
shellexpand = "2.1.0"
|
||||
shlex = "1.3.0"
|
||||
similar = "2.6"
|
||||
simplelog = "0.12.2"
|
||||
slotmap = "1.0.6"
|
||||
smallvec = { version = "1.6", features = ["union"] }
|
||||
@@ -655,7 +658,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 = "7249f999c5fdf9bf3cc5c288c964454e4dac0c00" }
|
||||
tiktoken-rs = { git = "https://github.com/zed-industries/tiktoken-rs", rev = "2570c4387a8505fb8f1d3f3557454b474f1e8271" }
|
||||
time = { version = "0.3", features = [
|
||||
"macros",
|
||||
"parsing",
|
||||
@@ -671,7 +674,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.0"
|
||||
tree-sitter-bash = "0.25.1"
|
||||
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,6 +53,10 @@ extension
|
||||
git
|
||||
= @cole-miller
|
||||
= @danilo-leal
|
||||
= @dvdsk
|
||||
= @kubkon
|
||||
= @Anthony-Eid
|
||||
= @cameron1024
|
||||
|
||||
gpui
|
||||
= @Anthony-Eid
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
<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>
|
||||
|
Before Width: | Height: | Size: 467 B |
@@ -1 +1,5 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 374 B After Width: | Height: | Size: 443 B |
@@ -1 +1,5 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 373 B After Width: | Height: | Size: 439 B |
@@ -1 +1,5 @@
|
||||
<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>
|
||||
<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>
|
||||
|
||||
|
Before Width: | Height: | Size: 468 B After Width: | Height: | Size: 627 B |
@@ -49,7 +49,8 @@
|
||||
"ctrl-cmd-f": "zed::ToggleFullScreen",
|
||||
"ctrl-cmd-z": "edit_prediction::RateCompletions",
|
||||
"ctrl-cmd-i": "edit_prediction::ToggleMenu",
|
||||
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
|
||||
"ctrl-cmd-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -589,8 +590,7 @@
|
||||
"cmd-.": "editor::ToggleCodeActions",
|
||||
"cmd-k r": "editor::RevealInFileManager",
|
||||
"cmd-k p": "editor::CopyPath",
|
||||
"cmd-\\": "pane::SplitRight",
|
||||
"ctrl-cmd-c": "editor::DisplayCursorNames"
|
||||
"cmd-\\": "pane::SplitRight"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -730,7 +730,8 @@
|
||||
"context": "Workspace && debugger_running",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"f5": "zed::NoAction"
|
||||
"f5": "zed::NoAction",
|
||||
"f11": "debugger::StepInto"
|
||||
}
|
||||
},
|
||||
{
|
||||
|
||||
@@ -36,12 +36,12 @@
|
||||
"shift-f5": "debugger::Stop",
|
||||
"ctrl-shift-f5": "debugger::RerunSession",
|
||||
"f6": "debugger::Pause",
|
||||
"f7": "debugger::StepOver",
|
||||
"ctrl-f11": "debugger::StepInto",
|
||||
"f10": "debugger::StepOver",
|
||||
"shift-f11": "debugger::StepOut",
|
||||
"f11": "zed::ToggleFullScreen",
|
||||
"ctrl-shift-i": "edit_prediction::ToggleMenu",
|
||||
"shift-alt-l": "lsp_tool::ToggleMenu"
|
||||
"shift-alt-l": "lsp_tool::ToggleMenu",
|
||||
"ctrl-shift-alt-c": "editor::DisplayCursorNames"
|
||||
}
|
||||
},
|
||||
{
|
||||
@@ -117,7 +117,7 @@
|
||||
"alt-g m": "git::OpenModifiedFiles",
|
||||
"menu": "editor::OpenContextMenu",
|
||||
"shift-f10": "editor::OpenContextMenu",
|
||||
"ctrl-shift-e": "editor::ToggleEditPrediction",
|
||||
"ctrl-alt-e": "editor::ToggleEditPrediction",
|
||||
"f9": "editor::ToggleBreakpoint",
|
||||
"shift-f9": "editor::EditLogBreakpoint"
|
||||
}
|
||||
@@ -215,7 +215,7 @@
|
||||
"context": "ContextEditor > Editor",
|
||||
"use_key_equivalents": true,
|
||||
"bindings": {
|
||||
"ctrl-enter": "assistant::Assist",
|
||||
"ctrl-i": "assistant::Assist",
|
||||
"ctrl-s": "workspace::Save",
|
||||
"ctrl-shift-,": "assistant::InsertIntoEditor",
|
||||
"shift-enter": "assistant::Split",
|
||||
@@ -500,10 +500,7 @@
|
||||
"ctrl-shift-l": "editor::SelectAllMatches", // Select all occurrences of current selection
|
||||
"ctrl-f2": "editor::SelectAllMatches", // Select all occurrences of current word
|
||||
"ctrl-d": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch / find_under_expand
|
||||
"ctrl-shift-down": ["editor::SelectNext", { "replace_newest": false }], // editor.action.addSelectionToNextFindMatch
|
||||
"ctrl-shift-up": ["editor::SelectPrevious", { "replace_newest": false }], // editor.action.addSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-d": ["editor::SelectNext", { "replace_newest": true }], // editor.action.moveSelectionToNextFindMatch / find_under_expand_skip
|
||||
"ctrl-k ctrl-shift-d": ["editor::SelectPrevious", { "replace_newest": true }], // editor.action.moveSelectionToPreviousFindMatch
|
||||
"ctrl-k ctrl-i": "editor::Hover",
|
||||
"ctrl-k ctrl-b": "editor::BlameHover",
|
||||
"ctrl-/": ["editor::ToggleComments", { "advance_downwards": false }],
|
||||
@@ -512,12 +509,8 @@
|
||||
"f2": "editor::Rename",
|
||||
"f12": "editor::GoToDefinition",
|
||||
"alt-f12": "editor::GoToDefinitionSplit",
|
||||
"ctrl-shift-f10": "editor::GoToDefinitionSplit",
|
||||
"ctrl-f12": "editor::GoToImplementation",
|
||||
"shift-f12": "editor::GoToTypeDefinition",
|
||||
"ctrl-alt-f12": "editor::GoToTypeDefinitionSplit",
|
||||
"shift-alt-f12": "editor::FindAllReferences",
|
||||
"ctrl-m": "editor::MoveToEnclosingBracket", // from jetbrains
|
||||
"ctrl-shift-\\": "editor::MoveToEnclosingBracket",
|
||||
"ctrl-shift-[": "editor::Fold",
|
||||
"ctrl-shift-]": "editor::UnfoldLines",
|
||||
@@ -541,7 +534,6 @@
|
||||
"ctrl-k r": "editor::RevealInFileManager",
|
||||
"ctrl-k p": "editor::CopyPath",
|
||||
"ctrl-\\": "pane::SplitRight",
|
||||
"ctrl-shift-alt-c": "editor::DisplayCursorNames",
|
||||
"alt-.": "editor::GoToHunk",
|
||||
"alt-,": "editor::GoToPreviousHunk"
|
||||
}
|
||||
@@ -1124,7 +1116,7 @@
|
||||
"shift-insert": "terminal::Paste",
|
||||
"ctrl-v": "terminal::Paste",
|
||||
"ctrl-shift-v": "terminal::Paste",
|
||||
"ctrl-enter": "assistant::InlineAssist",
|
||||
"ctrl-i": "assistant::InlineAssist",
|
||||
"alt-b": ["terminal::SendText", "\u001bb"],
|
||||
"alt-f": ["terminal::SendText", "\u001bf"],
|
||||
"alt-.": ["terminal::SendText", "\u001b."],
|
||||
|
||||
@@ -857,6 +857,8 @@
|
||||
"ctrl-w shift-right": "workspace::SwapPaneRight",
|
||||
"ctrl-w shift-up": "workspace::SwapPaneUp",
|
||||
"ctrl-w shift-down": "workspace::SwapPaneDown",
|
||||
"ctrl-w x": "workspace::SwapPaneAdjacent",
|
||||
"ctrl-w ctrl-x": "workspace::SwapPaneAdjacent",
|
||||
"ctrl-w shift-h": "workspace::MovePaneLeft",
|
||||
"ctrl-w shift-l": "workspace::MovePaneRight",
|
||||
"ctrl-w shift-k": "workspace::MovePaneUp",
|
||||
|
||||
@@ -1100,13 +1100,22 @@
|
||||
"preview_tabs": {
|
||||
// Whether preview tabs should be enabled.
|
||||
// Preview tabs allow you to open files in preview mode, where they close automatically
|
||||
// when you switch to another file unless you explicitly pin them.
|
||||
// when you open another preview tab.
|
||||
// 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 a preview tab gets replaced when code navigation is used to navigate away from the tab.
|
||||
"enable_preview_from_code_navigation": 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
|
||||
},
|
||||
// Settings related to the file finder.
|
||||
"file_finder": {
|
||||
@@ -1209,6 +1218,13 @@
|
||||
"tab_size": 4,
|
||||
// What debuggers are preferred by default for all languages.
|
||||
"debuggers": [],
|
||||
// Whether to enable word diff highlighting in the editor.
|
||||
//
|
||||
// When enabled, changed words within modified lines are highlighted
|
||||
// to show exactly what changed.
|
||||
//
|
||||
// Default: true
|
||||
"word_diff_enabled": true,
|
||||
// Control what info is collected by Zed.
|
||||
"telemetry": {
|
||||
// Send debug info like crash reports.
|
||||
|
||||
@@ -45,6 +45,7 @@
|
||||
"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,
|
||||
@@ -436,6 +437,7 @@
|
||||
"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,
|
||||
@@ -827,6 +829,7 @@
|
||||
"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,6 +46,7 @@
|
||||
"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,
|
||||
@@ -452,6 +453,7 @@
|
||||
"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,
|
||||
@@ -858,6 +860,7 @@
|
||||
"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,
|
||||
@@ -1264,6 +1267,7 @@
|
||||
"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,
|
||||
@@ -1670,6 +1674,7 @@
|
||||
"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,
|
||||
@@ -2076,6 +2081,7 @@
|
||||
"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,6 +45,7 @@
|
||||
"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,
|
||||
@@ -98,6 +99,8 @@
|
||||
"link_text.hover": "#74ade8ff",
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.word_added": "#2EA04859",
|
||||
"version_control.word_deleted": "#78081BCC",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"version_control.conflict_marker.ours": "#a1c1811a",
|
||||
"version_control.conflict_marker.theirs": "#74ade81a",
|
||||
@@ -446,6 +449,7 @@
|
||||
"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,
|
||||
@@ -499,6 +503,8 @@
|
||||
"link_text.hover": "#5c78e2ff",
|
||||
"version_control.added": "#27a657ff",
|
||||
"version_control.modified": "#d3b020ff",
|
||||
"version_control.word_added": "#2EA04859",
|
||||
"version_control.word_deleted": "#F85149CC",
|
||||
"version_control.deleted": "#e06c76ff",
|
||||
"conflict": "#a48819ff",
|
||||
"conflict.background": "#faf2e6ff",
|
||||
|
||||
@@ -201,17 +201,19 @@ impl ToolCall {
|
||||
};
|
||||
let mut content = Vec::with_capacity(tool_call.content.len());
|
||||
for item in tool_call.content {
|
||||
content.push(ToolCallContent::from_acp(
|
||||
if let Some(item) = ToolCallContent::from_acp(
|
||||
item,
|
||||
language_registry.clone(),
|
||||
path_style,
|
||||
terminals,
|
||||
cx,
|
||||
)?);
|
||||
)? {
|
||||
content.push(item);
|
||||
}
|
||||
}
|
||||
|
||||
let result = Self {
|
||||
id: tool_call.id,
|
||||
id: tool_call.tool_call_id,
|
||||
label: cx
|
||||
.new(|cx| Markdown::new(title.into(), Some(language_registry.clone()), None, cx)),
|
||||
kind: tool_call.kind,
|
||||
@@ -241,6 +243,7 @@ impl ToolCall {
|
||||
locations,
|
||||
raw_input,
|
||||
raw_output,
|
||||
..
|
||||
} = fields;
|
||||
|
||||
if let Some(kind) = kind {
|
||||
@@ -262,21 +265,29 @@ impl ToolCall {
|
||||
}
|
||||
|
||||
if let Some(content) = content {
|
||||
let new_content_len = content.len();
|
||||
let mut 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()) {
|
||||
old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
|
||||
let valid_content =
|
||||
old.update_from_acp(new, language_registry.clone(), path_style, terminals, cx)?;
|
||||
if !valid_content {
|
||||
new_content_len -= 1;
|
||||
}
|
||||
}
|
||||
for new in content {
|
||||
self.content.push(ToolCallContent::from_acp(
|
||||
if let Some(new) = 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);
|
||||
}
|
||||
@@ -425,6 +436,7 @@ impl From<acp::ToolCallStatus> for ToolCallStatus {
|
||||
acp::ToolCallStatus::InProgress => Self::InProgress,
|
||||
acp::ToolCallStatus::Completed => Self::Completed,
|
||||
acp::ToolCallStatus::Failed => Self::Failed,
|
||||
_ => Self::Pending,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -537,7 +549,7 @@ impl ContentBlock {
|
||||
..
|
||||
}) => Self::resource_link_md(&uri, path_style),
|
||||
acp::ContentBlock::Image(image) => Self::image_md(&image),
|
||||
acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => String::new(),
|
||||
_ => String::new(),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -591,15 +603,17 @@ impl ToolCallContent {
|
||||
path_style: PathStyle,
|
||||
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
cx: &mut App,
|
||||
) -> Result<Self> {
|
||||
) -> Result<Option<Self>> {
|
||||
match content {
|
||||
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| {
|
||||
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| {
|
||||
Diff::finalized(
|
||||
diff.path.to_string_lossy().into_owned(),
|
||||
diff.old_text,
|
||||
@@ -607,12 +621,13 @@ impl ToolCallContent {
|
||||
language_registry,
|
||||
cx,
|
||||
)
|
||||
}))),
|
||||
acp::ToolCallContent::Terminal { terminal_id } => terminals
|
||||
})))),
|
||||
acp::ToolCallContent::Terminal(acp::Terminal { terminal_id, .. }) => terminals
|
||||
.get(&terminal_id)
|
||||
.cloned()
|
||||
.map(Self::Terminal)
|
||||
.map(|terminal| Some(Self::Terminal(terminal)))
|
||||
.ok_or_else(|| anyhow::anyhow!("Terminal with id `{}` not found", terminal_id)),
|
||||
_ => Ok(None),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -623,9 +638,9 @@ impl ToolCallContent {
|
||||
path_style: PathStyle,
|
||||
terminals: &HashMap<acp::TerminalId, Entity<Terminal>>,
|
||||
cx: &mut App,
|
||||
) -> Result<()> {
|
||||
) -> Result<bool> {
|
||||
let needs_update = match (&self, &new) {
|
||||
(Self::Diff(old_diff), acp::ToolCallContent::Diff { diff: new_diff }) => {
|
||||
(Self::Diff(old_diff), acp::ToolCallContent::Diff(new_diff)) => {
|
||||
old_diff.read(cx).needs_update(
|
||||
new_diff.old_text.as_deref().unwrap_or(""),
|
||||
&new_diff.new_text,
|
||||
@@ -635,10 +650,14 @@ impl ToolCallContent {
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if needs_update {
|
||||
*self = Self::from_acp(new, language_registry, path_style, terminals, cx)?;
|
||||
if let Some(update) = Self::from_acp(new, language_registry, path_style, terminals, cx)? {
|
||||
if needs_update {
|
||||
*self = update;
|
||||
}
|
||||
Ok(true)
|
||||
} else {
|
||||
Ok(false)
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub fn to_markdown(&self, cx: &App) -> String {
|
||||
@@ -660,7 +679,7 @@ pub enum ToolCallUpdate {
|
||||
impl ToolCallUpdate {
|
||||
fn id(&self) -> &acp::ToolCallId {
|
||||
match self {
|
||||
Self::UpdateFields(update) => &update.id,
|
||||
Self::UpdateFields(update) => &update.tool_call_id,
|
||||
Self::UpdateDiff(diff) => &diff.id,
|
||||
Self::UpdateTerminal(terminal) => &terminal.id,
|
||||
}
|
||||
@@ -732,6 +751,7 @@ impl Plan {
|
||||
acp::PlanEntryStatus::Completed => {
|
||||
stats.completed += 1;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1154,6 +1174,7 @@ impl AcpThread {
|
||||
current_mode_id,
|
||||
..
|
||||
}) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
@@ -1287,11 +1308,7 @@ 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(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Tool call not found".to_string(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
"Tool call not found".into(),
|
||||
&languages,
|
||||
path_style,
|
||||
cx,
|
||||
@@ -1315,7 +1332,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.id, cx);
|
||||
self.resolve_locations(update.tool_call_id, cx);
|
||||
}
|
||||
}
|
||||
ToolCallUpdate::UpdateDiff(update) => {
|
||||
@@ -1353,7 +1370,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.id.clone();
|
||||
let id = update.tool_call_id.clone();
|
||||
|
||||
let agent = self.connection().telemetry_id();
|
||||
let session = self.session_id();
|
||||
@@ -1518,16 +1535,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.id.clone())
|
||||
Some(option.option_id.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}) {
|
||||
self.upsert_tool_call_inner(tool_call, ToolCallStatus::Pending, cx)?;
|
||||
return Ok(async {
|
||||
acp::RequestPermissionOutcome::Selected {
|
||||
option_id: allow_once_option,
|
||||
}
|
||||
acp::RequestPermissionOutcome::Selected(acp::SelectedPermissionOutcome::new(
|
||||
allow_once_option,
|
||||
))
|
||||
}
|
||||
.boxed());
|
||||
}
|
||||
@@ -1543,7 +1560,9 @@ impl AcpThread {
|
||||
|
||||
let fut = async {
|
||||
match rx.await {
|
||||
Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
|
||||
Ok(option) => acp::RequestPermissionOutcome::Selected(
|
||||
acp::SelectedPermissionOutcome::new(option),
|
||||
),
|
||||
Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
|
||||
}
|
||||
}
|
||||
@@ -1570,6 +1589,7 @@ impl AcpThread {
|
||||
acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => {
|
||||
ToolCallStatus::InProgress
|
||||
}
|
||||
_ => ToolCallStatus::InProgress,
|
||||
};
|
||||
|
||||
let curr_status = mem::replace(&mut call.status, new_status);
|
||||
@@ -1648,14 +1668,7 @@ impl AcpThread {
|
||||
message: &str,
|
||||
cx: &mut Context<Self>,
|
||||
) -> BoxFuture<'static, Result<()>> {
|
||||
self.send(
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: message.to_string(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
})],
|
||||
cx,
|
||||
)
|
||||
self.send(vec![message.into()], cx)
|
||||
}
|
||||
|
||||
pub fn send(
|
||||
@@ -1669,11 +1682,7 @@ impl AcpThread {
|
||||
self.project.read(cx).path_style(cx),
|
||||
cx,
|
||||
);
|
||||
let request = acp::PromptRequest {
|
||||
prompt: message.clone(),
|
||||
session_id: self.session_id.clone(),
|
||||
meta: None,
|
||||
};
|
||||
let request = acp::PromptRequest::new(self.session_id.clone(), message.clone());
|
||||
let git_store = self.project.read(cx).git_store().clone();
|
||||
|
||||
let message_id = if self.connection.truncate(&self.session_id, cx).is_some() {
|
||||
@@ -1765,7 +1774,7 @@ impl AcpThread {
|
||||
result,
|
||||
Ok(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
meta: None,
|
||||
..
|
||||
}))
|
||||
);
|
||||
|
||||
@@ -1781,7 +1790,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() {
|
||||
@@ -2017,7 +2026,7 @@ impl AcpThread {
|
||||
})?;
|
||||
Ok(project.open_buffer(path, cx))
|
||||
})
|
||||
.map_err(|e| acp::Error::internal_error().with_data(e.to_string()))
|
||||
.map_err(|e| acp::Error::internal_error().data(e.to_string()))
|
||||
.flatten()?;
|
||||
|
||||
let buffer = load.await?;
|
||||
@@ -2050,7 +2059,7 @@ impl AcpThread {
|
||||
let start_position = Point::new(line, 0);
|
||||
|
||||
if start_position > max_point {
|
||||
return Err(acp::Error::invalid_params().with_data(format!(
|
||||
return Err(acp::Error::invalid_params().data(format!(
|
||||
"Attempting to read beyond the end of the file, line {}:{}",
|
||||
max_point.row + 1,
|
||||
max_point.column
|
||||
@@ -2202,7 +2211,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(Uuid::new_v4().to_string().into());
|
||||
let terminal_id = acp::TerminalId::new(Uuid::new_v4().to_string());
|
||||
let terminal_task = cx.spawn({
|
||||
let terminal_id = terminal_id.clone();
|
||||
async move |_this, cx| {
|
||||
@@ -2412,7 +2421,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
|
||||
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
|
||||
|
||||
// Send Output BEFORE Created - should be buffered by acp_thread
|
||||
thread.update(cx, |thread, cx| {
|
||||
@@ -2474,7 +2483,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
let terminal_id = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
|
||||
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
|
||||
|
||||
// Send Output BEFORE Created
|
||||
thread.update(cx, |thread, cx| {
|
||||
@@ -2492,11 +2501,7 @@ mod tests {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id: terminal_id.clone(),
|
||||
status: acp::TerminalExitStatus {
|
||||
exit_code: Some(0),
|
||||
signal: None,
|
||||
meta: None,
|
||||
},
|
||||
status: acp::TerminalExitStatus::new().exit_code(0),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -2553,15 +2558,7 @@ mod tests {
|
||||
|
||||
// Test creating a new user message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
None,
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Hello, ".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
thread.push_user_content_block(None, "Hello, ".into(), cx);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
@@ -2577,15 +2574,7 @@ 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()),
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "world!".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
cx,
|
||||
);
|
||||
thread.push_user_content_block(Some(message_1_id.clone()), "world!".into(), cx);
|
||||
});
|
||||
|
||||
thread.update(cx, |thread, cx| {
|
||||
@@ -2600,26 +2589,14 @@ mod tests {
|
||||
|
||||
// Test creating new user message after assistant message
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "Assistant response".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
);
|
||||
thread.push_assistant_content_block("Assistant response".into(), false, cx);
|
||||
});
|
||||
|
||||
let message_2_id = UserMessageId::new();
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread.push_user_content_block(
|
||||
Some(message_2_id.clone()),
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
annotations: None,
|
||||
text: "New user message".to_string(),
|
||||
meta: None,
|
||||
}),
|
||||
"New user message".into(),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -2647,27 +2624,22 @@ mod tests {
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
|
||||
content: "Thinking ".into(),
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
|
||||
"Thinking ".into(),
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk {
|
||||
content: "hard!".into(),
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::AgentThoughtChunk(acp::ContentChunk::new(
|
||||
"hard!".into(),
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
}
|
||||
.boxed_local()
|
||||
},
|
||||
@@ -2735,10 +2707,7 @@ mod tests {
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
}
|
||||
.boxed_local()
|
||||
},
|
||||
@@ -2969,7 +2938,7 @@ mod tests {
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
let project = Project::test(fs, [], cx).await;
|
||||
let id = acp::ToolCallId("test".into());
|
||||
let id = acp::ToolCallId::new("test");
|
||||
|
||||
let connection = Rc::new(FakeAgentConnection::new().on_user_message({
|
||||
let id = id.clone();
|
||||
@@ -2979,26 +2948,17 @@ mod tests {
|
||||
thread
|
||||
.update(&mut cx, |thread, cx| {
|
||||
thread.handle_session_update(
|
||||
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,
|
||||
}),
|
||||
acp::SessionUpdate::ToolCall(
|
||||
acp::ToolCall::new(id.clone(), "Label")
|
||||
.kind(acp::ToolKind::Fetch)
|
||||
.status(acp::ToolCallStatus::InProgress),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
@@ -3040,14 +3000,10 @@ mod tests {
|
||||
thread
|
||||
.update(cx, |thread, cx| {
|
||||
thread.handle_session_update(
|
||||
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
|
||||
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
|
||||
id,
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}),
|
||||
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -3079,33 +3035,21 @@ mod tests {
|
||||
thread
|
||||
.update(&mut cx, |thread, cx| {
|
||||
thread.handle_session_update(
|
||||
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,
|
||||
}),
|
||||
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",
|
||||
))]),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.unwrap();
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
@@ -3158,18 +3102,14 @@ mod tests {
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
|
||||
content: content.text.to_uppercase().into(),
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
|
||||
content.text.to_uppercase().into(),
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
@@ -3325,34 +3265,22 @@ mod tests {
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
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,
|
||||
}),
|
||||
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"})),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
})?;
|
||||
|
||||
// Now return refusal because of the tool result
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::Refusal))
|
||||
} else {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
}
|
||||
}
|
||||
.boxed_local()
|
||||
@@ -3380,16 +3308,7 @@ 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![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Hello".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
})],
|
||||
cx,
|
||||
)
|
||||
});
|
||||
let send_task = thread.update(cx, |thread, cx| thread.send(vec!["Hello".into()], cx));
|
||||
cx.background_executor.spawn(send_task).detach();
|
||||
cx.run_until_parked();
|
||||
|
||||
@@ -3435,21 +3354,11 @@ mod tests {
|
||||
let refuse_next = refuse_next.clone();
|
||||
move |_request, _thread, _cx| {
|
||||
if refuse_next.load(SeqCst) {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
async move { Ok(acp::PromptResponse::new(acp::StopReason::Refusal)) }
|
||||
.boxed_local()
|
||||
} else {
|
||||
async move {
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
}
|
||||
.boxed_local()
|
||||
async move { Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)) }
|
||||
.boxed_local()
|
||||
}
|
||||
}
|
||||
}));
|
||||
@@ -3506,10 +3415,7 @@ mod tests {
|
||||
let refuse_next = refuse_next.clone();
|
||||
async move {
|
||||
if refuse_next.load(SeqCst) {
|
||||
return Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
});
|
||||
return Ok(acp::PromptResponse::new(acp::StopReason::Refusal));
|
||||
}
|
||||
|
||||
let acp::ContentBlock::Text(content) = &request.prompt[0] else {
|
||||
@@ -3518,18 +3424,14 @@ mod tests {
|
||||
thread.update(&mut cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
|
||||
content: content.text.to_uppercase().into(),
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
|
||||
content.text.to_uppercase().into(),
|
||||
)),
|
||||
cx,
|
||||
)
|
||||
.unwrap();
|
||||
})?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
}
|
||||
.boxed_local()
|
||||
}
|
||||
@@ -3668,13 +3570,12 @@ mod tests {
|
||||
_cwd: &Path,
|
||||
cx: &mut App,
|
||||
) -> Task<gpui::Result<Entity<AcpThread>>> {
|
||||
let session_id = acp::SessionId(
|
||||
let session_id = acp::SessionId::new(
|
||||
rand::rng()
|
||||
.sample_iter(&distr::Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
.into(),
|
||||
.collect::<String>(),
|
||||
);
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()));
|
||||
let thread = cx.new(|cx| {
|
||||
@@ -3684,12 +3585,12 @@ mod tests {
|
||||
project,
|
||||
action_log,
|
||||
session_id.clone(),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
}),
|
||||
watch::Receiver::constant(
|
||||
acp::PromptCapabilities::new()
|
||||
.image(true)
|
||||
.audio(true)
|
||||
.embedded_context(true),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -3718,10 +3619,7 @@ mod tests {
|
||||
let thread = thread.clone();
|
||||
cx.spawn(async move |cx| handler(params, thread, cx.clone()).await)
|
||||
} else {
|
||||
Task::ready(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
}))
|
||||
Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::EndTurn)))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3776,17 +3674,13 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Try to update a tool call that doesn't exist
|
||||
let nonexistent_id = acp::ToolCallId("nonexistent-tool-call".into());
|
||||
let nonexistent_id = acp::ToolCallId::new("nonexistent-tool-call");
|
||||
thread.update(cx, |thread, cx| {
|
||||
let result = thread.handle_session_update(
|
||||
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate {
|
||||
id: nonexistent_id.clone(),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::ToolCallUpdate(acp::ToolCallUpdate::new(
|
||||
nonexistent_id.clone(),
|
||||
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::Completed),
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
|
||||
@@ -3861,7 +3755,7 @@ mod tests {
|
||||
.unwrap();
|
||||
|
||||
// Create 2 terminals BEFORE the checkpoint that have completed running
|
||||
let terminal_id_1 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
|
||||
let terminal_id_1 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
|
||||
let mock_terminal_1 = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
@@ -3900,17 +3794,13 @@ mod tests {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id: terminal_id_1.clone(),
|
||||
status: acp::TerminalExitStatus {
|
||||
exit_code: Some(0),
|
||||
signal: None,
|
||||
meta: None,
|
||||
},
|
||||
status: acp::TerminalExitStatus::new().exit_code(0),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
});
|
||||
|
||||
let terminal_id_2 = acp::TerminalId(uuid::Uuid::new_v4().to_string().into());
|
||||
let terminal_id_2 = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
|
||||
let mock_terminal_2 = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
@@ -3949,11 +3839,7 @@ mod tests {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id: terminal_id_2.clone(),
|
||||
status: acp::TerminalExitStatus {
|
||||
exit_code: Some(0),
|
||||
signal: None,
|
||||
meta: None,
|
||||
},
|
||||
status: acp::TerminalExitStatus::new().exit_code(0),
|
||||
},
|
||||
cx,
|
||||
);
|
||||
@@ -3973,7 +3859,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(uuid::Uuid::new_v4().to_string().into());
|
||||
let terminal_id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
|
||||
let mock_terminal = cx.new(|cx| {
|
||||
let builder = ::terminal::TerminalBuilder::new_display_only(
|
||||
::terminal::terminal_settings::CursorShape::default(),
|
||||
@@ -4015,21 +3901,15 @@ mod tests {
|
||||
thread.update(cx, |thread, cx| {
|
||||
thread
|
||||
.handle_session_update(
|
||||
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,
|
||||
}),
|
||||
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"})),
|
||||
),
|
||||
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(self.sessions.lock().len().to_string().into());
|
||||
let session_id = acp::SessionId::new(self.sessions.lock().len().to_string());
|
||||
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 {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
}),
|
||||
watch::Receiver::constant(
|
||||
acp::PromptCapabilities::new()
|
||||
.image(true)
|
||||
.audio(true)
|
||||
.embedded_context(true),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
});
|
||||
@@ -389,10 +389,7 @@ mod test_support {
|
||||
response_tx.replace(tx);
|
||||
cx.spawn(async move |_| {
|
||||
let stop_reason = rx.await?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(stop_reason))
|
||||
})
|
||||
} else {
|
||||
for update in self.next_prompt_updates.lock().drain(..) {
|
||||
@@ -400,7 +397,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.id)
|
||||
&& let Some(options) = self.permission_requests.get(&tool_call.tool_call_id)
|
||||
{
|
||||
Some((tool_call.clone(), options.clone()))
|
||||
} else {
|
||||
@@ -429,10 +426,7 @@ mod test_support {
|
||||
|
||||
cx.spawn(async move |_| {
|
||||
try_join_all(tasks).await?;
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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(thread_id.into()),
|
||||
id: acp::SessionId::new(thread_id),
|
||||
name,
|
||||
})
|
||||
} else if let Some(path) = path.strip_prefix("/agent/text-thread/") {
|
||||
|
||||
@@ -75,11 +75,15 @@ impl Terminal {
|
||||
|
||||
let exit_status = exit_status.map(portable_pty::ExitStatus::from);
|
||||
|
||||
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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
status
|
||||
})
|
||||
.shared(),
|
||||
}
|
||||
@@ -101,27 +105,23 @@ impl Terminal {
|
||||
|
||||
pub fn current_output(&self, cx: &App) -> acp::TerminalOutputResponse {
|
||||
if let Some(output) = self.output.as_ref() {
|
||||
let exit_status = output.exit_status.map(portable_pty::ExitStatus::from);
|
||||
|
||||
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,
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
acp::TerminalOutputResponse::new(
|
||||
output.content.clone(),
|
||||
output.original_content_len > output.content.len(),
|
||||
)
|
||||
.exit_status(exit_status)
|
||||
} else {
|
||||
let (current_content, original_len) = self.truncated_output(cx);
|
||||
|
||||
acp::TerminalOutputResponse {
|
||||
truncated: current_content.len() < original_len,
|
||||
output: current_content,
|
||||
exit_status: None,
|
||||
meta: None,
|
||||
}
|
||||
let truncated = current_content.len() < original_len;
|
||||
acp::TerminalOutputResponse::new(current_content, truncated)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -83,6 +83,7 @@ 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(format!("{}/{}", model.provider_id().0, model.id().0).into())
|
||||
acp::ModelId::new(format!("{}/{}", model.provider_id().0, model.id().0))
|
||||
}
|
||||
|
||||
fn authenticate_all_language_model_providers(cx: &mut App) -> Task<()> {
|
||||
@@ -789,28 +789,12 @@ impl NativeAgentConnection {
|
||||
}
|
||||
ThreadEvent::AgentText(text) => {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
false,
|
||||
cx,
|
||||
)
|
||||
thread.push_assistant_content_block(text.into(), false, cx)
|
||||
})?;
|
||||
}
|
||||
ThreadEvent::AgentThinking(text) => {
|
||||
acp_thread.update(cx, |thread, cx| {
|
||||
thread.push_assistant_content_block(
|
||||
acp::ContentBlock::Text(acp::TextContent {
|
||||
text,
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
true,
|
||||
cx,
|
||||
)
|
||||
thread.push_assistant_content_block(text.into(), true, cx)
|
||||
})?;
|
||||
}
|
||||
ThreadEvent::ToolCallAuthorization(ToolCallAuthorization {
|
||||
@@ -824,8 +808,9 @@ impl NativeAgentConnection {
|
||||
)
|
||||
})??;
|
||||
cx.background_spawn(async move {
|
||||
if let acp::RequestPermissionOutcome::Selected { option_id } =
|
||||
outcome_task.await
|
||||
if let acp::RequestPermissionOutcome::Selected(
|
||||
acp::SelectedPermissionOutcome { option_id, .. },
|
||||
) = outcome_task.await
|
||||
{
|
||||
response
|
||||
.send(option_id)
|
||||
@@ -852,10 +837,7 @@ impl NativeAgentConnection {
|
||||
}
|
||||
ThreadEvent::Stop(stop_reason) => {
|
||||
log::debug!("Assistant message complete: {:?}", stop_reason);
|
||||
return Ok(acp::PromptResponse {
|
||||
stop_reason,
|
||||
meta: None,
|
||||
});
|
||||
return Ok(acp::PromptResponse::new(stop_reason));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -867,10 +849,7 @@ impl NativeAgentConnection {
|
||||
}
|
||||
|
||||
log::debug!("Response stream completed");
|
||||
anyhow::Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::EndTurn,
|
||||
meta: None,
|
||||
})
|
||||
anyhow::Ok(acp::PromptResponse::new(acp::StopReason::EndTurn))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1374,7 +1353,7 @@ mod internal_tests {
|
||||
IndexMap::from_iter([(
|
||||
AgentModelGroupName("Fake".into()),
|
||||
vec![AgentModelInfo {
|
||||
id: acp::ModelId("fake/fake".into()),
|
||||
id: acp::ModelId::new("fake/fake"),
|
||||
name: "Fake".into(),
|
||||
description: None,
|
||||
icon: Some(ui::IconName::ZedAssistant),
|
||||
@@ -1435,7 +1414,7 @@ mod internal_tests {
|
||||
|
||||
// Select a model
|
||||
let selector = connection.model_selector(&session_id).unwrap();
|
||||
let model_id = acp::ModelId("fake/fake".into());
|
||||
let model_id = acp::ModelId::new("fake/fake");
|
||||
cx.update(|cx| selector.select_model(model_id.clone(), cx))
|
||||
.await
|
||||
.unwrap();
|
||||
@@ -1521,20 +1500,14 @@ mod internal_tests {
|
||||
thread.send(
|
||||
vec![
|
||||
"What does ".into(),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink {
|
||||
name: "b.md".into(),
|
||||
uri: MentionUri::File {
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
|
||||
"b.md",
|
||||
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(id),
|
||||
id: acp::SessionId::new(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 collections::HashMap;
|
||||
use eval_utils::{EvalOutput, EvalOutputProcessor, OutcomeKind};
|
||||
use fs::FakeFs;
|
||||
use futures::{FutureExt, future::LocalBoxFuture};
|
||||
use gpui::{AppContext, TestAppContext, Timer};
|
||||
@@ -20,16 +20,62 @@ 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() {
|
||||
@@ -55,22 +101,19 @@ 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(
|
||||
100,
|
||||
0.95,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
|
||||
run_eval(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,
|
||||
@@ -102,9 +145,9 @@ fn eval_extract_handle_command_output() {
|
||||
),
|
||||
],
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::assert_diff_any(possible_diffs),
|
||||
),
|
||||
);
|
||||
EvalAssertion::assert_diff_any(possible_diffs.clone()),
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -122,18 +165,16 @@ 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(
|
||||
100,
|
||||
0.95,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
|
||||
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
|
||||
run_eval(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,
|
||||
@@ -166,8 +207,8 @@ fn eval_delete_run_git_blame() {
|
||||
],
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::assert_eq(output_file_content),
|
||||
),
|
||||
);
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -185,18 +226,16 @@ 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(
|
||||
200,
|
||||
1.,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
|
||||
eval_utils::eval(200, 1., mismatched_tag_threshold(0.05), move || {
|
||||
run_eval(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,
|
||||
@@ -229,8 +268,8 @@ fn eval_translate_doc_comments() {
|
||||
],
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::judge_diff("Doc comments were translated to Italian"),
|
||||
),
|
||||
);
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -249,33 +288,31 @@ 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(
|
||||
100,
|
||||
0.95,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
|
||||
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.05), move || {
|
||||
run_eval(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,
|
||||
@@ -352,11 +389,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]
|
||||
@@ -380,11 +417,8 @@ 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(
|
||||
100,
|
||||
0.51,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
eval_utils::eval(100, 0.51, mismatched_tag_threshold(0.05), move || {
|
||||
run_eval(EvalInput::from_conversation(
|
||||
vec![
|
||||
message(User, [text("Let's research how to cursor blinking works.")]),
|
||||
message(
|
||||
@@ -421,10 +455,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,
|
||||
@@ -440,9 +474,9 @@ fn eval_disable_cursor_blinking() {
|
||||
),
|
||||
],
|
||||
Some(input_file_content.into()),
|
||||
EvalAssertion::assert_diff_any(possible_diffs),
|
||||
),
|
||||
);
|
||||
EvalAssertion::assert_diff_any(possible_diffs.clone()),
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -467,20 +501,16 @@ 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(
|
||||
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(
|
||||
|
||||
eval_utils::eval(100, 0.95, mismatched_tag_threshold(0.25), move || {
|
||||
run_eval(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,
|
||||
@@ -545,92 +575,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(
|
||||
@@ -648,11 +678,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]
|
||||
@@ -670,11 +700,9 @@ fn eval_zode() {
|
||||
let input_file_path = "root/zode.py";
|
||||
let input_content = None;
|
||||
let edit_description = "Create the main Zode CLI script";
|
||||
eval(
|
||||
50,
|
||||
1.,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
|
||||
eval_utils::eval(50, 1., mismatched_tag_threshold(0.05), move || {
|
||||
run_eval(EvalInput::from_conversation(
|
||||
vec![
|
||||
message(User, [text(include_str!("evals/fixtures/zode/prompt.md"))]),
|
||||
message(
|
||||
@@ -733,7 +761,7 @@ fn eval_zode() {
|
||||
],
|
||||
),
|
||||
],
|
||||
input_content,
|
||||
input_content.clone(),
|
||||
EvalAssertion::new(async move |sample, _, _cx| {
|
||||
let invalid_starts = [' ', '`', '\n'];
|
||||
let mut message = String::new();
|
||||
@@ -758,8 +786,8 @@ fn eval_zode() {
|
||||
})
|
||||
}
|
||||
}),
|
||||
),
|
||||
);
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -777,19 +805,17 @@ 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(
|
||||
200,
|
||||
0.5, // TODO: make this eval better
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
|
||||
eval_utils::eval(200, 0.5, mismatched_tag_threshold(0.05), move || {
|
||||
run_eval(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,
|
||||
@@ -809,81 +835,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(
|
||||
@@ -963,17 +989,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",
|
||||
@@ -990,8 +1016,8 @@ fn eval_add_overwrite_test() {
|
||||
EvalAssertion::judge_diff(
|
||||
"A new test for overwritten files was created, without changing any previous test",
|
||||
),
|
||||
),
|
||||
);
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
#[test]
|
||||
@@ -1016,20 +1042,18 @@ fn eval_create_empty_file() {
|
||||
|
||||
let input_file_content = None;
|
||||
let expected_output_content = String::new();
|
||||
eval(
|
||||
100,
|
||||
0.99,
|
||||
0.05,
|
||||
EvalInput::from_conversation(
|
||||
|
||||
eval_utils::eval(100, 0.99, mismatched_tag_threshold(0.05), move || {
|
||||
run_eval(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",
|
||||
@@ -1051,8 +1075,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",
|
||||
@@ -1065,12 +1089,12 @@ fn eval_create_empty_file() {
|
||||
],
|
||||
),
|
||||
],
|
||||
input_file_content,
|
||||
input_file_content.clone(),
|
||||
// Bad behavior is to write something like
|
||||
// "I'll create an empty TODO3 file as requested."
|
||||
EvalAssertion::assert_eq(expected_output_content),
|
||||
),
|
||||
);
|
||||
EvalAssertion::assert_eq(expected_output_content.clone()),
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
fn message(
|
||||
@@ -1312,115 +1336,44 @@ impl EvalAssertion {
|
||||
}
|
||||
}
|
||||
|
||||
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>>) {
|
||||
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 output = cx.executor().block_test(async {
|
||||
let result = cx.executor().block_test(async {
|
||||
let test = EditAgentTest::new(&mut cx).await;
|
||||
test.eval(eval, &mut cx).await
|
||||
});
|
||||
tx.send(output).unwrap();
|
||||
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,
|
||||
},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct EvalOutput {
|
||||
struct EditEvalOutput {
|
||||
sample: EvalSample,
|
||||
assertion: EvalAssertionOutcome,
|
||||
}
|
||||
|
||||
impl Display for EvalOutput {
|
||||
impl Display for EditEvalOutput {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
writeln!(f, "Score: {:?}", self.assertion.score)?;
|
||||
if let Some(message) = self.assertion.message.as_ref() {
|
||||
@@ -1439,22 +1392,6 @@ impl Display for EvalOutput {
|
||||
}
|
||||
}
|
||||
|
||||
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>,
|
||||
@@ -1550,7 +1487,10 @@ impl EditAgentTest {
|
||||
})
|
||||
}
|
||||
|
||||
async fn eval(&self, eval: EvalInput, cx: &mut TestAppContext) -> Result<EvalOutput> {
|
||||
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;
|
||||
|
||||
let path = self
|
||||
.project
|
||||
.read_with(cx, |project, cx| {
|
||||
@@ -1656,7 +1596,7 @@ impl EditAgentTest {
|
||||
.run(&sample, self.judge_model.clone(), cx)
|
||||
.await?;
|
||||
|
||||
Ok(EvalOutput { assertion, sample })
|
||||
Ok(EditEvalOutput { 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(id.as_str().into()),
|
||||
)),
|
||||
SerializedRecentOpen::AcpThread(id) => {
|
||||
Some(HistoryEntryId::AcpThread(acp::SessionId::new(id.as_str())))
|
||||
}
|
||||
SerializedRecentOpen::TextThread(file_name) => Some(
|
||||
HistoryEntryId::TextThread(text_threads_dir().join(file_name).into()),
|
||||
),
|
||||
|
||||
@@ -66,11 +66,9 @@ pub async fn get_buffer_content_or_outline(
|
||||
let outline_text = render_outline(outline_items, None, 0, usize::MAX).await?;
|
||||
|
||||
let text = if let Some(path) = path {
|
||||
format!(
|
||||
"# File outline for {path} (file too large to show full content)\n\n{outline_text}",
|
||||
)
|
||||
format!("# File outline for {path}\n\n{outline_text}",)
|
||||
} else {
|
||||
format!("# File outline (file too large to show full content)\n\n{outline_text}",)
|
||||
format!("# File outline\n\n{outline_text}",)
|
||||
};
|
||||
Ok(BufferContent {
|
||||
text,
|
||||
|
||||
@@ -6,33 +6,6 @@ The text you output will be saved verbatim as the content of the file.
|
||||
Tool calls have been disabled.
|
||||
Start your response with ```.
|
||||
|
||||
IMPORTANT: Output ONLY the file content between the backticks. Do NOT include:
|
||||
- The file path or name (like "path/to/file.txt")
|
||||
- Any markdown formatting within the content
|
||||
- Any explanatory text before or after the backticks
|
||||
- Any nested code fences within your output
|
||||
|
||||
<example>
|
||||
If asked to create a file with "hello" inside:
|
||||
|
||||
CORRECT output:
|
||||
```
|
||||
hello
|
||||
```
|
||||
|
||||
INCORRECT output (includes file path):
|
||||
```path/to/file.txt
|
||||
hello
|
||||
```
|
||||
|
||||
INCORRECT output (nested code fences):
|
||||
```
|
||||
```
|
||||
hello
|
||||
```
|
||||
```
|
||||
</example>
|
||||
|
||||
<file_path>
|
||||
{{path}}
|
||||
</file_path>
|
||||
|
||||
@@ -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].id.clone())
|
||||
.send(tool_call_auth_1.options[1].option_id.clone())
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
|
||||
// Reject the second
|
||||
tool_call_auth_2
|
||||
.response
|
||||
.send(tool_call_auth_1.options[2].id.clone())
|
||||
.send(tool_call_auth_1.options[2].option_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.id.0.to_string().into(),
|
||||
tool_use_id: tool_call_auth_1.tool_call.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.id.0.to_string().into(),
|
||||
tool_use_id: tool_call_auth_2.tool_call.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].id.clone())
|
||||
.send(tool_call_auth_3.options[0].option_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.id.0.to_string().into(),
|
||||
tool_use_id: tool_call_auth_3.tool_call.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.id);
|
||||
echo_id = Some(tool_call.tool_call_id);
|
||||
}
|
||||
}
|
||||
ThreadEvent::ToolCallUpdate(acp_thread::ToolCallUpdate::UpdateFields(
|
||||
acp::ToolCallUpdate {
|
||||
id,
|
||||
tool_call_id,
|
||||
fields:
|
||||
acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::Completed),
|
||||
..
|
||||
},
|
||||
meta: None,
|
||||
..
|
||||
},
|
||||
)) if Some(&id) == echo_id.as_ref() => {
|
||||
)) if Some(&tool_call_id) == echo_id.as_ref() => {
|
||||
echo_completed = true;
|
||||
}
|
||||
_ => {}
|
||||
@@ -1995,11 +1995,7 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
|
||||
.update(|cx| {
|
||||
connection.prompt(
|
||||
Some(acp_thread::UserMessageId::new()),
|
||||
acp::PromptRequest {
|
||||
session_id: session_id.clone(),
|
||||
prompt: vec!["ghi".into()],
|
||||
meta: None,
|
||||
},
|
||||
acp::PromptRequest::new(session_id.clone(), vec!["ghi".into()]),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -2056,68 +2052,50 @@ 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 {
|
||||
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" })),
|
||||
}
|
||||
acp::ToolCall::new("1", "Thinking")
|
||||
.kind(acp::ToolKind::Think)
|
||||
.raw_input(json!({}))
|
||||
.meta(acp::Meta::from_iter([(
|
||||
"tool_name".into(),
|
||||
"thinking".into()
|
||||
)]))
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
assert_eq!(
|
||||
update,
|
||||
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,
|
||||
}
|
||||
acp::ToolCallUpdate::new(
|
||||
"1",
|
||||
acp::ToolCallUpdateFields::new()
|
||||
.title("Thinking")
|
||||
.kind(acp::ToolKind::Think)
|
||||
.raw_input(json!({ "content": "Thinking hard!"}))
|
||||
)
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
assert_eq!(
|
||||
update,
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId("1".into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
status: Some(acp::ToolCallStatus::InProgress),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
acp::ToolCallUpdate::new(
|
||||
"1",
|
||||
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress)
|
||||
)
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
assert_eq!(
|
||||
update,
|
||||
acp::ToolCallUpdate {
|
||||
id: acp::ToolCallId("1".into()),
|
||||
fields: acp::ToolCallUpdateFields {
|
||||
content: Some(vec!["Thinking hard!".into()]),
|
||||
..Default::default()
|
||||
},
|
||||
meta: None,
|
||||
}
|
||||
acp::ToolCallUpdate::new(
|
||||
"1",
|
||||
acp::ToolCallUpdateFields::new().content(vec!["Thinking hard!".into()])
|
||||
)
|
||||
);
|
||||
let update = expect_tool_call_update_fields(&mut events).await;
|
||||
assert_eq!(
|
||||
update,
|
||||
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,
|
||||
}
|
||||
acp::ToolCallUpdate::new(
|
||||
"1",
|
||||
acp::ToolCallUpdateFields::new()
|
||||
.status(acp::ToolCallStatus::Completed)
|
||||
.raw_output("Finished thinking.".into())
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -619,12 +619,9 @@ 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 {
|
||||
meta: None,
|
||||
image,
|
||||
audio: false,
|
||||
embedded_context: true,
|
||||
}
|
||||
acp::PromptCapabilities::new()
|
||||
.image(image)
|
||||
.embedded_context(true)
|
||||
}
|
||||
|
||||
pub fn new(
|
||||
@@ -640,7 +637,7 @@ impl Thread {
|
||||
let (prompt_capabilities_tx, prompt_capabilities_rx) =
|
||||
watch::channel(Self::prompt_capabilities(model.as_deref()));
|
||||
Self {
|
||||
id: acp::SessionId(uuid::Uuid::new_v4().to_string().into()),
|
||||
id: acp::SessionId::new(uuid::Uuid::new_v4().to_string()),
|
||||
prompt_id: PromptId::new(),
|
||||
updated_at: Utc::now(),
|
||||
title: None,
|
||||
@@ -737,17 +734,11 @@ impl Thread {
|
||||
let Some(tool) = tool else {
|
||||
stream
|
||||
.0
|
||||
.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,
|
||||
})))
|
||||
.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()),
|
||||
)))
|
||||
.ok();
|
||||
return;
|
||||
};
|
||||
@@ -775,24 +766,20 @@ impl Thread {
|
||||
.log_err();
|
||||
}
|
||||
|
||||
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()
|
||||
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
|
||||
}
|
||||
},
|
||||
);
|
||||
));
|
||||
if let Some(output) = output {
|
||||
fields = fields.raw_output(output);
|
||||
}
|
||||
stream.update_tool_call_fields(&tool_use.id, fields);
|
||||
}
|
||||
|
||||
pub fn from_db(
|
||||
@@ -1272,18 +1259,15 @@ impl Thread {
|
||||
while let Some(tool_result) = tool_results.next().await {
|
||||
log::debug!("Tool finished {:?}", tool_result);
|
||||
|
||||
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()
|
||||
},
|
||||
);
|
||||
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);
|
||||
this.update(cx, |this, _cx| {
|
||||
this.pending_message()
|
||||
.tool_results
|
||||
@@ -1560,12 +1544,10 @@ impl Thread {
|
||||
} else {
|
||||
event_stream.update_tool_call_fields(
|
||||
&tool_use.id,
|
||||
acp::ToolCallUpdateFields {
|
||||
title: Some(title.into()),
|
||||
kind: Some(kind),
|
||||
raw_input: Some(tool_use.input.clone()),
|
||||
..Default::default()
|
||||
},
|
||||
acp::ToolCallUpdateFields::new()
|
||||
.title(title)
|
||||
.kind(kind)
|
||||
.raw_input(tool_use.input.clone()),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1587,10 +1569,9 @@ 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 {
|
||||
status: Some(acp::ToolCallStatus::InProgress),
|
||||
..Default::default()
|
||||
});
|
||||
tool_event_stream.update_fields(
|
||||
acp::ToolCallUpdateFields::new().status(acp::ToolCallStatus::InProgress),
|
||||
);
|
||||
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);
|
||||
@@ -2381,19 +2362,13 @@ impl ThreadEventStream {
|
||||
kind: acp::ToolKind,
|
||||
input: serde_json::Value,
|
||||
) -> acp::ToolCall {
|
||||
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,
|
||||
}
|
||||
acp::ToolCall::new(id.to_string(), title)
|
||||
.kind(kind)
|
||||
.raw_input(input)
|
||||
.meta(acp::Meta::from_iter([(
|
||||
"tool_name".into(),
|
||||
tool_name.into(),
|
||||
)]))
|
||||
}
|
||||
|
||||
fn update_tool_call_fields(
|
||||
@@ -2403,12 +2378,7 @@ impl ThreadEventStream {
|
||||
) {
|
||||
self.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
|
||||
acp::ToolCallUpdate {
|
||||
meta: None,
|
||||
id: acp::ToolCallId(tool_use_id.to_string().into()),
|
||||
fields,
|
||||
}
|
||||
.into(),
|
||||
acp::ToolCallUpdate::new(tool_use_id.to_string(), fields).into(),
|
||||
)))
|
||||
.ok();
|
||||
}
|
||||
@@ -2471,7 +2441,7 @@ impl ToolCallEventStream {
|
||||
.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCallUpdate(
|
||||
acp_thread::ToolCallUpdateDiff {
|
||||
id: acp::ToolCallId(self.tool_use_id.to_string().into()),
|
||||
id: acp::ToolCallId::new(self.tool_use_id.to_string()),
|
||||
diff,
|
||||
}
|
||||
.into(),
|
||||
@@ -2489,33 +2459,26 @@ impl ToolCallEventStream {
|
||||
.0
|
||||
.unbounded_send(Ok(ThreadEvent::ToolCallAuthorization(
|
||||
ToolCallAuthorization {
|
||||
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()
|
||||
},
|
||||
},
|
||||
tool_call: acp::ToolCallUpdate::new(
|
||||
self.tool_use_id.to_string(),
|
||||
acp::ToolCallUpdateFields::new().title(title),
|
||||
),
|
||||
options: vec![
|
||||
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,
|
||||
},
|
||||
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,
|
||||
),
|
||||
],
|
||||
response: response_tx,
|
||||
},
|
||||
@@ -2660,7 +2623,15 @@ 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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2668,32 +2639,15 @@ impl UserMessageContent {
|
||||
impl From<UserMessageContent> for acp::ContentBlock {
|
||||
fn from(content: UserMessageContent) -> Self {
|
||||
match content {
|
||||
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::Text(text) => text.into(),
|
||||
UserMessageContent::Image(image) => {
|
||||
acp::ContentBlock::Image(acp::ImageContent::new(image.source, "image/png"))
|
||||
}
|
||||
UserMessageContent::Mention { uri, content } => acp::ContentBlock::Resource(
|
||||
acp::EmbeddedResource::new(acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents::new(content, uri.to_uri().to_string()),
|
||||
)),
|
||||
),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -44,16 +44,8 @@ pub struct EditFileToolInput {
|
||||
///
|
||||
/// NEVER mention the file path in this description.
|
||||
///
|
||||
/// IMPORTANT: Do NOT include markdown code fences (```) or other markdown formatting in this description.
|
||||
/// Just describe what should be done - another model will generate the actual content.
|
||||
///
|
||||
/// <example>Fix API endpoint URLs</example>
|
||||
/// <example>Update copyright year in `page_footer`</example>
|
||||
/// <example>Create a Python script that prints hello world</example>
|
||||
///
|
||||
/// INCORRECT examples (do not do this):
|
||||
/// <example>Create a file with:\n```python\nprint('hello')\n```</example>
|
||||
/// <example>Add this code:\n```\nif err:\n return\n```</example>
|
||||
///
|
||||
/// Make sure to include this field before all the others in the input object so that we can display it immediately.
|
||||
pub display_description: String,
|
||||
@@ -281,14 +273,9 @@ 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 {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path,
|
||||
line: None,
|
||||
meta: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
event_stream.update_fields(
|
||||
ToolCallUpdateFields::new().locations(vec![acp::ToolCallLocation::new(abs_path)]),
|
||||
);
|
||||
}
|
||||
|
||||
let authorize = self.authorize(&input, &event_stream, cx);
|
||||
@@ -397,10 +384,11 @@ impl AgentTool for EditFileTool {
|
||||
range.start.to_point(&buffer.snapshot()).row
|
||||
}).ok();
|
||||
if let Some(abs_path) = abs_path.clone() {
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
locations: Some(vec![ToolCallLocation { path: abs_path, line, meta: None }]),
|
||||
..Default::default()
|
||||
});
|
||||
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]));
|
||||
}
|
||||
emitted_location = true;
|
||||
}
|
||||
|
||||
@@ -118,33 +118,29 @@ 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 {
|
||||
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()
|
||||
});
|
||||
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(),
|
||||
),
|
||||
);
|
||||
|
||||
Ok(FindPathToolOutput {
|
||||
offset: input.offset,
|
||||
|
||||
@@ -17,6 +17,9 @@ use crate::{AgentTool, Thread, ToolCallEventStream, outline};
|
||||
/// Reads the content of the given file in the project.
|
||||
///
|
||||
/// - Never attempt to read a path that hasn't been previously mentioned.
|
||||
/// - For large files, this tool returns a file outline with symbol names and line numbers instead of the full content.
|
||||
/// This outline IS a successful response - use the line numbers to read specific sections with start_line/end_line.
|
||||
/// Do NOT retry reading the same file without line numbers if you receive an outline.
|
||||
#[derive(Debug, Serialize, Deserialize, JsonSchema)]
|
||||
pub struct ReadFileToolInput {
|
||||
/// The relative path of the file to read.
|
||||
@@ -149,15 +152,12 @@ 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 {
|
||||
locations: Some(vec![acp::ToolCallLocation {
|
||||
path: abs_path.clone(),
|
||||
line: input.start_line.map(|line| line.saturating_sub(1)),
|
||||
meta: None,
|
||||
}]),
|
||||
..Default::default()
|
||||
});
|
||||
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
|
||||
|
||||
if image_store::is_image_file(&self.project, &project_path, cx) {
|
||||
return cx.spawn(async move |cx| {
|
||||
@@ -254,16 +254,15 @@ impl AgentTool for ReadFileTool {
|
||||
|
||||
if buffer_content.is_outline {
|
||||
Ok(formatdoc! {"
|
||||
This file was too big to read all at once.
|
||||
SUCCESS: File outline retrieved. This file is too large to read all at once, so the outline below shows the file's structure with line numbers.
|
||||
|
||||
IMPORTANT: Do NOT retry this call without line numbers - you will get the same outline.
|
||||
Instead, use the line numbers below to read specific sections by calling this tool again with start_line and end_line parameters.
|
||||
|
||||
{}
|
||||
|
||||
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.
|
||||
|
||||
Alternatively, you can fall back to the `grep` tool (if available)
|
||||
to search the file for specific content.", buffer_content.text
|
||||
NEXT STEPS: To read a specific symbol's implementation, call read_file with the same path plus start_line and end_line from the outline above.
|
||||
For example, to read a function shown as [L100-150], use start_line: 100 and end_line: 150.", buffer_content.text
|
||||
}
|
||||
.into())
|
||||
} else {
|
||||
@@ -287,12 +286,9 @@ impl AgentTool for ReadFileTool {
|
||||
text,
|
||||
}
|
||||
.to_string();
|
||||
event_stream.update_fields(ToolCallUpdateFields {
|
||||
content: Some(vec![acp::ToolCallContent::Content {
|
||||
content: markdown.into(),
|
||||
}]),
|
||||
..Default::default()
|
||||
})
|
||||
event_stream.update_fields(ToolCallUpdateFields::new().content(vec![
|
||||
acp::ToolCallContent::Content(acp::Content::new(markdown)),
|
||||
]));
|
||||
}
|
||||
})?;
|
||||
|
||||
@@ -440,7 +436,7 @@ mod test {
|
||||
let content = result.to_str().unwrap();
|
||||
|
||||
assert_eq!(
|
||||
content.lines().skip(4).take(6).collect::<Vec<_>>(),
|
||||
content.lines().skip(7).take(6).collect::<Vec<_>>(),
|
||||
vec![
|
||||
"struct Test0 [L1-4]",
|
||||
" a [L2]",
|
||||
@@ -475,7 +471,7 @@ mod test {
|
||||
pretty_assertions::assert_eq!(
|
||||
content
|
||||
.lines()
|
||||
.skip(4)
|
||||
.skip(7)
|
||||
.take(expected_content.len())
|
||||
.collect::<Vec<_>>(),
|
||||
expected_content
|
||||
|
||||
@@ -112,10 +112,9 @@ impl AgentTool for TerminalTool {
|
||||
.await?;
|
||||
|
||||
let terminal_id = terminal.id(cx)?;
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![acp::ToolCallContent::Terminal { terminal_id }]),
|
||||
..Default::default()
|
||||
});
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields::new().content(vec![
|
||||
acp::ToolCallContent::Terminal(acp::Terminal::new(terminal_id)),
|
||||
]));
|
||||
|
||||
let exit_status = terminal.wait_for_exit(cx)?.await;
|
||||
let output = terminal.current_output(cx)?;
|
||||
|
||||
@@ -43,10 +43,8 @@ impl AgentTool for ThinkingTool {
|
||||
event_stream: ToolCallEventStream,
|
||||
_cx: &mut App,
|
||||
) -> Task<Result<String>> {
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
content: Some(vec![input.content.into()]),
|
||||
..Default::default()
|
||||
});
|
||||
event_stream
|
||||
.update_fields(acp::ToolCallUpdateFields::new().content(vec![input.content.into()]));
|
||||
Task::ready(Ok("Finished thinking.".to_string()))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -76,10 +76,8 @@ impl AgentTool for WebSearchTool {
|
||||
let response = match search_task.await {
|
||||
Ok(response) => response,
|
||||
Err(err) => {
|
||||
event_stream.update_fields(acp::ToolCallUpdateFields {
|
||||
title: Some("Web Search Failed".to_string()),
|
||||
..Default::default()
|
||||
});
|
||||
event_stream
|
||||
.update_fields(acp::ToolCallUpdateFields::new().title("Web Search Failed"));
|
||||
return Err(err);
|
||||
}
|
||||
};
|
||||
@@ -107,26 +105,23 @@ fn emit_update(response: &WebSearchResponse, event_stream: &ToolCallEventStream)
|
||||
} else {
|
||||
format!("{} results", response.results.len())
|
||||
};
|
||||
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()
|
||||
});
|
||||
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(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -76,7 +76,7 @@ pub async fn connect(
|
||||
Ok(Rc::new(conn) as _)
|
||||
}
|
||||
|
||||
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
|
||||
const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::ProtocolVersion::V1;
|
||||
|
||||
impl AcpConnection {
|
||||
pub async fn stdio(
|
||||
@@ -173,29 +173,27 @@ 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 {
|
||||
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,
|
||||
})
|
||||
.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),
|
||||
)
|
||||
.await?;
|
||||
|
||||
if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
|
||||
@@ -253,14 +251,13 @@ 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,
|
||||
..
|
||||
@@ -268,47 +265,41 @@ impl AgentConnection for AcpConnection {
|
||||
| project::context_server_store::ContextServerConfiguration::Extension {
|
||||
command,
|
||||
..
|
||||
} => 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![]
|
||||
},
|
||||
}),
|
||||
} => 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![]
|
||||
}),
|
||||
)),
|
||||
project::context_server_store::ContextServerConfiguration::Http {
|
||||
url,
|
||||
headers,
|
||||
} => 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(),
|
||||
}),
|
||||
} => 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(),
|
||||
),
|
||||
)),
|
||||
}
|
||||
})
|
||||
.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 { mcp_servers, cwd, meta: None })
|
||||
.new_session(acp::NewSessionRequest::new(cwd).mcp_servers(mcp_servers))
|
||||
.await
|
||||
.map_err(|err| {
|
||||
if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
|
||||
@@ -341,11 +332,7 @@ impl AgentConnection for AcpConnection {
|
||||
let modes = modes.clone();
|
||||
let conn = conn.clone();
|
||||
async move |_| {
|
||||
let result = conn.set_session_mode(acp::SetSessionModeRequest {
|
||||
session_id,
|
||||
mode_id: default_mode,
|
||||
meta: None,
|
||||
})
|
||||
let result = conn.set_session_mode(acp::SetSessionModeRequest::new(session_id, default_mode))
|
||||
.await.log_err();
|
||||
|
||||
if result.is_none() {
|
||||
@@ -388,11 +375,7 @@ impl AgentConnection for AcpConnection {
|
||||
let models = models.clone();
|
||||
let conn = conn.clone();
|
||||
async move |_| {
|
||||
let result = conn.set_session_model(acp::SetSessionModelRequest {
|
||||
session_id,
|
||||
model_id: default_model,
|
||||
meta: None,
|
||||
})
|
||||
let result = conn.set_session_model(acp::SetSessionModelRequest::new(session_id, default_model))
|
||||
.await.log_err();
|
||||
|
||||
if result.is_none() {
|
||||
@@ -456,12 +439,8 @@ 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 {
|
||||
method_id: method_id.clone(),
|
||||
meta: None,
|
||||
})
|
||||
.await?;
|
||||
|
||||
conn.authenticate(acp::AuthenticateRequest::new(method_id))
|
||||
.await?;
|
||||
Ok(())
|
||||
})
|
||||
}
|
||||
@@ -515,10 +494,7 @@ impl AgentConnection for AcpConnection {
|
||||
&& (details.contains("This operation was aborted")
|
||||
|| details.contains("The user aborted a request"))
|
||||
{
|
||||
Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Cancelled,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::PromptResponse::new(acp::StopReason::Cancelled))
|
||||
} else {
|
||||
Err(anyhow!(details))
|
||||
}
|
||||
@@ -535,10 +511,7 @@ impl AgentConnection for AcpConnection {
|
||||
session.suppress_abort_err = true;
|
||||
}
|
||||
let conn = self.connection.clone();
|
||||
let params = acp::CancelNotification {
|
||||
session_id: session_id.clone(),
|
||||
meta: None,
|
||||
};
|
||||
let params = acp::CancelNotification::new(session_id.clone());
|
||||
cx.foreground_executor()
|
||||
.spawn(async move { conn.cancel(params).await })
|
||||
.detach();
|
||||
@@ -619,11 +592,7 @@ 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 {
|
||||
session_id,
|
||||
mode_id,
|
||||
meta: None,
|
||||
})
|
||||
.set_session_mode(acp::SetSessionModeRequest::new(session_id, mode_id))
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
@@ -682,11 +651,7 @@ 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 {
|
||||
session_id,
|
||||
model_id,
|
||||
meta: None,
|
||||
})
|
||||
.set_session_model(acp::SetSessionModelRequest::new(session_id, model_id))
|
||||
.await;
|
||||
|
||||
if result.is_err() {
|
||||
@@ -748,10 +713,7 @@ impl acp::Client for ClientDelegate {
|
||||
|
||||
let outcome = task.await;
|
||||
|
||||
Ok(acp::RequestPermissionResponse {
|
||||
outcome,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::RequestPermissionResponse::new(outcome))
|
||||
}
|
||||
|
||||
async fn write_text_file(
|
||||
@@ -783,10 +745,7 @@ impl acp::Client for ClientDelegate {
|
||||
|
||||
let content = task.await?;
|
||||
|
||||
Ok(acp::ReadTextFileResponse {
|
||||
content,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::ReadTextFileResponse::new(content))
|
||||
}
|
||||
|
||||
async fn session_notification(
|
||||
@@ -821,7 +780,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(id_str.into());
|
||||
let terminal_id = acp::TerminalId::new(id_str);
|
||||
let cwd = terminal_info
|
||||
.get("cwd")
|
||||
.and_then(|v| v.as_str().map(PathBuf::from));
|
||||
@@ -837,7 +796,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.clone(),
|
||||
terminal_id,
|
||||
label: tc.title.clone(),
|
||||
cwd,
|
||||
output_byte_limit: None,
|
||||
@@ -862,15 +821,12 @@ 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(id_str.into());
|
||||
let terminal_id = acp::TerminalId::new(id_str);
|
||||
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: terminal_id.clone(),
|
||||
data,
|
||||
},
|
||||
TerminalProviderEvent::Output { terminal_id, data },
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -881,21 +837,19 @@ 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(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 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 _ = session.thread.update(&mut self.cx.clone(), |thread, cx| {
|
||||
thread.on_terminal_provider_event(
|
||||
TerminalProviderEvent::Exit {
|
||||
terminal_id: terminal_id.clone(),
|
||||
terminal_id,
|
||||
status,
|
||||
},
|
||||
cx,
|
||||
@@ -932,7 +886,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(uuid::Uuid::new_v4().to_string().into()),
|
||||
acp::TerminalId::new(uuid::Uuid::new_v4().to_string()),
|
||||
format!("{} {}", args.command, args.args.join(" ")),
|
||||
args.cwd.clone(),
|
||||
args.output_byte_limit,
|
||||
@@ -942,10 +896,7 @@ impl acp::Client for ClientDelegate {
|
||||
})?;
|
||||
let terminal_id =
|
||||
terminal_entity.read_with(&self.cx, |terminal, _| terminal.id().clone())?;
|
||||
Ok(acp::CreateTerminalResponse {
|
||||
terminal_id,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::CreateTerminalResponse::new(terminal_id))
|
||||
}
|
||||
|
||||
async fn kill_terminal_command(
|
||||
@@ -1006,10 +957,7 @@ impl acp::Client for ClientDelegate {
|
||||
})??
|
||||
.await;
|
||||
|
||||
Ok(acp::WaitForTerminalExitResponse {
|
||||
exit_status,
|
||||
meta: None,
|
||||
})
|
||||
Ok(acp::WaitForTerminalExitResponse::new(exit_status))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -41,7 +41,7 @@ impl AgentServer for ClaudeCode {
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
|
||||
.and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
|
||||
}
|
||||
|
||||
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(|m| acp::ModelId(m.into())))
|
||||
.and_then(|s| s.default_model.clone().map(acp::ModelId::new))
|
||||
}
|
||||
|
||||
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(|m| acp::SessionModeId(m.into())))
|
||||
.and_then(|s| s.default_mode.clone().map(acp::SessionModeId::new))
|
||||
}
|
||||
|
||||
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(|m| acp::ModelId(m.into())))
|
||||
.and_then(|s| s.default_model.clone().map(acp::ModelId::new))
|
||||
}
|
||||
|
||||
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(|m| acp::SessionModeId(m.into())))
|
||||
.and_then(|s| s.default_mode().map(acp::SessionModeId::new))
|
||||
}
|
||||
|
||||
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(|m| acp::ModelId(m.into())))
|
||||
.and_then(|s| s.default_model().map(acp::ModelId::new))
|
||||
}
|
||||
|
||||
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
|
||||
|
||||
@@ -82,26 +82,9 @@ where
|
||||
.update(cx, |thread, cx| {
|
||||
thread.send(
|
||||
vec![
|
||||
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,
|
||||
}),
|
||||
"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(),
|
||||
],
|
||||
cx,
|
||||
)
|
||||
@@ -429,7 +412,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($allow_option_id.into()),
|
||||
::agent_client_protocol::PermissionOptionId::new($allow_option_id),
|
||||
cx,
|
||||
)
|
||||
.await;
|
||||
|
||||
@@ -13,7 +13,8 @@ path = "src/agent_ui.rs"
|
||||
doctest = false
|
||||
|
||||
[features]
|
||||
test-support = ["gpui/test-support", "language/test-support"]
|
||||
test-support = ["gpui/test-support", "language/test-support", "reqwest_client"]
|
||||
unit-eval = []
|
||||
|
||||
[dependencies]
|
||||
acp_thread.workspace = true
|
||||
@@ -47,6 +48,7 @@ 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
|
||||
@@ -98,14 +100,17 @@ 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"] }
|
||||
@@ -115,5 +120,6 @@ 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,24 +432,11 @@ mod tests {
|
||||
let (workspace, cx) =
|
||||
cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
|
||||
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 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 connection = Rc::new(StubAgentConnection::new());
|
||||
let thread = cx
|
||||
.update(|_, cx| {
|
||||
|
||||
@@ -225,8 +225,13 @@ impl MessageEditor {
|
||||
.iter()
|
||||
.find(|command| command.name == command_name)?;
|
||||
|
||||
let acp::AvailableCommandInput::Unstructured { mut hint } =
|
||||
available_command.input.clone()?;
|
||||
let acp::AvailableCommandInput::Unstructured(acp::UnstructuredCommandInput {
|
||||
mut hint,
|
||||
..
|
||||
}) = available_command.input.clone()?
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
|
||||
let mut hint_pos = MultiBufferOffset(parsed_command.source_range.end) + 1usize;
|
||||
if hint_pos > snapshot.len() {
|
||||
@@ -403,34 +408,28 @@ impl MessageEditor {
|
||||
} => {
|
||||
all_tracked_buffers.extend(tracked_buffers.iter().cloned());
|
||||
if supports_embedded_context {
|
||||
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,
|
||||
},
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
|
||||
acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents::new(
|
||||
content.clone(),
|
||||
uri.to_uri().to_string(),
|
||||
),
|
||||
meta: None,
|
||||
})
|
||||
),
|
||||
))
|
||||
} else {
|
||||
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,
|
||||
})
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
|
||||
uri.name(),
|
||||
uri.to_uri().to_string(),
|
||||
))
|
||||
}
|
||||
}
|
||||
Mention::Image(mention_image) => {
|
||||
let uri = match uri {
|
||||
let mut image = acp::ImageContent::new(
|
||||
mention_image.data.clone(),
|
||||
mention_image.format.mime_type(),
|
||||
);
|
||||
|
||||
if let Some(uri) = match uri {
|
||||
MentionUri::File { .. } => Some(uri.to_uri().to_string()),
|
||||
MentionUri::PastedImage => None,
|
||||
other => {
|
||||
@@ -440,25 +439,14 @@ impl MessageEditor {
|
||||
);
|
||||
None
|
||||
}
|
||||
} {
|
||||
image = image.uri(uri)
|
||||
};
|
||||
acp::ContentBlock::Image(acp::ImageContent {
|
||||
annotations: None,
|
||||
data: mention_image.data.to_string(),
|
||||
mime_type: mention_image.format.mime_type().into(),
|
||||
uri,
|
||||
meta: None,
|
||||
})
|
||||
acp::ContentBlock::Image(image)
|
||||
}
|
||||
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,
|
||||
}),
|
||||
Mention::Link => acp::ContentBlock::ResourceLink(
|
||||
acp::ResourceLink::new(uri.name(), uri.to_uri().to_string()),
|
||||
),
|
||||
};
|
||||
chunks.push(chunk);
|
||||
ix = crease_range.end.0;
|
||||
@@ -746,8 +734,7 @@ impl MessageEditor {
|
||||
uri,
|
||||
data,
|
||||
mime_type,
|
||||
annotations: _,
|
||||
meta: _,
|
||||
..
|
||||
}) => {
|
||||
let mention_uri = if let Some(uri) = uri {
|
||||
MentionUri::parse(&uri, path_style)
|
||||
@@ -773,7 +760,7 @@ impl MessageEditor {
|
||||
}),
|
||||
));
|
||||
}
|
||||
acp::ContentBlock::Audio(_) | acp::ContentBlock::Resource(_) => {}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1092,12 +1079,7 @@ 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 {
|
||||
name: "help".to_string(),
|
||||
description: "Get help".to_string(),
|
||||
input: None,
|
||||
meta: None,
|
||||
}]);
|
||||
available_commands.replace(vec![acp::AvailableCommand::new("help", "Get help")]);
|
||||
|
||||
// Test that unsupported slash commands trigger an error when we have a list of available commands
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
@@ -1211,20 +1193,12 @@ 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 {
|
||||
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,
|
||||
},
|
||||
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>",
|
||||
)),
|
||||
),
|
||||
]));
|
||||
|
||||
let editor = workspace.update_in(&mut cx, |workspace, window, cx| {
|
||||
@@ -1504,12 +1478,12 @@ mod tests {
|
||||
editor.set_text("", window, cx);
|
||||
});
|
||||
|
||||
prompt_capabilities.replace(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
});
|
||||
prompt_capabilities.replace(
|
||||
acp::PromptCapabilities::new()
|
||||
.image(true)
|
||||
.audio(true)
|
||||
.embedded_context(true),
|
||||
);
|
||||
|
||||
cx.simulate_input("Lorem ");
|
||||
|
||||
@@ -1960,11 +1934,9 @@ mod tests {
|
||||
cx,
|
||||
);
|
||||
// Enable embedded context so files are actually included
|
||||
editor.prompt_capabilities.replace(acp::PromptCapabilities {
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
..Default::default()
|
||||
});
|
||||
editor
|
||||
.prompt_capabilities
|
||||
.replace(acp::PromptCapabilities::new().embedded_context(true));
|
||||
editor
|
||||
})
|
||||
});
|
||||
@@ -2043,7 +2015,7 @@ mod tests {
|
||||
|
||||
// Create a thread metadata to insert as summary
|
||||
let thread_metadata = agent::DbThreadMetadata {
|
||||
id: acp::SessionId("thread-123".into()),
|
||||
id: acp::SessionId::new("thread-123"),
|
||||
title: "Previous Conversation".into(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
};
|
||||
@@ -2150,14 +2122,7 @@ mod tests {
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
content,
|
||||
vec![acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "してhello world".into(),
|
||||
annotations: None,
|
||||
meta: None
|
||||
})]
|
||||
);
|
||||
assert_eq!(content, vec!["してhello world".into()]);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -2236,38 +2201,24 @@ mod tests {
|
||||
.0;
|
||||
|
||||
let main_rs_uri = if cfg!(windows) {
|
||||
"file:///C:/project/src/main.rs".to_string()
|
||||
"file:///C:/project/src/main.rs"
|
||||
} else {
|
||||
"file:///project/src/main.rs".to_string()
|
||||
"file:///project/src/main.rs"
|
||||
};
|
||||
|
||||
// When embedded context is `false` we should get a resource link
|
||||
pretty_assertions::assert_eq!(
|
||||
content,
|
||||
vec![
|
||||
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,
|
||||
})
|
||||
"What is in ".into(),
|
||||
acp::ContentBlock::ResourceLink(acp::ResourceLink::new("main.rs", main_rs_uri))
|
||||
]
|
||||
);
|
||||
|
||||
message_editor.update(cx, |editor, _cx| {
|
||||
editor.prompt_capabilities.replace(acp::PromptCapabilities {
|
||||
embedded_context: true,
|
||||
..Default::default()
|
||||
})
|
||||
editor
|
||||
.prompt_capabilities
|
||||
.replace(acp::PromptCapabilities::new().embedded_context(true))
|
||||
});
|
||||
|
||||
let content = message_editor
|
||||
@@ -2280,23 +2231,12 @@ mod tests {
|
||||
pretty_assertions::assert_eq!(
|
||||
content,
|
||||
vec![
|
||||
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
|
||||
})
|
||||
"What is in ".into(),
|
||||
acp::ContentBlock::Resource(acp::EmbeddedResource::new(
|
||||
acp::EmbeddedResourceResource::TextResourceContents(
|
||||
acp::TextResourceContents::new(file_content, main_rs_uri)
|
||||
)
|
||||
))
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -161,7 +161,7 @@ impl Render for ModeSelector {
|
||||
.map(|mode| mode.name.clone())
|
||||
.unwrap_or_else(|| "Unknown".into());
|
||||
|
||||
let this = cx.entity();
|
||||
let this = cx.weak_entity();
|
||||
|
||||
let icon = if self.menu_handle.is_deployed() {
|
||||
IconName::ChevronUp
|
||||
@@ -222,7 +222,8 @@ impl Render for ModeSelector {
|
||||
y: px(-2.0),
|
||||
})
|
||||
.menu(move |window, cx| {
|
||||
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
|
||||
this.update(cx, |this, cx| this.build_context_menu(window, cx))
|
||||
.ok()
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -464,7 +464,7 @@ mod tests {
|
||||
models
|
||||
.into_iter()
|
||||
.map(|model| acp_thread::AgentModelInfo {
|
||||
id: acp::ModelId(model.to_string().into()),
|
||||
id: acp::ModelId::new(model.to_string()),
|
||||
name: model.to_string().into(),
|
||||
description: None,
|
||||
icon: None,
|
||||
|
||||
@@ -498,17 +498,7 @@ impl AcpThreadView {
|
||||
Some(new_version_available_tx),
|
||||
);
|
||||
|
||||
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 connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
|
||||
let load_task = cx.spawn_in(window, async move |this, cx| {
|
||||
let connection = match connect_task.await {
|
||||
Ok((connection, login)) => {
|
||||
@@ -1486,18 +1476,8 @@ impl AcpThreadView {
|
||||
.iter()
|
||||
.any(|method| method.id.0.as_ref() == "claude-login")
|
||||
{
|
||||
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,
|
||||
});
|
||||
available_commands.push(acp::AvailableCommand::new("login", "Authenticate"));
|
||||
available_commands.push(acp::AvailableCommand::new("logout", "Authenticate"));
|
||||
}
|
||||
|
||||
let has_commands = !available_commands.is_empty();
|
||||
@@ -2572,7 +2552,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)
|
||||
@@ -2824,7 +2804,7 @@ impl AcpThreadView {
|
||||
})
|
||||
.gap_0p5()
|
||||
.children(options.iter().map(move |option| {
|
||||
let option_id = SharedString::from(option.id.0.clone());
|
||||
let option_id = SharedString::from(option.option_id.0.clone());
|
||||
Button::new((option_id, entry_ix), option.name.clone())
|
||||
.map(|this| {
|
||||
let (this, action) = match option.kind {
|
||||
@@ -2840,7 +2820,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)
|
||||
}
|
||||
};
|
||||
@@ -2865,7 +2845,7 @@ impl AcpThreadView {
|
||||
.label_size(LabelSize::Small)
|
||||
.on_click(cx.listener({
|
||||
let tool_call_id = tool_call_id.clone();
|
||||
let option_id = option.id.clone();
|
||||
let option_id = option.option_id.clone();
|
||||
let option_kind = option.kind;
|
||||
move |this, _, window, cx| {
|
||||
this.authorize_tool_call(
|
||||
@@ -3553,7 +3533,7 @@ impl AcpThreadView {
|
||||
);
|
||||
|
||||
this.authenticate(
|
||||
acp::AuthMethodId(method_id.clone()),
|
||||
acp::AuthMethodId::new(method_id.clone()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -3847,10 +3827,6 @@ 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)
|
||||
@@ -3864,6 +3840,12 @@ 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(),
|
||||
@@ -4437,7 +4419,7 @@ impl AcpThreadView {
|
||||
|
||||
self.authorize_tool_call(
|
||||
tool_call.id.clone(),
|
||||
option.id.clone(),
|
||||
option.option_id.clone(),
|
||||
option.kind,
|
||||
window,
|
||||
cx,
|
||||
@@ -6253,27 +6235,18 @@ pub(crate) mod tests {
|
||||
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
|
||||
init_test(cx);
|
||||
|
||||
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 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 connection =
|
||||
StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
|
||||
tool_call_id,
|
||||
vec![acp::PermissionOption {
|
||||
id: acp::PermissionOptionId("1".into()),
|
||||
name: "Allow".into(),
|
||||
kind: acp::PermissionOptionKind::AllowOnce,
|
||||
meta: None,
|
||||
}],
|
||||
vec![acp::PermissionOption::new(
|
||||
"1".into(),
|
||||
"Allow",
|
||||
acp::PermissionOptionKind::AllowOnce,
|
||||
)],
|
||||
)]));
|
||||
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
|
||||
@@ -6492,10 +6465,7 @@ pub(crate) mod tests {
|
||||
fn default_response() -> Self {
|
||||
let conn = StubAgentConnection::new();
|
||||
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk {
|
||||
content: "Default response".into(),
|
||||
meta: None,
|
||||
},
|
||||
acp::ContentChunk::new("Default response".into()),
|
||||
)]);
|
||||
Self::new(conn)
|
||||
}
|
||||
@@ -6552,13 +6522,13 @@ pub(crate) mod tests {
|
||||
self,
|
||||
project,
|
||||
action_log,
|
||||
SessionId("test".into()),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
}),
|
||||
SessionId::new("test"),
|
||||
watch::Receiver::constant(
|
||||
acp::PromptCapabilities::new()
|
||||
.image(true)
|
||||
.audio(true)
|
||||
.embedded_context(true),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
@@ -6616,13 +6586,13 @@ pub(crate) mod tests {
|
||||
self,
|
||||
project,
|
||||
action_log,
|
||||
SessionId("test".into()),
|
||||
watch::Receiver::constant(acp::PromptCapabilities {
|
||||
image: true,
|
||||
audio: true,
|
||||
embedded_context: true,
|
||||
meta: None,
|
||||
}),
|
||||
SessionId::new("test"),
|
||||
watch::Receiver::constant(
|
||||
acp::PromptCapabilities::new()
|
||||
.image(true)
|
||||
.audio(true)
|
||||
.embedded_context(true),
|
||||
),
|
||||
cx,
|
||||
)
|
||||
})))
|
||||
@@ -6646,10 +6616,7 @@ pub(crate) mod tests {
|
||||
_params: acp::PromptRequest,
|
||||
_cx: &mut App,
|
||||
) -> Task<gpui::Result<acp::PromptResponse>> {
|
||||
Task::ready(Ok(acp::PromptResponse {
|
||||
stop_reason: acp::StopReason::Refusal,
|
||||
meta: None,
|
||||
}))
|
||||
Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
|
||||
}
|
||||
|
||||
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
|
||||
@@ -6717,24 +6684,14 @@ pub(crate) mod tests {
|
||||
.unwrap();
|
||||
|
||||
// First user message
|
||||
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,
|
||||
})]);
|
||||
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"),
|
||||
)]),
|
||||
)]);
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
|
||||
@@ -6760,24 +6717,14 @@ pub(crate) mod tests {
|
||||
});
|
||||
|
||||
// Second user message
|
||||
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,
|
||||
})]);
|
||||
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"),
|
||||
)]),
|
||||
)]);
|
||||
|
||||
thread
|
||||
.update(cx, |thread, cx| thread.send_raw("Another one", cx))
|
||||
@@ -6851,14 +6798,7 @@ pub(crate) mod tests {
|
||||
let connection = StubAgentConnection::new();
|
||||
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Response".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
meta: None,
|
||||
},
|
||||
acp::ContentChunk::new("Response".into()),
|
||||
)]);
|
||||
|
||||
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
|
||||
@@ -6944,14 +6884,7 @@ pub(crate) mod tests {
|
||||
let connection = StubAgentConnection::new();
|
||||
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Response".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
meta: None,
|
||||
},
|
||||
acp::ContentChunk::new("Response".into()),
|
||||
)]);
|
||||
|
||||
let (thread_view, cx) =
|
||||
@@ -6991,14 +6924,7 @@ pub(crate) mod tests {
|
||||
|
||||
// Send
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "New Response".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
meta: None,
|
||||
},
|
||||
acp::ContentChunk::new("New Response".into()),
|
||||
)]);
|
||||
|
||||
user_message_editor.update_in(cx, |_editor, window, cx| {
|
||||
@@ -7086,14 +7012,7 @@ pub(crate) mod tests {
|
||||
cx.update(|_, cx| {
|
||||
connection.send_update(
|
||||
session_id.clone(),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Response".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
|
||||
cx,
|
||||
);
|
||||
connection.end_turn(session_id, acp::StopReason::EndTurn);
|
||||
@@ -7145,10 +7064,9 @@ pub(crate) mod tests {
|
||||
cx.update(|_, cx| {
|
||||
connection.send_update(
|
||||
session_id.clone(),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
|
||||
content: "Message 1 resp".into(),
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
|
||||
"Message 1 resp".into(),
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -7182,10 +7100,7 @@ pub(crate) mod tests {
|
||||
// Simulate a response sent after beginning to cancel
|
||||
connection.send_update(
|
||||
session_id.clone(),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
|
||||
content: "onse".into(),
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
|
||||
cx,
|
||||
);
|
||||
});
|
||||
@@ -7216,10 +7131,9 @@ pub(crate) mod tests {
|
||||
cx.update(|_, cx| {
|
||||
connection.send_update(
|
||||
session_id.clone(),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
|
||||
content: "Message 2 response".into(),
|
||||
meta: None,
|
||||
}),
|
||||
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
|
||||
"Message 2 response".into(),
|
||||
)),
|
||||
cx,
|
||||
);
|
||||
connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
|
||||
@@ -7258,14 +7172,7 @@ pub(crate) mod tests {
|
||||
|
||||
let connection = StubAgentConnection::new();
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Response".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
meta: None,
|
||||
},
|
||||
acp::ContentChunk::new("Response".into()),
|
||||
)]);
|
||||
|
||||
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
|
||||
@@ -7344,14 +7251,7 @@ pub(crate) mod tests {
|
||||
|
||||
let connection = StubAgentConnection::new();
|
||||
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
|
||||
acp::ContentChunk {
|
||||
content: acp::ContentBlock::Text(acp::TextContent {
|
||||
text: "Response".into(),
|
||||
annotations: None,
|
||||
meta: None,
|
||||
}),
|
||||
meta: None,
|
||||
},
|
||||
acp::ContentChunk::new("Response".into()),
|
||||
)]);
|
||||
|
||||
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
|
||||
@@ -7399,54 +7299,4 @@ 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,16 +2685,17 @@ 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,
|
||||
panel.read(cx).thread_store().clone(),
|
||||
thread_store,
|
||||
None,
|
||||
initial_prompt,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,8 @@ 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,6 +719,7 @@ impl CodegenAlternative {
|
||||
output_tokens = usage.output_tokens,
|
||||
)
|
||||
}
|
||||
|
||||
cx.emit(CodegenEvent::Finished);
|
||||
cx.notify();
|
||||
})
|
||||
|
||||
@@ -1114,7 +1114,6 @@ 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);
|
||||
|
||||
89
crates/agent_ui/src/evals.rs
Normal file
@@ -0,0 +1,89 @@
|
||||
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;
|
||||
use futures::{FutureExt, channel::mpsc};
|
||||
use gpui::{
|
||||
App, Context, Entity, Focusable, Global, HighlightStyle, Subscription, Task, UpdateGlobal,
|
||||
WeakEntity, Window, point,
|
||||
@@ -102,6 +102,7 @@ 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 {}
|
||||
@@ -123,9 +124,18 @@ 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>,
|
||||
@@ -287,7 +297,7 @@ impl InlineAssistant {
|
||||
action.prompt.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
);
|
||||
})
|
||||
}
|
||||
InlineAssistTarget::Terminal(active_terminal) => {
|
||||
@@ -301,8 +311,8 @@ impl InlineAssistant {
|
||||
action.prompt.clone(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
@@ -598,13 +608,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;
|
||||
return None;
|
||||
};
|
||||
|
||||
let assist_to_focus = self.batch_assist(
|
||||
@@ -624,6 +634,8 @@ impl InlineAssistant {
|
||||
if let Some(assist_id) = assist_to_focus {
|
||||
self.focus_assist(assist_id, window, cx);
|
||||
}
|
||||
|
||||
assist_to_focus
|
||||
}
|
||||
|
||||
pub fn suggest_assist(
|
||||
@@ -1740,6 +1752,16 @@ 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;
|
||||
@@ -1750,6 +1772,11 @@ 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() {
|
||||
@@ -1943,3 +1970,160 @@ 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,7 +341,6 @@ 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,11 +2622,13 @@ 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, window, cx));
|
||||
self.editor.update(cx, |editor, cx| {
|
||||
editor.update_matches(matches, active_match_index, window, cx)
|
||||
});
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
|
||||
|
||||
@@ -584,41 +584,100 @@ impl Model {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn cross_region_inference_id(&self, region: &str) -> anyhow::Result<String> {
|
||||
pub fn cross_region_inference_id(
|
||||
&self,
|
||||
region: &str,
|
||||
allow_global: bool,
|
||||
) -> anyhow::Result<String> {
|
||||
// List derived from here:
|
||||
// https://docs.aws.amazon.com/bedrock/latest/userguide/inference-profiles-support.html#inference-profiles-support-system
|
||||
let model_id = self.request_id();
|
||||
|
||||
let supports_global = matches!(
|
||||
self,
|
||||
Model::ClaudeOpus4_5
|
||||
| Model::ClaudeOpus4_5Thinking
|
||||
| Model::ClaudeHaiku4_5
|
||||
| Model::ClaudeSonnet4
|
||||
| Model::ClaudeSonnet4Thinking
|
||||
| Model::ClaudeSonnet4_5
|
||||
| Model::ClaudeSonnet4_5Thinking
|
||||
);
|
||||
|
||||
let region_group = if region.starts_with("us-gov-") {
|
||||
"us-gov"
|
||||
} else if region.starts_with("us-") {
|
||||
"us"
|
||||
} else if region.starts_with("us-")
|
||||
|| region.starts_with("ca-")
|
||||
|| region.starts_with("sa-")
|
||||
{
|
||||
if allow_global && supports_global {
|
||||
"global"
|
||||
} else {
|
||||
"us"
|
||||
}
|
||||
} else if region.starts_with("eu-") {
|
||||
"eu"
|
||||
if allow_global && supports_global {
|
||||
"global"
|
||||
} else {
|
||||
"eu"
|
||||
}
|
||||
} else if region.starts_with("ap-") || region == "me-central-1" || region == "me-south-1" {
|
||||
"apac"
|
||||
} else if region.starts_with("ca-") || region.starts_with("sa-") {
|
||||
// Canada and South America regions - default to US profiles
|
||||
"us"
|
||||
if allow_global && supports_global {
|
||||
"global"
|
||||
} else {
|
||||
"apac"
|
||||
}
|
||||
} else {
|
||||
anyhow::bail!("Unsupported Region {region}");
|
||||
};
|
||||
|
||||
let model_id = self.request_id();
|
||||
match (self, region_group, region) {
|
||||
(Model::Custom { .. }, _, _) => Ok(self.request_id().into()),
|
||||
|
||||
match (self, region_group) {
|
||||
// Custom models can't have CRI IDs
|
||||
(Model::Custom { .. }, _) => Ok(self.request_id().into()),
|
||||
(
|
||||
Model::ClaudeOpus4_5
|
||||
| Model::ClaudeOpus4_5Thinking
|
||||
| Model::ClaudeHaiku4_5
|
||||
| Model::ClaudeSonnet4
|
||||
| Model::ClaudeSonnet4Thinking
|
||||
| Model::ClaudeSonnet4_5
|
||||
| Model::ClaudeSonnet4_5Thinking,
|
||||
"global",
|
||||
_,
|
||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||
|
||||
// Models with US Gov only
|
||||
(Model::Claude3_5Sonnet, "us-gov") | (Model::Claude3Haiku, "us-gov") => {
|
||||
Ok(format!("{}.{}", region_group, model_id))
|
||||
(
|
||||
Model::Claude3Haiku
|
||||
| Model::Claude3_5Sonnet
|
||||
| Model::Claude3_7Sonnet
|
||||
| Model::Claude3_7SonnetThinking
|
||||
| Model::ClaudeSonnet4_5
|
||||
| Model::ClaudeSonnet4_5Thinking,
|
||||
"us-gov",
|
||||
_,
|
||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||
|
||||
(
|
||||
Model::ClaudeHaiku4_5 | Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking,
|
||||
"apac",
|
||||
"ap-southeast-2" | "ap-southeast-4",
|
||||
) => Ok(format!("au.{}", model_id)),
|
||||
|
||||
(
|
||||
Model::ClaudeHaiku4_5 | Model::ClaudeSonnet4_5 | Model::ClaudeSonnet4_5Thinking,
|
||||
"apac",
|
||||
"ap-northeast-1" | "ap-northeast-3",
|
||||
) => Ok(format!("jp.{}", model_id)),
|
||||
|
||||
(Model::AmazonNovaLite, "us", r) if r.starts_with("ca-") => {
|
||||
Ok(format!("ca.{}", model_id))
|
||||
}
|
||||
|
||||
// Available everywhere
|
||||
(Model::AmazonNovaLite | Model::AmazonNovaMicro | Model::AmazonNovaPro, _) => {
|
||||
Ok(format!("{}.{}", region_group, model_id))
|
||||
}
|
||||
|
||||
// Models in US
|
||||
(
|
||||
Model::AmazonNovaPremier
|
||||
| Model::AmazonNovaLite
|
||||
| Model::AmazonNovaMicro
|
||||
| Model::AmazonNovaPro
|
||||
| Model::Claude3_5Haiku
|
||||
| Model::ClaudeHaiku4_5
|
||||
| Model::Claude3_5Sonnet
|
||||
@@ -655,16 +714,18 @@ impl Model {
|
||||
| Model::PalmyraWriterX4
|
||||
| Model::PalmyraWriterX5,
|
||||
"us",
|
||||
_,
|
||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||
|
||||
// Models available in EU
|
||||
(
|
||||
Model::Claude3_5Sonnet
|
||||
Model::AmazonNovaLite
|
||||
| Model::AmazonNovaMicro
|
||||
| Model::AmazonNovaPro
|
||||
| Model::Claude3_5Sonnet
|
||||
| Model::ClaudeHaiku4_5
|
||||
| Model::Claude3_7Sonnet
|
||||
| Model::Claude3_7SonnetThinking
|
||||
| Model::ClaudeSonnet4
|
||||
| Model::ClaudeSonnet4Thinking
|
||||
| Model::ClaudeSonnet4_5
|
||||
| Model::ClaudeSonnet4_5Thinking
|
||||
| Model::Claude3Haiku
|
||||
@@ -673,26 +734,26 @@ impl Model {
|
||||
| Model::MetaLlama323BInstructV1
|
||||
| Model::MistralPixtralLarge2502V1,
|
||||
"eu",
|
||||
_,
|
||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||
|
||||
// Models available in APAC
|
||||
(
|
||||
Model::Claude3_5Sonnet
|
||||
Model::AmazonNovaLite
|
||||
| Model::AmazonNovaMicro
|
||||
| Model::AmazonNovaPro
|
||||
| Model::Claude3_5Sonnet
|
||||
| Model::Claude3_5SonnetV2
|
||||
| Model::ClaudeHaiku4_5
|
||||
| Model::Claude3Haiku
|
||||
| Model::Claude3Sonnet
|
||||
| Model::Claude3_7Sonnet
|
||||
| Model::Claude3_7SonnetThinking
|
||||
| Model::ClaudeSonnet4
|
||||
| Model::ClaudeSonnet4Thinking
|
||||
| Model::ClaudeSonnet4_5
|
||||
| Model::ClaudeSonnet4_5Thinking,
|
||||
| Model::Claude3Haiku
|
||||
| Model::Claude3Sonnet,
|
||||
"apac",
|
||||
_,
|
||||
) => Ok(format!("{}.{}", region_group, model_id)),
|
||||
|
||||
// Any other combination is not supported
|
||||
_ => Ok(self.request_id().into()),
|
||||
_ => Ok(model_id.into()),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -705,15 +766,15 @@ mod tests {
|
||||
fn test_us_region_inference_ids() -> anyhow::Result<()> {
|
||||
// Test US regions
|
||||
assert_eq!(
|
||||
Model::Claude3_5SonnetV2.cross_region_inference_id("us-east-1")?,
|
||||
Model::Claude3_5SonnetV2.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::Claude3_5SonnetV2.cross_region_inference_id("us-west-2")?,
|
||||
Model::Claude3_5SonnetV2.cross_region_inference_id("us-west-2", false)?,
|
||||
"us.anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::AmazonNovaPro.cross_region_inference_id("us-east-2")?,
|
||||
Model::AmazonNovaPro.cross_region_inference_id("us-east-2", false)?,
|
||||
"us.amazon.nova-pro-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
@@ -723,19 +784,19 @@ mod tests {
|
||||
fn test_eu_region_inference_ids() -> anyhow::Result<()> {
|
||||
// Test European regions
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4.cross_region_inference_id("eu-west-1")?,
|
||||
Model::ClaudeSonnet4.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1")?,
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::Claude3Sonnet.cross_region_inference_id("eu-west-1")?,
|
||||
Model::Claude3Sonnet.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.anthropic.claude-3-sonnet-20240229-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::AmazonNovaMicro.cross_region_inference_id("eu-north-1")?,
|
||||
Model::AmazonNovaMicro.cross_region_inference_id("eu-north-1", false)?,
|
||||
"eu.amazon.nova-micro-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
@@ -745,15 +806,15 @@ mod tests {
|
||||
fn test_apac_region_inference_ids() -> anyhow::Result<()> {
|
||||
// Test Asia-Pacific regions
|
||||
assert_eq!(
|
||||
Model::Claude3_5SonnetV2.cross_region_inference_id("ap-northeast-1")?,
|
||||
Model::Claude3_5SonnetV2.cross_region_inference_id("ap-northeast-1", false)?,
|
||||
"apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::Claude3_5SonnetV2.cross_region_inference_id("ap-southeast-2")?,
|
||||
Model::Claude3_5SonnetV2.cross_region_inference_id("ap-southeast-2", false)?,
|
||||
"apac.anthropic.claude-3-5-sonnet-20241022-v2:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::AmazonNovaLite.cross_region_inference_id("ap-south-1")?,
|
||||
Model::AmazonNovaLite.cross_region_inference_id("ap-south-1", false)?,
|
||||
"apac.amazon.nova-lite-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
@@ -763,11 +824,11 @@ mod tests {
|
||||
fn test_gov_region_inference_ids() -> anyhow::Result<()> {
|
||||
// Test Government regions
|
||||
assert_eq!(
|
||||
Model::Claude3_5Sonnet.cross_region_inference_id("us-gov-east-1")?,
|
||||
Model::Claude3_5Sonnet.cross_region_inference_id("us-gov-east-1", false)?,
|
||||
"us-gov.anthropic.claude-3-5-sonnet-20240620-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::Claude3Haiku.cross_region_inference_id("us-gov-west-1")?,
|
||||
Model::Claude3Haiku.cross_region_inference_id("us-gov-west-1", false)?,
|
||||
"us-gov.anthropic.claude-3-haiku-20240307-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
@@ -777,15 +838,15 @@ mod tests {
|
||||
fn test_meta_models_inference_ids() -> anyhow::Result<()> {
|
||||
// Test Meta models
|
||||
assert_eq!(
|
||||
Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1")?,
|
||||
Model::MetaLlama370BInstructV1.cross_region_inference_id("us-east-1", false)?,
|
||||
"meta.llama3-70b-instruct-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::MetaLlama3170BInstructV1.cross_region_inference_id("us-east-1")?,
|
||||
Model::MetaLlama3170BInstructV1.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.meta.llama3-1-70b-instruct-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1")?,
|
||||
Model::MetaLlama321BInstructV1.cross_region_inference_id("eu-west-1", false)?,
|
||||
"eu.meta.llama3-2-1b-instruct-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
@@ -796,11 +857,11 @@ mod tests {
|
||||
// Mistral models don't follow the regional prefix pattern,
|
||||
// so they should return their original IDs
|
||||
assert_eq!(
|
||||
Model::MistralMistralLarge2402V1.cross_region_inference_id("us-east-1")?,
|
||||
Model::MistralMistralLarge2402V1.cross_region_inference_id("us-east-1", false)?,
|
||||
"mistral.mistral-large-2402-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::MistralMixtral8x7BInstructV0.cross_region_inference_id("eu-west-1")?,
|
||||
Model::MistralMixtral8x7BInstructV0.cross_region_inference_id("eu-west-1", false)?,
|
||||
"mistral.mixtral-8x7b-instruct-v0:1"
|
||||
);
|
||||
Ok(())
|
||||
@@ -811,11 +872,11 @@ mod tests {
|
||||
// AI21 models don't follow the regional prefix pattern,
|
||||
// so they should return their original IDs
|
||||
assert_eq!(
|
||||
Model::AI21J2UltraV1.cross_region_inference_id("us-east-1")?,
|
||||
Model::AI21J2UltraV1.cross_region_inference_id("us-east-1", false)?,
|
||||
"ai21.j2-ultra-v1"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::AI21JambaInstructV1.cross_region_inference_id("eu-west-1")?,
|
||||
Model::AI21JambaInstructV1.cross_region_inference_id("eu-west-1", false)?,
|
||||
"ai21.jamba-instruct-v1:0"
|
||||
);
|
||||
Ok(())
|
||||
@@ -826,11 +887,11 @@ mod tests {
|
||||
// Cohere models don't follow the regional prefix pattern,
|
||||
// so they should return their original IDs
|
||||
assert_eq!(
|
||||
Model::CohereCommandRV1.cross_region_inference_id("us-east-1")?,
|
||||
Model::CohereCommandRV1.cross_region_inference_id("us-east-1", false)?,
|
||||
"cohere.command-r-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::CohereCommandTextV14_4k.cross_region_inference_id("ap-southeast-1")?,
|
||||
Model::CohereCommandTextV14_4k.cross_region_inference_id("ap-southeast-1", false)?,
|
||||
"cohere.command-text-v14:7:4k"
|
||||
);
|
||||
Ok(())
|
||||
@@ -850,10 +911,17 @@ mod tests {
|
||||
|
||||
// Custom model should return its name unchanged
|
||||
assert_eq!(
|
||||
custom_model.cross_region_inference_id("us-east-1")?,
|
||||
custom_model.cross_region_inference_id("us-east-1", false)?,
|
||||
"custom.my-model-v1:0"
|
||||
);
|
||||
|
||||
// Test that models without global support fall back to regional when allow_global is true
|
||||
assert_eq!(
|
||||
Model::AmazonNovaPro.cross_region_inference_id("us-east-1", true)?,
|
||||
"us.amazon.nova-pro-v1:0",
|
||||
"Nova Pro should fall back to regional profile even when allow_global is true"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -892,3 +960,28 @@ mod tests {
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_global_inference_ids() -> anyhow::Result<()> {
|
||||
// Test global inference for models that support it when allow_global is true
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", true)?,
|
||||
"global.anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4_5.cross_region_inference_id("eu-west-1", true)?,
|
||||
"global.anthropic.claude-sonnet-4-5-20250929-v1:0"
|
||||
);
|
||||
assert_eq!(
|
||||
Model::ClaudeHaiku4_5.cross_region_inference_id("ap-south-1", true)?,
|
||||
"global.anthropic.claude-haiku-4-5-20251001-v1:0"
|
||||
);
|
||||
|
||||
// Test that regional prefix is used when allow_global is false
|
||||
assert_eq!(
|
||||
Model::ClaudeSonnet4.cross_region_inference_id("us-east-1", false)?,
|
||||
"us.anthropic.claude-sonnet-4-20250514-v1:0"
|
||||
);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -12,7 +12,7 @@ workspace = true
|
||||
path = "src/buffer_diff.rs"
|
||||
|
||||
[features]
|
||||
test-support = []
|
||||
test-support = ["settings"]
|
||||
|
||||
[dependencies]
|
||||
anyhow.workspace = true
|
||||
@@ -24,6 +24,7 @@ language.workspace = true
|
||||
log.workspace = true
|
||||
pretty_assertions.workspace = true
|
||||
rope.workspace = true
|
||||
settings = { workspace = true, optional = true }
|
||||
sum_tree.workspace = true
|
||||
text.workspace = true
|
||||
util.workspace = true
|
||||
@@ -33,6 +34,7 @@ ctor.workspace = true
|
||||
gpui = { workspace = true, features = ["test-support"] }
|
||||
rand.workspace = true
|
||||
serde_json.workspace = true
|
||||
settings.workspace = true
|
||||
text = { workspace = true, features = ["test-support"] }
|
||||
unindent.workspace = true
|
||||
zlog.workspace = true
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
use futures::channel::oneshot;
|
||||
use git2::{DiffLineType as GitDiffLineType, DiffOptions as GitOptions, Patch as GitPatch};
|
||||
use gpui::{App, AppContext as _, AsyncApp, Context, Entity, EventEmitter, Task, TaskLabel};
|
||||
use language::{BufferRow, Language, LanguageRegistry};
|
||||
use language::{
|
||||
BufferRow, DiffOptions, File, Language, LanguageName, LanguageRegistry,
|
||||
language_settings::language_settings, word_diff_ranges,
|
||||
};
|
||||
use rope::Rope;
|
||||
use std::{
|
||||
cmp::Ordering,
|
||||
@@ -15,10 +18,12 @@ use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint
|
||||
use util::ResultExt;
|
||||
|
||||
pub static CALCULATE_DIFF_TASK: LazyLock<TaskLabel> = LazyLock::new(TaskLabel::new);
|
||||
pub const MAX_WORD_DIFF_LINE_COUNT: usize = 5;
|
||||
|
||||
pub struct BufferDiff {
|
||||
pub buffer_id: BufferId,
|
||||
inner: BufferDiffInner,
|
||||
// diff of the index vs head
|
||||
secondary_diff: Option<Entity<BufferDiff>>,
|
||||
}
|
||||
|
||||
@@ -31,6 +36,7 @@ pub struct BufferDiffSnapshot {
|
||||
#[derive(Clone)]
|
||||
struct BufferDiffInner {
|
||||
hunks: SumTree<InternalDiffHunk>,
|
||||
// Used for making staging mo
|
||||
pending_hunks: SumTree<PendingHunk>,
|
||||
base_text: language::BufferSnapshot,
|
||||
base_text_exists: bool,
|
||||
@@ -50,11 +56,18 @@ pub enum DiffHunkStatusKind {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||
/// Diff of Working Copy vs Index
|
||||
/// aka 'is this hunk staged or not'
|
||||
pub enum DiffHunkSecondaryStatus {
|
||||
/// Unstaged
|
||||
HasSecondaryHunk,
|
||||
/// Partially staged
|
||||
OverlapsWithSecondaryHunk,
|
||||
/// Staged
|
||||
NoSecondaryHunk,
|
||||
/// We are unstaging
|
||||
SecondaryHunkAdditionPending,
|
||||
/// We are stagind
|
||||
SecondaryHunkRemovalPending,
|
||||
}
|
||||
|
||||
@@ -68,6 +81,10 @@ pub struct DiffHunk {
|
||||
/// The range in the buffer's diff base text to which this hunk corresponds.
|
||||
pub diff_base_byte_range: Range<usize>,
|
||||
pub secondary_status: DiffHunkSecondaryStatus,
|
||||
// Anchors representing the word diff locations in the active buffer
|
||||
pub buffer_word_diffs: Vec<Range<Anchor>>,
|
||||
// Offsets relative to the start of the deleted diff that represent word diff locations
|
||||
pub base_word_diffs: Vec<Range<usize>>,
|
||||
}
|
||||
|
||||
/// We store [`InternalDiffHunk`]s internally so we don't need to store the additional row range.
|
||||
@@ -75,6 +92,8 @@ pub struct DiffHunk {
|
||||
struct InternalDiffHunk {
|
||||
buffer_range: Range<Anchor>,
|
||||
diff_base_byte_range: Range<usize>,
|
||||
base_word_diffs: Vec<Range<usize>>,
|
||||
buffer_word_diffs: Vec<Range<Anchor>>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
@@ -208,6 +227,13 @@ impl BufferDiffSnapshot {
|
||||
let base_text_pair;
|
||||
let base_text_exists;
|
||||
let base_text_snapshot;
|
||||
let diff_options = build_diff_options(
|
||||
None,
|
||||
language.as_ref().map(|l| l.name()),
|
||||
language.as_ref().map(|l| l.default_scope()),
|
||||
cx,
|
||||
);
|
||||
|
||||
if let Some(text) = &base_text {
|
||||
let base_text_rope = Rope::from(text.as_str());
|
||||
base_text_pair = Some((text.clone(), base_text_rope.clone()));
|
||||
@@ -225,7 +251,7 @@ impl BufferDiffSnapshot {
|
||||
.background_executor()
|
||||
.spawn_labeled(*CALCULATE_DIFF_TASK, {
|
||||
let buffer = buffer.clone();
|
||||
async move { compute_hunks(base_text_pair, buffer) }
|
||||
async move { compute_hunks(base_text_pair, buffer, diff_options) }
|
||||
});
|
||||
|
||||
async move {
|
||||
@@ -248,6 +274,12 @@ impl BufferDiffSnapshot {
|
||||
base_text_snapshot: language::BufferSnapshot,
|
||||
cx: &App,
|
||||
) -> impl Future<Output = Self> + use<> {
|
||||
let diff_options = build_diff_options(
|
||||
base_text_snapshot.file(),
|
||||
base_text_snapshot.language().map(|l| l.name()),
|
||||
base_text_snapshot.language().map(|l| l.default_scope()),
|
||||
cx,
|
||||
);
|
||||
let base_text_exists = base_text.is_some();
|
||||
let base_text_pair = base_text.map(|text| {
|
||||
debug_assert_eq!(&*text, &base_text_snapshot.text());
|
||||
@@ -259,7 +291,7 @@ impl BufferDiffSnapshot {
|
||||
inner: BufferDiffInner {
|
||||
base_text: base_text_snapshot,
|
||||
pending_hunks: SumTree::new(&buffer),
|
||||
hunks: compute_hunks(base_text_pair, buffer),
|
||||
hunks: compute_hunks(base_text_pair, buffer, diff_options),
|
||||
base_text_exists,
|
||||
},
|
||||
secondary_diff: None,
|
||||
@@ -602,11 +634,15 @@ impl BufferDiffInner {
|
||||
[
|
||||
(
|
||||
&hunk.buffer_range.start,
|
||||
(hunk.buffer_range.start, hunk.diff_base_byte_range.start),
|
||||
(
|
||||
hunk.buffer_range.start,
|
||||
hunk.diff_base_byte_range.start,
|
||||
hunk,
|
||||
),
|
||||
),
|
||||
(
|
||||
&hunk.buffer_range.end,
|
||||
(hunk.buffer_range.end, hunk.diff_base_byte_range.end),
|
||||
(hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
|
||||
),
|
||||
]
|
||||
});
|
||||
@@ -625,8 +661,11 @@ impl BufferDiffInner {
|
||||
let mut summaries = buffer.summaries_for_anchors_with_payload::<Point, _, _>(anchor_iter);
|
||||
iter::from_fn(move || {
|
||||
loop {
|
||||
let (start_point, (start_anchor, start_base)) = summaries.next()?;
|
||||
let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
|
||||
let (start_point, (start_anchor, start_base, hunk)) = summaries.next()?;
|
||||
let (mut end_point, (mut end_anchor, end_base, _)) = summaries.next()?;
|
||||
|
||||
let base_word_diffs = hunk.base_word_diffs.clone();
|
||||
let buffer_word_diffs = hunk.buffer_word_diffs.clone();
|
||||
|
||||
if !start_anchor.is_valid(buffer) {
|
||||
continue;
|
||||
@@ -696,6 +735,8 @@ impl BufferDiffInner {
|
||||
range: start_point..end_point,
|
||||
diff_base_byte_range: start_base..end_base,
|
||||
buffer_range: start_anchor..end_anchor,
|
||||
base_word_diffs,
|
||||
buffer_word_diffs,
|
||||
secondary_status,
|
||||
});
|
||||
}
|
||||
@@ -727,6 +768,8 @@ impl BufferDiffInner {
|
||||
buffer_range: hunk.buffer_range.clone(),
|
||||
// The secondary status is not used by callers of this method.
|
||||
secondary_status: DiffHunkSecondaryStatus::NoSecondaryHunk,
|
||||
base_word_diffs: hunk.base_word_diffs.clone(),
|
||||
buffer_word_diffs: hunk.buffer_word_diffs.clone(),
|
||||
})
|
||||
})
|
||||
}
|
||||
@@ -795,9 +838,36 @@ impl BufferDiffInner {
|
||||
}
|
||||
}
|
||||
|
||||
fn build_diff_options(
|
||||
file: Option<&Arc<dyn File>>,
|
||||
language: Option<LanguageName>,
|
||||
language_scope: Option<language::LanguageScope>,
|
||||
cx: &App,
|
||||
) -> Option<DiffOptions> {
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
{
|
||||
if !cx.has_global::<settings::SettingsStore>() {
|
||||
return Some(DiffOptions {
|
||||
language_scope,
|
||||
max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
|
||||
..Default::default()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
language_settings(language, file, cx)
|
||||
.word_diff_enabled
|
||||
.then_some(DiffOptions {
|
||||
language_scope,
|
||||
max_word_diff_line_count: MAX_WORD_DIFF_LINE_COUNT,
|
||||
..Default::default()
|
||||
})
|
||||
}
|
||||
|
||||
fn compute_hunks(
|
||||
diff_base: Option<(Arc<String>, Rope)>,
|
||||
buffer: text::BufferSnapshot,
|
||||
diff_options: Option<DiffOptions>,
|
||||
) -> SumTree<InternalDiffHunk> {
|
||||
let mut tree = SumTree::new(&buffer);
|
||||
|
||||
@@ -823,6 +893,8 @@ fn compute_hunks(
|
||||
InternalDiffHunk {
|
||||
buffer_range: buffer.anchor_before(0)..buffer.anchor_before(0),
|
||||
diff_base_byte_range: 0..diff_base.len() - 1,
|
||||
base_word_diffs: Vec::default(),
|
||||
buffer_word_diffs: Vec::default(),
|
||||
},
|
||||
&buffer,
|
||||
);
|
||||
@@ -838,6 +910,7 @@ fn compute_hunks(
|
||||
&diff_base_rope,
|
||||
&buffer,
|
||||
&mut divergence,
|
||||
diff_options.as_ref(),
|
||||
);
|
||||
tree.push(hunk, &buffer);
|
||||
}
|
||||
@@ -847,6 +920,8 @@ fn compute_hunks(
|
||||
InternalDiffHunk {
|
||||
buffer_range: Anchor::min_max_range_for_buffer(buffer.remote_id()),
|
||||
diff_base_byte_range: 0..0,
|
||||
base_word_diffs: Vec::default(),
|
||||
buffer_word_diffs: Vec::default(),
|
||||
},
|
||||
&buffer,
|
||||
);
|
||||
@@ -861,6 +936,7 @@ fn process_patch_hunk(
|
||||
diff_base: &Rope,
|
||||
buffer: &text::BufferSnapshot,
|
||||
buffer_row_divergence: &mut i64,
|
||||
diff_options: Option<&DiffOptions>,
|
||||
) -> InternalDiffHunk {
|
||||
let line_item_count = patch.num_lines_in_hunk(hunk_index).unwrap();
|
||||
assert!(line_item_count > 0);
|
||||
@@ -925,9 +1001,49 @@ fn process_patch_hunk(
|
||||
let start = Point::new(buffer_row_range.start, 0);
|
||||
let end = Point::new(buffer_row_range.end, 0);
|
||||
let buffer_range = buffer.anchor_before(start)..buffer.anchor_before(end);
|
||||
|
||||
let base_line_count = line_item_count.saturating_sub(buffer_row_range.len());
|
||||
|
||||
let (base_word_diffs, buffer_word_diffs) = if let Some(diff_options) = diff_options
|
||||
&& !buffer_row_range.is_empty()
|
||||
&& base_line_count == buffer_row_range.len()
|
||||
&& diff_options.max_word_diff_line_count >= base_line_count
|
||||
{
|
||||
let base_text: String = diff_base
|
||||
.chunks_in_range(diff_base_byte_range.clone())
|
||||
.collect();
|
||||
|
||||
let buffer_text: String = buffer.text_for_range(buffer_range.clone()).collect();
|
||||
|
||||
let (base_word_diffs, buffer_word_diffs_relative) = word_diff_ranges(
|
||||
&base_text,
|
||||
&buffer_text,
|
||||
DiffOptions {
|
||||
language_scope: diff_options.language_scope.clone(),
|
||||
..*diff_options
|
||||
},
|
||||
);
|
||||
|
||||
let buffer_start_offset = buffer_range.start.to_offset(buffer);
|
||||
let buffer_word_diffs = buffer_word_diffs_relative
|
||||
.into_iter()
|
||||
.map(|range| {
|
||||
let start = buffer.anchor_after(buffer_start_offset + range.start);
|
||||
let end = buffer.anchor_after(buffer_start_offset + range.end);
|
||||
start..end
|
||||
})
|
||||
.collect();
|
||||
|
||||
(base_word_diffs, buffer_word_diffs)
|
||||
} else {
|
||||
(Vec::default(), Vec::default())
|
||||
};
|
||||
|
||||
InternalDiffHunk {
|
||||
buffer_range,
|
||||
diff_base_byte_range,
|
||||
base_word_diffs,
|
||||
buffer_word_diffs,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -524,6 +524,16 @@ impl Room {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn room_id(&self) -> impl Future<Output = Option<String>> + 'static {
|
||||
let room = self.live_kit.as_ref().map(|lk| lk.room.clone());
|
||||
async move {
|
||||
let room = room?;
|
||||
let sid = room.sid().await;
|
||||
let name = room.name();
|
||||
Some(format!("{} (sid: {sid})", name))
|
||||
}
|
||||
}
|
||||
|
||||
pub fn status(&self) -> RoomStatus {
|
||||
self.status
|
||||
}
|
||||
|
||||
@@ -1723,6 +1723,10 @@ impl ProtoClient for Client {
|
||||
fn is_via_collab(&self) -> bool {
|
||||
true
|
||||
}
|
||||
|
||||
fn has_wsl_interop(&self) -> bool {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// prefix for the zed:// url scheme
|
||||
|
||||
@@ -206,11 +206,16 @@ pub struct AcceptEditPredictionBody {
|
||||
pub request_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct RejectEditPredictionsBody {
|
||||
pub rejections: Vec<EditPredictionRejection>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize)]
|
||||
pub struct RejectEditPredictionsBodyRef<'a> {
|
||||
pub rejections: &'a [EditPredictionRejection],
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
||||
pub struct EditPredictionRejection {
|
||||
pub request_id: String,
|
||||
|
||||
@@ -121,6 +121,8 @@ CREATE TABLE "project_repositories" (
|
||||
"merge_message" VARCHAR,
|
||||
"branch_summary" VARCHAR,
|
||||
"head_commit_details" VARCHAR,
|
||||
"remote_upstream_url" VARCHAR,
|
||||
"remote_origin_url" VARCHAR,
|
||||
PRIMARY KEY (project_id, id)
|
||||
);
|
||||
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
ALTER TABLE "project_repositories" ADD COLUMN "remote_upstream_url" VARCHAR;
|
||||
ALTER TABLE "project_repositories" ADD COLUMN "remote_origin_url" VARCHAR;
|
||||
@@ -362,6 +362,8 @@ impl Database {
|
||||
entry_ids: ActiveValue::set("[]".into()),
|
||||
head_commit_details: ActiveValue::set(None),
|
||||
merge_message: ActiveValue::set(None),
|
||||
remote_upstream_url: ActiveValue::set(None),
|
||||
remote_origin_url: ActiveValue::set(None),
|
||||
}
|
||||
}),
|
||||
)
|
||||
@@ -511,6 +513,8 @@ impl Database {
|
||||
serde_json::to_string(&update.current_merge_conflicts).unwrap(),
|
||||
)),
|
||||
merge_message: ActiveValue::set(update.merge_message.clone()),
|
||||
remote_upstream_url: ActiveValue::set(update.remote_upstream_url.clone()),
|
||||
remote_origin_url: ActiveValue::set(update.remote_origin_url.clone()),
|
||||
})
|
||||
.on_conflict(
|
||||
OnConflict::columns([
|
||||
@@ -1005,6 +1009,8 @@ impl Database {
|
||||
is_last_update: true,
|
||||
merge_message: db_repository_entry.merge_message,
|
||||
stash_entries: Vec::new(),
|
||||
remote_upstream_url: db_repository_entry.remote_upstream_url.clone(),
|
||||
remote_origin_url: db_repository_entry.remote_origin_url.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -796,6 +796,8 @@ impl Database {
|
||||
is_last_update: true,
|
||||
merge_message: db_repository.merge_message,
|
||||
stash_entries: Vec::new(),
|
||||
remote_upstream_url: db_repository.remote_upstream_url.clone(),
|
||||
remote_origin_url: db_repository.remote_origin_url.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -22,6 +22,8 @@ pub struct Model {
|
||||
pub branch_summary: Option<String>,
|
||||
// A JSON object representing the current Head commit values
|
||||
pub head_commit_details: Option<String>,
|
||||
pub remote_upstream_url: Option<String>,
|
||||
pub remote_origin_url: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
|
||||
@@ -469,6 +469,8 @@ impl Server {
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GetBlobContent>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitCreateBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitChangeBranch>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitCreateRemote>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::GitRemoveRemote>)
|
||||
.add_request_handler(forward_mutating_project_request::<proto::CheckForPushedCommits>)
|
||||
.add_message_handler(broadcast_project_message_from_host::<proto::AdvertiseContexts>)
|
||||
.add_message_handler(update_context)
|
||||
|
||||
@@ -25,6 +25,7 @@ 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},
|
||||
@@ -3192,13 +3193,12 @@ async fn test_lsp_pull_diagnostics(
|
||||
.collect::<Vec<_>>();
|
||||
let expected_messages = [
|
||||
expected_pull_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,
|
||||
expected_push_diagnostic_lib_message,
|
||||
];
|
||||
assert_eq!(
|
||||
all_diagnostics.len(),
|
||||
1,
|
||||
"Expected pull diagnostics, but got: {all_diagnostics:?}"
|
||||
2,
|
||||
"Expected pull and push diagnostics, but got: {all_diagnostics:?}"
|
||||
);
|
||||
for diagnostic in all_diagnostics {
|
||||
assert!(
|
||||
@@ -3258,14 +3258,15 @@ async fn test_lsp_pull_diagnostics(
|
||||
.diagnostics_in_range(MultiBufferOffset(0)..snapshot.len())
|
||||
.collect::<Vec<_>>();
|
||||
let expected_messages = [
|
||||
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,
|
||||
// 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,
|
||||
];
|
||||
assert_eq!(
|
||||
all_diagnostics.len(),
|
||||
1,
|
||||
"Expected pull diagnostics, but got: {all_diagnostics:?}"
|
||||
2,
|
||||
"Expected pull and push diagnostics, but got: {all_diagnostics:?}"
|
||||
);
|
||||
for diagnostic in all_diagnostics {
|
||||
assert!(
|
||||
@@ -3378,8 +3379,9 @@ async fn test_lsp_pull_diagnostics(
|
||||
"Another workspace diagnostics pull should happen after the diagnostics refresh server request"
|
||||
);
|
||||
{
|
||||
assert!(
|
||||
diagnostics_pulls_result_ids.lock().await.len() == diagnostic_pulls_result_ids,
|
||||
assert_eq!(
|
||||
diagnostics_pulls_result_ids.lock().await.len(),
|
||||
diagnostic_pulls_result_ids,
|
||||
"Pulls should not happen hence no extra ids should appear"
|
||||
);
|
||||
assert!(
|
||||
@@ -3397,7 +3399,7 @@ async fn test_lsp_pull_diagnostics(
|
||||
expected_pull_diagnostic_lib_message,
|
||||
expected_push_diagnostic_lib_message,
|
||||
];
|
||||
assert_eq!(all_diagnostics.len(), 1);
|
||||
assert_eq!(all_diagnostics.len(), 2);
|
||||
for diagnostic in &all_diagnostics {
|
||||
assert!(
|
||||
expected_messages.contains(&diagnostic.diagnostic.message.as_str()),
|
||||
@@ -3516,7 +3518,6 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
.into_iter()
|
||||
.map(|(sha, message)| (sha.parse().unwrap(), message.into()))
|
||||
.collect(),
|
||||
remote_url: Some("git@github.com:zed-industries/zed.git".to_string()),
|
||||
};
|
||||
client_a.fs().set_blame_for_repo(
|
||||
Path::new(path!("/my-repo/.git")),
|
||||
@@ -3601,10 +3602,6 @@ async fn test_git_blame_is_forwarded(cx_a: &mut TestAppContext, cx_b: &mut TestA
|
||||
for (idx, (buffer, entry)) in entries.iter().flatten().enumerate() {
|
||||
let details = blame.details_for_entry(*buffer, entry).unwrap();
|
||||
assert_eq!(details.message, format!("message for idx-{}", idx));
|
||||
assert_eq!(
|
||||
details.permalink.unwrap().to_string(),
|
||||
format!("https://github.com/zed-industries/zed/commit/{}", entry.sha)
|
||||
);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
@@ -37,7 +37,7 @@ use ui::{
|
||||
};
|
||||
use util::{ResultExt, TryFutureExt, maybe};
|
||||
use workspace::{
|
||||
Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
|
||||
CopyRoomId, Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
|
||||
dock::{DockPosition, Panel, PanelEvent},
|
||||
notifications::{DetachAndPromptErr, NotifyResultExt},
|
||||
};
|
||||
@@ -128,6 +128,32 @@ pub fn init(cx: &mut App) {
|
||||
workspace.register_action(|_, _: &LeaveCall, window, cx| {
|
||||
CollabPanel::leave_call(window, cx);
|
||||
});
|
||||
workspace.register_action(|workspace, _: &CopyRoomId, window, cx| {
|
||||
use workspace::notifications::{NotificationId, NotifyTaskExt as _};
|
||||
|
||||
struct RoomIdCopiedToast;
|
||||
|
||||
if let Some(room) = ActiveCall::global(cx).read(cx).room() {
|
||||
let romo_id_fut = room.read(cx).room_id();
|
||||
cx.spawn(async move |workspace, cx| {
|
||||
let room_id = romo_id_fut.await.context("Failed to get livekit room")?;
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
cx.write_to_clipboard(ClipboardItem::new_string(room_id));
|
||||
workspace.show_toast(
|
||||
workspace::Toast::new(
|
||||
NotificationId::unique::<RoomIdCopiedToast>(),
|
||||
"Room ID copied to clipboard",
|
||||
)
|
||||
.autohide(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})
|
||||
.detach_and_notify_err(window, cx);
|
||||
} else {
|
||||
workspace.show_error(&"There’s no active call; join one first.", cx);
|
||||
}
|
||||
});
|
||||
workspace.register_action(|workspace, _: &ShareProject, window, cx| {
|
||||
let project = workspace.project().clone();
|
||||
println!("{project:?}");
|
||||
|
||||
@@ -23,6 +23,9 @@ 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,6 +3,8 @@ 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")]
|
||||
@@ -70,11 +72,16 @@ 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");
|
||||
@@ -342,6 +349,57 @@ 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,11 +1017,13 @@ 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, window, cx))
|
||||
self.editor.update(cx, |e, cx| {
|
||||
e.update_matches(matches, active_match_index, window, cx)
|
||||
})
|
||||
}
|
||||
|
||||
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {
|
||||
|
||||
@@ -740,7 +740,7 @@ impl DebugPanel {
|
||||
}
|
||||
})
|
||||
.child(
|
||||
IconButton::new("debug-step-over", IconName::ArrowRight)
|
||||
IconButton::new("step-over", IconName::DebugStepOver)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(window.listener_for(
|
||||
running_state,
|
||||
@@ -762,32 +762,29 @@ impl DebugPanel {
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
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,
|
||||
)
|
||||
}
|
||||
}),
|
||||
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,
|
||||
)
|
||||
}
|
||||
}),
|
||||
)
|
||||
.child(
|
||||
IconButton::new("debug-step-out", IconName::ArrowUpRight)
|
||||
IconButton::new("step-out", IconName::DebugStepOut)
|
||||
.icon_size(IconSize::Small)
|
||||
.on_click(window.listener_for(
|
||||
running_state,
|
||||
|
||||
@@ -18,14 +18,14 @@ use gpui::{
|
||||
use language::{Anchor, Buffer, CharScopeContext, CodeLabel, TextBufferSnapshot, ToOffset};
|
||||
use menu::{Confirm, SelectNext, SelectPrevious};
|
||||
use project::{
|
||||
Completion, CompletionDisplayOptions, CompletionResponse,
|
||||
CompletionDisplayOptions, CompletionResponse,
|
||||
debugger::session::{CompletionsQuery, OutputToken, Session},
|
||||
lsp_store::CompletionDocumentation,
|
||||
search_history::{SearchHistory, SearchHistoryCursor},
|
||||
};
|
||||
use settings::Settings;
|
||||
use std::fmt::Write;
|
||||
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
|
||||
use std::{ops::Range, rc::Rc, usize};
|
||||
use theme::{Theme, ThemeSettings};
|
||||
use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
|
||||
use util::ResultExt;
|
||||
@@ -252,10 +252,11 @@ 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],
|
||||
color_fetcher(color),
|
||||
move |_, theme| color_fn(theme),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -553,24 +554,12 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_additional_edits_for_completion(
|
||||
&self,
|
||||
_buffer: Entity<Buffer>,
|
||||
_completions: Rc<RefCell<Box<[Completion]>>>,
|
||||
_completion_index: usize,
|
||||
_push_to_history: bool,
|
||||
_cx: &mut Context<Editor>,
|
||||
) -> gpui::Task<anyhow::Result<Option<language::Transaction>>> {
|
||||
Task::ready(Ok(None))
|
||||
}
|
||||
|
||||
fn is_completion_trigger(
|
||||
&self,
|
||||
buffer: &Entity<Buffer>,
|
||||
position: language::Anchor,
|
||||
text: &str,
|
||||
trigger_in_words: bool,
|
||||
menu_is_open: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool {
|
||||
let mut chars = text.chars();
|
||||
@@ -581,9 +570,6 @@ 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)
|
||||
|
||||
@@ -32,6 +32,8 @@ settings.workspace = true
|
||||
supermaven.workspace = true
|
||||
telemetry.workspace = true
|
||||
ui.workspace = true
|
||||
ui_input.workspace = true
|
||||
menu.workspace = true
|
||||
util.workspace = true
|
||||
workspace.workspace = true
|
||||
zed_actions.workspace = true
|
||||
|
||||
@@ -1,3 +1,7 @@
|
||||
mod sweep_api_token_modal;
|
||||
|
||||
pub use sweep_api_token_modal::SweepApiKeyModal;
|
||||
|
||||
use anyhow::Result;
|
||||
use client::{Client, UserStore, zed_urls};
|
||||
use cloud_llm_client::UsageLimit;
|
||||
@@ -40,8 +44,7 @@ use workspace::{
|
||||
notifications::NotificationId,
|
||||
};
|
||||
use zed_actions::OpenBrowser;
|
||||
use zeta::RateCompletions;
|
||||
use zeta::{SweepFeatureFlag, Zeta2FeatureFlag};
|
||||
use zeta::{RateCompletions, SweepFeatureFlag, Zeta2FeatureFlag};
|
||||
|
||||
actions!(
|
||||
edit_prediction,
|
||||
@@ -313,6 +316,10 @@ impl Render for EditPredictionButton {
|
||||
)
|
||||
);
|
||||
|
||||
let sweep_missing_token = is_sweep
|
||||
&& !zeta::Zeta::try_global(cx)
|
||||
.map_or(false, |zeta| zeta.read(cx).has_sweep_api_token());
|
||||
|
||||
let zeta_icon = match (is_sweep, enabled) {
|
||||
(true, _) => IconName::SweepAi,
|
||||
(false, true) => IconName::ZedPredict,
|
||||
@@ -360,19 +367,24 @@ impl Render for EditPredictionButton {
|
||||
let show_editor_predictions = self.editor_show_predictions;
|
||||
let user = self.user_store.read(cx).current_user();
|
||||
|
||||
let indicator_color = if sweep_missing_token {
|
||||
Some(Color::Error)
|
||||
} else if enabled && (!show_editor_predictions || over_limit) {
|
||||
Some(if over_limit {
|
||||
Color::Error
|
||||
} else {
|
||||
Color::Muted
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let icon_button = IconButton::new("zed-predict-pending-button", zeta_icon)
|
||||
.shape(IconButtonShape::Square)
|
||||
.when(
|
||||
enabled && (!show_editor_predictions || over_limit),
|
||||
|this| {
|
||||
this.indicator(Indicator::dot().when_else(
|
||||
over_limit,
|
||||
|dot| dot.color(Color::Error),
|
||||
|dot| dot.color(Color::Muted),
|
||||
))
|
||||
.when_some(indicator_color, |this, color| {
|
||||
this.indicator(Indicator::dot().color(color))
|
||||
.indicator_border_color(Some(cx.theme().colors().status_bar_background))
|
||||
},
|
||||
)
|
||||
})
|
||||
.when(!self.popover_menu_handle.is_deployed(), |element| {
|
||||
let user = user.clone();
|
||||
element.tooltip(move |_window, cx| {
|
||||
@@ -537,23 +549,23 @@ impl EditPredictionButton {
|
||||
|
||||
const ZED_AI_CALLOUT: &str =
|
||||
"Zed's edit prediction is powered by Zeta, an open-source, dataset mode.";
|
||||
const USE_SWEEP_API_TOKEN_CALLOUT: &str =
|
||||
"Set the SWEEP_API_TOKEN environment variable to use Sweep";
|
||||
|
||||
let other_providers: Vec<_> = available_providers
|
||||
let providers: Vec<_> = available_providers
|
||||
.into_iter()
|
||||
.filter(|p| *p != current_provider && *p != EditPredictionProvider::None)
|
||||
.filter(|p| *p != EditPredictionProvider::None)
|
||||
.collect();
|
||||
|
||||
if !other_providers.is_empty() {
|
||||
menu = menu.separator().header("Switch Providers");
|
||||
if !providers.is_empty() {
|
||||
menu = menu.separator().header("Providers");
|
||||
|
||||
for provider in other_providers {
|
||||
for provider in providers {
|
||||
let is_current = provider == current_provider;
|
||||
let fs = self.fs.clone();
|
||||
|
||||
menu = match provider {
|
||||
EditPredictionProvider::Zed => menu.item(
|
||||
ContextMenuEntry::new("Zed AI")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.documentation_aside(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Bottom,
|
||||
@@ -563,46 +575,77 @@ impl EditPredictionButton {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
EditPredictionProvider::Copilot => {
|
||||
menu.entry("GitHub Copilot", None, move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
})
|
||||
}
|
||||
EditPredictionProvider::Supermaven => {
|
||||
menu.entry("Supermaven", None, move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
})
|
||||
}
|
||||
EditPredictionProvider::Codestral => {
|
||||
menu.entry("Codestral", None, move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
})
|
||||
}
|
||||
EditPredictionProvider::Copilot => menu.item(
|
||||
ContextMenuEntry::new("GitHub Copilot")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
EditPredictionProvider::Supermaven => menu.item(
|
||||
ContextMenuEntry::new("Supermaven")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
EditPredictionProvider::Codestral => menu.item(
|
||||
ContextMenuEntry::new("Codestral")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_SWEEP_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
) => {
|
||||
let has_api_token = zeta::Zeta::try_global(cx)
|
||||
.map_or(false, |zeta| zeta.read(cx).has_sweep_api_token());
|
||||
|
||||
let entry = ContextMenuEntry::new("Sweep")
|
||||
.when(!has_api_token, |this| {
|
||||
this.disabled(true).documentation_aside(
|
||||
let should_open_modal = !has_api_token || is_current;
|
||||
|
||||
let entry = if has_api_token {
|
||||
ContextMenuEntry::new("Sweep")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
} else {
|
||||
ContextMenuEntry::new("Sweep")
|
||||
.icon(IconName::XCircle)
|
||||
.icon_color(Color::Error)
|
||||
.documentation_aside(
|
||||
DocumentationSide::Left,
|
||||
DocumentationEdge::Bottom,
|
||||
|_| Label::new(USE_SWEEP_API_TOKEN_CALLOUT).into_any_element(),
|
||||
|_| {
|
||||
Label::new("Click to configure your Sweep API token")
|
||||
.into_any_element()
|
||||
},
|
||||
)
|
||||
})
|
||||
.handler(move |_, cx| {
|
||||
};
|
||||
|
||||
let entry = entry.handler(move |window, cx| {
|
||||
if should_open_modal {
|
||||
if let Some(workspace) = window.root::<Workspace>().flatten() {
|
||||
workspace.update(cx, |workspace, cx| {
|
||||
workspace.toggle_modal(window, cx, |window, cx| {
|
||||
SweepApiKeyModal::new(window, cx)
|
||||
});
|
||||
});
|
||||
};
|
||||
} else {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
menu.item(entry)
|
||||
}
|
||||
EditPredictionProvider::Experimental(
|
||||
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
|
||||
) => menu.entry("Zeta2", None, move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
) => menu.item(
|
||||
ContextMenuEntry::new("Zeta2")
|
||||
.toggleable(IconPosition::Start, is_current)
|
||||
.handler(move |_, cx| {
|
||||
set_completion_provider(fs.clone(), cx, provider);
|
||||
}),
|
||||
),
|
||||
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => {
|
||||
continue;
|
||||
}
|
||||
@@ -1078,8 +1121,8 @@ impl EditPredictionButton {
|
||||
menu = menu
|
||||
.custom_row(move |_window, cx| {
|
||||
let description = indoc! {
|
||||
"Sign in for 2,000 worth of accepted suggestions at every keystroke, \
|
||||
powered by Zeta, our open-source, open-data model."
|
||||
"You get 2,000 accepted suggestions at every keystroke for free, \
|
||||
powered by Zeta, our open-source, open-data model"
|
||||
};
|
||||
|
||||
v_flex()
|
||||
@@ -1332,21 +1375,28 @@ fn render_zeta_tab_animation(cx: &App) -> impl IntoElement {
|
||||
.child("tab")
|
||||
.with_animation(
|
||||
ElementId::Integer(n),
|
||||
Animation::new(Duration::from_secs(4)).repeat(),
|
||||
Animation::new(Duration::from_secs(3)).repeat(),
|
||||
move |tab, delta| {
|
||||
let n_f32 = n as f32;
|
||||
|
||||
let delta = if inverted {
|
||||
(delta - 0.15 * (5.0 - n_f32)) / 0.7
|
||||
let offset = if inverted {
|
||||
0.2 * (4.0 - n_f32)
|
||||
} else {
|
||||
(delta - 0.15 * n_f32) / 0.7
|
||||
0.2 * n_f32
|
||||
};
|
||||
|
||||
let delta = 1.0 - (0.5 - delta).abs() * 2.;
|
||||
let delta = ease_in_out(delta.clamp(0., 1.));
|
||||
let delta = 0.1 + 0.5 * delta;
|
||||
let phase = (delta - offset + 1.0) % 1.0;
|
||||
let pulse = if phase < 0.6 {
|
||||
let t = phase / 0.6;
|
||||
1.0 - (0.5 - t).abs() * 2.0
|
||||
} else {
|
||||
0.0
|
||||
};
|
||||
|
||||
tab.text_color(text_color.opacity(delta))
|
||||
let eased = ease_in_out(pulse);
|
||||
let opacity = 0.1 + 0.5 * eased;
|
||||
|
||||
tab.text_color(text_color.opacity(opacity))
|
||||
},
|
||||
),
|
||||
)
|
||||
|
||||
84
crates/edit_prediction_button/src/sweep_api_token_modal.rs
Normal file
@@ -0,0 +1,84 @@
|
||||
use gpui::{
|
||||
DismissEvent, Entity, EventEmitter, FocusHandle, Focusable, IntoElement, ParentElement, Render,
|
||||
};
|
||||
use ui::{Button, ButtonStyle, Clickable, Headline, HeadlineSize, prelude::*};
|
||||
use ui_input::InputField;
|
||||
use workspace::ModalView;
|
||||
use zeta::Zeta;
|
||||
|
||||
pub struct SweepApiKeyModal {
|
||||
api_key_input: Entity<InputField>,
|
||||
focus_handle: FocusHandle,
|
||||
}
|
||||
|
||||
impl SweepApiKeyModal {
|
||||
pub fn new(window: &mut Window, cx: &mut Context<Self>) -> Self {
|
||||
let api_key_input = cx.new(|cx| InputField::new(window, cx, "Enter your Sweep API token"));
|
||||
|
||||
Self {
|
||||
api_key_input,
|
||||
focus_handle: cx.focus_handle(),
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel(&mut self, _: &menu::Cancel, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
|
||||
fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
|
||||
let api_key = self.api_key_input.read(cx).text(cx);
|
||||
let api_key = (!api_key.trim().is_empty()).then_some(api_key);
|
||||
|
||||
if let Some(zeta) = Zeta::try_global(cx) {
|
||||
zeta.update(cx, |zeta, cx| {
|
||||
zeta.sweep_ai
|
||||
.set_api_token(api_key, cx)
|
||||
.detach_and_log_err(cx);
|
||||
});
|
||||
}
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
impl EventEmitter<DismissEvent> for SweepApiKeyModal {}
|
||||
|
||||
impl ModalView for SweepApiKeyModal {}
|
||||
|
||||
impl Focusable for SweepApiKeyModal {
|
||||
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
||||
self.focus_handle.clone()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for SweepApiKeyModal {
|
||||
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
v_flex()
|
||||
.key_context("SweepApiKeyModal")
|
||||
.on_action(cx.listener(Self::cancel))
|
||||
.on_action(cx.listener(Self::confirm))
|
||||
.elevation_2(cx)
|
||||
.w(px(400.))
|
||||
.p_4()
|
||||
.gap_3()
|
||||
.child(Headline::new("Sweep API Token").size(HeadlineSize::Small))
|
||||
.child(self.api_key_input.clone())
|
||||
.child(
|
||||
h_flex()
|
||||
.justify_end()
|
||||
.gap_2()
|
||||
.child(Button::new("cancel", "Cancel").on_click(cx.listener(
|
||||
|_, _, _window, cx| {
|
||||
cx.emit(DismissEvent);
|
||||
},
|
||||
)))
|
||||
.child(
|
||||
Button::new("save", "Save")
|
||||
.style(ButtonStyle::Filled)
|
||||
.on_click(cx.listener(|this, _, window, cx| {
|
||||
this.confirm(&menu::Confirm, window, cx);
|
||||
})),
|
||||
),
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -118,6 +118,7 @@ tree-sitter-rust.workspace = true
|
||||
tree-sitter-typescript.workspace = true
|
||||
tree-sitter-yaml.workspace = true
|
||||
tree-sitter-bash.workspace = true
|
||||
tree-sitter-md.workspace = true
|
||||
unindent.workspace = true
|
||||
util = { workspace = true, features = ["test-support"] }
|
||||
workspace = { workspace = true, features = ["test-support"] }
|
||||
|
||||
@@ -43,7 +43,7 @@ impl Editor {
|
||||
.collect_array()
|
||||
};
|
||||
|
||||
let bracket_matches_by_accent = self.visible_excerpts(cx).into_iter().fold(
|
||||
let bracket_matches_by_accent = self.visible_excerpts(false, cx).into_iter().fold(
|
||||
HashMap::default(),
|
||||
|mut acc, (excerpt_id, (buffer, buffer_version, buffer_range))| {
|
||||
let buffer_snapshot = buffer.read(cx).snapshot();
|
||||
@@ -164,7 +164,7 @@ mod tests {
|
||||
|
||||
use super::*;
|
||||
use crate::{
|
||||
DisplayPoint, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
|
||||
DisplayPoint, EditorMode, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
|
||||
display_map::{DisplayRow, ToDisplayPoint},
|
||||
editor_tests::init_test,
|
||||
test::{
|
||||
@@ -276,6 +276,40 @@ where
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_file_less_file_colorization(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
language_settings.defaults.colorize_brackets = Some(true);
|
||||
});
|
||||
let editor = cx.add_window(|window, cx| {
|
||||
let multi_buffer = MultiBuffer::build_simple("fn main() {}", cx);
|
||||
multi_buffer.update(cx, |multi_buffer, cx| {
|
||||
multi_buffer
|
||||
.as_singleton()
|
||||
.unwrap()
|
||||
.update(cx, |buffer, cx| {
|
||||
buffer.set_language(Some(rust_lang()), cx);
|
||||
});
|
||||
});
|
||||
Editor::new(EditorMode::full(), multi_buffer, None, window, cx)
|
||||
});
|
||||
|
||||
cx.executor().advance_clock(Duration::from_millis(100));
|
||||
cx.executor().run_until_parked();
|
||||
|
||||
assert_eq!(
|
||||
"fn main«1()1» «1{}1»
|
||||
1 hsla(207.80, 16.20%, 69.19%, 1.00)
|
||||
",
|
||||
editor
|
||||
.update(cx, |editor, window, cx| {
|
||||
editor_bracket_colors_markup(&editor.snapshot(window, cx))
|
||||
})
|
||||
.unwrap(),
|
||||
"File-less buffer should still have its brackets colorized"
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_markdown_bracket_colorization(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |language_settings| {
|
||||
@@ -299,6 +333,19 @@ 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]
|
||||
|
||||
@@ -181,6 +181,8 @@ impl DisplayMap {
|
||||
.update(cx, |map, cx| map.sync(tab_snapshot, edits, cx));
|
||||
let block_snapshot = self.block_map.read(wrap_snapshot, edits).snapshot;
|
||||
|
||||
// todo word diff here?
|
||||
|
||||
DisplaySnapshot {
|
||||
block_snapshot,
|
||||
diagnostics_max_severity: self.diagnostics_max_severity,
|
||||
|
||||
@@ -146,8 +146,8 @@ use persistence::DB;
|
||||
use project::{
|
||||
BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
|
||||
CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
|
||||
InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem,
|
||||
ProjectPath, ProjectTransaction, TaskSourceKind,
|
||||
InvalidationStrategy, Location, LocationLink, LspAction, PrepareRenameResponse, Project,
|
||||
ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind,
|
||||
debugger::{
|
||||
breakpoint_store::{
|
||||
Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
|
||||
@@ -182,7 +182,7 @@ use std::{
|
||||
iter::{self, Peekable},
|
||||
mem,
|
||||
num::NonZeroU32,
|
||||
ops::{Deref, DerefMut, Not, Range, RangeInclusive},
|
||||
ops::{ControlFlow, Deref, DerefMut, Not, Range, RangeInclusive},
|
||||
path::{Path, PathBuf},
|
||||
rc::Rc,
|
||||
sync::Arc,
|
||||
@@ -191,7 +191,7 @@ use std::{
|
||||
use task::{ResolvedTask, RunnableTag, TaskTemplate, TaskVariables};
|
||||
use text::{BufferId, FromAnchor, OffsetUtf16, Rope, ToOffset as _};
|
||||
use theme::{
|
||||
ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
|
||||
AccentColors, ActiveTheme, PlayerColor, StatusColors, SyntaxTheme, Theme, ThemeSettings,
|
||||
observe_buffer_font_size_adjustment,
|
||||
};
|
||||
use ui::{
|
||||
@@ -284,6 +284,9 @@ pub enum ConflictsTheirs {}
|
||||
pub enum ConflictsOursMarker {}
|
||||
pub enum ConflictsTheirsMarker {}
|
||||
|
||||
pub struct HunkAddedColor;
|
||||
pub struct HunkRemovedColor;
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Eq)]
|
||||
pub enum Navigated {
|
||||
Yes,
|
||||
@@ -307,6 +310,7 @@ enum DisplayDiffHunk {
|
||||
display_row_range: Range<DisplayRow>,
|
||||
multi_buffer_range: Range<Anchor>,
|
||||
status: DiffHunkStatus,
|
||||
word_diffs: Vec<Range<MultiBufferOffset>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -722,7 +726,10 @@ impl EditorActionId {
|
||||
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
|
||||
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
|
||||
|
||||
type BackgroundHighlight = (fn(&Theme) -> Hsla, Arc<[Range<Anchor>]>);
|
||||
type BackgroundHighlight = (
|
||||
Arc<dyn Fn(&usize, &Theme) -> Hsla + Send + Sync>,
|
||||
Arc<[Range<Anchor>]>,
|
||||
);
|
||||
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
|
||||
|
||||
#[derive(Default)]
|
||||
@@ -1072,6 +1079,7 @@ pub struct Editor {
|
||||
show_breakpoints: Option<bool>,
|
||||
show_wrap_guides: Option<bool>,
|
||||
show_indent_guides: Option<bool>,
|
||||
buffers_with_disabled_indent_guides: HashSet<BufferId>,
|
||||
highlight_order: usize,
|
||||
highlighted_rows: HashMap<TypeId, Vec<RowHighlight>>,
|
||||
background_highlights: HashMap<HighlightKey, BackgroundHighlight>,
|
||||
@@ -1168,6 +1176,7 @@ 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>,
|
||||
@@ -1198,11 +1207,17 @@ pub struct Editor {
|
||||
select_next_is_case_sensitive: Option<bool>,
|
||||
pub lookup_key: Option<Box<dyn Any + Send + Sync>>,
|
||||
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
|
||||
accent_overrides: Vec<SharedString>,
|
||||
accent_data: Option<AccentData>,
|
||||
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
|
||||
use_base_text_line_numbers: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, PartialEq)]
|
||||
struct AccentData {
|
||||
colors: AccentColors,
|
||||
overrides: Vec<SharedString>,
|
||||
}
|
||||
|
||||
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
|
||||
if debounce_ms > 0 {
|
||||
Some(Duration::from_millis(debounce_ms))
|
||||
@@ -2190,6 +2205,7 @@ impl Editor {
|
||||
show_breakpoints: None,
|
||||
show_wrap_guides: None,
|
||||
show_indent_guides,
|
||||
buffers_with_disabled_indent_guides: HashSet::default(),
|
||||
highlight_order: 0,
|
||||
highlighted_rows: HashMap::default(),
|
||||
background_highlights: HashMap::default(),
|
||||
@@ -2312,6 +2328,7 @@ 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,
|
||||
@@ -2345,7 +2362,7 @@ impl Editor {
|
||||
lookup_key: None,
|
||||
select_next_is_case_sensitive: None,
|
||||
applicable_language_settings: HashMap::default(),
|
||||
accent_overrides: Vec::new(),
|
||||
accent_data: None,
|
||||
fetched_tree_sitter_chunks: HashMap::default(),
|
||||
use_base_text_line_numbers: false,
|
||||
};
|
||||
@@ -2355,7 +2372,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
editor.applicable_language_settings = editor.fetch_applicable_language_settings(cx);
|
||||
editor.accent_overrides = editor.fetch_accent_overrides(cx);
|
||||
editor.accent_data = editor.fetch_accent_data(cx);
|
||||
|
||||
if let Some(breakpoints) = editor.breakpoint_store.as_ref() {
|
||||
editor
|
||||
@@ -2488,7 +2505,6 @@ 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);
|
||||
}
|
||||
|
||||
@@ -5308,12 +5324,10 @@ impl Editor {
|
||||
|
||||
pub fn visible_excerpts(
|
||||
&self,
|
||||
lsp_related_only: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
|
||||
let Some(project) = self.project() else {
|
||||
return HashMap::default();
|
||||
};
|
||||
let project = project.read(cx);
|
||||
let project = self.project().cloned();
|
||||
let multi_buffer = self.buffer().read(cx);
|
||||
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
|
||||
let multi_buffer_visible_start = self
|
||||
@@ -5331,6 +5345,18 @@ impl Editor {
|
||||
.into_iter()
|
||||
.filter(|(_, excerpt_visible_range, _)| !excerpt_visible_range.is_empty())
|
||||
.filter_map(|(buffer, excerpt_visible_range, excerpt_id)| {
|
||||
if !lsp_related_only {
|
||||
return Some((
|
||||
excerpt_id,
|
||||
(
|
||||
multi_buffer.buffer(buffer.remote_id()).unwrap(),
|
||||
buffer.version().clone(),
|
||||
excerpt_visible_range.start.0..excerpt_visible_range.end.0,
|
||||
),
|
||||
));
|
||||
}
|
||||
|
||||
let project = project.as_ref()?.read(cx);
|
||||
let buffer_file = project::File::from_dyn(buffer.file())?;
|
||||
let buffer_worktree = project.worktree_for_id(buffer_file.worktree_id(cx), cx)?;
|
||||
let worktree_entry = buffer_worktree
|
||||
@@ -5495,6 +5521,22 @@ 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());
|
||||
@@ -5503,14 +5545,8 @@ 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() {
|
||||
let menu_is_open = matches!(
|
||||
self.context_menu.borrow().as_ref(),
|
||||
Some(CodeContextMenu::Completions(_))
|
||||
);
|
||||
if menu_is_open {
|
||||
self.hide_context_menu(window, cx);
|
||||
}
|
||||
if query.is_none() && menu_is_open {
|
||||
self.hide_context_menu(window, cx);
|
||||
}
|
||||
|
||||
let mut ignore_word_threshold = false;
|
||||
@@ -5599,14 +5635,6 @@ 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;
|
||||
@@ -5637,7 +5665,6 @@ impl Editor {
|
||||
position.text_anchor,
|
||||
trigger,
|
||||
trigger_in_words,
|
||||
completions_source.is_some(),
|
||||
cx,
|
||||
)
|
||||
})
|
||||
@@ -6137,9 +6164,43 @@ impl Editor {
|
||||
}
|
||||
|
||||
let provider = self.completion_provider.as_ref()?;
|
||||
|
||||
let lsp_store = self.project().map(|project| project.read(cx).lsp_store());
|
||||
let command = lsp_store.as_ref().and_then(|lsp_store| {
|
||||
let CompletionSource::Lsp {
|
||||
lsp_completion,
|
||||
server_id,
|
||||
..
|
||||
} = &completion.source
|
||||
else {
|
||||
return None;
|
||||
};
|
||||
let lsp_command = lsp_completion.command.as_ref()?;
|
||||
let available_commands = lsp_store
|
||||
.read(cx)
|
||||
.lsp_server_capabilities
|
||||
.get(server_id)
|
||||
.and_then(|server_capabilities| {
|
||||
server_capabilities
|
||||
.execute_command_provider
|
||||
.as_ref()
|
||||
.map(|options| options.commands.as_slice())
|
||||
})?;
|
||||
if available_commands.contains(&lsp_command.command) {
|
||||
Some(CodeAction {
|
||||
server_id: *server_id,
|
||||
range: language::Anchor::MIN..language::Anchor::MIN,
|
||||
lsp_action: LspAction::Command(lsp_command.clone()),
|
||||
resolved: false,
|
||||
})
|
||||
} else {
|
||||
None
|
||||
}
|
||||
});
|
||||
|
||||
drop(completion);
|
||||
let apply_edits = provider.apply_additional_edits_for_completion(
|
||||
buffer_handle,
|
||||
buffer_handle.clone(),
|
||||
completions_menu.completions.clone(),
|
||||
candidate_id,
|
||||
true,
|
||||
@@ -6153,8 +6214,29 @@ impl Editor {
|
||||
self.show_signature_help(&ShowSignatureHelp, window, cx);
|
||||
}
|
||||
|
||||
Some(cx.foreground_executor().spawn(async move {
|
||||
Some(cx.spawn_in(window, async move |editor, cx| {
|
||||
apply_edits.await?;
|
||||
|
||||
if let Some((lsp_store, command)) = lsp_store.zip(command) {
|
||||
let title = command.lsp_action.title().to_owned();
|
||||
let project_transaction = lsp_store
|
||||
.update(cx, |lsp_store, cx| {
|
||||
lsp_store.apply_code_action(buffer_handle, command, false, cx)
|
||||
})?
|
||||
.await
|
||||
.context("applying post-completion command")?;
|
||||
if let Some(workspace) = editor.read_with(cx, |editor, _| editor.workspace())? {
|
||||
Self::open_project_transaction(
|
||||
&editor,
|
||||
workspace.downgrade(),
|
||||
project_transaction,
|
||||
title,
|
||||
cx,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}))
|
||||
}
|
||||
@@ -6539,7 +6621,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,
|
||||
);
|
||||
});
|
||||
@@ -6740,6 +6822,9 @@ impl Editor {
|
||||
return;
|
||||
};
|
||||
|
||||
if self.blame.is_none() {
|
||||
self.start_git_blame(true, window, cx);
|
||||
}
|
||||
let Some(blame) = self.blame.as_ref() else {
|
||||
return;
|
||||
};
|
||||
@@ -6938,12 +7023,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();
|
||||
@@ -7051,7 +7136,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,
|
||||
)
|
||||
}
|
||||
@@ -7990,10 +8075,17 @@ impl Editor {
|
||||
|
||||
if self.edit_prediction_indent_conflict {
|
||||
let cursor_point = cursor.to_point(&multibuffer);
|
||||
let mut suggested_indent = None;
|
||||
multibuffer.suggested_indents_callback(
|
||||
cursor_point.row..cursor_point.row + 1,
|
||||
|_, indent| {
|
||||
suggested_indent = Some(indent);
|
||||
ControlFlow::Break(())
|
||||
},
|
||||
cx,
|
||||
);
|
||||
|
||||
let indents = multibuffer.suggested_indents(cursor_point.row..cursor_point.row + 1, cx);
|
||||
|
||||
if let Some((_, indent)) = indents.iter().next()
|
||||
if let Some(indent) = suggested_indent
|
||||
&& indent.len == cursor_point.column
|
||||
{
|
||||
self.edit_prediction_indent_conflict = false;
|
||||
@@ -16938,7 +17030,9 @@ impl Editor {
|
||||
})
|
||||
.collect();
|
||||
|
||||
let workspace = self.workspace();
|
||||
let Some(workspace) = self.workspace() else {
|
||||
return Task::ready(Ok(Navigated::No));
|
||||
};
|
||||
|
||||
cx.spawn_in(window, async move |editor, cx| {
|
||||
let locations: Vec<Location> = future::join_all(definitions)
|
||||
@@ -16964,10 +17058,6 @@ 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",
|
||||
@@ -16999,11 +17089,14 @@ 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,
|
||||
@@ -17020,10 +17113,9 @@ impl Editor {
|
||||
Ok(Navigated::Yes)
|
||||
}
|
||||
Some(Either::Right(path)) => {
|
||||
let Some(workspace) = workspace else {
|
||||
return Ok(Navigated::No);
|
||||
};
|
||||
|
||||
// TODO(andrew): respect preview tab settings
|
||||
// `enable_keep_preview_on_code_navigation` and
|
||||
// `enable_preview_file_from_code_navigation`
|
||||
workspace
|
||||
.update_in(cx, |workspace, window, cx| {
|
||||
workspace.open_resolved_path(path, window, cx)
|
||||
@@ -17034,10 +17126,6 @@ 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();
|
||||
|
||||
@@ -17061,11 +17149,19 @@ 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,
|
||||
)
|
||||
@@ -17342,11 +17438,14 @@ 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,
|
||||
@@ -17362,6 +17461,7 @@ 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>,
|
||||
@@ -17409,6 +17509,7 @@ 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(
|
||||
@@ -17436,7 +17537,7 @@ impl Editor {
|
||||
}
|
||||
editor.highlight_background::<Self>(
|
||||
&ranges,
|
||||
|theme| theme.colors().editor_highlighted_line_background,
|
||||
|_, theme| theme.colors().editor_highlighted_line_background,
|
||||
cx,
|
||||
);
|
||||
}
|
||||
@@ -17449,29 +17550,23 @@ impl Editor {
|
||||
});
|
||||
|
||||
let item = Box::new(editor);
|
||||
let item_id = item.item_id();
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
let pane = if split {
|
||||
workspace.adjacent_pane(window, cx)
|
||||
} else {
|
||||
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);
|
||||
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);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -18327,54 +18422,101 @@ impl Editor {
|
||||
return None;
|
||||
}
|
||||
let project = self.project()?.downgrade();
|
||||
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() {
|
||||
|
||||
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() {
|
||||
self.pull_diagnostics_task = Task::ready(());
|
||||
self.pull_diagnostics_background_task = Task::ready(());
|
||||
return None;
|
||||
}
|
||||
|
||||
self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| {
|
||||
cx.background_executor().timer(debounce).await;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
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;
|
||||
}
|
||||
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);
|
||||
}
|
||||
Err(e) => log::error!("Failed to update project diagnostics: {e:#}"),
|
||||
}
|
||||
false
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
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)
|
||||
})
|
||||
})
|
||||
.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:#}");
|
||||
}
|
||||
}
|
||||
})
|
||||
};
|
||||
|
||||
self.pull_diagnostics_task = make_spawn(edited_buffers, debounce);
|
||||
self.pull_diagnostics_background_task = make_spawn(related_open_buffers, debounce * 2);
|
||||
|
||||
Some(())
|
||||
}
|
||||
@@ -19447,6 +19589,10 @@ impl Editor {
|
||||
&hunks
|
||||
.map(|hunk| buffer_diff::DiffHunk {
|
||||
buffer_range: hunk.buffer_range,
|
||||
// We don't need to pass in word diffs here because they're only used for rendering and
|
||||
// this function changes internal state
|
||||
base_word_diffs: Vec::default(),
|
||||
buffer_word_diffs: Vec::default(),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.start.0
|
||||
..hunk.diff_base_byte_range.end.0,
|
||||
secondary_status: hunk.secondary_status,
|
||||
@@ -19946,6 +20092,20 @@ impl Editor {
|
||||
self.show_indent_guides
|
||||
}
|
||||
|
||||
pub fn disable_indent_guides_for_buffer(
|
||||
&mut self,
|
||||
buffer_id: BufferId,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.buffers_with_disabled_indent_guides.insert(buffer_id);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
pub fn has_indent_guides_disabled_for_buffer(&self, buffer_id: BufferId) -> bool {
|
||||
self.buffers_with_disabled_indent_guides
|
||||
.contains(&buffer_id)
|
||||
}
|
||||
|
||||
pub fn toggle_line_numbers(
|
||||
&mut self,
|
||||
_: &ToggleLineNumbers,
|
||||
@@ -20658,6 +20818,7 @@ impl Editor {
|
||||
locations,
|
||||
format!("Selections for '{title}'"),
|
||||
false,
|
||||
false,
|
||||
MultibufferSelectionMode::All,
|
||||
window,
|
||||
cx,
|
||||
@@ -20860,7 +21021,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,
|
||||
)
|
||||
}
|
||||
@@ -20876,12 +21037,12 @@ impl Editor {
|
||||
pub fn highlight_background<T: 'static>(
|
||||
&mut self,
|
||||
ranges: &[Range<Anchor>],
|
||||
color_fetcher: fn(&Theme) -> Hsla,
|
||||
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.background_highlights.insert(
|
||||
HighlightKey::Type(TypeId::of::<T>()),
|
||||
(color_fetcher, Arc::from(ranges)),
|
||||
(Arc::new(color_fetcher), Arc::from(ranges)),
|
||||
);
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
cx.notify();
|
||||
@@ -20891,12 +21052,12 @@ impl Editor {
|
||||
&mut self,
|
||||
key: usize,
|
||||
ranges: &[Range<Anchor>],
|
||||
color_fetcher: fn(&Theme) -> Hsla,
|
||||
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
self.background_highlights.insert(
|
||||
HighlightKey::TypePlus(TypeId::of::<T>(), key),
|
||||
(color_fetcher, Arc::from(ranges)),
|
||||
(Arc::new(color_fetcher), Arc::from(ranges)),
|
||||
);
|
||||
self.scrollbar_marker_state.dirty = true;
|
||||
cx.notify();
|
||||
@@ -21121,7 +21282,6 @@ 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
|
||||
@@ -21134,7 +21294,7 @@ impl Editor {
|
||||
}) {
|
||||
Ok(i) | Err(i) => i,
|
||||
};
|
||||
for range in &ranges[start_ix..] {
|
||||
for (index, range) in ranges[start_ix..].iter().enumerate() {
|
||||
if range
|
||||
.start
|
||||
.cmp(&search_range.end, &display_snapshot.buffer_snapshot())
|
||||
@@ -21143,6 +21303,7 @@ 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))
|
||||
@@ -21574,16 +21735,18 @@ impl Editor {
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn fetch_accent_overrides(&self, cx: &App) -> Vec<SharedString> {
|
||||
fn fetch_accent_data(&self, cx: &App) -> Option<AccentData> {
|
||||
if !self.mode.is_full() {
|
||||
return Vec::new();
|
||||
return None;
|
||||
}
|
||||
|
||||
let theme_settings = theme::ThemeSettings::get_global(cx);
|
||||
let theme = cx.theme();
|
||||
let accent_colors = theme.accents().clone();
|
||||
|
||||
theme_settings
|
||||
let accent_overrides = theme_settings
|
||||
.theme_overrides
|
||||
.get(cx.theme().name.as_ref())
|
||||
.get(theme.name.as_ref())
|
||||
.map(|theme_style| &theme_style.accents)
|
||||
.into_iter()
|
||||
.flatten()
|
||||
@@ -21596,7 +21759,12 @@ impl Editor {
|
||||
.flatten(),
|
||||
)
|
||||
.flat_map(|accent| accent.0.clone())
|
||||
.collect()
|
||||
.collect();
|
||||
|
||||
Some(AccentData {
|
||||
colors: accent_colors,
|
||||
overrides: accent_overrides,
|
||||
})
|
||||
}
|
||||
|
||||
fn fetch_applicable_language_settings(
|
||||
@@ -21626,9 +21794,9 @@ impl Editor {
|
||||
let language_settings_changed = new_language_settings != self.applicable_language_settings;
|
||||
self.applicable_language_settings = new_language_settings;
|
||||
|
||||
let new_accent_overrides = self.fetch_accent_overrides(cx);
|
||||
let accent_overrides_changed = new_accent_overrides != self.accent_overrides;
|
||||
self.accent_overrides = new_accent_overrides;
|
||||
let new_accents = self.fetch_accent_data(cx);
|
||||
let accents_changed = new_accents != self.accent_data;
|
||||
self.accent_data = new_accents;
|
||||
|
||||
if self.diagnostics_enabled() {
|
||||
let new_severity = EditorSettings::get_global(cx)
|
||||
@@ -21702,7 +21870,7 @@ impl Editor {
|
||||
}
|
||||
}
|
||||
|
||||
if language_settings_changed || accent_overrides_changed {
|
||||
if language_settings_changed || accents_changed {
|
||||
self.colorize_brackets(true, cx);
|
||||
}
|
||||
|
||||
@@ -21862,43 +22030,64 @@ impl Editor {
|
||||
};
|
||||
|
||||
for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer {
|
||||
let editor = buffer
|
||||
.read(cx)
|
||||
.file()
|
||||
.is_none()
|
||||
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)
|
||||
.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) =
|
||||
let (editor, pane_item_index, pane_item_id) =
|
||||
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))
|
||||
Some((editor, i, item.item_id()))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})?;
|
||||
pane.update(cx, |pane, cx| {
|
||||
pane.activate_item(pane_item_index, true, true, window, 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);
|
||||
}
|
||||
});
|
||||
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(),
|
||||
@@ -21922,10 +22111,11 @@ impl Editor {
|
||||
});
|
||||
}
|
||||
|
||||
// For now, don't allow opening excerpts in buffers that aren't backed by
|
||||
// regular project files.
|
||||
// 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.
|
||||
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_none_or(|file| project::File::from_dyn(Some(file)).is_some() || !file.is_local())
|
||||
}
|
||||
|
||||
fn marked_text_ranges(&self, cx: &App) -> Option<Vec<Range<MultiBufferOffsetUtf16>>> {
|
||||
@@ -22524,6 +22714,10 @@ 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()
|
||||
}
|
||||
@@ -22592,7 +22786,7 @@ impl Editor {
|
||||
if self.ignore_lsp_data() {
|
||||
return;
|
||||
}
|
||||
for (_, (visible_buffer, _, _)) in self.visible_excerpts(cx) {
|
||||
for (_, (visible_buffer, _, _)) in self.visible_excerpts(true, cx) {
|
||||
self.register_buffer(visible_buffer.read(cx).remote_id(), cx);
|
||||
}
|
||||
}
|
||||
@@ -23413,7 +23607,6 @@ pub trait CompletionProvider {
|
||||
position: language::Anchor,
|
||||
text: &str,
|
||||
trigger_in_words: bool,
|
||||
menu_is_open: bool,
|
||||
cx: &mut Context<Editor>,
|
||||
) -> bool;
|
||||
|
||||
@@ -23792,7 +23985,6 @@ 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();
|
||||
@@ -23807,9 +23999,6 @@ 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));
|
||||
@@ -24116,10 +24305,12 @@ impl EditorSnapshot {
|
||||
end_row.0 += 1;
|
||||
}
|
||||
let is_created_file = hunk.is_created_file();
|
||||
|
||||
DisplayDiffHunk::Unfolded {
|
||||
status: hunk.status(),
|
||||
diff_base_byte_range: hunk.diff_base_byte_range.start.0
|
||||
..hunk.diff_base_byte_range.end.0,
|
||||
word_diffs: hunk.word_diffs,
|
||||
display_row_range: hunk_display_start.row()..end_row,
|
||||
multi_buffer_range: Anchor::range_in_buffer(
|
||||
hunk.excerpt_id,
|
||||
|
||||
@@ -14755,6 +14755,180 @@ async fn test_completion(cx: &mut TestAppContext) {
|
||||
apply_additional_edits.await.unwrap();
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_can_run_commands(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_tree(
|
||||
path!("/a"),
|
||||
json!({
|
||||
"main.rs": "",
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/a").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
language_registry.add(rust_lang());
|
||||
let command_calls = Arc::new(AtomicUsize::new(0));
|
||||
let registered_command = "_the/command";
|
||||
|
||||
let closure_command_calls = command_calls.clone();
|
||||
let mut fake_servers = language_registry.register_fake_lsp(
|
||||
"Rust",
|
||||
FakeLspAdapter {
|
||||
capabilities: lsp::ServerCapabilities {
|
||||
completion_provider: Some(lsp::CompletionOptions {
|
||||
trigger_characters: Some(vec![".".to_string(), ":".to_string()]),
|
||||
..lsp::CompletionOptions::default()
|
||||
}),
|
||||
execute_command_provider: Some(lsp::ExecuteCommandOptions {
|
||||
commands: vec![registered_command.to_owned()],
|
||||
..lsp::ExecuteCommandOptions::default()
|
||||
}),
|
||||
..lsp::ServerCapabilities::default()
|
||||
},
|
||||
initializer: Some(Box::new(move |fake_server| {
|
||||
fake_server.set_request_handler::<lsp::request::Completion, _, _>(
|
||||
move |params, _| async move {
|
||||
Ok(Some(lsp::CompletionResponse::Array(vec![
|
||||
lsp::CompletionItem {
|
||||
label: "registered_command".to_owned(),
|
||||
text_edit: gen_text_edit(¶ms, ""),
|
||||
command: Some(lsp::Command {
|
||||
title: registered_command.to_owned(),
|
||||
command: "_the/command".to_owned(),
|
||||
arguments: Some(vec![serde_json::Value::Bool(true)]),
|
||||
}),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
lsp::CompletionItem {
|
||||
label: "unregistered_command".to_owned(),
|
||||
text_edit: gen_text_edit(¶ms, ""),
|
||||
command: Some(lsp::Command {
|
||||
title: "????????????".to_owned(),
|
||||
command: "????????????".to_owned(),
|
||||
arguments: Some(vec![serde_json::Value::Null]),
|
||||
}),
|
||||
..lsp::CompletionItem::default()
|
||||
},
|
||||
])))
|
||||
},
|
||||
);
|
||||
fake_server.set_request_handler::<lsp::request::ExecuteCommand, _, _>({
|
||||
let command_calls = closure_command_calls.clone();
|
||||
move |params, _| {
|
||||
assert_eq!(params.command, registered_command);
|
||||
let command_calls = command_calls.clone();
|
||||
async move {
|
||||
command_calls.fetch_add(1, atomic::Ordering::Release);
|
||||
Ok(Some(json!(null)))
|
||||
}
|
||||
}
|
||||
});
|
||||
})),
|
||||
..FakeLspAdapter::default()
|
||||
},
|
||||
);
|
||||
let workspace = cx.add_window(|window, cx| Workspace::test_new(project.clone(), window, cx));
|
||||
let cx = &mut VisualTestContext::from_window(*workspace, cx);
|
||||
let editor = workspace
|
||||
.update(cx, |workspace, window, cx| {
|
||||
workspace.open_abs_path(
|
||||
PathBuf::from(path!("/a/main.rs")),
|
||||
OpenOptions::default(),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.await
|
||||
.unwrap()
|
||||
.downcast::<Editor>()
|
||||
.unwrap();
|
||||
let _fake_server = fake_servers.next().await.unwrap();
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
cx.focus_self(window);
|
||||
editor.move_to_end(&MoveToEnd, window, cx);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
editor.update(cx, |editor, _| {
|
||||
assert!(editor.context_menu_visible());
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
let completion_labels = menu
|
||||
.completions
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|c| c.label.text.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
completion_labels,
|
||||
&["registered_command", "unregistered_command",],
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
command_calls.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"For completion with a registered command, Zed should send a command execution request",
|
||||
);
|
||||
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
cx.focus_self(window);
|
||||
editor.handle_input(".", window, cx);
|
||||
});
|
||||
cx.run_until_parked();
|
||||
editor.update(cx, |editor, _| {
|
||||
assert!(editor.context_menu_visible());
|
||||
if let Some(CodeContextMenu::Completions(menu)) = editor.context_menu.borrow_mut().as_ref()
|
||||
{
|
||||
let completion_labels = menu
|
||||
.completions
|
||||
.borrow()
|
||||
.iter()
|
||||
.map(|c| c.label.text.clone())
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(
|
||||
completion_labels,
|
||||
&["registered_command", "unregistered_command",],
|
||||
);
|
||||
} else {
|
||||
panic!("expected completion menu to be open");
|
||||
}
|
||||
});
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.context_menu_next(&Default::default(), window, cx);
|
||||
editor
|
||||
.confirm_completion(&ConfirmCompletion::default(), window, cx)
|
||||
.unwrap()
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
cx.run_until_parked();
|
||||
assert_eq!(
|
||||
command_calls.load(atomic::Ordering::Acquire),
|
||||
1,
|
||||
"For completion with an unregistered command, Zed should not send a command execution request",
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_completion_reuse(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -16804,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>(
|
||||
@@ -16814,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,
|
||||
);
|
||||
|
||||
@@ -18921,6 +19095,109 @@ async fn test_document_format_with_prettier(cx: &mut TestAppContext) {
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_document_format_with_prettier_explicit_language(cx: &mut TestAppContext) {
|
||||
init_test(cx, |settings| {
|
||||
settings.defaults.formatter = Some(FormatterList::Single(Formatter::Prettier))
|
||||
});
|
||||
|
||||
let fs = FakeFs::new(cx.executor());
|
||||
fs.insert_file(path!("/file.settings"), Default::default())
|
||||
.await;
|
||||
|
||||
let project = Project::test(fs, [path!("/file.settings").as_ref()], cx).await;
|
||||
let language_registry = project.read_with(cx, |project, _| project.languages().clone());
|
||||
|
||||
let ts_lang = Arc::new(Language::new(
|
||||
LanguageConfig {
|
||||
name: "TypeScript".into(),
|
||||
matcher: LanguageMatcher {
|
||||
path_suffixes: vec!["ts".to_string()],
|
||||
..LanguageMatcher::default()
|
||||
},
|
||||
prettier_parser_name: Some("typescript".to_string()),
|
||||
..LanguageConfig::default()
|
||||
},
|
||||
Some(tree_sitter_typescript::LANGUAGE_TYPESCRIPT.into()),
|
||||
));
|
||||
|
||||
language_registry.add(ts_lang.clone());
|
||||
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.prettier.get_or_insert_default().allowed = Some(true);
|
||||
});
|
||||
|
||||
let test_plugin = "test_plugin";
|
||||
let _ = language_registry.register_fake_lsp(
|
||||
"TypeScript",
|
||||
FakeLspAdapter {
|
||||
prettier_plugins: vec![test_plugin],
|
||||
..Default::default()
|
||||
},
|
||||
);
|
||||
|
||||
let prettier_format_suffix = project::TEST_PRETTIER_FORMAT_SUFFIX;
|
||||
let buffer = project
|
||||
.update(cx, |project, cx| {
|
||||
project.open_local_buffer(path!("/file.settings"), cx)
|
||||
})
|
||||
.await
|
||||
.unwrap();
|
||||
|
||||
project.update(cx, |project, cx| {
|
||||
project.set_language_for_buffer(&buffer, ts_lang, cx)
|
||||
});
|
||||
|
||||
let buffer_text = "one\ntwo\nthree\n";
|
||||
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
||||
let (editor, cx) = cx.add_window_view(|window, cx| build_editor(buffer, window, cx));
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
editor.set_text(buffer_text, window, cx)
|
||||
});
|
||||
|
||||
editor
|
||||
.update_in(cx, |editor, window, cx| {
|
||||
editor.perform_format(
|
||||
project.clone(),
|
||||
FormatTrigger::Manual,
|
||||
FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
.unwrap()
|
||||
.await;
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
buffer_text.to_string() + prettier_format_suffix + "\ntypescript",
|
||||
"Test prettier formatting was not applied to the original buffer text",
|
||||
);
|
||||
|
||||
update_test_language_settings(cx, |settings| {
|
||||
settings.defaults.formatter = Some(FormatterList::default())
|
||||
});
|
||||
let format = editor.update_in(cx, |editor, window, cx| {
|
||||
editor.perform_format(
|
||||
project.clone(),
|
||||
FormatTrigger::Manual,
|
||||
FormatTarget::Buffers(editor.buffer().read(cx).all_buffers()),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
});
|
||||
format.await.unwrap();
|
||||
|
||||
assert_eq!(
|
||||
editor.update(cx, |editor, cx| editor.text(cx)),
|
||||
buffer_text.to_string()
|
||||
+ prettier_format_suffix
|
||||
+ "\ntypescript\n"
|
||||
+ prettier_format_suffix
|
||||
+ "\ntypescript",
|
||||
"Autoformatting (via test prettier) was not applied to the original buffer text",
|
||||
);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_addition_reverts(cx: &mut TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
@@ -23799,7 +24076,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,
|
||||
);
|
||||
});
|
||||
@@ -23877,7 +24154,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,
|
||||
);
|
||||
});
|
||||
@@ -26415,7 +26692,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
}
|
||||
});
|
||||
|
||||
let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
|
||||
let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
|
||||
project.update(cx, |project, cx| {
|
||||
let buffer_id = editor
|
||||
.read(cx)
|
||||
@@ -26428,7 +26705,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
let buffer_result_id = project
|
||||
.lsp_store()
|
||||
.read(cx)
|
||||
.result_id(server_id, buffer_id, cx);
|
||||
.result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
|
||||
assert_eq!(expected, buffer_result_id);
|
||||
});
|
||||
};
|
||||
@@ -26445,7 +26722,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
.next()
|
||||
.await
|
||||
.expect("should have sent the first diagnostics pull request");
|
||||
ensure_result_id(Some("1".to_string()), cx);
|
||||
ensure_result_id(Some(SharedString::new("1")), cx);
|
||||
|
||||
// Editing should trigger diagnostics
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
@@ -26458,7 +26735,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
2,
|
||||
"Editing should trigger diagnostic request"
|
||||
);
|
||||
ensure_result_id(Some("2".to_string()), cx);
|
||||
ensure_result_id(Some(SharedString::new("2")), cx);
|
||||
|
||||
// Moving cursor should not trigger diagnostic request
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
@@ -26473,7 +26750,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
|
||||
2,
|
||||
"Cursor movement should not trigger diagnostic request"
|
||||
);
|
||||
ensure_result_id(Some("2".to_string()), cx);
|
||||
ensure_result_id(Some(SharedString::new("2")), cx);
|
||||
// Multiple rapid edits should be debounced
|
||||
for _ in 0..5 {
|
||||
editor.update_in(cx, |editor, window, cx| {
|
||||
@@ -26488,7 +26765,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(final_requests.to_string()), cx);
|
||||
ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
@@ -27125,7 +27402,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,
|
||||
);
|
||||
});
|
||||
@@ -27221,6 +27498,65 @@ async fn test_paste_url_from_other_app_creates_markdown_link_over_selected_text(
|
||||
));
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_markdown_list_indent_with_multi_cursor(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
cx.set_state(&indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
- [ˇ] Item 2
|
||||
- [ˇ] Item 2.a
|
||||
- [ˇ] Item 2.b
|
||||
"
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.handle_input("X", window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [ ] Item 1
|
||||
- [ ] Item 1.a
|
||||
- [Xˇ] Item 2
|
||||
- [Xˇ] Item 2.a
|
||||
- [Xˇ] Item 2.b
|
||||
"
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_markdown_list_indent_with_newline(cx: &mut gpui::TestAppContext) {
|
||||
init_test(cx, |_| {});
|
||||
|
||||
let markdown_language = languages::language("markdown", tree_sitter_md::LANGUAGE.into());
|
||||
let mut cx = EditorTestContext::new(cx).await;
|
||||
|
||||
cx.update_buffer(|buffer, cx| buffer.set_language(Some(markdown_language), cx));
|
||||
|
||||
cx.set_state(indoc! {"
|
||||
- [x] list item
|
||||
- [x] sub list itemˇ
|
||||
"
|
||||
});
|
||||
|
||||
cx.update_editor(|editor, window, cx| {
|
||||
editor.newline(&Newline, window, cx);
|
||||
});
|
||||
|
||||
cx.assert_editor_state(indoc! {"
|
||||
- [x] list item
|
||||
- [x] sub list item
|
||||
ˇ
|
||||
"
|
||||
});
|
||||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_paste_url_from_zed_copy_creates_markdown_link_over_selected_text(
|
||||
cx: &mut gpui::TestAppContext,
|
||||
|
||||
@@ -1227,7 +1227,13 @@ impl EditorElement {
|
||||
editor.hide_blame_popover(false, cx);
|
||||
}
|
||||
} else {
|
||||
editor.hide_blame_popover(false, cx);
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
let breakpoint_indicator = if gutter_hovered {
|
||||
@@ -2511,7 +2517,6 @@ 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> {
|
||||
@@ -2580,16 +2585,6 @@ 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 {
|
||||
@@ -2600,16 +2595,48 @@ impl EditorElement {
|
||||
})
|
||||
}
|
||||
|
||||
fn layout_blame_entry_popover(
|
||||
fn layout_blame_popover(
|
||||
&self,
|
||||
blame_entry: BlameEntry,
|
||||
blame: Entity<GitBlame>,
|
||||
line_height: Pixels,
|
||||
editor_snapshot: &EditorSnapshot,
|
||||
text_hitbox: &Hitbox,
|
||||
buffer: BufferId,
|
||||
line_height: Pixels,
|
||||
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
|
||||
@@ -2631,7 +2658,7 @@ impl EditorElement {
|
||||
popover_state.markdown,
|
||||
workspace,
|
||||
&blame,
|
||||
buffer,
|
||||
buffer_id,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
@@ -3888,6 +3915,8 @@ impl EditorElement {
|
||||
) -> impl IntoElement {
|
||||
let editor = self.editor.read(cx);
|
||||
let multi_buffer = editor.buffer.read(cx);
|
||||
let is_read_only = self.editor.read(cx).read_only(cx);
|
||||
|
||||
let file_status = multi_buffer
|
||||
.all_diff_hunks_expanded()
|
||||
.then(|| editor.status_for_buffer_id(for_excerpt.buffer_id, cx))
|
||||
@@ -3940,7 +3969,7 @@ impl EditorElement {
|
||||
.gap_1p5()
|
||||
.when(is_sticky, |el| el.shadow_md())
|
||||
.border_1()
|
||||
.map(|div| {
|
||||
.map(|border| {
|
||||
let border_color = if is_selected
|
||||
&& is_folded
|
||||
&& focus_handle.contains_focused(window, cx)
|
||||
@@ -3949,7 +3978,7 @@ impl EditorElement {
|
||||
} else {
|
||||
colors.border
|
||||
};
|
||||
div.border_color(border_color)
|
||||
border.border_color(border_color)
|
||||
})
|
||||
.bg(colors.editor_subheader_background)
|
||||
.hover(|style| style.bg(colors.element_hover))
|
||||
@@ -4029,13 +4058,15 @@ impl EditorElement {
|
||||
})
|
||||
.take(1),
|
||||
)
|
||||
.child(
|
||||
h_flex()
|
||||
.size_3()
|
||||
.justify_center()
|
||||
.flex_shrink_0()
|
||||
.children(indicator),
|
||||
)
|
||||
.when(!is_read_only, |this| {
|
||||
this.child(
|
||||
h_flex()
|
||||
.size_3()
|
||||
.justify_center()
|
||||
.flex_shrink_0()
|
||||
.children(indicator),
|
||||
)
|
||||
})
|
||||
.child(
|
||||
h_flex()
|
||||
.cursor_pointer()
|
||||
@@ -5572,6 +5603,50 @@ impl EditorElement {
|
||||
}
|
||||
}
|
||||
|
||||
fn layout_word_diff_highlights(
|
||||
display_hunks: &[(DisplayDiffHunk, Option<Hitbox>)],
|
||||
row_infos: &[RowInfo],
|
||||
start_row: DisplayRow,
|
||||
snapshot: &EditorSnapshot,
|
||||
highlighted_ranges: &mut Vec<(Range<DisplayPoint>, Hsla)>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let colors = cx.theme().colors();
|
||||
|
||||
let word_highlights = display_hunks
|
||||
.into_iter()
|
||||
.filter_map(|(hunk, _)| match hunk {
|
||||
DisplayDiffHunk::Unfolded {
|
||||
word_diffs, status, ..
|
||||
} => Some((word_diffs, status)),
|
||||
_ => None,
|
||||
})
|
||||
.filter(|(_, status)| status.is_modified())
|
||||
.flat_map(|(word_diffs, _)| word_diffs)
|
||||
.filter_map(|word_diff| {
|
||||
let start_point = word_diff.start.to_display_point(&snapshot.display_snapshot);
|
||||
let end_point = word_diff.end.to_display_point(&snapshot.display_snapshot);
|
||||
let start_row_offset = start_point.row().0.saturating_sub(start_row.0) as usize;
|
||||
|
||||
row_infos
|
||||
.get(start_row_offset)
|
||||
.and_then(|row_info| row_info.diff_status)
|
||||
.and_then(|diff_status| {
|
||||
let background_color = match diff_status.kind {
|
||||
DiffHunkStatusKind::Added => colors.version_control_word_added,
|
||||
DiffHunkStatusKind::Deleted => colors.version_control_word_deleted,
|
||||
DiffHunkStatusKind::Modified => {
|
||||
debug_panic!("modified diff status for row info");
|
||||
return None;
|
||||
}
|
||||
};
|
||||
Some((start_point..end_point, background_color))
|
||||
})
|
||||
});
|
||||
|
||||
highlighted_ranges.extend(word_highlights);
|
||||
}
|
||||
|
||||
fn layout_diff_hunk_controls(
|
||||
&self,
|
||||
row_range: Range<DisplayRow>,
|
||||
@@ -9122,7 +9197,7 @@ impl Element for EditorElement {
|
||||
);
|
||||
let end_row = DisplayRow(end_row);
|
||||
|
||||
let row_infos = snapshot
|
||||
let row_infos = snapshot // note we only get the visual range
|
||||
.row_infos(start_row)
|
||||
.take((start_row..end_row).len())
|
||||
.collect::<Vec<RowInfo>>();
|
||||
@@ -9153,16 +9228,27 @@ impl Element for EditorElement {
|
||||
|
||||
let is_light = cx.theme().appearance().is_light();
|
||||
|
||||
let mut highlighted_ranges = self
|
||||
.editor_with_selections(cx)
|
||||
.map(|editor| {
|
||||
editor.read(cx).background_highlights_in_range(
|
||||
start_anchor..end_anchor,
|
||||
&snapshot.display_snapshot,
|
||||
cx.theme(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
|
||||
for (ix, row_info) in row_infos.iter().enumerate() {
|
||||
let Some(diff_status) = row_info.diff_status else {
|
||||
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::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;
|
||||
@@ -9200,21 +9286,14 @@ impl Element for EditorElement {
|
||||
filled_highlight
|
||||
};
|
||||
|
||||
let base_display_point =
|
||||
DisplayPoint::new(start_row + DisplayRow(ix as u32), 0);
|
||||
|
||||
highlighted_rows
|
||||
.entry(start_row + DisplayRow(ix as u32))
|
||||
.entry(base_display_point.row())
|
||||
.or_insert(background);
|
||||
}
|
||||
|
||||
let highlighted_ranges = self
|
||||
.editor_with_selections(cx)
|
||||
.map(|editor| {
|
||||
editor.read(cx).background_highlights_in_range(
|
||||
start_anchor..end_anchor,
|
||||
&snapshot.display_snapshot,
|
||||
cx.theme(),
|
||||
)
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let highlighted_gutter_ranges =
|
||||
self.editor.read(cx).gutter_highlights_in_range(
|
||||
start_anchor..end_anchor,
|
||||
@@ -9387,7 +9466,7 @@ impl Element for EditorElement {
|
||||
let crease_trailers =
|
||||
window.with_element_namespace("crease_trailers", |window| {
|
||||
self.layout_crease_trailers(
|
||||
row_infos.iter().copied(),
|
||||
row_infos.iter().cloned(),
|
||||
&snapshot,
|
||||
window,
|
||||
cx,
|
||||
@@ -9403,6 +9482,15 @@ impl Element for EditorElement {
|
||||
cx,
|
||||
);
|
||||
|
||||
Self::layout_word_diff_highlights(
|
||||
&display_hunks,
|
||||
&row_infos,
|
||||
start_row,
|
||||
&snapshot,
|
||||
&mut highlighted_ranges,
|
||||
cx,
|
||||
);
|
||||
|
||||
let merged_highlighted_ranges =
|
||||
if let Some((_, colors)) = document_colors.as_ref() {
|
||||
&highlighted_ranges
|
||||
@@ -9756,7 +9844,6 @@ impl Element for EditorElement {
|
||||
scroll_position,
|
||||
scroll_pixel_position,
|
||||
line_height,
|
||||
&text_hitbox,
|
||||
window,
|
||||
cx,
|
||||
) {
|
||||
@@ -9954,6 +10041,8 @@ 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(
|
||||
|
||||
@@ -508,7 +508,19 @@ impl GitBlame {
|
||||
let buffer_edits = buffer.update(cx, |buffer, _| buffer.subscribe());
|
||||
|
||||
let blame_buffer = project.blame_buffer(&buffer, None, cx);
|
||||
Some(async move { (id, snapshot, buffer_edits, blame_buffer.await) })
|
||||
let remote_url = project
|
||||
.git_store()
|
||||
.read(cx)
|
||||
.repository_and_path_for_buffer_id(buffer.read(cx).remote_id(), cx)
|
||||
.and_then(|(repo, _)| {
|
||||
repo.read(cx)
|
||||
.remote_upstream_url
|
||||
.clone()
|
||||
.or(repo.read(cx).remote_origin_url.clone())
|
||||
});
|
||||
Some(
|
||||
async move { (id, snapshot, buffer_edits, blame_buffer.await, remote_url) },
|
||||
)
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
});
|
||||
@@ -524,13 +536,9 @@ impl GitBlame {
|
||||
.await;
|
||||
let mut res = vec![];
|
||||
let mut errors = vec![];
|
||||
for (id, snapshot, buffer_edits, blame) in blame {
|
||||
for (id, snapshot, buffer_edits, blame, remote_url) in blame {
|
||||
match blame {
|
||||
Ok(Some(Blame {
|
||||
entries,
|
||||
messages,
|
||||
remote_url,
|
||||
})) => {
|
||||
Ok(Some(Blame { entries, messages })) => {
|
||||
let entries = build_blame_entry_sum_tree(
|
||||
entries,
|
||||
snapshot.max_point().row,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -181,6 +181,10 @@ pub fn indent_guides_in_range(
|
||||
.buffer_snapshot()
|
||||
.indent_guides_in_range(start_anchor..end_anchor, ignore_disabled_for_language, cx)
|
||||
.filter(|indent_guide| {
|
||||
if editor.has_indent_guides_disabled_for_buffer(indent_guide.buffer_id) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if editor.is_buffer_folded(indent_guide.buffer_id, cx) {
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -291,7 +291,7 @@ impl Editor {
|
||||
}),
|
||||
};
|
||||
|
||||
let mut visible_excerpts = self.visible_excerpts(cx);
|
||||
let mut visible_excerpts = self.visible_excerpts(true, cx);
|
||||
let mut invalidate_hints_for_buffers = HashSet::default();
|
||||
let ignore_previous_fetches = match reason {
|
||||
InlayHintRefreshReason::ModifiersChanged(_)
|
||||
@@ -2211,7 +2211,7 @@ pub mod tests {
|
||||
cx: &mut gpui::TestAppContext,
|
||||
) -> Range<Point> {
|
||||
let ranges = editor
|
||||
.update(cx, |editor, _window, cx| editor.visible_excerpts(cx))
|
||||
.update(cx, |editor, _window, cx| editor.visible_excerpts(true, cx))
|
||||
.unwrap();
|
||||
assert_eq!(
|
||||
ranges.len(),
|
||||
|
||||
@@ -1487,6 +1487,7 @@ impl SearchableItem for Editor {
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
matches: &[Range<Anchor>],
|
||||
active_match_index: Option<usize>,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) {
|
||||
@@ -1497,7 +1498,13 @@ impl SearchableItem for Editor {
|
||||
let updated = existing_range != Some(matches);
|
||||
self.highlight_background::<BufferSearchHighlights>(
|
||||
matches,
|
||||
|theme| theme.colors().search_match_background,
|
||||
move |index, theme| {
|
||||
if active_match_index == Some(*index) {
|
||||
theme.colors().search_active_match_background
|
||||
} else {
|
||||
theme.colors().search_match_background
|
||||
}
|
||||
},
|
||||
cx,
|
||||
);
|
||||
if updated {
|
||||
@@ -1891,15 +1898,20 @@ 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.as_ref(), height, include_filename, cx)
|
||||
path_for_file(file, height, include_filename, cx)
|
||||
}
|
||||
|
||||
fn path_for_file<'a>(
|
||||
file: &'a dyn language::File,
|
||||
file: &'a Arc<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;
|
||||
|
||||
@@ -1946,11 +1958,11 @@ mod tests {
|
||||
|
||||
#[gpui::test]
|
||||
fn test_path_for_file(cx: &mut App) {
|
||||
let file = TestFile {
|
||||
let file: Arc<dyn language::File> = Arc::new(TestFile {
|
||||
path: RelPath::empty().into(),
|
||||
root_name: String::new(),
|
||||
local_root: None,
|
||||
};
|
||||
});
|
||||
assert_eq!(path_for_file(&file, 0, false, cx), None);
|
||||
}
|
||||
|
||||
|
||||
@@ -164,7 +164,7 @@ impl Editor {
|
||||
}
|
||||
|
||||
let visible_buffers = self
|
||||
.visible_excerpts(cx)
|
||||
.visible_excerpts(true, cx)
|
||||
.into_values()
|
||||
.map(|(buffer, ..)| buffer)
|
||||
.filter(|editor_buffer| {
|
||||
|
||||
@@ -243,20 +243,25 @@ impl Render for SplittableEditor {
|
||||
window: &mut ui::Window,
|
||||
cx: &mut ui::Context<Self>,
|
||||
) -> impl ui::IntoElement {
|
||||
let Some(active) = self.panes.panes().into_iter().next() else {
|
||||
return div().into_any_element();
|
||||
let inner = if self.secondary.is_none() {
|
||||
self.primary_editor.clone().into_any_element()
|
||||
} else if let Some(active) = self.panes.panes().into_iter().next() {
|
||||
self.panes
|
||||
.render(
|
||||
None,
|
||||
&ActivePaneDecorator::new(active, &self.workspace),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
.into_any_element()
|
||||
} else {
|
||||
div().into_any_element()
|
||||
};
|
||||
div()
|
||||
.id("splittable-editor")
|
||||
.on_action(cx.listener(Self::split))
|
||||
.on_action(cx.listener(Self::unsplit))
|
||||
.size_full()
|
||||
.child(self.panes.render(
|
||||
None,
|
||||
&ActivePaneDecorator::new(active, &self.workspace),
|
||||
window,
|
||||
cx,
|
||||
))
|
||||
.into_any_element()
|
||||
.child(inner)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,7 +261,7 @@ impl ExampleContext {
|
||||
.expect("Unknown tool_name content in meta");
|
||||
|
||||
tool_uses_by_id.insert(
|
||||
tool_call.id,
|
||||
tool_call.tool_call_id,
|
||||
ToolUse {
|
||||
name: tool_name.to_string(),
|
||||
value: tool_call.raw_input.unwrap_or_default(),
|
||||
@@ -277,7 +277,9 @@ 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.id) {
|
||||
if let Some(tool_use) =
|
||||
tool_uses_by_id.get_mut(&update.tool_call_id)
|
||||
{
|
||||
tool_use.value = raw_input;
|
||||
}
|
||||
}
|
||||
@@ -290,7 +292,7 @@ impl ExampleContext {
|
||||
update.fields.status == Some(acp::ToolCallStatus::Completed);
|
||||
|
||||
let tool_use = tool_uses_by_id
|
||||
.remove(&update.id)
|
||||
.remove(&update.tool_call_id)
|
||||
.expect("Unrecognized tool call completed");
|
||||
|
||||
let log_message = if succeeded {
|
||||
@@ -337,10 +339,7 @@ impl ExampleContext {
|
||||
acp::StopReason::MaxTurnRequests => {
|
||||
return Err(anyhow!("Exceeded maximum turn requests"));
|
||||
}
|
||||
acp::StopReason::Refusal => {
|
||||
return Err(anyhow!("Refusal"));
|
||||
}
|
||||
acp::StopReason::Cancelled => return Err(anyhow!("Cancelled")),
|
||||
stop_reason => return Err(anyhow!("{stop_reason:?}")),
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -303,13 +303,12 @@ 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(
|
||||
let session_id = acp::SessionId::new(
|
||||
rand::rng()
|
||||
.sample_iter(&distr::Alphanumeric)
|
||||
.take(7)
|
||||
.map(char::from)
|
||||
.collect::<String>()
|
||||
.into(),
|
||||
.collect::<String>(),
|
||||
);
|
||||
|
||||
let db_thread = agent::DbThread::from_json(json.as_bytes()).expect("Can't read serialized thread");
|
||||
@@ -640,7 +639,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(uuid::Uuid::new_v4().to_string().into());
|
||||
let id = acp::TerminalId::new(uuid::Uuid::new_v4().to_string());
|
||||
let terminal =
|
||||
acp_thread::create_terminal_entity(command, &[], vec![], cwd.clone(), &project, cx)
|
||||
.await?;
|
||||
|
||||
18
crates/eval_utils/Cargo.toml
Normal file
@@ -0,0 +1,18 @@
|
||||
[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
crates/eval_utils/LICENSE-GPL
Symbolic link
@@ -0,0 +1 @@
|
||||
LICENSE-GPL
|
||||
3
crates/eval_utils/README.md
Normal file
@@ -0,0 +1,3 @@
|
||||
# eval_utils
|
||||
|
||||
Utilities for evals of agents.
|
||||
128
crates/eval_utils/src/eval_utils.rs
Normal file
@@ -0,0 +1,128 @@
|
||||
//! 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,12 +1,13 @@
|
||||
[package]
|
||||
name = "zed_extension_api"
|
||||
version = "0.7.0"
|
||||
version = "0.8.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
|
||||
publish = true
|
||||
# Change back to `true` when we're ready to publish v0.8.0.
|
||||
publish = false
|
||||
license = "Apache-2.0"
|
||||
|
||||
[lints]
|
||||
|
||||
@@ -6,6 +6,7 @@ pub mod settings;
|
||||
|
||||
use core::fmt;
|
||||
|
||||
use crate::settings::LspSettings;
|
||||
use wit::*;
|
||||
|
||||
pub use serde_json;
|
||||
@@ -85,19 +86,21 @@ pub trait Extension: Send + Sync {
|
||||
/// Returns the initialization options to pass to the specified language server.
|
||||
fn language_server_initialization_options(
|
||||
&mut self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_worktree: &Worktree,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(None)
|
||||
LspSettings::for_worktree(language_server_id.as_ref(), worktree)
|
||||
.map(|settings| settings.initialization_options)
|
||||
}
|
||||
|
||||
/// Returns the workspace configuration options to pass to the language server.
|
||||
fn language_server_workspace_configuration(
|
||||
&mut self,
|
||||
_language_server_id: &LanguageServerId,
|
||||
_worktree: &Worktree,
|
||||
language_server_id: &LanguageServerId,
|
||||
worktree: &Worktree,
|
||||
) -> Result<Option<serde_json::Value>> {
|
||||
Ok(None)
|
||||
LspSettings::for_worktree(language_server_id.as_ref(), worktree)
|
||||
.map(|settings| settings.settings)
|
||||
}
|
||||
|
||||
/// Returns the initialization options to pass to the other language server.
|
||||
@@ -334,7 +337,7 @@ mod wit {
|
||||
|
||||
wit_bindgen::generate!({
|
||||
skip: ["init-extension"],
|
||||
path: "./wit/since_v0.6.0",
|
||||
path: "./wit/since_v0.8.0",
|
||||
});
|
||||
}
|
||||
|
||||
|
||||