Compare commits

..

1 Commits

Author SHA1 Message Date
Oleksiy Syvokon
53f6de825b Add zeta score command 2025-12-01 15:50:52 +02:00
314 changed files with 7034 additions and 15152 deletions

View File

@@ -31,7 +31,6 @@ body:
validations:
required: true
- type: textarea
id: environment
attributes:
label: Zed version and system specs
description: |

View File

@@ -6,18 +6,28 @@ body:
- type: textarea
attributes:
label: Reproduction steps
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.
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**.
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 youre 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”.
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**.
placeholder: |
Zed: v0.215.0 (Zed Nightly bfe141ea79aa4984028934067ba75c48d99136ae)
OS: macOS 15.1
@@ -27,7 +37,7 @@ body:
required: true
- type: textarea
attributes:
label: Attach Zed log file
label: If applicable, attach your Zed log file to this issue
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: |

19
.github/ISSUE_TEMPLATE/99_other.yml vendored Normal file
View File

@@ -0,0 +1,19 @@
name: Other [Staff Only]
description: Zed Staff Only
body:
- type: textarea
attributes:
label: Summary
value: |
<!-- Please insert a one line summary of the issue below -->
SUMMARY_SENTENCE_HERE
### Description
IF YOU DO NOT WORK FOR ZED INDUSTRIES DO NOT CREATE ISSUES WITH THIS TEMPLATE.
THEY WILL BE AUTO-CLOSED AND MAY RESULT IN YOU BEING BANNED FROM THE ZED ISSUE TRACKER.
FEATURE REQUESTS / SUPPORT REQUESTS SHOULD BE OPENED AS DISCUSSIONS:
https://github.com/zed-industries/zed/discussions/new/choose
validations:
required: true

View File

@@ -23,13 +23,11 @@ jobs:
AidanV
akbxr
AlvaroParker
amtoaer
artemevsevev
bajrangCoder
bcomnes
Be-ing
blopker
bnjjj
bobbymannino
CharlesChen0823
chbk
@@ -37,10 +35,8 @@ jobs:
davewa
ddoemonn
djsauble
errmayank
fantacell
findrakecil
FloppyDisco
gko
huacnlee
imumesh18
@@ -55,7 +51,6 @@ jobs:
marius851000
mikebronner
ognevny
playdohface
RemcoSmitsDev
romaninsh
Simek
@@ -63,14 +58,12 @@ jobs:
sourcefrog
suxiaoshao
Takk8IS
thedadams
tidely
timvermeulen
valentinegb
versecafe
vitallium
warrenjokinen
WhySoBad
ya7010
Zertsov
with:

View File

@@ -13,10 +13,6 @@ on:
description: bump-type
type: string
default: patch
force-bump:
description: force-bump
required: true
type: boolean
secrets:
app-id:
description: The app ID used to create the PR
@@ -25,6 +21,33 @@ on:
description: The app secret for the corresponding app ID
required: true
jobs:
check_extension:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
- id: cache-zed-extension-cli
name: extension_tests::cache_zed_extension_cli
uses: actions/cache@0057852bfaa89a56745cba8c7296529d2fc39830
with:
path: zed-extension
key: zed-extension-${{ env.ZED_EXTENSION_CLI_SHA }}
- name: extension_tests::download_zed_extension_cli
if: steps.cache-zed-extension-cli.outputs.cache-hit != 'true'
run: |
wget --quiet "https://zed-extension-cli.nyc3.digitaloceanspaces.com/$ZED_EXTENSION_CLI_SHA/x86_64-unknown-linux-gnu/zed-extension"
chmod +x zed-extension
shell: bash -euxo pipefail {0}
- name: extension_tests::check
run: |
mkdir -p /tmp/ext-scratch
mkdir -p /tmp/ext-output
./zed-extension --source-dir . --scratch-dir /tmp/ext-scratch --output-dir /tmp/ext-output
shell: bash -euxo pipefail {0}
timeout-minutes: 1
check_bump_needed:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-2x4-ubuntu-2404
@@ -33,24 +56,17 @@ jobs:
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
with:
clean: false
fetch-depth: 0
fetch-depth: 10
- id: compare-versions-check
name: extension_bump::compare_versions
run: |
CURRENT_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
PR_PARENT_SHA="${{ github.event.pull_request.head.sha }}"
if [[ -n "$PR_PARENT_SHA" ]]; then
git checkout "$PR_PARENT_SHA"
elif BRANCH_PARENT_SHA="$(git merge-base origin/main origin/zed-zippy-autobump)"; then
git checkout "$BRANCH_PARENT_SHA"
else
git checkout "$(git log -1 --format=%H)"~1
fi
git checkout "$(git log -1 --format=%H)"~1
PARENT_COMMIT_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
PREV_COMMIT_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
[[ "$CURRENT_VERSION" == "$PARENT_COMMIT_VERSION" ]] && \
[[ "$CURRENT_VERSION" == "$PREV_COMMIT_VERSION" ]] && \
echo "needs_bump=true" >> "$GITHUB_OUTPUT" || \
echo "needs_bump=false" >> "$GITHUB_OUTPUT"
@@ -62,10 +78,9 @@ jobs:
timeout-minutes: 1
bump_extension_version:
needs:
- check_extension
- check_bump_needed
if: |-
(github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') &&
(inputs.force-bump == 'true' || needs.check_bump_needed.outputs.needs_bump == 'true')
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && needs.check_bump_needed.outputs.needs_bump == 'true'
runs-on: namespace-profile-8x16-ubuntu-2204
steps:
- id: generate-token
@@ -84,21 +99,25 @@ jobs:
- id: bump-version
name: extension_bump::bump_version
run: |
OLD_VERSION="${{ needs.check_bump_needed.outputs.current_version }}"
OLD_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
BUMP_FILES=("extension.toml")
if [[ -f "Cargo.toml" ]]; then
BUMP_FILES+=("Cargo.toml")
fi
cat <<EOF > .bumpversion.cfg
[bumpversion]
current_version = "$OLD_VERSION"
bump2version --verbose --current-version "$OLD_VERSION" --no-configured-files ${{ inputs.bump-type }} "${BUMP_FILES[@]}"
[bumpversion:file:Cargo.toml]
if [[ -f "Cargo.toml" ]]; then
cargo update --workspace
fi
[bumpversion:file:extension.toml]
EOF
bump2version --verbose ${{ inputs.bump-type }}
NEW_VERSION="$(sed -n 's/version = \"\(.*\)\"/\1/p' < extension.toml)"
cargo update --workspace
rm .bumpversion.cfg
echo "old_version=${OLD_VERSION}" >> "$GITHUB_OUTPUT"
echo "new_version=${NEW_VERSION}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: extension_bump::create_pull_request
@@ -107,7 +126,7 @@ jobs:
title: Bump version to ${{ steps.bump-version.outputs.new_version }}
body: This PR bumps the version of this extension to v${{ steps.bump-version.outputs.new_version }}
commit-message: Bump version to v${{ steps.bump-version.outputs.new_version }}
branch: zed-zippy-autobump
branch: bump-from-${{ steps.bump-version.outputs.old_version }}
committer: zed-zippy[bot] <234243425+zed-zippy[bot]@users.noreply.github.com>
base: main
delete-branch: true
@@ -116,8 +135,9 @@ 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'
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions') && needs.check_bump_needed.outputs.needs_bump == 'false'
runs-on: namespace-profile-8x16-ubuntu-2204
steps:
- id: generate-token

View File

@@ -1,43 +0,0 @@
# Generated from xtask::workflows::extension_release
# Rebuild with `cargo xtask workflows`.
name: extension_release
on:
workflow_call:
secrets:
app-id:
description: The app ID used to create the PR
required: true
app-secret:
description: The app secret for the corresponding app ID
required: true
jobs:
create_release:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
runs-on: namespace-profile-8x16-ubuntu-2204
steps:
- id: generate-token
name: extension_bump::generate_token
uses: actions/create-github-app-token@v2
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:
clean: false
- id: get-extension-id
name: extension_release::get_extension_id
run: |
EXTENSION_ID="$(sed -n 's/id = \"\(.*\)\"/\1/p' < extension.toml)"
echo "extension_id=${EXTENSION_ID}" >> "$GITHUB_OUTPUT"
shell: bash -euxo pipefail {0}
- name: extension_release::release_action
uses: huacnlee/zed-extension-action@v2
with:
extension-name: ${{ steps.get-extension-id.outputs.extension_id }}
push-to: zed-industries/extensions
env:
COMMITTER_TOKEN: ${{ steps.generate-token.outputs.token }}

View File

@@ -7,7 +7,12 @@ env:
CARGO_INCREMENTAL: '0'
ZED_EXTENSION_CLI_SHA: 7cfce605704d41ca247e3f84804bf323f6c6caaf
on:
workflow_call: {}
workflow_call:
inputs:
run_tests:
description: Whether the workflow should run rust tests
required: true
type: boolean
jobs:
orchestrate:
if: (github.repository_owner == 'zed-industries' || github.repository_owner == 'zed-extensions')
@@ -68,12 +73,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:
@@ -103,7 +108,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: 2
timeout-minutes: 1
tests_pass:
needs:
- orchestrate

View File

@@ -13,7 +13,7 @@ jobs:
bundle_linux_aarch64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: namespace-profile-8x32-ubuntu-2004-arm-m4
env:
CARGO_INCREMENTAL: 0
@@ -56,7 +56,7 @@ jobs:
bundle_linux_x86_64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: namespace-profile-32x64-ubuntu-2004
env:
CARGO_INCREMENTAL: 0
@@ -99,7 +99,7 @@ jobs:
bundle_mac_aarch64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
@@ -145,7 +145,7 @@ jobs:
bundle_mac_x86_64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-mini-macos
env:
CARGO_INCREMENTAL: 0
@@ -191,7 +191,7 @@ jobs:
bundle_windows_aarch64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-32vcpu-windows-2022
env:
CARGO_INCREMENTAL: 0
@@ -229,7 +229,7 @@ jobs:
bundle_windows_x86_64:
if: |-
(github.event.action == 'labeled' && github.event.label.name == 'run-bundling') ||
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
(github.event.action == 'synchronize' && contains(github.event.pull_request.labels.*.name, 'run-bundling'))
runs-on: self-32vcpu-windows-2022
env:
CARGO_INCREMENTAL: 0

View File

@@ -13,14 +13,6 @@ on:
jobs:
cron_unit_evals:
runs-on: namespace-profile-16x32-ubuntu-2204
strategy:
matrix:
model:
- anthropic/claude-sonnet-4-5-latest
- anthropic/claude-opus-4-5-latest
- google/gemini-3-pro
- openai/gpt-5
fail-fast: false
steps:
- name: steps::checkout_repo
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683
@@ -57,7 +49,6 @@ jobs:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GOOGLE_AI_API_KEY: ${{ secrets.GOOGLE_AI_API_KEY }}
GOOGLE_CLOUD_PROJECT: ${{ secrets.GOOGLE_CLOUD_PROJECT }}
ZED_AGENT_MODEL: ${{ matrix.model }}
- name: steps::cleanup_cargo_config
if: always()
run: |

View File

@@ -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@2d0ce569feab1f8752f1dde43cc2f2aa53236e06
uses: crate-ci/typos@80c8a4945eec0f6d464eaf9e65ed98ef085283d1
with:
config: ./typos.toml
- name: steps::cargo_fmt
@@ -520,7 +520,6 @@ 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:

144
Cargo.lock generated
View File

@@ -159,7 +159,6 @@ dependencies = [
"derive_more 0.99.20",
"editor",
"env_logger 0.11.8",
"eval_utils",
"fs",
"futures 0.3.31",
"git",
@@ -216,9 +215,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.8.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3e639d6b544ad39f5b4e05802db5eb04e1518284eb05fda1839931003e0244c8"
checksum = "525705e39c11cd73f7bc784e3681a9386aa30c8d0630808d3dc2237eb4f9cb1b"
dependencies = [
"agent-client-protocol-schema",
"anyhow",
@@ -227,15 +226,16 @@ dependencies = [
"derive_more 2.0.1",
"futures 0.3.31",
"log",
"parking_lot",
"serde",
"serde_json",
]
[[package]]
name = "agent-client-protocol-schema"
version = "0.9.1"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f182f5e14bef8232b239719bd99166bb11e986c08fc211f28e392f880d3093ba"
checksum = "ecf16c18fea41282d6bbadd1549a06be6836bddb1893f44a6235f340fa24e2af"
dependencies = [
"anyhow",
"derive_more 2.0.1",
@@ -328,7 +328,6 @@ dependencies = [
"buffer_diff",
"chrono",
"client",
"clock",
"cloud_llm_client",
"collections",
"command_palette_hooks",
@@ -336,7 +335,6 @@ dependencies = [
"context_server",
"db",
"editor",
"eval_utils",
"extension",
"extension_host",
"feature_flags",
@@ -345,7 +343,6 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"gpui",
"gpui_tokio",
"html_to_markdown",
"http_client",
"image",
@@ -373,7 +370,6 @@ dependencies = [
"proto",
"rand 0.9.2",
"release_channel",
"reqwest_client",
"rope",
"rules_library",
"schemars",
@@ -2130,15 +2126,30 @@ dependencies = [
"syn 2.0.106",
]
[[package]]
name = "bit-set"
version = "0.5.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0700ddab506f33b20a03b13996eccd309a48e5ff77d0d95926aa0210fb4e95f1"
dependencies = [
"bit-vec 0.6.3",
]
[[package]]
name = "bit-set"
version = "0.8.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "08807e080ed7f9d5433fa9b275196cfc35414f66a0c79d864dc51a0d825231a3"
dependencies = [
"bit-vec",
"bit-vec 0.8.0",
]
[[package]]
name = "bit-vec"
version = "0.6.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb"
[[package]]
name = "bit-vec"
version = "0.8.0"
@@ -2317,9 +2328,9 @@ dependencies = [
[[package]]
name = "borrow-or-share"
version = "0.2.4"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dc0b364ead1874514c8c2855ab558056ebfeb775653e7ae45ff72f28f8f3166c"
checksum = "3eeab4423108c5d7c744f4d234de88d18d636100093ae04caf4825134b9c3a32"
[[package]]
name = "borsh"
@@ -2412,7 +2423,6 @@ dependencies = [
"rand 0.9.2",
"rope",
"serde_json",
"settings",
"sum_tree",
"text",
"unindent",
@@ -4173,7 +4183,6 @@ dependencies = [
"serde_json",
"smol",
"system_specs",
"windows 0.61.3",
"zstd 0.11.2+zstd.1.5.2",
]
@@ -5290,7 +5299,6 @@ dependencies = [
"indoc",
"language",
"lsp",
"menu",
"paths",
"project",
"regex",
@@ -5300,7 +5308,6 @@ dependencies = [
"telemetry",
"theme",
"ui",
"ui_input",
"util",
"workspace",
"zed_actions",
@@ -5360,7 +5367,6 @@ dependencies = [
"db",
"edit_prediction",
"emojis",
"feature_flags",
"file_icons",
"fs",
"futures 0.3.31",
@@ -5765,15 +5771,6 @@ dependencies = [
"watch",
]
[[package]]
name = "eval_utils"
version = "0.1.0"
dependencies = [
"gpui",
"serde",
"smol",
]
[[package]]
name = "event-listener"
version = "2.5.3"
@@ -5993,11 +5990,22 @@ checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
[[package]]
name = "fancy-regex"
version = "0.16.2"
version = "0.13.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "998b056554fbe42e03ae0e152895cd1a7e1002aec800fdc6635d20270260c46f"
checksum = "531e46835a22af56d1e3b66f04844bed63158bc094a628bec1d321d9b4c44bf2"
dependencies = [
"bit-set",
"bit-set 0.5.3",
"regex-automata",
"regex-syntax",
]
[[package]]
name = "fancy-regex"
version = "0.14.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6e24cb5a94bcae1e5408b0effca5cd7172ea3c5755049c5f3af4cd283a165298"
dependencies = [
"bit-set 0.8.0",
"regex-automata",
"regex-syntax",
]
@@ -6219,9 +6227,9 @@ checksum = "8bf7cc16383c4b8d58b9905a8509f02926ce3058053c056376248d958c9df1e8"
[[package]]
name = "fluent-uri"
version = "0.4.1"
version = "0.3.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bc74ac4d8359ae70623506d512209619e5cf8f347124910440dbc221714b328e"
checksum = "1918b65d96df47d3591bed19c5cca17e3fa5d0707318e4b5ef2eae01764df7e5"
dependencies = [
"borrow-or-share",
"ref-cast",
@@ -6960,7 +6968,7 @@ dependencies = [
[[package]]
name = "gh-workflow"
version = "0.8.0"
source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c"
source = "git+https://github.com/zed-industries/gh-workflow?rev=3eaa84abca0778eb54272f45a312cb24f9a0b435#3eaa84abca0778eb54272f45a312cb24f9a0b435"
dependencies = [
"async-trait",
"derive_more 2.0.1",
@@ -6977,7 +6985,7 @@ dependencies = [
[[package]]
name = "gh-workflow-macros"
version = "0.8.0"
source = "git+https://github.com/zed-industries/gh-workflow?rev=09acfdf2bd5c1d6254abefd609c808ff73547b2c#09acfdf2bd5c1d6254abefd609c808ff73547b2c"
source = "git+https://github.com/zed-industries/gh-workflow?rev=3eaa84abca0778eb54272f45a312cb24f9a0b435#3eaa84abca0778eb54272f45a312cb24f9a0b435"
dependencies = [
"heck 0.5.0",
"quote",
@@ -7100,7 +7108,6 @@ dependencies = [
"futures 0.3.31",
"fuzzy",
"git",
"git_hosting_providers",
"gpui",
"indoc",
"itertools 0.14.0",
@@ -7517,17 +7524,6 @@ dependencies = [
"serde",
]
[[package]]
name = "hashbrown"
version = "0.16.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100"
dependencies = [
"allocator-api2",
"equivalent",
"foldhash 0.2.0",
]
[[package]]
name = "hashlink"
version = "0.8.4"
@@ -8617,21 +8613,21 @@ dependencies = [
[[package]]
name = "jsonschema"
version = "0.37.4"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "73c9ffb2b5c56d58030e1b532d8e8389da94590515f118cf35b5cb68e4764a7e"
checksum = "f1b46a0365a611fbf1d2143104dcf910aada96fafd295bab16c60b802bf6fa1d"
dependencies = [
"ahash 0.8.12",
"base64 0.22.1",
"bytecount",
"data-encoding",
"email_address",
"fancy-regex",
"fancy-regex 0.14.0",
"fraction",
"getrandom 0.3.4",
"idna",
"itoa",
"num-cmp",
"num-traits",
"once_cell",
"percent-encoding",
"referencing",
"regex",
@@ -8639,7 +8635,6 @@ dependencies = [
"reqwest 0.12.24",
"serde",
"serde_json",
"unicode-general-category",
"uuid-simd",
]
@@ -9046,9 +9041,7 @@ dependencies = [
"serde_json",
"serde_json_lenient",
"settings",
"smallvec",
"smol",
"snippet",
"task",
"terminal",
"text",
@@ -10188,7 +10181,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2b977c445f26e49757f9aca3631c3b8b836942cb278d69a92e7b80d3b24da632"
dependencies = [
"arrayvec",
"bit-set",
"bit-set 0.8.0",
"bitflags 2.9.4",
"cfg_aliases 0.2.1",
"codespan-reporting 0.12.0",
@@ -13044,7 +13037,7 @@ dependencies = [
"dap",
"dap_adapters",
"extension",
"fancy-regex",
"fancy-regex 0.14.0",
"fs",
"futures 0.3.31",
"fuzzy",
@@ -13915,14 +13908,13 @@ dependencies = [
[[package]]
name = "referencing"
version = "0.37.4"
version = "0.30.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4283168a506f0dcbdce31c9f9cce3129c924da4c6bca46e46707fcb746d2d70c"
checksum = "c8eff4fa778b5c2a57e85c5f2fe3a709c52f0e60d23146e2151cbef5893f420e"
dependencies = [
"ahash 0.8.12",
"fluent-uri",
"getrandom 0.3.4",
"hashbrown 0.16.1",
"once_cell",
"parking_lot",
"percent-encoding",
"serde_json",
@@ -15123,7 +15115,6 @@ dependencies = [
"editor",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
"language",
"lsp",
"menu",
@@ -17116,7 +17107,7 @@ dependencies = [
"alacritty_terminal",
"anyhow",
"collections",
"fancy-regex",
"fancy-regex 0.14.0",
"futures 0.3.31",
"gpui",
"itertools 0.14.0",
@@ -17350,12 +17341,12 @@ dependencies = [
[[package]]
name = "tiktoken-rs"
version = "0.9.1"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=2570c4387a8505fb8f1d3f3557454b474f1e8271#2570c4387a8505fb8f1d3f3557454b474f1e8271"
source = "git+https://github.com/zed-industries/tiktoken-rs?rev=7249f999c5fdf9bf3cc5c288c964454e4dac0c00#7249f999c5fdf9bf3cc5c288c964454e4dac0c00"
dependencies = [
"anyhow",
"base64 0.22.1",
"bstr",
"fancy-regex",
"fancy-regex 0.13.0",
"lazy_static",
"regex",
"rustc-hash 1.1.0",
@@ -18007,9 +17998,9 @@ dependencies = [
[[package]]
name = "tree-sitter-bash"
version = "0.25.1"
version = "0.25.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9e5ec769279cc91b561d3df0d8a5deb26b0ad40d183127f409494d6d8fc53062"
checksum = "871b0606e667e98a1237ebdc1b0d7056e0aebfdc3141d12b399865d4cb6ed8a6"
dependencies = [
"cc",
"tree-sitter-language",
@@ -18487,12 +18478,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "ce61d488bcdc9bc8b5d1772c404828b17fc481c0a582b5581e95fb233aef503e"
[[package]]
name = "unicode-general-category"
version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0b993bddc193ae5bd0d623b49ec06ac3e9312875fdae725a975c51db1cc1677f"
[[package]]
name = "unicode-ident"
version = "1.0.19"
@@ -18727,6 +18712,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "23b082222b4f6619906941c17eb2297fff4c2fb96cb60164170522942a200bd8"
dependencies = [
"outref",
"uuid",
"vsimd",
]
@@ -21026,7 +21012,6 @@ dependencies = [
"indexmap",
"indoc",
"serde",
"serde_json",
"toml 0.8.23",
"toml_edit 0.22.27",
]
@@ -21211,7 +21196,7 @@ dependencies = [
[[package]]
name = "zed"
version = "0.217.0"
version = "0.216.0"
dependencies = [
"acp_tools",
"activity_indicator",
@@ -21501,8 +21486,6 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
dependencies = [
"serde",
"serde_json",
@@ -21511,7 +21494,9 @@ dependencies = [
[[package]]
name = "zed_extension_api"
version = "0.8.0"
version = "0.7.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0729d50b4ca0a7e28e590bbe32e3ca0194d97ef654961451a424c661a366fca0"
dependencies = [
"serde",
"serde_json",
@@ -21529,12 +21514,12 @@ dependencies = [
name = "zed_html"
version = "0.2.3"
dependencies = [
"zed_extension_api 0.7.0",
"zed_extension_api 0.7.0 (registry+https://github.com/rust-lang/crates.io-index)",
]
[[package]]
name = "zed_proto"
version = "0.2.3"
version = "0.2.2"
dependencies = [
"zed_extension_api 0.1.0",
]
@@ -21543,7 +21528,7 @@ dependencies = [
name = "zed_test_extension"
version = "0.1.0"
dependencies = [
"zed_extension_api 0.8.0",
"zed_extension_api 0.7.0",
]
[[package]]
@@ -21687,7 +21672,6 @@ dependencies = [
"collections",
"command_palette_hooks",
"copilot",
"credentials_provider",
"ctor",
"db",
"edit_prediction",

View File

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

View File

@@ -43,9 +43,8 @@ design
= @danilo-leal
docs
= @miguelraz
= @probably-neb
= @yeskunall
= @miguelraz
extension
= @kubkon
@@ -53,10 +52,6 @@ extension
git
= @cole-miller
= @danilo-leal
= @dvdsk
= @kubkon
= @Anthony-Eid
= @cameron1024
gpui
= @Anthony-Eid
@@ -104,9 +99,6 @@ settings_ui
= @danilo-leal
= @probably-neb
sum_tree
= @Veykril
support
= @miguelraz
@@ -118,9 +110,6 @@ terminal
= @kubkon
= @Veykril
text
= @Veykril
vim
= @ConradIrwin
= @dinocosta
@@ -130,4 +119,3 @@ vim
windows
= @localcc
= @reflectronic
= @Veykril

View File

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

After

Width:  |  Height:  |  Size: 467 B

View File

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

Before

Width:  |  Height:  |  Size: 443 B

After

Width:  |  Height:  |  Size: 374 B

View File

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

Before

Width:  |  Height:  |  Size: 439 B

After

Width:  |  Height:  |  Size: 373 B

View File

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

Before

Width:  |  Height:  |  Size: 627 B

After

Width:  |  Height:  |  Size: 468 B

View File

@@ -1,10 +0,0 @@
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
<g clip-path="url(#clip0_1_2)">
<path fill-rule="evenodd" clip-rule="evenodd" d="M4.58747 12.9359C4.35741 12.778 4.17558 12.625 4.17558 12.625L10.092 2.37749C10.092 2.37749 10.3355 2.46782 10.5367 2.56426C10.7903 2.6858 11.0003 2.80429 11.0003 2.80429C13.8681 4.46005 14.8523 8.13267 13.1965 11.0005C11.5407 13.8684 7.8681 14.8525 5.00023 13.1967C5.00023 13.1967 4.79936 13.0812 4.58747 12.9359ZM10.5003 3.67032L5.50023 12.3307C7.89013 13.7105 10.9506 12.8904 12.3305 10.5006C13.7102 8.1106 12.8902 5.05015 10.5003 3.67032ZM3.07664 11.4314C2.87558 11.1403 2.804 11.0006 2.804 11.0006C1.77036 9.20524 1.69456 6.92215 2.80404 5.00046C3.91353 3.07877 5.92859 2.00291 8.0003 2.00036C8.0003 2.00036 8.28 1.99964 8.51289 2.02194C8.86375 2.05556 9.09702 2.10083 9.09702 2.10083L3.43905 11.9007C3.43905 11.9007 3.30482 11.7618 3.07664 11.4314ZM7.40178 3.03702C5.89399 3.22027 4.48727 4.08506 3.67008 5.50052C2.85288 6.9159 2.80733 8.56653 3.40252 9.96401L7.40178 3.03702Z" fill="black" stroke="black" stroke-width="0.1"/>
</g>
<defs>
<clipPath id="clip0_1_2">
<rect width="16" height="16" fill="white"/>
</clipPath>
</defs>
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

View File

@@ -1279,7 +1279,6 @@
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"ctrl-,": "settings_editor::OpenCurrentFile",
"left": "settings_editor::ToggleFocusNav",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI

View File

@@ -49,8 +49,7 @@
"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-c": "editor::DisplayCursorNames"
"ctrl-cmd-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -590,7 +589,8 @@
"cmd-.": "editor::ToggleCodeActions",
"cmd-k r": "editor::RevealInFileManager",
"cmd-k p": "editor::CopyPath",
"cmd-\\": "pane::SplitRight"
"cmd-\\": "pane::SplitRight",
"ctrl-cmd-c": "editor::DisplayCursorNames"
}
},
{
@@ -730,8 +730,7 @@
"context": "Workspace && debugger_running",
"use_key_equivalents": true,
"bindings": {
"f5": "zed::NoAction",
"f11": "debugger::StepInto"
"f5": "zed::NoAction"
}
},
{
@@ -1383,7 +1382,6 @@
"escape": "workspace::CloseWindow",
"cmd-m": "settings_editor::Minimize",
"cmd-f": "search::FocusSearch",
"cmd-,": "settings_editor::OpenCurrentFile",
"left": "settings_editor::ToggleFocusNav",
"cmd-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI

View File

@@ -36,12 +36,12 @@
"shift-f5": "debugger::Stop",
"ctrl-shift-f5": "debugger::RerunSession",
"f6": "debugger::Pause",
"f10": "debugger::StepOver",
"f7": "debugger::StepOver",
"ctrl-f11": "debugger::StepInto",
"shift-f11": "debugger::StepOut",
"f11": "zed::ToggleFullScreen",
"ctrl-shift-i": "edit_prediction::ToggleMenu",
"shift-alt-l": "lsp_tool::ToggleMenu",
"ctrl-shift-alt-c": "editor::DisplayCursorNames"
"shift-alt-l": "lsp_tool::ToggleMenu"
}
},
{
@@ -117,7 +117,7 @@
"alt-g m": "git::OpenModifiedFiles",
"menu": "editor::OpenContextMenu",
"shift-f10": "editor::OpenContextMenu",
"ctrl-alt-e": "editor::ToggleEditPrediction",
"ctrl-shift-e": "editor::ToggleEditPrediction",
"f9": "editor::ToggleBreakpoint",
"shift-f9": "editor::EditLogBreakpoint"
}
@@ -215,7 +215,7 @@
"context": "ContextEditor > Editor",
"use_key_equivalents": true,
"bindings": {
"ctrl-i": "assistant::Assist",
"ctrl-enter": "assistant::Assist",
"ctrl-s": "workspace::Save",
"ctrl-shift-,": "assistant::InsertIntoEditor",
"shift-enter": "assistant::Split",
@@ -500,7 +500,10 @@
"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 }],
@@ -509,8 +512,12 @@
"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",
@@ -534,6 +541,7 @@
"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"
}
@@ -1116,7 +1124,7 @@
"shift-insert": "terminal::Paste",
"ctrl-v": "terminal::Paste",
"ctrl-shift-v": "terminal::Paste",
"ctrl-i": "assistant::InlineAssist",
"ctrl-enter": "assistant::InlineAssist",
"alt-b": ["terminal::SendText", "\u001bb"],
"alt-f": ["terminal::SendText", "\u001bf"],
"alt-.": ["terminal::SendText", "\u001b."],
@@ -1297,7 +1305,6 @@
"escape": "workspace::CloseWindow",
"ctrl-m": "settings_editor::Minimize",
"ctrl-f": "search::FocusSearch",
"ctrl-,": "settings_editor::OpenCurrentFile",
"left": "settings_editor::ToggleFocusNav",
"ctrl-shift-e": "settings_editor::ToggleFocusNav",
// todo(settings_ui): cut this down based on the max files and overflow UI

View File

@@ -857,8 +857,6 @@
"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",

View File

@@ -1100,22 +1100,13 @@
"preview_tabs": {
// Whether preview tabs should be enabled.
// Preview tabs allow you to open files in preview mode, where they close automatically
// when you open another preview tab.
// when you switch to another file unless you explicitly pin them.
// This is useful for quickly viewing files without cluttering your workspace.
"enabled": true,
// Whether to open tabs in preview mode when opened from the project panel with a single click.
"enable_preview_from_project_panel": true,
// Whether to open tabs in preview mode when selected from the file finder.
"enable_preview_from_file_finder": false,
// Whether to open tabs in preview mode when opened from a multibuffer.
"enable_preview_from_multibuffer": true,
// Whether to open tabs in preview mode when code navigation is used to open a multibuffer.
"enable_preview_multibuffer_from_code_navigation": false,
// Whether to open tabs in preview mode when code navigation is used to open a single file.
"enable_preview_file_from_code_navigation": true,
// Whether to keep tabs in preview mode when code navigation is used to navigate away from them.
// If `enable_preview_file_from_code_navigation` or `enable_preview_multibuffer_from_code_navigation` is also true, the new tab may replace the existing one.
"enable_keep_preview_on_code_navigation": false
// Whether a preview tab gets replaced when code navigation is used to navigate away from the tab.
"enable_preview_from_code_navigation": false
},
// Settings related to the file finder.
"file_finder": {
@@ -1218,13 +1209,6 @@
"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.
@@ -1366,8 +1350,6 @@
// "load_direnv": "direct"
// 2. Load direnv configuration through the shell hook, works for POSIX shells and fish.
// "load_direnv": "shell_hook"
// 3. Don't load direnv configuration at all.
// "load_direnv": "disabled"
"load_direnv": "direct",
"edit_predictions": {
// A list of globs representing files that edit predictions should be disabled for.

View File

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

View File

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

View File

@@ -45,7 +45,6 @@
"tab.inactive_background": "#2f343eff",
"tab.active_background": "#282c33ff",
"search.match_background": "#74ade866",
"search.active_match_background": "#e8af7466",
"panel.background": "#2f343eff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -99,8 +98,6 @@
"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",
@@ -449,7 +446,6 @@
"tab.inactive_background": "#ebebecff",
"tab.active_background": "#fafafaff",
"search.match_background": "#5c79e266",
"search.active_match_background": "#d0a92366",
"panel.background": "#ebebecff",
"panel.focused_border": null,
"pane.focused_border": null,
@@ -503,8 +499,6 @@
"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",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -66,9 +66,11 @@ 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}\n\n{outline_text}",)
format!(
"# File outline for {path} (file too large to show full content)\n\n{outline_text}",
)
} else {
format!("# File outline\n\n{outline_text}",)
format!("# File outline (file too large to show full content)\n\n{outline_text}",)
};
Ok(BufferContent {
text,

View File

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

View File

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

View File

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

View File

@@ -118,29 +118,33 @@ impl AgentTool for FindPathTool {
let paginated_matches: &[PathBuf] = &matches[cmp::min(input.offset, matches.len())
..cmp::min(input.offset + RESULTS_PER_PAGE, matches.len())];
event_stream.update_fields(
acp::ToolCallUpdateFields::new()
.title(if paginated_matches.is_empty() {
"No matches".into()
} else if paginated_matches.len() == 1 {
"1 match".into()
} else {
format!("{} matches", paginated_matches.len())
})
.content(
paginated_matches
.iter()
.map(|path| {
acp::ToolCallContent::Content(acp::Content::new(
acp::ContentBlock::ResourceLink(acp::ResourceLink::new(
path.to_string_lossy(),
format!("file://{}", path.display()),
)),
))
})
.collect(),
),
);
event_stream.update_fields(acp::ToolCallUpdateFields {
title: Some(if paginated_matches.is_empty() {
"No matches".into()
} else if paginated_matches.len() == 1 {
"1 match".into()
} else {
format!("{} matches", paginated_matches.len())
}),
content: Some(
paginated_matches
.iter()
.map(|path| acp::ToolCallContent::Content {
content: acp::ContentBlock::ResourceLink(acp::ResourceLink {
uri: format!("file://{}", path.display()),
name: path.to_string_lossy().into(),
annotations: None,
description: None,
mime_type: None,
size: None,
title: None,
meta: None,
}),
})
.collect(),
),
..Default::default()
});
Ok(FindPathToolOutput {
offset: input.offset,
@@ -173,7 +177,7 @@ fn search_paths(glob: &str, project: Entity<Project>, cx: &mut App) -> Task<Resu
let mut results = Vec::new();
for snapshot in snapshots {
for entry in snapshot.entries(false, 0) {
if path_matcher.is_match(&snapshot.root_name().join(&entry.path)) {
if path_matcher.is_match(snapshot.root_name().join(&entry.path).as_std_path()) {
results.push(snapshot.absolutize(&entry.path));
}
}

View File

@@ -32,21 +32,8 @@ pub struct GrepToolInput {
/// Do NOT specify a path here! This will only be matched against the code **content**.
pub regex: String,
/// A glob pattern for the paths of files to include in the search.
/// Supports standard glob patterns like "**/*.rs" or "frontend/src/**/*.ts".
/// Supports standard glob patterns like "**/*.rs" or "src/**/*.ts".
/// If omitted, all files in the project will be searched.
///
/// The glob pattern is matched against the full path including the project root directory.
///
/// <example>
/// If the project has the following root directories:
///
/// - /a/b/backend
/// - /c/d/frontend
///
/// Use "backend/**/*.rs" to search only Rust files in the backend root directory.
/// Use "frontend/src/**/*.ts" to search TypeScript files only in the frontend root directory (sub-directory "src").
/// Use "**/*.rs" to search Rust files across all root directories.
/// </example>
pub include_pattern: Option<String>,
/// Optional starting position for paginated results (0-based).
/// When not provided, starts from the beginning.
@@ -145,7 +132,8 @@ impl AgentTool for GrepTool {
let exclude_patterns = global_settings
.file_scan_exclusions
.sources()
.chain(global_settings.private_files.sources());
.iter()
.chain(global_settings.private_files.sources().iter());
match PathMatcher::new(exclude_patterns, path_style) {
Ok(matcher) => matcher,

View File

@@ -17,9 +17,6 @@ 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.
@@ -152,12 +149,15 @@ impl AgentTool for ReadFileTool {
}
let file_path = input.path.clone();
let mut location = acp::ToolCallLocation::new(&abs_path);
if let Some(line) = input.start_line {
location = location.line(line.saturating_sub(1));
}
event_stream.update_fields(ToolCallUpdateFields::new().locations(vec![location]));
event_stream.update_fields(ToolCallUpdateFields {
locations: Some(vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: input.start_line.map(|line| line.saturating_sub(1)),
meta: None,
}]),
..Default::default()
});
if image_store::is_image_file(&self.project, &project_path, cx) {
return cx.spawn(async move |cx| {
@@ -254,15 +254,16 @@ impl AgentTool for ReadFileTool {
if buffer_content.is_outline {
Ok(formatdoc! {"
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.
This file was too big to read all at once.
{}
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
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
}
.into())
} else {
@@ -286,9 +287,12 @@ impl AgentTool for ReadFileTool {
text,
}
.to_string();
event_stream.update_fields(ToolCallUpdateFields::new().content(vec![
acp::ToolCallContent::Content(acp::Content::new(markdown)),
]));
event_stream.update_fields(ToolCallUpdateFields {
content: Some(vec![acp::ToolCallContent::Content {
content: markdown.into(),
}]),
..Default::default()
})
}
})?;
@@ -436,7 +440,7 @@ mod test {
let content = result.to_str().unwrap();
assert_eq!(
content.lines().skip(7).take(6).collect::<Vec<_>>(),
content.lines().skip(4).take(6).collect::<Vec<_>>(),
vec![
"struct Test0 [L1-4]",
" a [L2]",
@@ -471,7 +475,7 @@ mod test {
pretty_assertions::assert_eq!(
content
.lines()
.skip(7)
.skip(4)
.take(expected_content.len())
.collect::<Vec<_>>(),
expected_content

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -446,10 +446,9 @@ impl AcpThreadHistory {
.tooltip(move |_window, cx| {
Tooltip::for_action("Delete", &RemoveSelectedThread, cx)
})
.on_click(cx.listener(move |this, _, _, cx| {
this.remove_thread(ix, cx);
cx.stop_propagation()
})),
.on_click(
cx.listener(move |this, _, _, cx| this.remove_thread(ix, cx)),
),
)
} else {
None

View File

@@ -498,7 +498,17 @@ impl AcpThreadView {
Some(new_version_available_tx),
);
let connect_task = agent.connect(root_dir.as_deref(), delegate, cx);
let agent_name = agent.name();
let timeout = cx.background_executor().timer(Duration::from_secs(30));
let connect_task = smol::future::or(
agent.connect(root_dir.as_deref(), delegate, cx),
async move {
timeout.await;
Err(anyhow::Error::new(LoadError::Other(
format!("{agent_name} is unable to initialize after 30 seconds.").into(),
)))
},
);
let load_task = cx.spawn_in(window, async move |this, cx| {
let connection = match connect_task.await {
Ok((connection, login)) => {
@@ -665,8 +675,6 @@ impl AcpThreadView {
})
});
this.message_editor.focus_handle(cx).focus(window);
cx.notify();
}
Err(err) => {
@@ -1001,10 +1009,6 @@ impl AcpThreadView {
}
}
pub fn is_loading(&self) -> bool {
matches!(self.thread_state, ThreadState::Loading { .. })
}
fn resume_chat(&mut self, cx: &mut Context<Self>) {
self.thread_error.take();
let Some(thread) = self.thread() else {
@@ -1476,8 +1480,18 @@ impl AcpThreadView {
.iter()
.any(|method| method.id.0.as_ref() == "claude-login")
{
available_commands.push(acp::AvailableCommand::new("login", "Authenticate"));
available_commands.push(acp::AvailableCommand::new("logout", "Authenticate"));
available_commands.push(acp::AvailableCommand {
name: "login".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
available_commands.push(acp::AvailableCommand {
name: "logout".to_owned(),
description: "Authenticate".to_owned(),
input: None,
meta: None,
});
}
let has_commands = !available_commands.is_empty();
@@ -2552,7 +2566,7 @@ impl AcpThreadView {
acp::ToolKind::Think => IconName::ToolThink,
acp::ToolKind::Fetch => IconName::ToolWeb,
acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
acp::ToolKind::Other | _ => IconName::ToolHammer,
acp::ToolKind::Other => IconName::ToolHammer,
})
}
.size(IconSize::Small)
@@ -2804,7 +2818,7 @@ impl AcpThreadView {
})
.gap_0p5()
.children(options.iter().map(move |option| {
let option_id = SharedString::from(option.option_id.0.clone());
let option_id = SharedString::from(option.id.0.clone());
Button::new((option_id, entry_ix), option.name.clone())
.map(|this| {
let (this, action) = match option.kind {
@@ -2820,7 +2834,7 @@ impl AcpThreadView {
this.icon(IconName::Close).icon_color(Color::Error),
Some(&RejectOnce as &dyn Action),
),
acp::PermissionOptionKind::RejectAlways | _ => {
acp::PermissionOptionKind::RejectAlways => {
(this.icon(IconName::Close).icon_color(Color::Error), None)
}
};
@@ -2845,7 +2859,7 @@ impl AcpThreadView {
.label_size(LabelSize::Small)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
let option_id = option.option_id.clone();
let option_id = option.id.clone();
let option_kind = option.kind;
move |this, _, window, cx| {
this.authorize_tool_call(
@@ -3533,7 +3547,7 @@ impl AcpThreadView {
);
this.authenticate(
acp::AuthMethodId::new(method_id.clone()),
acp::AuthMethodId(method_id.clone()),
window,
cx,
)
@@ -3798,64 +3812,48 @@ impl AcpThreadView {
}))
}
fn render_plan_entries(
&self,
plan: &Plan,
window: &mut Window,
cx: &Context<Self>,
) -> impl IntoElement {
v_flex()
.id("plan_items_list")
.max_h_40()
.overflow_y_scroll()
.children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
let element = h_flex()
.py_1()
.px_2()
.gap_2()
.justify_between()
.bg(cx.theme().colors().editor_background)
.when(index < plan.entries.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
.child(
h_flex()
.id(("plan_entry", index))
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(match entry.status {
acp::PlanEntryStatus::InProgress => {
Icon::new(IconName::TodoProgress)
.size(IconSize::Small)
.color(Color::Accent)
.with_rotate_animation(2)
.into_any_element()
}
acp::PlanEntryStatus::Completed => {
Icon::new(IconName::TodoComplete)
.size(IconSize::Small)
.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(),
plan_label_markdown_style(&entry.status, window, cx),
)),
);
fn render_plan_entries(&self, plan: &Plan, window: &mut Window, cx: &Context<Self>) -> Div {
v_flex().children(plan.entries.iter().enumerate().flat_map(|(index, entry)| {
let element = h_flex()
.py_1()
.px_2()
.gap_2()
.justify_between()
.bg(cx.theme().colors().editor_background)
.when(index < plan.entries.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
.child(
h_flex()
.id(("plan_entry", index))
.gap_1p5()
.max_w_full()
.overflow_x_scroll()
.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)
.color(Color::Accent)
.with_rotate_animation(2)
.into_any_element(),
acp::PlanEntryStatus::Completed => Icon::new(IconName::TodoComplete)
.size(IconSize::Small)
.color(Color::Success)
.into_any_element(),
})
.child(MarkdownElement::new(
entry.content.clone(),
plan_label_markdown_style(&entry.status, window, cx),
)),
);
Some(element)
}))
.into_any_element()
Some(element)
}))
}
fn render_edits_summary(
@@ -3993,136 +3991,126 @@ impl AcpThreadView {
changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
pending_edits: bool,
cx: &Context<Self>,
) -> impl IntoElement {
) -> Div {
let editor_bg_color = cx.theme().colors().editor_background;
v_flex()
.id("edited_files_list")
.max_h_40()
.overflow_y_scroll()
.children(
changed_buffers
.iter()
.enumerate()
.flat_map(|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let path_style = file.path_style(cx);
let separator = file.path_style(cx).primary_separator();
v_flex().children(changed_buffers.iter().enumerate().flat_map(
|(index, (buffer, _diff))| {
let file = buffer.read(cx).file()?;
let path = file.path();
let path_style = file.path_style(cx);
let separator = file.path_style(cx).primary_separator();
let file_path = path.parent().and_then(|parent| {
if parent.is_empty() {
None
} else {
Some(
Label::new(format!(
"{}{separator}",
parent.display(path_style)
))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx),
)
}
});
let file_name = path.file_name().map(|name| {
Label::new(name.to_string())
let file_path = path.parent().and_then(|parent| {
if parent.is_empty() {
None
} else {
Some(
Label::new(format!("{}{separator}", parent.display(path_style)))
.color(Color::Muted)
.size(LabelSize::XSmall)
.buffer_font(cx)
.ml_1p5()
});
.buffer_font(cx),
)
}
});
let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
.unwrap_or_else(|| {
Icon::new(IconName::File)
.color(Color::Muted)
.size(IconSize::Small)
});
let file_name = path.file_name().map(|name| {
Label::new(name.to_string())
.size(LabelSize::XSmall)
.buffer_font(cx)
.ml_1p5()
});
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(editor_bg_color, 1.),
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
);
let file_icon = FileIcons::get_icon(path.as_std_path(), cx)
.map(Icon::from_path)
.map(|icon| icon.color(Color::Muted).size(IconSize::Small))
.unwrap_or_else(|| {
Icon::new(IconName::File)
.color(Color::Muted)
.size(IconSize::Small)
});
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
.bg(editor_bg_color)
.when(index < changed_buffers.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
let overlay_gradient = linear_gradient(
90.,
linear_color_stop(editor_bg_color, 1.),
linear_color_stop(editor_bg_color.opacity(0.2), 0.),
);
let element = h_flex()
.group("edited-code")
.id(("file-container", index))
.py_1()
.pl_2()
.pr_1()
.gap_2()
.justify_between()
.bg(editor_bg_color)
.when(index < changed_buffers.len() - 1, |parent| {
parent.border_color(cx.theme().colors().border).border_b_1()
})
.child(
h_flex()
.id(("file-name-row", index))
.relative()
.pr_8()
.w_full()
.overflow_x_scroll()
.child(
h_flex()
.id(("file-name-row", index))
.relative()
.pr_8()
.w_full()
.overflow_x_scroll()
.child(
h_flex()
.id(("file-name-path", index))
.cursor_pointer()
.pr_0p5()
.gap_0p5()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.rounded_xs()
.child(file_icon)
.children(file_name)
.children(file_path)
.tooltip(Tooltip::text("Go to File"))
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(
div()
.absolute()
.h_full()
.w_12()
.top_0()
.bottom_0()
.right_0()
.bg(overlay_gradient),
),
.id(("file-name-path", index))
.cursor_pointer()
.pr_0p5()
.gap_0p5()
.hover(|s| s.bg(cx.theme().colors().element_hover))
.rounded_xs()
.child(file_icon)
.children(file_name)
.children(file_path)
.tooltip(Tooltip::text("Go to File"))
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(
h_flex()
.gap_1()
.visible_on_hover("edited-code")
.child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(Divider::vertical().color(DividerColor::BorderVariant))
.child(
Button::new("reject-file", "Reject")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
let action_log = action_log.clone();
let telemetry = telemetry.clone();
move |_, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log
div()
.absolute()
.h_full()
.w_12()
.top_0()
.bottom_0()
.right_0()
.bg(overlay_gradient),
),
)
.child(
h_flex()
.gap_1()
.visible_on_hover("edited-code")
.child(
Button::new("review", "Review")
.label_size(LabelSize::Small)
.on_click({
let buffer = buffer.clone();
cx.listener(move |this, _, window, cx| {
this.open_edited_buffer(&buffer, window, cx);
})
}),
)
.child(Divider::vertical().color(DividerColor::BorderVariant))
.child(
Button::new("reject-file", "Reject")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
let action_log = action_log.clone();
let telemetry = telemetry.clone();
move |_, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log
.reject_edits_in_ranges(
buffer.clone(),
vec![Anchor::min_max_range_for_buffer(
@@ -4132,38 +4120,37 @@ impl AcpThreadView {
cx,
)
.detach_and_log_err(cx);
})
}
}),
)
.child(
Button::new("keep-file", "Keep")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
let action_log = action_log.clone();
let telemetry = telemetry.clone();
move |_, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log.keep_edits_in_range(
buffer.clone(),
Anchor::min_max_range_for_buffer(
buffer.read(cx).remote_id(),
),
Some(telemetry.clone()),
cx,
);
})
}
}),
),
);
})
}
}),
)
.child(
Button::new("keep-file", "Keep")
.label_size(LabelSize::Small)
.disabled(pending_edits)
.on_click({
let buffer = buffer.clone();
let action_log = action_log.clone();
let telemetry = telemetry.clone();
move |_, _, cx| {
action_log.update(cx, |action_log, cx| {
action_log.keep_edits_in_range(
buffer.clone(),
Anchor::min_max_range_for_buffer(
buffer.read(cx).remote_id(),
),
Some(telemetry.clone()),
cx,
);
})
}
}),
),
);
Some(element)
}),
)
.into_any_element()
Some(element)
},
))
}
fn render_message_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
@@ -4184,10 +4171,8 @@ impl AcpThreadView {
.block_mouse_except_scroll();
let enable_editor = match self.thread_state {
ThreadState::Ready { .. } => true,
ThreadState::Loading { .. }
| ThreadState::Unauthenticated { .. }
| ThreadState::LoadError(..) => false,
ThreadState::Loading { .. } | ThreadState::Ready { .. } => true,
ThreadState::Unauthenticated { .. } | ThreadState::LoadError(..) => false,
};
v_flex()
@@ -4419,7 +4404,7 @@ impl AcpThreadView {
self.authorize_tool_call(
tool_call.id.clone(),
option.option_id.clone(),
option.id.clone(),
option.kind,
window,
cx,
@@ -5848,10 +5833,12 @@ fn placeholder_text(agent_name: &str, has_commands: bool) -> String {
impl Focusable for AcpThreadView {
fn focus_handle(&self, cx: &App) -> FocusHandle {
match self.thread_state {
ThreadState::Ready { .. } => self.active_editor(cx).focus_handle(cx),
ThreadState::Loading { .. }
| ThreadState::LoadError(_)
| ThreadState::Unauthenticated { .. } => self.focus_handle.clone(),
ThreadState::Loading { .. } | ThreadState::Ready { .. } => {
self.active_editor(cx).focus_handle(cx)
}
ThreadState::LoadError(_) | ThreadState::Unauthenticated { .. } => {
self.focus_handle.clone()
}
}
}
}
@@ -6235,18 +6222,27 @@ pub(crate) mod tests {
async fn test_notification_for_tool_authorization(cx: &mut TestAppContext) {
init_test(cx);
let tool_call_id = acp::ToolCallId::new("1");
let tool_call = acp::ToolCall::new(tool_call_id.clone(), "Label")
.kind(acp::ToolKind::Edit)
.content(vec!["hi".into()]);
let tool_call_id = acp::ToolCallId("1".into());
let tool_call = acp::ToolCall {
id: tool_call_id.clone(),
title: "Label".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Pending,
content: vec!["hi".into()],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
};
let connection =
StubAgentConnection::new().with_permission_requests(HashMap::from_iter([(
tool_call_id,
vec![acp::PermissionOption::new(
"1".into(),
"Allow",
acp::PermissionOptionKind::AllowOnce,
)],
vec![acp::PermissionOption {
id: acp::PermissionOptionId("1".into()),
name: "Allow".into(),
kind: acp::PermissionOptionKind::AllowOnce,
meta: None,
}],
)]));
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(tool_call)]);
@@ -6465,7 +6461,10 @@ pub(crate) mod tests {
fn default_response() -> Self {
let conn = StubAgentConnection::new();
conn.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Default response".into()),
acp::ContentChunk {
content: "Default response".into(),
meta: None,
},
)]);
Self::new(conn)
}
@@ -6522,13 +6521,13 @@ pub(crate) mod tests {
self,
project,
action_log,
SessionId::new("test"),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
SessionId("test".into()),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
})))
@@ -6586,13 +6585,13 @@ pub(crate) mod tests {
self,
project,
action_log,
SessionId::new("test"),
watch::Receiver::constant(
acp::PromptCapabilities::new()
.image(true)
.audio(true)
.embedded_context(true),
),
SessionId("test".into()),
watch::Receiver::constant(acp::PromptCapabilities {
image: true,
audio: true,
embedded_context: true,
meta: None,
}),
cx,
)
})))
@@ -6616,7 +6615,10 @@ pub(crate) mod tests {
_params: acp::PromptRequest,
_cx: &mut App,
) -> Task<gpui::Result<acp::PromptResponse>> {
Task::ready(Ok(acp::PromptResponse::new(acp::StopReason::Refusal)))
Task::ready(Ok(acp::PromptResponse {
stop_reason: acp::StopReason::Refusal,
meta: None,
}))
}
fn cancel(&self, _session_id: &acp::SessionId, _cx: &mut App) {
@@ -6684,14 +6686,24 @@ pub(crate) mod tests {
.unwrap();
// First user message
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
acp::ToolCall::new("tool1", "Edit file 1")
.kind(acp::ToolKind::Edit)
.status(acp::ToolCallStatus::Completed)
.content(vec![acp::ToolCallContent::Diff(
acp::Diff::new("/project/test1.txt", "new content 1").old_text("old content 1"),
)]),
)]);
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool1".into()),
title: "Edit file 1".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Completed,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/test1.txt".into(),
old_text: Some("old content 1".into()),
new_text: "new content 1".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
thread
.update(cx, |thread, cx| thread.send_raw("Give me a diff", cx))
@@ -6717,14 +6729,24 @@ pub(crate) mod tests {
});
// Second user message
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(
acp::ToolCall::new("tool2", "Edit file 2")
.kind(acp::ToolKind::Edit)
.status(acp::ToolCallStatus::Completed)
.content(vec![acp::ToolCallContent::Diff(
acp::Diff::new("/project/test2.txt", "new content 2").old_text("old content 2"),
)]),
)]);
connection.set_next_prompt_updates(vec![acp::SessionUpdate::ToolCall(acp::ToolCall {
id: acp::ToolCallId("tool2".into()),
title: "Edit file 2".into(),
kind: acp::ToolKind::Edit,
status: acp::ToolCallStatus::Completed,
content: vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: "/project/test2.txt".into(),
old_text: Some("old content 2".into()),
new_text: "new content 2".into(),
meta: None,
},
}],
locations: vec![],
raw_input: None,
raw_output: None,
meta: None,
})]);
thread
.update(cx, |thread, cx| thread.send_raw("Another one", cx))
@@ -6798,7 +6820,14 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -6884,7 +6913,14 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
let (thread_view, cx) =
@@ -6924,7 +6960,14 @@ pub(crate) mod tests {
// Send
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("New Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "New Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
user_message_editor.update_in(cx, |_editor, window, cx| {
@@ -7012,7 +7055,14 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("Response".into())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
}),
cx,
);
connection.end_turn(session_id, acp::StopReason::EndTurn);
@@ -7064,9 +7114,10 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"Message 1 resp".into(),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "Message 1 resp".into(),
meta: None,
}),
cx,
);
});
@@ -7100,7 +7151,10 @@ pub(crate) mod tests {
// Simulate a response sent after beginning to cancel
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new("onse".into())),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "onse".into(),
meta: None,
}),
cx,
);
});
@@ -7131,9 +7185,10 @@ pub(crate) mod tests {
cx.update(|_, cx| {
connection.send_update(
session_id.clone(),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk::new(
"Message 2 response".into(),
)),
acp::SessionUpdate::AgentMessageChunk(acp::ContentChunk {
content: "Message 2 response".into(),
meta: None,
}),
cx,
);
connection.end_turn(session_id.clone(), acp::StopReason::EndTurn);
@@ -7172,7 +7227,14 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -7251,7 +7313,14 @@ pub(crate) mod tests {
let connection = StubAgentConnection::new();
connection.set_next_prompt_updates(vec![acp::SessionUpdate::AgentMessageChunk(
acp::ContentChunk::new("Response".into()),
acp::ContentChunk {
content: acp::ContentBlock::Text(acp::TextContent {
text: "Response".into(),
annotations: None,
meta: None,
}),
meta: None,
},
)]);
let (thread_view, cx) = setup_thread_view(StubAgentServer::new(connection), cx).await;
@@ -7299,4 +7368,54 @@ pub(crate) mod tests {
assert_eq!(text, expected_txt);
})
}
#[gpui::test]
async fn test_initialize_timeout(cx: &mut TestAppContext) {
init_test(cx);
struct InfiniteInitialize;
impl AgentServer for InfiniteInitialize {
fn telemetry_id(&self) -> &'static str {
"test"
}
fn logo(&self) -> ui::IconName {
ui::IconName::Ai
}
fn name(&self) -> SharedString {
"Test".into()
}
fn connect(
&self,
_root_dir: Option<&Path>,
_delegate: AgentServerDelegate,
cx: &mut App,
) -> Task<gpui::Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>>
{
cx.spawn(async |_| futures::future::pending().await)
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
let (thread_view, cx) = setup_thread_view(InfiniteInitialize, cx).await;
cx.executor().advance_clock(Duration::from_secs(31));
cx.run_until_parked();
let error = thread_view.read_with(cx, |thread_view, _| match &thread_view.thread_state {
ThreadState::LoadError(err) => err.clone(),
_ => panic!("Incorrect thread state"),
});
match error {
LoadError::Other(str) => assert!(str.contains("initialize")),
_ => panic!("Unexpected load error"),
}
}
}

View File

@@ -1,4 +1,7 @@
use std::sync::{Arc, Mutex};
use std::{
path::PathBuf,
sync::{Arc, Mutex},
};
use anyhow::{Context as _, Result};
use collections::HashMap;
@@ -221,12 +224,11 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
Some((id, cmd)) => {
let args = serde_json::to_string(&cmd.args).unwrap();
let env = serde_json::to_string(&cmd.env.unwrap_or_default()).unwrap();
let cmd_path = serde_json::to_string(&cmd.path).unwrap();
(id.0.to_string(), cmd_path, args, env)
(id.0.to_string(), cmd.path, args, env)
}
None => (
"some-mcp-server".to_string(),
"".to_string(),
PathBuf::new(),
"[]".to_string(),
"{}".to_string(),
),
@@ -237,13 +239,14 @@ fn context_server_input(existing: Option<(ContextServerId, ContextServerCommand)
/// The name of your MCP server
"{name}": {{
/// The command which runs the MCP server
"command": {command},
"command": "{}",
/// The arguments to pass to the MCP server
"args": {args},
/// The environment variables to set
"env": {env}
}}
}}"#
}}"#,
command.display()
)
}

View File

@@ -493,7 +493,7 @@ impl Item for AgentDiffPane {
Some("Assistant Diff Opened")
}
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}

View File

@@ -1,4 +1,7 @@
use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration};
use std::ops::Range;
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use acp_thread::AcpThread;
use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore};
@@ -43,9 +46,9 @@ use extension::ExtensionEvents;
use extension_host::ExtensionStore;
use fs::Fs;
use gpui::{
Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, Corner, DismissEvent,
Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription,
Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between,
Action, AnyElement, App, AsyncWindowContext, Corner, DismissEvent, Entity, EventEmitter,
ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, Task, UpdateGlobal,
WeakEntity, prelude::*,
};
use language::LanguageRegistry;
use language_model::{ConfigurationError, LanguageModelRegistry};
@@ -55,9 +58,10 @@ use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
Callout, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle,
ProgressBar, Tab, Tooltip, prelude::*, utils::WithRemSize,
ProgressBar, Tab, Tooltip, prelude::*,
};
use util::ResultExt as _;
use workspace::{
@@ -2155,41 +2159,28 @@ impl AgentPanel {
let selected_agent_label = self.selected_agent.label();
let is_thread_loading = self
.active_thread_view()
.map(|thread| thread.read(cx).is_loading())
.unwrap_or(false);
let has_custom_icon = selected_agent_custom_icon.is_some();
let selected_agent = div()
.id("selected_agent_icon")
.when_some(selected_agent_custom_icon, |this, icon_path| {
let label = selected_agent_label.clone();
this.px_1()
.child(Icon::from_external_svg(icon_path).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
})
})
.when(!has_custom_icon, |this| {
this.when_some(self.selected_agent.icon(), |this, icon| {
this.px_1().child(Icon::new(icon).color(Color::Muted))
let label = selected_agent_label.clone();
this.px_1()
.child(Icon::new(icon).color(Color::Muted))
.tooltip(move |_window, cx| {
Tooltip::with_meta(label.clone(), None, "Selected Agent", cx)
})
})
})
.tooltip(move |_, cx| {
Tooltip::with_meta(selected_agent_label.clone(), None, "Selected Agent", cx)
});
let selected_agent = if is_thread_loading {
selected_agent
.with_animation(
"pulsating-icon",
Animation::new(Duration::from_secs(1))
.repeat()
.with_easing(pulsating_between(0.2, 0.6)),
|icon, delta| icon.opacity(delta),
)
.into_any_element()
} else {
selected_agent.into_any_element()
};
.into_any_element();
h_flex()
.id("agent-panel-toolbar")
@@ -2685,17 +2676,16 @@ impl rules_library::InlineAssistDelegate for PromptLibraryInlineAssist {
return;
};
let project = workspace.read(cx).project().downgrade();
let thread_store = panel.read(cx).thread_store().clone();
assistant.assist(
prompt_editor,
self.workspace.clone(),
project,
thread_store,
panel.read(cx).thread_store().clone(),
None,
initial_prompt,
window,
cx,
);
)
})
}

View File

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

View File

@@ -271,7 +271,7 @@ impl CodegenAlternative {
let mut buffer = Buffer::local_normalized(text, line_ending, cx);
buffer.set_language(language, cx);
if let Some(language_registry) = language_registry {
buffer.set_language_registry(language_registry);
buffer.set_language_registry(language_registry)
}
buffer
});
@@ -719,7 +719,6 @@ impl CodegenAlternative {
output_tokens = usage.output_tokens,
)
}
cx.emit(CodegenEvent::Finished);
cx.notify();
})

View File

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

View File

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

View File

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

View File

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

View File

@@ -2556,11 +2556,7 @@ impl Item for TextThreadEditor {
Some(self.title(cx).to_string().into())
}
fn as_searchable(
&self,
handle: &Entity<Self>,
_: &App,
) -> Option<Box<dyn SearchableItemHandle>> {
fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
@@ -2622,13 +2618,11 @@ impl SearchableItem for TextThreadEditor {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |editor, cx| {
editor.update_matches(matches, active_match_index, window, cx)
});
self.editor
.update(cx, |editor, cx| editor.update_matches(matches, window, cx));
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

@@ -233,11 +233,18 @@ fn collect_diagnostics(
options: Options,
cx: &mut App,
) -> Task<Result<Option<SlashCommandOutput>>> {
let error_source = if let Some(path_matcher) = &options.path_matcher {
debug_assert_eq!(path_matcher.sources().len(), 1);
Some(path_matcher.sources().first().cloned().unwrap_or_default())
} else {
None
};
let path_style = project.read(cx).path_style(cx);
let glob_is_exact_file_match = if let Some(path) = options
.path_matcher
.as_ref()
.and_then(|pm| pm.sources().next())
.and_then(|pm| pm.sources().first())
{
project
.read(cx)
@@ -259,13 +266,6 @@ fn collect_diagnostics(
.collect();
cx.spawn(async move |cx| {
let error_source = if let Some(path_matcher) = &options.path_matcher {
debug_assert_eq!(path_matcher.sources().count(), 1);
Some(path_matcher.sources().next().unwrap_or_default())
} else {
None
};
let mut output = SlashCommandOutput::default();
if let Some(error_source) = error_source.as_ref() {
@@ -277,7 +277,7 @@ fn collect_diagnostics(
let mut project_summary = DiagnosticSummary::default();
for (project_path, path, summary) in diagnostic_summaries {
if let Some(path_matcher) = &options.path_matcher
&& !path_matcher.is_match(&path)
&& !path_matcher.is_match(&path.as_std_path())
{
continue;
}

View File

@@ -12,7 +12,7 @@ workspace = true
path = "src/buffer_diff.rs"
[features]
test-support = ["settings"]
test-support = []
[dependencies]
anyhow.workspace = true
@@ -24,7 +24,6 @@ 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
@@ -34,7 +33,6 @@ 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

View File

@@ -1,10 +1,7 @@
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, DiffOptions, File, Language, LanguageName, LanguageRegistry,
language_settings::language_settings, word_diff_ranges,
};
use language::{Language, LanguageRegistry};
use rope::Rope;
use std::{
cmp::Ordering,
@@ -14,16 +11,14 @@ use std::{
sync::{Arc, LazyLock},
};
use sum_tree::SumTree;
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _, ToPoint as _};
use text::{Anchor, Bias, BufferId, OffsetRangeExt, Point, ToOffset as _};
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>>,
}
@@ -36,7 +31,6 @@ 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,
@@ -56,18 +50,11 @@ 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,
}
@@ -81,10 +68,6 @@ 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.
@@ -92,8 +75,6 @@ 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)]
@@ -107,7 +88,6 @@ struct PendingHunk {
#[derive(Debug, Clone)]
pub struct DiffHunkSummary {
buffer_range: Range<Anchor>,
diff_base_byte_range: Range<usize>,
}
impl sum_tree::Item for InternalDiffHunk {
@@ -116,7 +96,6 @@ impl sum_tree::Item for InternalDiffHunk {
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
DiffHunkSummary {
buffer_range: self.buffer_range.clone(),
diff_base_byte_range: self.diff_base_byte_range.clone(),
}
}
}
@@ -127,7 +106,6 @@ impl sum_tree::Item for PendingHunk {
fn summary(&self, _cx: &text::BufferSnapshot) -> Self::Summary {
DiffHunkSummary {
buffer_range: self.buffer_range.clone(),
diff_base_byte_range: self.diff_base_byte_range.clone(),
}
}
}
@@ -138,7 +116,6 @@ impl sum_tree::Summary for DiffHunkSummary {
fn zero(_cx: Self::Context<'_>) -> Self {
DiffHunkSummary {
buffer_range: Anchor::MIN..Anchor::MIN,
diff_base_byte_range: 0..0,
}
}
@@ -148,15 +125,6 @@ impl sum_tree::Summary for DiffHunkSummary {
.start
.min(&other.buffer_range.start, buffer);
self.buffer_range.end = *self.buffer_range.end.max(&other.buffer_range.end, buffer);
self.diff_base_byte_range.start = self
.diff_base_byte_range
.start
.min(other.diff_base_byte_range.start);
self.diff_base_byte_range.end = self
.diff_base_byte_range
.end
.max(other.diff_base_byte_range.end);
}
}
@@ -227,13 +195,6 @@ 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()));
@@ -251,7 +212,7 @@ impl BufferDiffSnapshot {
.background_executor()
.spawn_labeled(*CALCULATE_DIFF_TASK, {
let buffer = buffer.clone();
async move { compute_hunks(base_text_pair, buffer, diff_options) }
async move { compute_hunks(base_text_pair, buffer) }
});
async move {
@@ -274,12 +235,6 @@ 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());
@@ -291,7 +246,7 @@ impl BufferDiffSnapshot {
inner: BufferDiffInner {
base_text: base_text_snapshot,
pending_hunks: SumTree::new(&buffer),
hunks: compute_hunks(base_text_pair, buffer, diff_options),
hunks: compute_hunks(base_text_pair, buffer),
base_text_exists,
},
secondary_diff: None,
@@ -350,54 +305,6 @@ impl BufferDiffSnapshot {
let (new_id, new_empty) = (right.remote_id(), right.is_empty());
new_id == old_id || (new_empty && old_empty)
}
pub fn row_to_base_text_row(&self, row: BufferRow, buffer: &text::BufferSnapshot) -> u32 {
// TODO(split-diff) expose a parameter to reuse a cursor to avoid repeatedly seeking from the start
// Find the last hunk that starts before this position.
let mut cursor = self.inner.hunks.cursor::<DiffHunkSummary>(buffer);
let position = buffer.anchor_before(Point::new(row, 0));
cursor.seek(&position, Bias::Left);
if cursor
.item()
.is_none_or(|hunk| hunk.buffer_range.start.cmp(&position, buffer).is_gt())
{
cursor.prev();
}
let unclipped_point = if let Some(hunk) = cursor.item()
&& hunk.buffer_range.start.cmp(&position, buffer).is_le()
{
let mut unclipped_point = cursor
.end()
.diff_base_byte_range
.end
.to_point(self.base_text());
if position.cmp(&cursor.end().buffer_range.end, buffer).is_ge() {
unclipped_point +=
Point::new(row, 0) - cursor.end().buffer_range.end.to_point(buffer);
}
// Move the cursor so that at the next step we can clip with the start of the next hunk.
cursor.next();
unclipped_point
} else {
// Position is before the added region for the first hunk.
debug_assert!(self.inner.hunks.first().is_none_or(|first_hunk| {
position.cmp(&first_hunk.buffer_range.start, buffer).is_le()
}));
Point::new(row, 0)
};
let max_point = if let Some(next_hunk) = cursor.item() {
next_hunk
.diff_base_byte_range
.start
.to_point(self.base_text())
} else {
self.base_text().max_point()
};
unclipped_point.min(max_point).row
}
}
impl BufferDiffInner {
@@ -634,15 +541,11 @@ impl BufferDiffInner {
[
(
&hunk.buffer_range.start,
(
hunk.buffer_range.start,
hunk.diff_base_byte_range.start,
hunk,
),
(hunk.buffer_range.start, hunk.diff_base_byte_range.start),
),
(
&hunk.buffer_range.end,
(hunk.buffer_range.end, hunk.diff_base_byte_range.end, hunk),
(hunk.buffer_range.end, hunk.diff_base_byte_range.end),
),
]
});
@@ -661,11 +564,8 @@ 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, 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();
let (start_point, (start_anchor, start_base)) = summaries.next()?;
let (mut end_point, (mut end_anchor, end_base)) = summaries.next()?;
if !start_anchor.is_valid(buffer) {
continue;
@@ -735,8 +635,6 @@ 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,
});
}
@@ -768,8 +666,6 @@ 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(),
})
})
}
@@ -838,36 +734,9 @@ 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);
@@ -893,8 +762,6 @@ 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,
);
@@ -910,7 +777,6 @@ fn compute_hunks(
&diff_base_rope,
&buffer,
&mut divergence,
diff_options.as_ref(),
);
tree.push(hunk, &buffer);
}
@@ -920,8 +786,6 @@ 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,
);
@@ -936,7 +800,6 @@ 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);
@@ -1001,49 +864,9 @@ 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,
}
}
@@ -1123,7 +946,6 @@ impl BufferDiff {
if self.secondary_diff.is_some() {
self.inner.pending_hunks = SumTree::from_summary(DiffHunkSummary {
buffer_range: Anchor::min_min_range_for_buffer(self.buffer_id),
diff_base_byte_range: 0..0,
});
cx.emit(BufferDiffEvent::DiffChanged {
changed_range: Some(Anchor::min_max_range_for_buffer(self.buffer_id)),
@@ -1413,7 +1235,7 @@ impl DiffHunk {
pub fn is_created_file(&self) -> bool {
self.diff_base_byte_range == (0..0)
&& self.buffer_range.start.is_min()
&& self.buffer_range.end.is_max()
&& self.buffer_range.end.is_min()
}
pub fn status(&self) -> DiffHunkStatus {
@@ -2418,62 +2240,4 @@ mod tests {
hunks = found_hunks;
}
}
#[gpui::test]
async fn test_row_to_base_text_row(cx: &mut TestAppContext) {
let base_text = "
zero
one
two
three
four
five
six
seven
eight
"
.unindent();
let buffer_text = "
zero
ONE
two
NINE
five
seven
"
.unindent();
// zero
// - one
// + ONE
// two
// - three
// - four
// + NINE
// five
// - six
// seven
// + eight
let buffer = Buffer::new(ReplicaId::LOCAL, BufferId::new(1).unwrap(), buffer_text);
let buffer_snapshot = buffer.snapshot();
let diff = BufferDiffSnapshot::new_sync(buffer_snapshot.clone(), base_text, cx);
let expected_results = [
// don't format me
(0, 0),
(1, 2),
(2, 2),
(3, 5),
(4, 5),
(5, 7),
(6, 9),
];
for (buffer_row, expected) in expected_results {
assert_eq!(
diff.row_to_base_text_row(buffer_row, &buffer_snapshot),
expected,
"{buffer_row}"
);
}
}
}

View File

@@ -524,16 +524,6 @@ 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
}

View File

@@ -206,16 +206,11 @@ pub struct AcceptEditPredictionBody {
pub request_id: String,
}
#[derive(Debug, Clone, Deserialize)]
#[derive(Debug, Clone, Serialize, 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,

View File

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

View File

@@ -541,7 +541,7 @@ impl Item for ChannelView {
})
}
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}

View File

@@ -37,7 +37,7 @@ use ui::{
};
use util::{ResultExt, TryFutureExt, maybe};
use workspace::{
CopyRoomId, Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
Deafen, LeaveCall, Mute, OpenChannelNotes, ScreenShare, ShareProject, Workspace,
dock::{DockPosition, Panel, PanelEvent},
notifications::{DetachAndPromptErr, NotifyResultExt},
};
@@ -128,32 +128,6 @@ 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(&"Theres no active call; join one first.", cx);
}
});
workspace.register_action(|workspace, _: &ShareProject, window, cx| {
let project = workspace.project().clone();
println!("{project:?}");

View File

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

View File

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

View File

@@ -366,7 +366,7 @@ impl DebugAdapter for GoDebugAdapter {
dap::DebugRequest::Attach(attach_config) => {
json!({
"request": "attach",
"mode": "local",
"mode": "debug",
"processId": attach_config.process_id,
})
}

View File

@@ -998,11 +998,7 @@ impl Item for DapLogView {
None
}
fn as_searchable(
&self,
handle: &Entity<Self>,
_: &App,
) -> Option<Box<dyn SearchableItemHandle>> {
fn as_searchable(&self, handle: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(handle.clone()))
}
}
@@ -1017,13 +1013,11 @@ impl SearchableItem for DapLogView {
fn update_matches(
&mut self,
matches: &[Self::Match],
active_match_index: Option<usize>,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.editor.update(cx, |e, cx| {
e.update_matches(matches, active_match_index, window, cx)
})
self.editor
.update(cx, |e, cx| e.update_matches(matches, window, cx))
}
fn query_suggestion(&mut self, window: &mut Window, cx: &mut Context<Self>) -> String {

View File

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

View File

@@ -18,14 +18,14 @@ use gpui::{
use language::{Anchor, Buffer, CharScopeContext, CodeLabel, TextBufferSnapshot, ToOffset};
use menu::{Confirm, SelectNext, SelectPrevious};
use project::{
CompletionDisplayOptions, CompletionResponse,
Completion, CompletionDisplayOptions, CompletionResponse,
debugger::session::{CompletionsQuery, OutputToken, Session},
lsp_store::CompletionDocumentation,
search_history::{SearchHistory, SearchHistoryCursor},
};
use settings::Settings;
use std::fmt::Write;
use std::{ops::Range, rc::Rc, usize};
use std::{cell::RefCell, ops::Range, rc::Rc, usize};
use theme::{Theme, ThemeSettings};
use ui::{ContextMenu, Divider, PopoverMenu, SplitButton, Tooltip, prelude::*};
use util::ResultExt;
@@ -252,11 +252,10 @@ impl Console {
let start_offset = range.start;
let range = buffer.anchor_after(MultiBufferOffset(range.start))
..buffer.anchor_before(MultiBufferOffset(range.end));
let color_fn = color_fetcher(color);
console.highlight_background_key::<ConsoleAnsiHighlight>(
start_offset,
&[range],
move |_, theme| color_fn(theme),
color_fetcher(color),
cx,
);
}
@@ -554,12 +553,24 @@ 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();
@@ -570,6 +581,9 @@ impl CompletionProvider for ConsoleQueryBarCompletionProvider {
};
let snapshot = buffer.read(cx).snapshot();
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
return false;
}
let classifier = snapshot
.char_classifier_at(position)

View File

@@ -428,7 +428,7 @@ impl Item for StackTraceView {
}
}
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}

View File

@@ -890,7 +890,7 @@ impl Item for ProjectDiagnosticsEditor {
}
}
fn as_searchable(&self, _: &Entity<Self>, _: &App) -> Option<Box<dyn SearchableItemHandle>> {
fn as_searchable(&self, _: &Entity<Self>) -> Option<Box<dyn SearchableItemHandle>> {
Some(Box::new(self.editor.clone()))
}

View File

@@ -32,8 +32,6 @@ 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

View File

@@ -1,7 +1,3 @@
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;
@@ -44,7 +40,8 @@ use workspace::{
notifications::NotificationId,
};
use zed_actions::OpenBrowser;
use zeta::{RateCompletions, SweepFeatureFlag, Zeta2FeatureFlag};
use zeta::RateCompletions;
use zeta::{SweepFeatureFlag, Zeta2FeatureFlag};
actions!(
edit_prediction,
@@ -316,10 +313,6 @@ 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,
@@ -367,24 +360,19 @@ 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_some(indicator_color, |this, color| {
this.indicator(Indicator::dot().color(color))
.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),
))
.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| {
@@ -549,23 +537,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 providers: Vec<_> = available_providers
let other_providers: Vec<_> = available_providers
.into_iter()
.filter(|p| *p != EditPredictionProvider::None)
.filter(|p| *p != current_provider && *p != EditPredictionProvider::None)
.collect();
if !providers.is_empty() {
menu = menu.separator().header("Providers");
if !other_providers.is_empty() {
menu = menu.separator().header("Switch Providers");
for provider in providers {
let is_current = provider == current_provider;
for provider in other_providers {
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,
@@ -575,77 +563,46 @@ impl EditPredictionButton {
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::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::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 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(
let entry = ContextMenuEntry::new("Sweep")
.when(!has_api_token, |this| {
this.disabled(true).documentation_aside(
DocumentationSide::Left,
DocumentationEdge::Bottom,
|_| {
Label::new("Click to configure your Sweep API token")
.into_any_element()
},
|_| Label::new(USE_SWEEP_API_TOKEN_CALLOUT).into_any_element(),
)
};
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 {
})
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}
});
});
menu.item(entry)
}
EditPredictionProvider::Experimental(
EXPERIMENTAL_ZETA2_EDIT_PREDICTION_PROVIDER_NAME,
) => menu.item(
ContextMenuEntry::new("Zeta2")
.toggleable(IconPosition::Start, is_current)
.handler(move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
),
) => menu.entry("Zeta2", None, move |_, cx| {
set_completion_provider(fs.clone(), cx, provider);
}),
EditPredictionProvider::None | EditPredictionProvider::Experimental(_) => {
continue;
}
@@ -1121,8 +1078,8 @@ impl EditPredictionButton {
menu = menu
.custom_row(move |_window, cx| {
let description = indoc! {
"You get 2,000 accepted suggestions at every keystroke for free, \
powered by Zeta, our open-source, open-data model"
"Sign in for 2,000 worth of accepted suggestions at every keystroke, \
powered by Zeta, our open-source, open-data model."
};
v_flex()
@@ -1375,28 +1332,21 @@ fn render_zeta_tab_animation(cx: &App) -> impl IntoElement {
.child("tab")
.with_animation(
ElementId::Integer(n),
Animation::new(Duration::from_secs(3)).repeat(),
Animation::new(Duration::from_secs(4)).repeat(),
move |tab, delta| {
let n_f32 = n as f32;
let offset = if inverted {
0.2 * (4.0 - n_f32)
let delta = if inverted {
(delta - 0.15 * (5.0 - n_f32)) / 0.7
} else {
0.2 * n_f32
(delta - 0.15 * n_f32) / 0.7
};
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
};
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 eased = ease_in_out(pulse);
let opacity = 0.1 + 0.5 * eased;
tab.text_color(text_color.opacity(opacity))
tab.text_color(text_color.opacity(delta))
},
),
)

View File

@@ -1,84 +0,0 @@
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);
})),
),
)
}
}

View File

@@ -41,7 +41,6 @@ dap.workspace = true
db.workspace = true
buffer_diff.workspace = true
emojis.workspace = true
feature_flags.workspace = true
file_icons.workspace = true
futures.workspace = true
fuzzy.workspace = true

View File

@@ -43,7 +43,7 @@ impl Editor {
.collect_array()
};
let bracket_matches_by_accent = self.visible_excerpts(false, cx).into_iter().fold(
let bracket_matches_by_accent = self.visible_excerpts(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, EditorMode, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
DisplayPoint, EditorSnapshot, MoveToBeginning, MoveToEnd, MoveUp,
display_map::{DisplayRow, ToDisplayPoint},
editor_tests::init_test,
test::{
@@ -276,40 +276,6 @@ 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| {
@@ -333,19 +299,6 @@ where
&bracket_colors_markup(&mut cx),
"All markdown brackets should be colored based on their depth"
);
cx.set_state(indoc! {r#"ˇ{{}}"#});
cx.executor().advance_clock(Duration::from_millis(100));
cx.executor().run_until_parked();
assert_eq!(
r#"«1{«2{}2»}1»
1 hsla(207.80, 16.20%, 69.19%, 1.00)
2 hsla(29.00, 54.00%, 65.88%, 1.00)
"#,
&bracket_colors_markup(&mut cx),
"All markdown brackets should be colored based on their depth, again"
);
}
#[gpui::test]

View File

@@ -835,38 +835,36 @@ impl CompletionsMenu {
FontWeight::BOLD.into(),
)
}),
styled_runs_for_code_label(
&completion.label,
&style.syntax,
&style.local_player,
)
.map(|(range, mut highlight)| {
// Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches.
highlight.font_weight = None;
if completion
.source
.lsp_completion(false)
.and_then(|lsp_completion| {
match (lsp_completion.deprecated, &lsp_completion.tags) {
(Some(true), _) => Some(true),
(_, Some(tags)) => {
Some(tags.contains(&CompletionItemTag::DEPRECATED))
styled_runs_for_code_label(&completion.label, &style.syntax).map(
|(range, mut highlight)| {
// Ignore font weight for syntax highlighting, as we'll use it
// for fuzzy matches.
highlight.font_weight = None;
if completion
.source
.lsp_completion(false)
.and_then(|lsp_completion| {
match (lsp_completion.deprecated, &lsp_completion.tags)
{
(Some(true), _) => Some(true),
(_, Some(tags)) => Some(
tags.contains(&CompletionItemTag::DEPRECATED),
),
_ => None,
}
_ => None,
}
})
.unwrap_or(false)
{
highlight.strikethrough = Some(StrikethroughStyle {
thickness: 1.0.into(),
..Default::default()
});
highlight.color = Some(cx.theme().colors().text_muted);
}
})
.unwrap_or(false)
{
highlight.strikethrough = Some(StrikethroughStyle {
thickness: 1.0.into(),
..Default::default()
});
highlight.color = Some(cx.theme().colors().text_muted);
}
(range, highlight)
}),
(range, highlight)
},
),
);
let completion_label = StyledText::new(completion.label.text.clone())

View File

@@ -181,8 +181,6 @@ 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,

View File

@@ -1056,7 +1056,6 @@ impl Iterator for WrapRows<'_> {
RowInfo {
buffer_id: None,
buffer_row: None,
base_text_row: None,
multibuffer_row: None,
diff_status,
expand_info: None,

View File

@@ -36,7 +36,6 @@ mod persistence;
mod rust_analyzer_ext;
pub mod scroll;
mod selections_collection;
mod split;
pub mod tasks;
#[cfg(test)]
@@ -70,7 +69,6 @@ pub use multi_buffer::{
MultiBufferOffset, MultiBufferOffsetUtf16, MultiBufferSnapshot, PathKey, RowInfo, ToOffset,
ToPoint,
};
pub use split::SplittableEditor;
pub use text::Bias;
use ::git::{
@@ -146,8 +144,8 @@ use persistence::DB;
use project::{
BreakpointWithPosition, CodeAction, Completion, CompletionDisplayOptions, CompletionIntent,
CompletionResponse, CompletionSource, DisableAiSettings, DocumentHighlight, InlayHint, InlayId,
InvalidationStrategy, Location, LocationLink, LspAction, PrepareRenameResponse, Project,
ProjectItem, ProjectPath, ProjectTransaction, TaskSourceKind,
InvalidationStrategy, Location, LocationLink, PrepareRenameResponse, Project, ProjectItem,
ProjectPath, ProjectTransaction, TaskSourceKind,
debugger::{
breakpoint_store::{
Breakpoint, BreakpointEditAction, BreakpointSessionState, BreakpointState,
@@ -284,9 +282,6 @@ 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,
@@ -310,7 +305,6 @@ enum DisplayDiffHunk {
display_row_range: Range<DisplayRow>,
multi_buffer_range: Range<Anchor>,
status: DiffHunkStatus,
word_diffs: Vec<Range<MultiBufferOffset>>,
},
}
@@ -726,10 +720,7 @@ impl EditorActionId {
// type GetFieldEditorTheme = dyn Fn(&theme::Theme) -> theme::FieldEditor;
// type OverrideTextStyle = dyn Fn(&EditorStyle) -> Option<HighlightStyle>;
type BackgroundHighlight = (
Arc<dyn Fn(&usize, &Theme) -> Hsla + Send + Sync>,
Arc<[Range<Anchor>]>,
);
type BackgroundHighlight = (fn(&Theme) -> Hsla, Arc<[Range<Anchor>]>);
type GutterHighlight = (fn(&App) -> Hsla, Vec<Range<Anchor>>);
#[derive(Default)]
@@ -1175,7 +1166,6 @@ pub struct Editor {
gutter_breakpoint_indicator: (Option<PhantomBreakpointIndicator>, Option<Task<()>>),
hovered_diff_hunk_row: Option<DisplayRow>,
pull_diagnostics_task: Task<()>,
pull_diagnostics_background_task: Task<()>,
in_project_search: bool,
previous_search_ranges: Option<Arc<[Range<Anchor>]>>,
breadcrumb_header: Option<String>,
@@ -1208,7 +1198,6 @@ pub struct Editor {
applicable_language_settings: HashMap<Option<LanguageName>, LanguageSettings>,
accent_overrides: Vec<SharedString>,
fetched_tree_sitter_chunks: HashMap<ExcerptId, HashSet<Range<BufferRow>>>,
use_base_text_line_numbers: bool,
}
fn debounce_value(debounce_ms: u64) -> Option<Duration> {
@@ -1648,7 +1637,7 @@ pub(crate) struct FocusedBlock {
focus_handle: WeakFocusHandle,
}
#[derive(Clone, Debug)]
#[derive(Clone)]
enum JumpData {
MultiBufferRow {
row: MultiBufferRow,
@@ -2320,7 +2309,6 @@ impl Editor {
.unwrap_or_default(),
tasks_update_task: None,
pull_diagnostics_task: Task::ready(()),
pull_diagnostics_background_task: Task::ready(()),
colors: None,
refresh_colors_task: Task::ready(()),
inlay_hints: None,
@@ -2356,7 +2344,6 @@ impl Editor {
applicable_language_settings: HashMap::default(),
accent_overrides: Vec::new(),
fetched_tree_sitter_chunks: HashMap::default(),
use_base_text_line_numbers: false,
};
if is_minimap {
@@ -2497,6 +2484,7 @@ impl Editor {
if let Some(buffer) = multi_buffer.read(cx).as_singleton() {
editor.register_buffer(buffer.read(cx).remote_id(), cx);
}
editor.update_lsp_data(None, window, cx);
editor.report_editor_event(ReportEditorEvent::EditorOpened, None, cx);
}
@@ -5316,10 +5304,12 @@ impl Editor {
pub fn visible_excerpts(
&self,
lsp_related_only: bool,
cx: &mut Context<Editor>,
) -> HashMap<ExcerptId, (Entity<Buffer>, clock::Global, Range<usize>)> {
let project = self.project().cloned();
let Some(project) = self.project() else {
return HashMap::default();
};
let project = project.read(cx);
let multi_buffer = self.buffer().read(cx);
let multi_buffer_snapshot = multi_buffer.snapshot(cx);
let multi_buffer_visible_start = self
@@ -5337,18 +5327,6 @@ 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
@@ -5513,22 +5491,6 @@ impl Editor {
};
let buffer_snapshot = buffer.read(cx).snapshot();
let menu_is_open = matches!(
self.context_menu.borrow().as_ref(),
Some(CodeContextMenu::Completions(_))
);
let language = buffer_snapshot
.language_at(buffer_position.text_anchor)
.map(|language| language.name());
let language_settings = language_settings(language.clone(), buffer_snapshot.file(), cx);
let completion_settings = language_settings.completions.clone();
if !menu_is_open && trigger.is_some() && !language_settings.show_completions_on_input {
return;
}
let query: Option<Arc<String>> =
Self::completion_query(&multibuffer_snapshot, buffer_position)
.map(|query| query.into());
@@ -5537,8 +5499,14 @@ impl Editor {
// Hide the current completions menu when query is empty. Without this, cached
// completions from before the trigger char may be reused (#32774).
if query.is_none() && menu_is_open {
self.hide_context_menu(window, cx);
if query.is_none() {
let menu_is_open = matches!(
self.context_menu.borrow().as_ref(),
Some(CodeContextMenu::Completions(_))
);
if menu_is_open {
self.hide_context_menu(window, cx);
}
}
let mut ignore_word_threshold = false;
@@ -5627,6 +5595,14 @@ impl Editor {
(buffer_position..buffer_position, None)
};
let language = buffer_snapshot
.language_at(buffer_position)
.map(|language| language.name());
let completion_settings = language_settings(language.clone(), buffer_snapshot.file(), cx)
.completions
.clone();
let show_completion_documentation = buffer_snapshot
.settings_at(buffer_position, cx)
.show_completion_documentation;
@@ -5657,6 +5633,7 @@ impl Editor {
position.text_anchor,
trigger,
trigger_in_words,
completions_source.is_some(),
cx,
)
})
@@ -6156,43 +6133,9 @@ 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.clone(),
buffer_handle,
completions_menu.completions.clone(),
candidate_id,
true,
@@ -6206,29 +6149,8 @@ impl Editor {
self.show_signature_help(&ShowSignatureHelp, window, cx);
}
Some(cx.spawn_in(window, async move |editor, cx| {
Some(cx.foreground_executor().spawn(async move {
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(())
}))
}
@@ -6613,7 +6535,7 @@ impl Editor {
editor.update(cx, |editor, cx| {
editor.highlight_background::<Self>(
&ranges_to_highlight,
|_, theme| theme.colors().editor_highlighted_line_background,
|theme| theme.colors().editor_highlighted_line_background,
cx,
);
});
@@ -6814,9 +6736,6 @@ impl Editor {
return;
};
if self.blame.is_none() {
self.start_git_blame(true, window, cx);
}
let Some(blame) = self.blame.as_ref() else {
return;
};
@@ -7015,12 +6934,12 @@ impl Editor {
this.highlight_background::<DocumentHighlightRead>(
&read_ranges,
|_, theme| theme.colors().editor_document_highlight_read_background,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
this.highlight_background::<DocumentHighlightWrite>(
&write_ranges,
|_, theme| theme.colors().editor_document_highlight_write_background,
|theme| theme.colors().editor_document_highlight_write_background,
cx,
);
cx.notify();
@@ -7128,7 +7047,7 @@ impl Editor {
if !match_ranges.is_empty() {
editor.highlight_background::<SelectedTextHighlight>(
&match_ranges,
|_, theme| theme.colors().editor_document_highlight_bracket_background,
|theme| theme.colors().editor_document_highlight_bracket_background,
cx,
)
}
@@ -17015,9 +16934,7 @@ impl Editor {
})
.collect();
let Some(workspace) = self.workspace() else {
return Task::ready(Ok(Navigated::No));
};
let workspace = self.workspace();
cx.spawn_in(window, async move |editor, cx| {
let locations: Vec<Location> = future::join_all(definitions)
@@ -17043,6 +16960,10 @@ impl Editor {
}
if num_locations > 1 {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
let tab_kind = match kind {
Some(GotoDefinitionKind::Implementation) => "Implementations",
Some(GotoDefinitionKind::Symbol) | None => "Definitions",
@@ -17074,14 +16995,11 @@ impl Editor {
let opened = workspace
.update_in(cx, |workspace, window, cx| {
let allow_preview = PreviewTabsSettings::get_global(cx)
.enable_preview_multibuffer_from_code_navigation;
Self::open_locations_in_multibuffer(
workspace,
locations,
title,
split,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
@@ -17098,9 +17016,10 @@ impl Editor {
Ok(Navigated::Yes)
}
Some(Either::Right(path)) => {
// TODO(andrew): respect preview tab settings
// `enable_keep_preview_on_code_navigation` and
// `enable_preview_file_from_code_navigation`
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
workspace
.update_in(cx, |workspace, window, cx| {
workspace.open_resolved_path(path, window, cx)
@@ -17111,6 +17030,10 @@ impl Editor {
None => Ok(Navigated::No),
}
} else {
let Some(workspace) = workspace else {
return Ok(Navigated::No);
};
let (target_buffer, target_ranges) = locations.into_iter().next().unwrap();
let target_range = target_ranges.first().unwrap().clone();
@@ -17134,19 +17057,11 @@ impl Editor {
workspace.active_pane().clone()
};
let preview_tabs_settings = PreviewTabsSettings::get_global(cx);
let keep_old_preview = preview_tabs_settings
.enable_keep_preview_on_code_navigation;
let allow_new_preview = preview_tabs_settings
.enable_preview_file_from_code_navigation;
workspace.open_project_item(
pane,
target_buffer.clone(),
true,
true,
keep_old_preview,
allow_new_preview,
window,
cx,
)
@@ -17423,14 +17338,11 @@ impl Editor {
} else {
format!("References to {target}")
};
let allow_preview = PreviewTabsSettings::get_global(cx)
.enable_preview_multibuffer_from_code_navigation;
Self::open_locations_in_multibuffer(
workspace,
locations,
title,
false,
allow_preview,
MultibufferSelectionMode::First,
window,
cx,
@@ -17446,7 +17358,6 @@ impl Editor {
locations: std::collections::HashMap<Entity<Buffer>, Vec<Range<Point>>>,
title: String,
split: bool,
allow_preview: bool,
multibuffer_selection_mode: MultibufferSelectionMode,
window: &mut Window,
cx: &mut Context<Workspace>,
@@ -17494,7 +17405,6 @@ impl Editor {
.is_some_and(|it| *it == key)
})
});
let was_existing = existing.is_some();
let editor = existing.unwrap_or_else(|| {
cx.new(|cx| {
let mut editor = Editor::for_multibuffer(
@@ -17522,7 +17432,7 @@ impl Editor {
}
editor.highlight_background::<Self>(
&ranges,
|_, theme| theme.colors().editor_highlighted_line_background,
|theme| theme.colors().editor_highlighted_line_background,
cx,
);
}
@@ -17535,23 +17445,29 @@ impl Editor {
});
let item = Box::new(editor);
let item_id = item.item_id();
let pane = if split {
workspace.adjacent_pane(window, cx)
if split {
let pane = workspace.adjacent_pane(window, cx);
workspace.add_item(pane, item, None, true, true, window, cx);
} else if PreviewTabsSettings::get_global(cx).enable_preview_from_code_navigation {
let (preview_item_id, preview_item_idx) =
workspace.active_pane().read_with(cx, |pane, _| {
(pane.preview_item_id(), pane.preview_item_idx())
});
workspace.add_item_to_active_pane(item, preview_item_idx, true, window, cx);
if let Some(preview_item_id) = preview_item_id {
workspace.active_pane().update(cx, |pane, cx| {
pane.remove_item(preview_item_id, false, false, window, cx);
});
}
} else {
workspace.active_pane().clone()
};
let activate_pane = split;
let mut destination_index = None;
pane.update(cx, |pane, cx| {
if allow_preview && !was_existing {
destination_index = pane.replace_preview_item_id(item.item_id(), window, cx);
}
if was_existing && !allow_preview {
pane.unpreview_item_if_preview(item.item_id());
}
pane.add_item(item, activate_pane, true, destination_index, window, cx);
workspace.add_item_to_active_pane(item, None, true, window, cx);
}
workspace.active_pane().update(cx, |pane, cx| {
pane.set_preview_item_id(Some(item_id), cx);
});
}
@@ -18407,101 +18323,54 @@ impl Editor {
return None;
}
let project = self.project()?.downgrade();
let mut edited_buffer_ids = HashSet::default();
let mut edited_worktree_ids = HashSet::default();
let edited_buffers = match buffer_id {
Some(buffer_id) => {
let buffer = self.buffer().read(cx).buffer(buffer_id)?;
let worktree_id = buffer.read(cx).file().map(|f| f.worktree_id(cx))?;
edited_buffer_ids.insert(buffer.read(cx).remote_id());
edited_worktree_ids.insert(worktree_id);
vec![buffer]
}
None => self
.buffer()
.read(cx)
.all_buffers()
.into_iter()
.filter(|buffer| {
let buffer = buffer.read(cx);
match buffer.file().map(|f| f.worktree_id(cx)) {
Some(worktree_id) => {
edited_buffer_ids.insert(buffer.remote_id());
edited_worktree_ids.insert(worktree_id);
true
}
None => false,
}
})
.collect::<Vec<_>>(),
};
if edited_buffers.is_empty() {
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
let mut buffers = self.buffer.read(cx).all_buffers();
buffers.retain(|buffer| {
let buffer_id_to_retain = buffer.read(cx).remote_id();
buffer_id.is_none_or(|buffer_id| buffer_id == buffer_id_to_retain)
&& self.registered_buffers.contains_key(&buffer_id_to_retain)
});
if buffers.is_empty() {
self.pull_diagnostics_task = Task::ready(());
self.pull_diagnostics_background_task = Task::ready(());
return None;
}
let mut already_used_buffers = HashSet::default();
let related_open_buffers = self
.workspace
.as_ref()
.and_then(|(workspace, _)| workspace.upgrade())
.into_iter()
.flat_map(|workspace| workspace.read(cx).panes())
.flat_map(|pane| pane.read(cx).items_of_type::<Editor>())
.filter(|editor| editor != &cx.entity())
.flat_map(|editor| editor.read(cx).buffer().read(cx).all_buffers())
.filter(|buffer| {
let buffer = buffer.read(cx);
let buffer_id = buffer.remote_id();
if already_used_buffers.insert(buffer_id) {
if let Some(worktree_id) = buffer.file().map(|f| f.worktree_id(cx)) {
return !edited_buffer_ids.contains(&buffer_id)
&& !edited_worktree_ids.contains(&worktree_id);
}
}
false
})
.collect::<Vec<_>>();
self.pull_diagnostics_task = cx.spawn_in(window, async move |editor, cx| {
cx.background_executor().timer(debounce).await;
let debounce = Duration::from_millis(pull_diagnostics_settings.debounce_ms);
let make_spawn = |buffers: Vec<Entity<Buffer>>, delay: Duration| {
if buffers.is_empty() {
return Task::ready(());
}
let project_weak = project.clone();
cx.spawn_in(window, async move |_, cx| {
cx.background_executor().timer(delay).await;
let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
buffers
.into_iter()
.filter_map(|buffer| {
project_weak
.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.pull_diagnostics_for_buffer(buffer, cx)
})
let Ok(mut pull_diagnostics_tasks) = cx.update(|_, cx| {
buffers
.into_iter()
.filter_map(|buffer| {
project
.update(cx, |project, cx| {
project.lsp_store().update(cx, |lsp_store, cx| {
lsp_store.pull_diagnostics_for_buffer(buffer, cx)
})
.ok()
})
.collect::<FuturesUnordered<_>>()
}) else {
return;
};
})
.ok()
})
.collect::<FuturesUnordered<_>>()
}) else {
return;
};
while let Some(pull_task) = pull_diagnostics_tasks.next().await {
if let Err(e) = pull_task {
log::error!("Failed to update project diagnostics: {e:#}");
while let Some(pull_task) = pull_diagnostics_tasks.next().await {
match pull_task {
Ok(()) => {
if editor
.update_in(cx, |editor, window, cx| {
editor.update_diagnostics_state(window, cx);
})
.is_err()
{
return;
}
}
Err(e) => log::error!("Failed to update project diagnostics: {e:#}"),
}
})
};
self.pull_diagnostics_task = make_spawn(edited_buffers, debounce);
self.pull_diagnostics_background_task = make_spawn(related_open_buffers, debounce * 2);
}
});
Some(())
}
@@ -19335,10 +19204,6 @@ impl Editor {
self.display_map.read(cx).fold_placeholder.clone()
}
pub fn set_use_base_text_line_numbers(&mut self, show: bool, _cx: &mut Context<Self>) {
self.use_base_text_line_numbers = show;
}
pub fn set_expand_all_diff_hunks(&mut self, cx: &mut App) {
self.buffer.update(cx, |buffer, cx| {
buffer.set_all_diff_hunks_expanded(cx);
@@ -19574,10 +19439,6 @@ 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,
@@ -20789,7 +20650,6 @@ impl Editor {
locations,
format!("Selections for '{title}'"),
false,
false,
MultibufferSelectionMode::All,
window,
cx,
@@ -20992,7 +20852,7 @@ impl Editor {
pub fn set_search_within_ranges(&mut self, ranges: &[Range<Anchor>], cx: &mut Context<Self>) {
self.highlight_background::<SearchWithinRange>(
ranges,
|_, colors| colors.colors().editor_document_highlight_read_background,
|colors| colors.colors().editor_document_highlight_read_background,
cx,
)
}
@@ -21008,12 +20868,12 @@ impl Editor {
pub fn highlight_background<T: 'static>(
&mut self,
ranges: &[Range<Anchor>],
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
color_fetcher: fn(&Theme) -> Hsla,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::Type(TypeId::of::<T>()),
(Arc::new(color_fetcher), Arc::from(ranges)),
(color_fetcher, Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
@@ -21023,12 +20883,12 @@ impl Editor {
&mut self,
key: usize,
ranges: &[Range<Anchor>],
color_fetcher: impl Fn(&usize, &Theme) -> Hsla + Send + Sync + 'static,
color_fetcher: fn(&Theme) -> Hsla,
cx: &mut Context<Self>,
) {
self.background_highlights.insert(
HighlightKey::TypePlus(TypeId::of::<T>(), key),
(Arc::new(color_fetcher), Arc::from(ranges)),
(color_fetcher, Arc::from(ranges)),
);
self.scrollbar_marker_state.dirty = true;
cx.notify();
@@ -21253,6 +21113,7 @@ impl Editor {
) -> Vec<(Range<DisplayPoint>, Hsla)> {
let mut results = Vec::new();
for (color_fetcher, ranges) in self.background_highlights.values() {
let color = color_fetcher(theme);
let start_ix = match ranges.binary_search_by(|probe| {
let cmp = probe
.end
@@ -21265,7 +21126,7 @@ impl Editor {
}) {
Ok(i) | Err(i) => i,
};
for (index, range) in ranges[start_ix..].iter().enumerate() {
for range in &ranges[start_ix..] {
if range
.start
.cmp(&search_range.end, &display_snapshot.buffer_snapshot())
@@ -21274,7 +21135,6 @@ impl Editor {
break;
}
let color = color_fetcher(&(start_ix + index), theme);
let start = range.start.to_display_point(display_snapshot);
let end = range.end.to_display_point(display_snapshot);
results.push((start..end, color))
@@ -21994,64 +21854,43 @@ impl Editor {
};
for (buffer, (ranges, scroll_offset)) in new_selections_by_buffer {
let buffer_read = buffer.read(cx);
let (has_file, is_project_file) = if let Some(file) = buffer_read.file() {
(true, project::File::from_dyn(Some(file)).is_some())
} else {
(false, false)
};
// If project file is none workspace.open_project_item will fail to open the excerpt
// in a pre existing workspace item if one exists, because Buffer entity_id will be None
// so we check if there's a tab match in that case first
let editor = (!has_file || !is_project_file)
let editor = buffer
.read(cx)
.file()
.is_none()
.then(|| {
// Handle file-less buffers separately: those are not really the project items, so won't have a project path or entity id,
// so `workspace.open_project_item` will never find them, always opening a new editor.
// Instead, we try to activate the existing editor in the pane first.
let (editor, pane_item_index, pane_item_id) =
let (editor, pane_item_index) =
pane.read(cx).items().enumerate().find_map(|(i, item)| {
let editor = item.downcast::<Editor>()?;
let singleton_buffer =
editor.read(cx).buffer().read(cx).as_singleton()?;
if singleton_buffer == buffer {
Some((editor, i, item.item_id()))
Some((editor, i))
} else {
None
}
})?;
pane.update(cx, |pane, cx| {
pane.activate_item(pane_item_index, true, true, window, cx);
if !PreviewTabsSettings::get_global(cx)
.enable_preview_from_multibuffer
{
pane.unpreview_item_if_preview(pane_item_id);
}
pane.activate_item(pane_item_index, true, true, window, cx)
});
Some(editor)
})
.flatten()
.unwrap_or_else(|| {
let keep_old_preview = PreviewTabsSettings::get_global(cx)
.enable_keep_preview_on_code_navigation;
let allow_new_preview =
PreviewTabsSettings::get_global(cx).enable_preview_from_multibuffer;
workspace.open_project_item::<Self>(
pane.clone(),
buffer,
true,
true,
keep_old_preview,
allow_new_preview,
window,
cx,
)
});
editor.update(cx, |editor, cx| {
if has_file && !is_project_file {
editor.set_read_only(true);
}
let autoscroll = match scroll_offset {
Some(scroll_offset) => Autoscroll::top_relative(scroll_offset as usize),
None => Autoscroll::newest(),
@@ -22075,11 +21914,10 @@ impl Editor {
});
}
// Allow opening excerpts for buffers that either belong to the current project
// or represent synthetic/non-local files (e.g., git blobs). File-less buffers
// are also supported so tests and other in-memory views keep working.
// For now, don't allow opening excerpts in buffers that aren't backed by
// regular project files.
fn can_open_excerpts_in_file(file: Option<&Arc<dyn language::File>>) -> bool {
file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some() || !file.is_local())
file.is_none_or(|file| project::File::from_dyn(Some(file)).is_some())
}
fn marked_text_ranges(&self, cx: &App) -> Option<Vec<Range<MultiBufferOffsetUtf16>>> {
@@ -22678,10 +22516,6 @@ impl Editor {
}
}
pub fn last_gutter_dimensions(&self) -> &GutterDimensions {
&self.gutter_dimensions
}
pub fn wait_for_diff_to_load(&self) -> Option<Shared<Task<()>>> {
self.load_diff_task.clone()
}
@@ -22750,7 +22584,7 @@ impl Editor {
if self.ignore_lsp_data() {
return;
}
for (_, (visible_buffer, _, _)) in self.visible_excerpts(true, cx) {
for (_, (visible_buffer, _, _)) in self.visible_excerpts(cx) {
self.register_buffer(visible_buffer.read(cx).remote_id(), cx);
}
}
@@ -23571,6 +23405,7 @@ pub trait CompletionProvider {
position: language::Anchor,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool;
@@ -23949,6 +23784,7 @@ impl CompletionProvider for Entity<Project> {
position: language::Anchor,
text: &str,
trigger_in_words: bool,
menu_is_open: bool,
cx: &mut Context<Editor>,
) -> bool {
let mut chars = text.chars();
@@ -23963,6 +23799,9 @@ impl CompletionProvider for Entity<Project> {
let buffer = buffer.read(cx);
let snapshot = buffer.snapshot();
if !menu_is_open && !snapshot.settings_at(position, cx).show_completions_on_input {
return false;
}
let classifier = snapshot
.char_classifier_at(position)
.scope_context(Some(CharScopeContext::Completion));
@@ -24269,12 +24108,10 @@ 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,
@@ -25077,7 +24914,6 @@ pub fn diagnostic_style(severity: lsp::DiagnosticSeverity, colors: &StatusColors
pub fn styled_runs_for_code_label<'a>(
label: &'a CodeLabel,
syntax_theme: &'a theme::SyntaxTheme,
local_player: &'a theme::PlayerColor,
) -> impl 'a + Iterator<Item = (Range<usize>, HighlightStyle)> {
let fade_out = HighlightStyle {
fade_out: Some(0.35),
@@ -25090,17 +24926,7 @@ pub fn styled_runs_for_code_label<'a>(
.iter()
.enumerate()
.flat_map(move |(ix, (range, highlight_id))| {
let style = if *highlight_id == language::HighlightId::TABSTOP_INSERT_ID {
HighlightStyle {
color: Some(local_player.cursor),
..Default::default()
}
} else if *highlight_id == language::HighlightId::TABSTOP_REPLACE_ID {
HighlightStyle {
background_color: Some(local_player.selection),
..Default::default()
}
} else if let Some(style) = highlight_id.style(syntax_theme) {
let style = if let Some(style) = highlight_id.style(syntax_theme) {
style
} else {
return Default::default();

View File

@@ -35,9 +35,7 @@ use language_settings::Formatter;
use languages::markdown_lang;
use languages::rust_lang;
use lsp::CompletionParams;
use multi_buffer::{
IndentGuide, MultiBufferFilterMode, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey,
};
use multi_buffer::{IndentGuide, MultiBufferOffset, MultiBufferOffsetUtf16, PathKey};
use parking_lot::Mutex;
use pretty_assertions::{assert_eq, assert_ne};
use project::{
@@ -14755,180 +14753,6 @@ 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(&params, ""),
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(&params, ""),
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, |_| {});
@@ -16978,7 +16802,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
anchor_range(Point::new(6, 3)..Point::new(6, 5)),
anchor_range(Point::new(8, 4)..Point::new(8, 6)),
],
|_, _| Hsla::red(),
|_| Hsla::red(),
cx,
);
editor.highlight_background::<Type2>(
@@ -16988,7 +16812,7 @@ fn test_highlighted_ranges(cx: &mut TestAppContext) {
anchor_range(Point::new(7, 4)..Point::new(7, 7)),
anchor_range(Point::new(9, 5)..Point::new(9, 8)),
],
|_, _| Hsla::green(),
|_| Hsla::green(),
cx,
);
@@ -23973,7 +23797,7 @@ async fn test_rename_with_duplicate_edits(cx: &mut TestAppContext) {
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|_, theme| theme.colors().editor_document_highlight_read_background,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -24051,7 +23875,7 @@ async fn test_rename_without_prepare(cx: &mut TestAppContext) {
let highlight_range = highlight_range.to_anchors(&editor.buffer().read(cx).snapshot(cx));
editor.highlight_background::<DocumentHighlightRead>(
&[highlight_range],
|_, theme| theme.colors().editor_document_highlight_read_background,
|theme| theme.colors().editor_document_highlight_read_background,
cx,
);
});
@@ -26589,7 +26413,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
}
});
let ensure_result_id = |expected: Option<SharedString>, cx: &mut TestAppContext| {
let ensure_result_id = |expected: Option<String>, cx: &mut TestAppContext| {
project.update(cx, |project, cx| {
let buffer_id = editor
.read(cx)
@@ -26602,7 +26426,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
let buffer_result_id = project
.lsp_store()
.read(cx)
.result_id_for_buffer_pull(server_id, buffer_id, &None, cx);
.result_id(server_id, buffer_id, cx);
assert_eq!(expected, buffer_result_id);
});
};
@@ -26619,7 +26443,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
.next()
.await
.expect("should have sent the first diagnostics pull request");
ensure_result_id(Some(SharedString::new("1")), cx);
ensure_result_id(Some("1".to_string()), cx);
// Editing should trigger diagnostics
editor.update_in(cx, |editor, window, cx| {
@@ -26632,7 +26456,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
2,
"Editing should trigger diagnostic request"
);
ensure_result_id(Some(SharedString::new("2")), cx);
ensure_result_id(Some("2".to_string()), cx);
// Moving cursor should not trigger diagnostic request
editor.update_in(cx, |editor, window, cx| {
@@ -26647,7 +26471,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
2,
"Cursor movement should not trigger diagnostic request"
);
ensure_result_id(Some(SharedString::new("2")), cx);
ensure_result_id(Some("2".to_string()), cx);
// Multiple rapid edits should be debounced
for _ in 0..5 {
editor.update_in(cx, |editor, window, cx| {
@@ -26662,7 +26486,7 @@ async fn test_pulling_diagnostics(cx: &mut TestAppContext) {
final_requests <= 4,
"Multiple rapid edits should be debounced (got {final_requests} requests)",
);
ensure_result_id(Some(SharedString::new(final_requests.to_string())), cx);
ensure_result_id(Some(final_requests.to_string()), cx);
}
#[gpui::test]
@@ -27299,7 +27123,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,
);
});
@@ -28375,208 +28199,6 @@ async fn test_multibuffer_selections_with_folding(cx: &mut TestAppContext) {
"});
}
#[gpui::test]
async fn test_filtered_editor_pair(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let mut leader_cx = EditorTestContext::new(cx).await;
let diff_base = indoc!(
r#"
one
two
three
four
five
six
"#
);
let initial_state = indoc!(
r#"
ˇone
two
THREE
four
five
six
"#
);
leader_cx.set_state(initial_state);
leader_cx.set_head_text(&diff_base);
leader_cx.run_until_parked();
let follower = leader_cx.update_multibuffer(|leader, cx| {
leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
leader.set_all_diff_hunks_expanded(cx);
leader.get_or_create_follower(cx)
});
follower.update(cx, |follower, cx| {
follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
follower.set_all_diff_hunks_expanded(cx);
});
let follower_editor =
leader_cx.new_window_entity(|window, cx| build_editor(follower, window, cx));
// leader_cx.window.focus(&follower_editor.focus_handle(cx));
let mut follower_cx = EditorTestContext::for_editor_in(follower_editor, &mut leader_cx).await;
cx.run_until_parked();
leader_cx.assert_editor_state(initial_state);
follower_cx.assert_editor_state(indoc! {
r#"
ˇone
two
three
four
five
six
"#
});
follower_cx.editor(|editor, _window, cx| {
assert!(editor.read_only(cx));
});
leader_cx.update_editor(|editor, _window, cx| {
editor.edit([(Point::new(4, 0)..Point::new(5, 0), "FIVE\n")], cx);
});
cx.run_until_parked();
leader_cx.assert_editor_state(indoc! {
r#"
ˇone
two
THREE
four
FIVE
six
"#
});
follower_cx.assert_editor_state(indoc! {
r#"
ˇone
two
three
four
five
six
"#
});
leader_cx.update_editor(|editor, _window, cx| {
editor.edit([(Point::new(6, 0)..Point::new(6, 0), "SEVEN")], cx);
});
cx.run_until_parked();
leader_cx.assert_editor_state(indoc! {
r#"
ˇone
two
THREE
four
FIVE
six
SEVEN"#
});
follower_cx.assert_editor_state(indoc! {
r#"
ˇone
two
three
four
five
six
"#
});
leader_cx.update_editor(|editor, window, cx| {
editor.move_down(&MoveDown, window, cx);
editor.refresh_selected_text_highlights(true, window, cx);
});
leader_cx.run_until_parked();
}
#[gpui::test]
async fn test_filtered_editor_pair_complex(cx: &mut gpui::TestAppContext) {
init_test(cx, |_| {});
let base_text = "base\n";
let buffer_text = "buffer\n";
let buffer1 = cx.new(|cx| Buffer::local(buffer_text, cx));
let diff1 = cx.new(|cx| BufferDiff::new_with_base_text(base_text, &buffer1, cx));
let extra_buffer_1 = cx.new(|cx| Buffer::local("dummy text 1\n", cx));
let extra_diff_1 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_1, cx));
let extra_buffer_2 = cx.new(|cx| Buffer::local("dummy text 2\n", cx));
let extra_diff_2 = cx.new(|cx| BufferDiff::new_with_base_text("", &extra_buffer_2, cx));
let leader = cx.new(|cx| {
let mut leader = MultiBuffer::new(Capability::ReadWrite);
leader.set_all_diff_hunks_expanded(cx);
leader.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
leader
});
let follower = leader.update(cx, |leader, cx| leader.get_or_create_follower(cx));
follower.update(cx, |follower, _| {
follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
});
leader.update(cx, |leader, cx| {
leader.insert_excerpts_after(
ExcerptId::min(),
extra_buffer_2.clone(),
vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
cx,
);
leader.add_diff(extra_diff_2.clone(), cx);
leader.insert_excerpts_after(
ExcerptId::min(),
extra_buffer_1.clone(),
vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
cx,
);
leader.add_diff(extra_diff_1.clone(), cx);
leader.insert_excerpts_after(
ExcerptId::min(),
buffer1.clone(),
vec![ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
cx,
);
leader.add_diff(diff1.clone(), cx);
});
cx.run_until_parked();
let mut cx = cx.add_empty_window();
let leader_editor = cx
.new_window_entity(|window, cx| Editor::for_multibuffer(leader.clone(), None, window, cx));
let follower_editor = cx.new_window_entity(|window, cx| {
Editor::for_multibuffer(follower.clone(), None, window, cx)
});
let mut leader_cx = EditorTestContext::for_editor_in(leader_editor.clone(), &mut cx).await;
leader_cx.assert_editor_state(indoc! {"
ˇbuffer
dummy text 1
dummy text 2
"});
let mut follower_cx = EditorTestContext::for_editor_in(follower_editor.clone(), &mut cx).await;
follower_cx.assert_editor_state(indoc! {"
ˇbase
"});
}
#[gpui::test]
async fn test_multibuffer_scroll_cursor_top_margin(cx: &mut TestAppContext) {
init_test(cx, |_| {});

View File

@@ -89,7 +89,7 @@ use text::{BufferId, SelectionGoal};
use theme::{ActiveTheme, Appearance, BufferLineHeight, PlayerColor};
use ui::utils::ensure_minimum_contrast;
use ui::{
ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, prelude::*,
ButtonLike, ContextMenu, Indicator, KeyBinding, POPOVER_Y_PADDING, Tooltip, h_flex, prelude::*,
right_click_menu, scrollbars::ShowScrollbar, text_for_keystroke,
};
use unicode_segmentation::UnicodeSegmentation;
@@ -1227,13 +1227,7 @@ impl EditorElement {
editor.hide_blame_popover(false, cx);
}
} else {
let keyboard_grace = editor
.inline_blame_popover
.as_ref()
.is_some_and(|state| state.keyboard_grace);
if !keyboard_grace {
editor.hide_blame_popover(false, cx);
}
editor.hide_blame_popover(false, cx);
}
let breakpoint_indicator = if gutter_hovered {
@@ -2517,6 +2511,7 @@ impl EditorElement {
scroll_position: gpui::Point<ScrollOffset>,
scroll_pixel_position: gpui::Point<ScrollPixelOffset>,
line_height: Pixels,
text_hitbox: &Hitbox,
window: &mut Window,
cx: &mut App,
) -> Option<InlineBlameLayout> {
@@ -2585,6 +2580,16 @@ impl EditorElement {
let size = element.layout_as_root(AvailableSpace::min_size(), window, cx);
let bounds = Bounds::new(absolute_offset, size);
self.layout_blame_entry_popover(
entry.clone(),
blame,
line_height,
text_hitbox,
row_info.buffer_id?,
window,
cx,
);
element.prepaint_as_root(absolute_offset, AvailableSpace::min_size(), window, cx);
Some(InlineBlameLayout {
@@ -2595,48 +2600,16 @@ impl EditorElement {
})
}
fn layout_blame_popover(
fn layout_blame_entry_popover(
&self,
editor_snapshot: &EditorSnapshot,
text_hitbox: &Hitbox,
blame_entry: BlameEntry,
blame: Entity<GitBlame>,
line_height: Pixels,
text_hitbox: &Hitbox,
buffer: BufferId,
window: &mut Window,
cx: &mut App,
) {
if !self.editor.read(cx).inline_blame_popover.is_some() {
return;
}
let Some(blame) = self.editor.read(cx).blame.clone() else {
return;
};
let cursor_point = self
.editor
.read(cx)
.selections
.newest::<language::Point>(&editor_snapshot.display_snapshot)
.head();
let Some((buffer, buffer_point, _)) = editor_snapshot
.buffer_snapshot()
.point_to_buffer_point(cursor_point)
else {
return;
};
let row_info = RowInfo {
buffer_id: Some(buffer.remote_id()),
buffer_row: Some(buffer_point.row),
..Default::default()
};
let Some((buffer_id, blame_entry)) = blame
.update(cx, |blame, cx| blame.blame_for_rows(&[row_info], cx).next())
.flatten()
else {
return;
};
let Some((popover_state, target_point)) = self.editor.read_with(cx, |editor, _| {
editor
.inline_blame_popover
@@ -2658,7 +2631,7 @@ impl EditorElement {
popover_state.markdown,
workspace,
&blame,
buffer_id,
buffer,
window,
cx,
)
@@ -3301,8 +3274,6 @@ impl EditorElement {
line_number.clear();
let non_relative_number = if relative.wrapped() {
row_info.buffer_row.or(row_info.wrapped_buffer_row)? + 1
} else if self.editor.read(cx).use_base_text_line_numbers {
row_info.base_text_row?.0 + 1
} else {
row_info.buffer_row? + 1
};
@@ -3311,7 +3282,6 @@ impl EditorElement {
&& row_info
.diff_status
.is_some_and(|status| status.is_deleted())
&& !self.editor.read(cx).use_base_text_line_numbers
{
return None;
}
@@ -4058,7 +4028,7 @@ impl EditorElement {
)
.child(
h_flex()
.size_3()
.size(rems_from_px(12.0))
.justify_center()
.flex_shrink_0()
.children(indicator),
@@ -4066,12 +4036,11 @@ impl EditorElement {
.child(
h_flex()
.cursor_pointer()
.id("path_header_block")
.min_w_0()
.id("path header block")
.size_full()
.justify_between()
.overflow_hidden()
.child(h_flex().min_w_0().flex_1().gap_0p5().map(|path_header| {
.child(h_flex().gap_0p5().map(|path_header| {
let filename = filename
.map(SharedString::from)
.unwrap_or_else(|| "untitled".into());
@@ -4081,19 +4050,28 @@ impl EditorElement {
let path = path::Path::new(filename.as_str());
let icon =
FileIcons::get_icon(path, cx).unwrap_or_default();
el.child(Icon::from_path(icon).color(Color::Muted))
let icon = Icon::from_path(icon).color(Color::Muted);
el.child(icon)
})
.child(
ButtonLike::new("filename-button")
.style(ButtonStyle::Subtle)
.child(
Label::new(filename)
.single_line()
.color(file_status_label_color(file_status))
.when(
file_status.is_some_and(|s| s.is_deleted()),
|label| label.strikethrough(),
),
div()
.child(
Label::new(filename)
.single_line()
.color(file_status_label_color(
file_status,
))
.when(
file_status.is_some_and(|s| {
s.is_deleted()
}),
|label| label.strikethrough(),
),
)
.group_hover("", |div| div.underline()),
)
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
@@ -4108,11 +4086,11 @@ impl EditorElement {
})),
)
.when_some(parent_path, |then, path| {
then.child(Label::new(path).truncate().color(
then.child(div().child(path).text_color(
if file_status.is_some_and(FileStatus::is_deleted) {
Color::Custom(colors.text_disabled)
colors.text_disabled
} else {
Color::Custom(colors.text_muted)
colors.text_muted
},
))
})
@@ -4121,13 +4099,18 @@ impl EditorElement {
can_open_excerpts && is_selected && relative_path.is_some(),
|el| {
el.child(
Button::new("open-file-button", "Open File")
ButtonLike::new("open-file-button")
.style(ButtonStyle::OutlinedGhost)
.key_binding(KeyBinding::for_action_in(
&OpenExcerpts,
&focus_handle,
cx,
))
.child(
h_flex()
.gap_2p5()
.child(Label::new("Open file"))
.child(KeyBinding::for_action_in(
&OpenExcerpts,
&focus_handle,
cx,
)),
)
.on_click(window.listener_for(&self.editor, {
let jump_data = jump_data.clone();
move |editor, e: &ClickEvent, window, cx| {
@@ -5599,50 +5582,6 @@ 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>,
@@ -9193,7 +9132,7 @@ impl Element for EditorElement {
);
let end_row = DisplayRow(end_row);
let row_infos = snapshot // note we only get the visual range
let row_infos = snapshot
.row_infos(start_row)
.take((start_row..end_row).len())
.collect::<Vec<RowInfo>>();
@@ -9224,27 +9163,16 @@ 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;
@@ -9282,14 +9210,21 @@ impl Element for EditorElement {
filled_highlight
};
let base_display_point =
DisplayPoint::new(start_row + DisplayRow(ix as u32), 0);
highlighted_rows
.entry(base_display_point.row())
.entry(start_row + DisplayRow(ix as u32))
.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,
@@ -9462,7 +9397,7 @@ impl Element for EditorElement {
let crease_trailers =
window.with_element_namespace("crease_trailers", |window| {
self.layout_crease_trailers(
row_infos.iter().cloned(),
row_infos.iter().copied(),
&snapshot,
window,
cx,
@@ -9478,15 +9413,6 @@ 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
@@ -9840,6 +9766,7 @@ impl Element for EditorElement {
scroll_position,
scroll_pixel_position,
line_height,
&text_hitbox,
window,
cx,
) {
@@ -10037,8 +9964,6 @@ impl Element for EditorElement {
window,
cx,
);
self.layout_blame_popover(&snapshot, &hitbox, line_height, window, cx);
}
let mouse_context_menu = self.layout_mouse_context_menu(

View File

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

View File

@@ -291,7 +291,7 @@ impl Editor {
}),
};
let mut visible_excerpts = self.visible_excerpts(true, cx);
let mut visible_excerpts = self.visible_excerpts(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(true, cx))
.update(cx, |editor, _window, cx| editor.visible_excerpts(cx))
.unwrap();
assert_eq!(
ranges.len(),

View File

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

View File

@@ -164,7 +164,7 @@ impl Editor {
}
let visible_buffers = self
.visible_excerpts(true, cx)
.visible_excerpts(cx)
.into_values()
.map(|(buffer, ..)| buffer)
.filter(|editor_buffer| {

View File

@@ -276,8 +276,7 @@ pub fn deploy_context_menu(
!has_git_repo,
"Copy Permalink",
Box::new(CopyPermalinkToLine),
)
.action_disabled_when(!has_git_repo, "File History", Box::new(git::FileHistory));
);
match focus {
Some(focus) => builder.context(focus),
None => builder,

View File

@@ -1,267 +0,0 @@
use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
use gpui::{
Action, AppContext as _, Entity, EventEmitter, Focusable, NoAction, Subscription, WeakEntity,
};
use multi_buffer::{MultiBuffer, MultiBufferFilterMode};
use project::Project;
use ui::{
App, Context, InteractiveElement as _, IntoElement as _, ParentElement as _, Render,
Styled as _, Window, div,
};
use workspace::{
ActivePaneDecorator, Item, ItemHandle, Pane, PaneGroup, SplitDirection, Workspace,
};
use crate::{Editor, EditorEvent};
struct SplitDiffFeatureFlag;
impl FeatureFlag for SplitDiffFeatureFlag {
const NAME: &'static str = "split-diff";
fn enabled_for_staff() -> bool {
true
}
}
#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
#[action(namespace = editor)]
struct SplitDiff;
#[derive(Clone, Copy, PartialEq, Eq, Action, Default)]
#[action(namespace = editor)]
struct UnsplitDiff;
pub struct SplittableEditor {
primary_editor: Entity<Editor>,
secondary: Option<SecondaryEditor>,
panes: PaneGroup,
workspace: WeakEntity<Workspace>,
_subscriptions: Vec<Subscription>,
}
struct SecondaryEditor {
editor: Entity<Editor>,
pane: Entity<Pane>,
has_latest_selection: bool,
_subscriptions: Vec<Subscription>,
}
impl SplittableEditor {
pub fn primary_editor(&self) -> &Entity<Editor> {
&self.primary_editor
}
pub fn last_selected_editor(&self) -> &Entity<Editor> {
if let Some(secondary) = &self.secondary
&& secondary.has_latest_selection
{
&secondary.editor
} else {
&self.primary_editor
}
}
pub fn new_unsplit(
buffer: Entity<MultiBuffer>,
project: Entity<Project>,
workspace: Entity<Workspace>,
window: &mut Window,
cx: &mut Context<Self>,
) -> Self {
let primary_editor =
cx.new(|cx| Editor::for_multibuffer(buffer, Some(project.clone()), window, cx));
let pane = cx.new(|cx| {
let mut pane = Pane::new(
workspace.downgrade(),
project,
Default::default(),
None,
NoAction.boxed_clone(),
true,
window,
cx,
);
pane.set_should_display_tab_bar(|_, _| false);
pane.add_item(primary_editor.boxed_clone(), true, true, None, window, cx);
pane
});
let panes = PaneGroup::new(pane);
// TODO(split-diff) we might want to tag editor events with whether they came from primary/secondary
let subscriptions =
vec![
cx.subscribe(&primary_editor, |this, _, event: &EditorEvent, cx| {
if let EditorEvent::SelectionsChanged { .. } = event
&& let Some(secondary) = &mut this.secondary
{
secondary.has_latest_selection = false;
}
cx.emit(event.clone())
}),
];
window.defer(cx, {
let workspace = workspace.downgrade();
let primary_editor = primary_editor.downgrade();
move |window, cx| {
workspace
.update(cx, |workspace, cx| {
primary_editor.update(cx, |editor, cx| {
editor.added_to_workspace(workspace, window, cx);
})
})
.ok();
}
});
Self {
primary_editor,
secondary: None,
panes,
workspace: workspace.downgrade(),
_subscriptions: subscriptions,
}
}
fn split(&mut self, _: &SplitDiff, window: &mut Window, cx: &mut Context<Self>) {
if !cx.has_flag::<SplitDiffFeatureFlag>() {
return;
}
if self.secondary.is_some() {
return;
}
let Some(workspace) = self.workspace.upgrade() else {
return;
};
let project = workspace.read(cx).project().clone();
let follower = self.primary_editor.update(cx, |primary, cx| {
primary.buffer().update(cx, |buffer, cx| {
let follower = buffer.get_or_create_follower(cx);
buffer.set_filter_mode(Some(MultiBufferFilterMode::KeepInsertions));
follower
})
});
follower.update(cx, |follower, _| {
follower.set_filter_mode(Some(MultiBufferFilterMode::KeepDeletions));
});
let secondary_editor = workspace.update(cx, |workspace, cx| {
cx.new(|cx| {
let mut editor = Editor::for_multibuffer(follower, Some(project), window, cx);
// TODO(split-diff) this should be at the multibuffer level
editor.set_use_base_text_line_numbers(true, cx);
editor.added_to_workspace(workspace, window, cx);
editor
})
});
let secondary_pane = cx.new(|cx| {
let mut pane = Pane::new(
workspace.downgrade(),
workspace.read(cx).project().clone(),
Default::default(),
None,
NoAction.boxed_clone(),
true,
window,
cx,
);
pane.set_should_display_tab_bar(|_, _| false);
pane.add_item(
ItemHandle::boxed_clone(&secondary_editor),
false,
false,
None,
window,
cx,
);
pane
});
let subscriptions =
vec![
cx.subscribe(&secondary_editor, |this, _, event: &EditorEvent, cx| {
if let EditorEvent::SelectionsChanged { .. } = event
&& let Some(secondary) = &mut this.secondary
{
secondary.has_latest_selection = true;
}
cx.emit(event.clone())
}),
];
self.secondary = Some(SecondaryEditor {
editor: secondary_editor,
pane: secondary_pane.clone(),
has_latest_selection: false,
_subscriptions: subscriptions,
});
let primary_pane = self.panes.first_pane();
self.panes
.split(&primary_pane, &secondary_pane, SplitDirection::Left)
.unwrap();
cx.notify();
}
fn unsplit(&mut self, _: &UnsplitDiff, _: &mut Window, cx: &mut Context<Self>) {
let Some(secondary) = self.secondary.take() else {
return;
};
self.panes.remove(&secondary.pane).unwrap();
self.primary_editor.update(cx, |primary, cx| {
primary.buffer().update(cx, |buffer, _| {
buffer.set_filter_mode(None);
});
});
cx.notify();
}
pub fn added_to_workspace(
&mut self,
workspace: &mut Workspace,
window: &mut Window,
cx: &mut Context<Self>,
) {
self.workspace = workspace.weak_handle();
self.primary_editor.update(cx, |primary_editor, cx| {
primary_editor.added_to_workspace(workspace, window, cx);
});
if let Some(secondary) = &self.secondary {
secondary.editor.update(cx, |secondary_editor, cx| {
secondary_editor.added_to_workspace(workspace, window, cx);
});
}
}
}
impl EventEmitter<EditorEvent> for SplittableEditor {}
impl Focusable for SplittableEditor {
fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
self.primary_editor.read(cx).focus_handle(cx)
}
}
impl Render for SplittableEditor {
fn render(
&mut self,
window: &mut ui::Window,
cx: &mut ui::Context<Self>,
) -> impl ui::IntoElement {
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(inner)
}
}

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